144 lines
4.8 KiB
TypeScript
144 lines
4.8 KiB
TypeScript
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<string>): Promise<typeof CANCELLED | void> {
|
|
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<string>): Promise<typeof CANCELLED | void> {
|
|
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<string>): Promise<GpuVendor[] | typeof CANCELLED> {
|
|
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<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...");
|
|
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.");
|
|
}
|