aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/island
diff options
context:
space:
mode:
Diffstat (limited to 'desktop/island')
-rw-r--r--desktop/island/SceneIcons.js77
-rw-r--r--desktop/island/test_scene_icons.js62
2 files changed, 139 insertions, 0 deletions
diff --git a/desktop/island/SceneIcons.js b/desktop/island/SceneIcons.js
new file mode 100644
index 0000000..28118b7
--- /dev/null
+++ b/desktop/island/SceneIcons.js
@@ -0,0 +1,77 @@
+// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+.pragma library
+
+// The starter map, used when the config file is missing or unreadable, and the
+// seed for the settings page. The md-* glyphs are supplementary-plane, so they
+// are surrogate pairs; a five-digit \u escape would be a different character.
+var defaults = {
+ generic: "\udb80\uded7", // md-help_circle
+ scenes: {
+ "Desktop": "\udb80\udf79", // md-monitor
+ "Webcam Only": "\udb81\udd67", // md-video
+ "Window Only": "\uf2d0", // fa-window_maximize
+ "Intervallo": "\udb80\udd76", // md-coffee
+ },
+};
+
+// Exactly one code point; a surrogate pair counts as one.
+function validGlyph(s) {
+ if (typeof s !== "string" || s === "") return false;
+ let n = 0;
+ for (let i = 0; i < s.length; i++) {
+ const c = s.charCodeAt(i);
+ if (c >= 0xD800 && c <= 0xDBFF && i + 1 < s.length) {
+ const d = s.charCodeAt(i + 1);
+ if (d >= 0xDC00 && d <= 0xDFFF) i++;
+ }
+ n++;
+ }
+ return n === 1;
+}
+
+// The map as stored: an invalid generic falls back, invalid scenes drop.
+function normalize(map) {
+ const src = map || {};
+ const out = {
+ generic: validGlyph(src.generic) ? src.generic : defaults.generic,
+ scenes: {},
+ };
+ const sc = src.scenes || {};
+ for (const k in sc) if (validGlyph(sc[k])) out.scenes[k] = sc[k];
+ return out;
+}
+
+// A missing or unparseable file is the starter map; a valid file is used as-is.
+function parse(text) {
+ let v = null;
+ try { v = JSON.parse(text); } catch (e) {}
+ if (!v || typeof v !== "object" || Array.isArray(v)) return normalize(defaults);
+ return normalize(v);
+}
+
+function serialize(map) {
+ return JSON.stringify({ generic: map.generic, scenes: map.scenes }, null, 2) + "\n";
+}
+
+function iconFor(map, name) {
+ return (map && map.scenes && map.scenes[name]) || (map && map.generic) || defaults.generic;
+}
+
+// A draft may leave the generic empty; Save substitutes the default.
+function validMap(map) {
+ if (!map || typeof map !== "object") return false;
+ if (map.generic !== "" && !validGlyph(map.generic)) return false;
+ const sc = map.scenes || {};
+ for (const k in sc) if (!validGlyph(sc[k])) return false;
+ return true;
+}
diff --git a/desktop/island/test_scene_icons.js b/desktop/island/test_scene_icons.js
new file mode 100644
index 0000000..f09efdf
--- /dev/null
+++ b/desktop/island/test_scene_icons.js
@@ -0,0 +1,62 @@
+// Run: node desktop/island/test_scene_icons.js
+//
+// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// SceneIcons.js is a QML library, so its .pragma line is stripped before node
+// evaluates it.
+
+const fs = require("fs");
+const assert = require("assert");
+
+const src = fs.readFileSync(__dirname + "/SceneIcons.js", "utf8")
+ .replace(/^\.pragma library$/m, "");
+const I = new Function(src +
+ "\nreturn { defaults, validGlyph, validMap, normalize, parse, serialize, iconFor };")();
+
+// One code point, even when it is a surrogate pair (U+F02DC, md-home).
+assert.strictEqual(I.validGlyph("\u{f02dc}"), true);
+assert.strictEqual(I.validGlyph("a"), true);
+assert.strictEqual(I.validGlyph(""), false);
+assert.strictEqual(I.validGlyph("ab"), false);
+
+// A missing or unparseable file is the starter map, not an empty one.
+assert.deepStrictEqual(I.parse(""), I.defaults);
+assert.deepStrictEqual(I.parse("not json"), I.defaults);
+
+// A valid file is used as-is: no merge with the defaults.
+const one = I.parse('{"generic":"\\udb80\\uded7","scenes":{"Desktop":"\\udb80\\udf79"}}');
+assert.deepStrictEqual(one, { generic: "\udb80\uded7", scenes: { Desktop: "\udb80\udf79" } });
+assert.strictEqual(I.iconFor(one, "Desktop"), "\udb80\udf79");
+assert.strictEqual(I.iconFor(one, "Webcam Only"), "\udb80\uded7");
+
+// Invalid values are dropped; the generic falls back.
+const bad = I.parse('{"generic":"","scenes":{"A":"ab","B":"\\udb80\\udf79"}}');
+assert.deepStrictEqual(bad, { generic: I.defaults.generic, scenes: { B: "\udb80\udf79" } });
+
+// A non-string value is dropped, never passed to charCodeAt.
+assert.deepStrictEqual(I.parse('{"scenes":{"A":42}}'),
+ { generic: I.defaults.generic, scenes: {} });
+
+// normalize is what Save writes: empty generic -> default, invalid scene dropped.
+assert.deepStrictEqual(
+ I.normalize({ generic: "", scenes: { A: I.defaults.scenes.Desktop, B: "ab" } }),
+ { generic: I.defaults.generic, scenes: { A: I.defaults.scenes.Desktop } });
+
+// Whole-draft validity: an empty generic is allowed (Save substitutes the default).
+assert.strictEqual(I.validMap({ generic: "", scenes: { A: "\u{f02dc}" } }), true);
+assert.strictEqual(I.validMap({ generic: I.defaults.generic, scenes: { A: "ab" } }), false);
+
+// Round-trip.
+assert.deepStrictEqual(I.parse(I.serialize(one)), one);
+assert.ok(I.serialize(one).endsWith("\n"));
+
+console.log("ok");