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>
This commit is contained in:
parent
8feb3957a2
commit
9e49bd830d
3 changed files with 141 additions and 0 deletions
116
packages/active-path/active-path
Normal file
116
packages/active-path/active-path
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
#!/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
|
||||||
6
packages/active-path/package.nix
Normal file
6
packages/active-path/package.nix
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{pkgs, ...}:
|
||||||
|
pkgs.writeShellApplication {
|
||||||
|
name = "active-path";
|
||||||
|
runtimeInputs = [pkgs.procps];
|
||||||
|
text = builtins.readFile ./active-path;
|
||||||
|
}
|
||||||
19
packages/active-window/package.nix
Normal file
19
packages/active-window/package.nix
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
{pkgs, ...}:
|
||||||
|
pkgs.writeShellApplication {
|
||||||
|
name = "active-window";
|
||||||
|
runtimeInputs = [pkgs.jq];
|
||||||
|
text = ''
|
||||||
|
if [[ -n "''${NIRI_SOCKET:-}" ]]; then
|
||||||
|
FOCUSED_ID=$(niri msg --json focused-window | jq '.id')
|
||||||
|
niri msg --json windows \
|
||||||
|
| jq --argjson id "$FOCUSED_ID" \
|
||||||
|
'[.[] | select(.id == $id)] | first
|
||||||
|
| {compositor: "niri", class: .app_id, pid: .pid}'
|
||||||
|
elif [[ -n "''${HYPRLAND_INSTANCE_SIGNATURE:-}" ]]; then
|
||||||
|
hyprctl activewindow -j \
|
||||||
|
| jq '{compositor: "hyprland", class: .class, pid: .pid}'
|
||||||
|
else
|
||||||
|
printf '{"compositor":"unknown","class":null,"pid":null}\n'
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue