1import {2 Notice,3 Plugin,4 WorkspaceLeaf,5 normalizePath,6} from "obsidian";7import {8 type SublimatedSettings,9 DEFAULT_SETTINGS,10 SublimatedSettingTab,11} from "./settings";12import { type FileMetadata, readFileMetadata, isMacOS } from "./xattr";13import { VersionView, VERSION_VIEW_TYPE } from "./version-view";14import { WelcomeModal } from "./welcome-modal";15
16export default class SublimatedPlugin extends Plugin {17 settings: SublimatedSettings = DEFAULT_SETTINGS;18 lastMetadata: FileMetadata | null = null;19
20 private statusBarEl: HTMLElement | null = null;21
22 async onload(): Promise<void> {23 await this.loadSettings();24
25 // status bar26 this.statusBarEl = this.addStatusBarItem();27 this.statusBarEl.addClass("sbl-status-bar");28 this.statusBarEl.addEventListener("click", () =>29 this.openCurrentFileOnWeb()30 );31 this.updateStatusBar(null);32
33 // register version history view34 this.registerView(35 VERSION_VIEW_TYPE,36 (leaf) => new VersionView(leaf, this)37 );38
39 // commands40 this.addCommand({41 id: "open-on-web",42 name: "Open on sublimated.com",43 callback: () => this.openCurrentFileOnWeb(),44 });45
46 this.addCommand({47 id: "show-versions",48 name: "Show versions",49 callback: () => this.activateVersionView(),50 });51
52 this.addCommand({53 id: "copy-web-link",54 name: "Copy web link",55 callback: () => this.copyWebLink(),56 });57
58 // ribbon icon for version sidebar59 this.addRibbonIcon("history", "sublimated versions", () => {60 this.activateVersionView();61 });62
63 // URI handler: obsidian://sublimated?action=open&file=path/to/note.md64 this.registerObsidianProtocolHandler(65 "sublimated",66 (params) => this.handleUri(params)67 );68
69 // settings tab70 this.addSettingTab(new SublimatedSettingTab(this.app, this));71
72 // update status bar when active file changes73 this.registerEvent(74 this.app.workspace.on("active-leaf-change", () => {75 this.refreshMetadata();76 })77 );78
79 // refresh metadata after file modifications (xattrs update on sync)80 this.registerEvent(81 this.app.vault.on("modify", (file) => {82 const active = this.app.workspace.getActiveFile();83 if (active && file.path === active.path) {84 this.refreshAfterModify();85 }86 })87 );88
89 // initial metadata read90 this.refreshMetadata();91
92 // welcome modal on first install93 if (!this.settings.hasSeenWelcome) {94 new WelcomeModal(this.app).open();95 this.settings.hasSeenWelcome = true;96 await this.saveSettings();97 }98 }99
100 onunload(): void {101 this.app.workspace.detachLeavesOfType(VERSION_VIEW_TYPE);102 }103
104 async loadSettings(): Promise<void> {105 this.settings = Object.assign(106 {},107 DEFAULT_SETTINGS,108 await this.loadData()109 );110 }111
112 async saveSettings(): Promise<void> {113 await this.saveData(this.settings);114 }115
116 // detect workspace from xattrs on the currently active file.117 // every synced file carries com.sublimated.space#PS, so any file118 // with sublimated metadata identifies the workspace.119 detectWorkspace(): { space: string | null } | null {120 if (!this.lastMetadata) return null;121 if (!this.lastMetadata.space && !this.lastMetadata.id) return null;122 return {123 space: this.lastMetadata.space ?? null,124 };125 }126
127 private async refreshMetadata(): Promise<void> {128 const file = this.app.workspace.getActiveFile();129 if (!file) {130 this.lastMetadata = null;131 this.updateStatusBar(null);132 return;133 }134
135 if (!isMacOS()) {136 this.lastMetadata = null;137 this.updateStatusBar(null);138 return;139 }140
141 // get absolute path via vault adapter142 const adapter = this.app.vault.adapter;143 const basePath = (adapter as { getBasePath?: () => string })144 .getBasePath?.();145 if (!basePath) {146 this.lastMetadata = null;147 this.updateStatusBar(null);148 return;149 }150
151 const absPath = `${basePath}/${file.path}`;152 this.lastMetadata = await readFileMetadata(absPath);153 this.updateStatusBar(this.lastMetadata);154 }155
156 private updateStatusBar(meta: FileMetadata | null): void {157 if (!this.statusBarEl) return;158
159 // no sublimated xattrs → file isn't tracked by sublimated, hide status bar160 if (!meta || (!meta.space && !meta.id)) {161 this.statusBarEl.setText("");162 return;163 }164
165 // compact format: "✓ synced v21 by anton" (sync + version + author + optional branch/visibility)166 const parts: string[] = [];167
168 if (meta.syncStatus) {169 const icon = syncStatusIcon(meta.syncStatus);170 parts.push(icon ? `${icon} ${meta.syncStatus}` : meta.syncStatus);171 }172
173 if (meta.version) {174 parts.push(`v${meta.version}`);175 }176
177 if (meta.modifiedBy) {178 const prefix =179 meta.modifiedByType === "ai_agent" ? "by AI:" : "by";180 parts.push(`${prefix} ${meta.modifiedBy}`);181 }182
183 if (meta.branch && meta.branch !== "main") {184 parts.push(meta.branch);185 }186
187 if (meta.visibility) {188 parts.push(visibilityIcon(meta.visibility));189 }190
191 this.statusBarEl.setText(parts.join(" "));192
193 // show pointer cursor when URL available194 if (meta.url) {195 this.statusBarEl.addClass("sbl-clickable");196 } else {197 this.statusBarEl.removeClass("sbl-clickable");198 }199 }200
201 private openCurrentFileOnWeb(): void {202 const url = this.lastMetadata?.url;203 if (!url) {204 new Notice("No sublimated URL for this file");205 return;206 }207 window.open(`https://${url}`);208 }209
210 private async copyWebLink(): Promise<void> {211 const url = this.lastMetadata?.url;212 if (!url) {213 new Notice("No sublimated URL for this file");214 return;215 }216 await navigator.clipboard.writeText(`https://${url}`);217 new Notice("Link copied to clipboard");218 }219
220 private async activateVersionView(): Promise<void> {221 const existing =222 this.app.workspace.getLeavesOfType(VERSION_VIEW_TYPE);223 if (existing.length > 0) {224 this.app.workspace.revealLeaf(existing[0]);225 return;226 }227
228 const leaf = this.app.workspace.getRightLeaf(false);229 if (!leaf) return;230 await leaf.setViewState({231 type: VERSION_VIEW_TYPE,232 active: true,233 });234 this.app.workspace.revealLeaf(leaf);235 }236
237 private handleUri(params: Record<string, string>): void {238 const { action, file } = params;239
240 if (action === "open" && file) {241 const normalized = normalizePath(file);242 const existing = this.app.vault.getAbstractFileByPath(normalized);243 if (existing) {244 this.app.workspace.openLinkText(normalized, "", false);245 } else {246 new Notice(`File not found: ${file}`);247 }248 return;249 }250
251 if (action === "auth" && params.token) {252 // future: OAuth flow stores token from redirect253 this.settings.apiToken = params.token;254 this.saveSettings();255 new Notice("sublimated: signed in successfully");256 return;257 }258
259 new Notice(`Unknown sublimated action: ${action}`);260 }261}262
263function syncStatusIcon(status: string): string {264 switch (status) {265 case "synced":266 return "✓";267 case "uploading":268 return "↑";269 case "downloading":270 return "↓";271 case "error":272 return "!";273 case "conflict":274 return "⚠";275 default:276 return "";277 }278}279
280function visibilityIcon(visibility: string): string {281 switch (visibility) {282 case "public":283 return "🌐";284 case "shared":285 return "👥";286 case "private":287 return "🔒";288 default:289 return "";290 }291} v6 of v8
commit 96f31db
message edit main.ts via macOS
author anton
date Apr 14, 2026
size 6.9 KB