Compare commits

...

23 Commits

Author SHA1 Message Date
Olivier 380b428d9a Implement r5500 media stack configuration with NFS and Docker services
- Added configuration for media stack on r5500, including paths for Jellyfin, Sonarr, Radarr, and other media services.
- Integrated NFS client for accessing Jellyfin libraries from nixdesk.
- Established Docker services for Dispatcharr and Organizr, including necessary user and group setups.
- Created systemd services for managing media directories and ensuring proper permissions.
2026-06-04 16:57:15 -03:00
Olivier 2a911b057b Add navi deployment module and integrate SSH inventory for remote management
- Introduced a new `navi` module for managing deployments across multiple hosts.
- Enhanced SSH inventory management to support public key application for authorized hosts.
- Configured system deployment builder for seamless integration with Navi.
- Updated various host configurations to enable deployment capabilities and streamline SSH access.
2026-06-04 16:51:30 -03:00
Olivier 403cf2fde5 Update flake.nix to specify DDRM revision; modify 14900k host configuration.nix to enhance git settings and include devenv in services 2026-06-04 16:46:39 -03:00
Olivier 65064d971c Add Moonfin Flatpak bundle support and update Flatpak configuration 2026-06-04 16:44:23 -03:00
Olivier 875fa633c7 Update pokeclicker default.nix to include libxscrnsaver and libxtst dependencies 2026-06-04 16:43:17 -03:00
Olivier 1cbca207b6 Update gtk4 theming configuration in gtk-qt-theming.nix 2026-06-04 16:42:31 -03:00
Olivier f701177197 Disable Hyprland for Ideapad host 2026-06-04 16:41:41 -03:00
Olivier 778cde8c40 Update kernel reference in t2linux to use latest version (7.0) 2026-06-04 16:41:14 -03:00
Olivier f9d5c70fb1 Refactor cursor package handling in home-manager configuration
- Simplified the cursor package selection by removing the deprecated `cursor-agent` option and retaining only `cursor-cli`.
- Updated the activation script to remove the broken `cursor-agent` symlink, ensuring proper resolution to `cursor-cli`.
- Adjusted the configuration to reflect these changes in the Nix IDE tools integration.
2026-06-04 16:40:12 -03:00
Olivier 1fd25998fb Add raspberrypi-utils overlay for handling dropped attributes 2026-06-04 16:39:34 -03:00
Olivier 7994f174f6 Toggle dock visibility to false in home-manager configuration 2026-06-04 16:38:50 -03:00
Olivier d48e4f8a08 Update dgop package reference to use host platform 2026-06-04 16:38:25 -03:00
Olivier dcdd2c2d90 Rebase to flake parts #13 2026-05-30 21:26:13 -03:00
Olivier 9a4ed1b04b Rebase to flake parts #11 2026-05-29 00:08:10 -03:00
Olivier 6978396646 Rebase to flake parts #11 2026-05-25 13:48:47 -03:00
Olivier fba5a7a2aa Rebase to flake parts #10 2026-05-15 00:24:13 -03:00
Olivier f02606902c Rebase to flake parts #9 2026-05-10 01:45:16 -03:00
Olivier 34b89af77f Rebase to flake parts #8 2026-05-08 21:48:22 -03:00
Olivier f98606dcce Rebase to flake parts #7 2026-05-08 19:12:16 -03:00
Olivier 1015cf4577 Rebase to flake parts #6 2026-05-08 19:05:10 -03:00
Olivier d51f41566c Rebase to flake parts #5 2026-05-08 18:59:40 -03:00
Olivier 064ba9655a Rebase to flake parts #4 2026-05-01 17:25:23 -03:00
Olivier 2c576715de Rebase to flake parts #3 2026-05-01 17:19:42 -03:00
159 changed files with 12550 additions and 121 deletions
+26
View File
@@ -0,0 +1,26 @@
keys:
- &primary age1yyzgmazjxkvwtfcv9re3lqmt2ru5dcrfu3sauysm0wzfwzvyap8qkjkq32
- &host_14900k age1elk6zwmcylwfk7gd4pjda7g29upftjvxys8py42s8d42jklnyv7s7dm9z2
- &host_uConsole age193gw802ytal7h5p5q37kpd9079k2vsflzmnvupcwfxh2kjdrwqtsk3g6rm
- &host_t2mbp age1yr7vurfxc3w8ewfw9djfm54atw6ayze69qglamecuft5q0n9gu2sadsa2m
- &host_ideapad age1hya7pgpe8zal52w3pjf036tpapmehedatfm4r84h30t4wuh079ssedfd37
- &host_nix-server age1p05z980kdtngk9mw67hfev72h7xhslplpxfk9yskgmf0hl4lu3ls04zht9
- &host_r5500 age1pewusvlcgzcnk0kpskgc9qr6jlq8s2yzwnqrnh84p7v5z0kj3qhsya8h2x
creation_rules:
- path_regex: secrets/[^/]+\.(yaml|json|env|ini)$
key_groups:
- age:
- *primary
- *host_14900k
- *host_uConsole
- *host_t2mbp
- *host_ideapad
- *host_nix-server
- *host_r5500
# Host secrets at modules/hosts/nix-server/secrets.yaml (see configuration imports),
# or optional extra files under _secrets/.
- path_regex: modules/hosts/nix-server/(secrets\.(yaml|json|env|ini)|_secrets/.*\.(yaml|json|env|ini))$
key_groups:
- age:
- *primary
- *host_nix-server
Generated
+453 -90
View File
@@ -16,6 +16,21 @@
"type": "github"
}
},
"crane": {
"locked": {
"lastModified": 1770169865,
"narHash": "sha256-iPiy13xzDQ9GjpOez+NNIjh/qjl7i4RDf9dF2x5mF9I=",
"owner": "ipetkov",
"repo": "crane",
"rev": "8254ccf3b5b5131890ee073776f2e61c6d1e55d4",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"cursor": {
"inputs": {
"nixpkgs": [
@@ -23,11 +38,11 @@
]
},
"locked": {
"lastModified": 1776686554,
"narHash": "sha256-TaJnAbwSnfJ9My8Df8o6MKswuc3dq6qGF+zTZF906Eg=",
"lastModified": 1780452866,
"narHash": "sha256-Fq5cR/qGYcGwOOZf6sShtM+kljwecgntgcKXnIBrQDE=",
"ref": "refs/heads/main",
"rev": "c1a35069ee41595f8549fb529d29647565a30e75",
"revCount": 99,
"rev": "344f4065e69455adbcdfb4db1cd8d07a73ae7140",
"revCount": 114,
"type": "git",
"url": "https://git.chiasson.cloud/Olivier/cursor-nixos-flake"
},
@@ -36,6 +51,27 @@
"url": "https://git.chiasson.cloud/Olivier/cursor-nixos-flake"
}
},
"ddrm": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1777930144,
"narHash": "sha256-P7FfGTREtztsCTf4wSlqHf9EYlLt/AdrpmTfUezF6Uw=",
"ref": "refs/heads/main",
"rev": "c3d014f96855e45b53f7391ce19729493887cb96",
"revCount": 19,
"type": "git",
"url": "https://git.chiasson.cloud/Olivier/DDRM"
},
"original": {
"rev": "c3d014f96855e45b53f7391ce19729493887cb96",
"type": "git",
"url": "https://git.chiasson.cloud/Olivier/DDRM"
}
},
"dgop": {
"inputs": {
"nixpkgs": [
@@ -43,11 +79,11 @@
]
},
"locked": {
"lastModified": 1776181116,
"narHash": "sha256-aUNKF+jzGY+jRlR7Bp82v/zNHdI9bFELLuYYWbaM6fo=",
"lastModified": 1778679554,
"narHash": "sha256-zoPgnxIlDja91/4TmnCui+Fzc/xU/1jdJFu9bovtOk8=",
"owner": "AvengeMedia",
"repo": "dgop",
"rev": "e2078a7c5620be2e4897e7dabc08ade6dac9a454",
"rev": "06574b54fa4878a93d8605962d50b13e9528a4ca",
"type": "github"
},
"original": {
@@ -56,6 +92,29 @@
"type": "github"
}
},
"disko": {
"inputs": {
"nixpkgs": [
"navi",
"nixos-anywhere",
"nixpkgs"
]
},
"locked": {
"lastModified": 1769524058,
"narHash": "sha256-zygdD6X1PcVNR2PsyK4ptzrVEiAdbMqLos7utrMDEWE=",
"owner": "nix-community",
"repo": "disko",
"rev": "71a3fc97d80881e91710fe721f1158d3b96ae14d",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "master",
"repo": "disko",
"type": "github"
}
},
"dms": {
"inputs": {
"nixpkgs": [
@@ -64,11 +123,11 @@
"quickshell": "quickshell"
},
"locked": {
"lastModified": 1775588644,
"narHash": "sha256-iYBdSBvcW7bJtc84G6k5TFJEbPHQrif9KzZyE9Lbq8M=",
"lastModified": 1777431599,
"narHash": "sha256-g6r/Gx8PTDzO3jCNzzySA+Ff1lmLF9nDlMCNyyoQjoE=",
"owner": "AvengeMedia",
"repo": "DankMaterialShell",
"rev": "9798d78300d402178896f6ee1c370baed490158a",
"rev": "eb5afcdc40ea5446c27e18552ff4a19f9daf9484",
"type": "github"
},
"original": {
@@ -112,11 +171,11 @@
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1775087534,
"narHash": "sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=",
"lastModified": 1778716662,
"narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "3107b77cd68437b9a76194f0f7f9c55f2329ca5b",
"rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb",
"type": "github"
},
"original": {
@@ -130,11 +189,11 @@
"nixpkgs-lib": "nixpkgs-lib_2"
},
"locked": {
"lastModified": 1775087534,
"narHash": "sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=",
"lastModified": 1769996383,
"narHash": "sha256-AnYjnFWgS49RlqX7LrC4uA+sCCDBj0Ry/WOJ5XWAsa0=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "3107b77cd68437b9a76194f0f7f9c55f2329ca5b",
"rev": "57928607ea566b5db3ad13af0e57e921e6b12381",
"type": "github"
},
"original": {
@@ -144,6 +203,46 @@
}
},
"flake-parts_3": {
"inputs": {
"nixpkgs-lib": [
"navi",
"nixos-anywhere",
"nixpkgs"
]
},
"locked": {
"lastModified": 1768135262,
"narHash": "sha256-PVvu7OqHBGWN16zSi6tEmPwwHQ4rLPU9Plvs8/1TUBY=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "80daad04eddbbf5a4d883996a73f3f542fa437ac",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-parts_4": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib_3"
},
"locked": {
"lastModified": 1778716662,
"narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-parts_5": {
"inputs": {
"nixpkgs-lib": [
"nur",
@@ -166,7 +265,25 @@
},
"flake-utils": {
"inputs": {
"systems": "systems_2"
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_3"
},
"locked": {
"lastModified": 1731533236,
@@ -189,11 +306,11 @@
]
},
"locked": {
"lastModified": 1776721614,
"narHash": "sha256-zGuW7C4tsScib2560yE5VV6lY/MdRs30aU9cbg3RP+U=",
"lastModified": 1779507042,
"narHash": "sha256-7wOwi8B6D0BYsieZCnHZZj2sNUzgJhLoIVSfkwB7lxQ=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "c555a4a34a260493be5adb795c54e013c58f2d34",
"rev": "509ed3c603349a9d43de9e2ae6613baea6bd5b34",
"type": "github"
},
"original": {
@@ -210,11 +327,11 @@
]
},
"locked": {
"lastModified": 1776184304,
"narHash": "sha256-No6QGBmIv5ChiwKCcbkxjdEQ/RO2ZS1gD7SFy6EZ7rc=",
"lastModified": 1778805320,
"narHash": "sha256-nGFJ01m2CTBKD4ABtcY4vLhHrRN91LKr/pn41PcU78A=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "3c7524c68348ef79ce48308e0978611a050089b2",
"rev": "9846abe15e7d0d36b8acbd4d05f2b87461744c92",
"type": "github"
},
"original": {
@@ -225,11 +342,11 @@
},
"import-tree": {
"locked": {
"lastModified": 1773693634,
"narHash": "sha256-BtZ2dtkBdSUnFPPFc+n0kcMbgaTxzFNPv2iaO326Ffg=",
"lastModified": 1778781969,
"narHash": "sha256-Jjuz5CmSkur8KvLDoGa+vylEp+RkQtv4mt/qcMznpH0=",
"owner": "vic",
"repo": "import-tree",
"rev": "c41e7d58045f9057880b0d85e1152d6a4430dbf1",
"rev": "d321337efd0f23a9eb14a42adb7b2c29313ab274",
"type": "github"
},
"original": {
@@ -254,6 +371,50 @@
"type": "github"
}
},
"navi": {
"inputs": {
"crane": "crane",
"flake-parts": "flake-parts_2",
"nix-github-actions": "nix-github-actions",
"nixos-anywhere": "nixos-anywhere",
"nixpkgs": "nixpkgs",
"stable": "stable"
},
"locked": {
"lastModified": 1777283737,
"narHash": "sha256-sVwTOYwxzUUqAEr5kjOZooJ/v8H2hcuZ42f17MgCqX8=",
"owner": "cafkafk",
"repo": "navi",
"rev": "b060683e4ca2bd37a1c3bcb4e04abd2296190e9d",
"type": "github"
},
"original": {
"owner": "cafkafk",
"repo": "navi",
"type": "github"
}
},
"nix-github-actions": {
"inputs": {
"nixpkgs": [
"navi",
"nixpkgs"
]
},
"locked": {
"lastModified": 1729742964,
"narHash": "sha256-B4mzTcQ0FZHdpeWcpDYPERtyjJd/NIuaQ9+BV1h+MpA=",
"owner": "nix-community",
"repo": "nix-github-actions",
"rev": "e04df33f62cdcf93d73e9a04142464753a16db67",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nix-github-actions",
"type": "github"
}
},
"nix-monitor": {
"locked": {
"lastModified": 1771568669,
@@ -269,21 +430,44 @@
"type": "github"
}
},
"nix-vm-test": {
"inputs": {
"nixpkgs": [
"navi",
"nixos-anywhere",
"nixpkgs"
]
},
"locked": {
"lastModified": 1769079217,
"narHash": "sha256-R6qzhu+YJolxE2vUsPQWWwUKMbAG5nXX3pBtg8BNX38=",
"owner": "Enzime",
"repo": "nix-vm-test",
"rev": "58c15f78947b431d6c206e0966500c7e9139bd2f",
"type": "github"
},
"original": {
"owner": "Enzime",
"ref": "pr-105-latest",
"repo": "nix-vm-test",
"type": "github"
}
},
"nixcord": {
"inputs": {
"flake-compat": "flake-compat",
"flake-parts": "flake-parts_2",
"flake-parts": "flake-parts_4",
"nixpkgs": [
"nixpkgs"
],
"nixpkgs-nixcord": "nixpkgs-nixcord"
},
"locked": {
"lastModified": 1777125640,
"narHash": "sha256-jKmRu5PknoI0pk3WEqMhVReosUubUCq3M/izEQWzb+4=",
"lastModified": 1779498537,
"narHash": "sha256-6LQjFDS69JufrN4sVsMNsXxeSF6BbDzMSbN7sVApsaA=",
"owner": "KaylorBen",
"repo": "nixcord",
"rev": "0e738683dd7551a9cbfa343397b1592dfd785b7e",
"rev": "45a98c17b0d9e695bdee92ab00c76657eddf47e7",
"type": "github"
},
"original": {
@@ -292,7 +476,62 @@
"type": "github"
}
},
"nixos-anywhere": {
"inputs": {
"disko": "disko",
"flake-parts": "flake-parts_3",
"nix-vm-test": "nix-vm-test",
"nixos-images": "nixos-images",
"nixos-stable": "nixos-stable",
"nixpkgs": [
"navi",
"nixpkgs"
],
"treefmt-nix": "treefmt-nix"
},
"locked": {
"lastModified": 1772874867,
"narHash": "sha256-d79nCSys2aaV6E1tL/H4xEeKJvLFwEeP6yj5q0glbuQ=",
"owner": "cafkafk",
"repo": "nixos-anywhere",
"rev": "b88c2ee9734ce4ec5aa63cf74c2ea58f6f9970ec",
"type": "github"
},
"original": {
"owner": "cafkafk",
"ref": "kexec-ssh-args-bug",
"repo": "nixos-anywhere",
"type": "github"
}
},
"nixos-images": {
"inputs": {
"nixos-stable": [
"navi",
"nixos-anywhere",
"nixos-stable"
],
"nixos-unstable": [
"navi",
"nixos-anywhere",
"nixpkgs"
]
},
"locked": {
"lastModified": 1766770015,
"narHash": "sha256-kUmVBU+uBUPl/v3biPiWrk680b8N9rRMhtY97wsxiJc=",
"owner": "nix-community",
"repo": "nixos-images",
"rev": "e4dba54ddb6b2ad9c6550e5baaed2fa27938a5d2",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixos-images",
"type": "github"
}
},
"nixos-images_2": {
"inputs": {
"nixos-stable": [
"nixos-raspberrypi",
@@ -322,17 +561,17 @@
"inputs": {
"argononed": "argononed",
"flake-compat": "flake-compat_2",
"nixos-images": "nixos-images",
"nixos-images": "nixos-images_2",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1775857096,
"narHash": "sha256-+eSij7C0oMqz76rGnB99RuWptBuEkJBm9vgb5fIwRrg=",
"lastModified": 1779023229,
"narHash": "sha256-MInilg7B/06c34SwOuGSBho4l0H1EZcmvxTkSWCs5pE=",
"owner": "nvmd",
"repo": "nixos-raspberrypi",
"rev": "1dc4ca5f93587932383c0b61e1753f5eed1c3bba",
"rev": "06c6e3513e1ee64b651913193fc6ac38aa4963f5",
"type": "github"
},
"original": {
@@ -342,17 +581,33 @@
"type": "github"
}
},
"nixpkgs": {
"nixos-stable": {
"locked": {
"lastModified": 1776548001,
"narHash": "sha256-ZSK0NL4a1BwVbbTBoSnWgbJy9HeZFXLYQizjb2DPF24=",
"owner": "nixos",
"lastModified": 1769598131,
"narHash": "sha256-e7VO/kGLgRMbWtpBqdWl0uFg8Y2XWFMdz0uUJvlML8o=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b12141ef619e0a9c1c84dc8c684040326f27cdcc",
"rev": "fa83fd837f3098e3e678e6cf017b2b36102c7211",
"type": "github"
},
"original": {
"owner": "nixos",
"owner": "NixOS",
"ref": "nixos-25.11",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1750134718,
"narHash": "sha256-v263g4GbxXv87hMXMCpjkIxd/viIF7p3JpJrwgKdNiI=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9e83b64f727c88a7711a2c463a7b16eedb69a84c",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
@@ -360,11 +615,11 @@
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1774748309,
"narHash": "sha256-+U7gF3qxzwD5TZuANzZPeJTZRHS29OFQgkQ2kiTJBIQ=",
"lastModified": 1777168982,
"narHash": "sha256-GOkGPcboWE9BmGCRMLX3worL4EMnsnG8MyKmXNeYuhQ=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "333c4e0545a6da976206c74db8773a1645b5870a",
"rev": "f5901329dade4a6ea039af1433fb087bd9c1fe14",
"type": "github"
},
"original": {
@@ -375,11 +630,26 @@
},
"nixpkgs-lib_2": {
"locked": {
"lastModified": 1774748309,
"narHash": "sha256-+U7gF3qxzwD5TZuANzZPeJTZRHS29OFQgkQ2kiTJBIQ=",
"lastModified": 1769909678,
"narHash": "sha256-cBEymOf4/o3FD5AZnzC3J9hLbiZ+QDT/KDuyHXVJOpM=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "333c4e0545a6da976206c74db8773a1645b5870a",
"rev": "72716169fe93074c333e8d0173151350670b824c",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixpkgs.lib",
"type": "github"
}
},
"nixpkgs-lib_3": {
"locked": {
"lastModified": 1777168982,
"narHash": "sha256-GOkGPcboWE9BmGCRMLX3worL4EMnsnG8MyKmXNeYuhQ=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "f5901329dade4a6ea039af1433fb087bd9c1fe14",
"type": "github"
},
"original": {
@@ -390,11 +660,11 @@
},
"nixpkgs-nixcord": {
"locked": {
"lastModified": 1776734388,
"narHash": "sha256-vl3dkhlE5gzsItuHoEMVe+DlonsK+0836LIRDnm6MXQ=",
"lastModified": 1779102034,
"narHash": "sha256-vZJZjLo513IeI8hjzHFc6TDezUd4uCE2Eq4SNO3DNNg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "10e7ad5bbcb421fe07e3a4ad53a634b0cd57ffac",
"rev": "687f05a9184cad4eaf905c48b63649e3a86f5433",
"type": "github"
},
"original": {
@@ -406,11 +676,27 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1775579569,
"narHash": "sha256-/m3yyS/EnXqoPGBJYVy4jTOsirdgsEZ3JdN2gGkBr14=",
"lastModified": 1779357205,
"narHash": "sha256-cCO8aTqss5x9Ky8GWkpY0Hy5fyTZEbtifSUV8QjSzic=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "f83fc3c307e74bc5fd5adb7eb6b8b13ffd2a36e1",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1778869304,
"narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "dfd9566f82a6e1d55c30f861879186440614696e",
"rev": "d233902339c02a9c334e7e593de68855ad26c4cb",
"type": "github"
},
"original": {
@@ -422,17 +708,17 @@
},
"nur": {
"inputs": {
"flake-parts": "flake-parts_3",
"flake-parts": "flake-parts_5",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1776720544,
"narHash": "sha256-SjaFRV8Oqu3LtEGxr1q5K+bMPbxPPjc7z1adadC8yE8=",
"lastModified": 1779493406,
"narHash": "sha256-70dCjL6KdsNG+hPHqUsrTF/gQtnucRMo2B/oGvf8aOw=",
"owner": "nix-community",
"repo": "NUR",
"rev": "fe8c1a700dbbfb474f7e80f6ca6223d0bd61d79d",
"rev": "e27d8a76f2167da18bd37ab38f463c13daf2bc21",
"type": "github"
},
"original": {
@@ -451,11 +737,11 @@
]
},
"locked": {
"lastModified": 1773135655,
"narHash": "sha256-eb4/TZEU1cMpUPtUuxcr2sfiCciHtesBtPHzS1zh2Uo=",
"lastModified": 1778402771,
"narHash": "sha256-WS8hQ8Yk4M1rfkp2aUCaUkGVBU0ppCYAhklBk5kBdFU=",
"owner": "robertjakub",
"repo": "oom-hardware",
"rev": "ad592fd988ee7a7c1bd68ff8b819973e1ae900ef",
"rev": "9f338e9250b7c01ac97750851867fc8158e8f54c",
"type": "github"
},
"original": {
@@ -465,6 +751,27 @@
"type": "github"
}
},
"personal-website": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1779906441,
"narHash": "sha256-py8KJJMi4awjZHi5FWBPYfbRPvk3Rg9SeFkPJydsG2E=",
"ref": "refs/heads/main",
"rev": "339a9ba1ef79dc9976af77f1fea0302de8a696d0",
"revCount": 16,
"type": "git",
"url": "https://git.chiasson.cloud/Olivier/Personal-Website"
},
"original": {
"type": "git",
"url": "https://git.chiasson.cloud/Olivier/Personal-Website"
}
},
"quickshell": {
"inputs": {
"nixpkgs": [
@@ -473,16 +780,16 @@
]
},
"locked": {
"lastModified": 1766725085,
"narHash": "sha256-O2aMFdDUYJazFrlwL7aSIHbUSEm3ADVZjmf41uBJfHs=",
"lastModified": 1776854048,
"narHash": "sha256-lLbV66V3RMNp1l8/UelmR4YzoJ5ONtgvEtiUMJATH/o=",
"ref": "refs/heads/master",
"rev": "41828c4180fb921df7992a5405f5ff05d2ac2fff",
"revCount": 715,
"rev": "783c953987dc56ff0601abe6845ed96f1d00495a",
"revCount": 806,
"type": "git",
"url": "https://git.outfoxxed.me/quickshell/quickshell"
},
"original": {
"rev": "41828c4180fb921df7992a5405f5ff05d2ac2fff",
"rev": "783c953987dc56ff0601abe6845ed96f1d00495a",
"type": "git",
"url": "https://git.outfoxxed.me/quickshell/quickshell"
}
@@ -490,18 +797,21 @@
"root": {
"inputs": {
"cursor": "cursor",
"ddrm": "ddrm",
"dgop": "dgop",
"dms": "dms",
"flake-parts": "flake-parts",
"home-manager": "home-manager",
"import-tree": "import-tree",
"mobile-nixos": "mobile-nixos",
"navi": "navi",
"nix-monitor": "nix-monitor",
"nixcord": "nixcord",
"nixos-raspberrypi": "nixos-raspberrypi",
"nixpkgs": "nixpkgs",
"nixpkgs": "nixpkgs_2",
"nur": "nur",
"oom-hardware": "oom-hardware",
"personal-website": "personal-website",
"sops-nix": "sops-nix",
"spicetify-nix": "spicetify-nix",
"swiftshare": "swiftshare",
@@ -519,11 +829,11 @@
]
},
"locked": {
"lastModified": 1776119890,
"narHash": "sha256-Zm6bxLNnEOYuS/SzrAGsYuXSwk3cbkRQZY0fJnk8a5M=",
"lastModified": 1777944972,
"narHash": "sha256-VfGRo1qTBKOe3s2gOv8LSoA6Fk19PvBlwQ1ECN0Evn8=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "d4971dd58c6627bfee52a1ad4237637c0a2fb0cd",
"rev": "c591bf665727040c6cc5cb409079acb22dcce33c",
"type": "github"
},
"original": {
@@ -537,14 +847,14 @@
"nixpkgs": [
"nixpkgs"
],
"systems": "systems"
"systems": "systems_2"
},
"locked": {
"lastModified": 1776578704,
"narHash": "sha256-4+JHYCweZ/SSrMcu2nJ5gc7gop2scBk0JIIfaUKuTaQ=",
"lastModified": 1779000518,
"narHash": "sha256-wdtytSnzMe85J/qeXJALMzSLRFTZ1gBHwn81l1PtT8k=",
"owner": "Gerg-L",
"repo": "spicetify-nix",
"rev": "73f6d24b4f5bdacc3b41ddcf9965bef2781f97dd",
"rev": "5dde76b38418892ccb3d99e99bed7f8a43ac294c",
"type": "github"
},
"original": {
@@ -553,19 +863,35 @@
"type": "github"
}
},
"stable": {
"locked": {
"lastModified": 1750133334,
"narHash": "sha256-urV51uWH7fVnhIvsZIELIYalMYsyr2FCalvlRTzqWRw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "36ab78dab7da2e4e27911007033713bab534187b",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-25.05",
"repo": "nixpkgs",
"type": "github"
}
},
"swiftshare": {
"inputs": {
"flake-utils": "flake-utils",
"flake-utils": "flake-utils_2",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1777652580,
"narHash": "sha256-CO4RXrd0eQ2INc8/S2CTWCIHUdvVkqwKZ/9o7a/pcFg=",
"lastModified": 1779059662,
"narHash": "sha256-PkBItyS1oZ4MJ+eEgF5iLKxx28rmSyk/bHp63tjW/5g=",
"ref": "refs/heads/main",
"rev": "42575990389388d8d07da6fb4110d77ea7493159",
"revCount": 75,
"rev": "030da2f52d3cbe3c577ce12b5abbd35e90e1f093",
"revCount": 79,
"type": "git",
"url": "https://git.chiasson.cloud/Olivier/SwiftShare"
},
@@ -604,6 +930,21 @@
"type": "github"
}
},
"systems_3": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"t2fanrd": {
"inputs": {
"nixpkgs": [
@@ -611,11 +952,11 @@
]
},
"locked": {
"lastModified": 1775302822,
"narHash": "sha256-QoK8SYoIc0d/PoRdIUo+fkDNAHZIP2+AJ6PDM9ehGiY=",
"lastModified": 1777412856,
"narHash": "sha256-WrcIo3y9uFCuzgzbxc465FBS3zAZMQlfYszefkOUCWc=",
"owner": "GnomedDev",
"repo": "T2FanRD",
"rev": "5b1c0c10785b8e8dfe124a4d6aaa7c2becdac65c",
"rev": "48baf962697ec3d4d969c74cf601ee8e15b7aeaa",
"type": "github"
},
"original": {
@@ -627,11 +968,11 @@
"t2linux-patches": {
"flake": false,
"locked": {
"lastModified": 1776111571,
"narHash": "sha256-1neTptNNPtwbBYSQOE48GM8CYx780eI5JQTFYmwN0og=",
"lastModified": 1779914035,
"narHash": "sha256-VsHuI2CbQ8gFplW+51gUJvCqo1Ts10Ueks9aTtkAOiw=",
"owner": "t2linux",
"repo": "linux-t2-patches",
"rev": "76589a89790c33c137d173f2d98b6096cd16b132",
"rev": "7ee7d19c38e5df31a386b2a0c35ca8f064003960",
"type": "github"
},
"original": {
@@ -640,6 +981,28 @@
"type": "github"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
"navi",
"nixos-anywhere",
"nixpkgs"
]
},
"locked": {
"lastModified": 1769691507,
"narHash": "sha256-8aAYwyVzSSwIhP2glDhw/G0i5+wOrren3v6WmxkVonM=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "28b19c5844cc6e2257801d43f2772a4b4c050a1b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
},
"wallpapers": {
"flake": false,
"locked": {
@@ -658,14 +1021,14 @@
},
"wrapper-modules": {
"inputs": {
"nixpkgs": "nixpkgs_2"
"nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1776464146,
"narHash": "sha256-XwLFfJDz71vIF7BAhnbLhrzQjmDC2uXdo7N0oHUrYzA=",
"lastModified": 1779297405,
"narHash": "sha256-VFoBwH7ZjVxCnvZTb5ODRXt70sLtWMxstive0N+RS50=",
"owner": "BirdeeHub",
"repo": "nix-wrapper-modules",
"rev": "75febede14a0845f4ef429da692a0698bf433600",
"rev": "e7ed7a1205945befdf2e0d73ba7df91d935e5af1",
"type": "github"
},
"original": {
@@ -682,11 +1045,11 @@
]
},
"locked": {
"lastModified": 1776663782,
"narHash": "sha256-qzBBuxZbn7vPD9ZDl3xmCBGa6qEc8Q//76Cbx4W0tE4=",
"lastModified": 1779455631,
"narHash": "sha256-svU6Ro4xiMxMA1KJGwQ/nfKwz3yXE/SONCw2Z1qTXHA=",
"owner": "0xc000022070",
"repo": "zen-browser-flake",
"rev": "b93be06dc91630bf0ced69c54d0e1e05e56ae460",
"rev": "5bcdfcef664bf62831dcb4b947004d9c5fbf7201",
"type": "github"
},
"original": {
+14
View File
@@ -91,6 +91,20 @@
url = "git+https://git.chiasson.cloud/Olivier/SwiftShare";
inputs.nixpkgs.follows = "nixpkgs";
};
# After pushing Personal-Website, `nix flake update personal-website` refreshes the lock pin.
personal-website = {
url = "git+https://git.chiasson.cloud/Olivier/Personal-Website";
inputs.nixpkgs.follows = "nixpkgs";
};
# DDRM browser-extension backend
ddrm = {
url = "git+https://git.chiasson.cloud/Olivier/DDRM?rev=c3d014f96855e45b53f7391ce19729493887cb96";
inputs.nixpkgs.follows = "nixpkgs";
};
navi.url = "github:cafkafk/navi";
};
outputs = inputs:
+149
View File
@@ -0,0 +1,149 @@
{
inputs,
self,
lib,
...
}:
let
ssh = self.lib.sshInventory;
# Remote deploy identity (see `nixosModules.systemDeployBuilder`).
defaultTargetUser = _: "builder";
hostSpecs = {
"14900k" = {
configuration = self.nixosModules."14900kConfiguration";
system = "x86_64-linux";
specialArgs = {
inherit self inputs;
host = "14900k";
system = "x86_64-linux";
};
};
ideapad = {
configuration = self.nixosModules.ideapadConfiguration;
system = "aarch64-linux";
specialArgs = {
inherit self inputs;
host = "ideapad";
system = "aarch64-linux";
};
};
t2mbp = {
configuration = self.nixosModules.t2mbpConfiguration;
system = "x86_64-linux";
specialArgs = {
inherit self inputs;
host = "t2mbp";
system = "x86_64-linux";
};
};
uConsole = {
modules = [
inputs.nixos-raspberrypi.nixosModules.raspberry-pi-5.base
inputs.oom-hardware.nixosModules.uc.kernel
inputs.oom-hardware.nixosModules.uc.configtxt
inputs.oom-hardware.nixosModules.uc.base-cm5
self.nixosModules.uConsoleConfiguration
];
system = "aarch64-linux";
specialArgs = inputs // {
inherit self;
inputs = inputs;
host = "uConsole";
system = "aarch64-linux";
};
};
nix-server = {
configuration = self.nixosModules.nix-serverConfiguration;
system = "x86_64-linux";
specialArgs = {
inherit self inputs;
host = "nix-server";
system = "x86_64-linux";
};
};
r5500 = {
configuration = self.nixosModules.r5500Configuration;
system = "x86_64-linux";
specialArgs = {
inherit self inputs;
host = "r5500";
system = "x86_64-linux";
};
};
};
deployments = lib.mapAttrs (
name: entry:
{
targetHost = entry.hostName;
targetUser = defaultTargetUser name;
tags =
[ name ]
++ lib.optionals (name == "nix-server") [ "server" ]
++ lib.optionals (lib.elem name [
"ideapad"
"uConsole"
]) [ "aarch64" ];
}
// lib.optionalAttrs (name == "14900k") {
allowLocalDeployment = true;
}
// lib.optionalAttrs (name == "nix-server") {
targetPort = 22;
}
) ssh.activeHosts;
metaNixpkgs = import inputs.nixpkgs {
system = "x86_64-linux";
};
in
{
flake.navi = self.lib.mkNaviHiveConfig {
inherit metaNixpkgs hostSpecs deployments;
};
flake.naviHive = inputs.navi.lib.makeHive self.outputs.navi;
perSystem =
{
pkgs,
system,
...
}:
lib.optionalAttrs (lib.elem system [
"x86_64-linux"
"aarch64-linux"
]) {
devShells.default = pkgs.mkShell {
packages = [ inputs.navi.packages.${system}.default ];
shellHook = ''
echo "Navi fleet deploy (from repo root):"
echo " navi apply --on <host> # build + switch one host"
echo " navi apply-local --node 14900k --sudo # switch this machine locally (needs root), --node if hostname differs"
echo " navi tui # interactive fleet dashboard"
'';
};
apps = {
navi = {
type = "app";
program = lib.getExe inputs.navi.packages.${system}.default;
};
navi-tui = {
type = "app";
program =
toString (
pkgs.writeShellApplication {
name = "navi-tui";
runtimeInputs = [ inputs.navi.packages.${system}.default ];
text = ''
exec navi tui "$@"
'';
}
);
};
};
};
}
+14
View File
@@ -0,0 +1,14 @@
{ self, inputs, ... }: {
flake.nixosModules.desktop = {
imports = [
self.nixosModules.desktopShellDmsOptions
self.nixosModules.desktopHyprland
self.nixosModules.desktopNiri
self.nixosModules.desktopPlasma
self.nixosModules.desktopOptions
self.nixosModules.desktopGui
self.nixosModules.desktopWallpapers
self.nixosModules.desktopWaydroid
];
};
}
+142
View File
@@ -0,0 +1,142 @@
{ ... }: {
flake.nixosModules.desktopGui =
{ config, lib, pkgs, self, inputs, options, ... }:
let
pi5Greeter = self.lib.pi5NiriKdl;
cfg = config.chiasson.desktop;
guiEnabled = cfg.hyprland.enable || cfg.niri.enable || cfg.plasma.enable;
hmAvailable = lib.hasAttrByPath [ "home-manager" "sharedModules" ] options;
dm = cfg.displayManager;
variant = dm.variant or "sddm";
useGreeter = variant == "dankgreeter";
sddmTheme =
if dm.sddm.theme.package == null then
"breeze"
else
"${dm.sddm.theme.package}/share/sddm/themes/${dm.sddm.theme.id}";
effectiveDefaultSession =
if cfg.defaultSession != null then
cfg.defaultSession
else if cfg.hyprland.enable then
"hyprland"
else if cfg.niri.enable then
"niri"
else
"plasma";
greeterCompositor =
if effectiveDefaultSession == "niri" then
"niri"
else if effectiveDefaultSession == "hyprland" then
"hyprland"
else
"niri";
enabledUsers = config.chiasson.users.enabled or [ ];
greeterConfigHome =
if dm.greeter.configHome != null then
dm.greeter.configHome
else if enabledUsers != [ ] then
"/home/${builtins.head enabledUsers}"
else
null;
in
{
# Unconditional imports only — conditional imports that peek at `config` recurse during merge.
imports = [
inputs.dms.nixosModules.greeter
];
config = lib.mkMerge [
(lib.mkIf guiEnabled {
services.xserver.enable = true;
# Chromium/Electron (Edge, Vesktop, etc.) only add native Wayland + IME flags when this is
# set; nixpkgs wrappers gate on NIXOS_OZONE_WL and WAYLAND_DISPLAY. Helps PipeWire/desktop
# portal screen capture on wlroots-like sessions. Harmless on X11 (flags stay off).
environment.sessionVariables.NIXOS_OZONE_WL = lib.mkDefault "1";
xdg.portal.enable = true;
xdg.portal.extraPortals =
[ pkgs.xdg-desktop-portal-gtk ]
++ lib.optionals cfg.plasma.enable [ pkgs.kdePackages.xdg-desktop-portal-kde ];
# Backlight control: `brightnessctl` + its udev rule (auto-loaded from the package's
# `lib/udev/rules.d`). The rule grants the `video` group write access to
# `/sys/class/backlight/*/brightness`; the user catalog adds desktop users to `video`.
# The Hyprland defaults already bind XF86MonBrightness* to brightnessctl, and the Niri
# base config does the same — this guarantees the binary is actually present.
environment.systemPackages = [ pkgs.brightnessctl ];
services.udev.packages = [ pkgs.brightnessctl ];
})
(lib.mkIf (guiEnabled && !useGreeter) {
services.displayManager.sddm = {
enable = true;
wayland.enable = dm.sddm.wayland.enable;
theme = sddmTheme;
inherit (dm.sddm) enableHidpi settings;
extraPackages = lib.optionals (dm.sddm.theme.package != null) (
with pkgs.kdePackages; [
qtdeclarative
qtsvg
]
);
};
services.displayManager.defaultSession = effectiveDefaultSession;
})
(lib.mkIf (guiEnabled && useGreeter) {
assertions = [
{
assertion = greeterConfigHome != null;
message = "DankGreeter needs chiasson.desktop.displayManager.greeter.configHome or a non-empty chiasson.users.enabled list.";
}
];
services.displayManager.defaultSession = effectiveDefaultSession;
programs.dank-material-shell.greeter = {
enable = true;
compositor = {
name = greeterCompositor;
}
// lib.optionalAttrs (cfg.niri.raspberryPi5DrmWorkaround && greeterCompositor == "niri") {
customConfig = lib.mkDefault pi5Greeter.dankGreeterCompositorConfig;
};
configHome = greeterConfigHome;
};
})
(lib.mkIf (cfg.defaultPackages.enabled && guiEnabled) {
environment.systemPackages = cfg.defaultPackages.packages;
})
(lib.mkIf (cfg.extraPackages != [ ] && guiEnabled) {
environment.systemPackages = cfg.extraPackages;
})
(lib.mkIf (guiEnabled && cfg.keyring.enable) {
services.gnome.gnome-keyring.enable = true;
security.pam.services.login.enableGnomeKeyring = true;
services.xserver.updateDbusEnvironment = lib.mkDefault true;
# Electron apps (Element, Slack, etc.) use libsecret via keytar; without it they report
# "unsupported keyring" even if gnome-keyring is enabled.
environment.systemPackages = [ pkgs.libsecret ];
})
(lib.mkIf (guiEnabled && cfg.keyring.enable && !useGreeter) {
security.pam.services.sddm.enableGnomeKeyring = true;
})
(lib.mkIf (guiEnabled && cfg.keyring.enable && useGreeter) {
security.pam.services.greetd.enableGnomeKeyring = true;
})
(lib.mkIf (guiEnabled && cfg.keyring.enable && hmAvailable) {
"home-manager".sharedModules = [
({ lib, pkgs, ... }: {
services.gnome-keyring = {
enable = lib.mkDefault true;
components = [ "secrets" ];
};
home.packages = [
pkgs.gcr
pkgs.libsecret
];
})
];
})
];
};
}
+219
View File
@@ -0,0 +1,219 @@
{ self, ... }: {
flake.nixosModules.desktopHyprland =
{ config, options, lib, pkgs, ... }:
let
cfg = config.chiasson.desktop;
hmAvailable = lib.hasAttrByPath [ "home-manager" "sharedModules" ] options;
in
{
options.chiasson.desktop.hyprland = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Hyprland session + HM wiring.";
};
settings = lib.mkOption {
type = lib.types.attrs;
default = { };
description = "Extra `wayland.windowManager.hyprland.settings` merged with defaults.";
};
};
config = lib.mkMerge [
(lib.mkIf cfg.hyprland.enable {
programs.hyprland.enable = true;
})
(lib.mkIf (cfg.hyprland.enable && hmAvailable) {
"home-manager".sharedModules = [ self.homeManagerModules.desktopHyprland ];
})
];
};
flake.homeManagerModules.desktopHyprland = {
config,
lib,
osConfig ? { },
pkgs,
...
}:
let
hyprlandEnabled = osConfig.chiasson.desktop.hyprland.enable or false;
keyringEnabled = osConfig.chiasson.desktop.keyring.enable or false;
# nixpkgs hyprland-plugins pin is stale for current Hyprland — override to a known-good rev.
hyprbarsPatched =
let
hyprlandPluginsSrc = pkgs.fetchFromGitHub {
owner = "hyprwm";
repo = "hyprland-plugins";
rev = "b85a56b9531013c79f2f3846fd6ee2ff014b8960";
hash = "sha256-xwNa+1D8WPsDnJtUofDrtyDCZKZotbUymzV/R5s+M0I=";
};
in
pkgs.hyprlandPlugins.hyprbars.overrideAttrs (_: {
version = "unstable-2026-02-23";
src = "${hyprlandPluginsSrc}/hyprbars";
});
in
{
config = lib.mkIf hyprlandEnabled {
wayland.windowManager.hyprland = {
enable = true;
# null = use NixOS Hyprland/xdg-desktop-portal-hyprland (same versions as the session).
package = null;
portalPackage = null;
plugins = [ hyprbarsPatched ];
extraConfig = ''
source = ~/.config/hypr/colors.conf
'';
settings = lib.mkMerge [
(lib.mkIf keyringEnabled {
exec-once = lib.mkBefore [
"dbus-update-activation-environment --systemd --all"
];
})
{
monitor = [ ",preferred,auto,auto" ];
general = {
gaps_in = 8;
gaps_out = 4;
border_size = 2;
allow_tearing = false;
};
cursor.no_hardware_cursors = true;
env = [
"XCURSOR_THEME,phinger-cursors-dark"
"XCURSOR_SIZE,32"
];
input = {
kb_layout = "ca";
kb_variant = "";
numlock_by_default = true;
};
binds.scroll_event_delay = 50;
exec-once = [
"nm-applet --indicator &"
"sleep 1 && hyprctl reload"
];
# Default keybinds
bind =
[
"SUPER,T,exec,kitty -e fish"
"ControlSuper,T,exec,konsole"
"SUPER,D,exec,rofi -show drun"
"ControlSuper,D,exec,rofi -show window"
"SUPER,E,exec,dolphin"
"Super, Q, killactive"
"ControlSuper, Q, exec, hyprctl kill"
"Super, F, fullscreen, 0"
"Super, G, fullscreen, 1"
"ShiftSuper, F, fullscreenstate, 0 3"
"Super, Minus, splitratio, -0.1"
"Super, Equal, splitratio, 0.1"
"AltSuper, Space, togglefloating"
"Super, P, pin"
"Super, Space, exec, dms ipc call spotlight toggle"
"Super, I, exec, dms ipc call settings focusOrToggle"
"Super, N, exec, dms ipc call notepad toggle"
"ShiftSuper, N, exec, dms ipc call notifications toggle"
"Super, M, exec, dms ipc call processlist focusOrToggle"
"Super, L, exec, dms ipc call lock lock"
"ShiftSuper, V, exec, dms ipc call clipboard toggle"
"Super, Tab, cyclenext"
"Super, Tab, bringactivetotop"
"Super, left, movefocus, l"
"Super, right, movefocus, r"
"Super, up, movefocus, u"
"Super, down, movefocus, d"
"ShiftSuper, left, movewindow, l"
"ShiftSuper, right, movewindow, r"
"ShiftSuper, up, movewindow, u"
"ShiftSuper, down, movewindow, d"
]
++ (builtins.map (i: "Super, ${toString i}, workspace, ${toString i}") (builtins.genList (n: n + 1) 9))
++ (builtins.map (i: "ControlSuper, ${toString i}, focusworkspaceoncurrentmonitor, ${toString i}") (builtins.genList (n: n + 1) 9))
++ (builtins.map (i: "ShiftSuper, ${toString i}, movetoworkspacesilent, ${toString i}") (builtins.genList (n: n + 1) 9));
bindm = [
" , mouse:282, movewindow"
"Super, mouse:272, movewindow"
"Super, mouse:273, resizewindow"
];
bindl = [
"Super, mouse_up, splitratio, -0.1"
"Super, mouse_down, splitratio, 0.1"
" , XF86AudioPlay, exec, playerctl play-pause"
" , XF86AudioPrev, exec, playerctl previous"
" , XF86AudioNext, exec, playerctl next"
" , XF86AudioMute, exec, wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"
" , XF86MonBrightnessDown, exec, brightnessctl s 10%-"
" , XF86MonBrightnessUp, exec, brightnessctl s +10%"
];
bindel = [
" , XF86AudioRaiseVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+"
" , XF86AudioLowerVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"
];
# Decoration / blur
decoration = {
rounding = 10;
active_opacity = 0.95;
inactive_opacity = 0.85;
shadow = {
enabled = true;
range = 5;
render_power = 8;
};
blur = {
enabled = true;
new_optimizations = true;
xray = false;
size = 2;
passes = 4;
vibrancy = 10;
};
};
misc = {
disable_hyprland_logo = true;
disable_splash_rendering = true;
};
plugin.hyprbars = {
enabled = true;
bar_height = 30;
bar_blur = true;
bar_padding = 10;
bar_button_padding = 7;
bar_precedence_over_border = true;
bar_part_of_window = true;
bar_title_enabled = true;
bar_text_size = 10;
bar_text_font = "Sans";
bar_text_align = "center";
bar_buttons_alignment = "left";
icon_on_hover = true;
"hyprbars-button" = [
"rgb(ed6a5f), 12, 󰖭, hyprctl dispatch killactive, rgb(460804)"
"rgb(f6be50), 12, 󰖰, hyprctl dispatch movetoworkspacesilent special:minimized, rgb(90591d)"
"rgb(61c555), 12, 󰘖, hyprctl dispatch fullscreen 1, rgb(2a6218)"
];
};
windowrule = [
"sync_fullscreen 0, match:class ^(?i)microsoft-edge|Spotify|org.kde.gwenview|zen-beta$"
"opacity 1.0 override 0.95 override, match:class ^(?i)microsoft-edge$"
"opacity 1.0 override 1.00 override, match:class ^(?i)com.stremio.stremio$"
"opacity 1.0 override 0.85 override, match:class ^(?i)zen-beta$"
"no_screen_share on, match:class ^(?i)(microsoft-edge|zen-beta)$, match:title ^(?i).*(scotiabank|paypal).*"
"no_screen_share on, match:class ^(?i)microsoft-edge$, match:initial_title ^(?i).*(?i)personal 2.*edge.*$"
"hyprbars:no_bar on, match:class ^(?i)(microsoft-edge|Cursor|Flow|looking-glass-client|localsend_app)$"
];
}
(osConfig.chiasson.desktop.hyprland.settings or { })
];
};
};
};
}
+223
View File
@@ -0,0 +1,223 @@
{ self, inputs, ... }:
let
# Keep defaults in this let — a bare niri-settings.nix next to this file would get picked up by import-tree.
niriBaseSettings =
pkgs:
{
input.keyboard = {
xkb.layout = "ca";
xkb.variant = "";
};
input."focus-follows-mouse" = _: {
props."max-scroll-amount" = "45%";
content = { };
};
input."warp-mouse-to-focus" = _: { };
layout.gaps = 5;
window-rules = [
{
matches = [
{
app-id = "^$";
title = "^$";
}
];
open-floating = true;
open-focused = false;
}
];
#TODO[epic=Binds] Go over binds again
binds = {
"Mod+T"."spawn-sh" = "${pkgs.lib.getExe pkgs.kitty} -e fish"; #TODO[epic=Binds] This should only be set if having kitty
"Mod+Control+T"."spawn-sh" = "konsole"; #TODO[epic=Binds] This should only be set if having konsole
"Mod+D"."spawn-sh" = "rofi -show drun"; #TODO[epic=Binds] This should only be set if having rofi
"Mod+Space"."spawn-sh" = "dms ipc call spotlight toggle"; #TODO[epic=Binds] This should only be set if having dms
"Mod+E"."spawn-sh" = "dolphin"; #TODO[epic=Binds] This should only be set if having dolphin
"Mod+Control+O"."spawn-sh" = "rofi -show window"; #TODO[epic=Binds] This should only be set if having rofi
"Mod+V"."spawn-sh" = "dms ipc call clipboard toggle"; #TODO[epic=Binds] This should only be set if having dms
"Mod+N"."spawn-sh" = "dms ipc call notepad toggle"; #TODO[epic=Binds] This should only be set if having dms
"Mod+Shift+N"."spawn-sh" = "dms ipc call notifications toggle"; #TODO[epic=Binds] This should only be set if having dms
"Mod+M"."spawn-sh" = "dms ipc call processlist toggle"; #TODO[epic=Binds] This should only be set if having dms
"Mod+L"."spawn-sh" = "dms ipc call lock lock"; #TODO[epic=Binds] This should only be set if having dms
"Mod+Q"."close-window" = _: { };
"Mod+F"."maximize-column" = _: { };
"Mod+Shift+F"."fullscreen-window" = _: { };
"Mod+O"."toggle-overview" = _: { };
"Mod+Shift+NumberSign"."show-hotkey-overlay" = _: { };
"Mod+Shift+E".quit = _: { };
"Mod+Left"."focus-column-or-monitor-left" = _: { };
"Mod+Down"."focus-window-or-monitor-down" = _: { };
"Mod+Up"."focus-window-or-monitor-up" = _: { };
"Mod+Right"."focus-column-or-monitor-right" = _: { };
"Mod+Shift+WheelScrollDown"."focus-workspace-down" = _: { };
"Mod+Shift+WheelScrollUp"."focus-workspace-up" = _: { };
"Mod+WheelScrollDown"."focus-column-or-monitor-right" = _: { };
"Mod+WheelScrollUp"."focus-column-or-monitor-left" = _: { };
"Mod+Shift+Left"."move-column-left-or-to-monitor-left" = _: { };
"Mod+Shift+Down"."move-window-down" = _: { };
"Mod+Shift+Up"."move-window-up" = _: { };
"Mod+Shift+Right"."move-column-right-or-to-monitor-right" = _: { };
"Mod+Page_Up"."focus-workspace-up" = _: { };
"Mod+Page_Down"."focus-workspace-down" = _: { };
"Mod+Shift+Page_Up"."move-column-to-workspace-up" = _: { };
"Mod+Shift+Page_Down"."move-column-to-workspace-down" = _: { };
"Mod+R"."switch-preset-column-width" = _: { };
"Mod+BracketLeft"."consume-or-expel-window-left" = _: { };
"Mod+BracketRight"."consume-or-expel-window-right" = _: { };
"Mod+Comma"."consume-window-into-column" = _: { };
"Mod+Period"."expel-window-from-column" = _: { };
"Mod+Alt+Space"."toggle-window-floating" = _: { };
#"Mod+Shift+V"."switch-focus-between-floating-and-tiling" = _: { }; #TODO[epic=Binds] Find free bind that is not in the way and not overcomplicated to remember
"Mod+1"."focus-workspace" = 1;
"Mod+2"."focus-workspace" = 2;
"Mod+3"."focus-workspace" = 3;
"Mod+4"."focus-workspace" = 4;
"Mod+5"."focus-workspace" = 5;
"Mod+6"."focus-workspace" = 6;
"Mod+7"."focus-workspace" = 7;
"Mod+8"."focus-workspace" = 8;
"Mod+9"."focus-workspace" = 9;
"Mod+Ctrl+1"."move-column-to-workspace" = 1;
"Mod+Ctrl+2"."move-column-to-workspace" = 2;
"Mod+Ctrl+3"."move-column-to-workspace" = 3;
"Mod+Ctrl+4"."move-column-to-workspace" = 4;
"Mod+Ctrl+5"."move-column-to-workspace" = 5;
"Mod+Ctrl+6"."move-column-to-workspace" = 6;
"Mod+Ctrl+7"."move-column-to-workspace" = 7;
"Mod+Ctrl+8"."move-column-to-workspace" = 8;
"Mod+Ctrl+9"."move-column-to-workspace" = 9;
"XF86AudioRaiseVolume".spawn = [
"wpctl"
"set-volume"
"@DEFAULT_AUDIO_SINK@"
"0.05+"
];
"XF86AudioLowerVolume".spawn = [
"wpctl"
"set-volume"
"@DEFAULT_AUDIO_SINK@"
"0.05-"
];
"XF86AudioMute".spawn = [
"wpctl"
"set-mute"
"@DEFAULT_AUDIO_SINK@"
"toggle"
];
# Backlight: relies on `pkgs.brightnessctl` being on PATH (provided by `desktopGui` when
# any GUI session is enabled) and the user being in the `video` group (catalog default).
"XF86MonBrightnessUp".spawn = [ "brightnessctl" "set" "+5%" ];
"XF86MonBrightnessDown".spawn = [ "brightnessctl" "set" "5%-" ];
Print.screenshot = _: { };
"Ctrl+Print"."screenshot-screen" = _: { };
"Alt+Print"."screenshot-window" = _: { };
};
};
keyringNiriStartupKdl = ''
spawn-at-startup "dbus-update-activation-environment" "--systemd" "--all"
'';
mergeNiriSettings =
pkgs: niriCfg: keyringEnable:
let
lib = pkgs.lib;
pi5 = self.lib.pi5NiriKdl;
rpi5Extra = lib.optionalString (niriCfg.raspberryPi5DrmWorkaround or false) pi5.drmExtraConfig;
base = niriBaseSettings pkgs;
userExtra = niriCfg.extraSettings or { };
keyringExtra = lib.optionalString keyringEnable keyringNiriStartupKdl;
extraConfigMerged = keyringExtra + rpi5Extra + (userExtra.extraConfig or "");
windowRules = (base.window-rules or [ ]) ++ (userExtra.window-rules or [ ]);
in
lib.recursiveUpdate base (
lib.removeAttrs userExtra [ "window-rules" "extraConfig" ]
// lib.optionalAttrs (windowRules != [ ]) {
window-rules = windowRules;
}
// lib.optionalAttrs (rpi5Extra != "" || (userExtra.extraConfig or "") != "") {
extraConfig = extraConfigMerged;
}
);
in
{
flake.homeManagerModules.desktopNiri =
{ lib, pkgs, osConfig ? { }, ... }:
let
niriOs = osConfig.chiasson.desktop.niri or { };
niriEnabled = osConfig.chiasson.desktop.niri.enable or false;
keyringEnabled = osConfig.chiasson.desktop.keyring.enable or false;
mergedSettings = mergeNiriSettings pkgs niriOs keyringEnabled;
niriConfigPkg = inputs.wrapper-modules.wrappers.niri.wrap {
inherit pkgs;
settings = mergedSettings;
};
in
{
config = lib.mkIf niriEnabled {
xdg.configFile."niri/config.kdl".source = "${niriConfigPkg}/niri-config.kdl";
};
};
flake.nixosModules.desktopNiri =
{ config, options, lib, pkgs, self, ... }:
let
cfg = config.chiasson.desktop;
hmAvailable = lib.hasAttrByPath [ "home-manager" "sharedModules" ] options;
in
{
options.chiasson.desktop.niri = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Niri compositor session + NixOS packages.";
};
raspberryPi5DrmWorkaround = lib.mkEnableOption ''
Pi 5 + RP1 DSI: Niri DRM debug rules (renderD128, ignore card1/2) + matching DankGreeter niri config.
Opt-in only HDMI-only / other Pi setups do not need this.
'';
extraSettings = lib.mkOption {
type = lib.types.attrs;
default = { };
description = ''
Merged into shared defaults `config.kdl` via wrapper-modules. Put raw KDL in `extraConfig`.
'';
};
};
config = lib.mkMerge [
{
assertions = [
{
assertion = !cfg.niri.raspberryPi5DrmWorkaround || cfg.niri.enable;
message = "chiasson.desktop.niri.raspberryPi5DrmWorkaround requires chiasson.desktop.niri.enable.";
}
];
}
(lib.mkIf cfg.niri.enable {
programs.niri.enable = true;
programs.niri.package = pkgs.niri;
programs.xwayland.enable = true;
xdg.portal.extraPortals = [ pkgs.xdg-desktop-portal-gnome ];
# Niri resolves `xwayland-satellite` from PATH to provide XWayland + `$DISPLAY` for X11
# clients (Steam, etc.). See https://github.com/YaLTeR/niri/issues/452
environment.systemPackages = [ pkgs.xwayland-satellite ];
})
(lib.mkIf (cfg.niri.enable && hmAvailable) {
"home-manager".sharedModules = [ self.homeManagerModules.desktopNiri ];
})
];
};
}
+218
View File
@@ -0,0 +1,218 @@
{ ... }: {
flake.nixosModules.desktopOptions =
{ config, options, lib, pkgs, self, inputs, ... }:
let
cfg = config.chiasson.desktop;
hmAvailable = lib.hasAttrByPath [ "home-manager" "sharedModules" ] options;
guiEnabled = cfg.hyprland.enable || cfg.niri.enable || cfg.plasma.enable;
dmsEnabled = cfg.shell == "dms";
sddmIni = pkgs.formats.ini { };
# Pixie SDDM theme — Qt6 main; upstream has a qt5 branch if you need it.
pixieSddm = pkgs.stdenvNoCC.mkDerivation {
pname = "pixie-sddm";
version = "0-unstable-2026-03-29";
src = pkgs.fetchFromGitHub {
owner = "xCaptaiN09";
repo = "pixie-sddm";
rev = "12a5f459ebd6d699be42c188c10976c8bb7076d7";
hash = "sha256-lmE/49ySuAZDh5xLochWqfSw9qWrIV+fYaK5T2Ckck8=";
};
dontConfigure = true;
dontBuild = true;
installPhase = ''
mkdir -p "$out/share/sddm/themes/pixie"
cp -r "$src"/. "$out/share/sddm/themes/pixie/"
'';
meta = {
description = "Pixel / Material 3 inspired SDDM theme (Qt6)";
homepage = "https://github.com/xCaptaiN09/pixie-sddm";
license = lib.licenses.mit;
platforms = lib.platforms.linux;
};
};
in
{
options.chiasson.desktop = {
defaultSession = lib.mkOption {
type = lib.types.nullOr (lib.types.enum [
"hyprland"
"niri"
"plasma"
]);
default = null;
example = "niri";
description = ''
DM session preference; `null` picks from which compositor flags are enabled. Turn on only
the compositor you use so SDDM does not drag in extras.
'';
};
shell = lib.mkOption {
type = lib.types.nullOr (lib.types.enum [ "dms" ]);
default = null;
example = "dms";
description = "Desktop shell overlay (e.g. DMS). Extend the enum when you add another.";
};
displayManager = {
variant = lib.mkOption {
type = lib.types.nullOr (lib.types.enum [
"sddm"
"dankgreeter"
]);
default = null;
description = ''
SDDM vs DankGreeter (greetd + DMS [docs](https://danklinux.com/docs/dankgreeter/nixos-flake)).
`null`: Plasma-only SDDM; Hyprland/Niri + `desktop.shell = "dms"` DankGreeter; else SDDM.
'';
};
greeter = {
configHome = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/home/olivier";
description = ''
Whose DMS files to mirror into the greeter cache. `null` first entry in `chiasson.users.enabled`.
'';
};
};
};
displayManager.sddm = {
wayland.enable = lib.mkEnableOption ''
SDDM greeter on Wayland (nicer on HiDPI; turn off if the greeter glitches on your GPU).
'' // {
default = true;
};
enableHidpi = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Passed through to `services.displayManager.sddm.enableHidpi`.";
};
theme = {
package = lib.mkOption {
type = lib.types.nullOr lib.types.package;
default = pixieSddm;
description = ''
Package providing `share/sddm/themes/<id>`. Default: bundled [Pixie](https://github.com/xCaptaiN09/pixie-sddm).
`null` Breeze. Other nixpkgs examples: `catppuccin-sddm`, `sddm-sugar-dark`.
'';
};
id = lib.mkOption {
type = lib.types.str;
default = "pixie";
description = ''
Subdir under `share/sddm/themes/` (default `pixie`). Match your theme package (e.g. catppuccin ids).
'';
};
};
settings = lib.mkOption {
type = sddmIni.type;
default = { };
description = "Extra `services.displayManager.sddm.settings` (INI).";
};
};
plasma.enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Plasma 6 session bits for this flake.";
};
defaultPackages = {
enabled = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Shared desktop utility packages (ntfs3g, cifs-utils, ).";
};
packages = lib.mkOption {
type = lib.types.listOf lib.types.package;
default = with pkgs; [
ntfs3g
cifs-utils
usbutils
xhost
];
description = "Packages merged when `defaultPackages.enabled` is true.";
};
};
extraPackages = lib.mkOption {
type = lib.types.listOf lib.types.package;
default = [ ];
description = "Extra packages on GUI hosts.";
};
homeManager = {
bundleWisdom = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Add `wisdom` (baseline + bash) to HM `sharedModules`. Other slices still go in per-user `extraModules`.
'';
};
};
keyring = {
enable = lib.mkEnableOption ''
gnome-keyring + pam (login + sddm or greetd) + HM user service + `gcr` + `services.xserver.updateDbusEnvironment`.
niri/hyprland: `dbus-update-activation-environment` at compositor start so libsecret/Electron see `WAYLAND_DISPLAY`.
'' // {
default = true;
};
};
};
config = lib.mkMerge [
{
assertions = [
{
assertion = cfg.defaultSession != "hyprland" || cfg.hyprland.enable;
message = "chiasson.desktop.defaultSession = \"hyprland\" requires chiasson.desktop.hyprland.enable = true.";
}
{
assertion = cfg.defaultSession != "niri" || cfg.niri.enable;
message = "chiasson.desktop.defaultSession = \"niri\" requires chiasson.desktop.niri.enable = true.";
}
{
assertion = cfg.defaultSession != "plasma" || cfg.plasma.enable;
message = "chiasson.desktop.defaultSession = \"plasma\" requires chiasson.desktop.plasma.enable = true.";
}
{
assertion =
cfg.displayManager.variant != "dankgreeter" || cfg.hyprland.enable || cfg.niri.enable;
message = "chiasson.desktop.displayManager.variant = \"dankgreeter\" requires chiasson.desktop.hyprland or chiasson.desktop.niri.";
}
{
assertion = cfg.displayManager.variant != "dankgreeter" || cfg.shell == "dms";
message = "DankGreeter expects chiasson.desktop.shell = \"dms\" for DMS/matugen sync; use SDDM or enable DMS.";
}
];
}
(lib.mkIf guiEnabled {
chiasson.desktop.displayManager.variant = lib.mkDefault (
if cfg.plasma.enable && !(cfg.hyprland.enable || cfg.niri.enable) then
"sddm"
else if (cfg.hyprland.enable || cfg.niri.enable) && cfg.shell == "dms" then
"dankgreeter"
else
"sddm"
);
})
(lib.mkIf (guiEnabled && hmAvailable && cfg.homeManager.bundleWisdom) {
"home-manager".sharedModules = [ self.homeManagerModules.wisdom ];
})
(lib.mkIf (dmsEnabled && hmAvailable) {
"home-manager".sharedModules = [ self.homeManagerModules.desktopShellDms ];
})
(lib.mkIf (hmAvailable && (dmsEnabled || cfg.niri.enable)) {
"home-manager".extraSpecialArgs = { inherit inputs; };
})
];
};
}
+14
View File
@@ -0,0 +1,14 @@
{ ... }: {
flake.nixosModules.desktopPlasma =
{ config, lib, pkgs, ... }:
let
cfg = config.chiasson.desktop;
in
{
config = lib.mkIf cfg.plasma.enable {
services.desktopManager.plasma6.enable = true;
environment.etc."xdg/menus/applications.menu".text =
builtins.readFile "${pkgs.kdePackages.plasma-workspace}/etc/xdg/menus/plasma-applications.menu";
};
};
}
+90
View File
@@ -0,0 +1,90 @@
{ inputs, ... }: {
flake.nixosModules.desktopShellDmsOptions = { lib, ... }: {
options.chiasson.desktop.shells.dms = {
enableGpuTemp = lib.mkOption {
type = lib.types.bool;
default = true;
description = "GPU temp in DMS bar.";
};
obsidianSnippetsDir = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Legacy single Obsidian snippets dir for matugen.";
};
obsidianConfigDirs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Vault `.obsidian/` paths for matugen.";
};
obsidianSnippetsDirs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Explicit `.obsidian/snippets` paths.";
};
extraRightBarWidgets = lib.mkOption {
type = lib.types.listOf lib.types.attrs;
default = [ ];
description = "Extra right-bar widgets (prepended).";
};
enableWvkbdToggle = lib.mkEnableOption ''
wvkbd DMS plugin + bar toggle (touch / uConsole).
'';
enableRbwLockToggle = lib.mkEnableOption ''
rbw vault lock/unlock button in the DMS bar (Bitwarden CLI via rbw).
'';
rebuildCommand = lib.mkOption {
type = lib.types.nullOr (lib.types.listOf lib.types.str);
default = null;
example = [ "sudo" "nixos-rebuild" "switch" "--flake" ".#14900k" ];
description = "Command used by DMS nix-monitor widget for rebuild actions.";
};
};
};
flake.homeManagerModules.desktopShellDms = {
lib,
osConfig ? { },
...
}:
let
cfg = lib.attrByPath [ "chiasson" "desktop" "shells" "dms" ] { } osConfig;
selectedShell = lib.attrByPath [ "chiasson" "desktop" "shell" ] null osConfig;
dmsEnabled = selectedShell == "dms";
hostName = lib.attrByPath [ "networking" "hostName" ] "nixos" osConfig;
rebuildCommand =
if (cfg.rebuildCommand or null) != null then
cfg.rebuildCommand
else
[ "sudo" "nixos-rebuild" "switch" "--flake" ".#${hostName}" ];
in
{
imports = [
./home-manager/default.nix
];
config = lib.mkIf dmsEnabled {
dms.enable = true;
dms.enableGpuTemp = cfg.enableGpuTemp or true;
dms.obsidianSnippetsDir = cfg.obsidianSnippetsDir or null;
dms.obsidianConfigDirs = cfg.obsidianConfigDirs or [ ];
dms.obsidianSnippetsDirs = cfg.obsidianSnippetsDirs or [ ];
dms.enableWvkbdToggle = cfg.enableWvkbdToggle or false;
dms.enableRbwLockToggle = cfg.enableRbwLockToggle or false;
dms.extraRightBarWidgets =
(lib.optionals (cfg.enableWvkbdToggle or false) [
{
id = "wvkbdToggle";
enabled = true;
}
])
++ (lib.optionals (cfg.enableRbwLockToggle or false) [
{
id = "rbwLockToggle";
enabled = true;
}
])
++ (cfg.extraRightBarWidgets or [ ]);
programs.nix-monitor.rebuildCommand = rebuildCommand;
};
};
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,80 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.Plugins
PluginComponent {
id: root
property bool vaultUnlocked: false
function refreshLockState() {
if (!statusProcess.running)
statusProcess.exec(["rbw", "unlocked"]);
}
function toggleLock() {
if (root.vaultUnlocked) {
Quickshell.execDetached(["rbw", "lock"]);
} else {
Quickshell.execDetached(["rbw", "unlock"]);
}
}
pillClickAction: () => root.toggleLock()
Component.onCompleted: refreshLockState()
Timer {
interval: 2500
repeat: true
running: true
onTriggered: root.refreshLockState()
}
Process {
id: statusProcess
command: ["rbw", "unlocked"]
onExited: (exitCode, _exitStatus) => {
root.vaultUnlocked = exitCode === 0;
}
}
horizontalBarPill: Component {
MouseArea {
implicitWidth: iconH.implicitWidth
implicitHeight: iconH.implicitHeight
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.toggleLock()
DankIcon {
id: iconH
name: root.vaultUnlocked ? "lock_open_right" : "lock"
size: root.iconSize
color: parent.containsMouse ? Theme.primary : Theme.surfaceText
}
}
}
verticalBarPill: Component {
MouseArea {
implicitWidth: iconV.implicitWidth
implicitHeight: iconV.implicitHeight
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.toggleLock()
DankIcon {
id: iconV
name: root.vaultUnlocked ? "lock_open_right" : "lock"
size: root.iconSize
color: parent.containsMouse ? Theme.primary : Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
@@ -0,0 +1,16 @@
import QtQuick
import qs.Common
import qs.Modules.Plugins
PluginSettings {
id: root
pluginId: "rbwLockToggle"
StyledText {
width: parent.width
text: "Shows rbw vault state with a closed lock (locked) or open lock (unlocked). Click to run `rbw unlock` (pinentry) or `rbw lock`. Requires `rbw` on PATH in the DMS session and a working pinentry."
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
}
@@ -0,0 +1,13 @@
{
"id": "rbwLockToggle",
"name": "Bitwarden (rbw) lock",
"description": "Bar control for rbw vault: locked/unlocked padlock; click to unlock or lock",
"version": "1.0.0",
"author": "NixOS-V2",
"type": "widget",
"capabilities": ["dankbar-widget"],
"component": "./RbwLockToggle.qml",
"settings": "./RbwLockToggleSettings.qml",
"icon": "lock_open",
"permissions": ["settings_read"]
}
@@ -0,0 +1,51 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.Plugins
PluginComponent {
id: root
function toggleKeyboard() {
Quickshell.execDetached(["sh", "-c", "pkill -SIGRTMIN -x wvkbd-mobintl"]);
}
pillClickAction: () => root.toggleKeyboard()
horizontalBarPill: Component {
MouseArea {
implicitWidth: icon.implicitWidth
implicitHeight: icon.implicitHeight
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.toggleKeyboard()
DankIcon {
id: icon
name: "keyboard"
size: Theme.iconSize
color: parent.containsMouse ? Theme.primary : Theme.surfaceText
}
}
}
verticalBarPill: Component {
MouseArea {
implicitWidth: iconV.implicitWidth
implicitHeight: iconV.implicitHeight
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.toggleKeyboard()
DankIcon {
id: iconV
name: "keyboard"
size: Theme.iconSize
color: parent.containsMouse ? Theme.primary : Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
@@ -0,0 +1,16 @@
import QtQuick
import qs.Common
import qs.Modules.Plugins
PluginSettings {
id: root
pluginId: "wvkbdToggle"
StyledText {
width: parent.width
text: "Click the keyboard icon in the bar to show or hide the on-screen keyboard (wvkbd)."
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
}
@@ -0,0 +1,13 @@
{
"id": "wvkbdToggle",
"name": "Virtual Keyboard Toggle",
"description": "Bar button to show/hide the wvkbd on-screen keyboard (for touch/tablet)",
"version": "1.0.0",
"author": "NixOS-V2",
"type": "widget",
"capabilities": ["dankbar-widget"],
"component": "./WvkbdToggle.qml",
"settings": "./WvkbdToggleSettings.qml",
"icon": "keyboard",
"permissions": ["settings_read"]
}
@@ -0,0 +1,119 @@
/**
* Vesktop / Vencord theme — matugen + DMS wallpaper colors.
* Derived from pywal-vencord-style mapping (NixOS-New templates/colors-discord.css).
*/
* {
/* Surfaces */
--background-primary: {{colors.background.default.hex}};
--background-secondary: {{colors.surface.default.hex}};
--background-tertiary: {{colors.surface_dim.default.hex}};
--background-primary-alt: {{colors.background.default.hex}};
--background-secondary-alt: {{colors.surface.default.hex}};
--background-tertiary-alt: {{colors.surface_dim.default.hex}};
--channeltextarea-background: {{colors.surface.default.hex}};
--custom-channel-members-bg: {{colors.surface.default.hex}};
--profile-gradient-primary-color: {{colors.surface.default.hex}};
--profile-gradient-secondary-color: {{colors.surface_variant.default.hex}};
--__header-bar-background: {{colors.surface.default.hex}} !important;
--bg-base-tertiary: {{colors.background.default.hex}};
--card-primary-bg: color-mix(in srgb, {{colors.surface.default.hex}}, black 20%);
--input-background: color-mix(in srgb, {{colors.surface.default.hex}}, black 20%);
--autocomplete-bg: color-mix(in srgb, {{colors.surface.default.hex}}, black 20%);
--background-nested-floating: color-mix(in srgb, {{colors.surface.default.hex}}, black 20%);
--background-floating: color-mix(in srgb, {{colors.surface.default.hex}}, black 20%);
--scrollbar-auto-track: color-mix(in srgb, {{colors.surface.default.hex}}, black 20%);
--scrollbar-thin-track: color-mix(in srgb, {{colors.surface.default.hex}}, black 20%);
--border-subtle: color-mix(in srgb, {{colors.surface.default.hex}}, black 20%);
--background-base-lowest: color-mix(in srgb, {{colors.surface.default.hex}}, black 20%);
--background-surface-high: color-mix(in srgb, {{colors.surface.default.hex}}, black 20%);
--button-secondary-background: color-mix(in srgb, {{colors.surface.default.hex}}, black 30%);
--background-surface-higher: color-mix(in srgb, {{colors.surface.default.hex}}, black 30%);
--background-base-lower: color-mix(in srgb, {{colors.surface.default.hex}}, black 35%);
--background-message-hover: color-mix(in srgb, {{colors.surface.default.hex}}, black 40%);
--button-secondary-background-hover: color-mix(in srgb, {{colors.surface.default.hex}}, black 40%);
--background-base-low: color-mix(in srgb, {{colors.surface.default.hex}}, black 40%);
--background-surface-highest: color-mix(in srgb, {{colors.surface.default.hex}}, black 40%);
--chat-background-default: color-mix(in srgb, {{colors.surface.default.hex}}, black 45%);
--button-secondary-background-active: color-mix(in srgb, {{colors.surface.default.hex}}, black 60%);
--primary-630: {{colors.surface_variant.default.hex}};
/* Muted / secondary chrome */
--scrollbar-auto-thumb: {{colors.on_surface_variant.default.hex}};
--scrollbar-thin-thumb: {{colors.on_surface_variant.default.hex}};
--interactive-muted: {{colors.on_surface_variant.default.hex}};
--text-muted: {{colors.on_surface_variant.default.hex}};
--background-modifier-hover: color-mix(in srgb, {{colors.on_surface_variant.default.hex}}, black 40%);
--background-modifier-active: color-mix(in srgb, {{colors.on_surface_variant.default.hex}}, black 20%);
--background-modifier-accent: {{colors.secondary_container.default.hex}};
--background-accent: {{colors.secondary.default.hex}};
--input-border: {{colors.outline.default.hex}};
--border-normal: {{colors.outline.default.hex}};
--icon-secondary: {{colors.on_surface_variant.default.hex}};
--icon-tertiary: {{colors.on_surface_variant.default.hex}};
--channel-icon: {{colors.on_surface_variant.default.hex}};
--channels-default: {{colors.on_surface_variant.default.hex}};
--header-primary: {{colors.on_surface.default.hex}};
--__lottieIconColor: {{colors.on_surface_variant.default.hex}};
--interactive-normal: {{colors.on_surface_variant.default.hex}};
/* Selection / highlights */
--red-400: {{colors.error.default.hex}};
--background-modifier-selected: {{colors.secondary_container.default.hex}};
--notice-background-positive: color-mix(in srgb, {{colors.tertiary.default.hex}}, black 75%);
--notice-text-positive: {{colors.tertiary.default.hex}};
/* Danger */
--status-danger: {{colors.error.default.hex}};
--button-outline-danger-border: {{colors.error.default.hex}};
--button-outline-danger-text: {{colors.error.default.hex}};
--button-danger-background: {{colors.error.default.hex}};
--yellow-300: {{colors.error.default.hex}};
/* Brand / accents */
--brand-experiment: {{colors.primary.default.hex}};
--brand-experiment-360: {{colors.primary.default.hex}};
--brand-experiment-500: {{colors.primary.default.hex}};
--profile-gradient-button-color: {{colors.primary.default.hex}};
--green-360: {{colors.primary.default.hex}};
/* Foreground */
--text-normal: {{colors.on_surface.default.hex}};
--interactive-active: {{colors.on_surface.default.hex}};
}
/* Status indicators (legacy Discord hex fills) */
rect[fill="#d83a41"] {
fill: {{colors.error.default.hex}} !important;
}
rect[fill="#cc954c"] {
fill: {{colors.secondary.default.hex}} !important;
}
rect[fill="#40a258"] {
fill: {{colors.primary.default.hex}} !important;
}
.wrapper_ef3116 {
background: {{colors.background.default.hex}} !important;
}
.bar_c38106 {
background: {{colors.background.default.hex}} !important;
}
+52
View File
@@ -0,0 +1,52 @@
{ inputs, ... }: {
flake.nixosModules.desktopWallpapers =
{ config, lib, pkgs, ... }:
let
cfg = config.chiasson.desktop.wallpapers;
d = config.chiasson.desktop;
guiEnabled = d.hyprland.enable || d.niri.enable || d.plasma.enable;
wallpaperDir = pkgs.stdenvNoCC.mkDerivation {
pname = "nixos-v2-wallpapers";
version = "0.1";
src = builtins.path {
path = cfg.source;
name = "wallpapers-src";
};
dontConfigure = true;
dontBuild = true;
installPhase = ''
mkdir -p "$out/share/wallpapers"
find "$src" -mindepth 1 -maxdepth 1 ! -name .git -exec cp -a {} "$out/share/wallpapers/" \;
'';
};
installPath = "${wallpaperDir}/share/wallpapers";
in
{
options.chiasson.desktop.wallpapers = {
enable = lib.mkEnableOption ''
Copy `source` into the store; sets `NIXOS_V2_WALLPAPERS` and `/etc/wallpapers`.
'' // {
default = true;
};
source = lib.mkOption {
type = lib.types.path;
default = inputs.wallpapers;
description = ''
Directory copied into the store. Default: `inputs.wallpapers`
(`git+https://git.chiasson.cloud/Olivier/wallpapers`, pinned in `flake.lock`).
'';
};
};
config = lib.mkIf (guiEnabled && cfg.enable) {
environment = {
variables.NIXOS_V2_WALLPAPERS = installPath;
sessionVariables.NIXOS_V2_WALLPAPERS = installPath;
etc."wallpapers".source = installPath;
};
};
};
}
+127
View File
@@ -0,0 +1,127 @@
{ ... }: {
flake.nixosModules.desktopWaydroid =
{ config, lib, pkgs, ... }:
# `virtualisation.waydroid.package` defaults to `waydroid-nftables` when `networking.nftables.enable`
# is true — required on recent kernels where legacy `ip_tables` / `waydroid-net.sh` (iptables) fails
# with `waydroid session start` (nixpkgs#459520).
let
cfg = config.chiasson.desktop.waydroid;
in
{
options.chiasson.desktop.waydroid = {
enable = lib.mkEnableOption ''
Waydroid + synced base props / nav mode. Needs Wayland; desktop hosts only. This module also
sets `networking.nftables.enable` (default) so NixOS uses `waydroid-nftables` needed on newer
kernels where `waydroid-net.sh` / iptables fails. For **Google Play** apps (e.g. Hot Wheels
Showcase), run `sudo waydroid init -s GAPPS -f` once after the first `nixos-rebuild` (or reset
the container if you already initialized without GAPPS); then sign in and install from the Play
Store. If `dnsmasq` reports port 53 in use, free that port (Waydroid needs it).
'';
width = lib.mkOption {
type = lib.types.int;
default = 1920;
description = "Waydroid rendering width (`persist.waydroid.width`).";
};
height = lib.mkOption {
type = lib.types.int;
default = 1080;
description = "Waydroid rendering height (`persist.waydroid.height`).";
};
multiWindows = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable Waydroid multi-window integration.";
};
navigationMode = lib.mkOption {
type = lib.types.enum [ "3button" "gestures" ];
default = "gestures";
description = "Maps to Waydroid `navigation_mode` secure prop (3button=0, gestures=2).";
};
extraBaseProperties = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
description = ''
Extra `key = value` pairs written into `/var/lib/waydroid/waydroid_base.prop` on activation
(same mechanism as width/height). Use for upstream tweaks e.g. NVIDIA hosts often need
`ro.hardware.gralloc=default` and `ro.hardware.egl=swiftshader`; Linux 5.18+ may need
`sys.use_memfd=true` ([NixOS Waydroid wiki](https://wiki.nixos.org/wiki/Waydroid)).
'';
};
};
config = lib.mkIf cfg.enable {
networking.nftables.enable = lib.mkDefault true;
virtualisation.waydroid.enable = true;
system.activationScripts.waydroidProps = {
text = ''
PROP_FILE="/var/lib/waydroid/waydroid_base.prop"
if [ ! -f "$PROP_FILE" ]; then
echo "waydroid: $PROP_FILE not found yet, skipping prop sync."
echo "waydroid: run 'sudo waydroid init' once, then rebuild."
exit 0
fi
set_prop() {
key="$1"
value="$2"
if ${lib.getExe pkgs.gnugrep} -q "^''${key}=" "$PROP_FILE"; then
${pkgs.gnused}/bin/sed -i "s|^''${key}=.*|''${key}=''${value}|" "$PROP_FILE"
else
printf "%s=%s\n" "$key" "$value" >> "$PROP_FILE"
fi
}
set_prop "persist.waydroid.multi_windows" "${if cfg.multiWindows then "true" else "false"}"
set_prop "persist.waydroid.width" "${toString cfg.width}"
set_prop "persist.waydroid.height" "${toString cfg.height}"
${lib.concatStringsSep "\n" (
lib.mapAttrsToList (k: v: ''
set_prop ${lib.escapeShellArg k} ${lib.escapeShellArg v}'') cfg.extraBaseProperties
)}
'';
};
systemd.services.waydroid-navigation-mode = {
description = "Set Waydroid Android navigation mode";
wantedBy = [ "multi-user.target" ];
after = [ "waydroid-container.service" ];
wants = [ "waydroid-container.service" ];
path = [ config.virtualisation.waydroid.package ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
set -eu
nav_mode="${if cfg.navigationMode == "gestures" then "2" else "0"}"
if ! systemctl -q is-active waydroid-container.service; then
exit 0
fi
for _ in $(seq 1 30); do
if waydroid shell settings put secure navigation_mode "$nav_mode" >/dev/null 2>&1; then
exit 0
fi
sleep 1
done
echo "waydroid: warning: could not set navigation_mode=$nav_mode (container not ready?)" >&2
exit 0
'';
};
};
};
}
@@ -0,0 +1,61 @@
# Monitor layout for 14900k (ported from NixOS-New `hosts/clients/14900k/home.nix`).
# Niri: `chiasson.desktop.niri.extraSettings` (`extraConfig` KDL + `binds` merged with defaults).
# Hyprland: `chiasson.desktop.hyprland.settings` (merged in HM when `chiasson.desktop.hyprland.enable`).
#TODO[epic=Moderate] Clean this up, move to host's configuration.nix.
{ config, lib, ... }:
{
chiasson.desktop.niri.extraSettings = {
extraConfig = ''
output "DP-2" {
mode "2560x1080@144"
scale 1.0
position x=0 y=0
focus-at-startup
}
output "HDMI-A-3" {
mode "1920x1080@60"
scale 1.0
position x=-1920 y=0
}
output "DP-4" {
mode "1920x1080@144"
scale 1.0
position x=0 y=-1080
}
'';
binds."XF86Tools".spawn = [
"wpctl"
"set-mute"
"@DEFAULT_AUDIO_SOURCE@"
"toggle"
];
};
chiasson.desktop.hyprland.settings = lib.mkIf config.chiasson.desktop.hyprland.enable (
let
monitorList = [
"DP-2, 2560x1080@144, 0x0, 1"
"DP-4, 1920x1080@144, 0x-1080, 1"
"HDMI-A-3, 1920x1080@60, -1920x0, 1"
];
workspaceList = [
"1, monitor:DP-3, default:true"
"2, monitor:DP-3"
"3, monitor:DP-3"
"4, monitor:Unknown-2, default:true"
"5, monitor:Unknown-2"
"6, monitor:Unknown-2"
"7, monitor:DP-4"
"8, monitor:DP-4"
"9, monitor:DP-4"
];
in
{
monitor = lib.mkBefore monitorList;
workspace = workspaceList;
}
);
}
@@ -0,0 +1,70 @@
# NFS exports from nixdesk (14900k) to r5500 (192.168.2.100), formerly nix-server (192.168.2.238):
# - /mnt/deep/jellyfin → nix-server /mnt/nixdesk-jellyfin (Jellyfin bulk libraries)
#
# Jellyfin root on nixdesk uses owner olivier + group nfsmedia (990); dirs here are 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.
users.groups.nfsmedia = { gid = 990; };
users.users.nfsmedia = {
isSystemUser = true;
uid = 990;
group = "nfsmedia";
};
# olivier: owner for local use; nfsmedia: group used by NFS all_squash (990).
systemd.tmpfiles.settings."14900k-nfs-export-paths" = {
"/mnt/deep/jellyfin"."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/deep/jellyfin
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).
services.nfs.server = {
enable = true;
mountdPort = 4000;
lockdPort = 4001;
statdPort = 4002;
# 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/deep/jellyfin 192.168.2.100(rw,sync,no_subtree_check,crossmnt,root_squash,all_squash,anonuid=${toString olivierUid},anongid=990,fsid=1)
/mnt/deep/jellyfin 192.168.2.238(rw,sync,no_subtree_check,crossmnt,root_squash,all_squash,anonuid=${toString olivierUid},anongid=990,fsid=2)
'';
};
networking.firewall.allowedTCPPorts = [
111 # portmapper
2049
4000
4001
4002
];
networking.firewall.allowedUDPPorts = [
111
2049
4000
4001
4002
];
}
@@ -0,0 +1,47 @@
# 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;
# LABEL="MediaLibrary" (btrfs on sda1 by UUID). No subvol=@ — this disk has no @ subvolume.
fileSystems."/mnt/2nd" = {
device = "/dev/disk/by-uuid/17d8a981-db3b-415e-a0f7-7dbc519e04ab";
fsType = "btrfs";
options = [
"compress=zstd"
"noatime"
"nofail"
"x-systemd.device-timeout=30"
];
};
#new deep storage unit
fileSystems."/mnt/deep" = {
device = "/dev/disk/by-uuid/64fb08fe-da5d-4405-afa3-1603a411e9e5";
fsType = "btrfs";
options = [
"compress=zstd"
"noatime"
"nofail"
"x-systemd.device-timeout=30"
];
};
# 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"
# ];
#};
}
@@ -0,0 +1,14 @@
# Moonfin 2.0.0 Jellyfin client — upstream Flatpak bundle (not on Flathub yet).
# https://github.com/Moonfin-Client/Moonfin-Core/releases/tag/2.0.0
{ pkgs, ... }:
{
chiasson.system.flatpak.bundles = [
{
appId = "org.moonfin.linux";
bundle = pkgs.fetchurl {
url = "https://github.com/Moonfin-Client/Moonfin-Core/releases/download/2.0.0/Moonfin_Linux_v2.0.0.flatpak";
hash = "sha256-sLtrsqBaJ1wriTkIdLylqMc9ygNkHrNm4YS/816nIFQ=";
};
}
];
}
+19
View File
@@ -0,0 +1,19 @@
# NVIDIA for host desktop.
{ config, lib, pkgs, ... }:
{
boot.kernelParams = [ "snd_hda_core.gpu_bind=0" ];
boot.kernelPackages = lib.mkDefault pkgs.linuxPackages_latest;
services.xserver.videoDrivers = [ "nvidia" ];
hardware.nvidia = {
modesetting.enable = true;
powerManagement.enable = false;
powerManagement.finegrained = false;
open = true;
nvidiaSettings = true;
package = config.boot.kernelPackages.nvidiaPackages.latest;
};
hardware.nvidia-container-toolkit.enable = true;
}
@@ -0,0 +1,12 @@
# Logitech Unifying / Bolt receivers; Keychron VIA/VIAL on hidraw.
{ ... }:
{
hardware.logitech.wireless.enable = true;
services.udev.extraRules = ''
# Keychron VIA/VIAL on hidraw
KERNEL=="hidraw*", ATTRS{idVendor}=="3434", MODE="0660", GROUP="users", TAG+="uaccess", TAG+="udev-acl"
# PS5 DualSense & DualSense Edge controllers over USB hidraw
KERNEL=="hidraw*", ATTRS{idVendor}=="054c", ATTRS{idProduct}=="0ce6|0df2", MODE="0660", TAG+="uaccess"
'';
}
@@ -0,0 +1,15 @@
{ ... }: {
#TODO[epic=Moderate] Clean this up, move to host's configuration.nix.
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
powerManagement.cpuFreqGovernor = "performance";
hardware.enableRedistributableFirmware = true;
hardware.enableAllFirmware = true;
hardware.cpu.intel.updateMicrocode = true;
nix.settings.experimental-features = [ "nix-command" "flakes" ];
system.stateVersion = "25.11";
}
@@ -0,0 +1,21 @@
# Epson ET-2760 on WiFi via IPP/mDNS.
# Import from configuration.nix when you need printing (see commented import there).
{ pkgs, ... }:
{
services.printing = {
enable = true;
webInterface = true;
drivers = with pkgs; [
epson-escpr2
epson-escpr
];
};
services.avahi = {
enable = true;
nssmdns4 = true;
openFirewall = true;
};
networking.firewall.allowedTCPPorts = [ 631 ];
}
+68 -15
View File
@@ -15,7 +15,11 @@
./_private/platform.nix
./_private/nvidia.nix
./_private/peripherals.nix
# ./_private/printing-epson.nix
./_private/displays.nix
./_private/media-disk.nix
./_private/jellyfin-nfs-export.nix
./_private/moonfin-flatpak.nix
];
sops = {
@@ -36,7 +40,25 @@
group = "users";
mode = "0400";
};
services.cloudflare-warp.enable = true;
# Intel iGPU video acceleration (VA-API / QSV via oneVPL).
# This fixes common NixOS issues like `vaInitialize failed` and missing QSV encoders in apps.
hardware.graphics = {
enable = true;
enable32Bit = true; # Required by Wine/DXVK for 32-bit Vulkan userspace.
extraPackages = with pkgs; [
intel-media-driver # iHD (Gen8+)
vpl-gpu-rt # oneVPL runtime (QSV)
libvdpau-va-gl
];
};
environment.sessionVariables = {
LIBVA_DRIVER_NAME = "iHD";
# Force VA-API to use the Intel iGPU render node (otherwise libva may pick NVIDIA and iHD fails).
LIBVA_DRM_DEVICE = "/dev/dri/renderD128";
};
chiasson.system.caching.attic = {
enable = true;
@@ -64,33 +86,52 @@
};
};
chiasson.system.chromiumHevc.enable = true;
chiasson.system = {
# libvirt/QEMU + VFIO; host uses Intel iGPU for Niri while NVIDIA is passed through (see
# `_private/nvidia.nix`, `_private/displays.nix`). If your GPU is not RTX 2070-class IDs, set
# `chiasson.system.vm.gpuPassthrough.vfioIds` from `lspci -nn` (GPU + HDA functions in the same group).
vm = {
enable = true;
gpuPassthrough.enable = false;
};
ytDlpTelequebecPatch.enable = true;
remoteDesktop = {
enable = false;
moonlight.enable = false;
sunshine.enable = false;
};
audio.enable = true;
docker.enable = true;
gaming.enable = true;
gaming.launchers.enableBottles = false;
gaming.gamescope.enable = true;
gaming.steam.steamTinkerLaunch.enable = true;
gaming.sunshine.enable = true;
gaming.sunshine.cudaSupport = true;
monitorInput.enable = true;
flatpak.enable = true;
flatpak.flathub.appIds = [ "com.usebottles.bottles" ];
palera1n.enable = true;
uconsoleKernelBuilder.enable = true;
extraPackages = [ pkgs.sops pkgs.nodejs_22 ];
extraPackages = with pkgs; [
sops
nodejs_22
ffmpeg
bento4
yt-dlp
# Native install (avoid flatpak sandbox issues for QSV/VAAPI).
handbrake
qbittorrent
# Diagnostics
libva-utils # vainfo
vlc
element-desktop
thunderbird
prismlauncher
dualsensectl
devenv
];
networking = {
hostName = "nixdesk";
@@ -106,6 +147,9 @@
self.homeManagerModules.wisdomTerminalsKitty
self.homeManagerModules.wisdomBrowsersEdge
self.homeManagerModules.wisdomBrowsersFlow
self.homeManagerModules.wisdomBrowsersOrion
self.homeManagerModules.wisdomBrowsersZen
self.homeManagerModules.wisdomBrowsersChromiumHevc
self.homeManagerModules.wisdomEditorsCursor
self.homeManagerModules.wisdomEditorsObsidian
self.homeManagerModules.wisdomShellYazi
@@ -120,8 +164,10 @@
{
programs.git = {
enable = true;
userName = "OlivierChiasson";
userEmail = "olivierchiasson@hotmail.fr";
settings.user = {
name = "OlivierChiasson";
email = "olivierchiasson@hotmail.fr";
};
};
chiasson.home = {
@@ -139,6 +185,13 @@
browsers.edge.enable = true;
browsers.flow.enable = false;
browsers.orion.enable = true;
browsers.zen.enable = true;
browsers.chromiumHevc = {
enable = true;
packages = [ "google-chrome" ];
vaapi.gpu = "intel"; # Chromium + NVIDIA VA-API → frame pool errors in Jellyfin cuz chrome is proprietary rats nests, gecko engine might support NVIDIA VA-API
};
editors.cursor.enable = true;
editors.obsidian.enable = true;
+14
View File
@@ -0,0 +1,14 @@
{ self, inputs, ... }: {
flake.nixosConfigurations."14900k" = inputs.nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
specialArgs = {
inherit self inputs;
host = "14900k";
system = "x86_64-linux";
};
modules = [
self.nixosModules."14900kConfiguration"
];
};
}
+27
View File
@@ -0,0 +1,27 @@
{ self, ... }: {
#TODO[epic=Moderate] Move this somewhere else, would prefer not relying on this module
flake.nixosModules."client-services" = { ... }: {
imports = [
self.nixosModules.systemBluetooth
self.nixosModules.systemDeployBuilder
];
chiasson.system.deploy.builder.enable = true;
# Lab-ish SSH defaults on clients — tighten for anything exposed.
services.openssh = {
enable = true;
settings = {
KbdInteractiveAuthentication = false;
PasswordAuthentication = true;
PermitRootLogin = "yes"; # consider tightening later
UseDns = false;
};
};
# Printing, polkit, udisks.
services.printing.enable = true;
security.polkit.enable = true;
services.udisks2.enable = true;
};
}
@@ -0,0 +1,139 @@
# Cameras on the Lenovo Duet 3 (`lenovo-wormdingler`) — TODO
The front and rear cameras do **not** work on the current Mobile NixOS image.
This file is a starting point for picking the work back up later, so we don't
have to re-diagnose from scratch.
## Current state (May 2026)
Empirical, taken on the running device:
```
$ uname -a
Linux ideapad 6.5.0 #1-mobile-nixos SMP Tue Jan 1 00:00:00 UTC 1980 aarch64 GNU/Linux
$ ls /dev/video*
/dev/video0 ← Qualcomm Venus video decoder (h.264/h.265 dec)
/dev/video1 ← Qualcomm Venus video encoder (h.264/h.265 enc)
$ ls /dev/media* # nothing — no media controller graph at all
$ lsmod | grep -i camss # nothing
$ wpctl status | grep -A1 Sources # nothing — PipeWire has no camera sources
```
Relevant kernel config bits in `/proc/config.gz` on the running image:
```
CONFIG_MEDIA_CAMERA_SUPPORT=y ← media framework is on
# CONFIG_VIDEO_QCOM_CAMSS is not set ← Qualcomm Camera Subsystem driver is OFF
# CONFIG_VIDEO_OV5675 is not set ← every relevant sensor driver is OFF
# CONFIG_VIDEO_OV13858 is not set
# CONFIG_VIDEO_HI556 is not set
# (full grep of `VIDEO_OV*` / `VIDEO_HI*` is all `not set`)
```
So the *media framework* is enabled, but the *ISP driver* (CAMSS) and every
plausible sensor driver are off, and no out-of-tree modules are shipped. The
two `/dev/video*` nodes are the Venus codecs, not cameras — that's why
`snapshot` reports "no camera found."
## Why this is non-trivial
1. **Kernel rebuild required.** `mobile-nixos` builds its own kernel for this
device (see `mobile.kernel.structuredConfig` in `hardware.nix`, where we
already enable `CIFS` and `EXFAT_FS`). Adding camera support means adding:
- `VIDEO_QCOM_CAMSS = module;`
- The right sensor driver(s) — and we don't currently know which ones the
Duet 3 actually uses. The `wormdingler` Chromium-OS DT references a pair
of OV-series sensors but the exact part numbers can vary by SKU and
production batch.
- Their dependencies (`I2C`, `V4L2`, `MEDIA_CONTROLLER`, etc. — most are
already pulled in by `MEDIA_CAMERA_SUPPORT`).
2. **Device-tree wiring.** Even with the drivers compiled in, the device tree
has to describe the CCI bus, the sensor I²C addresses, the regulators, the
reset/enable GPIOs, and the CSI port mapping. Mobile NixOS' DT for
wormdingler may or may not include these nodes — needs verification by
reading the upstream device file at
`${inputs.mobile-nixos}/devices/lenovo-wormdingler/`.
3. **libcamera is the user-space side.** CAMSS is not a "single v4l2 device"
driver — it exposes a media-controller graph that has to be configured by
libcamera (per-sensor IPA, format negotiation, etc.). Apps then talk to
libcamera via the `libcamerasrc` PipeWire module or directly. So the
user-space stack is:
- `pkgs.libcamera` (system-wide)
- `pkgs.pipewire` already running, but needs the libcamera module enabled
(`services.pipewire.libcamera = …` or equivalent — check current option
name)
- GUI: `snapshot`, `gnome-camera`, or anything that talks to PipeWire
video sources.
## Investigation checklist
When picking this up again, do these in order:
1. **Identify the actual sensors.** The cleanest way:
- Read the upstream Mobile NixOS device file for wormdingler
(`devices/lenovo-wormdingler/default.nix` and any `.dts` overlays).
- Cross-check with the Chromium OS overlay at
<https://chromium.googlesource.com/chromiumos/overlays/board-overlays/+/refs/heads/main/overlay-trogdor/>
and the upstream Linux DTS at
`arch/arm64/boot/dts/qcom/sc7180-trogdor-wormdingler-*.dts`.
- As a runtime cross-check, when CAMSS is eventually loaded, `dmesg | grep -i
-E 'cci|sensor|isp'` will print the I²C probe attempts.
2. **Enable kernel options** in `modules/hosts/ideapad/hardware.nix` under
`mobile.kernel.structuredConfig`:
```nix
(helpers: with helpers; {
VIDEO_QCOM_CAMSS = module;
# plus whatever sensors step 1 identified, e.g.:
# VIDEO_OV5675 = module;
# VIDEO_OV13858 = module;
})
```
Cross-build on the 14900k via the existing flow (binfmt aarch64 + push back).
Reboot, then check:
```
ls /dev/media* # expect at least /dev/media0
sudo dmesg | grep -i -E 'camss|sensor|isp|cci' # probe history
sudo modprobe -v qcom-camss # if not auto-loaded
```
3. **Install diagnostic tools** for this round of work (do **not** keep these
in the long-term config unless cameras actually work):
```nix
environment.systemPackages = with pkgs; [
v4l-utils # provides v4l2-ctl, media-ctl
libcamera # provides `cam`, `qcam`
];
```
Then:
```
v4l2-ctl --list-devices
media-ctl -p # dumps the full media-controller graph
cam -l # libcamera's view of available cameras
```
4. **Wire libcamera into PipeWire.** Once `cam -l` shows at least one camera,
enable PipeWire's libcamera module (option name may have shifted; current
nixpkgs typically has `services.pipewire.wireplumber.extraConfig` or
similar). Then `wpctl status` should show new Sources under "Video", and
`snapshot` will see them.
5. **Re-add a camera GUI** to `configuration.nix`. `snapshot` is the simplest
touch-first option; `gnome-camera` and `cheese` are alternatives.
## Why nothing else is being touched right now
Steps 14 above are speculative — there's no guarantee the Duet 3 cameras have
working mainline-Linux sensor drivers at all. The conservative move is to
leave the config tablet-usable without them, document the dead end, and revisit
when there's time for a real spike.
@@ -0,0 +1,48 @@
{ pkgs, ... }: {
# ─────────────────────── Power & thermal ───────────────────────
# Snapdragon 7c (sc7180) on a tablet form factor: aim for battery life. `schedutil` is the
# right modern cpufreq governor on ARM (responsive + power-aware); use it instead of
# `powersave` to avoid pinning the CPU at minimum frequency under interactive load.
powerManagement.cpuFreqGovernor = "schedutil";
powerManagement.enable = true;
# ─────────────────────── logind: lid & power button ───────────────────────
# 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";
HandleLidSwitchDocked = "ignore";
HandlePowerKey = "suspend";
HandlePowerKeyLongPress = "poweroff";
};
# ─────────────────────── Idle / suspend tuning ───────────────────────
# Allow suspend-to-RAM but disable hibernate (ARM swap-resume is unreliable, and we don't
# have a swap device by default anyway).
systemd.sleep.settings.Sleep = {
AllowHibernation = "no";
AllowHybridSleep = "no";
AllowSuspendThenHibernate = "no";
};
# upower picks the right battery percentages for low/critical out of the box; just make
# sure the action on critical is hibernate-then-poweroff fallback (we disabled hibernate
# so it'll go straight to poweroff). DMS reads upower for the bar widget.
services.upower = {
enable = true;
criticalPowerAction = "PowerOff";
percentageLow = 15;
percentageCritical = 7;
percentageAction = 3;
};
# ─────────────────────── Bluetooth audio quality of life ───────────────────────
# Duet has limited mic/speaker hw — keep wpa_supplicant power-save off so audio doesn't crackle
# over Bluetooth when CPU is idle. (Wi-Fi + BT share the chip; aggressive power-save = stutter.)
networking.networkmanager.wifi.powersave = false;
}
@@ -0,0 +1,338 @@
# Host-only: ideapad tablet ergonomics — touchscreen calibration, IIO sensors, virtual keyboard,
# 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.<wm>.enable`
# so they stay dormant if you pick the other session at the greeter.
#
# Hyprland uses CW transforms via `hyprctl`; Niri uses CCW transforms via `niri msg output`.
{
config,
lib,
pkgs,
...
}:
let
# ─────────────────────── Hyprland helpers ───────────────────────
ideapadTabletModeDaemon = pkgs.writeShellScriptBin "ideapad-tablet-mode-daemon" ''
#!/usr/bin/env bash
set -euo pipefail
HYPRCTL="${pkgs.hyprland}/bin/hyprctl"
JQ="${pkgs.jq}/bin/jq"
PKILL="${pkgs.procps}/bin/pkill"
PGREP="${pkgs.procps}/bin/pgrep"
WVKBD="${pkgs.wvkbd}/bin/wvkbd-mobintl"
monitor_name="DSI-1"
keyboard_scale="1.25"
tablet_scale="1.6"
state_file="''${XDG_RUNTIME_DIR:-/run/user/$(${pkgs.coreutils}/bin/id -u)}/ideapad-input-mode.state"
current_transform() {
"$HYPRCTL" -j monitors 2>/dev/null | "$JQ" -r --arg mon "$monitor_name" '
([.[] | select(.name == $mon)][0].transform // 1 | tostring)
' 2>/dev/null || echo "1"
}
has_attached_pogo_dock() {
"$HYPRCTL" -j devices 2>/dev/null | "$JQ" -e '
any((([.keyboards[]?.name] + [.mice[]?.name])[]?); (ascii_downcase | test("google-inc\\.-hammer")))
' >/dev/null
}
# wvkbd is no longer auto-spawned at session start this daemon owns its lifecycle so we
# only have a virtual keyboard surface in memory when the pogo cover is detached.
start_wvkbd() {
"$PGREP" -x wvkbd-mobintl >/dev/null 2>&1 && return 0
"$WVKBD" --non-exclusive -H 520 -L 360 --fn 'DejaVu Sans 18' >/dev/null 2>&1 &
}
stop_wvkbd() {
"$PKILL" -x wvkbd-mobintl >/dev/null 2>&1 || true
}
apply_mode() {
mode="$1"
transform="$(current_transform)"
if [ "$mode" = "tablet" ]; then
"$HYPRCTL" keyword monitor "$monitor_name,1200x2000@60.0,0x0,$tablet_scale, transform, $transform" >/dev/null 2>&1 || true
start_wvkbd
else
"$HYPRCTL" keyword monitor "$monitor_name,1200x2000@60.0,0x0,$keyboard_scale, transform, $transform" >/dev/null 2>&1 || true
stop_wvkbd
fi
"$HYPRCTL" keyword input:touchdevice:output "$monitor_name" >/dev/null 2>&1 || true
"$HYPRCTL" keyword input:touchdevice:transform "$transform" >/dev/null 2>&1 || true
printf "%s\n" "$mode" > "$state_file"
}
# Always reapply on startup the cached state file may lie (e.g., session restart while
# pogo state changed) and wvkbd needs to be (re)spawned since nothing else launches it now.
previous_mode=""
while true; do
if has_attached_pogo_dock; then
mode="keyboard"
else
mode="tablet"
fi
if [ "$mode" != "$previous_mode" ]; then
apply_mode "$mode"
previous_mode="$mode"
fi
sleep 2
done
'';
ideapadAutoRotateDaemon = pkgs.writeShellScriptBin "ideapad-autorotate-daemon" ''
#!/usr/bin/env bash
set -euo pipefail
HYPRCTL="${pkgs.hyprland}/bin/hyprctl"
MONITOR_SENSOR="${pkgs.iio-sensor-proxy}/bin/monitor-sensor"
monitor_name="DSI-1"
keyboard_scale="1.25"
tablet_scale="1.6"
state_file="''${XDG_RUNTIME_DIR:-/run/user/$(${pkgs.coreutils}/bin/id -u)}/ideapad-input-mode.state"
scale_for_mode() {
mode="$(cat "$state_file" 2>/dev/null || echo keyboard)"
if [ "$mode" = "tablet" ]; then
printf "%s\n" "$tablet_scale"
else
printf "%s\n" "$keyboard_scale"
fi
}
transform_for_orientation() {
orientation="$1"
case "$orientation" in
normal) printf "0\n" ;;
bottom-up) printf "2\n" ;;
left-up) printf "1\n" ;;
right-up) printf "3\n" ;;
*) return 1 ;;
esac
}
apply_orientation() {
orientation="$1"
transform="$(transform_for_orientation "$orientation")" || return 0
scale="$(scale_for_mode)"
"$HYPRCTL" keyword monitor "$monitor_name,1200x2000@60.0,0x0,$scale, transform, $transform" >/dev/null 2>&1 || true
"$HYPRCTL" keyword input:touchdevice:output "$monitor_name" >/dev/null 2>&1 || true
"$HYPRCTL" keyword input:touchdevice:transform "$transform" >/dev/null 2>&1 || true
}
while true; do
"$MONITOR_SENSOR" --accel 2>/dev/null | while IFS= read -r line; do
case "$line" in
*normal*) apply_orientation "normal" ;;
*bottom-up*) apply_orientation "bottom-up" ;;
*left-up*) apply_orientation "left-up" ;;
*right-up*) apply_orientation "right-up" ;;
esac
done
sleep 2
done
'';
# ─────────────────────── Niri helpers ───────────────────────
# `niri msg output DSI-1 transform <T>` accepts: normal | 90 | 180 | 270.
#
# Empirical mapping for this exact device + ACCEL_MOUNT_MATRIX (lenovo-wormdingler family):
# physical position iio orientation correct niri transform
# ────────────────── ──────────────── ──────────────────────
# keyboard down (LS) left-up normal
# keyboard up (LS) right-up 180
# keyboard right (P) bottom-up 90
# keyboard left (P) normal 270
# iio's "normal" is *not* the natural landscape pose here — the panel-mount matrix in the
# mobile-nixos device file biases it toward portrait-with-keyboard-left. Don't trust the
# textbook iio→Wayland convention; trust the table above.
ideapadNiriTabletModeDaemon = pkgs.writeShellScriptBin "ideapad-niri-tablet-mode-daemon" ''
#!/usr/bin/env bash
set -euo pipefail
NIRI="${pkgs.niri}/bin/niri"
JQ="${pkgs.jq}/bin/jq"
PKILL="${pkgs.procps}/bin/pkill"
PGREP="${pkgs.procps}/bin/pgrep"
WVKBD="${pkgs.wvkbd}/bin/wvkbd-mobintl"
output_name="DSI-1"
keyboard_scale="1.25"
tablet_scale="1.6"
state_file="''${XDG_RUNTIME_DIR:-/run/user/$(${pkgs.coreutils}/bin/id -u)}/ideapad-input-mode.state"
has_attached_pogo_dock() {
# Niri doesn't expose input devices via IPC, so we look in `/dev/input/by-id` for the
# USB-class enumeration of the Google Hammer / Whiskers pogo keyboard. The directory
# contains symlinks; presence of either string means the cover is attached.
# NOTE: `find` ships in findutils, not coreutils using the wrong package here makes
# the pipeline fail silently (stderr is dropped) and pogo always reads as detached.
${pkgs.findutils}/bin/find /dev/input/by-id -maxdepth 1 -type l 2>/dev/null \
| ${pkgs.gnugrep}/bin/grep -i -E 'google.*hammer|google.*whiskers' >/dev/null
}
# wvkbd is owned by this daemon: spawn on pogo detach, kill on pogo attach. The DMS bar
# `pkill -SIGRTMIN -x wvkbd-mobintl` toggle still works while wvkbd is running (i.e., in
# tablet mode). When the pogo cover is on, the pill is a no-op that's intentional, the
# physical keyboard is the input.
start_wvkbd() {
"$PGREP" -x wvkbd-mobintl >/dev/null 2>&1 && return 0
"$WVKBD" --non-exclusive -H 520 -L 360 --fn 'DejaVu Sans 18' >/dev/null 2>&1 &
}
stop_wvkbd() {
"$PKILL" -x wvkbd-mobintl >/dev/null 2>&1 || true
}
apply_mode() {
mode="$1"
if [ "$mode" = "tablet" ]; then
"$NIRI" msg output "$output_name" scale "$tablet_scale" >/dev/null 2>&1 || true
start_wvkbd
else
"$NIRI" msg output "$output_name" scale "$keyboard_scale" >/dev/null 2>&1 || true
stop_wvkbd
fi
printf "%s\n" "$mode" > "$state_file"
}
# Always reapply on startup see Hyprland daemon's identical comment.
previous_mode=""
while true; do
if has_attached_pogo_dock; then
mode="keyboard"
else
mode="tablet"
fi
if [ "$mode" != "$previous_mode" ]; then
apply_mode "$mode"
previous_mode="$mode"
fi
sleep 2
done
'';
ideapadNiriAutoRotateDaemon = pkgs.writeShellScriptBin "ideapad-niri-autorotate-daemon" ''
#!/usr/bin/env bash
set -euo pipefail
NIRI="${pkgs.niri}/bin/niri"
MONITOR_SENSOR="${pkgs.iio-sensor-proxy}/bin/monitor-sensor"
output_name="DSI-1"
transform_for_orientation() {
orientation="$1"
case "$orientation" in
normal) printf "270\n" ;;
bottom-up) printf "90\n" ;;
left-up) printf "normal\n" ;;
right-up) printf "180\n" ;;
*) return 1 ;;
esac
}
apply_orientation() {
orientation="$1"
transform="$(transform_for_orientation "$orientation")" || return 0
"$NIRI" msg output "$output_name" transform "$transform" >/dev/null 2>&1 || true
}
while true; do
"$MONITOR_SENSOR" --accel 2>/dev/null | while IFS= read -r line; do
case "$line" in
*normal*) apply_orientation "normal" ;;
*bottom-up*) apply_orientation "bottom-up" ;;
*left-up*) apply_orientation "left-up" ;;
*right-up*) apply_orientation "right-up" ;;
esac
done
sleep 2
done
'';
in
{
# ─────────────────────── Hardware ───────────────────────
hardware.sensor.iio.enable = true;
# 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 = [
pkgs.wvkbd
pkgs.iio-sensor-proxy
ideapadTabletModeDaemon
ideapadAutoRotateDaemon
ideapadNiriTabletModeDaemon
ideapadNiriAutoRotateDaemon
];
# ─────────────────────── Niri session autostart ───────────────────────
# Set on the NixOS layer; the `desktopNiri` HM module merges this into per-user `niri/config.kdl`.
# Touch input is glued to DSI-1 so it follows whatever transform the autorotate daemon sets.
# wvkbd is intentionally NOT spawned here — `ideapad-niri-tablet-mode-daemon` starts/kills it
# in response to pogo-cover attach/detach so we don't keep a virtual-keyboard surface alive
# while a physical keyboard is plugged 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" ]
[ "ideapad-niri-tablet-mode-daemon" ]
];
};
# ─────────────────────── Hyprland session autostart ───────────────────────
# Same lifecycle policy as Niri: the tablet-mode daemon owns wvkbd, no exec-once for it.
chiasson.desktop.hyprland.settings = lib.mkIf config.chiasson.desktop.hyprland.enable {
exec-once = lib.mkAfter [
"${pkgs.procps}/bin/pkill -x ideapad-tablet-mode-daemon >/dev/null 2>&1 || true; ideapad-tablet-mode-daemon &"
"${pkgs.procps}/bin/pkill -x ideapad-autorotate-daemon >/dev/null 2>&1 || true; ideapad-autorotate-daemon &"
];
bind = lib.mkAfter [
"Super, K, exec, pkill -SIGRTMIN -x wvkbd-mobintl"
];
monitor = lib.mkAfter [
"DSI-1,1200x2000@60.0,0x0,1.25, transform, 1"
];
};
}
+193
View File
@@ -0,0 +1,193 @@
{ self, inputs, ... }: {
# Lenovo Chromebook Duet 3 (`lenovo-wormdingler`) on Mobile NixOS.
# 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,
config,
lib,
pkgs,
...
}:
{
imports = [
# Mobile NixOS device + family + depthcharge system-type.
(import "${inputs.mobile-nixos}/lib/configuration.nix" {
device = "lenovo-wormdingler";
})
self.nixosModules.ideapadHardware
inputs.home-manager.nixosModules.home-manager
inputs.sops-nix.nixosModules.sops
self.nixosModules.system
self.nixosModules.desktop
self.nixosModules.users
self.nixosModules."client-services"
# Host-only: IIO + touchscreen calibration + per-compositor tablet/autorotate helpers.
./_private/touch-tablet.nix
# Host-only: cpufreq, lid/power-button policy, upower thresholds.
./_private/platform.nix
];
# ─────────────────────── Sops ───────────────────────
# `host_ideapad` recipient in `.sops.yaml` derives from the new ed25519 host key (post-reflash).
sops = {
defaultSopsFile = ../../../secrets/secrets.yaml;
defaultSopsFormat = "yaml";
age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
};
sops.secrets."users/olivier/hashedPassword".neededForUsers = true;
sops.secrets."caching/attic/token" = {
owner = "olivier";
group = "users";
mode = "0400";
};
sops.secrets."swiftshare/API_KEY" = {
owner = "olivier";
group = "users";
mode = "0400";
};
# ─────────────────────── Mobile NixOS / firmware ───────────────────────
# mruby's test-suite breaks on aarch64 in the Nix sandbox; the overlay strips checks and
# rebuilds Mobile NixOS' script-loader against the patched mruby.
chiasson.system.ideapadMrubyOverlay.enable = true;
# Wi-Fi modem (qcom-wcn3990) + Bluetooth (QCA crnv32) need binary blobs.
nixpkgs.config.allowUnfreePredicate =
pkg: builtins.elem (lib.getName pkg) [
"chromeos-sc7180-unredistributable-firmware"
"chromeos-sc7180-unredistributable-firmware-zstd"
];
hardware.firmware = [ pkgs.chromeos-sc7180-unredistributable-firmware ];
hardware.enableRedistributableFirmware = true;
# ─────────────────────── Attic (substitution + push + CLI token) ───────
chiasson.system.caching.attic = {
enable = true;
cacheName = "nixos-new";
endpoint = "http://192.168.2.238:8080/";
publicKey = "nixos-new:8NySIcT0HP7KvGQKgBRWoWESxxRA8BVYo8S85UNpNX0=";
tokenFile = config.sops.secrets."caching/attic/token".path;
push.enable = true;
userCli.enable = true;
};
# ─────────────────────── System bits ───────────────────────
chiasson.system = {
audio.enable = true;
networking = {
hostName = "ideapad";
networkManager = {
enable = true;
unmanaged = [ ];
};
wifi.tools.enabled = true;
};
extraPackages = with pkgs; [
gitMinimal
sops
ssh-to-age
];
};
# ─────────────────────── Desktop ───────────────────────
# 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 = false;
defaultSession = "niri";
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.
rebuildCommand = [
"bash"
"-lc"
''
ssh -t olivier@nixdesk \
"nixos-rebuild switch --flake path:/home/olivier/NixOS-V2#ideapad --target-host olivier@ideapad --sudo --ask-sudo-password 2>&1"
''
];
};
};
# ─────────────────────── Users / HM ───────────────────────
chiasson.users.enabled = [ "olivier" ];
# Touch-friendly application set, mirroring uConsole's selection (no heavy IDEs / gaming).
chiasson.users.extraModules.olivier = [
self.homeManagerModules.wisdomFilebrowsersDolphin
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.wisdomDesktopScreenshot
{
chiasson.home = {
shell = {
fish.enable = true;
ohMyPosh.enable = true;
};
terminals.kitty.enable = true;
filebrowsers.dolphin.enable = true;
browsers.zen.enable = true;
editors.kate.enable = true;
editors.cursor.enable = true;
apps.spotify.enable = true;
apps.localsend.enable = true;
desktop = {
screenshot = {
enable = true;
swiftshareApiKeyFile = "/run/secrets/swiftshare/API_KEY"; #TODO[epic=sops] redo this by passing sops file output directly
};
};
};
}
# Tablet-class apps: kept inline rather than promoting to wisdom modules — these aren't
# part of the broader catalog (no use on uConsole / 14900k / servers) and adding a wisdom
# module per single-host package would just be ceremony. If a second tablet host ever
# appears, factor them out then.
#
# NOTE on cameras: no v4l2/libcamera GUI is installed. The Mobile NixOS kernel for
# `lenovo-wormdingler` ships with `CONFIG_VIDEO_QCOM_CAMSS` disabled and no
# `VIDEO_OV*`/`VIDEO_HI*` sensor drivers, so `/dev/video0`-`/dev/video1` only expose
# the Qualcomm Venus codecs (h.264/h.265 enc/dec) and there is no camera source for
# PipeWire / libcamera to pick up. See `_private/CAMERA-TODO.md` for the steps that
# would (potentially) bring the front/rear cameras online — it's a kernel-rebuild +
# device-tree + libcamera project, not a config tweak.
(
{ pkgs, ... }:
{
home.packages = with pkgs; [
# PDF viewer — fits the existing KDE app set (Dolphin + Kate).
kdePackages.okular
# ePub reader, GTK4, large touch targets.
foliate
];
}
)
];
system.stateVersion = "26.05";
};
}
+14
View File
@@ -0,0 +1,14 @@
{ self, inputs, ... }: {
flake.nixosConfigurations.ideapad = inputs.nixpkgs.lib.nixosSystem {
system = "aarch64-linux";
specialArgs = {
inherit self inputs;
host = "ideapad";
system = "aarch64-linux";
};
modules = [
self.nixosModules.ideapadConfiguration
];
};
}
+22
View File
@@ -0,0 +1,22 @@
{ ... }: {
flake.nixosModules.ideapadHardware =
# Mobile NixOS' depthcharge system-type wires up the disk image, kernel partitions and
# rootfs entirely; we don't need a generated `hardware-configuration.nix`. This module is a
# placeholder so the host follows the standard `<host>Configuration` / `<host>Hardware` shape
# and gives us a place to drop kernel-config knobs that aren't covered by the device family.
{ ... }:
{
# Useful on a portable: mounting USB sticks (often exFAT) and SMB shares.
boot.supportedFilesystems = [ "exfat" "cifs" ];
# Mobile NixOS builds its own kernel — the regular `boot.kernelModules` won't help if the
# module isn't compiled in. `mobile.kernel.structuredConfig` lives upstream in
# `modules/system-types/depthcharge/kernel/` and is the right layer to add features.
mobile.kernel.structuredConfig = [
(helpers: with helpers; {
CIFS = module;
EXFAT_FS = module;
})
];
};
}
@@ -0,0 +1,18 @@
# personal-website sops secrets
Add these keys to `secrets.yaml` (on a host with your age key):
```yaml
personal-website:
ghcr-token: <same PAT as swiftshare/ghcr-token, or new>
database-password: <strong password>
auth-secret: <openssl rand -base64 32>
oauth-discord-client-secret: <Discord OAuth secret>
```
```bash
cd modules/hosts/nix-server
sops secrets.yaml
```
After editing, verify with `sops -d secrets.yaml | yq '.personal-website'`.
@@ -0,0 +1,65 @@
{ config, lib, ... }:
{
sops = {
templates."atticd.env" = {
owner = "root";
group = "root";
mode = "0400";
content = ''
ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64=${config.sops.placeholder."attic/server-token-rs256-secret-base64"}
'';
};
};
sops.secrets."attic/server-token-rs256-secret-base64" = {
sopsFile = ../../../../secrets/attic-secrets.yaml;
owner = "root";
group = "root";
mode = "0400";
};
# SQLite on disk was the main source of random multi-minute stalls (see attic#113).
# NAR blobs stay in /var/lib/atticd/storage; only metadata moves to Postgres.
services.postgresql = {
enable = true;
ensureDatabases = [ "atticd" ];
ensureUsers = [
{
name = "atticd";
ensureDBOwnership = true;
}
];
};
services.atticd = {
enable = true;
environmentFile = config.sops.templates."atticd.env".path;
settings = {
listen = "0.0.0.0:8080";
jwt = { };
# Use a libpq socket URI format accepted by Attic's parser.
database.url = "postgresql:///atticd?host=/run/postgresql&user=atticd";
chunking = {
nar-size-threshold = 65536;
min-size = 16384;
avg-size = 65536;
max-size = 262144;
};
storage = {
type = "local";
path = "/var/lib/atticd/storage";
};
};
};
systemd.services.atticd = {
serviceConfig = {
Restart = lib.mkForce "always";
RestartSec = lib.mkForce 5;
# Large closures; default limits can wedge uploads under load.
LimitNOFILE = 1048576;
};
};
chiasson.system.networking.firewall.allowedTCPPorts = [ 8080 ];
}
@@ -0,0 +1,26 @@
# Cloudflare dynamic DNS via NixOS (kissgyorgy/cloudflare-dyndns).
{ config, ... }:
let
secretFilePath = ../secrets.yaml;
in
{
sops.secrets."cloudflare-ddns/api-token".sopsFile = secretFilePath;
services.cloudflare-dyndns = {
enable = true;
apiTokenFile = config.sops.secrets."cloudflare-ddns/api-token".path;
domains = [
"chiasson.cloud"
"chiassoncloud.services"
"swiftshare.cloud"
"blackfry.day"
"yestur.day"
"rp-own.life"
"xn--1iu.cc"
];
proxied = true;
ipv4 = true;
ipv6 = false;
# Default: *:0/5 (every 5 minutes).
};
}
@@ -0,0 +1,12 @@
# DDRM Flask backend (Widevine / PlayReady decrypt). Extension URL: http://<host>:58239
{ pkgs, inputs, ... }:
{
services.ddrm-media-server = {
enable = true;
port = 58239;
listenAddress = "0.0.0.0";
openFirewall = true;
package = inputs.ddrm.packages.${pkgs.stdenv.hostPlatform.system}.default;
# State: /var/lib/ddrm-media (venv + configs + CDMs on first run — needs network for pip).
};
}
@@ -0,0 +1,40 @@
# 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://<host>: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" = {
# Dispatcharr runs as a non-root user inside the container; keep /data writable.
# (Plugins like EPG Janitor write to `/data/*.json`.)
mode = "0777";
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 ];
}
@@ -0,0 +1,11 @@
{ ... }:
{
# FlareSolverr (Cloudflare / JS challenge solver for some indexers).
# Typically used by Prowlarr as an HTTP proxy.
#
# UI/endpoint: http://<host>:8191
services.flaresolverr.enable = true;
networking.firewall.allowedTCPPorts = [ 8191 ];
}
@@ -0,0 +1,47 @@
{ lib, ... }:
{
services.gitea = {
enable = true;
# Migrated sqlite DB and repos; do not provision a fresh database.
database = {
type = "sqlite3";
createDatabase = false;
};
settings = {
server = {
DOMAIN = "git.chiasson.cloud";
HTTP_PORT = 3002;
ROOT_URL = "https://git.chiasson.cloud/";
# Clone URLs and LAN git@… -p 222 (was Docker host 222 → container 22).
# Port 222 is <1024 (privileged); systemd must grant CAP_NET_BIND_SERVICE below.
SSH_PORT = 222;
START_SSH_SERVER = true;
SSH_LISTEN_HOST = "0.0.0.0";
SSH_LISTEN_PORT = 222;
};
service.DISABLE_REGISTRATION = false;
};
};
# First boot after migration runs DB migrate + hook regen; default WatchdogSec=30 kills
# gitea while storage/actions init is still running. Type=notify also fails if startup
# is slow; PrivateUsers breaks access to migrated files owned by the real gitea uid.
# Port 222 is privileged (<1024); Docker mapped host 222→container 22 as root.
systemd.services.gitea.serviceConfig = {
Type = lib.mkForce "simple";
PrivateUsers = lib.mkForce false;
NoNewPrivileges = lib.mkForce false;
AmbientCapabilities = "CAP_NET_BIND_SERVICE";
CapabilityBoundingSet = lib.mkForce [ "CAP_NET_BIND_SERVICE" ];
TimeoutStartSec = lib.mkForce "20min";
WatchdogSec = lib.mkForce 0;
};
networking.firewall.allowedTCPPorts = [
3002
222
];
}
@@ -0,0 +1,29 @@
{ config, ... }:
let
secretFilePath = ../secrets.yaml;
in
{
sops.secrets."immich/database-password".sopsFile = secretFilePath;
# Placeholders are expanded only inside template `content` (not in arbitrary Nix strings).
sops.templates."immich-db.env" = {
content = ''
POSTGRES_PASSWORD=${config.sops.placeholder."immich/database-password"}
DB_PASSWORD=${config.sops.placeholder."immich/database-password"}
'';
};
chiasson.system.services.immich = {
enable = true;
host = "0.0.0.0";
port = 2283;
timezone = "America/Moncton";
uploadLocation = "/var/lib/immich/library";
environmentFiles = [ config.sops.templates."immich-db.env".path ];
postgres = {
user = "postgres";
#password = ""; # Defined in sops.templates."immich-db.env"
database = "immich";
};
};
}
@@ -0,0 +1,61 @@
# 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 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.
{ lib, ... }:
{
nixpkgs.overlays = [
(final: prev: {
jellyfin-web = prev.jellyfin-web.overrideAttrs (oldAttrs: {
postInstall =
(oldAttrs.postInstall or "")
+ ''
# Blank default Jellyfin banner assets (read-only store otherwise). Wildcards
# track hashed filenames across jellyfin-web releases; bump if layout changes.
find "$out" -type f \( -name 'banner-light.*.png' -o -name 'banner-dark.*.png' \) \
-exec truncate -s 0 {} \;
'';
});
})
];
users.groups.media = { };
users.users.jellyfin.extraGroups = [ "media" ];
users.users.server.extraGroups = [ "media" ];
systemd.tmpfiles.settings."nix-server-var-lib-media" = {
"/var/lib/media"."d" = {
mode = "0775";
user = "root";
group = "media";
};
"/var/lib/media/movies"."d" = {
mode = "0775";
user = "root";
group = "media";
};
"/var/lib/media/tv"."d" = {
mode = "0775";
user = "root";
group = "media";
};
};
services.jellyfin = {
enable = true;
openFirewall = true;
};
# `users.users.jellyfin.extraGroups` does not affect systemd; the service must list
# supplementary groups explicitly. Without `media`, directories mode 775 root:media are
# not writable by uid jellyfin (it only had group `jellyfin`), so deletes fail.
systemd.services.jellyfin.serviceConfig = {
SupplementaryGroups = [ "media" ];
# Jellyfin libraries on NFS (e.g. /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;
};
}
@@ -0,0 +1,35 @@
# 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/deep/jellyfin";
fsType = "nfs";
options = nfsClientOpts;
};
}
@@ -0,0 +1,53 @@
# Organizr — homelab dashboard (Docker). UI: http://<host>:8888
# Official image: https://github.com/organizr/docker-organizr
#
# Wizard errors like "API … /default/ not writable" are almost always host permissions on
# `/var/lib/organizr`: the first container run may leave root-owned files under `/config`.
{ lib, pkgs, ... }:
{
users.groups.organizr = { gid = 950; };
users.users.organizr = {
isSystemUser = true;
uid = 950;
group = "organizr";
};
systemd.tmpfiles.settings."nix-server-organizr-config" = {
"/var/lib/organizr"."d" = {
mode = "0755";
user = "organizr";
group = "organizr";
};
};
# Recursively reset ownership (handles root-owned files from an earlier container run).
systemd.tmpfiles.settings."nix-server-organizr-config-perms" = {
"/var/lib/organizr"."Z" = {
mode = "0755";
user = "organizr";
group = "organizr";
};
};
systemd.services.docker-organizr.preStart = lib.mkBefore ''
${pkgs.coreutils}/bin/mkdir -p /var/lib/organizr
${pkgs.coreutils}/bin/chown -R organizr:organizr /var/lib/organizr
'';
virtualisation.oci-containers.containers.organizr = {
image = "ghcr.io/organizr/organizr:latest";
ports = [ "8888:80" ];
volumes = [
"/var/lib/organizr:/config"
];
environment = {
PUID = "950";
PGID = "950";
TZ = "America/Moncton";
# v2-master / master are stable v2; optional override:
# branch = "v2-master";
};
};
networking.firewall.allowedTCPPorts = [ 8888 ];
}
@@ -0,0 +1,56 @@
{ config, ... }:
let
secretFilePath = ../secrets.yaml;
in
{
sops.secrets."personal-website/database-password".sopsFile = secretFilePath;
sops.secrets."personal-website/auth-secret".sopsFile = secretFilePath;
sops.secrets."personal-website/oauth-discord-client-secret".sopsFile = secretFilePath;
sops.templates."personal-website-postgres.env" = {
content = ''
POSTGRES_PASSWORD=${config.sops.placeholder."personal-website/database-password"}
POSTGRES_USER=chiassoncloud
POSTGRES_DB=chiassoncloud
'';
};
sops.templates."personal-website.env" = {
content = ''
DATABASE_URL=postgresql://chiassoncloud:${config.sops.placeholder."personal-website/database-password"}@personal-website-db:5432/chiassoncloud
AUTH_SECRET=${config.sops.placeholder."personal-website/auth-secret"}
AUTH_DISCORD_SECRET=${config.sops.placeholder."personal-website/oauth-discord-client-secret"}
'';
};
services.personalWebsite = {
enable = true;
app = {
image = "ghcr.io/olivierchiasson/personal-website:main";
ghcr = {
username = "olivierchiasson";
passwordFile = config.sops.secrets."swiftshare/ghcr-token".path;
};
port = 3001;
authUrl = "https://chiasson.cloud";
publicUrl = "https://chiasson.cloud";
disableTelemetry = true;
environmentFiles = [ config.sops.templates."personal-website.env".path ];
};
database = {
user = "chiassoncloud";
name = "chiassoncloud";
environmentFiles = [ config.sops.templates."personal-website-postgres.env".path ];
};
auth.discord.clientId = "1400660345068191855";
umami = {
websiteId = "3b2f29d3-11b8-4a3b-bc76-bda3f27926d1";
scriptUrl = "https://analytics.chiasson.cloud/script.js";
};
};
}
@@ -0,0 +1,20 @@
{config, ...}: {
virtualisation = {
docker.enable = true;
oci-containers = {
backend = "docker";
containers = {
portainer = {
image = "portainer/portainer-ce:latest";
ports = [ "9443:9443" ];
volumes = [
"/var/run/docker.sock:/var/run/docker.sock"
"/var/lib/portainer:/data"
];
};
};
};
};
networking.firewall.allowedTCPPorts = [ 9443 ];
}
@@ -0,0 +1,22 @@
{ lib, ... }:
{
# Prowlarr (indexer manager). UI: http://<host>:9696
# Data dir is /var/lib/prowlarr (see systemd unit ExecStart -data=…), not ~/.config/Prowlarr.
services.prowlarr.enable = true;
# Useful when Prowlarr/Sonarr/Radarr need to write into shared areas (downloads, etc.).
users.groups.prowlarr = { };
users.users.prowlarr = {
isSystemUser = true;
group = "prowlarr";
extraGroups = [ "media" ];
};
systemd.services.prowlarr.preStart = lib.mkBefore ''
mkdir -p /var/lib/prowlarr/Definitions/Custom
ln -sf ${./prowlarr/torrent9-custom.yml} /var/lib/prowlarr/Definitions/Custom/torrent9-custom.yml
'';
networking.firewall.allowedTCPPorts = [ 9696 ];
}
@@ -0,0 +1,193 @@
---
id: torrent9-custom
name: Torrent9 (Custom URL)
description: "Torrent9 is a FRENCH Public site for MOVIES / TV / GENERAL"
language: fr-FR
type: public
encoding: UTF-8
followredirect: true
testlinktorrent: false
links:
- https://www.torrent9.club/
- https://www6.torrent9.to/
legacylinks:
- https://www.torrent9.pl/ # this is a proxy for torrent9clone
- https://torrent9.black-mirror.xyz/ # this is a proxy for torrent9clone
- https://torrent9.unblocked.casa/ # this is a proxy for torrent9clone
- https://torrent9.proxyportal.fun/ # this is a proxy for torrent9clone
- https://torrent9.uk-unblock.xyz/ # this is a proxy for torrent9clone
- https://torrent9.ind-unblock.xyz/ # this is a proxy for torrent9clone
- https://ww1.torrent9.to/
- https://www.torrent9.is/
- https://torrent9.li/ # not a proxy for torrent9 or torrent9clone
- https://www.oxtorrent.me/
- https://www.torrent9.gg/
- https://www.torrent9.fi/ # this is the torrent9clone domain
- https://www.torrent9.fm/
- https://torrent9.se/ # redirect to www.
- https://torrent9.ninjaproxy1.com/ # no response data
- https://torrent9.proxyninja.org/ # Error 1007
- https://www.torrent9.se/
- https://torrent9.unblockninja.com/ # 403 forbidden
- https://ww1.torrent9.fm/
- https://www.torrent9.zone/ # clone? details links are broken
- https://torrent9.to/
- https://ww2.torrent9.to/
- https://www5.torrent9.to/
caps:
# dont forget to update the search fields category case block
categorymappings:
- {id: films, cat: Movies, desc: "Movies"}
- {id: series, cat: TV, desc: "TV"}
- {id: musique, cat: Audio, desc: "Music"}
- {id: ebook, cat: Books, desc: "Books"}
- {id: logiciels, cat: PC, desc: "Software"}
- {id: jeux-pc, cat: PC/Games, desc: "PC Games"}
- {id: other, cat: Other, desc: "Other"} # dummy cat for results missing icon
modes:
search: [q]
tv-search: [q, season, ep]
movie-search: [q]
music-search: [q]
book-search: [q]
allowrawsearch: true
settings:
- name: info_flaresolverr
type: info_flaresolverr
- name: multilang
type: checkbox
label: Replace MULTi by another language in release name
default: false
- name: multilanguage
type: select
label: Replace MULTi by this language
default: FRENCH
options:
FRENCH: FRENCH
MULTi FRENCH: MULTi FRENCH
ENGLISH: ENGLISH
MULTi ENGLISH: MULTi ENGLISH
VOSTFR: VOSTFR
MULTi VOSTFR: MULTi VOSTFR
- name: vostfr
type: checkbox
label: Replace VOSTFR and SUBFRENCH with ENGLISH
default: false
- name: sort
type: select
label: Sort requested from site (Only works for searches with Keywords)
default: ".html"
options:
".html": best
".html,trie-date-d": created desc
".html,trie-date-a": created asc
".html,trie-seeds-d": seeders desc
".html,trie-seeds-a": seeders asc
".html,trie-poid-d": size desc
".html,trie-poid-a": size asc
".html,trie-nom-d": title desc
".html,trie-nom-a": title asc
download:
selectors:
- selector: a[href^="magnet:?"]
attribute: href
- selector: script:contains("magnet:?")
filters:
- name: regexp
args: "\\s'(magnet:\\?.+?)';"
search:
paths:
- path: "{{ if .Keywords }}search_torrent/{{ .Keywords }}{{ .Config.sort }}{{ else }}home/{{ end }}"
keywordsfilters:
# if searching for season packs with S01 to saison 1 #9712
- name: re_replace
args: ["(?i)(S0)(\\d{1,2})$", "saison $2"]
- name: re_replace
args: ["(?i)(S)(\\d{1,3})$", "saison $2"]
- name: replace
args: [" ", "-"]
headers:
# site blocks all Linux User-Agents, so use a slightly altered Windows Jackett UA here (e.g. Safari/537.36 > Safari/537.35)
User-Agent: ["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.35"]
rows:
selector: table.table-striped > tbody > tr
filters:
- name: andmatch
fields:
category_optional:
selector: td:nth-child(1) i
optional: true
case:
i[class="fa fa-video-camera"]: films
i[class="fa fa-tv"]: series # search by name
i[class="fa fa-desktop"]: series # keywordless search
i[class="fa fa-music"]: musique
i[class="fa fa-gamepad"]: jeux-pc
i[class="fa fa-laptop"]: logiciels
i[class="fa fa-book"]: ebook
category:
text: "{{ if .Result.category_optional }}{{ .Result.category_optional }}{{ else }}other{{ end }}"
title_default:
selector: td:nth-child(1) a
title_optional:
selector: td:nth-child(1) a[title]
attribute: title
optional: true
title_phase1:
text: "{{ if .Result.title_optional }}{{ .Result.title_optional }}{{ else }}{{ .Result.title_default }}{{ end }}"
filters:
- name: re_replace
args: ["(?i)\\b(FRENCH|MULTI|TRUEFRENCH|VOSTFR|SUBFRENCH)\\b(.+?)(\\b((19|20)\\d{2})\\b)$", "$3 $1$2"]
title_vostfr:
text: "{{ .Result.title_phase1 }}"
filters:
- name: re_replace
args: ["(?i)\\b(vostfr|subfrench)\\b", "ENGLISH"]
title_phase2:
text: "{{ if .Config.vostfr }}{{ .Result.title_vostfr }}{{ else }}{{ .Result.title_phase1 }}{{ end }}"
title_multilang:
text: "{{ .Result.title_phase2 }}"
filters:
- name: re_replace
args: ["(?i)\\b(MULTI(?!.*(?:FRENCH|ENGLISH|VOSTFR)))\\b", "{{ .Config.multilanguage }}"]
title:
text: "{{ if .Config.multilang }}{{ .Result.title_multilang }}{{ else }}{{ .Result.title_phase2 }}{{ end }}"
details:
selector: td:nth-child(1) a
attribute: href
download:
selector: td:nth-child(1) a
attribute: href
date:
selector: td:nth-child(2):contains("/")
optional: true
default: now
filters:
- name: dateparse
args: "dd/MM/yyyy"
size:
selector: "{{ if .Keywords }}td:nth-child(3){{ else }}td:nth-child(2){{ end }}"
filters:
- name: re_replace
args: ["(\\w)o", "$1B"]
seeders:
selector: "{{ if .Keywords }}td:nth-child(4){{ else }}td:nth-child(3){{ end }}"
optional: true
default: 0
leechers:
selector: "{{ if .Keywords }}td:nth-child(5){{ else }}td:nth-child(4){{ end }}"
optional: true
default: 0
downloadvolumefactor:
text: 0
uploadvolumefactor:
text: 1
# engine n/a
@@ -0,0 +1,36 @@
{ config, lib, ... }:
let
webPort = 8081;
btPort = 51413;
downloadsDir = "/var/lib/downloads";
in
{
# qBittorrent (headless). Web UI: http://<host>:8081
services.qbittorrent = {
enable = true;
openFirewall = true;
webuiPort = webPort;
# Prefer a stable port for NAT/firewall and for easier debugging.
torrentingPort = btPort;
};
users.groups.qbittorrent = { };
users.users.qbittorrent = {
isSystemUser = true;
group = "qbittorrent";
extraGroups = [ "media" ];
};
systemd.tmpfiles.settings."nix-server-downloads-dir" = {
"${downloadsDir}"."d" = {
mode = "2775";
user = "root";
group = "media";
};
};
# Some NixOS versions don't open UDP for torrenting even when openFirewall=true.
networking.firewall.allowedTCPPorts = [ webPort btPort ];
networking.firewall.allowedUDPPorts = [ btPort ];
}
@@ -0,0 +1,16 @@
{ ... }:
{
# Radarr (movie automation). UI: http://<host>:7878
services.radarr.enable = true;
# Keep permissions aligned with Jellyfin (/var/lib/media via group `media`).
users.groups.radarr = { };
users.users.radarr = {
isSystemUser = true;
group = "radarr";
extraGroups = [ "media" ];
};
networking.firewall.allowedTCPPorts = [ 7878 ];
}
@@ -0,0 +1,29 @@
{ ... }:
{
# Blank default Seerr branding assets (read-only store otherwise).
nixpkgs.overlays = [
(final: prev: {
seerr = prev.seerr.overrideAttrs (oldAttrs: {
postInstall =
(oldAttrs.postInstall or "")
+ ''
find "$out/share/public" -maxdepth 1 -type f \( -name 'logo_full.svg' -o -name 'logo_stacked.svg' \) \
-exec truncate -s 0 {} \;
'';
});
})
];
# "Seerr" request management. For Jellyfin, Jellyseerr is the right choice.
# UI: http://<host>:5055
services.seerr.enable = true;
users.groups.jellyseerr = { };
users.users.jellyseerr = {
isSystemUser = true;
group = "jellyseerr";
};
networking.firewall.allowedTCPPorts = [ 5055 ];
}
@@ -0,0 +1,16 @@
{ ... }:
{
# Sonarr (TV automation). UI: http://<host>:8989
services.sonarr.enable = true;
# Ensure Sonarr can manage the same libraries as Jellyfin.
users.groups.sonarr = { };
users.users.sonarr = {
isSystemUser = true;
group = "sonarr";
extraGroups = [ "media" ];
};
networking.firewall.allowedTCPPorts = [ 8989 ];
}
@@ -0,0 +1,113 @@
{ config, ... }:
let
secretFilePath = ../secrets.yaml;
in
{
sops.secrets."swiftshare/ghcr-token".sopsFile = secretFilePath;
sops.secrets."swiftshare/database-password".sopsFile = secretFilePath;
sops.secrets."swiftshare/oauth-discord-client-secret".sopsFile = secretFilePath;
sops.secrets."swiftshare/oauth-github-client-secret".sopsFile = secretFilePath;
sops.secrets."swiftshare/auth-secret".sopsFile = secretFilePath;
sops.secrets."swiftshare/oauth-google-client-id".sopsFile = secretFilePath;
sops.secrets."swiftshare/oauth-google-client-secret".sopsFile = secretFilePath;
sops.secrets."swiftshare/smtp-pass".sopsFile = secretFilePath;
sops.secrets."swiftshare/minio-access-key".sopsFile = secretFilePath;
sops.secrets."swiftshare/minio-secret-key".sopsFile = secretFilePath;
# Docker `--env-file` expects `KEY=value`. Separate snippets for DB/MinIO so only `swiftshare.env` hits the app container.
sops.templates."swiftshare-postgres.env" = {
content = ''
POSTGRES_PASSWORD=${config.sops.placeholder."swiftshare/database-password"}
'';
};
sops.templates."swiftshare-minio.env" = {
content = ''
MINIO_ROOT_USER=${config.sops.placeholder."swiftshare/minio-access-key"}
MINIO_ROOT_PASSWORD=${config.sops.placeholder."swiftshare/minio-secret-key"}
'';
};
sops.templates."swiftshare.env" = {
content = ''
DATABASE_URL=postgresql://swiftshare:${config.sops.placeholder."swiftshare/database-password"}@swiftshare-db:5432/swiftshare
AUTH_SECRET=${config.sops.placeholder."swiftshare/auth-secret"}
AUTH_DISCORD_SECRET=${config.sops.placeholder."swiftshare/oauth-discord-client-secret"}
AUTH_GITHUB_SECRET=${config.sops.placeholder."swiftshare/oauth-github-client-secret"}
AUTH_GOOGLE_SECRET=${config.sops.placeholder."swiftshare/oauth-google-client-secret"}
AUTH_GOOGLE_ID=${config.sops.placeholder."swiftshare/oauth-google-client-id"}
SMTP_PASS=${config.sops.placeholder."swiftshare/smtp-pass"}
STORAGE_ACCESS_KEY=${config.sops.placeholder."swiftshare/minio-access-key"}
STORAGE_SECRET_KEY=${config.sops.placeholder."swiftshare/minio-secret-key"}
'';
};
services.swiftshare = {
enable = true;
app = {
image = "ghcr.io/olivierchiasson/swiftshare:main";
ghcr = {
username = "olivierchiasson";
passwordFile = config.sops.secrets."swiftshare/ghcr-token".path;
};
origin = "https://swiftshare.cloud";
port = 3000;
uploadBodySizeLimit = "100mb";
disableTelemetry = true;
environmentFiles = [ config.sops.templates."swiftshare.env".path ];
};
database = {
user = "swiftshare";
#password = ""; # Defined in sops.templates."swiftshare-postgres.env"
name = "swiftshare";
environmentFiles = [ config.sops.templates."swiftshare-postgres.env".path ];
#exposePort.enable = true;
};
auth = {
#secret = "";
discord = {
clientId = "1400660345068191855";
#clientSecret = ""; # Defined in sops.templates."swiftshare.env"
};
# GitHub OAuth App (https://github.com/settings/developers) — replace placeholders.
github = {
clientId = "Ov23lifcVKR6B1iYDicU";
#clientSecret = ""; # Defined in sops.templates."swiftshare.env"
};
# Google Cloud OAuth 2.0 client — replace placeholders.
#google = {
# clientId = ""; # Defined in sops.templates."swiftshare.env"
# clientSecret = ""; # Defined in sops.templates."swiftshare.env"
#};
# SMTP for Better Auth email verification / password reset.
smtp = {
host = "smtp.purelymail.com";
port = 465;
secure = true;
user = "noreply@swiftshare.cloud";
#pass = ""; # Defined in sops.templates."swiftshare.env"
from = "noreply@swiftshare.cloud";
};
};
minio = {
#accessKey = ""; # Defined in sops.templates."swiftshare-minio.env"
#secretKey = ""; # Defined in sops.templates."swiftshare-minio.env"
bucketName = "swiftshare-assets";
environmentFiles = [ config.sops.templates."swiftshare-minio.env".path ];
};
umami = {
websiteId = "b4e1240d-a9d8-4075-b64d-0d3e0329cac8";
scriptUrl = "https://analytics.chiasson.cloud/script.js";
};
};
}
@@ -9,6 +9,8 @@
}:
{
imports = [
inputs.ddrm.nixosModules.default
self.nixosModules.systemDeployBuilder
self.nixosModules.nix-serverHardware
inputs.sops-nix.nixosModules.sops
self.nixosModules.system
@@ -16,7 +18,11 @@
./_services/attic-cache-server.nix
./_services/portainer.nix
./_services/swiftshare.nix
./_services/personal-website.nix
./_services/immich.nix
./_services/ddrm-media-server.nix
./_services/gitea.nix
./_services/cloudflare-dyndns.nix
];
boot.loader.grub = {
@@ -68,6 +74,8 @@
extraPackages = with pkgs; [ btop ];
};
chiasson.system.deploy.builder.enable = true;
chiasson.users = {
enabled = [ "server" ];
hostOverrides.server = {
+13
View File
@@ -0,0 +1,13 @@
{ self, inputs, ... }: {
flake.nixosConfigurations.nix-server = inputs.nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
specialArgs = {
inherit self inputs;
host = "nix-server";
system = "x86_64-linux";
};
modules = [
self.nixosModules.nix-serverConfiguration
];
};
}
+16 -10
View File
@@ -1,5 +1,5 @@
swiftshare:
ghcr-token: ENC[AES256_GCM,data:wNzBA8Ib5WjxoKkGiWkfeGspKzy/vzbwwAp/+cjRF9Vsmlyx67OovQ==,iv:MCrkALYCHiPDb1tNQaWRrxuYSRXD6JtzJzEOr1aqhBk=,tag:okQfIP5IJUUIFfwAlZM1ow==,type:str]
ghcr-token: ENC[AES256_GCM,data:V/5dLVLv4BbAxdMiBxXgmNbK17HAQkqzHJA2NWzOFfFlcy3dq8SnZQ==,iv:YTB3Bef+kZXunXVUCkFj/YZo1POdx2K+bNvzarSJ1Iw=,tag:HEBT4ZKMXTIy+ZEkNx3rHw==,type:str]
database-password: ENC[AES256_GCM,data:r9GSaoQ7bS644ipb3kU=,iv:KYDTzYtjfz5meDb0nemY1lhSFEorKHL0hSRIcQaHg5c=,tag:RVjAfb8XGsybAgIc2/hH+g==,type:str]
auth-secret: ENC[AES256_GCM,data:tTXLMWASBfF49gBFrf+CZ3R4oTt7hEGUhAqEdvoQtm0zbb2VUhTq7y4tH/c=,iv:Halfu9hBex4SEUMHLAicqApTxZP0NV9pJZTr+bBSek4=,tag:1WqN75zT+zoka9sIXOJGfQ==,type:str]
oauth-discord-client-secret: ENC[AES256_GCM,data:a9Iarcpl1HOFXdsDMh3H662T8yqVvGtfguVICwWVrAg=,iv:LsUserWQcEDV0TiRWj1sHh5/ZiFQzyc1gRWg+Ewwjik=,tag:33Ml08oHVXl0ZMmiwQ2mig==,type:str]
@@ -11,10 +11,15 @@ swiftshare:
minio-secret-key: ENC[AES256_GCM,data:szkx+MTbMWmfbQ==,iv:+1zlHJRKMR4XDv1rrkOeilz06YA1W/1o+egylm/ZjPs=,tag:70QO3dPp9WRd71Puzl47QA==,type:str]
immich:
database-password: ENC[AES256_GCM,data:YWLt2pty/yVrrF7K,iv:uqrQGfST/A6LzRZ4+O0puXA1bd/7CL5A/T7jU+/++X8=,tag:/gNGK3z4RembX+tBET4M5g==,type:str]
personal-website:
database-password: ENC[AES256_GCM,data:PR6nNKOqB/SE956hXA4=,iv:/1usgEXfY+ef9bOAaCdjduqBqoonAm5saFBSjdGhm1Q=,tag:mDThIsYVUKyN6vQlh2YYbQ==,type:str]
auth-secret: ENC[AES256_GCM,data:NHY+0tOA6FRmvkKZ/KgpogwOf+DuF42aoxspPUdVSi/iY5dF2yY8hwHaehI=,iv:9Gv/1YDcU+rMuX3PrwwT97qdsywIn+/wWEk17evyloo=,tag:R6RZbRhyXkwfZ0p/fZ+YcA==,type:str]
oauth-discord-client-secret: ENC[AES256_GCM,data:YegPgoSRKNcDaID9LPWxHDz4T7VnhFfuWMyALfFhpg8=,iv:VSLWA1HG1+Y70tKnRoFulBZSKdoJTYmIDzCXIZeFYCc=,tag:yNR8rrm/7Mrj/RIVNLFfsg==,type:str]
cloudflare-ddns:
api-token: ENC[AES256_GCM,data:wFKbclETO0YQTcfNUdKyr6mxQODeiaYn3gLeC1mWeRda97rOvlum+Q==,iv:IuT4exNhh0z+9DbY3WNnVqEy4398DTm7aluhOv9XFss=,tag:GGPoSJLBScTmXyQ7Vab6EA==,type:str]
sops:
age:
- recipient: age1yyzgmazjxkvwtfcv9re3lqmt2ru5dcrfu3sauysm0wzfwzvyap8qkjkq32
enc: |
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlVWpFK2RRSHRxVFVSdEdI
S01BSEZQUTZCV0tvM1lpSFNYc3g3ek5QNjJrClNyVUtKYnRtWVRYRkE2SStWRVRR
@@ -22,8 +27,8 @@ sops:
ZUpnemRBSmlSZVpmRW0wNFhIK3BibVkKdD14ki8dJbYMjsBkC1Nm5TOM6M33eLJ6
IUrKDWeZXEVe2sMhBb31Zv+tinwtHSsvpxDIsjstpxtH+5wTyoQVdA==
-----END AGE ENCRYPTED FILE-----
- recipient: age1elk6zwmcylwfk7gd4pjda7g29upftjvxys8py42s8d42jklnyv7s7dm9z2
enc: |
recipient: age1yyzgmazjxkvwtfcv9re3lqmt2ru5dcrfu3sauysm0wzfwzvyap8qkjkq32
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1bFJDNDdsWGIzMDl3TmRr
MStsZVFRa1dIVmJGU3krWWlpc2FZMU9EREF3CkdDZFc0Y3ZIMVZxNHorWFRHaWwy
@@ -31,8 +36,8 @@ sops:
NUZIYnZIMDRWTXpwTURMc2tzelp3VjAKHHBkHhz+t03W0ojsOBB2i3K4ZMUXvrwF
4mjNqNBcAJ1uHgJP7qvpNjxEW1LcsdQKmXavoqizX+XfLaA3zEwB0Q==
-----END AGE ENCRYPTED FILE-----
- recipient: age1p05z980kdtngk9mw67hfev72h7xhslplpxfk9yskgmf0hl4lu3ls04zht9
enc: |
recipient: age1elk6zwmcylwfk7gd4pjda7g29upftjvxys8py42s8d42jklnyv7s7dm9z2
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwTWR0N3UwdTB0UDZxRmV5
R3dkYUhZaElMbkxxSllTNWkrb05VSkJrMUNRCjZTUTlvVTU2MHY5ZS9oU2pCSlFu
@@ -40,7 +45,8 @@ sops:
WWZwbkR4dTFjK2NZcW9pTTNHd252N3cKiz8l9AWciFOBU+wcT9T1WA4bToPYfq8G
Nf0uOoSWPTJ/2SRNkSu7FMumATH4ldQ6TFSwKda3mBfBwhnFzLq10Q==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-04-30T18:45:45Z"
mac: ENC[AES256_GCM,data:DD9NZcYQVSByaQvGAB7b/Wpk7SWBBsWtzAM9MkIHMmyxNomiPPUFQR6+18QDUCHQXk1xXMUi79bnTRz8SdoBXVjbHG8Qhy3n6D1sFeEgXC42pgem7hBPfmJlgcIPNYEguXPISLsp/Zx9ISEnH5Zul0v8/G2ACN7Y/U3jtaHx4U8=,iv:g1k16EhTR+t9jCpvhmiXYZV99aMk1DrS4frpl5q93lM=,tag:FigaXNw+IbpZ7E0a+ySb3g==,type:str]
recipient: age1p05z980kdtngk9mw67hfev72h7xhslplpxfk9yskgmf0hl4lu3ls04zht9
lastmodified: "2026-05-28T21:04:07Z"
mac: ENC[AES256_GCM,data:L53UYFh5xtuMx19GKAg3jW7U0/DlwJ2usy/pup+4t1HQN3KHxMwbc4BzLYkLnRBTwKMJdfKXiYmmYiYvfbbWzsPtfXLxPnF/5ROiCJ2NlxAe86SmRy2nI++eTHAXRgexIhYyL7SchsroGRvW2B3aL1jV+Eu11fD9trA9Ex1EfuI=,iv:XrRJCFSgwW2+N+4FnWrFFZz8UEVzhuhpRtHGtf8dyqc=,tag:LcO4+ilwKdU+JPyjyKaGNw==,type:str]
unencrypted_suffix: _unencrypted
version: 3.12.2
version: 3.13.1
@@ -0,0 +1,52 @@
# Media stack storage on r5500: btrfs subvolume @media-stack on the OS disk (sda4).
{ config, pkgs, lib, ... }:
let
btrfsUuid = "934a5ec3-4bab-49c3-96c9-c857c50076ba";
btrfsDevice = "/dev/disk/by-uuid/${btrfsUuid}";
# Created under subvol=@ → full path is @/@media-stack (not a top-level @media-stack).
mediaSubvol = "@/@media-stack";
in
{
# Create @media-stack before /mnt/media-stack is mounted.
systemd.services.r5500-media-stack-subvolume = {
description = "Create btrfs subvolume @media-stack on sda4 if missing";
before = [ "mnt-media\\x2dmedia\\x2dstack.mount" ];
wantedBy = [ "local-fs-pre.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
rootMount="/mnt/.r5500-btrfs-root"
mkdir -p "$rootMount"
if ! mountpoint -q "$rootMount"; then
mount -o subvol=@ "${btrfsDevice}" "$rootMount"
umountAfter=1
else
umountAfter=0
fi
if ! ${pkgs.btrfs-progs}/bin/btrfs subvolume show "$rootMount/@media-stack" >/dev/null 2>&1; then
${pkgs.btrfs-progs}/bin/btrfs subvolume create "$rootMount/@media-stack"
fi
if [ "$umountAfter" -eq 1 ]; then
umount "$rootMount"
fi
'';
};
# Optional 1 TiB cap (run once after first boot if desired):
# sudo btrfs quota enable /mnt/media-stack && sudo btrfs qgroup limit 1T /mnt/media-stack
fileSystems."/mnt/media-stack" = {
device = btrfsDevice;
fsType = "btrfs";
neededForBoot = false;
options = [
"subvol=${mediaSubvol}"
"compress=zstd"
"noatime"
"nofail"
"x-systemd.device-timeout=30"
];
};
}
@@ -0,0 +1,70 @@
# Shared media group, directory layout, and Jellyfin config bind-mount on /mnt/media-stack.
{ config, lib, pkgs, ... }:
let
paths = import ./media-stack-paths.nix;
prowlarrCustomIndexer = "${./../_services/prowlarr/torrent9-custom.yml}";
inherit (paths)
mediaRoot
downloadsDir
downloadsIncompleteDir
sonarrDataDir
radarrDataDir
prowlarrDataDir
qbittorrentDataDir
seerrConfigDir
dispatcharrDataDir
organizrDataDir
jellyfinConfigDir
jellyfinMoviesDir
jellyfinTvDir
;
in
{
_module.args.mediaStackPaths = paths;
users.groups.media = { };
users.users.server.extraGroups = [ "media" ];
# Layout dirs only after /mnt/media-stack is mounted (tmpfiles at early boot would
# otherwise create paths on the root fs and break bind mounts / boot).
systemd.services.r5500-media-stack-dirs = {
description = "Create media-stack directory layout";
after = [ "mnt-media\\x2dmedia\\x2dstack.mount" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
set -e
install -d -m 0775 -o root -g media ${mediaRoot}
install -d -m 2775 -o root -g media ${jellyfinMoviesDir} ${jellyfinTvDir} ${downloadsDir}
install -d -m 2775 -o root -g media ${downloadsIncompleteDir}
install -d -m 0755 -o jellyfin -g jellyfin ${jellyfinConfigDir}
install -d -m 0700 -o sonarr -g sonarr ${sonarrDataDir}
install -d -m 0700 -o radarr -g radarr ${radarrDataDir}
install -d -m 0700 -o prowlarr -g prowlarr ${prowlarrDataDir}
install -d -m 0750 -o qbittorrent -g qbittorrent ${qbittorrentDataDir}
install -d -m 0755 -o jellyseerr -g jellyseerr ${seerrConfigDir} 2>/dev/null \
|| install -d -m 0755 -o root -g root ${seerrConfigDir}
install -d -m 0777 -o root -g root ${dispatcharrDataDir}
install -d -m 0755 -o organizr -g organizr ${organizrDataDir} 2>/dev/null \
|| install -d -m 0755 -o root -g root ${organizrDataDir}
install -d -m 0700 -o prowlarr -g prowlarr ${prowlarrDataDir}/Definitions/Custom
ln -sfn ${prowlarrCustomIndexer} ${prowlarrDataDir}/Definitions/Custom/torrent9-custom.yml
'';
};
# Jellyfin metadata on the media subvolume; nofail so a missing subvol never bricks boot.
fileSystems."/var/lib/jellyfin" = {
device = jellyfinConfigDir;
fsType = "none";
neededForBoot = false;
options = [
"bind"
"nofail"
"x-systemd.after=mnt-media\\x2dmedia\\x2dstack.mount"
];
};
}
@@ -0,0 +1,16 @@
# Path constants for the r5500 media stack (imported by _services/* and media-paths.nix).
{
mediaRoot = "/mnt/media-stack";
downloadsDir = "/mnt/media-stack/downloads";
downloadsIncompleteDir = "/mnt/media-stack/downloads/incomplete";
sonarrDataDir = "/mnt/media-stack/sonarr";
radarrDataDir = "/mnt/media-stack/radarr";
prowlarrDataDir = "/mnt/media-stack/prowlarr";
qbittorrentDataDir = "/mnt/media-stack/qbittorrent";
seerrConfigDir = "/mnt/media-stack/seerr";
dispatcharrDataDir = "/mnt/media-stack/dispatcharr";
organizrDataDir = "/mnt/media-stack/organizr";
jellyfinConfigDir = "/mnt/media-stack/jellyfin/config";
jellyfinMoviesDir = "/mnt/media-stack/jellyfin/movies";
jellyfinTvDir = "/mnt/media-stack/jellyfin/tv";
}
@@ -0,0 +1,34 @@
{ lib, pkgs, mediaStackPaths, ... }:
let
dataDir = mediaStackPaths.dispatcharrDataDir;
in
{
systemd.tmpfiles.settings."r5500-dispatcharr-data" = {
"${dataDir}"."d" = {
mode = "0777";
user = "root";
group = "root";
};
};
systemd.services.docker-dispatcharr.preStart = lib.mkBefore ''
${pkgs.coreutils}/bin/mkdir -p ${dataDir}
'';
virtualisation.oci-containers.containers.dispatcharr = {
image = "ghcr.io/dispatcharr/dispatcharr:latest";
ports = [ "9191:9191" ];
volumes = [
"${dataDir}:/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 ];
}
@@ -0,0 +1,6 @@
# Docker backend for Dispatcharr and Organizr on r5500.
{ ... }:
{
virtualisation.docker.enable = true;
virtualisation.oci-containers.backend = "docker";
}
@@ -0,0 +1,5 @@
{ ... }:
{
services.flaresolverr.enable = true;
networking.firewall.allowedTCPPorts = [ 8191 ];
}
@@ -0,0 +1,29 @@
# Jellyfin on r5500. Local libraries: /mnt/media-stack/jellyfin/{movies,tv}.
# Bulk libraries: /mnt/nixdesk-jellyfin/{movies,tv} (NFS from 14900k).
{ lib, ... }:
{
nixpkgs.overlays = [
(final: prev: {
jellyfin-web = prev.jellyfin-web.overrideAttrs (oldAttrs: {
postInstall =
(oldAttrs.postInstall or "")
+ ''
find "$out" -type f \( -name 'banner-light.*.png' -o -name 'banner-dark.*.png' \) \
-exec truncate -s 0 {} \;
'';
});
})
];
users.users.jellyfin.extraGroups = [ "media" ];
services.jellyfin = {
enable = true;
openFirewall = true;
};
systemd.services.jellyfin.serviceConfig = {
SupplementaryGroups = [ "media" ];
PrivateUsers = lib.mkForce false;
};
}
@@ -0,0 +1,28 @@
# NFS mounts of nixdesk (14900k) bulk storage for r5500. Exports live in
# modules/hosts/14900k/_private/jellyfin-nfs-export.nix
#
# Jellyfin library paths:
# Movies → /mnt/nixdesk-jellyfin/movies
# Shows → /mnt/nixdesk-jellyfin/tv
{ ... }:
let
nfsExportHost = "192.168.2.25";
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/deep/jellyfin";
fsType = "nfs";
options = nfsClientOpts;
};
}
@@ -0,0 +1,48 @@
{ lib, pkgs, mediaStackPaths, ... }:
let
configDir = mediaStackPaths.organizrDataDir;
in
{
users.groups.organizr = { gid = 950; };
users.users.organizr = {
isSystemUser = true;
uid = 950;
group = "organizr";
};
systemd.tmpfiles.settings."r5500-organizr-config" = {
"${configDir}"."d" = {
mode = "0755";
user = "organizr";
group = "organizr";
};
};
systemd.tmpfiles.settings."r5500-organizr-config-perms" = {
"${configDir}"."Z" = {
mode = "0755";
user = "organizr";
group = "organizr";
};
};
systemd.services.docker-organizr.preStart = lib.mkBefore ''
${pkgs.coreutils}/bin/mkdir -p ${configDir}
${pkgs.coreutils}/bin/chown -R organizr:organizr ${configDir}
'';
virtualisation.oci-containers.containers.organizr = {
image = "ghcr.io/organizr/organizr:latest";
ports = [ "8888:80" ];
volumes = [
"${configDir}:/config"
];
environment = {
PUID = "950";
PGID = "950";
TZ = "America/Moncton";
};
};
networking.firewall.allowedTCPPorts = [ 8888 ];
}
@@ -0,0 +1,20 @@
{ lib, mediaStackPaths, ... }:
{
services.prowlarr = {
enable = true;
dataDir = mediaStackPaths.prowlarrDataDir;
};
users.groups.prowlarr = { };
users.users.prowlarr = {
isSystemUser = true;
group = "prowlarr";
extraGroups = [ "media" ];
};
systemd.services.prowlarr.serviceConfig.ReadWritePaths = lib.mkAfter [
mediaStackPaths.prowlarrDataDir
];
networking.firewall.allowedTCPPorts = [ 9696 ];
}
@@ -0,0 +1,193 @@
---
id: torrent9-custom
name: Torrent9 (Custom URL)
description: "Torrent9 is a FRENCH Public site for MOVIES / TV / GENERAL"
language: fr-FR
type: public
encoding: UTF-8
followredirect: true
testlinktorrent: false
links:
- https://www.torrent9.club/
- https://www6.torrent9.to/
legacylinks:
- https://www.torrent9.pl/ # this is a proxy for torrent9clone
- https://torrent9.black-mirror.xyz/ # this is a proxy for torrent9clone
- https://torrent9.unblocked.casa/ # this is a proxy for torrent9clone
- https://torrent9.proxyportal.fun/ # this is a proxy for torrent9clone
- https://torrent9.uk-unblock.xyz/ # this is a proxy for torrent9clone
- https://torrent9.ind-unblock.xyz/ # this is a proxy for torrent9clone
- https://ww1.torrent9.to/
- https://www.torrent9.is/
- https://torrent9.li/ # not a proxy for torrent9 or torrent9clone
- https://www.oxtorrent.me/
- https://www.torrent9.gg/
- https://www.torrent9.fi/ # this is the torrent9clone domain
- https://www.torrent9.fm/
- https://torrent9.se/ # redirect to www.
- https://torrent9.ninjaproxy1.com/ # no response data
- https://torrent9.proxyninja.org/ # Error 1007
- https://www.torrent9.se/
- https://torrent9.unblockninja.com/ # 403 forbidden
- https://ww1.torrent9.fm/
- https://www.torrent9.zone/ # clone? details links are broken
- https://torrent9.to/
- https://ww2.torrent9.to/
- https://www5.torrent9.to/
caps:
# dont forget to update the search fields category case block
categorymappings:
- {id: films, cat: Movies, desc: "Movies"}
- {id: series, cat: TV, desc: "TV"}
- {id: musique, cat: Audio, desc: "Music"}
- {id: ebook, cat: Books, desc: "Books"}
- {id: logiciels, cat: PC, desc: "Software"}
- {id: jeux-pc, cat: PC/Games, desc: "PC Games"}
- {id: other, cat: Other, desc: "Other"} # dummy cat for results missing icon
modes:
search: [q]
tv-search: [q, season, ep]
movie-search: [q]
music-search: [q]
book-search: [q]
allowrawsearch: true
settings:
- name: info_flaresolverr
type: info_flaresolverr
- name: multilang
type: checkbox
label: Replace MULTi by another language in release name
default: false
- name: multilanguage
type: select
label: Replace MULTi by this language
default: FRENCH
options:
FRENCH: FRENCH
MULTi FRENCH: MULTi FRENCH
ENGLISH: ENGLISH
MULTi ENGLISH: MULTi ENGLISH
VOSTFR: VOSTFR
MULTi VOSTFR: MULTi VOSTFR
- name: vostfr
type: checkbox
label: Replace VOSTFR and SUBFRENCH with ENGLISH
default: false
- name: sort
type: select
label: Sort requested from site (Only works for searches with Keywords)
default: ".html"
options:
".html": best
".html,trie-date-d": created desc
".html,trie-date-a": created asc
".html,trie-seeds-d": seeders desc
".html,trie-seeds-a": seeders asc
".html,trie-poid-d": size desc
".html,trie-poid-a": size asc
".html,trie-nom-d": title desc
".html,trie-nom-a": title asc
download:
selectors:
- selector: a[href^="magnet:?"]
attribute: href
- selector: script:contains("magnet:?")
filters:
- name: regexp
args: "\\s'(magnet:\\?.+?)';"
search:
paths:
- path: "{{ if .Keywords }}search_torrent/{{ .Keywords }}{{ .Config.sort }}{{ else }}home/{{ end }}"
keywordsfilters:
# if searching for season packs with S01 to saison 1 #9712
- name: re_replace
args: ["(?i)(S0)(\\d{1,2})$", "saison $2"]
- name: re_replace
args: ["(?i)(S)(\\d{1,3})$", "saison $2"]
- name: replace
args: [" ", "-"]
headers:
# site blocks all Linux User-Agents, so use a slightly altered Windows Jackett UA here (e.g. Safari/537.36 > Safari/537.35)
User-Agent: ["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.35"]
rows:
selector: table.table-striped > tbody > tr
filters:
- name: andmatch
fields:
category_optional:
selector: td:nth-child(1) i
optional: true
case:
i[class="fa fa-video-camera"]: films
i[class="fa fa-tv"]: series # search by name
i[class="fa fa-desktop"]: series # keywordless search
i[class="fa fa-music"]: musique
i[class="fa fa-gamepad"]: jeux-pc
i[class="fa fa-laptop"]: logiciels
i[class="fa fa-book"]: ebook
category:
text: "{{ if .Result.category_optional }}{{ .Result.category_optional }}{{ else }}other{{ end }}"
title_default:
selector: td:nth-child(1) a
title_optional:
selector: td:nth-child(1) a[title]
attribute: title
optional: true
title_phase1:
text: "{{ if .Result.title_optional }}{{ .Result.title_optional }}{{ else }}{{ .Result.title_default }}{{ end }}"
filters:
- name: re_replace
args: ["(?i)\\b(FRENCH|MULTI|TRUEFRENCH|VOSTFR|SUBFRENCH)\\b(.+?)(\\b((19|20)\\d{2})\\b)$", "$3 $1$2"]
title_vostfr:
text: "{{ .Result.title_phase1 }}"
filters:
- name: re_replace
args: ["(?i)\\b(vostfr|subfrench)\\b", "ENGLISH"]
title_phase2:
text: "{{ if .Config.vostfr }}{{ .Result.title_vostfr }}{{ else }}{{ .Result.title_phase1 }}{{ end }}"
title_multilang:
text: "{{ .Result.title_phase2 }}"
filters:
- name: re_replace
args: ["(?i)\\b(MULTI(?!.*(?:FRENCH|ENGLISH|VOSTFR)))\\b", "{{ .Config.multilanguage }}"]
title:
text: "{{ if .Config.multilang }}{{ .Result.title_multilang }}{{ else }}{{ .Result.title_phase2 }}{{ end }}"
details:
selector: td:nth-child(1) a
attribute: href
download:
selector: td:nth-child(1) a
attribute: href
date:
selector: td:nth-child(2):contains("/")
optional: true
default: now
filters:
- name: dateparse
args: "dd/MM/yyyy"
size:
selector: "{{ if .Keywords }}td:nth-child(3){{ else }}td:nth-child(2){{ end }}"
filters:
- name: re_replace
args: ["(\\w)o", "$1B"]
seeders:
selector: "{{ if .Keywords }}td:nth-child(4){{ else }}td:nth-child(3){{ end }}"
optional: true
default: 0
leechers:
selector: "{{ if .Keywords }}td:nth-child(5){{ else }}td:nth-child(4){{ end }}"
optional: true
default: 0
downloadvolumefactor:
text: 0
uploadvolumefactor:
text: 1
# engine n/a
@@ -0,0 +1,43 @@
{ lib, mediaStackPaths, ... }:
let
webPort = 8081;
btPort = 51413;
inherit (mediaStackPaths) downloadsDir downloadsIncompleteDir qbittorrentDataDir;
in
{
services.qbittorrent = {
enable = true;
openFirewall = true;
webuiPort = webPort;
torrentingPort = btPort;
};
users.groups.qbittorrent = { };
users.users.qbittorrent = {
isSystemUser = true;
group = "qbittorrent";
extraGroups = [ "media" ];
home = qbittorrentDataDir;
};
fileSystems."/var/lib/qbittorrent" = {
device = qbittorrentDataDir;
fsType = "none";
neededForBoot = false;
options = [
"bind"
"nofail"
"x-systemd.after=mnt-media\\x2dmedia\\x2dstack.mount"
];
};
networking.firewall.allowedTCPPorts = [ webPort btPort ];
networking.firewall.allowedUDPPorts = [ btPort ];
# Default save path for new torrents (existing qBittorrent.conf may override after migration).
systemd.services.qbittorrent.preStart = lib.mkAfter ''
mkdir -p ${downloadsDir} ${downloadsIncompleteDir}
chmod 2775 ${downloadsDir} ${downloadsIncompleteDir}
chgrp media ${downloadsDir} ${downloadsIncompleteDir}
'';
}
+20
View File
@@ -0,0 +1,20 @@
{ lib, mediaStackPaths, ... }:
{
services.radarr = {
enable = true;
dataDir = mediaStackPaths.radarrDataDir;
};
users.groups.radarr = { };
users.users.radarr = {
isSystemUser = true;
group = "radarr";
extraGroups = [ "media" ];
};
systemd.services.radarr.serviceConfig.ReadWritePaths = lib.mkAfter [
mediaStackPaths.radarrDataDir
];
networking.firewall.allowedTCPPorts = [ 7878 ];
}
+35
View File
@@ -0,0 +1,35 @@
{ lib, mediaStackPaths, ... }:
{
nixpkgs.overlays = [
(final: prev: {
seerr = prev.seerr.overrideAttrs (oldAttrs: {
postInstall =
(oldAttrs.postInstall or "")
+ ''
find "$out/share/public" -maxdepth 1 -type f \( -name 'logo_full.svg' -o -name 'logo_stacked.svg' \) \
-exec truncate -s 0 {} \;
'';
});
})
];
services.seerr = {
enable = true;
openFirewall = true;
configDir = mediaStackPaths.seerrConfigDir;
};
users.groups.jellyseerr = { };
users.users.jellyseerr = {
isSystemUser = true;
group = "jellyseerr";
};
systemd.services.seerr.serviceConfig = {
DynamicUser = lib.mkForce false;
User = "jellyseerr";
Group = "jellyseerr";
ReadWritePaths = [ mediaStackPaths.seerrConfigDir ];
};
}
+20
View File
@@ -0,0 +1,20 @@
{ lib, mediaStackPaths, ... }:
{
services.sonarr = {
enable = true;
dataDir = mediaStackPaths.sonarrDataDir;
};
users.groups.sonarr = { };
users.users.sonarr = {
isSystemUser = true;
group = "sonarr";
extraGroups = [ "media" ];
};
systemd.services.sonarr.serviceConfig.ReadWritePaths = lib.mkAfter [
mediaStackPaths.sonarrDataDir
];
networking.firewall.allowedTCPPorts = [ 8989 ];
}
+85
View File
@@ -0,0 +1,85 @@
{ self, inputs, ... }: {
flake.nixosModules.r5500Configuration =
{
self,
config,
lib,
pkgs,
...
}:
{
imports = [
self.nixosModules.systemDeployBuilder
self.nixosModules.r5500Hardware
inputs.sops-nix.nixosModules.sops
self.nixosModules.system
self.nixosModules.users
./_private/media-disk.nix
./_private/media-paths.nix
./_services/docker-media.nix
./_services/nixdesk-nfs-client.nix
./_services/jellyfin.nix
./_services/sonarr.nix
./_services/radarr.nix
./_services/prowlarr.nix
./_services/flaresolverr.nix
./_services/seerr.nix
./_services/qbittorrent.nix
./_services/dispatcharr.nix
./_services/organizr.nix
];
boot.loader.grub = {
enable = true;
efiSupport = false;
device = "/dev/sda";
};
services.openssh = {
enable = true;
settings = {
PasswordAuthentication = true;
KbdInteractiveAuthentication = false;
PermitRootLogin = "no";
UseDns = false;
};
};
sops = {
defaultSopsFile = ../../../secrets/secrets.yaml;
defaultSopsFormat = "yaml";
age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
};
sops.secrets."users/server/hashedPassword".neededForUsers = true;
security.sudo.wheelNeedsPassword = true;
nix.settings = {
experimental-features = [ "nix-command" "flakes" ];
trusted-users = [ "root" "@wheel" ];
allowed-users = [ "root" "@wheel" ];
};
chiasson.system = {
networking = {
hostName = "r5500";
networkManager.enable = true;
};
extraPackages = with pkgs; [ btop git ];
};
chiasson.system.deploy.builder.enable = true;
chiasson.users = {
enabled = [ "server" ];
hostOverrides.server = {
hashedPasswordFile = config.sops.secrets."users/server/hashedPassword".path;
};
};
services.xserver.enable = lib.mkDefault false;
system.stateVersion = "25.11";
};
}
+13
View File
@@ -0,0 +1,13 @@
{ self, inputs, ... }: {
flake.nixosConfigurations.r5500 = inputs.nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
specialArgs = {
inherit self inputs;
host = "r5500";
system = "x86_64-linux";
};
modules = [
self.nixosModules.r5500Configuration
];
};
}
+51
View File
@@ -0,0 +1,51 @@
{ ... }: {
flake.nixosModules.r5500Hardware =
{
config,
lib,
pkgs,
modulesPath,
...
}:
{
imports = [
(modulesPath + "/installer/scan/not-detected.nix")
];
boot.initrd.availableKernelModules = [
"uhci_hcd"
"ehci_pci"
"ahci"
"usb_storage"
"usbhid"
"sd_mod"
];
boot.initrd.kernelModules = [ ];
boot.kernelModules = [ "kvm-intel" ];
boot.extraModulePackages = [ ];
fileSystems."/" = {
device = "/dev/disk/by-uuid/934a5ec3-4bab-49c3-96c9-c857c50076ba";
fsType = "btrfs";
options = [ "subvol=@" ];
};
fileSystems."/home" = {
device = "/dev/disk/by-uuid/934a5ec3-4bab-49c3-96c9-c857c50076ba";
fsType = "btrfs";
options = [ "subvol=@home" ];
};
fileSystems."/boot" = {
device = "/dev/disk/by-uuid/6399d086-687b-4ca9-ad34-da1dd85203d5";
fsType = "ext4";
};
swapDevices = [
{ device = "/dev/disk/by-uuid/ddb9fea1-7c44-44bc-bc74-79a3adb6cc35"; }
];
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
};
}
+14
View File
@@ -0,0 +1,14 @@
{ pkgs, ... }: {
# Apple T2 machines often need additional Broadcom firmware not shipped in
# linux-firmware. Firmware is stored in this host directory.
hardware.firmware = [
(pkgs.stdenvNoCC.mkDerivation (final: {
name = "t2mbp-brcm-firmware";
src = ./firmware/brcm;
installPhase = ''
mkdir -p "$out/lib/firmware/brcm"
cp -v ${final.src}/* "$out/lib/firmware/brcm"
'';
}))
];
}
+30
View File
@@ -0,0 +1,30 @@
{ ... }: {
# Bootloader and EFI
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
boot.loader.efi.efiSysMountPoint = "/boot";
boot.loader.systemd-boot.configurationLimit = 5;
# Hibernate support (resume from swap partition).
boot.resumeDevice = "/dev/disk/by-uuid/403c9698-b501-4198-96cf-f27f82f1eb1a";
# Suspend is allowed again: kernel cmdline + out-of-tree apple-bce are in
# `t2linux/_private/boot-tuning.nix` and `t2linux/_private/kernel.nix`. If you need
# to block sleep while debugging: `systemd.sleep.settings.Sleep.AllowSuspend = "no";`
# Power and thermal management.
powerManagement.cpuFreqGovernor = "ondemand";
services.thermald.enable = true;
hardware.sensor.iio.enable = true;
# Pull in linux-firmware (and friends) from nixpkgs.
hardware.enableRedistributableFirmware = true;
hardware.enableAllFirmware = true;
# Enable flakes and the newer CLI.
nix.settings.experimental-features = [ "nix-command" "flakes" ];
system.stateVersion = "25.11";
}
+4 -6
View File
@@ -6,7 +6,7 @@
imports = [
self.nixosModules.t2mbpHardware
self.nixosModules.t2linux
inputs.t2fanrd.nixosModules.t2fanrd
inputs.t2fanrd.nixosModules.t2fanrd #TODO[epic=Moderate] Convert to flake parts module
inputs.home-manager.nixosModules.home-manager
inputs.sops-nix.nixosModules.sops
@@ -95,14 +95,12 @@
};
defaultSession = "niri";
shell = "dms";
shells.dms = {
enableRbwLockToggle = true;
};
};
chiasson.system = {
remoteDesktop = {
enable = false;
moonlight.enable = false;
sunshine.enable = false;
};
audio.enable = true;
extraPackages = [ pkgs.sops ];
networking = {
+14
View File
@@ -0,0 +1,14 @@
{ self, inputs, ... }: {
flake.nixosConfigurations.t2mbp = inputs.nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
specialArgs = {
inherit self inputs;
host = "t2mbp";
system = "x86_64-linux";
};
modules = [
self.nixosModules.t2mbpConfiguration
];
};
}
@@ -0,0 +1,27 @@
{ ... }: {
# T2: align with https://github.com/deqrocks/apple-bce#required-kernel-parameters (suspend stack).
# (Older t2linux guidance used s2idle + pcie_ports=compat; deqrocks fork expects deep + auto + pm_async=off.)
boot.kernelParams = [
"mem_sleep_default=deep"
"intel_iommu=on"
"iommu=pt"
"pcie_ports=auto"
"pm_async=off"
# https://wiki.t2linux.org/guides/hybrid-graphics/#enabling-the-igpu — helps some post-suspend black screens.
"i915.enable_guc=3"
];
# Hybrid T2 Macs: prefer Intel for display/GL (saves power; avoids broken AMDGPU after resume).
# https://wiki.t2linux.org/guides/hybrid-graphics/
boot.extraModprobeConfig = ''
options apple-gmux force_igd=y
'';
# If the AMD dGPU misbehaves after S3 (Electron apps wont start, suspend breaks), keep it unloaded.
# See https://github.com/deqrocks/apple-bce#notes-for-dgpu-models — drop this list if you need DRI_PRIME
# or external displays wired through the dGPU.
boot.blacklistedKernelModules = [ "amdgpu" ];
boot.kernelModules = [ "apple-bce" ];
boot.initrd.availableKernelModules = [ "apple-bce" ];
}
@@ -0,0 +1,66 @@
{ lib, pkgs, inputs, config, ... }:
let
# Two-stage callPackage matches nixos-hardware apple/t2 generic.nix.
linuxT2GenericFromDir =
{ lib, ... }@args:
{ kernel, t2linuxPatchesSrc }:
let
patchNames = lib.sort lib.lessThan (
lib.filter (n: builtins.match "^[0-9]{4}-.*\\.patch$" n != null) (
lib.attrNames (builtins.readDir t2linuxPatchesSrc)
)
);
t2Patches = map
(name: {
inherit name;
patch = "${t2linuxPatchesSrc}/${name}";
})
patchNames;
in
kernel.override (
args
// {
pname = "linux-t2";
structuredExtraConfig = with lib.kernel; {
# In-tree staging driver disabled; we ship deqrocks/apple-bce via boot.extraModulePackages.
APPLE_BCE = no;
APPLE_GMUX = module;
APFS_FS = module;
BRCMFMAC = module;
BT_BCM = module;
BT_HCIBCM4377 = module;
BT_HCIUART_BCM = yes;
BT_HCIUART = module;
HID_APPLETB_BL = module;
HID_APPLETB_KBD = module;
HID_APPLE = module;
HID_MAGICMOUSE = module;
DRM_APPLETBDRM = module;
HID_SENSOR_ALS = module;
SND_PCM = module;
STAGING = yes;
};
kernelPatches = t2Patches ++ (args.kernelPatches or [ ]);
argsOverride.extraMeta = {
description = "The Linux kernel (with patches from the T2 Linux project)";
maintainers = with lib.maintainers; [ soopyc ];
};
}
// (args.argsOverride or { })
);
# linux_6_19 was dropped from nixpkgs; t2linux/linux-t2-patches HEAD targets 7.x (see 76589a8).
linuxT2Kernel = (pkgs.callPackage linuxT2GenericFromDir { }) {
kernel = pkgs.linux_latest;
t2linuxPatchesSrc = inputs.t2linux-patches;
};
in
{
boot.kernelPackages = lib.mkForce (pkgs.linuxPackagesFor linuxT2Kernel);
# https://github.com/deqrocks/apple-bce (branch no-state-suspend) — BCE + suspend-oriented fixes.
# README also mentions companion userspace: t2-upower, t2-kbd-tb (not packaged here yet).
boot.extraModulePackages = [
(config.boot.kernelPackages.callPackage ./apple-bce.nix { })
];
}
+8
View File
@@ -0,0 +1,8 @@
{ ... }: {
flake.nixosModules.t2linux = {
imports = [
./_private/kernel.nix
./_private/boot-tuning.nix
];
};
}
@@ -0,0 +1,37 @@
{ lib, pkgs, ... }:
let
uconsole4gRuntime = with pkgs; [
bash
coreutils
gnugrep
gnused
gawk
util-linux
usbutils
pciutils
lsb-release
libgpiod
modemmanager
iproute2
iputils
busybox
socat
systemd
];
# Use a lightweight wrapper to avoid ShellCheck gating the build.
uconsole4gCm5 = pkgs.writeShellScriptBin "uconsole-4g-cm5" ''
export PATH=${lib.makeBinPath uconsole4gRuntime}:$PATH
exec ${pkgs.bash}/bin/bash ${./uconsole-4g-cm5.sh} "$@"
'';
in
{
networking.modemmanager.enable = true;
environment.systemPackages = with pkgs; [
libgpiod
uconsole4gCm5
socat
ripgrep
];
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,16 @@
{ pkgs, ... }: {
# Keep Raspberry Pi kernel cmdline in sync with current system profile.
system.activationScripts.updateRpiCmdline.text = ''
export PATH="${pkgs.gnused}/bin:''${PATH}"
for cmdline in /boot/firmware/nixos/*/cmdline.txt; do
[ -e "$cmdline" ] || continue
current_init="$(readlink -f /nix/var/nix/profiles/system)/init"
if grep -q 'init=/nix/store/' "$cmdline"; then
sed -i "s#init=/nix/store/[^ ]*/init#init=$current_init#" "$cmdline"
fi
if ! grep -q 'root=' "$cmdline"; then
sed -i 's/ init=/ root=PARTUUID=4d44c78a-ee3c-4e3e-9eee-0f2eb10347b6 rootfstype=ext4 init=/' "$cmdline"
fi
done
'';
}
@@ -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
];
}
@@ -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 ];
};
}
@@ -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.99:${toString config.services.cockpit.port}"
];
plugins = with pkgs; [
cockpit-files
cockpit-machines
cockpit-podman
cockpitFileSharing
];
};
}
@@ -0,0 +1,48 @@
{ ... }: {
#TODO[epic=Moderate] Clean this up, move to host's configuration.nix.
# Native Raspberry Pi boot flow.
boot.loader.raspberry-pi.bootloader = "kernel";
boot.loader.grub.enable = false;
boot.consoleLogLevel = 7;
boot.kernelModules = [ "nvme" ];
# Root device for kernel cmdline (NVMe boot).
boot.kernelParams = [
"root=PARTUUID=4d44c78a-ee3c-4e3e-9eee-0f2eb10347b6"
"rootfstype=ext4"
];
console = {
earlySetup = true;
font = "ter-v32n";
};
# Enable PCIe x1 for CM5 NVMe adapter.
hardware.raspberry-pi.config.cm5.base-dt-params.pciex1 = {
enable = true;
value = "on";
};
powerManagement.cpuFreqGovernor = "powersave";
# Root + firmware as boot-critical mounts.
fileSystems."/".neededForBoot = true;
fileSystems."/boot/firmware".neededForBoot = true;
# Pi generational boot expects boot-firmware.mount to exist.
systemd.mounts = [
{
what = "/dev/disk/by-label/FIRMWARE";
where = "/boot/firmware";
type = "vfat";
options = "noatime,fmask=0022,dmask=0022";
wantedBy = [ "local-fs.target" ];
after = [ "local-fs-pre.target" ];
}
];
# There is no serial console on uConsole CM5 by default.
systemd.services."serial-getty@ttyS0".enable = false;
system.stateVersion = "25.11";
}
@@ -0,0 +1,14 @@
# nixpkgs unstable dropped several Raspberry Pi attrs; nixos-raspberrypi modules still
# reference them but ship derivations under pkgs/raspberrypi/.
{ inputs, ... }: {
nixpkgs.overlays = [
(final: _prev:
let
rp = "${inputs.nixos-raspberrypi}/pkgs/raspberrypi";
in
{
raspberrypi-utils = final.callPackage "${rp}/raspberrypi-utils.nix" { };
raspberrypi-udev-rules = final.callPackage "${rp}/udev-rules.nix" { };
})
];
}
@@ -0,0 +1,6 @@
{ ... }: {
#TODO[epic=Moderate] Clean this up, move to host's configuration.nix.
# Enable uinput for gamepad remap.
hardware.uinput.enable = true;
programs.mosh.enable = true;
}
@@ -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";
}
+3
View File
@@ -29,7 +29,10 @@
self.nixosModules."client-services"
./_private/platform.nix
./_private/raspberrypi-utils-overlay.nix
./_private/wifi-brcmfmac.nix
./_private/services.nix
./_private/cockpit.nix
./_private/activation.nix
./_private/4g/default.nix
];
+19
View File
@@ -0,0 +1,19 @@
{ self, inputs, ... }: {
flake.nixosConfigurations.uConsole = inputs.nixos-raspberrypi.lib.nixosSystem {
system = "aarch64-linux";
specialArgs = inputs // {
inherit self;
inputs = inputs;
host = "uConsole";
system = "aarch64-linux";
};
trustCaches = false;
modules = [
inputs.nixos-raspberrypi.nixosModules.raspberry-pi-5.base
inputs.oom-hardware.nixosModules.uc.kernel
inputs.oom-hardware.nixosModules.uc.configtxt
inputs.oom-hardware.nixosModules.uc.base-cm5
self.nixosModules.uConsoleConfiguration
];
};
}
+33
View File
@@ -0,0 +1,33 @@
# Build a raw Navi hive attrset from host specs + deployment targets.
# Call `inputs.navi.lib.makeHive` on the result to produce `flake.naviHive`.
{ lib, inputs, ... }: {
flake.lib.mkNaviHiveConfig =
{
metaNixpkgs,
hostSpecs,
deployments,
}:
let
deployNodes = lib.filterAttrs (name: _: deployments ? ${name}) hostSpecs;
in
{
meta = {
nixpkgs = metaNixpkgs;
nodeNixpkgs = lib.mapAttrs (
name: spec:
import inputs.nixpkgs {
system = spec.system;
}
) deployNodes;
nodeSpecialArgs = lib.mapAttrs (_: spec: spec.specialArgs) deployNodes;
allowApplyAll = false;
};
}
// lib.mapAttrs (
name: spec:
{
imports = spec.modules or [ spec.configuration ];
deployment = deployments.${name};
}
) deployNodes;
}
+39
View File
@@ -0,0 +1,39 @@
# Pi5 + DSI DRM KDL snippets (`desktop.niri.raspberryPi5DrmWorkaround`) — lives in `flake.lib`.
{ ... }:
let
drmExtraConfig = ''
debug {
render-drm-device "/dev/dri/renderD128"
ignore-drm-device "/dev/dri/card1"
ignore-drm-device "/dev/dri/card2"
}
'';
in
{
flake.lib.pi5NiriKdl = {
inherit drmExtraConfig;
# Keep in sync with DMS greeter niri template when upstream edits it.
dankGreeterCompositorConfig = ''
hotkey-overlay {
skip-at-startup
}
environment {
DMS_RUN_GREETER "1"
}
${drmExtraConfig}
gestures {
hot-corners {
off
}
}
layout {
background-color "#000000"
}
'';
};
}

Some files were not shown because too many files have changed in this diff Show More