dotfiles/packages/active-path/active-path
Christopher Mühl 9e49bd830d
feat: add compositor-agnostic window info utilities
- active-window: Get focused window info (class, pid) for Niri/Hyprland
- active-path: Get CWD of focused terminal window

These utilities abstract compositor-specific APIs for use in scripts.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-28 01:25:45 +01:00

116 lines
3.3 KiB
Bash

#!/usr/bin/env bash
# active-path <pid>
#
# Given a PID (e.g. from active-window), output the active working directory
# or project root for that process. Handles:
# - VSCode/Electron: parses --folder-uri from the process tree
# - Terminals (kitty, etc.): finds the deepest meaningful cwd in the tree
# - Fallback: own cwd
set -o errexit
set -o nounset
set -o pipefail
PID="${1:?Usage: active-path <pid>}"
PROJECT_MARKERS=(.git Cargo.toml package.json Gemfile pyproject.toml go.mod flake.nix)
# Walk up from a path looking for a project root marker.
# Returns the marker directory, or the original path if nothing is found.
find_project_root() {
local dir="$1"
local d="$dir"
while [[ "$d" != "/" && -n "$d" ]]; do
for marker in "${PROJECT_MARKERS[@]}"; do
[[ -e "$d/$marker" ]] && { printf '%s\n' "$d"; return 0; }
done
d="$(dirname "$d")"
done
printf '%s\n' "$dir"
}
# Decode a percent-encoded URI path component (file:/// paths are rarely
# exotic, but spaces and non-ASCII chars do occur).
url_decode() {
printf '%b' "${1//%/\\x}"
}
# Read null-terminated argv of a pid and look for --folder-uri.
# Prints the decoded path and returns 0 if found, else returns 1.
folder_uri_from_cmdline() {
local pid="$1"
local prev=""
while IFS= read -r -d $'\0' arg; do
case "$arg" in
--folder-uri=file://*)
url_decode "${arg#--folder-uri=file://}"
return 0
;;
file://*)
if [[ "$prev" == "--folder-uri" ]]; then
url_decode "${arg#file://}"
return 0
fi
;;
esac
prev="$arg"
done < "/proc/$pid/cmdline" 2>/dev/null
return 1
}
# Collect PIDs of all processes in the subtree rooted at $1.
all_pids() {
local pid="$1"
printf '%s\n' "$pid"
for child in $(pgrep -P "$pid" 2>/dev/null); do
all_pids "$child"
done
}
# Given a list of PIDs, return the most specific (longest) cwd that lives
# under the user's home directory. Falls back to any non-root, non-proc cwd.
best_cwd_from_pids() {
local best=""
for pid in "$@"; do
local cwd
cwd=$(readlink -f "/proc/$pid/cwd" 2>/dev/null) || continue
[[ "$cwd" == "/" || "$cwd" == "/root" || "$cwd" == "${HOME}" ]] && continue
[[ "$cwd" =~ ^/(proc|sys|run|dev) ]] && continue
# Prefer paths under home; among those prefer longer (more specific) ones
if [[ "$cwd" == "${HOME}"/* ]]; then
if [[ -z "$best" || "${#cwd}" -gt "${#best}" ]]; then
best="$cwd"
fi
elif [[ -z "$best" ]]; then
best="$cwd"
fi
done
[[ -n "$best" ]] && printf '%s\n' "$best"
}
# --- Main ---
readarray -t TREE < <(all_pids "$PID")
# 1. VSCode / Electron: search entire process tree for --folder-uri
for pid in "${TREE[@]}"; do
if path=$(folder_uri_from_cmdline "$pid" 2>/dev/null); then
find_project_root "$path"
exit 0
fi
done
# 2. Terminal / generic: find the best cwd among all processes in the tree
# (prefers the shell or its running child over the terminal emulator itself,
# because terminal emulators typically sit at $HOME while the shell has moved)
if cwd=$(best_cwd_from_pids "${TREE[@]}"); then
find_project_root "$cwd"
exit 0
fi
# 3. Absolute fallback: own cwd, even if it's /
own=$(readlink -f "/proc/$PID/cwd" 2>/dev/null || true)
[[ -n "$own" ]] && find_project_root "$own" && exit 0
exit 1