From 6865c049a3155881e55b169ae41f5e3a59dbf699 Mon Sep 17 00:00:00 2001 From: Nystik <236107-Nystik@users.noreply.gitlab.com> Date: Thu, 2 Apr 2026 02:47:18 +0200 Subject: [PATCH] improve workspaces handling --- src/shims/fs/transport.js | 48 +++++----- src/shims/init.js | 3 + src/shims/workspace.js | 188 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 217 insertions(+), 22 deletions(-) create mode 100644 src/shims/workspace.js diff --git a/src/shims/fs/transport.js b/src/shims/fs/transport.js index 7e5638b..63ded46 100644 --- a/src/shims/fs/transport.js +++ b/src/shims/fs/transport.js @@ -1,4 +1,10 @@ +import { + rewriteWorkspacePath, + rewriteWorkspacesContent, +} from "../workspace.js"; + const API_BASE = "/api/fs"; +const WORKSPACES_PATH = ".obsidian/workspaces.json"; function normPath(p) { return (p || "").replace(/^\/+/, ""); @@ -19,22 +25,6 @@ function vaultId() { return window.__currentVaultId || ""; } -const WORKSPACE_PATH = ".obsidian/workspace.json"; - -function rewriteWorkspacePath(normalizedPath) { - const name = window.__workspaceName; - - if (!name) { - return normalizedPath; - } - - if (normalizedPath === WORKSPACE_PATH) { - return `.obsidian/workspace.${name}.json`; - } - - return normalizedPath; -} - async function request(method, endpoint, params = {}) { const url = new URL(API_BASE + endpoint, window.location.origin); @@ -156,10 +146,17 @@ export const transport = { }, async writeFile(path, content, encoding) { - const isText = typeof content === "string"; + const norm = normPath(path); + let data = content; + + if (norm === WORKSPACES_PATH && typeof data === "string") { + data = rewriteWorkspacesContent(data); + } + + const isText = typeof data === "string"; return requestJson("POST", "/writeFile", { - path: rewriteWorkspacePath(normPath(path)), - content: isText ? content : uint8ToBase64(content), + path: rewriteWorkspacePath(norm), + content: isText ? data : uint8ToBase64(data), encoding: encoding || (isText ? "utf-8" : "binary"), base64: !isText, }); @@ -261,10 +258,17 @@ export const transport = { }, writeFileSync(path, content, encoding) { - const isText = typeof content === "string"; + const norm = normPath(path); + let data = content; + + if (norm === WORKSPACES_PATH && typeof data === "string") { + data = rewriteWorkspacesContent(data); + } + + const isText = typeof data === "string"; requestSync("POST", "/writeFile", { - path: rewriteWorkspacePath(normPath(path)), - content: isText ? content : uint8ToBase64(content), + path: rewriteWorkspacePath(norm), + content: isText ? data : uint8ToBase64(data), encoding: encoding || (isText ? "utf-8" : "binary"), base64: !isText, }); diff --git a/src/shims/init.js b/src/shims/init.js index 05df3d5..1ef697b 100644 --- a/src/shims/init.js +++ b/src/shims/init.js @@ -3,6 +3,7 @@ import { installRequestUrlShim } from "./request-url.js"; import { vaultService } from "../services/vault-service.js"; import { showPluginInstallDialog } from "../ui/bootstrap.js"; import { registerReadTransform } from "./fs/read-transforms.js"; +import { resolveWorkspaceName, initWorkspacePatch } from "./workspace.js"; function resolveVaultId() { const urlParams = new URLSearchParams(window.location.search); @@ -171,9 +172,11 @@ function initCoreSyncGuard() { export function initialize() { resolveVaultId(); initVaultConfig(); + resolveWorkspaceName(); initVaultList(); initMetadataCache(); initCoreSyncGuard(); installRequestUrlShim(); + initWorkspacePatch(); initPluginPrompt(); } diff --git a/src/shims/workspace.js b/src/shims/workspace.js new file mode 100644 index 0000000..0b30c31 --- /dev/null +++ b/src/shims/workspace.js @@ -0,0 +1,188 @@ +import { fsShim } from "./fs/index.js"; +import { registerReadTransform } from "./fs/read-transforms.js"; + +const WORKSPACE_PATH = ".obsidian/workspace.json"; +const WORKSPACES_PATH = ".obsidian/workspaces.json"; + +export function rewriteWorkspacePath(normalizedPath) { + const name = window.__workspaceName; + + if (!name) { + return normalizedPath; + } + + if (normalizedPath === WORKSPACE_PATH) { + return `.obsidian/workspace.${name}.json`; + } + + return normalizedPath; +} + +export function rewriteWorkspacesContent(content) { + const original = window.__originalActiveWorkspace; + + if (!original || !window.__workspaceName) { + return content; + } + + try { + const parsed = JSON.parse(content); + + if (parsed.active !== original) { + parsed.active = original; + return JSON.stringify(parsed); + } + } catch {} + + return content; +} + +function setWorkspaceParam(name) { + const url = new URL(window.location.href); + + if (name) { + url.searchParams.set("workspace", name); + } else { + url.searchParams.delete("workspace"); + } + + history.replaceState(null, "", url.toString()); +} + +export function resolveWorkspaceName() { + try { + const vaultParam = window.__currentVaultId + ? "?vault=" + encodeURIComponent(window.__currentVaultId) + : ""; + + const sep = vaultParam ? "&" : "?"; + + // If no param provided, check if workspaces plugin is enabled before resolving. + if (!window.__workspaceName) { + const coreXhr = new XMLHttpRequest(); + + coreXhr.open( + "GET", + "/api/fs/readFile" + vaultParam + sep + "path=.obsidian/core-plugins.json&encoding=utf-8", + false, + ); + coreXhr.send(); + + if (coreXhr.status !== 200) { + return; + } + + const corePlugins = JSON.parse(coreXhr.responseText); + + if (!corePlugins.workspaces) { + return; + } + } + + // Read workspaces.json to get the active field. + const xhr = new XMLHttpRequest(); + + xhr.open( + "GET", + "/api/fs/readFile" + vaultParam + sep + "path=.obsidian/workspaces.json&encoding=utf-8", + false, + ); + xhr.send(); + + if (xhr.status !== 200) { + return; + } + + const workspaces = JSON.parse(xhr.responseText); + + // Always store the original active value for the write transform. + if (workspaces.active) { + window.__originalActiveWorkspace = workspaces.active; + } + + // If no param was provided, seed from the active workspace. + if (!window.__workspaceName && workspaces.active) { + window.__workspaceName = workspaces.active; + setWorkspaceParam(workspaces.active); + console.log("[ignis] Workspace resolved from active:", workspaces.active); + } + } catch (e) { + console.warn("[ignis] Failed to resolve workspace name:", e); + } +} + +export function initWorkspacePatch() { + const observer = new MutationObserver(() => { + if (!document.querySelector(".workspace")) { + return; + } + + const plugin = + window.app && + window.app.internalPlugins && + window.app.internalPlugins.plugins && + window.app.internalPlugins.plugins.workspaces; + + if (!plugin || !plugin.enabled || !plugin.instance) { + return; + } + + observer.disconnect(); + + const instance = plugin.instance; + const origLoad = instance.loadWorkspace.bind(instance); + const origSave = instance.saveWorkspace.bind(instance); + + instance.loadWorkspace = function (name) { + window.__workspaceName = name; + setWorkspaceParam(name); + fsShim._contentCache.invalidate(".obsidian/workspace.json"); + return origLoad(name); + }; + + instance.saveWorkspace = function (name) { + // Grab the current layout before switching the transport target. + const currentLayout = fsShim._contentCache.get(".obsidian/workspace.json"); + + window.__workspaceName = name; + setWorkspaceParam(name); + fsShim._contentCache.invalidate(".obsidian/workspace.json"); + const result = origSave(name); + + // Write the layout to the new workspace file so it exists on disk immediately. + if (currentLayout) { + fsShim.writeFileSync(".obsidian/workspace.json", currentLayout, "utf-8"); + } + + return result; + }; + + // Override the active field on reads so the menu matches this tab's workspace. + registerReadTransform(".obsidian/workspaces.json", (data) => { + if (!window.__workspaceName) { + return data; + } + + let text = + typeof data === "string" ? data : new TextDecoder().decode(data); + + try { + const parsed = JSON.parse(text); + + if (parsed.active !== window.__workspaceName) { + parsed.active = window.__workspaceName; + return JSON.stringify(parsed); + } + } catch {} + + return data; + }); + + console.log("[ignis] Workspaces plugin patched, workspace:", window.__workspaceName || "(none)"); + }); + + observer.observe(document.documentElement, { + childList: true, + subtree: true, + }); +}