Compare commits

...

59 Commits

Author SHA1 Message Date
411e521921 Add sendspin-cli package with Python dependencies
Integrate sendspin-cli as a custom package using python3.withPackages.
Packages aiosendspin from GitHub since it's only available in
nixpkgs-unstable. Includes all required dependencies: aiohttp, av,
numpy, qrcode, readchar, rich, sounddevice, and native libraries
(portaudio, ffmpeg).

Adds uv2nix flake inputs for future migration when sendspin-cli
adds a uv.lock file.
2025-12-29 23:46:08 -08:00
05fed3ede1 Add virtual 4.1 surround sound configuration for zix790prors
Create a PipeWire virtual surround sink that routes audio to multiple
physical outputs:
- FL/FR channels → AmazonBasics USB speaker
- RL/RR channels → Fosi BT20A PRO Bluetooth speaker
- LFE channel → AmazonBasics (duplicated to both channels)

Uses loopback modules with systemd services to maintain correct routing,
as PipeWire's target.object parameter doesn't auto-connect properly.
A timer checks every 10 seconds and fixes incorrect connections.

Configuration is machine-specific and isolated in virtual-surround.nix.
2025-12-29 12:01:29 -08:00
0a9de8d159 Fix rbw-agent launching from systemd services
The rbw unlock systemd services were failing to launch the rbw-agent
daemon due to two issues:

1. Missing RBW_AGENT environment variable - rbw looks for this variable
   to locate the agent binary, falling back to PATH lookup. Systemd
   user services have minimal environments without the necessary PATH.

2. Default KillMode=control-group - when the oneshot service completed,
   systemd was killing all processes in the cgroup including the
   daemonized agent.

Fixed by:
- Setting RBW_AGENT environment variable to explicit agent binary path
- Using KillMode=process to only kill the main process, allowing the
  spawned agent daemon to persist after service completion
2025-12-29 10:21:58 -08:00
055d6ab421 Add systemd services to unlock rbw vault on login and resume
Adds two systemd user services to automatically unlock the rbw vault:
- rbw-unlock-on-login: Runs at graphical session start
- rbw-unlock-on-resume: Runs after resuming from suspend

This solves the issue of mbsync prompting for password every 5 minutes.
Once unlocked, the vault stays unlocked as long as mbsync syncs every
5 minutes (which resets the 1-hour lock timeout). Only prompts at login
or after long suspend periods.
2025-12-26 13:20:18 -08:00
d5c6342b84 [home-desktop] Add email role 2025-12-25 09:54:20 -08:00
e04dacdf65 Add email support with notmuch, mbsync, and msmtp
Set up complete email workflow in Emacs using:
- notmuch for email indexing and UI
- mbsync for IMAP synchronization from proton.johnogle.info
- msmtp for SMTP sending via port 25 with PLAIN auth
- systemd timer for automatic sync every 5 minutes

Configuration includes:
- New email role at home/roles/email with all mail tools
- Doom Emacs notmuch module enabled with saved searches
- Secure credential retrieval via rbw from Bitwarden
- Fixed systemd service PATH to access rbw-agent
- TLS/STARTTLS for secure connections

Email role enabled on nix-book (laptop-compact profile).
2025-12-22 23:00:23 -08:00
7d74917bdc [doom] update 2025-12-22 14:36:14 -08:00
5a4ab71849 Add script to update doomemacs to latest commit
Creates update-doomemacs.sh script that:
- Fetches latest commit SHA from doomemacs/doomemacs repo
- Automatically detects the default branch
- Updates both rev and sha256 in home/roles/emacs/default.nix
- Works from anywhere in the repo using git rev-parse

Also adds a flake app so it can be run with:
  nix run .#update-doomemacs
2025-12-22 14:34:56 -08:00
bcebf9b376 claude-code: Update to version 2.0.75 2025-12-22 14:27:10 -08:00
0f76939983 Increase jellyfinScaleFactor to 2.5 for boxy 2025-12-22 12:30:36 -08:00
a1da2f5cc1 Fix jellyfinScaleFactor for .desktop entry launches 2025-12-22 12:30:35 -08:00
175da48170 Fix jellyfinScaleFactor implementation in kodi role
The original implementation had several issues that prevented it from
building:
- Used buildInputs instead of nativeBuildInputs for makeWrapper
- Referenced wrong executable name (jellyfinmediaplayer vs jellyfin-desktop)
- Used wrapProgram which doesn't work with symlinks from symlinkJoin

Fixed by using makeWrapper directly with the correct executable path
after removing the symlink.

Also enabled jellyfinScaleFactor = 1.5 on boxy for UI scaling.
2025-12-22 12:17:54 -08:00
ac956ef48c [flake] update 2025-12-22 12:03:31 -08:00
0c1190f39c Add jellyfinScaleFactor option to kodi role for UI scaling 2025-12-22 12:02:15 -08:00
00f05d1bb2 [i3+sway] Setup brightness controll for ddc monitors 2025-12-20 11:52:27 -08:00
4e6c6ab81d Make nix gc options overridable with mkDefault 2025-12-08 14:34:53 -08:00
04e1a8563c Fix sketchybar memory indicator to show actual memory pressure
The previous implementation included inactive/cached pages and used
"Pages stored in compressor" (uncompressed size), resulting in inflated
percentages (~88%) that didn't reflect actual memory pressure.

Now uses:
- Anonymous pages (matches Activity Monitor's "App Memory")
- Pages wired down (system memory)
- Pages occupied by compressor (actual RAM used, not uncompressed size)

Also switches to awk for arithmetic to avoid bash integer overflow on
systems with >4GB RAM.
2025-12-08 14:34:22 -08:00
7278dc8306 Fix deprecated NixOS options to resolve flake check warnings
- Update system attribute to stdenv.hostPlatform.system in overlays
- Migrate git config to new settings structure (user.name, user.email)
- Move ssh.addKeysToAgent to matchBlocks configuration
- Disable ssh default config to prevent future deprecation warnings
2025-12-07 22:17:54 -08:00
066eea2999 [kodi] Fix lib.warn syntax for insecure package warning
Fix syntax error by wrapping permittedInsecurePackages list with
lib.warn function call. The warning now properly displays during
evaluation about qtwebengine-5.15.19 being required for
jellyfin-media-player until it migrates to qt6.
2025-12-07 21:11:25 -08:00
80633142fb [kodi] Fix jellyfin 2025-12-07 20:55:34 -08:00
3029e3d9a8 [kodi] jellyfin > jellyfin-media-player 2025-12-07 20:53:59 -08:00
3483e26bce Add 3D printing home role with orca-slicer and openscad
Create a new home-manager role for 3D printing applications including
orca-slicer for slicing and openscad-unstable for 3D modeling. Enable
the role in the desktop configuration for full-featured experience.
2025-12-06 19:22:37 -08:00
b3add6ddf8 Add automatic garbage collection for home-manager user profiles
Configure nix.gc to automatically clean up old home-manager generations
weekly, which complements the existing system-level gc. Uses
--delete-older-than 10d on Linux to maintain a rollback window, but
overrides to -d on Darwin to work around a launchd bug where multi-part
options aren't properly split into separate arguments.
2025-12-06 13:58:26 -08:00
89994e3fc8 Update SketchyBar styling to match Waybar appearance
- Change font from SF Mono to Fira Code for consistency with Waybar
- Reduce bar height from 32px to 30px
- Switch to solid dark gray background (#333333) instead of semi-transparent black
- Remove rounded corners on items (corner_radius 0 instead of 5)
- Make item backgrounds full height (30px) to match bar height
- Remove all separator items between modules for cleaner appearance
- Add topmost and sticky properties to prevent window shadows from darkening bar
- Adjust aerospace bottom gap from 40px to 38px to account for new bar height
- Set all workspace text to white with bold font for active workspace
2025-12-05 14:58:11 -08:00
0e9671a45f Fix SketchyBar workspace indicators not appearing after 25.11 upgrade
Add explicit `drawing=on` to all workspace item states (initial creation,
focused, non-empty, and empty) to prevent items from getting stuck with
`drawing=off`. With `updates=when_shown`, items with `drawing=off` never
run their update scripts, causing workspaces that start empty to never
appear even when focused or given windows.

Changes:
- Add drawing=on to initial workspace creation (line 393)
- Add drawing=on to focused workspace state (line 568)
- Add drawing=on to empty workspace state (line 582)
- Add drawing=on to non-empty workspace state (line 591)
- Update comment for empty workspace to explain drawing=on usage

This ensures workspace indicators automatically appear within 2 seconds
when focused or given windows, without needing manual `sketchybar --update`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 14:44:23 -08:00
f4078970b2 Merge branch '25.11'
* 25.11:
  Simplify aerospace launchd config and remove menu bar hiding
  Remove mbedtls_2 workaround for dolphin packages
  Re-enable packages after NixOS 25.11 upgrade
  Add --unsupported-gpu flag to Sway for zix790prors compatibility
  Fix deprecated package names for NixOS 25.11
  Upgrade NixOS to 25.11 and fix Jovian module organization
2025-12-05 14:20:49 -08:00
0ae4d84ca2 Simplify aerospace launchd config and remove menu bar hiding
- Remove menu bar hiding code (no longer needed for SketchyBar)
- Use built-in programs.aerospace.launchd.enable instead of custom agent
- Remove redundant launchd.agents.aerospace configuration block
2025-12-05 14:13:05 -08:00
7c877fde84 Remove mbedtls_2 workaround for dolphin packages
Dolphin-emu and dolphin-emu-primehack now use maintained mbedtls version
3.6.5 instead of unmaintained mbedtls_2. No longer need to permit insecure
packages.

Verified both packages build successfully without the workaround.
2025-12-04 21:22:49 -08:00
d53286e04c Re-enable packages after NixOS 25.11 upgrade
Re-enable dolphin-emu-primehack now that binary build is fixed in 25.11.
Re-enable fluffychat as security issues have been resolved in nixpkgs 25.11.

Both packages verified to build and function correctly.
2025-12-04 21:22:41 -08:00
bc42c4dc77 Fix SketchyBar workspace indicators not reappearing
The aerospace workspace plugin had two issues preventing workspace
indicators from properly showing/hiding:

1. The script expected workspace number as $1 but update_freq routine
   calls only provide $NAME environment variable. Now extracts from
   either source.

2. Using drawing=off to hide workspaces was unreliable - items wouldn't
   consistently reappear. Now uses width=0 with cleared icon/label
   content to collapse items instead.

Workspaces now properly appear within 2 seconds when windows are
created or moved, without requiring manual sketchybar --update.
2025-12-04 16:31:44 -08:00
445b0cd558 Add --unsupported-gpu flag to Sway for zix790prors compatibility 2025-12-04 16:30:55 -08:00
6d9686f14b Fix deprecated package names for NixOS 25.11
- Remove amdvlk from boxy config (replaced by RADV, enabled by default)
- Rename vaapiVdpau to libva-vdpau-driver in wixos config
- Fixes nix flake check errors
2025-12-04 16:21:43 -08:00
4164832eea Upgrade NixOS to 25.11 and fix Jovian module organization
- Update flake inputs from 25.05 to 25.11 (nixpkgs, home-manager, nix-darwin)
- Remove Jovian compatibility shim that's no longer needed
- Move SteamOS configuration to only import in nix-deck machine
- Fixes jovian module not found error during nixos-rebuild
2025-12-04 16:12:58 -08:00
585f9ef5c7 Remove macOS menu bar hiding from aerospace SketchyBar config 2025-12-04 10:51:12 -08:00
ade60ba5ec Add macOS Ctrl keyboard shortcuts with terminal-aware overrides
Implement Linux-style Ctrl shortcuts (Ctrl+C/V/X/Z for clipboard, Ctrl+N/T/W
for navigation, etc.) while preserving terminal behavior where Ctrl+C sends
SIGINT. Uses per-app NSUserKeyEquivalents to remap Ghostty back to Cmd for
clipboard operations.

Also consolidate aerospace configuration by moving spans-displays preference
from system-level module to home-manager role, allowing full aerospace
configuration to live in home-manager for better modularity.
2025-12-03 10:30:39 -08:00
48fb7cdada Add SketchyBar integration to aerospace with bottom bar positioning
Integrate SketchyBar status bar with aerospace window manager, providing
a native macOS status bar replacement with workspace indicators and system
monitoring. Key features:

- Add sketchybar.enable option to aerospace module
- Install sketchybar package and fonts conditionally
- Create main sketchybarrc with i3/sway color theme
- Position bar at bottom with 40px bottom gap
- Implement workspace indicators with dynamic visibility:
  - Hide empty workspaces
  - Show focused workspace with blue highlight
  - Show non-empty workspaces with inactive styling
  - Use centered icons with fixed 32px width
- Add system monitoring plugins: CPU, memory, disk, battery, volume, clock
- Integrate menu bar extras (Bluetooth, WiFi) as aliases
- Configure aerospace to trigger workspace change events
- Hide native macOS menu bar when SketchyBar enabled
- Set up launchd agent for auto-start
- Use SF Mono Regular 13.0 font matching waybar aesthetic
2025-12-03 09:51:22 -08:00
2d8cfe75a0 Align aerospace keybindings with i3+sway and add resize mode
- Enable programs.aerospace to ensure config generation
- Disable normalizations for pure i3-style tree management
- Update layout keybindings to match i3+sway:
  - cmd-w: accordion-horizontal (tabbed)
  - cmd-s: accordion-vertical (stacking)
  - cmd-e: tiles layout with orientation toggle
- Replace direct resize bindings with cmd-r resize mode
- Add resize mode with hjkl directional controls
2025-12-02 16:07:12 -08:00
385fd798de Fix aerospace namespace conflict and claude-code override
Rename custom aerospace module from services.aerospace to roles.aerospace
to avoid conflicting with nix-darwin's built-in aerospace service support.

Move claude-code package override to flake-level overlay to ensure the
GCS-distributed version is used instead of the npm registry version in
unstable. This is necessary for corporate environments where npm registry
access may be blocked.
2025-12-02 15:16:55 -08:00
fe6558e0c1 Refactor: Extract platform-specific roles to base-linux and base-darwin
Create base-linux and base-darwin modules to cleanly separate platform-
specific role imports from shared roles. This prevents importing modules
that require platform-specific home-manager modules (like plasma-manager
on NixOS) in environments where they don't exist (like nix-darwin).

- base-linux includes: plasma-manager, i3+sway
- base-darwin includes: aerospace
- roles/default.nix now only contains truly cross-platform roles

This architecture makes it immediately clear which roles are shared
versus platform-specific and makes it easy to add new platform-specific
roles in the future.
2025-12-02 15:16:42 -08:00
b9c48f9dd1 Complete migration of home-manager modules to roles
Migrate all remaining home-manager modules from home/modules/ to home/roles/
to establish a unified role-based configuration pattern. This completes the
migration started in Phase 1.

Changes:
- Phase 1-3: Migrated tmux, plasma-manager, kubectl, and emacs to roles
- Phase 4: Migrated aerospace with custom options under home.roles.aerospace.*
- Phase 5: Migrated i3+sway with shared config and override options
- Phase 6: Removed empty home/modules/ directory

All home configs now import only ./roles with role-based enable options.
Updated flake.nix machine-specific overrides to use new namespaces.

Verified with nix flake check - all configurations build successfully.
2025-12-02 14:34:11 -08:00
34351403d1 Extract aerospace configuration into reusable modules
Create both home-manager and nix-darwin modules for aerospace window
manager configuration, removing 110+ lines of duplicated config from
machine-specific files.

Changes:
- Add home/modules/aerospace module with configurable leader key
- Add modules/aerospace.nix for system-level macOS settings
- Include autoraise configuration in home module
- Update home-darwin-work.nix to use new modules
- Update johno-macbookpro configuration to use system module
- Remove inline aerospace/autoraise config and launchd agents
2025-12-01 19:02:00 -08:00
12820ce9ff [aerospace] Disable macOS Spaces mutli-display
This option is known to cause all sorts of issues with aerospace
apparently
2025-12-01 18:18:56 -08:00
0f5eb2e572 [i3+sway] Add focus child binding 2025-12-01 17:39:33 -08:00
f356c91fdb [gaming] Remove heavy build packages
dolphin-emu-primehack will be available as a pre-build in 25.11

retroarch-full -- in general I'm wondering of using steam>retroarch is a
better experience
2025-11-29 11:30:10 -08:00
6b42612135 [gaming] Use recommended steam setup
Updated to use programs.steam recommended from the NixOS wiki
2025-11-29 11:29:43 -08:00
50a8c44d10 [launchers] Add launcher wrappers for compact env
Adds a module to launch specific nixpkgs dynamically, such that they
won't be always included in the nix store
2025-11-29 11:28:04 -08:00
7011fb27a5 Update workspace button background color in waybar 2025-11-26 17:20:55 -08:00
1ff8b81f44 Add environment-specific claude-code package distribution system
Development environments now use standard nixpkgs claude-code by default.
Work environments override with custom binary distribution to bypass
corporate npm registry restrictions via Google Cloud Storage.
2025-11-26 17:20:31 -08:00
55f13dfb08 Rename claude-cli package to claude-code for consistency with nixpkgs 2025-11-26 17:20:17 -08:00
63bf19b85f [claude-code] update 2025-11-25 13:39:02 -08:00
1f9e9138ab Add automated update script for claude-cli package
Create update.sh script that fetches the latest claude-code version
and SHA256 hashes from Homebrew cask and automatically updates the
Nix package definition.

Features:
- Fetches version and all 4 platform SHA256 hashes from Homebrew
- Updates default.nix in-place using awk for reliable parsing
- Validates extracted data before updating
- Supports --dry-run mode to preview changes
- Supports --help flag for usage information
- Idempotent (safe to run multiple times)

Update README to document the automated update method as the
recommended approach, with manual update as a fallback option.
2025-11-25 13:39:02 -08:00
e218822566 Add configurable Claude Code model selection
Add allowArbitraryClaudeCodeModelSelection option to development role
to control whether model specifications are preserved in humanlayer
commands and agents. Defaults to false (strip models) for backward
compatibility. Enable on home-darwin-work to preserve model specs
for opus/sonnet selection.
2025-11-25 13:39:02 -08:00
e88f3580e9 Add thoughts directory to gitignore 2025-11-25 13:39:01 -08:00
5451e75480 Add custom claude-cli package to bypass npm registry restrictions
Create custom Nix package for Claude Code CLI that fetches directly from
Anthropic's Google Cloud Storage distribution instead of npm registry,
working around Block's Cloudflare Teams dependency confusion protection.

- Add claude-cli package with platform-specific binaries
- Include comprehensive README with update instructions
- Enable development role on work machine
- Switch from unstable.claude-code to custom.claude-cli
- Add google-cloud-sdk to work machine packages
2025-11-25 13:38:53 -08:00
fc9474a7c9 [home] Create explicit kubectl role
This allows work machines to enable development, while not including
non-work-related kubectl management stuffs.
2025-11-25 13:37:40 -08:00
20daebbd61 Add ccr and unstable codex 2025-11-24 22:16:12 -08:00
3be23304c4 [development] Fix humanlayer plugins
Removes model specification to better support Claude Code Pro plan.
Because of this, I also re-removed this role from home-darwin-work as we
can take advantage of more powerful models in that context. We will just
need to install the plugins external from nix. Eventually I can turn
this into a config option of course.

Also made sure we are installing the agents from the plugin in addition
to the commands.
2025-11-22 09:47:55 -08:00
9059a739a0 [development] Add claude code plugins 2025-11-22 09:32:54 -08:00
977125645b Remove transition documentation 2025-11-19 19:43:59 -08:00
49 changed files with 3011 additions and 862 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
result
thoughts

View File

@@ -1,280 +0,0 @@
# Steam Deck (nix-deck) Jovian-NixOS Setup Guide
This document describes the setup for installing and maintaining NixOS with Jovian-NixOS on a Steam Deck.
## Overview
The `nix-deck` configuration provides:
- **Jovian-NixOS integration** for Steam Deck hardware support
- **Remote building** using `zix790prors` as the build host
- **SteamOS role** for easy Steam Deck UI configuration
- **Compatibility shim** for using Jovian on NixOS 25.05 stable
## Architecture
### Remote Building
The setup uses distributed builds to avoid slow compilation on the Steam Deck:
- **Build Host**: `zix790prors` (powerful desktop)
- Runs as a dedicated `nix-builder` user (not root)
- Accepts SSH connections from client machines
- Performs all heavy compilation work
- **Build Clients**: `nix-book` and `nix-deck`
- Automatically offload builds to `zix790prors`
- Fall back to local building if remote builder is unavailable
- Steam Deck heavily prefers remote (speedFactor=4)
### Jovian-NixOS Integration
- **Jovian module**: Provides Steam Deck hardware support, drivers, and Steam UI
- **Compatibility layer**: `roles/jovian-compat.nix` provides `services.logind.settings` for NixOS 25.05
- **IMPORTANT**: Remove this when upgrading to NixOS 25.11+
- An assertion will fail the build if used on 25.11+
- **SteamOS role**: `roles.desktop.steamos` abstracts Jovian configuration
```nix
roles.desktop.steamos = {
enable = true;
autoStart = false; # Set to true to boot directly to Steam UI
desktopSession = "plasmawayland";
};
```
## Initial Installation
### Prerequisites
1. Steam Deck in recovery mode or booted to a live Linux environment
2. SSH access enabled on the Steam Deck
3. SSH key set up for passwordless authentication
### Option 1: Using nixos-anywhere (Initial Install Only)
```bash
# From your main machine
nix run github:nix-community/nixos-anywhere -- \
--flake .#nix-deck \
root@<steam-deck-ip>
```
**Note**: This is only for the initial install. For updates, see below.
### Option 2: Manual Installation
1. Boot Steam Deck from NixOS installer USB
2. Partition and format the disk
3. Mount filesystems
4. Clone this repository
5. Generate hardware config:
```bash
nixos-generate-config --show-hardware-config > /tmp/hw.nix
```
6. Copy the hardware config content to `machines/nix-deck/hardware-configuration.nix`
7. Keep the `jovian.devices.steamdeck` settings in the file
8. Install:
```bash
nixos-install --flake .#nix-deck
```
## Updates and Rebuilds
### Method 1: Remote Build and Deploy (Recommended)
Build on your main machine, deploy to Steam Deck:
```bash
# From nix-book or zix790prors
nixos-rebuild switch \
--flake .#nix-deck \
--target-host root@nix-deck \
--build-host localhost
```
### Method 2: On-Device Rebuild (Uses Remote Builder)
The Steam Deck is configured to automatically use `zix790prors` as a remote builder:
```bash
# SSH into the Steam Deck
ssh root@nix-deck
# This will automatically build on zix790prors
nixos-rebuild switch --flake /path/to/nixos-configs#nix-deck
```
The build will automatically happen on `zix790prors` and be deployed locally.
## Remote Builder Setup
### On the Build Host (zix790prors)
The configuration creates a `nix-builder` user that client machines connect to:
```nix
roles.remote-build.enableBuilder = true;
```
### On Client Machines (nix-book, nix-deck)
Configure the remote builder:
```nix
roles.remote-build.builders = [{
hostName = "zix790prors";
maxJobs = 16;
speedFactor = 4; # Higher = prefer remote more
}];
```
### SSH Key Setup
1. Generate SSH key on the builder host for the `nix-builder` user:
```bash
sudo -u nix-builder ssh-keygen -t ed25519 -f /var/lib/nix-builder/.ssh/id_ed25519
```
2. Copy the public key to client machines:
```bash
# Add to /var/lib/nix-builder/.ssh/authorized_keys on zix790prors
```
3. On client machines, ensure you can connect:
```bash
ssh nix-builder@zix790prors
```
## Configuration Files
### Key Files Created/Modified
- `flake.nix` - Added Jovian input and nix-deck configuration
- `roles/jovian-compat.nix` - Compatibility shim (remove in 25.11+)
- `roles/desktop/steamos.nix` - SteamOS/Jovian role abstraction
- `roles/remote-build/default.nix` - Remote builder role
- `machines/nix-deck/configuration.nix` - Steam Deck system config
- `machines/nix-deck/hardware-configuration.nix` - Hardware config (placeholder)
### Example Configuration
```nix
# machines/nix-deck/configuration.nix
{
roles = {
desktop = {
enable = true;
wayland = true;
gaming.enable = true;
kde = true;
sddm = true;
steamos = {
enable = true;
autoStart = false; # or true to boot to Steam UI
desktopSession = "plasmawayland";
};
};
remote-build.builders = [{
hostName = "zix790prors";
maxJobs = 16;
speedFactor = 4;
}];
};
}
```
## Jovian Features
### Enabled by Default
- Steam Deck hardware support (`jovian.devices.steamdeck.enable`)
- Steam UI (`jovian.steam.enable`)
- Decky Loader plugin system (`jovian.decky-loader.enable`)
### Optional Features
Set in the hardware-configuration.nix:
```nix
jovian.devices.steamdeck = {
enable = true;
autoUpdate = false; # Auto-update BIOS/controller firmware
};
```
### Manual Firmware Updates
```bash
# BIOS update
sudo jupiter-biosupdate
# Controller update
sudo jupiter-controller-update
# Docking station (connect via USB-C first)
jupiter-dock-updater
```
## Troubleshooting
### Remote Builds Not Working
1. Check SSH connectivity:
```bash
ssh nix-builder@zix790prors
```
2. Verify builder is trusted:
```bash
# On zix790prors
nix show-config | grep trusted-users
```
3. Check build logs:
```bash
journalctl -u nix-daemon -f
```
### Jovian Not Working
1. Ensure you're on NixOS 25.05 or the compatibility layer is removed for 25.11+
2. Check Jovian is imported in flake.nix
3. Verify hardware config has `jovian.devices.steamdeck.enable = true`
### Compatibility Layer Issues
If you see an error about `jovian-compat.nix` being incompatible:
1. You're running NixOS 25.11 or later
2. Remove `./roles/jovian-compat.nix` from `flake.nix`
3. Jovian should work natively on 25.11+
## Future Upgrades
### Upgrading to NixOS 25.11
1. Update `nixpkgs` input in flake.nix to 25.11
2. Remove `./roles/jovian-compat.nix` from flake.nix imports
3. The assertion in jovian-compat.nix will prevent accidental use
4. Test the build
5. Deploy
### Switching to Unstable
If you need Jovian to follow unstable nixpkgs:
1. Edit `flake.nix`:
```nix
jovian = {
url = "github:Jovian-Experiments/Jovian-NixOS";
inputs.nixpkgs.follows = "nixpkgs-unstable";
};
```
2. This only affects Jovian packages, not your base system
## Additional Resources
- [Jovian-NixOS Documentation](https://jovian-experiments.github.io/Jovian-NixOS/)
- [Jovian Steam Deck Guide](https://jovian-experiments.github.io/Jovian-NixOS/devices/valve-steam-deck/)
- [NixOS Remote Builds](https://nixos.org/manual/nix/stable/advanced-topics/distributed-builds.html)

146
flake.lock generated
View File

@@ -3,11 +3,11 @@
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1761588595,
"narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=",
"lastModified": 1765121682,
"narHash": "sha256-4VBOP18BFeiPkyhy9o4ssBNQEvfvv1kXkasAYd0+rrA=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5",
"rev": "65f23138d8d09a92e30f1e5c87611b23ef451bf3",
"type": "github"
},
"original": {
@@ -43,16 +43,16 @@
]
},
"locked": {
"lastModified": 1758463745,
"narHash": "sha256-uhzsV0Q0I9j2y/rfweWeGif5AWe0MGrgZ/3TjpDYdGA=",
"lastModified": 1766292113,
"narHash": "sha256-sWTtmkQujRpjWYCnZc8LWdDiCzrRlSBPrGovkZpLkBI=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "3b955f5f0a942f9f60cdc9cacb7844335d0f21c3",
"rev": "fdec8815a86db36f42fc9c8cb2931cd8485f5aed",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "release-25.05",
"ref": "release-25.11",
"repo": "home-manager",
"type": "github"
}
@@ -64,11 +64,11 @@
]
},
"locked": {
"lastModified": 1763416652,
"narHash": "sha256-8EBEEvtzQ11LCxpQHMNEBQAGtQiCu/pqP9zSovDSbNM=",
"lastModified": 1766282146,
"narHash": "sha256-0V/nKU93KdYGi+5LB/MVo355obBJw/2z9b2xS3bPJxY=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "ea164b7c9ccdc2321379c2ff78fd4317b4c41312",
"rev": "61fcc9de76b88e55578eb5d79fc80f2b236df707",
"type": "github"
},
"original": {
@@ -86,11 +86,11 @@
]
},
"locked": {
"lastModified": 1763223001,
"narHash": "sha256-Hi6XxTJJjKsDrO+D0fYXS88ehCYzQkZlp9qxX1zoM1s=",
"lastModified": 1766225187,
"narHash": "sha256-6hcaU8qtmixsaEUbjPiOFd5aJPZxAIBokl5d7dkab3k=",
"owner": "Jovian-Experiments",
"repo": "Jovian-NixOS",
"rev": "68a1bcc019378272e601558719f82005a80ddab0",
"rev": "bb53a85db9210204a98f771f10f1f5b4e06ccb2d",
"type": "github"
},
"original": {
@@ -106,16 +106,16 @@
]
},
"locked": {
"lastModified": 1762912391,
"narHash": "sha256-4hpBE7bGd24SfD28rzMdUGXsLsNEYxCCrTipFdoqoNM=",
"lastModified": 1765066094,
"narHash": "sha256-0YSU35gfRFJzx/lTGgOt6ubP8K6LeW0vaywzNNqxkl4=",
"owner": "nix-darwin",
"repo": "nix-darwin",
"rev": "d76299b2cd01837c4c271a7b5186e3d5d8ebd126",
"rev": "688427b1aab9afb478ca07989dc754fa543e03d5",
"type": "github"
},
"original": {
"owner": "nix-darwin",
"ref": "nix-darwin-25.05",
"ref": "nix-darwin-25.11",
"repo": "nix-darwin",
"type": "github"
}
@@ -148,11 +148,11 @@
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1763385941,
"narHash": "sha256-99CBNgyMvg3Zu/hxqixtShevrF4Kfr/qjtizQ6oseVI=",
"lastModified": 1765841014,
"narHash": "sha256-55V0AJ36V5Egh4kMhWtDh117eE3GOjwq5LhwxDn9eHg=",
"owner": "nix-community",
"repo": "NixOS-WSL",
"rev": "cc6483354b236c2fc95cc1d4ba1f0f40b7345e69",
"rev": "be4af8042e7a61fa12fda58fe9a3b3babdefe17b",
"type": "github"
},
"original": {
@@ -164,11 +164,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1762977756,
"narHash": "sha256-4PqRErxfe+2toFJFgcRKZ0UI9NSIOJa+7RXVtBhy4KE=",
"lastModified": 1765472234,
"narHash": "sha256-9VvC20PJPsleGMewwcWYKGzDIyjckEz8uWmT0vCDYK0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "c5ae371f1a6a7fd27823bc500d9390b38c05fa55",
"rev": "2fbfb1d73d239d2402a8fe03963e37aab15abe8b",
"type": "github"
},
"original": {
@@ -180,11 +180,11 @@
},
"nixpkgs-unstable": {
"locked": {
"lastModified": 1763283776,
"narHash": "sha256-Y7TDFPK4GlqrKrivOcsHG8xSGqQx3A6c+i7novT85Uk=",
"lastModified": 1766070988,
"narHash": "sha256-G/WVghka6c4bAzMhTwT2vjLccg/awmHkdKSd2JrycLc=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "50a96edd8d0db6cc8db57dab6bb6d6ee1f3dc49a",
"rev": "c6245e83d836d0433170a16eb185cefe0572f8b8",
"type": "github"
},
"original": {
@@ -196,16 +196,16 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1763049705,
"narHash": "sha256-A5LS0AJZ1yDPTa2fHxufZN++n8MCmtgrJDtxFxrH4S8=",
"lastModified": 1766201043,
"narHash": "sha256-eplAP+rorKKd0gNjV3rA6+0WMzb1X1i16F5m5pASnjA=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "3acb677ea67d4c6218f33de0db0955f116b7588c",
"rev": "b3aad468604d3e488d627c0b43984eb60e75e782",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-25.05",
"ref": "nixos-25.11",
"repo": "nixpkgs",
"type": "github"
}
@@ -220,11 +220,11 @@
]
},
"locked": {
"lastModified": 1762784320,
"narHash": "sha256-odsk96Erywk5hs0dhArF38zb7Oe0q6LZ70gXbxAPKno=",
"lastModified": 1763909441,
"narHash": "sha256-56LwV51TX/FhgX+5LCG6akQ5KrOWuKgcJa+eUsRMxsc=",
"owner": "nix-community",
"repo": "plasma-manager",
"rev": "7911a0f8a44c7e8b29d031be3149ee8943144321",
"rev": "b24ed4b272256dfc1cc2291f89a9821d5f9e14b4",
"type": "github"
},
"original": {
@@ -243,11 +243,11 @@
]
},
"locked": {
"lastModified": 1762784320,
"narHash": "sha256-odsk96Erywk5hs0dhArF38zb7Oe0q6LZ70gXbxAPKno=",
"lastModified": 1763909441,
"narHash": "sha256-56LwV51TX/FhgX+5LCG6akQ5KrOWuKgcJa+eUsRMxsc=",
"owner": "nix-community",
"repo": "plasma-manager",
"rev": "7911a0f8a44c7e8b29d031be3149ee8943144321",
"rev": "b24ed4b272256dfc1cc2291f89a9821d5f9e14b4",
"type": "github"
},
"original": {
@@ -256,6 +256,52 @@
"type": "github"
}
},
"pyproject-build-systems": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"pyproject-nix": [
"pyproject-nix"
],
"uv2nix": [
"uv2nix"
]
},
"locked": {
"lastModified": 1763662255,
"narHash": "sha256-4bocaOyLa3AfiS8KrWjZQYu+IAta05u3gYZzZ6zXbT0=",
"owner": "pyproject-nix",
"repo": "build-system-pkgs",
"rev": "042904167604c681a090c07eb6967b4dd4dae88c",
"type": "github"
},
"original": {
"owner": "pyproject-nix",
"repo": "build-system-pkgs",
"type": "github"
}
},
"pyproject-nix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1764134915,
"narHash": "sha256-xaKvtPx6YAnA3HQVp5LwyYG1MaN4LLehpQI8xEdBvBY=",
"owner": "pyproject-nix",
"repo": "pyproject.nix",
"rev": "2c8df1383b32e5443c921f61224b198a2282a657",
"type": "github"
},
"original": {
"owner": "pyproject-nix",
"repo": "pyproject.nix",
"type": "github"
}
},
"root": {
"inputs": {
"google-cookie-retrieval": "google-cookie-retrieval",
@@ -267,7 +313,33 @@
"nixpkgs": "nixpkgs_2",
"nixpkgs-unstable": "nixpkgs-unstable",
"plasma-manager": "plasma-manager",
"plasma-manager-unstable": "plasma-manager-unstable"
"plasma-manager-unstable": "plasma-manager-unstable",
"pyproject-build-systems": "pyproject-build-systems",
"pyproject-nix": "pyproject-nix",
"uv2nix": "uv2nix"
}
},
"uv2nix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"pyproject-nix": [
"pyproject-nix"
]
},
"locked": {
"lastModified": 1766021660,
"narHash": "sha256-UUfz7qWB1Rb2KjGVCimt//Jncv3TgJwffPqbzqpkmgY=",
"owner": "pyproject-nix",
"repo": "uv2nix",
"rev": "19fa99be3409f55ec05e823c66c9769df7a8dd17",
"type": "github"
},
"original": {
"owner": "pyproject-nix",
"repo": "uv2nix",
"type": "github"
}
}
},

View File

@@ -2,17 +2,17 @@
description = "A very basic flake";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05";
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11";
nixpkgs-unstable.url = "github:nixos/nixpkgs/nixos-unstable";
nixos-wsl.url = "github:nix-community/NixOS-WSL/main";
nix-darwin = {
url = "github:nix-darwin/nix-darwin/nix-darwin-25.05";
url = "github:nix-darwin/nix-darwin/nix-darwin-25.11";
inputs.nixpkgs.follows = "nixpkgs";
};
home-manager = {
url = "github:nix-community/home-manager/release-25.05";
url = "github:nix-community/home-manager/release-25.11";
inputs.nixpkgs.follows = "nixpkgs";
};
@@ -42,22 +42,39 @@
url = "github:Jovian-Experiments/Jovian-NixOS";
inputs.nixpkgs.follows = "nixpkgs-unstable";
};
pyproject-nix = {
url = "github:pyproject-nix/pyproject.nix";
inputs.nixpkgs.follows = "nixpkgs";
};
uv2nix = {
url = "github:pyproject-nix/uv2nix";
inputs.pyproject-nix.follows = "pyproject-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
pyproject-build-systems = {
url = "github:pyproject-nix/build-system-pkgs";
inputs.pyproject-nix.follows = "pyproject-nix";
inputs.uv2nix.follows = "uv2nix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs, nixpkgs-unstable, nixos-wsl, ... } @ inputs: let
outputs = { self, nixpkgs, nixpkgs-unstable, nixos-wsl, pyproject-nix, uv2nix, pyproject-build-systems, ... } @ inputs: let
nixosModules = [
./roles
] ++ [
./roles/jovian-compat.nix
inputs.home-manager.nixosModules.home-manager
{
nixpkgs.overlays = [
(final: prev: {
unstable = import nixpkgs-unstable {
system = prev.system;
system = prev.stdenv.hostPlatform.system;
config.allowUnfree = true;
};
custom = prev.callPackage ./packages {};
custom = prev.callPackage ./packages { inherit uv2nix pyproject-nix pyproject-build-systems; };
# Compatibility: bitwarden renamed to bitwarden-desktop in unstable
bitwarden-desktop = prev.bitwarden-desktop or prev.bitwarden;
})
@@ -82,10 +99,10 @@
nixpkgs.overlays = [
(final: prev: {
unstable = import nixpkgs-unstable {
system = prev.system;
system = prev.stdenv.hostPlatform.system;
config.allowUnfree = true;
};
custom = prev.callPackage ./packages {};
custom = prev.callPackage ./packages { inherit uv2nix pyproject-nix pyproject-build-systems; };
# Compatibility: bitwarden renamed to bitwarden-desktop in unstable
bitwarden-desktop = prev.bitwarden-desktop or prev.bitwarden;
})
@@ -108,10 +125,17 @@
nixpkgs.overlays = [
(final: prev: {
unstable = import nixpkgs-unstable {
system = prev.system;
system = prev.stdenv.hostPlatform.system;
config.allowUnfree = true;
overlays = [
# Override claude-code in unstable to use our custom GCS-based build
# (needed for corporate networks that block npm registry)
(ufinal: uprev: {
claude-code = prev.custom.claude-code or (prev.callPackage ./packages {}).claude-code;
})
];
};
custom = prev.callPackage ./packages {};
custom = prev.callPackage ./packages { inherit uv2nix pyproject-nix pyproject-build-systems; };
# Compatibility: bitwarden renamed to bitwarden-desktop in unstable
bitwarden-desktop = prev.bitwarden-desktop or prev.bitwarden;
})
@@ -133,7 +157,7 @@
home-manager.users.johno = {
imports = [ ./home/home-laptop-compact.nix ];
# Machine-specific overrides
home.i3_sway.extraSwayConfig = {
home.roles.i3_sway.extraSwayConfig = {
output.eDP-1.scale = "1.75";
};
};
@@ -214,5 +238,21 @@
}
];
};
# Flake apps
apps = nixpkgs.lib.genAttrs [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" ] (system:
let
pkgs = import nixpkgs { inherit system; };
update-doomemacs = pkgs.writeShellScriptBin "update-doomemacs" ''
export PATH="${pkgs.lib.makeBinPath [ pkgs.curl pkgs.jq pkgs.nix pkgs.git pkgs.gnused pkgs.gnugrep pkgs.coreutils ]}:$PATH"
${builtins.readFile ./scripts/update-doomemacs.sh}
'';
in {
update-doomemacs = {
type = "app";
program = "${update-doomemacs}/bin/update-doomemacs";
};
}
);
};
}

View File

@@ -1,8 +1,5 @@
{ config, lib, pkgs, globalInputs, system, ... }:
let
leader = "cmd"; # Change this to experiment with different leader keys (e.g., "cmd", "ctrl")
in
{
# Home Manager configuration for Darwin work laptop
# Corporate-friendly setup with essential development tools
@@ -13,39 +10,11 @@ in
# System packages
home.packages = with pkgs; [
autoraise
google-cloud-sdk
];
# Note: ghostty installed via Homebrew (managed outside of nix)
# Auto-start autoraise on login
launchd.agents.autoraise = {
enable = true;
config = {
ProgramArguments = [
"${pkgs.autoraise}/bin/AutoRaise"
"-pollMillis" "50"
"-delay" "2"
"-focusDelay" "2"
];
RunAtLoad = true;
KeepAlive = true;
};
};
# Auto-start aerospace on login
# NOTE: In 25.11+, this can be simplified to `programs.aerospace.launchd.enable = true`
launchd.agents.aerospace = {
enable = true;
config = {
Program = "${pkgs.aerospace}/Applications/AeroSpace.app/Contents/MacOS/AeroSpace";
RunAtLoad = true;
KeepAlive = true;
StandardOutPath = "/tmp/aerospace.log";
StandardErrorPath = "/tmp/aerospace.err.log";
};
};
# Override Darwin-incompatible settings from base role
programs.rbw.settings.pinentry = lib.mkForce pkgs.pinentry_mac;
@@ -127,107 +96,28 @@ in
home.shell.enableShellIntegration = true;
# TODO: Move this to its own role and/or module
programs.aerospace = {
enable = true;
userSettings.mode.main.binding = {
"${leader}-slash" = "layout tiles horizontal vertical";
"${leader}-comma" = "layout accordion horizontal vertical";
"${leader}-shift-q" = "close";
"${leader}-shift-f" = "fullscreen";
"${leader}-h" = "focus left";
"${leader}-j" = "focus down";
"${leader}-k" = "focus up";
"${leader}-l" = "focus right";
"${leader}-shift-h" = "move left";
"${leader}-shift-j" = "move down";
"${leader}-shift-k" = "move up";
"${leader}-shift-l" = "move right";
"${leader}-minus" = "resize smart -50";
"${leader}-equal" = "resize smart +50";
"${leader}-1" = "workspace 1";
"${leader}-2" = "workspace 2";
"${leader}-3" = "workspace 3";
"${leader}-4" = "workspace 4";
"${leader}-5" = "workspace 5";
"${leader}-6" = "workspace 6";
"${leader}-7" = "workspace 7";
"${leader}-8" = "workspace 8";
"${leader}-9" = "workspace 9";
"${leader}-0" = "workspace 10";
"${leader}-shift-1" = "move-node-to-workspace 1";
"${leader}-shift-2" = "move-node-to-workspace 2";
"${leader}-shift-3" = "move-node-to-workspace 3";
"${leader}-shift-4" = "move-node-to-workspace 4";
"${leader}-shift-5" = "move-node-to-workspace 5";
"${leader}-shift-6" = "move-node-to-workspace 6";
"${leader}-shift-7" = "move-node-to-workspace 7";
"${leader}-shift-8" = "move-node-to-workspace 8";
"${leader}-shift-9" = "move-node-to-workspace 9";
"${leader}-shift-0" = "move-node-to-workspace 10";
"${leader}-tab" = "workspace-back-and-forth";
"${leader}-shift-tab" = "move-workspace-to-monitor --wrap-around next";
"${leader}-enter" = ''
exec-and-forget osascript <<'APPLESCRIPT'
tell application "Ghostty"
activate
tell application "System Events"
keystroke "n" using {command down}
end tell
end tell
APPLESCRIPT
'';
"${leader}-shift-enter" = ''
exec-and-forget osascript <<'APPLESCRIPT'
tell application "Google Chrome"
set newWindow to make new window
activate
tell newWindow to set index to 1
end tell
APPLESCRIPT
'';
"${leader}-shift-e" = "exec-and-forget zsh --login -c \"emacsclient -c -n\"";
# Service mode: Deliberate aerospace window management
"${leader}-i" = "mode service";
# Passthrough mode: Temporarily disable aerospace to use macOS shortcuts
# Press Cmd-P, then use any macOS shortcut (like Cmd-K in Slack), then press Cmd-P again to exit
"${leader}-p" = "mode passthrough";
};
# Service mode: For deliberate aerospace window management operations
userSettings.mode.service.binding = {
esc = ["reload-config" "mode main"];
r = ["flatten-workspace-tree" "mode main"]; # reset layout
f = ["layout floating tiling" "mode main"]; # Toggle between floating and tiling layout
backspace = ["close-all-windows-but-current" "mode main"];
"${leader}-shift-h" = ["join-with left" "mode main"];
"${leader}-shift-j" = ["join-with down" "mode main"];
"${leader}-shift-k" = ["join-with up" "mode main"];
"${leader}-shift-l" = ["join-with right" "mode main"];
};
# Passthrough mode: All shortcuts pass through to macOS
# This mode has minimal bindings - just ways to exit back to main mode
userSettings.mode.passthrough.binding = {
esc = "mode main";
"${leader}-p" = "mode main"; # Toggle back with same key (Cmd-P)
};
};
home.roles = {
base.enable = true;
development = {
enable = true;
allowArbitraryClaudeCodeModelSelection = true;
};
tmux.enable = true;
emacs.enable = true;
aerospace = {
enable = true;
leader = "cmd";
ctrlShortcuts.enable = true;
sketchybar.enable = true;
# Optional: Add per-machine userSettings overrides
# userSettings = {
# mode.main.binding."${leader}-custom" = "custom-command";
# };
};
};
imports = [
./roles
./modules/emacs
./modules/kubectl
./modules/tmux
./roles/base-darwin
];
}

View File

@@ -8,14 +8,21 @@
# Enable all desktop roles for full-featured experience
home.roles = {
"3d-printing".enable = true;
base.enable = true;
desktop.enable = true;
emacs.enable = true;
email.enable = true;
i3_sway.enable = true;
office.enable = true;
media.enable = true;
development.enable = true;
communication.enable = true;
sync.enable = true;
kdeconnect.enable = true;
kubectl.enable = true;
tmux.enable = true;
plasma-manager.enable = true;
};
targets.genericLinux.enable = true;
@@ -24,10 +31,6 @@
imports = [
./roles
./modules/emacs
./modules/i3+sway
./modules/kubectl
./modules/plasma-manager
./modules/tmux
./roles/base-linux
];
}
}

View File

@@ -14,10 +14,23 @@
desktop.enable = true;
development.enable = true;
communication.enable = true;
email.enable = true;
kdeconnect.enable = true;
media.enable = true;
sync.enable = true;
# office.enable = false; # Excluded for storage constraints
kubectl.enable = true;
tmux.enable = true;
plasma-manager.enable = true;
emacs.enable = true;
i3_sway.enable = true;
# Launcher wrappers for excluded/optional packages
launchers = {
enable = true;
packages = [
"libreoffice"
];
};
};
targets.genericLinux.enable = true;
@@ -26,11 +39,6 @@
imports = [
./roles
./modules/emacs
./modules/i3+sway
./modules/kubectl
./modules/plasma-manager
./modules/tmux
./roles/base-linux
];
}

View File

@@ -12,8 +12,12 @@
home.roles = {
base.enable = true;
desktop.enable = true;
tmux.enable = true;
plasma-manager.enable = true;
emacs.enable = true;
i3_sway.enable = true;
# development.enable = false; # Not needed for live USB
# communication.enable = false; # Not needed for live USB
# communication.enable = false; # Not needed for live USB
# office.enable = false; # Not needed for live USB
# media.enable = false; # Not needed for live USB
# sync.enable = false; # No persistent sync on live USB
@@ -26,11 +30,7 @@
imports = [
./roles
./modules/emacs
./modules/i3+sway
./modules/kubectl
./modules/plasma-manager
./modules/tmux
./roles/base-linux
];
# Live USB specific overrides can go here if needed

View File

@@ -16,6 +16,10 @@
communication.enable = true;
kdeconnect.enable = true;
development.enable = true;
tmux.enable = true;
plasma-manager.enable = true;
emacs.enable = true;
i3_sway.enable = true;
# office.enable = false; # Not needed for media center
# sync.enable = false; # Shared machine, no personal file sync
};
@@ -26,11 +30,7 @@
imports = [
./roles
./modules/emacs
./modules/i3+sway
./modules/kubectl
./modules/plasma-manager
./modules/tmux
./roles/base-linux
];
# Media center specific overrides can go here if needed

View File

@@ -1,178 +0,0 @@
{ config, lib, pkgs, ... }:
# The current KDE config can be output with the command:
# nix run github:nix-community/plasma-manager
#
# Plasma-manager options documentation
# https://nix-community.github.io/plasma-manager/options.xhtml
#
# TODO: (ambitious) Add Kmail support to plasma-manager
{
programs.plasma = {
enable = true;
overrideConfig = true;
hotkeys.commands."launch-ghostty" = {
name = "Launch Ghostty";
key = "Meta+Return";
command = "ghostty";
};
shortcuts = {
kmix = {
"decrease_microphone_volume" = "Microphone Volume Down";
"decrease_volume" = "Volume Down";
"decrease_volume_small" = "Shift+Volume Down";
"increase_microphone_volume" = "Microphone Volume Up";
"increase_volume" = "Volume Up";
"increase_volume_small" = "Shift+Volume Up";
"mic_mute" = ["Microphone Mute" "Meta+Volume Mute,Microphone Mute" "Meta+Volume Mute,Mute Microphone"];
"mute" = "Volume Mute";
};
mediacontrol = {
"mediavolumedown" = "none,,Media volume down";
"mediavolumeup" = "none,,Media volume up";
"nextmedia" = "Media Next";
"pausemedia" = "Media Pause";
"playmedia" = "none,,Play media playback";
"playpausemedia" = "Media Play";
"previousmedia" = "Media Previous";
"stopmedia" = "Media Stop";
};
ksmserver = {
"Lock Session" = ["Meta+Ctrl+Q" "Screensaver" "Screensaver,Lock Session"];
};
kwin = {
"Window Close" = "Meta+Shift+Q";
"Kill Window" = "Meta+Ctrl+Esc";
"Window Operations Menu" = "Alt+F3";
"Window Resize" = "Meta+R,,Resize Window";
"Overview" = "Meta+Ctrl+W";
"Grid View" = "Meta+G";
"Edit Tiles" = "Meta+T";
"Activate Window Demanding Attention" = "Meta+Ctrl+A";
"Show Desktop" = "Meta+Ctrl+D";
"Walk Through Windows" = "Alt+Tab";
"Walk Through Windows (Reverse)" = "Alt+Shift+Tab";
"Walk Through Windows of Current Application" = "Alt+`";
"Walk Through Windows of Current Application (Reverse)" = "Alt+~";
"Window Quick Tile Bottom" = "Meta+Down";
"Window Quick Tile Left" = "Meta+Left";
"Window Quick Tile Right" = "Meta+Right";
"Window Quick Tile Top" = "Meta+Up";
"Switch to Desktop 1" = "Meta+1";
"Switch to Desktop 2" = "Meta+2";
"Switch to Desktop 3" = "Meta+3";
"Switch to Desktop 4" = "Meta+4";
"Switch to Desktop 5" = "Meta+5";
"Switch to Desktop 6" = "Meta+6";
"Switch to Desktop 7" = "Meta+7";
"Switch to Desktop 8" = "Meta+8";
"Switch to Desktop 9" = "Meta+9";
"Switch to Desktop 10" = "Meta+0";
"Window to Desktop 1" = "Meta+!"; # Meta+Shift+1
"Window to Desktop 2" = "Meta+@"; # Meta+Shift+2
"Window to Desktop 3" = "Meta+#"; # Meta+Shift+3
"Window to Desktop 4" = "Meta+$"; # Meta+Shift+4
"Window to Desktop 5" = "Meta+%"; # Meta+Shift+5
"Window to Desktop 6" = "Meta+^"; # Meta+Shift+6
"Window to Desktop 7" = "Meta+&"; # Meta+Shift+7
"Window to Desktop 8" = "Meta+*"; # Meta+Shift+8
"Window to Desktop 9" = "Meta+("; # Meta+Shift+9
"Window to Desktop 10" = "Meta+)"; # Meta+Shift+0
"view_actual_size" = "Meta+Ctrl+=";
"view_zoom_in" = ["Meta++" "Meta+=,Meta++" "Meta+=,Zoom In"];
"view_zoom_out" = "Meta+-";
};
"org_kde_powerdevil"."Decrease Keyboard Brightness" = "Keyboard Brightness Down";
"org_kde_powerdevil"."Decrease Screen Brightness" = "Monitor Brightness Down";
"org_kde_powerdevil"."Decrease Screen Brightness Small" = "Shift+Monitor Brightness Down";
"org_kde_powerdevil"."Hibernate" = "Hibernate";
"org_kde_powerdevil"."Increase Keyboard Brightness" = "Keyboard Brightness Up";
"org_kde_powerdevil"."Increase Screen Brightness" = "Monitor Brightness Up";
"org_kde_powerdevil"."Increase Screen Brightness Small" = "Shift+Monitor Brightness Up";
"org_kde_powerdevil"."PowerDown" = "Power Down";
"org_kde_powerdevil"."PowerOff" = "Power Off";
"org_kde_powerdevil"."Sleep" = "Sleep";
"org_kde_powerdevil"."Toggle Keyboard Backlight" = "Keyboard Light On/Off";
"org_kde_powerdevil"."Turn Off Screen" = [ ];
"org_kde_powerdevil"."powerProfile" = ["Battery" "Meta+B,Battery" "Meta+B,Switch Power Profile"];
plasmashell = {
"activate application launcher" = ["Meta" "Alt+F1,Meta" "Alt+F1,Activate Application Launcher"];
"activate task manager entry 1" = "none,,";
"activate task manager entry 2" = "none,,";
"activate task manager entry 3" = "none,,";
"activate task manager entry 4" = "none,,";
"activate task manager entry 5" = "none,,";
"activate task manager entry 6" = "none,,";
"activate task manager entry 7" = "none,,";
"activate task manager entry 8" = "none,,";
"activate task manager entry 9" = "none,,";
"activate task manager entry 10" = "none,,";
"show activity switcher" = "none,,";
};
};
configFile = {
kwinrc.Desktops.Number = {
value = 10;
immutable = true;
};
# Enable KWin tiling features
kwinrc.Tiling = {
# Enable tiling functionality
"padding" = 4;
};
# Enable krohnkite plugin automatically
kwinrc.Plugins = {
krohnkiteEnabled = true;
};
kwinrc.Effect-overview = {
# Configure overview effect for better tiling workflow
BorderActivate = 9; # Top-left corner activation
};
kcminputrc.Libinput = {
AccelerationProfile = "adaptive";
PointerAcceleration = 0.5;
};
kcminputrc.Mouse = {
X11LibInputXAccelProfileFlat = false;
XLbInptAccelProfileFlat = false;
};
kdeglobals.KDE.LookAndFeelPackage = "org.kde.breezedark.desktop";
# Focus follows mouse configuration
kwinrc.Windows = {
FocusPolicy = "FocusFollowsMouse";
AutoRaise = true; # Set to true if you want windows to auto-raise on focus
AutoRaiseInterval = 750; # Delay in ms before auto-raise (if enabled)
DelayFocusInterval = 0; # Delay in ms before focus follows mouse
};
# Desktop wallpaper configuration
plasma-localerc.Formats.LANG = "en_US.UTF-8";
# Set wallpaper for all desktops
plasmarc.Wallpapers.usersWallpapers = "${../../wallpapers/metroid-samus-returns-kz-3440x1440.jpg}";
};
};
}

View File

@@ -1,52 +0,0 @@
{ config, lib, pkgs, ... }:
let
tokyo-night = pkgs.tmuxPlugins.mkTmuxPlugin {
pluginName = "tokyo-night";
rtpFilePath = "tokyo-night.tmux";
version = "1.6.1";
src = pkgs.fetchFromGitHub {
owner = "janoamaral";
repo = "tokyo-night-tmux";
rev = "d610ced20d5f602a7995854931440e4a1e0ab780";
sha256 = "sha256-17vEgkL7C51p/l5gpT9dkOy0bY9n8l0/LV51mR1k+V8=";
};
};
in
{
programs.tmux.enable = true;
programs.tmux.terminal = "tmux-direct";
programs.tmux.keyMode = "vi";
programs.tmux.escapeTime = 0;
programs.tmux.mouse = true;
programs.tmux.newSession = true;
programs.tmux.historyLimit = 50000;
programs.tmux.clock24 = true;
programs.tmux.baseIndex = 1;
programs.tmux.prefix = "M-\\\\";
programs.tmux.plugins = with pkgs; [
tmuxPlugins.cpu
tmuxPlugins.battery
tmuxPlugins.better-mouse-mode
tmuxPlugins.net-speed
tmuxPlugins.online-status
tmuxPlugins.pain-control
tmuxPlugins.tilish
tmuxPlugins.yank
{
plugin = tmuxPlugins.resurrect;
extraConfig = "set -g @resurrect-strategy-nvim 'session'";
}
{
plugin = tmuxPlugins.continuum;
extraConfig = ''
set -g @continuum-restore 'on'
set -g @continuum-save-interval '15' # minutes
'';
}
tokyo-night
];
}

View File

@@ -0,0 +1,22 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.home.roles."3d-printing";
in
{
options.home.roles."3d-printing" = {
enable = mkEnableOption "Enable 3D printing applications and tools";
};
config = mkIf cfg.enable {
home.packages = with pkgs; [
# 3D Slicing Software
orca-slicer # G-code generator for 3D printers (Bambu, Prusa, Voron, etc.)
# 3D Modeling Software
openscad-unstable # 3D parametric model compiler (nightly build)
];
};
}

View File

@@ -0,0 +1,727 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.home.roles.aerospace;
in
{
options.home.roles.aerospace = {
enable = mkEnableOption "AeroSpace tiling window manager for macOS";
leader = mkOption {
type = types.str;
default = "cmd";
description = "Leader key for aerospace shortcuts (e.g., 'cmd', 'ctrl', 'alt')";
example = "ctrl";
};
launchd.enable = mkOption {
type = types.bool;
default = true;
description = "Whether to enable launchd agent for auto-starting aerospace";
};
userSettings = mkOption {
type = types.attrs;
default = {};
description = ''
Additional aerospace configuration settings to merge with defaults.
Use this to override or extend the default configuration on a per-machine basis.
'';
example = literalExpression ''
{
mode.main.binding."''${leader}-custom" = "custom-command";
}
'';
};
autoraise = {
enable = mkOption {
type = types.bool;
default = true;
description = "Whether to enable autoraise (auto-focus window on hover)";
};
pollMillis = mkOption {
type = types.int;
default = 50;
description = "Polling interval in milliseconds";
};
delay = mkOption {
type = types.int;
default = 2;
description = "Delay before raising window";
};
focusDelay = mkOption {
type = types.int;
default = 2;
description = "Delay before focusing window";
};
};
enableSpansDisplays = mkOption {
type = types.bool;
default = true;
description = ''
Configure macOS Spaces to span displays (required for aerospace multi-monitor support).
Sets com.apple.spaces.spans-displays to true.
NOTE: This was previously set at the system level in modules/aerospace.nix,
but has been moved to home-manager for better modularity.
'';
};
ctrlShortcuts = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Remap common macOS Cmd shortcuts to Ctrl equivalents for all operations.
This makes macOS behave more like Linux.
Shortcuts remapped globally:
- Ctrl+N: New Window
- Ctrl+T: New Tab
- Ctrl+W: Close Tab
- Ctrl+S: Save / Save As
- Ctrl+O: Open
- Ctrl+F: Find
- Ctrl+H: Find and Replace
- Ctrl+P: Print
- Ctrl+C/V/X: Copy/Paste/Cut
- Ctrl+Z: Undo
NOTE: Terminal emulators like Ghostty require per-app overrides (configured separately)
to preserve Ctrl+C as SIGINT instead of Copy.
'';
};
};
sketchybar = {
enable = mkOption {
type = types.bool;
default = false;
description = "Whether to enable SketchyBar status bar";
};
};
};
config = mkIf cfg.enable {
# Only apply on Darwin systems
assertions = [
{
assertion = pkgs.stdenv.isDarwin;
message = "Aerospace role is only supported on macOS (Darwin) systems";
}
];
# Configure macOS preferences via targets.darwin.defaults
targets.darwin.defaults = mkMerge [
# Spaces span displays (required for multi-monitor aerospace)
(mkIf cfg.enableSpansDisplays {
"com.apple.spaces" = {
spans-displays = true;
};
})
# Ctrl shortcuts to make macOS behave more like Linux
(mkIf cfg.ctrlShortcuts.enable {
NSGlobalDomain.NSUserKeyEquivalents = {
# Window/Tab operations
"New Window" = "^n";
"New Tab" = "^t";
"Close Tab" = "^w";
# File operations
"Save" = "^s";
"Save As" = "^$s"; # Ctrl+Shift+S
"Open" = "^o";
"Open" = "^o";
# Find operations
"Find" = "^f";
"Find" = "^f";
"Find and Replace" = "^h";
"Find and Replace" = "^h";
# Print
"Print" = "^p";
"Print" = "^p";
# Clipboard operations
"Copy" = "^c";
"Paste" = "^v";
"Cut" = "^x";
# Undo/Redo
"Undo" = "^z";
"Redo" = "^$z"; # Ctrl+Shift+Z
};
})
# Ghostty-specific overrides to preserve terminal behavior
# Remap clipboard operations back to Cmd (macOS default) so Ctrl+C remains SIGINT
(mkIf cfg.ctrlShortcuts.enable {
"com.mitchellh.ghostty".NSUserKeyEquivalents = {
# Remap back to Cmd for clipboard operations
"Copy" = "@c"; # Cmd+C
"Paste" = "@v"; # Cmd+V
"Cut" = "@x"; # Cmd+X
"Undo" = "@z"; # Cmd+Z
"Redo" = "@$z"; # Cmd+Shift+Z
};
})
];
# Install aerospace package and optional tools if enabled
home.packages = [ pkgs.aerospace ]
++ optionals cfg.autoraise.enable [ pkgs.autoraise ]
++ optionals cfg.sketchybar.enable [ pkgs.sketchybar pkgs.sketchybar-app-font ];
# Enable and configure aerospace
programs.aerospace.enable = true;
programs.aerospace.launchd.enable = cfg.launchd.enable;
programs.aerospace.userSettings = mkMerge [
# Default configuration with leader key substitution
{
# Disable normalizations for i3-like behavior
enable-normalization-flatten-containers = false;
enable-normalization-opposite-orientation-for-nested-containers = false;
mode.main.binding = {
"${cfg.leader}-w" = "layout accordion horizontal"; # tabbed
"${cfg.leader}-s" = "layout accordion vertical"; # stacking
"${cfg.leader}-e" = "layout tiles horizontal vertical"; # tiles, toggles orientation
"${cfg.leader}-shift-q" = "close";
"${cfg.leader}-shift-f" = "fullscreen";
"${cfg.leader}-h" = "focus left";
"${cfg.leader}-j" = "focus down";
"${cfg.leader}-k" = "focus up";
"${cfg.leader}-l" = "focus right";
"${cfg.leader}-shift-h" = "move left";
"${cfg.leader}-shift-j" = "move down";
"${cfg.leader}-shift-k" = "move up";
"${cfg.leader}-shift-l" = "move right";
"${cfg.leader}-r" = "mode resize";
"${cfg.leader}-1" = "workspace 1";
"${cfg.leader}-2" = "workspace 2";
"${cfg.leader}-3" = "workspace 3";
"${cfg.leader}-4" = "workspace 4";
"${cfg.leader}-5" = "workspace 5";
"${cfg.leader}-6" = "workspace 6";
"${cfg.leader}-7" = "workspace 7";
"${cfg.leader}-8" = "workspace 8";
"${cfg.leader}-9" = "workspace 9";
"${cfg.leader}-0" = "workspace 10";
"${cfg.leader}-shift-1" = "move-node-to-workspace 1";
"${cfg.leader}-shift-2" = "move-node-to-workspace 2";
"${cfg.leader}-shift-3" = "move-node-to-workspace 3";
"${cfg.leader}-shift-4" = "move-node-to-workspace 4";
"${cfg.leader}-shift-5" = "move-node-to-workspace 5";
"${cfg.leader}-shift-6" = "move-node-to-workspace 6";
"${cfg.leader}-shift-7" = "move-node-to-workspace 7";
"${cfg.leader}-shift-8" = "move-node-to-workspace 8";
"${cfg.leader}-shift-9" = "move-node-to-workspace 9";
"${cfg.leader}-shift-0" = "move-node-to-workspace 10";
"${cfg.leader}-tab" = "workspace-back-and-forth";
"${cfg.leader}-shift-tab" = "move-workspace-to-monitor --wrap-around next";
"${cfg.leader}-enter" = ''
exec-and-forget osascript <<'APPLESCRIPT'
tell application "Ghostty"
activate
tell application "System Events"
keystroke "n" using {command down}
end tell
end tell
APPLESCRIPT
'';
"${cfg.leader}-shift-enter" = ''
exec-and-forget osascript <<'APPLESCRIPT'
tell application "Google Chrome"
set newWindow to make new window
activate
tell newWindow to set index to 1
end tell
APPLESCRIPT
'';
"${cfg.leader}-shift-e" = "exec-and-forget zsh --login -c \"emacsclient -c -n\"";
# Service mode: Deliberate aerospace window management
"${cfg.leader}-i" = "mode service";
# Passthrough mode: Temporarily disable aerospace to use macOS shortcuts
"${cfg.leader}-p" = "mode passthrough";
};
# Resize mode: For window resizing operations
mode.resize.binding = {
h = "resize width -50";
j = "resize height +50";
k = "resize height -50";
l = "resize width +50";
minus = "resize smart -50";
equal = "resize smart +50";
esc = "mode main";
enter = "mode main";
};
# Service mode: For deliberate aerospace window management operations
mode.service.binding = {
esc = ["reload-config" "mode main"];
r = ["flatten-workspace-tree" "mode main"]; # reset layout
f = ["layout floating tiling" "mode main"]; # Toggle between floating and tiling layout
backspace = ["close-all-windows-but-current" "mode main"];
"${cfg.leader}-shift-h" = ["join-with left" "mode main"];
"${cfg.leader}-shift-j" = ["join-with down" "mode main"];
"${cfg.leader}-shift-k" = ["join-with up" "mode main"];
"${cfg.leader}-shift-l" = ["join-with right" "mode main"];
};
# Passthrough mode: All shortcuts pass through to macOS
mode.passthrough.binding = {
esc = "mode main";
"${cfg.leader}-p" = "mode main";
};
# SketchyBar integration - notify bar of workspace changes
exec-on-workspace-change = mkIf cfg.sketchybar.enable [
"/bin/bash" "-c"
"${pkgs.sketchybar}/bin/sketchybar --trigger aerospace_workspace_change FOCUSED=$AEROSPACE_FOCUSED_WORKSPACE PREV=$AEROSPACE_PREV_WORKSPACE"
];
}
# Gaps configuration - prevent windows from overlapping SketchyBar
(mkIf cfg.sketchybar.enable {
gaps = {
outer = {
top = 0;
bottom = 38;
left = 0;
right = 0;
};
};
})
cfg.userSettings
];
# Launchd agent for autoraise
launchd.agents.autoraise = mkIf cfg.autoraise.enable {
enable = true;
config = {
ProgramArguments = [
"${pkgs.autoraise}/bin/AutoRaise"
"-pollMillis" (toString cfg.autoraise.pollMillis)
"-delay" (toString cfg.autoraise.delay)
"-focusDelay" (toString cfg.autoraise.focusDelay)
];
RunAtLoad = true;
KeepAlive = true;
};
};
# SketchyBar configuration
home.file.".config/sketchybar/sketchybarrc" = mkIf cfg.sketchybar.enable {
executable = true;
onChange = "${pkgs.sketchybar}/bin/sketchybar --reload";
text = ''
#!/bin/bash
# Plugin directory
PLUGIN_DIR="$HOME/.config/sketchybar/plugins"
# Colors - i3/sway theme with exact color matching
# Focused window/workspace color from i3/sway
FOCUSED=0xff285577
# Background colors matching i3blocks bar
BAR_BG=0xff333333 # Dark gray
ITEM_BG=0xff333333 # Dark gray matching bar
# Text colors
TEXT=0xffffffff # White text
GRAY=0xff888888 # Muted text for inactive items
# Accent colors for warnings
WARNING=0xffff9900
CRITICAL=0xff900000
# Configure the bar appearance
${pkgs.sketchybar}/bin/sketchybar --bar \
position=bottom \
height=30 \
color=$BAR_BG \
border_width=0 \
corner_radius=0 \
padding_left=10 \
padding_right=10 \
shadow=off \
topmost=on \
sticky=on
# Set default properties for all items
# Using monospace font to match waybar's Fira Code styling
${pkgs.sketchybar}/bin/sketchybar --default \
updates=when_shown \
icon.font="Fira Code:Regular:13.0" \
icon.color=$TEXT \
icon.padding_left=4 \
icon.padding_right=4 \
label.font="Fira Code:Regular:13.0" \
label.color=$TEXT \
label.padding_left=4 \
label.padding_right=4 \
padding_left=4 \
padding_right=4 \
background.corner_radius=0 \
background.height=30
# Register aerospace workspace change event
${pkgs.sketchybar}/bin/sketchybar --add event aerospace_workspace_change
# Create workspace indicators for workspaces 1-10
for sid in 1 2 3 4 5 6 7 8 9 10; do
# Display "0" for workspace 10
if [ "$sid" = "10" ]; then
display="0"
else
display="$sid"
fi
${pkgs.sketchybar}/bin/sketchybar --add item space.$sid left \
--subscribe space.$sid aerospace_workspace_change \
--set space.$sid \
drawing=on \
update_freq=2 \
width=32 \
background.color=$ITEM_BG \
background.corner_radius=0 \
background.height=30 \
background.drawing=on \
icon="$display" \
icon.padding_left=13 \
icon.padding_right=11 \
icon.align=center \
label.drawing=off \
click_script="${pkgs.aerospace}/bin/aerospace workspace $sid" \
script="$PLUGIN_DIR/aerospace.sh $sid"
done
# System monitoring modules (right side)
# Note: Items added to 'right' appear in reverse order (last added = leftmost)
# Adding in reverse to get: disk | cpu | memory | battery | volume | calendar
${pkgs.sketchybar}/bin/sketchybar --add item calendar right \
--set calendar \
icon="📅" \
update_freq=30 \
background.color=$ITEM_BG \
background.drawing=on \
script="$PLUGIN_DIR/calendar.sh"
${pkgs.sketchybar}/bin/sketchybar --add item volume right \
--set volume \
background.color=$ITEM_BG \
background.drawing=on \
script="$PLUGIN_DIR/volume.sh" \
--subscribe volume volume_change
${pkgs.sketchybar}/bin/sketchybar --add item battery right \
--set battery \
update_freq=120 \
background.color=$ITEM_BG \
background.drawing=on \
script="$PLUGIN_DIR/battery.sh" \
--subscribe battery system_woke power_source_change
${pkgs.sketchybar}/bin/sketchybar --add item memory right \
--set memory \
update_freq=5 \
icon="🐏" \
background.color=$ITEM_BG \
background.drawing=on \
script="$PLUGIN_DIR/memory.sh"
${pkgs.sketchybar}/bin/sketchybar --add item cpu right \
--set cpu \
update_freq=2 \
icon="🧠" \
background.color=$ITEM_BG \
background.drawing=on \
script="$PLUGIN_DIR/cpu.sh"
${pkgs.sketchybar}/bin/sketchybar --add item disk right \
--set disk \
update_freq=60 \
icon="💾" \
background.color=$ITEM_BG \
background.drawing=on \
script="$PLUGIN_DIR/disk.sh"
# Menu bar extras / system tray items (rightmost)
# Note: Requires Screen Recording permission for SketchyBar in System Settings
# Use 'sketchybar --query default_menu_items' to discover available items
# Bluetooth
${pkgs.sketchybar}/bin/sketchybar --add alias "Control Center,Bluetooth" right \
--set "Control Center,Bluetooth" \
alias.update_freq=1 \
padding_left=0 \
padding_right=0
# WiFi
${pkgs.sketchybar}/bin/sketchybar --add alias "Control Center,WiFi" right \
--set "Control Center,WiFi" \
alias.update_freq=1 \
padding_left=0 \
padding_right=0
# Add other menu bar apps as discovered
# Common examples:
# - Cloudflare WARP: --add alias "Cloudflare WARP,Item-0" right
# - Notion Calendar: --add alias "Notion Calendar,Item-0" right
# Run 'sketchybar --query default_menu_items' to find exact names
# Update the bar
${pkgs.sketchybar}/bin/sketchybar --update
'';
};
# SketchyBar aerospace workspace plugin
home.file.".config/sketchybar/plugins/aerospace.sh" = mkIf cfg.sketchybar.enable {
executable = true;
text = ''
#!/bin/bash
# Colors
FOCUSED_COLOR=0xff285577
ITEM_BG=0xff333333
TEXT=0xffffffff
GRAY=0xff555555
# Get the currently focused workspace directly from aerospace
# Trim whitespace to ensure clean comparison
FOCUSED=$(${pkgs.aerospace}/bin/aerospace list-workspaces --focused | tr -d ' \n\r')
# Get list of empty workspaces
EMPTY_WORKSPACES=$(${pkgs.aerospace}/bin/aerospace list-workspaces --monitor all --empty)
# Get workspace number - from $1 if provided (event-triggered), otherwise extract from $NAME (routine update)
# $NAME is always available (e.g., "space.1", "space.2", etc.)
# $1 is only available when called via event trigger with positional argument
if [ -n "$1" ]; then
WORKSPACE_NUM=$(echo "$1" | tr -d ' \n\r')
else
# Extract number from item name: "space.1" -> "1", "space.10" -> "10"
WORKSPACE_NUM=$(echo "$NAME" | sed 's/space\.//')
fi
# Check if workspace has windows (is NOT empty)
IS_EMPTY=false
if echo "$EMPTY_WORKSPACES" | grep -q "^$WORKSPACE_NUM$"; then
IS_EMPTY=true
fi
# Check if this workspace is focused
IS_FOCUSED=false
if [ "$WORKSPACE_NUM" = "$FOCUSED" ]; then
IS_FOCUSED=true
fi
# Determine display value (workspace 10 displays as "0")
if [ "$WORKSPACE_NUM" = "10" ]; then
DISPLAY="0"
else
DISPLAY="$WORKSPACE_NUM"
fi
# Determine visibility and styling
# Always show focused workspace (even if empty) with fixed width
# Hide non-focused empty workspaces by setting width to 0 (collapsed)
# Show non-focused non-empty workspaces with fixed width and inactive styling
if [ "$IS_FOCUSED" = "true" ]; then
# Focused workspace - always show with focused styling and bold font
${pkgs.sketchybar}/bin/sketchybar --set space.$WORKSPACE_NUM \
drawing=on \
icon="$DISPLAY" \
width=32 \
icon.padding_left=13 \
icon.padding_right=11 \
icon.align=center \
background.color=$FOCUSED_COLOR \
background.drawing=on \
icon.color=$TEXT \
icon.font="Fira Code:Bold:13.0"
elif [ "$IS_EMPTY" = "true" ]; then
# Empty workspace (not focused) - hide by collapsing width and clearing content
# Using width=0 with drawing=on so updates=when_shown continues to run the script
${pkgs.sketchybar}/bin/sketchybar --set space.$WORKSPACE_NUM \
drawing=on \
icon="" \
label="" \
width=0 \
icon.padding_left=0 \
icon.padding_right=0 \
background.drawing=off
else
# Non-empty workspace (not focused) - show with inactive styling and white text
${pkgs.sketchybar}/bin/sketchybar --set space.$WORKSPACE_NUM \
drawing=on \
icon="$DISPLAY" \
width=32 \
icon.padding_left=13 \
icon.padding_right=11 \
icon.align=center \
background.color=$ITEM_BG \
background.drawing=on \
icon.color=$TEXT \
icon.font="Fira Code:Regular:13.0"
fi
'';
};
# SketchyBar CPU monitoring plugin
home.file.".config/sketchybar/plugins/cpu.sh" = mkIf cfg.sketchybar.enable {
executable = true;
text = ''
#!/bin/bash
CORE_COUNT=$(sysctl -n machdep.cpu.thread_count)
CPU_INFO=$(ps -eo pcpu,user)
CPU_SYS=$(echo "$CPU_INFO" | grep -v $(whoami) | sed "s/[^ 0-9\.]//g" | awk "{sum+=\$1} END {print sum/(100.0 * $CORE_COUNT)}")
CPU_USER=$(echo "$CPU_INFO" | grep $(whoami) | sed "s/[^ 0-9\.]//g" | awk "{sum+=\$1} END {print sum/(100.0 * $CORE_COUNT)}")
CPU_PERCENT="$(echo "$CPU_SYS $CPU_USER" | awk '{printf "%.0f\n", ($1 + $2)*100}')"
${pkgs.sketchybar}/bin/sketchybar --set $NAME label="$CPU_PERCENT%"
'';
};
# SketchyBar memory monitoring plugin
# Shows actual memory pressure (excludes file cache/inactive pages)
home.file.".config/sketchybar/plugins/memory.sh" = mkIf cfg.sketchybar.enable {
executable = true;
text = ''
#!/bin/bash
# Use awk for all arithmetic to avoid bash integer overflow on large RAM systems
# Memory pressure = Anonymous (app memory) + Wired + Compressor RAM
# - Anonymous pages: app-allocated memory (heap, stack) - matches Activity Monitor's "App Memory"
# - Wired: kernel/system memory that can't be paged out
# - Pages occupied by compressor: actual RAM used by compressor (NOT "stored in compressor")
TOTAL_RAM=$(sysctl -n hw.memsize)
MEMORY_PERCENT=$(vm_stat | awk -v total_ram="$TOTAL_RAM" '
/page size of/ { page_size = $8 }
/Anonymous pages/ { anon = $3 + 0 }
/Pages wired/ { wired = $4 + 0 }
/Pages occupied by compressor/ { compressor = $5 + 0 }
END {
used = (anon + wired + compressor) * page_size
printf "%.0f", used / total_ram * 100
}
')
${pkgs.sketchybar}/bin/sketchybar --set $NAME label="$MEMORY_PERCENT%"
'';
};
# SketchyBar disk monitoring plugin
home.file.".config/sketchybar/plugins/disk.sh" = mkIf cfg.sketchybar.enable {
executable = true;
text = ''
#!/bin/bash
DISK_USAGE=$(df -H / | grep -v Filesystem | awk '{print $5}')
${pkgs.sketchybar}/bin/sketchybar --set $NAME label="$DISK_USAGE"
'';
};
# SketchyBar battery monitoring plugin
home.file.".config/sketchybar/plugins/battery.sh" = mkIf cfg.sketchybar.enable {
executable = true;
text = ''
#!/bin/bash
PERCENTAGE=$(pmset -g batt | grep -Eo "\d+%" | cut -d% -f1)
CHARGING=$(pmset -g batt | grep 'AC Power')
if [ "$PERCENTAGE" = "" ]; then
exit 0
fi
# Select icon based on battery level
case ''${PERCENTAGE} in
9[0-9]|100) ICON="🔋"
;;
[6-8][0-9]) ICON="🔋"
;;
[3-5][0-9]) ICON="🔋"
;;
[1-2][0-9]) ICON="🔋"
;;
*) ICON="🪫"
esac
# Show charging icon if connected to power
if [[ $CHARGING != "" ]]; then
ICON=""
fi
${pkgs.sketchybar}/bin/sketchybar --set $NAME icon="$ICON" label="''${PERCENTAGE}%"
'';
};
# SketchyBar volume monitoring plugin
home.file.".config/sketchybar/plugins/volume.sh" = mkIf cfg.sketchybar.enable {
executable = true;
text = ''
#!/bin/bash
if [ "$SENDER" = "volume_change" ]; then
VOLUME=$(osascript -e "output volume of (get volume settings)")
MUTED=$(osascript -e "output muted of (get volume settings)")
if [ "$MUTED" = "true" ]; then
ICON="🔇"
LABEL=""
else
case $VOLUME in
[6-9][0-9]|100) ICON="🔊"
;;
[3-5][0-9]) ICON="🔉"
;;
*) ICON="🔈"
esac
LABEL="$VOLUME%"
fi
${pkgs.sketchybar}/bin/sketchybar --set $NAME icon="$ICON" label="$LABEL"
fi
'';
};
# SketchyBar calendar/clock plugin
home.file.".config/sketchybar/plugins/calendar.sh" = mkIf cfg.sketchybar.enable {
executable = true;
text = ''
#!/bin/bash
${pkgs.sketchybar}/bin/sketchybar --set $NAME label="$(date '+%Y-%m-%d %H:%M')"
'';
};
# Launchd agent for auto-starting sketchybar
launchd.agents.sketchybar = mkIf cfg.sketchybar.enable {
enable = true;
config = {
ProgramArguments = [ "${pkgs.sketchybar}/bin/sketchybar" ];
RunAtLoad = true;
KeepAlive = true;
StandardOutPath = "/tmp/sketchybar.log";
StandardErrorPath = "/tmp/sketchybar.err.log";
};
};
};
}

View File

@@ -0,0 +1,11 @@
{
# Base imports for Darwin home configurations
# Includes Darwin-specific roles that only work on macOS
imports = [
../aerospace
];
# Override to use -d instead of --delete-older-than on Darwin due to launchd bug
# https://github.com/nix-community/home-manager/issues/7211
nix.gc.options = "-d";
}

View File

@@ -0,0 +1,8 @@
{
# Base imports for Linux home configurations
# Includes Linux-specific roles that require Linux-only home-manager modules
imports = [
../plasma-manager
../i3+sway
];
}

View File

@@ -24,6 +24,17 @@ in
tree
];
# Automatic garbage collection for user profile (home-manager generations).
# This complements system-level gc which only cleans system generations.
# - Linux: Uses --delete-older-than to keep 10-day rollback window
# - Darwin: Overridden to use -d in base-darwin role to avoid launchd bug
# (https://github.com/nix-community/home-manager/issues/7211)
nix.gc = {
automatic = true;
randomizedDelaySec = mkIf pkgs.stdenv.isLinux "14m";
options = lib.mkDefault "--delete-older-than 10d";
};
# Essential programs everyone needs
programs.bash = {
enable = true;
@@ -41,9 +52,9 @@ in
programs.git = {
enable = true;
userName = "John Ogle";
userEmail = "john@ogle.fyi";
extraConfig = {
settings = {
user.name = "John Ogle";
user.email = "john@ogle.fyi";
safe.directory = "/etc/nixos";
};
};
@@ -58,8 +69,11 @@ in
programs.ssh = {
enable = true;
addKeysToAgent = "yes";
enableDefaultConfig = false;
matchBlocks = {
"*" = {
addKeysToAgent = "yes";
};
"nucdeb1" = {
hostname = "nucdeb1.oglehome";
user = "root";

View File

@@ -14,7 +14,8 @@ in
home.packages = [
# Communication apps
pkgs.element-desktop
#pkgs.fluffychat #marked insecure as of nixos 25.05
# Re-enabled in 25.11 after security issues were resolved
pkgs.fluffychat
pkgs.nextcloud-talk-desktop
# For logging back into google chat

View File

@@ -1,13 +1,22 @@
{
# Shared roles that work across all platforms (Linux, Darwin, etc.)
# Platform-specific roles are imported via base-linux or base-darwin
# in each home configuration file
imports = [
./3d-printing
./base
./communication
./desktop
./development
./email
./gaming
./kdeconnect
./kubectl
./launchers
./media
./office
./sync
./tmux
./emacs
];
}

View File

@@ -81,6 +81,45 @@ in
enable = true;
};
# rbw vault unlock on login and resume from suspend
systemd.user.services.rbw-unlock-on-login = {
Unit = {
Description = "Unlock rbw vault at login";
After = [ "graphical-session.target" ];
};
Service = {
Type = "oneshot";
ExecStart = "${pkgs.rbw}/bin/rbw unlock";
Environment = "RBW_AGENT=${pkgs.rbw}/bin/rbw-agent";
# KillMode = "process" prevents systemd from killing the rbw-agent daemon
# when this oneshot service completes. The agent is spawned by rbw unlock
# and needs to persist after the service exits.
KillMode = "process";
};
Install = {
WantedBy = [ "graphical-session.target" ];
};
};
systemd.user.services.rbw-unlock-on-resume = {
Unit = {
Description = "Unlock rbw vault after resume from suspend";
After = [ "suspend.target" ];
};
Service = {
Type = "oneshot";
ExecStart = "${pkgs.rbw}/bin/rbw unlock";
Environment = "RBW_AGENT=${pkgs.rbw}/bin/rbw-agent";
# KillMode = "process" prevents systemd from killing the rbw-agent daemon
# when this oneshot service completes. The agent is spawned by rbw unlock
# and needs to persist after the service exits.
KillMode = "process";
};
Install = {
WantedBy = [ "suspend.target" ];
};
};
# KDE environment variables for proper integration
home.sessionVariables = {
QT_QPA_PLATFORMTHEME = "kde";

View File

@@ -4,22 +4,90 @@ with lib;
let
cfg = config.home.roles.development;
# Fetch the claude-plugins repository
# Update the rev to get newer versions of the commands
claudePluginsRepo = builtins.fetchGit {
url = "https://github.com/jeffh/claude-plugins.git";
# To update: change this to the latest commit hash
# You can find the latest commit at: https://github.com/jeffh/claude-plugins/commits/main
rev = "5e3e4d937162185b6d78c62022cbfd1c8ad42c4c";
ref = "main";
};
in
{
options.home.roles.development = {
enable = mkEnableOption "Enable development tools and utilities";
allowArbitraryClaudeCodeModelSelection = mkOption {
type = types.bool;
default = false;
description = ''
Whether to preserve model specifications in Claude Code humanlayer commands and agents.
When false (default), the model: line is stripped from frontmatter, allowing Claude Code
to use its default model selection.
When true, the model: specifications from the source files are preserved, allowing
commands to specify opus/sonnet/haiku explicitly.
'';
};
};
config = mkIf cfg.enable {
home.packages = [
pkgs.unstable.claude-code
pkgs.codex
pkgs.unstable.claude-code-router
pkgs.unstable.codex
# Custom packages
pkgs.custom.tea-rbw
];
programs.kubectl-secure.enable = true;
# Install Claude Code humanlayer command and agent plugins
home.activation.claudeCodeCommands = lib.hm.dag.entryAfter ["writeBoundary"] ''
# Clean up old plugin-installed commands and agents to avoid duplicates
rm -f ~/.claude/commands/humanlayer:* 2>/dev/null || true
rm -f ~/.claude/agents/humanlayer:* 2>/dev/null || true
# Create directories if they don't exist
mkdir -p ~/.claude/commands
mkdir -p ~/.claude/agents
# Copy all humanlayer command files and remove model specifications
for file in ${claudePluginsRepo}/humanlayer/commands/*.md; do
if [ -f "$file" ]; then
filename=$(basename "$file" .md)
dest="$HOME/.claude/commands/humanlayer:''${filename}.md"
# Copy file and conditionally remove the "model:" line from frontmatter
${if cfg.allowArbitraryClaudeCodeModelSelection
then "cp \"$file\" \"$dest\""
else "${pkgs.gnused}/bin/sed '/^model:/d' \"$file\" > \"$dest\""
}
fi
done
# Copy all humanlayer agent files and remove model specifications
for file in ${claudePluginsRepo}/humanlayer/agents/*.md; do
if [ -f "$file" ]; then
filename=$(basename "$file" .md)
dest="$HOME/.claude/agents/humanlayer:''${filename}.md"
# Copy file and conditionally remove the "model:" line from frontmatter
${if cfg.allowArbitraryClaudeCodeModelSelection
then "cp \"$file\" \"$dest\""
else "${pkgs.gnused}/bin/sed '/^model:/d' \"$file\" > \"$dest\""
}
fi
done
$DRY_RUN_CMD echo "Claude Code humanlayer commands and agents installed successfully${
if cfg.allowArbitraryClaudeCodeModelSelection
then " (model specifications preserved)"
else " (model selection removed)"
}"
'';
# Note: modules must be imported at top-level home config
};

View File

@@ -3,11 +3,13 @@
with lib;
let
cfg = config.home.roles.emacs;
doomEmacs = pkgs.fetchFromGitHub {
owner = "doomemacs";
repo = "doomemacs";
rev = "8f55404781edacf66fa330205533b002de3fb5ee";
sha256 = "sha256-vHwgENjip2+AFzs4oZfnKEAJKwf5Zid7fakImvxxQUw=";
rev = "762f47805ac2a6411e11747f86f7c19a03da326e";
sha256 = "sha256-0w0eXGB2cgxu/hr5wTiJSZDJw0NF+fZvLbzEylH5URU=";
};
# Shared emacs packages
@@ -17,20 +19,23 @@ let
];
# Default emacs configuration with vterm support
defaultEmacsPackage =
if pkgs.stdenv.isDarwin
defaultEmacsPackage =
if pkgs.stdenv.isDarwin
then pkgs.emacs-macport.pkgs.withPackages emacsPackages
else pkgs.emacs.pkgs.withPackages emacsPackages;
in
{
config = {
options.home.roles.emacs = {
enable = mkEnableOption "Doom Emacs with vterm and tree-sitter support";
};
config = mkIf cfg.enable {
home.packages = [
pkgs.emacs-all-the-icons-fonts
pkgs.fira-code
pkgs.fontconfig
pkgs.graphviz
pkgs.isort
#pkgs.libvterm # native vterm library
pkgs.nerd-fonts.fira-code
pkgs.nerd-fonts.droid-sans-mono
pkgs.nil # nix lsp language server
@@ -66,7 +71,7 @@ in
home.activation.doomConfig = lib.hm.dag.entryAfter ["writeBoundary"] ''
# Always remove and recreate the symlink to ensure it points to the source directory
rm -rf "${config.xdg.configHome}/doom"
ln -sf "${config.home.homeDirectory}/nixos-configs/home/modules/emacs/doom" "${config.xdg.configHome}/doom"
ln -sf "${config.home.homeDirectory}/nixos-configs/home/roles/emacs/doom" "${config.xdg.configHome}/doom"
'';
};
}

View File

@@ -159,6 +159,32 @@
(dolist (module '("bbdb" "buffer" "elisp" "emacs" "gnus" "os" "search-and-replace" "url"))
(gptel-tool-library-load-module module)))
;; Notmuch email configuration
(after! notmuch
(setq notmuch-search-oldest-first nil
notmuch-show-logo nil
notmuch-fcc-dirs "proton/Sent"
;; User identity
user-mail-address "john@ogle.fyi"
user-full-name "John Ogle"
;; Sending mail via msmtp
message-send-mail-function 'message-send-mail-with-sendmail
sendmail-program (executable-find "msmtp")
message-sendmail-envelope-from 'header
mail-envelope-from 'header
mail-specify-envelope-from t
;; Saved searches for quick access
notmuch-saved-searches
'((:name "inbox" :query "tag:inbox" :key "i")
(:name "unread" :query "tag:unread" :key "u")
(:name "flagged" :query "tag:flagged" :key "f")
(:name "sent" :query "tag:sent" :key "t")
(:name "drafts" :query "tag:draft" :key "d")
(:name "all" :query "*" :key "a"))))
;; Whenever you reconfigure a package, make sure to wrap your config in an
;; `after!' block, otherwise Doom's defaults may override your settings. E.g.
;;

View File

@@ -177,7 +177,7 @@
:email
;;(mu4e +org +gmail)
;;notmuch
notmuch
;;(wanderlust +gmail)
:app

View File

@@ -0,0 +1,128 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.home.roles.email;
in
{
options.home.roles.email = {
enable = mkEnableOption "Enable email with notmuch, mbsync, and msmtp";
};
config = mkIf cfg.enable {
home.packages = with pkgs; [
isync # provides mbsync for IMAP sync
msmtp # for SMTP sending
notmuch # email indexing and search
openssl # for certificate management
];
# Ensure Mail directory exists
home.file."Mail/.keep".text = "";
# mbsync configuration
home.file.".mbsyncrc".text = ''
# IMAP Account Configuration
IMAPAccount proton
Host proton.johnogle.info
Port 143
User john@ogle.fyi
PassCmd "${pkgs.rbw}/bin/rbw get proton.johnogle.info"
TLSType STARTTLS
AuthMechs PLAIN
# Remote Storage
IMAPStore proton-remote
Account proton
# Local Storage
MaildirStore proton-local
Path ~/Mail/
Inbox ~/Mail/INBOX
SubFolders Verbatim
# Channel Configuration - Sync All
Channel proton
Far :proton-remote:
Near :proton-local:
Patterns *
Create Both
Expunge Both
SyncState *
'';
# Notmuch configuration
home.file.".notmuch-config".text = ''
[database]
path=${config.home.homeDirectory}/Mail
[user]
name=John Ogle
primary_email=john@ogle.fyi
[new]
tags=unread;inbox;
ignore=
[search]
exclude_tags=deleted;spam;
[maildir]
synchronize_flags=true
'';
# msmtp configuration
home.file.".msmtprc".text = ''
# Default settings
defaults
auth plain
tls on
tls_starttls on
tls_trust_file /etc/ssl/certs/ca-certificates.crt
logfile ${config.home.homeDirectory}/.msmtp.log
# Proton mail account
account proton
host proton.johnogle.info
port 25
from john@ogle.fyi
user john@ogle.fyi
passwordeval rbw get proton.johnogle.info
# Set default account
account default : proton
'';
# Systemd service for mail sync
systemd.user.services.mbsync = {
Unit = {
Description = "Mailbox synchronization service";
After = [ "network-online.target" ];
Wants = [ "network-online.target" ];
};
Service = {
Type = "oneshot";
ExecStart = "${pkgs.bash}/bin/bash -c '${pkgs.isync}/bin/mbsync -a && ${pkgs.notmuch}/bin/notmuch new'";
Environment = "PATH=${pkgs.rbw}/bin:${pkgs.coreutils}/bin";
StandardOutput = "journal";
StandardError = "journal";
};
};
# Systemd timer for automatic sync
systemd.user.timers.mbsync = {
Unit = {
Description = "Mailbox synchronization timer";
};
Timer = {
OnBootSec = "2min";
OnUnitActiveSec = "5min";
Unit = "mbsync.service";
};
Install = {
WantedBy = [ "timers.target" ];
};
};
};
}

View File

@@ -3,7 +3,7 @@
with lib;
let
cfg = config.home.i3_sway;
cfg = config.home.roles.i3_sway;
shared_config = recursiveUpdate rec {
modifier = "Mod4";
@@ -14,6 +14,9 @@ let
"${shared_config.modifier}+Return" = "exec ${terminal}";
"${shared_config.modifier}+Shift+q" = "kill";
"${shared_config.modifier}+a" = "focus parent";
"${shared_config.modifier}+Shift+a" = "focus child";
"${shared_config.modifier}+h" = "focus left";
"${shared_config.modifier}+j" = "focus down";
"${shared_config.modifier}+k" = "focus up";
@@ -45,8 +48,6 @@ let
"${shared_config.modifier}+Shift+space" = "floating toggle";
"${shared_config.modifier}+space" = "focus mode_toggle";
"${shared_config.modifier}+a" = "focus parent";
"${shared_config.modifier}+Shift+minus" = "move scratchpad";
"${shared_config.modifier}+minus" = "scratchpad show";
@@ -92,19 +93,29 @@ let
};
} cfg.extraSharedConfig;
in {
options.home.i3_sway = {
options.home.roles.i3_sway = {
enable = mkEnableOption "i3 and Sway tiling window managers with waybar and rofi";
extraSharedConfig = mkOption {
type = types.attrs;
default = {};
description = "Extra configuration shared between i3 and sway";
};
extraI3Config = mkOption {
type = types.attrs;
default = {};
description = "Extra i3-specific configuration";
};
extraSwayConfig = mkOption {
type = types.attrs;
default = {};
description = "Extra sway-specific configuration";
};
};
config = {
config = mkIf cfg.enable {
# i3blocks configuration file
home.file.".config/i3blocks/config".text = ''
# i3blocks config - replicating waybar setup
@@ -316,6 +327,7 @@ in {
};
in {
enable = true;
extraOptions = [ "--unsupported-gpu" ];
config = recursiveUpdate base_sway_config cfg.extraSwayConfig;
};
@@ -331,7 +343,7 @@ in {
modules-left = [ "sway/workspaces" "sway/mode" ];
modules-center = [ ];
modules-right = [ "disk" "cpu" "memory" "pulseaudio" "backlight" "network" "battery" "tray" "clock" ];
modules-right = [ "disk" "cpu" "memory" "pulseaudio" "custom/backlight-ddc" "backlight" "network" "battery" "tray" "clock" ];
"sway/workspaces" = {
disable-scroll = true;
@@ -388,6 +400,23 @@ in {
tooltip = false;
};
"custom/backlight-ddc" = {
exec = pkgs.writeShellScript "waybar-backlight-ddc" ''
if command -v ddcutil &>/dev/null; then
# Display current brightness
brightness=$(ddcutil getvcp 10 --brief 2>/dev/null | awk '{print $4}')
if [ -n "$brightness" ]; then
echo " $brightness%"
fi
fi
'';
interval = 5;
format = "{}";
on-scroll-up = "ddcutil setvcp 10 + 5 2>/dev/null &";
on-scroll-down = "ddcutil setvcp 10 - 5 2>/dev/null &";
tooltip = false;
};
"network" = {
format-wifi = "📶 {essid} ({signalStrength}%)";
format-ethernet = "🔌 {ipaddr}";
@@ -419,7 +448,7 @@ in {
#workspaces button {
padding: 0 8px;
background-color: transparent;
background-color: #333333;
color: #ffffff;
border: none;
}

View File

@@ -1,13 +1,13 @@
{ config, lib, pkgs, ... }:
{ config, lib, pkgs, globalInputs, system, ... }:
with lib;
let
cfg = config.programs.kubectl-secure;
cfg = config.home.roles.kubectl;
in
{
options.programs.kubectl-secure = {
enable = mkEnableOption "secure kubectl configuration with Bitwarden integration";
options.home.roles.kubectl = {
enable = mkEnableOption "management tools for the homelab k3s oglenet cluster with secure Bitwarden integration";
};
config = mkIf cfg.enable {
@@ -21,29 +21,29 @@ in
programs.bash.initExtra = mkAfter ''
# Kubectl secure session management
export KUBECTL_SESSION_DIR="/dev/shm/kubectl-$$"
kube-select() {
if [[ $# -ne 1 ]]; then
echo "Usage: kube-select <context-name>"
echo "Available contexts: $(kube-list)"
return 1
fi
local context="$1"
# Clean up any existing session first
kube-clear 2>/dev/null
# Create new session directory
mkdir -p "$KUBECTL_SESSION_DIR"
chmod 700 "$KUBECTL_SESSION_DIR"
# Set cleanup trap for this shell session
trap "rm -rf '$KUBECTL_SESSION_DIR' 2>/dev/null" EXIT
# Set KUBECONFIG for this session
export KUBECONFIG="$KUBECTL_SESSION_DIR/config"
# Load config from Bitwarden secure notes
if ! rbw get "kubectl-$context" > "$KUBECONFIG" 2>/dev/null; then
echo "Error: Could not retrieve kubectl-$context from Bitwarden"
@@ -51,37 +51,37 @@ in
kube-clear
return 1
fi
# Verify the kubeconfig is valid
if ! kubectl config view >/dev/null 2>&1; then
echo "Error: Invalid kubeconfig retrieved from Bitwarden"
kube-clear
return 1
fi
echo " Loaded kubectl context: $context (session: $$)"
echo " Config location: $KUBECONFIG"
}
kube-list() {
echo "Available kubectl contexts in Bitwarden:"
rbw search kubectl- 2>/dev/null | grep "^kubectl-" | sed 's/^kubectl-/ - /' || echo " (none found or rbw not accessible)"
}
kube-clear() {
if [[ -n "$KUBECTL_TIMEOUT_PID" ]]; then
kill "$KUBECTL_TIMEOUT_PID" 2>/dev/null
unset KUBECTL_TIMEOUT_PID
fi
if [[ -d "$KUBECTL_SESSION_DIR" ]]; then
rm -rf "$KUBECTL_SESSION_DIR"
echo "Cleared kubectl session ($$)"
fi
unset KUBECONFIG
}
kube-status() {
if [[ -f "$KUBECONFIG" ]]; then
local current_context
@@ -89,7 +89,7 @@ in
if [[ -n "$current_context" ]]; then
echo "Active kubectl context: $current_context"
echo "Session: $$ | Config: $KUBECONFIG"
# Show cluster info
local cluster_server
cluster_server=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}' 2>/dev/null)
@@ -128,33 +128,33 @@ in
echo "Note: Kubeconfigs are stored as secure notes in Bitwarden"
}
'';
programs.zsh.initExtra = mkAfter ''
# Kubectl secure session management (zsh)
export KUBECTL_SESSION_DIR="/dev/shm/kubectl-$$"
kube-select() {
if [[ $# -ne 1 ]]; then
echo "Usage: kube-select <context-name>"
echo "Available contexts: $(kube-list)"
return 1
fi
local context="$1"
# Clean up any existing session first
kube-clear 2>/dev/null
# Create new session directory
mkdir -p "$KUBECTL_SESSION_DIR"
chmod 700 "$KUBECTL_SESSION_DIR"
# Set cleanup trap for this shell session
trap "rm -rf '$KUBECTL_SESSION_DIR' 2>/dev/null" EXIT
# Set KUBECONFIG for this session
export KUBECONFIG="$KUBECTL_SESSION_DIR/config"
# Load config from Bitwarden secure notes
if ! rbw get "kubectl-$context" > "$KUBECONFIG" 2>/dev/null; then
echo "Error: Could not retrieve kubectl-$context from Bitwarden"
@@ -162,43 +162,37 @@ in
kube-clear
return 1
fi
# Verify the kubeconfig is valid
if ! kubectl config view >/dev/null 2>&1; then
echo "Error: Invalid kubeconfig retrieved from Bitwarden"
kube-clear
return 1
fi
echo " Loaded kubectl context: $context (session: $$)"
echo " Config location: $KUBECONFIG"
# Optional: Set timeout cleanup
if [[ ${toString cfg.sessionTimeout} -gt 0 ]]; then
(sleep ${toString cfg.sessionTimeout}; kube-clear 2>/dev/null) &
export KUBECTL_TIMEOUT_PID=$!
fi
}
kube-list() {
echo "Available kubectl contexts in Bitwarden:"
rbw search kubectl- 2>/dev/null | grep "^kubectl-" | sed 's/^kubectl-/ - /' || echo " (none found or rbw not accessible)"
}
kube-clear() {
if [[ -n "$KUBECTL_TIMEOUT_PID" ]]; then
kill "$KUBECTL_TIMEOUT_PID" 2>/dev/null
unset KUBECTL_TIMEOUT_PID
fi
if [[ -d "$KUBECTL_SESSION_DIR" ]]; then
rm -rf "$KUBECTL_SESSION_DIR"
echo "Cleared kubectl session ($$)"
fi
unset KUBECONFIG
}
kube-status() {
if [[ -f "$KUBECONFIG" ]]; then
local current_context
@@ -206,7 +200,7 @@ in
if [[ -n "$current_context" ]]; then
echo "Active kubectl context: $current_context"
echo "Session: $$ | Config: $KUBECONFIG"
# Show cluster info
local cluster_server
cluster_server=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}' 2>/dev/null)

View File

@@ -0,0 +1,36 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.home.roles.launchers;
# Generate a wrapper script for a package
makeLauncher = packageName: pkgs.writeShellScriptBin packageName ''
exec env NIXPKGS_ALLOW_UNFREE=1 ${pkgs.nix}/bin/nix run --impure nixpkgs#${packageName} -- "$@"
'';
# Generate all launcher scripts from the package list
launcherPackages = map makeLauncher cfg.packages;
in
{
options.home.roles.launchers = {
enable = mkEnableOption "wrapper launchers for excluded packages";
packages = mkOption {
type = types.listOf types.str;
default = [];
example = [ "steam" "libreoffice" "lutris" ];
description = ''
List of package names to create launcher wrappers for.
Each wrapper will run: NIXPKGS_ALLOW_UNFREE=1 nix run --impure nixpkgs#<package>
This is useful for occasionally running packages without permanently installing them.
'';
};
};
config = mkIf cfg.enable {
home.packages = launcherPackages;
};
}

View File

@@ -0,0 +1,188 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.home.roles.plasma-manager;
in
{
options.home.roles.plasma-manager = {
enable = mkEnableOption "KDE Plasma desktop environment configuration";
};
config = mkIf cfg.enable {
# The current KDE config can be output with the command:
# nix run github:nix-community/plasma-manager
#
# Plasma-manager options documentation
# https://nix-community.github.io/plasma-manager/options.xhtml
#
# TODO: (ambitious) Add Kmail support to plasma-manager
programs.plasma = {
enable = true;
overrideConfig = true;
hotkeys.commands."launch-ghostty" = {
name = "Launch Ghostty";
key = "Meta+Return";
command = "ghostty";
};
shortcuts = {
kmix = {
"decrease_microphone_volume" = "Microphone Volume Down";
"decrease_volume" = "Volume Down";
"decrease_volume_small" = "Shift+Volume Down";
"increase_microphone_volume" = "Microphone Volume Up";
"increase_volume" = "Volume Up";
"increase_volume_small" = "Shift+Volume Up";
"mic_mute" = ["Microphone Mute" "Meta+Volume Mute,Microphone Mute" "Meta+Volume Mute,Mute Microphone"];
"mute" = "Volume Mute";
};
mediacontrol = {
"mediavolumedown" = "none,,Media volume down";
"mediavolumeup" = "none,,Media volume up";
"nextmedia" = "Media Next";
"pausemedia" = "Media Pause";
"playmedia" = "none,,Play media playback";
"playpausemedia" = "Media Play";
"previousmedia" = "Media Previous";
"stopmedia" = "Media Stop";
};
ksmserver = {
"Lock Session" = ["Meta+Ctrl+Q" "Screensaver" "Screensaver,Lock Session"];
};
kwin = {
"Window Close" = "Meta+Shift+Q";
"Kill Window" = "Meta+Ctrl+Esc";
"Window Operations Menu" = "Alt+F3";
"Window Resize" = "Meta+R,,Resize Window";
"Overview" = "Meta+Ctrl+W";
"Grid View" = "Meta+G";
"Edit Tiles" = "Meta+T";
"Activate Window Demanding Attention" = "Meta+Ctrl+A";
"Show Desktop" = "Meta+Ctrl+D";
"Walk Through Windows" = "Alt+Tab";
"Walk Through Windows (Reverse)" = "Alt+Shift+Tab";
"Walk Through Windows of Current Application" = "Alt+`";
"Walk Through Windows of Current Application (Reverse)" = "Alt+~";
"Window Quick Tile Bottom" = "Meta+Down";
"Window Quick Tile Left" = "Meta+Left";
"Window Quick Tile Right" = "Meta+Right";
"Window Quick Tile Top" = "Meta+Up";
"Switch to Desktop 1" = "Meta+1";
"Switch to Desktop 2" = "Meta+2";
"Switch to Desktop 3" = "Meta+3";
"Switch to Desktop 4" = "Meta+4";
"Switch to Desktop 5" = "Meta+5";
"Switch to Desktop 6" = "Meta+6";
"Switch to Desktop 7" = "Meta+7";
"Switch to Desktop 8" = "Meta+8";
"Switch to Desktop 9" = "Meta+9";
"Switch to Desktop 10" = "Meta+0";
"Window to Desktop 1" = "Meta+!"; # Meta+Shift+1
"Window to Desktop 2" = "Meta+@"; # Meta+Shift+2
"Window to Desktop 3" = "Meta+#"; # Meta+Shift+3
"Window to Desktop 4" = "Meta+$"; # Meta+Shift+4
"Window to Desktop 5" = "Meta+%"; # Meta+Shift+5
"Window to Desktop 6" = "Meta+^"; # Meta+Shift+6
"Window to Desktop 7" = "Meta+&"; # Meta+Shift+7
"Window to Desktop 8" = "Meta+*"; # Meta+Shift+8
"Window to Desktop 9" = "Meta+("; # Meta+Shift+9
"Window to Desktop 10" = "Meta+)"; # Meta+Shift+0
"view_actual_size" = "Meta+Ctrl+=";
"view_zoom_in" = ["Meta++" "Meta+=,Meta++" "Meta+=,Zoom In"];
"view_zoom_out" = "Meta+-";
};
"org_kde_powerdevil"."Decrease Keyboard Brightness" = "Keyboard Brightness Down";
"org_kde_powerdevil"."Decrease Screen Brightness" = "Monitor Brightness Down";
"org_kde_powerdevil"."Decrease Screen Brightness Small" = "Shift+Monitor Brightness Down";
"org_kde_powerdevil"."Hibernate" = "Hibernate";
"org_kde_powerdevil"."Increase Keyboard Brightness" = "Keyboard Brightness Up";
"org_kde_powerdevil"."Increase Screen Brightness" = "Monitor Brightness Up";
"org_kde_powerdevil"."Increase Screen Brightness Small" = "Shift+Monitor Brightness Up";
"org_kde_powerdevil"."PowerDown" = "Power Down";
"org_kde_powerdevil"."PowerOff" = "Power Off";
"org_kde_powerdevil"."Sleep" = "Sleep";
"org_kde_powerdevil"."Toggle Keyboard Backlight" = "Keyboard Light On/Off";
"org_kde_powerdevil"."Turn Off Screen" = [ ];
"org_kde_powerdevil"."powerProfile" = ["Battery" "Meta+B,Battery" "Meta+B,Switch Power Profile"];
plasmashell = {
"activate application launcher" = ["Meta" "Alt+F1,Meta" "Alt+F1,Activate Application Launcher"];
"activate task manager entry 1" = "none,,";
"activate task manager entry 2" = "none,,";
"activate task manager entry 3" = "none,,";
"activate task manager entry 4" = "none,,";
"activate task manager entry 5" = "none,,";
"activate task manager entry 6" = "none,,";
"activate task manager entry 7" = "none,,";
"activate task manager entry 8" = "none,,";
"activate task manager entry 9" = "none,,";
"activate task manager entry 10" = "none,,";
"show activity switcher" = "none,,";
};
};
configFile = {
kwinrc.Desktops.Number = {
value = 10;
immutable = true;
};
# Enable KWin tiling features
kwinrc.Tiling = {
# Enable tiling functionality
"padding" = 4;
};
# Enable krohnkite plugin automatically
kwinrc.Plugins = {
krohnkiteEnabled = true;
};
kwinrc.Effect-overview = {
# Configure overview effect for better tiling workflow
BorderActivate = 9; # Top-left corner activation
};
kcminputrc.Libinput = {
AccelerationProfile = "adaptive";
PointerAcceleration = 0.5;
};
kcminputrc.Mouse = {
X11LibInputXAccelProfileFlat = false;
XLbInptAccelProfileFlat = false;
};
kdeglobals.KDE.LookAndFeelPackage = "org.kde.breezedark.desktop";
# Focus follows mouse configuration
kwinrc.Windows = {
FocusPolicy = "FocusFollowsMouse";
AutoRaise = true; # Set to true if you want windows to auto-raise on focus
AutoRaiseInterval = 750; # Delay in ms before auto-raise (if enabled)
DelayFocusInterval = 0; # Delay in ms before focus follows mouse
};
# Desktop wallpaper configuration
plasma-localerc.Formats.LANG = "en_US.UTF-8";
# Set wallpaper for all desktops
plasmarc.Wallpapers.usersWallpapers = "${../../wallpapers/metroid-samus-returns-kz-3440x1440.jpg}";
};
};
};
}

View File

@@ -0,0 +1,62 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.home.roles.tmux;
tokyo-night = pkgs.tmuxPlugins.mkTmuxPlugin {
pluginName = "tokyo-night";
rtpFilePath = "tokyo-night.tmux";
version = "1.6.1";
src = pkgs.fetchFromGitHub {
owner = "janoamaral";
repo = "tokyo-night-tmux";
rev = "d610ced20d5f602a7995854931440e4a1e0ab780";
sha256 = "sha256-17vEgkL7C51p/l5gpT9dkOy0bY9n8l0/LV51mR1k+V8=";
};
};
in
{
options.home.roles.tmux = {
enable = mkEnableOption "tmux terminal multiplexer with Tokyo Night theme";
};
config = mkIf cfg.enable {
programs.tmux.enable = true;
programs.tmux.terminal = "tmux-direct";
programs.tmux.keyMode = "vi";
programs.tmux.escapeTime = 0;
programs.tmux.mouse = true;
programs.tmux.newSession = true;
programs.tmux.historyLimit = 50000;
programs.tmux.clock24 = true;
programs.tmux.baseIndex = 1;
programs.tmux.prefix = "M-\\\\";
programs.tmux.plugins = with pkgs; [
tmuxPlugins.cpu
tmuxPlugins.battery
tmuxPlugins.better-mouse-mode
tmuxPlugins.net-speed
tmuxPlugins.online-status
tmuxPlugins.pain-control
tmuxPlugins.tilish
tmuxPlugins.yank
{
plugin = tmuxPlugins.resurrect;
extraConfig = "set -g @resurrect-strategy-nvim 'session'";
}
{
plugin = tmuxPlugins.continuum;
extraConfig = ''
set -g @continuum-restore 'on'
set -g @continuum-save-interval '15' # minutes
'';
}
tokyo-night
];
};
}

View File

@@ -26,6 +26,7 @@ with lib;
enable = true;
autologin = true;
wayland = true;
jellyfinScaleFactor = 2.5;
};
users.enable = true;
};
@@ -39,12 +40,7 @@ with lib;
services.xserver.videoDrivers = [ "amdgpu" ];
hardware.graphics.enable = true;
hardware.graphics.enable32Bit = true;
hardware.graphics.extraPackages = with pkgs; [
amdvlk
];
hardware.graphics.extraPackages32 = with pkgs; [
driversi686Linux.amdvlk
];
# RADV (AMD's Vulkan driver) is now enabled by default, amdvlk was removed
# This option defines the first version of NixOS you have installed on this particular machine,
# and is used to maintain compatibility with application data (e.g. databases) created on older NixOS versions.

View File

@@ -15,7 +15,7 @@
desktop = {
enable = true;
wayland = true;
gaming.enable = false;
gaming.enable = true;
kde = true;
sddm = true;
};

View File

@@ -2,6 +2,7 @@
{
imports = [
./hardware-configuration.nix
../../roles/desktop/steamos.nix
];
roles = {

View File

@@ -36,7 +36,7 @@
extraPackages = with pkgs; [
mesa
libvdpau-va-gl
vaapiVdpau
libva-vdpau-driver
];
};
environment.sessionVariables = {

View File

@@ -7,10 +7,10 @@
with lib;
{
imports =
[ # Include the results of the hardware scan.
./hardware-configuration.nix
];
imports = [
./hardware-configuration.nix
./virtual-surround.nix
];
roles = {
audio.enable = true;

View File

@@ -0,0 +1,132 @@
# Virtual 4.1 surround sound setup
# Routes FL/FR to AmazonBasics USB speaker, RL/RR to Fosi BT20A PRO Bluetooth speaker
{ pkgs, ... }:
{
services.pipewire.extraConfig.pipewire."10-virtual-surround" = {
"context.objects" = [
{
factory = "adapter";
args = {
"factory.name" = "support.null-audio-sink";
"node.name" = "virtual_surround_sink";
"node.description" = "Virtual 4.1 Surround (AmazonBasics + Fosi)";
"media.class" = "Audio/Sink";
"audio.position" = [ "FL" "FR" "RL" "RR" "LFE" ];
"monitor.channel-volumes" = true;
};
}
];
"context.modules" = [
{
name = "libpipewire-module-loopback";
args = {
"node.description" = "Route Front to AmazonBasics";
"capture.props" = {
"node.name" = "route_front_capture";
"audio.position" = [ "FL" "FR" ];
"stream.dont-remix" = true;
"node.passive" = true;
};
"playback.props" = {
"node.name" = "route_front_playback";
"node.target" = "alsa_output.usb-C-Media_Electronics_Inc._AmazonBasics_Professional_Mic_2-00.analog-stereo";
"audio.position" = [ "FL" "FR" ];
"stream.dont-remix" = true;
};
};
}
{
name = "libpipewire-module-loopback";
args = {
"node.description" = "Route Rear to Fosi Audio";
"capture.props" = {
"node.name" = "route_rear_capture";
"audio.position" = [ "RL" "RR" ];
"stream.dont-remix" = true;
"node.passive" = true;
};
"playback.props" = {
"node.name" = "route_rear_playback";
"node.target" = "bluez_output.F4_4E_FD_FB_58_62.1";
"audio.position" = [ "FL" "FR" ];
"stream.dont-remix" = true;
};
};
}
{
name = "libpipewire-module-loopback";
args = {
"node.description" = "Route Subwoofer to AmazonBasics";
"capture.props" = {
"node.name" = "route_lfe_capture";
"audio.position" = [ "LFE" ];
"stream.dont-remix" = true;
"node.passive" = true;
};
"playback.props" = {
"node.name" = "route_lfe_playback";
"node.target" = "alsa_output.usb-C-Media_Electronics_Inc._AmazonBasics_Professional_Mic_2-00.analog-stereo";
"audio.position" = [ "MONO" ];
"stream.dont-remix" = false;
};
};
}
];
};
# Systemd services to fix PipeWire loopback routing for virtual surround
systemd.user.services.pipewire-surround-link = {
description = "Link virtual surround sink to loopback captures";
after = [ "pipewire.service" "wireplumber.service" ];
requires = [ "pipewire.service" "wireplumber.service" ];
wantedBy = [ "pipewire.service" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = false;
ExecStart = pkgs.writeShellScript "surround-link" ''
sleep 2
# Disconnect wrong connections
${pkgs.pipewire}/bin/pw-link -d alsa_input.pci-0000_00_1f.3.pro-input-2:capture_AUX0 route_front_capture:input_FL 2>/dev/null || true
${pkgs.pipewire}/bin/pw-link -d alsa_input.pci-0000_00_1f.3.pro-input-2:capture_AUX1 route_front_capture:input_FR 2>/dev/null || true
${pkgs.pipewire}/bin/pw-link -d alsa_input.pci-0000_00_1f.3.pro-input-2:capture_AUX0 route_rear_capture:input_RL 2>/dev/null || true
${pkgs.pipewire}/bin/pw-link -d alsa_input.pci-0000_00_1f.3.pro-input-2:capture_AUX1 route_rear_capture:input_RR 2>/dev/null || true
${pkgs.pipewire}/bin/pw-link -d alsa_input.pci-0000_00_1f.3.pro-input-2:capture_AUX0 route_lfe_capture:input_LFE 2>/dev/null || true
# Create correct connections
${pkgs.pipewire}/bin/pw-link virtual_surround_sink:monitor_FL route_front_capture:input_FL 2>/dev/null || true
${pkgs.pipewire}/bin/pw-link virtual_surround_sink:monitor_FR route_front_capture:input_FR 2>/dev/null || true
${pkgs.pipewire}/bin/pw-link virtual_surround_sink:monitor_RL route_rear_capture:input_RL 2>/dev/null || true
${pkgs.pipewire}/bin/pw-link virtual_surround_sink:monitor_RR route_rear_capture:input_RR 2>/dev/null || true
${pkgs.pipewire}/bin/pw-link virtual_surround_sink:monitor_LFE route_lfe_capture:input_LFE 2>/dev/null || true
'';
};
};
systemd.user.services.pipewire-surround-link-check = {
description = "Check and fix surround sink links";
after = [ "pipewire.service" "wireplumber.service" ];
serviceConfig = {
Type = "oneshot";
ExecStart = pkgs.writeShellScript "surround-link-check" ''
if ${pkgs.pipewire}/bin/pw-cli ls Node 2>/dev/null | grep -q "bluez_output.F4_4E_FD_FB_58_62"; then
if ${pkgs.pipewire}/bin/pw-link -l 2>/dev/null | grep -q "route_front_capture:input_FL.*alsa_input"; then
${pkgs.systemd}/bin/systemctl --user start pipewire-surround-link.service
fi
if ! ${pkgs.pipewire}/bin/pw-link -l 2>/dev/null | grep -q "virtual_surround_sink:monitor_FL.*route_front_capture"; then
${pkgs.systemd}/bin/systemctl --user start pipewire-surround-link.service
fi
fi
'';
};
};
systemd.user.timers.pipewire-surround-link-check = {
description = "Periodically check surround sink links";
wantedBy = [ "default.target" ];
timerConfig = {
OnStartupSec = "10s";
OnUnitActiveSec = "10s";
Unit = "pipewire-surround-link-check.service";
};
};
}

View File

@@ -0,0 +1,116 @@
# claude-cli
Custom Nix package for Claude Code CLI.
## Why This Package Exists
The official `claude-code` package in nixpkgs tries to fetch from npm registry, which is blocked by Block's corporate security (Cloudflare Teams dependency confusion protection). This custom package fetches directly from Anthropic's Google Cloud Storage distribution, bypassing the npm registry entirely.
## Updating to a New Version
### Automated Update (Recommended)
Run the update script to automatically fetch and update to the latest version:
```bash
cd packages/claude-cli
./update.sh
```
The script will:
- Fetch the latest version from Homebrew cask
- Update version and all SHA256 hashes in default.nix
- Show you what changed
For a dry-run to see what would change:
```bash
./update.sh --dry-run
```
After the script completes, follow the "Test the Build" steps below.
### Manual Update
If you prefer to update manually, or if the automated script fails:
#### 1. Find the Latest Version and Hashes
Check the Homebrew cask formula for the latest version info:
```bash
curl -s "https://raw.githubusercontent.com/Homebrew/homebrew-cask/HEAD/Casks/c/claude-code.rb" | head -50
```
This will show:
- The latest `version` number
- SHA256 hashes for all platforms (`arm64`, `x86_64`, `x86_64_linux`, `arm64_linux`)
#### 2. Update default.nix
Edit `default.nix` and update:
1. The `version` variable (line 9):
```nix
version = "2.0.51"; # Update this
```
2. All four platform sha256 hashes in the `srcs` attribute set (lines 11-27):
```nix
aarch64-darwin = {
sha256 = "..."; # Update from Homebrew cask "arm:" value
};
x86_64-darwin = {
sha256 = "..."; # Update from Homebrew cask "x86_64:" value
};
x86_64-linux = {
sha256 = "..."; # Update from Homebrew cask "x86_64_linux:" value
};
aarch64-linux = {
sha256 = "..."; # Update from Homebrew cask "arm64_linux:" value
};
```
#### 3. Test the Build
Before committing, test that the package builds successfully:
```bash
NIXPKGS_ALLOW_UNFREE=1 nix-build -E 'with import <nixpkgs> { config.allowUnfree = true; }; callPackage ./packages/claude-cli {}'
```
Verify the version:
```bash
./result/bin/claude --version
```
Clean up the test build:
```bash
rm result
```
#### 4. Deploy
Commit your changes and rebuild:
```bash
git add packages/claude-cli/
git commit -m "claude-cli: Update to version X.Y.Z"
darwin-rebuild switch --flake .#blkfv4yf49kt7
```
## Alternative: Automated Hash Fetching
If you prefer to fetch hashes automatically, you can use `nix-prefetch-url`:
```bash
# For macOS ARM64 (your current platform)
nix-prefetch-url "https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/VERSION/darwin-arm64/claude"
# For other platforms, replace VERSION and adjust the platform string:
# darwin-x64, linux-x64, linux-arm64
```
This will download the file and output the SHA256 hash.

View File

@@ -0,0 +1,60 @@
{ lib
, stdenv
, fetchurl
, autoPatchelfHook
}:
let
version = "2.0.75";
srcs = {
aarch64-darwin = {
url = "https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/${version}/darwin-arm64/claude";
sha256 = "a96eb18218e112486b7ecebd1551d927ffb310ab5fb06d2e8db25fb31367537e";
};
x86_64-darwin = {
url = "https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/${version}/darwin-x64/claude";
sha256 = "e27313053d3268a0bc1e0080f8c2ef7155325f0a95e72971163eef698a71e829";
};
x86_64-linux = {
url = "https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/${version}/linux-x64/claude";
sha256 = "62160f8766681d8c933e9133398d3dde6ad0df08038881a66eddb993b4b6a33f";
};
aarch64-linux = {
url = "https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/${version}/linux-arm64/claude";
sha256 = "681fbd1a84b2de883dc954441693766b43ea4faafb3e72b88c99a33645cd3507";
};
};
src = srcs.${stdenv.hostPlatform.system} or (throw "Unsupported system: ${stdenv.hostPlatform.system}");
in stdenv.mkDerivation {
pname = "claude-code";
inherit version;
src = fetchurl {
inherit (src) url sha256;
};
dontUnpack = true;
dontBuild = true;
nativeBuildInputs = lib.optionals stdenv.isLinux [ autoPatchelfHook ];
installPhase = ''
runHook preInstall
install -Dm755 $src $out/bin/claude
runHook postInstall
'';
meta = with lib; {
description = "Terminal-based AI coding assistant from Anthropic";
homepage = "https://www.anthropic.com/claude-code";
license = licenses.unfree;
maintainers = [ ];
platforms = [ "aarch64-darwin" "x86_64-darwin" "x86_64-linux" "aarch64-linux" ];
mainProgram = "claude";
};
}

View File

@@ -0,0 +1,34 @@
{ lib
, buildNpmPackage
, fetchurl
, nodejs_18
}:
buildNpmPackage {
pname = "claude-cli";
version = "0.2.65";
src = fetchurl {
url = "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-0.2.65.tgz";
sha256 = "0wwaqq7k9p5aw4vqhfpdgf3da09x64q55wibqaprk6kjvn130i92";
};
npmDepsHash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; # Will be updated after first build
nodejs = nodejs_18;
# Don't run npm audit or other network operations during build
npmConfigHook = ''
npm config set audit false
npm config set fund false
'';
meta = with lib; {
description = "Terminal-based AI coding assistant from Anthropic (npm distribution)";
homepage = "https://www.anthropic.com/claude-code";
license = licenses.unfree;
maintainers = [ ];
platforms = platforms.all;
mainProgram = "claude";
};
}

132
packages/claude-code/update.sh Executable file
View File

@@ -0,0 +1,132 @@
#!/usr/bin/env bash
set -euo pipefail
DRY_RUN=false
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--dry-run|-n)
DRY_RUN=true
shift
;;
--help|-h)
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Options:"
echo " --dry-run, -n Show what would be updated without making changes"
echo " --help, -h Show this help message"
exit 0
;;
*)
echo "Unknown option: $1"
echo "Use --help for usage information"
exit 1
;;
esac
done
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
CASK_URL="https://raw.githubusercontent.com/Homebrew/homebrew-cask/HEAD/Casks/c/claude-code.rb"
NIX_FILE="$(dirname "$0")/default.nix"
echo "Fetching latest claude-code version from Homebrew cask..."
# Fetch the cask file
CASK_CONTENT=$(curl -fsSL "$CASK_URL")
# Extract version (format: version "X.Y.Z")
NEW_VERSION=$(echo "$CASK_CONTENT" | grep -m1 'version' | sed -E 's/.*version "([^"]+)".*/\1/')
# Extract SHA256 hashes (be specific to match sha256 lines only)
SHA_ARM=$(echo "$CASK_CONTENT" | grep 'sha256 arm:' | sed -E 's/.*"([a-f0-9]{64})".*/\1/')
SHA_X86_64=$(echo "$CASK_CONTENT" | grep 'x86_64:' | sed -E 's/.*"([a-f0-9]{64})".*/\1/')
SHA_X86_64_LINUX=$(echo "$CASK_CONTENT" | grep 'x86_64_linux:' | sed -E 's/.*"([a-f0-9]{64})".*/\1/')
SHA_ARM64_LINUX=$(echo "$CASK_CONTENT" | grep 'arm64_linux:' | sed -E 's/.*"([a-f0-9]{64})".*/\1/')
# Get current version
CURRENT_VERSION=$(grep -m1 'version = ' "$NIX_FILE" | sed -E 's/.*version = "([^"]+)".*/\1/')
# Validate extracted data
if [ -z "$NEW_VERSION" ] || [ -z "$SHA_ARM" ] || [ -z "$SHA_X86_64" ] || [ -z "$SHA_X86_64_LINUX" ] || [ -z "$SHA_ARM64_LINUX" ]; then
echo -e "${RED}Error: Failed to extract all required values from Homebrew cask${NC}"
echo "Version: $NEW_VERSION"
echo "ARM: $SHA_ARM"
echo "x86_64: $SHA_X86_64"
echo "x86_64_linux: $SHA_X86_64_LINUX"
echo "arm64_linux: $SHA_ARM64_LINUX"
exit 1
fi
# Check if update is needed
if [ "$CURRENT_VERSION" = "$NEW_VERSION" ]; then
echo -e "${GREEN}Already up to date: $CURRENT_VERSION${NC}"
exit 0
fi
echo -e "${YELLOW}Updating from $CURRENT_VERSION to $NEW_VERSION${NC}"
if [ "$DRY_RUN" = true ]; then
echo -e "${YELLOW}DRY RUN - No changes will be made${NC}"
echo ""
echo "Would update:"
echo " Version: $CURRENT_VERSION -> $NEW_VERSION"
echo " aarch64-darwin SHA: $SHA_ARM"
echo " x86_64-darwin SHA: $SHA_X86_64"
echo " x86_64-linux SHA: $SHA_X86_64_LINUX"
echo " aarch64-linux SHA: $SHA_ARM64_LINUX"
exit 0
fi
# Update version
sed -i.tmp "s/version = \".*\";/version = \"$NEW_VERSION\";/" "$NIX_FILE"
# Update SHA256 hashes using awk for more reliable parsing
awk -v sha_arm="$SHA_ARM" -v sha_x86="$SHA_X86_64" -v sha_x86_linux="$SHA_X86_64_LINUX" -v sha_arm_linux="$SHA_ARM64_LINUX" '
/aarch64-darwin = {/ { in_arm = 1 }
/x86_64-darwin = {/ { in_x86 = 1 }
/x86_64-linux = {/ { in_x86_linux = 1 }
/aarch64-linux = {/ { in_arm_linux = 1 }
/};/ {
in_arm = 0
in_x86 = 0
in_x86_linux = 0
in_arm_linux = 0
}
/sha256 = / {
if (in_arm) {
sub(/sha256 = ".*";/, "sha256 = \"" sha_arm "\";")
} else if (in_x86) {
sub(/sha256 = ".*";/, "sha256 = \"" sha_x86 "\";")
} else if (in_x86_linux) {
sub(/sha256 = ".*";/, "sha256 = \"" sha_x86_linux "\";")
} else if (in_arm_linux) {
sub(/sha256 = ".*";/, "sha256 = \"" sha_arm_linux "\";")
}
}
{ print }
' "$NIX_FILE" > "$NIX_FILE.new"
mv "$NIX_FILE.new" "$NIX_FILE"
# Clean up temp files
rm -f "$NIX_FILE.tmp"
echo -e "${GREEN}Successfully updated to version $NEW_VERSION${NC}"
echo ""
echo "Updated SHA256 hashes:"
echo " aarch64-darwin: $SHA_ARM"
echo " x86_64-darwin: $SHA_X86_64"
echo " x86_64-linux: $SHA_X86_64_LINUX"
echo " aarch64-linux: $SHA_ARM64_LINUX"
echo ""
echo "Next steps:"
echo " 1. Review changes: git diff $NIX_FILE"
echo " 2. Test build: NIXPKGS_ALLOW_UNFREE=1 nix-build -E 'with import <nixpkgs> { config.allowUnfree = true; }; callPackage ./packages/claude-code {}'"
echo " 3. Verify version: ./result/bin/claude --version"
echo " 4. Commit: git add $NIX_FILE && git commit -m 'claude-code: Update to version $NEW_VERSION'"

View File

@@ -1,6 +1,8 @@
{ pkgs, ... }:
{ pkgs, uv2nix ? null, pyproject-nix ? null, pyproject-build-systems ? null, ... }:
{
vulkanHDRLayer = pkgs.callPackage ./vulkan-hdr-layer {};
tea-rbw = pkgs.callPackage ./tea-rbw {};
app-launcher-server = pkgs.callPackage ./app-launcher-server {};
claude-code = pkgs.callPackage ./claude-code {};
sendspin-cli = pkgs.callPackage ./sendspin-cli { inherit uv2nix pyproject-nix pyproject-build-systems; };
}

View File

@@ -0,0 +1,91 @@
{ pkgs
, lib
, fetchFromGitHub
, uv2nix ? null
, pyproject-nix ? null
, pyproject-build-systems ? null
}:
# Simple package build
# Note: uv2nix would be ideal but requires uv.lock which sendspin-cli doesn't have yet
let
# Package aiosendspin from GitHub since it's only in nixpkgs-unstable
aiosendspin = pkgs.python312Packages.buildPythonPackage rec {
pname = "aiosendspin";
version = "1.2.0";
pyproject = true;
src = fetchFromGitHub {
owner = "Sendspin";
repo = "aiosendspin";
rev = version;
sha256 = "sha256-3vTEfXeFqouPswRKST/9U7yg9ah7J9m2KAMoxaBZNR0=";
};
build-system = with pkgs.python312Packages; [
hatchling
setuptools
];
dependencies = with pkgs.python312Packages; [
aiohttp
av
mashumaro
orjson
pillow
zeroconf
];
pythonImportsCheck = [ "aiosendspin" ];
meta = {
description = "Async Python implementation of the Sendspin Protocol";
homepage = "https://github.com/Sendspin-Protocol/aiosendspin";
license = lib.licenses.asl20;
};
};
python = pkgs.python312.withPackages (ps: with ps; [
# Core dependencies from pyproject.toml
aiosendspin
av
numpy
qrcode
readchar
rich
sounddevice
setuptools
]);
in
pkgs.stdenv.mkDerivation rec {
pname = "sendspin-cli";
version = "0.0.0";
src = fetchFromGitHub {
owner = "Sendspin";
repo = "sendspin-cli";
rev = "main";
sha256 = "sha256-z8ieaDHv4C6WNLpPGybhcfB+E6Jj/rCc7zSRpL6vdk0=";
};
buildInputs = [ python pkgs.portaudio pkgs.ffmpeg ];
installPhase = ''
mkdir -p $out/bin $out/lib
cp -r sendspin $out/lib/
cat > $out/bin/sendspin <<EOF
#!/bin/sh
export PYTHONPATH="$out/lib:\$PYTHONPATH"
export LD_LIBRARY_PATH="${pkgs.portaudio}/lib:${pkgs.ffmpeg}/lib:\$LD_LIBRARY_PATH"
exec ${python}/bin/python3 -m sendspin.cli "\$@"
EOF
chmod +x $out/bin/sendspin
'';
meta = {
description = "Synchronized audio player for Sendspin servers";
homepage = "https://github.com/Sendspin/sendspin-cli";
license = lib.licenses.asl20;
platforms = lib.platforms.linux;
};
}

View File

@@ -22,6 +22,5 @@ with lib;
./kde.nix
./programs.nix
./sddm.nix
./steamos.nix
];
}

View File

@@ -9,24 +9,27 @@ in
config = mkMerge [
(mkIf (cfg.enable && cfg.gaming.enable) {
environment.systemPackages = with pkgs; [
steam
lutris
moonlight
# Emulators
dolphin-emu
# Re-enabled in 25.11 after binary build was fixed
dolphin-emu-primehack
retroarch-full
# Experimenting with just using the steam version + downloading
# indiviudal cores
#retroarch-full
ryubing
];
# TODO: Remove me once dolphin-emu and dolphin-emu-primehack update
# dependencies to mbedtls from mbedtls_2 (which is currently)
# unmaintained
nixpkgs.config.permittedInsecurePackages = [ "mbedtls-2.28.10" ];
warnings = [
"Using insecure mbedtls-2.28.10 for Dolphin Emu - check for updates regularly"
];
programs.steam = {
enable = true;
remotePlay.openFirewall = true;
dedicatedServer.openFirewall = true;
localNetworkGameTransfers.openFirewall = true;
};
})
];
}

View File

@@ -1,64 +0,0 @@
{ lib, config, ... }:
# Minimal Jovian compatibility layer for NixOS stable (25.05)
# Defines only the Jovian options used by roles/desktop/steamos.nix
# No actual implementation - just option definitions to prevent evaluation errors
# REMOVE THIS FILE when all systems are on NixOS 25.11+ or unstable
with lib;
let
nixosVersion = config.system.nixos.release;
isCompatibleVersion = versionOlder nixosVersion "25.11";
in
{
options.jovian = {
steam = {
enable = mkEnableOption "Steam (jovian-compat stub)";
autoStart = mkOption {
type = types.bool;
default = false;
description = "Auto-start Steam (jovian-compat stub)";
};
user = mkOption {
type = types.str;
default = "user";
description = "Steam user (jovian-compat stub)";
};
desktopSession = mkOption {
type = types.nullOr types.str;
default = null;
description = "Desktop session (jovian-compat stub)";
};
};
decky-loader = {
enable = mkEnableOption "Decky Loader (jovian-compat stub)";
};
};
config = mkMerge [
{
assertions = [
{
assertion = isCompatibleVersion;
message = ''
The Jovian compatibility shim (roles/jovian-compat.nix) is only needed for NixOS 25.05 and earlier.
You are running NixOS ${nixosVersion}.
Please remove 'roles/jovian-compat.nix' from your flake.nix nixosModules list.
'';
}
];
}
# No config implementation - these options do nothing on stable systems
# steamos role is only enabled on nix-deck which uses unstable anyway
(mkIf config.jovian.steam.enable {
warnings = [
"Jovian is enabled but you're using the compatibility stub. This won't work correctly. Use NixOS unstable for Jovian support."
];
})
];
}

View File

@@ -14,6 +14,11 @@ in
wayland = mkOption {
default = true;
};
jellyfinScaleFactor = mkOption {
type = types.nullOr types.float;
default = null;
description = "Scale factor for Jellyfin Media Player UI (e.g., 1.5 for 150% scaling)";
};
appLauncherServer = {
enable = mkOption {
type = types.bool;
@@ -37,6 +42,28 @@ in
steam-library
youtube
]);
jellyfinMediaPlayerPkg =
if cfg.jellyfinScaleFactor != null
then pkgs.symlinkJoin {
name = "jellyfin-media-player-scaled";
paths = [ pkgs.jellyfin-media-player ];
nativeBuildInputs = [ pkgs.makeWrapper ];
postBuild = ''
mkdir -p $out/bin
rm -f $out/bin/jellyfin-desktop
makeWrapper ${pkgs.jellyfin-media-player}/bin/jellyfin-desktop $out/bin/jellyfin-desktop \
--add-flags "--tv --scale-factor ${toString cfg.jellyfinScaleFactor}"
# Update .desktop file to include scale factor and TV mode arguments
mkdir -p $out/share/applications
rm -f $out/share/applications/org.jellyfin.JellyfinDesktop.desktop
substitute ${pkgs.jellyfin-media-player}/share/applications/org.jellyfin.JellyfinDesktop.desktop \
$out/share/applications/org.jellyfin.JellyfinDesktop.desktop \
--replace-fail "Exec=jellyfin-desktop" "Exec=jellyfin-desktop --tv --scale-factor ${toString cfg.jellyfinScaleFactor}"
'';
}
else pkgs.jellyfin-media-player;
in mkIf cfg.enable
{
users.extraUsers.kodi = {
@@ -50,11 +77,18 @@ in
};
environment.systemPackages = with pkgs; [
jellyfinMediaPlayerPkg
kodiPkg
wget
firefox
] ++ optional cfg.appLauncherServer.enable pkgs.custom.app-launcher-server;
nixpkgs.config.permittedInsecurePackages = lib.warn
"Allowing insecure package qtwebengine-5.15.19 as a jellyfin-media-player dependency. Remove this once jellyfin is updated to use qt6"
[
"qtwebengine-5.15.19"
];
programs.kdeconnect.enable = true;
systemd.user.services = mkIf cfg.appLauncherServer.enable {

View File

@@ -25,7 +25,7 @@ in
users.users.johno = {
isNormalUser = true;
description = "John Ogle";
extraGroups = [ "wheel" "networkmanager" "audio" "video" ] ++ cfg.extraGroups;
extraGroups = [ "wheel" "networkmanager" "audio" "video" "i2c" ] ++ cfg.extraGroups;
};
users.users.eli = mkIf cfg.kids {

82
scripts/update-doomemacs.sh Executable file
View File

@@ -0,0 +1,82 @@
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Configuration
OWNER="doomemacs"
REPO="doomemacs"
FILE="home/roles/emacs/default.nix"
# Use current working directory as repo root (allows running from anywhere in the repo)
REPO_ROOT="${REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
TARGET_FILE="$REPO_ROOT/$FILE"
echo -e "${GREEN}Updating DoomEmacs to latest commit...${NC}"
# Check if file exists
if [[ ! -f "$TARGET_FILE" ]]; then
echo -e "${RED}Error: $TARGET_FILE not found${NC}"
exit 1
fi
# Get the default branch first
echo "Fetching repository information..."
DEFAULT_BRANCH=$(curl -s "https://api.github.com/repos/$OWNER/$REPO" | jq -r '.default_branch')
if [[ -z "$DEFAULT_BRANCH" ]] || [[ "$DEFAULT_BRANCH" == "null" ]]; then
echo -e "${RED}Error: Failed to fetch default branch${NC}"
exit 1
fi
# Get the latest commit SHA from GitHub
echo "Fetching latest commit SHA from $DEFAULT_BRANCH branch..."
LATEST_SHA=$(curl -s "https://api.github.com/repos/$OWNER/$REPO/commits/$DEFAULT_BRANCH" | jq -r '.sha')
if [[ -z "$LATEST_SHA" ]] || [[ "$LATEST_SHA" == "null" ]]; then
echo -e "${RED}Error: Failed to fetch latest commit SHA${NC}"
exit 1
fi
echo -e "Latest commit: ${YELLOW}$LATEST_SHA${NC}"
# Get current SHA from file
CURRENT_SHA=$(grep -oP 'rev = "\K[^"]+' "$TARGET_FILE")
echo -e "Current commit: ${YELLOW}$CURRENT_SHA${NC}"
if [[ "$CURRENT_SHA" == "$LATEST_SHA" ]]; then
echo -e "${GREEN}Already up to date!${NC}"
exit 0
fi
# Update the rev field
echo "Updating rev in $FILE..."
sed -i "s/rev = \".*\"/rev = \"$LATEST_SHA\"/" "$TARGET_FILE"
# Fetch the new sha256 hash using nix-prefetch
echo "Fetching new sha256 hash..."
NEW_SHA256=$(nix-prefetch-url --unpack "https://github.com/$OWNER/$REPO/archive/$LATEST_SHA.tar.gz" 2>/dev/null)
if [[ -z "$NEW_SHA256" ]]; then
echo -e "${RED}Error: Failed to fetch sha256 hash${NC}"
# Revert the rev change
sed -i "s/rev = \".*\"/rev = \"$CURRENT_SHA\"/" "$TARGET_FILE"
exit 1
fi
# Convert to SRI hash format
SRI_HASH=$(nix hash to-sri --type sha256 "$NEW_SHA256")
echo -e "New sha256: ${YELLOW}$SRI_HASH${NC}"
# Update the sha256 field
sed -i "s|sha256 = \".*\"|sha256 = \"$SRI_HASH\"|" "$TARGET_FILE"
echo -e "${GREEN}Successfully updated DoomEmacs!${NC}"
echo -e " Old commit: ${YELLOW}$CURRENT_SHA${NC}"
echo -e " New commit: ${YELLOW}$LATEST_SHA${NC}"
echo -e " New sha256: ${YELLOW}$SRI_HASH${NC}"
echo ""
echo "You can now rebuild your system with the updated DoomEmacs."

View File

@@ -0,0 +1,660 @@
# Sendspin-CLI Integration Implementation Plan
## Overview
Integrate sendspin-cli (https://github.com/Sendspin/sendspin-cli) into the NixOS configuration using uv2nix for Python packaging. Provide a flexible systemd service template that runs as the graphical user, supporting multiple use cases: standalone media centers (like `boxy` running as `kodi` user) and desktop workstations (like `zix790prors` running as `johno` user).
## Current State Analysis
**Existing Infrastructure:**
- Custom package system using overlays at flake.nix:54-59
- Packages defined in `packages/default.nix` and exposed as `pkgs.custom.<name>`
- Role-based configuration system with audio role at roles/audio/default.nix:1-41
- User-level systemd services pattern demonstrated in machines/zix790prors/virtual-surround.nix:79-103
- Python packaging pattern using `python3.withPackages` at packages/app-launcher-server/default.nix:1-10
**Sendspin-CLI Analysis:**
- Python 3.12+ application with setuptools build system
- Entry point: `sendspin.cli:main` (sendspin-cli/sendspin/cli.py:143)
- Dependencies: aiosendspin, av, numpy, qrcode, readchar, rich, sounddevice (sendspin-cli/pyproject.toml:16-23)
- Native dependencies needed: portaudio (for sounddevice), ffmpeg (for av)
- Supports headless mode via `--headless` flag (sendspin-cli/sendspin/cli.py:115-117)
- Device selection via `--audio-device <index|name>` (sendspin-cli/sendspin/cli.py:96-102)
- Device discovery via `--list-audio-devices` (sendspin-cli/sendspin/cli.py:15-34)
- Auto-discovers servers via mDNS unless `--url` specified (sendspin-cli/sendspin/cli.py:69-71)
- No `uv.lock` file currently in repository (only `pyproject.toml`)
**Gap:**
- No uv2nix flake inputs (pyproject-nix, uv2nix, pyproject-build-systems)
- No sendspin-cli package definition
- No sendspin role or systemd service configuration
## Desired End State
After implementation completion:
1. **Package available**: `pkgs.custom.sendspin-cli` builds successfully with all dependencies
2. **Role available**: `roles.sendspin.enable = true` provides sendspin with configurable service
3. **Service template**: Systemd user service runs as graphical session user with configurable audio device
4. **Audio device compatibility**: Uses sendspin's native device specification (index or name prefix)
5. **Flexible user context**: Service can run as kodi, johno, or any graphical session user
### Verification Commands:
```bash
# Package builds successfully
nix build .#nixosConfigurations.zix790prors.config.environment.systemPackages --no-link | grep sendspin-cli
# Package contains working executable
$(nix-build -E '(import <nixpkgs> {}).callPackage ./packages/sendspin-cli {}')/bin/sendspin --help
# Service template is generated
nixos-rebuild dry-build --flake .#zix790prors 2>&1 | grep sendspin
```
## Key Discoveries
### uv2nix Integration Points
- **No lock file**: sendspin-cli has `pyproject.toml` but no `uv.lock` - uv2nix will resolve from pyproject.toml
- **Native dependencies**: sounddevice and av require portaudio and ffmpeg in buildInputs
- **Workspace loading**: uv2nix.lib.workspace.loadWorkspace works with pyproject.toml-only projects
- **Build system**: Uses setuptools (declared in pyproject.toml:1-3)
### Service Architecture
- **User services**: Must use `systemd.user.services` (not system services) for audio access
- **Automatic user detection**: User services run in the logged-in graphical user's session
- **PipeWire dependency**: Service must start after `pipewire.service` and `wireplumber.service`
- **Auto-restart**: Use `Restart=always` for persistent background operation
### Audio Device Handling
- **Native format**: Sendspin expects device index (0, 1, 2) or name prefix ("AmazonBasics")
- **Discovery**: `sendspin --list-audio-devices` shows available devices
- **PipeWire independence**: No need to use PipeWire node names; sendspin queries via sounddevice library
## What We're NOT Doing
To prevent scope creep:
1. **Not creating uv.lock**: Using pyproject.toml directly; lock file can be added upstream later
2. **Not implementing multi-instance configuration**: Providing single-instance template; machines can extend for multiple instances
3. **Not configuring specific machines**: Template only; zix790prors multi-instance setup is future work
4. **Not creating home-manager module**: Using system-level role with user services
5. **Not implementing server mode**: Client-only integration; `sendspin serve` can be added later
6. **Not auto-detecting graphical user**: Relying on systemd user service behavior; explicit user selection can be added later
7. **Not packaging dev dependencies**: Only runtime dependencies; test tools (mypy, ruff) excluded
## Implementation Approach
Use uv2nix to package sendspin-cli from its GitHub repository, accessing pyproject.toml for dependency resolution. Create a NixOS role following the spotifyd pattern with a systemd user service template. The service runs in the logged-in user's session (automatic user detection) and can be configured per-machine for different audio devices.
**Key Technical Decisions:**
1. **uv2nix over python3.withPackages**: Better maintainability, automatic dependency resolution, aligns with upstream development
2. **System-level role with user services**: Follows existing patterns (virtual-surround.nix), enables per-user configuration
3. **Headless mode default**: Services always use `--headless`; TUI available via manual `sendspin` command
4. **mDNS discovery default**: No `--url` by default; let sendspin auto-discover servers on the network
---
## Phase 1: Add uv2nix Flake Inputs
### Overview
Add pyproject-nix, uv2nix, and pyproject-build-systems as flake inputs and thread them through to package definitions.
### Changes Required
#### 1. Flake Inputs
**File**: `flake.nix`
**Changes**: Add new inputs after existing inputs (after line 44)
```nix
pyproject-nix = {
url = "github:pyproject-nix/pyproject.nix";
inputs.nixpkgs.follows = "nixpkgs";
};
uv2nix = {
url = "github:pyproject-nix/uv2nix";
inputs.pyproject-nix.follows = "pyproject-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
pyproject-build-systems = {
url = "github:pyproject-nix/build-system-pkgs";
inputs.pyproject-nix.follows = "pyproject-nix";
inputs.uv2nix.follows = "uv2nix";
inputs.nixpkgs.follows = "nixpkgs";
};
```
#### 2. Outputs Signature
**File**: `flake.nix`
**Changes**: Update outputs function signature (line 47)
```nix
# Before
outputs = { self, nixpkgs, nixpkgs-unstable, nixos-wsl, ... } @ inputs:
# After
outputs = { self, nixpkgs, nixpkgs-unstable, nixos-wsl, pyproject-nix, uv2nix, pyproject-build-systems, ... } @ inputs:
```
#### 3. Pass Inputs to Packages
**File**: `flake.nix`
**Changes**: Update custom package overlay (line 59 and 87)
```nix
# Before
custom = prev.callPackage ./packages {};
# After
custom = prev.callPackage ./packages { inherit uv2nix pyproject-nix pyproject-build-systems; };
```
#### 4. Update Packages Default.nix Signature
**File**: `packages/default.nix`
**Changes**: Accept new parameters
```nix
# Before
{ pkgs, ... }:
# After
{ pkgs, uv2nix ? null, pyproject-nix ? null, pyproject-build-systems ? null, ... }:
```
**Note**: Parameters are optional to maintain compatibility with direct `nix-build` calls.
### Success Criteria
#### Automated Verification:
- [x] Flake evaluation succeeds: `nix flake check`
- [x] Custom packages still build: `nix build .#nixosConfigurations.zix790prors.config.environment.systemPackages`
- [x] No evaluation errors: `nixos-rebuild dry-build --flake .#zix790prors`
#### Manual Verification:
- [x] Flake inputs show pyproject-nix, uv2nix, and pyproject-build-systems: `nix flake metadata`
- [x] Existing machines still build without errors
**Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation before proceeding to Phase 2.
---
## Phase 2: Create Sendspin-CLI Package
### Overview
Create uv2nix-based package for sendspin-cli that handles Python dependencies and native libraries (portaudio, ffmpeg).
### Changes Required
#### 1. Package Definition
**File**: `packages/sendspin-cli/default.nix`
**Changes**: Create new file
```nix
{ pkgs
, uv2nix ? null
, pyproject-nix ? null
, pyproject-build-systems ? null
, lib
, fetchFromGitHub
}:
# Fallback to simple package if uv2nix not available
if uv2nix == null || pyproject-nix == null || pyproject-build-systems == null then
let
python = pkgs.python312.withPackages (ps: with ps; [
# Core dependencies from pyproject.toml
# Note: aiosendspin may need to be packaged separately if not in nixpkgs
av
numpy
qrcode
readchar
rich
sounddevice
# Build dependencies
setuptools
]);
in
pkgs.stdenv.mkDerivation rec {
pname = "sendspin-cli";
version = "0.0.0-fallback";
src = fetchFromGitHub {
owner = "Sendspin";
repo = "sendspin-cli";
rev = "main";
sha256 = lib.fakeSha256; # Replace with actual hash after first build
};
buildInputs = [ python pkgs.portaudio pkgs.ffmpeg ];
installPhase = ''
mkdir -p $out/bin $out/lib
cp -r sendspin $out/lib/
cat > $out/bin/sendspin <<EOF
#!/bin/sh
export PYTHONPATH="$out/lib:\$PYTHONPATH"
exec ${python}/bin/python3 -m sendspin.cli "\$@"
EOF
chmod +x $out/bin/sendspin
'';
meta = {
description = "Synchronized audio player for Sendspin servers (fallback build)";
homepage = "https://github.com/Sendspin/sendspin-cli";
license = lib.licenses.asl20;
};
}
else
let
# Fetch sendspin-cli source
src = fetchFromGitHub {
owner = "Sendspin";
repo = "sendspin-cli";
rev = "main"; # TODO: Pin to specific release tag
sha256 = lib.fakeSha256; # Replace with actual hash after first build
};
# Load workspace from pyproject.toml
workspace = uv2nix.lib.workspace.loadWorkspace {
workspaceRoot = src;
};
# Create overlay from pyproject.toml dependencies
overlay = workspace.mkPyprojectOverlay {
sourcePreference = "wheel"; # Prefer wheels for faster builds
};
# Build Python package set with native dependency overrides
pythonSet = (pkgs.callPackage pyproject-nix.build.packages {
python = pkgs.python312;
}).overrideScope (lib.composeManyExtensions [
pyproject-build-systems.overlays.default
overlay
# Override for packages with native dependencies
(final: prev: {
# sounddevice needs portaudio
sounddevice = prev.sounddevice.overrideAttrs (old: {
buildInputs = (old.buildInputs or []) ++ [ pkgs.portaudio ];
nativeBuildInputs = (old.nativeBuildInputs or []) ++ [ pkgs.portaudio ];
});
# av (PyAV) needs ffmpeg
av = prev.av.overrideAttrs (old: {
buildInputs = (old.buildInputs or []) ++ [ pkgs.ffmpeg ];
nativeBuildInputs = (old.nativeBuildInputs or []) ++ [ pkgs.pkg-config ];
});
})
]);
# Create virtual environment with all dependencies
venv = pythonSet.mkVirtualEnv "sendspin-cli-env" workspace.deps.default;
in
pkgs.stdenv.mkDerivation {
pname = "sendspin-cli";
version = "0.0.0";
inherit src;
buildInputs = [ venv pkgs.portaudio pkgs.ffmpeg ];
installPhase = ''
mkdir -p $out/bin
# Copy virtual environment
cp -r ${venv} $out/venv
# Create wrapper script
cat > $out/bin/sendspin <<EOF
#!/bin/sh
export LD_LIBRARY_PATH="${pkgs.portaudio}/lib:${pkgs.ffmpeg}/lib:\$LD_LIBRARY_PATH"
exec $out/venv/bin/sendspin "\$@"
EOF
chmod +x $out/bin/sendspin
'';
meta = {
description = "Synchronized audio player for Sendspin servers";
homepage = "https://github.com/Sendspin/sendspin-cli";
license = lib.licenses.asl20;
platforms = lib.platforms.linux;
};
}
```
#### 2. Register Package
**File**: `packages/default.nix`
**Changes**: Add sendspin-cli to exports (after line 6)
```nix
{ pkgs, uv2nix ? null, pyproject-nix ? null, pyproject-build-systems ? null, ... }:
{
vulkanHDRLayer = pkgs.callPackage ./vulkan-hdr-layer {};
tea-rbw = pkgs.callPackage ./tea-rbw {};
app-launcher-server = pkgs.callPackage ./app-launcher-server {};
claude-code = pkgs.callPackage ./claude-code {};
sendspin-cli = pkgs.callPackage ./sendspin-cli { inherit uv2nix pyproject-nix pyproject-build-systems; };
}
```
### Success Criteria
#### Automated Verification:
- [x] Package builds successfully: `nix build .#nixosConfigurations.zix790prors.pkgs.custom.sendspin-cli`
- [x] Binary exists in output: `nix path-info .#nixosConfigurations.zix790prors.pkgs.custom.sendspin-cli`
- [x] No build errors in dry-run: `nixos-rebuild dry-build --flake .#zix790prors`
#### Manual Verification:
- [ ] Help text displays correctly: `$(nix-build '<nixpkgs>' -A custom.sendspin-cli)/bin/sendspin --help`
- [ ] List audio devices works: `$(nix-build '<nixpkgs>' -A custom.sendspin-cli)/bin/sendspin --list-audio-devices`
- [ ] Version information is correct: `$(nix-build '<nixpkgs>' -A custom.sendspin-cli)/bin/sendspin --version` (if supported)
- [ ] Dependencies are bundled: Check that output closure contains portaudio and ffmpeg libraries
**Implementation Note**: The first build will fail with `lib.fakeSha256` error. Copy the actual hash from the error message and replace `lib.fakeSha256` with the real hash. After completing this phase and all automated verification passes, pause here for manual confirmation before proceeding to Phase 3.
---
## Phase 3: Create Sendspin Role with Service Template
### Overview
Create NixOS role that provides sendspin-cli package and configurable systemd user service template for running sendspin as a background service.
### Changes Required
#### 1. Role Definition
**File**: `roles/sendspin/default.nix`
**Changes**: Create new file
```nix
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.roles.sendspin;
in
{
options.roles.sendspin = {
enable = mkEnableOption "Enable the sendspin role";
audioDevice = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Audio output device by index (e.g., "0", "1") or name prefix (e.g., "AmazonBasics").
Use `sendspin --list-audio-devices` to see available devices.
If null, uses system default audio device.
'';
example = "0";
};
clientName = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Friendly name for this Sendspin client.
Defaults to hostname if not specified.
'';
example = "Living Room Speakers";
};
clientId = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Unique identifier for this Sendspin client.
Defaults to sendspin-cli-<hostname> if not specified.
'';
example = "sendspin-livingroom";
};
serverUrl = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
WebSocket URL of the Sendspin server.
If null, auto-discovers servers via mDNS.
'';
example = "ws://192.168.1.100:8927";
};
staticDelayMs = mkOption {
type = types.float;
default = 0.0;
description = ''
Extra playback delay in milliseconds applied after clock sync.
Useful for compensating audio latency differences between devices.
'';
example = 50.0;
};
logLevel = mkOption {
type = types.enum [ "DEBUG" "INFO" "WARNING" "ERROR" "CRITICAL" ];
default = "INFO";
description = "Logging level for sendspin service";
};
};
config = mkIf cfg.enable {
# Ensure audio infrastructure is available
roles.audio.enable = true;
# Make sendspin-cli available system-wide
environment.systemPackages = with pkgs; [
custom.sendspin-cli
];
# Systemd user service for running sendspin in headless mode
systemd.user.services.sendspin = {
description = "Sendspin Audio Sync Client";
documentation = [ "https://github.com/Sendspin/sendspin-cli" ];
# Start after audio services are ready
after = [ "pipewire.service" "wireplumber.service" ];
requires = [ "pipewire.service" "wireplumber.service" ];
# Auto-start with pipewire (which starts with graphical session)
wantedBy = [ "pipewire.service" ];
serviceConfig = {
Type = "simple";
Restart = "always";
RestartSec = "5s";
# Build command with configured options
ExecStart = pkgs.writeShellScript "sendspin-start" ''
exec ${pkgs.custom.sendspin-cli}/bin/sendspin \
--headless \
--log-level ${cfg.logLevel} \
${optionalString (cfg.audioDevice != null) "--audio-device '${cfg.audioDevice}'"} \
${optionalString (cfg.clientName != null) "--name '${cfg.clientName}'"} \
${optionalString (cfg.clientId != null) "--id '${cfg.clientId}'"} \
${optionalString (cfg.serverUrl != null) "--url '${cfg.serverUrl}'"} \
${optionalString (cfg.staticDelayMs != 0.0) "--static-delay-ms ${toString cfg.staticDelayMs}"}
'';
};
};
# Open firewall for mDNS discovery
networking.firewall.allowedUDPPorts = [ 5353 ];
};
}
```
#### 2. Register Role
**File**: `roles/default.nix`
**Changes**: Add sendspin to imports (after line 16)
```nix
imports = [
./audio
./bluetooth
./btrfs
./desktop
./kodi
./nfs-mounts
./nvidia
./printing
./remote-build
./sendspin
./spotifyd
./users
./virtualisation
];
```
### Success Criteria
#### Automated Verification:
- [ ] Configuration evaluates: `nixos-rebuild dry-build --flake .#zix790prors`
- [ ] Service unit is generated: `nixos-rebuild dry-build --flake .#zix790prors 2>&1 | grep -i sendspin`
- [ ] No syntax errors: `nix eval .#nixosConfigurations.zix790prors.config.roles.sendspin.enable`
#### Manual Verification:
- [ ] Role can be enabled in machine config without errors
- [ ] Service dependencies are correct (after pipewire/wireplumber)
- [ ] Firewall rule for mDNS is present
- [ ] Sendspin-cli is in system packages when role is enabled
- [ ] All configuration options (audioDevice, clientName, etc.) are exposed
- [ ] Service starts successfully after enabling role and rebuilding
**Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation. Test the service by adding `roles.sendspin.enable = true;` to a machine configuration, rebuilding, and verifying the service runs as the logged-in user.
---
## Testing Strategy
### Unit Tests (Per-Phase)
**Phase 1 (Flake Inputs):**
```bash
# Verify flake is valid
nix flake check
# Verify inputs are available
nix flake metadata | grep -E "(pyproject-nix|uv2nix|pyproject-build-systems)"
# Verify existing builds still work
nix build .#nixosConfigurations.zix790prors.config.system.build.toplevel
```
**Phase 2 (Package):**
```bash
# Build package
nix build .#nixosConfigurations.zix790prors.pkgs.custom.sendspin-cli
# Test executable
result/bin/sendspin --help
result/bin/sendspin --list-audio-devices
# Verify dependencies
nix-store --query --requisites result | grep -E "(portaudio|ffmpeg)"
```
**Phase 3 (Role):**
```bash
# Evaluate with role enabled
nix eval .#nixosConfigurations.zix790prors.config.roles.sendspin.enable
# Check service definition
nixos-rebuild dry-build --flake .#zix790prors
systemctl --user cat sendspin.service # After rebuild
# Verify audio device option works
nix eval '.#nixosConfigurations.zix790prors.config.roles.sendspin.audioDevice'
```
### Integration Tests
**Basic Service Test:**
1. Enable role on a test machine: `roles.sendspin.enable = true;`
2. Rebuild system: `make switch`
3. Check service status: `systemctl --user status sendspin.service`
4. Verify logs show connection attempts: `journalctl --user -u sendspin -f`
5. Run server locally: `sendspin serve --demo`
6. Verify client connects and plays audio
**Multi-Device Test (Future - zix790prors):**
1. Create multiple service instances with different audio devices
2. Verify each instance targets correct device
3. Test audio sync between devices
**User Context Test:**
1. Test on `boxy` running as `kodi` user
2. Test on `zix790prors` running as `johno` user
3. Verify service runs in correct user session
### Manual Testing Steps
1. **Package Installation:**
```bash
# As root
nix-shell -p 'pkgs.custom.sendspin-cli'
sendspin --list-audio-devices
```
2. **Service Functionality:**
```bash
# After enabling role and rebuilding
systemctl --user status sendspin
journalctl --user -u sendspin -n 50
```
3. **Audio Device Selection:**
```bash
# Configure specific device
roles.sendspin.audioDevice = "0";
# Rebuild and verify service uses correct device
```
4. **Server Discovery:**
```bash
# Without serverUrl, verify mDNS discovery
sendspin --list-servers
```
5. **Delay Calibration:**
```bash
# Test delay configuration
roles.sendspin.staticDelayMs = 50.0;
# Verify in service logs
```
## Performance Considerations
**Build Time:**
- uv2nix initial build may take 5-10 minutes (Python dependency resolution)
- Subsequent builds use Nix cache
- Consider using binary cache if building on multiple machines
**Runtime:**
- Sendspin client is lightweight (~20-50 MB memory)
- CPU usage minimal when not playing audio
- Network: Uses mDNS (UDP 5353) and WebSocket connection to server
**Storage:**
- Package closure size: ~200-300 MB (Python + dependencies + libraries)
- No persistent cache by sendspin-cli itself
## Migration Notes
**For Existing Systems:**
1. Add role to machine configuration
2. Configure audio device if not using default
3. Rebuild and enable service
4. No data migration needed (stateless service)
**For Multi-Instance Setups (Future):**
When implementing multiple instances for zix790prors:
1. Disable default service: `systemd.user.services.sendspin.wantedBy = lib.mkForce [];`
2. Create per-device service instances manually
3. Each instance needs unique `--id` and `--audio-device`
## References
- Original research: `thoughts/shared/research/2025-12-29-sendspin-cli-integration.md`
- Sendspin-CLI source: `~/src/sendspin-cli/` (GitHub: https://github.com/Sendspin/sendspin-cli)
- Sendspin pyproject.toml: `~/src/sendspin-cli/pyproject.toml`
- Sendspin CLI implementation: `~/src/sendspin-cli/sendspin/cli.py:143-222`
- Custom packages pattern: `packages/default.nix:1-7`
- Python package pattern: `packages/app-launcher-server/default.nix:1-10`
- Audio role pattern: `roles/audio/default.nix:1-41`
- Service role pattern: `roles/spotifyd/default.nix:1-40`
- User service pattern: `machines/zix790prors/virtual-surround.nix:79-132`
- Flake overlay: `flake.nix:54-59`
- uv2nix documentation: https://pyproject-nix.github.io/uv2nix/
- uv2nix getting started: https://pyproject-nix.github.io/uv2nix/usage/getting-started.html