14 Commits

Author SHA1 Message Date
Claude 3f736a9ac3 niri: add full desktop stack (bar, launcher, notifications, configs)
Moves programs.niri into a dedicated niri.nix module and installs the
usual Wayland userland — alacritty, fuzzel, waybar, mako, swaybg,
grim/slurp/wl-clipboard, swaylock/swayidle/wlogout, xwayland-satellite,
plus nm-applet/pavucontrol/playerctl/brightnessctl.

Ships starter configs for niri (keybinds mirroring sway/KDE habits,
screenshots to ~/Pictures/Screenshots, media keys, workspace switching,
auto-lock via swayidle), waybar with a Catppuccin-ish style, mako, and
fuzzel — all wired up through home-manager's xdg.configFile.

https://claude.ai/code/session_01NrGdGEBRbDbWvHUb3Lho44
2026-04-23 14:57:14 +00:00
Claude 4a6258f8f9 desktop: enable niri compositor
Adds niri as an available Wayland session alongside Plasma6, selectable
from SDDM.

https://claude.ai/code/session_01NrGdGEBRbDbWvHUb3Lho44
2026-04-23 14:50:24 +00:00
chexit 5a5822b27e add openvpn 2026-04-23 17:46:22 +03:00
chexit 5afd18dc12 add thunderbird 2026-04-19 23:33:20 +03:00
chexit cb0bcc0cf8 add spotify 2026-04-19 19:25:32 +03:00
Vladimir be12ef4465 Merge pull request #8 from Chexit/claude/add-yami-dev-stack-zHOMn
feat(yami): add dataDir option for separate-disk state
2026-04-19 19:05:04 +03:00
chexit 6775a88ca8 feat(yami): add dataDir option for storing stack state on separate disk
- Bind-mounts /var/lib/{gitea,postgresql} under configurable dataDir
- gitea-runner intentionally excluded: its DynamicUser + StateDirectory
  conflicts with pre-existing mount points ("File exists")
- Runner token moved to /var/lib/gitea-runner-secret/ to avoid the same
  symlink/StateDirectory conflict inside /var/lib/gitea-runner
- Enable the stack on this host with dataDir = /mnt/ssd_110/yami
2026-04-19 18:54:28 +03:00
Vladimir 61b1cf5097 Merge pull request #4 from Chexit/claude/switch-to-fish-shell-LDSqe
Switch default shell from Zsh to Fish with Starship prompt
2026-04-19 18:29:56 +03:00
Vladimir da4ad4b025 Merge pull request #7 from Chexit/claude/add-yami-dev-stack-zHOMn
docs(yami): note that runner tokenFile must be env-file format (TOKEN…
2026-04-19 18:24:38 +03:00
chexit a83500e382 docs(yami): note that runner tokenFile must be env-file format (TOKEN=...) 2026-04-19 18:23:56 +03:00
Vladimir 7bf3369d53 Merge pull request #6 from Chexit/claude/add-yami-dev-stack-zHOMn
fix(yami): tmpfiles for runner token must use root (DynamicUser has n…
2026-04-19 18:18:11 +03:00
chexit ecf8b5974a fix(yami): tmpfiles for runner token must use root (DynamicUser has no static user) 2026-04-19 18:17:24 +03:00
Vladimir fac913504f Merge pull request #5 from Chexit/claude/add-yami-dev-stack-zHOMn
fix(yami): use services.gitea.lfs.enable instead of raw LFS_START_SERVER
2026-04-19 18:02:39 +03:00
chexit 6a2fb0970c fix(yami): use services.gitea.lfs.enable instead of raw LFS_START_SERVER
Raw LFS_START_SERVER made gitea try to generate and persist LFS_JWT_SECRET
into app.ini, which NixOS intentionally keeps read-only. The lfs.enable
helper lets the module own the secret and wire it in correctly.
2026-04-19 18:00:03 +03:00
10 changed files with 518 additions and 7 deletions
+8 -1
View File
@@ -5,9 +5,10 @@
./hardware-configuration.nix
./packages.nix
./desktop.nix
./niri.nix
./nvidia.nix
./shell.nix
# ./yami-dev-stack.nix # uncomment to enable Yami dev stack, then set services.yami-dev-stack below
./yami-dev-stack.nix
];
nix = {
@@ -104,5 +105,11 @@
# Docker is now managed by yami-dev-stack.nix when that module is enabled.
# services.yami-dev-stack = { enable = true; user = "chexit"; };
services.yami-dev-stack = {
enable = true;
user = "chexit";
dataDir = "/mnt/ssd_110/yami";
};
system.stateVersion = "25.11";
}
+26
View File
@@ -17,5 +17,31 @@
enableFishIntegration = true;
};
programs.alacritty = {
enable = true;
settings = {
window = {
padding = { x = 10; y = 10; };
opacity = 0.95;
decorations = "None";
};
font = {
normal = { family = "JetBrainsMono Nerd Font"; style = "Regular"; };
size = 11;
};
colors = {
primary = { background = "#1e1e2e"; foreground = "#cdd6f4"; };
};
};
};
xdg.configFile = {
"niri/config.kdl".source = ./niri/config.kdl;
"waybar/config.jsonc".source = ./niri/waybar/config.jsonc;
"waybar/style.css".source = ./niri/waybar/style.css;
"mako/config".source = ./niri/mako/config;
"fuzzel/fuzzel.ini".source = ./niri/fuzzel/fuzzel.ini;
};
programs.home-manager.enable = true;
}
+43
View File
@@ -0,0 +1,43 @@
{ pkgs, ... }:
{
programs.niri.enable = true;
environment.systemPackages = with pkgs; [
# terminal + launcher
alacritty
fuzzel
# bar, notifications, wallpaper
waybar
mako
swaybg
# screenshots + clipboard
grim
slurp
swappy
wl-clipboard
# lock + idle
swaylock-effects
swayidle
wlogout
# XWayland shim for niri
xwayland-satellite
# tray + system utilities
networkmanagerapplet
pavucontrol
playerctl
brightnessctl
libnotify
xdg-utils
];
environment.sessionVariables = {
XDG_CURRENT_DESKTOP = "niri";
XDG_SESSION_DESKTOP = "niri";
};
}
+201
View File
@@ -0,0 +1,201 @@
// niri configuration
// docs: https://github.com/YaLTeR/niri/wiki/Configuration:-Overview
input {
keyboard {
xkb {
layout "us"
}
repeat-delay 300
repeat-rate 40
}
touchpad {
tap
dwt
natural-scroll
accel-speed 0.2
}
mouse {
accel-speed 0.0
}
warp-mouse-to-focus
}
output "*" {
// let niri autodetect; override here per-monitor if needed
}
layout {
gaps 12
center-focused-column "never"
preset-column-widths {
proportion 0.33333
proportion 0.5
proportion 0.66667
}
default-column-width { proportion 0.5; }
focus-ring {
width 2
active-color "#7fc8ff"
inactive-color "#3a3a3a"
}
border {
off
}
struts {
left 8
right 8
top 4
bottom 4
}
}
cursor {
xcursor-theme "Adwaita"
xcursor-size 24
}
prefer-no-csd
screenshot-path "~/Pictures/Screenshots/Screenshot-%Y-%m-%d-%H%M%S.png"
hotkey-overlay {
skip-at-startup
}
environment {
DISPLAY ":0"
QT_QPA_PLATFORM "wayland;xcb"
GDK_BACKEND "wayland,x11"
MOZ_ENABLE_WAYLAND "1"
_JAVA_AWT_WM_NONREPARENTING "1"
}
spawn-at-startup "xwayland-satellite"
spawn-at-startup "waybar"
spawn-at-startup "mako"
spawn-at-startup "swaybg" "-c" "#1e1e2e"
spawn-at-startup "nm-applet" "--indicator"
spawn-at-startup "swayidle" "-w" \
"timeout" "600" "swaylock -f" \
"timeout" "900" "niri msg action power-off-monitors" \
"before-sleep" "swaylock -f"
window-rule {
geometry-corner-radius 8
clip-to-geometry true
}
window-rule {
match app-id="^org\\.keepassxc\\.KeePassXC$"
match app-id="^1Password$"
block-out-from "screen-capture"
}
binds {
Mod+Shift+Slash { show-hotkey-overlay; }
// launchers
Mod+Return { spawn "alacritty"; }
Mod+D { spawn "fuzzel"; }
Mod+E { spawn "dolphin"; }
Mod+B { spawn "firefox"; }
Mod+Ctrl+L { spawn "swaylock" "-f"; }
Mod+Shift+Escape { spawn "wlogout"; }
// media keys
XF86AudioRaiseVolume allow-when-locked=true { spawn "wpctl" "set-volume" "-l" "1.0" "@DEFAULT_AUDIO_SINK@" "5%+"; }
XF86AudioLowerVolume allow-when-locked=true { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "5%-"; }
XF86AudioMute allow-when-locked=true { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SINK@" "toggle"; }
XF86AudioMicMute allow-when-locked=true { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SOURCE@" "toggle"; }
XF86AudioPlay { spawn "playerctl" "play-pause"; }
XF86AudioPause { spawn "playerctl" "play-pause"; }
XF86AudioNext { spawn "playerctl" "next"; }
XF86AudioPrev { spawn "playerctl" "previous"; }
XF86MonBrightnessUp { spawn "brightnessctl" "set" "5%+"; }
XF86MonBrightnessDown { spawn "brightnessctl" "set" "5%-"; }
// window management
Mod+Q { close-window; }
Mod+Left { focus-column-left; }
Mod+Right { focus-column-right; }
Mod+Up { focus-window-up; }
Mod+Down { focus-window-down; }
Mod+H { focus-column-left; }
Mod+L { focus-column-right; }
Mod+K { focus-window-up; }
Mod+J { focus-window-down; }
Mod+Ctrl+Left { move-column-left; }
Mod+Ctrl+Right { move-column-right; }
Mod+Ctrl+Up { move-window-up; }
Mod+Ctrl+Down { move-window-down; }
Mod+Ctrl+H { move-column-left; }
Mod+Ctrl+L { move-column-right; }
Mod+Ctrl+K { move-window-up; }
Mod+Ctrl+J { move-window-down; }
Mod+Home { focus-column-first; }
Mod+End { focus-column-last; }
Mod+Page_Down { focus-workspace-down; }
Mod+Page_Up { focus-workspace-up; }
Mod+U { focus-workspace-down; }
Mod+I { focus-workspace-up; }
Mod+Shift+Page_Down { move-column-to-workspace-down; }
Mod+Shift+Page_Up { move-column-to-workspace-up; }
Mod+Shift+U { move-column-to-workspace-down; }
Mod+Shift+I { move-column-to-workspace-up; }
Mod+1 { focus-workspace 1; }
Mod+2 { focus-workspace 2; }
Mod+3 { focus-workspace 3; }
Mod+4 { focus-workspace 4; }
Mod+5 { focus-workspace 5; }
Mod+6 { focus-workspace 6; }
Mod+7 { focus-workspace 7; }
Mod+8 { focus-workspace 8; }
Mod+9 { focus-workspace 9; }
Mod+Shift+1 { move-column-to-workspace 1; }
Mod+Shift+2 { move-column-to-workspace 2; }
Mod+Shift+3 { move-column-to-workspace 3; }
Mod+Shift+4 { move-column-to-workspace 4; }
Mod+Shift+5 { move-column-to-workspace 5; }
Mod+Shift+6 { move-column-to-workspace 6; }
Mod+Shift+7 { move-column-to-workspace 7; }
Mod+Shift+8 { move-column-to-workspace 8; }
Mod+Shift+9 { move-column-to-workspace 9; }
Mod+Comma { consume-window-into-column; }
Mod+Period { expel-window-from-column; }
Mod+R { switch-preset-column-width; }
Mod+F { maximize-column; }
Mod+Shift+F { fullscreen-window; }
Mod+C { center-column; }
Mod+Minus { set-column-width "-10%"; }
Mod+Equal { set-column-width "+10%"; }
Mod+Shift+Minus { set-window-height "-10%"; }
Mod+Shift+Equal { set-window-height "+10%"; }
// screenshots
Print { screenshot; }
Ctrl+Print { screenshot-screen; }
Alt+Print { screenshot-window; }
// session
Mod+Shift+E { quit; }
Mod+Shift+P { power-off-monitors; }
}
+23
View File
@@ -0,0 +1,23 @@
font=JetBrainsMono Nerd Font:size=12
dpi-aware=yes
terminal=alacritty
prompt=" "
icon-theme=Adwaita
width=40
lines=12
horizontal-pad=20
vertical-pad=14
inner-pad=10
[colors]
background=1e1e2eee
text=cdd6f4ff
match=f9e2afff
selection=89b4faff
selection-text=1e1e2eff
selection-match=1e1e2eff
border=89b4faff
[border]
width=2
radius=10
+20
View File
@@ -0,0 +1,20 @@
font=JetBrainsMono Nerd Font 10
background-color=#1e1e2eee
text-color=#cdd6f4
border-color=#89b4fa
border-size=2
border-radius=8
padding=12
margin=8
default-timeout=5000
ignore-timeout=1
max-visible=5
layer=overlay
anchor=top-right
[urgency=low]
border-color=#6c7086
[urgency=high]
border-color=#f38ba8
default-timeout=0
+77
View File
@@ -0,0 +1,77 @@
{
"layer": "top",
"position": "top",
"height": 30,
"spacing": 6,
"margin-top": 4,
"margin-left": 8,
"margin-right": 8,
"modules-left": ["niri/workspaces", "niri/window"],
"modules-center": ["clock"],
"modules-right": [
"tray",
"network",
"pulseaudio",
"cpu",
"memory",
"custom/power"
],
"niri/workspaces": {
"format": "{icon}",
"format-icons": {
"default": "",
"active": "",
"urgent": ""
}
},
"niri/window": {
"format": "{title}",
"max-length": 80,
"separate-outputs": true
},
"clock": {
"format": "{:%a %d %b %H:%M}",
"tooltip-format": "<tt><small>{calendar}</small></tt>"
},
"cpu": {
"format": " {usage}%",
"interval": 2
},
"memory": {
"format": " {used:0.1f}G",
"interval": 5
},
"network": {
"format-wifi": " {essid}",
"format-ethernet": " {ifname}",
"format-disconnected": "⚠ offline",
"tooltip-format": "{ifname} · {ipaddr}"
},
"pulseaudio": {
"format": "{icon} {volume}%",
"format-muted": " muted",
"format-icons": {
"default": ["", "", ""]
},
"on-click": "pavucontrol"
},
"tray": {
"icon-size": 16,
"spacing": 8
},
"custom/power": {
"format": "⏻",
"tooltip": false,
"on-click": "wlogout"
}
}
+62
View File
@@ -0,0 +1,62 @@
* {
font-family: "JetBrainsMono Nerd Font", "Geist", sans-serif;
font-size: 13px;
border: none;
border-radius: 0;
min-height: 0;
}
window#waybar {
background: rgba(30, 30, 46, 0.85);
color: #cdd6f4;
border-radius: 10px;
}
#workspaces button {
padding: 0 8px;
color: #9399b2;
background: transparent;
border-radius: 6px;
}
#workspaces button.active {
color: #1e1e2e;
background: #89b4fa;
}
#workspaces button.urgent {
background: #f38ba8;
color: #1e1e2e;
}
#window,
#clock,
#cpu,
#memory,
#network,
#pulseaudio,
#tray,
#custom-power {
padding: 0 10px;
margin: 4px 2px;
border-radius: 6px;
background: rgba(49, 50, 68, 0.6);
}
#clock {
color: #f9e2af;
font-weight: 600;
}
#custom-power {
color: #f38ba8;
padding: 0 12px;
}
#network.disconnected {
color: #f38ba8;
}
#pulseaudio.muted {
color: #6c7086;
}
+3
View File
@@ -20,6 +20,9 @@
materialgram
termius
throne
spotify
thunderbird
openvpn
# dev
vscode
+54 -5
View File
@@ -13,9 +13,20 @@ in
type = types.str;
description = "User that will own runner token and be added to docker group";
};
dataDir = mkOption {
type = types.nullOr types.path;
default = null;
example = "/mnt/ssd_110/yami";
description = ''
If set, bind-mounts /var/lib/{gitea,postgresql,gitea-runner} under this
path so all stack data lives on a separate disk. Directories are created
automatically; existing data must be rsync'd manually before enabling.
'';
};
};
config = mkIf cfg.enable {
config = mkIf cfg.enable (mkMerge [ {
# ── Systemd target ─────────────────────────────────────────────────────────
# Manual-only: nothing in wantedBy, so it never starts on boot.
@@ -48,6 +59,10 @@ in
enable = true;
appName = "Yami Git";
# LFS enabled via the NixOS helper so the module manages LFS_JWT_SECRET
# (writing it to app.ini, which is read-only by design).
lfs.enable = true;
database = {
type = "postgres";
socket = "/run/postgresql";
@@ -63,7 +78,6 @@ in
HTTP_PORT = 3000;
SSH_PORT = 2222;
START_SSH_SERVER = true;
LFS_START_SERVER = true;
};
service = {
@@ -125,7 +139,11 @@ in
enable = true;
name = "home-runner";
url = "http://localhost:3000";
tokenFile = "/var/lib/gitea-runner/token";
# NOT under /var/lib/gitea-runner — that path is systemd-managed for the
# service (DynamicUser + StateDirectory means it becomes a symlink to
# /var/lib/private/gitea-runner). Pre-creating it ourselves breaks the
# service with "Failed to set up special execution directory: File exists".
tokenFile = "/var/lib/gitea-runner-secret/token";
labels = [
"ubuntu-latest:docker://node:20"
"native:host"
@@ -139,9 +157,14 @@ in
requires = [ "gitea.service" "docker.service" ];
};
# Directory for the runner token file; owner matches the runner service user.
# Runner uses DynamicUser, so there's no static gitea-runner user/group.
# Token lives in a sibling dir (not /var/lib/gitea-runner — that's systemd's
# StateDirectory and pre-creating it breaks DynamicUser bind setup).
# The file MUST be in env-file format, not a raw token:
# TOKEN=<paste-registration-token-from-gitea-ui>
# Get the token from Site Administration → Actions → Runners → Create new Runner.
systemd.tmpfiles.rules = [
"d /var/lib/gitea-runner 0700 gitea-runner gitea-runner -"
"d /var/lib/gitea-runner-secret 0700 root root -"
];
# ── Shell aliases ──────────────────────────────────────────────────────────
@@ -155,5 +178,31 @@ in
# ── Firewall ───────────────────────────────────────────────────────────────
# Merged with existing rules (Steam etc.); does not overwrite them.
networking.firewall.allowedTCPPorts = [ 3000 2222 ];
}
# ── Optional: move all stack data to a separate disk via bind-mounts ─────────
(mkIf (cfg.dataDir != null) (
let
# gitea-runner intentionally NOT bind-mounted: its service uses
# DynamicUser + StateDirectory, which refuses to adopt an existing
# mount point ("Failed to set up special execution directory: File exists").
# Its data is tiny (token + .runner state) so it stays on the system disk.
subs = [ "gitea" "postgresql" ];
mkBind = name: {
name = "/var/lib/${name}";
value = {
device = "${cfg.dataDir}/${name}";
fsType = "none";
options = [ "bind" ];
};
};
in {
# Ensure target dirs exist on the data disk before mounts happen.
systemd.tmpfiles.rules = map (n: "d ${cfg.dataDir}/${n} 0755 root root -") subs
++ [ "d ${cfg.dataDir} 0755 root root -" ];
fileSystems = listToAttrs (map mkBind subs);
}
))
]);
}