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 private isSublimatedVault = false;22
23 async onload(): Promise<void> {24 await this.loadSettings();25
26 // detect if this vault is a sublimated workspace27 this.isSublimatedVault = await this.checkSublimatedVault();28
29 // status bar30 this.statusBarEl = this.addStatusBarItem();31 this.statusBarEl.addClass("sbl-status-bar");32 this.statusBarEl.addEventListener("click", () =>33 this.openCurrentFileOnWeb()34 );35 this.updateStatusBar(null);36
37 // register version history view38 this.registerView(39 VERSION_VIEW_TYPE,40 (leaf) => new VersionView(leaf, this)41 );42
43 // commands44 this.addCommand({45 id: "open-on-web",46 name: "Open on sublimated.com",47 callback: () => this.openCurrentFileOnWeb(),48 });49
50 this.addCommand({51 id: "show-versions",52 name: "Show versions",53 callback: () => this.activateVersionView(),54 });55
56 this.addCommand({57 id: "copy-web-link",58 name: "Copy web link",59 callback: () => this.copyWebLink(),60 });61
62 // ribbon icon for version sidebar63 this.addRibbonIcon("history", "sublimated versions", () => {64 this.activateVersionView();65 });66
67 // URI handler: obsidian://sublimated?action=open&file=path/to/note.md68 this.registerObsidianProtocolHandler(69 "sublimated",70 (params) => this.handleUri(params)71 );72
73 // settings tab74 this.addSettingTab(new SublimatedSettingTab(this.app, this));75
76 // update status bar when active file changes77 this.registerEvent(78 this.app.workspace.on("active-leaf-change", () => {79 this.refreshMetadata();80 })81 );82
83 // initial metadata read84 this.refreshMetadata();85
86 // welcome modal on first install87 if (!this.settings.hasSeenWelcome) {88 new WelcomeModal(this.app).open();89 this.settings.hasSeenWelcome = true;90 await this.saveSettings();91 }92 }93
94 onunload(): void {95 this.app.workspace.detachLeavesOfType(VERSION_VIEW_TYPE);96 }97
98 async loadSettings(): Promise<void> {99 this.settings = Object.assign(100 {},101 DEFAULT_SETTINGS,102 await this.loadData()103 );104 }105
106 async saveSettings(): Promise<void> {107 await this.saveData(this.settings);108 }109
110 // check for .sbl/ folder in vault root111 private async checkSublimatedVault(): Promise<boolean> {112 const adapter = this.app.vault.adapter;113 return adapter.exists(normalizePath(".sbl"));114 }115
116 detectWorkspace(): { space: string | null } | null {117 if (!this.isSublimatedVault) return null;118 return {119 space: this.lastMetadata?.space ?? null,120 };121 }122
123 private async refreshMetadata(): Promise<void> {124 const file = this.app.workspace.getActiveFile();125 if (!file) {126 this.lastMetadata = null;127 this.updateStatusBar(null);128 return;129 }130
131 if (!isMacOS()) {132 this.lastMetadata = null;133 this.updateStatusBar(null);134 return;135 }136
137 // get absolute path via vault adapter138 const adapter = this.app.vault.adapter;139 const basePath = (adapter as { getBasePath?: () => string })140 .getBasePath?.();141 if (!basePath) {142 this.lastMetadata = null;143 this.updateStatusBar(null);144 return;145 }146
147 const absPath = `${basePath}/${file.path}`;148 this.lastMetadata = await readFileMetadata(absPath);149 this.updateStatusBar(this.lastMetadata);150 }151
152 private updateStatusBar(meta: FileMetadata | null): void {153 if (!this.statusBarEl) return;154
155 if (!this.isSublimatedVault) {156 this.statusBarEl.setText("");157 return;158 }159
160 if (!meta) {161 this.statusBarEl.setText("sublimated");162 return;163 }164
165 const parts: string[] = [];166
167 if (meta.version) {168 parts.push(`v${meta.version}`);169 }170
171 if (meta.syncStatus) {172 const icon = syncStatusIcon(meta.syncStatus);173 parts.push(`${icon} ${meta.syncStatus}`);174 }175
176 if (meta.modifiedBy) {177 const prefix =178 meta.modifiedByType === "ai_agent" ? "by AI:" : "by";179 parts.push(`${prefix} ${meta.modifiedBy}`);180 }181
182 if (meta.branch && meta.branch !== "main") {183 parts.push(meta.branch);184 }185
186 if (meta.visibility) {187 parts.push(visibilityIcon(meta.visibility));188 }189
190 if (parts.length > 0) {191 this.statusBarEl.setText(`sublimated: ${parts.join(" · ")}`);192 } else {193 this.statusBarEl.setText("sublimated");194 }195
196 // show pointer cursor when URL available197 if (meta.url) {198 this.statusBarEl.addClass("sbl-clickable");199 } else {200 this.statusBarEl.removeClass("sbl-clickable");201 }202 }203
204 private openCurrentFileOnWeb(): void {205 const url = this.lastMetadata?.url;206 if (!url) {207 new Notice("No sublimated URL for this file");208 return;209 }210 window.open(`https://${url}`);211 }212
213 private async copyWebLink(): Promise<void> {214 const url = this.lastMetadata?.url;215 if (!url) {216 new Notice("No sublimated URL for this file");217 return;218 }219 await navigator.clipboard.writeText(`https://${url}`);220 new Notice("Link copied to clipboard");221 }222
223 private async activateVersionView(): Promise<void> {224 const existing =225 this.app.workspace.getLeavesOfType(VERSION_VIEW_TYPE);226 if (existing.length > 0) {227 this.app.workspace.revealLeaf(existing[0]);228 return;229 }230
231 const leaf = this.app.workspace.getRightLeaf(false);232 if (!leaf) return;233 await leaf.setViewState({234 type: VERSION_VIEW_TYPE,235 active: true,236 });237 this.app.workspace.revealLeaf(leaf);238 }239
240 private handleUri(params: Record<string, string>): void {241 const { action, file } = params;242
243 if (action === "open" && file) {244 const normalized = normalizePath(file);245 const existing = this.app.vault.getAbstractFileByPath(normalized);246 if (existing) {247 this.app.workspace.openLinkText(normalized, "", false);248 } else {249 new Notice(`File not found: ${file}`);250 }251 return;252 }253
254 if (action === "auth" && params.token) {255 // future: OAuth flow stores token from redirect256 this.settings.apiToken = params.token;257 this.saveSettings();258 new Notice("sublimated: signed in successfully");259 return;260 }261
262 new Notice(`Unknown sublimated action: ${action}`);263 }264}265
266function syncStatusIcon(status: string): string {267 switch (status) {268 case "synced":269 return "✓";270 case "uploading":271 return "↑";272 case "downloading":273 return "↓";274 case "error":275 return "!";276 case "conflict":277 return "⚠";278 default:279 return "";280 }281}282
283function visibilityIcon(visibility: string): string {284 switch (visibility) {285 case "public":286 return "🌐";287 case "shared":288 return "👥";289 case "private":290 return "🔒";291 default:292 return "";293 }294} v3 of v8
commit 25463ef
message edit main.ts via macOS
author anton
date Apr 8, 2026
size 6.7 KB