ghost-tab 2.6.0
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.
- package/README.md +151 -0
- package/VERSION +1 -0
- package/bin/ghost-tab +360 -0
- package/bin/npx-ghost-tab.js +141 -0
- package/ghostty/config +5 -0
- package/lib/ai-select-tui.sh +66 -0
- package/lib/ai-tools.sh +19 -0
- package/lib/config-tui.sh +95 -0
- package/lib/ghostty-config.sh +26 -0
- package/lib/input.sh +39 -0
- package/lib/install.sh +224 -0
- package/lib/loading.sh +190 -0
- package/lib/menu-tui.sh +189 -0
- package/lib/notification-setup.sh +210 -0
- package/lib/process.sh +13 -0
- package/lib/project-actions-tui.sh +29 -0
- package/lib/project-actions.sh +9 -0
- package/lib/projects.sh +18 -0
- package/lib/settings-json.sh +178 -0
- package/lib/settings-menu-tui.sh +32 -0
- package/lib/setup.sh +16 -0
- package/lib/statusline-setup.sh +60 -0
- package/lib/statusline.sh +31 -0
- package/lib/tab-title-watcher.sh +118 -0
- package/lib/terminal-select-tui.sh +119 -0
- package/lib/terminals/adapter.sh +19 -0
- package/lib/terminals/ghostty.sh +58 -0
- package/lib/terminals/iterm2.sh +51 -0
- package/lib/terminals/kitty.sh +40 -0
- package/lib/terminals/registry.sh +37 -0
- package/lib/terminals/wezterm.sh +50 -0
- package/lib/tmux-session.sh +49 -0
- package/lib/tui.sh +80 -0
- package/lib/update.sh +52 -0
- package/package.json +42 -0
- package/templates/ccstatusline-settings.json +29 -0
- package/templates/statusline-command.sh +30 -0
- package/templates/statusline-wrapper.sh +40 -0
- package/terminals/ghostty/config +5 -0
- package/terminals/kitty/config +1 -0
- package/terminals/wezterm/config.lua +4 -0
- package/wrapper.sh +222 -0
package/lib/menu-tui.sh
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# TUI wrapper for main menu
|
|
3
|
+
# Uses ghost-tab-tui main-menu subcommand
|
|
4
|
+
|
|
5
|
+
# Compute the target path for a new worktree.
|
|
6
|
+
# Args: project_path project_name branch worktree_base
|
|
7
|
+
# Outputs the computed path to stdout.
|
|
8
|
+
compute_worktree_path() {
|
|
9
|
+
local project_path="$1"
|
|
10
|
+
local project_name="$2"
|
|
11
|
+
local branch="$3"
|
|
12
|
+
local worktree_base="$4"
|
|
13
|
+
|
|
14
|
+
# Sanitize branch name: strip origin/ prefix, replace / with -
|
|
15
|
+
local sanitized="${branch#origin/}"
|
|
16
|
+
sanitized="${sanitized//\//-}"
|
|
17
|
+
|
|
18
|
+
if [[ -n "$worktree_base" ]]; then
|
|
19
|
+
echo "${worktree_base}/${project_name}--${sanitized}"
|
|
20
|
+
else
|
|
21
|
+
local parent_dir
|
|
22
|
+
parent_dir="$(dirname "$project_path")"
|
|
23
|
+
echo "${parent_dir}/${project_name}--${sanitized}"
|
|
24
|
+
fi
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# Interactive project selection using ghost-tab-tui main-menu
|
|
28
|
+
# Returns 0 if an actionable item was selected, 1 if quit/cancelled
|
|
29
|
+
# Sets: _selected_project_name, _selected_project_path, _selected_project_action, _selected_ai_tool
|
|
30
|
+
select_project_interactive() {
|
|
31
|
+
local projects_file="$1"
|
|
32
|
+
|
|
33
|
+
if ! command -v ghost-tab-tui &>/dev/null; then
|
|
34
|
+
error "ghost-tab-tui binary not found. Please reinstall."
|
|
35
|
+
return 1
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# Read preferences from settings file
|
|
39
|
+
local ghost_display="animated"
|
|
40
|
+
local tab_title="full"
|
|
41
|
+
local settings_file="${XDG_CONFIG_HOME:-$HOME/.config}/ghost-tab/settings"
|
|
42
|
+
if [ -f "$settings_file" ]; then
|
|
43
|
+
local saved_display
|
|
44
|
+
saved_display=$(grep '^ghost_display=' "$settings_file" 2>/dev/null | cut -d= -f2)
|
|
45
|
+
if [ -n "$saved_display" ]; then
|
|
46
|
+
ghost_display="$saved_display"
|
|
47
|
+
fi
|
|
48
|
+
local saved_tab_title
|
|
49
|
+
saved_tab_title=$(grep '^tab_title=' "$settings_file" 2>/dev/null | cut -d= -f2)
|
|
50
|
+
if [ -n "$saved_tab_title" ]; then
|
|
51
|
+
tab_title="$saved_tab_title"
|
|
52
|
+
fi
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
# Read sound notification state
|
|
56
|
+
local sound_name=""
|
|
57
|
+
local gt_config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/ghost-tab"
|
|
58
|
+
if type get_sound_name &>/dev/null; then
|
|
59
|
+
sound_name="$(get_sound_name "${SELECTED_AI_TOOL:-claude}" "$gt_config_dir")"
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# Build AI tools comma-separated list
|
|
63
|
+
local ai_tools_csv
|
|
64
|
+
ai_tools_csv=$(IFS=,; echo "${AI_TOOLS_AVAILABLE[*]}")
|
|
65
|
+
|
|
66
|
+
# Build command args
|
|
67
|
+
local ai_tool_file="${XDG_CONFIG_HOME:-$HOME/.config}/ghost-tab/ai-tool"
|
|
68
|
+
local cmd_args=("main-menu" "--projects-file" "$projects_file")
|
|
69
|
+
cmd_args+=("--ai-tool" "${SELECTED_AI_TOOL:-claude}")
|
|
70
|
+
cmd_args+=("--ai-tools" "$ai_tools_csv")
|
|
71
|
+
cmd_args+=("--ai-tool-file" "$ai_tool_file")
|
|
72
|
+
cmd_args+=("--ghost-display" "$ghost_display")
|
|
73
|
+
cmd_args+=("--tab-title" "$tab_title")
|
|
74
|
+
cmd_args+=("--settings-file" "$settings_file")
|
|
75
|
+
local sound_file="$gt_config_dir/${SELECTED_AI_TOOL:-claude}-features.json"
|
|
76
|
+
cmd_args+=("--sound-file" "$sound_file")
|
|
77
|
+
if [[ -n "$sound_name" ]]; then
|
|
78
|
+
cmd_args+=("--sound-name" "$sound_name")
|
|
79
|
+
fi
|
|
80
|
+
if [ -n "${_update_version:-}" ]; then
|
|
81
|
+
cmd_args+=("--update-version" "$_update_version")
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
local result
|
|
85
|
+
if ! result=$(ghost-tab-tui "${cmd_args[@]}" 2>/dev/null); then
|
|
86
|
+
return 1
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
local action
|
|
90
|
+
if ! action=$(echo "$result" | jq -r '.action' 2>/dev/null); then
|
|
91
|
+
error "Failed to parse menu response"
|
|
92
|
+
return 1
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
if [[ -z "$action" || "$action" == "null" ]]; then
|
|
96
|
+
return 1
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
# Update AI tool if changed (persist regardless of exit action)
|
|
100
|
+
local ai_tool
|
|
101
|
+
ai_tool=$(echo "$result" | jq -r '.ai_tool // ""' 2>/dev/null)
|
|
102
|
+
if [[ -n "$ai_tool" && "$ai_tool" != "null" ]]; then
|
|
103
|
+
_selected_ai_tool="$ai_tool"
|
|
104
|
+
# Persist for next session if tool changed
|
|
105
|
+
if [[ "$ai_tool" != "${SELECTED_AI_TOOL:-}" ]]; then
|
|
106
|
+
local ai_tool_file="${XDG_CONFIG_HOME:-$HOME/.config}/ghost-tab/ai-tool"
|
|
107
|
+
mkdir -p "$(dirname "$ai_tool_file")"
|
|
108
|
+
echo "$ai_tool" > "$ai_tool_file"
|
|
109
|
+
fi
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
_selected_project_action="$action"
|
|
113
|
+
|
|
114
|
+
case "$action" in
|
|
115
|
+
select-project|open-once)
|
|
116
|
+
local name path
|
|
117
|
+
name=$(echo "$result" | jq -r '.name' 2>/dev/null)
|
|
118
|
+
path=$(echo "$result" | jq -r '.path' 2>/dev/null)
|
|
119
|
+
|
|
120
|
+
if [[ -z "$name" || "$name" == "null" ]]; then
|
|
121
|
+
error "TUI returned invalid project name"
|
|
122
|
+
return 1
|
|
123
|
+
fi
|
|
124
|
+
if [[ -z "$path" || "$path" == "null" ]]; then
|
|
125
|
+
error "TUI returned invalid project path"
|
|
126
|
+
return 1
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
_selected_project_name="$name"
|
|
130
|
+
_selected_project_path="$path"
|
|
131
|
+
return 0
|
|
132
|
+
;;
|
|
133
|
+
quit)
|
|
134
|
+
return 1
|
|
135
|
+
;;
|
|
136
|
+
add-worktree)
|
|
137
|
+
local wt_project_name wt_project_path
|
|
138
|
+
wt_project_name=$(echo "$result" | jq -r '.name' 2>/dev/null)
|
|
139
|
+
wt_project_path=$(echo "$result" | jq -r '.path' 2>/dev/null)
|
|
140
|
+
|
|
141
|
+
if [[ -z "$wt_project_path" || "$wt_project_path" == "null" ]]; then
|
|
142
|
+
error "TUI returned invalid project path for worktree"
|
|
143
|
+
_selected_project_action="add-worktree"
|
|
144
|
+
return 0
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
# Launch branch picker
|
|
148
|
+
local branch_result
|
|
149
|
+
if ! branch_result=$(ghost-tab-tui select-branch --project-path "$wt_project_path" --ai-tool "${SELECTED_AI_TOOL:-claude}" 2>/dev/null); then
|
|
150
|
+
_selected_project_action="add-worktree"
|
|
151
|
+
return 0
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
local branch_selected
|
|
155
|
+
branch_selected=$(echo "$branch_result" | jq -r '.selected' 2>/dev/null)
|
|
156
|
+
if [[ "$branch_selected" != "true" ]]; then
|
|
157
|
+
_selected_project_action="add-worktree"
|
|
158
|
+
return 0
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
local branch
|
|
162
|
+
branch=$(echo "$branch_result" | jq -r '.branch' 2>/dev/null)
|
|
163
|
+
|
|
164
|
+
# Read worktree base from settings
|
|
165
|
+
local worktree_base=""
|
|
166
|
+
if [ -f "$settings_file" ]; then
|
|
167
|
+
worktree_base=$(grep '^worktree_base=' "$settings_file" 2>/dev/null | cut -d= -f2)
|
|
168
|
+
fi
|
|
169
|
+
|
|
170
|
+
# Compute worktree path
|
|
171
|
+
local wt_path
|
|
172
|
+
wt_path=$(compute_worktree_path "$wt_project_path" "$wt_project_name" "$branch" "$worktree_base")
|
|
173
|
+
|
|
174
|
+
# Create worktree
|
|
175
|
+
if git -C "$wt_project_path" worktree add "$wt_path" "$branch" 2>/dev/null; then
|
|
176
|
+
success "Created worktree at $wt_path"
|
|
177
|
+
else
|
|
178
|
+
error "Failed to create worktree for branch '$branch'"
|
|
179
|
+
fi
|
|
180
|
+
|
|
181
|
+
_selected_project_action="add-worktree"
|
|
182
|
+
return 0
|
|
183
|
+
;;
|
|
184
|
+
*)
|
|
185
|
+
# Other actions (plain-terminal, settings)
|
|
186
|
+
return 0
|
|
187
|
+
;;
|
|
188
|
+
esac
|
|
189
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Notification setup — sound hooks.
|
|
3
|
+
# Depends on: tui.sh (success, warn)
|
|
4
|
+
|
|
5
|
+
# Play notification sound if enabled for the given AI tool.
|
|
6
|
+
# Reads sound preference from features JSON and plays via afplay in background.
|
|
7
|
+
# Usage: play_notification_sound <ai_tool> <config_dir>
|
|
8
|
+
play_notification_sound() {
|
|
9
|
+
local ai_tool="$1" config_dir="$2"
|
|
10
|
+
local sound_name
|
|
11
|
+
sound_name="$(get_sound_name "$ai_tool" "$config_dir")"
|
|
12
|
+
if [[ -n "$sound_name" ]]; then
|
|
13
|
+
afplay "/System/Library/Sounds/${sound_name}.aiff" &
|
|
14
|
+
fi
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
# Set up sound notification for the given AI tool.
|
|
18
|
+
# Manages only the notification channel (to prevent double sounds).
|
|
19
|
+
# Sound playback is handled by the tab-title-watcher.
|
|
20
|
+
# Usage: setup_sound_notification <config_dir>
|
|
21
|
+
setup_sound_notification() {
|
|
22
|
+
local config_dir="$1"
|
|
23
|
+
set_claude_notif_channel "$config_dir"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# Set Claude Code's preferredNotifChannel to terminal_bell to prevent
|
|
27
|
+
# double sounds (ghost-tab hook + built-in notification).
|
|
28
|
+
# Saves the previous value to <config_dir>/prev-notif-channel for restoration.
|
|
29
|
+
# Usage: set_claude_notif_channel <config_dir>
|
|
30
|
+
set_claude_notif_channel() {
|
|
31
|
+
local config_dir="$1"
|
|
32
|
+
if ! command -v claude &>/dev/null; then
|
|
33
|
+
return 0
|
|
34
|
+
fi
|
|
35
|
+
mkdir -p "$config_dir"
|
|
36
|
+
local prev
|
|
37
|
+
prev="$(CLAUDECODE="" claude config get preferredNotifChannel 2>/dev/null || true)"
|
|
38
|
+
echo "$prev" > "$config_dir/prev-notif-channel"
|
|
39
|
+
CLAUDECODE="" claude config set preferredNotifChannel terminal_bell 2>/dev/null || true
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# Restore Claude Code's preferredNotifChannel from saved value.
|
|
43
|
+
# If no saved value exists, does nothing.
|
|
44
|
+
# Usage: restore_claude_notif_channel <config_dir>
|
|
45
|
+
restore_claude_notif_channel() {
|
|
46
|
+
local config_dir="$1"
|
|
47
|
+
local saved_file="$config_dir/prev-notif-channel"
|
|
48
|
+
if [ ! -f "$saved_file" ]; then
|
|
49
|
+
return 0
|
|
50
|
+
fi
|
|
51
|
+
if ! command -v claude &>/dev/null; then
|
|
52
|
+
return 0
|
|
53
|
+
fi
|
|
54
|
+
local prev
|
|
55
|
+
prev="$(tr -d '[:space:]' < "$saved_file")"
|
|
56
|
+
if [[ -n "$prev" ]]; then
|
|
57
|
+
CLAUDECODE="" claude config set preferredNotifChannel "$prev" 2>/dev/null || true
|
|
58
|
+
else
|
|
59
|
+
CLAUDECODE="" claude config set preferredNotifChannel "" 2>/dev/null || true
|
|
60
|
+
fi
|
|
61
|
+
rm -f "$saved_file"
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Check if sound notifications are enabled for the given AI tool.
|
|
65
|
+
# Usage: is_sound_enabled <tool> <config_dir>
|
|
66
|
+
# Outputs "true" or "false".
|
|
67
|
+
is_sound_enabled() {
|
|
68
|
+
local tool="$1" config_dir="$2"
|
|
69
|
+
local features_file="$config_dir/${tool}-features.json"
|
|
70
|
+
if [ -f "$features_file" ]; then
|
|
71
|
+
local val
|
|
72
|
+
val="$(python3 -c "
|
|
73
|
+
import json, sys
|
|
74
|
+
try:
|
|
75
|
+
d = json.load(open(sys.argv[1]))
|
|
76
|
+
print('false' if d.get('sound') is False else 'true')
|
|
77
|
+
except Exception:
|
|
78
|
+
print('true')
|
|
79
|
+
" "$features_file" 2>/dev/null)"
|
|
80
|
+
echo "${val:-true}"
|
|
81
|
+
else
|
|
82
|
+
echo "true"
|
|
83
|
+
fi
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# Get the sound name for the given AI tool.
|
|
87
|
+
# Returns the sound name (e.g. "Bottle") or empty string if sound is disabled.
|
|
88
|
+
# Usage: get_sound_name <tool> <config_dir>
|
|
89
|
+
get_sound_name() {
|
|
90
|
+
local tool="$1" config_dir="$2"
|
|
91
|
+
local features_file="$config_dir/${tool}-features.json"
|
|
92
|
+
if [ -f "$features_file" ]; then
|
|
93
|
+
python3 -c "
|
|
94
|
+
import json, sys
|
|
95
|
+
try:
|
|
96
|
+
d = json.load(open(sys.argv[1]))
|
|
97
|
+
if d.get('sound') is False:
|
|
98
|
+
print('')
|
|
99
|
+
else:
|
|
100
|
+
print(d.get('sound_name', 'Bottle'))
|
|
101
|
+
except Exception:
|
|
102
|
+
print('Bottle')
|
|
103
|
+
" "$features_file" 2>/dev/null
|
|
104
|
+
else
|
|
105
|
+
echo "Bottle"
|
|
106
|
+
fi
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# Set the sound name for the given AI tool.
|
|
110
|
+
# Usage: set_sound_name <tool> <config_dir> <name>
|
|
111
|
+
set_sound_name() {
|
|
112
|
+
local tool="$1" config_dir="$2" name="$3"
|
|
113
|
+
local features_file="$config_dir/${tool}-features.json"
|
|
114
|
+
mkdir -p "$config_dir"
|
|
115
|
+
python3 -c "
|
|
116
|
+
import json, sys
|
|
117
|
+
path = sys.argv[1]
|
|
118
|
+
name = sys.argv[2]
|
|
119
|
+
try:
|
|
120
|
+
d = json.load(open(path))
|
|
121
|
+
except Exception:
|
|
122
|
+
d = {}
|
|
123
|
+
d['sound_name'] = name
|
|
124
|
+
with open(path, 'w') as f:
|
|
125
|
+
json.dump(d, f)
|
|
126
|
+
f.write('\n')
|
|
127
|
+
" "$features_file" "$name"
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# Set sound feature flag for the given AI tool.
|
|
131
|
+
# Usage: set_sound_feature_flag <tool> <config_dir> <true|false>
|
|
132
|
+
set_sound_feature_flag() {
|
|
133
|
+
local tool="$1" config_dir="$2" enabled="$3"
|
|
134
|
+
local features_file="$config_dir/${tool}-features.json"
|
|
135
|
+
mkdir -p "$config_dir"
|
|
136
|
+
python3 -c "
|
|
137
|
+
import json, sys, os
|
|
138
|
+
path = sys.argv[1]
|
|
139
|
+
enabled = sys.argv[2] == 'true'
|
|
140
|
+
try:
|
|
141
|
+
d = json.load(open(path))
|
|
142
|
+
except Exception:
|
|
143
|
+
d = {}
|
|
144
|
+
d['sound'] = enabled
|
|
145
|
+
with open(path, 'w') as f:
|
|
146
|
+
json.dump(d, f)
|
|
147
|
+
f.write('\n')
|
|
148
|
+
" "$features_file" "$enabled"
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# Remove sound notification setup for the given AI tool.
|
|
152
|
+
# Restores the notification channel.
|
|
153
|
+
# Usage: remove_sound_notification <config_dir>
|
|
154
|
+
remove_sound_notification() {
|
|
155
|
+
local config_dir="$1"
|
|
156
|
+
restore_claude_notif_channel "$config_dir"
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
# Toggle sound notification for the given AI tool.
|
|
160
|
+
# Usage: toggle_sound_notification <tool> <config_dir>
|
|
161
|
+
# Reads current state, flips it, applies the change.
|
|
162
|
+
toggle_sound_notification() {
|
|
163
|
+
local tool="$1" config_dir="$2"
|
|
164
|
+
local current
|
|
165
|
+
current="$(is_sound_enabled "$tool" "$config_dir")"
|
|
166
|
+
|
|
167
|
+
if [[ "$current" == "true" ]]; then
|
|
168
|
+
set_sound_feature_flag "$tool" "$config_dir" false
|
|
169
|
+
case "$tool" in
|
|
170
|
+
claude)
|
|
171
|
+
remove_sound_notification "$config_dir"
|
|
172
|
+
;;
|
|
173
|
+
esac
|
|
174
|
+
success "Sound notifications disabled"
|
|
175
|
+
else
|
|
176
|
+
set_sound_feature_flag "$tool" "$config_dir" true
|
|
177
|
+
case "$tool" in
|
|
178
|
+
claude)
|
|
179
|
+
setup_sound_notification "$config_dir"
|
|
180
|
+
;;
|
|
181
|
+
esac
|
|
182
|
+
success "Sound notifications enabled"
|
|
183
|
+
fi
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
# Apply sound notification state for the given AI tool.
|
|
187
|
+
# Usage: apply_sound_notification <tool> <config_dir> <sound_name>
|
|
188
|
+
# If sound_name is empty, disables sound. Otherwise enables with that sound.
|
|
189
|
+
apply_sound_notification() {
|
|
190
|
+
local tool="$1" config_dir="$2" sound_name="$3"
|
|
191
|
+
|
|
192
|
+
if [[ -z "$sound_name" ]]; then
|
|
193
|
+
set_sound_feature_flag "$tool" "$config_dir" false
|
|
194
|
+
case "$tool" in
|
|
195
|
+
claude)
|
|
196
|
+
remove_sound_notification "$config_dir"
|
|
197
|
+
;;
|
|
198
|
+
esac
|
|
199
|
+
success "Sound notifications disabled"
|
|
200
|
+
else
|
|
201
|
+
set_sound_feature_flag "$tool" "$config_dir" true
|
|
202
|
+
set_sound_name "$tool" "$config_dir" "$sound_name"
|
|
203
|
+
case "$tool" in
|
|
204
|
+
claude)
|
|
205
|
+
setup_sound_notification "$config_dir"
|
|
206
|
+
;;
|
|
207
|
+
esac
|
|
208
|
+
success "Sound notifications enabled"
|
|
209
|
+
fi
|
|
210
|
+
}
|
package/lib/process.sh
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Process management helpers — no side effects on source.
|
|
3
|
+
|
|
4
|
+
# Recursively kills a process tree (depth-first: children first, then parent).
|
|
5
|
+
kill_tree() {
|
|
6
|
+
local pid=$1
|
|
7
|
+
local sig=${2:-TERM}
|
|
8
|
+
for child in $(pgrep -P "$pid" 2>/dev/null); do
|
|
9
|
+
kill_tree "$child" "$sig"
|
|
10
|
+
done
|
|
11
|
+
kill -"$sig" "$pid" 2>/dev/null
|
|
12
|
+
return 0
|
|
13
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Interactive TUI wrappers for project actions using ghost-tab-tui
|
|
3
|
+
|
|
4
|
+
# Interactive project addition using ghost-tab-tui
|
|
5
|
+
# Returns 0 if confirmed, 1 if cancelled
|
|
6
|
+
# Sets: _add_project_name, _add_project_path
|
|
7
|
+
add_project_interactive() {
|
|
8
|
+
if ! command -v ghost-tab-tui &>/dev/null; then
|
|
9
|
+
error "ghost-tab-tui binary not found. Please reinstall."
|
|
10
|
+
return 1
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
local result
|
|
14
|
+
if ! result=$(ghost-tab-tui add-project 2>/dev/null); then
|
|
15
|
+
return 1
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
local confirmed
|
|
19
|
+
confirmed=$(echo "$result" | jq -r '.confirmed')
|
|
20
|
+
|
|
21
|
+
if [[ "$confirmed" != "true" ]]; then
|
|
22
|
+
return 1
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
_add_project_name=$(echo "$result" | jq -r '.name')
|
|
26
|
+
_add_project_path=$(echo "$result" | jq -r '.path')
|
|
27
|
+
|
|
28
|
+
return 0
|
|
29
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Project file operations — add, delete, validate.
|
|
3
|
+
|
|
4
|
+
# Append a project entry to the projects file (creates parent dirs).
|
|
5
|
+
add_project_to_file() {
|
|
6
|
+
local name="$1" path="$2" projects_file="$3"
|
|
7
|
+
mkdir -p "$(dirname "$projects_file")"
|
|
8
|
+
echo "${name}:${path}" >> "$projects_file"
|
|
9
|
+
}
|
package/lib/projects.sh
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Project file helpers — pure, no side effects on source.
|
|
3
|
+
|
|
4
|
+
# Reads a projects file and outputs valid lines (skips blanks and comments).
|
|
5
|
+
# Usage: mapfile -t projects < <(load_projects "$file")
|
|
6
|
+
load_projects() {
|
|
7
|
+
local file="$1" line
|
|
8
|
+
[ ! -f "$file" ] && return
|
|
9
|
+
while IFS= read -r line; do
|
|
10
|
+
[[ -z "$line" || "$line" == \#* ]] && continue
|
|
11
|
+
echo "$line"
|
|
12
|
+
done < "$file"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
# Expands ~ to $HOME at the start of a path.
|
|
16
|
+
path_expand() {
|
|
17
|
+
echo "${1/#\~/$HOME}"
|
|
18
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Claude settings.json manipulation helpers.
|
|
3
|
+
|
|
4
|
+
# Merge statusLine into Claude settings.json (create if missing).
|
|
5
|
+
merge_claude_settings() {
|
|
6
|
+
local path="$1"
|
|
7
|
+
mkdir -p "$(dirname "$path")"
|
|
8
|
+
if [ -f "$path" ]; then
|
|
9
|
+
if grep -q '"statusLine"' "$path"; then
|
|
10
|
+
success "Claude status line already configured"
|
|
11
|
+
else
|
|
12
|
+
sed -i '' '$ s/}$/,\n "statusLine": {\n "type": "command",\n "command": "bash ~\/.claude\/statusline-wrapper.sh"\n }\n}/' "$path"
|
|
13
|
+
success "Added status line to Claude settings"
|
|
14
|
+
fi
|
|
15
|
+
else
|
|
16
|
+
cat > "$path" << 'CSEOF'
|
|
17
|
+
{
|
|
18
|
+
"statusLine": {
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "bash ~/.claude/statusline-wrapper.sh"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
CSEOF
|
|
24
|
+
success "Created Claude settings with status line"
|
|
25
|
+
fi
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# Add waiting indicator hooks (Stop + PreToolUse + UserPromptSubmit) to settings.json.
|
|
29
|
+
# Uses $GHOST_TAB_MARKER_FILE env var so hooks are safe outside Ghost Tab.
|
|
30
|
+
# Outputs "added", "upgraded", or "exists".
|
|
31
|
+
add_waiting_indicator_hooks() {
|
|
32
|
+
local path="$1"
|
|
33
|
+
mkdir -p "$(dirname "$path")"
|
|
34
|
+
python3 - "$path" << 'PYEOF'
|
|
35
|
+
import json, sys, os
|
|
36
|
+
|
|
37
|
+
settings_path = sys.argv[1]
|
|
38
|
+
|
|
39
|
+
if os.path.exists(settings_path):
|
|
40
|
+
try:
|
|
41
|
+
with open(settings_path, "r") as f:
|
|
42
|
+
settings = json.load(f)
|
|
43
|
+
except (json.JSONDecodeError, ValueError):
|
|
44
|
+
settings = {}
|
|
45
|
+
else:
|
|
46
|
+
settings = {}
|
|
47
|
+
|
|
48
|
+
hooks = settings.setdefault("hooks", {})
|
|
49
|
+
|
|
50
|
+
stop_cmd = 'if [ -n "$GHOST_TAB_MARKER_FILE" ]; then touch "$GHOST_TAB_MARKER_FILE"; fi'
|
|
51
|
+
clear_cmd = 'if [ -n "$GHOST_TAB_MARKER_FILE" ]; then rm -f "$GHOST_TAB_MARKER_FILE"; fi'
|
|
52
|
+
|
|
53
|
+
marker = "GHOST_TAB_MARKER_FILE"
|
|
54
|
+
|
|
55
|
+
# Check if current Stop-based format is already installed
|
|
56
|
+
stop_list = hooks.get("Stop", [])
|
|
57
|
+
stop_exists = any(
|
|
58
|
+
marker in h.get("command", "")
|
|
59
|
+
for entry in stop_list
|
|
60
|
+
for h in entry.get("hooks", [])
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Check if old Notification-based format exists (needs upgrade)
|
|
64
|
+
notif_list = hooks.get("Notification", [])
|
|
65
|
+
notif_exists = any(
|
|
66
|
+
marker in h.get("command", "")
|
|
67
|
+
for entry in notif_list
|
|
68
|
+
for h in entry.get("hooks", [])
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Check if old Stop format without matcher exists (needs upgrade)
|
|
72
|
+
pre_list = hooks.get("PreToolUse", [])
|
|
73
|
+
old_stop_needs_upgrade = stop_exists and not any(
|
|
74
|
+
entry.get("matcher") == "AskUserQuestion"
|
|
75
|
+
for entry in pre_list
|
|
76
|
+
if any(marker in h.get("command", "") for h in entry.get("hooks", []))
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if stop_exists and not old_stop_needs_upgrade:
|
|
80
|
+
# Current Stop format already installed
|
|
81
|
+
print("exists")
|
|
82
|
+
sys.exit(0)
|
|
83
|
+
elif notif_exists or old_stop_needs_upgrade:
|
|
84
|
+
# Old format — remove ghost-tab hooks so they get re-added below
|
|
85
|
+
for event in ["Stop", "Notification", "PreToolUse", "UserPromptSubmit"]:
|
|
86
|
+
event_list = hooks.get(event, [])
|
|
87
|
+
new_list = [
|
|
88
|
+
entry for entry in event_list
|
|
89
|
+
if not any(marker in h.get("command", "") for h in entry.get("hooks", []))
|
|
90
|
+
]
|
|
91
|
+
if new_list:
|
|
92
|
+
hooks[event] = new_list
|
|
93
|
+
elif event in hooks:
|
|
94
|
+
del hooks[event]
|
|
95
|
+
action = "upgraded"
|
|
96
|
+
else:
|
|
97
|
+
action = "added"
|
|
98
|
+
|
|
99
|
+
# Add Stop hook (fires immediately when Claude stops generating)
|
|
100
|
+
hooks.setdefault("Stop", []).append({
|
|
101
|
+
"hooks": [{"type": "command", "command": stop_cmd}]
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
# Add PreToolUse hook with matcher for AskUserQuestion (creates marker — user input needed)
|
|
105
|
+
hooks.setdefault("PreToolUse", []).append({
|
|
106
|
+
"matcher": "AskUserQuestion",
|
|
107
|
+
"hooks": [{"type": "command", "command": stop_cmd}]
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
# Add PreToolUse catch-all hook (clears marker — Claude is actively working)
|
|
111
|
+
hooks.setdefault("PreToolUse", []).append({
|
|
112
|
+
"hooks": [{"type": "command", "command": clear_cmd}]
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
# Add UserPromptSubmit hook (clears marker when user answers)
|
|
116
|
+
hooks.setdefault("UserPromptSubmit", []).append({
|
|
117
|
+
"hooks": [{"type": "command", "command": clear_cmd}]
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
with open(settings_path, "w") as f:
|
|
121
|
+
json.dump(settings, f, indent=2)
|
|
122
|
+
f.write("\n")
|
|
123
|
+
|
|
124
|
+
print(action)
|
|
125
|
+
PYEOF
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# Remove waiting indicator hooks from settings.json.
|
|
129
|
+
# Outputs "removed" or "not_found".
|
|
130
|
+
remove_waiting_indicator_hooks() {
|
|
131
|
+
local path="$1"
|
|
132
|
+
if [ ! -f "$path" ]; then
|
|
133
|
+
echo "not_found"
|
|
134
|
+
return 0
|
|
135
|
+
fi
|
|
136
|
+
python3 - "$path" << 'PYEOF'
|
|
137
|
+
import json, sys, os
|
|
138
|
+
|
|
139
|
+
settings_path = sys.argv[1]
|
|
140
|
+
marker = "GHOST_TAB_MARKER_FILE"
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
with open(settings_path, "r") as f:
|
|
144
|
+
settings = json.load(f)
|
|
145
|
+
except (json.JSONDecodeError, ValueError, FileNotFoundError):
|
|
146
|
+
print("not_found")
|
|
147
|
+
sys.exit(0)
|
|
148
|
+
|
|
149
|
+
hooks = settings.get("hooks", {})
|
|
150
|
+
found = False
|
|
151
|
+
|
|
152
|
+
for event in ["Stop", "Notification", "PreToolUse", "UserPromptSubmit"]:
|
|
153
|
+
event_list = hooks.get(event, [])
|
|
154
|
+
new_list = [
|
|
155
|
+
entry for entry in event_list
|
|
156
|
+
if not any(marker in h.get("command", "") for h in entry.get("hooks", []))
|
|
157
|
+
]
|
|
158
|
+
if len(new_list) != len(event_list):
|
|
159
|
+
found = True
|
|
160
|
+
if new_list:
|
|
161
|
+
hooks[event] = new_list
|
|
162
|
+
else:
|
|
163
|
+
del hooks[event]
|
|
164
|
+
|
|
165
|
+
if not found:
|
|
166
|
+
print("not_found")
|
|
167
|
+
sys.exit(0)
|
|
168
|
+
|
|
169
|
+
if not hooks:
|
|
170
|
+
del settings["hooks"]
|
|
171
|
+
|
|
172
|
+
with open(settings_path, "w") as f:
|
|
173
|
+
json.dump(settings, f, indent=2)
|
|
174
|
+
f.write("\n")
|
|
175
|
+
|
|
176
|
+
print("removed")
|
|
177
|
+
PYEOF
|
|
178
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# TUI wrapper for settings menu
|
|
3
|
+
# Uses ghost-tab-tui settings-menu subcommand
|
|
4
|
+
|
|
5
|
+
# Interactive settings menu using ghost-tab-tui
|
|
6
|
+
# Returns action string (or empty if quit)
|
|
7
|
+
settings_menu_interactive() {
|
|
8
|
+
if ! command -v ghost-tab-tui &>/dev/null; then
|
|
9
|
+
error "ghost-tab-tui binary not found. Please reinstall."
|
|
10
|
+
return 1
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
local result
|
|
14
|
+
if ! result=$(ghost-tab-tui settings-menu 2>/dev/null); then
|
|
15
|
+
return 1
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
local action
|
|
19
|
+
if ! action=$(echo "$result" | jq -r '.action' 2>/dev/null); then
|
|
20
|
+
error "Failed to parse settings menu response"
|
|
21
|
+
return 1
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
# Validate null/empty (empty is OK for quit, but null is not)
|
|
25
|
+
if [[ "$action" == "null" ]]; then
|
|
26
|
+
error "TUI returned invalid action"
|
|
27
|
+
return 1
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
echo "$action"
|
|
31
|
+
return 0
|
|
32
|
+
}
|