// Copyright (C) 2026 Danilo M. // // 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"); }