소스 검색

dev: automated commit - 2026-01-26 20:06:47

Mariano Z. 1 주 전
부모
커밋
675b6aab92
6개의 변경된 파일761개의 추가작업 그리고 217개의 파일을 삭제
  1. 78 0
      i3/.config/dunst/dunstrc
  2. 280 0
      i3/.config/i3/config
  3. 61 0
      i3/.config/i3status-rust/config.toml
  4. 243 217
      local-bin/.local/bin/dev-launcher
  5. 95 0
      local-bin/.local/bin/i3-launch-or-focus
  6. 4 0
      zsh/.zshenv

+ 78 - 0
i3/.config/dunst/dunstrc

@@ -0,0 +1,78 @@
+[global]
+    # Display
+    monitor = 0
+    follow = mouse
+
+    # Geometry
+    width = 350
+    height = 150
+    origin = top-right
+    offset = 20x20
+    notification_limit = 5
+
+    # Progress bar
+    progress_bar = true
+    progress_bar_height = 10
+    progress_bar_frame_width = 1
+    progress_bar_min_width = 150
+    progress_bar_max_width = 300
+
+    # Appearance
+    transparency = 0
+    padding = 16
+    horizontal_padding = 16
+    text_icon_padding = 16
+    frame_width = 2
+    frame_color = "#c4a7e7"
+    gap_size = 8
+    separator_height = 2
+    separator_color = frame
+    corner_radius = 8
+    sort = yes
+
+    # Text
+    font = monospace 11
+    line_height = 0
+    markup = full
+    format = "<b>%s</b>\n%b"
+    alignment = left
+    vertical_alignment = center
+    show_age_threshold = 60
+    ellipsize = middle
+    ignore_newline = no
+    stack_duplicates = true
+    hide_duplicate_count = false
+    show_indicators = yes
+    word_wrap = yes
+
+    # Icons
+    enable_recursive_icon_lookup = true
+    icon_theme = Papirus-Dark
+    icon_position = left
+    min_icon_size = 32
+    max_icon_size = 64
+
+    # Interaction
+    sticky_history = yes
+    history_length = 20
+    mouse_left_click = close_current
+    mouse_middle_click = do_action, close_current
+    mouse_right_click = close_all
+
+[urgency_low]
+    background = "#191724"
+    foreground = "#908caa"
+    frame_color = "#26233a"
+    timeout = 5
+
+[urgency_normal]
+    background = "#191724"
+    foreground = "#e0def4"
+    frame_color = "#c4a7e7"
+    timeout = 10
+
+[urgency_critical]
+    background = "#191724"
+    foreground = "#e0def4"
+    frame_color = "#eb6f92"
+    timeout = 0

+ 280 - 0
i3/.config/i3/config

@@ -0,0 +1,280 @@
+# i3 config — adapted for NixOS
+# Dotfiles managed via Stow; packages provided by myOptions.i3 module
+
+###############################################################################
+# General
+###############################################################################
+set $mod Mod4
+
+font pango:monospace 12
+floating_modifier $mod
+
+# Border style (Rose Pine inspired)
+client.focused          #191724 #191724 #e0def4 #191724 #e0def4
+client.focused_inactive #191724 #191724 #908caa #191724 #908caa
+client.unfocused        #191724 #191724 #6e6a86 #191724 #6e6a86
+client.urgent           #191724 #eb6f92 #191724 #eb6f92 #eb6f92
+
+###############################################################################
+# Workspaces
+###############################################################################
+set $ws1  "1:  "
+set $ws2  "2:  "
+set $ws3  "3:  "
+set $ws4  "4:  "
+set $ws5  "5:  "
+set $ws6  "6:  "
+set $ws7  "7:  "
+set $ws8  "8:  "
+set $ws9  "9:  "
+set $ws10 "0:  "
+
+###############################################################################
+# Appearance
+###############################################################################
+new_window pixel 1
+new_float pixel 1
+
+gaps inner 10
+gaps outer 5
+
+popup_during_fullscreen smart
+
+###############################################################################
+# Keybindings — Window management
+###############################################################################
+bindsym $mod+Return exec alacritty
+bindsym $mod+Shift+q kill
+bindsym $mod+c kill
+
+# Focus (vim keys)
+bindsym $mod+h focus left
+bindsym $mod+j focus down
+bindsym $mod+k focus up
+bindsym $mod+l focus right
+
+# Focus (arrow keys)
+bindsym $mod+Left  focus left
+bindsym $mod+Down  focus down
+bindsym $mod+Up    focus up
+bindsym $mod+Right focus right
+
+# Move window (vim keys)
+bindsym $mod+Shift+h move left
+bindsym $mod+Shift+j move down
+bindsym $mod+Shift+k move up
+bindsym $mod+Shift+l move right
+
+# Move window (arrow keys)
+bindsym $mod+Shift+Left  move left
+bindsym $mod+Shift+Down  move down
+bindsym $mod+Shift+Up    move up
+bindsym $mod+Shift+Right move right
+
+# Move workspace between outputs
+bindsym $mod+bracketright move workspace to output right
+bindsym $mod+bracketleft  move workspace to output left
+
+# Split orientation
+bindsym $mod+semicolon split h
+
+# Fullscreen
+bindsym $mod+f fullscreen toggle
+
+# Layout
+bindsym $mod+s layout stacking
+bindsym $mod+w layout toggle tabbed split
+bindsym $mod+Ctrl+e layout toggle split
+
+# Floating
+bindsym $mod+Shift+space floating toggle
+bindsym $mod+space       focus mode_toggle
+
+# Parent container
+bindsym $mod+a focus parent
+
+# Switch workspace
+bindsym $mod+1 workspace $ws1
+bindsym $mod+2 workspace $ws2
+bindsym $mod+3 workspace $ws3
+bindsym $mod+4 workspace $ws4
+bindsym $mod+5 workspace $ws5
+bindsym $mod+6 workspace $ws6
+bindsym $mod+7 workspace $ws7
+bindsym $mod+8 workspace $ws8
+bindsym $mod+9 workspace $ws9
+bindsym $mod+0 workspace $ws10
+
+# Move container to workspace
+bindsym $mod+Shift+1 move container to workspace $ws1
+bindsym $mod+Shift+2 move container to workspace $ws2
+bindsym $mod+Shift+3 move container to workspace $ws3
+bindsym $mod+Shift+4 move container to workspace $ws4
+bindsym $mod+Shift+5 move container to workspace $ws5
+bindsym $mod+Shift+6 move container to workspace $ws6
+bindsym $mod+Shift+7 move container to workspace $ws7
+bindsym $mod+Shift+8 move container to workspace $ws8
+bindsym $mod+Shift+9 move container to workspace $ws9
+bindsym $mod+Shift+0 move container to workspace $ws10
+
+# i3 management
+bindsym $mod+Shift+c reload
+bindsym $mod+Shift+r restart
+bindsym $mod+Shift+e exec --no-startup-id "i3-nagbar -t warning -m 'Exit i3?' -b 'Yes' 'i3-msg exit'"
+
+###############################################################################
+# Resize mode
+###############################################################################
+bindsym $mod+r mode "resize"
+
+mode "resize" {
+        bindsym h resize shrink width  10 px or 10 ppt
+        bindsym j resize grow   height 10 px or 10 ppt
+        bindsym k resize shrink height 10 px or 10 ppt
+        bindsym l resize grow   width  10 px or 10 ppt
+
+        bindsym Left  resize shrink width  10 px or 10 ppt
+        bindsym Down  resize grow   height 10 px or 10 ppt
+        bindsym Up    resize shrink height 10 px or 10 ppt
+        bindsym Right resize grow   width  10 px or 10 ppt
+
+        bindsym Return mode "default"
+        bindsym Escape mode "default"
+        bindsym $mod+r mode "default"
+}
+
+###############################################################################
+# Keybindings — Launchers & utilities
+###############################################################################
+# rofi
+bindsym $mod+d exec --no-startup-id rofi -show drun -dpi 1
+bindsym $mod+y exec --no-startup-id clipmenu -i -fn 'monospace:size=12' -nb '#191724' -nf '#e0def4' -sb '#c4a7e7' -sf '#191724'
+
+# Project launcher
+bindsym $mod+t exec ~/.local/bin/dev-launcher ~/Dev
+# File manager (match niri)
+bindsym $mod+e exec --no-startup-id ~/.local/bin/i3-launch-or-focus thunar
+# StrongDM UI
+bindsym $ood+n exec ~/.local/bin/i3-launch-or-focus --class sdm-connect alacritty -e ~/.local/bin/sdm-ui.sh
+
+# Power menu
+bindsym XF86PowerOff exec --no-startup-id echo -e "lock\nsuspend\nreboot\nshutdown\nlogout" | $dmenu -p "Power" | xargs -I{} sh -c 'case {} in lock) betterlockscreen -l;; suspend) systemctl suspend;; reboot) systemctl reboot;; shutdown) systemctl poweroff;; logout) i3-msg exit;; esac'
+bindsym $mod+x  exec --no-startup-id $HOME/.config/rofi/kill.sh | $dmenu -p "Kill" | xargs -I{} kill {}
+
+# Screenshot
+bindsym $mod+p exec --no-startup-id flameshot gui
+
+# Pause notifications
+bindsym $mod+F5 exec --no-startup-id ~/.bin/pause-notifications
+
+###############################################################################
+# Mouse bindings
+###############################################################################
+bindsym --release button2           kill
+bindsym --whole-window $mod+button2 kill
+bindsym button3                     floating toggle
+bindsym $mod+button3                floating toggle
+
+###############################################################################
+# Scratchpad
+###############################################################################
+bindsym $mod+u [instance="dropdown"] scratchpad show; [instance="dropdown"] move position center
+bindsym $mod+Shift+u exec --no-startup-id alacritty --class dropdown
+
+bindsym $mod+Shift+minus move scratchpad
+bindsym $mod+minus       scratchpad show
+
+###############################################################################
+# Window rules
+###############################################################################
+for_window [class=".*"]                   border pixel 0
+for_window [instance="dropdown"]          floating enable
+for_window [instance="dropdown"]          resize set 2366 1568
+for_window [instance="dropdown"]          move scratchpad
+for_window [instance="dropdown"]          border pixel 1
+
+for_window [instance="sdm-connect"] floating enable, resize set 2366 1568, border pixel 1, move position center
+
+# Workspace assignments
+for_window [workspace=$ws1] layout tabbed
+assign [class="^Slack"]                   $ws1
+assign [instance="crx__cifhbcnohmdccbgoicgdjpfamggdegmo"] $ws1
+assign [instance="crx__faolnafnngnfdaknnbpnkhgohbobgegn"] $ws1
+assign [class="^zen"]                     $ws2
+assign [class="^thunderbird"]             $ws4
+assign [class="^vesktop$"]               $ws4
+
+###############################################################################
+# Media & hardware keys
+###############################################################################
+# Volume (PipeWire/PulseAudio via pactl)
+bindsym XF86AudioRaiseVolume exec --no-startup-id pactl set-sink-volume @DEFAULT_SINK@ +2%
+bindsym XF86AudioLowerVolume exec --no-startup-id pactl set-sink-volume @DEFAULT_SINK@ -2%
+bindsym XF86AudioMute        exec --no-startup-id pactl set-sink-mute @DEFAULT_SINK@ toggle
+
+# Brightness (brightnessctl)
+bindsym XF86MonBrightnessUp   exec --no-startup-id brightnessctl set +5%
+bindsym XF86MonBrightnessDown exec --no-startup-id brightnessctl set 5%-
+
+# Media playback (playerctl)
+bindsym XF86AudioPlay  exec --no-startup-id playerctl play-pause
+bindsym XF86AudioNext  exec --no-startup-id playerctl next
+bindsym XF86AudioPrev  exec --no-startup-id playerctl previous
+
+###############################################################################
+# Bar — i3status-rust
+###############################################################################
+bar {
+        status_command i3status-rs ~/.config/i3status-rust/config.toml
+        position top
+        font pango:JetBrainsMono Nerd Font 9
+        strip_workspace_numbers false
+
+        colors {
+                background #191724
+                statusline #e0def4
+                separator  #6e6a86
+
+                #                  border  bg      text
+                focused_workspace  #c4a7e7 #c4a7e7 #191724
+                active_workspace   #26233a #26233a #e0def4
+                inactive_workspace #191724 #191724 #908caa
+                urgent_workspace   #eb6f92 #eb6f92 #191724
+        }
+}
+
+###############################################################################
+# Autostart
+###############################################################################
+# X11
+exec --no-startup-id setxkbmap us -variant altgr-intl -option caps:escape
+exec --no-startup-id numlockx on
+exec --no-startup-id xrdb -merge ~/.Xresources
+exec --no-startup-id picom
+exec --no-startup-id dunst
+exec --no-startup-id xidlehook \
+  --detect-sleep \
+  --not-when-audio \
+  --not-when-fullscreen \
+  --timer 300 'betterlockscreen -l' '' \
+  --timer 120 'xset dpms force off' '' \
+  --timer 300 'systemctl suspend' ''
+exec --no-startup-id lxqt-policykit-agent
+exec --no-startup-id clipmenud
+exec --no-startup-id autorandr --change
+exec_always --no-startup-id feh --randomize --bg-fill ~/Pictures/* --no-fehbg
+exec --no-startup-id betterlockscreen -u ~/Pictures/*
+
+# Scratchpad
+exec --no-startup-id alacritty --class dropdown
+
+# Apps
+exec --no-startup-id easyeffects --gapplication-service
+exec --no-startup-id nm-applet
+# exec --no-startup-id zen-twilight
+# exec --no-startup-id thunderbird
+# exec --no-startup-id discord
+# exec --no-startup-id slack
+# exec --no-startup-id gtk-launch msedge-cifhbcnohmdccbgoicgdjpfamggdegmo-Default
+# exec --no-startup-id gtk-launch msedge-pkooggnaalmfkidjmlhoelhdllpphaga-Default
+# exec --no-startup-id fusuma

+ 61 - 0
i3/.config/i3status-rust/config.toml

@@ -0,0 +1,61 @@
+[icons]
+icons = "material-nf"
+
+[theme]
+# theme = "plain"
+theme = "ctp-mocha"
+
+[theme.overrides]
+idle_bg = "#191724"
+idle_fg = "#e0def4"
+info_bg = "#191724"
+info_fg = "#9ccfd8"
+good_bg = "#191724"
+good_fg = "#e0def4"
+warning_bg = "#191724"
+warning_fg = "#f6c177"
+critical_bg = "#191724"
+critical_fg = "#eb6f92"
+separator = "  "
+separator_bg = "#191724"
+separator_fg = "#6e6a86"
+
+[[block]]
+block = "cpu"
+format = " $icon $utilization"
+interval = 5
+
+[[block]]
+block = "memory"
+format = " $icon $mem_used_percents"
+
+[[block]]
+block = "disk_space"
+path = "/"
+format = " $icon $available"
+alert = 10.0
+warning = 20.0
+
+[[block]]
+block = "net"
+format = " $icon $signal_strength $ssid"
+missing_format = ""
+
+[[block]]
+block = "battery"
+
+[[block]]
+block = "sound"
+[[block.click]]
+button = "left"
+cmd = "pavucontrol"
+
+[[block]]
+block = "backlight"
+format = " $icon $brightness"
+missing_format = ""
+
+[[block]]
+block = "time"
+format = " $icon $timestamp.datetime(f:'%a %d %b  %H:%M')"
+interval = 30

+ 243 - 217
local-bin/.local/bin/dev-launcher

@@ -1,218 +1,244 @@
-#!/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
+#!/usr/bin/env python3
+"""i3 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"i3-dev-launcher-cache-{dev_dir_hash}",
+        cache_dir / f"i3-dev-launcher-mru-{dev_dir_hash}",
+    )
+
+
+_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)
+
+    if not no_cache and cache_file.exists():
+        if time.time() - os.path.getmtime(cache_file) < CACHE_MAX_AGE:
+            return [r for r in cache_file.read_text().splitlines() if r]
+
+    if not Path(dev_dir).exists():
+        return []
+
+    if _has_fd():
+        result = subprocess.run(
+            ["fd", "-H", "-t", "d", "^\\.git$", dev_dir, "-d", "3", "-0"],
+            capture_output=True,
+            text=True,
+        )
+    else:
+        result = subprocess.run(
+            ["find", dev_dir, "-maxdepth", "3", "-type", "d", "-name", ".git", "-print0"],
+            capture_output=True,
+            text=True,
+        )
+
+    repos = []
+    if result.returncode == 0:
+        repos = [str(Path(p).parent) for p in result.stdout.strip("\0").split("\0") if p]
+
+    repos = sorted(set(repos))
+    cache_file.parent.mkdir(parents=True, exist_ok=True)
+    cache_file.write_text("\n".join(repos))
+    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 = mru_file.read_text().splitlines() if mru_file.exists() else []
+    mru = [x for x in mru if x != selected_display]
+    mru.insert(0, selected_display)
+    mru_file.parent.mkdir(parents=True, exist_ok=True)
+    mru_file.write_text("\n".join(mru[:MRU_SIZE]))
+
+
+def sort_by_mru(display_list, dev_dir):
+    _, mru_file = get_cache_files(dev_dir)
+    if not mru_file.exists():
+        return sorted(display_list)
+
+    mru = [x for x in mru_file.read_text().splitlines() if x]
+    mru_set = set(mru)
+
+    starred = [f"⭐ {d}" for d in display_list if d in mru_set]
+    rest = [d for d in display_list if d not in mru_set]
+    return sorted(starred) + sorted(rest)
+
+
+def find_window_by_class(class_name):
+    try:
+        result = subprocess.run(
+            ["i3-msg", "-t", "get_tree"],
+            capture_output=True,
+            text=True,
+            check=False,
+        )
+        tree = json.loads(result.stdout)
+
+        def walk(node):
+            wp = node.get("window_properties") or {}
+            if wp.get("class", "").lower() == class_name.lower():
+                return node.get("id")
+            for child in node.get("nodes", []) + node.get("floating_nodes", []):
+                found = walk(child)
+                if found:
+                    return found
+            return None
+
+        return walk(tree)
+    except Exception:
+        return None
+
+
+def focus_or_launch_alacritty(class_name, title, working_dir, session_name):
+    window_id = find_window_by_class(class_name)
+
+    if window_id:
+        subprocess.run(
+            ["i3-msg", f"[con_id={window_id}] focus"],
+            stdout=subprocess.DEVNULL,
+            stderr=subprocess.DEVNULL,
+        )
         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"
+
+    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",
+            "--class",
+            f"{class_name},{class_name}",
+            "--title",
+            title,
+            "--working-directory",
+            working_dir,
+            "-e",
+            "bash",
+            "-c",
+            tmux_cmd,
+        ],
+        stdout=subprocess.DEVNULL,
+        stderr=subprocess.DEVNULL,
+        start_new_session=True,
+    )
+
+
+def run_menu(options):
+    if shutil.which("rofi"):
+        proc = subprocess.Popen(
+            ["rofi", "-dmenu", "-i", "-p", "dev", "-dpi", "1"],
+            stdin=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            text=True,
+        )
+    else:
+        proc = subprocess.Popen(
+            ["dmenu", "-i", "-l", "20", "-p", "dev"],
+            stdin=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            text=True,
+        )
+
+    out, _ = proc.communicate("\n".join(options))
+    return out.strip()
+
+
+def main():
+    parser = argparse.ArgumentParser(description="i3 Dev Launcher")
+    parser.add_argument("dev_dir", nargs="?", default=os.path.expanduser("~/Dev"))
+    parser.add_argument("--no-cache", action="store_true")
+    parser.add_argument("--clear-cache", action="store_true")
+    args = parser.parse_args()
+
+    cache_file, mru_file = get_cache_files(args.dev_dir)
+
+    if args.clear_cache:
+        if cache_file.exists():
+            cache_file.unlink()
+        if mru_file.exists():
+            mru_file.unlink()
+        sys.exit(0)
+
+    repos = find_git_repos(args.dev_dir, args.no_cache)
+    if not repos:
+        sys.exit(1)
+
+    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
+
+    sorted_list = sort_by_mru(display_list, args.dev_dir)
+    selected_display = run_menu(sorted_list)
+
+    if not selected_display:
+        sys.exit(0)
+
+    clean = selected_display.replace("⭐ ", "", 1)
+    selected = display_to_path.get(clean)
+    if not selected or not Path(selected).is_dir():
+        sys.exit(0)
+
+    update_mru(clean, args.dev_dir)
+
+    project_name = get_project_name(selected)
+    session_name = f"dev-{project_name}"
+    class_name = f"com.mzunino.dev.{project_name}"
+
+    focus_or_launch_alacritty(class_name, project_name, selected, session_name)
+
+
+if __name__ == "__main__":
+    main()

+ 95 - 0
local-bin/.local/bin/i3-launch-or-focus

@@ -0,0 +1,95 @@
+#!/usr/bin/env python3
+"""
+Launch an application or focus it if already running in i3.
+Usage: i3-launch-or-focus --class CLASS <command> [args...]
+
+Examples:
+  i3-launch-or-focus --class sdm-connect alacritty -e ~/.local/bin/sdm-ui.sh
+  i3-launch-or-focus --class dropdown alacritty
+"""
+
+import argparse
+import json
+import subprocess
+import sys
+
+
+def find_window(tree, instance=None, title=None):
+    """Recursively search the i3 tree for a window matching criteria."""
+    props = tree.get("window_properties", {})
+
+    if instance and props.get("instance", "").lower() == instance.lower():
+        return tree.get("id")
+    if title and tree.get("name", "") == title:
+        return tree.get("id")
+
+    for node in tree.get("nodes", []) + tree.get("floating_nodes", []):
+        result = find_window(node, instance=instance, title=title)
+        if result:
+            return result
+
+    return None
+
+
+def focus_window(instance=None, title=None):
+    """Focus a window using i3-msg criteria."""
+    if instance:
+        criteria = f'[instance="{instance}"]'
+    elif title:
+        criteria = f'[title="{title}"]'
+    else:
+        return
+    subprocess.run(
+        ["i3-msg", f"{criteria} focus"],
+        stdout=subprocess.DEVNULL,
+        stderr=subprocess.DEVNULL,
+    )
+
+
+def launch_command(command, args):
+    """Launch a command, replacing this process."""
+    import os
+
+    os.execvp(command, [command] + args)
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        description="Launch an application or focus it if already running in i3"
+    )
+    parser.add_argument("command", help="Command to run")
+    parser.add_argument("--class", dest="class_name", help="Window instance name")
+    parser.add_argument("--title", help="Window title")
+    parser.add_argument(
+        "args", nargs=argparse.REMAINDER, help="Additional arguments for command"
+    )
+
+    args = parser.parse_args()
+
+    try:
+        result = subprocess.run(
+            ["i3-msg", "-t", "get_tree"],
+            capture_output=True,
+            text=True,
+            check=False,
+        )
+        tree = json.loads(result.stdout)
+    except (json.JSONDecodeError, subprocess.SubprocessError):
+        tree = {}
+
+    window_id = find_window(tree, instance=args.class_name, title=args.title)
+
+    if window_id:
+        focus_window(instance=args.class_name, title=args.title)
+    else:
+        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)
+        launch_command(args.command, cmd_args)
+
+
+if __name__ == "__main__":
+    main()

+ 4 - 0
zsh/.zshenv

@@ -27,3 +27,7 @@ export WGETRC="$XDG_CONFIG_HOME/wgetrc"
 export JAVA_HOME="/usr/lib/jvm/default"
 export PATH="$HOME/.local/bin:$PATH"
 
+# Scaling
+export QT_AUTO_SCREEN_SCALE_FACTOR=1
+export XCURSOR_SIZE=24
+