diff options
Diffstat (limited to 'desktop/modules/assistant/Config.js')
| -rw-r--r-- | desktop/modules/assistant/Config.js | 142 |
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"); +} |
