expose Ignis API, implement shared ws client

This commit is contained in:
Nystik
2026-05-24 21:51:02 +02:00
parent 9eeff3c1b3
commit 28effab1ed
29 changed files with 824 additions and 745 deletions

View File

@@ -95,64 +95,44 @@ function display(containerEl, app) {
addServerStatus(containerEl);
}
function getWsStatus() {
const ws = window.__ignisWs;
const STATUS_LABELS = {
open: "Connected",
connecting: "Connecting...",
closed: "Disconnected",
};
if (!ws) {
return "disconnected";
}
switch (ws.readyState) {
case WebSocket.CONNECTING:
return "connecting";
case WebSocket.OPEN:
return "connected";
case WebSocket.CLOSING:
case WebSocket.CLOSED:
return "disconnected";
default:
return "disconnected";
}
}
function statusLabel(status) {
switch (status) {
case "connected":
return "Connected";
case "connecting":
return "Connecting...";
case "disconnected":
return "Disconnected";
default:
return "Unknown";
}
}
const STATUS_DOT_CLASSES = {
open: "ignis-status-connected",
connecting: "ignis-status-connecting",
closed: "ignis-status-disconnected",
};
function addServerStatus(containerEl) {
const status = getWsStatus();
const ws = window.__ignis.ws;
const setting = new Setting(containerEl).setName("Server status");
const dotEl = setting.controlEl.createEl("span", {
cls: `ignis-status-dot ignis-status-${status}`,
cls: "ignis-status-dot",
});
const labelEl = setting.controlEl.createEl("span", {
text: statusLabel(status),
cls: "ignis-status-label",
});
const update = () => {
const s = getWsStatus();
dotEl.className = `ignis-status-dot ignis-status-${s}`;
labelEl.textContent = statusLabel(s);
};
function render(state) {
dotEl.className = `ignis-status-dot ${STATUS_DOT_CLASSES[state] || STATUS_DOT_CLASSES.closed}`;
labelEl.textContent = STATUS_LABELS[state] || STATUS_LABELS.closed;
}
const pollInterval = setInterval(update, 3000);
render(ws.isOpen() ? "open" : "closed");
const unsub = ws.onStateChange(render);
// Detach when the settings tab DOM goes away.
const observer = new MutationObserver(() => {
if (!containerEl.isConnected) {
clearInterval(pollInterval);
unsub();
observer.disconnect();
}
});

View File

@@ -2,6 +2,7 @@ const generalTab = require("./general-tab");
const serverPluginsTab = require("./server-plugins-tab");
const { createNavEl, createTab, createGroup } = require("./settings-ui");
const {
allIgnisNavEls,
setupPluginTabs,
reconcilePluginTabs,
hideIgnisFromCommunityPlugins,
@@ -24,10 +25,6 @@ function removeExistingIgnisGroups(tabHeadersEl) {
}
}
// All ignis-managed nav elements (both Ignis group and Ignis Core Plugins group).
// Collected here so the openTab patch can manage is-active across all of them.
const allIgnisNavEls = new Map(); // tab id -> nav element
function replaceInstallerVersionRow(setting, ignisVersion) {
const container = setting.tabContentContainer || setting.contentEl;
@@ -117,7 +114,7 @@ function injectIgnisSettings(setting, app, plugin) {
setting.tabHeadersEl.appendChild(corePlugins.group);
hideIgnisFromCommunityPlugins(setting);
setupPluginTabs(setting, corePlugins.items, allIgnisNavEls);
setupPluginTabs(setting, corePlugins.items);
}
function patchSettingsModal(plugin) {
@@ -142,7 +139,4 @@ function unpatchSettingsModal(plugin) {
clearOwnedPluginIds();
}
window.__ignisReconcilePluginTabs = (setting) =>
reconcilePluginTabs(setting, allIgnisNavEls);
module.exports = { patchSettingsModal, unpatchSettingsModal, reconcilePluginTabs };

View File

@@ -2,10 +2,14 @@ const { setIcon } = require("obsidian");
const { findGroupByTitle } = require("./settings-ui");
const { isIgnisPlugin } = require("../plugin-registry");
// All ignis-managed nav elements (both Ignis group and Ignis Core Plugins group).
// Shared with inject.js so the openTab patch can manage is-active across all of them.
const allIgnisNavEls = new Map(); // tab id -> nav element
// Tracks which plugin IDs have nav items we created.
const ownedPluginIds = new Set();
function addPluginNavItem(pluginId, setting, corePluginsItems, ignisNavEls) {
function addPluginNavItem(pluginId, setting, corePluginsItems) {
const tab = setting.pluginTabs.find((t) => t.id === pluginId);
if (!tab) {
@@ -41,16 +45,16 @@ function addPluginNavItem(pluginId, setting, corePluginsItems, ignisNavEls) {
corePluginsItems.appendChild(nav);
ownedPluginIds.add(pluginId);
ignisNavEls.set(pluginId, nav);
allIgnisNavEls.set(pluginId, nav);
}
function removePluginNavItem(pluginId, ignisNavEls) {
const nav = ignisNavEls.get(pluginId);
function removePluginNavItem(pluginId) {
const nav = allIgnisNavEls.get(pluginId);
if (nav && ownedPluginIds.has(pluginId)) {
nav.remove();
ownedPluginIds.delete(pluginId);
ignisNavEls.delete(pluginId);
allIgnisNavEls.delete(pluginId);
}
}
@@ -116,11 +120,11 @@ function hideIgnisNavFromCommunityGroup(setting) {
communityGroup.style.display = hasVisible ? "" : "none";
}
function hideCorePluginsGroupIfEmpty(ignisNavEls) {
function hideCorePluginsGroupIfEmpty() {
let hasConnected = false;
for (const id of ownedPluginIds) {
const nav = ignisNavEls.get(id);
const nav = allIgnisNavEls.get(id);
if (nav?.isConnected) {
hasConnected = true;
@@ -140,15 +144,15 @@ function hideCorePluginsGroupIfEmpty(ignisNavEls) {
}
}
function setupPluginTabs(setting, corePluginsItems, ignisNavEls) {
function setupPluginTabs(setting, corePluginsItems) {
for (const tab of setting.pluginTabs) {
if (isIgnisPlugin(tab.id) && tab.id !== "ignis-bridge") {
addPluginNavItem(tab.id, setting, corePluginsItems, ignisNavEls);
addPluginNavItem(tab.id, setting, corePluginsItems);
}
}
hideIgnisNavFromCommunityGroup(setting);
hideCorePluginsGroupIfEmpty(ignisNavEls);
hideCorePluginsGroupIfEmpty();
const communityGroup = findGroupByTitle(
setting.tabHeadersEl,
@@ -159,12 +163,12 @@ function setupPluginTabs(setting, corePluginsItems, ignisNavEls) {
const observer = new MutationObserver(() => {
for (const tab of setting.pluginTabs) {
if (isIgnisPlugin(tab.id) && tab.id !== "ignis-bridge") {
addPluginNavItem(tab.id, setting, corePluginsItems, ignisNavEls);
addPluginNavItem(tab.id, setting, corePluginsItems);
}
}
hideIgnisNavFromCommunityGroup(setting);
hideCorePluginsGroupIfEmpty(ignisNavEls);
hideCorePluginsGroupIfEmpty();
});
observer.observe(communityGroup, { childList: true, subtree: true });
@@ -186,7 +190,7 @@ function setupPluginTabs(setting, corePluginsItems, ignisNavEls) {
}
}
function reconcilePluginTabs(setting, ignisNavEls) {
function reconcilePluginTabs(setting) {
const corePluginsGroup = findGroupByTitle(
setting.tabHeadersEl,
"Ignis Core Plugins",
@@ -212,16 +216,16 @@ function reconcilePluginTabs(setting, ignisNavEls) {
for (const id of ownedPluginIds) {
if (!activeIds.has(id)) {
removePluginNavItem(id, ignisNavEls);
removePluginNavItem(id);
}
}
for (const id of activeIds) {
addPluginNavItem(id, setting, corePluginsItems, ignisNavEls);
addPluginNavItem(id, setting, corePluginsItems);
}
hideIgnisNavFromCommunityGroup(setting);
hideCorePluginsGroupIfEmpty(ignisNavEls);
hideCorePluginsGroupIfEmpty();
}
function clearOwnedPluginIds() {
@@ -229,6 +233,7 @@ function clearOwnedPluginIds() {
}
module.exports = {
allIgnisNavEls,
setupPluginTabs,
reconcilePluginTabs,
hideIgnisFromCommunityPlugins,

View File

@@ -1,18 +1,10 @@
const { Setting, Notice } = require("obsidian");
const { reconcilePluginTabs } = require("./plugin-tabs");
function getVaultId() {
return window.__currentVaultId || "";
}
async function refreshPluginCache(bundledPluginId) {
const pluginPath = `.obsidian/plugins/${bundledPluginId}`;
const fs = require("fs");
if (fs._refreshSubtree) {
await fs._refreshSubtree(pluginPath);
}
}
async function fetchPlugins() {
const res = await fetch("/api/plugins");
@@ -23,7 +15,7 @@ async function fetchPlugins() {
return res.json();
}
async function togglePlugin(pluginId, enable, app) {
async function togglePlugin(pluginId, enable) {
const action = enable ? "enable" : "disable";
const vaultId = getVaultId();
@@ -41,25 +33,10 @@ async function togglePlugin(pluginId, enable, app) {
return res.json();
}
async function activateBundledPlugin(bundledPluginId, enable, app) {
if (!bundledPluginId) {
return;
}
const plugins = app.plugins;
if (enable) {
await plugins.loadManifests();
await plugins.enablePluginAndSave(bundledPluginId);
} else {
await plugins.disablePluginAndSave(bundledPluginId);
}
}
function display(containerEl, app) {
containerEl.createEl("h2", { text: "Ignis Core Plugins" });
const descEl = containerEl.createEl("p", {
containerEl.createEl("p", {
text:
"Ignis plugins extend server functionality and run alongside your vaults. " +
"They are separate from Obsidian's built-in plugins.",
@@ -92,28 +69,16 @@ function display(containerEl, app) {
toggle.setValue(enabled);
toggle.onChange(async (value) => {
try {
await togglePlugin(plugin.id, value, app);
if (value && plugin.bundledPluginId) {
await refreshPluginCache(plugin.bundledPluginId);
}
await activateBundledPlugin(
plugin.bundledPluginId,
value,
app,
);
await togglePlugin(plugin.id, value);
new Notice(
`${plugin.name} ${value ? "enabled" : "disabled"} for this vault.`,
);
// Give Obsidian a moment to update its plugin tabs,
// then reconcile our sidebar groups.
// The server's WS broadcast drives the actual load/unload via virtual-plugin-loader.
// Reconcile the settings sidebar so the new plugin's settings tab gets grouped correctly.
setTimeout(() => {
if (typeof window.__ignisReconcilePluginTabs === "function") {
window.__ignisReconcilePluginTabs(app.setting);
}
reconcilePluginTabs(app.setting);
}, 100);
} catch (e) {
new Notice(`Failed: ${e.message}`);

View File

@@ -1,27 +1,18 @@
function getWsStatus() {
const ws = window.__ignisWs;
if (!ws) {
return "disconnected";
}
switch (ws.readyState) {
case WebSocket.CONNECTING:
return "connecting";
case WebSocket.OPEN:
return "connected";
default:
return "disconnected";
}
}
const STATUS_LABELS = {
connected: "Ignis server: Connected",
open: "Ignis server: Connected",
connecting: "Ignis server: Connecting...",
disconnected: "Ignis server: Disconnected",
closed: "Ignis server: Disconnected",
};
const STATUS_DOT_CLASSES = {
open: "ignis-statusbar-connected",
connecting: "ignis-statusbar-connecting",
closed: "ignis-statusbar-disconnected",
};
function initStatusBar(plugin) {
const ws = window.__ignis.ws;
const item = plugin.addStatusBarItem();
item.addClass("ignis-statusbar-item");
@@ -29,20 +20,16 @@ function initStatusBar(plugin) {
cls: "ignis-statusbar-dot",
});
item.setAttribute("aria-label", "Ignis: Checking...");
item.setAttribute("data-tooltip-position", "top");
const update = () => {
const status = getWsStatus();
dot.className = `ignis-statusbar-dot ignis-statusbar-${status}`;
item.setAttribute("aria-label", STATUS_LABELS[status] || "Ignis: Unknown");
};
function render(state) {
dot.className = `ignis-statusbar-dot ${STATUS_DOT_CLASSES[state] || STATUS_DOT_CLASSES.closed}`;
item.setAttribute("aria-label", STATUS_LABELS[state] || STATUS_LABELS.closed);
}
update();
render(ws.isOpen() ? "open" : "closed");
const interval = setInterval(update, 3000);
return interval;
return ws.onStateChange(render);
}
module.exports = { initStatusBar };