From 9e49bd830db5f6f3bed9fbb4451595e3dba17c24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=BChl?= Date: Sat, 28 Feb 2026 01:25:45 +0100 Subject: [PATCH] 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 --- packages/active-path/active-path | 116 +++++++++++++++++++++++++++++ packages/active-path/package.nix | 6 ++ packages/active-window/package.nix | 19 +++++ 3 files changed, 141 insertions(+) create mode 100644 packages/active-path/active-path create mode 100644 packages/active-path/package.nix create mode 100644 packages/active-window/package.nix diff --git a/packages/active-path/active-path b/packages/active-path/active-path new file mode 100644 index 0000000..52f6d43 --- /dev/null +++ b/packages/active-path/active-path @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +# active-path +# +# 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 }" + +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 diff --git a/packages/active-path/package.nix b/packages/active-path/package.nix new file mode 100644 index 0000000..acd1b31 --- /dev/null +++ b/packages/active-path/package.nix @@ -0,0 +1,6 @@ +{pkgs, ...}: +pkgs.writeShellApplication { + name = "active-path"; + runtimeInputs = [pkgs.procps]; + text = builtins.readFile ./active-path; +} diff --git a/packages/active-window/package.nix b/packages/active-window/package.nix new file mode 100644 index 0000000..3c97263 --- /dev/null +++ b/packages/active-window/package.nix @@ -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 + ''; +}