@nonamelego/dsh-catppuccin 0.4.3 → 0.5.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -271,11 +271,13 @@ Then add `@nonamelego/dsh-catppuccin` to the profile's `package.json`
271
271
  A: The stock Appearance row only lists the built-in light / dark / follow-system
272
272
  preferences. The four flavours live in the **Catppuccin** row right below it.
273
273
  - **Q: "How is my theme choice remembered?"**
274
- A: The choice persists in `catppuccin-state.json` under the DSH home (shared across
275
- every DSH instance on the machine), with localStorage as an in-browser cache and
276
- cross-tab sync. So plain `dsh web`, `dsh web --port <custom>`, and **DSH Desktop**
277
- (which boots on a fresh random loopback port every launch) all restore the preference
278
- automatically. The glass switch and every knob persist the same way.
274
+ A: The choice persists in DSH's official settings document (the `catppuccin` namespace,
275
+ stored under the DSH home, shared across every DSH instance on the machine), with
276
+ localStorage as an in-browser cache and cross-tab sync. So plain `dsh web`,
277
+ `dsh web --port <custom>`, and **DSH Desktop** (which boots on a fresh random loopback
278
+ port every launch) all restore the preference automatically. The glass switch and every
279
+ knob persist the same way. Since 0.5.0, a legacy `catppuccin-state.json` is migrated
280
+ into the settings document once on first launch (the file is kept as a rollback copy).
279
281
  - **Q: "How do I know if this plugin has a new version?"**
280
282
  A: Settings → General → **Check Catppuccin plugin updates** compares against npm in one
281
283
  click and gives a copyable upgrade command; or run
package/README.md CHANGED
@@ -253,11 +253,12 @@ pnpm --dir C:\Users\LeGo\.dsh\profiles\web add link:D:\Vibe-Coding\dsh-catppucci
253
253
  A: 官方外观行只列出内置的浅色/深色/跟随系统偏好。四个主题在它正下方的
254
254
  **Catppuccin** 行里。
255
255
  - Q: **_"我的主题选择是怎么记住的?"_**
256
- A: 选择持久保存在 DSH home 下的 `catppuccin-state.json`(同一台机器的
257
- DSH 共享这份偏好),浏览器 localStorage 作为即时缓存与多标签页同步。
258
- 因此普通 `dsh web`、`dsh web --port` 自定义端口,以及 **DSH Desktop**
259
- (每次启动用随机回环端口)都能跨重启自动恢复;玻璃质感开关与各旋钮同样
260
- 持久保存。
256
+ A: 选择持久保存在 DSH 的官方 settings 文档(`catppuccin` namespace,存于
257
+ DSH home 下,同一台机器的 DSH 共享这份偏好),浏览器 localStorage 作为
258
+ 即时缓存与多标签页同步。因此普通 `dsh web`、`dsh web --port` 自定义端口,
259
+ 以及 **DSH Desktop**(每次启动用随机回环端口)都能跨重启自动恢复;玻璃质感
260
+ 开关与各旋钮同样持久保存。0.5.0 起旧版 `catppuccin-state.json` 会在首次
261
+ 启动时一次性迁移进 settings 文档(文件保留作回退)。
261
262
  - Q: **_"怎么知道这个插件有没有新版本?"_**
262
263
  A: 设置 → 常规 → **检查 Catppuccin 插件更新** 一键检测本插件在 npm 上的最新版本,
263
264
  发现新版会给出可复制的升级命令;也可以随时手动执行
package/lib/client.js CHANGED
@@ -1037,8 +1037,9 @@ window.__ModuleLoader__.load({
1037
1037
  observer.disconnect();
1038
1038
  };
1039
1039
  }
1040
- /** Host route answering the durable state (GET reads, PUT writes). */
1041
- const STATE_ROUTE_PATH = "/catppuccin/state";
1040
+ /** Settings namespace both halves address: Host registers it, Client binds
1041
+ * it through `ctx.settingsScope` (see `src/settings-catppuccin.ts`). */
1042
+ const CATPPUCCIN_SETTINGS_NS = "catppuccin";
1042
1043
  /** The four registered Catppuccin theme ids. MUST stay in sync with the
1043
1044
  * `themeId` of `CATPPUCCIN_FLAVORS` in `src/client/palettes.ts` — guarded by
1044
1045
  * `tests/state.spec.ts` — because the persisted flavour value IS the theme id. */
@@ -1098,6 +1099,23 @@ window.__ModuleLoader__.load({
1098
1099
  const expected = defaultState();
1099
1100
  return state.flavor === expected.flavor && state.glass.enabled === expected.glass.enabled && state.glass.mode === expected.glass.mode && state.glass.blur === expected.glass.blur && state.glass.frost === expected.glass.frost && state.glass.brightness === expected.glass.brightness;
1100
1101
  }
1102
+ /** Drop the synthetic `version` field: full state → settings-document section. */
1103
+ function settingsSectionFromState(state) {
1104
+ return {
1105
+ flavor: state.flavor,
1106
+ glass: { ...state.glass }
1107
+ };
1108
+ }
1109
+ /** Lift a settings-document section back into the full state-shaped contract
1110
+ * (re-sanitized so a hand-edited document can never poison the plugin). */
1111
+ function stateFromSettingsSection(section) {
1112
+ return sanitizeState(section);
1113
+ }
1114
+ /** Whether two settings sections are field-for-field equal (used to skip
1115
+ * redundant mirror writes and to recognize the Host echoing our own write). */
1116
+ function settingsSectionsEqual(a, b) {
1117
+ return a.flavor === b.flavor && a.glass.enabled === b.glass.enabled && a.glass.mode === b.glass.mode && a.glass.blur === b.glass.blur && a.glass.frost === b.glass.frost && a.glass.brightness === b.glass.brightness;
1118
+ }
1101
1119
  //#endregion
1102
1120
  //#region src/client/glass/glass-layer.ts
1103
1121
  /** html attribute selecting the glass layer: CSS hooks and page effects. */
@@ -1927,75 +1945,98 @@ window.__ModuleLoader__.load({
1927
1945
  //#endregion
1928
1946
  //#region src/client/state-sync.ts
1929
1947
  /**
1930
- * Browser half of the durable Catppuccin state a thin wrapper over the
1931
- * Host's `/catppuccin/state` route (same-origin relative fetch, so it works on
1932
- * DSH Desktop's per-launch random loopback port). The Host file is the source
1933
- * of truth; this module adds the fetch wrapper plus a debounced trailing
1934
- * writer so rapid knob drags coalesce into a single PUT. Every failure is
1935
- * non-fatal: the plugin keeps working from browser localStorage alone.
1948
+ * Bind the Catppuccin namespace on this context. The scope derives from the
1949
+ * shared document mirror (no wire read of its own) and its life is bound to
1950
+ * the calling fiber, so binding never blocks on the settings transport.
1936
1951
  */
1937
- /** Read the durable state from the Host file (same-origin GET). */
1938
- async function readDurableStateRemote() {
1939
- try {
1940
- const response = await fetch(STATE_ROUTE_PATH, { headers: { accept: "application/json" } });
1941
- if (!response.ok) return {
1942
- available: false,
1943
- state: null
1944
- };
1945
- const payload = await response.json();
1946
- if (payload.ok !== true) return {
1947
- available: false,
1948
- state: null
1949
- };
1950
- return {
1951
- available: true,
1952
- state: payload.state === null || payload.state === void 0 ? null : sanitizeState(payload.state)
1953
- };
1954
- } catch {
1955
- return {
1956
- available: false,
1957
- state: null
1958
- };
1959
- }
1952
+ function bindCatppuccinScope(ctx) {
1953
+ return ctx.settingsScope.bind({ namespace: CATPPUCCIN_SETTINGS_NS });
1960
1954
  }
1961
- /** Write the durable state to the Host file (same-origin PUT). */
1962
- async function persistDurableState(state) {
1955
+ /** Whether the scope is usable for durable persistence: the document is
1956
+ * resolved (`ready`) and the Host persistence is active (`host`). A `memory`
1957
+ * scope (non-loopback pages / absent transport) or a not-yet-ready one is
1958
+ * NOT usable — the caller keeps localStorage as the only store. */
1959
+ function isScopeUsable(snapshot) {
1960
+ return snapshot.status === "ready" && snapshot.mode === "host";
1961
+ }
1962
+ /** The local state contract (with the synthetic `version`) for a usable
1963
+ * scope snapshot; `null` while the section is not resolved. */
1964
+ function durableStateFromSnapshot(snapshot) {
1965
+ if (!isScopeUsable(snapshot) || snapshot.value === void 0) return null;
1966
+ return stateFromSettingsSection(snapshot.value);
1967
+ }
1968
+ /** One atomic mutation covering the whole local state (revision-fenced).
1969
+ * Writing every field keeps the section self-contained — a partial write
1970
+ * could otherwise leave a knob stuck on a stale override after a document
1971
+ * edit elsewhere. */
1972
+ function stateToMutateOps(state) {
1973
+ const section = settingsSectionFromState(state);
1974
+ return [
1975
+ {
1976
+ op: "set",
1977
+ path: ["flavor"],
1978
+ value: section.flavor
1979
+ },
1980
+ {
1981
+ op: "set",
1982
+ path: ["glass", "enabled"],
1983
+ value: section.glass.enabled
1984
+ },
1985
+ {
1986
+ op: "set",
1987
+ path: ["glass", "mode"],
1988
+ value: section.glass.mode
1989
+ },
1990
+ {
1991
+ op: "set",
1992
+ path: ["glass", "blur"],
1993
+ value: section.glass.blur
1994
+ },
1995
+ {
1996
+ op: "set",
1997
+ path: ["glass", "frost"],
1998
+ value: section.glass.frost
1999
+ },
2000
+ {
2001
+ op: "set",
2002
+ path: ["glass", "brightness"],
2003
+ value: section.glass.brightness
2004
+ }
2005
+ ];
2006
+ }
2007
+ /** Persist the local state through the scope as one atomic mutation.
2008
+ * Resolves `true` when the write was issued. */
2009
+ async function persistStateToScope(scope, state) {
1963
2010
  try {
1964
- return (await fetch(STATE_ROUTE_PATH, {
1965
- method: "PUT",
1966
- headers: {
1967
- accept: "application/json",
1968
- "content-type": "application/json"
1969
- },
1970
- body: JSON.stringify(sanitizeState(state))
1971
- })).ok;
2011
+ await scope.mutate(stateToMutateOps(state));
2012
+ return true;
1972
2013
  } catch {
1973
2014
  return false;
1974
2015
  }
1975
2016
  }
1976
2017
  let pendingTimer;
1977
- let pendingGetState;
2018
+ let pendingWrite;
1978
2019
  /**
1979
2020
  * Debounced trailing persist: after a burst of changes (a slider drag emits a
1980
- * stream of snapshot updates) the latest state is written to the Host once,
1981
- * a short beat after the last change. Pass an accessor so the snapshot is
1982
- * taken at flush time, never stale.
2021
+ * stream of snapshot updates) the write happens once, a short beat after the
2022
+ * last change. Pass an accessor so the state snapshot is taken at flush time,
2023
+ * never stale.
1983
2024
  */
1984
- function scheduleDurablePersist(getState, delayMs = 300) {
2025
+ function scheduleDurablePersist(write, delayMs = 300) {
1985
2026
  if (pendingTimer !== void 0) window.clearTimeout(pendingTimer);
1986
- pendingGetState = getState;
2027
+ pendingWrite = write;
1987
2028
  pendingTimer = window.setTimeout(() => {
1988
2029
  pendingTimer = void 0;
1989
- const state = pendingGetState;
1990
- pendingGetState = void 0;
1991
- if (state !== void 0) persistDurableState(state()).catch(() => {});
2030
+ const run = pendingWrite;
2031
+ pendingWrite = void 0;
2032
+ if (run !== void 0) Promise.resolve().then(run).catch(() => {});
1992
2033
  }, delayMs);
1993
2034
  }
1994
2035
  /** Drop any queued persist (plugin unload / page teardown). */
1995
2036
  function cancelDurablePersist() {
1996
2037
  if (pendingTimer !== void 0) window.clearTimeout(pendingTimer);
1997
2038
  pendingTimer = void 0;
1998
- pendingGetState = void 0;
2039
+ pendingWrite = void 0;
1999
2040
  }
2000
2041
  //#endregion
2001
2042
  //#region \0dsh-css:/home/runner/work/dsh-catppuccin-theme/dsh-catppuccin-theme/src/client/glass/glass.module.css.mjs
@@ -2086,11 +2127,13 @@ window.__ModuleLoader__.load({
2086
2127
  if (preference !== "light" && preference !== "dark") return false;
2087
2128
  return preference === livePick;
2088
2129
  }
2089
- /** Required services: slots + locale (settings rows) and theme (register + switch). */
2130
+ /** Required services: slots + locale (settings rows), theme (register +
2131
+ * switch), and the settings scope (durable persistence). */
2090
2132
  const inject = [
2091
2133
  "slots",
2092
2134
  "locale",
2093
- "theme"
2135
+ "theme",
2136
+ "settingsScope"
2094
2137
  ];
2095
2138
  /**
2096
2139
  * Register the Catppuccin dictionaries, the four flavour themes, and the
@@ -2121,11 +2164,16 @@ window.__ModuleLoader__.load({
2121
2164
  };
2122
2165
  }, "catppuccin: flavour themes");
2123
2166
  const glass = new GlassLayer(ctx);
2167
+ const scope = bindCatppuccinScope(ctx);
2124
2168
  const buildLocalState = () => ({
2125
2169
  version: 1,
2126
2170
  flavor: readFlavor(),
2127
2171
  glass: glass.getRemoteState()
2128
2172
  });
2173
+ const persistLocal = () => {
2174
+ if (!isScopeUsable(scope.getSnapshot())) return;
2175
+ persistStateToScope(scope, buildLocalState());
2176
+ };
2129
2177
  ctx.effect(() => {
2130
2178
  let liveBuiltinPick = null;
2131
2179
  const originalSetTheme = theme.setTheme;
@@ -2155,22 +2203,26 @@ window.__ModuleLoader__.load({
2155
2203
  };
2156
2204
  window.addEventListener("storage", onStorage);
2157
2205
  const offGlass = glass.subscribe(() => {
2158
- scheduleDurablePersist(buildLocalState);
2206
+ scheduleDurablePersist(persistLocal);
2159
2207
  });
2160
- let cancelled = false;
2161
- readDurableStateRemote().then((result) => {
2162
- if (cancelled) return;
2163
- if (!result.available) return;
2164
- if (result.state === null) {
2165
- if (!isDefaultState(buildLocalState())) scheduleDurablePersist(buildLocalState);
2208
+ const applyScopeSnapshot = () => {
2209
+ const snapshot = scope.getSnapshot();
2210
+ const state = durableStateFromSnapshot(snapshot);
2211
+ if (state === null) return;
2212
+ const local = buildLocalState();
2213
+ if (snapshot.user === void 0) {
2214
+ if (!isDefaultState(local)) scheduleDurablePersist(persistLocal);
2166
2215
  return;
2167
2216
  }
2168
- writeFlavor(result.state.flavor);
2169
- glass.applyRemote(result.state.glass);
2217
+ if (settingsSectionsEqual(settingsSectionFromState(local), settingsSectionFromState(state))) return;
2218
+ writeFlavor(state.flavor);
2219
+ glass.applyRemote(state.glass);
2170
2220
  applyDesired();
2171
- });
2221
+ };
2222
+ applyScopeSnapshot();
2223
+ const offScope = scope.subscribe(applyScopeSnapshot);
2172
2224
  return () => {
2173
- cancelled = true;
2225
+ offScope();
2174
2226
  offGlass();
2175
2227
  disposer();
2176
2228
  window.removeEventListener("storage", onStorage);
@@ -2193,14 +2245,14 @@ window.__ModuleLoader__.load({
2193
2245
  if (choice === "off") {
2194
2246
  writeFlavor("off");
2195
2247
  theme.setTheme(readRestoredPreference());
2196
- scheduleDurablePersist(buildLocalState);
2248
+ scheduleDurablePersist(persistLocal);
2197
2249
  return;
2198
2250
  }
2199
2251
  const flavor = flavorInfo(choice);
2200
2252
  if (!flavor) return;
2201
2253
  writeFlavor(flavor.themeId);
2202
2254
  theme.setTheme(flavor.themeId);
2203
- scheduleDurablePersist(buildLocalState);
2255
+ scheduleDurablePersist(persistLocal);
2204
2256
  }
2205
2257
  });
2206
2258
  ctx.slots.inject("settings.general.item", () => ctx.slots.register({