Skip to content

Commit 499a116

Browse files
davila7claude
andcommitted
feat: Add worktree-ghostty hook component
Opens a 3-panel Ghostty layout (Claude | lazygit / yazi) when creating worktrees in sibling directories. Handles both WorktreeCreate and WorktreeRemove events with a single companion script. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f52a0b9 commit 499a116

File tree

3 files changed

+1726
-1455
lines changed

3 files changed

+1726
-1455
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"description": "Worktree Ghostty Layout. Opens a 3-panel Ghostty layout when creating worktrees: Claude Code (left) | lazygit (top-right) / yazi (bottom-right). Creates worktrees in a sibling directory (../worktrees/<repo>/<name>/) and cleans up on removal. macOS only. Requires: jq, Ghostty terminal, lazygit, yazi. Ghostty keybindings required: super+d = new_split:right, super+shift+d = new_split:down.",
3+
"hooks": {
4+
"WorktreeCreate": [
5+
{
6+
"hooks": [
7+
{
8+
"type": "command",
9+
"command": "bash \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/worktree-ghostty.sh",
10+
"timeout": 30
11+
}
12+
]
13+
}
14+
],
15+
"WorktreeRemove": [
16+
{
17+
"hooks": [
18+
{
19+
"type": "command",
20+
"command": "bash \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/worktree-ghostty.sh",
21+
"timeout": 15
22+
}
23+
]
24+
}
25+
]
26+
}
27+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
#!/bin/bash
2+
# Hook: Worktree Ghostty Layout
3+
#
4+
# Creates git worktrees in a sibling directory and opens a Ghostty terminal
5+
# layout with lazygit (top-right) and yazi (bottom-right).
6+
#
7+
# Events:
8+
# - WorktreeCreate: Creates worktree + opens Ghostty 3-panel layout
9+
# - WorktreeRemove: Removes worktree, branch, and empty directories
10+
#
11+
# Requirements:
12+
# - jq (JSON parsing)
13+
# - Ghostty terminal (macOS)
14+
# - lazygit (git TUI)
15+
# - yazi (file manager TUI)
16+
#
17+
# Ghostty keybindings required:
18+
# super+d = new_split:right
19+
# super+shift+d = new_split:down
20+
21+
INPUT=$(cat)
22+
23+
if ! command -v jq &>/dev/null; then
24+
echo "jq is required but not installed" >&2
25+
exit 1
26+
fi
27+
28+
HOOK_EVENT=$(echo "$INPUT" | jq -r '.hook_event_name')
29+
CWD=$(echo "$INPUT" | jq -r '.cwd')
30+
31+
#-----------------------------------------------------------------------
32+
# WorktreeCreate: Create worktree in sibling dir + open Ghostty layout
33+
#-----------------------------------------------------------------------
34+
create_worktree() {
35+
local NAME
36+
NAME=$(echo "$INPUT" | jq -r '.name')
37+
38+
local REPO_NAME PARENT_DIR WORKTREE_DIR BRANCH_NAME
39+
REPO_NAME=$(basename "$CWD")
40+
PARENT_DIR=$(cd "$CWD/.." && pwd)
41+
WORKTREE_DIR="$PARENT_DIR/worktrees/$REPO_NAME/$NAME"
42+
BRANCH_NAME="worktree-$NAME"
43+
44+
# Detect default remote branch
45+
local DEFAULT_BRANCH
46+
DEFAULT_BRANCH=$(cd "$CWD" && git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@')
47+
: "${DEFAULT_BRANCH:=main}"
48+
49+
# Create git worktree in sibling directory
50+
mkdir -p "$PARENT_DIR/worktrees/$REPO_NAME" >&2
51+
cd "$CWD" || exit 1
52+
git fetch origin &>/dev/null || true
53+
git worktree add -b "$BRANCH_NAME" "$WORKTREE_DIR" "origin/$DEFAULT_BRANCH" >&2 || {
54+
echo "Failed to create worktree: $NAME" >&2
55+
exit 1
56+
}
57+
58+
# Open Ghostty layout: Claude (left) | lazygit (top-right) / yazi (bottom-right)
59+
# Uses clipboard + Cmd+V for reliable text input (keystroke is unreliable for long paths)
60+
{
61+
sleep 1.5
62+
osascript <<APPLESCRIPT >/dev/null 2>&1
63+
-- Save current clipboard
64+
try
65+
set oldClip to the clipboard as text
66+
on error
67+
set oldClip to ""
68+
end try
69+
70+
tell application "System Events"
71+
tell process "Ghostty"
72+
-- Split right (Cmd+D)
73+
keystroke "d" using {command down}
74+
delay 1.0
75+
76+
-- cd + lazygit
77+
set the clipboard to "cd '${WORKTREE_DIR}' && lazygit"
78+
keystroke "v" using {command down}
79+
delay 0.3
80+
key code 36
81+
delay 2.0
82+
83+
-- Split down (Cmd+Shift+D)
84+
keystroke "d" using {command down, shift down}
85+
delay 1.0
86+
87+
-- cd + yazi
88+
set the clipboard to "cd '${WORKTREE_DIR}' && yazi"
89+
keystroke "v" using {command down}
90+
delay 0.3
91+
key code 36
92+
end tell
93+
end tell
94+
95+
-- Restore clipboard
96+
delay 0.5
97+
set the clipboard to oldClip
98+
APPLESCRIPT
99+
} &>/dev/null &
100+
101+
# Output the worktree path (the ONLY stdout Claude Code reads)
102+
echo "$WORKTREE_DIR"
103+
}
104+
105+
#-----------------------------------------------------------------------
106+
# WorktreeRemove: Clean up worktree, branch, and empty directories
107+
#-----------------------------------------------------------------------
108+
remove_worktree() {
109+
local WORKTREE_PATH
110+
WORKTREE_PATH=$(echo "$INPUT" | jq -r '.worktree_path')
111+
112+
[ ! -d "$WORKTREE_PATH" ] && exit 0
113+
114+
# Find main repo (first entry in worktree list)
115+
local MAIN_REPO BRANCH_NAME
116+
MAIN_REPO=$(git -C "$WORKTREE_PATH" worktree list --porcelain 2>/dev/null | head -1 | sed 's/^worktree //')
117+
BRANCH_NAME="worktree-$(basename "$WORKTREE_PATH")"
118+
119+
# Remove worktree and branch
120+
cd "$MAIN_REPO" 2>/dev/null || exit 0
121+
git worktree remove "$WORKTREE_PATH" --force 2>/dev/null || rm -rf "$WORKTREE_PATH"
122+
git branch -D "$BRANCH_NAME" 2>/dev/null
123+
124+
# Clean up empty parent directories
125+
rmdir "$(dirname "$WORKTREE_PATH")" 2>/dev/null
126+
}
127+
128+
#-----------------------------------------------------------------------
129+
# Event dispatcher
130+
#-----------------------------------------------------------------------
131+
case "$HOOK_EVENT" in
132+
WorktreeCreate) create_worktree ;;
133+
WorktreeRemove) remove_worktree ;;
134+
esac

0 commit comments

Comments
 (0)