{ writeShellScriptBin, rofi, libnotify, coreutils, gnugrep, gnused, jq, }: writeShellScriptBin "rofi-project-opener" '' #!/usr/bin/env bash # rofi-project-opener - Rofi-based project directory launcher set -euo pipefail # Core utilities NOTIFY="${libnotify}/bin/notify-send" ROFI="${rofi}/bin/rofi" MKDIR="${coreutils}/bin/mkdir" RM="${coreutils}/bin/rm" SORT="${coreutils}/bin/sort" CUT="${coreutils}/bin/cut" DIRNAME="${coreutils}/bin/dirname" BASENAME="${coreutils}/bin/basename" GREP="${gnugrep}/bin/grep" SED="${gnused}/bin/sed" JQ="${jq}/bin/jq" # 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 source "$CONFIG_FILE" fi # Fallback to environment variables or defaults 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.$$" # Cleanup on exit cleanup() { [[ -f "$PROJECTS_LIST" ]] && "$RM" -f "$PROJECTS_LIST" } trap cleanup EXIT # Ensure runtime directory exists "$MKDIR" -p "$("$DIRNAME" "$PROJECTS_LIST")" # Show error notification show_error() { local message="$1" echo "[rofi-project-opener] Error: $message" >&2 "$NOTIFY" -u critical -a "rofi-project-opener" "$message" } # Build list of projects from JSON config # Format: display_name|full_path|args build_project_list() { > "$PROJECTS_LIST" # 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_path="''${base_path/#\~/$HOME}" # Expand $HOME variable (allows both ~ and $HOME in config) base_path="$(eval echo "$base_path")" # Skip if directory doesn't exist 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_path"/*/; do if [[ -d "$project" ]]; then # Get directory name without trailing slash project_path="''${project%/}" project_name="$("$BASENAME" "$project_path")" # Skip hidden directories if [[ "$project_name" == .* ]]; then continue fi # Use key as display prefix (e.g., "dev/myproject") echo "$key/$project_name|$project_path|$base_args" >> "$PROJECTS_LIST" fi done done # Check if we found any projects [[ -s "$PROJECTS_LIST" ]] } # Build project list if ! build_project_list; then show_error "No projects found in configured directories" exit 1 fi # Deduplicate and sort "$SORT" -t '|' -k1 -u "$PROJECTS_LIST" -o "$PROJECTS_LIST" # Display in rofi and get selection # shellcheck disable=SC2086 selection=$("$CUT" -d '|' -f1 "$PROJECTS_LIST" | "$ROFI" $ROFI_ARGS -p "$ROFI_PROMPT") || true # Exit if cancelled if [[ -z "$selection" ]]; then exit 0 fi # 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 show_error "Could not find project path for: $selection" exit 1 fi # Verify directory still exists if [[ ! -d "$project_path" ]]; then show_error "Project directory no longer exists: $project_path" exit 1 fi # Build terminal command # Placeholders: %s = project path, %a = project args if [[ -n "$TERMINAL_CMD" ]]; then # 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' && 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 ''