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, ensureYay, ensureZsh, ensureZshIsDefaultShell, installPackages, run, stow, writeAutostartConfig, writeLocaleConfig, writeNvidiaEnvConfig, writeState, writeTerminalConfig, } from "./stow.js"; const CANCELLED = Symbol("cancelled"); const TERMINAL_CANDIDATES = ["kitty", "foot"]; async function selectGroupPackages(chosen: Set): Promise { for (const group of groups) { const groupOptions = packages.filter((pkg) => pkg.group === group); const pick = await p.select({ message: `Choose a ${group}`, options: groupOptions.map((pkg) => ({ value: pkg.name, label: pkg.name })), }); if (p.isCancel(pick)) return CANCELLED; chosen.add(pick as string); } } async function selectRecommendedPackages(chosen: Set): Promise { const ungrouped = packages.filter((pkg) => pkg.tier === "recommended" && !pkg.group); const picks = await p.multiselect({ message: "Choose recommended packages", options: ungrouped.map((pkg) => ({ value: pkg.name, label: pkg.name })), initialValues: ungrouped.map((pkg) => pkg.name), required: false, }); if (p.isCancel(picks)) return CANCELLED; for (const name of picks as string[]) chosen.add(name); } /** Returns the vendors whose driver packages the user kept (added to `chosen`), or CANCELLED. */ async function selectGpuDriverPackages(chosen: Set): Promise { const vendors = detectGpuVendors(); const options = vendors.flatMap((vendor) => driverPackagesFor([vendor]).map((pkg) => ({ value: pkg, label: `${vendor}: ${pkg}` })), ); if (options.length === 0) return vendors; const picked = await p.multiselect({ message: "Detected GPU(s) — confirm driver packages to install", options, initialValues: options.map((o) => o.value), required: false, }); if (p.isCancel(picked)) return CANCELLED; for (const name of picked) chosen.add(name); 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): Promise { 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..."); run("sudo", ["reboot"]); return; } const reboot = await p.confirm({ message: "Reboot now to apply changes?" }); if (!p.isCancel(reboot) && reboot) run("sudo", ["reboot"]); } export async function install({ yes = false }: { yes?: boolean } = {}) { p.intro("Hyprland dotfiles install"); p.log.warn( "This installer assumes a clean Arch Linux system. Running it elsewhere, " + "or on an already-configured machine, may back up and replace existing config.", ); const chosen = new Set(packages.filter((pkg) => pkg.tier === "required").map((pkg) => pkg.name)); 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."); p.note([...chosen].join("\n"), "Packages to install"); if (!yes) { const proceed = await p.confirm({ message: "Proceed?" }); if (p.isCancel(proceed) || !proceed) return p.cancel("Install cancelled."); } ensureSudoSession(); ensureYay(); installPackages([...chosen]); ensureSddmEnabled(); if (chosen.has("zsh")) { ensureZsh(); ensureZshIsDefaultShell(); } if (keptGpuVendors.includes("nvidia")) writeNvidiaEnvConfig(); writeAutostartConfig(autostartCmdsFor([...chosen])); writeLocaleConfig(); if (mainTerminal) writeTerminalConfig(mainTerminal); const dirs = toDirs([...chosen]); const backupDir = backupConflicts(dirs); if (backupDir) p.log.info(`Backed up conflicting files to ${backupDir}`); stow(dirs); writeState({ dirs }); await maybeReboot(yes); p.outro("Done."); }