dsh-any-background 0.2.8 → 0.2.9

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
@@ -27,10 +27,14 @@ const VIDEO_ROUTE = "/dsh-any-background/video";
27
27
  const UPLOAD_ROUTE = "/dsh-any-background/video/upload";
28
28
  const WALLPAPER_ROUTE = "/dsh-any-background/wallpaper";
29
29
  const WALLPAPER_UPLOAD_ROUTE = "/dsh-any-background/wallpaper/upload";
30
+ const FONT_ROUTE = "/dsh-any-background/font";
31
+ const FONT_UPLOAD_ROUTE = "/dsh-any-background/font/upload";
30
32
  const UPLOAD_TMP = "wallpaper.upload.tmp";
31
33
  const VIDEO_UPLOAD_TMP = "video.upload.tmp";
34
+ const FONT_UPLOAD_TMP = "font.upload.tmp";
32
35
  const WALLPAPER_UPLOAD_MAX = 104857600;
33
36
  const VIDEO_UPLOAD_MAX = 2147483648;
37
+ const FONT_UPLOAD_MAX = 104857600;
34
38
  const WALLPAPER_FETCH_MAX = 26214400;
35
39
  const WALLPAPER_FETCH_TIMEOUT = 2e4;
36
40
  const VIDEO_FETCH_MAX = 2147483648;
@@ -54,28 +58,100 @@ const VIDEO_CANDIDATES = [
54
58
  "wallpaper.mkv",
55
59
  "wallpaper.video"
56
60
  ];
61
+ /** Font slot: one font owns the slot, named by format like the video slot. */
62
+ function fontFileName(mime) {
63
+ switch (mime) {
64
+ case "font/woff2": return "font.woff2";
65
+ case "font/woff": return "font.woff";
66
+ case "font/otf": return "font.otf";
67
+ case "font/ttf": return "font.ttf";
68
+ default: return "font.ttf";
69
+ }
70
+ }
71
+ const FONT_CANDIDATES = [
72
+ "font.woff2",
73
+ "font.woff",
74
+ "font.otf",
75
+ "font.ttf"
76
+ ];
77
+ /** Sniff a font's container format from its leading magic bytes (null when the
78
+ * bytes are not a recognized font — uploads are rejected rather than stored). */
79
+ function sniffFontMime(buf) {
80
+ if (buf.length >= 4 && buf[0] === 119 && buf[1] === 79 && buf[2] === 70 && buf[3] === 50) return "font/woff2";
81
+ if (buf.length >= 4 && buf[0] === 119 && buf[1] === 79 && buf[2] === 70 && buf[3] === 70) return "font/woff";
82
+ if (buf.length >= 4 && buf[0] === 79 && buf[1] === 84 && buf[2] === 84 && buf[3] === 79) return "font/otf";
83
+ if (buf.length >= 4 && buf[0] === 0 && buf[1] === 1 && buf[2] === 0 && buf[3] === 0) return "font/ttf";
84
+ return null;
85
+ }
57
86
  const DEFAULT_CONFIG = {
58
87
  color: null,
59
88
  opacities: {
60
- bg: .85,
61
- sidebar: .93,
62
- card: 1,
63
- input: 1
89
+ bg: .5,
90
+ sidebar: .5,
91
+ card: .5,
92
+ input: .5
64
93
  },
65
94
  blurs: {
66
- bg: 0,
67
- sidebar: 0,
68
- card: 0,
69
- settings: 0,
70
- chat: 0,
71
- trajectory: 0,
72
- input: 0,
73
- panel: 0,
74
- produced: 0
95
+ bg: 30,
96
+ sidebar: 30,
97
+ card: 30,
98
+ settings: 30,
99
+ chat: 30,
100
+ trajectory: 30,
101
+ input: 30,
102
+ panel: 30,
103
+ produced: 30
75
104
  },
76
- settingsOpacity: 1,
105
+ strokes: {
106
+ bg: {
107
+ width: 0,
108
+ color: "auto",
109
+ customColor: "#808080"
110
+ },
111
+ sidebar: {
112
+ width: 0,
113
+ color: "auto",
114
+ customColor: "#808080"
115
+ },
116
+ card: {
117
+ width: 0,
118
+ color: "auto",
119
+ customColor: "#808080"
120
+ },
121
+ settings: {
122
+ width: 0,
123
+ color: "auto",
124
+ customColor: "#808080"
125
+ },
126
+ chat: {
127
+ width: 0,
128
+ color: "auto",
129
+ customColor: "#808080"
130
+ },
131
+ trajectory: {
132
+ width: 0,
133
+ color: "auto",
134
+ customColor: "#808080"
135
+ },
136
+ input: {
137
+ width: 0,
138
+ color: "auto",
139
+ customColor: "#808080"
140
+ },
141
+ panel: {
142
+ width: 0,
143
+ color: "auto",
144
+ customColor: "#808080"
145
+ },
146
+ produced: {
147
+ width: 0,
148
+ color: "auto",
149
+ customColor: "#808080"
150
+ }
151
+ },
152
+ settingsOpacity: .5,
77
153
  wallpaperOpacity: 1,
78
- blur: 0,
154
+ blur: 30,
79
155
  bgState: {
80
156
  zoom: 1,
81
157
  x: 0,
@@ -93,12 +169,14 @@ const DEFAULT_CONFIG = {
93
169
  backgroundType: "image",
94
170
  bgMode: "fit",
95
171
  videoMime: null,
172
+ fontMime: null,
173
+ fontEnabled: true,
96
174
  generatedBg: null,
97
175
  regenerateOnReload: false,
98
- chatTextOpacity: 0,
99
- trajectoryOpacity: 1,
100
- panelOpacity: 1,
101
- producedOpacity: 1,
176
+ chatTextOpacity: .5,
177
+ trajectoryOpacity: .5,
178
+ panelOpacity: .5,
179
+ producedOpacity: .5,
102
180
  profiles: [],
103
181
  rotation: {
104
182
  enabled: false,
@@ -123,6 +201,7 @@ const dataDir = () => dshHomePath(DATA_DIR);
123
201
  const configPath = () => dshHomePath(DATA_DIR, CONFIG_FILE);
124
202
  const wallpaperPath = () => dshHomePath(DATA_DIR, WALLPAPER_FILE);
125
203
  const videoPathFor = (mime) => dshHomePath(DATA_DIR, videoFileName(mime));
204
+ const fontPathFor = (mime) => dshHomePath(DATA_DIR, fontFileName(mime));
126
205
  const exists = async (p) => {
127
206
  try {
128
207
  await access(p);
@@ -162,6 +241,43 @@ async function findVideoFile() {
162
241
  function clamp(n, lo, hi, def) {
163
242
  return typeof n === "number" && isFinite(n) ? Math.min(hi, Math.max(lo, n)) : def;
164
243
  }
244
+ /** Locate the stored font: the recorded MIME decides the expected name; stray
245
+ * files from a lost config write are adopted via rename (mirrors the video
246
+ * slot's recovery). */
247
+ async function findFontFile() {
248
+ const mime = (await readConfig()).fontMime ?? "font/ttf";
249
+ const expected = fontPathFor(mime);
250
+ if (await exists(expected)) return {
251
+ path: expected,
252
+ mime
253
+ };
254
+ for (const name of FONT_CANDIDATES) {
255
+ const p = dshHomePath(DATA_DIR, name);
256
+ if (!await exists(p)) continue;
257
+ const foundMime = mimeForFontFile(name);
258
+ try {
259
+ await rename(p, expected);
260
+ return {
261
+ path: expected,
262
+ mime: foundMime
263
+ };
264
+ } catch {
265
+ return null;
266
+ }
267
+ }
268
+ return null;
269
+ }
270
+ function mimeForFontFile(name) {
271
+ switch (name) {
272
+ case "font.woff2": return "font/woff2";
273
+ case "font.woff": return "font/woff";
274
+ case "font.otf": return "font/otf";
275
+ default: return "font/ttf";
276
+ }
277
+ }
278
+ async function fontUrl() {
279
+ return await findFontFile() ? FONT_ROUTE : null;
280
+ }
165
281
  function normalizeBgState(s) {
166
282
  return {
167
283
  zoom: clamp(s.zoom, .1, 10, 1),
@@ -172,6 +288,40 @@ function normalizeBgState(s) {
172
288
  };
173
289
  }
174
290
  /** Coerce an unknown persisted value into a valid ThemeConfig, falling back per-field. */
291
+ const STROKE_GROUPS = [
292
+ "bg",
293
+ "sidebar",
294
+ "card",
295
+ "settings",
296
+ "chat",
297
+ "trajectory",
298
+ "input",
299
+ "panel",
300
+ "produced"
301
+ ];
302
+ const STROKE_COLOR_KEYS = [
303
+ "auto",
304
+ "gray",
305
+ "black",
306
+ "white",
307
+ "theme",
308
+ "custom"
309
+ ];
310
+ const HEX_RE = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
311
+ function normalizeStroke(raw) {
312
+ const s = raw ?? {};
313
+ return {
314
+ width: clamp(s.width, 0, 4, 0),
315
+ color: STROKE_COLOR_KEYS.includes(s.color) ? s.color : "auto",
316
+ customColor: typeof s.customColor === "string" && HEX_RE.test(s.customColor) ? s.customColor : "#808080"
317
+ };
318
+ }
319
+ function normalizeStrokes(raw) {
320
+ const s = raw ?? {};
321
+ const out = {};
322
+ for (const k of STROKE_GROUPS) out[k] = normalizeStroke(s[k]);
323
+ return out;
324
+ }
175
325
  function normalizeConfig(raw) {
176
326
  const r = raw ?? {};
177
327
  const c = r.color;
@@ -220,6 +370,7 @@ function normalizeConfig(raw) {
220
370
  input: clamp(ops.input, 0, 1, DEFAULT_CONFIG.opacities.input)
221
371
  },
222
372
  blurs,
373
+ strokes: normalizeStrokes(r.strokes),
223
374
  settingsOpacity: clamp(r.settingsOpacity, 0, 1, DEFAULT_CONFIG.settingsOpacity),
224
375
  wallpaperOpacity: clamp(r.wallpaperOpacity, 0, 1, DEFAULT_CONFIG.wallpaperOpacity),
225
376
  blur: clamp(r.blur, 0, 60, DEFAULT_CONFIG.blur),
@@ -228,6 +379,8 @@ function normalizeConfig(raw) {
228
379
  backgroundType: bgType,
229
380
  bgMode,
230
381
  videoMime: typeof r.videoMime === "string" ? r.videoMime : null,
382
+ fontMime: typeof r.fontMime === "string" ? r.fontMime : null,
383
+ fontEnabled: typeof r.fontEnabled === "boolean" ? r.fontEnabled : DEFAULT_CONFIG.fontEnabled,
231
384
  generatedBg,
232
385
  regenerateOnReload: typeof r.regenerateOnReload === "boolean" ? r.regenerateOnReload : DEFAULT_CONFIG.regenerateOnReload,
233
386
  chatTextOpacity: clamp(r.chatTextOpacity, 0, 1, DEFAULT_CONFIG.chatTextOpacity),
@@ -311,6 +464,7 @@ function normalizeProfileAppearance(raw) {
311
464
  input: clamp(ops.input, 0, 1, DEFAULT_CONFIG.opacities.input)
312
465
  },
313
466
  blurs,
467
+ strokes: normalizeStrokes(a.strokes),
314
468
  settingsOpacity: clamp(a.settingsOpacity, 0, 1, DEFAULT_CONFIG.settingsOpacity),
315
469
  wallpaperOpacity: clamp(a.wallpaperOpacity, 0, 1, DEFAULT_CONFIG.wallpaperOpacity),
316
470
  blur: clamp(a.blur, 0, 60, DEFAULT_CONFIG.blur),
@@ -406,7 +560,11 @@ function warnUnknownConfigKeys(raw, normalized) {
406
560
  if (known.has(key) || LEGACY_CONFIG_KEYS.has(key)) continue;
407
561
  warn(key);
408
562
  }
409
- for (const group of ["blurs", "opacities"]) {
563
+ for (const group of [
564
+ "blurs",
565
+ "opacities",
566
+ "strokes"
567
+ ]) {
410
568
  const got = r[group];
411
569
  if (got === null || typeof got !== "object") continue;
412
570
  const have = new Set(Object.keys(normalized[group]));
@@ -1123,6 +1281,164 @@ async function handleWallpaperUpload(req, res) {
1123
1281
  } catch {}
1124
1282
  }
1125
1283
  }
1284
+ /** Stream the stored custom font: sniffed MIME, no caching (uploads replace
1285
+ * the file in place). Fonts need no Range support — the browser fetches once. */
1286
+ async function serveFont(req, res) {
1287
+ if (req.method !== "GET" && req.method !== "HEAD") {
1288
+ res.writeHead(405, { "Content-Type": "application/json" });
1289
+ res.end(JSON.stringify({
1290
+ ok: false,
1291
+ error: "font route only serves GET/HEAD"
1292
+ }));
1293
+ return;
1294
+ }
1295
+ try {
1296
+ const found = await findFontFile();
1297
+ if (found === null) {
1298
+ res.writeHead(404);
1299
+ res.end("no custom font stored");
1300
+ return;
1301
+ }
1302
+ const st = await stat(found.path);
1303
+ res.writeHead(200, {
1304
+ "Content-Type": found.mime,
1305
+ "Content-Length": st.size,
1306
+ "Cache-Control": "no-store"
1307
+ });
1308
+ if (req.method === "HEAD") {
1309
+ res.end();
1310
+ return;
1311
+ }
1312
+ createReadStream(found.path).pipe(res);
1313
+ } catch (e) {
1314
+ console.error("dsh-any-background: failed to serve the custom font", e);
1315
+ try {
1316
+ res.writeHead(500);
1317
+ res.end();
1318
+ } catch {}
1319
+ }
1320
+ }
1321
+ /** Accept a raw font upload (POST): pipe the body into a temp file, sniff the
1322
+ * container format from its magic bytes (rejecting anything that is not a
1323
+ * recognizable font), then rename it into the format-derived slot and record
1324
+ * the MIME in the config so serve/find resolve immediately. */
1325
+ async function handleFontUpload(req, res) {
1326
+ if (req.method !== "POST") {
1327
+ res.writeHead(405);
1328
+ res.end();
1329
+ return;
1330
+ }
1331
+ try {
1332
+ await ensureDir();
1333
+ const tmp = dshHomePath(DATA_DIR, FONT_UPLOAD_TMP);
1334
+ const out = createWriteStream(tmp);
1335
+ let received = 0;
1336
+ let failed = false;
1337
+ const fail = () => {
1338
+ if (failed) return;
1339
+ failed = true;
1340
+ out.destroy();
1341
+ rm(tmp, { force: true });
1342
+ };
1343
+ req.on("aborted", fail);
1344
+ req.on("error", fail);
1345
+ out.on("error", () => {
1346
+ fail();
1347
+ try {
1348
+ res.writeHead(500);
1349
+ res.end();
1350
+ } catch {}
1351
+ });
1352
+ req.on("data", (chunk) => {
1353
+ received += chunk.byteLength;
1354
+ if (received > FONT_UPLOAD_MAX) {
1355
+ req.destroy();
1356
+ fail();
1357
+ }
1358
+ });
1359
+ req.pipe(out);
1360
+ out.on("finish", async () => {
1361
+ if (failed) return;
1362
+ try {
1363
+ if (received === 0) {
1364
+ fail();
1365
+ res.writeHead(400, { "Content-Type": "application/json" });
1366
+ res.end(JSON.stringify({
1367
+ ok: false,
1368
+ error: "empty upload"
1369
+ }));
1370
+ return;
1371
+ }
1372
+ const mime = sniffFontMime(await new Promise((resolve, reject) => {
1373
+ const chunks = [];
1374
+ const s = createReadStream(tmp, {
1375
+ start: 0,
1376
+ end: 15
1377
+ });
1378
+ s.on("data", (c) => chunks.push(c));
1379
+ s.on("end", () => resolve(Buffer.concat(chunks)));
1380
+ s.on("error", reject);
1381
+ }));
1382
+ if (mime === null) {
1383
+ fail();
1384
+ res.writeHead(415, { "Content-Type": "application/json" });
1385
+ res.end(JSON.stringify({
1386
+ ok: false,
1387
+ error: "not a font (expected ttf/otf/woff/woff2)"
1388
+ }));
1389
+ return;
1390
+ }
1391
+ const target = fontPathFor(mime);
1392
+ for (const name of FONT_CANDIDATES) {
1393
+ const p = dshHomePath(DATA_DIR, name);
1394
+ if (p !== target) await rm(p, { force: true });
1395
+ }
1396
+ await rm(target, { force: true });
1397
+ await rename(tmp, target);
1398
+ const cfg = await readConfig();
1399
+ if (cfg.fontMime !== mime) {
1400
+ cfg.fontMime = mime;
1401
+ await writeConfig(cfg);
1402
+ }
1403
+ res.writeHead(200, { "Content-Type": "application/json" });
1404
+ res.end(JSON.stringify({
1405
+ ok: true,
1406
+ fontUrl: FONT_ROUTE,
1407
+ mime
1408
+ }));
1409
+ } catch (e) {
1410
+ console.error("dsh-any-background: failed to finalize the font upload", e);
1411
+ rm(tmp, { force: true });
1412
+ try {
1413
+ res.writeHead(500);
1414
+ res.end();
1415
+ } catch {}
1416
+ }
1417
+ });
1418
+ } catch (e) {
1419
+ console.error("dsh-any-background: failed to accept the font upload", e);
1420
+ try {
1421
+ res.writeHead(500);
1422
+ res.end();
1423
+ } catch {}
1424
+ }
1425
+ }
1426
+ /** Remove every stored font variant and clear the recorded MIME. */
1427
+ async function removeFontFile() {
1428
+ try {
1429
+ for (const name of FONT_CANDIDATES) await rm(dshHomePath(DATA_DIR, name), { force: true });
1430
+ await rm(dshHomePath(DATA_DIR, FONT_UPLOAD_TMP), { force: true });
1431
+ const cfg = await readConfig();
1432
+ if (cfg.fontMime !== null) {
1433
+ cfg.fontMime = null;
1434
+ await writeConfig(cfg);
1435
+ }
1436
+ return true;
1437
+ } catch (e) {
1438
+ console.error("dsh-any-background: failed to remove the custom font", e);
1439
+ return false;
1440
+ }
1441
+ }
1126
1442
  const NS = "dshAnyBackground";
1127
1443
  const RPC_CHANNEL = "/dsh-any-background";
1128
1444
  const RPC_BODY_MAX = 314572800;
@@ -1177,13 +1493,18 @@ async function handleRpcMethod(endpoint, payload) {
1177
1493
  switch (method) {
1178
1494
  case "read": {
1179
1495
  const rotated = await advanceRotationIfDue();
1496
+ const config = await readConfig();
1497
+ const firstRun = !await exists(configPath());
1498
+ if (firstRun) await writeConfig(config);
1180
1499
  return {
1181
1500
  ok: true,
1182
1501
  value: {
1183
- config: await readConfig(),
1502
+ config,
1184
1503
  wallpaperUrl: await wallpaperServeUrl(),
1185
1504
  videoUrl: await videoUrl(),
1186
- rotated
1505
+ fontUrl: await fontUrl(),
1506
+ rotated,
1507
+ firstRun
1187
1508
  }
1188
1509
  };
1189
1510
  }
@@ -1219,6 +1540,10 @@ async function handleRpcMethod(endpoint, payload) {
1219
1540
  ok: true,
1220
1541
  value: await handleRotationSet(payload)
1221
1542
  };
1543
+ case "removeFont": return {
1544
+ ok: true,
1545
+ value: await removeFontFile()
1546
+ };
1222
1547
  default: return {
1223
1548
  ok: false,
1224
1549
  error: {
@@ -1364,7 +1689,17 @@ function apply(ctx) {
1364
1689
  path: WALLPAPER_UPLOAD_ROUTE,
1365
1690
  handler: fenceUpload(handleWallpaperUpload)
1366
1691
  }), "dsh-any-background: wallpaper upload route");
1692
+ webCtx.effect(() => webCtx.webServer.register({
1693
+ kind: "prefix",
1694
+ path: FONT_ROUTE,
1695
+ handler: serveFont
1696
+ }), "dsh-any-background: font route");
1697
+ webCtx.effect(() => webCtx.webServer.register({
1698
+ kind: "exact",
1699
+ path: FONT_UPLOAD_ROUTE,
1700
+ handler: fenceUpload(handleFontUpload)
1701
+ }), "dsh-any-background: font upload route");
1367
1702
  });
1368
1703
  }
1369
1704
  //#endregion
1370
- export { apply, inject, name };
1705
+ export { DEFAULT_CONFIG, apply, inject, name };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-any-background",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Appearance plugin for DeepSeek Harness: custom theme color (PS-style color wheel), background wallpaper with opacity/blur controls, and separate main/settings interface opacity.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -103,6 +103,7 @@
103
103
  },
104
104
  "dsh": {
105
105
  "compatibility": {
106
+ "dsh": "0.1.5-rc.2",
106
107
  "dshReleases": {
107
108
  "0.1.5-rc.2": "compatible"
108
109
  }