// 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 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"); }