From 03bd9b6ad5335aff65d9cb6fb1d117b5cb4b7919 Mon Sep 17 00:00:00 2001 From: Arpit Chakladar Date: Sat, 18 Jul 2026 14:55:21 +0530 Subject: refactor(scripts): split monolithic module into per-script submodules Extract shared `mkScript` and `mkScriptModule` helpers into `lib.nix` Split each script into `modules/scripts//` with its own `default.nix`, `script.sh`, and `assertions.nix` Remove `conditions` parameter from `mkScriptModule`; move condition assertions into per-script `assertions.nix` files with inline checks and error messages Simplify `mkScriptModule` to return `{ options, moduleConfig }` instead of a module function, taking `config` as a direct parameter Add `description` field to `mkScript` that inserts the description as a `# ` comment between the shebang and env vars Thread `description` through `mkScriptModule` to each `default.nix` Move top comment lines from each `script.sh` into the `description` field and remove from sources to avoid duplication Remove old flat `.sh` files from `modules/scripts/` root --- modules/scripts/aria2-run.sh | 2 - modules/scripts/aria2-run/assertions.nix | 7 + modules/scripts/aria2-run/default.nix | 26 ++ modules/scripts/aria2-run/script.sh | 1 + modules/scripts/deep-clean.sh | 33 -- modules/scripts/deep-clean/default.nix | 19 ++ modules/scripts/deep-clean/script.sh | 30 ++ modules/scripts/default.nix | 365 ++-------------------- modules/scripts/file-preview-clean.sh | 2 - modules/scripts/file-preview-clean/assertions.nix | 7 + modules/scripts/file-preview-clean/default.nix | 22 ++ modules/scripts/file-preview-clean/script.sh | 1 + modules/scripts/file-preview.sh | 143 --------- modules/scripts/file-preview/assertions.nix | 19 ++ modules/scripts/file-preview/default.nix | 36 +++ modules/scripts/file-preview/script.sh | 142 +++++++++ modules/scripts/fzf-launcher.sh | 50 --- modules/scripts/fzf-launcher/assertions.nix | 7 + modules/scripts/fzf-launcher/default.nix | 26 ++ modules/scripts/fzf-launcher/script.sh | 48 +++ modules/scripts/i3-keybindings.sh | 67 ---- modules/scripts/i3-keybindings/assertions.nix | 11 + modules/scripts/i3-keybindings/default.nix | 30 ++ modules/scripts/i3-keybindings/script.sh | 66 ++++ modules/scripts/lib.nix | 94 ++++++ modules/scripts/neomutt-sync.sh | 63 ---- modules/scripts/neomutt-sync/assertions.nix | 7 + modules/scripts/neomutt-sync/default.nix | 30 ++ modules/scripts/neomutt-sync/script.sh | 61 ++++ modules/scripts/screen-recording.sh | 81 ----- modules/scripts/screen-recording/assertions.nix | 11 + modules/scripts/screen-recording/default.nix | 29 ++ modules/scripts/screen-recording/script.sh | 77 +++++ modules/scripts/system-monitor.sh | 29 -- modules/scripts/system-monitor/assertions.nix | 19 ++ modules/scripts/system-monitor/default.nix | 32 ++ modules/scripts/system-monitor/script.sh | 25 ++ modules/scripts/vpn-connect.sh | 78 ----- modules/scripts/vpn-connect/assertions.nix | 11 + modules/scripts/vpn-connect/default.nix | 32 ++ modules/scripts/vpn-connect/script.sh | 77 +++++ modules/scripts/vpn-disconnect.sh | 28 -- modules/scripts/vpn-disconnect/assertions.nix | 7 + modules/scripts/vpn-disconnect/default.nix | 21 ++ modules/scripts/vpn-disconnect/script.sh | 27 ++ 45 files changed, 1088 insertions(+), 911 deletions(-) delete mode 100644 modules/scripts/aria2-run.sh create mode 100644 modules/scripts/aria2-run/assertions.nix create mode 100644 modules/scripts/aria2-run/default.nix create mode 100644 modules/scripts/aria2-run/script.sh delete mode 100644 modules/scripts/deep-clean.sh create mode 100644 modules/scripts/deep-clean/default.nix create mode 100644 modules/scripts/deep-clean/script.sh delete mode 100644 modules/scripts/file-preview-clean.sh create mode 100644 modules/scripts/file-preview-clean/assertions.nix create mode 100644 modules/scripts/file-preview-clean/default.nix create mode 100644 modules/scripts/file-preview-clean/script.sh delete mode 100644 modules/scripts/file-preview.sh create mode 100644 modules/scripts/file-preview/assertions.nix create mode 100644 modules/scripts/file-preview/default.nix create mode 100644 modules/scripts/file-preview/script.sh delete mode 100644 modules/scripts/fzf-launcher.sh create mode 100644 modules/scripts/fzf-launcher/assertions.nix create mode 100644 modules/scripts/fzf-launcher/default.nix create mode 100644 modules/scripts/fzf-launcher/script.sh delete mode 100644 modules/scripts/i3-keybindings.sh create mode 100644 modules/scripts/i3-keybindings/assertions.nix create mode 100644 modules/scripts/i3-keybindings/default.nix create mode 100644 modules/scripts/i3-keybindings/script.sh create mode 100644 modules/scripts/lib.nix delete mode 100644 modules/scripts/neomutt-sync.sh create mode 100644 modules/scripts/neomutt-sync/assertions.nix create mode 100644 modules/scripts/neomutt-sync/default.nix create mode 100644 modules/scripts/neomutt-sync/script.sh delete mode 100644 modules/scripts/screen-recording.sh create mode 100644 modules/scripts/screen-recording/assertions.nix create mode 100644 modules/scripts/screen-recording/default.nix create mode 100644 modules/scripts/screen-recording/script.sh delete mode 100644 modules/scripts/system-monitor.sh create mode 100644 modules/scripts/system-monitor/assertions.nix create mode 100644 modules/scripts/system-monitor/default.nix create mode 100644 modules/scripts/system-monitor/script.sh delete mode 100644 modules/scripts/vpn-connect.sh create mode 100644 modules/scripts/vpn-connect/assertions.nix create mode 100644 modules/scripts/vpn-connect/default.nix create mode 100644 modules/scripts/vpn-connect/script.sh delete mode 100644 modules/scripts/vpn-disconnect.sh create mode 100644 modules/scripts/vpn-disconnect/assertions.nix create mode 100644 modules/scripts/vpn-disconnect/default.nix create mode 100644 modules/scripts/vpn-disconnect/script.sh (limited to 'modules') diff --git a/modules/scripts/aria2-run.sh b/modules/scripts/aria2-run.sh deleted file mode 100644 index e151b53..0000000 --- a/modules/scripts/aria2-run.sh +++ /dev/null @@ -1,2 +0,0 @@ -# Start aria2 RPC daemon -aria2c --enable-rpc --rpc-listen-all=false --rpc-listen-port=6800 diff --git a/modules/scripts/aria2-run/assertions.nix b/modules/scripts/aria2-run/assertions.nix new file mode 100644 index 0000000..8d1e1cd --- /dev/null +++ b/modules/scripts/aria2-run/assertions.nix @@ -0,0 +1,7 @@ +{ config, ... }: +[ + { + assertion = !config.scripts.aria2-run.enable || config.web.aria2.enable; + message = "scripts.aria2-run is enabled but requires `web.aria2.enable`."; + } +] diff --git a/modules/scripts/aria2-run/default.nix b/modules/scripts/aria2-run/default.nix new file mode 100644 index 0000000..0cdc456 --- /dev/null +++ b/modules/scripts/aria2-run/default.nix @@ -0,0 +1,26 @@ +{ + config, + lib, + pkgs, + ... +}: +let + inherit ((import ../lib.nix { inherit lib pkgs; })) mkScriptModule; + base = mkScriptModule { + name = "aria2-run"; + path = ./script.sh; + description = "Start aria2 RPC daemon"; + deps = [ config.web.aria2.package ]; + desktop = { + enable = true; + displayName = "Aria2 Download Manager"; + }; + inherit config; + }; +in +{ + options = base.options; + config = base.moduleConfig // { + assertions = base.moduleConfig.assertions ++ (import ./assertions.nix { inherit config lib; }); + }; +} diff --git a/modules/scripts/aria2-run/script.sh b/modules/scripts/aria2-run/script.sh new file mode 100644 index 0000000..167fc5d --- /dev/null +++ b/modules/scripts/aria2-run/script.sh @@ -0,0 +1 @@ +aria2c --enable-rpc --rpc-listen-all=false --rpc-listen-port=6800 diff --git a/modules/scripts/deep-clean.sh b/modules/scripts/deep-clean.sh deleted file mode 100644 index 5d1c87a..0000000 --- a/modules/scripts/deep-clean.sh +++ /dev/null @@ -1,33 +0,0 @@ -# Deep clean script for Nix systems -# Removes old generations, garbage, and optimizes store -# WARNING: Do NOT run with sudo - run as normal user -echo "--- Starting Nix Deep Clean ---" - -if [[ $EUID -eq 0 ]]; then - echo "Error: Do not run this script with 'sudo ./clean-nix.sh'." - echo "Run it as your normal user; it will ask for your password when needed." - exit 1 -fi - -echo "Cleaning system generations..." -sudo nix-env -p /nix/var/nix/profiles/system --delete-generations old - -if command -v home-manager &> /dev/null; then - echo "Cleaning Home Manager generations..." - home-manager expire-generations "-0 days" -fi - -echo "Cleaning user generations..." -nix-env --delete-generations old - -echo "Removing 'result' symlinks in current directory..." -find . -name "result" -type l -delete - -echo "Running Garbage Collector..." -sudo nix-collect-garbage -d -nix-collect-garbage -d - -echo "Optimising the store (deduplicating files)..." -nix store optimise - -echo "--- Clean Up Complete ---" diff --git a/modules/scripts/deep-clean/default.nix b/modules/scripts/deep-clean/default.nix new file mode 100644 index 0000000..dc93c2c --- /dev/null +++ b/modules/scripts/deep-clean/default.nix @@ -0,0 +1,19 @@ +{ + 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"; + 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 new file mode 100644 index 0000000..dcbd62b --- /dev/null +++ b/modules/scripts/deep-clean/script.sh @@ -0,0 +1,30 @@ +echo "--- Starting Nix Deep Clean ---" + +if [[ $EUID -eq 0 ]]; then + echo "Error: Do not run this script with 'sudo ./clean-nix.sh'." + echo "Run it as your normal user; it will ask for your password when needed." + exit 1 +fi + +echo "Cleaning system generations..." +sudo nix-env -p /nix/var/nix/profiles/system --delete-generations old + +if command -v home-manager &> /dev/null; then + echo "Cleaning Home Manager generations..." + home-manager expire-generations "-0 days" +fi + +echo "Cleaning user generations..." +nix-env --delete-generations old + +echo "Removing 'result' symlinks in current directory..." +find . -name "result" -type l -delete + +echo "Running Garbage Collector..." +sudo nix-collect-garbage -d +nix-collect-garbage -d + +echo "Optimising the store (deduplicating files)..." +nix store optimise + +echo "--- Clean Up Complete ---" diff --git a/modules/scripts/default.nix b/modules/scripts/default.nix index 789c32d..abecc38 100644 --- a/modules/scripts/default.nix +++ b/modules/scripts/default.nix @@ -1,351 +1,46 @@ -# Scripts - Generates wrapped scripts with environment variables and dependencies { config, lib, pkgs, ... }: -let - shell = pkgs.runtimeShell; - - mkScript = - name: path: env: deps: - let - envVars = lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v: "${n}=\"${toString v}\"") env); - wrappedScript = pkgs.writeTextFile { - name = name; - executable = true; - destination = "/bin/${name}"; - text = '' - #!${shell} - ${envVars} - ${builtins.readFile path} - ''; - }; - in - pkgs.runCommand name - { - name = name; - nativeBuildInputs = [ pkgs.makeWrapper ]; - meta.mainProgram = name; - } - '' - mkdir -p $out/bin - cp ${wrappedScript}/bin/${name} $out/bin/${name} - chmod +x $out/bin/${name} - - wrapProgram $out/bin/${name} \ - --prefix PATH : ${lib.makeBinPath deps} - ''; - - # Script definitions: { path, env?, deps?, conditions? } - # condition: attrset of { option (string path), value (expected value) } - scriptDefs = { - aria2-run = { - path = ./aria2-run.sh; - deps = [ - config.web.aria2.package - ]; - conditions = [ - { - option = "web.aria2.enable"; - value = true; - } - ]; - }; - deep-clean = { - path = ./deep-clean.sh; - }; - file-preview = { - path = ./file-preview.sh; - deps = [ - config.terminal.kitty.package - config.terminal.bat.package - config.media.ffmpeg.package - config.file-management.ouch.package - pkgs.coreutils - pkgs.file - pkgs.findutils - pkgs.fontconfig - pkgs.librsvg - pkgs.antiword - pkgs.pandoc - pkgs.poppler-utils - pkgs.util-linux - ]; - conditions = [ - { - option = "terminal.kitty.enable"; - value = true; - } - { - option = "media.ffmpeg.enable"; - value = true; - } - { - option = "file-management.ouch.enable"; - value = true; - } - { - option = "terminal.bat.enable"; - value = true; - } - ]; - }; - file-preview-clean = { - path = ./file-preview-clean.sh; - deps = [ - config.terminal.kitty.package - ]; - conditions = [ - { - option = "scripts.file-preview.enable"; - value = true; - } - ]; - }; - fzf-launcher = { - path = ./fzf-launcher.sh; - deps = [ - config.terminal.fzf.package - pkgs.dex - pkgs.util-linux - ]; - conditions = [ - { - option = "terminal.fzf.enable"; - value = true; - } - ]; - }; - i3-keybindings = { - path = ./i3-keybindings.sh; - deps = [ - pkgs.coreutils - pkgs.gawk - pkgs.ncurses - ]; - conditions = [ - { - option = "desktop.enable"; - value = true; - } - { - option = "terminal.less.enable"; - value = true; - } - ]; - }; - screen-recording = { - path = ./screen-recording.sh; - deps = [ - config.media.ffmpeg.package - config.media.slop.package - ]; - conditions = [ - { - option = "media.ffmpeg.enable"; - value = true; - } - { - option = "media.slop.enable"; - value = true; - } - ]; - }; - system-monitor = { - path = ./system-monitor.sh; - deps = [ - config.system.bottom.package - config.system.nvtop.package - config.terminal.tmux.package - config.terminal.kitty.package - pkgs.bash - ]; - conditions = [ - { - option = "system.bottom.enable"; - value = true; - } - { - option = "system.nvtop.enable"; - value = true; - } - { - option = "terminal.tmux.enable"; - value = true; - } - { - option = "terminal.kitty.enable"; - value = true; - } - ]; - }; - vpn-connect = { - path = ./vpn-connect.sh; - env = { - SYSTEMD_RESOLVED_PATH = "${config.security.openvpn.package}/libexec/update-systemd-resolved"; - }; - deps = [ - config.terminal.fzf.package - config.security.openvpn.package - pkgs.coreutils - pkgs.findutils - pkgs.gnugrep - pkgs.gnused - ]; - conditions = [ - { - option = "security.openvpn.enable"; - value = true; - } - { - option = "terminal.fzf.enable"; - value = true; - } - ]; - }; - vpn-disconnect = { - path = ./vpn-disconnect.sh; - conditions = [ - { - option = "scripts.vpn-connect.enable"; - value = true; - } - ]; - }; - neomutt-sync = { - path = ./neomutt-sync.sh; - deps = [ - pkgs.dialog - pkgs.coreutils - pkgs.gawk - pkgs.gnused - pkgs.util-linux - config.programs.mbsync.package - config.programs.notmuch.package - ]; - conditions = [ - { - option = "communication.neomutt.enable"; - value = true; - } - ]; - }; - }; - - # Check whether all conditions for a script are satisfied - conditionsMet = - name: def: - lib.all (cond: lib.attrByPath (lib.splitString "." cond.option) false config == cond.value) ( - def.conditions or [ ] - ); - - # Generate a .desktop entry that launches the script in kitty - mkDesktopItem = - name: def: - let - sc = config.scripts.${name}; - in - if sc.enable && sc.desktop.enable then - pkgs.makeDesktopItem { - name = name; - desktopName = sc.desktop.displayName; - exec = "${lib.getExe config.terminal.kitty.package} -e ${lib.getExe sc.package}"; - icon = "kitty"; - categories = [ "Utility" ]; - terminal = false; - type = "Application"; - } - else - null; - - desktopItems = lib.mapAttrsToList mkDesktopItem scriptDefs; - -in { - options.scripts = lib.mapAttrs (name: def: { - enable = lib.mkOption { - type = lib.types.bool; - # Auto-enable if all required tool options are satisfied - default = conditionsMet name def; - description = '' - Whether to enable the ${name} script. - Defaults to true when all required options are set. - ''; - }; - package = lib.mkOption { - type = lib.types.nullOr lib.types.package; - readOnly = true; - description = "The derivation for the ${name} script. Set automatically when enabled."; - }; - 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."; - }; - }; - }) scriptDefs; + imports = [ + ./aria2-run + ./deep-clean + ./file-preview + ./file-preview-clean + ./fzf-launcher + ./i3-keybindings + ./neomutt-sync + ./screen-recording + ./system-monitor + ./vpn-connect + ./vpn-disconnect + ]; config = { - # Wire up .package for each enabled script, with assertions for unmet conditions - scripts = - lib.recursiveUpdate - (lib.mapAttrs (name: def: { - package = lib.mkIf config.scripts.${name}.enable ( - mkScript name def.path (def.env or { }) (def.deps or [ ]) - ); - }) scriptDefs) - { - aria2-run.desktop = { - enable = true; - displayName = "Aria2 Download Manager"; - }; - i3-keybindings.desktop = { - enable = true; - displayName = "i3 Keybindings"; - }; - screen-recording.desktop = { - enable = true; - displayName = "Screen Recording"; - }; - system-monitor.desktop = { - enable = true; - displayName = "System Monitor"; - }; - }; - - assertions = - lib.concatLists ( - lib.mapAttrsToList ( - name: def: - map (cond: { - assertion = - !config.scripts.${name}.enable - || lib.attrByPath (lib.splitString "." cond.option) false config == cond.value; - message = "scripts.${name} is enabled but requires `${cond.option} = ${builtins.toJSON cond.value}`."; - }) (def.conditions or [ ]) - ) scriptDefs - ) - ++ lib.mapAttrsToList (name: _: { - 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."; - }) scriptDefs; - home.packages = [ pkgs.file config.terminal.bat.package ] ++ lib.filter (x: x != null) (lib.mapAttrsToList (_: s: s.package) config.scripts) - ++ lib.filter (x: x != null) desktopItems; + ++ lib.filter (x: x != null) ( + lib.mapAttrsToList ( + name: sc: + if sc.enable && sc.desktop.enable then + pkgs.makeDesktopItem { + name = name; + desktopName = sc.desktop.displayName; + exec = "${lib.getExe config.terminal.kitty.package} -e ${lib.getExe sc.package}"; + icon = "kitty"; + categories = [ "Utility" ]; + terminal = false; + type = "Application"; + } + else + null + ) config.scripts + ); }; } diff --git a/modules/scripts/file-preview-clean.sh b/modules/scripts/file-preview-clean.sh deleted file mode 100644 index 006f63c..0000000 --- a/modules/scripts/file-preview-clean.sh +++ /dev/null @@ -1,2 +0,0 @@ -# Clear kitty icat preview after lf exits -exec kitten icat --clear --stdin no --transfer-mode file /dev/tty diff --git a/modules/scripts/file-preview-clean/assertions.nix b/modules/scripts/file-preview-clean/assertions.nix new file mode 100644 index 0000000..b801ee3 --- /dev/null +++ b/modules/scripts/file-preview-clean/assertions.nix @@ -0,0 +1,7 @@ +{ config, ... }: +[ + { + assertion = !config.scripts.file-preview-clean.enable || config.scripts.file-preview.enable; + message = "scripts.file-preview-clean is enabled but requires `scripts.file-preview.enable`."; + } +] diff --git a/modules/scripts/file-preview-clean/default.nix b/modules/scripts/file-preview-clean/default.nix new file mode 100644 index 0000000..776e042 --- /dev/null +++ b/modules/scripts/file-preview-clean/default.nix @@ -0,0 +1,22 @@ +{ + config, + lib, + pkgs, + ... +}: +let + inherit ((import ../lib.nix { inherit lib pkgs; })) mkScriptModule; + base = mkScriptModule { + name = "file-preview-clean"; + path = ./script.sh; + description = "Clear kitty icat preview after lf exits"; + deps = [ config.terminal.kitty.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/file-preview-clean/script.sh b/modules/scripts/file-preview-clean/script.sh new file mode 100644 index 0000000..ee8ca73 --- /dev/null +++ b/modules/scripts/file-preview-clean/script.sh @@ -0,0 +1 @@ +exec kitten icat --clear --stdin no --transfer-mode file /dev/tty diff --git a/modules/scripts/file-preview.sh b/modules/scripts/file-preview.sh deleted file mode 100644 index 7712754..0000000 --- a/modules/scripts/file-preview.sh +++ /dev/null @@ -1,143 +0,0 @@ -# Timeout protection (prevents hanging) -TIMEOUT=5 -if [ -z "$LF_PREVIEW_RUNNING" ]; then - export LF_PREVIEW_RUNNING=1 - exec timeout "$TIMEOUT" "$0" "$@" -fi - -# Parameters from lf -FILE_PATH="$1" -WIDTH="$2" -HEIGHT="$3" -X="$4" -Y="$5" - -# Temp dir -TMP_DIR="/tmp/file-preview-$(id -u)" -mkdir -p "$TMP_DIR" - -# Unique temp file per actual file using inode -INODE=$(stat -c '%i' "$FILE_PATH" 2>/dev/null || echo "0") - -# Render image via kitty -render_image() { - local file="$1" - local tries=0 - while [ ! -s "$file" ]; do - sleep 0.2 - tries=$((tries + 1)) - done - [ ! -s "$file" ] && exit 0 - kitten icat --stdin no --transfer-mode file --place "${WIDTH}x${HEIGHT}@${X}x${Y}" "$file" /dev/tty - exit 1 -} - -# Ensure file exists -[ ! -r "$FILE_PATH" ] && exit 0 - -MIMETYPE=$(file --dereference --brief --mime-type "$FILE_PATH") -case "$MIMETYPE" in - # --- IMAGES --- - image/svg+xml) - TMP_IMG="$TMP_DIR/svg-${INODE}.png" - if [ ! -f "$TMP_IMG" ]; then - rsvg-convert "$FILE_PATH" -o "$TMP_IMG" >/dev/null 2>&1 - [ -s "$TMP_IMG" ] && mv "$TMP_IMG" "$TMP_IMG" - fi - render_image "$TMP_IMG" - ;; - - image/*) - render_image "$FILE_PATH" - ;; - - # --- VIDEOS --- - video/*) - TMP_IMG="$TMP_DIR/vid-${INODE}.png" - if [ ! -f "$TMP_IMG" ]; then - if command -v ffmpegthumbnailer >/dev/null; then - ffmpegthumbnailer -i "$FILE_PATH" -o "$TMP_IMG" -s 0 -q 5 >/dev/null 2>&1 - else - ffmpeg -y -i "$FILE_PATH" -ss 00:00:01 -vframes 1 "$TMP_IMG" >/dev/null 2>&1 - fi - fi - render_image "$TMP_IMG" - ;; - - # --- AUDIO --- - audio/*) - TMP_IMG="$TMP_DIR/aud-${INODE}.png" - if [ ! -f "$TMP_IMG" ]; then - ffmpeg -y -i "$FILE_PATH" \ - -filter_complex "showwavespic=s=${WIDTH}x${HEIGHT}:colors=white" \ - -frames:v 1 "$TMP_IMG" >/dev/null 2>&1 - fi - ffprobe -hide_banner -v quiet -show_format -show_streams "$FILE_PATH" - render_image "$TMP_IMG" - exit 0 - ;; - - # --- PDF --- - application/pdf) - TMP_IMG="$TMP_DIR/pdf-${INODE}.png" - if [ ! -f "$TMP_IMG" ]; then - pdftoppm -f 1 -l 1 -png -singlefile \ - -scale-to $((WIDTH * 8)) \ - -r 72 \ - "$FILE_PATH" "${TMP_IMG%.png}" >/dev/null 2>&1 - fi - render_image "$TMP_IMG" - ;; - - # --- EPUB / MOBI --- - application/epub+zip|application/x-mobipocket-ebook) - TMP_IMG="$TMP_DIR/epub-${INODE}.png" - if [ ! -f "$TMP_IMG" ]; then - EPUB_DIR="$TMP_DIR/epub-${INODE}" - ouch decompress --yes --dir "$EPUB_DIR" "$FILE_PATH" >/dev/null 2>&1 - COVER=$(find "$EPUB_DIR" -type f \( -iname 'cover*.jpg' -o -iname 'cover*.jpeg' -o -iname 'cover*.png' \) -print -quit) - [ -n "$COVER" ] && cp "$COVER" "$TMP_IMG" - fi - [ -s "$TMP_IMG" ] && render_image "$TMP_IMG" - ;; - - # --- OFFICE DOCUMENTS --- - # Extract text in the terminal instead of pulling in a graphical office suite. - application/vnd.openxmlformats-officedocument.wordprocessingml.document|application/vnd.oasis.opendocument.text) - pandoc --to plain "$FILE_PATH" 2>/dev/null - exit 0 - ;; - - application/msword) - antiword "$FILE_PATH" 2>/dev/null - exit 0 - ;; - - # --- ARCHIVES --- - application/zip|application/x-7z-compressed|application/x-rar|application/x-tar|application/x-gzip) - ouch list -t "$FILE_PATH" 2>/dev/null - exit 0 - ;; - - # --- TEXT / CODE --- - text/*|application/json|application/javascript|application/xml) - bat --color=always --style=numbers --line-range :200 "$FILE_PATH" 2>/dev/null - exit 0 - ;; - - # --- FONTS --- - font/*|application/vnd.ms-fontobject|application/x-font-ttf) - fc-scan "$FILE_PATH" - exit 0 - ;; - - # --- FALLBACK --- - *) - file --dereference --brief "$FILE_PATH" - echo "------------------------------------------------" - hexdump -C "$FILE_PATH" | head -n 100 - exit 0 - ;; -esac - -exit 0 diff --git a/modules/scripts/file-preview/assertions.nix b/modules/scripts/file-preview/assertions.nix new file mode 100644 index 0000000..41fd4d6 --- /dev/null +++ b/modules/scripts/file-preview/assertions.nix @@ -0,0 +1,19 @@ +{ config, ... }: +[ + { + assertion = !config.scripts.file-preview.enable || config.terminal.kitty.enable; + message = "scripts.file-preview is enabled but requires `terminal.kitty.enable`."; + } + { + assertion = !config.scripts.file-preview.enable || config.media.ffmpeg.enable; + message = "scripts.file-preview is enabled but requires `media.ffmpeg.enable`."; + } + { + assertion = !config.scripts.file-preview.enable || config.file-management.ouch.enable; + message = "scripts.file-preview is enabled but requires `file-management.ouch.enable`."; + } + { + assertion = !config.scripts.file-preview.enable || config.terminal.bat.enable; + message = "scripts.file-preview is enabled but requires `terminal.bat.enable`."; + } +] diff --git a/modules/scripts/file-preview/default.nix b/modules/scripts/file-preview/default.nix new file mode 100644 index 0000000..eec5fb2 --- /dev/null +++ b/modules/scripts/file-preview/default.nix @@ -0,0 +1,36 @@ +{ + config, + lib, + pkgs, + ... +}: +let + inherit ((import ../lib.nix { inherit lib pkgs; })) mkScriptModule; + base = mkScriptModule { + name = "file-preview"; + path = ./script.sh; + description = "Timeout protection (prevents hanging)"; + deps = [ + config.terminal.kitty.package + config.terminal.bat.package + config.media.ffmpeg.package + config.file-management.ouch.package + pkgs.coreutils + pkgs.file + pkgs.findutils + pkgs.fontconfig + pkgs.librsvg + pkgs.antiword + pkgs.pandoc + pkgs.poppler-utils + pkgs.util-linux + ]; + inherit config; + }; +in +{ + options = base.options; + config = base.moduleConfig // { + assertions = base.moduleConfig.assertions ++ (import ./assertions.nix { inherit config lib; }); + }; +} diff --git a/modules/scripts/file-preview/script.sh b/modules/scripts/file-preview/script.sh new file mode 100644 index 0000000..748b1ea --- /dev/null +++ b/modules/scripts/file-preview/script.sh @@ -0,0 +1,142 @@ +TIMEOUT=5 +if [ -z "$LF_PREVIEW_RUNNING" ]; then + export LF_PREVIEW_RUNNING=1 + exec timeout "$TIMEOUT" "$0" "$@" +fi + +# Parameters from lf +FILE_PATH="$1" +WIDTH="$2" +HEIGHT="$3" +X="$4" +Y="$5" + +# Temp dir +TMP_DIR="/tmp/file-preview-$(id -u)" +mkdir -p "$TMP_DIR" + +# Unique temp file per actual file using inode +INODE=$(stat -c '%i' "$FILE_PATH" 2>/dev/null || echo "0") + +# Render image via kitty +render_image() { + local file="$1" + local tries=0 + while [ ! -s "$file" ]; do + sleep 0.2 + tries=$((tries + 1)) + done + [ ! -s "$file" ] && exit 0 + kitten icat --stdin no --transfer-mode file --place "${WIDTH}x${HEIGHT}@${X}x${Y}" "$file" /dev/tty + exit 1 +} + +# Ensure file exists +[ ! -r "$FILE_PATH" ] && exit 0 + +MIMETYPE=$(file --dereference --brief --mime-type "$FILE_PATH") +case "$MIMETYPE" in + # --- IMAGES --- + image/svg+xml) + TMP_IMG="$TMP_DIR/svg-${INODE}.png" + if [ ! -f "$TMP_IMG" ]; then + rsvg-convert "$FILE_PATH" -o "$TMP_IMG" >/dev/null 2>&1 + [ -s "$TMP_IMG" ] && mv "$TMP_IMG" "$TMP_IMG" + fi + render_image "$TMP_IMG" + ;; + + image/*) + render_image "$FILE_PATH" + ;; + + # --- VIDEOS --- + video/*) + TMP_IMG="$TMP_DIR/vid-${INODE}.png" + if [ ! -f "$TMP_IMG" ]; then + if command -v ffmpegthumbnailer >/dev/null; then + ffmpegthumbnailer -i "$FILE_PATH" -o "$TMP_IMG" -s 0 -q 5 >/dev/null 2>&1 + else + ffmpeg -y -i "$FILE_PATH" -ss 00:00:01 -vframes 1 "$TMP_IMG" >/dev/null 2>&1 + fi + fi + render_image "$TMP_IMG" + ;; + + # --- AUDIO --- + audio/*) + TMP_IMG="$TMP_DIR/aud-${INODE}.png" + if [ ! -f "$TMP_IMG" ]; then + ffmpeg -y -i "$FILE_PATH" \ + -filter_complex "showwavespic=s=${WIDTH}x${HEIGHT}:colors=white" \ + -frames:v 1 "$TMP_IMG" >/dev/null 2>&1 + fi + ffprobe -hide_banner -v quiet -show_format -show_streams "$FILE_PATH" + render_image "$TMP_IMG" + exit 0 + ;; + + # --- PDF --- + application/pdf) + TMP_IMG="$TMP_DIR/pdf-${INODE}.png" + if [ ! -f "$TMP_IMG" ]; then + pdftoppm -f 1 -l 1 -png -singlefile \ + -scale-to $((WIDTH * 8)) \ + -r 72 \ + "$FILE_PATH" "${TMP_IMG%.png}" >/dev/null 2>&1 + fi + render_image "$TMP_IMG" + ;; + + # --- EPUB / MOBI --- + application/epub+zip|application/x-mobipocket-ebook) + TMP_IMG="$TMP_DIR/epub-${INODE}.png" + if [ ! -f "$TMP_IMG" ]; then + EPUB_DIR="$TMP_DIR/epub-${INODE}" + ouch decompress --yes --dir "$EPUB_DIR" "$FILE_PATH" >/dev/null 2>&1 + COVER=$(find "$EPUB_DIR" -type f \( -iname 'cover*.jpg' -o -iname 'cover*.jpeg' -o -iname 'cover*.png' \) -print -quit) + [ -n "$COVER" ] && cp "$COVER" "$TMP_IMG" + fi + [ -s "$TMP_IMG" ] && render_image "$TMP_IMG" + ;; + + # --- OFFICE DOCUMENTS --- + # Extract text in the terminal instead of pulling in a graphical office suite. + application/vnd.openxmlformats-officedocument.wordprocessingml.document|application/vnd.oasis.opendocument.text) + pandoc --to plain "$FILE_PATH" 2>/dev/null + exit 0 + ;; + + application/msword) + antiword "$FILE_PATH" 2>/dev/null + exit 0 + ;; + + # --- ARCHIVES --- + application/zip|application/x-7z-compressed|application/x-rar|application/x-tar|application/x-gzip) + ouch list -t "$FILE_PATH" 2>/dev/null + exit 0 + ;; + + # --- TEXT / CODE --- + text/*|application/json|application/javascript|application/xml) + bat --color=always --style=numbers --line-range :200 "$FILE_PATH" 2>/dev/null + exit 0 + ;; + + # --- FONTS --- + font/*|application/vnd.ms-fontobject|application/x-font-ttf) + fc-scan "$FILE_PATH" + exit 0 + ;; + + # --- FALLBACK --- + *) + file --dereference --brief "$FILE_PATH" + echo "------------------------------------------------" + hexdump -C "$FILE_PATH" | head -n 100 + exit 0 + ;; +esac + +exit 0 diff --git a/modules/scripts/fzf-launcher.sh b/modules/scripts/fzf-launcher.sh deleted file mode 100644 index 0858078..0000000 --- a/modules/scripts/fzf-launcher.sh +++ /dev/null @@ -1,50 +0,0 @@ -# Fzf-launcher - FZF-based application launcher (scans .desktop files) - -DIRS=( - /run/current-system/sw/share/applications - "$HOME/.nix-profile/share/applications" - "$HOME/.local/share/applications" -) - -shopt -s nullglob - -entries="" -for dir in "${DIRS[@]}"; do - [[ -d "$dir" ]] || continue - for f in "$dir"/*.desktop; do - [[ -f "$f" ]] || continue - - grep -q "^NoDisplay=true" "$f" && continue - grep -q "^Hidden=true" "$f" && continue - grep -q "^Type=Application" "$f" || continue - grep -q "^Terminal=true" "$f" && continue - - categories=$(grep -m1 "^Categories=" "$f" | cut -d= -f2-) - if [[ -n "$categories" ]]; then - echo "$categories" | grep -qE "(Settings|Screensaver|DesktopSettings)" && continue - fi - - name=$(grep -m1 "^Name=" "$f" | cut -d= -f2-) - if [[ -z "$name" ]]; then - name=$(grep -m1 -E "^Name\[.*\]=" "$f" | cut -d= -f2-) - fi - [[ -z "$name" ]] && continue - - entries+="$name"$'\t'"$f"$'\n' - done -done - -entries=$(printf '%s' "$entries" | sort -fu -t$'\t' -k1,1) - -chosen=$(printf '%s' "$entries" | fzf \ - --prompt='Launch: ' \ - --layout=reverse \ - --info=inline \ - --delimiter=$'\t' \ - --with-nth=1) - -[[ -z "$chosen" ]] && exit 0 - -desktop_file=$(printf '%s' "$chosen" | cut -d$'\t' -f2-) - -setsid -f dex "$desktop_file" /dev/null 2>&1 diff --git a/modules/scripts/fzf-launcher/assertions.nix b/modules/scripts/fzf-launcher/assertions.nix new file mode 100644 index 0000000..308a818 --- /dev/null +++ b/modules/scripts/fzf-launcher/assertions.nix @@ -0,0 +1,7 @@ +{ config, ... }: +[ + { + assertion = !config.scripts.fzf-launcher.enable || config.terminal.fzf.enable; + message = "scripts.fzf-launcher is enabled but requires `terminal.fzf.enable`."; + } +] diff --git a/modules/scripts/fzf-launcher/default.nix b/modules/scripts/fzf-launcher/default.nix new file mode 100644 index 0000000..e9c065d --- /dev/null +++ b/modules/scripts/fzf-launcher/default.nix @@ -0,0 +1,26 @@ +{ + config, + lib, + pkgs, + ... +}: +let + inherit ((import ../lib.nix { inherit lib pkgs; })) mkScriptModule; + base = mkScriptModule { + name = "fzf-launcher"; + path = ./script.sh; + description = "Fzf-launcher - FZF-based application launcher (scans .desktop files)"; + deps = [ + config.terminal.fzf.package + pkgs.dex + pkgs.util-linux + ]; + inherit config; + }; +in +{ + options = base.options; + config = base.moduleConfig // { + assertions = base.moduleConfig.assertions ++ (import ./assertions.nix { inherit config lib; }); + }; +} diff --git a/modules/scripts/fzf-launcher/script.sh b/modules/scripts/fzf-launcher/script.sh new file mode 100644 index 0000000..2fce83a --- /dev/null +++ b/modules/scripts/fzf-launcher/script.sh @@ -0,0 +1,48 @@ +DIRS=( + /run/current-system/sw/share/applications + "$HOME/.nix-profile/share/applications" + "$HOME/.local/share/applications" +) + +shopt -s nullglob + +entries="" +for dir in "${DIRS[@]}"; do + [[ -d "$dir" ]] || continue + for f in "$dir"/*.desktop; do + [[ -f "$f" ]] || continue + + grep -q "^NoDisplay=true" "$f" && continue + grep -q "^Hidden=true" "$f" && continue + grep -q "^Type=Application" "$f" || continue + grep -q "^Terminal=true" "$f" && continue + + categories=$(grep -m1 "^Categories=" "$f" | cut -d= -f2-) + if [[ -n "$categories" ]]; then + echo "$categories" | grep -qE "(Settings|Screensaver|DesktopSettings)" && continue + fi + + name=$(grep -m1 "^Name=" "$f" | cut -d= -f2-) + if [[ -z "$name" ]]; then + name=$(grep -m1 -E "^Name\[.*\]=" "$f" | cut -d= -f2-) + fi + [[ -z "$name" ]] && continue + + entries+="$name"$'\t'"$f"$'\n' + done +done + +entries=$(printf '%s' "$entries" | sort -fu -t$'\t' -k1,1) + +chosen=$(printf '%s' "$entries" | fzf \ + --prompt='Launch: ' \ + --layout=reverse \ + --info=inline \ + --delimiter=$'\t' \ + --with-nth=1) + +[[ -z "$chosen" ]] && exit 0 + +desktop_file=$(printf '%s' "$chosen" | cut -d$'\t' -f2-) + +setsid -f dex "$desktop_file" /dev/null 2>&1 diff --git a/modules/scripts/i3-keybindings.sh b/modules/scripts/i3-keybindings.sh deleted file mode 100644 index 22ddef4..0000000 --- a/modules/scripts/i3-keybindings.sh +++ /dev/null @@ -1,67 +0,0 @@ -# Path to your i3 config file -I3_CONFIG="$HOME/.config/i3/config" - -# Function to wrap text -wrap_text() { - local text="$1" - local width="$2" - echo "$text" | fold -s -w "$width" -} - -# Get terminal width and calculate columns -terminal_width=$(tput cols) -table_width=$((terminal_width - 4)) -max_keybinding_length=$((table_width / 3)) # Keybindings are usually shorter in i3 -max_keybinding_content_length=$((max_keybinding_length - 2)) -description_width=$((table_width - max_keybinding_length - 1)) -description_content_width=$((description_width - 2)) - -# Border lines -top_border="┌$(printf '%.0s─' $(seq 1 $max_keybinding_length))┬$(printf '%.0s─' $(seq 1 $description_width))┐" -row_sep="├$(printf '%.0s─' $(seq 1 $max_keybinding_length))┼$(printf '%.0s─' $(seq 1 $description_width))┤" -bottom_border="└$(printf '%.0s─' $(seq 1 $max_keybinding_length))┴$(printf '%.0s─' $(seq 1 $description_width))┘" - -{ - echo "$top_border" - printf "│ %-*s │ %-*s │\n" "$max_keybinding_content_length" "KEYBINDING" "$description_content_width" "DESCRIPTION" - - last_comment="" - - # Read i3 config line by line - while IFS= read -r line; do - # If line is a comment, store it as a potential description - if [[ $line =~ ^#\ (.*) ]]; then - last_comment="${BASH_REMATCH[1]}" - - # If line contains a bindsym - elif [[ $line =~ ^bindsym ]]; then - # Extract the key combo (the parts between 'bindsym' and the start of the command/exec) - # This regex captures bindsym [options] - keybinding=$(echo "$line" | awk '{print $2}') - - # If the comment is empty, use the command itself as the description - if [[ -z "$last_comment" ]]; then - display_desc=$(echo "$line" | cut -d' ' -f3-) - else - display_desc="$last_comment" - fi - - wrapped_description=$(wrap_text "$display_desc" "$description_content_width") - - echo "$row_sep" - - # Print first line of binding and description - printf "│ %-*s │ %-*s │\n" "$max_keybinding_content_length" "$keybinding" "$description_content_width" "$(echo "$wrapped_description" | head -n 1)" - - # Print subsequent lines of wrapped description - echo "$wrapped_description" | sed '1d' | while read -r continuation; do - printf "│ %-*s │ %-*s │\n" "$max_keybinding_content_length" "" "$description_content_width" "$continuation" - done - - # Reset comment after use - last_comment="" - fi - done < "$I3_CONFIG" - - echo "$bottom_border" -} | less -R diff --git a/modules/scripts/i3-keybindings/assertions.nix b/modules/scripts/i3-keybindings/assertions.nix new file mode 100644 index 0000000..ce1dd6b --- /dev/null +++ b/modules/scripts/i3-keybindings/assertions.nix @@ -0,0 +1,11 @@ +{ config, ... }: +[ + { + assertion = !config.scripts.i3-keybindings.enable || config.desktop.enable; + message = "scripts.i3-keybindings is enabled but requires `desktop.enable`."; + } + { + assertion = !config.scripts.i3-keybindings.enable || config.terminal.less.enable; + message = "scripts.i3-keybindings is enabled but requires `terminal.less.enable`."; + } +] diff --git a/modules/scripts/i3-keybindings/default.nix b/modules/scripts/i3-keybindings/default.nix new file mode 100644 index 0000000..e1be7fe --- /dev/null +++ b/modules/scripts/i3-keybindings/default.nix @@ -0,0 +1,30 @@ +{ + config, + lib, + pkgs, + ... +}: +let + inherit ((import ../lib.nix { inherit lib pkgs; })) mkScriptModule; + base = mkScriptModule { + name = "i3-keybindings"; + path = ./script.sh; + description = "Path to your i3 config file"; + deps = [ + pkgs.coreutils + pkgs.gawk + pkgs.ncurses + ]; + desktop = { + enable = true; + displayName = "i3 Keybindings"; + }; + inherit config; + }; +in +{ + options = base.options; + config = base.moduleConfig // { + assertions = base.moduleConfig.assertions ++ (import ./assertions.nix { inherit config lib; }); + }; +} diff --git a/modules/scripts/i3-keybindings/script.sh b/modules/scripts/i3-keybindings/script.sh new file mode 100644 index 0000000..326e8bd --- /dev/null +++ b/modules/scripts/i3-keybindings/script.sh @@ -0,0 +1,66 @@ +I3_CONFIG="$HOME/.config/i3/config" + +# Function to wrap text +wrap_text() { + local text="$1" + local width="$2" + echo "$text" | fold -s -w "$width" +} + +# Get terminal width and calculate columns +terminal_width=$(tput cols) +table_width=$((terminal_width - 4)) +max_keybinding_length=$((table_width / 3)) # Keybindings are usually shorter in i3 +max_keybinding_content_length=$((max_keybinding_length - 2)) +description_width=$((table_width - max_keybinding_length - 1)) +description_content_width=$((description_width - 2)) + +# Border lines +top_border="┌$(printf '%.0s─' $(seq 1 $max_keybinding_length))┬$(printf '%.0s─' $(seq 1 $description_width))┐" +row_sep="├$(printf '%.0s─' $(seq 1 $max_keybinding_length))┼$(printf '%.0s─' $(seq 1 $description_width))┤" +bottom_border="└$(printf '%.0s─' $(seq 1 $max_keybinding_length))┴$(printf '%.0s─' $(seq 1 $description_width))┘" + +{ + echo "$top_border" + printf "│ %-*s │ %-*s │\n" "$max_keybinding_content_length" "KEYBINDING" "$description_content_width" "DESCRIPTION" + + last_comment="" + + # Read i3 config line by line + while IFS= read -r line; do + # If line is a comment, store it as a potential description + if [[ $line =~ ^#\ (.*) ]]; then + last_comment="${BASH_REMATCH[1]}" + + # If line contains a bindsym + elif [[ $line =~ ^bindsym ]]; then + # Extract the key combo (the parts between 'bindsym' and the start of the command/exec) + # This regex captures bindsym [options] + keybinding=$(echo "$line" | awk '{print $2}') + + # If the comment is empty, use the command itself as the description + if [[ -z "$last_comment" ]]; then + display_desc=$(echo "$line" | cut -d' ' -f3-) + else + display_desc="$last_comment" + fi + + wrapped_description=$(wrap_text "$display_desc" "$description_content_width") + + echo "$row_sep" + + # Print first line of binding and description + printf "│ %-*s │ %-*s │\n" "$max_keybinding_content_length" "$keybinding" "$description_content_width" "$(echo "$wrapped_description" | head -n 1)" + + # Print subsequent lines of wrapped description + echo "$wrapped_description" | sed '1d' | while read -r continuation; do + printf "│ %-*s │ %-*s │\n" "$max_keybinding_content_length" "" "$description_content_width" "$continuation" + done + + # Reset comment after use + last_comment="" + fi + done < "$I3_CONFIG" + + echo "$bottom_border" +} | less -R diff --git a/modules/scripts/lib.nix b/modules/scripts/lib.nix new file mode 100644 index 0000000..8b04b5f --- /dev/null +++ b/modules/scripts/lib.nix @@ -0,0 +1,94 @@ +{ lib, pkgs }: +let + shell = pkgs.runtimeShell; + + mkScript = + name: path: description: env: deps: + 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: "${n}=\"${toString v}\"") env); + wrappedScript = pkgs.writeTextFile { + name = name; + executable = true; + destination = "/bin/${name}"; + text = '' + #!${shell} + ${descComment}${envVars} + ${builtins.readFile path} + ''; + }; + in + pkgs.runCommand name + { + nativeBuildInputs = [ pkgs.makeWrapper ]; + meta.mainProgram = name; + } + '' + mkdir -p $out/bin + cp ${wrappedScript}/bin/${name} $out/bin/${name} + chmod +x $out/bin/${name} + + wrapProgram $out/bin/${name} \ + --prefix PATH : ${lib.makeBinPath deps} + ''; + + mkScriptModule = + { + name, + path, + env ? { }, + deps ? [ ], + desktop ? null, + config, + description ? "", + }: + { + 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."; + }; + 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."; + }; + }; + }; + + moduleConfig = { + scripts.${name} = lib.mkIf config.scripts.${name}.enable ( + { + package = mkScript name path description env deps; + } + // lib.optionalAttrs (desktop != null) { inherit desktop; } + ); + 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."; + } + ]; + }; + }; +in +{ + inherit mkScript mkScriptModule; +} diff --git a/modules/scripts/neomutt-sync.sh b/modules/scripts/neomutt-sync.sh deleted file mode 100644 index e5aae34..0000000 --- a/modules/scripts/neomutt-sync.sh +++ /dev/null @@ -1,63 +0,0 @@ -# Neomutt-sync - Interactive mail sync with dialog progress bar -# 1. Create temporary files for the dark theme and notmuch logs -export DIALOGRC=$(mktemp) -NOTMUCH_LOG=$(mktemp) -TITLE="[SYNCING MAIL]" -BACK_TITLE= -trap 'rm -f "$DIALOGRC" "$NOTMUCH_LOG"' EXIT # Clean up everything on exit - -cat << 'EOF' > "$DIALOGRC" -use_shadow = OFF -use_colors = ON -screen_color = (WHITE,BLACK,OFF) -dialog_color = (WHITE,BLACK,OFF) -title_color = (CYAN,BLACK,ON) -gauge_color = (WHITE,BLACK,ON) -border_color = (WHITE,BLACK,OFF) -border2_color = (WHITE,BLACK,OFF) -shadow_color = (BLACK,BLACK,OFF) - -# Active buttons: Blue text and border with no background -button_active_color = (BLUE,BLACK,ON) -button_key_active_color = (BLUE,BLACK,ON) -button_label_active_color = (BLUE,BLACK,ON) - -# Inactive buttons: Yellow text and border with no background -button_inactive_color = (YELLOW,BLACK,OFF) -button_key_inactive_color = (YELLOW,BLACK,OFF) -button_label_inactive_color = (YELLOW,BLACK,OFF) -EOF - -# 2. Run the sync and indexing progress bar -( - # --- PHASE 1: mbsync --- - 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() - }' - - # --- PHASE 2: notmuch --- - printf "XXX\n100\nIndexing new mail with notmuch...\nXXX\n" - notmuch new > "$NOTMUCH_LOG" 2>&1 -) | dialog --title "$TITLE" --gauge "Initializing..." 8 80 0 - -# 3. Format the captured notmuch output into a single clean line -CLEAN_OUT=$(tr '\n' ' ' < "$NOTMUCH_LOG" | sed 's/ */ /g') - -# 4. Display final results and wait for user input (Enter/Esc) to dismiss -dialog --title "$TITLE" --msgbox "Done!\n$CLEAN_OUT" 8 80 - -clear diff --git a/modules/scripts/neomutt-sync/assertions.nix b/modules/scripts/neomutt-sync/assertions.nix new file mode 100644 index 0000000..38f48e4 --- /dev/null +++ b/modules/scripts/neomutt-sync/assertions.nix @@ -0,0 +1,7 @@ +{ 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 new file mode 100644 index 0000000..816c9f7 --- /dev/null +++ b/modules/scripts/neomutt-sync/default.nix @@ -0,0 +1,30 @@ +{ + 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.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 new file mode 100644 index 0000000..96d866d --- /dev/null +++ b/modules/scripts/neomutt-sync/script.sh @@ -0,0 +1,61 @@ +export DIALOGRC=$(mktemp) +NOTMUCH_LOG=$(mktemp) +TITLE="[SYNCING MAIL]" +BACK_TITLE= +trap 'rm -f "$DIALOGRC" "$NOTMUCH_LOG"' EXIT # Clean up everything on exit + +cat << 'EOF' > "$DIALOGRC" +use_shadow = OFF +use_colors = ON +screen_color = (WHITE,BLACK,OFF) +dialog_color = (WHITE,BLACK,OFF) +title_color = (CYAN,BLACK,ON) +gauge_color = (WHITE,BLACK,ON) +border_color = (WHITE,BLACK,OFF) +border2_color = (WHITE,BLACK,OFF) +shadow_color = (BLACK,BLACK,OFF) + +# Active buttons: Blue text and border with no background +button_active_color = (BLUE,BLACK,ON) +button_key_active_color = (BLUE,BLACK,ON) +button_label_active_color = (BLUE,BLACK,ON) + +# Inactive buttons: Yellow text and border with no background +button_inactive_color = (YELLOW,BLACK,OFF) +button_key_inactive_color = (YELLOW,BLACK,OFF) +button_label_inactive_color = (YELLOW,BLACK,OFF) +EOF + +# 2. Run the sync and indexing progress bar +( + # --- PHASE 1: mbsync --- + 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() + }' + + # --- PHASE 2: notmuch --- + printf "XXX\n100\nIndexing new mail with notmuch...\nXXX\n" + notmuch new > "$NOTMUCH_LOG" 2>&1 +) | dialog --title "$TITLE" --gauge "Initializing..." 8 80 0 + +# 3. Format the captured notmuch output into a single clean line +CLEAN_OUT=$(tr '\n' ' ' < "$NOTMUCH_LOG" | sed 's/ */ /g') + +# 4. Display final results and wait for user input (Enter/Esc) to dismiss +dialog --title "$TITLE" --msgbox "Done!\n$CLEAN_OUT" 8 80 + +clear diff --git a/modules/scripts/screen-recording.sh b/modules/scripts/screen-recording.sh deleted file mode 100644 index c0e8525..0000000 --- a/modules/scripts/screen-recording.sh +++ /dev/null @@ -1,81 +0,0 @@ -# Screen recording script using FFmpeg and slop for region selection -# Records X11 screen to MP4 with x264 encoding -# Usage: screen-recording.sh [-s|--select] -# -s, --select Launch slop to select recording region -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' - -# --- 1. ARGUMENT PARSING --- -SELECT_MODE=false -while [[ "$#" -gt 0 ]]; do - case $1 in - -s|--select) SELECT_MODE=true ;; - esac - shift -done - -# --- 2. GEOMETRY SELECTION --- -# Default to a standard 1080p fallback if detection fails -WIDTH=1920 -HEIGHT=1080 -X_OFF=0 -Y_OFF=0 - -# Try to get actual root window size if xwininfo exists -if command -v xwininfo >/dev/null 2>&1; then - WIDTH=$(xwininfo -root | grep 'Width:' | awk '{print $2}') - HEIGHT=$(xwininfo -root | grep 'Height:' | awk '{print $2}') -fi - -if [ "$SELECT_MODE" = true ]; then - if command -v slop >/dev/null 2>&1; then - echo -e "${YELLOW}Select a window or draw a box...${NC}" - # slop output: 123 456 10 20 - read -r W H X Y < <(slop -f "%w %h %x %y" --nokeyboard) - - if [ -n "$W" ]; then - WIDTH=$W - HEIGHT=$H - X_OFF=$X - Y_OFF=$Y - else - echo "Selection cancelled. Exiting." - exit 1 - fi - else - echo -e "${RED}Error: 'slop' not found.${NC} Recording full screen..." - sleep 1 - fi -fi - -# Ensure even dimensions (FFmpeg/x264 requirement) -WIDTH=$(( WIDTH + WIDTH % 2 )) -HEIGHT=$(( HEIGHT + HEIGHT % 2 )) - -# --- 3. 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 "------------------------------------------------" - -# We pass arguments explicitly here to avoid "Unrecognized option" shell errors -ffmpeg -hide_banner \ - -f x11grab \ - -video_size "${WIDTH}x${HEIGHT}" \ - -framerate 30 \ - -i "${DISPLAY:-:0.0}+${X_OFF},${Y_OFF}" \ - -c:v libx264 \ - -preset ultrafast \ - -crf 23 \ - -pix_fmt yuv420p \ - "$FILENAME" - -echo -e "\n${GREEN}Done!${NC} Video saved to $FILENAME" diff --git a/modules/scripts/screen-recording/assertions.nix b/modules/scripts/screen-recording/assertions.nix new file mode 100644 index 0000000..ad4d1eb --- /dev/null +++ b/modules/scripts/screen-recording/assertions.nix @@ -0,0 +1,11 @@ +{ config, ... }: +[ + { + assertion = !config.scripts.screen-recording.enable || config.media.ffmpeg.enable; + message = "scripts.screen-recording is enabled but requires `media.ffmpeg.enable`."; + } + { + assertion = !config.scripts.screen-recording.enable || config.media.slop.enable; + message = "scripts.screen-recording is enabled but requires `media.slop.enable`."; + } +] diff --git a/modules/scripts/screen-recording/default.nix b/modules/scripts/screen-recording/default.nix new file mode 100644 index 0000000..f687d36 --- /dev/null +++ b/modules/scripts/screen-recording/default.nix @@ -0,0 +1,29 @@ +{ + 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 FFmpeg and slop for region selection\nRecords X11 screen to MP4 with x264 encoding\nUsage: screen-recording.sh [-s|--select]\n -s, --select Launch slop to select recording region"; + deps = [ + config.media.ffmpeg.package + config.media.slop.package + ]; + desktop = { + enable = true; + displayName = "Screen Recording"; + }; + inherit config; + }; +in +{ + options = base.options; + config = base.moduleConfig // { + assertions = base.moduleConfig.assertions ++ (import ./assertions.nix { inherit config lib; }); + }; +} diff --git a/modules/scripts/screen-recording/script.sh b/modules/scripts/screen-recording/script.sh new file mode 100644 index 0000000..a3bf86a --- /dev/null +++ b/modules/scripts/screen-recording/script.sh @@ -0,0 +1,77 @@ +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' + +# --- 1. ARGUMENT PARSING --- +SELECT_MODE=false +while [[ "$#" -gt 0 ]]; do + case $1 in + -s|--select) SELECT_MODE=true ;; + esac + shift +done + +# --- 2. GEOMETRY SELECTION --- +# Default to a standard 1080p fallback if detection fails +WIDTH=1920 +HEIGHT=1080 +X_OFF=0 +Y_OFF=0 + +# Try to get actual root window size if xwininfo exists +if command -v xwininfo >/dev/null 2>&1; then + WIDTH=$(xwininfo -root | grep 'Width:' | awk '{print $2}') + HEIGHT=$(xwininfo -root | grep 'Height:' | awk '{print $2}') +fi + +if [ "$SELECT_MODE" = true ]; then + if command -v slop >/dev/null 2>&1; then + echo -e "${YELLOW}Select a window or draw a box...${NC}" + # slop output: 123 456 10 20 + read -r W H X Y < <(slop -f "%w %h %x %y" --nokeyboard) + + if [ -n "$W" ]; then + WIDTH=$W + HEIGHT=$H + X_OFF=$X + Y_OFF=$Y + else + echo "Selection cancelled. Exiting." + exit 1 + fi + else + echo -e "${RED}Error: 'slop' not found.${NC} Recording full screen..." + sleep 1 + fi +fi + +# Ensure even dimensions (FFmpeg/x264 requirement) +WIDTH=$(( WIDTH + WIDTH % 2 )) +HEIGHT=$(( HEIGHT + HEIGHT % 2 )) + +# --- 3. 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 "------------------------------------------------" + +# We pass arguments explicitly here to avoid "Unrecognized option" shell errors +ffmpeg -hide_banner \ + -f x11grab \ + -video_size "${WIDTH}x${HEIGHT}" \ + -framerate 30 \ + -i "${DISPLAY:-:0.0}+${X_OFF},${Y_OFF}" \ + -c:v libx264 \ + -preset ultrafast \ + -crf 23 \ + -pix_fmt yuv420p \ + "$FILENAME" + +echo -e "\n${GREEN}Done!${NC} Video saved to $FILENAME" diff --git a/modules/scripts/system-monitor.sh b/modules/scripts/system-monitor.sh deleted file mode 100644 index cadae3a..0000000 --- a/modules/scripts/system-monitor.sh +++ /dev/null @@ -1,29 +0,0 @@ -# System monitor script using tmux to run bottom and nvtop side-by-side -# Opens a tmux session with: -# - Left pane: bottom (system/process monitor) -# - Right pane: nvtop (GPU monitor) -SESSION="system-monitor" - -kitty @ set-font-size -- -4 - -cleanup() { - kitty @ set-font-size -- +4 -} -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/system-monitor/assertions.nix b/modules/scripts/system-monitor/assertions.nix new file mode 100644 index 0000000..0710fc3 --- /dev/null +++ b/modules/scripts/system-monitor/assertions.nix @@ -0,0 +1,19 @@ +{ 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 new file mode 100644 index 0000000..1f4990b --- /dev/null +++ b/modules/scripts/system-monitor/default.nix @@ -0,0 +1,32 @@ +{ + 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"; + }; + inherit config; + }; +in +{ + options = base.options; + config = base.moduleConfig // { + assertions = base.moduleConfig.assertions ++ (import ./assertions.nix { inherit config lib; }); + }; +} diff --git a/modules/scripts/system-monitor/script.sh b/modules/scripts/system-monitor/script.sh new file mode 100644 index 0000000..951428c --- /dev/null +++ b/modules/scripts/system-monitor/script.sh @@ -0,0 +1,25 @@ +SESSION="system-monitor" + +kitty @ set-font-size -- -4 + +cleanup() { + kitty @ set-font-size -- +4 +} +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/vpn-connect.sh b/modules/scripts/vpn-connect.sh deleted file mode 100644 index 337fa2e..0000000 --- a/modules/scripts/vpn-connect.sh +++ /dev/null @@ -1,78 +0,0 @@ -# VPN-connect - OpenVPN connection script with credential caching and monitoring -CONF_DIR="$HOME/.config/openvpn" -CREDS_DIR="$CONF_DIR/credentials" -SERVERS_DIR="$CONF_DIR/servers" -CACHE_FILE="$HOME/.cache/openvpn/last_connection" -PID_FILE="$HOME/.cache/openvpn/vpn.pid" -LOG_FILE="$HOME/.cache/openvpn/vpn.log" - -mkdir -p "$CREDS_DIR" "$SERVERS_DIR" "$(dirname "$CACHE_FILE")" - -REFRESH=false -if [[ "$1" == "--refresh" || "$1" == "-r" ]]; then - REFRESH=true -fi - -# Load cache -if [[ "$REFRESH" == false && -f "$CACHE_FILE" ]]; then - source "$CACHE_FILE" - if [[ ! -f "$CRED" || ! -f "$SERVER" ]]; then - REFRESH=true - fi -else - REFRESH=true -fi - -# Select if needed -if [[ "$REFRESH" == true ]]; then - CRED=$(find "$CREDS_DIR" -type f | fzf --prompt="Select Credentials > ") - [ -z "$CRED" ] && exit 1 - - SERVER=$(find "$SERVERS_DIR" -type f \( -name "*.conf" -o -name "*.ovpn" \) | fzf --prompt="Select VPN Server > ") - [ -z "$SERVER" ] && exit 1 - - echo "CRED=\"$CRED\"" > "$CACHE_FILE" - echo "SERVER=\"$SERVER\"" >> "$CACHE_FILE" -fi - -echo "Connecting to $SERVER..." - -TMP_CONF=$(mktemp /tmp/vpn-config.XXXXXX) - -sed -e '/up \/etc\/openvpn\/update-resolv-conf/d' \ - -e '/down \/etc\/openvpn\/update-resolv-conf/d' "$SERVER" > "$TMP_CONF" - -# Run in background -sudo -v || exit 1 - -nohup sudo openvpn \ - --config "$TMP_CONF" \ - --auth-user-pass "$CRED" \ - --script-security 2 \ - --up "$SYSTEMD_RESOLVED_PATH" \ - --down "$SYSTEMD_RESOLVED_PATH" \ - --down-pre \ - > "$LOG_FILE" 2>&1 & - -VPN_PID=$! - -echo $VPN_PID > "$PID_FILE" -echo "VPN started in background (PID: $VPN_PID)" -echo "Logs: $LOG_FILE" - -# Wait until connected -echo -n "Waiting for VPN connection" -while true; do - if grep -q "Initialization Sequence Completed" "$LOG_FILE" 2>/dev/null; then - echo "" - echo "VPN connected successfully." - break - fi - if ! kill -0 "$VPN_PID" 2>/dev/null; then - echo "" - echo "VPN process died. Check logs: $LOG_FILE" - exit 1 - fi - sleep 1 - echo -n "." -done diff --git a/modules/scripts/vpn-connect/assertions.nix b/modules/scripts/vpn-connect/assertions.nix new file mode 100644 index 0000000..252df86 --- /dev/null +++ b/modules/scripts/vpn-connect/assertions.nix @@ -0,0 +1,11 @@ +{ config, ... }: +[ + { + assertion = !config.scripts.vpn-connect.enable || config.security.openvpn.enable; + message = "scripts.vpn-connect is enabled but requires `security.openvpn.enable`."; + } + { + assertion = !config.scripts.vpn-connect.enable || config.terminal.fzf.enable; + message = "scripts.vpn-connect is enabled but requires `terminal.fzf.enable`."; + } +] diff --git a/modules/scripts/vpn-connect/default.nix b/modules/scripts/vpn-connect/default.nix new file mode 100644 index 0000000..194513f --- /dev/null +++ b/modules/scripts/vpn-connect/default.nix @@ -0,0 +1,32 @@ +{ + config, + lib, + pkgs, + ... +}: +let + inherit ((import ../lib.nix { inherit lib pkgs; })) mkScriptModule; + base = mkScriptModule { + name = "vpn-connect"; + path = ./script.sh; + description = "VPN-connect - OpenVPN connection script with credential caching and monitoring"; + env = { + SYSTEMD_RESOLVED_PATH = "${config.security.openvpn.package}/libexec/update-systemd-resolved"; + }; + deps = [ + config.terminal.fzf.package + config.security.openvpn.package + pkgs.coreutils + pkgs.findutils + pkgs.gnugrep + pkgs.gnused + ]; + inherit config; + }; +in +{ + options = base.options; + config = base.moduleConfig // { + assertions = base.moduleConfig.assertions ++ (import ./assertions.nix { inherit config lib; }); + }; +} diff --git a/modules/scripts/vpn-connect/script.sh b/modules/scripts/vpn-connect/script.sh new file mode 100644 index 0000000..4ba8f3d --- /dev/null +++ b/modules/scripts/vpn-connect/script.sh @@ -0,0 +1,77 @@ +CONF_DIR="$HOME/.config/openvpn" +CREDS_DIR="$CONF_DIR/credentials" +SERVERS_DIR="$CONF_DIR/servers" +CACHE_FILE="$HOME/.cache/openvpn/last_connection" +PID_FILE="$HOME/.cache/openvpn/vpn.pid" +LOG_FILE="$HOME/.cache/openvpn/vpn.log" + +mkdir -p "$CREDS_DIR" "$SERVERS_DIR" "$(dirname "$CACHE_FILE")" + +REFRESH=false +if [[ "$1" == "--refresh" || "$1" == "-r" ]]; then + REFRESH=true +fi + +# Load cache +if [[ "$REFRESH" == false && -f "$CACHE_FILE" ]]; then + source "$CACHE_FILE" + if [[ ! -f "$CRED" || ! -f "$SERVER" ]]; then + REFRESH=true + fi +else + REFRESH=true +fi + +# Select if needed +if [[ "$REFRESH" == true ]]; then + CRED=$(find "$CREDS_DIR" -type f | fzf --prompt="Select Credentials > ") + [ -z "$CRED" ] && exit 1 + + SERVER=$(find "$SERVERS_DIR" -type f \( -name "*.conf" -o -name "*.ovpn" \) | fzf --prompt="Select VPN Server > ") + [ -z "$SERVER" ] && exit 1 + + echo "CRED=\"$CRED\"" > "$CACHE_FILE" + echo "SERVER=\"$SERVER\"" >> "$CACHE_FILE" +fi + +echo "Connecting to $SERVER..." + +TMP_CONF=$(mktemp /tmp/vpn-config.XXXXXX) + +sed -e '/up \/etc\/openvpn\/update-resolv-conf/d' \ + -e '/down \/etc\/openvpn\/update-resolv-conf/d' "$SERVER" > "$TMP_CONF" + +# Run in background +sudo -v || exit 1 + +nohup sudo openvpn \ + --config "$TMP_CONF" \ + --auth-user-pass "$CRED" \ + --script-security 2 \ + --up "$SYSTEMD_RESOLVED_PATH" \ + --down "$SYSTEMD_RESOLVED_PATH" \ + --down-pre \ + > "$LOG_FILE" 2>&1 & + +VPN_PID=$! + +echo $VPN_PID > "$PID_FILE" +echo "VPN started in background (PID: $VPN_PID)" +echo "Logs: $LOG_FILE" + +# Wait until connected +echo -n "Waiting for VPN connection" +while true; do + if grep -q "Initialization Sequence Completed" "$LOG_FILE" 2>/dev/null; then + echo "" + echo "VPN connected successfully." + break + fi + if ! kill -0 "$VPN_PID" 2>/dev/null; then + echo "" + echo "VPN process died. Check logs: $LOG_FILE" + exit 1 + fi + sleep 1 + echo -n "." +done diff --git a/modules/scripts/vpn-disconnect.sh b/modules/scripts/vpn-disconnect.sh deleted file mode 100644 index 9a70231..0000000 --- a/modules/scripts/vpn-disconnect.sh +++ /dev/null @@ -1,28 +0,0 @@ -# VPN-disconnect - Kill VPN process and clean up -PID_FILE="$HOME/.cache/openvpn/vpn.pid" - -if [[ ! -f "$PID_FILE" ]]; then - echo "No VPN PID file found." - exit 1 -fi - -PID=$(cat "$PID_FILE") - -if ps -p "$PID" > /dev/null 2>&1; then - echo "Stopping VPN (PID: $PID)..." - sudo kill "$PID" - - # Wait for clean shutdown - sleep 2 - - if ps -p "$PID" > /dev/null 2>&1; then - echo "Force killing VPN..." - sudo kill -9 "$PID" - fi - - echo "VPN disconnected." -else - echo "VPN process not running." -fi - -rm -f "$PID_FILE" diff --git a/modules/scripts/vpn-disconnect/assertions.nix b/modules/scripts/vpn-disconnect/assertions.nix new file mode 100644 index 0000000..60c4e9b --- /dev/null +++ b/modules/scripts/vpn-disconnect/assertions.nix @@ -0,0 +1,7 @@ +{ config, ... }: +[ + { + assertion = !config.scripts.vpn-disconnect.enable || config.scripts.vpn-connect.enable; + message = "scripts.vpn-disconnect is enabled but requires `scripts.vpn-connect.enable`."; + } +] diff --git a/modules/scripts/vpn-disconnect/default.nix b/modules/scripts/vpn-disconnect/default.nix new file mode 100644 index 0000000..61f4738 --- /dev/null +++ b/modules/scripts/vpn-disconnect/default.nix @@ -0,0 +1,21 @@ +{ + config, + lib, + pkgs, + ... +}: +let + inherit ((import ../lib.nix { inherit lib pkgs; })) mkScriptModule; + base = mkScriptModule { + name = "vpn-disconnect"; + path = ./script.sh; + description = "VPN-disconnect - Kill VPN process and clean up"; + inherit config; + }; +in +{ + options = base.options; + config = base.moduleConfig // { + assertions = base.moduleConfig.assertions ++ (import ./assertions.nix { inherit config lib; }); + }; +} diff --git a/modules/scripts/vpn-disconnect/script.sh b/modules/scripts/vpn-disconnect/script.sh new file mode 100644 index 0000000..ead912c --- /dev/null +++ b/modules/scripts/vpn-disconnect/script.sh @@ -0,0 +1,27 @@ +PID_FILE="$HOME/.cache/openvpn/vpn.pid" + +if [[ ! -f "$PID_FILE" ]]; then + echo "No VPN PID file found." + exit 1 +fi + +PID=$(cat "$PID_FILE") + +if ps -p "$PID" > /dev/null 2>&1; then + echo "Stopping VPN (PID: $PID)..." + sudo kill "$PID" + + # Wait for clean shutdown + sleep 2 + + if ps -p "$PID" > /dev/null 2>&1; then + echo "Force killing VPN..." + sudo kill -9 "$PID" + fi + + echo "VPN disconnected." +else + echo "VPN process not running." +fi + +rm -f "$PID_FILE" -- cgit v1.2.3