Compare commits

..
10 Commits
Author SHA1 Message Date
jpporta a2d55bf983 hermes 2026-07-21 09:47:02 -03:00
jpporta 726681e5c2 Squashed commit of the following:
commit 19ed08c375be6d6a0b344ab1b9d3f76f3c29514b
Author: João Porta <jpedro.porta@gmail.com>
Date:   Fri Jul 17 23:12:12 2026 +0000

    zsh

commit 19df08188fa07b40b9b28a4665b015c4fa525dff
Merge: 577d753 4e4cc38
Author: João Porta <jpedro.porta@gmail.com>
Date:   Thu Jul 16 22:29:29 2026 +0000

    Merge remote-tracking branch 'refs/remotes/origin/tailscale' into tailscale

commit 577d753475be867146ebcacc79330a1e227043a5
Author: João Porta <jpedro.porta@gmail.com>
Date:   Thu Jul 16 15:29:53 2026 +0000

    tailscale

commit 4e4cc381b9e333592184490ea9f749fe87ebbbb9
Author: João Porta <jpedro.porta@gmail.com>
Date:   Thu Jul 16 15:29:53 2026 +0000

    tailscale
2026-07-17 23:15:07 +00:00
jpporta cdbad8910c Squashed commit of the following:
commit bf4873eb012235b4d82cd79c4f508064ead301ab
Author: Joao Porta <jpedro.porta@gmail.com>
Date:   Fri Jul 17 17:24:26 2026 -0300

    wake on lan
2026-07-17 17:25:16 -03:00
jpporta 85d5fa453a pt kb on writter deck 2026-07-16 22:26:35 +00:00
jpporta ec10527c0d update 2026-07-16 21:36:06 +00:00
jpporta e5fb44c367 cage foot 2026-07-16 19:11:46 +00:00
jpporta 66164d9671 power profiles 2026-07-15 14:14:12 -03:00
jpporta 0c69e71585 ssh in lan 2026-07-15 14:14:07 -03:00
jpporta 0aa5c4021e powermanager config 2026-07-15 11:25:31 -03:00
jpporta 1ca1d04e8d tmux 2026-07-15 11:25:31 -03:00
28 changed files with 981 additions and 51 deletions
Generated
+162 -5
View File
@@ -21,6 +21,27 @@
"type": "github" "type": "github"
} }
}, },
"flake-parts": {
"inputs": {
"nixpkgs-lib": [
"hermes-agent",
"nixpkgs"
]
},
"locked": {
"lastModified": 1772408722,
"narHash": "sha256-rHuJtdcOjK7rAHpHphUb1iCvgkU3GpfvicLMwwnfMT0=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "f20dc5d9b8027381c474144ecabc9034d6a839a3",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-utils": { "flake-utils": {
"inputs": { "inputs": {
"systems": "systems" "systems": "systems"
@@ -39,6 +60,29 @@
"type": "github" "type": "github"
} }
}, },
"hermes-agent": {
"inputs": {
"flake-parts": "flake-parts",
"nixpkgs": "nixpkgs",
"npm-lockfile-fix": "npm-lockfile-fix",
"pyproject-build-systems": "pyproject-build-systems",
"pyproject-nix": "pyproject-nix",
"uv2nix": "uv2nix"
},
"locked": {
"lastModified": 1784633374,
"narHash": "sha256-kaRIIOZwlfLI3dwI5TgnyuLyG5RsuwM6jsuw3s5m5wc=",
"owner": "NousResearch",
"repo": "hermes-agent",
"rev": "64702f8f91661149128ca1a721f7a0fd4c22113b",
"type": "github"
},
"original": {
"owner": "NousResearch",
"repo": "hermes-agent",
"type": "github"
}
},
"home-manager": { "home-manager": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
@@ -103,16 +147,16 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1783703440, "lastModified": 1775036866,
"narHash": "sha256-O3/YajjWo001VUIgD8BwaRdSNLUFe7nZ1qV5TwhRBcw=", "narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "8f0500b9660505dc3cb647775fe9a978a74b5283", "rev": "6201e203d09599479a3b3450ed24fa81537ebc4e",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "NixOS",
"ref": "nixos-26.05", "ref": "nixos-unstable",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }
@@ -133,12 +177,100 @@
"type": "github" "type": "github"
} }
}, },
"nixpkgs_2": {
"locked": {
"lastModified": 1783703440,
"narHash": "sha256-O3/YajjWo001VUIgD8BwaRdSNLUFe7nZ1qV5TwhRBcw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "8f0500b9660505dc3cb647775fe9a978a74b5283",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-26.05",
"repo": "nixpkgs",
"type": "github"
}
},
"npm-lockfile-fix": {
"inputs": {
"nixpkgs": [
"hermes-agent",
"nixpkgs"
]
},
"locked": {
"lastModified": 1775903712,
"narHash": "sha256-2GV79U6iVH4gKAPWYrxUReB0S41ty/Y3dBLquU8AlaA=",
"owner": "jeslie0",
"repo": "npm-lockfile-fix",
"rev": "c6093acb0c0548e0f9b8b3d82918823721930fe8",
"type": "github"
},
"original": {
"owner": "jeslie0",
"repo": "npm-lockfile-fix",
"type": "github"
}
},
"pyproject-build-systems": {
"inputs": {
"nixpkgs": [
"hermes-agent",
"nixpkgs"
],
"pyproject-nix": [
"hermes-agent",
"pyproject-nix"
],
"uv2nix": [
"hermes-agent",
"uv2nix"
]
},
"locked": {
"lastModified": 1772555609,
"narHash": "sha256-3BA3HnUvJSbHJAlJj6XSy0Jmu7RyP2gyB/0fL7XuEDo=",
"owner": "pyproject-nix",
"repo": "build-system-pkgs",
"rev": "c37f66a953535c394244888598947679af231863",
"type": "github"
},
"original": {
"owner": "pyproject-nix",
"repo": "build-system-pkgs",
"type": "github"
}
},
"pyproject-nix": {
"inputs": {
"nixpkgs": [
"hermes-agent",
"nixpkgs"
]
},
"locked": {
"lastModified": 1772865871,
"narHash": "sha256-/ZTSg97aouL0SlPHaokA4r3iuH9QzHVuWPACD2CUCFY=",
"owner": "pyproject-nix",
"repo": "pyproject.nix",
"rev": "e537db02e72d553cea470976b9733581bcf5b3ed",
"type": "github"
},
"original": {
"owner": "pyproject-nix",
"repo": "pyproject.nix",
"type": "github"
}
},
"root": { "root": {
"inputs": { "inputs": {
"awsvpnclient": "awsvpnclient", "awsvpnclient": "awsvpnclient",
"hermes-agent": "hermes-agent",
"home-manager": "home-manager", "home-manager": "home-manager",
"home-manager-deck": "home-manager-deck", "home-manager-deck": "home-manager-deck",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs_2",
"nixpkgs-deck": "nixpkgs-deck", "nixpkgs-deck": "nixpkgs-deck",
"zen-browser": "zen-browser" "zen-browser": "zen-browser"
} }
@@ -158,6 +290,31 @@
"type": "github" "type": "github"
} }
}, },
"uv2nix": {
"inputs": {
"nixpkgs": [
"hermes-agent",
"nixpkgs"
],
"pyproject-nix": [
"hermes-agent",
"pyproject-nix"
]
},
"locked": {
"lastModified": 1773039484,
"narHash": "sha256-+boo33KYkJDw9KItpeEXXv8+65f7hHv/earxpcyzQ0I=",
"owner": "pyproject-nix",
"repo": "uv2nix",
"rev": "b68be7cfeacbed9a3fa38a2b5adc0cfb81d9bb1f",
"type": "github"
},
"original": {
"owner": "pyproject-nix",
"repo": "uv2nix",
"type": "github"
}
},
"zen-browser": { "zen-browser": {
"inputs": { "inputs": {
"home-manager": "home-manager_2", "home-manager": "home-manager_2",
+1
View File
@@ -24,6 +24,7 @@
url = "github:ymatsiuk/awsvpnclient"; url = "github:ymatsiuk/awsvpnclient";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
hermes-agent.url = "github:NousResearch/hermes-agent";
}; };
outputs = outputs =
+8 -1
View File
@@ -14,6 +14,8 @@
../../modules/nixos/steam ../../modules/nixos/steam
../../modules/nixos/awsvpn ../../modules/nixos/awsvpn
../../modules/nixos/tailscale ../../modules/nixos/tailscale
../../modules/nixos/wake-on-lan
../../modules/nixos/hermes
]; ];
# Bootloader # Bootloader
@@ -102,6 +104,7 @@
shell = pkgs.zsh; shell = pkgs.zsh;
openssh.authorizedKeys.keys = [ openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHtBfMUaNtpUYOi0dFsGsAv/ypw535+SouH7fn8eYDvr writterdeck" "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHtBfMUaNtpUYOi0dFsGsAv/ypw535+SouH7fn8eYDvr writterdeck"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG8115XYzQTL/ZfAerfm2carlSsIDHN6qM/K5V+AQ0St Generated By Termius"
]; ];
}; };
@@ -115,6 +118,8 @@
hyprland.enable = true; hyprland.enable = true;
steam.enable = true; steam.enable = true;
tailscale.enable = true; tailscale.enable = true;
wake-on-lan.enable = true;
hermes.enable = true;
}; };
##------------------------------ ##------------------------------
@@ -134,6 +139,8 @@
ollama-rocm ollama-rocm
glib glib
gsettings-desktop-schemas gsettings-desktop-schemas
zip
yarn
]; ];
virtualisation.docker.enable = true; # docker, buildx virtualisation.docker.enable = true; # docker, buildx
@@ -213,7 +220,7 @@
# broadly on every network interface by the firewall. # broadly on every network interface by the firewall.
services.openssh = { services.openssh = {
enable = true; enable = true;
openFirewall = false; openFirewall = true;
settings = { settings = {
PasswordAuthentication = false; PasswordAuthentication = false;
KbdInteractiveAuthentication = false; KbdInteractiveAuthentication = false;
+3 -1
View File
@@ -86,7 +86,9 @@
nvim.enable = true; nvim.enable = true;
pi.enable = true; pi.enable = true;
tmux.enable = true; tmux = {
enable = true;
};
openspec.enable = true; openspec.enable = true;
power-profiles.enable = true; power-profiles.enable = true;
}; };
+52
View File
@@ -0,0 +1,52 @@
# Cage intentionally has no IPC API, so status-line clients cannot query its
# active XKB group. When CAGE_XKB_STATE_FILE is set, keep that file synchronized
# with the zero-based effective layout index.
--- a/seat.c
+++ b/seat.c
@@ -12,6 +12,7 @@
#include <assert.h>
#include <linux/input-event-codes.h>
+#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wayland-server-core.h>
@@ -245,8 +246,29 @@
}
static void
+write_xkb_layout_state(struct wlr_keyboard *keyboard)
+{
+ const char *path = getenv("CAGE_XKB_STATE_FILE");
+ if (path == NULL || path[0] == '\0' || keyboard->xkb_state == NULL) {
+ return;
+ }
+
+ FILE *state_file = fopen(path, "w");
+ if (state_file == NULL) {
+ return;
+ }
+
+ xkb_layout_index_t layout =
+ xkb_state_serialize_layout(keyboard->xkb_state, XKB_STATE_LAYOUT_EFFECTIVE);
+ fprintf(state_file, "%u\n", (unsigned int)layout);
+ fclose(state_file);
+}
+
+static void
handle_modifier_event(struct wlr_keyboard *keyboard, struct cg_seat *seat)
{
+ write_xkb_layout_state(keyboard);
+
wlr_seat_set_keyboard(seat->seat, keyboard);
wlr_seat_keyboard_notify_modifiers(seat->seat, &keyboard->modifiers);
@@ -371,6 +393,8 @@
wl_signal_add(&cg_group->wlr_group->keyboard.events.modifiers, &cg_group->modifiers);
cg_group->modifiers.notify = handle_keyboard_group_modifiers;
+ write_xkb_layout_state(&cg_group->wlr_group->keyboard);
+
return;
cleanup:
+71 -12
View File
@@ -1,25 +1,52 @@
{ { config
config, , pkgs
pkgs, , lib
lib, , ...
...
}: }:
let let
mainFont = "Berkeley Mono Nerd Font Mono"; mainFont = "Berkeley Mono Nerd Font Mono";
fontSize = 14; fontSize = 14;
# Cage has no IPC for querying its active XKB group. This deck-only build
# writes that group to CAGE_XKB_STATE_FILE for the tmux indicator below.
cageWithLayoutState = pkgs.cage.overrideAttrs (oldAttrs: {
patches = (oldAttrs.patches or [ ]) ++ [ ./cage-xkb-state.patch ];
});
keyboardLayout = pkgs.writeShellApplication {
name = "keyboard-layout";
runtimeInputs = [ pkgs.coreutils ];
text = ''
runtime_dir="''${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
state_file="''${CAGE_XKB_STATE_FILE:-$runtime_dir/writter-deck-keyboard-layout}"
layout=0
if [[ -r "$state_file" ]]; then
IFS= read -r layout < "$state_file" || true
fi
case "$layout" in
0) printf 'US' ;;
1) printf 'PT' ;;
*) printf '?' ;;
esac
'';
};
in in
{ {
imports = [ imports = [
../../modules/home-manager/oh-my-posh ../../modules/home-manager/oh-my-posh
../../modules/home-manager/zshrc ../../modules/home-manager/zshrc
../../modules/home-manager/bat ../../modules/home-manager/bat
../../modules/home-manager/cedilla
../../modules/home-manager/opencode ../../modules/home-manager/opencode
../../modules/home-manager/nvim ../../modules/home-manager/nvim
../../modules/home-manager/pi ../../modules/home-manager/pi
../../modules/home-manager/tmux ../../modules/home-manager/tmux
../../modules/home-manager/eza ../../modules/home-manager/eza
../../modules/home-manager/zoxide ../../modules/home-manager/zoxide
../../modules/home-manager/tailscale-daemon
]; ];
home = { home = {
@@ -38,27 +65,48 @@ in
enable = true; enable = true;
fastfetch = false; fastfetch = false;
}; };
tmux.enable = true; tmux = {
enable = true;
extraConfig = ''
# Cage does not expose the XKB group through Wayland. The deck-only
# Cage patch updates the state file consumed by this status segment.
set -g status-interval 1
set -ag status-right "#[fg=#89b4fa] KBD:#(${keyboardLayout}/bin/keyboard-layout)"
'';
};
cedilla = {
enable = true;
# Foot/libxkbcommon handles ~/.XCompose directly; the deck has no NixOS
# fcitx5 service at /run/current-system/sw.
startFcitx5 = false;
};
oh-my-posh.enable = true; oh-my-posh.enable = true;
pi.enable = true; pi.enable = true;
nvim.enable = true; nvim.enable = true;
eza.enable = true; eza.enable = true;
bat.enable = true; bat.enable = true;
zoxide.enable = true; zoxide.enable = true;
tailscale-daemon = {
enable = true;
acceptRoutes = true;
};
}; };
home.packages = with pkgs; [ home.packages = with pkgs; [
cage cageWithLayoutState
keyboardLayout
foot foot
wlr-randr
git git
ripgrep ripgrep
fd fd
mosh mosh
tailscale
fastfetch fastfetch
lazygit
stow stow
tmux bluetuith
]; ];
# `tailscale`/`ts` shell helpers come from the tailscale-daemon module.
# Non-NixOS host: fixes locales, ld paths, XDG desktop entries # Non-NixOS host: fixes locales, ld paths, XDG desktop entries
targets.genericLinux.enable = true; targets.genericLinux.enable = true;
@@ -122,10 +170,21 @@ in
bash = { bash = {
enable = true; enable = true;
profileExtra = '' profileExtra = ''
if [ -z "$WAYLAND_DISPLAY" ] && [ "$XDG_VTNR" = "1" ]; then # Auto-launch cage + foot when logging in on tty1 (skip over SSH).
# If cage exits, you drop back to the login shell on tty1 instead
# of being stuck in a restart loop.
if [ "$(tty 2>/dev/null)" = "/dev/tty1" ] \
&& [ -z "$WAYLAND_DISPLAY" ] \
&& [ -z "$CAGE_RUNNING" ]; then
export CAGE_RUNNING=1
export WLR_RENDERER=pixman export WLR_RENDERER=pixman
export XDG_RUNTIME_DIR=''${XDG_RUNTIME_DIR:-/run/user/$(id -u)} export XDG_RUNTIME_DIR=''${XDG_RUNTIME_DIR:-/run/user/$(id -u)}
exec cage -- foot export XKB_DEFAULT_LAYOUT="us,us"
export XKB_DEFAULT_VARIANT=",intl"
export XKB_DEFAULT_OPTIONS="grp:alt_shift_toggle"
export CAGE_XKB_STATE_FILE="$XDG_RUNTIME_DIR/writter-deck-keyboard-layout"
rm -f "$CAGE_XKB_STATE_FILE"
exec ${cageWithLayoutState}/bin/cage -- ${pkgs.foot}/bin/foot
fi fi
''; '';
}; };
@@ -160,7 +219,7 @@ in
serverAliveInterval = 30; serverAliveInterval = 30;
}; };
"pc-ts" = { "pc-ts" = {
hostname = "jpporta-nixos"; hostname = "jpporta-nixos.taild23e4.ts.net";
user = "jpporta"; user = "jpporta";
serverAliveInterval = 30; serverAliveInterval = 30;
}; };
+5 -10
View File
@@ -20,36 +20,31 @@
size = 10.3; size = 10.3;
normal = { normal = {
family = "BerkeleyMono Nerd Font Mono"; family = "BerkeleyMono Nerd Font Mono";
style = "Regular";
}; };
bold = { bold = {
family = "BerkeleyMono Nerd Font Mono"; family = "BerkeleyMono Nerd Font Mono";
style = "ExtraBold";
}; };
italic = { italic = {
family = "BerkeleyMono Nerd Font Mono"; family = "BerkeleyMono Nerd Font Mono";
style = "Oblique";
}; };
bold_italic = { bold_italic = {
family = "BerkeleyMono Nerd Font Mono"; family = "BerkeleyMono Nerd Font Mono";
style = "ExtraBold Oblique";
}; };
}; };
window = { window = {
decorations = "None"; decorations = "None";
opacity = 0.8; opacity = 0.8;
blur = true; blur = false;
dynamic_padding = true; dynamic_padding = true;
padding = { padding = {
x = 10; x = 10;
y = 16; y = 16;
}; };
}; };
keyboard = {
bindings = [
{
key = "Return";
mods = "Shift";
chars = "\u001B\r";
}
];
};
}; };
}; };
}; };
+11 -3
View File
@@ -1,8 +1,16 @@
{ lib, config, ... }: { lib, config, ... }:
{ {
options.custom.cedilla = {
enable = lib.mkEnableOption "enable cedilla - apply Compose configuration for cedilla";
options.custom = { startFcitx5 = lib.mkOption {
cedilla.enable = lib.mkEnableOption "enable cedilla - apply Compose configuration for cedilla"; type = lib.types.bool;
default = true;
description = ''
Start the NixOS fcitx5 user service. Disable this when only the Compose
rules are needed, for example on a non-NixOS Wayland host.
'';
};
}; };
config = lib.mkIf config.custom.cedilla.enable { config = lib.mkIf config.custom.cedilla.enable {
@@ -13,7 +21,7 @@
<dead_acute> <C> : "Ç" Ccedilla <dead_acute> <C> : "Ç" Ccedilla
''; '';
systemd.user.services.fcitx5 = { systemd.user.services.fcitx5 = lib.mkIf config.custom.cedilla.startFcitx5 {
Unit = { Unit = {
Description = "Fcitx5 input method"; Description = "Fcitx5 input method";
PartOf = [ "graphical-session.target" ]; PartOf = [ "graphical-session.target" ];
@@ -12,6 +12,13 @@
plugin = [ plugin = [
"@dietrichgebert/ponytail" "@dietrichgebert/ponytail"
]; ];
mcp = {
n8n = {
type = "remote";
url = "https://n8n.joaoporta.com/mcp-server/http";
enabled = true;
};
};
}; };
}; };
}; };
@@ -29,14 +29,14 @@ let
} }
listener { listener {
timeout = 300 timeout = 4
on-timeout = loginctl lock-session on-timeout = loginctl lock-session
} }
listener { listener {
timeout = 330 timeout = 5
on-timeout = hyprctl dispatch dpms off on-timeout = hyprctl dispatch 'hl.dsp.dpms({ action = "off" })'
on-resume = hyprctl dispatch dpms on on-resume = hyprctl dispatch 'hl.dsp.dpms({ action = "on" })'
} }
# No suspend listener. # No suspend listener.
''; '';
@@ -45,7 +45,7 @@ let
general { general {
lock_cmd = pidof hyprlock || hyprlock lock_cmd = pidof hyprlock || hyprlock
before_sleep_cmd = loginctl lock-session before_sleep_cmd = loginctl lock-session
after_sleep_cmd = hyprctl dispatch dpms on after_sleep_cmd = hyprctl dispatch 'hl.dsp.dpms({ action = "on" })'
} }
listener { listener {
@@ -55,8 +55,8 @@ let
listener { listener {
timeout = 660 timeout = 660
on-timeout = hyprctl dispatch dpms off on-timeout = hyprctl dispatch 'hl.dsp.dpms({ action = "off" })'
on-resume = hyprctl dispatch dpms on on-resume = hyprctl dispatch 'hl.dsp.dpms({ action = "on" })'
} }
listener { listener {
@@ -74,7 +74,7 @@ let
set -euo pipefail set -euo pipefail
STATE_FILE="''${XDG_STATE_HOME:-$HOME/.local/state}/power-profile" STATE_FILE="''${XDG_STATE_HOME:-$HOME/.local/state}/power-profile"
ACTIVE_LINK="''${XDG_CONFIG_HOME:-$HOME/.config}/hypr/hypridle.conf" ACTIVE_LINK="''${XDG_CONFIG_HOME:-$HOME/.config}/hypr/hypridle-active.conf"
PROFILES_NORMAL="normal" PROFILES_NORMAL="normal"
PROFILES_CAFFE="caffeinated" PROFILES_CAFFE="caffeinated"
@@ -199,6 +199,7 @@ in
home.packages = [ home.packages = [
powerProfileScript powerProfileScript
waybarScript waybarScript
pkgs.hypridle
]; ];
# We replace the default hypridle.service with our own service that is # We replace the default hypridle.service with our own service that is
@@ -0,0 +1,142 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.custom.tailscale-daemon;
# The writter-deck is a non-NixOS host that can't create a real tailscale0
# interface (missing TUN drivers). Userspace networking avoids that need.
#
# Layout:
# - Socket: %t/tailscaled/tailscaled.sock (per-session tmpfs)
# - State: $HOME/.local/state/tailscale/... (persistent)
#
# In systemd specifiers:
# %t → $XDG_RUNTIME_DIR (=/run/user/$UID) — works in systemd 249+
# %S → $XDG_CONFIG_HOME (! NOT $XDG_STATE_HOME) — works in systemd 249+
#
# Because %S points at the wrong place, we hard-code the persistent state
# path under $HOME/.local/state/, which is the XDG_STATE_HOME default.
homeDir = config.home.homeDirectory;
stateDir = "${homeDir}/.local/state/tailscale";
stateFile = "${stateDir}/tailscaled.state";
# Becomes "$XDG_RUNTIME_DIR/tailscaled/tailscaled.sock" at runtime.
socketFile = "%t/tailscaled/tailscaled.sock";
in
{
options.custom.tailscale-daemon = {
enable = lib.mkEnableOption ''
Tailscale daemon (tailscaled) running as a systemd user service with
userspace networking. For non-NixOS hosts without TUN device drivers.
'';
acceptRoutes = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
If true, run `tailscale up --accept-routes` automatically on first
start. Otherwise you'll need to authenticate manually once.
'';
};
acceptDns = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to accept DNS settings from the coordination server.";
};
};
config = lib.mkIf cfg.enable {
# Create the persistent state dir up front so the systemd service can
# write to it immediately.
home.activation.createTailscaleDirs = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
run mkdir -p ${lib.escapeShellArg stateDir}
run chmod 0700 ${lib.escapeShellArg stateDir}
'';
# Export the socket path (systemd-249-expanded) so any tool (systray,
# scripts) picks the user-scope daemon automatically. Note: this
# contains a literal `%t` which systemd will expand when the daemon
# or its consumers run.
home.sessionVariables.TAILSCALED_SOCKET = "/run/user/$(id -u)/tailscaled/tailscaled.sock";
home.packages = [
pkgs.tailscale
# Wrapper so the CLI client can find the user-scope socket without flags.
(pkgs.writeShellApplication {
name = "ts";
runtimeInputs = [ pkgs.tailscale ];
text = ''
exec tailscale --socket="/run/user/$(id -u)/tailscaled/tailscaled.sock" "$@"
'';
})
];
systemd.user.services.tailscaled = {
Unit = {
Description = "Tailscale daemon (userspace networking, no TUN device)";
# No "After=default.target" — it would deadlock since default.target
# is what brings us up. Just wait for the runtime dir to exist.
RequiresMountsFor = [ "%t" ];
# Survive transient network blips / flap.
StartLimitIntervalSec = 60;
StartLimitBurst = 5;
};
Service = {
Type = "notify";
ExecStart = lib.escapeShellArgs [
"${pkgs.tailscale}/bin/tailscaled"
"--tun=userspace-networking"
"--state=${stateFile}"
"--socket=${socketFile}"
];
# systemd-managed runtime dir:
# $XDG_RUNTIME_DIR/tailscaled (RuntimeDirectory, mode 0700)
# State is in $HOME/.local/state/tailscale and managed by us, not
# via StateDirectory, so we don't need to fight protectHome.
RuntimeDirectory = "tailscaled";
RuntimeDirectoryMode = "0700";
Restart = "on-failure";
RestartSec = 5;
# Light sandboxing. systemd user services can't drop capabilities
# the way the system unit can, so we keep restrictions to a
# conservative subset that doesn't fight journald.
PrivateTmp = true;
ProtectSystem = "strict";
ProtectHome = true;
ReadWritePaths = [ "%t/tailscaled" "${stateDir}" ];
};
Install.WantedBy = [ "default.target" "graphical-session.target" ];
};
# One-shot: authenticate on first start if not already logged in.
# Idempotent: tailscale up is a no-op once you're logged in.
systemd.user.services.tailscale-up = {
Unit = {
Description = "Tailscale: authenticate and connect";
After = [ "tailscaled.service" ];
Requires = [ "tailscaled.service" ];
};
Service = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = lib.escapeShellArgs ([
"${pkgs.tailscale}/bin/tailscale"
"--socket=${socketFile}"
"up"
(if cfg.acceptRoutes then "--accept-routes" else "--no-accept-routes")
(if cfg.acceptDns then "--accept-dns=true" else "--accept-dns=false")
]);
# Don't fail the whole stack if auth isn't completed yet.
SuccessExitStatus = [ 0 1 ];
};
Install.WantedBy = [ "default.target" "graphical-session.target" ];
};
};
}
+172 -8
View File
@@ -1,16 +1,180 @@
{ lib, config, pkgs, ... }:
{ {
lib,
config,
pkgs,
...
}:
options.custom = { {
tmux.enable = lib.mkEnableOption "enable tmux - terminal multiplexer"; options.custom.tmux = {
enable = lib.mkEnableOption "enable tmux - terminal multiplexer";
defaultShell = lib.mkOption {
type = lib.types.package;
default = pkgs.zsh;
defaultText = lib.literalExpression "pkgs.zsh";
description = ''
Shell used by tmux for new windows and panes. Resolved to its
store path so it works the same on NixOS and non-NixOS hosts.
'';
example = lib.literalExpression "pkgs.bash";
};
defaultTerminal = lib.mkOption {
type = lib.types.str;
default = "tmux-256color";
description = "Default terminal type reported to applications inside tmux.";
};
catppuccinFlavor = lib.mkOption {
type = lib.types.enum [
"latte"
"frappe"
"macchiato"
"mocha"
];
default = "mocha";
description = "Catppuccin flavor used by the tmux theme plugin.";
};
extraConfig = lib.mkOption {
type = lib.types.lines;
default = "";
description = "Extra lines appended verbatim to ~/.config/tmux/tmux.conf.";
};
}; };
config = lib.mkIf config.custom.tmux.enable { config = lib.mkIf config.custom.tmux.enable {
home.packages = with pkgs; [ home.packages = with pkgs; [
tmux tmux
]; ];
xdg.configFile."tmux/tmux.conf".source = config.lib.file.mkOutOfStoreSymlink "${config.home.homeDirectory}/ndots/modules/home-manager/tmux/tmux.conf"; xdg.configFile."tmux/tmux.conf".text = ''
##### Terminal & Color Support #####
# Kitty = truecolor
set -g default-terminal "${config.custom.tmux.defaultTerminal}"
set -ga terminal-overrides ",*:Tc"
# Key line:
set -g pane-border-style "bg=default"
set -g pane-active-border-style "bg=default"
set -g status-style "bg=default"
set -g window-style "bg=default"
set -g window-active-style "bg=default"
set -g default-shell ${config.custom.tmux.defaultShell}/bin/${
config.custom.tmux.defaultShell.meta.mainProgram or (lib.getName config.custom.tmux.defaultShell)
}
set -g extended-keys on
set -g extended-keys-format csi-u
# Reduce escape sequence delay (fixes lag in nvim/zsh)
set -sg escape-time 0
# Prefix to C-a
# set -g prefix C-a
# unbind C-b
# bind-key C-a send-prefix
# Mouse follow
set -g mouse on
# Split with better keys
# unbind %
# bind | split-window -h -c "#{pane_current_path}"
# unbind '"'
# bind - split-window -v -c "#{pane_current_path}"
# Easy config reload
unbind r
bind r source-file ~/.config/tmux/tmux.conf
# Move between panes with tmux-navigator
is_vim="ps -o state= -o comm= -t '#{pane_tty}' \
| grep -iqE '^[^TXZ ]+ +(\\S+\\/)?g?(view|l?n?vim?x?|fzf)(diff)?$'"
bind-key -n 'C-h' if-shell "$is_vim" 'send-keys M-h' 'select-pane -L'
bind-key -n 'C-j' if-shell "$is_vim" 'send-keys M-j' 'select-pane -D'
bind-key -n 'C-k' if-shell "$is_vim" 'send-keys M-k' 'select-pane -U'
bind-key -n 'C-l' if-shell "$is_vim" 'send-keys M-l' 'select-pane -R'
# No automatic window renaming
set-option -g allow-rename off
# Bell
set -g visual-activity off
set -g visual-bell off
set -g visual-silence off
setw -g monitor-activity off
set -g bell-action none
# Status bar
set -g status-position top
set -g status-justify left
# Start index on 1
set -g base-index 1
set -g pane-base-index 1
set-window-option -g pane-base-index 1
set-option -g renumber-windows on
# Resize panes
bind -r J resize-pane -D 5
bind -r K resize-pane -U 5
bind -r L resize-pane -R 5
bind -r H resize-pane -L 5
bind -r m resize-pane -Z
# Resize dragging
unbind -T copy-mode-vi MouseDragEnd1Pane # don't exit copy mode after dragging with mouse
# Vim motions
set-window-option -g mode-keys vi
# Selection
bind-key -T copy-mode-vi 'v' send -X begin-selection # start selecting text with "v"
bind-key -T copy-mode-vi 'y' send -X copy-selection # copy text with "y"
set -g @plugin "dweidner/tmux-theme"
set -g @plugin 'tmux-plugins/tmux-yank'
set -g @plugin 'tmux-plugins/tmux-cpu'
set -g @plugin 'tmux-plugins/tmux-battery'
set -g @plugin 'catppuccin/tmux#v2.1.1' # See https://github.com/catppuccin/tmux/tags for additional tags
set -g @plugin 'b0o/tmux-autoreload'
set -g @plugin 'tmux-plugins/tpm'
bind-key -T copy-mode-vi v send-keys -X begin-selection
bind-key -T copy-mode-vi C-v send-keys -X rectangle-toggle
bind-key -T copy-mode-vi y send-keys -X copy-selection-and-cancel
# Catppuccin config
set -g @catppuccin_status_background none
set -g @catppuccin_flavor '${config.custom.tmux.catppuccinFlavor}'
set -g @catppuccin_window_status_style "basic"
set -g status-right-length 100
set -g status-left ""
set -g status-left-length 200
set -g status-right "#{E:@catppuccin_status_application}"
set -agF status-right "#{E:@catppuccin_status_cpu}"
set -ag status-right "#{E:@catppuccin_status_session}"
set -ag status-right "#{E:@catppuccin_status_uptime}"
set -agF status-right "#{E:@catppuccin_status_battery}"
# Neovim delay fix
set -sg escape-time 0
# Tmux keep session
set-option -g detach-on-destroy off
# Focus events
set-option -g focus-events on
bind-key z resize-pane -Z \; if-shell "tmux list-panes -F '#F' | grep -q Z" "set -g status off" "set -g status on"
# Clipboard
set -g set-clipboard on
run '~/.tmux/plugins/tpm/tpm'
''
+ config.custom.tmux.extraConfig;
}; };
} }
+27 -3
View File
@@ -50,11 +50,35 @@
nix-update = "sudo nix-channel --update && sudo nixos-rebuild switch"; nix-update = "sudo nix-channel --update && sudo nixos-rebuild switch";
hms = "cd ~/ndots && git add . && home-manager switch --flake .#jpporta-deck"; hms = "cd ~/ndots && git add . && home-manager switch --flake .#jpporta-deck";
s = "sesh connect $(sesh list | fzf)"; s = "sesh connect $(sesh list | fzf)";
rot90 = "wlr-randr --output HDMI-A-1 --transform 90";
rot0 = "wlr-randr --output HDMI-A-1 --transform normal";
rotl = "wlr-randr --output HDMI-A-1 --transform 270";
rot180 = "wlr-randr --output HDMI-A-1 --transform 180";
# Tailscale shortcuts (writter-deck only really needs these, but
# they don't hurt anyone else either). `ts` is the wrapper that
# points the CLI at the user-scope socket from the tailscale-daemon
# module.
ts = "tailscale --socket=/tmp/ts-$UID/tailscaled.sock";
ts-status = "ts status";
ts-ping = "ts ping -c 3 jpporta-nixos";
}; };
initContent = lib.mkIf config.custom.zsh.fastfetch '' initContent = lib.mkMerge [
fastfetch (lib.mkOrder 700 ''
''; # Home Manager normally defines ZSH in .zshenv. Keep it available
# when .zshrc is sourced directly as well.
export ZSH="${config.programs.zsh.oh-my-zsh.package}/share/oh-my-zsh"
'')
(lib.mkOrder 1000 ''
${lib.optionalString config.custom.zsh.fastfetch "fastfetch"}
# Auto-rotate screen to portrait when running inside cage/foot
# on the writer-deck. Skipped over SSH and other non-Wayland sessions.
if [ -n "$WAYLAND_DISPLAY" ] && [ -n "$CAGE_RUNNING" ] && [ "$ROTATED" != "1" ]; then
export ROTATED=1
wlr-randr --output HDMI-A-1 --transform 90 >/dev/null 2>&1
fi
'')
];
}; };
}; };
} }
+19
View File
@@ -0,0 +1,19 @@
{
config,
lib,
inputs,
pkgs,
...
}:
let
cfg = config.custom.hermes;
in
{
options.custom.hermes.enable = lib.mkEnableOption "Hermes Agent CLI";
config = lib.mkIf cfg.enable {
environment.systemPackages = [
inputs.hermes-agent.packages.${pkgs.system}.default
];
};
}
+71
View File
@@ -0,0 +1,71 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.custom.wake-on-lan;
iface = "enp14s0";
mac = "d8:43:ae:5a:ae:12";
wake-jpporta-nixos = pkgs.writeShellScriptBin "wake-jpporta-nixos" ''
exec ${pkgs.wakeonlan}/bin/wakeonlan -i "''${1:-255.255.255.255}" ${mac}
'';
in
{
options.custom.wake-on-lan = {
enable = lib.mkEnableOption "Wake-on-LAN on the wired NIC (magic-packet mode)";
};
config = lib.mkIf cfg.enable {
# Keep `wol g` applied on the NIC after the link is up (boot path).
systemd.services."wol-${iface}" = {
description = "Enable Wake-on-LAN (magic packet) on ${iface}";
wantedBy = [ "multi-user.target" ];
wants = [
"network-online.target"
"NetworkManager.service"
];
after = [
"network-online.target"
"NetworkManager.service"
"NetworkManager-wait-online.service"
];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "${pkgs.ethtool}/bin/ethtool -s ${iface} wol g";
};
};
# Re-apply `wol g` after resume from suspend / hibernate so NetworkManager
# cannot silently drop the WOL flag on link renegotiation.
systemd.services."wol-${iface}-resume" = {
description = "Re-apply Wake-on-LAN (magic packet) on ${iface} after resume";
wantedBy = [
"suspend.target"
"hibernate.target"
"hybrid-sleep.target"
];
after = [
"systemd-suspend.service"
"systemd-hibernate.service"
"systemd-hybrid-sleep.service"
];
serviceConfig = {
Type = "oneshot";
ExecStart = "${pkgs.ethtool}/bin/ethtool -s ${iface} wol g";
};
};
environment.systemPackages = [
pkgs.ethtool
pkgs.wakeonlan
wake-jpporta-nixos
];
};
}
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-07-17
+59
View File
@@ -0,0 +1,59 @@
## Context
`jpporta-nixos` is a desktop machine with a wired Ethernet interface (`enp14s0`, MAC `d8:43:ae:5a:ae:12`) managed by NetworkManager. The user wants to power the machine on remotely via Wake-on-LAN so it can be reached over Tailscale + SSH without manual intervention.
WOL requires three layers of configuration:
1. **Firmware (BIOS/UEFI):** "Wake on LAN" must be enabled on the NIC, and the machine must remain in a low-power state after shutdown (most desktop boards default to this; some servers default to "power on after AC loss" which would defeat the purpose).
2. **Kernel/driver (NixOS):** The NIC driver must be told to keep listening for magic packets while the system is off. For most Intel NICs this means `ethtool -s <iface> wol g` (magic packet mode).
3. **Network:** Magic packets are sent to the layer-2 broadcast address (`ff:ff:ff:ff:ff:ff`) and rely on the NIC matching its own MAC. The host cannot route or filter them above the NIC, so no userspace daemon is required to *receive* a packet. A userspace tool (`wakeonlan`) is required to *send* one.
NixOS already provides `networking.wakeOnLan.enable`, but that option is hard-coded to `eth0` and is incompatible with predictable interface names (`enp14s0`). The NixOS-supported path for a named, predictable interface is a custom systemd unit that invokes `ethtool` against the specific interface. This is the approach used here because it survives reboots, resumes, and NetworkManager reconnects.
## Goals / Non-Goals
**Goals:**
- Persist `wol g` on `enp14s0` across reboots and resume-from-suspend.
- Provide `ethtool` and `wakeonlan` on `$PATH` so the user can inspect and send magic packets.
- Provide a convenience script `wake-jpporta-nixos` that sends a WOL packet to this machine's MAC.
- Wrap the configuration in a reusable module (`custom.wake-on-lan.enable`) consistent with existing patterns.
- Document the out-of-band BIOS / router configuration steps.
**Non-Goals:**
- Setting BIOS options (firmware is out of reach from NixOS).
- Configuring router port-forwarding for WOL-over-internet (router is out of scope).
- Listening for / relaying WOL packets on this host — that is the NIC's job once `wol g` is set.
- Changing the network interface naming scheme or replacing NetworkManager.
## Decisions
1. **Per-interface systemd unit over `networking.wakeOnLan.enable`.**
- The top-level `networking.wakeOnLan.enable` is hard-coded to `eth0`, which doesn't match the predictable interface name `enp14s0` used here.
- Alternative considered: `networking.interfaces.enp14s0.wakeOnLan.enable = true`. This option does exist in modern NixOS, but on this machine it interacts awkwardly with NetworkManager (which re-loads NIC settings on reconnect) and the underlying implementation still relies on an ethtool invocation that may be racy with NetworkManager bringing the link up.
- **Decision:** Use a dedicated systemd `oneshot` service with a `network-online.target` dependency. This makes the WOL setting idempotent, runs after NetworkManager is up, and can also be triggered on resume-from-suspend by adding it to the `suspend.target` resume path. Concretely we ship a unit `wol-enp14s0.service` that runs `ethtool -s enp14s0 wol g`.
2. **Package set: `ethtool` + `iputils` (which provides `wakeonlan`).**
- `pkgs.ethtool` is the standard tool for inspecting and setting NIC driver options, including WOL modes.
- `pkgs.iputils` provides `wakeonlan`, the simplest CLI for sending a magic packet to a target MAC.
- Alternative considered: `pkgs.wol` (the separate, older `wol` program). `wakeonlan` from iputils is more widely used, has a simpler interface (`wakeonlan <mac>`), and is what most WOL guides assume.
3. **Convenience wrapper script `wake-jpporta-nixos`.**
- Hard-codes the MAC of this machine's wired NIC (`d8:43:ae:5a:ae:12`) into a small `writeShellScriptBin`-packaged script.
- Rationale: avoids having to remember or look up the MAC every time. The wrapper accepts an optional broadcast IP argument so the user can target `192.168.0.255` from off-LAN once the router is configured to forward UDP 9 to that broadcast.
4. **Module pattern: `modules/nixos/wake-on-lan/default.nix` with `custom.wake-on-lan.enable`.**
- Mirrors the existing pattern used by `modules/nixos/tailscale/` and `modules/nixos/hyprland/`.
- Defaults to disabled. Host (`jpporta-nixos`) opts in via `custom.wake-on-lan.enable = true`.
## Risks / Trade-offs
- **[Risk]** `ethtool -s ... wol g` only takes effect while the kernel NIC driver is loaded. Some BIOS/firmware combinations fully power down the NIC on shutdown, making WOL impossible regardless of the OS setting.
- **Mitigation:** Document this clearly in the change notes. The user must verify with `ethtool enp14s0` after a `systemctl poweroff` that `Supports Wake-on: pumbg` and `Wake-on: g` are still present.
- **[Risk]** Some desktop boards default to "Power on after AC loss = Power on" which would make the machine boot after any power outage regardless of WOL — undermining the use case.
- **Mitigation:** Document the BIOS setting ("After Power Loss" → "Power Off" or "Stay Off") in the change notes.
- **[Risk]** NetworkManager may re-apply link settings on resume, overwriting `wol g`.
- **Mitigation:** The systemd unit is wired to run on resume via a `path`/`service` pair (or a manual `systemctl restart wol-enp14s0.service` hook). For the initial implementation we trigger it on boot and rely on the user to re-run if it ever drifts; the follow-up task in `tasks.md` adds the resume hook.
- **[Trade-off]** Hard-coding the MAC and interface name in the Nix module means the module is specific to `jpporta-nixos`. We accept this trade-off because the module is only enabled on this host and the alternative (parameterizing) would add ceremony for a single user.
+27
View File
@@ -0,0 +1,27 @@
## Why
The user wants to power on `jpporta-nixos` remotely via Wake-on-LAN (WOL). This is useful when the machine is shut down but the user wants to access it from another device (e.g., over Tailscale + SSH) without manually pressing the power button. Currently the machine has no WOL configuration: nothing keeps the NIC in a low-power listening state, and no tooling exists on the host to inspect or send WOL packets.
## What Changes
- Enable Wake-on-LAN on the wired Ethernet interface (`enp14s0`) so the NIC keeps listening for magic packets while the system is powered off.
- Add `ethtool` so the user can inspect the current WOL settings and supported modes on the NIC.
- Add `wol` (the `iputils`-style `wakeonlan` command) so WOL packets can be sent from this host to other machines, and from other machines back to this one (using its MAC `d8:43:ae:5a:ae:12`).
- Add a small helper script (`wake-jpporta-nixos`) that sends a WOL magic packet to this machine's MAC address, so it can be invoked by name.
- Document the manual BIOS-level steps that are required for WOL to work (these cannot be set from NixOS).
## Capabilities
### New Capabilities
- `wake-on-lan`: Configures Wake-on-LAN on the host's wired NIC and provides tooling/scripts to inspect and send magic packets.
### Modified Capabilities
- None
## Impact
- **Code:** A new optional module `modules/nixos/wake-on-lan/` will be added and imported into `hosts/jpporta-nixos/configuration.nix`. The module will expose a `custom.wake-on-lan.enable` toggle consistent with the project's existing module pattern (see `modules/nixos/tailscale/`).
- **System:** A systemd unit will be installed that runs `ethtool -s enp14s0 wol g` on boot and on resume, persisting the WOL setting across reboots and sleep states.
- **User:** Two new commands will be on `$PATH`: `ethtool` and `wakeonlan`, plus a convenience wrapper `wake-jpporta-nixos`.
- **Hardware:** Requires BIOS-level "Wake on LAN" to be enabled on the motherboard and a stable AC power source. The user must configure these out-of-band.
- **Network:** To wake the machine from outside the LAN, the router must forward UDP port 9 (or 7) to the broadcast address. This is a router-side concern, not configurable from NixOS.
@@ -0,0 +1,51 @@
## ADDED Requirements
### Requirement: Persistent Wake-on-LAN on the wired NIC
The system SHALL keep the wired Ethernet interface (`enp14s0`) configured to wake on magic-packet while powered off.
#### Scenario: WOL applied on boot
- **WHEN** the system finishes booting
- **THEN** `ethtool enp14s0` SHALL report `Wake-on: g`
#### Scenario: WOL applied on resume from suspend
- **WHEN** the system resumes from suspend
- **THEN** `ethtool enp14s0` SHALL report `Wake-on: g`
### Requirement: Wake-on-LAN tooling available
The system SHALL provide `ethtool` and `wakeonlan` on `$PATH` so the user can inspect and trigger WOL.
#### Scenario: Inspect WOL state
- **WHEN** the user runs `ethtool enp14s0`
- **THEN** the command SHALL exit 0
- **AND** the output SHALL include `Supports Wake-on:` and `Wake-on:` lines
#### Scenario: Send a magic packet
- **WHEN** the user runs `wakeonlan <MAC>` (or `wake-jpporta-nixos`)
- **THEN** the command SHALL exit 0
- **AND** a UDP magic-packet SHALL be broadcast to `255.255.255.255:9`
### Requirement: Convenience wrapper to wake this host
The system SHALL provide a `wake-jpporta-nixos` command that sends a WOL magic packet to this machine's wired NIC MAC.
#### Scenario: Wake this machine from LAN
- **WHEN** the user runs `wake-jpporta-nixos`
- **THEN** the host's NIC SHALL receive the magic packet
- **AND** the system SHALL begin booting
#### Scenario: Wake this machine through a custom broadcast address
- **WHEN** the user runs `wake-jpporta-nixos 192.168.0.255`
- **THEN** the magic packet SHALL be sent to the `192.168.0.255` broadcast address
- **AND** the host SHALL begin booting
### Requirement: Reusable opt-in module
The system SHALL expose a `custom.wake-on-lan.enable` option that, when enabled, configures all of the above.
#### Scenario: Module disabled
- **WHEN** `custom.wake-on-lan.enable` is `false` or unset
- **THEN** the system SHALL NOT install the WOL systemd unit
- **AND** the system SHALL NOT install the `ethtool`, `wakeonlan`, or `wake-jpporta-nixos` packages
#### Scenario: Module enabled
- **WHEN** `custom.wake-on-lan.enable` is `true`
- **THEN** the WOL systemd unit SHALL be active
- **AND** the `wake-jpporta-nixos` command SHALL be on `$PATH`
+26
View File
@@ -0,0 +1,26 @@
## 1. Create the WOL NixOS module
- [x] 1.1 Create `modules/nixos/wake-on-lan/default.nix` with a `custom.wake-on-lan.enable` option.
- [x] 1.2 Define a systemd `oneshot` service `wol-enp14s0.service` that runs `ethtool -s enp14s0 wol g`.
- [x] 1.3 Wire the unit's `after`/`wants` to `network-online.target` and `NetworkManager.service` so it runs after the link is up.
- [x] 1.4 Add the unit to the resume path so `wol g` is re-applied after suspend/resume (use `systemd-suspend.service` or a `path` unit hook).
## 2. Add packages and the convenience wrapper
- [x] 2.1 Add `pkgs.ethtool` and `pkgs.wakeonlan` to `environment.systemPackages` when the module is enabled. *(Note: `pkgs.iputils` does not provide `wakeonlan` in nixpkgs — see implementation note.)*
- [x] 2.2 Create a `wake-jpporta-nixos` shell script via `pkgs.writeShellScriptBin` that invokes `wakeonlan -i ${1:-255.255.255.255} d8:43:ae:5a:ae:12`.
- [x] 2.3 Add the wrapper to `environment.systemPackages`.
## 3. Wire the module into the host
- [x] 3.1 Add `../../modules/nixos/wake-on-lan` to `imports` in `hosts/jpporta-nixos/configuration.nix`.
- [x] 3.2 Add `custom.wake-on-lan.enable = true;` under the existing `custom = { ... };` block in the host config.
## 4. Verify and document
- [x] 4.1 Rebuild the system with `sudo nixos-rebuild switch --flake .#jpporta-nixos`.
- [x] 4.2 Run `ethtool enp14s0` and confirm `Supports Wake-on:` lists `g` and `Wake-on: g`.
- [x] 4.3 Run `systemctl status wol-enp14s0.service` and confirm the unit is `active (exited)`.
- [x] 4.4 Run `wake-jpporta-nixos` once while the machine is up to confirm the script runs without error (it will be a no-op when the host is already up).
- [x] 4.5 Shut the machine down, then run `wake-jpporta-nixos` from another machine on the same LAN and confirm `jpporta-nixos` powers on.
- [x] 4.6 Document the BIOS-level prerequisite ("Wake on LAN" enabled, "After Power Loss" → Power Off / Stay Off) in the change notes or a README so it's not lost.
@@ -0,0 +1,54 @@
# Power Profile Management
## Purpose
This capability provides configurable power management profiles that control system behavior such as screen blanking, locking, and suspension.
## ADDED Requirements
### Requirement: Manage Power Profiles
The system SHALL provide three distinct power management profiles: `caffeinated`, `headless`, and `normal`.
#### Scenario: Caffeinated Profile
- **WHEN** the `caffeinated` profile is active
- **THEN** the system SHALL NOT blank the screen.
- **AND** the system SHALL NOT lock the screen.
- **AND** the system SHALL NOT suspend.
#### Scenario: Headless Profile
- **WHEN** the `headless` profile is active
- **THEN** the system SHALL blank the screen after a timeout.
- **AND** the system SHALL lock the screen when it blanks.
- **AND** the system SHALL NOT suspend.
#### Scenario: Normal Profile
- **WHEN** the `normal` profile is active
- **THEN** the system SHALL blank the screen after a default timeout.
- **AND** the system SHALL lock the screen when it blanks.
- **AND** the system SHALL suspend after a longer default timeout.
### Requirement: Cycle Through Power Profiles
The system SHALL allow the user to cycle through the power profiles using a keybinding.
#### Scenario: Cycle Forward
- **WHEN** the user presses the "next profile" keybinding
- **THEN** the active power profile SHALL change from `normal` to `caffeinated`, `caffeinated` to `headless`, and `headless` to `normal`.
#### Scenario: Cycle Backward
- **WHEN** the user presses the "previous profile" keybinding
- **THEN** the active power profile SHALL change from `normal` to `headless`, `headless` to `caffeinated`, and `caffeinated` to `normal`.
### Requirement: Visual Indicator for Current Profile
The system SHALL display the current power profile mode in the Waybar status bar.
#### Scenario: Display Caffeinated Icon
- **WHEN** the `caffeinated` profile is active
- **THEN** a "caffeinated" icon SHALL be visible in the Waybar status bar.
#### Scenario: Display Headless Icon
- **WHEN** the `headless` profile is active
- **THEN** a "headless" icon SHALL be visible in the Waybar status bar.
#### Scenario: Display Normal Icon
- **WHEN** the `normal` profile is active
- **THEN** a "normal" icon SHALL be visible in the Waybar status bar.
Symlink
+1
View File
@@ -0,0 +1 @@
/nix/store/kldv63c3f6aqvk95rbwp7jf9ms7qjbb9-home-manager-generation
+1
View File
@@ -0,0 +1 @@
/nix/store/z1w7hmcjbxlj4zif3lwb8ifkn66rjy60-home-manager-generation