mirror of
https://github.com/prdlk/dotfiles.git
synced 2026-11-04 11:47:37 +00:00
feat(cli): add scratchpad toggle and exclude scratch terminal from cycle
This commit is contained in:
1 parent
a507b25c85
commit
4f9c36cb79
3 files changed
+448
-58
No files matched your search
@@ -22,12 +22,16 @@ declare -A KITTY_CMDS=(
|
|||||||
# --- Hyprland helpers --------------------------------------------------------
|
# --- Hyprland helpers --------------------------------------------------------
|
||||||
|
|
||||||
# Print the addresses of all windows whose class or initialClass matches the
|
# Print the addresses of all windows whose class or initialClass matches the
|
||||||
# regex in $1. Windows are sorted by workspace, then by address, so the cycle
|
# regex in $1. An optional regex in $2 excludes matches again — used to keep
|
||||||
# order stays the same between calls.
|
# scratch windows out of a cycle. Windows are sorted by workspace, then by
|
||||||
|
# address, so the cycle order stays the same between calls.
|
||||||
addresses_matching() {
|
addresses_matching() {
|
||||||
hyprctl clients -j | jq -r --arg re "$1" '
|
hyprctl clients -j | jq -r --arg re "$1" --arg not "${2:-}" '
|
||||||
[ .[]
|
[ .[]
|
||||||
| select(((.class // "") | test($re; "i")) or ((.initialClass // "") | test($re; "i")))
|
| select(((.class // "") | test($re; "i")) or ((.initialClass // "") | test($re; "i")))
|
||||||
|
| select($not == "" or (
|
||||||
|
(((.class // "") | test($not; "i")) or ((.initialClass // "") | test($not; "i"))) | not
|
||||||
|
))
|
||||||
]
|
]
|
||||||
| sort_by(.workspace.id, .address)
|
| sort_by(.workspace.id, .address)
|
||||||
| .[].address'
|
| .[].address'
|
||||||
@@ -125,8 +129,9 @@ elif [[ $APP == "brave" ]]; then
|
|||||||
DEFAULT_SPAWN="brave-origin-nightly"
|
DEFAULT_SPAWN="brave-origin-nightly"
|
||||||
|
|
||||||
elif [[ $APP == "terminal" ]]; then
|
elif [[ $APP == "terminal" ]]; then
|
||||||
# Cycle all terminals.
|
# Cycle all terminals. kitty-herdr is the SUPER + A scratch terminal
|
||||||
INSTANCES=($(addresses_matching '(kitty|alacritty|ghostty|foot|wezterm)'))
|
# (omarchy-toggle-scratch owns it), so it stays out of the rotation.
|
||||||
|
INSTANCES=($(addresses_matching '(kitty|alacritty|ghostty|foot|wezterm)' '^kitty-herdr$'))
|
||||||
DEFAULT_LAUNCHER="omarchy-launch-terminal"
|
DEFAULT_LAUNCHER="omarchy-launch-terminal"
|
||||||
|
|
||||||
elif [[ $APP == "ai" ]]; then
|
elif [[ $APP == "ai" ]]; then
|
||||||
|
|||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:summary=Toggle the scratchpad, starting the kitty/herdr scratch terminal when it holds no windows.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
NAME="scratchpad"
|
||||||
|
WORKSPACE="special:$NAME"
|
||||||
|
CLASS="kitty-herdr"
|
||||||
|
LAUNCH="kitty --class=$CLASS --config $HOME/.config/kitty/herdr.conf"
|
||||||
|
|
||||||
|
# Windows living on the scratchpad, whether it is shown or hidden.
|
||||||
|
scratch_windows() {
|
||||||
|
hyprctl clients -j | jq --arg ws "$WORKSPACE" '[.[] | select(.workspace.name == $ws)] | length'
|
||||||
|
}
|
||||||
|
|
||||||
|
# True when some monitor currently displays the scratchpad.
|
||||||
|
scratch_visible() {
|
||||||
|
hyprctl monitors -j | jq -e --arg ws "$WORKSPACE" 'any(.[]; .specialWorkspace.name == $ws)' >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Hyprland's lua config takes lua expressions on `hyprctl dispatch`; the
|
||||||
|
# classic dispatcher is the pre-lua fallback (same shape as omarchy-cycle).
|
||||||
|
toggle() {
|
||||||
|
hyprctl dispatch "hl.dsp.workspace.toggle_special(\"$NAME\")" >/dev/null 2>&1 ||
|
||||||
|
hyprctl dispatch togglespecialworkspace "$NAME" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($(scratch_windows) > 0)); then
|
||||||
|
toggle
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Empty scratchpad: there is nothing to toggle into view, so start the herdr
|
||||||
|
# terminal instead. hypr/hyprland.lua pins kitty-herdr to the scratchpad, so
|
||||||
|
# only the reveal is left here — and deciding it after the window lands keeps
|
||||||
|
# this correct whether or not the rule already showed the workspace.
|
||||||
|
setsid uwsm-app -- $LAUNCH >/dev/null 2>&1 &
|
||||||
|
|
||||||
|
for _ in {1..40}; do
|
||||||
|
sleep 0.1
|
||||||
|
(($(scratch_windows) > 0)) && break
|
||||||
|
done
|
||||||
|
|
||||||
|
scratch_visible || toggle
|
||||||
+393
-53
@@ -1,71 +1,411 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# fzf package.json script runner with auto-detected package manager.
|
# pj — fzf task runner across every task source in the project.
|
||||||
|
#
|
||||||
|
# Sources: package.json scripts, Makefile targets, justfile recipes, mise
|
||||||
|
# tasks, devbox scripts, and pyproject.toml task tables (poe, pdm, rye,
|
||||||
|
# taskipy, hatch envs, console scripts).
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
# Internal: `pj --preview KIND TARGET ROOT FILE` renders the fzf preview.
|
||||||
|
|
||||||
# ── Locate nearest package.json (walk up) ──────────────────────────
|
have() { command -v "$1" >/dev/null 2>&1; }
|
||||||
dir=$PWD
|
|
||||||
pkg=""
|
# Resolve this script's path so the fzf preview can re-enter it.
|
||||||
while :; do
|
self=$0
|
||||||
if [ -f "$dir/package.json" ]; then
|
case $self in
|
||||||
pkg="$dir/package.json"
|
*/*) ;;
|
||||||
break
|
*) 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+.
|
||||||
|
py_toml() {
|
||||||
|
python3 - "$@" <<'PY' 2>/dev/null
|
||||||
|
import json, sys, tomllib
|
||||||
|
|
||||||
|
mode, path = sys.argv[1], sys.argv[2]
|
||||||
|
try:
|
||||||
|
with open(path, "rb") as fh:
|
||||||
|
data = tomllib.load(fh)
|
||||||
|
except Exception:
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
def table(obj, *keys):
|
||||||
|
for key in keys:
|
||||||
|
if not isinstance(obj, dict):
|
||||||
|
return {}
|
||||||
|
obj = obj.get(key) or {}
|
||||||
|
return obj if isinstance(obj, dict) else {}
|
||||||
|
|
||||||
|
|
||||||
|
def tasks():
|
||||||
|
"""(tool, target, body) for every supported pyproject task table."""
|
||||||
|
out = []
|
||||||
|
for tool, tbl in (
|
||||||
|
("poe", table(data, "tool", "poe", "tasks")),
|
||||||
|
("pdm", table(data, "tool", "pdm", "scripts")),
|
||||||
|
("rye", table(data, "tool", "rye", "scripts")),
|
||||||
|
("taskipy", table(data, "tool", "taskipy", "tasks")),
|
||||||
|
):
|
||||||
|
for name, body in tbl.items():
|
||||||
|
if tool == "pdm" and (name.startswith("_") or name.startswith("post_")):
|
||||||
|
continue # pdm's shared/hook keys are not runnable targets
|
||||||
|
out.append((tool, name, body))
|
||||||
|
|
||||||
|
for env, cfg in table(data, "tool", "hatch", "envs").items():
|
||||||
|
for name, body in table(cfg, "scripts").items():
|
||||||
|
out.append(("hatch", f"{env}:{name}", body))
|
||||||
|
|
||||||
|
seen = set()
|
||||||
|
for tbl in (table(data, "tool", "poetry", "scripts"), table(data, "project", "scripts")):
|
||||||
|
for name, body in tbl.items():
|
||||||
|
if name in seen:
|
||||||
|
continue
|
||||||
|
seen.add(name)
|
||||||
|
out.append(("script", name, body))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def render(body):
|
||||||
|
if isinstance(body, str):
|
||||||
|
return body
|
||||||
|
if isinstance(body, list):
|
||||||
|
return "\n".join(x if isinstance(x, str) else json.dumps(x) for x in body)
|
||||||
|
if isinstance(body, dict):
|
||||||
|
for key in ("cmd", "shell", "script", "expr", "ref"):
|
||||||
|
if isinstance(body.get(key), str):
|
||||||
|
return body[key]
|
||||||
|
if isinstance(body.get("sequence"), list):
|
||||||
|
return render(body["sequence"])
|
||||||
|
return json.dumps(body, indent=2)
|
||||||
|
return str(body)
|
||||||
|
|
||||||
|
|
||||||
|
if mode == "list":
|
||||||
|
for tool, name, _ in tasks():
|
||||||
|
print(f"{tool}\t{name}")
|
||||||
|
else:
|
||||||
|
target, tool = sys.argv[3], sys.argv[4]
|
||||||
|
for t, name, body in tasks():
|
||||||
|
if name == target and t == tool:
|
||||||
|
print(render(body))
|
||||||
|
break
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── fzf preview ────────────────────────────────────────────────────
|
||||||
|
if [ "${1-}" = --preview ]; then
|
||||||
|
kind=${2-} target=${3-} root=${4-} file=${5-}
|
||||||
|
cd "$root" 2>/dev/null || exit 0
|
||||||
|
case $kind in
|
||||||
|
npm)
|
||||||
|
jq -r --arg s "$target" '.scripts[$s] // "(no such script)"' "$file"
|
||||||
|
;;
|
||||||
|
make)
|
||||||
|
awk -v t="$target" '
|
||||||
|
!hit && /^[^\t]/ {
|
||||||
|
ci = index($0, ":")
|
||||||
|
if (ci > 0) {
|
||||||
|
rest = substr($0, ci)
|
||||||
|
if (rest !~ /^::?=/) {
|
||||||
|
n = split(substr($0, 1, ci - 1), p, /[ \t]+/)
|
||||||
|
for (i = 1; i <= n; i++)
|
||||||
|
if (p[i] == t) { hit = 1; print; next }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hit && (/^\t/ || /^ +[^ ]/) { print; next }
|
||||||
|
hit { exit }
|
||||||
|
END { if (!hit) print "(no recipe — declared target only)" }
|
||||||
|
' "$file"
|
||||||
|
;;
|
||||||
|
just)
|
||||||
|
just --justfile "$file" --working-directory "$root" --show "$target" 2>&1
|
||||||
|
;;
|
||||||
|
mise)
|
||||||
|
mise tasks info "$target" 2>&1
|
||||||
|
;;
|
||||||
|
devbox)
|
||||||
|
jq -r --arg s "$target" \
|
||||||
|
'.shell.scripts[$s] | if type == "array" then join("\n") else (. // "(no such script)") end' \
|
||||||
|
"$file"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
py_toml show "$file" "$target" "$kind"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Collect targets ────────────────────────────────────────────────
|
||||||
|
mise_config() {
|
||||||
|
local d=$1 c
|
||||||
|
for c in mise.toml .mise.toml mise/config.toml .mise/config.toml .config/mise/config.toml; do
|
||||||
|
[ -f "$d/$c" ] && { echo "$d/$c"; return 0; }
|
||||||
|
done
|
||||||
|
for c in mise-tasks .mise/tasks mise/tasks; do
|
||||||
|
[ -d "$d/$c" ] && { echo "$d/$c"; return 0; }
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Record layout (tab separated): kind, target, root, manifest, runner.
|
||||||
|
recs=()
|
||||||
|
add() { recs+=("$1"$'\t'"$2"$'\t'"$3"$'\t'"$4"$'\t'"$5"); }
|
||||||
|
|
||||||
|
collect_npm() {
|
||||||
|
local dir=$1 pm d s
|
||||||
|
local pkg=$dir/package.json
|
||||||
|
[ -f "$pkg" ] || return
|
||||||
|
|
||||||
|
# Priority: packageManager field (corepack) → lockfiles (walking up past
|
||||||
|
# $dir to catch monorepo-root locks) → npm fallback
|
||||||
|
pm=$(jq -r '.packageManager // empty' "$pkg" 2>/dev/null)
|
||||||
|
pm=${pm%%@*}
|
||||||
|
if [ -z "$pm" ]; then
|
||||||
|
d=$dir
|
||||||
|
while [ -z "$pm" ]; do
|
||||||
|
if [ -f "$d/bun.lockb" ] || [ -f "$d/bun.lock" ]; then
|
||||||
|
pm=bun
|
||||||
|
elif [ -f "$d/pnpm-lock.yaml" ]; then
|
||||||
|
pm=pnpm
|
||||||
|
elif [ -f "$d/yarn.lock" ]; then
|
||||||
|
pm=yarn
|
||||||
|
elif [ -f "$d/package-lock.json" ]; then
|
||||||
|
pm=npm
|
||||||
|
fi
|
||||||
|
[ "$d" = / ] && break
|
||||||
|
d=$(dirname "$d")
|
||||||
|
done
|
||||||
|
[ -n "$pm" ] || pm=npm
|
||||||
fi
|
fi
|
||||||
[ "$dir" = / ] && break
|
have "$pm" || pm=npm
|
||||||
dir=$(dirname "$dir")
|
have "$pm" || return
|
||||||
|
|
||||||
|
while IFS= read -r s; do
|
||||||
|
[ -n "$s" ] && add npm "$s" "$dir" "$pkg" "$pm run"
|
||||||
|
done < <(jq -r '.scripts // {} | keys[]' "$pkg" 2>/dev/null)
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_make() {
|
||||||
|
local dir=$1 f="" c t
|
||||||
|
have make || return
|
||||||
|
for c in GNUmakefile makefile Makefile; do
|
||||||
|
[ -f "$dir/$c" ] && { f=$dir/$c; break; }
|
||||||
|
done
|
||||||
|
[ -n "$f" ] || return
|
||||||
|
|
||||||
|
# Text parse: explicit rules plus .PHONY operands (targets built from
|
||||||
|
# variables only show up in .PHONY). `make -p` is not used — it expands
|
||||||
|
# $(shell ...) and would run code just to list names.
|
||||||
|
local -A seen=()
|
||||||
|
while IFS= read -r t; do
|
||||||
|
[ -n "$t" ] && [ -z "${seen[$t]-}" ] || continue
|
||||||
|
seen[$t]=1
|
||||||
|
add make "$t" "$dir" "$f" make
|
||||||
|
done < <(awk '
|
||||||
|
/^\t/ { next }
|
||||||
|
/^[ \t]*#/ { next }
|
||||||
|
{
|
||||||
|
line = $0
|
||||||
|
sub(/#.*/, "", line)
|
||||||
|
ci = index(line, ":")
|
||||||
|
if (ci == 0) next
|
||||||
|
lhs = substr(line, 1, ci - 1)
|
||||||
|
rest = substr(line, ci + 1)
|
||||||
|
if (substr(rest, 1, 1) == "=") next # := assignment
|
||||||
|
if (lhs ~ /[$%=]/) next # variables, pattern rules
|
||||||
|
n = split(lhs, parts, /[ \t]+/)
|
||||||
|
if (parts[1] == ".PHONY") {
|
||||||
|
sub(/^:+/, "", rest)
|
||||||
|
n = split(rest, parts, /[ \t]+/)
|
||||||
|
for (i = 1; i <= n; i++)
|
||||||
|
if (parts[i] != "" && parts[i] !~ /[$%]/) print parts[i]
|
||||||
|
next
|
||||||
|
}
|
||||||
|
for (i = 1; i <= n; i++)
|
||||||
|
if (parts[i] != "" && parts[i] !~ /^\./) print parts[i]
|
||||||
|
}
|
||||||
|
' "$f")
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_just() {
|
||||||
|
local dir=$1 f="" c t
|
||||||
|
have just || return
|
||||||
|
for c in justfile .justfile Justfile JUSTFILE; do
|
||||||
|
[ -f "$dir/$c" ] && { f=$dir/$c; break; }
|
||||||
|
done
|
||||||
|
[ -n "$f" ] || return
|
||||||
|
|
||||||
|
for t in $(just --justfile "$f" --working-directory "$dir" --summary --unsorted 2>/dev/null); do
|
||||||
|
add just "$t" "$dir" "$f" just
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_mise() {
|
||||||
|
local dir=$1 cfg t
|
||||||
|
have mise || return
|
||||||
|
cfg=$(mise_config "$dir") || return
|
||||||
|
|
||||||
|
# mise also inherits parent-directory configs; filter to tasks defined
|
||||||
|
# under $dir so parents are not duplicated as we walk up.
|
||||||
|
while IFS= read -r t; do
|
||||||
|
[ -n "$t" ] && add mise "$t" "$dir" "$cfg" "mise run"
|
||||||
|
done < <(mise -C "$dir" tasks ls --json 2>/dev/null |
|
||||||
|
jq -r --arg dir "$dir/" '
|
||||||
|
.[]
|
||||||
|
| select((.hide // false) | not)
|
||||||
|
| select((.source // "") | startswith($dir))
|
||||||
|
| .name' 2>/dev/null)
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_devbox() {
|
||||||
|
local dir=$1 s
|
||||||
|
local f=$dir/devbox.json
|
||||||
|
[ -f "$f" ] || return
|
||||||
|
have devbox || return
|
||||||
|
|
||||||
|
while IFS= read -r s; do
|
||||||
|
[ -n "$s" ] && add devbox "$s" "$dir" "$f" "devbox run"
|
||||||
|
done < <(jq -r '.shell.scripts // {} | keys[]' "$f" 2>/dev/null)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Runner prefix for a pyproject task table, or empty when nothing can run it.
|
||||||
|
# Tools that live in the project venv (poe, taskipy) fall back to `uv run`.
|
||||||
|
py_runner() {
|
||||||
|
case $1 in
|
||||||
|
poe) have poe && echo poe || { have uv && echo "uv run poe"; } ;;
|
||||||
|
taskipy) have task && echo task || { have uv && echo "uv run task"; } ;;
|
||||||
|
pdm) have pdm && echo "pdm run" ;;
|
||||||
|
rye) have rye && echo "rye run" ;;
|
||||||
|
hatch) have hatch && echo "hatch run" ;;
|
||||||
|
script)
|
||||||
|
if have poetry && grep -q '^\[tool\.poetry' "$2"; then
|
||||||
|
echo "poetry run"
|
||||||
|
elif have uv; then
|
||||||
|
echo "uv run"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_py() {
|
||||||
|
local dir=$1 tool target runner
|
||||||
|
local f=$dir/pyproject.toml
|
||||||
|
[ -f "$f" ] || return
|
||||||
|
have python3 || return
|
||||||
|
|
||||||
|
local -A runners=()
|
||||||
|
while IFS=$'\t' read -r tool target; do
|
||||||
|
[ -n "$tool" ] && [ -n "$target" ] || continue
|
||||||
|
if [ -z "${runners[$tool]-}" ]; then
|
||||||
|
runners[$tool]=$(py_runner "$tool" "$f")
|
||||||
|
[ -n "${runners[$tool]}" ] || runners[$tool]="-"
|
||||||
|
fi
|
||||||
|
runner=${runners[$tool]}
|
||||||
|
[ "$runner" = - ] && continue
|
||||||
|
add "$tool" "$target" "$dir" "$f" "$runner"
|
||||||
|
done < <(py_toml list "$f")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Walk cwd upward: to the git toplevel inside a repo, otherwise until a
|
||||||
|
# directory actually yields targets (a source file with no tasks in it must
|
||||||
|
# not hide a parent's Makefile).
|
||||||
|
git_top=$(git rev-parse --show-toplevel 2>/dev/null)
|
||||||
|
d=$PWD
|
||||||
|
while :; do
|
||||||
|
collect_npm "$d"
|
||||||
|
collect_make "$d"
|
||||||
|
collect_just "$d"
|
||||||
|
collect_mise "$d"
|
||||||
|
collect_devbox "$d"
|
||||||
|
collect_py "$d"
|
||||||
|
|
||||||
|
[ "$d" = "$git_top" ] && break
|
||||||
|
[ "$d" = / ] && break
|
||||||
|
[ -z "$git_top" ] && [ ${#recs[@]} -gt 0 ] && break
|
||||||
|
d=$(dirname "$d")
|
||||||
done
|
done
|
||||||
|
|
||||||
if [ -z "$pkg" ]; then
|
if [ ${#recs[@]} -eq 0 ]; then
|
||||||
echo "pj: no package.json found in this tree" >&2
|
[ -n "${PJ_QUIET-}" ] || echo "pj: no task sources found in this tree" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
root=$(dirname "$pkg")
|
|
||||||
|
|
||||||
# ── Detect package manager ─────────────────────────────────────────
|
# ── Pick a target ──────────────────────────────────────────────────
|
||||||
# Priority: packageManager field (corepack) → lockfiles (walking up
|
# Tag each row with its kind (npm rows use the package manager) so `make`,
|
||||||
# past $root to catch monorepo-root locks) → npm fallback
|
# `just` or `poe` are typeable filters in fzf.
|
||||||
pm=$(jq -r '.packageManager // empty' "$pkg")
|
# The directory column only appears when more than one project contributed.
|
||||||
pm=${pm%%@*}
|
primary=${recs[0]#*$'\t'}
|
||||||
|
primary=${primary#*$'\t'}
|
||||||
|
primary=${primary%%$'\t'*}
|
||||||
|
multi=0
|
||||||
|
for r in "${recs[@]}"; do
|
||||||
|
root=${r#*$'\t'}
|
||||||
|
root=${root#*$'\t'}
|
||||||
|
root=${root%%$'\t'*}
|
||||||
|
[ "$root" = "$primary" ] || { multi=1; break; }
|
||||||
|
done
|
||||||
|
|
||||||
if [ -z "$pm" ]; then
|
lines=()
|
||||||
d=$root
|
for r in "${recs[@]}"; do
|
||||||
while [ -z "$pm" ]; do
|
IFS=$'\t' read -r kind target root _file runner <<<"$r"
|
||||||
if [ -f "$d/bun.lockb" ] || [ -f "$d/bun.lock" ]; then
|
case $kind in
|
||||||
pm=bun
|
npm) tag=${runner%% *} ;; # npm | pnpm | yarn | bun
|
||||||
elif [ -f "$d/pnpm-lock.yaml" ]; then
|
script) tag=py ;; # pyproject console entry points
|
||||||
pm=pnpm
|
*) tag=$kind ;;
|
||||||
elif [ -f "$d/yarn.lock" ]; then
|
esac
|
||||||
pm=yarn
|
if [ "$multi" = 1 ]; then
|
||||||
elif [ -f "$d/package-lock.json" ]; then
|
display=$(printf '%-8s %-26s \033[2m%s\033[0m' "$tag" "$target" "${root##*/}")
|
||||||
pm=npm
|
else
|
||||||
fi
|
display=$(printf '%-8s %s' "$tag" "$target")
|
||||||
[ "$d" = / ] && break
|
fi
|
||||||
d=$(dirname "$d")
|
lines+=("$display"$'\t'"$r")
|
||||||
done
|
done
|
||||||
[ -n "$pm" ] || pm=npm
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Pick a script ──────────────────────────────────────────────────
|
selection=$(printf '%s\n' "${lines[@]}" | fzf \
|
||||||
script=$(jq -r '.scripts // {} | keys[]' "$pkg" | fzf \
|
--ansi \
|
||||||
--prompt="$pm run ❯ " \
|
--delimiter='\t' --with-nth=1 \
|
||||||
--header="$root" \
|
--prompt='run ❯ ' \
|
||||||
|
--header="$primary" \
|
||||||
--height=40% --layout=reverse --border \
|
--height=40% --layout=reverse --border \
|
||||||
--preview="jq -r --arg s {} '.scripts[\$s]' \"$pkg\"" \
|
--preview="'$self' --preview {2} {3} {4} {5}" \
|
||||||
--preview-window=up:2:wrap)
|
--preview-window=up:6:wrap)
|
||||||
|
|
||||||
[ -n "$script" ] || exit 0
|
[ -n "$selection" ] || exit 0
|
||||||
|
|
||||||
# ── Run (npm needs `--` to forward extra args) ────────────────────
|
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=("$@")
|
extra=("$@")
|
||||||
if [ "$pm" = npm ] && [ $# -gt 0 ]; then
|
if [ $# -gt 0 ]; then
|
||||||
extra=(-- "$@")
|
case "$kind:$runner" in
|
||||||
|
npm:'npm run' | mise:* | devbox:*) extra=(-- "$@") ;;
|
||||||
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Wrap long-running server scripts in openlogs (live log tail for agents).
|
# shellcheck disable=SC2206 # word splitting is how the runner prefix is built
|
||||||
# Matches dev/start/serve and namespaced variants like dev:web, start:api.
|
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=""
|
wrapper=""
|
||||||
if [[ $script =~ ^(dev|start|serve|watch)(:.*)?$ ]]; then
|
if [[ $target =~ ^(dev|start|serve|watch)(:.*)?$ ]]; then
|
||||||
# Resolve the binary: PATH → mise resolution → mise installs glob
|
# Resolve the binary: PATH → mise resolution → mise installs glob
|
||||||
ol=$(command -v openlogs || true)
|
ol=$(command -v openlogs || true)
|
||||||
if [ -z "$ol" ] && command -v mise >/dev/null 2>&1; then
|
if [ -z "$ol" ] && have mise; then
|
||||||
ol=$(mise which openlogs 2>/dev/null || true)
|
ol=$(mise which openlogs 2>/dev/null || true)
|
||||||
fi
|
fi
|
||||||
if [ -z "$ol" ]; then
|
if [ -z "$ol" ]; then
|
||||||
@@ -82,9 +422,9 @@ if [[ $script =~ ^(dev|start|serve|watch)(:.*)?$ ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "$wrapper" ]; then
|
if [ -n "$wrapper" ]; then
|
||||||
echo "→ $wrapper $pm run $script $*" >&2
|
echo "→ $wrapper ${cmd[*]}" >&2
|
||||||
exec "$wrapper" "$pm" run "$script" "${extra[@]}"
|
exec "$wrapper" "${cmd[@]}"
|
||||||
else
|
else
|
||||||
echo "→ $pm run $script $*" >&2
|
echo "→ ${cmd[*]}" >&2
|
||||||
exec "$pm" run "$script" "${extra[@]}"
|
exec "${cmd[@]}"
|
||||||
fi
|
fi
|
||||||
Reference in new issue
Block a user