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 // initial metadata read80 this.refreshMetadata();81
82 // welcome modal on first install83 if (!this.settings.hasSeenWelcome) {84 new WelcomeModal(this.app).open();85 this.settings.hasSeenWelcome = true;86 await this.saveSettings();87 }88 }89
90 onunload(): void {91 this.app.workspace.detachLeavesOfType(VERSION_VIEW_TYPE);92 }93
94 async loadSettings(): Promise<void> {95 this.settings = Object.assign(96 {},97 DEFAULT_SETTINGS,98 await this.loadData()99 );100 }101
102 async saveSettings(): Promise<void> {103 await this.saveData(this.settings);104 }105
106 // detect workspace from xattrs on the currently active file.107 // every synced file carries com.sublimated.space#PS, so any file108 // with sublimated metadata identifies the workspace.109 detectWorkspace(): { space: string | null } | null {110 if (!this.lastMetadata) return null;111 if (!this.lastMetadata.space && !this.lastMetadata.id) return null;112 return {113 space: this.lastMetadata.space ?? null,114 };115 }116
117 private async refreshMetadata(): Promise<void> {118 const file = this.app.workspace.getActiveFile();119 if (!file) {120 this.lastMetadata = null;121 this.updateStatusBar(null);122 return;123 }124
125 if (!isMacOS()) {126 this.lastMetadata = null;127 this.updateStatusBar(null);128 return;129 }130
131 // get absolute path via vault adapter132 const adapter = this.app.vault.adapter;133 const basePath = (adapter as { getBasePath?: () => string })134 .getBasePath?.();135 if (!basePath) {136 this.lastMetadata = null;137 this.updateStatusBar(null);138 return;139 }140
141 const absPath = `${basePath}/${file.path}`;142 this.lastMetadata = await readFileMetadata(absPath);143 this.updateStatusBar(this.lastMetadata);144 }145
146 private updateStatusBar(meta: FileMetadata | null): void {147 if (!this.statusBarEl) return;148
149 // no sublimated xattrs → file isn't tracked by sublimated, hide status bar150 if (!meta || (!meta.space && !meta.id)) {151 this.statusBarEl.setText("");152 return;153 }154
155 // compact format: "✓ synced v21 by anton" (sync + version + author + optional branch/visibility)156 const parts: string[] = [];157
158 if (meta.syncStatus) {159 const icon = syncStatusIcon(meta.syncStatus);160 parts.push(icon ? `${icon} ${meta.syncStatus}` : meta.syncStatus);161 }162
163 if (meta.version) {164 parts.push(`v${meta.version}`);165 }166
167 if (meta.modifiedBy) {168 const prefix =169 meta.modifiedByType === "ai_agent" ? "by AI:" : "by";170 parts.push(`${prefix} ${meta.modifiedBy}`);171 }172
173 if (meta.branch && meta.branch !== "main") {174 parts.push(meta.branch);175 }176
177 if (meta.visibility) {178 parts.push(visibilityIcon(meta.visibility));179 }180
181 this.statusBarEl.setText(parts.join(" "));182
183 // show pointer cursor when URL available184 if (meta.url) {185 this.statusBarEl.addClass("sbl-clickable");186 } else {187 this.statusBarEl.removeClass("sbl-clickable");188 }189 }190
191 private openCurrentFileOnWeb(): void {192 const url = this.lastMetadata?.url;193 if (!url) {194 new Notice("No sublimated URL for this file");195 return;196 }197 window.open(`https://${url}`);198 }199
200 private async copyWebLink(): Promise<void> {201 const url = this.lastMetadata?.url;202 if (!url) {203 new Notice("No sublimated URL for this file");204 return;205 }206 await navigator.clipboard.writeText(`https://${url}`);207 new Notice("Link copied to clipboard");208 }209
210 private async activateVersionView(): Promise<void> {211 const existing =212 this.app.workspace.getLeavesOfType(VERSION_VIEW_TYPE);213 if (existing.length > 0) {214 this.app.workspace.revealLeaf(existing[0]);215 return;216 }217
218 const leaf = this.app.workspace.getRightLeaf(false);219 if (!leaf) return;220 await leaf.setViewState({221 type: VERSION_VIEW_TYPE,222 active: true,223 });224 this.app.workspace.revealLeaf(leaf);225 }226
227 private handleUri(params: Record<string, string>): void {228 const { action, file } = params;229
230 if (action === "open" && file) {231 const normalized = normalizePath(file);232 const existing = this.app.vault.getAbstractFileByPath(normalized);233 if (existing) {234 this.app.workspace.openLinkText(normalized, "", false);235 } else {236 new Notice(`File not found: ${file}`);237 }238 return;239 }240
241 if (action === "auth" && params.token) {242 // future: OAuth flow stores token from redirect243 this.settings.apiToken = params.token;244 this.saveSettings();245 new Notice("sublimated: signed in successfully");246 return;247 }248
249 new Notice(`Unknown sublimated action: ${action}`);250 }251}252
253function syncStatusIcon(status: string): string {254 switch (status) {255 case "synced":256 return "✓";257 case "uploading":258 return "↑";259 case "downloading":260 return "↓";261 case "error":262 return "!";263 case "conflict":264 return "⚠";265 default:266 return "";267 }268}269
270function visibilityIcon(visibility: string): string {271 switch (visibility) {272 case "public":273 return "🌐";274 case "shared":275 return "👥";276 case "private":277 return "🔒";278 default:279 return "";280 }281} v4 of v8
commit c2533b8
message edit main.ts via macOS
author anton
date Apr 11, 2026
size 6.6 KB