Refactor installation process and enhance configuration management; add locale detection and autostart support
This commit is contained in:
@@ -33,7 +33,7 @@ There is no build step — `tsx` runs the TypeScript directly.
|
|||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
- `src/packages.ts` — the package model. Each entry has a `tier` (`required` installs with no prompt; `recommended` is offered via checkbox), an optional `group` for mutually-exclusive alternatives (e.g. `waybar` vs `ags` both have `group: "bar"`, only one gets picked via a select prompt), and an optional `dir` when the Stow directory name differs from the package name (e.g. package `rofi-wayland` → dir `rofi`). `toDirs()` converts chosen package names to their Stow directory names.
|
- `src/packages.ts` — the package model. Each entry has a `tier` (`required` installs with no prompt; `recommended` is offered via checkbox), an optional `group` for mutually-exclusive alternatives (e.g. `waybar` vs `ags` both have `group: "bar"`, only one gets picked via a select prompt — `launcher` currently has just `hyprlauncher` but is kept as a group so alternatives can be added the same way), and an optional `dir` for when a package's Stow directory name needs to differ from the package name. `toDirs()` converts chosen package names to their Stow directory names.
|
||||||
- `src/gpu.ts` — `detectGpuVendors()` shells out to `lspci` and matches VGA/3D controller lines against NVIDIA/AMD/Intel keywords (word-boundary regexes — a naive `/amd|ati/i` used to false-positive on the "ati" inside "Corporation"); `driverPackagesFor()` maps detected vendors to their Arch driver packages. `install.ts` shows every detected vendor's packages in a pre-checked confirm prompt (installing all of them on hybrid/Optimus laptops), and if NVIDIA is kept, `stow.ts`'s `writeNvidiaEnvConfig()` writes `~/.config/hypr/conf.d/nvidia.lua` (machine-generated, not stowed) with the Wayland env vars Hyprland's NVIDIA docs recommend.
|
- `src/gpu.ts` — `detectGpuVendors()` shells out to `lspci` and matches VGA/3D controller lines against NVIDIA/AMD/Intel keywords (word-boundary regexes — a naive `/amd|ati/i` used to false-positive on the "ati" inside "Corporation"); `driverPackagesFor()` maps detected vendors to their Arch driver packages. `install.ts` shows every detected vendor's packages in a pre-checked confirm prompt (installing all of them on hybrid/Optimus laptops), and if NVIDIA is kept, `stow.ts`'s `writeNvidiaEnvConfig()` writes `~/.config/hypr/conf.d/nvidia.lua` (machine-generated, not stowed) with the Wayland env vars Hyprland's NVIDIA docs recommend.
|
||||||
- `src/stow.ts` — all filesystem/process side effects live here: bootstrapping `yay` and oh-my-zsh/zinit if missing, installing packages via `yay`, enabling the `sddm` systemd service (`ensureSddmEnabled`), backing up real (non-symlink) files that would collide with a Stow target into `~/.dotfiles-backup/<timestamp>/`, and wrapping `stow`/`--restow`/`-D` (always invoked with an explicit `-d <PACKAGES_DIR>` so behavior doesn't depend on cwd). Also reads/writes `~/.config/dotfiles/state.json`, which records which package dirs were chosen at install time — `update` and `remove` act on this list rather than re-prompting.
|
- `src/stow.ts` — all filesystem/process side effects live here: bootstrapping `yay` and oh-my-zsh/zinit if missing, installing packages via `yay`, enabling the `sddm` systemd service (`ensureSddmEnabled`), backing up real (non-symlink) files that would collide with a Stow target into `~/.dotfiles-backup/<timestamp>/`, and wrapping `stow`/`--restow`/`-D` (always invoked with an explicit `-d <PACKAGES_DIR>` so behavior doesn't depend on cwd). Also reads/writes `~/.config/dotfiles/state.json`, which records which package dirs were chosen at install time — `update` and `remove` act on this list rather than re-prompting.
|
||||||
- `src/install.ts`, `src/update.ts`, `src/remove.ts` — the three subcommands, each a thin sequence of calls into `stow.ts`, using `@clack/prompts` for interactive selection in `install.ts`.
|
- `src/install.ts`, `src/update.ts`, `src/remove.ts` — the three subcommands, each a thin sequence of calls into `stow.ts`, using `@clack/prompts` for interactive selection in `install.ts`.
|
||||||
|
|||||||
@@ -1,22 +1,17 @@
|
|||||||
hl.monitor({ output = "", mode = "preferred", position = "auto", scale = 1 })
|
hl.monitor({ output = "", mode = "preferred", position = "auto", scale = 1 })
|
||||||
|
|
||||||
hl.on("hyprland.start", function()
|
|
||||||
hl.exec_cmd("waybar")
|
|
||||||
hl.exec_cmd("mako")
|
|
||||||
hl.exec_cmd("hyprpaper")
|
|
||||||
end)
|
|
||||||
|
|
||||||
local mainMod = "SUPER"
|
local mainMod = "SUPER"
|
||||||
|
|
||||||
hl.bind(mainMod .. " + T", hl.dsp.exec_cmd("kitty"))
|
hl.bind(mainMod .. " + T", hl.dsp.exec_cmd("kitty"))
|
||||||
hl.bind(mainMod .. " + D", hl.dsp.exec_cmd("wofi --show drun"))
|
hl.bind(mainMod .. " + D", hl.dsp.exec_cmd("hyprlauncher"))
|
||||||
hl.bind(mainMod .. " + Q", hl.dsp.window.close())
|
hl.bind(mainMod .. " + Q", hl.dsp.window.close())
|
||||||
hl.bind(mainMod .. " + M", hl.dsp.exit())
|
hl.bind(mainMod .. " + M", hl.dsp.exit())
|
||||||
|
|
||||||
hl.config({
|
hl.config({
|
||||||
input = { kb_layout = "us" },
|
|
||||||
general = { gaps_in = 4, gaps_out = 8, border_size = 2 },
|
general = { gaps_in = 4, gaps_out = 8, border_size = 2 },
|
||||||
})
|
})
|
||||||
|
|
||||||
-- optional, machine-generated, only present when install detected an NVIDIA GPU
|
-- machine-generated by src/install.ts based on chosen packages/detected hardware/locale
|
||||||
|
pcall(require, "conf.d.autostart")
|
||||||
|
pcall(require, "conf.d.locale")
|
||||||
pcall(require, "conf.d.nvidia")
|
pcall(require, "conf.d.nvidia")
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
general {
|
||||||
|
grab_focus = true
|
||||||
|
}
|
||||||
|
|
||||||
|
cache {
|
||||||
|
enabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
finders {
|
||||||
|
default_finder = desktop
|
||||||
|
}
|
||||||
|
|
||||||
|
ui {
|
||||||
|
window_size = 500 300
|
||||||
|
}
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
configuration {
|
|
||||||
modi: "drun,run";
|
|
||||||
show-icons: true;
|
|
||||||
display-drun: "Apps";
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
width=600
|
|
||||||
height=400
|
|
||||||
location=center
|
|
||||||
show=drun
|
|
||||||
prompt=Search...
|
|
||||||
+77
-41
@@ -1,18 +1,78 @@
|
|||||||
import * as p from "@clack/prompts";
|
import * as p from "@clack/prompts";
|
||||||
import { groups, packages, toDirs } from "./packages.js";
|
import { groups, packages, toDirs } from "./packages.js";
|
||||||
import { detectGpuVendors, driverPackagesFor } from "./gpu.js";
|
import { detectGpuVendors, driverPackagesFor, type GpuVendor } from "./gpu.js";
|
||||||
import {
|
import {
|
||||||
backupConflicts,
|
backupConflicts,
|
||||||
ensureSddmEnabled,
|
ensureSddmEnabled,
|
||||||
ensureSudoSession,
|
ensureSudoSession,
|
||||||
ensureYay,
|
ensureYay,
|
||||||
ensureZsh,
|
ensureZsh,
|
||||||
|
ensureZshIsDefaultShell,
|
||||||
installPackages,
|
installPackages,
|
||||||
|
run,
|
||||||
stow,
|
stow,
|
||||||
|
writeAutostartConfig,
|
||||||
|
writeLocaleConfig,
|
||||||
writeNvidiaEnvConfig,
|
writeNvidiaEnvConfig,
|
||||||
writeState,
|
writeState,
|
||||||
} from "./stow.js";
|
} from "./stow.js";
|
||||||
|
|
||||||
|
const CANCELLED = Symbol("cancelled");
|
||||||
|
|
||||||
|
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)));
|
||||||
|
}
|
||||||
|
|
||||||
|
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 } = {}) {
|
export async function install({ yes = false }: { yes?: boolean } = {}) {
|
||||||
p.intro("Hyprland dotfiles install");
|
p.intro("Hyprland dotfiles install");
|
||||||
|
|
||||||
@@ -23,42 +83,10 @@ export async function install({ yes = false }: { yes?: boolean } = {}) {
|
|||||||
|
|
||||||
const chosen = new Set(packages.filter((pkg) => pkg.tier === "required").map((pkg) => pkg.name));
|
const chosen = new Set(packages.filter((pkg) => pkg.tier === "required").map((pkg) => pkg.name));
|
||||||
|
|
||||||
for (const group of groups) {
|
if ((await selectGroupPackages(chosen)) === CANCELLED) return p.cancel("Install cancelled.");
|
||||||
const groupOptions = packages.filter((pkg) => pkg.group === group);
|
if ((await selectRecommendedPackages(chosen)) === CANCELLED) return p.cancel("Install cancelled.");
|
||||||
const pick = await p.select({
|
const keptGpuVendors = await selectGpuDriverPackages(chosen);
|
||||||
message: `Choose a ${group}`,
|
if (keptGpuVendors === CANCELLED) return p.cancel("Install cancelled.");
|
||||||
options: groupOptions.map((pkg) => ({ value: pkg.name, label: pkg.name })),
|
|
||||||
});
|
|
||||||
if (p.isCancel(pick)) return p.cancel("Install cancelled.");
|
|
||||||
chosen.add(pick as string);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 p.cancel("Install cancelled.");
|
|
||||||
for (const name of picks as string[]) chosen.add(name);
|
|
||||||
|
|
||||||
const gpuVendors = detectGpuVendors();
|
|
||||||
const gpuOptions = gpuVendors.flatMap((vendor) =>
|
|
||||||
driverPackagesFor([vendor]).map((pkg) => ({ value: pkg, label: `${vendor}: ${pkg}` })),
|
|
||||||
);
|
|
||||||
let gpuPicks: string[] = [];
|
|
||||||
if (gpuOptions.length > 0) {
|
|
||||||
const picked = await p.multiselect({
|
|
||||||
message: "Detected GPU(s) — confirm driver packages to install",
|
|
||||||
options: gpuOptions,
|
|
||||||
initialValues: gpuOptions.map((o) => o.value),
|
|
||||||
required: false,
|
|
||||||
});
|
|
||||||
if (p.isCancel(picked)) return p.cancel("Install cancelled.");
|
|
||||||
gpuPicks = picked as string[];
|
|
||||||
for (const name of gpuPicks) chosen.add(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
p.note([...chosen].join("\n"), "Packages to install");
|
p.note([...chosen].join("\n"), "Packages to install");
|
||||||
if (!yes) {
|
if (!yes) {
|
||||||
@@ -72,12 +100,18 @@ export async function install({ yes = false }: { yes?: boolean } = {}) {
|
|||||||
|
|
||||||
ensureSddmEnabled();
|
ensureSddmEnabled();
|
||||||
|
|
||||||
if (chosen.has("zsh")) ensureZsh();
|
if (chosen.has("zsh")) {
|
||||||
|
ensureZsh();
|
||||||
if (gpuVendors.includes("nvidia") && driverPackagesFor(["nvidia"]).some((pkg) => gpuPicks.includes(pkg))) {
|
ensureZshIsDefaultShell();
|
||||||
writeNvidiaEnvConfig();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
writeLocaleConfig();
|
||||||
|
|
||||||
const dirs = toDirs([...chosen]);
|
const dirs = toDirs([...chosen]);
|
||||||
const backupDir = backupConflicts(dirs);
|
const backupDir = backupConflicts(dirs);
|
||||||
if (backupDir) p.log.info(`Backed up conflicting files to ${backupDir}`);
|
if (backupDir) p.log.info(`Backed up conflicting files to ${backupDir}`);
|
||||||
@@ -85,5 +119,7 @@ export async function install({ yes = false }: { yes?: boolean } = {}) {
|
|||||||
stow(dirs);
|
stow(dirs);
|
||||||
writeState({ dirs });
|
writeState({ dirs });
|
||||||
|
|
||||||
|
await maybeReboot(yes);
|
||||||
|
|
||||||
p.outro("Done.");
|
p.outro("Done.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
|
||||||
|
export function detectKeyboardLayout(): string {
|
||||||
|
try {
|
||||||
|
const output = execFileSync("localectl", ["status"], { encoding: "utf8" });
|
||||||
|
const match = output.match(/X11 Layout:\s*(\S+)/);
|
||||||
|
if (match) return match[1];
|
||||||
|
} catch {
|
||||||
|
// localectl unavailable — fall back below
|
||||||
|
}
|
||||||
|
// ponytail: no VC-keymap fallback or multi-layout support — good enough
|
||||||
|
// for the common case of a machine with an X11 keymap already configured.
|
||||||
|
return "us";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function detectLang(): string | null {
|
||||||
|
return process.env.LANG ?? null;
|
||||||
|
}
|
||||||
+1
-2
@@ -16,8 +16,7 @@ export const packages: Pkg[] = [
|
|||||||
{ name: "ttf-0xproto-nerd", tier: "required" },
|
{ name: "ttf-0xproto-nerd", tier: "required" },
|
||||||
{ name: "waybar", tier: "recommended", group: "bar" },
|
{ name: "waybar", tier: "recommended", group: "bar" },
|
||||||
{ name: "ags", tier: "recommended", group: "bar", aur: true },
|
{ name: "ags", tier: "recommended", group: "bar", aur: true },
|
||||||
{ name: "wofi", tier: "recommended", group: "launcher" },
|
{ name: "hyprlauncher", tier: "recommended", group: "launcher" },
|
||||||
{ name: "rofi-wayland", dir: "rofi", tier: "recommended", group: "launcher", aur: true },
|
|
||||||
{ name: "kitty", tier: "recommended" },
|
{ name: "kitty", tier: "recommended" },
|
||||||
{ name: "foot", tier: "recommended" },
|
{ name: "foot", tier: "recommended" },
|
||||||
{ name: "mako", tier: "recommended" },
|
{ name: "mako", tier: "recommended" },
|
||||||
|
|||||||
+55
-11
@@ -10,8 +10,9 @@ import {
|
|||||||
rmSync,
|
rmSync,
|
||||||
writeFileSync,
|
writeFileSync,
|
||||||
} from "node:fs";
|
} from "node:fs";
|
||||||
import { homedir, tmpdir } from "node:os";
|
import { homedir, tmpdir, userInfo } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
import { detectKeyboardLayout, detectLang } from "./locale.js";
|
||||||
|
|
||||||
const REPO_ROOT = new URL("..", import.meta.url).pathname;
|
const REPO_ROOT = new URL("..", import.meta.url).pathname;
|
||||||
const PACKAGES_DIR = join(REPO_ROOT, "packages");
|
const PACKAGES_DIR = join(REPO_ROOT, "packages");
|
||||||
@@ -19,9 +20,12 @@ const STATE_DIR = join(homedir(), ".config", "dotfiles");
|
|||||||
const STATE_FILE = join(STATE_DIR, "state.json");
|
const STATE_FILE = join(STATE_DIR, "state.json");
|
||||||
const BACKUP_ROOT = join(homedir(), ".dotfiles-backup");
|
const BACKUP_ROOT = join(homedir(), ".dotfiles-backup");
|
||||||
|
|
||||||
export function run(cmd: string, args: string[], options: { cwd?: string } = {}) {
|
type RunOptions = { cwd?: string; env?: NodeJS.ProcessEnv; stdin?: "inherit" | "ignore" };
|
||||||
|
|
||||||
|
export function run(cmd: string, args: string[], options: RunOptions = {}) {
|
||||||
|
const { stdin = "inherit", ...rest } = options;
|
||||||
try {
|
try {
|
||||||
execFileSync(cmd, args, { stdio: "inherit", ...options });
|
execFileSync(cmd, args, { stdio: [stdin, "inherit", "inherit"], ...rest });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
|
if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
|
||||||
throw new Error(`Command not found: ${cmd}. Is it installed and on PATH?`);
|
throw new Error(`Command not found: ${cmd}. Is it installed and on PATH?`);
|
||||||
@@ -70,20 +74,34 @@ export function installPackages(names: string[]) {
|
|||||||
export function ensureZsh() {
|
export function ensureZsh() {
|
||||||
const omzDir = join(homedir(), ".oh-my-zsh");
|
const omzDir = join(homedir(), ".oh-my-zsh");
|
||||||
if (!existsSync(omzDir)) {
|
if (!existsSync(omzDir)) {
|
||||||
run("sh", [
|
run(
|
||||||
|
"sh",
|
||||||
|
[
|
||||||
"-c",
|
"-c",
|
||||||
'sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended',
|
'sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended',
|
||||||
]);
|
],
|
||||||
|
{ stdin: "ignore", env: { ...process.env, CHSH: "no", RUNZSH: "no", KEEP_ZSHRC: "yes" } },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const zinitDir = join(homedir(), ".local", "share", "zinit", "zinit.git");
|
const zinitDir = join(homedir(), ".local", "share", "zinit", "zinit.git");
|
||||||
if (!existsSync(zinitDir)) {
|
if (!existsSync(zinitDir)) {
|
||||||
run("sh", [
|
run(
|
||||||
"-c",
|
"sh",
|
||||||
'sh -c "$(curl -fsSL https://raw.githubusercontent.com/zdharma-continuum/zinit/HEAD/scripts/install.sh)"',
|
["-c", 'sh -c "$(curl -fsSL https://raw.githubusercontent.com/zdharma-continuum/zinit/HEAD/scripts/install.sh)"'],
|
||||||
]);
|
{ stdin: "ignore" },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ensureZshIsDefaultShell() {
|
||||||
|
// Arch's zsh package always installs to /usr/bin/zsh and registers exactly that path in
|
||||||
|
// /etc/shells; `which zsh` can resolve to the /usr/sbin symlink instead (usr-merge), which
|
||||||
|
// doesn't literally match /etc/shells and makes chsh warn spuriously.
|
||||||
|
const zshPath = "/usr/bin/zsh";
|
||||||
|
if (process.env.SHELL === zshPath) return;
|
||||||
|
run("sudo", ["chsh", "-s", zshPath, userInfo().username]);
|
||||||
|
}
|
||||||
|
|
||||||
export function ensureSddmEnabled() {
|
export function ensureSddmEnabled() {
|
||||||
try {
|
try {
|
||||||
execFileSync("systemctl", ["is-enabled", "--quiet", "sddm"]);
|
execFileSync("systemctl", ["is-enabled", "--quiet", "sddm"]);
|
||||||
@@ -92,9 +110,16 @@ export function ensureSddmEnabled() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Machine-generated, not stowed — only written when install detects an NVIDIA GPU.
|
// Machine-generated Hyprland config lives under ~/.config/hypr/conf.d, sourced from the
|
||||||
|
// stowed hyprland.lua via pcall(require, "conf.d.<name>") — never stowed itself, since it
|
||||||
|
// varies per machine/per install run (chosen packages, detected hardware/locale).
|
||||||
|
function hyprConfD(): string {
|
||||||
|
return join(homedir(), ".config", "hypr", "conf.d");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only written when install detects an NVIDIA GPU.
|
||||||
export function writeNvidiaEnvConfig() {
|
export function writeNvidiaEnvConfig() {
|
||||||
const dir = join(homedir(), ".config", "hypr", "conf.d");
|
const dir = hyprConfD();
|
||||||
mkdirSync(dir, { recursive: true });
|
mkdirSync(dir, { recursive: true });
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
join(dir, "nvidia.lua"),
|
join(dir, "nvidia.lua"),
|
||||||
@@ -102,6 +127,25 @@ export function writeNvidiaEnvConfig() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Autostart list reflecting what was actually chosen (bar group pick, optional mako/hyprpaper) —
|
||||||
|
// the static hyprland.lua no longer hardcodes this, so deselected/uninstalled apps are never exec'd.
|
||||||
|
export function writeAutostartConfig(execCmds: string[]) {
|
||||||
|
const dir = hyprConfD();
|
||||||
|
mkdirSync(dir, { recursive: true });
|
||||||
|
const body = execCmds.map((cmd) => ` hl.exec_cmd("${cmd}")`).join("\n");
|
||||||
|
writeFileSync(join(dir, "autostart.lua"), `hl.on("hyprland.start", function()\n${body}\nend)\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writeLocaleConfig() {
|
||||||
|
const dir = hyprConfD();
|
||||||
|
mkdirSync(dir, { recursive: true });
|
||||||
|
const layout = detectKeyboardLayout();
|
||||||
|
const lang = detectLang();
|
||||||
|
const lines = [`hl.config({ input = { kb_layout = "${layout}" } })`];
|
||||||
|
if (lang) lines.push(`hl.env("LANG", "${lang}")`);
|
||||||
|
writeFileSync(join(dir, "locale.lua"), lines.join("\n") + "\n");
|
||||||
|
}
|
||||||
|
|
||||||
// Move real (non-symlink) files that would collide with a stow target out of the way.
|
// Move real (non-symlink) files that would collide with a stow target out of the way.
|
||||||
export function backupConflicts(dirs: string[]): string | null {
|
export function backupConflicts(dirs: string[]): string | null {
|
||||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { test } from "node:test";
|
||||||
|
import assert from "node:assert";
|
||||||
|
import { mkdtempSync, writeFileSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { detectKeyboardLayout, detectLang } from "../src/locale.js";
|
||||||
|
|
||||||
|
function withFakeLocalectl(output: string, fn: () => void) {
|
||||||
|
const dir = mkdtempSync(join(tmpdir(), "fake-localectl-"));
|
||||||
|
writeFileSync(join(dir, "localectl"), `#!/bin/sh\ncat <<'EOF'\n${output}\nEOF\n`, { mode: 0o755 });
|
||||||
|
const originalPath = process.env.PATH;
|
||||||
|
process.env.PATH = `${dir}:${originalPath}`;
|
||||||
|
try {
|
||||||
|
fn();
|
||||||
|
} finally {
|
||||||
|
process.env.PATH = originalPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("detectKeyboardLayout reads the X11 Layout line", () => {
|
||||||
|
withFakeLocalectl(
|
||||||
|
` System Locale: LANG=de_DE.UTF-8
|
||||||
|
VC Keymap: de-latin1
|
||||||
|
X11 Layout: de
|
||||||
|
X11 Model: pc105`,
|
||||||
|
() => {
|
||||||
|
assert.strictEqual(detectKeyboardLayout(), "de");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("detectKeyboardLayout falls back to 'us' when localectl has no X11 Layout line", () => {
|
||||||
|
withFakeLocalectl(` System Locale: LANG=en_US.UTF-8\n VC Keymap: us`, () => {
|
||||||
|
assert.strictEqual(detectKeyboardLayout(), "us");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("detectKeyboardLayout falls back to 'us' when localectl is missing entirely", () => {
|
||||||
|
const originalPath = process.env.PATH;
|
||||||
|
process.env.PATH = "/nonexistent";
|
||||||
|
try {
|
||||||
|
assert.strictEqual(detectKeyboardLayout(), "us");
|
||||||
|
} finally {
|
||||||
|
process.env.PATH = originalPath;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("detectLang reads $LANG", () => {
|
||||||
|
const original = process.env.LANG;
|
||||||
|
process.env.LANG = "de_DE.UTF-8";
|
||||||
|
try {
|
||||||
|
assert.strictEqual(detectLang(), "de_DE.UTF-8");
|
||||||
|
} finally {
|
||||||
|
if (original === undefined) delete process.env.LANG;
|
||||||
|
else process.env.LANG = original;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("detectLang returns null when $LANG is unset", () => {
|
||||||
|
const original = process.env.LANG;
|
||||||
|
delete process.env.LANG;
|
||||||
|
try {
|
||||||
|
assert.strictEqual(detectLang(), null);
|
||||||
|
} finally {
|
||||||
|
if (original !== undefined) process.env.LANG = original;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -2,10 +2,6 @@ import { test } from "node:test";
|
|||||||
import assert from "node:assert";
|
import assert from "node:assert";
|
||||||
import { groups, packages, toDirs } from "../src/packages.js";
|
import { groups, packages, toDirs } from "../src/packages.js";
|
||||||
|
|
||||||
test("toDirs maps a package name to its dir when set", () => {
|
|
||||||
assert.deepStrictEqual(toDirs(["rofi-wayland"]), ["rofi"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("toDirs falls back to the package name when no dir is set", () => {
|
test("toDirs falls back to the package name when no dir is set", () => {
|
||||||
assert.deepStrictEqual(toDirs(["waybar", "kitty"]), ["waybar", "kitty"]);
|
assert.deepStrictEqual(toDirs(["waybar", "kitty"]), ["waybar", "kitty"]);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user