Quellcode durchsuchen

dev: automated commit - 2025-11-28 21:56:48

Mariano Z. vor 2 Monaten
Ursprung
Commit
0aface6777

+ 2 - 3
alacritty/.config/alacritty/alacritty.toml

@@ -1,8 +1,8 @@
 # Alacritty Configuration
 # Migrated from Ghostty configuration
 
-# Import noctalia theme
-import = ["~/.config/alacritty/themes/noctalia.toml"]
+# Import rose-pine theme
+import = ["~/.config/alacritty/themes/rose-pine.toml"]
 
 # Font configuration
 [font]
@@ -46,4 +46,3 @@ action = "DecreaseFontSize"
 key = "Key0"
 mods = "Control|Shift"
 action = "ResetFontSize"
-

+ 238 - 272
local-bin/.local/bin/niri-dev-launcher

@@ -1,272 +1,238 @@
-#!/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 <<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/niri-dev-launcher-cache-${DEV_DIR_HASH}"
-MRU_FILE="$HOME/.cache/niri-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 "Niri 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"
-}
-
-# 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"
+#!/usr/bin/env python3
+"""Niri Dev Launcher - Launch development environments from git repositories"""
+
+import argparse
+import hashlib
+import json
+import os
+import re
+import shutil
+import subprocess
+import sys
+import time
+from pathlib import Path
+
+CACHE_MAX_AGE = 300
+MRU_SIZE = 5
+
+
+def debug(msg):
+    if os.getenv("DEBUG") == "1":
+        print(f"[DEBUG] {msg}", file=sys.stderr)
+
+
+def get_cache_files(dev_dir):
+    dev_dir_hash = hashlib.sha256(dev_dir.encode()).hexdigest()[:16]
+    cache_dir = Path.home() / ".cache"
+    return (
+        cache_dir / f"niri-dev-launcher-cache-{dev_dir_hash}",
+        cache_dir / f"niri-dev-launcher-mru-{dev_dir_hash}",
+    )
+
+
+# Cache fd availability check
+_HAS_FD = None
+
+def _has_fd():
+    global _HAS_FD
+    if _HAS_FD is None:
+        _HAS_FD = shutil.which("fd") is not None
+    return _HAS_FD
+
+
+def find_git_repos(dev_dir, no_cache=False):
+    cache_file, _ = get_cache_files(dev_dir)
+    debug("find_git_repos: checking cache...")
+    
+    if not no_cache and cache_file.exists():
+        cache_age = time.time() - os.path.getmtime(cache_file)
+        debug(f"Cache age: {cache_age:.0f}s (max: {CACHE_MAX_AGE}s)")
+        if cache_age < CACHE_MAX_AGE:
+            debug("Using cached repos")
+            return [r for r in cache_file.read_text().strip().split("\n") if r]
+        debug("Cache expired, rescanning...")
+    
+    debug(f"Scanning for git repositories in {dev_dir}...")
+    if not Path(dev_dir).exists():
+        return []
+    
+    repos = []
+    if _has_fd():
+        debug("Using fd for scanning")
+        result = subprocess.run(
+            ["fd", "-H", "-t", "d", "^\.git$", dev_dir, "-d", "3", "-0"],
+            capture_output=True, text=True,
+        )
+    else:
+        debug("Using find for scanning")
+        result = subprocess.run(
+            ["find", dev_dir, "-maxdepth", "3", "-type", "d", "-name", ".git", "-print0"],
+            capture_output=True, text=True,
+        )
+    
+    if result.returncode == 0:
+        repos = [str(Path(g).parent) for g in result.stdout.strip("\0").split("\0") if g]
+    
+    repos = sorted(set(repos))
+    cache_file.parent.mkdir(parents=True, exist_ok=True)
+    cache_file.write_text("\n".join(repos))
+    debug(f"Found {len(repos)} repositories")
+    return repos
+
+
+def get_project_name(repo_path):
+    name = Path(repo_path).name.lower()
+    name = re.sub(r"[^a-z0-9]", "-", name)
+    name = re.sub(r"-+", "-", name)
+    return name.strip("-")
+
+
+def update_mru(selected_display, dev_dir):
+    _, mru_file = get_cache_files(dev_dir)
+    mru_list = [l.strip() for l in mru_file.read_text().split("\n") if l.strip()] if mru_file.exists() else []
+    # Remove if exists (using list comprehension is faster than remove+insert for small lists)
+    mru_list = [s for s in mru_list if s != selected_display]
+    mru_list.insert(0, selected_display)
+    mru_file.parent.mkdir(parents=True, exist_ok=True)
+    mru_file.write_text("\n".join(mru_list[:MRU_SIZE]))
+
+
+def sort_by_mru(display_list, dev_dir):
+    _, mru_file = get_cache_files(dev_dir)
+    mru_set = set()
+    if mru_file.exists():
+        mru_set = {l.strip() for l in mru_file.read_text().split("\n") if l.strip()}
+    
+    if not mru_set:
+        return sorted(display_list)
+    
+    # Use set for O(1) lookup instead of list
+    mru_repos = sorted([f"⭐ {d}" for d in display_list if d in mru_set])
+    non_mru_repos = sorted([d for d in display_list if d not in mru_set])
+    return mru_repos + non_mru_repos
+
+
+def find_window_by_app_id(app_id):
+    debug(f"Searching for window with app-id: {app_id}")
+    try:
+        result = subprocess.run(["niri", "msg", "--json", "windows"], capture_output=True, text=True, check=False)
+        windows_json = result.stdout.strip()
+        if not windows_json or windows_json == "[]":
+            return None
+        windows = json.loads(windows_json)
+        for window in windows:
+            if window.get("app_id", "").lower() == app_id.lower():
+                return str(window.get("id"))
+        return None
+    except (json.JSONDecodeError, subprocess.SubprocessError, KeyError):
+        return None
+
+
+def focus_or_launch_alacritty(class_name, title, working_dir, session_name):
+    debug(f"focus_or_launch_alacritty: class={class_name}, title={title}, dir={working_dir}")
+    window_id = find_window_by_app_id(class_name)
+    
+    if window_id:
+        debug("Found existing window, focusing...")
+        subprocess.run(["niri", "msg", "action", "focus-window", "--id", window_id], 
+                      stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+    else:
+        debug("No existing window found, launching alacritty...")
+        editor = os.getenv("EDITOR", "nvim")
+        tmux_cmd = f"""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"""
+        subprocess.Popen(
+            ["alacritty", f"--class={class_name}", f"--title={title}", 
+             f"--working-directory={working_dir}", "-e", "bash", "-c", tmux_cmd],
+            stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True,
+        )
+
+
+def main():
+    parser = argparse.ArgumentParser(description="Niri Dev Launcher")
+    parser.add_argument("dev_dir", nargs="?", default=os.path.expanduser("~/Dev"), help="Development directory")
+    parser.add_argument("--no-cache", action="store_true", help="Bypass cache and rescan repositories")
+    parser.add_argument("--clear-cache", action="store_true", help="Clear cache and MRU files")
+    args = parser.parse_args()
+    
+    debug(f"Starting niri-dev-launcher with args: {sys.argv[1:]}")
+    debug(f"DEV_DIR: {args.dev_dir}")
+    cache_file, mru_file = get_cache_files(args.dev_dir)
+    debug(f"CACHE_FILE: {cache_file}")
+    debug(f"MRU_FILE: {mru_file}")
+    
+    if args.clear_cache:
+        if cache_file.exists():
+            cache_file.unlink()
+        if mru_file.exists():
+            mru_file.unlink()
+        subprocess.run(["notify-send", "Niri Dev Launcher", "Cache cleared"], check=False)
+        sys.exit(0)
+    
+    repos = find_git_repos(args.dev_dir, args.no_cache)
+    if not repos:
+        debug("No repositories found!")
+        subprocess.run(["notify-send", "Niri Dev Launcher", f"No git repositories found in {args.dev_dir}"], check=False)
+        sys.exit(1)
+    
+    debug("Found repositories, proceeding...")
+    display_to_path = {}
+    display_list = []
+    for repo in repos:
+        display = str(Path(repo).relative_to(args.dev_dir)) if repo.startswith(args.dev_dir) else repo
+        display_list.append(display)
+        display_to_path[display] = repo
+    
+    debug(f"Mapped {len(display_to_path)} projects")
+    debug("Sorting by MRU...")
+    sorted_list = sort_by_mru(display_list, args.dev_dir)
+    debug("Presenting in fuzzel...")
+    
+    # Use default font to avoid subprocess call - can be overridden via env if needed    
+    fuzzel_process = subprocess.Popen(
+        ["fuzzel", "--prompt", "💀 Poison: ", "--dmenu", "--width", "30", "--lines", "20",
+         "--border-width", "2", "--background-color", "#191724ff",
+         "--text-color", "#e0def4ff", "--match-color", "#31748fff", "--selection-color", "#1f1d2eff",
+         "--selection-text-color", "#31748fff", "--selection-match-color", "#31748fff", "--prompt-color", "#f6c177ff"],
+        stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True,
+    )
+    
+    stdout, _ = fuzzel_process.communicate(input="\n".join(sorted_list))
+    selected_display = stdout.strip()
+    debug(f"Selected display: '{selected_display}'")
+    
+    if not selected_display:
+        debug("No selection, exiting")
+        sys.exit(0)
+    
+    selected_display_clean = selected_display.replace("⭐ ", "", 1)
+    selected = display_to_path.get(selected_display_clean, "")
+    debug(f"Selected display (clean): '{selected_display_clean}'")
+    debug(f"Selected path: '{selected}'")
+    
+    if not selected or not Path(selected).is_dir():
+        debug("Invalid selection, exiting")
+        sys.exit(0)
+    
+    debug("Updating MRU...")
+    update_mru(selected_display_clean, args.dev_dir)
+    
+    project_name = get_project_name(selected)
+    session_name = f"dev-{project_name}"
+    class_name = f"com.mzunino.dev.{project_name}"
+    debug(f"Project name: {project_name}")
+    debug(f"Session name: {session_name}")
+    debug(f"Class name: {class_name}")
+    debug("Focusing or launching alacritty...")
+    
+    focus_or_launch_alacritty(class_name, project_name, selected, session_name)
+
+
+if __name__ == "__main__":
+    main()

+ 108 - 109
local-bin/.local/bin/niri-launch-or-focus

@@ -1,109 +1,108 @@
-#!/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 <command> [--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 <command> [--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
+#!/usr/bin/env python3
+"""
+Simple script to launch an application or focus it if already running in niri
+Usage: niri-launch-or-focus <command> [--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
+"""
+
+import argparse
+import json
+import subprocess
+import sys
+
+
+def find_window(class_name=None, title=None, app_name=None):
+    """Find window by app_id (class), title, or app name."""
+    try:
+        result = subprocess.run(
+            ["niri", "msg", "--json", "windows"],
+            capture_output=True,
+            text=True,
+            check=False,
+        )
+        windows_json = result.stdout.strip()
+        
+        if not windows_json or windows_json == "[]":
+            return None
+        
+        windows = json.loads(windows_json)
+        
+        for window in windows:
+            app_id = window.get("app_id", "").lower()
+            
+            if class_name:
+                if app_id == class_name.lower():
+                    return window.get("id")
+            elif title:
+                if window.get("title") == title:
+                    return window.get("id")
+            elif app_name:
+                if app_id == app_name.lower():
+                    return window.get("id")
+        
+        return None
+    except (json.JSONDecodeError, subprocess.SubprocessError, KeyError):
+        return None
+
+
+def focus_window(window_id):
+    """Focus a window by ID."""
+    subprocess.run(
+        ["niri", "msg", "action", "focus-window", "--id", str(window_id)],
+        stdout=subprocess.DEVNULL,
+        stderr=subprocess.DEVNULL,
+    )
+
+
+def launch_command(command, args):
+    """Launch a command in the background."""
+    subprocess.Popen(
+        [command] + args,
+        stdout=subprocess.DEVNULL,
+        stderr=subprocess.DEVNULL,
+        start_new_session=True,
+    )
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        description="Launch an application or focus it if already running in niri"
+    )
+    parser.add_argument("command", help="Command to run")
+    parser.add_argument("--class", dest="class_name", help="Window class (app_id)")
+    parser.add_argument("--title", help="Window title")
+    parser.add_argument("args", nargs=argparse.REMAINDER, help="Additional arguments for command")
+    
+    args = parser.parse_args()
+    
+    # Extract app name from command (basename)
+    app_name = args.command.split("/")[-1]
+    
+    # Try to find existing window
+    window_id = find_window(
+        class_name=args.class_name,
+        title=args.title,
+        app_name=app_name if not args.class_name and not args.title else None,
+    )
+    
+    # Build command arguments
+    cmd_args = []
+    if args.class_name:
+        cmd_args.append(f"--class={args.class_name}")
+    if args.title:
+        cmd_args.append(f"--title={args.title}")
+    cmd_args.extend(args.args)
+    
+    # Focus if found, otherwise launch
+    if window_id:
+        focus_window(window_id)
+    else:
+        launch_command(args.command, cmd_args)
+
+
+if __name__ == "__main__":
+    main()

+ 97 - 0
local-bin/.local/bin/nscratch

@@ -0,0 +1,97 @@
+#!/usr/bin/env python3
+# Adapted from the many ideas shared at: https://github.com/YaLTeR/niri/discussions/329
+
+import argparse
+import json
+import os
+import subprocess
+import sys
+
+# the found scratchpad window (id, workspace_id, is_focused, is_floating)
+scratch_window = {}
+# the focused workspace data
+focused_workspace = {}
+# the scratchpad workspace name
+scratch_workspace = os.getenv("NS_WORKSPACE", "scratch")
+
+def niri_cmd(cmd_args):
+    subprocess.run(["niri", "msg", "action"] + cmd_args)
+
+def move_window_to_scratchpad(window_id, animations):
+    niri_cmd(["move-window-to-workspace", "--window-id", str(window_id), scratch_workspace, "--focus=false"])
+    if animations:
+        niri_cmd(["move-window-to-tiling", "--id", str(window_id)])
+
+def bring_scratchpad_window_to_focus(window_id, args):
+    niri_cmd(["move-window-to-workspace", "--window-id", str(window_id), str(focused_workspace["idx"])])
+    if args.animations and not scratch_window["is_floating"]:
+        niri_cmd(["move-window-to-floating", "--id", str(window_id)])
+    niri_cmd(["focus-window", "--id", str(window_id)])
+
+def find_scratch_window(args, windows):
+    for window in windows:
+        if window["app_id"] == args.app_id:
+            scratch_window["id"] = window["id"]
+            scratch_window["workspace_id"] = window["workspace_id"]
+            scratch_window["is_focused"] = window["is_focused"]
+            scratch_window["is_floating"] = window["is_floating"]
+            break
+
+def fetch_focused_workspace():
+    props = subprocess.run(
+        ["niri", "msg", "--json", "workspaces"],
+        capture_output=True,
+        text=True,
+    )
+    workspaces = json.loads(props.stdout)
+
+    # get the focused workspace
+    for workspace in workspaces:
+        if workspace["is_focused"]:
+            focused_workspace["idx"] = workspace["idx"]
+            focused_workspace["output"] = workspace["output"]
+            return workspace["id"]
+
+def ns(parser):
+    args = parser.parse_args()
+
+    props = subprocess.run(
+        ["niri", "msg", "--json", "windows"],
+        capture_output=True,
+        text=True,
+    )
+    windows = json.loads(props.stdout)
+
+    find_scratch_window(args, windows)
+
+    # scratchpad does not yet exist, spawn?
+    if not scratch_window:
+        if args.spawn:
+            niri_cmd(["spawn", "--"] + args.spawn.split(' '))
+            sys.exit(0)
+        else:
+            parser.print_help()
+            sys.exit(1)
+
+    window_id = scratch_window["id"]
+
+    # the scratchpad window exists and it's focused
+    if not scratch_window["is_focused"]:
+        workspace_id = fetch_focused_workspace()
+        # the window is not in the focused workspace
+        if scratch_window["workspace_id"] != workspace_id:
+            bring_scratchpad_window_to_focus(window_id, args)
+            return
+
+    move_window_to_scratchpad(window_id, args.animations)
+
+def main():
+    parser = argparse.ArgumentParser(prog='nscratch', description='Niri Scratchpad support')
+    parser.add_argument('-id', '--app-id', required=True, help='The application identifier')
+    parser.add_argument('-s', '--spawn', help='The process name to spawn when non-existing')
+    parser.add_argument('-a', '--animations', action='store_true', help='Enable animations')
+
+    ns(parser)
+
+if __name__ == "__main__":
+    main()

+ 2 - 2
niri/.config/niri/config.d/binds.kdl

@@ -4,11 +4,11 @@ binds {
     Mod+Return { spawn "alacritty"; }
     Mod+T { spawn-sh "~/.local/bin/niri-dev-launcher ~/Dev"; }
     Mod+Shift+T { spawn-sh "~/.local/bin/niri-dev-launcher --no-cache ~/Dev"; }
-    Mod+U { spawn-sh "~/test.py -id 'uy.com.mzunino'"; }
-    Mod+Shift+U { spawn-sh "alacritty --class=uy.com.mzunino"; }
+    Mod+U { spawn-sh "~/.local/bin/nscratch -id 'uy.com.mzunino' -s 'alacritty --class=uy.com.mzunino'"; }
     Mod+D hotkey-overlay-title="Run an Application: fuzzel" { spawn "fuzzel"; }
     Mod+N { spawn-sh "~/.local/bin/sdm-ui.sh dmenu"; }
     Mod+E { spawn-sh "~/.local/bin/niri-launch-or-focus thunar"; }
+    Mod+Shift+N { spawn-sh "~/.local/bin/dotedit"; }
     Super+Alt+L hotkey-overlay-title="Lock the Screen: swaylock" { spawn "swaylock"; }
     Super+Alt+S allow-when-locked=true hotkey-overlay-title=null { spawn-sh "pkill orca || exec orca"; }
 

+ 0 - 1
niri/.config/niri/config.d/startup.kdl

@@ -10,5 +10,4 @@ spawn-sh-at-startup "gtk-launch com.microsoft.Edge.flextop.msedge-cifhbcnohmdccb
 spawn-sh-at-startup "gtk-launch com.microsoft.Edge.flextop.msedge-faolnafnngnfdaknnbpnkhgohbobgegn-Default"
 // spawn-sh-at-startup "ghostty --gtk-single-instance=true --quit-after-last-window-closed=false --initial-window=false"
 // spawn-sh-at-startup "ghostty --class=uy.com.mzunino"
-spawn-sh-at-startup "alacritty --class=uy.com.mzunino"
 spawn-at-startup "niri" "msg" "action" "focus-workspace" "2"

+ 0 - 9
niri/.config/niri/noctalia/binds.kdl

@@ -1,8 +1,4 @@
 binds {
-    Mod+Space hotkey-overlay-title="Application Launcher" {
-        spawn "noctalia-shell" "ipc" "call" "launcher" "toggle";
-    }
-
     Mod+Y hotkey-overlay-title="Clipboard Manager" {
         spawn "noctalia-shell" "ipc" "call" "launcher" "clipboard";
     }
@@ -11,11 +7,6 @@ binds {
         spawn "noctalia-shell" "ipc" "call" "settings" "toggle";
     }
 
-    Mod+Shift+N hotkey-overlay-title="Notification Center" {
-        spawn "noctalia-shell" "ipc" "call" "notifications" "toggleHistory";
-    }
-
-
     // Audio
     XF86AudioRaiseVolume allow-when-locked=true {
         spawn "noctalia-shell" "ipc" "call" "volume" "increase";

+ 0 - 154
tmux/.config/tmux/tmux.conf.backup.20251026_165838

@@ -1,154 +0,0 @@
-# ==============================================
-# ===           TMUX Configuration           ===
-# ==============================================
-
-# General Settings
-# ----------------
-set -g prefix C-a                          # Use CTRL+a as our tmux command prefix
-unbind C-b                                 # Unbind default prefix
-set -g base-index 1                        # Start windows numbering at 1
-setw -g pane-base-index 1                  # Start pane numbering at 1
-set -g renumber-windows on                 # Renumber windows when a window is closed
-set -s escape-time 1                       # Lower the default tmux delay
-set -g mouse on                            # Enable mouse support
-setw -g aggressive-resize on               # Only resize screen if smaller screen is active
-set -g history-limit 10000                 # Store 10k lines of history
-set-option -g allow-rename off             # Disable automatic window renaming
-set-option -s set-clipboard on             # Enable clipboard support
-setw -g mode-keys vi                       # Use vi keys in copy mode
-
-
-# Key Bindings
-# ------------
-bind r source-file $XDG_CONFIG_HOME/tmux/tmux.conf \; display "tmux reloaded!"  # Reload config
-bind v split-window -h -c "#{pane_current_path}"   # Split vertically
-bind s split-window -v -c "#{pane_current_path}"   # Split horizontally
-
-# Vim-like pane navigation
-bind h select-pane -L
-bind j select-pane -D
-bind k select-pane -U
-bind l select-pane -R
-
-# Vim-like pane resizing
-bind -r H resize-pane -L 5
-bind -r J resize-pane -D 5
-bind -r K resize-pane -U 5
-bind -r L resize-pane -R 5
-
-# Vim-like copy mode navigation
-bind-key -T copy-mode-vi h send-keys -X cursor-left
-bind-key -T copy-mode-vi j send-keys -X cursor-down
-bind-key -T copy-mode-vi k send-keys -X cursor-up
-bind-key -T copy-mode-vi l send-keys -X cursor-right
-bind-key -T copy-mode-vi w send-keys -X next-word
-bind-key -T copy-mode-vi b send-keys -X previous-word
-bind-key -T copy-mode-vi 0 send-keys -X start-of-line
-bind-key -T copy-mode-vi $ send-keys -X end-of-line
-bind-key -T copy-mode-vi G send-keys -X history-bottom
-bind-key -T copy-mode-vi g send-keys -X history-top
-bind-key -T copy-mode-vi / command-prompt -T search -I "#{pane_current_path}" "send -X search-forward \"%%\""
-bind-key -T copy-mode-vi ? command-prompt -T search -I "#{pane_current_path}" "send -X search-backward \"%%\""
-bind-key -T copy-mode-vi n send-keys -X search-again
-bind-key -T copy-mode-vi N send-keys -X search-reverse
-bind-key -T copy-mode-vi v send-keys -X begin-selection
-bind-key -T copy-mode-vi y send-keys -X copy-selection-and-cancel
-bind-key -T copy-mode-vi Escape send-keys -X cancel
-
-# Move pane to a different window
-bind-key m choose-window -F "#{window_index}: #{window_name}" "join-pane -h -t %%"
-bind-key M choose-window -F "#{window_index}: #{window_name}" "join-pane -v -t %%"
-
-# Visual Settings
-# ---------------
-set -g status-interval 2
-set-option -g visual-activity off
-set-option -g visual-bell off
-set-option -g visual-silence off
-set-window-option -g monitor-activity off
-set-option -g bell-action none
-
-# Terminal Settings
-# -----------------
-set -gq allow-passthrough on
-set -g visual-activity off
-
-set -ga update-environment TERM
-set -ga update-environment TERM_PROGRAM
-set -g default-terminal "tmux-256color"
-set -g terminal-overrides ',xterm-256color:Tc'
-
-# Plugins
-# -------
-set -g @plugin 'tmux-plugins/tpm'
-set -g @plugin 'tmux-plugins/tmux-sensible'
-set -g @plugin 'tmux-plugins/tmux-yank'
-set -g @plugin 'catppuccin/tmux#v0.2.0'
-set -g @plugin 'tmux-plugins/tmux-battery'  # Re-enabled with our fixes
-set -g @plugin 'MaximilianGaedig/tmux-filter'
-
-# Plugin Settings
-# ---------------
-set -g @yank_selection_mouse 'clipboard'
-set -g @yank_selection 'clipboard'
-set -g @yank_action 'copy-pipe' # or 'copy-pipe-and-cancel' for the default
-
-# Catppuccin Theme Configuration
-set -g status-position top
-set -g @catppuccin_window_left_separator "█"
-set -g @catppuccin_window_right_separator "█"
-set -g @catppuccin_window_number_position "left"
-set -g @catppuccin_window_middle_separator "█ "
-set -g @catppuccin_window_default_fill "number"
-set -g @catppuccin_window_current_fill "number"
-set -g @catppuccin_window_current_text "#W#{?window_zoomed_flag,(🔍),}"
-set -g @catppuccin_window_default_text "#W"
-
-# Determine status modules based on hostname
-run-shell "if [ $(hostname) = 'fw' ]; then \
-    tmux set -g @catppuccin_status_modules_right 'date_time battery session'; \
-  else \
-    tmux set -g @catppuccin_status_modules_right 'date_time session'; \
-  fi"
-
-set -g @catppuccin_status_left_separator  ""
-set -g @catppuccin_status_right_separator " "
-set -g @catppuccin_status_right_separator_inverse "yes"
-set -g @catppuccin_status_fill "all"
-set -g @catppuccin_status_connect_separator "yes"
-set -g @catppuccin_date_time_text "%Y-%m-%d %H:%M:%S"
-
-# Rose Pine Color Scheme
-set -g @catppuccin_pane_color "#1e1e2e"
-set -g @catppuccin_pane_background_color "#181825"
-set -g @catppuccin_window_current_color "#eb6f92"
-set -g @catppuccin_window_current_background_color "#f5e0dc"
-set -g @catppuccin_window_default_color "#ebbcba"
-set -g @catppuccin_window_default_background_color "#f5e0dc"
-set -g @catppuccin_session_color "#9ccfd8"
-set -g @catppuccin_session_background_color "#f5e0dc"
-set -g @catppuccin_directory_color "#c4a7e7"
-set -g @catppuccin_directory_background_color "#f5e0dc"
-set -g @catppuccin_date_time_color "#f6c177"
-set -g @catppuccin_date_time_background_color "#f5e0dc"
-
-# Battery Icons (Glyphs)
-set -g @batt_icon_charge_tier8 ''
-set -g @batt_icon_charge_tier7 ''
-set -g @batt_icon_charge_tier6 ''
-set -g @batt_icon_charge_tier5 ''
-set -g @batt_icon_charge_tier4 ''
-set -g @batt_icon_charge_tier3 ''
-set -g @batt_icon_charge_tier2 ''
-set -g @batt_icon_charge_tier1 ''
-
-# Plugin Manager
-set-environment -g TMUX_PLUGIN_MANAGER_PATH "$XDG_DATA_HOME/tmux/plugins"
-
-# Install TPM if not already installed
-if "test ! -d ~/.local/share/tmux/plugins/tpm" \
-   "run 'git clone https://github.com/tmux-plugins/tpm ~/.local/share/tmux/plugins/tpm'"
-
-# Initialize TPM (keep this line at the very bottom of tmux.conf)
-run '~/.local/share/tmux/plugins/tpm/tpm'
-

+ 0 - 139
tmux/.config/tmux/tmux.conf.backup.20251027_115806

@@ -1,139 +0,0 @@
-# ==============================================
-# ===           TMUX Configuration           ===
-# ==============================================
-
-# General Settings
-# ----------------
-set -g prefix C-a                          # Use CTRL+a as our tmux command prefix
-unbind C-b                                 # Unbind default prefix
-set -g base-index 1                        # Start windows numbering at 1
-setw -g pane-base-index 1                  # Start pane numbering at 1
-set -g renumber-windows on                 # Renumber windows when a window is closed
-set -s escape-time 1                       # Lower the default tmux delay
-set -g mouse on                            # Enable mouse support
-setw -g aggressive-resize on               # Only resize screen if smaller screen is active
-set -g history-limit 10000                 # Store 10k lines of history
-set-option -g allow-rename off             # Disable automatic window renaming
-set-option -s set-clipboard on             # Enable clipboard support
-setw -g mode-keys vi                       # Use vi keys in copy mode
-
-
-# Key Bindings
-# ------------
-bind r source-file $XDG_CONFIG_HOME/tmux/tmux.conf \; display "tmux reloaded!"  # Reload config
-bind v split-window -h -c "#{pane_current_path}"   # Split vertically
-bind s split-window -v -c "#{pane_current_path}"   # Split horizontally
-
-# Vim-like pane navigation
-bind h select-pane -L
-bind j select-pane -D
-bind k select-pane -U
-bind l select-pane -R
-
-# Vim-like pane resizing
-bind -r H resize-pane -L 5
-bind -r J resize-pane -D 5
-bind -r K resize-pane -U 5
-bind -r L resize-pane -R 5
-
-# Vim-like copy mode navigation
-bind-key -T copy-mode-vi h send-keys -X cursor-left
-bind-key -T copy-mode-vi j send-keys -X cursor-down
-bind-key -T copy-mode-vi k send-keys -X cursor-up
-bind-key -T copy-mode-vi l send-keys -X cursor-right
-bind-key -T copy-mode-vi w send-keys -X next-word
-bind-key -T copy-mode-vi b send-keys -X previous-word
-bind-key -T copy-mode-vi 0 send-keys -X start-of-line
-bind-key -T copy-mode-vi $ send-keys -X end-of-line
-bind-key -T copy-mode-vi G send-keys -X history-bottom
-bind-key -T copy-mode-vi g send-keys -X history-top
-bind-key -T copy-mode-vi / command-prompt -T search -I "#{pane_current_path}" "send -X search-forward \"%%\""
-bind-key -T copy-mode-vi ? command-prompt -T search -I "#{pane_current_path}" "send -X search-backward \"%%\""
-bind-key -T copy-mode-vi n send-keys -X search-again
-bind-key -T copy-mode-vi N send-keys -X search-reverse
-bind-key -T copy-mode-vi v send-keys -X begin-selection
-bind-key -T copy-mode-vi y send-keys -X copy-selection-and-cancel
-bind-key -T copy-mode-vi Escape send-keys -X cancel
-
-# Move pane to a different window
-bind-key m choose-window -F "#{window_index}: #{window_name}" "join-pane -h -t %%"
-bind-key M choose-window -F "#{window_index}: #{window_name}" "join-pane -v -t %%"
-
-# Visual Settings
-# ---------------
-set -g status-interval 2
-set-option -g visual-activity off
-set-option -g visual-bell off
-set-option -g visual-silence off
-set-window-option -g monitor-activity off
-set-option -g bell-action none
-
-# Terminal Settings
-# -----------------
-set -gq allow-passthrough on
-set -g visual-activity off
-
-set -ga update-environment TERM
-set -ga update-environment TERM_PROGRAM
-set -g default-terminal "tmux-256color"
-set -g terminal-overrides ',xterm-256color:Tc'
-
-# Plugins
-# -------
-set -g @plugin 'tmux-plugins/tpm'
-set -g @plugin 'tmux-plugins/tmux-sensible'
-set -g @plugin 'tmux-plugins/tmux-yank'
-set -g @plugin 'rose-pine/tmux'
-set -g @plugin 'tmux-plugins/tmux-battery'  # Re-enabled with our fixes
-set -g @plugin 'MaximilianGaedig/tmux-filter'
-
-# Plugin Settings
-# ---------------
-set -g @yank_selection_mouse 'clipboard'
-set -g @yank_selection 'clipboard'
-set -g @yank_action 'copy-pipe' # or 'copy-pipe-and-cancel' for the default
-
-# Rosé Pine Theme Configuration
-# -----------------------------
-set -g status-position top
-set -g @rose_pine_variant 'main' # Options are 'main', 'moon' or 'dawn'
-
-# Enable extra modules
-set -g @rose_pine_host 'on' # Enables hostname in the status bar
-set -g @rose_pine_hostname_short 'on' # Makes the hostname shorter by using tmux's '#h' format
-set -g @rose_pine_date_time '%Y-%m-%d %H:%M:%S' # Date/time format
-set -g @rose_pine_user 'on' # Turn on the username component in the statusbar
-set -g @rose_pine_directory 'on' # Turn on the current folder component in the status bar
-set -g @rose_pine_bar_bg_disable 'on' # Disables background color, for transparent terminal emulators
-
-# Battery module - determine if battery is available
-run-shell "if [ -d /sys/class/power_supply/BAT* ]; then \
-    tmux set -g @rose_pine_status_right_append_section '#{tmux_battery_status_bg}'; \
-  fi"
-
-# Battery Icons (Glyphs)
-set -g @batt_icon_charge_tier8 ''
-set -g @batt_icon_charge_tier7 ''
-set -g @batt_icon_charge_tier6 ''
-set -g @batt_icon_charge_tier5 ''
-set -g @batt_icon_charge_tier4 ''
-set -g @batt_icon_charge_tier3 ''
-set -g @batt_icon_charge_tier2 ''
-set -g @batt_icon_charge_tier1 ''
-
-# Optional customization options
-# set -g @rose_pine_left_separator ' > '
-# set -g @rose_pine_right_separator ' < '
-# set -g @rose_pine_field_separator ' | '
-set -g @rose_pine_window_separator ' #{?window_zoomed_flag, ,}' # Replaces the default `:` between the window number and name
-
-# Plugin Manager
-set-environment -g TMUX_PLUGIN_MANAGER_PATH "$XDG_DATA_HOME/tmux/plugins"
-
-# Install TPM if not already installed
-if "test ! -d ~/.local/share/tmux/plugins/tpm" \
-   "run 'git clone https://github.com/tmux-plugins/tpm ~/.local/share/tmux/plugins/tpm'"
-
-# Initialize TPM (keep this line at the very bottom of tmux.conf)
-run '~/.local/share/tmux/plugins/tpm/tpm'
-

+ 3 - 3
zsh/.config/zsh/init.zsh

@@ -74,7 +74,7 @@ zsh-defer source "$PLUGIN_DIR/zsh-history-substring-search/zsh-history-substring
 source "$PLUGIN_DIR/minimal/minimal.zsh"
 
 source "$ZDOTDIR/opts.zsh"
-source "$ZDOTDIR/rose-pine.sh"
+# source "$ZDOTDIR/rose-pine.sh"
 
 source "$ZDOTDIR/tmux.zsh"
 
@@ -104,11 +104,11 @@ _compile_zsh_files() {
             "$ZDOTDIR/pnpm.zsh"
             "$ZDOTDIR/mise.zsh"
         )
-        
+
         for func_file in "$ZDOTDIR/functions"/*.zsh; do
             files+=("$func_file")
         done
-        
+
         for file in "${files[@]}"; do
             [[ ! -f "$file" ]] && continue
             local zwc="${file}.zwc"