mirror of
https://github.com/prdlk/dotfiles.git
synced 2026-11-02 11:32:42 +00:00
wip: pre-split snapshot
This commit is contained in:
1 parent
b4f74342ff
commit
ab29365a3d
21 files changed
+321
-65
No files matched your search
@@ -2,6 +2,13 @@
|
||||
# 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
|
||||
@@ -31,4 +38,79 @@ if [ ! -d "$dir" ]; then
|
||||
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 (dotfiles, configs)
|
||||
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"
|
||||
@@ -1,10 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Toggle a scratch workspace (scratchpad, claude, fastmail, beeper, home), starting its resident apps when any is missing.
|
||||
# omarchy:summary=Toggle a scratch workspace (scratchpad, claude, fastmail, beeper, home, lmstudio), starting its resident apps when any is missing.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
# Usage: omarchy-toggle-scratch [scratchpad|claude|fastmail|beeper|home]
|
||||
# Usage: omarchy-toggle-scratch [scratchpad|claude|fastmail|beeper|home|lmstudio]
|
||||
#
|
||||
# Each profile is a special workspace, the monitor it opens on, and its
|
||||
# resident apps (class pattern -> launch command; the pattern is an anchored
|
||||
@@ -43,8 +43,12 @@ home)
|
||||
NAME="home"
|
||||
MEMBERS=("brave-home\.google\.com__.*|gtk-launch 'Google Home'")
|
||||
;;
|
||||
lmstudio)
|
||||
NAME="lmstudio"
|
||||
MEMBERS=("ai\.elementlabs\.lmstudio|gtk-launch ai.elementlabs.lmstudio")
|
||||
;;
|
||||
*)
|
||||
echo "usage: ${0##*/} [scratchpad|claude|fastmail|beeper|home]" >&2
|
||||
echo "usage: ${0##*/} [scratchpad|claude|fastmail|beeper|home|lmstudio]" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -3,27 +3,45 @@
|
||||
# ~/.config/tmux-palette/palettes/tasks.json).
|
||||
#
|
||||
# pj-palette print palette items (JSON) for the active pane's
|
||||
# project: quick actions (install, dev, sync, make,
|
||||
# …) and every task `pj --list` finds
|
||||
# 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
|
||||
#
|
||||
# Every item opens a full-width split along the bottom of the window (25% of
|
||||
# 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
|
||||
# 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.
|
||||
trap : INT
|
||||
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
|
||||
@@ -32,6 +50,16 @@ finish() {
|
||||
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
|
||||
@@ -45,10 +73,42 @@ case ${1-} in
|
||||
"$@"
|
||||
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 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
|
||||
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
|
||||
|
||||
@@ -65,29 +125,72 @@ icon_for() {
|
||||
esac
|
||||
}
|
||||
|
||||
# tmux action that opens a full-width bottom split (25%) in DIR running ARGV
|
||||
# directly (no shell, so nothing is re-parsed by fish). The palette wrapper
|
||||
# evals the string in bash, hence %q.
|
||||
split_action() {
|
||||
local dir=$1
|
||||
shift
|
||||
local q
|
||||
q=$(printf '%q ' "$@")
|
||||
printf 'split-window -v -f -l 25%% -c %q %s' "$dir" "$q"
|
||||
# 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) tmux-action
|
||||
items+=("$(jq -cn --arg c "$1" --arg i "$2" --arg t "$3" --arg d "$4" --arg a "$5" --arg x "$6" \
|
||||
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: {tmux: $x}}')")
|
||||
aliases: ($a | split(",") | map(select(. != ""))), action: {shell: $x}}
|
||||
+ if $k == "" then {} else {iconColor: $k} end')")
|
||||
}
|
||||
quick() { # icon title aliases dir argv…
|
||||
local icon=$1 title=$2 aliases=$3 dir=$4
|
||||
shift 4
|
||||
item Quick "$icon" "$title" "in ${dir/#$HOME/~}" "$aliases" "$(split_action "$dir" "$self" --cmd "$dir" "$@")"
|
||||
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)
|
||||
@@ -100,33 +203,41 @@ if [ -f "$root/package.json" ]; then
|
||||
fi
|
||||
fi
|
||||
if have "$pm"; then
|
||||
quick "$(icon_for "$pm")" "$pm install" "install,i" "$root" "$pm" install
|
||||
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" "$root" "$pm" run dev
|
||||
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" "$root" uv sync
|
||||
quick "$(icon_for uv)" "uv run python" "py,repl" "$root" uv run python
|
||||
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" "$root" make
|
||||
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
|
||||
[ -f "$root/go.mod" ] && have go && quick "$(icon_for go)" "go run ." "run" "$root" go run .
|
||||
[ -f "$root/Cargo.toml" ] && have cargo && quick "$(icon_for cargo)" "cargo run" "run" "$root" cargo run
|
||||
|
||||
# ── 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"
|
||||
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" "" \
|
||||
"$(split_action "$troot" "$self" --run "$kind" "$target" "$troot" "$runner")"
|
||||
"$(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/~}" "" "display-message 'pj: no task sources here'"
|
||||
item Tasks '' "No tasks found" "in ${cwd/#$HOME/\~}" "" "tmux display-message 'pj: no task sources here'"
|
||||
fi
|
||||
printf '%s\n' "${items[@]}" | jq -s .
|
||||
Reference in new issue
Block a user