Compare commits

...
25 Commits
Author SHA1 Message Date
ahmza 9abc47a8e1 attempt to update the screen settings 2026-08-20 11:38:09 +01:00
ahmza d63409dad8 updated to new path for lidswitch handling 2026-08-20 11:30:33 +01:00
ahmza 66f9e08617 correct moonlight package 2026-08-20 11:27:58 +01:00
ahmza 36b1f78886 added moonlight 2026-08-20 11:16:03 +01:00
ahmza 15be29ba2a added yazi 2026-08-20 11:12:19 +01:00
ahmza eab86bebc7 bollock scommit 2026-08-20 11:09:24 +01:00
ahmza a0d9135695 t480: only arm the keyboard for wake, not the root hubs
Arming the built-in xHCI root hubs made them wake on hub-level events
(USB-C PD negotiation, SD card reader, internal peripherals), causing the
laptop to immediately self-wake from suspend. Drop the root-hub rule and arm
only the keyboard leaf by VID:PID; a leaf device's remote-wakeup propagates
through the hub without the hub's own wakeup flag being set.
2026-08-19 23:50:55 +01:00
ahmza d7894065f7 track cad/freecad/openscad modules referenced by t480
These modules define nixosModules.cad (and freecad/openscad) referenced by
the t480 host module list but were untracked, so the flake could not resolve
them (undefined variable 'cad'). Track them so the build evaluates.
2026-08-19 23:50:48 +01:00
ahmza 6a66db0b20 t480: wake from suspend via built-in-USB keyboard
Add a udev rule (t480UsbWakeup) that arms USB remote-wakeup for a keyboard
on the built-in xHCI controller (00:14.0), so pressing a key wakes the T480
from S3. The dock connects over the Thunderbolt-3 controller (3c:00.0), which
powers down in S3 and cannot detect a cold replug, so dock-connected input
cannot wake the machine after an unplug-triggered suspend; a built-in-port
keyboard can. Match the keyboard by VID:PID since its USB bus path is unstable.

Also wires up the logind lid-switch policy (suspend undocked, ignore docked)
for the clamshell workflow.
2026-08-19 23:39:56 +01:00
ahmza 46271e24bb mount nas shares on demand 2026-08-16 17:54:19 +01:00
ahmza 63c1037c85 enable new desktop features on shadow 2026-08-16 17:53:55 +01:00
ahmza 64cbb91aac add seat-scoped via and qmk access 2026-08-16 17:53:31 +01:00
ahmza 3220ab4300 configure et-8550 printing and scanning 2026-08-16 17:53:05 +01:00
ahmza 97b5f89182 add nextcloud and prusaslicer clients 2026-08-16 17:52:43 +01:00
ahmza 61c36b6eef add screenshot tooling to hyprland 2026-08-16 17:52:18 +01:00
ahmza 91332d7e16 drop the git-secret scaffolding, which was never finished
42c805f added the ignore rules ahead of running `git secret init`, which is
the right order - the rules should exist before any key material does. but
init was then never completed, so the repo has been sitting on a .gitsecret
containing nothing but a zero-byte paths/mapping.cfg: no keys directory, no
random_seed, no pubring, and no secrets ever registered.

nothing here is being encrypted and nothing is planned to be, so the rules
guard a workflow that does not exist. removing both halves rather than
leaving a half-initialised directory around, since the latter reads as "we
use git-secret" to anyone glancing at the tree.

the `!*.secret` negation goes with them. it only ever un-ignored the
encrypted blobs and never protected the plaintext originals, so it is not
load-bearing on its own.

nothing sensitive was present at any point, and nothing was committed.
2026-08-07 14:36:48 +01:00
ahmza a009a4d984 add a netops attrset with winbox and an unraid usb creator repack
two network/storage admin tools that only shadow needs, grouped behind one
attrset so the host list stays a list of intents rather than of packages.

winbox is a one-liner: pkgs.winbox resolves to winbox4, which nixpkgs
already builds through autoPatchelfHook against the qt/xcb stack it needs.
it does not go anywhere near nix-ld, so nothing has to be added to the
nix-ld library list for it - a comment says so, since the opposite is an
easy assumption to make about a vendored mikrotik binary. the installed
binary is WinBox, capital w, which is worth knowing at a shell prompt.

unraid-usb-creator is not in nixpkgs and upstream ships an appimage only,
so it is a repack. the appimage is a qt6/qml app - a fork of rpi-imager -
built with linuxdeploy, which means it already vendors the whole qt6 stack
plus xcb, icu, gnutls and krb5 under usr/lib. that bundle is kept verbatim
and autopatchelfed against only the handful of libraries linuxdeploy
deliberately leaves to the host: libc, libstdc++, GL, x11, fontconfig and
friends. no FHS wrapper, so no ~400mb buildFHSEnv closure. the usr/ layout
has to survive as a unit because qt.conf sitting next to the binary
declares Prefix=../ with Plugins=plugins and Qml2Imports=qml.

the non-obvious part is appendRunpaths rather than runtimeDependencies for
openssl. qt's tls backend dlopens libssl/libcrypto, so autopatchelf never
sees the dependency, and without it the app cannot fetch the release json
or stream the image. it has to be appendRunpaths because in auto-patchelf
`rpath += runtime_deps` sits inside the set-interpreter branch and so only
ever reaches executables, while `rpath.extend(append_rpaths)` runs for
every patched file. the dlopen happens from plugins/tls/libqopensslbackend
.so and glibc resolves dlopen against the calling object's DT_RUNPATH - the
wrapper's runpath is not consulted - so the path must land on the plugin
itself.

QT_QPA_PLATFORM is pinned to xcb because linuxdeploy bundled only the xcb
platform plugin; there is no libqwayland*.so anywhere in the appimage, so
this always runs through xwayland under hyprland regardless. --set-default
rather than --set, so it can still be overridden if upstream ever ships the
wayland plugin.

udisks2 is enabled from this module even though hyprland/default.nix
already enables it for udiskie. the app enumerates and unmounts removable
drives over the udisks2 d-bus api rather than poking /dev, which is how it
avoids needing root, and repeating the option keeps the module standalone
on a host with no hyprland. bool options merge on equal values, so this
costs nothing.

v1.1.0 is pinned by hash. note the tag is not returned by the github
releases api - which only lists the v2.0.x line - but the asset url that
unraid.net/download redirects to is live and matches.

verified: shadow evaluates clean, both packages build, the .desktop Exec
is rewritten from the bare name to the absolute store path, ldd on the
main binary reports nothing unresolved, and the openssl lib dir is present
on libqopensslbackend.so's runpath. not yet exercised at runtime against
an actual usb stick.
2026-08-07 14:36:36 +01:00
ahmza f8552bc7bb run hypridle and hyprsunset as graphical-session user units
both were installed but never launched. they ship their own units, but in
$out/share/systemd/user - not $out/lib/systemd/user - and that distinction
is why they need redeclaring rather than merely enabling:

  * systemd.packages would collect nothing, since nixpkgs' generateUnits
    globs only $pkg/etc/systemd/user and $pkg/lib/systemd/user.
  * the units are nonetheless visible to the user manager, via
    environment.pathsToLink -> /run/current-system/sw/share/systemd/user,
    which is on the search path because XDG_DATA_DIRS ends with
    /run/current-system/sw/share. but visible is not enabled - nothing
    creates the WantedBy symlink, so they sit unstarted.
  * wantedBy alone would not help either: with no other keys nixos
    synthesises a replacement unit with an empty [Service] section, and it
    shadows upstream's because /etc/systemd/user takes precedence over
    $XDG_DATA_DIRS.

binding them to graphical-session.target, which uwsm manages properly,
means they start and stop with the session instead of leaking across
logins - same reasoning as udiskie, so that moves into the same attrset.

close to upstream's definitions but not identical: hyprsunset's
Requires=graphical-session.target is dropped, because PartOf already
propagates the stop while Requires would additionally make starting
hyprsunset by hand pull up the whole session target. the Slice=session.slice
asymmetry is upstream's own - hyprsunset sets it, hypridle does not - and is
noted inline so it does not get "fixed" later.

hypridle gets an explicit path because its config shells out to hyprlock,
pidof, brightnessctl, hyprctl, loginctl, systemctl and notify-send. that
config lives in the dotfiles repo, so this list can silently drift out of
sync with it: a binary used there but missing here fails as a bare "command
not found" inside hypridle, and Restart=on-failure will not catch it because
hypridle itself stays up. flagged in a comment at both ends.
2026-08-06 17:09:30 +01:00
ahmza 29707ba0f6 add a 40gib swapfile and wire up hibernation on shadow
swap exists for hibernation, not for memory pressure - 31gib of ram on a
desktop rarely needs paging. the hibernation image must fit in swap so the
floor is "as big as ram"; 40gib leaves headroom. the 3090's vram does not
land here: with NVreg_PreserveVideoMemoryAllocations=1 the driver spools it
to NVreg_TemporaryFilePath, which nixpkgs leaves unset and so defaults to
/tmp. that is only safe while /tmp is on the real root - boot.tmp.useTmpfs
would route a full vram dump into ram and break hibernation outright.

resuming needs both boot.resumeDevice and resume_offset. the device half is
mandatory rather than optional here because boot.initrd.systemd.enable is
true, and under systemd initrd the resume= parameter is emitted only from
boot.resumeDevice - the legacy stage-1 script that scans swapDevices is not
in play. without it hibernate succeeds and the machine then cold-boots,
silently discarding the session.

resume_offset is the physical block offset of the swapfile's first extent,
since the kernel addresses the file by raw block. that value is hand-copied,
and a stale one fails silently: no swsusp signature at the given block means
`PM: Image not found (code -22)` and a cold boot, with no error at hibernate
time. so an activation check warns on drift. it is a detector, not a gate -
nixpkgs only recreates the swapfile when size changes, so on the rebuild
that resizes it the check still sees the old file and fires on the next one.
it warns rather than fails, and expects to read nothing on a fresh install
where mkswap-var-lib-swapfile.service has not run yet.

sleeping via suspend-then-hibernate (hypridle invokes it): s3 first so a
quick return is instant, then a real suspend-to-disk once the machine has
clearly been abandoned. deliberately not hybrid-sleep, which writes the disk
image up front on every single sleep - measured here at ~59s of filesystem
sync plus an 11gib write before the screens even went off, and because an s3
wake invalidates the image the next boot always reported `Image not found`.
full hibernation price paid every time, redeemed only on a power cut.
suspend-then-hibernate defers that write until it is worth doing, and unlike
hybrid-sleep it is a mode nvidia explicitly supports.

HibernateDelaySec is explicit because systemd otherwise derives the handoff
from battery discharge estimation, and this desktop has no system battery -
the sole /sys/class/power_supply entry is a logitech mouse reporting
scope=Device, which is ignored for that purpose.

also corrects a comment: a sparse swapfile does not silently corrupt.
swapon refuses files with holes outright ("skipping - it appears to have
holes"), so the failure is loud and the system simply ends up with no swap.
genuine silent corruption is the copy-on-write case, which is why btrfs
needs a NOCOW-from-creation file.
2026-08-06 17:09:14 +01:00
ahmza 67bafd4f73 switch to open nvidia modules and make sleep transitions actually work
three related changes, all needed together to get this card through a
sleep cycle with its framebuffer intact.

open = true. mandatory to set explicitly on driver >= 560, where the
option defaults to null and nixpkgs asserts against null. true because
every card this module targets is turing or later, which is what upstream
recommends open modules for. mkDefault so a future pre-turing host can
override at host level - and note that without it, importing both
gpuNvidia and gpuNvidiaPrime would be an outright merge conflict rather
than something resolvable. this selects between two real builds of the
same release, nvidia-open vs the proprietary kernel modules; it has no
effect on driver version. confirmed after reboot:
/proc/driver/nvidia/version reports the open module.

kernelSuspendNotifier = false. nixpkgs defaults this to
`useOpenModules && version >= 595`, which is silently true here, and that
path wedges the gpu on resume - nv_pm_notifier warns, the card never
comes back, and an unrecoverable gsp rpc storm follows. pinning it false
selects the nvidia-{suspend,hibernate,resume}.service path instead.

installing upstream's system-sleep hook, because those units are wired
only to systemd-{suspend,hibernate}.service, while suspend-then-hibernate
runs as a single systemd-suspend-then-hibernate.service that performs
both transitions internally. no nvidia unit fires for it at all - the
units cannot express it, which is exactly why nvidia ships the hook.
nixpkgs sets systemd.packages from the driver package, but that only
collects lib/systemd/system/, never lib/systemd/system-sleep/, so the
hook is silently discarded.

it cannot be installed verbatim, for two separate reasons.

first, past its suspend-then-hibernate branch it ends in an
unconditional `case "$1" in post) nvidia-sleep.sh resume`. for plain
suspend that duplicates nvidia-resume.service, and destructively:
nvidia-sleep.sh's RestoreVT reads /run/nvidia-sleep/Xorg.vt_number,
deletes it, then chvt's back. chvt lives in pkgs.kbd, which nixpkgs puts
on nvidia-resume.service's path but which is absent from the stripped
path systemd-sleep gives its hooks. the hook would consume the vt file,
fail to chvt, and the real unit would then find nothing to restore.

second, its suspend-then-hibernate dispatch handles post:*,
pre:hibernate and pre:suspend-after-failed-hibernate - but there is no
pre:suspend, which is the *first* leg of suspend-then-hibernate. it
matched nothing, fell through, and the driver never got its vram-spool
handshake, so with NVreg_PreserveVideoMemoryAllocations=1 the gpu refused
to suspend and the whole operation aborted:

  nvidia 0000:2d:00.0: PM: failed to suspend async: error -5
  PM: Some devices failed to suspend, or early wake event detected
  systemd-sleep: Failed to put system to sleep. System resumed again:
    Input/output error

so the wrapper gates out everything except suspend-then-hibernate, fills
in pre:suspend the same way upstream fills in pre:hibernate, and hands
the rest to upstream unchanged. worth reporting upstream - as shipped
that hook cannot work for suspend-then-hibernate whenever
PreserveVideoMemoryAllocations is on.

verified on hardware: plain suspend and plain hibernate both clean with
no chvt failure and the vt file consumed by the right unit, and a full
suspend-then-hibernate cycle now runs all four transitions - suspend
entry (deep), rtc wake, hibernation entry, hibernation exit.
2026-08-06 17:08:55 +01:00
ahmza b7e6418fa6 chainload the windows boot manager from grub on shadow
windows sits on the 476g nvme - the disk with three partitions: a 16m
"microsoft reserved" msr with no filesystem, the 476g ntfs c: volume, and
a 832m winre. it has no efi system partition of its own, because it was
installed against the esp that nixos now owns (fs-uuid 5873-AD05). both
oses therefore boot from one shared esp; there is no separate windows
boot drive.

the nixos install reformatted that esp, which wiped
EFI/Microsoft/Boot/{bootmgfw.efi,BCD} and left windows unbootable. that
has since been repaired from winre, so the entry is live. the bcdboot
command is kept in the comment for the next time the esp is reformatted -
it has to run from windows media or winre because the bcd store cannot be
generated from linux, and copying bootmgfw.efi alone fails 0xc000000f.

chainloaded explicitly rather than via grub.useOSProber: deterministic,
pulls in no extra tooling, and avoids probing every disk on each
bootloader install. the -f guard makes the entry self-disabling, so a
future esp reformat degrades to a missing menu item rather than one that
boots into an error. because grub evaluates it at boot rather than at nix
eval time, repairing windows needed no rebuild.

searching by fs-uuid rather than (hdX,gptN) is deliberate: nvme
enumeration is not stable on this board, and /boot has been seen as both
nvme0n1p1 and nvme1n1p1. the -n "$esp" test is a separate nested if
rather than `[ -n "$esp" -a -f ... ]` because grub's test builtin
evaluates both operands of -a instead of short-circuiting, so the
combined form would still expand the path test against an unset device.
2026-08-06 17:08:30 +01:00
ahmza fc1772e5db pin the bootloader explicitly and bound the grub menu to 10 generations
grub.enable defaults to true, so adding a boot.loader block silently
switched this host from the systemd-boot it was installed with over to
grub. nixos removes neither the old bootloader's files nor its nvram
entry, so both lived on the esp and the firmware kept booting a frozen
systemd-boot menu. pinning both options means a conflicting definition
elsewhere now fails loudly instead of quietly winning. note the pins
delete nothing - stale EFI/systemd/, loader/ and the leftover nvram entry
still have to be cleared by hand. done on shadow, not verified on t480.

configurationLimit bounds esp usage for real, not just the menu:
install-grub.pl unlinks any $boot/kernels/* that no kept generation
references. sizing measured on shadow rather than assumed - 10 kept
generations referenced 1 bzImage and 3 initrds for 114mib total, about
11mib per generation, because generations share aggressively. the
worst case is higher, around 47mib each, since nixpkgs embeds
/etc/modprobe.d/nixos.conf inside the initrd and so every kernel module
parameter edit mints a fresh one. 10 therefore costs ~115mib typically
and ~470mib worst case against a 1gib esp that also carries the grub
modules, the catppuccin theme, and windows' own boot files.

keeping it conservative matters because overflow fails as `copy ... or
die` in install-grub.pl, which runs *after* the system profile has
already been switched - the result is an activated but unbootable
generation. t480 reaches this module too and its esp may be smaller.
2026-08-06 17:08:15 +01:00
ahmza 42c805f430 untrack the build result symlink and harden .gitignore
result is a symlink into /nix/store that nix build rewrites on every
invocation, so tracking it churns the history and pins a store path that
means nothing on any other machine. it was committed pointing at a t480
system while sitting on shadow.

the two git-secret lines are what `git secret init` writes. worth being
explicit about what they do and do not cover: `!*.secret` un-ignores the
*encrypted* blobs only. nothing here protects the plaintext originals -
that relies entirely on `git secret add` appending each plaintext path to
this file, so always run it from the repo root rather than hand-editing
.gitsecret/paths/mapping.cfg.

.gitsecret/keys is git-secret's gpg homedir. pubring.kbx is meant to be
committed, but a stray `gpg --homedir .gitsecret/keys --import` of a
private key would drop private-keys-v1.d/ in there, so those paths are
ignored up front rather than after the fact.
2026-08-06 17:07:59 +01:00
ahmza e556cb31af enable quickshell in hyprland to eventually replace waybar. nix-ld also updated to enable qmlls in nvim 2026-08-06 14:02:26 +01:00
ahmza 35970bfd49 folded t480 hardware config into the flake to get rid of --impure 2026-08-06 14:00:50 +01:00
31 changed files with 940 additions and 35 deletions
+5
View File
@@ -1 +1,6 @@
.session
# `nix build` / `nixos-rebuild build` output symlink. Points into /nix/store and
# churns on every build, so it must not be tracked.
result
result-*
+16
View File
@@ -0,0 +1,16 @@
{
self,
moduleWithSystem,
...
}: {
flake.nixosModules.cad = moduleWithSystem ({...}: let
modules = with self.nixosModules; [
freecad
openscad
prusa-slicer
];
in {
imports = modules;
});
}
+15
View File
@@ -0,0 +1,15 @@
{
self,
moduleWithSystem,
...
}: {
flake.nixosModules.netops = moduleWithSystem ({...}: let
modules = with self.nixosModules; [
winbox
unraid-usb-creator
];
in {
imports = modules;
});
}
+9
View File
@@ -0,0 +1,9 @@
{...}: {
# Screenshot capture, region selection, annotation, and clipboard support for
# the Hyprland screenshot keybinds managed in the dotfiles repo.
flake.nixosModules.freecad = {pkgs, ...}: {
environment.systemPackages = with pkgs; [
freecad
];
};
}
+90 -15
View File
@@ -10,6 +10,8 @@
fuzzel
ghostty
waybar
quickshell
screenshot
];
in {
imports = modules;
@@ -36,20 +38,94 @@
# are declared in the host's disks.nix instead.
services.udisks2.enable = true;
# Run udiskie as a user unit rather than from the hypr autostart — it was
# previously only installed, never actually launched. Binding it to
# graphical-session.target (which UWSM manages properly) means it starts and
# stops with the session instead of leaking across logins.
systemd.user.services.udiskie = {
description = "udiskie removable media automounter";
partOf = ["graphical-session.target"];
after = ["graphical-session.target"];
wantedBy = ["graphical-session.target"];
serviceConfig = {
Type = "simple";
# -s = smart tray: the icon appears only while removable media is present.
ExecStart = "${pkgs.udiskie}/bin/udiskie -s";
Restart = "on-failure";
# Session daemons run as systemd user units bound to graphical-session.target
# (which UWSM manages properly) rather than from the hypr autostart, so they
# start and stop with the session instead of leaking across logins.
#
# hypridle and hyprsunset both ship their own unit, but in
# $out/share/systemd/user — NOT $out/lib/systemd/user. That distinction is
# why they need redeclaring rather than just enabling:
#
# * `systemd.packages = [pkgs.hypridle]` would collect nothing. nixpkgs'
# generateUnits globs only $pkg/etc/systemd/user and $pkg/lib/systemd/user.
# * The units are nonetheless *visible* to the user manager, via
# environment.pathsToLink -> /run/current-system/sw/share/systemd/user,
# which is on the search path because XDG_DATA_DIRS ends with
# /run/current-system/sw/share. But visible is not enabled: nothing
# creates the WantedBy symlink, so they sit unstarted.
# * `systemd.user.services.<n>.wantedBy` alone would not help either — with
# no other keys NixOS synthesises a *replacement* unit with an empty
# [Service] section, shadowing upstream's via /etc/systemd/user (which
# takes precedence over $XDG_DATA_DIRS).
#
# So the full definition below is deliberate. It is close to upstream's but
# not identical: hyprsunset's `Requires=graphical-session.target` is dropped
# on purpose (PartOf already propagates the stop, while Requires would also
# make starting hyprsunset by hand pull up the whole session target), and
# hypridle gains an explicit `path` because its config shells out.
systemd.user.services = {
udiskie = {
description = "udiskie removable media automounter";
partOf = ["graphical-session.target"];
after = ["graphical-session.target"];
wantedBy = ["graphical-session.target"];
serviceConfig = {
Type = "simple";
# -s = smart tray: the icon appears only while removable media is present.
ExecStart = "${pkgs.udiskie}/bin/udiskie -s";
Restart = "on-failure";
};
};
hypridle = {
description = "Hyprland idle daemon";
documentation = ["https://wiki.hyprland.org/Hypr-Ecosystem/hypridle"];
partOf = ["graphical-session.target"];
after = ["graphical-session.target"];
wantedBy = ["graphical-session.target"];
# ~/.config/hypr/hypridle.conf calls out to these: hyprlock (lock_cmd),
# pidof (the `pidof hyprlock ||` guard), brightnessctl (backlight
# listener), hyprctl (dpms), loginctl + systemctl (sleep/lock),
# notify-send (unlock_cmd).
#
# MAINTENANCE HAZARD: that config lives in a *different* repo (the stow'd
# `hypr` package in the dotfiles repo), so this list can silently drift
# out of sync with it. A binary used there but missing here fails as a
# bare "command not found" inside hypridle, and Restart=on-failure will
# NOT catch it because hypridle itself stays up — the idle action just
# quietly stops working. Add any new binary to both places.
path = with pkgs; [
hyprlock
hyprland
brightnessctl
libnotify
procps
systemd
];
unitConfig.ConditionEnvironment = "WAYLAND_DISPLAY";
serviceConfig = {
Type = "simple";
ExecStart = "${pkgs.hypridle}/bin/hypridle";
Restart = "on-failure";
};
};
hyprsunset = {
description = "Hyprland blue-light filter";
documentation = ["https://wiki.hyprland.org/Hypr-Ecosystem/hyprsunset"];
partOf = ["graphical-session.target"];
after = ["graphical-session.target"];
wantedBy = ["graphical-session.target"];
unitConfig.ConditionEnvironment = "WAYLAND_DISPLAY";
serviceConfig = {
Type = "simple";
ExecStart = "${pkgs.hyprsunset}/bin/hyprsunset";
Restart = "on-failure";
# Intentionally only on hyprsunset, not hypridle/udiskie: upstream's
# hyprsunset.service sets Slice=session.slice and upstream's
# hypridle.service does not. Not an oversight — don't "fix" it.
Slice = "session.slice";
};
};
};
@@ -73,7 +149,6 @@
udiskie
brightnessctl
# clipboard
wl-clipboard
cliphist
# theming
gsettings-desktop-schemas
+7
View File
@@ -0,0 +1,7 @@
{...}: {
flake.nixosModules.moonlight = {pkgs, ...}: {
environment.systemPackages = with pkgs; [
moonlight-qt
];
};
}
+8
View File
@@ -0,0 +1,8 @@
{...}: {
# Nextcloud desktop sync client. Account and sync settings remain per-user.
flake.nixosModules.nextcloud = {pkgs, ...}: {
environment.systemPackages = with pkgs; [
nextcloud-client
];
};
}
+10
View File
@@ -0,0 +1,10 @@
{...}: {
# Screenshot capture, region selection, annotation, and clipboard support for
# the Hyprland screenshot keybinds managed in the dotfiles repo.
flake.nixosModules.openscad = {pkgs, ...}: {
environment.systemPackages = with pkgs; [
openscad
openscad-lsp
];
};
}
+104
View File
@@ -0,0 +1,104 @@
{...}: {
# CUPS printing + eSCL scanning for the networked EPSON ET-8550.
#
# Two queues, deliberately:
# EPSON_ET_8550 driverless / IPP Everywhere. The default. PDF is
# passed straight through and the printer's own RIP
# does the work. 51 page sizes, 26 borderless, and the
# only queue that can print CD/DVDs (MediaType Disc).
# EPSON_ET_8550_Photo Epson ESC/P-R. Host rasterises to ESC/P-R at 360dpi.
# 34 sizes, 12 borderless, no disc support - but it is
# the only way to reach Epson's named media/quality
# presets ("Epson Ultra Glossy-Max Quality" etc) that
# IPP Everywhere cannot express. Kept for photo A/B.
#
# Note both PPDs are 360x360 HWResolution - escpr2 is not higher resolution.
#
# Both queues use the mDNS name, not 10.10.20.62, so a DHCP lease change
# doesn't break them. services.avahi.nssmdns4 is what resolves it.
# Port 9100 is closed on this printer, so socket:// is not an option.
#
# cups-browsed is off: it would auto-create a third duplicate queue for the
# same printer. avahi stays on for mDNS resolution and scanner discovery.
flake.nixosModules.printing = {
config,
lib,
pkgs,
...
}: {
programs.system-config-printer.enable = true;
services.avahi = {
enable = true;
nssmdns4 = true;
};
services.printing = {
enable = true;
browsed.enable = false;
drivers = with pkgs; [
epson-escpr2
];
};
hardware.printers = {
ensureDefaultPrinter = "EPSON_ET_8550";
ensurePrinters = [
{
name = "EPSON_ET_8550";
description = "EPSON ET-8550 (driverless)";
location = "Network";
deviceUri = "ipp://EPSONA70973.local:631/ipp/print";
model = "everywhere";
}
{
name = "EPSON_ET_8550_Photo";
description = "EPSON ET-8550 (ESC/P-R photo presets)";
location = "Network";
deviceUri = "ipp://EPSONA70973.local:631/ipp/print";
model = "epson-inkjet-printer-escpr2/Epson-ET-8550_Series-epson-escpr2-en.ppd";
}
];
};
# hardware/printers.nix orders ensure-printers only after cups.service, but
# our deviceUri is an mDNS name and the driverless queue queries the printer
# while it is created. Run the generated script asynchronously so an offline
# printer cannot fail boot or nixos-rebuild, then retry until it is reachable.
systemd.services.ensure-printers = lib.mkIf (config.hardware.printers.ensurePrinters != [] && config.services.printing.enable) {
after = [
"avahi-daemon.service"
"network-online.target"
"nss-lookup.target"
];
wants = [
"avahi-daemon.service"
"network-online.target"
];
serviceConfig = {
Type = lib.mkForce "simple";
Restart = "on-failure";
RestartSec = "1min";
};
};
# eSCL/AirScan. The printer advertises _uscan._tcp with rs=eSCL and
# mopria-certified-scan=1.3, so sane-airscan drives it with no vendor
# backend. No scanner/lp group needed - there's no local udev device.
#
# sane-backends ships its own `escl` backend which also claims eSCL
# devices. Left enabled it double-lists the scanner and, worse, GETs the
# printer's web root and dumps the HTML response onto scanimage's stdout.
# Disabling it leaves sane-airscan as the only eSCL backend, which reports
# a far better device string (airscan:e0:EPSON ET-8550 Series).
hardware.sane = {
enable = true;
extraBackends = [pkgs.sane-airscan];
disabledDefaultBackends = ["escl"];
};
environment.systemPackages = with pkgs; [
simple-scan
];
};
}
@@ -0,0 +1,8 @@
{...}: {
# PrusaSlicer package only; printer and filament profiles remain per-user.
flake.nixosModules.prusa-slicer = {pkgs, ...}: {
environment.systemPackages = with pkgs; [
prusa-slicer
];
};
}
+1
View File
@@ -5,5 +5,6 @@
environment.systemPackages = with pkgs; [
quickshell
];
qt.enable = true;
};
}
+12
View File
@@ -0,0 +1,12 @@
{...}: {
# Screenshot capture, region selection, annotation, and clipboard support for
# the Hyprland screenshot keybinds managed in the dotfiles repo.
flake.nixosModules.screenshot = {pkgs, ...}: {
environment.systemPackages = with pkgs; [
grim
slurp
satty
wl-clipboard
];
};
}
+13
View File
@@ -0,0 +1,13 @@
{...}: {
# TODO: previously injected SLSsteam via LD_AUDIT from the sls-steam flake. Now stock.
flake.nixosModules.sunshine = {pkgs, ...}: {
services.sunshine = {
enable = true;
autoStart = true;
capSysAdmin = true;
openFirewall = true;
settings.output_name = "DP-3"
};
hardware.uinput.enable = true;
};
}
@@ -0,0 +1,119 @@
{...}: {
# Unraid USB Creator - writes Unraid OS straight to a USB stick, streaming the
# image from unraid.net instead of making you download it first.
#
# Not in nixpkgs, and upstream ships an AppImage only, so this is a repack.
# The AppImage is a Qt6/QML app (a fork of rpi-imager) built with linuxdeploy,
# meaning it already vendors the entire Qt6 stack, xcb, icu, gnutls and krb5 in
# usr/lib. We keep that bundle verbatim and only autoPatchelf it against the
# handful of libraries linuxdeploy deliberately left out (libc, libstdc++, GL,
# X11, fontconfig, ...). No FHS wrapper needed, so no 400MB buildFHSEnv closure.
flake.nixosModules.unraid-usb-creator = {
pkgs,
lib,
...
}: let
pname = "unraid-usb-creator";
version = "1.1.0";
# unraid.net/download's "stable" link is a redirect to this GitHub asset.
# Note the v1.1.0 tag is not returned by the releases API (the API only lists
# the v2.0.x line), but the download URL is live and matches the checksum.
appimage = pkgs.fetchurl {
url = "https://github.com/unraid/usb-creator-next/releases/download/v${version}/${pname}-${version}.appimage";
hash = "sha256-NI/SMgOuzifvjhtK4Py96lbuI/uvJO0NiMr2zNtNSUk=";
};
unraid-usb-creator = pkgs.stdenvNoCC.mkDerivation {
inherit pname version;
src = pkgs.appimageTools.extract {
inherit pname version;
src = appimage;
};
nativeBuildInputs = with pkgs; [
autoPatchelfHook
makeWrapper
];
# Only the libraries the AppImage does *not* vendor. Everything else
# resolves inside $out/share/unraid-usb-creator/lib.
buildInputs = with pkgs; [
e2fsprogs # libcom_err.so.2, pulled in by the vendored krb5
fontconfig
freetype
gmp # pulled in by the vendored nettle/gnutls
libgpg-error
libglvnd # libGL / libGLX / libEGL / libOpenGL
libx11 # also provides libX11-xcb
libxcb
stdenv.cc.cc # libstdc++ / libgcc_s
zlib
];
# Qt's openssl TLS backend resolves libssl/libcrypto with dlopen, so
# autoPatchelf never sees the dependency. Without this the app cannot fetch
# the release JSON or stream the image over HTTPS.
#
# This has to be appendRunpaths and NOT runtimeDependencies: in
# auto-patchelf, `rpath += runtime_deps` lives inside the set-interpreter
# branch, so it only ever reaches executables, while `rpath.extend(
# append_rpaths)` runs for every patched file. The dlopen happens from
# plugins/tls/libqopensslbackend.so, and glibc resolves dlopen against the
# calling object's DT_RUNPATH - the wrapper binary's RUNPATH is not
# consulted - so the path has to be on the plugin itself.
appendRunpaths = ["${pkgs.openssl.out}/lib"];
installPhase = ''
runHook preInstall
# qt.conf next to the binary declares Prefix=../ with Plugins=plugins and
# Qml2Imports=qml, so the usr/ layout has to be preserved as a unit.
install -Dm755 usr/bin/unraid-usb-creator -t $out/share/${pname}/bin
install -Dm444 usr/bin/qt.conf -t $out/share/${pname}/bin
cp -r usr/lib usr/plugins usr/qml $out/share/${pname}/
cp -r usr/share/icons $out/share/
install -Dm444 usr/share/metainfo/*.xml -t $out/share/metainfo
install -Dm444 usr/share/applications/*.desktop -t $out/share/applications
# mkfs.fat is shelled out to when formatting the stick; syslinux is NOT
# needed as a runtime dep because upstream statically vendors it.
#
# linuxdeploy only bundled the xcb platform plugin - there is no
# libqwayland*.so anywhere in the AppImage - so this always runs through
# XWayland under Hyprland. Pinning the platform avoids Qt first trying
# wayland and falling back; --set-default rather than --set so it can
# still be overridden if upstream ever ships the Wayland plugin.
makeWrapper $out/share/${pname}/bin/unraid-usb-creator $out/bin/${pname} \
--prefix PATH : ${lib.makeBinPath (with pkgs; [dosfstools util-linux])} \
--set-default QT_QPA_PLATFORM xcb
runHook postInstall
'';
postFixup = ''
substituteInPlace $out/share/applications/*.desktop \
--replace-fail "Exec=unraid-usb-creator" "Exec=$out/bin/${pname}"
'';
meta = {
description = "Tool for writing Unraid OS to USB drives";
homepage = "https://github.com/unraid/usb-creator-next";
license = lib.licenses.asl20;
sourceProvenance = [lib.sourceTypes.binaryNativeCode];
platforms = ["x86_64-linux"];
mainProgram = pname;
};
};
in {
environment.systemPackages = [unraid-usb-creator];
# The app enumerates and unmounts removable drives over the UDisks2 D-Bus API
# rather than poking /dev directly, which is also how it avoids needing root.
# hyprland/default.nix already enables this for udiskie; repeated here so the
# module is self-contained on a host that does not use Hyprland.
services.udisks2.enable = true;
};
}
+15
View File
@@ -0,0 +1,15 @@
{...}: {
# QMK/VIA keyboard tooling. QMK's uaccess rule lets the active local seat talk
# to HID devices; remote and non-seat sessions intentionally get no access.
flake.nixosModules.via = {pkgs, ...}: {
environment.systemPackages = with pkgs; [
via
qmk
vial
];
# Do not add pkgs.via or pkgs.vial to services.udev.packages: both make
# every hidraw device world-writable. QMK's rules use seat-scoped ACLs.
hardware.keyboard.qmk.enable = true;
};
}
+10
View File
@@ -0,0 +1,10 @@
{...}: {
# MikroTik RouterOS configuration client. pkgs.winbox is winbox4, which is
# already autoPatchelf'd against the Qt/xcb stack it needs, so it does not
# rely on nix-ld. Note the installed binary is `WinBox`, not `winbox`.
flake.nixosModules.winbox = {pkgs, ...}: {
environment.systemPackages = with pkgs; [
winbox
];
};
}
+8
View File
@@ -0,0 +1,8 @@
{...}: {
# Yazi a cli file explorer.
flake.nixosModules.yazi = {pkgs, ...}: {
environment.systemPackages = with pkgs; [
yazi
];
};
}
+6 -1
View File
@@ -19,7 +19,12 @@
discord
gaming
bottles
netops
prusa-slicer
printing
via
nextcloud
sunshine
];
};
}
+115 -2
View File
@@ -6,7 +6,7 @@
# (`nix run .#sync-hardware`) safe: re-running it can never clobber these
# definitions, nor produce duplicates for whichever data drives happened to
# be mounted at the time it ran.
flake.nixosModules.shadowDisks = {...}: let
flake.nixosModules.shadowDisks = {pkgs, ...}: let
# Secondary data drives, addressed by filesystem label.
#
# nofail on all of them: it stops a missing or dying disk from becoming a
@@ -53,6 +53,22 @@
"x-systemd.device-timeout=10"
];
};
# Pin the NFS server until nas.hfx.ahmza.com points at 10.10.100.4.
# Automounting keeps an unavailable NAS out of the boot path.
nfsShare = export: {
device = "10.10.100.4:${export}";
fsType = "nfs";
options = [
"nfsvers=4.2"
"noauto"
"nofail"
"_netdev"
"x-systemd.automount"
"x-systemd.idle-timeout=600"
"x-systemd.mount-timeout=10"
];
};
in {
fileSystems = {
# Boot essentials, by UUID. UUIDs rather than labels here on purpose:
@@ -72,8 +88,105 @@
"/mnt/dump" = spinningDrive "DUMP";
"/mnt/bak" = spinningDrive "BAK";
"/mnt/games" = eagerDrive "GAMES";
"/mnt/nas" = nfsShare "/srv/nas";
"/mnt/media" = nfsShare "/srv/media";
};
swapDevices = [];
# Swap exists for hibernation, not for memory pressure — 31GiB of RAM on a
# desktop rarely needs paging. The hibernation image must fit in swap, so the
# floor is "as big as RAM"; 40GiB leaves headroom above that.
#
# NVIDIA video memory does NOT land here: with
# NVreg_PreserveVideoMemoryAllocations=1 (set by
# hardware.nvidia.powerManagement.enable) the driver spools the 3090's VRAM
# to NVreg_TemporaryFilePath, which is unset and so defaults to /tmp. That is
# only safe because /tmp is on this ext4 root — if /tmp ever becomes a tmpfs
# (boot.tmp.useTmpfs) that VRAM dump goes into RAM and hibernation breaks.
#
# On non-btrfs filesystems NixOS creates this with `dd if=/dev/zero`, so the
# file is fully allocated rather than sparse. That matters because the kernel
# writes swap through the block layer using the extent map swapon built,
# bypassing the filesystem — so the mapping has to be stable and complete.
#
# The failure is loud, not silent, though: swapon refuses a file with holes
# outright ("skipping - it appears to have holes"), so a sparse file would
# simply leave the system with no swap rather than corrupt anything. Genuine
# silent corruption is the copy-on-write case, which is why btrfs needs
# chattr +C and a NOCOW-from-creation file.
swapDevices = [
{
device = "/var/lib/swapfile";
size = 40 * 1024; # MiB
}
];
# Resuming from hibernation needs the kernel to be told where the image is,
# and for a swap *file* that means both halves below.
#
# boot.initrd.systemd.enable is true on this host, which makes this
# mandatory rather than optional: under systemd initrd the `resume=` kernel
# param is emitted ONLY from boot.resumeDevice. The legacy stage-1 script
# that scans swapDevices to find a resume device is not in play. Without
# this, hibernate succeeds and the machine then cold-boots, silently
# discarding the session.
#
# resume_offset is the physical block offset of the swapfile's first extent,
# because the kernel addresses the file by raw block, not by path. Recompute
# it with the following if the swapfile is ever recreated, resized, or moved
# — a stale value resumes from the wrong blocks:
#
# sudo filefrag -v /var/lib/swapfile | awk '$1=="0:" {gsub(/\.\./,"",$4); print $4}'
#
# filefrag reports in 4096-byte filesystem blocks and the kernel wants
# PAGE_SIZE units; on x86_64 both are 4096, so the number transfers directly.
boot.resumeDevice = "/dev/disk/by-uuid/deff63fc-1738-4cfc-a79c-06e58c78471a";
boot.kernelParams = ["resume_offset=499355648"];
# A stale resume_offset fails *silently*: the kernel finds no swsusp
# signature at the given block, logs `PM: Image not found (code -22)`, and
# cold-boots — the hibernated session is simply gone, with no error at
# hibernate time. Since the value above is hand-copied, check it on every
# activation so drift surfaces as a warning instead of as a lost session.
#
# Two known limits, both acceptable:
# * nixpkgs only recreates the swapfile when `size` changes, so on the one
# rebuild that resizes it this check still sees the old file and passes;
# it fires on the next rebuild instead. It is a drift detector, not a
# pre-flight gate.
# * Warn rather than fail: activation must not be blocked over this, and it
# is expected to read nothing at all on a fresh install where
# mkswap-var-lib-swapfile.service has not run yet.
system.activationScripts.checkResumeOffset.text = ''
swapfile=/var/lib/swapfile
want=499355648
if [ -f "$swapfile" ]; then
got=$(${pkgs.e2fsprogs}/bin/filefrag -v "$swapfile" 2>/dev/null \
| ${pkgs.gawk}/bin/awk '$1=="0:" {gsub(/\.\./,"",$4); print $4; exit}') || got=""
if [ -n "$got" ] && [ "$got" != "$want" ]; then
echo "WARNING: resume_offset is pinned to $want in modules/hosts/shadow/disks.nix," >&2
echo " but $swapfile now starts at block $got. Hibernation will cold-boot" >&2
echo " and lose the session until boot.kernelParams is updated to $got." >&2
fi
fi
'';
# This host sleeps via suspend-then-hibernate (hypridle in the dotfiles repo
# is what invokes it): S3 first so a quick return is instant, then a real
# suspend-to-disk once the machine has clearly been abandoned.
#
# Deliberately NOT hybrid-sleep, which writes the disk image up front on
# every single sleep. Measured here that cost ~59s of filesystem sync plus an
# 11GiB write before the screens even went off, and because an S3 wake
# invalidates the image the next boot always reported `PM: Image not found
# (code -22)` — full hibernation price paid every time, redeemed only on a
# power cut. suspend-then-hibernate defers that write until it is actually
# worth doing, and unlike hybrid-sleep it is a mode NVIDIA explicitly
# supports (see the system-sleep hook installed by the gpuNvidia module).
#
# HibernateDelaySec is set explicitly because systemd otherwise derives the
# handoff from battery discharge estimation, and this desktop has no system
# battery — the sole /sys/class/power_supply entry is a Logitech mouse
# reporting scope=Device, which is ignored for that purpose.
systemd.sleep.settings.Sleep.HibernateDelaySec = "30min";
};
}
@@ -7,6 +7,93 @@
networking = {
hostName = "shadow";
};
# Windows lives on the 476G NVMe -- the one with three partitions: 16M
# "Microsoft reserved" MSR, 476G NTFS (fs-uuid 70E4E841E4E80AE4, the C:
# volume), 832M WinRE. Identified by layout rather than by nvmeXnY on
# purpose; see the enumeration note below.
#
# That disk has NO EFI System Partition of its own -- its 16M partition is a
# "Microsoft reserved" MSR with no filesystem. Windows was installed against
# the ESP that NixOS now owns: the 1G vfat partition fs-uuid 5873-AD05 on the
# 1.9T NVMe (that disk holds only two partitions, the ESP and the ext4 root
# deff63fc-1738-4cfc-a79c-06e58c78471a). So both OSes boot from one shared
# ESP; there is no separate Windows boot drive.
#
# The NixOS install reformatted that ESP, which wiped
# EFI/Microsoft/Boot/{bootmgfw.efi,BCD} and left Windows unbootable. That has
# since been repaired from WinRE and both files are back in place, so the
# menuentry below is live. Keep the recovery command to hand in case the ESP
# is ever reformatted again -- it has to be run from Windows install media or
# WinRE, because the BCD store cannot be generated from Linux:
#
# bcdboot C:\Windows /s S: /f UEFI (S: = the ESP, fs-uuid 5873-AD05)
#
# Copying bootmgfw.efi alone is not enough; without BCD it fails 0xc000000f.
# Note bcdboot also drops a copy of the Windows boot manager at
# EFI/BOOT/bootx64.efi -- see the fallback-path note in core/boot.nix.
#
# Chainloaded explicitly rather than via grub.useOSProber: deterministic,
# pulls in no extra tooling, and avoids probing every disk on each bootloader
# install. The `-f` guard makes the entry self-disabling: it renders only
# while bootmgfw.efi is actually present, so a future ESP reformat degrades
# to a missing menu item rather than one that boots into an error.
#
# fs-uuid is shadow's ESP, matching the "/boot" entry in disks.nix. Searching
# by UUID rather than (hdX,gptN) matters here: NVMe enumeration is not stable
# on this board -- /boot has been seen as both nvme0n1p1 and nvme1n1p1. For
# the same reason, re-derive device names before running anything
# destructive:
#
# lsblk -o NAME,SIZE,FSTYPE,PARTTYPENAME,UUID,MOUNTPOINT
#
# Windows' boot files now occupy ~30-60MiB of that shared 1G ESP and Windows
# Update rewrites them periodically, which is part of why
# grub.configurationLimit is held at 10 in core/boot.nix.
#
# Before dual-booting, turn off Windows Fast Startup. hiberfil.sys is present
# on the 476G NTFS volume, and Fast Startup plus this host's hibernation
# setup will corrupt any NTFS volume mounted read-write from both sides.
#
# On the GRUB script itself:
#
# The outer `search` + `-f` test run at *boot*, while GRUB parses grub.cfg —
# not at Nix eval time. That is why repairing Windows needed no rebuild: the
# entry started rendering as soon as bcdboot put bootmgfw.efi back, and it
# will stop again by itself if the file disappears.
#
# The `-n "$esp"` test is a *separate,
# nested* if rather than `[ -n "$esp" -a -f ... ]`: a failed search leaves
# $esp unset, and GRUB's `test` builtin evaluates both operands of `-a`
# rather than short-circuiting, so the combined form would still expand the
# `-f` to `()/...` and could complain about a bad device on every boot.
# Nesting guarantees the path test is not reached at all.
#
# No `insmod` is needed for the outer search: GRUB's EFI core image already
# embeds part_gpt/fat/search_fs_uuid, since it must find its own $prefix on
# the FAT ESP before reading this file. NixOS' own install-grub.pl relies on
# exactly that for the search it emits. The insmods inside the menuentry are
# kept only because `chain` genuinely is a separate module.
#
# The inner `search` deliberately repeats the outer one rather than reusing
# `set root=$esp`. It costs one extra disk scan, and only when the entry is
# actually selected, in exchange for a menuentry that is self-contained and
# cannot be broken by anything that clobbers $esp in between. This also
# matches the per-entry search install-grub.pl generates.
boot.loader.grub.extraEntries = ''
search --no-floppy --set=esp --fs-uuid 5873-AD05
if [ -n "$esp" ]; then
if [ -f ($esp)/EFI/Microsoft/Boot/bootmgfw.efi ]; then
menuentry "Windows Boot Manager" --class windows --class os {
insmod part_gpt
insmod fat
insmod chain
search --no-floppy --set=root --fs-uuid 5873-AD05
chainloader /EFI/Microsoft/Boot/bootmgfw.efi
}
fi
fi
'';
};
}
+27
View File
@@ -0,0 +1,27 @@
# GENERATED FILE — do not edit by hand.
#
# Regenerate on the host itself with: nix run .#sync-hardware
#
# Produced by `nixos-generate-config --show-hardware-config --no-filesystems`.
# The --no-filesystems flag is deliberate: fileSystems and swapDevices are
# declared by hand in ./disks.nix, so regenerating this file can never clobber
# them or emit duplicates for whatever happened to be mounted at the time.
#
# Leading underscore keeps import-tree from auto-importing this as a
# flake-parts module (its filter drops any path containing "/_"); it is
# imported explicitly from ./default.nix instead.
{ config, lib, pkgs, modulesPath, ... }:
{
imports =
[ (modulesPath + "/installer/scan/not-detected.nix")
];
boot.initrd.availableKernelModules = [ "xhci_pci" "nvme" "usb_storage" "usbhid" "sd_mod" ];
boot.initrd.kernelModules = [ ];
boot.kernelModules = [ "kvm-intel" ];
boot.extraModulePackages = [ ];
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
}
+10 -12
View File
@@ -5,19 +5,10 @@
}: {
flake.nixosConfigurations.t480 = inputs.nixpkgs.lib.nixosSystem {
modules = with self.nixosModules; [
# TODO: still importing by absolute path, so evaluating t480 requires
# --impure and picks up the hardware file of whatever machine you build
# from (on shadow, that means shadow's disk UUIDs — wrong but harmless
# until you actually deploy). Fix by running, ON THE T480 ITSELF:
#
# nix run .#sync-hardware
#
# then create modules/hosts/t480/disks.nix (mirroring shadow's) and swap
# this line for `./_hardware.nix` + `t480Disks`. Cannot be done remotely:
# the hardware scan has to happen on the target machine.
/etc/nixos/hardware-configuration.nix
desktop
./_hardware.nix
t480Disks
t480Configuration
desktop
# CPU microcode + integrated Intel GPU. The T480 also has an NVIDIA MX150;
# to enable it in PRIME offload mode, swap gpuIntel for gpuNvidiaPrime
# (which includes gpuIntel). Left on Intel-only by default to save power.
@@ -28,6 +19,13 @@
obsidian
discord
gaming
winbox
cad
prusa-slicer
netops
via
t480UsbWakeup
moonlight
];
};
}
+19
View File
@@ -0,0 +1,19 @@
{...}: {
flake.nixosModules.t480Disks = {...}: {
fileSystems = {
"/" = {
device = "/dev/disk/by-uuid/25316a27-6846-4922-8a45-854b9e40a19f";
fsType = "ext4";
};
"/boot" = {
device = "/dev/disk/by-uuid/562C-B760";
fsType = "vfat";
options = [ "fmask=0077" "dmask=0077" ];
};
};
swapDevices = [
{ device = "/dev/disk/by-uuid/54e31e3a-af6a-4fd6-8101-4c168d3e482a"; }
];
};
}
+7
View File
@@ -7,6 +7,13 @@
networking = {
hostName = "t480";
};
services.logind.settings.Login = {
HandleLidSwitch = "suspend";
HandleLidSwitchDocked = "ignore";
};
services.fwupd = {
enable = true;
};
};
}
+19
View File
@@ -0,0 +1,19 @@
{...}: {
flake.nixosModules.t480UsbWakeup = {...}: {
services.udev.extraRules = ''
# Wake the T480 from S3 suspend via a keyboard plugged into a built-in USB
# port. The dock connects over the Thunderbolt-3 controller (3c:00.0), which
# powers down in S3 and cannot detect a cold replug — so dock-connected input
# cannot wake the machine after an unplug-triggered suspend. The built-in xHCI
# (00:14.0) stays wake-capable in S3, so a keyboard on a built-in port works.
#
# Arm ONLY the keyboard itself (match by VID:PID — the USB bus path is
# unstable across re-enumeration). Deliberately do NOT arm the built-in root
# hubs: arming a whole root hub makes it wake on hub-level events (USB-C PD
# negotiation, the SD card reader, internal peripherals), causing spurious
# self-wakes. A leaf device's remote-wakeup propagates through the hub without
# the hub's own wakeup flag being set.
ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="320f", ATTR{idProduct}=="5055", ATTR{power/wakeup}="enabled"
'';
};
}
+57
View File
@@ -10,10 +10,67 @@
efi = {
canTouchEfiVariables = true;
};
# Pinned explicitly. grub.enable defaults to true, so this host silently
# switched from systemd-boot (used at install time) to GRUB when this
# block was added. NixOS never removes the previous bootloader's files
# or its NVRAM entry, so both lived on the ESP and the firmware kept
# booting the frozen systemd-boot menu. Keep this explicit, and never
# set systemd-boot.enable alongside it.
#
# Note what this does and does not do. Both of these lines are no-ops
# against nixpkgs' defaults (grub.enable is already true,
# systemd-boot.enable already false); their value is that a conflicting
# definition elsewhere now fails loudly instead of silently winning. They
# delete nothing — stale EFI/systemd/, loader/ and the leftover NVRAM
# entry have to be removed by hand on any host installed with
# systemd-boot. Done on shadow; NOT yet verified on t480.
#
# This is stock UEFI GRUB: NixOS installs EFI/<distro><esp>/grubx64.efi
# (here EFI/NixOS-boot/grubx64.efi) and registers a matching NVRAM entry
# via efibootmgr, because canTouchEfiVariables is true. That NVRAM entry
# is what makes GRUB the default; NixOS does not manage the removable
# fallback path EFI/BOOT/BOOTX64.EFI and we deliberately do not ask it to
# (grub.efiInstallAsRemovable, which would need canTouchEfiVariables=false,
# is only for firmware that ignores NVRAM entries — neither host needs it).
#
# On shadow that fallback path is NOT empty, though: bcdboot left a copy of
# the *Windows* boot manager at EFI/BOOT/bootx64.efi (byte-identical to
# EFI/Microsoft/Boot/bootmgfw.efi). Normal boots are unaffected because the
# firmware follows its NVRAM entry to GRUB. But it does mean that if NVRAM
# is ever cleared — CMOS reset, battery, firmware update — the fallback
# lands on Windows rather than GRUB. Left as-is by choice; recover by
# rerunning `nixos-rebuild boot` to reinstate the NVRAM entry.
systemd-boot.enable = false;
grub = {
enable = true;
efiSupport = true;
device = "nodev";
theme = pkgs.catppuccin-grub;
# Bound the menu so the ESP cannot fill up with old kernels.
# install-grub.pl unlinks any $boot/kernels/* not referenced by a kept
# generation, so this genuinely caps ESP usage rather than just
# trimming the menu.
#
# Sizing, measured on shadow rather than assumed. Generations share
# aggressively in practice: 10 kept generations referenced 1 bzImage
# (13.3MiB) and 3 initrds (~33.4MiB each) for 114MiB total, i.e. ~11MiB
# per generation, not ~47MiB. Only a kernel or initrd change mints a new
# file — and the initrd does change more often than you'd expect, since
# nixpkgs embeds /etc/modprobe.d/nixos.conf inside it, so every kernel
# module parameter edit produces a fresh one.
#
# So the real bound is between ~11MiB and ~47MiB per generation
# depending on churn: 10 generations costs ~115MiB typically and ~470MiB
# worst case, against a 1GiB ESP that also carries grub/x86_64-efi, the
# catppuccin theme, and Windows' own boot files (bootmgfw.efi + BCD,
# ~30-60MiB) since Windows was bcdboot'd onto this same ESP. 10 keeps
# the worst case under half the partition.
#
# Keep this conservative: overflow fails as `copy ... or die` in
# install-grub.pl, which runs *after* the system profile has already
# been switched, so the result is an activated but unbootable
# generation. t480 reaches this module too and its ESP may be smaller.
configurationLimit = 10;
};
};
plymouth = {
+1
View File
@@ -36,6 +36,7 @@
extraGroups = [
"networkmanager"
"wheel"
"uinput"
];
};
};
+1
View File
@@ -9,6 +9,7 @@
hyprland
sddm
network
yazi
];
in {
imports = modules;
+137 -4
View File
@@ -3,6 +3,7 @@
# For Intel+NVIDIA laptops (e.g. the T480), use gpuNvidiaPrime instead.
flake.nixosModules.gpuNvidia = {
config,
lib,
pkgs,
...
}: {
@@ -15,15 +16,147 @@
hardware.nvidia = {
modesetting.enable = true;
# Proprietary kernel modules. Set to true only for Turing+ (RTX 20xx and
# newer) where the open modules are well supported.
open = false;
# Open kernel modules. Mandatory to set explicitly on driver >= 560 —
# the option defaults to null there and nixpkgs asserts against null, so
# this line cannot simply be dropped.
#
# true because every card this module targets is Turing or later (RTX 20xx
# and newer), which is exactly what upstream recommends open modules for.
# Set false only for a genuinely pre-Turing card, which would want the
# gpuNvidia module pointed at a legacy package branch anyway.
#
# This is NOT cosmetic and NOT a version switch: `open` selects between two
# real builds of the same release — nvidia-open (~12MB) vs the proprietary
# nvidia-kernel-modules (~85MB). It has no effect on driver version, which
# comes solely from `package` below.
#
# Kernel modules only change on reboot, so `cat /proc/driver/nvidia/version`
# describes the RUNNING module and can disagree with this setting until the
# next boot. Compare /run/booted-system against /run/current-system to see
# a pending swap.
#
# mkDefault because "Turing or later" is an assumption about the hosts that
# happen to import this module today, not a property of the module itself.
# A future pre-Turing card can then override it at the host level instead of
# having to edit this file (and note that without mkDefault, importing both
# gpuNvidia and gpuNvidiaPrime would be an outright merge conflict rather
# than something resolvable).
open = lib.mkDefault true;
nvidiaSettings = true;
# Fixes suspend/resume corruption on many setups.
# Required for suspend/hibernate to come back with an intact framebuffer:
# this sets NVreg_PreserveVideoMemoryAllocations=1, so VRAM contents are
# spooled out across sleep instead of being discarded.
#
# The driver spools VRAM to NVreg_TemporaryFilePath, which nixpkgs leaves
# unset so it defaults to /tmp. Keep /tmp on a real filesystem — setting
# boot.tmp.useTmpfs would route a full VRAM dump into RAM, which on a
# large card is enough to break hibernation outright.
powerManagement.enable = true;
# Which mechanism tells the driver about sleep transitions. Two options:
#
# true -> NVreg_UseKernelSuspendNotifiers=1; the driver hooks the
# in-kernel PM notifier chain and the systemd units are NOT
# generated.
# false -> nvidia-{suspend,hibernate,resume}.service call nvidia-sleep.sh
# around systemd-{suspend,hibernate}.service.
#
# Pinned to false deliberately. nixpkgs defaults this to
# `useOpenModules && version >= 595`, which is silently true here (open =
# true above, driver 595), and that path wedges the GPU on resume — the
# notifier callback warns and then the card never comes back:
#
# WARNING: nvidia/nv.c:4564 at nv_restore_user_channels+0x1d9/0x240
# nv_pm_notifier+0x35/0xa0 [nvidia]
# nvidia-modeset: ERROR: GPU:0: Failed detecting connected display devices
# NVRM: GPU0 _issueRpcAndWait: rpcSendMessage failed with status 0x11
#
# followed by an unrecoverable GSP RPC storm, i.e. black screens until a
# hard power cycle. Revisit once a newer driver fixes the notifier path;
# test by flipping this to true and confirming a hibernate round-trip
# relights the displays before leaving it that way.
powerManagement.kernelSuspendNotifier = false;
package = config.boot.kernelPackages.nvidiaPackages.stable;
};
# The nvidia-{suspend,hibernate,resume}.service units nixpkgs generates are
# wired only to systemd-{suspend,hibernate}.service. suspend-then-hibernate
# runs as a single systemd-suspend-then-hibernate.service that performs both
# transitions internally, so those units never fire for it and the GPU would
# sleep with VRAM unspooled.
#
# NVIDIA ships a hook that handles exactly this, dispatching on
# $SYSTEMD_SLEEP_ACTION and covering the suspend-after-failed-hibernate case
# too. nixpkgs sets `systemd.packages = nvidia_x11.out`, but that only
# collects lib/systemd/system/, never lib/systemd/system-sleep/, so upstream's
# hook is silently discarded. Install it by hand.
#
# It must NOT be installed verbatim, though. Past its suspend-then-hibernate
# branch the hook ends in an unconditional fallthrough:
#
# case "$1" in post) nvidia-sleep.sh "resume" ;; esac
#
# For plain suspend/hibernate that duplicates nvidia-resume.service, and the
# duplicate is destructive rather than merely redundant: nvidia-sleep.sh's
# RestoreVT() reads /run/nvidia-sleep/Xorg.vt_number, *deletes* it, then
# chvt's back. `chvt` lives in pkgs.kbd, which nixpkgs puts on
# nvidia-resume.service's `path` but which is absent from the stripped PATH
# systemd-sleep gives its hooks. So the hook consumes the VT file, fails to
# chvt, and the real unit then finds nothing to restore — no VT switch at
# all, i.e. the same black-screen-on-resume this module exists to avoid.
#
# Hence the first line of the wrapper: bail out unless systemd is actually
# doing suspend-then-hibernate, the one transition nixpkgs' units do not
# cover. Upstream's own comment notes that its suspend-then-hibernate path
# deliberately writes /proc/driver/nvidia/suspend directly and leaves the VT
# switch to nvidia-resume.service, so gating here loses nothing.
#
# The wrapper also has to PATCH A HOLE in upstream's dispatch. Its
# suspend-then-hibernate branch is:
#
# case "$1:$SYSTEMD_SLEEP_ACTION" in
# post:*) echo resume > ... ;;
# pre:hibernate) echo hibernate > ... ;;
# pre:suspend-after-failed-hibernate) echo suspend > ... ;;
# esac
#
# There is no `pre:suspend`. But that is exactly the FIRST leg of
# suspend-then-hibernate: systemd invokes the hook with $1=pre and
# SYSTEMD_SLEEP_ACTION=suspend before the S3 transition. It matches nothing,
# the case falls through, and the driver never gets its VRAM-spool handshake
# — so with NVreg_PreserveVideoMemoryAllocations=1 the GPU refuses to
# suspend and the whole operation aborts:
#
# nvidia 0000:2d:00.0: PM: failed to suspend async: error -5
# PM: Some devices failed to suspend, or early wake event detected
# systemd-sleep: Failed to put system to sleep. System resumed again:
# Input/output error
#
# Observed on this host: plain suspend and plain hibernate both worked (their
# nvidia-*.service units did the handshake) while suspend-then-hibernate
# failed at the S3 leg with no nvidia unit having run at all. So handle
# pre:suspend here, the same way upstream handles pre:hibernate, then hand
# everything else to upstream unchanged.
#
# Guarded on the same condition nixpkgs uses for the units: with the kernel
# suspend notifier active the driver handles transitions in-kernel and this
# hook must not also poke /proc/driver/nvidia/suspend.
environment.etc."systemd/system-sleep/nvidia" = lib.mkIf (config.hardware.nvidia.powerManagement.enable
&& !config.hardware.nvidia.powerManagement.kernelSuspendNotifier) {
source = pkgs.writeShellScript "nvidia-system-sleep-hook" ''
[ "$2" = "suspend-then-hibernate" ] || exit 0
# The gap upstream leaves: the S3 leg of suspend-then-hibernate.
if [ "$1" = "pre" ] && [ "$SYSTEMD_SLEEP_ACTION" = "suspend" ]; then
if [ -f /proc/driver/nvidia/suspend ]; then
echo suspend > /proc/driver/nvidia/suspend
fi
exit 0
fi
exec ${config.hardware.nvidia.package.out}/lib/systemd/system-sleep/nvidia "$@"
'';
};
environment.systemPackages = with pkgs; [
vulkan-tools
];
+4
View File
@@ -8,6 +8,10 @@
stdenv.cc.cc
zlib
libusb1
# required for qmlls in nvim
brotli
unixodbc
glib
];
};
};
-1
View File
@@ -1 +0,0 @@
/nix/store/dgd4pa9cl1n266zsbf8xi277p304zxcw-nixos-system-t480-26.05.20260803.531670d