|
|
@@ -0,0 +1,167 @@
|
|
|
+#!/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 <<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}"
|
|
|
+
|
|
|
+# 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}"
|
|
|
+
|
|
|
+# 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() {
|
|
|
+ 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"
|