Compare commits

...

54 Commits

Author SHA1 Message Date
Olivier 6b8fa60540 docs: update README with feature overview and module structure 2026-07-22 18:02:21 -03:00
Olivier 86dc80eb3c feat(wisdom): enhance vscode module with java support and writable settings
Add Java extension support and configure multiple JDK runtimes.
Introduce a wrapped VS Code package that includes JDK 25 in the PATH
to support the Red Hat Java language server.

Switch from using `programs.vscode.userSettings` to a manually managed
`settings.json` file via `home.file`. This allows the settings file to
be writable by external tools like `kilo-code` at runtime while
preserving them across Nix configuration switches by using `force = false`.
2026-07-19 18:13:30 -03:00
Olivier f1bd5146a3 refactor(hosts): simplify streaming display configuration
Refactor the streaming display logic for the 14900k host to use the
`streamingDisplay` configuration object directly instead of separate
variables. This simplifies the monitor layout logic for both Niri and
Hyprland.

Additionally, remove the fragile `sunshineOutputIndex` option in favor
of using the stable connector name for Sunshine's `output_name`. This
prevents issues where numeric monitor indices drift after hotplugging.
The change also includes an explicit encoder list for Sunshine to avoid
probing for AV1 on hardware that does not support it.
2026-07-18 09:35:52 -03:00
Olivier 6f35d60aab refactor(system): simplify steamTinkerLaunch logic and package assignment
Refactor the steamExtraPkgs conditional logic into a single line and
introduce a launcherSet variable to reduce redundancy when assigning
packages to the system or specific users.
2026-07-17 17:37:38 -03:00
Olivier 3d2e74d115 feat(wisdom): add vscode editor module
Add a new module for Visual Studio Code configuration including
default extensions (nix-ide, markdown-all-in-one, prettier) and
nixd/nixfmt settings. Enable vscode on 14900k, t2mbp, and uConsole
hosts with additional extensions.

Also clean up unfree predicate logic in ideapad configuration.
2026-07-17 17:17:14 -03:00
Olivier 548a438f6a refactor(wisdom): rename wisdomSimpleSlice to wisdomSlice
Rename the helper function `wisdomSimpleSlice` to `wisdomSlice` across
the codebase and update documentation to reflect the new naming
convention. This change simplifies the API for creating trivial
single-package slices.
2026-07-17 11:44:56 -03:00
Olivier 0939766e3a refactor(wisdom): migrate app and browser modules to wisdomSimpleSlice
Replace manual Home Manager module definitions in `localsend`,
`pokeclicker`, and `flow-browser` with the `wisdomSimpleSlice`
helper. This reduces boilerplate by centralizing the option
declaration and configuration logic.
2026-07-17 11:31:35 -03:00
Olivier f424ab98c7 refactor(hosts): remove redundant host and system arguments
Remove explicit `host` and `system` definitions from `specialArgs` in
various host `default.nix` files. These values are redundant as they
can be derived from the flake context or are already provided by the
calling environment.
2026-07-17 11:30:34 -03:00
Olivier 15a4d8d5e8 feat(nfs): centralize nixdesk jellyfin nfs client module
Move the nixdesk jellyfin NFS client configuration from individual
host services to a centralized NixOS module in `modules/system/nfs/`.

This change:
- Creates `modules/system/nfs/default.nix` containing the `nixdeskJellyfin`
  flake module.
- Removes redundant NFS client definitions from `modules/hosts/nix-server/`
  and `modules/hosts/r5500/`.
- Updates `modules/hosts/r5500/configuration.nix` to use the new
  centralized module.
- Cleans up unused local disk configuration in `modules/hosts/14900k/`.
2026-07-17 11:11:21 -03:00
Olivier 5f7e9cada5 refactor(desktop): clean up options formatting and simplify slice logic
Refactor `modules/desktop/options.nix` to improve code readability by
expanding the `optPath` list definition and simplifying the
`wisdomSimpleSlice` configuration logic.

Also apply minor style fixes to `modules/wisdom/filebrowsers/nemo.nix`
to ensure consistent spacing within list definitions.
2026-07-16 20:55:53 -03:00
Olivier c4a0eb0a98 feat(chromium-hevc): enhance GPU configuration options and streamline desktop item creation 2026-07-16 19:51:06 -03:00
Olivier 103a485bf8 feat(wisdom): introduce wisdomSimpleSlice helper for package modules
Implement a `wisdomSimpleSlice` helper in `desktop/options.nix` to reduce
boilerplate for simple Home Manager modules that only manage packages.
This helper handles the `enable` option creation, platform-gating via
`availableOn`, and conditional inclusion in `home.packages`.

Refactor several existing wisdom modules to use this new helper:
- `wisdomBrowsersChrome`
- `wisdomBrowsersEdge`
- `wisdomEditorsKate`
- `wisdomEditorsObsidian`

Additionally, clean up `wisdom/default.nix` by removing the explicit
import of `wisdomShellBash` (which is now handled via auto-wiring) and
improving code formatting.
2026-07-16 19:46:23 -03:00
Olivier 13f35677be refactor(wisdom): decentralize module defaults and update conventions
Move Home Manager module defaults from a centralized `desktop-home-defaults.nix`
file into their respective `wisdom` slices. This change aligns with the
updated documentation regarding module ownership, where defaults live
with the option that owns them rather than in a shared defaults file.

Additionally, update `docs/conventions.md` to reflect this new pattern for
defining `mkEnableOption` defaults and move base git/encryption
configurations into the core `wisdom` module.

- Delete `modules/hosts/desktop-home-defaults.nix`
- Update `modules/wisdom/default.nix` to include shared git and package defaults
- Add `default = ...` to `mkEnableOption` in various `wisdom` modules
- Update `docs/conventions.md` with new HM slice guidelines
2026-07-16 13:51:48 -03:00
Olivier 579a120326 feat(14900k): update dms-defaults settings and plugin configurations
Add various UI and system configuration parameters to dms-defaults,
including matugen contrast, widget styling, hyprland layout overrides,
elevation settings, battery indicator options, and workspace color
customizations. Also update plugin settings to include a Home Assistant
refresh counter.
2026-07-16 13:45:31 -03:00
Olivier c90d368432 refactor(ssh): restructure inventory and modularize helper logic
Relocate SSH inventory and rbw socket wiring from `modules/lib/` to
dedicated `modules/ssh/` modules to improve logical grouping.

- Move `ssh-inventory.nix` to `modules/ssh/inventory.nix`.
- Move `rbw-ssh-socket.nix` to `modules/ssh/rbw.nix`.
- Relocate `users-merge.nix` logic into `modules/system/users.nix` to
  reduce dependency on generic library modules.
- Update `docs/conventions.md` to reflect new module paths.
- Refactor `modules/deploy/navi.nix` to use a local helper for
  generating Navi hive configurations.
- Add Pi5 specific Niri KDL configuration snippets.
2026-07-15 23:27:03 -03:00
Olivier a787840ce2 style(desktop): reformat nix code and simplify module logic
Refactor `modules/desktop/options.nix` to improve code readability through
consistent indentation and whitespace management. Simplified the
conditional logic for `home-manager.extraSpecialArgs` to remove
unnecessary complexity.

Clean up host configurations by removing commented-out code, removing
unnecessary whitespace, and flattening nested attribute sets into
single-line declarations where appropriate.
2026-07-15 22:20:07 -03:00
Olivier e2fd47aa58 feat(users): add declarative rbw configuration for olivier
Introduce rbw settings within olivier's home-manager module to
centralize the email and base_url configuration. This replaces
manual per-host configuration with a declarative approach.
2026-07-15 18:58:54 -03:00
Olivier 5d70868e4f feat(14900k): replace windsurf with devin-desktop in olivier's extra packages
Update the extra packages for user olivier in the private AI module
to include devin-desktop instead of windsurf.
2026-07-15 18:20:50 -03:00
Olivier db88a44435 refactor(ssh): centralize identity file logic and optimize host selection
Refactor the SSH configuration generation to reduce duplication and
improve efficiency.

- Extract `IdentityFile` line generation into a shared variable in
  `ssh-inventory.nix` to ensure consistency between per-host and
  gitea blocks.
- Optimize `selectedHosts` resolution in `home-manager/default.nix`
  by reusing `activeHosts` when the "all" wildcard is used.
- Tighten IPv4 regex matching in `ssh-inventory.nix` using anchors.
- Update `users-merge.nix` to propagate `extraIdentities` to the
  rbw outbound configuration.
- Set default `authorizedHosts` to "all" for inbound SSH users.
2026-07-15 18:20:40 -03:00
Olivier 524abb9ceb Flake lock update 2026-07-15 10:38:27 -03:00
Olivier 6291076494 chore(14900k): disable handbrake due to ffmpeg-full incompatibility
Comment out handbrake installation because the current ffmpeg-full-8.1.2
version in nixpkgs contains a broken A01-mov-read patch that prevents
successful builds. This is a temporary measure until nixpkgs is updated
or the system is switched to a stable channel.
2026-07-15 10:38:08 -03:00
Olivier f3d358ce12 feat(14900k): add MediaTek MT7927 WiFi and Bluetooth support
Add a private module to enable MediaTek MT7927 (Filogic 380) support.
This includes a firmware path fix for Bluetooth and a systemd service
to force-load patched out-of-tree kernel modules, bypassing in-tree
drivers that lack support for this hardware on nixos-unstable.
2026-07-15 10:37:43 -03:00
Olivier 645e1f0f31 Refactor ssh&users 2026-07-15 10:02:04 -03:00
Olivier e68321ac1e Add Virtual-1 display configuration to DMS settings
- Included a new display entry for "Virtual-1" in the DMS settings.json file, enhancing display management capabilities.
2026-07-13 12:39:05 -03:00
Olivier 5357ba307b Fix dms-plugins
- Transitioned to an auto-derived plugin registry from flake inputs, simplifying the addition of new plugins by requiring edits only in `flake.nix`.
- Implemented a camelCase transformation for plugin IDs to align with upstream conventions, with exceptions for specific plugins.
- Enhanced the `shipPlugin` function to route plugin sources correctly, ensuring compatibility with the DMS structure.
- Added assertions to validate Obsidian vault paths at build time, improving error handling and user guidance.
2026-07-13 12:39:05 -03:00
Olivier 9960e874c4 Refactor DMS IPC bindings for Niri and Hyprland
- Updated `binds.nix` files for both Niri and Hyprland to utilize a centralized `dmsIpcBinds` structure, improving maintainability and consistency across keybindings.
- Introduced `dmsIpcBinds` in `default.nix` for both modules to streamline the command definitions for DMS IPC calls.
- Enhanced `dms` module to define shared IPC commands, ensuring uniform command formatting for different compositors.
2026-07-13 12:39:05 -03:00
Olivier 2f28ec735d Add freebuff to t2mbp 2026-07-13 11:47:23 -03:00
Olivier 2cdfaa3ed7 Update desktop-home-defaults.nix to include feishin package 2026-07-12 23:49:22 -03:00
Olivier 32cb8955eb Add VM self-test configuration for NixOS 2026-07-12 23:48:56 -03:00
Olivier d9b82bbf2b Updated the nvidia.nix file to set hardware.nvidia-container-toolkit.enable to a default value of true, ensuring consistent behavior across configurations. 2026-07-12 21:07:57 -03:00
Olivier 6f0c6b9c31 Remove Tailscale configuration from t2mbp setup
- Deleted the Tailscale auth key configuration and related networking settings from `configuration.nix` to streamline the host setup.
- Maintained other networking configurations for continued functionality.
2026-07-12 21:03:54 -03:00
Olivier bab8bdecb7 Simplify modulesTree definition
- Streamlined the construction of modulesTree by consolidating filter logic into a single line for improved readability and maintainability.
2026-07-12 21:02:09 -03:00
Olivier 15fcf88da2 Add Cloudflare Turnstile integration to Gitea configuration
- Introduced `cloudflare-turnstile` secrets in `secrets.yaml` for enhanced CAPTCHA support.
- Updated Gitea service configuration to enable Turnstile CAPTCHA with the new secret and site keys.
- Modified last modified date and MAC in `secrets.yaml` for security compliance.
2026-07-10 17:54:04 -03:00
Olivier b70f9f4822 Update Cloudflare DynDNS configuration to replace domain "xn--1iu.cc" with "慧.cc" for improved domain management. 2026-07-10 17:44:55 -03:00
Olivier 87f839d16a Enhance flake configuration with new modules and updates
- Added `hermes` module to integrate the Hermes agent into the flake.
- Introduced new `ai.nix` module for Ollama service configuration, enabling AI capabilities.
- Updated `flake.lock` to include new dependencies and revisions for `hermes`, `npm-lockfile-fix`, and other related packages.
- Refactored existing flake structure to improve organization and maintainability.
- Enabled `nix-ld` in the host configuration for enhanced linking capabilities.
2026-07-10 17:44:40 -03:00
Olivier 25cf0167c1 Enhance 14900k configuration with streaming display support and virtual EDID generation
- Added a new `streaming-display.nix` module to configure a headless 4K virtual output for Sunshine and Moonlight.
- Introduced a Python script `generate-virtual-edid.py` to create a virtual display EDID with 4K capabilities.
- Updated `configuration.nix` to integrate the new streaming display module and enable related services.
- Modified `displays.nix` to accommodate the new streaming display settings and ensure compatibility with existing GPU passthrough configurations.
2026-07-10 17:32:37 -03:00
Olivier dc06c70c43 Refactor DMS configuration and introduce home-manager integration
- Simplified `default.nix` by removing legacy home-manager settings and delegating them to a new `_private/hm.nix` module for better organization.
- Added a new script `dms-restart-once.sh` to handle service restarts and cache management for DMS plugins.
- Updated the DMS configuration to streamline the integration of third-party plugins and improve overall modularity.
2026-06-22 13:14:08 -03:00
Olivier fa62ba2b8c Refactor desktop home configuration and introduce caching module
- Simplified `desktop-home-base.nix` by removing legacy git and application settings, delegating them to a new `desktop-home-defaults.nix` module for better organization.
- Added a caching configuration for `chiasson.system.caching.attic` in `desktop-home-base.nix`, enabling caching features with customizable options.
- Updated host-specific configurations to remove direct caching settings, ensuring a cleaner and more modular setup.
2026-06-22 12:29:16 -03:00
Olivier 3b5c031619 Add rbw SSH socket support in home-manager modules
- Introduced a new module `rbw-ssh-socket.nix` to manage SSH_AUTH_SOCK for rbw.
- Updated `default.nix` to utilize the new SSH socket variable for session management.
- Refactored Bash and Fish shell configurations to use the new module for setting SSH_AUTH_SOCK, improving maintainability and consistency across shell environments.
2026-06-22 12:28:25 -03:00
Olivier 1524100ff5 Add DMS plugins for rbw lock and wvkbd toggle
- Introduced new DMS plugins: `rbw-lock-toggle` for managing Bitwarden vault state and `wvkbd-toggle` for showing/hiding the on-screen keyboard.
- Updated `flake.lock` and `flake.nix` to include the new plugins and their configurations.
- Removed legacy options and files related to the rbw lock toggle, streamlining the DMS configuration.
- Enhanced the home-manager module to support the new plugins and their integration into the DMS environment.
2026-06-22 12:26:25 -03:00
Olivier 662d24a89c Add Hyprland and Niri configuration files
- Introduced `binds.nix` for Hyprland, defining keybindings for various applications and workspace management.
- Added `base-settings.nix` and `window-rules.nix` for Niri, establishing keyboard input settings and window management rules.
- Updated `default.nix` for both Hyprland and Niri to import new configuration files, enhancing modularity and maintainability.
2026-06-22 11:48:59 -03:00
Olivier 41ed709f4a Update Moonfin Flatpak URL and hash to version 2.2.0 2026-06-22 11:45:19 -03:00
Olivier 3fdb859ff8 Refactor DMS configuration and enhance Obsidian integration
- Replaced legacy Obsidian snippet directory options with a unified `obsidianVaults` option for better management.
- Updated DMS configuration to streamline widget handling and removed deprecated options.
- Introduced new templates for Hyprland colors and Obsidian themes to improve user customization.
- Added scripts for SwiftShare upload functionality, enhancing screenshot capabilities within the DMS environment.
2026-06-21 15:56:27 -03:00
Olivier 8b66fa8665 Add DMS configuration and helper scripts
- Introduced a new `justfile` for managing DMS-related tasks, including a recipe for syncing live DMS configuration.
- Added a `devshell.nix` file to set up a development environment with necessary packages and shell hooks for DMS.
- Updated DMS settings to replace `defaultSettingsFile` with `defaultSeedDir` for improved configuration management.
- Created a new script `sync-seed.sh` to facilitate syncing DMS settings from the live configuration to host-specific directories.
- Enhanced documentation and examples for DMS configuration and usage.
2026-06-21 04:45:30 -03:00
Olivier 4ca225ed60 Add DMS plugins and configuration enhancements
- Introduced multiple DMS plugins including ambient sound, calculator, home assistant, dropdown menu, OCR scanner, and more.
- Updated flake.nix to include new plugin URLs and configurations.
- Enhanced the DMS home-manager module to support third-party plugins and their settings.
- Added keybindings for new plugins in the Niri configuration.
- Updated default settings to include new plugins and their visibility options.
2026-06-21 04:18:36 -03:00
Olivier 02c6b3a45f Add default settings file support for Dank Material Shell (DMS)
- Introduced a new option `defaultSettingsFile` in the DMS configuration to allow host-specific first-run settings.
- Updated the home-manager module to read from the specified settings file if provided, otherwise falling back to the bundled defaults.
- Enhanced the documentation in the default settings file to clarify its purpose and usage.
2026-06-20 21:39:48 -03:00
Olivier de1f9fc770 Add Home Assistant secret configuration 2026-06-20 21:20:22 -03:00
Olivier 596427e491 Add sysctl settings for inotify limits in NixOS configuration
- Increased maximum user watches to 1,048,576 and user instances to 2,048 to enhance file monitoring capabilities.
2026-06-20 21:20:08 -03:00
Olivier 17337b9675 Add DMS configuration enhancements and new default settings
- Introduced a new private default settings file for Dank Material Shell (DMS) to manage user-specific settings.
- Updated DMS home-manager module to generate a settings seed file, merging extra right-bar widgets into the first-run configuration.
- Added support for dynamic inclusion of DMS window rules based on user configuration.
- Enhanced the Niri configuration with a preference for no client-side decorations and improved keybindings for bar toggling.
2026-06-20 21:20:00 -03:00
Olivier 1a34db6f22 Replace Dolphin with Nemo as the default file manager across various configurations. Enable GVFS for better file management support in GTK applications. Remove Dolphin configuration files and introduce Nemo configuration files to streamline the desktop experience. 2026-06-20 21:18:03 -03:00
Olivier 9b20b71b50 Fix Sops configuration for user 'olivier' by updating the path to the hashed password secret. 2026-06-20 16:52:59 -03:00
Olivier 4034a6d81d Add git-crypt support and update Sops configuration for multiple hosts
- Introduced a new secrets file for git-crypt personal documents.
- Updated .sops.yaml to include new key groups for host-specific secrets.
- Configured Sops settings in client-services.nix for managing secrets.
- Removed redundant Sops configurations from multiple host configurations (14900k, ideapad, t2mbp, uConsole) to streamline secret management.
- Added git-crypt to desktop-home-base package list.
2026-06-20 16:52:08 -03:00
Olivier 026aea4ac4 Added keybindings for toggling the main bar and bar 2 in the Niri setup. 2026-06-20 16:49:00 -03:00
Olivier fe202a6843 Add window rules for Picture-in-Picture and Minecraft in Niri configuration 2026-06-20 16:48:36 -03:00
109 changed files with 5666 additions and 3360 deletions
+8
View File
@@ -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))$
+14 -4
View File
@@ -17,16 +17,26 @@ How to work on it: [conventions.md](./conventions.md).
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).
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 + hints
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>`.
@@ -37,11 +47,11 @@ Plain rebuild still works: `sudo nixos-rebuild switch --flake .#<host>`.
modules/
hosts/<host>/ # per-machine composition
system/ # nixosModules.system aggregate
desktop/ # GUI stack
desktop/ # GUI stack (niri, hyprland, plasma, DMS)
wisdom/ # HM modules (exports still named wisdom*)
ssh/ # inbound NixOS + outbound HM
deploy/ # navi hive
lib/ # pure helpers → flake.lib
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.
+73 -9
View File
@@ -47,7 +47,7 @@ Leave upstream `services.*`, `networking.*`, `home.*`, etc. alone.
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/lib/wisdom-catalog.nix`); hosts only set matching `chiasson.home.*.enable` toggles — no re-import in `home.nix`.
**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).
@@ -63,13 +63,75 @@ User apps / dotfiles → wisdom. Daemons, firewall, kernel → NixOS. Sometimes
- `desktopWallpapers` copies `inputs.wallpapers` into the store; override `chiasson.desktop.wallpapers.source` if needed.
## Users
## Users & SSH
User definitions live in `usersCatalogDefaults` (`modules/system/users/catalog-default.nix`) — olivier, server, builder, etc. Pick who exists on a host with `chiasson.users.enabled`.
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.
On a host:
### Host overrides
```nix
# configuration.nix — machine policy
@@ -77,6 +139,8 @@ 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
@@ -86,7 +150,7 @@ modules = [
self.nixosModules."14900kHome"
];
# home.nix — flake fragment, per-host `chiasson.home.*` toggles
# home.nix — flake fragment, per-host chiasson.home.* toggles
{ self, inputs, ... }: {
flake.nixosModules."14900kHome" = { self, pkgs, ... }: {
imports = [ self.nixosModules.desktopHomeBase ];
@@ -102,15 +166,15 @@ modules = [
`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.
`usersHomeIntegration` turns that into `users.users` + HM. Don't hand-roll catalog users unless you're changing the users module itself.
SSH: `sshInbound` on NixOS, outbound/rbw under `modules/ssh/home-manager/`.
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 except `wisdom` / `wisdomShellBash`). Gate packages on `chiasson.home.*.enable`, set `mkDefault true` in `desktop-home-base.nix` if shared, or `enable = true` in a host `home.nix`. Upstream HM deps stay imported unconditionally — use `mkIf` on `cfg.enable` for config (never `config`-dependent `imports`; that recurses).
**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.
Generated
+674 -104
View File
File diff suppressed because it is too large Load Diff
+99 -10
View File
@@ -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";
@@ -110,6 +200,12 @@
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:
@@ -117,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
+8
View File
@@ -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
View File
@@ -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
{
flake.navi = self.lib.mkNaviHiveConfig {
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 = 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";
+4
View File
@@ -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).
+75
View File
@@ -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%-"
];
};
}
+11 -63
View File
@@ -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"
"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;
+18
View File
@@ -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;
};
}
+104
View File
@@ -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" = _: { };
};
}
+50 -140
View File
@@ -1,140 +1,39 @@
{ self, inputs, ... }:
{ self, inputs, ... }: {
imports = [
./window-rules.nix
./base-settings.nix
./binds.nix
];
flake.homeManagerModules.desktopNiri =
{ config, lib, pkgs, osConfig ? { }, ... }:
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; # shell from kitty HM config
"Mod+Control+T"."spawn-sh" = "konsole"; #TODO[epic=Binds] This should only be set if having konsole
"Mod+D"."spawn-sh" = "rofi -show drun"; #TODO[epic=Binds] This should only be set if having rofi
"Mod+Space"."spawn-sh" = "dms ipc call spotlight toggle"; #TODO[epic=Binds] This should only be set if having dms
"Mod+E"."spawn-sh" = "dolphin"; #TODO[epic=Binds] This should only be set if having dolphin
"Mod+Control+O"."spawn-sh" = "rofi -show window"; #TODO[epic=Binds] This should only be set if having rofi
"Mod+V"."spawn-sh" = "dms ipc call clipboard toggle"; #TODO[epic=Binds] This should only be set if having dms
"Mod+N"."spawn-sh" = "dms ipc call notepad toggle"; #TODO[epic=Binds] This should only be set if having dms
"Mod+Shift+N"."spawn-sh" = "dms ipc call notifications toggle"; #TODO[epic=Binds] This should only be set if having dms
"Mod+M"."spawn-sh" = "dms ipc call processlist toggle"; #TODO[epic=Binds] This should only be set if having dms
"Mod+L"."spawn-sh" = "dms ipc call lock lock"; #TODO[epic=Binds] This should only be set if having dms
"Mod+Q"."close-window" = _: { };
"Mod+F"."maximize-column" = _: { };
"Mod+Shift+F"."fullscreen-window" = _: { };
"Mod+O"."toggle-overview" = _: { };
"Mod+Shift+NumberSign"."show-hotkey-overlay" = _: { };
"Mod+Shift+E".quit = _: { };
"Mod+Left"."focus-column-or-monitor-left" = _: { };
"Mod+Down"."focus-window-or-monitor-down" = _: { };
"Mod+Up"."focus-window-or-monitor-up" = _: { };
"Mod+Right"."focus-column-or-monitor-right" = _: { };
"Mod+Shift+WheelScrollDown"."focus-workspace-down" = _: { };
"Mod+Shift+WheelScrollUp"."focus-workspace-up" = _: { };
"Mod+WheelScrollDown"."focus-column-or-monitor-right" = _: { };
"Mod+WheelScrollUp"."focus-column-or-monitor-left" = _: { };
"Mod+Shift+Left"."move-column-left-or-to-monitor-left" = _: { };
"Mod+Shift+Down"."move-window-down" = _: { };
"Mod+Shift+Up"."move-window-up" = _: { };
"Mod+Shift+Right"."move-column-right-or-to-monitor-right" = _: { };
"Mod+Page_Up"."focus-workspace-up" = _: { };
"Mod+Page_Down"."focus-workspace-down" = _: { };
"Mod+Shift+Page_Up"."move-column-to-workspace-up" = _: { };
"Mod+Shift+Page_Down"."move-column-to-workspace-down" = _: { };
"Mod+R"."switch-preset-column-width" = _: { };
"Mod+BracketLeft"."consume-or-expel-window-left" = _: { };
"Mod+BracketRight"."consume-or-expel-window-right" = _: { };
"Mod+Comma"."consume-window-into-column" = _: { };
"Mod+Period"."expel-window-from-column" = _: { };
"Mod+Alt+Space"."toggle-window-floating" = _: { };
#"Mod+Shift+V"."switch-focus-between-floating-and-tiling" = _: { }; #TODO[epic=Binds] Find free bind that is not in the way and not overcomplicated to remember
"Mod+1"."focus-workspace" = 1;
"Mod+2"."focus-workspace" = 2;
"Mod+3"."focus-workspace" = 3;
"Mod+4"."focus-workspace" = 4;
"Mod+5"."focus-workspace" = 5;
"Mod+6"."focus-workspace" = 6;
"Mod+7"."focus-workspace" = 7;
"Mod+8"."focus-workspace" = 8;
"Mod+9"."focus-workspace" = 9;
"Mod+Ctrl+1"."move-column-to-workspace" = 1;
"Mod+Ctrl+2"."move-column-to-workspace" = 2;
"Mod+Ctrl+3"."move-column-to-workspace" = 3;
"Mod+Ctrl+4"."move-column-to-workspace" = 4;
"Mod+Ctrl+5"."move-column-to-workspace" = 5;
"Mod+Ctrl+6"."move-column-to-workspace" = 6;
"Mod+Ctrl+7"."move-column-to-workspace" = 7;
"Mod+Ctrl+8"."move-column-to-workspace" = 8;
"Mod+Ctrl+9"."move-column-to-workspace" = 9;
"XF86AudioRaiseVolume".spawn = [
"wpctl"
"set-volume"
"@DEFAULT_AUDIO_SINK@"
"0.05+"
];
"XF86AudioLowerVolume".spawn = [
"wpctl"
"set-volume"
"@DEFAULT_AUDIO_SINK@"
"0.05-"
];
"XF86AudioMute".spawn = [
"wpctl"
"set-mute"
"@DEFAULT_AUDIO_SINK@"
"toggle"
];
# Backlight: relies on `pkgs.brightnessctl` being on PATH (provided by `desktopGui` when
# any GUI session is enabled) and the user being in the `video` group (catalog default).
"XF86MonBrightnessUp".spawn = [ "brightnessctl" "set" "+5%" ];
"XF86MonBrightnessDown".spawn = [ "brightnessctl" "set" "5%-" ];
Print.screenshot = _: { };
"Ctrl+Print"."screenshot-screen" = _: { };
"Alt+Print"."screenshot-window" = _: { };
};
};
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 "");
@@ -153,28 +52,41 @@ let
// 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 =
@@ -228,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) {
+37
View File
@@ -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;
}
];
}
+53 -12
View File
@@ -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;
};
@@ -159,10 +199,12 @@
};
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;
}
];
}
+276
View File
@@ -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
View File
@@ -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
+44 -64
View File
@@ -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": "chiasson.cloud",
"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": "chiasson.cloud",
"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
}
+31
View File
@@ -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"
'';
};
};
}
+11
View File
@@ -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];}];
}
+24 -38
View File
@@ -1,16 +1,17 @@
# Monitor layout for 14900k.
# NVIDIA (default): DP-2 ultrawide, HDMI-A-3 + DP-4 side/top stack.
# Intel iGPU (gpu passthru): DP-1 ultrawide, HDMI-A-2 Samsung to the left.
# Niri: `extraSettings` (KDL via `extraConfig`, other wrapper-modules keys) + `extraBinds` (merged keybinds).
# Hyprland: `chiasson.desktop.hyprland.settings` (merged in HM when `chiasson.desktop.hyprland.enable`).
# 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, ... }:
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
(if gpuPassthru then
''
output "DP-1" {
mode "2560x1080@144"
@@ -42,43 +43,31 @@ let
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}
}
'';
# 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"
]
(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"
];
[ "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"
]
[ "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"
];
[ "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 = {
@@ -86,10 +75,7 @@ in
};
chiasson.desktop.niri.extraBinds."XF86Tools".spawn = [
"wpctl"
"set-mute"
"@DEFAULT_AUDIO_SOURCE@"
"toggle"
"wpctl" "set-mute" "@DEFAULT_AUDIO_SOURCE@" "toggle"
];
chiasson.desktop.hyprland.settings = lib.mkIf config.chiasson.desktop.hyprland.enable {
@@ -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()
@@ -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"
'';
};
}
@@ -6,8 +6,8 @@
{
appId = "org.moonfin.linux";
bundle = pkgs.fetchurl {
url = "https://github.com/Moonfin-Client/Moonfin-Core/releases/download/2.1.0/Moonfin_Linux_v2.1.0.flatpak";
hash = "sha256-dJcI/bzKS/+SCkHarSBesei5CKCm93SjRJBp09Jn810=";
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 -1
View File
@@ -15,5 +15,5 @@ lib.mkIf (!config.chiasson.system.gpuPassthru.enable) {
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\"}]";
};
})
]);
}
+44
View File
@@ -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 ];
};
};
}
+16 -30
View File
@@ -15,34 +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" ];
};
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;
programs.nix-ld.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.
@@ -55,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;
@@ -84,7 +64,7 @@ services.cloudflare-warp.enable = true;
"--flake"
".#14900k"
];
enableRbwLockToggle = true;
defaultSeedDir = ./_private/dms-defaults;
};
};
@@ -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
-2
View File
@@ -4,8 +4,6 @@
system = "x86_64-linux";
specialArgs = {
inherit self inputs;
host = "14900k";
system = "x86_64-linux";
};
modules = [
self.nixosModules."14900kConfiguration"
+7
View File
@@ -23,6 +23,13 @@
pokeclicker.enable = true;
};
editors.vscode = {
enable = true;
extensions = with pkgs.vscode-extensions; [
alefragnani.project-manager
johnpapa.vscode-peacock
];
};
};
}
];
+32
View File
@@ -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";
};
};
}
+11 -79
View File
@@ -1,90 +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, inputs, ... }:
{ self, ... }:
{
flake.nixosModules.desktopHomeBase =
{ self, lib, pkgs, ... }:
{ config, lib, ... }:
{
chiasson.system.chromiumHevc.enable = lib.mkDefault true;
chiasson.users.extraModules.olivier =
self.lib.wisdomCatalogExtraModules self
++ [
(
{ lib, pkgs, ... }:
let
aarch64 = pkgs.stdenv.hostPlatform.isAarch64;
in
{
programs.git = {
enable = lib.mkDefault true;
settings.user = {
name = "OlivierChiasson";
email = "olivierchiasson@hotmail.fr";
};
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.home = {
# Catalog of `chiasson.home.*` toggles — host `home.nix` sets `enable = true` to override `mkDefault false`.
shell = {
fish.enable = lib.mkDefault true;
ohMyPosh.enable = lib.mkDefault true;
yazi.enable = lib.mkDefault true;
};
terminals.kitty.enable = lib.mkDefault true;
filebrowsers.dolphin.enable = lib.mkDefault true;
browsers = {
chrome.enable = lib.mkDefault false;
chromiumHevc = {
enable = lib.mkDefault true;
packages = lib.mkDefault (
if aarch64 then
[ "chromium" ]
else
[ "google-chrome" ]
);
} // lib.optionalAttrs (!aarch64) {
# Chromium + NVIDIA VA-API → frame pool errors in Jellyfin; gecko may fare better on NVIDIA.
vaapi.gpu = lib.mkDefault "intel";
};
edge.enable = lib.mkDefault false;
flow.enable = lib.mkDefault false;
orion.enable = lib.mkDefault false;
zen.enable = lib.mkDefault false;
};
editors = {
cursor.enable = lib.mkDefault true;
kate.enable = lib.mkDefault false;
obsidian.enable = lib.mkDefault true;
};
apps = {
discord.enable = lib.mkDefault false;
localsend.enable = lib.mkDefault true;
pokeclicker.enable = lib.mkDefault false;
spotify = {
enable = lib.mkDefault false;
openDiscoveryFirewall = lib.mkDefault false;
};
};
desktop = {
screenshot = {
enable = lib.mkDefault true;
swiftshareApiKeyFile = "/run/secrets/swiftshare/API_KEY"; #TODO[epic=sops] redo this by passing sops file output directly
};
# WhiteSur GTK/icons, Phinger cursor, Qt via KDE platform theme; imports DMS matugen `dank-colors.css`.
theming.enable = lib.mkDefault true;
};
hardware.uconsoleGamepad.enable = lib.mkDefault false;
};
}
)
];
chiasson.users.extraModules.olivier = self.lib.wisdomCatalogExtraModules self;
};
}
+1 -39
View File
@@ -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.
-2
View File
@@ -4,8 +4,6 @@
system = "aarch64-linux";
specialArgs = {
inherit self inputs;
host = "ideapad";
system = "aarch64-linux";
};
modules = [
self.nixosModules.ideapadConfiguration
-13
View File
@@ -4,18 +4,5 @@
{ self, ... }:
{
imports = [ self.nixosModules.desktopHomeBase ];
# Host-only HM overrides (tablet apps, toggles off from desktop-home-base, …).
chiasson.users.extraModules.olivier = [
# {
# chiasson.home = {
# # editors.kate.enable = true;
# };
# }
# (
# { pkgs, ... }:
# { home.packages = with pkgs; [ ]; }
# )
];
};
}
@@ -16,7 +16,7 @@ in
"blackfry.day"
"yestur.day"
"rp-own.life"
"xn--1iu.cc"
".cc"
];
proxied = true;
ipv4 = true;
+19 -2
View File
@@ -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;
};
}
-2
View File
@@ -3,8 +3,6 @@
system = "x86_64-linux";
specialArgs = {
inherit self inputs;
host = "nix-server";
system = "x86_64-linux";
};
modules = [
self.nixosModules.nix-serverConfiguration
+5 -2
View File
@@ -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-06-12T00:44:20Z"
mac: ENC[AES256_GCM,data:cyI5NH6+OR/Q4T3atFN+gXsJbRaR/Q5I+MyJHy5qXsShh8LnQvEKBnnIPKz2xpuDwFvKd4DabMOxcu5xe6IRH9TN7G411vp+vruM+TicoCJ4U2xAP/J9Tlh3YKT6kZJqj/zAhvP5qPCzvz7xkFTvluLrWgPlaHWn/rlCskh0t5M=,iv:WBeS1rqjJblNGMVa7hEn5MXbZJU8WKd6541BCxu2rGI=,tag:OfOzQI6ESUdUYCbBEq9GFw==,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;
};
}
+1 -1
View File
@@ -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
-2
View File
@@ -3,8 +3,6 @@
system = "x86_64-linux";
specialArgs = {
inherit self inputs;
host = "r5500";
system = "x86_64-linux";
};
modules = [
self.nixosModules.r5500Configuration
-42
View File
@@ -19,29 +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";
};
sops.secrets."tailscale/auth-key" = {
mode = "0400";
};
chiasson.system.librepods.enable = true;
chiasson.system.palera1n.enable = true;
@@ -73,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 = {
@@ -98,9 +64,6 @@
};
defaultSession = "niri";
shell = "dms";
shells.dms = {
enableRbwLockToggle = true;
};
};
chiasson.system = {
@@ -109,11 +72,6 @@
networking = {
hostName = "t2mbp";
networkManager.enable = true;
tailscale = {
enable = true;
authKeyFile = config.sops.secrets."tailscale/auth-key".path;
acceptRoutes = true;
};
};
};
-2
View File
@@ -4,8 +4,6 @@
system = "x86_64-linux";
specialArgs = {
inherit self inputs;
host = "t2mbp";
system = "x86_64-linux";
};
modules = [
self.nixosModules.t2mbpConfiguration
+10 -4
View File
@@ -1,21 +1,27 @@
{ self, inputs, ... }:
{
flake.nixosModules.t2mbpHome =
{ self, ... }:
{ self, pkgs, ... }:
{
imports = [ self.nixosModules.desktopHomeBase ];
chiasson.users.extraModules.olivier = [
{
chiasson.home = {
browsers = {
edge.enable = true;
};
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
];
};
};
}
];
-31
View File
@@ -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;
-2
View File
@@ -4,8 +4,6 @@
specialArgs = inputs // {
inherit self;
inputs = inputs;
host = "uConsole";
system = "aarch64-linux";
};
trustCaches = false;
modules = [
+9 -3
View File
@@ -1,14 +1,20 @@
{ self, inputs, ... }:
{
flake.nixosModules.uConsoleHome =
{ self, ... }:
{ self, pkgs, ... }:
{
imports = [ self.nixosModules.desktopHomeBase ];
chiasson.users.extraModules.olivier = [
{
chiasson.home = {
hardware.uconsoleGamepad.enable = true;
chiasson.home.hardware.uconsoleGamepad.enable = true;
chiasson.home.editors.vscode = {
enable = true;
extensions = with pkgs.vscode-extensions; [
alefragnani.project-manager
johnpapa.vscode-peacock
];
};
}
];
-33
View File
@@ -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;
}
-131
View File
@@ -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 { };
};
}
-99
View File
@@ -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))
];
};
}
-17
View File
@@ -1,17 +0,0 @@
# Resolve `wisdom*` HM slices for NixOS `extraModules` (`self` is not an HM specialArg).
# Slices are gated by `chiasson.home.*.enable`; hosts only flip toggles in `home.nix`.
{ lib, ... }: {
flake.lib.wisdomCatalogExtraModules =
self:
let
names = lib.sort builtins.lessThan (
lib.filter (
n:
lib.hasPrefix "wisdom" n
&& n != "wisdom"
&& n != "wisdomShellBash"
) (builtins.attrNames self.homeManagerModules)
);
in
map (name: self.homeManagerModules.${name}) names;
}
+102 -44
View File
@@ -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
'';
}
]);
])
]
);
};
}
+159
View File
@@ -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;
};
}
+28 -12
View File
@@ -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;
};
})
];
};
}
+39
View File
@@ -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 -6
View File
@@ -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;
});
})
]);
+38
View File
@@ -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;
};
};
}
+339
View File
@@ -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;
})
];
};
}
-96
View File
@@ -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;
};
};
};
}
-55
View File
@@ -1,55 +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))
// {
merge =
loc: defs:
let
values = map (d: d.value) defs;
names = lib.unique (lib.concatLists (map builtins.attrNames values));
in
lib.genAttrs names (
name: lib.concatLists (map (v: v.${name} or [ ]) values)
);
};
default = { };
description = ''
Per-user Home Manager `extraModules` keyed by catalog user name.
Keys must match `chiasson.users.enabled`. Lists from multiple modules
are concatenated (e.g. `desktop-home-base.nix` + host `home.nix`).
'';
};
homeManager = {
autoWire = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Create HM users from the catalog when true.";
};
};
};
};
}
-27
View File
@@ -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
];
};
}
-71
View File
@@ -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;
})
];
};
}
+3 -1
View File
@@ -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 [
{
+6 -19
View File
@@ -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 ];
};
}
+6 -14
View File
@@ -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 { }) ];
};
}
+6 -17
View File
@@ -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 ];
};
}
+49 -47
View File
@@ -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 {
+6 -15
View File
@@ -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 ];
};
}
+13 -23
View File
@@ -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;
};
})
];
};
}
+3 -1
View File
@@ -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 ];
+3 -1
View File
@@ -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 = {
+28 -20
View File
@@ -1,32 +1,21 @@
# HM side of the flake; option tree is `chiasson.home.*` (docs/conventions.md).
{ self, inputs, ... }: {
imports = [
./apps/discord.nix
./apps/localsend.nix
./apps/pokeclicker
./apps/spotify.nix
./browsers/orion.nix
./desktop/screenshot.nix
./hardware/uconsole-gamepad.nix
];
# Root module: chiasson.home.enable + bash. Other `wisdom*` slices auto-wire via
# `lib.wisdomCatalogExtraModules`; hosts flip `chiasson.home.*.enable` rather than re-importing.
# Root module: chiasson.home.enable + git identity. Other `wisdom*` slices (including
# the bash shell) auto-wire via `lib.wisdomCatalogExtraModules`; hosts flip
# `chiasson.home.*.enable` rather than re-importing.
flake.homeManagerModules.wisdom =
{ config, lib, ... }:
{ config, lib, pkgs, ... }:
let
cfg = config.chiasson.home;
in
{
imports = [
self.homeManagerModules.wisdomShellBash
];
options.chiasson.home = {
enable = lib.mkEnableOption ''
enable =
lib.mkEnableOption ''
HM profile root for this flake (bash on by default). Desktop hosts use
`lib.wisdomCatalogExtraModules` once per user and flip `chiasson.home.*.enable` on the host.
'' // {
''
// {
default = true;
};
@@ -39,6 +28,25 @@
};
};
config = lib.mkIf cfg.enable { home.packages = cfg.extraPackages; };
config = lib.mkIf cfg.enable (
lib.mkMerge [
{ home.packages = cfg.extraPackages; }
# Base git identity + encryption/player tools shared by every HM user.
{
programs.git = {
enable = true;
settings.user = {
name = "OlivierChiasson";
email = "olivierchiasson@hotmail.fr";
};
};
home.packages = [
pkgs.git-crypt
pkgs.feishin
];
}
]
);
};
}
+21 -13
View File
@@ -1,25 +1,26 @@
{ ... }: {
flake.homeManagerModules.wisdomDesktopGtkQtTheming =
{ config, lib, pkgs, ... }:
{ config, lib, pkgs, osConfig ? { }, ... }:
let
root = config.chiasson.home;
cfg = config.chiasson.home.desktop.theming;
dmsShell = (lib.attrByPath [ "chiasson" "desktop" "shell" ] null osConfig) == "dms";
qtPlatformTheme = if dmsShell then "qt6ct" else cfg.qt.platformTheme;
in
{
options.chiasson.home.desktop.theming = {
enable = lib.mkEnableOption ''
WhiteSur GTK + icon themes, Phinger cursor, and Qt via the KDE platform theme same idea as
the old `home-shared.nix` stack for Hyprland/Niri (no full Plasma session required). Optional
`dank-colors.css` import for DMS/matugen GTK accents.
'';
WhiteSur GTK + icon themes, Phinger cursor, and Qt via qt6ct (DMS) or KDE platform theme.
Matugen accent colors load via gtk3/gtk4 extraCss when matugenGtkColors is enabled.
'' // {
default = true;
};
matugenGtkColors = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Add `@import url("dank-colors.css");` to gtk3/gtk4 extraCss. DMS/matugen writes
`~/.config/gtk-3.0` / `gtk-4.0` `dank-colors.css` when dynamic theming is on disable if
you use this module without DMS.
Add `@import url("dank-colors.css");` to gtk3/gtk4 extraCss (DMS matugen writes the file).
'';
};
@@ -65,23 +66,30 @@
type = lib.types.str;
default = "kde";
description = ''
`QT_QPA_PLATFORMTHEME` (e.g. `kde` for Qt/KDE integration, matches previous flake).
`QT_QPA_PLATFORMTHEME` when not using DMS shell. DMS hosts use qt6ct automatically.
'';
};
extraPackages = lib.mkOption {
type = lib.types.listOf lib.types.package;
default = [ pkgs.whitesur-kde pkgs.qt6Packages.qt6ct ];
description = "Extra packages (KDE look-and-feel + qt6ct GUI).";
default = [ pkgs.kdePackages.qt6ct ];
description = "Qt platform integration packages.";
};
};
};
config = lib.mkIf (root.enable && cfg.enable) (lib.mkMerge [
{
home.packages = cfg.qt.extraPackages;
home.packages =
if dmsShell then
[ pkgs.kdePackages.qt6ct ]
else
cfg.qt.extraPackages;
home.sessionVariables = {
QT_QPA_PLATFORMTHEME = cfg.qt.platformTheme;
QT_QPA_PLATFORMTHEME = qtPlatformTheme;
}
// lib.optionalAttrs (qtPlatformTheme == "qt6ct") {
QT_QPA_PLATFORMTHEME_QT6 = "qt6ct";
};
home.pointerCursor = {
+14 -158
View File
@@ -7,15 +7,19 @@
hyprlandHm = lib.attrByPath [ "wayland" "windowManager" "hyprland" ] { } config;
hyprlandHmEnabled = hyprlandHm.enable or false;
keyOk = cfg.swiftshareApiKeyFile != null && cfg.swiftshareApiKeyFile != "";
uploadScript = builtins.readFile ./scripts/swiftshare-upload.sh;
screenshotScript = builtins.readFile ./scripts/swiftshare-screenshot.sh;
in
{
options.chiasson.home.desktop.screenshot.enable = lib.mkEnableOption ''
grim/slurp/swappy + SwiftShare helpers; Hyprland binds if HM Hyprland is on.
'';
'' // {
default = true;
};
options.chiasson.home.desktop.screenshot.swiftshareApiKeyFile = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
default = "/run/secrets/swiftshare/API_KEY";
description = ''
File with SwiftShare API key (sops path is fine). Required when screenshot module is on.
'';
@@ -33,6 +37,10 @@
(lib.mkIf (root.enable && cfg.enable && keyOk) (
let
apiKeyFile = cfg.swiftshareApiKeyFile;
swiftshareUpload = pkgs.writeShellScriptBin "swiftshare-upload" (
builtins.replaceStrings [ "@API_KEY_FILE@" ] [ apiKeyFile ] uploadScript
);
swiftshareScreenshot = pkgs.writeShellScriptBin "swiftshare-screenshot" screenshotScript;
in
lib.mkMerge [
{
@@ -42,162 +50,10 @@
swappy
wl-clipboard
libnotify
(writeShellScriptBin "swiftshare-upload" ''
#!${pkgs.bash}/bin/bash
set -euo pipefail
COPY_URL=0
if [ "$#" -ge 1 ] && [ "$1" = "--copy-url" ]; then
COPY_URL=1
shift
fi
APP_NAME=""
if [ "$#" -ge 2 ] && [ "$1" = "--app-name" ]; then
APP_NAME="$2"
shift 2
fi
API_KEY_FILE=${lib.escapeShellArg apiKeyFile}
if [ -r "$API_KEY_FILE" ]; then
SWIFTSHARE_API_KEY="$(tr -d '\n' < "$API_KEY_FILE")"
fi
if [ -z "''${SWIFTSHARE_API_KEY:-}" ]; then
${pkgs.libnotify}/bin/notify-send "SwiftShare upload" "SwiftShare API key missing (expected readable: $API_KEY_FILE)"
echo "Error: SwiftShare API key missing (expected readable: $API_KEY_FILE)" >&2
exit 1
fi
IMAGE_FILE=""
RESPONSE_FILE=""
cleanup() {
if [ -n "$RESPONSE_FILE" ] && [ -f "$RESPONSE_FILE" ]; then
rm -f "$RESPONSE_FILE"
fi
}
trap cleanup EXIT
if [ "$#" -ge 1 ] && [ "$1" != "-" ]; then
IMAGE_FILE="$1"
if [ "''${IMAGE_FILE#'/'}" = "''${IMAGE_FILE}" ]; then
IMAGE_FILE="$(${pkgs.coreutils}/bin/readlink -f "''${IMAGE_FILE}")"
fi
if [ ! -f "$IMAGE_FILE" ]; then
echo "Error: file not found: $IMAGE_FILE" >&2
exit 1
fi
else
APP_NAME="''${APP_NAME:-screenshot}"
APP_NAME="''${APP_NAME%% *}"
APP_NAME="''${APP_NAME//[^A-Za-z0-9]/}"
APP_NAME="''${APP_NAME,,}"
if [ -z "$APP_NAME" ]; then
APP_NAME="screenshot"
fi
IMAGE_FILE="$(${pkgs.coreutils}/bin/mktemp --suffix=.png "''${TMPDIR:-/tmp}/''${APP_NAME}_XXXXXX")"
cat > "$IMAGE_FILE"
fi
if [ ! -s "$IMAGE_FILE" ]; then
${pkgs.libnotify}/bin/notify-send "SwiftShare" "Empty capture (maybe canceled) not uploading"
echo "Empty image file, not uploading." >&2
exit 0
fi
RESPONSE_FILE="$(mktemp)"
set +e
HTTP_STATUS="$(${pkgs.curl}/bin/curl -sS -o "''${RESPONSE_FILE}" -w '%{http_code}' \
-X POST "https://swiftshare.cloud/api/upload/sharex" \
-F "upload=@''${IMAGE_FILE}" \
-F "apiKey=''${SWIFTSHARE_API_KEY}")"
CURL_EXIT=$?
set -e
RESPONSE="$(cat "''${RESPONSE_FILE}")"
if [ "''${CURL_EXIT}" -ne 0 ]; then
${pkgs.libnotify}/bin/notify-send "SwiftShare upload failed" "Network or HTTP error (curl exit ''${CURL_EXIT})"
echo "SwiftShare upload failed (curl exit ''${CURL_EXIT})." >&2
echo "Response body:" >&2
echo "''${RESPONSE}" >&2
exit 1
fi
if ! echo "''${HTTP_STATUS}" | grep -qE '^2[0-9][0-9]$'; then
ERROR_MSG="$(${pkgs.jq}/bin/jq -r '.error // empty' <<< "''${RESPONSE}")"
if [ -z "''${ERROR_MSG}" ] || [ "''${ERROR_MSG}" = "null" ]; then
ERROR_MSG="Failed to upload file"
fi
${pkgs.libnotify}/bin/notify-send "SwiftShare upload failed (''${HTTP_STATUS})" "''${ERROR_MSG}"
echo "SwiftShare upload failed (HTTP ''${HTTP_STATUS}): ''${ERROR_MSG}" >&2
exit 1
fi
URL="$(${pkgs.jq}/bin/jq -r '.url // empty' <<< "''${RESPONSE}")"
THUMBNAIL="$(${pkgs.jq}/bin/jq -r '.thumbnail // empty' <<< "''${RESPONSE}")"
if [ -z "$URL" ] || [ "$URL" = "null" ]; then
${pkgs.libnotify}/bin/notify-send "SwiftShare upload failed" "Could not parse URL from response"
echo "Upload failed. Raw response:" >&2
echo "''${RESPONSE}" >&2
exit 1
fi
echo "$URL"
if [ -n "$THUMBNAIL" ] && [ "$THUMBNAIL" != "null" ]; then
echo "$THUMBNAIL"
fi
if [ "$COPY_URL" = "1" ]; then
${pkgs.wl-clipboard}/bin/wl-copy <<< "$URL"
fi
if [ -n "$IMAGE_FILE" ] && [ -f "$IMAGE_FILE" ]; then
${pkgs.libnotify}/bin/notify-send \
-a "SwiftShare" \
-i "$IMAGE_FILE" \
-h string:image-path:"$IMAGE_FILE" \
"SwiftShare upload" "Uploaded image: $URL"
else
${pkgs.libnotify}/bin/notify-send "SwiftShare upload" "Uploaded image: $URL"
fi
'')
(writeShellScriptBin "swiftshare-screenshot" ''
#!${pkgs.bash}/bin/bash
set -euo pipefail
LOG_DIR="$HOME/.local/state/swiftshare"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/screenshot.log"
APP_CLASS="$(${pkgs.hyprland}/bin/hyprctl activewindow -j 2>/dev/null | ${pkgs.jq}/bin/jq -r '.class // .initialClass // empty' 2>/dev/null || true)"
APP_CLASS="''${APP_CLASS%% *}"
APP_CLASS="''${APP_CLASS//[^A-Za-z0-9]/}"
APP_CLASS="''${APP_CLASS,,}"
if [ -z "$APP_CLASS" ]; then
APP_CLASS="screenshot"
fi
GEOM="$(${pkgs.slurp}/bin/slurp)"
SLURP_EXIT=$?
if [ "$SLURP_EXIT" -ne 0 ] || [ -z "$GEOM" ]; then
${pkgs.libnotify}/bin/notify-send "SwiftShare" "Capture canceled"
{
echo "==== $(date) ==== capture canceled (slurp exit $SLURP_EXIT, geom='$GEOM')"
} >>"$LOG_FILE" 2>&1
exit 0
fi
{
echo "==== $(date) ===="
echo "Geometry: $GEOM"
${pkgs.grim}/bin/grim -g "$GEOM" - | ${pkgs.swappy}/bin/swappy -f - -o - | swiftshare-upload --copy-url --app-name "$APP_CLASS"
} >>"$LOG_FILE" 2>&1
'')
curl
jq
swiftshareUpload
swiftshareScreenshot
];
}
(lib.mkIf hyprlandHmEnabled {
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
set -euo pipefail
LOG_DIR="$HOME/.local/state/swiftshare"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/screenshot.log"
APP_CLASS="$(hyprctl activewindow -j 2>/dev/null | jq -r '.class // .initialClass // empty' 2>/dev/null || true)"
APP_CLASS="${APP_CLASS%% *}"
APP_CLASS="${APP_CLASS//[^A-Za-z0-9]/}"
APP_CLASS="${APP_CLASS,,}"
if [ -z "$APP_CLASS" ]; then
APP_CLASS="screenshot"
fi
GEOM="$(slurp)"
SLURP_EXIT=$?
if [ "$SLURP_EXIT" -ne 0 ] || [ -z "$GEOM" ]; then
notify-send "SwiftShare" "Capture canceled"
{
echo "==== $(date) ==== capture canceled (slurp exit $SLURP_EXIT, geom='$GEOM')"
} >>"$LOG_FILE" 2>&1
exit 0
fi
{
echo "==== $(date) ===="
echo "Geometry: $GEOM"
grim -g "$GEOM" - | swappy -f - -o - | swiftshare-upload --copy-url --app-name "$APP_CLASS"
} >>"$LOG_FILE" 2>&1
@@ -0,0 +1,121 @@
#!/usr/bin/env bash
# API key path substituted at build time (@API_KEY_FILE@).
set -euo pipefail
COPY_URL=0
if [ "$#" -ge 1 ] && [ "$1" = "--copy-url" ]; then
COPY_URL=1
shift
fi
APP_NAME=""
if [ "$#" -ge 2 ] && [ "$1" = "--app-name" ]; then
APP_NAME="$2"
shift 2
fi
API_KEY_FILE=@API_KEY_FILE@
if [ -r "$API_KEY_FILE" ]; then
SWIFTSHARE_API_KEY="$(tr -d '\n' < "$API_KEY_FILE")"
fi
if [ -z "${SWIFTSHARE_API_KEY:-}" ]; then
notify-send "SwiftShare upload" "SwiftShare API key missing (expected readable: $API_KEY_FILE)"
echo "Error: SwiftShare API key missing (expected readable: $API_KEY_FILE)" >&2
exit 1
fi
IMAGE_FILE=""
RESPONSE_FILE=""
cleanup() {
if [ -n "$RESPONSE_FILE" ] && [ -f "$RESPONSE_FILE" ]; then
rm -f "$RESPONSE_FILE"
fi
}
trap cleanup EXIT
if [ "$#" -ge 1 ] && [ "$1" != "-" ]; then
IMAGE_FILE="$1"
if [ "${IMAGE_FILE#/}" = "${IMAGE_FILE}" ]; then
IMAGE_FILE="$(readlink -f "${IMAGE_FILE}")"
fi
if [ ! -f "$IMAGE_FILE" ]; then
echo "Error: file not found: $IMAGE_FILE" >&2
exit 1
fi
else
APP_NAME="${APP_NAME:-screenshot}"
APP_NAME="${APP_NAME%% *}"
APP_NAME="${APP_NAME//[^A-Za-z0-9]/}"
APP_NAME="${APP_NAME,,}"
if [ -z "$APP_NAME" ]; then
APP_NAME="screenshot"
fi
IMAGE_FILE="$(mktemp --suffix=.png "${TMPDIR:-/tmp}/${APP_NAME}_XXXXXX")"
cat > "$IMAGE_FILE"
fi
if [ ! -s "$IMAGE_FILE" ]; then
notify-send "SwiftShare" "Empty capture (maybe canceled) not uploading"
echo "Empty image file, not uploading." >&2
exit 0
fi
RESPONSE_FILE="$(mktemp)"
set +e
HTTP_STATUS="$(curl -sS -o "${RESPONSE_FILE}" -w '%{http_code}' \
-X POST "https://swiftshare.cloud/api/upload/sharex" \
-F "upload=${IMAGE_FILE}" \
-F "apiKey=${SWIFTSHARE_API_KEY}")"
CURL_EXIT=$?
set -e
RESPONSE="$(cat "${RESPONSE_FILE}")"
if [ "${CURL_EXIT}" -ne 0 ]; then
notify-send "SwiftShare upload failed" "Network or HTTP error (curl exit ${CURL_EXIT})"
echo "SwiftShare upload failed (curl exit ${CURL_EXIT})." >&2
echo "Response body:" >&2
echo "${RESPONSE}" >&2
exit 1
fi
if ! echo "${HTTP_STATUS}" | grep -qE '^2[0-9][0-9]$'; then
ERROR_MSG="$(jq -r '.error // empty' <<< "${RESPONSE}")"
if [ -z "${ERROR_MSG}" ] || [ "${ERROR_MSG}" = "null" ]; then
ERROR_MSG="Failed to upload file"
fi
notify-send "SwiftShare upload failed (${HTTP_STATUS})" "${ERROR_MSG}"
echo "SwiftShare upload failed (HTTP ${HTTP_STATUS}): ${ERROR_MSG}" >&2
exit 1
fi
URL="$(jq -r '.url // empty' <<< "${RESPONSE}")"
THUMBNAIL="$(jq -r '.thumbnail // empty' <<< "${RESPONSE}")"
if [ -z "$URL" ] || [ "$URL" = "null" ]; then
notify-send "SwiftShare upload failed" "Could not parse URL from response"
echo "Upload failed. Raw response:" >&2
echo "${RESPONSE}" >&2
exit 1
fi
echo "$URL"
if [ -n "$THUMBNAIL" ] && [ "$THUMBNAIL" != "null" ]; then
echo "$THUMBNAIL"
fi
if [ "$COPY_URL" = "1" ]; then
wl-copy <<< "$URL"
fi
if [ -n "$IMAGE_FILE" ] && [ -f "$IMAGE_FILE" ]; then
notify-send \
-a "SwiftShare" \
-i "$IMAGE_FILE" \
-h string:image-path:"$IMAGE_FILE" \
"SwiftShare upload" "Uploaded image: $URL"
else
notify-send "SwiftShare upload" "Uploaded image: $URL"
fi
+3 -1
View File
@@ -33,7 +33,9 @@
in
{
options.chiasson.home.editors.cursor = {
enable = lib.mkEnableOption "Cursor editor from the `cursor` flake input.";
enable = lib.mkEnableOption "Cursor editor from the `cursor` flake input." // {
default = true;
};
setAsDefaultEditor = lib.mkOption {
type = lib.types.bool;
default = true;
+6 -15
View File
@@ -1,17 +1,8 @@
{ ... }: {
flake.homeManagerModules.wisdomEditorsKate =
{ config, lib, pkgs, ... }:
let
root = config.chiasson.home;
cfg = config.chiasson.home.editors.kate;
in
{
options.chiasson.home.editors.kate.enable = lib.mkEnableOption "Kate.";
config = lib.mkIf (root.enable && cfg.enable) {
home.packages = lib.optional (
lib.meta.availableOn pkgs.stdenv.hostPlatform pkgs.kdePackages.kate
) pkgs.kdePackages.kate;
};
{ self, ... }: {
flake.homeManagerModules.wisdomEditorsKate = self.lib.wisdomSlice {
path = "editors.kate";
default = false;
description = "Kate.";
packages = pkgs: [ pkgs.kdePackages.kate ];
};
}
+6 -17
View File
@@ -1,19 +1,8 @@
{ ... }: {
flake.homeManagerModules.wisdomEditorsObsidian =
{ config, lib, pkgs, ... }:
let
root = config.chiasson.home;
cfg = config.chiasson.home.editors.obsidian;
in
{
options.chiasson.home.editors.obsidian.enable = lib.mkEnableOption ''
Obsidian (unfree); skipped if unavailable here.
'';
config = lib.mkIf (root.enable && cfg.enable) {
home.packages = lib.optional (
lib.meta.availableOn pkgs.stdenv.hostPlatform pkgs.obsidian
) pkgs.obsidian;
};
{ self, ... }: {
flake.homeManagerModules.wisdomEditorsObsidian = self.lib.wisdomSlice {
path = "editors.obsidian";
default = true;
description = "Obsidian (unfree); skipped if unavailable here.";
packages = pkgs: [ pkgs.obsidian ];
};
}
+100
View File
@@ -0,0 +1,100 @@
# Visual Studio Code (Microsoft build, unfree). Default extensions + settings shared by all
# hosts; per-host extras (peacock, comment-anchors, project-manager) go via
# `chiasson.home.editors.vscode.extensions`.
{ self, ... }: {
flake.homeManagerModules.wisdomEditorsVscode =
{ config, lib, pkgs, ... }:
let
root = config.chiasson.home;
cfg = config.chiasson.home.editors.vscode;
defaultExtensions = with pkgs.vscode-extensions; [
jnoortheen.nix-ide
yzhang.markdown-all-in-one
esbenp.prettier-vscode
vscjava.vscode-java-pack
redhat.java
];
# Wrap VS Code so a JDK is on PATH — the Red Hat Java language server (and
# other extension-bundled JVMs) need `java` to run, independent of the
# per-project `java.configuration.runtimes` setting.
vscodeWithJdk = pkgs.symlinkJoin {
name = "vscode-with-jdk";
paths = [ pkgs.vscode ];
nativeBuildInputs = [ pkgs.makeWrapper ];
postBuild = ''
wrapProgram "$out/bin/code" \
--prefix PATH : "${pkgs.jdk25}/bin"
'';
};
coreSettings = {
"nix.enableLanguageServer" = true;
"nix.serverPath" = "nixd";
"nix.serverSettings" = {
nixd = {
formatting = {
command = "nixfmt";
};
};
};
"diffEditor.ignoreTrimWhitespace" = false;
"chat.disableAIFeatures" = true;
"java.configuration.runtimes" = [
{
name = "JavaSE-21";
path = "${pkgs.jdk21}/lib/openjdk";
}
{
name = "JavaSE-25";
path = "${pkgs.jdk25}/lib/openjdk";
default = true;
}
];
};
# Seed a *writable* settings.json (not a store symlink) so extensions like
# kilo-code can edit it at runtime. `force = false` in the config means HM
# only writes it when absent, preserving kilo's later edits across `switch`.
settingsJson = pkgs.writeText "vscode-user-settings" (
builtins.toJSON coreSettings
);
in
{
options.chiasson.home.editors.vscode = {
enable = lib.mkEnableOption ''
Visual Studio Code (Microsoft build; needs `nixpkgs.config.allowUnfree`). Core
extensions (nix-ide, markdown-all-in-one, prettier) + nixd/nixfmt settings are included.
'' // {
default = false;
};
extensions = lib.mkOption {
type = lib.types.listOf lib.types.package;
default = [ ];
description = ''
Extra extensions not wanted on every host (e.g. peacock, comment-anchors,
project-manager). Per-host via `chiasson.users.extraModules`.
'';
};
};
config = lib.mkIf (root.enable && cfg.enable) {
home.packages = [ ];
# Writable, seed-once user settings (kilo-code can append to it).
home.file.".config/Code/User/settings.json" = {
source = settingsJson;
force = false;
};
programs.vscode = {
enable = true;
package = vscodeWithJdk;
extensions = defaultExtensions ++ cfg.extensions;
};
};
};
}
-76
View File
@@ -1,76 +0,0 @@
{ ... }: {
flake.homeManagerModules.wisdomFilebrowsersDolphin =
{ config, lib, pkgs, ... }:
let
root = config.chiasson.home;
cfg = root.filebrowsers.dolphin;
in
{
options.chiasson.home.filebrowsers.dolphin.enable = lib.mkEnableOption "Dolphin + declarative dolphinrc.";
config = lib.mkIf (root.enable && cfg.enable) {
home.packages = [ pkgs.kdePackages.dolphin ];
xdg.configFile."dolphinrc".text = ''
[ContentDisplay]
UsePermissionsFormat=CombinedFormat
[General]
StartupPath=~
DoubleClickViewAction=show_hidden_files
ShowFullPath=true
ShowFullPathInTitlebar=true
ShowStatusBar=FullWidth
UseTabForSwitchingSplitView=true
Version=202
ViewPropsTimestamp=2025,11,17,23,21,57.762
[KFileDialog Settings]
Places Icons Auto-resize=false
Places Icons Static Size=22
[MainWindow]
MenuBar=Disabled
ToolBarsMovable=Disabled
[PreviewSettings]
Plugins=appimagethumbnail,audiothumbnail,blenderthumbnail,comicbookthumbnail,cursorthumbnail,djvuthumbnail,ebookthumbnail,exrthumbnail,directorythumbnail,fontthumbnail,imagethumbnail,jpegthumbnail,kraorathumbnail,windowsexethumbnail,windowsimagethumbnail,mobithumbnail,opendocumentthumbnail,gsthumbnail,rawthumbnail,svgthumbnail,ffmpegthumbs
[IconsMode]
IconSize=48
PreviewSize=48
TextLines=2
UseThumbnails=true
[DetailsMode]
FontWeight=50
HighlightEntireRow=true
[ViewProperties]
Mode=1
ColumnWidths=50,50,50,50,50,50,50,50,50,50
SortColumn=0
SortOrder=0
SortFoldersFirst=true
SortHiddenLast=false
SortCaseSensitively=false
ShowPreviews=true
ShowInGroups=false
ShowFoldersFirst=true
ShowHiddenFilesLast=false
NaturalSorting=true
[ContextMenu]
ShowCopyToMenu=true
ShowMoveToMenu=true
[Search]
Location=Everywhere
[SettingsWindow]
SidebarWidth=180
SplitterState=AAAA/wAAAAD9AAAAAAAAAAAAAAABAAAAAQAAAAEAAAAAQAAAAEAAAAA=
'';
};
};
}

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