#!/usr/bin/env bash set -euo pipefail # Configuration CACHE_MAX_AGE=300 MRU_SIZE=20 # Parse arguments - flags first, then DEV_DIR NO_CACHE=false CLEAR_CACHE=false EDITOR="${EDITOR:-nvim}" DEV_DIR="" for arg in "$@"; do case "$arg" in --help|-h) cat </dev/null || true exit 0 fi # Find git repositories find_git_repos() { if [ "$NO_CACHE" = false ] && [ -f "$CACHE_FILE" ]; then local cache_age=$(($(date +%s) - $(stat -c %Y "$CACHE_FILE" 2>/dev/null || echo 0))) [ $cache_age -lt $CACHE_MAX_AGE ] && cat "$CACHE_FILE" && return fi if command -v fd >/dev/null 2>&1; then fd -H -t d "^\.git$" "$DEV_DIR" -d 3 -0 | xargs -0 -n1 dirname | sort -u > "$CACHE_FILE" else find "$DEV_DIR" -maxdepth 3 -type d -name ".git" -print0 | \ xargs -0 -n1 dirname | sort -u > "$CACHE_FILE" fi 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 temp_file=$(mktemp) [ -f "$MRU_FILE" ] && mru_list=$(cat "$MRU_FILE" | grep -v '^$') || mru_list="" # Output MRU items first with indicator echo "$mru_list" | sed 's/^/⭐ /' # Output non-MRU items using comm (fast set difference) 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) [ -z "$REPOS" ] && notify-send "Dev Launcher" "No git repositories found in $DEV_DIR" 2>/dev/null && exit 1 # Build display list (relative paths) and mapping DISPLAY_LIST=$(echo "$REPOS" | sed "s|^${DEV_DIR}/||") declare -A DISPLAY_TO_PATH while IFS=$'\t' read -r repo display; do DISPLAY_TO_PATH["$display"]="$repo" done < <(paste <(echo "$REPOS") <(echo "$DISPLAY_LIST")) # Sort by MRU and present in tofi SORTED_LIST=$(sort_by_mru "$DISPLAY_LIST") 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 14 \ --background-color '#191724' \ --text-color '#e0def4' \ --selection-color '#31748f' \ --selection-background '#1f1d2e' \ --border-color '#31748f' \ --prompt-color '#f6c177') [ -z "$SELECTED_DISPLAY" ] && exit 0 # Remove star indicator and lookup path SELECTED_DISPLAY_CLEAN=$(echo "$SELECTED_DISPLAY" | sed 's/^⭐ //') SELECTED="${DISPLAY_TO_PATH[$SELECTED_DISPLAY_CLEAN]:-}" [ -z "$SELECTED" ] || [ ! -d "$SELECTED" ] && exit 0 # Update MRU and launch update_mru "$SELECTED_DISPLAY_CLEAN" PROJECT_NAME=$(get_project_name "$SELECTED") SESSION_NAME="dev-${PROJECT_NAME}" launch-or-focus ghostty \ --working-directory="$SELECTED" \ --title="$PROJECT_NAME" \ --class="$PROJECT_NAME" \ -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"