| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218 |
- #!/usr/bin/env bash
- set -euo pipefail
- # Configuration
- CACHE_MAX_AGE=300
- MRU_SIZE=5
- # Debug logging (set DEBUG=1 to enable via environment variable)
- DEBUG="${DEBUG:-0}"
- debug() {
- [ "$DEBUG" = "1" ] && echo "[DEBUG] $*" >&2 || true
- }
- # Parse arguments - flags first, then DEV_DIR
- NO_CACHE=false
- CLEAR_CACHE=false
- EDITOR="${EDITOR:-nvim}"
- DEV_DIR=""
- debug "Starting dev-launcher with args: $*"
- for arg in "$@"; do
- case "$arg" in
- --help | -h)
- cat <<EOF
- Usage: $0 [DEV_DIR] [OPTIONS]
- Options:
- --no-cache Bypass cache and rescan repositories
- --clear-cache Clear cache and MRU files
- --help, -h Show this help message
- EOF
- exit 0
- ;;
- --no-cache)
- NO_CACHE=true
- ;;
- --clear-cache)
- CLEAR_CACHE=true
- ;;
- *)
- [ -z "$DEV_DIR" ] && DEV_DIR="$arg"
- ;;
- esac
- done
- DEV_DIR="${DEV_DIR:-$HOME/Dev}"
- debug "DEV_DIR: $DEV_DIR"
- debug "NO_CACHE: $NO_CACHE"
- debug "CLEAR_CACHE: $CLEAR_CACHE"
- # Setup cache files (unique per dev directory)
- DEV_DIR_HASH=$(echo -n "$DEV_DIR" | sha256sum | cut -d' ' -f1 | head -c 16)
- CACHE_FILE="$HOME/.cache/dev-launcher-cache-${DEV_DIR_HASH}"
- MRU_FILE="$HOME/.cache/dev-launcher-mru-${DEV_DIR_HASH}"
- debug "CACHE_FILE: $CACHE_FILE"
- debug "MRU_FILE: $MRU_FILE"
- # Handle --clear-cache
- if [ "$CLEAR_CACHE" = true ]; then
- rm -f "$CACHE_FILE" "$MRU_FILE"
- notify-send "Dev Launcher" "Cache cleared" 2>/dev/null || true
- exit 0
- fi
- # Find git repositories
- find_git_repos() {
- debug "find_git_repos: checking cache..."
- if [ "$NO_CACHE" = false ] && [ -f "$CACHE_FILE" ]; then
- local cache_age=$(($(date +%s) - $(stat -c %Y "$CACHE_FILE" 2>/dev/null || echo 0)))
- debug "Cache age: ${cache_age}s (max: ${CACHE_MAX_AGE}s)"
- if [ $cache_age -lt $CACHE_MAX_AGE ]; then
- debug "Using cached repos"
- cat "$CACHE_FILE"
- return
- fi
- debug "Cache expired, rescanning..."
- fi
- debug "Scanning for git repositories in $DEV_DIR..."
- if command -v fd >/dev/null 2>&1; then
- debug "Using fd for scanning"
- fd -H -t d "^\.git$" "$DEV_DIR" -d 3 -0 | xargs -0 -n1 dirname | sort -u >"$CACHE_FILE"
- else
- debug "Using find for scanning"
- find "$DEV_DIR" -maxdepth 3 -type d -name ".git" -print0 |
- xargs -0 -n1 dirname | sort -u >"$CACHE_FILE"
- fi
- local repo_count=$(wc -l <"$CACHE_FILE")
- debug "Found $repo_count repositories"
- cat "$CACHE_FILE"
- }
- # Get sanitized project name for tmux session
- get_project_name() {
- basename "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-\|-$//g'
- }
- # Update MRU list
- update_mru() {
- local selected="$1"
- local temp_file
- temp_file=$(mktemp)
- echo "$selected" >"$temp_file"
- [ -f "$MRU_FILE" ] && grep -vFx "$selected" "$MRU_FILE" >>"$temp_file" || true
- head -n "$MRU_SIZE" "$temp_file" >"$MRU_FILE"
- rm -f "$temp_file"
- }
- # Sort by MRU (MRU items first with ⭐, then others)
- sort_by_mru() {
- local all_repos="$1"
- local mru_list
- local temp_file
- [ -f "$MRU_FILE" ] && mru_list=$(grep -v '^$' "$MRU_FILE") || mru_list=""
- # If MRU list is empty, just return sorted repos
- if [ -z "$mru_list" ]; then
- echo "$all_repos" | sort
- return
- fi
- # Output MRU items first with indicator
- echo "$mru_list" | sed 's/^/⭐ /'
- # Output non-MRU items using comm (fast set difference)
- temp_file=$(mktemp)
- echo "$all_repos" | sort >"$temp_file.all"
- echo "$mru_list" | sort >"$temp_file.mru"
- comm -23 "$temp_file.all" "$temp_file.mru" 2>/dev/null || cat "$temp_file.all"
- rm -f "$temp_file.all" "$temp_file.mru"
- }
- # Main execution
- REPOS=$(find_git_repos)
- if [ -z "$REPOS" ]; then
- debug "No repositories found!"
- notify-send "Dev Launcher" "No git repositories found in $DEV_DIR" 2>/dev/null || true
- exit 1
- fi
- debug "Found repositories, proceeding..."
- # Build display list (relative paths) and mapping
- DISPLAY_LIST=$(echo "$REPOS" | sed "s|^${DEV_DIR}/||")
- declare -A DISPLAY_TO_PATH
- debug "Building display mapping..."
- while IFS=$'\t' read -r repo display; do
- DISPLAY_TO_PATH["$display"]="$repo"
- done < <(paste <(echo "$REPOS") <(echo "$DISPLAY_LIST"))
- debug "Mapped ${#DISPLAY_TO_PATH[@]} projects"
- # Sort by MRU and present in tofi
- debug "Sorting by MRU..."
- SORTED_LIST=$(sort_by_mru "$DISPLAY_LIST")
- debug "Presenting in tofi..."
- SELECTED_DISPLAY=$(echo "$SORTED_LIST" | tofi \
- --prompt-text "💀 Poison: " \
- --fuzzy-match true \
- --width 30% \
- --height 50% \
- --anchor center \
- --padding-left 20 \
- --padding-right 20 \
- --padding-top 15 \
- --padding-bottom 15 \
- --border-width 2 \
- --outline-width 0 \
- --font "$(fc-match -f '%{family}' 2>/dev/null || echo 'sans')" \
- --font-size 16 \
- --background-color '#191724' \
- --text-color '#e0def4' \
- --selection-color '#31748f' \
- --selection-background '#1f1d2e' \
- --border-color '#31748f' \
- --prompt-color '#f6c177')
- debug "Selected display: '$SELECTED_DISPLAY'"
- [ -z "$SELECTED_DISPLAY" ] && debug "No selection, exiting" && exit 0
- # Remove star indicator and lookup path
- SELECTED_DISPLAY_CLEAN=$(echo "$SELECTED_DISPLAY" | sed 's/^⭐ //')
- SELECTED="${DISPLAY_TO_PATH[$SELECTED_DISPLAY_CLEAN]:-}"
- debug "Selected display (clean): '$SELECTED_DISPLAY_CLEAN'"
- debug "Selected path: '$SELECTED'"
- [ -z "$SELECTED" ] || [ ! -d "$SELECTED" ] && debug "Invalid selection, exiting" && exit 0
- # Update MRU and launch
- debug "Updating MRU..."
- update_mru "$SELECTED_DISPLAY_CLEAN"
- PROJECT_NAME=$(get_project_name "$SELECTED")
- SESSION_NAME="dev-${PROJECT_NAME}"
- CLASS_NAME="com.mzunino.dev.${PROJECT_NAME}"
- debug "Project name: $PROJECT_NAME"
- debug "Session name: $SESSION_NAME"
- debug "Class name: $CLASS_NAME"
- debug "Launching ghostty with launch-or-focus..."
- launch-or-focus ghostty \
- --class "$CLASS_NAME" \
- --title "$PROJECT_NAME" \
- --working-directory="$SELECTED" \
- -e bash -c "if tmux has-session -t '$SESSION_NAME' 2>/dev/null; then \
- tmux attach -t '$SESSION_NAME'; \
- else \
- tmux new-session -c '$SELECTED' -s '$SESSION_NAME' -d '$EDITOR'; \
- tmux split-window -h -c '$SELECTED' -t '$SESSION_NAME'; \
- tmux select-pane -t '$SESSION_NAME:0.0'; \
- tmux attach -t '$SESSION_NAME'; \
- fi"
|