From 41bdeb88009dd232808edee9fc0e4ae94358d8d6 Mon Sep 17 00:00:00 2001 From: Arpit Chakladar Date: Sat, 29 Aug 2026 02:02:23 +0530 Subject: refactor: moving script into modules that makes sense - instead of collecting all scripts into modules/scripts we are moving them to modules that makes sense (yazi-file-chooser.sh into yazi) --- modules/communication/neomutt/default.nix | 47 ++++- modules/communication/neomutt/macros.nix | 2 +- modules/communication/neomutt/neomutt-sync.sh | 62 ++++++ modules/default.nix | 4 +- modules/file-management/yazi/default.nix | 88 +++++--- modules/file-management/yazi/yazi-file-chooser.sh | 21 ++ modules/lib/default.nix | 48 +++++ modules/lib/script.nix | 146 ++++++++++++++ modules/media/default.nix | 1 + modules/media/screen-recording/assertions.nix | 11 + modules/media/screen-recording/default.nix | 40 ++++ modules/media/screen-recording/screen-recording.sh | 52 +++++ .../media/screen-recording/screen-recording.zsh | 4 + modules/networking/usque/default.nix | 30 ++- modules/networking/usque/usque-warp.sh | 223 +++++++++++++++++++++ modules/networking/usque/usque-warp.zsh | 17 ++ modules/scripts/deep-clean/default.nix | 22 -- modules/scripts/deep-clean/script.sh | 35 ---- modules/scripts/default.nix | 34 ---- modules/scripts/gopass-ssh-load/default.nix | 32 --- modules/scripts/gopass-ssh-load/script.sh | 46 ----- modules/scripts/gpg-backup/completion.zsh | 23 --- modules/scripts/gpg-backup/default.nix | 27 --- modules/scripts/gpg-backup/script.sh | 149 -------------- modules/scripts/lib.nix | 137 ------------- modules/scripts/neomutt-sync/assertions.nix | 7 - modules/scripts/neomutt-sync/default.nix | 31 --- modules/scripts/neomutt-sync/script.sh | 62 ------ modules/scripts/nix-update/completion.zsh | 22 -- modules/scripts/nix-update/default.nix | 26 --- modules/scripts/nix-update/script.sh | 122 ----------- modules/scripts/screen-recording/assertions.nix | 11 - modules/scripts/screen-recording/completion.zsh | 4 - modules/scripts/screen-recording/default.nix | 35 ---- modules/scripts/screen-recording/script.sh | 52 ----- modules/scripts/system-monitor/assertions.nix | 19 -- modules/scripts/system-monitor/default.nix | 36 ---- modules/scripts/system-monitor/script.sh | 26 --- modules/scripts/usque-warp/completion.zsh | 17 -- modules/scripts/usque-warp/default.nix | 24 --- modules/scripts/usque-warp/script.sh | 223 --------------------- modules/scripts/yazi-file-chooser/default.nix | 25 --- modules/scripts/yazi-file-chooser/script.sh | 21 -- modules/security/gopass/default.nix | 78 ++++--- modules/security/gopass/gopass-ssh-load.sh | 46 +++++ modules/security/gpg/default.nix | 61 ++++-- modules/security/gpg/gpg-backup.sh | 149 ++++++++++++++ modules/security/gpg/gpg-backup.zsh | 23 +++ modules/system/deep-clean/deep-clean.sh | 35 ++++ modules/system/deep-clean/default.nix | 18 ++ modules/system/default.nix | 3 + modules/system/nix-update/default.nix | 24 +++ modules/system/nix-update/nix-update.sh | 122 +++++++++++ modules/system/nix-update/nix-update.zsh | 22 ++ modules/system/system-monitor/assertions.nix | 19 ++ modules/system/system-monitor/default.nix | 41 ++++ modules/system/system-monitor/system-monitor.sh | 26 +++ 57 files changed, 1379 insertions(+), 1352 deletions(-) create mode 100644 modules/communication/neomutt/neomutt-sync.sh create mode 100644 modules/file-management/yazi/yazi-file-chooser.sh create mode 100644 modules/lib/default.nix create mode 100644 modules/lib/script.nix create mode 100644 modules/media/screen-recording/assertions.nix create mode 100644 modules/media/screen-recording/default.nix create mode 100644 modules/media/screen-recording/screen-recording.sh create mode 100644 modules/media/screen-recording/screen-recording.zsh create mode 100644 modules/networking/usque/usque-warp.sh create mode 100644 modules/networking/usque/usque-warp.zsh delete mode 100644 modules/scripts/deep-clean/default.nix delete mode 100644 modules/scripts/deep-clean/script.sh delete mode 100644 modules/scripts/default.nix delete mode 100644 modules/scripts/gopass-ssh-load/default.nix delete mode 100644 modules/scripts/gopass-ssh-load/script.sh delete mode 100644 modules/scripts/gpg-backup/completion.zsh delete mode 100644 modules/scripts/gpg-backup/default.nix delete mode 100644 modules/scripts/gpg-backup/script.sh delete mode 100644 modules/scripts/lib.nix delete mode 100644 modules/scripts/neomutt-sync/assertions.nix delete mode 100644 modules/scripts/neomutt-sync/default.nix delete mode 100644 modules/scripts/neomutt-sync/script.sh delete mode 100644 modules/scripts/nix-update/completion.zsh delete mode 100644 modules/scripts/nix-update/default.nix delete mode 100644 modules/scripts/nix-update/script.sh delete mode 100644 modules/scripts/screen-recording/assertions.nix delete mode 100644 modules/scripts/screen-recording/completion.zsh delete mode 100644 modules/scripts/screen-recording/default.nix delete mode 100644 modules/scripts/screen-recording/script.sh delete mode 100644 modules/scripts/system-monitor/assertions.nix delete mode 100644 modules/scripts/system-monitor/default.nix delete mode 100644 modules/scripts/system-monitor/script.sh delete mode 100644 modules/scripts/usque-warp/completion.zsh delete mode 100644 modules/scripts/usque-warp/default.nix delete mode 100644 modules/scripts/usque-warp/script.sh delete mode 100644 modules/scripts/yazi-file-chooser/default.nix delete mode 100644 modules/scripts/yazi-file-chooser/script.sh create mode 100644 modules/security/gopass/gopass-ssh-load.sh create mode 100644 modules/security/gpg/gpg-backup.sh create mode 100644 modules/security/gpg/gpg-backup.zsh create mode 100644 modules/system/deep-clean/deep-clean.sh create mode 100644 modules/system/deep-clean/default.nix create mode 100644 modules/system/nix-update/default.nix create mode 100644 modules/system/nix-update/nix-update.sh create mode 100644 modules/system/nix-update/nix-update.zsh create mode 100644 modules/system/system-monitor/assertions.nix create mode 100644 modules/system/system-monitor/default.nix create mode 100644 modules/system/system-monitor/system-monitor.sh (limited to 'modules') diff --git a/modules/communication/neomutt/default.nix b/modules/communication/neomutt/default.nix index 8613032..8e5d51b 100644 --- a/modules/communication/neomutt/default.nix +++ b/modules/communication/neomutt/default.nix @@ -2,8 +2,32 @@ { config, lib, + pkgs, ... }: +let + inherit ((import ../../lib/script.nix { inherit lib pkgs; })) mkScriptModule; + neomuttSync = mkScriptModule { + scope = [ + "communication" + "neomutt" + ]; + name = "neomutt-sync"; + path = ./neomutt-sync.sh; + description = "Neomutt-sync - Interactive mail sync with dialog progress bar"; + deps = [ + pkgs.bash + pkgs.dialog + pkgs.coreutils + pkgs.gawk + pkgs.gnused + pkgs.util-linux + config.programs.mbsync.package + config.programs.notmuch.package + ]; + inherit config; + }; +in { imports = [ ./account @@ -23,10 +47,23 @@ default = config.programs.neomutt.package; description = "The neomutt package to use, wrapped with urlscan in PATH."; }; - }; + } + // neomuttSync.options.communication.neomutt; - config = lib.mkIf config.communication.neomutt.enable { - accounts.email.maildirBasePath = "${config.home.homeDirectory}/.local/share/mail"; - home.sessionVariables.MAILDIR = config.accounts.email.maildirBasePath; - }; + config = lib.mkMerge [ + (lib.mkIf config.communication.neomutt.enable { + accounts.email.maildirBasePath = "${config.home.homeDirectory}/.local/share/mail"; + home.sessionVariables.MAILDIR = config.accounts.email.maildirBasePath; + }) + neomuttSync.config + { + assertions = [ + { + assertion = + !config.communication.neomutt.neomutt-sync.enable || config.communication.neomutt.enable; + message = "communication.neomutt.neomutt-sync is enabled but requires `communication.neomutt.enable`."; + } + ]; + } + ]; } diff --git a/modules/communication/neomutt/macros.nix b/modules/communication/neomutt/macros.nix index 57444d9..e5ff437 100644 --- a/modules/communication/neomutt/macros.nix +++ b/modules/communication/neomutt/macros.nix @@ -9,7 +9,7 @@ "pager" ]; key = "gs"; - action = "set my_wait_key=$wait_key wait_key=no${lib.getExe config.scripts.neomutt-sync.package}set wait_key=$my_wait_key"; + action = "set my_wait_key=$wait_key wait_key=no${lib.getExe config.communication.neomutt.neomutt-sync.package}set wait_key=$my_wait_key"; } { # Extract every URL from the message via urlscan into a picker menu diff --git a/modules/communication/neomutt/neomutt-sync.sh b/modules/communication/neomutt/neomutt-sync.sh new file mode 100644 index 0000000..d0dac63 --- /dev/null +++ b/modules/communication/neomutt/neomutt-sync.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +DIALOGRC=$(mktemp) +NOTMUCH_LOG=$(mktemp) +TITLE="[󰇮 SYNCING MAIL]" +export DIALOGRC +trap 'rm -f "$DIALOGRC" "$NOTMUCH_LOG"' EXIT # Clean up everything on exit + +cat << 'EOF' > "$DIALOGRC" +use_shadow = ON +use_colors = ON +screen_color = (WHITE,BLACK,OFF) +dialog_color = (WHITE,BLACK,ON) +title_color = (YELLOW,BLACK,ON) +gauge_color = (GREEN,BLACK,ON) +border_color = (CYAN,BLACK,ON) +border2_color = (BLUE,BLACK,ON) +shadow_color = (BLACK,BLACK,ON) + +# Active buttons: magenta emphasis, matching Base16 base0E +button_active_color = (MAGENTA,BLACK,ON) +button_key_active_color = (MAGENTA,BLACK,ON) +button_label_active_color = (MAGENTA,BLACK,ON) + +# Inactive buttons: cyan secondary emphasis, matching Base16 base0C +button_inactive_color = (CYAN,BLACK,OFF) +button_key_inactive_color = (CYAN,BLACK,OFF) +button_label_inactive_color = (CYAN,BLACK,OFF) +EOF + +# Run the sync and indexing progress bar +( + # mbsync phase + script -q -e -c "mbsync -a" /dev/null | awk -v RS='\r' ' + { + c_str = "" + b_str = "" + for(i=1; i<=NF; i++) { + if ($i == "C:") c_str = $(i+1) + if ($i == "B:") b_str = $(i+1) + } + if (b_str != "") { + split(b_str, arr, "/") + if (arr[2] > 0) { + percent = int((arr[1] / arr[2]) * 100) + printf "XXX\n%d\nSyncing | Channel: %s | Mailbox: %s\nXXX\n", percent, c_str, b_str + } + } + fflush() + }' + + # notmuch phase + printf "XXX\n100\nIndexing new mail with notmuch...\nXXX\n" + notmuch new > "$NOTMUCH_LOG" 2>&1 +) | dialog --title "$TITLE" --gauge "Initializing..." 8 80 0 + +# Format the captured notmuch output into a single clean line +CLEAN_OUT=$(tr '\n' ' ' < "$NOTMUCH_LOG" | sed 's/ */ /g') + +# Display final results and wait for user input +dialog --title "$TITLE" --msgbox "Done!\n$CLEAN_OUT" 8 80 + +clear diff --git a/modules/default.nix b/modules/default.nix index f537af4..05b1dca 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -1,5 +1,5 @@ # Top-level module aggregator importing all submodules -{ config, ... }: +{ ... }: { imports = [ ./communication @@ -8,10 +8,10 @@ ./file-management ./fonts ./gaming + ./lib ./media ./networking ./office - ./scripts ./security ./system ./terminal diff --git a/modules/file-management/yazi/default.nix b/modules/file-management/yazi/default.nix index 8d59446..d704614 100644 --- a/modules/file-management/yazi/default.nix +++ b/modules/file-management/yazi/default.nix @@ -1,5 +1,29 @@ # Terminal file manager with native previews -{ config, lib, ... }: +{ + config, + lib, + pkgs, + ... +}: +let + inherit ((import ../../lib/script.nix { inherit lib pkgs; })) mkScriptModule; + yaziFileChooser = mkScriptModule { + scope = [ + "file-management" + "yazi" + ]; + name = "yazi-file-chooser"; + path = ./yazi-file-chooser.sh; + description = "Yazi-based file chooser for XDG Desktop Portal"; + deps = [ + pkgs.bash + config.file-management.yazi.package + config.terminal.kitty.package + ]; + extraLinks = [ "~/.config/xdg-desktop-portal-termfilechooser/config" ]; + inherit config; + }; +in { imports = [ ./theme.nix @@ -15,40 +39,44 @@ defaultText = lib.literalExpression "config.programs.yazi.finalPackage"; description = "The yazi package to use. Defaults to the wrapped finalPackage from programs.yazi."; }; - }; + } + // yaziFileChooser.options.file-management.yazi; - config = lib.mkIf config.file-management.yazi.enable { - programs.yazi = { - enable = true; - shellWrapperName = "yy"; - enableZshIntegration = config.terminal.zsh.enable; - settings = { - mgr = { - show_hidden = true; + config = lib.mkMerge [ + (lib.mkIf config.file-management.yazi.enable { + programs.yazi = { + enable = true; + shellWrapperName = "yy"; + enableZshIntegration = config.terminal.zsh.enable; + settings = { + mgr = { + show_hidden = true; + }; }; }; - }; - home.file.".local/share/icons/hicolor/scalable/apps/yazi.png" = { - source = config.lib.file.mkOutOfStoreSymlink "${config.file-management.yazi.package}/share/pixmaps/yazi.png"; - }; + home.file.".local/share/icons/hicolor/scalable/apps/yazi.png" = { + source = config.lib.file.mkOutOfStoreSymlink "${config.file-management.yazi.package}/share/pixmaps/yazi.png"; + }; - xdg.desktopEntries."yazi" = { - name = "Yazi"; - exec = "${lib.getExe config.terminal.kitty.package} --class yazi -e ${lib.getExe config.file-management.yazi.package}"; - icon = "yazi"; - categories = [ "Utility" ]; - comment = "Terminal file manager"; - terminal = false; - type = "Application"; - }; + xdg.desktopEntries."yazi" = { + name = "Yazi"; + exec = "${lib.getExe config.terminal.kitty.package} --class yazi -e ${lib.getExe config.file-management.yazi.package}"; + icon = "yazi"; + categories = [ "Utility" ]; + comment = "Terminal file manager"; + terminal = false; + type = "Application"; + }; - xdg.mimeApps.defaultApplications = { - "inode/directory" = "yazi.desktop"; - }; + xdg.mimeApps.defaultApplications = { + "inode/directory" = "yazi.desktop"; + }; - home.sessionVariables = { - TERMCMD = lib.mkIf config.terminal.kitty.enable "${lib.getExe config.terminal.kitty.package} --class file-explorer --title 'Yazi'"; - }; - }; + home.sessionVariables = { + TERMCMD = lib.mkIf config.terminal.kitty.enable "${lib.getExe config.terminal.kitty.package} --class file-explorer --title 'Yazi'"; + }; + }) + yaziFileChooser.config + ]; } diff --git a/modules/file-management/yazi/yazi-file-chooser.sh b/modules/file-management/yazi/yazi-file-chooser.sh new file mode 100644 index 0000000..7978222 --- /dev/null +++ b/modules/file-management/yazi/yazi-file-chooser.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# $1 toggles multiple selection +# $2 toggles directory select mode +# $3 toggles save file mode +# $4 is the initial directory path +# $5 is the output path file + +# multiple="$1" +directory="$2" +# save="$3" +path="$4" +out="$5" + +# Pick directory vs single/multiple file +if [ "$directory" = "1" ]; then + # Outputs the final working directory path on exit + exec yazi "$path" --cwd-file="$out" +else + # Writes all selected file paths to the output file on exit + exec yazi "$path" --chooser-file="$out" +fi diff --git a/modules/lib/default.nix b/modules/lib/default.nix new file mode 100644 index 0000000..fd54fa2 --- /dev/null +++ b/modules/lib/default.nix @@ -0,0 +1,48 @@ +# Internal aggregation for script modules registered via mkScriptModule +{ + config, + lib, + ... +}: +{ + options.scriptPaths = lib.mkOption { + type = lib.types.listOf (lib.types.listOf lib.types.str); + default = [ ]; + internal = true; + description = "Attr paths of script modules registered via mkScriptModule."; + }; + + config = { + home.packages = lib.filter (p: p != null) ( + map (path: (lib.getAttrFromPath path config).package) config.scriptPaths + ); + + xdg.desktopEntries = lib.listToAttrs ( + map + ( + path: + let + name = lib.last path; + sc = lib.getAttrFromPath path config; + in + lib.nameValuePair name { + name = sc.desktop.displayName; + exec = "${lib.getExe config.terminal.kitty.package} --class ${name} -e ${lib.getExe sc.package}"; + icon = sc.desktop.icon; + categories = [ "Utility" ]; + terminal = false; + type = "Application"; + } + ) + ( + lib.filter ( + path: + let + sc = lib.getAttrFromPath path config; + in + sc.enable && sc.desktop.enable + ) config.scriptPaths + ) + ); + }; +} diff --git a/modules/lib/script.nix b/modules/lib/script.nix new file mode 100644 index 0000000..9871b88 --- /dev/null +++ b/modules/lib/script.nix @@ -0,0 +1,146 @@ +# Library for creating script modules +{ + lib, + pkgs, +}: +let + shellArgs = { + zsh = name: file: "--zsh --name _${name} ${file}"; + bash = name: file: "--bash --name ${name}.bash ${file}"; + }; + + mkScript = + name: path: description: env: deps: completion: + let + descLines = lib.filter (s: s != "") (lib.splitString "\n" description); + descComment = + if descLines == [ ] then "" else lib.concatMapStringsSep "\n" (line: "# ${line}") descLines + "\n"; + envVars = lib.concatStringsSep "\n" ( + lib.mapAttrsToList (n: v: "export ${n}=${lib.escapeShellArg (toString v)}") env + ); + base = pkgs.writeShellApplication { + inherit name; + runtimeInputs = deps; + text = '' + ${descComment}${envVars} + ${builtins.readFile path} + ''; + }; + completionDrv = + pkgs.runCommand "${name}-completion" + { + nativeBuildInputs = [ pkgs.installShellFiles ]; + } + ( + "mkdir -p $out\n" + + lib.concatStringsSep "\n" ( + lib.mapAttrsToList (shell: content: '' + installShellCompletion ${shellArgs.${shell} name (pkgs.writeText "_${name}.${shell}" content)} + '') completion + ) + ); + in + if completion == { } then + base + else + pkgs.symlinkJoin { + name = name; + paths = [ + base + completionDrv + ]; + meta = base.meta or { }; + }; + + mkScriptModule = + { + scope ? [ ], + name, + path, + env ? { }, + deps ? [ ], + desktop ? null, + extraLinks ? [ ], + config, + description ? "", + completion ? { }, + }: + let + scriptAttrs = scope ++ [ name ]; + dottedName = lib.concatStringsSep "." scriptAttrs; + c = lib.getAttrFromPath scriptAttrs config; + scriptDrv = mkScript name path description env deps completion; + in + { + options = lib.setAttrByPath scriptAttrs { + enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Whether to enable the ${name} script."; + }; + package = lib.mkOption { + type = lib.types.nullOr lib.types.package; + readOnly = true; + description = "The derivation for the ${name} script."; + }; + completion.enable = lib.mkOption { + type = lib.types.bool; + default = completion != { }; + description = "Whether to install completions for the ${name} script."; + }; + desktop = { + enable = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Whether to create a .desktop entry for this script."; + }; + displayName = lib.mkOption { + type = lib.types.str; + default = name; + description = "Display name for the .desktop entry."; + }; + icon = lib.mkOption { + type = lib.types.str; + default = "kitty"; + description = "Icon for the .desktop entry."; + }; + }; + }; + + config = + lib.setAttrByPath scriptAttrs ( + lib.mkIf c.enable ( + { + package = scriptDrv; + } + // lib.optionalAttrs (desktop != null) { inherit desktop; } + ) + ) + // { + home.file = lib.mkIf c.enable ( + builtins.listToAttrs ( + map (linkPath: { + name = lib.removePrefix "~/" linkPath; + value = { + source = "${scriptDrv}/bin/${name}"; + }; + }) extraLinks + ) + ); + assertions = [ + { + assertion = !c.enable || !c.desktop.enable || config.terminal.kitty.enable; + message = "${dottedName}.desktop.enable requires terminal.kitty.enable because desktop entries launch scripts in kitty."; + } + { + assertion = !c.completion.enable || completion != { }; + message = "${dottedName}.completion.enable is true but no completion text was provided to mkScriptModule."; + } + ]; + scriptPaths = [ scriptAttrs ]; + }; + }; +in +{ + inherit mkScript mkScriptModule; +} diff --git a/modules/media/default.nix b/modules/media/default.nix index 74cc62d..0f466fa 100644 --- a/modules/media/default.nix +++ b/modules/media/default.nix @@ -6,6 +6,7 @@ ./grim ./pamixer ./playerctl + ./screen-recording ./slurp ./vlc ./wf-recorder diff --git a/modules/media/screen-recording/assertions.nix b/modules/media/screen-recording/assertions.nix new file mode 100644 index 0000000..200d5ea --- /dev/null +++ b/modules/media/screen-recording/assertions.nix @@ -0,0 +1,11 @@ +{ config, ... }: +[ + { + assertion = !config.media.screen-recording.enable || config.media.slurp.enable; + message = "media.screen-recording is enabled but requires `media.slurp.enable`."; + } + { + assertion = !config.media.screen-recording.enable || config.media.wf-recorder.enable; + message = "media.screen-recording is enabled but requires `media.wf-recorder.enable`."; + } +] diff --git a/modules/media/screen-recording/default.nix b/modules/media/screen-recording/default.nix new file mode 100644 index 0000000..7a68f08 --- /dev/null +++ b/modules/media/screen-recording/default.nix @@ -0,0 +1,40 @@ +# Screen recording script using wf-recorder and slurp +{ + config, + lib, + pkgs, + ... +}: +let + inherit ((import ../../lib/script.nix { inherit lib pkgs; })) mkScriptModule; + script = mkScriptModule { + scope = [ "media" ]; + name = "screen-recording"; + path = ./screen-recording.sh; + description = "Screen recording script using wf-recorder and slurp for region selection\nRecords Wayland screen to MP4\nUsage: screen-recording.sh [-s|--select]\n -s, --select Launch slurp to select recording region"; + deps = [ + pkgs.bash + config.media.slurp.package + config.media.wf-recorder.package + ]; + completion.zsh = builtins.readFile ./screen-recording.zsh; + desktop = { + enable = true; + displayName = "Screen Recording"; + icon = "obs"; + }; + inherit config; + }; +in +{ + options = script.options; + config = lib.mkMerge [ + script.config + { + assertions = import ./assertions.nix { inherit config lib; }; + home.file.".local/share/icons/hicolor/scalable/apps/obs.svg" = { + source = ../../../assets/icons/obs.svg; + }; + } + ]; +} diff --git a/modules/media/screen-recording/screen-recording.sh b/modules/media/screen-recording/screen-recording.sh new file mode 100644 index 0000000..336c32e --- /dev/null +++ b/modules/media/screen-recording/screen-recording.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +OUT_DIR="$HOME/Videos/Recordings" +mkdir -p "$OUT_DIR" + +# Colors +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +# Argument parsing +SELECT_MODE=false +while [[ "$#" -gt 0 ]]; do + case $1 in + -s|--select) SELECT_MODE=true ;; + esac + shift +done + +# Geometry selection +GEOMETRY="" + +if [ "$SELECT_MODE" = true ]; then + if command -v slurp >/dev/null 2>&1; then + echo -e "${YELLOW}Select a window or draw a box...${NC}" + GEOMETRY=$(slurp) + + if [ -z "$GEOMETRY" ]; then + echo "Selection cancelled. Exiting." + exit 1 + fi + else + echo -e "${RED}Error: 'slurp' not found.${NC} Recording full screen..." + sleep 1 + fi +fi + +# Start recording +FILENAME="$OUT_DIR/recording-$(date +%Y%m%d-%H%M%S).mp4" + +echo -e "${GREEN}Recording started!${NC}" +echo -e "Press ${YELLOW}'q'${NC} in this terminal to stop." +echo "File: $FILENAME" +echo "------------------------------------------------" + +if [ -n "$GEOMETRY" ]; then + wf-recorder -g "$GEOMETRY" -f "$FILENAME" +else + wf-recorder -f "$FILENAME" +fi + +echo -e "\n${GREEN}Done!${NC} Video saved to $FILENAME" diff --git a/modules/media/screen-recording/screen-recording.zsh b/modules/media/screen-recording/screen-recording.zsh new file mode 100644 index 0000000..835ec43 --- /dev/null +++ b/modules/media/screen-recording/screen-recording.zsh @@ -0,0 +1,4 @@ +#compdef screen-recording + +_arguments \ + '(-s --select)'{-s,--select}'[Launch slurp to select recording region]' diff --git a/modules/networking/usque/default.nix b/modules/networking/usque/default.nix index b8c25ee..9d04ab7 100644 --- a/modules/networking/usque/default.nix +++ b/modules/networking/usque/default.nix @@ -5,6 +5,24 @@ lib, ... }: +let + inherit ((import ../../lib/script.nix { inherit lib pkgs; })) mkScriptModule; + usqueWarp = mkScriptModule { + scope = [ + "networking" + "usque" + ]; + name = "usque-warp"; + path = ./usque-warp.sh; + description = "Connect/disconnect to Cloudflare WARP via usque MASQUE tunnel"; + deps = [ + config.networking.usque.package + pkgs.bash + ]; + completion.zsh = builtins.readFile ./usque-warp.zsh; + inherit config; + }; +in { options.networking.usque = { enable = lib.mkEnableOption "Enables usque."; @@ -13,9 +31,13 @@ default = pkgs.usque; description = "The usque package to use."; }; - }; + } + // usqueWarp.options.networking.usque; - config = lib.mkIf config.networking.usque.enable { - home.packages = [ config.networking.usque.package ]; - }; + config = lib.mkMerge [ + (lib.mkIf config.networking.usque.enable { + home.packages = [ config.networking.usque.package ]; + }) + usqueWarp.config + ]; } diff --git a/modules/networking/usque/usque-warp.sh b/modules/networking/usque/usque-warp.sh new file mode 100644 index 0000000..9d32872 --- /dev/null +++ b/modules/networking/usque/usque-warp.sh @@ -0,0 +1,223 @@ +#!/usr/bin/env bash +set -euo pipefail +CONFIG_DIR="$HOME/.cache/usque" +CONFIG="$CONFIG_DIR/config.json" +RUNTIME_DIR="${XDG_RUNTIME_DIR:-/tmp}" +PID_FILE="$RUNTIME_DIR/usque-warp.pid" +STATE_FILE="$RUNTIME_DIR/usque-warp.state" +IFACE_FILE="$RUNTIME_DIR/usque-warp.iface" +LOG_FILE="$RUNTIME_DIR/usque-warp.log" + +list_tun_ifaces() { + ip -o link show 2>/dev/null | awk -F': ' '{print $2}' | grep -E '^tun[0-9]+$' || true +} + +detect_iface() { + if [[ -f "$IFACE_FILE" ]]; then + cat "$IFACE_FILE" + return + fi + list_tun_ifaces | head -n1 +} + +is_running() { + [[ -f "$PID_FILE" ]] || return 1 + local pid + pid=$(cat "$PID_FILE" 2>/dev/null || true) + [[ -n "$pid" ]] || return 1 + sudo kill -0 "$pid" 2>/dev/null +} + +ensure_config() { + echo "Creating $CONFIG_DIR..." + mkdir -p "$CONFIG_DIR" + echo "Registering Cloudflare WARP account..." + usque -c "$CONFIG" register < <(yes) + if [[ ! -f "$CONFIG" ]]; then + echo "Failed to create config file: $CONFIG" + exit 1 + fi + echo "Config created successfully." +} + +remove_tun_default_routes() { + local dev="$1" + while ip route show | grep -qE "^default .*dev $dev"; do + ROUTE=$(ip route show | grep -E "^default .*dev $dev" | head -n1) + echo "Removing route: $ROUTE" + sudo ip route del "$ROUTE" || break + done +} + +connect() { + sudo -v + ensure_config + if [[ -f "$PID_FILE" ]]; then + OLD_PID=$(cat "$PID_FILE") + if sudo kill -0 "$OLD_PID" 2>/dev/null; then + echo "usque-warp is already running (PID $OLD_PID)" + exit 1 + else + echo "Removing stale PID file..." + rm -f "$PID_FILE" + fi + fi + + echo "Saving current default route..." + DEFAULT_ROUTE=$(ip route show default | grep -vE 'dev tun[0-9]+' | head -n1) + if [[ -z "$DEFAULT_ROUTE" ]]; then + echo "Could not determine current default route" + exit 1 + fi + echo "$DEFAULT_ROUTE" > "$STATE_FILE" + + echo "Recording pre-existing tun interfaces..." + BEFORE_IFACES=$(list_tun_ifaces) + + echo "Starting usque..." + sudo usque nativetun -c "$CONFIG" 2>&1 | sudo tee "$LOG_FILE" >/dev/null & + echo $! > "$PID_FILE" + + echo "Waiting for MASQUE connection..." + MASQUE_IP="" + for _ in {1..30}; do + MASQUE_IP=$(grep -oP 'MASQUE connection to \K[0-9.]+(?=:443)' "$LOG_FILE" 2>/dev/null || true) + if [[ -n "$MASQUE_IP" ]]; then + break + fi + sleep 1 + done + if [[ -z "$MASQUE_IP" ]]; then + echo "Failed to detect MASQUE endpoint" + sudo kill "$(cat "$PID_FILE")" 2>/dev/null || true + rm -f "$PID_FILE" + exit 1 + fi + + echo "Waiting for usque interface..." + TUN_DEV="" + for _ in {1..30}; do + AFTER_IFACES=$(list_tun_ifaces) + TUN_DEV=$(comm -13 <(echo "$BEFORE_IFACES" | sort) <(echo "$AFTER_IFACES" | sort) | head -n1) + [[ -n "$TUN_DEV" ]] && break + sleep 1 + done + if [[ -z "$TUN_DEV" ]]; then + echo "Failed to detect usque interface" + sudo kill "$(cat "$PID_FILE")" 2>/dev/null || true + rm -f "$PID_FILE" + exit 1 + fi + echo "$TUN_DEV" > "$IFACE_FILE" + echo "Detected interface: $TUN_DEV" + + GATEWAY=$(echo "$DEFAULT_ROUTE" | awk '{for(i=1;i<=NF;i++) if($i=="via") print $(i+1)}') + INTERFACE=$(echo "$DEFAULT_ROUTE" | awk '{for(i=1;i<=NF;i++) if($i=="dev") print $(i+1)}') + if [[ -z "$GATEWAY" || -z "$INTERFACE" ]]; then + echo "Cannot determine gateway/interface" + exit 1 + fi + echo "MASQUE_IP=$MASQUE_IP GATEWAY=$GATEWAY INTERFACE=$INTERFACE" >> "$STATE_FILE" + + echo "Allowing MASQUE endpoint outside tunnel..." + sudo ip route replace \ + "$MASQUE_IP" \ + via "$GATEWAY" \ + dev "$INTERFACE" + + echo "Removing old tun routes..." + remove_tun_default_routes "$TUN_DEV" + + echo "Switching default route to $TUN_DEV..." + sudo ip route add default dev "$TUN_DEV" metric 1 + echo "Connected" +} + +disconnect() { + sudo -v + echo "Disconnecting..." + local dev + if [[ -f "$IFACE_FILE" ]]; then + dev=$(cat "$IFACE_FILE") + else + dev=$(list_tun_ifaces | head -n1) + fi + + # Kill usque FIRST so the kernel tears down tun0 (and every route + # bound to it) as a single atomic operation, instead of us racing + # it by pulling routes out from under a device that's still up. + if [[ -f "$PID_FILE" ]]; then + PID=$(cat "$PID_FILE") + if sudo kill -0 "$PID" 2>/dev/null; then + echo "Stopping usque..." + sudo kill "$PID" 2>/dev/null || true + for _ in {1..20}; do + sudo kill -0 "$PID" 2>/dev/null || break + sleep 0.2 + done + fi + rm -f "$PID_FILE" + fi + + # Belt-and-braces: if the interface (or any of its routes) somehow + # survived, clean them up explicitly. No-ops if tun0 is already gone. + if [[ -n "${dev:-}" ]]; then + echo "Flushing $dev routes..." + sudo ip route flush dev "$dev" 2>/dev/null || true + remove_tun_default_routes "$dev" + fi + + if [[ -f "$STATE_FILE" ]]; then + MASQUE_IP=$(grep -oP 'MASQUE_IP=\K[0-9.]+' "$STATE_FILE" || true) + if [[ -n "$MASQUE_IP" ]]; then + echo "Removing MASQUE route: $MASQUE_IP" + sudo ip route del "$MASQUE_IP" 2>/dev/null || true + fi + + # Explicitly restore the pre-connect default route rather than + # assuming it's still intact. 'replace' is idempotent. + ORIGINAL_DEFAULT=$(head -n1 "$STATE_FILE") + if [[ "$ORIGINAL_DEFAULT" == default* ]]; then + echo "Restoring original default route..." + sudo ip route replace "$ORIGINAL_DEFAULT" \ + || echo "Warning: could not restore original default route" + fi + + rm -f "$STATE_FILE" + fi + + rm -f "$IFACE_FILE" + echo "Disconnected" +} + +status() { + local iface running=false + iface=$(detect_iface) + is_running && running=true + + if [[ -n "$iface" && "$running" == true ]]; then + printf '{"text":"%s","tooltip":"WARP connected via %s","class":"connected"}\n' "$iface" "$iface" + elif [[ -n "$iface" ]]; then + printf '{"text":"%s","tooltip":"Interface %s up, but usque-warp process not tracked","class":"connected"}\n' "$iface" "$iface" + elif [[ "$running" == true ]]; then + printf '{"text":"connecting","tooltip":"usque starting...","class":"connecting"}\n' + else + printf '{"text":"","tooltip":"WARP disconnected","class":"disconnected"}\n' + fi +} + +case "${1:-}" in + connect) + connect + ;; + disconnect) + disconnect + ;; + status) + status + ;; + *) + echo "Usage: $0 {connect|disconnect|status}" + exit 1 + ;; +esac diff --git a/modules/networking/usque/usque-warp.zsh b/modules/networking/usque/usque-warp.zsh new file mode 100644 index 0000000..fbb340e --- /dev/null +++ b/modules/networking/usque/usque-warp.zsh @@ -0,0 +1,17 @@ +#compdef usque-warp + +_arguments \ + '1: :->cmds' \ + '*::arg:->args' + +case $state in + cmds) + local -a commands + commands=( + 'connect:Connect to WARP' + 'disconnect:Disconnect from WARP' + 'status:Show connection status' + ) + _describe 'command' commands + ;; +esac diff --git a/modules/scripts/deep-clean/default.nix b/modules/scripts/deep-clean/default.nix deleted file mode 100644 index 56f6aa0..0000000 --- a/modules/scripts/deep-clean/default.nix +++ /dev/null @@ -1,22 +0,0 @@ -{ - config, - lib, - pkgs, - ... -}: -let - inherit ((import ../lib.nix { inherit lib pkgs; })) mkScriptModule; - base = mkScriptModule { - name = "deep-clean"; - path = ./script.sh; - description = "Deep clean script for Nix systems\nRemoves old generations, garbage, and optimizes store\nWARNING: Do NOT run with sudo - run as normal user"; - deps = [ - pkgs.bash - ]; - inherit config; - }; -in -{ - options = base.options; - config = base.moduleConfig; -} diff --git a/modules/scripts/deep-clean/script.sh b/modules/scripts/deep-clean/script.sh deleted file mode 100644 index bb28182..0000000 --- a/modules/scripts/deep-clean/script.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -echo "--- Starting Nix Deep Clean (keeping only current generations) ---" - -if [[ $EUID -eq 0 ]]; then - echo "Error: Do not run this script with sudo." - echo "Run it as your normal user; it will ask for sudo when required." - exit 1 -fi - -echo "Removing old NixOS system generations..." -sudo nix-env -p /nix/var/nix/profiles/system --delete-generations +1 - -echo "Removing old Home Manager generations..." -if command -v home-manager &> /dev/null; then - home-manager generations | \ - awk 'NR>1 {print $1}' | \ - xargs -r home-manager remove-generations -fi - -echo "Removing old user profile generations..." -nix-env --delete-generations +1 - -echo "Removing result symlinks..." -find . -name "result" -type l -delete - -echo "Running garbage collector..." -sudo nix-collect-garbage -d -nix-collect-garbage -d - -echo "Optimising Nix store..." -nix store optimise - -echo "--- Cleanup Complete ---" diff --git a/modules/scripts/default.nix b/modules/scripts/default.nix deleted file mode 100644 index ae18329..0000000 --- a/modules/scripts/default.nix +++ /dev/null @@ -1,34 +0,0 @@ -{ - config, - lib, - ... -}: -{ - imports = [ - ./deep-clean - ./gopass-ssh-load - ./gpg-backup - ./nix-update - ./neomutt-sync - ./screen-recording - ./system-monitor - ./usque-warp - ./yazi-file-chooser - ]; - - config = { - home.packages = lib.filter (x: x != null) (lib.mapAttrsToList (_: s: s.package) config.scripts); - - xdg.desktopEntries = lib.mapAttrs' ( - name: sc: - lib.nameValuePair name { - name = sc.desktop.displayName; - exec = "${lib.getExe config.terminal.kitty.package} --class ${name} -e ${lib.getExe sc.package}"; - icon = sc.desktop.icon; - categories = [ "Utility" ]; - terminal = false; - type = "Application"; - } - ) (lib.filterAttrs (_: sc: sc.enable && sc.desktop.enable) config.scripts); - }; -} diff --git a/modules/scripts/gopass-ssh-load/default.nix b/modules/scripts/gopass-ssh-load/default.nix deleted file mode 100644 index 764f704..0000000 --- a/modules/scripts/gopass-ssh-load/default.nix +++ /dev/null @@ -1,32 +0,0 @@ -{ - config, - lib, - pkgs, - ... -}: -let - inherit ((import ../lib.nix { inherit lib pkgs; })) mkScriptModule; - - gopassKeys = config.security.ssh.gopassKeys; - - base = mkScriptModule { - name = "gopass-ssh-load"; - path = ./script.sh; - description = "Load SSH keys from gopass password store"; - env = { - GNUPGHOME = config.home.sessionVariables.GNUPGHOME; - GOPASS_SSH_KEYS = lib.concatStringsSep " " gopassKeys; - }; - deps = with pkgs; [ - config.security.gopass.package - gnupg - openssh - bash - ]; - inherit config; - }; -in -{ - options = base.options; - config = base.moduleConfig; -} diff --git a/modules/scripts/gopass-ssh-load/script.sh b/modules/scripts/gopass-ssh-load/script.sh deleted file mode 100644 index c6b9a29..0000000 --- a/modules/scripts/gopass-ssh-load/script.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash -set -o errexit -set -o nounset -set -o pipefail - -# Load SSH keys from gopass password store -export GNUPGHOME="${GNUPGHOME:-$HOME/.local/share/gnupg}" - -SSH_AUTH_SOCK="$(gpgconf --list-dirs agent-ssh-socket)" -export SSH_AUTH_SOCK -if [ -z "$SSH_AUTH_SOCK" ] || [ ! -S "$SSH_AUTH_SOCK" ]; then - echo "Error: SSH_AUTH_SOCK is not set or valid." >&2 - exit 1 -fi - -if ssh-add -l 2>/dev/null | grep -qE "(ED25519|RSA|ECDSA)"; then - exit 0 -fi - -# GOPASS_SSH_KEYS holds a space-separated list of gopass entry names under ssh -if [ -z "${GOPASS_SSH_KEYS:-}" ]; then - echo "Error: GOPASS_SSH_KEYS is not set. Example: GOPASS_SSH_KEYS=\"github gitlab\"" >&2 - exit 1 -fi - -# shellcheck disable=SC2086 -read -r -a keys <<< "$GOPASS_SSH_KEYS" - -for key in "${keys[@]}"; do - if gopass cat "ssh/$key" > /dev/null 2>&1; then - tmpdir=$(mktemp -d) - keyfile="$tmpdir/key" - gopass cat "ssh/$key" > "$keyfile" 2>/dev/null - chmod 600 "$keyfile" - - passphrase=$(gopass cat "ssh/$key/passphrase" 2>/dev/null || true) - if [ -n "$passphrase" ]; then - ssh-keygen -p -P "$passphrase" -N "" -f "$keyfile" 2>/dev/null - fi - - ssh-add "$keyfile" 2>/dev/null - rm -rf "$tmpdir" - else - echo "Warning: no gopass entry ssh/$key" >&2 - fi -done diff --git a/modules/scripts/gpg-backup/completion.zsh b/modules/scripts/gpg-backup/completion.zsh deleted file mode 100644 index f47bbe8..0000000 --- a/modules/scripts/gpg-backup/completion.zsh +++ /dev/null @@ -1,23 +0,0 @@ -#compdef gpg-backup - -_arguments \ - '1: :->cmds' \ - '*::arg:->args' - -case $state in - cmds) - local -a commands - commands=( - 'export:Export all GPG keys to a passphrase-protected file' - 'import:Import GPG keys from a backup file' - ) - _describe 'command' commands - ;; - args) - case $words[1] in - export|import) - _files - ;; - esac - ;; -esac diff --git a/modules/scripts/gpg-backup/default.nix b/modules/scripts/gpg-backup/default.nix deleted file mode 100644 index b97aad8..0000000 --- a/modules/scripts/gpg-backup/default.nix +++ /dev/null @@ -1,27 +0,0 @@ -{ - config, - lib, - pkgs, - ... -}: -let - inherit ((import ../lib.nix { inherit lib pkgs; })) mkScriptModule; - base = mkScriptModule { - name = "gpg-backup"; - path = ./script.sh; - description = "Export/import all GPG keys as a single passphrase-protected file with maximum S2K iteration count\nUsage: gpg-backup export filename.gpg | gpg-backup import filename.gpg"; - deps = [ - pkgs.bash - pkgs.gnupg - pkgs.gnutar - pkgs.coreutils - pkgs.findutils - ]; - completion.zsh = builtins.readFile ./completion.zsh; - inherit config; - }; -in -{ - options = base.options; - config = base.moduleConfig; -} diff --git a/modules/scripts/gpg-backup/script.sh b/modules/scripts/gpg-backup/script.sh deleted file mode 100644 index 57c216b..0000000 --- a/modules/scripts/gpg-backup/script.sh +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -WORKDIR="$(mktemp -d)" - -cleanup() { - if [[ -d "$WORKDIR" ]]; then - find "$WORKDIR" -type f -exec shred -u -z {} \; 2>/dev/null || true - rm -rf "$WORKDIR" - fi -} -trap cleanup EXIT - -usage() { - echo "Usage:" - echo " $0 export Export all GPG keys to an encrypted file" - echo " $0 import Decrypt and import keys from a backup file" - exit 1 -} - -do_export() { - local outfile="$1" - - if [[ -e "$outfile" ]]; then - echo "!! Refusing to overwrite existing file: $outfile" >&2 - exit 1 - fi - - echo "==> Exporting public keys..." - gpg --export --armor > "$WORKDIR/public-keys.asc" - - echo "==> Exporting secret keys..." - gpg --export-secret-keys --armor > "$WORKDIR/secret-keys.asc" - - echo "==> Exporting secret subkeys (if any)..." - gpg --export-secret-subkeys --armor > "$WORKDIR/secret-subkeys.asc" || true - - echo "==> Exporting owner trust database..." - gpg --export-ownertrust > "$WORKDIR/ownertrust.txt" - - echo "==> Exporting revocation certificates..." - mkdir -p "$WORKDIR/revocation-certs" - if [[ -d "$HOME/.gnupg/openpgp-revocs.d" ]]; then - cp "$HOME"/.gnupg/openpgp-revocs.d/*.rev "$WORKDIR/revocation-certs/" 2>/dev/null || true - fi - - echo "==> Bundling everything into a single archive..." - tar -C "$WORKDIR" -cf "$WORKDIR/gpg-full-backup.tar" \ - public-keys.asc \ - secret-keys.asc \ - secret-subkeys.asc \ - ownertrust.txt \ - revocation-certs - - echo "==> Encrypting with GPG (AES256, SHA512, max S2K iteration count)..." - echo " You will be prompted for a passphrase — use a strong one." - gpg --symmetric \ - --cipher-algo AES256 \ - --digest-algo SHA512 \ - --s2k-mode 3 \ - --s2k-digest-algo SHA512 \ - --s2k-count 65011712 \ - --output "$outfile" \ - "$WORKDIR/gpg-full-backup.tar" - - echo "==> Verifying: attempting decryption to confirm it works..." - if gpg --decrypt "$outfile" > "$WORKDIR/verify.tar" 2>/dev/null; then - if cmp -s "$WORKDIR/gpg-full-backup.tar" "$WORKDIR/verify.tar"; then - echo "==> Verification succeeded: backup decrypts correctly." - else - echo "!! WARNING: decrypted content does not match original. Investigate before trusting this backup." >&2 - exit 1 - fi - else - echo "!! WARNING: decryption test failed." >&2 - exit 1 - fi - - echo - echo "==> Done." - echo " Encrypted backup: $outfile" - echo " Store this file somewhere safe (offline media, encrypted drive)." - echo " The S2K iteration count only helps if your passphrase itself" - echo " has real entropy (e.g. a long diceware passphrase)." -} - -do_import() { - local infile="$1" - - if [[ ! -f "$infile" ]]; then - echo "!! File not found: $infile" >&2 - exit 1 - fi - - echo "==> Decrypting $infile ..." - echo " You will be prompted for the backup's passphrase." - echo " Note: this may take a while due to the high S2K iteration count." - gpg --decrypt "$infile" > "$WORKDIR/gpg-full-backup.tar" - - echo "==> Extracting archive..." - tar -C "$WORKDIR" -xf "$WORKDIR/gpg-full-backup.tar" - - echo "==> Importing public keys..." - gpg --import "$WORKDIR/public-keys.asc" - - echo "==> Importing secret keys..." - gpg --import "$WORKDIR/secret-keys.asc" - - if [[ -s "$WORKDIR/secret-subkeys.asc" ]]; then - echo "==> Importing secret subkeys..." - gpg --import "$WORKDIR/secret-subkeys.asc" || true - fi - - if [[ -f "$WORKDIR/ownertrust.txt" ]]; then - echo "==> Importing owner trust database..." - gpg --import-ownertrust "$WORKDIR/ownertrust.txt" - fi - - if [[ -d "$WORKDIR/revocation-certs" ]] && [[ -n "$(ls -A "$WORKDIR/revocation-certs" 2>/dev/null)" ]]; then - echo "==> Restoring revocation certificates..." - mkdir -p "$HOME/.gnupg/openpgp-revocs.d" - cp "$WORKDIR"/revocation-certs/*.rev "$HOME/.gnupg/openpgp-revocs.d/" 2>/dev/null || true - fi - - echo - echo "==> Done. Keys imported into your GPG keyring." - echo " Run 'gpg --list-secret-keys' to confirm." -} - -# Main - -if [[ $# -ne 2 ]]; then - usage -fi - -command="$1" -filename="$2" - -case "$command" in - export) - do_export "$filename" - ;; - import) - do_import "$filename" - ;; - *) - usage - ;; -esac diff --git a/modules/scripts/lib.nix b/modules/scripts/lib.nix deleted file mode 100644 index 31d7e5b..0000000 --- a/modules/scripts/lib.nix +++ /dev/null @@ -1,137 +0,0 @@ -{ lib, pkgs }: -let - shellArgs = { - zsh = name: file: "--zsh --name _${name} ${file}"; - bash = name: file: "--bash --name ${name}.bash ${file}"; - }; - - mkScript = - name: path: description: env: deps: completion: - let - descLines = lib.filter (s: s != "") (lib.splitString "\n" description); - descComment = - if descLines == [ ] then "" else lib.concatMapStringsSep "\n" (line: "# ${line}") descLines + "\n"; - envVars = lib.concatStringsSep "\n" ( - lib.mapAttrsToList (n: v: "export ${n}=${lib.escapeShellArg (toString v)}") env - ); - base = pkgs.writeShellApplication { - inherit name; - runtimeInputs = deps; - text = '' - ${descComment}${envVars} - ${builtins.readFile path} - ''; - }; - completionDrv = - pkgs.runCommand "${name}-completion" - { - nativeBuildInputs = [ pkgs.installShellFiles ]; - } - ( - "mkdir -p $out\n" - + lib.concatStringsSep "\n" ( - lib.mapAttrsToList (shell: content: '' - installShellCompletion ${shellArgs.${shell} name (pkgs.writeText "_${name}.${shell}" content)} - '') completion - ) - ); - in - if completion == { } then - base - else - pkgs.symlinkJoin { - name = name; - paths = [ - base - completionDrv - ]; - meta = base.meta or { }; - }; - mkScriptModule = - { - name, - path, - env ? { }, - deps ? [ ], - desktop ? null, - extraLinks ? [ ], - config, - description ? "", - completion ? { }, - }: - let - scriptDrv = mkScript name path description env deps completion; - in - { - options.scripts.${name} = { - enable = lib.mkOption { - type = lib.types.bool; - default = true; - description = "Whether to enable the ${name} script."; - }; - package = lib.mkOption { - type = lib.types.nullOr lib.types.package; - readOnly = true; - description = "The derivation for the ${name} script."; - }; - completion = { - enable = lib.mkOption { - type = lib.types.bool; - default = completion != { }; - description = "Whether to install completions for the ${name} script."; - }; - }; - desktop = { - enable = lib.mkOption { - type = lib.types.bool; - default = false; - description = "Whether to create a .desktop entry for this script."; - }; - displayName = lib.mkOption { - type = lib.types.str; - default = name; - description = "Display name for the .desktop entry."; - }; - icon = lib.mkOption { - type = lib.types.str; - default = "kitty"; - description = "Icon for the .desktop entry."; - }; - }; - }; - moduleConfig = { - scripts.${name} = lib.mkIf config.scripts.${name}.enable ( - { - package = scriptDrv; - } - // lib.optionalAttrs (desktop != null) { inherit desktop; } - ); - home.file = lib.mkIf config.scripts.${name}.enable ( - builtins.listToAttrs ( - map (linkPath: { - name = lib.removePrefix "~/" linkPath; - value = { - source = "${scriptDrv}/bin/${name}"; - }; - }) extraLinks - ) - ); - assertions = [ - { - assertion = - !config.scripts.${name}.enable - || !config.scripts.${name}.desktop.enable - || config.terminal.kitty.enable; - message = "scripts.${name}.desktop.enable requires terminal.kitty.enable because desktop entries launch scripts in kitty."; - } - { - assertion = !config.scripts.${name}.completion.enable || completion != { }; - message = "scripts.${name}.completion.enable is true but no completion text was provided to mkScriptModule."; - } - ]; - }; - }; -in -{ - inherit mkScript mkScriptModule; -} diff --git a/modules/scripts/neomutt-sync/assertions.nix b/modules/scripts/neomutt-sync/assertions.nix deleted file mode 100644 index 38f48e4..0000000 --- a/modules/scripts/neomutt-sync/assertions.nix +++ /dev/null @@ -1,7 +0,0 @@ -{ config, ... }: -[ - { - assertion = !config.scripts.neomutt-sync.enable || config.communication.neomutt.enable; - message = "scripts.neomutt-sync is enabled but requires `communication.neomutt.enable`."; - } -] diff --git a/modules/scripts/neomutt-sync/default.nix b/modules/scripts/neomutt-sync/default.nix deleted file mode 100644 index d990210..0000000 --- a/modules/scripts/neomutt-sync/default.nix +++ /dev/null @@ -1,31 +0,0 @@ -{ - config, - lib, - pkgs, - ... -}: -let - inherit ((import ../lib.nix { inherit lib pkgs; })) mkScriptModule; - base = mkScriptModule { - name = "neomutt-sync"; - path = ./script.sh; - description = "Neomutt-sync - Interactive mail sync with dialog progress bar"; - deps = [ - pkgs.bash - pkgs.dialog - pkgs.coreutils - pkgs.gawk - pkgs.gnused - pkgs.util-linux - config.programs.mbsync.package - config.programs.notmuch.package - ]; - inherit config; - }; -in -{ - options = base.options; - config = base.moduleConfig // { - assertions = base.moduleConfig.assertions ++ (import ./assertions.nix { inherit config lib; }); - }; -} diff --git a/modules/scripts/neomutt-sync/script.sh b/modules/scripts/neomutt-sync/script.sh deleted file mode 100644 index d0dac63..0000000 --- a/modules/scripts/neomutt-sync/script.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash -DIALOGRC=$(mktemp) -NOTMUCH_LOG=$(mktemp) -TITLE="[󰇮 SYNCING MAIL]" -export DIALOGRC -trap 'rm -f "$DIALOGRC" "$NOTMUCH_LOG"' EXIT # Clean up everything on exit - -cat << 'EOF' > "$DIALOGRC" -use_shadow = ON -use_colors = ON -screen_color = (WHITE,BLACK,OFF) -dialog_color = (WHITE,BLACK,ON) -title_color = (YELLOW,BLACK,ON) -gauge_color = (GREEN,BLACK,ON) -border_color = (CYAN,BLACK,ON) -border2_color = (BLUE,BLACK,ON) -shadow_color = (BLACK,BLACK,ON) - -# Active buttons: magenta emphasis, matching Base16 base0E -button_active_color = (MAGENTA,BLACK,ON) -button_key_active_color = (MAGENTA,BLACK,ON) -button_label_active_color = (MAGENTA,BLACK,ON) - -# Inactive buttons: cyan secondary emphasis, matching Base16 base0C -button_inactive_color = (CYAN,BLACK,OFF) -button_key_inactive_color = (CYAN,BLACK,OFF) -button_label_inactive_color = (CYAN,BLACK,OFF) -EOF - -# Run the sync and indexing progress bar -( - # mbsync phase - script -q -e -c "mbsync -a" /dev/null | awk -v RS='\r' ' - { - c_str = "" - b_str = "" - for(i=1; i<=NF; i++) { - if ($i == "C:") c_str = $(i+1) - if ($i == "B:") b_str = $(i+1) - } - if (b_str != "") { - split(b_str, arr, "/") - if (arr[2] > 0) { - percent = int((arr[1] / arr[2]) * 100) - printf "XXX\n%d\nSyncing | Channel: %s | Mailbox: %s\nXXX\n", percent, c_str, b_str - } - } - fflush() - }' - - # notmuch phase - printf "XXX\n100\nIndexing new mail with notmuch...\nXXX\n" - notmuch new > "$NOTMUCH_LOG" 2>&1 -) | dialog --title "$TITLE" --gauge "Initializing..." 8 80 0 - -# Format the captured notmuch output into a single clean line -CLEAN_OUT=$(tr '\n' ' ' < "$NOTMUCH_LOG" | sed 's/ */ /g') - -# Display final results and wait for user input -dialog --title "$TITLE" --msgbox "Done!\n$CLEAN_OUT" 8 80 - -clear diff --git a/modules/scripts/nix-update/completion.zsh b/modules/scripts/nix-update/completion.zsh deleted file mode 100644 index a9500e6..0000000 --- a/modules/scripts/nix-update/completion.zsh +++ /dev/null @@ -1,22 +0,0 @@ -#compdef nix-update - -_arguments \ - '1: :->target' \ - '*:: :->flags' - -case $state in - target) - local -a commands - commands=( - 'home-manager:Update and switch home-manager configuration' - 'nixos:Update and switch NixOS configuration' - 'both:Update and switch both configurations' - ) - _describe 'command' commands - ;; - flags) - _arguments \ - '--only-flake[Only run nix flake update, skip switch commands]' \ - '--only-switch[Only run switch commands, skip nix flake update]' - ;; -esac diff --git a/modules/scripts/nix-update/default.nix b/modules/scripts/nix-update/default.nix deleted file mode 100644 index 633ff3d..0000000 --- a/modules/scripts/nix-update/default.nix +++ /dev/null @@ -1,26 +0,0 @@ -{ - config, - lib, - pkgs, - ... -}: -let - inherit ((import ../lib.nix { inherit lib pkgs; })) mkScriptModule; - base = mkScriptModule { - name = "nix-update"; - path = ./script.sh; - description = "Update NixOS and/or Home Manager flake configuration\nRuns nix flake update and rebuilds the system"; - deps = [ - pkgs.git - pkgs.nix - config.programs.home-manager.package - pkgs.bash - ]; - completion.zsh = builtins.readFile ./completion.zsh; - inherit config; - }; -in -{ - options = base.options; - config = base.moduleConfig; -} diff --git a/modules/scripts/nix-update/script.sh b/modules/scripts/nix-update/script.sh deleted file mode 100644 index 0f1c0c1..0000000 --- a/modules/scripts/nix-update/script.sh +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -HM_DIR="$HOME/.config/home-manager" -NIXOS_DIR="/etc/nixos" -ONLY_FLAKE=false -ONLY_SWITCH=false -TARGET="" - -usage() { - echo "Usage: nix-update [--only-flake|--only-switch]" - echo "" - echo "Options:" - echo " home-manager Update and switch home-manager configuration" - echo " nixos Update and switch NixOS configuration" - echo " both Update and switch both configurations" - echo "" - echo "Flags:" - echo " --only-flake Only run nix flake update, skip switch commands" - echo " --only-switch Only run switch commands, skip nix flake update" - exit 1 -} - -parse_args() { - while [[ $# -gt 0 ]]; do - case "$1" in - home-manager | nixos | both) - if [[ -n "$TARGET" ]]; then - echo "Error: only one target allowed" - usage - fi - TARGET="$1" - shift - ;; - --only-flake) - ONLY_FLAKE=true - shift - ;; - --only-switch) - ONLY_SWITCH=true - shift - ;; - -h | --help) - usage - ;; - *) - echo "Error: unknown option '$1'" - usage - ;; - esac - done - - if [[ -z "$TARGET" ]]; then - echo "Error: no target specified" - usage - fi -} - -update_home_manager() { - echo "==> Updating home-manager..." - cd "$HM_DIR" - - echo " Staging modules/private..." - git add modules/private -f - - if [[ "$ONLY_SWITCH" == false ]]; then - echo " Running nix flake update..." - nix flake update - fi - - if [[ "$ONLY_FLAKE" == false ]]; then - echo " Running home-manager switch..." - home-manager switch --flake "$HM_DIR#arpit" - fi - - echo " Unstaging modules/private..." - for f in modules/private/*.nix; do - [[ "$f" == *.example.nix ]] || git rm --cached "$f" - done - - echo "==> home-manager update complete" -} - -update_nixos() { - echo "==> Updating NixOS..." - sudo -v - cd "$NIXOS_DIR" - - echo " Staging hardware-configuration.nix..." - git add hardware-configuration.nix -f - - if [[ "$ONLY_SWITCH" == false ]]; then - echo " Running nix flake update..." - nix flake update - fi - - if [[ "$ONLY_FLAKE" == false ]]; then - echo " Running nixos-rebuild switch..." - sudo nixos-rebuild switch - fi - - echo " Unstaging hardware-configuration.nix..." - git rm --cached hardware-configuration.nix - - echo "==> NixOS update complete" -} - -parse_args "$@" - -case "$TARGET" in - home-manager) - update_home_manager - ;; - nixos) - update_nixos - ;; - both) - update_nixos - update_home_manager - ;; -esac diff --git a/modules/scripts/screen-recording/assertions.nix b/modules/scripts/screen-recording/assertions.nix deleted file mode 100644 index f858623..0000000 --- a/modules/scripts/screen-recording/assertions.nix +++ /dev/null @@ -1,11 +0,0 @@ -{ config, ... }: -[ - { - assertion = !config.scripts.screen-recording.enable || config.media.slurp.enable; - message = "scripts.screen-recording is enabled but requires `media.slurp.enable`."; - } - { - assertion = !config.scripts.screen-recording.enable || config.media.wf-recorder.enable; - message = "scripts.screen-recording is enabled but requires `media.wf-recorder.enable`."; - } -] diff --git a/modules/scripts/screen-recording/completion.zsh b/modules/scripts/screen-recording/completion.zsh deleted file mode 100644 index 835ec43..0000000 --- a/modules/scripts/screen-recording/completion.zsh +++ /dev/null @@ -1,4 +0,0 @@ -#compdef screen-recording - -_arguments \ - '(-s --select)'{-s,--select}'[Launch slurp to select recording region]' diff --git a/modules/scripts/screen-recording/default.nix b/modules/scripts/screen-recording/default.nix deleted file mode 100644 index 6aa0f89..0000000 --- a/modules/scripts/screen-recording/default.nix +++ /dev/null @@ -1,35 +0,0 @@ -{ - config, - lib, - pkgs, - ... -}: -let - inherit ((import ../lib.nix { inherit lib pkgs; })) mkScriptModule; - base = mkScriptModule { - name = "screen-recording"; - path = ./script.sh; - description = "Screen recording script using wf-recorder and slurp for region selection\nRecords Wayland screen to MP4\nUsage: screen-recording.sh [-s|--select]\n -s, --select Launch slurp to select recording region"; - deps = [ - pkgs.bash - config.media.slurp.package - config.media.wf-recorder.package - ]; - completion.zsh = builtins.readFile ./completion.zsh; - desktop = { - enable = true; - displayName = "Screen Recording"; - icon = "obs"; - }; - inherit config; - }; -in -{ - options = base.options; - config = base.moduleConfig // { - assertions = base.moduleConfig.assertions ++ (import ./assertions.nix { inherit config lib; }); - home.file.".local/share/icons/hicolor/scalable/apps/obs.svg" = { - source = ../../../assets/icons/obs.svg; - }; - }; -} diff --git a/modules/scripts/screen-recording/script.sh b/modules/scripts/screen-recording/script.sh deleted file mode 100644 index 336c32e..0000000 --- a/modules/scripts/screen-recording/script.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env bash -OUT_DIR="$HOME/Videos/Recordings" -mkdir -p "$OUT_DIR" - -# Colors -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -RED='\033[0;31m' -NC='\033[0m' - -# Argument parsing -SELECT_MODE=false -while [[ "$#" -gt 0 ]]; do - case $1 in - -s|--select) SELECT_MODE=true ;; - esac - shift -done - -# Geometry selection -GEOMETRY="" - -if [ "$SELECT_MODE" = true ]; then - if command -v slurp >/dev/null 2>&1; then - echo -e "${YELLOW}Select a window or draw a box...${NC}" - GEOMETRY=$(slurp) - - if [ -z "$GEOMETRY" ]; then - echo "Selection cancelled. Exiting." - exit 1 - fi - else - echo -e "${RED}Error: 'slurp' not found.${NC} Recording full screen..." - sleep 1 - fi -fi - -# Start recording -FILENAME="$OUT_DIR/recording-$(date +%Y%m%d-%H%M%S).mp4" - -echo -e "${GREEN}Recording started!${NC}" -echo -e "Press ${YELLOW}'q'${NC} in this terminal to stop." -echo "File: $FILENAME" -echo "------------------------------------------------" - -if [ -n "$GEOMETRY" ]; then - wf-recorder -g "$GEOMETRY" -f "$FILENAME" -else - wf-recorder -f "$FILENAME" -fi - -echo -e "\n${GREEN}Done!${NC} Video saved to $FILENAME" diff --git a/modules/scripts/system-monitor/assertions.nix b/modules/scripts/system-monitor/assertions.nix deleted file mode 100644 index 0710fc3..0000000 --- a/modules/scripts/system-monitor/assertions.nix +++ /dev/null @@ -1,19 +0,0 @@ -{ config, ... }: -[ - { - assertion = !config.scripts.system-monitor.enable || config.system.bottom.enable; - message = "scripts.system-monitor is enabled but requires `system.bottom.enable`."; - } - { - assertion = !config.scripts.system-monitor.enable || config.system.nvtop.enable; - message = "scripts.system-monitor is enabled but requires `system.nvtop.enable`."; - } - { - assertion = !config.scripts.system-monitor.enable || config.terminal.tmux.enable; - message = "scripts.system-monitor is enabled but requires `terminal.tmux.enable`."; - } - { - assertion = !config.scripts.system-monitor.enable || config.terminal.kitty.enable; - message = "scripts.system-monitor is enabled but requires `terminal.kitty.enable`."; - } -] diff --git a/modules/scripts/system-monitor/default.nix b/modules/scripts/system-monitor/default.nix deleted file mode 100644 index 65c1074..0000000 --- a/modules/scripts/system-monitor/default.nix +++ /dev/null @@ -1,36 +0,0 @@ -{ - config, - lib, - pkgs, - ... -}: -let - inherit ((import ../lib.nix { inherit lib pkgs; })) mkScriptModule; - base = mkScriptModule { - name = "system-monitor"; - path = ./script.sh; - description = "System monitor script using tmux to run bottom and nvtop side-by-side\nOpens a tmux session with:\n - Left pane: bottom (system/process monitor)\n - Right pane: nvtop (GPU monitor)"; - deps = [ - config.system.bottom.package - config.system.nvtop.package - config.terminal.tmux.package - config.terminal.kitty.package - pkgs.bash - ]; - desktop = { - enable = true; - displayName = "System Monitor"; - icon = "bottom-system-monitor"; - }; - inherit config; - }; -in -{ - options = base.options; - config = base.moduleConfig // { - assertions = base.moduleConfig.assertions ++ (import ./assertions.nix { inherit config lib; }); - home.file.".local/share/icons/hicolor/scalable/apps/bottom-system-monitor.svg" = { - source = ../../../assets/icons/bottom-system-monitor.svg; - }; - }; -} diff --git a/modules/scripts/system-monitor/script.sh b/modules/scripts/system-monitor/script.sh deleted file mode 100644 index 7a9e7ea..0000000 --- a/modules/scripts/system-monitor/script.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash -SESSION="system-monitor" - -kitty @ set-font-size -- -5 - -cleanup() { - kitty @ set-font-size -- +5 -} -trap cleanup EXIT INT TERM - -# Kill any existing session -tmux kill-session -t "$SESSION" 2>/dev/null - -# Start new tmux session with btm wrapped to kill session on exit -tmux new-session -d -s "$SESSION" -c "$HOME" "bash -c 'btm; tmux kill-session -t $SESSION'" - -# Split horizontally: right pane runs nvtop wrapped similarly -tmux split-window -h -t "$SESSION:0" -c "$HOME" "bash -c 'nvtop; tmux kill-session -t $SESSION'" - -tmux set -g status off -tmux set -g mouse on -tmux set -g focus-events on - -# Select main pane and attach -tmux select-pane -t "$SESSION:0.0" -tmux attach-session -t "$SESSION" diff --git a/modules/scripts/usque-warp/completion.zsh b/modules/scripts/usque-warp/completion.zsh deleted file mode 100644 index fbb340e..0000000 --- a/modules/scripts/usque-warp/completion.zsh +++ /dev/null @@ -1,17 +0,0 @@ -#compdef usque-warp - -_arguments \ - '1: :->cmds' \ - '*::arg:->args' - -case $state in - cmds) - local -a commands - commands=( - 'connect:Connect to WARP' - 'disconnect:Disconnect from WARP' - 'status:Show connection status' - ) - _describe 'command' commands - ;; -esac diff --git a/modules/scripts/usque-warp/default.nix b/modules/scripts/usque-warp/default.nix deleted file mode 100644 index 1831a44..0000000 --- a/modules/scripts/usque-warp/default.nix +++ /dev/null @@ -1,24 +0,0 @@ -{ - config, - lib, - pkgs, - ... -}: -let - inherit ((import ../lib.nix { inherit lib pkgs; })) mkScriptModule; - base = mkScriptModule { - name = "usque-warp"; - path = ./script.sh; - description = "Connect/disconnect to Cloudflare WARP via usque MASQUE tunnel"; - deps = [ - config.networking.usque.package - pkgs.bash - ]; - completion.zsh = builtins.readFile ./completion.zsh; - inherit config; - }; -in -{ - options = base.options; - config = base.moduleConfig; -} diff --git a/modules/scripts/usque-warp/script.sh b/modules/scripts/usque-warp/script.sh deleted file mode 100644 index 9d32872..0000000 --- a/modules/scripts/usque-warp/script.sh +++ /dev/null @@ -1,223 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -CONFIG_DIR="$HOME/.cache/usque" -CONFIG="$CONFIG_DIR/config.json" -RUNTIME_DIR="${XDG_RUNTIME_DIR:-/tmp}" -PID_FILE="$RUNTIME_DIR/usque-warp.pid" -STATE_FILE="$RUNTIME_DIR/usque-warp.state" -IFACE_FILE="$RUNTIME_DIR/usque-warp.iface" -LOG_FILE="$RUNTIME_DIR/usque-warp.log" - -list_tun_ifaces() { - ip -o link show 2>/dev/null | awk -F': ' '{print $2}' | grep -E '^tun[0-9]+$' || true -} - -detect_iface() { - if [[ -f "$IFACE_FILE" ]]; then - cat "$IFACE_FILE" - return - fi - list_tun_ifaces | head -n1 -} - -is_running() { - [[ -f "$PID_FILE" ]] || return 1 - local pid - pid=$(cat "$PID_FILE" 2>/dev/null || true) - [[ -n "$pid" ]] || return 1 - sudo kill -0 "$pid" 2>/dev/null -} - -ensure_config() { - echo "Creating $CONFIG_DIR..." - mkdir -p "$CONFIG_DIR" - echo "Registering Cloudflare WARP account..." - usque -c "$CONFIG" register < <(yes) - if [[ ! -f "$CONFIG" ]]; then - echo "Failed to create config file: $CONFIG" - exit 1 - fi - echo "Config created successfully." -} - -remove_tun_default_routes() { - local dev="$1" - while ip route show | grep -qE "^default .*dev $dev"; do - ROUTE=$(ip route show | grep -E "^default .*dev $dev" | head -n1) - echo "Removing route: $ROUTE" - sudo ip route del "$ROUTE" || break - done -} - -connect() { - sudo -v - ensure_config - if [[ -f "$PID_FILE" ]]; then - OLD_PID=$(cat "$PID_FILE") - if sudo kill -0 "$OLD_PID" 2>/dev/null; then - echo "usque-warp is already running (PID $OLD_PID)" - exit 1 - else - echo "Removing stale PID file..." - rm -f "$PID_FILE" - fi - fi - - echo "Saving current default route..." - DEFAULT_ROUTE=$(ip route show default | grep -vE 'dev tun[0-9]+' | head -n1) - if [[ -z "$DEFAULT_ROUTE" ]]; then - echo "Could not determine current default route" - exit 1 - fi - echo "$DEFAULT_ROUTE" > "$STATE_FILE" - - echo "Recording pre-existing tun interfaces..." - BEFORE_IFACES=$(list_tun_ifaces) - - echo "Starting usque..." - sudo usque nativetun -c "$CONFIG" 2>&1 | sudo tee "$LOG_FILE" >/dev/null & - echo $! > "$PID_FILE" - - echo "Waiting for MASQUE connection..." - MASQUE_IP="" - for _ in {1..30}; do - MASQUE_IP=$(grep -oP 'MASQUE connection to \K[0-9.]+(?=:443)' "$LOG_FILE" 2>/dev/null || true) - if [[ -n "$MASQUE_IP" ]]; then - break - fi - sleep 1 - done - if [[ -z "$MASQUE_IP" ]]; then - echo "Failed to detect MASQUE endpoint" - sudo kill "$(cat "$PID_FILE")" 2>/dev/null || true - rm -f "$PID_FILE" - exit 1 - fi - - echo "Waiting for usque interface..." - TUN_DEV="" - for _ in {1..30}; do - AFTER_IFACES=$(list_tun_ifaces) - TUN_DEV=$(comm -13 <(echo "$BEFORE_IFACES" | sort) <(echo "$AFTER_IFACES" | sort) | head -n1) - [[ -n "$TUN_DEV" ]] && break - sleep 1 - done - if [[ -z "$TUN_DEV" ]]; then - echo "Failed to detect usque interface" - sudo kill "$(cat "$PID_FILE")" 2>/dev/null || true - rm -f "$PID_FILE" - exit 1 - fi - echo "$TUN_DEV" > "$IFACE_FILE" - echo "Detected interface: $TUN_DEV" - - GATEWAY=$(echo "$DEFAULT_ROUTE" | awk '{for(i=1;i<=NF;i++) if($i=="via") print $(i+1)}') - INTERFACE=$(echo "$DEFAULT_ROUTE" | awk '{for(i=1;i<=NF;i++) if($i=="dev") print $(i+1)}') - if [[ -z "$GATEWAY" || -z "$INTERFACE" ]]; then - echo "Cannot determine gateway/interface" - exit 1 - fi - echo "MASQUE_IP=$MASQUE_IP GATEWAY=$GATEWAY INTERFACE=$INTERFACE" >> "$STATE_FILE" - - echo "Allowing MASQUE endpoint outside tunnel..." - sudo ip route replace \ - "$MASQUE_IP" \ - via "$GATEWAY" \ - dev "$INTERFACE" - - echo "Removing old tun routes..." - remove_tun_default_routes "$TUN_DEV" - - echo "Switching default route to $TUN_DEV..." - sudo ip route add default dev "$TUN_DEV" metric 1 - echo "Connected" -} - -disconnect() { - sudo -v - echo "Disconnecting..." - local dev - if [[ -f "$IFACE_FILE" ]]; then - dev=$(cat "$IFACE_FILE") - else - dev=$(list_tun_ifaces | head -n1) - fi - - # Kill usque FIRST so the kernel tears down tun0 (and every route - # bound to it) as a single atomic operation, instead of us racing - # it by pulling routes out from under a device that's still up. - if [[ -f "$PID_FILE" ]]; then - PID=$(cat "$PID_FILE") - if sudo kill -0 "$PID" 2>/dev/null; then - echo "Stopping usque..." - sudo kill "$PID" 2>/dev/null || true - for _ in {1..20}; do - sudo kill -0 "$PID" 2>/dev/null || break - sleep 0.2 - done - fi - rm -f "$PID_FILE" - fi - - # Belt-and-braces: if the interface (or any of its routes) somehow - # survived, clean them up explicitly. No-ops if tun0 is already gone. - if [[ -n "${dev:-}" ]]; then - echo "Flushing $dev routes..." - sudo ip route flush dev "$dev" 2>/dev/null || true - remove_tun_default_routes "$dev" - fi - - if [[ -f "$STATE_FILE" ]]; then - MASQUE_IP=$(grep -oP 'MASQUE_IP=\K[0-9.]+' "$STATE_FILE" || true) - if [[ -n "$MASQUE_IP" ]]; then - echo "Removing MASQUE route: $MASQUE_IP" - sudo ip route del "$MASQUE_IP" 2>/dev/null || true - fi - - # Explicitly restore the pre-connect default route rather than - # assuming it's still intact. 'replace' is idempotent. - ORIGINAL_DEFAULT=$(head -n1 "$STATE_FILE") - if [[ "$ORIGINAL_DEFAULT" == default* ]]; then - echo "Restoring original default route..." - sudo ip route replace "$ORIGINAL_DEFAULT" \ - || echo "Warning: could not restore original default route" - fi - - rm -f "$STATE_FILE" - fi - - rm -f "$IFACE_FILE" - echo "Disconnected" -} - -status() { - local iface running=false - iface=$(detect_iface) - is_running && running=true - - if [[ -n "$iface" && "$running" == true ]]; then - printf '{"text":"%s","tooltip":"WARP connected via %s","class":"connected"}\n' "$iface" "$iface" - elif [[ -n "$iface" ]]; then - printf '{"text":"%s","tooltip":"Interface %s up, but usque-warp process not tracked","class":"connected"}\n' "$iface" "$iface" - elif [[ "$running" == true ]]; then - printf '{"text":"connecting","tooltip":"usque starting...","class":"connecting"}\n' - else - printf '{"text":"","tooltip":"WARP disconnected","class":"disconnected"}\n' - fi -} - -case "${1:-}" in - connect) - connect - ;; - disconnect) - disconnect - ;; - status) - status - ;; - *) - echo "Usage: $0 {connect|disconnect|status}" - exit 1 - ;; -esac diff --git a/modules/scripts/yazi-file-chooser/default.nix b/modules/scripts/yazi-file-chooser/default.nix deleted file mode 100644 index 3e99aad..0000000 --- a/modules/scripts/yazi-file-chooser/default.nix +++ /dev/null @@ -1,25 +0,0 @@ -{ - config, - lib, - pkgs, - ... -}: -let - inherit ((import ../lib.nix { inherit lib pkgs; })) mkScriptModule; - base = mkScriptModule { - name = "yazi-file-chooser"; - path = ./script.sh; - description = "Yazi-based file chooser for XDG Desktop Portal"; - deps = [ - pkgs.bash - config.file-management.yazi.package - config.terminal.kitty.package - ]; - extraLinks = [ "~/.config/xdg-desktop-portal-termfilechooser/config" ]; - inherit config; - }; -in -{ - options = base.options; - config = base.moduleConfig; -} diff --git a/modules/scripts/yazi-file-chooser/script.sh b/modules/scripts/yazi-file-chooser/script.sh deleted file mode 100644 index 7978222..0000000 --- a/modules/scripts/yazi-file-chooser/script.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env bash -# $1 toggles multiple selection -# $2 toggles directory select mode -# $3 toggles save file mode -# $4 is the initial directory path -# $5 is the output path file - -# multiple="$1" -directory="$2" -# save="$3" -path="$4" -out="$5" - -# Pick directory vs single/multiple file -if [ "$directory" = "1" ]; then - # Outputs the final working directory path on exit - exec yazi "$path" --cwd-file="$out" -else - # Writes all selected file paths to the output file on exit - exec yazi "$path" --chooser-file="$out" -fi diff --git a/modules/security/gopass/default.nix b/modules/security/gopass/default.nix index d154226..4fefdd2 100644 --- a/modules/security/gopass/default.nix +++ b/modules/security/gopass/default.nix @@ -5,6 +5,32 @@ pkgs, ... }: +let + inherit ((import ../../lib/script.nix { inherit lib pkgs; })) mkScriptModule; + + gopassKeys = config.security.ssh.gopassKeys; + + gopassSshLoad = mkScriptModule { + scope = [ + "security" + "gopass" + ]; + name = "gopass-ssh-load"; + path = ./gopass-ssh-load.sh; + description = "Load SSH keys from gopass password store"; + env = { + GNUPGHOME = config.home.sessionVariables.GNUPGHOME; + GOPASS_SSH_KEYS = lib.concatStringsSep " " gopassKeys; + }; + deps = with pkgs; [ + config.security.gopass.package + gnupg + openssh + bash + ]; + inherit config; + }; +in { options.security.gopass = { enable = lib.mkEnableOption "Enables gopass."; @@ -15,33 +41,37 @@ description = "The gopass package to use."; }; ssh-agent.enable = lib.mkEnableOption "gopass-backed SSH keys for git"; - }; + } + // gopassSshLoad.options.security.gopass; - config = lib.mkIf config.security.gopass.enable { - programs.password-store = { - enable = true; - package = pkgs.gopass.override { passAlias = true; }; - settings = { - PASSWORD_STORE_DIR = "${config.home.homeDirectory}/.local/share/pass"; + config = lib.mkMerge [ + (lib.mkIf config.security.gopass.enable { + programs.password-store = { + enable = true; + package = pkgs.gopass.override { passAlias = true; }; + settings = { + PASSWORD_STORE_DIR = "${config.home.homeDirectory}/.local/share/pass"; + }; }; - }; - home.sessionVariables = { - PASSWORD_STORE_DIR = config.programs.password-store.settings.PASSWORD_STORE_DIR; - }; + home.sessionVariables = { + PASSWORD_STORE_DIR = config.programs.password-store.settings.PASSWORD_STORE_DIR; + }; - home.file.".local/share/icons/hicolor/scalable/apps/gopass.svg" = { - source = ../../../assets/icons/gopass.svg; - }; + home.file.".local/share/icons/hicolor/scalable/apps/gopass.svg" = { + source = ../../../assets/icons/gopass.svg; + }; - xdg.desktopEntries."gopass" = { - name = "gopass"; - exec = "${lib.getExe config.terminal.kitty.package} --class gopass -e ${lib.getExe config.security.gopass.package}"; - icon = "gopass"; - comment = "Standard Unix password manager (Go implementation)"; - categories = [ "Utility" ]; - terminal = false; - type = "Application"; - }; - }; + xdg.desktopEntries."gopass" = { + name = "gopass"; + exec = "${lib.getExe config.terminal.kitty.package} --class gopass -e ${lib.getExe config.security.gopass.package}"; + icon = "gopass"; + comment = "Standard Unix password manager (Go implementation)"; + categories = [ "Utility" ]; + terminal = false; + type = "Application"; + }; + }) + gopassSshLoad.config + ]; } diff --git a/modules/security/gopass/gopass-ssh-load.sh b/modules/security/gopass/gopass-ssh-load.sh new file mode 100644 index 0000000..c6b9a29 --- /dev/null +++ b/modules/security/gopass/gopass-ssh-load.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +set -o errexit +set -o nounset +set -o pipefail + +# Load SSH keys from gopass password store +export GNUPGHOME="${GNUPGHOME:-$HOME/.local/share/gnupg}" + +SSH_AUTH_SOCK="$(gpgconf --list-dirs agent-ssh-socket)" +export SSH_AUTH_SOCK +if [ -z "$SSH_AUTH_SOCK" ] || [ ! -S "$SSH_AUTH_SOCK" ]; then + echo "Error: SSH_AUTH_SOCK is not set or valid." >&2 + exit 1 +fi + +if ssh-add -l 2>/dev/null | grep -qE "(ED25519|RSA|ECDSA)"; then + exit 0 +fi + +# GOPASS_SSH_KEYS holds a space-separated list of gopass entry names under ssh +if [ -z "${GOPASS_SSH_KEYS:-}" ]; then + echo "Error: GOPASS_SSH_KEYS is not set. Example: GOPASS_SSH_KEYS=\"github gitlab\"" >&2 + exit 1 +fi + +# shellcheck disable=SC2086 +read -r -a keys <<< "$GOPASS_SSH_KEYS" + +for key in "${keys[@]}"; do + if gopass cat "ssh/$key" > /dev/null 2>&1; then + tmpdir=$(mktemp -d) + keyfile="$tmpdir/key" + gopass cat "ssh/$key" > "$keyfile" 2>/dev/null + chmod 600 "$keyfile" + + passphrase=$(gopass cat "ssh/$key/passphrase" 2>/dev/null || true) + if [ -n "$passphrase" ]; then + ssh-keygen -p -P "$passphrase" -N "" -f "$keyfile" 2>/dev/null + fi + + ssh-add "$keyfile" 2>/dev/null + rm -rf "$tmpdir" + else + echo "Warning: no gopass entry ssh/$key" >&2 + fi +done diff --git a/modules/security/gpg/default.nix b/modules/security/gpg/default.nix index 472270b..07d9d4b 100644 --- a/modules/security/gpg/default.nix +++ b/modules/security/gpg/default.nix @@ -5,6 +5,27 @@ pkgs, ... }: +let + inherit ((import ../../lib/script.nix { inherit lib pkgs; })) mkScriptModule; + gpgBackup = mkScriptModule { + scope = [ + "security" + "gpg" + ]; + name = "gpg-backup"; + path = ./gpg-backup.sh; + description = "Export/import all GPG keys as a single passphrase-protected file with maximum S2K iteration count\nUsage: gpg-backup export filename.gpg | gpg-backup import filename.gpg"; + deps = [ + pkgs.bash + pkgs.gnupg + pkgs.gnutar + pkgs.coreutils + pkgs.findutils + ]; + completion.zsh = builtins.readFile ./gpg-backup.zsh; + inherit config; + }; +in { options.security.gpg = { enable = lib.mkEnableOption "Enables gpg."; @@ -16,25 +37,29 @@ defaultText = lib.literalExpression "config.programs.gpg.package"; description = "The gpg package to use."; }; - }; + } + // gpgBackup.options.security.gpg; - config = lib.mkIf config.security.gpg.enable { - programs.gpg = { - enable = true; - homedir = "${config.xdg.dataHome}/gnupg"; - }; + config = lib.mkMerge [ + (lib.mkIf config.security.gpg.enable { + programs.gpg = { + enable = true; + homedir = "${config.xdg.dataHome}/gnupg"; + }; - home.sessionVariables = { - GNUPGHOME = config.programs.gpg.homedir; - }; + home.sessionVariables = { + GNUPGHOME = config.programs.gpg.homedir; + }; - services.gpg-agent = { - enable = true; - enableZshIntegration = true; - defaultCacheTtl = 3600; - maxCacheTtl = 86400; - enableSshSupport = config.security.ssh.enable; - pinentry.package = pkgs.pinentry-rofi; - }; - }; + services.gpg-agent = { + enable = true; + enableZshIntegration = true; + defaultCacheTtl = 3600; + maxCacheTtl = 86400; + enableSshSupport = config.security.ssh.enable; + pinentry.package = pkgs.pinentry-rofi; + }; + }) + gpgBackup.config + ]; } diff --git a/modules/security/gpg/gpg-backup.sh b/modules/security/gpg/gpg-backup.sh new file mode 100644 index 0000000..57c216b --- /dev/null +++ b/modules/security/gpg/gpg-backup.sh @@ -0,0 +1,149 @@ +#!/usr/bin/env bash +set -euo pipefail + +WORKDIR="$(mktemp -d)" + +cleanup() { + if [[ -d "$WORKDIR" ]]; then + find "$WORKDIR" -type f -exec shred -u -z {} \; 2>/dev/null || true + rm -rf "$WORKDIR" + fi +} +trap cleanup EXIT + +usage() { + echo "Usage:" + echo " $0 export Export all GPG keys to an encrypted file" + echo " $0 import Decrypt and import keys from a backup file" + exit 1 +} + +do_export() { + local outfile="$1" + + if [[ -e "$outfile" ]]; then + echo "!! Refusing to overwrite existing file: $outfile" >&2 + exit 1 + fi + + echo "==> Exporting public keys..." + gpg --export --armor > "$WORKDIR/public-keys.asc" + + echo "==> Exporting secret keys..." + gpg --export-secret-keys --armor > "$WORKDIR/secret-keys.asc" + + echo "==> Exporting secret subkeys (if any)..." + gpg --export-secret-subkeys --armor > "$WORKDIR/secret-subkeys.asc" || true + + echo "==> Exporting owner trust database..." + gpg --export-ownertrust > "$WORKDIR/ownertrust.txt" + + echo "==> Exporting revocation certificates..." + mkdir -p "$WORKDIR/revocation-certs" + if [[ -d "$HOME/.gnupg/openpgp-revocs.d" ]]; then + cp "$HOME"/.gnupg/openpgp-revocs.d/*.rev "$WORKDIR/revocation-certs/" 2>/dev/null || true + fi + + echo "==> Bundling everything into a single archive..." + tar -C "$WORKDIR" -cf "$WORKDIR/gpg-full-backup.tar" \ + public-keys.asc \ + secret-keys.asc \ + secret-subkeys.asc \ + ownertrust.txt \ + revocation-certs + + echo "==> Encrypting with GPG (AES256, SHA512, max S2K iteration count)..." + echo " You will be prompted for a passphrase — use a strong one." + gpg --symmetric \ + --cipher-algo AES256 \ + --digest-algo SHA512 \ + --s2k-mode 3 \ + --s2k-digest-algo SHA512 \ + --s2k-count 65011712 \ + --output "$outfile" \ + "$WORKDIR/gpg-full-backup.tar" + + echo "==> Verifying: attempting decryption to confirm it works..." + if gpg --decrypt "$outfile" > "$WORKDIR/verify.tar" 2>/dev/null; then + if cmp -s "$WORKDIR/gpg-full-backup.tar" "$WORKDIR/verify.tar"; then + echo "==> Verification succeeded: backup decrypts correctly." + else + echo "!! WARNING: decrypted content does not match original. Investigate before trusting this backup." >&2 + exit 1 + fi + else + echo "!! WARNING: decryption test failed." >&2 + exit 1 + fi + + echo + echo "==> Done." + echo " Encrypted backup: $outfile" + echo " Store this file somewhere safe (offline media, encrypted drive)." + echo " The S2K iteration count only helps if your passphrase itself" + echo " has real entropy (e.g. a long diceware passphrase)." +} + +do_import() { + local infile="$1" + + if [[ ! -f "$infile" ]]; then + echo "!! File not found: $infile" >&2 + exit 1 + fi + + echo "==> Decrypting $infile ..." + echo " You will be prompted for the backup's passphrase." + echo " Note: this may take a while due to the high S2K iteration count." + gpg --decrypt "$infile" > "$WORKDIR/gpg-full-backup.tar" + + echo "==> Extracting archive..." + tar -C "$WORKDIR" -xf "$WORKDIR/gpg-full-backup.tar" + + echo "==> Importing public keys..." + gpg --import "$WORKDIR/public-keys.asc" + + echo "==> Importing secret keys..." + gpg --import "$WORKDIR/secret-keys.asc" + + if [[ -s "$WORKDIR/secret-subkeys.asc" ]]; then + echo "==> Importing secret subkeys..." + gpg --import "$WORKDIR/secret-subkeys.asc" || true + fi + + if [[ -f "$WORKDIR/ownertrust.txt" ]]; then + echo "==> Importing owner trust database..." + gpg --import-ownertrust "$WORKDIR/ownertrust.txt" + fi + + if [[ -d "$WORKDIR/revocation-certs" ]] && [[ -n "$(ls -A "$WORKDIR/revocation-certs" 2>/dev/null)" ]]; then + echo "==> Restoring revocation certificates..." + mkdir -p "$HOME/.gnupg/openpgp-revocs.d" + cp "$WORKDIR"/revocation-certs/*.rev "$HOME/.gnupg/openpgp-revocs.d/" 2>/dev/null || true + fi + + echo + echo "==> Done. Keys imported into your GPG keyring." + echo " Run 'gpg --list-secret-keys' to confirm." +} + +# Main + +if [[ $# -ne 2 ]]; then + usage +fi + +command="$1" +filename="$2" + +case "$command" in + export) + do_export "$filename" + ;; + import) + do_import "$filename" + ;; + *) + usage + ;; +esac diff --git a/modules/security/gpg/gpg-backup.zsh b/modules/security/gpg/gpg-backup.zsh new file mode 100644 index 0000000..f47bbe8 --- /dev/null +++ b/modules/security/gpg/gpg-backup.zsh @@ -0,0 +1,23 @@ +#compdef gpg-backup + +_arguments \ + '1: :->cmds' \ + '*::arg:->args' + +case $state in + cmds) + local -a commands + commands=( + 'export:Export all GPG keys to a passphrase-protected file' + 'import:Import GPG keys from a backup file' + ) + _describe 'command' commands + ;; + args) + case $words[1] in + export|import) + _files + ;; + esac + ;; +esac diff --git a/modules/system/deep-clean/deep-clean.sh b/modules/system/deep-clean/deep-clean.sh new file mode 100644 index 0000000..bb28182 --- /dev/null +++ b/modules/system/deep-clean/deep-clean.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "--- Starting Nix Deep Clean (keeping only current generations) ---" + +if [[ $EUID -eq 0 ]]; then + echo "Error: Do not run this script with sudo." + echo "Run it as your normal user; it will ask for sudo when required." + exit 1 +fi + +echo "Removing old NixOS system generations..." +sudo nix-env -p /nix/var/nix/profiles/system --delete-generations +1 + +echo "Removing old Home Manager generations..." +if command -v home-manager &> /dev/null; then + home-manager generations | \ + awk 'NR>1 {print $1}' | \ + xargs -r home-manager remove-generations +fi + +echo "Removing old user profile generations..." +nix-env --delete-generations +1 + +echo "Removing result symlinks..." +find . -name "result" -type l -delete + +echo "Running garbage collector..." +sudo nix-collect-garbage -d +nix-collect-garbage -d + +echo "Optimising Nix store..." +nix store optimise + +echo "--- Cleanup Complete ---" diff --git a/modules/system/deep-clean/default.nix b/modules/system/deep-clean/default.nix new file mode 100644 index 0000000..af75382 --- /dev/null +++ b/modules/system/deep-clean/default.nix @@ -0,0 +1,18 @@ +# Deep clean script for Nix systems +{ + config, + lib, + pkgs, + ... +}: +let + inherit ((import ../../lib/script.nix { inherit lib pkgs; })) mkScriptModule; +in +mkScriptModule { + scope = [ "system" ]; + name = "deep-clean"; + path = ./deep-clean.sh; + description = "Deep clean script for Nix systems\nRemoves old generations, garbage, and optimizes store\nWARNING: Do NOT run with sudo - run as normal user"; + deps = [ pkgs.bash ]; + inherit config; +} diff --git a/modules/system/default.nix b/modules/system/default.nix index 591cd10..9bd3a01 100644 --- a/modules/system/default.nix +++ b/modules/system/default.nix @@ -4,8 +4,11 @@ imports = [ ./bottom ./brightnessctl + ./deep-clean ./htop + ./nix-update ./nvtop + ./system-monitor ./systemctl-tui ]; } diff --git a/modules/system/nix-update/default.nix b/modules/system/nix-update/default.nix new file mode 100644 index 0000000..13c15f6 --- /dev/null +++ b/modules/system/nix-update/default.nix @@ -0,0 +1,24 @@ +# Update NixOS and/or Home Manager flake configuration +{ + config, + lib, + pkgs, + ... +}: +let + inherit ((import ../../lib/script.nix { inherit lib pkgs; })) mkScriptModule; +in +mkScriptModule { + scope = [ "system" ]; + name = "nix-update"; + path = ./nix-update.sh; + description = "Update NixOS and/or Home Manager flake configuration\nRuns nix flake update and rebuilds the system"; + deps = [ + pkgs.git + pkgs.nix + config.programs.home-manager.package + pkgs.bash + ]; + completion.zsh = builtins.readFile ./nix-update.zsh; + inherit config; +} diff --git a/modules/system/nix-update/nix-update.sh b/modules/system/nix-update/nix-update.sh new file mode 100644 index 0000000..0f1c0c1 --- /dev/null +++ b/modules/system/nix-update/nix-update.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash + +set -euo pipefail + +HM_DIR="$HOME/.config/home-manager" +NIXOS_DIR="/etc/nixos" +ONLY_FLAKE=false +ONLY_SWITCH=false +TARGET="" + +usage() { + echo "Usage: nix-update [--only-flake|--only-switch]" + echo "" + echo "Options:" + echo " home-manager Update and switch home-manager configuration" + echo " nixos Update and switch NixOS configuration" + echo " both Update and switch both configurations" + echo "" + echo "Flags:" + echo " --only-flake Only run nix flake update, skip switch commands" + echo " --only-switch Only run switch commands, skip nix flake update" + exit 1 +} + +parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + home-manager | nixos | both) + if [[ -n "$TARGET" ]]; then + echo "Error: only one target allowed" + usage + fi + TARGET="$1" + shift + ;; + --only-flake) + ONLY_FLAKE=true + shift + ;; + --only-switch) + ONLY_SWITCH=true + shift + ;; + -h | --help) + usage + ;; + *) + echo "Error: unknown option '$1'" + usage + ;; + esac + done + + if [[ -z "$TARGET" ]]; then + echo "Error: no target specified" + usage + fi +} + +update_home_manager() { + echo "==> Updating home-manager..." + cd "$HM_DIR" + + echo " Staging modules/private..." + git add modules/private -f + + if [[ "$ONLY_SWITCH" == false ]]; then + echo " Running nix flake update..." + nix flake update + fi + + if [[ "$ONLY_FLAKE" == false ]]; then + echo " Running home-manager switch..." + home-manager switch --flake "$HM_DIR#arpit" + fi + + echo " Unstaging modules/private..." + for f in modules/private/*.nix; do + [[ "$f" == *.example.nix ]] || git rm --cached "$f" + done + + echo "==> home-manager update complete" +} + +update_nixos() { + echo "==> Updating NixOS..." + sudo -v + cd "$NIXOS_DIR" + + echo " Staging hardware-configuration.nix..." + git add hardware-configuration.nix -f + + if [[ "$ONLY_SWITCH" == false ]]; then + echo " Running nix flake update..." + nix flake update + fi + + if [[ "$ONLY_FLAKE" == false ]]; then + echo " Running nixos-rebuild switch..." + sudo nixos-rebuild switch + fi + + echo " Unstaging hardware-configuration.nix..." + git rm --cached hardware-configuration.nix + + echo "==> NixOS update complete" +} + +parse_args "$@" + +case "$TARGET" in + home-manager) + update_home_manager + ;; + nixos) + update_nixos + ;; + both) + update_nixos + update_home_manager + ;; +esac diff --git a/modules/system/nix-update/nix-update.zsh b/modules/system/nix-update/nix-update.zsh new file mode 100644 index 0000000..a9500e6 --- /dev/null +++ b/modules/system/nix-update/nix-update.zsh @@ -0,0 +1,22 @@ +#compdef nix-update + +_arguments \ + '1: :->target' \ + '*:: :->flags' + +case $state in + target) + local -a commands + commands=( + 'home-manager:Update and switch home-manager configuration' + 'nixos:Update and switch NixOS configuration' + 'both:Update and switch both configurations' + ) + _describe 'command' commands + ;; + flags) + _arguments \ + '--only-flake[Only run nix flake update, skip switch commands]' \ + '--only-switch[Only run switch commands, skip nix flake update]' + ;; +esac diff --git a/modules/system/system-monitor/assertions.nix b/modules/system/system-monitor/assertions.nix new file mode 100644 index 0000000..08b9f7c --- /dev/null +++ b/modules/system/system-monitor/assertions.nix @@ -0,0 +1,19 @@ +{ config, ... }: +[ + { + assertion = !config.system.system-monitor.enable || config.system.bottom.enable; + message = "system.system-monitor is enabled but requires `system.bottom.enable`."; + } + { + assertion = !config.system.system-monitor.enable || config.system.nvtop.enable; + message = "system.system-monitor is enabled but requires `system.nvtop.enable`."; + } + { + assertion = !config.system.system-monitor.enable || config.terminal.tmux.enable; + message = "system.system-monitor is enabled but requires `terminal.tmux.enable`."; + } + { + assertion = !config.system.system-monitor.enable || config.terminal.kitty.enable; + message = "system.system-monitor is enabled but requires `terminal.kitty.enable`."; + } +] diff --git a/modules/system/system-monitor/default.nix b/modules/system/system-monitor/default.nix new file mode 100644 index 0000000..e103ed3 --- /dev/null +++ b/modules/system/system-monitor/default.nix @@ -0,0 +1,41 @@ +# System monitor script using tmux to run bottom and nvtop side-by-side +{ + config, + lib, + pkgs, + ... +}: +let + inherit ((import ../../lib/script.nix { inherit lib pkgs; })) mkScriptModule; + script = mkScriptModule { + scope = [ "system" ]; + name = "system-monitor"; + path = ./system-monitor.sh; + description = "System monitor script using tmux to run bottom and nvtop side-by-side\nOpens a tmux session with:\n - Left pane: bottom (system/process monitor)\n - Right pane: nvtop (GPU monitor)"; + deps = [ + config.system.bottom.package + config.system.nvtop.package + config.terminal.tmux.package + config.terminal.kitty.package + pkgs.bash + ]; + desktop = { + enable = true; + displayName = "System Monitor"; + icon = "bottom-system-monitor"; + }; + inherit config; + }; +in +{ + options = script.options; + config = lib.mkMerge [ + script.config + { + assertions = import ./assertions.nix { inherit config lib; }; + home.file.".local/share/icons/hicolor/scalable/apps/bottom-system-monitor.svg" = { + source = ../../../assets/icons/bottom-system-monitor.svg; + }; + } + ]; +} diff --git a/modules/system/system-monitor/system-monitor.sh b/modules/system/system-monitor/system-monitor.sh new file mode 100644 index 0000000..7a9e7ea --- /dev/null +++ b/modules/system/system-monitor/system-monitor.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +SESSION="system-monitor" + +kitty @ set-font-size -- -5 + +cleanup() { + kitty @ set-font-size -- +5 +} +trap cleanup EXIT INT TERM + +# Kill any existing session +tmux kill-session -t "$SESSION" 2>/dev/null + +# Start new tmux session with btm wrapped to kill session on exit +tmux new-session -d -s "$SESSION" -c "$HOME" "bash -c 'btm; tmux kill-session -t $SESSION'" + +# Split horizontally: right pane runs nvtop wrapped similarly +tmux split-window -h -t "$SESSION:0" -c "$HOME" "bash -c 'nvtop; tmux kill-session -t $SESSION'" + +tmux set -g status off +tmux set -g mouse on +tmux set -g focus-events on + +# Select main pane and attach +tmux select-pane -t "$SESSION:0.0" +tmux attach-session -t "$SESSION" -- cgit v1.2.3