Compare commits

..

1 Commits

Author SHA1 Message Date
4b0adcc020 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:55 -08:00
90 changed files with 1607 additions and 4170 deletions

44
.beads/.gitignore vendored
View File

@@ -1,44 +0,0 @@
# SQLite databases
*.db
*.db?*
*.db-journal
*.db-wal
*.db-shm
# Daemon runtime files
daemon.lock
daemon.log
daemon.pid
bd.sock
sync-state.json
last-touched
# Local version tracking (prevents upgrade notification spam after git ops)
.local_version
# Legacy database files
db.sqlite
bd.db
# Worktree redirect file (contains relative path to main repo's .beads/)
# Must not be committed as paths would be wrong in other clones
redirect
# Merge artifacts (temporary files from 3-way merge)
beads.base.jsonl
beads.base.meta.json
beads.left.jsonl
beads.left.meta.json
beads.right.jsonl
beads.right.meta.json
# Sync state (local-only, per-machine)
# These files are machine-specific and should not be shared across clones
.sync.lock
sync_base.jsonl
# NOTE: Do NOT add negation patterns (e.g., !issues.jsonl) here.
# They would override fork protection in .git/info/exclude, allowing
# contributors to accidentally commit upstream issue databases.
# The JSONL files (issues.jsonl, interactions.jsonl) and config files
# are tracked by git by default since no pattern above ignores them.

View File

View File

@@ -1,81 +0,0 @@
# Beads - AI-Native Issue Tracking
Welcome to Beads! This repository uses **Beads** for issue tracking - a modern, AI-native tool designed to live directly in your codebase alongside your code.
## What is Beads?
Beads is issue tracking that lives in your repo, making it perfect for AI coding agents and developers who want their issues close to their code. No web UI required - everything works through the CLI and integrates seamlessly with git.
**Learn more:** [github.com/steveyegge/beads](https://github.com/steveyegge/beads)
## Quick Start
### Essential Commands
```bash
# Create new issues
bd create "Add user authentication"
# View all issues
bd list
# View issue details
bd show <issue-id>
# Update issue status
bd update <issue-id> --status in_progress
bd update <issue-id> --status done
# Sync with git remote
bd sync
```
### Working with Issues
Issues in Beads are:
- **Git-native**: Stored in `.beads/issues.jsonl` and synced like code
- **AI-friendly**: CLI-first design works perfectly with AI coding agents
- **Branch-aware**: Issues can follow your branch workflow
- **Always in sync**: Auto-syncs with your commits
## Why Beads?
**AI-Native Design**
- Built specifically for AI-assisted development workflows
- CLI-first interface works seamlessly with AI coding agents
- No context switching to web UIs
🚀 **Developer Focused**
- Issues live in your repo, right next to your code
- Works offline, syncs when you push
- Fast, lightweight, and stays out of your way
🔧 **Git Integration**
- Automatic sync with git commits
- Branch-aware issue tracking
- Intelligent JSONL merge resolution
## Get Started with Beads
Try Beads in your own projects:
```bash
# Install Beads
curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash
# Initialize in your repo
bd init
# Create your first issue
bd create "Try out Beads"
```
## Learn More
- **Documentation**: [github.com/steveyegge/beads/docs](https://github.com/steveyegge/beads/tree/main/docs)
- **Quick Start Guide**: Run `bd quickstart`
- **Examples**: [github.com/steveyegge/beads/examples](https://github.com/steveyegge/beads/tree/main/examples)
---
*Beads: Issue tracking that moves at the speed of thought*

View File

@@ -1,64 +0,0 @@
# Beads Configuration File
# This file configures default behavior for all bd commands in this repository
# All settings can also be set via environment variables (BD_* prefix)
# or overridden with command-line flags
# Issue prefix for this repository (used by bd init)
# If not set, bd init will auto-detect from directory name
# Example: issue-prefix: "myproject" creates issues like "myproject-1", "myproject-2", etc.
issue-prefix: "x"
# Use no-db mode: load from JSONL, no SQLite, write back after each command
# When true, bd will use .beads/issues.jsonl as the source of truth
# instead of SQLite database
# no-db: false
# Disable daemon for RPC communication (forces direct database access)
# no-daemon: false
# Disable auto-flush of database to JSONL after mutations
# no-auto-flush: false
# Disable auto-import from JSONL when it's newer than database
# no-auto-import: false
# Enable JSON output by default
# json: false
# Default actor for audit trails (overridden by BD_ACTOR or --actor)
# actor: ""
# Path to database (overridden by BEADS_DB or --db)
# db: ""
# Auto-start daemon if not running (can also use BEADS_AUTO_START_DAEMON)
# auto-start-daemon: true
# Debounce interval for auto-flush (can also use BEADS_FLUSH_DEBOUNCE)
# flush-debounce: "5s"
# Git branch for beads commits (bd sync will commit to this branch)
# IMPORTANT: Set this for team projects so all clones use the same sync branch.
# This setting persists across clones (unlike database config which is gitignored).
# Can also use BEADS_SYNC_BRANCH env var for local override.
# If not set, bd sync will require you to run 'bd config set sync.branch <branch>'.
sync-branch: "beads-sync"
# Multi-repo configuration (experimental - bd-307)
# Allows hydrating from multiple repositories and routing writes to the correct JSONL
# repos:
# primary: "." # Primary repo (where this database lives)
# additional: # Additional repos to hydrate from (read-only)
# - ~/beads-planning # Personal planning repo
# - ~/work-planning # Work planning repo
# Integration settings (access with 'bd config get/set')
# These are stored in the database, not in this file:
# - jira.url
# - jira.project
# - linear.url
# - linear.api-key
# - github.org
# - github.repo
routing.mode: "explicit"

0
.gitattributes vendored
View File

View File

@@ -1,107 +0,0 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: https://git.johnogle.info/johno/gitea-actions/nix-setup@v1
- name: Check flake
run: nix flake check
env:
NIX_CONFIG: "access-tokens = git.johnogle.info=${{ secrets.GITEA_ACCESS_TOKEN }}"
build-and-cache:
runs-on: ubuntu-latest
needs: check
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v6
- uses: https://git.johnogle.info/johno/gitea-actions/nix-setup@v1
- name: Setup SSH for cache
run: |
mkdir -p ~/.ssh
echo "${{ secrets.CACHE_SSH_KEY }}" > ~/.ssh/cache_key
chmod 600 ~/.ssh/cache_key
ssh-keyscan -H ${{ secrets.CACHE_HOST }} >> ~/.ssh/known_hosts 2>/dev/null || true
- name: Setup signing key
run: |
echo "${{ secrets.NIX_SIGNING_KEY }}" > /tmp/signing-key
chmod 600 /tmp/signing-key
- name: Build, sign, and cache all packages
run: |
PACKAGES=(
custom-claude-code
custom-app-launcher-server
custom-mcrcon-rbw
custom-tea-rbw
custom-rclone-torbox-setup
custom-nextcloud-talk-desktop
qt-pinned-jellyfin-media-player
qt-pinned-stremio
nix-deck-kernel
plasma-bigscreen
)
FAILED=()
SKIPPED=()
for pkg in "${PACKAGES[@]}"; do
echo "::group::Building $pkg"
# Check if package is already cached by evaluating its store path and checking the remote
OUT_PATH=$(nix eval ".#$pkg.outPath" --raw 2>/dev/null)
if [ -n "$OUT_PATH" ] && ssh -i ~/.ssh/cache_key ${{ secrets.CACHE_USER }}@${{ secrets.CACHE_HOST }} \
"nix path-info '$OUT_PATH' >/dev/null 2>&1"; then
echo "⏭ $pkg already cached ($OUT_PATH), skipping"
SKIPPED+=("$pkg")
echo "::endgroup::"
continue
fi
# --cores 2 limits parallel jobs to reduce RAM pressure on john-endesktop
if BUILD_OUTPUT=$(nix build ".#$pkg" --no-link --print-out-paths --cores 2 2>&1); then
OUT_PATH=$(echo "$BUILD_OUTPUT" | grep '^/nix/store/' | tail -1)
echo "$BUILD_OUTPUT"
echo "Store path: $OUT_PATH"
# Sign the closure
nix store sign --key-file /tmp/signing-key -r "$OUT_PATH"
# Push to cache
nix copy --to "ssh-ng://${{ secrets.CACHE_USER }}@${{ secrets.CACHE_HOST }}?ssh-key=$HOME/.ssh/cache_key" "$OUT_PATH"
# Create GC root to prevent garbage collection
OUT_HASH=$(basename "$OUT_PATH" | cut -d'-' -f1)
ssh -i ~/.ssh/cache_key ${{ secrets.CACHE_USER }}@${{ secrets.CACHE_HOST }} \
"mkdir -p /nix/var/nix/gcroots/ci-cache && ln -sfn $OUT_PATH /nix/var/nix/gcroots/ci-cache/${OUT_HASH}"
echo "✓ $pkg cached successfully"
else
echo "✗ $pkg failed to build"
FAILED+=("$pkg")
fi
echo "::endgroup::"
done
if [ ${#SKIPPED[@]} -gt 0 ]; then
echo "Skipped (already cached): ${SKIPPED[*]}"
fi
if [ ${#FAILED[@]} -gt 0 ]; then
echo "::error::Failed packages: ${FAILED[*]}"
exit 1
fi
env:
NIX_CONFIG: "access-tokens = git.johnogle.info=${{ secrets.GITEA_ACCESS_TOKEN }}"

6
.gitignore vendored
View File

@@ -1,8 +1,2 @@
result
thoughts
.beads
# Gas Town (added by gt)
.runtime/
.claude/
.logs/

View File

@@ -9,7 +9,7 @@ Directory Structure:
----------------------
• packages/ - Custom Nix packages leveraged across various configurations.
• roles/ - Role-based configurations (e.g., kodi, bluetooth) each with its own module (default.nix) for inclusion in machine setups.
• machines/ - Machine-specific configurations (e.g., nix-book, zix790prors, boxy) including configuration.nix and hardware-configuration.nix tailored for each hardware.
• machines/ - Machine-specific configurations (e.g., nix-book, z790prors, boxy, wixos) including configuration.nix and hardware-configuration.nix tailored for each hardware.
• home/ - Home-manager configurations for personal environments and application settings (e.g., home-nix-book.nix, home-z790prors.nix).
Design Principles:

View File

@@ -10,7 +10,7 @@ This is a NixOS configuration repository using flakes, managing multiple machine
### Flake Structure
- **flake.nix**: Main entry point defining inputs (nixpkgs, home-manager, plasma-manager, etc.) and outputs for multiple NixOS configurations
- **Machines**: `nix-book`, `boxy`, `zix790prors`, `live-usb`, `johno-macbookpro` (Darwin/macOS)
- **Machines**: `nix-book`, `boxy`, `wixos` (WSL configuration), `zix790prors`, `live-usb`, `johno-macbookpro` (Darwin/macOS)
- **Home configurations**: Standalone home-manager configuration for user `johno`
### Directory Structure
@@ -74,6 +74,7 @@ The repository also uses a modular home-manager role system for user-space confi
- **nix-book**: Compact laptop → excludes office/media roles due to SSD space constraints
- **boxy**: Living room media center → optimized for media consumption, excludes sync/office (shared machine)
- **zix790prors**: All-purpose workstation → full desktop experience with all roles enabled
- **wixos**: WSL2 development → full desktop experience, inherits from zix790prors Windows host
- **live-usb**: Temporary environment → only base + desktop roles, no persistent services
- **johno-macbookpro**: macOS work laptop → Darwin-specific configuration with development tools
@@ -106,6 +107,7 @@ darwin-rebuild build --flake .#johno-macbookpro
- `nix-book`: Compact laptop with storage constraints, uses `home/home-laptop-compact.nix`
- `boxy`: Shared living room media center/gaming desktop with AMD GPU, uses `home/home-media-center.nix`
- `zix790prors`: Powerful all-purpose workstation (gaming, 3D modeling, development), dual-boots Windows 11 with shared btrfs /games partition, uses `home/home-desktop.nix`
- `wixos`: WSL2 development environment running in Windows partition of zix790prors, uses `home/home-desktop.nix`
- `live-usb`: Bootable ISO configuration, uses `home/home-live-usb.nix`
- `johno-macbookpro`: macOS work laptop, uses `home/home-darwin-work.nix`
@@ -170,58 +172,6 @@ Creates an ISO suitable for Ventoy and other USB boot tools in `./result/iso/`.
- **Garbage collection**: Automatic, deletes older than 10 days
- **Unfree packages**: Allowed globally
## Issue Tracking (Gitea)
**Tea CLI for Gitea:**
```bash
# Note: When using tea CLI, you must specify --repo johno/nixos-configs
# The CLI doesn't automatically detect the repo from git remote
# List all issues (open by default)
tea issues --repo johno/nixos-configs
# List closed issues
tea issues --repo johno/nixos-configs --state closed
# View specific issue
tea issue --repo johno/nixos-configs 2
# Create new issue
tea issues create --repo johno/nixos-configs --title "Issue title" --body "Description"
# Add comment to issue
tea comment --repo johno/nixos-configs 2 "Comment text"
# Close issue (note: 'issues' is plural, issue number comes last)
tea issues close --repo johno/nixos-configs 2
```
## Important Notes
- **Sudo access**: Claude Code does not have sudo access. Ask the user to run elevated commands like `sudo nixos-rebuild switch`
## Landing the Plane (Session Completion)
**When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until `git push` succeeds.
**MANDATORY WORKFLOW:**
1. **File issues for remaining work** - Create issues for anything that needs follow-up
2. **Run quality gates** (if code changed) - Tests, linters, builds
3. **Update issue status** - Close finished work, update in-progress items
4. **PUSH TO REMOTE** - This is MANDATORY:
```bash
git pull --rebase
bd sync
git push
git status # MUST show "up to date with origin"
```
5. **Clean up** - Clear stashes, prune remote branches
6. **Verify** - All changes committed AND pushed
7. **Hand off** - Provide context for next session
**CRITICAL RULES:**
- Work is NOT complete until `git push` succeeds
- NEVER stop before pushing - that leaves work stranded locally
- NEVER say "ready to push when you are" - YOU must push
- If push fails, resolve and retry until it succeeds
- **Sudo access**: Claude Code does not have sudo access. Ask the user to run elevated commands like `sudo nixos-rebuild switch`

4
scripts/bootstrap.sh → bootstrap.sh Normal file → Executable file
View File

@@ -1,7 +1,6 @@
#!/usr/bin/env bash
# bootstrap.sh
# Usage: nix run .#bootstrap -- <hostname>
# Or: sudo ./scripts/bootstrap.sh <hostname>
# Usage: sudo ./bootstrap.sh <hostname>
set -euo pipefail
NEW_HOSTNAME="${1:?missing hostname}"
@@ -9,3 +8,4 @@ FLAKE_URI="git+https://git.johnogle.info/johno/nixos-configs.git#${NEW_HOSTNAME}
export NIX_CONFIG="experimental-features = nix-command flakes"
nixos-rebuild switch --flake "$FLAKE_URI"

19
build-liveusb.sh Executable file
View File

@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Build Live USB ISO from flake configuration
# Creates an uncompressed ISO suitable for Ventoy and other USB boot tools
set -e
echo "Building Live USB ISO..."
nix build .#nixosConfigurations.live-usb.config.system.build.isoImage --show-trace
if [ -f "./result/iso/"*.iso ]; then
iso_file=$(ls ./result/iso/*.iso)
echo "✅ Build complete!"
echo "📁 ISO location: $iso_file"
echo "💾 Ready for Ventoy or dd to USB"
else
echo "❌ Build failed - no ISO file found"
exit 1
fi

242
flake.lock generated
View File

@@ -1,41 +1,18 @@
{
"nodes": {
"doomemacs": {
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1774080407,
"narHash": "sha256-FYbalilgDFjIVwK+D6DjDos1IMmMGA20lRf8k6Ykm1Y=",
"owner": "doomemacs",
"repo": "doomemacs",
"rev": "d8d75443d39d95f3c5256504eb838e0acc62ef44",
"lastModified": 1765121682,
"narHash": "sha256-4VBOP18BFeiPkyhy9o4ssBNQEvfvv1kXkasAYd0+rrA=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "65f23138d8d09a92e30f1e5c87611b23ef451bf3",
"type": "github"
},
"original": {
"owner": "doomemacs",
"repo": "doomemacs",
"type": "github"
}
},
"emacs-overlay": {
"inputs": {
"nixpkgs": [
"nix-doom-emacs-unstraightened"
],
"nixpkgs-stable": [
"nix-doom-emacs-unstraightened"
]
},
"locked": {
"lastModified": 1774256052,
"narHash": "sha256-7OLaUBQCOCt4XXbjHq9xqBopOJJpbV6Cl8mWdMLzazc=",
"owner": "nix-community",
"repo": "emacs-overlay",
"rev": "c4b7915a9467aa611c7346d2322514cdf8c1ba45",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "emacs-overlay",
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
@@ -46,11 +23,11 @@
]
},
"locked": {
"lastModified": 1768846578,
"narHash": "sha256-82f/+e8HAwmBukiLlr7I3HYvM/2GCd5SOc+BC+qzsOQ=",
"lastModified": 1761423376,
"narHash": "sha256-pMy3cnUFfue4vz/y0jx71BfcPGxZf+hk/DtnzWvfU0c=",
"ref": "refs/heads/main",
"rev": "c11ff9d3c67372a843a0fa6bf23132e986bd6955",
"revCount": 14,
"rev": "a1f695665771841a988afc965526cbf99160cd77",
"revCount": 11,
"type": "git",
"url": "https://git.johnogle.info/johno/google-cookie-retrieval.git"
},
@@ -66,11 +43,11 @@
]
},
"locked": {
"lastModified": 1774274588,
"narHash": "sha256-dnHvv5EMUgTzGZmA+3diYjQU2O6BEpGLEOgJ1Qe9LaY=",
"lastModified": 1766292113,
"narHash": "sha256-sWTtmkQujRpjWYCnZc8LWdDiCzrRlSBPrGovkZpLkBI=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "cf9686ba26f5ef788226843bc31fda4cf72e373b",
"rev": "fdec8815a86db36f42fc9c8cb2931cd8485f5aed",
"type": "github"
},
"original": {
@@ -87,11 +64,11 @@
]
},
"locked": {
"lastModified": 1774292006,
"narHash": "sha256-RI5sjkDEwIiD2eZHd7iM6ZqPoPWZvn3KdBiMumA3IYI=",
"lastModified": 1766282146,
"narHash": "sha256-0V/nKU93KdYGi+5LB/MVo355obBJw/2z9b2xS3bPJxY=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "3cea83bf84abeb72581bdee380fa526d7fcd7e5b",
"rev": "61fcc9de76b88e55578eb5d79fc80f2b236df707",
"type": "github"
},
"original": {
@@ -109,11 +86,11 @@
]
},
"locked": {
"lastModified": 1774168156,
"narHash": "sha256-+pwZSARdlM2RQQ6V0q76+WMKW9aNIcxkSOIThcz/f0A=",
"lastModified": 1766225187,
"narHash": "sha256-6hcaU8qtmixsaEUbjPiOFd5aJPZxAIBokl5d7dkab3k=",
"owner": "Jovian-Experiments",
"repo": "Jovian-NixOS",
"rev": "939caad56508542d0f19cab963e2bc693f5f2831",
"rev": "bb53a85db9210204a98f771f10f1f5b4e06ccb2d",
"type": "github"
},
"original": {
@@ -129,11 +106,11 @@
]
},
"locked": {
"lastModified": 1772129556,
"narHash": "sha256-Utk0zd8STPsUJPyjabhzPc5BpPodLTXrwkpXBHYnpeg=",
"lastModified": 1765066094,
"narHash": "sha256-0YSU35gfRFJzx/lTGgOt6ubP8K6LeW0vaywzNNqxkl4=",
"owner": "nix-darwin",
"repo": "nix-darwin",
"rev": "ebec37af18215214173c98cf6356d0aca24a2585",
"rev": "688427b1aab9afb478ca07989dc754fa543e03d5",
"type": "github"
},
"original": {
@@ -143,27 +120,6 @@
"type": "github"
}
},
"nix-doom-emacs-unstraightened": {
"inputs": {
"doomemacs": "doomemacs",
"emacs-overlay": "emacs-overlay",
"nixpkgs": [],
"systems": "systems"
},
"locked": {
"lastModified": 1774265710,
"narHash": "sha256-ar8pFUSAxXhV7DpVRjNvgviWuqOqWPAImb4MM7lSh5Y=",
"owner": "marienz",
"repo": "nix-doom-emacs-unstraightened",
"rev": "f6022b9192e034a817373692ede18a9319cf9730",
"type": "github"
},
"original": {
"owner": "marienz",
"repo": "nix-doom-emacs-unstraightened",
"type": "github"
}
},
"nix-github-actions": {
"inputs": {
"nixpkgs": [
@@ -186,45 +142,49 @@
"type": "github"
}
},
"nixpkgs": {
"nixos-wsl": {
"inputs": {
"flake-compat": "flake-compat",
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1774244481,
"narHash": "sha256-4XfMXU0DjN83o6HWZoKG9PegCvKvIhNUnRUI19vzTcQ=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "4590696c8693fea477850fe379a01544293ca4e2",
"lastModified": 1765841014,
"narHash": "sha256-55V0AJ36V5Egh4kMhWtDh117eE3GOjwq5LhwxDn9eHg=",
"owner": "nix-community",
"repo": "NixOS-WSL",
"rev": "be4af8042e7a61fa12fda58fe9a3b3babdefe17b",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-25.11",
"repo": "nixpkgs",
"owner": "nix-community",
"ref": "main",
"repo": "NixOS-WSL",
"type": "github"
}
},
"nixpkgs-qt": {
"nixpkgs": {
"locked": {
"lastModified": 1774244481,
"narHash": "sha256-4XfMXU0DjN83o6HWZoKG9PegCvKvIhNUnRUI19vzTcQ=",
"owner": "nixos",
"lastModified": 1765472234,
"narHash": "sha256-9VvC20PJPsleGMewwcWYKGzDIyjckEz8uWmT0vCDYK0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "4590696c8693fea477850fe379a01544293ca4e2",
"rev": "2fbfb1d73d239d2402a8fe03963e37aab15abe8b",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-25.11",
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-unstable": {
"locked": {
"lastModified": 1774106199,
"narHash": "sha256-US5Tda2sKmjrg2lNHQL3jRQ6p96cgfWh3J1QBliQ8Ws=",
"lastModified": 1766070988,
"narHash": "sha256-G/WVghka6c4bAzMhTwT2vjLccg/awmHkdKSd2JrycLc=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "6c9a78c09ff4d6c21d0319114873508a6ec01655",
"rev": "c6245e83d836d0433170a16eb185cefe0572f8b8",
"type": "github"
},
"original": {
@@ -234,6 +194,22 @@
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1766201043,
"narHash": "sha256-eplAP+rorKKd0gNjV3rA6+0WMzb1X1i16F5m5pASnjA=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "b3aad468604d3e488d627c0b43984eb60e75e782",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-25.11",
"repo": "nixpkgs",
"type": "github"
}
},
"plasma-manager": {
"inputs": {
"home-manager": [
@@ -244,11 +220,11 @@
]
},
"locked": {
"lastModified": 1772361940,
"narHash": "sha256-B1Cz+ydL1iaOnGlwOFld/C8lBECPtzhiy/pP93/CuyY=",
"lastModified": 1763909441,
"narHash": "sha256-56LwV51TX/FhgX+5LCG6akQ5KrOWuKgcJa+eUsRMxsc=",
"owner": "nix-community",
"repo": "plasma-manager",
"rev": "a4b33606111c9c5dcd10009042bb710307174f51",
"rev": "b24ed4b272256dfc1cc2291f89a9821d5f9e14b4",
"type": "github"
},
"original": {
@@ -267,11 +243,11 @@
]
},
"locked": {
"lastModified": 1772361940,
"narHash": "sha256-B1Cz+ydL1iaOnGlwOFld/C8lBECPtzhiy/pP93/CuyY=",
"lastModified": 1763909441,
"narHash": "sha256-56LwV51TX/FhgX+5LCG6akQ5KrOWuKgcJa+eUsRMxsc=",
"owner": "nix-community",
"repo": "plasma-manager",
"rev": "a4b33606111c9c5dcd10009042bb710307174f51",
"rev": "b24ed4b272256dfc1cc2291f89a9821d5f9e14b4",
"type": "github"
},
"original": {
@@ -280,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",
@@ -287,26 +309,36 @@
"home-manager-unstable": "home-manager-unstable",
"jovian": "jovian",
"nix-darwin": "nix-darwin",
"nix-doom-emacs-unstraightened": "nix-doom-emacs-unstraightened",
"nixpkgs": "nixpkgs",
"nixpkgs-qt": "nixpkgs-qt",
"nixos-wsl": "nixos-wsl",
"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"
}
},
"systems": {
"uv2nix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"pyproject-nix": [
"pyproject-nix"
]
},
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"lastModified": 1766021660,
"narHash": "sha256-UUfz7qWB1Rb2KjGVCimt//Jncv3TgJwffPqbzqpkmgY=",
"owner": "pyproject-nix",
"repo": "uv2nix",
"rev": "19fa99be3409f55ec05e823c66c9769df7a8dd17",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"owner": "pyproject-nix",
"repo": "uv2nix",
"type": "github"
}
}

537
flake.nix
View File

@@ -4,10 +4,8 @@
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11";
nixpkgs-unstable.url = "github:nixos/nixpkgs/nixos-unstable";
# Separate nixpkgs for qt5webengine-dependent packages (jellyfin-media-player, etc.)
# Updates on separate Renovate schedule to avoid massive qt rebuilds
nixpkgs-qt.url = "github:nixos/nixpkgs/nixos-25.11";
nixos-wsl.url = "github:nix-community/NixOS-WSL/main";
nix-darwin = {
url = "github:nix-darwin/nix-darwin/nix-darwin-25.11";
inputs.nixpkgs.follows = "nixpkgs";
@@ -45,349 +43,216 @@
inputs.nixpkgs.follows = "nixpkgs-unstable";
};
nix-doom-emacs-unstraightened = {
url = "github:marienz/nix-doom-emacs-unstraightened";
# Don't follow nixpkgs to avoid rebuild issues with emacs-overlay
inputs.nixpkgs.follows = "";
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,
...
}@inputs:
let
# Shared overlay function to reduce duplication across module sets
# Parameters:
# unstableOverlays: Additional overlays to apply when importing nixpkgs-unstable
mkBaseOverlay =
outputs = { self, nixpkgs, nixpkgs-unstable, nixos-wsl, pyproject-nix, uv2nix, pyproject-build-systems, ... } @ inputs: let
nixosModules = [
./roles
] ++ [
inputs.home-manager.nixosModules.home-manager
{
nixpkgs.overlays = [
(final: prev: {
unstable = import nixpkgs-unstable {
system = prev.stdenv.hostPlatform.system;
config.allowUnfree = true;
};
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;
})
];
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.sharedModules = [
inputs.plasma-manager.homeModules.plasma-manager
];
home-manager.extraSpecialArgs = {
globalInputs = inputs;
};
}
];
# Modules for unstable-based systems (like nix-deck)
nixosModulesUnstable = [
./roles
] ++ [
inputs.home-manager-unstable.nixosModules.home-manager
inputs.jovian.nixosModules.jovian
{
nixpkgs.overlays = [
(final: prev: {
unstable = import nixpkgs-unstable {
system = prev.stdenv.hostPlatform.system;
config.allowUnfree = true;
};
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;
})
];
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.sharedModules = [
inputs.plasma-manager-unstable.homeModules.plasma-manager
];
home-manager.extraSpecialArgs = {
globalInputs = inputs;
};
}
];
darwinModules = [
./roles/darwin.nix
] ++ [
inputs.home-manager.darwinModules.home-manager
{
nixpkgs.overlays = [
(final: prev: {
unstable = import nixpkgs-unstable {
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 { inherit uv2nix pyproject-nix pyproject-build-systems; };
# Compatibility: bitwarden renamed to bitwarden-desktop in unstable
bitwarden-desktop = prev.bitwarden-desktop or prev.bitwarden;
})
];
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.extraSpecialArgs = {
globalInputs = inputs;
};
}
];
in {
nixosConfigurations.nix-book = nixpkgs.lib.nixosSystem rec {
system = "x86_64-linux";
modules = nixosModules ++ [
./machines/nix-book/configuration.nix
{
unstableOverlays ? [ ],
}:
(final: prev: {
unstable = import nixpkgs-unstable {
system = prev.stdenv.hostPlatform.system;
config.allowUnfree = true;
overlays = unstableOverlays;
};
# Separate nixpkgs for qt5webengine-heavy packages to avoid rebuild churn
qt-pinned = import inputs.nixpkgs-qt {
system = prev.stdenv.hostPlatform.system;
config = {
allowUnfree = true;
permittedInsecurePackages = [ "qtwebengine-5.15.19" ];
home-manager.users.johno = {
imports = [ ./home/home-laptop-compact.nix ];
# Machine-specific overrides
home.roles.i3_sway.extraSwayConfig = {
output.eDP-1.scale = "1.75";
};
};
custom = prev.callPackage ./packages { };
# Compatibility: bitwarden renamed to bitwarden-desktop in unstable
bitwarden-desktop = prev.bitwarden-desktop or prev.bitwarden;
});
# Shared home-manager configuration factory
# Parameters:
# sharedModules: Additional modules to include in home-manager.sharedModules
mkHomeManagerConfig =
{
sharedModules ? [ ],
}:
{
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.sharedModules = sharedModules ++ [
inputs.nix-doom-emacs-unstraightened.homeModule
];
home-manager.extraSpecialArgs = {
globalInputs = inputs;
};
};
# Shared unstable overlays for custom package builds
customUnstableOverlays = [
# Override claude-code in unstable to use our custom GCS-based build
# (needed for corporate networks that block npm registry)
(ufinal: uprev: {
claude-code = uprev.callPackage ./packages/claude-code { };
})
home-manager.extraSpecialArgs = { inherit system; };
}
];
};
nixosModules = [
./roles
nixosConfigurations.boxy = nixpkgs.lib.nixosSystem rec {
system = "x86_64-linux";
modules = nixosModules ++ [
./machines/boxy/configuration.nix
inputs.home-manager.nixosModules.home-manager
{
nixpkgs.overlays = [ (mkBaseOverlay { unstableOverlays = customUnstableOverlays; }) ];
home-manager.users.johno = import ./home/home-media-center.nix;
home-manager.extraSpecialArgs = { inherit system; };
}
(mkHomeManagerConfig {
sharedModules = [ inputs.plasma-manager.homeModules.plasma-manager ];
})
];
# Modules for unstable-based systems (like nix-deck)
nixosModulesUnstable = [
./roles
inputs.home-manager-unstable.nixosModules.home-manager
inputs.jovian.nixosModules.jovian
{
nixpkgs.overlays = [ (mkBaseOverlay { unstableOverlays = customUnstableOverlays; }) ];
}
(mkHomeManagerConfig {
sharedModules = [ inputs.plasma-manager-unstable.homeModules.plasma-manager ];
})
];
darwinModules = [
./roles/darwin.nix
inputs.home-manager.darwinModules.home-manager
{
nixpkgs.overlays = [ (mkBaseOverlay { unstableOverlays = customUnstableOverlays; }) ];
}
(mkHomeManagerConfig { sharedModules = [ ]; })
];
in
{
nixosConfigurations.nix-book = nixpkgs.lib.nixosSystem rec {
system = "x86_64-linux";
modules = nixosModules ++ [
./machines/nix-book/configuration.nix
{
home-manager.users.johno = {
imports = [ ./home/home-laptop-compact.nix ];
# Machine-specific overrides
home.roles.i3_sway.extraSwayConfig = {
output.eDP-1.scale = "1.75";
};
};
home-manager.extraSpecialArgs = { inherit system; };
}
];
};
nixosConfigurations.boxy = nixpkgs.lib.nixosSystem rec {
system = "x86_64-linux";
modules = nixosModules ++ [
./machines/boxy/configuration.nix
{
home-manager.users.johno = import ./home/home-media-center.nix;
# kodi user: AVR volume control + minimal Plasma config for Bigscreen session
home-manager.users.kodi = import ./home/home-kodi.nix;
home-manager.extraSpecialArgs = { inherit system; };
}
];
};
nixosConfigurations.gym-box = nixpkgs.lib.nixosSystem rec {
system = "x86_64-linux";
modules = nixosModules ++ [
./machines/gym-box/configuration.nix
{
home-manager.users.johno = import ./home/home-media-center.nix;
home-manager.users.kodi = import ./home/home-kodi.nix;
home-manager.extraSpecialArgs = { inherit system; };
}
];
};
nixosConfigurations.zix790prors = nixpkgs.lib.nixosSystem rec {
system = "x86_64-linux";
modules = nixosModules ++ [
./machines/zix790prors/configuration.nix
{
home-manager.users.johno = {
imports = [ ./home/home-desktop.nix ];
home.roles.i3_sway.extraSwayConfig = {
output = {
"DP-1" = {
mode = "3440x1440@164.900Hz";
};
};
};
};
home-manager.extraSpecialArgs = { inherit system; };
}
];
};
# Live USB ISO configuration
nixosConfigurations.live-usb = nixpkgs.lib.nixosSystem rec {
system = "x86_64-linux";
modules = nixosModules ++ [
./machines/live-usb/configuration.nix
{
home-manager.users.nixos = import ./home/home-live-usb.nix;
home-manager.extraSpecialArgs = { inherit system; };
}
];
};
# Steam Deck configuration (using unstable for better Jovian compatibility)
nixosConfigurations.nix-deck = nixpkgs-unstable.lib.nixosSystem rec {
system = "x86_64-linux";
modules = nixosModulesUnstable ++ [
./machines/nix-deck/configuration.nix
{
home-manager.users.johno = import ./home/home-desktop.nix;
home-manager.extraSpecialArgs = { inherit system; };
}
];
};
# ZFS/NFS server configuration
nixosConfigurations.john-endesktop = nixpkgs.lib.nixosSystem rec {
system = "x86_64-linux";
modules = nixosModules ++ [
./machines/john-endesktop/configuration.nix
inputs.home-manager.nixosModules.home-manager
{
home-manager.users.johno = import ./home/home-server.nix;
home-manager.extraSpecialArgs = { inherit system; };
}
];
};
# Darwin/macOS configurations
darwinConfigurations."BLKFV4YF49KT7" = inputs.nix-darwin.lib.darwinSystem rec {
system = "aarch64-darwin";
modules = darwinModules ++ [
./machines/johno-macbookpro/configuration.nix
{
home-manager.users.johno = import ./home/home-darwin-work.nix;
home-manager.extraSpecialArgs = { inherit system; };
}
];
};
# Packages for CI caching (custom packages, flake inputs, and qt-pinned)
packages = nixpkgs.lib.genAttrs [ "x86_64-linux" "aarch64-linux" ] (
system:
let
pkgs = import nixpkgs {
inherit system;
config.allowUnfree = true;
overlays = [ (mkBaseOverlay { }) ];
};
pkgsQt = import inputs.nixpkgs-qt {
inherit system;
config = {
allowUnfree = true;
permittedInsecurePackages = [ "qtwebengine-5.15.19" ];
};
};
in
{
"custom-claude-code" = pkgs.custom.claude-code;
"custom-app-launcher-server" = pkgs.custom.app-launcher-server;
"custom-mcrcon-rbw" = pkgs.custom.mcrcon-rbw;
"custom-tea-rbw" = pkgs.custom.tea-rbw;
"custom-rclone-torbox-setup" = pkgs.custom.rclone-torbox-setup;
"custom-opencode" = pkgs.custom.opencode;
"qt-pinned-jellyfin-media-player" = pkgsQt.jellyfin-media-player;
"qt-pinned-stremio" = pkgsQt.stremio;
# Plasma Bigscreen — not yet in nixpkgs, built from upstream
"plasma-bigscreen" = pkgs.kdePackages.callPackage ./roles/plasma-bigscreen/package.nix { };
}
// (
if system == "x86_64-linux" then
{
"custom-nextcloud-talk-desktop" = pkgs.custom.nextcloud-talk-desktop;
# nix-deck kernel from Jovian-NixOS (Steam Deck) - expensive to build
"nix-deck-kernel" = self.nixosConfigurations.nix-deck.config.boot.kernelPackages.kernel;
}
else
{ }
)
);
# Flake apps
apps = nixpkgs.lib.genAttrs [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" ] (
system:
let
pkgs = import nixpkgs { inherit system; };
commonDeps = [
pkgs.curl
pkgs.jq
pkgs.nix
pkgs.git
pkgs.gnused
pkgs.gnugrep
pkgs.coreutils
pkgs.gawk
];
update-doomemacs = pkgs.writeShellScriptBin "update-doomemacs" ''
export PATH="${pkgs.lib.makeBinPath commonDeps}:$PATH"
${builtins.readFile ./scripts/update-doomemacs.sh}
'';
update-claude-code = pkgs.writeShellScriptBin "update-claude-code" ''
export PATH="${pkgs.lib.makeBinPath commonDeps}:$PATH"
${builtins.readFile ./packages/claude-code/update.sh}
'';
update-opencode = pkgs.writeShellScriptBin "update-opencode" ''
export PATH="${pkgs.lib.makeBinPath commonDeps}:$PATH"
${builtins.readFile ./packages/opencode/update.sh}
'';
rotate-wallpaper = pkgs.writeShellScriptBin "rotate-wallpaper" ''
export PATH="${pkgs.lib.makeBinPath commonDeps}:$PATH"
${builtins.readFile ./scripts/rotate-wallpaper.sh}
'';
upgrade = pkgs.writeShellScriptBin "upgrade" ''
export PATH="${pkgs.lib.makeBinPath commonDeps}:$PATH"
${builtins.readFile ./scripts/upgrade.sh}
'';
bootstrap = pkgs.writeShellScriptBin "bootstrap" ''
export PATH="${pkgs.lib.makeBinPath commonDeps}:$PATH"
${builtins.readFile ./scripts/bootstrap.sh}
'';
build-liveusb = pkgs.writeShellScriptBin "build-liveusb" ''
export PATH="${pkgs.lib.makeBinPath commonDeps}:$PATH"
${builtins.readFile ./scripts/build-liveusb.sh}
'';
in
{
update-doomemacs = {
type = "app";
program = "${update-doomemacs}/bin/update-doomemacs";
meta.description = "Update Doom Emacs configuration";
};
update-claude-code = {
type = "app";
program = "${update-claude-code}/bin/update-claude-code";
meta.description = "Update Claude Code package version";
};
update-opencode = {
type = "app";
program = "${update-opencode}/bin/update-opencode";
meta.description = "Update OpenCode package version";
};
rotate-wallpaper = {
type = "app";
program = "${rotate-wallpaper}/bin/rotate-wallpaper";
meta.description = "Rotate desktop wallpaper";
};
upgrade = {
type = "app";
program = "${upgrade}/bin/upgrade";
meta.description = "Upgrade NixOS configuration";
};
bootstrap = {
type = "app";
program = "${bootstrap}/bin/bootstrap";
meta.description = "Bootstrap a new NixOS machine";
};
build-liveusb = {
type = "app";
program = "${build-liveusb}/bin/build-liveusb";
meta.description = "Build a bootable Live USB ISO";
};
}
);
};
nixosConfigurations.wixos = nixpkgs.lib.nixosSystem rec {
system = "x86_64-linux";
modules = nixosModules ++ [
nixos-wsl.nixosModules.default
./machines/wixos/configuration.nix
inputs.home-manager.nixosModules.home-manager
{
home-manager.users.johno = import ./home/home-desktop.nix;
home-manager.extraSpecialArgs = { inherit system; };
}
];
};
nixosConfigurations.zix790prors = nixpkgs.lib.nixosSystem rec {
system = "x86_64-linux";
modules = nixosModules ++ [
./machines/zix790prors/configuration.nix
inputs.home-manager.nixosModules.home-manager
{
home-manager.users.johno = import ./home/home-desktop.nix;
home-manager.extraSpecialArgs = { inherit system; };
}
];
};
# Live USB ISO configuration
nixosConfigurations.live-usb = nixpkgs.lib.nixosSystem rec {
system = "x86_64-linux";
modules = nixosModules ++ [
./machines/live-usb/configuration.nix
{
home-manager.users.nixos = import ./home/home-live-usb.nix;
home-manager.extraSpecialArgs = { inherit system; };
}
];
};
# Steam Deck configuration (using unstable for better Jovian compatibility)
nixosConfigurations.nix-deck = nixpkgs-unstable.lib.nixosSystem rec {
system = "x86_64-linux";
modules = nixosModulesUnstable ++ [
./machines/nix-deck/configuration.nix
{
home-manager.users.johno = import ./home/home-desktop.nix;
home-manager.extraSpecialArgs = { inherit system; };
}
];
};
# Darwin/macOS configurations
darwinConfigurations."blkfv4yf49kt7" = inputs.nix-darwin.lib.darwinSystem rec {
system = "aarch64-darwin";
modules = darwinModules ++ [
./machines/johno-macbookpro/configuration.nix
{
home-manager.users.johno = import ./home/home-darwin-work.nix;
home-manager.extraSpecialArgs = { inherit system; };
}
];
};
# 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

@@ -107,7 +107,7 @@
aerospace = {
enable = true;
leader = "cmd";
ctrlShortcuts.enable = false;
ctrlShortcuts.enable = true;
sketchybar.enable = true;
# Optional: Add per-machine userSettings overrides
# userSettings = {

View File

@@ -10,7 +10,6 @@
home.roles = {
"3d-printing".enable = true;
base.enable = true;
gaming.enable = true;
desktop.enable = true;
emacs.enable = true;
email.enable = true;
@@ -24,7 +23,6 @@
kubectl.enable = true;
tmux.enable = true;
plasma-manager.enable = true;
starship.enable = true;
};
targets.genericLinux.enable = true;

View File

@@ -1,30 +0,0 @@
{ pkgs, globalInputs, system, ... }:
{
# Home Manager configuration for kodi user on boxy
# Focused on media center volume control via Home Assistant
home.username = "kodi";
home.homeDirectory = "/home/kodi";
home.stateVersion = "24.05";
# Enable minimal roles for kodi user
home.roles = {
base.enable = true;
plasma-manager-kodi.enable = true;
kdeconnect.enable = true;
};
home.packages = with pkgs; [
kdePackages.kconfig
];
targets.genericLinux.enable = true;
home.sessionVariables = {};
home.sessionPath = [];
imports = [
./roles
./roles/base-linux
];
}

View File

@@ -12,7 +12,6 @@
home.roles = {
base.enable = true;
desktop.enable = true;
gaming.enable = true;
development.enable = true;
communication.enable = true;
email.enable = true;
@@ -24,7 +23,6 @@
plasma-manager.enable = true;
emacs.enable = true;
i3_sway.enable = true;
starship.enable = true;
# Launcher wrappers for excluded/optional packages
launchers = {

View File

@@ -14,14 +14,8 @@
desktop.enable = true;
tmux.enable = true;
plasma-manager.enable = true;
emacs = {
enable = true;
# Use pre-built Doom Emacs - all packages built at nix build time
# This means no doom sync is needed after booting the live USB
prebuiltDoom = true;
};
emacs.enable = true;
i3_sway.enable = true;
starship.enable = true;
# development.enable = false; # Not needed for live USB
# communication.enable = false; # Not needed for live USB
# office.enable = false; # Not needed for live USB

View File

@@ -12,7 +12,6 @@
home.roles = {
base.enable = true;
desktop.enable = true;
gaming.enable = true;
media.enable = true;
communication.enable = true;
kdeconnect.enable = true;
@@ -21,7 +20,6 @@
plasma-manager.enable = true;
emacs.enable = true;
i3_sway.enable = true;
starship.enable = true;
# office.enable = false; # Not needed for media center
# sync.enable = false; # Shared machine, no personal file sync
};

View File

@@ -1,27 +0,0 @@
{ pkgs, globalInputs, system, ... }:
{
# Home Manager configuration for servers (minimal with development tools)
home.username = "johno";
home.homeDirectory = "/home/johno";
home.stateVersion = "24.05";
# Minimal roles for server with development capability
home.roles = {
base.enable = true;
development.enable = true;
emacs.enable = true;
kubectl.enable = true;
starship.enable = true;
tmux.enable = true;
};
targets.genericLinux.enable = true;
home.sessionVariables = {};
home.sessionPath = [];
imports = [
./roles
./roles/base-linux
];
}

View File

@@ -632,9 +632,7 @@ in
text = ''
#!/bin/bash
# Monitor /System/Volumes/Data which contains user data on APFS
# The root / is a read-only snapshot with minimal usage
DISK_USAGE=$(df -H /System/Volumes/Data | grep -v Filesystem | awk '{print $5}')
DISK_USAGE=$(df -H / | grep -v Filesystem | awk '{print $5}')
${pkgs.sketchybar}/bin/sketchybar --set $NAME label="$DISK_USAGE"
'';

View File

@@ -3,7 +3,6 @@
# Includes Linux-specific roles that require Linux-only home-manager modules
imports = [
../plasma-manager
../plasma-manager-kodi
../i3+sway
];
}

View File

@@ -1,9 +1,4 @@
{
config,
lib,
pkgs,
...
}:
{ config, lib, pkgs, ... }:
with lib;
@@ -23,13 +18,10 @@ in
htop
killall
less
lnav
ncdu
shellcheck
tmux
tree
watch
custom.opencode
];
# Automatic garbage collection for user profile (home-manager generations).
@@ -60,7 +52,6 @@ in
programs.git = {
enable = true;
signing.format = null;
settings = {
user.name = "John Ogle";
user.email = "john@ogle.fyi";

View File

@@ -4,7 +4,6 @@ with lib;
let
cfg = config.home.roles.communication;
isLinux = pkgs.stdenv.isLinux;
in
{
options.home.roles.communication = {
@@ -13,14 +12,14 @@ in
config = mkIf cfg.enable {
home.packages = [
# For logging back into google chat (cross-platform)
globalInputs.google-cookie-retrieval.packages.${system}.default
] ++ optionals isLinux [
# Linux-only communication apps (Electron apps don't build on Darwin)
# Communication apps
pkgs.element-desktop
# Re-enabled in 25.11 after security issues were resolved
pkgs.fluffychat
pkgs.custom.nextcloud-talk-desktop
pkgs.nextcloud-talk-desktop
# For logging back into google chat
globalInputs.google-cookie-retrieval.packages.${system}.default
];
};
}

View File

@@ -18,6 +18,5 @@
./sync
./tmux
./emacs
./starship
];
}

View File

@@ -4,7 +4,6 @@ with lib;
let
cfg = config.home.roles.desktop;
isLinux = pkgs.stdenv.isLinux;
in
{
options.home.roles.desktop = {
@@ -13,63 +12,61 @@ in
config = mkIf cfg.enable {
home.packages = with pkgs; [
# Cross-platform desktop applications
# Desktop applications
bitwarden-desktop
keepassxc
xdg-utils # XDG utilities for opening files/URLs with default applications
] ++ optionals isLinux [
# Linux-only desktop applications
dunst
keepassxc
unstable.ghostty
# Linux-only desktop utilities
# Desktop utilities
feh # Image viewer and wallpaper setter for X11
rofi # Application launcher for X11
solaar # Logitech management software
waybar
wofi # Application launcher for Wayland
# Linux-only system utilities with GUI components
xdg-utils # XDG utilities for opening files/URLs with default applications
# System utilities with GUI components
(snapcast.override { pulseaudioSupport = true; })
# KDE tiling window management (Linux-only)
# KDE tiling window management
kdePackages.krohnkite # Dynamic tiling extension for KWin 6
# KDE PIM applications for email, calendar, and contacts (Linux-only)
# KDE PIM applications for email, calendar, and contacts
kdePackages.kmail
kdePackages.kmail-account-wizard
kdePackages.kmailtransport
kdePackages.korganizer
kdePackages.kaddressbook
kdePackages.kontact
# KDE System components needed for proper integration (Linux-only)
# KDE System components needed for proper integration
kdePackages.kded
kdePackages.systemsettings
kdePackages.kmenuedit
# Desktop menu support (Linux-only)
# Desktop menu support
kdePackages.plasma-desktop # Contains applications.menu
# KDE Online Accounts support (Linux-only)
# KDE Online Accounts support
kdePackages.kaccounts-integration
kdePackages.kaccounts-providers
kdePackages.signond
# KDE Mapping (Linux-only)
# KDE Mapping
kdePackages.marble # Virtual globe and world atlas
# KDE Productivity (Linux-only)
# KDE Productivity
kdePackages.kate # Advanced text editor with syntax highlighting
kdePackages.okular # Universal document viewer (PDF, ePub, etc.)
kdePackages.spectacle # Screenshot capture utility
kdePackages.filelight # Visual disk usage analyzer
# KDE Multimedia (Linux-only)
# KDE Multimedia
kdePackages.gwenview # Image viewer and basic editor
kdePackages.elisa # Music player
# KDE System Utilities (Linux-only)
# KDE System Utilities
kdePackages.ark # Archive manager (zip, tar, 7z, etc.)
kdePackages.yakuake # Drop-down terminal emulator
];
@@ -80,66 +77,61 @@ in
programs.spotify-player.enable = true;
# Linux-only: GNOME keyring service
services.gnome-keyring = mkIf isLinux {
services.gnome-keyring = {
enable = true;
};
# Linux-only: systemd user services for rbw vault unlock
systemd.user.services = mkIf isLinux {
# rbw vault unlock on login
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" ];
};
# 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" ];
};
# rbw vault unlock on resume from suspend
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" ];
};
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" ];
};
};
# Linux-only: KDE environment variables for proper integration
home.sessionVariables = mkIf isLinux {
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";
KDE_SESSION_VERSION = "6";
};
xdg = {
enable = true;
# Ensure desktop files are made available for discovery
desktopEntries = {}; # This creates the desktop files directory structure
mimeApps = {
enable = true;
associations.added = {
@@ -149,14 +141,13 @@ in
"x-scheme-handler/https" = "firefox.desktop";
};
defaultApplications = {
# Web browsers (cross-platform)
# Web browsers
"text/html" = "firefox.desktop";
"x-scheme-handler/http" = "firefox.desktop";
"x-scheme-handler/https" = "firefox.desktop";
"x-scheme-handler/about" = "firefox.desktop";
"x-scheme-handler/unknown" = "firefox.desktop";
} // optionalAttrs isLinux {
# Linux-only: KDE application associations
# Documents
"application/pdf" = "okular.desktop";
"text/plain" = "kate.desktop";
@@ -164,7 +155,7 @@ in
"text/x-c" = "kate.desktop";
"text/x-python" = "kate.desktop";
"application/x-shellscript" = "kate.desktop";
# Images
"image/png" = "gwenview.desktop";
"image/jpeg" = "gwenview.desktop";
@@ -173,25 +164,25 @@ in
"image/bmp" = "gwenview.desktop";
"image/tiff" = "gwenview.desktop";
"image/webp" = "gwenview.desktop";
# Archives
"application/zip" = "ark.desktop";
"application/x-tar" = "ark.desktop";
"application/x-compressed-tar" = "ark.desktop";
"application/x-7z-compressed" = "ark.desktop";
"application/x-rar" = "ark.desktop";
# Audio
"audio/mpeg" = "elisa.desktop";
"audio/mp4" = "elisa.desktop";
"audio/flac" = "elisa.desktop";
"audio/ogg" = "elisa.desktop";
"audio/wav" = "elisa.desktop";
# Email
"message/rfc822" = "kmail.desktop";
"x-scheme-handler/mailto" = "kmail.desktop";
# Calendar
"text/calendar" = "korganizer.desktop";
"application/x-vnd.akonadi.calendar.event" = "korganizer.desktop";
@@ -199,11 +190,9 @@ in
};
};
# Linux-only: Fix for KDE applications.menu file issue on Plasma 6
# Fix for KDE applications.menu file issue on Plasma 6
# KDE still looks for applications.menu but Plasma 6 renamed it to plasma-applications.menu
xdg.configFile."menus/applications.menu" = mkIf isLinux {
source = "${pkgs.kdePackages.plasma-workspace}/etc/xdg/menus/plasma-applications.menu";
};
xdg.configFile."menus/applications.menu".source = "${pkgs.kdePackages.plasma-workspace}/etc/xdg/menus/plasma-applications.menu";
# Note: modules must be imported at top-level home config
};

View File

@@ -5,7 +5,7 @@ with lib;
let
cfg = config.home.roles.development;
# Fetch the claude-plugins repository (for humanlayer commands/agents)
# 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";
@@ -14,13 +14,6 @@ let
rev = "5e3e4d937162185b6d78c62022cbfd1c8ad42c4c";
ref = "main";
};
# Claude Code statusline: shows model, cwd, git branch, and context usage %
claudeCodeStatusLineConfig = pkgs.writeText "claude-statusline.json" (builtins.toJSON {
type = "command";
command = ''input=$(cat); model=$(echo "$input" | jq -r '.model.display_name'); cwd=$(echo "$input" | jq -r '.workspace.current_dir'); if git -C "$cwd" rev-parse --git-dir > /dev/null 2>&1; then branch=$(git -C "$cwd" --no-optional-locks rev-parse --abbrev-ref HEAD 2>/dev/null || echo ""); if [ -n "$branch" ]; then git_info=" on $branch"; else git_info=""; fi; else git_info=""; fi; usage=$(echo "$input" | jq '.context_window.current_usage'); if [ "$usage" != "null" ]; then current=$(echo "$usage" | jq '.input_tokens + .cache_creation_input_tokens + .cache_read_input_tokens'); size=$(echo "$input" | jq '.context_window.context_window_size'); pct=$((current * 100 / size)); context_info=" | ''${pct}% context"; else context_info=""; fi; printf "%s in %s%s%s" "$model" "$cwd" "$git_info" "$context_info"'';
});
in
{
options.home.roles.development = {
@@ -44,12 +37,11 @@ in
config = mkIf cfg.enable {
home.packages = [
pkgs.unstable.claude-code
pkgs.unstable.claude-code-router
pkgs.unstable.codex
pkgs.sqlite
# Custom packages
pkgs.custom.tea-rbw
pkgs.custom.pi-coding-agent
];
# Install Claude Code humanlayer command and agent plugins
@@ -58,9 +50,6 @@ in
rm -f ~/.claude/commands/humanlayer:* 2>/dev/null || true
rm -f ~/.claude/agents/humanlayer:* 2>/dev/null || true
# Remove explicitly blocked commands that may have been installed previously
rm -f ~/.claude/commands/humanlayer:create_handoff.md 2>/dev/null || true
# Create directories if they don't exist
mkdir -p ~/.claude/commands
mkdir -p ~/.claude/agents
@@ -69,21 +58,13 @@ in
for file in ${claudePluginsRepo}/humanlayer/commands/*.md; do
if [ -f "$file" ]; then
filename=$(basename "$file" .md)
# Skip blocked commands
case "$filename" in
create_handoff) continue ;;
esac
dest="$HOME/.claude/commands/humanlayer:''${filename}.md"
rm -f "$dest" 2>/dev/null || true
# 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\""
}
chmod u+w "$dest" 2>/dev/null || true
fi
done
@@ -92,30 +73,20 @@ in
if [ -f "$file" ]; then
filename=$(basename "$file" .md)
dest="$HOME/.claude/agents/humanlayer:''${filename}.md"
rm -f "$dest" 2>/dev/null || true
# 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\""
}
chmod u+w "$dest" 2>/dev/null || true
fi
done
$DRY_RUN_CMD echo "Claude Code plugins installed: humanlayer commands/agents"
'';
# Configure Claude Code statusline (merge into existing settings.json)
home.activation.claudeCodeStatusLine = lib.hm.dag.entryAfter ["writeBoundary"] ''
SETTINGS="$HOME/.claude/settings.json"
mkdir -p "$HOME/.claude"
if [ -f "$SETTINGS" ]; then
${pkgs.jq}/bin/jq --slurpfile sl ${claudeCodeStatusLineConfig} '.statusLine = $sl[0]' "$SETTINGS" > "''${SETTINGS}.tmp" && mv "''${SETTINGS}.tmp" "$SETTINGS"
else
${pkgs.jq}/bin/jq -n --slurpfile sl ${claudeCodeStatusLineConfig} '{statusLine: $sl[0]}' > "$SETTINGS"
fi
$DRY_RUN_CMD echo "Claude Code statusline configured"
$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

@@ -1,9 +1,4 @@
{
config,
lib,
pkgs,
...
}:
{ config, lib, pkgs, ... }:
with lib;
@@ -13,8 +8,8 @@ let
doomEmacs = pkgs.fetchFromGitHub {
owner = "doomemacs";
repo = "doomemacs";
rev = "d23bbe87721c61f4d5a605f2914b32780bb89949";
sha256 = "sha256-z+3c0AGkrMf1xZ+pq57aVp4Zo4KsqFMIjEVzSZinghc=";
rev = "762f47805ac2a6411e11747f86f7c19a03da326e";
sha256 = "sha256-0w0eXGB2cgxu/hr5wTiJSZDJw0NF+fZvLbzEylH5URU=";
};
# Shared emacs packages
@@ -25,94 +20,58 @@ let
# Default emacs configuration with vterm support
defaultEmacsPackage =
if pkgs.stdenv.isDarwin then
pkgs.emacs-macport.pkgs.withPackages emacsPackages
else
pkgs.emacs.pkgs.withPackages emacsPackages;
# Path to doom config directory (relative to this file)
doomConfigDir = ./doom;
if pkgs.stdenv.isDarwin
then pkgs.emacs-macport.pkgs.withPackages emacsPackages
else pkgs.emacs.pkgs.withPackages emacsPackages;
in
{
options.home.roles.emacs = {
enable = mkEnableOption "Doom Emacs with vterm and tree-sitter support";
prebuiltDoom = mkOption {
type = types.bool;
default = false;
description = ''
Use nix-doom-emacs-unstraightened to pre-build all Doom packages at
nix build time. This eliminates the need to run `doom sync` after
first boot, making it ideal for live USB images or immutable systems.
When enabled, the doom configuration is read-only (stored in nix store).
'';
};
};
config = mkIf cfg.enable (mkMerge [
# Common configuration for both modes
{
home.packages = [
pkgs.emacs-all-the-icons-fonts
pkgs.fira-code
pkgs.fontconfig
pkgs.graphviz
pkgs.isort
pkgs.nerd-fonts.fira-code
pkgs.nerd-fonts.droid-sans-mono
pkgs.nil # nix lsp language server
pkgs.nixfmt
(pkgs.ripgrep.override { withPCRE2 = true; })
pkgs.pipenv
pkgs.poetry
pkgs.python3
];
config = mkIf cfg.enable {
home.packages = [
pkgs.emacs-all-the-icons-fonts
pkgs.fira-code
pkgs.fontconfig
pkgs.graphviz
pkgs.isort
pkgs.nerd-fonts.fira-code
pkgs.nerd-fonts.droid-sans-mono
pkgs.nil # nix lsp language server
pkgs.nixfmt-rfc-style
(pkgs.ripgrep.override {withPCRE2 = true;})
pkgs.pipenv
pkgs.poetry
pkgs.python3
];
fonts.fontconfig.enable = true;
}
programs.emacs = {
enable = true;
package = defaultEmacsPackage;
};
# Standard Doom Emacs mode (requires doom sync at runtime)
(mkIf (!cfg.prebuiltDoom) {
programs.emacs = {
enable = true;
package = defaultEmacsPackage;
};
fonts.fontconfig.enable = true;
# Mount emacs and tree-sitter grammars from nix store
home.file = {
"${config.xdg.configHome}/emacs".source = doomEmacs;
};
# Mount emacs and tree-sitter grammars from nix store
home.file = {
"${config.xdg.configHome}/emacs".source = doomEmacs;
};
home.sessionPath = [
"${config.xdg.configHome}/emacs/bin"
];
home.sessionPath = [
"${config.xdg.configHome}/emacs/bin"
];
home.sessionVariables = {
DOOMDIR = "${config.xdg.configHome}/doom";
DOOMLOCALDIR = "${config.xdg.dataHome}/doom";
};
home.sessionVariables = {
DOOMDIR = "${config.xdg.configHome}/doom";
DOOMLOCALDIR = "${config.xdg.dataHome}/doom";
};
# TODO: Use mkOutOfStoreSymlink instead?
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/roles/emacs/doom" "${config.xdg.configHome}/doom"
'';
})
# Pre-built Doom Emacs mode (no doom sync needed - ideal for live USB)
(mkIf cfg.prebuiltDoom {
programs.doom-emacs = {
enable = true;
doomDir = doomConfigDir;
doomLocalDir = "${config.xdg.dataHome}/doom";
# Add extra packages that aren't part of Doom but needed for our config
extraPackages = epkgs: [
epkgs.vterm
epkgs.treesit-grammars.with-all-grammars
];
};
})
]);
# TODO: Use mkOutOfStoreSymlink instead?
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/roles/emacs/doom" "${config.xdg.configHome}/doom"
'';
};
}

View File

@@ -28,7 +28,7 @@
;; up, `M-x eval-region' to execute elisp code, and 'M-x doom/reload-font' to
;; refresh your font settings. If Emacs still can't find your font, it likely
;; wasn't installed correctly. Font issues are rarely Doom issues!
(setq doom-font (font-spec :family "Fira Code" :size 16))
(setq doom-font (font-spec :family "Fira Code"))
;; Auto-install nerd-icons fonts if they're missing
(defun my/ensure-nerd-icons-fonts ()
@@ -53,22 +53,6 @@
;; change `org-directory'. It must be set before org loads!
(setq org-directory "~/org/")
(after! org
;; Skip recurring events past their CALDAV_UNTIL date
;; org-caldav ignores UNTIL from RRULE, so we store it as a property
;; and filter here in the agenda
(defun my/skip-if-past-until ()
"Return non-nil if entry has CALDAV_UNTIL and current date is past it."
(let ((until-str (org-entry-get nil "CALDAV_UNTIL")))
(when (and until-str
(string-match "^\\([0-9]\\{4\\}\\)\\([0-9]\\{2\\}\\)\\([0-9]\\{2\\}\\)" until-str))
(let* ((until-year (string-to-number (match-string 1 until-str)))
(until-month (string-to-number (match-string 2 until-str)))
(until-day (string-to-number (match-string 3 until-str)))
(until-time (encode-time 0 0 0 until-day until-month until-year))
(today (current-time)))
(when (time-less-p until-time today)
(org-end-of-subtree t))))))
(setq org-agenda-span 'week
org-agenda-start-with-log-mode t
my-agenda-dirs '("projects" "roam")
@@ -77,7 +61,6 @@
"\.org$"))
my-agenda-dirs))
org-log-done 'time
org-agenda-skip-function-global #'my/skip-if-past-until
org-agenda-custom-commands '(("n" "Agenda"
((agenda "")
(tags-todo "-someday-recurring")))
@@ -88,10 +71,7 @@
org-journal-file-format "%Y-%m-%d.org"
org-capture-templates
'(("t" "Todo" entry (file+headline "~/org/todo.org" "Inbox")
"* TODO %? \n %i \n%a" :prepend t)))
;; Make blocked tasks more visible in agenda (they have subtasks to do!)
(custom-set-faces!
'(org-agenda-dimmed-todo-face :foreground "#bb9af7" :weight normal)))
"* TODO %? \n %i \n%a" :prepend t))))
(map! :after org-agenda
:map org-agenda-mode-map
@@ -100,135 +80,25 @@
"d" #'org-agenda-day-view
"w" #'org-agenda-week-view))
;; org-caldav: Sync Org entries with Nextcloud CalDAV
;; Setup requirements:
;; 1. Create Nextcloud app password: Settings -> Security -> Devices & sessions
;; 2. Store in rbw: rbw add nextcloud-caldav (put app password as the secret)
;; 3. Run: doom sync
;; 4. Test: M-x my/org-caldav-sync-with-rbw (or SPC o a s)
;;
;; Note: Conflict resolution is "Org always wins" - treat Org as source of truth
;; for entries that originated in Org.
;; (use-package! org-caldav
;; :defer t
;; :config
;; (setq org-caldav-url "https://nextcloud.johnogle.info/remote.php/dav/calendars/johno"
;; org-caldav-calendar-id "personal"
;; org-icalendar-timezone "America/Los_Angeles"
;; org-caldav-inbox "~/org/calendar.org"
;; org-caldav-files nil
;; org-caldav-sync-direction 'cal->org))
;; Define sync wrapper before use-package (so keybinding works)
(defun my/org-caldav-sync-with-rbw ()
"Run org-caldav-sync with credentials from rbw embedded in URL."
(interactive)
(require 'org)
(require 'org-caldav)
(let* ((password (my/get-rbw-password "nextcloud-caldav"))
;; Embed credentials in URL (url-encode password in case of special chars)
(encoded-pass (url-hexify-string password)))
(setq org-caldav-url
(format "https://johno:%s@nextcloud.johnogle.info/remote.php/dav/calendars/johno"
encoded-pass))
(org-caldav-sync)))
(use-package! org-caldav
:after org
:commands (org-caldav-sync my/org-caldav-sync-with-rbw)
:init
(map! :leader
(:prefix ("o" . "open")
(:prefix ("a" . "agenda/calendar")
:desc "Sync CalDAV" "s" #'my/org-caldav-sync-with-rbw)))
:config
;; Nextcloud CalDAV base URL (credentials added dynamically by sync wrapper)
(setq org-caldav-url "https://nextcloud.johnogle.info/remote.php/dav/calendars/johno")
;; Timezone for iCalendar export
(setq org-icalendar-timezone "America/Los_Angeles")
;; Sync state storage (in org directory for multi-machine sync)
(setq org-caldav-save-directory (expand-file-name ".org-caldav/" org-directory))
;; Backup file for entries before modification
(setq org-caldav-backup-file (expand-file-name ".org-caldav/backup.org" org-directory))
;; Limit past events to 30 days (avoids uploading years of scheduled tasks)
(setq org-caldav-days-in-past 30)
;; Sync behavior: bidirectional by default
(setq org-caldav-sync-direction 'twoway)
;; What changes from calendar sync back to Org (conservative: title and timestamp only)
(setq org-caldav-sync-changes-to-org 'title-and-timestamp)
;; Deletion handling: never auto-delete to prevent accidental mass deletion
(setq org-caldav-delete-calendar-entries 'never)
(setq org-caldav-delete-org-entries 'never)
;; Enable TODO/VTODO sync
(setq org-icalendar-include-todo 'all)
(setq org-caldav-sync-todo t)
;; Map VTODO percent-complete to org-todo-keywords
;; Format: (PERCENT "KEYWORD") - percent thresholds map to states
(setq org-caldav-todo-percent-states
'((0 "TODO")
(25 "WAIT")
(50 "IN-PROGRESS")
(100 "DONE")
(100 "KILL")))
;; Allow export with broken links (mu4e links can't be resolved during export)
(setq org-export-with-broken-links 'mark)
;; Calendar-specific configuration
(setq org-caldav-calendars
'(;; Personal calendar: two-way sync with family-shared Nextcloud calendar
(:calendar-id "personal"
:inbox "~/org/personal-calendar.org"
:files ("~/org/personal-calendar.org"))
;; Tasks calendar: one-way sync (org → calendar only)
;; SCHEDULED/DEADLINE items from todo.org push to private Tasks calendar.
;; No inbox = no download from calendar (effectively one-way).
;; Note: Create 'tasks' calendar in Nextcloud first, keep it private.
(:calendar-id "tasks"
:files ("~/org/todo.org"))))
;; Handle UNTIL in recurring events
;; org-caldav ignores UNTIL from RRULE - events repeat forever.
;; This advice extracts UNTIL and stores it as a property for agenda filtering.
(defun my/org-caldav-add-until-property (orig-fun eventdata-alist)
"Advice to store CALDAV_UNTIL property for recurring events."
(let ((result (funcall orig-fun eventdata-alist)))
(let* ((rrule-props (alist-get 'rrule-props eventdata-alist))
(until-str (cadr (assoc 'UNTIL rrule-props)))
(summary (alist-get 'summary eventdata-alist)))
;; Debug: log what we're seeing
(message "CALDAV-DEBUG: %s | rrule-props: %S | until: %s"
(or summary "?") rrule-props until-str)
(when until-str
(save-excursion
(org-back-to-heading t)
(org-entry-put nil "CALDAV_UNTIL" until-str))))
result))
(advice-add 'org-caldav-insert-org-event-or-todo
:around #'my/org-caldav-add-until-property)
)
(defun my/get-rbw-password (alias &optional no-error)
"Return the password for ALIAS via rbw, unlocking the vault only if needed.
If NO-ERROR is non-nil, return nil instead of signaling an error when
rbw is unavailable or the entry is not found."
(if (not (executable-find "rbw"))
(if no-error
nil
(user-error "rbw: not installed or not in PATH"))
(let* ((cmd (format "rbw get %s 2>/dev/null" (shell-quote-argument alias)))
(output (string-trim (shell-command-to-string cmd))))
(if (string-empty-p output)
(if no-error
nil
(user-error "rbw: no entry found for '%s' - run: rbw add %s" alias alias))
output))))
(defun my/get-rbw-password (alias)
"Return the password for ALIAS via rbw, unlocking the vault only if needed."
(let* ((cmd (format "rbw get %s 2>&1" alias))
(output (shell-command-to-string cmd)))
(string-trim output)))
(after! gptel
:config
(setq! gptel-api-key (my/get-rbw-password "openai-api-key-chatgpt-el" t)
(setq! gptel-api-key (my/get-rbw-password "openai-api-key-chatgpt-el")
gptel-default-mode 'org-mode
gptel-use-tools t
gptel-confirm-tool-calls 'always
@@ -274,37 +144,13 @@ rbw is unavailable or the entry is not found."
(error (format "Error listing directory %s: %s" dirpath (error-message-string err)))))
:args (list '(:name "dirpath" :type "string" :description "Directory path to list"))))
(use-package! pi-coding-agent
:commands (pi-coding-agent pi-coding-agent-toggle)
:init
(defalias 'pi 'pi-coding-agent)
(map! :leader
(:prefix ("o" . "open")
:desc "Pi Coding Agent" "p" #'pi-coding-agent))
:config
;; Tree-sitter grammars are managed by Nix (treesit-grammars.with-all-grammars),
;; so suppress the auto-install prompt
(setq pi-coding-agent-essential-grammar-action 'warn))
(use-package! claude-code-ide
:commands (claude-code-ide-menu claude-code-ide-open-here)
:init
(map! :leader
(:prefix ("o" . "open")
:desc "Claude Code IDE" "c" #'claude-code-ide-menu))
:defer t
:config
(claude-code-ide-emacs-tools-setup)
(setq claude-code-ide-cli-path "claude"
claude-code-ide-cli-extra-flags "--dangerously-skip-permissions"
claude-code-ide-focus-claude-after-ediff t
claude-code-ide-focus-on-open t
claude-code-ide-show-claude-window-in-ediff t
claude-code-ide-switch-tab-on-ediff t
claude-code-ide-use-ide-diff t
claude-code-ide-use-side-window t
claude-code-ide-window-height 20
claude-code-ide-window-side 'right
claude-code-ide-window-width 90))
(map! :leader
(:prefix ("o" . "open")
:desc "Claude Code IDE" "c" #'claude-code-ide-menu)))
(after! gptel
(require 'gptel-tool-library)
@@ -313,53 +159,31 @@ rbw is unavailable or the entry is not found."
(dolist (module '("bbdb" "buffer" "elisp" "emacs" "gnus" "os" "search-and-replace" "url"))
(gptel-tool-library-load-module module)))
;; mu4e email configuration
;; Add NixOS mu4e to load-path (installed via mu.mu4e package)
(when-let ((mu-path (executable-find "mu")))
(add-to-list 'load-path
(expand-file-name "../share/emacs/site-lisp/mu4e"
(file-name-directory mu-path))))
;; Notmuch email configuration
(after! notmuch
(setq notmuch-search-oldest-first nil
notmuch-show-logo nil
notmuch-fcc-dirs "proton/Sent"
(after! mu4e
;; User identity
(setq user-mail-address "john@ogle.fyi"
user-full-name "John Ogle")
;; User identity
user-mail-address "john@ogle.fyi"
user-full-name "John Ogle"
;; Maildir location (no account prefix - single account)
(setq mu4e-maildir "~/Mail"
mu4e-attachment-dir "~/Downloads")
;; 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
;; Folder config (matches ~/Mail/INBOX, ~/Mail/Sent, etc.)
(setq mu4e-sent-folder "/Sent"
mu4e-drafts-folder "/Drafts"
mu4e-trash-folder "/Trash"
mu4e-refile-folder "/Archive")
;; Shortcuts for common folders
(setq mu4e-maildir-shortcuts
'((:maildir "/INBOX" :key ?i)
(:maildir "/Archive" :key ?a)
(:maildir "/Sent" :key ?s)
(:maildir "/Trash" :key ?t)))
;; Behavior settings
(setq mu4e-get-mail-command "mbsync -a"
mu4e-update-interval 300 ; 5 minutes (matches systemd timer)
mu4e-change-filenames-when-moving t ; required for mbsync
mu4e-headers-date-format "%Y-%m-%d"
mu4e-headers-time-format "%H:%M")
;; Sending mail via msmtp
;; NOTE: message-sendmail-f-is-evil and --read-envelope-from are required
;; to prevent msmtp from stripping the email body when processing headers.
;; Without these, multipart messages (especially from org-msg) may arrive
;; with empty bodies.
(setq sendmail-program (executable-find "msmtp")
send-mail-function #'message-send-mail-with-sendmail
message-send-mail-function #'message-send-mail-with-sendmail
message-sendmail-f-is-evil t
message-sendmail-extra-arguments '("--read-envelope-from")
message-sendmail-envelope-from 'header))
;; 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

@@ -176,8 +176,8 @@
;;zig ; C, but simpler
:email
(mu4e +org)
;;notmuch
;;(mu4e +org +gmail)
notmuch
;;(wanderlust +gmail)
:app

View File

@@ -49,26 +49,13 @@
;; ...Or *all* packages (NOT RECOMMENDED; will likely break things)
;; (unpin! t)
(package! org-caldav)
;; Pin org-msg - upstream doom pin references a force-pushed commit
(package! org-msg :pin "aa608b399586fb771ad37045a837f8286a0b6124")
;; Note: Packages with custom recipes must be pinned for nix-doom-emacs-unstraightened
;; to build deterministically. Update pins when upgrading packages.
;; (package! org-caldav)
(package! gptel :recipe (:nonrecursive t))
(package! claude-code-ide
:recipe (:host github :repo "manzaltu/claude-code-ide.el")
:pin "760240d7f03ff16f90ede9d4f4243cd94f3fed73")
:recipe (:host github :repo "manzaltu/claude-code-ide.el"))
(package! gptel-tool-library
:recipe (:host github :repo "aard-fi/gptel-tool-library"
:files ("*.el"))
:pin "baffc3b0d74a2b7cbda0d5cd6dd7726d6ccaca83")
(package! pi-coding-agent
:recipe (:host github :repo "dnouri/pi-coding-agent"
:files ("*.el"))
:pin "8d8158b0a6150ce13d91e561a1223790670acaa7")
:files ("*.el")))

View File

@@ -4,19 +4,17 @@ with lib;
let
cfg = config.home.roles.email;
isLinux = pkgs.stdenv.isLinux;
in
{
options.home.roles.email = {
enable = mkEnableOption "Enable email with mu4e, mbsync, and msmtp";
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
mu # email indexer for mu4e
mu.mu4e # mu4e elisp files for Emacs
notmuch # email indexing and search
openssl # for certificate management
];
@@ -44,28 +42,34 @@ in
Inbox ~/Mail/INBOX
SubFolders Verbatim
# Channel Configuration - Main (excludes Sent)
Channel proton-main
# Channel Configuration - Sync All
Channel proton
Far :proton-remote:
Near :proton-local:
Patterns * !Sent
Patterns *
Create Both
Expunge Both
SyncState *
'';
# Channel Configuration - Sent (pull only)
Channel proton-sent
Far :proton-remote:Sent
Near :proton-local:Sent
Create Near
Expunge Near
Sync Pull
SyncState *
# Notmuch configuration
home.file.".notmuch-config".text = ''
[database]
path=${config.home.homeDirectory}/Mail
# Group both channels
Group proton
Channel proton-main
Channel proton-sent
[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
@@ -90,38 +94,34 @@ in
account default : proton
'';
# Linux-only: Systemd service for mail sync (Darwin uses launchd instead)
systemd.user.services = mkIf isLinux {
mbsync = {
Unit = {
Description = "Mailbox synchronization service";
After = [ "network-online.target" ];
Wants = [ "network-online.target" ];
};
Service = {
Type = "oneshot";
ExecStart = "${pkgs.bash}/bin/bash -c 'mkdir -p ~/Mail && ${pkgs.isync}/bin/mbsync -a && (${pkgs.mu}/bin/mu info >/dev/null 2>&1 || ${pkgs.mu}/bin/mu init --maildir ~/Mail --personal-address=john@ogle.fyi) && ${pkgs.mu}/bin/mu index'";
Environment = "PATH=${pkgs.rbw}/bin:${pkgs.coreutils}/bin";
StandardOutput = "journal";
StandardError = "journal";
};
# 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";
};
};
# Linux-only: Systemd timer for automatic sync
systemd.user.timers = mkIf isLinux {
mbsync = {
Unit = {
Description = "Mailbox synchronization timer";
};
Timer = {
OnBootSec = "2min";
OnUnitActiveSec = "5min";
Unit = "mbsync.service";
};
Install = {
WantedBy = [ "timers.target" ];
};
# 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

@@ -12,7 +12,9 @@ in
config = mkIf cfg.enable {
home.packages = with pkgs; [
custom.mcrcon-rbw
# Gaming applications would go here
# This role is created for future expansion
# moonlight-qt is currently in media role but could be moved here
];
};
}

View File

@@ -4,8 +4,6 @@ with lib;
let
cfg = config.home.roles.i3_sway;
wallpaperConfig = import ../../wallpapers;
currentWallpaper = builtins.elemAt wallpaperConfig.wallpapers wallpaperConfig.currentIndex;
shared_config = recursiveUpdate rec {
modifier = "Mod4";
@@ -14,7 +12,6 @@ let
keybindings = {
"${shared_config.modifier}+Return" = "exec ${terminal}";
"${shared_config.modifier}+Shift+Return" = "exec ${cfg.browser}";
"${shared_config.modifier}+Shift+q" = "kill";
"${shared_config.modifier}+a" = "focus parent";
@@ -99,12 +96,6 @@ in {
options.home.roles.i3_sway = {
enable = mkEnableOption "i3 and Sway tiling window managers with waybar and rofi";
browser = mkOption {
type = types.str;
default = "firefox --new-window";
description = "Browser to use for new window keybinding";
};
extraSharedConfig = mkOption {
type = types.attrs;
default = {};
@@ -290,7 +281,7 @@ in {
}
# Set wallpaper with feh
{
command = "feh ${currentWallpaper.feh} ${currentWallpaper.file}";
command = "feh --bg-scale ${../../wallpapers/metroid-samus-returns-kz-3440x1440.jpg}";
always = false;
notification = false;
}
@@ -323,7 +314,7 @@ in {
};
output = {
"*" = {
bg = "${currentWallpaper.file} ${currentWallpaper.sway}";
bg = "${../../wallpapers/metroid-samus-returns-kz-3440x1440.jpg} fill";
};
};
startup = [

View File

@@ -4,15 +4,13 @@ with lib;
let
cfg = config.home.roles.kdeconnect;
isLinux = pkgs.stdenv.isLinux;
in
{
options.home.roles.kdeconnect = {
enable = mkEnableOption "Enable KDE Connect for device integration";
};
# KDE Connect services are Linux-only (requires D-Bus and systemd)
config = mkIf (cfg.enable && isLinux) {
config = mkIf cfg.enable {
services.kdeconnect = {
enable = true;
indicator = true;

View File

@@ -1,199 +0,0 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.home.roles.plasma-manager-kodi;
# Define the volume control scripts as derivations
volumeUpScript = pkgs.writeShellScript "avr-volume-up" ''
#!/usr/bin/env bash
# Configuration
HA_URL="https://home-assistant.johnogle.info"
ENTITY_ID="media_player.denon_avr_s970h_2"
MAX_RETRIES=3
# Read token from KDE Wallet and strip whitespace
TOKEN=$(${pkgs.kdePackages.kwallet}/bin/kwallet-query -r ha_avr_token kdewallet -f Passwords 2>/dev/null | tr -d '[:space:]')
if [ -z "$TOKEN" ]; then
${pkgs.libnotify}/bin/notify-send -u critical "Volume Control Error" "Failed to retrieve Home Assistant token from KDE Wallet"
exit 1
fi
# Send volume up command with retry logic
for i in $(seq 1 $MAX_RETRIES); do
RESPONSE=$(${pkgs.curl}/bin/curl -s -w "\n%{http_code}" -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"entity_id\": \"$ENTITY_ID\"}" \
"$HA_URL/api/services/media_player/volume_up" 2>&1)
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
if [ "$HTTP_CODE" = "200" ]; then
exit 0
fi
# Wait before retry (except on last attempt)
if [ $i -lt $MAX_RETRIES ]; then
sleep 0.5
fi
done
# All retries failed
${pkgs.libnotify}/bin/notify-send -u critical "Volume Control Error" "Failed to increase volume after $MAX_RETRIES attempts"
exit 1
'';
volumeDownScript = pkgs.writeShellScript "avr-volume-down" ''
#!/usr/bin/env bash
# Configuration
HA_URL="https://home-assistant.johnogle.info"
ENTITY_ID="media_player.denon_avr_s970h_2"
MAX_RETRIES=3
# Read token from KDE Wallet and strip whitespace
TOKEN=$(${pkgs.kdePackages.kwallet}/bin/kwallet-query -r ha_avr_token kdewallet -f Passwords 2>/dev/null | tr -d '[:space:]')
if [ -z "$TOKEN" ]; then
${pkgs.libnotify}/bin/notify-send -u critical "Volume Control Error" "Failed to retrieve Home Assistant token from KDE Wallet"
exit 1
fi
# Send volume down command with retry logic
for i in $(seq 1 $MAX_RETRIES); do
RESPONSE=$(${pkgs.curl}/bin/curl -s -w "\n%{http_code}" -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"entity_id\": \"$ENTITY_ID\"}" \
"$HA_URL/api/services/media_player/volume_down" 2>&1)
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
if [ "$HTTP_CODE" = "200" ]; then
exit 0
fi
# Wait before retry (except on last attempt)
if [ $i -lt $MAX_RETRIES ]; then
sleep 0.5
fi
done
# All retries failed
${pkgs.libnotify}/bin/notify-send -u critical "Volume Control Error" "Failed to decrease volume after $MAX_RETRIES attempts"
exit 1
'';
volumeMuteScript = pkgs.writeShellScript "avr-volume-mute" ''
#!/usr/bin/env bash
# Configuration
HA_URL="https://home-assistant.johnogle.info"
ENTITY_ID="media_player.denon_avr_s970h_2"
MAX_RETRIES=3
# Read token from KDE Wallet and strip whitespace
TOKEN=$(${pkgs.kdePackages.kwallet}/bin/kwallet-query -r ha_avr_token kdewallet -f Passwords 2>/dev/null | tr -d '[:space:]')
if [ -z "$TOKEN" ]; then
${pkgs.libnotify}/bin/notify-send -u critical "Volume Control Error" "Failed to retrieve Home Assistant token from KDE Wallet"
exit 1
fi
# Get current mute state
STATE_RESPONSE=$(${pkgs.curl}/bin/curl -s -H "Authorization: Bearer $TOKEN" \
"$HA_URL/api/states/$ENTITY_ID")
CURRENT_MUTE=$(echo "$STATE_RESPONSE" | ${pkgs.jq}/bin/jq -r '.attributes.is_volume_muted // false')
# Toggle: if currently muted (true), unmute (false), and vice versa
if [ "$CURRENT_MUTE" = "true" ]; then
NEW_MUTE="false"
NOTIFY_MSG="Unmuted"
else
NEW_MUTE="true"
NOTIFY_MSG="Muted"
fi
# Send mute toggle command with retry logic
for i in $(seq 1 $MAX_RETRIES); do
RESPONSE=$(${pkgs.curl}/bin/curl -s -w "\n%{http_code}" -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"entity_id\": \"$ENTITY_ID\", \"is_volume_muted\": $NEW_MUTE}" \
"$HA_URL/api/services/media_player/volume_mute" 2>&1)
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
if [ "$HTTP_CODE" = "200" ]; then
exit 0
fi
# Wait before retry (except on last attempt)
if [ $i -lt $MAX_RETRIES ]; then
sleep 0.5
fi
done
# All retries failed
${pkgs.libnotify}/bin/notify-send -u critical "Volume Control Error" "Failed to toggle mute after $MAX_RETRIES attempts"
exit 1
'';
in
{
options.home.roles.plasma-manager-kodi = {
enable = mkEnableOption "KDE Plasma volume control for kodi user via Home Assistant";
};
config = mkIf cfg.enable {
programs.plasma = {
enable = true;
overrideConfig = true;
# Disable default kmix volume shortcuts to prevent conflicts
shortcuts.kmix = {
"increase_volume" = "none";
"decrease_volume" = "none";
"mute" = "none";
};
# Define custom volume control commands with key bindings
hotkeys.commands = {
"volume-up-avr" = {
name = "Volume Up AVR";
key = "Volume Up";
command = toString volumeUpScript;
};
"volume-down-avr" = {
name = "Volume Down AVR";
key = "Volume Down";
command = toString volumeDownScript;
};
"volume-mute-avr" = {
name = "Mute Toggle AVR";
key = "Volume Mute";
command = toString volumeMuteScript;
};
};
# KDE Settings customization
configFile = {
# Session restore settings
"ksmserverrc"."General"."loginMode" = "emptySession";
# Screen locking settings
"kscreenlockerrc"."Daemon"."Autolock" = false;
"kscreenlockerrc"."Daemon"."LockOnResume" = false;
# Theme settings
"kdeglobals"."KDE"."LookAndFeelPackage" = "org.kde.breezedark.desktop";
};
};
};
}

View File

@@ -4,8 +4,6 @@ with lib;
let
cfg = config.home.roles.plasma-manager;
wallpaperConfig = import ../../wallpapers;
currentWallpaper = builtins.elemAt wallpaperConfig.wallpapers wallpaperConfig.currentIndex;
in
{
options.home.roles.plasma-manager = {
@@ -183,7 +181,7 @@ in
plasma-localerc.Formats.LANG = "en_US.UTF-8";
# Set wallpaper for all desktops
plasmarc.Wallpapers.usersWallpapers = "${currentWallpaper.file}";
plasmarc.Wallpapers.usersWallpapers = "${../../wallpapers/metroid-samus-returns-kz-3440x1440.jpg}";
};
};
};

View File

@@ -1,72 +0,0 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.home.roles.starship;
in
{
options.home.roles.starship = {
enable = mkEnableOption "starship cross-shell prompt";
};
config = mkIf cfg.enable {
programs.starship = {
enable = true;
enableBashIntegration = true;
enableZshIntegration = true;
settings = {
add_newline = true;
character = {
success_symbol = "[>](bold green)";
error_symbol = "[x](bold red)";
vimcmd_symbol = "[<](bold green)";
};
directory = {
truncation_length = 4;
truncate_to_repo = true;
};
git_branch = {
symbol = "";
format = "[$symbol$branch(:$remote_branch)]($style) ";
};
git_status = {
format = "([$all_status$ahead_behind]($style) )";
};
nix_shell = {
symbol = "";
format = "[$symbol$state( \\($name\\))]($style) ";
};
cmd_duration = {
min_time = 2000;
format = "[$duration]($style) ";
};
# Disable modules that are noisy or rarely needed
package.disabled = true;
nodejs.disabled = true;
python.disabled = true;
ruby.disabled = true;
java.disabled = true;
golang.disabled = true;
rust.disabled = true;
php.disabled = true;
lua.disabled = true;
perl.disabled = true;
terraform.disabled = true;
kubernetes.disabled = true;
docker_context.disabled = true;
aws.disabled = true;
gcloud.disabled = true;
azure.disabled = true;
};
};
};
}

View File

@@ -4,7 +4,6 @@ with lib;
let
cfg = config.home.roles.sync;
isLinux = pkgs.stdenv.isLinux;
in
{
options.home.roles.sync = {
@@ -12,10 +11,9 @@ in
};
config = mkIf cfg.enable {
# Linux-only: syncthingtray requires system tray support
home.packages = optionals isLinux (with pkgs; [
home.packages = with pkgs; [
syncthingtray
]);
];
services.syncthing = {
enable = true;

View File

@@ -1,45 +0,0 @@
# Wallpaper rotation system
# The currentIndex is incremented by `nix run .#rotate-wallpaper`
# and gets committed as part of `nix run .#upgrade`
{
currentIndex = 2; # Index into wallpapers list
wallpapers = [
{
name = "metroid-samus-returns";
file = ./metroid-samus-returns-kz-3440x1440.jpg;
sway = "fill";
feh = "--bg-fill";
}
{
name = "metroid3_map";
file = ./metroid3_map.gif;
sway = "fit";
feh = "--bg-max";
}
{
name = "super-metroid-gunship-cavern";
file = ./super-metroid-gunship-cavern.jpg;
sway = "fit";
feh = "--bg-max";
}
{
name = "super-metroid-samus-statue";
file = ./super-metroid-samus-statue.png;
sway = "fit";
feh = "--bg-max";
}
{
name = "metroid-samus-action-4k";
file = ./metroid-samus-action-4k.jpg;
sway = "fit";
feh = "--bg-max";
}
{
name = "metroid-creature-minimalist";
file = ./metroid-creature-minimalist.jpg;
sway = "fit";
feh = "--bg-max";
}
];
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 MiB

View File

@@ -22,24 +22,15 @@ with lib;
sddm = true;
wayland = true;
};
plasma-bigscreen = {
kodi = {
enable = true;
autologin = true;
jellyfinScaleFactor = 1.0;
appLauncherServer.enable = true;
wayland = true;
jellyfinScaleFactor = 2.5;
};
nfs-mounts.enable = true;
users.enable = true;
};
# Enable KDE Wallet PAM integration for auto-unlock
security.pam.services.sddm = {
kwallet = {
enable = true;
package = pkgs.kdePackages.kwallet-pam;
};
};
# Use the systemd-boot EFI boot loader.
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
@@ -71,3 +62,4 @@ with lib;
system.stateVersion = "24.05"; # Did you read the comment?
}

View File

@@ -1,74 +0,0 @@
# Edit this configuration file to define what should be installed on
# your system. Help is available in the configuration.nix(5) man page, on
# https://search.nixos.org/options and in the NixOS manual (`nixos-help`).
{ lib, pkgs, ... }:
with lib;
{
imports = [
# Include the results of the hardware scan.
./hardware-configuration.nix
];
roles = {
audio.enable = true;
bluetooth.enable = true;
desktop = {
enable = true;
gaming.enable = true;
kde = true;
sddm = true;
wayland = true;
};
plasma-bigscreen = {
enable = true;
autologin = true;
jellyfinScaleFactor = 1.0;
appLauncherServer.enable = true;
};
nfs-mounts.enable = true;
users.enable = true;
};
# Enable KDE Wallet PAM integration for auto-unlock
security.pam.services.sddm = {
kwallet = {
enable = true;
package = pkgs.kdePackages.kwallet-pam;
};
};
# Use the systemd-boot EFI boot loader.
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
networking.hostName = "gym-box";
networking.networkmanager.enable = true;
services.xserver.videoDrivers = [ "amdgpu" ];
hardware.graphics.enable = true;
hardware.graphics.enable32Bit = true;
# 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.
#
# Most users should NEVER change this value after the initial install, for any reason,
# even if you've upgraded your system to a new NixOS release.
#
# This value does NOT affect the Nixpkgs version your packages and OS are pulled from,
# so changing it will NOT upgrade your system - see https://nixos.org/manual/nixos/stable/#sec-upgrading for how
# to actually do that.
#
# This value being lower than the current NixOS release does NOT mean your system is
# out of date, out of support, or vulnerable.
#
# Do NOT change this value unless you have manually inspected all the changes it would make to your configuration,
# and migrated your data accordingly.
#
# For more information, see `man configuration.nix` or https://nixos.org/manual/nixos/stable/options#opt-system.stateVersion .
system.stateVersion = "24.05"; # Did you read the comment?
}

View File

@@ -1,31 +0,0 @@
# Do not modify this file! It was generated by nixos-generate-config
# and may be overwritten by future invocations. Please make changes
# to /etc/nixos/configuration.nix instead.
{ config, lib, pkgs, modulesPath, ... }:
{
imports =
[ (modulesPath + "/installer/scan/not-detected.nix")
];
boot.initrd.availableKernelModules = [ "nvme" "xhci_pci" "thunderbolt" "uas" "usbhid" "usb_storage" "sd_mod" ];
boot.initrd.kernelModules = [ ];
boot.kernelModules = [ "kvm-amd" ];
boot.extraModulePackages = [ ];
fileSystems."/" =
{ device = "/dev/disk/by-uuid/59c0df78-c6fa-415d-8592-13547a3fada6";
fsType = "btrfs";
};
fileSystems."/boot" =
{ device = "/dev/disk/by-uuid/DC66-D04C";
fsType = "vfat";
options = [ "fmask=0022" "dmask=0022" ];
};
swapDevices = [ ];
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
hardware.cpu.amd.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
}

View File

@@ -1,424 +0,0 @@
# Migration Plan: Arch Linux to NixOS on john-endesktop (ZFS/NFS Server)
## Overview
This document outlines the plan to migrate the john-endesktop server from Arch Linux to NixOS while maintaining the existing ZFS pools and NFS exports that serve your k3s cluster.
## Current System State
### Hardware
- **Boot disk**: nvme0n1
- nvme0n1p3: 1000M EFI partition (UUID: F5C6-D570)
- nvme0n1p4: 120GB ext4 / (current Arch root)
- nvme0n1p5: 810GB - **Target for NixOS** (being removed from media pool)
- **Network**: enp0s31f6 @ 10.0.0.43/24 (DHCP)
### ZFS Pools
- **media**: ~3.5TB JBOD pool (2 drives after nvme0n1p5 removal)
- wwn-0x50014ee2ba653d70-part2
- ata-WDC_WD20EZBX-00AYRA0_WD-WX62D627X7Z8-part2
- Contains: /media/media/nix (bind mounted to /nix on Arch)
- NFS: Shared to 10.0.0.0/24 via ZFS sharenfs property
- **swarmvols**: 928GB mirror pool - **PRODUCTION DATA**
- wwn-0x5002538f52707e2d-part2
- wwn-0x5002538f52707e81-part2
- Contains: iocage jails and k3s persistent volumes
- NFS: Shared to 10.0.0.0/24 via ZFS sharenfs property
- Backed up nightly to remote borg
### Services
- NFS server exporting /media and /swarmvols to k3s cluster
- ZFS managing pools with automatic exports via sharenfs property
## Prerequisites
### Before Starting
1. ✅ Ensure nvme0n1p5 removal from media pool is complete
```bash
ssh 10.0.0.43 "zpool status media"
# Should show no "removing" devices
```
2. ✅ Verify recent backups exist
```bash
# Verify swarmvols backup is recent (< 24 hours)
# Check your borg backup system
```
3. ✅ Notify k3s cluster users of planned maintenance window
- NFS shares will be unavailable during migration
- Estimate: 30-60 minutes downtime
4. ✅ Build NixOS configuration from your workstation
```bash
cd ~/nixos-configs
nix build .#nixosConfigurations.john-endesktop.config.system.build.toplevel
```
## Migration Steps
### Phase 1: Prepare NixOS Installation Media
1. **Download NixOS minimal ISO**
```bash
wget https://channels.nixos.org/nixos-25.11/latest-nixos-minimal-x86_64-linux.iso
```
2. **Create bootable USB**
```bash
# Identify USB device (e.g., /dev/sdb)
lsblk
# Write ISO to USB
sudo dd if=latest-nixos-minimal-x86_64-linux.iso of=/dev/sdX bs=4M status=progress
sudo sync
```
### Phase 2: Backup and Shutdown
1. **On the server, verify ZFS pool status**
```bash
ssh 10.0.0.43 "zpool status"
ssh 10.0.0.43 "zfs list"
```
2. **Export ZFS pools cleanly**
```bash
ssh 10.0.0.43 "sudo zpool export media"
ssh 10.0.0.43 "sudo zpool export swarmvols"
```
3. **Shutdown Arch Linux**
```bash
ssh 10.0.0.43 "sudo shutdown -h now"
```
### Phase 3: Install NixOS
1. **Boot from NixOS USB**
- Insert USB drive
- Power on and select USB in boot menu
2. **Connect to network**
```bash
# If DHCP doesn't work automatically:
sudo systemctl start dhcpcd
ip a # Verify you have 10.0.0.43 or another IP
```
3. **Enable SSH for remote installation (recommended)**
```bash
# Set password for nixos user
sudo passwd nixos
# Start SSH
sudo systemctl start sshd
# From your workstation:
ssh nixos@10.0.0.43
```
4. **Partition nvme0n1p5 with btrfs**
```bash
# Verify the device is clear
lsblk
sudo wipefs -a /dev/nvme0n1p5
# Create btrfs filesystem
sudo mkfs.btrfs -L nixos /dev/nvme0n1p5
# Mount and create subvolumes
sudo mount /dev/nvme0n1p5 /mnt
sudo btrfs subvolume create /mnt/@
sudo btrfs subvolume create /mnt/@home
sudo btrfs subvolume create /mnt/@nix
sudo btrfs subvolume create /mnt/@log
sudo umount /mnt
# Mount root subvolume
sudo mount -o subvol=@,compress=zstd,noatime /dev/nvme0n1p5 /mnt
# Create mount points
sudo mkdir -p /mnt/{boot,home,nix,var/log}
# Mount other subvolumes
sudo mount -o subvol=@home,compress=zstd,noatime /dev/nvme0n1p5 /mnt/home
sudo mount -o subvol=@nix,compress=zstd,noatime /dev/nvme0n1p5 /mnt/nix
sudo mount -o subvol=@log,compress=zstd,noatime /dev/nvme0n1p5 /mnt/var/log
# Mount EFI partition
sudo mount /dev/nvme0n1p3 /mnt/boot
```
5. **Import ZFS pools**
```bash
# Import pools (should be visible)
sudo zpool import
# Import with force if needed due to hostid
sudo zpool import -f media
sudo zpool import -f swarmvols
# Verify pools are mounted
zfs list
ls -la /media /swarmvols
```
6. **Generate initial hardware configuration**
```bash
sudo nixos-generate-config --root /mnt
```
7. **Get the new root filesystem UUID**
```bash
blkid /dev/nvme0n1p5
# Note the UUID for updating hardware-configuration.nix
/dev/nvme0n1p5: LABEL="nixos" UUID="5f4ad025-bfab-4aed-a933-6638348059e5" UUID_SUB="4734d820-7b8a-4b7f-853a-026021c1d204" BLOCK_SIZE="4096" TYPE="btrfs" PARTLABEL="data" PARTUUID="9ea025df-cdb7-48fd-b5d4-37cd5d8588eb"
```
8. **Copy your NixOS configuration to the server**
```bash
# From your workstation:
scp -r ~/nixos-configs/machines/john-endesktop/* nixos@10.0.0.43:/tmp/
# On server:
sudo mkdir -p /mnt/etc/nixos
sudo cp /tmp/configuration.nix /mnt/etc/nixos/
sudo cp /tmp/hardware-configuration.nix /mnt/etc/nixos/
# Edit hardware-configuration.nix to update the root filesystem UUID
sudo nano /mnt/etc/nixos/hardware-configuration.nix
# Change: device = "/dev/disk/by-uuid/CHANGE-THIS-TO-YOUR-UUID";
# To: device = "/dev/disk/by-uuid/[UUID from blkid]";
```
9. **Install NixOS**
```bash
sudo nixos-install
# Set root password when prompted
# Set user password
sudo nixos-install --no-root-passwd
```
10. **Reboot into NixOS**
```bash
sudo reboot
# Remove USB drive
```
### Phase 4: Post-Installation Verification
1. **Boot into NixOS and verify system**
```bash
ssh johno@10.0.0.43
# Check NixOS version
nixos-version
# Verify hostname
hostname # Should be: john-endesktop
```
2. **Verify ZFS pools imported correctly**
```bash
zpool status
zpool list
zfs list
# Check for hostid mismatch warnings (should be gone)
# Verify both pools show ONLINE status
```
3. **Verify NFS exports are active**
```bash
sudo exportfs -v
systemctl status nfs-server
# Should see /media and /swarmvols exported to 10.0.0.0/24
```
4. **Test NFS mount from another machine**
```bash
# From a k3s node or your workstation:
sudo mount -t nfs 10.0.0.43:/swarmvols /mnt
ls -la /mnt
sudo umount /mnt
sudo mount -t nfs 10.0.0.43:/media /mnt
ls -la /mnt
sudo umount /mnt
```
5. **Verify ZFS sharenfs properties preserved**
```bash
zfs get sharenfs media
zfs get sharenfs swarmvols
# Should show: sec=sys,mountpoint,no_subtree_check,no_root_squash,rw=@10.0.0.0/24
```
6. **Check swap device**
```bash
swapon --show
free -h
# Should show /dev/zvol/media/swap
```
### Phase 5: Restore k3s Cluster Access
1. **Restart k3s nodes or remount NFS shares**
```bash
# On each k3s node:
sudo systemctl restart k3s # or k3s-agent
```
2. **Verify k3s pods have access to persistent volumes**
```bash
# On k3s master:
kubectl get pv
kubectl get pvc
# Check that volumes are bound and accessible
```
## Rollback Plan
If something goes wrong during migration, you can roll back to Arch Linux:
### Quick Rollback (If NixOS won't boot)
1. **Boot from NixOS USB (or Arch USB)**
2. **Import ZFS pools**
```bash
sudo zpool import -f media
sudo zpool import -f swarmvols
```
3. **Start NFS manually (temporary)**
```bash
sudo mkdir -p /media /swarmvols
sudo systemctl start nfs-server
sudo exportfs -o rw,sync,no_subtree_check,no_root_squash 10.0.0.0/24:/media
sudo exportfs -o rw,sync,no_subtree_check,no_root_squash 10.0.0.0/24:/swarmvols
sudo exportfs -v
```
This will restore k3s cluster access immediately while you diagnose.
4. **Boot back into Arch Linux**
```bash
# Reboot and select nvme0n1p4 (Arch) in GRUB/boot menu
sudo reboot
```
5. **Verify Arch boots and services start**
```bash
ssh johno@10.0.0.43
zpool status
systemctl status nfs-server
```
### Full Rollback (If needed)
1. **Follow Quick Rollback steps above**
2. **Re-add nvme0n1p5 to media pool (if desired)**
```bash
# Only if you want to restore the original configuration
sudo zpool add media /dev/nvme0n1p5
```
3. **Clean up NixOS partition**
```bash
# If you want to reclaim nvme0n1p5 for other uses
sudo wipefs -a /dev/nvme0n1p5
```
## Risk Mitigation
### Data Safety
- ✅ **swarmvols** (production): Mirrored + nightly borg backups
- ⚠️ **media** (important): JBOD - no redundancy, but not catastrophic
- ✅ **NixOS install**: Separate partition, doesn't touch ZFS pools
- ✅ **Arch Linux**: Remains bootable on nvme0n1p4 until verified
### Service Continuity
- Downtime: 30-60 minutes expected
- k3s cluster: Will reconnect automatically when NFS returns
- Rollback time: < 10 minutes to restore Arch
### Testing Approach
1. Test NFS exports from NixOS live environment before installation
2. Test single NFS mount from k3s node before full cluster restart
3. Keep Arch Linux boot option until 24-48 hours of stable NixOS operation
## Post-Migration Tasks
After successful migration and 24-48 hours of stable operation:
1. **Update k3s NFS mounts (if needed)**
- Verify no hardcoded references to old system
2. **Optional: Repurpose Arch partition**
```bash
# After you're confident NixOS is stable
# You can wipe nvme0n1p4 and repurpose it
```
3. **Update documentation**
- Update infrastructure docs with NixOS configuration
- Document any deviations from this plan
4. **Consider setting up NixOS remote deployment**
```bash
# From your workstation:
nixos-rebuild switch --target-host johno@10.0.0.43 --flake .#john-endesktop
```
## Timeline
- **Preparation**: 1-2 hours (testing config build, downloading ISO)
- **Migration window**: 1-2 hours (installation + verification)
- **Verification period**: 24-48 hours (before removing Arch)
- **Total**: ~3 days from start to declaring success
## Emergency Contacts
- Borg backup location: [Document your borg repo location]
- K3s cluster nodes: [Document your k3s nodes]
- Critical services on k3s: [Document what's running that depends on these NFS shares]
## Checklist
Pre-migration:
- [x] nvme0n1p5 removal from media pool complete
- [x] Recent backup verified (< 24 hours)
- [x] Maintenance window scheduled
- [x] NixOS ISO downloaded
- [x] Bootable USB created
- [x] NixOS config builds successfully
During migration:
- [ ] ZFS pools exported
- [ ] Arch Linux shutdown cleanly
- [ ] Booted from NixOS USB
- [ ] nvme0n1p5 formatted with btrfs
- [ ] Btrfs subvolumes created
- [ ] ZFS pools imported
- [ ] NixOS installed
- [ ] Root password set
Post-migration:
- [ ] NixOS boots successfully
- [ ] ZFS pools mounted automatically
- [ ] NFS server running
- [ ] NFS exports verified
- [ ] Test mount from k3s node successful
- [ ] k3s cluster reconnected
- [ ] Persistent volumes accessible
- [ ] No hostid warnings in zpool status
- [ ] Arch Linux still bootable (for rollback)
Final verification (after 24-48 hours):
- [ ] All services stable
- [ ] No unexpected issues
- [ ] Performance acceptable
- [ ] Ready to remove Arch partition (optional)
- [ ] Ready to remove /swarmvols/media-backup (optional)

View File

@@ -1,167 +0,0 @@
# NixOS configuration for john-endesktop (ZFS/NFS server)
# Migrated from Arch Linux to provide ZFS pools via NFS to k3s cluster
{ config, lib, pkgs, ... }:
with lib;
{
imports = [
./hardware-configuration.nix
];
# Boot configuration
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
# ZFS support
boot.supportedFilesystems = [ "zfs" ];
boot.zfs.forceImportRoot = false;
boot.zfs.extraPools = [ "media" "swarmvols" ];
# Set ZFS hostid to match current system (from Arch Linux)
# This resolves the hostid mismatch warnings
networking.hostId = "007f0101";
# Hostname
networking.hostName = "john-endesktop";
# Network configuration - using DHCP on enp0s31f6
networking.useDHCP = false;
networking.interfaces.enp0s31f6.useDHCP = true;
# NFS Server configuration
services.nfs.server = {
enable = true;
# NFS protocol versions
# v3 for broader compatibility, v4 for better performance
exports = ''
# These are managed by ZFS sharenfs properties
# but we enable the NFS server here
'';
};
# Enable NFS4 with proper configuration
services.rpcbind.enable = true;
# Firewall configuration for NFS
networking.firewall = {
enable = true;
allowedTCPPorts = [
111 # rpcbind
2049 # nfs
4000 # nfs callback
4001 # nlockmgr
4002 # mountd
5000 # harmonia binary cache
20048 # mountd
];
allowedUDPPorts = [
111 # rpcbind
2049 # nfs
4000 # nfs callback
4001 # nlockmgr
4002 # mountd
20048 # mountd
];
# Allow NFS from local network
extraCommands = ''
iptables -A nixos-fw -p tcp -s 10.0.0.0/24 -j ACCEPT
iptables -A nixos-fw -p udp -s 10.0.0.0/24 -j ACCEPT
'';
};
# ZFS maintenance
services.zfs = {
autoScrub = {
enable = true;
interval = "monthly";
};
trim = {
enable = true;
interval = "weekly";
};
};
# Basic system packages
environment.systemPackages = with pkgs; [
vim
git
htop
tmux
zfs
rclone
custom.rclone-torbox-setup # Helper script to set up TorBox credentials via rbw
];
# Enable SSH
services.openssh = {
enable = true;
settings = {
PermitRootLogin = "no";
PasswordAuthentication = true;
};
};
# User configuration
roles.users.enable = true;
# Enable as remote builder (similar to zix790prors)
roles.remote-build.enableBuilder = true;
# k3s agent configuration
roles.k3s-node = {
enable = true;
role = "agent";
# serverAddr defaults to https://10.0.0.222:6443
# tokenFile defaults to /etc/k3s/token
extraFlags = [
# Node labels for workload scheduling
# fast-cpu: This node has a faster CPU than other cluster nodes
"--node-label=fast-cpu=true"
# fast-storage: This node is the NFS host with fast local storage access
"--node-label=fast-storage=true"
# k3s-upgrade=disabled: NixOS manages k3s upgrades via Nix, not system-upgrade-controller
"--node-label=k3s-upgrade=disabled"
];
};
roles.virtualisation.enable = true;
# TorBox WebDAV mount for rdt-client and Jellyfin
roles.rclone-mount = {
enable = true;
mounts.torbox = {
webdavUrl = "https://webdav.torbox.app";
username = "john@ogle.fyi"; # TorBox account email
mountPoint = "/media/media/torbox-rclone";
environmentFile = "/etc/rclone/torbox.env";
vfsCacheMode = "full"; # Best for streaming media
dirCacheTime = "5m";
extraArgs = [
"--buffer-size=64M"
"--vfs-read-chunk-size=32M"
"--vfs-read-chunk-size-limit=off"
];
# Wait for ZFS media pool to be mounted before starting
requiresMountsFor = [ "/media" ];
};
};
# Harmonia binary cache server
# Replaces the broken k8s deployment with native NixOS service
services.harmonia = {
enable = true;
signKeyPaths = [ "/etc/harmonia/signing-key.private" ];
settings = {
bind = "[::]:5000";
};
};
# Time zone
time.timeZone = "America/Los_Angeles"; # Adjust as needed
# NixOS version
system.stateVersion = "25.11";
}

View File

@@ -1,63 +0,0 @@
# Hardware configuration for john-endesktop
# This file should be regenerated after NixOS installation using:
# nixos-generate-config --show-hardware-config
{ config, lib, pkgs, modulesPath, ... }:
{
imports = [
(modulesPath + "/installer/scan/not-detected.nix")
];
# Boot configuration
boot.initrd.availableKernelModules = [ "xhci_pci" "ahci" "nvme" "usbhid" "usb_storage" "sd_mod" ];
boot.initrd.kernelModules = [ ];
boot.kernelModules = [ "kvm-intel" ];
boot.extraModulePackages = [ ];
# File systems - these will need to be updated after installation
# The nvme0n1p5 partition will be formatted as btrfs for NixOS root
fileSystems."/" = {
device = "/dev/disk/by-uuid/5f4ad025-bfab-4aed-a933-6638348059e5";
fsType = "btrfs";
options = [ "subvol=@" "compress=zstd" "noatime" ];
};
fileSystems."/home" = {
device = "/dev/disk/by-uuid/5f4ad025-bfab-4aed-a933-6638348059e5";
fsType = "btrfs";
options = [ "subvol=@home" "compress=zstd" "noatime" ];
};
fileSystems."/nix" = {
device = "/dev/disk/by-uuid/5f4ad025-bfab-4aed-a933-6638348059e5";
fsType = "btrfs";
options = [ "subvol=@nix" "compress=zstd" "noatime" ];
};
fileSystems."/var/log" = {
device = "/dev/disk/by-uuid/5f4ad025-bfab-4aed-a933-6638348059e5";
fsType = "btrfs";
options = [ "subvol=@log" "compress=zstd" "noatime" ];
};
fileSystems."/boot" = {
# This should match your current EFI partition
device = "/dev/disk/by-uuid/F5C6-D570";
fsType = "vfat";
options = [ "fmask=0022" "dmask=0022" ];
};
# ZFS pools - these are imported by ZFS, not managed by fileSystems
# The pools should be imported automatically via boot.zfs.extraPools
# /media and /swarmvols will be mounted by ZFS
# No swap needed - 23GB RAM is sufficient for this NFS/ZFS server
swapDevices = [ ];
# CPU microcode
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
# Networking
networking.useDHCP = lib.mkDefault true;
}

View File

@@ -21,41 +21,17 @@
};
nfs-mounts.enable = true;
printing.enable = true;
remote-build.builders = [
{
hostName = "zix790prors.oglehome";
maxJobs = 16;
speedFactor = 3;
}
{
hostName = "john-endesktop.oglehome";
maxJobs = 1;
speedFactor = 1;
}
];
remote-build.builders = [{
hostName = "zix790prors";
maxJobs = 16;
speedFactor = 3;
}];
spotifyd.enable = true;
users = {
enable = true;
extraGroups = [ "video" ];
};
virtualisation = {
enable = true;
waydroid = true;
};
wireguard = {
enable = true;
autostart = true;
interfaceName = "ogleNet";
address = [ "192.168.4.2/32" ];
privateKeyFile = "/etc/wireguard/oglehome-private-key";
dns = [ "192.168.4.1" ];
peers = [{
publicKey = "AWkmtaz0poyyKJGnRcabO5ecd6ESh1lKu+XRb3ObxBc=";
endpoint = "pi.johnogle.info:6666";
allowedIPs = [ "0.0.0.0/0" ];
persistentKeepalive = 25;
}];
};
virtualisation.enable = true;
};
# Bootloader.
@@ -63,18 +39,19 @@
boot.loader.efi.canTouchEfiVariables = true;
boot.initrd.luks.devices."luks-b614167b-9045-4234-a441-ac6f60a96d81".device = "/dev/disk/by-uuid/b614167b-9045-4234-a441-ac6f60a96d81";
services.logind.settings.Login = {
HandlePowerKey = "hibernate";
HandlePowerKeyLongPress = "poweroff";
};
networking.hostName = "nix-book"; # Define your hostname.
# networking.wireless.enable = true; # Enables wireless support via wpa_supplicant.
# Enable networking
networking.networkmanager.enable = true;
# WireGuard setup
networking.wg-quick.interfaces = {
ogleNet = {
configFile = "/root/Oglehome-VPN-johno-nixbook.conf";
};
};
hardware.graphics = {
enable = true;
extraPackages = with pkgs; [

View File

@@ -19,18 +19,11 @@
desktopSession = "plasma";
};
};
remote-build.builders = [
{
hostName = "zix790prors.oglehome";
maxJobs = 16;
speedFactor = 4;
}
{
hostName = "john-endesktop.oglehome";
maxJobs = 1;
speedFactor = 2;
}
];
remote-build.builders = [{
hostName = "zix790prors";
maxJobs = 16;
speedFactor = 4; # Prefer remote heavily on Steam Deck
}];
users = {
enable = true;
extraGroups = [ "video" ];

View File

@@ -0,0 +1,62 @@
# Edit this configuration file to define what should be installed on
# your system. Help is available in the configuration.nix(5) man page, on
# https://search.nixos.org/options and in the NixOS manual (`nixos-help`).
# NixOS-WSL specific options are documented on the NixOS-WSL repository:
# https://github.com/nix-community/NixOS-WSL
{ config, lib, pkgs, ... }:
{
imports = [
];
roles = {
audio.enable = true;
desktop = {
enable = true;
wayland = true;
};
users.enable = true;
};
networking.hostName = "wixos";
wsl.enable = true;
wsl.defaultUser = "johno";
wsl.startMenuLaunchers = true;
wsl.useWindowsDriver = true;
wsl.wslConf.network.hostname = "wixos";
wsl.wslConf.user.default = "johno";
services.xserver.videoDrivers = [ "nvidia" ];
hardware.graphics = {
enable = true;
extraPackages = with pkgs; [
mesa
libvdpau-va-gl
libva-vdpau-driver
];
};
environment.sessionVariables = {
LD_LIBRARY_PATH = [
"/usr/lib/wsl/lib"
"/run/opengl-driver/lib"
];
};
hardware.nvidia = {
modesetting.enable = true;
nvidiaSettings = true;
open = true;
package = config.boot.kernelPackages.nvidiaPackages.latest;
};
# This value determines the NixOS release from which the default
# settings for stateful data, like file locations and database versions
# on your system were taken. It's perfectly fine and recommended to leave
# this value at the release version of the first install of this system.
# Before changing this value read the documentation for this option
# (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).
system.stateVersion = "24.05"; # Did you read the comment?
}

View File

@@ -25,12 +25,8 @@ with lib;
wayland = true;
x11 = true;
};
kodi.enable = true;
nfs-mounts.enable = true;
nvidia = {
enable = true;
graphics.enable32Bit = true;
};
nvidia.enable = true;
printing.enable = true;
remote-build.enableBuilder = true;
users.enable = true;
@@ -51,11 +47,27 @@ with lib;
# Fix dual boot clock sync - tell Linux to use local time for hardware clock
time.hardwareClockInLocalTime = true;
# NVIDIA Graphics configuration
services.xserver.videoDrivers = [ "nvidia" ];
hardware.graphics.enable = true;
hardware.graphics.enable32Bit = true;
# Set DP-0 as primary display with 164.90Hz refresh rate
services.xserver.displayManager.sessionCommands = ''
${pkgs.xorg.xrandr}/bin/xrandr --output DP-0 --mode 3440x1440 --rate 164.90 --primary
'';
hardware.nvidia = {
modesetting.enable = true;
nvidiaSettings = true;
package = pkgs.linuxPackages.nvidiaPackages.stable;
open = true;
# For gaming performance
powerManagement.enable = false;
powerManagement.finegrained = false;
};
services.ollama = {
enable = true;
acceleration = "cuda";

View File

@@ -1,29 +1,28 @@
{ lib
, stdenv
, fetchurl
, patchelf
, glibc
, autoPatchelfHook
}:
let
version = "2.1.75";
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 = "8c541a5e924eda2070eaf1702a48047af671c4dff6a11a5e762076614a082675";
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 = "82c90b91a0a18f60191f817b9b42304d8b17dbed75795b715c41f4fdfe4c782d";
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 = "328b0a429c05a04f911157d886be5123cf1824a19ba8ca1f9d594c004eac32c9";
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 = "ec8f4f7f7bb50611dae70c109a76ee1da6a3ab45511c65f117df215848ecc905";
sha256 = "681fbd1a84b2de883dc954441693766b43ea4faafb3e72b88c99a33645cd3507";
};
};
@@ -39,14 +38,8 @@ in stdenv.mkDerivation {
dontUnpack = true;
dontBuild = true;
# Bun standalone binaries have JS code appended after the ELF sections
# stripping/patching would remove or corrupt this appended data
dontStrip = true;
dontPatchELF = true;
# Don't use autoPatchelfHook - it rewrites the ELF and strips the appended
# bun bundle (the JS code is appended after the ELF sections)
nativeBuildInputs = lib.optionals stdenv.isLinux [ patchelf ];
nativeBuildInputs = lib.optionals stdenv.isLinux [ autoPatchelfHook ];
installPhase = ''
runHook preInstall
@@ -56,14 +49,6 @@ in stdenv.mkDerivation {
runHook postInstall
'';
# Manually patch the interpreter for bun standalone binaries
# patchelf --set-interpreter modifies in-place without rewriting the entire ELF,
# preserving the appended JS bundle that bun needs at runtime
postFixup = lib.optionalString stdenv.isLinux ''
interpreter="${glibc}/lib/${if stdenv.hostPlatform.system == "aarch64-linux" then "ld-linux-aarch64.so.1" else "ld-linux-x86-64.so.2"}"
patchelf --set-interpreter "$interpreter" $out/bin/claude
'';
meta = with lib; {
description = "Terminal-based AI coding assistant from Anthropic";
homepage = "https://www.anthropic.com/claude-code";

View File

@@ -33,8 +33,7 @@ YELLOW='\033[1;33m'
NC='\033[0m' # No Color
CASK_URL="https://raw.githubusercontent.com/Homebrew/homebrew-cask/HEAD/Casks/c/claude-code.rb"
REPO_ROOT="${REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
NIX_FILE="$REPO_ROOT/packages/claude-code/default.nix"
NIX_FILE="$(dirname "$0")/default.nix"
echo "Fetching latest claude-code version from Homebrew cask..."

View File

@@ -1,11 +1,8 @@
{ pkgs, ... }:
{ pkgs, uv2nix ? null, pyproject-nix ? null, pyproject-build-systems ? null, ... }:
{
tea-rbw = pkgs.callPackage ./tea-rbw { };
app-launcher-server = pkgs.callPackage ./app-launcher-server { };
claude-code = pkgs.callPackage ./claude-code { };
mcrcon-rbw = pkgs.callPackage ./mcrcon-rbw { };
rclone-torbox-setup = pkgs.callPackage ./rclone-torbox-setup { };
pi-coding-agent = pkgs.callPackage ./pi-coding-agent { };
nextcloud-talk-desktop = pkgs.callPackage ./nextcloud-talk-desktop { };
opencode = pkgs.callPackage ./opencode { };
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

@@ -1,40 +0,0 @@
{ pkgs, ... }:
pkgs.writeShellScriptBin "mcrcon" ''
set -euo pipefail
# Configuration - can be overridden with environment variables
MINECRAFT_RCON_HOST="''${MCRCON_HOST:-10.0.0.165}"
MINECRAFT_RCON_PORT="''${MCRCON_PORT:-25575}"
RBW_ENTRY="minecraft-rcon"
# Check if rbw is available
if ! command -v rbw &> /dev/null; then
echo "Error: rbw is not available. Please ensure rbw is installed and configured."
exit 1
fi
# Retrieve password from Bitwarden
if ! MCRCON_PASS=$(rbw get "$RBW_ENTRY" 2>/dev/null); then
echo "Error: Failed to retrieve RCON password from rbw entry '$RBW_ENTRY'"
echo "Please ensure the entry exists in Bitwarden and rbw is synced."
echo ""
echo "To create the entry:"
echo " 1. Add 'minecraft-rcon' to Bitwarden with the RCON password"
echo " 2. Run 'rbw sync' to refresh the local cache"
exit 1
fi
# Export for mcrcon
export MCRCON_HOST="$MINECRAFT_RCON_HOST"
export MCRCON_PORT="$MINECRAFT_RCON_PORT"
export MCRCON_PASS
# If no arguments provided, start interactive terminal mode
if [[ $# -eq 0 ]]; then
exec ${pkgs.mcrcon}/bin/mcrcon -t
fi
# Execute mcrcon with all provided arguments
exec ${pkgs.mcrcon}/bin/mcrcon "$@"
''

View File

@@ -1,60 +0,0 @@
# Patched Nextcloud Talk Desktop with Wayland screen sharing support
# Applies the core change from upstream draft PR #1022:
# https://github.com/nextcloud/talk-desktop/pull/1022
#
# Patches the webpack bundle in app.asar to add setDisplayMediaRequestHandler
# with useSystemPicker: true, enabling native PipeWire/portal-based
# screen sharing on Wayland (Sway, Hyprland, etc.)
{ lib
, nextcloud-talk-desktop
, nodejs
, asar
}:
nextcloud-talk-desktop.overrideAttrs (old: {
pname = "nextcloud-talk-desktop-patched";
nativeBuildInputs = (old.nativeBuildInputs or []) ++ [ asar nodejs ];
# Patch the asar after the main installPhase creates the output
postFixup = (old.postFixup or "") + ''
echo "Patching app.asar for Wayland screen sharing..."
ASAR_PATH="$out/opt/Nextcloud Talk-linux-x64/resources/app.asar"
WORK=$(mktemp -d)
asar extract "$ASAR_PATH" "$WORK/app"
# In the webpack bundle:
# session = l, desktopCapturer = a, app = n
# We inject setDisplayMediaRequestHandler right after n.whenReady().then((async()=>{
# useSystemPicker: true makes Electron use the native system picker
# (PipeWire/xdg-desktop-portal on Wayland)
node -e "
const fs = require('fs');
const p = '$WORK/app/.webpack/main/index.js';
let c = fs.readFileSync(p, 'utf8');
if (c.includes('setDisplayMediaRequestHandler')) {
console.log('Already patched');
process.exit(0);
}
const marker = 'n.whenReady().then((async()=>{';
const idx = c.indexOf(marker);
if (idx === -1) {
console.error('ERROR: Could not find whenReady marker in webpack bundle');
process.exit(1);
}
// Inject after the marker
const injection = 'l.defaultSession.setDisplayMediaRequestHandler(async(e,t)=>{const s=await a.getSources({types:[\"screen\",\"window\"]});s.length>0?t({video:s[0]}):t({})},{useSystemPicker:!0});';
c = c.slice(0, idx + marker.length) + injection + c.slice(idx + marker.length);
fs.writeFileSync(p, c, 'utf8');
console.log('Successfully patched main bundle for Wayland screen sharing');
"
asar pack "$WORK/app" "$ASAR_PATH"
rm -rf "$WORK"
'';
})

View File

@@ -1,82 +0,0 @@
{
lib,
stdenv,
fetchzip,
patchelf,
glibc,
}:
let
version = "1.4.0";
srcs = {
aarch64-darwin = {
url = "https://github.com/anomalyco/opencode/releases/download/v${version}/opencode-darwin-arm64.zip";
sha256 = "0m97j2vln8yhhvnsjl92phx6dac24y7hgh75csmbkbhawkz9xm4l";
};
x86_64-darwin = {
url = "https://github.com/anomalyco/opencode/releases/download/v${version}/opencode-darwin-x64.zip";
sha256 = "17n04j06pdc2raxjm91y6p87gwpnra0liabpbjwdmyd1iqgqv0q8";
};
x86_64-linux = {
url = "https://github.com/anomalyco/opencode/releases/download/v${version}/opencode-linux-x64.tar.gz";
sha256 = "16117lwfj2lb8wjbq5cyf77vhi52ada5ys3212hjqw3qw3wrcc0r";
};
aarch64-linux = {
url = "https://github.com/anomalyco/opencode/releases/download/v${version}/opencode-linux-arm64.tar.gz";
sha256 = "06lvm1qiji74xdd3psqn6lwxak65gqsbmkib1pjb4n65f9246jwm";
};
};
src =
srcs.${stdenv.hostPlatform.system} or (throw "Unsupported system: ${stdenv.hostPlatform.system}");
in
stdenv.mkDerivation {
pname = "opencode";
inherit version;
src = fetchzip {
inherit (src) url sha256;
};
# Bun standalone binaries have JS code appended after the ELF sections
# stripping/patching would remove or corrupt this appended data
dontStrip = true;
dontPatchELF = true;
nativeBuildInputs = lib.optionals stdenv.isLinux [ patchelf ];
installPhase = ''
runHook preInstall
install -Dm755 $src/opencode $out/bin/opencode
runHook postInstall
'';
# Manually patch the interpreter for bun standalone binaries on Linux
postFixup = lib.optionalString stdenv.isLinux ''
interpreter="${glibc}/lib/${
if stdenv.hostPlatform.system == "aarch64-linux" then
"ld-linux-aarch64.so.1"
else
"ld-linux-x86-64.so.2"
}"
patchelf --set-interpreter "$interpreter" $out/bin/opencode
'';
meta = with lib; {
description = "Terminal-based AI coding assistant";
homepage = "https://opencode.ai";
license = licenses.mit;
maintainers = [ ];
platforms = [
"aarch64-darwin"
"x86_64-darwin"
"x86_64-linux"
"aarch64-linux"
];
mainProgram = "opencode";
};
}

View File

@@ -1,148 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
DRY_RUN=false
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
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
REPO_ROOT="${REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
NIX_FILE="$REPO_ROOT/packages/opencode/default.nix"
echo "Fetching latest opencode version from GitHub API..."
RELEASE_INFO=$(curl -fsSL https://api.github.com/repos/anomalyco/opencode/releases/latest)
NEW_VERSION=$(echo "$RELEASE_INFO" | jq -r '.tag_name' | sed 's/^v//')
if [ -z "$NEW_VERSION" ] || [ "$NEW_VERSION" = "null" ]; then
echo -e "${RED}Error: Failed to fetch version from GitHub API${NC}"
exit 1
fi
CURRENT_VERSION=$(grep -m1 'version = ' "$NIX_FILE" | sed -E 's/.*version = "([^"]+)".*/\1/')
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}"
# Compute SHA256 hashes for each platform
# fetchzip hashes the unpacked directory, so we need to extract and hash
compute_unpacked_hash() {
local url="$1"
local ext="$2"
local tmpdir=$(mktemp -d)
local archive="/tmp/opencode-archive.$ext"
curl -fsSL "$url" -o "$archive"
if [ "$ext" = "zip" ]; then
(cd "$tmpdir" && unzip -q "$archive")
else
(cd "$tmpdir" && tar xzf "$archive")
fi
local sri_hash=$(nix hash path "$tmpdir")
local nix32_hash=$(nix hash convert --hash-algo sha256 --to nix32 "$sri_hash")
rm -rf "$tmpdir" "$archive"
echo "$nix32_hash"
}
echo "Computing SHA256 hashes (this may take a moment)..."
SHA_DARWIN_ARM=$(compute_unpacked_hash "https://github.com/anomalyco/opencode/releases/download/v${NEW_VERSION}/opencode-darwin-arm64.zip" "zip")
echo " aarch64-darwin: $SHA_DARWIN_ARM"
SHA_DARWIN_X64=$(compute_unpacked_hash "https://github.com/anomalyco/opencode/releases/download/v${NEW_VERSION}/opencode-darwin-x64.zip" "zip")
echo " x86_64-darwin: $SHA_DARWIN_X64"
SHA_LINUX_X64=$(compute_unpacked_hash "https://github.com/anomalyco/opencode/releases/download/v${NEW_VERSION}/opencode-linux-x64.tar.gz" "tar.gz")
echo " x86_64-linux: $SHA_LINUX_X64"
SHA_LINUX_ARM64=$(compute_unpacked_hash "https://github.com/anomalyco/opencode/releases/download/v${NEW_VERSION}/opencode-linux-arm64.tar.gz" "tar.gz")
echo " aarch64-linux: $SHA_LINUX_ARM64"
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_DARWIN_ARM"
echo " x86_64-darwin SHA: $SHA_DARWIN_X64"
echo " x86_64-linux SHA: $SHA_LINUX_X64"
echo " aarch64-linux SHA: $SHA_LINUX_ARM64"
exit 0
fi
# Update version
sed -i.tmp "s/version = \".*\";/version = \"$NEW_VERSION\";/" "$NIX_FILE"
# Update SHA256 hashes using awk
awk -v sha_arm="$SHA_DARWIN_ARM" -v sha_x64="$SHA_DARWIN_X64" -v sha_linux_x64="$SHA_LINUX_X64" -v sha_linux_arm="$SHA_LINUX_ARM64" '
/aarch64-darwin = {/ { in_arm = 1 }
/x86_64-darwin = {/ { in_x64 = 1; in_arm = 0 }
/x86_64-linux = {/ { in_linux_x64 = 1; in_x64 = 0 }
/aarch64-linux = {/ { in_linux_arm = 1; in_linux_x64 = 0 }
/};/ {
in_arm = 0
in_x64 = 0
in_linux_x64 = 0
in_linux_arm = 0
}
/sha256 = / {
if (in_arm) {
sub(/sha256 = ".*";/, "sha256 = \"" sha_arm "\";")
} else if (in_x64) {
sub(/sha256 = ".*";/, "sha256 = \"" sha_x64 "\";")
} else if (in_linux_x64) {
sub(/sha256 = ".*";/, "sha256 = \"" sha_linux_x64 "\";")
} else if (in_linux_arm) {
sub(/sha256 = ".*";/, "sha256 = \"" sha_linux_arm "\";")
}
}
{ print }
' "$NIX_FILE" > "$NIX_FILE.new"
mv "$NIX_FILE.new" "$NIX_FILE"
rm -f "$NIX_FILE.tmp"
echo -e "${GREEN}Successfully updated to version $NEW_VERSION${NC}"
echo ""
echo "Updated SHA256 hashes:"
echo " aarch64-darwin: $SHA_DARWIN_ARM"
echo " x86_64-darwin: $SHA_DARWIN_X64"
echo " x86_64-linux: $SHA_LINUX_X64"
echo " aarch64-linux: $SHA_LINUX_ARM64"
echo ""
echo "Next steps:"
echo " 1. Review changes: git diff $NIX_FILE"
echo " 2. Test build: nix build .#custom-opencode"
echo " 3. Verify version: ./result/bin/opencode --version"
echo " 4. Commit: git add $NIX_FILE && git commit -m 'opencode: Update to version $NEW_VERSION'"

View File

@@ -1,79 +0,0 @@
{ lib
, stdenv
, fetchurl
, patchelf
, glibc
, makeWrapper
}:
let
version = "0.55.4";
srcs = {
aarch64-darwin = {
url = "https://github.com/badlogic/pi-mono/releases/download/v${version}/pi-darwin-arm64.tar.gz";
sha256 = "0vsav9frvnzskk6p6j60i7klrs3m8lphhyi4c39mv2mvhpm8fkl5";
};
x86_64-darwin = {
url = "https://github.com/badlogic/pi-mono/releases/download/v${version}/pi-darwin-x64.tar.gz";
sha256 = "1377rvhsiiww1bbpgv2v46fjm7iz2smmh8g2yhm28kbsq3gwvvr0";
};
x86_64-linux = {
url = "https://github.com/badlogic/pi-mono/releases/download/v${version}/pi-linux-x64.tar.gz";
sha256 = "1wnfwnkfq5ffz6wyqyhciv4lz06bpxims0hv0dlhz0f9vliyc1md";
};
aarch64-linux = {
url = "https://github.com/badlogic/pi-mono/releases/download/v${version}/pi-linux-arm64.tar.gz";
sha256 = "00fp37hgjl40kc59jfpv189i7np53ymm037hvds6k9y2sz818wjy";
};
};
src = srcs.${stdenv.hostPlatform.system} or (throw "Unsupported system: ${stdenv.hostPlatform.system}");
in stdenv.mkDerivation {
pname = "pi-coding-agent";
inherit version;
src = fetchurl {
inherit (src) url sha256;
};
sourceRoot = "pi";
# Bun standalone binaries have JS code appended after the ELF sections
dontStrip = true;
dontPatchELF = true;
nativeBuildInputs = [ makeWrapper ]
++ lib.optionals stdenv.isLinux [ patchelf ];
installPhase = ''
runHook preInstall
# Install the full pi directory structure (binary + supporting files)
mkdir -p $out/lib/pi-coding-agent
cp -r . $out/lib/pi-coding-agent/
# Create bin wrapper that runs the binary from its lib directory
# (pi expects supporting files like themes and wasm relative to itself)
mkdir -p $out/bin
makeWrapper $out/lib/pi-coding-agent/pi $out/bin/pi
runHook postInstall
'';
# Manually patch the interpreter for bun standalone binaries on Linux
postFixup = lib.optionalString stdenv.isLinux ''
interpreter="${glibc}/lib/${if stdenv.hostPlatform.system == "aarch64-linux" then "ld-linux-aarch64.so.1" else "ld-linux-x86-64.so.2"}"
patchelf --set-interpreter "$interpreter" $out/lib/pi-coding-agent/pi
'';
meta = with lib; {
description = "Minimal terminal coding agent with extensible tools and session management";
homepage = "https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent";
license = licenses.mit;
maintainers = [ ];
platforms = [ "aarch64-darwin" "x86_64-darwin" "x86_64-linux" "aarch64-linux" ];
mainProgram = "pi";
};
}

View File

@@ -1,98 +0,0 @@
{ pkgs, ... }:
pkgs.writeShellScriptBin "rclone-torbox-setup" ''
set -euo pipefail
# Default values
RBW_ENTRY="''${1:-torbox}"
ENV_FILE="''${2:-/etc/rclone/torbox.env}"
usage() {
echo "Usage: rclone-torbox-setup [rbw-entry] [env-file]"
echo ""
echo "Sets up rclone credentials for TorBox WebDAV mount."
echo "Retrieves password from rbw (Bitwarden), obscures it for rclone,"
echo "and writes it to the environment file for the systemd service."
echo ""
echo "Arguments:"
echo " rbw-entry Name of the Bitwarden entry containing the password (default: torbox)"
echo " env-file Path to write the environment file (default: /etc/rclone/torbox.env)"
echo ""
echo "The Bitwarden entry should contain your TorBox password as the password field."
echo ""
echo "Example:"
echo " rclone-torbox-setup torbox-password /etc/rclone/torbox.env"
exit 1
}
if [[ "''${1:-}" == "-h" ]] || [[ "''${1:-}" == "--help" ]]; then
usage
fi
echo "rclone TorBox credential setup"
echo "=============================="
echo ""
# Check if rbw is available
if ! command -v rbw &> /dev/null; then
echo "Error: rbw is not available. Please ensure rbw is installed and configured."
exit 1
fi
# Check if rclone is available
if ! command -v rclone &> /dev/null; then
echo "Error: rclone is not available. Please ensure rclone is installed."
exit 1
fi
echo "Retrieving password from rbw entry: $RBW_ENTRY"
# Retrieve password from Bitwarden
if ! TORBOX_PASS=$(rbw get "$RBW_ENTRY" 2>/dev/null); then
echo ""
echo "Error: Failed to retrieve password from rbw entry '$RBW_ENTRY'"
echo ""
echo "Please ensure:"
echo " 1. The entry '$RBW_ENTRY' exists in Bitwarden"
echo " 2. rbw is unlocked: rbw unlock"
echo " 3. rbw is synced: rbw sync"
echo ""
echo "To create the entry in Bitwarden:"
echo " - Name: $RBW_ENTRY"
echo " - Password: Your TorBox password"
exit 1
fi
echo "Password retrieved successfully"
# Obscure the password for rclone
echo "Obscuring password for rclone..."
if ! OBSCURED_PASS=$(echo -n "$TORBOX_PASS" | rclone obscure -); then
echo "Error: Failed to obscure password with rclone"
exit 1
fi
# Create the directory if needed (requires sudo)
ENV_DIR=$(dirname "$ENV_FILE")
if [[ ! -d "$ENV_DIR" ]]; then
echo "Creating directory $ENV_DIR (requires sudo)..."
sudo mkdir -p "$ENV_DIR"
fi
# Write the environment file
echo "Writing environment file to $ENV_FILE (requires sudo)..."
echo "RCLONE_WEBDAV_PASS=$OBSCURED_PASS" | sudo tee "$ENV_FILE" > /dev/null
sudo chmod 600 "$ENV_FILE"
echo ""
echo "Setup complete!"
echo ""
echo "The environment file has been created at: $ENV_FILE"
echo "The rclone-mount-torbox systemd service will use this file."
echo ""
echo "To activate the mount after NixOS rebuild:"
echo " sudo systemctl start rclone-mount-torbox"
echo ""
echo "To check status:"
echo " sudo systemctl status rclone-mount-torbox"
''

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

@@ -0,0 +1,34 @@
{ lib, stdenv, fetchFromGitHub, meson, pkg-config, vulkan-loader, ninja, writeText, vulkan-headers, vulkan-utility-libraries, jq, libX11, libXrandr, libxcb, wayland, wayland-scanner }:
stdenv.mkDerivation rec {
pname = "vulkan-hdr-layer";
version = "63d2eec";
src = (fetchFromGitHub {
owner = "Zamundaaa";
repo = "VK_hdr_layer";
rev = "869199cd2746e7f69cf19955153080842b6dacfc";
fetchSubmodules = true;
hash = "sha256-xfVYI+Aajmnf3BTaY2Ysg5fyDO6SwDFGyU0L+F+E3is=";
}).overrideAttrs (_: {
GIT_CONFIG_COUNT = 1;
GIT_CONFIG_KEY_0 = "url.https://github.com/.insteadOf";
GIT_CONFIG_VALUE_0 = "git@github.com:";
});
nativeBuildInputs = [ vulkan-headers meson ninja pkg-config jq ];
buildInputs = [ vulkan-headers vulkan-loader vulkan-utility-libraries libX11 libXrandr libxcb wayland wayland-scanner ];
# Help vulkan-loader find the validation layers
setupHook = writeText "setup-hook" ''
addToSearchPath XDG_DATA_DIRS @out@/share
'';
meta = with lib; {
description = "Layers providing Vulkan HDR";
homepage = "https://github.com/Zamundaaa/VK_hdr_layer";
platforms = platforms.linux;
license = licenses.mit;
};
}

View File

@@ -1,82 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"timezone": "America/Los_Angeles",
"gitAuthor": "Renovate Bot <renovate@ogle.fyi>",
"nix": {
"enabled": true
},
"github-actions": {
"managerFilePatterns": [
"/.gitea/workflows/.+\\.ya?ml$/"
]
},
"lockFileMaintenance": {
"enabled": true,
"schedule": [
"after 5pm and before 7pm on Saturday"
]
},
"dependencyDashboard": true,
"dependencyDashboardAutoclose": false,
"dependencyDashboardTitle": "NixOS Configs Dependency Dashboard",
"packageRules": [
{
"description": "Group all GitHub Actions updates",
"matchManagers": [
"github-actions"
],
"groupName": "github-actions"
},
{
"description": "Group stable NixOS ecosystem inputs",
"matchManagers": [
"nix"
],
"groupName": "nix-stable-ecosystem",
"matchPackageNames": [
"/^nixpkgs$/",
"/^home-manager$/",
"/^nix-darwin$/"
],
"schedule": [
"after 5pm and before 7pm on Saturday"
]
},
{
"description": "Group unstable NixOS ecosystem inputs",
"matchManagers": [
"nix"
],
"groupName": "nix-unstable-ecosystem",
"matchPackageNames": [
"/nixpkgs-unstable/",
"/home-manager-unstable/"
],
"schedule": [
"after 5pm and before 7pm on Saturday"
]
},
{
"description": "nixpkgs-qt updates on Saturday (staggered from main ecosystem)",
"matchManagers": [
"nix"
],
"matchPackageNames": [
"/nixpkgs-qt/"
],
"schedule": [
"after 7pm and before 9pm on Saturday"
]
},
{
"description": "Ignore private Gitea inputs (handle separately)",
"matchManagers": [
"nix"
],
"enabled": false,
"matchPackageNames": [
"/google-cookie-retrieval/"
]
}
]
}

View File

@@ -21,11 +21,17 @@ in
services.pipewire = {
enable = true;
alsa.enable = true;
alsa.support32Bit = true;
pulse.enable = true;
};
services.pulseaudio = {
package = pkgs.pulseaudioFull;
extraConfig = ''
load-module module-combine-sink
load-module module-switch-on-connect
'';
};
services.squeezelite = {
#enable = true;
pulseAudio = true;

View File

@@ -1,43 +0,0 @@
# Common configuration shared between NixOS and Darwin
{ lib, pkgs, ... }:
{
config = {
time.timeZone = "America/Los_Angeles";
environment.systemPackages = with pkgs; [
git
glances
pciutils
tree
usbutils
vim
] ++ lib.optionals pkgs.stdenv.isLinux [
ghostty.terminfo # So tmux works when SSH'ing from ghostty
];
nix = {
package = pkgs.nix;
settings = {
experimental-features = [ "nix-command" "flakes" ];
max-jobs = "auto";
trusted-users = [ "johno" ];
substituters = [
"http://john-endesktop.oglehome:5000"
];
trusted-public-keys = [
"harmonia.john-endesktop:1iGr4xZrsR7WtXOlPCgFF3LcODYBpu+B3TS54MyBn4M="
];
fallback = true;
connect-timeout = 5;
};
gc = {
automatic = true;
options = "--delete-older-than 10d";
};
};
nixpkgs.config.allowUnfree = true;
};
}

View File

@@ -7,10 +7,6 @@ let
setEnvironmentPath = "${config.system.build.setEnvironment}";
in
{
imports = [
./common.nix
];
config = {
# Salt manages /etc/bashrc, /etc/zshrc, /etc/zshenv
# nix-darwin writes to .local variants for nix-specific configuration
@@ -47,6 +43,8 @@ in
fi
'';
time.timeZone = "America/Los_Angeles";
# System preferences
system.defaults = {
# Custom keyboard shortcuts
@@ -81,5 +79,42 @@ in
};
};
};
environment.systemPackages = with pkgs; [
git
glances
pciutils
tree
usbutils
vim
];
nix = {
package = pkgs.nix;
# distributedBuilds = true;
# buildMachines = [{
# hostName = "z790prors.oglehome";
# system = "x86_64-linux";
# protocol = "ssh-ng";
# sshUser = "johno";
# sshKey = "/root/.ssh/id_ed25519";
# maxJobs = 3;
# speedFactor = 2;
# }];
settings = {
experimental-features = [ "nix-command" "flakes" ];
max-jobs = "auto";
trusted-users = [ "johno" ];
substituters = [
];
};
gc = {
automatic = true;
options = "--delete-older-than 10d";
};
};
nixpkgs.config.allowUnfree = true;
};
}
}

View File

@@ -4,23 +4,18 @@ with lib;
{
imports = [
./common.nix
./audio
./bluetooth
./btrfs
./desktop
./k3s-node
./kodi
./nfs-mounts
./plasma-bigscreen
./nvidia
./printing
./rclone-mount
./remote-build
./spotifyd
./users
./virtualisation
./wireguard
];
config = {
@@ -36,6 +31,7 @@ with lib;
LC_TELEPHONE = "en_US.UTF-8";
LC_TIME = "en_US.UTF-8";
};
time.timeZone = "America/Los_Angeles";
services.xserver.xkb = {
layout = "us";
@@ -53,7 +49,42 @@ with lib;
# Enable the OpenSSH daemon.
services.openssh.enable = true;
# NixOS-specific gc option (not available on Darwin)
nix.gc.randomizedDelaySec = "14m";
environment.systemPackages = with pkgs; [
git
glances
pciutils
tree
usbutils
vim
];
nix = {
package = pkgs.nix;
# distributedBuilds = true;
# buildMachines = [{
# hostName = "z790prors.oglehome";
# system = "x86_64-linux";
# protocol = "ssh-ng";
# sshUser = "johno";
# sshKey = "/root/.ssh/id_ed25519";
# maxJobs = 3;
# speedFactor = 2;
# }];
settings = {
experimental-features = [ "nix-command" "flakes" ];
max-jobs = "auto";
trusted-users = [ "johno" ];
substituters = [
];
};
gc = {
automatic = true;
randomizedDelaySec = "14m";
options = "--delete-older-than 10d";
};
};
nixpkgs.config.allowUnfree = true;
};
}

View File

@@ -22,8 +22,6 @@ in
# indiviudal cores
#retroarch-full
ryubing
yarg
];
programs.steam = {

View File

@@ -17,10 +17,9 @@ in
services.gnome.gnome-keyring.enable = true;
programs.kdeconnect.enable = true;
# XDG Desktop Portal for default application handling
# XDG Desktop Portal for default application handling in non-KDE environments
xdg.portal = {
enable = true;
wlr.enable = cfg.wayland; # xdg-desktop-portal-wlr for Sway screen sharing
extraPortals = with pkgs; [
kdePackages.xdg-desktop-portal-kde # For KDE application integration
xdg-desktop-portal-gtk # Fallback for GTK applications

View File

@@ -11,8 +11,9 @@ in
enable = true;
wrapperFeatures.gtk = true;
};
programs.light.enable = true;
environment.systemPackages = with pkgs; [
brightnessctl
grim
slurp
wl-clipboard

View File

@@ -1,81 +0,0 @@
{ lib, config, pkgs, ... }:
with lib;
let
cfg = config.roles.k3s-node;
in
{
options.roles.k3s-node = {
enable = mkEnableOption "Enable k3s node";
role = mkOption {
type = types.enum [ "server" "agent" ];
default = "agent";
description = "k3s role: server (control plane) or agent (worker)";
};
serverAddr = mkOption {
type = types.str;
default = "https://10.0.0.222:6443";
description = "URL of k3s server to join (required for agents, used for HA servers)";
};
tokenFile = mkOption {
type = types.path;
default = "/etc/k3s/token";
description = "Path to file containing the cluster join token";
};
clusterInit = mkOption {
type = types.bool;
default = false;
description = "Initialize a new cluster (first server only)";
};
extraFlags = mkOption {
type = types.listOf types.str;
default = [];
description = "Additional flags to pass to k3s";
};
gracefulNodeShutdown = mkOption {
type = types.bool;
default = true;
description = "Enable graceful node shutdown";
};
openFirewall = mkOption {
type = types.bool;
default = true;
description = "Open firewall ports for k3s";
};
};
config = mkIf cfg.enable {
# k3s service configuration
services.k3s = {
enable = true;
role = cfg.role;
tokenFile = cfg.tokenFile;
extraFlags = cfg.extraFlags;
gracefulNodeShutdown.enable = cfg.gracefulNodeShutdown;
serverAddr = if (cfg.role == "agent" || !cfg.clusterInit) then cfg.serverAddr else "";
clusterInit = cfg.role == "server" && cfg.clusterInit;
};
# Firewall rules for k3s
networking.firewall = mkIf cfg.openFirewall {
allowedTCPPorts = [
6443 # k3s API server
10250 # kubelet metrics
] ++ optionals (cfg.role == "server") [
2379 # etcd clients (HA)
2380 # etcd peers (HA)
];
allowedUDPPorts = [
8472 # flannel VXLAN
];
};
};
}

View File

@@ -22,7 +22,7 @@ in
appLauncherServer = {
enable = mkOption {
type = types.bool;
default = false;
default = true;
description = "Enable HTTP app launcher server for remote control";
};
port = mkOption {
@@ -47,23 +47,23 @@ in
if cfg.jellyfinScaleFactor != null
then pkgs.symlinkJoin {
name = "jellyfin-media-player-scaled";
paths = [ pkgs.qt-pinned.jellyfin-media-player ];
paths = [ pkgs.jellyfin-media-player ];
nativeBuildInputs = [ pkgs.makeWrapper ];
postBuild = ''
mkdir -p $out/bin
rm -f $out/bin/jellyfin-desktop
makeWrapper ${pkgs.qt-pinned.jellyfin-media-player}/bin/jellyfin-desktop $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.qt-pinned.jellyfin-media-player}/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.qt-pinned.jellyfin-media-player;
else pkgs.jellyfin-media-player;
in mkIf cfg.enable
{
users.extraUsers.kodi = {
@@ -77,15 +77,14 @@ in
};
environment.systemPackages = with pkgs; [
firefox
jellyfinMediaPlayerPkg
kodiPkg
qt-pinned.stremio
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/stremio dependency. These are pinned to nixpkgs-qt to avoid rebuilds - update that input separately when you have time."
"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"
];

View File

@@ -8,21 +8,6 @@ in
{
options.roles.nfs-mounts = {
enable = mkEnableOption "Enable default NFS mounts";
server = mkOption {
type = types.str;
default = "10.0.0.43";
description = "IP address or hostname of the NFS server";
};
remotePath = mkOption {
type = types.str;
default = "/media";
description = "Remote path to mount from the NFS server";
};
mountPoint = mkOption {
type = types.str;
default = "/media";
description = "Local mount point for the NFS share";
};
# TODO: implement requireMount
requireMount = mkOption {
type = types.bool;
@@ -33,8 +18,8 @@ in
config = mkIf cfg.enable
{
fileSystems.${cfg.mountPoint} = {
device = "${cfg.server}:${cfg.remotePath}";
fileSystems."/media" = {
device = "10.0.0.43:/media";
fsType = "nfs";
options = [
"defaults"

View File

@@ -8,89 +8,9 @@ in
{
options.roles.nvidia = {
enable = mkEnableOption "Enable the nvidia role";
# Driver configuration options
open = mkOption {
type = types.bool;
default = true;
description = "Use the open source nvidia kernel driver (for Turing and newer GPUs).";
};
modesetting = mkOption {
type = types.bool;
default = true;
description = "Enable kernel modesetting for nvidia.";
};
nvidiaSettings = mkOption {
type = types.bool;
default = true;
description = "Enable the nvidia-settings GUI.";
};
package = mkOption {
type = types.enum [ "stable" "latest" "beta" "vulkan_beta" "production" ];
default = "stable";
description = "The nvidia driver package to use.";
};
powerManagement = {
enable = mkOption {
type = types.bool;
default = false;
description = "Enable nvidia power management (useful for laptops, not recommended for desktops).";
};
finegrained = mkOption {
type = types.bool;
default = false;
description = "Enable fine-grained power management for Turing and newer GPUs.";
};
};
graphics = {
enable = mkOption {
type = types.bool;
default = true;
description = "Enable hardware graphics support.";
};
enable32Bit = mkOption {
type = types.bool;
default = false;
description = "Enable 32-bit graphics libraries (needed for some games).";
};
extraPackages = mkOption {
type = types.listOf types.package;
default = [];
description = "Extra packages to add to hardware.graphics.extraPackages.";
};
};
};
config = mkIf cfg.enable {
# Set xserver video driver
services.xserver.videoDrivers = [ "nvidia" ];
# Graphics configuration
hardware.graphics = {
enable = cfg.graphics.enable;
enable32Bit = cfg.graphics.enable32Bit;
extraPackages = cfg.graphics.extraPackages;
};
# NVIDIA driver configuration
hardware.nvidia = {
modesetting.enable = cfg.modesetting;
nvidiaSettings = cfg.nvidiaSettings;
open = cfg.open;
package = config.boot.kernelPackages.nvidiaPackages.${cfg.package};
powerManagement.enable = cfg.powerManagement.enable;
powerManagement.finegrained = cfg.powerManagement.finegrained;
};
# Additional packages for nvidia support
environment.systemPackages = with pkgs; [
libva-utils
nvidia-vaapi-driver

View File

@@ -1,134 +0,0 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.roles.plasma-bigscreen;
# Plasma Bigscreen package — not yet in nixpkgs, built from upstream master.
# TODO: Replace with pkgs.kdePackages.plasma-bigscreen once available.
plasma-bigscreen = pkgs.kdePackages.callPackage ./package.nix {};
jellyfinMediaPlayerPkg =
if cfg.jellyfinScaleFactor != null
then pkgs.symlinkJoin {
name = "jellyfin-media-player-scaled";
paths = [ pkgs.qt-pinned.jellyfin-media-player ];
nativeBuildInputs = [ pkgs.makeWrapper ];
postBuild = ''
mkdir -p $out/bin
rm -f $out/bin/jellyfin-desktop
makeWrapper ${pkgs.qt-pinned.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.qt-pinned.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.qt-pinned.jellyfin-media-player;
in
{
options.roles.plasma-bigscreen = {
enable = mkEnableOption "Plasma Bigscreen TV interface";
autologin = mkOption {
type = types.bool;
default = false;
description = "Auto-login to Plasma Bigscreen session";
};
user = mkOption {
type = types.str;
default = "kodi";
description = "User account for the Bigscreen session";
};
jellyfinScaleFactor = mkOption {
type = types.nullOr types.float;
default = null;
description = "Scale factor for Jellyfin Media Player UI (e.g., 1.0 for 100% scaling)";
};
appLauncherServer = {
enable = mkOption {
type = types.bool;
default = false;
description = "Enable HTTP app launcher server for remote control";
};
port = mkOption {
type = types.int;
default = 8081;
description = "Port for the app launcher HTTP server";
};
};
};
config = mkIf cfg.enable {
# Create the bigscreen user
users.extraUsers.${cfg.user} = {
isNormalUser = true;
extraGroups = [ "wheel" "networkmanager" "audio" "video" ];
};
# Plasma Bigscreen is a Plasma 6 shell — needs Plasma 6 desktop manager
services.desktopManager.plasma6.enable = true;
# Register the bigscreen session with the display manager
services.displayManager = {
sessionPackages = [ plasma-bigscreen ];
} // optionalAttrs cfg.autologin {
autoLogin.enable = true;
autoLogin.user = cfg.user;
defaultSession = "plasma-bigscreen-wayland";
};
xdg.portal.configPackages = [ plasma-bigscreen ];
# Fix homescreen not being focused after quitting app or on boot
# xwaylandvideobridge can interfere with focus; exclude if present
environment.plasma6.excludePackages =
lib.optional (pkgs.kdePackages ? xwaylandvideobridge) pkgs.kdePackages.xwaylandvideobridge;
# Firewall for remote control
networking.firewall = {
allowedTCPPorts = optional cfg.appLauncherServer.enable cfg.appLauncherServer.port;
};
environment.systemPackages = with pkgs; [
plasma-bigscreen
firefox
jellyfinMediaPlayerPkg
qt-pinned.stremio
wget
] ++ 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/stremio dependency."
[
"qtwebengine-5.15.19"
];
programs.kdeconnect.enable = true;
systemd.user.services = mkIf cfg.appLauncherServer.enable {
app-launcher-server = {
description = "HTTP App Launcher Server";
wantedBy = [ "graphical-session.target" ];
after = [ "graphical-session.target" ];
serviceConfig = {
Type = "simple";
ExecStart = "${pkgs.custom.app-launcher-server}/bin/app-launcher-server ${toString cfg.appLauncherServer.port}";
Restart = "always";
RestartSec = "5s";
Environment = [
"PATH=${pkgs.firefox}/bin:/run/current-system/sw/bin"
];
};
};
};
};
}

View File

@@ -1,120 +0,0 @@
# Plasma Bigscreen — TV interface for Plasma 6
# Not yet released or packaged in nixpkgs; built from upstream master.
#
# TODO: Remove this file once plasma-bigscreen lands in nixpkgs.
# Tracking issue: https://github.com/NixOS/nixpkgs/issues/428077
# Draft nixpkgs PR: https://github.com/NixOS/nixpkgs/pull/428353
# When available, switch to pkgs.kdePackages.plasma-bigscreen.
#
# Upstream: https://invent.kde.org/plasma/plasma-bigscreen
{
mkKdeDerivation,
lib,
fetchFromGitLab,
pkg-config,
# KDE Frameworks 6
ki18n,
kdeclarative,
kcmutils,
knotifications,
kio,
kwayland,
kwindowsystem,
ksvg,
kiconthemes,
kglobalaccel,
kdbusaddons,
# KDE Plasma 6
plasma-workspace,
plasma-nano,
plasma-nm,
plasma-activities,
plasma-activities-stats,
milou,
libkscreen,
kdeconnect-kde,
# Qt 6
qtdeclarative, # needed for Qt6::QmlPrivate — see QCoro workaround in postPatch
qtmultimedia,
qtwebengine,
# Other
bluez-qt,
qcoro,
plasma-wayland-protocols,
wayland,
sdl3,
}:
mkKdeDerivation {
pname = "plasma-bigscreen";
version = "unstable-2026-03-07";
src = fetchFromGitLab {
domain = "invent.kde.org";
owner = "plasma";
repo = "plasma-bigscreen";
rev = "bd143fea7e386bac1652b8150a3ed3d5ef7cf93c";
hash = "sha256-y439IX7e0+XqxqFj/4+P5le0hA7DiwA+smDsD0UH/fI=";
};
extraNativeBuildInputs = [
pkg-config
];
extraBuildInputs = [
# KDE Frameworks (auto-injected by mkKdeDerivation: ki18n, kcmutils,
# knotifications, kio, kwayland, kwindowsystem, ksvg, kiconthemes)
kdeclarative
kglobalaccel
kdbusaddons
# Plasma (auto-injected: plasma-workspace, plasma-activities,
# plasma-activities-stats, libkscreen)
plasma-nano
plasma-nm
milou
kdeconnect-kde
# Qt — qtdeclarative is needed for Qt6::QmlPrivate (see postPatch)
qtdeclarative
qtmultimedia
qtwebengine
# Other (auto-injected: bluez-qt)
qcoro
plasma-wayland-protocols
wayland
sdl3
];
postPatch = ''
substituteInPlace bin/plasma-bigscreen-wayland.in \
--replace @KDE_INSTALL_FULL_LIBEXECDIR@ "${plasma-workspace}/libexec"
# WORKAROUND: Plasma version numbers must match; we're building an
# unreleased package against a stable Plasma release. Remove once
# bigscreen is part of the Plasma release cycle.
substituteInPlace CMakeLists.txt \
--replace-fail 'set(PROJECT_VERSION "6.5.80")' 'set(PROJECT_VERSION "${plasma-workspace.version}")'
# WORKAROUND: QCoro6Qml's cmake config links against Qt6::QmlPrivate but
# doesn't call find_package to import the target. This is arguably a QCoro
# packaging bug in nixpkgs (it should propagate qtdeclarative). Remove
# once QCoro or the nixpkgs plasma-bigscreen package is fixed upstream.
substituteInPlace CMakeLists.txt \
--replace-fail 'find_package(QCoro6' 'find_package(Qt6 ''${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS QmlPrivate)
find_package(QCoro6'
'';
preFixup = ''
wrapQtApp $out/bin/plasma-bigscreen-wayland
'';
passthru.providedSessions = [
"plasma-bigscreen-wayland"
];
meta = {
description = "Plasma shell for TVs (Plasma Bigscreen)";
homepage = "https://plasma-bigscreen.org";
license = lib.licenses.gpl2Plus;
platforms = lib.platforms.linux;
};
}

View File

@@ -8,21 +8,6 @@ in
{
options.roles.printing = {
enable = mkEnableOption "Enable default printing setup";
printerName = mkOption {
type = types.str;
default = "MFC-L8900CDW_series";
description = "Name for the default printer";
};
printerUri = mkOption {
type = types.str;
default = "ipp://brother.oglehome/ipp/print";
description = "Device URI for the default printer (e.g., ipp://hostname/ipp/print)";
};
printerModel = mkOption {
type = types.str;
default = "everywhere";
description = "PPD model for the printer (use 'everywhere' for driverless IPP)";
};
};
config = mkIf cfg.enable
@@ -36,11 +21,11 @@ in
};
hardware.printers.ensurePrinters = [{
name = cfg.printerName;
deviceUri = cfg.printerUri;
model = cfg.printerModel;
name = "MFC-L8900CDW_series";
deviceUri = "ipp://brother.oglehome/ipp/print";
model = "everywhere";
}];
hardware.printers.ensureDefaultPrinter = cfg.printerName;
hardware.printers.ensureDefaultPrinter = "MFC-L8900CDW_series";
# Fix ensure-printers service to wait for network availability
systemd.services.ensure-printers = {

View File

@@ -1,149 +0,0 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.roles.rclone-mount;
# Generate systemd service for a single mount
mkMountService = name: mountCfg: {
description = "rclone mount for ${name}";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
# Wait for parent mount points (e.g., ZFS pools) to be available
unitConfig = mkIf (mountCfg.requiresMountsFor != []) {
RequiresMountsFor = mountCfg.requiresMountsFor;
};
serviceConfig = {
Type = "notify";
ExecStartPre = "${pkgs.coreutils}/bin/mkdir -p ${mountCfg.mountPoint}";
ExecStart = concatStringsSep " " ([
"${pkgs.rclone}/bin/rclone mount"
":webdav:${mountCfg.remotePath}"
"${mountCfg.mountPoint}"
"--webdav-url=${mountCfg.webdavUrl}"
"--webdav-vendor=${mountCfg.webdavVendor}"
"--webdav-user=${mountCfg.username}"
"--allow-other"
"--vfs-cache-mode=${mountCfg.vfsCacheMode}"
"--dir-cache-time=${mountCfg.dirCacheTime}"
"--poll-interval=${mountCfg.pollInterval}"
"--log-level=${mountCfg.logLevel}"
] ++ mountCfg.extraArgs);
ExecStop = "${pkgs.fuse}/bin/fusermount -uz ${mountCfg.mountPoint}";
Restart = "on-failure";
RestartSec = "10s";
EnvironmentFile = mountCfg.environmentFile;
};
};
in
{
options.roles.rclone-mount = {
enable = mkEnableOption "Enable rclone WebDAV mounts";
mounts = mkOption {
type = types.attrsOf (types.submodule {
options = {
webdavUrl = mkOption {
type = types.str;
description = "WebDAV server URL (e.g., https://webdav.torbox.app)";
};
webdavVendor = mkOption {
type = types.enum [ "other" "nextcloud" "owncloud" "sharepoint" "sharepoint-ntlm" "fastmail" ];
default = "other";
description = "WebDAV server vendor for optimizations";
};
username = mkOption {
type = types.str;
description = "WebDAV username (often email address)";
};
environmentFile = mkOption {
type = types.path;
description = ''
Path to environment file containing RCLONE_WEBDAV_PASS.
The password should be obscured using: rclone obscure <password>
File format: RCLONE_WEBDAV_PASS=<obscured_password>
'';
};
mountPoint = mkOption {
type = types.str;
description = "Local mount point path";
};
remotePath = mkOption {
type = types.str;
default = "/";
description = "Remote path on WebDAV server to mount";
};
vfsCacheMode = mkOption {
type = types.enum [ "off" "minimal" "writes" "full" ];
default = "full";
description = ''
VFS cache mode. For streaming media, 'full' is recommended.
- off: No caching (direct reads/writes)
- minimal: Cache open files only
- writes: Cache writes and open files
- full: Full caching of all files
'';
};
dirCacheTime = mkOption {
type = types.str;
default = "5m";
description = "Time to cache directory entries";
};
pollInterval = mkOption {
type = types.str;
default = "1m";
description = "Poll interval for remote changes";
};
logLevel = mkOption {
type = types.enum [ "DEBUG" "INFO" "NOTICE" "ERROR" ];
default = "INFO";
description = "rclone log level";
};
extraArgs = mkOption {
type = types.listOf types.str;
default = [];
description = "Extra arguments to pass to rclone mount";
};
requiresMountsFor = mkOption {
type = types.listOf types.str;
default = [];
description = ''
List of mount points that must be available before this service starts.
Use this when the mount point's parent is on a ZFS pool or other filesystem
that may not be mounted at boot time.
Example: [ "/media" ] to wait for the media ZFS pool to mount.
'';
};
};
});
default = {};
description = "Attribute set of rclone WebDAV mounts to configure";
};
};
config = mkIf cfg.enable {
# Ensure FUSE is available
environment.systemPackages = [ pkgs.rclone pkgs.fuse ];
programs.fuse.userAllowOther = true;
# Create systemd services for each mount
systemd.services = mapAttrs' (name: mountCfg:
nameValuePair "rclone-mount-${name}" (mkMountService name mountCfg)
) cfg.mounts;
};
}

View File

@@ -1,66 +1,3 @@
# Remote Build Role
#
# This module configures Nix distributed builds, allowing machines to offload
# builds to more powerful remote machines.
#
# SETUP INSTRUCTIONS
# ==================
#
# 1. BUILDER MACHINE SETUP
# On machines that will serve as builders (e.g., zix790prors, john-endesktop):
#
# a) Enable the builder role in configuration.nix:
# roles.remote-build.enableBuilder = true;
#
# b) After nixos-rebuild, the nix-builder user is created automatically.
# You need to add client SSH public keys to the builder. Either:
#
# Option A - Manual (recommended for initial setup):
# sudo mkdir -p /var/lib/nix-builder/.ssh
# sudo bash -c 'cat >> /var/lib/nix-builder/.ssh/authorized_keys' << 'EOF'
# ssh-ed25519 AAAA... root@client-hostname
# EOF
# sudo chown -R nix-builder:nix-builder /var/lib/nix-builder/.ssh
# sudo chmod 700 /var/lib/nix-builder/.ssh
# sudo chmod 600 /var/lib/nix-builder/.ssh/authorized_keys
#
# Option B - Via NixOS config (if you store keys in the repo):
# users.users.nix-builder.openssh.authorizedKeys.keys = [
# "ssh-ed25519 AAAA... root@client-hostname"
# ];
#
# 2. CLIENT MACHINE SETUP
# On machines that will use remote builders (e.g., nix-book):
#
# a) Configure builders in configuration.nix:
# roles.remote-build.builders = [
# {
# hostName = "zix790prors.oglehome";
# maxJobs = 16; # Number of parallel build jobs
# speedFactor = 3; # Higher = prefer this builder
# }
# {
# hostName = "john-endesktop.oglehome";
# maxJobs = 1; # Conservative for busy machines
# speedFactor = 1;
# }
# ];
#
# b) Generate SSH key for root (if not exists) and copy to builders:
# sudo ssh-keygen -t ed25519 -f /root/.ssh/id_ed25519 -N ""
# sudo cat /root/.ssh/id_ed25519.pub # Add this to builder's authorized_keys
#
# c) Accept the builder's host key (as root):
# sudo ssh nix-builder@zix790prors echo "Connected!"
# sudo ssh nix-builder@john-endesktop echo "Connected!"
#
# 3. VERIFY SETUP
# Test that distributed builds work:
# nix build --rebuild nixpkgs#hello --print-build-logs
#
# Check builder connectivity:
# nix store ping --store ssh-ng://nix-builder@zix790prors
#
{ lib, config, pkgs, ... }:
with lib;

View File

@@ -8,16 +8,6 @@ in
{
options.roles.virtualisation = {
enable = mkEnableOption "Enable virtualisation";
dockerUsers = mkOption {
type = types.listOf types.str;
default = [ "johno" ];
description = "List of users to add to the docker group";
};
waydroid = mkOption {
type = types.bool;
default = false;
description = "Enable waydroid support";
};
};
config = mkIf cfg.enable
@@ -25,7 +15,6 @@ in
virtualisation.libvirtd.enable = true;
programs.virt-manager.enable = true;
virtualisation.docker.enable = true;
users.extraGroups.docker.members = cfg.dockerUsers;
virtualisation.waydroid.enable = cfg.waydroid;
users.extraGroups.docker.members = [ "johno" ];
};
}

View File

@@ -1,71 +0,0 @@
{ config, lib, ... }:
with lib;
let
cfg = config.roles.wireguard;
in
{
options.roles.wireguard = {
enable = mkEnableOption "Enable WireGuard VPN";
interfaceName = mkOption {
type = types.str;
default = "wg0";
description = "Name of the WireGuard interface";
};
address = mkOption {
type = types.listOf types.str;
description = "Address(es) for the WireGuard interface";
};
privateKeyFile = mkOption {
type = types.path;
description = "Path to a root-owned file containing the WireGuard private key";
};
dns = mkOption {
type = types.listOf types.str;
default = [];
description = "DNS servers to use when the tunnel is active";
};
peers = mkOption {
type = types.listOf (types.submodule {
options = {
publicKey = mkOption {
type = types.str;
description = "Public key of the peer";
};
endpoint = mkOption {
type = types.str;
description = "Endpoint address of the peer (host:port)";
};
allowedIPs = mkOption {
type = types.listOf types.str;
description = "List of allowed IP ranges for this peer";
};
persistentKeepalive = mkOption {
type = types.int;
default = 25;
description = "Persistent keepalive interval in seconds";
};
};
});
description = "WireGuard peers";
};
autostart = mkOption {
type = types.bool;
default = false;
description = "Whether to start the VPN automatically on boot";
};
};
config = mkIf cfg.enable {
networking.wg-quick.interfaces.${cfg.interfaceName} = {
inherit (cfg) address dns autostart peers;
privateKeyFile = cfg.privateKeyFile;
};
systemd.services."wg-quick-${cfg.interfaceName}" = {
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
};
};
}

View File

@@ -1,22 +0,0 @@
#!/usr/bin/env bash
# Build Live USB ISO from flake configuration
# Creates an uncompressed ISO suitable for Ventoy and other USB boot tools
# Usage: nix run .#build-liveusb
# Or: ./scripts/build-liveusb.sh
set -euo pipefail
REPO_ROOT="${REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
echo "Building Live USB ISO..."
nix build "${REPO_ROOT}#nixosConfigurations.live-usb.config.system.build.isoImage" --show-trace
if ls "${REPO_ROOT}/result/iso/"*.iso 1> /dev/null 2>&1; then
iso_file=$(ls "${REPO_ROOT}/result/iso/"*.iso)
echo "Build complete!"
echo "ISO location: $iso_file"
echo "Ready for Ventoy or dd to USB"
else
echo "Build failed - no ISO file found"
exit 1
fi

View File

@@ -1,65 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--help|-h)
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Rotate to the next wallpaper in the configured list."
echo ""
echo "This script increments the currentIndex in home/wallpapers/default.nix,"
echo "cycling through available wallpapers. Rebuild your system to apply"
echo "the new wallpaper."
echo ""
echo "Options:"
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
# Configuration
REPO_ROOT="${REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
WALLPAPER_FILE="$REPO_ROOT/home/wallpapers/default.nix"
echo -e "${GREEN}Rotating wallpaper...${NC}"
# Check if file exists
if [[ ! -f "$WALLPAPER_FILE" ]]; then
echo -e "${RED}Error: $WALLPAPER_FILE not found${NC}"
exit 1
fi
# Get current index
CURRENT_INDEX=$(grep -oP 'currentIndex = \K\d+' "$WALLPAPER_FILE")
echo -e "Current index: ${YELLOW}$CURRENT_INDEX${NC}"
# Count wallpapers (count occurrences of "name = " in the wallpapers list)
WALLPAPER_COUNT=$(grep -c 'name = "' "$WALLPAPER_FILE")
echo -e "Total wallpapers: ${YELLOW}$WALLPAPER_COUNT${NC}"
# Calculate next index (wrap around)
NEXT_INDEX=$(( (CURRENT_INDEX + 1) % WALLPAPER_COUNT ))
echo -e "Next index: ${YELLOW}$NEXT_INDEX${NC}"
# Update the currentIndex
sed -i "s/currentIndex = $CURRENT_INDEX;/currentIndex = $NEXT_INDEX;/" "$WALLPAPER_FILE"
echo -e "${GREEN}Successfully rotated wallpaper!${NC}"
echo -e " Old index: ${YELLOW}$CURRENT_INDEX${NC}"
echo -e " New index: ${YELLOW}$NEXT_INDEX${NC}"
echo ""
echo "Rebuild your system to apply the new wallpaper."

View File

@@ -1,30 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--help|-h)
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Update Doom Emacs to the latest commit from the doomemacs repository."
echo ""
echo "This script fetches the latest commit SHA from the default branch,"
echo "updates the rev and sha256 in home/roles/emacs/default.nix, and"
echo "prepares the configuration for a system rebuild."
echo ""
echo "Options:"
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'

View File

@@ -1,75 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--help|-h)
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Perform a major upgrade of the NixOS configuration."
echo ""
echo "This script runs the following steps:"
echo " 1. Update all flake inputs (nix flake update)"
echo " 2. Update Doom Emacs to the latest commit"
echo " 3. Update Claude Code to the latest version"
echo " 4. Rotate to the next wallpaper"
echo ""
echo "After completion, review changes with 'git diff' and rebuild"
echo "your system with 'sudo nixos-rebuild switch --flake .'"
echo ""
echo "Options:"
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'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
REPO_ROOT="${REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE} NixOS Configuration Major Upgrade${NC}"
echo -e "${BLUE}========================================${NC}"
echo ""
# Step 1: Update flake inputs
echo -e "${GREEN}[1/4] Updating flake inputs...${NC}"
cd "$REPO_ROOT"
nix flake update
echo ""
# Step 2: Update Doom Emacs
echo -e "${GREEN}[2/4] Updating Doom Emacs...${NC}"
"$REPO_ROOT/scripts/update-doomemacs.sh"
echo ""
# Step 3: Update Claude Code
echo -e "${GREEN}[3/4] Updating Claude Code...${NC}"
"$REPO_ROOT/packages/claude-code/update.sh"
echo ""
# Step 4: Rotate wallpaper
echo -e "${GREEN}[4/4] Rotating wallpaper...${NC}"
"$REPO_ROOT/scripts/rotate-wallpaper.sh"
echo ""
echo -e "${BLUE}========================================${NC}"
echo -e "${GREEN}Upgrade complete!${NC}"
echo -e "${BLUE}========================================${NC}"
echo ""
echo "Next steps:"
echo " 1. Review changes: git diff"
echo " 2. Rebuild system: sudo nixos-rebuild switch --flake ."
echo " 3. If satisfied, commit: git add -A && git commit -m 'chore: Major upgrade'"

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