diff --git a/local-bin/.local/bin/niri-dev-launcher b/local-bin/.local/bin/niri-dev-launcher new file mode 100755 index 0000000..bdd61e8 --- /dev/null +++ b/local-bin/.local/bin/niri-dev-launcher @@ -0,0 +1,272 @@ +#!/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" diff --git a/local-bin/.local/bin/niri-launch-or-focus b/local-bin/.local/bin/niri-launch-or-focus new file mode 100755 index 0000000..eafb734 --- /dev/null +++ b/local-bin/.local/bin/niri-launch-or-focus @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Simple script to launch an application or focus it if already running in niri +# Usage: niri-launch-or-focus [--class CLASS] [--title TITLE] [args...] +# +# Examples: +# niri-launch-or-focus firefox +# niri-launch-or-focus code --class Code --title "My Project" +# niri-launch-or-focus kitty --class kitty + +CLASS="" +TITLE="" +ARGS=() + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --class) + CLASS="$2" + shift 2 + ;; + --title) + TITLE="$2" + shift 2 + ;; + *) + ARGS+=("$1") + shift + ;; + esac +done + +if [ ${#ARGS[@]} -eq 0 ]; then + echo "Usage: $0 [--class CLASS] [--title TITLE] [args...]" >&2 + exit 1 +fi + +COMMAND="${ARGS[0]}" +APP_NAME=$(basename "$COMMAND") +unset 'ARGS[0]' +ARGS=("${ARGS[@]}") + +# If class/title were provided as flags, add them to command args (for apps that support them) +if [ -n "$CLASS" ]; then + ARGS=("--class=$CLASS" "${ARGS[@]}") +fi +if [ -n "$TITLE" ]; then + ARGS=("--title=$TITLE" "${ARGS[@]}") +fi + +# Find window by app_id (niri uses app-id), title, or app name +find_window() { + local class="$1" + local title="$2" + local app="$3" + + # Check if jq is available + if ! command -v jq >/dev/null 2>&1; then + echo "Error: jq is required but not found. Please install jq." >&2 + return 1 + 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 + + if [ -n "$class" ]; then + # Match by class (app_id) + window_id=$(echo "$windows_json" | jq -r --arg class "$class" ' + .[] | select(.app_id | ascii_downcase == ($class | ascii_downcase)) | .id + ' | head -1) + elif [ -n "$title" ]; then + # Match by title + window_id=$(echo "$windows_json" | jq -r --arg title "$title" ' + .[] | select(.title == $title) | .id + ' | head -1) + else + # Match by app name (basename of command) + window_id=$(echo "$windows_json" | jq -r --arg app "$app" ' + .[] | select(.app_id | ascii_downcase == ($app | ascii_downcase)) | .id + ' | head -1) + fi + + if [ -n "$window_id" ] && [ "$window_id" != "null" ]; then + echo "$window_id" + return 0 + fi + + return 1 +} + +# Try to find existing window +WINDOW_ID=$(find_window "$CLASS" "$TITLE" "$APP_NAME" || echo "") + +# Focus if found, otherwise launch +if [ -n "$WINDOW_ID" ]; then + niri msg action focus-window --id "$WINDOW_ID" >/dev/null 2>&1 +else + "$COMMAND" "${ARGS[@]}" >/dev/null 2>&1 & + disown +fi diff --git a/local-bin/.local/bin/niri-scratchpad.sh b/local-bin/.local/bin/niri-scratchpad.sh new file mode 100755 index 0000000..33cd878 --- /dev/null +++ b/local-bin/.local/bin/niri-scratchpad.sh @@ -0,0 +1,18 @@ +#!/run/current-system/sw/bin/bash + +# Check if scratchpad terminal is already running +if pgrep -f "kitty.*scratchpad" > /dev/null; then + # Get the window ID of the scratchpad terminal + WINDOW_ID=$(niri msg windows | grep -A 10 'app-id="scratchpad"' | head -1 | grep 'Window ID' | cut -d' ' -f3 | cut -d: -f1) + + if [ -n "$WINDOW_ID" ]; then + # Focus the existing scratchpad window + niri msg action focus-window "$WINDOW_ID" + else + # If we can't find the window ID, spawn a new one + kitty --class scratchpad --title scratchpad & + fi +else + # Spawn a new scratchpad terminal + kitty --class scratchpad --title scratchpad & +fi