dsh-background-by-model 0.3.1 → 0.4.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/lib/index.js CHANGED
@@ -1,73 +1,66 @@
1
1
  import { access, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
2
2
  import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
3
- //#region src/index.ts
3
+ //#region src/schema.ts
4
+ /** Every accepted `bgMode`, in UI order. */
5
+ const BG_MODES = [
6
+ "fit",
7
+ "fill",
8
+ "stretch",
9
+ "tile",
10
+ "center"
11
+ ];
4
12
  /**
5
- * Node half of dsh-background-by-model: file-backed rule persistence.
6
- *
7
- * Owns the `~/.dsh/.dsh-background-by-model-data/` store and exposes a small RPC
8
- * surface on the dedicated `/dsh-background-by-model` channel (never the shared
9
- * `/api`, so slash commands stay intact).
10
- *
11
- * theme-config.json the ordered rule list + the global interface settings
12
- * modelbg-<slot> one background image per rule (raw bytes, no extension;
13
- * the MIME is sniffed from the magic bytes when served)
14
- *
15
- * Images travel as data URLs over the RPC channel — there is no separate HTTP
16
- * image route. A legacy single-wallpaper / video-background store is migrated
17
- * to rule 1 on first read.
13
+ * Main interface opacities (0..1), global (Interface page):
14
+ * `bg` main background (`--dsw-alias-bg-base`)
15
+ * `sidebar` sidebar (`--dsw-specific-sidebar-fill`)
16
+ * `card` cards/panels (`--dsw-alias-bg-layer-1/2/3`, `--dsw-specific-menu`)
17
+ * `input` input/control surfaces (`--dsw-specific-input-major`)
18
18
  */
19
- const name = "dsh-background-by-model";
20
- const inject = ["connection", "webServer"];
21
- const DATA_DIR = ".dsh-background-by-model-data";
22
- const CONFIG_FILE = "theme-config.json";
23
- const IMAGE_PREFIX = "modelbg-";
24
- const LEGACY_WALLPAPER = "wallpaper.jpg";
25
- const LEGACY_FILES = [
26
- "wallpaper.mp4",
27
- "wallpaper.webm",
28
- "wallpaper.ogv",
29
- "wallpaper.mov",
30
- "wallpaper.mkv",
31
- "wallpaper.video",
32
- "wallpaper.upload.tmp"
19
+ const PART_OPACITY_KEYS = [
20
+ "bg",
21
+ "sidebar",
22
+ "card",
23
+ "input"
33
24
  ];
34
- /** Slot names reach the filesystem, so they are strictly whitelisted. */
35
- const SLOT_RE = /^[A-Za-z0-9_-]{1,32}$/;
36
- const WALLPAPER_FETCH_MAX = 26214400;
37
- const WALLPAPER_FETCH_TIMEOUT = 2e4;
38
- const DEFAULT_CONFIG = {
39
- rules: [],
40
- opacities: {
41
- bg: .85,
42
- sidebar: .93,
43
- card: 1,
44
- input: 1
45
- },
46
- blurs: {
47
- bg: 0,
48
- sidebar: 0,
49
- card: 0,
50
- settings: 0,
51
- chat: 0,
52
- trajectory: 0,
53
- input: 0
54
- },
55
- settingsOpacity: 1,
56
- chatTextOpacity: 0,
57
- trajectoryOpacity: 1
25
+ const DEFAULT_PART_OPACITIES = {
26
+ bg: .85,
27
+ sidebar: .93,
28
+ card: 1,
29
+ input: 1
58
30
  };
59
- const dataDir = () => dshHomePath(DATA_DIR);
60
- const configPath = () => dshHomePath(DATA_DIR, CONFIG_FILE);
61
- const legacyWallpaperPath = () => dshHomePath(DATA_DIR, LEGACY_WALLPAPER);
62
- const imagePath = (slot) => dshHomePath(DATA_DIR, `${IMAGE_PREFIX}${slot}`);
63
- const exists = async (p) => {
64
- try {
65
- await access(p);
66
- return true;
67
- } catch {
68
- return false;
69
- }
31
+ /**
32
+ * Interface blur (px, 0..60), global (Interface page):
33
+ * `bg` main background (AppFrame grid)
34
+ * `sidebar` sidebar column
35
+ * `card` cards/panels (center + details columns, dialog option panels)
36
+ * `settings` settings panel
37
+ * `chat` conversation text region (message column of the chat view)
38
+ * `trajectory` trajectory view surface
39
+ * `rightbar` file-preview panel — the right sidebar a file click opens
40
+ * `input` input/control surfaces ([data-composer-card], [data-cordis-panel])
41
+ */
42
+ const PART_BLUR_KEYS = [
43
+ "bg",
44
+ "sidebar",
45
+ "card",
46
+ "settings",
47
+ "chat",
48
+ "trajectory",
49
+ "rightbar",
50
+ "input"
51
+ ];
52
+ const DEFAULT_PART_BLURS = {
53
+ bg: 0,
54
+ sidebar: 0,
55
+ card: 0,
56
+ settings: 0,
57
+ chat: 0,
58
+ trajectory: 0,
59
+ rightbar: 0,
60
+ input: 0
70
61
  };
62
+ /** Slot names reach the filesystem, so they are strictly whitelisted. */
63
+ const SLOT_RE = /^[A-Za-z0-9_-]{1,32}$/;
71
64
  function clamp(n, lo, hi, def) {
72
65
  return typeof n === "number" && isFinite(n) ? Math.min(hi, Math.max(lo, n)) : def;
73
66
  }
@@ -82,31 +75,29 @@ function normalizeBgState(s) {
82
75
  ih: typeof v.ih === "number" && v.ih > 0 ? v.ih : 0
83
76
  };
84
77
  }
78
+ /** One `[h, s, l]` triple, or null when the value is not a complete finite triple. */
79
+ function normalizeHsl(raw) {
80
+ if (!Array.isArray(raw) || raw.length !== 3) return null;
81
+ if (!raw.every((n) => typeof n === "number" && isFinite(n))) return null;
82
+ return [
83
+ clamp(raw[0], 0, 360, 220),
84
+ clamp(raw[1], 0, 1, .55),
85
+ clamp(raw[2], 0, 1, .25)
86
+ ];
87
+ }
85
88
  /** Coerce one persisted rule, or null when it lacks a usable id/slot. */
86
89
  function normalizeRule(raw) {
87
90
  const r = raw ?? {};
88
91
  const id = typeof r.id === "string" && r.id !== "" ? r.id : null;
89
92
  const slot = typeof r.slot === "string" && SLOT_RE.test(r.slot) ? r.slot : null;
90
93
  if (id === null || slot === null) return null;
91
- const c = r.color;
92
- const color = Array.isArray(c) && c.length === 3 && c.every((n) => typeof n === "number" && isFinite(n)) ? [
93
- clamp(c[0], 0, 360, 220),
94
- clamp(c[1], 0, 1, .55),
95
- clamp(c[2], 0, 1, .25)
96
- ] : null;
97
- const mode = [
98
- "fit",
99
- "fill",
100
- "stretch",
101
- "tile",
102
- "center"
103
- ].includes(r.bgMode) ? r.bgMode : "fit";
94
+ const mode = BG_MODES.includes(r.bgMode) ? r.bgMode : "fit";
104
95
  return {
105
96
  id,
106
97
  slot,
107
98
  match: typeof r.match === "string" ? r.match : "",
108
99
  enabled: r.enabled !== false,
109
- color,
100
+ color: normalizeHsl(r.color),
110
101
  bgMode: mode,
111
102
  wallpaperOpacity: clamp01(r.wallpaperOpacity, 1),
112
103
  blur: clamp(r.blur, 0, 60, 0),
@@ -120,29 +111,66 @@ function normalizeConfig(raw) {
120
111
  const ops = r.opacities ?? {};
121
112
  const bl = r.blurs ?? {};
122
113
  const blurs = {};
123
- for (const k of [
124
- "bg",
125
- "sidebar",
126
- "card",
127
- "settings",
128
- "chat",
129
- "trajectory",
130
- "input"
131
- ]) blurs[k] = clamp(bl[k], 0, 60, DEFAULT_CONFIG.blurs[k]);
114
+ for (const k of PART_BLUR_KEYS) blurs[k] = clamp(bl[k], 0, 60, DEFAULT_PART_BLURS[k]);
115
+ const opacities = {};
116
+ for (const k of PART_OPACITY_KEYS) opacities[k] = clamp01(ops[k], DEFAULT_PART_OPACITIES[k]);
132
117
  return {
133
118
  rules,
134
- opacities: {
135
- bg: clamp01(ops.bg, DEFAULT_CONFIG.opacities.bg),
136
- sidebar: clamp01(ops.sidebar, DEFAULT_CONFIG.opacities.sidebar),
137
- card: clamp01(ops.card, DEFAULT_CONFIG.opacities.card),
138
- input: clamp01(ops.input, DEFAULT_CONFIG.opacities.input)
139
- },
119
+ opacities,
140
120
  blurs,
141
- settingsOpacity: clamp01(r.settingsOpacity, DEFAULT_CONFIG.settingsOpacity),
142
- chatTextOpacity: clamp01(r.chatTextOpacity, DEFAULT_CONFIG.chatTextOpacity),
143
- trajectoryOpacity: clamp01(r.trajectoryOpacity, DEFAULT_CONFIG.trajectoryOpacity)
121
+ settingsOpacity: clamp01(r.settingsOpacity, 1),
122
+ chatTextOpacity: clamp01(r.chatTextOpacity, 0),
123
+ trajectoryOpacity: clamp01(r.trajectoryOpacity, 1),
124
+ rightbarOpacity: typeof r.rightbarOpacity === "number" && isFinite(r.rightbarOpacity) ? clamp01(r.rightbarOpacity, 1) : null,
125
+ autoExtract: r.autoExtract !== false
144
126
  };
145
127
  }
128
+ //#endregion
129
+ //#region src/index.ts
130
+ /**
131
+ * Node half of dsh-background-by-model: file-backed rule persistence.
132
+ *
133
+ * Owns the `~/.dsh/.dsh-background-by-model-data/` store and exposes a small RPC
134
+ * surface on the dedicated `/dsh-background-by-model` channel (never the shared
135
+ * `/api`, so slash commands stay intact).
136
+ *
137
+ * theme-config.json the ordered rule list + the global interface settings
138
+ * modelbg-<slot> one background image per rule (raw bytes, no extension;
139
+ * the MIME is sniffed from the magic bytes when served)
140
+ *
141
+ * Images travel as data URLs over the RPC channel — there is no separate HTTP
142
+ * image route. A legacy single-wallpaper / video-background store is migrated
143
+ * to rule 1 on first read.
144
+ */
145
+ const name = "dsh-background-by-model";
146
+ const inject = ["connection", "webServer"];
147
+ const DATA_DIR = ".dsh-background-by-model-data";
148
+ const CONFIG_FILE = "theme-config.json";
149
+ const IMAGE_PREFIX = "modelbg-";
150
+ const LEGACY_WALLPAPER = "wallpaper.jpg";
151
+ const LEGACY_FILES = [
152
+ "wallpaper.mp4",
153
+ "wallpaper.webm",
154
+ "wallpaper.ogv",
155
+ "wallpaper.mov",
156
+ "wallpaper.mkv",
157
+ "wallpaper.video",
158
+ "wallpaper.upload.tmp"
159
+ ];
160
+ const WALLPAPER_FETCH_MAX = 26214400;
161
+ const WALLPAPER_FETCH_TIMEOUT = 2e4;
162
+ const dataDir = () => dshHomePath(DATA_DIR);
163
+ const configPath = () => dshHomePath(DATA_DIR, CONFIG_FILE);
164
+ const legacyWallpaperPath = () => dshHomePath(DATA_DIR, LEGACY_WALLPAPER);
165
+ const imagePath = (slot) => dshHomePath(DATA_DIR, `${IMAGE_PREFIX}${slot}`);
166
+ const exists = async (p) => {
167
+ try {
168
+ await access(p);
169
+ return true;
170
+ } catch {
171
+ return false;
172
+ }
173
+ };
146
174
  /**
147
175
  * One-shot migration of the pre-0.3 store.
148
176
  *
@@ -185,7 +213,8 @@ async function migrateLegacy(raw) {
185
213
  blurs: r.blurs,
186
214
  settingsOpacity: r.settingsOpacity,
187
215
  chatTextOpacity: r.chatTextOpacity,
188
- trajectoryOpacity: r.trajectoryOpacity
216
+ trajectoryOpacity: r.trajectoryOpacity,
217
+ rightbarOpacity: r.rightbarOpacity
189
218
  };
190
219
  try {
191
220
  await writeFile(configPath(), JSON.stringify(next, null, 2), "utf8");
@@ -211,10 +240,44 @@ async function readConfig() {
211
240
  }
212
241
  return normalizeConfig(await migrateLegacy(raw));
213
242
  }
243
+ const LEGACY_CONFIG_KEYS = /* @__PURE__ */ new Set([
244
+ "color",
245
+ "bgMode",
246
+ "wallpaperOpacity",
247
+ "blur",
248
+ "bgState"
249
+ ]);
250
+ const warnedConfigKeys = /* @__PURE__ */ new Set();
251
+ function warnUnknownConfigKeys(raw, normalized) {
252
+ if (raw === null || typeof raw !== "object") return;
253
+ const r = raw;
254
+ const warned = (id) => {
255
+ if (warnedConfigKeys.has(id)) return;
256
+ warnedConfigKeys.add(id);
257
+ console.warn(`dsh-background-by-model: ignoring unknown config field "${id}" (declared in one half only?)`);
258
+ };
259
+ const known = new Set(Object.keys(normalized));
260
+ for (const key of Object.keys(r)) if (!known.has(key) && !LEGACY_CONFIG_KEYS.has(key)) warned(key);
261
+ const groups = [["blurs", normalized.blurs], ["opacities", normalized.opacities]];
262
+ for (const [group, have] of groups) {
263
+ const got = r[group];
264
+ if (got === null || typeof got !== "object") continue;
265
+ const keys = new Set(Object.keys(have));
266
+ for (const key of Object.keys(got)) if (!keys.has(key)) warned(`${group}.${key}`);
267
+ }
268
+ const sent = Array.isArray(r.rules) ? r.rules[0] : void 0;
269
+ const have = normalized.rules[0];
270
+ if (sent !== null && typeof sent === "object" && have !== void 0) {
271
+ const keys = new Set(Object.keys(have));
272
+ for (const key of Object.keys(sent)) if (!keys.has(key)) warned(`rules[].${key}`);
273
+ }
274
+ }
214
275
  async function writeConfig(config) {
215
276
  await ensureDir();
216
277
  try {
217
- await writeFile(configPath(), JSON.stringify(normalizeConfig(config), null, 2), "utf8");
278
+ const normalized = normalizeConfig(config);
279
+ warnUnknownConfigKeys(config, normalized);
280
+ await writeFile(configPath(), JSON.stringify(normalized, null, 2), "utf8");
218
281
  return true;
219
282
  } catch (e) {
220
283
  console.error(`dsh-background-by-model: failed to write "${CONFIG_FILE}"`, e);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-background-by-model",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Appearance plugin for DeepSeek Harness: an ordered list of model rules, each carrying its own wallpaper, theme color, layout mode, opacity and blur — the background follows the model you switch to.",
5
5
  "keywords": [
6
6
  "deepseek-harness",