From 3295fbbf0b99eb0da21c37df33d66877d41de0f7 Mon Sep 17 00:00:00 2001 From: Erik Simon Date: Sat, 25 Jul 2026 01:26:52 +0200 Subject: [PATCH] terra: desktop setup, flatpak, unstable packages, key management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Hyprland workspace rules: start-communications.sh launches telegram + discord into special:communications; qbz/discord/telegram switched to flatpak (nix-flatpak, Flathub) — removes qbz and proton-pass-cli flake inputs - proton-pass-cli and claude-code sourced from nixpkgs-unstable; unstable pkgs set threaded into home-manager via extraSpecialArgs - GTK/libadwaita dark theme fixed: dconf color-scheme = prefer-dark written declaratively instead of a per-session gsettings call - scripts/keys: store/restore SSH host keys and sops age keys via Proton Pass (ssh_host# / age# / age#admin naming) --- dotfiles/quickshell/open_launcher.sh | 3 + dotfiles/scripts/start-communications.sh | 4 + flake.lock | 69 ++++++ flake.nix | 20 +- hosts/terra/configuration.nix | 21 ++ hosts/terra/home.nix | 59 ++--- hosts/terra/home/hyprland.nix | 270 +++++++++++++++++++++++ scripts/keys | 175 +++++++++++++++ services/desktop/desktop-apps.nix | 4 - services/desktop/desktop-hyprland.nix | 6 +- 10 files changed, 580 insertions(+), 51 deletions(-) create mode 100755 dotfiles/quickshell/open_launcher.sh create mode 100755 dotfiles/scripts/start-communications.sh create mode 100644 hosts/terra/home/hyprland.nix create mode 100755 scripts/keys diff --git a/dotfiles/quickshell/open_launcher.sh b/dotfiles/quickshell/open_launcher.sh new file mode 100755 index 0000000..a4ec215 --- /dev/null +++ b/dotfiles/quickshell/open_launcher.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +hyprctl dispatch 'hl.dsp.global ("quickshell:launcher7")' diff --git a/dotfiles/scripts/start-communications.sh b/dotfiles/scripts/start-communications.sh new file mode 100755 index 0000000..9385365 --- /dev/null +++ b/dotfiles/scripts/start-communications.sh @@ -0,0 +1,4 @@ +#!/bin/bash +# Start both comms apps; Hyprland window rules move them to special:communications. +telegram-desktop & +discord & diff --git a/flake.lock b/flake.lock index 4dec192..dcc8497 100644 --- a/flake.lock +++ b/flake.lock @@ -118,6 +118,24 @@ "type": "github" } }, + "flake-utils_2": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, "home-manager": { "inputs": { "nixpkgs": [ @@ -332,6 +350,22 @@ "type": "github" } }, + "nixpkgs_3": { + "locked": { + "lastModified": 1775888245, + "narHash": "sha256-nwASzrRDD1JBEu/o8ekKYEXm/oJW6EMCzCRdrwcLe90=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "13043924aaa7375ce482ebe2494338e058282925", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, "proton-pass-cli": { "inputs": { "nixpkgs": [ @@ -452,6 +486,25 @@ "type": "github" } }, + "qbz": { + "inputs": { + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs_3" + }, + "locked": { + "lastModified": 1784877312, + "narHash": "sha256-eQ4xvBKnQp4C5lEVukx/75umFFmWyCjbuO82qyFDw14=", + "owner": "vicrodh", + "repo": "qbz", + "rev": "c4cc755fd02d999084883e864c0697139a8b8648", + "type": "github" + }, + "original": { + "owner": "vicrodh", + "repo": "qbz", + "type": "github" + } + }, "root": { "inputs": { "authentik-nix": "authentik-nix", @@ -463,6 +516,7 @@ "nixpkgs": "nixpkgs_2", "nixpkgs-unstable": "nixpkgs-unstable", "proton-pass-cli": "proton-pass-cli", + "qbz": "qbz", "sops-nix": "sops-nix" } }, @@ -501,6 +555,21 @@ "type": "github" } }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, "treefmt-nix": { "inputs": { "nixpkgs": [ diff --git a/flake.nix b/flake.nix index 7acda86..e0ce239 100644 --- a/flake.nix +++ b/flake.nix @@ -3,10 +3,6 @@ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05"; - # Second nixpkgs, used for ONE package: immich. 26.05 pins 2.7.5, but - # jupiter's imported database was written by 3.0.0 and immich never - # migrates a schema backwards. NOT `follows` — the point is a different - # package set. See services/media/immich.nix. nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; disko = { url = "github:nix-community/disko"; @@ -16,18 +12,10 @@ url = "github:Mic92/sops-nix"; inputs.nixpkgs.follows = "nixpkgs"; }; - # NB: no `inputs.nixpkgs.follows` here — nixos-images has no `nixpkgs` - # input (it takes nixos-stable / nixos-unstable), so declaring one only - # printed "override for a non-existent input" on every nix command. nixos-images = { url = "github:nix-community/nixos-images"; inputs.nixos-stable.follows = "nixpkgs"; }; - # Pinned as an input rather than `nix run github:...` from scripts/deploy. - # Both it and disko run at the exact moment a disk is being wiped, so the - # revision has to come from flake.lock — reviewed, reproducible, and - # resolvable from the local store — instead of whatever upstream master - # happens to be that day (which also fails outright with no network). nixos-anywhere = { url = "github:nix-community/nixos-anywhere"; inputs.nixpkgs.follows = "nixpkgs"; @@ -43,16 +31,13 @@ url = "github:strangeglyph/mediamanager-nix"; inputs.nixpkgs.follows = "nixpkgs"; }; - # Deliberately NOT `inputs.nixpkgs.follows` — upstream states overriding it - # breaks their pinned python dependency set. Costs a second nixpkgs in the - # lock; builds come prebuilt from nix-community's Cachix. authentik-nix.url = "github:nix-community/authentik-nix"; - # Unofficial packaging of Proton's pass-cli (not in nixpkgs) — used by - # ./scripts/deploy to pull sudo/ssh passwords from the "HomeLab" vault. proton-pass-cli = { url = "github:tomsch/proton-pass-cli-nix"; inputs.nixpkgs.follows = "nixpkgs"; }; + nix-flatpak.url = "github:gmodena/nix-flatpak"; + qbz.url = "github:vicrodh/qbz"; # NOTE: the `tome` input (private repo git+ssh://gitea@git.mgaction.town) was # removed temporarily — the credential-less installer-iso can't fetch it, so # `install terra localhost` failed at nixos-install (post-disko). Re-add it @@ -105,6 +90,7 @@ disko.nixosModules.disko sops-nix.nixosModules.sops home-manager.nixosModules.home-manager + inputs.nix-flatpak.nixosModules.nix-flatpak ./hosts/terra/configuration.nix ]; }; diff --git a/hosts/terra/configuration.nix b/hosts/terra/configuration.nix index 2017dae..db2e225 100644 --- a/hosts/terra/configuration.nix +++ b/hosts/terra/configuration.nix @@ -18,6 +18,16 @@ networking.hostName = "terra"; + services.flatpak = { + enable = true; + remotes = [{ name = "flathub"; location = "https://dl.flathub.org/repo/flathub.flatpakrepo"; }]; + packages = [ + { appId = "com.blitzfc.qbz"; origin = "flathub"; } + { appId = "com.discordapp.Discord"; origin = "flathub"; } + { appId = "org.telegram.desktop"; origin = "flathub"; } + ]; + }; + # Proton Pass CLI — not in nixpkgs; ./scripts/deploy uses it to autofill # sudo/ssh passwords from the "HomeLab" vault. Packaged by the # proton-pass-cli flake input (github:tomsch/proton-pass-cli-nix). @@ -34,6 +44,17 @@ boot.loader.efi.canTouchEfiVariables = true; hardware.cpu.amd.updateMicrocode = true; + # ---- GPU (Radeon RX 6800 XT / Navi 21) ---- + # amdgpu needs the redistributable navi21 firmware blobs to bind the card. + # Without them the module loads but never initialises the GPU: there's no + # DRM card for it, the display falls back to the 1024x768 EFI + # simple-framebuffer as "Unknown-1", and the real DP-2/HDMI-A-1 outputs + # never appear — so hyprland's monitor rules match nothing. + hardware.enableRedistributableFirmware = true; + # Early KMS: bind amdgpu in the initrd so it drives the console + greeter + # from boot instead of handing over from simple-framebuffer later. + boot.initrd.kernelModules = [ "amdgpu" ]; + # ---- Dev-data disks — NOT in disko, mounted read-write, never wiped ---- # UUIDs captured from the running CachyOS box; verify after install # (`lsblk -o NAME,UUID,MOUNTPOINT`) in case disko/kernel enumerates differently. diff --git a/hosts/terra/home.nix b/hosts/terra/home.nix index acb03ea..d7f8803 100644 --- a/hosts/terra/home.nix +++ b/hosts/terra/home.nix @@ -1,28 +1,23 @@ { pkgs, ... }: -# home-manager profile for darman on terra. System-level Hyprland enable -# (session entry, portals) lives in ../../services/desktop/desktop-hyprland.nix; this -# manages the user's own hyprland.conf + session packages. +# home-manager profile for darman on terra. The Hyprland config lives in its +# own module (./hyprland.nix); this handles the rest of the user session +# (theming, terminal, packages). { - home.stateVersion = "26.05"; + imports = [ ./home/hyprland.nix ]; - wayland.windowManager.hyprland = { - enable = true; - # Starter config — replace with your real dotfiles. - settings = { - monitor = [ ",preferred,auto,1" ]; - "$mod" = "SUPER"; - bind = [ - "$mod, Return, exec, alacritty" - "$mod, Q, killactive" - "$mod, D, exec, wofi --show drun" - ]; - }; - }; + home.stateVersion = "26.05"; + home.keyboard.layout = "de"; programs.git.enable = true; programs.home-manager.enable = true; + # Give darman a managed ~/.zshrc. Without any zsh dotfile, zsh runs its + # first-run `zsh-newuser-install` prompt on every new terminal. oh-my-zsh + + # powerlevel10k still come from the system /etc/zshrc (common.nix), which + # always loads for interactive shells before this ~/.zshrc. + programs.zsh.enable = true; + # ---- Dracula theming (GTK + Qt) ---- gtk = { enable = true; @@ -58,20 +53,26 @@ # be a package in home.packages, not a plain xdg.dataFile. xdg.mime.enable = true; xdg.configFile."quickshell".source = ../../dotfiles/quickshell; + xdg.configFile."scripts".source = ../../dotfiles/scripts; home.packages = [ (pkgs.writeTextDir "share/mime/packages/application-x-ms-sln.xml" (builtins.readFile ../../dotfiles/mime/application-x-ms-sln.xml)) pkgs.claude-code pkgs.quickshell pkgs.opencode + pkgs.hyprcursor + pkgs.bibata-cursors ]; programs.alacritty = { enable = true; settings = { - env.SHELL = "/bin/zsh"; + # NixOS has no /bin/zsh (only /bin/sh); point at the real store path or + # alacritty exits instantly trying to exec a missing shell — which looked + # like "the SUPER+Return keybind doesn't work". + env.SHELL = "${pkgs.zsh}/bin/zsh"; terminal.shell = { - program = "/bin/zsh"; + program = "${pkgs.zsh}/bin/zsh"; args = [ "-l" ]; }; window = { @@ -86,17 +87,17 @@ background = "#222831"; foreground = "#ffd369"; }; - hints.enabled = [ - { - hyperlinks = true; - regex = "(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file:|git://|ssh:|ftp://)[^\\u0000-\\u001F\\u007F-\\u009F<>\"\\s{-}\\^⟨⟩`]+"; - command = "xdg-open"; - mouse.enabled = true; - } - ]; + # hints.enabled = [ + # { + # hyperlinks = true; + # regex = "(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file:|git://|ssh:|ftp://)[^\\u0000-\\u001F\\u007F-\\u009F<>\"\\s{-}\\^⟨⟩`]+"; + # command = "xdg-open"; + # mouse.enabled = true; + # } + # ]; keyboard.bindings = [ - # ESC + CR: nix strings have no \u escape, so fromJSON (which - # supports \u001B) is used to get the literal control chars here. + # ESC + CR: nix has no literal escape for the ESC control char, so + # fromJSON decodes it from the JSON unicode escape below. { key = "Return"; mods = "Shift"; chars = builtins.fromJSON ''"\u001B\r"''; } ]; }; diff --git a/hosts/terra/home/hyprland.nix b/hosts/terra/home/hyprland.nix new file mode 100644 index 0000000..d4c94b5 --- /dev/null +++ b/hosts/terra/home/hyprland.nix @@ -0,0 +1,270 @@ +{ lib, ... }: + +# Hyprland config migrated from github.com/darman96/hyprland-dotfiles (the +# hyprlang `hypr/*.conf` files) into the home-manager lua-style `settings` +# (configType defaults to "lua" on stateVersion 26.05). Each top-level +# `settings` attr becomes an `hl.(...)` call in ~/.config/hypr/hyprland.lua; +# `_args` lists become multi-arg calls, `_var` locals become `local x = ...`, and +# `lib.generators.mkLuaInline` values render as raw Lua expressions. +# +# Imported by home.nix. System-level Hyprland enable (session entry, portals) +# lives in ../../services/desktop/desktop-hyprland.nix; this manages the user's +# own hyprland.lua. +# +# Deliberately NOT migrated: +# - hyprbars.conf / hyprredsquare.conf: config for the third-party plugins +# `hyprbevelbars` + `hyprredsquare`, which aren't packaged in nixpkgs. Load +# the plugins via `wayland.windowManager.hyprland.plugins` and re-add their +# config once they're available. +# - hyprqt6engine.conf + `QT_QPA_PLATFORMTHEME=hyprqt6engine`: terra themes Qt +# through qtct/Dracula in home.nix, so that env var is left off to avoid a conflict. +# - hyprlock.conf: a separate program (use `programs.hyprlock` if wanted). +# - the duplicate pamixer/amixer + `.wob` volume binds: kept only the clean +# pipewire `wpctl`/`playerctl` set (no wob overlay is configured here). +# - `XDG_MENU_PREFIX=arch-` and `VCPKG_ROOT`: Arch-/user-specific. +# Many binds reference apps/scripts not packaged on terra yet (vivaldi-stable, +# dolphin, vicinae, grimblast, waypaper, discord, gitkraken, qbz, +# ~/.config/scripts/start-communications.sh); add them separately. + +let + lua = lib.generators.mkLuaInline; + + # Dispatchers → the new hl.dsp.* API (signatures verified against hyprland + # 0.55's src/config/lua/bindings/LuaBindingsDispatchers.cpp). + dsp = { + exec = cmd: lua ''hl.dsp.exec_cmd("${cmd}")''; + global = name: lua ''hl.dsp.global("${name}")''; + exit = lua "hl.dsp.exit()"; + killactive = lua "hl.dsp.window.close()"; + togglefloating = lua ''hl.dsp.window.float({ action = "toggle" })''; + fullscreen = lua "hl.dsp.window.fullscreen()"; # mode 0 + maximize = lua ''hl.dsp.window.fullscreen({ mode = "maximized" })''; # mode 1 + togglegroup = lua "hl.dsp.group.toggle()"; + changegroupactive = lua "hl.dsp.group.next()"; + drag = lua "hl.dsp.window.drag()"; + resizemouse = lua "hl.dsp.window.resize()"; + movefocus = dir: lua ''hl.dsp.focus({ direction = "${dir}" })''; + movewindow = dir: lua ''hl.dsp.window.move({ direction = "${dir}" })''; + resizeactive = x: y: + lua ''hl.dsp.window.resize({ x = ${toString x}, y = ${toString y}, relative = true })''; + workspace = n: lua ''hl.dsp.focus({ workspace = ${toString n} })''; + workspaceRef = s: lua ''hl.dsp.focus({ workspace = "${s}" })''; + movetoworkspace = n: lua ''hl.dsp.window.move({ workspace = ${toString n} })''; + togglespecial = name: lua ''hl.dsp.workspace.toggle_special("${name}")''; + }; + + bind = keys: dispatcher: { _args = [ keys dispatcher ]; }; + bindo = keys: dispatcher: opts: { _args = [ keys dispatcher opts ]; }; + + # SUPER + 1..9 → workspaces 1..9, SUPER + 0 → workspace 10. + wsKeys = [ + { k = "1"; n = 1; } { k = "2"; n = 2; } { k = "3"; n = 3; } + { k = "4"; n = 4; } { k = "5"; n = 5; } { k = "6"; n = 6; } + { k = "7"; n = 7; } { k = "8"; n = 8; } { k = "9"; n = 9; } + { k = "0"; n = 10; } + ]; +in +{ + wayland.windowManager.hyprland = { + enable = true; + settings = { + # ---- colours (from colors.conf) ---- + fg_color = { _var = "rgba(eeeeeeff)"; }; + fg_accent = { _var = "rgba(FFD063ff)"; }; + bg_color = { _var = "rgba(0F1012ff)"; }; + bg_accent = { _var = "rgba(292C30ff)"; }; + + # ---- monitors ---- + monitor = [ + { output = "DP-2"; mode = "2560x1440@144"; position = "1920x0"; scale = 1; } + { output = "HDMI-A-1"; mode = "1920x1080@60"; position = "0x360"; scale = 1; } + ]; + + # ---- variables (general/decoration/input/misc/...) ---- + config = { + input = { + kb_layout = "de"; + numlock_by_default = true; + follow_mouse = 1; + sensitivity = 0; + }; + + debug.disable_logs = false; + + general = { + border_size = 1; + col = { + inactive_border = lua "bg_accent"; + active_border = lua "fg_accent"; + }; + layout = "dwindle"; + gaps_in = 4; + gaps_out = 8; + }; + + decoration = { + dim_special = 0.3; + rounding = 10; + blur = { + enabled = true; + special = true; # blur behind the special workspace + size = 6; + passes = 2; + ignore_opacity = true; + }; + shadow.enabled = false; + active_opacity = 0.9; + inactive_opacity = 0.9; + }; + + animations.enabled = true; + + misc = { + close_special_on_empty = true; + disable_hyprland_logo = true; + disable_splash_rendering = true; + }; + + binds = { + hide_special_on_workspace_change = true; + workspace_back_and_forth = true; + allow_workspace_cycles = true; + }; + }; + + # ---- animations ---- + # specialWorkspace, enabled, speed 8, default curve, slidefadevert -50% + animation = [ + { leaf = "specialWorkspace"; enabled = true; speed = 8; bezier = "default"; style = "slidefadevert -50%"; } + ]; + + # ---- environment (environment.conf) ---- + env = [ + { _args = [ "HYPRCURSOR_THEME" "Bibata-Modern-Classic" ]; } + { _args = [ "HYPRCURSOR_SIZE" "24" ]; } + { _args = [ "XCURSOR_THEME" "Bibata-Modern-Classic" ]; } + { _args = [ "XCURSOR_SIZE" "24" ]; } + { _args = [ "GDK_BACKEND" "wayland,x11" ]; } + { _args = [ "SDL_VIDEODRIVER" "wayland" ]; } + { _args = [ "CLUTTER_BACKEND" "wayland" ]; } + { _args = [ "XDG_CURRENT_DESKTOP" "Hyprland" ]; } + { _args = [ "XDG_SESSION_DESKTOP" "Hyprland" ]; } + { _args = [ "XDG_SESSION_TYPE" "wayland" ]; } + ]; + + # ---- keybinds (keybinds.conf) ---- + bind = [ + (bindo "SUPER + SUPER_L" (dsp.exec "bash $HOME/.config/quickshell/open_launcher.sh") { release = true; }) + (bind "SUPER + Return" (dsp.exec "alacritty")) + + # quickshell app-launcher variants (evaluating — pick one) + ] + ++ (map (n: bind "SUPER + CTRL + ${toString n}" (dsp.global "quickshell:launcher${toString n}")) (lib.range 1 7)) + ++ [ + # restart quickshell (also starts it if not running) + (bind "SUPER + CTRL + 0" (dsp.exec "qs kill; sleep 0.3; qs")) + # toggle the Slant sidebar + (bind "SUPER + CTRL + S" (dsp.global "quickshell:sidebar")) + + (bind "SUPER + B" (dsp.exec "vivaldi")) + (bind "SUPER + E" (dsp.exec "dolphin")) + (bind "SUPER + SHIFT + G" (dsp.workspaceRef "game")) + (bind "SUPER + S" (dsp.exec "grim -o DP-2 ~/screenshot.png")) + (bind "SUPER + SHIFT + S" (dsp.exec "grimblast copy area --freeze")) + (bind "SUPER + L" (dsp.exec "hyprlock")) + (bind "SUPER + W" (dsp.exec "waypaper --random")) + + # window management + (bind "SUPER + Q" dsp.killactive) + (bind "SUPER + SHIFT + Q" dsp.exit) + (bind "SUPER + F" dsp.maximize) + (bind "SUPER + SHIFT + F" dsp.fullscreen) + (bind "SUPER + Space" dsp.togglefloating) + + # focus + (bind "SUPER + left" (dsp.movefocus "left")) + (bind "SUPER + right" (dsp.movefocus "right")) + (bind "SUPER + up" (dsp.movefocus "up")) + (bind "SUPER + down" (dsp.movefocus "down")) + + # move + (bind "SUPER + SHIFT + left" (dsp.movewindow "left")) + (bind "SUPER + SHIFT + right" (dsp.movewindow "right")) + (bind "SUPER + SHIFT + up" (dsp.movewindow "up")) + (bind "SUPER + SHIFT + down" (dsp.movewindow "down")) + + # resize + (bind "SUPER + CTRL + left" (dsp.resizeactive (-20) 0)) + (bind "SUPER + CTRL + right" (dsp.resizeactive 20 0)) + (bind "SUPER + CTRL + up" (dsp.resizeactive 0 (-20))) + (bind "SUPER + CTRL + down" (dsp.resizeactive 0 20)) + + # tabbed / group + (bind "SUPER + g" dsp.togglegroup) + (bind "SUPER + tab" dsp.changegroupactive) + + # special workspaces + (bind "SUPER + T" (dsp.togglespecial "terminal")) + (bind "SUPER + C" (dsp.togglespecial "communications")) + (bind "SUPER + M" (dsp.togglespecial "music")) + (bind "SUPER + V" (dsp.togglespecial "version_control")) + + # cycle workspaces + (bind "SUPER + ALT + up" (dsp.workspaceRef "e+1")) + (bind "SUPER + ALT + down" (dsp.workspaceRef "e-1")) + + # mouse move / resize + (bindo "SUPER + mouse:272" dsp.drag { mouse = true; }) + (bindo "SUPER + mouse:273" dsp.resizemouse { mouse = true; }) + + # multimedia (pipewire) + (bindo "XF86AudioRaiseVolume" (dsp.exec "wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+") { repeating = true; }) + (bindo "XF86AudioLowerVolume" (dsp.exec "wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-") { repeating = true; }) + (bind "XF86AudioMute" (dsp.exec "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle")) + (bind "XF86AudioPlay" (dsp.exec "playerctl play-pause")) + (bind "XF86AudioPause" (dsp.exec "playerctl play-pause")) + (bind "XF86AudioNext" (dsp.exec "playerctl next")) + (bind "XF86AudioPrev" (dsp.exec "playerctl previous")) + ] + ++ (map (w: bind "SUPER + ${w.k}" (dsp.workspace w.n)) wsKeys) + ++ (map (w: bind "SUPER + SHIFT + ${w.k}" (dsp.movetoworkspace w.n)) wsKeys); + + # ---- window rules (windowrules.conf) ---- + window_rule = [ + { name = "games"; match.class = "gamescope"; workspace = "name:game"; opacity = "1 1 override"; } + { name = "vivaldi"; match.title = "(.*)(- YouTube - Vivaldi)"; opacity = "1 1 override"; } + { name = "jetbrains"; match.class = "(jetbrains-)(.*)"; no_initial_focus = true; float = true; center = true; } + { name = "jetbrains-toolbox"; match.class = "jetbrains-toolbox"; move = "1933 21"; } + { name = "comms"; match.class = "discord|org.telegram.desktop"; workspace = "special:communications"; } + { name = "music"; match.class = "qbz"; workspace = "special:music"; } + { name = "vicinae"; match.class = "vicinae"; stay_focused = true; } + { name = "gitkraken"; match.class = "gitkraken"; workspace = "special:version_control"; } + ]; + + # ---- workspace rules (workspacerules.conf) ---- + workspace_rule = [ + { workspace = "name:game"; monitor = "DP-2"; decorate = false; } + { workspace = "special:terminal"; on_created_empty = "alacritty"; gaps_out = 256; } + { workspace = "special:communications"; on_created_empty = "$HOME/.config/scripts/start-communications.sh"; gaps_out = 128; } + { workspace = "special:music"; on_created_empty = "com.blitzfc.qbz"; gaps_out = 128; } + { workspace = "special:version_control"; on_created_empty = "com.axosoft.GitKraken"; gaps_out = 128; } + ]; + + # ---- autostart (autostart.conf) ---- + on = { + _args = [ + "hyprland.start" + (lua '' + function() + hl.exec_cmd("systemctl --user start hyprpolkitagent") + hl.exec_cmd("gsettings set org.gnome.desktop.interface color-scheme 'prefer-dark'") + hl.exec_cmd("vicinae server") + hl.exec_cmd("quickshell") + hl.exec_cmd("alacritty", { workspace = "special:terminal silent" }) + hl.exec_cmd("kbuildsycoca6 --noincremental") + end'') + ]; + }; + }; + }; +} diff --git a/scripts/keys b/scripts/keys new file mode 100755 index 0000000..6625394 --- /dev/null +++ b/scripts/keys @@ -0,0 +1,175 @@ +#!/usr/bin/env bash +# Manage homelab SSH host keys and sops age keys in the Proton Pass HomeLab vault. +# +# Vault naming (HomeLab vault; override with HOMELAB_PASS_VAULT): +# +# ssh_host# ssh-key item ↔ ~/.config/homelab//ssh_host_ed25519_key{,.pub} +# age# note item ↔ ~/.config/homelab//age.txt +# age#admin note item ↔ ~/.config/sops/age/keys.txt +# +# Usage: +# ./scripts/keys store [--force] [...] +# ./scripts/keys restore [...] +# +# store: upload local keys to the vault. Skips items that already exist +# unless --force is given (deletes the existing item first). +# restore: download vault items to local files with correct permissions. +# +# With no args both subcommands operate on every hosts/ directory. +# The admin age key is always included regardless of args. +set -euo pipefail + +SCRIPT_DIR="$(dirname "$(realpath "$0")")" +REPO="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || dirname "$SCRIPT_DIR")" +cd "$REPO" + +KEYDIR="${HOMELAB_KEY_DIR:-${HOME:-/root}/.config/homelab}" +ADMIN_AGE="${HOME:-/root}/.config/sops/age/keys.txt" +VAULT="${HOMELAB_PASS_VAULT:-HomeLab}" + +die() { echo "error: $*" >&2; exit 1; } +need() { command -v "$1" >/dev/null 2>&1 || die "missing required tool: $1"; } + +need pass-cli +need jq + +# Resolve an active vault item by title → item ID, or empty string. +# Active-only filter avoids the trashed-item-shadows-active bug (see deploy). +resolve_item() { + local title="$1" + pass-cli item list --vault-name "$VAULT" --filter-state active --output human 2>/dev/null \ + | awk -v t="$title" ' + { i = index($0, "]: "); if (i == 0) next + id = substr($0, 4, i - 4) + rest = substr($0, i + 3) + sub(/ \(state=[^)]*\)$/, "", rest) + if (rest == t) { print id; exit } }' || true +} + +# All configs: every hosts/ that has a configuration.nix. +all_configs() { + for d in hosts/*/; do + [ -f "${d}configuration.nix" ] && basename "$d" + done +} + +# --------------------------------------------------------------------------- +# store helpers +# --------------------------------------------------------------------------- + +# Returns 0 (skip) if item exists and --force was not given; 1 (proceed) otherwise. +# Deletes the existing item when --force is set. +should_store() { + local title="$1" id + id="$(resolve_item "$title")" + if [ -n "$id" ]; then + if [ -n "$FORCE" ]; then + echo ">> $title: deleting existing item (--force)" + pass-cli item delete --vault-name "$VAULT" --item-id "$id" + else + echo ">> $title: already in vault — skipping (use --force to overwrite)" + return 0 + fi + fi + return 1 +} + +store_ssh_key() { + local config="$1" keyfile="$KEYDIR/$1/ssh_host_ed25519_key" title="ssh_host#$1" + [ -f "$keyfile" ] || { echo ">> $title: $keyfile not found — skipping"; return; } + should_store "$title" && return + echo ">> $title: uploading" + pass-cli item create ssh-key import \ + --from-private-key "$keyfile" \ + --title "$title" \ + --vault-name "$VAULT" +} + +store_age_key() { + local title="$1" src="$2" + [ -f "$src" ] || { echo ">> $title: $src not found — skipping"; return; } + should_store "$title" && return + echo ">> $title: uploading" + pass-cli item create note \ + --title "$title" \ + --note "$(cat "$src")" \ + --vault-name "$VAULT" +} + +# --------------------------------------------------------------------------- +# restore helpers +# --------------------------------------------------------------------------- + +restore_ssh_key() { + local config="$1" title="ssh_host#$1" id json + id="$(resolve_item "$title")" + if [ -z "$id" ]; then + echo ">> $title: not in vault — skipping" + return + fi + echo ">> $title: restoring" + json="$(pass-cli item view --vault-name "$VAULT" --item-id "$id" --output json)" + private_key="$(jq -r '.item.content.content.SshKey.private_key' <<< "$json")" + public_key="$(jq -r '.item.content.content.SshKey.public_key' <<< "$json")" + [ -n "$private_key" ] || die "$title: private_key missing from vault item JSON" + [ -n "$public_key" ] || die "$title: public_key missing from vault item JSON" + mkdir -p "$KEYDIR/$config" + printf '%s' "$private_key" > "$KEYDIR/$config/ssh_host_ed25519_key" + chmod 600 "$KEYDIR/$config/ssh_host_ed25519_key" + printf '%s\n' "$public_key" > "$KEYDIR/$config/ssh_host_ed25519_key.pub" + chmod 644 "$KEYDIR/$config/ssh_host_ed25519_key.pub" + echo " → $KEYDIR/$config/ssh_host_ed25519_key{,.pub}" +} + +restore_age_key() { + local title="$1" dest="$2" id content + id="$(resolve_item "$title")" + if [ -z "$id" ]; then + echo ">> $title: not in vault — skipping" + return + fi + echo ">> $title: restoring" + content="$(pass-cli item view --vault-name "$VAULT" --item-id "$id" \ + --output json | jq -r '.item.content.note')" + [ -n "$content" ] || die "$title: note field is empty in vault item" + mkdir -p "$(dirname "$dest")" + printf '%s\n' "$content" > "$dest" + chmod 600 "$dest" + echo " → $dest" +} + +# --------------------------------------------------------------------------- +# main +# --------------------------------------------------------------------------- + +cmd="${1:-}" +[ -n "$cmd" ] || die "usage: ./scripts/keys [--force] [...]" +shift + +FORCE="" +[ "${1:-}" = "--force" ] && { FORCE=1; shift; } + +configs=("$@") +[ "${#configs[@]}" -gt 0 ] || mapfile -t configs < <(all_configs) + +case "$cmd" in + store) + for config in "${configs[@]}"; do + store_ssh_key "$config" + store_age_key "age#$config" "$KEYDIR/$config/age.txt" + done + store_age_key "age#admin" "$ADMIN_AGE" + ;; + + restore) + for config in "${configs[@]}"; do + restore_ssh_key "$config" + restore_age_key "age#$config" "$KEYDIR/$config/age.txt" + done + restore_age_key "age#admin" "$ADMIN_AGE" + ;; + + *) + die "unknown command '$cmd' — usage: ./scripts/keys [--force] [...]" + ;; +esac diff --git a/services/desktop/desktop-apps.nix b/services/desktop/desktop-apps.nix index 14820ed..74dab2a 100644 --- a/services/desktop/desktop-apps.nix +++ b/services/desktop/desktop-apps.nix @@ -35,10 +35,6 @@ in kdePackages.dolphin vivaldi rishot - # tome removed for now — private git+ssh flake input the installer can't - # fetch. The dev SDKs below stay (public, cached) for when it comes back. - - # Tome development (Tome.App targets net10.0; ClientApp is Preact/Vite). dotnetCorePackages.sdk_10_0 nodejs ]; diff --git a/services/desktop/desktop-hyprland.nix b/services/desktop/desktop-hyprland.nix index 39489f2..f95cd7c 100644 --- a/services/desktop/desktop-hyprland.nix +++ b/services/desktop/desktop-hyprland.nix @@ -5,10 +5,14 @@ { programs.hyprland.enable = true; + services.gnome.gnome-keyring.enable = true; + security.pam.services.login.enableGnomeKeyring = true; + security.pam.services.greetd.enableGnomeKeyring = true; + services.greetd = { enable = true; settings.default_session.command = - "${pkgs.tuigreet}/bin/tuigreet --time --cmd Hyprland"; + "${pkgs.tuigreet}/bin/tuigreet --time --cmd start-hyprland"; }; # Audio (pipewire replaces pulseaudio/jack).