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/setup.sh
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Setup helper functions — pure, no side effects on source.
|
|
3
|
+
|
|
4
|
+
# Determines the SHARE_DIR (where supporting files live).
|
|
5
|
+
# Usage: resolve_share_dir "$SCRIPT_DIR" "$BREW_PREFIX"
|
|
6
|
+
# When script is in $BREW_PREFIX/bin, returns $BREW_PREFIX/share/ghost-tab.
|
|
7
|
+
# Otherwise, returns the parent of the script directory.
|
|
8
|
+
resolve_share_dir() {
|
|
9
|
+
local script_dir="$1"
|
|
10
|
+
local brew_prefix="$2"
|
|
11
|
+
if [[ -n "$brew_prefix" && "$script_dir" == "$brew_prefix/bin" ]]; then
|
|
12
|
+
echo "$brew_prefix/share/ghost-tab"
|
|
13
|
+
else
|
|
14
|
+
(cd "$script_dir/.." && pwd)
|
|
15
|
+
fi
|
|
16
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Statusline setup — install ccstatusline, copy configs and scripts.
|
|
3
|
+
# Depends on: tui.sh (success, warn, info), settings-json.sh (merge_claude_settings)
|
|
4
|
+
|
|
5
|
+
# Check whether npm is available. Extracted for testability.
|
|
6
|
+
_has_npm() { command -v npm &>/dev/null; }
|
|
7
|
+
|
|
8
|
+
# Install and configure the Claude Code status line.
|
|
9
|
+
# Usage: setup_statusline <share_dir> <claude_settings_path> <home_dir>
|
|
10
|
+
setup_statusline() {
|
|
11
|
+
local share_dir="$1" claude_settings_path="$2" home_dir="$3"
|
|
12
|
+
|
|
13
|
+
# Check for npm, install Node.js LTS if needed
|
|
14
|
+
if ! _has_npm; then
|
|
15
|
+
info "Installing Node.js LTS..."
|
|
16
|
+
if brew install node@22 &>/dev/null; then
|
|
17
|
+
export PATH="/opt/homebrew/opt/node@22/bin:$PATH"
|
|
18
|
+
success "Node.js LTS installed"
|
|
19
|
+
else
|
|
20
|
+
warn "Node.js installation failed — skipping status line setup"
|
|
21
|
+
return 0
|
|
22
|
+
fi
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
if ! _has_npm; then
|
|
26
|
+
return 0
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# Install ccstatusline
|
|
30
|
+
if npm list -g ccstatusline &>/dev/null; then
|
|
31
|
+
success "ccstatusline already installed"
|
|
32
|
+
else
|
|
33
|
+
info "Installing ccstatusline..."
|
|
34
|
+
if npm install -g ccstatusline &>/dev/null; then
|
|
35
|
+
success "ccstatusline installed"
|
|
36
|
+
else
|
|
37
|
+
warn "Failed to install ccstatusline — skipping status line setup"
|
|
38
|
+
return 0
|
|
39
|
+
fi
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
if npm list -g ccstatusline &>/dev/null; then
|
|
43
|
+
# Create ccstatusline config
|
|
44
|
+
mkdir -p "$home_dir/.config/ccstatusline"
|
|
45
|
+
cp "$share_dir/templates/ccstatusline-settings.json" "$home_dir/.config/ccstatusline/settings.json"
|
|
46
|
+
success "Created ccstatusline config"
|
|
47
|
+
|
|
48
|
+
# Create statusline scripts
|
|
49
|
+
mkdir -p "$home_dir/.claude"
|
|
50
|
+
cp "$share_dir/templates/statusline-command.sh" "$home_dir/.claude/statusline-command.sh"
|
|
51
|
+
cp "$share_dir/templates/statusline-wrapper.sh" "$home_dir/.claude/statusline-wrapper.sh"
|
|
52
|
+
cp "$share_dir/lib/statusline.sh" "$home_dir/.claude/statusline-helpers.sh"
|
|
53
|
+
chmod +x "$home_dir/.claude/statusline-command.sh"
|
|
54
|
+
chmod +x "$home_dir/.claude/statusline-wrapper.sh"
|
|
55
|
+
success "Created statusline scripts"
|
|
56
|
+
|
|
57
|
+
# Update Claude settings.json
|
|
58
|
+
merge_claude_settings "$claude_settings_path"
|
|
59
|
+
fi
|
|
60
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Statusline helper functions — pure, no side effects on source.
|
|
3
|
+
|
|
4
|
+
# Returns total RSS in KB for a process and all its descendants.
|
|
5
|
+
# Usage: get_tree_rss_kb 12345 => "92160"
|
|
6
|
+
get_tree_rss_kb() {
|
|
7
|
+
local root_pid="$1"
|
|
8
|
+
local total=0
|
|
9
|
+
local queue=("$root_pid")
|
|
10
|
+
|
|
11
|
+
while [ ${#queue[@]} -gt 0 ]; do
|
|
12
|
+
local pid="${queue[0]}"
|
|
13
|
+
queue=("${queue[@]:1}")
|
|
14
|
+
|
|
15
|
+
local rss
|
|
16
|
+
rss=$(ps -o rss= -p "$pid" 2>/dev/null | tr -d ' ')
|
|
17
|
+
if [ -n "$rss" ] && [ "$rss" -gt 0 ] 2>/dev/null; then
|
|
18
|
+
total=$((total + rss))
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
local children
|
|
22
|
+
children=$(pgrep -P "$pid" 2>/dev/null) || true
|
|
23
|
+
if [ -n "$children" ]; then
|
|
24
|
+
while IFS= read -r child; do
|
|
25
|
+
queue+=("$child")
|
|
26
|
+
done <<< "$children"
|
|
27
|
+
fi
|
|
28
|
+
done
|
|
29
|
+
|
|
30
|
+
echo "$total"
|
|
31
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tab title watcher — detects AI tool waiting state, updates terminal tab title.
|
|
3
|
+
# Depends on: tui.sh (set_tab_title, set_tab_title_waiting)
|
|
4
|
+
|
|
5
|
+
_TAB_TITLE_WATCHER_PID=""
|
|
6
|
+
|
|
7
|
+
# Return the age of the marker file in seconds.
|
|
8
|
+
# Usage: marker_age <file>
|
|
9
|
+
# Outputs the number of seconds since the file was last modified.
|
|
10
|
+
# Returns 1 if the file does not exist or stat fails.
|
|
11
|
+
marker_age() {
|
|
12
|
+
local file="$1"
|
|
13
|
+
local now mtime
|
|
14
|
+
now=$(date +%s)
|
|
15
|
+
mtime=$(stat -f %m "$file" 2>/dev/null) || return 1
|
|
16
|
+
echo $(( now - mtime ))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# Check if the AI tool is waiting for user input.
|
|
20
|
+
# Usage: check_ai_tool_state <ai_tool> <session_name> <tmux_cmd> <marker_file> <pane_index>
|
|
21
|
+
# Outputs "waiting" or "active".
|
|
22
|
+
check_ai_tool_state() {
|
|
23
|
+
local ai_tool="$1" session_name="$2" tmux_cmd="$3" marker_file="$4"
|
|
24
|
+
local pane_index="${5:-3}"
|
|
25
|
+
|
|
26
|
+
if [ "$ai_tool" = "claude" ]; then
|
|
27
|
+
# Claude uses marker-file-only detection.
|
|
28
|
+
# Stop hook creates the marker (Claude stopped generating).
|
|
29
|
+
# UserPromptSubmit hook removes it (user answered).
|
|
30
|
+
# PreToolUse hook also removes it (Claude is calling tools).
|
|
31
|
+
if [ -f "$marker_file" ]; then
|
|
32
|
+
echo "waiting"
|
|
33
|
+
else
|
|
34
|
+
echo "active"
|
|
35
|
+
fi
|
|
36
|
+
else
|
|
37
|
+
local content last_line
|
|
38
|
+
content=$("$tmux_cmd" capture-pane -t "$session_name:0.$pane_index" -p 2>/dev/null || true)
|
|
39
|
+
last_line=$(echo "$content" | grep -v '^$' | tail -1)
|
|
40
|
+
if echo "$last_line" | grep -qE '[>$❯]\s*$'; then
|
|
41
|
+
echo "waiting"
|
|
42
|
+
else
|
|
43
|
+
echo "active"
|
|
44
|
+
fi
|
|
45
|
+
fi
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Discover the AI tool pane (rightmost pane in the tmux session).
|
|
49
|
+
# Usage: discover_ai_pane <session_name> <tmux_cmd>
|
|
50
|
+
# Outputs the pane index of the rightmost pane.
|
|
51
|
+
discover_ai_pane() {
|
|
52
|
+
local session_name="$1" tmux_cmd="$2"
|
|
53
|
+
"$tmux_cmd" list-panes -t "$session_name" -F '#{pane_index} #{pane_left}' 2>/dev/null \
|
|
54
|
+
| sort -k2 -rn | head -1 | awk '{print $1}'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# Start the tab title watcher background loop.
|
|
58
|
+
# Usage: start_tab_title_watcher <session_name> <ai_tool> <project_name> <tab_title_setting> <tmux_cmd> <marker_file> [config_dir]
|
|
59
|
+
start_tab_title_watcher() {
|
|
60
|
+
local session_name="$1" ai_tool="$2" project_name="$3"
|
|
61
|
+
local tab_title_setting="$4" tmux_cmd="$5" marker_file="$6"
|
|
62
|
+
local config_dir="${7:-}"
|
|
63
|
+
|
|
64
|
+
(
|
|
65
|
+
# Find the AI tool pane (rightmost pane in the layout)
|
|
66
|
+
local ai_pane=""
|
|
67
|
+
while [ -z "$ai_pane" ]; do
|
|
68
|
+
ai_pane=$(discover_ai_pane "$session_name" "$tmux_cmd")
|
|
69
|
+
[ -z "$ai_pane" ] && sleep 0.5
|
|
70
|
+
done
|
|
71
|
+
|
|
72
|
+
local was_waiting=false
|
|
73
|
+
while true; do
|
|
74
|
+
sleep 0.5
|
|
75
|
+
local state
|
|
76
|
+
state=$(check_ai_tool_state "$ai_tool" "$session_name" "$tmux_cmd" "$marker_file" "$ai_pane")
|
|
77
|
+
|
|
78
|
+
if [ "$state" = "waiting" ] && [ "$was_waiting" = false ]; then
|
|
79
|
+
# Debounce: only notify if marker has existed for >= 1 second.
|
|
80
|
+
# Subagent completions create/remove markers within milliseconds,
|
|
81
|
+
# so they never reach the 1s threshold.
|
|
82
|
+
local age
|
|
83
|
+
age=$(marker_age "$marker_file") || continue
|
|
84
|
+
if [ "$age" -ge 1 ]; then
|
|
85
|
+
if [ "$tab_title_setting" = "full" ]; then
|
|
86
|
+
set_tab_title_waiting "$project_name" "$ai_tool"
|
|
87
|
+
else
|
|
88
|
+
set_tab_title_waiting "$project_name"
|
|
89
|
+
fi
|
|
90
|
+
if [[ -n "$config_dir" ]]; then
|
|
91
|
+
play_notification_sound "$ai_tool" "$config_dir"
|
|
92
|
+
fi
|
|
93
|
+
was_waiting=true
|
|
94
|
+
fi
|
|
95
|
+
elif [ "$state" = "active" ] && [ "$was_waiting" = true ]; then
|
|
96
|
+
if [ "$tab_title_setting" = "full" ]; then
|
|
97
|
+
set_tab_title "$project_name" "$ai_tool"
|
|
98
|
+
else
|
|
99
|
+
set_tab_title "$project_name"
|
|
100
|
+
fi
|
|
101
|
+
was_waiting=false
|
|
102
|
+
fi
|
|
103
|
+
done
|
|
104
|
+
) &
|
|
105
|
+
_TAB_TITLE_WATCHER_PID=$!
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# Stop the tab title watcher and clean up.
|
|
109
|
+
# Usage: stop_tab_title_watcher [marker_file]
|
|
110
|
+
stop_tab_title_watcher() {
|
|
111
|
+
local marker_file="${1:-}"
|
|
112
|
+
if [ -n "$_TAB_TITLE_WATCHER_PID" ]; then
|
|
113
|
+
kill "$_TAB_TITLE_WATCHER_PID" 2>/dev/null || true
|
|
114
|
+
fi
|
|
115
|
+
if [ -n "$marker_file" ]; then
|
|
116
|
+
rm -f "$marker_file"
|
|
117
|
+
fi
|
|
118
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Terminal selection TUI wrapper using ghost-tab-tui
|
|
3
|
+
|
|
4
|
+
# Interactive terminal selection.
|
|
5
|
+
# Returns 0 if selected, 1 if cancelled.
|
|
6
|
+
# Sets: _selected_terminal
|
|
7
|
+
#
|
|
8
|
+
# If GHOST_TAB_TERMINAL_PREF is set, reads current terminal from that file
|
|
9
|
+
# and passes it to the TUI via --current flag.
|
|
10
|
+
select_terminal_interactive() {
|
|
11
|
+
if ! command -v ghost-tab-tui &>/dev/null; then
|
|
12
|
+
error "ghost-tab-tui binary not found. Please reinstall."
|
|
13
|
+
return 1
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
local _tui_stderr
|
|
17
|
+
_tui_stderr="$(mktemp 2>/dev/null || echo "/tmp/ghost-tab-tui-err.$$")"
|
|
18
|
+
|
|
19
|
+
while true; do
|
|
20
|
+
# Build command args
|
|
21
|
+
local args=("select-terminal")
|
|
22
|
+
if [[ -n "$GHOST_TAB_TERMINAL_PREF" && -f "$GHOST_TAB_TERMINAL_PREF" ]]; then
|
|
23
|
+
local current
|
|
24
|
+
current="$(tr -d '[:space:]' < "$GHOST_TAB_TERMINAL_PREF")"
|
|
25
|
+
if [[ -n "$current" ]]; then
|
|
26
|
+
args+=("--current" "$current")
|
|
27
|
+
fi
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
local result
|
|
31
|
+
# Capture output regardless of exit code — bubbletea may exit non-zero
|
|
32
|
+
# during cleanup even when the user completed an action successfully.
|
|
33
|
+
# Capture stderr to a temp file so we can show it on failure.
|
|
34
|
+
result=$(ghost-tab-tui "${args[@]}" 2>"$_tui_stderr") || true
|
|
35
|
+
|
|
36
|
+
# If no output at all, the TUI failed to run
|
|
37
|
+
if [[ -z "$result" ]]; then
|
|
38
|
+
local tui_err=""
|
|
39
|
+
if [[ -f "$_tui_stderr" ]]; then
|
|
40
|
+
tui_err="$(cat "$_tui_stderr")"
|
|
41
|
+
fi
|
|
42
|
+
if [[ -n "$tui_err" ]]; then
|
|
43
|
+
error "Terminal selector failed: $tui_err"
|
|
44
|
+
else
|
|
45
|
+
error "Terminal selector failed to produce output. Try running: ghost-tab-tui select-terminal"
|
|
46
|
+
fi
|
|
47
|
+
rm -f "$_tui_stderr"
|
|
48
|
+
return 1
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
local selected
|
|
52
|
+
if ! selected=$(echo "$result" | jq -r '.selected' 2>/dev/null); then
|
|
53
|
+
error "Failed to parse terminal selection response"
|
|
54
|
+
rm -f "$_tui_stderr"
|
|
55
|
+
return 1
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
# Check for install action — run brew then loop back to TUI
|
|
59
|
+
local action
|
|
60
|
+
action=$(echo "$result" | jq -r '.action // empty' 2>/dev/null)
|
|
61
|
+
if [[ "$action" == "install" ]]; then
|
|
62
|
+
local cask terminal
|
|
63
|
+
cask=$(echo "$result" | jq -r '.cask // empty' 2>/dev/null)
|
|
64
|
+
terminal=$(echo "$result" | jq -r '.terminal // empty' 2>/dev/null)
|
|
65
|
+
# Fall back to terminal name if cask field is missing
|
|
66
|
+
if [[ -z "$cask" ]]; then
|
|
67
|
+
cask="$terminal"
|
|
68
|
+
fi
|
|
69
|
+
if [[ -n "$cask" && "$cask" != "null" ]]; then
|
|
70
|
+
echo ""
|
|
71
|
+
info "Installing $cask via Homebrew..."
|
|
72
|
+
if brew install --cask "$cask"; then
|
|
73
|
+
success "Installed $cask"
|
|
74
|
+
echo ""
|
|
75
|
+
# Auto-select the just-installed terminal
|
|
76
|
+
_selected_terminal="$terminal"
|
|
77
|
+
rm -f "$_tui_stderr"
|
|
78
|
+
return 0
|
|
79
|
+
else
|
|
80
|
+
error "Failed to install $cask"
|
|
81
|
+
fi
|
|
82
|
+
echo ""
|
|
83
|
+
fi
|
|
84
|
+
continue
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
# User cancelled (or TUI failed and fell back to selected:false)
|
|
88
|
+
if [[ -z "$selected" || "$selected" == "null" || "$selected" != "true" ]]; then
|
|
89
|
+
# If stderr has content, the TUI likely failed — show the error
|
|
90
|
+
local tui_err=""
|
|
91
|
+
if [[ -f "$_tui_stderr" ]]; then
|
|
92
|
+
tui_err="$(cat "$_tui_stderr")"
|
|
93
|
+
fi
|
|
94
|
+
if [[ -n "$tui_err" ]]; then
|
|
95
|
+
error "Terminal selector failed: $tui_err"
|
|
96
|
+
fi
|
|
97
|
+
rm -f "$_tui_stderr"
|
|
98
|
+
return 1
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
# User selected an installed terminal
|
|
102
|
+
local terminal
|
|
103
|
+
if ! terminal=$(echo "$result" | jq -r '.terminal' 2>/dev/null); then
|
|
104
|
+
error "Failed to parse selected terminal"
|
|
105
|
+
rm -f "$_tui_stderr"
|
|
106
|
+
return 1
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
if [[ -z "$terminal" || "$terminal" == "null" ]]; then
|
|
110
|
+
error "TUI returned empty terminal selection"
|
|
111
|
+
rm -f "$_tui_stderr"
|
|
112
|
+
return 1
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
_selected_terminal="$terminal"
|
|
116
|
+
rm -f "$_tui_stderr"
|
|
117
|
+
return 0
|
|
118
|
+
done
|
|
119
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Terminal adapter loader — sources the correct adapter for a terminal.
|
|
3
|
+
|
|
4
|
+
# Load the adapter for the given terminal identifier.
|
|
5
|
+
# After calling this, terminal_setup_config, terminal_get_config_path, etc. are available.
|
|
6
|
+
load_terminal_adapter() {
|
|
7
|
+
local terminal="$1"
|
|
8
|
+
local adapter_dir
|
|
9
|
+
adapter_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
10
|
+
|
|
11
|
+
local adapter_file="$adapter_dir/${terminal}.sh"
|
|
12
|
+
if [ ! -f "$adapter_file" ]; then
|
|
13
|
+
error "Unknown terminal: $terminal"
|
|
14
|
+
return 1
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
# shellcheck disable=SC1090 # Dynamic adapter loading
|
|
18
|
+
source "$adapter_file"
|
|
19
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Ghostty terminal adapter.
|
|
3
|
+
|
|
4
|
+
# Return the path to Ghostty's config file.
|
|
5
|
+
terminal_get_config_path() {
|
|
6
|
+
echo "$HOME/.config/ghostty/config"
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
# Return the path where the wrapper script should be.
|
|
10
|
+
terminal_get_wrapper_path() {
|
|
11
|
+
echo "$HOME/.config/ghost-tab/wrapper.sh"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
# Install Ghostty: check for the app, open download page if missing.
|
|
15
|
+
terminal_install() {
|
|
16
|
+
local app_path="${GHOSTTY_APP_PATH:-/Applications/Ghostty.app}"
|
|
17
|
+
if [ -d "$app_path" ]; then
|
|
18
|
+
success "Ghostty found"
|
|
19
|
+
return 0
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
info "Ghostty not found. Opening download page..."
|
|
23
|
+
open "https://ghostty.org/download"
|
|
24
|
+
echo ""
|
|
25
|
+
echo " Download and install Ghostty from the page that just opened."
|
|
26
|
+
echo " Press Enter when installation is complete."
|
|
27
|
+
read -r < /dev/tty
|
|
28
|
+
|
|
29
|
+
if [ ! -d "$app_path" ]; then
|
|
30
|
+
error "Ghostty still not found at $app_path"
|
|
31
|
+
info "Install Ghostty and re-run: ghost-tab --terminal"
|
|
32
|
+
return 1
|
|
33
|
+
fi
|
|
34
|
+
success "Ghostty installed"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Write or merge the wrapper command into Ghostty config.
|
|
38
|
+
# Args: config_path wrapper_path
|
|
39
|
+
terminal_setup_config() {
|
|
40
|
+
local config_path="$1" wrapper_path="$2"
|
|
41
|
+
local wrapper_line="command = $wrapper_path"
|
|
42
|
+
|
|
43
|
+
if [ -f "$config_path" ] && grep -q '^command[[:space:]]*=' "$config_path"; then
|
|
44
|
+
sed -i '' 's|^command[[:space:]]*=.*|'"$wrapper_line"'|' "$config_path"
|
|
45
|
+
success "Replaced existing command line in config"
|
|
46
|
+
else
|
|
47
|
+
echo "$wrapper_line" >> "$config_path"
|
|
48
|
+
success "Appended wrapper command to config"
|
|
49
|
+
fi
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Remove ghost-tab command line from Ghostty config.
|
|
53
|
+
terminal_cleanup_config() {
|
|
54
|
+
local config_path="$1"
|
|
55
|
+
if [ -f "$config_path" ]; then
|
|
56
|
+
sed -i '' '/^command[[:space:]]*=/d' "$config_path"
|
|
57
|
+
fi
|
|
58
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# iTerm2 terminal adapter using Dynamic Profiles.
|
|
3
|
+
|
|
4
|
+
# Return the path to the Ghost Tab dynamic profile.
|
|
5
|
+
terminal_get_config_path() {
|
|
6
|
+
echo "$HOME/Library/Application Support/iTerm2/DynamicProfiles/ghost-tab.json"
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
# Return the path where the wrapper script should be.
|
|
10
|
+
terminal_get_wrapper_path() {
|
|
11
|
+
echo "$HOME/.config/ghost-tab/wrapper.sh"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
# Install iTerm2 via Homebrew cask.
|
|
15
|
+
terminal_install() {
|
|
16
|
+
ensure_cask "iterm2" "iTerm"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# Create a "Ghost Tab" dynamic profile for iTerm2.
|
|
20
|
+
# Args: profile_path wrapper_path
|
|
21
|
+
terminal_setup_config() {
|
|
22
|
+
local profile_path="$1" wrapper_path="$2"
|
|
23
|
+
|
|
24
|
+
mkdir -p "$(dirname "$profile_path")"
|
|
25
|
+
|
|
26
|
+
cat > "$profile_path" << EOF
|
|
27
|
+
{
|
|
28
|
+
"Profiles": [
|
|
29
|
+
{
|
|
30
|
+
"Name": "Ghost Tab",
|
|
31
|
+
"Guid": "ghost-tab-profile",
|
|
32
|
+
"Custom Command": "Yes",
|
|
33
|
+
"Command": "$wrapper_path"
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
EOF
|
|
38
|
+
|
|
39
|
+
success "Created Ghost Tab profile in iTerm2"
|
|
40
|
+
info "Set 'Ghost Tab' as your default profile in iTerm2 Preferences → Profiles"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Remove the Ghost Tab dynamic profile from iTerm2.
|
|
44
|
+
terminal_cleanup_config() {
|
|
45
|
+
local profile_path="$1"
|
|
46
|
+
|
|
47
|
+
if [ -f "$profile_path" ]; then
|
|
48
|
+
rm -f "$profile_path"
|
|
49
|
+
success "Removed Ghost Tab profile from iTerm2"
|
|
50
|
+
fi
|
|
51
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# kitty terminal adapter.
|
|
3
|
+
|
|
4
|
+
# Return the path to kitty's config file.
|
|
5
|
+
terminal_get_config_path() {
|
|
6
|
+
echo "$HOME/.config/kitty/kitty.conf"
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
# Return the path where the wrapper script should be.
|
|
10
|
+
terminal_get_wrapper_path() {
|
|
11
|
+
echo "$HOME/.config/ghost-tab/wrapper.sh"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
# Install kitty via Homebrew cask.
|
|
15
|
+
terminal_install() {
|
|
16
|
+
ensure_cask "kitty" "kitty"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# Write or merge the wrapper command into kitty config.
|
|
20
|
+
# Args: config_path wrapper_path
|
|
21
|
+
terminal_setup_config() {
|
|
22
|
+
local config_path="$1" wrapper_path="$2"
|
|
23
|
+
local shell_line="shell $wrapper_path"
|
|
24
|
+
|
|
25
|
+
if [ -f "$config_path" ] && grep -q '^shell[[:space:]]' "$config_path"; then
|
|
26
|
+
sed -i '' 's|^shell[[:space:]].*|'"$shell_line"'|' "$config_path"
|
|
27
|
+
success "Replaced existing shell line in config"
|
|
28
|
+
else
|
|
29
|
+
echo "$shell_line" >> "$config_path"
|
|
30
|
+
success "Appended wrapper command to config"
|
|
31
|
+
fi
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Remove ghost-tab shell line from kitty config.
|
|
35
|
+
terminal_cleanup_config() {
|
|
36
|
+
local config_path="$1"
|
|
37
|
+
if [ -f "$config_path" ]; then
|
|
38
|
+
sed -i '' '/^shell[[:space:]]/d' "$config_path"
|
|
39
|
+
fi
|
|
40
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Terminal registry — lists supported terminals and manages preference.
|
|
3
|
+
|
|
4
|
+
# Print supported terminal identifiers, one per line.
|
|
5
|
+
get_supported_terminals() {
|
|
6
|
+
echo "ghostty"
|
|
7
|
+
echo "iterm2"
|
|
8
|
+
echo "wezterm"
|
|
9
|
+
echo "kitty"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
# Return the human-readable display name for a terminal identifier.
|
|
13
|
+
get_terminal_display_name() {
|
|
14
|
+
local terminal="$1"
|
|
15
|
+
case "$terminal" in
|
|
16
|
+
ghostty) echo "Ghostty" ;;
|
|
17
|
+
iterm2) echo "iTerm2" ;;
|
|
18
|
+
wezterm) echo "WezTerm" ;;
|
|
19
|
+
kitty) echo "kitty" ;;
|
|
20
|
+
*) echo "$terminal" ;;
|
|
21
|
+
esac
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# Read saved terminal preference from file. Prints the terminal name or empty.
|
|
25
|
+
load_terminal_preference() {
|
|
26
|
+
local pref_file="$1"
|
|
27
|
+
if [ -f "$pref_file" ]; then
|
|
28
|
+
tr -d '[:space:]' < "$pref_file"
|
|
29
|
+
fi
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# Save terminal preference to file.
|
|
33
|
+
save_terminal_preference() {
|
|
34
|
+
local terminal="$1" pref_file="$2"
|
|
35
|
+
mkdir -p "$(dirname "$pref_file")"
|
|
36
|
+
echo "$terminal" > "$pref_file"
|
|
37
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# WezTerm terminal adapter.
|
|
3
|
+
|
|
4
|
+
# Return the path to WezTerm's config file.
|
|
5
|
+
terminal_get_config_path() {
|
|
6
|
+
echo "$HOME/.wezterm.lua"
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
# Return the path where the wrapper script should be.
|
|
10
|
+
terminal_get_wrapper_path() {
|
|
11
|
+
echo "$HOME/.config/ghost-tab/wrapper.sh"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
# Install WezTerm via Homebrew cask.
|
|
15
|
+
terminal_install() {
|
|
16
|
+
ensure_cask "wezterm" "WezTerm"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# Write or merge the wrapper command into WezTerm Lua config.
|
|
20
|
+
# Args: config_path wrapper_path
|
|
21
|
+
terminal_setup_config() {
|
|
22
|
+
local config_path="$1" wrapper_path="$2"
|
|
23
|
+
|
|
24
|
+
if [ -f "$config_path" ] && grep -q 'default_prog' "$config_path"; then
|
|
25
|
+
# Replace existing default_prog line
|
|
26
|
+
sed -i '' "s|config\.default_prog[[:space:]]*=.*|config.default_prog = { '$wrapper_path' }|" "$config_path"
|
|
27
|
+
success "Replaced existing default_prog in WezTerm config"
|
|
28
|
+
elif [ -f "$config_path" ]; then
|
|
29
|
+
# Insert before 'return config'
|
|
30
|
+
sed -i '' "s|return config|config.default_prog = { '$wrapper_path' }\nreturn config|" "$config_path"
|
|
31
|
+
success "Added default_prog to WezTerm config"
|
|
32
|
+
else
|
|
33
|
+
# Create minimal config
|
|
34
|
+
cat > "$config_path" << EOF
|
|
35
|
+
local wezterm = require 'wezterm'
|
|
36
|
+
local config = wezterm.config_builder()
|
|
37
|
+
config.default_prog = { '$wrapper_path' }
|
|
38
|
+
return config
|
|
39
|
+
EOF
|
|
40
|
+
success "Created WezTerm config with wrapper"
|
|
41
|
+
fi
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Remove ghost-tab default_prog from WezTerm config.
|
|
45
|
+
terminal_cleanup_config() {
|
|
46
|
+
local config_path="$1"
|
|
47
|
+
if [ -f "$config_path" ]; then
|
|
48
|
+
sed -i '' '/default_prog/d' "$config_path"
|
|
49
|
+
fi
|
|
50
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tmux session helpers — build launch command, cleanup.
|
|
3
|
+
# Depends on: process.sh (kill_tree)
|
|
4
|
+
|
|
5
|
+
# Build the AI tool launch command string.
|
|
6
|
+
# Usage: build_ai_launch_cmd <tool> <claude_cmd> <codex_cmd> <copilot_cmd> <opencode_cmd> [extra_args_or_project_dir]
|
|
7
|
+
build_ai_launch_cmd() {
|
|
8
|
+
local tool="$1" claude_cmd="$2" codex_cmd="$3" copilot_cmd="$4" opencode_cmd="$5"
|
|
9
|
+
shift 5
|
|
10
|
+
local extra="$*"
|
|
11
|
+
|
|
12
|
+
case "$tool" in
|
|
13
|
+
codex)
|
|
14
|
+
echo "$codex_cmd --cd \"$extra\""
|
|
15
|
+
;;
|
|
16
|
+
copilot)
|
|
17
|
+
echo "$copilot_cmd"
|
|
18
|
+
;;
|
|
19
|
+
opencode)
|
|
20
|
+
echo "$opencode_cmd \"$extra\""
|
|
21
|
+
;;
|
|
22
|
+
*)
|
|
23
|
+
if [ -n "$extra" ]; then
|
|
24
|
+
echo "$claude_cmd $extra"
|
|
25
|
+
else
|
|
26
|
+
echo "$claude_cmd"
|
|
27
|
+
fi
|
|
28
|
+
;;
|
|
29
|
+
esac
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# Clean up a tmux session: kill watcher, TERM pane trees, KILL survivors, destroy session.
|
|
33
|
+
cleanup_tmux_session() {
|
|
34
|
+
local session_name="$1" watcher_pid="$2" tmux_cmd="$3"
|
|
35
|
+
|
|
36
|
+
kill "$watcher_pid" 2>/dev/null || true
|
|
37
|
+
|
|
38
|
+
local pane_pid
|
|
39
|
+
for pane_pid in $("$tmux_cmd" list-panes -s -t "$session_name" -F '#{pane_pid}' 2>/dev/null); do
|
|
40
|
+
kill_tree "$pane_pid" TERM
|
|
41
|
+
done
|
|
42
|
+
|
|
43
|
+
sleep 0.3
|
|
44
|
+
for pane_pid in $("$tmux_cmd" list-panes -s -t "$session_name" -F '#{pane_pid}' 2>/dev/null); do
|
|
45
|
+
kill_tree "$pane_pid" KILL
|
|
46
|
+
done
|
|
47
|
+
|
|
48
|
+
"$tmux_cmd" kill-session -t "$session_name" 2>/dev/null || true
|
|
49
|
+
}
|