From 6e6e81915045dfe0d0af3eceb0d836db6dc2c781 Mon Sep 17 00:00:00 2001 From: m3tm3re Date: Tue, 6 Jan 2026 05:54:39 +0100 Subject: [PATCH] project-launcher changes --- README.md | 1 + docs/README.md | 2 + .../home-manager/cli/rofi-project-opener.nix | 71 ++++++++++---- pkgs/rofi-project-opener/default.nix | 92 +++++++++++-------- 4 files changed, 112 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 31aaf19..4754d95 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ nix run git+https://code.m3ta.dev/m3tam3re/nixpkgs#zellij-ps | `mem0` | AI memory assistant with vector storage | | `msty-studio` | Msty Studio application | | `pomodoro-timer` | Pomodoro timer utility | +| `rofi-project-opener` | Rofi-based project launcher | | `stt-ptt` | Push to Talk Speech to Text | | `tuxedo-backlight` | Backlight control for Tuxedo laptops | | `zellij-ps` | Project switcher for Zellij | diff --git a/docs/README.md b/docs/README.md index d420855..b90379c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -34,6 +34,7 @@ Documentation for all custom packages: - [mem0](./packages/mem0.md) - AI memory assistant with vector storage - [msty-studio](./packages/msty-studio.md) - Msty Studio application - [pomodoro-timer](./packages/pomodoro-timer.md) - Pomodoro timer utility +- [rofi-project-opener](./packages/rofi-project-opener.md) - Rofi-based project launcher with custom args - [stt-ptt](./packages/stt-ptt.md) - Push to Talk Speech to Text using Whisper - [tuxedo-backlight](./packages/tuxedo-backlight.md) - Backlight control for Tuxedo laptops - [zellij-ps](./packages/zellij-ps.md) - Project switcher for Zellij @@ -50,6 +51,7 @@ Configuration modules for NixOS and Home Manager: #### Home Manager Modules - [Overview](./modules/home-manager/overview.md) - Home Manager modules overview - [CLI Tools](./modules/home-manager/cli/) - CLI-related modules + - [rofi-project-opener](./modules/home-manager/cli/rofi-project-opener.md) - Rofi-based project launcher - [stt-ptt](./modules/home-manager/cli/stt-ptt.md) - Push to Talk Speech to Text - [zellij-ps](./modules/home-manager/cli/zellij-ps.md) - Zellij project switcher - [Coding](./modules/home-manager/coding/) - Development-related modules diff --git a/modules/home-manager/cli/rofi-project-opener.nix b/modules/home-manager/cli/rofi-project-opener.nix index 94d0648..ec7e204 100644 --- a/modules/home-manager/cli/rofi-project-opener.nix +++ b/modules/home-manager/cli/rofi-project-opener.nix @@ -7,23 +7,56 @@ with lib; let cfg = config.cli.rofi-project-opener; - # Convert list of paths to colon-separated string - projectDirsStr = concatStringsSep ":" cfg.projectDirs; + # Project directory submodule type + projectDirType = types.submodule { + options = { + path = mkOption { + type = types.str; + description = "Base directory path to scan for project subdirectories."; + example = "~/dev"; + }; + args = mkOption { + type = types.str; + default = ""; + description = "Additional arguments to pass to opencode when launching projects from this directory."; + example = "--agent Planner-Sisyphus"; + }; + }; + }; + + # Convert projectDirs attrset to JSON for config file + projectDirsJson = builtins.toJSON ( + mapAttrs (name: value: { + path = value.path; + args = value.args; + }) cfg.projectDirs + ); in { options.cli.rofi-project-opener = { enable = mkEnableOption "Rofi-based project directory launcher"; projectDirs = mkOption { - type = types.listOf types.str; - default = ["~/dev" "~/projects"]; + type = types.attrsOf projectDirType; + default = { + dev = { path = "~/dev"; }; + projects = { path = "~/projects"; }; + }; description = '' - List of base directories to scan for project subdirectories. + Attribute set of base directories to scan for project subdirectories. Each directory will be scanned for immediate subdirectories (non-hidden). Projects are displayed as "base_dir/project_name" in rofi. - Supports ~ for home directory expansion. + Each entry can specify: + - path: Base directory path (supports ~ for home directory) + - args: Optional arguments to pass to opencode for projects in this directory + ''; + example = literalExpression '' + { + nixpkgs = { path = "~/p/NIX/nixpkgs"; args = "--agent Planner-Sisyphus"; }; + dev = { path = "~/dev"; }; + work = { path = "~/work"; args = "--agent work-agent"; }; + } ''; - example = literalExpression ''["~/dev" "~/projects" "~/code"]''; }; terminal = mkOption { @@ -37,17 +70,21 @@ in { type = types.str; default = ""; description = '' - Custom command to pass to the terminal. + Custom command to run in the terminal. - Use %s as a placeholder for the project path. - If empty, defaults to opening a shell in the project directory and running opencode. + Placeholders: + - %s = project path + - %a = project args (from projectDirs..args) + + If empty, defaults to: cd to project, run "opencode %a" Examples: - - "" (empty) - Uses default: cd to project, run opencode - - "-e zsh -c 'cd %s && opencode'" - Custom shell with explicit path - - "--hold -e nvim" - Open editor directly (no %s = no cd) + - "" (empty) - Uses default: cd to project, run opencode with args + - "opencode %a" - Run opencode with project-specific args + - "nvim" - Open editor (no args) + - "myapp %s %a" - Custom app with path and args ''; - example = literalExpression ''"-e zsh -c 'cd %s && opencode'"''; + example = literalExpression ''"opencode %a"''; }; rofiPrompt = mkOption { @@ -77,10 +114,12 @@ in { config = mkIf cfg.enable { home.packages = [pkgs.rofi-project-opener]; - # Write config file (shell-independent) + # Write JSON config file for project directories + xdg.configFile."rofi-project-opener/projects.json".text = projectDirsJson; + + # Write shell config file for other settings xdg.configFile."rofi-project-opener/config".text = '' # rofi-project-opener configuration - PROJECT_DIRS="${projectDirsStr}" TERMINAL="${if isDerivation cfg.terminal then "${cfg.terminal}/bin/${cfg.terminal.pname or (builtins.baseNameOf (toString cfg.terminal))}" else cfg.terminal}" ${optionalString (cfg.terminalCommand != "") ''TERMINAL_CMD="${cfg.terminalCommand}"''} ROFI_PROMPT="${cfg.rofiPrompt}" diff --git a/pkgs/rofi-project-opener/default.nix b/pkgs/rofi-project-opener/default.nix index 8637ee9..55583d9 100644 --- a/pkgs/rofi-project-opener/default.nix +++ b/pkgs/rofi-project-opener/default.nix @@ -1,11 +1,11 @@ { - lib, writeShellScriptBin, rofi, libnotify, coreutils, gnugrep, gnused, + jq, }: writeShellScriptBin "rofi-project-opener" '' #!/usr/bin/env bash @@ -24,9 +24,12 @@ writeShellScriptBin "rofi-project-opener" '' BASENAME="${coreutils}/bin/basename" GREP="${gnugrep}/bin/grep" SED="${gnused}/bin/sed" + JQ="${jq}/bin/jq" - # Configuration from config file or environment variables - CONFIG_FILE="''${XDG_CONFIG_HOME:-$HOME/.config}/rofi-project-opener/config" + # Configuration files + CONFIG_DIR="''${XDG_CONFIG_HOME:-$HOME/.config}/rofi-project-opener" + CONFIG_FILE="$CONFIG_DIR/config" + PROJECTS_JSON="$CONFIG_DIR/projects.json" if [[ -f "$CONFIG_FILE" ]]; then # shellcheck disable=SC1090 @@ -34,12 +37,19 @@ writeShellScriptBin "rofi-project-opener" '' fi # Fallback to environment variables or defaults - PROJECT_DIRS="''${PROJECT_DIRS:-$HOME}" TERMINAL="''${TERMINAL:-kitty}" TERMINAL_CMD="''${TERMINAL_CMD:-}" ROFI_PROMPT="''${ROFI_PROMPT:-Select project}" ROFI_ARGS="''${ROFI_ARGS:--dmenu -i}" + # Check for projects.json + if [[ ! -f "$PROJECTS_JSON" ]]; then + echo "[rofi-project-opener] Error: No projects.json found at $PROJECTS_JSON" >&2 + "$NOTIFY" -u critical -a "rofi-project-opener" "No projects.json configuration found" + exit 1 + fi + PROJECTS_JSON_DATA="$(cat "$PROJECTS_JSON")" + # Temporary file for project list PROJECTS_LIST="''${XDG_RUNTIME_DIR:-/tmp}/rofi-project-opener.$$" @@ -59,26 +69,33 @@ writeShellScriptBin "rofi-project-opener" '' "$NOTIFY" -u critical -a "rofi-project-opener" "$message" } - # Build list of projects + # Build list of projects from JSON config + # Format: display_name|full_path|args build_project_list() { > "$PROJECTS_LIST" - # Split PROJECT_DIRS by colon and iterate - IFS=':' read -ra DIR_ARRAY <<< "$PROJECT_DIRS" - for base_dir in "''${DIR_ARRAY[@]}"; do + # Parse JSON and iterate over each base directory entry + local keys + keys=$(echo "$PROJECTS_JSON_DATA" | "$JQ" -r 'keys[]') + + for key in $keys; do + local base_path base_args + base_path=$(echo "$PROJECTS_JSON_DATA" | "$JQ" -r --arg k "$key" '.[$k].path') + base_args=$(echo "$PROJECTS_JSON_DATA" | "$JQ" -r --arg k "$key" '.[$k].args // ""') + # Expand ~ to $HOME - base_dir="''${base_dir/#\~/$HOME}" + base_path="''${base_path/#\~/$HOME}" # Expand $HOME variable (allows both ~ and $HOME in config) - base_dir="$(eval echo "$base_dir")" + base_path="$(eval echo "$base_path")" # Skip if directory doesn't exist - if [[ ! -d "$base_dir" ]]; then - echo "[rofi-project-opener] Warning: Directory does not exist: $base_dir" >&2 + if [[ ! -d "$base_path" ]]; then + echo "[rofi-project-opener] Warning: Directory does not exist: $base_path" >&2 continue fi # Find 1st level subdirectories (non-hidden) - for project in "$base_dir"/*/; do + for project in "$base_path"/*/; do if [[ -d "$project" ]]; then # Get directory name without trailing slash project_path="''${project%/}" @@ -89,8 +106,8 @@ writeShellScriptBin "rofi-project-opener" '' continue fi - base_display="$("$BASENAME" "$base_dir")" - echo "$base_display/$project_name|$project_path" >> "$PROJECTS_LIST" + # Use key as display prefix (e.g., "dev/myproject") + echo "$key/$project_name|$project_path|$base_args" >> "$PROJECTS_LIST" fi done done @@ -101,7 +118,7 @@ writeShellScriptBin "rofi-project-opener" '' # Build project list if ! build_project_list; then - show_error "No projects found in PROJECT_DIRS: $PROJECT_DIRS" + show_error "No projects found in configured directories" exit 1 fi @@ -117,8 +134,10 @@ writeShellScriptBin "rofi-project-opener" '' exit 0 fi - # Get the full path from selection - project_path=$("$GREP" "^$selection|" "$PROJECTS_LIST" | "$CUT" -d '|' -f2) + # Get the full path and args from selection + selected_line=$("$GREP" "^$selection|" "$PROJECTS_LIST") + project_path=$(echo "$selected_line" | "$CUT" -d '|' -f2) + project_args=$(echo "$selected_line" | "$CUT" -d '|' -f3) # Exit if path not found (shouldn't happen) if [[ -z "$project_path" ]]; then @@ -133,33 +152,30 @@ writeShellScriptBin "rofi-project-opener" '' fi # Build terminal command + # Placeholders: %s = project path, %a = project args if [[ -n "$TERMINAL_CMD" ]]; then - # Check if %s placeholder is present - if [[ "$TERMINAL_CMD" == *"%s"* ]]; then - # Replace %s with project path and use as-is - final_cmd="''${TERMINAL_CMD//%s/$project_path}" - # shellcheck disable=SC2086 - exec "$TERMINAL" $final_cmd - else - # Treat as command to run: wrap in shell with cd (--hold keeps terminal open) - # Source /etc/profile (base PATH) and HM session vars (sessionPath) for NixOS - exec "$TERMINAL" --hold -e bash --login -c " - unset __HM_SESS_VARS_SOURCED - for f in ~/.nix-profile/etc/profile.d/hm-session-vars.sh /etc/profiles/per-user/\$USER/etc/profile.d/hm-session-vars.sh; do - [ -f \"\$f\" ] && . \"\$f\" - done - cd '$project_path' && exec $TERMINAL_CMD - " - fi - else - # Default: open terminal, cd to project, run opencode + # Substitute placeholders + final_cmd="$TERMINAL_CMD" + final_cmd="''${final_cmd//%s/$project_path}" + final_cmd="''${final_cmd//%a/$project_args}" + # Source /etc/profile (base PATH) and HM session vars (sessionPath) for NixOS exec "$TERMINAL" --hold -e bash --login -c " unset __HM_SESS_VARS_SOURCED for f in ~/.nix-profile/etc/profile.d/hm-session-vars.sh /etc/profiles/per-user/\$USER/etc/profile.d/hm-session-vars.sh; do [ -f \"\$f\" ] && . \"\$f\" done - cd '$project_path' && opencode + cd '$project_path' && exec $final_cmd + " + else + # Default: open terminal, cd to project, run opencode with args + # Source /etc/profile (base PATH) and HM session vars (sessionPath) for NixOS + exec "$TERMINAL" --hold -e bash --login -c " + unset __HM_SESS_VARS_SOURCED + for f in ~/.nix-profile/etc/profile.d/hm-session-vars.sh /etc/profiles/per-user/\$USER/etc/profile.d/hm-session-vars.sh; do + [ -f \"\$f\" ] && . \"\$f\" + done + cd '$project_path' && opencode $project_args " fi ''