Author SHA1 Message Date
lyrathorpe 5afd365d96 feat(p7zip): always install p7zip
CI / flake (push) Skipped
2026-08-31 03:04:32 +01:00
lyrathorpe a8701061ff feat(smb): add smb support
CI / flake (push) Skipped
2026-08-31 02:35:28 +01:00
8 changed files with 56 additions and 239 deletions
-3
View File
@@ -5,6 +5,3 @@ modules/firmware/*
# Local scratch project, not part of this flake.
tf-inspect/
# Per-machine Claude Code settings; not shared configuration.
.claude/
+33 -159
View File
@@ -9,12 +9,17 @@ A 4th-generation Core i7 (Haswell) on a UEFI board with an NVIDIA GeForce GTX
is driven from the sofa with a Bluetooth controller; keyboard and mouse are
supported but secondary.
## Disk
## Not installed yet
`hardware-configuration.nix` is a real `nixos-generate-config` scan from the
installed machine: GPT with a vfat ESP at `/boot` and an ext4 root, both
addressed by UUID, and no swap device. Nothing else in the host config depends
on the disk layout.
`hardware-configuration.nix` in this host directory is a **placeholder**, not a
hardware scan. It exists so the flake evaluates in CI and assumes the install
labels its partitions `nixos` (root, ext4) and `BOOT` (ESP, vfat). Replace the
whole file with the output of `nixos-generate-config` run on the machine and
commit that. If the labels do not match, the boot fails on a missing device
rather than touching the wrong disk.
Partition the disk GPT with an ESP (vfat, 512 MB is comfortable). Nothing else
in the host config depends on the disk layout.
## Bootloader
@@ -83,7 +88,7 @@ to ReGreet, where the ordinary Sway session is available for everything else.
`services.greetd.restart` defaults to `false` once `initial_session` is set,
which is what stops a logout looping straight back into autologin.
Three details worth knowing:
Two details worth knowing:
- The launcher is referenced as `/run/current-system/sw/bin/steam-gamescope`.
The steam module builds that script privately and only adds it to
@@ -91,92 +96,17 @@ Three details worth knowing:
- `programs.gamescope.capSysNice = true` installs gamescope as a setcap wrapper
in `/run/wrappers/bin` instead of the system profile. That path precedes the
system profile on `PATH`, so `steam-gamescope` still resolves it.
- greetd gives the session the VT for its standard streams, so anything the
Steam session prints as it dies is erased the moment the greeter repaints.
`initial_session` therefore runs the launcher under `systemd-cat`, and the
output is readable after the fact with `journalctl -b -t steam-session`.
### When the television lands on ReGreet instead of Steam
greetd starts `initial_session` **once per boot** — the check is the presence of
`/run/greetd.run` — and if the session process starts and then exits, greetd
falls through to `default_session`, i.e. ReGreet. A gamescope that dies on
startup is therefore indistinguishable from autologin never having run, unless
the logs are read:
```sh
journalctl -b -t steam-session # what the session itself said
journalctl -b -u greetd # whether greetd started it at all
ls -l /run/greetd.run # exists => greetd has already had its one go
```
If greetd never even reaches the session, it prints `unable to start greeter`
and **exits** — there is no greeter to fall back to and the screen stays blank.
Recover over SSH (`modules/ssh.nix`), or hold space at power-on and boot the
previous generation.
To reproduce a session failure by hand, switch to a free VT and run
`steam-gamescope` there. Running it from inside Sway is not the same test:
gamescope picks its nested Wayland backend when `WAYLAND_DISPLAY` is set, and it
is the DRM backend — the one the boot session uses — that is in question.
The greeter is **Dvorak**, like every other host here (`modules/sway.nix` forces
`XKB_DEFAULT_VARIANT=dvorak` on cage). Only relevant if someone else has to type
a password.
### The Sway session behind it
### Adding RetroArch and Clone Hero to Big Picture
Two fleet-wide rules in `home/sway.nix` matter more on a television than
anywhere else:
- Any output running at 4K is forced to **scale 2** (1920x1080 logical). Sway's
config cannot key an output rule on resolution, so a small user service
applies it at session start and again on every output event — which is what
makes it survive the TV being switched off and on.
- The emulators inhibit idle **while focused**, not only while fullscreen.
PCSX2 and Dolphin default to borderless-windowed rather than compositor
fullscreen, so the generic `inhibit_idle fullscreen` rules never fired and
swaylock cut in mid-game.
Neither applies in the Steam session: `swayidle` is a home-manager user service
wanted by `graphical-session.target` and conditional on `WAYLAND_DISPLAY`, and
the gamescope session starts neither, so nothing locks the screen in Big
Picture.
### Adding non-Steam titles to Big Picture
RetroArch, PCSX2, Dolphin and Clone Hero are installed system-wide but are not
Steam titles. Added as Steam shortcuts they appear in Big Picture and inherit
Steam Input, so the controller works in them without further configuration.
Steam's own **Games → Add a Non-Steam Game** is the documented route and is also
the one that does not work here: the dialog never appears at all. That dialog's
file browsing goes through the XDG desktop portal `FileChooser` interface, and
when the portal is unreachable the client falls back to a legacy chooser that is
blank or inert — Valve
[#9447](https://github.com/ValveSoftware/steam-for-linux/issues/9447), not fixed.
Check the portal side before blaming the client:
```sh
busctl --user introspect org.freedesktop.portal.Desktop \
/org/freedesktop/portal/desktop | grep FileChooser
```
`modules/sway.nix` enables `xdg-desktop-portal-gtk`, which is the backend that
implements `FileChooser` (`xdg-desktop-portal-wlr` implements only screenshot
and screencast), and the generated `sway-portals.conf` routes everything but
those two to `gtk`. If the interface is missing from the introspection above,
the backend is not running and that is the fault to chase.
Independent of the dialog, `steamtinkerlaunch` writes `shortcuts.vdf` directly:
```sh
# Steam must be closed -- it rewrites shortcuts.vdf on exit.
steamtinkerlaunch addnonsteamgame -an="RetroArch" -ep="$(command -v retroarch)"
steamtinkerlaunch addnonsteamgame -an="PCSX2" -ep="$(command -v pcsx2-qt)"
steamtinkerlaunch addnonsteamgame -an="Dolphin" -ep="$(command -v dolphin-emu)"
steamtinkerlaunch addnonsteamgame -an="Clone Hero" -ep="$(command -v clonehero)"
```
Both are installed system-wide but are not Steam titles. Add each once, from a
Sway session, via Steam's **Games → Add a Non-Steam Game**; they then appear in
Big Picture and inherit Steam Input, so the controller works in them without
further configuration.
## Emulation — RetroArch
@@ -237,15 +167,15 @@ system directory on this host:
Be honest about the two heaviest cores. `dolphin` and `pcsx2` are libretro ports
of emulators whose upstream effort goes into their **standalone** builds; the
cores lag on compatibility and are the first place to look when a GameCube, Wii
or PS2 title misbehaves.
or PS2 title misbehaves. If a game does not cooperate, add the standalone
emulators to `environment.systemPackages` in `gaming.nix`:
That has already happened for PS2: the LRPS2 core was not good enough in
practice. **`pkgs.pcsx2` and `pkgs.dolphin-emu` are both installed** in
`gaming.nix` alongside their cores, and are what PS2, GameCube and Wii titles
should be run through. The cores stay for the sake of a single frontend and a
shared save/state layout.
```nix
pkgs.dolphin-emu # GameCube / Wii
pkgs.pcsx2 # PlayStation 2
```
Both are controller-driven and are added to Big Picture the same way as
Both are controller-driven and can be added to Big Picture the same way as
RetroArch. The hardware is not the limit here — a GTX 1070 and a Haswell i7 run
PS2 and Wii comfortably.
@@ -296,56 +226,10 @@ RetroArch is pointed at these paths declaratively. The other two have to be told
once, in their own UIs:
- **Steam** — Settings → Storage → the `+` control → add `/srv/games/steam` as a
library folder. Games installed there survive a reinstall of the OS. See
[When Steam will not register the library folder](#when-steam-will-not-register-the-library-folder)
below; it does not currently take.
library folder. Games installed there survive a reinstall of the OS.
- **Clone Hero** — set the song library path to `/srv/games/clonehero/songs` from
its settings screen. Clone Hero keeps its own config in `~/.clonehero`.
### When Steam will not register the library folder
Current behaviour on this host: the file picker opens, Steam creates the
directory structure at `/srv/games/steam`, and the library then never appears in
the Storage UI. So this is not the portal problem the non-Steam-game dialog has
— the picker works and Steam can write. The failure is in the validation Steam
runs before it records the folder in `libraryfolders.vdf`.
That validation is a write-and-execute probe: Steam drops a
`.steam_exec_test.sh` in the candidate folder, marks it executable and runs it.
If either step fails the folder is silently dropped, which is
[#8450](https://github.com/ValveSoftware/steam-for-linux/issues/8450) and is
usually a `noexec` mount or an ownership problem — neither of which is obviously
true here, since `/srv` is on the ext4 root and the tree is
`lyrathorpe:users` 2775.
Reproduce the probe **inside Steam's FHS sandbox**, which is where the client
actually runs it — `steam-run` is installed by the steam module for exactly this
kind of thing:
```sh
steam-run sh -c '
printf "#!/bin/sh\nexit 0\n" > /srv/games/steam/.steam_exec_test.sh &&
chmod +x /srv/games/steam/.steam_exec_test.sh &&
/srv/games/steam/.steam_exec_test.sh &&
echo "probe ok"
'
rm -f /srv/games/steam/.steam_exec_test.sh
```
Then ask the client directly. Steam's console reports a reason where the UI
says nothing — open Steam with `steam -console`, pick the **CONSOLE** tab and
run:
```
library_folder_add /srv/games/steam
```
The directory must already exist and be writable by the user Steam runs as,
which `systemd.tmpfiles` guarantees; the command does not create it. Whatever it
prints, plus `~/.steam/steam/logs/content_log.txt`, is the evidence to work
from.
## Steam and Proton
`programs.steam.enable` already arranges most of what Proton needs, and it is
@@ -427,27 +311,17 @@ wired 360 pads — as well as `mt76x2u`, and pulls in proprietary dongle firmwar
Not worth the side effects unless that dongle is actually in use, and if it ever
is, the 360 pads have to be re-tested.
## Open problems
## Untested claims
The machine is installed and Sway works. Still outstanding:
This host has not been built or booted yet. Two things are worth watching on
first boot:
- **Autologin into Steam does not happen** — the television comes up on ReGreet.
Not yet root-caused; the config side is correct (`initial_session` is present
in the generated `greetd.toml` and `services.greetd.restart` evaluates to
`false`), so the suspicion is that gamescope starts and dies on the
proprietary NVIDIA driver, which greetd treats as a finished session and
answers by starting the greeter. `gaming.nix` sets `GBM_BACKEND=nvidia-drm`
and `__GLX_VENDOR_LIBRARY_NAME=nvidia`, the standard fix, but the combination
has a history of needing more. Read `journalctl -b -t steam-session` first —
see [When the television lands on ReGreet instead of Steam](#when-the-television-lands-on-regreet-instead-of-steam).
- **Steam will not register `/srv/games/steam` as a library folder.** The picker
opens and the directory structure gets created; the library never appears in
the Storage UI. Not the portal — see
[When Steam will not register the library folder](#when-steam-will-not-register-the-library-folder).
- **Steam's Add a Non-Steam Game dialog never appears.** Separate symptom,
probably the portal `FileChooser`; `steamtinkerlaunch` writes the shortcuts
without it in the meantime.
- **Boot into Big Picture has not been exercised**, since autologin has not run.
- **gamescope on the proprietary NVIDIA driver.** `gaming.nix` sets
`GBM_BACKEND=nvidia-drm` and `__GLX_VENDOR_LIBRARY_NAME=nvidia` for the
session, which is the standard fix, but the combination has a history of
needing tweaks. If the session dies at startup, switch the greeter back to
interactive by commenting out `services.greetd.settings.initial_session`, log
into Sway, and read `journalctl --user -b`.
- **Television resolution and refresh.** gamescope takes the output's native
mode by default. Add `gamescopeSession.args = [ "-W" "3840" "-H" "2160" "-r"
"60" ]` if a specific mode is wanted.
+2 -2
View File
@@ -18,8 +18,8 @@ rebuild, never the generated dotfiles.
- Letter keys are **keysyms** (the character produced), not physical positions.
The keyboard is **Dvorak** (`us`/`dvorak`), so e.g. "Super+s" is whatever key
types `s` in Dvorak.
- Shortcuts apply to every Sway host (MBP, T400, Mac Pro, Console); brightness
keys are laptop-only, as noted.
- Shortcuts apply to every Sway host (MBP, T400, Mac Pro); brightness keys are
laptop-only, as noted.
---
+1
View File
@@ -42,6 +42,7 @@ in
pkgs.jnv # interactive jq filter builder (jq itself stays for scripts)
pkgs.hexyl # hex viewer
pkgs.fq # jq for binary formats
pkgs.p7zip
];
# tldr pages: worked examples for a command, next to (not instead of) man.
-54
View File
@@ -30,28 +30,6 @@ let
- | ${pkgs.swappy}/bin/swappy -f -
'';
# Sway's config cannot express an output rule keyed on resolution, so scale is
# applied from outside it: once at session start, then on every output event,
# which is what makes it survive a display being unplugged or power-cycled.
# The `.scale != 2` guard stops the subscription feeding itself, since setting
# a scale is itself an output event.
scaleHidpi = pkgs.writeShellScript "sway-scale-hidpi" ''
apply() {
${pkgs.sway}/bin/swaymsg -r -t get_outputs \
| ${pkgs.jq}/bin/jq -r '.[]
| select(.active and .current_mode.width >= 3840 and .scale != 2)
| .name' \
| while read -r name; do
${pkgs.sway}/bin/swaymsg output "$name" scale 2
done
}
apply
${pkgs.sway}/bin/swaymsg -t subscribe -m '["output"]' | while read -r _; do
apply
done
'';
# Binding-mode names. The string is both the `modes` attr key and what the
# bar's mode indicator shows, so the keys are spelled out in the label.
layoutMode = "layout: [s]tacking [w]tabbed [e]split";
@@ -82,8 +60,6 @@ in
tap = "enabled";
natural_scroll = "enabled";
};
# Mice (the MX Master 3 among them) scroll the same way as the touchpads.
input."type:pointer".natural_scroll = "enabled";
# Solid Catppuccin Mocha base as the wallpaper (no image dependency).
output."*".bg = "#${ctp.base} solid_color";
@@ -135,18 +111,6 @@ in
criteria.class = ".*";
command = "inhibit_idle fullscreen";
}
# Emulators default to borderless-windowed rather than compositor
# fullscreen, so the rules above never fire and swaylock cuts in
# mid-game. Inhibit on focus instead, which does not depend on how the
# application asked to fill the screen.
{
criteria.app_id = "(?i)^(pcsx2|dolphin-emu|retroarch|clone.?hero)";
command = "inhibit_idle focus";
}
{
criteria.class = "(?i)^(pcsx2|dolphin-emu|retroarch|clone.?hero)";
command = "inhibit_idle focus";
}
];
# Binding modes (submenus). Entered from keybindings below; each action
@@ -343,24 +307,6 @@ in
Install.WantedBy = [ "sway-session.target" ];
};
# Every 4K output runs at scale 2 (1920x1080 logical), on every host and
# whatever the display is called. kanshi below owns mode and position; this
# owns scale for the one case that is a fleet-wide rule rather than a
# per-display fact, and the two do not fight because no kanshi profile here
# sets a scale.
systemd.user.services.sway-scale-hidpi = {
Unit = {
Description = "Force scale 2 on 4K Sway outputs";
PartOf = [ "graphical-session.target" ];
After = [ "graphical-session.target" ];
};
Service = {
ExecStart = "${scaleHidpi}";
Restart = "on-failure";
};
Install.WantedBy = [ "sway-session.target" ];
};
# Output/display management. Reacts to hotplug and applies per-display
# mode/scale/position. Profiles are hardware-specific: the safe default below
# just enables the internal laptop panel; add docked/desktop profiles with the
+18 -20
View File
@@ -61,14 +61,6 @@ let
"clonehero/backgrounds"
];
# greetd gives a session the VT for stdio, so a failed Steam session leaves no
# evidence once the greeter takes the VT back. Log to the journal instead:
# `journalctl -b -t steam-session`.
steamSession = pkgs.writeShellScript "steam-session" ''
exec ${pkgs.systemd}/bin/systemd-cat --identifier=steam-session \
/run/current-system/sw/bin/steam-gamescope
'';
# RetroArch and the cores this machine is expected to run. The wrapper already
# points RetroArch at the packaged assets, core info and joypad autoconfig
# profiles; `settings` here is merged on top of those.
@@ -186,11 +178,13 @@ in
# ask for it; Steam's Proton builds and most native titles do.
programs.gamemode.enable = true;
# Autologin into the Steam session. greetd's `restart` option defaults to
# false once initial_session is set, which is what stops a logout from looping
# straight back into autologin.
# Autologin into the Steam session. The launcher is not exposed as a package
# by the steam module -- it is built inside it and added to
# environment.systemPackages -- so reference it through the system profile.
# greetd's `restart` option defaults to false once initial_session is set,
# which is what stops a logout from looping straight back into autologin.
services.greetd.settings.initial_session = {
command = "${steamSession}";
command = "/run/current-system/sw/bin/steam-gamescope";
user = tvUser;
};
@@ -246,17 +240,21 @@ in
environment.systemPackages = [
retroarch
pkgs.clonehero
# Standalone GameCube/Wii and PS2, alongside the libretro cores of the same
# name: upstream effort goes into these builds, so they are what the two
# heaviest systems should actually be run through.
pkgs.dolphin-emu
pkgs.pcsx2
pkgs.dualsensectl # DualSense LED/battery/mic control from the shell
# Writes shortcuts.vdf from the shell. Steam's own "Add a Non-Steam Game"
# dialog depends on a working portal file picker and frequently does not
# open; see docs/hosts/console.md.
pkgs.steamtinkerlaunch
pkgs.mangohud # FPS/frametime overlay; use `mangohud %command%` in Steam
pkgs.vulkan-tools # vulkaninfo, for checking the 32/64-bit ICDs Proton needs
pkgs.cifs-utils
];
fileSystems."/mnt/share" = {
device = "//fs-01.inf.cbg.emmaisvery.gay/torrent";
fsType = "cifs";
options =
let
# this line prevents hanging on network split
automount_opts = "x-systemd.automount,noauto,x-systemd.idle-timeout=60,x-systemd.device-timeout=5s,x-systemd.mount-timeout=5s";
in
[ "${automount_opts},credentials=/etc/nixos/smb-secrets,uid=1000,gid=100" ];
};
}
+2
View File
@@ -48,6 +48,8 @@
fastfetch
];
services.gvfs.enable = true;
# Fonts on every host. The Nerd Font carries the powerline/Nerd glyphs the
# tmux statusline uses (foot names it explicitly in home/sway.nix); Noto sans +
# colour emoji prevent tofu in terminals/TUIs/Firefox -- important on the WSL
-1
View File
@@ -43,7 +43,6 @@ in
playerctl # MPRIS media keys
sway-launcher-desktop
pavucontrol
wlr-randr # output query/adjustment; prints the criteria kanshi keys on
];
};
fonts.packages = with pkgs; [