Compare commits
66 Commits
380b428d9a
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b8fa60540 | |||
| 86dc80eb3c | |||
| f1bd5146a3 | |||
| 6f35d60aab | |||
| 3d2e74d115 | |||
| 548a438f6a | |||
| 0939766e3a | |||
| f424ab98c7 | |||
| 15a4d8d5e8 | |||
| 5f7e9cada5 | |||
| c4a0eb0a98 | |||
| 103a485bf8 | |||
| 13f35677be | |||
| 579a120326 | |||
| c90d368432 | |||
| a787840ce2 | |||
| e2fd47aa58 | |||
| 5d70868e4f | |||
| db88a44435 | |||
| 524abb9ceb | |||
| 6291076494 | |||
| f3d358ce12 | |||
| 645e1f0f31 | |||
| e68321ac1e | |||
| 5357ba307b | |||
| 9960e874c4 | |||
| 2f28ec735d | |||
| 2cdfaa3ed7 | |||
| 32cb8955eb | |||
| d9b82bbf2b | |||
| 6f0c6b9c31 | |||
| bab8bdecb7 | |||
| 15fcf88da2 | |||
| b70f9f4822 | |||
| 87f839d16a | |||
| 25cf0167c1 | |||
| dc06c70c43 | |||
| fa62ba2b8c | |||
| 3b5c031619 | |||
| 1524100ff5 | |||
| 662d24a89c | |||
| 41ed709f4a | |||
| 3fdb859ff8 | |||
| 8b66fa8665 | |||
| 4ca225ed60 | |||
| 02c6b3a45f | |||
| de1f9fc770 | |||
| 596427e491 | |||
| 17337b9675 | |||
| 1a34db6f22 | |||
| 9b20b71b50 | |||
| 4034a6d81d | |||
| 026aea4ac4 | |||
| fe202a6843 | |||
| 55df00bafb | |||
| 65093a1f87 | |||
| f0fb1ba5f3 | |||
| ce79bcd03f | |||
| 202555c5f1 | |||
| 3dfaeb111a | |||
| 691b71a5cd | |||
| cea0050597 | |||
| 73401750a0 | |||
| 3ca626dffe | |||
| 2bdb173ac7 | |||
| d05212ffd3 |
@@ -17,6 +17,14 @@ creation_rules:
|
||||
- *host_ideapad
|
||||
- *host_nix-server
|
||||
- *host_r5500
|
||||
- path_regex: secrets/gitcrypt-personal-docs-key$
|
||||
key_groups:
|
||||
- age:
|
||||
- *primary
|
||||
- *host_14900k
|
||||
- *host_uConsole
|
||||
- *host_t2mbp
|
||||
- *host_ideapad
|
||||
# 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))$
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# chiasson-nix
|
||||
|
||||
My personal NixOS + Home Manager flake. [flake.nix](../flake.nix) feeds `./modules` through flake-parts and [import-tree](https://github.com/vic/import-tree) so config stays in small files instead of one monster module.
|
||||
|
||||
How to work on it: [conventions.md](./conventions.md).
|
||||
|
||||
## Hosts
|
||||
|
||||
| Host | What it is |
|
||||
|------|------------|
|
||||
| `t2mbp` | Intel MacBook with T2 chip — Niri/DMS, `t2linux` + fan daemon |
|
||||
| `14900k` | Main x86 desktop — same GUI stack, also cross-builds aarch64 and exports NFS for Jellyfin |
|
||||
| `ideapad` | Lenovo IdeaPad Duet 3 (Chromebook, `lenovo-wormdingler`) — Mobile NixOS on Snapdragon, Niri/DMS, touch/tablet bits in `_private/` |
|
||||
| `uConsole` | Clockwork Pi uConsole (Pi 5 CM) — Niri/DMS, built with `nixos-raspberrypi` + oom-hardware |
|
||||
| `nix-server` | Headless x86 VM — Attic, Gitea, Immich, SwiftShare, Personal-Website, DDRM, Portainer, … |
|
||||
| `r5500` | Headless media box — Jellyfin, *arr, qBittorrent, Dispatcharr; library on NFS from `nix-server` |
|
||||
|
||||
Each machine: `modules/hosts/<name>/default.nix` → `nixosConfigurations.<name>`, real config in `*Configuration` + optional `_private/` and `_services/`.
|
||||
|
||||
## Features at a glance
|
||||
|
||||
- **Users catalog** — one catalog in `modules/system/users.nix`, pick who exists on a host with `chiasson.users.enabled = [ "example" ]`, override per host. Home Manager wires itself from the same list.
|
||||
- **SSH + Bitwarden (rbw) keys** — one ed25519 keypair per catalog user, private key in Bitwarden, public key in `modules/ssh/inventory.nix`. Outbound SSH uses a `.pub` filter against the rbw agent; inbound `authorized_keys` are generated from the same inventory. [Full wiring in conventions.md](./conventions.md#ssh-model).
|
||||
- **Wisdom modules** — Home Manager slices in `modules/wisdom/` auto-catalog via `lib.wisdomCatalogExtraModules` (defined in `modules/desktop/options.nix`). The baseline `wisdom` module handles git identity + shared tools; all other `wisdom*` exports are pulled in once per user through `desktopHomeBase` → `chiasson.users.extraModules.olivier`. Hosts only flip `chiasson.home.<category>.<slice>.enable` — no manual imports in `home.nix`. Categories include `apps` (spotify, discord, localsend), `browsers` (chrome, edge, zen, …), `editors` (cursor, vscode, obsidian, …), `shells` (bash, fish, oh-my-posh, yazi), `desktop` (gtk-qt-theming, screenshot), and `terminals` (kitty). Trivial single-package slices can be written with the `lib.wisdomSlice` helper instead of repeating the `root`/`cfg` + `mkIf` boilerplate.
|
||||
- **Wallpapers from a dedicated repo** — `modules/desktop/wallpapers.nix` pins `inputs.wallpapers` into the store and exposes it as `CHIASSON_NIX_WALLPAPERS` and `/etc/wallpapers`. Override `chiasson.desktop.wallpapers.source` if needed.
|
||||
- **Private host data** — import-tree skips `_private/` globally, so machine-only files (firmware quirks, display configs, service tweaks) live next to the host module without leaking elsewhere.
|
||||
|
||||
## Deploy / rebuild
|
||||
|
||||
Remote builds use the `builder` user (`systemDeployBuilder`, wired through `client-services` on desktops/laptops/tablets).
|
||||
|
||||
Fleet deploy is [Navi](https://github.com/cafkafk/navi) — config in `modules/deploy/navi.nix`, outputs `flake.navi` / `flake.naviHive`.
|
||||
|
||||
```bash
|
||||
nix develop # devShell has navi + just + hints
|
||||
just # list local helpers like `sync-dms`
|
||||
navi apply --on <host>
|
||||
navi apply-local --node 14900k --sudo # this machine, if hostname matches
|
||||
navi tui # interactive fleet dashboard
|
||||
```
|
||||
|
||||
Plain rebuild still works: `sudo nixos-rebuild switch --flake .#<host>`.
|
||||
|
||||
## Repo layout
|
||||
|
||||
```
|
||||
modules/
|
||||
hosts/<host>/ # per-machine composition
|
||||
system/ # nixosModules.system aggregate
|
||||
desktop/ # GUI stack (niri, hyprland, plasma, DMS)
|
||||
wisdom/ # HM modules (exports still named wisdom*)
|
||||
ssh/ # inbound NixOS + outbound HM
|
||||
deploy/ # navi hive
|
||||
patches/ # one-off patches (yt-dlp, t2fanrd, …)
|
||||
```
|
||||
|
||||
Machine-only stuff lives in `hosts/<host>/_private/` — import-tree skips `_private/` globally, so those files only get pulled in where you import them.
|
||||
@@ -0,0 +1,191 @@
|
||||
# Conventions
|
||||
|
||||
## How the flake is wired
|
||||
|
||||
`flake.nix` runs import-tree on `./modules` with a few extra filters: skip `/package/`, `/packages/`, and `dms/home-manager/` (callPackage noise + DMS HM colocation). Don't add a global `/home-manager/` filter — `modules/ssh/home-manager/` needs to load.
|
||||
|
||||
Every scanned `.nix` should be a flake fragment:
|
||||
|
||||
```nix
|
||||
{ self, inputs, ... }: {
|
||||
flake.nixosModules.systemFoo = { config, lib, pkgs, ... }: { /* … */ };
|
||||
}
|
||||
```
|
||||
|
||||
Don't define the same `flake.nixosModules.foo` in two files — merge order isn't something to bet on.
|
||||
|
||||
**Outputs I actually use:** `nixosConfigurations.*`, `nixosModules.*`, `homeManagerModules.*`, `lib.*`, `packages.*`, `navi` / `naviHive`. `modules/parts.nix` declares supported systems and the `flake.lib` / `flake.homeManagerModules` option slots.
|
||||
|
||||
## Option namespaces
|
||||
|
||||
Project options live under `chiasson.*` only:
|
||||
|
||||
| Prefix | For |
|
||||
|--------|-----|
|
||||
| `chiasson.system.*` | Machine policy — docker, flatpak, audio, gaming, deploy builder, … |
|
||||
| `chiasson.desktop.*` | GUI — compositors, DMS, wallpapers, display manager |
|
||||
| `chiasson.home.*` | Home Manager toggles (module exports are still `wisdom*`) |
|
||||
| `chiasson.users.*` | Catalog, `enabled`, `hostOverrides`, `extraModules` |
|
||||
| `chiasson.ssh.*` | SSH inventory |
|
||||
|
||||
Leave upstream `services.*`, `networking.*`, `home.*`, etc. alone.
|
||||
|
||||
## Module shape
|
||||
|
||||
**NixOS leaf** — camelCase export, usually prefixed (`systemDocker`, `desktopNiri`):
|
||||
|
||||
```nix
|
||||
{ ... }: {
|
||||
flake.nixosModules.systemSomething = { config, lib, pkgs, ... }:
|
||||
let cfg = config.chiasson.system.something;
|
||||
in {
|
||||
options.chiasson.system.something.enable = lib.mkEnableOption "…";
|
||||
config = lib.mkIf cfg.enable { /* … */ };
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Aggregates: `nixosModules.system` and `nixosModules.desktop` import their leaves. Host configs import those stacks and set options — they shouldn't reimplement whole subsystems.
|
||||
|
||||
**Home** — files under `modules/wisdom/`. Baseline is `homeManagerModules.wisdom` (`chiasson.home.enable`, wired via `chiasson.desktop.homeManager.bundleWisdom`). Other `wisdom*` slices auto-wire once per user via `lib.wisdomCatalogExtraModules self` (`modules/desktop/options.nix`); hosts only set matching `chiasson.home.*.enable` toggles — no re-import in `home.nix`.
|
||||
|
||||
User apps / dotfiles → wisdom. Daemons, firewall, kernel → NixOS. Sometimes both (LocalSend: HM installs, `systemLocalsend` opens the firewall).
|
||||
|
||||
**Odd exports**
|
||||
|
||||
- `"client-services"` — shared setup for laptops/tablets/desktops (not the VMs). Turns on Bluetooth, lets Navi log in as `builder` to update the machine, enables printing and mounting USB drives, and sets SSH to password. Used on `t2mbp`, `14900k`, `ideapad`, `uConsole`. Skipped on `nix-server` and `r5500`.
|
||||
- `systemUconsoleKernelBuilder` — cross-build the uConsole kernel on x86_64, not for running on the Pi.
|
||||
- `systemFlatpak` — allowlist sync; skips heavy `flatpak update` when lists unchanged (`runHeavyMaintenanceEverySwitch`).
|
||||
|
||||
**Desktop notes**
|
||||
|
||||
- `chiasson.desktop.displayManager.variant`: `sddm` vs `dankgreeter` (DMS + Hyprland/Niri tends toward DankGreeter).
|
||||
- `desktopWallpapers` copies `inputs.wallpapers` into the store; override `chiasson.desktop.wallpapers.source` if needed.
|
||||
|
||||
|
||||
## Users & SSH
|
||||
|
||||
User definitions live in the catalog defaults block inside `modules/system/users.nix` (was previously split across four files in `modules/system/users/`; **folded into one for cohesion**). olivier, server, builder, etc. Pick who exists on a host with `chiasson.users.enabled`.
|
||||
|
||||
### SSH model — read this once
|
||||
|
||||
SSH authentication has two independent sides:
|
||||
|
||||
| Side | Holds | Decides |
|
||||
|------|-------|---------|
|
||||
| **Client** (connecting from) | identity private keys | "Here's proof I own key K" |
|
||||
| **Server** (connecting to) | `authorized_keys` | "Do I trust key K?" |
|
||||
|
||||
A key is just a cryptographic identity — it's not bound to a host or a machine account name. The model is **one key per user account**: each catalog account (`olivier`, `server`, `builder`) has its own keypair in `modules/ssh/inventory.nix`, regardless of which machine that account runs on. Concretely: when `olivier@14900k` sshes to `server@r5500`, OpenSSH routes via a `Match user server` block (generated from `chiasson.ssh.outbound.rbw.extraIdentities`) → `IdentityFile ~/.ssh/id_ed25519_server.pub` (written from `users.server.publicKey`) to ask the rbw agent for the `server` private key; r5500's `server` `authorized_keys` holds the matching server pubkey. olivier's catalog default sets `extraIdentities = [ "server" "builder" ]` so `ssh <account>@<host>` Just Works for every catalog role. We also support `from="ip,cidr"` constraints per account to scope which source IPs may authenticate as it.
|
||||
|
||||
This repo enforces "one key per user": every user has one ed25519 keypair, the private key in Bitwarden, the public key inline in `modules/ssh/inventory.nix`. On SSH connect:
|
||||
|
||||
1. `~/.ssh/config` (managed by home-manager) sets `IdentityAgent SSH_AUTH_SOCK` and `IdentityFile ~/.ssh/id_ed25519_<user>.pub` for every host block.
|
||||
2. OpenSSH reads the `.pub` file as a **filter** against the agent (OpenSSH 7.3+ trick — `.pub` as IdentityFile tells the agent "give me the private key matching this pub"), so even with multiple keys loaded, only the matching one is tried. This is why the old "too many auth failures" symptom is gone by construction.
|
||||
3. rbw holds the private key; HM-managed shells + the desktop session expose `SSH_AUTH_SOCK=$XDG_RUNTIME_DIR/rbw/…` declaratively (no activation scripts).
|
||||
|
||||
### Bitwarden onboarding
|
||||
|
||||
For each new user (run on the user's primary machine):
|
||||
|
||||
```sh
|
||||
# 1. Generate a keypair with rbw (creates a Bitwarden item + prints the pubkey).
|
||||
rbw gen ssh-key olivier
|
||||
|
||||
# 2. Paste the printed `ssh-ed25519 AAAA… olivier` line into
|
||||
# modules/lib/ssh-inventory.nix under users.olivier.publicKey.
|
||||
|
||||
# 3. Push the user to a host:
|
||||
ssh root@nix-server nixos-rebuild switch --flake .#nix-server
|
||||
|
||||
# 4. Have the user ssh into the fleet from any client — their rbw key
|
||||
# is the only thing offered, and every server already has it in
|
||||
# authorized_keys.
|
||||
```
|
||||
|
||||
### Adding a host
|
||||
|
||||
Edit `modules/ssh/inventory.nix` and add:
|
||||
|
||||
```nix
|
||||
hosts."newhost" = {
|
||||
hostName = "192.168.2.X"; # IP — required for navi to deploy here
|
||||
aliases = [ "newhost" ]; # what you type at the ssh prompt
|
||||
};
|
||||
```
|
||||
|
||||
Then add `self.nixosModules.<host>Configuration` to `modules/deploy/navi.nix`.
|
||||
|
||||
### Adding a catalog user
|
||||
|
||||
1. Generate a keypair: `rbw gen ssh-key <name>`.
|
||||
2. Add a `users.<name> = { publicKey = "ssh-ed25519 …"; };` line under `users` in `modules/ssh/inventory.nix`.
|
||||
3. Add a `<name> = { isNormalUser = true; … ssh.inbound.enable = true; ssh.inbound.authorizedHosts = "all"; }` entry under `chiasson.users.catalog` in `modules/system/users.nix`.
|
||||
4. Enable on every host that needs that user, e.g.:
|
||||
|
||||
```nix
|
||||
chiasson.users.enabled = [ "olivier" "<name>" ];
|
||||
```
|
||||
|
||||
### Passwords
|
||||
|
||||
Passwords aren't in the repo. They're in `secrets/secrets.yaml` (encrypted with sops). Each host that has `olivier` must declare that secret and set `neededForUsers = true`; sops-nix decrypts it at boot, and the catalog points `hashedPasswordFile` at that file. That's why the catalog is a proper module instead of a static list — it needs `config.sops.secrets.…` at eval time.
|
||||
|
||||
### Host overrides
|
||||
|
||||
```nix
|
||||
# configuration.nix — machine policy
|
||||
chiasson.users.enabled = [ "olivier" ];
|
||||
chiasson.users.hostOverrides.<name> = { /* optional */ };
|
||||
```
|
||||
|
||||
### Home-manager per-host slices (desktop/laptop hosts)
|
||||
|
||||
Desktop hosts also have `home.nix` exporting `flake.nixosModules.<host>Home`, wired from `default.nix` alongside `*Configuration`:
|
||||
|
||||
```nix
|
||||
# default.nix
|
||||
modules = [
|
||||
self.nixosModules."14900kConfiguration"
|
||||
self.nixosModules."14900kHome"
|
||||
];
|
||||
|
||||
# home.nix — flake fragment, per-host chiasson.home.* toggles
|
||||
{ self, inputs, ... }: {
|
||||
flake.nixosModules."14900kHome" = { self, pkgs, ... }: {
|
||||
imports = [ self.nixosModules.desktopHomeBase ];
|
||||
chiasson.users.extraModules.olivier = [
|
||||
{
|
||||
chiasson.home.browsers.edge.enable = true;
|
||||
# …
|
||||
}
|
||||
];
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
`flake.nixosModules.desktopHomeBase` expands `lib.wisdomCatalogExtraModules` plus shared desktop toggles. Host `*Home` modules append per-host `chiasson.home` overrides (and rare inline `home.packages` blocks). `chiasson.users.extraModules` concatenates lists from multiple modules (base + host), so both can set the same user key.
|
||||
|
||||
The integration block at the bottom of `modules/system/users.nix` turns the catalog + overrides into `users.users` + HM + inbound SSH. Don't hand-roll catalog users unless you're changing this module.
|
||||
|
||||
## Adding things
|
||||
|
||||
**New NixOS leaf:** export `flake.nixosModules.whatever`, wire from `system/default.nix` or `desktop/default.nix` if it's global, or only from a host `configuration.nix` if it's not. `nix flake check`. Git-add new paths if eval uses the git tree.
|
||||
|
||||
**New HM slice:** add `modules/wisdom/.../foo.nix` exporting `flake.homeManagerModules.wisdomFoo` (import-tree picks it up; `wisdomCatalogExtraModules` includes every `wisdom*` export). Every `wisdom*` slice auto-catalogs — including the bash shell — so the root module no longer manually `imports` it. Gate packages on `chiasson.home.*.enable`, and set the slice's own `enable` default with `mkEnableOption "…" // { default = true/false }` in that same module — defaults live with the option that owns them, not in a shared defaults file. Hosts flip toggles in `home.nix`. Upstream HM deps stay imported unconditionally — use `mkIf` on `cfg.enable` for config (never `config`-dependent `imports`; that recurses).
|
||||
|
||||
**Trivial single-package slice:** use `self.lib.wisdomSlice { path = "a.b"; default = true/false; description = "…"; packages = pkgs: [ … ]; }` instead of the `root`/`cfg` + `mkIf (root.enable && cfg.enable)` boilerplate. `packages` is a `pkgs -> [ drv ]` function responsible for its own `availableOn`/platform gating. Several such slices may share one `simple.nix` in their category dir (each still exporting its own `wisdom*` module), rather than one file per slice.
|
||||
|
||||
**Derivations:** `let` inside a fragment, or `flake.packages` / `flake.lib` — not a bare `mkDerivation` file import-tree will try to load.
|
||||
|
||||
**New host:** `hosts/<name>/default.nix` with `nixosSystem` listing `*Configuration` (+ `*Home` for desktops), `configuration.nix` exporting `*Configuration`, hardware + `_private/` as needed. Desktop/laptop hosts also get `home.nix` exporting `*Home` (import `self.nixosModules.desktopHomeBase` unless it's a special case). Register in `modules/deploy/navi.nix` if it should be in the fleet.
|
||||
|
||||
## When editing
|
||||
|
||||
- `nix flake check` after touching `modules/`.
|
||||
- Stage/commit new files if the flake is read from git.
|
||||
- Small diffs; `mkEnableOption` + `mkIf` for toggles.
|
||||
- Cross-module deps: assertions or `mkDefault`, mention it in the option description.
|
||||
- New flake inputs in `flake.nix` with `follows` where versions should track nixpkgs.
|
||||
- `client-services` SSH defaults are for my LAN — don't copy to anything internet-facing without tightening.
|
||||
- If I'll forget the behavior in six months, update these docs in the same change.
|
||||
Generated
+699
-109
File diff suppressed because it is too large
Load Diff
@@ -23,6 +23,96 @@
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
dms-plugin-dank-vault = {
|
||||
url = "github:alcxyz/DankVault";
|
||||
flake = false;
|
||||
};
|
||||
|
||||
dms-plugin-calculator = {
|
||||
url = "github:rochacbruno/DankCalculator";
|
||||
flake = false;
|
||||
};
|
||||
|
||||
dms-plugin-home-assistant = {
|
||||
url = "github:xxyangyoulin/dms-plugin-hass";
|
||||
flake = false;
|
||||
};
|
||||
|
||||
dms-plugin-dropdown-menu = {
|
||||
url = "github:rdannenbring/dropdown-menu";
|
||||
flake = false;
|
||||
};
|
||||
|
||||
dms-plugin-ocr-scanner = {
|
||||
url = "github:hthienloc/dms-ocr-scanner";
|
||||
flake = false;
|
||||
};
|
||||
|
||||
dms-plugin-unified-taskbar = {
|
||||
url = "github:jslandau/dms-unified-taskbar";
|
||||
flake = false;
|
||||
};
|
||||
|
||||
dms-plugin-widget-group = {
|
||||
url = "github:rdannenbring/widget-group";
|
||||
flake = false;
|
||||
};
|
||||
|
||||
dms-plugin-nix-package-runner = {
|
||||
url = "github:iahccc/NixPackageRunner";
|
||||
flake = false;
|
||||
};
|
||||
|
||||
dms-plugin-ambient-sound = {
|
||||
url = "github:hthienloc/dms-ambient-sound";
|
||||
flake = false;
|
||||
};
|
||||
|
||||
dms-plugin-cava-visualizer = {
|
||||
url = "github:ernestowgg/cava-visualizer";
|
||||
flake = false;
|
||||
};
|
||||
|
||||
dms-plugin-emoji-launcher = {
|
||||
url = "github:devnullvoid/dms-emoji-launcher";
|
||||
flake = false;
|
||||
};
|
||||
|
||||
dms-plugin-official = {
|
||||
url = "github:AvengeMedia/dms-plugins";
|
||||
flake = false;
|
||||
};
|
||||
|
||||
dms-plugin-web-search = {
|
||||
url = "github:devnullvoid/dms-web-search";
|
||||
flake = false;
|
||||
};
|
||||
|
||||
dms-plugin-wallpaper-carousel = {
|
||||
url = "github:motor-dev/wallpaperCarousel";
|
||||
flake = false;
|
||||
};
|
||||
|
||||
dms-plugin-ephemera = {
|
||||
url = "github:nicolasgarcia214/Ephemera";
|
||||
flake = false;
|
||||
};
|
||||
|
||||
dms-plugin-app-shortcut = {
|
||||
url = "github:oabragh/AppShortcut";
|
||||
flake = false;
|
||||
};
|
||||
|
||||
dms-plugin-wvkbd-toggle = {
|
||||
url = "git+https://git.chiasson.cloud/dms-plugins/wvkbd-toggle";
|
||||
flake = false;
|
||||
};
|
||||
|
||||
dms-plugin-rbw-lock-toggle = {
|
||||
url = "git+https://git.chiasson.cloud/dms-plugins/rbw-lock-toggle";
|
||||
flake = false;
|
||||
};
|
||||
|
||||
dgop = {
|
||||
url = "github:AvengeMedia/dgop";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
@@ -105,6 +195,17 @@
|
||||
};
|
||||
|
||||
navi.url = "github:cafkafk/navi";
|
||||
|
||||
nixvirt = {
|
||||
url = "https://flakehub.com/f/AshleyYakeley/NixVirt/*.tar.gz";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
hermes = {
|
||||
url = "github:NousResearch/hermes-agent";
|
||||
};
|
||||
mt7927 = {
|
||||
url = "github:cmspam/mt7927-nixos";
|
||||
};
|
||||
};
|
||||
|
||||
outputs = inputs:
|
||||
@@ -112,18 +213,11 @@
|
||||
lib = inputs.nixpkgs.lib;
|
||||
# import-tree default: only .nix, skip paths with /_/ (see
|
||||
# https://import-tree.oeiuwq.com/guides/filtering/ ). filterNot composes AND with that.
|
||||
# Skip /package/, /packages/, and dms/home-manager/ (callPackage + DMS HM colocation).
|
||||
# Skip /package/ and /packages/ (callPackage colocation).
|
||||
# Avoid a global "/home-manager/" filter — it would drop modules/ssh/home-manager/.
|
||||
modulesTree =
|
||||
lib.pipe inputs.import-tree [
|
||||
modulesTree = lib.pipe inputs.import-tree [
|
||||
(it: it.withLib lib)
|
||||
(it:
|
||||
it.filterNot (
|
||||
p:
|
||||
lib.hasInfix "/package/" p
|
||||
|| lib.hasInfix "/packages/" p
|
||||
|| lib.hasInfix "dms/home-manager/" p
|
||||
))
|
||||
(it: it.filterNot (p: lib.hasInfix "/package/" p || lib.hasInfix "/packages/" p))
|
||||
(it: it ./modules)
|
||||
];
|
||||
in
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# Repo helpers. Run `just` to list recipes.
|
||||
|
||||
default:
|
||||
@just --list
|
||||
|
||||
# Sync live DMS config into this host's seed dir. Pass --dry-run, --settings-only, or a host module name.
|
||||
sync-dms *args='':
|
||||
./modules/desktop/shells/dms/_private/sync-seed.sh {{args}}
|
||||
+33
-12
@@ -93,14 +93,45 @@ let
|
||||
// lib.optionalAttrs (name == "nix-server") {
|
||||
targetPort = 22;
|
||||
}
|
||||
) ssh.activeHosts;
|
||||
) ssh.activeHosts /* nix-deploy target list — IPs only */;
|
||||
|
||||
metaNixpkgs = import inputs.nixpkgs {
|
||||
system = "x86_64-linux";
|
||||
};
|
||||
# Build a raw Navi hive attrset from host specs + deployment targets.
|
||||
# `inputs.navi.lib.makeHive` turns the result into `flake.naviHive`.
|
||||
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;
|
||||
in
|
||||
{
|
||||
flake.navi = self.lib.mkNaviHiveConfig {
|
||||
flake.navi = mkNaviHiveConfig {
|
||||
inherit metaNixpkgs hostSpecs deployments;
|
||||
};
|
||||
|
||||
@@ -116,16 +147,6 @@ in
|
||||
"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";
|
||||
|
||||
@@ -49,6 +49,10 @@
|
||||
(lib.mkIf guiEnabled {
|
||||
services.xserver.enable = true;
|
||||
|
||||
# Trash, MTP, network mounts, etc. for GTK file managers (Nemo). Without gvfs the Delete
|
||||
# key silently fails (move-to-trash) while Shift+Delete still works (permanent delete).
|
||||
services.gvfs.enable = lib.mkDefault 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).
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
{ ... }: {
|
||||
flake.lib.hyprlandBinds =
|
||||
{ lib, dmsEnabled ? false, dmsIpcBinds }:
|
||||
let
|
||||
workspaceBinds =
|
||||
(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
|
||||
));
|
||||
in
|
||||
{
|
||||
bind =
|
||||
[
|
||||
"SUPER,T,exec,kitty"
|
||||
"ControlSuper,T,exec,konsole"
|
||||
"SUPER,D,exec,rofi -show drun"
|
||||
"ControlSuper,D,exec,rofi -show window"
|
||||
"SUPER,E,exec,nemo"
|
||||
"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"
|
||||
] ++ lib.optionals dmsEnabled [
|
||||
"Super, Space, exec, ${dmsIpcBinds.spotlight}"
|
||||
"Super, I, exec, ${dmsIpcBinds.settingsFocus}"
|
||||
"Super, N, exec, ${dmsIpcBinds.notepad}"
|
||||
"ShiftSuper, N, exec, ${dmsIpcBinds.notifications}"
|
||||
"Super, M, exec, ${dmsIpcBinds.processlistFocus}"
|
||||
"Super, L, exec, ${dmsIpcBinds.lock}"
|
||||
"ShiftSuper, V, exec, ${dmsIpcBinds.clipboard}"
|
||||
] ++ [
|
||||
"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"
|
||||
]
|
||||
++ workspaceBinds;
|
||||
|
||||
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%-"
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
{ self, ... }: {
|
||||
imports = [
|
||||
./binds.nix
|
||||
];
|
||||
|
||||
flake.nixosModules.desktopHyprland =
|
||||
{ config, options, lib, pkgs, ... }:
|
||||
let
|
||||
@@ -38,6 +42,7 @@
|
||||
}:
|
||||
let
|
||||
hyprlandEnabled = osConfig.chiasson.desktop.hyprland.enable or false;
|
||||
dmsEnabled = (osConfig.chiasson.desktop.shell or null) == "dms";
|
||||
keyringEnabled = osConfig.chiasson.desktop.keyring.enable or false;
|
||||
# nixpkgs hyprland-plugins pin is stale for current Hyprland — override to a known-good rev.
|
||||
hyprbarsPatched =
|
||||
@@ -95,69 +100,12 @@
|
||||
"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
|
||||
}
|
||||
(self.lib.hyprlandBinds {
|
||||
inherit lib dmsEnabled;
|
||||
dmsIpcBinds = self.lib.dmsIpcBinds;
|
||||
})
|
||||
{
|
||||
decoration = {
|
||||
rounding = 10;
|
||||
active_opacity = 0.95;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
{ self, ... }: {
|
||||
flake.lib.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;
|
||||
prefer-no-csd = true;
|
||||
window-rules = self.lib.niriWindowRules;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
{ ... }: {
|
||||
flake.lib.niriBinds =
|
||||
{ pkgs, dmsEnabled ? false, dmsIpcBinds }:
|
||||
let
|
||||
lib = pkgs.lib;
|
||||
in
|
||||
{
|
||||
"Mod+T"."spawn-sh" = lib.getExe pkgs.kitty;
|
||||
"Mod+Control+T"."spawn-sh" = "konsole";
|
||||
"Mod+D"."spawn-sh" = "rofi -show drun";
|
||||
"Mod+Control+O"."spawn-sh" = "rofi -show window";
|
||||
"Mod+E"."spawn-sh" = "nemo";
|
||||
}
|
||||
// lib.optionalAttrs dmsEnabled {
|
||||
"Mod+Space"."spawn-sh" = dmsIpcBinds.spotlight;
|
||||
"Mod+V"."spawn-sh" = dmsIpcBinds.clipboard;
|
||||
"Mod+N"."spawn-sh" = dmsIpcBinds.notepad;
|
||||
"Mod+Shift+N"."spawn-sh" = dmsIpcBinds.notifications;
|
||||
"Mod+M"."spawn-sh" = dmsIpcBinds.processlist;
|
||||
"Mod+B"."spawn-sh" = dmsIpcBinds.barToggle;
|
||||
"Mod+Shift+B"."spawn-sh" = dmsIpcBinds.barToggleBar2;
|
||||
"Mod+A"."spawn-sh" = dmsIpcBinds.ephemera;
|
||||
"Mod+numbersign"."spawn-sh" = dmsIpcBinds.wallpaperCarousel;
|
||||
"Mod+L"."spawn-sh" = dmsIpcBinds.lock;
|
||||
}
|
||||
// {
|
||||
"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+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"
|
||||
];
|
||||
"XF86MonBrightnessUp".spawn = [ "brightnessctl" "set" "+5%" ];
|
||||
"XF86MonBrightnessDown".spawn = [ "brightnessctl" "set" "5%-" ];
|
||||
|
||||
Print.screenshot = _: { };
|
||||
"Ctrl+Print"."screenshot-screen" = _: { };
|
||||
"Alt+Print"."screenshot-window" = _: { };
|
||||
};
|
||||
}
|
||||
@@ -1,172 +1,92 @@
|
||||
{ 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"
|
||||
{ self, inputs, ... }: {
|
||||
imports = [
|
||||
./window-rules.nix
|
||||
./base-settings.nix
|
||||
./binds.nix
|
||||
];
|
||||
|
||||
# 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" = _: { };
|
||||
};
|
||||
};
|
||||
flake.homeManagerModules.desktopNiri =
|
||||
{ config, lib, pkgs, osConfig ? { }, ... }:
|
||||
let
|
||||
home = config.home.homeDirectory;
|
||||
niriOs = osConfig.chiasson.desktop.niri or { };
|
||||
niriEnabled = osConfig.chiasson.desktop.niri.enable or false;
|
||||
keyringEnabled = osConfig.chiasson.desktop.keyring.enable or false;
|
||||
dmsEnabled = (osConfig.chiasson.desktop.shell or null) == "dms";
|
||||
|
||||
keyringNiriStartupKdl = ''
|
||||
spawn-at-startup "dbus-update-activation-environment" "--systemd" "--all"
|
||||
'';
|
||||
|
||||
niriBaseSettings =
|
||||
pkgs: dmsEnabled:
|
||||
self.lib.niriBaseSettings pkgs // {
|
||||
binds = self.lib.niriBinds {
|
||||
inherit pkgs dmsEnabled;
|
||||
dmsIpcBinds = self.lib.dmsIpcBinds;
|
||||
};
|
||||
};
|
||||
|
||||
mergeNiriSettings =
|
||||
pkgs: niriCfg: keyringEnable:
|
||||
pkgs: niriCfg: keyringEnable: dmsEnabled:
|
||||
let
|
||||
lib = pkgs.lib;
|
||||
pi5 = self.lib.pi5NiriKdl;
|
||||
rpi5Extra = lib.optionalString (niriCfg.raspberryPi5DrmWorkaround or false) pi5.drmExtraConfig;
|
||||
base = niriBaseSettings pkgs;
|
||||
base = niriBaseSettings pkgs dmsEnabled;
|
||||
userExtra = niriCfg.extraSettings or { };
|
||||
keyringExtra = lib.optionalString keyringEnable keyringNiriStartupKdl;
|
||||
extraConfigMerged = keyringExtra + rpi5Extra + (userExtra.extraConfig or "");
|
||||
windowRules = (base.window-rules or [ ]) ++ (userExtra.window-rules or [ ]);
|
||||
userExtraNoSpecial = lib.removeAttrs userExtra [ "window-rules" "extraConfig" "binds" ];
|
||||
mergedBinds =
|
||||
(base.binds or { })
|
||||
// (userExtra.binds or { })
|
||||
// (niriCfg.extraBinds or { });
|
||||
in
|
||||
lib.recursiveUpdate base (
|
||||
lib.removeAttrs userExtra [ "window-rules" "extraConfig" ]
|
||||
userExtraNoSpecial
|
||||
// {
|
||||
binds = mergedBinds;
|
||||
}
|
||||
// lib.optionalAttrs (windowRules != [ ]) {
|
||||
window-rules = windowRules;
|
||||
}
|
||||
// lib.optionalAttrs (rpi5Extra != "" || (userExtra.extraConfig or "") != "") {
|
||||
// lib.optionalAttrs (extraConfigMerged != "") {
|
||||
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;
|
||||
|
||||
dmsWindowRulesPath = "${home}/.config/niri/dms/windowrules.kdl";
|
||||
dmsWindowRulesInclude = lib.optionalString dmsEnabled ''
|
||||
include "${dmsWindowRulesPath}"
|
||||
'';
|
||||
baseMergedSettings = mergeNiriSettings pkgs niriOs keyringEnabled dmsEnabled;
|
||||
mergedSettings = lib.recursiveUpdate baseMergedSettings {
|
||||
extraConfig = dmsWindowRulesInclude + (baseMergedSettings.extraConfig or "");
|
||||
};
|
||||
niriConfigPkg = inputs.wrapper-modules.wrappers.niri.wrap {
|
||||
inherit pkgs;
|
||||
settings = mergedSettings;
|
||||
disableConfigValidation = dmsEnabled;
|
||||
};
|
||||
in
|
||||
{
|
||||
config = lib.mkIf niriEnabled {
|
||||
config = lib.mkIf niriEnabled (lib.mkMerge [
|
||||
{
|
||||
xdg.configFile."niri/config.kdl".source = "${niriConfigPkg}/niri-config.kdl";
|
||||
};
|
||||
}
|
||||
(lib.mkIf dmsEnabled {
|
||||
home.activation.dmsNiriWindowRulesSeed = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
|
||||
rulesDir="$HOME/.config/niri/dms"
|
||||
rulesFile="$rulesDir/windowrules.kdl"
|
||||
mkdir -p "$rulesDir"
|
||||
if [ ! -e "$rulesFile" ]; then
|
||||
${pkgs.coreutils}/bin/touch "$rulesFile"
|
||||
fi
|
||||
'';
|
||||
})
|
||||
]);
|
||||
};
|
||||
|
||||
flake.nixosModules.desktopNiri =
|
||||
@@ -195,6 +115,15 @@ in
|
||||
Merged into shared defaults → `config.kdl` via wrapper-modules. Put raw KDL in `extraConfig`.
|
||||
'';
|
||||
};
|
||||
|
||||
extraBinds = lib.mkOption {
|
||||
type = lib.types.attrsOf lib.types.anything;
|
||||
default = { };
|
||||
description = ''
|
||||
Keybind overrides merged with shared defaults and `extraSettings.binds`.
|
||||
Use this when multiple modules need to add binds without overwriting each other.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkMerge [
|
||||
@@ -211,8 +140,6 @@ in
|
||||
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) {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
{ ... }: {
|
||||
flake.lib.niriWindowRules = [
|
||||
{
|
||||
matches = [
|
||||
{
|
||||
app-id = "^$";
|
||||
title = "^$";
|
||||
}
|
||||
];
|
||||
open-floating = true;
|
||||
open-focused = false;
|
||||
}
|
||||
{
|
||||
matches = [
|
||||
{ title = "^(Picture-in-Picture|Picture in picture)$"; }
|
||||
];
|
||||
open-floating = true;
|
||||
open-focused = false;
|
||||
default-column-width = { fixed = 640; };
|
||||
default-window-height = { fixed = 360; };
|
||||
default-floating-position = _: {
|
||||
props = {
|
||||
x = 32;
|
||||
y = 32;
|
||||
relative-to = "bottom-right";
|
||||
};
|
||||
};
|
||||
}
|
||||
{
|
||||
matches = [
|
||||
{ app-id = "^Minecraft\\*"; }
|
||||
{ app-id = "^Minecraft$"; }
|
||||
];
|
||||
tiled-state = true;
|
||||
}
|
||||
];
|
||||
}
|
||||
+54
-13
@@ -1,4 +1,38 @@
|
||||
{ ... }: {
|
||||
{ lib, ... }:
|
||||
{
|
||||
flake.lib.wisdomCatalogExtraModules =
|
||||
self:
|
||||
let
|
||||
names = lib.sort builtins.lessThan (
|
||||
lib.filter (n: lib.hasPrefix "wisdom" n && n != "wisdom") (
|
||||
builtins.attrNames self.homeManagerModules
|
||||
)
|
||||
);
|
||||
in
|
||||
map (name: self.homeManagerModules.${name}) names;
|
||||
|
||||
flake.lib.wisdomSlice =
|
||||
{ path, default, description, packages }:
|
||||
{ config, lib, pkgs, ... }:
|
||||
let
|
||||
segs = lib.splitString "." path;
|
||||
optPath = [
|
||||
"chiasson"
|
||||
"home"
|
||||
]
|
||||
++ segs
|
||||
++ [ "enable" ];
|
||||
root = config.chiasson.home;
|
||||
cfg = lib.getAttrFromPath segs root;
|
||||
enabled = lib.getAttrFromPath optPath config;
|
||||
in
|
||||
{
|
||||
options = lib.setAttrByPath optPath (lib.mkEnableOption description // { inherit default; });
|
||||
config = lib.mkIf (root.enable && enabled) {
|
||||
home.packages = lib.filter (lib.meta.availableOn pkgs.stdenv.hostPlatform) (packages pkgs);
|
||||
};
|
||||
};
|
||||
|
||||
flake.nixosModules.desktopOptions =
|
||||
{ config, options, lib, pkgs, self, inputs, ... }:
|
||||
let
|
||||
@@ -34,11 +68,13 @@
|
||||
{
|
||||
options.chiasson.desktop = {
|
||||
defaultSession = lib.mkOption {
|
||||
type = lib.types.nullOr (lib.types.enum [
|
||||
type = lib.types.nullOr (
|
||||
lib.types.enum [
|
||||
"hyprland"
|
||||
"niri"
|
||||
"plasma"
|
||||
]);
|
||||
]
|
||||
);
|
||||
default = null;
|
||||
example = "niri";
|
||||
description = ''
|
||||
@@ -56,10 +92,12 @@
|
||||
|
||||
displayManager = {
|
||||
variant = lib.mkOption {
|
||||
type = lib.types.nullOr (lib.types.enum [
|
||||
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)).
|
||||
@@ -80,9 +118,11 @@
|
||||
};
|
||||
|
||||
displayManager.sddm = {
|
||||
wayland.enable = lib.mkEnableOption ''
|
||||
wayland.enable =
|
||||
lib.mkEnableOption ''
|
||||
SDDM greeter on Wayland (nicer on HiDPI; turn off if the greeter glitches on your GPU).
|
||||
'' // {
|
||||
''
|
||||
// {
|
||||
default = true;
|
||||
};
|
||||
|
||||
@@ -153,16 +193,18 @@
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Add `wisdom` (baseline + bash) to HM `sharedModules`. Other slices still go in per-user `extraModules`.
|
||||
Add `wisdom` (baseline + bash) to HM `sharedModules`. Per-user `extraModules` should use `lib.wisdomCatalogExtraModules self` once and set `chiasson.home.*.enable`.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
keyring = {
|
||||
enable = lib.mkEnableOption ''
|
||||
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;
|
||||
};
|
||||
};
|
||||
@@ -184,8 +226,7 @@
|
||||
message = "chiasson.desktop.defaultSession = \"plasma\" requires chiasson.desktop.plasma.enable = true.";
|
||||
}
|
||||
{
|
||||
assertion =
|
||||
cfg.displayManager.variant != "dankgreeter" || cfg.hyprland.enable || cfg.niri.enable;
|
||||
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.";
|
||||
}
|
||||
{
|
||||
@@ -210,7 +251,7 @@
|
||||
(lib.mkIf (dmsEnabled && hmAvailable) {
|
||||
"home-manager".sharedModules = [ self.homeManagerModules.desktopShellDms ];
|
||||
})
|
||||
(lib.mkIf (hmAvailable && (dmsEnabled || cfg.niri.enable)) {
|
||||
(lib.mkIf hmAvailable {
|
||||
"home-manager".extraSpecialArgs = { inherit inputs; };
|
||||
})
|
||||
];
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
# Generic defaults written once to ~/.config/DankMaterialShell/settings.json on first login.
|
||||
# Hosts can override via `chiasson.desktop.shells.dms.defaultSeedDir` (see 14900k).
|
||||
# After seeding, DMS settings are owned by the UI — rebuilds do not overwrite them.
|
||||
#
|
||||
# To refresh host seeds from live DMS config: `just sync-dms`
|
||||
{
|
||||
lib,
|
||||
gpuTempEnabled ? true,
|
||||
extraRightBarWidgets ? [ ],
|
||||
}:
|
||||
let
|
||||
gpuTempBarWidget = {
|
||||
id = "gpuTemp";
|
||||
enabled = true;
|
||||
selectedGpuIndex = 0;
|
||||
pciId = "10de:1f07";
|
||||
};
|
||||
in
|
||||
{
|
||||
theme = "dark";
|
||||
dynamicTheming = true;
|
||||
currentThemeName = "dynamic";
|
||||
matugenScheme = "scheme-tonal-spot";
|
||||
runUserMatugenTemplates = true;
|
||||
matugenTemplateVesktop = false;
|
||||
showDock = false;
|
||||
gtkThemingEnabled = true;
|
||||
qtThemingEnabled = true;
|
||||
terminalsAlwaysDark = true;
|
||||
syncModeWithPortal = true;
|
||||
configVersion = 5;
|
||||
|
||||
barConfigs = [
|
||||
{
|
||||
id = "default";
|
||||
name = "Main Bar";
|
||||
enabled = true;
|
||||
position = 0;
|
||||
screenPreferences = [ "all" ];
|
||||
showOnLastDisplay = true;
|
||||
|
||||
leftWidgets =
|
||||
[
|
||||
{
|
||||
id = "launcherButton";
|
||||
enabled = true;
|
||||
}
|
||||
{
|
||||
id = "nixMonitor";
|
||||
enabled = true;
|
||||
}
|
||||
{
|
||||
id = "cpuUsage";
|
||||
enabled = true;
|
||||
}
|
||||
{
|
||||
id = "cpuTemp";
|
||||
enabled = true;
|
||||
}
|
||||
]
|
||||
++ lib.optionals gpuTempEnabled [ gpuTempBarWidget ]
|
||||
++ [
|
||||
{
|
||||
id = "memUsage";
|
||||
enabled = true;
|
||||
}
|
||||
];
|
||||
|
||||
centerWidgets = [
|
||||
{
|
||||
id = "workspaceSwitcher";
|
||||
enabled = true;
|
||||
}
|
||||
];
|
||||
|
||||
rightWidgets = extraRightBarWidgets
|
||||
++ [
|
||||
{
|
||||
id = "systemTray";
|
||||
enabled = true;
|
||||
}
|
||||
{
|
||||
id = "notificationButton";
|
||||
enabled = true;
|
||||
}
|
||||
{
|
||||
id = "battery";
|
||||
enabled = true;
|
||||
}
|
||||
{
|
||||
id = "controlCenterButton";
|
||||
enabled = true;
|
||||
}
|
||||
{
|
||||
id = "clock";
|
||||
enabled = true;
|
||||
clockCompactMode = false;
|
||||
}
|
||||
];
|
||||
|
||||
spacing = 0;
|
||||
innerPadding = 4;
|
||||
bottomGap = 0;
|
||||
transparency = 1;
|
||||
widgetTransparency = 0.45;
|
||||
squareCorners = true;
|
||||
maximizeDetection = true;
|
||||
visible = true;
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
# Home Manager wiring for DMS (imported by default.nix; lives under _private/ for import-tree).
|
||||
{
|
||||
inputs,
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
osConfig ? { },
|
||||
...
|
||||
}:
|
||||
let
|
||||
home = config.home.homeDirectory;
|
||||
dmsEnabled = lib.attrByPath [ "chiasson" "desktop" "shell" ] null osConfig == "dms";
|
||||
dmsOs = lib.attrByPath [ "chiasson" "desktop" "shells" "dms" ] { } osConfig;
|
||||
hostName = lib.attrByPath [ "networking" "hostName" ] "nixos" osConfig;
|
||||
rebuildCommand =
|
||||
if (dmsOs.rebuildCommand or null) != null then
|
||||
dmsOs.rebuildCommand
|
||||
else
|
||||
[ "sudo" "nixos-rebuild" "switch" "--flake" ".#${hostName}" ];
|
||||
gpuTempEnabled = dmsOs.enableGpuTemp or true;
|
||||
hyprlandEnabled = lib.attrByPath [ "chiasson" "desktop" "hyprland" "enable" ] false osConfig;
|
||||
discordEnabled = lib.attrByPath [ "chiasson" "home" "apps" "discord" "enable" ] false config;
|
||||
ohMyPoshEnabled = config.programs.oh-my-posh.enable or false;
|
||||
obsidianSnippetDirs = map (v: v + "/snippets") (dmsOs.obsidianVaults or [ ]);
|
||||
matugenOutputDirs =
|
||||
lib.optionals hyprlandEnabled [ "${home}/.config/hypr" ]
|
||||
++ lib.optionals ohMyPoshEnabled [ "${home}/.config/oh-my-posh" ]
|
||||
++ lib.optionals discordEnabled [ "${home}/.config/vesktop/themes" ]
|
||||
++ [
|
||||
"${home}/.config/qt6ct/colors"
|
||||
"${home}/.config/qt5ct/colors"
|
||||
];
|
||||
ensureMatugenBody =
|
||||
lib.concatStringsSep "\n" (map (dir: "mkdir -p ${lib.escapeShellArg dir}") matugenOutputDirs)
|
||||
+ lib.concatStringsSep "\n" (map (dir: ''
|
||||
snippetDir=${lib.escapeShellArg dir}
|
||||
parentDir="$(dirname "$snippetDir")"
|
||||
if [ -d "$snippetDir" ]; then
|
||||
:
|
||||
elif [ ! -d "$parentDir" ] || [ ! -w "$parentDir" ]; then
|
||||
echo "dms: skipping Obsidian snippet dir '$snippetDir' (parent missing or not writable)" >&2
|
||||
else
|
||||
mkdir -p "$snippetDir"
|
||||
fi
|
||||
'') obsidianSnippetDirs)
|
||||
+ lib.optionalString ohMyPoshEnabled ''
|
||||
dest="$HOME/.config/oh-my-posh/theme.omp.json"
|
||||
template="$HOME/.config/matugen/templates/ohmyposh-theme.omp.json"
|
||||
if [ -L "$dest" ] || { [ -e "$dest" ] && ! [ -f "$dest" ]; }; then
|
||||
rm -f "$dest"
|
||||
fi
|
||||
if [ ! -f "$dest" ]; then
|
||||
cp -L "$template" "$dest"
|
||||
fi
|
||||
'';
|
||||
ensureMatugenOutputDirsScript = pkgs.writeShellScript "dms-ensure-matugen-output-dirs" ''
|
||||
set -euo pipefail
|
||||
${ensureMatugenBody}
|
||||
'';
|
||||
|
||||
dmsRestartOnceScript = pkgs.writeShellScript "dms-plugin-restart-once" (
|
||||
builtins.replaceStrings
|
||||
[ "@FLOCK@" "@SYSTEMCTL@" ]
|
||||
[
|
||||
"${pkgs.util-linux}/bin/flock"
|
||||
"${pkgs.systemd}/bin/systemctl"
|
||||
]
|
||||
(builtins.readFile ../scripts/dms-restart-once.sh)
|
||||
);
|
||||
|
||||
jsonFormat = pkgs.formats.json { };
|
||||
dmsConfigDir = "${config.xdg.configHome}/DankMaterialShell";
|
||||
defaultSeedDir = dmsOs.defaultSeedDir or null;
|
||||
|
||||
dmsSeedSources =
|
||||
if defaultSeedDir != null then
|
||||
{
|
||||
"settings.json" = defaultSeedDir + "/settings.json";
|
||||
}
|
||||
// lib.optionalAttrs (builtins.pathExists (defaultSeedDir + "/plugin_settings.json")) {
|
||||
"plugin_settings.json" = defaultSeedDir + "/plugin_settings.json";
|
||||
}
|
||||
else
|
||||
{
|
||||
"settings.json" = jsonFormat.generate "dms-settings-seed.json" (
|
||||
import ./default-settings.nix {
|
||||
inherit lib gpuTempEnabled;
|
||||
extraRightBarWidgets = dmsOs.extraRightBarWidgets or [ ];
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
dmsSeedBody = lib.concatMapStringsSep "\n" (
|
||||
fileName:
|
||||
let
|
||||
dest = "${dmsConfigDir}/${fileName}";
|
||||
src = dmsSeedSources.${fileName};
|
||||
in
|
||||
''
|
||||
dest=${lib.escapeShellArg dest}
|
||||
mkdir -p "$(dirname "$dest")"
|
||||
if [ -L "$dest" ]; then
|
||||
${pkgs.coreutils}/bin/cp --remove-destination -L "$dest" "$dest"
|
||||
fi
|
||||
if [ ! -e "$dest" ]; then
|
||||
${pkgs.coreutils}/bin/cp ${lib.escapeShellArg src} "$dest"
|
||||
chmod u+w "$dest"
|
||||
fi
|
||||
''
|
||||
) (lib.attrNames dmsSeedSources);
|
||||
dmsSeedConfigScript = pkgs.writeShellScript "dms-seed-config" ''
|
||||
set -euo pipefail
|
||||
${dmsSeedBody}
|
||||
'';
|
||||
|
||||
matugenConfigText = import ./matugen-config.nix {
|
||||
inherit
|
||||
lib
|
||||
home
|
||||
hyprlandEnabled
|
||||
ohMyPoshEnabled
|
||||
discordEnabled
|
||||
obsidianSnippetDirs
|
||||
;
|
||||
};
|
||||
|
||||
# Plugin registry auto-derived from flake inputs matching `dms-plugin-*`.
|
||||
# Adding a plugin now requires editing only `flake.nix`. The on-disk
|
||||
# plugin id (= path under ~/.config/DankMaterialShell/plugins/) is camelCase
|
||||
# per upstream convention; the kebab→camel transform matches it. The
|
||||
# `idExceptions` map carries the one plugin whose manifest id does not
|
||||
# follow the default rule.
|
||||
pluginInputs = lib.filterAttrs (n: _: lib.hasPrefix "dms-plugin-" n) inputs;
|
||||
|
||||
# `dank-vault` → `dankVault`. `lib.splitString` yields ≥ 1 element so
|
||||
# `head parts` is safe; `map` runs over the tail only (no-op for
|
||||
# single-word names like `calculator`). `builtins.substring 1 (-1) p`
|
||||
# returns the rest of `p` from index 1, dropping the first char
|
||||
# (Nix treats a negative `len` as "rest of string from `start`").
|
||||
toCamel = s:
|
||||
let parts = lib.splitString "-" s; in
|
||||
builtins.head parts
|
||||
+ lib.concatStrings (map (
|
||||
p: if p == "" then ""
|
||||
else lib.toUpper (builtins.substring 0 1 p)
|
||||
+ builtins.substring 1 (-1) p
|
||||
) (builtins.tail parts));
|
||||
|
||||
# The `home-assistant` plugin ships its manifest under id
|
||||
# `homeAssistantMonitor` (not the default kebab→camel result).
|
||||
idExceptions = { "home-assistant" = "homeAssistantMonitor"; };
|
||||
|
||||
# NB: don't rewrite as `idExceptions.${stripped} or toCamel stripped`.
|
||||
# Nix grammar: `or` binds tighter than function application, so the
|
||||
# `toCamel` and its arg parse as separate postfix args to the select.
|
||||
# When the lookup succeeds the resolved string is then applied as a
|
||||
# function, producing `attempt to call something which is not a function
|
||||
# but a string: "homeAssistantMonitor"`. Explicit `if/hasAttr` sidesteps
|
||||
# the parse entirely.
|
||||
camelMap = lib.mapAttrs' (n: src:
|
||||
let stripped = lib.removePrefix "dms-plugin-" n; in
|
||||
lib.nameValuePair
|
||||
(if builtins.hasAttr stripped idExceptions
|
||||
then idExceptions.${stripped}
|
||||
else toCamel stripped)
|
||||
src
|
||||
) pluginInputs;
|
||||
|
||||
# The official plugin pack bundles multiple QML apps. Ship only the
|
||||
# DankDesktopWeather subdir (verified by its manifest) to slim closure
|
||||
# and avoid overlapping with future dedicated `dms-plugin-*` flakes.
|
||||
thirdPartyPlugins =
|
||||
builtins.removeAttrs camelMap [ "official" ] // {
|
||||
dankDesktopWeather = inputs.dms-plugin-official + "/DankDesktopWeather";
|
||||
};
|
||||
|
||||
# DMS reads plugins from ~/.config/DankMaterialShell/plugins/<id>/, so
|
||||
# we route each plugin source there via HM's `xdg.configFile.target`.
|
||||
# The attrset name is scoped under `dms-plugin-${id}` (not the bare
|
||||
# camelCase id) so unrelated HM modules can ship a same-named file
|
||||
# (e.g. `xdg.configFile.calculator`) without colliding with us.
|
||||
shipPlugin = id: src: {
|
||||
source = src;
|
||||
target = "DankMaterialShell/plugins/${id}";
|
||||
onChange = "${dmsRestartOnceScript}";
|
||||
};
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
inputs.dms.homeModules.dank-material-shell
|
||||
inputs.nix-monitor.homeManagerModules.default
|
||||
];
|
||||
|
||||
config = lib.mkIf dmsEnabled (lib.mkMerge [
|
||||
(lib.mkIf (dmsOs.bundleThirdPartyPlugins or true) {
|
||||
xdg.configFile = lib.mapAttrs' (id: src: lib.nameValuePair
|
||||
"dms-plugin-${id}" (shipPlugin id src)) thirdPartyPlugins;
|
||||
})
|
||||
{
|
||||
programs.nix-monitor = {
|
||||
enable = true;
|
||||
inherit rebuildCommand;
|
||||
generationsCommand = [
|
||||
"bash"
|
||||
"-c"
|
||||
"readlink /nix/var/nix/profiles/system | cut -d- -f2"
|
||||
];
|
||||
};
|
||||
|
||||
home.packages = with pkgs; [
|
||||
kdePackages.qtdeclarative
|
||||
kdePackages.kirigami.unwrapped
|
||||
kdePackages.qtmultimedia
|
||||
kdePackages.sonnet
|
||||
tesseract
|
||||
mpv
|
||||
socat
|
||||
wl-clipboard
|
||||
jq
|
||||
cava
|
||||
curl
|
||||
libsecret
|
||||
];
|
||||
|
||||
programs.dank-material-shell = {
|
||||
enable = true;
|
||||
package = inputs.dms.packages.${pkgs.stdenv.hostPlatform.system}.dms-shell.override {
|
||||
extraQtPackages = with pkgs.kdePackages; [ qtwebsockets ];
|
||||
};
|
||||
systemd = {
|
||||
enable = true;
|
||||
restartIfChanged = true;
|
||||
};
|
||||
enableSystemMonitoring = true;
|
||||
dgop.package = inputs.dgop.packages.${pkgs.stdenv.hostPlatform.system}.default;
|
||||
enableVPN = true;
|
||||
enableDynamicTheming = true;
|
||||
enableAudioWavelength = true;
|
||||
enableCalendarEvents = false;
|
||||
# Empty on purpose: DMS owns ~/.config/DankMaterialShell/{settings,session}.json
|
||||
# at runtime (seeded by `home.activation.dmsSeedConfig`). Setting these
|
||||
# to non-empty here would clobber the runtime seed on every activation.
|
||||
settings = { };
|
||||
session = { };
|
||||
};
|
||||
|
||||
# Surface bad obsidianVaults paths at build time instead of at first matugen run.
|
||||
# Each entry must point at the `<vault>/.obsidian` dir (matugen writes
|
||||
# `<vault>/.obsidian/snippets/matugen.css` from there).
|
||||
assertions = lib.mkIf ((dmsOs.obsidianVaults or [ ]) != [ ]) [
|
||||
{
|
||||
assertion = builtins.all (v: lib.hasSuffix "/.obsidian" v) (dmsOs.obsidianVaults or [ ]);
|
||||
message = "chiasson.desktop.shells.dms.obsidianVaults entries must end in '/.obsidian' (matugen writes <vault>/.obsidian/snippets/matugen.css). Example: /home/olivier/Documents/HomeLab/.obsidian";
|
||||
}
|
||||
];
|
||||
|
||||
home.activation.dmsSeedConfig = lib.hm.dag.entryAfter [ "writeBoundary" ] "${dmsSeedConfigScript}";
|
||||
|
||||
systemd.user.services.dms.serviceConfig.ExecStartPre =
|
||||
lib.mkOrder 100 "${ensureMatugenOutputDirsScript}";
|
||||
|
||||
xdg.configFile."matugen/config.toml".text = matugenConfigText;
|
||||
xdg.configFile."matugen/templates/dank-discord.css".source = ../templates/dank-discord.css;
|
||||
xdg.configFile."matugen/templates/hyprland-colors.conf".source = ../templates/hyprland-colors.conf;
|
||||
xdg.configFile."matugen/templates/ohmyposh-theme.omp.json".source = ../templates/ohmyposh-theme.omp.json;
|
||||
xdg.configFile."matugen/templates/obsidian-minimal-matugen-colors.css".source =
|
||||
../templates/obsidian-minimal-matugen-colors.css;
|
||||
}
|
||||
(lib.mkIf ohMyPoshEnabled {
|
||||
programs.oh-my-posh = {
|
||||
useTheme = lib.mkForce null;
|
||||
configFile = lib.mkForce "${config.xdg.configHome}/oh-my-posh/theme.omp.json";
|
||||
};
|
||||
})
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
# Builds matugen config.toml text for DMS dynamic theming.
|
||||
{
|
||||
lib,
|
||||
home,
|
||||
hyprlandEnabled,
|
||||
ohMyPoshEnabled,
|
||||
discordEnabled,
|
||||
obsidianSnippetDirs,
|
||||
}:
|
||||
let
|
||||
templateBlock = name: input: output: ''
|
||||
|
||||
[templates.${name}]
|
||||
input_path = '${input}'
|
||||
output_path = '${output}'
|
||||
'';
|
||||
in
|
||||
''
|
||||
[config]
|
||||
''
|
||||
+ lib.optionalString hyprlandEnabled (
|
||||
templateBlock "hyprland"
|
||||
"${home}/.config/matugen/templates/hyprland-colors.conf"
|
||||
"${home}/.config/hypr/colors.conf"
|
||||
)
|
||||
+ lib.optionalString ohMyPoshEnabled (
|
||||
templateBlock "ohmyposh"
|
||||
"${home}/.config/matugen/templates/ohmyposh-theme.omp.json"
|
||||
"${home}/.config/oh-my-posh/theme.omp.json"
|
||||
)
|
||||
+ lib.optionalString discordEnabled (
|
||||
templateBlock "dank-discord"
|
||||
"${home}/.config/matugen/templates/dank-discord.css"
|
||||
"${home}/.config/vesktop/themes/dank-discord.css"
|
||||
)
|
||||
+ lib.optionalString (obsidianSnippetDirs != [ ]) (
|
||||
lib.concatStringsSep "\n" (
|
||||
lib.lists.imap0 (i: snippetDir:
|
||||
templateBlock "Obsidian${toString i}"
|
||||
"${home}/.config/matugen/templates/obsidian-minimal-matugen-colors.css"
|
||||
"${snippetDir}/matugen.css"
|
||||
) obsidianSnippetDirs
|
||||
)
|
||||
)
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env bash
|
||||
# Sync live DMS config into modules/hosts/<host>/_private/dms-defaults/ (see defaultSeedDir).
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [options] [host]
|
||||
|
||||
Copy ~/.config/DankMaterialShell/{settings,plugin_settings}.json into the
|
||||
host seed directory (pretty-printed). Host defaults to this machine's hostname.
|
||||
|
||||
Options:
|
||||
--settings-only Skip plugin_settings.json
|
||||
-n, --dry-run Preview without writing
|
||||
-h, --help
|
||||
|
||||
Examples:
|
||||
just sync-dms
|
||||
$(basename "$0") --dry-run ideapad
|
||||
EOF
|
||||
}
|
||||
|
||||
find_repo_root() {
|
||||
local dir="${1:-$PWD}"
|
||||
while [[ "$dir" != "/" ]]; do
|
||||
if [[ -f "$dir/flake.nix" && -d "$dir/modules/hosts" ]]; then
|
||||
printf '%s\n' "$dir"
|
||||
return 0
|
||||
fi
|
||||
dir="$(dirname "$dir")"
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
resolve_host_module() {
|
||||
local hn="$1" repo_root="$2"
|
||||
local hosts_dir="${repo_root}/modules/hosts" dir name
|
||||
|
||||
if [[ -d "${hosts_dir}/${hn}/_private" ]]; then
|
||||
printf '%s' "$hn"
|
||||
return 0
|
||||
fi
|
||||
|
||||
for dir in "${hosts_dir}"/*/; do
|
||||
name="$(basename "$dir")"
|
||||
[[ -f "${dir}configuration.nix" ]] \
|
||||
&& grep -qF "hostName = \"${hn}\";" "${dir}configuration.nix" \
|
||||
&& { printf '%s' "$name"; return 0; }
|
||||
done
|
||||
|
||||
echo "error: no host module for hostname '${hn}' (pass module name explicitly)" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
sync_json() {
|
||||
local source="$1" dest="$2" label="$3"
|
||||
|
||||
[[ -f "$source" ]] || { echo "skip: $label not found" >&2; return 0; }
|
||||
|
||||
jq empty "$source"
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
echo " $source -> $dest"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local tmp
|
||||
tmp="$(mktemp)"
|
||||
jq . "$source" >"$tmp"
|
||||
mv "$tmp" "$dest"
|
||||
echo "Updated $dest"
|
||||
UPDATED+=("$dest")
|
||||
}
|
||||
|
||||
DRY_RUN=0
|
||||
SETTINGS_ONLY=0
|
||||
HOST=""
|
||||
UPDATED=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-n | --dry-run) DRY_RUN=1; shift ;;
|
||||
--settings-only) SETTINGS_ONLY=1; shift ;;
|
||||
-h | --help) usage; exit 0 ;;
|
||||
-*) echo "error: unknown option: $1" >&2; usage >&2; exit 1 ;;
|
||||
*) HOST="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
REPO_ROOT="${CHIASSON_NIX_ROOT:-$(find_repo_root "$PWD" || true)}"
|
||||
[[ -n "$REPO_ROOT" ]] || { echo "error: run from chiasson-nix repo root" >&2; exit 1; }
|
||||
command -v jq >/dev/null || { echo "error: jq required" >&2; exit 1; }
|
||||
|
||||
if [[ -z "$HOST" ]]; then
|
||||
SYSTEM_HOSTNAME="$(hostname -s 2>/dev/null || hostname)"
|
||||
HOST="$(resolve_host_module "$SYSTEM_HOSTNAME" "$REPO_ROOT")"
|
||||
[[ "$HOST" == "$SYSTEM_HOSTNAME" ]] \
|
||||
&& echo "Using host module: $HOST" \
|
||||
|| echo "Using host module: $HOST (hostname: $SYSTEM_HOSTNAME)"
|
||||
fi
|
||||
|
||||
HOST_PRIVATE="${REPO_ROOT}/modules/hosts/${HOST}/_private"
|
||||
SEED_DIR="${HOST_PRIVATE}/dms-defaults"
|
||||
DMS_CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}/DankMaterialShell"
|
||||
|
||||
[[ -d "$HOST_PRIVATE" ]] || { echo "error: missing $HOST_PRIVATE" >&2; exit 1; }
|
||||
[[ -f "${DMS_CONFIG}/settings.json" ]] || {
|
||||
echo "error: missing ${DMS_CONFIG}/settings.json" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ "$DRY_RUN" -eq 1 ]] && echo "Would sync into $SEED_DIR:"
|
||||
[[ "$DRY_RUN" -eq 0 ]] && mkdir -p "$SEED_DIR"
|
||||
|
||||
sync_json "${DMS_CONFIG}/settings.json" "${SEED_DIR}/settings.json" settings.json
|
||||
[[ "$SETTINGS_ONLY" -eq 0 ]] \
|
||||
&& sync_json "${DMS_CONFIG}/plugin_settings.json" "${SEED_DIR}/plugin_settings.json" plugin_settings.json
|
||||
|
||||
[[ "$DRY_RUN" -eq 1 || ${#UPDATED[@]} -eq 0 ]] && exit 0
|
||||
git -C "$REPO_ROOT" diff --stat -- "${UPDATED[@]}" 2>/dev/null || true
|
||||
@@ -1,4 +1,4 @@
|
||||
{ inputs, ... }: {
|
||||
{ ... }: {
|
||||
flake.nixosModules.desktopShellDmsOptions = { lib, ... }: {
|
||||
options.chiasson.desktop.shells.dms = {
|
||||
enableGpuTemp = lib.mkOption {
|
||||
@@ -6,85 +6,65 @@
|
||||
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 {
|
||||
obsidianVaults = 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.";
|
||||
example = [ "/mnt/zimaos/Obsidian/Home/.obsidian" ];
|
||||
description = "Vault `.obsidian/` dirs; matugen writes `<vault>/snippets/matugen.css`.";
|
||||
};
|
||||
extraRightBarWidgets = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.attrs;
|
||||
default = [ ];
|
||||
description = "Extra right-bar widgets (prepended).";
|
||||
description = "Extra right-bar widgets merged into the first-run DMS settings seed.";
|
||||
};
|
||||
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.";
|
||||
};
|
||||
defaultSeedDir = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
default = null;
|
||||
example = ./dms-defaults;
|
||||
description = ''
|
||||
Host-specific first-run DMS config seed directory. When set, copies
|
||||
`settings.json` (required) and `plugin_settings.json` (optional) from this
|
||||
directory into `~/.config/DankMaterialShell/` on fresh profiles.
|
||||
Replaces the bundled `default-settings.nix` template for `settings.json`.
|
||||
'';
|
||||
};
|
||||
bundleThirdPartyPlugins = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Install bundled DMS plugins from flake inputs (third-party + chiasson).
|
||||
Disable individual plugins in DMS settings; set this to false to skip all bundled plugins.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
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
|
||||
];
|
||||
flake.homeManagerModules.desktopShellDms = import ./_private/hm.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;
|
||||
};
|
||||
# DMS IPC commands shared between Niri and Hyprland keybind tables.
|
||||
# Defines flake.lib.dmsIpcBinds so each compositor formats the same
|
||||
# command string in its native shape — Niri attrs vs Hyprland string-list.
|
||||
# Adding a new DMS keybind requires editing this file plus the
|
||||
# per-compositor bind layout, not the command string.
|
||||
flake.lib.dmsIpcBinds = {
|
||||
spotlight = "dms ipc call spotlight toggle";
|
||||
clipboard = "dms ipc call clipboard toggle";
|
||||
notepad = "dms ipc call notepad toggle";
|
||||
notifications = "dms ipc call notifications toggle";
|
||||
processlist = "dms ipc call processlist toggle";
|
||||
# Hyprland prefers focusOrToggle (window-aware); niri subscribes to toggle.
|
||||
processlistFocus = "dms ipc call processlist focusOrToggle";
|
||||
settingsFocus = "dms ipc call settings focusOrToggle";
|
||||
barToggle = ''dms ipc call bar toggle name "Main Bar"; dms ipc call bar toggle name "Bar 2"'';
|
||||
barToggleBar2 = ''dms ipc call bar toggle name "Bar 2"'';
|
||||
ephemera = "dms ipc call ephemera toggle";
|
||||
wallpaperCarousel = "dms ipc call wallpaperCarousel toggle";
|
||||
lock = "dms ipc call lock lock";
|
||||
};
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,80 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"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"]
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"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,8 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
rt="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
|
||||
exec 9>"$rt/dms-plugin-restart.lock"
|
||||
@FLOCK@ -n 9 || exit 0
|
||||
cache="${XDG_CACHE_HOME:-$HOME/.cache}/quickshell/qmlcache"
|
||||
rm -rf "$cache"
|
||||
@SYSTEMCTL@ --user try-restart dms.service 2>/dev/null || true
|
||||
@@ -0,0 +1,21 @@
|
||||
# Hyprland colors generated by DMS/matugen
|
||||
# Keep `.default` so DMS can swap to `.dark` when "Always use Dark Theme" is enabled.
|
||||
|
||||
general {
|
||||
col.active_border = rgb({{colors.primary.default.hex_stripped}}) rgb({{colors.secondary.default.hex_stripped}}) rgb({{colors.tertiary.default.hex_stripped}})
|
||||
col.inactive_border = rgba({{colors.surface_variant.default.hex_stripped}}ee)
|
||||
}
|
||||
|
||||
decoration {
|
||||
shadow {
|
||||
color = rgba({{colors.shadow.default.hex_stripped}}cc)
|
||||
}
|
||||
}
|
||||
|
||||
# Hyprbars colors
|
||||
plugin {
|
||||
hyprbars {
|
||||
bar_color = rgba({{colors.surface.default.hex_stripped}}ff)
|
||||
col.text = rgba({{colors.on_surface.default.hex_stripped}}ff)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
.theme-dark {
|
||||
--accent-h:calc({{colors.primary.dark.hue}}/255*360);
|
||||
--accent-s:calc({{colors.primary.dark.saturation}}/255*100%);
|
||||
--accent-l:calc({{colors.primary.dark.lightness}}/255*100%);
|
||||
|
||||
--text-normal:{{colors.on_surface.dark.hex}};
|
||||
--text-muted:{{colors.on_surface_variant.dark.hex}};
|
||||
--text-faint:{{colors.outline.dark.hex}};
|
||||
|
||||
--background-primary:{{colors.surface.dark.hex}}; /* editor and right ribbon */
|
||||
--background-secondary:{{colors.surface_dim.dark.hex}}; /* left ribbon */
|
||||
--background-modifier-hover:{{colors.secondary_container.dark.hex}};
|
||||
--background-modifier-active-hover:{{colors.secondary.dark.hex}};
|
||||
--background-modifier-message:{{colors.surface_dim.dark.hex}};
|
||||
--background-modifier-form-field:{{colors.primary_container.dark.hex}};
|
||||
|
||||
--ribbon-background:{{colors.surface.dark.hex}};
|
||||
--divider-color:{{colors.secondary_container.dark.hex}};
|
||||
--scrollbar-thumb-bg:{{colors.on_secondary_container.dark.hex}};
|
||||
--status-bar-border-color:{{colors.secondary_container.dark.hex}};
|
||||
--status-bar-background:{{colors.surface.dark.hex}};
|
||||
--titlebar-background:{{colors.surface.dark.hex}};
|
||||
--titlebar-text-color:{{colors.on_surface.dark.hex}};
|
||||
|
||||
--nav-item-color:{{colors.on_surface_variant.dark.hex}};
|
||||
--nav-item-color-active:{{colors.on_primary_container.dark.hex}};
|
||||
--nav-item-background-active:{{colors.primary_container.dark.hex}};
|
||||
--nav-item-color-hover:{{colors.on_secondary_container.dark.hex}};
|
||||
--nav-item-background-hover:{{colors.secondary_container.dark.hex}};
|
||||
|
||||
--tab-background-active:{{colors.primary_container.dark.hex}};
|
||||
--tab-text-color-active:{{colors.on_primary_container.dark.hex}};
|
||||
--tab-text-color:{{colors.on_surface_variant.dark.hex}};
|
||||
|
||||
--icon-color:{{colors.on_surface.dark.hex}};
|
||||
|
||||
--h1-color:{{colors.primary.dark.hex}};
|
||||
--h2-color: color-mix(in srgb, {{colors.primary.dark.hex}}, {{colors.tertiary.dark.hex}});
|
||||
--h3-color:{{colors.tertiary.dark.hex}};
|
||||
--h4-color: color-mix(in srgb, {{colors.primary.dark.hex}}, {{colors.secondary.dark.hex}});
|
||||
--h5-color:{{colors.secondary.dark.hex}};
|
||||
--h6-color: color-mix(in srgb, {{colors.tertiary.dark.hex}}, {{colors.secondary.dark.hex}});
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* add parts not accessible without specific selector?*/
|
||||
.theme-dark .sidebar-toggle-button.mod-left {
|
||||
color:{{colors.on_surface.dark.hex}};
|
||||
background:{{colors.surface.dark.hex}};
|
||||
}
|
||||
.theme-dark .sidebar-toggle-button.mod-right {
|
||||
color:{{colors.on_surface.dark.hex}};
|
||||
background:{{colors.surface.dark.hex}};
|
||||
}
|
||||
|
||||
.theme-dark .workspace-tab-header-container{
|
||||
background:{{colors.surface.dark.hex}};
|
||||
}
|
||||
|
||||
|
||||
/* copy for light mode */
|
||||
.theme-light {
|
||||
--accent-h:calc({{colors.primary.light.hue}}/255*360);
|
||||
--accent-s:calc({{colors.primary.light.saturation}}/255*100%);
|
||||
--accent-l:calc({{colors.primary.light.lightness}}/255*100%);
|
||||
|
||||
--text-normal:{{colors.on_surface.light.hex}};
|
||||
--text-muted:{{colors.on_surface_variant.light.hex}};
|
||||
--text-faint:{{colors.outline.light.hex}};
|
||||
|
||||
--background-primary:{{colors.surface.light.hex}}; /* editor and right ribbon */
|
||||
--background-secondary:{{colors.surface_dim.light.hex}}; /* left ribbon */
|
||||
--background-modifier-hover:{{colors.secondary_container.light.hex}};
|
||||
--background-modifier-active-hover:{{colors.secondary.light.hex}};
|
||||
--background-modifier-message:{{colors.surface_dim.light.hex}};
|
||||
--background-modifier-form-field:{{colors.primary_container.light.hex}};
|
||||
|
||||
--ribbon-background:{{colors.surface.light.hex}};
|
||||
--divider-color:{{colors.secondary_container.light.hex}};
|
||||
--scrollbar-thumb-bg:{{colors.on_secondary_container.light.hex}};
|
||||
--status-bar-border-color:{{colors.secondary_container.light.hex}};
|
||||
--status-bar-background:{{colors.surface.light.hex}};
|
||||
--titlebar-background:{{colors.surface.light.hex}};
|
||||
--titlebar-text-color:{{colors.on_surface.light.hex}};
|
||||
|
||||
--nav-item-color:{{colors.on_surface_variant.light.hex}};
|
||||
--nav-item-color-active:{{colors.on_primary_container.light.hex}};
|
||||
--nav-item-background-active:{{colors.primary_container.light.hex}};
|
||||
--nav-item-color-hover:{{colors.on_secondary_container.light.hex}};
|
||||
--nav-item-background-hover:{{colors.secondary_container.light.hex}};
|
||||
|
||||
--tab-background-active:{{colors.primary_container.light.hex}};
|
||||
--tab-text-color-active:{{colors.on_primary_container.light.hex}};
|
||||
--tab-text-color:{{colors.on_surface_variant.light.hex}};
|
||||
|
||||
--icon-color:{{colors.on_surface.light.hex}};
|
||||
|
||||
--h1-color:{{colors.primary.light.hex}};
|
||||
--h2-color: color-mix(in srgb, {{colors.primary.light.hex}}, {{colors.tertiary.light.hex}});
|
||||
--h3-color:{{colors.tertiary.light.hex}};
|
||||
--h4-color: color-mix(in srgb, {{colors.primary.light.hex}}, {{colors.secondary.light.hex}});
|
||||
--h5-color:{{colors.secondary.light.hex}};
|
||||
--h6-color: color-mix(in srgb, {{colors.tertiary.light.hex}}, {{colors.secondary.light.hex}});
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* add parts not accessible without specific selector?*/
|
||||
.theme-light .sidebar-toggle-button.mod-left {
|
||||
color:{{colors.on_surface.light.hex}};
|
||||
background:{{colors.surface.light.hex}};
|
||||
}
|
||||
.theme-light .sidebar-toggle-button.mod-right {
|
||||
color:{{colors.on_surface.light.hex}};
|
||||
background:{{colors.surface.light.hex}};
|
||||
}
|
||||
|
||||
.theme-light .workspace-tab-header-container{
|
||||
background:{{colors.surface.light.hex}};
|
||||
}
|
||||
|
||||
/* Hide window controls (frameless titlebar buttons) */
|
||||
div[aria-label="Close window"],
|
||||
div[aria-label="Minimize"],
|
||||
div[aria-label="Restore down"],
|
||||
div[aria-label="Maximize"]{
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Remove the extra right-side spacing reserved for those buttons */
|
||||
.is-hidden-frameless:not(.is-fullscreen) .workspace-tabs.mod-top-right-space .workspace-tab-header-container{
|
||||
padding-right: 0px !important;
|
||||
}
|
||||
|
||||
.is-hidden-frameless:not(.is-fullscreen) .workspace-tabs.mod-top-right-space .workspace-tab-header-container:after{
|
||||
display: none !important;
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/schema.json",
|
||||
"blocks": [
|
||||
{
|
||||
"alignment": "left",
|
||||
"segments": [
|
||||
{
|
||||
"background": "#3A456E",
|
||||
"foreground": "#ffbebc",
|
||||
"leading_diamond": "\ue0b6",
|
||||
"style": "diamond",
|
||||
"template": "\u007B\u007B .UserName \u007D\u007D@\u007B\u007B .HostName \u007D\u007D <#ffffff>on</>",
|
||||
"type": "session"
|
||||
},
|
||||
{
|
||||
"type": "os",
|
||||
"style": "diamond",
|
||||
"background": "#3A456E",
|
||||
"foreground": "#ffffff",
|
||||
"trailing_diamond": "\ue0b4",
|
||||
"template": " \u007B\u007B .Icon \u007D\u007D "
|
||||
},
|
||||
{"type": "session",
|
||||
"style": "diamond",
|
||||
"background": "#3A456E",
|
||||
"foreground": "#ffbebc",
|
||||
"template": "\u007B\u007B if .SSHSession \u007D\u007Dvia SSH\u007B\u007B end \u007D\u007D",
|
||||
"trailing_diamond": "\ue0b4",
|
||||
"type": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"style": "diamond",
|
||||
"background": "#3A456E",
|
||||
"foreground": "#ffbebc",
|
||||
"template": "\u007B\u007B if .Env.IN_NIX_SHELL \u007D\u007Din nix-shell\u007B\u007B end \u007D\u007D",
|
||||
"trailing_diamond": "\ue0b4"
|
||||
},
|
||||
{
|
||||
"background": "#3A456E",
|
||||
"foreground": "#bc93ff",
|
||||
"leading_diamond": "\ue0b6",
|
||||
"properties": {
|
||||
"time_format": "Monday <#ffffff>at</> 3:04 PM"
|
||||
},
|
||||
"style": "diamond",
|
||||
"trailing_diamond": "\ue0b4",
|
||||
"template": "\u007B\u007B .CurrentDate | date .Format \u007D\u007D",
|
||||
"type": "time"
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
"background": "#3A456E",
|
||||
"foreground": "#43CCEA",
|
||||
"foreground_templates": [
|
||||
"\u007B\u007B if or (.Working.Changed) (.Staging.Changed) \u007D\u007D#FF9248\u007B\u007B end \u007D\u007D",
|
||||
"\u007B\u007B if and (gt .Ahead 0) (gt .Behind 0) \u007D\u007D#ff4500\u007B\u007B end \u007D\u007D",
|
||||
"\u007B\u007B if gt .Ahead 0 \u007D\u007D#B388FF\u007B\u007B end \u007D\u007D",
|
||||
"\u007B\u007B if gt .Behind 0 \u007D\u007D#B388FF\u007B\u007B end \u007D\u007D"
|
||||
],
|
||||
"leading_diamond": " \ue0b6",
|
||||
"options": {
|
||||
"branch_template": "\u007B\u007B trunc 25 .Branch \u007D\u007D",
|
||||
"fetch_status": true,
|
||||
"fetch_upstream_icon": true
|
||||
},
|
||||
"style": "diamond",
|
||||
"template": " \u007B\u007B .UpstreamIcon \u007D\u007D\u007B\u007B .HEAD \u007D\u007D\u007B\u007Bif .BranchStatus \u007D\u007D \u007B\u007B .BranchStatus \u007D\u007D\u007B\u007B end \u007D\u007D\u007B\u007B if .Working.Changed \u007D\u007D \uf044 \u007B\u007B .Working.String \u007D\u007D\u007B\u007B end \u007D\u007D\u007B\u007B if and (.Working.Changed) (.Staging.Changed) \u007D\u007D |\u007B\u007B end \u007D\u007D\u007B\u007B if .Staging.Changed \u007D\u007D \uf046 \u007B\u007B .Staging.String \u007D\u007D\u007B\u007B end \u007D\u007D\u007B\u007B if gt .StashCount 0 \u007D\u007D \ueb4b \u007B\u007B .StashCount \u007D\u007D\u007B\u007B end \u007D\u007D ",
|
||||
"trailing_diamond": "\ue0b4",
|
||||
"type": "git"
|
||||
},
|
||||
{
|
||||
"background": "#3A456E",
|
||||
"foreground": "#E4F34A",
|
||||
"leading_diamond": " \ue0b6",
|
||||
"options": {
|
||||
"fetch_version": false
|
||||
},
|
||||
"style": "diamond",
|
||||
"template": "\ue235 \u007B\u007B if .Error \u007D\u007D\u007B\u007B .Error \u007D\u007D\u007B\u007B else \u007D\u007D\u007B\u007B if .Venv \u007D\u007D\u007B\u007B .Venv \u007D\u007D \u007B\u007B end \u007D\u007D\u007B\u007B .Full \u007D\u007D\u007B\u007B end \u007D\u007D",
|
||||
"trailing_diamond": "\ue0b4",
|
||||
"type": "python"
|
||||
},
|
||||
{
|
||||
"background": "#3A456E",
|
||||
"foreground": "#7FD5EA",
|
||||
"leading_diamond": " \ue0b6",
|
||||
"options": {
|
||||
"fetch_version": false
|
||||
},
|
||||
"style": "diamond",
|
||||
"template": "\ue626\u007B\u007B if .Error \u007D\u007D\u007B\u007B .Error \u007D\u007D\u007B\u007B else \u007D\u007D\u007B\u007B .Full \u007D\u007D\u007B\u007B end \u007D\u007D",
|
||||
"trailing_diamond": "\ue0b4",
|
||||
"type": "go"
|
||||
},
|
||||
{
|
||||
"background": "#3A456E",
|
||||
"foreground": "#42E66C",
|
||||
"leading_diamond": " \ue0b6",
|
||||
"options": {
|
||||
"fetch_version": false
|
||||
},
|
||||
"style": "diamond",
|
||||
"template": "\ue718\u007B\u007B if .PackageManagerIcon \u007D\u007D\u007B\u007B .PackageManagerIcon \u007D\u007D \u007B\u007B end \u007D\u007D\u007B\u007B .Full \u007D\u007D",
|
||||
"trailing_diamond": "\ue0b4",
|
||||
"type": "node"
|
||||
},
|
||||
{
|
||||
"background": "#3A456E",
|
||||
"foreground": "#E64747",
|
||||
"leading_diamond": " \ue0b6",
|
||||
"options": {
|
||||
"fetch_version": false
|
||||
},
|
||||
"style": "diamond",
|
||||
"template": "\ue791\u007B\u007B if .Error \u007D\u007D\u007B\u007B .Error \u007D\u007D\u007B\u007B else \u007D\u007D\u007B\u007B .Full \u007D\u007D\u007B\u007B end \u007D\u007D",
|
||||
"trailing_diamond": "\ue0b4",
|
||||
"type": "ruby"
|
||||
},
|
||||
{
|
||||
"background": "#3A456E",
|
||||
"foreground": "#E64747",
|
||||
"leading_diamond": " \ue0b6",
|
||||
"options": {
|
||||
"fetch_version": false
|
||||
},
|
||||
"style": "diamond",
|
||||
"template": "\ue738\u007B\u007B if .Error \u007D\u007D\u007B\u007B .Error \u007D\u007D\u007B\u007B else \u007D\u007D\u007B\u007B .Full \u007D\u007D\u007B\u007B end \u007D\u007D",
|
||||
"trailing_diamond": "\ue0b4",
|
||||
"type": "java"
|
||||
},
|
||||
{
|
||||
"background": "#3A456E",
|
||||
"foreground": "#9B6BDF",
|
||||
"leading_diamond": " \ue0b6",
|
||||
"options": {
|
||||
"fetch_version": false
|
||||
},
|
||||
"style": "diamond",
|
||||
"template": "\ue624\u007B\u007B if .Error \u007D\u007D\u007B\u007B .Error \u007D\u007D\u007B\u007B else \u007D\u007D\u007B\u007B .Full \u007D\u007D\u007B\u007B end \u007D\u007D ",
|
||||
"trailing_diamond": "\ue0b4",
|
||||
"type": "julia"
|
||||
},
|
||||
{
|
||||
"type": "php",
|
||||
"style": "diamond",
|
||||
"foreground": "#ffffff",
|
||||
"background": "#4063D8",
|
||||
"leading_diamond": " \ue0b6",
|
||||
"options": {
|
||||
"fetch_version": false
|
||||
},
|
||||
"template": "\ue73d \u007B\u007B .Full \u007D\u007D ",
|
||||
"trailing_diamond": "\ue0b4"
|
||||
},
|
||||
{
|
||||
"background": "#3A456E",
|
||||
"foreground": "#9B6BDF",
|
||||
"foreground_templates": [
|
||||
"\u007B\u007Bif eq \"Charging\" .State.String\u007D\u007D#40c4ff\u007B\u007Bend\u007D\u007D",
|
||||
"\u007B\u007Bif eq \"Discharging\" .State.String\u007D\u007D#ff5722\u007B\u007Bend\u007D\u007D",
|
||||
"\u007B\u007Bif eq \"Full\" .State.String\u007D\u007D#4caf50\u007B\u007Bend\u007D\u007D"
|
||||
],
|
||||
"leading_diamond": " \ue0b6",
|
||||
"options": {
|
||||
"charged_icon": " ",
|
||||
"charging_icon": "\u21e1 ",
|
||||
"discharging_icon": "\u21e3 "
|
||||
},
|
||||
"style": "diamond",
|
||||
"template": "\u007B\u007B if not .Error \u007D\u007D\u007B\u007B .Icon \u007D\u007D\u007B\u007B .Percentage \u007D\u007D\u007B\u007B end \u007D\u007D\u007B\u007B .Error \u007D\u007D",
|
||||
"trailing_diamond": "\ue0b4",
|
||||
"type": "battery"
|
||||
}
|
||||
],
|
||||
"type": "prompt"
|
||||
},
|
||||
{
|
||||
"alignment": "right",
|
||||
"segments": [
|
||||
{
|
||||
"background": "#3A456E",
|
||||
"foreground": "#AEA4BF",
|
||||
"leading_diamond": "\ue0b6",
|
||||
"options": {
|
||||
"style": "austin",
|
||||
"threshold": 150
|
||||
},
|
||||
"style": "diamond",
|
||||
"template": "\u007B\u007B .FormattedMs \u007D\u007D",
|
||||
"trailing_diamond": "\ue0b4 ",
|
||||
"type": "executiontime"
|
||||
}
|
||||
],
|
||||
"type": "prompt"
|
||||
},
|
||||
{
|
||||
"alignment": "left",
|
||||
"newline": true,
|
||||
"segments": [
|
||||
{
|
||||
"background": "#3A456E",
|
||||
"foreground": "#3EC669",
|
||||
"leading_diamond": "\ue0b6",
|
||||
"properties": {
|
||||
"style": "agnoster_full",
|
||||
"cycle": [
|
||||
"#3EC669,#3A456E",
|
||||
"#43CCEA,#3A456E",
|
||||
"#E4F34A,#3A456E",
|
||||
"#9B6BDF,#3A456E"
|
||||
],
|
||||
"cycle_folder_separator": true
|
||||
},
|
||||
"style": "diamond",
|
||||
"template": "\ue5ff \u007B\u007B .Path \u007D\u007D",
|
||||
"trailing_diamond": "\ue0b4",
|
||||
"type": "path"
|
||||
},
|
||||
{
|
||||
"background": "#3A456E",
|
||||
"foreground": "#ffbebc",
|
||||
"leading_diamond": "\ue0b6",
|
||||
"style": "diamond",
|
||||
"template": "\ue602",
|
||||
"trailing_diamond": "\ue0b4",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"type": "prompt"
|
||||
}
|
||||
],
|
||||
"final_space": true,
|
||||
"version": 4
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
guiEnabled = d.hyprland.enable || d.niri.enable || d.plasma.enable;
|
||||
|
||||
wallpaperDir = pkgs.stdenvNoCC.mkDerivation {
|
||||
pname = "nixos-v2-wallpapers";
|
||||
pname = "chiasson-nix-wallpapers";
|
||||
version = "0.1";
|
||||
src = builtins.path {
|
||||
path = cfg.source;
|
||||
@@ -26,7 +26,7 @@
|
||||
{
|
||||
options.chiasson.desktop.wallpapers = {
|
||||
enable = lib.mkEnableOption ''
|
||||
Copy `source` into the store; sets `NIXOS_V2_WALLPAPERS` and `/etc/wallpapers`.
|
||||
Copy `source` into the store; sets `CHIASSON_NIX_WALLPAPERS` and `/etc/wallpapers`.
|
||||
'' // {
|
||||
default = true;
|
||||
};
|
||||
@@ -43,8 +43,8 @@
|
||||
|
||||
config = lib.mkIf (guiEnabled && cfg.enable) {
|
||||
environment = {
|
||||
variables.NIXOS_V2_WALLPAPERS = installPath;
|
||||
sessionVariables.NIXOS_V2_WALLPAPERS = installPath;
|
||||
variables.CHIASSON_NIX_WALLPAPERS = installPath;
|
||||
sessionVariables.CHIASSON_NIX_WALLPAPERS = installPath;
|
||||
etc."wallpapers".source = installPath;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
{ inputs, lib, ... }:
|
||||
{
|
||||
perSystem =
|
||||
{
|
||||
pkgs,
|
||||
system,
|
||||
...
|
||||
}:
|
||||
lib.optionalAttrs (lib.elem system [
|
||||
"x86_64-linux"
|
||||
"aarch64-linux"
|
||||
]) {
|
||||
devShells.default = pkgs.mkShell {
|
||||
packages = [
|
||||
inputs.navi.packages.${system}.default
|
||||
pkgs.just
|
||||
pkgs.jq
|
||||
];
|
||||
shellHook = ''
|
||||
echo "Repo helpers (from repo root):"
|
||||
echo " just # list justfile recipes"
|
||||
echo " just sync-dms [--dry-run] # copy live DMS config into host seed dir"
|
||||
echo ""
|
||||
echo "Navi fleet deploy:"
|
||||
echo " navi apply --on <host> # build + switch one host"
|
||||
echo " navi apply-local --node 14900k --sudo # switch locally (needs root)"
|
||||
echo " navi tui # interactive fleet dashboard"
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{config, inputs, pkgs, ...}: {
|
||||
imports = [
|
||||
inputs.hermes.nixosModules.default
|
||||
];
|
||||
services.ollama = {
|
||||
enable = true;
|
||||
# Replace the old acceleration line with this:
|
||||
package = pkgs.ollama-cuda;
|
||||
};
|
||||
chiasson.users.extraModules.olivier = [{chiasson.home.extraPackages = with pkgs; [ zed-editor devin-desktop];}];
|
||||
}
|
||||
@@ -1,13 +1,33 @@
|
||||
# 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`).
|
||||
# Monitor layout for 14900k.
|
||||
# NVIDIA (default): DP-3 ultrawide, HDMI-A-3 + DP-4 stack; DP-2 virtual 4K when streamingDisplay is on.
|
||||
# Intel iGPU (gpu passthru): DP-1 ultrawide, HDMI-A-2 to the left.
|
||||
# Niri uses `extraSettings.extraConfig`; Hyprland uses `chiasson.desktop.hyprland.settings`.
|
||||
|
||||
#TODO[epic=Moderate] Clean this up, move to host's configuration.nix.
|
||||
{ config, lib, ... }:
|
||||
{
|
||||
chiasson.desktop.niri.extraSettings = {
|
||||
extraConfig = ''
|
||||
output "DP-2" {
|
||||
let
|
||||
streamingDisplay = config.chiasson.system.streamingDisplay;
|
||||
gpuPassthru = config.chiasson.system.gpuPassthru.enable;
|
||||
|
||||
# Niri outputs: a static block per GPU layout, plus the optional virtual 4K panel.
|
||||
niriOutputs =
|
||||
(if gpuPassthru then
|
||||
''
|
||||
output "DP-1" {
|
||||
mode "2560x1080@144"
|
||||
scale 1.0
|
||||
position x=0 y=0
|
||||
focus-at-startup
|
||||
}
|
||||
output "HDMI-A-2" {
|
||||
mode "1920x1080@60"
|
||||
scale 1.0
|
||||
position x=-1920 y=0
|
||||
}
|
||||
''
|
||||
else
|
||||
''
|
||||
output "DP-3" {
|
||||
mode "2560x1080@144"
|
||||
scale 1.0
|
||||
position x=0 y=0
|
||||
@@ -23,39 +43,43 @@
|
||||
scale 1.0
|
||||
position x=0 y=-1080
|
||||
}
|
||||
|
||||
'')
|
||||
+ lib.optionalString streamingDisplay.enable ''
|
||||
output "${streamingDisplay.connector}" {
|
||||
mode "${streamingDisplay.mode}"
|
||||
scale ${toString streamingDisplay.niriScale}
|
||||
position ${streamingDisplay.niriPosition}
|
||||
}
|
||||
'';
|
||||
|
||||
binds."XF86Tools".spawn = [
|
||||
"wpctl"
|
||||
"set-mute"
|
||||
"@DEFAULT_AUDIO_SOURCE@"
|
||||
"toggle"
|
||||
];
|
||||
# Hyprland monitor strings, with the virtual panel appended when enabled.
|
||||
hyprlandMonitors =
|
||||
(if gpuPassthru then
|
||||
[ "DP-1, 2560x1080@144, 0x0, 1" "HDMI-A-2, 1920x1080@60, -1920x0, 1" ]
|
||||
else
|
||||
[ "DP-3, 2560x1080@144, 0x0, 1" "DP-4, 1920x1080@144, 0x-1080, 1" "HDMI-A-3, 1920x1080@60, -1920x0, 1" ])
|
||||
++ lib.optional streamingDisplay.enable "${streamingDisplay.connector}, ${streamingDisplay.mode}, 7680x0, ${toString streamingDisplay.niriScale}";
|
||||
|
||||
hyprlandWorkspaces =
|
||||
if gpuPassthru then
|
||||
[ "1, monitor:DP-1, default:true" "2, monitor:DP-1" "3, monitor:DP-1"
|
||||
"4, monitor:HDMI-A-2, default:true" "5, monitor:HDMI-A-2" "6, monitor:HDMI-A-2" ]
|
||||
else
|
||||
[ "1, monitor:DP-3, default:true" "2, monitor:DP-3" "3, monitor:DP-3"
|
||||
"4, monitor:HDMI-A-3, default:true" "5, monitor:HDMI-A-3" "6, monitor:HDMI-A-3"
|
||||
"7, monitor:DP-4" "8, monitor:DP-4" "9, monitor:DP-4" ];
|
||||
in
|
||||
{
|
||||
chiasson.desktop.niri.extraSettings = {
|
||||
extraConfig = niriOutputs;
|
||||
};
|
||||
|
||||
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"
|
||||
chiasson.desktop.niri.extraBinds."XF86Tools".spawn = [
|
||||
"wpctl" "set-mute" "@DEFAULT_AUDIO_SOURCE@" "toggle"
|
||||
];
|
||||
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;
|
||||
}
|
||||
);
|
||||
|
||||
chiasson.desktop.hyprland.settings = lib.mkIf config.chiasson.desktop.hyprland.enable {
|
||||
monitor = lib.mkBefore hyprlandMonitors;
|
||||
workspace = hyprlandWorkspaces;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
{
|
||||
"dankDesktopWeather": {
|
||||
"enabled": true
|
||||
},
|
||||
"nixMonitor": {
|
||||
"enabled": true
|
||||
},
|
||||
"newClock": {
|
||||
"enabled": true,
|
||||
"clockStyle": "digital",
|
||||
"showSeconds": true,
|
||||
"showDate": true
|
||||
},
|
||||
"rbwLockToggle": {
|
||||
"enabled": true
|
||||
},
|
||||
"ambientSound": {
|
||||
"enabled": true,
|
||||
"whenDoneActions": [],
|
||||
"soundVolumes": {
|
||||
"acoustic-guitar": 100,
|
||||
"warm-piano": 100,
|
||||
"lofi-beats": 100
|
||||
}
|
||||
},
|
||||
"bongoCat": {
|
||||
"enabled": true,
|
||||
"selectedDevicePath": "/dev/input/event4"
|
||||
},
|
||||
"calculator": {
|
||||
"enabled": true,
|
||||
"trigger": "="
|
||||
},
|
||||
"dankGifSearch": {
|
||||
"enabled": false
|
||||
},
|
||||
"homeAssistantMonitor": {
|
||||
"enabled": true,
|
||||
"hassUrl": "https://home.chiasson.cloud/",
|
||||
"hassTokenPath": "/run/secrets/home-assistant/auth-token",
|
||||
"showButtonsOnStatusBar": true,
|
||||
"showAttributes": false,
|
||||
"haRefreshCounter": 1
|
||||
},
|
||||
"unifiedTaskbar": {
|
||||
"enabled": true,
|
||||
"allMonitors": false,
|
||||
"reverseMonitorOrder": false,
|
||||
"groupByApp": false,
|
||||
"compactMode": false,
|
||||
"filledPills": false,
|
||||
"iconPadding": 4,
|
||||
"itemSpacing": 2
|
||||
},
|
||||
"widgetGroup": {
|
||||
"enabled": true,
|
||||
"variants": [
|
||||
{
|
||||
"icon": "widgets",
|
||||
"label": "",
|
||||
"display": "both",
|
||||
"expandDir": "right",
|
||||
"targets": [
|
||||
"rbwLockToggle"
|
||||
],
|
||||
"mainTarget": "",
|
||||
"mainClickButton": "right",
|
||||
"mainMarkerColor": "primary",
|
||||
"expandIndicatorPosition": "",
|
||||
"showArrow": false,
|
||||
"showArrowOnlyOnHover": false,
|
||||
"hideMain": false,
|
||||
"id": "variant_1781997418516",
|
||||
"name": "Test",
|
||||
"autoCollapse": false,
|
||||
"autoCollapseSeconds": 5,
|
||||
"autoCollapseOnLeave": false
|
||||
},
|
||||
{
|
||||
"icon": "bug_report",
|
||||
"label": "",
|
||||
"display": "both",
|
||||
"expandDir": "right",
|
||||
"targets": [
|
||||
"nixMonitor",
|
||||
"cpuUsage",
|
||||
"memUsage",
|
||||
"diskUsage",
|
||||
"cpuTemp",
|
||||
"gpuTemp"
|
||||
],
|
||||
"mainTarget": "cpuUsage",
|
||||
"mainClickButton": "right",
|
||||
"mainMarkerColor": "primary",
|
||||
"expandIndicatorPosition": "",
|
||||
"showArrow": false,
|
||||
"showArrowOnlyOnHover": false,
|
||||
"hideMain": false,
|
||||
"id": "variant_1781999113601",
|
||||
"name": "SysInfo",
|
||||
"autoCollapse": false,
|
||||
"autoCollapseSeconds": 5,
|
||||
"autoCollapseOnLeave": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"desktopWidgetToggle": {
|
||||
"enabled": false,
|
||||
"activeGroupIds": [],
|
||||
"activeGroupId": "",
|
||||
"groups": [
|
||||
{
|
||||
"id": "g1",
|
||||
"name": "Group 1",
|
||||
"icon": "widgets",
|
||||
"widgets": [],
|
||||
"overrideIndividual": false
|
||||
},
|
||||
{
|
||||
"id": "g_1781998217464_d0ngn3bo3",
|
||||
"name": "New Group",
|
||||
"icon": "widgets",
|
||||
"widgets": [],
|
||||
"overrideIndividual": true,
|
||||
"widgetOverrides": {}
|
||||
}
|
||||
],
|
||||
"conflictMode": "single"
|
||||
},
|
||||
"dropdownMenu": {
|
||||
"enabled": true,
|
||||
"variants": [
|
||||
{
|
||||
"icon": "expand_circle_down",
|
||||
"text": "",
|
||||
"items": [
|
||||
{
|
||||
"type": "popout",
|
||||
"icon": "",
|
||||
"label": "Home Assistant",
|
||||
"display": "both",
|
||||
"widgetId": "homeAssistantMonitor"
|
||||
}
|
||||
],
|
||||
"id": "variant_1781998267426",
|
||||
"name": "Main Drop"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ocrScanner": {
|
||||
"enabled": true
|
||||
},
|
||||
"obsidianSearch": {
|
||||
"enabled": true,
|
||||
"vaultPath": "/mnt/zimaos/Obsidian/Home/",
|
||||
"isFlatpak": true
|
||||
},
|
||||
"dankObsidian": {
|
||||
"enabled": false
|
||||
},
|
||||
"dankPinentry": {
|
||||
"enabled": true
|
||||
},
|
||||
"dankVault": {
|
||||
"enabled": true,
|
||||
"backend": "rbw"
|
||||
},
|
||||
"nixPackageRunner": {
|
||||
"enabled": true,
|
||||
"runInTerminal": false
|
||||
},
|
||||
"dmsconky": {
|
||||
"enabled": true
|
||||
},
|
||||
"discordVoice": {
|
||||
"enabled": false
|
||||
},
|
||||
"emojiLauncher": {
|
||||
"enabled": true,
|
||||
"trigger": ":",
|
||||
"pasteOnSelect": true,
|
||||
"recentEmojis": "🃏"
|
||||
},
|
||||
"webSearch": {
|
||||
"enabled": true,
|
||||
"searchEngines": [],
|
||||
"disabledEngines": [],
|
||||
"trigger": "#"
|
||||
},
|
||||
"wallpaperCarousel": {
|
||||
"enabled": true,
|
||||
"cacheSize": 30,
|
||||
"holdDelay": 1206
|
||||
},
|
||||
"ephemera": {
|
||||
"enabled": true,
|
||||
"provider": "gemini",
|
||||
"model": "gemini-3-flash-preview"
|
||||
},
|
||||
"aiAssistant": {
|
||||
"enabled": true,
|
||||
"providers": {
|
||||
"openai": {
|
||||
"baseUrl": "https://api.openai.com",
|
||||
"model": "gpt-5.2",
|
||||
"apiKey": "",
|
||||
"saveApiKey": false,
|
||||
"apiKeyEnvVar": "",
|
||||
"temperature": 0.7,
|
||||
"maxTokens": 4096,
|
||||
"timeout": 30
|
||||
},
|
||||
"anthropic": {
|
||||
"baseUrl": "https://api.anthropic.com",
|
||||
"model": "claude-sonnet-4-5",
|
||||
"apiKey": "",
|
||||
"saveApiKey": false,
|
||||
"apiKeyEnvVar": "",
|
||||
"temperature": 0.7,
|
||||
"maxTokens": 4096,
|
||||
"timeout": 30
|
||||
},
|
||||
"gemini": {
|
||||
"baseUrl": "https://generativelanguage.googleapis.com",
|
||||
"model": "gemini-3-flash-preview",
|
||||
"apiKey": "",
|
||||
"saveApiKey": false,
|
||||
"apiKeyEnvVar": "",
|
||||
"temperature": 0.7,
|
||||
"maxTokens": 4096,
|
||||
"timeout": 30,
|
||||
"geminiWebSearch": false
|
||||
},
|
||||
"inception": {
|
||||
"baseUrl": "https://api.inceptionlabs.ai/v1",
|
||||
"model": "mercury-2",
|
||||
"apiKey": "",
|
||||
"saveApiKey": false,
|
||||
"apiKeyEnvVar": "",
|
||||
"temperature": 0.75,
|
||||
"maxTokens": 8192,
|
||||
"timeout": 30,
|
||||
"inceptionReasoningEffort": "medium",
|
||||
"inceptionReasoningSummary": true,
|
||||
"inceptionReasoningSummaryWait": false
|
||||
},
|
||||
"ollama": {
|
||||
"baseUrl": "http://localhost:11434",
|
||||
"model": "",
|
||||
"apiKey": "",
|
||||
"saveApiKey": false,
|
||||
"apiKeyEnvVar": "",
|
||||
"temperature": 0.7,
|
||||
"maxTokens": 4096,
|
||||
"timeout": 30
|
||||
},
|
||||
"custom": {
|
||||
"baseUrl": "https://api.openai.com",
|
||||
"model": "gpt-5.2",
|
||||
"apiKey": "",
|
||||
"saveApiKey": false,
|
||||
"apiKeyEnvVar": "",
|
||||
"temperature": 0.7,
|
||||
"maxTokens": 4096,
|
||||
"timeout": 30
|
||||
}
|
||||
},
|
||||
"provider": "openai",
|
||||
"baseUrl": "https://api.openai.com",
|
||||
"model": "gpt-5.2",
|
||||
"apiKey": "",
|
||||
"saveApiKey": false,
|
||||
"apiKeyEnvVar": "",
|
||||
"temperature": 0.7,
|
||||
"maxTokens": 4096,
|
||||
"timeout": 30,
|
||||
"geminiWebSearch": false
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,191 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate a virtual-display EDID with 4K CTA VICs (trimmed from EnriqueWood POC)."""
|
||||
|
||||
import math
|
||||
import sys
|
||||
|
||||
|
||||
def cvt_rb(w, h, refresh):
|
||||
hblank, hfront, hsync = 160, 48, 32
|
||||
htotal = w + hblank
|
||||
vfront, vsync = 3, 8
|
||||
rb_min_vblank = 460e-6
|
||||
vblank = math.ceil(rb_min_vblank * refresh * h / (1.0 - rb_min_vblank * refresh))
|
||||
vblank = max(vblank, vfront + vsync + 6)
|
||||
vtotal = h + vblank
|
||||
pixclk_khz = int(round(htotal * vtotal * refresh / 10000.0)) * 10
|
||||
return dict(
|
||||
w=w,
|
||||
h=h,
|
||||
refresh=refresh,
|
||||
htotal=htotal,
|
||||
vtotal=vtotal,
|
||||
hblank=hblank,
|
||||
hfront=hfront,
|
||||
hsync=hsync,
|
||||
vblank=vblank,
|
||||
vfront=vfront,
|
||||
vsync=vsync,
|
||||
pixclk_khz=pixclk_khz,
|
||||
)
|
||||
|
||||
|
||||
def make_dtd(t, hmm=600, vmm=340):
|
||||
w, h = t["w"], t["h"]
|
||||
hblank, vblank = t["hblank"], t["vblank"]
|
||||
hfront, hsync = t["hfront"], t["hsync"]
|
||||
vfront, vsync = t["vfront"], t["vsync"]
|
||||
pc = t["pixclk_khz"] // 10
|
||||
d = bytearray(18)
|
||||
d[0] = pc & 0xFF
|
||||
d[1] = (pc >> 8) & 0xFF
|
||||
d[2] = w & 0xFF
|
||||
d[3] = hblank & 0xFF
|
||||
d[4] = ((w >> 4) & 0xF0) | ((hblank >> 8) & 0x0F)
|
||||
d[5] = h & 0xFF
|
||||
d[6] = vblank & 0xFF
|
||||
d[7] = ((h >> 4) & 0xF0) | ((vblank >> 8) & 0x0F)
|
||||
d[8] = hfront & 0xFF
|
||||
d[9] = hsync & 0xFF
|
||||
d[10] = ((vfront & 0x0F) << 4) | (vsync & 0x0F)
|
||||
d[11] = (
|
||||
(((hfront >> 8) & 3) << 6)
|
||||
| (((hsync >> 8) & 3) << 4)
|
||||
| (((vfront >> 4) & 3) << 2)
|
||||
| ((vsync >> 4) & 3)
|
||||
)
|
||||
d[12] = hmm & 0xFF
|
||||
d[13] = vmm & 0xFF
|
||||
d[14] = ((hmm >> 4) & 0xF0) | ((vmm >> 8) & 0x0F)
|
||||
d[17] = 0x1E
|
||||
return bytes(d)
|
||||
|
||||
|
||||
def make_name(name):
|
||||
d = bytearray(18)
|
||||
d[3] = 0xFC
|
||||
nb = name.encode("ascii")[:13]
|
||||
d[5 : 5 + len(nb)] = nb
|
||||
pos = 5 + len(nb)
|
||||
if pos < 18:
|
||||
d[pos] = 0x0A
|
||||
pos += 1
|
||||
for i in range(pos, 18):
|
||||
d[i] = 0x20
|
||||
return bytes(d)
|
||||
|
||||
|
||||
def make_range(min_v, max_v, min_h, max_h, max_pixclk_mhz):
|
||||
d = bytearray(18)
|
||||
d[3] = 0xFD
|
||||
flags = 0
|
||||
mv, mh = max_v, max_h
|
||||
if mh > 255:
|
||||
flags |= 0x08
|
||||
mh -= 255
|
||||
if mv > 255:
|
||||
flags |= 0x02
|
||||
mv -= 255
|
||||
d[4] = flags
|
||||
d[5] = min_v
|
||||
d[6] = mv
|
||||
d[7] = min_h
|
||||
d[8] = mh
|
||||
d[9] = max_pixclk_mhz // 10
|
||||
d[10] = 0x01
|
||||
for i in range(11, 18):
|
||||
d[i] = 0x0A if i == 11 else 0x20
|
||||
return bytes(d)
|
||||
|
||||
|
||||
def checksum(block):
|
||||
return (256 - (sum(block[:-1]) % 256)) % 256
|
||||
|
||||
|
||||
VICS = [97, 118, 96, 95, 63, 16, 4]
|
||||
|
||||
|
||||
def build_base(base_dtds, name, max_pixclk_mhz):
|
||||
b = bytearray(128)
|
||||
b[0:8] = b"\x00\xFF\xFF\xFF\xFF\xFF\xFF\x00"
|
||||
b[8:10] = b"\x50\x74"
|
||||
b[10:12] = b"\x70\x02"
|
||||
b[16] = 22
|
||||
b[17] = 34
|
||||
b[18] = 1
|
||||
b[19] = 4
|
||||
b[20] = 0xA5
|
||||
b[21] = 60
|
||||
b[22] = 34
|
||||
b[23] = 120
|
||||
b[24] = 0x2E
|
||||
b[25:35] = bytes([0x35, 0x85, 0xA6, 0x56, 0x48, 0x9A, 0x24, 0x12, 0x50, 0x54])
|
||||
for i in range(8):
|
||||
b[38 + i * 2] = 0x01
|
||||
b[39 + i * 2] = 0x01
|
||||
b[54:72] = make_dtd(base_dtds[0])
|
||||
b[72:90] = make_dtd(base_dtds[1])
|
||||
b[90:108] = make_range(24, 240, 30, 510, max_pixclk_mhz)
|
||||
b[108:126] = make_name(name)
|
||||
b[126] = 1
|
||||
b[127] = checksum(b)
|
||||
return b
|
||||
|
||||
|
||||
def build_cta_primary(dtds):
|
||||
b = bytearray(128)
|
||||
b[0] = 0x02
|
||||
b[1] = 0x03
|
||||
pos = 4
|
||||
b[pos] = (2 << 5) | len(VICS)
|
||||
for i, v in enumerate(VICS):
|
||||
b[pos + 1 + i] = v
|
||||
pos += 1 + len(VICS)
|
||||
b[pos] = (1 << 5) | 3
|
||||
b[pos + 1], b[pos + 2], b[pos + 3] = 0x17, 0x7F, 0x07
|
||||
pos += 4
|
||||
b[pos] = (4 << 5) | 3
|
||||
b[pos + 1] = 0x4F
|
||||
pos += 4
|
||||
b[2] = pos
|
||||
b[3] = 0x40
|
||||
used, p = [], pos
|
||||
for t in dtds:
|
||||
if p + 18 > 127:
|
||||
break
|
||||
b[p : p + 18] = make_dtd(t)
|
||||
used.append(t)
|
||||
p += 18
|
||||
b[127] = checksum(b)
|
||||
return b, used
|
||||
|
||||
|
||||
def build(custom, name, max_pixclk_mhz=1200):
|
||||
if len(custom) < 2:
|
||||
custom = custom + custom[:1] * (2 - len(custom))
|
||||
base = build_base(custom[0:2], name, max_pixclk_mhz)
|
||||
rest = custom[2:]
|
||||
cta1, _used = build_cta_primary(rest)
|
||||
base[126] = 1
|
||||
base[127] = checksum(base)
|
||||
return bytes(base + cta1)
|
||||
|
||||
|
||||
def main():
|
||||
output = sys.argv[1]
|
||||
custom = [
|
||||
cvt_rb(3840, 2160, 60),
|
||||
cvt_rb(2560, 1440, 60),
|
||||
cvt_rb(1920, 1080, 60),
|
||||
]
|
||||
edid = build(custom, "SUN-4K")
|
||||
with open(output, "wb") as f:
|
||||
f.write(edid)
|
||||
assert len(edid) % 128 == 0
|
||||
for i in range(len(edid) // 128):
|
||||
block = edid[i * 128 : (i + 1) * 128]
|
||||
assert sum(block) % 256 == 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,71 @@
|
||||
# 14900k: RTX 2070 VFIO passthrough + Windows 11 gaming VM on Crucial MX500.
|
||||
{ config, lib, ... }:
|
||||
let
|
||||
cfg = config.chiasson.system.gpuPassthru;
|
||||
in
|
||||
{
|
||||
chiasson.system.gpuPassthru = {
|
||||
enable = false;
|
||||
pciIds = [
|
||||
"10de:1f07" # RTX 2070 VGA
|
||||
"10de:10f9" # HDMI audio
|
||||
"10de:1ada" # USB 3.1
|
||||
"10de:1adb" # USB-C UCSI
|
||||
"14c3:7927" # MediaTek MT7927 WiFi (M.2, no Linux driver — Windows only)
|
||||
];
|
||||
windowsDisk = "/dev/disk/by-id/ata-CT1000MX500SSD1_1830E14B054A";
|
||||
lookingGlass = {
|
||||
enable = true;
|
||||
shmSizeMiB = 32;
|
||||
captureOnFocus = false;
|
||||
};
|
||||
vm = {
|
||||
enable = true;
|
||||
name = "win11";
|
||||
memoryGiB = 16;
|
||||
vcpus = 8;
|
||||
installMode = false;
|
||||
};
|
||||
libvirtUsers = [ "olivier" ];
|
||||
};
|
||||
|
||||
chiasson.desktop.niri.extraBinds = lib.mkIf (cfg.enable && cfg.lookingGlass.enable) {
|
||||
"Mod+J"."spawn-sh" = "looking-glass-toggle";
|
||||
};
|
||||
|
||||
# Applied last in configuration.nix imports — wins over system module defaults.
|
||||
virtualisation.libvirtd.qemu.verbatimConfig = lib.mkIf (cfg.lookingGlass.enable) (
|
||||
lib.mkForce ''
|
||||
namespaces = []
|
||||
seccomp_sandbox = 0
|
||||
security_driver = "none"
|
||||
security_default_confined = 0
|
||||
security_require_confined = 0
|
||||
cgroup_device_acl = [
|
||||
"/dev/null", "/dev/full", "/dev/zero",
|
||||
"/dev/random", "/dev/urandom",
|
||||
"/dev/ptmx", "/dev/kvm", "/dev/kqemu",
|
||||
"/dev/rtc", "/dev/hpet",
|
||||
"/dev/vfio/", "/dev/vfio/vfio",
|
||||
"/dev/kvmfr0", "c 234:* rwm",
|
||||
"/dev/disk/", "/dev/disk/by-id/",
|
||||
"/dev/bus/usb/",
|
||||
]
|
||||
''
|
||||
);
|
||||
|
||||
virtualisation.libvirt = lib.mkIf (cfg.enable && cfg.vm.enable) {
|
||||
enable = true;
|
||||
swtpm.enable = true;
|
||||
connections."qemu:///system".domains = [
|
||||
{
|
||||
definition =
|
||||
if cfg.vm.installMode then
|
||||
./win11-domain-install.xml
|
||||
else
|
||||
./win11-domain.xml;
|
||||
active = false;
|
||||
}
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
# 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.
|
||||
@@ -33,15 +29,4 @@ in
|
||||
# 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,107 @@
|
||||
{ pkgs, inputs, ... }:
|
||||
# All MediaTek MT7927 / MT6639 (Filogic 380) WiFi 7 + Bluetooth support for this
|
||||
# host in one place: the upstream flake module, the hardware enablement, and the
|
||||
# workarounds needed to actually make it function on nixos-unstable.
|
||||
#
|
||||
# Workaround background: on current nixos-unstable, `boot.extraModulePackages`
|
||||
# modules placed in `extra/` are NOT indexed by `depmod` (modules.dep /
|
||||
# modules.alias contain zero `extra/` entries). As a result `modprobe`/udev
|
||||
# resolve mt7925e / btusb to the stock in-tree drivers, which do not support the
|
||||
# MediaTek MT7927 (14c3:7927). We therefore force the patched out-of-tree modules
|
||||
# to load by explicit path, blacklist the in-tree collisions so udev cannot bind
|
||||
# them first, and fix the BT firmware path the driver expects.
|
||||
let
|
||||
# The upstream mt7927 flake installs the BT firmware under
|
||||
# `mediatek/mt6639/`, but the patched btmtk driver requests it from
|
||||
# `mediatek/mt7927/`. Provide it at the expected path.
|
||||
mt7927BtFirmwareFix = pkgs.runCommand "mt7927-bt-firmware-fix" { } ''
|
||||
src="${inputs.mt7927.packages.${pkgs.system}.firmware}/lib/firmware/mediatek/mt6639/BT_RAM_CODE_MT6639_2_1_hdr.bin"
|
||||
install -Dm644 "$src" \
|
||||
"$out/lib/firmware/mediatek/mt7927/BT_RAM_CODE_MT6639_2_1_hdr.bin"
|
||||
'';
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
inputs.mt7927.nixosModules.default
|
||||
];
|
||||
|
||||
hardware.mediatek-mt7927 = {
|
||||
enable = true;
|
||||
enableWifi = true;
|
||||
enableBluetooth = true;
|
||||
disableAspm = true; # Highly recommended to fix upload speeds
|
||||
};
|
||||
|
||||
hardware.firmware = [ mt7927BtFirmwareFix ];
|
||||
|
||||
boot.blacklistedKernelModules = [
|
||||
# Whole mt76 stack (patched extra/ copy replaces it entirely).
|
||||
"mt76"
|
||||
"mt76_connac_lib"
|
||||
"mt792x_lib"
|
||||
"mt7921_common"
|
||||
"mt7921e"
|
||||
"mt7925_common"
|
||||
"mt7925e"
|
||||
# Patched bluetooth.
|
||||
"btusb"
|
||||
"btmtk"
|
||||
];
|
||||
|
||||
systemd.services.mt7927-force = {
|
||||
description = "Force-load patched MediaTek MT7927 (Filogic 380) kernel modules";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
before = [ "network.target" "NetworkManager.service" ];
|
||||
path = [ pkgs.kmod ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
script = ''
|
||||
KVER="$(uname -r)"
|
||||
EXTRA="/run/booted-system/kernel-modules/lib/modules/$KVER/extra"
|
||||
|
||||
# Load in-tree core dependencies (these ARE indexed, so modprobe works).
|
||||
modprobe mac80211 cfg80211 rfkill led-class bluetooth || true
|
||||
|
||||
# Defensive: drop any in-tree mt76 modules that may have been pulled in
|
||||
# (e.g. via boot.kernelModules) so the patched copies can bind instead.
|
||||
rmmod mt7925e mt7921e mt7925_common mt7921_common mt792x_lib mt76_connac_lib mt76 2>/dev/null || true
|
||||
|
||||
insmod_x() {
|
||||
local mod="$1"
|
||||
local name
|
||||
name="''${mod##*/}"; name="''${name%.ko}"
|
||||
if lsmod | grep -q "^$name "; then
|
||||
echo "mt7927-force: $name already loaded, skipping"
|
||||
else
|
||||
echo "mt7927-force: insmod $mod"
|
||||
insmod "$mod" || echo "mt7927-force: FAILED to insmod $mod"
|
||||
fi
|
||||
}
|
||||
|
||||
# Patched mt76 stack (full replacement, dependency order).
|
||||
insmod_x "$EXTRA/mt76/mt76.ko"
|
||||
insmod_x "$EXTRA/mt76/mt76-connac-lib.ko"
|
||||
insmod_x "$EXTRA/mt76/mt792x-lib.ko"
|
||||
insmod_x "$EXTRA/mt76/mt7921/mt7921-common.ko"
|
||||
insmod_x "$EXTRA/mt76/mt7925/mt7925-common.ko"
|
||||
insmod_x "$EXTRA/mt76/mt7921/mt7921e.ko"
|
||||
insmod_x "$EXTRA/mt76/mt7925/mt7925e.ko"
|
||||
|
||||
# Patched bluetooth. The patched btmtk/btusb need symbols from the
|
||||
# in-tree bluetooth core (bluetooth.ko) and the btrtl/btintel/btbcm
|
||||
# helpers, so load those first (they are indexed and modprobe-able).
|
||||
for m in bluetooth btrtl btintel btbcm rfkill ecdh_generic ecc crc16; do
|
||||
if modprobe "$m" 2>/dev/null; then
|
||||
echo "mt7927-force: modprobe $m ok"
|
||||
else
|
||||
echo "mt7927-force: modprobe $m FAILED (continuing)"
|
||||
fi
|
||||
done
|
||||
|
||||
insmod_x "$EXTRA/bluetooth/btmtk.ko"
|
||||
insmod_x "$EXTRA/bluetooth/btusb.ko"
|
||||
'';
|
||||
};
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
# 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
|
||||
# Moonfin 2.1.0 Jellyfin client — upstream Flatpak bundle (not on Flathub yet).
|
||||
# https://github.com/Moonfin-Client/Moonfin-Core/releases/tag/2.1.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=";
|
||||
url = "https://github.com/Moonfin-Client/Moonfin-Core/releases/download/2.2.0/Moonfin_Linux_v2.2.0.flatpak";
|
||||
hash = "sha256-66YaGJQEK3u+SAmtsXLbCH4ZF8gL/Jl1S5PRGDfZ5fE=";
|
||||
};
|
||||
}
|
||||
];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# NVIDIA for host desktop.
|
||||
# NVIDIA for host desktop (disabled when GPU passthrough binds the card to VFIO).
|
||||
{ config, lib, pkgs, ... }:
|
||||
{
|
||||
lib.mkIf (!config.chiasson.system.gpuPassthru.enable) {
|
||||
boot.kernelParams = [ "snd_hda_core.gpu_bind=0" ];
|
||||
boot.kernelPackages = lib.mkDefault pkgs.linuxPackages_latest;
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
package = config.boot.kernelPackages.nvidiaPackages.latest;
|
||||
};
|
||||
|
||||
hardware.nvidia-container-toolkit.enable = true;
|
||||
hardware.nvidia-container-toolkit.enable = lib.mkDefault true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
# Headless 4K virtual output for Sunshine → Moonlight on a client TV.
|
||||
#
|
||||
# NVIDIA: force-enable a spare DRM connector with a custom EDID via kernel params:
|
||||
# video=<connector>:e force-enables the connector
|
||||
# drm.edid_firmware=<connector>:… loads modes from the EDID blob
|
||||
#
|
||||
# Capture is pinned to the connector by name (Sunshine `output_name`), which is
|
||||
# stable across reboots/hotplug — unlike a numeric monitor index.
|
||||
# Reboot after changing connector or EDID. 4K@60 is realistic on RTX 2070.
|
||||
{ config, lib, pkgs, ... }:
|
||||
let
|
||||
cfg = config.chiasson.system.streamingDisplay;
|
||||
|
||||
edidFirmware = pkgs.runCommand "sunshine-virtual-4k-edid" {
|
||||
nativeBuildInputs = [ pkgs.python3 ];
|
||||
} ''
|
||||
mkdir -p $out/lib/firmware/edid
|
||||
${pkgs.python3}/bin/python3 ${./generate-virtual-edid.py} $out/lib/firmware/edid/${cfg.edidFileName}
|
||||
'';
|
||||
|
||||
# On stream start: scale + focus the virtual 4K output so streamed windows
|
||||
# land there. Capture is pinned to the connector via Sunshine `output_name`,
|
||||
# so no fragile numeric-index detection is needed.
|
||||
streamDisplayUp = pkgs.writeShellScriptBin "stream-display-up" ''
|
||||
${pkgs.niri}/bin/niri msg output ${cfg.connector} scale ${toString cfg.niriScale}
|
||||
${pkgs.niri}/bin/niri msg action focus-monitor ${cfg.connector}
|
||||
${pkgs.niri}/bin/niri msg action move-window-to-monitor ${cfg.connector} || true
|
||||
'';
|
||||
|
||||
streamDisplayDown = pkgs.writeShellScriptBin "stream-display-down" ''
|
||||
${pkgs.niri}/bin/niri msg action focus-monitor ${cfg.focusMonitorOnEnd}
|
||||
'';
|
||||
in
|
||||
{
|
||||
options.chiasson.system.streamingDisplay = {
|
||||
enable = lib.mkEnableOption ''
|
||||
Force-enable a spare NVIDIA DRM connector with a 4K EDID for Sunshine/Moonlight.
|
||||
Requires reboot. Pair with `chiasson.desktop.niri` output config in `displays.nix`.
|
||||
'';
|
||||
|
||||
connector = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "DP-2";
|
||||
example = "HDMI-A-1";
|
||||
description = ''
|
||||
DRM connector name (e.g. `DP-2`, `HDMI-A-1`). Must be unused on the host —
|
||||
14900k desk uses DP-3, DP-4, and HDMI-A-3 by default.
|
||||
'';
|
||||
};
|
||||
|
||||
edidFileName = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "virtual-4k.bin";
|
||||
description = "Filename under `/lib/firmware/edid/` referenced by `drm.edid_firmware`.";
|
||||
};
|
||||
|
||||
mode = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "3840x2160@60";
|
||||
description = "Mode Niri should use on the virtual output (must match EDID).";
|
||||
};
|
||||
|
||||
niriPosition = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "x=7680 y=0";
|
||||
description = ''
|
||||
Niri `position` for the virtual panel — keep it clear of desk monitors
|
||||
so streamed windows do not overlap your ultrawide layout.
|
||||
'';
|
||||
};
|
||||
|
||||
niriScale = lib.mkOption {
|
||||
type = lib.types.float;
|
||||
default = 1.5;
|
||||
example = 1.5;
|
||||
description = ''
|
||||
Niri `scale` on the virtual panel. Values above 1.0 enlarge UI and text
|
||||
on the 4K framebuffer (helpful when viewing on a TV from the couch).
|
||||
'';
|
||||
};
|
||||
|
||||
focusMonitorOnEnd = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "DP-3";
|
||||
description = "Niri monitor to refocus when a Moonlight session ends.";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable (lib.mkMerge [
|
||||
{
|
||||
hardware.firmware = [ edidFirmware ];
|
||||
|
||||
boot.kernelParams = [
|
||||
"video=${cfg.connector}:e"
|
||||
"drm.edid_firmware=${cfg.connector}:edid/${cfg.edidFileName}"
|
||||
];
|
||||
|
||||
environment.systemPackages = [
|
||||
streamDisplayUp
|
||||
streamDisplayDown
|
||||
];
|
||||
}
|
||||
(lib.mkIf config.chiasson.system.gaming.sunshine.enable {
|
||||
services.sunshine.settings = {
|
||||
capture = "wlr";
|
||||
# Pin capture to the virtual display by connector name (stable across
|
||||
# reboots/hotplug). A numeric index drifted to the ultrawide before.
|
||||
output_name = cfg.connector;
|
||||
# RTX 2070 has no AV1 NVENC; avoid probing av1_nvenc.
|
||||
encoder = "hevc_nvenc,h264_nvenc";
|
||||
global_prep_cmd = "[{\"do\":\"${streamDisplayUp}/bin/stream-display-up\",\"undo\":\"${streamDisplayDown}/bin/stream-display-down\"}]";
|
||||
};
|
||||
})
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
# VM self-test overrides — used with nixos-rebuild build-vm to try the system
|
||||
# in a VM without rebuilding/rebooting the host.
|
||||
{ config, lib, pkgs, ... }:
|
||||
{
|
||||
virtualisation.vmVariant = {
|
||||
config = {
|
||||
|
||||
|
||||
hardware.nvidia-container-toolkit.enable = lib.mkForce false;
|
||||
hardware.nvidia.package = lib.mkForce null;
|
||||
services.xserver.videoDrivers = lib.mkForce [ "modesetting" ];
|
||||
|
||||
virtualisation = {
|
||||
memorySize = 4096;
|
||||
cores = 3;
|
||||
graphics = true;
|
||||
qemu.options = [
|
||||
"-device virtio-vga-gl"
|
||||
"-display gtk,gl=on"
|
||||
];
|
||||
};
|
||||
|
||||
users.users.olivier = {
|
||||
initialPassword = lib.mkForce "";
|
||||
hashedPasswordFile = lib.mkForce null; # Disable the secret file dependency
|
||||
};
|
||||
|
||||
# Force greetd's `initial_session` directly. We can't rely on
|
||||
# `services.displayManager.autoLogin` because DankGreeter's flake module
|
||||
# (inputs.dms.nixosModules.greeter) aggressively overwrites
|
||||
# `services.greetd.settings`, clobbering whatever the displayManager
|
||||
# module injects for auto-login. mkForce on `initial_session` alone wins
|
||||
# against DMS without touching `default_session` (DankGreeter), so a
|
||||
# manual logout in the VM still shows DankGreeter. VM-only — host build
|
||||
# is unaffected because this whole block lives under vmVariant.
|
||||
services.greetd.settings.initial_session = lib.mkForce {
|
||||
command = "${pkgs.niri}/bin/niri-session";
|
||||
user = "olivier";
|
||||
};
|
||||
|
||||
environment.systemPackages = [ pkgs.grc ];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<!-- Windows 11 install profile: emulated display (virt-manager console), no GPU passthrough.
|
||||
After install + NVIDIA driver: set vm.installMode = false and rebuild for gaming profile. -->
|
||||
<domain type='kvm'>
|
||||
<name>win11</name>
|
||||
<uuid>a7b3c8d1-4e2f-5a6b-9c0d-1e2f3a4b5c6d</uuid>
|
||||
<memory unit='GiB'>16</memory>
|
||||
<currentMemory unit='GiB'>16</currentMemory>
|
||||
<vcpu placement='static'>8</vcpu>
|
||||
<os>
|
||||
<type arch='x86_64' machine='pc-q35-9.0'>hvm</type>
|
||||
<loader readonly='yes' secure='yes' type='pflash'>/run/libvirt/nix-ovmf/edk2-x86_64-secure-code.fd</loader>
|
||||
<nvram template='/run/libvirt/nix-ovmf/edk2-i386-vars.fd'>/var/lib/libvirt/qemu/nvram/win11_VARS.fd</nvram>
|
||||
<bootmenu enable='yes'/>
|
||||
<boot dev='cdrom'/>
|
||||
<boot dev='hd'/>
|
||||
</os>
|
||||
<features>
|
||||
<acpi/>
|
||||
<apic/>
|
||||
</features>
|
||||
<cpu mode='host-passthrough' check='none' migratable='on'/>
|
||||
<clock offset='localtime'>
|
||||
<timer name='rtc' tickpolicy='catchup'/>
|
||||
<timer name='pit' tickpolicy='delay'/>
|
||||
<timer name='hpet' present='no'/>
|
||||
</clock>
|
||||
<on_poweroff>destroy</on_poweroff>
|
||||
<on_reboot>restart</on_reboot>
|
||||
<on_crash>preserve</on_crash>
|
||||
<devices>
|
||||
<emulator>/run/libvirt/nix-emulators/qemu-system-x86_64</emulator>
|
||||
|
||||
<disk type='block' device='disk'>
|
||||
<driver name='qemu' type='raw' cache='none' io='native' discard='unmap'/>
|
||||
<source dev='/dev/disk/by-id/ata-CT1000MX500SSD1_1830E14B054A'/>
|
||||
<target dev='sda' bus='sata'/>
|
||||
<address type='drive' controller='0' bus='0' target='0' unit='0'/>
|
||||
</disk>
|
||||
|
||||
<disk type='file' device='cdrom'>
|
||||
<driver name='qemu' type='raw'/>
|
||||
<source file='/var/lib/libvirt/boot/win11.iso'/>
|
||||
<target dev='sdb' bus='sata'/>
|
||||
<readonly/>
|
||||
<address type='drive' controller='0' bus='0' target='0' unit='1'/>
|
||||
</disk>
|
||||
|
||||
<controller type='sata' index='0'>
|
||||
<address type='pci' domain='0x0000' bus='0x00' slot='0x1f' function='0x2'/>
|
||||
</controller>
|
||||
<controller type='pci' index='0' model='pcie-root'/>
|
||||
<controller type='pci' index='1' model='pcie-root-port'>
|
||||
<model name='pcie-root-port'/>
|
||||
<target chassis='1' port='0x10'/>
|
||||
<address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x0'/>
|
||||
</controller>
|
||||
<controller type='usb' index='0' model='qemu-xhci' ports='15'>
|
||||
<address type='pci' domain='0x0000' bus='0x00' slot='0x07' function='0x0'/>
|
||||
</controller>
|
||||
|
||||
<interface type='network'>
|
||||
<source network='default'/>
|
||||
<model type='e1000e'/>
|
||||
<address type='pci' domain='0x0000' bus='0x01' slot='0x00' function='0x0'/>
|
||||
</interface>
|
||||
|
||||
<rng model='virtio'>
|
||||
<backend model='random'>/dev/urandom</backend>
|
||||
<address type='pci' domain='0x0000' bus='0x05' slot='0x00' function='0x0'/>
|
||||
</rng>
|
||||
|
||||
<memballoon model='none'/>
|
||||
|
||||
<graphics type='spice' port='5900' autoport='no' listen='127.0.0.1'>
|
||||
<listen type='address' address='127.0.0.1'/>
|
||||
</graphics>
|
||||
<video>
|
||||
<model type='vga'/>
|
||||
</video>
|
||||
<input type='mouse' bus='virtio'/>
|
||||
<input type='keyboard' bus='virtio'/>
|
||||
<sound model='ich9'>
|
||||
<audio id='1'/>
|
||||
</sound>
|
||||
<audio id='1' type='spice'/>
|
||||
|
||||
<tpm model='tpm-crb'>
|
||||
<backend type='emulator' version='2.0'/>
|
||||
</tpm>
|
||||
</devices>
|
||||
</domain>
|
||||
@@ -0,0 +1,177 @@
|
||||
<!-- Gaming: GPU + Looking Glass (kvmfr) -->
|
||||
<domain type='kvm' xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0' xmlns:libosinfo='http://libosinfo.org/xmlns/libvirt/domain/1.0'>
|
||||
<name>win11</name>
|
||||
<uuid>a7b3c8d1-4e2f-5a6b-9c0d-1e2f3a4b5c6d</uuid>
|
||||
<memory unit='GiB'>16</memory>
|
||||
<currentMemory unit='GiB'>16</currentMemory>
|
||||
<vcpu placement='static'>8</vcpu>
|
||||
<metadata>
|
||||
<libosinfo:libosinfo>
|
||||
<libosinfo:os id='http://microsoft.com/win/11'/>
|
||||
</libosinfo:libosinfo>
|
||||
</metadata>
|
||||
<os>
|
||||
<type arch='x86_64' machine='pc-q35-9.0'>hvm</type>
|
||||
<loader readonly='yes' secure='yes' type='pflash'>/run/libvirt/nix-ovmf/edk2-x86_64-secure-code.fd</loader>
|
||||
<nvram template='/run/libvirt/nix-ovmf/edk2-i386-vars.fd'>/var/lib/libvirt/qemu/nvram/win11_VARS.fd</nvram>
|
||||
<smbios mode='host'/>
|
||||
<boot dev='hd'/>
|
||||
</os>
|
||||
<features>
|
||||
<acpi/>
|
||||
<apic/>
|
||||
<hyperv mode='custom'>
|
||||
<relaxed state='on'/>
|
||||
<vapic state='on'/>
|
||||
<spinlocks state='on' retries='8191'/>
|
||||
<vendor_id state='on' value='GenuineIntel'/>
|
||||
</hyperv>
|
||||
<kvm>
|
||||
<hidden state='on'/>
|
||||
</kvm>
|
||||
<vmport state='off'/>
|
||||
<smm state='on'/>
|
||||
<ioapic driver='kvm'/>
|
||||
</features>
|
||||
<cpu mode='host-passthrough' check='none' migratable='off'>
|
||||
<topology sockets='1' dies='1' cores='4' threads='2'/>
|
||||
<maxphysaddr mode='passthrough' limit='40'/>
|
||||
<feature policy='disable' name='hypervisor'/>
|
||||
</cpu>
|
||||
<clock offset='localtime'>
|
||||
<timer name='rtc' tickpolicy='catchup'/>
|
||||
<timer name='pit' tickpolicy='delay'/>
|
||||
<timer name='hpet' present='no'/>
|
||||
<timer name='hypervclock' present='yes'/>
|
||||
</clock>
|
||||
<on_poweroff>destroy</on_poweroff>
|
||||
<on_reboot>restart</on_reboot>
|
||||
<on_crash>preserve</on_crash>
|
||||
<pm>
|
||||
<suspend-to-mem enabled='no'/>
|
||||
<suspend-to-disk enabled='no'/>
|
||||
</pm>
|
||||
<devices>
|
||||
<emulator>/run/libvirt/nix-emulators/qemu-system-x86_64</emulator>
|
||||
|
||||
<disk type='block' device='disk'>
|
||||
<driver name='qemu' type='raw' cache='none' io='native' discard='unmap'/>
|
||||
<source dev='/dev/disk/by-id/ata-CT1000MX500SSD1_1830E14B054A'/>
|
||||
<target dev='sda' bus='sata'/>
|
||||
<address type='drive' controller='0' bus='0' target='0' unit='0'/>
|
||||
</disk>
|
||||
|
||||
<controller type='sata' index='0'>
|
||||
<address type='pci' domain='0x0000' bus='0x00' slot='0x1f' function='0x2'/>
|
||||
</controller>
|
||||
<controller type='pci' index='0' model='pcie-root'/>
|
||||
<controller type='pci' index='1' model='pcie-root-port'>
|
||||
<model name='pcie-root-port'/>
|
||||
<target chassis='1' port='0x10'/>
|
||||
<address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x0'/>
|
||||
</controller>
|
||||
<controller type='pci' index='2' model='pcie-root-port'>
|
||||
<model name='pcie-root-port'/>
|
||||
<target chassis='2' port='0x11'/>
|
||||
<address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x1'/>
|
||||
</controller>
|
||||
<controller type='pci' index='3' model='pcie-root-port'>
|
||||
<model name='pcie-root-port'/>
|
||||
<target chassis='3' port='0x12'/>
|
||||
<address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x2'/>
|
||||
</controller>
|
||||
<controller type='pci' index='4' model='pcie-root-port'>
|
||||
<model name='pcie-root-port'/>
|
||||
<target chassis='4' port='0x13'/>
|
||||
<address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x3'/>
|
||||
</controller>
|
||||
<controller type='pci' index='5' model='pcie-root-port'>
|
||||
<model name='pcie-root-port'/>
|
||||
<target chassis='5' port='0x14'/>
|
||||
<address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x4'/>
|
||||
</controller>
|
||||
<controller type='usb' index='0' model='qemu-xhci' ports='15'>
|
||||
<address type='pci' domain='0x0000' bus='0x00' slot='0x07' function='0x0'/>
|
||||
</controller>
|
||||
|
||||
<interface type='network'>
|
||||
<mac address='04:7c:16:c9:48:a7'/>
|
||||
<source network='default'/>
|
||||
<model type='e1000e'/>
|
||||
<address type='pci' domain='0x0000' bus='0x01' slot='0x00' function='0x0'/>
|
||||
</interface>
|
||||
|
||||
<input type='keyboard' bus='ps2'/>
|
||||
<input type='mouse' bus='ps2'/>
|
||||
|
||||
<memballoon model='none'/>
|
||||
|
||||
<graphics type='spice' port='5900' autoport='no' listen='127.0.0.1'>
|
||||
<listen type='address' address='127.0.0.1'/>
|
||||
<image compression='off'/>
|
||||
</graphics>
|
||||
<video>
|
||||
<model type='none'/>
|
||||
</video>
|
||||
<sound model='ich9'>
|
||||
<audio id='1'/>
|
||||
</sound>
|
||||
<audio id='1' type='spice'/>
|
||||
|
||||
<hostdev mode='subsystem' type='pci' managed='yes'>
|
||||
<source>
|
||||
<address domain='0x0000' bus='0x01' slot='0x00' function='0x0'/>
|
||||
</source>
|
||||
<address type='pci' domain='0x0000' bus='0x06' slot='0x00' function='0x0' multifunction='on'/>
|
||||
</hostdev>
|
||||
<hostdev mode='subsystem' type='pci' managed='yes'>
|
||||
<source>
|
||||
<address domain='0x0000' bus='0x01' slot='0x00' function='0x1'/>
|
||||
</source>
|
||||
<address type='pci' domain='0x0000' bus='0x06' slot='0x00' function='0x1'/>
|
||||
</hostdev>
|
||||
<hostdev mode='subsystem' type='pci' managed='yes'>
|
||||
<source>
|
||||
<address domain='0x0000' bus='0x01' slot='0x00' function='0x2'/>
|
||||
</source>
|
||||
<address type='pci' domain='0x0000' bus='0x06' slot='0x00' function='0x2'/>
|
||||
</hostdev>
|
||||
<hostdev mode='subsystem' type='pci' managed='yes'>
|
||||
<source>
|
||||
<address domain='0x0000' bus='0x01' slot='0x00' function='0x3'/>
|
||||
</source>
|
||||
<address type='pci' domain='0x0000' bus='0x06' slot='0x00' function='0x3'/>
|
||||
</hostdev>
|
||||
|
||||
<hostdev mode='subsystem' type='pci' managed='yes'>
|
||||
<source>
|
||||
<address domain='0x0000' bus='0x03' slot='0x00' function='0x0'/>
|
||||
</source>
|
||||
<address type='pci' domain='0x0000' bus='0x04' slot='0x00' function='0x0'/>
|
||||
</hostdev>
|
||||
|
||||
<hostdev mode='subsystem' type='usb' managed='yes'>
|
||||
<source startupPolicy='optional'>
|
||||
<vendor id='0x046d'/>
|
||||
<product id='0xc08b'/>
|
||||
</source>
|
||||
</hostdev>
|
||||
|
||||
<hostdev mode='subsystem' type='usb' managed='yes'>
|
||||
<source startupPolicy='optional'>
|
||||
<vendor id='0x0489'/>
|
||||
<product id='0xe10f'/>
|
||||
</source>
|
||||
</hostdev>
|
||||
|
||||
<tpm model='tpm-crb'>
|
||||
<backend type='emulator' version='2.0'/>
|
||||
</tpm>
|
||||
</devices>
|
||||
<qemu:commandline>
|
||||
<qemu:arg value='-device'/>
|
||||
<qemu:arg value='{"driver":"ivshmem-plain","id":"shmem0","memdev":"looking-glass"}'/>
|
||||
<qemu:arg value='-object'/>
|
||||
<qemu:arg value='{"qom-type":"memory-backend-file","id":"looking-glass","mem-path":"/dev/kvmfr0","size":33554432,"share":true}'/>
|
||||
</qemu:commandline>
|
||||
</domain>
|
||||
@@ -6,6 +6,7 @@
|
||||
self.nixosModules."14900kHardware"
|
||||
inputs.home-manager.nixosModules.home-manager
|
||||
inputs.sops-nix.nixosModules.sops
|
||||
inputs.nixvirt.nixosModules.default
|
||||
|
||||
self.nixosModules.system
|
||||
self.nixosModules.desktop
|
||||
@@ -14,33 +15,24 @@
|
||||
self.nixosModules."client-services"
|
||||
./_private/platform.nix
|
||||
./_private/nvidia.nix
|
||||
./_private/vm-self.nix
|
||||
./_private/gpu-passthru.nix
|
||||
./_private/peripherals.nix
|
||||
# ./_private/printing-epson.nix
|
||||
./_private/displays.nix
|
||||
./_private/streaming-display.nix
|
||||
./_private/ai.nix
|
||||
./_private/media-disk.nix
|
||||
./_private/mediatek-mt7927.nix
|
||||
./_private/jellyfin-nfs-export.nix
|
||||
./_private/moonfin-flatpak.nix
|
||||
];
|
||||
|
||||
sops = {
|
||||
defaultSopsFile = ../../../secrets/secrets.yaml;
|
||||
defaultSopsFormat = "yaml";
|
||||
age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
|
||||
};
|
||||
services.cloudflare-warp.enable = true;
|
||||
programs.nix-ld.enable = true;
|
||||
|
||||
sops.secrets."caching/attic/token" = {
|
||||
owner = "olivier";
|
||||
group = "users";
|
||||
mode = "0400";
|
||||
};
|
||||
|
||||
sops.secrets."users/olivier/hashedPassword".neededForUsers = true;
|
||||
sops.secrets."swiftshare/API_KEY" = {
|
||||
owner = "olivier";
|
||||
group = "users";
|
||||
mode = "0400";
|
||||
};
|
||||
services.cloudflare-warp.enable = true;
|
||||
boot.kernel.sysctl."fs.inotify.max_user_watches" = 1048576;
|
||||
boot.kernel.sysctl."fs.inotify.max_user_instances" = 2048;
|
||||
|
||||
# Intel iGPU video acceleration (VA-API / QSV via oneVPL).
|
||||
# This fixes common NixOS issues like `vaInitialize failed` and missing QSV encoders in apps.
|
||||
@@ -53,22 +45,12 @@ services.cloudflare-warp.enable = true;
|
||||
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;
|
||||
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;
|
||||
};
|
||||
|
||||
chiasson.desktop = {
|
||||
niri.enable = true;
|
||||
@@ -82,12 +64,10 @@ services.cloudflare-warp.enable = true;
|
||||
"--flake"
|
||||
".#14900k"
|
||||
];
|
||||
enableRbwLockToggle = true;
|
||||
defaultSeedDir = ./_private/dms-defaults;
|
||||
};
|
||||
};
|
||||
|
||||
chiasson.system.chromiumHevc.enable = true;
|
||||
|
||||
chiasson.system = {
|
||||
ytDlpTelequebecPatch.enable = true;
|
||||
|
||||
@@ -100,6 +80,10 @@ services.cloudflare-warp.enable = true;
|
||||
gaming.sunshine.enable = true;
|
||||
gaming.sunshine.cudaSupport = true;
|
||||
|
||||
streamingDisplay.enable = true;
|
||||
# Spare NVIDIA port — verify with: grep -H . /sys/class/drm/card*-*/status
|
||||
streamingDisplay.connector = "DP-2";
|
||||
|
||||
monitorInput.enable = true;
|
||||
|
||||
flatpak.enable = true;
|
||||
@@ -116,7 +100,9 @@ services.cloudflare-warp.enable = true;
|
||||
yt-dlp
|
||||
|
||||
# Native install (avoid flatpak sandbox issues for QSV/VAAPI).
|
||||
handbrake
|
||||
# handbrake # TEMP removed 2026-07-13 — depends on broken ffmpeg-full-8.1.2
|
||||
# # (A01-mov-read patch fails against 8.1.2 source). Re-enable
|
||||
# # when nixpkgs regenerates the patch or when switching to stable.
|
||||
|
||||
qbittorrent
|
||||
|
||||
@@ -141,78 +127,5 @@ services.cloudflare-warp.enable = true;
|
||||
};
|
||||
|
||||
chiasson.users.enabled = [ "olivier" ];
|
||||
|
||||
chiasson.users.extraModules.olivier = [
|
||||
self.homeManagerModules.wisdomFilebrowsersDolphin
|
||||
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
|
||||
self.homeManagerModules.wisdomShellFish
|
||||
self.homeManagerModules.wisdomShellOhMyPosh
|
||||
self.homeManagerModules.wisdomAppsDiscord
|
||||
self.homeManagerModules.wisdomAppsSpotify
|
||||
self.homeManagerModules.wisdomAppsLocalsend
|
||||
self.homeManagerModules.wisdomAppsPokeclicker
|
||||
self.homeManagerModules.wisdomDesktopScreenshot
|
||||
self.homeManagerModules.wisdomDesktopGtkQtTheming
|
||||
{
|
||||
programs.git = {
|
||||
enable = true;
|
||||
settings.user = {
|
||||
name = "OlivierChiasson";
|
||||
email = "olivierchiasson@hotmail.fr";
|
||||
};
|
||||
};
|
||||
|
||||
chiasson.home = {
|
||||
extraPackages = [ pkgs.parsec-bin ];
|
||||
|
||||
shell = {
|
||||
fish.enable = true;
|
||||
yazi.enable = true;
|
||||
ohMyPosh.enable = true;
|
||||
};
|
||||
|
||||
terminals.kitty.enable = true;
|
||||
|
||||
filebrowsers.dolphin.enable = true;
|
||||
|
||||
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;
|
||||
|
||||
apps = {
|
||||
discord.enable = true;
|
||||
spotify.enable = true;
|
||||
spotify.openDiscoveryFirewall = true;
|
||||
localsend.enable = true;
|
||||
pokeclicker.enable = true;
|
||||
};
|
||||
|
||||
desktop = {
|
||||
screenshot = {
|
||||
enable = true;
|
||||
swiftshareApiKeyFile = "/run/secrets/swiftshare/API_KEY"; #TODO[epic=sops] redo this by passing sops file output directly
|
||||
};
|
||||
theming.enable = true;
|
||||
};
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,11 +4,10 @@
|
||||
system = "x86_64-linux";
|
||||
specialArgs = {
|
||||
inherit self inputs;
|
||||
host = "14900k";
|
||||
system = "x86_64-linux";
|
||||
};
|
||||
modules = [
|
||||
self.nixosModules."14900kConfiguration"
|
||||
self.nixosModules."14900kHome"
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
{ self, inputs, ... }:
|
||||
{
|
||||
flake.nixosModules."14900kHome" =
|
||||
{ self, pkgs, ... }:
|
||||
{
|
||||
imports = [ self.nixosModules.desktopHomeBase ];
|
||||
|
||||
chiasson.users.extraModules.olivier = [
|
||||
{
|
||||
chiasson.home = {
|
||||
extraPackages = [ pkgs.parsec-bin ];
|
||||
|
||||
browsers = {
|
||||
edge.enable = true;
|
||||
orion.enable = true;
|
||||
zen.enable = true;
|
||||
};
|
||||
|
||||
apps = {
|
||||
discord.enable = true;
|
||||
spotify.enable = true;
|
||||
spotify.openDiscoveryFirewall = true;
|
||||
pokeclicker.enable = true;
|
||||
};
|
||||
|
||||
editors.vscode = {
|
||||
enable = true;
|
||||
extensions = with pkgs.vscode-extensions; [
|
||||
alefragnani.project-manager
|
||||
johnpapa.vscode-peacock
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -23,5 +23,37 @@
|
||||
services.printing.enable = true;
|
||||
security.polkit.enable = true;
|
||||
services.udisks2.enable = true;
|
||||
|
||||
sops = {
|
||||
defaultSopsFile = ../../secrets/secrets.yaml;
|
||||
defaultSopsFormat = "yaml";
|
||||
age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
|
||||
};
|
||||
|
||||
sops.secrets."caching/attic/token" = {
|
||||
owner = "olivier";
|
||||
group = "users";
|
||||
mode = "0400";
|
||||
};
|
||||
|
||||
sops.secrets."swiftshare/API_KEY" = {
|
||||
owner = "olivier";
|
||||
group = "users";
|
||||
mode = "0400";
|
||||
};
|
||||
|
||||
sops.secrets."home-assistant/auth-token" = {
|
||||
owner = "olivier";
|
||||
group = "users";
|
||||
mode = "0400";
|
||||
};
|
||||
|
||||
sops.secrets."gitcrypt/personal-docs-key" = {
|
||||
sopsFile = ../../secrets/gitcrypt-personal-docs-key;
|
||||
format = "binary";
|
||||
owner = "olivier";
|
||||
group = "users";
|
||||
mode = "0400";
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Shared Home Manager selection for all desktop hosts (14900k, t2mbp, ideapad, uConsole).
|
||||
# Wisdom slices resolved here (NixOS has `self`); toggles select what actually installs.
|
||||
{ self, ... }:
|
||||
{
|
||||
flake.nixosModules.desktopHomeBase =
|
||||
{ config, lib, ... }:
|
||||
{
|
||||
chiasson.system.chromiumHevc.enable = lib.mkDefault true;
|
||||
|
||||
chiasson.system.caching.attic = lib.mkDefault {
|
||||
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;
|
||||
};
|
||||
|
||||
chiasson.users.extraModules.olivier = self.lib.wisdomCatalogExtraModules self;
|
||||
};
|
||||
}
|
||||
@@ -37,51 +37,15 @@
|
||||
./_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"
|
||||
];
|
||||
# Wi-Fi modem (qcom-wcn3990) + Bluetooth (QCA crnv32) need binary blobs
|
||||
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;
|
||||
@@ -110,8 +74,6 @@
|
||||
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.
|
||||
@@ -120,74 +82,15 @@
|
||||
"-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"
|
||||
"nixos-rebuild switch --flake path:/home/olivier/chiasson-nix#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";
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,11 +4,10 @@
|
||||
system = "aarch64-linux";
|
||||
specialArgs = {
|
||||
inherit self inputs;
|
||||
host = "ideapad";
|
||||
system = "aarch64-linux";
|
||||
};
|
||||
modules = [
|
||||
self.nixosModules.ideapadConfiguration
|
||||
self.nixosModules.ideapadHome
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{ self, inputs, ... }:
|
||||
{
|
||||
flake.nixosModules.ideapadHome =
|
||||
{ self, ... }:
|
||||
{
|
||||
imports = [ self.nixosModules.desktopHomeBase ];
|
||||
};
|
||||
}
|
||||
@@ -16,7 +16,7 @@ in
|
||||
"blackfry.day"
|
||||
"yestur.day"
|
||||
"rp-own.life"
|
||||
"xn--1iu.cc"
|
||||
"慧.cc"
|
||||
];
|
||||
proxied = true;
|
||||
ipv4 = true;
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
{ lib, ... }:
|
||||
{ lib, config, ... }:
|
||||
let
|
||||
secretFilePath = ../secrets.yaml;
|
||||
in
|
||||
{
|
||||
|
||||
sops.secrets."cloudflare-turnstile/secret-key".sopsFile = secretFilePath;
|
||||
sops.secrets."cloudflare-turnstile/site-key".sopsFile = secretFilePath;
|
||||
|
||||
|
||||
services.gitea = {
|
||||
enable = true;
|
||||
|
||||
@@ -22,7 +30,16 @@
|
||||
SSH_LISTEN_PORT = 222;
|
||||
};
|
||||
|
||||
service.DISABLE_REGISTRATION = false;
|
||||
service = {
|
||||
DISABLE_REGISTRATION = false;
|
||||
ENABLE_CAPTCHA = true;
|
||||
CAPTCHA_TYPE = "cfturnstile";
|
||||
CF_TURNSTILE_SECRET = config.sops.secrets."cloudflare-turnstile/secret-key".path;
|
||||
CF_TURNSTILE_SITEKEY = config.sops.secrets."cloudflare-turnstile/site-key".path;
|
||||
REGISTER_EMAIL_CONFIRM = false;
|
||||
ENABLE_NOTIFY_MAIL = false;
|
||||
};
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
# 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;
|
||||
};
|
||||
}
|
||||
@@ -50,6 +50,9 @@
|
||||
};
|
||||
|
||||
sops.secrets."users/server/hashedPassword".neededForUsers = true;
|
||||
sops.secrets."tailscale/auth-key" = {
|
||||
mode = "0400";
|
||||
};
|
||||
|
||||
security.sudo.wheelNeedsPassword = true;
|
||||
|
||||
@@ -63,6 +66,11 @@
|
||||
networking = {
|
||||
hostName = "nix-server";
|
||||
networkManager.enable = true;
|
||||
tailscale = {
|
||||
enable = true;
|
||||
authKeyFile = config.sops.secrets."tailscale/auth-key".path;
|
||||
subnetRouter.enable = true;
|
||||
};
|
||||
};
|
||||
|
||||
caching.attic = {
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
system = "x86_64-linux";
|
||||
specialArgs = {
|
||||
inherit self inputs;
|
||||
host = "nix-server";
|
||||
system = "x86_64-linux";
|
||||
};
|
||||
modules = [
|
||||
self.nixosModules.nix-serverConfiguration
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
swiftshare:
|
||||
ghcr-token: ENC[AES256_GCM,data:V/5dLVLv4BbAxdMiBxXgmNbK17HAQkqzHJA2NWzOFfFlcy3dq8SnZQ==,iv:YTB3Bef+kZXunXVUCkFj/YZo1POdx2K+bNvzarSJ1Iw=,tag:HEBT4ZKMXTIy+ZEkNx3rHw==,type:str]
|
||||
ghcr-token: ENC[AES256_GCM,data:MFNjMrh3mlH2gGZlzp9lty3n/+6/SQSFbBowqrR3QFO4/t/afBtv2Q==,iv:3wZzjUU4uzctX5woHiAY4Y9QJIiBLeGdmrJzhmpVy9E=,tag:sK/Jy53jBsbdd1YWoOVT5A==,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]
|
||||
@@ -17,6 +17,9 @@ personal-website:
|
||||
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]
|
||||
cloudflare-turnstile:
|
||||
secret-key: ENC[AES256_GCM,data:fSsH5HRLvzwyovBezNQnil7due6+FCE2yW/S91aOwqb4yow=,iv:7hTu9YyACvOys42n8XFBmVykKJ0op8btb3xQ6tGpqP4=,tag:Z4FvKKPsb1rAQuWY4v5frg==,type:str]
|
||||
site-key: ENC[AES256_GCM,data:qKmx5EY3Yl5A3zsofFfskN3DrZI+yrHJ,iv:ZGnhkm12VG/O0TNQsKOIb53BlxXPqhBmSPh6rlLGpN4=,tag:C0vQsXRil72z6SRoLiifcA==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
@@ -46,7 +49,7 @@ sops:
|
||||
Nf0uOoSWPTJ/2SRNkSu7FMumATH4ldQ6TFSwKda3mBfBwhnFzLq10Q==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
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]
|
||||
lastmodified: "2026-07-10T20:47:30Z"
|
||||
mac: ENC[AES256_GCM,data:gqrsaZ7GwS+qkuVRwGkrTJjL1qfBid3RdbGLEZIr1P4XuKatqiFWs0ihj/Qm6jItktoPXwMHcejzx/wQgTImBvdNXDW5TbgF39VJYCZ8r/ToTX2LdZXCBJHim8F9eVE+aBJfsb/LHyvTJLcp3BgrOFyNYjt067585hDt/D3Tmh0=,iv:cKUPZI1lH1wMJVhgR0/TB3iDU6TUSv8vLo+vlThk2AU=,tag:eT2BwsN8mL5RgpQ+pAaKPg==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.13.1
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
# 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;
|
||||
};
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
./_private/media-disk.nix
|
||||
./_private/media-paths.nix
|
||||
./_services/docker-media.nix
|
||||
./_services/nixdesk-nfs-client.nix
|
||||
self.nixosModules.nixdeskJellyfin
|
||||
./_services/jellyfin.nix
|
||||
./_services/sonarr.nix
|
||||
./_services/radarr.nix
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
system = "x86_64-linux";
|
||||
specialArgs = {
|
||||
inherit self inputs;
|
||||
host = "r5500";
|
||||
system = "x86_64-linux";
|
||||
};
|
||||
modules = [
|
||||
self.nixosModules.r5500Configuration
|
||||
|
||||
@@ -19,26 +19,6 @@
|
||||
./_private/firmware.nix
|
||||
];
|
||||
|
||||
# ───────────────────────────── Sops (see repo secrets/.sops.yaml) ───────────
|
||||
sops = {
|
||||
defaultSopsFile = ../../../secrets/secrets.yaml;
|
||||
defaultSopsFormat = "yaml";
|
||||
age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
|
||||
};
|
||||
|
||||
sops.secrets."caching/attic/token" = {
|
||||
owner = "olivier";
|
||||
group = "users";
|
||||
mode = "0400";
|
||||
};
|
||||
|
||||
sops.secrets."users/olivier/hashedPassword".neededForUsers = true;
|
||||
sops.secrets."swiftshare/API_KEY" = {
|
||||
owner = "olivier";
|
||||
group = "users";
|
||||
mode = "0400";
|
||||
};
|
||||
|
||||
chiasson.system.librepods.enable = true;
|
||||
chiasson.system.palera1n.enable = true;
|
||||
|
||||
@@ -70,17 +50,6 @@
|
||||
# Dynamic function row on the Touch Bar (`tiny-dfr`; needs `DRM_APPLETBDRM` — see t2linux kernel opts).
|
||||
hardware.apple.touchBar.enable = true;
|
||||
|
||||
# ─────────────────────── Attic (pull + 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;
|
||||
};
|
||||
|
||||
# ─────────────────────── Display Server & Desktop ──────────────────────────
|
||||
chiasson.desktop = {
|
||||
niri = {
|
||||
@@ -95,9 +64,6 @@
|
||||
};
|
||||
defaultSession = "niri";
|
||||
shell = "dms";
|
||||
shells.dms = {
|
||||
enableRbwLockToggle = true;
|
||||
};
|
||||
};
|
||||
|
||||
chiasson.system = {
|
||||
@@ -110,59 +76,6 @@
|
||||
};
|
||||
|
||||
chiasson.users.enabled = [ "olivier" ];
|
||||
|
||||
chiasson.users.extraModules.olivier = [
|
||||
self.homeManagerModules.wisdomFilebrowsersDolphin
|
||||
self.homeManagerModules.wisdomTerminalsKitty
|
||||
self.homeManagerModules.wisdomBrowsersZen
|
||||
self.homeManagerModules.wisdomBrowsersChrome
|
||||
self.homeManagerModules.wisdomBrowsersEdge
|
||||
self.homeManagerModules.wisdomEditorsCursor
|
||||
self.homeManagerModules.wisdomEditorsKate
|
||||
self.homeManagerModules.wisdomEditorsObsidian
|
||||
self.homeManagerModules.wisdomShellYazi
|
||||
self.homeManagerModules.wisdomShellFish
|
||||
self.homeManagerModules.wisdomShellOhMyPosh
|
||||
self.homeManagerModules.wisdomAppsDiscord
|
||||
self.homeManagerModules.wisdomAppsSpotify
|
||||
self.homeManagerModules.wisdomAppsLocalsend
|
||||
self.homeManagerModules.wisdomAppsPokeclicker
|
||||
self.homeManagerModules.wisdomDesktopScreenshot
|
||||
{
|
||||
chiasson.home = {
|
||||
shell = {
|
||||
fish.enable = true;
|
||||
yazi.enable = true;
|
||||
ohMyPosh.enable = true;
|
||||
};
|
||||
|
||||
terminals.kitty.enable = true;
|
||||
filebrowsers.dolphin.enable = true;
|
||||
browsers = {
|
||||
zen.enable = false;
|
||||
chrome.enable = false;
|
||||
edge.enable = true;
|
||||
};
|
||||
editors = {
|
||||
cursor.enable = true;
|
||||
kate.enable = false;
|
||||
obsidian.enable = true;
|
||||
};
|
||||
apps = {
|
||||
discord.enable = true;
|
||||
spotify.enable = false;
|
||||
localsend.enable = true;
|
||||
pokeclicker.enable = true;
|
||||
};
|
||||
desktop = {
|
||||
screenshot = {
|
||||
enable = true;
|
||||
swiftshareApiKeyFile = "/run/secrets/swiftshare/API_KEY"; #TODO[epic=sops] redo this by passing sops file output directly
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
}
|
||||
@@ -4,11 +4,10 @@
|
||||
system = "x86_64-linux";
|
||||
specialArgs = {
|
||||
inherit self inputs;
|
||||
host = "t2mbp";
|
||||
system = "x86_64-linux";
|
||||
};
|
||||
modules = [
|
||||
self.nixosModules.t2mbpConfiguration
|
||||
self.nixosModules.t2mbpHome
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{ self, inputs, ... }:
|
||||
{
|
||||
flake.nixosModules.t2mbpHome =
|
||||
{ self, pkgs, ... }:
|
||||
{
|
||||
imports = [ self.nixosModules.desktopHomeBase ];
|
||||
|
||||
chiasson.users.extraModules.olivier = [
|
||||
{
|
||||
chiasson.home = {
|
||||
browsers.edge.enable = true;
|
||||
|
||||
apps = {
|
||||
discord.enable = true;
|
||||
pokeclicker.enable = true;
|
||||
};
|
||||
|
||||
editors.vscode = {
|
||||
enable = true;
|
||||
extensions = with pkgs.vscode-extensions; [
|
||||
alefragnani.project-manager
|
||||
johnpapa.vscode-peacock
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -42,37 +42,9 @@
|
||||
require-sigs = false;
|
||||
};
|
||||
|
||||
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";
|
||||
};
|
||||
|
||||
chiasson.system.librepods.enable = true;
|
||||
chiasson.system.palera1n.enable = true;
|
||||
|
||||
# ─────────────────────── Attic (pull + 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;
|
||||
};
|
||||
# ─────────────────────── Display Server & Desktop ──────────────────────────
|
||||
|
||||
chiasson.desktop = {
|
||||
@@ -84,9 +56,6 @@
|
||||
};
|
||||
defaultSession = "niri";
|
||||
shell = "dms";
|
||||
shells.dms = {
|
||||
enableWvkbdToggle = true;
|
||||
};
|
||||
};
|
||||
chiasson.system = {
|
||||
audio.enable = true;
|
||||
@@ -103,40 +72,6 @@
|
||||
|
||||
chiasson.users.enabled = [ "olivier" ];
|
||||
|
||||
chiasson.users.extraModules.olivier = [
|
||||
self.homeManagerModules.wisdomFilebrowsersDolphin
|
||||
self.homeManagerModules.wisdomTerminalsKitty
|
||||
self.homeManagerModules.wisdomBrowsersZen
|
||||
self.homeManagerModules.wisdomEditorsKate
|
||||
self.homeManagerModules.wisdomShellFish
|
||||
self.homeManagerModules.wisdomShellOhMyPosh
|
||||
self.homeManagerModules.wisdomAppsSpotify
|
||||
self.homeManagerModules.wisdomAppsLocalsend
|
||||
self.homeManagerModules.wisdomDesktopScreenshot
|
||||
self.homeManagerModules.wisdomHardwareUconsoleGamepad
|
||||
{
|
||||
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;
|
||||
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
|
||||
};
|
||||
};
|
||||
hardware.uconsoleGamepad.enable = true;
|
||||
};
|
||||
}
|
||||
];
|
||||
|
||||
console.packages = with pkgs; [ terminus_font ];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
specialArgs = inputs // {
|
||||
inherit self;
|
||||
inputs = inputs;
|
||||
host = "uConsole";
|
||||
system = "aarch64-linux";
|
||||
};
|
||||
trustCaches = false;
|
||||
modules = [
|
||||
@@ -14,6 +12,7 @@
|
||||
inputs.oom-hardware.nixosModules.uc.configtxt
|
||||
inputs.oom-hardware.nixosModules.uc.base-cm5
|
||||
self.nixosModules.uConsoleConfiguration
|
||||
self.nixosModules.uConsoleHome
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{ self, inputs, ... }:
|
||||
{
|
||||
flake.nixosModules.uConsoleHome =
|
||||
{ self, pkgs, ... }:
|
||||
{
|
||||
imports = [ self.nixosModules.desktopHomeBase ];
|
||||
|
||||
chiasson.users.extraModules.olivier = [
|
||||
{
|
||||
chiasson.home.hardware.uconsoleGamepad.enable = true;
|
||||
|
||||
chiasson.home.editors.vscode = {
|
||||
enable = true;
|
||||
extensions = with pkgs.vscode-extensions; [
|
||||
alefragnani.project-manager
|
||||
johnpapa.vscode-peacock
|
||||
];
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
# 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;
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
{ lib, ... }: {
|
||||
flake.lib.sshInventory =
|
||||
let
|
||||
hosts = {
|
||||
"14900k" = {
|
||||
hostName = "192.168.2.25";
|
||||
aliases = [ "14900k" "nixdesk" ];
|
||||
publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILwUevBGnf+Y/sL1ZsB4bt0c50a89iqwPRoYUGP4UHsL 14900k";
|
||||
};
|
||||
|
||||
ideapad = {
|
||||
hostName = "192.168.2.229";
|
||||
aliases = [ "ideapad" ];
|
||||
publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIQwaaI90xIMjZ46EcMyO8kBwGCxf7qVL75IYhw8Ssze ideapad";
|
||||
};
|
||||
|
||||
t2mbp = {
|
||||
hostName = "192.168.2.15";
|
||||
aliases = [ "t2mbp" ];
|
||||
publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMhVWB9YXl/FuQvufle4VWUas/QM8qCKoRd5a83Tt3S6 t2mbp";
|
||||
};
|
||||
|
||||
uConsole = {
|
||||
hostName = "192.168.2.99";
|
||||
aliases = [ "uConsole" "uconsole" ];
|
||||
publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAVPR0lRAcywPR7iTchM3+eO7NCdXAR6NPzYXxalr+dP uConsole";
|
||||
};
|
||||
|
||||
test = {
|
||||
hostName = "test";
|
||||
aliases = [ "test" ];
|
||||
publicKey = null;
|
||||
};
|
||||
|
||||
nix-server = {
|
||||
hostName = "192.168.2.238";
|
||||
aliases = [ "nix-server" ];
|
||||
publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL3KDicMjtOFR6LfZrFzfAD1gdYUdwv6ZM4PSgtmIuzd nix-server";
|
||||
};
|
||||
|
||||
r5500 = {
|
||||
hostName = "192.168.2.100";
|
||||
aliases = [ "r5500" ];
|
||||
publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK7iWCEtkYDLZFRF3w1gzyAok5VCAGUOwu4iWZdMjf3D r5500";
|
||||
};
|
||||
};
|
||||
|
||||
mkIdentityFileName = hostName: ".ssh/id_ed25519_${lib.strings.toLower hostName}.pub";
|
||||
activeHosts = builtins.removeAttrs hosts (
|
||||
builtins.filter (name: hosts.${name}.publicKey == null) (builtins.attrNames hosts)
|
||||
);
|
||||
|
||||
mkIdentityFiles = selectedHosts:
|
||||
builtins.listToAttrs (
|
||||
builtins.map
|
||||
(hostName: {
|
||||
name = mkIdentityFileName hostName;
|
||||
value.text = "${selectedHosts.${hostName}.publicKey}\n";
|
||||
})
|
||||
(builtins.attrNames selectedHosts)
|
||||
);
|
||||
|
||||
# Gitea git-over-SSH listens on port 222. System SSH (nix deploy, server@…) uses port 22
|
||||
# via the catalog `nix-server` Host block — never list nix-server or 192.168.2.238 here.
|
||||
giteaSshBlock = identityAgent: ''
|
||||
Host git.chiasson.cloud gitea
|
||||
HostName 192.168.2.238
|
||||
Port 222
|
||||
User git
|
||||
IdentityAgent ${identityAgent}
|
||||
IdentitiesOnly no
|
||||
|
||||
Match host nix-server,192.168.2.238 user git
|
||||
HostName 192.168.2.238
|
||||
Port 222
|
||||
User git
|
||||
IdentityAgent ${identityAgent}
|
||||
IdentitiesOnly no
|
||||
'';
|
||||
|
||||
mkSshConfigTemplate =
|
||||
{
|
||||
selectedHosts ? activeHosts,
|
||||
user ? null,
|
||||
identityAgent ? "__RBW_SSH_SOCK__",
|
||||
}:
|
||||
let
|
||||
hostBlocks = builtins.map
|
||||
(hostName:
|
||||
let
|
||||
entry = selectedHosts.${hostName};
|
||||
hostPatterns = builtins.concatStringsSep " " (entry.aliases ++ [ entry.hostName ]);
|
||||
userLine = if user == null then "" else " User ${user}\n";
|
||||
portLine =
|
||||
if hostName == "nix-server" then
|
||||
" Port 22\n"
|
||||
else
|
||||
"";
|
||||
in
|
||||
''
|
||||
Host ${hostPatterns}
|
||||
HostName ${entry.hostName}
|
||||
${userLine}${portLine} IdentityFile ~/${mkIdentityFileName hostName}
|
||||
IdentityAgent ${identityAgent}
|
||||
IdentitiesOnly yes
|
||||
'')
|
||||
(builtins.attrNames selectedHosts);
|
||||
in
|
||||
builtins.concatStringsSep "\n" (
|
||||
[
|
||||
(giteaSshBlock identityAgent)
|
||||
]
|
||||
++ hostBlocks
|
||||
++ [
|
||||
''
|
||||
Host *
|
||||
IdentitiesOnly yes
|
||||
IdentityAgent none
|
||||
''
|
||||
]
|
||||
);
|
||||
in
|
||||
{
|
||||
inherit hosts activeHosts mkIdentityFiles mkSshConfigTemplate;
|
||||
authorizedKeys = lib.unique (
|
||||
builtins.map (entry: entry.publicKey) (builtins.attrValues activeHosts)
|
||||
);
|
||||
identityFiles = mkIdentityFiles activeHosts;
|
||||
sshConfigTemplate = mkSshConfigTemplate { };
|
||||
};
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
# Pure helpers: catalog → NixOS/HM/SSH shapes (`self.lib.usersMerge lib`).
|
||||
{ ... }: {
|
||||
flake.lib.usersMerge =
|
||||
lib:
|
||||
let
|
||||
userHm = user: user.homeManager or { };
|
||||
userSsh = user: user.ssh or { };
|
||||
in
|
||||
rec {
|
||||
resolveHomeManagerModule =
|
||||
moduleSpec:
|
||||
let
|
||||
t = builtins.typeOf moduleSpec;
|
||||
in
|
||||
if t == "path" || t == "string" then import moduleSpec else moduleSpec;
|
||||
|
||||
selectedUsersAttr =
|
||||
{ catalog, enabled, hostOverrides }:
|
||||
lib.listToAttrs (
|
||||
map (name: {
|
||||
inherit name;
|
||||
value = lib.recursiveUpdate catalog.${name} (hostOverrides.${name} or { });
|
||||
}) enabled
|
||||
);
|
||||
|
||||
missingEnabledNames = catalog: enabled: builtins.filter (name: !(builtins.hasAttr name catalog)) enabled;
|
||||
|
||||
strayHomeUserKeys = homeUsers: enabled:
|
||||
builtins.filter (k: !(builtins.elem k enabled)) (builtins.attrNames homeUsers);
|
||||
|
||||
mkNixosUser =
|
||||
name: user:
|
||||
{
|
||||
isNormalUser = user.isNormalUser or true;
|
||||
description = user.description or name;
|
||||
extraGroups = user.extraGroups or [ ];
|
||||
}
|
||||
// lib.optionalAttrs (user ? hashedPasswordFile && user.hashedPasswordFile != null) {
|
||||
hashedPasswordFile = user.hashedPasswordFile;
|
||||
};
|
||||
|
||||
hmWiredNames =
|
||||
selectedUsers:
|
||||
lib.attrNames (
|
||||
lib.filterAttrs (_: user:
|
||||
let
|
||||
hm = userHm user;
|
||||
in
|
||||
(hm.enable or false) && (hm.module or null) != null
|
||||
) selectedUsers
|
||||
);
|
||||
|
||||
rbwOutboundSnippet =
|
||||
name: user:
|
||||
let
|
||||
outboundCfg = ((userSsh user).outbound or { }).rbw or { };
|
||||
in
|
||||
lib.mkIf (outboundCfg.enable or false) {
|
||||
chiasson.ssh.outbound.rbw.enable = true;
|
||||
chiasson.ssh.outbound.rbw.user = name;
|
||||
chiasson.ssh.outbound.rbw.hosts =
|
||||
if (outboundCfg.hosts or "all") == "all" then [ "all" ] else outboundCfg.hosts;
|
||||
};
|
||||
|
||||
mkHmUserModule =
|
||||
{ name, user, hostExtraModules }:
|
||||
let
|
||||
hm = userHm user;
|
||||
hmModule = resolveHomeManagerModule hm.module;
|
||||
in
|
||||
lib.mkMerge (
|
||||
[
|
||||
hmModule
|
||||
(rbwOutboundSnippet name user)
|
||||
]
|
||||
++ (hm.extraModules or [ ])
|
||||
++ hostExtraModules
|
||||
);
|
||||
|
||||
inboundAuthorizedOrNull =
|
||||
user:
|
||||
let
|
||||
inboundCfg = (userSsh user).inbound or { };
|
||||
in
|
||||
if !(inboundCfg.enable or false) then
|
||||
null
|
||||
else if (inboundCfg.authorizedHosts or "all") == "all" then
|
||||
"all"
|
||||
else
|
||||
inboundCfg.authorizedHosts;
|
||||
|
||||
inboundHostsAttr =
|
||||
selectedUsers:
|
||||
lib.pipe selectedUsers [
|
||||
(lib.mapAttrs (_: inboundAuthorizedOrNull))
|
||||
(lib.filterAttrs (_: v: v != null))
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -1,37 +1,48 @@
|
||||
{ self, ... }: {
|
||||
flake.homeManagerModules.sshOutboundRbw = {
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
flake.homeManagerModules.sshOutboundRbw =
|
||||
{ config, lib, pkgs, ... }:
|
||||
let
|
||||
cfg = config.chiasson.ssh.outbound.rbw;
|
||||
inventory = self.lib.sshInventory;
|
||||
# Resolve which hosts to emit blocks for, and which user identity to
|
||||
# use as the IdentityFile filter against the rbw agent.
|
||||
selectedHostNames =
|
||||
if cfg.hosts == [ "all" ] then builtins.attrNames inventory.activeHosts else cfg.hosts;
|
||||
missingHosts = builtins.filter (name: !(builtins.hasAttr name inventory.hosts)) selectedHostNames;
|
||||
# Resolve to the actual host attrs for the template. The "all" case
|
||||
# reuses `activeHosts` directly instead of rebuilding the same attrset.
|
||||
selectedHosts =
|
||||
if cfg.hosts == [ "all" ] then
|
||||
builtins.attrNames inventory.activeHosts
|
||||
inventory.activeHosts
|
||||
else
|
||||
cfg.hosts;
|
||||
missing = builtins.filter (name: !(builtins.hasAttr name inventory.hosts)) selectedHostNames;
|
||||
selectedHosts = builtins.listToAttrs (
|
||||
builtins.map (name: {
|
||||
inherit name;
|
||||
value = inventory.hosts.${name};
|
||||
builtins.listToAttrs (
|
||||
map (n: {
|
||||
name = n;
|
||||
value = inventory.hosts.${n};
|
||||
}) selectedHostNames
|
||||
);
|
||||
sshConfigTemplate = inventory.mkSshConfigTemplate {
|
||||
selectedHosts = selectedHosts;
|
||||
user = cfg.user;
|
||||
};
|
||||
# All identities this HM user may authenticate AS: cfg.user ∪
|
||||
# extraIdentities. `cfg.user` can be null (e.g. user not in inventory
|
||||
# yet) — in that case the Host-block IdentityFile line is suppressed
|
||||
# and cross-account SSH is wired from extraIdentities alone.
|
||||
effectiveIdentities = lib.filter (n: n != null) ([ cfg.user ] ++ cfg.extraIdentities);
|
||||
defaultUserHasKey = cfg.user == null || (inventory.users.${cfg.user}.publicKey or "") != "";
|
||||
# Each extra identity must have a pubkey in the inventory. Dropping
|
||||
# it silently would produce a dead Match block — fail the build.
|
||||
missingExtraKeys = lib.filter (n: (inventory.users.${n}.publicKey or "") == "") cfg.extraIdentities;
|
||||
in
|
||||
{
|
||||
options.chiasson.ssh.outbound.rbw = {
|
||||
enable = lib.mkEnableOption "Generated `~/.ssh/config` + rbw agent socket.";
|
||||
user = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = config.home.username;
|
||||
description = "`User` in generated `Host` blocks.";
|
||||
description = ''
|
||||
Inventory user whose public key is written to `~/.ssh/id_ed25519_<user>.pub`
|
||||
and used as `IdentityFile` to filter the rbw agent. Set `null`
|
||||
to disable the per-user .pub file / IdentityFile line (e.g.
|
||||
when the user isn't in the inventory yet).
|
||||
'';
|
||||
};
|
||||
hosts = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
@@ -41,36 +52,84 @@
|
||||
manageSshConfig = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = "Write `~/.ssh/config` from the template.";
|
||||
description = "Write `~/.ssh/config` (disable for manual management).";
|
||||
};
|
||||
# Cross-account SSH on the operator's laptop/desktop. Each entry
|
||||
# adds a `~/.ssh/id_ed25519_<account>.pub` filter file and a
|
||||
# `Match user <account>` block in `~/.ssh/config` so
|
||||
# `ssh <account>@<host>` picks the right key from the rbw agent.
|
||||
# `cfg.user` is already wired by the Host blocks — don't list it here.
|
||||
extraIdentities = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
example = [
|
||||
"server"
|
||||
"builder"
|
||||
];
|
||||
description = ''
|
||||
Catalog accounts this HM user may authenticate AS, in addition
|
||||
to `cfg.user`. Each entry must have a publicKey pasted into
|
||||
`modules/ssh/inventory.nix`. Typical for the operator's
|
||||
primary laptop: `[ "server" "builder" ]` so fleet/ops commands
|
||||
like `ssh server@r5500` or `ssh builder@nix-server` work
|
||||
without touching `/etc/passwd` on each host.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable (lib.mkMerge [
|
||||
config = lib.mkIf cfg.enable (
|
||||
lib.mkMerge [
|
||||
{
|
||||
assertions = [
|
||||
{
|
||||
assertion = missing == [ ];
|
||||
message = "ssh.outbound.rbw: unknown host keys: ${builtins.concatStringsSep ", " missing}";
|
||||
assertion = missingHosts == [ ];
|
||||
message = "ssh.outbound.rbw: unknown host keys: ${builtins.concatStringsSep ", " missingHosts}";
|
||||
}
|
||||
{
|
||||
assertion = defaultUserHasKey;
|
||||
message = "ssh.outbound.rbw: no publicKey for inventory user `${cfg.user}` in `modules/ssh/inventory.nix`.";
|
||||
}
|
||||
{
|
||||
assertion = missingExtraKeys == [ ];
|
||||
message = "ssh.outbound.rbw: extraIdentities has entries with no publicKey in `modules/ssh/inventory.nix`: ${builtins.concatStringsSep ", " missingExtraKeys}";
|
||||
}
|
||||
# `enable=true` with neither a default user nor any extras gives the
|
||||
# agent no IdentityFile at all; with IdentitiesOnly yes the user
|
||||
# would silently be unable to authenticate as any catalog user.
|
||||
{
|
||||
assertion = effectiveIdentities != [ ];
|
||||
message = "ssh.outbound.rbw: enabled but no identities wired. Set `chiasson.ssh.outbound.rbw.user` (defaults to `home.username`) or list catalog accounts under `extraIdentities`.";
|
||||
}
|
||||
];
|
||||
}
|
||||
(lib.mkMerge [
|
||||
{
|
||||
home.packages = [ pkgs.rbw pkgs.pinentry-qt ];
|
||||
home.sessionVariables.SSH_AUTH_SOCK = "$XDG_RUNTIME_DIR/rbw/ssh-agent-socket";
|
||||
home.file = inventory.mkIdentityFiles selectedHosts;
|
||||
|
||||
home.packages = with pkgs; [
|
||||
rbw
|
||||
pinentry-gtk2
|
||||
];
|
||||
home.sessionVariables.SSH_AUTH_SOCK = self.lib.rbwSshSocket.sessionVariable;
|
||||
# Write a `.pub` file per identity this HM user may act as
|
||||
# (cfg.user ∪ extraIdentities). OpenSSH reads each one via
|
||||
# IdentityFile/Match to filter agent keys — they never hold
|
||||
# private key material, so 0644 (HM's default) is fine.
|
||||
# OpenSSH's StrictModes only rejects group/other-writable files.
|
||||
home.file = lib.listToAttrs (
|
||||
map (n: {
|
||||
name = inventory.mkIdentityFileName n;
|
||||
value.text = "${inventory.users.${n}.publicKey}\n";
|
||||
}) effectiveIdentities
|
||||
);
|
||||
programs.ssh.enable = lib.mkIf cfg.manageSshConfig false;
|
||||
home.activation.rbwSshConfig = lib.mkIf cfg.manageSshConfig (lib.hm.dag.entryAfter [ "writeBoundary" ] ''
|
||||
mkdir -p "$HOME/.ssh"
|
||||
chmod 700 "$HOME/.ssh"
|
||||
RBW_SSH_SOCK="/run/user/$(id -u)/rbw/ssh-agent-socket"
|
||||
cat > "$HOME/.ssh/config" <<'EOF'
|
||||
${sshConfigTemplate}
|
||||
EOF
|
||||
sed -i "s|__RBW_SSH_SOCK__|$RBW_SSH_SOCK|g" "$HOME/.ssh/config"
|
||||
chmod 600 "$HOME/.ssh/config"
|
||||
'');
|
||||
|
||||
}
|
||||
(lib.mkIf cfg.manageSshConfig {
|
||||
home.file.".ssh/config".text = inventory.mkSshConfigTemplate {
|
||||
inherit selectedHosts;
|
||||
user = cfg.user;
|
||||
inherit (cfg) extraIdentities;
|
||||
};
|
||||
})
|
||||
{
|
||||
systemd.user.services.rbw-agent-bootstrap = {
|
||||
Unit = {
|
||||
Description = "Bootstrap rbw SSH agent";
|
||||
@@ -82,15 +141,14 @@ EOF
|
||||
ExecStart = "${pkgs.bash}/bin/bash -lc '${pkgs.rbw}/bin/rbw unlocked >/dev/null 2>&1 || true'";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
Install = {
|
||||
WantedBy = [ "graphical-session.target" ];
|
||||
Install.WantedBy = [ "graphical-session.target" ];
|
||||
};
|
||||
};
|
||||
|
||||
home.activation.rbwPinentryConfig = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
|
||||
${pkgs.rbw}/bin/rbw config set pinentry "${pkgs.pinentry-qt}/bin/pinentry-qt" >/dev/null 2>&1 || true
|
||||
${pkgs.rbw}/bin/rbw config set pinentry "${pkgs.pinentry-gtk2}/bin/pinentry-gtk-2" >/dev/null 2>&1 || true
|
||||
'';
|
||||
}
|
||||
]);
|
||||
])
|
||||
]
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
{ lib, ... }: {
|
||||
flake.lib.sshInventory =
|
||||
let
|
||||
# Hosts: pure network endpoints. No public keys live here anymore —
|
||||
# keys are per USER (see `users` below), one key per person.
|
||||
hosts = {
|
||||
"14900k" = {
|
||||
hostName = "192.168.2.25";
|
||||
aliases = [ "14900k" "nixdesk" ];
|
||||
};
|
||||
ideapad = {
|
||||
hostName = "192.168.2.229";
|
||||
aliases = [ "ideapad" ];
|
||||
};
|
||||
t2mbp = {
|
||||
hostName = "192.168.2.15";
|
||||
aliases = [ "t2mbp" ];
|
||||
};
|
||||
"uConsole" = {
|
||||
hostName = "192.168.2.99";
|
||||
aliases = [ "uConsole" "uconsole" ];
|
||||
};
|
||||
# Scratchpad / recovery target — hostName isn't an IP, so `activeHosts`
|
||||
# excludes it from navi deployments. Leave it in `hosts` for inventory
|
||||
# visibility while recovery is in progress.
|
||||
test = {
|
||||
hostName = "test";
|
||||
aliases = [ "test" ];
|
||||
};
|
||||
"nix-server" = {
|
||||
hostName = "192.168.2.238";
|
||||
aliases = [ "nix-server" ];
|
||||
};
|
||||
r5500 = {
|
||||
hostName = "192.168.2.100";
|
||||
aliases = [ "r5500" ];
|
||||
};
|
||||
};
|
||||
|
||||
# Per-user public keys. One keypair per person, generated once with
|
||||
# `ssh-keygen -t ed25519 -C <comment>` (or `rbw gen ssh-key <user>`),
|
||||
# private key stored in Bitwarden under the same name. Paste the
|
||||
# `ssh-ed25519 AAAA… <comment>` line here.
|
||||
users = {
|
||||
olivier = {
|
||||
publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICpORzDw8kEOobjywl7bQaEp6Tez1c/ZP+7VBDsTcM8I olivier";
|
||||
};
|
||||
server = {
|
||||
publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICsksvNGOt/ebJtpRi9ocLBjwyl16VSaf+BsL2Hz5PRy server";
|
||||
};
|
||||
builder = {
|
||||
publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIILBtfW1cq9mG6uAjAfpZuVa1EWDSTPSCThapDXXlLgV builder";
|
||||
};
|
||||
};
|
||||
|
||||
# Hosts SSH is actually safe to deploy/scan to: anything whose hostName
|
||||
# parses as an IPv4 dotted-quad. Filters out scratchpad/non-routable
|
||||
# entries (e.g. `hostName = "test"`).
|
||||
activeHosts = lib.filterAttrs
|
||||
(_: h: lib.match "^[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$" h.hostName != null)
|
||||
hosts;
|
||||
|
||||
# .pub filename convention. OpenSSH 7.3+ accepts a `.pub` file as
|
||||
# IdentityFile to filter `IdentityAgent` (rbw) keys without storing the
|
||||
# private key on disk — keep this trick.
|
||||
mkIdentityFileName = userName: ".ssh/id_ed25519_${lib.strings.toLower userName}.pub";
|
||||
|
||||
mkSshConfigTemplate =
|
||||
{
|
||||
selectedHosts ? activeHosts,
|
||||
user ? null,
|
||||
# Catalog accounts the operator may authenticate AS via
|
||||
# `ssh <account>@<host>`. Each entry gets a `Match user <account>`
|
||||
# block that overrides IdentityFile so OpenSSH asks the rbw agent
|
||||
# for that account's private key. `cfg.user` is already covered
|
||||
# by the Host blocks, so don't list it here.
|
||||
extraIdentities ? [ ],
|
||||
identityAgent ? "SSH_AUTH_SOCK",
|
||||
}:
|
||||
let
|
||||
# Shared `IdentityFile` filter line (when `user` is set). Used by
|
||||
# both the per-host and gitea blocks so the two can't drift apart.
|
||||
idLine = lib.optionalString (user != null) " IdentityFile ~/${mkIdentityFileName user}\n";
|
||||
|
||||
# Match blocks for cross-account SSH. Filter out cfg.user since
|
||||
# the matching pub is already loaded via the Host block's
|
||||
# IdentityFile line — re-emitting it would just duplicate.
|
||||
matchBlocks = lib.concatStringsSep "\n" (map
|
||||
(n: ''
|
||||
Match user ${n}
|
||||
IdentityFile ~/${mkIdentityFileName n}
|
||||
IdentitiesOnly yes
|
||||
'')
|
||||
(builtins.filter (n: n != user) extraIdentities));
|
||||
|
||||
# Per-host block. `IdentityFile` (when `user` is set) is a .pub file
|
||||
# pointing at the agent's matching private key. With IdentitiesOnly
|
||||
# yes, only agent keys matching that pub are tried — fixes the old
|
||||
# "too many auth failures" symptom by design.
|
||||
hostBlock = hostName:
|
||||
let
|
||||
e = selectedHosts.${hostName};
|
||||
hostPatterns = lib.concatStringsSep " " (e.aliases ++ [ e.hostName ]);
|
||||
in ''
|
||||
Host ${hostPatterns}
|
||||
HostName ${e.hostName}
|
||||
${idLine} IdentityAgent ${identityAgent}
|
||||
IdentitiesOnly yes
|
||||
'';
|
||||
|
||||
# Gitea block: always emitted. `User git`, port 222. olivier's
|
||||
# public key (passed via `IdentityFile`) is what gitea accepts
|
||||
# because it gets pushed into gitea's authorized_keys through
|
||||
# gitea's own UI for that account.
|
||||
giteaBlock = ''
|
||||
Host git.chiasson.cloud gitea
|
||||
HostName 192.168.2.238
|
||||
Port 222
|
||||
User git
|
||||
${idLine} IdentityAgent ${identityAgent}
|
||||
IdentitiesOnly yes
|
||||
'';
|
||||
|
||||
hostBlocks = lib.concatStringsSep "\n" (map hostBlock (lib.attrNames selectedHosts));
|
||||
in
|
||||
# Filter empty sections so an absent Match block (no extraIdentities)
|
||||
# doesn't add leading whitespace to the rendered config.
|
||||
lib.concatStringsSep "\n" (lib.filter (s: s != "") [
|
||||
matchBlocks
|
||||
giteaBlock
|
||||
hostBlocks
|
||||
''
|
||||
Host *
|
||||
IdentitiesOnly yes
|
||||
IdentityAgent none
|
||||
''
|
||||
]);
|
||||
|
||||
# Per-user line for `users.users.<u>.openssh.authorizedKeys.keys`.
|
||||
# `selection` is either "all" or a list of source identifiers for
|
||||
# sshd's `from=` constraint (IP, CIDR, or hostname if reverse-DNS works).
|
||||
# Returns null if inventory is missing the user's pubkey — caller is
|
||||
# expected to assert and fail the build rather than silently lock out.
|
||||
mkAuthorizedKeyLine = userName: selection:
|
||||
let
|
||||
entry = users.${userName} or null;
|
||||
in
|
||||
if entry == null then null
|
||||
else if (entry.publicKey or "") == "" then null
|
||||
else if selection == "all" then entry.publicKey
|
||||
else ''from="${lib.concatStringsSep "," selection}" ${entry.publicKey}'';
|
||||
in
|
||||
{
|
||||
inherit hosts users activeHosts;
|
||||
mkSshConfigTemplate = mkSshConfigTemplate;
|
||||
mkAuthorizedKeyLine = mkAuthorizedKeyLine;
|
||||
mkIdentityFileName = mkIdentityFileName;
|
||||
};
|
||||
}
|
||||
@@ -1,35 +1,51 @@
|
||||
{ self, ... }: {
|
||||
flake.nixosModules.sshInbound = {
|
||||
config,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
flake.nixosModules.sshInbound =
|
||||
{ config, lib, ... }:
|
||||
let
|
||||
cfg = config.chiasson.ssh.inbound;
|
||||
inventory = self.lib.sshInventory;
|
||||
# Every inbound-enabled user must have a publicKey pasted into the
|
||||
# inventory — silently dropping the key would lock the user out.
|
||||
missingKeys = lib.filter
|
||||
(name:
|
||||
let line = inventory.mkAuthorizedKeyLine name (cfg.userAuthorizedHosts.${name} or "all");
|
||||
in line == null)
|
||||
(lib.attrNames cfg.userAuthorizedHosts);
|
||||
in
|
||||
{
|
||||
options.chiasson.ssh.inbound = {
|
||||
enable = lib.mkEnableOption "Apply SSH inventory public keys to `authorized_keys`.";
|
||||
enable = lib.mkEnableOption "Write user-specific SSH inventory public keys into `authorized_keys`.";
|
||||
userAuthorizedHosts = lib.mkOption {
|
||||
type = lib.types.attrsOf (lib.types.either (lib.types.enum [ "all" ]) (lib.types.listOf lib.types.str));
|
||||
default = { };
|
||||
example = {
|
||||
olivier = "all";
|
||||
admin = [ "14900k" "t2mbp" ];
|
||||
server = [ "192.168.2.25" "192.168.2.15" ];
|
||||
};
|
||||
description = ''
|
||||
Catalog users that receive the SSH inventory public keys in `authorized_keys`.
|
||||
Catalog users whose inventory public key is installed in
|
||||
`authorized_keys`. The value is either `"all"` (no `from=`
|
||||
restriction) or a list of source identifiers appended to
|
||||
sshd's `from=` constraint. List entries should be IPs or CIDRs;
|
||||
hostnames require reverse-DNS to resolve cleanly on the target.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
config = lib.mkMerge [
|
||||
{
|
||||
assertions = [{
|
||||
assertion = cfg.enable -> missingKeys == [ ];
|
||||
message = "chiasson.ssh.inbound: users enabled but missing public key in `modules/ssh/inventory.nix`: ${lib.concatStringsSep ", " missingKeys}";
|
||||
}];
|
||||
}
|
||||
(lib.mkIf cfg.enable {
|
||||
users.users = lib.mapAttrs
|
||||
(_user: _selection: {
|
||||
openssh.authorizedKeys.keys = inventory.authorizedKeys;
|
||||
(name: selection: {
|
||||
openssh.authorizedKeys.keys = [ (inventory.mkAuthorizedKeyLine name selection) ];
|
||||
})
|
||||
cfg.userAuthorizedHosts;
|
||||
};
|
||||
})
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# rbw (Bitwarden) SSH-agent socket wiring, shared across shells and the
|
||||
# outbound-rbw Home Manager module. Exposed as `flake.lib.rbwSshSocket`.
|
||||
{ ... }: {
|
||||
flake.lib.rbwSshSocket =
|
||||
let
|
||||
socketPath = "$XDG_RUNTIME_DIR/rbw/ssh-agent-socket";
|
||||
|
||||
# Bash init snippet — used by wisdom/shells/bash.nix (profile + interactive).
|
||||
bashSnippet = ''
|
||||
if [ -z "''${SSH_AUTH_SOCK:-}" ]; then
|
||||
if [ -n "''${XDG_RUNTIME_DIR:-}" ]; then
|
||||
export SSH_AUTH_SOCK="${socketPath}"
|
||||
else
|
||||
export SSH_AUTH_SOCK="/run/user/$(id -u)/rbw/ssh-agent-socket"
|
||||
fi
|
||||
fi
|
||||
'';
|
||||
|
||||
# Fish init snippet — used by wisdom/shells/fish.nix.
|
||||
fishSnippet = pkgs: ''
|
||||
if test -z "$SSH_AUTH_SOCK"
|
||||
if test -n "$XDG_RUNTIME_DIR"
|
||||
set -gx SSH_AUTH_SOCK "${socketPath}"
|
||||
else
|
||||
set -l _hm_uid (${pkgs.coreutils}/bin/id -u)
|
||||
set -gx SSH_AUTH_SOCK "/run/user/$_hm_uid/rbw/ssh-agent-socket"
|
||||
end
|
||||
end
|
||||
'';
|
||||
in
|
||||
{
|
||||
inherit bashSnippet fishSnippet;
|
||||
# Used by home-manager modules to expose SSH_AUTH_SOCK to GUI apps
|
||||
# and HM-managed shells declaratively (no activation script needed).
|
||||
sessionVariable = socketPath;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -5,17 +5,20 @@
|
||||
self.nixosModules.systemLocalization
|
||||
self.nixosModules.systemFonts
|
||||
self.nixosModules.systemNetworking
|
||||
self.nixosModules.systemTailscale
|
||||
self.nixosModules.systemLocalsend
|
||||
self.nixosModules.systemChromiumHevcVaapi
|
||||
self.nixosModules.systemMonitorInput
|
||||
self.nixosModules.systemSpotify
|
||||
self.nixosModules.systemPackagesDefaults
|
||||
self.nixosModules.systemDocker
|
||||
self.nixosModules.systemDockerRegistryAuth
|
||||
self.nixosModules.systemFlatpak
|
||||
self.nixosModules.systemAudio
|
||||
self.nixosModules.systemIdeapadMrubyOverlay
|
||||
self.nixosModules.systemYtDlpTelequebecPatch
|
||||
self.nixosModules.systemGaming
|
||||
self.nixosModules.systemGpuPassthru
|
||||
self.nixosModules.systemUconsoleKernelBuilder
|
||||
self.nixosModules.systemLibrepods
|
||||
self.nixosModules.systemPalera1n
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# oci-containers runs `docker login` in ExecStartPre, which stores plaintext credentials
|
||||
# in /root/.docker/config.json. For any container with `login` set, use in-memory
|
||||
# DOCKER_AUTH_CONFIG instead (no per-service list to maintain).
|
||||
{ ... }: {
|
||||
flake.nixosModules.systemDockerRegistryAuth =
|
||||
{ config, lib, pkgs, ... }:
|
||||
let
|
||||
cfg = config.chiasson.system.docker.ephemeralRegistryAuth;
|
||||
ociCfg = lib.attrByPath [ "virtualisation" "oci-containers" ] null config;
|
||||
backend = if ociCfg == null then "docker" else ociCfg.backend or "docker";
|
||||
|
||||
containerBackend =
|
||||
if backend == "podman" then
|
||||
lib.attrByPath [ "virtualisation" "podman" "package" ] pkgs.podman config
|
||||
else
|
||||
lib.attrByPath [ "virtualisation" "docker" "package" ] pkgs.docker config;
|
||||
|
||||
isValidLogin =
|
||||
login: login.registry != null && login.username != null && login.passwordFile != null;
|
||||
|
||||
mkRegistryAuthPreStart =
|
||||
login: lib.mkBefore ''
|
||||
export DOCKER_AUTH_CONFIG="$(${pkgs.jq}/bin/jq -cn \
|
||||
--arg registry ${lib.escapeShellArg login.registry} \
|
||||
--arg user ${lib.escapeShellArg login.username} \
|
||||
--arg pass "$(${pkgs.coreutils}/bin/tr -d '\n\r' < ${login.passwordFile})" \
|
||||
'{auths: {($registry): {username: $user, password: $pass, auth: (($user + ":" + $pass) | @base64)}}}')"
|
||||
'';
|
||||
|
||||
mkPreStartWithoutLogin =
|
||||
name: container:
|
||||
pkgs.writeShellScript "oci-pre-start-${name}" ''
|
||||
${containerBackend}/bin/${backend} rm -f ${lib.escapeShellArg name} || true
|
||||
${
|
||||
lib.optionalString (container.imageFile != null) ''
|
||||
${containerBackend}/bin/${backend} load -i ${container.imageFile}
|
||||
''
|
||||
}
|
||||
${
|
||||
lib.optionalString (container.imageStream != null) ''
|
||||
${container.imageStream} | ${containerBackend}/bin/${backend} load
|
||||
''
|
||||
}
|
||||
'';
|
||||
in
|
||||
{
|
||||
options.chiasson.system.docker.ephemeralRegistryAuth = {
|
||||
enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Authenticate oci-containers registry pulls via in-memory `DOCKER_AUTH_CONFIG`
|
||||
instead of `docker login`. Applies automatically to every container that sets
|
||||
`virtualisation.oci-containers.containers.<name>.login`.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
# Read merged container config only when defining systemd overrides — never write back
|
||||
# to `virtualisation.oci-containers.containers` (that causes infinite recursion).
|
||||
config =
|
||||
let
|
||||
containers =
|
||||
lib.attrByPath [ "virtualisation" "oci-containers" "containers" ] { } config;
|
||||
containersWithLogin = lib.filterAttrs (_: c: isValidLogin (c.login or { })) containers;
|
||||
in
|
||||
lib.mkIf (cfg.enable && containersWithLogin != { }) {
|
||||
systemd.services = lib.mapAttrs' (
|
||||
name: container:
|
||||
lib.nameValuePair (container.serviceName or "${backend}-${name}") {
|
||||
preStart = mkRegistryAuthPreStart container.login;
|
||||
serviceConfig.ExecStartPre = lib.mkForce [
|
||||
"${mkPreStartWithoutLogin name container}"
|
||||
];
|
||||
}
|
||||
) containersWithLogin;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -17,10 +17,9 @@
|
||||
++ lib.optionals pkgs.stdenv.isx86_64 [ heroic ];
|
||||
|
||||
steamExtraPkgs =
|
||||
if !cfg.steam.steamTinkerLaunch.enable then
|
||||
[ ]
|
||||
else
|
||||
lib.optional (lib.meta.availableOn pkgs.stdenv.hostPlatform pkgs.steamtinkerlaunch) pkgs.steamtinkerlaunch;
|
||||
lib.optional (cfg.steam.steamTinkerLaunch.enable && lib.meta.availableOn pkgs.stdenv.hostPlatform pkgs.steamtinkerlaunch) pkgs.steamtinkerlaunch;
|
||||
|
||||
launcherSet = launcherPkgs ++ steamExtraPkgs ++ cfg.launchers.extraPackages;
|
||||
in
|
||||
{
|
||||
options.chiasson.system.gaming = {
|
||||
@@ -194,11 +193,11 @@
|
||||
];
|
||||
}
|
||||
(lib.mkIf (cfg.launchers.forUsers == null) {
|
||||
environment.systemPackages = launcherPkgs ++ steamExtraPkgs ++ cfg.launchers.extraPackages;
|
||||
environment.systemPackages = launcherSet;
|
||||
})
|
||||
(lib.mkIf (cfg.launchers.forUsers != null) {
|
||||
users.users = lib.genAttrs cfg.launchers.forUsers (_: {
|
||||
packages = launcherPkgs ++ steamExtraPkgs ++ cfg.launchers.extraPackages;
|
||||
packages = launcherSet;
|
||||
});
|
||||
})
|
||||
]);
|
||||
|
||||
@@ -0,0 +1,383 @@
|
||||
{ ... }: {
|
||||
flake.nixosModules.systemGpuPassthru =
|
||||
{ config, lib, pkgs, ... }:
|
||||
let
|
||||
cfg = config.chiasson.system.gpuPassthru;
|
||||
vfioIds = lib.concatStringsSep "," cfg.pciIds;
|
||||
# Must match the Windows host installer build (B7-244-4bb2c58f).
|
||||
lgRev = "4bb2c58fb6d0df9e863ad45924dd4decc7e9cf4e";
|
||||
lgVersion = "B7-244-4bb2c58f";
|
||||
# Tarball hashes omit submodules; fetchSubmodules needs the git tree hash.
|
||||
lgHash = "sha256-Dc0sFnJVM0xJ0FfIQqGQKxFUj6mhp3qgB772h3Dgowc=";
|
||||
# https://wiki.nixos.org/wiki/Looking_Glass — systemd 258+ needs namespaces=[]
|
||||
# and an explicit ACL; seccomp can block mmap on /dev/kvmfr0.
|
||||
lgQemuVerbatimConfig = ''
|
||||
namespaces = []
|
||||
seccomp_sandbox = 0
|
||||
security_driver = "none"
|
||||
security_default_confined = 0
|
||||
security_require_confined = 0
|
||||
cgroup_device_acl = [
|
||||
"/dev/null", "/dev/full", "/dev/zero",
|
||||
"/dev/random", "/dev/urandom",
|
||||
"/dev/ptmx", "/dev/kvm", "/dev/kqemu",
|
||||
"/dev/rtc", "/dev/hpet",
|
||||
"/dev/vfio/", "/dev/vfio/vfio",
|
||||
"/dev/kvmfr0", "c 234:* rwm",
|
||||
"/dev/disk/", "/dev/disk/by-id/",
|
||||
"/dev/bus/usb/",
|
||||
]
|
||||
'';
|
||||
in
|
||||
{
|
||||
options.chiasson.system.gpuPassthru = {
|
||||
enable = lib.mkEnableOption ''
|
||||
Bind passthrough GPU(s) to VFIO, run the host on Intel iGPU, and enable libvirt +
|
||||
Looking Glass for a Windows gaming VM.
|
||||
|
||||
Prerequisites (manual, one-time):
|
||||
- BIOS: VT-d/IOMMU on, Above 4G Decoding / Resizable BAR on
|
||||
- Move host monitors to motherboard (Intel) ports; dummy HDMI/DP plug on passthrough GPU
|
||||
- Wipe the VM disk before first boot: `wipefs -a <windowsDisk>`
|
||||
- Verify IOMMU groups after switch: GPU functions should be isolated
|
||||
|
||||
Rollout: leave `enable = false` until cabling and disk backup are done, then rebuild and switch.
|
||||
'';
|
||||
|
||||
pciIds = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
example = [ "10de:1f07" "10de:10f9" "10de:1ada" "10de:1adb" ];
|
||||
description = ''
|
||||
PCI vendor:device IDs to bind to vfio-pci (all GPU functions: VGA, audio, USB, etc.).
|
||||
'';
|
||||
};
|
||||
|
||||
windowsDisk = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "";
|
||||
example = "/dev/disk/by-id/ata-CT1000MX500SSD1_1830E14B054A";
|
||||
description = ''
|
||||
Whole-disk by-id path for the Windows VM (raw block passthrough). Must not be mounted by the host.
|
||||
'';
|
||||
};
|
||||
|
||||
lookingGlass = {
|
||||
enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Load the kvmfr kernel module and grant QEMU access to /dev/kvmfr0 for Looking Glass
|
||||
(required for NVIDIA VFIO; plain /dev/shm IVSHMEM causes DMA mapping failures).
|
||||
'';
|
||||
};
|
||||
|
||||
shmSizeMiB = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
default = 32;
|
||||
description = ''
|
||||
kvmfr shared memory size (MiB). Must match `kvmfr.static_size_mb` and the VM
|
||||
`memory-backend-file` size in the domain XML.
|
||||
'';
|
||||
};
|
||||
|
||||
captureOnFocus = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Grab keyboard/mouse when the Looking Glass window is focused. Set `false` to
|
||||
keep input on the Linux host while viewing the guest.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
vm = {
|
||||
enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Declaratively define the Windows VM via NixVirt (host module must import NixVirt and set
|
||||
`virtualisation.libvirt.connections`).
|
||||
'';
|
||||
};
|
||||
|
||||
name = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "win11";
|
||||
description = "libvirt domain name.";
|
||||
};
|
||||
|
||||
memoryGiB = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
default = 16;
|
||||
description = "VM RAM in GiB (informational; domain XML is authoritative).";
|
||||
};
|
||||
|
||||
vcpus = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
default = 8;
|
||||
description = "vCPU count (informational; domain XML is authoritative).";
|
||||
};
|
||||
|
||||
installMode = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Use install domain (ISO, no GPU). Set `false` after Windows is installed.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
hugepages.enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = "Reserve 2 MiB hugepages for Looking Glass IVSHMEM (enable if frame drops).";
|
||||
};
|
||||
|
||||
libvirtUsers = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
description = "Users added to libvirtd, kvm, and disk groups.";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable (lib.mkMerge [
|
||||
{
|
||||
assertions = [
|
||||
{
|
||||
assertion = cfg.pciIds != [ ];
|
||||
message = "chiasson.system.gpuPassthru: pciIds must list every passthrough GPU function.";
|
||||
}
|
||||
{
|
||||
assertion = cfg.windowsDisk != "";
|
||||
message = "chiasson.system.gpuPassthru: windowsDisk must be set to a /dev/disk/by-id/ path.";
|
||||
}
|
||||
{
|
||||
assertion = cfg.libvirtUsers != [ ];
|
||||
message = "chiasson.system.gpuPassthru: libvirtUsers must name at least one user.";
|
||||
}
|
||||
];
|
||||
|
||||
boot.kernelParams = [
|
||||
"intel_iommu=on"
|
||||
"iommu=pt"
|
||||
"kvm.ignore_msrs=1"
|
||||
"vfio-pci.ids=${vfioIds}"
|
||||
];
|
||||
|
||||
boot.initrd.kernelModules = [
|
||||
"vfio_pci"
|
||||
"vfio"
|
||||
"vfio_iommu_type1"
|
||||
];
|
||||
|
||||
boot.blacklistedKernelModules = [
|
||||
"nouveau"
|
||||
"nvidia"
|
||||
"nvidia_drm"
|
||||
"nvidia_modeset"
|
||||
"i2c_nvidia_gpu"
|
||||
"mt7921e"
|
||||
"mt7925e"
|
||||
];
|
||||
|
||||
boot.extraModprobeConfig = ''
|
||||
softdep nvidia pre: vfio-pci
|
||||
softdep nvidia_drm pre: vfio-pci
|
||||
softdep nouveau pre: vfio-pci
|
||||
options vfio_iommu_type1 allow_unsafe_interrupts=1
|
||||
'';
|
||||
|
||||
services.xserver.videoDrivers = lib.mkForce [ "modesetting" ];
|
||||
|
||||
hardware.nvidia-container-toolkit.enable = lib.mkForce false;
|
||||
chiasson.system.gaming.sunshine.cudaSupport = lib.mkForce false;
|
||||
|
||||
# libvirt enables ZFS storage pools on Linux by default, which pulls in
|
||||
# zfs-user (currently broken to build on nixos-unstable). Raw block disk
|
||||
# passthrough does not need libvirt ZFS support.
|
||||
nixpkgs.overlays =
|
||||
[
|
||||
(final: prev: {
|
||||
libvirt = prev.libvirt.override { enableZfs = false; };
|
||||
})
|
||||
]
|
||||
++ lib.optionals cfg.lookingGlass.enable [
|
||||
(final: prev: {
|
||||
looking-glass-client = prev.looking-glass-client.overrideAttrs (old: {
|
||||
version = lgVersion;
|
||||
src = prev.fetchFromGitHub {
|
||||
owner = "gnif";
|
||||
repo = "LookingGlass";
|
||||
rev = lgRev;
|
||||
hash = lgHash;
|
||||
fetchSubmodules = true;
|
||||
};
|
||||
patches = old.patches;
|
||||
postUnpack = ''
|
||||
echo ${lgRev} > $sourceRoot/VERSION
|
||||
export sourceRoot="$sourceRoot/client"
|
||||
'';
|
||||
});
|
||||
})
|
||||
];
|
||||
|
||||
programs.virt-manager.enable = true;
|
||||
virtualisation.spiceUSBRedirection.enable = true;
|
||||
|
||||
virtualisation.libvirtd = {
|
||||
enable = true;
|
||||
onBoot = "ignore";
|
||||
onShutdown = "shutdown";
|
||||
qemu = {
|
||||
package = pkgs.qemu_kvm;
|
||||
# GPU + raw block passthrough needs root (or fragile per-device ACLs).
|
||||
runAsRoot = true;
|
||||
swtpm.enable = true;
|
||||
verbatimConfig = lib.mkIf cfg.lookingGlass.enable (lib.mkForce lgQemuVerbatimConfig);
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.libvirtd.restartTriggers = [
|
||||
"${pkgs.qemu_kvm}/bin/qemu-system-x86_64"
|
||||
];
|
||||
|
||||
environment.systemPackages =
|
||||
with pkgs;
|
||||
[
|
||||
virt-manager
|
||||
qemu_kvm
|
||||
OVMF
|
||||
edk2
|
||||
looking-glass-client
|
||||
swtpm
|
||||
]
|
||||
++ lib.optionals cfg.lookingGlass.enable [
|
||||
(writeShellScriptBin "looking-glass-toggle" ''
|
||||
set -euo pipefail
|
||||
pkill -f 'looking-glass-client' || true
|
||||
exec looking-glass-client -F -m 97
|
||||
'')
|
||||
];
|
||||
|
||||
# B7 defaults to /dev/kvmfr0; pin SPICE to the fixed port in the domain XMLs.
|
||||
environment.etc."looking-glass-client.ini" = lib.mkIf cfg.lookingGlass.enable (lib.mkForce {
|
||||
text = ''
|
||||
[app]
|
||||
shmFile=/dev/kvmfr0
|
||||
allowDMA=yes
|
||||
|
||||
[win]
|
||||
fullScreen=yes
|
||||
|
||||
[spice]
|
||||
enable=yes
|
||||
host=127.0.0.1
|
||||
port=5900
|
||||
input=yes
|
||||
audio=yes
|
||||
clipboard=yes
|
||||
|
||||
[input]
|
||||
captureOnFocus=${lib.boolToString cfg.lookingGlass.captureOnFocus}
|
||||
escapeKey=97
|
||||
'';
|
||||
});
|
||||
|
||||
users.groups.libvirtd.members = cfg.libvirtUsers;
|
||||
users.users =
|
||||
lib.genAttrs cfg.libvirtUsers (_: {
|
||||
extraGroups = [
|
||||
"libvirtd"
|
||||
"kvm"
|
||||
"disk"
|
||||
];
|
||||
})
|
||||
// {
|
||||
qemu-libvirtd.extraGroups = [
|
||||
"kvm"
|
||||
"disk"
|
||||
];
|
||||
};
|
||||
}
|
||||
{
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /var/lib/libvirt/boot 0755 root root -"
|
||||
];
|
||||
}
|
||||
(lib.mkIf cfg.lookingGlass.enable {
|
||||
boot.extraModulePackages = lib.mkAfter [ config.boot.kernelPackages.kvmfr ];
|
||||
boot.initrd.kernelModules = lib.mkAfter [ "kvmfr" ];
|
||||
boot.kernelModules = lib.mkAfter [ "kvmfr" ];
|
||||
boot.kernelParams = lib.mkAfter [
|
||||
"kvmfr.static_size_mb=${toString cfg.lookingGlass.shmSizeMiB}"
|
||||
];
|
||||
|
||||
boot.extraModprobeConfig = lib.mkAfter ''
|
||||
options kvmfr static_size_mb=${toString cfg.lookingGlass.shmSizeMiB}
|
||||
'';
|
||||
|
||||
services.udev.packages = [
|
||||
(pkgs.writeTextFile {
|
||||
name = "kvmfr-udev";
|
||||
text = ''
|
||||
SUBSYSTEM=="kvmfr", GROUP="kvm", MODE="0660", TAG+="uaccess"
|
||||
'';
|
||||
destination = "/etc/udev/rules.d/70-kvmfr.rules";
|
||||
})
|
||||
];
|
||||
|
||||
# insmod from the nix store: extraModulePackages only lands in
|
||||
# /run/booted-system/kernel-modules after a reboot.
|
||||
systemd.services.kvmfr = {
|
||||
description = "Looking Glass kvmfr module";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
before = [
|
||||
"libvirtd.service"
|
||||
"libvirtd-config.service"
|
||||
"libvirt-guests.service"
|
||||
];
|
||||
after = [ "systemd-modules-load.service" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
ExecStart = pkgs.writeShellScript "kvmfr-start" (
|
||||
let
|
||||
kvmfrKo = "${config.boot.kernelPackages.kvmfr}/lib/modules/${config.boot.kernelPackages.kernel.modDirVersion}/kernel/drivers/misc/kvmfr.ko";
|
||||
shmMiB = toString cfg.lookingGlass.shmSizeMiB;
|
||||
in
|
||||
''
|
||||
set -euo pipefail
|
||||
if [ -e /dev/kvmfr0 ] && [ ! -c /dev/kvmfr0 ]; then
|
||||
rm -f /dev/kvmfr0
|
||||
fi
|
||||
if [ -c /dev/kvmfr0 ]; then
|
||||
exit 0
|
||||
fi
|
||||
if ${pkgs.kmod}/bin/lsmod | ${pkgs.gnugrep}/bin/grep -q '^kvmfr '; then
|
||||
${pkgs.kmod}/bin/rmmod kvmfr
|
||||
fi
|
||||
if [ ! -f ${kvmfrKo} ]; then
|
||||
echo "kvmfr: module missing at ${kvmfrKo}" >&2
|
||||
exit 1
|
||||
fi
|
||||
${pkgs.kmod}/bin/insmod ${kvmfrKo} static_size_mb=${shmMiB}
|
||||
if [ ! -c /dev/kvmfr0 ]; then
|
||||
echo "kvmfr: /dev/kvmfr0 not created (static_size_mb=${shmMiB})" >&2
|
||||
exit 1
|
||||
fi
|
||||
''
|
||||
);
|
||||
};
|
||||
};
|
||||
systemd.services.libvirtd = lib.mkIf cfg.lookingGlass.enable {
|
||||
after = [ "kvmfr.service" ];
|
||||
requires = [ "kvmfr.service" ];
|
||||
};
|
||||
})
|
||||
(lib.mkIf cfg.hugepages.enable {
|
||||
boot.kernelParams = [ "hugepagesz=2M" "hugepages=64" ];
|
||||
})
|
||||
]);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
# NFS mount of nixdesk (14900k) bulk storage for clients. Exports live in
|
||||
# modules/hosts/14900k/_private/jellyfin-nfs-export.nix
|
||||
#
|
||||
# Jellyfin library paths (see also 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
|
||||
{ ... }:
|
||||
{
|
||||
flake.nixosModules.nixdeskJellyfin =
|
||||
{ lib, ... }:
|
||||
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;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
wget
|
||||
unzip
|
||||
jq
|
||||
ripgrep
|
||||
pfetch
|
||||
];
|
||||
description = "Default packages to install on all hosts.";
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
{ ... }: {
|
||||
flake.nixosModules.systemTailscale =
|
||||
{ config, lib, ... }:
|
||||
let
|
||||
cfg = config.chiasson.system.networking.tailscale;
|
||||
tsCfg = config.services.tailscale;
|
||||
in
|
||||
{
|
||||
options.chiasson.system.networking.tailscale = {
|
||||
enable = lib.mkEnableOption ''
|
||||
Tailscale mesh VPN. Joins this host to your tailnet for encrypted access
|
||||
from anywhere without opening inbound ports on your router.
|
||||
'';
|
||||
|
||||
authKeyFile = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
default = null;
|
||||
example = "/run/secrets/tailscale/auth-key";
|
||||
description = ''
|
||||
Reusable Tailscale auth key (sops path). Create one at
|
||||
https://login.tailscale.com/admin/settings/keys — enable Reusable and
|
||||
Pre-approved. Store under `tailscale/auth-key` in secrets/secrets.yaml.
|
||||
'';
|
||||
};
|
||||
|
||||
openFirewall = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = "Allow Tailscale UDP 41641 through the host firewall.";
|
||||
};
|
||||
|
||||
acceptRoutes = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Accept subnet routes advertised by other tailnet nodes (e.g. home LAN
|
||||
via nix-server). Enable on portable clients like t2mbp.
|
||||
'';
|
||||
};
|
||||
|
||||
subnetRouter = {
|
||||
enable = lib.mkEnableOption ''
|
||||
Advertise the home LAN through this node so other tailnet devices can
|
||||
reach local IPs (SSH, Attic, Gitea, Jellyfin, etc.). Enable on an
|
||||
always-on home host (nix-server). Approve the route in the Tailscale
|
||||
admin console after first connect.
|
||||
'';
|
||||
|
||||
routes = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ "192.168.2.0/24" ];
|
||||
example = [ "192.168.2.0/24" ];
|
||||
description = "Subnets to advertise to the tailnet.";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable (lib.mkMerge [
|
||||
{
|
||||
services.tailscale = {
|
||||
enable = true;
|
||||
openFirewall = cfg.openFirewall;
|
||||
authKeyFile = cfg.authKeyFile;
|
||||
};
|
||||
|
||||
networking.firewall.trustedInterfaces = lib.mkAfter [ "tailscale0" ];
|
||||
}
|
||||
(lib.mkIf cfg.acceptRoutes {
|
||||
services.tailscale.useRoutingFeatures = "client";
|
||||
services.tailscale.extraUpFlags = [ "--accept-routes" ];
|
||||
})
|
||||
(lib.mkIf cfg.subnetRouter.enable {
|
||||
services.tailscale.useRoutingFeatures = "both";
|
||||
services.tailscale.extraUpFlags =
|
||||
map (route: "--advertise-routes=${route}") cfg.subnetRouter.routes;
|
||||
})
|
||||
(lib.mkIf (cfg.authKeyFile != null) {
|
||||
# Upstream uses `$(cat $authKeyFile)` which breaks when sops leaves a trailing newline.
|
||||
systemd.services.tailscaled-autoconnect.script = lib.mkOverride 50 ''
|
||||
getState() {
|
||||
tailscale status --json --peers=false | jq -r '.BackendState'
|
||||
}
|
||||
|
||||
lastState=""
|
||||
while state="$(getState)"; do
|
||||
if [[ "$state" != "$lastState" ]]; then
|
||||
case "$state" in
|
||||
NeedsLogin|NeedsMachineAuth|Stopped)
|
||||
echo "Server needs authentication, sending auth key"
|
||||
tailscale up --auth-key "$(tr -d '\n\r' < ${cfg.authKeyFile})" ${lib.escapeShellArgs tsCfg.extraUpFlags}
|
||||
;;
|
||||
Running)
|
||||
echo "Tailscale is running"
|
||||
systemd-notify --ready
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Waiting for Tailscale State = Running or systemd timeout"
|
||||
;;
|
||||
esac
|
||||
echo "State = $state"
|
||||
fi
|
||||
lastState="$state"
|
||||
sleep .5
|
||||
done
|
||||
'';
|
||||
})
|
||||
]);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
# Users & SSH inbound: options + catalog defaults + integration. Single module
|
||||
# that used to be split across `modules/system/users/{default,catalog-options,
|
||||
# catalog-default,home-integration}.nix`. Per-host overrides / extra HM modules
|
||||
# work the same way they always have; only the file structure changed.
|
||||
{ self, ... }:
|
||||
{
|
||||
flake.nixosModules.users =
|
||||
{ config, options, lib, ... }:
|
||||
let
|
||||
cfg = config.chiasson.users;
|
||||
usersLib =
|
||||
let
|
||||
userHm = user: user.homeManager or { };
|
||||
userSsh = user: user.ssh or { };
|
||||
in
|
||||
rec {
|
||||
resolveHomeManagerModule =
|
||||
moduleSpec:
|
||||
let
|
||||
t = builtins.typeOf moduleSpec;
|
||||
in
|
||||
if t == "path" || t == "string" then import moduleSpec else moduleSpec;
|
||||
|
||||
selectedUsersAttr =
|
||||
{ catalog, enabled, hostOverrides }:
|
||||
lib.listToAttrs (
|
||||
map (name: {
|
||||
inherit name;
|
||||
value = lib.recursiveUpdate catalog.${name} (hostOverrides.${name} or { });
|
||||
}) enabled
|
||||
);
|
||||
|
||||
missingEnabledNames =
|
||||
catalog: enabled: builtins.filter (name: !(builtins.hasAttr name catalog)) enabled;
|
||||
|
||||
strayHomeUserKeys =
|
||||
homeUsers: enabled: builtins.filter (k: !(builtins.elem k enabled)) (builtins.attrNames homeUsers);
|
||||
|
||||
mkNixosUser =
|
||||
name: user:
|
||||
{
|
||||
isNormalUser = user.isNormalUser or true;
|
||||
description = user.description or name;
|
||||
extraGroups = user.extraGroups or [ ];
|
||||
}
|
||||
// lib.optionalAttrs (user ? hashedPasswordFile && user.hashedPasswordFile != null) {
|
||||
hashedPasswordFile = user.hashedPasswordFile;
|
||||
};
|
||||
|
||||
hmWiredNames =
|
||||
selectedUsers:
|
||||
lib.attrNames (
|
||||
lib.filterAttrs (
|
||||
_: user:
|
||||
let
|
||||
hm = userHm user;
|
||||
in
|
||||
(hm.enable or false) && (hm.module or null) != null
|
||||
) selectedUsers
|
||||
);
|
||||
|
||||
rbwOutboundSnippet =
|
||||
name: user:
|
||||
let
|
||||
outboundCfg = ((userSsh user).outbound or { }).rbw or { };
|
||||
in
|
||||
lib.mkIf (outboundCfg.enable or false) {
|
||||
chiasson.ssh.outbound.rbw.enable = true;
|
||||
chiasson.ssh.outbound.rbw.user = name;
|
||||
chiasson.ssh.outbound.rbw.hosts =
|
||||
if (outboundCfg.hosts or "all") == "all" then [ "all" ] else outboundCfg.hosts;
|
||||
chiasson.ssh.outbound.rbw.extraIdentities = outboundCfg.extraIdentities or [ ];
|
||||
};
|
||||
|
||||
mkHmUserModule =
|
||||
{ name, user, hostExtraModules }:
|
||||
let
|
||||
hm = userHm user;
|
||||
hmModule = resolveHomeManagerModule hm.module;
|
||||
in
|
||||
lib.mkMerge (
|
||||
[
|
||||
hmModule
|
||||
(rbwOutboundSnippet name user)
|
||||
]
|
||||
++ (hm.extraModules or [ ])
|
||||
++ hostExtraModules
|
||||
);
|
||||
|
||||
inboundAuthorizedOrNull =
|
||||
user:
|
||||
let
|
||||
inboundCfg = (userSsh user).inbound or { };
|
||||
in
|
||||
if !(inboundCfg.enable or false) then
|
||||
null
|
||||
else if (inboundCfg.authorizedHosts or "all") == "all" then
|
||||
"all"
|
||||
else
|
||||
inboundCfg.authorizedHosts;
|
||||
|
||||
inboundHostsAttr =
|
||||
selectedUsers:
|
||||
lib.pipe selectedUsers [
|
||||
(lib.mapAttrs (_: inboundAuthorizedOrNull))
|
||||
(lib.filterAttrs (_: v: v != null))
|
||||
];
|
||||
};
|
||||
inventory = self.lib.sshInventory;
|
||||
|
||||
olivierEnabled = lib.elem "olivier" cfg.enabled;
|
||||
|
||||
# Merge catalog + per-host overrides into the final user attrs.
|
||||
selectedUsers = lib.listToAttrs (
|
||||
map (name: {
|
||||
inherit name;
|
||||
value = lib.recursiveUpdate cfg.catalog.${name} (cfg.hostOverrides.${name} or { });
|
||||
}) cfg.enabled
|
||||
);
|
||||
|
||||
names = usersLib.hmWiredNames selectedUsers;
|
||||
missing = usersLib.missingEnabledNames cfg.catalog cfg.enabled;
|
||||
stray = usersLib.strayHomeUserKeys cfg.extraModules cfg.enabled;
|
||||
hmAvailable = lib.hasAttrByPath [ "home-manager" "users" ] options;
|
||||
inboundUsersAttr = usersLib.inboundHostsAttr selectedUsers;
|
||||
|
||||
hmUsersAttr = lib.listToAttrs (
|
||||
map (name: {
|
||||
inherit name;
|
||||
value = usersLib.mkHmUserModule {
|
||||
inherit name;
|
||||
user = selectedUsers.${name};
|
||||
hostExtraModules = cfg.extraModules.${name} or [ ];
|
||||
};
|
||||
}) names
|
||||
);
|
||||
|
||||
# Fish shell wiring: HM's fish module declares its package, but
|
||||
# /etc/passwd + /etc/shells need updating so login shells work.
|
||||
# Only fires for HM users that actually enable fish.
|
||||
hmFishUsers =
|
||||
if !hmAvailable then
|
||||
{ }
|
||||
else
|
||||
lib.filterAttrs (
|
||||
name: hmUser: (hmUser.programs.fish.enable or false) && builtins.elem name names
|
||||
) config.home-manager.users;
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
self.nixosModules.sshInbound
|
||||
{ _module.args = { inherit self usersLib; }; }
|
||||
];
|
||||
|
||||
####################
|
||||
# Options
|
||||
####################
|
||||
options.chiasson.users = {
|
||||
catalog = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = { };
|
||||
description = "User records; defaults below. Override with `hostOverrides` or `mkForce`.";
|
||||
};
|
||||
enabled = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
description = "Catalog names to materialize as `users.users` on this machine.";
|
||||
};
|
||||
hostOverrides = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = { };
|
||||
description = "`recursiveUpdate`'d onto catalog users on this host.";
|
||||
};
|
||||
# Standard `attrsOf (listOf …)` already concatenates lists across
|
||||
# module definitions; no custom merge function needed.
|
||||
extraModules = lib.mkOption {
|
||||
type = lib.types.attrsOf (lib.types.listOf lib.types.unspecified);
|
||||
default = { };
|
||||
description = ''
|
||||
Per-user Home Manager extraModules keyed by catalog name. Lists
|
||||
from multiple modules (desktopHomeBase + this host's home.nix)
|
||||
are concatenated.
|
||||
'';
|
||||
};
|
||||
homeManager.autoWire = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = "Auto-create Home Manager users from the catalog.";
|
||||
};
|
||||
};
|
||||
|
||||
####################
|
||||
# Catalog defaults + integration
|
||||
####################
|
||||
# Nix won't accept both `config.X = …` and `config = …` in the same
|
||||
# attrset (the path-attr partially defines `config`, then the explicit
|
||||
# `config = …` is a redefinition error). All `config.` writes must
|
||||
# therefore live INSIDE the single mkMerge block below.
|
||||
config = lib.mkMerge [
|
||||
####################
|
||||
# Catalog defaults
|
||||
####################
|
||||
{
|
||||
chiasson.users.catalog = {
|
||||
olivier = {
|
||||
isNormalUser = true;
|
||||
description = "Olivier";
|
||||
extraGroups = [
|
||||
"networkmanager"
|
||||
"wheel"
|
||||
"docker"
|
||||
"fuse"
|
||||
"uinput"
|
||||
"kvm"
|
||||
# `video` lets brightnessctl/light udev rules own /sys/backlight without sudo.
|
||||
# Harmless on headless hosts (no devices).
|
||||
"video"
|
||||
# DRI render + input for gamescope / Steam on Wayland, no sudo.
|
||||
"render"
|
||||
"input"
|
||||
];
|
||||
hashedPasswordFile = lib.mkIf olivierEnabled (
|
||||
config.sops.secrets."users/olivier/hashedPassword".path
|
||||
);
|
||||
homeManager = {
|
||||
enable = true;
|
||||
module = { pkgs, ... }: {
|
||||
home.username = "olivier";
|
||||
home.homeDirectory = "/home/olivier";
|
||||
home.stateVersion = "25.11";
|
||||
programs.home-manager.enable = true;
|
||||
|
||||
# Declarative rbw config — replaces per-host
|
||||
# `rbw config set email|base_url`. Neither value is a
|
||||
# secret (URL + login email), so plaintext is fine, same
|
||||
# as the public keys committed in ssh-inventory.nix.
|
||||
programs.rbw = {
|
||||
enable = true;
|
||||
settings = {
|
||||
email = "olivierchiasson@hotmail.fr";
|
||||
base_url = "https://bitwarden.chiasson.cloud";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
ssh.inbound.enable = true;
|
||||
ssh.inbound.authorizedHosts = "all";
|
||||
ssh.outbound.rbw.enable = true;
|
||||
ssh.outbound.rbw.hosts = [ "all" ];
|
||||
# olivier's laptop: the HM SSH module writes the matching
|
||||
# `.pub` filter for each of these and emits a `Match user`
|
||||
# block, so `ssh server@r5500` / `ssh builder@nix-server`
|
||||
# route the right private key from the rbw agent.
|
||||
ssh.outbound.rbw.extraIdentities = [
|
||||
"server"
|
||||
"builder"
|
||||
];
|
||||
};
|
||||
|
||||
server = {
|
||||
isNormalUser = true;
|
||||
description = "Server user";
|
||||
extraGroups = [ "wheel" ];
|
||||
homeManager = {
|
||||
enable = false;
|
||||
module = null;
|
||||
};
|
||||
ssh.inbound.enable = true;
|
||||
ssh.inbound.authorizedHosts = "all";
|
||||
ssh.outbound.rbw.enable = false;
|
||||
ssh.outbound.rbw.hosts = [ "all" ];
|
||||
};
|
||||
|
||||
builder = {
|
||||
isNormalUser = true;
|
||||
description = "Navi fleet deploy (push + activate only)";
|
||||
extraGroups = [ ];
|
||||
createHome = false;
|
||||
homeManager = {
|
||||
enable = false;
|
||||
module = null;
|
||||
};
|
||||
ssh.inbound.enable = true;
|
||||
ssh.inbound.authorizedHosts = "all";
|
||||
};
|
||||
};
|
||||
|
||||
# Pull in olivier's hashed password from sops; `neededForUsers`
|
||||
# makes nixos decrypt it at boot so the user record can read it.
|
||||
sops.secrets."users/olivier/hashedPassword" = lib.mkIf olivierEnabled {
|
||||
neededForUsers = true;
|
||||
};
|
||||
}
|
||||
|
||||
####################
|
||||
# Integration
|
||||
####################
|
||||
{
|
||||
assertions = [
|
||||
{
|
||||
assertion = missing == [ ];
|
||||
message = "chiasson.users.enabled references unknown catalog names: ${lib.concatStringsSep ", " missing}";
|
||||
}
|
||||
{
|
||||
assertion = stray == [ ];
|
||||
message = "chiasson.users.extraModules has keys not in chiasson.users.enabled: ${lib.concatStringsSep ", " stray}";
|
||||
}
|
||||
];
|
||||
|
||||
# NixOS user accounts (no HM, raw).
|
||||
users.users = lib.mapAttrs (name: user: usersLib.mkNixosUser name user) selectedUsers;
|
||||
}
|
||||
(lib.optionalAttrs hmAvailable {
|
||||
"home-manager".useGlobalPkgs = lib.mkIf (cfg.homeManager.autoWire && names != [ ]) true;
|
||||
"home-manager".sharedModules = lib.mkIf (cfg.homeManager.autoWire && names != [ ]) [
|
||||
self.homeManagerModules.sshOutboundRbw
|
||||
];
|
||||
"home-manager".users = lib.mkIf (cfg.homeManager.autoWire && names != [ ]) hmUsersAttr;
|
||||
})
|
||||
# Wire user-specific ACLs into the inbound module — fail the build
|
||||
# if a user has no matching pubkey.
|
||||
(lib.mkIf (inboundUsersAttr != { }) {
|
||||
chiasson.ssh.inbound.enable = true;
|
||||
chiasson.ssh.inbound.userAuthorizedHosts = inboundUsersAttr;
|
||||
})
|
||||
# Fish-shell wiring: HM knows the package; we sync /etc/passwd +
|
||||
# /etc/shells with `mkForce`. `mkNixosUser` doesn't emit `shell`, so
|
||||
# the force is uncontested.
|
||||
(lib.mkIf (hmFishUsers != { }) {
|
||||
environment.shells = lib.mkAfter (
|
||||
lib.mapAttrsToList (_: hmUser: lib.getExe hmUser.programs.fish.package) hmFishUsers
|
||||
);
|
||||
users.users = lib.mapAttrs (name: hmUser: {
|
||||
shell = lib.mkForce (lib.getExe hmUser.programs.fish.package);
|
||||
}) hmFishUsers;
|
||||
})
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
# Shared user definitions for all hosts that import `nixosModules.users`.
|
||||
# Module (not bare attrset) so catalog entries can use `config.*` for sops paths etc.
|
||||
{ ... }: {
|
||||
flake.nixosModules.usersCatalogDefaults =
|
||||
{ config, ... }:
|
||||
{
|
||||
config.chiasson.users.catalog = {
|
||||
olivier = {
|
||||
isNormalUser = true;
|
||||
description = "Olivier";
|
||||
extraGroups = [
|
||||
"networkmanager"
|
||||
"wheel"
|
||||
"docker"
|
||||
"fuse"
|
||||
"uinput"
|
||||
"kvm"
|
||||
# `video` is required for the brightnessctl/light udev rules to grant write access
|
||||
# to /sys/class/backlight/*/brightness without sudo. Harmless on hosts without a
|
||||
# backlight (servers, desktop towers): the group simply has no devices to own.
|
||||
"video"
|
||||
# DRI render nodes and input devices for gamescope / Steam on Wayland (no sudo).
|
||||
"render"
|
||||
"input"
|
||||
];
|
||||
|
||||
# Host must set `sops.secrets."users/olivier/hashedPassword".neededForUsers = true`.
|
||||
# With that, `.path` is under /run/secrets-for-users/… (sops-nix README).
|
||||
hashedPasswordFile = config.sops.secrets."users/olivier/hashedPassword".path;
|
||||
|
||||
homeManager = {
|
||||
enable = true;
|
||||
module =
|
||||
{ ... }:
|
||||
{
|
||||
home.username = "olivier";
|
||||
home.homeDirectory = "/home/olivier";
|
||||
home.stateVersion = "25.11";
|
||||
programs.home-manager.enable = true;
|
||||
};
|
||||
};
|
||||
|
||||
ssh = {
|
||||
inbound = {
|
||||
enable = true;
|
||||
authorizedHosts = "all";
|
||||
};
|
||||
outbound = {
|
||||
rbw = {
|
||||
enable = true;
|
||||
hosts = "all";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
server = {
|
||||
isNormalUser = true;
|
||||
description = "Server user";
|
||||
extraGroups = [ "wheel" ];
|
||||
|
||||
homeManager = {
|
||||
enable = false;
|
||||
module = null;
|
||||
};
|
||||
|
||||
ssh = {
|
||||
inbound = {
|
||||
enable = true;
|
||||
authorizedHosts = "all";
|
||||
};
|
||||
outbound = {
|
||||
rbw = {
|
||||
enable = false;
|
||||
hosts = "all";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
builder = {
|
||||
isNormalUser = true;
|
||||
description = "Navi fleet deploy (push + activate only)";
|
||||
extraGroups = [ ];
|
||||
createHome = false;
|
||||
|
||||
homeManager = {
|
||||
enable = false;
|
||||
module = null;
|
||||
};
|
||||
|
||||
ssh.inbound.enable = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
{ ... }: {
|
||||
flake.nixosModules.usersCatalogOptions =
|
||||
{ lib, ... }:
|
||||
{
|
||||
options.chiasson.users = {
|
||||
catalog = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = { };
|
||||
description = ''
|
||||
User records merged from `usersCatalogDefaults`; override with `hostOverrides` or `mkForce`.
|
||||
'';
|
||||
};
|
||||
enabled = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
description = "Catalog names to materialize as `users.users` on this machine.";
|
||||
};
|
||||
hostOverrides = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = { };
|
||||
description = ''
|
||||
`recursiveUpdate`d onto catalog users.
|
||||
'';
|
||||
};
|
||||
extraModules = lib.mkOption {
|
||||
type = lib.types.attrsOf (lib.types.listOf lib.types.unspecified);
|
||||
default = { };
|
||||
description = ''
|
||||
Per-user Home Manager `extraModules` keyed by catalog user name.
|
||||
Keys must match `chiasson.users.enabled`.
|
||||
'';
|
||||
};
|
||||
homeManager = {
|
||||
autoWire = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = "Create HM users from the catalog when true.";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
# Catalog → NixOS `users.users` + Home Manager + SSH inbound.
|
||||
{ self, ... }: {
|
||||
flake.nixosModules.users =
|
||||
{ config, lib, ... }:
|
||||
let
|
||||
usersLib = self.lib.usersMerge lib;
|
||||
selectUsers =
|
||||
c:
|
||||
let
|
||||
uc = c.chiasson.users;
|
||||
in
|
||||
usersLib.selectedUsersAttr {
|
||||
catalog = uc.catalog;
|
||||
enabled = uc.enabled;
|
||||
hostOverrides = uc.hostOverrides;
|
||||
};
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
self.nixosModules.sshInbound
|
||||
self.nixosModules.usersCatalogOptions
|
||||
self.nixosModules.usersCatalogDefaults
|
||||
{ _module.args = { inherit self usersLib selectUsers; }; }
|
||||
self.nixosModules.usersHomeIntegration
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
{ ... }: {
|
||||
flake.nixosModules.usersHomeIntegration =
|
||||
{ config, options, lib, self, usersLib, selectUsers, ... }:
|
||||
let
|
||||
cfg = config.chiasson.users;
|
||||
selected = selectUsers config;
|
||||
missing = usersLib.missingEnabledNames cfg.catalog cfg.enabled;
|
||||
stray = usersLib.strayHomeUserKeys cfg.extraModules cfg.enabled;
|
||||
names = usersLib.hmWiredNames selected;
|
||||
hmAvailable = lib.hasAttrByPath [ "home-manager" "users" ] options;
|
||||
hmUsersAttr = lib.listToAttrs (
|
||||
map (name: {
|
||||
inherit name;
|
||||
value = usersLib.mkHmUserModule {
|
||||
inherit name;
|
||||
user = selected.${name};
|
||||
hostExtraModules = cfg.extraModules.${name} or [ ];
|
||||
};
|
||||
}) names
|
||||
);
|
||||
inboundUsersAttr = usersLib.inboundHostsAttr selected;
|
||||
|
||||
# HM configures fish in ~/.config/fish but no longer sets /etc/passwd or /etc/shells.
|
||||
hmFishUsers =
|
||||
if !hmAvailable then { }
|
||||
else
|
||||
lib.filterAttrs (
|
||||
name: hmUser: (hmUser.programs.fish.enable or false) && builtins.elem name names
|
||||
) config.home-manager.users;
|
||||
in
|
||||
{
|
||||
config = lib.mkMerge [
|
||||
{
|
||||
assertions = [
|
||||
{
|
||||
assertion = missing == [ ];
|
||||
message = "chiasson.users.enabled references unknown catalog users: ${builtins.concatStringsSep ", " missing}";
|
||||
}
|
||||
{
|
||||
assertion = stray == [ ];
|
||||
message = "chiasson.users.extraModules has keys not in chiasson.users.enabled: ${builtins.concatStringsSep ", " stray}";
|
||||
}
|
||||
];
|
||||
}
|
||||
{
|
||||
users.users = lib.mapAttrs (name: user: usersLib.mkNixosUser name user) selected;
|
||||
}
|
||||
(lib.optionalAttrs hmAvailable {
|
||||
"home-manager".useGlobalPkgs = lib.mkIf (cfg.homeManager.autoWire && names != [ ]) true;
|
||||
"home-manager".sharedModules = lib.mkIf (cfg.homeManager.autoWire && names != [ ]) [ self.homeManagerModules.sshOutboundRbw ];
|
||||
"home-manager".users = lib.mkIf (cfg.homeManager.autoWire && names != [ ]) hmUsersAttr;
|
||||
})
|
||||
(lib.mkIf (inboundUsersAttr != { }) {
|
||||
chiasson.ssh.inbound.enable = true;
|
||||
chiasson.ssh.inbound.userAuthorizedHosts = inboundUsersAttr;
|
||||
})
|
||||
(lib.mkIf (hmFishUsers != { }) {
|
||||
environment.shells = lib.mkAfter (
|
||||
lib.mapAttrsToList (
|
||||
_: hmUser: lib.getExe hmUser.programs.fish.package
|
||||
) hmFishUsers
|
||||
);
|
||||
users.users = lib.mapAttrs (
|
||||
name: hmUser: {
|
||||
shell = lib.mkForce (lib.getExe hmUser.programs.fish.package);
|
||||
}
|
||||
) hmFishUsers;
|
||||
})
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
#
|
||||
# git diff master..ie/telequebec-update -- \
|
||||
# yt_dlp/extractor/telequebec.py yt_dlp/extractor/_extractors.py \
|
||||
# > path/to/NixOS-V2/modules/patches/yt-dlp-telequebec.patch
|
||||
# > path/to/chiasson-nix/modules/patches/yt-dlp-telequebec.patch
|
||||
#
|
||||
{ ... }: {
|
||||
flake.nixosModules.systemYtDlpTelequebecPatch =
|
||||
|
||||
@@ -12,7 +12,9 @@
|
||||
|
||||
options.chiasson.home.apps.discord.enable = lib.mkEnableOption ''
|
||||
Vesktop (nixcord). Needs nixpkgs `discord` — breaks on e.g. aarch64-linux.
|
||||
'';
|
||||
'' // {
|
||||
default = false;
|
||||
};
|
||||
|
||||
config = lib.mkIf root.enable (lib.mkMerge [
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{ ... }: {
|
||||
{ self, ... }: {
|
||||
flake.nixosModules.systemLocalsend =
|
||||
{ config, lib, ... }:
|
||||
let
|
||||
@@ -19,23 +19,10 @@
|
||||
};
|
||||
};
|
||||
|
||||
flake.homeManagerModules.wisdomAppsLocalsend =
|
||||
{ config, lib, pkgs, ... }:
|
||||
let
|
||||
root = config.chiasson.home;
|
||||
cfg = config.chiasson.home.apps.localsend;
|
||||
in
|
||||
{
|
||||
options.chiasson.home.apps.localsend = {
|
||||
enable = lib.mkEnableOption ''
|
||||
LocalSend client; open the firewall on NixOS with `system.localsend` if you want inbound.
|
||||
'';
|
||||
|
||||
package = lib.mkPackageOption pkgs "localsend" { };
|
||||
};
|
||||
|
||||
config = lib.mkIf (root.enable && cfg.enable) {
|
||||
home.packages = [ cfg.package ];
|
||||
};
|
||||
flake.homeManagerModules.wisdomAppsLocalsend = self.lib.wisdomSlice {
|
||||
path = "apps.localsend";
|
||||
default = true;
|
||||
description = "LocalSend client; open the firewall on NixOS with `system.localsend` if you want inbound.";
|
||||
packages = pkgs: [ pkgs.localsend ];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
{ ... }: {
|
||||
flake.homeManagerModules.wisdomAppsPokeclicker =
|
||||
{ config, lib, pkgs, ... }:
|
||||
let
|
||||
root = config.chiasson.home;
|
||||
cfg = config.chiasson.home.apps.pokeclicker;
|
||||
pokeclickerPkg = pkgs.callPackage ./package { };
|
||||
in
|
||||
{
|
||||
options.chiasson.home.apps.pokeclicker.enable = lib.mkEnableOption "PokéClicker desktop (Farigh fork .deb → nix).";
|
||||
|
||||
config = lib.mkIf (root.enable && cfg.enable) {
|
||||
home.packages = [ pokeclickerPkg ];
|
||||
};
|
||||
{ self, ... }: {
|
||||
flake.homeManagerModules.wisdomAppsPokeclicker = self.lib.wisdomSlice {
|
||||
path = "apps.pokeclicker";
|
||||
default = false;
|
||||
description = "PokéClicker desktop (Farigh fork .deb → nix).";
|
||||
packages = pkgs: [ (pkgs.callPackage ./package { }) ];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,19 +1,8 @@
|
||||
{ ... }: {
|
||||
flake.homeManagerModules.wisdomBrowsersChrome =
|
||||
{ config, lib, pkgs, ... }:
|
||||
let
|
||||
root = config.chiasson.home;
|
||||
cfg = config.chiasson.home.browsers.chrome;
|
||||
in
|
||||
{
|
||||
options.chiasson.home.browsers.chrome.enable = lib.mkEnableOption ''
|
||||
Chrome (unfree, needs `allowUnfree`); skipped if nixpkgs has no build for this platform.
|
||||
'';
|
||||
|
||||
config = lib.mkIf (root.enable && cfg.enable) {
|
||||
home.packages = lib.optional (
|
||||
lib.meta.availableOn pkgs.stdenv.hostPlatform pkgs.google-chrome
|
||||
) pkgs.google-chrome;
|
||||
};
|
||||
{ self, ... }: {
|
||||
flake.homeManagerModules.wisdomBrowsersChrome = self.lib.wisdomSlice {
|
||||
path = "browsers.chrome";
|
||||
default = false;
|
||||
description = "Chrome (unfree, needs `allowUnfree`); skipped if nixpkgs has no build for this platform.";
|
||||
packages = pkgs: [ pkgs.google-chrome ];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,25 +5,43 @@
|
||||
cfg = config.chiasson.system.chromiumHevc;
|
||||
in
|
||||
{
|
||||
options.chiasson.system.chromiumHevc.enable = lib.mkEnableOption ''
|
||||
options.chiasson.system.chromiumHevc = {
|
||||
enable = lib.mkEnableOption ''
|
||||
VA-API packages for Chromium HEVC (Intel iHD + optional NVIDIA nvidia-vaapi-driver).
|
||||
Pair with `wisdomBrowsersChromiumHevc` on the user side.
|
||||
'';
|
||||
|
||||
gpu = lib.mkOption {
|
||||
type = lib.types.enum [
|
||||
"intel"
|
||||
"nvidia"
|
||||
];
|
||||
default = "intel";
|
||||
description = ''
|
||||
VA-API stack for the `*-hevc` browser launchers. Use **intel** for Jellyfin
|
||||
(Chromium + nvidia-vaapi-driver is unsupported upstream and fails with
|
||||
`failed Initialize()ing the frame pool` in Jellyfin). **nvidia** adds the
|
||||
nvidia-vaapi-driver and keep renderD129 + VaapiOnNvidiaGPUs for experiments only.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
hardware.graphics.enable = lib.mkDefault true;
|
||||
hardware.graphics.extraPackages = lib.mkAfter (
|
||||
with pkgs;
|
||||
[ nvidia-vaapi-driver ]
|
||||
lib.optionals (cfg.gpu == "nvidia") [
|
||||
pkgs.nvidia-vaapi-driver
|
||||
]
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
flake.homeManagerModules.wisdomBrowsersChromiumHevc =
|
||||
{ config, lib, pkgs, ... }:
|
||||
{ config, lib, pkgs, osConfig ? { }, ... }:
|
||||
let
|
||||
root = config.chiasson.home;
|
||||
cfg = config.chiasson.home.browsers.chromiumHevc;
|
||||
gpu = lib.attrByPath [ "chiasson" "system" "chromiumHevc" "gpu" ] "intel" osConfig;
|
||||
|
||||
browserCatalog = {
|
||||
"google-chrome" = {
|
||||
@@ -53,7 +71,6 @@
|
||||
intel = {
|
||||
driver = "iHD";
|
||||
drmDevice = "/dev/dri/renderD128";
|
||||
nvdBackend = "direct";
|
||||
enableFeatures = [
|
||||
"VaapiVideoDecodeLinuxGL"
|
||||
"VaapiVideoDecoder"
|
||||
@@ -69,7 +86,6 @@
|
||||
nvidia = {
|
||||
driver = "nvidia";
|
||||
drmDevice = "/dev/dri/renderD129";
|
||||
nvdBackend = "direct";
|
||||
enableFeatures = [
|
||||
"VaapiVideoDecoder"
|
||||
"VaapiIgnoreDriverChecks"
|
||||
@@ -85,21 +101,17 @@
|
||||
};
|
||||
};
|
||||
|
||||
activeGpu = gpuProfiles.${cfg.vaapi.gpu};
|
||||
|
||||
mkChromiumHevc =
|
||||
packageName:
|
||||
let
|
||||
spec = browserCatalog.${packageName};
|
||||
browser = spec.package;
|
||||
launcherName = spec.launcher;
|
||||
activeGpu = gpuProfiles.${gpu};
|
||||
enableFeatures = lib.concatStringsSep "," activeGpu.enableFeatures;
|
||||
disableFeatures = lib.concatStringsSep "," activeGpu.disableFeatures;
|
||||
desktopItem = pkgs.makeDesktopItem {
|
||||
name = launcherName;
|
||||
|
||||
mkDesktopItem =
|
||||
spec:
|
||||
pkgs.makeDesktopItem {
|
||||
name = spec.launcher;
|
||||
desktopName = spec.desktopName;
|
||||
genericName = "Web Browser";
|
||||
exec = "${launcherName} %U";
|
||||
exec = "${spec.launcher} %U";
|
||||
icon = spec.icon;
|
||||
categories = [
|
||||
"Network"
|
||||
@@ -113,6 +125,14 @@
|
||||
"x-scheme-handler/https"
|
||||
];
|
||||
};
|
||||
|
||||
mkChromiumHevc =
|
||||
packageName:
|
||||
let
|
||||
spec = browserCatalog.${packageName};
|
||||
browser = spec.package;
|
||||
launcherName = spec.launcher;
|
||||
desktopItem = mkDesktopItem spec;
|
||||
in
|
||||
pkgs.runCommand launcherName
|
||||
{
|
||||
@@ -126,7 +146,7 @@
|
||||
makeWrapper ${browser}/bin/${spec.binary} $out/bin/${launcherName} \
|
||||
--set LIBVA_DRIVER_NAME ${lib.escapeShellArg activeGpu.driver} \
|
||||
--set LIBVA_DRM_DEVICE ${lib.escapeShellArg activeGpu.drmDevice} \
|
||||
--set NVD_BACKEND ${lib.escapeShellArg activeGpu.nvdBackend} \
|
||||
--set NVD_BACKEND direct \
|
||||
--add-flags "--enable-features=${enableFeatures}" \
|
||||
--add-flags "--disable-features=${disableFeatures}" \
|
||||
${lib.concatMapStringsSep " " (a: "--add-flags ${lib.escapeShellArg a}") cfg.extraCommandLineArgs}
|
||||
@@ -136,46 +156,28 @@
|
||||
'';
|
||||
|
||||
selectedPackages = lib.filter (
|
||||
name:
|
||||
let
|
||||
spec = browserCatalog.${name};
|
||||
in
|
||||
lib.meta.availableOn pkgs.stdenv.hostPlatform spec.package
|
||||
name: lib.meta.availableOn pkgs.stdenv.hostPlatform browserCatalog.${name}.package
|
||||
) cfg.packages;
|
||||
|
||||
wrappers = map mkChromiumHevc selectedPackages;
|
||||
in
|
||||
{
|
||||
options.chiasson.home.browsers.chromiumHevc = {
|
||||
enable = lib.mkEnableOption ''
|
||||
enable =
|
||||
lib.mkEnableOption ''
|
||||
`google-chrome-hevc`: Chromium with VA-API HEVC for Jellyfin / MSE playback.
|
||||
|
||||
Default GPU is **Intel** (`vaapi.gpu = "intel"`): Chromium + NVIDIA VA-API is
|
||||
unsupported upstream (`nvidia-vaapi-driver` README) and fails with
|
||||
`failed Initialize()ing the frame pool` in Jellyfin.
|
||||
|
||||
GPU stack comes from `chiasson.system.chromiumHevc.gpu` (default **intel**).
|
||||
Requires `chiasson.system.chromiumHevc.enable` on NixOS.
|
||||
'';
|
||||
|
||||
packages = lib.mkOption {
|
||||
type = lib.types.listOf (
|
||||
lib.types.enum (lib.attrNames browserCatalog)
|
||||
);
|
||||
default = [ "google-chrome" ];
|
||||
description = "Chromium-based browsers to wrap.";
|
||||
''
|
||||
// {
|
||||
default = true;
|
||||
};
|
||||
|
||||
vaapi.gpu = lib.mkOption {
|
||||
type = lib.types.enum [
|
||||
"intel"
|
||||
"nvidia"
|
||||
];
|
||||
default = "intel";
|
||||
description = ''
|
||||
VA-API stack for `google-chrome-hevc`. Use **intel** for Jellyfin (Chromium +
|
||||
nvidia-vaapi-driver is unsupported and hits frame-pool init errors). **nvidia**
|
||||
keeps renderD129 + VaapiOnNvidiaGPUs for experiments only.
|
||||
'';
|
||||
packages = lib.mkOption {
|
||||
type = lib.types.listOf (lib.types.enum (lib.attrNames browserCatalog));
|
||||
default = if pkgs.stdenv.hostPlatform.isAarch64 then [ "chromium" ] else [ "google-chrome" ];
|
||||
description = "Chromium-based browsers to wrap.";
|
||||
};
|
||||
|
||||
extraCommandLineArgs = lib.mkOption {
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
{ ... }: {
|
||||
flake.homeManagerModules.wisdomBrowsersEdge =
|
||||
{ config, lib, pkgs, ... }:
|
||||
let
|
||||
root = config.chiasson.home;
|
||||
cfg = config.chiasson.home.browsers.edge;
|
||||
in
|
||||
{
|
||||
options.chiasson.home.browsers.edge.enable = lib.mkEnableOption "Edge (unfree); skipped if unavailable on this platform.";
|
||||
|
||||
config = lib.mkIf (root.enable && cfg.enable) {
|
||||
home.packages = lib.optional (
|
||||
lib.meta.availableOn pkgs.stdenv.hostPlatform pkgs.microsoft-edge
|
||||
) pkgs.microsoft-edge;
|
||||
};
|
||||
{ self, ... }: {
|
||||
flake.homeManagerModules.wisdomBrowsersEdge = self.lib.wisdomSlice {
|
||||
path = "browsers.edge";
|
||||
default = false;
|
||||
description = "Edge (unfree); skipped if unavailable on this platform.";
|
||||
packages = pkgs: [ pkgs.microsoft-edge ];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
{ ... }: {
|
||||
flake.homeManagerModules.wisdomBrowsersFlow =
|
||||
{ config, lib, pkgs, ... }:
|
||||
let
|
||||
root = config.chiasson.home;
|
||||
cfg = config.chiasson.home.browsers.flow;
|
||||
|
||||
flow-browser =
|
||||
{ self, ... }: {
|
||||
flake.homeManagerModules.wisdomBrowsersFlow = self.lib.wisdomSlice {
|
||||
path = "browsers.flow";
|
||||
default = false;
|
||||
description = "[Flow](https://github.com/MultiboxLabs/flow-browser) — upstream AppImage wrapped for NixOS.";
|
||||
packages =
|
||||
pkgs:
|
||||
let
|
||||
pname = "flow-browser";
|
||||
version = "0.11.0";
|
||||
@@ -25,7 +24,8 @@
|
||||
|
||||
appimageContents = pkgs.appimageTools.extractType2 { inherit pname version src; };
|
||||
in
|
||||
pkgs.appimageTools.wrapType2 {
|
||||
[
|
||||
(pkgs.appimageTools.wrapType2 {
|
||||
inherit pname version src;
|
||||
|
||||
nativeBuildInputs = [ pkgs.makeWrapper ];
|
||||
@@ -45,23 +45,13 @@
|
||||
meta = {
|
||||
description = "Chromium-based browser (upstream AppImage)";
|
||||
homepage = "https://github.com/MultiboxLabs/flow-browser";
|
||||
license = lib.licenses.gpl3Plus;
|
||||
sourceProvenance = with lib.sourceTypes; [ binaryNativeCode ];
|
||||
license = pkgs.lib.licenses.gpl3Plus;
|
||||
sourceProvenance = with pkgs.lib.sourceTypes; [ binaryNativeCode ];
|
||||
platforms = [ "x86_64-linux" "aarch64-linux" ];
|
||||
mainProgram = pname;
|
||||
maintainers = [ ];
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
options.chiasson.home.browsers.flow.enable = lib.mkEnableOption ''
|
||||
[Flow](https://github.com/MultiboxLabs/flow-browser) — upstream AppImage wrapped for NixOS.
|
||||
'';
|
||||
|
||||
config = lib.mkIf (root.enable && cfg.enable) {
|
||||
home.packages = lib.optional (
|
||||
lib.meta.availableOn pkgs.stdenv.hostPlatform flow-browser
|
||||
) flow-browser;
|
||||
};
|
||||
})
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -31,7 +31,9 @@
|
||||
{
|
||||
options.chiasson.home.browsers.orion.enable = lib.mkEnableOption ''
|
||||
[Orion](https://orionbrowser.com/) (Kagi) — installs the upstream Flatpak bundle and provides `oriongtk`.
|
||||
'';
|
||||
'' // {
|
||||
default = false;
|
||||
};
|
||||
|
||||
config = lib.mkIf (root.enable && cfg.enable) {
|
||||
home.packages = [ oriongtk ];
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
{
|
||||
imports = [ inputs.zen-browser.homeModules.beta ];
|
||||
|
||||
options.chiasson.home.browsers.zen.enable = lib.mkEnableOption "Zen Browser + locked-down policies / extensions.";
|
||||
options.chiasson.home.browsers.zen.enable = lib.mkEnableOption "Zen Browser + locked-down policies / extensions." // {
|
||||
default = false;
|
||||
};
|
||||
|
||||
config = lib.mkIf (root.enable && cfg.enable) {
|
||||
programs.zen-browser = {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user