diff options
Diffstat (limited to 'desktop/island')
| -rw-r--r-- | desktop/island/SceneIcons.js | 77 | ||||
| -rw-r--r-- | desktop/island/test_scene_icons.js | 62 |
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"); |
