mirror of
https://github.com/prdlk/dotfiles.git
synced 2026-11-02 11:32:42 +00:00
feat(cli): add mtime-sort utility, enhance pj task runner, and remove legacy executable_gho
This commit is contained in:
1 parent
1d1f0be08b
commit
f033b29a7d
3 files changed
+121
-115
No files matched your search
@@ -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
|
||||
Executable
+24
@@ -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;
|
||||
' -- "$@"
|
||||
+97
-84
@@ -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"
|
||||
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##*/}")
|
||||
else
|
||||
display=$(printf '%-8s %s' "$tag" "$target")
|
||||
fi
|
||||
lines+=("$display"$'\t'"$r")
|
||||
done
|
||||
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 %s' "$tag" "$target" "${root##*/}")
|
||||
else
|
||||
display=$(printf '%-8s %s' "$tag" "$target")
|
||||
fi
|
||||
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"
|
||||
Reference in new issue
Block a user