Compare commits

..

2 Commits

Author SHA1 Message Date
433cfe3985 Update plan to use WinBtrfs 2025-07-29 11:18:24 -07:00
ffd7ce45a7 Initial plan using symlinks 2025-07-29 10:34:50 -07:00
81 changed files with 1121 additions and 6389 deletions

1
.gitignore vendored
View File

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

177
AGENTS.md
View File

@@ -1,177 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Repository Overview
This is a NixOS configuration repository using flakes, managing multiple machines and home-manager configurations. The repository follows a modular architecture with reusable "roles" that can be composed for different machines.
## Architecture
### 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`, `wixos` (WSL configuration), `zix790prors`, `live-usb`, `johno-macbookpro` (Darwin/macOS)
- **Home configurations**: Standalone home-manager configuration for user `johno`
### Directory Structure
- `machines/`: Machine-specific configurations with hardware-configuration.nix
- `roles/`: Modular system configurations (audio, bluetooth, desktop, users, etc.)
- `home/`: Home Manager configurations and user-specific modules
- `home/modules/`: User environment modules (emacs, i3+sway, plasma-manager, tmux)
- `packages/`: Custom package definitions
### Role-Based Configuration System
The repository uses a custom "roles" system where each role is a NixOS module with enable options:
- `roles.desktop`: Desktop environment with sub-options for X11, Wayland, KDE, gaming, SDDM
- `roles.audio`: Audio configuration
- `roles.bluetooth`: Bluetooth support
- `roles.users`: User account management
- `roles.virtualisation`: Virtualization setup
- `roles.kodi`: Kodi media center
- `roles.nvidia`: NVIDIA GPU configuration
- `roles.printing`: Printing support (CUPS)
- `roles.spotifyd`: Spotify daemon
- `roles.btrfs`: Btrfs filesystem configuration
- `roles.nfs-mounts`: NFS mount configuration
- `roles.darwin`: macOS-specific configurations
Example role usage in machine configuration:
```nix
roles = {
audio.enable = true;
desktop = {
enable = true;
gaming = true;
kde = true;
wayland = true;
};
users.enable = true;
};
```
### Home-Manager Role System
The repository also uses a modular home-manager role system for user-space configuration:
**Available Home Roles:**
- `home.roles.base`: Core CLI tools, git, ssh, bash, rbw (enabled everywhere)
- `home.roles.desktop`: GUI applications, Firefox, KDE services
- `home.roles.office`: LibreOffice, OpenSCAD (heavy packages)
- `home.roles.media`: VLC, Jellyfin, Moonlight (media consumption)
- `home.roles.development`: Custom packages, kubectl, development tools
- `home.roles.communication`: Element, Nextcloud Talk, Google cookie tools
- `home.roles.sync`: Syncthing service and tray (for file synchronization)
- `home.roles.kdeconnect`: KDE Connect for device integration
- `home.roles.gaming`: Gaming applications (future expansion)
**Role-Based Home Configurations:**
- `home-desktop.nix`: Full-featured desktop for development workstations
- `home-media-center.nix`: Living room media consumption and gaming setup (boxy)
- `home-laptop-compact.nix`: Essential tools only, excludes office/media for storage constraints (nix-book)
- `home-live-usb.nix`: Minimal setup for live environments, no persistent services
- `home-darwin-work.nix`: macOS work laptop configuration
**Machine-Specific Role Usage:**
- **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
## Common Commands
### Building and Switching Configurations
**NixOS (Linux):**
```bash
# Build and switch to a specific machine configuration
sudo nixos-rebuild switch --flake .#<hostname>
# Build without switching
nixos-rebuild build --flake .#<hostname>
# Build home-manager configuration only
home-manager switch --flake .#johno
```
**Darwin (macOS):**
```bash
# Build and switch to Darwin configuration
darwin-rebuild switch --flake .#johno-macbookpro
# Build without switching
darwin-rebuild build --flake .#johno-macbookpro
```
### Available Machine Configurations
- `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`
### Flake Operations
```bash
# Update flake inputs
nix flake update
# Check flake
nix flake check
# Show flake info
nix flake show
```
### Bootstrap New Machine
Use the provided bootstrap script:
```bash
sudo ./bootstrap.sh <hostname>
```
This script pulls from the remote git repository and applies the configuration.
### Build Live USB ISO
Use the provided script to build a bootable ISO:
```bash
./build-liveusb.sh
```
Creates an ISO suitable for Ventoy and other USB boot tools in `./result/iso/`.
## Development Workflow
### Adding New Machines
**NixOS:**
1. Create new directory in `machines/<hostname>/`
2. Add `configuration.nix` with role assignments
3. Include hardware-configuration.nix (generated by nixos-generate-config)
4. Add nixosConfiguration to flake.nix outputs
**Darwin (macOS):**
1. Create new directory in `machines/<hostname>/`
2. Add `configuration.nix` with Darwin role assignments
3. Add darwinConfiguration to flake.nix outputs
### Adding New Roles
1. Create directory in `roles/<role-name>/`
2. Create `default.nix` with module definition using mkEnableOption
3. Add role import to `roles/default.nix`
4. Configure role options in machine configurations
### Home Manager Modules
- Located in `home/modules/`
- Each module has its own `default.nix`
- Imported in main home configuration files
## Key Configuration Details
- **Experimental features**: nix-command and flakes are enabled
- **User**: Primary user is `johno` with trusted-user privileges
- **Locale**: en_US.UTF-8, America/Los_Angeles timezone
- **SSH**: OpenSSH enabled on all configurations
- **Garbage collection**: Automatic, deletes older than 10 days
- **Unfree packages**: Allowed globally
## Important Notes
- **Sudo access**: Claude Code does not have sudo access. Ask the user to run elevated commands like `sudo nixos-rebuild switch`

110
CLAUDE.md Normal file
View File

@@ -0,0 +1,110 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Repository Overview
This is a NixOS configuration repository using flakes, managing multiple machines and home-manager configurations. The repository follows a modular architecture with reusable "roles" that can be composed for different machines.
## Architecture
### 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`, `wixos` (WSL configuration)
- **Home configurations**: Standalone home-manager configuration for user `johno`
### Directory Structure
- `machines/`: Machine-specific configurations with hardware-configuration.nix
- `roles/`: Modular system configurations (audio, bluetooth, desktop, users, etc.)
- `home/`: Home Manager configurations and user-specific modules
- `home/modules/`: User environment modules (emacs, i3+sway, plasma-manager, tmux)
- `packages/`: Custom package definitions
### Role-Based Configuration System
The repository uses a custom "roles" system where each role is a NixOS module with enable options:
- `roles.desktop`: Desktop environment with sub-options for X11, Wayland, KDE, gaming, SDDM
- `roles.audio`: Audio configuration
- `roles.bluetooth`: Bluetooth support
- `roles.users`: User account management
- `roles.virtualisation`: Virtualization setup
- `roles.kodi`: Kodi media center
Example role usage in machine configuration:
```nix
roles = {
audio.enable = true;
desktop = {
enable = true;
gaming = true;
kde = true;
wayland = true;
};
users.enable = true;
};
```
## Common Commands
### Building and Switching Configurations
```bash
# Build and switch to a specific machine configuration
sudo nixos-rebuild switch --flake .#<hostname>
# Build without switching
nixos-rebuild build --flake .#<hostname>
# Build home-manager configuration only
home-manager switch --flake .#johno
```
### Available Machine Configurations
- `nix-book`: Uses `home/home-nix-book.nix`
- `boxy`: Gaming desktop with AMD GPU, uses `home/home.nix`
- `wixos`: WSL configuration, uses `home/home.nix`
### Flake Operations
```bash
# Update flake inputs
nix flake update
# Check flake
nix flake check
# Show flake info
nix flake show
```
### Bootstrap New Machine
Use the provided bootstrap script:
```bash
sudo ./bootstrap.sh <hostname>
```
This script pulls from the remote git repository and applies the configuration.
## Development Workflow
### Adding New Machines
1. Create new directory in `machines/<hostname>/`
2. Add `configuration.nix` with role assignments
3. Include hardware-configuration.nix (generated by nixos-generate-config)
4. Add nixosConfiguration to flake.nix outputs
### Adding New Roles
1. Create directory in `roles/<role-name>/`
2. Create `default.nix` with module definition using mkEnableOption
3. Add role import to `roles/default.nix`
4. Configure role options in machine configurations
### Home Manager Modules
- Located in `home/modules/`
- Each module has its own `default.nix`
- Imported in main home configuration files
## Key Configuration Details
- **Experimental features**: nix-command and flakes are enabled
- **User**: Primary user is `johno` with trusted-user privileges
- **Locale**: en_US.UTF-8, America/Los_Angeles timezone
- **SSH**: OpenSSH enabled on all configurations
- **Garbage collection**: Automatic, deletes older than 10 days
- **Unfree packages**: Allowed globally

View File

@@ -1,19 +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
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

250
flake.lock generated
View File

@@ -3,11 +3,11 @@
"flake-compat": { "flake-compat": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1765121682, "lastModified": 1747046372,
"narHash": "sha256-4VBOP18BFeiPkyhy9o4ssBNQEvfvv1kXkasAYd0+rrA=", "narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=",
"owner": "edolstra", "owner": "edolstra",
"repo": "flake-compat", "repo": "flake-compat",
"rev": "65f23138d8d09a92e30f1e5c87611b23ef451bf3", "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -23,11 +23,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1761423376, "lastModified": 1752428473,
"narHash": "sha256-pMy3cnUFfue4vz/y0jx71BfcPGxZf+hk/DtnzWvfU0c=", "narHash": "sha256-IsE7fdAYbRlZuc0H5FtPfhhuHvlxnDGoAxdlnjpVNCU=",
"ref": "refs/heads/main", "ref": "refs/heads/main",
"rev": "a1f695665771841a988afc965526cbf99160cd77", "rev": "1fad66b55144ab6beaecd900172a21ac3c34dc52",
"revCount": 11, "revCount": 10,
"type": "git", "type": "git",
"url": "https://git.johnogle.info/johno/google-cookie-retrieval.git" "url": "https://git.johnogle.info/johno/google-cookie-retrieval.git"
}, },
@@ -43,116 +43,30 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1766292113, "lastModified": 1752402455,
"narHash": "sha256-sWTtmkQujRpjWYCnZc8LWdDiCzrRlSBPrGovkZpLkBI=", "narHash": "sha256-mCHfZhQKdTj2JhCFcqfOfa3uKZbwUkPQbd0/zPnhOE8=",
"owner": "nix-community", "owner": "nix-community",
"repo": "home-manager", "repo": "home-manager",
"rev": "fdec8815a86db36f42fc9c8cb2931cd8485f5aed", "rev": "bf893ad4cbf46610dd1b620c974f824e266cd1df",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "nix-community", "owner": "nix-community",
"ref": "release-25.11",
"repo": "home-manager", "repo": "home-manager",
"type": "github" "type": "github"
} }
}, },
"home-manager-unstable": {
"inputs": {
"nixpkgs": [
"nixpkgs-unstable"
]
},
"locked": {
"lastModified": 1766282146,
"narHash": "sha256-0V/nKU93KdYGi+5LB/MVo355obBJw/2z9b2xS3bPJxY=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "61fcc9de76b88e55578eb5d79fc80f2b236df707",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "master",
"repo": "home-manager",
"type": "github"
}
},
"jovian": {
"inputs": {
"nix-github-actions": "nix-github-actions",
"nixpkgs": [
"nixpkgs-unstable"
]
},
"locked": {
"lastModified": 1766225187,
"narHash": "sha256-6hcaU8qtmixsaEUbjPiOFd5aJPZxAIBokl5d7dkab3k=",
"owner": "Jovian-Experiments",
"repo": "Jovian-NixOS",
"rev": "bb53a85db9210204a98f771f10f1f5b4e06ccb2d",
"type": "github"
},
"original": {
"owner": "Jovian-Experiments",
"repo": "Jovian-NixOS",
"type": "github"
}
},
"nix-darwin": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1765066094,
"narHash": "sha256-0YSU35gfRFJzx/lTGgOt6ubP8K6LeW0vaywzNNqxkl4=",
"owner": "nix-darwin",
"repo": "nix-darwin",
"rev": "688427b1aab9afb478ca07989dc754fa543e03d5",
"type": "github"
},
"original": {
"owner": "nix-darwin",
"ref": "nix-darwin-25.11",
"repo": "nix-darwin",
"type": "github"
}
},
"nix-github-actions": {
"inputs": {
"nixpkgs": [
"jovian",
"nixpkgs"
]
},
"locked": {
"lastModified": 1729697500,
"narHash": "sha256-VFTWrbzDlZyFHHb1AlKRiD/qqCJIripXKiCSFS8fAOY=",
"owner": "zhaofengli",
"repo": "nix-github-actions",
"rev": "e418aeb728b6aa5ca8c5c71974e7159c2df1d8cf",
"type": "github"
},
"original": {
"owner": "zhaofengli",
"ref": "matrix-name",
"repo": "nix-github-actions",
"type": "github"
}
},
"nixos-wsl": { "nixos-wsl": {
"inputs": { "inputs": {
"flake-compat": "flake-compat", "flake-compat": "flake-compat",
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs"
}, },
"locked": { "locked": {
"lastModified": 1765841014, "lastModified": 1752199438,
"narHash": "sha256-55V0AJ36V5Egh4kMhWtDh117eE3GOjwq5LhwxDn9eHg=", "narHash": "sha256-xSBMmGtq8K4Qv80TMqREmESCAsRLJRHAbFH2T/2Bf1Y=",
"owner": "nix-community", "owner": "nix-community",
"repo": "NixOS-WSL", "repo": "NixOS-WSL",
"rev": "be4af8042e7a61fa12fda58fe9a3b3babdefe17b", "rev": "d34d9412556d3a896e294534ccd25f53b6822e80",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -164,11 +78,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1765472234, "lastModified": 1751792365,
"narHash": "sha256-9VvC20PJPsleGMewwcWYKGzDIyjckEz8uWmT0vCDYK0=", "narHash": "sha256-J1kI6oAj25IG4EdVlg2hQz8NZTBNYvIS0l4wpr9KcUo=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "2fbfb1d73d239d2402a8fe03963e37aab15abe8b", "rev": "1fd8bada0b6117e6c7eb54aad5813023eed37ccb",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -178,34 +92,18 @@
"type": "github" "type": "github"
} }
}, },
"nixpkgs-unstable": {
"locked": {
"lastModified": 1766070988,
"narHash": "sha256-G/WVghka6c4bAzMhTwT2vjLccg/awmHkdKSd2JrycLc=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "c6245e83d836d0433170a16eb185cefe0572f8b8",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1766201043, "lastModified": 1751984180,
"narHash": "sha256-eplAP+rorKKd0gNjV3rA6+0WMzb1X1i16F5m5pASnjA=", "narHash": "sha256-LwWRsENAZJKUdD3SpLluwDmdXY9F45ZEgCb0X+xgOL0=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "b3aad468604d3e488d627c0b43984eb60e75e782", "rev": "9807714d6944a957c2e036f84b0ff8caf9930bc0",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "nixos", "owner": "nixos",
"ref": "nixos-25.11", "ref": "nixos-unstable",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }
@@ -220,11 +118,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1763909441, "lastModified": 1748196248,
"narHash": "sha256-56LwV51TX/FhgX+5LCG6akQ5KrOWuKgcJa+eUsRMxsc=", "narHash": "sha256-1iHjsH6/5UOerJEoZKE+Gx1BgAoge/YcnUsOA4wQ/BU=",
"owner": "nix-community", "owner": "nix-community",
"repo": "plasma-manager", "repo": "plasma-manager",
"rev": "b24ed4b272256dfc1cc2291f89a9821d5f9e14b4", "rev": "b7697abe89967839b273a863a3805345ea54ab56",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -233,113 +131,13 @@
"type": "github" "type": "github"
} }
}, },
"plasma-manager-unstable": {
"inputs": {
"home-manager": [
"home-manager-unstable"
],
"nixpkgs": [
"nixpkgs-unstable"
]
},
"locked": {
"lastModified": 1763909441,
"narHash": "sha256-56LwV51TX/FhgX+5LCG6akQ5KrOWuKgcJa+eUsRMxsc=",
"owner": "nix-community",
"repo": "plasma-manager",
"rev": "b24ed4b272256dfc1cc2291f89a9821d5f9e14b4",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "plasma-manager",
"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": { "root": {
"inputs": { "inputs": {
"google-cookie-retrieval": "google-cookie-retrieval", "google-cookie-retrieval": "google-cookie-retrieval",
"home-manager": "home-manager", "home-manager": "home-manager",
"home-manager-unstable": "home-manager-unstable",
"jovian": "jovian",
"nix-darwin": "nix-darwin",
"nixos-wsl": "nixos-wsl", "nixos-wsl": "nixos-wsl",
"nixpkgs": "nixpkgs_2", "nixpkgs": "nixpkgs_2",
"nixpkgs-unstable": "nixpkgs-unstable", "plasma-manager": "plasma-manager"
"plasma-manager": "plasma-manager",
"plasma-manager-unstable": "plasma-manager-unstable",
"pyproject-build-systems": "pyproject-build-systems",
"pyproject-nix": "pyproject-nix",
"uv2nix": "uv2nix"
}
},
"uv2nix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"pyproject-nix": [
"pyproject-nix"
]
},
"locked": {
"lastModified": 1766021660,
"narHash": "sha256-UUfz7qWB1Rb2KjGVCimt//Jncv3TgJwffPqbzqpkmgY=",
"owner": "pyproject-nix",
"repo": "uv2nix",
"rev": "19fa99be3409f55ec05e823c66c9769df7a8dd17",
"type": "github"
},
"original": {
"owner": "pyproject-nix",
"repo": "uv2nix",
"type": "github"
} }
} }
}, },

209
flake.nix
View File

@@ -2,165 +2,48 @@
description = "A very basic flake"; description = "A very basic flake";
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11"; nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
nixpkgs-unstable.url = "github:nixos/nixpkgs/nixos-unstable";
nixos-wsl.url = "github:nix-community/NixOS-WSL/main"; 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";
};
home-manager = { home-manager = {
url = "github:nix-community/home-manager/release-25.11"; url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
home-manager-unstable = {
url = "github:nix-community/home-manager/master";
inputs.nixpkgs.follows = "nixpkgs-unstable";
};
plasma-manager = { plasma-manager = {
url = "github:nix-community/plasma-manager"; url = "github:nix-community/plasma-manager";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
inputs.home-manager.follows = "home-manager"; inputs.home-manager.follows = "home-manager";
}; };
plasma-manager-unstable = {
url = "github:nix-community/plasma-manager";
inputs.nixpkgs.follows = "nixpkgs-unstable";
inputs.home-manager.follows = "home-manager-unstable";
};
google-cookie-retrieval = { google-cookie-retrieval = {
url = "git+https://git.johnogle.info/johno/google-cookie-retrieval.git"; url = "git+https://git.johnogle.info/johno/google-cookie-retrieval.git";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
jovian = {
url = "github:Jovian-Experiments/Jovian-NixOS";
inputs.nixpkgs.follows = "nixpkgs-unstable";
};
pyproject-nix = {
url = "github:pyproject-nix/pyproject.nix";
inputs.nixpkgs.follows = "nixpkgs";
};
uv2nix = {
url = "github:pyproject-nix/uv2nix";
inputs.pyproject-nix.follows = "pyproject-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
pyproject-build-systems = {
url = "github:pyproject-nix/build-system-pkgs";
inputs.pyproject-nix.follows = "pyproject-nix";
inputs.uv2nix.follows = "uv2nix";
inputs.nixpkgs.follows = "nixpkgs";
};
}; };
outputs = { self, nixpkgs, nixpkgs-unstable, nixos-wsl, pyproject-nix, uv2nix, pyproject-build-systems, ... } @ inputs: let outputs = { self, nixpkgs, nixos-wsl, ... } @ inputs: let
nixosModules = [ baseModules = [
./roles ./roles
] ++ [
inputs.home-manager.nixosModules.home-manager 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.useGlobalPkgs = true;
home-manager.useUserPackages = true; home-manager.useUserPackages = true;
home-manager.sharedModules = [ home-manager.sharedModules = [
inputs.plasma-manager.homeModules.plasma-manager inputs.plasma-manager.homeManagerModules.plasma-manager
]; ];
home-manager.extraSpecialArgs = { home-manager.extraSpecialArgs = {
globalInputs = inputs; 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 { in {
nixosConfigurations.nix-book = nixpkgs.lib.nixosSystem rec { nixosConfigurations.nix-book = nixpkgs.lib.nixosSystem rec {
system = "x86_64-linux"; system = "x86_64-linux";
modules = nixosModules ++ [ modules = baseModules ++ [
./machines/nix-book/configuration.nix ./machines/nix-book/configuration.nix
{ {
home-manager.users.johno = { home-manager.users.johno = import ./home/home-nix-book.nix;
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; }; home-manager.extraSpecialArgs = { inherit system; };
} }
]; ];
@@ -168,11 +51,11 @@
nixosConfigurations.boxy = nixpkgs.lib.nixosSystem rec { nixosConfigurations.boxy = nixpkgs.lib.nixosSystem rec {
system = "x86_64-linux"; system = "x86_64-linux";
modules = nixosModules ++ [ modules = baseModules ++ [
./machines/boxy/configuration.nix ./machines/boxy/configuration.nix
inputs.home-manager.nixosModules.home-manager inputs.home-manager.nixosModules.home-manager
{ {
home-manager.users.johno = import ./home/home-media-center.nix; home-manager.users.johno = import ./home/home.nix;
home-manager.extraSpecialArgs = { inherit system; }; home-manager.extraSpecialArgs = { inherit system; };
} }
]; ];
@@ -180,79 +63,27 @@
nixosConfigurations.wixos = nixpkgs.lib.nixosSystem rec { nixosConfigurations.wixos = nixpkgs.lib.nixosSystem rec {
system = "x86_64-linux"; system = "x86_64-linux";
modules = nixosModules ++ [ modules = baseModules ++ [
nixos-wsl.nixosModules.default nixos-wsl.nixosModules.default
./machines/wixos/configuration.nix ./machines/wixos/configuration.nix
inputs.home-manager.nixosModules.home-manager inputs.home-manager.nixosModules.home-manager
{ {
home-manager.users.johno = import ./home/home-desktop.nix; home-manager.users.johno = import ./home/home.nix;
home-manager.extraSpecialArgs = { inherit system; }; home-manager.extraSpecialArgs = { inherit system; };
} }
]; ];
}; };
nixosConfigurations.zix790prors = nixpkgs.lib.nixosSystem rec { homeConfigurations."johno" = inputs.home-manager.lib.homeManagerConfiguration {
system = "x86_64-linux"; pkgs = inputs.nixpkgs.legacyPackages."x86_64-linux";
modules = nixosModules ++ [ modules = [
./machines/zix790prors/configuration.nix inputs.plasma-manager.homeManagerModules.plasma-manager
inputs.home-manager.nixosModules.home-manager ./home/home.nix
{
home-manager.users.johno = import ./home/home-desktop.nix;
home-manager.extraSpecialArgs = { inherit system; };
}
]; ];
extraSpecialArgs = {
system = "x86_64-linux";
globalInputs = inputs;
};
}; };
# 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

@@ -1,123 +0,0 @@
{ config, lib, pkgs, globalInputs, system, ... }:
{
# Home Manager configuration for Darwin work laptop
# Corporate-friendly setup with essential development tools
home.username = lib.mkForce "johno";
home.homeDirectory = lib.mkForce "/Users/johno";
home.stateVersion = "24.05";
# System packages
home.packages = with pkgs; [
google-cloud-sdk
];
# Note: ghostty installed via Homebrew (managed outside of nix)
# Override Darwin-incompatible settings from base role
programs.rbw.settings.pinentry = lib.mkForce pkgs.pinentry_mac;
# Disable Home Manager from managing shell RC files
# topsoil/compost will manage these files instead
programs.bash.enable = lib.mkForce false;
programs.zsh.enable = lib.mkForce false;
# Create a local nix integration file that topsoil-managed configs can source
home.file.".nix-integration.sh" = {
text = ''
# Source Home Manager session variables (nix paths, environment, etc.)
if [ -e /etc/profiles/per-user/johno/etc/profile.d/hm-session-vars.sh ]; then
. /etc/profiles/per-user/johno/etc/profile.d/hm-session-vars.sh
fi
# Setup bash completions from nix profiles
if [[ ! -v BASH_COMPLETION_VERSINFO ]] && [ -n "$NIX_PROFILES" ]; then
for profile in $NIX_PROFILES; do
if [ -f "$profile/etc/profile.d/bash_completion.sh" ]; then
. "$profile/etc/profile.d/bash_completion.sh"
break
fi
done
fi
# command-not-found handler
command_not_found_handle() {
local p=/nix/var/nix/profiles/per-user/root/channels/nixos/programs.sqlite
if [ -n "$NIX_PROFILES" ]; then
for profile in $NIX_PROFILES; do
if [ -x "$profile/bin/command-not-found" ] && [ -f "$p" ]; then
"$profile/bin/command-not-found" "$@"
return $?
fi
done
fi
echo "$1: command not found" >&2
return 127
}
'';
};
home.file.".nix-integration.zsh" = {
text = ''
# Source Home Manager session variables (nix paths, environment, etc.)
if [ -e /etc/profiles/per-user/johno/etc/profile.d/hm-session-vars.sh ]; then
. /etc/profiles/per-user/johno/etc/profile.d/hm-session-vars.sh
fi
# Setup zsh completions from nix profiles
typeset -U path cdpath fpath manpath
for profile in ''${(z)NIX_PROFILES}; do
fpath+=($profile/share/zsh/site-functions $profile/share/zsh/$ZSH_VERSION/functions $profile/share/zsh/vendor-completions)
done
autoload -U compinit && compinit
# command-not-found handler
command_not_found_handler() {
local p=/nix/var/nix/profiles/per-user/root/channels/nixos/programs.sqlite
if [ -n "$NIX_PROFILES" ]; then
for profile in ''${(z)NIX_PROFILES}; do
if [ -x "$profile/bin/command-not-found" ] && [ -f "$p" ]; then
"$profile/bin/command-not-found" "$@"
return $?
fi
done
fi
echo "$1: command not found" >&2
return 127
}
'';
};
# Keep SSH and Git disabled to avoid conflicts with work environment
programs.ssh.enable = lib.mkForce false;
programs.git.enable = lib.mkForce false;
programs.rbw.enable = lib.mkForce false;
home.shell.enableShellIntegration = true;
home.roles = {
base.enable = true;
development = {
enable = true;
allowArbitraryClaudeCodeModelSelection = true;
};
tmux.enable = true;
emacs.enable = true;
aerospace = {
enable = true;
leader = "cmd";
ctrlShortcuts.enable = true;
sketchybar.enable = true;
# Optional: Add per-machine userSettings overrides
# userSettings = {
# mode.main.binding."${leader}-custom" = "custom-command";
# };
};
};
imports = [
./roles
./roles/base-darwin
];
}

View File

@@ -1,36 +0,0 @@
{ pkgs, globalInputs, system, ... }:
{
# Home Manager configuration for full desktop experience
home.username = "johno";
home.homeDirectory = "/home/johno";
home.stateVersion = "24.05";
# Enable all desktop roles for full-featured experience
home.roles = {
"3d-printing".enable = true;
base.enable = true;
desktop.enable = true;
emacs.enable = true;
email.enable = true;
i3_sway.enable = true;
office.enable = true;
media.enable = true;
development.enable = true;
communication.enable = true;
sync.enable = true;
kdeconnect.enable = true;
kubectl.enable = true;
tmux.enable = true;
plasma-manager.enable = true;
};
targets.genericLinux.enable = true;
home.sessionVariables = {};
home.sessionPath = [];
imports = [
./roles
./roles/base-linux
];
}

View File

@@ -1,44 +0,0 @@
{ config, lib, pkgs, globalInputs, system, ... }:
{
# Home Manager configuration for compact laptop setups
# Optimized for space-constrained environments
home.username = "johno";
home.homeDirectory = "/home/johno";
home.stateVersion = "24.05";
# Enable essential roles only (exclude heavy office/media packages)
home.roles = {
base.enable = true;
desktop.enable = true;
development.enable = true;
communication.enable = true;
email.enable = true;
kdeconnect.enable = true;
media.enable = true;
sync.enable = true;
kubectl.enable = true;
tmux.enable = true;
plasma-manager.enable = true;
emacs.enable = true;
i3_sway.enable = true;
# Launcher wrappers for excluded/optional packages
launchers = {
enable = true;
packages = [
"libreoffice"
];
};
};
targets.genericLinux.enable = true;
home.sessionVariables = {};
home.sessionPath = [];
imports = [
./roles
./roles/base-linux
];
}

View File

@@ -1,37 +0,0 @@
{ pkgs, globalInputs, system, ... }:
{
# Home Manager configuration for live USB environments
# Minimal setup without persistent services
home.username = "nixos";
home.homeDirectory = "/home/nixos";
home.stateVersion = "24.05";
# Enable minimal roles only (no sync or kdeconnect for live environment)
home.roles = {
base.enable = true;
desktop.enable = true;
tmux.enable = true;
plasma-manager.enable = true;
emacs.enable = true;
i3_sway.enable = true;
# development.enable = false; # Not needed for live USB
# communication.enable = false; # Not needed for live USB
# office.enable = false; # Not needed for live USB
# media.enable = false; # Not needed for live USB
# sync.enable = false; # No persistent sync on live USB
# kdeconnect.enable = false; # No device integration on live USB
};
targets.genericLinux.enable = true;
home.sessionVariables = {};
home.sessionPath = [];
imports = [
./roles
./roles/base-linux
];
# Live USB specific overrides can go here if needed
}

View File

@@ -1,37 +0,0 @@
{ pkgs, globalInputs, system, ... }:
{
# Home Manager configuration for media center setups
# Optimized for living room media consumption and gaming
home.username = "johno";
home.homeDirectory = "/home/johno";
home.stateVersion = "24.05";
# Enable media center focused roles
home.roles = {
base.enable = true;
desktop.enable = true;
media.enable = true;
communication.enable = true;
kdeconnect.enable = true;
development.enable = true;
tmux.enable = true;
plasma-manager.enable = true;
emacs.enable = true;
i3_sway.enable = true;
# office.enable = false; # Not needed for media center
# sync.enable = false; # Shared machine, no personal file sync
};
targets.genericLinux.enable = true;
home.sessionVariables = {};
home.sessionPath = [];
imports = [
./roles
./roles/base-linux
];
# Media center specific overrides can go here if needed
}

11
home/home-nix-book.nix Normal file
View File

@@ -0,0 +1,11 @@
{ config, lib, pkgs, ... }:
{
imports = [
./home.nix
];
home.i3_sway.extraSwayConfig = {
output.eDP-1.scale = "1.75";
};
}

204
home/home.nix Normal file
View File

@@ -0,0 +1,204 @@
{ pkgs, customPkgs, globalInputs, system, ... }:
let
customPkgs = pkgs.callPackage ../packages {};
in
{
# Home Manager needs a bit of information about you and the paths it should
# manage.
home.username = "johno";
home.homeDirectory = "/home/johno";
# This value determines the Home Manager release that your configuration is
# compatible with. This helps avoid breakage when a new Home Manager release
# introduces backwards incompatible changes.
#
# You should not change this value, even if you update Home Manager. If you do
# want to update the value, then make sure to first check the Home Manager
# release notes.
home.stateVersion = "24.05"; # Please read the comment before changing.
# The home.packages option allows you to install Nix packages into your
# environment.
home.packages = [
# # Adds the 'hello' command to your environment. It prints a friendly
# # "Hello, world!" when run.
# pkgs.hello
# # It is sometimes useful to fine-tune packages, for example, by applying
# # overrides. You can do that directly here, just don't forget the
# # parentheses. Maybe you want to install Nerd Fonts with a limited number of
# # fonts?
# (pkgs.nerdfonts.override { fonts = [ "FantasqueSansMono" ]; })
# # You can also create simple shell scripts directly inside your
# # configuration. For example, this adds a command 'my-hello' to your
# # environment:
# (pkgs.writeShellScriptBin "my-hello" ''
# echo "Hello, ${config.home.username}!"
# '')
pkgs.bitwarden
pkgs.claude-code
pkgs.codex
pkgs.dunst
pkgs.element-desktop
pkgs.fd
#pkgs.fluffychat # security vulnerability in current version
pkgs.goose-cli
pkgs.gzip
pkgs.htop
pkgs.jellyfin-media-player
pkgs.keepassxc
pkgs.killall
pkgs.kitty
pkgs.less
pkgs.moonlight-qt
pkgs.ncdu
pkgs.nextcloud-talk-desktop
pkgs.openscad-unstable
pkgs.pandoc
#pkgs.pinentry-qt
#pkgs.pytest
pkgs.shellcheck
pkgs.solaar # Logitech management software
(pkgs.snapcast.override { pulseaudioSupport = true; })
pkgs.tmux
pkgs.waybar
pkgs.wofi
pkgs.vlc
## Kubernetes cluster management
pkgs.kubectl
pkgs.kubernetes-helm
globalInputs.google-cookie-retrieval.packages.${system}.default
];
# Home Manager is pretty good at managing dotfiles. The primary way to manage
# plain files is through 'home.file'.
home.file = {
# # Building this configuration will create a copy of 'dotfiles/screenrc' in
# # the Nix store. Activating the configuration will then make '~/.screenrc' a
# # symlink to the Nix store copy.
# ".screenrc".source = dotfiles/screenrc;
# # You can also set the file content immediately.
# ".gradle/gradle.properties".text = ''
# org.gradle.console=verbose
# org.gradle.daemon.idletimeout=3600000
# '';
};
targets.genericLinux.enable = true;
# Home Manager can also manage your environment variables through
# 'home.sessionVariables'. These will be explicitly sourced when using a
# shell provided by Home Manager. If you don't want to manage your shell
# through Home Manager then you have to manually source 'hm-session-vars.sh'
# located at either
#
# ~/.nix-profile/etc/profile.d/hm-session-vars.sh
#
# or
#
# ~/.local/state/nix/profiles/profile/etc/profile.d/hm-session-vars.sh
#
# or
#
# /etc/profiles/per-user/johno/etc/profile.d/hm-session-vars.sh
#
home.sessionVariables = {
};
home.sessionPath = [
];
imports = [
./modules/emacs
./modules/i3+sway
./modules/plasma-manager
./modules/tmux
];
programs.bash = {
enable = true;
initExtra = ''
codex() {
local key
key="$(rbw get openai-api-key-codex)"
OPENAI_API_KEY="$key" command codex "$@"
}
'';
};
# Let Home Manager install and manage itself.
programs.home-manager.enable = true;
programs.command-not-found.enable = true;
programs.firefox = {
enable = true;
};
programs.git = {
enable = true;
userName = "John Ogle";
userEmail = "john@ogle.fyi";
extraConfig = {
safe.directory = "/etc/nixos";
};
};
programs.jq.enable = true;
programs.k9s.enable = true;
programs.neovim = {
enable = true;
viAlias = true;
vimAlias = true;
};
programs.rbw = {
enable = true;
settings = {
email = "john@johnogle.info";
base_url = "https://bitwarden.johnogle.info";
pinentry = pkgs.pinentry-qt;
};
};
programs.spotify-player.enable = true;
programs.ssh = {
enable = true;
addKeysToAgent = "yes";
matchBlocks = {
"nucdeb1" = {
hostname = "nucdeb1.oglehome";
user = "root";
};
};
};
services.kdeconnect = {
enable = true;
indicator = true;
package = pkgs.kdePackages.kdeconnect-kde;
};
services.gnome-keyring = {
enable = true;
};
services.syncthing = {
enable = true;
tray = {
enable = true;
command = "syncthingtray --wait";
};
};
xdg.enable = true;
}

View File

@@ -0,0 +1,47 @@
{ config, lib, pkgs, ... }:
with lib;
let
doomEmacs = pkgs.fetchFromGitHub {
owner = "doomemacs";
repo = "doomemacs";
rev = "8406c1ff22b95bd0f816de4a0223fa3ce3c82568";
sha256 = "sha256-rOkgOdmLESVAbOeEM9nJTzxyI+akdk48Ed2VlktOy3Q=";
};
in
{
config = {
home.packages = [
pkgs.emacs
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;
home.file."${config.xdg.configHome}/emacs".source = doomEmacs;
home.sessionPath = [
"${config.xdg.configHome}/emacs/bin"
];
home.sessionVariables = {
DOOMDIR = "${config.xdg.configHome}/doom";
DOOMLOCALDIR = "${config.xdg.dataHome}/doom";
};
home.file."${config.xdg.configHome}/doom".source = ./doom;
};
}

View File

@@ -30,20 +30,10 @@
;; wasn't installed correctly. Font issues are rarely Doom issues! ;; wasn't installed correctly. Font issues are rarely Doom issues!
(setq doom-font (font-spec :family "Fira Code")) (setq doom-font (font-spec :family "Fira Code"))
;; Auto-install nerd-icons fonts if they're missing
(defun my/ensure-nerd-icons-fonts ()
"Check if nerd-icons fonts are installed and install them if missing."
(when (display-graphic-p)
(unless (find-font (font-spec :name "Symbols Nerd Font Mono"))
(when (fboundp 'nerd-icons-install-fonts)
(nerd-icons-install-fonts t)))))
(add-hook 'doom-init-ui-hook #'my/ensure-nerd-icons-fonts)
;; There are two ways to load a theme. Both assume the theme is installed and ;; There are two ways to load a theme. Both assume the theme is installed and
;; available. You can either set `doom-theme' or manually load a theme with the ;; available. You can either set `doom-theme' or manually load a theme with the
;; `load-theme' function. This is the default: ;; `load-theme' function. This is the default:
(setq doom-theme 'doom-tokyo-night) (setq doom-theme 'doom-one)
;; This determines the style of line numbers in effect. If set to `nil', line ;; This determines the style of line numbers in effect. If set to `nil', line
;; numbers are disabled. For relative line numbers, set this to `relative'. ;; numbers are disabled. For relative line numbers, set this to `relative'.
@@ -54,7 +44,6 @@
(setq org-directory "~/org/") (setq org-directory "~/org/")
(after! org (after! org
(setq org-agenda-span 'week (setq org-agenda-span 'week
org-agenda-start-with-log-mode t
my-agenda-dirs '("projects" "roam") my-agenda-dirs '("projects" "roam")
org-agenda-files (cons org-directory (mapcan (lambda (x) (directory-files-recursively org-agenda-files (cons org-directory (mapcan (lambda (x) (directory-files-recursively
(expand-file-name x org-directory) (expand-file-name x org-directory)
@@ -73,13 +62,6 @@
'(("t" "Todo" entry (file+headline "~/org/todo.org" "Inbox") '(("t" "Todo" entry (file+headline "~/org/todo.org" "Inbox")
"* TODO %? \n %i \n%a" :prepend t)))) "* TODO %? \n %i \n%a" :prepend t))))
(map! :after org-agenda
:map org-agenda-mode-map
:localleader
(:prefix ("v" . "view")
"d" #'org-agenda-day-view
"w" #'org-agenda-week-view))
;; (use-package! org-caldav ;; (use-package! org-caldav
;; :defer t ;; :defer t
;; :config ;; :config
@@ -96,94 +78,10 @@
(output (shell-command-to-string cmd))) (output (shell-command-to-string cmd)))
(string-trim output))) (string-trim output)))
(after! gptel (use-package! gptel
:config
(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
gptel-include-reasoning 'ignore
gptel-model "qwen3:30b")
;; Set default backend to be Ollama-Local
(setq! gptel-backend
(gptel-make-ollama "Ollama-Local"
:host "localhost:11434"
:stream t
:models '(deepseek-r1 deepseek-r1-fullctx qwen3:30b qwen3:4b llama3.1 qwen2.5-coder mistral-nemo gpt-oss)))
;; Define custom tools
(gptel-make-tool
:name "run_shell_command"
:description "Execute shell commands and return output. Use this to run system commands, check file contents, or perform system operations."
:function (lambda (command)
(condition-case err
(shell-command-to-string command)
(error (format "Error running command: %s" (error-message-string err)))))
:args (list '(:name "command" :type "string" :description "Shell command to execute")))
(gptel-make-tool
:name "read_file"
:description "Read the contents of a file and return as text"
:function (lambda (filepath)
(condition-case err
(with-temp-buffer
(insert-file-contents (expand-file-name filepath))
(buffer-string))
(error (format "Error reading file %s: %s" filepath (error-message-string err)))))
:args (list '(:name "filepath" :type "string" :description "Path to the file to read")))
(gptel-make-tool
:name "list_directory"
:description "List contents of a directory"
:function (lambda (dirpath)
(condition-case err
(mapconcat 'identity
(directory-files (expand-file-name dirpath) nil "^[^.]")
"\n")
(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! claude-code-ide
:defer t :defer t
:config :config
(claude-code-ide-emacs-tools-setup) (setq! gptel-api-key (my/get-rbw-password "openai-api-key-chatgpt-el")))
(map! :leader
(:prefix ("o" . "open")
:desc "Claude Code IDE" "c" #'claude-code-ide-menu)))
(after! gptel
(require 'gptel-tool-library)
(setq gptel-tool-library-use-maybe-safe t
gptel-tool-library-use-unsafe t)
(dolist (module '("bbdb" "buffer" "elisp" "emacs" "gnus" "os" "search-and-replace" "url"))
(gptel-tool-library-load-module module)))
;; Notmuch email configuration
(after! notmuch
(setq notmuch-search-oldest-first nil
notmuch-show-logo nil
notmuch-fcc-dirs "proton/Sent"
;; User identity
user-mail-address "john@ogle.fyi"
user-full-name "John Ogle"
;; Sending mail via msmtp
message-send-mail-function 'message-send-mail-with-sendmail
sendmail-program (executable-find "msmtp")
message-sendmail-envelope-from 'header
mail-envelope-from 'header
mail-specify-envelope-from t
;; Saved searches for quick access
notmuch-saved-searches
'((:name "inbox" :query "tag:inbox" :key "i")
(:name "unread" :query "tag:unread" :key "u")
(:name "flagged" :query "tag:flagged" :key "f")
(:name "sent" :query "tag:sent" :key "t")
(:name "drafts" :query "tag:draft" :key "d")
(:name "all" :query "*" :key "a"))))
;; Whenever you reconfigure a package, make sure to wrap your config in an ;; 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. ;; `after!' block, otherwise Doom's defaults may override your settings. E.g.

View File

@@ -33,7 +33,7 @@
doom ; what makes DOOM look the way it does doom ; what makes DOOM look the way it does
doom-dashboard ; a nifty splash screen for Emacs doom-dashboard ; a nifty splash screen for Emacs
;;doom-quit ; DOOM quit-message prompts when you quit Emacs ;;doom-quit ; DOOM quit-message prompts when you quit Emacs
;;(emoji +unicode) ; 🙂 (emoji +unicode) ; 🙂
hl-todo ; highlight TODO/FIXME/NOTE/DEPRECATED/HACK/REVIEW hl-todo ; highlight TODO/FIXME/NOTE/DEPRECATED/HACK/REVIEW
;;indent-guides ; highlighted indent columns ;;indent-guides ; highlighted indent columns
;;ligatures ; ligatures and symbols to make your code pretty again ;;ligatures ; ligatures and symbols to make your code pretty again
@@ -45,7 +45,7 @@
(popup +defaults) ; tame sudden yet inevitable temporary windows (popup +defaults) ; tame sudden yet inevitable temporary windows
;;tabs ; a tab bar for Emacs ;;tabs ; a tab bar for Emacs
;;treemacs ; a project drawer, like neotree but cooler ;;treemacs ; a project drawer, like neotree but cooler
unicode ; extended unicode support for various languages ;;unicode ; extended unicode support for various languages
(vc-gutter +pretty) ; vcs diff in the fringe (vc-gutter +pretty) ; vcs diff in the fringe
vi-tilde-fringe ; fringe tildes to mark beyond EOB vi-tilde-fringe ; fringe tildes to mark beyond EOB
;;window-select ; visually switch windows ;;window-select ; visually switch windows
@@ -59,7 +59,7 @@
;;(format +onsave) ; automated prettiness ;;(format +onsave) ; automated prettiness
;;god ; run Emacs commands without modifier keys ;;god ; run Emacs commands without modifier keys
;;lispy ; vim for lisp, for people who don't like vim ;;lispy ; vim for lisp, for people who don't like vim
multiple-cursors ; editing in many places at once ;;multiple-cursors ; editing in many places at once
;;objed ; text object editing for the innocent ;;objed ; text object editing for the innocent
;;parinfer ; turn lisp into python, sort of ;;parinfer ; turn lisp into python, sort of
;;rotate-text ; cycle region at point between text candidates ;;rotate-text ; cycle region at point between text candidates
@@ -77,7 +77,7 @@
;;eshell ; the elisp shell that works everywhere ;;eshell ; the elisp shell that works everywhere
;;shell ; simple shell REPL for Emacs ;;shell ; simple shell REPL for Emacs
;;term ; basic terminal emulator for Emacs ;;term ; basic terminal emulator for Emacs
vterm ; the best terminal emulation in Emacs ;;vterm ; the best terminal emulation in Emacs
:checkers :checkers
syntax ; tasing you for every semicolon you forget syntax ; tasing you for every semicolon you forget
@@ -94,7 +94,6 @@
;;editorconfig ; let someone else argue about tabs vs spaces ;;editorconfig ; let someone else argue about tabs vs spaces
;;ein ; tame Jupyter notebooks with emacs ;;ein ; tame Jupyter notebooks with emacs
(eval +overlay) ; run code, run (also, repls) (eval +overlay) ; run code, run (also, repls)
llm ; When I said you needed friends, I didn't mean...
lookup ; navigate your code and its documentation lookup ; navigate your code and its documentation
lsp ; M-x vscode lsp ; M-x vscode
magit ; a git porcelain for Emacs magit ; a git porcelain for Emacs
@@ -177,7 +176,7 @@
:email :email
;;(mu4e +org +gmail) ;;(mu4e +org +gmail)
notmuch ;;notmuch
;;(wanderlust +gmail) ;;(wanderlust +gmail)
:app :app

View File

@@ -52,10 +52,3 @@
;; (package! org-caldav) ;; (package! org-caldav)
(package! gptel :recipe (:nonrecursive t)) (package! gptel :recipe (:nonrecursive t))
(package! claude-code-ide
:recipe (:host github :repo "manzaltu/claude-code-ide.el"))
(package! gptel-tool-library
:recipe (:host github :repo "aard-fi/gptel-tool-library"
:files ("*.el")))

View File

@@ -0,0 +1,146 @@
{ config, lib, ... }:
with lib;
let
cfg = config.home.i3_sway;
i3_cfg = config.xsession.windowManager.i3.config;
shared_config = recursiveUpdate rec {
modifier = "Mod4";
terminal = "kitty";
defaultWorkspace = "workspace number 1";
keybindings = {
"${shared_config.modifier}+Return" = "exec ${terminal}";
"${shared_config.modifier}+Shift+q" = "kill";
"${shared_config.modifier}+d" = "exec ${i3_cfg.menu}";
"${shared_config.modifier}+h" = "focus left";
"${shared_config.modifier}+j" = "focus down";
"${shared_config.modifier}+k" = "focus up";
"${shared_config.modifier}+l" = "focus right";
"${shared_config.modifier}+Shift+h" = "move left";
"${shared_config.modifier}+Shift+j" = "move down";
"${shared_config.modifier}+Shift+k" = "move up";
"${shared_config.modifier}+Shift+l" = "move right";
"${shared_config.modifier}+Left" = "focus left";
"${shared_config.modifier}+Down" = "focus down";
"${shared_config.modifier}+Up" = "focus up";
"${shared_config.modifier}+Right" = "focus right";
"${shared_config.modifier}+Shift+Left" = "move left";
"${shared_config.modifier}+Shift+Down" = "move down";
"${shared_config.modifier}+Shift+Up" = "move up";
"${shared_config.modifier}+Shift+Right" = "move right";
#"${shared_config.modifier}+h" = "split h";
"${shared_config.modifier}+v" = "split v";
"${shared_config.modifier}+f" = "fullscreen toggle";
"${shared_config.modifier}+s" = "layout stacking";
"${shared_config.modifier}+w" = "layout tabbed";
"${shared_config.modifier}+e" = "layout toggle split";
"${shared_config.modifier}+Shift+space" = "floating toggle";
"${shared_config.modifier}+space" = "focus mode_toggle";
"${shared_config.modifier}+a" = "focus parent";
"${shared_config.modifier}+Shift+minus" = "move scratchpad";
"${shared_config.modifier}+minus" = "scratchpad show";
"${shared_config.modifier}+1" = "workspace number 1";
"${shared_config.modifier}+2" = "workspace number 2";
"${shared_config.modifier}+3" = "workspace number 3";
"${shared_config.modifier}+4" = "workspace number 4";
"${shared_config.modifier}+5" = "workspace number 5";
"${shared_config.modifier}+6" = "workspace number 6";
"${shared_config.modifier}+7" = "workspace number 7";
"${shared_config.modifier}+8" = "workspace number 8";
"${shared_config.modifier}+9" = "workspace number 9";
"${shared_config.modifier}+0" = "workspace number 10";
"${shared_config.modifier}+Shift+1" =
"move container to workspace number 1";
"${shared_config.modifier}+Shift+2" =
"move container to workspace number 2";
"${shared_config.modifier}+Shift+3" =
"move container to workspace number 3";
"${shared_config.modifier}+Shift+4" =
"move container to workspace number 4";
"${shared_config.modifier}+Shift+5" =
"move container to workspace number 5";
"${shared_config.modifier}+Shift+6" =
"move container to workspace number 6";
"${shared_config.modifier}+Shift+7" =
"move container to workspace number 7";
"${shared_config.modifier}+Shift+8" =
"move container to workspace number 8";
"${shared_config.modifier}+Shift+9" =
"move container to workspace number 9";
"${shared_config.modifier}+Shift+0" =
"move container to workspace number 10";
"${shared_config.modifier}+Shift+c" = "reload";
"${shared_config.modifier}+Shift+r" = "restart";
"${shared_config.modifier}+r" = "mode resize";
"XF86MonBrightnessUp" = "exec brightnessctl s +5%";
"XF86MonBrightnessDown" = "exec brightnessctl s 5%-";
};
} cfg.extraSharedConfig;
in {
options.home.i3_sway = {
extraSharedConfig = mkOption {
default = {};
};
extraI3Config = mkOption {
default = {};
};
extraSwayConfig = mkOption {
default = {};
};
};
config = {
xsession.windowManager.i3 = let
base_i3_config = recursiveUpdate shared_config {
keybindings = {
"${shared_config.modifier}+Shift+e" =
"exec i3-nagbar -t warning -m 'Do you want to exit i3?' -b 'Yes' 'i3-msg exit'";
};
};
in {
enable = true;
config = recursiveUpdate base_i3_config cfg.extraI3Config;
};
wayland.windowManager.sway = let
base_sway_config = recursiveUpdate shared_config {
keybindings = {
"${shared_config.modifier}+Shift+e" =
"exec swaynag -t warning -m 'You pressed the exit shortcut. Do you really want to exit sway? This will end your Wayland session.' -b 'Yes, exit sway' 'swaymsg exit'";
};
input = {
"type:keyboard" = {
xkb_options = "caps:escape";
};
"type:touchpad" = {
tap = "enabled";
tap_button_map = "lrm";
drag = "enabled";
natural_scroll = "disabled";
dwt = "enabled";
};
};
};
in {
enable = true;
config = recursiveUpdate base_sway_config cfg.extraSwayConfig;
};
};
}

View File

@@ -0,0 +1,127 @@
{ config, lib, pkgs, ... }:
# The current KDE config can be output with the command:
# nix run github:nix-community/plasma-manager
#
# Plasma-manager options documentation
# https://nix-community.github.io/plasma-manager/options.xhtml
#
# TODO: (ambitious) Add Kmail support to plasma-manager
{
programs.plasma = {
enable = true;
overrideConfig = true;
hotkeys.commands."launch-konsole" = {
name = "Launch Konsole";
key = "Meta+Return";
command = "konsole";
};
shortcuts = {
kmix = {
"decrease_microphone_volume" = "Microphone Volume Down";
"decrease_volume" = "Volume Down";
"decrease_volume_small" = "Shift+Volume Down";
"increase_microphone_volume" = "Microphone Volume Up";
"increase_volume" = "Volume Up";
"increase_volume_small" = "Shift+Volume Up";
"mic_mute" = ["Microphone Mute" "Meta+Volume Mute,Microphone Mute" "Meta+Volume Mute,Mute Microphone"];
"mute" = "Volume Mute";
};
mediacontrol = {
"mediavolumedown" = "none,,Media volume down";
"mediavolumeup" = "none,,Media volume up";
"nextmedia" = "Media Next";
"pausemedia" = "Media Pause";
"playmedia" = "none,,Play media playback";
"playpausemedia" = "Media Play";
"previousmedia" = "Media Previous";
"stopmedia" = "Media Stop";
};
ksmserver = {
"Lock Session" = ["Meta+Ctrl+Q" "Screensaver" "Screensaver,Lock Session"];
};
kwin = {
"Window Close" = "Meta+Shift+Q";
"Kill Window" = "Meta+Ctrl+Esc";
"Window Operations Menu" = "Alt+F3";
"Window Resize" = "Meta+R,,Resize Window";
"Overview" = "Meta+W";
"Grid View" = "Meta+G";
"Edit Tiles" = "Meta+T";
"Activate Window Demanding Attention" = "Meta+Ctrl+A";
"Show Desktop" = "Meta+D";
"Walk Through Windows" = "Alt+Tab";
"Walk Through Windows (Reverse)" = "Alt+Shift+Tab";
"Walk Through Windows of Current Application" = "Alt+`";
"Walk Through Windows of Current Application (Reverse)" = "Alt+~";
"Window Fullscreen" = "Meta+Shift+F,,Make Window Fullscreen";
"Window Quick Tile Bottom" = "Meta+Down";
"Window Quick Tile Left" = "Meta+Left";
"Window Quick Tile Right" = "Meta+Right";
"Window Quick Tile Top" = "Meta+Up";
"view_actual_size" = "Meta+0";
"view_zoom_in" = ["Meta++" "Meta+=,Meta++" "Meta+=,Zoom In"];
"view_zoom_out" = "Meta+-";
};
"org_kde_powerdevil"."Decrease Keyboard Brightness" = "Keyboard Brightness Down";
"org_kde_powerdevil"."Decrease Screen Brightness" = "Monitor Brightness Down";
"org_kde_powerdevil"."Decrease Screen Brightness Small" = "Shift+Monitor Brightness Down";
"org_kde_powerdevil"."Hibernate" = "Hibernate";
"org_kde_powerdevil"."Increase Keyboard Brightness" = "Keyboard Brightness Up";
"org_kde_powerdevil"."Increase Screen Brightness" = "Monitor Brightness Up";
"org_kde_powerdevil"."Increase Screen Brightness Small" = "Shift+Monitor Brightness Up";
"org_kde_powerdevil"."PowerDown" = "Power Down";
"org_kde_powerdevil"."PowerOff" = "Power Off";
"org_kde_powerdevil"."Sleep" = "Sleep";
"org_kde_powerdevil"."Toggle Keyboard Backlight" = "Keyboard Light On/Off";
"org_kde_powerdevil"."Turn Off Screen" = [ ];
"org_kde_powerdevil"."powerProfile" = ["Battery" "Meta+B,Battery" "Meta+B,Switch Power Profile"];
plasmashell = {
"activate application launcher" = ["Meta" "Alt+F1,Meta" "Alt+F1,Activate Application Launcher"];
"activate task manager entry 1" = "none,,";
"activate task manager entry 2" = "none,,";
"activate task manager entry 3" = "none,,";
"activate task manager entry 4" = "none,,";
"activate task manager entry 5" = "none,,";
"activate task manager entry 6" = "none,,";
"activate task manager entry 7" = "none,,";
"activate task manager entry 8" = "none,,";
"activate task manager entry 9" = "none,,";
"activate task manager entry 10" = "none,,";
"show activity switcher" = "none,,";
};
};
configFile = {
kwinrc.Desktops.Number = {
value = 10;
immutable = true;
};
kcminputrc.Libinput = {
AccelerationProfile = "adaptive";
PointerAcceleration = 0.5;
};
kcminputrc.Mouse = {
X11LibInputXAccelProfileFlat = false;
XLbInptAccelProfileFlat = false;
};
kdeglobals.KDE.LookAndFeelPackage = "org.kde.breezedark.desktop";
};
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,95 +0,0 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.home.roles.base;
in
{
options.home.roles.base = {
enable = mkEnableOption "Enable base CLI tools and essential programs";
};
config = mkIf cfg.enable {
home.packages = with pkgs; [
fd
glances
gzip
htop
killall
less
ncdu
shellcheck
tmux
tree
];
# Automatic garbage collection for user profile (home-manager generations).
# This complements system-level gc which only cleans system generations.
# - Linux: Uses --delete-older-than to keep 10-day rollback window
# - Darwin: Overridden to use -d in base-darwin role to avoid launchd bug
# (https://github.com/nix-community/home-manager/issues/7211)
nix.gc = {
automatic = true;
randomizedDelaySec = mkIf pkgs.stdenv.isLinux "14m";
options = lib.mkDefault "--delete-older-than 10d";
};
# Essential programs everyone needs
programs.bash = {
enable = true;
initExtra = ''
codex() {
local key
key="$(rbw get openai-api-key-codex)"
OPENAI_API_KEY="$key" command codex "$@"
}
'';
};
programs.home-manager.enable = true;
programs.command-not-found.enable = true;
programs.git = {
enable = true;
settings = {
user.name = "John Ogle";
user.email = "john@ogle.fyi";
safe.directory = "/etc/nixos";
};
};
programs.jq.enable = true;
programs.neovim = {
enable = true;
viAlias = true;
vimAlias = true;
};
programs.ssh = {
enable = true;
enableDefaultConfig = false;
matchBlocks = {
"*" = {
addKeysToAgent = "yes";
};
"nucdeb1" = {
hostname = "nucdeb1.oglehome";
user = "root";
};
};
};
programs.rbw = {
enable = true;
settings = {
email = "john@johnogle.info";
base_url = "https://bitwarden.johnogle.info";
pinentry = pkgs.pinentry-qt;
};
};
# Note: modules must be imported at top-level home config
};
}

View File

@@ -1,25 +0,0 @@
{ config, lib, pkgs, globalInputs, system, ... }:
with lib;
let
cfg = config.home.roles.communication;
in
{
options.home.roles.communication = {
enable = mkEnableOption "Enable communication and messaging applications";
};
config = mkIf cfg.enable {
home.packages = [
# Communication apps
pkgs.element-desktop
# Re-enabled in 25.11 after security issues were resolved
pkgs.fluffychat
pkgs.nextcloud-talk-desktop
# For logging back into google chat
globalInputs.google-cookie-retrieval.packages.${system}.default
];
};
}

View File

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

View File

@@ -1,199 +0,0 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.home.roles.desktop;
in
{
options.home.roles.desktop = {
enable = mkEnableOption "Enable desktop GUI applications and utilities";
};
config = mkIf cfg.enable {
home.packages = with pkgs; [
# Desktop applications
bitwarden-desktop
dunst
keepassxc
unstable.ghostty
# 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
xdg-utils # XDG utilities for opening files/URLs with default applications
# System utilities with GUI components
(snapcast.override { pulseaudioSupport = true; })
# KDE tiling window management
kdePackages.krohnkite # Dynamic tiling extension for KWin 6
# 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
kdePackages.kded
kdePackages.systemsettings
kdePackages.kmenuedit
# Desktop menu support
kdePackages.plasma-desktop # Contains applications.menu
# KDE Online Accounts support
kdePackages.kaccounts-integration
kdePackages.kaccounts-providers
kdePackages.signond
# KDE Mapping
kdePackages.marble # Virtual globe and world atlas
# 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
kdePackages.gwenview # Image viewer and basic editor
kdePackages.elisa # Music player
# KDE System Utilities
kdePackages.ark # Archive manager (zip, tar, 7z, etc.)
kdePackages.yakuake # Drop-down terminal emulator
];
programs.firefox = {
enable = true;
};
programs.spotify-player.enable = true;
services.gnome-keyring = {
enable = true;
};
# rbw vault unlock on login and resume from suspend
systemd.user.services.rbw-unlock-on-login = {
Unit = {
Description = "Unlock rbw vault at login";
After = [ "graphical-session.target" ];
};
Service = {
Type = "oneshot";
ExecStart = "${pkgs.rbw}/bin/rbw unlock";
Environment = "RBW_AGENT=${pkgs.rbw}/bin/rbw-agent";
# KillMode = "process" prevents systemd from killing the rbw-agent daemon
# when this oneshot service completes. The agent is spawned by rbw unlock
# and needs to persist after the service exits.
KillMode = "process";
};
Install = {
WantedBy = [ "graphical-session.target" ];
};
};
systemd.user.services.rbw-unlock-on-resume = {
Unit = {
Description = "Unlock rbw vault after resume from suspend";
After = [ "suspend.target" ];
};
Service = {
Type = "oneshot";
ExecStart = "${pkgs.rbw}/bin/rbw unlock";
Environment = "RBW_AGENT=${pkgs.rbw}/bin/rbw-agent";
# KillMode = "process" prevents systemd from killing the rbw-agent daemon
# when this oneshot service completes. The agent is spawned by rbw unlock
# and needs to persist after the service exits.
KillMode = "process";
};
Install = {
WantedBy = [ "suspend.target" ];
};
};
# KDE environment variables for proper integration
home.sessionVariables = {
QT_QPA_PLATFORMTHEME = "kde";
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 = {
# Ensure associations are properly registered
"text/html" = "firefox.desktop";
"x-scheme-handler/http" = "firefox.desktop";
"x-scheme-handler/https" = "firefox.desktop";
};
defaultApplications = {
# 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";
# Documents
"application/pdf" = "okular.desktop";
"text/plain" = "kate.desktop";
"text/x-tex" = "kate.desktop";
"text/x-c" = "kate.desktop";
"text/x-python" = "kate.desktop";
"application/x-shellscript" = "kate.desktop";
# Images
"image/png" = "gwenview.desktop";
"image/jpeg" = "gwenview.desktop";
"image/jpg" = "gwenview.desktop";
"image/gif" = "gwenview.desktop";
"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";
};
};
};
# 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".source = "${pkgs.kdePackages.plasma-workspace}/etc/xdg/menus/plasma-applications.menu";
# Note: modules must be imported at top-level home config
};
}

View File

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

View File

@@ -1,77 +0,0 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.home.roles.emacs;
doomEmacs = pkgs.fetchFromGitHub {
owner = "doomemacs";
repo = "doomemacs";
rev = "762f47805ac2a6411e11747f86f7c19a03da326e";
sha256 = "sha256-0w0eXGB2cgxu/hr5wTiJSZDJw0NF+fZvLbzEylH5URU=";
};
# Shared emacs packages
emacsPackages = epkgs: [
epkgs.vterm
epkgs.treesit-grammars.with-all-grammars
];
# Default emacs configuration with vterm support
defaultEmacsPackage =
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";
};
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
];
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;
};
home.sessionPath = [
"${config.xdg.configHome}/emacs/bin"
];
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"
'';
};
}

View File

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

View File

@@ -1,20 +0,0 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.home.roles.gaming;
in
{
options.home.roles.gaming = {
enable = mkEnableOption "Enable gaming applications and tools";
};
config = mkIf cfg.enable {
home.packages = with pkgs; [
# 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

@@ -1,487 +0,0 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.home.roles.i3_sway;
shared_config = recursiveUpdate rec {
modifier = "Mod4";
terminal = "ghostty";
defaultWorkspace = "workspace number 1";
keybindings = {
"${shared_config.modifier}+Return" = "exec ${terminal}";
"${shared_config.modifier}+Shift+q" = "kill";
"${shared_config.modifier}+a" = "focus parent";
"${shared_config.modifier}+Shift+a" = "focus child";
"${shared_config.modifier}+h" = "focus left";
"${shared_config.modifier}+j" = "focus down";
"${shared_config.modifier}+k" = "focus up";
"${shared_config.modifier}+l" = "focus right";
"${shared_config.modifier}+Shift+h" = "move left";
"${shared_config.modifier}+Shift+j" = "move down";
"${shared_config.modifier}+Shift+k" = "move up";
"${shared_config.modifier}+Shift+l" = "move right";
"${shared_config.modifier}+Left" = "focus left";
"${shared_config.modifier}+Down" = "focus down";
"${shared_config.modifier}+Up" = "focus up";
"${shared_config.modifier}+Right" = "focus right";
"${shared_config.modifier}+Shift+Left" = "move left";
"${shared_config.modifier}+Shift+Down" = "move down";
"${shared_config.modifier}+Shift+Up" = "move up";
"${shared_config.modifier}+Shift+Right" = "move right";
#"${shared_config.modifier}+h" = "split h";
"${shared_config.modifier}+v" = "split v";
"${shared_config.modifier}+Shift+f" = "fullscreen toggle";
"${shared_config.modifier}+s" = "layout stacking";
"${shared_config.modifier}+w" = "layout tabbed";
"${shared_config.modifier}+e" = "layout toggle split";
"${shared_config.modifier}+Shift+space" = "floating toggle";
"${shared_config.modifier}+space" = "focus mode_toggle";
"${shared_config.modifier}+Shift+minus" = "move scratchpad";
"${shared_config.modifier}+minus" = "scratchpad show";
"${shared_config.modifier}+1" = "workspace number 1";
"${shared_config.modifier}+2" = "workspace number 2";
"${shared_config.modifier}+3" = "workspace number 3";
"${shared_config.modifier}+4" = "workspace number 4";
"${shared_config.modifier}+5" = "workspace number 5";
"${shared_config.modifier}+6" = "workspace number 6";
"${shared_config.modifier}+7" = "workspace number 7";
"${shared_config.modifier}+8" = "workspace number 8";
"${shared_config.modifier}+9" = "workspace number 9";
"${shared_config.modifier}+0" = "workspace number 10";
"${shared_config.modifier}+Shift+1" =
"move container to workspace number 1";
"${shared_config.modifier}+Shift+2" =
"move container to workspace number 2";
"${shared_config.modifier}+Shift+3" =
"move container to workspace number 3";
"${shared_config.modifier}+Shift+4" =
"move container to workspace number 4";
"${shared_config.modifier}+Shift+5" =
"move container to workspace number 5";
"${shared_config.modifier}+Shift+6" =
"move container to workspace number 6";
"${shared_config.modifier}+Shift+7" =
"move container to workspace number 7";
"${shared_config.modifier}+Shift+8" =
"move container to workspace number 8";
"${shared_config.modifier}+Shift+9" =
"move container to workspace number 9";
"${shared_config.modifier}+Shift+0" =
"move container to workspace number 10";
"${shared_config.modifier}+Shift+c" = "reload";
"${shared_config.modifier}+Shift+r" = "restart";
"${shared_config.modifier}+r" = "mode resize";
"XF86MonBrightnessUp" = "exec ddcutil setvcp 10 + 5";
"XF86MonBrightnessDown" = "exec ddcutil setvcp 10 - 5";
};
} cfg.extraSharedConfig;
in {
options.home.roles.i3_sway = {
enable = mkEnableOption "i3 and Sway tiling window managers with waybar and rofi";
extraSharedConfig = mkOption {
type = types.attrs;
default = {};
description = "Extra configuration shared between i3 and sway";
};
extraI3Config = mkOption {
type = types.attrs;
default = {};
description = "Extra i3-specific configuration";
};
extraSwayConfig = mkOption {
type = types.attrs;
default = {};
description = "Extra sway-specific configuration";
};
};
config = mkIf cfg.enable {
# i3blocks configuration file
home.file.".config/i3blocks/config".text = ''
# i3blocks config - replicating waybar setup
separator_block_width=15
markup=pango
[disk]
command=df -h / | awk 'NR==2 {print "💾 " $5}'
interval=30
separator=true
[cpu]
command=top -bn1 | grep "Cpu(s)" | sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | awk '{print "🧠 " int(100 - $1) "%"}'
interval=2
separator=true
[memory]
command=free | awk 'NR==2 {printf "🐏 %.0f%%\n", $3*100/$2}'
interval=5
separator=true
[pulseaudio]
command=${pkgs.writeShellScript "i3blocks-pulseaudio" ''
volume=$(pactl get-sink-volume @DEFAULT_SINK@ | grep -Po '\d+%' | head -1)
muted=$(pactl get-sink-mute @DEFAULT_SINK@ | grep -o 'yes')
if [ "$muted" = "yes" ]; then
echo "🔇"
else
vol_num=''${volume%\%}
if [ $vol_num -le 33 ]; then
echo "🔈 $volume"
elif [ $vol_num -le 66 ]; then
echo "🔉 $volume"
else
echo "🔊 $volume"
fi
fi
''}
interval=1
signal=10
separator=true
[backlight]
command=${pkgs.writeShellScript "i3blocks-backlight" ''
if command -v ddcutil &>/dev/null; then
# Handle mouse scroll events
case $BLOCK_BUTTON in
4) ddcutil setvcp 10 + 5 ;; # Scroll up - increase brightness
5) ddcutil setvcp 10 - 5 ;; # Scroll down - decrease brightness
esac
# Display current brightness
brightness=$(ddcutil getvcp 10 2>/dev/null | grep -oP 'current value =\s*\K\d+')
if [ -n "$brightness" ]; then
echo " $brightness%"
fi
fi
''}
interval=5
separator=true
[network]
command=${pkgs.writeShellScript "i3blocks-network" ''
if iwgetid -r &>/dev/null; then
ssid=$(iwgetid -r)
signal=$(grep "^\s*w" /proc/net/wireless | awk '{print int($3 * 100 / 70)}')
echo "📶 $ssid ($signal%)"
else
ip=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -1)
if [ -n "$ip" ]; then
echo "🔌 $ip"
else
echo ""
fi
fi
''}
interval=5
separator=true
[battery]
command=${pkgs.writeShellScript "i3blocks-battery" ''
if [ -d /sys/class/power_supply/BAT0 ]; then
capacity=$(cat /sys/class/power_supply/BAT0/capacity)
status=$(cat /sys/class/power_supply/BAT0/status)
if [ "$status" = "Charging" ]; then
echo " $capacity%"
else
echo "🔋 $capacity%"
fi
fi
''}
interval=10
separator=true
[time]
command=date '+%Y-%m-%d %H:%M'
interval=1
separator=false
'';
xsession.windowManager.i3 = let
base_i3_config = recursiveUpdate shared_config {
bars = [{
position = "bottom";
statusCommand = "${pkgs.i3blocks}/bin/i3blocks";
trayOutput = "primary"; # Enable system tray on primary output
fonts = {
names = [ "Fira Code" "monospace" ];
size = 11.0;
};
colors = {
background = "#000000";
statusline = "#ffffff";
separator = "#666666";
# Workspace button colors (matching waybar)
focusedWorkspace = {
border = "#285577";
background = "#285577";
text = "#ffffff";
};
activeWorkspace = {
border = "#5f676a";
background = "#5f676a";
text = "#ffffff";
};
inactiveWorkspace = {
border = "#222222";
background = "#222222";
text = "#888888";
};
urgentWorkspace = {
border = "#900000";
background = "#900000";
text = "#ffffff";
};
};
}];
keybindings = shared_config.keybindings // {
"${shared_config.modifier}+d" = "exec rofi -show drun";
"${shared_config.modifier}+Shift+e" =
"exec i3-nagbar -t warning -m 'Do you want to exit i3?' -b 'Yes' 'i3-msg exit'";
};
startup = [
# GNOME polkit authentication agent
{
command = "/run/current-system/sw/libexec/polkit-gnome-authentication-agent-1";
always = false;
notification = false;
}
# Picom compositor for smooth rendering and no tearing (important for Nvidia)
{
command = "picom --backend glx -b";
always = false;
notification = false;
}
# NetworkManager system tray applet
{
command = "nm-applet";
always = false;
notification = false;
}
# Set wallpaper with feh
{
command = "feh --bg-scale ${../../wallpapers/metroid-samus-returns-kz-3440x1440.jpg}";
always = false;
notification = false;
}
];
};
in {
enable = true;
config = recursiveUpdate base_i3_config cfg.extraI3Config;
};
wayland.windowManager.sway = let
base_sway_config = recursiveUpdate shared_config {
bars = []; # Disable default bar, use waybar instead
keybindings = shared_config.keybindings // {
"${shared_config.modifier}+d" = "exec wofi --show drun";
"${shared_config.modifier}+Shift+e" =
"exec swaynag -t warning -m 'You pressed the exit shortcut. Do you really want to exit sway? This will end your Wayland session.' -b 'Yes, exit sway' 'swaymsg exit'";
};
input = {
"type:keyboard" = {
xkb_options = "caps:escape";
};
"type:touchpad" = {
tap = "enabled";
tap_button_map = "lrm";
drag = "enabled";
natural_scroll = "disabled";
dwt = "enabled";
};
};
output = {
"*" = {
bg = "${../../wallpapers/metroid-samus-returns-kz-3440x1440.jpg} fill";
};
};
startup = [
# Launch waybar status bar
{
command = "waybar";
always = false;
}
];
};
in {
enable = true;
extraOptions = [ "--unsupported-gpu" ];
config = recursiveUpdate base_sway_config cfg.extraSwayConfig;
};
programs.waybar = {
enable = true;
systemd.enable = false; # Don't auto-start via systemd - only launch in sway
settings = {
mainBar = {
layer = "top";
position = "bottom";
height = 30;
spacing = 4;
modules-left = [ "sway/workspaces" "sway/mode" ];
modules-center = [ ];
modules-right = [ "disk" "cpu" "memory" "pulseaudio" "custom/backlight-ddc" "backlight" "network" "battery" "tray" "clock" ];
"sway/workspaces" = {
disable-scroll = true;
all-outputs = true;
};
"clock" = {
format = "{:%Y-%m-%d %H:%M}";
tooltip-format = "<tt><small>{calendar}</small></tt>";
calendar = {
mode = "year";
mode-mon-col = 3;
weeks-pos = "right";
on-scroll = 1;
format = {
months = "<span color='#ffead3'><b>{}</b></span>";
days = "<span color='#ecc6d9'><b>{}</b></span>";
weeks = "<span color='#99ffdd'><b>W{}</b></span>";
weekdays = "<span color='#ffcc66'><b>{}</b></span>";
today = "<span color='#ff6699'><b><u>{}</u></b></span>";
};
};
};
"disk" = {
interval = 30;
format = "💾 {percentage_used}%";
path = "/";
tooltip-format = "Used: {used} / {total} ({percentage_used}%)\nFree: {free} ({percentage_free}%)";
};
"cpu" = {
format = "🧠 {usage}%";
tooltip = false;
};
"memory" = {
format = "🐏 {percentage}%";
tooltip-format = "RAM: {used:0.1f}G / {total:0.1f}G";
};
"pulseaudio" = {
format = "{icon} {volume}%";
format-muted = "🔇";
format-icons = {
headphone = "🎧";
default = [ "🔈" "🔉" "🔊" ];
};
on-click = "pavucontrol";
};
"backlight" = {
format = " {percent}%";
tooltip = false;
};
"custom/backlight-ddc" = {
exec = pkgs.writeShellScript "waybar-backlight-ddc" ''
if command -v ddcutil &>/dev/null; then
# Display current brightness
brightness=$(ddcutil getvcp 10 --brief 2>/dev/null | awk '{print $4}')
if [ -n "$brightness" ]; then
echo " $brightness%"
fi
fi
'';
interval = 5;
format = "{}";
on-scroll-up = "ddcutil setvcp 10 + 5 2>/dev/null &";
on-scroll-down = "ddcutil setvcp 10 - 5 2>/dev/null &";
tooltip = false;
};
"network" = {
format-wifi = "📶 {essid} ({signalStrength}%)";
format-ethernet = "🔌 {ipaddr}";
format-disconnected = "";
tooltip-format = "{ifname}: {ipaddr}/{cidr}";
};
"battery" = {
states = {
warning = 30;
critical = 15;
};
format = "{icon} {capacity}%";
format-charging = " {capacity}%";
format-icons = [ "🪫" "🔋" "🔋" "🔋" "🔋" ];
};
"tray" = {
spacing = 10;
};
};
};
style = ''
* {
padding: 0 4px;
font-family: "Fira Code", monospace;
font-size: 13px;
}
#workspaces button {
padding: 0 8px;
background-color: #333333;
color: #ffffff;
border: none;
}
#workspaces button.focused {
background-color: #285577;
font-weight: bold;
}
#workspaces button.visible {
background-color: #5f676a;
}
#workspaces button.urgent {
background-color: #900000;
}
'';
};
programs.rofi = {
enable = true;
theme = "solarized";
extraConfig = {
modi = "drun,run,window";
show-icons = true;
drun-display-format = "{name}";
disable-history = false;
hide-scrollbar = true;
display-drun = " Apps";
display-run = " Run";
display-window = " Windows";
sidebar-mode = true;
};
};
};
}

View File

@@ -1,20 +0,0 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.home.roles.kdeconnect;
in
{
options.home.roles.kdeconnect = {
enable = mkEnableOption "Enable KDE Connect for device integration";
};
config = mkIf cfg.enable {
services.kdeconnect = {
enable = true;
indicator = true;
package = pkgs.kdePackages.kdeconnect-kde;
};
};
}

View File

@@ -1,243 +0,0 @@
{ config, lib, pkgs, globalInputs, system, ... }:
with lib;
let
cfg = config.home.roles.kubectl;
in
{
options.home.roles.kubectl = {
enable = mkEnableOption "management tools for the homelab k3s oglenet cluster with secure Bitwarden integration";
};
config = mkIf cfg.enable {
home.packages = with pkgs; [
kubectl
kubernetes-helm
];
programs.k9s.enable = true;
programs.bash.initExtra = mkAfter ''
# Kubectl secure session management
export KUBECTL_SESSION_DIR="/dev/shm/kubectl-$$"
kube-select() {
if [[ $# -ne 1 ]]; then
echo "Usage: kube-select <context-name>"
echo "Available contexts: $(kube-list)"
return 1
fi
local context="$1"
# Clean up any existing session first
kube-clear 2>/dev/null
# Create new session directory
mkdir -p "$KUBECTL_SESSION_DIR"
chmod 700 "$KUBECTL_SESSION_DIR"
# Set cleanup trap for this shell session
trap "rm -rf '$KUBECTL_SESSION_DIR' 2>/dev/null" EXIT
# Set KUBECONFIG for this session
export KUBECONFIG="$KUBECTL_SESSION_DIR/config"
# Load config from Bitwarden secure notes
if ! rbw get "kubectl-$context" > "$KUBECONFIG" 2>/dev/null; then
echo "Error: Could not retrieve kubectl-$context from Bitwarden"
echo "Make sure the entry exists with name: kubectl-$context"
kube-clear
return 1
fi
# Verify the kubeconfig is valid
if ! kubectl config view >/dev/null 2>&1; then
echo "Error: Invalid kubeconfig retrieved from Bitwarden"
kube-clear
return 1
fi
echo " Loaded kubectl context: $context (session: $$)"
echo " Config location: $KUBECONFIG"
}
kube-list() {
echo "Available kubectl contexts in Bitwarden:"
rbw search kubectl- 2>/dev/null | grep "^kubectl-" | sed 's/^kubectl-/ - /' || echo " (none found or rbw not accessible)"
}
kube-clear() {
if [[ -n "$KUBECTL_TIMEOUT_PID" ]]; then
kill "$KUBECTL_TIMEOUT_PID" 2>/dev/null
unset KUBECTL_TIMEOUT_PID
fi
if [[ -d "$KUBECTL_SESSION_DIR" ]]; then
rm -rf "$KUBECTL_SESSION_DIR"
echo "Cleared kubectl session ($$)"
fi
unset KUBECONFIG
}
kube-status() {
if [[ -f "$KUBECONFIG" ]]; then
local current_context
current_context=$(kubectl config current-context 2>/dev/null)
if [[ -n "$current_context" ]]; then
echo "Active kubectl context: $current_context"
echo "Session: $$ | Config: $KUBECONFIG"
# Show cluster info
local cluster_server
cluster_server=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}' 2>/dev/null)
if [[ -n "$cluster_server" ]]; then
echo "Cluster: $cluster_server"
fi
else
echo "No active context in current session"
fi
else
echo "No kubectl session active in this shell"
echo "Use 'kube-select <context>' to start a session"
fi
}
# Helper function to show available commands
kube-help() {
echo "Secure kubectl session management commands:"
echo ""
echo "Session management:"
echo " kube-select <context> - Load kubeconfig from Bitwarden"
echo " kube-status - Show current session status"
echo " kube-clear - Clear current session"
echo ""
echo "Configuration management:"
echo " kube-list - List available contexts in Bitwarden"
echo ""
echo "Help:"
echo " kube-help - Show this help"
echo ""
echo "Examples:"
echo " kube-select prod # Loads from secure note"
echo " kubectl get pods"
echo " kube-clear"
echo ""
echo "Note: Kubeconfigs are stored as secure notes in Bitwarden"
}
'';
programs.zsh.initExtra = mkAfter ''
# Kubectl secure session management (zsh)
export KUBECTL_SESSION_DIR="/dev/shm/kubectl-$$"
kube-select() {
if [[ $# -ne 1 ]]; then
echo "Usage: kube-select <context-name>"
echo "Available contexts: $(kube-list)"
return 1
fi
local context="$1"
# Clean up any existing session first
kube-clear 2>/dev/null
# Create new session directory
mkdir -p "$KUBECTL_SESSION_DIR"
chmod 700 "$KUBECTL_SESSION_DIR"
# Set cleanup trap for this shell session
trap "rm -rf '$KUBECTL_SESSION_DIR' 2>/dev/null" EXIT
# Set KUBECONFIG for this session
export KUBECONFIG="$KUBECTL_SESSION_DIR/config"
# Load config from Bitwarden secure notes
if ! rbw get "kubectl-$context" > "$KUBECONFIG" 2>/dev/null; then
echo "Error: Could not retrieve kubectl-$context from Bitwarden"
echo "Make sure the entry exists with name: kubectl-$context"
kube-clear
return 1
fi
# Verify the kubeconfig is valid
if ! kubectl config view >/dev/null 2>&1; then
echo "Error: Invalid kubeconfig retrieved from Bitwarden"
kube-clear
return 1
fi
echo " Loaded kubectl context: $context (session: $$)"
echo " Config location: $KUBECONFIG"
}
kube-list() {
echo "Available kubectl contexts in Bitwarden:"
rbw search kubectl- 2>/dev/null | grep "^kubectl-" | sed 's/^kubectl-/ - /' || echo " (none found or rbw not accessible)"
}
kube-clear() {
if [[ -n "$KUBECTL_TIMEOUT_PID" ]]; then
kill "$KUBECTL_TIMEOUT_PID" 2>/dev/null
unset KUBECTL_TIMEOUT_PID
fi
if [[ -d "$KUBECTL_SESSION_DIR" ]]; then
rm -rf "$KUBECTL_SESSION_DIR"
echo "Cleared kubectl session ($$)"
fi
unset KUBECONFIG
}
kube-status() {
if [[ -f "$KUBECONFIG" ]]; then
local current_context
current_context=$(kubectl config current-context 2>/dev/null)
if [[ -n "$current_context" ]]; then
echo "Active kubectl context: $current_context"
echo "Session: $$ | Config: $KUBECONFIG"
# Show cluster info
local cluster_server
cluster_server=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}' 2>/dev/null)
if [[ -n "$cluster_server" ]]; then
echo "Cluster: $cluster_server"
fi
else
echo "No active context in current session"
fi
else
echo "No kubectl session active in this shell"
echo "Use 'kube-select <context>' to start a session"
fi
}
# Helper function to show available commands
kube-help() {
echo "Secure kubectl session management commands:"
echo ""
echo "Session management:"
echo " kube-select <context> - Load kubeconfig from Bitwarden"
echo " kube-status - Show current session status"
echo " kube-clear - Clear current session"
echo ""
echo "Configuration management:"
echo " kube-list - List available contexts in Bitwarden"
echo ""
echo "Help:"
echo " kube-help - Show this help"
echo ""
echo "Examples:"
echo " kube-select prod # Loads from secure note"
echo " kubectl get pods"
echo " kube-clear"
echo ""
echo "Note: Kubeconfigs are stored as secure notes in Bitwarden"
}
'';
};
}

View File

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

View File

@@ -1,27 +0,0 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.home.roles.media;
in
{
options.home.roles.media = {
enable = mkEnableOption "Enable media and multimedia applications";
};
config = mkIf cfg.enable {
home.packages = with pkgs; [
# Media players and streaming
# Using delfin instead of jellyfin-media-player to avoid qtwebengine security issues
# For full Jellyfin features, use web interface at http://jellyfin-server:8096
delfin
moonlight-qt
vlc
# Spotify client
# Using unstable version for better authentication support
unstable.ncspot
];
};
}

View File

@@ -1,22 +0,0 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.home.roles.office;
in
{
options.home.roles.office = {
enable = mkEnableOption "Enable office applications and document processing tools";
};
config = mkIf cfg.enable {
home.packages = with pkgs; [
# Office suite
libreoffice
# CAD/Design tools
openscad-unstable
];
};
}

View File

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

View File

@@ -1,22 +0,0 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.home.roles.sync;
in
{
options.home.roles.sync = {
enable = mkEnableOption "Enable file synchronization services";
};
config = mkIf cfg.enable {
home.packages = with pkgs; [
syncthingtray
];
services.syncthing = {
enable = true;
};
};
}

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

View File

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

View File

@@ -1,23 +0,0 @@
{ config, lib, pkgs, ... }:
{
# Basic system configuration for macOS work laptop
system.stateVersion = 6;
# Set primary user for nix-darwin
system.primaryUser = "johno";
# System preferences (can be expanded later)
system.defaults = {
dock.autohide = true;
finder.AppleShowAllExtensions = true;
NSGlobalDomain.AppleShowAllExtensions = true;
};
# TODO: Find a way to not duplicate this
launchd.user.envVariables = {
# DOOM Emacs environment variables
DOOMDIR = "/Users/johno/.config/doom";
DOOMLOCALDIR = "/Users/johno/.local/doom";
};
}

View File

@@ -1,91 +0,0 @@
# Live USB ISO configuration for recovery and installation
{ pkgs, modulesPath, ... }:
{
imports = [
# Use minimal installation CD as base
(modulesPath + "/installer/cd-dvd/installation-cd-minimal.nix")
];
# Use roles structure for consistent configuration
roles = {
audio.enable = true;
bluetooth.enable = true;
desktop = {
enable = true;
kde = true;
x11 = true;
wayland = true;
sddm = true;
};
};
# Allow unfree packages for broader hardware support
nixpkgs.config.allowUnfree = true;
# Essential packages for system recovery and installation
environment.systemPackages = with pkgs; [
# Text editors
neovim
nano
# System tools
git
curl
wget
htop
tree
lsof
strace
# Filesystem tools
btrfs-progs
e2fsprogs
xfsprogs
ntfs3g
dosfstools
# Network tools
networkmanager
wirelesstools
# Hardware tools
pciutils
usbutils
smartmontools
# Archive tools
unzip
p7zip
# Development tools (for quick fixes)
gcc
binutils
];
# Enable NetworkManager for easy wifi setup
networking.networkmanager.enable = true;
# Disable wireless networking (conflicts with NetworkManager)
networking.wireless.enable = false;
# Enable SSH daemon for remote access
services.openssh = {
enable = true;
settings = {
PermitRootLogin = "yes";
PasswordAuthentication = true;
};
};
# ISO customization
isoImage = {
volumeID = "NIXOS-LIVE";
};
# Enable some useful services
services.udisks2.enable = true; # For mounting USB drives
# Hardware support
hardware.enableAllFirmware = true;
hardware.enableRedistributableFirmware = true;
}

View File

@@ -15,17 +15,12 @@
desktop = { desktop = {
enable = true; enable = true;
wayland = true; wayland = true;
gaming.enable = true; gaming = false;
kde = true; kde = true;
sddm = true; sddm = true;
}; };
nfs-mounts.enable = true; nfs-mounts.enable = true;
printing.enable = true; printing.enable = true;
remote-build.builders = [{
hostName = "zix790prors";
maxJobs = 16;
speedFactor = 3;
}];
spotifyd.enable = true; spotifyd.enable = true;
users = { users = {
enable = true; enable = true;
@@ -42,16 +37,11 @@
networking.hostName = "nix-book"; # Define your hostname. networking.hostName = "nix-book"; # Define your hostname.
# networking.wireless.enable = true; # Enables wireless support via wpa_supplicant. # networking.wireless.enable = true; # Enables wireless support via wpa_supplicant.
boot.kernelPackages = pkgs.linuxPackages_latest;
# Enable networking # Enable networking
networking.networkmanager.enable = true; networking.networkmanager.enable = true;
# WireGuard setup
networking.wg-quick.interfaces = {
ogleNet = {
configFile = "/root/Oglehome-VPN-johno-nixbook.conf";
};
};
hardware.graphics = { hardware.graphics = {
enable = true; enable = true;
extraPackages = with pkgs; [ extraPackages = with pkgs; [

View File

@@ -10,27 +10,15 @@
boot.initrd.availableKernelModules = [ "xhci_pci" "thunderbolt" "vmd" "nvme" "sdhci_pci" ]; boot.initrd.availableKernelModules = [ "xhci_pci" "thunderbolt" "vmd" "nvme" "sdhci_pci" ];
boot.initrd.kernelModules = [ ]; boot.initrd.kernelModules = [ ];
boot.initrd.luks.devices."luks-4126fbd4-bd09-4ece-af0d-6fff414c21b3".device = "/dev/disk/by-uuid/4126fbd4-bd09-4ece-af0d-6fff414c21b3";
boot.kernelModules = [ "kvm-intel" ]; boot.kernelModules = [ "kvm-intel" ];
boot.extraModulePackages = [ ]; boot.extraModulePackages = [ ];
roles.btrfs = { fileSystems."/" =
enable = true; { device = "/dev/disk/by-uuid/bd396529-e2c4-47cb-b844-8d6ed841f81a";
filesystems."/dev/disk/by-uuid/223a44e5-91e2-4272-830e-129166042a1d" = { fsType = "ext4";
mountpoints = {
"/" = {
compression = "zstd";
extraOptions = [ "noatime" ];
};
};
scrub.enable = true;
deduplication = {
enable = true;
hashTableSizeMB = 32;
verbosity = "err";
};
}; };
};
boot.initrd.luks.devices."luks-4126fbd4-bd09-4ece-af0d-6fff414c21b3".device = "/dev/disk/by-uuid/4126fbd4-bd09-4ece-af0d-6fff414c21b3";
fileSystems."/boot" = fileSystems."/boot" =
{ device = "/dev/disk/by-uuid/7A0B-CF88"; { device = "/dev/disk/by-uuid/7A0B-CF88";

View File

@@ -1,223 +0,0 @@
# NixBook ext4 to btrfs Migration Guide
## Overview
This guide converts your nixbook machine from ext4 to btrfs with zstd compression and beesd deduplication while preserving your LUKS encryption and all data.
## Current System Info
- **Hostname**: nix-book
- **Root filesystem**: ext4 on `/dev/disk/by-uuid/bd396529-e2c4-47cb-b844-8d6ed841f81a`
- **Encryption**: LUKS with two devices configured
- **Current usage**: 138GB used / 225GB total (65% full)
- **Free space**: 76GB available (sufficient for conversion)
## Pre-Migration Checklist
### 1. Create Full System Backup (CRITICAL)
```bash
# Boot from NixOS live USB
# Mount encrypted filesystem
cryptsetup luksOpen /dev/disk/by-uuid/4126fbd4-bd09-4ece-af0d-6fff414c21b3 luks-nixbook
mount /dev/mapper/luks-nixbook /mnt
# Create backup to external drive (adjust target as needed)
rsync -avxHAX --progress /mnt/ /path/to/backup/nixbook-backup/
```
### 2. Verify Configuration Changes
The following files have been updated for btrfs:
- `machines/nix-book/configuration.nix` - Added beesd service
- `machines/nix-book/hardware-configuration.nix` - Changed fsType to btrfs with compression
## Migration Process
### Phase 1: Boot to Live Environment
1. **Create NixOS live USB**:
```bash
# Download latest NixOS ISO
# Flash to USB drive
dd if=nixos-minimal-xx.xx-x86_64-linux.iso of=/dev/sdX bs=4M status=progress
```
2. **Boot from live USB** and ensure you can access the encrypted drives
### Phase 2: Filesystem Conversion
3. **Unlock LUKS volumes**:
```bash
cryptsetup luksOpen /dev/disk/by-uuid/4126fbd4-bd09-4ece-af0d-6fff414c21b3 luks-nixbook
cryptsetup luksOpen /dev/disk/by-uuid/b614167b-9045-4234-a441-ac6f60a96d81 luks-nixbook2
```
4. **Check filesystem before conversion**:
```bash
fsck.ext4 -f /dev/mapper/luks-nixbook
```
5. **Convert ext4 to btrfs** (this preserves all data):
```bash
# Install btrfs-progs if not available
nix-shell -p btrfs-progs
# Convert the filesystem (takes 15-45 minutes depending on data)
btrfs-convert /dev/mapper/luks-nixbook
# Verify conversion succeeded
mount /dev/mapper/luks-nixbook /mnt
ls -la /mnt # Should show your normal filesystem
btrfs filesystem show /mnt
```
6. **Get new filesystem UUID** (may have changed):
```bash
blkid /dev/mapper/luks-nixbook
# Note the new UUID if it changed
```
### Phase 3: Configuration Update
7. **Mount and chroot into system**:
```bash
mount -o compress=zstd,noatime /dev/mapper/luks-nixbook /mnt
mount /dev/disk/by-uuid/7A0B-CF88 /mnt/boot
nixos-enter --root /mnt
```
8. **Update hardware-configuration.nix** if UUID changed:
```bash
# Edit /etc/nixos/hardware-configuration.nix if needed
# Update the UUID in fileSystems."/" section
```
9. **Rebuild system with btrfs configuration**:
```bash
cd /home/johno/nixos-configs
nixos-rebuild switch --flake .#nix-book
```
### Phase 4: Enable Compression and Deduplication
10. **Reboot into new btrfs system**:
```bash
exit # Exit chroot
umount -R /mnt
reboot
```
11. **Verify btrfs is working**:
```bash
mount | grep btrfs
btrfs filesystem usage /
```
12. **Enable and start beesd**:
```bash
systemctl status beesd-root
systemctl start beesd-root
systemctl enable beesd-root
```
13. **Force compression on existing files** (optional but recommended):
```bash
# This will compress existing files with zstd
btrfs filesystem defragment -r -czstd /
```
## Post-Migration Verification
### Check System Health
```bash
# Verify btrfs health
btrfs scrub start /
btrfs scrub status /
# Check compression effectiveness
compsize /
# Monitor beesd deduplication
journalctl -u beesd-root -f
# Check filesystem usage
btrfs filesystem usage /
df -h /
```
### Performance Monitoring
```bash
# Monitor beesd hash table
ls -lh /.beeshash
# Check compression ratio over time
compsize /home /nix /var
```
## Expected Benefits
### Space Savings
- **Compression**: 20-30% reduction in disk usage from zstd
- **Deduplication**: Additional 10-20% savings on duplicate files
- **Combined**: Potentially 30-40% total space savings
### Performance Impact
- **Compression**: Minimal CPU overhead, often improves I/O performance
- **Deduplication**: Background process, minimal impact during normal use
- **Overall**: Should be neutral to positive performance impact
## Rollback Plan (Emergency)
If something goes wrong:
1. **Boot from live USB**
2. **Restore from backup**:
```bash
cryptsetup luksOpen /dev/disk/by-uuid/4126fbd4-bd09-4ece-af0d-6fff414c21b3 luks-nixbook
mkfs.ext4 /dev/mapper/luks-nixbook
mount /dev/mapper/luks-nixbook /mnt
rsync -avxHAX --progress /path/to/backup/nixbook-backup/ /mnt/
```
3. **Restore original hardware-configuration.nix** with ext4 settings
4. **Rebuild and reboot**
## Troubleshooting
### Common Issues
**"Device busy" during conversion**:
- Ensure no processes are accessing the filesystem
- Check with `lsof` and `fuser`
**UUID changed after conversion**:
- Update hardware-configuration.nix with new UUID
- Regenerate initrd: `nixos-rebuild switch`
**Beesd service fails to start**:
- Check disk space for hash table
- Verify filesystem is btrfs: `mount | grep btrfs`
- Check logs: `journalctl -u beesd-root`
**Boot issues after conversion**:
- Boot from live USB
- Check /boot partition is mounted correctly
- Verify LUKS UUIDs match in configuration
- Rebuild bootloader: `nixos-rebuild switch --install-bootloader`
## Maintenance
### Regular Tasks
```bash
# Monthly scrub (checks for corruption)
btrfs scrub start /
# Monitor compression effectiveness
compsize /
# Check beesd deduplication status
systemctl status beesd-root
```
### Space Management
```bash
# Balance filesystem (defragments and optimizes)
btrfs balance start -dusage=50 /
# Check for space issues
btrfs filesystem usage /
```
This migration preserves all your data while gaining the benefits of modern btrfs features including transparent compression and automatic deduplication.

View File

@@ -1,41 +0,0 @@
{ pkgs, ... }:
{
imports = [
./hardware-configuration.nix
../../roles/desktop/steamos.nix
];
roles = {
audio.enable = true;
bluetooth.enable = true;
desktop = {
enable = true;
wayland = true;
gaming.enable = true;
kde = true;
steamos = {
enable = true;
autoStart = true;
desktopSession = "plasma";
};
};
remote-build.builders = [{
hostName = "zix790prors";
maxJobs = 16;
speedFactor = 4; # Prefer remote heavily on Steam Deck
}];
users = {
enable = true;
extraGroups = [ "video" ];
};
};
# Bootloader
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
networking.hostName = "nix-deck";
networking.networkmanager.enable = true;
system.stateVersion = "25.05";
}

View File

@@ -1,51 +0,0 @@
# Hardware configuration for Steam Deck (nix-deck)
# Generated from nixos-generate-config on 2025-11-17
{ config, lib, pkgs, modulesPath, ... }:
{
imports = [
(modulesPath + "/installer/scan/not-detected.nix")
];
# Steam Deck specific hardware configuration (Jovian)
jovian.devices.steamdeck = {
enable = true;
autoUpdate = false; # Set to true if you want automatic firmware updates
};
# Kernel modules detected by nixos-generate-config
boot.initrd.availableKernelModules = [
"nvme"
"xhci_pci"
"usb_storage"
"uas"
"usbhid"
"sd_mod"
"sdhci_pci"
];
boot.initrd.kernelModules = [ ];
boot.kernelModules = [ "kvm-amd" ];
boot.extraModulePackages = [ ];
# IMPORTANT: Update these filesystem configurations based on your actual partition layout
# The configuration below is a placeholder - adjust according to how you partitioned the disk
fileSystems."/" = {
device = "/dev/disk/by-label/nixos";
fsType = "ext4";
};
fileSystems."/boot" = {
device = "/dev/disk/by-label/boot";
fsType = "vfat";
};
swapDevices = [{
device = "/swapfile";
size = 8192; # 8GB swap file
}];
# AMD CPU microcode updates
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
hardware.cpu.amd.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
}

View File

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

View File

@@ -1,31 +0,0 @@
* zix790prors
The re-birthed NixOS install of my 2024/2025 gaming pc / workstation.
** Specs
- **CPU:** Intel Core i7-14700K (20 cores, 28 threads, up to 5.6 GHz)
- **Memory:** 64 GB RAM
- **Storage:**
- 4TB NVMe SSD (main drive with dual-boot partitions)
- Windows 11 partition (NTFS)
- NixOS /nix/store partition (btrfs)
- Shared /games partition (btrfs, accessible from both Windows and NixOS)
- **GPU:** NVIDIA GeForce RTX 4070 Ti
- **Boot:** UEFI with 100MB EFI System Partition
This is a powerful all-purpose workstation optimized for gaming, 3D modeling, and development. It dual-boots Windows 11 with a shared btrfs /games partition accessible from both operating systems.
*** Validation
Given the above specs, I want to run shell commands to validate them for accuracy. Use the run_shell_command tool to get the results required to complete this validation. Do not return to the user until you have exhausted your self-serve options for accomplishing your task.
** BIOS Settings
**2025-09-08**
I underclocked the CPU today. I set the cpu/cache voltage offset to -50mV and lowered the P-Core multiplier from 56x to 50x. I was able to run Intel XTU benchmarks and the CPU stayed around 80C without any throttling kicking in, whereas before it would bounce around various cores at 100C with lots of throttling taking place.
My goals for this change are:
- CPU longevity
- Fan noise
- Addressing various apps that say 100C is a "critical temperature" (though I believe the CPU is rated for up to 110C)
I'm leaving some performance on the table, but it almost feels like the default settings for this CPU were to effectively be overclocked.

View File

@@ -1,96 +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 = [
./hardware-configuration.nix
#./virtual-surround.nix
];
roles = {
audio.enable = true;
bluetooth.enable = true;
desktop = {
enable = true;
gaming = {
enable = true;
};
kde = true;
sddm = true;
wayland = true;
x11 = true;
};
nfs-mounts.enable = true;
nvidia.enable = true;
printing.enable = true;
remote-build.enableBuilder = true;
users.enable = true;
virtualisation.enable = true;
};
# Use the systemd-boot EFI boot loader.
boot.loader.systemd-boot.enable = true;
boot.loader.systemd-boot.configurationLimit = 20;
boot.loader.efi.canTouchEfiVariables = true;
boot.loader.timeout = 10;
networking.hostName = "zix790prors"; # Define your hostname.
# Enable networking
networking.networkmanager.enable = true;
# 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";
loadModels = [ "gpt-oss" "deepseek-r1" "qwen3:30b" ];
};
# 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 = "25.11"; # Did you read the comment?
}

View File

@@ -1,57 +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 = [ "xhci_pci" "ahci" "nvme" "usbhid" "usb_storage" "sd_mod" ];
boot.initrd.kernelModules = [ ];
boot.kernelModules = [ "kvm-intel" ];
boot.extraModulePackages = [ ];
fileSystems."/boot" =
{ device = "/dev/disk/by-uuid/11C1-EB58";
fsType = "vfat";
options = [ "fmask=0077" "dmask=0077" ];
};
roles.btrfs = {
enable = true;
filesystems."/dev/disk/by-uuid/ec22734b-d1a3-4c99-8c6f-86f6a8d79007" = {
mountpoints = {
"/" = {
compression = "zstd";
extraOptions = [ "noatime" ];
};
};
scrub.enable = true;
deduplication = {
enable = true;
hashTableSizeMB = 128;
verbosity = "err";
};
};
filesystems."/dev/disk/by-uuid/4f9844ac-c1ad-4426-8eb3-21f2306345fb" = {
mountpoints = {
"/games" = {
extraOptions = [ "noatime" ];
};
};
scrub.enable = true;
deduplication = {
enable = true;
hashTableSizeMB = 256;
verbosity = "err";
};
};
};
swapDevices = [ ];
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
}

View File

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

View File

@@ -0,0 +1,333 @@
# NixOS Steam Dual Boot Implementation Plan (Btrfs + Beesd Edition)
## Goals & Motivations
### Primary Goals
1. **Eliminate storage waste**: Avoid duplicating terabytes of Steam games across Windows and NixOS
2. **Minimize maintenance overhead**: Create a solution that works reliably without constant tweaking
3. **Preserve Windows stability**: Ensure Windows Steam functionality remains unaffected by the dual-boot setup
4. **Maintain gaming performance**: No significant performance degradation on either OS
### Secondary Goals
- **Seamless game access**: Games should be available on both OSes without manual intervention
- **Update compatibility**: Game updates from either OS should be usable by both
- **Future-proof architecture**: Solution should be extensible and maintainable
- **Multi-user support**: Handle 3 Windows users sharing libraries, with 1 user also using Linux
## Architectural Overview
### The Problem with Previous Approaches
**Shared NTFS Library (Traditional)**:
- ❌ Proton creates files with colons, corrupting NTFS
- ❌ Requires fragile symlinks that Windows can break
- ❌ Permission issues plague the setup
- ❌ Valve officially discourages this approach
**Complex Symlink Management**:
- ❌ Requires custom scripts and maintenance
- ❌ Fragile edge cases with Steam updates
- ❌ Potential compatibility issues with Proton and anti-cheat
**Separate Libraries**:
- ❌ Wastes terabytes of storage
- ❌ Games must be installed twice
- ❌ No benefit from either OS's installations
### Our Solution: Btrfs + Beesd Automatic Deduplication
```
Windows Steam → /steam/windows/ (btrfs via WinBtrfs driver)
Linux Steam → /steam/linux/ (native btrfs)
[beesd automatically deduplicates identical files]
```
**Key Insight**: Let each Steam installation work independently, rely on proven btrfs deduplication technology to eliminate duplicate storage automatically.
## Architecture Deep Dive
### Component 1: Shared Btrfs Filesystem
**Purpose**: Single high-performance filesystem for all game storage
- **Location**: `/steam` (dedicated btrfs partition)
- **Contents**:
- `/steam/windows/` - Windows Steam library
- `/steam/linux/` - Linux Steam library
- **Access**: Native on Linux, WinBtrfs driver on Windows
**Why this satisfies our goals**:
-**Automatic deduplication**: Beesd handles duplicate elimination transparently
-**No maintenance**: Zero custom scripts or symlink management
-**Independent operation**: Each Steam installation works normally
-**Performance**: Optimized mount options for gaming workloads
### Component 2: Beesd Deduplication Service
**Purpose**: Automatic background deduplication of game files
**Core Functionality**:
- Continuously scans `/steam` for duplicate blocks
- Automatically deduplicates identical files between `/steam/windows/` and `/steam/linux/`
- Operates transparently - games never know deduplication is happening
- Handles common duplicates: DirectX runtimes, Visual C++ redistributables, game engines, shared assets
**Expected Efficiency**:
- **40-70% storage savings** for typical game libraries
- **Common targets**: Unity/Unreal engine files, shared libraries, identical texture assets
- **Real-time operation**: New duplicates eliminated automatically
### Component 3: WinBtrfs Driver Integration
**Purpose**: Provide Windows with native btrfs read/write access
```nix
# No special configuration needed - standard btrfs mount
fileSystems."/steam" = {
device = "/dev/disk/by-uuid/YOUR-BTRFS-UUID";
fsType = "btrfs";
options = [
"noatime" # Don't update access times - major gaming performance boost
"ssd" # SSD optimizations
];
};
```
**Windows Requirements**:
- Install WinBtrfs driver (https://github.com/maharmstone/btrfs)
- Configure Steam library path to point to `/steam/windows/`
- No special configuration needed - works like any other drive
### Component 4: Beesd Configuration
**Purpose**: Optimized deduplication for gaming workloads
```nix
services.beesd.filesystems = {
steam = {
spec = "/steam";
hashTableSizeMB = 512; # Sized for ~4TB of game data (128MB per TB)
verbosity = "err"; # Only show actual problems
};
};
```
**Hash Table Storage**:
- Stored as file: `/steam/.beeshash`
- 512MB handles up to 4TB of game data efficiently (beesd recommends 128MB per TB)
- Loaded into RAM on-demand, not all resident simultaneously
- Smaller hash table = better performance and less disk usage
## Partition Strategy
### 4-Partition Architecture
1. **Windows Boot Partition** (NTFS) - *Existing*
- Standard Windows system drive
- Unchanged from current setup
2. **Linux Boot Partition** (btrfs/ext4) - *New*
- NixOS system installation
- Standard Linux root filesystem
3. **Shared Steam Library** (btrfs) - *New*
- `/steam/windows/` - Windows Steam library
- `/steam/linux/` - Linux Steam library
- Automatic beesd deduplication
4. **Legacy Windows Steam** (NTFS) - *Existing, Optional*
- Keep for anti-cheat games that may not work on btrfs
- Can be eliminated if all games work on btrfs
- Provides fallback option during testing
### Multi-User Considerations
**Current Setup**: 3 Windows users sharing Steam libraries
**Migration Strategy**:
- Other Windows users continue using existing NTFS library
- Primary user (johno) experiments with btrfs library
- Easy rollback: point Steam back to NTFS if issues arise
- Gradual migration as confidence builds
## Implementation Strategy
### Phase 1: Base Setup
1. **Partition Creation**:
- Create new btrfs partition for `/steam` (recommend 2TB+ for modern libraries)
- Install NixOS on separate Linux boot partition
- Keep existing Windows partitions untouched
2. **NixOS Configuration**:
```nix
fileSystems."/steam" = {
device = "/dev/disk/by-uuid/YOUR-BTRFS-UUID";
fsType = "btrfs";
options = [ "noatime" "ssd" ];
};
services.beesd.filesystems = {
steam = {
spec = "/steam";
hashTableSizeMB = 512; # 128MB per TB recommended
verbosity = "err";
};
};
```
3. **Windows Setup**:
- Install WinBtrfs driver
- Add Steam library pointing to `/steam/windows/`
- Test with a few small games initially
### Phase 2: Gradual Migration
1. **Safe Game Testing**:
- Start with single-player games
- Test save game compatibility
- Verify performance matches NTFS installation
2. **Anti-Cheat Evaluation**:
- Test multiplayer games progressively
- Document which games work on btrfs vs require NTFS
- Keep problematic games on legacy NTFS partition
3. **Deduplication Verification**:
- Monitor beesd logs for successful deduplication
- Use `btrfs filesystem du /steam` to verify space savings
- Benchmark game loading times vs separate installations
### Phase 3: Optimization
1. **Performance Tuning**:
- Monitor btrfs performance under gaming workloads
- Adjust mount options if needed
- Optimize beesd parameters based on actual usage
2. **Monitoring Setup**:
- SystemD service monitoring for beesd
- Disk space alerts for `/steam` partition
- Basic health checks for WinBtrfs stability
## Trade-offs and Considerations
### Advantages
**Storage Efficiency**:
- ✅ 40-70% storage savings through automatic deduplication
- ✅ No manual intervention required
- ✅ Works with any game installation method
**Simplicity**:
- ✅ No custom scripts to maintain
- ✅ No symlink complexity
- ✅ Standard Steam library management on both OSes
**Reliability**:
- ✅ Each Steam installation completely independent
- ✅ Btrfs and beesd are mature, proven technologies
- ✅ Graceful degradation if deduplication fails
**Performance**:
- ✅ Optimized mount options for gaming
- ✅ No compression overhead
- ✅ Native filesystem performance on both OSes
### Limitations
**Windows Dependencies**:
- ⚠️ Requires WinBtrfs third-party driver
- ⚠️ Driver updates needed with major Windows releases
- ⚠️ Potential compatibility issues with some anti-cheat systems
**Complexity Trade-offs**:
- ⚠️ More partitions to manage than single-filesystem approach
- ⚠️ Beesd adds background CPU/disk usage (minimal but present)
- ⚠️ Hash table requires disk space (512MB for large libraries)
**Platform Compatibility**:
- ⚠️ Some games may prefer NTFS for maximum compatibility
- ⚠️ Anti-cheat systems may flag non-NTFS installations
- ⚠️ WinBtrfs stability depends on third-party development
### Risk Mitigation
**Backup Strategy**:
- Regular btrfs snapshots of `/steam` partition
- Keep legacy NTFS Steam library as fallback
- Steam's built-in backup/restore for critical games
**Fallback Options**:
- Easy to revert games to NTFS library if needed
- Linux can install games locally if `/steam` partition fails
- Independent operation means failure in one OS doesn't affect the other
**Monitoring**:
- SystemD service status for beesd
- Disk space monitoring for early warning
- Game launch testing after major updates
## Expected Outcomes
### Immediate Benefits
- **40-70% storage savings** for typical game libraries
- **Zero maintenance** after initial setup
- **Identical performance** to native installations
- **Future-proof** architecture using standard technologies
### Long-term Benefits
- **Automatic optimization**: New games deduplicated without intervention
- **Simplified management**: One shared library instead of separate installations
- **Technology leverage**: Benefits from ongoing btrfs and beesd improvements
### Success Metrics
- ✅ Games launch successfully from both OSes
- ✅ Save games work correctly on both platforms
- ✅ Updates from either OS don't break the other
- ✅ Storage usage 40-70% less than separate libraries
- ✅ No performance degradation vs native installations
- ✅ Anti-cheat compatibility acceptable for target games
### Monitoring and Maintenance
**Automated Monitoring**:
```nix
# Add to NixOS configuration for basic monitoring
systemd.services.steam-health-check = {
description = "Check Steam partition and beesd health";
serviceConfig = {
Type = "oneshot";
ExecStart = pkgs.writeScript "steam-health" ''
#!/bin/bash
# Check beesd service status
systemctl is-active beesd-steam >/dev/null || echo "WARN: beesd not running"
# Check disk space
USAGE=$(df /steam | tail -1 | awk '{print $5}' | sed 's/%//')
[ "$USAGE" -gt 90 ] && echo "WARN: Steam partition >90% full"
# Verify both directories exist
[ ! -d "/steam/windows" ] && echo "ERROR: Windows Steam directory missing"
[ ! -d "/steam/linux" ] && echo "ERROR: Linux Steam directory missing"
'';
};
};
systemd.timers.steam-health-check = {
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "daily";
Persistent = true;
};
};
```
## Implementation Files Needed
1. **NixOS Configuration Addition** - Add to existing machine config
2. **WinBtrfs Installation Guide** - Windows setup instructions
3. **Migration Checklist** - Step-by-step game migration process
4. **Troubleshooting Guide** - Common issues and solutions
This architecture provides maximum storage efficiency with minimal complexity, leveraging proven technologies instead of custom solutions. The automatic nature of btrfs deduplication eliminates the maintenance overhead of complex symlink management while providing excellent storage savings.

View File

@@ -1,176 +0,0 @@
#!/usr/bin/env python3
import json
import logging
import os
import subprocess
import sys
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse
import psutil
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# Allowlisted applications that can be launched
ALLOWED_APPS = {
'firefox': 'firefox',
'kodi': 'kodi'
}
def is_app_running(app_name):
"""Check if an application is already running, returns (is_running, pid)"""
command = ALLOWED_APPS.get(app_name)
if not command:
return False, None
logger.debug(f"Looking for processes related to app '{app_name}' (command: '{command}')")
for proc in psutil.process_iter(['name', 'cmdline', 'pid']):
try:
proc_name = proc.info['name']
cmdline = proc.info['cmdline'] or []
logger.debug(f"Checking process PID {proc.info['pid']}: name='{proc_name}', cmdline={cmdline}")
# Check multiple patterns for the application:
# 1. Process name exactly matches command
# 2. Process name contains the command (e.g., "kodi.bin" contains "kodi")
# 3. Command line starts with the command
# 4. Command line contains the wrapped version (e.g., ".kodi-wrapped")
# 5. Any command line argument ends with the command executable
matches = False
match_reason = ""
if proc_name == command:
matches = True
match_reason = f"exact process name match: '{proc_name}'"
elif command in proc_name:
matches = True
match_reason = f"process name contains command: '{proc_name}' contains '{command}'"
elif cmdline and cmdline[0] == command:
matches = True
match_reason = f"exact cmdline match: '{cmdline[0]}'"
elif cmdline and cmdline[0].endswith('/' + command):
matches = True
match_reason = f"cmdline path ends with command: '{cmdline[0]}'"
elif cmdline and any(f'.{command}-wrapped' in arg for arg in cmdline):
matches = True
match_reason = f"wrapped command in cmdline: {cmdline}"
elif cmdline and any(f'{command}.bin' in arg for arg in cmdline):
matches = True
match_reason = f"binary command in cmdline: {cmdline}"
if matches:
logger.info(f"Found running {app_name} process: PID {proc.info['pid']} ({match_reason})")
return True, proc.info['pid']
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
continue
logger.debug(f"No running process found for {app_name}")
return False, None
class AppLauncherHandler(BaseHTTPRequestHandler):
def log_message(self, format, *args):
logger.info(format % args)
def do_GET(self):
if self.path == '/':
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.end_headers()
response = {
'status': 'running',
'available_apps': list(ALLOWED_APPS.keys()),
'usage': 'POST /launch/<app_name> to launch an application'
}
self.wfile.write(json.dumps(response, indent=2).encode())
else:
self.send_error(404)
def do_POST(self):
parsed_path = urlparse(self.path)
path_parts = parsed_path.path.strip('/').split('/')
if len(path_parts) == 2 and path_parts[0] == 'launch':
app_name = path_parts[1]
self.launch_app(app_name)
else:
self.send_error(404, "Invalid endpoint. Use /launch/<app_name>")
def launch_app(self, app_name):
if app_name not in ALLOWED_APPS:
self.send_error(400, f"Application '{app_name}' not allowed. Available apps: {list(ALLOWED_APPS.keys())}")
return
command = ALLOWED_APPS[app_name]
# Check if app is already running
is_running, existing_pid = is_app_running(app_name)
if is_running:
logger.info(f"Application {app_name} is already running (PID: {existing_pid}), skipping launch")
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.end_headers()
response = {
'status': 'success',
'message': f'{app_name} is already running',
'pid': existing_pid,
'already_running': True
}
self.wfile.write(json.dumps(response).encode())
return
try:
# Launch the application in the background
# Ensure we have the proper environment for GUI apps
env = os.environ.copy()
logger.info(f"Launching application: {command}")
process = subprocess.Popen(
[command],
env=env,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True
)
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.end_headers()
response = {
'status': 'success',
'message': f'Successfully launched {app_name}',
'pid': process.pid,
'already_running': False
}
self.wfile.write(json.dumps(response).encode())
except FileNotFoundError:
logger.error(f"Application not found: {command}")
self.send_error(500, f"Application '{app_name}' not found on system")
except Exception as e:
logger.error(f"Error launching {command}: {e}")
self.send_error(500, f"Failed to launch {app_name}: {str(e)}")
def main():
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8081
server = HTTPServer(('0.0.0.0', port), AppLauncherHandler)
logger.info(f"App launcher server starting on port {port}")
logger.info(f"Available applications: {list(ALLOWED_APPS.keys())}")
try:
server.serve_forever()
except KeyboardInterrupt:
logger.info("Server shutting down...")
server.server_close()
if __name__ == '__main__':
main()

View File

@@ -1,10 +0,0 @@
{ pkgs }:
let
python = pkgs.python3.withPackages (ps: with ps; [
psutil
]);
in
pkgs.writeShellScriptBin "app-launcher-server" ''
exec ${python}/bin/python3 ${./app-launcher-server.py} "$@"
''

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,4 @@
{ pkgs, uv2nix ? null, pyproject-nix ? null, pyproject-build-systems ? null, ... }: { pkgs, ... }:
{ {
vulkanHDRLayer = pkgs.callPackage ./vulkan-hdr-layer {}; 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,91 +0,0 @@
{ 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

@@ -1,58 +0,0 @@
{ pkgs, ... }:
pkgs.writeShellScriptBin "tea" ''
set -euo pipefail
# Check if tea config directory exists and has authentication
TEA_CONFIG_DIR="''${XDG_CONFIG_HOME:-$HOME/.config}/tea"
TEA_CONFIG_FILE="$TEA_CONFIG_DIR/config.yml"
# Function to setup tea authentication with rbw
setup_tea_auth() {
echo "Tea authentication not found. Setting up with rbw..."
# 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
# Try to get the token from rbw
echo "Attempting to retrieve Gitea token from rbw..."
echo "Please enter the rbw entry name for your Gitea token:"
read -r rbw_entry
if ! token=$(rbw get "$rbw_entry" 2>/dev/null); then
echo "Error: Failed to retrieve token from rbw entry '$rbw_entry'"
echo "Available rbw entries:"
rbw list 2>/dev/null || echo "Failed to list rbw entries"
exit 1
fi
# Prompt for Gitea URL
echo "Please enter your Gitea URL (e.g., https://git.example.com):"
read -r gitea_url
# Create tea config directory if it doesn't exist
mkdir -p "$TEA_CONFIG_DIR"
# Setup tea login
if ! ${pkgs.tea}/bin/tea login add --name "default" --url "$gitea_url" --token "$token"; then
echo "Error: Failed to setup tea authentication"
exit 1
fi
echo "Tea authentication setup complete!"
}
# Check if tea is already configured
if [[ ! -f "$TEA_CONFIG_FILE" ]]; then
setup_tea_auth
elif ! ${pkgs.tea}/bin/tea whoami &>/dev/null; then
echo "Tea config exists but authentication failed. Re-running setup..."
setup_tea_auth
fi
# Execute tea with all provided arguments
exec ${pkgs.tea}/bin/tea "$@"
''

View File

@@ -13,7 +13,6 @@ in
config = mkIf cfg.enable config = mkIf cfg.enable
{ {
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
easyeffects
paprefs paprefs
pavucontrol pavucontrol
pulsemixer pulsemixer

View File

@@ -1,173 +0,0 @@
{ lib, config, pkgs, ... }:
with lib;
let
cfg = config.roles.btrfs;
in
{
options.roles.btrfs = {
enable = mkEnableOption "Enable btrfs filesystem management";
filesystems = mkOption {
type = types.attrsOf (types.submodule {
options = {
# Filesystem-level maintenance options
scrub = {
enable = mkOption {
type = types.bool;
default = true;
description = "Enable automatic scrubbing for this filesystem";
};
interval = mkOption {
type = types.str;
default = "weekly";
description = "Scrub interval (systemd timer format)";
};
};
deduplication = {
enable = mkOption {
type = types.bool;
default = false;
description = "Enable beesd deduplication for this filesystem";
};
hashTableSizeMB = mkOption {
type = types.int;
default = 1024;
description = "Hash table size in MB (should be multiple of 16)";
};
verbosity = mkOption {
type = types.str;
default = "info";
description = "Logging verbosity level";
};
};
balance = {
enable = mkOption {
type = types.bool;
default = false;
description = "Enable periodic balance operations";
};
interval = mkOption {
type = types.str;
default = "monthly";
description = "Balance interval (systemd timer format)";
};
dataUsage = mkOption {
type = types.int;
default = 50;
description = "Data usage threshold for balance";
};
metadataUsage = mkOption {
type = types.int;
default = 50;
description = "Metadata usage threshold for balance";
};
};
# Mountpoint-based configuration
mountpoints = mkOption {
type = types.attrsOf (types.submodule {
options = {
subvolume = mkOption {
type = types.nullOr types.str;
default = null;
description = "Subvolume name. If null, uses default subvolume.";
};
compression = mkOption {
type = types.str;
default = "zstd";
description = "Compression algorithm (zstd, lzo, lz4, none)";
};
autodefrag = mkOption {
type = types.bool;
default = false;
description = "Enable automatic defragmentation";
};
extraOptions = mkOption {
type = types.listOf types.str;
default = [];
description = "Additional mount options";
};
};
});
default = {};
description = "Mountpoint configurations for this filesystem";
};
};
});
default = {};
description = "Btrfs filesystems configuration";
};
};
config = mkIf cfg.enable {
environment.systemPackages = with pkgs; [
btrfs-progs
compsize
];
# Generate fileSystems configuration from mountpoints
fileSystems = mkMerge (flatten (mapAttrsToList (device: fsCfg:
mapAttrsToList (mountpoint: mountCfg:
{
${mountpoint} = {
device = device;
fsType = "btrfs";
options =
(optional (mountCfg.subvolume != null) "subvol=${mountCfg.subvolume}") ++
[ "compress=${mountCfg.compression}" ] ++
(optional mountCfg.autodefrag "autodefrag") ++
mountCfg.extraOptions;
};
}
) fsCfg.mountpoints
) cfg.filesystems));
# Configure scrub service using NixOS built-in
services.btrfs.autoScrub = mkIf (any (fs: fs.scrub.enable) (attrValues cfg.filesystems)) {
enable = true;
interval = "weekly"; # TODO: Make this configurable per filesystem
fileSystems = attrNames (filterAttrs (_: fs: fs.scrub.enable) cfg.filesystems);
};
# Configure beesd for filesystems with deduplication enabled
services.beesd.filesystems = mapAttrs' (device: fsCfg:
nameValuePair (replaceStrings ["/"] ["_"] (replaceStrings ["-"] ["_"] device)) {
spec = device;
hashTableSizeMB = fsCfg.deduplication.hashTableSizeMB;
verbosity = fsCfg.deduplication.verbosity;
}
) (filterAttrs (_: fs: fs.deduplication.enable) cfg.filesystems);
# Custom balance services for filesystems with balance enabled
systemd.services = mkMerge (mapAttrsToList (device: fsCfg: mkIf fsCfg.balance.enable {
"btrfs-balance-${replaceStrings ["/"] ["-"] (replaceStrings ["-"] ["_"] device)}" = {
description = "Balance btrfs filesystem ${device}";
script = ''
${pkgs.btrfs-progs}/bin/btrfs balance start \
-dusage=${toString fsCfg.balance.dataUsage} \
-musage=${toString fsCfg.balance.metadataUsage} \
${device}
'';
serviceConfig = {
Type = "oneshot";
Nice = 19;
IOSchedulingClass = "idle";
};
};
}) cfg.filesystems);
# Balance timers
systemd.timers = mkMerge (mapAttrsToList (device: fsCfg: mkIf fsCfg.balance.enable {
"btrfs-balance-${replaceStrings ["/"] ["-"] (replaceStrings ["-"] ["_"] device)}" = {
description = "Periodic balance for ${device}";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = fsCfg.balance.interval;
Persistent = true;
};
};
}) cfg.filesystems);
};
}

View File

@@ -1,120 +0,0 @@
{ config, lib, pkgs, ... }:
with lib;
let
# Extract the set-environment path that nix-darwin generates
setEnvironmentPath = "${config.system.build.setEnvironment}";
in
{
config = {
# Salt manages /etc/bashrc, /etc/zshrc, /etc/zshenv
# nix-darwin writes to .local variants for nix-specific configuration
# Disable nix-darwin from managing the main shell files
environment.etc."bashrc".enable = false;
environment.etc."zshrc".enable = false;
environment.etc."zshenv".enable = false;
environment.etc."zprofile".enable = false;
# Create .local files with nix environment setup
environment.etc."bash.local".text = ''
# Nix environment setup
if [ -z "$__NIX_DARWIN_SET_ENVIRONMENT_DONE" ]; then
. ${setEnvironmentPath}
fi
'';
environment.etc."zshrc.local".text = ''
# Nix environment setup (already done in zshenv.local)
'';
environment.etc."zshenv.local".text = ''
# Nix environment setup
if [[ -o rcs ]]; then
if [ -z "''${__NIX_DARWIN_SET_ENVIRONMENT_DONE-}" ]; then
. ${setEnvironmentPath}
fi
# Tell zsh how to find installed completions
for p in ''${(z)NIX_PROFILES}; do
fpath=($p/share/zsh/site-functions $p/share/zsh/$ZSH_VERSION/functions $p/share/zsh/vendor-completions $fpath)
done
fi
'';
time.timeZone = "America/Los_Angeles";
# System preferences
system.defaults = {
# Custom keyboard shortcuts
CustomUserPreferences = {
"com.apple.symbolichotkeys" = {
AppleSymbolicHotKeys = {
# Screenshot - Capture entire screen (Cmd+Ctrl+3)
"28" = {
enabled = true;
value = {
parameters = [ 51 20 1310720 ];
type = "standard";
};
};
# Screenshot - Capture selected portion (Cmd+Ctrl+4)
"30" = {
enabled = true;
value = {
parameters = [ 52 21 1310720 ];
type = "standard";
};
};
# Screenshot - Show screenshot toolbar (Cmd+Ctrl+5)
"184" = {
enabled = true;
value = {
parameters = [ 53 23 1310720 ];
type = "standard";
};
};
};
};
};
};
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

@@ -6,13 +6,10 @@ with lib;
imports = [ imports = [
./audio ./audio
./bluetooth ./bluetooth
./btrfs
./desktop ./desktop
./kodi ./kodi
./nfs-mounts ./nfs-mounts
./nvidia
./printing ./printing
./remote-build
./spotifyd ./spotifyd
./users ./users
./virtualisation ./virtualisation

View File

@@ -9,9 +9,7 @@ with lib;
x11 = mkOption { type = types.bool; default = false; description = "Enable X11 support."; }; x11 = mkOption { type = types.bool; default = false; description = "Enable X11 support."; };
wayland = mkOption { type = types.bool; default = false; description = "Enable Wayland support."; }; wayland = mkOption { type = types.bool; default = false; description = "Enable Wayland support."; };
kde = mkOption { type = types.bool; default = false; description = "Enable KDE."; }; kde = mkOption { type = types.bool; default = false; description = "Enable KDE."; };
gaming = { gaming = mkOption { type = types.bool; default = false; description = "Enable gaming support."; };
enable = mkOption { type = types.bool; default = false; description = "Enable gaming support."; };
};
sddm = mkOption { type = types.bool; default = false; description = "Enable SDDM greeter."; }; sddm = mkOption { type = types.bool; default = false; description = "Enable SDDM greeter."; };
}; };

View File

@@ -6,30 +6,13 @@ let
cfg = config.roles.desktop; cfg = config.roles.desktop;
in in
{ {
config = mkMerge [ config = mkIf (cfg.enable && cfg.gaming) {
(mkIf (cfg.enable && cfg.gaming.enable) { environment.systemPackages = with pkgs; [
environment.systemPackages = with pkgs; [ steam
lutris lutris
moonlight moonlight
];
# Emulators # Possibly other gaming specific services or settings
dolphin-emu };
# Re-enabled in 25.11 after binary build was fixed
dolphin-emu-primehack
# Experimenting with just using the steam version + downloading
# indiviudal cores
#retroarch-full
ryubing
];
programs.steam = {
enable = true;
remotePlay.openFirewall = true;
dedicatedServer.openFirewall = true;
localNetworkGameTransfers.openFirewall = true;
};
})
];
} }

View File

@@ -16,22 +16,5 @@ in
programs.dconf.enable = true; programs.dconf.enable = true;
services.gnome.gnome-keyring.enable = true; services.gnome.gnome-keyring.enable = true;
programs.kdeconnect.enable = true; programs.kdeconnect.enable = true;
# XDG Desktop Portal for default application handling in non-KDE environments
xdg.portal = {
enable = true;
extraPortals = with pkgs; [
kdePackages.xdg-desktop-portal-kde # For KDE application integration
xdg-desktop-portal-gtk # Fallback for GTK applications
];
config = {
common = {
default = "kde";
};
i3 = {
default = ["kde" "gtk"];
};
};
};
}; };
} }

View File

@@ -1,51 +0,0 @@
{ lib, config, pkgs, ... }:
with lib;
let
cfg = config.roles.desktop;
in
{
options.roles.desktop.steamos = {
enable = mkEnableOption "SteamOS (Jovian) configuration";
autoStart = mkOption {
type = types.bool;
default = false;
description = "Automatically start Steam Deck UI on boot";
};
user = mkOption {
type = types.str;
default = "johno";
description = "User to run Steam as";
};
desktopSession = mkOption {
type = types.nullOr types.str;
default = null;
description = "Desktop session to launch when switching to Desktop Mode";
};
enableDeckyLoader = mkOption {
type = types.bool;
default = true;
description = "Enable Decky Loader plugin system";
};
};
config = mkIf (cfg.enable && cfg.steamos.enable) {
jovian.steam = {
enable = true;
autoStart = cfg.steamos.autoStart;
user = cfg.steamos.user;
desktopSession = cfg.steamos.desktopSession;
};
jovian.decky-loader.enable = cfg.steamos.enableDeckyLoader;
environment.systemPackages = with pkgs; [
maliit-keyboard
];
};
}

View File

@@ -12,27 +12,8 @@ in
windowManager.i3 = { windowManager.i3 = {
enable = true; enable = true;
extraPackages = with pkgs; [ extraPackages = with pkgs; [ dmenu i3status i3lock ];
dmenu
i3status
i3lock
polkit_gnome # GNOME polkit authentication agent (more stable with i3)
picom # Compositor for smooth rendering (important for Nvidia)
networkmanagerapplet # NetworkManager system tray applet
ddcutil # DDC/CI monitor control for brightness
];
}; };
}; };
# Enable DDC/CI support for monitor brightness control
boot.kernelModules = [ "i2c-dev" ];
# Add ddcutil udev rules and user permissions
hardware.i2c.enable = true;
# Install ddcutil system-wide
environment.systemPackages = with pkgs; [
ddcutil
];
}; };
} }

View File

@@ -14,23 +14,6 @@ in
wayland = mkOption { wayland = mkOption {
default = true; default = true;
}; };
jellyfinScaleFactor = mkOption {
type = types.nullOr types.float;
default = null;
description = "Scale factor for Jellyfin Media Player UI (e.g., 1.5 for 150% scaling)";
};
appLauncherServer = {
enable = mkOption {
type = types.bool;
default = true;
description = "Enable HTTP app launcher server for remote control";
};
port = mkOption {
type = types.int;
default = 8081;
description = "Port for the app launcher HTTP server";
};
};
}; };
@@ -42,28 +25,6 @@ in
steam-library steam-library
youtube youtube
]); ]);
jellyfinMediaPlayerPkg =
if cfg.jellyfinScaleFactor != null
then pkgs.symlinkJoin {
name = "jellyfin-media-player-scaled";
paths = [ pkgs.jellyfin-media-player ];
nativeBuildInputs = [ pkgs.makeWrapper ];
postBuild = ''
mkdir -p $out/bin
rm -f $out/bin/jellyfin-desktop
makeWrapper ${pkgs.jellyfin-media-player}/bin/jellyfin-desktop $out/bin/jellyfin-desktop \
--add-flags "--tv --scale-factor ${toString cfg.jellyfinScaleFactor}"
# Update .desktop file to include scale factor and TV mode arguments
mkdir -p $out/share/applications
rm -f $out/share/applications/org.jellyfin.JellyfinDesktop.desktop
substitute ${pkgs.jellyfin-media-player}/share/applications/org.jellyfin.JellyfinDesktop.desktop \
$out/share/applications/org.jellyfin.JellyfinDesktop.desktop \
--replace-fail "Exec=jellyfin-desktop" "Exec=jellyfin-desktop --tv --scale-factor ${toString cfg.jellyfinScaleFactor}"
'';
}
else pkgs.jellyfin-media-player;
in mkIf cfg.enable in mkIf cfg.enable
{ {
users.extraUsers.kodi = { users.extraUsers.kodi = {
@@ -72,46 +33,24 @@ in
}; };
networking.firewall = { networking.firewall = {
allowedTCPPorts = [ 8080 ] ++ optional cfg.appLauncherServer.enable cfg.appLauncherServer.port; allowedTCPPorts = [ 8080 ];
allowedUDPPorts = [ 8080 ]; allowedUDPPorts = [ 8080 ];
}; };
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
jellyfinMediaPlayerPkg
kodiPkg kodiPkg
wget wget
firefox ];
] ++ optional cfg.appLauncherServer.enable pkgs.custom.app-launcher-server;
nixpkgs.config.permittedInsecurePackages = lib.warn
"Allowing insecure package qtwebengine-5.15.19 as a jellyfin-media-player dependency. Remove this once jellyfin is updated to use qt6"
[
"qtwebengine-5.15.19"
];
programs.kdeconnect.enable = true; programs.kdeconnect.enable = true;
systemd.user.services = mkIf cfg.appLauncherServer.enable { services = if cfg.autologin then {
app-launcher-server = { displayManager = {
description = "HTTP App Launcher Server"; autoLogin.enable = true;
wantedBy = [ "graphical-session.target" ]; autoLogin.user = "kodi";
after = [ "graphical-session.target" ]; defaultSession = "kodi";
serviceConfig = { sessionData.autologinSession = "plasma";
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:${kodiPkg}/bin:/run/current-system/sw/bin"
];
}; };
}; } else {};
};
services.displayManager = mkIf cfg.autologin {
autoLogin.enable = true;
autoLogin.user = "kodi";
defaultSession = "plasma";
};
}; };
} }

View File

@@ -1,20 +0,0 @@
{ lib, config, pkgs, ... }:
with lib;
let
cfg = config.roles.nvidia;
in
{
options.roles.nvidia = {
enable = mkEnableOption "Enable the nvidia role";
};
config = mkIf cfg.enable {
environment.systemPackages = with pkgs; [
libva-utils
nvidia-vaapi-driver
nvtopPackages.nvidia
];
};
}

View File

@@ -22,15 +22,9 @@ in
hardware.printers.ensurePrinters = [{ hardware.printers.ensurePrinters = [{
name = "MFC-L8900CDW_series"; name = "MFC-L8900CDW_series";
deviceUri = "ipp://brother.oglehome/ipp/print"; deviceUri = "dnssd://Brother%20MFC-L8900CDW%20series._ipp._tcp.local/?uuid=e3248000-80ce-11db-8000-b422006699d8";
model = "everywhere"; model = "everywhere";
}]; }];
hardware.printers.ensureDefaultPrinter = "MFC-L8900CDW_series"; hardware.printers.ensureDefaultPrinter = "MFC-L8900CDW_series";
# Fix ensure-printers service to wait for network availability
systemd.services.ensure-printers = {
after = [ "cups.service" "network-online.target" ];
wants = [ "cups.service" "network-online.target" ];
};
}; };
} }

View File

@@ -1,132 +0,0 @@
{ lib, config, pkgs, ... }:
with lib;
let
cfg = config.roles.remote-build;
in
{
options.roles.remote-build = {
enableBuilder = mkOption {
type = types.bool;
default = false;
description = "Enable this machine as a remote build host for other machines";
};
builderUser = mkOption {
type = types.str;
default = "nix-builder";
description = "User account for remote builders to connect as";
};
builders = mkOption {
type = types.listOf (types.submodule {
options = {
hostName = mkOption {
type = types.str;
description = "Hostname or IP address of the build machine";
};
systems = mkOption {
type = types.listOf types.str;
default = [ "x86_64-linux" ];
description = "Supported systems";
};
maxJobs = mkOption {
type = types.int;
default = 8;
description = "Maximum number of parallel build jobs";
};
speedFactor = mkOption {
type = types.int;
default = 2;
description = "Speed factor compared to local building (higher = prefer remote)";
};
supportedFeatures = mkOption {
type = types.listOf types.str;
default = [ "nixos-test" "benchmark" "big-parallel" "kvm" ];
description = "Supported build features";
};
sshUser = mkOption {
type = types.str;
default = "nix-builder";
description = "SSH user for connecting to the builder";
};
sshKey = mkOption {
type = types.nullOr types.path;
default = null;
description = "Path to SSH private key for authentication";
};
};
});
default = [];
description = "List of remote build machines to use";
};
fallbackToLocalBuild = mkOption {
type = types.bool;
default = true;
description = "Fallback to local building if remote builders are unavailable";
};
};
config = mkMerge [
# Builder host configuration
(mkIf cfg.enableBuilder {
# Create dedicated builder user
users.users.${cfg.builderUser} = {
isSystemUser = true;
group = cfg.builderUser;
description = "Nix remote build user";
home = "/var/lib/${cfg.builderUser}";
createHome = true;
shell = pkgs.bashInteractive;
openssh.authorizedKeys.keyFiles = []; # Will be populated by client machines
};
users.groups.${cfg.builderUser} = {};
# Ensure home directory has correct permissions
systemd.tmpfiles.rules = [
"d /var/lib/${cfg.builderUser} 0700 ${cfg.builderUser} ${cfg.builderUser} -"
];
# Allow builder user to perform builds
nix.settings.trusted-users = [ cfg.builderUser ];
# Allow remote builds
services.openssh.enable = true;
# Ensure nix-daemon is accessible
nix.settings.allowed-users = [ "*" ];
})
# Client configuration (machines using remote builders)
(mkIf (cfg.builders != []) {
nix.buildMachines = map (builder: {
hostName = builder.hostName;
systems = builder.systems;
maxJobs = builder.maxJobs;
speedFactor = builder.speedFactor;
supportedFeatures = builder.supportedFeatures;
sshUser = builder.sshUser;
sshKey = builder.sshKey;
}) cfg.builders;
nix.distributedBuilds = true;
# Use substitutes from remote builders
nix.extraOptions = ''
builders-use-substitutes = true
'';
# Fallback to local build if remote unavailable
nix.settings.fallback = cfg.fallbackToLocalBuild;
})
];
}

View File

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

View File

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

View File

@@ -1,660 +0,0 @@
# 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