aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules/autostart/Autostart.js
diff options
context:
space:
mode:
Diffstat (limited to 'desktop/modules/autostart/Autostart.js')
-rw-r--r--desktop/modules/autostart/Autostart.js129
1 files changed, 129 insertions, 0 deletions
diff --git a/desktop/modules/autostart/Autostart.js b/desktop/modules/autostart/Autostart.js
new file mode 100644
index 0000000..81510d7
--- /dev/null
+++ b/desktop/modules/autostart/Autostart.js
@@ -0,0 +1,129 @@
+// 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 the autostart page, tested by test_autostart.js under node.
+//
+// autostart.lua is walked line by line. A line that is exactly
+// `hl.exec_cmd("...")` is an entry, the same behind `-- ` is a disabled entry,
+// and every other line passes through untouched. Each edit returns the new
+// file text, or null when the line it targets is no longer what was parsed
+// (the file changed underneath), so a stale page never edits the wrong line.
+
+const RE = /^(\s*)(-- ?)?hl\.exec_cmd\("((?:[^"\\]|\\.)*)"\)\s*$/;
+
+// ponytail: only \" and \\ are decoded, the two escapes the file uses; a
+// \n would show as n. Fine while commands are typed on one line.
+function unquote(s) { return s.replace(/\\(.)/g, "$1"); }
+function quote(s) { return s.replace(/[\\"]/g, c => "\\" + c); }
+
+// A command's leading `[rules]`, as hl.exec_cmd takes them, and the rest.
+function split(cmd) {
+ const m = /^\[([^\]]*)\]\s*(.*)$/.exec(cmd);
+ return m ? { rule: m[1].trim(), body: m[2] } : { rule: "", body: cmd };
+}
+
+// {ws, silent} for no rule or a lone workspace rule; null for any other rule,
+// which the page shows as is and does not offer to edit.
+function workspace(rule) {
+ if (rule === "") return { ws: "", silent: false };
+ const m = /^workspace\s+(\S+)(\s+silent)?$/.exec(rule);
+ return m ? { ws: m[1], silent: !!m[2] } : null;
+}
+
+function compose(ws, silent, body) {
+ return ws ? "[workspace " + ws + (silent ? " silent" : "") + "] " + body : body;
+}
+
+// Shell words, with quotes and backslashes resolved. ponytail: no $(...),
+// globs or operators beyond splitting at them; enough to find a program.
+function words(s) {
+ const out = [];
+ let cur = null, q = "";
+ for (let i = 0; i < s.length; i++) {
+ const c = s[i];
+ if (q) {
+ if (c === q) q = "";
+ else if (c === "\\" && q === '"' && i + 1 < s.length) cur += s[++i];
+ else cur += c;
+ } else if (c === "'" || c === '"') { q = c; cur = cur ?? ""; }
+ else if (c === "\\" && i + 1 < s.length) cur = (cur ?? "") + s[++i];
+ else if (/\s/.test(c) || ";&|<>".includes(c)) {
+ if (cur !== null) out.push(cur);
+ cur = null;
+ if (!/\s/.test(c)) out.push(c);
+ } else cur = (cur ?? "") + c;
+ }
+ if (cur !== null) out.push(cur);
+ return out;
+}
+
+// The program a command line runs, for a PATH check: past a `[rules]`
+// prefix, VAR=value assignments, env and exec, and into `sh -c '...'`.
+// `~/` is expanded against home. Empty if there is none.
+function program(cmd, home) {
+ const w = words(split(cmd).body);
+ let i = 0;
+ while (i < w.length && (/^[A-Za-z_][A-Za-z0-9_]*=/.test(w[i]) || w[i] === "env" || w[i] === "exec")) i++;
+ const p = w[i] ?? "";
+ if (/^(ba|z|da)?sh$/.test(p.replace(/.*\//, "")) && w[i + 1] === "-c" && w[i + 2] !== undefined)
+ return program(w[i + 2], home);
+ return p.startsWith("~/") ? home + p.slice(1) : p;
+}
+
+// A .desktop Exec line as a command: the field codes (%U, %f, ...) dropped,
+// %% kept as a literal %.
+function fromExec(exec) {
+ return exec.replace(/%%/g, "\0").replace(/\s*%[a-zA-Z]/g, "").replace(/\0/g, "%").trim();
+}
+
+function line(indent, cmd, enabled) {
+ return indent + (enabled ? "" : "-- ") + 'hl.exec_cmd("' + quote(cmd) + '")';
+}
+
+// [{index, raw, indent, cmd, enabled}] in file order.
+function parse(text) {
+ const out = [];
+ text.split("\n").forEach((raw, index) => {
+ const m = RE.exec(raw);
+ if (m) out.push({ index, raw, indent: m[1], cmd: unquote(m[3]), enabled: !m[2] });
+ });
+ return out;
+}
+
+function edit(text, e, fn) {
+ const a = text.split("\n");
+ if (a[e.index] !== e.raw) return null;
+ fn(a);
+ return a.join("\n");
+}
+
+function toggle(text, e) {
+ return edit(text, e, a => a[e.index] = line(e.indent, e.cmd, !e.enabled));
+}
+
+function remove(text, e) {
+ return edit(text, e, a => a.splice(e.index, 1));
+}
+
+// A new enabled entry before the hook's closing `end)`, indented like the
+// last entry. Null if there is no `end)` to put it before.
+function add(text, cmd) {
+ const a = text.split("\n");
+ let end = -1;
+ for (let i = a.length - 1; i >= 0; i--)
+ if (/^end\)\s*$/.test(a[i])) { end = i; break; }
+ if (end < 0) return null;
+ const last = parse(text).pop();
+ a.splice(end, 0, line(last ? last.indent : " ", cmd, true));
+ return a.join("\n");
+}