feat(cli): add mtime-sort utility, enhance pj task runner, and remove legacy executable_gho

This commit is contained in:
Prad Nukala committed 2026-09-30 11:14:12 -04:00
1 parent 1d1f0be08b
commit f033b29a7d
3 files changed
+112 -106

No files matched your search

-31
View File
@@ -1,31 +0,0 @@
#!/bin/sh
# Pick a ghq repo or an Obsidian vault with fzf (tab toggles the list);
# prints the directory for the shell wrapper to cd into.
vaults="$HOME/Documents/Obsidian/Personal $HOME/Documents/Obsidian/Walletbase $HOME/Documents/Obsidian/Research"
# chezmoi's source tree is a normal checkout of github.com/prdlk/dotfiles, but
# it lives under ~/.local/share, not ghq's root, so `ghq list` never sees it.
# List it under its GitHub name and translate that back to the real path below.
chezmoi_dir="$HOME/.local/share/chezmoi"
chezmoi_entry="github.com/prdlk/dotfiles"
# One command string, used both for the initial list and for fzf's reload
# binding, so the two can't drift.
repos="{ ghq list; [ -d $chezmoi_dir ] && echo $chezmoi_entry; } | sort"
# fzf runs bind actions with $SHELL -c; pin it to sh so the transform
# snippet below is interpreted consistently regardless of login shell.
selection=$(eval "$repos" | SHELL=/bin/sh fzf --prompt="repo: " --tiebreak=end \
--bind "tab:transform:if [ \"\$FZF_PROMPT\" = 'repo: ' ]; then
echo \"change-prompt(vault: )+reload(printf '%s\\n' $vaults)\"
else
echo 'change-prompt(repo: )+reload($repos)'
fi") || exit 1
[ -n "$selection" ] || exit 1
# Vault entries are absolute paths; ghq entries are host/user/repo
case "$selection" in
/*) printf '%s\n' "$selection" ;;
"$chezmoi_entry") printf '%s\n' "$chezmoi_dir" ;;
*) printf '%s\n' "$(ghq root)/$selection" ;;
esac
+24
View File
@@ -0,0 +1,24 @@
#!/bin/sh
# Sort stdin lines newest-first by mtime — the base order of the television
# cables (tv then layers match quality and frecency on top).
#
# The path is each line's last tab-separated field (the whole line when it
# has no tab). With --repo, <path>/.git/index is stat'ed first, so a repo
# ranks by its last git activity rather than its directory mtime. Lines whose
# path does not exist are dropped; ties keep their input order.
#
# Usage: <lines> | mtime-sort [--repo]
exec perl -e '
my $repo = @ARGV && $ARGV[0] eq "--repo";
my ($i, @rows) = (0);
while (my $line = <STDIN>) {
chomp $line;
my $path = (split /\t/, $line)[-1];
next unless defined $path && length $path;
my $mtime;
$mtime = (stat "$path/.git/index")[9] if $repo;
$mtime = (stat $path)[9] unless defined $mtime;
push @rows, [$mtime, $i++, $line] if defined $mtime;
}
print "$_->[2]\n" for sort { $b->[0] <=> $a->[0] || $a->[1] <=> $b->[1] } @rows;
' -- "$@"
+88 -75
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env bash
# pj — fzf task runner across every task source in the project.
# pj — task runner across every task source in the project, picked with the
# television `pj` cable (~/.config/television/cable/pj.toml).
#
# Sources: package.json scripts, Makefile targets, justfile recipes, mise
# tasks, devbox scripts, and pyproject.toml task tables (poe, pdm, rye,
@@ -8,22 +9,21 @@
# Scanning: cwd upward to the git toplevel (inclusive); outside a repo it
# stops at the nearest directory that holds a task source. A source is only
# listed when its runner is actually installed, so the list is never dead.
# Tasks from the most recently modified manifest come first.
#
# Usage: pj [extra args…] extra args are forwarded to the picked task
#
# Env:
# PJ_QUIET=1 suppress the "no task sources" error — the fish down-arrow
# binding uses this, where an empty tree is the normal case.
# PJ_QUIET=1 suppress the "no task sources" error — the down-arrow
# bindings use this, where an empty tree is the normal case.
#
# Internal: `pj --preview KIND TARGET ROOT FILE` renders the fzf preview.
# Cable entry points (tab-separated record fields, see `--list`):
# pj --list print one record per task
# pj --preview KIND TARGET ROOT FILE render a task's body
# pj --run KIND TARGET ROOT RUNNER run a task ($PJ_EXTRA holds extra args)
have() { command -v "$1" >/dev/null 2>&1; }
# Resolve this script's path so the fzf preview can re-enter it.
self=$0
case $self in
*/*) ;;
*) self=$(command -v -- "$self" 2>/dev/null || echo "$self") ;;
esac
# ── pyproject.toml reader ──────────────────────────────────────────
# `list FILE` prints "tool<TAB>target" per task; `show FILE TARGET TOOL`
# prints one task's body. tomllib is stdlib on python 3.11+.
@@ -102,7 +102,7 @@ else:
PY
}
# ── fzf preview ────────────────────────────────────────────────────
# ── Preview ────────────────────────────────────────────────────────
if [ "${1-}" = --preview ]; then
kind=${2-} target=${3-} root=${4-} file=${5-}
cd "$root" 2>/dev/null || exit 0
@@ -146,6 +146,55 @@ if [ "${1-}" = --preview ]; then
exit 0
fi
# ── Run (the cable's enter action) ────────────────────────────────
if [ "${1-}" = --run ]; then
kind=${2-} target=${3-} root=${4-} runner=${5-}
cd "$root" || exit 1
# Extra args from `pj a b …`, shell-quoted into $PJ_EXTRA by the picker.
extra=()
[ -n "${PJ_EXTRA-}" ] && eval "extra=($PJ_EXTRA)"
# Runners that need `--` to forward extra args to the task itself.
if [ ${#extra[@]} -gt 0 ]; then
case "$kind:$runner" in
npm:'npm run' | mise:* | devbox:*) extra=(-- "${extra[@]}") ;;
esac
fi
# shellcheck disable=SC2206 # word splitting is how the runner prefix is built
cmd=(${runner} "$target" "${extra[@]}")
# Wrap long-running server tasks in openlogs (live log tail for agents).
# Matches dev/start/serve/watch and namespaced variants like dev:web.
wrapper=""
if [[ $target =~ ^(dev|start|serve|watch)(:.*)?$ ]]; then
# Resolve the binary: PATH → mise resolution → mise installs glob
ol=$(command -v openlogs || true)
if [ -z "$ol" ] && have mise; then
ol=$(mise which openlogs 2>/dev/null || true)
fi
if [ -z "$ol" ]; then
for cand in "$HOME"/.local/share/mise/installs/node/*/bin/openlogs; do
[ -x "$cand" ] && { ol=$cand; break; }
done
fi
if [ -n "$ol" ] && [ -x "$ol" ]; then
wrapper=$ol
else
echo "pj: openlogs not found, running unwrapped" >&2
fi
fi
if [ -n "$wrapper" ]; then
echo "→ $wrapper ${cmd[*]}" >&2
exec "$wrapper" "${cmd[@]}"
else
echo "→ ${cmd[*]}" >&2
exec "${cmd[@]}"
fi
fi
# ── Collect targets ────────────────────────────────────────────────
mise_config() {
local d=$1 c
@@ -341,10 +390,12 @@ if [ ${#recs[@]} -eq 0 ]; then
exit 1
fi
# ── Pick a target ──────────────────────────────────────────────────
# Tag each row with its kind (npm rows use the package manager) so `make`,
# `just` or `poe` are typeable filters in fzf.
# The directory column only appears when more than one project contributed.
# ── List ───────────────────────────────────────────────────────────
# Record (tab separated): display, kind, target, root, runner, manifest.
# Tag each display with its kind (npm rows use the package manager) so `make`,
# `just` or `poe` are typeable filters. The directory column only appears
# when more than one project contributed. The manifest is the last field so
# mtime-sort ranks tasks by their manifest's mtime.
primary=${recs[0]#*$'\t'}
primary=${primary#*$'\t'}
primary=${primary%%$'\t'*}
@@ -356,75 +407,37 @@ for r in "${recs[@]}"; do
[ "$root" = "$primary" ] || { multi=1; break; }
done
lines=()
for r in "${recs[@]}"; do
IFS=$'\t' read -r kind target root _file runner <<<"$r"
list() {
local r kind target root file runner tag display
for r in "${recs[@]}"; do
IFS=$'\t' read -r kind target root file runner <<<"$r"
case $kind in
npm) tag=${runner%% *} ;; # npm | pnpm | yarn | bun
script) tag=py ;; # pyproject console entry points
*) tag=$kind ;;
esac
if [ "$multi" = 1 ]; then
display=$(printf '%-8s %-26s \033[2m%s\033[0m' "$tag" "$target" "${root##*/}")
display=$(printf '%-8s %-26s %s' "$tag" "$target" "${root##*/}")
else
display=$(printf '%-8s %s' "$tag" "$target")
fi
lines+=("$display"$'\t'"$r")
done
printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$display" "$kind" "$target" "$root" "$runner" "$file"
done | mtime-sort
}
selection=$(printf '%s\n' "${lines[@]}" | fzf \
--ansi \
--delimiter='\t' --with-nth=1 \
--prompt='run ❯ ' \
--header="$primary" \
--height=40% --layout=reverse --border \
--preview="'$self' --preview {2} {3} {4} {5}" \
--preview-window=up:6:wrap)
if [ "${1-}" = --list ]; then
list
exit 0
fi
[ -n "$selection" ] || exit 0
IFS=$'\t' read -r _display kind target root _file runner <<<"$selection"
# ── Run ────────────────────────────────────────────────────────────
cd "$root" || exit 1
# Runners that need `--` to forward extra args to the task itself.
extra=("$@")
# ── Pick ───────────────────────────────────────────────────────────
# The scan already ran, so hand tv the result instead of letting the cable
# re-run `pj --list`; tv's enter action calls `pj --run`.
tmp=$(mktemp "${TMPDIR:-/tmp}/pj.XXXXXX") || exit 1
trap 'rm -f "$tmp"' EXIT
list >"$tmp"
if [ $# -gt 0 ]; then
case "$kind:$runner" in
npm:'npm run' | mise:* | devbox:*) extra=(-- "$@") ;;
esac
fi
# shellcheck disable=SC2206 # word splitting is how the runner prefix is built
cmd=(${runner} "$target" "${extra[@]}")
# Wrap long-running server tasks in openlogs (live log tail for agents).
# Matches dev/start/serve/watch and namespaced variants like dev:web.
wrapper=""
if [[ $target =~ ^(dev|start|serve|watch)(:.*)?$ ]]; then
# Resolve the binary: PATH → mise resolution → mise installs glob
ol=$(command -v openlogs || true)
if [ -z "$ol" ] && have mise; then
ol=$(mise which openlogs 2>/dev/null || true)
fi
if [ -z "$ol" ]; then
for cand in "$HOME"/.local/share/mise/installs/node/*/bin/openlogs; do
[ -x "$cand" ] && { ol=$cand; break; }
done
fi
if [ -n "$ol" ] && [ -x "$ol" ]; then
wrapper=$ol
else
echo "pj: openlogs not found, running unwrapped" >&2
fi
fi
if [ -n "$wrapper" ]; then
echo "→ $wrapper ${cmd[*]}" >&2
exec "$wrapper" "${cmd[@]}"
else
echo "→ ${cmd[*]}" >&2
exec "${cmd[@]}"
PJ_EXTRA=$(printf '%q ' "$@")
export PJ_EXTRA
fi
tv pj --source-command "cat '$tmp'" --input-header "$primary"