Add Hyprland skill and configuration files; enhance installation process with GPU detection and SDDM support
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
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 { detectGpuVendors, driverPackagesFor, type GpuVendor } from "../src/gpu.js";
|
||||
|
||||
function withFakeLspci(output: string, fn: () => void) {
|
||||
const dir = mkdtempSync(join(tmpdir(), "fake-lspci-"));
|
||||
writeFileSync(join(dir, "lspci"), `#!/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("detects an NVIDIA + Intel machine", () => {
|
||||
withFakeLspci(
|
||||
`00:02.0 VGA compatible controller: Intel Corporation blah (rev 0a)
|
||||
01:00.0 VGA compatible controller: NVIDIA Corporation GA104 [GeForce RTX 3070] (rev a1)
|
||||
01:00.1 Audio device: NVIDIA Corporation GA104 High Definition Audio Controller (rev a1)`,
|
||||
() => {
|
||||
assert.deepStrictEqual(detectGpuVendors().sort(), ["intel", "nvidia"]);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("detects an Intel + NVIDIA hybrid/Optimus laptop", () => {
|
||||
withFakeLspci(
|
||||
`00:02.0 VGA compatible controller: Intel Corporation TigerLake-LP GT2 [Iris Xe Graphics]
|
||||
01:00.0 3D controller: NVIDIA Corporation GA107M [GeForce RTX 3050 Mobile]`,
|
||||
() => {
|
||||
assert.deepStrictEqual(detectGpuVendors().sort(), ["intel", "nvidia"]);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("detects an AMD-only machine without false-positiving on 'Corporation'", () => {
|
||||
// Regression test: /amd|ati/i used to match the "ati" inside "Corporation",
|
||||
// flagging every vendor line (including Intel/NVIDIA ones) as AMD.
|
||||
withFakeLspci(
|
||||
`03:00.0 VGA compatible controller: Advanced Micro Devices, Inc. [AMD/ATI] Navi 23 [Radeon RX 6600]`,
|
||||
() => {
|
||||
assert.deepStrictEqual(detectGpuVendors(), ["amd"]);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("does not false-positive AMD on an NVIDIA-only 'Corporation' line", () => {
|
||||
withFakeLspci(
|
||||
`01:00.0 VGA compatible controller: NVIDIA Corporation GA104 [GeForce RTX 3070]`,
|
||||
() => {
|
||||
assert.deepStrictEqual(detectGpuVendors(), ["nvidia"]);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("returns no vendors when lspci reports no controllers", () => {
|
||||
withFakeLspci("", () => {
|
||||
assert.deepStrictEqual(detectGpuVendors(), []);
|
||||
});
|
||||
});
|
||||
|
||||
test("returns no vendors when lspci is missing entirely", () => {
|
||||
const originalPath = process.env.PATH;
|
||||
process.env.PATH = "/nonexistent";
|
||||
try {
|
||||
assert.deepStrictEqual(detectGpuVendors(), []);
|
||||
} finally {
|
||||
process.env.PATH = originalPath;
|
||||
}
|
||||
});
|
||||
|
||||
test("driverPackagesFor maps each vendor to its package list", () => {
|
||||
assert.deepStrictEqual(driverPackagesFor(["amd"]), ["mesa", "vulkan-radeon", "libva-mesa-driver"]);
|
||||
assert.deepStrictEqual(driverPackagesFor([]), []);
|
||||
const vendors: GpuVendor[] = ["nvidia", "intel"];
|
||||
assert.deepStrictEqual(driverPackagesFor(vendors), [
|
||||
"nvidia-open-dkms",
|
||||
"nvidia-utils",
|
||||
"egl-wayland",
|
||||
"mesa",
|
||||
"vulkan-intel",
|
||||
"intel-media-driver",
|
||||
]);
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert";
|
||||
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", () => {
|
||||
assert.deepStrictEqual(toDirs(["waybar", "kitty"]), ["waybar", "kitty"]);
|
||||
});
|
||||
|
||||
test("toDirs falls back to the raw name for unknown packages", () => {
|
||||
assert.deepStrictEqual(toDirs(["not-a-real-package"]), ["not-a-real-package"]);
|
||||
});
|
||||
|
||||
test("groups contains exactly the distinct group values declared on packages, no duplicates", () => {
|
||||
const expected = [...new Set(packages.filter((p) => p.group).map((p) => p.group!))];
|
||||
assert.deepStrictEqual(groups, expected);
|
||||
assert.strictEqual(new Set(groups).size, groups.length, "groups should have no duplicates");
|
||||
});
|
||||
|
||||
test("every grouped package's group appears in the groups list", () => {
|
||||
for (const pkg of packages.filter((p) => p.group)) {
|
||||
assert.ok(groups.includes(pkg.group!), `${pkg.name}'s group "${pkg.group}" missing from groups`);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
// Integration-style tests for src/stow.ts. These exercise the real `stow` binary and
|
||||
// real filesystem calls, so each test:
|
||||
// - runs against a throwaway $HOME (a fresh mkdtempSync dir), never the real one
|
||||
// - is skipped when `stow` isn't installed (confirmed missing on a bare host) —
|
||||
// run the full suite for real via `./docker/test.sh "npm test"`
|
||||
import { test, after } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync, lstatSync, mkdtempSync, readFileSync, rmSync, writeFileSync, mkdirSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
function hasStow(): boolean {
|
||||
try {
|
||||
execFileSync("which", ["stow"], { stdio: "ignore" });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const skip = !hasStow();
|
||||
|
||||
// os.homedir() reads $HOME on POSIX, so setting this before importing stow.js
|
||||
// (whose module-level consts are computed from homedir() at import time) sandboxes
|
||||
// every path the module touches, with no changes to production code.
|
||||
const fakeHome = mkdtempSync(join(tmpdir(), "dotfiles-test-home-"));
|
||||
process.env.HOME = fakeHome;
|
||||
after(() => rmSync(fakeHome, { recursive: true, force: true }));
|
||||
|
||||
const {
|
||||
backupConflicts,
|
||||
restoreLatestBackup,
|
||||
restow,
|
||||
stow,
|
||||
unstow,
|
||||
writeState,
|
||||
readState,
|
||||
} = skip ? ({} as any) : await import("../src/stow.js");
|
||||
|
||||
const dirs = ["hyprland", "waybar", "kitty", "hyprpaper"];
|
||||
const hyprlandTarget = join(fakeHome, ".config", "hypr", "hyprland.lua");
|
||||
const wallpaperTarget = join(fakeHome, ".config", "hypr", "wallpapers", "wallpaper.png");
|
||||
|
||||
test("seeds a pre-existing conflicting file", { skip }, () => {
|
||||
mkdirSync(join(fakeHome, ".config", "hypr"), { recursive: true });
|
||||
writeFileSync(hyprlandTarget, "old config\n");
|
||||
assert.strictEqual(readFileSync(hyprlandTarget, "utf8").trim(), "old config");
|
||||
});
|
||||
|
||||
test("backupConflicts moves the pre-existing file out of the way", { skip }, () => {
|
||||
const backupDir = backupConflicts(dirs);
|
||||
assert.ok(backupDir, "expected a backup dir since a conflict existed");
|
||||
assert.ok(!existsSync(hyprlandTarget), "conflicting file should have been moved out of the way");
|
||||
});
|
||||
|
||||
test("stow symlinks the chosen packages into $HOME", { skip }, () => {
|
||||
stow(dirs);
|
||||
assert.ok(lstatSync(hyprlandTarget).isSymbolicLink(), "hyprland.lua should now be a symlink");
|
||||
assert.ok(existsSync(wallpaperTarget), "wallpaper.png should be reachable through the stowed dir");
|
||||
});
|
||||
|
||||
test("writeState/readState round-trip the chosen dirs", { skip }, () => {
|
||||
writeState({ dirs });
|
||||
assert.deepStrictEqual(readState().dirs, dirs);
|
||||
});
|
||||
|
||||
test("restow keeps the symlinks intact", { skip }, () => {
|
||||
restow(dirs);
|
||||
assert.ok(lstatSync(hyprlandTarget).isSymbolicLink(), "restow should keep the symlink");
|
||||
assert.ok(existsSync(wallpaperTarget), "restow should keep wallpaper reachable");
|
||||
});
|
||||
|
||||
test("unstow removes the symlinks", { skip }, () => {
|
||||
unstow(dirs);
|
||||
assert.ok(
|
||||
!existsSync(hyprlandTarget) || !lstatSync(hyprlandTarget).isSymbolicLink(),
|
||||
"unstow should remove the symlink",
|
||||
);
|
||||
assert.ok(!existsSync(wallpaperTarget), "unstow should remove the wallpaper symlink");
|
||||
});
|
||||
|
||||
test("restoreLatestBackup brings back the original file content", { skip }, () => {
|
||||
const restored = restoreLatestBackup();
|
||||
assert.ok(restored, "expected a backup to restore");
|
||||
assert.strictEqual(readFileSync(hyprlandTarget, "utf8").trim(), "old config", "original content should be restored");
|
||||
});
|
||||
Reference in New Issue
Block a user