diff --git a/flake.lock b/flake.lock index 7fa2a15..3dce54c 100644 --- a/flake.lock +++ b/flake.lock @@ -23,11 +23,11 @@ ] }, "locked": { - "lastModified": 1776686554, - "narHash": "sha256-TaJnAbwSnfJ9My8Df8o6MKswuc3dq6qGF+zTZF906Eg=", + "lastModified": 1778241768, + "narHash": "sha256-vyKpTnkTD0GId4PZUg21oBdWZhuHl/c3YgO5Ruehq2M=", "ref": "refs/heads/main", - "rev": "c1a35069ee41595f8549fb529d29647565a30e75", - "revCount": 99, + "rev": "735b1cc776a8d7e26763bf1ac121866b326bd98f", + "revCount": 104, "type": "git", "url": "https://git.chiasson.cloud/Olivier/cursor-nixos-flake" }, diff --git a/modules/desktop/shells/dms/home-manager/default.nix b/modules/desktop/shells/dms/home-manager/default.nix index 9a74e97..439d92e 100644 --- a/modules/desktop/shells/dms/home-manager/default.nix +++ b/modules/desktop/shells/dms/home-manager/default.nix @@ -752,6 +752,14 @@ in { "trailing_diamond": "\ue0b4", "type": "session" }, + { + "type": "text", + "style": "diamond", + "background": "#3A456E", + "foreground": "#ffbebc", + "template": "\u007B\u007B if .Env.IN_NIX_SHELL \u007D\u007Din nix-shell\u007B\u007B end \u007D\u007D", + "trailing_diamond": "\ue0b4" + }, { "background": "#3A456E", "foreground": "#bc93ff", diff --git a/modules/hosts/14900k/_private/jellyfin-nfs-export.nix b/modules/hosts/14900k/_private/jellyfin-nfs-export.nix index 97c9966..68dd80c 100644 --- a/modules/hosts/14900k/_private/jellyfin-nfs-export.nix +++ b/modules/hosts/14900k/_private/jellyfin-nfs-export.nix @@ -1,9 +1,15 @@ -# Export large Jellyfin media trees to nix-server. Local path must already exist -# (e.g. /mnt/test/jellyfin/{movies,tv}). On nix-server this is mounted at /mnt/nixdesk-jellyfin. +# NFS exports from nixdesk (14900k) to nix-server (192.168.2.238): +# - /mnt/test/jellyfin → nix-server /mnt/nixdesk-jellyfin (Jellyfin bulk libraries) +# - /mnt/media → nix-server /mnt/media (Btrfs MediaLibrary disk; see media-disk.nix) # -# After deploy: ensure Jellyfin can read files over NFS — typical fix: -# chmod -R a+rX /mnt/test/jellyfin -{ ... }: +# NTFS on nixdesk uses uid=olivier + gid=nfsmedia (990); dirs here are olivier:nfsmedia 2775 so +# local writes and NFS all_squash (anonuid=olivier, anongid=990) get rwx via owner or group. +# +# Legacy trees may still need a one-time `chgrp -R nfsmedia` / `chmod -R g+rwX` on deep folders. +{ config, pkgs, ... }: +let + olivierUid = config.users.users.olivier.uid or 1000; +in { # Avoid UID/GID mismatches across machines: map all NFS writes from nix-server to a single # local system user/group on this server. @@ -14,10 +20,32 @@ group = "nfsmedia"; }; - systemd.tmpfiles.settings."14900k-jellyfin-media-dirs" = { - "/mnt/test/jellyfin"."d" = { mode = "2775"; user = "nfsmedia"; group = "nfsmedia"; }; - "/mnt/test/jellyfin/movies"."d" = { mode = "2775"; user = "nfsmedia"; group = "nfsmedia"; }; - "/mnt/test/jellyfin/tv"."d" = { mode = "2775"; user = "nfsmedia"; group = "nfsmedia"; }; + # olivier: owner for local use; nfsmedia: group matches NTFS gid=990 and NFS all_squash (990). + systemd.tmpfiles.settings."14900k-nfs-export-paths" = { + "/mnt/test"."d" = { mode = "2775"; user = "olivier"; group = "nfsmedia"; }; + "/mnt/test/jellyfin"."d" = { mode = "2775"; user = "olivier"; group = "nfsmedia"; }; + "/mnt/test/jellyfin/movies"."d" = { mode = "2775"; user = "olivier"; group = "nfsmedia"; }; + "/mnt/test/jellyfin/tv"."d" = { mode = "2775"; user = "olivier"; group = "nfsmedia"; }; + "/mnt/media"."d" = { mode = "2775"; user = "olivier"; group = "nfsmedia"; }; + "/mnt/media/Movies"."d" = { mode = "2775"; user = "olivier"; group = "nfsmedia"; }; + "/mnt/media/TV"."d" = { mode = "2775"; user = "olivier"; group = "nfsmedia"; }; + "/mnt/media/Videos"."d" = { mode = "2775"; user = "olivier"; group = "nfsmedia"; }; + }; + + # After exports are up, ensure group nfsmedia can write throughout library roots (idempotent; + # scoped to library folders only — not whole disks). Runs on each `nixos-rebuild switch`. + system.activationScripts.nfs-export-group-write = { + deps = [ "specialfs" ]; + text = '' + for d in \ + /mnt/media/TV /mnt/media/Movies /mnt/media/Videos \ + /mnt/test/jellyfin/tv /mnt/test/jellyfin/movies + do + [ -d "$d" ] || continue + ${pkgs.acl}/bin/setfacl -R -m g:nfsmedia:rwx "$d" 2>/dev/null || true + ${pkgs.acl}/bin/setfacl -R -d -m g:nfsmedia:rwx "$d" 2>/dev/null || true + done + ''; }; # Fixed ports so the firewall can allow NFS v3 helpers (see networking.firewall below). @@ -26,9 +54,12 @@ mountdPort = 4000; lockdPort = 4001; statdPort = 4002; - # fsid= stabilizes file handles across server reboots/remounts of this tree (avoids client ESTALE). + # fsid= unique per export tree (avoids client ESTALE when multiple paths are exported). + # Squash nix-server clients to olivier:nfsmedia so Jellyfin can write .nfo/posters into + # existing olivier-owned library folders (990-only squash was "other" r-x on typical 755 trees). exports = '' - /mnt/test/jellyfin 192.168.2.238(rw,sync,no_subtree_check,crossmnt,root_squash,all_squash,anonuid=990,anongid=990,fsid=1) + /mnt/test/jellyfin 192.168.2.238(rw,sync,no_subtree_check,crossmnt,root_squash,all_squash,anonuid=${toString olivierUid},anongid=990,fsid=1) + /mnt/media 192.168.2.238(rw,sync,no_subtree_check,crossmnt,root_squash,all_squash,anonuid=${toString olivierUid},anongid=990,fsid=2) ''; }; diff --git a/modules/hosts/14900k/_private/media-disk.nix b/modules/hosts/14900k/_private/media-disk.nix new file mode 100644 index 0000000..58c4f7f --- /dev/null +++ b/modules/hosts/14900k/_private/media-disk.nix @@ -0,0 +1,34 @@ +# Extra local disks. Declared here, not in hardware.nix (hardware.nix is generated). +{ config, lib, ... }: +let + # Stable UID so NTFS `uid=` matches `users.users.olivier` (override if your account is not 1000). + olivierUid = config.users.users.olivier.uid or 1000; +in +{ + users.users.olivier.uid = lib.mkDefault 1000; + + fileSystems."/mnt/media" = { + device = "/dev/disk/by-uuid/17d8a981-db3b-415e-a0f7-7dbc519e04ab"; + fsType = "btrfs"; + options = [ + "subvol=@" + "compress=zstd" + "noatime" + ]; + }; + + # LABEL="Deep Storage Unit". Owner olivier, group nfsmedia (990) so: + # - local logins write as user 1000 (owner rwx); + # - NFS (all_squash → uid/gid 990) matches group 990 → rwx (see jellyfin-nfs-export). + fileSystems."/mnt/test" = { + device = "/dev/disk/by-uuid/BC12E55E12E51DE0"; + fsType = "ntfs-3g"; + options = [ + "rw" + "force" + "uid=${toString olivierUid}" + "gid=990" + "umask=0002" + ]; + }; +} diff --git a/modules/hosts/14900k/configuration.nix b/modules/hosts/14900k/configuration.nix index 0e35199..5e560bb 100644 --- a/modules/hosts/14900k/configuration.nix +++ b/modules/hosts/14900k/configuration.nix @@ -17,6 +17,7 @@ ./_private/peripherals.nix # ./_private/printing-epson.nix ./_private/displays.nix + ./_private/media-disk.nix ./_private/jellyfin-nfs-export.nix ]; @@ -108,8 +109,13 @@ services.cloudflare-warp.enable = true; # Native install (avoid flatpak sandbox issues for QSV/VAAPI). handbrake + qbittorrent + # Diagnostics libva-utils # vainfo + vlc + element-desktop + thunderbird ]; @@ -136,6 +142,7 @@ services.cloudflare-warp.enable = true; self.homeManagerModules.wisdomAppsDiscord self.homeManagerModules.wisdomAppsSpotify self.homeManagerModules.wisdomAppsLocalsend + self.homeManagerModules.wisdomAppsSpacedrive self.homeManagerModules.wisdomAppsPokeclicker self.homeManagerModules.wisdomDesktopScreenshot self.homeManagerModules.wisdomDesktopGtkQtTheming @@ -171,6 +178,7 @@ services.cloudflare-warp.enable = true; spotify.enable = true; spotify.openDiscoveryFirewall = true; localsend.enable = true; + spacedrive.enable = true; pokeclicker.enable = true; }; diff --git a/modules/hosts/ideapad/_private/platform.nix b/modules/hosts/ideapad/_private/platform.nix index 5053588..028a7b9 100644 --- a/modules/hosts/ideapad/_private/platform.nix +++ b/modules/hosts/ideapad/_private/platform.nix @@ -7,9 +7,12 @@ powerManagement.enable = true; # ─────────────────────── logind: lid & power button ─────────────────────── - # Closing the lid suspends, even on AC — Duet 3 is a tablet, treat it like one. - # Short press on power: suspend (matches ChromeOS/iOS); long press: poweroff. - # The DMS bar power menu is the way to reboot / shut down explicitly. + # Tablet form factor: lid close = suspend (even on AC), short power-press = suspend, long + # power-press = poweroff. Niri's own power-key handler must stay disabled — see the + # `input.disable-power-key-handling` flag in `_private/touch-tablet.nix` — otherwise niri's + # `block` inhibitor on `handle-power-key` pre-empts logind and turns the wake-from-suspend + # press (which the EC re-delivers as KEY_POWER) into an immediate re-suspend loop + # (https://github.com/niri-wm/niri/issues/2233). services.logind.settings.Login = { HandleLidSwitch = "suspend"; HandleLidSwitchExternalPower = "suspend"; diff --git a/modules/hosts/ideapad/_private/touch-tablet.nix b/modules/hosts/ideapad/_private/touch-tablet.nix index 912e72c..d174e09 100644 --- a/modules/hosts/ideapad/_private/touch-tablet.nix +++ b/modules/hosts/ideapad/_private/touch-tablet.nix @@ -1,17 +1,10 @@ # Host-only: ideapad tablet ergonomics — touchscreen calibration, IIO sensors, virtual keyboard, -# and per-session helper daemons (tablet-mode toggle + auto-rotation via iio-sensor-proxy) for both -# Niri and Hyprland. +# touch-controller resume fix, and per-session helper daemons (tablet-mode toggle + auto-rotation +# via iio-sensor-proxy) for both Niri and Hyprland. Lives at the NixOS layer because the hardware +# bits are system-wide; the per-compositor autostart hooks are gated on `chiasson.desktop..enable` +# so they stay dormant if you pick the other session at the greeter. # -# Why all of this lives at the *NixOS* layer (not the home-manager catalog under wisdom/): -# - The hardware bits (`hardware.sensor.iio.enable`, the udev calibration matrix) are system-wide -# and tied to this exact device, so they belong with the host module. -# - The compositor helpers run via session-specific autostart hooks (Niri `spawn-at-startup`, -# Hyprland `exec-once`); the wiring is gated on the matching `chiasson.desktop..enable`, -# so picking a different session at the greeter just leaves them dormant. -# -# Two compositor flavours of each daemon: -# - Hyprland (CW transforms via `hyprctl`) — original; matches the old NixOS-New setup. -# - Niri (CCW transforms via `niri msg output`) — needed because Niri is the V2 default. +# Hyprland uses CW transforms via `hyprctl`; Niri uses CCW transforms via `niri msg output`. { config, lib, @@ -268,15 +261,33 @@ in # ─────────────────────── Hardware ─────────────────────── hardware.sensor.iio.enable = true; - # Touchscreen calibration — solved empirically with `niri msg output DSI-1 transform normal` - # in the natural kb-down pose. The panel's touch hardware reports raw coordinates already - # aligned with the panel-native frame (HW(visual_top_left) = (0,0), etc.), so identity is - # correct. `niri input.touch.map-to-output = "DSI-1"` then handles per-orientation rotation - # on top — never re-tune this matrix per orientation; rotate the *output* instead. + # Touchscreen calibration — identity matrix is correct: hardware coordinates are already aligned + # with the panel-native frame, and per-orientation rotation is handled by `niri msg output`, + # not by re-tuning this matrix. Rotate the *output*, never this matrix. services.udev.extraRules = '' SUBSYSTEM=="input", ENV{ID_INPUT_TOUCHSCREEN}=="1", ENV{LIBINPUT_CALIBRATION_MATRIX}="1 0 0 0 1 0" ''; + # ─────────────────────── Touch controller resume fix ─────────────────────── + # The hid-over-i2c touch controller at i2c bus 4-0001 wedges across S3 suspend: after resume it + # re-enumerates with the correct capabilities but reports zero events on touch. Cycling the + # `i2c_hid_of` driver (unbind + bind) un-wedges it. systemd-sleep runs every executable in + # `/etc/systemd/system-sleep/` with `$1 = pre|post`; we only act on `post`. Driver name is + # discovered at runtime so a future kernel rename to `i2c_hid` doesn't break this. + environment.etc."systemd/system-sleep/ideapad-touch-rebind".source = + pkgs.writeShellScript "ideapad-touch-rebind" '' + set -eu + [ "$1" = post ] || exit 0 + dev=4-0001 + dev_dir=/sys/bus/i2c/devices/$dev + [ -L "$dev_dir/driver" ] || exit 0 + drv=$(${pkgs.coreutils}/bin/basename "$(${pkgs.coreutils}/bin/readlink "$dev_dir/driver")") + echo "ideapad-touch-rebind: cycling $drv for $dev" >&2 + echo "$dev" > "/sys/bus/i2c/drivers/$drv/unbind" || true + ${pkgs.coreutils}/bin/sleep 0.3 + echo "$dev" > "/sys/bus/i2c/drivers/$drv/bind" + ''; + # ─────────────────────── User-facing tools ─────────────────────── # System-wide so any user session (Niri or Hyprland) can launch wvkbd / hyprctl / niri-msg helpers. environment.systemPackages = [ @@ -297,6 +308,12 @@ in chiasson.desktop.niri.extraSettings = lib.mkIf config.chiasson.desktop.niri.enable { input.touch.map-to-output = "DSI-1"; + # Required for logind's `HandlePowerKey` in `_private/platform.nix` to take effect: otherwise + # niri grabs a `block` inhibitor on `handle-power-key` and suspends via D-Bus, including on + # the EC's wake-from-suspend KEY_POWER event → instant re-suspend loop. + # https://github.com/niri-wm/niri/issues/2233 + input."disable-power-key-handling" = _: { }; + # wrapper-modules schema: each entry is a `command argv` list of strings (or a single string). spawn-at-startup = [ [ "ideapad-niri-autorotate-daemon" ] diff --git a/modules/hosts/ideapad/configuration.nix b/modules/hosts/ideapad/configuration.nix index 56f4f5e..d663ccc 100644 --- a/modules/hosts/ideapad/configuration.nix +++ b/modules/hosts/ideapad/configuration.nix @@ -1,10 +1,9 @@ { self, inputs, ... }: { # Lenovo Chromebook Duet 3 (`lenovo-wormdingler`) on Mobile NixOS. - # - # Phase 1 (minimal bootstrap) lived here previously; we now run the full V2 stack: - # mobile-nixos device + Niri/Hyprland/DMS, DankGreeter, Waydroid (tablet-class), wvkbd, - # IIO sensors, touchscreen calibration, attic cache, sops, and the standard user catalog. + # Full V2 stack: mobile-nixos device + Niri/Hyprland/DMS, DankGreeter, wvkbd, IIO sensors, + # touchscreen calibration + resume-rebind, attic cache, sops, and the standard user catalog. + # Host-only quirks live in `_private/touch-tablet.nix` and `_private/platform.nix`. flake.nixosModules.ideapadConfiguration = { self, @@ -102,9 +101,8 @@ }; # ─────────────────────── Desktop ─────────────────────── - # Both compositors enabled; DankGreeter lets you pick at login. Default = Niri (V2 convention), - # Hyprland session is where the tablet-mode + autorotate daemons in `_private/touch-tablet.nix` - # actually run (they hook `exec-once`). + # Both compositors are enabled — DankGreeter picks at login, V2 default is Niri. + # Per-session tablet-mode / autorotate daemons live in `_private/touch-tablet.nix`. chiasson.desktop = { niri.enable = true; hyprland.enable = true; @@ -113,6 +111,7 @@ shell = "dms"; shells.dms = { enableWvkbdToggle = true; + enableRbwLockToggle = true; # Cross-build on the 14900k via binfmt and push back over LAN — much faster than # rebuilding aarch64 closure on the Snapdragon. Mirrors the old NixOS-New flow: # ssh out to nixdesk, run nixos-rebuild --target-host pointing back at us. @@ -126,15 +125,6 @@ ]; }; - # Tablet-class screen → constrain Waydroid to a sane portrait-ish frame and use gesture nav - # instead of 3-button so it feels like the ChromeOS tablet UI. - #waydroid = { - # enable = true; - # multiWindows = false; - # width = 1600; - # height = 960; - # navigationMode = "gestures"; - #}; }; # ─────────────────────── Users / HM ─────────────────────── @@ -146,10 +136,12 @@ self.homeManagerModules.wisdomTerminalsKitty self.homeManagerModules.wisdomBrowsersZen self.homeManagerModules.wisdomEditorsKate + self.homeManagerModules.wisdomEditorsCursor self.homeManagerModules.wisdomShellFish self.homeManagerModules.wisdomShellOhMyPosh self.homeManagerModules.wisdomAppsSpotify self.homeManagerModules.wisdomAppsLocalsend + self.homeManagerModules.wisdomAppsSpacedrive self.homeManagerModules.wisdomDesktopScreenshot { chiasson.home = { @@ -161,8 +153,10 @@ filebrowsers.dolphin.enable = true; browsers.zen.enable = true; editors.kate.enable = true; + editors.cursor.enable = true; apps.spotify.enable = true; apps.localsend.enable = true; + apps.spacedrive.enable = true; desktop = { screenshot = { enable = true; diff --git a/modules/hosts/nix-server/_services/dispatcharr.nix b/modules/hosts/nix-server/_services/dispatcharr.nix new file mode 100644 index 0000000..3b2fae3 --- /dev/null +++ b/modules/hosts/nix-server/_services/dispatcharr.nix @@ -0,0 +1,38 @@ +# Dispatcharr — IPTV / M3U / EPG / HDHomeRun-style proxy (Docker, AIO image). +# Docs: https://dispatcharr.github.io/Dispatcharr-Docs/ +# Compose reference: https://github.com/Dispatcharr/Dispatcharr/blob/main/docker/docker-compose.aio.yml +# +# Web UI: http://:9191 +# After deploy: create an admin user, add playlists / EPG; point Jellyfin at the tuner/M3U URLs +# Dispatcharr shows in its UI. +{ lib, pkgs, ... }: +{ + systemd.tmpfiles.settings."nix-server-dispatcharr-data" = { + "/var/lib/dispatcharr"."d" = { + mode = "0755"; + user = "root"; + group = "root"; + }; + }; + + systemd.services.docker-dispatcharr.preStart = lib.mkBefore '' + ${pkgs.coreutils}/bin/mkdir -p /var/lib/dispatcharr + ''; + + virtualisation.oci-containers.containers.dispatcharr = { + image = "ghcr.io/dispatcharr/dispatcharr:latest"; + ports = [ "9191:9191" ]; + volumes = [ + "/var/lib/dispatcharr:/data" + ]; + environment = { + DISPATCHARR_ENV = "aio"; + REDIS_HOST = "localhost"; + CELERY_BROKER_URL = "redis://localhost:6379/0"; + DISPATCHARR_LOG_LEVEL = "info"; + TZ = "America/Moncton"; + }; + }; + + networking.firewall.allowedTCPPorts = [ 9191 ]; +} diff --git a/modules/hosts/nix-server/_services/jellyfin-remote-storage.nix b/modules/hosts/nix-server/_services/jellyfin-remote-storage.nix deleted file mode 100644 index 622c543..0000000 --- a/modules/hosts/nix-server/_services/jellyfin-remote-storage.nix +++ /dev/null @@ -1,27 +0,0 @@ -# NFS read-only mount of nixdesk (14900k) bulk storage for extra Jellyfin libraries. -# Source: ssh inventory hostName for 14900k. Export is defined in -# modules/hosts/14900k/_private/jellyfin-nfs-export.nix -# -# In Jellyfin (in addition to local /var/lib/media/...), add e.g.: -# Movies → /mnt/nixdesk-jellyfin/movies -# Shows → /mnt/nixdesk-jellyfin/tv -{ ... }: -let - # Must match LAN IP of the NFS server (flake `sshInventory` → hosts."14900k".hostName). - nfsExportHost = "192.168.2.25"; -in -{ - fileSystems."/mnt/nixdesk-jellyfin" = { - device = "${nfsExportHost}:/mnt/test/jellyfin"; - fsType = "nfs"; - options = [ - "rw" - "noatime" - "nofail" - "_netdev" - "x-systemd.automount" - "x-systemd.idle-timeout=600" - ]; - }; - -} diff --git a/modules/hosts/nix-server/_services/jellyfin.nix b/modules/hosts/nix-server/_services/jellyfin.nix index 94310bf..0060756 100644 --- a/modules/hosts/nix-server/_services/jellyfin.nix +++ b/modules/hosts/nix-server/_services/jellyfin.nix @@ -1,5 +1,5 @@ # Jellyfin (native NixOS service). Local media: /var/lib/media (group `media`; jellyfin + server). -# Dashboard: Movies → /var/lib/media/movies, Shows → /var/lib/media/tv (see jellyfin-remote-storage.nix +# Dashboard: Movies → /var/lib/media/movies, Shows → /var/lib/media/tv (see nixdesk-nfs-client.nix # for bulk libraries on nixdesk at /mnt/nixdesk-jellyfin/{movies,tv}). # Do not use "Mixed Movies and Shows" (deprecated): https://jellyfin.org/docs/general/server/media/mixed-movies-and-shows # Dedicated disk: fileSystems."/var/lib/media" in hardware.nix, then fix ownership. @@ -53,8 +53,9 @@ # not writable by uid jellyfin (it only had group `jellyfin`), so deletes fail. systemd.services.jellyfin.serviceConfig = { SupplementaryGroups = [ "media" ]; - # Jellyfin libraries may live on NFS (e.g. /mnt/nixdesk-jellyfin). PrivateUsers breaks - # uid mapping for NFS auth in practice; disable so deletes use the real host jellyfin uid. + # Jellyfin libraries on NFS (e.g. /mnt/media, /mnt/nixdesk-jellyfin). PrivateUsers breaks + # uid mapping for NFS auth in practice; disable so metadata writes use the real jellyfin uid + # (squashed to olivier:nfsmedia on nixdesk exports). PrivateUsers = lib.mkForce false; }; } diff --git a/modules/hosts/nix-server/_services/nixdesk-nfs-client.nix b/modules/hosts/nix-server/_services/nixdesk-nfs-client.nix new file mode 100644 index 0000000..ac9e0e5 --- /dev/null +++ b/modules/hosts/nix-server/_services/nixdesk-nfs-client.nix @@ -0,0 +1,41 @@ +# NFS mounts of nixdesk (14900k) bulk storage for nix-server. Exports live in +# modules/hosts/14900k/_private/jellyfin-nfs-export.nix +# +# Jellyfin library paths (see also services/jellyfin.nix): +# Movies → /mnt/nixdesk-jellyfin/movies +# Shows → /mnt/nixdesk-jellyfin/tv +# +# If you see "Stale file handle" under /mnt after changing exports or fsid on nixdesk, drop the +# old client mount and let automount reattach, e.g.: +# sudo umount -l /mnt/nixdesk-jellyfin +# ls /mnt/nixdesk-jellyfin +# (or reboot nix-server.) +{ ... }: +let + nfsExportHost = "192.168.2.25"; + # nfsvers+tcp: predictable Linux↔Linux; lookupcache=none: fewer stale dentries after export changes. + nfsClientOpts = [ + "rw" + "noatime" + "nofail" + "_netdev" + "nfsvers=3" + "tcp" + "lookupcache=none" + "x-systemd.automount" + "x-systemd.idle-timeout=3600" + ]; +in +{ + fileSystems."/mnt/nixdesk-jellyfin" = { + device = "${nfsExportHost}:/mnt/test/jellyfin"; + fsType = "nfs"; + options = nfsClientOpts; + }; + + fileSystems."/mnt/media" = { + device = "${nfsExportHost}:/mnt/media"; + fsType = "nfs"; + options = nfsClientOpts; + }; +} diff --git a/modules/hosts/nix-server/configuration.nix b/modules/hosts/nix-server/configuration.nix index 49f3c7b..f0e32a4 100644 --- a/modules/hosts/nix-server/configuration.nix +++ b/modules/hosts/nix-server/configuration.nix @@ -20,7 +20,7 @@ ./_services/swiftshare.nix ./_services/immich.nix ./_services/jellyfin.nix - ./_services/jellyfin-remote-storage.nix + ./_services/nixdesk-nfs-client.nix ./_services/ddrm-media-server.nix ./_services/sonarr.nix ./_services/prowlarr.nix @@ -28,6 +28,7 @@ ./_services/radarr.nix ./_services/qbittorrent.nix ./_services/seerr.nix + ./_services/dispatcharr.nix ]; boot.loader.grub = { diff --git a/modules/hosts/t2mbp/configuration.nix b/modules/hosts/t2mbp/configuration.nix index 493c844..150f6f8 100644 --- a/modules/hosts/t2mbp/configuration.nix +++ b/modules/hosts/t2mbp/configuration.nix @@ -95,6 +95,9 @@ }; defaultSession = "niri"; shell = "dms"; + shells.dms = { + enableRbwLockToggle = true; + }; }; chiasson.system = { diff --git a/modules/hosts/uConsole/_private/cockpit-file-sharing-services.nix b/modules/hosts/uConsole/_private/cockpit-file-sharing-services.nix new file mode 100644 index 0000000..cd22ff5 --- /dev/null +++ b/modules/hosts/uConsole/_private/cockpit-file-sharing-services.nix @@ -0,0 +1,37 @@ +{ config, ... }: +{ + # cockpit-file-sharing expects a live Samba stack: /etc/samba/smb.conf, smbd, and + # `include = registry` in [global] for net registry share management. + services.samba = { + enable = true; + openFirewall = true; + winbindd.enable = false; + settings.global = { + workgroup = "WORKGROUP"; + "server string" = config.networking.hostName; + include = "registry"; + }; + }; + + services.nfs.server = { + enable = true; + mountdPort = 4000; + lockdPort = 4001; + statdPort = 4002; + }; + + networking.firewall.allowedTCPPorts = [ + 111 + 2049 + 4000 + 4001 + 4002 + ]; + networking.firewall.allowedUDPPorts = [ + 111 + 2049 + 4000 + 4001 + 4002 + ]; +} diff --git a/modules/hosts/uConsole/_private/cockpit-file-sharing/package.nix b/modules/hosts/uConsole/_private/cockpit-file-sharing/package.nix new file mode 100644 index 0000000..11e7c13 --- /dev/null +++ b/modules/hosts/uConsole/_private/cockpit-file-sharing/package.nix @@ -0,0 +1,60 @@ +{ + acl, + bash, + coreutils, + dpkg, + fetchurl, + findutils, + hostname, + iproute2, + jq, + lib, + nfs-utils, + python3Packages, + samba, + stdenv, + systemd, +}: + +stdenv.mkDerivation { + pname = "cockpit-file-sharing"; + version = "4.5.6-1"; + + src = fetchurl { + url = "https://github.com/45Drives/cockpit-file-sharing/releases/download/v4.5.6-1/cockpit-file-sharing_4.5.6-1jammy_all.deb"; + hash = "sha256-ViTdhiCmqwuBvAfzT8hr2kqZqyWkV9OZ9FEPD10ajF8="; + }; + + nativeBuildInputs = [ dpkg ]; + + unpackPhase = "dpkg-deb -x $src source"; + + installPhase = '' + runHook preInstall + mkdir -p $out/share/cockpit + cp -r source/usr/share/cockpit/file-sharing $out/share/cockpit/ + runHook postInstall + ''; + + passthru.cockpitPath = [ + acl + bash + coreutils + findutils + hostname + iproute2 + jq + nfs-utils + python3Packages.botocore + samba + systemd + ]; + + meta = { + description = "Cockpit plugin to manage Samba and NFS file sharing (45Drives)"; + homepage = "https://github.com/45Drives/cockpit-file-sharing"; + license = lib.licenses.gpl3Plus; + platforms = lib.platforms.linux; + sourceProvenance = with lib.sourceTypes; [ binaryNativeCode ]; + }; +} diff --git a/modules/hosts/uConsole/_private/cockpit.nix b/modules/hosts/uConsole/_private/cockpit.nix new file mode 100644 index 0000000..acbee79 --- /dev/null +++ b/modules/hosts/uConsole/_private/cockpit.nix @@ -0,0 +1,22 @@ +{ config, pkgs, ... }: +let + cockpitFileSharing = pkgs.callPackage ./cockpit-file-sharing/package.nix { }; +in +{ + imports = [ ./cockpit-file-sharing-services.nix ]; + + services.cockpit = { + enable = true; + openFirewall = true; + allowed-origins = [ + "https://${config.networking.hostName}:${toString config.services.cockpit.port}" + "https://192.168.2.60:${toString config.services.cockpit.port}" + ]; + plugins = with pkgs; [ + cockpit-files + cockpit-machines + cockpit-podman + cockpitFileSharing + ]; + }; +} diff --git a/modules/hosts/uConsole/_private/wifi-brcmfmac.nix b/modules/hosts/uConsole/_private/wifi-brcmfmac.nix new file mode 100644 index 0000000..8c953d6 --- /dev/null +++ b/modules/hosts/uConsole/_private/wifi-brcmfmac.nix @@ -0,0 +1,35 @@ +# Pi 5 / CM5 onboard Wi-Fi (brcmfmac). Without a regulatory domain the driver scans +# disallowed channels and spams: brcmf_set_channel: set chanspec … fail, reason -52 +# (open raspberrypi/linux#6049). Wi-Fi often still works; logs and scans are noisy. +{ pkgs, ... }: +let + wifiCountry = "CA"; # change if you are not in Canada +in +{ + boot.kernelParams = [ + "cfg80211.ieee80211_regdom=${wifiCountry}" + ]; + + # Firmware country code (config.txt) — picked up before userspace regdom. + hardware.raspberry-pi.extra-config = '' + country=${wifiCountry} + ''; + + # Apply regdom before NetworkManager starts scanning. + systemd.services.wifi-regulatory-domain = { + description = "Set Wi-Fi regulatory domain for brcmfmac"; + wantedBy = [ "multi-user.target" ]; + before = [ "NetworkManager.service" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + ${pkgs.iw}/bin/iw reg set ${wifiCountry} + ''; + }; + + networking.networkmanager.wifi.powersave = false; + networking.networkmanager.wifi.scanRandMacAddress = false; + networking.networkmanager.connectionConfig."wifi.bgscan" = "0"; +} diff --git a/modules/hosts/uConsole/configuration.nix b/modules/hosts/uConsole/configuration.nix index d3202cf..02502cf 100644 --- a/modules/hosts/uConsole/configuration.nix +++ b/modules/hosts/uConsole/configuration.nix @@ -29,7 +29,9 @@ self.nixosModules."client-services" ./_private/platform.nix + ./_private/wifi-brcmfmac.nix ./_private/services.nix + ./_private/cockpit.nix ./_private/activation.nix ./_private/4g/default.nix ]; diff --git a/modules/system/users/home-integration.nix b/modules/system/users/home-integration.nix index a2cb5c2..e22756b 100644 --- a/modules/system/users/home-integration.nix +++ b/modules/system/users/home-integration.nix @@ -19,6 +19,14 @@ }) names ); inboundUsersAttr = usersLib.inboundHostsAttr selected; + + # HM configures fish in ~/.config/fish but no longer sets /etc/passwd or /etc/shells. + hmFishUsers = + if !hmAvailable then { } + else + lib.filterAttrs ( + name: hmUser: (hmUser.programs.fish.enable or false) && builtins.elem name names + ) config.home-manager.users; in { config = lib.mkMerge [ @@ -46,6 +54,18 @@ chiasson.ssh.inbound.enable = true; chiasson.ssh.inbound.userAuthorizedHosts = inboundUsersAttr; }) + (lib.mkIf (hmFishUsers != { }) { + environment.shells = lib.mkAfter ( + lib.mapAttrsToList ( + _: hmUser: lib.getExe hmUser.programs.fish.package + ) hmFishUsers + ); + users.users = lib.mapAttrs ( + name: hmUser: { + shell = lib.mkForce (lib.getExe hmUser.programs.fish.package); + } + ) hmFishUsers; + }) ]; }; } diff --git a/modules/wisdom/apps/spacedrive/default.nix b/modules/wisdom/apps/spacedrive/default.nix new file mode 100644 index 0000000..4057702 --- /dev/null +++ b/modules/wisdom/apps/spacedrive/default.nix @@ -0,0 +1,26 @@ +{ ... }: { + flake.homeManagerModules.wisdomAppsSpacedrive = + { config, lib, pkgs, ... }: + let + root = config.chiasson.home; + cfg = config.chiasson.home.apps.spacedrive; + spacedrivePkg = pkgs.callPackage ./package { }; + in + { + options.chiasson.home.apps.spacedrive = { + enable = lib.mkEnableOption '' + [Spacedrive](https://spacedrive.com/) v2 alpha — upstream `.deb` repackaged for NixOS. + ''; + + package = lib.mkOption { + type = lib.types.package; + default = spacedrivePkg; + description = "Spacedrive package (defaults to upstream v2.0.0-alpha.2)."; + }; + }; + + config = lib.mkIf (root.enable && cfg.enable) { + home.packages = [ cfg.package ]; + }; + }; +} diff --git a/modules/wisdom/apps/spacedrive/package/default.nix b/modules/wisdom/apps/spacedrive/package/default.nix new file mode 100644 index 0000000..0f2caca --- /dev/null +++ b/modules/wisdom/apps/spacedrive/package/default.nix @@ -0,0 +1,112 @@ +{ + lib, + stdenv, + fetchurl, + dpkg, + makeWrapper, + autoPatchelfHook, + wrapGAppsHook3, + adwaita-icon-theme, + ffmpeg, + gdk-pixbuf, + glib, + glib-networking, + gst_all_1, + gtk3, + hicolor-icon-theme, + libsoup_3, + webkitgtk_4_1, + xdotool, +}: + +let + version = "2.0.0-alpha.2"; + + srcInfo = + if stdenv.hostPlatform.system == "x86_64-linux" then + { + url = "https://github.com/spacedriveapp/spacedrive/releases/download/v${version}/Spacedrive-linux-x86_64.deb"; + hash = "sha256-KzRPBtyX5x4ZLlZd6SgAS/cy/7irXt7v+b3Yuq9GETo="; + } + else if stdenv.hostPlatform.system == "aarch64-linux" then + { + url = "https://github.com/spacedriveapp/spacedrive/releases/download/v${version}/Spacedrive-linux-aarch64.deb"; + hash = "sha256-Arq4seJxd69XdraIaYJSv1p9g+Bz/7rez/l9EP6dc9k="; + } + else + throw "spacedrive ${version}: unsupported platform: ${stdenv.hostPlatform.system}"; +in +stdenv.mkDerivation { + pname = "spacedrive"; + inherit version; + + src = fetchurl srcInfo; + + nativeBuildInputs = [ + dpkg + makeWrapper + autoPatchelfHook + wrapGAppsHook3 + ]; + + buildInputs = [ + adwaita-icon-theme + ffmpeg + gdk-pixbuf + glib + glib-networking + gtk3 + hicolor-icon-theme + libsoup_3 + webkitgtk_4_1 + xdotool + gst_all_1.gst-plugins-ugly + gst_all_1.gst-plugins-bad + gst_all_1.gst-plugins-base + gst_all_1.gstreamer + ]; + + # WebKitGTK + TLS + icons; ffmpeg/ffprobe on PATH (alpha Linux builds omit bundled ffmpeg). + preFixup = '' + gappsWrapperArgs+=( + "--prefix" "PATH" ":" "${lib.makeBinPath [ ffmpeg ]}" + "--set-default" "WEBKIT_DISABLE_DMABUF_RENDERER" "1" + ) + ''; + + postFixup = '' + # Core daemon is not GTK-linked; wrapGAppsHook3 skips it — still needs ffmpeg for media paths. + wrapProgram $out/bin/sd-daemon --prefix PATH : "${lib.makeBinPath [ ffmpeg ]}" + ''; + + unpackPhase = "dpkg-deb -x $src source"; + + installPhase = '' + runHook preInstall + + mkdir -p $out/{bin,lib,share} + cp -r source/usr/bin/* $out/bin/ + cp -r source/usr/lib/* $out/lib/ + cp -r source/usr/share/* $out/share/ + + ln -sf Spacedrive $out/bin/spacedrive + + substituteInPlace $out/share/applications/Spacedrive.desktop \ + --replace-fail 'Exec=Spacedrive' 'Exec=spacedrive' + + runHook postInstall + ''; + + meta = with lib; { + description = "Local-first file manager and virtual distributed filesystem (v2 alpha)"; + homepage = "https://spacedrive.com"; + changelog = "https://github.com/spacedriveapp/spacedrive/releases/tag/v${version}"; + license = licenses.agpl3Plus; + platforms = [ + "x86_64-linux" + "aarch64-linux" + ]; + mainProgram = "spacedrive"; + sourceProvenance = with sourceTypes; [ binaryNativeCode ]; + }; +} diff --git a/modules/wisdom/default.nix b/modules/wisdom/default.nix index 4e337f6..17c565b 100644 --- a/modules/wisdom/default.nix +++ b/modules/wisdom/default.nix @@ -3,6 +3,7 @@ imports = [ ./apps/discord.nix ./apps/localsend.nix + ./apps/spacedrive ./apps/pokeclicker ./apps/spotify.nix ./browsers/orion.nix diff --git a/modules/wisdom/shells/fish.nix b/modules/wisdom/shells/fish.nix index ec16f77..5e5fb83 100644 --- a/modules/wisdom/shells/fish.nix +++ b/modules/wisdom/shells/fish.nix @@ -6,7 +6,17 @@ cfg = config.chiasson.home.shell.fish; in { - options.chiasson.home.shell.fish.enable = lib.mkEnableOption "Fish + grc, quiet greeting, rbw SSH socket defaults."; + options.chiasson.home.shell.fish = { + enable = lib.mkEnableOption "Fish + grc, quiet greeting, rbw SSH socket defaults."; + + nixYourShell = { + enable = lib.mkEnableOption '' + Wrap `nix` / `nix-shell` so ephemeral shells use fish (and keep oh-my-posh) instead of bash. + '' // { + default = true; + }; + }; + }; config = lib.mkIf (root.enable && cfg.enable) { # `fishPlugins.grc` only installs the plugin; the wrapper invokes the `grc` binary. @@ -32,6 +42,11 @@ } ]; }; + + programs.nix-your-shell = lib.mkIf cfg.nixYourShell.enable { + enable = true; + enableFishIntegration = true; + }; }; }; }