#!/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 niri-dev-launcher with args: $*" for arg in "$@"; do case "$arg" in --help | -h) cat </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" } # Find window by app-id in niri find_window_by_app_id() { local app_id="$1" debug "Searching for window with app-id: $app_id" # Check if jq is available if ! command -v jq >/dev/null 2>&1; then debug "jq not available, falling back to grep method" # Get windows list and search for matching app-id niri msg windows 2>/dev/null | grep -A 1 "App ID: \"$app_id\"" | head -1 | grep -q "Window ID" || return 1 return 0 fi # Get windows list from niri as JSON local windows_json windows_json=$(niri msg -j windows 2>/dev/null || echo "[]") if [ -z "$windows_json" ] || [ "$windows_json" = "[]" ]; then return 1 fi # Use jq to find matching window local window_id window_id=$(echo "$windows_json" | jq -r --arg app_id "$app_id" ' .[] | select(.app_id | ascii_downcase == ($app_id | ascii_downcase)) | .id ' | head -1) if [ -n "$window_id" ] && [ "$window_id" != "null" ]; then echo "$window_id" return 0 fi return 1 } # Focus or launch alacritty window focus_or_launch_alacritty() { local class_name="$1" local title="$2" local working_dir="$3" local session_name="$4" debug "focus_or_launch_alacritty: class=$class_name, title=$title, dir=$working_dir" # Try to find existing window local window_id window_id=$(find_window_by_app_id "$class_name" || echo "") if [ -n "$window_id" ]; then debug "Found existing window, focusing..." niri msg action focus-window --id "$window_id" >/dev/null 2>&1 else debug "No existing window found, launching alacritty..." alacritty \ --class="$class_name" \ --title="$title" \ --working-directory="$working_dir" \ -e bash -c "if tmux has-session -t '$session_name' 2>/dev/null; then \ tmux attach -t '$session_name'; \ else \ tmux new-session -c '$working_dir' -s '$session_name' -d '$EDITOR'; \ tmux split-window -h -c '$working_dir' -t '$session_name'; \ tmux select-pane -t '$session_name:0.0'; \ tmux attach -t '$session_name'; \ fi" >/dev/null 2>&1 & disown fi } # Main execution REPOS=$(find_git_repos) if [ -z "$REPOS" ]; then debug "No repositories found!" notify-send "Niri 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 fuzzel debug "Sorting by MRU..." SORTED_LIST=$(sort_by_mru "$DISPLAY_LIST") debug "Presenting in fuzzel..." # Configure fuzzel with similar style to original tofi SELECTED_DISPLAY=$(echo "$SORTED_LIST" | fuzzel \ --prompt "💀 Poison: " \ --dmenu \ --width 30 \ --lines 20 \ --border-width 2 \ --font "$(fc-match -f '%{family}:size=16' 2>/dev/null || echo 'sans:size=16')" \ --background-color '#191724ff' \ --text-color '#e0def4ff' \ --match-color '#31748fff' \ --selection-color '#1f1d2eff' \ --selection-text-color '#31748fff' \ --selection-match-color '#31748fff' \ --prompt-color '#f6c177ff') 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 "Focusing or launching alacritty..." focus_or_launch_alacritty "$CLASS_NAME" "$PROJECT_NAME" "$SELECTED" "$SESSION_NAME"