me like nix
1{
2 pkgs,
3 lib,
4 config,
5 inputs,
6 ...
7}:
8
9let
10 jellyfinKodiSyncQueue = pkgs.fetchzip {
11 url = "https://repo.jellyfin.org/releases/plugin/kodi-sync-queue/kodi-sync-queue_15.0.0.0.zip";
12 stripRoot = false;
13 hash = "sha256-xtlG3UQ/WClt/Hvxe+oId2CeJ+PWMDXBUJXh5+k+mZQ=";
14 };
15
16 bambu-studio =
17 let
18 pname = "bambu-studio";
19 version = "02.05.00.67";
20 ubuntu_version = "24.04_PR-9540";
21
22 src = pkgs.fetchurl {
23 url = "https://github.com/bambulab/BambuStudio/releases/download/v${version}/Bambu_Studio_ubuntu-${ubuntu_version}.AppImage";
24 hash = "sha256-3ubZblrsOJzz1p34QiiwiagKaB7nI8xDeadFWHBkWfg=";
25 };
26
27 appimage-contents = pkgs.appimageTools.extractType2 {
28 inherit src pname version;
29 };
30
31 wrapped = pkgs.appimageTools.wrapType2 {
32 inherit src pname version;
33
34 profile = ''
35 export SSL_CERT_FILE="${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"
36 export GIO_MODULE_DIR="${pkgs.glib-networking}/lib/gio/modules/"
37 export __GLX_VENDOR_LIBRARY_NAME=nvidia
38 '';
39
40 extraPkgs =
41 p: with p; [
42 cacert
43 glib
44 glib-networking
45 gst_all_1.gst-plugins-bad
46 gst_all_1.gst-plugins-base
47 gst_all_1.gst-plugins-good
48 webkitgtk_4_1
49 ];
50 };
51 in
52 pkgs.runCommand "bambu-studio-${version}" { } ''
53 mkdir -p $out/bin
54 ln -s ${wrapped}/bin/${pname} $out/bin/bambu-studio
55 ln -s ${wrapped}/bin/${pname} $out/bin/BambuStudio
56
57 # Install desktop file with correct exec path
58 mkdir -p $out/share/applications
59 substitute ${appimage-contents}/BambuStudio.desktop $out/share/applications/BambuStudio.desktop \
60 --replace-fail "Exec=AppRun" "Exec=$out/bin/BambuStudio"
61
62 # Install icons
63 if [ -d ${appimage-contents}/usr/share/icons ]; then
64 cp -r ${appimage-contents}/usr/share/icons $out/share/
65 fi
66 '';
67 # Steam/gamescope calls steamos-session-select when the user presses
68 # "Switch to Desktop". Without this script, the button does nothing.
69 # Returning 0 lets gamescope proceed to exit, returning to greetd/regreet.
70 steamos-session-select = pkgs.writeShellScriptBin "steamos-session-select" ''
71 echo "Switching session to: $1"
72 '';
73
74 # Pin gamescope to 3.14.29 — versions 3.15+ crash on NVIDIA due to
75 # a confirmed driver bug (#5701801) in Vulkan YCbCr sampler pipeline compilation.
76 gamescope_3_14 = pkgs.gamescope.overrideAttrs (old: {
77 version = "3.14.29";
78 src = pkgs.fetchFromGitHub {
79 owner = "ValveSoftware";
80 repo = "gamescope";
81 tag = "3.14.29";
82 fetchSubmodules = true;
83 hash = "sha256-q3HEbFqUeNczKYUlou+quxawCTjpM5JNLrML84tZVYE=";
84 };
85 # Drop the system-libraries fetchpatch (3rd patch) — only applies to 3.16.x
86 patches = lib.take 2 (old.patches or []);
87 # 3.14.29 uses pkg-config for glm/stb, not meson subprojects
88 mesonFlags = builtins.filter
89 (f: builtins.match ".*glm_include_dir.*" f == null
90 && builtins.match ".*stb_include_dir.*" f == null)
91 (old.mesonFlags or []);
92 # Fix vendored OpenVR CMakeLists.txt requiring CMake < 3.5 (incompatible with CMake 4.x)
93 env = (old.env or { }) // { CMAKE_POLICY_VERSION_MINIMUM = "3.5"; };
94 # default_extras_install.sh doesn't exist in 3.14.29;
95 # also provide glm/stb as meson subprojects pointing to system packages
96 postPatch = builtins.replaceStrings
97 [ "patchShebangs default_extras_install.sh" ] [ "" ]
98 (old.postPatch or "")
99 + ''
100 rm -rf subprojects/glm subprojects/glm.wrap subprojects/stb subprojects/stb.wrap
101 mkdir -p subprojects/glm subprojects/stb
102
103 cat > subprojects/glm/meson.build << 'GLMEOF'
104 project('glm', 'cpp', version: '1.0.0')
105 glm_dep = declare_dependency(include_directories: include_directories('${lib.getInclude pkgs.glm}/include', is_system: true))
106 meson.override_dependency('glm', glm_dep)
107 GLMEOF
108
109 cat > subprojects/stb/meson.build << 'STBEOF'
110 project('stb', 'c', version: '0.0.1')
111 stb_dep = declare_dependency(include_directories: include_directories('${lib.getInclude pkgs.stb}/include/stb', is_system: true))
112 meson.override_dependency('stb', stb_dep)
113 STBEOF
114 '';
115 });
116in
117{
118 imports = [
119 # Include the results of the hardware scan.
120 ./hardware-configuration.nix
121 ../common/common.nix
122 ];
123
124 networking.hostName = "mira"; # Define your hostname.
125
126 # Prevent NetworkManager from managing USB Ethernet
127 networking.networkmanager.unmanaged = [ "interface-name:enp0s20f0u4u3" ];
128
129 # ZRAM swap to prevent OOM freezes
130 zramSwap = {
131 enable = true;
132 memoryPercent = 50;
133 };
134
135 # Kill runaway processes before the system locks up
136 services.earlyoom = {
137 enable = true;
138 freeMemThreshold = 5;
139 freeSwapThreshold = 5;
140 enableNotifications = true;
141 };
142 # networking.wireless.enable = true; # Enables wireless support via wpa_supplicant.
143
144 # Configure network proxy if necessary
145 # networking.proxy.default = "http://user:password@proxy:port/";
146 # networking.proxy.noProxy = "127.0.0.1,localhost,internal.domain";
147
148 # this is like a network devices discovery thing
149 services.avahi = {
150 enable = true;
151 nssmdns4 = true;
152 openFirewall = true;
153 };
154
155 services.copyparty.enable = true;
156
157 services.openssh = {
158 enable = true;
159 settings = {
160 PasswordAuthentication = false;
161 KbdInteractiveAuthentication = false;
162 PermitRootLogin = "no";
163 AllowUsers = [ "sean" ];
164 };
165 };
166
167 users.users.sean.openssh.authorizedKeys.keys = [
168 "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDCIqgZ7kedxo+mOW7YG73Vp3zel3h180y3GKvHtRsXfGlpIIvRDy7pgCBQ4AGXYD4y78URQmFohYSAPqCPOPaWcU2un3XG9KvCzEsHmsbskPonitUmCiKvrKkb6oW4jCBtd7AEtBn+AiajAQFtPZ7NN2Df3AmTypvR6Irg7R+nxnfc9NTIHmGvxSDyWcbb4pguL20sctUSqGL6xGh8q/bqhdOThSimM+z9bEUNxK/5rPhwkNniMrp4pJcUrUiAh5/4DiRFG6KT+oeg+/myoz/Z1sPvAs7u/8JDQI4RshRD8Hu0oTkRBN6Hxj478q2SXbeBUZlD6IdjP3RhGpmSecoDdtWqKbpuV3eVRtQtba3KL86GBeV/bugaOdJ1Aud+1SOFJreAAuvxzMMKT+cdQZk6oOPP148DA/No+mDm/2S43lcdCXh79wA6YRAmKQ8jmZxTCtPutrvuZK1rguvvUlEoG/vhdNHh7eDa4Td07V6bjCRPUl8qk/e4M0E3pwsTlZc="
169 "no-touch-required sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAILdilHXHdAP/V8Zq28EzHKtLAMMaFPu4+1det2N50QfhAAAABHNzaDo= sean@framework16"
170 ];
171
172 # List services that you want to enable:
173 services.flaresolverr.enable = true;
174 age.secrets.wireguard.file = ../../secrets/wireguard.age;
175
176 nixarr = {
177 enable = true;
178 mediaDir = "/mnt/storage1/nixarr/media";
179 vpn = {
180 enable = true;
181 wgConf = config.age.secrets.wireguard.path;
182 };
183
184 jellyfin = {
185 enable = true;
186 openFirewall = true;
187 };
188
189 transmission = {
190 enable = true;
191 vpn.enable = true;
192 };
193 sabnzbd = {
194 enable = true;
195 vpn.enable = true;
196 openFirewall = true;
197 };
198
199 prowlarr.enable = true;
200 radarr.enable = true;
201 sonarr.enable = true;
202 jellyseerr = {
203 enable = true;
204 openFirewall = true;
205 };
206
207 recyclarr = {
208 enable = true;
209 configuration = {
210 sonarr = {
211 series = {
212 base_url = "http://localhost:8989";
213 api_key = "!env_var SONARR_API_KEY";
214 quality_definition = {
215 type = "series";
216 };
217 delete_old_custom_formats = true;
218 custom_formats = [
219 {
220 trash_ids = [
221 "85c61753df5da1fb2aab6f2a47426b09" # BR-DISK
222 "9c11cd3f07101cdba90a2d81cf0e56b4" # LQ
223 ];
224 assign_scores_to = [
225 {
226 name = "WEB-DL (1080p)";
227 score = -10000;
228 }
229 ];
230 }
231 ];
232 };
233 };
234 radarr = {
235 movies = {
236 base_url = "http://localhost:7878";
237 api_key = "!env_var RADARR_API_KEY";
238 quality_definition = {
239 type = "movie";
240 };
241 delete_old_custom_formats = true;
242 custom_formats = [
243 {
244 trash_ids = [
245 "570bc9ebecd92723d2d21500f4be314c" # Remaster
246 "eca37840c13c6ef2dd0262b141a5482f" # 4K Remaster
247 ];
248 assign_scores_to = [
249 {
250 name = "HD Bluray + WEB";
251 score = 25;
252 }
253 ];
254 }
255 ];
256 };
257 };
258 };
259 };
260 };
261
262 # Install Kodi Sync Queue plugin into Jellyfin
263 systemd.services.jellyfin.serviceConfig.ExecStartPre =
264 let
265 pluginDir = "/data/.state/nixarr/jellyfin/data/plugins/Kodi Sync Queue/15.0.0.0";
266 in
267 pkgs.writeShellScript "install-jellyfin-plugins" ''
268 mkdir -p "${pluginDir}"
269 cp -f ${jellyfinKodiSyncQueue}/*.dll ${jellyfinKodiSyncQueue}/meta.json "${pluginDir}/"
270 '';
271
272 # MQTT broker for Home Assistant (Tasmota devices, Frigate)
273 services.mosquitto = {
274 enable = true;
275 listeners = [
276 {
277 acl = [ "pattern readwrite #" ];
278 omitPasswordAuth = true;
279 settings.allow_anonymous = true;
280 }
281 ];
282 };
283
284 # Frigate NVR for camera recording and AI object detection
285 services.frigate = {
286 enable = true;
287 hostname = "frigate";
288 settings = {
289 mqtt = {
290 enabled = true;
291 host = "localhost";
292 port = 1883;
293 };
294
295 detectors = {
296 cpu = {
297 type = "cpu";
298 num_threads = 2;
299 };
300 };
301
302 cameras = {
303 picam = {
304 enabled = true;
305 ffmpeg = {
306 hwaccel_args = "preset-nvidia-h264";
307 inputs = [
308 {
309 path = "rtsp://pi:8554/picam";
310 roles = [
311 "detect"
312 "record"
313 ];
314 }
315 ];
316 };
317 detect = {
318 enabled = true;
319 width = 1920;
320 height = 1080;
321 fps = 3;
322 };
323 record = {
324 enabled = true;
325 retain = {
326 days = 7;
327 mode = "active_objects";
328 };
329 };
330 snapshots = {
331 enabled = true;
332 bounding_box = true;
333 retain = {
334 default = 14;
335 };
336 };
337 objects = {
338 track = [
339 "person"
340 "dog"
341 "cat"
342 "car"
343 ];
344 };
345 zones = {
346 driveway = {
347 coordinates = "0,0.243,1,0.544,1,1,0,1,0,0.75";
348 objects = [
349 "person"
350 "car"
351 "dog"
352 "cat"
353 ];
354 };
355 };
356 };
357
358 pizerocam = {
359 enabled = true;
360 ffmpeg = {
361 hwaccel_args = "preset-nvidia-h264";
362 inputs = [
363 {
364 path = "rtsp://pizero:8554/pizerocam";
365 roles = [ "record" ];
366 }
367 ];
368 };
369 detect = {
370 enabled = false;
371 };
372 record = {
373 enabled = true;
374 retain = {
375 days = 2;
376 mode = "all";
377 };
378 };
379 };
380 };
381
382 record = {
383 enabled = true;
384 retain = {
385 days = 7;
386 mode = "active_objects";
387 };
388 };
389 };
390 };
391
392 # Frigate manages many ffmpeg child processes that ignore SIGTERM;
393 # shorten the stop timeout so it doesn't block shutdown for 90s.
394 systemd.services.frigate.serviceConfig = {
395 TimeoutStopSec = 5;
396 KillMode = "mixed"; # SIGTERM to main, SIGKILL to children after timeout
397 };
398
399 # Home Assistant service
400 services.home-assistant = {
401 enable = true;
402 customComponents = with pkgs.home-assistant-custom-components; [
403 frigate
404 ];
405 extraComponents = [
406 "esphome"
407 "met"
408 "radio_browser"
409 "homekit"
410 "homekit_controller"
411 "isal"
412 "mqtt"
413 "tasmota"
414 "wiz"
415 "google_translate" # TTS - was missing gtts module
416 "ecobee" # Was missing pyecobee module
417 "ibeacon" # Was missing ibeacon_ble module
418 "go2rtc" # Camera streaming
419 "generic" # Generic camera integration
420 ];
421 config = {
422 homeassistant = {
423 time_zone = "America/Toronto";
424 };
425 default_config = { };
426 zeroconf = { };
427 # MQTT configuration - broker must be set up via UI
428 mqtt = { };
429 # Automations
430 automation = [
431 {
432 id = "1761448856909";
433 alias = "Lower heat at night";
434 trigger = [
435 {
436 platform = "time";
437 at = "23:00:00";
438 }
439 ];
440 condition = [ ];
441 action = [
442 {
443 action = "climate.set_temperature";
444 target.device_id = "bfe22d32a4532f8ae991d6daffb48267";
445 data = {
446 hvac_mode = "heat";
447 temperature = 18;
448 };
449 }
450 ];
451 mode = "single";
452 }
453 {
454 id = "1766200000001";
455 alias = "Raise heat in morning";
456 trigger = [
457 {
458 platform = "time";
459 at = "06:00:00";
460 }
461 ];
462 condition = [ ];
463 action = [
464 {
465 action = "climate.set_temperature";
466 target.device_id = "bfe22d32a4532f8ae991d6daffb48267";
467 data = {
468 hvac_mode = "heat";
469 temperature = 21;
470 };
471 }
472 ];
473 mode = "single";
474 }
475 {
476 id = "1766153071796";
477 alias = "Close Garage Door";
478 trigger = [
479 {
480 platform = "device";
481 device_id = "d8dedd8cd0ce1488d9830c455bb0a761";
482 domain = "cover";
483 entity_id = "cf36763543169888aa106b1acb02ad72";
484 type = "opened";
485 for = {
486 hours = 0;
487 minutes = 10;
488 seconds = 0;
489 };
490 }
491 ];
492 condition = [ ];
493 action = [
494 {
495 device_id = "d8dedd8cd0ce1488d9830c455bb0a761";
496 domain = "cover";
497 entity_id = "cf36763543169888aa106b1acb02ad72";
498 type = "close";
499 }
500 ];
501 mode = "single";
502 }
503 ];
504 };
505 };
506
507
508 # NVIDIA needs GBM/EGL env vars for cage (wlroots) to initialize GPU on greetd restart
509 services.greetd.settings.default_session.command = lib.mkOverride 49
510 "${pkgs.dbus}/bin/dbus-run-session ${lib.getExe pkgs.cage} -s -d -- env GBM_BACKEND=nvidia-drm __GLX_VENDOR_LIBRARY_NAME=nvidia GDK_SCALE=2 ${lib.getExe pkgs.greetd.regreet}";
511
512 programs.steam = {
513 enable = true;
514 remotePlay.openFirewall = true;
515 gamescopeSession = {
516 enable = true;
517 args = [
518 "-r" "120"
519 "-R" "120"
520 ];
521 env = {
522 STEAM_DESKTOP_SESSION = "niri";
523 ENABLE_GAMESCOPE_WSI = "0";
524 };
525 };
526 extraCompatPackages = with pkgs; [
527 proton-ge-bin
528 ];
529 };
530
531 programs.gamemode.enable = true;
532
533 programs.gamescope = {
534 enable = true;
535 capSysNice = false;
536 package = gamescope_3_14;
537 };
538
539 # Manually add the gamescope capability wrapper without triggering
540 # the steam module's setuid bwrap override (which zeros CapBnd
541 # inside the FHS sandbox, preventing games from launching)
542 security.wrappers.gamescope = {
543 owner = "root";
544 group = "root";
545 source = "${gamescope_3_14}/bin/gamescope";
546 capabilities = "cap_sys_nice+pie";
547 };
548
549 environment.systemPackages = [
550 pkgs.lm_sensors
551 bambu-studio
552 steamos-session-select
553 ];
554
555 # Enable the OpenSSH daemon.
556 # services.openssh.enable = true;
557
558 security.pam.loginLimits = [
559 {
560 domain = "*";
561 type = "soft";
562 item = "nofile";
563 value = "8192";
564 }
565 ];
566
567 # trmnl-rs server
568 systemd.services.trmnl-rs = {
569 description = "TRMNL Server";
570 wantedBy = [ "multi-user.target" ];
571 wants = [ "network-online.target" ];
572 after = [
573 "network-online.target"
574 "nss-lookup.target"
575 ];
576 serviceConfig = {
577 ExecStart = "${inputs.trmnl-rs.packages.x86_64-linux.default}/bin/server";
578 Restart = "on-failure";
579 RestartSec = 5;
580 DynamicUser = true;
581 StateDirectory = "trmnl-rs";
582 WorkingDirectory = "/var/lib/trmnl-rs";
583 };
584 };
585
586 # Open ports in the firewall.
587 networking.firewall.allowedTCPPorts = [
588 8096 # jellyfin
589 5055 # jellyseer
590 3000 # vite dev port
591 1883 # MQTT for Tasmota devices
592 2300 # trmnl
593 5000 # Frigate web UI
594 8971 # Frigate API
595 config.services.home-assistant.config.http.server_port
596 ];
597 networking.firewall.allowedUDPPorts = [
598 ];
599 # networking.firewall.enable = false;
600
601 # This value determines the NixOS release from which the default
602 # settings for stateful data, like file locations and database versions
603 # on your system were taken. It‘s perfectly fine and recommended to leave
604 # this value at the release version of the first install of this system.
605 # Before changing this value read the documentation for this option
606 # (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).
607 system.stateVersion = "25.05"; # Did you read the comment?
608
609}