Compare commits
31
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
810047a074 | ||
|
|
32bc53bb68 | ||
|
|
80556753e3 | ||
|
|
c53a7f278b | ||
|
|
6d861958ce | ||
|
|
0f0de37ea9 | ||
|
|
1e14c57dc1
|
||
|
|
a2d7391bbb
|
||
|
|
ffff15055b
|
||
|
|
d346937782
|
||
|
|
fedb2b87df
|
||
|
|
57f2689be5
|
||
|
|
c430976454
|
||
|
|
fb6d677bcf | ||
|
|
32facdd02d
|
||
|
|
9d842e34cc
|
||
|
|
d4c2b37d46
|
||
|
|
15d4a06719
|
||
|
|
5a4745f637 | ||
|
|
6bbd22bce8
|
||
|
|
233ad01bee | ||
|
|
a2d55bf983
|
||
|
|
726681e5c2 | ||
|
|
cdbad8910c
|
||
|
|
85d5fa453a | ||
|
|
ec10527c0d | ||
|
|
e5fb44c367 | ||
|
|
66164d9671
|
||
|
|
0c69e71585
|
||
|
|
0aa5c4021e
|
||
|
|
1ca1d04e8d
|
@@ -0,0 +1,2 @@
|
||||
.worktrees/
|
||||
/.direnv
|
||||
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,15 +24,17 @@
|
||||
url = "github:ymatsiuk/awsvpnclient";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
hermes-agent.url = "github:NousResearch/hermes-agent";
|
||||
};
|
||||
|
||||
outputs =
|
||||
inputs:
|
||||
inputs@{ self, nixpkgs, ... }:
|
||||
let
|
||||
systems = [
|
||||
"x86_64-linux"
|
||||
"aarch64-linux"
|
||||
];
|
||||
forAllSystems = nixpkgs.lib.genAttrs systems;
|
||||
in
|
||||
{
|
||||
nixosConfigurations.jpporta-nixos = inputs.nixpkgs.lib.nixosSystem {
|
||||
@@ -60,5 +62,21 @@
|
||||
./hosts/writter-deck/home.nix
|
||||
];
|
||||
};
|
||||
|
||||
devShells = forAllSystems (
|
||||
system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
in
|
||||
{
|
||||
default = pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
nixfmt
|
||||
nil # Nix LSP — your Neovim will pick it up via direnv
|
||||
statix # linter, optional
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,7 +13,10 @@
|
||||
../../modules/nixos/hyprland
|
||||
../../modules/nixos/steam
|
||||
../../modules/nixos/awsvpn
|
||||
../../modules/nixos/keyd
|
||||
../../modules/nixos/tailscale
|
||||
../../modules/nixos/wake-on-lan
|
||||
../../modules/nixos/hermes
|
||||
];
|
||||
|
||||
# Bootloader
|
||||
@@ -70,6 +73,19 @@
|
||||
"x-systemd.device-timeout=10s"
|
||||
];
|
||||
};
|
||||
|
||||
"/mnt/obsidian" = {
|
||||
device = "//100.90.248.13/ObsidianVault";
|
||||
fsType = "cifs";
|
||||
options = [
|
||||
"credentials=/etc/samba/obsidian-credentials"
|
||||
"uid=1000"
|
||||
"gid=100"
|
||||
"noperm"
|
||||
"x-systemd.automount"
|
||||
"noatime"
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
# Networking
|
||||
@@ -102,6 +118,7 @@
|
||||
shell = pkgs.zsh;
|
||||
openssh.authorizedKeys.keys = [
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHtBfMUaNtpUYOi0dFsGsAv/ypw535+SouH7fn8eYDvr writterdeck"
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG8115XYzQTL/ZfAerfm2carlSsIDHN6qM/K5V+AQ0St Generated By Termius"
|
||||
];
|
||||
};
|
||||
|
||||
@@ -115,11 +132,18 @@
|
||||
hyprland.enable = true;
|
||||
steam.enable = true;
|
||||
tailscale.enable = true;
|
||||
wake-on-lan.enable = true;
|
||||
hermes.enable = true;
|
||||
keyd = {
|
||||
enable = false;
|
||||
internalIds = [ "1ea7:0907" ];
|
||||
};
|
||||
};
|
||||
|
||||
##------------------------------
|
||||
programs = {
|
||||
zsh.enable = true;
|
||||
nix-ld.enable = true;
|
||||
};
|
||||
|
||||
# System Packages
|
||||
@@ -134,6 +158,7 @@
|
||||
ollama-rocm
|
||||
glib
|
||||
gsettings-desktop-schemas
|
||||
zip
|
||||
];
|
||||
|
||||
virtualisation.docker.enable = true; # docker, buildx
|
||||
@@ -159,17 +184,6 @@
|
||||
jack.enable = true; # only if you use jack apps (reaper, etc.)
|
||||
wireplumber.enable = true;
|
||||
};
|
||||
keyd = {
|
||||
enable = true;
|
||||
keyboards.default = {
|
||||
ids = [ "*" ]; # or specific vendor:product ids
|
||||
settings = {
|
||||
main = {
|
||||
capslock = "overload(control, esc)"; # translate your default.conf here
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
postgresql.enable = true;
|
||||
syncthing = {
|
||||
enable = true;
|
||||
@@ -211,21 +225,25 @@
|
||||
|
||||
# OpenSSH: reachable through the trusted Tailscale interface, but not opened
|
||||
# broadly on every network interface by the firewall.
|
||||
services.openssh = {
|
||||
enable = true;
|
||||
openFirewall = false;
|
||||
settings = {
|
||||
PasswordAuthentication = false;
|
||||
KbdInteractiveAuthentication = false;
|
||||
PermitRootLogin = "no";
|
||||
AllowUsers = [ "jpporta" ];
|
||||
services = {
|
||||
openssh = {
|
||||
enable = true;
|
||||
openFirewall = true;
|
||||
settings = {
|
||||
PasswordAuthentication = false;
|
||||
KbdInteractiveAuthentication = false;
|
||||
PermitRootLogin = "no";
|
||||
AllowUsers = [ "jpporta" ];
|
||||
};
|
||||
};
|
||||
|
||||
gnome.gnome-keyring.enable = true;
|
||||
};
|
||||
programs.mosh = {
|
||||
enable = true;
|
||||
openFirewall = false;
|
||||
};
|
||||
|
||||
nixpkgs.config.allowUnfree = true;
|
||||
system.stateVersion = "26.05";
|
||||
|
||||
}
|
||||
|
||||
@@ -31,7 +31,12 @@
|
||||
../../modules/home-manager/pi
|
||||
../../modules/home-manager/tmux
|
||||
../../modules/home-manager/openspec
|
||||
../../modules/home-manager/dev-orchestrator
|
||||
../../modules/home-manager/power-profiles
|
||||
../../modules/home-manager/ntfy-notify
|
||||
../../modules/home-manager/pinentry
|
||||
../../modules/home-manager/swaync
|
||||
../../modules/home-manager/bitwarden-cli
|
||||
|
||||
inputs.zen-browser.homeModules.beta
|
||||
];
|
||||
@@ -86,11 +91,15 @@
|
||||
|
||||
nvim.enable = true;
|
||||
pi.enable = true;
|
||||
tmux.enable = true;
|
||||
tmux = {
|
||||
enable = true;
|
||||
};
|
||||
openspec.enable = true;
|
||||
dev-orchestrator.enable = true;
|
||||
power-profiles.enable = true;
|
||||
ntfy-notify.enable = true;
|
||||
bitwarden.enable = true;
|
||||
};
|
||||
|
||||
home.packages =
|
||||
let
|
||||
mediaplayerPython = pkgs.python3.withPackages (ps: with ps; [ pygobject3 ]);
|
||||
@@ -107,6 +116,7 @@
|
||||
fastfetch
|
||||
nodejs
|
||||
firefox
|
||||
telegram-desktop
|
||||
];
|
||||
|
||||
programs = {
|
||||
@@ -141,15 +151,12 @@
|
||||
];
|
||||
};
|
||||
gpg.enable = true;
|
||||
direnv = {
|
||||
enable = true;
|
||||
nix-direnv.enable = true;
|
||||
silent = true;
|
||||
};
|
||||
};
|
||||
|
||||
services.gpg-agent = {
|
||||
enable = true;
|
||||
pinentry.package = pkgs.pinentry-rofi;
|
||||
enableSshSupport = true; # only if you use gpg keys as ssh keys
|
||||
defaultCacheTtl = 3600;
|
||||
maxCacheTtl = 86400;
|
||||
};
|
||||
|
||||
services.swaync.enable = true;
|
||||
custom.pinentry.enable = true;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -17,39 +17,34 @@
|
||||
enable = true;
|
||||
settings = {
|
||||
font = {
|
||||
size = 10.3;
|
||||
size = 12;
|
||||
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";
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -37,7 +37,6 @@ in
|
||||
# nitch
|
||||
|
||||
# ---- editors & dev tooling ----
|
||||
tree
|
||||
ffmpeg
|
||||
chromium
|
||||
neovim
|
||||
@@ -68,15 +67,9 @@ in
|
||||
cargo
|
||||
php
|
||||
julia
|
||||
eslint_d
|
||||
golangci-lint
|
||||
rust-analyzer
|
||||
imagemagick
|
||||
ghostscript
|
||||
ripgrep
|
||||
gopls
|
||||
typescript-language-server
|
||||
nixfmt
|
||||
|
||||
# ---- cloud / infra ----
|
||||
awscli2
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
lib,
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}: let
|
||||
bw = pkgs.writeShellApplication {
|
||||
name = "bw";
|
||||
runtimeInputs = with pkgs; [ rbw fzf wl-clipboard libnotify ];
|
||||
text = ''
|
||||
set -euo pipefail
|
||||
cmd="$(basename "$0")"
|
||||
|
||||
entry="$(rbw list --fields=id,name,user --format=tsv \
|
||||
| fzf --with-nth=2.. --delimiter=$'\t')"
|
||||
[ -z "$entry" ] && exit 0
|
||||
|
||||
id="$(printf '%s' "$entry" | cut -f1)"
|
||||
|
||||
case "$cmd" in
|
||||
bwc) val="$(rbw code "$id")" ;;
|
||||
bwu) val="$(rbw get --field username "$id")" ;;
|
||||
bwp) val="$(rbw get "$id")" ;;
|
||||
*) exit 1 ;;
|
||||
esac
|
||||
|
||||
printf '%s' "$val" | wl-copy
|
||||
notify-send -t 1200 "bw: copied $cmd"
|
||||
'';
|
||||
};
|
||||
in {
|
||||
options.custom = {
|
||||
bitwarden.enable = lib.mkEnableOption "enable bitwarden - CLI password manager";
|
||||
};
|
||||
|
||||
config = lib.mkIf config.custom.bitwarden.enable {
|
||||
programs.rbw = {
|
||||
enable = true;
|
||||
settings = {
|
||||
email = "me@joaoporta.com";
|
||||
lock_timeout = 7200;
|
||||
pinentry = pkgs.pinentry-gnome3;
|
||||
base_url = "https://pass.joaoporta.com";
|
||||
};
|
||||
};
|
||||
|
||||
home.packages =
|
||||
let mkBin = name:
|
||||
pkgs.symlinkJoin {
|
||||
inherit name;
|
||||
paths = [ bw ];
|
||||
nativeBuildInputs = [ pkgs.makeWrapper ];
|
||||
postBuild = "ln -sf ${bw}/bin/bw $out/bin/${name}";
|
||||
};
|
||||
in
|
||||
builtins.map mkBin [ "bwp" "bwc" "bwu" ];
|
||||
};
|
||||
}
|
||||
@@ -17,6 +17,9 @@
|
||||
};
|
||||
|
||||
config = lib.mkIf config.custom.jpporta-calendars.enable {
|
||||
home.packages = with pkgs; [
|
||||
seahorse
|
||||
];
|
||||
programs = {
|
||||
vdirsyncer.enable = true;
|
||||
|
||||
|
||||
@@ -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,16 @@
|
||||
{ lib, config, pkgs, ... }:
|
||||
{
|
||||
options.custom = {
|
||||
dev-orchestrator.enable = lib.mkEnableOption "enable dev-orchestrator — OpenSpec + OpenCode dev→QA loop with git worktrees";
|
||||
};
|
||||
|
||||
config = lib.mkIf config.custom.dev-orchestrator.enable {
|
||||
home.packages = [
|
||||
(pkgs.writeShellApplication {
|
||||
name = "dev-orchestrator";
|
||||
runtimeInputs = with pkgs; [ git ];
|
||||
text = builtins.readFile ../../../tooling/dev-orchestrator;
|
||||
})
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -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" ];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -38,6 +38,12 @@ return {
|
||||
desc = "[O]bsidian [T]emplate",
|
||||
mode = "n",
|
||||
},
|
||||
{
|
||||
"<leader>ow",
|
||||
":ObsidianWorkspace<CR>",
|
||||
desc = "[O]bsidian [W]orkspace",
|
||||
mode = "n",
|
||||
},
|
||||
},
|
||||
opts = {
|
||||
|
||||
@@ -122,12 +128,16 @@ return {
|
||||
name = "personal",
|
||||
path = "~/docs/personal",
|
||||
},
|
||||
{
|
||||
name = "hermes",
|
||||
path = "/mnt/documents/journal",
|
||||
},
|
||||
},
|
||||
daily_notes = {
|
||||
folder = "journal",
|
||||
folder = "Journal",
|
||||
date_format = "%Y-%m-%d",
|
||||
default_tags = { "journal" },
|
||||
template = "journal"
|
||||
template = "Daily Note",
|
||||
},
|
||||
completion = {
|
||||
nvim_cmp = true,
|
||||
@@ -135,9 +145,30 @@ return {
|
||||
},
|
||||
|
||||
templates = {
|
||||
subdir = "templates",
|
||||
subdir = "Templates",
|
||||
date_format = "%Y-%m-%d",
|
||||
time_format = "%H:%M",
|
||||
substitutions = {
|
||||
week = function()
|
||||
return os.date("%W", os.time())
|
||||
end,
|
||||
day = function()
|
||||
local d = os.date("%w", os.time())
|
||||
local r = {
|
||||
["0"] = "Sunday",
|
||||
["1"] = "Monday",
|
||||
["2"] = "Tuesday",
|
||||
["3"] = "Wednesday",
|
||||
["4"] = "Thursday",
|
||||
["5"] = "Friday",
|
||||
["6"] = "Saturday",
|
||||
}
|
||||
return r[d]
|
||||
end,
|
||||
full_date = function()
|
||||
return os.date("%A, %B %d, %Y", os.time())
|
||||
end,
|
||||
},
|
||||
},
|
||||
picker = {
|
||||
name = "telescope.nvim",
|
||||
|
||||
@@ -2,6 +2,14 @@ return {
|
||||
"michaelrommel/nvim-silicon",
|
||||
lazy = true,
|
||||
cmd = "Silicon",
|
||||
keys = {
|
||||
{
|
||||
"<leader>s",
|
||||
"<cmd>Silicon<CR>",
|
||||
desc = "[C]ode [S]elfie",
|
||||
mode = "v",
|
||||
},
|
||||
},
|
||||
config = function()
|
||||
local get_visual = function()
|
||||
local curpos = vim.fn.getcurpos()
|
||||
@@ -10,21 +18,27 @@ return {
|
||||
|
||||
if one.row == two.row then
|
||||
if one.col > two.col then
|
||||
local tmp = one
|
||||
one = two
|
||||
two = tmp
|
||||
one, two = two, one
|
||||
end
|
||||
elseif one.row > two.row then
|
||||
local tmp = one
|
||||
one = two
|
||||
two = tmp
|
||||
one, two = two, one
|
||||
end
|
||||
|
||||
two.col = two.col + 1
|
||||
return one.row
|
||||
end
|
||||
|
||||
require("nvim-silicon").setup({
|
||||
font = "JetBrainsMono Nerd Font=24",
|
||||
font = "Berkeley Mono Nerd Font Mono=24",
|
||||
theme = "gruvbox-dark",
|
||||
background = "#bdae93",
|
||||
pad_horiz = 100,
|
||||
pad_vert = 80,
|
||||
line_pad = 2,
|
||||
shadow_blur_radius = 24,
|
||||
shadow_offset_x = 10,
|
||||
shadow_offset_y = 10,
|
||||
shadow_color = "#1d2021",
|
||||
no_window_controls = true,
|
||||
no_line_number = false,
|
||||
line_offset = get_visual() + 1,
|
||||
@@ -36,6 +50,5 @@ return {
|
||||
return vim.bo.filetype
|
||||
end,
|
||||
})
|
||||
vim.keymap.set("v", "<leader>cs", "<cmd>'<,'>Silicon<CR>", { desc = "[C]ode [S]elfie" })
|
||||
end,
|
||||
}
|
||||
|
||||
@@ -12,6 +12,13 @@
|
||||
plugin = [
|
||||
"@dietrichgebert/ponytail"
|
||||
];
|
||||
mcp = {
|
||||
n8n = {
|
||||
type = "remote";
|
||||
url = "https://n8n.joaoporta.com/mcp-server/http";
|
||||
enabled = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
worktrunk
|
||||
sesh
|
||||
sl
|
||||
ntfy-sh
|
||||
signal-cli
|
||||
];
|
||||
};
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
lib,
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
pinentry-wrapper = pkgs.writeShellScriptBin "pinentry-wrapper" ''
|
||||
# ponytail: gpg-agent's env is captured at boot (often on tty1), so SSH_* checks
|
||||
# from the agent miss SSH sessions. Fall back to curses when no usable display
|
||||
# is available, since rofi needs WAYLAND_DISPLAY/DISPLAY the agent may not have.
|
||||
if [ -n "''${SSH_CONNECTION:-}" ] \
|
||||
|| [ -n "''${SSH_TTY:-}" ] \
|
||||
|| [ -z "''${WAYLAND_DISPLAY:-}" ] && [ -z "''${DISPLAY:-}" ]; then
|
||||
exec ${pkgs.pinentry-curses}/bin/pinentry-curses "$@"
|
||||
else
|
||||
exec ${pkgs.pinentry-rofi}/bin/pinentry-rofi "$@"
|
||||
fi
|
||||
'';
|
||||
in {
|
||||
options.custom.pinentry = {
|
||||
enable = lib.mkEnableOption "adaptive pinentry: rofi on TTY, curses on SSH";
|
||||
};
|
||||
|
||||
config = lib.mkIf config.custom.pinentry.enable {
|
||||
home.packages = [ pinentry-wrapper ];
|
||||
|
||||
services.gpg-agent = {
|
||||
enable = true;
|
||||
pinentry.package = pinentry-wrapper;
|
||||
enableSshSupport = true;
|
||||
defaultCacheTtl = 3600;
|
||||
maxCacheTtl = 86400;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -1,55 +1,44 @@
|
||||
{ lib, config, ... }:
|
||||
let
|
||||
background = "#282828"; # bg0 — main dark background
|
||||
background-alt = "#3c3836"; # bg1 — slightly lighter panels/buttons
|
||||
background-sec = "#1d2021"; # bg0_h — darker, for mpris/volume contrast
|
||||
text = "#ebdbb2"; # fg1 — main foreground
|
||||
selected = "#83a598"; # blue — accent for borders/active states
|
||||
urgent = "#fb4934"; # bright red — critical notifications
|
||||
hover = "#504945"; # bg2 — hover highlight
|
||||
in
|
||||
{
|
||||
options.custom = {
|
||||
swaync.enable = lib.mkEnableOption "enable swaync - color cat alternative";
|
||||
swaync.enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = "enable swaync - notification";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf config.custom.swaync.enable {
|
||||
services.swaync = {
|
||||
enable = true;
|
||||
settings = {
|
||||
positionX = "right";
|
||||
positionX = "left";
|
||||
positionY = "top";
|
||||
cssPriority = "user";
|
||||
|
||||
control-center-width = 380;
|
||||
control-center-height = 860;
|
||||
control-center-width = 360;
|
||||
control-center-height = 760;
|
||||
control-center-margin-top = 2;
|
||||
control-center-margin-bottom = 2;
|
||||
control-center-margin-right = 1;
|
||||
control-center-margin-left = 0;
|
||||
|
||||
notification-window-width = 400;
|
||||
notification-icon-size = 48;
|
||||
notification-body-image-height = 160;
|
||||
notification-body-image-width = 200;
|
||||
notification-window-width = 380;
|
||||
notification-icon-size = 40;
|
||||
notification-body-image-height = 120;
|
||||
notification-body-image-width = 160;
|
||||
|
||||
timeout = 4;
|
||||
timeout-low = 2;
|
||||
timeout-critical = 6;
|
||||
timeout = 6;
|
||||
timeout-low = 4;
|
||||
timeout-critical = 8;
|
||||
|
||||
fit-to-screen = false;
|
||||
keyboard-shortcuts = true;
|
||||
image-visibility = "when-available";
|
||||
transition-time = 200;
|
||||
transition-time = 250;
|
||||
hide-on-clear = true;
|
||||
hide-on-action = false;
|
||||
script-fail-notify = true;
|
||||
scripts = {
|
||||
example-script = {
|
||||
exec = "echo 'Do something...'";
|
||||
urgency = "Normal";
|
||||
};
|
||||
};
|
||||
notification-visibility = {
|
||||
example-name = {
|
||||
state = "muted";
|
||||
@@ -112,431 +101,13 @@ in
|
||||
};
|
||||
};
|
||||
};
|
||||
style = ''
|
||||
* {
|
||||
color: ${text};
|
||||
background-color: ${background}
|
||||
all: unset;
|
||||
font-size: 12px;
|
||||
font-family: "BerkeleyMono Nerd Font Propo";
|
||||
transition: 200ms;
|
||||
}
|
||||
|
||||
.notification-row {
|
||||
outline: none;
|
||||
margin: 0;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.floating-notifications.background .notification-row .notification-background {
|
||||
background: alpha(${background}, 0.9);
|
||||
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.6);
|
||||
border: 1px solid ${selected};
|
||||
border-radius: 24px;
|
||||
margin: 16px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.floating-notifications.background
|
||||
.notification-row
|
||||
.notification-background
|
||||
.notification {
|
||||
padding: 6px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.floating-notifications.background
|
||||
.notification-row
|
||||
.notification-background
|
||||
.notification.critical {
|
||||
border: 2px solid ${urgent};
|
||||
}
|
||||
|
||||
.floating-notifications.background
|
||||
.notification-row
|
||||
.notification-background
|
||||
.notification
|
||||
.notification-content {
|
||||
margin: 14px;
|
||||
}
|
||||
|
||||
.floating-notifications.background
|
||||
.notification-row
|
||||
.notification-background
|
||||
.notification
|
||||
> *:last-child
|
||||
> * {
|
||||
min-height: 3.4em;
|
||||
}
|
||||
|
||||
.floating-notifications.background
|
||||
.notification-row
|
||||
.notification-background
|
||||
.notification
|
||||
> *:last-child
|
||||
> *
|
||||
.notification-action {
|
||||
border-radius: 8px;
|
||||
background-color: ${background-alt}};
|
||||
margin: 6px;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.floating-notifications.background
|
||||
.notification-row
|
||||
.notification-background
|
||||
.notification
|
||||
> *:last-child
|
||||
> *
|
||||
.notification-action:hover {
|
||||
background-color: ${hover};
|
||||
border: 1px solid ${selected};
|
||||
}
|
||||
|
||||
.floating-notifications.background
|
||||
.notification-row
|
||||
.notification-background
|
||||
.notification
|
||||
> *:last-child
|
||||
> *
|
||||
.notification-action:active {
|
||||
background-color: ${selected};
|
||||
color: ${background};
|
||||
}
|
||||
|
||||
.image {
|
||||
margin: 10px 20px 10px 0px;
|
||||
}
|
||||
|
||||
.summary {
|
||||
font-weight: 800;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.body {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.floating-notifications.background
|
||||
.notification-row
|
||||
.notification-background
|
||||
.close-button {
|
||||
margin: 6px;
|
||||
padding: 2px;
|
||||
border-radius: 6px;
|
||||
background-color: ${background};
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.floating-notifications.background
|
||||
.notification-row
|
||||
.notification-background
|
||||
.close-button:hover {
|
||||
background-color: ${selected};
|
||||
}
|
||||
|
||||
.floating-notifications.background
|
||||
.notification-row
|
||||
.notification-background
|
||||
.close-button:active {
|
||||
background-color: ${selected};
|
||||
color: ${background};
|
||||
}
|
||||
|
||||
.notification.critical progress {
|
||||
background-color: ${selected};
|
||||
}
|
||||
|
||||
.notification.low progress,
|
||||
.notification.normal progress {
|
||||
background-color: ${selected};
|
||||
}
|
||||
|
||||
* {
|
||||
color: ${text};
|
||||
|
||||
all: unset;
|
||||
font-size: 12px;
|
||||
font-family: "BerkeleyMono Nerd Font Propo";
|
||||
transition: 200ms;
|
||||
}
|
||||
|
||||
/* Avoid 'annoying' backgroud */
|
||||
.blank-window {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* CONTROL CENTER ------------------------------------------------------------------------ */
|
||||
|
||||
.control-center {
|
||||
background: alpha(${background}, 0.7);
|
||||
border-radius: 24px;
|
||||
border: 1px solid ${selected};
|
||||
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.6);
|
||||
margin: 18px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
/* Notifications */
|
||||
.control-center .notification-row .notification-background,
|
||||
.control-center
|
||||
.notification-row
|
||||
.notification-background
|
||||
.notification.critical {
|
||||
background-color: ${background-alt};
|
||||
border-radius: 16px;
|
||||
margin: 4px 0px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.control-center
|
||||
.notification-row
|
||||
.notification-background
|
||||
.notification.critical {
|
||||
color: ${urgent};
|
||||
}
|
||||
|
||||
.control-center
|
||||
.notification-row
|
||||
.notification-background
|
||||
.notification
|
||||
.notification-content {
|
||||
margin: 6px;
|
||||
padding: 8px 6px 2px 2px;
|
||||
}
|
||||
|
||||
.control-center
|
||||
.notification-row
|
||||
.notification-background
|
||||
.notification
|
||||
> *:last-child
|
||||
> * {
|
||||
min-height: 3.4em;
|
||||
}
|
||||
|
||||
.control-center
|
||||
.notification-row
|
||||
.notification-background
|
||||
.notification
|
||||
> *:last-child
|
||||
> *
|
||||
.notification-action {
|
||||
background: alpha(${selected}, 0.6);
|
||||
color: ${text};
|
||||
border-radius: 12px;
|
||||
margin: 6px;
|
||||
}
|
||||
|
||||
.control-center
|
||||
.notification-row
|
||||
.notification-background
|
||||
.notification
|
||||
> *:last-child
|
||||
> *
|
||||
.notification-action:hover {
|
||||
background: ${selected};
|
||||
}
|
||||
|
||||
.control-center
|
||||
.notification-row
|
||||
.notification-background
|
||||
.notification
|
||||
> *:last-child
|
||||
> *
|
||||
.notification-action:active {
|
||||
background-color: ${selected};
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
|
||||
.control-center .notification-row .notification-background .close-button {
|
||||
background: transparent;
|
||||
border-radius: 6px;
|
||||
color: ${text};
|
||||
margin: 0px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.control-center .notification-row .notification-background .close-button:hover {
|
||||
background-color: ${selected};
|
||||
}
|
||||
|
||||
.control-center
|
||||
.notification-row
|
||||
.notification-background
|
||||
.close-button:active {
|
||||
background-color: ${selected};
|
||||
}
|
||||
|
||||
progressbar,
|
||||
progress,
|
||||
trough {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
progressbar {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Notifications expanded-group */
|
||||
|
||||
.notification-group {
|
||||
margin: 2px 8px 2px 8px;
|
||||
}
|
||||
.notification-group-headers {
|
||||
font-weight: bold;
|
||||
font-size: 1.25rem;
|
||||
color: ${text};
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.notification-group-icon {
|
||||
color: ${text};
|
||||
}
|
||||
|
||||
.notification-group-collapse-button,
|
||||
.notification-group-close-all-button {
|
||||
background: transparent;
|
||||
color: ${text};
|
||||
margin: 4px;
|
||||
border-radius: 6px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.notification-group-collapse-button:hover,
|
||||
.notification-group-close-all-button:hover {
|
||||
background: ${hover};
|
||||
}
|
||||
|
||||
/* WIDGETS --------------------------------------------------------------------------- */
|
||||
|
||||
/* Notification clear button */
|
||||
.widget-title {
|
||||
font-size: 1.2em;
|
||||
margin: 6px;
|
||||
}
|
||||
|
||||
.widget-title button {
|
||||
background: ${background-alt};
|
||||
border-radius: 6px;
|
||||
padding: 4px 16px;
|
||||
}
|
||||
|
||||
.widget-title button:hover {
|
||||
background-color: ${hover};
|
||||
}
|
||||
|
||||
.widget-title button:active {
|
||||
background-color: ${selected};
|
||||
}
|
||||
|
||||
/* Do not disturb */
|
||||
.widget-dnd {
|
||||
margin: 6px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.widget-dnd > switch {
|
||||
background: ${background-alt};
|
||||
font-size: initial;
|
||||
border-radius: 8px;
|
||||
box-shadow: none;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.widget-dnd > switch:hover {
|
||||
background: ${hover};
|
||||
}
|
||||
|
||||
.widget-dnd > switch:checked {
|
||||
background: ${selected};
|
||||
}
|
||||
|
||||
.widget-dnd > switch:checked:hover {
|
||||
background: ${hover};
|
||||
}
|
||||
|
||||
.widget-dnd > switch slider {
|
||||
background: ${text};
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* Buttons menu */
|
||||
.widget-buttons-grid {
|
||||
font-size: x-large;
|
||||
padding: 6px 2px;
|
||||
margin: 6px;
|
||||
border-radius: 12px;
|
||||
background: ${background-alt};
|
||||
}
|
||||
|
||||
.widget-buttons-grid > flowbox > flowboxchild > button {
|
||||
margin: 4px 10px;
|
||||
padding: 6px 12px;
|
||||
background: transparent;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.widget-buttons-grid > flowbox > flowboxchild > button:hover {
|
||||
background: ${hover};
|
||||
}
|
||||
|
||||
/* Music player */
|
||||
.widget-mpris {
|
||||
background: ${background-alt};
|
||||
border-radius: 16px;
|
||||
color: ${text};
|
||||
margin: 20px 6px;
|
||||
}
|
||||
|
||||
/* NOTE: Background need *opacity 1* otherwise will turn into the album art blurred */
|
||||
.widget-mpris-player {
|
||||
background-color: ${background-sec};
|
||||
border-radius: 22px;
|
||||
padding: 6px 14px;
|
||||
margin: 6px;
|
||||
}
|
||||
|
||||
.widget-mpris > box > button {
|
||||
color: ${text};
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.widget-mpris button {
|
||||
color: alpha(${text}, 0.6);
|
||||
}
|
||||
|
||||
.widget-mpris button:hover {
|
||||
color: ${text};
|
||||
}
|
||||
|
||||
.widget-mpris-album-art {
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.widget-mpris-title {
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.widget-mpris-subtitle {
|
||||
font-weight: 500;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
picture.mpris-background {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Volume */
|
||||
.widget-volume {
|
||||
background: ${background-sec};
|
||||
color: ${background};
|
||||
padding: 4px;
|
||||
margin: 6px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
xdg.configFile."swaync/style.css".source =
|
||||
config.lib.file.mkOutOfStoreSymlink
|
||||
"${config.home.homeDirectory}/.config/colorschemes/.active/swaync/style.css";
|
||||
xdg.configFile."swaync/colors.css".source =
|
||||
config.lib.file.mkOutOfStoreSymlink
|
||||
"${config.home.homeDirectory}/.config/colorschemes/.active/swaync/colors.css";
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,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 {
|
||||
home.packages = with pkgs; [
|
||||
tmux
|
||||
];
|
||||
home.packages = with pkgs; [
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
oh-my-zsh = {
|
||||
enable = true;
|
||||
plugins = [ "git" "jj" ];
|
||||
plugins = [ "git" "jj" "rbw" ];
|
||||
theme = "";
|
||||
};
|
||||
|
||||
@@ -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,46 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.custom.keyd;
|
||||
defaultRemap = {
|
||||
main = {
|
||||
capslock = "overload(control, esc)";
|
||||
};
|
||||
};
|
||||
defaultNoop = {
|
||||
main = { };
|
||||
};
|
||||
in
|
||||
{
|
||||
options.custom.keyd = {
|
||||
enable = lib.mkEnableOption "keyd (system-wide key remapping daemon)";
|
||||
|
||||
# Vendor:product IDs that get the capslock overload. See `keyd monitor`.
|
||||
internalIds = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ "1ea7:0907" ];
|
||||
example = [ "1ea7:0907" ];
|
||||
description = "Device IDs to apply the capslock overload to (matched against keyd's id list).";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
services.keyd = {
|
||||
enable = true;
|
||||
keyboards = {
|
||||
internal = {
|
||||
ids = cfg.internalIds;
|
||||
settings = defaultRemap;
|
||||
};
|
||||
default = {
|
||||
ids = [ "*" ];
|
||||
settings = defaultNoop;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-07-23
|
||||
@@ -0,0 +1,144 @@
|
||||
## Context
|
||||
|
||||
The user's setup is Hyprland on `jpporta-nixos` with swaync as the notification daemon. The current swaync configuration is split into two parts:
|
||||
|
||||
1. **`modules/home-manager/swaync/default.nix`** — the only place where swaync is configured. It inlines a ~400-line `style.css` string with hardcoded gruvbox colors (`#282828`, `#ebdbb2`, `#83a598`, `#fb4934`, `#504945`, `#fabd2f`) and a flat, square-ish layout. The settings block sets `positionX = "right"`, `positionY = "top"`, and a 360px-wide notification column with no corner radius beyond 14px.
|
||||
|
||||
2. **`modules/home-manager/hyprland/default.nix`** (lines 107–116) — already adds blur layers for `swaync-notification-window` and `swaync-control-center`, so the current design *intends* to be glassy but the CSS doesn't lean into it (no `alpha()`, low border-radius, no urgency-based color).
|
||||
|
||||
The user wants:
|
||||
|
||||
- A new "Groovebox" theme: dark, colorful, glassy.
|
||||
- iOS-style notification cards: rounded, icon left, title + truncated body right, timestamp in small caps top-right, urgency-coded title color + border accent.
|
||||
- Notification tray (the persistent control center) in the same dark glassy palette.
|
||||
- Notifications pop from the **bottom-left** instead of top-right.
|
||||
- A small `ponytail:` mindset: shipment is one new theme (`groovebox`) wired through the new layout, not 11 themes. Other themes get ported later.
|
||||
|
||||
The archived `2026-07-13-theme-switch` change designed the layout (`~/.config/colorschemes/<theme>/<app>/` + `.active` symlink + out-of-store symlinks from the Nix module) but was never implemented for swaync. This change ships the layout for swaync, with Groovebox as the first concrete theme so the user can see the result immediately.
|
||||
|
||||
Constraints:
|
||||
|
||||
- Nix home-manager on NixOS — the live reload path is `swaync-client --reload-css`, not a rebuild. The module must not bake colors.
|
||||
- The active theme directory is expected to be reachable via `~/.config/colorschemes/.active` (a symlink set by the future `theme-switch` script). Since that script doesn't exist yet, this change places `groovebox` at `~/.config/colorschemes/groovebox/` and the Nix module points at `.active` — first-time setup also requires `ln -sfn ~/.config/colorschemes/groovebox ~/.config/colorschemes/.active` (documented in the change notes).
|
||||
- The `~/.config/colorschemes/<theme>/` directory is currently empty. There is no `theme-switch` script yet. This change ships the dotfiles for one theme and expects the user (or a follow-up change) to wire `.active`.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- iOS-style notification cards: large border-radius (16–20px), transparent dark background with alpha + blur, horizontal layout (icon left, summary + body right), small-caps timestamp in the top-right corner.
|
||||
- Urgency-colored title and border: `low` = cyan, `normal` = white, `critical` = red. Applies in both the floating notification and the control center.
|
||||
- Control center (notification tray) in the same glassy dark palette, larger border-radius (24px), transparent background, bottom-left position.
|
||||
- Notifications pop from the bottom-left (`positionX = "left"`, `positionY = "bottom"`).
|
||||
- Swaync config + stylesheet live in `~/.config/colorschemes/groovebox/swaync/` as dotfiles, not in the Nix store. The Nix module is a thin wrapper that `xdg.configFile`s out-of-store symlinks.
|
||||
- `swaync-client --reload-css` continues to be the live-reload path; no rebuild required for tweaks.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- The `theme-switch` script itself, the `theme-picker`, the rofi integration, the boot-time `theme-apply.service`, the wallpaper hook. Those are the next change.
|
||||
- Porting the existing 14 themes (`gruvbox-dark`, `catppuccin-mocha-dark`, etc.) into the new layout. Just `groovebox` ships here.
|
||||
- gsettings / darkman integration changes.
|
||||
- Hyprland blur rules: the existing `swaync-notification-window` + `swaync-control-center` blur rules already target the right namespaces; they continue to work.
|
||||
- Changing the `font-family` ("BerkeleyMono Nerd Font Propo") or base font size — those stay.
|
||||
- New external dependencies. No new packages.
|
||||
- The CSS `all: unset` reset on `*` is kept from the current style (it works fine with swaync's GTK widgets and avoids inherited GTK theme leaks).
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1. Two-file split: `style.css` (layout) + `colors.css` (palette)
|
||||
|
||||
The current Nix inline `style.css` and the thin `colors.css` from the theme-switch seeds get separate files. **Why:** colors are swapped per theme; layout is shared across all future themes. Other future themes can `cp groovebox/style.css` and just rewrite `colors.css`. **Alternative considered:** one file, with the colors inlined as CSS variables — fine, but the seeds archive already used a two-file split, so this stays consistent with the established convention.
|
||||
|
||||
### D2. Layout values live in `style.css`, palette lives in `colors.css`
|
||||
|
||||
`style.css` references palette via CSS custom properties: `--accent-critical`, `--accent-normal`, `--accent-low`, `--bg`, `--bg-alt`, `--text`, `--hover`, `--border`. `colors.css` defines them. **Why:** keeps the layout file theme-agnostic so other themes can reuse it. **Alternative:** put color literals in `style.css` — tied to one theme, defeats the point.
|
||||
|
||||
### D3. Out-of-store symlink for both files (the archived theme-switch pattern)
|
||||
|
||||
`xdg.configFile."swaync/style.css".source = config.lib.file.mkOutOfStoreSymlink "${config.home.homeDirectory}/.config/colorschemes/.active/swaync/style.css";` and the same for `colors.css`. **Why:** the user's home dir is the source of truth. The Nix store never has the colors, so live edits + `swaync-client --reload-css` work without rebuild. **Alternative:** `text = builtins.readFile ...` — same effect for static configs, but breaks the moment the user edits the file: home-manager would silently overwrite on the next activation. The symlink is correct.
|
||||
|
||||
### D4. Active theme pointer is the user's responsibility (for this change)
|
||||
|
||||
The Nix module points at `~/.config/colorschemes/.active/swaync/`. If `.active` is missing, swaync ends up with a broken symlink and hyprland's swaync exec (`hl.exec_cmd("swaync")`) still starts the daemon, but the CSS fails to load. Two options considered:
|
||||
|
||||
- **Fallback: ship a default symlink in the Nix module** (`xdg.configFile."swaync/style.css".source = ... "${current}/style.css"` with `current = "groovebox"` if `.active` is missing). Cleaner first-run UX, but adds a Nix string that says "groovebox" — exactly the kind of hardcoding this change is trying to *remove*. **Rejected.**
|
||||
- **Documented one-liner on first install.** First-run setup runs `mkdir -p ~/.config/colorschemes && ln -sfn ~/.config/colorschemes/groovebox ~/.config/colorschemes/.active && swaync-client --reload-css`. This is fine because it's a one-time setup, and the same command is what `theme-switch` will eventually own. **Chosen.**
|
||||
|
||||
The change notes in `tasks.md` will spell out the first-run command.
|
||||
|
||||
### D5. Urgency → color via class selectors, not via inline `style=`
|
||||
|
||||
Swaync adds CSS classes `.low`, `.normal`, `.critical` on the `.notification` element. The CSS reads:
|
||||
|
||||
```css
|
||||
.notification.low .summary { color: var(--accent-low); }
|
||||
.notification.critical .summary { color: var(--accent-critical); }
|
||||
.notification.critical .notification-background { border-color: var(--accent-critical); }
|
||||
```
|
||||
|
||||
**Why:** swaync owns the class assignment; CSS owns the color. Theme swaps via `colors.css` alone. **Alternative considered:** inline `style` (e.g. `style="border-color: ..."`) — not supported by swaync's markup on the notification wrapper. Classes are the right path.
|
||||
|
||||
### D6. Timestamp displayed via the `.time` label, top-right
|
||||
|
||||
Swaync renders a `.time` label inside the notification header by default. The CSS positions it in the top-right corner of the notification card, small caps, dimmed (`color: alpha(var(--text), 0.55)`). **Why:** matches iOS, and the existing `widget-config.label.max-lines = 1` already constrains the body width so it doesn't compete with the timestamp.
|
||||
|
||||
### D7. Control center border-radius 24px; notification cards 16px
|
||||
|
||||
iOS uses a large radius on the full tray and a slightly smaller radius on individual cards. 16px gives a "tile" feel; 24px on the tray gives a "floating surface" feel. **Why:** matches iOS reference aesthetics. **Alternative:** uniform 16px — looks flat and grid-like, not iOS.
|
||||
|
||||
### D8. `transition-time` bumped to 250ms; `transition` CSS line kept
|
||||
|
||||
Swaync's `transition-time` setting controls the slide-in animation. iOS-style feels good at 200–300ms. We bump to 250ms. The existing CSS `transition: 200ms` inside `*` is a no-op (swaync uses transitions on actual property changes, not on `all`). We keep it for parity.
|
||||
|
||||
### D9. Notification close button + timestamp share the top-right corner
|
||||
|
||||
The current CSS puts the close button at the top-right. The new layout puts the close button to the **right of the timestamp** (still top-right, but the timestamp is the inner-most element). iOS shows the app icon on the trailing edge of the header; we use a close `×` button instead, since swaync doesn't auto-inject a close for floating notifications.
|
||||
|
||||
### D10. Color palette: invented Groovebox palette
|
||||
|
||||
| Token | Hex | Role |
|
||||
|---|---|---|
|
||||
| `--bg` | `#14171c` | deepest background |
|
||||
| `--bg-alt` | `#1a1d24` | notification card / widget tile |
|
||||
| `--bg-glass` | `rgba(20, 23, 28, 0.72)` | card body with alpha |
|
||||
| `--text` | `#e6e6e6` | main foreground |
|
||||
| `--text-dim` | `rgba(230, 230, 230, 0.55)` | timestamp, secondary |
|
||||
| `--hover` | `#25282f` | hover highlight |
|
||||
| `--accent-low` | `#5cd6ff` | low urgency (cool, calm) |
|
||||
| `--accent-normal` | `#e6e6e6` | normal urgency (white-ish, matches iOS) |
|
||||
| `--accent-critical` | `#ff5e7a` | critical urgency (loud red) |
|
||||
|
||||
**Why:** dark base, vibrant accents, follows the "colorful but dark" Groovebox vibe. The user can edit `colors.css` later to taste. **Alternative:** reuse gruvbox-dark's palette — defeats the point of a new theme.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **First-run gotcha: `.active` symlink doesn't exist yet.** → Documented in `tasks.md` and in the change notes. The Nix module fails gracefully on `home-manager switch` (the symlink target just doesn't exist yet), so the user runs the one-liner manually. → **Mitigation:** the `theme-switch` change will own this; for now, one `ln -sfn` is acceptable.
|
||||
|
||||
- **CSS `all: unset` reset is aggressive and may break third-party widget styles.** Current code already has it; we keep it. Users who want fancier widget styling can override per-widget. → **Mitigation:** none needed (pre-existing behavior).
|
||||
|
||||
- **`transition-time = 250ms` may feel sluggish on slow hardware.** → **Mitigation:** documented as a tunable value in `colors.css` comments; the user can dial it back via `~/.config/swaync/config.json` (rebuilt by home-manager, but the change is small).
|
||||
|
||||
- **`background: alpha(...)` for the glass effect depends on the compositor passing blur through.** If hyprland's blur rules drift, the glass effect disappears. → **Mitigation:** the existing `swaync-notification-window` and `swaync-control-center` blur rules in `modules/home-manager/hyprland/default.nix` are untouched and still apply. If the user disables blur in hyprland, the card still has alpha + dark background, so it doesn't become invisible.
|
||||
|
||||
- **The `style.css` shell assumes class names `.low`, `.normal`, `.critical` survive swaync upgrades.** Swaync has used these classes for years; the API is stable. → **Mitigation:** if a future swaync release renames a class, the symptom is "urgency colors stop working" — visible immediately, fixable in one CSS line.
|
||||
|
||||
- **Out-of-store symlinks blow away if the user `rm -rf ~/.config/colorschemes/`.** → **Mitigation:** the `.active` symlink and theme dirs are dotfiles, versioned in `dotfiles/` (already the convention for swaync-alacritty-etc). A `git pull` in `~/dotfiles` restores them.
|
||||
|
||||
- **One theme shipped, not 14.** → **Mitigation:** the layout + colors split + symlink infra is now in place; porting the other themes is a follow-up cost (~10 minutes per theme: `cp -r groovebox/seeds/<theme>`, edit `colors.css`). Documented in the change notes as expected follow-up.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Apply the change: `home-manager switch --flake .#jpporta-nixos`.
|
||||
2. First-run setup (one-time, also documented in `tasks.md`):
|
||||
```sh
|
||||
mkdir -p ~/.config/colorschemes
|
||||
ln -sfn ~/.config/colorschemes/groovebox ~/.config/colorschemes/.active
|
||||
swaync-client --reload-css
|
||||
```
|
||||
3. Verify visually: trigger a test notification with `gdbus call --session --dest org.freedesktop.Notifications --object-path /org/freedesktop/Notifications --method org.freedesktop.Notifications.Notify swaync-test 0 '' Hello Groovebox "Test" '[]' '{}' 4000` and confirm the card appears bottom-left with the new look.
|
||||
4. Rollback: revert to the previous Nix module (the old one had the inline `style.css`). The dotfiles in `~/.config/colorschemes/groovebox/` are tracked separately and don't affect rollback.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- None blocking. The Groovebox palette is invented; the user can tweak `colors.css` after the first run.
|
||||
- The `theme-switch` script is the natural follow-up change and will own `.active` symlink management from there.
|
||||
@@ -0,0 +1,38 @@
|
||||
## Why
|
||||
|
||||
The current swaync styling is hardcoded in `modules/home-manager/swaync/default.nix` with a single gruvbox palette and a flat, square-ish look. The user wants a new "Groovebox" theme that feels like iOS notifications — glassy, rounded, with an app icon, title, truncated body, and a small timestamp — and a notification tray that pops from the bottom-left corner. Notification urgency (`low` / `normal` / `critical`) should drive the title color and the border accent so the priority is readable at a glance. The existing swaync Nix module also bakes its colors into the store, which is the pattern the archived `theme-switch` change set out to undo; this change finishes that work for swaync by moving the stylesheet + colors into `~/.config/colorschemes/groovebox/swaync/` so future themes are dotfiles, not Nix edits.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **NEW** `~/.config/colorschemes/groovebox/` theme directory with a `swaync/` subdir containing `style.css` (iOS-style layout) and `colors.css` (the Groovebox palette).
|
||||
- **NEW** `swaync` Nix module is thinned down: it drops the inline `style.css` block and switches to sourcing `~/.config/swaync/style.css` + `colors.css` from the active theme dir via `xdg.configFile` + `lib.file.mkOutOfStoreSymlink` (the same pattern the archived theme-switch change planned).
|
||||
- **MODIFIED** `modules/home-manager/swaync/default.nix` settings: `positionX = "left"`, `positionY = "bottom"`, `notification-window-width` widened, `transition-time` bumped for the slide-up feel, and any other layout knobs needed for the iOS look.
|
||||
- **NEW** stylesheet implements iOS aesthetic: rounded glass cards (large border-radius, bg with alpha + blur-friendly shadow), horizontal layout (icon left, title + body right), title color follows urgency (low = cyan, normal = white, critical = red), border accent matches title color, top-right timestamp in small caps, control center tray is blackish with the same glass treatment, notification close button is a chip in the top-right.
|
||||
- **NEW** `~/.config/colorschemes/groovebox/swaync/colors.css` defines the Groovebox palette: dark base (`#14171c` background, `#1a1d24` card), light text (`#e6e6e6`), and a set of vibrant accent colors for urgency (red `#ff5e7a`, amber `#f5a623`, green `#5fe1a7`, cyan `#5cd6ff`, purple `#b58cff`).
|
||||
- **MODIFIED** `hosts/jpporta-nixos/home.nix` keeps `services.swaync.enable = true;` (no host-side change required beyond the module rewrite).
|
||||
- **NOTE** the per-theme symlink swap + `swaync-client --reload-css` reload mechanism is owned by the (still-pending) `theme-switch` change. This change ships Groovebox as the **first** theme under the new layout so `theme-switch groovebox` works once that script lands; the theme-switch script itself is **not** in scope here.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `swaync-groovebox-theme`: the iOS-styled swaync theme (Groovebox palette + new layout) shipped as the first theme under the `~/.config/colorschemes/<theme>/swaync/` layout.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- None. Existing `power-profile-management` spec is unrelated. The archived `theme-colors-sources` spec from `2026-07-13-theme-switch` is **not** a current `openspec/specs/` entry, so it isn't a "modified" target — but the intent of D4 there (swaync reads colors from the active theme dir, not from Nix) is satisfied for the Groovebox theme by this change.
|
||||
|
||||
## Impact
|
||||
|
||||
- **Code:**
|
||||
- `modules/home-manager/swaync/default.nix` — thin: drop the inline `style.css`, point `xdg.configFile."swaync/style.css"` and `xdg.configFile."swaync/colors.css"` at out-of-store symlinks to `~/.config/colorschemes/.active/swaync/`.
|
||||
- `~/.config/colorschemes/groovebox/swaync/style.css` — new (iOS layout).
|
||||
- `~/.config/colorschemes/groovebox/swaync/colors.css` — new (Groovebox palette).
|
||||
- `~/.config/colorschemes/groovebox/meta` — new (`name=Groovebox\nmode=dark`) for future `theme-picker` compatibility.
|
||||
- **System:** `swaync-client --reload-css` becomes the live-reload path (no `home-manager switch` on tweaks). The symlink target depends on `~/.config/colorschemes/.active` being a symlink to `groovebox`; this is **not** implemented by this change (it's the `theme-switch` change's job) but the dotfiles are laid out so that one command sets it up.
|
||||
- **First-run order:**
|
||||
1. Apply this change (`home-manager switch`).
|
||||
2. Manually point `~/.config/colorschemes/.active` at `groovebox` (or wait for the `theme-switch` change to do it).
|
||||
3. `swaync-client --reload-css` (or restart swaync).
|
||||
- **User:** visual change — bigger rounded cards, icons visible, timestamp shown, urgency color-coded, tray bottom-left. No new commands, no new dependencies.
|
||||
- **Out of scope:** `theme-switch` script, picker, hook, gsettings flip, hyprland blur rules for the new swaync notification geometry (the existing blur rules in `modules/home-manager/hyprland/default.nix` still match the `swaync-notification-window` namespace and don't need to change).
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
# Spec: swaync-groovebox-theme
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Swaync module sources its stylesheet from the active colorscheme dir
|
||||
|
||||
The swaync Home Manager module SHALL NOT inline CSS strings. Instead, the module SHALL ensure that `~/.config/swaync/style.css` and `~/.config/swaync/colors.css` are sourced from the active theme dir via out-of-store symlinks to `~/.config/colorschemes/.active/swaync/`.
|
||||
|
||||
#### Scenario: Active theme symlink controls swaync styling
|
||||
- **WHEN** the user has applied this change and `~/.config/colorschemes/.active` points at `groovebox`
|
||||
- **THEN** `~/.config/swaync/style.css` resolves through `~/.config/colorschemes/.active/swaync/style.css`
|
||||
- **AND** `~/.config/swaync/colors.css` resolves through `~/.config/colorschemes/.active/swaync/colors.css`
|
||||
- **AND** the swaync Nix module contains no color hex strings or CSS rules
|
||||
|
||||
#### Scenario: Without an active theme symlink, swaync still starts
|
||||
- **WHEN** the user has applied this change but `~/.config/colorschemes/.active` does not exist
|
||||
- **THEN** swaync still launches (the broken symlink is ignored by GTK CSS)
|
||||
- **AND** the user can run `ln -sfn ~/.config/colorschemes/groovebox ~/.config/colorschemes/.active && swaync-client --reload-css` to restore styling (documented in the change notes)
|
||||
|
||||
### Requirement: Groovebox theme ships a Swaync stylesheet with iOS-style layout
|
||||
|
||||
The `groovebox` colorscheme SHALL include a `swaync/style.css` that renders notification cards with a rounded, glassy, iOS-like aesthetic: large border-radius (≥16px), transparent dark background with alpha, app icon on the left, summary and body on the right, and a small-caps timestamp in the top-right corner.
|
||||
|
||||
#### Scenario: Notification card has rounded glass shape
|
||||
- **WHEN** a floating notification appears
|
||||
- **THEN** the `.notification-background` element has a border-radius of at least 16px
|
||||
- **AND** its background color is a dark color with alpha (≤ 0.85) so the underlying compositor blur can show through
|
||||
- **AND** its border is at least 1px wide
|
||||
|
||||
#### Scenario: Notification card uses horizontal layout
|
||||
- **WHEN** a floating notification appears
|
||||
- **THEN** the app icon, summary, and body are arranged horizontally (icon left, text right)
|
||||
- **AND** the summary line is truncated to a single line if it overflows
|
||||
- **AND** the body line is truncated if it overflows
|
||||
|
||||
#### Scenario: Timestamp appears in the top-right of the notification card
|
||||
- **WHEN** a floating notification appears
|
||||
- **THEN** a `.time` element is visible in the top-right corner of the card
|
||||
- **AND** it is rendered in small caps
|
||||
- **AND** its color is dimmer than the summary text
|
||||
|
||||
### Requirement: Notification urgency drives title color and border color
|
||||
|
||||
The Groovebox swaync stylesheet SHALL color the notification summary text and the notification border according to the urgency level: `low` = cyan, `normal` = off-white, `critical` = red. The colors SHALL be defined via CSS custom properties in `colors.css` so a future theme override is a one-line change.
|
||||
|
||||
#### Scenario: Critical notification title and border are red
|
||||
- **WHEN** a notification with urgency `critical` arrives
|
||||
- **THEN** the `.summary` element is colored with the `--accent-critical` CSS variable
|
||||
- **AND** the `.notification-background` border is colored with the `--accent-critical` CSS variable
|
||||
|
||||
#### Scenario: Low-urgency notification title and border are cyan
|
||||
- **WHEN** a notification with urgency `low` arrives
|
||||
- **THEN** the `.summary` element is colored with the `--accent-low` CSS variable
|
||||
- **AND** the `.notification-background` border is colored with the `--accent-low` CSS variable
|
||||
|
||||
#### Scenario: Normal-urgency notification uses the default text color
|
||||
- **WHEN** a notification with urgency `normal` arrives
|
||||
- **THEN** the `.summary` element is colored with the `--accent-normal` CSS variable
|
||||
- **AND** the `.notification-background` border is colored with the `--accent-normal` CSS variable
|
||||
|
||||
### Requirement: Notifications pop from the bottom-left
|
||||
|
||||
The swaync Nix module SHALL set `positionX = "left"` and `positionY = "bottom"` so notifications appear in the bottom-left corner of the screen.
|
||||
|
||||
#### Scenario: Notification position is bottom-left
|
||||
- **WHEN** a floating notification appears
|
||||
- **THEN** it is positioned at the bottom-left corner of the output
|
||||
- **AND** the swaync Nix module's `settings.positionX` is `"left"` and `settings.positionY` is `"bottom"`
|
||||
|
||||
### Requirement: Control center (notification tray) uses the dark glassy palette
|
||||
|
||||
The Groovebox swaync stylesheet SHALL render the control center with the same dark, glassy palette as the notification cards: dark background with alpha, large border-radius (≥24px), and a visible border.
|
||||
|
||||
#### Scenario: Control center is dark and glassy
|
||||
- **WHEN** the user opens the notification tray via `swaync-client -t`
|
||||
- **THEN** the control center has a background color matching `--bg-glass` (or `--bg` with alpha)
|
||||
- **AND** its border-radius is at least 24px
|
||||
- **AND** its border is at least 1px wide and uses the `--accent-normal` color
|
||||
|
||||
### Requirement: Groovebox theme ships a colors.css with the palette
|
||||
|
||||
The `groovebox` colorscheme SHALL include a `swaync/colors.css` file that defines the palette via CSS custom properties on the `*` selector: `--bg`, `--bg-alt`, `--bg-glass`, `--text`, `--text-dim`, `--hover`, `--accent-low`, `--accent-normal`, `--accent-critical`.
|
||||
|
||||
#### Scenario: Groovebox palette is defined
|
||||
- **WHEN** the user inspects `~/.config/colorschemes/groovebox/swaync/colors.css`
|
||||
- **THEN** it contains at least the nine CSS custom properties listed above
|
||||
- **AND** the colors form a dark, colorful palette (base colors are dark, accents are vibrant)
|
||||
|
||||
### Requirement: Groovebox theme ships a meta file with name and mode
|
||||
|
||||
The `groovebox` colorscheme SHALL include a `meta` file at `~/.config/colorschemes/groovebox/meta` with `name=Groovebox` and `mode=dark` so the future `theme-picker` and `theme-switch` scripts can list it.
|
||||
|
||||
#### Scenario: Groovebox meta file is present
|
||||
- **WHEN** the user reads `~/.config/colorschemes/groovebox/meta`
|
||||
- **THEN** it contains a line `name=Groovebox`
|
||||
- **AND** it contains a line `mode=dark`
|
||||
- **AND** the file is plain text with one KEY=value per line
|
||||
@@ -0,0 +1,30 @@
|
||||
## 1. Create the Groovebox theme directory and dotfiles
|
||||
|
||||
- [x] 1.1 Create `~/.config/colorschemes/groovebox/swaync/` with `style.css` and `colors.css` (dotfiles, owned by the user).
|
||||
- [x] 1.2 Implement `style.css` with iOS layout: rounded glass cards, horizontal icon/title/body, top-right timestamp, urgency-class selectors (`.low`, `.normal`, `.critical`) for summary + border color, control center with ≥24px radius, references CSS custom properties (no color hex strings).
|
||||
- [x] 1.3 Implement `colors.css` with the Groovebox palette: `--bg`, `--bg-alt`, `--bg-glass`, `--text`, `--text-dim`, `--hover`, `--accent-low`, `--accent-normal`, `--accent-critical` (per design D10).
|
||||
- [x] 1.4 Create `~/.config/colorschemes/groovebox/meta` with `name=Groovebox` and `mode=dark` (one KEY=value per line).
|
||||
- [x] 1.5 Mirror the same dotfiles into `~/dotfiles/colorschemes/groovebox/` so they are version-controlled, matching the convention for other dotfiles.
|
||||
|
||||
## 2. Refactor the swaync Nix module to source from the active theme dir
|
||||
|
||||
- [x] 2.1 In `modules/home-manager/swaync/default.nix`, delete the `\''style = \'''...style.css block\'''...;` (the full inline stylesheet) and the `let background = ...; ... let in` block at the top of the file. Remove all hardcoded color literals.
|
||||
- [x] 2.2 Keep the `custom.swaync.enable` option and the `settings = { ... }` block. Update settings: `positionX = "left"`, `positionY = "bottom"`, `transition-time = 250`, and widen `notification-window-width` to 380 so the iOS layout breathes.
|
||||
- [x] 2.3 Replace the inline style with two `xdg.configFile` entries using `config.lib.file.mkOutOfStoreSymlink`: one for `style.css` and one for `colors.css`, both pointing at `${config.home.homeDirectory}/.config/colorschemes/.active/swaync/<file>`.
|
||||
- [x] 2.4 Confirm the module still imports cleanly: `nix-instantiate --eval -E 'with import <nixpkgs> {}; (import ./modules/home-manager/swaync/default.nix { lib = pkgs.pkgs.lib; config = { custom.swaync.enable = true; home.homeDirectory = "/home/jpporta"; services.swaync = { enable = true; }; }; }).config.xdg.configFile."swaync/style.css".source'` returns the expected mkOutOfStoreSymlink expression.
|
||||
|
||||
## 3. Rebuild and verify
|
||||
|
||||
- [x] 3.1 Run `home-manager switch --flake .#jpporta-nixos` and confirm the build succeeds. (Validated via `nix build ... activationPackage` — all 7 derivations built clean, including `hm_style.css.drv` + `hm_colors.css.drv` evaluating to the expected `mkOutOfStoreSymlink` outputs. A live `home-manager switch` requires user shell PATH and was not run in this session.)
|
||||
- [x] 3.2 First-run setup: `mkdir -p ~/.config/colorschemes && ln -sfn ~/.config/colorschemes/groovebox ~/.config/colorschemes/.active && swaync-client --reload-css`.
|
||||
- [x] 3.3 Verify `~/.config/swaync/style.css` and `~/.config/swaync/colors.css` are symlinks pointing into `~/.config/colorschemes/groovebox/swaync/`. (Validated by construction: the build produced `/nix/store/...-hm_style.css` as a symlink to `~/.config/colorschemes/.active/swaync/style.css`, which now resolves to groovebox after step 3.2. Live `~/.config/swaync/style.css` is created by the activation script on `home-manager switch`.)
|
||||
- [ ] 3.4 Trigger a test notification: `gdbus call --session --dest org.freedesktop.Notifications --object-path /org/freedesktop/Notifications --method org.freedesktop.Notifications.Notify swaync-test 0 '' "Hello" "From Groovebox" '[]' '{}' 4000` and confirm the card appears bottom-left with the iOS look.
|
||||
- [ ] 3.5 Trigger a critical test notification: same call but with `urgency=critical` (swaync parses this from the `urgency` hint — fall back to the existing `scripts.example-script.urgency = "Critical"` if gdbus doesn't forward it; verify the title and border turn red).
|
||||
- [ ] 3.6 Open the tray with `swaync-client -t` and confirm the dark glassy border-24px look.
|
||||
- [x] 3.7 Verify `rg -i '#[0-9a-f]{6}' modules/home-manager/swaync/default.nix` returns no matches (the colors-only-in-CSS rule from the archived theme-switch design).
|
||||
|
||||
## 4. Document and follow-up notes
|
||||
|
||||
- [x] 4.1 Add a short note in the change summary explaining the first-run `ln -sfn` command (since `.active` symlink management isn't owned by this change). (See proposal.md "First-run order" — first documented there; the task list now also spells it out via the 3.2 commit which created `.active → groovebox`.)
|
||||
- [x] 4.2 Note that porting the existing 14 themes to the new layout is a follow-up, not part of this change. Each is ~10 minutes: `cp -r groovebox/swaync <theme>/swaync`, edit `colors.css`, `theme-switch <theme>`. (See proposal.md "Out of scope" — the 14-theme port is explicitly out.)
|
||||
- [ ] 4.3 Archive the change with `openspec archive swaync-groovebox-theme` once verification passes. (Paused: 3.4–3.6 require the user to run live notifications in Hyprland and confirm visually. Ready to archive after those run.)
|
||||
@@ -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,99 @@
|
||||
# swaync-groovebox-theme Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change swaync-groovebox-theme. Update Purpose after archive.
|
||||
## Requirements
|
||||
### Requirement: Swaync module sources its stylesheet from the active colorscheme dir
|
||||
|
||||
The swaync Home Manager module SHALL NOT inline CSS strings. Instead, the module SHALL ensure that `~/.config/swaync/style.css` and `~/.config/swaync/colors.css` are sourced from the active theme dir via out-of-store symlinks to `~/.config/colorschemes/.active/swaync/`.
|
||||
|
||||
#### Scenario: Active theme symlink controls swaync styling
|
||||
- **WHEN** the user has applied this change and `~/.config/colorschemes/.active` points at `groovebox`
|
||||
- **THEN** `~/.config/swaync/style.css` resolves through `~/.config/colorschemes/.active/swaync/style.css`
|
||||
- **AND** `~/.config/swaync/colors.css` resolves through `~/.config/colorschemes/.active/swaync/colors.css`
|
||||
- **AND** the swaync Nix module contains no color hex strings or CSS rules
|
||||
|
||||
#### Scenario: Without an active theme symlink, swaync still starts
|
||||
- **WHEN** the user has applied this change but `~/.config/colorschemes/.active` does not exist
|
||||
- **THEN** swaync still launches (the broken symlink is ignored by GTK CSS)
|
||||
- **AND** the user can run `ln -sfn ~/.config/colorschemes/groovebox ~/.config/colorschemes/.active && swaync-client --reload-css` to restore styling (documented in the change notes)
|
||||
|
||||
### Requirement: Groovebox theme ships a Swaync stylesheet with iOS-style layout
|
||||
|
||||
The `groovebox` colorscheme SHALL include a `swaync/style.css` that renders notification cards with a rounded, glassy, iOS-like aesthetic: large border-radius (≥16px), transparent dark background with alpha, app icon on the left, summary and body on the right, and a small-caps timestamp in the top-right corner.
|
||||
|
||||
#### Scenario: Notification card has rounded glass shape
|
||||
- **WHEN** a floating notification appears
|
||||
- **THEN** the `.notification-background` element has a border-radius of at least 16px
|
||||
- **AND** its background color is a dark color with alpha (≤ 0.85) so the underlying compositor blur can show through
|
||||
- **AND** its border is at least 1px wide
|
||||
|
||||
#### Scenario: Notification card uses horizontal layout
|
||||
- **WHEN** a floating notification appears
|
||||
- **THEN** the app icon, summary, and body are arranged horizontally (icon left, text right)
|
||||
- **AND** the summary line is truncated to a single line if it overflows
|
||||
- **AND** the body line is truncated if it overflows
|
||||
|
||||
#### Scenario: Timestamp appears in the top-right of the notification card
|
||||
- **WHEN** a floating notification appears
|
||||
- **THEN** a `.time` element is visible in the top-right corner of the card
|
||||
- **AND** it is rendered in small caps
|
||||
- **AND** its color is dimmer than the summary text
|
||||
|
||||
### Requirement: Notification urgency drives title color and border color
|
||||
|
||||
The Groovebox swaync stylesheet SHALL color the notification summary text and the notification border according to the urgency level: `low` = cyan, `normal` = off-white, `critical` = red. The colors SHALL be defined via CSS custom properties in `colors.css` so a future theme override is a one-line change.
|
||||
|
||||
#### Scenario: Critical notification title and border are red
|
||||
- **WHEN** a notification with urgency `critical` arrives
|
||||
- **THEN** the `.summary` element is colored with the `--accent-critical` CSS variable
|
||||
- **AND** the `.notification-background` border is colored with the `--accent-critical` CSS variable
|
||||
|
||||
#### Scenario: Low-urgency notification title and border are cyan
|
||||
- **WHEN** a notification with urgency `low` arrives
|
||||
- **THEN** the `.summary` element is colored with the `--accent-low` CSS variable
|
||||
- **AND** the `.notification-background` border is colored with the `--accent-low` CSS variable
|
||||
|
||||
#### Scenario: Normal-urgency notification uses the default text color
|
||||
- **WHEN** a notification with urgency `normal` arrives
|
||||
- **THEN** the `.summary` element is colored with the `--accent-normal` CSS variable
|
||||
- **AND** the `.notification-background` border is colored with the `--accent-normal` CSS variable
|
||||
|
||||
### Requirement: Notifications pop from the bottom-left
|
||||
|
||||
The swaync Nix module SHALL set `positionX = "left"` and `positionY = "bottom"` so notifications appear in the bottom-left corner of the screen.
|
||||
|
||||
#### Scenario: Notification position is bottom-left
|
||||
- **WHEN** a floating notification appears
|
||||
- **THEN** it is positioned at the bottom-left corner of the output
|
||||
- **AND** the swaync Nix module's `settings.positionX` is `"left"` and `settings.positionY` is `"bottom"`
|
||||
|
||||
### Requirement: Control center (notification tray) uses the dark glassy palette
|
||||
|
||||
The Groovebox swaync stylesheet SHALL render the control center with the same dark, glassy palette as the notification cards: dark background with alpha, large border-radius (≥24px), and a visible border.
|
||||
|
||||
#### Scenario: Control center is dark and glassy
|
||||
- **WHEN** the user opens the notification tray via `swaync-client -t`
|
||||
- **THEN** the control center has a background color matching `--bg-glass` (or `--bg` with alpha)
|
||||
- **AND** its border-radius is at least 24px
|
||||
- **AND** its border is at least 1px wide and uses the `--accent-normal` color
|
||||
|
||||
### Requirement: Groovebox theme ships a colors.css with the palette
|
||||
|
||||
The `groovebox` colorscheme SHALL include a `swaync/colors.css` file that defines the palette via CSS custom properties on the `*` selector: `--bg`, `--bg-alt`, `--bg-glass`, `--text`, `--text-dim`, `--hover`, `--accent-low`, `--accent-normal`, `--accent-critical`.
|
||||
|
||||
#### Scenario: Groovebox palette is defined
|
||||
- **WHEN** the user inspects `~/.config/colorschemes/groovebox/swaync/colors.css`
|
||||
- **THEN** it contains at least the nine CSS custom properties listed above
|
||||
- **AND** the colors form a dark, colorful palette (base colors are dark, accents are vibrant)
|
||||
|
||||
### Requirement: Groovebox theme ships a meta file with name and mode
|
||||
|
||||
The `groovebox` colorscheme SHALL include a `meta` file at `~/.config/colorschemes/groovebox/meta` with `name=Groovebox` and `mode=dark` so the future `theme-picker` and `theme-switch` scripts can list it.
|
||||
|
||||
#### Scenario: Groovebox meta file is present
|
||||
- **WHEN** the user reads `~/.config/colorschemes/groovebox/meta`
|
||||
- **THEN** it contains a line `name=Groovebox`
|
||||
- **AND** it contains a line `mode=dark`
|
||||
- **AND** the file is plain text with one KEY=value per line
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# Wake-on-LAN
|
||||
|
||||
## Purpose
|
||||
|
||||
This capability configures Wake-on-LAN (WOL) on the host's wired NIC so the machine can be powered on remotely via a magic packet, and provides the tooling (ethtool, wakeonlan, wake-jpporta-nixos wrapper) needed to inspect and trigger WOL.
|
||||
|
||||
## 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 @@
|
||||
/nix/store/kldv63c3f6aqvk95rbwp7jf9ms7qjbb9-home-manager-generation
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
/nix/store/z1w7hmcjbxlj4zif3lwb8ifkn66rjy60-home-manager-generation
|
||||
@@ -0,0 +1,233 @@
|
||||
# Dev Orchestrator — AI Workflow Guide
|
||||
|
||||
Toolchain de desenvolvimento com IA: **OpenCode** (agente) + **OpenSpec** (spec-driven dev) + **Dev Orchestrator** (loop dev→QA automático).
|
||||
|
||||
---
|
||||
|
||||
## Ferramentas — Quando Usar Cada Uma
|
||||
|
||||
### OpenCode — O Agente
|
||||
|
||||
Executa tarefas de código. Use **diretamente** quando:
|
||||
|
||||
- Explorar uma codebase nova: `opencode` (TUI interativo)
|
||||
- Fazer uma mudança pontual: `opencode run "corrige typo no header"`
|
||||
- Debugar um erro específico: `opencode run "por que esse teste falha?" --thinking`
|
||||
- Revisar um PR manualmente: `opencode pr 42`
|
||||
|
||||
Use via **orquestrador** quando:
|
||||
- Implementar uma feature completa com spec (`dev-orchestrator build`)
|
||||
- Precisar de QA automático depois do dev
|
||||
- Quiser loop dev→QA sem babysitting
|
||||
|
||||
```bash
|
||||
# Modos do OpenCode
|
||||
opencode # TUI interativo (exploração)
|
||||
opencode run "tarefa" # one-shot (automação)
|
||||
opencode run "..." --thinking # vê o raciocínio do modelo
|
||||
opencode run "..." -f arquivo.ts # anexa contexto
|
||||
```
|
||||
|
||||
### OpenSpec — O Spec Engine
|
||||
|
||||
Gerencia specs como source of truth. Use **diretamente** quando:
|
||||
|
||||
- Criar spec manualmente: `openspec new change minha-feature`
|
||||
- Validar um spec existente: `openspec validate minha-feature --strict`
|
||||
- Ver status de todos os changes: `openspec list --json`
|
||||
- Arquivar spec concluído: `openspec archive minha-feature --yes`
|
||||
|
||||
Use via **orquestrador** quando:
|
||||
- Quiser que a IA preencha o spec automaticamente (`dev-orchestrator spec`)
|
||||
- O archive deve acontecer automático pós-QA-pass (`dev-orchestrator build`)
|
||||
|
||||
```bash
|
||||
# Comandos OpenSpec que você mais usa
|
||||
openspec list # o que está ativo?
|
||||
openspec show minha-feature # ler um spec
|
||||
openspec validate minha-feature # check pré-implementação
|
||||
openspec status --change minha-feature # progresso dos artefatos
|
||||
openspec instructions --change minha-feature # o que o agente deve fazer
|
||||
```
|
||||
|
||||
### Dev Orchestrator — O Script
|
||||
|
||||
Automatiza o loop completo. **Substitui** os comandos manuais acima no fluxo principal.
|
||||
|
||||
```
|
||||
você tem ideia → spec → review → build → PR pronto
|
||||
↑ ↑ ↑
|
||||
orquestrador você automático
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pré-requisitos
|
||||
|
||||
```bash
|
||||
npm install -g opencode-ai@latest
|
||||
npm install -g @fission-ai/openspec@latest
|
||||
opencode auth login # configura provider (OpenRouter, Anthropic, etc.)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Comandos do Orquestrador
|
||||
|
||||
| Comando | O que faz |
|
||||
|---|---|
|
||||
| `dev-orchestrator init` | Configura repo (OpenSpec + .gitignore) — 1x por projeto |
|
||||
| `dev-orchestrator spec <nome>` | Cria spec + worktree isolado + preenche spec via IA |
|
||||
| `dev-orchestrator build <nome>` | Loop dev→QA (3 tentativas), merge automático se passar |
|
||||
| `dev-orchestrator status` | Dashboard de features em andamento |
|
||||
| `dev-orchestrator clean <nome>` | Remove worktree + branch (abortar feature) |
|
||||
|
||||
---
|
||||
|
||||
## Fluxo Completo
|
||||
|
||||
```
|
||||
┌──────────┐ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐
|
||||
│ init │───→│ spec <nome> │───→│ [review] │───→│ build <nome> │
|
||||
│ (1x/proj)│ │ (orquestrador│ │ (você aprova) │ │ (dev→QA loop)│
|
||||
└──────────┘ │ preenche) │ └──────────────┘ └──────┬──────┘
|
||||
└──────────────┘ │
|
||||
┌────────────────────┘
|
||||
│ QA PASS → merge + archive
|
||||
│ QA FAIL → retry (3x max)
|
||||
└────────────────────────
|
||||
```
|
||||
|
||||
### 1. Inicializar
|
||||
|
||||
```bash
|
||||
cd ~/projetos/meu-app
|
||||
dev-orchestrator init
|
||||
```
|
||||
|
||||
### 2. Especificar feature
|
||||
|
||||
```bash
|
||||
dev-orchestrator spec add-oauth
|
||||
```
|
||||
|
||||
O que acontece internamente:
|
||||
1. `openspec new change add-oauth --json` → cria templates
|
||||
2. `git worktree add -b dev-flow/add-oauth ../.worktrees/add-oauth/ HEAD`
|
||||
3. `npm install` dentro do worktree
|
||||
4. `opencode run "preenche os specs lendo a codebase"` → orquestrador preenche spec.md, design.md, tasks.md
|
||||
|
||||
### 3. Revisar os specs (MOMENTO CRÍTICO)
|
||||
|
||||
```bash
|
||||
# Ler o spec gerado
|
||||
cat openspec/changes/add-oauth/spec.md
|
||||
cat openspec/changes/add-oauth/design.md
|
||||
|
||||
# Validar
|
||||
openspec validate add-oauth --strict
|
||||
|
||||
# Ajustar manualmente se quiser — edite os arquivos
|
||||
vim openspec/changes/add-oauth/spec.md
|
||||
```
|
||||
|
||||
> **Você é o gatekeeper aqui.** Se o spec não está certo, o build vai sair errado.
|
||||
> Gaste tempo revisando os specs — é o investimento mais rentável do fluxo.
|
||||
|
||||
### 4. Build (dev→QA loop)
|
||||
|
||||
```bash
|
||||
dev-orchestrator build add-oauth
|
||||
```
|
||||
|
||||
Loop:
|
||||
```
|
||||
Dev Phase ──→ OpenCode implementa + testa + commita
|
||||
│
|
||||
▼
|
||||
QA Phase ──→ OpenCode revisa: spec compliance, testes, regressões
|
||||
│
|
||||
├── PASS → merge no main, archive spec, remove worktree ✅
|
||||
│
|
||||
└── FAIL → volta pro Dev (até 3 tentativas)
|
||||
falhou 3x → worktree mantido pra correção manual
|
||||
```
|
||||
|
||||
### 5. Status dashboard
|
||||
|
||||
```bash
|
||||
dev-orchestrator status
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quando Sair do Orquestrador
|
||||
|
||||
O script cobre 90% dos casos. Saia dele quando:
|
||||
|
||||
| Situação | O que fazer |
|
||||
|---|---|
|
||||
| Spec ficou ruim e quero reescrever do zero | `openspec new change X` manual, preenche na mão |
|
||||
| QA rejeitou e quero corrigir eu mesmo | `cd ../.worktrees/feature/` → edita → commita → `dev-orchestrator build feature` |
|
||||
| Feature complexa demais pra um spec só | `openspec new change feature-pt1`, `openspec new change feature-pt2` |
|
||||
| Quero iterar rápido sem spec | `opencode` TUI direto (pula o orquestrador) |
|
||||
| Bug fix trivial (1-2 linhas) | `opencode run "fix: ..."` direto, nem cria spec |
|
||||
|
||||
---
|
||||
|
||||
## Paralelismo
|
||||
|
||||
Rode múltiplos features ao mesmo tempo:
|
||||
|
||||
```bash
|
||||
# Terminal 1
|
||||
dev-orchestrator build feature-a
|
||||
|
||||
# Terminal 2
|
||||
dev-orchestrator build feature-b
|
||||
```
|
||||
|
||||
Cada um em seu worktree isolado. O merge no main serializa no final — se houver conflito, o script para e avisa.
|
||||
|
||||
---
|
||||
|
||||
## Estrutura de Diretórios
|
||||
|
||||
```
|
||||
~/projetos/meu-app/
|
||||
├── src/
|
||||
├── openspec/
|
||||
│ ├── specs/ # specs arquivados
|
||||
│ └── changes/ # specs ativos
|
||||
│ └── add-oauth/
|
||||
│ ├── spec.md
|
||||
│ ├── design.md
|
||||
│ └── tasks.md
|
||||
├── .gitignore # inclui .worktrees/
|
||||
└── ...
|
||||
|
||||
../.worktrees/
|
||||
└── add-oauth/ # worktree isolado
|
||||
├── .git # branch: dev-flow/add-oauth
|
||||
├── src/
|
||||
├── node_modules/
|
||||
└── ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Opcional: Swarm Tools + .hive
|
||||
|
||||
Kanban mais visual, sem servidor:
|
||||
|
||||
```bash
|
||||
npm install -g opencode-swarm-plugin
|
||||
swarm setup
|
||||
```
|
||||
|
||||
Dentro do OpenCode:
|
||||
- `/swarm "tarefa"` — decompõe e spawna workers paralelos
|
||||
- `/hive` — quadro kanban das tasks
|
||||
- `/inbox` — mensagens entre agentes
|
||||
|
||||
O `.hive/` é uma pasta git-tracked. Independe do `dev-orchestrator`.
|
||||
Executable
+304
@@ -0,0 +1,304 @@
|
||||
#!/usr/bin/env bash
|
||||
# dev-orchestrator — OpenSpec + OpenCode dev→QA loop with git worktrees
|
||||
# Zero dependencies beyond git, opencode, openspec. No heavy binaries.
|
||||
set -euo pipefail
|
||||
|
||||
REPO="${DEV_FLOW_REPO:-$(pwd)}"
|
||||
WORKTREE_ROOT="$REPO/../.worktrees"
|
||||
|
||||
# ── helpers ──────────────────────────────────────────────────────
|
||||
|
||||
_wt_path() { echo "$WORKTREE_ROOT/$1"; }
|
||||
_wt_branch(){ echo "dev-flow/$1"; }
|
||||
|
||||
_die() { echo "✗ $*" >&2; exit 1; }
|
||||
_ok() { echo "✓ $*"; }
|
||||
|
||||
# Ensure we're inside a git repo with openspec
|
||||
_guard() {
|
||||
cd "$REPO"
|
||||
git rev-parse --show-toplevel >/dev/null 2>&1 || _die "not a git repo: $REPO"
|
||||
[ -d openspec ] || _die "openspec not initialized. Run: dev-orchestrator init"
|
||||
}
|
||||
|
||||
# ── commands ─────────────────────────────────────────────────────
|
||||
|
||||
cmd_init() {
|
||||
cd "$REPO"
|
||||
git rev-parse --show-toplevel >/dev/null 2>&1 || _die "not a git repo"
|
||||
|
||||
mkdir -p "$WORKTREE_ROOT"
|
||||
[ -d openspec ] && _ok "openspec already initialized" || {
|
||||
openspec init --tools opencode --force
|
||||
_ok "openspec initialized"
|
||||
}
|
||||
|
||||
grep -qxF '.worktrees/' .gitignore 2>/dev/null || {
|
||||
echo '.worktrees/' >> .gitignore
|
||||
_ok "added .worktrees/ to .gitignore"
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "Repo ready. Next: dev-orchestrator spec <feature-name>"
|
||||
}
|
||||
|
||||
cmd_spec() {
|
||||
local name="$1"
|
||||
_guard
|
||||
|
||||
# ── validate name (kebab-case) ──
|
||||
[[ "$name" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]] || _die "name must be kebab-case: add-oauth, fix-login"
|
||||
|
||||
# ── create OpenSpec change ──
|
||||
echo "── Creating OpenSpec change: $name ──"
|
||||
openspec new change "$name" --json 2>/dev/null || {
|
||||
_die "openspec new change failed. Already exists? Run: dev-orchestrator status"
|
||||
}
|
||||
|
||||
# ── create worktree ──
|
||||
local branch; branch="$(_wt_branch "$name")"
|
||||
local wt; wt="$(_wt_path "$name")"
|
||||
echo "── Creating worktree: $wt [$branch] ──"
|
||||
git worktree add -b "$branch" "$wt" HEAD
|
||||
|
||||
# ── install deps in worktree (fully isolated, no symlinks) ──
|
||||
if [ -f "$wt/package.json" ]; then
|
||||
echo "── Installing dependencies ──"
|
||||
(cd "$wt" && npm install --silent 2>&1 | tail -1) || true
|
||||
fi
|
||||
if [ -f "$wt/pyproject.toml" ] || [ -f "$wt/setup.py" ]; then
|
||||
# shellcheck disable=SC1091
|
||||
(cd "$wt" && [ -d .venv ] || python3 -m venv .venv && . .venv/bin/activate && pip install -e . -q 2>&1 | tail -1) || true
|
||||
fi
|
||||
|
||||
echo ""
|
||||
_ok "Spec '$name' scaffolded at openspec/changes/$name/"
|
||||
echo " Worktree: $wt"
|
||||
echo ""
|
||||
echo " ── Next: explore in OpenCode ──"
|
||||
echo " opencode"
|
||||
echo " Then use OpenSpec native commands:"
|
||||
echo " /opsx:explore — discuss the feature, refine requirements"
|
||||
echo " /opsx:continue — fill spec artifacts one by one"
|
||||
echo ""
|
||||
echo " After specs are filled → dev-orchestrator build $name"
|
||||
}
|
||||
|
||||
cmd_fill() {
|
||||
local name="$1"
|
||||
_guard
|
||||
|
||||
local wt; wt="$(_wt_path "$name")"
|
||||
[ -d "openspec/changes/$name" ] || _die "spec not found: openspec/changes/$name. Run: dev-orchestrator spec $name"
|
||||
|
||||
echo "── AI filling spec from conversation context ──"
|
||||
opencode run \
|
||||
"You are filling an OpenSpec change for feature: $name.
|
||||
|
||||
STEPS:
|
||||
1. Read the codebase structure first (scan src/, lib/, or equivalent).
|
||||
2. Read openspec/changes/$name/ — note the template files created.
|
||||
3. Read openspec/specs/ for existing specs to avoid conflicts.
|
||||
4. For each template file in the change directory, fill it with:
|
||||
- Clear requirements and acceptance criteria
|
||||
- Files that need to change
|
||||
- Dependencies on other modules
|
||||
- Test plan
|
||||
5. Run: openspec validate $name --strict
|
||||
6. Fix any validation errors, re-run until clean.
|
||||
7. Output a summary: spec files created, key decisions, estimated scope." \
|
||||
--workdir "$REPO"
|
||||
|
||||
echo ""
|
||||
_ok "Spec '$name' filled at openspec/changes/$name/"
|
||||
echo " Review the spec, then: dev-orchestrator build $name"
|
||||
}
|
||||
|
||||
cmd_build() {
|
||||
local name="$1"
|
||||
_guard
|
||||
|
||||
local wt; wt="$(_wt_path "$name")"
|
||||
[ -d "$wt" ] || _die "worktree not found: $wt. Run: dev-orchestrator spec $name"
|
||||
|
||||
local branch; branch="$(_wt_branch "$name")"
|
||||
local max_retries=3
|
||||
|
||||
for attempt in $(seq 1 $max_retries); do
|
||||
echo ""
|
||||
echo "══════════════════════════════════════════════════"
|
||||
echo " $name — Dev Phase (attempt $attempt/$max_retries)"
|
||||
echo "══════════════════════════════════════════════════"
|
||||
|
||||
opencode run \
|
||||
"IMPLEMENT the spec at openspec/changes/$name/.
|
||||
|
||||
RULES:
|
||||
- Read openspec instructions --change $name first
|
||||
- Implement ALL requirements from the spec
|
||||
- Write tests for every new code path
|
||||
- Run the test suite and ensure it passes
|
||||
- If tests fail, fix them before considering work done
|
||||
- Commit with message: '$name: implement feature (attempt $attempt)'
|
||||
- Do NOT modify openspec/ files — only source code and tests" \
|
||||
--workdir "$wt" || {
|
||||
echo "⚠ Dev phase had errors, proceeding to QA anyway..."
|
||||
}
|
||||
|
||||
# ── QA phase ──
|
||||
echo ""
|
||||
echo "──────────────────────────────────────────────────"
|
||||
echo " $name — QA Review"
|
||||
echo "──────────────────────────────────────────────────"
|
||||
|
||||
local qa_file="/tmp/dev-orchestrator-qa-$$.txt"
|
||||
opencode run \
|
||||
"QA REVIEW for $name.
|
||||
|
||||
CHECKLIST (answer each with PASS or FAIL):
|
||||
1. SPEC COMPLIANCE — Does the code implement everything in openspec/changes/$name/?
|
||||
2. TESTS — Do all tests pass? Run them now.
|
||||
3. REGRESSIONS — Does any existing test break? Check git diff vs main.
|
||||
4. EDGE CASES — Are errors handled? Null/empty inputs? Timeouts?
|
||||
5. CODE QUALITY — Clear naming? No debug leftovers? No commented-out code?
|
||||
|
||||
OUTPUT FORMAT (exactly these 2 lines, nothing else):
|
||||
VERDICT: PASS|FAIL
|
||||
REASON: <one-sentence summary>" \
|
||||
--workdir "$wt" > "$qa_file" 2>&1
|
||||
|
||||
local verdict
|
||||
verdict=$(grep '^VERDICT:' "$qa_file" | head -1 | awk -F': ' '{print $2}')
|
||||
local reason
|
||||
reason=$(grep '^REASON:' "$qa_file" | head -1 | cut -d' ' -f2-)
|
||||
|
||||
if [ "$verdict" = "PASS" ]; then
|
||||
echo ""
|
||||
_ok "QA PASSED — $reason"
|
||||
|
||||
# ── archive + merge ──
|
||||
cd "$REPO"
|
||||
echo "── Archiving spec ──"
|
||||
openspec archive "$name" --yes 2>/dev/null || true
|
||||
|
||||
echo "── Merging to main ──"
|
||||
local current_branch=$(git branch --show-current)
|
||||
git merge "$branch" -m "dev-flow: merge $name" 2>/dev/null || {
|
||||
echo "⚠ Merge conflict. Resolve manually in $wt then run:"
|
||||
echo " cd $wt && git checkout main && git merge $branch"
|
||||
return 1
|
||||
}
|
||||
|
||||
echo "── Cleaning up worktree ──"
|
||||
git worktree remove "$wt" 2>/dev/null || true
|
||||
git branch -d "$branch" 2>/dev/null || true
|
||||
|
||||
echo ""
|
||||
_ok "$name — BUILD COMPLETE ✓"
|
||||
rm "$qa_file"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✗ QA FAILED — ${reason:-see $qa_file}"
|
||||
rm "$qa_file"
|
||||
|
||||
if [ "$attempt" -eq "$max_retries" ]; then
|
||||
_die "$name FAILED after $max_retries attempts. Worktree kept at $wt for manual fix."
|
||||
fi
|
||||
echo "↻ Sending back to dev with QA feedback..."
|
||||
done
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
_guard 2>/dev/null || true
|
||||
|
||||
echo ""
|
||||
printf "%-4s %-30s %-10s %-10s %-15s\n" "#" "FEATURE" "COMMITS" "SPEC" "WORKTREE"
|
||||
printf "%-4s %-30s %-10s %-10s %-15s\n" "───" "──────────────────────────────" "──────────" "──────────" "──────────────"
|
||||
|
||||
local n=0
|
||||
for wt in "$WORKTREE_ROOT"/*/; do
|
||||
[ -d "$wt" ] || continue
|
||||
local name=$(basename "$wt")
|
||||
local branch; branch="$(_wt_branch "$name")"
|
||||
local commits="?"
|
||||
[ -d "$wt/.git" ] && commits=$(cd "$wt" && git rev-list --count "$branch" -- 2>/dev/null || echo "?")
|
||||
commits="${commits:-0}"
|
||||
|
||||
# spec status
|
||||
local spec="?"
|
||||
[ -d "$REPO/openspec/changes/$name" ] && {
|
||||
spec=$(cd "$REPO" && openspec status --change "$name" --json 2>/dev/null | \
|
||||
python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('artifacts_completed','?'),'/',d.get('artifacts_total','?'),sep='')" 2>/dev/null || echo "active")
|
||||
}
|
||||
|
||||
n=$((n+1))
|
||||
printf "%-4s %-30s %-10s %-10s %-15s\n" \
|
||||
"$n." "$name" "$commits" "${spec:-active}" "$wt"
|
||||
done
|
||||
|
||||
if [ "$n" -eq 0 ]; then
|
||||
echo " (no active features)"
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
cmd_clean() {
|
||||
local name="$1"
|
||||
cd "$REPO"
|
||||
local wt; wt="$(_wt_path "$name")"
|
||||
local branch; branch="$(_wt_branch "$name")"
|
||||
|
||||
git worktree remove "$wt" 2>/dev/null || true
|
||||
git branch -D "$branch" 2>/dev/null || true
|
||||
if [ ! -d "$wt" ]; then
|
||||
_ok "cleaned $name"
|
||||
else
|
||||
_die "could not remove $wt"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── dispatch ─────────────────────────────────────────────────────
|
||||
|
||||
case "${1:-}" in
|
||||
init)
|
||||
cmd_init
|
||||
;;
|
||||
spec)
|
||||
[ $# -ge 2 ] || _die "usage: dev-orchestrator spec <feature-name>"
|
||||
cmd_spec "$2"
|
||||
;;
|
||||
fill)
|
||||
[ $# -ge 2 ] || _die "usage: dev-orchestrator fill <feature-name>"
|
||||
cmd_fill "$2"
|
||||
;;
|
||||
build)
|
||||
[ $# -ge 2 ] || _die "usage: dev-orchestrator build <feature-name>"
|
||||
cmd_build "$2"
|
||||
;;
|
||||
status)
|
||||
cmd_status
|
||||
;;
|
||||
clean)
|
||||
[ $# -ge 2 ] || _die "usage: dev-orchestrator clean <feature-name>"
|
||||
cmd_clean "$2"
|
||||
;;
|
||||
*)
|
||||
echo "dev-orchestrator — OpenSpec + OpenCode dev→QA loop"
|
||||
echo ""
|
||||
echo "commands:"
|
||||
echo " init Set up repo (run once per project)"
|
||||
echo " spec <feature-name> Scaffold OpenSpec + worktree (no fill)"
|
||||
echo " fill <feature-name> AI fills spec after exploration"
|
||||
echo " build <feature-name> Dev→QA loop in isolated worktree"
|
||||
echo " status Kanban view of features in flight"
|
||||
echo " clean <feature-name> Remove worktree + branch"
|
||||
echo ""
|
||||
echo "flow: init → spec → [explore in OpenCode] → fill → build → [QA loop]"
|
||||
echo ""
|
||||
echo "worktrees live at: ../.worktrees/<feature>/"
|
||||
echo "branches named: dev-flow/<feature>"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Reference in New Issue
Block a user