dotfiles/local-bin/.local/bin/dev-launcher

218 lines
6.2 KiB
Bash
Executable file

#!/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"