Initial import: chezmoi source for CachyOS + Umbriel + Noctalia
Imported from prdlk/dotfiles master (efdd827). Drops the niri, Hyprland/Omarchy, macOS, and Blume docs-site layers; docs/ is plain Markdown.
This commit is contained in:
commit
ee355bcfd5
152 files changed
+11564
No files matched your search
Executable
+11
@@ -0,0 +1,11 @@
|
||||
#!/bin/sh
|
||||
# Choose and launch an AI CLI via gum.
|
||||
choice=$(gum choose --header 'AI CLI' claude opencode codex hermes fractal) || exit 0
|
||||
[ -n "$choice" ] || exit 0
|
||||
|
||||
case "$choice" in
|
||||
claude) exec claude --dangerously-skip-permissions ;;
|
||||
# labels match the binary names, so just run it;
|
||||
# add a dedicated case here if another tool needs flags
|
||||
*) exec "$choice" ;;
|
||||
esac
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
"""atuin-pick: atuin history picker for a kitty split (ctrl+shift+p).
|
||||
|
||||
atuin search -i draws its TUI on the tty and prints the selected command
|
||||
to stdout (Esc prints nothing). Filter to history from the current
|
||||
directory. Hand the selection to an interactive fish via --init-command:
|
||||
it runs inside that shell (aliases/abbr resolve, cd persists) with its
|
||||
output in the split, and the prompt drops down below it.
|
||||
"""
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
|
||||
# kitty's launch env has no ATUIN_SESSION (atuin's shell init never ran), so
|
||||
# seed it here. atuin itself comes from the package manager, on PATH.
|
||||
atuin = "atuin"
|
||||
if "ATUIN_SESSION" not in os.environ:
|
||||
os.environ["ATUIN_SESSION"] = subprocess.run(
|
||||
[atuin, "uuid"], stdout=subprocess.PIPE, text=True
|
||||
).stdout.strip()
|
||||
|
||||
sel = subprocess.run(
|
||||
[atuin, "search", "-i", "--filter-mode", "directory"],
|
||||
stdout=subprocess.PIPE,
|
||||
text=True,
|
||||
).stdout.strip()
|
||||
|
||||
argv = ["fish"]
|
||||
if sel:
|
||||
argv += ["-C", f"echo {shlex.quote('\u276f ' + sel)}; {sel}"]
|
||||
os.execvp("fish", argv)
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
#!/bin/sh
|
||||
# brave-new-tab URL
|
||||
# Open URL in a new tab of the running Brave Origin (its last active window),
|
||||
# or start Brave Origin with it. Detached, so the caller never blocks.
|
||||
# Without a scheme, localhost, *.localhost, *.local, and 127.x get http://;
|
||||
# anything else gets https://.
|
||||
set -eu
|
||||
|
||||
[ $# -eq 1 ] || { echo "usage: brave-new-tab URL" >&2; exit 2; }
|
||||
|
||||
url=$1
|
||||
case $url in
|
||||
*://* | about:*) ;;
|
||||
*)
|
||||
host=${url%%[/?#]*}
|
||||
host=$(printf '%s' "${host%:*}" | tr '[:upper:]' '[:lower:]')
|
||||
case $host in
|
||||
localhost | *.localhost | *.local | 127.*) url="http://$url" ;;
|
||||
*) url="https://$url" ;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
|
||||
exec setsid -f brave-origin --new-tab "$url" >/dev/null 2>&1
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
# Code2Prompt with fuzzy-search template.
|
||||
tpl=$(fd --type f --extension hbs . "$HOME/.config/code2prompt" | fzf) || exit 0
|
||||
[ -n "$tpl" ] || exit 0
|
||||
exec code2prompt --template "$tpl"
|
||||
Executable
+43
@@ -0,0 +1,43 @@
|
||||
#!/bin/sh
|
||||
# Fuzzy-pick a desktop app ID and copy to clipboard.
|
||||
existing_dirs=""
|
||||
for d in /usr/share/applications \
|
||||
/usr/local/share/applications \
|
||||
"$HOME/.local/share/applications" \
|
||||
/var/lib/flatpak/exports/share/applications \
|
||||
"$HOME/.local/share/flatpak/exports/share/applications" \
|
||||
/var/lib/snapd/desktop/applications; do
|
||||
[ -d "$d" ] && existing_dirs="$existing_dirs $d"
|
||||
done
|
||||
|
||||
if [ -z "$existing_dirs" ]; then
|
||||
echo "No application directories found." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# word splitting of $existing_dirs is intended
|
||||
# shellcheck disable=SC2086
|
||||
chosen=$(
|
||||
fd --type f --extension desktop . $existing_dirs \
|
||||
| while IFS= read -r f; do
|
||||
app_id=$(basename "$f" .desktop)
|
||||
name=$(sed -n 's/^Name=//p' "$f" | head -n1)
|
||||
[ -z "$name" ] && name=$app_id
|
||||
printf '%s\t%s\n' "$name" "$app_id"
|
||||
done \
|
||||
| sort -u -t "$(printf '\t')" -k1 \
|
||||
| fzf \
|
||||
--delimiter='\t' \
|
||||
--with-nth=1,2 \
|
||||
--prompt="App ID › " \
|
||||
--height=50% \
|
||||
--layout=reverse \
|
||||
--border=rounded \
|
||||
--info=inline
|
||||
)
|
||||
|
||||
[ -n "$chosen" ] || exit 0
|
||||
|
||||
app_id=$(printf '%s\n' "$chosen" | cut -f2)
|
||||
printf '%s' "$app_id" | wl-copy
|
||||
echo "Copied: $app_id"
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
# Copy all AGENTS.md files to a target directory while preserving structure.
|
||||
out_dir=$1
|
||||
|
||||
if [ -z "$out_dir" ]; then
|
||||
echo "Error: Output directory required." >&2
|
||||
echo "Usage: extract_agents <absolute_or_relative_output_path>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$out_dir"
|
||||
|
||||
# Find and copy preserving relative paths
|
||||
fd -t f -g "AGENTS.md" -x cp --parents "{}" "$out_dir"
|
||||
|
||||
echo "Extraction complete. Check $out_dir."
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#!/bin/sh
|
||||
# Fuzzy find and execute a Fabric pattern.
|
||||
json_file=$HOME/.config/fabric/pattern_explainations.json
|
||||
|
||||
if [ ! -f "$json_file" ]; then
|
||||
printf '\033[31mError: %s not found.\033[0m\n' "$json_file" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# fabric is aliased to fabric-ai in the shells; resolve the real binary here
|
||||
if command -v fabric-ai >/dev/null 2>&1; then
|
||||
fabric_bin=fabric-ai
|
||||
else
|
||||
fabric_bin=fabric
|
||||
fi
|
||||
|
||||
# 1. Safely capture piped input (if any) to a temp file — preserves all
|
||||
# formatting and newlines, avoids variable escaping issues
|
||||
temp_input=$(mktemp)
|
||||
trap 'rm -f "$temp_input"' EXIT INT TERM
|
||||
|
||||
if [ ! -t 0 ]; then
|
||||
cat > "$temp_input"
|
||||
fi
|
||||
|
||||
# 2. Parse JSON to TSV -> fzf (name in list, description in preview)
|
||||
selection=$(jq -r '.[] | "\(.name)\t\(.description)"' "$json_file" \
|
||||
| fzf --delimiter '\t' \
|
||||
--with-nth 1 \
|
||||
--preview 'echo {2}' \
|
||||
--preview-window 'right:60%:wrap' \
|
||||
--prompt 'Pattern > ' \
|
||||
--info inline \
|
||||
--bind 'ctrl-/:toggle-preview')
|
||||
|
||||
# 3. Execute with or without the piped data
|
||||
if [ -n "$selection" ]; then
|
||||
pattern=$(printf '%s\n' "$selection" | cut -f1)
|
||||
if [ -s "$temp_input" ]; then
|
||||
"$fabric_bin" --pattern "$pattern" < "$temp_input"
|
||||
else
|
||||
"$fabric_bin" --pattern "$pattern"
|
||||
fi
|
||||
fi
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
# Discard all git changes with gum confirmation.
|
||||
if gum confirm "Discard all uncommitted changes? This is irreversible."; then
|
||||
git reset --hard HEAD
|
||||
git clean -fd
|
||||
echo "Changes wiped."
|
||||
else
|
||||
echo "Operation aborted."
|
||||
fi
|
||||
Executable
+163
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env bash
|
||||
# gh-sonr-org-migrate
|
||||
# Migrates all repos from orgs matching "sonr" into sonr-io,
|
||||
# prompting via gum for renames on collision, then deletes emptied orgs.
|
||||
#
|
||||
# Requires: gh (authenticated as org owner on all orgs), gum, jq
|
||||
|
||||
TARGET_ORG=sonr-io
|
||||
|
||||
red() { printf '\033[31m%s\033[0m\n' "$*"; }
|
||||
green() { printf '\033[32m%s\033[0m\n' "$*"; }
|
||||
yellow() { printf '\033[33m%s\033[0m\n' "$*"; }
|
||||
cyan() { printf '\033[36m%s\033[0m\n' "$*"; }
|
||||
bcyan() { printf '\033[1;36m%s\033[0m\n' "$*"; }
|
||||
bgreen() { printf '\033[1;32m%s\033[0m\n' "$*"; }
|
||||
|
||||
# ── Preflight ────────────────────────────────────────────────────────────────
|
||||
for bin in gh gum jq; do
|
||||
if ! command -v "$bin" >/dev/null 2>&1; then
|
||||
red "missing dependency: $bin"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if ! gh auth status >/dev/null 2>&1; then
|
||||
red "gh is not authenticated"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Discover source orgs ─────────────────────────────────────────────────────
|
||||
# All orgs the user belongs to whose login contains "sonr", excluding target.
|
||||
mapfile -t source_orgs < <(gh api user/orgs --paginate --jq '.[].login' \
|
||||
| grep 'sonr' | grep -vx "$TARGET_ORG")
|
||||
|
||||
if [ "${#source_orgs[@]}" -eq 0 ]; then
|
||||
echo "No source orgs found. Nothing to do."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cyan "Source orgs:"
|
||||
printf ' %s\n' "${source_orgs[@]}"
|
||||
cyan "Target org:"
|
||||
echo " $TARGET_ORG"
|
||||
echo
|
||||
|
||||
# Let the user confirm / trim the list interactively
|
||||
mapfile -t selected_orgs < <(printf '%s\n' "${source_orgs[@]}" | gum choose --no-limit \
|
||||
--header "Select orgs to migrate into $TARGET_ORG (space to toggle, enter to confirm)")
|
||||
|
||||
if [ "${#selected_orgs[@]}" -eq 0 ]; then
|
||||
echo "Nothing selected. Exiting."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
repo_exists_in_target() {
|
||||
gh api "repos/$TARGET_ORG/$1" --silent >/dev/null 2>&1
|
||||
}
|
||||
|
||||
transfer_repo() {
|
||||
# $1 = source org, $2 = repo name, $3 = (optional) new name
|
||||
local src=$1 repo=$2 new_name=${3:-}
|
||||
local args=(-f "new_owner=$TARGET_ORG")
|
||||
if [ -n "$new_name" ]; then
|
||||
args+=(-f "new_name=$new_name")
|
||||
fi
|
||||
gh api -X POST "repos/$src/$repo/transfer" "${args[@]}" --silent
|
||||
}
|
||||
|
||||
# ── Migration ────────────────────────────────────────────────────────────────
|
||||
failed_transfers=()
|
||||
|
||||
for org in "${selected_orgs[@]}"; do
|
||||
echo
|
||||
bcyan "── $org ──"
|
||||
|
||||
mapfile -t repos < <(gh api "orgs/$org/repos" --paginate --jq '.[].name')
|
||||
|
||||
if [ "${#repos[@]}" -eq 0 ]; then
|
||||
echo " (no repos)"
|
||||
continue
|
||||
fi
|
||||
|
||||
for repo in "${repos[@]}"; do
|
||||
new_name=""
|
||||
|
||||
if repo_exists_in_target "$repo"; then
|
||||
yellow " ⚠ $repo already exists in $TARGET_ORG"
|
||||
while true; do
|
||||
new_name=$(gum input \
|
||||
--header "New name for $org/$repo (exists in $TARGET_ORG)" \
|
||||
--placeholder "$org-$repo" \
|
||||
--value "$org-$repo")
|
||||
if [ -z "$new_name" ]; then
|
||||
echo " skipped $org/$repo"
|
||||
new_name=__SKIP__
|
||||
break
|
||||
fi
|
||||
if repo_exists_in_target "$new_name"; then
|
||||
yellow " $new_name also exists — pick another"
|
||||
else
|
||||
break
|
||||
fi
|
||||
done
|
||||
[ "$new_name" = __SKIP__ ] && continue
|
||||
fi
|
||||
|
||||
label="$org/$repo → $TARGET_ORG/${new_name:-$repo}"
|
||||
if transfer_repo "$org" "$repo" "$new_name"; then
|
||||
green " ✓ $label"
|
||||
else
|
||||
red " ✗ failed: $label"
|
||||
failed_transfers+=("$org/$repo")
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
if [ "${#failed_transfers[@]}" -gt 0 ]; then
|
||||
echo
|
||||
red "Failed transfers (orgs containing these will NOT be deleted):"
|
||||
printf ' %s\n' "${failed_transfers[@]}"
|
||||
fi
|
||||
|
||||
# ── Wait for transfers to settle ─────────────────────────────────────────────
|
||||
# GitHub repo transfers are asynchronous; give them time to drain.
|
||||
echo
|
||||
gum spin --title "Waiting 30s for transfers to settle..." -- sleep 30
|
||||
|
||||
# ── Delete emptied orgs ──────────────────────────────────────────────────────
|
||||
echo
|
||||
for org in "${selected_orgs[@]}"; do
|
||||
# Skip orgs with failed transfers
|
||||
org_had_failure=0
|
||||
for f in ${failed_transfers[@]+"${failed_transfers[@]}"}; do
|
||||
case "$f" in
|
||||
"$org"/*) org_had_failure=1; break ;;
|
||||
esac
|
||||
done
|
||||
if [ "$org_had_failure" -eq 1 ]; then
|
||||
yellow "skipping delete: $org (had failures)"
|
||||
continue
|
||||
fi
|
||||
|
||||
remaining=$(gh api "orgs/$org/repos" --paginate --jq '. | length' | jq -s 'add // 0')
|
||||
|
||||
if [ "$remaining" -gt 0 ]; then
|
||||
yellow "skipping delete: $org still has $remaining repo(s) (transfer pending?)"
|
||||
continue
|
||||
fi
|
||||
|
||||
if gum confirm "Delete empty org $org?"; then
|
||||
if gh api -X DELETE "orgs/$org" --silent; then
|
||||
green "✓ deleted $org"
|
||||
else
|
||||
red "✗ could not delete $org (API delete requires org owner; may need manual deletion in Settings)"
|
||||
fi
|
||||
else
|
||||
echo "kept $org"
|
||||
fi
|
||||
done
|
||||
|
||||
echo
|
||||
bgreen "Done."
|
||||
Executable
+116
@@ -0,0 +1,116 @@
|
||||
#!/bin/sh
|
||||
# ghq get + resolve the on-disk clone path; prints the repo directory.
|
||||
# Shell wrappers (fish/bash/zsh `ghg` functions) cd into the printed path —
|
||||
# a child process can't change the caller's cwd.
|
||||
#
|
||||
# Then sets up a prad.codes mirror when the repo has none (best effort; the
|
||||
# clone never fails over it):
|
||||
# github.com/<owner>/<name> you admin → pull mirror into a prad.codes org
|
||||
# (picked with gum, guessed from the language; esc skips), every 8h
|
||||
# prad.codes/<org>/<name> that isn't itself a mirror → push mirror to
|
||||
# github.com/<you>/<name> (created if missing), on every push and every 8h
|
||||
repo=$1
|
||||
if [ -z "$repo" ]; then
|
||||
echo "usage: ghg-clone <repo>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ghq natively understands https://, ssh://, git@host:…, and host/user/repo.
|
||||
# Keep its output on stderr so stdout stays clean for the wrapper's cd.
|
||||
ghq get "$repo" >&2 || exit
|
||||
|
||||
# Reduce the input to ghq's on-disk shape: <host>/<user>/<repo> —
|
||||
# strip scheme (https://, ssh://, git://), user@ (git@github.com:…),
|
||||
# turn scp host:path into host/path, drop trailing .git.
|
||||
rel=$(printf '%s\n' "$repo" \
|
||||
| sed -E 's#^[[:alnum:]]+://##; s#^[^@/]+@##; s#:#/#; s#\.git$##')
|
||||
|
||||
# bare "user/repo" with no host → default to github.com
|
||||
case "$rel" in
|
||||
*.*/*) ;;
|
||||
*) rel=github.com/$rel ;;
|
||||
esac
|
||||
|
||||
dir=$(ghq root)/$rel
|
||||
if [ ! -d "$dir" ]; then
|
||||
echo "ghg: expected clone at $dir but it does not exist" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
command -v zoxide >/dev/null 2>&1 && zoxide add "$dir"
|
||||
|
||||
# ── Mirrors ───────────────────────────────────────────────────────────────
|
||||
# Everything below talks on stderr; stdout carries only the final path.
|
||||
have() { command -v "$1" >/dev/null 2>&1; }
|
||||
|
||||
# prad.codes orgs are per language; guess one from GitHub's primary language.
|
||||
lang_org() {
|
||||
case $1 in
|
||||
Go) echo go ;;
|
||||
Python | "Jupyter Notebook") echo py ;;
|
||||
Rust) echo rs ;;
|
||||
Dart) echo dart ;;
|
||||
TeX) echo tex ;;
|
||||
JavaScript | TypeScript | Astro | Svelte | Vue | HTML | CSS) echo js ;;
|
||||
Markdown | MDX) echo md ;;
|
||||
*) echo env ;; # Lua, Shell, no code at all (configs, env repos)
|
||||
esac
|
||||
}
|
||||
|
||||
pull_mirror() { # owner name
|
||||
info=$(gh repo view "$1/$2" --json viewerPermission,isPrivate,primaryLanguage \
|
||||
-q '[.viewerPermission, .isPrivate, .primaryLanguage.name // ""] | @tsv' 2>/dev/null) || return 0
|
||||
perm=$(printf '%s\n' "$info" | cut -f1)
|
||||
private=$(printf '%s\n' "$info" | cut -f2)
|
||||
lang=$(printf '%s\n' "$info" | cut -f3)
|
||||
[ "$perm" = ADMIN ] || return 0 # someone else's repo: just a clone
|
||||
# tea api exits 0 even on errors, hence jq -e on every response.
|
||||
tea api "/api/v1/repos/search?q=$2&limit=50" | jq -e --arg u "github.com/$1/$2" \
|
||||
'any(.data[]; (.original_url // "") | ascii_downcase | sub("(\\.git)?/?$"; "") | endswith($u | ascii_downcase))' \
|
||||
>/dev/null && return 0
|
||||
org=$(tea api '/api/v1/orgs?limit=50' | jq -r '.[].username' |
|
||||
gum choose --header "Pull-mirror github.com/$1/$2 into which prad.codes org? (esc skips)" \
|
||||
--selected "$(lang_org "$lang")") || return 0
|
||||
[ -n "$org" ] || return 0
|
||||
echo "→ pull mirror: github.com/$1/$2 → prad.codes/$org/$2 (every 8h)" >&2
|
||||
priv=
|
||||
[ "$private" = true ] && priv=--private
|
||||
# shellcheck disable=SC2086 # $priv is one optional flag
|
||||
tea repos migrate --name "$2" --owner "$org" --clone-url "https://github.com/$1/$2.git" \
|
||||
--service git --mirror --mirror-interval 8h --auth-token "$(gh auth token)" $priv >&2 ||
|
||||
echo "ghg: pull mirror failed" >&2
|
||||
}
|
||||
|
||||
push_mirror() { # org name
|
||||
repo=$(tea api "/api/v1/repos/$1/$2")
|
||||
printf '%s' "$repo" | jq -e '.id and (.mirror | not)' >/dev/null || return 0
|
||||
tea api "/api/v1/repos/$1/$2/push_mirrors" | jq -e 'length == 0' >/dev/null || return 0
|
||||
me=$(gh api user -q .login 2>/dev/null) || return 0
|
||||
if gh repo view "$me/$2" >/dev/null 2>&1; then
|
||||
gum confirm "Push-mirror prad.codes/$1/$2 to the existing github.com/$me/$2? Mirror pushes overwrite it." || return 0
|
||||
else
|
||||
gum confirm "Push-mirror prad.codes/$1/$2 to a new github.com/$me/$2?" || return 0
|
||||
vis=--public
|
||||
[ "$(printf '%s' "$repo" | jq -r .private)" = true ] && vis=--private
|
||||
gh repo create "$me/$2" "$vis" --description "Mirror of https://prad.codes/$1/$2" >&2 || return 0
|
||||
fi
|
||||
if jq -n --arg url "https://github.com/$me/$2.git" --arg user "$me" --arg pass "$(gh auth token)" \
|
||||
'{remote_address: $url, remote_username: $user, remote_password: $pass,
|
||||
interval: "8h0m0s", sync_on_commit: true}' |
|
||||
tea api -d @- "/api/v1/repos/$1/$2/push_mirrors" | jq -e .remote_name >/dev/null; then
|
||||
tea api -X POST "/api/v1/repos/$1/$2/push_mirrors-sync" >/dev/null # first push now
|
||||
echo "→ push mirror: prad.codes/$1/$2 → github.com/$me/$2 (on push + every 8h)" >&2
|
||||
else
|
||||
echo "ghg: push mirror failed" >&2
|
||||
fi
|
||||
}
|
||||
|
||||
if have gh && have tea && have jq && have gum; then
|
||||
owner=${rel#*/} name=${owner#*/} owner=${owner%%/*}
|
||||
case $rel in
|
||||
github.com/*/*) pull_mirror "$owner" "$name" ;;
|
||||
prad.codes/*/*) push_mirror "$owner" "$name" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
printf '%s\n' "$dir"
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
#!/bin/sh
|
||||
# Create a new private repo (prad.codes via tea, or github.com via gh),
|
||||
# clone it with ghq, and print the clone directory for the shell wrapper to cd.
|
||||
repo=$1
|
||||
if [ -z "$repo" ]; then
|
||||
echo "usage: ghn <repo-name>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
host=$(gum choose "prad.codes" "github.com") || exit 1
|
||||
|
||||
if [ "$host" = prad.codes ]; then
|
||||
owner=$(tea organizations list -o simple | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | gum choose) || exit 1
|
||||
[ -n "$owner" ] || exit 1
|
||||
|
||||
tea repos create --name "$repo" --owner "$owner" --private >&2 || exit 1
|
||||
|
||||
ghq get "https://prad.codes/$owner/$repo" >&2 || exit 1
|
||||
printf '%s\n' "$(ghq root)/prad.codes/$owner/$repo"
|
||||
else
|
||||
owner=$(gum choose "prdlk" "wltbs") || exit 1
|
||||
[ -n "$owner" ] || exit 1
|
||||
|
||||
gh repo create "$owner/$repo" --private >&2 || exit 1
|
||||
|
||||
ghq get "github.com/$owner/$repo" >&2 || exit 1
|
||||
printf '%s\n' "$(ghq root)/github.com/$owner/$repo"
|
||||
fi
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
# Ghostty's `command`: every Ghostty window gets its own tmux session, never
|
||||
# a mirror of another window's. Reclaim the oldest detached session (left by
|
||||
# a closed window, or restored by tmux-continuum after a reboot) so closed
|
||||
# windows don't pile up sessions; otherwise start a new one. pj-palette's
|
||||
# background task sessions (@pj_task, named <session>:<task>) are never
|
||||
# reclaimed, and targets use the session id since names may contain ':'.
|
||||
# The first window after boot starts the server, which is what triggers
|
||||
# continuum's restore.
|
||||
# ponytail: two windows opened in the same instant can claim the same detached
|
||||
# session; take a lock (flock) here if that ever bites.
|
||||
|
||||
# A server started here inherits this PATH. Ghostty launched from the desktop
|
||||
# has no shell init, so add mise's shims for tools tmux plugins shell out to
|
||||
# (bun for tmux-palette, fzf, zoxide).
|
||||
[[ -d $HOME/.local/share/mise/shims ]] && PATH="$HOME/.local/share/mise/shims:$PATH"
|
||||
|
||||
oldest=$(tmux list-sessions -f '#{&&:#{==:#{session_attached},0},#{!:#{@pj_task}}}' \
|
||||
-F '#{session_created} #{session_id}' 2>/dev/null | sort -n | head -1 | cut -d' ' -f2)
|
||||
|
||||
[[ -n $oldest ]] && exec tmux attach-session -t "$oldest"
|
||||
exec tmux new-session
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/bin/sh
|
||||
# Aggregate all AGENTS.md files in the current git repo via code2prompt.
|
||||
repo_root=$(git rev-parse --show-toplevel 2>/dev/null)
|
||||
if [ -z "$repo_root" ]; then
|
||||
echo "git-summary: not inside a git repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
template=$HOME/.config/code2prompt/agents-md.hbs
|
||||
if [ ! -f "$template" ]; then
|
||||
echo "git-summary: template not found at $template" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Pass through any extra flags (e.g. --output-file, --tokens)
|
||||
exec code2prompt "$repo_root" \
|
||||
--include "AGENTS.md" \
|
||||
--include "**/AGENTS.md" \
|
||||
--template "$template" \
|
||||
"$@"
|
||||
Executable
+69
@@ -0,0 +1,69 @@
|
||||
#!/bin/sh
|
||||
# Open a git repo's web page in the browser — host-agnostic, so it works for
|
||||
# github.com, prad.codes (Gitea), or any other forge whose web layout is
|
||||
# https://<host>/<owner>/<repo> (all of them, in practice).
|
||||
#
|
||||
# Usage: gop [<remote> | <owner>/<repo> | <url>]
|
||||
# no arg → origin, else upstream, else the first remote of the current repo
|
||||
# bare owner/repo → github.com
|
||||
|
||||
target=$1
|
||||
url=
|
||||
|
||||
if [ -n "$target" ]; then
|
||||
case $target in
|
||||
*://* | *@*:* | */*) url=$target ;; # explicit url / scp / owner/repo
|
||||
*)
|
||||
url=$(git remote get-url "$target" 2>/dev/null) || {
|
||||
echo "gop: no remote named '$target'" >&2
|
||||
exit 1
|
||||
}
|
||||
;;
|
||||
esac
|
||||
else
|
||||
for remote in origin upstream $(git remote 2>/dev/null); do
|
||||
url=$(git remote get-url "$remote" 2>/dev/null) && break
|
||||
url=
|
||||
done
|
||||
if [ -z "$url" ]; then
|
||||
echo "gop: not in a git repo, or it has no remotes" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Reduce any remote form to <host>/<owner>/<repo>. The web page is https
|
||||
# unless the remote itself says plain http (a LAN forge, typically).
|
||||
proto=https
|
||||
case $url in
|
||||
*://*)
|
||||
# scheme form: drop scheme and credentials; drop the port for ssh/git
|
||||
# (their port is not the web port), keep it for http/https.
|
||||
scheme=${url%%://*}
|
||||
rest=${url#*://}
|
||||
rest=${rest#*@}
|
||||
case $scheme in
|
||||
ssh | git)
|
||||
host=${rest%%/*}
|
||||
rest="${host%%:*}/${rest#*/}"
|
||||
;;
|
||||
http) proto=http ;;
|
||||
esac
|
||||
url=$rest
|
||||
;;
|
||||
*@*:*)
|
||||
# scp form: git@host:owner/repo
|
||||
url=$(printf '%s\n' "${url#*@}" | sed 's#:#/#')
|
||||
;;
|
||||
esac
|
||||
url=${url%.git}
|
||||
url=${url%/}
|
||||
case $url in
|
||||
*/*/*) ;; # already host/owner/repo
|
||||
*/*) url=github.com/$url ;;
|
||||
*)
|
||||
echo "gop: could not parse a repo out of: ${target:-$url}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
exec ${BROWSER:-xdg-open} "$proto://$url"
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
# Save every kitty daily session (~/.config/kitty/day_session.py) right before
|
||||
# shutdown/reboot.
|
||||
#
|
||||
# kitty-day-save.service runs this under a logind "delay" inhibitor. When a
|
||||
# shutdown or reboot starts, logind broadcasts PrepareForShutdown(true) and
|
||||
# waits (InhibitDelayMaxSec, 5 s by default) for the lock to be released, which
|
||||
# happens when this script exits. Nothing has been killed yet at that point, so
|
||||
# every kitty still has all of its windows. The flush also stops further saves
|
||||
# for a minute, so windows dying during the shutdown can't shrink the saved day.
|
||||
#
|
||||
# Usage: kitty-day-save hold the lock, wait for a shutdown, flush, exit
|
||||
# kitty-day-save --now flush every running kitty immediately
|
||||
set -u
|
||||
|
||||
case "${1-}" in
|
||||
--now)
|
||||
# Every kitty on kitty.conf listens on @kitty-<pid>; ones that don't own
|
||||
# a day session ignore the flush.
|
||||
for s in $(grep -oE '@kitty-[0-9]+$' /proc/net/unix | sort -u); do
|
||||
timeout 3 kitten @ --to "unix:$s" action kitten day_session.py flush >/dev/null 2>&1 &
|
||||
done
|
||||
wait
|
||||
;;
|
||||
--wait)
|
||||
# grep returns on the first match; gdbus is left to the service's cgroup.
|
||||
grep -m1 -q 'PrepareForShutdown (true' < <(
|
||||
gdbus monitor --system --dest org.freedesktop.login1 --object-path /org/freedesktop/login1
|
||||
) && exec "$0" --now
|
||||
;;
|
||||
*)
|
||||
exec systemd-inhibit --what=shutdown --mode=delay --who=kitty \
|
||||
--why='Saving kitty sessions' "$0" --wait
|
||||
;;
|
||||
esac
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
# Overlay omp on the current kitty window with a livediff split beside it.
|
||||
#
|
||||
# kitty appends -PID to config-file listen_on sockets, so a hardcoded
|
||||
# --to unix:@kitty-local never matches. kitty @ resolves the real
|
||||
# address from $KITTY_LISTEN_ON on its own.
|
||||
if [ -z "${KITTY_LISTEN_ON:-}" ]; then
|
||||
echo "launch_omp_livediff_overlay: no kitty remote-control socket (KITTY_LISTEN_ON unset)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# omp overlays the current window; capture its window id
|
||||
wid=$(kitty @ launch --type=overlay-main --cwd=current omp) || exit
|
||||
|
||||
# livediff side by side in the same tab; keep focus on omp
|
||||
exec kitty @ launch --match "id:$wid" --location=vsplit --keep-focus --cwd=current livediff
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/bin/sh
|
||||
# fzf-pick LeetCode solution files and run /leetcode-eli5 on each via omp.
|
||||
root=${1:-.}
|
||||
|
||||
# Match only numbered problem files: 1365.some-slug.js anywhere in the tree
|
||||
files=$(fd --type f '^\d+\..+\.(js|ts)$' "$root" \
|
||||
| fzf --multi \
|
||||
--prompt='eli5> ' \
|
||||
--delimiter=/ \
|
||||
--with-nth=-3.. \
|
||||
--preview 'bat --color=always --line-range=:40 {}' \
|
||||
--preview-window=right:60%)
|
||||
[ -n "$files" ] || exit 1
|
||||
|
||||
printf '%s\n' "$files" | while IFS= read -r f; do
|
||||
echo "── /leetcode-eli5 $f"
|
||||
omp -p "/leetcode-eli5 $f"
|
||||
done
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
# mandible-pick — pick a tool for mandible (it requires one), then open it.
|
||||
# fzf over the executables installed on PATH (no shell builtins, keywords or
|
||||
# aliases). A query with a space, such as "git remote add", or one that
|
||||
# matches nothing is used as typed (tool + subcommand path).
|
||||
# Used by the tmux-palette "mandible" item (alias: man).
|
||||
|
||||
list() { # portable: no GNU find -printf
|
||||
local d f
|
||||
IFS=: read -ra dirs <<<"$PATH"
|
||||
for d in "${dirs[@]}"; do
|
||||
for f in "$d"/*; do [[ -x $f && ! -d $f ]] && printf '%s\n' "${f##*/}"; done
|
||||
done
|
||||
}
|
||||
# enter: a query with a space ("git remote add") is a tool + subcommand path,
|
||||
# taken as typed; otherwise the highlighted command, or the query if none.
|
||||
q=$(list | sort -u | fzf --prompt 'mandible> ' --height 100% --reverse \
|
||||
--with-shell 'bash -c' \
|
||||
--bind 'enter:transform:[[ {q} == *" "* ]] && echo print-query || echo accept-or-print-query') || exit 0
|
||||
[ -n "$q" ] || exit 0
|
||||
# shellcheck disable=SC2086 # split "git remote" into tool + subcommands
|
||||
exec mandible $q
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/bin/sh
|
||||
# Fetch a URL as markdown, copy to clipboard, preview with mdfried.
|
||||
url=$1
|
||||
if [ -z "$url" ]; then
|
||||
echo "usage: mdcp <url>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if command -v wl-copy >/dev/null 2>&1; then
|
||||
copy=wl-copy paste=wl-paste
|
||||
else
|
||||
echo "mdcp: no clipboard tool (wl-copy) found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
curl.md "$url" | "$copy"
|
||||
"$paste" | mdfried
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/bin/sh
|
||||
# Interactive Markdown selector and previewer.
|
||||
file=$(fd --type f --extension md --max-depth 3 \
|
||||
| fzf --preview 'mdfried {}' --preview-window=right:60%:wrap) || exit 0
|
||||
[ -n "$file" ] || exit 0
|
||||
exec mdfried "$file"
|
||||
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;
|
||||
' -- "$@"
|
||||
Executable
+296
@@ -0,0 +1,296 @@
|
||||
#!/usr/bin/env python3
|
||||
"""omp-session — start or resume omp for the current git repo (ctrl-a in fish
|
||||
and zsh), and the backend of the television `omp-sessions` cable
|
||||
(~/.config/television/cable/omp-sessions.toml).
|
||||
|
||||
Launch rules, inside a git repo:
|
||||
default branch resume the newest session on it if it was written less than
|
||||
3 hours ago; otherwise pick one with `tv omp-sessions`
|
||||
(all sessions of the repo). No sessions at all → new session.
|
||||
other branch resume the newest session on it; none → new session.
|
||||
detached HEAD pick with `tv omp-sessions` (no sessions → new session).
|
||||
Outside a git repo: plain `omp`.
|
||||
|
||||
A session belongs to the repo when its cwd is the worktree root or below it,
|
||||
excluding worktrees nested inside it. omp does not record the git branch, so
|
||||
a session's branch is the branch HEAD was on (per `git reflog`) when the
|
||||
session file was last written.
|
||||
|
||||
Cable entry points:
|
||||
omp-session --list one "display<TAB>session file" record per session
|
||||
omp-session --preview FILE title, cwd, and the conversation's message text
|
||||
omp-session --resume FILE the cable's enter action: `omp --resume FILE`
|
||||
omp-session --new the cable's ctrl-n action: a new `omp` session
|
||||
|
||||
Env:
|
||||
OMP_SESSION_STATE=FILE --resume/--new write `ran` to it, so the launcher can
|
||||
tell a started session from a cancelled picker and
|
||||
only move the cursor back up after a cancel (tv
|
||||
--inline).
|
||||
PI_CODING_AGENT_DIR omp's agent dir (default ~/.omp/agent).
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
RECENT = 3 * 3600
|
||||
SESSIONS = Path(os.environ.get("PI_CODING_AGENT_DIR") or Path.home() / ".omp" / "agent") / "sessions"
|
||||
|
||||
# tv closes the preview pipe early on long sessions; exit quietly.
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
|
||||
MOVES = (
|
||||
# (pattern, group of the branch HEAD moved to, group of the branch it left)
|
||||
(re.compile(r"checkout: moving from (.+) to (.+)$"), 2, 1),
|
||||
(re.compile(r"Branch: renamed refs/heads/(.+) to refs/heads/(.+)$"), 2, 1),
|
||||
# rebase (start) detaches HEAD; the finish returns to the rebased branch.
|
||||
(re.compile(r"rebase.*\(finish\): returning to refs/heads/(.+)$"), 1, 1),
|
||||
)
|
||||
|
||||
|
||||
def git(*args):
|
||||
r = subprocess.run(["git", *args], capture_output=True, text=True)
|
||||
return r.stdout.strip() if r.returncode == 0 else ""
|
||||
|
||||
|
||||
def inside(path, root):
|
||||
return path == root or path.startswith(root.rstrip("/") + "/")
|
||||
|
||||
|
||||
class Session:
|
||||
__slots__ = ("path", "mtime", "cwd", "title")
|
||||
|
||||
def __init__(self, path, mtime, cwd, title):
|
||||
self.path, self.mtime, self.cwd, self.title = path, mtime, cwd, title
|
||||
|
||||
|
||||
def header(path):
|
||||
"""(cwd, title) from the head of a session file: an optional title record,
|
||||
then the session record."""
|
||||
title = None
|
||||
try:
|
||||
with open(path, encoding="utf-8", errors="replace") as fh:
|
||||
for _, line in zip(range(4), fh):
|
||||
rec = json.loads(line)
|
||||
if rec.get("type") == "title":
|
||||
title = rec.get("title")
|
||||
elif rec.get("type") == "session":
|
||||
return rec.get("cwd"), title
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def repo_sessions(top):
|
||||
"""Sessions whose cwd is inside the worktree `top`, newest first."""
|
||||
nested = [
|
||||
line[len("worktree "):]
|
||||
for line in git("worktree", "list", "--porcelain").splitlines()
|
||||
if line.startswith("worktree ")
|
||||
]
|
||||
nested = [w for w in nested if w != top and inside(w, top)]
|
||||
out = []
|
||||
for path in SESSIONS.glob("*/*.jsonl"):
|
||||
h = header(path)
|
||||
if not h or not h[0]:
|
||||
continue
|
||||
cwd, title = h
|
||||
if not inside(cwd, top) or any(inside(cwd, w) for w in nested):
|
||||
continue
|
||||
try:
|
||||
mtime = path.stat().st_mtime
|
||||
except OSError:
|
||||
continue
|
||||
out.append(Session(path, mtime, cwd, title))
|
||||
out.sort(key=lambda s: s.mtime, reverse=True)
|
||||
return out
|
||||
|
||||
|
||||
class Timeline:
|
||||
"""Which branch HEAD was on at a given time, from this worktree's reflog."""
|
||||
|
||||
def __init__(self, current):
|
||||
self.current = current
|
||||
self.moves = [] # (unix time, to, from), newest first
|
||||
for line in git("reflog", "--date=unix", "--format=%gd%x09%gs").splitlines():
|
||||
ref, _, subject = line.partition("\t")
|
||||
m = re.search(r"@\{(\d+)\}", ref)
|
||||
if not m:
|
||||
continue
|
||||
for pat, to, frm in MOVES:
|
||||
hit = pat.search(subject)
|
||||
if hit:
|
||||
self.moves.append((int(m.group(1)), hit.group(to), hit.group(frm)))
|
||||
break
|
||||
|
||||
def branch_at(self, t):
|
||||
for ts, to, _ in self.moves:
|
||||
if ts <= t:
|
||||
return to
|
||||
# Older than every recorded move: the branch the oldest move left.
|
||||
return self.moves[-1][2] if self.moves else self.current
|
||||
|
||||
|
||||
def default_branch():
|
||||
ref = git("symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD")
|
||||
if ref:
|
||||
return ref.split("/", 1)[1]
|
||||
for b in (git("config", "init.defaultBranch"), "main", "master"):
|
||||
if b and git("rev-parse", "--verify", "--quiet", f"refs/heads/{b}"):
|
||||
return b
|
||||
return ""
|
||||
|
||||
|
||||
def first_user_text(path):
|
||||
try:
|
||||
with open(path, encoding="utf-8", errors="replace") as fh:
|
||||
for line in fh:
|
||||
if '"role":"user"' not in line:
|
||||
continue
|
||||
text = message_text(json.loads(line).get("message") or {})
|
||||
if text.strip():
|
||||
return text.strip().splitlines()[0]
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
return ""
|
||||
|
||||
|
||||
def message_text(msg):
|
||||
content = msg.get("content")
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
if isinstance(content, list):
|
||||
return "\n".join(p.get("text", "") for p in content if isinstance(p, dict) and p.get("type") == "text")
|
||||
return ""
|
||||
|
||||
|
||||
def age(seconds):
|
||||
for unit, size in (("w", 604800), ("d", 86400), ("h", 3600), ("m", 60)):
|
||||
if seconds >= size:
|
||||
return f"{int(seconds // size)}{unit}"
|
||||
return "now"
|
||||
|
||||
|
||||
# ── Cable entry points ─────────────────────────────────────────────
|
||||
|
||||
|
||||
def cmd_list():
|
||||
top = git("rev-parse", "--show-toplevel")
|
||||
if not top:
|
||||
return
|
||||
sessions = repo_sessions(top)
|
||||
timeline = Timeline(git("branch", "--show-current"))
|
||||
now = time.time()
|
||||
rows = []
|
||||
for s in sessions:
|
||||
branch = timeline.branch_at(s.mtime)
|
||||
title = s.title or first_user_text(s.path) or s.path.stem
|
||||
sub = os.path.relpath(s.cwd, top)
|
||||
rows.append((age(now - s.mtime), branch, title + ("" if sub == "." else f" · {sub}"), s.path))
|
||||
width = min(max((len(r[1]) for r in rows), default=0), 24)
|
||||
for when, branch, title, path in rows:
|
||||
print(f"{when:>4} {branch[:width]:<{width}} {title}\t{path}")
|
||||
|
||||
|
||||
def cmd_preview(path):
|
||||
path = Path(path)
|
||||
h = header(path)
|
||||
cwd, title = h if h else ("", None)
|
||||
updated = time.strftime("%Y-%m-%d %H:%M", time.localtime(path.stat().st_mtime))
|
||||
print(f"\033[1m{title or path.stem}\033[0m")
|
||||
print(f"\033[2m{updated} · {cwd}\033[0m")
|
||||
colors = {"user": "\033[1;36m", "assistant": "\033[1;35m"}
|
||||
with open(path, encoding="utf-8", errors="replace") as fh:
|
||||
for line in fh:
|
||||
if '"type":"message"' not in line:
|
||||
continue
|
||||
try:
|
||||
msg = json.loads(line).get("message") or {}
|
||||
except ValueError:
|
||||
continue
|
||||
role = msg.get("role")
|
||||
text = message_text(msg).strip() if role in colors else ""
|
||||
if text:
|
||||
print(f"\n{colors[role]}▌ {role}\033[0m\n{text}")
|
||||
|
||||
|
||||
def mark_ran():
|
||||
state = os.environ.get("OMP_SESSION_STATE")
|
||||
if state:
|
||||
Path(state).write_text("ran\n")
|
||||
|
||||
|
||||
def cmd_resume(path):
|
||||
mark_ran()
|
||||
os.execvp("omp", ["omp", "--resume", str(path)])
|
||||
|
||||
|
||||
# ── Launcher ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def new_session():
|
||||
mark_ran()
|
||||
os.execvp("omp", ["omp"])
|
||||
|
||||
|
||||
def pick(sessions):
|
||||
if not sessions:
|
||||
new_session()
|
||||
fd, state = tempfile.mkstemp(prefix="omp-session.")
|
||||
os.close(fd)
|
||||
try:
|
||||
subprocess.run(["tv", "omp-sessions", "--inline"], env={**os.environ, "OMP_SESSION_STATE": state})
|
||||
ran = Path(state).read_text().strip() == "ran"
|
||||
finally:
|
||||
os.unlink(state)
|
||||
if not ran:
|
||||
# Cancelled: tv left the cursor on the line below the prompt.
|
||||
with open("/dev/tty", "w") as tty:
|
||||
tty.write("\033[A")
|
||||
|
||||
|
||||
def launch():
|
||||
top = git("rev-parse", "--show-toplevel")
|
||||
if not top:
|
||||
new_session()
|
||||
sessions = repo_sessions(top)
|
||||
branch = git("branch", "--show-current")
|
||||
if not branch:
|
||||
pick(sessions)
|
||||
return
|
||||
timeline = Timeline(branch)
|
||||
mine = next((s for s in sessions if timeline.branch_at(s.mtime) == branch), None)
|
||||
if branch == default_branch():
|
||||
if mine and time.time() - mine.mtime < RECENT:
|
||||
cmd_resume(mine.path)
|
||||
pick(sessions)
|
||||
elif mine:
|
||||
cmd_resume(mine.path)
|
||||
else:
|
||||
new_session()
|
||||
|
||||
|
||||
def main(argv):
|
||||
if not argv:
|
||||
launch()
|
||||
elif argv[0] == "--list":
|
||||
cmd_list()
|
||||
elif argv[0] == "--preview" and len(argv) == 2:
|
||||
cmd_preview(argv[1])
|
||||
elif argv[0] == "--resume" and len(argv) == 2:
|
||||
cmd_resume(argv[1])
|
||||
elif argv == ["--new"]:
|
||||
new_session()
|
||||
else:
|
||||
sys.exit("usage: omp-session [--list | --preview FILE | --resume FILE | --new]")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(sys.argv[1:])
|
||||
Executable
+450
@@ -0,0 +1,450 @@
|
||||
#!/usr/bin/env bash
|
||||
# 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,
|
||||
# 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.
|
||||
# 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 down-arrow
|
||||
# bindings use this, where an empty tree is the normal case.
|
||||
# PJ_STATE=FILE key bindings pass a path; pj writes `open` to it when the
|
||||
# picker opens and `pj --run` writes `ran`, so the binding
|
||||
# can tell "no tasks" / "cancelled" / "task printed output"
|
||||
# apart and redraw the prompt accordingly (tv --inline).
|
||||
#
|
||||
# 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; }
|
||||
|
||||
# ── 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
|
||||
}
|
||||
|
||||
# ── 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
|
||||
|
||||
# ── Run (the cable's enter action) ────────────────────────────────
|
||||
if [ "${1-}" = --run ]; then
|
||||
kind=${2-} target=${3-} root=${4-} runner=${5-}
|
||||
cd "$root" || exit 1
|
||||
# Tell the key binding a task ran (its output now sits below the prompt).
|
||||
[ -n "${PJ_STATE-}" ] && echo ran >"$PJ_STATE"
|
||||
|
||||
# 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
|
||||
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
|
||||
have "$pm" || pm=npm
|
||||
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
|
||||
|
||||
if [ ${#recs[@]} -eq 0 ]; then
|
||||
[ -n "${PJ_QUIET-}" ] || echo "pj: no task sources found in this tree" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── 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'*}
|
||||
multi=0
|
||||
for r in "${recs[@]}"; do
|
||||
root=${r#*$'\t'}
|
||||
root=${root#*$'\t'}
|
||||
root=${root%%$'\t'*}
|
||||
[ "$root" = "$primary" ] || { multi=1; break; }
|
||||
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
|
||||
}
|
||||
|
||||
if [ "${1-}" = --list ]; then
|
||||
list
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── 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
|
||||
PJ_EXTRA=$(printf '%q ' "$@")
|
||||
export PJ_EXTRA
|
||||
fi
|
||||
[ -n "${PJ_STATE-}" ] && echo open >"$PJ_STATE"
|
||||
tv pj --inline --source-command "cat '$tmp'" --input-header "$primary"
|
||||
Executable
+244
@@ -0,0 +1,244 @@
|
||||
#!/usr/bin/env bash
|
||||
# pj-palette — pj's task runner as a tmux-palette source (the "tasks" palette,
|
||||
# ~/.config/tmux-palette/palettes/tasks.json).
|
||||
#
|
||||
# pj-palette print palette items (JSON) for the active pane's
|
||||
# project: running background tasks, quick actions
|
||||
# (install, dev, sync, make, compose up, …) and
|
||||
# every task `pj --list` finds
|
||||
# pj-palette --run KIND TARGET ROOT RUNNER
|
||||
# run a pj task (the item's action)
|
||||
# pj-palette --cmd ROOT CMD…
|
||||
# run a quick-action command in ROOT
|
||||
# pj-palette --bg NAME [ROOT CMD…]
|
||||
# show background session NAME in a popup, first
|
||||
# starting CMD… in ROOT if it isn't running
|
||||
# pj-palette --omp print the omp palette's items: this tmux
|
||||
# session's omp (open or start), every other
|
||||
# running <session>:omp, and Quit items
|
||||
#
|
||||
# One-off items open a full-width split along the bottom of the window (25% of
|
||||
# its height) in the project root. The command runs in the foreground; ctrl-c
|
||||
# stops it (the output stays). Once it has exited, enter or ctrl-c closes the
|
||||
# pane.
|
||||
#
|
||||
# Long-running items (dev/start/serve/watch/preview tasks, compose up) run in a
|
||||
# detached session named <session>:<tool-and-command> (leetcode:bun-run-dev),
|
||||
# shown in a popup: q hides the popup and leaves the task running, ctrl-c
|
||||
# asks for confirmation, then stops the task and ends its session. Running
|
||||
# ones are listed first, under Running, to bring them back. tmux.conf's
|
||||
# prefix + a starts omp the same way (<session>:omp); omp needs q, so its
|
||||
# popup hides with ctrl-c (or prefix + a) and ctrl-q quits it.
|
||||
|
||||
set -uo pipefail
|
||||
self=$(readlink -f "$0")
|
||||
|
||||
# While the task runs, ctrl-c reaches only the task: with SIGINT trapped,
|
||||
# bash survives the child's death instead of exiting along with it.
|
||||
interrupted=
|
||||
trap 'interrupted=1' INT
|
||||
|
||||
finish() {
|
||||
local status=$1 key
|
||||
# A background task stopped with ctrl-c ends its session (and popup).
|
||||
[ -n "${PJ_BG-}" ] && [ -n "$interrupted" ] && exit 0
|
||||
printf '\n\033[2m[exit %s] enter or ctrl-c to close\033[0m' "$status"
|
||||
trap 'exit 0' INT
|
||||
while IFS= read -rsn1 key; do
|
||||
[ -z "$key" ] && exit 0 # enter
|
||||
done
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Id of session NAME. Background session names contain ':', which -t reads
|
||||
# as session:window, so they are targeted by id.
|
||||
session_id() {
|
||||
local id name
|
||||
while IFS=$'\t' read -r id name; do
|
||||
[ "$name" = "$1" ] && { printf '%s' "$id"; return 0; }
|
||||
done < <(tmux list-sessions -F $'#{session_id}\t#{session_name}' 2>/dev/null)
|
||||
return 1
|
||||
}
|
||||
|
||||
case ${1-} in
|
||||
--run)
|
||||
shift
|
||||
pj --run "$@"
|
||||
finish $?
|
||||
;;
|
||||
--cmd)
|
||||
cd "$2" || exit 1
|
||||
shift 2
|
||||
printf '→ %s\n' "$*" >&2
|
||||
"$@"
|
||||
finish $?
|
||||
;;
|
||||
--bg)
|
||||
name=$2
|
||||
if ! id=$(session_id "$name"); then
|
||||
[ $# -ge 4 ] || exit 0 # a Running item whose task has since ended
|
||||
root=$3
|
||||
shift 3
|
||||
id=$(tmux new-session -d -P -F '#{session_id}' -s "$name" -c "$root" -e PJ_BG=1 "$@") || exit 1
|
||||
# @pj_task marks the session for the Running list (and for tmux.conf's
|
||||
# prefix + a, which hides any of these popups).
|
||||
tmux set -t "$id" @pj_task "$root" \; set -t "$id" status off \; \
|
||||
set -t "$id" detach-on-destroy on
|
||||
# Each session gets a key table for its hide/quit keys; other keys
|
||||
# reach the program. A task's output takes no input, so q hides and
|
||||
# ctrl-c (after a y/n confirm) stops the task. Interactive programs
|
||||
# (omp) need q and typing, so ctrl-c hides and ctrl-q kills the session.
|
||||
if [ "$1" = "$self" ]; then
|
||||
tmux set -t "$id" key-table pj-task \; bind -T pj-task q detach-client \; \
|
||||
bind -T pj-task C-c confirm-before -p 'Stop #{session_name}? (y/n)' 'send-keys C-c'
|
||||
else
|
||||
tmux set -t "$id" key-table pj-app \; bind -T pj-app C-c detach-client \; \
|
||||
bind -T pj-app C-q kill-session
|
||||
fi
|
||||
fi
|
||||
# Task output gets a big centered popup; interactive programs (omp) dock
|
||||
# bottom-right beside the work. Positions past the client edge clamp to
|
||||
# it (-x R would follow whichever pane tmux picks as current).
|
||||
geometry=(-w 90% -h 80%)
|
||||
[ "$(tmux display -p -t "$id" '#{key-table}')" = pj-app ] &&
|
||||
geometry=(-w 35% -h 60% -x '#{client_width}' -y '#{client_height}')
|
||||
tmux display-popup -E "${geometry[@]}" -b rounded -T " $name " "tmux attach -t '$id'"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
have() { command -v "$1" >/dev/null 2>&1; }
|
||||
cwd=$(tmux display -p '#{pane_current_path}' 2>/dev/null) || cwd=$PWD
|
||||
session=$(tmux display -p '#{session_name}' 2>/dev/null)
|
||||
cd "$cwd" || exit 1
|
||||
root=$(git rev-parse --show-toplevel 2>/dev/null) || root=$cwd
|
||||
|
||||
icon_for() {
|
||||
case $1 in
|
||||
bun) printf '' ;;
|
||||
npm | yarn) printf '' ;;
|
||||
pnpm) printf '' ;;
|
||||
make) printf '' ;;
|
||||
go) printf '' ;;
|
||||
cargo) printf '' ;;
|
||||
uv | py | poe | pdm | rye | taskipy | hatch | script) printf '' ;;
|
||||
*) printf '' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Tasks that run until stopped: dev/start/serve/watch/preview, also as a
|
||||
# namespace segment (dev:web, api:dev).
|
||||
long_running() { [[ $1 =~ (^|:)(dev|start|serve|watch|preview)(:|$) ]]; }
|
||||
slug() { local s=${*,,}; s=${s//[^a-z0-9]/-}; tr -s - <<<"$s"; }
|
||||
|
||||
# Item action (a bash line the palette evals) running `pj-palette ARGV…`:
|
||||
# in a full-width bottom split (25%) in DIR, or, given NAME, in the background
|
||||
# session <session>:NAME shown in a popup. Argv goes to tmux as is (no shell,
|
||||
# so fish re-parses nothing).
|
||||
task_action() { # dir name argv…
|
||||
local dir=$1 name=$2
|
||||
shift 2
|
||||
if [ -n "$name" ]; then
|
||||
printf '%q ' "$self" --bg "$session:$name" "$dir" "$self" "$@"
|
||||
else
|
||||
printf '%q ' tmux split-window -v -f -l 25% -c "$dir" "$self" "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
items=()
|
||||
item() { # category icon title description aliases(csv) action [icon color]
|
||||
items+=("$(jq -cn --arg c "$1" --arg i "$2" --arg t "$3" --arg d "$4" --arg a "$5" --arg x "$6" --arg k "${7-}" \
|
||||
'{category: $c, icon: $i, title: $t, description: $d,
|
||||
aliases: ($a | split(",") | map(select(. != ""))), action: {shell: $x}}
|
||||
+ if $k == "" then {} else {iconColor: $k} end')")
|
||||
}
|
||||
quick() { # icon title aliases background(0/1) dir argv…
|
||||
local icon=$1 title=$2 aliases=$3 dir=$5 name="" desc="in ${5/#$HOME/\~}"
|
||||
[ "$4" = 1 ] && { name=$(slug "$title"); desc+=" · background"; }
|
||||
shift 5
|
||||
item Quick "$icon" "$title" "$desc" "$aliases" "$(task_action "$dir" "$name" --cmd "$dir" "$@")"
|
||||
}
|
||||
|
||||
# ── omp palette (palettes/omp.json): the <session>:omp agent sessions ──────
|
||||
if [ "${1-}" = --omp ]; then
|
||||
here=$session:omp live='#5eff6c' idle='#7b8496'
|
||||
omps=$(tmux list-sessions -F $'#{session_id}\t#{session_name}\t#{@pj_task}\t#{session_attached}' 2>/dev/null |
|
||||
awk -F'\t' '$2 ~ /:omp$/ && $3 != ""')
|
||||
state() { [ "$1" -gt 0 ] && printf open || printf hidden; }
|
||||
found=
|
||||
while IFS=$'\t' read -r id name dir attached; do
|
||||
[ "$name" = "$here" ] || continue
|
||||
found=1
|
||||
item "This session" '' "$name" "$(state "$attached") · in ${dir/#$HOME/\~}" "" \
|
||||
"$(printf '%q ' "$self" --bg "$name")" "$live"
|
||||
done <<<"$omps"
|
||||
[ -n "$found" ] || item "This session" '' "$here" "not running · start in ${cwd/#$HOME/\~}" "new" \
|
||||
"$(printf '%q ' "$self" --bg "$here" "$cwd" omp)" "$idle"
|
||||
while IFS=$'\t' read -r id name dir attached; do
|
||||
[ -n "$name" ] && [ "$name" != "$here" ] || continue
|
||||
item Running '' "$name" "$(state "$attached") · in ${dir/#$HOME/\~}" "" \
|
||||
"$(printf '%q ' "$self" --bg "$name")" "$live"
|
||||
done <<<"$omps"
|
||||
while IFS=$'\t' read -r id name dir attached; do
|
||||
[ -n "$name" ] && item Quit '' "Quit $name" "kill the session" "kill" \
|
||||
"$(printf '%q ' tmux kill-session -t "$id")"
|
||||
done <<<"$omps"
|
||||
printf '%s\n' "${items[@]}" | jq -s .
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Background tasks still running (any project), to bring back ──────────
|
||||
while IFS=$'\t' read -r name dir; do
|
||||
[ -n "$dir" ] && item Running '' "$name" "in ${dir/#$HOME/\~}" "" "$(printf '%q ' "$self" --bg "$name")"
|
||||
done < <(tmux list-sessions -F $'#{session_name}\t#{@pj_task}' 2>/dev/null)
|
||||
|
||||
# ── Quick actions, from the files at the project root ─────────────────────
|
||||
if [ -f "$root/package.json" ]; then
|
||||
pm=$(jq -r '.packageManager // empty' "$root/package.json" 2>/dev/null)
|
||||
pm=${pm%%@*}
|
||||
if [ -z "$pm" ]; then
|
||||
if [ -f "$root/bun.lock" ] || [ -f "$root/bun.lockb" ]; then pm=bun
|
||||
elif [ -f "$root/pnpm-lock.yaml" ]; then pm=pnpm
|
||||
elif [ -f "$root/yarn.lock" ]; then pm=yarn
|
||||
else pm=npm
|
||||
fi
|
||||
fi
|
||||
if have "$pm"; then
|
||||
quick "$(icon_for "$pm")" "$pm install" "install,i" 0 "$root" "$pm" install
|
||||
jq -e '.scripts.dev' "$root/package.json" >/dev/null 2>&1 &&
|
||||
quick "$(icon_for "$pm")" "$pm run dev" "dev" 1 "$root" "$pm" run dev
|
||||
fi
|
||||
fi
|
||||
if [ -f "$root/pyproject.toml" ] && have uv; then
|
||||
quick "$(icon_for uv)" "uv sync" "sync,install" 0 "$root" uv sync
|
||||
quick "$(icon_for uv)" "uv run python" "py,repl" 0 "$root" uv run python
|
||||
fi
|
||||
if have make && { [ -f "$root/Makefile" ] || [ -f "$root/makefile" ] || [ -f "$root/GNUmakefile" ]; }; then
|
||||
quick "$(icon_for make)" "make" "m,build" 0 "$root" make
|
||||
fi
|
||||
[ -f "$root/go.mod" ] && have go && quick "$(icon_for go)" "go run ." "run" 0 "$root" go run .
|
||||
[ -f "$root/Cargo.toml" ] && have cargo && quick "$(icon_for cargo)" "cargo run" "run" 0 "$root" cargo run
|
||||
if have docker; then
|
||||
for f in compose.yaml compose.yml docker-compose.yaml docker-compose.yml; do
|
||||
[ -f "$root/$f" ] || continue
|
||||
quick '' "docker compose up" "up,compose" 1 "$root" docker compose up
|
||||
break
|
||||
done
|
||||
fi
|
||||
|
||||
# ── Every task pj finds (npm/bun/pnpm scripts, make, just, mise, devbox,
|
||||
# pyproject task tables and console scripts) ─────────────────────────────
|
||||
while IFS=$'\t' read -r display kind target troot runner _file; do
|
||||
[ -n "$kind" ] || continue
|
||||
tag=${display%% *}
|
||||
desc="$runner $target" name=""
|
||||
[ "$troot" != "$root" ] && desc+=" (${troot#"$root"/})"
|
||||
long_running "$target" && { name=$(slug "$runner $target"); desc+=" · background"; }
|
||||
item "Tasks: $tag" "$(icon_for "$tag")" "$target" "$desc" "" \
|
||||
"$(task_action "$troot" "$name" --run "$kind" "$target" "$troot" "$runner")"
|
||||
done < <(PJ_QUIET=1 pj --list 2>/dev/null)
|
||||
|
||||
if [ ${#items[@]} -eq 0 ]; then
|
||||
item Tasks '' "No tasks found" "in ${cwd/#$HOME/\~}" "" "tmux display-message 'pj: no task sources here'"
|
||||
fi
|
||||
printf '%s\n' "${items[@]}" | jq -s .
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
# Mirror every repo under the ghq root (~/Code/<host>/<owner>/<repo>) as a
|
||||
# symlink ~/Projects/<repo>. Repos sharing a name across owners get
|
||||
# <owner>-<repo>. Links into ~/Code whose repo is gone are removed; anything
|
||||
# in ~/Projects that is not such a link is never touched. Run by
|
||||
# projects-sync.timer (systemd user).
|
||||
set -euo pipefail
|
||||
|
||||
code=${GHQ_ROOT:-$HOME/Code}
|
||||
projects=$HOME/Projects
|
||||
mkdir -p "$projects"
|
||||
|
||||
mapfile -t repos < <(find "$code" -mindepth 4 -maxdepth 4 -name .git -printf '%h\n' 2>/dev/null | sort)
|
||||
|
||||
declare -A count keep
|
||||
for repo in "${repos[@]}"; do
|
||||
name=${repo##*/}
|
||||
count[$name]=$((${count[$name]:-0} + 1))
|
||||
done
|
||||
|
||||
for repo in "${repos[@]}"; do
|
||||
name=${repo##*/}
|
||||
if ((count[$name] > 1)); then
|
||||
owner=${repo%/*}
|
||||
name=${owner##*/}-$name
|
||||
fi
|
||||
link=$projects/$name
|
||||
keep[$name]=1
|
||||
[[ -e $link && ! -L $link ]] && continue
|
||||
[[ $(readlink "$link") == "$repo" ]] || ln -sfn "$repo" "$link"
|
||||
done
|
||||
|
||||
for link in "$projects"/*; do
|
||||
[[ -L $link && $(readlink "$link") == "$code"/* && -z ${keep[${link##*/}]:-} ]] && rm "$link"
|
||||
done
|
||||
exit 0
|
||||
Executable
+95
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env bash
|
||||
# tmux-sessions — tmux session switcher as a tmux-palette source (the
|
||||
# "sessions" palette, ~/.config/tmux-palette/palettes/sessions.json).
|
||||
#
|
||||
# tmux-sessions print palette items (JSON): the other sessions
|
||||
# (last one first), every window of every session,
|
||||
# zoxide's directories, ~/.config's directories,
|
||||
# and a Rename and a Kill item per session
|
||||
# tmux-sessions --new QUERY the palette's query item (what was typed): switch
|
||||
# to session QUERY, or start it in QUERY if that is
|
||||
# a directory, else in zoxide's best match for it
|
||||
# tmux-sessions --dir DIR switch to, or start in DIR, the session named
|
||||
# after DIR's basename without dots
|
||||
#
|
||||
# pj-palette's background task sessions (@pj_task) are left out. Sessions are
|
||||
# targeted by id since names may contain ':'.
|
||||
|
||||
set -uo pipefail
|
||||
self=$(readlink -f "$0")
|
||||
|
||||
session_id() { # name
|
||||
local id name
|
||||
while IFS=$'\t' read -r id name; do
|
||||
[ "$name" = "$1" ] && { printf '%s' "$id"; return 0; }
|
||||
done < <(tmux list-sessions -F $'#{session_id}\t#{session_name}' 2>/dev/null)
|
||||
return 1
|
||||
}
|
||||
|
||||
# Switch to session NAME, first starting it in DIR (when given) if missing.
|
||||
open() { # name [dir]
|
||||
local id
|
||||
id=$(session_id "$1") ||
|
||||
id=$(tmux new-session -d -P -F '#{session_id}' -s "$1" ${2:+-c "$2"}) || exit 1
|
||||
tmux switch-client -t "$id"
|
||||
}
|
||||
|
||||
case ${1-} in
|
||||
--dir)
|
||||
dir=${2%/}
|
||||
name=${dir##*/}
|
||||
open "${name//./}" "$2"
|
||||
exit
|
||||
;;
|
||||
--new)
|
||||
query=$2
|
||||
[ -d "${query/#\~/$HOME}" ] && exec "$self" --dir "${query/#\~/$HOME}"
|
||||
dir=
|
||||
if ! session_id "$query" >/dev/null; then
|
||||
read -ra words <<<"$query"
|
||||
dir=$(zoxide query -- "${words[@]}" 2>/dev/null)
|
||||
fi
|
||||
open "$query" "$dir"
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
|
||||
{
|
||||
tmux list-sessions -F $'S\t#{session_id}\t#{session_name}\t#{@pj_task}\t#{session_attached}\t#{session_windows}\t#{session_path}'
|
||||
tmux list-windows -a -F $'W\t#{session_id}\t#{session_name}\t#{@pj_task}\t#{window_id}\t#{window_index}\t#{window_name}\t#{pane_current_path}'
|
||||
zoxide query -l 2>/dev/null | sed 's/^/Z\t/'
|
||||
find -L "$HOME/.config" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort | sed 's/^/C\t/'
|
||||
} | jq -Rn --arg self "$self" --arg home "$HOME" \
|
||||
--arg cur "$(tmux display -p '#{session_id}')" --arg last "$(tmux display -p '#{client_last_session}')" '
|
||||
def tilde: if startswith($home) then "~" + .[($home | length):] else . end;
|
||||
def session_name: rtrimstr("/") | split("/") | last | gsub("\\."; "");
|
||||
[inputs | split("\t")] as $rows
|
||||
| ($rows | map(select(.[0] == "S" and .[3] == ""))) as $sessions
|
||||
| ($rows | map(select(.[0] == "S") | .[2])) as $names
|
||||
| ($rows | map(select(.[0] == "Z") | .[1])) as $zoxide
|
||||
| ($sessions | map(select(.[1] != $cur))) as $others
|
||||
| def dir($category; $icon):
|
||||
(.[1] | session_name) as $name
|
||||
| {category: $category, icon: $icon, title: (.[1] | tilde),
|
||||
description: (if $names | any(. == $name) then "switch to \($name)" else "new session \($name)" end),
|
||||
action: {shell: "\($self | @sh) --dir \(.[1] | @sh)"}};
|
||||
[
|
||||
(($others | map(select(.[2] == $last)) + map(select(.[2] != $last)))[]
|
||||
| {category: "Sessions", icon: "", title: .[2],
|
||||
description: "\(.[5]) window\(if .[5] == "1" then "" else "s" end)\(if .[4] != "0" then " · attached" else "" end) · \(.[6] | tilde)",
|
||||
action: {tmux: "switch-client -t \(.[1] | @sh)"}}
|
||||
+ (if .[4] != "0" then {iconColor: "#5eff6c"} else {} end)),
|
||||
($rows[] | select(.[0] == "W" and .[3] == "")
|
||||
| (.[6] | capture("^(?<icon>[^\\x00-\\x7f]) (?<title>.*)$") // {icon: "", title: .}) as $w
|
||||
| {category: "Windows", icon: $w.icon, title: $w.title,
|
||||
description: "\(.[2]):\(.[5]) · \(.[7] | tilde)",
|
||||
action: {tmux: "switch-client -t \("\(.[1]):\(.[4])" | @sh)"}}),
|
||||
($rows[] | select(.[0] == "Z") | dir("Zoxide"; "")),
|
||||
($rows[] | select(.[0] == "C" and (.[1] as $d | $zoxide | any(. == $d) | not)) | dir("Config"; "")),
|
||||
($sessions[]
|
||||
| {category: "Rename", icon: "", title: "Rename \(.[2])", description: "rename the session",
|
||||
action: {tmux: "command-prompt -I \(.[2] | @sh) -p \("Rename \(.[2]):" | @sh) \("rename-session -t \(.[1] | @sh) -- \"%%%\"" | @sh)"}}),
|
||||
($others[]
|
||||
| {category: "Kill", icon: "", title: "Kill \(.[2])", description: "kill the session",
|
||||
action: {tmux: "kill-session -t \(.[1] | @sh)"}})
|
||||
]'
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
#!/bin/sh
|
||||
# umbriel-float-square
|
||||
# Floating focused window: square, as tall as the usable height (below
|
||||
# Noctalia's bar) minus PAD px above and below, centered on its output.
|
||||
# Tiled: center its column (the old Mod+C). Extents are fractions of the
|
||||
# usable area, so both sides are computed from the output's usable size.
|
||||
set -eu
|
||||
|
||||
PAD=4
|
||||
|
||||
window=$(umbriel windows --json | jq -c '[.[] | select(.active)][0] // empty')
|
||||
[ -n "$window" ] || exit 0
|
||||
|
||||
if [ "$(printf '%s' "$window" | jq -r .floating)" != true ]; then
|
||||
exec umbriel msg column-center
|
||||
fi
|
||||
|
||||
id=$(printf '%s' "$window" | jq -r .id)
|
||||
output=$(printf '%s' "$window" | jq -r .output)
|
||||
extents=$(umbriel outputs --json | jq -r --arg o "$output" --argjson pad "$PAD" '
|
||||
def frac: . * 10000 | floor / 10000;
|
||||
.[] | select(.name == $o) | .usable_area
|
||||
| (.height - 2 * $pad) as $side
|
||||
| "\($side) \([$side / .width, 1] | min | frac) \($side / .height | frac)"')
|
||||
set -- $extents
|
||||
side=$1
|
||||
|
||||
umbriel msg "window-set-secondary-extent:$3"
|
||||
umbriel msg "window-set-primary-extent:$2"
|
||||
|
||||
# window-center uses the size the client has committed; centering before the
|
||||
# resize lands centers the old width. Wait up to 1 s for the new width.
|
||||
i=0
|
||||
while [ $i -lt 20 ]; do
|
||||
w=$(umbriel windows --json | jq -r --arg id "$id" '.[] | select(.id == $id) | .w')
|
||||
[ $((w - side)) -ge -1 ] && [ $((w - side)) -le 1 ] && break
|
||||
sleep 0.05
|
||||
i=$((i + 1))
|
||||
done
|
||||
exec umbriel msg window-center
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
#!/bin/sh
|
||||
# umbriel-launch PATTERN COMMAND...
|
||||
# Focus the next workspace window whose app id matches PATTERN (a
|
||||
# case-insensitive jq regex), cycling through matches on repeated presses;
|
||||
# with none open, spawn COMMAND. Scratchpad windows are skipped: they have
|
||||
# their own toggles. set -eu
|
||||
|
||||
[ $# -ge 2 ] || { echo "usage: umbriel-launch PATTERN COMMAND..." >&2; exit 2; }
|
||||
pattern=$1
|
||||
shift
|
||||
|
||||
# Matches in a stable order (window id); the one after the focused match, or
|
||||
# the first when focus is elsewhere. `.active` is the one window with keyboard
|
||||
# focus; `.focused` is set on every output's last-focused window.
|
||||
target=$(umbriel windows --json | jq -r --arg p "$pattern" '
|
||||
[.[] | select(.scratchpad == "" and (.app_id | test($p; "i")))] | sort_by(.id)
|
||||
| if length == 0 then empty
|
||||
else (map(.active) | index(true)) as $i
|
||||
| if $i == null then .[0].id else .[($i + 1) % length].id end
|
||||
end')
|
||||
|
||||
if [ -n "$target" ]; then
|
||||
exec umbriel msg "window-focus:$target"
|
||||
fi
|
||||
exec umbriel msg spawn "$@"
|
||||
Executable
+37
@@ -0,0 +1,37 @@
|
||||
#!/bin/sh
|
||||
# umbriel-smart-move left|right
|
||||
# Consume-or-expel move, extended across outputs:
|
||||
# - stacked in a column → expel to a new column on that side
|
||||
# - alone, column beside it → consume into that column
|
||||
# - alone, at the strip's end → move to the output on that side
|
||||
# Pressing twice therefore steps a window past its neighbour: in, then out the
|
||||
# far side. Dwindle and master workspaces have no columns; there the window
|
||||
# moves in that direction, or onto the next output at the edge. Floating and
|
||||
# scratchpad windows are left alone.
|
||||
set -eu
|
||||
|
||||
case ${1:-} in
|
||||
left | right) dir=$1 ;;
|
||||
*) echo "usage: umbriel-smart-move left|right" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
windows=$(umbriel windows --json)
|
||||
layout=$(umbriel workspaces --json | jq -r '.[] | select(.focused) | .layout')
|
||||
|
||||
# "<action>" for the focused window, or nothing when it should stay put.
|
||||
action=$(printf '%s' "$windows" | jq -r --arg dir "$dir" --arg layout "$layout" '
|
||||
(map(select(.focused)) | first) as $w
|
||||
| if $w == null or $w.floating or $w.scratchpad != "" then empty
|
||||
elif $layout != "scrolling" then "window-move-or-output-\($dir)"
|
||||
else
|
||||
[ .[] | select(.workspace == $w.workspace and (.floating | not) and .scratchpad == "") ] as $tiled
|
||||
| ([ $tiled[] | select(.column == $w.column) ] | length) as $rows
|
||||
| ([ $tiled[] | .column ] | if $dir == "left" then min else max end) as $edge
|
||||
| if $rows > 1 or $w.column != $edge then "window-consume-or-expel-\($dir)"
|
||||
else "window-move-or-output-\($dir)"
|
||||
end
|
||||
end')
|
||||
|
||||
[ -n "$action" ] || exit 0
|
||||
# At the outermost output, "no output to the left/right" is the expected end.
|
||||
umbriel msg "$action" >/dev/null 2>&1 || true
|
||||
Executable
+26
@@ -0,0 +1,26 @@
|
||||
#!/bin/sh
|
||||
# Yazi `edit` opener. Inside a Neovim terminal ($NVIM set) files open in the
|
||||
# HOST instance via _G.EditFromYazi (see ~/.config/nvim/lua/config/yazi_session.lua)
|
||||
# and the yazi window hides itself; standalone yazi gets a regular blocking nvim.
|
||||
#
|
||||
# usage: nvim-open.sh [vsplit|split|tabedit] file... (default mode: edit)
|
||||
|
||||
case "$1" in
|
||||
vsplit | split | tabedit) mode="$1"; shift ;;
|
||||
*) mode="edit" ;;
|
||||
esac
|
||||
|
||||
[ -n "$YAZI_OPEN_TRACE" ] && printf 'NVIM=%s MODE=%s ARGS=%s\n' "$NVIM" "$mode" "$*" >> "$YAZI_OPEN_TRACE"
|
||||
|
||||
if [ -z "$NVIM" ]; then
|
||||
# split modes are meaningless outside an nvim terminal — quiet no-op
|
||||
[ "$mode" = "edit" ] || exit 0
|
||||
exec nvim "$@"
|
||||
fi
|
||||
|
||||
for f in "$@"; do
|
||||
abs=$(realpath -- "$f") || continue
|
||||
# escape for a vimscript double-quoted string
|
||||
esc=$(printf '%s' "$abs" | sed 's/[\\"]/\\&/g')
|
||||
nvim --server "$NVIM" --remote-expr "v:lua.EditFromYazi(\"$mode\", \"$esc\")"
|
||||
done
|
||||
Reference in new issue
Block a user