Nix-Vibe public snapshot (squashed history)
Current state of main at 0240060 feat(hp-laptop): install TeleportFling from its flake. History intentionally collapsed to a single commit; this repo mirrors only the latest state.
This commit is contained in:
@@ -0,0 +1,82 @@
|
|||||||
|
---
|
||||||
|
name: flake-update
|
||||||
|
description: 'Safely update flake inputs: ensure a clean committed/pushed working tree, run nix flake update, then dry-build EVERY host before committing flake.lock. Use when the user asks to update the flake, refresh flake.lock, bump inputs, or upgrade nixpkgs/dependencies.'
|
||||||
|
---
|
||||||
|
|
||||||
|
# Flake Update
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
- When the user asks to "update the flake", "bump inputs", "refresh flake.lock", or "update nixpkgs"
|
||||||
|
- Before a planned fleet-wide input upgrade
|
||||||
|
|
||||||
|
## Procedure
|
||||||
|
|
||||||
|
### 1. Clean Working Tree (MANDATORY)
|
||||||
|
|
||||||
|
Never run `nix flake update` with uncommitted changes — mixing unrelated edits with a lockfile bump makes rollback painful.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git status
|
||||||
|
```
|
||||||
|
|
||||||
|
- If there are uncommitted changes: validate and commit them first (follow the **nix-flake-rebuild** skill: stage, `nixpkgs-fmt .`, `nix flake check`, dry-build affected hosts, commit).
|
||||||
|
- Then push everything to the remote:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git push
|
||||||
|
```
|
||||||
|
|
||||||
|
Only proceed when `git status` is clean and the branch is fully pushed.
|
||||||
|
|
||||||
|
### 2. Update Inputs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# All inputs
|
||||||
|
nix flake update
|
||||||
|
|
||||||
|
# OR a single input, if the user asked for one
|
||||||
|
nix flake update <input-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Flake Check
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nix flake check
|
||||||
|
```
|
||||||
|
|
||||||
|
Fix any evaluation errors before building. Input bumps often surface deprecated/renamed NixOS and Home Manager options — check warnings and refer to release notes for replacements.
|
||||||
|
|
||||||
|
### 4. Dry-Build EVERY Host
|
||||||
|
|
||||||
|
An input update affects the whole fleet — dry-build every host, no exceptions. Generate the host list dynamically from the flake so new hosts are never missed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for host in $(nix eval .#nixosConfigurations --apply 'attrs: builtins.concatStringsSep " " (builtins.attrNames attrs)' --raw); do
|
||||||
|
echo "=== $host ==="
|
||||||
|
nixos-rebuild dry-build --flake .#$host || break
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
If any host fails, do NOT commit. Either fix the breakage or roll back (see below).
|
||||||
|
|
||||||
|
### 5. Commit & Push Lockfile (only if ALL hosts pass)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add flake.lock
|
||||||
|
git commit -m "flake: update inputs"
|
||||||
|
git push
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
Because step 1 guaranteed a clean, pushed tree, the previous lockfile is always recoverable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git restore --source=HEAD~1 flake.lock
|
||||||
|
```
|
||||||
|
|
||||||
|
Or check out an older known-good lockfile from history.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- This skill only **validates** the update. Actually applying it (`nixos-rebuild switch`) is a separate, per-host step.
|
||||||
|
- Review the `git diff flake.lock` before committing if the user wants to know which inputs moved.
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
---
|
||||||
|
name: nix-flake-rebuild
|
||||||
|
description: 'Validate Nix Flake changes end-to-end: stage, check, dry-build, and commit. Use after ANY change to .nix files, flake inputs, or module imports. Use when the user asks to apply, test, validate, rebuild, or commit NixOS config changes.'
|
||||||
|
argument-hint: '[hostname]'
|
||||||
|
---
|
||||||
|
|
||||||
|
# Nix Flake Rebuild Validation
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
- After ANY modification to `.nix` files, `flake.nix`, or module imports
|
||||||
|
- Before applying configs with `nixos-rebuild switch`
|
||||||
|
- When the user asks to "test", "validate", "rebuild", or "apply" changes
|
||||||
|
- After adding new files, modules, or hosts
|
||||||
|
|
||||||
|
## Critical Pre-Check: Git Staging
|
||||||
|
|
||||||
|
Nix flakes ONLY see Git-tracked files. If you created new files or directories, **you must stage them first**:
|
||||||
|
```bash
|
||||||
|
git add <new-file> <new-directory/>
|
||||||
|
```
|
||||||
|
Failure to do this will cause `nix flake check` to fail with confusing "file not found" errors.
|
||||||
|
|
||||||
|
## Procedure
|
||||||
|
|
||||||
|
### 1. Sync Documentation (if needed)
|
||||||
|
If software was added/removed or a host role changed, update the software inventory table and host overview in `README.md`.
|
||||||
|
|
||||||
|
### 2. Stage ALL Changes
|
||||||
|
```bash
|
||||||
|
git add -A
|
||||||
|
```
|
||||||
|
This ensures flake evaluation can see every file.
|
||||||
|
|
||||||
|
### 3. Format Code
|
||||||
|
```bash
|
||||||
|
nixpkgs-fmt .
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Run Flake Check
|
||||||
|
```bash
|
||||||
|
nix flake check
|
||||||
|
```
|
||||||
|
Fix any errors before proceeding. Common issues:
|
||||||
|
- Missing `git add` on new files
|
||||||
|
- Deprecated NixOS/Home Manager options
|
||||||
|
- Syntax errors in `.nix` files
|
||||||
|
|
||||||
|
### 5. Dry-Build for Affected Hosts
|
||||||
|
```bash
|
||||||
|
nixos-rebuild dry-build --flake .#<hostname>
|
||||||
|
```
|
||||||
|
Run for EACH host affected by the changes. If unsure which hosts are affected, run for all. Generate the host list dynamically from the flake so new hosts are never missed:
|
||||||
|
```bash
|
||||||
|
for host in $(nix eval .#nixosConfigurations --apply 'attrs: builtins.concatStringsSep " " (builtins.attrNames attrs)' --raw); do
|
||||||
|
echo "=== $host ==="
|
||||||
|
nixos-rebuild dry-build --flake .#$host || break
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Commit (only if ALL checks pass)
|
||||||
|
```bash
|
||||||
|
git commit -m "descriptive message"
|
||||||
|
```
|
||||||
|
Do NOT commit if `nix flake check` or any dry-build failed.
|
||||||
|
|
||||||
|
## Host Reference
|
||||||
|
|
||||||
|
> Informational only — the dry-build loop above generates the host list dynamically and is authoritative. Keep this table roughly in sync when adding hosts (used to check which overlay a host gets).
|
||||||
|
|
||||||
|
| Host | Type | Overlay |
|
||||||
|
|------|------|---------|
|
||||||
|
| `x1carbon` | Desktop/laptop | Full (`overlays`: CUDA/NDI/stable) |
|
||||||
|
| `caitlin-x1` | Desktop/laptop | `desktopOverlays` |
|
||||||
|
| `x470` | Desktop/laptop | `desktopOverlays` |
|
||||||
|
| `mary-x270` | Desktop | `desktopOverlays` |
|
||||||
|
| `richmond-server` | Server | `serverOverlays` |
|
||||||
|
| `homeserver-1` | Server | `serverOverlays` |
|
||||||
|
| `mcf-server` | Server | `serverOverlays` |
|
||||||
|
| `mcf-stream` | Desktop/laptop | `desktopOverlays` |
|
||||||
|
| `hp-laptop` | Desktop/laptop | `desktopOverlays` |
|
||||||
|
|
||||||
|
## Post-Validation
|
||||||
|
After a successful commit, to actually apply:
|
||||||
|
```bash
|
||||||
|
# Local
|
||||||
|
sudo nixos-rebuild switch --flake .#<hostname>
|
||||||
|
|
||||||
|
# Remote
|
||||||
|
nixos-rebuild switch --target-host <user@host> --flake .#<hostname> --use-remote-sudo
|
||||||
|
```
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
---
|
||||||
|
name: nix-module
|
||||||
|
description: 'Create or update a NixOS or Home Manager module following Nix-Vibe conventions. Use when adding a new service, desktop app, hardware support, or shared configuration module. Use when the user asks to add a package, service, or application module.'
|
||||||
|
argument-hint: '<module-name> [service|desktop|hardware|core]'
|
||||||
|
---
|
||||||
|
|
||||||
|
# Nix Module Creation
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
- Adding a new service (e.g., a new containerized app, daemon, or web service)
|
||||||
|
- Adding a new desktop application module
|
||||||
|
- Adding hardware support (e.g., new device driver, firmware)
|
||||||
|
- Creating a new shared core module
|
||||||
|
- The user asks to "add support for X" or "create a module for X"
|
||||||
|
|
||||||
|
## Module Pattern
|
||||||
|
|
||||||
|
All service/feature modules MUST follow this pattern:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
options.services.<name> = {
|
||||||
|
enable = lib.mkEnableOption "description of the service";
|
||||||
|
# Additional options as needed
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf config.services.<name>.enable {
|
||||||
|
# Configuration here
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Reference implementation: [modules/services/immich.nix](../../modules/services/immich.nix)
|
||||||
|
|
||||||
|
## Procedure
|
||||||
|
|
||||||
|
### 1. Determine Module Type
|
||||||
|
|
||||||
|
| Type | Directory | Example |
|
||||||
|
|------|-----------|---------|
|
||||||
|
| Service (daemon, container, webapp) | `modules/services/` | immich, jellyfin, ntfy |
|
||||||
|
| Desktop app | `modules/desktop/apps/` | soundux, opencode, freeshow |
|
||||||
|
| Desktop environment | `modules/desktop/` | gnome.nix, gui.nix |
|
||||||
|
| Hardware support | `modules/hardware/` | fingerprint.nix |
|
||||||
|
| Core system config | `modules/core/` | common.nix, settings.nix, fonts.nix |
|
||||||
|
|
||||||
|
### 2. Create the Module File
|
||||||
|
|
||||||
|
#### Service Module Template
|
||||||
|
```nix
|
||||||
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
options.services.<service-name> = {
|
||||||
|
enable = lib.mkEnableOption "<Service Description>";
|
||||||
|
port = lib.mkOption {
|
||||||
|
type = lib.types.port;
|
||||||
|
default = <default-port>;
|
||||||
|
description = "Port for <service>";
|
||||||
|
};
|
||||||
|
# Add data directories, user config, etc. as needed
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf config.services.<service-name>.enable {
|
||||||
|
# Service-specific configuration
|
||||||
|
|
||||||
|
# For stateful services, ensure data directories exist
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d /var/lib/<service> 0700 <user> <group> -"
|
||||||
|
];
|
||||||
|
|
||||||
|
# Open firewall if needed
|
||||||
|
networking.firewall.allowedTCPPorts = [ config.services.<service-name>.port ];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Desktop App Module Template
|
||||||
|
```nix
|
||||||
|
{ pkgs, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
<package-name>
|
||||||
|
];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Register in Host Configuration
|
||||||
|
|
||||||
|
Import the module in the target host's `configuration.nix`:
|
||||||
|
```nix
|
||||||
|
imports = [
|
||||||
|
# ...existing imports...
|
||||||
|
../../modules/services/<module-name>.nix
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
Then enable it:
|
||||||
|
```nix
|
||||||
|
services.<service-name>.enable = true;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Update README.md
|
||||||
|
Add the new software to the Software Inventory table with the appropriate host columns.
|
||||||
|
|
||||||
|
### 5. Validate
|
||||||
|
Run the full validation workflow. See [nix-flake-rebuild](../nix-flake-rebuild/SKILL.md).
|
||||||
|
|
||||||
|
## Key Conventions
|
||||||
|
|
||||||
|
### Secrets
|
||||||
|
Never hardcode secrets. Use SOPS:
|
||||||
|
```nix
|
||||||
|
sops.secrets."<host>/<secret-name>" = {
|
||||||
|
owner = "<user>";
|
||||||
|
group = "users";
|
||||||
|
mode = "0440";
|
||||||
|
};
|
||||||
|
```
|
||||||
|
See [docs/sops-secrets.md](../../docs/sops-secrets.md) for details.
|
||||||
|
|
||||||
|
### Overlay Awareness
|
||||||
|
- If a package needs NVIDIA/CUDA or NDI, it should only be added to `x1carbon` (the only host using the full `overlays` set)
|
||||||
|
- Desktops/laptops use `desktopOverlays` (`stable` without CUDA); servers use `serverOverlays`
|
||||||
|
|
||||||
|
### Module Composition
|
||||||
|
- Core modules (`modules/core/common.nix`) apply to ALL hosts
|
||||||
|
- Desktop modules only apply to GUI hosts
|
||||||
|
- Service modules are imported per-host as needed
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
---
|
||||||
|
name: nix-new-host
|
||||||
|
description: 'Scaffold a new NixOS host in this flake: create host directory, config files, wire into flake.nix, and set up Home Manager users. Use when adding a new machine (desktop, laptop, or server) to the Nix-Vibe configuration.'
|
||||||
|
argument-hint: '<hostname> [desktop|server]'
|
||||||
|
---
|
||||||
|
|
||||||
|
# New Host Scaffolding
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
- Adding a brand new machine to the Nix-Vibe configuration
|
||||||
|
- User asks to "add a host", "create a new machine config", or "scaffold a server/laptop"
|
||||||
|
- Migrating a new device into this flake-based setup
|
||||||
|
|
||||||
|
## Procedure
|
||||||
|
|
||||||
|
### 1. Gather Information
|
||||||
|
Before creating files, confirm with the user:
|
||||||
|
- **Hostname** (e.g., `new-laptop`)
|
||||||
|
- **Type**: Desktop/laptop (needs GUI) or Server (headless, CLI only)
|
||||||
|
- **Users** who will have Home Manager configs on this host
|
||||||
|
- **Any special hardware** (NVIDIA GPU, fingerprint sensor, etc.)
|
||||||
|
- **Any services** this host should run
|
||||||
|
|
||||||
|
### 2. Create Host Directory
|
||||||
|
```bash
|
||||||
|
mkdir -p hosts/<hostname>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Create Required Host Files
|
||||||
|
|
||||||
|
#### `hosts/<hostname>/hardware-configuration.nix`
|
||||||
|
Generate on the target machine after NixOS install:
|
||||||
|
```bash
|
||||||
|
nixos-generate-config --show-hardware-config > hosts/<hostname>/hardware-configuration.nix
|
||||||
|
```
|
||||||
|
If the target machine isn't available yet, create a minimal placeholder:
|
||||||
|
```nix
|
||||||
|
# hardware-configuration.nix for <hostname>
|
||||||
|
# Generated placeholder — run nixos-generate-config on the target machine
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
imports = [ ];
|
||||||
|
boot.initrd.availableKernelModules = [ ];
|
||||||
|
boot.initrd.kernelModules = [ ];
|
||||||
|
boot.kernelModules = [ ];
|
||||||
|
fileSystems."/" = {
|
||||||
|
device = "/dev/disk/by-label/nixos";
|
||||||
|
fsType = "ext4";
|
||||||
|
};
|
||||||
|
swapDevices = [ ];
|
||||||
|
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
|
||||||
|
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `hosts/<hostname>/disko-config.nix`
|
||||||
|
Copy from an existing host of the same type (desktop vs server) and adjust disk layout:
|
||||||
|
```bash
|
||||||
|
cp hosts/x1carbon/disko-config.nix hosts/<hostname>/disko-config.nix
|
||||||
|
# Then edit to match the target disk layout
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `hosts/<hostname>/configuration.nix`
|
||||||
|
Use the appropriate template below.
|
||||||
|
|
||||||
|
**Desktop/laptop template:**
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
inputs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
./hardware-configuration.nix
|
||||||
|
../../modules/core/common.nix
|
||||||
|
../../modules/desktop/gui.nix
|
||||||
|
(import ../../modules/storage/disko.nix {
|
||||||
|
inherit inputs lib config;
|
||||||
|
diskoConfigPath = ./disko-config.nix;
|
||||||
|
})
|
||||||
|
../../modules/desktop/gnome.nix
|
||||||
|
../../modules/core/management.nix
|
||||||
|
../../modules/core/podman.nix
|
||||||
|
../../modules/core/dev.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
networking.hostName = "<hostname>";
|
||||||
|
|
||||||
|
hardware.graphics.enable = true;
|
||||||
|
hardware.graphics.enable32Bit = true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Server template:**
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
inputs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
../../modules/core/common.nix
|
||||||
|
(import ../../modules/storage/disko.nix {
|
||||||
|
inherit inputs lib config;
|
||||||
|
diskoConfigPath = ./disko-config.nix;
|
||||||
|
})
|
||||||
|
./hardware-configuration.nix
|
||||||
|
../../modules/core/podman.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
networking.hostName = "<hostname>";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Wire into `flake.nix`
|
||||||
|
|
||||||
|
#### Add users in `hostUsers`:
|
||||||
|
```nix
|
||||||
|
hostUsers = {
|
||||||
|
# ...existing hosts...
|
||||||
|
<hostname> = [ "user1" "user2" ];
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Add nixosConfiguration:
|
||||||
|
If **desktop/laptop** (uses full overlays):
|
||||||
|
```nix
|
||||||
|
nixosConfigurations = {
|
||||||
|
# ...existing configs...
|
||||||
|
<hostname> = mkNixosSystem "<hostname>" { };
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
If **server** (uses serverOverlays):
|
||||||
|
```nix
|
||||||
|
nixosConfigurations = {
|
||||||
|
# ...existing configs...
|
||||||
|
<hostname> = mkNixosSystem "<hostname>" {
|
||||||
|
hostOverlays = serverOverlays;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Create Home Manager User Configs (if needed)
|
||||||
|
If the host has users without existing Home Manager configs, create:
|
||||||
|
```bash
|
||||||
|
mkdir -p home-manager/users
|
||||||
|
```
|
||||||
|
|
||||||
|
Minimal user template (`home-manager/users/<username>.nix`):
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
inputs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
home-manager.users.<username> = {
|
||||||
|
home.username = "<username>";
|
||||||
|
home.homeDirectory = "/home/<username>";
|
||||||
|
home.stateVersion = "24.11";
|
||||||
|
|
||||||
|
programs.home-manager.enable = true;
|
||||||
|
programs.git.enable = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Update README.md
|
||||||
|
Add the new host to:
|
||||||
|
- The **Host Roles** section with role description and key features
|
||||||
|
- The **Software Inventory** table (add a column for the new host)
|
||||||
|
|
||||||
|
### 7. Register Host in nix-flake-rebuild Skill
|
||||||
|
The dry-build loop in [nix-flake-rebuild](../nix-flake-rebuild/SKILL.md) is dynamic (hosts are read from `nixosConfigurations`), so nothing breaks if you skip this — but add `<hostname>` to its **Host Reference** table (with the correct Type and Overlay — `serverOverlays` for servers, Full for desktops/laptops) to keep the overlay reference accurate.
|
||||||
|
|
||||||
|
### 8. Validate
|
||||||
|
Follow the [nix-flake-rebuild](../nix-flake-rebuild/SKILL.md) skill to validate all changes.
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Adding Desktop Apps
|
||||||
|
Import app modules in the host config:
|
||||||
|
```nix
|
||||||
|
imports = [
|
||||||
|
# ...existing imports...
|
||||||
|
../../modules/desktop/apps/soundux.nix
|
||||||
|
../../modules/desktop/apps/opencode.nix
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding Services
|
||||||
|
Import service modules and enable:
|
||||||
|
```nix
|
||||||
|
imports = [
|
||||||
|
# ...existing imports...
|
||||||
|
../../modules/services/immich.nix
|
||||||
|
];
|
||||||
|
services.immich-server.enable = true;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hardware-Specific Modules
|
||||||
|
```nix
|
||||||
|
imports = [
|
||||||
|
# ...existing imports...
|
||||||
|
../../modules/hardware/fingerprint.nix # if fingerprint reader
|
||||||
|
../../modules/hardware/nvidia.nix # if NVIDIA GPU
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
For an NVIDIA GPU, enable via the shared option (instead of hand-writing
|
||||||
|
`hardware.nvidia`):
|
||||||
|
```nix
|
||||||
|
my.hardware.nvidia = {
|
||||||
|
enable = true;
|
||||||
|
nvidiaSettings = true;
|
||||||
|
package = config.boot.kernelPackages.nvidiaPackages.legacy_580; # optional
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Shared Users (petere)
|
||||||
|
|
||||||
|
The admin user `petere` is defined once in `modules/core/users.nix` (imported via
|
||||||
|
`common.nix`). New hosts do **not** re-declare the full `users.users.petere`
|
||||||
|
block — override only what differs:
|
||||||
|
```nix
|
||||||
|
my.users.petere = {
|
||||||
|
hashedPasswordFile = config.sops.secrets."users/petere-password".path;
|
||||||
|
subUidStart = 165536; # optional rootless-podman range
|
||||||
|
subGidStart = 165536;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
`petere` also needs a Home Manager config in `hostUsers` (step 4).
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
# Nix build results
|
||||||
|
result*
|
||||||
|
|
||||||
|
# Tailscale authkey (unencrypted, should NOT be committed)
|
||||||
|
hosts/richmond-server/tailscale_authkey.txt
|
||||||
|
hosts/richmond-server/mcf-notices.env
|
||||||
|
hosts/richmond-server/castopod-container.env
|
||||||
|
hosts/richmond-server/castopod-api.env
|
||||||
|
hosts/richmond-server/pihole.env
|
||||||
|
|
||||||
|
# Local sops age keys (generated per-machine at deploy time)
|
||||||
|
# The public key goes in .sops.yaml, encrypted secrets go in secrets.yaml
|
||||||
|
/root/.config/sops/age/
|
||||||
|
/var/lib/sops-nix/
|
||||||
|
|
||||||
|
# Decrypted temporary sops files
|
||||||
|
.*decrypted*
|
||||||
|
|
||||||
|
extra-files/
|
||||||
|
extra-files/**
|
||||||
|
extra-files/
|
||||||
|
extra-files/**
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
creation_rules:
|
||||||
|
- path_regex: secrets\.yaml$
|
||||||
|
age: age145xh9ecu2hye2r9s9lqgap49vydttwyhhfc4x93juy9d78f92pnsucj8wg,
|
||||||
|
age17z3fuzlfmerpnsrum9g4sfmkmgghtlltfq07lp79uzugm5are3cs64dnqy,
|
||||||
|
age1wzt34k82v2shr443zqmkfu2la8ewdfszwg8s93mqh8m6n7mfd3ssfegyd0,
|
||||||
|
age1ge9zg2kz80xyq9xk94kc70g3d44j3lurpclgg8cel8u3skmsx4kss6ny8a,
|
||||||
|
age10p0dv642mdlwzt0xgrrz6udndu0yg939hletl2zxk558ycufxypqp6qcve,
|
||||||
|
age1kra9xjk5709jluhdwvn2x6jczx26z5z2ffygsf0q3p2tvqvnff4shn345g,
|
||||||
|
age1k23adf45f8ay5g65axc4v8ahfeuq2gg7fmjw3vhaf4zl30a0c52sw4fhza,
|
||||||
|
age16d5anx997as6syyzzj0cs70kvsknl2pjs4afx7l77fjpawp7vy2qnvm458,
|
||||||
|
age14kdra7c48a9262vxtsrmu09lhk6xt05hre560s89td46aadwpulq5esjqy,
|
||||||
|
age17sc2wm2zr8q84knuzr0jdhxasmt6wvvncgxuduqxz8h0juequfmqmw363q
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"signage.vscode-sops"
|
||||||
|
]
|
||||||
|
}
|
||||||
Vendored
+8
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"servers": {
|
||||||
|
"nixos": {
|
||||||
|
"command": "mcp-nixos",
|
||||||
|
"args": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+10
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"sops.ageKeyFile": "${env:HOME}/.config/sops/age/keys.txt",
|
||||||
|
"sops.encryptOnSave": true,
|
||||||
|
"sops.yaml.keyPaths": [
|
||||||
|
".sops.yaml"
|
||||||
|
],
|
||||||
|
"chat.tools.terminal.autoApprove": {
|
||||||
|
"nix": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# AGENTS.md — Nix-Vibe AI Agent Instructions
|
||||||
|
|
||||||
|
A Nix Flakes-based multi-host NixOS + Home Manager configuration repository.
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
- **Build/Test**: `nix flake check` (must `git add` new files first)
|
||||||
|
- **Task runner**: `just` (see `justfile`) — `just` deploys a host + restarts quickshell, `just check`, `just dry-build`, `just test`, `just restart-qs`
|
||||||
|
- **Format**: `nixpkgs-fmt .` (or `nix fmt` — the flake defines a `nixpkgs-fmt` formatter)
|
||||||
|
- **Dev shell**: `nix develop` (provides `sops`, `age`, `nixpkgs-fmt`)
|
||||||
|
- **Apply**: `sudo nixos-rebuild switch --flake .#<hostname>`
|
||||||
|
- **Test (no activation)**: `sudo nixos-rebuild test --flake .#<hostname>`
|
||||||
|
- **Remote apply**: `nixos-rebuild switch --target-host <host> --flake .#<hostname> --use-remote-sudo`
|
||||||
|
- **Dry-build**: `nixos-rebuild dry-build --flake .#<hostname>`
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
| Directory | Purpose |
|
||||||
|
|-----------|---------|
|
||||||
|
| `hosts/<name>/` | Per-machine configs (`configuration.nix`, `disko-config.nix`, `hardware-configuration.nix`) |
|
||||||
|
| `modules/core/` | Shared NixOS modules (common, fonts, settings, sops, podman, dev, users, management, known-hosts) |
|
||||||
|
| `modules/desktop/` | GUI desktop modules and app-specific modules |
|
||||||
|
| `modules/hardware/` | Hardware-specific modules (fingerprint, nvidia) |
|
||||||
|
| `modules/services/` | Service modules (immich, jellyfin, ntfy, paperless, immich-proxy) |
|
||||||
|
| `modules/storage/` | Disk configuration via Disko |
|
||||||
|
| `home-manager/modules/` | Shared Home Manager modules (zsh, gnome, firefox, kitty, etc.) |
|
||||||
|
| `home-manager/users/` | Per-user Home Manager configs |
|
||||||
|
| `flake.nix` | Entry point — defines `nixosConfigurations` via `mkNixosSystem` helper |
|
||||||
|
|
||||||
|
Host overview and software inventory: see [README.md](./README.md).
|
||||||
|
|
||||||
|
## Critical Rules
|
||||||
|
|
||||||
|
### Style
|
||||||
|
- **Language**: Nix — follow conventions used in the `nixpkgs` repository.
|
||||||
|
- **Formatter**: `nixpkgs-fmt .` (run before every commit).
|
||||||
|
|
||||||
|
### Git & Flakes
|
||||||
|
- **NEW FILES MUST BE STAGED** (`git add`) before `nix flake check` or `nixos-rebuild`. Flake evaluation only sees Git-tracked files.
|
||||||
|
- Do NOT commit until `nix flake check` AND a dry-build pass.
|
||||||
|
|
||||||
|
### Module Pattern
|
||||||
|
- New service modules follow the pattern: `lib.mkEnableOption` for `enable` + `lib.mkIf config.services.<name>.enable { ... }`.
|
||||||
|
- Example: [modules/services/immich.nix](./modules/services/immich.nix)
|
||||||
|
|
||||||
|
### Secrets
|
||||||
|
- Secrets are managed via [SOPS + age](./docs/sops-secrets.md) in `secrets.yaml`.
|
||||||
|
- Reference in host configs as `sops.secrets."<path>" = { ... };`.
|
||||||
|
- Rendered config files (e.g. container env files) use `sops.templates` — see [docs/sops-secrets.md](./docs/sops-secrets.md).
|
||||||
|
|
||||||
|
### Sudo
|
||||||
|
- Passwordless sudo is granted to **`petere` only** via `security.sudo.extraRules` (NOPASSWD) in `modules/core/common.nix`. All other wheel users must enter a password for `sudo`.
|
||||||
|
|
||||||
|
### Zsh
|
||||||
|
- `programs.zsh.enable = true` must be set in NixOS config for Zsh users.
|
||||||
|
- Shared Zsh config uses `home-manager.sharedModules` in `flake.nix` — do NOT use `home.file.".zshrc"`.
|
||||||
|
|
||||||
|
### Servers vs Desktops
|
||||||
|
- Servers (`richmond-server`, `homeserver-1`, `mcf-server`) use `serverOverlays` (lightweight).
|
||||||
|
- Desktops/laptops use `desktopOverlays` (`stable` without CUDA) by default.
|
||||||
|
- Only `x1carbon` uses the full `overlays` set with CUDA, NDI, stable packages (OBS Studio).
|
||||||
|
|
||||||
|
### Deprecated Options
|
||||||
|
- Pay attention to warnings about deprecated options during `nix flake check` or `nixos-rebuild`. Refer to NixOS/Home Manager release notes for updated options. Keeping configurations current prevents surprises on channel updates.
|
||||||
|
|
||||||
|
### Homepage Dashboard (gethomepage.dev)
|
||||||
|
- Dashboard definition for homeserver-1 lives in `hosts/homeserver-1/homepage.nix` (imported by the host config), NOT in `configuration.nix`.
|
||||||
|
- Full guide for adding machines/tabs/services: [docs/homepage-dashboard.md](./docs/homepage-dashboard.md).
|
||||||
|
- Widget group names MUST be unique — the credentialed proxy resolves config by leaf group name; duplicate names cause one machine's stats to render on another's tiles.
|
||||||
|
- Every Glances widget tile needs a `metric` field (`info`, `cpu`, `memory`, `fs:/`, `process`, ...) and `version = 4`; omitting `metric` throws `t.metric is undefined`.
|
||||||
|
- Tailscale widget `deviceid` must be the **numeric** device ID (from the Tailscale API), not the `...CNTRL` value.
|
||||||
|
- API keys are injected as `HOMEPAGE_VAR_*` env vars from the SOPS secret `homeserver-1/homepage-env` — never hardcode keys in `homepage.nix`.
|
||||||
|
- After editing `homepage.nix`, if changes don't appear after rebuild, restart the service: `sudo systemctl restart homepage-dashboard` (config files are only read at service start).
|
||||||
|
- Homepage is exposed on Tailscale only (port 8082).
|
||||||
|
|
||||||
|
## Post-Modification Workflow
|
||||||
|
|
||||||
|
1. Update `README.md` software inventory and host overview if relevant
|
||||||
|
2. When adding a new host, add it to the Host Reference table in `.github/skills/nix-flake-rebuild/SKILL.md` (informational; the dry-build loop is dynamic)
|
||||||
|
3. `git add` all modified/new files
|
||||||
|
4. `nix flake check`
|
||||||
|
5. `nixos-rebuild dry-build --flake .#<hostname>` for affected hosts
|
||||||
|
6. Commit only after both checks pass
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- [README.md](./README.md) — Full architecture, host roles, and entry point for all docs
|
||||||
|
- [docs/software-inventory.md](./docs/software-inventory.md) — Cross-reference matrix of software per host
|
||||||
|
- [docs/installation.md](./docs/installation.md) — nixos-anywhere deployment guide
|
||||||
|
- [docs/sops-secrets.md](./docs/sops-secrets.md) — SOPS/age secrets management
|
||||||
|
- [docs/borg-backup-setup.md](./docs/borg-backup-setup.md) — BorgBackup server setup (deprecated - migrated to Backrest)
|
||||||
|
- [docs/homepage-dashboard.md](./docs/homepage-dashboard.md) — Homepage dashboard: adding machines, tabs & services
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# Nix-Vibe: Multi-Host NixOS Configuration
|
||||||
|
|
||||||
|
This repository contains a modular NixOS and Home Manager configuration managed via Nix Flakes. The architecture is designed for reproducibility and consistency across diverse hardware, from powerful desktops to headless servers.
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
The system is built on a modular foundation, separating core configurations, hardware-specific settings, and user environments.
|
||||||
|
|
||||||
|
### Host Roles
|
||||||
|
|
||||||
|
* **`x1carbon`**: Advanced mobile workstation.
|
||||||
|
* **Role**: Portable audio engineering and personal document management.
|
||||||
|
* **Key Features**: Specialized audio control (X32-Edit, Mixing Station), Paperless-ngx, image editing (GIMP), desktop publishing (Scribus), 2-in-1 tablet mode (auto-rotation via accelerometer + on-screen keyboard via wvkbd).
|
||||||
|
* **`caitlin-x1`**: User-focused laptop.
|
||||||
|
* **Role**: Daily productivity and gaming.
|
||||||
|
* **Key Features**: Steam integration, Prism Launcher for Minecraft, and Soundux for audio management.
|
||||||
|
* **`x470`**: Lightweight guest/general use laptop.
|
||||||
|
* **Role**: Basic productivity and remote network management.
|
||||||
|
* **Key Features**: Winbox for networking and FreeShow for presentations.
|
||||||
|
* **`mary-x270`**: User-focused desktop workstation.
|
||||||
|
* **Role**: Daily productivity workstation for Mary.
|
||||||
|
* **Key Features**: Shared GNOME desktop environment, Home Manager configuration, and multi-user access (`mary` & `petere`).
|
||||||
|
* **`richmond-server`**: Headless infrastructure core.
|
||||||
|
* **Role**: Network services, container hosting, and central backup storage.
|
||||||
|
* **Key Features**: Podman-hosted services (Pi-hole, Castopod, MCFNotices), Backrest backup server, and complex VLAN networking.
|
||||||
|
* **`homeserver-1`**: Headless general server.
|
||||||
|
* **Role**: Auxiliary network server and container environment.
|
||||||
|
* **Key Features**: Tailscale, Podman, Zsh, Disko, SOPS secret management, Immich, Jellyfin, Backrest, Pocket ID, and a Homepage dashboard.
|
||||||
|
|
||||||
|
* **`mcf-server`**: Headless server.
|
||||||
|
* **Role**: Remote backup target for `homeserver-1` (restic).
|
||||||
|
* **Key Features**: SSH and Glances monitoring on the tailnet.
|
||||||
|
* **`mcf-stream`**: Desktop.
|
||||||
|
* **Role**: Newly scaffolded desktop host.
|
||||||
|
* **Key Features**: GNOME desktop, Home Manager for `petere`.
|
||||||
|
* **`hp-laptop`**: End-user laptop.
|
||||||
|
* **Role**: Daily productivity laptop for `petere`.
|
||||||
|
* **Key Features**: Hyprland + QuickShell desktop (no GNOME), greetd/tuigreet login, Home Manager for `petere`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Software Inventory
|
||||||
|
|
||||||
|
See the full cross-reference table in **[docs/software-inventory.md](docs/software-inventory.md)**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Homepage Dashboard
|
||||||
|
|
||||||
|
The Homepage dashboard (gethomepage.dev) runs on `homeserver-1` at
|
||||||
|
`http://homeserver-1:8082` and displays services from across the tailnet.
|
||||||
|
|
||||||
|
- Dashboard definition: `hosts/homeserver-1/homepage.nix`
|
||||||
|
- Adding machines, tabs, and services: **[docs/homepage-dashboard.md](docs/homepage-dashboard.md)**
|
||||||
|
- Widget API keys are managed via SOPS (`secrets.yaml` → `HOMEPAGE_VAR_*` env vars)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Initial Installation
|
||||||
|
|
||||||
|
This repository uses [nixos-anywhere](https://github.com/nix-community/nixos-anywhere) for deployment to new hardware. See **[docs/installation.md](docs/installation.md)** for prerequisites and step-by-step instructions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Managing Secrets
|
||||||
|
|
||||||
|
This project uses [sops-nix](https://github.com/Mic92/sops-nix) and `age` to manage secrets.
|
||||||
|
|
||||||
|
For instructions on adding new secrets, managing keys, and using secrets in your configuration, please see the **[SOPS Secrets Management Guide](docs/sops-secrets.md)**.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.7 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 522 KiB |
@@ -0,0 +1,105 @@
|
|||||||
|
# Borg Backup Server Setup for Richmond-Server
|
||||||
|
|
||||||
|
This document outlines the research and recommended approach for setting up a secure Borg backup server on the NixOS host `richmond-server`.
|
||||||
|
|
||||||
|
## 1. Research Findings
|
||||||
|
|
||||||
|
The core security principle for a Borg server is to use SSH with a dedicated, unprivileged user account whose command execution is strictly limited to `borg serve`. I investigated two main ways to achieve this on your server.
|
||||||
|
|
||||||
|
### Method 1: Manual NixOS Configuration (Recommended)
|
||||||
|
|
||||||
|
This method involves declaratively configuring the necessary components directly within your `configuration.nix`. It leverages standard NixOS options for user management and OpenSSH, allowing for precise control and integration with the rest of your system.
|
||||||
|
|
||||||
|
* **How it works**: You define a new system user (e.g., `borg-x1carbon`) and configure its SSH `authorized_keys` entry. For security, the SSH access for this user is restricted by prepending `command="borg serve --restrict-to-path /path/to/repo"` to the public key entry. This ensures that when a client connects as this user via SSH, it can *only* execute the `borg serve` command and *only* within the specified repository path.
|
||||||
|
|
||||||
|
#### Pros
|
||||||
|
- **Highly Secure**: Directly implements Borg's recommended security model (restricted SSH command).
|
||||||
|
- **Idiomatic & Declarative**: Managed entirely within your NixOS configuration using standard options.
|
||||||
|
- **Flexible**: Allows fine-grained control over user permissions and repository paths.
|
||||||
|
- **Integrated**: Works seamlessly with other NixOS components like `users.users` and `services.openssh`.
|
||||||
|
|
||||||
|
#### Cons
|
||||||
|
- Requires manual definition of each user and their SSH keys.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
### Method 2: Podman Container
|
||||||
|
|
||||||
|
This approach involves running a community-provided Docker image (like `borgmatic/borgserver` or `nold360/borgserver`) as a Podman container on `richmond-server`.
|
||||||
|
|
||||||
|
* **How it works**: You would define a `virtualisation.oci-containers.containers.<name>` block. This would involve:
|
||||||
|
1. Pulling a suitable Borg server image from Docker Hub.
|
||||||
|
2. Mapping a host directory (e.g., `/var/lib/borg-backups`) into the container as a volume to persist the backup data.
|
||||||
|
3. Mapping a host directory containing the `authorized_keys` file into the container's SSH directory.
|
||||||
|
4. Publishing the container's SSH port (e.g., 2222) to a port on the host.
|
||||||
|
|
||||||
|
#### Pros
|
||||||
|
- **Encapsulated**: The Borg environment and its dependencies are isolated from the host system.
|
||||||
|
- **Consistent Workflow**: Aligns with the existing use of Podman containers on `richmond-server`.
|
||||||
|
|
||||||
|
#### Cons
|
||||||
|
- **Increased Complexity**: Managing persistent storage and SSH keys via volumes is more complex and prone to misconfiguration.
|
||||||
|
- **Manual Security**: You are responsible for ensuring the container image is secure and that the SSH key restrictions are correctly implemented inside the container.
|
||||||
|
- **Less Integrated**: Does not tie into the host's user or firewall management as cleanly as the native NixOS configuration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## 2. Recommendation
|
||||||
|
|
||||||
|
**The Manual NixOS Configuration is the best method.**
|
||||||
|
|
||||||
|
It is more secure, simpler to manage, and more robust than a container-based solution for this specific use case on a NixOS system. It perfectly embodies the declarative and security-focused principles of both NixOS and Borg.
|
||||||
|
|
||||||
|
## 3. Example Configuration for `richmond-server`
|
||||||
|
|
||||||
|
Here is a proposed configuration snippet that you would add to `hosts/richmond-server/configuration.nix`. This example sets up a repository for a hypothetical client named `x1carbon-laptop`.
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
config, pkgs, lib, ...
|
||||||
|
}:
|
||||||
|
|
||||||
|
{
|
||||||
|
# ... existing configuration ...
|
||||||
|
|
||||||
|
# Borg Backup Server Configuration
|
||||||
|
# Create a dedicated system user for Borg backups
|
||||||
|
users.users.borg-x1carbon = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = "borg-x1carbon";
|
||||||
|
home = "/var/lib/borgbackup/x1carbon-main-backup"; # Home directory for this repo
|
||||||
|
createHome = true;
|
||||||
|
};
|
||||||
|
users.groups.borg-x1carbon = {};
|
||||||
|
|
||||||
|
# Configure OpenSSH to allow access for the borg user with restricted commands
|
||||||
|
services.openssh.enable = true; # Ensure OpenSSH is enabled
|
||||||
|
services.openssh.authorizedKeys.keys = {
|
||||||
|
"borg-x1carbon" = [
|
||||||
|
# IMPORTANT: Replace this with the actual public SSH key from your x1carbon laptop.
|
||||||
|
# The 'command' option restricts this key to only execute Borg serve commands.
|
||||||
|
"command=\"/run/current-system/sw/bin/borg serve --restrict-to-path /var/lib/borgbackup/x1carbon-main-backup\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICyour_clients_public_key_here user@x1carbon"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Ensure the base directory for backups exists and has correct permissions
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d /var/lib/borgbackup 0700 root root -"
|
||||||
|
];
|
||||||
|
|
||||||
|
# Borg uses SSH, so ensure the SSH port is open in your firewall.
|
||||||
|
# This is likely already enabled on your server.
|
||||||
|
networking.firewall.allowedTCPPorts = [ 22 ];
|
||||||
|
|
||||||
|
# ... rest of your configuration ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Helpful Links
|
||||||
|
|
||||||
|
- [**BorgBackup Official Documentation - Usage with SSH**](https://borgbackup.readthedocs.io/en/stable/usage/ssh.html)
|
||||||
|
- [**NixOS Wiki on BorgBackup**](https://nixos.wiki/wiki/BorgBackup)
|
||||||
|
- [**NixOS `users.users` Options**](https://search.nixos.org/options?channel=unstable&show=users.users&from=0&size=50&sort=relevance&type=packages&query=users.users)
|
||||||
|
- [**NixOS `services.openssh` Options**](https://search.nixos.org/options?channel=unstable&show=services.openssh&from=0&size=50&sort=relevance&type=packages&query=services.openssh)
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
# Homepage Dashboard — Adding Machines, Tabs & Services
|
||||||
|
|
||||||
|
This guide covers how to add new machines, tabs, and services to the **Homepage**
|
||||||
|
dashboards on **homeserver-1** (and, by extension, any host that imports the
|
||||||
|
Homepage module).
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Homepage (https://gethomepage.dev) is a self-hosted dashboard. All dashboard
|
||||||
|
configuration lives in Nix and is rendered to YAML files that Homepage reads
|
||||||
|
at `/etc/homepage-dashboard/`.
|
||||||
|
|
||||||
|
| Item | Location |
|
||||||
|
|------|----------|
|
||||||
|
| Homepage NixOS module (service wrapper) | `modules/services/homepage.nix` |
|
||||||
|
| Dashboard definition for homeserver-1 | `hosts/homeserver-1/homepage.nix` |
|
||||||
|
| Deployed config files | `/etc/homepage-dashboard/{settings,services,widgets}.yaml` |
|
||||||
|
| API keys / secrets | `secrets.yaml` (SOPS) → `HOMEPAGE_VAR_*` env vars |
|
||||||
|
|
||||||
|
The dashboard definition is split from the host config so it's easy to edit:
|
||||||
|
|
||||||
|
- `hosts/homeserver-1/configuration.nix` enables Glances + firewall and imports:
|
||||||
|
```nix
|
||||||
|
imports = [
|
||||||
|
# ...
|
||||||
|
./homepage.nix
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
## Applying Changes
|
||||||
|
|
||||||
|
After editing `homepage.nix`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nixos-rebuild switch --target-host petere@homeserver-1 --flake .#homeserver-1 --use-remote-sudo
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Important:** Homepage reads its config files when the service starts. After
|
||||||
|
> a `nixos-rebuild`, the unit's definition usually changes, but if only the
|
||||||
|
> *contents* of the config files changed (e.g. just editing `homepage.nix`), the
|
||||||
|
> service may keep the old config in memory. If your changes don't appear, restart it:
|
||||||
|
>
|
||||||
|
> ```bash
|
||||||
|
> ssh petere@homeserver-1 "sudo systemctl restart homepage-dashboard"
|
||||||
|
> ```
|
||||||
|
|
||||||
|
## Structure of the Dashboard Definition
|
||||||
|
|
||||||
|
`hosts/homeserver-1/homepage.nix` has three main parts:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
services.homepage = {
|
||||||
|
enable = true;
|
||||||
|
port = 8082;
|
||||||
|
allowedHosts = [ ... ]; # Host header values Homepage responds to
|
||||||
|
environmentFiles = [ ... ]; # SOPS-secret env file with HOMEPAGE_VAR_* keys
|
||||||
|
|
||||||
|
settings = {
|
||||||
|
title = "HomeServer";
|
||||||
|
# ...
|
||||||
|
layout = { ... }; # Controls grouping, tabs, and column widths
|
||||||
|
};
|
||||||
|
|
||||||
|
services = [ ... ]; # The service groups and tiles (services.yaml)
|
||||||
|
widgets = [ ... ]; # Header info widgets (resources, search, clock)
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tabs
|
||||||
|
|
||||||
|
Tabs are enabled by adding a `tab` field to a group's **layout** entry.
|
||||||
|
|
||||||
|
- Groups with the **same** `tab` value appear on that tab.
|
||||||
|
- Groups with **no** `tab` appear on **every** tab.
|
||||||
|
- Tabs are sorted by their order in the `layout` block.
|
||||||
|
- Each tab is deep-linkable: `#monitoring`, `#homeserver-1`, etc.
|
||||||
|
|
||||||
|
Current tabs:
|
||||||
|
|
||||||
|
```
|
||||||
|
Monitoring (default tab)
|
||||||
|
├── Homeserver-1 Monitoring → 5 Glances tiles (localhost)
|
||||||
|
└── MCF Server Monitoring → 5 Glances tiles (mcf-server via tailnet)
|
||||||
|
|
||||||
|
Homeserver-1 (#homeserver-1)
|
||||||
|
├── Media → Jellyfin, Immich
|
||||||
|
└── System → Backrest, Pocket ID, Tailscale
|
||||||
|
|
||||||
|
MCF Server (reserved; uncomment the placeholder to enable)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding a Service
|
||||||
|
|
||||||
|
1. **Add the service tile** to the relevant group in the `services` list:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
services = [
|
||||||
|
# ...
|
||||||
|
{
|
||||||
|
System = [
|
||||||
|
# ...existing tiles...
|
||||||
|
{
|
||||||
|
MyService = {
|
||||||
|
icon = "sh-myservice";
|
||||||
|
href = "http://homeserver-1.gerbil-opah.ts.net:<port>";
|
||||||
|
description = "What it does";
|
||||||
|
siteMonitor = "http://127.0.0.1:<port>"; # green/red status
|
||||||
|
widget = { # optional live stats
|
||||||
|
type = "myservice";
|
||||||
|
url = "http://127.0.0.1:<port>";
|
||||||
|
key = "{{HOMEPAGE_VAR_MYSERVICE_API_KEY}}"; # only if it needs a key
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Set the layout** so the group renders where you want (columns = how many
|
||||||
|
tiles per row; groups span the full width with `style = "row"` when they're
|
||||||
|
a single top-level group):
|
||||||
|
|
||||||
|
```nix
|
||||||
|
layout = {
|
||||||
|
# ...
|
||||||
|
MyService = { # or add to an existing group's entry
|
||||||
|
tab = "Homeserver-1";
|
||||||
|
style = "row";
|
||||||
|
columns = 4;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **API keys**: never hardcode secrets. Add the value to `secrets.yaml` under
|
||||||
|
the machine section and reference it via the environment file. The secret
|
||||||
|
`homeserver-1/homepage-env` already provides `HOMEPAGE_VAR_JELLYFIN_API_KEY`,
|
||||||
|
`HOMEPAGE_VAR_IMMICH_API_KEY`, `HOMEPAGE_VAR_TAILSCALE_API_KEY` and
|
||||||
|
`HOMEPAGE_VAR_TAILSCALE_DEVICEID`. To add another:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sops --set '["homeserver-1"]["homepage-env"] "HOMEPAGE_VAR_MYSERVICE_API_KEY=<value>"' secrets.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
(This replaces the whole env file — include every existing `HOMEPAGE_VAR_*`
|
||||||
|
line when setting it.)
|
||||||
|
|
||||||
|
## Adding a Tab
|
||||||
|
|
||||||
|
Add a new group to `services` and give it a `tab` in the layout:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
# services
|
||||||
|
{
|
||||||
|
"My New Group" = [
|
||||||
|
{ "MyService" = { href = "..."; }; }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
# layout
|
||||||
|
"My New Group" = {
|
||||||
|
tab = "My Tab";
|
||||||
|
style = "row";
|
||||||
|
columns = 4;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding a New Machine to the Dashboard
|
||||||
|
|
||||||
|
To monitor another machine (e.g. `richmond-server`):
|
||||||
|
|
||||||
|
### 1. Enable Glances on the target machine
|
||||||
|
|
||||||
|
Add to that host's `configuration.nix` (Glances exposes system stats to the
|
||||||
|
dashboard):
|
||||||
|
|
||||||
|
```nix
|
||||||
|
services.glances = {
|
||||||
|
enable = true;
|
||||||
|
port = 61208;
|
||||||
|
extraArgs = [ "--webserver" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Expose on Tailscale only
|
||||||
|
networking.firewall.interfaces.tailscale.allowedTCPPorts = [ 61208 ];
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Add the monitoring group
|
||||||
|
|
||||||
|
In `hosts/homeserver-1/homepage.nix`, add a group with Glances tiles. **The
|
||||||
|
group name must be unique** — Homepage's widget proxy resolves widget config by
|
||||||
|
leaf group name, so duplicate group names cause one machine's stats to display
|
||||||
|
on another's tiles.
|
||||||
|
|
||||||
|
```nix
|
||||||
|
# services
|
||||||
|
{
|
||||||
|
"Richmond Server Monitoring" = [
|
||||||
|
{ "System" = { widget = { type = "glances"; url = "http://richmond-server.gerbil-opah.ts.net:61208"; version = 4; metric = "info"; }; }; }
|
||||||
|
{ "CPU" = { widget = { type = "glances"; url = "http://richmond-server.gerbil-opah.ts.net:61208"; version = 4; metric = "cpu"; }; }; }
|
||||||
|
{ "Memory" = { widget = { type = "glances"; url = "http://richmond-server.gerbil-opah.ts.net:61208"; version = 4; metric = "memory"; }; }; }
|
||||||
|
{ "Disk" = { widget = { type = "glances"; url = "http://richmond-server.gerbil-opah.ts.net:61208"; version = 4; metric = "fs:/"; }; }; }
|
||||||
|
{ "Processes" = { widget = { type = "glances"; url = "http://richmond-server.gerbil-opah.ts.net:61208"; version = 4; metric = "process"; }; }; }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
# layout
|
||||||
|
"Richmond Server Monitoring" = {
|
||||||
|
tab = "Monitoring";
|
||||||
|
style = "row";
|
||||||
|
columns = 5;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Add non-monitoring services for that machine
|
||||||
|
|
||||||
|
Add a group and give it its own tab (or reuse an existing one):
|
||||||
|
|
||||||
|
```nix
|
||||||
|
# services
|
||||||
|
{
|
||||||
|
"Richmond Server Apps" = [
|
||||||
|
{ "Ntfy" = { icon = "sh-ntfy"; href = "http://richmond-server.gerbil-opah.ts.net:8080"; }; }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
# layout
|
||||||
|
"Richmond Server Apps" = {
|
||||||
|
tab = "Richmond Server";
|
||||||
|
style = "row";
|
||||||
|
columns = 4;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. (Optional) Tailscale widget for the machine
|
||||||
|
|
||||||
|
Each machine's Tailscale node can be shown with its own widget, using the
|
||||||
|
machine's numeric device ID (find it via the Tailscale API/admin console):
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
"Richmond Server" = [
|
||||||
|
{
|
||||||
|
Tailscale = {
|
||||||
|
icon = "sh-tailscale";
|
||||||
|
href = "https://login.tailscale.com/admin/machines";
|
||||||
|
widget = {
|
||||||
|
type = "tailscale";
|
||||||
|
deviceid = "<numeric-device-id>"; # NOT the ...CNTRL value
|
||||||
|
key = "{{HOMEPAGE_VAR_TAILSCALE_API_KEY}}";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Cause / Fix |
|
||||||
|
|---------|-------------|
|
||||||
|
| Changes not appearing | Service not restarted — run `sudo systemctl restart homepage-dashboard` |
|
||||||
|
| `t.metric is undefined` | Glances widget missing the `metric` field — every glances tile needs `metric` (e.g. `cpu`, `memory`, `fs:/`, `process`, `info`) |
|
||||||
|
| `no manageable device matching this ID found` | Tailscale widget `deviceid` is wrong — must be the **numeric** device ID, not the `...CNTRL` value |
|
||||||
|
| Two machines show the same stats | Duplicate group names — every group (especially monitoring) needs a unique name |
|
||||||
|
| `Host validation failed` | The `Host` header isn't in `allowedHosts` — add the hostname (with port) to `allowedHosts` |
|
||||||
|
| API keys showing in config | Keys must come from the SOPS env file (`HOMEPAGE_VAR_*`), never hardcoded |
|
||||||
|
|
||||||
|
## Glances Metrics
|
||||||
|
|
||||||
|
The Glances widget (`type = "glances"`) requires a `version` and `metric`:
|
||||||
|
|
||||||
|
- `version = 4` for Glances v4.x (installed)
|
||||||
|
- `metric`: `info` (system summary), `cpu`, `memory`, `process`, `containers`,
|
||||||
|
`fs:/` (disk usage), `network:<iface>`, `sensor:<id>`, `disk:<id>`, `gpu:<id>`
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# Initial Installation
|
||||||
|
|
||||||
|
This repository uses [nixos-anywhere](https://github.com/nix-community/nixos-anywhere) for seamless deployment to new hardware.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. **SSH Access**: The target machine must be booted into a Linux environment (e.g., NixOS Installer ISO) with SSH enabled and your public key authorized.
|
||||||
|
2. **Secrets Management**: If the target host requires secrets, ensure its Age key is generated and added to `.sops.yaml` as described in the [SOPS Guide](sops-secrets.md).
|
||||||
|
3. **Disko**: Ensure the `disko-config.nix` for the host matches the target hardware's drive names (e.g., `/dev/nvme0n1` vs `/dev/sda`).
|
||||||
|
|
||||||
|
## Deployment Command
|
||||||
|
|
||||||
|
Run this command from the root of the repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nix run github:nix-community/nixos-anywhere -- --flake .#<hostname> <target-ip>
|
||||||
|
```
|
||||||
|
|
||||||
|
*Example:* `nix run github:nix-community/nixos-anywhere -- --flake .#x1carbon 192.168.1.50`
|
||||||
|
|
||||||
|
The process will automatically partition the drive via `disko`, install the system, and reboot into the new NixOS environment.
|
||||||
|
|
||||||
|
## Post-Installation
|
||||||
|
|
||||||
|
After the first boot, apply the configuration locally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo nixos-rebuild switch --flake /etc/nixos/#<hostname>
|
||||||
|
```
|
||||||
|
|
||||||
|
For ongoing management, clone this repository and use the commands in the main [README.md](../README.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Secrets: Using Age Keys with `--extra-files`
|
||||||
|
|
||||||
|
Some hosts require SOPS secrets at build time (e.g., user passwords with `neededForUsers = true`). The private age key must be available during `nixos-rebuild`, but it should **never** be committed to Git. Use the `extra-files/` directory (in `.gitignore`) and the `--extra-files` flag to supply the key securely.
|
||||||
|
|
||||||
|
### 1. Create the Age Key
|
||||||
|
|
||||||
|
Generate a new age key and store it in the untracked `extra-files/` tree:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p extra-files/root/.config/sops/age
|
||||||
|
age-keygen -o extra-files/root/.config/sops/age/keys.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Extract the **public key** and add it to `.sops.yaml`, then re-encrypt the secrets file so this key can decrypt them:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat extra-files/root/.config/sops/age/keys.txt | age-keygen -y
|
||||||
|
# Copy the output public key into .sops.yaml under the `age` key list
|
||||||
|
sops updatekeys secrets.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Deploy with `--extra-files`
|
||||||
|
|
||||||
|
The `--extra-files` flag copies the local `extra-files/` directory into the Nix store so the age key is available at build time:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo nixos-rebuild switch --flake .#<hostname> --extra-files extra-files
|
||||||
|
```
|
||||||
|
|
||||||
|
This makes `extra-files/root/.config/sops/age/keys.txt` available at `/root/.config/sops/age/keys.txt` during evaluation, allowing SOPS to decrypt `secrets.yaml` without the key ever touching the target machine's filesystem.
|
||||||
|
|
||||||
|
> **Note**: For `nixos-anywhere` initial deployments, supply the age key via `--extra-files` as well:
|
||||||
|
> ```bash
|
||||||
|
> nix run github:nix-community/nixos-anywhere -- --extra-files extra-files --flake .#<hostname> <target-ip>
|
||||||
|
> ```
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# Software Inventory
|
||||||
|
|
||||||
|
This table tracks which software is installed on each host and how it's installed.
|
||||||
|
|
||||||
|
| Software | Installation Method | X1C | CX1 | X470 | MX270 | RS | HS1 | HPL |
|
||||||
|
| :------------------- | :------------------------------- | :-- | :-- | :--- | :---- | :- | :-- | :-: |
|
||||||
|
| **Zsh** | NixOS System / HM Module | x | x | x | x | x | x | x |
|
||||||
|
| **Tailscale** | NixOS System | x | x | x | x | x | x | x |
|
||||||
|
| **GNOME** | NixOS System | x | x | x | x | | | |
|
||||||
|
| **Hyprland** | NixOS System | | | | | | | x |
|
||||||
|
| **QuickShell** | NixOS System | | | | | | | x |
|
||||||
|
| **Wofi** | NixOS System | | | | | | | x |
|
||||||
|
| **Thunar** | NixOS System | | | | | | | x |
|
||||||
|
| **Power Mgmt** | NixOS System (upower, power-profiles-daemon, hypridle) | | | | | | | x |
|
||||||
|
| **vdirsyncer** | HM (cal sync) | | | | | | | x |
|
||||||
|
| **khal** | HM (cal sync) | | | | | | | x |
|
||||||
|
| **Firefox** | NixOS System / HM Module | x | x | x | x | | | x |
|
||||||
|
| **Chromium** | NixOS System | x | x | x | x | | | x |
|
||||||
|
| **Thunderbird** | NixOS System | x | x | x | x | | | x |
|
||||||
|
| **Bitwarden** | NixOS System | x | x | x | x | | | x |
|
||||||
|
| **VLC** | NixOS System | x | x | x | x | | | x |
|
||||||
|
| **Element Desktop** | NixOS System | x | x | x | x | | | x |
|
||||||
|
| **Kdenlive** | NixOS System | x | x | x | x | | | x |
|
||||||
|
| **Nextcloud Client** | NixOS System | x | x | x | x | | | x |
|
||||||
|
| **OnlyOffice** | NixOS System | x | x | x | x | | | x |
|
||||||
|
| **Gear Lever** | Flatpak | x | x | x | x | | | x |
|
||||||
|
| **Flatseal** | Flatpak | x | x | x | x | | | x |
|
||||||
|
| **Soundux** | Flatpak | x | x | | | | | |
|
||||||
|
| **OpenLP** | Flatpak Bundle | | | x | | | | |
|
||||||
|
| **FreeShow** | AppImage | x | x | x | | | | |
|
||||||
|
| **GIMP** | NixOS System | x | | | | | | |
|
||||||
|
| **Scribus** | NixOS System | x | | | | | | |
|
||||||
|
| **OpenShot** | NixOS System | x | | | | | | |
|
||||||
|
| **OBS Studio** | NixOS System (NDI / CUDA) | x | | | | | | |
|
||||||
|
| **OpenCode** | NixOS System | x | | | | | | |
|
||||||
|
| **MCP-NixOS** | HM User Package (x1carbon) | x | | | | | | |
|
||||||
|
| **LM Studio** | NixOS System | x | | | | | | |
|
||||||
|
| **Mixing Station** | NixOS System (Custom Derivation) | x | | | | | | |
|
||||||
|
| **X32-Edit** | NixOS System | x | | | | | | |
|
||||||
|
| **Vorta** | NixOS System | x | | | | | | |
|
||||||
|
| **Paperless-ngx** | NixOS System (Module) | x | | | | | | |
|
||||||
|
| **Winbox** | NixOS System | x | | x | | | | |
|
||||||
|
| **wvkbd** | HM User Package (x1carbon) | x | | | | | | |
|
||||||
|
| **matugen** | HM User Package (x1carbon) | x | | | | | | |
|
||||||
|
| **Xournal++** | NixOS System | x | x | | | | | |
|
||||||
|
| **Steam** | NixOS System | x | x | | | | | |
|
||||||
|
| **Prism Launcher** | NixOS System | | x | | | | | |
|
||||||
|
| **Rclone** | NixOS System | | x | | | | | |
|
||||||
|
| **Pi-hole** | Container (Podman) | | | | | x | | |
|
||||||
|
| **Castopod** | Container (Podman) | | | | | x | | |
|
||||||
|
| **MCFNotices** | Container (Podman) | | | | | x | | |
|
||||||
|
| **Ntfy** | NixOS System (Module) | | | | | x | | |
|
||||||
|
| **Immich** | NixOS System (Module) | | | | | | x | |
|
||||||
|
| **Pocket ID** | NixOS System (Module) | | | | | | x | |
|
||||||
|
| **Jellyfin** | NixOS System (Module) | | | | | | x | |
|
||||||
|
| **Backrest** | NixOS System (Module) | | | | | | x | |
|
||||||
|
| **Homepage** | NixOS System (Module) | | | | | | x | |
|
||||||
|
| **MariaDB** | NixOS System | | | | | x | | |
|
||||||
|
| **BorgBackup** | _Removed - migrated to Backrest_ | | | | | | | |
|
||||||
|
| **NVIDIA Drivers** | NixOS System | x | | | | | x | |
|
||||||
|
| **VS Code** | HM Module | x | x | x | x | | | x |
|
||||||
|
| **Kitty** | HM Module | x | x | x | x | | | x |
|
||||||
|
| **Direnv** | HM User | x | x | x | x | x | | x |
|
||||||
|
| **Git** | HM User / System | x | x | x | x | x | x | x |
|
||||||
|
| **Fastfetch** | HM User | x | x | x | x | x | | x |
|
||||||
|
| **Python / UV** | NixOS System | x | x | | x | | | x |
|
||||||
|
| **Flutter** | NixOS System | x | x | | x | | | x |
|
||||||
|
| **Disko** | NixOS Module | x | x | x | x | x | x | x |
|
||||||
|
| **SOPS** | NixOS Module / System | x | x | x | x | x | x | x |
|
||||||
|
| **Microsoft Core Fonts** | NixOS System (Fonts) | x | x | x | x | x | x | x |
|
||||||
|
| **Admin tools** | NixOS System (`management.nix`) | x | x | x | x | x | x | x |
|
||||||
|
|
||||||
|
## Legend
|
||||||
|
|
||||||
|
| Code | Host |
|
||||||
|
|------|------|
|
||||||
|
| **X1C** | `x1carbon` |
|
||||||
|
| **CX1** | `caitlin-x1` |
|
||||||
|
| **X470** | `x470` |
|
||||||
|
| **MX270** | `mary-x270` |
|
||||||
|
| **RS** | `richmond-server` |
|
||||||
|
| **HS1** | `homeserver-1` |
|
||||||
|
| **HPL** | `hp-laptop` |
|
||||||
|
|
||||||
|
## Installation Methods
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| **NixOS System** | Package installed via `environment.systemPackages` or `services.<name>.enable` |
|
||||||
|
| **HM Module** | Home Manager module (`programs.<name>.enable`) |
|
||||||
|
| **HM User** | Home Manager user-level config (`home.packages`) |
|
||||||
|
| **Flatpak** | Installed via `services.flatpak.packages` |
|
||||||
|
| **Container (Podman)** | Runs as an OCI container via Podman |
|
||||||
|
| **AppImage** | Standalone AppImage bundle |
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
# SOPS Secrets Management Guide
|
||||||
|
|
||||||
|
This guide covers how to manage encrypted secrets in Nix-Vibe using `sops-nix` with `age` encryption.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
- **Secrets File**: `secrets.yaml` (encrypted in Git)
|
||||||
|
- **Configuration**: `.sops.yaml` (public keys and creation rules)
|
||||||
|
- **Private Key**: Locally stored at `~/.config/sops/age/keys.txt` (Never commit this!)
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
The project includes `sops` in the default development environment. You can also run it temporarily:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nix shell nixpkgs#sops nixpkgs#age
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding or Updating Secrets
|
||||||
|
|
||||||
|
The easiest way to add a new secret (like a password or API key) is to use the `sops --set` command from the root of the repository.
|
||||||
|
|
||||||
|
### Adding a Key-Value Pair
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sops --set '["<host-or-category>"]["<secret-name>"] "<value>"' secrets.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
*Example (Adding a server password):*
|
||||||
|
```bash
|
||||||
|
sops --set '["richmond-server"]["new-password"] "supersecret123"' secrets.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
*Example (Adding a global user password):*
|
||||||
|
```bash
|
||||||
|
sops --set '["users"]["petere-password"] "mypassword"' secrets.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Editing the Secrets File Directly
|
||||||
|
|
||||||
|
To open the entire decrypted file in your editor:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sops secrets.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using Secrets in Configuration
|
||||||
|
|
||||||
|
### 1. NixOS System Secrets
|
||||||
|
|
||||||
|
In `hosts/<hostname>/configuration.nix`:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
sops.secrets."machine-name/new-secret" = {
|
||||||
|
owner = "root";
|
||||||
|
group = "root";
|
||||||
|
mode = "0400";
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** For user passwords, add `neededForUsers = true;` to ensure the secret is decrypted early enough for the account to be created.
|
||||||
|
|
||||||
|
### 2. Home Manager Secrets
|
||||||
|
|
||||||
|
Home Manager secrets are defined in the user's profile (e.g., `home-manager/users/petere.nix`):
|
||||||
|
|
||||||
|
```nix
|
||||||
|
sops.secrets."gemini-api-key" = { };
|
||||||
|
```
|
||||||
|
|
||||||
|
Access the decrypted path in your configuration:
|
||||||
|
`config.sops.secrets."gemini-api-key".path`
|
||||||
|
|
||||||
|
### 3. Rendered Templates (`sops.templates`)
|
||||||
|
|
||||||
|
For config files that embed a secret (e.g. an env file consumed by a container),
|
||||||
|
use `sops.templates` so sops-nix renders the file with correct permissions and
|
||||||
|
re-renders it at boot/switch — no shell `preStart` needed:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
sops.templates."pihole-env" = {
|
||||||
|
content = ''
|
||||||
|
FTLCONF_webserver_api_password=${config.sops.placeholder."richmond-server/pihole-password"}
|
||||||
|
'';
|
||||||
|
path = "/run/pihole-env";
|
||||||
|
mode = "0600";
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
The `${config.sops.placeholder."<secret>"}` reference is substituted with the
|
||||||
|
decrypted value at runtime. Live examples: `hosts/richmond-server/configuration.nix`
|
||||||
|
(pihole + castopod env files).
|
||||||
|
|
||||||
|
### Multi-line Secrets
|
||||||
|
|
||||||
|
Store SSH keys and other multi-line values as YAML **block scalars** (using `|`)
|
||||||
|
so they decrypt with real newlines. `hosts/homeserver-1/configuration.nix`
|
||||||
|
installs the `restic-ssh-key` verbatim with `install`, so that secret must be
|
||||||
|
multi-line (not a single line with escaped spaces).
|
||||||
|
|
||||||
|
## Rotating / Adding New Machine Keys
|
||||||
|
|
||||||
|
When deploying to a new machine, you must generate an age key and add its public key to `.sops.yaml`.
|
||||||
|
|
||||||
|
1. **Generate the key on the target machine**:
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/.config/sops/age
|
||||||
|
age-keygen -o ~/.config/sops/age/keys.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Get the public key**:
|
||||||
|
```bash
|
||||||
|
cat ~/.config/sops/age/keys.txt | age-keygen -y
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Update `.sops.yaml`**:
|
||||||
|
Add the new public key to the `age` list.
|
||||||
|
|
||||||
|
4. **Re-encrypt the secrets file**:
|
||||||
|
```bash
|
||||||
|
sops updatekeys secrets.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **"No sops config found"**: Ensure you are in the repository root.
|
||||||
|
- **Decryption Failure**: Ensure your private key is at `~/.config/sops/age/keys.txt` or set `export SOPS_AGE_KEY_FILE=...`.
|
||||||
|
- **Pure Evaluation Mode**: Nix Flakes in pure mode cannot read absolute paths (like `/run/secrets/...`). Use `sops.templates` or runtime injection instead of `preStart` (see `hosts/richmond-server/configuration.nix`).
|
||||||
Generated
+345
@@ -0,0 +1,345 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"disko": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1781152676,
|
||||||
|
"narHash": "sha256-RxWs5ND31KzTG7wvMM+PMfUjyNpmIEr999lqNARaM5o=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "disko",
|
||||||
|
"rev": "ff8702b4de27f72b4c78573dfb89ec74e36abdf1",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "disko",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"disko_2": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixos-anywhere",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1781152676,
|
||||||
|
"narHash": "sha256-RxWs5ND31KzTG7wvMM+PMfUjyNpmIEr999lqNARaM5o=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "disko",
|
||||||
|
"rev": "ff8702b4de27f72b4c78573dfb89ec74e36abdf1",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"ref": "master",
|
||||||
|
"repo": "disko",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"home-manager": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1788651960,
|
||||||
|
"narHash": "sha256-v9wJd32eZ2bvhBzVOd7TIjLQd011P7nwOhjKtWlci5I=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "home-manager",
|
||||||
|
"rev": "2c0350c759688177331b8f5242311fae8877bdb3",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "home-manager",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"home-manager-stable": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs-stable"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1747688870,
|
||||||
|
"narHash": "sha256-ypL9WAZfmJr5V70jEVzqGjjQzF0uCkz+AFQF7n9NmNc=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "home-manager",
|
||||||
|
"rev": "d5f1f641b289553927b3801580598d200a501863",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"ref": "release-24.11",
|
||||||
|
"repo": "home-manager",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nix-flatpak": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1783368811,
|
||||||
|
"narHash": "sha256-0H8jDwR4Kegb3heaTrH1ftbgKfZVDT8JE+46uXxDy/Q=",
|
||||||
|
"owner": "gmodena",
|
||||||
|
"repo": "nix-flatpak",
|
||||||
|
"rev": "20d42f0ee98c9fe9f85e8d1de474f1409ed10d05",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "gmodena",
|
||||||
|
"repo": "nix-flatpak",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nix-vm-test": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixos-anywhere",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1786747096,
|
||||||
|
"narHash": "sha256-9QqhmaLVsPhKdMSBaWKjDqeGRn8G4ov4cVuZ6JFwXbo=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "nix-vm-test",
|
||||||
|
"rev": "c8781a0ea2d8417506fff7722eae5a6316461212",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "nix-vm-test",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixos-anywhere": {
|
||||||
|
"inputs": {
|
||||||
|
"disko": "disko_2",
|
||||||
|
"nix-vm-test": "nix-vm-test",
|
||||||
|
"nixos-images": "nixos-images",
|
||||||
|
"nixos-stable": "nixos-stable",
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"treefmt-nix": "treefmt-nix"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1787728766,
|
||||||
|
"narHash": "sha256-g2oZlrBU3AI2ubCiY/UyE9ALTIDueTcF//QP3vaY9IQ=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "nixos-anywhere",
|
||||||
|
"rev": "6b77f26ec4538ced04bf1d02f374b0ec02e9c27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "nixos-anywhere",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixos-images": {
|
||||||
|
"inputs": {
|
||||||
|
"nixos-stable": [
|
||||||
|
"nixos-anywhere",
|
||||||
|
"nixos-stable"
|
||||||
|
],
|
||||||
|
"nixos-unstable": [
|
||||||
|
"nixos-anywhere",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1787222173,
|
||||||
|
"narHash": "sha256-acp6QJnWVnLvnanC59CMkiDC/i0ZhdFKiB7prru9SHw=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "nixos-images",
|
||||||
|
"rev": "e17386d9193d6d5a90f1b4b6a8a5cd2620d34b56",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "nixos-images",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixos-stable": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1787204541,
|
||||||
|
"narHash": "sha256-OURZPknrTjQrlNyxPdqzyqmU/81Wes1CUP/Ft1Rv/YI=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "5880666fd9eb563038431edb35c2d0aa595884e6",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-26.05",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1788752844,
|
||||||
|
"narHash": "sha256-VaWGJ6+cIYN2erfSecbRV+4ljI185Ty2wUrXyvQbgOw=",
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "dc5d91f840324650bac8c379428c7037a416959a",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs-stable": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1751274312,
|
||||||
|
"narHash": "sha256-/bVBlRpECLVzjV19t5KMdMFWSwKLtb5RyXdjz3LJT+g=",
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "50ab793786d9de88ee30ec4e4c24fb4236fc2674",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"ref": "nixos-24.11",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1789684949,
|
||||||
|
"narHash": "sha256-ZKhUe/2IJUq1JhKxKMu8rbkgSGmPP2ZCqlIPn40aGCM=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "e554fab72f81915600f3f449b786fd9af40439a5",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"disko": "disko",
|
||||||
|
"home-manager": "home-manager",
|
||||||
|
"home-manager-stable": "home-manager-stable",
|
||||||
|
"nix-flatpak": "nix-flatpak",
|
||||||
|
"nixos-anywhere": "nixos-anywhere",
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"nixpkgs-stable": "nixpkgs-stable",
|
||||||
|
"sops-nix": "sops-nix",
|
||||||
|
"teleportfling": "teleportfling"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sops-nix": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1788337237,
|
||||||
|
"narHash": "sha256-gkSH8VUtCo6hnysNmb9DbTuDepH2t5pv+QWjP75xKAk=",
|
||||||
|
"owner": "Mic92",
|
||||||
|
"repo": "sops-nix",
|
||||||
|
"rev": "fbf759290e0cb0a98dfc813a4eb7d53ad1dacb57",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "Mic92",
|
||||||
|
"repo": "sops-nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"teleportfling": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs_2"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1789810501,
|
||||||
|
"narHash": "sha256-OAdYGZP2lcN5eRmy/liflctV7gSWObGJBD2w9QQgN8E=",
|
||||||
|
"ref": "refs/heads/main",
|
||||||
|
"rev": "921e6d65dfc6267f339a30474a5b37072a99c474",
|
||||||
|
"revCount": 20,
|
||||||
|
"type": "git",
|
||||||
|
"url": "http://homeserver:3050/pedley/TeleportFling.git"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "http://homeserver:3050/pedley/TeleportFling.git"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"treefmt-nix": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixos-anywhere",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1786901030,
|
||||||
|
"narHash": "sha256-WSFCsDSE5ffgD2MqzkM2CYjeFiKhRF/dJUN8uedb6YE=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "treefmt-nix",
|
||||||
|
"rev": "27b3b12a8e6375f28ebe122f07d230ca5459bbfa",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "treefmt-nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
{
|
||||||
|
description = "A vibrant NixOS configuration for multiple machines";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||||
|
nixpkgs-stable.url = "github:nixos/nixpkgs/nixos-24.11";
|
||||||
|
|
||||||
|
home-manager = {
|
||||||
|
url = "github:nix-community/home-manager";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
|
||||||
|
home-manager-stable = {
|
||||||
|
url = "github:nix-community/home-manager/release-24.11";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs-stable";
|
||||||
|
};
|
||||||
|
|
||||||
|
disko = {
|
||||||
|
url = "github:nix-community/disko";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
|
||||||
|
nixos-anywhere = {
|
||||||
|
url = "github:nix-community/nixos-anywhere";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
|
||||||
|
sops-nix = {
|
||||||
|
url = "github:Mic92/sops-nix";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
|
||||||
|
nix-flatpak.url = "github:gmodena/nix-flatpak";
|
||||||
|
|
||||||
|
# TeleportFling: standalone screen + audio sender for OBS Teleport.
|
||||||
|
teleportfling.url = "git+http://homeserver:3050/pedley/TeleportFling.git";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
{ self
|
||||||
|
, nixpkgs
|
||||||
|
, nixpkgs-stable
|
||||||
|
, home-manager
|
||||||
|
, home-manager-stable
|
||||||
|
, disko
|
||||||
|
, nixos-anywhere
|
||||||
|
, sops-nix
|
||||||
|
, nix-flatpak
|
||||||
|
, teleportfling
|
||||||
|
, ...
|
||||||
|
}@inputs:
|
||||||
|
let
|
||||||
|
system = "x86_64-linux";
|
||||||
|
serverOverlays = [
|
||||||
|
# Empty - antigravity removed
|
||||||
|
];
|
||||||
|
# Overlays shared by all desktops/laptops (no CUDA/NDI)
|
||||||
|
desktopOverlays = [
|
||||||
|
(final: prev: {
|
||||||
|
stable = import nixpkgs-stable {
|
||||||
|
system = final.stdenv.hostPlatform.system;
|
||||||
|
config = {
|
||||||
|
allowUnfree = true;
|
||||||
|
allowBroken = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
})
|
||||||
|
];
|
||||||
|
# Full overlay set used by x1carbon only (CUDA + NDI for OBS Studio etc.)
|
||||||
|
overlays = [
|
||||||
|
(final: prev: {
|
||||||
|
stable = import nixpkgs-stable {
|
||||||
|
system = final.stdenv.hostPlatform.system;
|
||||||
|
config = {
|
||||||
|
allowUnfree = true;
|
||||||
|
allowBroken = true;
|
||||||
|
cudaSupport = true;
|
||||||
|
cudaCapabilities = [ "6.1" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
})
|
||||||
|
(final: prev: {
|
||||||
|
ndi-6 = prev.ndi-6.overrideAttrs (old: {
|
||||||
|
src = prev.fetchurl {
|
||||||
|
url = "https://downloads.ndi.tv/SDK/NDI_SDK_Linux/Install_NDI_SDK_v6_Linux.tar.gz";
|
||||||
|
hash = "sha256-8DFPJFRG3vxIi2POtGiazxqWWu79ray3BXG7IWqMwYM=";
|
||||||
|
};
|
||||||
|
});
|
||||||
|
})
|
||||||
|
];
|
||||||
|
# Packages with known vulnerabilities that are explicitly permitted.
|
||||||
|
# Single-sourced here; referenced by the mkNixosSystem module below.
|
||||||
|
permittedInsecurePackages = [
|
||||||
|
"electron-39.8.10"
|
||||||
|
"qtwebengine-5.15.19"
|
||||||
|
"ventoy-1.1.17"
|
||||||
|
];
|
||||||
|
|
||||||
|
# Specific pkgs for Desktop with CUDA support (using stable to avoid broken CUDA builds)
|
||||||
|
# No longer needed as a separate input, we use overlays now.
|
||||||
|
# pkgsDesktop = ... (Removed)
|
||||||
|
|
||||||
|
# Function to generate a NixOS system configuration
|
||||||
|
hostUsers = {
|
||||||
|
x1carbon = [ "petere" ];
|
||||||
|
caitlin-x1 = [
|
||||||
|
"caitlin"
|
||||||
|
"petere"
|
||||||
|
];
|
||||||
|
x470 = [
|
||||||
|
"petere"
|
||||||
|
"guest"
|
||||||
|
];
|
||||||
|
mary-x270 = [
|
||||||
|
"mary"
|
||||||
|
"petere"
|
||||||
|
];
|
||||||
|
richmond-server = [ "petere" ];
|
||||||
|
homeserver-1 = [ "petere" ];
|
||||||
|
mcf-server = [ "petere" ];
|
||||||
|
mcf-stream = [
|
||||||
|
"guest"
|
||||||
|
"petere"
|
||||||
|
];
|
||||||
|
hp-laptop = [ "petere" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Function to generate a NixOS system configuration
|
||||||
|
mkNixosSystem =
|
||||||
|
hostname:
|
||||||
|
{ specialArgs ? { }
|
||||||
|
, modules ? [ ]
|
||||||
|
, hostOverlays ? desktopOverlays
|
||||||
|
, nixpkgsInput ? nixpkgs
|
||||||
|
, homeManagerInput ? home-manager
|
||||||
|
,
|
||||||
|
}:
|
||||||
|
nixpkgsInput.lib.nixosSystem {
|
||||||
|
# Use the original nixpkgs.lib here
|
||||||
|
specialArgs = {
|
||||||
|
inherit inputs;
|
||||||
|
}
|
||||||
|
// specialArgs; # Removed pkgs from specialArgs
|
||||||
|
modules = [
|
||||||
|
{
|
||||||
|
nixpkgs.config.allowUnfree = true;
|
||||||
|
nixpkgs.config.permittedInsecurePackages = permittedInsecurePackages;
|
||||||
|
nixpkgs.overlays = hostOverlays;
|
||||||
|
}
|
||||||
|
sops-nix.nixosModules.sops
|
||||||
|
(./hosts + "/${hostname}/configuration.nix")
|
||||||
|
homeManagerInput.nixosModules.home-manager
|
||||||
|
nix-flatpak.nixosModules.nix-flatpak
|
||||||
|
{
|
||||||
|
home-manager.useGlobalPkgs = true;
|
||||||
|
home-manager.useUserPackages = true;
|
||||||
|
home-manager.backupFileExtension = "backup";
|
||||||
|
home-manager.extraSpecialArgs = { inherit inputs; };
|
||||||
|
home-manager.sharedModules = [
|
||||||
|
./home-manager/modules/zsh.nix
|
||||||
|
];
|
||||||
|
}
|
||||||
|
]
|
||||||
|
++ (nixpkgs.lib.map # Use original nixpkgs.lib here
|
||||||
|
(user: import (./home-manager/users + "/${user}.nix"))
|
||||||
|
(hostUsers.${hostname} or [ ])
|
||||||
|
)
|
||||||
|
++ modules;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
nixosConfigurations = {
|
||||||
|
x1carbon = mkNixosSystem "x1carbon" {
|
||||||
|
hostOverlays = overlays;
|
||||||
|
};
|
||||||
|
|
||||||
|
caitlin-x1 = mkNixosSystem "caitlin-x1" { };
|
||||||
|
|
||||||
|
x470 = mkNixosSystem "x470" { };
|
||||||
|
|
||||||
|
mary-x270 = mkNixosSystem "mary-x270" { };
|
||||||
|
|
||||||
|
richmond-server = mkNixosSystem "richmond-server" {
|
||||||
|
hostOverlays = serverOverlays;
|
||||||
|
};
|
||||||
|
|
||||||
|
homeserver-1 = mkNixosSystem "homeserver-1" {
|
||||||
|
hostOverlays = serverOverlays;
|
||||||
|
};
|
||||||
|
|
||||||
|
mcf-server = mkNixosSystem "mcf-server" {
|
||||||
|
hostOverlays = serverOverlays;
|
||||||
|
};
|
||||||
|
|
||||||
|
mcf-stream = mkNixosSystem "mcf-stream" { };
|
||||||
|
|
||||||
|
hp-laptop = mkNixosSystem "hp-laptop" { };
|
||||||
|
};
|
||||||
|
|
||||||
|
packages.${system} = { };
|
||||||
|
|
||||||
|
formatter.${system} = nixpkgs.legacyPackages.${system}.nixfmt;
|
||||||
|
|
||||||
|
devShells.${system}.default =
|
||||||
|
let
|
||||||
|
devPkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
in
|
||||||
|
devPkgs.mkShell {
|
||||||
|
packages = with devPkgs; [
|
||||||
|
sops
|
||||||
|
age
|
||||||
|
nixfmt
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
{ pkgs, lib, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
./gnome.nix
|
||||||
|
./kitty.nix
|
||||||
|
./firefox.nix
|
||||||
|
./tools/just.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
home.activation.linkOnlyOfficeFonts = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
|
||||||
|
FONT_DIR="/run/current-system/sw/share/X11/fonts"
|
||||||
|
USER_FONTS="$HOME/.local/share/fonts"
|
||||||
|
NATIVE_TARGET="$HOME/.local/share/onlyoffice/desktopeditors/data/fonts"
|
||||||
|
FLATPAK_TARGET="$HOME/.var/app/org.onlyoffice.desktopeditors/data/onlyoffice/desktopeditors/data/fonts"
|
||||||
|
|
||||||
|
if [ -d "$FONT_DIR" ]; then
|
||||||
|
for TARGET_DIR in "$USER_FONTS" "$NATIVE_TARGET" "$FLATPAK_TARGET"; do
|
||||||
|
$DRY_RUN_CMD mkdir -p "$TARGET_DIR"
|
||||||
|
$DRY_RUN_CMD rm -f "$TARGET_DIR/AllFonts.js"* "$TARGET_DIR/fonts_thumbnail"* "$TARGET_DIR/font_selection.bin" "$TARGET_DIR/fonts.log"
|
||||||
|
$DRY_RUN_CMD cp -Lf "$FONT_DIR"/*.ttf "$FONT_DIR"/*.otf "$FONT_DIR"/*.ttc "$TARGET_DIR/" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
|
||||||
|
programs.vscode = {
|
||||||
|
enable = true;
|
||||||
|
package = pkgs.vscode;
|
||||||
|
profiles.default = {
|
||||||
|
extensions =
|
||||||
|
with pkgs.vscode-extensions;
|
||||||
|
[
|
||||||
|
bbenoist.nix
|
||||||
|
ms-python.python
|
||||||
|
dart-code.flutter
|
||||||
|
]
|
||||||
|
++ [
|
||||||
|
# OpenCode Go: Copilot Provider - pinned to specific version for reproducibility.
|
||||||
|
# This extension is not in nixpkgs, only available from VSCode Marketplace.
|
||||||
|
(pkgs.vscode-utils.extensionFromVscodeMarketplace {
|
||||||
|
name = "opencode-go-for-copilot";
|
||||||
|
publisher = "DenizhanDaklr";
|
||||||
|
version = "0.1.18";
|
||||||
|
sha256 = "0igdkgv6dra3zpd80lhwymdh216dqn8m8kqwsxh258wyk9fzgfk0";
|
||||||
|
})
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
fonts.fontconfig.enable = true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://github.com/fastfetch-cli/fastfetch/raw/dev/doc/json_schema.json",
|
||||||
|
"modules": [
|
||||||
|
"title",
|
||||||
|
"separator",
|
||||||
|
"os",
|
||||||
|
"host",
|
||||||
|
"kernel",
|
||||||
|
"uptime",
|
||||||
|
"packages",
|
||||||
|
"shell",
|
||||||
|
"display",
|
||||||
|
"de",
|
||||||
|
"wm",
|
||||||
|
"wmtheme",
|
||||||
|
"theme",
|
||||||
|
"icons",
|
||||||
|
"font",
|
||||||
|
"cursor",
|
||||||
|
"terminal",
|
||||||
|
"terminalfont",
|
||||||
|
"cpu",
|
||||||
|
"gpu",
|
||||||
|
"memory",
|
||||||
|
"swap",
|
||||||
|
"disk",
|
||||||
|
"localip",
|
||||||
|
"battery",
|
||||||
|
"poweradapter",
|
||||||
|
"locale",
|
||||||
|
"break",
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"key": "Age Public Key",
|
||||||
|
"text": "cat /tmp/age-public-key.txt 2>/dev/null || echo 'Not generated yet'"
|
||||||
|
},
|
||||||
|
"colors"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{ pkgs, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
programs.firefox = {
|
||||||
|
enable = true;
|
||||||
|
configPath = ".mozilla/firefox";
|
||||||
|
profiles.petere = {
|
||||||
|
isDefault = true;
|
||||||
|
settings = {
|
||||||
|
"ui.systemUsesDarkTheme" = 1;
|
||||||
|
"browser.in-content.dark-mode" = true;
|
||||||
|
"extensions.activeThemeID" = "firefox-compact-dark@mozilla.org";
|
||||||
|
"browser.newtabpage.enabled" = false;
|
||||||
|
"browser.startup.homepage" = "about:blank";
|
||||||
|
"browser.aboutConfig.showWarning" = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{ pkgs, ... }:
|
||||||
|
|
||||||
|
# Shared GNOME extensions: AppIndicator, User Themes, Caffeine
|
||||||
|
{
|
||||||
|
dconf.settings = {
|
||||||
|
"org/gnome/shell" = {
|
||||||
|
enabled-extensions = [
|
||||||
|
"user-theme@gnome-shell-extensions.gcampax.github.com"
|
||||||
|
"appindicatorsupport@rgcjonas.gmail.com"
|
||||||
|
"caffeine@patapon.info"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
home.packages = with pkgs; [
|
||||||
|
gnomeExtensions.appindicator
|
||||||
|
gnomeExtensions.user-themes
|
||||||
|
gnomeExtensions.caffeine
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
{ config
|
||||||
|
, pkgs
|
||||||
|
, lib
|
||||||
|
, ...
|
||||||
|
}:
|
||||||
|
|
||||||
|
{
|
||||||
|
dconf.settings = {
|
||||||
|
# Desktop interface settings
|
||||||
|
"org/gnome/desktop/interface" = {
|
||||||
|
color-scheme = "prefer-dark";
|
||||||
|
enable-hot-corners = false;
|
||||||
|
clock-show-weekday = true;
|
||||||
|
show-battery-percentage = true;
|
||||||
|
|
||||||
|
icon-theme = "Papirus-Dark";
|
||||||
|
cursor-theme = "Bibata-Modern-Classic";
|
||||||
|
};
|
||||||
|
|
||||||
|
"org/gnome/desktop/input-sources" = {
|
||||||
|
sources = [
|
||||||
|
(lib.gvariant.mkTuple [
|
||||||
|
"xkb"
|
||||||
|
"gb"
|
||||||
|
])
|
||||||
|
];
|
||||||
|
xkb-options = [ "terminate:ctrl_alt_bksp" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Window manager preferences
|
||||||
|
"org/gnome/desktop/wm/preferences" = {
|
||||||
|
button-layout = "appmenu:minimize,maximize,close";
|
||||||
|
num-workspaces = 4;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Keybindings
|
||||||
|
"org/gnome/desktop/wm/keybindings" = {
|
||||||
|
close = [ "<Super>q" ];
|
||||||
|
switch-to-workspace-1 = [ "<Super>1" ];
|
||||||
|
switch-to-workspace-2 = [ "<Super>2" ];
|
||||||
|
switch-to-workspace-3 = [ "<Super>3" ];
|
||||||
|
switch-to-workspace-4 = [ "<Super>4" ];
|
||||||
|
move-to-workspace-1 = [ "<Super><Shift>1" ];
|
||||||
|
move-to-workspace-2 = [ "<Super><Shift>2" ];
|
||||||
|
move-to-workspace-3 = [ "<Super><Shift>3" ];
|
||||||
|
move-to-workspace-4 = [ "<Super><Shift>4" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Shell settings
|
||||||
|
"org/gnome/shell" = {
|
||||||
|
favorite-apps = [
|
||||||
|
"firefox.desktop"
|
||||||
|
"kitty.desktop"
|
||||||
|
"org.gnome.Nautilus.desktop"
|
||||||
|
"thunderbird.desktop"
|
||||||
|
"chromium-browser.desktop"
|
||||||
|
];
|
||||||
|
enabled-extensions = [
|
||||||
|
"user-theme@gnome-shell-extensions.gcampax.github.com"
|
||||||
|
"appindicatorsupport@rgcjonas.gmail.com"
|
||||||
|
"caffeine@patapon.info"
|
||||||
|
"screen-rotate@shyzus.github.io"
|
||||||
|
"gsconnect@andyholmes.github.io"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Mutter (window manager) settings
|
||||||
|
"org/gnome/mutter" = {
|
||||||
|
dynamic-workspaces = false;
|
||||||
|
edge-tiling = true;
|
||||||
|
workspaces-only-on-primary = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
# File manager (Nautilus) settings
|
||||||
|
"org/gnome/nautilus/preferences" = {
|
||||||
|
default-folder-viewer = "list-view";
|
||||||
|
search-filter-time-type = "last_modified";
|
||||||
|
show-hidden-files = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Power settings
|
||||||
|
"org/gnome/settings-daemon/plugins/power" = {
|
||||||
|
sleep-inactive-ac-type = "nothing";
|
||||||
|
sleep-inactive-battery-timeout = 1800;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Privacy settings
|
||||||
|
"org/gnome/desktop/privacy" = {
|
||||||
|
remember-recent-files = true;
|
||||||
|
remove-old-temp-files = true;
|
||||||
|
remove-old-trash-files = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Session settings
|
||||||
|
"org/gnome/desktop/session" = {
|
||||||
|
idle-delay = lib.gvariant.mkUint32 900; # 15 minutes
|
||||||
|
};
|
||||||
|
|
||||||
|
# Screensaver settings
|
||||||
|
"org/gnome/desktop/screensaver" = {
|
||||||
|
lock-enabled = true;
|
||||||
|
lock-delay = lib.gvariant.mkUint32 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Caffeine extension settings
|
||||||
|
"org/gnome/shell/extensions/caffeine" = {
|
||||||
|
enable-fullscreen = true;
|
||||||
|
restore-state = true;
|
||||||
|
show-indicator = "always";
|
||||||
|
show-notifications = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# GNOME-specific packages
|
||||||
|
home.packages = with pkgs; [
|
||||||
|
gnome-tweaks
|
||||||
|
gnomeExtensions.appindicator
|
||||||
|
gnomeExtensions.user-themes
|
||||||
|
gnomeExtensions.caffeine
|
||||||
|
gnomeExtensions.gsconnect
|
||||||
|
dconf-editor
|
||||||
|
catppuccin-gtk
|
||||||
|
papirus-icon-theme
|
||||||
|
bibata-cursors
|
||||||
|
];
|
||||||
|
|
||||||
|
# GTK theme configuration
|
||||||
|
gtk = {
|
||||||
|
enable = true;
|
||||||
|
gtk4.theme = config.gtk.theme;
|
||||||
|
theme = {
|
||||||
|
name = "catppuccin-frappe-blue-standard";
|
||||||
|
package = pkgs.catppuccin-gtk;
|
||||||
|
};
|
||||||
|
iconTheme = {
|
||||||
|
name = "Papirus-Dark";
|
||||||
|
package = pkgs.papirus-icon-theme;
|
||||||
|
};
|
||||||
|
cursorTheme = {
|
||||||
|
name = "Bibata-Modern-Classic";
|
||||||
|
package = pkgs.bibata-cursors;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Home Manager cursor configuration
|
||||||
|
home.pointerCursor = {
|
||||||
|
enable = true;
|
||||||
|
name = "Bibata-Modern-Classic";
|
||||||
|
package = pkgs.bibata-cursors;
|
||||||
|
gtk.enable = true;
|
||||||
|
x11.enable = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
home.file."Pictures/wallpapers/tokyo-night.png".source = ../../assets/wallpapers/tokyo-night.png;
|
||||||
|
|
||||||
|
# Set initial default wallpaper on first install if user hasn't configured a custom wallpaper yet.
|
||||||
|
# If the user changes their wallpaper via GNOME Settings, dconf read detects it and leaves it untouched across reboots/updates.
|
||||||
|
home.activation.setInitialWallpaper = lib.hm.dag.entryAfter [ "dconf" ] ''
|
||||||
|
WALLPAPER="${config.home.homeDirectory}/Pictures/wallpapers/tokyo-night.png"
|
||||||
|
VAL="$(${pkgs.dconf}/bin/dconf read /org/gnome/desktop/background/picture-uri 2>/dev/null || true)"
|
||||||
|
if [ -z "$VAL" ]; then
|
||||||
|
$DRY_RUN_CMD ${pkgs.dconf}/bin/dconf write /org/gnome/desktop/background/picture-uri "'file://$WALLPAPER'"
|
||||||
|
$DRY_RUN_CMD ${pkgs.dconf}/bin/dconf write /org/gnome/desktop/background/picture-uri-dark "'file://$WALLPAPER'"
|
||||||
|
$DRY_RUN_CMD ${pkgs.dconf}/bin/dconf write /org/gnome/desktop/background/picture-options "'zoom'"
|
||||||
|
$DRY_RUN_CMD ${pkgs.dconf}/bin/dconf write /org/gnome/desktop/screensaver/picture-uri "'file://$WALLPAPER'"
|
||||||
|
$DRY_RUN_CMD ${pkgs.dconf}/bin/dconf write /org/gnome/desktop/screensaver/picture-options "'zoom'"
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
}
|
||||||
@@ -0,0 +1,459 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# hyprland-tablet-daemon.py — Hyprland tablet-mode rotation + on-screen keyboard.
|
||||||
|
#
|
||||||
|
# Wired to the ThinkPad X1 Yoga Gen 6 (Intel HID switches, /dev/input/event17):
|
||||||
|
# * SW_TABLET_MODE tells us the LCD is folded into tablet mode.
|
||||||
|
# * "Accelerometer orientation changed: <orient>" lines are parsed from
|
||||||
|
# `monitor-sensor` (a thin client of iio-sensor-proxy, which runs as a
|
||||||
|
# system service on this host). iio-sensor-proxy merges the accel axes +
|
||||||
|
# mount matrix into a single compass-style orientation string.
|
||||||
|
#
|
||||||
|
# While in tablet mode the eDP-1 monitor transform (and matching per-device
|
||||||
|
# transforms for touch inputs) follow the accelerometer. The on-screen
|
||||||
|
# keyboard (wvkbd) follows text focus instead of Hyprland's input-method v2
|
||||||
|
# protocol (which Hyprland advertises but never fires):
|
||||||
|
# * We watch `.socket2.sock` "activewindow>>class,title" events. If the
|
||||||
|
# focused window class is in the text-capable allowlist the keyboard is
|
||||||
|
# shown (SIGUSR2), otherwise hidden (SIGUSR1). This gives reliable
|
||||||
|
# auto-hide when there is nothing to type into.
|
||||||
|
# * Manual control (super+crtl+k / the bar chip) flips wvkbd directly via
|
||||||
|
# SIGRTMIN. The daemon re-applies focus state on the next focus change,
|
||||||
|
# so a manual dismissal lasts until the user switches windows.
|
||||||
|
# Leaving tablet mode snaps transform back to 0 and kills wvkbd.
|
||||||
|
#
|
||||||
|
# State is mirrored to a small JSON file ($XDG_RUNTIME_DIR/hyprland-tablet)
|
||||||
|
# that the QuickShell bar chip reads via `hyprland-tablet status`:
|
||||||
|
# {"tablet": bool, "osk": visible-or-not}
|
||||||
|
#
|
||||||
|
# Control commands arrive over a Unix datagram socket
|
||||||
|
# ($XDG_RUNTIME_DIR/hyprland-tablet.ctl): "toggle" | "show" | "hide".
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import signal
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
# Orientation string (from iio-sensor-proxy) -> Hyprland monitor transform.
|
||||||
|
# Hyprland transform: 0 normal, 1 90° CCW, 2 180°, 3 270° CCW.
|
||||||
|
# If the screen rotates the wrong way on hardware, swap the 1 and 3 below.
|
||||||
|
ORIENTATION_TRANSFORM = {
|
||||||
|
"normal": 0,
|
||||||
|
"left-up": 1,
|
||||||
|
"bottom-up": 2,
|
||||||
|
"right-up": 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
SW_TABLET_MODE = 0x01 # input event code for SW_TABLET_MODE
|
||||||
|
|
||||||
|
# Paths resolved at build time by the Nix module (wrapped with pkgs.python3).
|
||||||
|
# The placeholder strings below are in at-sign-delimited form because
|
||||||
|
# pkgs.replaceVarsWith substitutes exactly that syntax.
|
||||||
|
EVDEV_DEVICE = "@EVDEV_DEVICE@"
|
||||||
|
WVKBD_PATH = "@WVKBD_PATH@"
|
||||||
|
WVKBD_ARGS = @WVKBD_ARGS@ # replaced with a JSON array (valid Python list)
|
||||||
|
MONITOR_SENSOR = "@MONITOR_SENSOR@"
|
||||||
|
HYPRCTL = "@HYPRCTL@"
|
||||||
|
TEXT_APPS = @TEXT_APPS_JSON@ # replaced with a JSON array (valid Python list)
|
||||||
|
# State file lives in $XDG_RUNTIME_DIR (set by the user systemd service); the
|
||||||
|
# QuickShell bar chip + CLI read the same path.
|
||||||
|
STATE_FILE = os.path.join(os.environ.get("XDG_RUNTIME_DIR", "/tmp"), "hyprland-tablet")
|
||||||
|
CONTROL_SOCKET = os.path.join(os.environ.get("XDG_RUNTIME_DIR", "/tmp"), "hyprland-tablet.ctl")
|
||||||
|
|
||||||
|
DEBUG = os.environ.get("HYPRLAND_TABLET_DEBUG") == "1"
|
||||||
|
|
||||||
|
|
||||||
|
def log(msg: str) -> None:
|
||||||
|
if DEBUG:
|
||||||
|
print(f"[hyprland-tablet] {msg}", file=sys.stderr, flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
_state_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def write_state(tablet: bool, osk: bool) -> None:
|
||||||
|
try:
|
||||||
|
with open(STATE_FILE, "w") as f:
|
||||||
|
json.dump({"tablet": tablet, "osk": osk}, f)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def hyprctl_transform(monitor: str, transform: int) -> None:
|
||||||
|
"""Apply a transform to the monitor and every touch/tablet device."""
|
||||||
|
# hyprland.nix uses the Lua config parser (0.55+), so the legacy
|
||||||
|
# `monitor NAME,transform,N` / input keyword lines don't apply at runtime
|
||||||
|
# ("keyword can't work with non-legacy parsers"). The equivalent Lua API:
|
||||||
|
# hl.monitor({ output = "NAME", mode = ..., scale = ..., transform = N })
|
||||||
|
# hl.config({ input = { touchdevice/tablet = { transform = N } } })
|
||||||
|
# scale MUST be passed explicitly: omitting it makes Hyprland re-derive
|
||||||
|
# HiDPI zoom (1.5 here), blowing up the bar on rotation.
|
||||||
|
tx = [HYPRCTL, "eval", f'hl.monitor({{ output = "{monitor}", mode = "preferred", position = "auto", scale = 1, transform = {transform} }})']
|
||||||
|
subprocess.run(tx, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False)
|
||||||
|
# touchdevice transform follows the display so touch coordinates track the
|
||||||
|
# rotated framebuffer. input:touchdevice:transform is a global input option
|
||||||
|
# (not per-device device:touchdevice:transform which doesn't exist).
|
||||||
|
subprocess.run(
|
||||||
|
[HYPRCTL, "eval", f'hl.config({{ input = {{ touchdevice = {{ transform = {transform} }} }} }})'],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
# same for the tablet (pen) — this Yoga presents the Wacom digitizer as a
|
||||||
|
# tablet device, and without this the pen coordinates stay in screen space.
|
||||||
|
subprocess.run(
|
||||||
|
[HYPRCTL, "eval", f'hl.config({{ input = {{ tablet = {{ transform = {transform} }} }} }})'],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def transform_for(orientation: str) -> int:
|
||||||
|
return ORIENTATION_TRANSFORM.get(orientation, 0)
|
||||||
|
|
||||||
|
|
||||||
|
def _hypr_socket(name: str) -> str:
|
||||||
|
"""Locate a Hyprland IPC socket (".socket.sock" / ".socket2.sock")."""
|
||||||
|
runtime = os.environ.get("XDG_RUNTIME_DIR", "/tmp")
|
||||||
|
inst = os.environ.get("HYPRLAND_INSTANCE_SIGNATURE")
|
||||||
|
if inst:
|
||||||
|
candidate = os.path.join(runtime, "hypr", inst, name)
|
||||||
|
if os.path.exists(candidate):
|
||||||
|
return candidate
|
||||||
|
base = os.path.join(runtime, "hypr")
|
||||||
|
try:
|
||||||
|
for entry in os.listdir(base):
|
||||||
|
candidate = os.path.join(base, entry, name)
|
||||||
|
if os.path.exists(candidate):
|
||||||
|
return candidate
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
class Daemon:
|
||||||
|
def __init__(self, monitor: str) -> None:
|
||||||
|
self.monitor = monitor
|
||||||
|
self.tablet = False
|
||||||
|
self.orientation = "normal"
|
||||||
|
self.current_transform = 0
|
||||||
|
self.osk_proc = None
|
||||||
|
self.osk_visible = False
|
||||||
|
self.dev = None
|
||||||
|
self.sensor = None
|
||||||
|
self.last_focus_class = ""
|
||||||
|
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# evdev switch watcher (thread)
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
def open_evdev(self) -> bool:
|
||||||
|
try:
|
||||||
|
import evdev
|
||||||
|
import evdev.ecodes as ecodes
|
||||||
|
|
||||||
|
self.dev = evdev.InputDevice(EVDEV_DEVICE)
|
||||||
|
except Exception as e:
|
||||||
|
log(f"cannot open {EVDEV_DEVICE}: {e}")
|
||||||
|
return False
|
||||||
|
# Seed the current switch state via EVIOCGSW so the daemon is correct
|
||||||
|
# if it starts mid-tablet.
|
||||||
|
EVIOCGSW = (2 << 30) | (ord("E") << 8) | 0x1B | (64 << 16)
|
||||||
|
buf = bytearray(64)
|
||||||
|
try:
|
||||||
|
import fcntl
|
||||||
|
|
||||||
|
fcntl.ioctl(self.dev.fd, EVIOCGSW, buf)
|
||||||
|
vals = struct.unpack("16i", buf)
|
||||||
|
self.tablet = bool(vals[0] >> SW_TABLET_MODE & 1)
|
||||||
|
except OSError:
|
||||||
|
self.tablet = False
|
||||||
|
log(f"opened {self.dev.name}, starting tablet={self.tablet}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def evdev_thread(self) -> None:
|
||||||
|
import evdev.ecodes as ecodes
|
||||||
|
|
||||||
|
for event in self.dev.read_loop():
|
||||||
|
if event.type != ecodes.EV_SW or event.code != SW_TABLET_MODE:
|
||||||
|
continue
|
||||||
|
new = event.value == 1
|
||||||
|
if new != self.tablet:
|
||||||
|
self.tablet = new
|
||||||
|
log(f"SW_TABLET_MODE -> {new}")
|
||||||
|
self.on_tablet_changed(new)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# orientation watcher (thread)
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
def _start_sensor(self) -> bool:
|
||||||
|
try:
|
||||||
|
self.sensor = subprocess.Popen(
|
||||||
|
[MONITOR_SENSOR],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
text=True,
|
||||||
|
bufsize=1,
|
||||||
|
)
|
||||||
|
except FileNotFoundError:
|
||||||
|
log("monitor-sensor not found")
|
||||||
|
return False
|
||||||
|
return self.sensor.stdout is not None
|
||||||
|
|
||||||
|
def sensor_thread(self) -> None:
|
||||||
|
# monitor-sensor (a thin client of iio-sensor-proxy) only emits
|
||||||
|
# "Accelerometer orientation changed:" on *changes*; the initial
|
||||||
|
# orientation arrives as "Has accelerometer (orientation: X, ...)".
|
||||||
|
# Match both, and restart the client if it ever exits so orientation
|
||||||
|
# tracking survives.
|
||||||
|
first = True
|
||||||
|
while True:
|
||||||
|
if not self._start_sensor():
|
||||||
|
return
|
||||||
|
if self.sensor is None or self.sensor.stdout is None:
|
||||||
|
return
|
||||||
|
if first:
|
||||||
|
log("monitor-sensor started")
|
||||||
|
first = False
|
||||||
|
for line in self.sensor.stdout:
|
||||||
|
m = re.search(r"Accelerometer orientation changed:\s*(\S+)", line.strip())
|
||||||
|
if m:
|
||||||
|
self.on_orientation(m.group(1))
|
||||||
|
continue
|
||||||
|
m = re.search(r"Has accelerometer \(orientation:\s*(\S+)", line.strip())
|
||||||
|
if m:
|
||||||
|
# value is "normal," / "left-up," — strip the separator
|
||||||
|
self.on_orientation(m.group(1).rstrip(","))
|
||||||
|
# monitor-sensor exited: reopen it (iio-sensor-proxy may have
|
||||||
|
# dropped our client if a second one connected).
|
||||||
|
log("monitor-sensor exited, restarting")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# Hyprland focus watcher (thread) — drives auto show/hide
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
def focus_thread(self) -> None:
|
||||||
|
path = _hypr_socket(".socket2.sock")
|
||||||
|
if not path:
|
||||||
|
log("hyprland .socket2.sock not found; focus auto-hide disabled")
|
||||||
|
return
|
||||||
|
log(f"watching focus events on {path}")
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
|
s.connect(path)
|
||||||
|
f = s.makefile("r")
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if not line.startswith("activewindow>>"):
|
||||||
|
continue
|
||||||
|
payload = line.split(">>", 1)[1].split(",", 1)
|
||||||
|
self.on_focus_changed(payload[0].strip())
|
||||||
|
except OSError as e:
|
||||||
|
log(f"focus socket error: {e}")
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
def _is_text_app(self, window_class: str) -> bool:
|
||||||
|
if not window_class:
|
||||||
|
return False
|
||||||
|
lowered = window_class.lower()
|
||||||
|
for spec in TEXT_APPS:
|
||||||
|
if lowered == spec or lowered.startswith(spec):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def on_focus_changed(self, window_class: str) -> None:
|
||||||
|
self.last_focus_class = window_class
|
||||||
|
if not self.tablet:
|
||||||
|
return # in laptop mode the OSK duty is entirely manual
|
||||||
|
want = self._is_text_app(window_class)
|
||||||
|
log(f"focus={window_class!r} text_app={want}")
|
||||||
|
self.set_osk_visible(want)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# control socket (thread) — manual show/hide/toggle
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
def control_thread(self) -> None:
|
||||||
|
try:
|
||||||
|
os.unlink(CONTROL_SOCKET)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
s = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
|
||||||
|
s.bind(CONTROL_SOCKET)
|
||||||
|
s.settimeout(1.0)
|
||||||
|
except OSError as e:
|
||||||
|
log(f"cannot bind control socket: {e}")
|
||||||
|
return
|
||||||
|
log(f"control socket ready at {CONTROL_SOCKET}")
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
data, _ = s.recvfrom(128)
|
||||||
|
except socket.timeout:
|
||||||
|
continue
|
||||||
|
except OSError:
|
||||||
|
break
|
||||||
|
cmd = data.decode("ascii", "replace").strip()
|
||||||
|
log(f"control command: {cmd}")
|
||||||
|
if cmd == "toggle":
|
||||||
|
self.toggle_osk()
|
||||||
|
elif cmd == "show":
|
||||||
|
self.set_osk_visible(True)
|
||||||
|
elif cmd == "hide":
|
||||||
|
self.set_osk_visible(False)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# actions
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
def ensure_osk(self) -> None:
|
||||||
|
with _state_lock:
|
||||||
|
if self.osk_proc is not None and self.osk_proc.poll() is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
# Start hidden: focus state decides whether to SIGUSR2 it.
|
||||||
|
self.osk_proc = subprocess.Popen([WVKBD_PATH, *WVKBD_ARGS, "--hidden"])
|
||||||
|
self.osk_visible = False
|
||||||
|
log(f"wvkbd started (hidden, args={WVKBD_ARGS!r})")
|
||||||
|
except FileNotFoundError:
|
||||||
|
log("wvkbd not found")
|
||||||
|
write_state(self.tablet, self.osk_visible)
|
||||||
|
|
||||||
|
def kill_osk(self) -> None:
|
||||||
|
with _state_lock:
|
||||||
|
if self.osk_proc is not None and self.osk_proc.poll() is None:
|
||||||
|
self.osk_proc.terminate()
|
||||||
|
try:
|
||||||
|
self.osk_proc.wait(timeout=3)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
self.osk_proc.kill()
|
||||||
|
self.osk_proc.wait()
|
||||||
|
log("wvkbd stopped")
|
||||||
|
self.osk_proc = None
|
||||||
|
self.osk_visible = False
|
||||||
|
write_state(self.tablet, False)
|
||||||
|
|
||||||
|
def _signal_osk(self, sig: int) -> None:
|
||||||
|
if self.osk_proc is not None and self.osk_proc.poll() is None:
|
||||||
|
try:
|
||||||
|
os.kill(self.osk_proc.pid, sig)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def set_osk_visible(self, visible: bool) -> None:
|
||||||
|
if visible == self.osk_visible:
|
||||||
|
return
|
||||||
|
if visible:
|
||||||
|
if self.osk_proc is None or self.osk_proc.poll() is not None:
|
||||||
|
self.ensure_osk()
|
||||||
|
if self.osk_proc is None:
|
||||||
|
return
|
||||||
|
self._signal_osk(signal.SIGUSR2)
|
||||||
|
self.osk_visible = True
|
||||||
|
log("wvkbd show")
|
||||||
|
else:
|
||||||
|
self._signal_osk(signal.SIGUSR1)
|
||||||
|
self.osk_visible = False
|
||||||
|
log("wvkbd hide")
|
||||||
|
write_state(self.tablet, self.osk_visible)
|
||||||
|
|
||||||
|
def toggle_osk(self) -> None:
|
||||||
|
with _state_lock:
|
||||||
|
if self.osk_proc is None or self.osk_proc.poll() is not None:
|
||||||
|
self.osk_proc = subprocess.Popen([WVKBD_PATH, *WVKBD_ARGS, "--hidden"])
|
||||||
|
self.osk_visible = False
|
||||||
|
log("wvkbd started (hidden, manual toggle)")
|
||||||
|
write_state(self.tablet, self.osk_visible)
|
||||||
|
return
|
||||||
|
# wvkbd SIGRTMIN toggles visibility; we shadow its state here.
|
||||||
|
self.osk_visible = not self.osk_visible
|
||||||
|
self._signal_osk(signal.SIGRTMIN)
|
||||||
|
log(f"wvkbd toggled -> visible={self.osk_visible}")
|
||||||
|
write_state(self.tablet, self.osk_visible)
|
||||||
|
|
||||||
|
def apply_transform(self, t: int) -> None:
|
||||||
|
if t != self.current_transform:
|
||||||
|
hyprctl_transform(self.monitor, t)
|
||||||
|
self.current_transform = t
|
||||||
|
log(f"transform -> {t}")
|
||||||
|
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# event handlers
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
def on_tablet_changed(self, new: bool) -> None:
|
||||||
|
if new:
|
||||||
|
self.apply_transform(transform_for(self.orientation))
|
||||||
|
self.ensure_osk()
|
||||||
|
else:
|
||||||
|
self.apply_transform(0)
|
||||||
|
self.kill_osk()
|
||||||
|
write_state(self.tablet, self.osk_visible)
|
||||||
|
|
||||||
|
def on_orientation(self, orient: str) -> None:
|
||||||
|
self.orientation = orient
|
||||||
|
if self.tablet:
|
||||||
|
self.apply_transform(transform_for(orient))
|
||||||
|
# laptop mode: never rotate (gated by the switch above)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
def run(self) -> int:
|
||||||
|
have_evdev = self.open_evdev()
|
||||||
|
|
||||||
|
# If we started mid-tablet, bring Hyprland to the current state right
|
||||||
|
# away instead of waiting for the next SW_TABLET_MODE edge. The OSK
|
||||||
|
# duty for the currently-focused window is applied below.
|
||||||
|
if self.tablet:
|
||||||
|
self.apply_transform(transform_for(self.orientation))
|
||||||
|
self.ensure_osk()
|
||||||
|
cls = self.cur_focus_class()
|
||||||
|
if cls:
|
||||||
|
self.on_focus_changed(cls)
|
||||||
|
write_state(self.tablet, self.osk_visible)
|
||||||
|
|
||||||
|
threads = []
|
||||||
|
if have_evdev:
|
||||||
|
t = threading.Thread(target=self.evdev_thread, daemon=True)
|
||||||
|
t.start()
|
||||||
|
threads.append(t)
|
||||||
|
t = threading.Thread(target=self.sensor_thread, daemon=True)
|
||||||
|
t.start()
|
||||||
|
threads.append(t)
|
||||||
|
t = threading.Thread(target=self.focus_thread, daemon=True)
|
||||||
|
t.start()
|
||||||
|
threads.append(t)
|
||||||
|
t = threading.Thread(target=self.control_thread, daemon=True)
|
||||||
|
t.start()
|
||||||
|
threads.append(t)
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
time.sleep(3600)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def cur_focus_class(self) -> str:
|
||||||
|
try:
|
||||||
|
out = subprocess.run(
|
||||||
|
[HYPRCTL, "activewindow", "-j"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=3,
|
||||||
|
)
|
||||||
|
info = json.loads(out.stdout)
|
||||||
|
return info.get("class") or ""
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
monitor = os.environ.get("HYPRLAND_TABLET_MONITOR", "eDP-1")
|
||||||
|
d = Daemon(monitor)
|
||||||
|
return d.run()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
palette = import ./palette.nix;
|
||||||
|
evdevPython = pkgs.python3.withPackages (ps: [ ps.evdev ]);
|
||||||
|
|
||||||
|
# Text-capable window classes. wvkbd's own --auto relies on Hyprland's
|
||||||
|
# input-method-v2 protocol, which Hyprland advertises but never actually
|
||||||
|
# fires — so auto show/hide is driven here by watching activewindow events.
|
||||||
|
# Add classes (lowercase prefixes are allowed) for anything you type into.
|
||||||
|
textApps = [
|
||||||
|
"kitty"
|
||||||
|
"alacritty"
|
||||||
|
"wezterm"
|
||||||
|
"konsole"
|
||||||
|
"com.mitchellh.ghostty"
|
||||||
|
"ghostty"
|
||||||
|
"xterm"
|
||||||
|
"firefox"
|
||||||
|
"librewolf"
|
||||||
|
"chromium"
|
||||||
|
"chromium-browser"
|
||||||
|
"google-chrome"
|
||||||
|
"microsoft-edge"
|
||||||
|
"brave-browser"
|
||||||
|
"thunderbird"
|
||||||
|
"com.github.xournalpp.xournalpp"
|
||||||
|
"code"
|
||||||
|
"code-url-handler"
|
||||||
|
"zen"
|
||||||
|
];
|
||||||
|
|
||||||
|
# Shell-escaped arg list for the CLI fallback (same content as wvkbdFlags
|
||||||
|
# but space-joined for shell expansion). The daemon receives the JSON form.
|
||||||
|
# The font spec is double-quoted so it survives shell word-splitting.
|
||||||
|
wvkbdFlagsShell = pkgs.lib.concatStringsSep " " [
|
||||||
|
"-l"
|
||||||
|
"simple"
|
||||||
|
"-H"
|
||||||
|
"300"
|
||||||
|
"-L"
|
||||||
|
"200"
|
||||||
|
"--fn"
|
||||||
|
"\"DejaVu Sans 24\""
|
||||||
|
"--alpha"
|
||||||
|
"235"
|
||||||
|
"--bg"
|
||||||
|
"${palette.wvkbdBg}"
|
||||||
|
"--fg"
|
||||||
|
"${palette.surface}"
|
||||||
|
"--fg-sp"
|
||||||
|
"${palette.wvkbdFgSp}"
|
||||||
|
"--press"
|
||||||
|
"${palette.neon}"
|
||||||
|
"--press-sp"
|
||||||
|
"${palette.violet}"
|
||||||
|
"--text"
|
||||||
|
"${palette.text}"
|
||||||
|
"--text-sp"
|
||||||
|
"${palette.muted}"
|
||||||
|
"--text-press"
|
||||||
|
"${palette.ink}"
|
||||||
|
"--text-press-sp"
|
||||||
|
"${palette.ink}"
|
||||||
|
"--swipe"
|
||||||
|
"${palette.cyan}"
|
||||||
|
"--swipe-sp"
|
||||||
|
"${palette.violet}"
|
||||||
|
"--text-swipe"
|
||||||
|
"${palette.ink}"
|
||||||
|
"--text-swipe-sp"
|
||||||
|
"${palette.ink}"
|
||||||
|
];
|
||||||
|
|
||||||
|
# wvkbd argv as a JSON array. The daemon splices this into place as a bare
|
||||||
|
# Python expression — a JSON array of strings IS valid Python (list of
|
||||||
|
# double-quoted strings), so no quote-escaping headaches at build time.
|
||||||
|
wvkbdArgs = [
|
||||||
|
"-l"
|
||||||
|
"simple"
|
||||||
|
"-H"
|
||||||
|
"300"
|
||||||
|
"-L"
|
||||||
|
"200"
|
||||||
|
"--fn"
|
||||||
|
"DejaVu Sans 24"
|
||||||
|
"--alpha"
|
||||||
|
"235"
|
||||||
|
"--bg"
|
||||||
|
"${palette.wvkbdBg}"
|
||||||
|
"--fg"
|
||||||
|
"${palette.surface}"
|
||||||
|
"--fg-sp"
|
||||||
|
"${palette.wvkbdFgSp}"
|
||||||
|
"--press"
|
||||||
|
"${palette.neon}"
|
||||||
|
"--press-sp"
|
||||||
|
"${palette.violet}"
|
||||||
|
"--text"
|
||||||
|
"${palette.text}"
|
||||||
|
"--text-sp"
|
||||||
|
"${palette.muted}"
|
||||||
|
"--text-press"
|
||||||
|
"${palette.ink}"
|
||||||
|
"--text-press-sp"
|
||||||
|
"${palette.ink}"
|
||||||
|
"--swipe"
|
||||||
|
"${palette.cyan}"
|
||||||
|
"--swipe-sp"
|
||||||
|
"${palette.violet}"
|
||||||
|
"--text-swipe"
|
||||||
|
"${palette.ink}"
|
||||||
|
"--text-swipe-sp"
|
||||||
|
"${palette.ink}"
|
||||||
|
];
|
||||||
|
wvkbdArgsJson = builtins.toJSON wvkbdArgs;
|
||||||
|
|
||||||
|
# Text-app allowlist as a JSON array, spliced the same way (valid Python
|
||||||
|
# list literal after substitution).
|
||||||
|
textAppsJson = builtins.toJSON textApps;
|
||||||
|
|
||||||
|
# Place build-time paths into the daemon (replaceVars honours the
|
||||||
|
# @VAR@ placeholders) and run it with the evdev-enabled python.
|
||||||
|
daemon = pkgs.replaceVarsWith {
|
||||||
|
name = "hyprland-tablet-daemon";
|
||||||
|
src = pkgs.writeScript "hyprland-tablet-daemon-src" ''
|
||||||
|
#!${evdevPython}/bin/python3
|
||||||
|
${builtins.readFile ./hyprland-tablet-daemon.py}
|
||||||
|
'';
|
||||||
|
isExecutable = true;
|
||||||
|
replacements = {
|
||||||
|
EVDEV_DEVICE = "/dev/input/event17";
|
||||||
|
WVKBD_PATH = "${pkgs.wvkbd}/bin/wvkbd-mobintl";
|
||||||
|
WVKBD_ARGS = wvkbdArgsJson;
|
||||||
|
TEXT_APPS_JSON = textAppsJson;
|
||||||
|
MONITOR_SENSOR = "${pkgs.iio-sensor-proxy}/bin/monitor-sensor";
|
||||||
|
HYPRCTL = "${pkgs.hyprland}/bin/hyprctl";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Toggle/show/hide are forwarded to the daemon over a Unix datagram socket
|
||||||
|
# so manual control and focus-driven auto-hide share one visibility state.
|
||||||
|
cli = pkgs.writeShellScriptBin "hyprland-tablet" ''
|
||||||
|
set -eu
|
||||||
|
state_file="$XDG_RUNTIME_DIR/hyprland-tablet"
|
||||||
|
ctl_sock="$XDG_RUNTIME_DIR/hyprland-tablet.ctl"
|
||||||
|
|
||||||
|
ctl_send() {
|
||||||
|
if [ -S "$ctl_sock" ]; then
|
||||||
|
${pkgs.python3}/bin/python3 -c '
|
||||||
|
import os, socket, sys
|
||||||
|
path = os.environ.get("XDG_RUNTIME_DIR", "/tmp") + "/hyprland-tablet.ctl"
|
||||||
|
s = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
|
||||||
|
s.sendto(sys.argv[1].encode(), path)
|
||||||
|
' "$1"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
osk_pid() {
|
||||||
|
pgrep -f 'wvkbd-mobintl' | head -n1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
case "''${1:-}" in
|
||||||
|
status)
|
||||||
|
if [ -f "$state_file" ]; then
|
||||||
|
cat "$state_file"
|
||||||
|
else
|
||||||
|
echo '{"tablet":false,"osk":false}'
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
# Toggle on-screen keyboard visibility. Manual control wins over the
|
||||||
|
# daemon's focus-derived state until the next activewindow change.
|
||||||
|
keyboard-toggle|osk-toggle)
|
||||||
|
ctl_send toggle || {
|
||||||
|
pid="$(osk_pid)"
|
||||||
|
if [ -z "$pid" ]; then
|
||||||
|
nohup ${pkgs.wvkbd}/bin/wvkbd-mobintl ${wvkbdFlagsShell} >/dev/null 2>&1 &
|
||||||
|
else
|
||||||
|
kill -SIGRTMIN "$pid"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
;;
|
||||||
|
osk-show)
|
||||||
|
ctl_send show || { pid="$(osk_pid)"; [ -n "$pid" ] && kill -SIGUSR2 "$pid"; }
|
||||||
|
;;
|
||||||
|
osk-hide)
|
||||||
|
ctl_send hide || { pid="$(osk_pid)"; [ -n "$pid" ] && kill -SIGUSR1 "$pid"; }
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "usage: hyprland-tablet {status|keyboard-toggle|osk-show|osk-hide}" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
{
|
||||||
|
home.packages = [ cli ];
|
||||||
|
|
||||||
|
systemd.user.services.hyprland-tablet = {
|
||||||
|
Unit = {
|
||||||
|
Description = "Hyprland tablet-mode auto-rotation + on-screen keyboard";
|
||||||
|
After = [ "graphical-session.target" ];
|
||||||
|
PartOf = [ "graphical-session.target" ];
|
||||||
|
};
|
||||||
|
Service = {
|
||||||
|
Type = "simple";
|
||||||
|
ExecStart = daemon;
|
||||||
|
Restart = "on-failure";
|
||||||
|
};
|
||||||
|
Install = {
|
||||||
|
WantedBy = [ "graphical-session.target" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,82 @@
|
|||||||
|
{ pkgs, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
palette = import ./palette.nix;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
programs.kitty = {
|
||||||
|
enable = true;
|
||||||
|
font = {
|
||||||
|
name = "FiraCode Nerd Font";
|
||||||
|
size = 12;
|
||||||
|
};
|
||||||
|
settings = {
|
||||||
|
enabled_layouts = "tall:bias=50;full_size=1;mirrored=false,fat,grid,stack";
|
||||||
|
scrollback_lines = 10000;
|
||||||
|
enable_audio_bell = false;
|
||||||
|
update_check_interval = 0;
|
||||||
|
shell_integration = "enabled";
|
||||||
|
|
||||||
|
# Performance
|
||||||
|
repaint_delay = 10;
|
||||||
|
input_delay = 3;
|
||||||
|
sync_to_monitor = "yes";
|
||||||
|
|
||||||
|
# Window layout
|
||||||
|
window_padding_width = 10;
|
||||||
|
confirm_os_window_close = 0;
|
||||||
|
background_opacity = "0.85";
|
||||||
|
dynamic_background_opacity = "yes";
|
||||||
|
|
||||||
|
# Tabs (colors live in colors.conf so `qs-theme` can override them)
|
||||||
|
tab_bar_style = "powerline";
|
||||||
|
};
|
||||||
|
keybindings = {
|
||||||
|
"ctrl+shift+c" = "copy_to_clipboard";
|
||||||
|
"ctrl+shift+v" = "paste_from_clipboard";
|
||||||
|
"ctrl+shift+enter" = "new_window";
|
||||||
|
"ctrl+shift+]" = "next_window";
|
||||||
|
"ctrl+shift+[" = "previous_window";
|
||||||
|
"ctrl+shift+l" = "next_layout";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Colors live in an include so a wallpaper-driven palette (`qs-theme`,
|
||||||
|
# via matugen) can overwrite them without touching the rest of kitty.conf.
|
||||||
|
extraConfig = "include colors.conf";
|
||||||
|
};
|
||||||
|
|
||||||
|
xdg.configFile."kitty/colors.conf".text = ''
|
||||||
|
# Color palette (Tokyo Night inspired) — declared in home-manager.
|
||||||
|
# Regenerated by `~/.local/bin/qs-theme <wallpaper>` (matugen) at runtime.
|
||||||
|
background ${palette.hash palette.bgDark}
|
||||||
|
foreground ${palette.hash palette.muted}
|
||||||
|
selection_background ${palette.hash palette.selection}
|
||||||
|
selection_foreground ${palette.hash palette.text}
|
||||||
|
url_color ${palette.hash palette.teal}
|
||||||
|
cursor ${palette.hash palette.text}
|
||||||
|
cursor_text_color ${palette.hash palette.bgDark}
|
||||||
|
|
||||||
|
active_tab_background ${palette.hash palette.blue}
|
||||||
|
active_tab_foreground ${palette.hash palette.tabFgBright}
|
||||||
|
inactive_tab_background ${palette.hash palette.tabBg}
|
||||||
|
inactive_tab_foreground ${palette.hash palette.tabFg}
|
||||||
|
|
||||||
|
# ANSI 16
|
||||||
|
color0 ${palette.hash palette.color0}
|
||||||
|
color1 ${palette.hash palette.danger}
|
||||||
|
color2 ${palette.hash palette.green}
|
||||||
|
color3 ${palette.hash palette.yellow}
|
||||||
|
color4 ${palette.hash palette.blue}
|
||||||
|
color5 ${palette.hash palette.purple}
|
||||||
|
color6 ${palette.hash palette.cyan}
|
||||||
|
color7 ${palette.hash palette.muted}
|
||||||
|
color8 ${palette.hash palette.color8}
|
||||||
|
color9 ${palette.hash palette.danger}
|
||||||
|
color10 ${palette.hash palette.green}
|
||||||
|
color11 ${palette.hash palette.yellow}
|
||||||
|
color12 ${palette.hash palette.blue}
|
||||||
|
color13 ${palette.hash palette.purple}
|
||||||
|
color14 ${palette.hash palette.cyan}
|
||||||
|
color15 ${palette.hash palette.text}
|
||||||
|
'';
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
background {{colors.surface.dark.hex}}
|
||||||
|
foreground {{colors.on_surface.dark.hex}}
|
||||||
|
selection_background {{colors.primary.dark.hex}}
|
||||||
|
selection_foreground {{colors.on_primary.dark.hex}}
|
||||||
|
url_color {{colors.tertiary.dark.hex}}
|
||||||
|
cursor {{colors.on_surface.dark.hex}}
|
||||||
|
cursor_text_color {{colors.surface.dark.hex}}
|
||||||
|
|
||||||
|
active_tab_background {{colors.primary.dark.hex}}
|
||||||
|
active_tab_foreground {{colors.on_primary.dark.hex}}
|
||||||
|
inactive_tab_background {{colors.surface_container_low.dark.hex}}
|
||||||
|
inactive_tab_foreground {{colors.on_surface_variant.dark.hex}}
|
||||||
|
|
||||||
|
# ANSI 16 — mapped to Material-You roles (NOT base16: matugen's base16 dark
|
||||||
|
# accents collapse to near-black, making colored terminal text unreadable).
|
||||||
|
# Material roles are the dark-mode light-tinted variants, all >10:1 contrast
|
||||||
|
# on the surface, so both text and UI stay legible.
|
||||||
|
color0 {{colors.surface.dark.hex}}
|
||||||
|
color1 {{colors.error.dark.hex}}
|
||||||
|
color2 {{colors.tertiary.dark.hex}}
|
||||||
|
color3 {{colors.secondary.dark.hex}}
|
||||||
|
color4 {{colors.primary.dark.hex}}
|
||||||
|
color5 {{colors.tertiary.dark.hex}}
|
||||||
|
color6 {{colors.secondary.dark.hex}}
|
||||||
|
color7 {{colors.on_surface.dark.hex}}
|
||||||
|
color8 {{colors.outline.dark.hex}}
|
||||||
|
color9 {{colors.error.dark.hex}}
|
||||||
|
color10 {{colors.tertiary.dark.hex}}
|
||||||
|
color11 {{colors.secondary.dark.hex}}
|
||||||
|
color12 {{colors.primary.dark.hex}}
|
||||||
|
color13 {{colors.tertiary.dark.hex}}
|
||||||
|
color14 {{colors.secondary.dark.hex}}
|
||||||
|
color15 {{colors.on_surface.dark.hex}}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"surface": "{{colors.surface.dark.hex}}",
|
||||||
|
"ink": "{{colors.background.dark.hex}}",
|
||||||
|
"text": "{{colors.on_surface.dark.hex}}",
|
||||||
|
"muted": "{{colors.on_surface_variant.dark.hex}}",
|
||||||
|
"line": "{{colors.outline.dark.hex}}",
|
||||||
|
"neon": "{{colors.primary.dark.hex}}",
|
||||||
|
"violet": "{{colors.tertiary.dark.hex}}",
|
||||||
|
"magenta": "{{colors.secondary.dark.hex}}",
|
||||||
|
"danger": "{{colors.error.dark.hex}}"
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Wallpaper-driven Material-You theming (matugen) for the QuickShell shell.
|
||||||
|
# `qs-theme <wallpaper>` regenerates:
|
||||||
|
# - ~/.cache/quickshell/theme.json → live-repaints the bar (FileView watcher)
|
||||||
|
# - ~/.config/kitty/colors.conf → kitty palette + ANSI 16
|
||||||
|
# The Nix-declared colors.conf symlink is restored on the next home-manager
|
||||||
|
# rebuild; until then the generated palette wins (see the rm in qs-theme).
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
home.packages = [ pkgs.matugen ];
|
||||||
|
|
||||||
|
# Default matugen config — renders our two templates on every run.
|
||||||
|
xdg.configFile."matugen/config.toml".text = ''
|
||||||
|
[config]
|
||||||
|
|
||||||
|
[templates.quick-shell]
|
||||||
|
input_path = "${config.home.homeDirectory}/.config/matugen/themes/theme.json.template"
|
||||||
|
output_path = "${config.home.homeDirectory}/.cache/quickshell/theme.json"
|
||||||
|
|
||||||
|
[templates.kitty]
|
||||||
|
input_path = "${config.home.homeDirectory}/.config/matugen/themes/kitty-colors.conf.template"
|
||||||
|
output_path = "${config.home.homeDirectory}/.config/kitty/colors.conf"
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Template sources live in the repo so matugen can read them at runtime.
|
||||||
|
xdg.configFile."matugen/themes/theme.json.template".text =
|
||||||
|
builtins.readFile ./matugen-themes/theme.json.template;
|
||||||
|
xdg.configFile."matugen/themes/kitty-colors.conf.template".text =
|
||||||
|
builtins.readFile ./matugen-themes/kitty-colors.conf.template;
|
||||||
|
|
||||||
|
# qs-theme — regenerate the wallpaper palette, then repaint the live bar.
|
||||||
|
home.file.".local/bin/qs-theme" = {
|
||||||
|
executable = true;
|
||||||
|
text = ''
|
||||||
|
#!/bin/sh
|
||||||
|
# Regenerate the Material-You palette (QuickShell bar + kitty) from a
|
||||||
|
# wallpaper. The bar picks it up live via its theme.json watcher.
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ "$#" -lt 1 ]; then
|
||||||
|
echo "usage: qs-theme <wallpaper>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# matugen refuses to overwrite the Nix-declared symlink; unlink it first
|
||||||
|
# (home-manager restores it on the next rebuild).
|
||||||
|
rm -f "$HOME/.config/kitty/colors.conf"
|
||||||
|
|
||||||
|
exec ${pkgs.matugen}/bin/matugen image "$1" -m dark --prefer darkness
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{ pkgs, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
# nil (Nix language server) for x1carbon only — wired into VS Code via the
|
||||||
|
# nix-community Nix IDE extension and into opencode (see opencode.nix).
|
||||||
|
|
||||||
|
home.packages = [ pkgs.nil ];
|
||||||
|
|
||||||
|
programs.vscode = {
|
||||||
|
enable = true;
|
||||||
|
profiles.default = {
|
||||||
|
extensions = [ pkgs.vscode-extensions.jnoortheen.nix-ide ];
|
||||||
|
userSettings = {
|
||||||
|
"nix.enableLanguageServer" = true;
|
||||||
|
"nix.serverPath" = "${pkgs.nil}/bin/nil";
|
||||||
|
# All nil LSP settings nest under a "nil" key. Formatting shells out
|
||||||
|
# to nixfmt (the flake formatter), reading stdin / writing stdout.
|
||||||
|
"nix.serverSettings" = {
|
||||||
|
nil = {
|
||||||
|
formatting = {
|
||||||
|
command = [
|
||||||
|
"nixfmt"
|
||||||
|
"-"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
{ pkgs, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
# mcp-nixos gives opencode + VS Code real NixOS package/option data instead of
|
||||||
|
# hallucinated package names. Only installed on x1carbon (this module is only
|
||||||
|
# imported from hosts/x1carbon), so it does not leak to other machines.
|
||||||
|
home.packages = [ pkgs.mcp-nixos ];
|
||||||
|
|
||||||
|
xdg.configFile."opencode/opencode.json".text = builtins.toJSON {
|
||||||
|
"$schema" = "https://opencode.ai/config.json";
|
||||||
|
provider = {
|
||||||
|
# lmstudio = {
|
||||||
|
# npm = "@ai-sdk/openai-compatible";
|
||||||
|
# name = "LM Studio (local)";
|
||||||
|
# options = {
|
||||||
|
# baseURL = "http://127.0.0.1:1234/v1";
|
||||||
|
# };
|
||||||
|
# models = {
|
||||||
|
# "google/gemma-4-e4b" = {
|
||||||
|
# name = "Gemma 4 E4B (LM Studio)";
|
||||||
|
# limit = {
|
||||||
|
# context = 65536;
|
||||||
|
# output = 32768;
|
||||||
|
# };
|
||||||
|
# };
|
||||||
|
# };
|
||||||
|
# };
|
||||||
|
opencode-go = { };
|
||||||
|
};
|
||||||
|
model = "opencode-go/kimi-k2.6";
|
||||||
|
lsp = {
|
||||||
|
# nil (Nix language server, installed by nix-lsp.nix) as a custom LSP
|
||||||
|
# server so the agent gets Nix diagnostics. Formatter: nixfmt (stdin).
|
||||||
|
nil = {
|
||||||
|
command = [ "${pkgs.nil}/bin/nil" ];
|
||||||
|
extensions = [ ".nix" ];
|
||||||
|
initialization = {
|
||||||
|
nil = {
|
||||||
|
formatting = {
|
||||||
|
command = [
|
||||||
|
"nixfmt"
|
||||||
|
"-"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
mcp = {
|
||||||
|
# NixOS packages/options MCP server (stdlib CLI; binary provided by
|
||||||
|
# pkgs.mcp-nixos). Use "the nix tool" to look up real package names,
|
||||||
|
# options and versions while editing flake/host configs.
|
||||||
|
nixos = {
|
||||||
|
type = "local";
|
||||||
|
command = [ "mcp-nixos" ];
|
||||||
|
enabled = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# Shared color palette for the "sci-fi dark glass + vivid neon" desktop.
|
||||||
|
# Single source of truth: every consumer (QuickShell QML theme, kitty,
|
||||||
|
# wvkbd, wofi, hyprlock, Hyprland borders/shadows, matugen fallback) reads
|
||||||
|
# its colors from here instead of hardcoding hex literals.
|
||||||
|
#
|
||||||
|
# Colors are stored as raw hex WITHOUT the leading '#'. Use the helpers below
|
||||||
|
# to format for each consumer:
|
||||||
|
# palette.hash "#rrggbb" (QML, kitty, wofi borders)
|
||||||
|
# palette.cssRgba "rgba(r, g, b, a)" (wofi)
|
||||||
|
# palette.lockRgba "rgba(r, g, b, a)" (hyprlock)
|
||||||
|
# palette.hyprRgba "rgba(rrggbbaa)" (Hyprland, compact hex+alpha)
|
||||||
|
# palette.rgb [ r g b ] decimals
|
||||||
|
rec {
|
||||||
|
# ---- Surfaces (translucent over the Hyprland layer blur) ----
|
||||||
|
# glass/glassPanel are 8-digit AARRGGBB for QML only.
|
||||||
|
glass = "8c0f111c";
|
||||||
|
glassPanel = "c2141627";
|
||||||
|
surface = "24283b";
|
||||||
|
panel = "141627"; # wofi / hyprlock solid panel
|
||||||
|
line = "394b70";
|
||||||
|
|
||||||
|
# ---- Text ----
|
||||||
|
text = "c0caf5";
|
||||||
|
muted = "a9b1d6";
|
||||||
|
ink = "0b0e14";
|
||||||
|
|
||||||
|
# ---- Neon accents ----
|
||||||
|
neon = "00e5ff";
|
||||||
|
magenta = "ff2e97";
|
||||||
|
violet = "7c4dff";
|
||||||
|
cyan = "7dcfff";
|
||||||
|
blue = "7aa2f7";
|
||||||
|
green = "9ece6a";
|
||||||
|
yellow = "e0af68";
|
||||||
|
purple = "bb9af7";
|
||||||
|
teal = "73daca"; # kitty url_color
|
||||||
|
danger = "f7768e";
|
||||||
|
|
||||||
|
# ---- kitty extras (Tokyo Night inspired) ----
|
||||||
|
bgDark = "1a1b26";
|
||||||
|
color0 = "15161e";
|
||||||
|
color8 = "414868";
|
||||||
|
selection = "33467c";
|
||||||
|
tabBg = "292e42";
|
||||||
|
tabFg = "545c7e";
|
||||||
|
tabFgBright = "16161e";
|
||||||
|
|
||||||
|
# ---- wvkbd ----
|
||||||
|
wvkbdBg = "1b1e2d";
|
||||||
|
wvkbdFgSp = "1f2740";
|
||||||
|
|
||||||
|
# ---- helpers ----
|
||||||
|
hexToInt =
|
||||||
|
h:
|
||||||
|
let
|
||||||
|
m = {
|
||||||
|
"0" = 0;
|
||||||
|
"1" = 1;
|
||||||
|
"2" = 2;
|
||||||
|
"3" = 3;
|
||||||
|
"4" = 4;
|
||||||
|
"5" = 5;
|
||||||
|
"6" = 6;
|
||||||
|
"7" = 7;
|
||||||
|
"8" = 8;
|
||||||
|
"9" = 9;
|
||||||
|
a = 10;
|
||||||
|
b = 11;
|
||||||
|
c = 12;
|
||||||
|
d = 13;
|
||||||
|
e = 14;
|
||||||
|
f = 15;
|
||||||
|
};
|
||||||
|
len = builtins.stringLength h;
|
||||||
|
chars = builtins.genList (i: builtins.substring i 1 h) len;
|
||||||
|
in
|
||||||
|
builtins.foldl' (acc: c: acc * 16 + m.${c}) 0 chars;
|
||||||
|
|
||||||
|
rgb = h: [
|
||||||
|
(hexToInt (builtins.substring 0 2 h))
|
||||||
|
(hexToInt (builtins.substring 2 2 h))
|
||||||
|
(hexToInt (builtins.substring 4 2 h))
|
||||||
|
];
|
||||||
|
|
||||||
|
hash = c: "#${c}";
|
||||||
|
cssRgba = c: a: "rgba(${builtins.concatStringsSep ", " (map toString (rgb c))}, ${a})";
|
||||||
|
lockRgba = c: a: "rgba(${builtins.concatStringsSep ", " (map toString (rgb c))}, ${a})";
|
||||||
|
hyprRgba = c: a: "rgba(${c}${a})";
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
|
# QuickShell autostart app manager: lets the user pick from the gear
|
||||||
|
# quick-settings panel which installed apps start at login. The candidate
|
||||||
|
# *pool* is declared here (services.quickshell-apps.apps) and seeded into
|
||||||
|
# ~/.config/quickshell/autostartseed.json; runtime on/off state and any apps
|
||||||
|
# added from the UI live in ~/.cache/quickshell/autostart.json. The Hyprland
|
||||||
|
# login autostart runs `qs-apps run` (see home-manager/modules/hyprland.nix)
|
||||||
|
# to launch whatever is enabled.
|
||||||
|
{
|
||||||
|
options.services.quickshell-apps = {
|
||||||
|
enable = lib.mkEnableOption "QuickShell autostart app manager (runtime toggle from the gear panel)";
|
||||||
|
|
||||||
|
apps = lib.mkOption {
|
||||||
|
type = lib.types.listOf (
|
||||||
|
lib.types.submodule {
|
||||||
|
options = {
|
||||||
|
name = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "Display name shown in the gear panel.";
|
||||||
|
};
|
||||||
|
cmd = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "Command to launch the app (shell-style, argv 0 must name the binary).";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
default = [ ];
|
||||||
|
description = "Candidate pool the user can toggle/start from the shell.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf config.services.quickshell-apps.enable {
|
||||||
|
# Store the manager in the Nix store with the procps tools baked in (the
|
||||||
|
# Hyprland login autostart PATH is not guaranteed to have pgrep/pkill).
|
||||||
|
xdg.configFile."quickshell/qs-apps.py".text =
|
||||||
|
builtins.replaceStrings
|
||||||
|
[
|
||||||
|
"@pgrep@"
|
||||||
|
"@pkill@"
|
||||||
|
]
|
||||||
|
[
|
||||||
|
"${pkgs.procps}/bin/pgrep"
|
||||||
|
"${pkgs.procps}/bin/pkill"
|
||||||
|
]
|
||||||
|
(builtins.readFile ./quickshell-apps.py);
|
||||||
|
|
||||||
|
# Nix-declared pool. The runtime state file is authoritative for on/off
|
||||||
|
# and for UI-added apps; the manager merges this in on every load, adding
|
||||||
|
# new entries as disabled.
|
||||||
|
xdg.configFile."quickshell/autostartseed.json".text = builtins.toJSON (
|
||||||
|
map (app: {
|
||||||
|
name = app.name;
|
||||||
|
cmd = app.cmd;
|
||||||
|
}) config.services.quickshell-apps.apps
|
||||||
|
);
|
||||||
|
|
||||||
|
home.file.".local/bin/qs-apps" = {
|
||||||
|
executable = true;
|
||||||
|
text = ''
|
||||||
|
#!/bin/sh
|
||||||
|
exec ${pkgs.python3}/bin/python3 \
|
||||||
|
${config.home.homeDirectory}/.config/quickshell/qs-apps.py "$@"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,287 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""QuickShell autostart app manager.
|
||||||
|
|
||||||
|
Lets the user pick which installed apps start at login, toggled live from the
|
||||||
|
gear quick-settings panel (no rebuild needed for preference changes).
|
||||||
|
|
||||||
|
Model
|
||||||
|
-----
|
||||||
|
The *pool* of candidates is declared in Nix (host config) and written to
|
||||||
|
~/.config/quickshell/autostartseed.json. The runtime *state* lives in
|
||||||
|
~/.cache/quickshell/autostart.json and is authoritative for on/off and for
|
||||||
|
apps the user adds from the UI (source = "user"). On every `list`/`run` the
|
||||||
|
seed is merged in: new seed entries are added disabled and, for existing seed
|
||||||
|
entries, the Nix command is re-applied while keeping the enabled flag. A
|
||||||
|
user-added app named like a seed entry wins (the seed never re-enables it).
|
||||||
|
|
||||||
|
Commands
|
||||||
|
--------
|
||||||
|
list -> JSON {apps: [{name, cmd, enabled, source}]} (after merge)
|
||||||
|
toggle <name> -> flip enabled; start/stop the app now, then persist
|
||||||
|
add <name> <cmd> -> create a user entry (enabled, starts now); if the name
|
||||||
|
already exists, just enable it
|
||||||
|
remove <name> -> stop (best-effort) and forget the entry
|
||||||
|
run -> login autostart: launch every enabled app (no dupes)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shlex
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
HOME = os.path.expanduser("~")
|
||||||
|
CACHE_DIR = os.path.join(HOME, ".cache", "quickshell")
|
||||||
|
STATE_FILE = os.path.join(CACHE_DIR, "autostart.json")
|
||||||
|
SEED_FILE = os.path.join(HOME, ".config", "quickshell", "autostartseed.json")
|
||||||
|
|
||||||
|
PGREP = "/run/current-system/sw/bin/pgrep"
|
||||||
|
PKILL = "/run/current-system/sw/bin/pkill"
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_cache_dir():
|
||||||
|
os.makedirs(CACHE_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def load_state():
|
||||||
|
try:
|
||||||
|
with open(STATE_FILE) as fh:
|
||||||
|
data = json.load(fh)
|
||||||
|
except (OSError, ValueError):
|
||||||
|
data = {"apps": []}
|
||||||
|
if not isinstance(data, dict) or not isinstance(data.get("apps"), list):
|
||||||
|
data = {"apps": []}
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def save_state(data):
|
||||||
|
ensure_cache_dir()
|
||||||
|
with open(STATE_FILE, "w") as fh:
|
||||||
|
json.dump(data, fh, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
def load_seed():
|
||||||
|
try:
|
||||||
|
with open(SEED_FILE) as fh:
|
||||||
|
seed = json.load(fh)
|
||||||
|
except (OSError, ValueError):
|
||||||
|
return []
|
||||||
|
if not isinstance(seed, list):
|
||||||
|
return []
|
||||||
|
out = []
|
||||||
|
for entry in seed:
|
||||||
|
if isinstance(entry, dict) and entry.get("name") and entry.get("cmd"):
|
||||||
|
out.append({"name": str(entry["name"]), "cmd": str(entry["cmd"])})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def merge_seed(data):
|
||||||
|
"""Apply the Nix-declared pool to the runtime state (add-missing only)."""
|
||||||
|
seed = load_seed()
|
||||||
|
for s in seed:
|
||||||
|
found = next(
|
||||||
|
(a for a in data["apps"] if a.get("name", "").lower() == s["name"].lower()),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if found is None:
|
||||||
|
data["apps"].append(
|
||||||
|
{"name": s["name"], "cmd": s["cmd"], "enabled": False, "source": "seed"}
|
||||||
|
)
|
||||||
|
elif found.get("source") == "seed":
|
||||||
|
found["cmd"] = s["cmd"]
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def find_app(data, name):
|
||||||
|
return next(
|
||||||
|
(a for a in data["apps"] if a.get("name", "").lower() == name.lower()), None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def binary_of(cmd):
|
||||||
|
try:
|
||||||
|
toks = shlex.split(cmd)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
return os.path.basename(toks[0]) if toks else None
|
||||||
|
|
||||||
|
|
||||||
|
def pattern_of(cmd):
|
||||||
|
"""pgrep/pkill -f pattern for the app's executable.
|
||||||
|
|
||||||
|
Electron apps (element-desktop, bitwarden-desktop) run with comm=electron,
|
||||||
|
so comm (-x) matching misses them and we match the full command line
|
||||||
|
instead. The bracket trick 'e[e]lement-desktop' still matches
|
||||||
|
element-desktop but never the pgrep/pkill command itself (its own cmdline
|
||||||
|
contains the literal bracketed form) — avoiding the classic -f self-match.
|
||||||
|
"""
|
||||||
|
binary = binary_of(cmd)
|
||||||
|
if not binary:
|
||||||
|
return None
|
||||||
|
if len(binary) < 2:
|
||||||
|
return binary
|
||||||
|
return binary[:1] + "[" + binary[1:2] + "]" + binary[2:]
|
||||||
|
|
||||||
|
|
||||||
|
def _self_and_ancestors():
|
||||||
|
"""PIDs of this process and its parent chain.
|
||||||
|
|
||||||
|
The -f guard can be fooled by the *invoking* process: `qs-apps add X "cmd"`
|
||||||
|
passes the command as an argv element, so our own cmdline (and ancestors
|
||||||
|
like a `sh -c 'qs-apps add X cmd'` wrapper) contains the app's binary name
|
||||||
|
and pgrep -f would match it instead of the real app. Excluding the whole
|
||||||
|
ancestor chain makes the guard see only genuinely running apps.
|
||||||
|
"""
|
||||||
|
pids = {os.getpid()}
|
||||||
|
ppid = os.getppid()
|
||||||
|
seen = set()
|
||||||
|
while ppid > 1 and ppid not in seen:
|
||||||
|
seen.add(ppid)
|
||||||
|
pids.add(ppid)
|
||||||
|
try:
|
||||||
|
with open("/proc/%d/stat" % ppid) as fh:
|
||||||
|
parts = fh.read().split()
|
||||||
|
ppid = int(parts[3])
|
||||||
|
except (OSError, IndexError, ValueError):
|
||||||
|
break
|
||||||
|
return pids
|
||||||
|
|
||||||
|
|
||||||
|
def is_running(pattern):
|
||||||
|
if not pattern:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
r = subprocess.run([PGREP, "-f", pattern], capture_output=True, text=True)
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
if r.returncode != 0:
|
||||||
|
return False
|
||||||
|
self_pids = _self_and_ancestors()
|
||||||
|
for line in r.stdout.splitlines():
|
||||||
|
try:
|
||||||
|
pid = int(line.strip())
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
if pid not in self_pids:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def start_app(cmd):
|
||||||
|
"""Launch cmd detached. Returns True if it is (or already is) running."""
|
||||||
|
try:
|
||||||
|
toks = shlex.split(cmd)
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
if not toks:
|
||||||
|
return False
|
||||||
|
pattern = pattern_of(cmd)
|
||||||
|
if pattern and is_running(pattern):
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
subprocess.Popen(
|
||||||
|
toks,
|
||||||
|
stdin=subprocess.DEVNULL,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
start_new_session=True,
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def stop_app(cmd):
|
||||||
|
"""Best-effort kill by -f pattern (comm is 'electron' for Electron apps)."""
|
||||||
|
pattern = pattern_of(cmd)
|
||||||
|
if not pattern:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
subprocess.run([PKILL, "-f", pattern], capture_output=True)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_toggle(name):
|
||||||
|
data = merge_seed(load_state())
|
||||||
|
app = find_app(data, name)
|
||||||
|
if app is None:
|
||||||
|
return {"ok": False, "error": "no app named " + name}
|
||||||
|
app["enabled"] = not app["enabled"]
|
||||||
|
if app["enabled"]:
|
||||||
|
started = start_app(app["cmd"])
|
||||||
|
else:
|
||||||
|
stop_app(app["cmd"])
|
||||||
|
started = True
|
||||||
|
save_state(data)
|
||||||
|
return {"ok": started, "enabled": app["enabled"]}
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_add(name, cmd):
|
||||||
|
data = merge_seed(load_state())
|
||||||
|
app = find_app(data, name)
|
||||||
|
if app is None:
|
||||||
|
data["apps"].append(
|
||||||
|
{"name": name, "cmd": cmd, "enabled": True, "source": "user"}
|
||||||
|
)
|
||||||
|
app = data["apps"][-1]
|
||||||
|
else:
|
||||||
|
app["enabled"] = True
|
||||||
|
if app.get("source") == "user":
|
||||||
|
app["cmd"] = cmd
|
||||||
|
started = start_app(app["cmd"])
|
||||||
|
save_state(data)
|
||||||
|
return {"ok": started}
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_remove(name):
|
||||||
|
data = merge_seed(load_state())
|
||||||
|
app = find_app(data, name)
|
||||||
|
if app is None:
|
||||||
|
return {"ok": True}
|
||||||
|
stop_app(app["cmd"])
|
||||||
|
data["apps"] = [a for a in data["apps"] if a is not app]
|
||||||
|
save_state(data)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_run():
|
||||||
|
data = merge_seed(load_state())
|
||||||
|
for app in data["apps"]:
|
||||||
|
if app.get("enabled"):
|
||||||
|
start_app(app["cmd"])
|
||||||
|
save_state(data)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_list():
|
||||||
|
data = merge_seed(load_state())
|
||||||
|
save_state(data)
|
||||||
|
return {"apps": data["apps"]}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = sys.argv[1:]
|
||||||
|
cmd = args[0] if args else "list"
|
||||||
|
|
||||||
|
if cmd == "list":
|
||||||
|
out = cmd_list()
|
||||||
|
print(json.dumps(out))
|
||||||
|
elif cmd == "run":
|
||||||
|
print(json.dumps(cmd_run()))
|
||||||
|
elif cmd == "toggle":
|
||||||
|
name = args[1] if len(args) > 1 else ""
|
||||||
|
print(json.dumps(cmd_toggle(name)))
|
||||||
|
elif cmd == "add":
|
||||||
|
name = args[1] if len(args) > 1 else ""
|
||||||
|
command = args[2] if len(args) > 2 else ""
|
||||||
|
print(json.dumps(cmd_add(name, command)))
|
||||||
|
elif cmd == "remove":
|
||||||
|
name = args[1] if len(args) > 1 else ""
|
||||||
|
print(json.dumps(cmd_remove(name)))
|
||||||
|
else:
|
||||||
|
print(json.dumps({"ok": False, "error": "unknown command: " + cmd}))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""QuickShell bluetooth manager backend.
|
||||||
|
|
||||||
|
Talks to bluez via `bluetoothctl` (text/shell parsing — the tool has no JSON
|
||||||
|
mode). State is read live from the adapter (`bluetoothctl show`) and per device
|
||||||
|
(`bluetoothctl info <mac>`), so it reflects reality even after pairing from
|
||||||
|
another app. The `qs-bt` wrapper (home-manager hyprland module) passes the
|
||||||
|
absolute bluetoothctl path in $BLUECTL.
|
||||||
|
|
||||||
|
Commands
|
||||||
|
--------
|
||||||
|
status -> JSON adapter + devices
|
||||||
|
{powered,name,alias,pairable,discoverable,
|
||||||
|
discovering,devices:[{mac,name,icon,paired,
|
||||||
|
trusted,connected,battery}]}
|
||||||
|
power on|off -> toggle adapter power -> JSON ok
|
||||||
|
scan -> discovery for SCAN_SECONDS, then stop; JSON
|
||||||
|
list of {mac,name} found during the scan
|
||||||
|
connect <mac> / disconnect <mac>
|
||||||
|
pair <mac> -> pair (bounded; pairing prompts end it)
|
||||||
|
trust <mac> / untrust <mac>
|
||||||
|
remove <mac> -> unpair / forget
|
||||||
|
All non-status commands return {"ok": bool, "error": "..."}.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
BLUECTL = os.environ.get("BLUECTL", "bluetoothctl")
|
||||||
|
SCAN_SECONDS = 8
|
||||||
|
PAIR_TIMEOUT = 30
|
||||||
|
CONNECT_TIMEOUT = 12
|
||||||
|
WAIT_TIMEOUT = 10
|
||||||
|
|
||||||
|
|
||||||
|
def run(args, timeout=WAIT_TIMEOUT):
|
||||||
|
try:
|
||||||
|
p = subprocess.run(
|
||||||
|
[BLUECTL] + args,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
return p.returncode, p.stdout, p.stderr
|
||||||
|
except subprocess.TimeoutExpired as exc:
|
||||||
|
tail = (exc.stdout or exc.stderr or b"")
|
||||||
|
if isinstance(tail, bytes):
|
||||||
|
tail = tail.decode(errors="replace")
|
||||||
|
return -1, tail, "command timed out"
|
||||||
|
except OSError as exc:
|
||||||
|
return -2, "", str(exc)
|
||||||
|
|
||||||
|
|
||||||
|
def field(line, key):
|
||||||
|
low = line.lstrip()
|
||||||
|
return low.split(":", 1)[1].strip() if low.startswith(key + ":") else None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_adapter(out):
|
||||||
|
"""Adapter sections from `bluetoothctl show`; prefer the [default] one."""
|
||||||
|
sections = []
|
||||||
|
cur = None
|
||||||
|
for line in out.splitlines():
|
||||||
|
if line.startswith("Controller "):
|
||||||
|
bits = line.split()
|
||||||
|
cur = {"mac": bits[1], "default": "[default]" in line}
|
||||||
|
sections.append(cur)
|
||||||
|
elif cur is not None:
|
||||||
|
v = field(line, "Powered")
|
||||||
|
if v:
|
||||||
|
cur["powered"] = v == "yes"
|
||||||
|
v = field(line, "Discovering")
|
||||||
|
if v:
|
||||||
|
cur["discovering"] = v == "yes"
|
||||||
|
v = field(line, "Pairable")
|
||||||
|
if v:
|
||||||
|
cur["pairable"] = v == "yes"
|
||||||
|
v = field(line, "Discoverable")
|
||||||
|
if v:
|
||||||
|
cur["discoverable"] = v == "yes"
|
||||||
|
v = field(line, "Name")
|
||||||
|
if v:
|
||||||
|
cur["name"] = v
|
||||||
|
v = field(line, "Alias")
|
||||||
|
if v:
|
||||||
|
cur["alias"] = v
|
||||||
|
for s in sections:
|
||||||
|
if s.get("default"):
|
||||||
|
return s
|
||||||
|
return sections[0] if sections else None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_device(mac):
|
||||||
|
rc, out, _ = run(["info", mac], timeout=WAIT_TIMEOUT)
|
||||||
|
dev = {
|
||||||
|
"mac": mac,
|
||||||
|
"name": "",
|
||||||
|
"icon": "",
|
||||||
|
"paired": False,
|
||||||
|
"trusted": False,
|
||||||
|
"connected": False,
|
||||||
|
"battery": None,
|
||||||
|
}
|
||||||
|
if rc != 0:
|
||||||
|
return dev
|
||||||
|
for line in out.splitlines():
|
||||||
|
v = field(line, "Name")
|
||||||
|
if v:
|
||||||
|
dev["name"] = v
|
||||||
|
v = field(line, "Icon")
|
||||||
|
if v:
|
||||||
|
dev["icon"] = v
|
||||||
|
v = field(line, "Paired")
|
||||||
|
if v:
|
||||||
|
dev["paired"] = v == "yes"
|
||||||
|
v = field(line, "Trusted")
|
||||||
|
if v:
|
||||||
|
dev["trusted"] = v == "yes"
|
||||||
|
v = field(line, "Connected")
|
||||||
|
if v:
|
||||||
|
dev["connected"] = v == "yes"
|
||||||
|
m = re.search(r"Battery Percentage:\s*0x[0-9A-Fa-f]+\s*\((\d+)\)", line)
|
||||||
|
if m:
|
||||||
|
dev["battery"] = int(m.group(1))
|
||||||
|
return dev
|
||||||
|
|
||||||
|
|
||||||
|
def list_devices():
|
||||||
|
rc, out, _ = run(["devices"], timeout=WAIT_TIMEOUT)
|
||||||
|
macs = []
|
||||||
|
if rc == 0:
|
||||||
|
for line in out.splitlines():
|
||||||
|
bits = line.split()
|
||||||
|
if len(bits) >= 2 and bits[0] == "Device":
|
||||||
|
mac = bits[1]
|
||||||
|
if mac not in macs:
|
||||||
|
macs.append(mac)
|
||||||
|
return [parse_device(m) for m in macs]
|
||||||
|
|
||||||
|
|
||||||
|
def status():
|
||||||
|
rc, out, _ = run(["show"], timeout=WAIT_TIMEOUT)
|
||||||
|
adapter = parse_adapter(out) if rc == 0 else None
|
||||||
|
devs = list_devices()
|
||||||
|
# connected first, then alphabetical — the stable order the popup renders.
|
||||||
|
devs.sort(key=lambda d: (not d["connected"], (d["name"] or d["mac"]).lower()))
|
||||||
|
return {
|
||||||
|
"powered": bool(adapter and adapter.get("powered")),
|
||||||
|
"name": adapter.get("name") if adapter else "",
|
||||||
|
"alias": adapter.get("alias") if adapter else "",
|
||||||
|
"pairable": bool(adapter and adapter.get("pairable")),
|
||||||
|
"discoverable": bool(adapter and adapter.get("discoverable")),
|
||||||
|
"discovering": bool(adapter and adapter.get("discovering")),
|
||||||
|
"devices": devs,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def scan():
|
||||||
|
"""Discovery for SCAN_SECONDS; bluetoothctl parses NEW/CHG device lines."""
|
||||||
|
rc, out, _ = run(
|
||||||
|
["--timeout", str(SCAN_SECONDS), "scan", "on"],
|
||||||
|
timeout=SCAN_SECONDS + 8,
|
||||||
|
)
|
||||||
|
found = {}
|
||||||
|
for line in out.splitlines():
|
||||||
|
if line.startswith("[NEW] Device"):
|
||||||
|
mac, _, name = line.split()[2:5]
|
||||||
|
found[mac] = name
|
||||||
|
elif line.startswith("[CHG] Device"):
|
||||||
|
bits = line.split()
|
||||||
|
if len(bits) >= 5 and bits[3] == "Name:":
|
||||||
|
found[bits[2]] = line.split("Name:", 1)[1].strip()
|
||||||
|
return [{"mac": m, "name": n} for m, n in sorted(found.items())]
|
||||||
|
|
||||||
|
|
||||||
|
def action(args):
|
||||||
|
rc, out, err = run(args, timeout=PAIR_TIMEOUT if args and args[0] == "pair" else CONNECT_TIMEOUT)
|
||||||
|
if rc == 0:
|
||||||
|
return {"ok": True, "error": ""}
|
||||||
|
# bluetoothctl prints failures like "Failed to connect: ..." to stdout.
|
||||||
|
detail = ""
|
||||||
|
for line in (out or "").splitlines():
|
||||||
|
if "Failed" in line or "not" in line.lower() or "error" in line.lower():
|
||||||
|
detail = line.strip()
|
||||||
|
break
|
||||||
|
return {"ok": False, "error": detail or (err or "command failed")}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = sys.argv[1:]
|
||||||
|
cmd = args[0] if args else "status"
|
||||||
|
if cmd == "status":
|
||||||
|
print(json.dumps(status()))
|
||||||
|
elif cmd == "power":
|
||||||
|
val = args[1] if len(args) > 1 else ""
|
||||||
|
print(json.dumps(action(["power", val]) if val in ("on", "off") else {"ok": False, "error": "power on|off"}))
|
||||||
|
elif cmd == "scan":
|
||||||
|
print(json.dumps(scan()))
|
||||||
|
elif cmd in ("pair", "connect", "disconnect", "trust", "untrust", "remove"):
|
||||||
|
mac = args[1] if len(args) > 1 else ""
|
||||||
|
if not mac:
|
||||||
|
print(json.dumps({"ok": False, "error": "missing device address"}))
|
||||||
|
else:
|
||||||
|
print(json.dumps(action([cmd, mac])))
|
||||||
|
else:
|
||||||
|
print(json.dumps({"ok": False, "error": "unknown command: " + cmd}))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
|
let
|
||||||
|
# Python with the ICS parsing stack baked in: icalendar + recurring
|
||||||
|
# recurrence expansion for the reminder scanner.
|
||||||
|
python = pkgs.python3.withPackages (ps: [
|
||||||
|
ps.icalendar
|
||||||
|
ps.recurring-ical-events
|
||||||
|
]);
|
||||||
|
in
|
||||||
|
|
||||||
|
{
|
||||||
|
options.services.quickshell-cal = {
|
||||||
|
enable = lib.mkEnableOption "QuickShell calendar + weather sync (Nextcloud CalDAV via vdirsyncer/khal, Open-Meteo)";
|
||||||
|
|
||||||
|
eventDays = lib.mkOption {
|
||||||
|
type = lib.types.int;
|
||||||
|
default = 31;
|
||||||
|
description = "How many days ahead of today khal should expand/return events for the shell popup.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf config.services.quickshell-cal.enable {
|
||||||
|
# The one Nextcloud fact the sync pipeline needs beyond the secret: the
|
||||||
|
# base user-principal CalDAV URL (vdirsyncer discovers every calendar
|
||||||
|
# under it). The username + password live in the SOPS secret
|
||||||
|
# "hp-laptop/nextcloud-cal-env".
|
||||||
|
home.packages = [
|
||||||
|
python
|
||||||
|
pkgs.vdirsyncer
|
||||||
|
pkgs.pipewire
|
||||||
|
pkgs.libnotify
|
||||||
|
];
|
||||||
|
|
||||||
|
# ---------- sync script ----------
|
||||||
|
# Store the python in the Nix store with tool paths baked in (user units
|
||||||
|
# have an unpredictable PATH), then expose it on the QuickShell PATH.
|
||||||
|
# Substitutions keep the source tree readable.
|
||||||
|
xdg.configFile."quickshell-cal/qs-cal-sync.py".text =
|
||||||
|
builtins.replaceStrings
|
||||||
|
[
|
||||||
|
"@vdirsyncer@"
|
||||||
|
"@libnotify@"
|
||||||
|
"@pipewire@"
|
||||||
|
]
|
||||||
|
[
|
||||||
|
"${pkgs.vdirsyncer}"
|
||||||
|
"${pkgs.libnotify}"
|
||||||
|
"${pkgs.pipewire}"
|
||||||
|
]
|
||||||
|
(builtins.readFile ./quickshell-cal.py);
|
||||||
|
|
||||||
|
home.file.".local/bin/qs-cal-sync" = {
|
||||||
|
executable = true;
|
||||||
|
text = ''
|
||||||
|
#!/bin/sh
|
||||||
|
exec ${python}/bin/python3 \
|
||||||
|
${config.home.homeDirectory}/.config/quickshell-cal/qs-cal-sync.py "$@"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# ---------- khal config ----------
|
||||||
|
# Generated at runtime by the sync script from the vdirsyncer discovery:
|
||||||
|
# one [[calendar]] section per subdir of ~/.local/share/vdirsyncer (khal
|
||||||
|
# cannot glob, and the calendar set only changes when Nextcloud does).
|
||||||
|
# Removing this file from HM keeps ~/.config/khal/config a plain user
|
||||||
|
# file the script may rewrite on every sync.
|
||||||
|
|
||||||
|
# ---------- periodic sync ----------
|
||||||
|
# Runs as the user so caches are written to ~/.cache without privilege
|
||||||
|
# games, and the SOPS env file is readable because the host config sets
|
||||||
|
# owner petere / group users / mode 0440 on it.
|
||||||
|
systemd.user.services.qs-cal-sync = {
|
||||||
|
Unit = {
|
||||||
|
Description = "Sync Nextcloud calendar + weather for the QuickShell popup";
|
||||||
|
After = [ "network-online.target" ];
|
||||||
|
};
|
||||||
|
Service = {
|
||||||
|
Type = "oneshot";
|
||||||
|
ExecStart = "${config.home.homeDirectory}/.local/bin/qs-cal-sync sync";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.user.timers.qs-cal-sync = {
|
||||||
|
Unit = {
|
||||||
|
Description = "Periodic Nextcloud calendar + weather sync";
|
||||||
|
After = [ "network-online.target" ];
|
||||||
|
};
|
||||||
|
Install = {
|
||||||
|
WantedBy = [ "timers.target" ];
|
||||||
|
};
|
||||||
|
Timer = {
|
||||||
|
OnBootSec = "1min";
|
||||||
|
OnUnitActiveSec = "20min";
|
||||||
|
Persistent = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# ---------- reminders ----------
|
||||||
|
# Fires VALARM due-tones once per minute: plays the cached chime and
|
||||||
|
# sends a desktop notification that lands in the QuickShell notification
|
||||||
|
# center. Reads only the local .ics stores (no network), so it is cheap
|
||||||
|
# enough to run every 60s.
|
||||||
|
systemd.user.services.qs-cal-remind = {
|
||||||
|
Unit = {
|
||||||
|
Description = "Fire QuickShell calendar reminders";
|
||||||
|
};
|
||||||
|
Service = {
|
||||||
|
Type = "oneshot";
|
||||||
|
ExecStart = "${config.home.homeDirectory}/.local/bin/qs-cal-sync remind";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.user.timers.qs-cal-remind = {
|
||||||
|
Unit = {
|
||||||
|
Description = "Per-minute QuickShell calendar reminder check";
|
||||||
|
};
|
||||||
|
Install = {
|
||||||
|
WantedBy = [ "timers.target" ];
|
||||||
|
};
|
||||||
|
Timer = {
|
||||||
|
OnBootSec = "1min";
|
||||||
|
OnUnitActiveSec = "30s";
|
||||||
|
# default AccuracySec is a whole minute, which (plus polling phase)
|
||||||
|
# lets alerts land up to ~2min late; 1s keeps current-skew tiny.
|
||||||
|
AccuracySec = "1s";
|
||||||
|
Persistent = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,393 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""qs-launch — wofi-based launcher modes: emoji pick, calculator, DuckDuckGo search.
|
||||||
|
|
||||||
|
Subcommands:
|
||||||
|
emoji feed a curated emoji list into wofi --dmenu, copy the picked emoji
|
||||||
|
calc type an expression in wofi, evaluate with qalc, copy + toast result
|
||||||
|
search type a query in wofi, open DuckDuckGo in the default browser
|
||||||
|
|
||||||
|
All binary paths are injected via $QS_* env vars by the .local/bin/qs-launch
|
||||||
|
wrapper so execution works regardless of PATH (same pattern as qs-bt's
|
||||||
|
$BLUECTL). When no DISPLAY/WAYLAND_DISPLAY is present, wofi can't open, so each
|
||||||
|
mode exits silently with code 1.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
# Curated emoji list: "<char> <shortcode>". Searchable by name, output splits
|
||||||
|
# on the first whitespace so only the emoji character gets copied.
|
||||||
|
EMOJI = """😀 grin
|
||||||
|
😃 smiley
|
||||||
|
😁 beam
|
||||||
|
😂 joy
|
||||||
|
🤣 rofl
|
||||||
|
😊 blush
|
||||||
|
🙂 slight smile
|
||||||
|
😉 wink
|
||||||
|
😍 heart eyes
|
||||||
|
😘 kiss
|
||||||
|
🤩 star struck
|
||||||
|
🥳 party
|
||||||
|
😎 cool
|
||||||
|
🤓 nerd
|
||||||
|
😇 halo
|
||||||
|
😏 smirk
|
||||||
|
😜 winky tongue
|
||||||
|
🥺 plead
|
||||||
|
😬 grimace
|
||||||
|
🤔 think
|
||||||
|
🤯 mind blown
|
||||||
|
😴 sleepy
|
||||||
|
😭 cry
|
||||||
|
😢 sad
|
||||||
|
😞 disappointed
|
||||||
|
😠 angry
|
||||||
|
😤 triumph
|
||||||
|
🤬 swear
|
||||||
|
🙄 roll eyes
|
||||||
|
😐 neutral
|
||||||
|
😑 expressionless
|
||||||
|
🙂🙃 upside down
|
||||||
|
😱 scream
|
||||||
|
🤒 sick
|
||||||
|
🤕 hurt
|
||||||
|
💀 skull
|
||||||
|
👻 ghost
|
||||||
|
👽 alien
|
||||||
|
🤖 robot
|
||||||
|
🎃 pumpkin
|
||||||
|
💩 poo
|
||||||
|
🙌 yes
|
||||||
|
👏 clap
|
||||||
|
🙏 pray
|
||||||
|
🤝 handshake
|
||||||
|
👍 thumbs up
|
||||||
|
👎 thumbs down
|
||||||
|
👊 fist
|
||||||
|
✊ raised fist
|
||||||
|
🤞 crossed fingers
|
||||||
|
✌️ peace
|
||||||
|
👋 hello wave
|
||||||
|
🖐️ hand
|
||||||
|
✍️ writing
|
||||||
|
💪 muscle
|
||||||
|
🫶 heart hands
|
||||||
|
🦾 bionic arm
|
||||||
|
🧠 brain
|
||||||
|
👀 eyes
|
||||||
|
👁️ eye
|
||||||
|
👄 lips
|
||||||
|
👅 taste tongue
|
||||||
|
🧛 vampire
|
||||||
|
🧟 zombie
|
||||||
|
❤️ heart
|
||||||
|
🧡 orange heart
|
||||||
|
💛 yellow heart
|
||||||
|
💚 green heart
|
||||||
|
💙 blue heart
|
||||||
|
💜 purple heart
|
||||||
|
🖤 black heart
|
||||||
|
🤍 white heart
|
||||||
|
🤎 brown heart
|
||||||
|
💔 broken heart
|
||||||
|
❤️🔥 heart fire
|
||||||
|
💯 hundred
|
||||||
|
💥 boom
|
||||||
|
💫 dizzy
|
||||||
|
✨ sparkles
|
||||||
|
🔥 fire
|
||||||
|
🌟 star
|
||||||
|
⭐ star white
|
||||||
|
☀️ sun
|
||||||
|
🌙 moon
|
||||||
|
🌑 new moon
|
||||||
|
🌈 rainbow
|
||||||
|
☁️ cloud
|
||||||
|
⛈️ storm
|
||||||
|
❄️ snow
|
||||||
|
🌊 ocean wave
|
||||||
|
🌍 earth
|
||||||
|
🌺 hibiscus
|
||||||
|
🌸 cherry blossom
|
||||||
|
🌹 rose
|
||||||
|
🌷 tulip
|
||||||
|
🌵 cactus
|
||||||
|
🌴 palm
|
||||||
|
🍀 clover
|
||||||
|
🍄 mushroom
|
||||||
|
🐶 dog
|
||||||
|
🐱 cat
|
||||||
|
🦊 fox
|
||||||
|
🐻 bear
|
||||||
|
🐼 panda
|
||||||
|
🐨 koala
|
||||||
|
🦁 lion
|
||||||
|
🐯 tiger
|
||||||
|
🐸 frog
|
||||||
|
🐵 monkey
|
||||||
|
🐧 penguin
|
||||||
|
🦄 unicorn
|
||||||
|
🐝 bee
|
||||||
|
🦋 butterfly
|
||||||
|
🐢 turtle
|
||||||
|
🐙 octopus
|
||||||
|
🦈 shark
|
||||||
|
🐬 dolphin
|
||||||
|
🍍 pineapple
|
||||||
|
🥭 mango
|
||||||
|
🍒 cherries
|
||||||
|
🍓 strawberry
|
||||||
|
🍑 peach
|
||||||
|
🥑 avocado
|
||||||
|
🍕 pizza
|
||||||
|
🍔 burger
|
||||||
|
🍟 fries
|
||||||
|
🌭 hotdog
|
||||||
|
🍿 popcorn
|
||||||
|
🌮 taco
|
||||||
|
🍰 cake
|
||||||
|
🍩 donut
|
||||||
|
🥐 croissant
|
||||||
|
☕ coffee
|
||||||
|
🍵 tea
|
||||||
|
🍺 beer
|
||||||
|
🍻 cheers
|
||||||
|
⚽ football
|
||||||
|
🏀 basketball
|
||||||
|
🎾 tennis
|
||||||
|
⚾ baseball
|
||||||
|
🏈 american football
|
||||||
|
🎮 game
|
||||||
|
🎲 dice
|
||||||
|
🎯 target
|
||||||
|
🎵 music
|
||||||
|
🎶 notes
|
||||||
|
🎤 mic
|
||||||
|
🎧 headphones
|
||||||
|
📻 radio
|
||||||
|
🎬 movie
|
||||||
|
📺 tv
|
||||||
|
💻 laptop
|
||||||
|
📱 phone
|
||||||
|
🖥️ computer
|
||||||
|
🖱️ mouse
|
||||||
|
💾 floppy
|
||||||
|
💿 disc
|
||||||
|
🔋 battery
|
||||||
|
🔌 plug
|
||||||
|
💡 bulb
|
||||||
|
🔦 flashlight
|
||||||
|
🗑️ trash
|
||||||
|
🔒 lock
|
||||||
|
🔓 unlocked
|
||||||
|
🔑 key
|
||||||
|
✂️ scissors
|
||||||
|
📎 paperclip
|
||||||
|
📌 pin
|
||||||
|
📍 location
|
||||||
|
🧲 magnet
|
||||||
|
🛠️ tools
|
||||||
|
⚙️ gear
|
||||||
|
🔧 wrench
|
||||||
|
✏️ pencil
|
||||||
|
🖊️ pen
|
||||||
|
📖 book
|
||||||
|
📚 books
|
||||||
|
📄 doc
|
||||||
|
📁 folder
|
||||||
|
📦 package
|
||||||
|
✉️ mail
|
||||||
|
📝 memo
|
||||||
|
📊 chart
|
||||||
|
💰 money
|
||||||
|
💳 card
|
||||||
|
🧾 receipt
|
||||||
|
🏆 trophy
|
||||||
|
🥇 gold
|
||||||
|
🥈 silver
|
||||||
|
🎁 gift
|
||||||
|
🎈 balloon
|
||||||
|
🎉 party popper
|
||||||
|
🥂 toast
|
||||||
|
🎊 confetti
|
||||||
|
🧹 clean
|
||||||
|
🧽 sponge
|
||||||
|
⏰ alarm
|
||||||
|
⏱️ stopwatch
|
||||||
|
🕰️ clock
|
||||||
|
📅 calendar
|
||||||
|
📆 date
|
||||||
|
🔔 bell
|
||||||
|
🔕 bell off
|
||||||
|
❌ cross
|
||||||
|
✅ check
|
||||||
|
⚠️ warning
|
||||||
|
🚫 no entry
|
||||||
|
➕ plus
|
||||||
|
➖ minus
|
||||||
|
➗ divide
|
||||||
|
✖️ multiply
|
||||||
|
🔄 refresh
|
||||||
|
⬆️ up
|
||||||
|
⬇️ down
|
||||||
|
⬅️ left
|
||||||
|
➡️ right
|
||||||
|
↩️ return
|
||||||
|
🔙 back
|
||||||
|
🔜 soon
|
||||||
|
🌐 globe
|
||||||
|
🚀 rocket
|
||||||
|
🛸 saucer
|
||||||
|
✈️ plane
|
||||||
|
🚗 car
|
||||||
|
🚕 taxi
|
||||||
|
🚌 bus
|
||||||
|
🚂 train
|
||||||
|
🚲 bike
|
||||||
|
🏍️ motorcycle
|
||||||
|
👻 spooky
|
||||||
|
☠️ skull crossbones
|
||||||
|
💣 bomb
|
||||||
|
🚁 chopper
|
||||||
|
🚢 ship
|
||||||
|
🚄 bullet train
|
||||||
|
🚦 traffic light
|
||||||
|
⛽ fuel
|
||||||
|
🅿️ parking
|
||||||
|
🚧 roadwork
|
||||||
|
🌉 bridge
|
||||||
|
🏰 castle
|
||||||
|
⛺ camp
|
||||||
|
🌋 volcano
|
||||||
|
🗼 tower
|
||||||
|
🏯 pagoda
|
||||||
|
🌆 city dusk
|
||||||
|
🌃 city night
|
||||||
|
🏙️ skyline
|
||||||
|
🖼️ frame
|
||||||
|
🎨 palette
|
||||||
|
🖌️ brush
|
||||||
|
🧶 yarn
|
||||||
|
🧵 thread
|
||||||
|
💍 ring
|
||||||
|
👟 sneaker
|
||||||
|
👠 heel
|
||||||
|
🧢 cap
|
||||||
|
🎓 graduate
|
||||||
|
👒 hat
|
||||||
|
🧣 scarf
|
||||||
|
🧤 gloves
|
||||||
|
🧥 coat
|
||||||
|
👔 tie
|
||||||
|
👕 shirt
|
||||||
|
👖 jeans
|
||||||
|
💭 thought
|
||||||
|
💬 speech
|
||||||
|
🗯️ anger
|
||||||
|
⌨️ keyboard
|
||||||
|
🖨️ printer
|
||||||
|
🧮 abacus
|
||||||
|
🕹️ joystick
|
||||||
|
🎳 bowling
|
||||||
|
🏓 ping pong
|
||||||
|
🏸 badminton
|
||||||
|
🥊 boxing
|
||||||
|
🥋 martial arts
|
||||||
|
🎽 running shirt
|
||||||
|
🚴 cyclist
|
||||||
|
🧗 climber
|
||||||
|
🏊 swimmer
|
||||||
|
🎪 circus
|
||||||
|
🎠 carousel
|
||||||
|
🗿 moai
|
||||||
|
🏛️ classical
|
||||||
|
🏟️ stadium
|
||||||
|
🏫 school
|
||||||
|
🏥 hospital
|
||||||
|
🏦 bank
|
||||||
|
🏪 store
|
||||||
|
🏭 factory
|
||||||
|
🏝️ island
|
||||||
|
🏖️ beach
|
||||||
|
⛱️ umbrella
|
||||||
|
🧭 compass"""
|
||||||
|
|
||||||
|
|
||||||
|
def run(command, **kwargs):
|
||||||
|
return subprocess.run(command, capture_output=True, text=True, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def wofi(entries, prompt, exec_search=False):
|
||||||
|
cmd = [os.environ.get("QS_WOFI", "wofi"), "--dmenu", "--prompt", prompt]
|
||||||
|
if exec_search:
|
||||||
|
cmd.append("--exec-search")
|
||||||
|
result = run(cmd, input=entries)
|
||||||
|
if result.returncode != 0:
|
||||||
|
return None
|
||||||
|
return result.stdout.rstrip("\n")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_mode():
|
||||||
|
if "WAYLAND_DISPLAY" not in os.environ and "DISPLAY" not in os.environ:
|
||||||
|
return 1
|
||||||
|
sub = sys.argv[1] if len(sys.argv) > 1 else ""
|
||||||
|
if sub == "emoji":
|
||||||
|
return emoji_mode()
|
||||||
|
if sub == "calc":
|
||||||
|
return calc_mode()
|
||||||
|
if sub == "search":
|
||||||
|
return search_mode()
|
||||||
|
print(__doc__, file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
|
||||||
|
def emoji_mode():
|
||||||
|
picked = wofi(EMOJI, "\U0001f600 Emoji")
|
||||||
|
if not picked:
|
||||||
|
return 0
|
||||||
|
char = picked.split(" ", 1)[0]
|
||||||
|
if not char:
|
||||||
|
return 0
|
||||||
|
wl_copy = os.environ.get("QS_WLCOPY", "wl-copy")
|
||||||
|
run([wl_copy, char])
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def calc_mode():
|
||||||
|
expr = wofi("", "=? \u2014 1+2 then Enter", exec_search=True)
|
||||||
|
if not expr:
|
||||||
|
return 0
|
||||||
|
qalc = os.environ.get("QS_QALC", "qalc")
|
||||||
|
result = run([qalc, "-t", expr])
|
||||||
|
if result.returncode != 0:
|
||||||
|
return 0
|
||||||
|
answer = result.stdout.strip()
|
||||||
|
wl_copy = os.environ.get("QS_WLCOPY", "wl-copy")
|
||||||
|
run([wl_copy, answer])
|
||||||
|
notify(os.environ.get("QS_NOTIFY", "notify-send"),
|
||||||
|
"\U0001f4d0 " + expr, "= " + answer + " (copied)")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def search_mode():
|
||||||
|
query = wofi("", "Search the web", exec_search=True)
|
||||||
|
if not query:
|
||||||
|
return 0
|
||||||
|
url = "https://duckduckgo.com/?q=" + urllib.parse.quote_plus(query)
|
||||||
|
xdg_open = os.environ.get("QS_XDGO", "xdg-open")
|
||||||
|
subprocess.Popen([xdg_open, url])
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def notify(sender, title, body):
|
||||||
|
try:
|
||||||
|
run([sender, "-a", "qs-launch", title, body])
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(cmd_mode())
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,172 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""qs-shot — screenshot capture with a QuickShell toast preview.
|
||||||
|
|
||||||
|
Subcommands:
|
||||||
|
pick show a wofi chooser (select area / full screen) then capture
|
||||||
|
area interactively select a region (grimblast)
|
||||||
|
screen capture the current output (grimblast)
|
||||||
|
open <path> open a saved screenshot in the default viewer (toast action)
|
||||||
|
copy <path> re-push a saved screenshot to the clipboard (toast action)
|
||||||
|
|
||||||
|
The capture is copied to the clipboard AND saved to a temp file. A
|
||||||
|
notification toast is posted with the image preview and Open / Copy
|
||||||
|
actions; the shell intercepts those in calNotifAction (identifier
|
||||||
|
payload carries the saved file path).
|
||||||
|
|
||||||
|
All binary paths are injected via $QS_* env vars by the .local/bin/qs-shot
|
||||||
|
wrapper so execution works regardless of PATH. When no DISPLAY/WAYLAND_DISPLAY
|
||||||
|
is present, wofi can't open, so the picker exits silently with code 1.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
TARGETS = {
|
||||||
|
"area": "Select area",
|
||||||
|
"screen": "Full screen",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def run(command, **kwargs):
|
||||||
|
return subprocess.run(command, capture_output=True, text=True, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def wofi(entries, prompt):
|
||||||
|
cmd = [os.environ.get("QS_WOFI", "wofi"), "--dmenu", "--prompt", prompt]
|
||||||
|
result = run(cmd, input=entries)
|
||||||
|
if result.returncode != 0:
|
||||||
|
return None
|
||||||
|
return result.stdout.rstrip("\n")
|
||||||
|
|
||||||
|
|
||||||
|
def _output_path():
|
||||||
|
return os.path.join(
|
||||||
|
os.environ.get("XDG_CACHE_HOME", os.path.expanduser("~/.cache")),
|
||||||
|
"qs-shot",
|
||||||
|
"shot-%s.png" % time.strftime("%Y%m%d-%H%M%S"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _grab(target, out):
|
||||||
|
"""grimblast save <target> <out> — file only, no clipboard."""
|
||||||
|
grimblast = os.environ.get("QS_GRIMBLAST", "grimblast")
|
||||||
|
return subprocess.run(
|
||||||
|
[grimblast, "save", target, out],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
).returncode == 0
|
||||||
|
|
||||||
|
|
||||||
|
def _copy_to_clip(out):
|
||||||
|
"""wl-copy < out — fire-and-forget, mirrors the `copy` subcommand."""
|
||||||
|
wl_copy = os.environ.get("QS_WLCOPY", "wl-copy")
|
||||||
|
subprocess.Popen(["sh", "-c",
|
||||||
|
'exec "$1" --type image/png < "$2"', "--",
|
||||||
|
wl_copy, out],
|
||||||
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
|
||||||
|
|
||||||
|
def _notify(out, target):
|
||||||
|
summary = "Screenshot" if target == "screen" else "Screenshot (area)"
|
||||||
|
body = "\u2702 + \U0001f4be " + out
|
||||||
|
notify = os.environ.get("QS_NOTIFY", "notify-send")
|
||||||
|
# fire-and-forget: notify-send with --action stays alive as the D-Bus action
|
||||||
|
# sender waiting for ActionInvoked; quickshell handles the shot-* actions by
|
||||||
|
# identifier in QML instead, so the sender can exit immediately.
|
||||||
|
subprocess.Popen([
|
||||||
|
notify,
|
||||||
|
"-a", "Screenshot",
|
||||||
|
"-t", "8000",
|
||||||
|
"--hint=string:image-path:" + out,
|
||||||
|
"--action=shot-open:%s=OPEN" % out,
|
||||||
|
"--action=shot-copy:%s=COPY" % out,
|
||||||
|
summary,
|
||||||
|
body,
|
||||||
|
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
|
||||||
|
|
||||||
|
def pick_mode():
|
||||||
|
if "WAYLAND_DISPLAY" not in os.environ and "DISPLAY" not in os.environ:
|
||||||
|
return 1
|
||||||
|
out = _output_path()
|
||||||
|
os.makedirs(os.path.dirname(out), exist_ok=True)
|
||||||
|
_prune_old_shots(os.path.dirname(out), keep=40)
|
||||||
|
# Capture BEFORE the chooser opens. wofi's layer surface lingers as a
|
||||||
|
# composited zombie after it exits (Hyprland keeps it in hyprctl layers even
|
||||||
|
# with a dead pid), so any post-exit wait races teardown and can bake the
|
||||||
|
# menu into the shot. A pre-capture makes that impossible for full screen.
|
||||||
|
if not _grab("screen", out):
|
||||||
|
return 1
|
||||||
|
label = wofi("\n".join(TARGETS.values()), "\U0001f4f7 Screenshot")
|
||||||
|
if not label:
|
||||||
|
try:
|
||||||
|
os.unlink(out)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return 0
|
||||||
|
target = None
|
||||||
|
for t, entry in TARGETS.items():
|
||||||
|
if label == entry:
|
||||||
|
target = t
|
||||||
|
break
|
||||||
|
if target is None:
|
||||||
|
try:
|
||||||
|
os.unlink(out)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return 0
|
||||||
|
if target == "area":
|
||||||
|
if not _grab("area", out):
|
||||||
|
return 1
|
||||||
|
_copy_to_clip(out)
|
||||||
|
_notify(out, target)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _prune_old_shots(directory, keep=40):
|
||||||
|
try:
|
||||||
|
files = sorted(
|
||||||
|
os.path.join(directory, f) for f in os.listdir(directory)
|
||||||
|
if f.startswith("shot-") and f.endswith(".png")
|
||||||
|
)
|
||||||
|
for old in files[:-keep]:
|
||||||
|
os.unlink(old)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def capture(target):
|
||||||
|
out = _output_path()
|
||||||
|
os.makedirs(os.path.dirname(out), exist_ok=True)
|
||||||
|
_prune_old_shots(os.path.dirname(out), keep=40)
|
||||||
|
if not _grab(target, out):
|
||||||
|
return 1
|
||||||
|
_copy_to_clip(out)
|
||||||
|
_notify(out, target)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
sub = sys.argv[1] if len(sys.argv) > 1 else "pick"
|
||||||
|
if sub == "area" or sub == "screen":
|
||||||
|
return capture(sub)
|
||||||
|
if sub == "pick":
|
||||||
|
return pick_mode()
|
||||||
|
if sub == "open" and len(sys.argv) > 2:
|
||||||
|
xdg_open = os.environ.get("QS_XDGO", "xdg-open")
|
||||||
|
subprocess.Popen([xdg_open, sys.argv[2]])
|
||||||
|
return 0
|
||||||
|
if sub == "copy" and len(sys.argv) > 2:
|
||||||
|
wl_copy = os.environ.get("QS_WLCOPY", "wl-copy")
|
||||||
|
subprocess.Popen(["sh", "-c",
|
||||||
|
'exec "$1" --type image/png < "$2"', "--",
|
||||||
|
wl_copy, sys.argv[2]])
|
||||||
|
return 0
|
||||||
|
print(__doc__, file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
// SciSlider.qml — themed slider for the quick-settings panel.
|
||||||
|
// Imperative two-way use: set `.value` from the outside, read `.moved(v)`.
|
||||||
|
// (Avoids QML binding loops with live PipeWire/brightness state.)
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: slider
|
||||||
|
|
||||||
|
implicitHeight: 24
|
||||||
|
|
||||||
|
property real value: 0.5 // 0..1
|
||||||
|
property color accent: "#00e5ff"
|
||||||
|
property color trackColor: "#24283b"
|
||||||
|
readonly property bool pressed: mouseArea.pressed
|
||||||
|
|
||||||
|
signal moved(real v)
|
||||||
|
|
||||||
|
function setFromMouse(mx) {
|
||||||
|
if (width <= 0)
|
||||||
|
return;
|
||||||
|
let v = mx / width;
|
||||||
|
if (v < 0)
|
||||||
|
v = 0;
|
||||||
|
if (v > 1)
|
||||||
|
v = 1;
|
||||||
|
if (v !== slider.value) {
|
||||||
|
slider.value = v;
|
||||||
|
slider.moved(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// track
|
||||||
|
Rectangle {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width
|
||||||
|
height: 6
|
||||||
|
radius: 3
|
||||||
|
color: slider.trackColor
|
||||||
|
border.color: "#394b70"
|
||||||
|
border.width: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// filled portion
|
||||||
|
Rectangle {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.left: parent.left
|
||||||
|
width: slider.value * parent.width
|
||||||
|
height: 6
|
||||||
|
radius: 3
|
||||||
|
color: slider.accent
|
||||||
|
}
|
||||||
|
|
||||||
|
// knob
|
||||||
|
Rectangle {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
x: slider.value * (parent.width - width)
|
||||||
|
width: 14
|
||||||
|
height: 14
|
||||||
|
radius: 7
|
||||||
|
color: slider.pressed ? "#ffffff" : slider.accent
|
||||||
|
border.color: "#0b0e14"
|
||||||
|
border.width: 1
|
||||||
|
Behavior on color { ColorAnimation { duration: 120 } }
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
anchors {
|
||||||
|
fill: parent
|
||||||
|
topMargin: -6
|
||||||
|
bottomMargin: -6
|
||||||
|
}
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
hoverEnabled: true
|
||||||
|
onPressed: mouse => slider.setFromMouse(mouse.x)
|
||||||
|
onPositionChanged: mouse => {
|
||||||
|
if (pressed)
|
||||||
|
slider.setFromMouse(mouse.x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""qs-stats: one-shot system stats probe for the QuickShell stats popup.
|
||||||
|
|
||||||
|
Emits a single JSON object on stdout:
|
||||||
|
{
|
||||||
|
"ts": 1730000000,
|
||||||
|
"cpu": 12.3,
|
||||||
|
"ram": {"used": 8.2, "total": 31.9, "pct": 25.7},
|
||||||
|
"swap": {"used": 0.0, "total": 8.0, "pct": 0.0},
|
||||||
|
"disk": {"used": 156.4, "total": 934.1, "pct": 16.7},
|
||||||
|
"load": [2.01, 2.29, 2.10],
|
||||||
|
"uptime": 7861.0,
|
||||||
|
"temps": [{"name": "TCPU", "temp": 58.0}, ...],
|
||||||
|
"freq": 3.8
|
||||||
|
}
|
||||||
|
|
||||||
|
CPU% is computed from two /proc/stat samples ~250ms apart (mostly an idle
|
||||||
|
sleep: ~free on battery). Memory/swap/disk come from /proc/meminfo +
|
||||||
|
os.statvfs. Temperatures come from /sys/class/thermal (kernel hwmon zones).
|
||||||
|
The bar runs this on a slow 15s timer; the stats popup refreshes every 3s
|
||||||
|
while open.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
CPU_SAMPLE_MS = 250
|
||||||
|
|
||||||
|
|
||||||
|
def cpu_usage():
|
||||||
|
def sample():
|
||||||
|
with open("/proc/stat") as f:
|
||||||
|
parts = f.readline().split()
|
||||||
|
# cpu user nice system idle iowait irq softirq steal guest guest_nice
|
||||||
|
vals = list(map(int, parts[1:]))
|
||||||
|
idle = vals[3] + vals[4]
|
||||||
|
total = sum(vals)
|
||||||
|
return idle, total
|
||||||
|
|
||||||
|
idle0, total0 = sample()
|
||||||
|
time.sleep(CPU_SAMPLE_MS / 1000.0)
|
||||||
|
idle1, total1 = sample()
|
||||||
|
d_idle = idle1 - idle0
|
||||||
|
d_total = total1 - total0
|
||||||
|
if d_total <= 0:
|
||||||
|
return 0.0
|
||||||
|
return round(100.0 * (1.0 - d_idle / d_total), 1)
|
||||||
|
|
||||||
|
|
||||||
|
def meminfo_gi():
|
||||||
|
mem = {}
|
||||||
|
try:
|
||||||
|
with open("/proc/meminfo") as f:
|
||||||
|
for line in f:
|
||||||
|
key, _, rest = line.partition(":")
|
||||||
|
val = rest.strip().split()[0]
|
||||||
|
mem[key] = int(val) # kB
|
||||||
|
except OSError:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def gi(kb):
|
||||||
|
return round(kb / 1024.0 / 1024.0, 1)
|
||||||
|
|
||||||
|
ram = {
|
||||||
|
"total": gi(mem.get("MemTotal", 0)),
|
||||||
|
# MemAvailable is the honest "swappable minus thrash" number
|
||||||
|
"available": gi(mem.get("MemAvailable", mem.get("MemFree", 0))),
|
||||||
|
}
|
||||||
|
ram["used"] = round(ram["total"] - ram["available"], 1)
|
||||||
|
ram["pct"] = (
|
||||||
|
round(100.0 * ram["used"] / ram["total"], 1) if ram["total"] > 0 else 0.0
|
||||||
|
)
|
||||||
|
|
||||||
|
swap = {
|
||||||
|
"total": gi(mem.get("SwapTotal", 0)),
|
||||||
|
"free": gi(mem.get("SwapFree", 0)),
|
||||||
|
}
|
||||||
|
swap["used"] = round(swap["total"] - swap["free"], 1)
|
||||||
|
swap["pct"] = (
|
||||||
|
round(100.0 * swap["used"] / swap["total"], 1) if swap["total"] > 0 else 0.0
|
||||||
|
)
|
||||||
|
return ram, swap
|
||||||
|
|
||||||
|
|
||||||
|
def disk_pct(path="/"):
|
||||||
|
try:
|
||||||
|
st = os.statvfs(path)
|
||||||
|
except OSError:
|
||||||
|
return {"used": 0.0, "total": 0.0, "pct": 0.0}
|
||||||
|
total = st.f_blocks * st.f_frsize
|
||||||
|
free = st.f_bavail * st.f_frsize
|
||||||
|
used = total - free
|
||||||
|
return {
|
||||||
|
"used": round(used / 1024.0**3, 1),
|
||||||
|
"total": round(total / 1024.0**3, 1),
|
||||||
|
"pct": round(100.0 * used / total, 1) if total > 0 else 0.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def load_avg():
|
||||||
|
try:
|
||||||
|
return [round(float(x), 2) for x in os.getloadavg()]
|
||||||
|
except OSError:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def uptime():
|
||||||
|
try:
|
||||||
|
with open("/proc/uptime") as f:
|
||||||
|
return round(float(f.read().split()[0]), 1)
|
||||||
|
except OSError:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def temps():
|
||||||
|
out = []
|
||||||
|
base = "/sys/class/thermal"
|
||||||
|
try:
|
||||||
|
zones = sorted(os.listdir(base))
|
||||||
|
except OSError:
|
||||||
|
return out
|
||||||
|
for z in zones:
|
||||||
|
if not z.startswith("thermal_zone"):
|
||||||
|
continue
|
||||||
|
tpath = os.path.join(base, z)
|
||||||
|
typefile = os.path.join(tpath, "type")
|
||||||
|
tempfile = os.path.join(tpath, "temp")
|
||||||
|
try:
|
||||||
|
with open(typefile) as f:
|
||||||
|
name = f.read().strip()
|
||||||
|
with open(tempfile) as f:
|
||||||
|
millideg = int(f.read().strip())
|
||||||
|
except (OSError, ValueError):
|
||||||
|
continue
|
||||||
|
# Skip the ACPI "INT3400 Thermal" umbrella zone (always ~20C, noise)
|
||||||
|
if "INT3400" in name:
|
||||||
|
continue
|
||||||
|
out.append({"name": name, "temp": round(millideg / 1000.0, 0)})
|
||||||
|
# Keep a sane display order: CPU forward, wifi last
|
||||||
|
def rank(n):
|
||||||
|
n = n.lower()
|
||||||
|
if "cpu" in n or "tctl" in n or "pkg" in n:
|
||||||
|
return 0
|
||||||
|
if "sen" in n:
|
||||||
|
return 1
|
||||||
|
return 2
|
||||||
|
|
||||||
|
out.sort(key=lambda t: (rank(t["name"]), t["name"]))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def scaled_vout():
|
||||||
|
# Current CPU P-state / GHz (x86-package-temp zone present ⇒ has cpufreq).
|
||||||
|
try:
|
||||||
|
with open("/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq") as f:
|
||||||
|
khz = int(f.read().strip())
|
||||||
|
return round(khz / 1_000_000.0, 2)
|
||||||
|
except OSError:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ram, swap = meminfo_gi()
|
||||||
|
payload = {
|
||||||
|
"ts": int(time.time()),
|
||||||
|
"cpu": cpu_usage(),
|
||||||
|
"ram": ram,
|
||||||
|
"swap": swap,
|
||||||
|
"disk": disk_pct(),
|
||||||
|
"load": load_avg(),
|
||||||
|
"uptime": uptime(),
|
||||||
|
"temps": temps(),
|
||||||
|
"freq": scaled_vout(),
|
||||||
|
}
|
||||||
|
sys.stdout.write(json.dumps(payload))
|
||||||
|
sys.stdout.write("\n")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""QuickShell wallpaper picker backend.
|
||||||
|
|
||||||
|
Wallpapers are the image files in ~/Pictures/wallpapers (hyprpaper reads that
|
||||||
|
dir as the initial set; the picker surfaces whatever is there at runtime).
|
||||||
|
|
||||||
|
Commands
|
||||||
|
--------
|
||||||
|
list -> JSON array of {name, path, active} (active = matches the state file)
|
||||||
|
set <p> -> persist <p> to ~/.cache/quickshell/wallpaper, then apply it now
|
||||||
|
apply -> re-apply the persisted choice (login autostart; retries while
|
||||||
|
hyprpaper's control socket comes up)
|
||||||
|
path -> print the persisted path (empty if never picked)
|
||||||
|
|
||||||
|
Apply mechanism
|
||||||
|
---------------
|
||||||
|
hyprpaper >= 0.8 ships a new control protocol: the legacy `preload`/`reload`
|
||||||
|
IPC commands are gone, and `hyprctl hyprpaper wallpaper <monitor>,<path>`
|
||||||
|
takes a real output name (no `*`). We query `hyprctl monitors -j` for the
|
||||||
|
current output names and apply to each. Persisting to a cache file (not
|
||||||
|
hyprpaper.conf) keeps the Nix-generated config authoritative; the login
|
||||||
|
autostart line in hyprland.nix re-applies the choice after hyprpaper starts,
|
||||||
|
defaulting to the config wallpaper until the user picks something.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
HOME = os.path.expanduser("~")
|
||||||
|
WALL_DIR = os.path.join(HOME, "Pictures", "wallpapers")
|
||||||
|
CACHE_DIR = os.path.join(HOME, ".cache", "quickshell")
|
||||||
|
STATE_FILE = os.path.join(CACHE_DIR, "wallpaper")
|
||||||
|
|
||||||
|
IMAGE_EXT = (".png", ".jpg", ".jpeg", ".webp", ".bmp", ".gif")
|
||||||
|
HYPRCTL = "hyprctl"
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_cache_dir():
|
||||||
|
os.makedirs(CACHE_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def scan_names():
|
||||||
|
if not os.path.isdir(WALL_DIR):
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
names = os.listdir(WALL_DIR)
|
||||||
|
except OSError:
|
||||||
|
return []
|
||||||
|
return sorted(n for n in names if n.lower().endswith(IMAGE_EXT))
|
||||||
|
|
||||||
|
|
||||||
|
def read_state():
|
||||||
|
try:
|
||||||
|
with open(STATE_FILE) as fh:
|
||||||
|
path = fh.read().strip()
|
||||||
|
return path if os.path.isfile(path) else ""
|
||||||
|
except OSError:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def list_walls():
|
||||||
|
current = read_state()
|
||||||
|
entries = []
|
||||||
|
for name in scan_names():
|
||||||
|
path = os.path.join(WALL_DIR, name)
|
||||||
|
entries.append({"name": name, "path": path, "active": path == current})
|
||||||
|
return entries
|
||||||
|
|
||||||
|
|
||||||
|
def monitors():
|
||||||
|
"""Current Hyprland output names (e.g. eDP-1), empty if unavailable."""
|
||||||
|
try:
|
||||||
|
out = subprocess.run(
|
||||||
|
[HYPRCTL, "monitors", "-j"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
except OSError:
|
||||||
|
return []
|
||||||
|
if out.returncode != 0:
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
data = json.loads(out.stdout)
|
||||||
|
return [m["name"] for m in data if "name" in m]
|
||||||
|
except (ValueError, KeyError, TypeError):
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def apply_path(path, retries=10, delay=0.5):
|
||||||
|
"""Apply via hyprctl hyprpaper, absorbing the login socket race."""
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
return False, "no such file: %s" % path
|
||||||
|
last = ""
|
||||||
|
for _ in range(retries):
|
||||||
|
mons = monitors()
|
||||||
|
if not mons:
|
||||||
|
last = "no monitors available"
|
||||||
|
time.sleep(delay)
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
errs = []
|
||||||
|
ok = True
|
||||||
|
for mon in mons:
|
||||||
|
setw = subprocess.run(
|
||||||
|
[HYPRCTL, "hyprpaper", "wallpaper", mon + "," + path],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
if setw.returncode != 0:
|
||||||
|
ok = False
|
||||||
|
errs.append(setw.stderr.strip())
|
||||||
|
except OSError as exc:
|
||||||
|
return False, "hyprctl unavailable: %s" % exc
|
||||||
|
if ok:
|
||||||
|
return True, ""
|
||||||
|
last = "; ".join(errs) or "hyprctl hyprpaper wallpaper failed"
|
||||||
|
time.sleep(delay)
|
||||||
|
return False, last
|
||||||
|
|
||||||
|
|
||||||
|
def set_wall(path):
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
return {"ok": False, "error": "no such file: %s" % path}
|
||||||
|
ensure_cache_dir()
|
||||||
|
with open(STATE_FILE, "w") as fh:
|
||||||
|
fh.write(path + "\n")
|
||||||
|
ok, err = apply_path(path)
|
||||||
|
return {"ok": ok, "stderr": err}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = sys.argv[1:]
|
||||||
|
cmd = args[0] if args else "list"
|
||||||
|
if cmd == "list":
|
||||||
|
print(json.dumps(list_walls()))
|
||||||
|
elif cmd == "set":
|
||||||
|
print(json.dumps(set_wall(args[1] if len(args) > 1 else "")))
|
||||||
|
elif cmd == "apply":
|
||||||
|
current = read_state()
|
||||||
|
print(json.dumps(set_wall(current) if current else {"ok": True, "stderr": ""}))
|
||||||
|
elif cmd == "path":
|
||||||
|
print(read_state())
|
||||||
|
else:
|
||||||
|
print(json.dumps({"ok": False, "error": "unknown command: " + cmd}))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{ lib, pkgs, ... }:
|
||||||
|
let
|
||||||
|
justfileContent = builtins.readFile ./justfile;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
home.packages = [ pkgs.just ];
|
||||||
|
|
||||||
|
home.file.".config/just/justfile".text = justfileContent;
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
nix_flake := "/home/petere/Nix-Vibe"
|
||||||
|
hostname := `hostname`
|
||||||
|
|
||||||
|
# list available recipes
|
||||||
|
default:
|
||||||
|
@just --list --justfile {{justfile()}} 2>/dev/null || echo "Run: just -g --list"
|
||||||
|
|
||||||
|
# rebuild current system
|
||||||
|
[group('NixOS')]
|
||||||
|
update:
|
||||||
|
cd {{nix_flake}} && sudo nixos-rebuild switch --flake .#{{hostname}}
|
||||||
|
|
||||||
|
# remote deploy: just deploy x470
|
||||||
|
[group('NixOS')]
|
||||||
|
deploy target:
|
||||||
|
cd {{nix_flake}} && nixos-rebuild switch --target-host petere@{{target}} --flake .#{{target}} --sudo
|
||||||
|
|
||||||
|
# update flake.lock
|
||||||
|
[group('NixOS')]
|
||||||
|
flake-lock:
|
||||||
|
cd {{nix_flake}} && nix flake update
|
||||||
|
|
||||||
|
# update flake + rebuild
|
||||||
|
[group('NixOS')]
|
||||||
|
upgrade:
|
||||||
|
cd {{nix_flake}} && nix flake update && sudo nixos-rebuild switch --flake .#{{hostname}}
|
||||||
|
|
||||||
|
# build without switching (test config)
|
||||||
|
[group('NixOS')]
|
||||||
|
build:
|
||||||
|
cd {{nix_flake}} && nixos-rebuild build --flake .#{{hostname}}
|
||||||
|
|
||||||
|
# dry activation (check if config is valid)
|
||||||
|
[group('NixOS')]
|
||||||
|
check:
|
||||||
|
cd {{nix_flake}} && nixos-rebuild dry-activate --flake .#{{hostname}}
|
||||||
|
|
||||||
|
# switch home-manager config only
|
||||||
|
[group('NixOS')]
|
||||||
|
hm-switch:
|
||||||
|
cd {{nix_flake}} && home-manager switch --flake .#{{hostname}}
|
||||||
|
|
||||||
|
# garbage collect nix store
|
||||||
|
[group('System')]
|
||||||
|
gc:
|
||||||
|
nix-collect-garbage -d
|
||||||
|
sudo nix-collect-garbage -d
|
||||||
|
|
||||||
|
# remove old generations
|
||||||
|
[group('System')]
|
||||||
|
clean:
|
||||||
|
sudo nix-env --delete-generations old -p /nix/var/nix/profiles/system
|
||||||
|
sudo nix-collect-garbage -d
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
{ pkgs, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
# Configure Zsh
|
||||||
|
programs.zsh = {
|
||||||
|
enable = true;
|
||||||
|
enableCompletion = true;
|
||||||
|
autosuggestion.enable = true;
|
||||||
|
syntaxHighlighting.enable = true;
|
||||||
|
autocd = true;
|
||||||
|
|
||||||
|
oh-my-zsh = {
|
||||||
|
enable = true;
|
||||||
|
plugins = [
|
||||||
|
"git"
|
||||||
|
"eza"
|
||||||
|
"vscode"
|
||||||
|
"sudo"
|
||||||
|
];
|
||||||
|
theme = "lukerandall";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Use initContent to add custom Zsh configuration after Home Manager's setup
|
||||||
|
initContent = ''
|
||||||
|
# Load Gemini API Key from SOPS
|
||||||
|
if [ -f "/run/secrets/gemini-api-key" ]; then
|
||||||
|
export GOOGLE_GENERATIVE_AI_API_KEY=$(cat "/run/secrets/gemini-api-key")
|
||||||
|
export GEMINI_API_KEY="$GOOGLE_GENERATIVE_AI_API_KEY"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Load OpenCode API Key from SOPS
|
||||||
|
if [ -f "/run/secrets/opencode-api-key" ]; then
|
||||||
|
export OPENCODE_API_KEY=$(cat "/run/secrets/opencode-api-key")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fastfetch check
|
||||||
|
if command -v fastfetch >/dev/null 2>&1; then
|
||||||
|
fastfetch --config ${./fastfetch.json}
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
programs.zoxide.enable = true;
|
||||||
|
programs.eza = {
|
||||||
|
enable = true;
|
||||||
|
extraOptions = [
|
||||||
|
"-l"
|
||||||
|
"--icons"
|
||||||
|
"--git"
|
||||||
|
"-a"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
{ config
|
||||||
|
, pkgs
|
||||||
|
, lib
|
||||||
|
, inputs
|
||||||
|
, ...
|
||||||
|
}:
|
||||||
|
|
||||||
|
{
|
||||||
|
# Home Manager configuration for caitlin
|
||||||
|
home-manager.users.caitlin = {
|
||||||
|
home.username = "caitlin";
|
||||||
|
home.homeDirectory = "/home/caitlin";
|
||||||
|
home.stateVersion = "23.11"; # Set to your NixOS release version
|
||||||
|
|
||||||
|
home.file.".config/containers/containers.conf".text = ''
|
||||||
|
[service_destinations]
|
||||||
|
[service_destinations.local]
|
||||||
|
uri = "unix:///run/user/1000/podman/podman.sock"
|
||||||
|
|
||||||
|
[engine]
|
||||||
|
active_service = "local"
|
||||||
|
'';
|
||||||
|
|
||||||
|
programs.home-manager.enable = true;
|
||||||
|
programs.git = {
|
||||||
|
enable = true;
|
||||||
|
settings.user = {
|
||||||
|
name = "Caitlin";
|
||||||
|
email = "caitlin@example.com"; # Placeholder email
|
||||||
|
};
|
||||||
|
};
|
||||||
|
programs.direnv = {
|
||||||
|
enable = true;
|
||||||
|
nix-direnv.enable = true;
|
||||||
|
};
|
||||||
|
systemd.user.startServices = "sd-switch";
|
||||||
|
|
||||||
|
home.packages = with pkgs; [
|
||||||
|
fastfetch
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{ config
|
||||||
|
, pkgs
|
||||||
|
, lib
|
||||||
|
, ...
|
||||||
|
}:
|
||||||
|
|
||||||
|
{
|
||||||
|
# User definition moved to host NixOS config (x470)
|
||||||
|
|
||||||
|
# Home Manager configuration for guest
|
||||||
|
home-manager.users.guest = {
|
||||||
|
home.username = "guest";
|
||||||
|
home.homeDirectory = "/home/guest";
|
||||||
|
home.stateVersion = "23.11"; # Set to your NixOS release version
|
||||||
|
|
||||||
|
programs.home-manager.enable = true;
|
||||||
|
|
||||||
|
home.packages = with pkgs; [
|
||||||
|
fastfetch
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
{ config
|
||||||
|
, pkgs
|
||||||
|
, lib
|
||||||
|
, inputs
|
||||||
|
, ...
|
||||||
|
}:
|
||||||
|
|
||||||
|
{
|
||||||
|
# Home Manager configuration for mary
|
||||||
|
home-manager.users.mary = {
|
||||||
|
home.username = "mary";
|
||||||
|
home.homeDirectory = "/home/mary";
|
||||||
|
home.stateVersion = "24.11";
|
||||||
|
|
||||||
|
home.file.".config/containers/containers.conf".text = ''
|
||||||
|
[service_destinations]
|
||||||
|
[service_destinations.local]
|
||||||
|
uri = "unix:///run/user/1000/podman/podman.sock"
|
||||||
|
|
||||||
|
[engine]
|
||||||
|
active_service = "local"
|
||||||
|
'';
|
||||||
|
|
||||||
|
programs.home-manager.enable = true;
|
||||||
|
programs.git = {
|
||||||
|
enable = true;
|
||||||
|
settings.user = {
|
||||||
|
name = "Mary";
|
||||||
|
email = "mary@example.com";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
programs.direnv = {
|
||||||
|
enable = true;
|
||||||
|
nix-direnv.enable = true;
|
||||||
|
};
|
||||||
|
systemd.user.startServices = "sd-switch";
|
||||||
|
|
||||||
|
home.packages = with pkgs; [
|
||||||
|
fastfetch
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
{ config
|
||||||
|
, pkgs
|
||||||
|
, lib
|
||||||
|
, inputs
|
||||||
|
, ...
|
||||||
|
}:
|
||||||
|
|
||||||
|
{
|
||||||
|
# User password is set in NixOS host configuration
|
||||||
|
|
||||||
|
users.users.petere.extraGroups = [
|
||||||
|
"optical"
|
||||||
|
"cdrom"
|
||||||
|
];
|
||||||
|
|
||||||
|
# Home Manager configuration for petere
|
||||||
|
home-manager.users.petere = {
|
||||||
|
home.username = "petere";
|
||||||
|
home.homeDirectory = "/home/petere";
|
||||||
|
home.stateVersion = "23.11"; # Set to your NixOS release version
|
||||||
|
|
||||||
|
home.file.".config/containers/containers.conf".text = ''
|
||||||
|
[service_destinations]
|
||||||
|
[service_destinations.local]
|
||||||
|
uri = "unix:///run/user/1000/podman/podman.sock"
|
||||||
|
|
||||||
|
[engine]
|
||||||
|
active_service = "local"
|
||||||
|
'';
|
||||||
|
|
||||||
|
programs.home-manager.enable = true;
|
||||||
|
programs.git = {
|
||||||
|
enable = true;
|
||||||
|
settings.user = {
|
||||||
|
name = "Peter Edley";
|
||||||
|
email = "peter@edleyit.com";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
programs.direnv = {
|
||||||
|
enable = true;
|
||||||
|
nix-direnv.enable = true;
|
||||||
|
};
|
||||||
|
systemd.user.startServices = "sd-switch";
|
||||||
|
|
||||||
|
home.packages = with pkgs; [
|
||||||
|
fastfetch
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# /hosts/nixos/configuration.nix
|
||||||
|
{ config
|
||||||
|
, pkgs
|
||||||
|
, inputs
|
||||||
|
, lib
|
||||||
|
, ...
|
||||||
|
}:
|
||||||
|
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
./hardware-configuration.nix
|
||||||
|
../../modules/core/common.nix
|
||||||
|
../../modules/desktop/gui.nix
|
||||||
|
(import ../../modules/storage/disko.nix {
|
||||||
|
inherit inputs lib config;
|
||||||
|
diskoConfigPath = ./disko-config.nix;
|
||||||
|
})
|
||||||
|
../../modules/desktop/gnome.nix
|
||||||
|
../../modules/desktop/apps/soundux.nix
|
||||||
|
../../modules/desktop/apps/freeshow.nix
|
||||||
|
../../modules/core/management.nix
|
||||||
|
../../modules/hardware/laptop.nix
|
||||||
|
../../modules/core/dev.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
sops.secrets = {
|
||||||
|
"users/petere-password" = {
|
||||||
|
neededForUsers = true;
|
||||||
|
};
|
||||||
|
"users/caitlin-password" = {
|
||||||
|
neededForUsers = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
services.displayManager.gdm.settings = {
|
||||||
|
"greeter" = {
|
||||||
|
"Exclude" = "petere";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
home-manager.users.caitlin.imports = [
|
||||||
|
../../home-manager/modules/desktop-user.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
networking.hostName = "caitlin-x1";
|
||||||
|
networking.modemmanager.enable = true;
|
||||||
|
|
||||||
|
# Laptop-specific hardware (fingerprint reader, fwupd)
|
||||||
|
my.hardware.laptop.enable = true;
|
||||||
|
|
||||||
|
hardware.sensor.iio.enable = true;
|
||||||
|
|
||||||
|
users.users.caitlin = {
|
||||||
|
isNormalUser = true;
|
||||||
|
extraGroups = [ "wheel" ];
|
||||||
|
hashedPasswordFile = config.sops.secrets."users/caitlin-password".path;
|
||||||
|
subUidRanges = [
|
||||||
|
{
|
||||||
|
startUid = 100000;
|
||||||
|
count = 65536;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
subGidRanges = [
|
||||||
|
{
|
||||||
|
startGid = 100000;
|
||||||
|
count = 65536;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
services.openssh.enable = true;
|
||||||
|
|
||||||
|
my.users.petere = {
|
||||||
|
hashedPasswordFile = config.sops.secrets."users/petere-password".path;
|
||||||
|
subUidStart = 165536;
|
||||||
|
subGidStart = 165536;
|
||||||
|
};
|
||||||
|
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
xournalpp
|
||||||
|
prismlauncher
|
||||||
|
jdk21
|
||||||
|
rclone
|
||||||
|
steam
|
||||||
|
];
|
||||||
|
|
||||||
|
hardware.graphics.enable = true;
|
||||||
|
programs.steam.enable = true;
|
||||||
|
|
||||||
|
# Allow Steam to use 32-bit libraries
|
||||||
|
programs.steam.package = pkgs.steam.override {
|
||||||
|
extraLibraries = ps: [ ];
|
||||||
|
};
|
||||||
|
|
||||||
|
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# /hosts/x1carbon/disko-config.nix
|
||||||
|
# This file will contain your disko configuration for x1carbon.
|
||||||
|
{
|
||||||
|
disk = {
|
||||||
|
nixos = {
|
||||||
|
type = "disk";
|
||||||
|
device = "/dev/nvme0n1";
|
||||||
|
content = {
|
||||||
|
type = "gpt";
|
||||||
|
partitions = {
|
||||||
|
boot = {
|
||||||
|
size = "1M";
|
||||||
|
type = "EF02"; # for grub MBR
|
||||||
|
};
|
||||||
|
ESP = {
|
||||||
|
size = "512M";
|
||||||
|
type = "EF00";
|
||||||
|
content = {
|
||||||
|
type = "filesystem";
|
||||||
|
format = "vfat";
|
||||||
|
mountpoint = "/boot";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
swap = {
|
||||||
|
size = "20G";
|
||||||
|
type = "8200";
|
||||||
|
content = {
|
||||||
|
type = "swap";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
root = {
|
||||||
|
size = "100%";
|
||||||
|
content = {
|
||||||
|
type = "filesystem";
|
||||||
|
format = "ext4";
|
||||||
|
mountpoint = "/";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# /hosts/x1carbon/hardware-configuration.nix
|
||||||
|
# This file will be generated by NixOS during installation or by 'nixos-generate-config'.
|
||||||
|
# It contains hardware-specific settings for x1carbon.
|
||||||
|
{ config
|
||||||
|
, lib
|
||||||
|
, pkgs
|
||||||
|
, modulesPath
|
||||||
|
, ...
|
||||||
|
}:
|
||||||
|
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
(modulesPath + "/installer/scan/not-detected.nix")
|
||||||
|
];
|
||||||
|
|
||||||
|
#hardware.ipu6.enable = true;
|
||||||
|
#hardware.ipu6.platform = "ipu6";
|
||||||
|
|
||||||
|
boot.initrd.availableKernelModules = [
|
||||||
|
"xhci_pci"
|
||||||
|
"nvme"
|
||||||
|
"usb_storage"
|
||||||
|
"sd_mod"
|
||||||
|
];
|
||||||
|
boot.initrd.kernelModules = [ ];
|
||||||
|
boot.kernelModules = [ "kvm-intel" ];
|
||||||
|
boot.extraModulePackages = [ ];
|
||||||
|
|
||||||
|
# fileSystems."/" =
|
||||||
|
# { device = "/dev/disk/by-uuid/882a76d6-c1ac-4efd-aff8-b56d43ec7ca5";
|
||||||
|
# fsType = "ext4";
|
||||||
|
# };
|
||||||
|
|
||||||
|
# fileSystems."/boot" =
|
||||||
|
# { device = "/dev/disk/by-uuid/EF30-EBA7";
|
||||||
|
# fsType = "vfat";
|
||||||
|
# options = [ "fmask=0077" "dmask=0077" ];
|
||||||
|
# };
|
||||||
|
|
||||||
|
# swapDevices =
|
||||||
|
# [ { device = "/dev/disk/by-uuid/1c488737-a2e7-4258-b0f3-bd1d14155d03"; }
|
||||||
|
# ];
|
||||||
|
|
||||||
|
# Enables DHCP on each ethernet and wireless interface. In case of scripted networking
|
||||||
|
# (the default) this is the recommended approach. When using systemd-networkd it's
|
||||||
|
# still possible to use this option, but it's recommended to use it in conjunction
|
||||||
|
# with explicit per-interface declarations with `networking.interfaces.<interface>.useDHCP`.
|
||||||
|
networking.useDHCP = lib.mkDefault true;
|
||||||
|
# networking.interfaces.enp0s31f6.useDHCP = lib.mkDefault true;
|
||||||
|
# networking.interfaces.wlp0s20f3.useDHCP = lib.mkDefault true;
|
||||||
|
|
||||||
|
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
|
||||||
|
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
# NixOS configuration for homeserver-1
|
||||||
|
# Headless server environment (Command Line Interface only)
|
||||||
|
|
||||||
|
{ config
|
||||||
|
, pkgs
|
||||||
|
, lib
|
||||||
|
, inputs
|
||||||
|
, ...
|
||||||
|
}:
|
||||||
|
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
../../modules/core/common.nix
|
||||||
|
(import ../../modules/storage/disko.nix {
|
||||||
|
inherit inputs lib config;
|
||||||
|
diskoConfigPath = ./disko-config.nix;
|
||||||
|
})
|
||||||
|
./hardware-configuration.nix
|
||||||
|
../../modules/core/management.nix
|
||||||
|
../../modules/core/podman.nix
|
||||||
|
../../modules/hardware/nvidia.nix
|
||||||
|
../../modules/core/known-hosts.nix
|
||||||
|
../../modules/services/immich.nix
|
||||||
|
../../modules/services/jellyfin.nix
|
||||||
|
../../modules/services/backrest.nix
|
||||||
|
../../modules/services/homepage.nix
|
||||||
|
./homepage.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
services.backrest = {
|
||||||
|
enable = true;
|
||||||
|
host = "0.0.0.0";
|
||||||
|
port = 9898;
|
||||||
|
dataDir = "/data/backrest";
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services.backrest = {
|
||||||
|
after = [ "restic-ssh-key-format.service" ];
|
||||||
|
wants = [ "restic-ssh-key-format.service" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Allow backrest to read backup source directories recursively.
|
||||||
|
# Uses ACLs with a default mask so newly created files also inherit access.
|
||||||
|
# Runs after immich and the postgresql backup so the dirs/files exist.
|
||||||
|
systemd.services.backrest-permissions = {
|
||||||
|
description = "Grant backrest read access to backup source dirs";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
after = [
|
||||||
|
"immich-server.service"
|
||||||
|
"postgresqlBackup-immich.service"
|
||||||
|
];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
};
|
||||||
|
script = ''
|
||||||
|
# Recursive read+execute ACL for backrest on immich media
|
||||||
|
${pkgs.acl}/bin/setfacl -R -m u:backrest:rx -m m::r-x /data/immich
|
||||||
|
# Default ACL so future immich files are readable by backrest
|
||||||
|
${pkgs.acl}/bin/setfacl -R -m d:u:backrest:rx -m d:m::r-x /data/immich
|
||||||
|
# Recursive read for backrest on postgresql backups
|
||||||
|
${pkgs.acl}/bin/setfacl -R -m u:backrest:rx -m m::r-x /data/backup/postgresql
|
||||||
|
${pkgs.acl}/bin/setfacl -R -m d:u:backrest:rx -m d:m::r-x /data/backup/postgresql
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
services.immich-server = {
|
||||||
|
enable = true;
|
||||||
|
port = 2283;
|
||||||
|
mediaLocation = "/data/immich";
|
||||||
|
};
|
||||||
|
|
||||||
|
services.jellyfin-server = {
|
||||||
|
enable = true;
|
||||||
|
port = 8096;
|
||||||
|
mediaLocation = "/data/jellyfin";
|
||||||
|
};
|
||||||
|
|
||||||
|
services.postgresqlBackup = {
|
||||||
|
enable = true;
|
||||||
|
databases = [ "immich" ];
|
||||||
|
location = "/data/backup/postgresql";
|
||||||
|
compression = "zstd";
|
||||||
|
startAt = "weekly";
|
||||||
|
};
|
||||||
|
|
||||||
|
# The services.postgresqlBackup module creates a tmpfiles `d` rule that sets
|
||||||
|
# the backup directory to 0700 on every rebuild/switch. On a directory with
|
||||||
|
# POSIX ACLs, `chmod 0700` resets the ACL mask to `---`, which nullifies the
|
||||||
|
# `user:backrest` read ACL that backrest uses to back up these dumps. Override
|
||||||
|
# the mode so rebuilds keep the directory group-traversable (mask stays rx).
|
||||||
|
# Note: systemd-tmpfiles dedupes conflicting rules and keeps the FIRST one for
|
||||||
|
# a path, so our override must appear before the module's rule.
|
||||||
|
systemd.tmpfiles.rules = lib.mkBefore [
|
||||||
|
"d /data/backup/postgresql 0750 postgres - - -"
|
||||||
|
];
|
||||||
|
|
||||||
|
sops.secrets = {
|
||||||
|
"users/petere-password" = {
|
||||||
|
neededForUsers = true;
|
||||||
|
};
|
||||||
|
"pocket-id-env" = {
|
||||||
|
neededForUsers = false;
|
||||||
|
};
|
||||||
|
"homeserver-1/restic-passphrase" = { };
|
||||||
|
"homeserver-1/restic-ssh-key" = { };
|
||||||
|
"homeserver-1/homepage-env" = { };
|
||||||
|
"homeserver-1/samba-petere-password" = { };
|
||||||
|
};
|
||||||
|
|
||||||
|
# Set petere's Samba password from SOPS at boot. Samba keeps its own password
|
||||||
|
# database (smbpasswd/tdbsam), separate from Linux login, so this must run
|
||||||
|
# smbpasswd. Idempotent: re-applied on every boot from the secret.
|
||||||
|
systemd.services.samba-set-petere-password = {
|
||||||
|
description = "Set petere's Samba password from SOPS";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
after = [ "sops-nix.service" ];
|
||||||
|
before = [ "samba-smbd.service" ];
|
||||||
|
serviceConfig.Type = "oneshot";
|
||||||
|
script = ''
|
||||||
|
PASS="$(cat ${config.sops.secrets."homeserver-1/samba-petere-password".path})"
|
||||||
|
${pkgs.samba}/bin/smbpasswd -s -a petere <<EOF
|
||||||
|
$PASS
|
||||||
|
$PASS
|
||||||
|
EOF
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# Install the restic SSH key (stored multi-line in sops) and set up SSH access
|
||||||
|
# for the backrest user so restic can reach mcf-server.
|
||||||
|
# Writes to a persistent location (NOT /run) because sops-nix clears /run/secrets.
|
||||||
|
systemd.services.restic-ssh-key-format = {
|
||||||
|
description = "Install restic SSH key and configure SSH for backrest";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
after = [ "sops-nix.service" ];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
};
|
||||||
|
script = ''
|
||||||
|
KEY_FILE="${config.sops.secrets."homeserver-1/restic-ssh-key".path}"
|
||||||
|
FORMATTED="/data/backrest/restic-ssh-key"
|
||||||
|
${pkgs.coreutils}/bin/install -m 640 -o root -g backrest "$KEY_FILE" "$FORMATTED"
|
||||||
|
|
||||||
|
# SSH config for backrest so restic (via Backrest) uses the correct key
|
||||||
|
mkdir -p /data/backrest/.ssh
|
||||||
|
cat > /data/backrest/.ssh/config <<EOF
|
||||||
|
Host mcf-server
|
||||||
|
HostName mcf-server
|
||||||
|
User restic-homeserver1
|
||||||
|
IdentityFile /data/backrest/restic-ssh-key
|
||||||
|
IdentitiesOnly yes
|
||||||
|
Host richmond-server
|
||||||
|
HostName richmond-server
|
||||||
|
User restic-homeserver1
|
||||||
|
IdentityFile /data/backrest/restic-ssh-key
|
||||||
|
IdentitiesOnly yes
|
||||||
|
EOF
|
||||||
|
chown -R backrest:backrest /data/backrest/.ssh
|
||||||
|
chmod 700 /data/backrest/.ssh
|
||||||
|
chmod 600 /data/backrest/.ssh/config
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
networking.hostName = "homeserver-1";
|
||||||
|
|
||||||
|
# SSH configuration
|
||||||
|
services.openssh.enable = true;
|
||||||
|
|
||||||
|
# Trusted host keys for restic backup targets (via Backrest).
|
||||||
|
my.knownHosts = {
|
||||||
|
mcfServer = true;
|
||||||
|
richmondServer = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Glances system monitor - exposed to the tailnet so the Homepage
|
||||||
|
# dashboard can display real-time stats for this machine (localhost).
|
||||||
|
services.glances = {
|
||||||
|
enable = true;
|
||||||
|
port = 61208;
|
||||||
|
extraArgs = [ "--webserver" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Expose services on Tailscale only (not the LAN).
|
||||||
|
# Immich (2283): accessed via nginx proxy on another machine over Tailscale.
|
||||||
|
# Backrest (9898), Pocket ID (8443), Homepage (8082), Glances (61208): admin services.
|
||||||
|
networking.firewall.interfaces.tailscale.allowedTCPPorts = lib.mkAfter [
|
||||||
|
2283 # Immich
|
||||||
|
8443 # Pocket ID
|
||||||
|
9898 # Backrest
|
||||||
|
8082 # Homepage
|
||||||
|
61208 # Glances
|
||||||
|
];
|
||||||
|
|
||||||
|
# Add Pocket ID package for tooling
|
||||||
|
environment.systemPackages = with pkgs; [ pocket-id ];
|
||||||
|
|
||||||
|
# Pocket ID service configuration
|
||||||
|
services.pocket-id = {
|
||||||
|
enable = true;
|
||||||
|
environmentFile = config.sops.secrets."pocket-id-env".path;
|
||||||
|
settings = {
|
||||||
|
APP_URL = "https://homeserver-1.gerbil-opah.ts.net:8443";
|
||||||
|
PORT = 8443;
|
||||||
|
TRUST_PROXY = true;
|
||||||
|
TLS_CERT_FILE = "/etc/ssl/certs/pocket-id.crt";
|
||||||
|
TLS_KEY_FILE = "/etc/ssl/private/pocket-id.key";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services.pocket-id = {
|
||||||
|
wants = [ "pocket-id-tailscale-cert.service" ];
|
||||||
|
after = [ "pocket-id-tailscale-cert.service" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Systemd service to obtain TLS cert via Tailscale
|
||||||
|
systemd.services.pocket-id-tailscale-cert = {
|
||||||
|
description = "Obtain TLS cert for Pocket-ID via Tailscale";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
wants = [
|
||||||
|
"network-online.target"
|
||||||
|
"tailscaled.service"
|
||||||
|
];
|
||||||
|
after = [
|
||||||
|
"network-online.target"
|
||||||
|
"tailscaled.service"
|
||||||
|
];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
ExecStart = pkgs.writeShellScript "get-tailscale-cert" ''
|
||||||
|
set -eu
|
||||||
|
mkdir -p /etc/ssl/certs /etc/ssl/private
|
||||||
|
if [ ! -f /etc/ssl/certs/pocket-id.crt ] || [ ! -f /etc/ssl/private/pocket-id.key ]; then
|
||||||
|
${pkgs.tailscale}/bin/tailscale cert --cert-file /etc/ssl/certs/pocket-id.crt --key-file /etc/ssl/private/pocket-id.key homeserver-1.gerbil-opah.ts.net
|
||||||
|
fi
|
||||||
|
chown root:pocket-id /etc/ssl/private/pocket-id.key
|
||||||
|
chmod 640 /etc/ssl/private/pocket-id.key
|
||||||
|
chmod 644 /etc/ssl/certs/pocket-id.crt
|
||||||
|
'';
|
||||||
|
User = "root";
|
||||||
|
Group = "root";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# NVIDIA GPU Configuration for GeForce GTX 960 (Maxwell GM206)
|
||||||
|
my.hardware.nvidia = {
|
||||||
|
enable = true;
|
||||||
|
# GTX 960 (Maxwell) needs the 580.xx legacy driver branch; the default
|
||||||
|
# driver no longer supports it (NVRM: No NVIDIA GPU found).
|
||||||
|
package = config.boot.kernelPackages.nvidiaPackages.legacy_580;
|
||||||
|
};
|
||||||
|
|
||||||
|
boot.kernelModules = [ "sg" ];
|
||||||
|
|
||||||
|
# Root account is locked (no password login); access is via SSH key + sudo.
|
||||||
|
users.users.root.hashedPassword = "!";
|
||||||
|
|
||||||
|
# Standard user account (shared definition in modules/core/users.nix)
|
||||||
|
my.users.petere = {
|
||||||
|
hashedPasswordFile = config.sops.secrets."users/petere-password".path;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Trusted users for Nix operations
|
||||||
|
nix.settings.trusted-users = [
|
||||||
|
"root"
|
||||||
|
"petere"
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
{
|
||||||
|
disk = {
|
||||||
|
main = {
|
||||||
|
type = "disk";
|
||||||
|
device = "/dev/nvme0n1";
|
||||||
|
content = {
|
||||||
|
type = "gpt";
|
||||||
|
partitions = {
|
||||||
|
boot = {
|
||||||
|
size = "1M";
|
||||||
|
type = "EF02"; # for GRUB MBR fallback
|
||||||
|
};
|
||||||
|
ESP = {
|
||||||
|
size = "512M";
|
||||||
|
type = "EF00";
|
||||||
|
content = {
|
||||||
|
type = "filesystem";
|
||||||
|
format = "vfat";
|
||||||
|
mountpoint = "/boot";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
root = {
|
||||||
|
size = "100%";
|
||||||
|
content = {
|
||||||
|
type = "filesystem";
|
||||||
|
format = "ext4";
|
||||||
|
mountpoint = "/";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
data1 = {
|
||||||
|
type = "disk";
|
||||||
|
device = "/dev/sda";
|
||||||
|
content = {
|
||||||
|
type = "gpt";
|
||||||
|
partitions = {
|
||||||
|
data = {
|
||||||
|
size = "100%";
|
||||||
|
content = {
|
||||||
|
type = "btrfs";
|
||||||
|
extraArgs = [ "-f" ];
|
||||||
|
mountOptions = [
|
||||||
|
"defaults"
|
||||||
|
"nofail"
|
||||||
|
];
|
||||||
|
mountpoint = "/data";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
data2 = {
|
||||||
|
type = "disk";
|
||||||
|
device = "/dev/sdb";
|
||||||
|
content = {
|
||||||
|
type = "gpt";
|
||||||
|
partitions = {
|
||||||
|
data = {
|
||||||
|
size = "100%";
|
||||||
|
content = {
|
||||||
|
type = "btrfs";
|
||||||
|
extraArgs = [ "-f" ];
|
||||||
|
postCreateHook = ''
|
||||||
|
btrfs device add -f /dev/disk/by-partlabel/disk-data2-data /mnt/data || true
|
||||||
|
btrfs balance start -dconvert=raid1 -mconvert=raid1 /mnt/data || true
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{ config
|
||||||
|
, lib
|
||||||
|
, pkgs
|
||||||
|
, modulesPath
|
||||||
|
, ...
|
||||||
|
}:
|
||||||
|
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
(modulesPath + "/installer/scan/not-detected.nix")
|
||||||
|
];
|
||||||
|
|
||||||
|
boot.initrd.availableKernelModules = [
|
||||||
|
"nvme"
|
||||||
|
"xhci_pci"
|
||||||
|
"ahci"
|
||||||
|
"usb_storage"
|
||||||
|
"usbhid"
|
||||||
|
"sd_mod"
|
||||||
|
];
|
||||||
|
boot.initrd.kernelModules = [ ];
|
||||||
|
boot.supportedFilesystems = [
|
||||||
|
"btrfs"
|
||||||
|
"ext4"
|
||||||
|
"vfat"
|
||||||
|
];
|
||||||
|
boot.kernelModules = [ "kvm-intel" ];
|
||||||
|
boot.extraModulePackages = [ ];
|
||||||
|
|
||||||
|
networking.useDHCP = lib.mkDefault true;
|
||||||
|
|
||||||
|
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
|
||||||
|
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
|
||||||
|
}
|
||||||
@@ -0,0 +1,516 @@
|
|||||||
|
# Homepage dashboard (gethomepage.dev) configuration for homeserver-1
|
||||||
|
#
|
||||||
|
# This file holds the full dashboard definition (settings, services, widgets).
|
||||||
|
# It is imported by configuration.nix. Edit this file to change what the
|
||||||
|
# dashboard shows, then run:
|
||||||
|
# nixos-rebuild switch --target-host petere@homeserver-1 --flake .#homeserver-1 --use-remote-sudo
|
||||||
|
|
||||||
|
{ config
|
||||||
|
, lib
|
||||||
|
, ...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
# Build a Tailscale widget tile for the Tailnet tab. Shared so the
|
||||||
|
# highlight rules (expiry / last-seen) are defined once.
|
||||||
|
# Returns a service entry: { "<name>" = { icon; href; description; widget; } }
|
||||||
|
tailscaleTile = name: deviceid: description: {
|
||||||
|
${name} = {
|
||||||
|
icon = "sh-tailscale";
|
||||||
|
href = "https://login.tailscale.com/admin/machines";
|
||||||
|
inherit description;
|
||||||
|
widget = {
|
||||||
|
type = "tailscale";
|
||||||
|
inherit deviceid;
|
||||||
|
key = "{{HOMEPAGE_VAR_TAILSCALE_API_KEY}}";
|
||||||
|
# Highlight rules match the rendered field values (e.g. "24w", "Never",
|
||||||
|
# "8h Ago", "2w Ago"). Warn = expiring within a week, danger = offline >24h.
|
||||||
|
highlight = {
|
||||||
|
expires = {
|
||||||
|
string = [
|
||||||
|
{
|
||||||
|
level = "warn";
|
||||||
|
when = "regex";
|
||||||
|
# 1-7 days, or hours/minutes/seconds remaining (i.e. within a week)
|
||||||
|
value = "^\\d+[dhms]$";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
last_seen = {
|
||||||
|
string = [
|
||||||
|
{
|
||||||
|
level = "danger";
|
||||||
|
when = "regex";
|
||||||
|
# days/weeks/years ago (i.e. not seen for more than 24 hours)
|
||||||
|
value = "^\\d+[dwy] Ago$";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
services.homepage = {
|
||||||
|
enable = true;
|
||||||
|
port = 8082;
|
||||||
|
allowedHosts = [
|
||||||
|
"localhost"
|
||||||
|
"127.0.0.1"
|
||||||
|
"homeserver-1"
|
||||||
|
"homeserver-1.gerbil-opah.ts.net"
|
||||||
|
];
|
||||||
|
openFirewall = false; # Exposed on Tailscale only (see firewall in configuration.nix)
|
||||||
|
|
||||||
|
# API keys / secrets for service widgets (HOMEPAGE_VAR_* vars)
|
||||||
|
environmentFiles = [ config.sops.secrets."homeserver-1/homepage-env".path ];
|
||||||
|
|
||||||
|
settings = {
|
||||||
|
title = "HomeServer";
|
||||||
|
language = "en";
|
||||||
|
theme = "dark";
|
||||||
|
color = "slate";
|
||||||
|
statusStyle = "dot";
|
||||||
|
# Tabs: each layout group's `tab` value controls which tab it appears on.
|
||||||
|
# Groups without a `tab` (or with no layout entry) show on every tab.
|
||||||
|
layout = {
|
||||||
|
# ---- Monitoring tab ----
|
||||||
|
"Homeserver-1 Monitoring" = {
|
||||||
|
tab = "Monitoring";
|
||||||
|
style = "row";
|
||||||
|
columns = 1;
|
||||||
|
"HS1 System" = {
|
||||||
|
style = "row";
|
||||||
|
columns = 2;
|
||||||
|
};
|
||||||
|
"HS1 Disks" = {
|
||||||
|
style = "row";
|
||||||
|
columns = 2; # NVMe + SDA side by side
|
||||||
|
};
|
||||||
|
};
|
||||||
|
"MCF Server Monitoring" = {
|
||||||
|
tab = "Monitoring";
|
||||||
|
style = "row";
|
||||||
|
columns = 1;
|
||||||
|
"MCF System" = {
|
||||||
|
style = "row";
|
||||||
|
columns = 2;
|
||||||
|
};
|
||||||
|
"MCF Disks" = {
|
||||||
|
style = "row";
|
||||||
|
columns = 2; # SDB (system) + SDA (data) side by side
|
||||||
|
};
|
||||||
|
};
|
||||||
|
"Richmond Server Monitoring" = {
|
||||||
|
tab = "Monitoring";
|
||||||
|
style = "row";
|
||||||
|
columns = 1;
|
||||||
|
"Richmond System" = {
|
||||||
|
style = "row";
|
||||||
|
columns = 2;
|
||||||
|
};
|
||||||
|
"Richmond Disks" = {
|
||||||
|
style = "row";
|
||||||
|
columns = 2; # SDB (system) + SDA (data) side by side
|
||||||
|
};
|
||||||
|
};
|
||||||
|
# ---- Homeserver-1 tab ----
|
||||||
|
Media = {
|
||||||
|
tab = "Homeserver-1";
|
||||||
|
style = "row";
|
||||||
|
columns = 2;
|
||||||
|
};
|
||||||
|
System = {
|
||||||
|
tab = "Homeserver-1";
|
||||||
|
style = "row";
|
||||||
|
columns = 3;
|
||||||
|
};
|
||||||
|
# ---- MCF Server tab (populate with future mcf-server services) ----
|
||||||
|
# "MCF Server Apps" = {
|
||||||
|
# tab = "MCF Server";
|
||||||
|
# style = "row";
|
||||||
|
# columns = 4;
|
||||||
|
# };
|
||||||
|
# ---- Richmond Server tab ----
|
||||||
|
"Richmond Server Apps" = {
|
||||||
|
tab = "Richmond Server";
|
||||||
|
style = "row";
|
||||||
|
columns = 3;
|
||||||
|
};
|
||||||
|
# ---- Tailnet tab (one Tailscale widget per machine) ----
|
||||||
|
Tailnet = {
|
||||||
|
tab = "Tailnet";
|
||||||
|
style = "row";
|
||||||
|
columns = 3;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
services = [
|
||||||
|
{
|
||||||
|
"Homeserver-1 Monitoring" = [
|
||||||
|
{
|
||||||
|
"HS1 System" = [
|
||||||
|
{
|
||||||
|
"System" = {
|
||||||
|
widget = {
|
||||||
|
type = "glances";
|
||||||
|
url = "http://127.0.0.1:61208";
|
||||||
|
version = 4; # Glances v4.x
|
||||||
|
metric = "info";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
"CPU" = {
|
||||||
|
widget = {
|
||||||
|
type = "glances";
|
||||||
|
url = "http://127.0.0.1:61208";
|
||||||
|
version = 4;
|
||||||
|
metric = "cpu";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
"Memory" = {
|
||||||
|
widget = {
|
||||||
|
type = "glances";
|
||||||
|
url = "http://127.0.0.1:61208";
|
||||||
|
version = 4;
|
||||||
|
metric = "memory";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
"Processes" = {
|
||||||
|
widget = {
|
||||||
|
type = "glances";
|
||||||
|
url = "http://127.0.0.1:61208";
|
||||||
|
version = 4;
|
||||||
|
metric = "process";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
{
|
||||||
|
"HS1 Disks" = [
|
||||||
|
{
|
||||||
|
"NVMe - System" = {
|
||||||
|
widget = {
|
||||||
|
type = "glances";
|
||||||
|
url = "http://127.0.0.1:61208";
|
||||||
|
version = 4;
|
||||||
|
metric = "disk:nvme0n1";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
"SDA - Data" = {
|
||||||
|
widget = {
|
||||||
|
type = "glances";
|
||||||
|
url = "http://127.0.0.1:61208";
|
||||||
|
version = 4;
|
||||||
|
metric = "disk:sda";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
{
|
||||||
|
"MCF Server Monitoring" = [
|
||||||
|
{
|
||||||
|
"MCF System" = [
|
||||||
|
{
|
||||||
|
"System" = {
|
||||||
|
widget = {
|
||||||
|
type = "glances";
|
||||||
|
url = "http://mcf-server.gerbil-opah.ts.net:61208";
|
||||||
|
version = 4; # Glances v4.x
|
||||||
|
metric = "info";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
"CPU" = {
|
||||||
|
widget = {
|
||||||
|
type = "glances";
|
||||||
|
url = "http://mcf-server.gerbil-opah.ts.net:61208";
|
||||||
|
version = 4;
|
||||||
|
metric = "cpu";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
"Memory" = {
|
||||||
|
widget = {
|
||||||
|
type = "glances";
|
||||||
|
url = "http://mcf-server.gerbil-opah.ts.net:61208";
|
||||||
|
version = 4;
|
||||||
|
metric = "memory";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
"Processes" = {
|
||||||
|
widget = {
|
||||||
|
type = "glances";
|
||||||
|
url = "http://mcf-server.gerbil-opah.ts.net:61208";
|
||||||
|
version = 4;
|
||||||
|
metric = "process";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
{
|
||||||
|
"MCF Disks" = [
|
||||||
|
{
|
||||||
|
"SDB - System" = {
|
||||||
|
widget = {
|
||||||
|
type = "glances";
|
||||||
|
url = "http://mcf-server.gerbil-opah.ts.net:61208";
|
||||||
|
version = 4;
|
||||||
|
metric = "disk:sdb";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
"SDA - Data" = {
|
||||||
|
widget = {
|
||||||
|
type = "glances";
|
||||||
|
url = "http://mcf-server.gerbil-opah.ts.net:61208";
|
||||||
|
version = 4;
|
||||||
|
metric = "disk:sda";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
{
|
||||||
|
"Richmond Server Monitoring" = [
|
||||||
|
{
|
||||||
|
"Richmond System" = [
|
||||||
|
{
|
||||||
|
"System" = {
|
||||||
|
widget = {
|
||||||
|
type = "glances";
|
||||||
|
url = "http://richmond-server.gerbil-opah.ts.net:61208";
|
||||||
|
version = 4; # Glances v4.x
|
||||||
|
metric = "info";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
"CPU" = {
|
||||||
|
widget = {
|
||||||
|
type = "glances";
|
||||||
|
url = "http://richmond-server.gerbil-opah.ts.net:61208";
|
||||||
|
version = 4;
|
||||||
|
metric = "cpu";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
"Memory" = {
|
||||||
|
widget = {
|
||||||
|
type = "glances";
|
||||||
|
url = "http://richmond-server.gerbil-opah.ts.net:61208";
|
||||||
|
version = 4;
|
||||||
|
metric = "memory";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
"Processes" = {
|
||||||
|
widget = {
|
||||||
|
type = "glances";
|
||||||
|
url = "http://richmond-server.gerbil-opah.ts.net:61208";
|
||||||
|
version = 4;
|
||||||
|
metric = "process";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
{
|
||||||
|
"Richmond Disks" = [
|
||||||
|
{
|
||||||
|
"SDB - System" = {
|
||||||
|
widget = {
|
||||||
|
type = "glances";
|
||||||
|
url = "http://richmond-server.gerbil-opah.ts.net:61208";
|
||||||
|
version = 4;
|
||||||
|
metric = "disk:sdb";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
"SDA - Data" = {
|
||||||
|
widget = {
|
||||||
|
type = "glances";
|
||||||
|
url = "http://richmond-server.gerbil-opah.ts.net:61208";
|
||||||
|
version = 4;
|
||||||
|
metric = "disk:sda";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
{
|
||||||
|
Media = [
|
||||||
|
{
|
||||||
|
Jellyfin = {
|
||||||
|
icon = "sh-jellyfin";
|
||||||
|
href = "http://jellyfin.edley.me";
|
||||||
|
description = "Movies & TV";
|
||||||
|
siteMonitor = "http://127.0.0.1:8096";
|
||||||
|
widget = {
|
||||||
|
type = "jellyfin";
|
||||||
|
url = "http://127.0.0.1:8096";
|
||||||
|
key = "{{HOMEPAGE_VAR_JELLYFIN_API_KEY}}";
|
||||||
|
enableBlocks = true;
|
||||||
|
enableNowPlaying = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
Immich = {
|
||||||
|
icon = "sh-immich";
|
||||||
|
href = "http://immich.edley.me";
|
||||||
|
description = "Photo & Video";
|
||||||
|
siteMonitor = "http://127.0.0.1:2283";
|
||||||
|
widget = {
|
||||||
|
type = "immich";
|
||||||
|
url = "http://127.0.0.1:2283";
|
||||||
|
key = "{{HOMEPAGE_VAR_IMMICH_API_KEY}}";
|
||||||
|
version = 2; # Immich >= 1.118
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
{
|
||||||
|
System = [
|
||||||
|
{
|
||||||
|
Backrest = {
|
||||||
|
icon = "sh-backrest";
|
||||||
|
href = "http://homeserver-1.gerbil-opah.ts.net:9898";
|
||||||
|
description = "Restic backup UI";
|
||||||
|
siteMonitor = "http://127.0.0.1:9898";
|
||||||
|
widget = {
|
||||||
|
type = "backrest";
|
||||||
|
url = "http://127.0.0.1:9898";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
"Pocket ID" = {
|
||||||
|
icon = "sh-pocketbase";
|
||||||
|
href = "https://homeserver-1.gerbil-opah.ts.net:8443";
|
||||||
|
description = "SSO / Identity";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
# ---- MCF Server tab: add future mcf-server services here ----
|
||||||
|
# {
|
||||||
|
# "MCF Server Apps" = [
|
||||||
|
# {
|
||||||
|
# "MyApp" = {
|
||||||
|
# icon = "sh-myservice";
|
||||||
|
# href = "http://mcf-server.gerbil-opah.ts.net:<port>";
|
||||||
|
# siteMonitor = "http://mcf-server.gerbil-opah.ts.net:<port>";
|
||||||
|
# };
|
||||||
|
# }
|
||||||
|
# ];
|
||||||
|
# }
|
||||||
|
{
|
||||||
|
"Richmond Server Apps" = [
|
||||||
|
{
|
||||||
|
PiHole = {
|
||||||
|
icon = "sh-pihole";
|
||||||
|
href = "http://richmond-server.gerbil-opah.ts.net/admin";
|
||||||
|
description = "Network-wide ad blocking";
|
||||||
|
siteMonitor = "http://richmond-server.gerbil-opah.ts.net";
|
||||||
|
widget = {
|
||||||
|
type = "pihole";
|
||||||
|
url = "http://richmond-server.gerbil-opah.ts.net";
|
||||||
|
version = 6; # Pi-hole v6
|
||||||
|
key = "{{HOMEPAGE_VAR_PIHOLE_API_KEY}}";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
Castopod = {
|
||||||
|
icon = "sh-castopod";
|
||||||
|
href = "http://richmond-server.gerbil-opah.ts.net:8080";
|
||||||
|
description = "Podcasting platform";
|
||||||
|
siteMonitor = "http://richmond-server.gerbil-opah.ts.net:8080";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
Ntfy = {
|
||||||
|
icon = "sh-ntfy";
|
||||||
|
href = "https://ntfy.edley.me";
|
||||||
|
description = "Push notifications";
|
||||||
|
siteMonitor = "http://richmond-server.gerbil-opah.ts.net:8085";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
# ---- Tailnet tab: one Tailscale widget per machine ----
|
||||||
|
{
|
||||||
|
Tailnet = [
|
||||||
|
(tailscaleTile "Homeserver-1" "7629334038136604" "homeserver-1.gerbil-opah.ts.net")
|
||||||
|
(tailscaleTile "MCF Server" "6531912398509392" "mcf-server.gerbil-opah.ts.net")
|
||||||
|
(tailscaleTile "Richmond Server" "4591058654038528" "richmond-server.gerbil-opah.ts.net")
|
||||||
|
(tailscaleTile "x1carbon" "3486385364789872" "x1carbon.gerbil-opah.ts.net")
|
||||||
|
(tailscaleTile "x470" "210979648507396" "x470.gerbil-opah.ts.net")
|
||||||
|
(tailscaleTile "caitlin-x1" "4406025993575891" "caitlin-x1.gerbil-opah.ts.net")
|
||||||
|
(tailscaleTile "Mary Laptop" "5830538092696610" "mary-laptop.gerbil-opah.ts.net")
|
||||||
|
(tailscaleTile "Pluto" "4170550062707667" "pluto.gerbil-opah.ts.net")
|
||||||
|
(tailscaleTile "TheBorg" "2268885677960290" "theborg.gerbil-opah.ts.net")
|
||||||
|
(tailscaleTile "Server" "3605859775136634" "server.gerbil-opah.ts.net")
|
||||||
|
(tailscaleTile "Homeserver" "5665047953117744" "homeserver.gerbil-opah.ts.net")
|
||||||
|
(tailscaleTile "MCF Projector" "5131578183597553" "mcf-projector.gerbil-opah.ts.net")
|
||||||
|
(tailscaleTile "MCF Stream" "6804850380243723" "mcf-stream.gerbil-opah.ts.net")
|
||||||
|
(tailscaleTile "Yoga 12" "1716848030756573" "yoga12.gerbil-opah.ts.net")
|
||||||
|
(tailscaleTile "Pixel 9 Pro XL" "6766405315162342" "pixel-9-pro-xl.gerbil-opah.ts.net")
|
||||||
|
];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
widgets = [
|
||||||
|
{
|
||||||
|
resources = {
|
||||||
|
cpu = true;
|
||||||
|
memory = true;
|
||||||
|
disk = "/";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
search = {
|
||||||
|
provider = "duckduckgo";
|
||||||
|
target = "_blank";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
datetime = {
|
||||||
|
text_size = "xl";
|
||||||
|
locale = "en-GB"; # UK date format (dd/mm/yy)
|
||||||
|
format = {
|
||||||
|
dateStyle = "short";
|
||||||
|
timeStyle = "short";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
# /hosts/hp-laptop/configuration.nix
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
inputs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
./hardware-configuration.nix
|
||||||
|
../../modules/core/common.nix
|
||||||
|
../../modules/desktop/gui.nix
|
||||||
|
(import ../../modules/storage/disko.nix {
|
||||||
|
inherit inputs lib config;
|
||||||
|
diskoConfigPath = ./disko-config.nix;
|
||||||
|
})
|
||||||
|
../../modules/desktop/hyprland.nix
|
||||||
|
../../modules/core/management.nix
|
||||||
|
../../modules/hardware/laptop.nix
|
||||||
|
../../modules/hardware/hp-battery-limit.nix
|
||||||
|
../../modules/core/dev.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
sops.secrets = {
|
||||||
|
"users/petere-password" = {
|
||||||
|
neededForUsers = true;
|
||||||
|
};
|
||||||
|
# Nextcloud CalDAV credentials for the QuickShell calendar popup
|
||||||
|
# (vdirsyncer/khal sync). Values live in secrets.yaml under
|
||||||
|
# hp-laptop/nextcloud-cal-env; sync runs as petere so it must be readable.
|
||||||
|
"hp-laptop/nextcloud-cal-env" = {
|
||||||
|
owner = "petere";
|
||||||
|
group = "users";
|
||||||
|
mode = "0440";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
home-manager.users.petere.imports = [
|
||||||
|
../../home-manager/modules/hyprland.nix
|
||||||
|
../../home-manager/modules/quickshell-cal.nix
|
||||||
|
../../home-manager/modules/quickshell-apps.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
home-manager.users.petere.services.quickshell-cal.enable = true;
|
||||||
|
|
||||||
|
# Autostart app pool (toggleable from the gear quick-settings panel).
|
||||||
|
# The on/off choice is stored at runtime in
|
||||||
|
# ~/.cache/quickshell/autostart.json - adding an app here just makes it
|
||||||
|
# available as an (off by default) entry next time the panel loads.
|
||||||
|
home-manager.users.petere.services.quickshell-apps = {
|
||||||
|
enable = true;
|
||||||
|
apps = [
|
||||||
|
{
|
||||||
|
name = "Element";
|
||||||
|
cmd = "element-desktop";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "Nextcloud";
|
||||||
|
cmd = "nextcloud";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "Bitwarden";
|
||||||
|
cmd = "bitwarden";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
networking.hostName = "hp-laptop";
|
||||||
|
|
||||||
|
# Open legacy game-streaming ports.
|
||||||
|
networking.firewall = {
|
||||||
|
enable = true;
|
||||||
|
allowedTCPPorts = [ 9756 ];
|
||||||
|
allowedUDPPorts = [ 9999 ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# TeleportFling: standalone screen + audio sender for OBS Teleport.
|
||||||
|
environment.systemPackages = [
|
||||||
|
inputs.teleportfling.packages.${pkgs.system}.teleportfling-gui
|
||||||
|
inputs.teleportfling.packages.${pkgs.system}.teleportfling
|
||||||
|
];
|
||||||
|
|
||||||
|
# Laptop-specific hardware support (fingerprint reader, fwupd)
|
||||||
|
my.hardware.laptop.enable = true;
|
||||||
|
|
||||||
|
# Charging cap at 80% (protect battery longevity). Board 81AD firmware lacks
|
||||||
|
# a percentage threshold sysfs, so this runs SBCC/SBCO via acpi_call: a root
|
||||||
|
# poller inhibits charge at 80% and re-enables auto below 75%. Toggleable
|
||||||
|
# from the QuickShell gear panel (CHARGE LIMIT).
|
||||||
|
my.hardware.hpBatteryLimit.enable = true;
|
||||||
|
|
||||||
|
# Intel integrated GPU (HP consumer laptops)
|
||||||
|
hardware.graphics.enable = true;
|
||||||
|
|
||||||
|
my.users.petere = {
|
||||||
|
description = "Peter Edley";
|
||||||
|
hashedPasswordFile = config.sops.secrets."users/petere-password".path;
|
||||||
|
subUidStart = 165536;
|
||||||
|
subGidStart = 165536;
|
||||||
|
};
|
||||||
|
|
||||||
|
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# /hosts/hp-laptop/disko-config.nix
|
||||||
|
# Two-disk layout:
|
||||||
|
# /dev/sda -> boot + ESP (/boot) + swap + root (/)
|
||||||
|
# /dev/sdb -> home (/home)
|
||||||
|
{
|
||||||
|
disk = {
|
||||||
|
root = {
|
||||||
|
type = "disk";
|
||||||
|
device = "/dev/sda";
|
||||||
|
content = {
|
||||||
|
type = "gpt";
|
||||||
|
partitions = {
|
||||||
|
boot = {
|
||||||
|
size = "1M";
|
||||||
|
type = "EF02";
|
||||||
|
};
|
||||||
|
ESP = {
|
||||||
|
size = "512M";
|
||||||
|
type = "EF00";
|
||||||
|
content = {
|
||||||
|
type = "filesystem";
|
||||||
|
format = "vfat";
|
||||||
|
mountpoint = "/boot";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
swap = {
|
||||||
|
size = "16G";
|
||||||
|
type = "8200";
|
||||||
|
content = {
|
||||||
|
type = "swap";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
root = {
|
||||||
|
size = "100%";
|
||||||
|
content = {
|
||||||
|
type = "filesystem";
|
||||||
|
format = "ext4";
|
||||||
|
mountpoint = "/";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
home = {
|
||||||
|
type = "disk";
|
||||||
|
device = "/dev/sdb";
|
||||||
|
content = {
|
||||||
|
type = "gpt";
|
||||||
|
partitions = {
|
||||||
|
home = {
|
||||||
|
size = "100%";
|
||||||
|
content = {
|
||||||
|
type = "filesystem";
|
||||||
|
format = "ext4";
|
||||||
|
mountpoint = "/home";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# hardware-configuration.nix for hp-laptop
|
||||||
|
# Generated placeholder — run `sudo nixos-generate-config --show-hardware-config` on the
|
||||||
|
# target machine after the initial (disko / nixos-anywhere) install and replace this file.
|
||||||
|
# NOTE: Do NOT copy the output of `nixos-generate-config` run from the installer ISO — it
|
||||||
|
# reflects the live environment (tmpfs /, /iso, squashfs overlay), not the installed system.
|
||||||
|
# NOTE: `fileSystems` and `swapDevices` are intentionally omitted here — they are
|
||||||
|
# provided by the disko module (hosts/hp-laptop/disko-config.nix).
|
||||||
|
# Disk layout (see disko-config.nix): /dev/sda = boot + ESP + swap + root (/);
|
||||||
|
# /dev/sdb = home (/home).
|
||||||
|
{ config
|
||||||
|
, lib
|
||||||
|
, pkgs
|
||||||
|
, modulesPath
|
||||||
|
, ...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
(modulesPath + "/installer/scan/not-detected.nix")
|
||||||
|
];
|
||||||
|
|
||||||
|
boot.initrd.availableKernelModules = [
|
||||||
|
"xhci_pci"
|
||||||
|
"nvme"
|
||||||
|
"usb_storage"
|
||||||
|
"sd_mod"
|
||||||
|
];
|
||||||
|
boot.initrd.kernelModules = [ ];
|
||||||
|
boot.kernelModules = [ "kvm-intel" ];
|
||||||
|
boot.extraModulePackages = [ ];
|
||||||
|
|
||||||
|
networking.useDHCP = lib.mkDefault true;
|
||||||
|
# networking.interfaces.wlp2s0.useDHCP = lib.mkDefault true;
|
||||||
|
|
||||||
|
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
|
||||||
|
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# /hosts/mary-x270/configuration.nix
|
||||||
|
{ config
|
||||||
|
, pkgs
|
||||||
|
, inputs
|
||||||
|
, lib
|
||||||
|
, ...
|
||||||
|
}:
|
||||||
|
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
./hardware-configuration.nix
|
||||||
|
../../modules/core/common.nix
|
||||||
|
../../modules/desktop/gui.nix
|
||||||
|
(import ../../modules/storage/disko.nix {
|
||||||
|
inherit inputs lib config;
|
||||||
|
diskoConfigPath = ./disko-config.nix;
|
||||||
|
})
|
||||||
|
../../modules/desktop/gnome.nix
|
||||||
|
../../modules/core/management.nix
|
||||||
|
../../modules/core/dev.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
sops.secrets = {
|
||||||
|
"users/petere-password" = {
|
||||||
|
neededForUsers = true;
|
||||||
|
};
|
||||||
|
"users/mary-password" = {
|
||||||
|
neededForUsers = true;
|
||||||
|
};
|
||||||
|
"opencode-api-key" = {
|
||||||
|
owner = "petere";
|
||||||
|
group = "users";
|
||||||
|
mode = "0440";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
home-manager.users.mary.imports = [
|
||||||
|
../../home-manager/modules/desktop-user.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
home-manager.users.petere.imports = [
|
||||||
|
../../home-manager/modules/desktop-user.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
networking.hostName = "mary-x270";
|
||||||
|
|
||||||
|
users.users.mary = {
|
||||||
|
isNormalUser = true;
|
||||||
|
description = "Mary";
|
||||||
|
extraGroups = [
|
||||||
|
"wheel"
|
||||||
|
"networkmanager"
|
||||||
|
"video"
|
||||||
|
"audio"
|
||||||
|
];
|
||||||
|
hashedPasswordFile = config.sops.secrets."users/mary-password".path;
|
||||||
|
subUidRanges = [
|
||||||
|
{
|
||||||
|
startUid = 200000;
|
||||||
|
count = 65536;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
subGidRanges = [
|
||||||
|
{
|
||||||
|
startGid = 200000;
|
||||||
|
count = 65536;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
my.users.petere = {
|
||||||
|
description = "Peter Edley";
|
||||||
|
hashedPasswordFile = config.sops.secrets."users/petere-password".path;
|
||||||
|
subUidStart = 165536;
|
||||||
|
subGidStart = 165536;
|
||||||
|
};
|
||||||
|
|
||||||
|
hardware.graphics.enable = true;
|
||||||
|
|
||||||
|
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# /hosts/mary-x270/disko-config.nix
|
||||||
|
{
|
||||||
|
disk = {
|
||||||
|
nixos = {
|
||||||
|
type = "disk";
|
||||||
|
device = "/dev/sda";
|
||||||
|
content = {
|
||||||
|
type = "gpt";
|
||||||
|
partitions = {
|
||||||
|
boot = {
|
||||||
|
size = "1M";
|
||||||
|
type = "EF02";
|
||||||
|
};
|
||||||
|
ESP = {
|
||||||
|
size = "512M";
|
||||||
|
type = "EF00";
|
||||||
|
content = {
|
||||||
|
type = "filesystem";
|
||||||
|
format = "vfat";
|
||||||
|
mountpoint = "/boot";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
swap = {
|
||||||
|
size = "16G";
|
||||||
|
type = "8200";
|
||||||
|
content = {
|
||||||
|
type = "swap";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
root = {
|
||||||
|
size = "100%";
|
||||||
|
content = {
|
||||||
|
type = "filesystem";
|
||||||
|
format = "ext4";
|
||||||
|
mountpoint = "/";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# /hosts/mary-x270/hardware-configuration.nix
|
||||||
|
{ config
|
||||||
|
, lib
|
||||||
|
, pkgs
|
||||||
|
, modulesPath
|
||||||
|
, ...
|
||||||
|
}:
|
||||||
|
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
(modulesPath + "/installer/scan/not-detected.nix")
|
||||||
|
];
|
||||||
|
|
||||||
|
boot.initrd.availableKernelModules = [
|
||||||
|
"xhci_pci"
|
||||||
|
"nvme"
|
||||||
|
"usb_storage"
|
||||||
|
"sd_mod"
|
||||||
|
];
|
||||||
|
boot.initrd.kernelModules = [ ];
|
||||||
|
boot.kernelModules = [ "kvm-intel" ];
|
||||||
|
boot.extraModulePackages = [ ];
|
||||||
|
|
||||||
|
networking.useDHCP = lib.mkDefault true;
|
||||||
|
|
||||||
|
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
|
||||||
|
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# NixOS configuration for mcf-server
|
||||||
|
# Headless server environment (Command Line Interface only)
|
||||||
|
|
||||||
|
{ config
|
||||||
|
, pkgs
|
||||||
|
, lib
|
||||||
|
, inputs
|
||||||
|
, ...
|
||||||
|
}:
|
||||||
|
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
../../modules/core/common.nix
|
||||||
|
(import ../../modules/storage/disko.nix {
|
||||||
|
inherit inputs lib config;
|
||||||
|
diskoConfigPath = ./disko-config.nix;
|
||||||
|
})
|
||||||
|
./hardware-configuration.nix
|
||||||
|
../../modules/core/management.nix
|
||||||
|
../../modules/core/podman.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
networking.hostName = "mcf-server";
|
||||||
|
|
||||||
|
# Create restic user for homeserver-1 backups
|
||||||
|
users.users.restic-homeserver1 = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = "restic-homeserver1";
|
||||||
|
shell = pkgs.zsh;
|
||||||
|
openssh.authorizedKeys.keys = [
|
||||||
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICwGZFEr6OMm7SIPrYlt6wuuesvlmBIezqQVDxhXOHjD restic@homeserver-1"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
users.groups.restic-homeserver1 = { };
|
||||||
|
|
||||||
|
# Standard user account (shared definition in modules/core/users.nix).
|
||||||
|
# SSH-key-only access; no password on this host.
|
||||||
|
|
||||||
|
# SSH configuration
|
||||||
|
services.openssh.enable = true;
|
||||||
|
|
||||||
|
# Glances system monitor - exposed to the tailnet so homeserver-1's
|
||||||
|
# Homepage dashboard can display real-time stats for this machine.
|
||||||
|
services.glances = {
|
||||||
|
enable = true;
|
||||||
|
port = 61208;
|
||||||
|
# Webserver mode (default). Binds to 0.0.0.0; the firewall rule below
|
||||||
|
# restricts access to the Tailscale interface only.
|
||||||
|
extraArgs = [ "--webserver" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Expose Glances (61208) on Tailscale only
|
||||||
|
networking.firewall.interfaces.tailscale.allowedTCPPorts = [ 61208 ];
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
disk = {
|
||||||
|
main = {
|
||||||
|
type = "disk";
|
||||||
|
device = "/dev/sdb";
|
||||||
|
content = {
|
||||||
|
type = "gpt";
|
||||||
|
partitions = {
|
||||||
|
boot = {
|
||||||
|
size = "1M";
|
||||||
|
type = "EF02"; # for grub MBR
|
||||||
|
};
|
||||||
|
ESP = {
|
||||||
|
size = "512M";
|
||||||
|
type = "EF00";
|
||||||
|
content = {
|
||||||
|
type = "filesystem";
|
||||||
|
format = "vfat";
|
||||||
|
mountpoint = "/boot";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
root = {
|
||||||
|
size = "100%";
|
||||||
|
content = {
|
||||||
|
type = "filesystem";
|
||||||
|
format = "ext4";
|
||||||
|
mountpoint = "/";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
data = {
|
||||||
|
type = "disk";
|
||||||
|
device = "/dev/sda";
|
||||||
|
content = {
|
||||||
|
type = "gpt";
|
||||||
|
partitions = {
|
||||||
|
data = {
|
||||||
|
size = "100%";
|
||||||
|
content = {
|
||||||
|
type = "filesystem";
|
||||||
|
format = "btrfs";
|
||||||
|
mountpoint = "/data";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Hardware configuration for mcf-server
|
||||||
|
# Generated by nixos-generate-config; live-ISO filesystem entries removed (managed by Disko)
|
||||||
|
|
||||||
|
{ config
|
||||||
|
, lib
|
||||||
|
, pkgs
|
||||||
|
, modulesPath
|
||||||
|
, ...
|
||||||
|
}:
|
||||||
|
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
(modulesPath + "/installer/scan/not-detected.nix")
|
||||||
|
];
|
||||||
|
|
||||||
|
boot.initrd.availableKernelModules = [
|
||||||
|
"xhci_pci"
|
||||||
|
"ahci"
|
||||||
|
"usbhid"
|
||||||
|
"usb_storage"
|
||||||
|
"sd_mod"
|
||||||
|
"sr_mod"
|
||||||
|
];
|
||||||
|
boot.initrd.kernelModules = [ ];
|
||||||
|
boot.kernelModules = [ "kvm-intel" ];
|
||||||
|
boot.extraModulePackages = [ ];
|
||||||
|
|
||||||
|
# fileSystems are managed by Disko — update disko-config.nix for disk layout
|
||||||
|
|
||||||
|
swapDevices = [ ];
|
||||||
|
|
||||||
|
networking.useDHCP = lib.mkDefault true;
|
||||||
|
|
||||||
|
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
|
||||||
|
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# /hosts/mcf-stream/configuration.nix
|
||||||
|
{ config
|
||||||
|
, pkgs
|
||||||
|
, inputs
|
||||||
|
, lib
|
||||||
|
, ...
|
||||||
|
}:
|
||||||
|
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
./hardware-configuration.nix
|
||||||
|
../../modules/core/common.nix
|
||||||
|
../../modules/desktop/gui.nix
|
||||||
|
(import ../../modules/storage/disko.nix {
|
||||||
|
inherit inputs lib config;
|
||||||
|
diskoConfigPath = ./disko-config.nix;
|
||||||
|
})
|
||||||
|
../../modules/desktop/gnome.nix
|
||||||
|
../../modules/core/management.nix
|
||||||
|
../../modules/hardware/nvidia.nix
|
||||||
|
../../modules/desktop/apps/obs.nix
|
||||||
|
../../modules/desktop/apps/dvd.nix
|
||||||
|
../../modules/desktop/apps/carla.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
sops.secrets = {
|
||||||
|
"users/petere-password" = {
|
||||||
|
neededForUsers = true;
|
||||||
|
};
|
||||||
|
"users/guest-password" = {
|
||||||
|
neededForUsers = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Auto-login as guest for this shared/streaming desktop
|
||||||
|
services.displayManager.autoLogin = {
|
||||||
|
enable = true;
|
||||||
|
user = "guest";
|
||||||
|
};
|
||||||
|
|
||||||
|
home-manager.users.petere.imports = [
|
||||||
|
../../home-manager/modules/desktop-user.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
home-manager.users.guest.imports = [
|
||||||
|
../../home-manager/modules/gnome-extensions.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
networking.hostName = "mcf-stream";
|
||||||
|
|
||||||
|
my.hardware.nvidia = {
|
||||||
|
enable = true;
|
||||||
|
nvidiaSettings = true;
|
||||||
|
# GTX 1050 Ti (Pascal) needs the 580.xx legacy driver branch
|
||||||
|
package = config.boot.kernelPackages.nvidiaPackages.legacy_580;
|
||||||
|
};
|
||||||
|
|
||||||
|
my.users.petere = {
|
||||||
|
description = "Peter Edley";
|
||||||
|
hashedPasswordFile = config.sops.secrets."users/petere-password".path;
|
||||||
|
};
|
||||||
|
|
||||||
|
users.users.guest = {
|
||||||
|
isNormalUser = true;
|
||||||
|
description = "Guest";
|
||||||
|
hashedPasswordFile = config.sops.secrets."users/guest-password".path;
|
||||||
|
extraGroups = [
|
||||||
|
"networkmanager"
|
||||||
|
"video"
|
||||||
|
"audio"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# ffmpeg with NVENC encoders (h264/hevc/av1_nvenc) for GPU-accelerated
|
||||||
|
# encoding on the NVIDIA GPU
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
ffmpeg
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# /hosts/mcf-stream/disko-config.nix
|
||||||
|
# This file will contain your disko configuration for mcf-stream.
|
||||||
|
{
|
||||||
|
disk = {
|
||||||
|
nixos = {
|
||||||
|
type = "disk";
|
||||||
|
device = "/dev/nvme0n1";
|
||||||
|
content = {
|
||||||
|
type = "gpt";
|
||||||
|
partitions = {
|
||||||
|
boot = {
|
||||||
|
size = "1M";
|
||||||
|
type = "EF02"; # for grub MBR
|
||||||
|
};
|
||||||
|
ESP = {
|
||||||
|
size = "512M";
|
||||||
|
type = "EF00";
|
||||||
|
content = {
|
||||||
|
type = "filesystem";
|
||||||
|
format = "vfat";
|
||||||
|
mountpoint = "/boot";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
swap = {
|
||||||
|
size = "20G";
|
||||||
|
type = "8200";
|
||||||
|
content = {
|
||||||
|
type = "swap";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
root = {
|
||||||
|
size = "100%";
|
||||||
|
content = {
|
||||||
|
type = "filesystem";
|
||||||
|
format = "ext4";
|
||||||
|
mountpoint = "/";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Hardware configuration for mcf-stream
|
||||||
|
# Generated for a single NVMe drive (/dev/nvme0n1)
|
||||||
|
{ config
|
||||||
|
, lib
|
||||||
|
, pkgs
|
||||||
|
, modulesPath
|
||||||
|
, ...
|
||||||
|
}:
|
||||||
|
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
(modulesPath + "/installer/scan/not-detected.nix")
|
||||||
|
];
|
||||||
|
|
||||||
|
boot.initrd.availableKernelModules = [
|
||||||
|
"xhci_pci"
|
||||||
|
"ahci"
|
||||||
|
"nvme"
|
||||||
|
"usb_storage"
|
||||||
|
"usbhid"
|
||||||
|
"sd_mod"
|
||||||
|
];
|
||||||
|
boot.initrd.kernelModules = [ ];
|
||||||
|
boot.kernelModules = [ "kvm-intel" ];
|
||||||
|
boot.extraModulePackages = [ ];
|
||||||
|
|
||||||
|
# fileSystems are managed by Disko — update disko-config.nix for disk layout
|
||||||
|
|
||||||
|
swapDevices = [ ];
|
||||||
|
|
||||||
|
networking.useDHCP = lib.mkDefault true;
|
||||||
|
|
||||||
|
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
|
||||||
|
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
|
||||||
|
}
|
||||||
@@ -0,0 +1,374 @@
|
|||||||
|
# This is the NixOS configuration for richmond-server.
|
||||||
|
# It is a server and does not require a GUI.
|
||||||
|
|
||||||
|
{ config
|
||||||
|
, pkgs
|
||||||
|
, lib
|
||||||
|
, inputs
|
||||||
|
, # Re-add inputs here
|
||||||
|
... # specialArgs from flake.nix
|
||||||
|
}:
|
||||||
|
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
# Import your common modules here
|
||||||
|
../../modules/core/common.nix
|
||||||
|
../../modules/core/management.nix
|
||||||
|
# ../../modules/some-common-module.nix
|
||||||
|
(import ../../modules/storage/disko.nix {
|
||||||
|
inherit inputs lib config;
|
||||||
|
diskoConfigPath = ./disko-config.nix;
|
||||||
|
})
|
||||||
|
./hardware-configuration.nix # Import hardware configuration
|
||||||
|
../../modules/core/podman.nix
|
||||||
|
../../modules/services/ntfy.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
sops.secrets = {
|
||||||
|
"richmond-server/tailscale-authkey" = {
|
||||||
|
mode = "0600";
|
||||||
|
owner = "root";
|
||||||
|
};
|
||||||
|
"richmond-server/pihole-password" = { };
|
||||||
|
"richmond-server/castopod-api-password" = {
|
||||||
|
key = "richmond-server/castopod-api-password";
|
||||||
|
};
|
||||||
|
"richmond-server/mcf-notices-env" = { };
|
||||||
|
"richmond-server/castopod-env" = { };
|
||||||
|
"richmond-server/castopod-api-env" = { };
|
||||||
|
"users/petere-password" = {
|
||||||
|
neededForUsers = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Set your hostname
|
||||||
|
networking.hostName = "richmond-server";
|
||||||
|
|
||||||
|
# Set explicit nameservers for the host to ensure it can reach registries
|
||||||
|
# regardless of local container or Tailscale DNS state.
|
||||||
|
networking.nameservers = [
|
||||||
|
"1.1.1.1"
|
||||||
|
"8.8.8.8"
|
||||||
|
];
|
||||||
|
|
||||||
|
# Allow rootless containers to bind to privileged ports
|
||||||
|
boot.kernel.sysctl = {
|
||||||
|
"net.ipv4.ip_unprivileged_port_start" = 53;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Enable SSH
|
||||||
|
services.openssh.enable = true;
|
||||||
|
|
||||||
|
# Backup Server Configuration
|
||||||
|
# Borg Backup removed - x1carbon migrated to Backrest on homeserver-1
|
||||||
|
# Richmond-server now serves as backup target for Backrest (homeserver-1) only
|
||||||
|
|
||||||
|
# Create a dedicated system user for Backrest backups from homeserver-1
|
||||||
|
users.users.restic-homeserver1 = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = "restic-homeserver1";
|
||||||
|
shell = pkgs.zsh;
|
||||||
|
openssh.authorizedKeys.keys = [
|
||||||
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICwGZFEr6OMm7SIPrYlt6wuuesvlmBIezqQVDxhXOHjD restic@homeserver-1"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Admin user (shared definition in modules/core/users.nix)
|
||||||
|
my.users.petere = {
|
||||||
|
hashedPasswordFile = config.sops.secrets."users/petere-password".path;
|
||||||
|
};
|
||||||
|
|
||||||
|
users.groups = {
|
||||||
|
restic-homeserver1 = { };
|
||||||
|
};
|
||||||
|
|
||||||
|
# Disable systemd-resolved to prevent it from binding to port 53,
|
||||||
|
# allowing Pi-hole to take over DNS duties.
|
||||||
|
services.resolved.enable = false;
|
||||||
|
# The 'services.resolved.extraConfig' option is deprecated and now causes a build failure.
|
||||||
|
# The 'services.resolved.enable = false;' line above is sufficient to free up port 53 for Pi-hole.
|
||||||
|
# services.resolved.extraConfig = ''
|
||||||
|
# DNSStubListener=no
|
||||||
|
# '';
|
||||||
|
|
||||||
|
# Enable Tailscale (globally enabled in modules/core/settings.nix)
|
||||||
|
services.tailscale.authKeyFile = config.sops.secrets."richmond-server/tailscale-authkey".path;
|
||||||
|
# NOTE: no "--ssh" here. Tailscale SSH would intercept port 22 on the tailnet
|
||||||
|
# and enforce the tailnet ACL, blocking the restic-homeserver1 user that
|
||||||
|
# Backrest (homeserver-1) uses for backups. Regular OpenSSH handles SSH instead.
|
||||||
|
services.tailscale.extraUpFlags = [
|
||||||
|
"--accept-dns=false"
|
||||||
|
];
|
||||||
|
|
||||||
|
# Glances system monitor - exposed to the tailnet so the Homepage
|
||||||
|
# dashboard on homeserver-1 can display real-time stats for this machine.
|
||||||
|
services.glances = {
|
||||||
|
enable = true;
|
||||||
|
port = 61208;
|
||||||
|
extraArgs = [ "--webserver" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# VLAN configuration
|
||||||
|
networking.vlans = {
|
||||||
|
management = {
|
||||||
|
id = 5;
|
||||||
|
interface = "enp1s0";
|
||||||
|
};
|
||||||
|
office = {
|
||||||
|
id = 10;
|
||||||
|
interface = "enp1s0";
|
||||||
|
};
|
||||||
|
tech = {
|
||||||
|
id = 20;
|
||||||
|
interface = "enp1s0";
|
||||||
|
};
|
||||||
|
advice = {
|
||||||
|
id = 30;
|
||||||
|
interface = "enp1s0";
|
||||||
|
};
|
||||||
|
words = {
|
||||||
|
id = 40;
|
||||||
|
interface = "enp1s0";
|
||||||
|
};
|
||||||
|
general = {
|
||||||
|
id = 50;
|
||||||
|
interface = "enp1s0";
|
||||||
|
};
|
||||||
|
printers = {
|
||||||
|
id = 60;
|
||||||
|
interface = "enp1s0";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
networking.interfaces = {
|
||||||
|
management.useDHCP = true;
|
||||||
|
office.useDHCP = true;
|
||||||
|
tech.useDHCP = true;
|
||||||
|
advice.useDHCP = true;
|
||||||
|
words.useDHCP = true;
|
||||||
|
general.useDHCP = true;
|
||||||
|
printers.useDHCP = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
# podman configuration is now in ../../modules/core/podman.nix
|
||||||
|
|
||||||
|
# --- Pi-hole Container Configuration ---
|
||||||
|
virtualisation.oci-containers.containers.pihole = {
|
||||||
|
image = "docker.io/pihole/pihole:2026.07.2";
|
||||||
|
autoStart = true;
|
||||||
|
|
||||||
|
ports = [
|
||||||
|
"53:53/tcp"
|
||||||
|
"53:53/udp"
|
||||||
|
"80:80/tcp" # Web UI on port 80
|
||||||
|
];
|
||||||
|
|
||||||
|
volumes = [
|
||||||
|
"/var/lib/pihole/etc-pihole:/etc/pihole"
|
||||||
|
"/var/lib/pihole/etc-dnsmasq.d:/etc/dnsmasq.d"
|
||||||
|
];
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
TZ = "Europe/London";
|
||||||
|
DNSMASQ_LISTENING = "all";
|
||||||
|
PIHOLE_INTERFACE = "all";
|
||||||
|
};
|
||||||
|
|
||||||
|
environmentFiles = [
|
||||||
|
"/run/pihole-env"
|
||||||
|
];
|
||||||
|
|
||||||
|
extraOptions = [
|
||||||
|
"--cap-add=NET_ADMIN" # Needed for DHCP features
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# --- MCFNotices Container Configuration ---
|
||||||
|
virtualisation.oci-containers.containers.mcf-notices = {
|
||||||
|
image = "docker.io/pedley/slideshow-builder:2.6.0";
|
||||||
|
autoStart = true;
|
||||||
|
volumes = [
|
||||||
|
# Persistent runtime config (notices-data) — app stores settings.db at
|
||||||
|
# /data/settings.db inside the container.
|
||||||
|
"/home/petere/mcf_data:/data"
|
||||||
|
];
|
||||||
|
environmentFiles = [
|
||||||
|
config.sops.secrets."richmond-server/mcf-notices-env".path
|
||||||
|
];
|
||||||
|
extraOptions = [
|
||||||
|
"--no-healthcheck"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Improve container service resilience and ensure network is ready before starting.
|
||||||
|
systemd.services.podman-pihole = {
|
||||||
|
after = [
|
||||||
|
"network-online.target"
|
||||||
|
"pihole-data-dirs.service"
|
||||||
|
];
|
||||||
|
wants = [ "network-online.target" ];
|
||||||
|
preStart = ''
|
||||||
|
${pkgs.podman}/bin/podman rm -f pihole || true
|
||||||
|
'';
|
||||||
|
unitConfig = {
|
||||||
|
StartLimitIntervalSec = 0;
|
||||||
|
};
|
||||||
|
serviceConfig = {
|
||||||
|
Restart = lib.mkForce "always";
|
||||||
|
RestartSec = "10s";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Rendered by sops-nix from the pihole-password secret; the Pi-hole container
|
||||||
|
# reads this file for FTLCONF_webserver_api_password.
|
||||||
|
sops.templates."pihole-env" = {
|
||||||
|
content = ''
|
||||||
|
FTLCONF_webserver_api_password=${config.sops.placeholder."richmond-server/pihole-password"}
|
||||||
|
'';
|
||||||
|
path = "/run/pihole-env";
|
||||||
|
mode = "0600";
|
||||||
|
};
|
||||||
|
|
||||||
|
services.mysql = {
|
||||||
|
enable = true;
|
||||||
|
package = pkgs.mariadb;
|
||||||
|
settings = {
|
||||||
|
mysqld = {
|
||||||
|
bind-address = "0.0.0.0"; # Allow container access
|
||||||
|
character-set-server = "utf8mb4";
|
||||||
|
collation-server = "utf8mb4_unicode_ci";
|
||||||
|
};
|
||||||
|
client = {
|
||||||
|
default-character-set = "utf8mb4";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# --- Castopod Container Configuration ---
|
||||||
|
virtualisation.oci-containers.containers.castopod = {
|
||||||
|
image = "castopod/castopod:1.15.5";
|
||||||
|
autoStart = true;
|
||||||
|
volumes = [
|
||||||
|
"/var/lib/castopod/media:/var/www/html/public/media"
|
||||||
|
"/var/lib/castopod/writable:/var/www/html/writable"
|
||||||
|
"/var/lib/castopod/plugins:/var/www/html/plugins"
|
||||||
|
"/var/lib/castopod/plugins/mcf/episode-filter:/var/www/html/plugins/mcf/episode-filter"
|
||||||
|
"/var/lib/castopod/public/plugins/mcf/episode-filter/assets:/var/www/html/public/plugins/mcf/episode-filter/assets"
|
||||||
|
];
|
||||||
|
environmentFiles = [
|
||||||
|
config.sops.secrets."richmond-server/castopod-env".path
|
||||||
|
"/run/castopod-api-env"
|
||||||
|
];
|
||||||
|
extraOptions = [
|
||||||
|
"--network=host"
|
||||||
|
];
|
||||||
|
# We use a "configuration injection" trick here via the /run/castopod-api-env file.
|
||||||
|
};
|
||||||
|
|
||||||
|
# Improve Castopod container resilience and ensure directories exist
|
||||||
|
systemd.services.podman-castopod = {
|
||||||
|
after = [
|
||||||
|
"network-online.target"
|
||||||
|
"castopod-data-dirs.service"
|
||||||
|
];
|
||||||
|
wants = [ "network-online.target" ];
|
||||||
|
unitConfig = {
|
||||||
|
StartLimitIntervalSec = 0;
|
||||||
|
};
|
||||||
|
serviceConfig = {
|
||||||
|
Restart = lib.mkForce "always";
|
||||||
|
RestartSec = "10s";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# NOTE: Castopod is an experiment. This env file is a "configuration injection"
|
||||||
|
# used to pass restapi settings; the original preStart wrote a single line with
|
||||||
|
# literal \n sequences. This template writes real newlines instead — review
|
||||||
|
# Castopod's behaviour on the next pass.
|
||||||
|
sops.templates."castopod-api-env" = {
|
||||||
|
content = ''
|
||||||
|
CP_DATABASE_PREFIX=cp_"
|
||||||
|
restapi.enabled=true
|
||||||
|
restapi.basicAuth=true
|
||||||
|
restapi.basicAuthUsername=pedley
|
||||||
|
restapi.basicAuthPassword=${config.sops.placeholder."richmond-server/castopod-api-password"}
|
||||||
|
dummy="
|
||||||
|
'';
|
||||||
|
path = "/run/castopod-api-env";
|
||||||
|
mode = "0600";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Create persistent data directories for Castopod.
|
||||||
|
systemd.services.castopod-data-dirs = {
|
||||||
|
description = "Create data directories for Castopod container";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
before = [ "podman-castopod.service" ];
|
||||||
|
serviceConfig.Type = "oneshot";
|
||||||
|
script = ''
|
||||||
|
mkdir -p /var/lib/castopod/media
|
||||||
|
mkdir -p /var/lib/castopod/writable
|
||||||
|
mkdir -p /var/lib/castopod/plugins
|
||||||
|
mkdir -p /var/lib/castopod/public/plugins/mcf/episode-filter/assets
|
||||||
|
# Ensure the container's www-data user (UID 33) can write to these
|
||||||
|
chown -R 33:33 /var/lib/castopod/media /var/lib/castopod/writable /var/lib/castopod/plugins /var/lib/castopod/public
|
||||||
|
chmod -R 775 /var/lib/castopod/media /var/lib/castopod/writable /var/lib/castopod/plugins /var/lib/castopod/public
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# --- ntfy Configuration ---
|
||||||
|
services.ntfy-container = {
|
||||||
|
enable = true;
|
||||||
|
port = 8085;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Open firewall ports for Pi-hole and ntfy globally.
|
||||||
|
networking.firewall.enable = true;
|
||||||
|
networking.firewall.allowedTCPPorts = [
|
||||||
|
53
|
||||||
|
80
|
||||||
|
8085
|
||||||
|
];
|
||||||
|
networking.firewall.allowedUDPPorts = [ 53 ];
|
||||||
|
|
||||||
|
# Restrict Castopod (8080) to only be accessible via Tailscale
|
||||||
|
networking.firewall.interfaces.tailscale0.allowedTCPPorts = [
|
||||||
|
8080
|
||||||
|
61208
|
||||||
|
];
|
||||||
|
|
||||||
|
# Create persistent data directories for Pi-hole.
|
||||||
|
systemd.services.pihole-data-dirs = {
|
||||||
|
description = "Create data directories for Pi-hole container";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
before = [ "podman-pihole.service" ];
|
||||||
|
serviceConfig.Type = "oneshot";
|
||||||
|
script = ''
|
||||||
|
mkdir -p /var/lib/pihole/etc-pihole
|
||||||
|
mkdir -p /var/lib/pihole/etc-dnsmasq.d
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# Create the persistent data directory for the MCFNotices container.
|
||||||
|
systemd.services.mcf-notices-data-dirs = {
|
||||||
|
description = "Create data directory for MCFNotices container";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
before = [ "podman-mcf-notices.service" ];
|
||||||
|
serviceConfig.Type = "oneshot";
|
||||||
|
script = ''
|
||||||
|
mkdir -p /home/petere/mcf_data
|
||||||
|
chown petere:users /home/petere/mcf_data
|
||||||
|
chmod 775 /home/petere/mcf_data
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# Ensure the base directory for backups exists and has correct permissions
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
# Borg backup directories removed (migrated to Backrest)
|
||||||
|
"d /home/backup/restic/immich-backup 0700 restic-homeserver1 restic-homeserver1 -"
|
||||||
|
];
|
||||||
|
|
||||||
|
nix.settings.trusted-users = [
|
||||||
|
"root"
|
||||||
|
"petere"
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
disk = {
|
||||||
|
main = {
|
||||||
|
type = "disk";
|
||||||
|
device = "/dev/sdb";
|
||||||
|
content = {
|
||||||
|
type = "gpt";
|
||||||
|
partitions = {
|
||||||
|
boot = {
|
||||||
|
size = "1M";
|
||||||
|
type = "EF02"; # for grub MBR
|
||||||
|
};
|
||||||
|
ESP = {
|
||||||
|
size = "512M";
|
||||||
|
type = "EF00";
|
||||||
|
content = {
|
||||||
|
type = "filesystem";
|
||||||
|
format = "vfat";
|
||||||
|
mountpoint = "/boot";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
root = {
|
||||||
|
size = "100%";
|
||||||
|
content = {
|
||||||
|
type = "filesystem";
|
||||||
|
format = "ext4";
|
||||||
|
mountpoint = "/";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
home = {
|
||||||
|
type = "disk";
|
||||||
|
device = "/dev/sda";
|
||||||
|
content = {
|
||||||
|
type = "gpt";
|
||||||
|
partitions = {
|
||||||
|
home = {
|
||||||
|
size = "100%";
|
||||||
|
content = {
|
||||||
|
type = "filesystem";
|
||||||
|
format = "btrfs";
|
||||||
|
mountpoint = "/home";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# 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"
|
||||||
|
"usb_storage"
|
||||||
|
"usbhid"
|
||||||
|
"sd_mod"
|
||||||
|
"sr_mod"
|
||||||
|
"rtsx_usb_sdmmc"
|
||||||
|
];
|
||||||
|
boot.initrd.kernelModules = [ ];
|
||||||
|
boot.kernelModules = [ "kvm-intel" ];
|
||||||
|
boot.extraModulePackages = [ ];
|
||||||
|
|
||||||
|
# fileSystems."/" =
|
||||||
|
# { device = "/dev/disk/by-uuid/e0430ebf-8a47-44cb-aac7-77639f2184b1";
|
||||||
|
# fsType = "ext4";
|
||||||
|
# };
|
||||||
|
|
||||||
|
# fileSystems."/boot" =
|
||||||
|
# { device = "/dev/disk/by-uuid/63DD-EA06";
|
||||||
|
# fsType = "vfat";
|
||||||
|
# options = [ "fmask=0077" "dmask=0077" ];
|
||||||
|
# };
|
||||||
|
|
||||||
|
# fileSystems."/home" =
|
||||||
|
# { device = "/dev/disk/by-uuid/f60ae16e-2580-452c-a9bc-f23d9a7446bc";
|
||||||
|
# fsType = "btrfs";
|
||||||
|
# };
|
||||||
|
|
||||||
|
swapDevices = [ ];
|
||||||
|
|
||||||
|
# Enables DHCP on each ethernet and wireless interface. In case of scripted networking
|
||||||
|
# (the default) this is the recommended approach. When using systemd-networkd it's
|
||||||
|
# still possible to use this option, but it's recommended to use it in conjunction
|
||||||
|
# with explicit per-interface declarations with `networking.interfaces.<interface>.useDHCP`.
|
||||||
|
networking.useDHCP = lib.mkDefault true;
|
||||||
|
# networking.interfaces.enp1s0.useDHCP = lib.mkDefault true;
|
||||||
|
# networking.interfaces.wlp2s0.useDHCP = lib.mkDefault true;
|
||||||
|
|
||||||
|
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
|
||||||
|
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
# /hosts/nixos/configuration.nix
|
||||||
|
{ config
|
||||||
|
, pkgs
|
||||||
|
, inputs
|
||||||
|
, lib
|
||||||
|
, ...
|
||||||
|
}:
|
||||||
|
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
./hardware-configuration.nix
|
||||||
|
../../modules/core/common.nix
|
||||||
|
../../modules/desktop/gui.nix
|
||||||
|
(import ../../modules/storage/disko.nix {
|
||||||
|
inherit inputs lib config;
|
||||||
|
diskoConfigPath = ./disko-config.nix;
|
||||||
|
})
|
||||||
|
../../modules/desktop/hyprland.nix
|
||||||
|
../../modules/desktop/apps/soundux.nix
|
||||||
|
../../modules/desktop/apps/freeshow.nix
|
||||||
|
../../modules/desktop/apps/x32edit.nix
|
||||||
|
../../modules/desktop/apps/mixing-station.nix
|
||||||
|
../../modules/desktop/apps/opencode.nix
|
||||||
|
../../modules/core/management.nix
|
||||||
|
../../modules/hardware/laptop.nix
|
||||||
|
../../modules/hardware/nvidia.nix
|
||||||
|
../../modules/hardware/thinkpad-battery-limit.nix
|
||||||
|
../../modules/hardware/tablet-mode.nix
|
||||||
|
../../modules/core/known-hosts.nix
|
||||||
|
../../modules/core/podman.nix
|
||||||
|
../../modules/core/dev.nix
|
||||||
|
../../modules/services/paperless.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
sops.secrets = {
|
||||||
|
"x1carbon/borg-passphrase" = {
|
||||||
|
mode = "0600";
|
||||||
|
owner = "root";
|
||||||
|
};
|
||||||
|
"x1carbon/borg-ssh-key" = {
|
||||||
|
mode = "0600";
|
||||||
|
owner = "root";
|
||||||
|
};
|
||||||
|
# Nextcloud CalDAV credentials for the QuickShell calendar popup. Rendered
|
||||||
|
# to /run/secrets/hp-laptop/nextcloud-cal-env (the default path the
|
||||||
|
# qs-cal-sync backend expects) — reuse the same secret for both hosts.
|
||||||
|
"hp-laptop/nextcloud-cal-env" = {
|
||||||
|
owner = "petere";
|
||||||
|
group = "users";
|
||||||
|
mode = "0440";
|
||||||
|
};
|
||||||
|
"users/petere-password" = {
|
||||||
|
neededForUsers = true;
|
||||||
|
};
|
||||||
|
"x1carbon/telegram-bot-token" = {
|
||||||
|
owner = "petere";
|
||||||
|
group = "users";
|
||||||
|
mode = "0440";
|
||||||
|
};
|
||||||
|
"opencode-api-key" = {
|
||||||
|
owner = "petere";
|
||||||
|
group = "users";
|
||||||
|
mode = "0440";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
#TEMPORARY FIX
|
||||||
|
# nixpkgs.overlays = [
|
||||||
|
# (final: prev: {
|
||||||
|
# pnpm = prev.pnpm // { nodejs-slim = final.nodejs-slim; };
|
||||||
|
# pnpm_10 = prev.pnpm_10 // { nodejs-slim = final.nodejs-slim; };
|
||||||
|
# pnpm_11 = prev.pnpm_11 // { nodejs-slim = final.nodejs-slim; };
|
||||||
|
# })
|
||||||
|
# ];
|
||||||
|
|
||||||
|
services.paperless-service.enable = true;
|
||||||
|
|
||||||
|
hardware.sensor.iio.enable = true;
|
||||||
|
|
||||||
|
home-manager.users.petere.imports = [
|
||||||
|
../../home-manager/modules/hyprland.nix
|
||||||
|
../../home-manager/modules/quickshell-cal.nix
|
||||||
|
../../home-manager/modules/quickshell-apps.nix
|
||||||
|
../../home-manager/modules/opencode.nix
|
||||||
|
../../home-manager/modules/nix-lsp.nix
|
||||||
|
../../home-manager/modules/matugen.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
home-manager.users.petere.services.quickshell-cal.enable = true;
|
||||||
|
|
||||||
|
home-manager.users.petere.services.quickshell-apps = {
|
||||||
|
enable = true;
|
||||||
|
apps = [
|
||||||
|
{
|
||||||
|
name = "Element";
|
||||||
|
cmd = "element-desktop";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "Nextcloud";
|
||||||
|
cmd = "nextcloud";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "Bitwarden";
|
||||||
|
cmd = "bitwarden";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
networking.hostName = "x1carbon";
|
||||||
|
networking.modemmanager.enable = true;
|
||||||
|
|
||||||
|
# Laptop-specific hardware (fingerprint reader, fwupd)
|
||||||
|
my.hardware.laptop.enable = true;
|
||||||
|
|
||||||
|
# Charging cap at 80% via the ThinkPad's native sysfs thresholds
|
||||||
|
# (charge_control_start/end_threshold). The EC at 76% won't resume charging
|
||||||
|
# until it drops to/below the resume threshold (75) — expect the level to
|
||||||
|
# hover between ~76 and ~80 while "on". Despite the name, this is the
|
||||||
|
# standard ThinkPad behaviour; the QML gear toggle drives this via
|
||||||
|
# `battery-charge-limit on|off|status`.
|
||||||
|
my.hardware.thinkpadBatteryLimit.enable = true;
|
||||||
|
|
||||||
|
my.hardware.nvidia = {
|
||||||
|
enable = true;
|
||||||
|
nvidiaSettings = true;
|
||||||
|
package = config.boot.kernelPackages.nvidiaPackages.legacy_580;
|
||||||
|
};
|
||||||
|
|
||||||
|
my.hardware.tabletMode.enable = true;
|
||||||
|
|
||||||
|
programs.steam.enable = true;
|
||||||
|
|
||||||
|
services.hardware.bolt.enable = true;
|
||||||
|
|
||||||
|
# Establish trust for SSH to richmond-server (borg backup target, now using Backrest).
|
||||||
|
my.knownHosts.richmondServer = true;
|
||||||
|
|
||||||
|
# Borg Backup removed - migrated to Backrest (see hosts/homeserver-1/configuration.nix)
|
||||||
|
my.users.petere = {
|
||||||
|
hashedPasswordFile = config.sops.secrets."users/petere-password".path;
|
||||||
|
subUidStart = 100000;
|
||||||
|
subGidStart = 100000;
|
||||||
|
};
|
||||||
|
|
||||||
|
networking.firewall = {
|
||||||
|
enable = true;
|
||||||
|
allowedTCPPorts = [
|
||||||
|
9756 # TeleportFling screen/audio streaming
|
||||||
|
];
|
||||||
|
allowedUDPPorts = [
|
||||||
|
9999 # TeleportFling multicast discovery
|
||||||
|
];
|
||||||
|
allowedTCPPortRanges = [
|
||||||
|
{
|
||||||
|
from = 1714;
|
||||||
|
to = 1764;
|
||||||
|
} # KDE Connect / GSConnect
|
||||||
|
{
|
||||||
|
from = 5960;
|
||||||
|
to = 6000;
|
||||||
|
} # NDI streams
|
||||||
|
];
|
||||||
|
allowedUDPPortRanges = [
|
||||||
|
{
|
||||||
|
from = 1714;
|
||||||
|
to = 1764;
|
||||||
|
} # KDE Connect / GSConnect discovery
|
||||||
|
{
|
||||||
|
from = 5960;
|
||||||
|
to = 6000;
|
||||||
|
} # NDI reliable UDP
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
boot.kernelModules = [
|
||||||
|
"sg"
|
||||||
|
"v4l2loopback"
|
||||||
|
];
|
||||||
|
boot.extraModulePackages = [ pkgs.linuxPackages.v4l2loopback ];
|
||||||
|
boot.extraModprobeConfig = ''
|
||||||
|
options v4l2loopback devices=1 video_nr=1 card_label="OBS Cam" exclusive_caps=1
|
||||||
|
'';
|
||||||
|
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
openshot-qt
|
||||||
|
(
|
||||||
|
(wrapOBS.override {
|
||||||
|
obs-studio = obs-studio.override { cudaSupport = true; };
|
||||||
|
})
|
||||||
|
{
|
||||||
|
plugins = with obs-studio-plugins; [
|
||||||
|
distroav
|
||||||
|
obs-backgroundremoval
|
||||||
|
obs-teleport
|
||||||
|
];
|
||||||
|
}
|
||||||
|
)
|
||||||
|
vorta
|
||||||
|
xournalpp
|
||||||
|
winbox
|
||||||
|
steam
|
||||||
|
lmstudio
|
||||||
|
lm_sensors
|
||||||
|
gimp
|
||||||
|
scribus
|
||||||
|
ventoy
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# /hosts/x1carbon/disko-config.nix
|
||||||
|
# This file will contain your disko configuration for x1carbon.
|
||||||
|
{
|
||||||
|
disk = {
|
||||||
|
nixos = {
|
||||||
|
type = "disk";
|
||||||
|
device = "/dev/nvme0n1";
|
||||||
|
content = {
|
||||||
|
type = "gpt";
|
||||||
|
partitions = {
|
||||||
|
boot = {
|
||||||
|
size = "1M";
|
||||||
|
type = "EF02"; # for grub MBR
|
||||||
|
};
|
||||||
|
ESP = {
|
||||||
|
size = "512M";
|
||||||
|
type = "EF00";
|
||||||
|
content = {
|
||||||
|
type = "filesystem";
|
||||||
|
format = "vfat";
|
||||||
|
mountpoint = "/boot";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
swap = {
|
||||||
|
size = "20G";
|
||||||
|
type = "8200";
|
||||||
|
content = {
|
||||||
|
type = "swap";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
root = {
|
||||||
|
size = "100%";
|
||||||
|
content = {
|
||||||
|
type = "filesystem";
|
||||||
|
format = "ext4";
|
||||||
|
mountpoint = "/";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# /hosts/x1carbon/hardware-configuration.nix
|
||||||
|
# This file will be generated by NixOS during installation or by 'nixos-generate-config'.
|
||||||
|
# It contains hardware-specific settings for x1carbon.
|
||||||
|
{ config
|
||||||
|
, lib
|
||||||
|
, pkgs
|
||||||
|
, modulesPath
|
||||||
|
, ...
|
||||||
|
}:
|
||||||
|
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
(modulesPath + "/installer/scan/not-detected.nix")
|
||||||
|
];
|
||||||
|
|
||||||
|
#hardware.ipu6.enable = true;
|
||||||
|
#hardware.ipu6.platform = "ipu6";
|
||||||
|
|
||||||
|
boot.initrd.availableKernelModules = [
|
||||||
|
"xhci_pci"
|
||||||
|
"nvme"
|
||||||
|
"usb_storage"
|
||||||
|
"sd_mod"
|
||||||
|
];
|
||||||
|
boot.initrd.kernelModules = [ ];
|
||||||
|
boot.kernelModules = [ "kvm-intel" ];
|
||||||
|
boot.extraModulePackages = [ ];
|
||||||
|
|
||||||
|
# fileSystems."/" =
|
||||||
|
# { device = "/dev/disk/by-uuid/882a76d6-c1ac-4efd-aff8-b56d43ec7ca5";
|
||||||
|
# fsType = "ext4";
|
||||||
|
# };
|
||||||
|
|
||||||
|
# fileSystems."/boot" =
|
||||||
|
# { device = "/dev/disk/by-uuid/EF30-EBA7";
|
||||||
|
# fsType = "vfat";
|
||||||
|
# options = [ "fmask=0077" "dmask=0077" ];
|
||||||
|
# };
|
||||||
|
|
||||||
|
# swapDevices =
|
||||||
|
# [ { device = "/dev/disk/by-uuid/1c488737-a2e7-4258-b0f3-bd1d14155d03"; }
|
||||||
|
# ];
|
||||||
|
|
||||||
|
# Enables DHCP on each ethernet and wireless interface. In case of scripted networking
|
||||||
|
# (the default) this is the recommended approach. When using systemd-networkd it's
|
||||||
|
# still possible to use this option, but it's recommended to use it in conjunction
|
||||||
|
# with explicit per-interface declarations with `networking.interfaces.<interface>.useDHCP`.
|
||||||
|
networking.useDHCP = lib.mkDefault true;
|
||||||
|
# networking.interfaces.enp0s31f6.useDHCP = lib.mkDefault true;
|
||||||
|
# networking.interfaces.wlp0s20f3.useDHCP = lib.mkDefault true;
|
||||||
|
|
||||||
|
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
|
||||||
|
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# /hosts/nixos/configuration.nix
|
||||||
|
{ config
|
||||||
|
, pkgs
|
||||||
|
, inputs
|
||||||
|
, lib
|
||||||
|
, ...
|
||||||
|
}:
|
||||||
|
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
./hardware-configuration.nix
|
||||||
|
../../modules/core/common.nix
|
||||||
|
../../modules/core/management.nix
|
||||||
|
../../modules/desktop/gui.nix
|
||||||
|
(import ../../modules/storage/disko.nix {
|
||||||
|
inherit inputs lib config;
|
||||||
|
diskoConfigPath = ./disko-config.nix;
|
||||||
|
})
|
||||||
|
../../modules/desktop/gnome.nix
|
||||||
|
../../modules/desktop/apps/freeshow.nix
|
||||||
|
../../modules/desktop/apps/openlp.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
sops.secrets = {
|
||||||
|
"users/petere-password" = {
|
||||||
|
neededForUsers = true;
|
||||||
|
};
|
||||||
|
"users/guest-password" = {
|
||||||
|
neededForUsers = true;
|
||||||
|
};
|
||||||
|
"opencode-api-key" = {
|
||||||
|
owner = "petere";
|
||||||
|
group = "users";
|
||||||
|
mode = "0440";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
home-manager.users.petere.imports = [ ../../home-manager/modules/desktop-user.nix ];
|
||||||
|
|
||||||
|
networking.hostName = "x470"; # Define your hostname
|
||||||
|
|
||||||
|
my.users.petere = {
|
||||||
|
description = "Peter Edley";
|
||||||
|
hashedPasswordFile = config.sops.secrets."users/petere-password".path;
|
||||||
|
};
|
||||||
|
|
||||||
|
users.users.guest = {
|
||||||
|
isNormalUser = true;
|
||||||
|
hashedPasswordFile = config.sops.secrets."users/guest-password".path;
|
||||||
|
};
|
||||||
|
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
winbox
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# /hosts/x470/disko-config.nix
|
||||||
|
# This file will contain your disko configuration for x470.
|
||||||
|
{
|
||||||
|
disk = {
|
||||||
|
nixos = {
|
||||||
|
type = "disk";
|
||||||
|
device = "/dev/nvme0n1";
|
||||||
|
content = {
|
||||||
|
type = "gpt";
|
||||||
|
partitions = {
|
||||||
|
boot = {
|
||||||
|
size = "1M";
|
||||||
|
type = "EF02"; # for grub MBR
|
||||||
|
};
|
||||||
|
ESP = {
|
||||||
|
size = "512M";
|
||||||
|
type = "EF00";
|
||||||
|
content = {
|
||||||
|
type = "filesystem";
|
||||||
|
format = "vfat";
|
||||||
|
mountpoint = "/boot";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
swap = {
|
||||||
|
size = "20G";
|
||||||
|
type = "8200";
|
||||||
|
content = {
|
||||||
|
type = "swap";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
root = {
|
||||||
|
size = "100%";
|
||||||
|
content = {
|
||||||
|
type = "filesystem";
|
||||||
|
format = "ext4";
|
||||||
|
mountpoint = "/";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# /hosts/x470/hardware-configuration.nix
|
||||||
|
# This file will be generated by NixOS during installation or by 'nixos-generate-config'.
|
||||||
|
# It contains hardware-specific settings for x470.
|
||||||
|
{ config
|
||||||
|
, lib
|
||||||
|
, pkgs
|
||||||
|
, modulesPath
|
||||||
|
, ...
|
||||||
|
}:
|
||||||
|
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
(modulesPath + "/installer/scan/not-detected.nix")
|
||||||
|
];
|
||||||
|
|
||||||
|
boot.initrd.availableKernelModules = [
|
||||||
|
"xhci_pci"
|
||||||
|
"nvme"
|
||||||
|
"usb_storage"
|
||||||
|
"sd_mod"
|
||||||
|
];
|
||||||
|
boot.initrd.kernelModules = [ ];
|
||||||
|
boot.kernelModules = [ "kvm-intel" ];
|
||||||
|
boot.extraModulePackages = [ ];
|
||||||
|
|
||||||
|
# Enables DHCP on each ethernet and wireless interface. In case of scripted networking
|
||||||
|
# (the default) this is the recommended approach. When using systemd-networkd it's
|
||||||
|
# still possible to use this option, but it's recommended to use it in conjunction
|
||||||
|
# with explicit per-interface declarations with `networking.interfaces.<interface>.useDHCP`.
|
||||||
|
networking.useDHCP = lib.mkDefault true;
|
||||||
|
# networking.interfaces.enp0s31f6.useDHCP = lib.mkDefault true;
|
||||||
|
# networking.interfaces.wlp4s0.useDHCP = lib.mkDefault true;
|
||||||
|
|
||||||
|
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
|
||||||
|
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# Nix-Vibe task runner (see AGENTS.md for the underlying workflows)
|
||||||
|
|
||||||
|
set shell := ["zsh", "-c"]
|
||||||
|
|
||||||
|
# Apply a host's NixOS config, then relaunch QuickShell so it loads the freshly
|
||||||
|
# built shell.qml (deployed to ~/.config/quickshell as a store symlink — the
|
||||||
|
# live-reload watcher misses the new path, so a manual restart is required).
|
||||||
|
# Usage: just [hostname] — defaults to x1carbon.
|
||||||
|
default host='x1carbon':
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
sudo nixos-rebuild switch --flake ".#{{host}}"
|
||||||
|
if pgrep -f "[H]yprland" >/dev/null 2>&1; then
|
||||||
|
# comm name is ".quickshell-wra" (kernel-truncated from .quickshell-wrapped);
|
||||||
|
# -x avoids matching this recipe's own argv.
|
||||||
|
pkill -x .quickshell-wra || true
|
||||||
|
sleep 1
|
||||||
|
QS=quic"kshell"
|
||||||
|
command "$QS" >/dev/null 2>&1 &
|
||||||
|
disown || true
|
||||||
|
echo "quickshell relaunched"
|
||||||
|
else
|
||||||
|
echo "no Hyprland session on this host; skipped quickshell restart"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ===== pre-commit workflow =====
|
||||||
|
|
||||||
|
# Format the tree (nixfmt, as declared by the flake formatter).
|
||||||
|
format:
|
||||||
|
nix fmt
|
||||||
|
|
||||||
|
# Stage new files (flake only evaluates git-tracked files), then flake check.
|
||||||
|
check:
|
||||||
|
git add -A
|
||||||
|
nix flake check
|
||||||
|
|
||||||
|
# ===== deployment helpers (no quickshell restart) =====
|
||||||
|
|
||||||
|
# Build and test a config without activating it.
|
||||||
|
# Usage: just dry-build [hostname]
|
||||||
|
dry-build host='x1carbon':
|
||||||
|
sudo nixos-rebuild dry-build --flake .#{{host}}
|
||||||
|
|
||||||
|
# Apply a config without switching (for servers / remote hosts).
|
||||||
|
# Usage: just test [hostname]
|
||||||
|
test host='x1carbon':
|
||||||
|
sudo nixos-rebuild test --flake .#{{host}}
|
||||||
|
|
||||||
|
# Relaunch QuickShell on the current Hyprland session (picks up a rebuilt
|
||||||
|
# shell.qml without a full deploy).
|
||||||
|
restart-qs:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
if ! pgrep -f "[H]yprland" >/dev/null 2>&1; then
|
||||||
|
echo "not in a Hyprland session; nothing to restart" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# comm name is ".quickshell-wra" (kernel-truncated from .quickshell-wrapped);
|
||||||
|
# -x avoids matching this recipe's own argv.
|
||||||
|
pkill -x .quickshell-wra || true
|
||||||
|
sleep 1
|
||||||
|
QS=quic"kshell"
|
||||||
|
command "$QS" >/dev/null 2>&1 &
|
||||||
|
disown || true
|
||||||
|
echo "quickshell relaunched"
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
{ config
|
||||||
|
, pkgs
|
||||||
|
, lib
|
||||||
|
, ...
|
||||||
|
}:
|
||||||
|
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
./settings.nix
|
||||||
|
./fonts.nix
|
||||||
|
./sops.nix
|
||||||
|
./users.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
boot.loader.systemd-boot.enable = true;
|
||||||
|
boot.loader.efi.canTouchEfiVariables = true;
|
||||||
|
|
||||||
|
programs.zsh.enable = true;
|
||||||
|
|
||||||
|
# SSH is key-only on all hosts. petere (the only SSH user) authenticates with
|
||||||
|
# an authorized key; GUI users keep their local password for console/GUI login
|
||||||
|
# but cannot use it over SSH. Root SSH is fully disabled.
|
||||||
|
services.openssh = {
|
||||||
|
enable = true;
|
||||||
|
settings = {
|
||||||
|
PasswordAuthentication = false;
|
||||||
|
KbdInteractiveAuthentication = false;
|
||||||
|
PermitRootLogin = "no";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Passwordless sudo for the admin/agent user only. Other wheel members
|
||||||
|
# (e.g. caitlin, mary) must enter their password for sudo.
|
||||||
|
security.sudo.extraRules = [
|
||||||
|
{
|
||||||
|
users = [ "petere" ];
|
||||||
|
commands = [
|
||||||
|
{
|
||||||
|
command = "ALL";
|
||||||
|
options = [ "NOPASSWD" ];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
system.activationScripts.exportAgeKey = {
|
||||||
|
text = ''
|
||||||
|
if [ -f /root/.config/sops/age/keys.txt ]; then
|
||||||
|
${pkgs.age}/bin/age-keygen -y < /root/.config/sops/age/keys.txt > /tmp/age-public-key.txt
|
||||||
|
chmod 644 /tmp/age-public-key.txt
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
deps = [ ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Automatic Nix store garbage collection and optimisation to prevent disk
|
||||||
|
# creep. Runs every 2 hours; persistent ensures missed runs are caught up on boot.
|
||||||
|
nix.gc = {
|
||||||
|
automatic = true;
|
||||||
|
dates = "*-*-* 0/2:00:00";
|
||||||
|
persistent = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Keep the last 5 system generations (regardless of age), then collect garbage.
|
||||||
|
# nix.gc.options only accepts nix-collect-garbage flags (age-based), so we
|
||||||
|
# override the service to add count-based generation pruning.
|
||||||
|
systemd.services.nix-gc.script = lib.mkForce ''
|
||||||
|
${pkgs.nix}/bin/nix-env --profile /nix/var/nix/profiles/system --delete-generations +5
|
||||||
|
exec ${pkgs.nix}/bin/nix-collect-garbage
|
||||||
|
'';
|
||||||
|
|
||||||
|
nix.optimise.automatic = true;
|
||||||
|
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
# Antigravity removed - no longer used
|
||||||
|
];
|
||||||
|
|
||||||
|
system.stateVersion = "23.11";
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{ pkgs, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
# nixfmt is the standard Nix formatter (faster, stricter, future-proof).
|
||||||
|
# The flake formatter is also set to nixfmt.
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
nixfmt
|
||||||
|
python3
|
||||||
|
flutter
|
||||||
|
uv
|
||||||
|
sops
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{ config, pkgs, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
fonts.fontconfig.enable = true;
|
||||||
|
fonts.fontDir.enable = true;
|
||||||
|
|
||||||
|
fonts.packages = with pkgs; [
|
||||||
|
fira-code
|
||||||
|
fira-code-symbols
|
||||||
|
nerd-fonts.fira-code
|
||||||
|
corefonts
|
||||||
|
vista-fonts
|
||||||
|
];
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"L+ /usr/share/fonts - - - - /run/current-system/sw/share/X11/fonts"
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{ config, lib, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
cfg = config.my.knownHosts;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.my.knownHosts = {
|
||||||
|
mcfServer = lib.mkEnableOption "mcf-server SSH host key";
|
||||||
|
richmondServer = lib.mkEnableOption "richmond-server SSH host key";
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkMerge [
|
||||||
|
(lib.mkIf cfg.mcfServer {
|
||||||
|
services.openssh.knownHosts."mcf-server".publicKey =
|
||||||
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINOeOgD+GNQw5Isw/AumZcDFzdzO6YnKJEFWcuUcKPI2";
|
||||||
|
})
|
||||||
|
(lib.mkIf cfg.richmondServer {
|
||||||
|
services.openssh.knownHosts."richmond-server".publicKey =
|
||||||
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEqDXL9w8QwUcqxtW3kyHW/LUDqCGqf6JQ3ZZw52vRQY";
|
||||||
|
})
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{ pkgs, ... }:
|
||||||
|
{
|
||||||
|
# Common admin/monitoring tools available on every host.
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
htop
|
||||||
|
bottom
|
||||||
|
iotop
|
||||||
|
ncdu
|
||||||
|
ripgrep
|
||||||
|
fd
|
||||||
|
jq
|
||||||
|
lsof
|
||||||
|
tmux
|
||||||
|
];
|
||||||
|
|
||||||
|
# Firmware updates for all machines (laptops AND servers).
|
||||||
|
services.fwupd.enable = true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{ pkgs, config, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
virtualisation.podman = {
|
||||||
|
enable = true;
|
||||||
|
dockerCompat = true;
|
||||||
|
dockerSocket.enable = true;
|
||||||
|
defaultNetwork.settings.dns_enabled = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
virtualisation.oci-containers.backend = "podman";
|
||||||
|
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
podman-compose
|
||||||
|
];
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{ config
|
||||||
|
, pkgs
|
||||||
|
, lib
|
||||||
|
, ...
|
||||||
|
}:
|
||||||
|
|
||||||
|
{
|
||||||
|
# Set your time zone
|
||||||
|
time.timeZone = "Europe/London";
|
||||||
|
|
||||||
|
# Configure console keyboard layout
|
||||||
|
console.keyMap = "uk";
|
||||||
|
|
||||||
|
# Configure X server keyboard layout if X server is enabled
|
||||||
|
services.xserver.xkb.layout = lib.mkIf config.services.xserver.enable "gb";
|
||||||
|
|
||||||
|
services.tailscale.enable = true;
|
||||||
|
|
||||||
|
users.mutableUsers = false;
|
||||||
|
|
||||||
|
nix.settings = {
|
||||||
|
accept-flake-config = true;
|
||||||
|
trusted-users = [ "petere" ];
|
||||||
|
experimental-features = [
|
||||||
|
"nix-command"
|
||||||
|
"flakes"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
# gemini-cli
|
||||||
|
kitty.terminfo
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{ config
|
||||||
|
, pkgs
|
||||||
|
, inputs
|
||||||
|
, ...
|
||||||
|
}:
|
||||||
|
|
||||||
|
{
|
||||||
|
sops = {
|
||||||
|
age = {
|
||||||
|
keyFile = "/root/.config/sops/age/keys.txt";
|
||||||
|
generateKey = false;
|
||||||
|
};
|
||||||
|
defaultSopsFile = ../../secrets.yaml;
|
||||||
|
secrets."gemini-api-key" = {
|
||||||
|
owner = "petere";
|
||||||
|
group = "users";
|
||||||
|
mode = "0440";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
{ config
|
||||||
|
, pkgs
|
||||||
|
, lib
|
||||||
|
, ...
|
||||||
|
}:
|
||||||
|
|
||||||
|
let
|
||||||
|
cfg = config.my.users.petere;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.my.users.petere = {
|
||||||
|
enable = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Whether to create the petere admin user.";
|
||||||
|
};
|
||||||
|
|
||||||
|
description = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
description = "Optional GECOS description for petere.";
|
||||||
|
};
|
||||||
|
|
||||||
|
hashedPasswordFile = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
description = "Path to the hashed password file, or null for SSH-key-only access.";
|
||||||
|
};
|
||||||
|
|
||||||
|
subUidStart = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.int;
|
||||||
|
default = null;
|
||||||
|
description = "Start UID for petere's rootless subuid range, or null to disable.";
|
||||||
|
};
|
||||||
|
|
||||||
|
subGidStart = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.int;
|
||||||
|
default = null;
|
||||||
|
description = "Start GID for petere's rootless subgid range, or null to disable.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
users.users.petere = {
|
||||||
|
isNormalUser = true;
|
||||||
|
shell = pkgs.zsh;
|
||||||
|
extraGroups = [ "wheel" ];
|
||||||
|
description = lib.mkIf (cfg.description != null) cfg.description;
|
||||||
|
hashedPasswordFile = lib.mkIf (cfg.hashedPasswordFile != null) cfg.hashedPasswordFile;
|
||||||
|
openssh.authorizedKeys.keys = [
|
||||||
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJiCtkYDBfieK3i4TbVomeyXa185yCFZUvrbMamR4bqs petere@x1carbon"
|
||||||
|
];
|
||||||
|
subUidRanges = lib.mkIf (cfg.subUidStart != null) [
|
||||||
|
{
|
||||||
|
startUid = cfg.subUidStart;
|
||||||
|
count = 65536;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
subGidRanges = lib.mkIf (cfg.subGidStart != null) [
|
||||||
|
{
|
||||||
|
startGid = cfg.subGidStart;
|
||||||
|
count = 65536;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{ pkgs, ... }:
|
||||||
|
|
||||||
|
# Carla — audio plugin host / JACK & PipeWire patchbay
|
||||||
|
{
|
||||||
|
environment.systemPackages = [
|
||||||
|
pkgs.carla
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{ pkgs, ... }:
|
||||||
|
|
||||||
|
# DVD authoring and burning tools for mcf-stream
|
||||||
|
{
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
brasero # GNOME disc burning (data & video DVD)
|
||||||
|
dvdstyler # DVD authoring with menus
|
||||||
|
dvdauthor # CLI tooling for authoring DVD video
|
||||||
|
libdvdcss # Play/rip encrypted DVDs
|
||||||
|
gnome-multi-writer # USB/optical image writer
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
{ config, pkgs, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
freeshowAppImage = pkgs.stdenv.mkDerivation {
|
||||||
|
pname = "freeshow-appimage";
|
||||||
|
version = "1.5.6";
|
||||||
|
|
||||||
|
src = pkgs.fetchurl {
|
||||||
|
url = "https://github.com/ChurchApps/FreeShow/releases/download/v1.5.6/FreeShow-1.5.6-x86_64.AppImage";
|
||||||
|
sha256 = "10z1xgqzwfbs4api12myyx368yp8ahzbmjpfmwnh8c340f8b22f4";
|
||||||
|
};
|
||||||
|
|
||||||
|
dontUnpack = true;
|
||||||
|
dontBuild = true;
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
mkdir -p $out/bin
|
||||||
|
ln -s ${pkgs.appimage-run}/bin/appimage-run $out/bin/freeshow
|
||||||
|
mkdir -p $out/share/applications
|
||||||
|
cat > $out/share/applications/freeshow.desktop << EOF
|
||||||
|
[Desktop Entry]
|
||||||
|
Name=FreeShow
|
||||||
|
Exec=${pkgs.appimage-run}/bin/appimage-run $src
|
||||||
|
Icon=freeshow
|
||||||
|
Type=Application
|
||||||
|
Categories=Multimedia;
|
||||||
|
EOF
|
||||||
|
'';
|
||||||
|
|
||||||
|
meta = with pkgs.lib; {
|
||||||
|
description = "FreeShow AppImage wrapper";
|
||||||
|
homepage = "https://github.com/ChurchApps/FreeShow";
|
||||||
|
license = licenses.gpl3Only; # Assuming GPLv3 based on GitHub repo
|
||||||
|
platforms = platforms.linux;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
environment.systemPackages = [
|
||||||
|
freeshowAppImage
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
{ pkgs, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
mixingStation = pkgs.stdenv.mkDerivation rec {
|
||||||
|
pname = "mixing-station";
|
||||||
|
version = "3.1.4";
|
||||||
|
|
||||||
|
src = pkgs.fetchurl {
|
||||||
|
url = "https://mixingstation.app/backend/api/web/download/update/mixing-station-pc/release";
|
||||||
|
sha256 = "sha256-8d+rGfcOMz8ZoaTy4wd9SEExyc/IaUDysf1WC8xyTC0=";
|
||||||
|
};
|
||||||
|
|
||||||
|
nativeBuildInputs = [
|
||||||
|
pkgs.unzip
|
||||||
|
pkgs.makeWrapper
|
||||||
|
];
|
||||||
|
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
libGL
|
||||||
|
libx11
|
||||||
|
libxext
|
||||||
|
libxcursor
|
||||||
|
libxrandr
|
||||||
|
libxxf86vm
|
||||||
|
libxi
|
||||||
|
libpulseaudio
|
||||||
|
];
|
||||||
|
|
||||||
|
unpackPhase = ''
|
||||||
|
unzip $src
|
||||||
|
'';
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
mkdir -p $out/bin
|
||||||
|
mkdir -p $out/share/mixing-station
|
||||||
|
cp -r * $out/share/mixing-station/
|
||||||
|
|
||||||
|
makeWrapper ${pkgs.openjdk21}/bin/java $out/bin/mixing-station \
|
||||||
|
--add-flags "-jar $out/share/mixing-station/mixing-station-desktop.jar" \
|
||||||
|
--prefix LD_LIBRARY_PATH : ${pkgs.lib.makeLibraryPath buildInputs}
|
||||||
|
|
||||||
|
mkdir -p $out/share/applications
|
||||||
|
cat > $out/share/applications/mixing-station.desktop << EOF
|
||||||
|
[Desktop Entry]
|
||||||
|
Name=Mixing Station
|
||||||
|
Exec=$out/bin/mixing-station
|
||||||
|
Icon=audio-mixer
|
||||||
|
Type=Application
|
||||||
|
Categories=AudioVideo;Audio;
|
||||||
|
Comment=Remote control for digital mixers
|
||||||
|
EOF
|
||||||
|
'';
|
||||||
|
|
||||||
|
meta = with pkgs.lib; {
|
||||||
|
description = "Remote control for digital mixers";
|
||||||
|
homepage = "https://mixingstation.app/";
|
||||||
|
license = licenses.unfree;
|
||||||
|
platforms = platforms.linux;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
environment.systemPackages = [
|
||||||
|
mixingStation
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{ pkgs, ... }:
|
||||||
|
|
||||||
|
# OBS Studio for live streaming/recording on mcf-stream
|
||||||
|
{
|
||||||
|
environment.systemPackages = [
|
||||||
|
(
|
||||||
|
(pkgs.wrapOBS.override {
|
||||||
|
# Enable NVIDIA NVENC GPU encoding
|
||||||
|
obs-studio = pkgs.obs-studio.override { cudaSupport = true; };
|
||||||
|
})
|
||||||
|
{
|
||||||
|
plugins = with pkgs.obs-studio-plugins; [
|
||||||
|
distroav
|
||||||
|
obs-backgroundremoval
|
||||||
|
obs-teleport
|
||||||
|
];
|
||||||
|
}
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{ pkgs, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
opencode
|
||||||
|
opencode-desktop
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
{ pkgs, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
# Install OpenLP from official flatpak bundle
|
||||||
|
# Update the version in the URL when new releases are available
|
||||||
|
systemd.services.openlp-flatpak-install = {
|
||||||
|
description = "Install OpenLP from official flatpak bundle";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
after = [ "network-online.target" ];
|
||||||
|
wants = [ "network-online.target" ];
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
ExecStart = pkgs.writeShellScript "install-openlp" ''
|
||||||
|
set -e
|
||||||
|
|
||||||
|
OPENLP_VERSION="3.1.7"
|
||||||
|
OPENLP_URL="https://get.openlp.org/$OPENLP_VERSION/openlp-$OPENLP_VERSION-1.flatpak"
|
||||||
|
OPENLP_ID="org.openlp.OpenLP"
|
||||||
|
|
||||||
|
# Check if already installed
|
||||||
|
if ${pkgs.flatpak}/bin/flatpak list --app | grep -q "$OPENLP_ID"; then
|
||||||
|
echo "OpenLP is already installed"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Download and install the bundle
|
||||||
|
echo "Downloading OpenLP $OPENLP_VERSION..."
|
||||||
|
TEMP_FILE=$(${pkgs.coreutils}/bin/mktemp)
|
||||||
|
${pkgs.curl}/bin/curl -L -o "$TEMP_FILE" "$OPENLP_URL"
|
||||||
|
|
||||||
|
echo "Installing OpenLP from bundle..."
|
||||||
|
${pkgs.flatpak}/bin/flatpak install --system --bundle --noninteractive -y "$TEMP_FILE"
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
${pkgs.coreutils}/bin/rm -f "$TEMP_FILE"
|
||||||
|
echo "OpenLP installed successfully"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
services.flatpak.packages = [
|
||||||
|
"io.github.Soundux"
|
||||||
|
];
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user