7d81d8d antonApr 8, 2026 api

create main.ts via macOS

A /sublimated/undefined +244 -0
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 workspace
27 this.isSublimatedVault = await this.checkSublimatedVault();
28
29 // status bar
30 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 view
38 this.registerView(
39 VERSION_VIEW_TYPE,
40 (leaf) => new VersionView(leaf, this)
41 );
42
43 // commands
44 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 sidebar
63 this.addRibbonIcon("history", "sublimated versions", () => {
64 this.activateVersionView();
65 });
66
67 // URI handler: obsidian://sublimated?action=open&file=path/to/note.md
68 this.registerObsidianProtocolHandler(
69 "sublimated",
70 (params) => this.handleUri(params)
71 );
72
73 // settings tab
74 this.addSettingTab(new SublimatedSettingTab(this.app, this));
75
76 // update status bar when active file changes
77 this.registerEvent(
78 this.app.workspace.on("active-leaf-change", () => {
79 this.refreshMetadata();
80 })
81 );
82
83 // initial metadata read
84 this.refreshMetadata();
85
86 // welcome modal on first install
87 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 root
111 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 adapter
138 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 if (meta.version) parts.push(`v${meta.version}`);
167 if (meta.syncStatus) parts.push(meta.syncStatus);
168 if (meta.modifiedBy) parts.push(`by ${meta.modifiedBy}`);
169
170 if (parts.length > 0) {
171 this.statusBarEl.setText(`sublimated: ${parts.join(" · ")}`);
172 } else {
173 this.statusBarEl.setText("sublimated");
174 }
175
176 // show pointer cursor when URL available
177 if (meta.url) {
178 this.statusBarEl.addClass("sbl-clickable");
179 } else {
180 this.statusBarEl.removeClass("sbl-clickable");
181 }
182 }
183
184 private openCurrentFileOnWeb(): void {
185 const url = this.lastMetadata?.url;
186 if (!url) {
187 new Notice("No sublimated URL for this file");
188 return;
189 }
190 window.open(`https://${url}`);
191 }
192
193 private async copyWebLink(): Promise<void> {
194 const url = this.lastMetadata?.url;
195 if (!url) {
196 new Notice("No sublimated URL for this file");
197 return;
198 }
199 await navigator.clipboard.writeText(`https://${url}`);
200 new Notice("Link copied to clipboard");
201 }
202
203 private async activateVersionView(): Promise<void> {
204 const existing =
205 this.app.workspace.getLeavesOfType(VERSION_VIEW_TYPE);
206 if (existing.length > 0) {
207 this.app.workspace.revealLeaf(existing[0]);
208 return;
209 }
210
211 const leaf = this.app.workspace.getRightLeaf(false);
212 if (!leaf) return;
213 await leaf.setViewState({
214 type: VERSION_VIEW_TYPE,
215 active: true,
216 });
217 this.app.workspace.revealLeaf(leaf);
218 }
219
220 private handleUri(params: Record<string, string>): void {
221 const { action, file } = params;
222
223 if (action === "open" && file) {
224 const normalized = normalizePath(file);
225 const existing = this.app.vault.getAbstractFileByPath(normalized);
226 if (existing) {
227 this.app.workspace.openLinkText(normalized, "", false);
228 } else {
229 new Notice(`File not found: ${file}`);
230 }
231 return;
232 }
233
234 if (action === "auth" && params.token) {
235 // future: OAuth flow stores token from redirect
236 this.settings.apiToken = params.token;
237 this.saveSettings();
238 new Notice("sublimated: signed in successfully");
239 return;
240 }
241
242 new Notice(`Unknown sublimated action: ${action}`);
243 }
244}