Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47258a6317 | ||
|
|
5db594897b | ||
|
|
e2abf72735 | ||
|
|
2e52d33ba5 | ||
|
|
a2d55bf983
|
||
|
|
726681e5c2 | ||
|
|
cdbad8910c
|
||
|
|
85d5fa453a | ||
|
|
ec10527c0d | ||
|
|
e5fb44c367 | ||
|
|
66164d9671
|
||
|
|
0c69e71585
|
||
|
|
0aa5c4021e
|
||
|
|
1ca1d04e8d
|
@@ -0,0 +1 @@
|
||||
.worktrees/
|
||||
Generated
+162
-5
@@ -21,6 +21,27 @@
|
||||
"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": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
@@ -39,6 +60,29 @@
|
||||
"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": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
@@ -103,16 +147,16 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1783703440,
|
||||
"narHash": "sha256-O3/YajjWo001VUIgD8BwaRdSNLUFe7nZ1qV5TwhRBcw=",
|
||||
"lastModified": 1775036866,
|
||||
"narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "8f0500b9660505dc3cb647775fe9a978a74b5283",
|
||||
"rev": "6201e203d09599479a3b3450ed24fa81537ebc4e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-26.05",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
@@ -133,12 +177,100 @@
|
||||
"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": {
|
||||
"inputs": {
|
||||
"awsvpnclient": "awsvpnclient",
|
||||
"hermes-agent": "hermes-agent",
|
||||
"home-manager": "home-manager",
|
||||
"home-manager-deck": "home-manager-deck",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"nixpkgs": "nixpkgs_2",
|
||||
"nixpkgs-deck": "nixpkgs-deck",
|
||||
"zen-browser": "zen-browser"
|
||||
}
|
||||
@@ -158,6 +290,31 @@
|
||||
"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": {
|
||||
"inputs": {
|
||||
"home-manager": "home-manager_2",
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
url = "github:ymatsiuk/awsvpnclient";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
hermes-agent.url = "github:NousResearch/hermes-agent";
|
||||
};
|
||||
|
||||
outputs =
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
../../modules/nixos/steam
|
||||
../../modules/nixos/awsvpn
|
||||
../../modules/nixos/tailscale
|
||||
../../modules/nixos/wake-on-lan
|
||||
../../modules/nixos/hermes
|
||||
];
|
||||
|
||||
# Bootloader
|
||||
@@ -102,6 +104,7 @@
|
||||
shell = pkgs.zsh;
|
||||
openssh.authorizedKeys.keys = [
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHtBfMUaNtpUYOi0dFsGsAv/ypw535+SouH7fn8eYDvr writterdeck"
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG8115XYzQTL/ZfAerfm2carlSsIDHN6qM/K5V+AQ0St Generated By Termius"
|
||||
];
|
||||
};
|
||||
|
||||
@@ -115,6 +118,8 @@
|
||||
hyprland.enable = true;
|
||||
steam.enable = true;
|
||||
tailscale.enable = true;
|
||||
wake-on-lan.enable = true;
|
||||
hermes.enable = true;
|
||||
};
|
||||
|
||||
##------------------------------
|
||||
@@ -134,6 +139,8 @@
|
||||
ollama-rocm
|
||||
glib
|
||||
gsettings-desktop-schemas
|
||||
zip
|
||||
yarn
|
||||
];
|
||||
|
||||
virtualisation.docker.enable = true; # docker, buildx
|
||||
@@ -213,7 +220,7 @@
|
||||
# broadly on every network interface by the firewall.
|
||||
services.openssh = {
|
||||
enable = true;
|
||||
openFirewall = false;
|
||||
openFirewall = true;
|
||||
settings = {
|
||||
PasswordAuthentication = false;
|
||||
KbdInteractiveAuthentication = false;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
Warning: Identity file /home/jpporta/.ssh/id_rsa not accessible: No such file or directory.
|
||||
Warning: Identity file /home/jpporta/.ssh/id_rsa not accessible: No such file or directory.
|
||||
Warning: Identity file /home/jpporta/.ssh/id_rsa not accessible: No such file or directory.
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
@@ -32,6 +35,7 @@
|
||||
../../modules/home-manager/tmux
|
||||
../../modules/home-manager/openspec
|
||||
../../modules/home-manager/power-profiles
|
||||
../../modules/home-manager/ntfy-notify
|
||||
|
||||
inputs.zen-browser.homeModules.beta
|
||||
];
|
||||
@@ -86,11 +90,14 @@
|
||||
|
||||
nvim.enable = true;
|
||||
pi.enable = true;
|
||||
tmux.enable = true;
|
||||
tmux = {
|
||||
enable = true;
|
||||
};
|
||||
openspec.enable = true;
|
||||
power-profiles.enable = true;
|
||||
};
|
||||
ntfy-notify.enable = true;
|
||||
|
||||
};
|
||||
home.packages =
|
||||
let
|
||||
mediaplayerPython = pkgs.python3.withPackages (ps: with ps; [ pygobject3 ]);
|
||||
|
||||
@@ -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
@@ -1,25 +1,52 @@
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
{ config
|
||||
, pkgs
|
||||
, lib
|
||||
, ...
|
||||
}:
|
||||
|
||||
let
|
||||
mainFont = "Berkeley Mono Nerd Font Mono";
|
||||
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
|
||||
{
|
||||
imports = [
|
||||
../../modules/home-manager/oh-my-posh
|
||||
../../modules/home-manager/zshrc
|
||||
../../modules/home-manager/bat
|
||||
../../modules/home-manager/cedilla
|
||||
../../modules/home-manager/opencode
|
||||
../../modules/home-manager/nvim
|
||||
../../modules/home-manager/pi
|
||||
../../modules/home-manager/tmux
|
||||
../../modules/home-manager/eza
|
||||
../../modules/home-manager/zoxide
|
||||
../../modules/home-manager/tailscale-daemon
|
||||
];
|
||||
|
||||
home = {
|
||||
@@ -38,27 +65,48 @@ in
|
||||
enable = true;
|
||||
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;
|
||||
pi.enable = true;
|
||||
nvim.enable = true;
|
||||
eza.enable = true;
|
||||
bat.enable = true;
|
||||
zoxide.enable = true;
|
||||
tailscale-daemon = {
|
||||
enable = true;
|
||||
acceptRoutes = true;
|
||||
};
|
||||
};
|
||||
|
||||
home.packages = with pkgs; [
|
||||
cage
|
||||
cageWithLayoutState
|
||||
keyboardLayout
|
||||
foot
|
||||
wlr-randr
|
||||
git
|
||||
ripgrep
|
||||
fd
|
||||
mosh
|
||||
tailscale
|
||||
fastfetch
|
||||
lazygit
|
||||
stow
|
||||
tmux
|
||||
bluetuith
|
||||
];
|
||||
# `tailscale`/`ts` shell helpers come from the tailscale-daemon module.
|
||||
|
||||
# Non-NixOS host: fixes locales, ld paths, XDG desktop entries
|
||||
targets.genericLinux.enable = true;
|
||||
@@ -122,10 +170,21 @@ in
|
||||
bash = {
|
||||
enable = true;
|
||||
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 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
|
||||
'';
|
||||
};
|
||||
@@ -160,7 +219,7 @@ in
|
||||
serverAliveInterval = 30;
|
||||
};
|
||||
"pc-ts" = {
|
||||
hostname = "jpporta-nixos";
|
||||
hostname = "jpporta-nixos.taild23e4.ts.net";
|
||||
user = "jpporta";
|
||||
serverAliveInterval = 30;
|
||||
};
|
||||
|
||||
@@ -20,36 +20,31 @@
|
||||
size = 10.3;
|
||||
normal = {
|
||||
family = "BerkeleyMono Nerd Font Mono";
|
||||
style = "Regular";
|
||||
};
|
||||
bold = {
|
||||
family = "BerkeleyMono Nerd Font Mono";
|
||||
style = "ExtraBold";
|
||||
};
|
||||
italic = {
|
||||
family = "BerkeleyMono Nerd Font Mono";
|
||||
style = "Oblique";
|
||||
};
|
||||
bold_italic = {
|
||||
family = "BerkeleyMono Nerd Font Mono";
|
||||
style = "ExtraBold Oblique";
|
||||
};
|
||||
};
|
||||
window = {
|
||||
decorations = "None";
|
||||
opacity = 0.8;
|
||||
blur = true;
|
||||
blur = false;
|
||||
dynamic_padding = true;
|
||||
padding = {
|
||||
x = 10;
|
||||
y = 16;
|
||||
};
|
||||
};
|
||||
keyboard = {
|
||||
bindings = [
|
||||
{
|
||||
key = "Return";
|
||||
mods = "Shift";
|
||||
chars = "\u001B\r";
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
{ lib, config, ... }:
|
||||
{
|
||||
options.custom.cedilla = {
|
||||
enable = lib.mkEnableOption "enable cedilla - apply Compose configuration for cedilla";
|
||||
|
||||
options.custom = {
|
||||
cedilla.enable = lib.mkEnableOption "enable cedilla - apply Compose configuration for cedilla";
|
||||
startFcitx5 = lib.mkOption {
|
||||
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 {
|
||||
@@ -13,7 +21,7 @@
|
||||
<dead_acute> <C> : "Ç" Ccedilla
|
||||
'';
|
||||
|
||||
systemd.user.services.fcitx5 = {
|
||||
systemd.user.services.fcitx5 = lib.mkIf config.custom.cedilla.startFcitx5 {
|
||||
Unit = {
|
||||
Description = "Fcitx5 input method";
|
||||
PartOf = [ "graphical-session.target" ];
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.custom.ntfy-notify;
|
||||
in
|
||||
{
|
||||
options.custom.ntfy-notify = {
|
||||
enable = lib.mkEnableOption "ntfy desktop notification subscriber";
|
||||
|
||||
server = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "ntfy.sh";
|
||||
description = "ntfy server base URL (e.g. ntfy.sh or self-hosted).";
|
||||
};
|
||||
|
||||
subscriptions = lib.mkOption {
|
||||
type = lib.types.listOf (lib.types.submodule {
|
||||
options = {
|
||||
topic = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "ntfy topic to subscribe to.";
|
||||
};
|
||||
priority = lib.mkOption {
|
||||
type = lib.types.enum [ "low" "normal" "critical" ];
|
||||
default = "normal";
|
||||
description = "Urgency level passed to notify-send -u.";
|
||||
};
|
||||
};
|
||||
});
|
||||
default = [
|
||||
{
|
||||
topic = "joao-kanban";
|
||||
priority = "critical";
|
||||
}
|
||||
];
|
||||
description = ''
|
||||
List of ntfy topics to subscribe to. Each topic gets a notify-send
|
||||
command that feeds into the desktop notification daemon (SwayNC).
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
home.packages = [ pkgs.ntfy-sh ];
|
||||
|
||||
# Client config with subscription → notify-send command mappings.
|
||||
# ntfy subscribe (without args) reads subscriptions from client.yml.
|
||||
xdg.configFile."ntfy/client.yml" = {
|
||||
text = ''
|
||||
# Generated by ndots ntfy-notify module.
|
||||
# Each message spawns notify-send → your notification daemon.
|
||||
subscribe:
|
||||
'' + lib.concatMapStringsSep "\n" (sub:
|
||||
let
|
||||
cmd = "notify-send -a Kanban -u ${sub.priority} \"Kanban Update\" \"$m\"";
|
||||
in
|
||||
" - topic: ${cfg.server}/${sub.topic}\n"
|
||||
+ " command: ${cmd}"
|
||||
) cfg.subscriptions;
|
||||
};
|
||||
|
||||
systemd.user.services.ntfy-notify = {
|
||||
Unit = {
|
||||
Description = "ntfy push notification subscriber (kanban → notify-send)";
|
||||
After = [ "graphical-session.target" ];
|
||||
PartOf = [ "graphical-session.target" ];
|
||||
};
|
||||
Service = {
|
||||
Type = "simple";
|
||||
ExecStart = "${pkgs.ntfy-sh}/bin/ntfy subscribe";
|
||||
Restart = "on-failure";
|
||||
RestartSec = 10;
|
||||
};
|
||||
Install.WantedBy = [ "graphical-session.target" ];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -12,6 +12,13 @@
|
||||
plugin = [
|
||||
"@dietrichgebert/ponytail"
|
||||
];
|
||||
mcp = {
|
||||
n8n = {
|
||||
type = "remote";
|
||||
url = "https://n8n.joaoporta.com/mcp-server/http";
|
||||
enabled = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -29,14 +29,14 @@ let
|
||||
}
|
||||
|
||||
listener {
|
||||
timeout = 300
|
||||
timeout = 4
|
||||
on-timeout = loginctl lock-session
|
||||
}
|
||||
|
||||
listener {
|
||||
timeout = 330
|
||||
on-timeout = hyprctl dispatch dpms off
|
||||
on-resume = hyprctl dispatch dpms on
|
||||
timeout = 5
|
||||
on-timeout = hyprctl dispatch 'hl.dsp.dpms({ action = "off" })'
|
||||
on-resume = hyprctl dispatch 'hl.dsp.dpms({ action = "on" })'
|
||||
}
|
||||
# No suspend listener.
|
||||
'';
|
||||
@@ -45,7 +45,7 @@ let
|
||||
general {
|
||||
lock_cmd = pidof hyprlock || hyprlock
|
||||
before_sleep_cmd = loginctl lock-session
|
||||
after_sleep_cmd = hyprctl dispatch dpms on
|
||||
after_sleep_cmd = hyprctl dispatch 'hl.dsp.dpms({ action = "on" })'
|
||||
}
|
||||
|
||||
listener {
|
||||
@@ -55,8 +55,8 @@ let
|
||||
|
||||
listener {
|
||||
timeout = 660
|
||||
on-timeout = hyprctl dispatch dpms off
|
||||
on-resume = hyprctl dispatch dpms on
|
||||
on-timeout = hyprctl dispatch 'hl.dsp.dpms({ action = "off" })'
|
||||
on-resume = hyprctl dispatch 'hl.dsp.dpms({ action = "on" })'
|
||||
}
|
||||
|
||||
listener {
|
||||
@@ -74,7 +74,7 @@ let
|
||||
set -euo pipefail
|
||||
|
||||
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_CAFFE="caffeinated"
|
||||
@@ -199,6 +199,7 @@ in
|
||||
home.packages = [
|
||||
powerProfileScript
|
||||
waybarScript
|
||||
pkgs.hypridle
|
||||
];
|
||||
|
||||
# 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" ];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,9 +1,47 @@
|
||||
|
||||
{ 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 {
|
||||
@@ -11,6 +49,132 @@
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -50,11 +50,35 @@
|
||||
nix-update = "sudo nix-channel --update && sudo nixos-rebuild switch";
|
||||
hms = "cd ~/ndots && git add . && home-manager switch --flake .#jpporta-deck";
|
||||
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 ''
|
||||
fastfetch
|
||||
'';
|
||||
initContent = lib.mkMerge [
|
||||
(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
|
||||
'')
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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`
|
||||
@@ -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.
|
||||
@@ -0,0 +1 @@
|
||||
/nix/store/kldv63c3f6aqvk95rbwp7jf9ms7qjbb9-home-manager-generation
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
/nix/store/z1w7hmcjbxlj4zif3lwb8ifkn66rjy60-home-manager-generation
|
||||
Reference in New Issue
Block a user