diff --git a/.sops.yaml b/.sops.yaml index aaf3e03..e10bd39 100644 --- a/.sops.yaml +++ b/.sops.yaml @@ -3,6 +3,7 @@ keys: - &muho age1v4s4hg7u3vjjkarvrk7v6ev7w3wja2r5xm7f4t06culw3fuq7qns8sfju7 - &mups age1n7qz2w3hkf7fcdv92kxw9k6uef487na2tlc87486rcjwj8lyfuws5q46gn - &murk age1mgjhkqy9x27gv2t2xvq46dxcajkr9c8zes7rr3dj0ac7md2j6vas43dftp + - &musk age1m97a3eptxwpdd7h5kkqe9gkmhg6rquc64qjmlsfqfhfqv8q72crqrylhgc creation_rules: - path_regex: modules/nixos/sops/secrets.ya?ml$ @@ -12,6 +13,7 @@ creation_rules: - *muho - *mups - *murk + - *musk - path_regex: modules/home/sops/secrets.ya?ml$ key_groups: @@ -20,3 +22,4 @@ creation_rules: - *muho - *mups - *murk + - *musk diff --git a/flake.lock b/flake.lock index f958a84..5ed46c3 100644 --- a/flake.lock +++ b/flake.lock @@ -139,11 +139,11 @@ ] }, "locked": { - "lastModified": 1760948891, - "narHash": "sha256-TmWcdiUUaWk8J4lpjzu4gCGxWY6/Ok7mOK4fIFfBuU4=", + "lastModified": 1769996383, + "narHash": "sha256-AnYjnFWgS49RlqX7LrC4uA+sCCDBj0Ry/WOJ5XWAsa0=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "864599284fc7c0ba6357ed89ed5e2cd5040f0c04", + "rev": "57928607ea566b5db3ad13af0e57e921e6b12381", "type": "github" }, "original": { @@ -233,11 +233,11 @@ ] }, "locked": { - "lastModified": 1768598210, - "narHash": "sha256-kkgA32s/f4jaa4UG+2f8C225Qvclxnqs76mf8zvTVPg=", + "lastModified": 1773367248, + "narHash": "sha256-FFMc1uAwy2GYasd0rdNDVxKyAgzuoJH2M+GglBQbqf0=", "owner": "nix-community", "repo": "home-manager", - "rev": "c47b2cc64a629f8e075de52e4742de688f930dc6", + "rev": "be0c641a6a5564caa33982faa1fe2c60d92131c7", "type": "github" }, "original": { @@ -254,11 +254,11 @@ ] }, "locked": { - "lastModified": 1747978958, - "narHash": "sha256-pQQnbxWpY3IiZqgelXHIe/OAE/Yv4NSQq7fch7M6nXQ=", + "lastModified": 1768598210, + "narHash": "sha256-kkgA32s/f4jaa4UG+2f8C225Qvclxnqs76mf8zvTVPg=", "owner": "nix-community", "repo": "home-manager", - "rev": "7419250703fd5eb50e99bdfb07a86671939103ea", + "rev": "c47b2cc64a629f8e075de52e4742de688f930dc6", "type": "github" }, "original": { @@ -275,11 +275,11 @@ ] }, "locked": { - "lastModified": 1767104570, - "narHash": "sha256-GKgwu5//R+cLdKysZjGqvUEEOGXXLdt93sNXeb2M/Lk=", + "lastModified": 1772330611, + "narHash": "sha256-UZjPc/d5XRxvjDbk4veAO4XFdvx6BUum2l40V688Xq8=", "owner": "nix-community", "repo": "home-manager", - "rev": "e4e78a2cbeaddd07ab7238971b16468cc1d14daf", + "rev": "58fd7ff0eec2cda43e705c4c0585729ec471d400", "type": "github" }, "original": { @@ -294,11 +294,11 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1767822991, - "narHash": "sha256-iyrn9AcPZCoyxX4OT8eMkBsjG7SRUQXXS/V1JzxS7rA=", + "lastModified": 1769548169, + "narHash": "sha256-03+JxvzmfwRu+5JafM0DLbxgHttOQZkUtDWBmeUkN8Y=", "owner": "nix-community", "repo": "impermanence", - "rev": "82e5bc4508cab9e8d5a136626276eb5bbce5e9c5", + "rev": "7b1d382faf603b6d264f58627330f9faa5cba149", "type": "github" }, "original": { @@ -309,11 +309,11 @@ }, "mnw": { "locked": { - "lastModified": 1758834834, - "narHash": "sha256-Y7IvY4F8vajZyp3WGf+KaiIVwondEkMFkt92Cr9NZmg=", + "lastModified": 1770419553, + "narHash": "sha256-b1XqsH7AtVf2dXmq2iyRr2NC1yG7skY7Z6N2MpWHlK4=", "owner": "Gerg-L", "repo": "mnw", - "rev": "cfbc7d1cc832e318d0863a5fc91d940a96034001", + "rev": "2aaffa8030d0b262176146adbb6b0e6374ce2957", "type": "github" }, "original": { @@ -330,15 +330,16 @@ ] }, "locked": { - "lastModified": 1765720983, - "narHash": "sha256-tWtukpABmux6EC/FuCJEgA1kmRjcRPtED44N+GGPq+4=", + "lastModified": 1768214250, + "narHash": "sha256-hnBZDQWUxJV3KbtvyGW5BKLO/fAwydrxm5WHCWMQTbw=", "owner": "feel-co", "repo": "ndg", - "rev": "f399ace8bb8e1f705dd8942b24d207aa4d75c936", + "rev": "a6bd3c1ce2668d096e4fdaaa03ad7f03ba1fbca8", "type": "github" }, "original": { "owner": "feel-co", + "ref": "refs/tags/v2.6.0", "repo": "ndg", "type": "github" } @@ -350,11 +351,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1768475717, - "narHash": "sha256-185VOlWF4K9gzwr7M56ArjqDt6beN/5TxCYLEyVPOcs=", + "lastModified": 1771150922, + "narHash": "sha256-+oQJun4CFDlOQRocbZpqQDj7agoy56/4ZjT1oUR7NOs=", "owner": "thiagokokada", "repo": "nix-alien", - "rev": "a579610c67dc946f39c2a64656699eb29eb2ffb5", + "rev": "96045e886ba0dd45b27590e7c0c6e77bbb54033d", "type": "github" }, "original": { @@ -387,11 +388,11 @@ ] }, "locked": { - "lastModified": 1765267181, - "narHash": "sha256-d3NBA9zEtBu2JFMnTBqWj7Tmi7R5OikoU2ycrdhQEws=", + "lastModified": 1771130777, + "narHash": "sha256-UIKOwG0D9XVIJfNWg6+gENAvQP+7LO46eO0Jpe+ItJ0=", "owner": "nix-community", "repo": "nix-index-database", - "rev": "82befcf7dc77c909b0f2a09f5da910ec95c5b78f", + "rev": "efec7aaad8d43f8e5194df46a007456093c40f88", "type": "github" }, "original": { @@ -422,11 +423,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1748026106, - "narHash": "sha256-6m1Y3/4pVw1RWTsrkAK2VMYSzG4MMIj7sqUy7o8th1o=", + "lastModified": 1768564909, + "narHash": "sha256-Kell/SpJYVkHWMvnhqJz/8DqQg2b6PguxVWOuadbHCc=", "owner": "nixos", "repo": "nixpkgs", - "rev": "063f43f2dbdef86376cc29ad646c45c46e93234c", + "rev": "e4bae1bd10c9c57b2cf517953ab70060a828ee6f", "type": "github" }, "original": { @@ -438,11 +439,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1768305791, - "narHash": "sha256-AIdl6WAn9aymeaH/NvBj0H9qM+XuAuYbGMZaP0zcXAQ=", + "lastModified": 1771008912, + "narHash": "sha256-gf2AmWVTs8lEq7z/3ZAsgnZDhWIckkb+ZnAo5RzSxJg=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "1412caf7bf9e660f2f962917c14b1ea1c3bc695e", + "rev": "a82ccc39b39b621151d6732718e3e250109076fa", "type": "github" }, "original": { @@ -470,11 +471,11 @@ }, "nixpkgs_4": { "locked": { - "lastModified": 1768564909, - "narHash": "sha256-Kell/SpJYVkHWMvnhqJz/8DqQg2b6PguxVWOuadbHCc=", + "lastModified": 1773282481, + "narHash": "sha256-b/GV2ysM8mKHhinse2wz+uP37epUrSE+sAKXy/xvBY4=", "owner": "nixos", "repo": "nixpkgs", - "rev": "e4bae1bd10c9c57b2cf517953ab70060a828ee6f", + "rev": "fe416aaedd397cacb33a610b33d60ff2b431b127", "type": "github" }, "original": { @@ -537,16 +538,15 @@ "systems": "systems_2" }, "locked": { - "lastModified": 1767628834, - "narHash": "sha256-qiPFYDicHq4/ji0/9QxVM8hhjspsJrYcMR/S3zKlfjQ=", - "owner": "thamenato", + "lastModified": 1773343795, + "narHash": "sha256-0+HEuOytpwyPt7i1jj6v2QJ+NXXisCYnL2XNwPBltvg=", + "owner": "NotAShelf", "repo": "nvf", - "rev": "7161c8d857cf7c641433cc750a1a3666f82a3ff0", + "rev": "83b44eaf50b96bd5d06b1a56a3a51f1b2362db52", "type": "github" }, "original": { - "owner": "thamenato", - "ref": "fix-nvim-treesitter", + "owner": "NotAShelf", "repo": "nvf", "type": "github" } @@ -573,11 +573,11 @@ ] }, "locked": { - "lastModified": 1768481291, - "narHash": "sha256-NjKtkJraCZEnLHAJxLTI+BfdU//9coAz9p5TqveZwPU=", + "lastModified": 1773096132, + "narHash": "sha256-M3zEnq9OElB7zqc+mjgPlByPm1O5t2fbUrH3t/Hm5Ag=", "owner": "Mic92", "repo": "sops-nix", - "rev": "e085e303dfcce21adcb5fec535d65aacb066f101", + "rev": "d1ff3b1034d5bab5d7d8086a7803c5a5968cd784", "type": "github" }, "original": { @@ -626,11 +626,11 @@ "tinted-zed": "tinted-zed" }, "locked": { - "lastModified": 1768603455, - "narHash": "sha256-ih6dYNhX1oSg0emfSAvf3iRcgsJtMmS6RUaoCX8kNoU=", + "lastModified": 1772296853, + "narHash": "sha256-pAtzPsgHRKw/2Kv8HgAjSJg450FDldHPWsP3AKG/Xj0=", "owner": "danth", "repo": "stylix", - "rev": "590e5c68c4d5e8c766420473c0185d75113f653b", + "rev": "c4b8e80a1020e09a1f081ad0f98ce804a6e85acf", "type": "github" }, "original": { @@ -794,11 +794,11 @@ ] }, "locked": { - "lastModified": 1768638486, - "narHash": "sha256-+LC0wOiliUXbIj6zT2hCoOQ0zn33BD2NxGoy0QqP3Eo=", + "lastModified": 1773290887, + "narHash": "sha256-L1yMYmFffHfZNP+hKJGRBmrFKkn/VDhu7jEbVftBQuM=", "owner": "0xc000022070", "repo": "zen-browser-flake", - "rev": "76bbc35c59419b8b0616fb779ce5600e85edab11", + "rev": "9346698c4562819f61b4e5097151ec0b17729fab", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index f478b06..a84ead5 100644 --- a/flake.nix +++ b/flake.nix @@ -14,8 +14,7 @@ nix-alien.url = "github:thiagokokada/nix-alien"; nvf = { - # url = "github:NotAShelf/nvf"; - url = "github:thamenato/nvf/fix-nvim-treesitter"; + url = "github:NotAShelf/nvf"; inputs.nixpkgs.follows = "nixpkgs"; }; @@ -47,35 +46,32 @@ }; utils = import ./utils.nix {inherit inputs system sources;}; + + # Discover hosts: all subdirectories of hosts/ + hosts = builtins.attrNames (nixpkgs.lib.filterAttrs + (_: type: type == "directory") + (builtins.readDir ./hosts)); + + nixosConfigs = builtins.listToAttrs (map (host: { + name = host; + value = utils.mkHost ./hosts/${host}/configuration.nix; + }) hosts); in { - nixosConfigurations = { - # desktop - muon = utils.mkHost ./hosts/muon/configuration.nix; - - # laptop - muop = utils.mkHost ./hosts/muop/configuration.nix; - - # server - muho = utils.mkHost ./hosts/muho/configuration.nix; - - # vps - mups = utils.mkHost ./hosts/mups/configuration.nix; - - # vm - muvm = utils.mkHost ./hosts/muvm/configuration.nix; - - # work - murk = utils.mkHost ./hosts/murk/configuration.nix; - - # lenovo - muvo = utils.mkHost ./hosts/muvo/configuration.nix; - - # installer - muin = utils.mkHost ./hosts/muin/configuration.nix; - }; + nixosConfigurations = nixosConfigs; homeManagerModules.default = ./modules/home; + # Standalone HM configurations — one per host. + # osConfig is injected so all modules using it continue to work. + # Use: home-manager switch --flake '.#muon@' + homeConfigurations = builtins.listToAttrs (map (host: { + name = "muon@${host}"; + value = utils.mkHome { + hostConfig = nixosConfigs.${host}; + homeFile = ./hosts/${host}/home.nix; + }; + }) hosts); + colmena = { meta = { nixpkgs = import inputs.nixpkgs {inherit system;}; diff --git a/hosts/muho/configuration.nix b/hosts/muho/configuration.nix index 8e3315d..3e41fbb 100644 --- a/hosts/muho/configuration.nix +++ b/hosts/muho/configuration.nix @@ -50,6 +50,7 @@ in { mods.server.ntfy.enable = true; mods.server.lemmy.enable = true; mods.server.audio.enable = true; + mods.server.murmur.enable = true; mods.server.atuin.enable = true; mods.server.seedbox.enable = true; diff --git a/hosts/muho/home.nix b/hosts/muho/home.nix index 84075d4..19bfdad 100644 --- a/hosts/muho/home.nix +++ b/hosts/muho/home.nix @@ -10,6 +10,8 @@ in { mods.xdg.enable = true; mods.social.enable = false; mods.i3.enable = false; + mods.terminal.wezterm.enable = true; + mods.terminal.nushell.enable = true; mods.terminal.zsh.enable = true; mods.terminal.emulator.enable = false; mods.terminal.development.enable = true; diff --git a/hosts/muon/configuration.nix b/hosts/muon/configuration.nix index 615d210..5166155 100644 --- a/hosts/muon/configuration.nix +++ b/hosts/muon/configuration.nix @@ -24,8 +24,16 @@ in { gnumeric opensnitch opensnitch-ui + mumble ]; + nixpkgs.config.permittedInsecurePackages = [ + "libsoup-2.74.3" + ]; + + users.users.muon.extraGroups = ["docker"]; + virtualisation.docker.enable = true; + # System mods.user.name = "muon"; networking.hostName = cfg.user.name; @@ -61,6 +69,20 @@ in { services.xserver.windowManager.i3.enable = true; services.actual.enable = true; + # Reverse proxy: *.word.local -> localhost:3030 + services.nginx = { + enable = true; + recommendedProxySettings = true; + virtualHosts."~^(?.+)\\.word\\.local$" = { + serverName = "~^(?.+)\\.word\\.local$"; + locations."/" = { + proxyPass = "http://127.0.0.1:3030"; + proxyWebsockets = true; + }; + }; + }; + networking.firewall.allowedTCPPorts = [ 80 ]; + virtualisation.virtualbox.host.enable = true; users.extraGroups.vboxusers.members = ["user-with-access-to-virtualbox"]; diff --git a/hosts/muon/home.nix b/hosts/muon/home.nix index 063bfdd..101a66b 100644 --- a/hosts/muon/home.nix +++ b/hosts/muon/home.nix @@ -10,15 +10,21 @@ in { mods.xdg.enable = true; mods.social.enable = true; mods.i3.enable = true; + mods.terminal.wezterm.enable = true; + mods.terminal.nushell.enable = true; mods.terminal.zsh.enable = true; mods.terminal.emulator.enable = true; mods.terminal.development.enable = true; mods.terminal.tools.enable = true; + mods.terminal.hr.enable = true; + mods.terminal.gh.enable = true; mods.desktop.development.enable = true; mods.desktop.productivity.enable = true; mods.desktop.media.enable = true; mods.zen.enable = true; mods.obsidian.enable = true; + mods.opencode.enable = true; + mods.octo.enable = true; # Hardware preferences @@ -64,6 +70,16 @@ in { input.sensitivity = -0.4; }; + # SSH: auto-add work key to gpg-agent on first use + programs.ssh = { + enable = true; + matchBlocks."*" = { + identityFile = "~/.ssh/work_ed25519"; + addKeysToAgent = "yes"; + }; + matchBlocks."muho".identityFile = "~/.ssh/id_ed25519"; + }; + # Version of first install home.stateVersion = "23.05"; } diff --git a/hosts/murk/home.nix b/hosts/murk/home.nix index cf923d1..e7fa8c3 100644 --- a/hosts/murk/home.nix +++ b/hosts/murk/home.nix @@ -19,11 +19,15 @@ in { mods.terminal.emulator.enable = true; mods.terminal.development.enable = true; mods.terminal.tools.enable = true; + mods.terminal.gh.enable = true; + mods.terminal.hr.enable = true; mods.desktop.development.enable = true; mods.desktop.productivity.enable = false; mods.zen.enable = true; mods.obsidian.enable = true; mods.theme.slideshow.enable = true; + mods.octo.enable = true; + mods.theme.slideshow = true; home.packages = with pkgs; [ @@ -43,7 +47,6 @@ in { go rainfrog tealdeer - gh (callPackage ./packages/mender-cli.nix {}) ] # Non-free /tmp/ss.png;if [ ! -s /tmp/ss.png ]; then - exit 1 - fi - AUTH=$(cat ${config.sops.secrets.zipline-auth.path}) - curl -H "authorization: $AUTH" https://share.muon.host/api/upload -F file=@/tmp/ss.png -H "Content-Type: multipart/form-data" -H "Format: date" -H "Image-Compression-Percent: 90" -H "No-JSON: true" | tr -d '\n' | xsel -ib; - ''; - }; + # Bootstrap script that runs as the first program inside a new wezterm window. + # It sets up the three tabs synchronously before the shell prompt appears, + # so by the time the window is visible the layout is already complete. + # Receives the project path as $1. + wezterm-zmenu-init = pkgs.writeShellApplication { + name = "wezterm-zmenu-init"; + runtimeInputs = [pkgs.wezterm pkgs.zsh]; + text = '' + ZPATH=$1 + WIN=$(wezterm cli list --format json 2>/dev/null \ + | tr ',' '\n' \ + | awk -v pane="$WEZTERM_PANE" ' + /window_id/ { gsub(/.*: */,""); w=int($0) } + /pane_id/ { gsub(/.*: */,""); if (int($0)==pane) { print w; exit } }') + P1=$(wezterm cli spawn --window-id "$WIN" --cwd "$ZPATH" -- direnv exec . nvim) + wezterm cli spawn --window-id "$WIN" --cwd "$ZPATH" > /dev/null + wezterm cli spawn --window-id "$WIN" --cwd "$ZPATH" -- lazygit > /dev/null + wezterm cli activate-tab --tab-index 0 --pane-id "$P1" 2>/dev/null + ''; + }; + zmenu = with pkgs; writeShellApplication { name = "zmenu"; - runtimeInputs = [zellij zoxide wmctrl i3 rofi alacritty zsh]; - text = '' - ZPATH=$(zoxide query -l | sed -e "s|$HOME/||g" | rofi -dmenu) - [[ -z "$ZPATH" ]] && exit - ZSESH=$(echo "$ZPATH" | tr / -) - ZWIND=$(wmctrl -l | grep "$ZSESH$" || echo "") - cd "$ZPATH" - if [[ -z "$ZWIND" ]]; then - alacritty -T "$ZSESH" -e zsh -c "zellij -s $ZSESH -n dev || zellij a $ZSESH" - else - wmctrl -a "$ZSESH" - fi - ''; + runtimeInputs = + [zoxide wmctrl i3 rofi zsh] + ++ lib.optionals config.mods.terminal.wezterm.enable [wezterm] + ++ lib.optionals (!config.mods.terminal.wezterm.enable) [zellij alacritty]; + text = + if config.mods.terminal.wezterm.enable + then '' + ZPATH=$(zoxide query -l | sed -e "s|$HOME/||g" | rofi -dmenu) + [[ -z "$ZPATH" ]] && exit + ZSESH=$(echo "$ZPATH" | tr / -) + + # Per-workspace socket registry so we can focus existing workspaces. + SOCKDIR="$XDG_RUNTIME_DIR/zmenu-wez" + mkdir -p "$SOCKDIR" + SOCKFILE="$SOCKDIR/$ZSESH" + + SOCK="" + if [[ -f "$SOCKFILE" ]]; then + SOCK=$(cat "$SOCKFILE") + # Extract the PID from the socket path (gui-sock-) and check + # if the process is still alive. Dead sockets linger on disk. + SOCK_PID=''${SOCK##*-} + if ! kill -0 "$SOCK_PID" 2>/dev/null; then + rm -f "$SOCKFILE" + SOCK="" + fi + fi + + if [[ -n "$SOCK" ]]; then + # Workspace open: raise its window by matching the wezterm PID in + # wmctrl's process list, then activate a pane to ensure focus. + SOCK_PID=''${SOCK##*-} + WIN_HEX=$(wmctrl -lp | awk -v pid="$SOCK_PID" '$3==pid {print $1; exit}') + wmctrl -i -a "$WIN_HEX" + PANE=$(WEZTERM_UNIX_SOCKET="$SOCK" wezterm cli list --format json 2>/dev/null \ + | tr ',' '\n' \ + | awk '/pane_id/{gsub(/.*: */,""); print int($0); exit}') + WEZTERM_UNIX_SOCKET="$SOCK" wezterm cli activate-pane --pane-id "$PANE" 2>/dev/null + else + # Not open: the init script runs as the first program inside the new + # window. It sets up all tabs synchronously before the shell prompt + # appears, so the layout is complete before anything is visible. + wezterm start --always-new-process \ + --workspace "$ZSESH" --cwd "$HOME/$ZPATH" \ + -- ${lib.getExe wezterm-zmenu-init} "$HOME/$ZPATH" & + WEZ_PID=$! + + # Record the socket for future focus calls. + for _ in $(seq 50); do + sleep 0.1 + [[ -S /run/user/$UID/wezterm/gui-sock-$WEZ_PID ]] && break + done + echo "/run/user/$UID/wezterm/gui-sock-$WEZ_PID" > "$SOCKFILE" + fi + '' + else '' + ZPATH=$(zoxide query -l | sed -e "s|$HOME/||g" | rofi -dmenu) + [[ -z "$ZPATH" ]] && exit + ZSESH=$(echo "$ZPATH" | tr / -) + ZWIND=$(wmctrl -l | grep "$ZSESH$" || echo "") + cd "$HOME/$ZPATH" + if [[ -z "$ZWIND" ]]; then + alacritty -T "$ZSESH" -e zsh -c "zellij -s $ZSESH -n dev || zellij a $ZSESH" + else + wmctrl -a "$ZSESH" + fi + ''; }; in with lib; { @@ -106,7 +169,10 @@ in enable = true; config = { modifier = modifier; - terminal = "alacritty"; + terminal = + if config.mods.terminal.wezterm.enable + then "wezterm" + else "alacritty"; menu = "rofi -show drun"; window = { @@ -155,8 +221,10 @@ in // { "XF86AudioRaiseVolume" = "exec --no-startup-id pactl set-sink-volume 0 +2%"; "XF86AudioLowerVolume" = "exec --no-startup-id pactl set-sink-volume 0 -2%"; - "Print" = "exec ${getExe fsss}"; + "Print" = "exec flameshot gui -c -s"; "${modifier}+z" = "exec ${getExe zmenu}"; + "${modifier}+p" = "exec clipmenu"; + "${modifier}+b" = "exec ${getExe pkgs.rofi-rbw-x11}"; "${modifier}+y" = "sticky toggle"; "${modifier}+g" = "floating toggle"; }); diff --git a/modules/home/desktop/packages/librediscord.nix b/modules/home/desktop/packages/librediscord.nix new file mode 100644 index 0000000..d5250b5 --- /dev/null +++ b/modules/home/desktop/packages/librediscord.nix @@ -0,0 +1,54 @@ +{ + fetchFromGitLab, + gtk3, + libsoup_2_4, + json-glib, + leveldb, + rtaudio, + libopus, + libsodium, + libsecret, + pkg-config, + stdenv, + lib, +}: let + version = "0.1.0"; +in + stdenv.mkDerivation { + inherit version; + + pname = "LibreDiscord"; + + src = fetchFromGitLab { + owner = "Zipdox"; + repo = "LibreDiscord"; + rev = "a8130fab059e69437f30e320374fe6f5d21398f8"; + hash = "sha256-yr2pxW0e2ruMnDzkQMv2BQrOcN18m8zdzovnD4Dxr3M="; + }; + + nativeBuildInputs = [ + pkg-config + ]; + + buildInputs = [ + gtk3 + libsoup_2_4 + json-glib + leveldb + rtaudio + libopus + libsodium + libsecret + ]; + + installPhase = '' + mkdir -p $out/bin + cp build/librediscord $out/bin + ''; + + meta = { + description = "Voice client for Discord written in C using GTK3 and GLib"; + homepage = "https://gitlab.com/Zipdox/LibreDiscord"; + license = lib.licenses.gpl3Only; + }; + } diff --git a/modules/home/desktop/productivity.nix b/modules/home/desktop/productivity.nix index f18c5b5..cb240d4 100644 --- a/modules/home/desktop/productivity.nix +++ b/modules/home/desktop/productivity.nix @@ -48,12 +48,12 @@ in programs.zsh.sessionVariables.BROWSER = "librewolf"; services.flameshot = { - enable = false; + enable = true; settings = { General = { disabledTrayIcon = true; showStartupLaunchMessage = false; - startupLaunch = false; + startupLaunch = true; uiColor = color.base01; contrastUiColor = color.base00; diff --git a/modules/home/desktop/social.nix b/modules/home/desktop/social.nix index 31b9fd8..544059f 100644 --- a/modules/home/desktop/social.nix +++ b/modules/home/desktop/social.nix @@ -17,10 +17,19 @@ in { # Communication # kotatogram-desktop signal-desktop + abaddon vesktop-nogain + # (callPackage ./packages/librediscord.nix {}) + jami # Video freetube + + # Security + gcr ]; + + services.gnome-keyring.enable = true; + services.dunst.enable = true; }; } diff --git a/modules/home/sops/default.nix b/modules/home/sops/default.nix index 01b2b21..791fab8 100644 --- a/modules/home/sops/default.nix +++ b/modules/home/sops/default.nix @@ -17,5 +17,7 @@ in secrets.atuin-auth = {}; secrets.hr-password = {}; secrets.sops-key = {}; + secrets.google-db-test = {}; + secrets.google-db-prod = {}; }; } diff --git a/modules/home/sops/secrets.yaml b/modules/home/sops/secrets.yaml index 81d63e7..5ee893b 100644 --- a/modules/home/sops/secrets.yaml +++ b/modules/home/sops/secrets.yaml @@ -2,6 +2,8 @@ zipline-auth: ENC[AES256_GCM,data:RkJI6GuH7RzdcSlKn32gMGojjB6rkdDcnNUvsi/BTfJk2s atuin-auth: ENC[AES256_GCM,data:LDkiXWIwxor8Ro383gonJCyqu+nyxS7DrI2J8uo4Cqu2X61rBUlnpNR6YirUZS/lYAnWYJhZM7sR0G7ZNh9EgQ==,iv:UEs2KW8ImMnaQrSLrIGbVXEq86QiVPAPNIXBZpa3jFI=,tag:N0rhnPbasFzkoI3CJ9CV+Q==,type:str] hr-password: ENC[AES256_GCM,data:QZuzAnTJ2KgPnffHvdCWrJEM5d/FXxhX3dA1,iv:FgDw6aXDY0jCpJiYc9WOobR96TXNtnvN7neJu8drxMM=,tag:YT82wryVy3V+41w0YbMOrA==,type:str] sops-key: ENC[AES256_GCM,data:msX0EJqJauteOBICUsLcVgqNxqGcqvD+Xi/B2EhUX2OAoyBH5oDae8XWlQCi2RdOm4NtnrSTnG8FRQXfkXO+tne0VEfYTCjeVtU=,iv:qxpvofr56Ey17xcPpju/mQgiz+0cOYED5caAHs3myXw=,tag:oDFXh0rlc0tmV2IUJ1ezBQ==,type:str] +google-db-test: ENC[AES256_GCM,data:ZMm/BF/k+XnZZkHMDSV/fk3ds0LAOHAmag==,iv:tmfJ7ju5yAO6Oco3jXYNyqzJr7cgshyd/SkjfYnEl6U=,tag:jMD2N6TsgbRwefhJ/XYhtg==,type:str] +google-db-prod: ENC[AES256_GCM,data:fIPL9XKk9sAmpVsQBubSVbh3DlEEKadG9g==,iv:R34zkCIUDlk5/wg8eU8RZIanGayL+nX+7ZhyVmbcQC0=,tag:lu24b28742O46fjUaw2UBA==,type:str] sops: age: - recipient: age1m97a3eptxwpdd7h5kkqe9gkmhg6rquc64qjmlsfqfhfqv8q72crqrylhgc @@ -40,7 +42,7 @@ sops: a0V1N2VjUDE4Z3R5MGxMQVNmOVp0bVUK9cppJW33tKFOSvbIn/2Dga8k7/McaTpK m7M+83guMzNoOlpJ/WYU1BaePcM974AgjVR0WD/v+xGBvGKubKHqtw== -----END AGE ENCRYPTED FILE----- - lastmodified: "2025-08-04T07:58:56Z" - mac: ENC[AES256_GCM,data:aJw3KK4GMj5/Q06v1C5rdSerdO21cNxpTIJYoxmfhBKudzD7lSL6l+d47kWoB0U4J5jtbs9obWz2MH3CvyPBapjJaSFnYEXk1JuGihf8GK3QrqLAt+dmF2ZD1FBLpQELripueneyHkzT32180hpXGnppNlgOuATlIMSPosvlpVI=,iv:SpGAyTqqbpuxcLkMq7VnLQUoR6oW0ERgnyPaqVHpaN8=,tag:OSNGT8/5E+PRhoR8dIyaSA==,type:str] + lastmodified: "2026-01-21T14:37:21Z" + mac: ENC[AES256_GCM,data:bxr3U1Ig0qjuOcxHeOlOrXO0xtZs0vKTuXn8GE1dJGCFDjVgakbIwiW6+2WNYUbIpipCAwdecgb0jBngwt3zKGS4PMzapUXxl7RoCr5DWCh6kSD4CCUH4v8guuy0k8SMQXDO3CdbUd/5/asIPfxlvEESCQL54X2OJlt5xpE7PsU=,iv:m/lrHHFYXFKCVEOK462II8bcFvw7k4rKEuMOHHmzT/8=,tag:jgEQso2bAShLJERsOHhrKw==,type:str] unencrypted_suffix: _unencrypted - version: 3.10.2 + version: 3.11.0 diff --git a/modules/home/terminal/default.nix b/modules/home/terminal/default.nix index 3c3eebf..7beb495 100644 --- a/modules/home/terminal/default.nix +++ b/modules/home/terminal/default.nix @@ -6,12 +6,17 @@ imports = [ ./shell.nix ./emulator.nix + ./wezterm.nix + ./nushell.nix ./development.nix ./tools.nix ./yazi.nix + ./hr ./helix ./nvim ./zellij + ./opencode + ./gh.nix ]; config = lib.mkIf osConfig.mods.desktop.enable { diff --git a/modules/home/terminal/development.nix b/modules/home/terminal/development.nix index 89392a4..b0f55d6 100644 --- a/modules/home/terminal/development.nix +++ b/modules/home/terminal/development.nix @@ -23,9 +23,12 @@ in { enable = true; nix-direnv.enable = true; enableZshIntegration = lib.mkIf config.mods.terminal.zsh.enable true; + enableNushellIntegration = lib.mkIf config.mods.terminal.nushell.enable true; }; home.sessionVariables.EDITOR = "nvim"; - programs.zsh.sessionVariables.EDITOR = "nvim"; + programs.zsh.sessionVariables = lib.mkIf config.mods.terminal.zsh.enable { + EDITOR = "nvim"; + }; }; } diff --git a/modules/home/terminal/gh.nix b/modules/home/terminal/gh.nix new file mode 100644 index 0000000..f8b5649 --- /dev/null +++ b/modules/home/terminal/gh.nix @@ -0,0 +1,30 @@ +{ + pkgs, + lib, + config, + ... +}: { + options.mods.terminal.gh.enable = lib.mkEnableOption "enables gh and gh-dash"; + + config = lib.mkIf config.mods.terminal.gh.enable { + programs.gh = { + enable = true; + extensions = [pkgs.gh-dash]; + }; + + programs.gh-dash = { + enable = true; + settings = { + keybindings = { + prs = [ + { + key = "C"; + command = "nvim -c 'Octo pr edit {{.PrNumber}}'"; + description = "Open PR in Octo.nvim"; + } + ]; + }; + }; + }; + }; +} diff --git a/modules/home/terminal/helix/default.nix b/modules/home/terminal/helix/default.nix index 36f2b17..9781b41 100644 --- a/modules/home/terminal/helix/default.nix +++ b/modules/home/terminal/helix/default.nix @@ -62,7 +62,7 @@ { name = "nix"; auto-format = true; - formatter.command = "${pkgs.nixfmt-classic}/bin/nixfmt"; + formatter.command = "${pkgs.nixfmt}/bin/nixfmt"; } { name = "rust"; diff --git a/modules/home/terminal/hr/default.nix b/modules/home/terminal/hr/default.nix new file mode 100644 index 0000000..e8446f6 --- /dev/null +++ b/modules/home/terminal/hr/default.nix @@ -0,0 +1,65 @@ +{ + pkgs, + lib, + config, + ... +}: let + cfg = config.mods.terminal; + + test-port = "5436"; + prod-port = "5437"; +in { + options.mods.terminal.hr.enable = lib.mkEnableOption "Hefring (Work Tooling)"; + + config = lib.mkIf cfg.hr.enable { + programs.nushell = lib.mkIf cfg.nushell.enable { + extraConfig = '' + $env.PROJECT_ID = if ($env | get -o PROJECT_ID | is-empty) { "mk2-test" } else { $env.PROJECT_ID } + '' + builtins.readFile ./hr.nu; + }; + + programs.starship.settings.custom.project_id = { + command = "if $env.PROJECT_ID =~ 'prod' { $'(ansi yellow_bold) (ansi blue_bold)($env.PROJECT_ID)(ansi reset)' } else { $'(ansi blue_bold)($env.PROJECT_ID)(ansi reset)' }"; + when = "not ($env | get -o PROJECT_ID | is-empty)"; + shell = ["nu" "--no-config-file" "-c"]; + format = "on $output "; + }; + + programs.zsh.initContent = + '' + export MK2_TEST_SQL_INSTANCE_USER=gijs + export MK2_TEST_SQL_INSTANCE_PASSWORD="$(cat ${config.sops.secrets.google-db-test.path})" + export MK2_TEST_SQL_INSTANCE_PORT=${test-port} + export MK2_TEST_SQL_INSTANCE_HOST=localhost + export MK2_PROD_SQL_INSTANCE_USER=gijs + export MK2_PROD_SQL_INSTANCE_PASSWORD="$(cat ${config.sops.secrets.google-db-prod.path})" + export MK2_PROD_SQL_INSTANCE_HOST=localhost + export MK2_PROD_SQL_INSTANCE_PORT=${prod-port} + '' + + builtins.readFile ./hr.sh; + + systemd.user.services = let + proxy-service = name: port: { + "google-db-proxy-${name}" = { + Unit = { + Description = "Google Cloud SQL Proxy (${name})"; + After = ["network.target"]; + }; + Service = { + Type = "simple"; + Environment = [ + "GOOGLE_APPLICATION_CREDENTIALS=${config.home.homeDirectory}/.config/gcloud/application_default_credentials.json" + ]; + ExecStart = "${pkgs.google-cloud-sql-proxy}/bin/cloud-sql-proxy mk2-${name}:europe-west1:mk2-${name}-sql-instance -p ${port}"; + Restart = "always"; + }; + Install = { + WantedBy = ["default.target"]; + }; + }; + }; + in +proxy-service "test" test-port + // proxy-service "prod" prod-port; + }; +} diff --git a/modules/home/terminal/hr/hr.nu b/modules/home/terminal/hr/hr.nu new file mode 100644 index 0000000..e8ec346 --- /dev/null +++ b/modules/home/terminal/hr/hr.nu @@ -0,0 +1,474 @@ +# HR - Hefring work tooling for nushell +# +# This module provides commands for working with Hefring Cloud Run services and Cloud Functions. +# +# Key features: +# - Shell-style argument passing: -key value supports dot notation for nested structures +# Example: hr call service -items.0.type "trip" -items.0.id "123" +# +# - Nushell record syntax with --data flag for complex payloads: +# Example: hr call service --data {company_id: "abc", items: [{type: "trip"}]} +# +# - Automatic type detection: numbers, booleans, and JSON are parsed automatically +# Example: -count "42" → number, -active "true" → boolean +# +# - Pretty display: Payloads are shown as Nushell tables before sending + +def _hr_usage [] { + print "Usage: hr " + print "Commands:" + print " switch Switch PROJECT_ID between mk2-test and mk2-prod" + print " call Call a Cloud Run service route" + print " cf Call a Cloud Function" + print " init py Initialize a python devenv environment (git-ignored)" + print " init go Initialize a go devenv environment (git-ignored)" + print " init rs Initialize a rust devenv environment (git-ignored)" + print " init cpp Initialize a C++ devenv environment (git-ignored)" + print " freeze Freeze dependencies to requirements.txt" +} + +def _hr_init_devenv [] { + if (".gitignore" | path exists) { + cp .gitignore .gitignore.bak + } + + if (which devenv | is-empty) { + error make { msg: "Error: devenv not found in path." } + } + + devenv init + print "Direnv allowed" + + if (".gitignore.bak" | path exists) { + mv .gitignore.bak .gitignore + } else if (".gitignore" | path exists) { + rm .gitignore + } +} + +def _hr_add_ignores [files: list] { + let git_dir = (do { git rev-parse --git-dir } | complete) + if $git_dir.exit_code != 0 { + print "Warning: Not a git repository. Skipping git ignore setup." + return + } + + let exclude_file = (git rev-parse --git-path info/exclude) + mkdir (($exclude_file) | path dirname) + + for file in $files { + let already_exists = ( + if ($exclude_file | path exists) { + open $exclude_file | lines | any { |line| $line == $file } + } else { + false + } + ) + if not $already_exists { + $"($file)\n" | save --append $exclude_file + print $"Added ($file) to local git exclude \(($exclude_file)\)" + } + } +} + +def _hr_py_files [] { + " +{pkgs, ...}: { + packages = [ pkgs.google-cloud-sdk pkgs.libpq ]; + + languages.python = { + enable = true; + venv.enable = true; + uv = { + enable = true; + sync = { + enable = true; + allExtras = true; + }; + }; + }; + + # We use the named index \"google\" defined in uv.toml + env.UV_INDEX_GOOGLE_USERNAME = \"oauth2accesstoken\"; + env.PROJECT_ID = \"mk2-test\"; + + enterShell = '' + export PATH=\"$DEVENV_STATE/venv/bin:$PATH\" + if ! gcloud auth print-access-token >/dev/null 2>&1; then + echo \"⚠️ gcloud not authenticated. Run 'gcloud auth login' to access Google Artifact Registry.\" + else + export UV_INDEX_GOOGLE_PASSWORD=$(gcloud auth print-access-token) + fi + ''; +} +" | save -f devenv.nix + + " +[[index]] +name = \"google\" +url = \"https://europe-west1-python.pkg.dev/mk2-prod/python-packages/simple/\" +" | save -f uv.toml +} + +def _hr_rs_files [] { + " +{pkgs, ...}: { + languages.rust = { + enable = true; + channel = \"stable\"; + }; +} +" | save -f devenv.nix + + " +inputs: + rust-overlay: + url: github:oxalica/rust-overlay + inputs: + nixpkgs: + follows: nixpkgs +" | save -f devenv.yaml +} + +def _hr_cpp_files [] { + " +{ pkgs, ... }: +let + # Use glibc-compatible static openssl to match system libs + staticOpenSSL = pkgs.openssl.override { static = true; }; + + # Shim to satisfy CMake looking for \"ssl.a\" + compatOpenSSL = pkgs.runCommand \"openssl-compat\" {} '' + mkdir -p $out/lib + ln -s ${staticOpenSSL.out}/lib/libssl.a $out/lib/ssl.a + ln -s ${staticOpenSSL.out}/lib/libcrypto.a $out/lib/crypto.a + ''; +in { + packages = [ + pkgs.cmake + pkgs.clang-tools + pkgs.pkg-config + pkgs.mosquitto + + staticOpenSSL + compatOpenSSL + ]; + # Explicitly add lib paths so linker finds -lssl AND ssl.a + env.LIBRARY_PATH = \"${staticOpenSSL.out}/lib:${compatOpenSSL}/lib\"; + env.CPATH = \"${staticOpenSSL.dev}/include\"; + + languages.cplusplus.enable = true; +} +" | save -f devenv.nix +} + +def _hr_go_files [] { + " +{pkgs, ...}: { + languages.go = { + enable = true; + }; +} +" | save -f devenv.nix +} + +def _hr_init_base [name: string, write_files: closure, ignores: list] { + print $"Initializing ($name) devenv..." + + # 1. Init devenv + _hr_init_devenv + + # 2. Write language-specific files + do $write_files + + # 3. Add to local git exclude + let base_ignores = [ + ".devenv*" + ".direnv" + "devenv.nix" + "devenv.yaml" + "devenv.lock" + ".envrc" + ] + _hr_add_ignores ($base_ignores ++ $ignores) + + direnv allow +} + +def _hr_setpath [data: any, path: list, value: any] { + # Recursively build nested structure, similar to jq's setpath + if ($path | is-empty) { + $value + } else if ($path | length) == 1 { + $data | upsert ($path | first) $value + } else { + let key = ($path | first) + let rest = ($path | skip 1) + let next_key = ($rest | first) + + # Check if key exists in data and get intermediate value + let key_type = ($key | describe) + let data_type = ($data | describe) + let is_list = ($data_type | str starts-with "list") or ($data_type | str starts-with "table") + + # Determine if key exists and get intermediate structure + let intermediate = if $key_type == "int" and $is_list { + # Array index + if $key < ($data | length) { + $data | get $key + } else { + # Index doesn't exist, create structure based on next key + if ($next_key | describe) == "int" { [] } else { {} } + } + } else if $key_type != "int" { + # Object key + if $key in $data { + $data | get $key + } else { + # Key doesn't exist, create structure based on next key + if ($next_key | describe) == "int" { [] } else { {} } + } + } else { + # Data is not a list but key is int - create array + if ($next_key | describe) == "int" { [] } else { {} } + } + + # Recursively set the rest of the path + let updated_intermediate = (_hr_setpath $intermediate $rest $value) + + # Update data with the new intermediate value + $data | upsert $key $updated_intermediate + } +} + +def _hr_add_json_field [json: string, key: string, value: string] { + # Determine if value should be parsed as raw JSON or treated as a string + let is_bool = ($value == "true" or $value == "false") + let is_number = ($value =~ '^-?(0|[1-9][0-9]*)(\.[0-9]+)?$') + let is_json_container = ($value =~ '^\[' or $value =~ '^\{') + + # Parse the value into the appropriate type + let parsed_value = if $is_bool or $is_number { + $value | from json + } else if $is_json_container { + let parsed = (do { $value | from json } | complete) + if $parsed.exit_code == 0 { + $value | from json + } else { + print $"Warning: Value for '($key)' looks like JSON but is invalid. Treating as string." --stderr + $value + } + } else { + $value + } + + # Convert key path (e.g., "items.0.type") to a list of path segments + # Numbers are converted to integers for array indexing + let path_segments = ($key | split row "." | each { |segment| + if ($segment =~ '^[0-9]+$') { + $segment | into int + } else { + $segment + } + }) + + let data = ($json | from json) + _hr_setpath $data $path_segments $parsed_value | to json +} + +def _hr_parse_flags [args: list] { + mut json_str = "{}" + mut i = 0 + while $i < ($args | length) { + let arg = ($args | get $i) + if ($arg | str starts-with "-") { + let key = ($arg | str replace --regex '^-+' '') + let next_i = $i + 1 + if $next_i >= ($args | length) { + error make { msg: $"Error: Missing value for option ($key)" } + } + let next = ($args | get $next_i) + if ($next | str starts-with "-") { + error make { msg: $"Error: Missing value for option ($key)" } + } + $json_str = (_hr_add_json_field $json_str $key $next) + $i = $i + 2 + } else { + error make { msg: $"Error: Unexpected argument '($arg)'" } + } + } + $json_str | from json +} + +def _hr_call [args: list] { + if ($args | is-empty) { + print "Usage: hr call [/path] [OPTIONS]" + print "" + print "Options:" + print " -key value Set JSON field (supports dot notation, e.g., -items.0.type \"trip\")" + print " --data Pass structured data as a Nushell record" + return + } + + let route_arg = ($args | first) + let rest = ($args | skip 1) + + let parts = if ($route_arg | str contains "/") { + let service = ($route_arg | split row "/" | first) + let path = "/" + ($route_arg | split row "/" | skip 1 | str join "/") + { service: $service, path: $path } + } else { + { service: $route_arg, path: "" } + } + + let project_number = if $env.PROJECT_ID == "mk2-prod" { + "1013087376822" + } else { + "322048751601" + } + + # Check if --data flag is present + let data_idx = ($rest | enumerate | where item == "--data" | get index.0? | default (-1)) + + let payload_record = if $data_idx >= 0 { + # Use --data record + if ($data_idx + 1) >= ($rest | length) { + error make { msg: "Error: --data requires a record argument" } + } + let data_str = ($rest | get ($data_idx + 1)) + # Try to parse as Nushell code to get a record + try { + nu -c $data_str + } catch { + error make { msg: $"Error: --data argument must be a valid Nushell record, got: ($data_str)" } + } + } else { + # Use -key value pairs + _hr_parse_flags $rest + } + + let payload = ($payload_record | to json -r) + let url = $"https://($parts.service)-($project_number).europe-west1.run.app($parts.path)" + + print $"Calling ($url)..." + print ($payload | from json) + + let token = (gcloud auth print-identity-token) + ^curl -s -S -L $url -H $"Authorization: Bearer ($token)" -H "Content-Type: application/json" -d $payload +} + +def _hr_cf [args: list] { + if ($args | is-empty) { + print "Usage: hr cf [OPTIONS]" + print "" + print "Options:" + print " -key value Set JSON field (supports dot notation, e.g., -items.0.type \"trip\")" + print " --data Pass structured data as a Nushell record" + return + } + + let function_name = ($args | first) + let rest = ($args | skip 1) + + # Check if --data flag is present + let data_idx = ($rest | enumerate | where item == "--data" | get index.0? | default (-1)) + + let payload_record = if $data_idx >= 0 { + # Use --data record + if ($data_idx + 1) >= ($rest | length) { + error make { msg: "Error: --data requires a record argument" } + } + let data_str = ($rest | get ($data_idx + 1)) + # Try to parse as Nushell code to get a record + try { + nu -c $data_str + } catch { + error make { msg: $"Error: --data argument must be a valid Nushell record, got: ($data_str)" } + } + } else { + # Use -key value pairs + _hr_parse_flags $rest + } + + let payload = ($payload_record | to json -r) + let url = $"https://europe-west1-($env.PROJECT_ID).cloudfunctions.net/($function_name)" + + print $"Calling ($url)..." + print ($payload | from json) + + let token = (gcloud auth print-identity-token) + ^curl -s -S -L $url -H $"Authorization: Bearer ($token)" -H "Content-Type: application/json" -d $payload +} + +# HR - Hefring work tooling +export def --env --wrapped hr [...args: string] { + if ($args | is-empty) { + _hr_usage + return + } + + let cmd = ($args | first) + let rest = ($args | skip 1) + + match $cmd { + "switch" => { + if ($rest | is-empty) { + if $env.PROJECT_ID == "mk2-test" { + $env.PROJECT_ID = "mk2-prod" + print "Switched PROJECT_ID to mk2-prod" + } else { + $env.PROJECT_ID = "mk2-test" + print "Switched PROJECT_ID to mk2-test" + } + } else { + match ($rest | first) { + "test" => { + $env.PROJECT_ID = "mk2-test" + print "Set PROJECT_ID to mk2-test" + } + "prod" => { + $env.PROJECT_ID = "mk2-prod" + print "Set PROJECT_ID to mk2-prod" + } + _ => { + print "Usage: hr switch [test|prod]" + } + } + } + } + "init" => { + if ($rest | is-empty) { + _hr_usage + return + } + match ($rest | first) { + "py" => { _hr_init_base "Python" { _hr_py_files } ["uv.lock" "uv.toml"] } + "rs" => { _hr_init_base "Rust" { _hr_rs_files } [] } + "go" => { _hr_init_base "Go" { _hr_go_files } [] } + "cpp" => { + _hr_init_base "C++" { _hr_cpp_files } [] + mkdir build + cd build + cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_BUILD_TYPE=Release .. + make -j (sys cpu | length) + cp compile_commands.json .. + } + _ => { _hr_usage } + } + } + "freeze" => { + let extra_index_url = "https://europe-west1-python.pkg.dev/mk2-prod/python-packages/simple/" + uv pip install keyrings.google-artifactregistry-auth==1.1.2 keyring + uv pip install --no-cache -e ".[test]" --extra-index-url $extra_index_url --keyring-provider subprocess + $"--extra-index-url ($extra_index_url)\n" | save -f requirements.txt + uv pip freeze --exclude-editable | save --append requirements.txt + } + "call" => { + _hr_call $rest + } + "cf" => { + _hr_cf $rest + } + _ => { _hr_usage } + } +} diff --git a/modules/home/terminal/hr/hr.sh b/modules/home/terminal/hr/hr.sh new file mode 100644 index 0000000..83ab944 --- /dev/null +++ b/modules/home/terminal/hr/hr.sh @@ -0,0 +1,414 @@ +# Set default PROJECT_ID if not already set +if [[ -z "$PROJECT_ID" ]]; then + export PROJECT_ID="mk2-test" +fi + +_hr_usage() { + echo "Usage: hr " + echo "Commands:" + echo " switch Switch PROJECT_ID between mk2-test and mk2-prod" + echo " call Call a Cloud Run service route" + echo " cf Call a Cloud Function" + echo " init py Initialize a python devenv environment (git-ignored)" + echo " init go Initialize a go devenv environment (git-ignored)" + echo " freeze Freeze dependencies to requirements.txt" +} + +_hr_init_devenv() { + if [ -f .gitignore ]; then + cp .gitignore .gitignore.bak + fi + + if command -v devenv >/dev/null; then + devenv init + echo "Direnv allowed" + else + echo "Error: devenv not found in path." + return 1 + fi + + if [ -f .gitignore.bak ]; then + mv .gitignore.bak .gitignore + elif [ -f .gitignore ]; then + rm .gitignore + fi +} + +_hr_add_ignores() { + if git rev-parse --git-dir >/dev/null 2>&1; then + EXCLUDE_FILE=$(git rev-parse --git-path info/exclude) + mkdir -p "$(dirname "$EXCLUDE_FILE")" + + for file in "$@"; do + if ! grep -Fxq "$file" "$EXCLUDE_FILE" 2>/dev/null; then + echo "$file" >>"$EXCLUDE_FILE" + echo "Added $file to local git exclude ($EXCLUDE_FILE)" + fi + done + else + echo "Warning: Not a git repository. Skipping git ignore setup." + fi +} + +_hr_py_files() { + cat <devenv.nix +{pkgs, ...}: { + packages = [ pkgs.google-cloud-sdk pkgs.libpq ]; + + languages.python = { + enable = true; + venv.enable = true; + uv = { + enable = true; + sync = { + enable = true; + allExtras = true; + }; + }; + }; + + # We use the named index "google" defined in uv.toml + env.UV_INDEX_GOOGLE_USERNAME = "oauth2accesstoken"; + env.PROJECT_ID = "mk2-test"; + + enterShell = '' + export PATH="\$DEVENV_STATE/venv/bin:\$PATH" + if ! gcloud auth print-access-token >/dev/null 2>&1; then + echo "⚠️ gcloud not authenticated. Run 'gcloud auth login' to access Google Artifact Registry." + else + export UV_INDEX_GOOGLE_PASSWORD=\$(gcloud auth print-access-token) + fi + ''; +} +EOF + + cat <uv.toml +[[index]] +name = "google" +url = "https://europe-west1-python.pkg.dev/mk2-prod/python-packages/simple/" +EOF +} + +_hr_rs_files() { + cat <devenv.nix +{pkgs, ...}: { + languages.rust = { + enable = true; + channel = "stable"; + }; +} +EOF + + cat <devenv.yaml +inputs: + rust-overlay: + url: github:oxalica/rust-overlay + inputs: + nixpkgs: + follows: nixpkgs +EOF +} + +_hr_cpp_files() { + cat <devenv.nix +{ pkgs, ... }: +let + # Use glibc-compatible static openssl to match system libs + staticOpenSSL = pkgs.openssl.override { static = true; }; + + # Shim to satisfy CMake looking for "ssl.a" + compatOpenSSL = pkgs.runCommand "openssl-compat" {} '' + mkdir -p \$out/lib + ln -s \${staticOpenSSL.out}/lib/libssl.a \$out/lib/ssl.a + ln -s \${staticOpenSSL.out}/lib/libcrypto.a \$out/lib/crypto.a + ''; +in { + packages = [ + pkgs.cmake + pkgs.clang-tools + pkgs.pkg-config + pkgs.mosquitto + + staticOpenSSL + compatOpenSSL + ]; + # Explicitly add lib paths so linker finds -lssl AND ssl.a + env.LIBRARY_PATH = "\${staticOpenSSL.out}/lib:\${compatOpenSSL}/lib"; + env.CPATH = "\${staticOpenSSL.dev}/include"; + + languages.cplusplus.enable = true; +} +EOF +} + +_hr_go_files() { + cat <devenv.nix +{pkgs, ...}: { + languages.go = { + enable = true; + }; +} +EOF +} + +_hr_init_base() { + local name="$1" + local func="$2" + shift 2 + local ignores=("$@") + echo "Initializing $name devenv..." + + # 1. Init devenv + _hr_init_devenv + + # 2. Replace devenv.nix + "$func" + + # 3. Add to local git exclude + IGNORES=( + ".devenv*" + ".direnv" + "devenv.nix" + "devenv.yaml" + "devenv.lock" + ".envrc" + "${ignores[@]}" + ) + _hr_add_ignores "${IGNORES[@]}" + + direnv allow +} + +_hr_init_py() { + IGNORES=( + "uv.lock" + "uv.toml" + ) + _hr_init_base "Python" _hr_py_files "${IGNORES[@]}" +} + +_hr_init_rs() { + _hr_init_base "Rust" _hr_rs_files +} + +_hr_init_cpp() { + _hr_init_base "C++" _hr_cpp_files + mkdir -p build && + cd build && + cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_BUILD_TYPE=Release .. && + make -j$(nproc) && + cp compile_commands.json .. +} + +_hr_init_go() { + _hr_init_base "Go" _hr_go_files +} + +_hr_freeze() { + local extra_index_url="https://europe-west1-python.pkg.dev/mk2-prod/python-packages/simple/" + + # Install the auth plugin and keyring CLI + uv pip install keyrings.google-artifactregistry-auth==1.1.2 keyring + + # Install project dependencies using the subprocess keyring provider + uv pip install --no-cache -e ".[test]" --extra-index-url "${extra_index_url}" --keyring-provider subprocess + + # Generate requirements.txt + echo "--extra-index-url ${extra_index_url}" >requirements.txt + uv pip freeze --exclude-editable >>requirements.txt +} + +_hr_add_json_field() { + local json="$1" + local key="$2" + local value="$3" + local jq_opt="--arg" + + # Check if explicit boolean + if [[ "$value" == "true" || "$value" == "false" ]]; then + jq_opt="--argjson" + # Check if number (integer or float, no leading zeros unless just 0) + elif [[ "$value" =~ ^-?(0|[1-9][0-9]*)(\.[0-9]+)?$ ]]; then + jq_opt="--argjson" + # Check if object or array + elif [[ "$value" == "["* || "$value" == "{"* ]]; then + if echo "$value" | jq empty >/dev/null 2>&1; then + jq_opt="--argjson" + else + # Warn to stderr, but proceed as string + echo "Warning: Value for '$key' looks like JSON but is invalid. Treating as string." >&2 + fi + fi + + # Apply the value at the path defined by the key (dot-notation supported) + # paths like items.0.id are converted to ["items", 0, "id"] + echo "$json" | jq --arg k "$key" $jq_opt v "$value" \ + 'setpath($k | split(".") | map(if test("^[0-9]+$") then tonumber else . end); $v)' +} + +_hr_call() { + local route_arg="$1" + shift + + if [[ -z "$route_arg" ]]; then + echo "Usage: hr call [/path] " + return 1 + fi + + local service_name + local url_path + + if [[ "$route_arg" == */* ]]; then + service_name="${route_arg%%/*}" + url_path="/${route_arg#*/}" + else + service_name="$route_arg" + url_path="" + fi + + local project_number + if [[ "$PROJECT_ID" == "mk2-prod" ]]; then + project_number="1013087376822" + else + project_number="322048751601" + fi + + if ! command -v jq >/dev/null; then + echo "Error: jq is required but not installed." + return 1 + fi + + local json_payload="{}" + + while [[ $# -gt 0 ]]; do + if [[ "$1" == -* ]]; then + local key="${1#-}" + if [[ -z "$2" || "$2" == -* ]]; then + echo "Error: Missing value for option $key" + return 1 + fi + local value="$2" + + json_payload=$(_hr_add_json_field "$json_payload" "$key" "$value") + shift 2 + else + echo "Error: Unexpected argument '$1'" + return 1 + fi + done + + local url="https://${service_name}-${project_number}.europe-west1.run.app${url_path}" + + echo "Calling $url..." + echo "$json_payload" + + curl "$url" \ + -H "Authorization: bearer $(gcloud auth print-identity-token)" \ + -H "Content-Type: application/json" \ + -d "$json_payload" +} + +_hr_cf() { + local function_name="$1" + shift + + if [[ -z "$function_name" ]]; then + echo "Usage: hr cf " + return 1 + fi + + if ! command -v jq >/dev/null; then + echo "Error: jq is required but not installed." + return 1 + fi + + local json_payload="{}" + + while [[ $# -gt 0 ]]; do + if [[ "$1" == -* ]]; then + local key="${1#-}" + if [[ -z "$2" || "$2" == -* ]]; then + echo "Error: Missing value for option $key" + return 1 + fi + local value="$2" + + json_payload=$(_hr_add_json_field "$json_payload" "$key" "$value") + shift 2 + else + echo "Error: Unexpected argument '$1'" + return 1 + fi + done + + local url="https://europe-west1-${PROJECT_ID}.cloudfunctions.net/${function_name}" + + echo "Calling $url..." + echo "$json_payload" + + curl "$url" \ + -H "Authorization: bearer $(gcloud auth print-identity-token)" \ + -H "Content-Type: application/json" \ + -d "$json_payload" +} + +hr() { + if [[ $# -eq 0 ]]; then + _hr_usage + return 1 + fi + + local command="$1" + shift + + if [[ "$command" == "switch" ]]; then + if [[ -z "$1" ]]; then + # Toggle between test and prod + if [[ "$PROJECT_ID" == "mk2-test" ]]; then + export PROJECT_ID="mk2-prod" + echo "Switched PROJECT_ID to mk2-prod" + else + export PROJECT_ID="mk2-test" + echo "Switched PROJECT_ID to mk2-test" + fi + elif [[ "$1" == "test" ]]; then + export PROJECT_ID="mk2-test" + echo "Set PROJECT_ID to mk2-test" + elif [[ "$1" == "prod" ]]; then + export PROJECT_ID="mk2-prod" + echo "Set PROJECT_ID to mk2-prod" + else + echo "Usage: hr switch [test|prod]" + return 1 + fi + return 0 + fi + + # Run original logic in a subshell to preserve set -e behavior without affecting current shell + ( + set -e + # Restore arguments for processing + set -- "$command" "$@" + + if [ "$1" = "init" ] && [ "$2" = "py" ]; then + _hr_init_py + elif [ "$1" = "init" ] && [ "$2" = "rs" ]; then + _hr_init_rs + elif [ "$1" = "init" ] && [ "$2" = "go" ]; then + _hr_init_go + elif [ "$1" = "init" ] && [ "$2" = "cpp" ]; then + _hr_init_cpp + elif [ "$1" = "freeze" ]; then + _hr_freeze + elif [ "$1" = "call" ]; then + shift + _hr_call "$@" + elif [ "$1" = "cf" ]; then + shift + _hr_cf "$@" + else + _hr_usage + exit 1 + fi + ) +} diff --git a/modules/home/terminal/nushell.nix b/modules/home/terminal/nushell.nix new file mode 100644 index 0000000..dbdab6d --- /dev/null +++ b/modules/home/terminal/nushell.nix @@ -0,0 +1,79 @@ +{ + pkgs, + lib, + config, + ... +}: let + cfg = config.mods.terminal; + + aliases = { + la = "ls -la"; + ".." = "cd .."; + "..." = "cd ../.."; + "...." = "cd ../../.."; + "....." = "cd ../../../.."; + "......" = "cd ../../../../.."; + }; +in { + options.mods.terminal.nushell.enable = lib.mkEnableOption "enables nushell"; + + config = lib.mkIf cfg.nushell.enable { + programs.nushell = { + enable = true; + + shellAliases = aliases; + + # vi mode + sensible defaults via flat assignments (avoids clobbering other modules) + settings = { + show_banner = false; + edit_mode = "vi"; + cursor_shape = { + vi_insert = "line"; + vi_normal = "block"; + }; + history = { + max_size = 2097152; + sync_on_enter = true; + file_format = "sqlite"; + isolation = false; + }; + completions = { + case_sensitive = false; + quick = true; + partial = true; + algorithm = "fuzzy"; + }; + table.mode = "rounded"; + }; + + # Append the / keybinding after all integrations (including atuin) are sourced, + # so _atuin_search_cmd is defined when this runs. + extraConfig = lib.mkAfter '' + $env.config = ( + $env.config | upsert keybindings ( + $env.config.keybindings | append { + name: atuin_search_vi_normal + modifier: none + keycode: char_/ + mode: vi_normal + event: { send: executehostcommand cmd: (_atuin_search_cmd) } + } + ) + ) + ''; + + # Carry over zsh session variables + extraEnv = lib.optionalString config.mods.terminal.development.enable '' + $env.EDITOR = "nvim" + ''; + }; + + # Starship prompt (same as zsh) + programs.starship.enable = true; + + # direnv nushell integration + programs.direnv = lib.mkIf config.mods.terminal.development.enable { + enableNushellIntegration = true; + }; + }; +} diff --git a/modules/home/terminal/nvim/default.nix b/modules/home/terminal/nvim/default.nix index 9ea251e..469f656 100644 --- a/modules/home/terminal/nvim/default.nix +++ b/modules/home/terminal/nvim/default.nix @@ -23,6 +23,7 @@ in { imports = [ inputs.nvf.homeManagerModules.default ./obsidian.nix + ./octo.nix ]; config = lib.mkIf config.mods.nvim.enable { @@ -60,6 +61,13 @@ in { action = "lua vim.lsp.buf.definition()"; desc = "Go to Definition"; } + { + key = ""; + mode = ["n"]; + action = "FzfLua combine pickers=keymaps;commands"; + silent = true; + desc = "FzfLua keymaps and commands"; + } ]; languages = { @@ -93,6 +101,7 @@ in { statusline.lualine.enable = true; telescope.enable = true; + fzf-lua.enable = true; autocomplete.nvim-cmp.enable = true; autopairs.nvim-autopairs.enable = true; tabline.nvimBufferline.enable = true; @@ -103,6 +112,7 @@ in { clipboard = { enable = true; providers.xclip.enable = true; + registers = "unnamed,unnamedplus"; }; mini = { diff --git a/modules/home/terminal/nvim/octo.nix b/modules/home/terminal/nvim/octo.nix new file mode 100644 index 0000000..98db5ba --- /dev/null +++ b/modules/home/terminal/nvim/octo.nix @@ -0,0 +1,54 @@ +{ + pkgs, + lib, + config, + ... +}: { + options.mods.octo.enable = + lib.mkEnableOption "enables octo"; + + config = lib.mkIf config.mods.octo.enable { + programs.nvf.settings.vim.lazy.plugins."octo.nvim" = { + package = pkgs.vimPlugins.octo-nvim; + setupModule = "octo"; + setupOpts = { + picker = "telescope"; + enable_builtin = true; + }; + cmd = ["Octo"]; + keys = [ + { + key = "oi"; + mode = "n"; + action = "Octo issue list"; + desc = "List GitHub Issues"; + } + { + key = "op"; + mode = "n"; + action = "Octo pr list"; + desc = "List GitHub PullRequests"; + } + { + key = "od"; + mode = "n"; + action = "Octo discussion list"; + desc = "List GitHub Discussions"; + } + { + key = "on"; + mode = "n"; + action = "Octo notification list"; + desc = "List GitHub Notifications"; + } + { + key = "os"; + mode = "n"; + action = "function() require('octo.utils').create_base_search_command { include_current_repo = true } end"; + lua = true; + desc = "Search GitHub"; + } + ]; + }; + }; +} diff --git a/modules/home/terminal/opencode/dcg.nix b/modules/home/terminal/opencode/dcg.nix new file mode 100644 index 0000000..d3df397 --- /dev/null +++ b/modules/home/terminal/opencode/dcg.nix @@ -0,0 +1,38 @@ +{ + lib, + fetchFromGitHub, + rustPlatform, + openssl, + pkg-config, + git, +}: +rustPlatform.buildRustPackage (finalAttrs: { + pname = "dcg"; + version = "v0.4.0"; + + src = fetchFromGitHub { + owner = "Dicklesworthstone"; + repo = "destructive_command_guard"; + tag = finalAttrs.version; + hash = "sha256-tkjHhSMoLRV56AwUa0DkoDMoEj6gUZx/ih0VTC9C+4o="; + }; + + cargoHash = "sha256-G6cOjl5tLdjBg7A+Itnk/t6tLzoU7gKYOTYlZm3HSlA="; + + doCheck = false; + + nativeBuildInputs = [ + pkg-config + openssl + openssl.dev + git + ]; + PKG_CONFIG_PATH = "${openssl.dev}/lib/pkgconfig"; + + meta = { + description = "A high-performance hook for AI coding agents that blocks destructive commands before they execute, protecting your work from accidental deletion"; + homepage = "https://github.com/Dicklesworthstone/destructive_command_guard"; + license = lib.licenses.mit; + maintainers = []; + }; +}) diff --git a/modules/home/terminal/opencode/default.nix b/modules/home/terminal/opencode/default.nix new file mode 100644 index 0000000..4448cfc --- /dev/null +++ b/modules/home/terminal/opencode/default.nix @@ -0,0 +1,38 @@ +{ + lib, + config, + pkgs, + ... +}: { + options.mods.opencode.enable = lib.mkEnableOption "enables opencode"; + + # imports = [ + # ./package.nix + # ./dcg.nix + # ]; + + config = lib.mkIf config.mods.opencode.enable { + home.packages = with pkgs; [ + (callPackage ./dcg.nix {}) + ]; + + programs.opencode = { + enable = true; + settings = { + "plugin" = ["opencode-gemini-auth"]; + + "permission" = { + "bash" = { + "*" = "ask"; + "rm *" = "deny"; + "rmdir *" = "deny"; + "unlink *" = "deny"; + "*rm *" = "ask"; + "*rmdir *" = "ask"; + "*unlink *" = "ask"; + }; + }; + }; + }; + }; +} diff --git a/modules/home/terminal/shell.nix b/modules/home/terminal/shell.nix index b8ad4a0..bbb298d 100644 --- a/modules/home/terminal/shell.nix +++ b/modules/home/terminal/shell.nix @@ -23,6 +23,7 @@ in { programs.zsh = lib.mkIf cfg.zsh.enable { enable = true; + dotDir = "${config.xdg.configHome}/zsh"; enableCompletion = true; autosuggestion.enable = true; syntaxHighlighting.enable = true; diff --git a/modules/home/terminal/tools.nix b/modules/home/terminal/tools.nix index b3c00dd..88b4745 100644 --- a/modules/home/terminal/tools.nix +++ b/modules/home/terminal/tools.nix @@ -42,6 +42,7 @@ in atuin = { enable = true; enableZshIntegration = true; + enableNushellIntegration = true; flags = ["--disable-up-arrow"]; settings = { sync_frequency = "5m"; diff --git a/modules/home/terminal/wezterm.nix b/modules/home/terminal/wezterm.nix new file mode 100644 index 0000000..967b2a8 --- /dev/null +++ b/modules/home/terminal/wezterm.nix @@ -0,0 +1,599 @@ +{ + pkgs, + lib, + config, + ... +}: let + cfg = config.mods.terminal; + color = config.lib.stylix.colors.withHashtag; + + # Shell to use inside wezterm + shell = + if cfg.nushell.enable + then lib.getExe pkgs.nushell + else if cfg.zsh.enable + then lib.getExe pkgs.zsh + else null; + + # ── Lua codegen helpers ────────────────────────────────────────────────────── + + luaKey = { + key, + mods ? null, + action, + indent ? " ", + }: let + modsStr = lib.optionalString (mods != null) ", mods = \"${mods}\""; + in "${indent}{ key = \"${key}\"${modsStr}, action = ${action} },\n"; + + # vi directions: key, arrow alias, wezterm direction name + viDirs = [ + { + key = "h"; + arrow = "LeftArrow"; + dir = "Left"; + } + { + key = "j"; + arrow = "DownArrow"; + dir = "Down"; + } + { + key = "k"; + arrow = "UpArrow"; + dir = "Up"; + } + { + key = "l"; + arrow = "RightArrow"; + dir = "Right"; + } + ]; + + # hjkl + arrow equivalents; mkAction :: dir -> lua-action-string + viDirKeys = { + mods ? null, + indent ? " ", + mkAction, + }: + lib.concatMapStrings (d: + luaKey { + inherit mods indent; + key = d.key; + action = mkAction d.dir; + } + + luaKey { + inherit mods indent; + key = d.arrow; + action = mkAction d.dir; + }) + viDirs; + + # 1-9 tab-jump bindings + tabJumpKeys = { + mods ? null, + indent ? " ", + }: + lib.concatStrings (builtins.genList (i: + luaKey { + inherit mods indent; + key = toString (i + 1); + action = "act.ActivateTab(${toString i})"; + }) + 9); + + # hjkl + arrows for AdjustPaneSize at a given step + resizeDirKeys = { + step, + indent ? " ", + }: + lib.concatMapStrings (d: + luaKey { + inherit indent; + key = d.key; + action = "act.AdjustPaneSize({ \"${d.dir}\", ${toString step} })"; + } + + luaKey { + inherit indent; + key = d.arrow; + action = "act.AdjustPaneSize({ \"${d.dir}\", ${toString step} })"; + }) + viDirs; + + # Uppercase HJKL fine-tune resize + resizeShiftKeys = { + step, + indent ? " ", + }: + lib.concatMapStrings (d: + luaKey { + inherit indent; + key = lib.toUpper d.key; + action = "act.AdjustPaneSize({ \"${d.dir}\", ${toString step} })"; + }) + viDirs; + + # Wrap a lua action string so it also pops the key table (return to locked) + andPop = action: "act.Multiple({ ${action}, act.PopKeyTable })"; + + # Standard exit bindings, given the mode's own pop key + exitKeys = { + selfKey, + indent ? " ", + }: + lib.concatStrings [ + (luaKey { + inherit indent; + key = selfKey; + action = "act.PopKeyTable"; + }) + (luaKey { + inherit indent; + key = "Escape"; + action = "act.PopKeyTable"; + }) + (luaKey { + inherit indent; + key = "Enter"; + action = "act.PopKeyTable"; + }) + (luaKey { + inherit indent; + key = "Space"; + mods = "ALT"; + action = "act.PopKeyTable"; + }) + ]; + + # ── Status bar ─────────────────────────────────────────────────────────────── + + # Per-mode accent colour (base16) and hint string shown in the status bar + # Hints use nerd font glyphs: separators, icons, key labels + modes = { + locked = { + fg = color.base03; + hint = "⌥󱁐:󰌌 ⌥hl:󰹳 ^ud:󰹹 ⌥1-9:󰓩 ⌥n:󰤻 ⌥s:󱇳 ⌥w:󰅙 ⌥t:󰓩 ⌥p:󱃔"; + }; + normal = { + fg = color.base0D; + hint = "t:󰐕 x:󰅙 r:󰩨 c:󰆏 s:󰍉 esc:󱊷"; + }; + resize = { + fg = color.base0A; + hint = "hjkl:󰁌 HJKL:󰁎 esc:󱊷"; + }; + copy_mode = { + fg = color.base0E; + hint = "hjkl:󰹳 v:󰒆 V:󰒇 ^v:󰒈 y:󰆏 /:󰍉 n/p:󰁹 esc:󱊷"; + }; + search_mode = { + fg = color.base08; + hint = "type:󰈙 ^n/^p:󰁹 ^r:󰑓 ↵/esc:󰆐"; + }; + }; + + # Lua table literal mapping mode name -> { fg, hint } + modeTableEntries = lib.concatStringsSep "\n " (lib.mapAttrsToList (name: m: '' + ["${name}"] = { fg = "${m.fg}", hint = "${m.hint}" }, + '') + modes); +in { + options.mods.terminal.wezterm.enable = lib.mkEnableOption "enables wezterm"; + + config = lib.mkIf cfg.wezterm.enable { + programs.wezterm = { + enable = true; + + extraConfig = '' + local wezterm = require("wezterm") + local act = wezterm.action + + -- ─── Helpers ─────────────────────────────────────────────────────────────── + + -- move_focus_or_tab: mirrors Zellij's MoveFocusOrTab. + -- Move focus in direction; if at the edge, switch to the adjacent tab. + local function move_focus_or_tab(direction) + return wezterm.action_callback(function(window, pane) + local neighbour = pane:tab():get_pane_direction(direction) + if neighbour ~= nil then + window:perform_action(act.ActivatePaneDirection(direction), pane) + else + if direction == "Left" then + window:perform_action(act.ActivateTabRelative(-1), pane) + elseif direction == "Right" then + window:perform_action(act.ActivateTabRelative(1), pane) + end + end + end) + end + + -- scroll_or_passthrough: Ctrl+key scrolls at the shell prompt but passes + -- the key through when a program has taken the alt screen (vim, less, fzf…). + local function scroll_or_passthrough(key, pages) + return wezterm.action_callback(function(window, pane) + if pane:is_alt_screen_active() then + window:perform_action(act.SendKey({ key = key, mods = "CTRL" }), pane) + else + window:perform_action(act.ScrollByPage(pages), pane) + end + end) + end + + -- tab_is_zoomed: true if any pane in the current tab is zoomed. + -- Used by move_vertical to decide whether to carry zoom forward. + local function tab_is_zoomed(pane) + for _, info in ipairs(pane:tab():panes_with_info()) do + if info.is_zoomed then return true end + end + return false + end + + -- move_vertical: move focus Up/Down. + -- If any pane in the tab is currently zoomed (zoom-mode is "on" for this + -- tab), zoom the destination pane too — stacked-panel behaviour. + -- Otherwise just move focus plainly. + local function move_vertical(direction) + return wezterm.action_callback(function(window, pane) + local neighbour = pane:tab():get_pane_direction(direction) + if neighbour == nil then return end + if tab_is_zoomed(pane) then + -- unzoom_on_switch_pane unzooms the current pane when we move; + -- then SetPaneZoomState(true) zooms the newly active pane. + window:perform_action( + act.Multiple({ + act.ActivatePaneDirection(direction), + act.SetPaneZoomState(true), + }), + pane + ) + else + window:perform_action(act.ActivatePaneDirection(direction), pane) + end + end) + end + + -- ─── Status bar ──────────────────────────────────────────────────────────── + + -- Per-mode accent colour and hint text, generated from Nix/stylix palette + local mode_info = { + ${modeTableEntries} + } + + local bg_bar = "${color.base01}" + local fg_text = "${color.base05}" + local fg_dim = "${color.base03}" + local bg_main = "${color.base00}" + + -- Render hints with the key in the mode accent colour and the icon dimmed. + -- Each hint token is "key:icon"; we colour them individually. + local function render_hints(hint, accent) + local cells = {} + for token in hint:gmatch("%S+") do + local key, icon = token:match("^(.-):(.)$") + if key and icon then + table.insert(cells, { Background = { Color = bg_bar } }) + table.insert(cells, { Foreground = { Color = accent } }) + table.insert(cells, { Text = key }) + table.insert(cells, { Foreground = { Color = fg_text } }) + table.insert(cells, { Text = ":" .. icon .. " " }) + else + table.insert(cells, { Background = { Color = bg_bar } }) + table.insert(cells, { Foreground = { Color = fg_dim } }) + table.insert(cells, { Text = token .. " " }) + end + end + return cells + end + + wezterm.on("format-window-title", function(tab, _pane, _tabs, _panes, _config) + local ok, win = pcall(function() return wezterm.mux.get_window(tab.window_id) end) + if ok and win then + local ws = win:get_workspace() + if ws ~= "default" then + return ws .. " — " .. tab.active_pane.title + end + end + return tab.active_pane.title + end) + + -- format-tab-title: show "rank/total title" when the active tab is in + -- stacked (zoom) mode; otherwise show plain " title ". + -- Returning a cell table gives us explicit padding and colours; + -- returning a plain string loses the retro tab bar's built-in spacing. + local tab_bg_active = "${color.base0D}" + local tab_fg_active = "${color.base00}" + local tab_bg_inactive = "${color.base01}" + local tab_fg_inactive = "${color.base03}" + + wezterm.on("format-tab-title", function(tab, _tabs, panes, _config, _hover, _max_width) + local title = (tab.tab_title and tab.tab_title ~= "") and tab.tab_title or tab.active_pane.title + local bg = tab.is_active and tab_bg_active or tab_bg_inactive + local fg = tab.is_active and tab_fg_active or tab_fg_inactive + + -- Only compute stack position for the active tab. + -- The `panes` event parameter only contains the active pane snapshot + -- and lacks `top`, so we use wezterm.mux.get_tab():panes_with_info() + -- which returns full PaneInformation including top/is_zoomed/is_active. + if tab.is_active then + local mux_tab = wezterm.mux.get_tab(tab.tab_id) + if mux_tab then + local infos = mux_tab:panes_with_info() + table.sort(infos, function(a, b) return a.top < b.top end) + + local any_zoomed = false + local active_rank, total = 0, 0 + for _, p in ipairs(infos) do + total = total + 1 + if p.is_active then active_rank = total end + if p.is_zoomed then any_zoomed = true end + end + + if any_zoomed and total > 1 and active_rank > 0 then + title = active_rank .. "/" .. total .. " " .. title + end + end + end + + return { + { Background = { Color = bg } }, + { Foreground = { Color = fg } }, + { Text = " " .. title .. " " }, + } + end) + + wezterm.on("update-status", function(window, _pane) + local kt = window:active_key_table() + local mode = kt or "locked" + local info = mode_info[mode] or mode_info["locked"] + + window:set_left_status("") + + -- Right: coloured hints then mode badge + local cells = { { Text = " " } } + for _, c in ipairs(render_hints(info.hint, info.fg)) do + table.insert(cells, c) + end + table.insert(cells, "ResetAttributes") + table.insert(cells, { Background = { Color = info.fg } }) + table.insert(cells, { Foreground = { Color = bg_bar } }) + table.insert(cells, { Text = "" }) + table.insert(cells, { Foreground = { Color = bg_main } }) + table.insert(cells, { Attribute = { Intensity = "Bold" } }) + table.insert(cells, { Text = " " .. mode:upper() .. " " }) + table.insert(cells, "ResetAttributes") + window:set_right_status(wezterm.format(cells)) + end) + + -- ─── Appearance ──────────────────────────────────────────────────────────── + + local config = wezterm.config_builder() + + -- Stylix sets the color scheme via its wezterm integration; + -- this is a fallback in case it is not active. + config.color_scheme = "Synthwave (Gogh)" + + config.font = wezterm.font("CommitMono Nerd Font") + config.font_size = 12.0 + + config.window_padding = { left = 6, right = 6, top = 6, bottom = 6 } + + config.default_cursor_style = "BlinkingBar" + config.hide_tab_bar_if_only_one_tab = false + config.use_fancy_tab_bar = false + config.tab_max_width = 24 + + config.colors = { + tab_bar = { + background = "${color.base01}", + active_tab = { bg_color = "${color.base0D}", fg_color = "${color.base00}", intensity = "Bold" }, + inactive_tab = { bg_color = "${color.base01}", fg_color = "${color.base03}" }, + inactive_tab_hover = { bg_color = "${color.base02}", fg_color = "${color.base04}" }, + new_tab = { bg_color = "${color.base01}", fg_color = "${color.base03}" }, + new_tab_hover = { bg_color = "${color.base02}", fg_color = "${color.base04}" }, + }, + } + config.tab_bar_at_bottom = false + + -- ─── SSH Domains ─────────────────────────────────────────────────────────── + + config.ssh_domains = { + { + name = "muho", + remote_address = "muho", + username = "muon", + default_prog = { "nu" }, + }, + } + + -- ─── Shell ───────────────────────────────────────────────────────────────── + + ${lib.optionalString (shell != null) '' + config.default_prog = { "${shell}" } + ''} + + -- ─── Scrollback ──────────────────────────────────────────────────────────── + + config.scrollback_lines = 10000 + + -- ─── Stacked panes ───────────────────────────────────────────────────────── + -- When switching away from a zoomed pane, unzoom it automatically. + -- Paired with stack_focus (Alt+j/k) this gives Zellij-style stacked panels. + config.unzoom_on_switch_pane = true + + -- ─── Key bindings ────────────────────────────────────────────────────────── + -- + -- Default mode = "locked" (all keys pass through to the shell). + -- Alt+Space enters "normal" mode. + -- From normal: t→new tab x→close tab p/n→split r→resize mode + -- Esc / Enter / Alt+Space → back to locked from any mode. + + config.disable_default_key_bindings = true + + config.keys = { + -- Alt+h/l: move focus or switch tab at edge (Zellij MoveFocusOrTab) + -- Alt+j/k: initial bindings (overridden below by stacked-focus variants) + ${viDirKeys { + mods = "ALT"; + indent = " "; + mkAction = d: + if d == "Left" || d == "Right" + then "move_focus_or_tab(\"${d}\")" + else "act.ActivatePaneDirection(\"${d}\")"; + }} + -- Alt+1-9: jump to tab by number + ${tabJumpKeys { + mods = "ALT"; + indent = " "; + }} + -- Alt+n: new pane (split right) + { key = "n", mods = "ALT", action = act.SplitHorizontal({ domain = "CurrentPaneDomain" }) }, + -- Alt+s: split below, then zoom the new pane so stacked mode starts. + -- Two separate perform_action calls: the split runs first and focuses + -- the new pane, then the second call zooms window:active_pane() which + -- is now the new pane (not the original `pane` argument). + { key = "s", mods = "ALT", action = wezterm.action_callback(function(window, pane) + window:perform_action(act.SplitVertical({ domain = "CurrentPaneDomain" }), pane) + window:perform_action(act.SetPaneZoomState(true), window:active_pane()) + end) + }, + -- Alt+j/k: move_vertical — plain focus move normally, stacked zoom + -- when any pane in the tab is already zoomed (mirrors Alt+f state). + { key = "j", mods = "ALT", action = move_vertical("Down") }, + { key = "k", mods = "ALT", action = move_vertical("Up") }, + { key = "DownArrow", mods = "ALT", action = move_vertical("Down") }, + { key = "UpArrow", mods = "ALT", action = move_vertical("Up") }, + -- Alt+f: toggle zoom + { key = "f", mods = "ALT", action = act.TogglePaneZoomState }, + -- Alt+[/]: rotate panes + { key = "[", mods = "ALT", action = act.RotatePanes("CounterClockwise") }, + { key = "]", mods = "ALT", action = act.RotatePanes("Clockwise") }, + -- Alt++/-/=: font size + { key = "+", mods = "ALT", action = act.IncreaseFontSize }, + { key = "-", mods = "ALT", action = act.DecreaseFontSize }, + { key = "=", mods = "ALT", action = act.ResetFontSize }, + -- Alt+i/o: reorder tabs + { key = "i", mods = "ALT", action = act.MoveTabRelative(-1) }, + { key = "o", mods = "ALT", action = act.MoveTabRelative(1) }, + -- Alt+w: close current pane; if tab was in stacked (zoom) mode, re-zoom + -- the new active pane so zoom mode persists across close. + -- confirm=false because the callback must run synchronously after close; + -- a confirm dialog would make the zoom fire before the user answers. + -- Guard: only re-zoom when there will still be panes left after close. + { key = "w", mods = "ALT", action = wezterm.action_callback(function(window, pane) + local panes = pane:tab():panes() + local was_zoomed = tab_is_zoomed(pane) + window:perform_action(act.CloseCurrentPane({ confirm = false }), pane) + if was_zoomed and #panes > 1 then + window:perform_action(act.SetPaneZoomState(true), window:active_pane()) + end + end) + }, + -- Alt+p: command palette + { key = "p", mods = "ALT", action = act.ActivateCommandPalette }, + -- Alt+t: tab navigator (searchable tab list) + { key = "t", mods = "ALT", action = act.ShowTabNavigator }, + -- Alt+q: quit + { key = "q", mods = "ALT", action = act.QuitApplication }, + -- Ctrl+U/D: scroll half page when at the shell prompt; pass through + -- to the running program when it is using the alt screen (vim, less, + -- fzf, etc. switch to the alt screen and need the raw key). + { key = "u", mods = "CTRL", action = scroll_or_passthrough("u", -0.5) }, + { key = "d", mods = "CTRL", action = scroll_or_passthrough("d", 0.5) }, + -- Copy / paste + { key = "c", mods = "CTRL|SHIFT", action = act.CopyTo("Clipboard") }, + { key = "v", mods = "CTRL|SHIFT", action = act.PasteFrom("Clipboard") }, + -- Enter normal mode + { key = "Space", mods = "ALT", action = act.ActivateKeyTable({ name = "normal", one_shot = false }) }, + } + + config.key_tables = { + + -- ── NORMAL ───────────────────────────────────────────────────────────── + normal = { + { key = "t", action = ${andPop "act.SpawnTab(\"CurrentPaneDomain\")"} }, + { key = "x", action = ${andPop "act.CloseCurrentTab({ confirm = true })"} }, + { key = "r", action = act.ActivateKeyTable({ name = "resize", one_shot = false }) }, + { key = "c", action = ${andPop "act.ActivateCopyMode"} }, + { key = "s", action = ${andPop "act.Search({ CaseSensitiveString = \"\" })"} }, + ${exitKeys {selfKey = "Escape";}} }, + + -- ── RESIZE ───────────────────────────────────────────────────────────── + resize = { + ${resizeDirKeys {step = 5;}}${resizeShiftKeys {step = 1;}} + { key = "+", action = act.AdjustPaneSize({ "Right", 5 }) }, + { key = "-", action = act.AdjustPaneSize({ "Left", 5 }) }, + { key = "=", action = act.AdjustPaneSize({ "Right", 5 }) }, + ${exitKeys {selfKey = "r";}} }, + + -- ── COPY MODE ────────────────────────────────────────────────────────── + copy_mode = { + -- movement + { key = "h", action = act.CopyMode("MoveLeft") }, + { key = "j", action = act.CopyMode("MoveDown") }, + { key = "k", action = act.CopyMode("MoveUp") }, + { key = "l", action = act.CopyMode("MoveRight") }, + { key = "LeftArrow", action = act.CopyMode("MoveLeft") }, + { key = "DownArrow", action = act.CopyMode("MoveDown") }, + { key = "UpArrow", action = act.CopyMode("MoveUp") }, + { key = "RightArrow", action = act.CopyMode("MoveRight") }, + { key = "w", action = act.CopyMode("MoveForwardWord") }, + { key = "b", action = act.CopyMode("MoveBackwardWord") }, + { key = "e", action = act.CopyMode("MoveForwardWordEnd") }, + { key = "0", action = act.CopyMode("MoveToStartOfLine") }, + { key = "^", action = act.CopyMode("MoveToStartOfLineContent") }, + { key = "$", action = act.CopyMode("MoveToEndOfLineContent") }, + { key = "g", action = act.CopyMode("MoveToScrollbackTop") }, + { key = "G", action = act.CopyMode("MoveToScrollbackBottom") }, + { key = "Enter", action = act.CopyMode("MoveToStartOfNextLine") }, + { key = "u", mods = "CTRL", action = act.CopyMode({ MoveByPage = -0.5 }) }, + { key = "d", mods = "CTRL", action = act.CopyMode({ MoveByPage = 0.5 }) }, + { key = "b", mods = "CTRL", action = act.CopyMode("PageUp") }, + { key = "f", mods = "CTRL", action = act.CopyMode("PageDown") }, + { key = "PageUp", action = act.CopyMode("PageUp") }, + { key = "PageDown", action = act.CopyMode("PageDown") }, + -- selection + { key = "v", action = act.CopyMode({ SetSelectionMode = "Cell" }) }, + { key = "V", action = act.CopyMode({ SetSelectionMode = "Line" }) }, + { key = "v", mods = "CTRL", action = act.CopyMode({ SetSelectionMode = "Block" }) }, + { key = "o", action = act.CopyMode("MoveToSelectionOtherEnd") }, + { key = "O", action = act.CopyMode("MoveToSelectionOtherEndHoriz") }, + -- yank and exit + { key = "y", action = act.Multiple({ + act.CopyTo("ClipboardAndPrimarySelection"), + act.Multiple({ act.ScrollToBottom, act.CopyMode("Close") }), + }) + }, + -- search within copy mode + { key = "/", action = act.Search({ CaseSensitiveString = "" }) }, + { key = "n", action = act.CopyMode("NextMatch") }, + { key = "p", action = act.CopyMode("PriorMatch") }, + -- exit + { key = "q", action = act.Multiple({ act.ScrollToBottom, act.CopyMode("Close") }) }, + { key = "Escape", action = act.Multiple({ act.ScrollToBottom, act.CopyMode("Close") }) }, + { key = "c", mods = "CTRL", action = act.Multiple({ act.ScrollToBottom, act.CopyMode("Close") }) }, + }, + + -- ── SEARCH MODE ──────────────────────────────────────────────────────── + search_mode = { + -- navigate matches + { key = "Enter", action = act.CopyMode("AcceptPattern") }, + { key = "n", mods = "CTRL", action = act.CopyMode("NextMatch") }, + { key = "p", mods = "CTRL", action = act.CopyMode("PriorMatch") }, + { key = "PageUp", action = act.CopyMode("PriorMatchPage") }, + { key = "PageDown", action = act.CopyMode("NextMatchPage") }, + { key = "UpArrow", action = act.CopyMode("PriorMatch") }, + { key = "DownArrow", action = act.CopyMode("NextMatch") }, + -- cycle match type (case-sensitive / insensitive / regex) + { key = "r", mods = "CTRL", action = act.CopyMode("CycleMatchType") }, + -- clear search input + { key = "u", mods = "CTRL", action = act.CopyMode("ClearPattern") }, + -- Escape: dismiss search bar, return to copy mode at current match + { key = "Escape", action = act.CopyMode("AcceptPattern") }, + }, + } + + return config + ''; + }; + }; +} diff --git a/modules/home/terminal/yazi.nix b/modules/home/terminal/yazi.nix index 160b944..e366d5d 100644 --- a/modules/home/terminal/yazi.nix +++ b/modules/home/terminal/yazi.nix @@ -5,6 +5,7 @@ in with lib; { config = mkIf cfg.tools.enable { programs.yazi = { enable = true; + shellWrapperName = "y"; settings = { manager = { ratio = [ 1 4 3 ]; diff --git a/modules/nixos/core/network.nix b/modules/nixos/core/network.nix index 91317d8..abce4bf 100644 --- a/modules/nixos/core/network.nix +++ b/modules/nixos/core/network.nix @@ -100,6 +100,15 @@ in 10.0.0.2 muon 10.0.0.3 muho 10.0.0.4 muop + 127.0.0.1 word.local + ''; + + # Wildcard DNS: *.word.local -> 127.0.0.1 via NetworkManager's built-in dnsmasq + networking.networkmanager.dns = "dnsmasq"; + # Force resolv.conf to use local dnsmasq so wildcard DNS is actually queried + networking.resolvconf.useLocalResolver = true; + environment.etc."NetworkManager/dnsmasq.d/word-local.conf".text = '' + address=/.word.local/127.0.0.1 ''; # gateway = diff --git a/modules/nixos/core/nix.nix b/modules/nixos/core/nix.nix index 697fed8..5d88a0c 100644 --- a/modules/nixos/core/nix.nix +++ b/modules/nixos/core/nix.nix @@ -51,6 +51,7 @@ environment.systemPackages = with pkgs; [ inputs.nix-alien.packages.${system}.nix-alien + inputs.home-manager.packages.${system}.home-manager colmena ]; programs.nix-ld.enable = true; diff --git a/modules/nixos/flatpak/default.nix b/modules/nixos/flatpak/default.nix index 2cabac2..27c2ed7 100644 --- a/modules/nixos/flatpak/default.nix +++ b/modules/nixos/flatpak/default.nix @@ -22,5 +22,6 @@ in xdg.portal.extraPortals = [ pkgs.xdg-desktop-portal ]; + xdg.portal.config.common.default = "*"; }; } diff --git a/modules/nixos/server/default.nix b/modules/nixos/server/default.nix index 26912c8..a291e7d 100644 --- a/modules/nixos/server/default.nix +++ b/modules/nixos/server/default.nix @@ -1,4 +1,8 @@ -{ pkgs, lib, ... }: { +{ + pkgs, + lib, + ... +}: { imports = [ ./containers ./gaming @@ -25,5 +29,6 @@ ./lemmy.nix ./audio.nix ./atuin.nix + ./murmur.nix ]; } diff --git a/modules/nixos/server/murmur.nix b/modules/nixos/server/murmur.nix new file mode 100644 index 0000000..cc5302e --- /dev/null +++ b/modules/nixos/server/murmur.nix @@ -0,0 +1,25 @@ +{ + pkgs, + lib, + config, + ... +}: let + cfg = config.mods.server.murmur; +in + with lib; { + options.mods.server = { + murmur = { + enable = mkEnableOption { + default = false; + description = "enables murmur server"; + }; + }; + }; + + config = mkIf cfg.enable { + services.murmur = { + enable = true; + openFirewall = true; + }; + }; + } diff --git a/modules/nixos/server/search.nix b/modules/nixos/server/search.nix index 1a1a94c..5089449 100644 --- a/modules/nixos/server/search.nix +++ b/modules/nixos/server/search.nix @@ -1,5 +1,9 @@ -{ pkgs, lib, config, ... }: -let +{ + pkgs, + lib, + config, + ... +}: let inherit (lib) types mkOption mkEnableOption; cfg = config.mods.server.search; port = config.mods.server.nginx.ports.search; @@ -20,22 +24,23 @@ in { server.bind_address = "0.0.0.0"; server.secret_key = "temporary-before-sops"; - enabled_plugins = [ "Hostnames plugin" "Tracker URL remover" ]; - hostnames.remove = [ "(.*.)?facebook.com$" ]; + enabled_plugins = ["Hostnames plugin" "Tracker URL remover"]; + hostnames.remove = ["(.*.)?facebook.com$"]; hostnames.replace = { # Self-hosted - "(.*.)?reddit.com$" = "reddit.muon.host"; + # "(.*.)?reddit.com$" = "reddit.muon.host"; # "(.*.)?youtube.com$" = "videos.muon.host"; # TODO not working # External - "(.*.)?youtube.com$" = "invidious.nerdvpn.de"; - "(.*.)?imdb.com$" = "libremdb.iket.me"; - "(.*.)?imgur.com$" = "rimgo.privacyredirect.com"; - "(.*.)?twitch.com$" = "safetwitch.privacyredirect.com"; - "(.*.)?wikipedia.com$" = "wikiless.privacyredirect.com"; - "(.*.)?medium.com$" = "scribe.privacyredirect.com"; - "(.*.)?stackoverflow.com$" = "anonymousoverflow.privacyredirect.com"; - "(.*.)?github.com$" = "gothub.privacyredirect.com"; + "(.*.)?reddit.com$" = "old.reddit.com"; + # "(.*.)?youtube.com$" = "invidious.nerdvpn.de"; + # "(.*.)?imdb.com$" = "libremdb.iket.me"; + # "(.*.)?imgur.com$" = "rimgo.privacyredirect.com"; + # "(.*.)?twitch.com$" = "safetwitch.privacyredirect.com"; + # "(.*.)?wikipedia.com$" = "wikiless.privacyredirect.com"; + # "(.*.)?medium.com$" = "scribe.privacyredirect.com"; + # "(.*.)?stackoverflow.com$" = "anonymousoverflow.privacyredirect.com"; + # "(.*.)?github.com$" = "gothub.privacyredirect.com"; }; }; }; diff --git a/modules/nixos/theme/default.nix b/modules/nixos/theme/default.nix index e9de1ce..0023f43 100644 --- a/modules/nixos/theme/default.nix +++ b/modules/nixos/theme/default.nix @@ -23,7 +23,8 @@ in { stylix = { enable = true; autoEnable = true; - base16Scheme = cfg.scheme; + # base16Scheme = cfg.scheme; + base16Scheme = ./synthwave84.yaml; image = cfg.wallpaper; cursor = { name = "phinger-cursors-light"; diff --git a/modules/nixos/theme/synthwave84.yaml b/modules/nixos/theme/synthwave84.yaml new file mode 100644 index 0000000..08be6c2 --- /dev/null +++ b/modules/nixos/theme/synthwave84.yaml @@ -0,0 +1,24 @@ +system: "base16" +name: "synthwave84" +slug: "synthwave84" +author: "Someone" +variant: "dark" +palette: + base00: "2a2139" + base01: "241b2f" + base02: "34294f" + base03: "8e6fe4" + base04: "c1c3e6" + base05: "f5f5ff" + base06: "ffffff" + base07: "fffdf8" + base08: "f92aad" + base09: "f97e72" + base0A: "ffe261" + base0B: "72f1b8" + base0C: "78dcff" + base0D: "36f9f6" + base0E: "ff7edb" + base0F: "bb8977" + + diff --git a/modules/nixos/unfree/nvidia.nix b/modules/nixos/unfree/nvidia.nix index e990289..d67ba90 100644 --- a/modules/nixos/unfree/nvidia.nix +++ b/modules/nixos/unfree/nvidia.nix @@ -1,5 +1,10 @@ -{ pkgs, lib, config, ... }: -let cfg = config.mods.unfree.nvidia; +{ + pkgs, + lib, + config, + ... +}: let + cfg = config.mods.unfree.nvidia; in { options.mods.unfree.nvidia = { enable = lib.mkEnableOption { @@ -7,7 +12,7 @@ in { description = "enables proprietary nvidia drivers"; }; packages = lib.mkOption { - default = [ "nvidia-x11" "nvidia-settings" ]; + default = ["nvidia-x11" "nvidia-settings"]; description = "unfree packages"; }; driver = lib.mkOption { @@ -23,7 +28,7 @@ in { # enable32Bit = true; # }; - services.xserver.videoDrivers = [ "nvidia" "nvidia-dkms" ]; + services.xserver.videoDrivers = ["nvidia"]; hardware.nvidia = { modesetting.enable = true; diff --git a/shell.nix b/shell.nix index 5d84124..ffc8955 100644 --- a/shell.nix +++ b/shell.nix @@ -1,7 +1,6 @@ -{ pkgs ? import { }, ... }: { +{pkgs ? import {}, ...}: { default = pkgs.mkShell { - NIX_CONFIG = - "extra-experimental-features = nix-command flakes ca-derivations"; + NIX_CONFIG = "extra-experimental-features = nix-command flakes ca-derivations"; nativeBuildInputs = with pkgs; [ nix git diff --git a/utils.nix b/utils.nix index fdb6c8b..b3c1b7d 100644 --- a/utils.nix +++ b/utils.nix @@ -1,4 +1,7 @@ -{ inputs, system, sources, ... }: { +{ inputs, system, sources, ... }: +let + pkgs = import inputs.nixpkgs { inherit system; }; +in { mkHost = host: inputs.nixpkgs.lib.nixosSystem { specialArgs = { inherit inputs system sources; }; @@ -11,4 +14,41 @@ inputs.impermanence.nixosModules.impermanence ]; }; + + # Build a standalone HM configuration for a given host NixOS config and + # home.nix file. osConfig is injected as a specialArg so all modules that + # reference it continue to work. + mkHome = { hostConfig, homeFile, username ? "muon" }: + inputs.home-manager.lib.homeManagerConfiguration { + inherit pkgs; + extraSpecialArgs = { + inherit inputs system sources; + osConfig = hostConfig.config; + }; + modules = [ + homeFile + inputs.self.outputs.homeManagerModules.default + inputs.stylix.homeManagerModules.stylix + ({ osConfig, ... }: { + home.username = username; + home.homeDirectory = "/home/${username}"; + # Mirror the stylix settings from the NixOS config so the standalone + # HM build has the same theme without re-declaring it. + stylix = { + enable = osConfig.stylix.enable; + autoEnable = osConfig.stylix.autoEnable; + base16Scheme = osConfig.stylix.base16Scheme; + image = osConfig.stylix.image; + cursor = osConfig.stylix.cursor; + # Forward individual font options; stylix derives fonts.packages + # internally as read-only so we must not forward that attr. + fonts.monospace = osConfig.stylix.fonts.monospace; + fonts.sansSerif = osConfig.stylix.fonts.sansSerif; + fonts.serif = osConfig.stylix.fonts.serif; + fonts.emoji = osConfig.stylix.fonts.emoji; + fonts.sizes = osConfig.stylix.fonts.sizes; + }; + }) + ]; + }; }