1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
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");
}
|