Compare commits

...
17 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
24 changed files with 415 additions and 20 deletions
-18
View File
@@ -4,21 +4,3 @@
# churns on every build, so it must not be tracked.
result
result-*
# git-secret. `git secret init` writes the random_seed and the `!*.secret`
# negation; the rest is hardening.
#
# WARNING: `!*.secret` only un-ignores the *encrypted* blobs. It does nothing to
# protect the plaintext originals -- that relies entirely on `git secret add`
# appending each plaintext path below. Always run `git secret add <file>` from
# the repo root (never hand-edit .gitsecret/paths/mapping.cfg) and verify with
# `git status` before committing.
.gitsecret/keys/random_seed
# .gitsecret/keys is git-secret's GPG homedir. pubring.kbx is meant to be
# committed; private key material and sockets never are.
.gitsecret/keys/private-keys-v1.d/
.gitsecret/keys/openpgp-revocs.d/
.gitsecret/keys/*.gpg~
.gitsecret/keys/.#*
.gitsecret/keys/S.*
!*.secret
+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
];
};
}
+1 -1
View File
@@ -11,6 +11,7 @@
ghostty
waybar
quickshell
screenshot
];
in {
imports = modules;
@@ -148,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
];
};
}
+18
View File
@@ -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,6 +88,8 @@
"/mnt/dump" = spinningDrive "DUMP";
"/mnt/bak" = spinningDrive "BAK";
"/mnt/games" = eagerDrive "GAMES";
"/mnt/nas" = nfsShare "/srv/nas";
"/mnt/media" = nfsShare "/srv/media";
};
# Swap exists for hibernation, not for memory pressure — 31GiB of RAM on a
+7
View File
@@ -19,6 +19,13 @@
obsidian
discord
gaming
winbox
cad
prusa-slicer
netops
via
t480UsbWakeup
moonlight
];
};
}
+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"
'';
};
}
+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;