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