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.
Files changed (42) hide show
  1. package/README.md +151 -0
  2. package/VERSION +1 -0
  3. package/bin/ghost-tab +360 -0
  4. package/bin/npx-ghost-tab.js +141 -0
  5. package/ghostty/config +5 -0
  6. package/lib/ai-select-tui.sh +66 -0
  7. package/lib/ai-tools.sh +19 -0
  8. package/lib/config-tui.sh +95 -0
  9. package/lib/ghostty-config.sh +26 -0
  10. package/lib/input.sh +39 -0
  11. package/lib/install.sh +224 -0
  12. package/lib/loading.sh +190 -0
  13. package/lib/menu-tui.sh +189 -0
  14. package/lib/notification-setup.sh +210 -0
  15. package/lib/process.sh +13 -0
  16. package/lib/project-actions-tui.sh +29 -0
  17. package/lib/project-actions.sh +9 -0
  18. package/lib/projects.sh +18 -0
  19. package/lib/settings-json.sh +178 -0
  20. package/lib/settings-menu-tui.sh +32 -0
  21. package/lib/setup.sh +16 -0
  22. package/lib/statusline-setup.sh +60 -0
  23. package/lib/statusline.sh +31 -0
  24. package/lib/tab-title-watcher.sh +118 -0
  25. package/lib/terminal-select-tui.sh +119 -0
  26. package/lib/terminals/adapter.sh +19 -0
  27. package/lib/terminals/ghostty.sh +58 -0
  28. package/lib/terminals/iterm2.sh +51 -0
  29. package/lib/terminals/kitty.sh +40 -0
  30. package/lib/terminals/registry.sh +37 -0
  31. package/lib/terminals/wezterm.sh +50 -0
  32. package/lib/tmux-session.sh +49 -0
  33. package/lib/tui.sh +80 -0
  34. package/lib/update.sh +52 -0
  35. package/package.json +42 -0
  36. package/templates/ccstatusline-settings.json +29 -0
  37. package/templates/statusline-command.sh +30 -0
  38. package/templates/statusline-wrapper.sh +40 -0
  39. package/terminals/ghostty/config +5 -0
  40. package/terminals/kitty/config +1 -0
  41. package/terminals/wezterm/config.lua +4 -0
  42. 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
+ }