Enhance installation process with terminal selection, update autostart commands, and add PolyForm license; remove unused AGS config

This commit is contained in:
2026-07-06 07:02:34 +02:00
parent 3ee00f2de3
commit ed0a09bfcc
14 changed files with 274 additions and 30 deletions
+21 -3
View File
@@ -2,6 +2,7 @@ import * as p from "@clack/prompts";
import { groups, packages, toDirs } from "./packages.js";
import { detectGpuVendors, driverPackagesFor, type GpuVendor } from "./gpu.js";
import {
autostartCmdsFor,
backupConflicts,
ensureSddmEnabled,
ensureSudoSession,
@@ -15,9 +16,11 @@ import {
writeLocaleConfig,
writeNvidiaEnvConfig,
writeState,
writeTerminalConfig,
} from "./stow.js";
const CANCELLED = Symbol("cancelled");
const TERMINAL_CANDIDATES = ["kitty", "foot"];
async function selectGroupPackages(chosen: Set<string>): Promise<typeof CANCELLED | void> {
for (const group of groups) {
@@ -63,6 +66,20 @@ async function selectGpuDriverPackages(chosen: Set<string>): Promise<GpuVendor[]
return vendors.filter((vendor) => driverPackagesFor([vendor]).some((pkg) => picked.includes(pkg)));
}
/** null means no terminal was chosen — the SUPER+T keybind is skipped. */
async function selectMainTerminal(chosen: Set<string>): Promise<string | null | typeof CANCELLED> {
const candidates = TERMINAL_CANDIDATES.filter((name) => chosen.has(name));
if (candidates.length === 0) return null;
if (candidates.length === 1) return candidates[0];
const picked = await p.select({
message: "Choose your main terminal (bound to SUPER+T)",
options: candidates.map((name) => ({ value: name, label: name })),
});
if (p.isCancel(picked)) return CANCELLED;
return picked;
}
async function maybeReboot(yes: boolean) {
if (yes) {
p.log.info("Rebooting now...");
@@ -85,6 +102,8 @@ export async function install({ yes = false }: { yes?: boolean } = {}) {
if ((await selectGroupPackages(chosen)) === CANCELLED) return p.cancel("Install cancelled.");
if ((await selectRecommendedPackages(chosen)) === CANCELLED) return p.cancel("Install cancelled.");
const mainTerminal = await selectMainTerminal(chosen);
if (mainTerminal === CANCELLED) return p.cancel("Install cancelled.");
const keptGpuVendors = await selectGpuDriverPackages(chosen);
if (keptGpuVendors === CANCELLED) return p.cancel("Install cancelled.");
@@ -107,10 +126,9 @@ export async function install({ yes = false }: { yes?: boolean } = {}) {
if (keptGpuVendors.includes("nvidia")) writeNvidiaEnvConfig();
const autostartCmds = ["waybar", "ags", "mako", "hyprpaper"].filter((name) => chosen.has(name));
if (chosen.has("hyprlauncher")) autostartCmds.push("hyprlauncher -d");
writeAutostartConfig(autostartCmds);
writeAutostartConfig(autostartCmdsFor([...chosen]));
writeLocaleConfig();
if (mainTerminal) writeTerminalConfig(mainTerminal);
const dirs = toDirs([...chosen]);
const backupDir = backupConflicts(dirs);
-1
View File
@@ -15,7 +15,6 @@ export const packages: Pkg[] = [
{ name: "stow", tier: "required" },
{ name: "ttf-0xproto-nerd", tier: "required" },
{ name: "waybar", tier: "recommended", group: "bar" },
{ name: "ags", tier: "recommended", group: "bar", aur: true },
{ name: "hyprlauncher", tier: "recommended", group: "launcher" },
{ name: "kitty", tier: "recommended" },
{ name: "foot", tier: "recommended" },
+43 -8
View File
@@ -1,4 +1,4 @@
import { execFileSync } from "node:child_process";
import { execFileSync, spawn } from "node:child_process";
import {
existsSync,
lstatSync,
@@ -36,13 +36,22 @@ export function run(cmd: string, args: string[], options: RunOptions = {}) {
export function ensureSudoSession() {
execFileSync("sudo", ["-v"], { stdio: "inherit" });
setInterval(() => {
try {
execFileSync("sudo", ["-n", "-v"]);
} catch {
// credential expired or revoked; the next real sudo call will re-prompt
}
}, 60_000).unref();
// A JS setInterval can't refresh this during a long synchronous step (e.g. makepkg's
// multi-minute build in ensureYay) — execFileSync blocks the entire single-threaded event
// loop, so the timer callback never gets a chance to run and the ticket silently expires
// mid-build. Spawn the refresh loop as a real child process instead, so it keeps running
// regardless of what our own process is blocked on. It's deliberately NOT `detached` —
// sudo's default tty-scoped ticket is tied to the session/controlling-tty, and `detached`
// calls setsid() on Linux, which moves the child to a new session and makes its `sudo -n -v`
// fail to match our ticket (verified empirically: a setsid'd sudo call can't reuse a ticket
// established outside it, while a plain background child in the same session can). It exits
// on its own once this process (checked via `kill -0`) is gone.
const child = spawn(
"sh",
["-c", `while kill -0 ${process.pid} 2>/dev/null; do sleep 60; sudo -n -v 2>/dev/null; done`],
{ stdio: "ignore" },
);
child.unref();
}
function commandExists(cmd: string): boolean {
@@ -60,6 +69,14 @@ export function ensureYay() {
const tmp = mkdtempSync(join(tmpdir(), "yay-bootstrap-"));
try {
run("git", ["clone", "https://aur.archlinux.org/yay.git", tmp]);
// makepkg's own root pacman calls are always `sudo -k ...` (confirmed by reading
// /usr/bin/makepkg) — per sudo(8), `-k` combined with a command deliberately ignores any
// cached credential and forces a fresh password prompt. This is upstream makepkg's own
// design, not something ensureSudoSession's ticket caching can avoid, and it happens once
// per root escalation makepkg performs (dependency install, then package install), so a
// clean install (yay never already present) will ask for the password again here — possibly
// more than once.
console.log("Building yay from source (one-time) — you may be asked for your password again during this step.");
run("makepkg", ["-si", "--noconfirm"], { cwd: tmp });
} finally {
rmSync(tmp, { recursive: true, force: true });
@@ -136,6 +153,24 @@ export function writeAutostartConfig(execCmds: string[]) {
writeFileSync(join(dir, "autostart.lua"), `hl.on("hyprland.start", function()\n${body}\nend)\n`);
}
// Autostart commands for whichever of these package names are present. Assumes chosen
// package names == state.dirs (true today: no package sets a custom `dir`). If a future
// package needs dir != name AND is autostart-relevant, store chosen names in state.json too.
export function autostartCmdsFor(names: string[]): string[] {
const set = new Set(names);
const cmds = ["waybar", "mako", "hyprpaper"].filter((name) => set.has(name));
if (set.has("hyprlauncher")) cmds.push("hyprlauncher -d");
return cmds;
}
// SUPER+T's target — the static hyprland.lua no longer hardcodes a terminal, since kitty/foot
// are both optional and either, both, or neither may be installed.
export function writeTerminalConfig(terminal: string) {
const dir = hyprConfD();
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, "terminal.lua"), `hl.bind("SUPER + T", hl.dsp.exec_cmd("${terminal}"))\n`);
}
export function writeLocaleConfig() {
const dir = hyprConfD();
mkdirSync(dir, { recursive: true });
+2 -1
View File
@@ -1,5 +1,5 @@
import * as p from "@clack/prompts";
import { ensureSudoSession, readState, restow, run } from "./stow.js";
import { autostartCmdsFor, ensureSudoSession, readState, restow, run, writeAutostartConfig } from "./stow.js";
export async function update({ yes = false }: { yes?: boolean } = {}) {
p.intro("Hyprland dotfiles update");
@@ -18,5 +18,6 @@ export async function update({ yes = false }: { yes?: boolean } = {}) {
run("sudo", ["pacman", "-Syu"]);
run("yay", ["-Syu"]);
restow(dirs);
writeAutostartConfig(autostartCmdsFor(dirs));
p.outro("Done.");
}