wtcode 0.5.0__py3-none-any.whl → 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wtcode/__init__.py +1 -1
- wtcode/wtcode.sh +530 -72
- {wtcode-0.5.0.dist-info → wtcode-0.7.0.dist-info}/METADATA +8 -4
- wtcode-0.7.0.dist-info/RECORD +9 -0
- {wtcode-0.5.0.dist-info → wtcode-0.7.0.dist-info}/WHEEL +1 -1
- wtcode-0.5.0.dist-info/RECORD +0 -9
- {wtcode-0.5.0.dist-info → wtcode-0.7.0.dist-info}/entry_points.txt +0 -0
- {wtcode-0.5.0.dist-info → wtcode-0.7.0.dist-info}/licenses/LICENSE +0 -0
- {wtcode-0.5.0.dist-info → wtcode-0.7.0.dist-info}/top_level.txt +0 -0
wtcode/__init__.py
CHANGED
wtcode/wtcode.sh
CHANGED
|
@@ -6,11 +6,32 @@
|
|
|
6
6
|
set -eu
|
|
7
7
|
shopt -s extglob
|
|
8
8
|
|
|
9
|
+
# detect well-known AI coding agents via the env vars they set on their own
|
|
10
|
+
# subprocesses -- send-keys/AppleScript fire into whatever pane/tab/window
|
|
11
|
+
# happens to be current, which is unsafe to do behind an agent's back while
|
|
12
|
+
# it's working (it may be a pane/window the user is watching, or one running
|
|
13
|
+
# an unrelated task). exec is always safe: it just replaces the agent's own
|
|
14
|
+
# child process.
|
|
15
|
+
--running-under-coding-agent() {
|
|
16
|
+
[[ -n ${CLAUDECODE:-} || -n ${CURSOR_AGENT:-} || -n ${GEMINI_CLI:-} ||
|
|
17
|
+
-n ${CODEX_SANDBOX:-} || -n ${CODEX_SANDBOX_NETWORK_DISABLED:-} ]]
|
|
18
|
+
}
|
|
19
|
+
|
|
9
20
|
# declare all WTCODE_* env vars and their defaults in one place
|
|
10
|
-
: ${WTCODE_CMD
|
|
21
|
+
: ${WTCODE_TOOL:=${WTCODE_CMD:-}} # WTCODE_CMD: deprecated alias
|
|
11
22
|
: ${WTCODE_USE_CURRENT_BRANCH:=false}
|
|
12
|
-
: ${
|
|
13
|
-
: ${
|
|
23
|
+
: ${WTCODE_BASE_REF:=} # ref to fork new branches from (also: --base REF)
|
|
24
|
+
: ${WTCODE_BRANCH_SCOPE:=} # initial picker scope: all|worktree|local|remote
|
|
25
|
+
: ${WTCODE_BRANCH_ORDER:=} # initial picker order: recent|name|full
|
|
26
|
+
# where the picker remembers its last state; set to empty to disable
|
|
27
|
+
: ${WTCODE_STATE_FILE=${XDG_STATE_HOME:-$HOME/.local/state}/wtcode/state}
|
|
28
|
+
if --running-under-coding-agent; then
|
|
29
|
+
: ${WTCODE_TMUX_MODE:=exec}
|
|
30
|
+
: ${WTCODE_TERMINAL_MODE:=exec}
|
|
31
|
+
else
|
|
32
|
+
: ${WTCODE_TMUX_MODE:=send-keys}
|
|
33
|
+
: ${WTCODE_TERMINAL_MODE:=send-keys}
|
|
34
|
+
fi
|
|
14
35
|
: ${WTCODE_DEBUG:=}
|
|
15
36
|
|
|
16
37
|
${WTCODE_DEBUG:+set -x}
|
|
@@ -25,16 +46,18 @@ ${WTCODE_DEBUG:+set -x}
|
|
|
25
46
|
tr '[:upper:]' '[:lower:]' | # lowercase
|
|
26
47
|
sed 's/[^a-z0-9/_-]/-/g' | # replace non-alnum to hyphens
|
|
27
48
|
sed 's/--*/-/g' | # collapse consecutive hyphens
|
|
28
|
-
sed 's
|
|
49
|
+
sed 's|//*|/|g' | # collapse consecutive slashes
|
|
50
|
+
sed 's|-*/-*|/|g' | # no hyphens adjacent to slashes
|
|
51
|
+
sed 's|^[-/]*||; s|[-/]*$||' # trim leading/trailing hyphens/slashes
|
|
29
52
|
}
|
|
30
53
|
|
|
31
|
-
WTCODE_VERSION=0.
|
|
54
|
+
WTCODE_VERSION=0.7.0
|
|
32
55
|
--version() { echo "wtcode $WTCODE_VERSION"; }
|
|
33
56
|
--help() {
|
|
34
57
|
cat <<USAGE
|
|
35
58
|
wtcode $WTCODE_VERSION -- launch a code tool in a git worktree
|
|
36
59
|
|
|
37
|
-
Usage: wtcode [BRANCH] [CMD [CMD-ARGS...]]
|
|
60
|
+
Usage: wtcode [--base REF] [BRANCH] [CMD [CMD-ARGS...]]
|
|
38
61
|
wtcode --exec CMD [CMD-ARGS...]
|
|
39
62
|
|
|
40
63
|
BRANCH Git branch or worktree name to switch to.
|
|
@@ -43,28 +66,59 @@ Usage: wtcode [BRANCH] [CMD [CMD-ARGS...]]
|
|
|
43
66
|
(use multiple colons to avoid fzf matching, e.g., :::my-branch
|
|
44
67
|
or my-branch:::).
|
|
45
68
|
|
|
46
|
-
CMD Command to launch in the worktree (default: \$
|
|
47
|
-
or first available of: ${
|
|
69
|
+
CMD Command to launch in the worktree (default: \$WTCODE_TOOL,
|
|
70
|
+
or first available of: ${WTCODE_TOOLS_TO_TRY[*]}, \$SHELL).
|
|
48
71
|
|
|
49
72
|
--exec Skip branch argument; select interactively, then launch CMD.
|
|
50
73
|
|
|
74
|
+
--base REF Fork a brand-new branch from REF (same as \$WTCODE_BASE_REF).
|
|
75
|
+
|
|
76
|
+
Interactive picker keys (when BRANCH is omitted):
|
|
77
|
+
ctrl-s scope: all, worktree, local, remote (all and worktree also show and
|
|
78
|
+
match a worktree directory that drifted from its branch name
|
|
79
|
+
or sits outside the main checkout and worktrees root)
|
|
80
|
+
ctrl-o order: recent, name (remote shown as a suffix), remote/name
|
|
81
|
+
ctrl-r base for a new branch: the default, the current branch (or
|
|
82
|
+
origin/HEAD, when that differs from the default)
|
|
83
|
+
ctrl-t tool: nested picker of installed tools and recent commands;
|
|
84
|
+
type a new command/args to launch it (unless CMD was given)
|
|
85
|
+
alt-enter create a new branch named after what you typed (never the
|
|
86
|
+
highlighted one); plain Enter does that too when nothing matches
|
|
87
|
+
ctrl-l git remote update, then reload
|
|
88
|
+
Changes made in the picker are remembered for next time, even if canceled.
|
|
89
|
+
|
|
51
90
|
Environment variables:
|
|
52
|
-
|
|
91
|
+
WTCODE_TOOL Default tool to launch (e.g., claude, lazygit, vim); WTCODE_CMD is a deprecated alias
|
|
53
92
|
WTCODE_USE_CURRENT_BRANCH
|
|
54
93
|
When creating a brand-new branch, fork it from the
|
|
55
94
|
current HEAD instead of the default: origin/HEAD
|
|
56
95
|
(falling back to origin/main, then origin/master)
|
|
96
|
+
WTCODE_BASE_REF Fork brand-new branches from this ref (overrides the above)
|
|
97
|
+
WTCODE_BRANCH_SCOPE Initial picker scope (all|worktree|local|remote)
|
|
98
|
+
WTCODE_BRANCH_ORDER Initial picker order (recent|name|full)
|
|
99
|
+
WTCODE_STATE_FILE Where the picker remembers its last state
|
|
100
|
+
(default: \$XDG_STATE_HOME/wtcode/state; empty disables)
|
|
57
101
|
WTCODE_TMUX_MODE When in tmux, control how the tool is launched:
|
|
58
|
-
send-keys - send command to current pane (default in tmux
|
|
102
|
+
send-keys - send command to current pane (default in tmux,
|
|
103
|
+
unless run by a known AI coding agent -- see below)
|
|
59
104
|
split-window - create split pane with send-keys
|
|
60
105
|
new-window - create new window with send-keys
|
|
61
106
|
exec - exec the tool directly (opt out of send-keys)
|
|
62
107
|
WTCODE_TERMINAL_MODE Outside tmux, control how the tool is launched:
|
|
63
108
|
send-keys - reuse the current tab/window via AppleScript
|
|
64
|
-
(default; macOS only
|
|
65
|
-
|
|
66
|
-
|
|
109
|
+
(default; macOS only, unless run by a known AI
|
|
110
|
+
coding agent -- see below). Terminal.app and
|
|
111
|
+
Ghostty 1.3+ use their own scripting support;
|
|
112
|
+
any other terminal falls back to simulated
|
|
113
|
+
keystrokes
|
|
67
114
|
exec - exec the tool directly (opt out of send-keys)
|
|
115
|
+
|
|
116
|
+
When wtcode is invoked by a known AI coding agent (detected via
|
|
117
|
+
CLAUDECODE, CURSOR_AGENT, GEMINI_CLI, CODEX_SANDBOX, or
|
|
118
|
+
CODEX_SANDBOX_NETWORK_DISABLED), both modes default to exec
|
|
119
|
+
instead of send-keys, since send-keys/AppleScript would inject
|
|
120
|
+
keystrokes into a pane/tab/window the agent doesn't control while
|
|
121
|
+
it's still working. Set the env var explicitly to override.
|
|
68
122
|
WTCODE_DEBUG Enable debug tracing when set
|
|
69
123
|
GIT_WORKTREE_ROOT Override the directory where worktrees are created
|
|
70
124
|
|
|
@@ -74,13 +128,13 @@ Examples:
|
|
|
74
128
|
wtcode feature-x claude --resume # launch claude with --resume
|
|
75
129
|
wtcode --exec claude --resume # select interactively, launch claude --resume
|
|
76
130
|
wtcode :new-feature # create new branch and worktree
|
|
77
|
-
|
|
131
|
+
WTCODE_TOOL=cursor wtcode feature # use cursor as the default tool
|
|
78
132
|
USAGE
|
|
79
133
|
}
|
|
80
134
|
|
|
81
135
|
# commands to try as default, in order of preference
|
|
82
|
-
|
|
83
|
-
$
|
|
136
|
+
WTCODE_TOOLS_TO_TRY=(
|
|
137
|
+
$WTCODE_TOOL
|
|
84
138
|
claude
|
|
85
139
|
aider
|
|
86
140
|
codex
|
|
@@ -96,6 +150,10 @@ WTCODE_CMDS_TO_TRY=(
|
|
|
96
150
|
# the current HEAD. only consults local remote-tracking refs -- never
|
|
97
151
|
# touches the network.
|
|
98
152
|
--default-base-ref() {
|
|
153
|
+
if [[ -n $WTCODE_BASE_REF ]]; then
|
|
154
|
+
echo "$WTCODE_BASE_REF"
|
|
155
|
+
return
|
|
156
|
+
fi
|
|
99
157
|
if [[ $WTCODE_USE_CURRENT_BRANCH == true ]]; then
|
|
100
158
|
git rev-parse HEAD
|
|
101
159
|
return
|
|
@@ -107,6 +165,430 @@ WTCODE_CMDS_TO_TRY=(
|
|
|
107
165
|
git rev-parse HEAD
|
|
108
166
|
}
|
|
109
167
|
|
|
168
|
+
###############################################################################
|
|
169
|
+
## interactive picker support -- remembered state, key-binding callbacks
|
|
170
|
+
###############################################################################
|
|
171
|
+
# The fzf picker lets you cycle some options with key bindings:
|
|
172
|
+
# ctrl-s scope ctrl-o order ctrl-r base ref ctrl-l update
|
|
173
|
+
# alt-enter create a new branch named after what you typed, not the highlighted one
|
|
174
|
+
# ctrl-t tool: opens a nested picker of installed tools and recently
|
|
175
|
+
# launched commands; type anything to launch a new command/args
|
|
176
|
+
# The current values live in small files under a temp dir ($1 of the --fzf-*
|
|
177
|
+
# callbacks, which fzf runs as separate processes); every change is also saved
|
|
178
|
+
# to $WTCODE_STATE_FILE right away, for next time. none of this is touched when
|
|
179
|
+
# a BRANCH is given on the command line.
|
|
180
|
+
|
|
181
|
+
WTCODE_SCOPES=(all worktree local remote)
|
|
182
|
+
WTCODE_ORDERS=(recent name full) # full: sort by remote/name, name: by name alone
|
|
183
|
+
|
|
184
|
+
--self() {
|
|
185
|
+
local src=${BASH_SOURCE[0]}
|
|
186
|
+
echo "$(cd "$(dirname "$src")" && pwd)/$(basename "$src")"
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
# --state-get KEY -- last remembered value (empty if none)
|
|
190
|
+
--state-get() {
|
|
191
|
+
[[ -n $WTCODE_STATE_FILE && -r $WTCODE_STATE_FILE ]] || return 0
|
|
192
|
+
sed -n "s/^$1=//p" "$WTCODE_STATE_FILE" | tail -1
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
# --state-save KEY=VALUE... -- remember values, keeping the other keys
|
|
196
|
+
--state-save() {
|
|
197
|
+
[[ -n $WTCODE_STATE_FILE ]] || return 0
|
|
198
|
+
{
|
|
199
|
+
mkdir -p "$(dirname "$WTCODE_STATE_FILE")"
|
|
200
|
+
local tmp=$WTCODE_STATE_FILE.$$ kv
|
|
201
|
+
{ [[ -r $WTCODE_STATE_FILE ]] && cat "$WTCODE_STATE_FILE"; true; } > "$tmp"
|
|
202
|
+
for kv; do
|
|
203
|
+
grep -v "^${kv%%=*}=" "$tmp" > "$tmp.2" || true
|
|
204
|
+
printf '%s\n' "$kv" >> "$tmp.2"
|
|
205
|
+
mv "$tmp.2" "$tmp"
|
|
206
|
+
done
|
|
207
|
+
mv "$tmp" "$WTCODE_STATE_FILE"
|
|
208
|
+
} 2>/dev/null || --msg "could not save state to $WTCODE_STATE_FILE"
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
# --pick-valid VALUE DEFAULT CHOICES... -- VALUE if among CHOICES, else DEFAULT
|
|
212
|
+
--pick-valid() {
|
|
213
|
+
local v=$1 d=$2 c; shift 2
|
|
214
|
+
for c; do [[ $c == "$v" ]] && { echo "$v"; return; }; done
|
|
215
|
+
echo "$d"
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
# --next-in-cycle CURRENT ITEMS... -- the item after CURRENT, wrapping around
|
|
219
|
+
--next-in-cycle() {
|
|
220
|
+
local cur=$1 item take=false; shift
|
|
221
|
+
for item; do
|
|
222
|
+
$take && { echo "$item"; return; }
|
|
223
|
+
[[ $item == "$cur" ]] && take=true
|
|
224
|
+
done
|
|
225
|
+
echo "$1"
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
# --base-ref-for MODE -- the ref a new branch is forked from in each base mode
|
|
229
|
+
--base-ref-for() {
|
|
230
|
+
case $1 in
|
|
231
|
+
current) echo HEAD ;;
|
|
232
|
+
origin) git rev-parse --verify -q origin/HEAD &>/dev/null && echo origin/HEAD || --default-base-ref ;;
|
|
233
|
+
*) --default-base-ref ;;
|
|
234
|
+
esac
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
# --base-modes -- the distinct bases ^r cycles: default, current branch, and
|
|
238
|
+
# origin/HEAD (only when that isn't what default already is)
|
|
239
|
+
--base-modes() {
|
|
240
|
+
local def head; def=$(--default-base-ref); head=$(git rev-parse HEAD 2>/dev/null) || head=
|
|
241
|
+
echo default
|
|
242
|
+
[[ $def == "$head" ]] || echo current # default fell back to HEAD: same thing
|
|
243
|
+
[[ $(--base-ref-for origin) == "$def" ]] || echo origin
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
# --base-label MODE -- "<exact branch> (why)", what a new branch would fork from
|
|
247
|
+
--base-label() {
|
|
248
|
+
local ref name
|
|
249
|
+
case $1 in
|
|
250
|
+
current) name=$(git branch --show-current 2>/dev/null); echo "${name:-HEAD} (current)" ;;
|
|
251
|
+
origin) echo "$(git rev-parse --abbrev-ref origin/HEAD 2>/dev/null || echo origin/HEAD) (origin/HEAD)" ;;
|
|
252
|
+
*)
|
|
253
|
+
ref=$(--default-base-ref)
|
|
254
|
+
if [[ -z $WTCODE_BASE_REF && $ref =~ ^[0-9a-f]{40}$ ]]; then name=$(git branch --show-current 2>/dev/null); name=${name:-HEAD} # (the fallback to HEAD)
|
|
255
|
+
elif [[ $ref == origin/HEAD ]]; then name=$(git rev-parse --abbrev-ref origin/HEAD 2>/dev/null || echo "$ref")
|
|
256
|
+
else name=$ref
|
|
257
|
+
fi
|
|
258
|
+
echo "$name (default)" ;;
|
|
259
|
+
esac
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
# --split-command STRING -- shell-like words (quotes and backslashes), NUL-separated
|
|
263
|
+
--split-command() {
|
|
264
|
+
local s=$1 i=0 c q= word= started=false
|
|
265
|
+
while (( i < ${#s} )); do
|
|
266
|
+
c=${s:i:1}
|
|
267
|
+
if [[ $q == "'" ]]; then
|
|
268
|
+
if [[ $c == "'" ]]; then q=; else word+=$c; fi
|
|
269
|
+
elif [[ $q == '"' ]]; then
|
|
270
|
+
if [[ $c == '"' ]]; then q=
|
|
271
|
+
elif [[ $c == '\' && ${s:i+1:1} == ['"\$`'] ]]; then word+=${s:i+1:1}; i=$((i+1))
|
|
272
|
+
else word+=$c
|
|
273
|
+
fi
|
|
274
|
+
else
|
|
275
|
+
case $c in
|
|
276
|
+
"'"|'"') q=$c; started=true ;;
|
|
277
|
+
'\') word+=${s:i+1:1}; i=$((i+1)); started=true ;;
|
|
278
|
+
[[:space:]]) if $started; then printf '%s\0' "$word"; word=; started=false; fi ;;
|
|
279
|
+
*) word+=$c; started=true ;;
|
|
280
|
+
esac
|
|
281
|
+
fi
|
|
282
|
+
i=$((i+1))
|
|
283
|
+
done
|
|
284
|
+
if $started; then printf '%s\0' "$word"; fi
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
# --available-tools -- installed tools worth offering, one per line
|
|
288
|
+
--available-tools() {
|
|
289
|
+
local c seen=' '
|
|
290
|
+
for c in ${WTCODE_TOOL:+"$WTCODE_TOOL"} "${WTCODE_TOOLS_TO_TRY[@]}" "${SHELL:-bash}"; do
|
|
291
|
+
[[ -n $c && $seen != *" $c "* ]] || continue
|
|
292
|
+
# an explicit $WTCODE_TOOL is always offered; the rest only if installed
|
|
293
|
+
if [[ $c == "${WTCODE_TOOL-}" ]] || type "$c" &>/dev/null; then
|
|
294
|
+
echo "$c"; seen+="$c "
|
|
295
|
+
fi
|
|
296
|
+
done
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
# --fzf-init -- make the state dir for one picker session, print its path
|
|
300
|
+
--fzf-init() {
|
|
301
|
+
local dir v
|
|
302
|
+
dir=$(mktemp -d "${TMPDIR:-/tmp}/wtcode.XXXXXX")
|
|
303
|
+
# env vars/args win over what was remembered
|
|
304
|
+
v=${WTCODE_BRANCH_SCOPE:-$(--state-get scope)}
|
|
305
|
+
--pick-valid "$v" all "${WTCODE_SCOPES[@]}" > "$dir/scope"
|
|
306
|
+
v=${WTCODE_BRANCH_ORDER:-$(--state-get order)}
|
|
307
|
+
--pick-valid "$v" recent "${WTCODE_ORDERS[@]}" > "$dir/order"
|
|
308
|
+
if [[ -n $WTCODE_BASE_REF || $WTCODE_USE_CURRENT_BRANCH == true ]]; then
|
|
309
|
+
echo default > "$dir/base"
|
|
310
|
+
else
|
|
311
|
+
--pick-valid "$(--state-get base)" default $(--base-modes) > "$dir/base"
|
|
312
|
+
fi
|
|
313
|
+
--base-label "$(<"$dir/base")" > "$dir/base_label"
|
|
314
|
+
if [[ -n ${WTCODE_TOOL_FIXED-} ]]; then
|
|
315
|
+
echo "$WTCODE_TOOL_FIXED" > "$dir/tool_fixed"
|
|
316
|
+
else
|
|
317
|
+
# initial: $WTCODE_TOOL, else the last used command if still installed,
|
|
318
|
+
# else the first installed tool
|
|
319
|
+
v=${WTCODE_TOOL:-$(--state-get tool)}
|
|
320
|
+
[[ -n $WTCODE_TOOL ]] || type "${v%% *}" &>/dev/null || v=$(--available-tools | head -1)
|
|
321
|
+
printf '%s\n' "$v" > "$dir/tool"
|
|
322
|
+
fi
|
|
323
|
+
echo "$dir"
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
# --fzf-commit DIR -- apply the final picker state (changes were already saved
|
|
327
|
+
# as they were made, so they survive canceling too)
|
|
328
|
+
--fzf-commit() {
|
|
329
|
+
local dir=$1 base tool
|
|
330
|
+
base=$(<"$dir/base")
|
|
331
|
+
[[ $base == default ]] || WTCODE_PICKED_BASE=$(--base-ref-for "$base") # (not WTCODE_BASE_REF: that may be exported to the tool)
|
|
332
|
+
if [[ -r $dir/tool ]]; then
|
|
333
|
+
tool=$(<"$dir/tool")
|
|
334
|
+
WTCODE_PICKED_TOOL=$tool
|
|
335
|
+
--history-add "$tool"
|
|
336
|
+
fi
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
# --fzf-cycle DIR KIND -- advance scope|order|base|tool to its next value
|
|
340
|
+
--fzf-cycle() {
|
|
341
|
+
local dir=$1 kind=$2 choices
|
|
342
|
+
case $kind in
|
|
343
|
+
scope) choices=("${WTCODE_SCOPES[@]}") ;;
|
|
344
|
+
order) choices=("${WTCODE_ORDERS[@]}") ;;
|
|
345
|
+
base) choices=($(--base-modes)) ;;
|
|
346
|
+
esac
|
|
347
|
+
--next-in-cycle "$(<"$dir/$kind")" "${choices[@]}" > "$dir/$kind.new"
|
|
348
|
+
mv "$dir/$kind.new" "$dir/$kind"
|
|
349
|
+
# a scope/order reload cancels an update in flight, so its marker must go
|
|
350
|
+
[[ $kind == base ]] || rm -f "$dir/updating"
|
|
351
|
+
--state-save "$kind=$(<"$dir/$kind")" # remembered right away, even if the picker is canceled
|
|
352
|
+
[[ $kind == base ]] && --base-label "$(<"$dir/base")" > "$dir/base_label"
|
|
353
|
+
return 0
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
# recently launched commands, most recent first
|
|
357
|
+
--history-file() { [[ -n $WTCODE_STATE_FILE ]] && echo "$WTCODE_STATE_FILE.cmds" || true; }
|
|
358
|
+
--history-add() {
|
|
359
|
+
local f; f=$(--history-file); [[ -n $f && -n $1 ]] || return 0
|
|
360
|
+
{ { printf '%s\n' "$1"; grep -vxF -- "$1" "$f" 2>/dev/null || true; } | head -20 > "$f.new" && mv "$f.new" "$f"; } 2>/dev/null || true
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
# --fzf-pick-tool DIR -- nested picker for the tool (run via ctrl-t)
|
|
364
|
+
# lists recent commands and installed tools; typing text that matches nothing
|
|
365
|
+
# (or pressing alt-enter) takes the typed text as-is, e.g. `claude --resume`
|
|
366
|
+
--fzf-pick-tool() {
|
|
367
|
+
local dir=$1 f out query key choice prompt='tool> '
|
|
368
|
+
[[ -e $dir/in_popup ]] || prompt='wtcode tool> '
|
|
369
|
+
f=$(--history-file)
|
|
370
|
+
out=$(
|
|
371
|
+
{ [[ -n $f && -r $f ]] && cat "$f"; --available-tools; } | awk '!seen[$0]++' |
|
|
372
|
+
fzf --print-query --expect alt-enter --prompt "$prompt" --height 100% --layout reverse \
|
|
373
|
+
--header 'Pick a tool/recent command, or type a new command with args.
|
|
374
|
+
enter: pick highlighted (or typed text if nothing matches) alt-enter: use typed text as-is' ||
|
|
375
|
+
true
|
|
376
|
+
)
|
|
377
|
+
# fzf prints just the query when nothing matches, else query, key, choice
|
|
378
|
+
{ read -r query || true; read -r key || true; read -r choice || true; } <<< "$out"
|
|
379
|
+
if [[ $key == alt-enter || -z $choice ]]; then choice=$query; fi
|
|
380
|
+
choice=${choice#"${choice%%[![:space:]]*}"} choice=${choice%"${choice##*[![:space:]]}"} # trim
|
|
381
|
+
if [[ -n $choice ]]; then
|
|
382
|
+
printf '%s\n' "$choice" > "$dir/tool"
|
|
383
|
+
# remembered right away: it's a deliberate choice that should survive
|
|
384
|
+
# canceling the main picker
|
|
385
|
+
--state-save tool="$choice"
|
|
386
|
+
--history-add "$choice"
|
|
387
|
+
fi
|
|
388
|
+
return 0
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
# --tmux-at-least MAJOR MINOR -- is the running tmux at least this version?
|
|
392
|
+
--tmux-at-least() {
|
|
393
|
+
local v; v=$(tmux -V 2>/dev/null) || return 1
|
|
394
|
+
[[ $v =~ ([0-9]+)\.([0-9]+) ]] || return 1
|
|
395
|
+
(( BASH_REMATCH[1] > $1 || (BASH_REMATCH[1] == $1 && BASH_REMATCH[2] >= $2) ))
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
# --fzf-pick DIR -- run the picker, print fzf's output (query, then selection).
|
|
399
|
+
# inside tmux, fzf's own --tmux is NOT used: since tmux 3.7/fzf 0.7x it opens
|
|
400
|
+
# a floating *pane*, whose keys go through tmux's key tables, so the prefix
|
|
401
|
+
# (e.g. OPTMUX's C-t) and root bindings (bind -n) get swallowed before fzf
|
|
402
|
+
# sees them. a display-popup overlay hands every key to the program instead.
|
|
403
|
+
--fzf-pick() {
|
|
404
|
+
local dir=$1
|
|
405
|
+
if [[ -n ${TMUX:-} ]] && type tmux &>/dev/null && --tmux-at-least 3 3; then
|
|
406
|
+
local self cmd envs=() v
|
|
407
|
+
self=$(--self)
|
|
408
|
+
# started marks that the popup really ran: if it was then dismissed (Esc on
|
|
409
|
+
# the popup, client detached, ...) that's a cancel, NOT a reason to fall
|
|
410
|
+
# back to showing a second, different picker
|
|
411
|
+
printf -v cmd ': > %q; %q %q --fzf-run %q > %q' "$dir/started" "$BASH" "$self" "$dir" "$dir/out"
|
|
412
|
+
: > "$dir/in_popup"
|
|
413
|
+
# a popup doesn't inherit our environment; pass along what matters
|
|
414
|
+
# (not exported, but the key-binding callbacks need them)
|
|
415
|
+
envs+=(-e "WTCODE_BASE_REF=$WTCODE_BASE_REF" -e "WTCODE_USE_CURRENT_BRANCH=$WTCODE_USE_CURRENT_BRANCH")
|
|
416
|
+
for v in $(compgen -e); do
|
|
417
|
+
[[ $v =~ ^(PATH|HOME|SHELL|USER|LOGNAME|TMPDIR|LANG|LC_[A-Z]+|XDG_[A-Z_]+|WTCODE_[A-Z_]+|GIT_[A-Z_]+|FZF_[A-Z_]+)$ ]] &&
|
|
418
|
+
envs+=(-e "$v=${!v}")
|
|
419
|
+
done
|
|
420
|
+
tmux display-popup -E ${TMUX_PANE:+-t "$TMUX_PANE"} -d "$PWD" -w 90% -h 70% -b rounded \
|
|
421
|
+
-T ' wtcode ' ${envs[@]+"${envs[@]}"} "$cmd" 2>/dev/null || true
|
|
422
|
+
if [[ -e $dir/started ]]; then
|
|
423
|
+
cat "$dir/out" 2>/dev/null || true # (empty when dismissed or canceled)
|
|
424
|
+
return 0
|
|
425
|
+
fi
|
|
426
|
+
rm -f "$dir/in_popup" # no client to show a popup on, etc.: fall back
|
|
427
|
+
fi
|
|
428
|
+
--fzf-run "$dir"
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
# --fzf-run DIR -- the picker itself (also what runs inside the tmux popup)
|
|
432
|
+
--fzf-run() {
|
|
433
|
+
local dir=$1 self b dir_q
|
|
434
|
+
{ set +x; } &>/dev/null
|
|
435
|
+
self=$(--self)
|
|
436
|
+
printf -v b '%q %q' "$BASH" "$self" # command prefix for the key bindings
|
|
437
|
+
printf -v dir_q '%q' "$dir"
|
|
438
|
+
local opts=() prompt='branch> '
|
|
439
|
+
# a popup is titled ' wtcode '; anywhere else the prompt says what this is
|
|
440
|
+
[[ -e $dir/in_popup ]] || { opts+=(--tmux 90%,70%); prompt="wtcode $prompt"; }
|
|
441
|
+
if [[ ! -e $dir/tool_fixed ]]; then
|
|
442
|
+
opts+=(--bind "ctrl-t:execute($b --fzf-pick-tool $dir_q)+transform-header($b --fzf-header $dir_q)")
|
|
443
|
+
fi
|
|
444
|
+
--list-branches "$(<"$dir/scope")" "$(<"$dir/order")" |
|
|
445
|
+
WTCODE_BASE_REF=$WTCODE_BASE_REF WTCODE_USE_CURRENT_BRANCH=$WTCODE_USE_CURRENT_BRANCH \
|
|
446
|
+
fzf --ansi --color --print-query --delimiter=$'\t' --with-nth=2 --nth=1 \
|
|
447
|
+
--preview "$b --fzf-preview {1} {3}" \
|
|
448
|
+
--preview-window 'right,50%,wrap,<120(down,40%,wrap)' \
|
|
449
|
+
--header "$(--fzf-header "$dir")" \
|
|
450
|
+
--bind "ctrl-s:execute-silent($b --fzf-cycle $dir_q scope)+reload($b --fzf-list $dir_q)" \
|
|
451
|
+
--bind "ctrl-o:execute-silent($b --fzf-cycle $dir_q order)+reload($b --fzf-list $dir_q)" \
|
|
452
|
+
--bind "ctrl-r:execute-silent($b --fzf-cycle $dir_q base)+transform-header($b --fzf-header $dir_q)" \
|
|
453
|
+
--bind "ctrl-l:execute-silent(: > $dir_q/updating)+transform-header($b --fzf-header $dir_q)+reload($b --fzf-update $dir_q)" \
|
|
454
|
+
--bind "alt-enter:execute-silent(: > $dir_q/force_new)+accept" \
|
|
455
|
+
--bind "load:transform-header($b --fzf-header $dir_q)" \
|
|
456
|
+
${opts[@]+"${opts[@]}"} \
|
|
457
|
+
--prompt "$prompt" || true
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
# --fzf-preview BRANCH [LINE] -- preview pane: the picker's one-line summary,
|
|
461
|
+
# then what BRANCH has relative to the base new branches fork from
|
|
462
|
+
# (git diff --stat BASE...BRANCH, plus the commits that are only on BRANCH)
|
|
463
|
+
--fzf-preview() {
|
|
464
|
+
local branch=${1-} line=${2-} base b=$'\e[1m' r=$'\e[0m'
|
|
465
|
+
[[ -z $line ]] || printf '%s\n\n' "$line"
|
|
466
|
+
[[ -n $branch ]] || return 0
|
|
467
|
+
base=$(--default-base-ref 2>/dev/null) || return 0
|
|
468
|
+
if [[ $branch == "$base" ]] || ! git rev-parse --verify -q "$branch^{commit}" &>/dev/null; then
|
|
469
|
+
return 0
|
|
470
|
+
fi
|
|
471
|
+
printf '%s%s...%s%s\n' "$b" "${base:0:40}" "$branch" "$r"
|
|
472
|
+
git log --oneline --no-decorate --color=never -n 10 "$base..$branch" 2>/dev/null
|
|
473
|
+
echo
|
|
474
|
+
git -c color.ui=always diff --stat=${FZF_PREVIEW_COLUMNS:-80} "$base...$branch" 2>/dev/null
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
--fzf-list() { --list-branches "$(<"$1/scope")" "$(<"$1/order")"; }
|
|
478
|
+
|
|
479
|
+
--fzf-update() {
|
|
480
|
+
GIT_TERMINAL_PROMPT=0 git remote update </dev/null &>/dev/null || true
|
|
481
|
+
rm -f "$1/updating" # (the header, in place, goes back on the next result)
|
|
482
|
+
--fzf-list "$1"
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
# --fzf-header DIR -- the current values are bold (fzf --ansi styles headers)
|
|
486
|
+
--fzf-header() {
|
|
487
|
+
local dir=$1 scope order tool update b=$'\e[1m' r=$'\e[0m'
|
|
488
|
+
case $(<"$dir/scope") in
|
|
489
|
+
worktree) scope=worktrees ;;
|
|
490
|
+
*) scope="$(<"$dir/scope") branches" ;; # all/local/remote branches
|
|
491
|
+
esac
|
|
492
|
+
case $(<"$dir/order") in full) order=remote/name ;; *) order=$(<"$dir/order") ;; esac
|
|
493
|
+
if [[ -r $dir/tool_fixed ]]; then tool="tool: $b$(<"$dir/tool_fixed")$r (given)"; else tool="^t tool: $b$(<"$dir/tool")$r"; fi
|
|
494
|
+
update='^l update remotes'
|
|
495
|
+
[[ ! -e $dir/updating ]] || update="^l ${b}updating remotes...$r" # inline: the rest of the header stays
|
|
496
|
+
echo "Enter: select, or create if nothing matches $tool"
|
|
497
|
+
echo "alt-enter: create new worktree ^r branch from: $b$(<"$dir/base_label")$r"
|
|
498
|
+
echo "^s scope: $b$scope$r ^o order: $b$order$r $update"
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
# --list-branches [SCOPE] [ORDER] -- candidates for the picker
|
|
502
|
+
# format: branch_name<TAB>indicator branch_name<TAB>date hash [upstream] subject
|
|
503
|
+
--list-branches() {
|
|
504
|
+
local scope=${1:-all} order=${2:-recent}
|
|
505
|
+
{ set +x; } &>/dev/null
|
|
506
|
+
# NOTE: git branch --format uses ref-filter syntax which doesn't support
|
|
507
|
+
# pretty-format's %<(N,trunc) truncation. Using bash to handle truncation,
|
|
508
|
+
# alignment, and coloring instead.
|
|
509
|
+
local c_reset=$'\e[0m' c_green=$'\e[32m' c_cyan=$'\e[36m' c_blue=$'\e[34m' c_yellow=$'\e[33m' c_muted=$'\e[2;90m' # dim gray: the remote/ part and worktree paths
|
|
510
|
+
local c_red=$'\e[31m' local_branches sort_args
|
|
511
|
+
local_branches=$(git branch --format='%(refname:lstrip=2)')
|
|
512
|
+
# the two places worktrees are expected: the main checkout, and the root
|
|
513
|
+
# where wtcode puts the rest (see --enter-git-worktree)
|
|
514
|
+
local main_wt wt_root=${GIT_WORKTREE_ROOT:-}
|
|
515
|
+
main_wt=$(cd "$(git rev-parse --git-common-dir)/.." 2>/dev/null && pwd -P) || main_wt=
|
|
516
|
+
[[ -z $wt_root && -z $main_wt ]] || : ${wt_root:=$main_wt.worktrees}
|
|
517
|
+
# compare like with like: git reports canonical paths (no symlinks, no trailing /)
|
|
518
|
+
[[ ! -d $wt_root ]] || wt_root=$(cd "$wt_root" && pwd -P)
|
|
519
|
+
case $order in
|
|
520
|
+
name) sort_args=(-k3,3f -k1,1n) ;; # by name alone; local before remote on ties
|
|
521
|
+
full) sort_args=(-k4,4f) ;; # by remote/name
|
|
522
|
+
*) sort_args=(-k1,1n -k2,2nr) ;; # local first, then remote, each by recency
|
|
523
|
+
esac
|
|
524
|
+
{
|
|
525
|
+
# stage 1: isremote epoch namekey, then the x-prefixed fields (x keeps
|
|
526
|
+
# read from collapsing empty tab-separated ones)
|
|
527
|
+
if [[ $scope == all || $scope == local || $scope == worktree ]]; then
|
|
528
|
+
git branch --format=$'%(committerdate:unix)\tx%(refname:lstrip=2)\tx%(HEAD)\tx%(worktreepath)\tx%(committerdate:relative)\tx%(objectname:short)\tx%(upstream:track)\tx%(contents:subject)' |
|
|
529
|
+
while IFS=$'\t' read -r epoch branch head worktree rest; do
|
|
530
|
+
[[ $scope == worktree && $head != 'x*' && -z ${worktree#x} ]] && continue
|
|
531
|
+
printf '0\t%s\t%s\t%s\t%s\t%s\t%s\n' "$epoch" "$branch" "$branch" "$head" "$worktree" "$rest" || break
|
|
532
|
+
done
|
|
533
|
+
fi
|
|
534
|
+
# remote-tracking branches that don't have a local counterpart
|
|
535
|
+
if [[ $scope == all || $scope == remote ]]; then
|
|
536
|
+
git branch -r --format=$'%(committerdate:unix)\tx%(refname:lstrip=2)\tx>\tx\tx%(committerdate:relative)\tx%(objectname:short)\tx\tx%(contents:subject)' |
|
|
537
|
+
while IFS=$'\t' read -r epoch remote_ref head worktree rest; do
|
|
538
|
+
remote_ref=${remote_ref#x}
|
|
539
|
+
[[ $remote_ref == */HEAD ]] && continue
|
|
540
|
+
grep -qxF "${remote_ref#*/}" <<< "$local_branches" && continue
|
|
541
|
+
printf '1\t%s\tx%s\tx%s\t%s\t%s\t%s\n' "$epoch" "${remote_ref#*/}" "$remote_ref" "$head" "$worktree" "$rest" || break
|
|
542
|
+
done
|
|
543
|
+
fi
|
|
544
|
+
} |
|
|
545
|
+
sort -t $'\t' "${sort_args[@]}" |
|
|
546
|
+
cut -f4- |
|
|
547
|
+
while IFS=$'\t' read -r branch head worktree date hash upstream subject; do
|
|
548
|
+
# strip the x prefix from all fields
|
|
549
|
+
branch=${branch#x} head=${head#x} worktree=${worktree#x} date=${date#x} hash=${hash#x} upstream=${upstream#x} subject=${subject#x}
|
|
550
|
+
# determine indicator and color: HEAD (*) takes precedence, then worktree (+)
|
|
551
|
+
local pre= suf= nm=$branch
|
|
552
|
+
if [[ $head == '*' ]]; then ind='*'; c=$c_green
|
|
553
|
+
elif [[ -n $worktree ]]; then ind='+'; c=$c_cyan
|
|
554
|
+
elif [[ $head == '>' ]]; then
|
|
555
|
+
# remote-only: mute the remote part, as a prefix (remote/name) or,
|
|
556
|
+
# when sorting by name alone, as a suffix: name (remote)
|
|
557
|
+
ind=' '; c=$c_reset
|
|
558
|
+
if [[ $order == name ]]; then nm=${branch#*/} suf=" (${branch%%/*})"; else pre=${branch%%/*}/ nm=${branch#*/}; fi
|
|
559
|
+
else ind=' '; c=$c_reset
|
|
560
|
+
fi
|
|
561
|
+
# worktrees also show where they are, searchable too, when that is
|
|
562
|
+
# unexpected: under the root but drifted from the branch name (wtcode puts
|
|
563
|
+
# branch X at <root>/X), or somewhere else entirely. the main checkout and
|
|
564
|
+
# in-place worktrees show nothing. local/remote scopes are about branches
|
|
565
|
+
if [[ -n $worktree && $worktree != "$main_wt" && ( $scope == all || $scope == worktree ) ]]; then
|
|
566
|
+
local rel=$worktree
|
|
567
|
+
if [[ -n $wt_root && $worktree == "$wt_root"/* ]]; then
|
|
568
|
+
rel=${worktree#"$wt_root"/}
|
|
569
|
+
elif [[ $worktree == "$HOME"/* ]]; then
|
|
570
|
+
rel="~/${worktree#"$HOME"/}"
|
|
571
|
+
fi
|
|
572
|
+
(( ${#rel} > 28 )) && rel="..${rel: -26}"
|
|
573
|
+
[[ $rel == "$branch" ]] || suf=" @ $rel"
|
|
574
|
+
fi
|
|
575
|
+
# truncate the name to fit, then pad (before adding colors)
|
|
576
|
+
local room=$(( 50 - ${#pre} - ${#suf} )) padw
|
|
577
|
+
(( room < 4 )) && room=4
|
|
578
|
+
(( ${#nm} > room )) && nm="${nm:0:room-2}.."
|
|
579
|
+
padw=$(( 50 - ${#pre} - ${#nm} - ${#suf} )); (( padw < 0 )) && padw=0
|
|
580
|
+
printf -v pad '%*s' "$padw" ''
|
|
581
|
+
printf -v date '%14s' "$date"
|
|
582
|
+
# build display fields with colors embedded
|
|
583
|
+
name_shown="${c_muted}${pre}${c_reset}${c}${nm}${c_reset}${c_muted}${suf}${c_reset}${pad}"
|
|
584
|
+
date_shown="${c_blue}${date}${c_reset}"
|
|
585
|
+
hash_shown="${c_yellow}${hash}${c_reset}"
|
|
586
|
+
if [[ -n $upstream ]]; then upstream_shown="${c_red}${upstream}${c_reset} "; else upstream_shown=''; fi
|
|
587
|
+
printf '%s\t%s %s\t%s %s %s%s\n' \
|
|
588
|
+
"$branch" "$ind" "$name_shown" "$date_shown" "$hash_shown" "$upstream_shown" "$subject" || break
|
|
589
|
+
done
|
|
590
|
+
}
|
|
591
|
+
|
|
110
592
|
###############################################################################
|
|
111
593
|
## --enter-git-worktree -- select branch and create/switch worktree
|
|
112
594
|
###############################################################################
|
|
@@ -118,60 +600,22 @@ WTCODE_CMDS_TO_TRY=(
|
|
|
118
600
|
if [[ $# -gt 0 ]]; then
|
|
119
601
|
branch_name=$1; shift
|
|
120
602
|
elif type fzf &>/dev/null; then
|
|
121
|
-
# if branch name unspecified, use fzf to select one or enter a new name
|
|
122
|
-
#
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
remote_ref=${remote_ref#x}
|
|
138
|
-
[[ $remote_ref == */HEAD ]] && continue
|
|
139
|
-
local_name=${remote_ref#*/}
|
|
140
|
-
grep -qxF "$local_name" <<< "$local_branches" && continue
|
|
141
|
-
printf 'x%s\t%s\n' "$remote_ref" "$rest"
|
|
142
|
-
done
|
|
143
|
-
} |
|
|
144
|
-
while IFS=$'\t' read -r branch head worktree date hash upstream subject; do
|
|
145
|
-
# strip the x prefix from all fields
|
|
146
|
-
branch=${branch#x} head=${head#x} worktree=${worktree#x} date=${date#x} hash=${hash#x} upstream=${upstream#x} subject=${subject#x}
|
|
147
|
-
# determine indicator and color: HEAD (*) takes precedence, then worktree (+)
|
|
148
|
-
if [[ $head == '*' ]]; then ind='*'; c=$c_green
|
|
149
|
-
elif [[ -n $worktree ]]; then ind='+'; c=$c_cyan
|
|
150
|
-
elif [[ $head == '>' ]]; then ind=' '; c=$c_dim
|
|
151
|
-
else ind=' '; c=$c_reset
|
|
152
|
-
fi
|
|
153
|
-
# truncate then pad branch name, right-pad date (before adding colors)
|
|
154
|
-
if (( ${#branch} > 50 )); then name="${branch:0:48}.."; else name=$branch; fi
|
|
155
|
-
printf -v name '%-50s' "$name"
|
|
156
|
-
printf -v date '%14s' "$date"
|
|
157
|
-
# build display fields with colors embedded
|
|
158
|
-
name_shown="${c}${name}${c_reset}"
|
|
159
|
-
date_shown="${c_blue}${date}${c_reset}"
|
|
160
|
-
hash_shown="${c_yellow}${hash}${c_reset}"
|
|
161
|
-
if [[ -n $upstream ]]; then upstream_shown="${c_red}${upstream}${c_reset} "; else upstream_shown=''; fi
|
|
162
|
-
printf '%s\t%s %s\t%s %s %s%s\n' \
|
|
163
|
-
"$branch" "$ind" "$name_shown" "$date_shown" "$hash_shown" "$upstream_shown" "$subject"
|
|
164
|
-
done |
|
|
165
|
-
fzf --ansi --color --tmux 90%,70% --print-query --delimiter=$'\t' --with-nth=2 --nth=1 \
|
|
166
|
-
--preview 'echo {3}' --preview-window 'down:2:wrap' \
|
|
167
|
-
--header "$(
|
|
168
|
-
echo '[wtcode] Select the worktree/branch or create new to launch your code tool in it.'
|
|
169
|
-
echo ' Typing colons `:` avoids selecting an existing one to create new with similar names.'
|
|
170
|
-
)" \
|
|
171
|
-
--prompt '> ' |
|
|
172
|
-
cut -f1 |
|
|
173
|
-
tail -1
|
|
174
|
-
)
|
|
603
|
+
# if branch name unspecified, use fzf to select one or enter a new name.
|
|
604
|
+
# key bindings cycle scope/order/base/tool, see --fzf-* below
|
|
605
|
+
local fzf_dir
|
|
606
|
+
fzf_dir=$(--fzf-init)
|
|
607
|
+
# fzf prints the typed query, then the selection (if any)
|
|
608
|
+
local picked
|
|
609
|
+
picked=$(--fzf-pick "$fzf_dir")
|
|
610
|
+
if [[ -e $fzf_dir/force_new ]]; then
|
|
611
|
+
# alt-enter: the typed text is a new name, never an existing ref
|
|
612
|
+
branch_name=$(head -1 <<< "$picked")
|
|
613
|
+
[[ -n $branch_name ]] && branch_name=:$branch_name
|
|
614
|
+
else
|
|
615
|
+
branch_name=$(cut -f1 <<< "$picked" | tail -1)
|
|
616
|
+
fi
|
|
617
|
+
[[ -z ${branch_name-} ]] || --fzf-commit "$fzf_dir"
|
|
618
|
+
rm -rf "$fzf_dir"
|
|
175
619
|
else # abort if fzf not available
|
|
176
620
|
branch_name=${1:?Need a worktree/branch name as first argument}; shift
|
|
177
621
|
fi
|
|
@@ -272,7 +716,7 @@ WTCODE_CMDS_TO_TRY=(
|
|
|
272
716
|
# set WTCODE_USE_CURRENT_BRANCH=true to always fork from the current
|
|
273
717
|
# branch instead
|
|
274
718
|
local base_ref
|
|
275
|
-
base_ref=$(--default-base-ref)
|
|
719
|
+
base_ref=${WTCODE_PICKED_BASE:-$(--default-base-ref)}
|
|
276
720
|
--msg "creating worktree with new branch: $branch_name (from $base_ref)"
|
|
277
721
|
git worktree add -b "$branch_name" "$worktree_path" "$base_ref"
|
|
278
722
|
fi
|
|
@@ -465,8 +909,14 @@ APPLESCRIPT
|
|
|
465
909
|
###############################################################################
|
|
466
910
|
--launch-code-tool() {
|
|
467
911
|
# resolve the command to launch if not specified
|
|
912
|
+
if [[ $# -eq 0 && -n ${WTCODE_PICKED_TOOL-} ]]; then
|
|
913
|
+
# chosen in the interactive picker; may carry args, e.g. `claude --resume`
|
|
914
|
+
local _w _words=()
|
|
915
|
+
while IFS= read -r -d '' _w; do _words+=("$_w"); done < <(--split-command "$WTCODE_PICKED_TOOL")
|
|
916
|
+
[[ ${#_words[@]} -eq 0 ]] || set -- "${_words[@]}"
|
|
917
|
+
fi
|
|
468
918
|
if [[ $# -eq 0 ]]; then
|
|
469
|
-
for _cmd in "${
|
|
919
|
+
for _cmd in "${WTCODE_TOOLS_TO_TRY[@]}"; do
|
|
470
920
|
[[ -n "$_cmd" ]] && type "$_cmd" &>/dev/null && set -- "$_cmd" && break
|
|
471
921
|
done
|
|
472
922
|
# fall back to an interactive shell
|
|
@@ -555,6 +1005,7 @@ APPLESCRIPT
|
|
|
555
1005
|
-h() { --help "$@"; }
|
|
556
1006
|
|
|
557
1007
|
--exec() {
|
|
1008
|
+
[[ $# -gt 0 ]] && WTCODE_TOOL_FIXED=$1 # CMD given: the picker can't change it
|
|
558
1009
|
--enter-git-worktree
|
|
559
1010
|
--launch-code-tool "$@"
|
|
560
1011
|
}
|
|
@@ -568,6 +1019,13 @@ APPLESCRIPT
|
|
|
568
1019
|
--launch-code-tool "$@"
|
|
569
1020
|
}
|
|
570
1021
|
|
|
1022
|
+
# global options (before BRANCH/CMD): --base REF
|
|
1023
|
+
while [[ ${1-} == --base || ${1-} == --base=* ]]; do
|
|
1024
|
+
if [[ $1 == --base=* ]]; then WTCODE_BASE_REF=${1#--base=}; shift
|
|
1025
|
+
else WTCODE_BASE_REF=${2:?--base needs a ref}; shift 2
|
|
1026
|
+
fi
|
|
1027
|
+
done
|
|
1028
|
+
|
|
571
1029
|
# dispatch $1 as a function when it starts with -
|
|
572
1030
|
[[ ${1-} == -* ]] || set -- -- "$@"
|
|
573
1031
|
"$@"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wtcode
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Launch your favorite code tool in a git worktree
|
|
5
5
|
Author-email: Jaeho Shin <netj@sparcs.org>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -60,7 +60,7 @@ wtcode --exec CMD [CMD-ARGS...]
|
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
- **`BRANCH`** -- Git branch or worktree name. If omitted and [fzf](https://github.com/junegunn/fzf) is available, interactively select one. Surround with `:` to create a new branch (use `:::name` or `name:::` to avoid fzf matching the colons).
|
|
63
|
-
- **`CMD`** -- Command to launch in the worktree. Defaults to `$
|
|
63
|
+
- **`CMD`** -- Command to launch in the worktree. Defaults to `$WTCODE_TOOL`, or the first available of: `claude`, `aider`, `codex`, `$SHELL`.
|
|
64
64
|
When `CMD` resolves to plain `claude` (no extra args) in a worktree that already existed, wtcode launches `claude /resume` so it opens straight into your last session there. Press `Esc` in the picker to start a new session instead.
|
|
65
65
|
- **`--exec`** -- Skip the branch argument; select interactively via fzf, then launch `CMD`.
|
|
66
66
|
- **`--help`** / **`--version`** -- Show help or version info.
|
|
@@ -74,15 +74,19 @@ wtcode feature-x claude --resume # launch claude with flags
|
|
|
74
74
|
wtcode --exec claude --resume # select interactively, launch claude --resume
|
|
75
75
|
wtcode :new-feature # create new branch and worktree
|
|
76
76
|
wtcode # interactive branch selection via fzf
|
|
77
|
-
|
|
77
|
+
WTCODE_TOOL=cursor wtcode feature # use cursor as default tool
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
## Environment variables
|
|
81
81
|
|
|
82
82
|
| Variable | Description |
|
|
83
83
|
|---|---|
|
|
84
|
-
| `
|
|
84
|
+
| `WTCODE_TOOL` | Default tool to launch (e.g., `claude`, `lazygit`, `vim`, `cursor`); `WTCODE_CMD` is a deprecated alias |
|
|
85
85
|
| `WTCODE_USE_CURRENT_BRANCH` | When creating a brand-new branch, fork it from the current HEAD instead of the default: `origin/HEAD` (falling back to `origin/main`, then `origin/master`) |
|
|
86
|
+
| `WTCODE_BASE_REF` | Fork brand-new branches from this ref (also `wtcode --base REF`) |
|
|
87
|
+
| `WTCODE_BRANCH_SCOPE` | Initial picker scope: `all`, `worktree`, `local`, `remote` |
|
|
88
|
+
| `WTCODE_BRANCH_ORDER` | Initial picker order: `recent`, `name`, `full` (remote/name) |
|
|
89
|
+
| `WTCODE_STATE_FILE` | Where the picker remembers its last state (default `$XDG_STATE_HOME/wtcode/state`; empty disables) |
|
|
86
90
|
| `WTCODE_DEBUG` | Enable debug tracing when set to any value |
|
|
87
91
|
| `GIT_WORKTREE_ROOT` | Override the directory where worktrees are created |
|
|
88
92
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
wtcode/__init__.py,sha256=h7R8i7Zgn-_8dPxK2hYN7eTNgSYrnmVyvmaDSvGQF_c,89
|
|
2
|
+
wtcode/__main__.py,sha256=hOvSYANO-q_rf2yy-AQoEOr18KFRSOF8Ezosf8eeFmM,335
|
|
3
|
+
wtcode/wtcode.sh,sha256=fIGxitblECbt7Rjdj8IeIHv0-pgke7BCZCccIgN6iS4,44180
|
|
4
|
+
wtcode-0.7.0.dist-info/licenses/LICENSE,sha256=MV9gr1nAVj5R60EM2BQuBhBxCG5V9ulX6-Ep_-LcLqA,1067
|
|
5
|
+
wtcode-0.7.0.dist-info/METADATA,sha256=YTVpAMP3q1RN8Yi31XUKnLZDmjgPcDofX28D-N1tdzM,4756
|
|
6
|
+
wtcode-0.7.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
7
|
+
wtcode-0.7.0.dist-info/entry_points.txt,sha256=QkSPHU86FQa-ujQk07fF4sX4DBQgWzDtgusop4AA5bc,48
|
|
8
|
+
wtcode-0.7.0.dist-info/top_level.txt,sha256=oftTSYYjDMqt33wa0c9MC15KuQ3eyRyoTLXy3PdkVfE,7
|
|
9
|
+
wtcode-0.7.0.dist-info/RECORD,,
|
wtcode-0.5.0.dist-info/RECORD
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
wtcode/__init__.py,sha256=nbWCIJu9WB6FsYqofznzsWb1GwPw1td8hQEnu2FAYJ4,89
|
|
2
|
-
wtcode/__main__.py,sha256=hOvSYANO-q_rf2yy-AQoEOr18KFRSOF8Ezosf8eeFmM,335
|
|
3
|
-
wtcode/wtcode.sh,sha256=tkm04c_aArDTAd3PWCBAvayWfsHe7227oUyafFqxNMo,22879
|
|
4
|
-
wtcode-0.5.0.dist-info/licenses/LICENSE,sha256=MV9gr1nAVj5R60EM2BQuBhBxCG5V9ulX6-Ep_-LcLqA,1067
|
|
5
|
-
wtcode-0.5.0.dist-info/METADATA,sha256=UPiHK_0pi1vWGZ22c1ker-cW70dq2hw3eelfsJxB-ec,4327
|
|
6
|
-
wtcode-0.5.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
7
|
-
wtcode-0.5.0.dist-info/entry_points.txt,sha256=QkSPHU86FQa-ujQk07fF4sX4DBQgWzDtgusop4AA5bc,48
|
|
8
|
-
wtcode-0.5.0.dist-info/top_level.txt,sha256=oftTSYYjDMqt33wa0c9MC15KuQ3eyRyoTLXy3PdkVfE,7
|
|
9
|
-
wtcode-0.5.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|