aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules/assistant/Config.js
diff options
context:
space:
mode:
Diffstat (limited to 'desktop/modules/assistant/Config.js')
-rw-r--r--desktop/modules/assistant/Config.js142
1 files changed, 142 insertions, 0 deletions
diff --git a/desktop/modules/assistant/Config.js b/desktop/modules/assistant/Config.js
new file mode 100644
index 0000000..c30ea8b
--- /dev/null
+++ b/desktop/modules/assistant/Config.js
@@ -0,0 +1,142 @@
+// 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
+
+// Pure helpers for desktop-assistant's config.toml, tested by
+// test_config.js under node.
+//
+// The file is edited line by line so its comments survive: the example it
+// is copied from documents every key in place. Only the shapes this page
+// writes are understood: top-level `key = value` before the first table,
+// and `KEY = "value"` lines in [env]. A top-level key that is unset sits
+// commented out (`# key = value`), which is how the example ships, so
+// setting one activates its line and unsetting comments it back out.
+// ponytail: no multi-line strings or arrays; the page never writes one,
+// and Save validates the whole file with tomllib before writing.
+
+const KEY = /^(\s*)(#\s*)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/;
+const HEADER = /^\s*\[/;
+
+function quote(s) {
+ return '"' + String(s).replace(/[\\"]/g, c => "\\" + c)
+ .replace(/\n/g, "\\n").replace(/\t/g, "\\t").replace(/\r/g, "\\r") + '"';
+}
+
+// The value at the start of rest and where it ends, so what follows (an
+// inline comment) can be kept.
+function value(rest) {
+ if (rest.startsWith('"')) {
+ let out = "";
+ for (let i = 1; i < rest.length; i++) {
+ const c = rest[i];
+ if (c === '"') return { v: out, end: i + 1 };
+ if (c === "\\" && i + 1 < rest.length) {
+ const n = rest[++i];
+ out += n === "n" ? "\n" : n === "t" ? "\t" : n === "r" ? "\r" : n;
+ } else out += c;
+ }
+ return { v: out, end: rest.length };
+ }
+ const m = /^[^\s#]*/.exec(rest)[0];
+ const v = m === "true" ? true : m === "false" ? false
+ : m !== "" && !isNaN(Number(m)) ? Number(m) : m;
+ return { v: v, end: m.length };
+}
+
+function firstHeader(lines) {
+ const i = lines.findIndex(l => HEADER.test(l) && !/^\s*#/.test(l));
+ return i < 0 ? lines.length : i;
+}
+
+// {key: value} for the active top-level keys among keys.
+function scalars(text, keys) {
+ const lines = text.split("\n");
+ const out = {};
+ for (let i = 0; i < firstHeader(lines); i++) {
+ const m = KEY.exec(lines[i]);
+ if (m && !m[2] && keys.includes(m[3])) out[m[3]] = value(m[4]).v;
+ }
+ return out;
+}
+
+// toml is the value already written as TOML (quote() for a string), or null
+// to unset.
+function setScalar(text, key, toml) {
+ const lines = text.split("\n");
+ const end = firstHeader(lines);
+ let commented = -1;
+ for (let i = 0; i < end; i++) {
+ const m = KEY.exec(lines[i]);
+ if (!m || m[3] !== key) continue;
+ if (!m[2]) {
+ const tail = m[4].slice(value(m[4]).end);
+ lines[i] = toml === null ? m[1] + "# " + key + " = " + m[4]
+ : m[1] + key + " = " + toml + tail;
+ return lines.join("\n");
+ }
+ if (commented < 0) commented = i;
+ }
+ if (toml === null) return text;
+ if (commented >= 0) {
+ const m = KEY.exec(lines[commented]);
+ lines[commented] = m[1] + key + " = " + toml + m[4].slice(value(m[4]).end);
+ } else {
+ // Before the blank line that separates the first table, if any.
+ const at = end > 0 && lines[end - 1].trim() === "" ? end - 1 : end;
+ lines.splice(at, 0, key + " = " + toml);
+ }
+ return lines.join("\n");
+}
+
+function envRange(lines) {
+ const h = lines.findIndex(l => /^\s*\[env\]\s*(#.*)?$/.test(l));
+ if (h < 0) return null;
+ let e = lines.findIndex((l, i) => i > h && HEADER.test(l));
+ return { h: h, end: e < 0 ? lines.length : e };
+}
+
+// [{key, value}] for the active lines of [env], in file order.
+function env(text) {
+ const lines = text.split("\n");
+ const r = envRange(lines);
+ if (!r) return [];
+ const out = [];
+ for (let i = r.h + 1; i < r.end; i++) {
+ const m = KEY.exec(lines[i]);
+ if (m && !m[2]) out.push({ key: m[3], value: String(value(m[4]).v) });
+ }
+ return out;
+}
+
+// value is a plain string, quoted here; null removes the key.
+function setEnv(text, key, val) {
+ const lines = text.split("\n");
+ const r = envRange(lines);
+ if (!r) {
+ if (val === null) return text;
+ return text.replace(/\n*$/, "") + "\n\n[env]\n" + key + " = " + quote(val) + "\n";
+ }
+ let last = r.h;
+ for (let i = r.h + 1; i < r.end; i++) {
+ const m = KEY.exec(lines[i]);
+ if (!m || m[2]) continue;
+ if (m[3] === key) {
+ if (val === null) lines.splice(i, 1);
+ else lines[i] = m[1] + key + " = " + quote(val) + m[4].slice(value(m[4]).end);
+ return lines.join("\n");
+ }
+ last = i;
+ }
+ if (val === null) return text;
+ lines.splice(last + 1, 0, key + " = " + quote(val));
+ return lines.join("\n");
+}