From 66049c965139d821747ea76960b1e91316f8110a Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Sat, 17 Oct 2026 09:54:18 +0200 Subject: desktop: a settings window, and autostart as its first page The drawer's tiles were turning into a full settings app. The drawer stays the quick area, live state and things toggled often; pages set once and left alone go to a settings window, a normal Hyprland toplevel listing them down the left. A module joins with settings: true, which also drops its tile. A gear tile and ipc call settings open|toggle reach it. Autostart edits hypr-theme's autostart.lua directly, line by line: an hl.exec_cmd line is an entry, behind -- it is disabled, everything else passes through. Edits refuse when their line moved since the parse, and writes are atomic because Hyprland reloads on every change. A wrong entry only shows at the next login, so adding is guided rather than a free text field: pick an installed app or type a command whose program must resolve on PATH before Add enables, then a workspace and silent, composed into a [workspace X silent] rule. Try runs the line through hyprctl dispatch so the rule applies as at login. Existing rows get no Try: they ran at login, and a second start stacks an instance. Co-Authored-By: Claude Opus 5.5 --- desktop/modules/autostart/Autostart.js | 129 +++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 desktop/modules/autostart/Autostart.js (limited to 'desktop/modules/autostart/Autostart.js') 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. +// +// 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"); +} -- cgit v1.2.3