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
@@ -0,0 +1,66 @@
1
+ #!/bin/bash
2
+ # AI tool selection TUI wrapper using ghost-tab-tui
3
+
4
+ # Interactive AI tool multi-selection
5
+ # Returns 0 if selected, 1 if cancelled
6
+ # Sets: _selected_ai_tool (first tool by priority)
7
+ # Sets: _selected_ai_tools (space-separated list of all selected tools)
8
+ select_ai_tool_interactive() {
9
+ if ! command -v ghost-tab-tui &>/dev/null; then
10
+ error "ghost-tab-tui binary not found. Please reinstall."
11
+ return 1
12
+ fi
13
+
14
+ local result
15
+ if ! result=$(ghost-tab-tui multi-select-ai-tool 2>/dev/null); then
16
+ return 1
17
+ fi
18
+
19
+ local confirmed
20
+ if ! confirmed=$(echo "$result" | jq -r '.confirmed' 2>/dev/null); then
21
+ error "Failed to parse AI tool selection response"
22
+ return 1
23
+ fi
24
+
25
+ # Validate against null/empty
26
+ if [[ -z "$confirmed" || "$confirmed" == "null" ]]; then
27
+ error "TUI returned invalid confirmation status"
28
+ return 1
29
+ fi
30
+
31
+ if [[ "$confirmed" != "true" ]]; then
32
+ return 1
33
+ fi
34
+
35
+ local tools_json
36
+ if ! tools_json=$(echo "$result" | jq -r '.tools[]' 2>/dev/null); then
37
+ error "Failed to parse selected tools"
38
+ return 1
39
+ fi
40
+
41
+ # Validate against empty selection
42
+ if [[ -z "$tools_json" ]]; then
43
+ error "TUI returned empty tool selection"
44
+ return 1
45
+ fi
46
+
47
+ # Set space-separated list of all selected tools
48
+ _selected_ai_tools="$tools_json"
49
+
50
+ # Pick first tool by priority: claude > codex > copilot > opencode
51
+ local priority_order=("claude" "codex" "copilot" "opencode")
52
+ _selected_ai_tool=""
53
+ for priority_tool in "${priority_order[@]}"; do
54
+ if echo "$tools_json" | grep -qx "$priority_tool"; then
55
+ _selected_ai_tool="$priority_tool"
56
+ break
57
+ fi
58
+ done
59
+
60
+ # Fallback to first tool if none matched priority
61
+ if [[ -z "$_selected_ai_tool" ]]; then
62
+ _selected_ai_tool=$(echo "$tools_json" | head -n1)
63
+ fi
64
+
65
+ return 0
66
+ }
@@ -0,0 +1,19 @@
1
+ #!/bin/bash
2
+ # AI tool helper functions — pure, no side effects on source.
3
+
4
+ # Validates SELECTED_AI_TOOL against AI_TOOLS_AVAILABLE.
5
+ # Falls back to first available if current selection is invalid.
6
+ # Optional arg $1: path to preference file (writes corrected value if provided).
7
+ validate_ai_tool() {
8
+ local _valid=0 _t
9
+ for _t in "${AI_TOOLS_AVAILABLE[@]}"; do
10
+ [ "$_t" == "$SELECTED_AI_TOOL" ] && _valid=1
11
+ done
12
+ if [ "$_valid" -eq 0 ] && [ ${#AI_TOOLS_AVAILABLE[@]} -gt 0 ]; then
13
+ SELECTED_AI_TOOL="${AI_TOOLS_AVAILABLE[0]}"
14
+ if [ -n "${1:-}" ]; then
15
+ mkdir -p "$(dirname "$1")"
16
+ echo "$SELECTED_AI_TOOL" > "$1"
17
+ fi
18
+ fi
19
+ }
@@ -0,0 +1,95 @@
1
+ #!/bin/bash
2
+ # Config menu TUI dispatcher
3
+ # Uses ghost-tab-tui config-menu subcommand in a loop
4
+
5
+ # Source dependencies if not already loaded
6
+ _config_tui_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
+ # shellcheck source=lib/tui.sh
8
+ [ "$(type -t header 2>/dev/null)" = "function" ] || source "$_config_tui_dir/tui.sh"
9
+ # shellcheck source=lib/terminal-select-tui.sh
10
+ [ "$(type -t select_terminal_interactive 2>/dev/null)" = "function" ] || source "$_config_tui_dir/terminal-select-tui.sh"
11
+ # shellcheck source=lib/terminals/registry.sh
12
+ [ "$(type -t get_terminal_display_name 2>/dev/null)" = "function" ] || source "$_config_tui_dir/terminals/registry.sh"
13
+ # shellcheck source=lib/project-actions-tui.sh
14
+ [ "$(type -t add_project_interactive 2>/dev/null)" = "function" ] || source "$_config_tui_dir/project-actions-tui.sh"
15
+ # shellcheck source=lib/project-actions.sh
16
+ [ "$(type -t add_project_to_file 2>/dev/null)" = "function" ] || source "$_config_tui_dir/project-actions.sh"
17
+ # shellcheck source=lib/ai-select-tui.sh
18
+ [ "$(type -t select_ai_tool_interactive 2>/dev/null)" = "function" ] || source "$_config_tui_dir/ai-select-tui.sh"
19
+ # shellcheck source=lib/settings-menu-tui.sh
20
+ [ "$(type -t settings_menu_interactive 2>/dev/null)" = "function" ] || source "$_config_tui_dir/settings-menu-tui.sh"
21
+
22
+ # Interactive config menu loop.
23
+ # Calls ghost-tab-tui config-menu, dispatches on action, loops until quit.
24
+ config_menu_interactive() {
25
+ if ! command -v ghost-tab-tui &>/dev/null; then
26
+ error "ghost-tab-tui binary not found. Please reinstall."
27
+ return 1
28
+ fi
29
+
30
+ local config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/ghost-tab"
31
+
32
+ while true; do
33
+ local result
34
+ if ! result=$(ghost-tab-tui config-menu 2>/dev/null); then
35
+ return 1
36
+ fi
37
+
38
+ local action
39
+ if ! action=$(echo "$result" | jq -r '.action' 2>/dev/null); then
40
+ error "Failed to parse config menu response"
41
+ return 1
42
+ fi
43
+
44
+ case "$action" in
45
+ manage-terminals)
46
+ export GHOST_TAB_TERMINAL_PREF="$config_dir/terminal"
47
+ if select_terminal_interactive; then
48
+ # shellcheck disable=SC2154
49
+ echo "$_selected_terminal" > "$config_dir/terminal"
50
+ success "Terminal set to $(get_terminal_display_name "$_selected_terminal")"
51
+ echo ""
52
+ read -rsn1 -p "Press any key to continue..." </dev/tty
53
+ fi
54
+ ;;
55
+ manage-projects)
56
+ if add_project_interactive; then
57
+ # shellcheck disable=SC2154
58
+ add_project_to_file "$_add_project_name" "$_add_project_path" "$config_dir/projects"
59
+ success "Added project: $_add_project_name"
60
+ echo ""
61
+ read -rsn1 -p "Press any key to continue..." </dev/tty
62
+ fi
63
+ ;;
64
+ select-ai-tools)
65
+ if select_ai_tool_interactive; then
66
+ # shellcheck disable=SC2154
67
+ echo "$_selected_ai_tool" > "$config_dir/ai-tool"
68
+ success "Default AI tool set to $_selected_ai_tool"
69
+ echo ""
70
+ read -rsn1 -p "Press any key to continue..." </dev/tty
71
+ fi
72
+ ;;
73
+ display-settings)
74
+ settings_menu_interactive
75
+ ;;
76
+ reinstall)
77
+ local script_dir
78
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
79
+ if [ -f "$script_dir/bin/ghost-tab" ]; then
80
+ exec bash "$script_dir/bin/ghost-tab"
81
+ else
82
+ error "Installer not found. Re-clone the repository."
83
+ echo ""
84
+ read -rsn1 -p "Press any key to continue..." </dev/tty
85
+ fi
86
+ ;;
87
+ quit|"")
88
+ return 0
89
+ ;;
90
+ *)
91
+ error "Unknown action: $action"
92
+ ;;
93
+ esac
94
+ done
95
+ }
@@ -0,0 +1,26 @@
1
+ #!/bin/bash
2
+ # Ghostty config file helpers.
3
+
4
+ # Merge a command line into an existing Ghostty config.
5
+ # If a "command = ..." line exists, replace it; otherwise append.
6
+ merge_ghostty_config() {
7
+ local config_path="$1" wrapper_line="$2"
8
+ if grep -q '^command[[:space:]]*=' "$config_path"; then
9
+ sed -i '' 's|^command[[:space:]]*=.*|'"$wrapper_line"'|' "$config_path"
10
+ success "Replaced existing command line in config"
11
+ else
12
+ echo "$wrapper_line" >> "$config_path"
13
+ success "Appended wrapper command to config"
14
+ fi
15
+ }
16
+
17
+ # Backup an existing config and replace it with the source config.
18
+ backup_replace_ghostty_config() {
19
+ local config_path="$1" source_config="$2"
20
+ local backup
21
+ backup="${config_path}.backup.$(date +%s)"
22
+ cp "$config_path" "$backup"
23
+ success "Backed up existing config to $backup"
24
+ cp "$source_config" "$config_path"
25
+ success "Replaced config with ghost-tab defaults"
26
+ }
package/lib/input.sh ADDED
@@ -0,0 +1,39 @@
1
+ #!/bin/bash
2
+ # Input parsing helpers — no side effects on source.
3
+
4
+ # Interactive confirmation using ghost-tab-tui
5
+ # Usage: confirm_tui "Delete project 'foo'?"
6
+ # Returns: 0 if confirmed, 1 if cancelled
7
+ confirm_tui() {
8
+ local msg="$1"
9
+
10
+ if ! command -v ghost-tab-tui &>/dev/null; then
11
+ # Fallback to simple bash prompt
12
+ read -rp "$msg (y/N) " response </dev/tty
13
+ [[ "$response" =~ ^[Yy]$ ]]
14
+ return $?
15
+ fi
16
+
17
+ local result
18
+ if ! result=$(ghost-tab-tui confirm "$msg" 2>/dev/null); then
19
+ return 1
20
+ fi
21
+
22
+ local confirmed
23
+ if ! confirmed=$(echo "$result" | jq -r '.confirmed' 2>/dev/null); then
24
+ # Source tui.sh for error function if not already loaded
25
+ if ! declare -F error &>/dev/null; then
26
+ echo "ERROR: Failed to parse confirmation response" >&2
27
+ else
28
+ error "Failed to parse confirmation response"
29
+ fi
30
+ return 1
31
+ fi
32
+
33
+ # Validate against "null" string (learned from Task 3)
34
+ if [[ "$confirmed" == "null" || -z "$confirmed" ]]; then
35
+ return 1
36
+ fi
37
+
38
+ [[ "$confirmed" == "true" ]]
39
+ }
package/lib/install.sh ADDED
@@ -0,0 +1,224 @@
1
+ #!/bin/bash
2
+ # Package installation helpers for the installer.
3
+
4
+ # Detect CPU architecture: outputs "arm64" or "x86_64"
5
+ detect_arch() {
6
+ local arch
7
+ arch="$(uname -m)"
8
+ case "$arch" in
9
+ arm64) echo "arm64" ;;
10
+ x86_64) echo "x86_64" ;;
11
+ *)
12
+ error "Unsupported architecture: $arch"
13
+ return 1 ;;
14
+ esac
15
+ }
16
+
17
+ # Get the latest release tag from a GitHub repo (e.g. "v1.2.3")
18
+ # Uses the /releases/latest redirect — no API key required.
19
+ get_latest_release_tag() {
20
+ local repo="$1" tag
21
+ tag="$(curl -fsSI "https://github.com/$repo/releases/latest" 2>/dev/null \
22
+ | grep -i '^location:' \
23
+ | sed 's|.*/tag/||' \
24
+ | tr -d '[:space:]\r')"
25
+ if [[ -z "$tag" ]]; then
26
+ error "Failed to fetch release tag for $repo"
27
+ return 1
28
+ fi
29
+ echo "$tag"
30
+ }
31
+
32
+ # Download a binary from $url to $dest and make it executable.
33
+ install_binary() {
34
+ local url="$1" dest="$2" display_name="$3"
35
+ info "Downloading $display_name..."
36
+ mkdir -p "$(dirname "$dest")"
37
+ if curl -fsSL -o "$dest" "$url"; then
38
+ chmod +x "$dest"
39
+ success "$display_name installed"
40
+ else
41
+ warn "Failed to download $display_name from $url"
42
+ return 1
43
+ fi
44
+ }
45
+
46
+ # Install jq from jqlang/jq GitHub releases.
47
+ ensure_jq() {
48
+ if command -v jq &>/dev/null; then
49
+ success "jq already installed"
50
+ return 0
51
+ fi
52
+ local arch jq_arch
53
+ arch="$(detect_arch)" || return 1
54
+ case "$arch" in
55
+ arm64) jq_arch="macos-arm64" ;;
56
+ x86_64) jq_arch="macos-amd64" ;;
57
+ esac
58
+ install_binary \
59
+ "https://github.com/jqlang/jq/releases/latest/download/jq-${jq_arch}" \
60
+ "$HOME/.local/bin/jq" \
61
+ "jq"
62
+ }
63
+
64
+ # Install tmux from tmux/tmux-builds GitHub releases.
65
+ ensure_tmux() {
66
+ if command -v tmux &>/dev/null; then
67
+ success "tmux already installed"
68
+ return 0
69
+ fi
70
+ local arch tag version tmp_dir url
71
+ arch="$(detect_arch)" || return 1
72
+ tag="$(get_latest_release_tag "tmux/tmux-builds")" || return 1
73
+ version="${tag#v}"
74
+ tmp_dir="$(mktemp -d)"
75
+ # shellcheck disable=SC2064
76
+ trap "rm -rf '$tmp_dir'" RETURN
77
+ url="https://github.com/tmux/tmux-builds/releases/download/${tag}/tmux-${version}-macos-${arch}.tar.gz"
78
+ info "Downloading tmux..."
79
+ if curl -fsSL -o "$tmp_dir/tmux.tar.gz" "$url"; then
80
+ tar -xzf "$tmp_dir/tmux.tar.gz" -C "$tmp_dir" tmux
81
+ mkdir -p "$HOME/.local/bin"
82
+ mv "$tmp_dir/tmux" "$HOME/.local/bin/tmux"
83
+ chmod +x "$HOME/.local/bin/tmux"
84
+ success "tmux installed"
85
+ else
86
+ warn "Failed to install tmux"
87
+ return 1
88
+ fi
89
+ }
90
+
91
+ # Install lazygit from jesseduffield/lazygit GitHub releases.
92
+ ensure_lazygit() {
93
+ if command -v lazygit &>/dev/null; then
94
+ success "lazygit already installed"
95
+ return 0
96
+ fi
97
+ local arch tag version tmp_dir url
98
+ arch="$(detect_arch)" || return 1
99
+ tag="$(get_latest_release_tag "jesseduffield/lazygit")" || return 1
100
+ version="${tag#v}"
101
+ tmp_dir="$(mktemp -d)"
102
+ # shellcheck disable=SC2064
103
+ trap "rm -rf '$tmp_dir'" RETURN
104
+ url="https://github.com/jesseduffield/lazygit/releases/download/${tag}/lazygit_${version}_darwin_${arch}.tar.gz"
105
+ info "Downloading lazygit..."
106
+ if curl -fsSL -o "$tmp_dir/lazygit.tar.gz" "$url"; then
107
+ tar -xzf "$tmp_dir/lazygit.tar.gz" -C "$tmp_dir" lazygit
108
+ mkdir -p "$HOME/.local/bin"
109
+ mv "$tmp_dir/lazygit" "$HOME/.local/bin/lazygit"
110
+ chmod +x "$HOME/.local/bin/lazygit"
111
+ success "lazygit installed"
112
+ else
113
+ warn "Failed to install lazygit"
114
+ return 1
115
+ fi
116
+ }
117
+
118
+ # Install broot from Canop/broot GitHub releases.
119
+ ensure_broot() {
120
+ if command -v broot &>/dev/null; then
121
+ success "broot already installed"
122
+ return 0
123
+ fi
124
+ local arch broot_arch tag version tmp_dir url
125
+ arch="$(detect_arch)" || return 1
126
+ case "$arch" in
127
+ arm64) broot_arch="aarch64-apple-darwin" ;;
128
+ x86_64) broot_arch="x86_64-apple-darwin" ;;
129
+ esac
130
+ tag="$(get_latest_release_tag "Canop/broot")" || return 1
131
+ version="${tag#v}"
132
+ tmp_dir="$(mktemp -d)"
133
+ # shellcheck disable=SC2064
134
+ trap "rm -rf '$tmp_dir'" RETURN
135
+ url="https://github.com/Canop/broot/releases/download/${tag}/broot_${version}.zip"
136
+ info "Downloading broot..."
137
+ if curl -fsSL -o "$tmp_dir/broot.zip" "$url"; then
138
+ unzip -q -d "$tmp_dir" "$tmp_dir/broot.zip"
139
+ mkdir -p "$HOME/.local/bin"
140
+ mv "$tmp_dir/${broot_arch}/broot" "$HOME/.local/bin/broot"
141
+ chmod +x "$HOME/.local/bin/broot"
142
+ success "broot installed"
143
+ else
144
+ warn "Failed to install broot"
145
+ return 1
146
+ fi
147
+ }
148
+
149
+ # Install or update ghost-tab-tui by downloading the pre-built binary from the ghost-tab release.
150
+ # Args: share_dir (to read VERSION from)
151
+ # Checks installed binary version against VERSION file and re-downloads if mismatched.
152
+ ensure_ghost_tab_tui() {
153
+ local share_dir="$1"
154
+
155
+ local version
156
+ version="$(tr -d '[:space:]' < "$share_dir/VERSION" 2>/dev/null)"
157
+ if [[ -z "$version" ]]; then
158
+ error "Cannot determine ghost-tab-tui version: VERSION file missing in $share_dir"
159
+ return 1
160
+ fi
161
+
162
+ if command -v ghost-tab-tui &>/dev/null; then
163
+ # Check if installed version matches expected version
164
+ local installed_version
165
+ installed_version="$(ghost-tab-tui --version 2>/dev/null | sed 's/.*version //' || echo "")"
166
+ if [[ "$installed_version" == "$version" ]]; then
167
+ success "ghost-tab-tui is up to date ($version)"
168
+ return 0
169
+ fi
170
+ info "Updating ghost-tab-tui ($installed_version -> $version)..."
171
+ fi
172
+
173
+ local arch url
174
+ arch="$(detect_arch)" || return 1
175
+ url="https://github.com/JackUait/ghost-tab/releases/download/v${version}/ghost-tab-tui-darwin-${arch}"
176
+
177
+ mkdir -p "$HOME/.local/bin"
178
+ install_binary "$url" "$HOME/.local/bin/ghost-tab-tui" "ghost-tab-tui" || return 1
179
+ }
180
+
181
+ # Install base CLI requirements.
182
+ ensure_base_requirements() {
183
+ ensure_jq
184
+ ensure_tmux
185
+ ensure_lazygit
186
+ ensure_broot
187
+ }
188
+
189
+ # Install a Homebrew cask if the .app isn't in /Applications.
190
+ # Usage: ensure_cask "cask_name" "AppDisplayName"
191
+ # Respects APPLICATIONS_DIR env var for testing (defaults to /Applications).
192
+ ensure_cask() {
193
+ local cask="$1" app_name="$2"
194
+ local app_dir="${APPLICATIONS_DIR:-/Applications}"
195
+ if [ -d "${app_dir}/${app_name}.app" ]; then
196
+ success "$app_name found"
197
+ else
198
+ info "Installing $app_name..."
199
+ if brew install --cask "$cask"; then
200
+ success "$app_name installed"
201
+ else
202
+ error "$app_name installation failed."
203
+ info "Install manually or run: brew install --cask $cask"
204
+ return 1
205
+ fi
206
+ fi
207
+ }
208
+
209
+ # Install a command-line tool if not already on PATH.
210
+ # Usage: ensure_command "cmd" "install_cmd" "post_msg" "display_name"
211
+ ensure_command() {
212
+ local cmd="$1" install_cmd="$2" post_msg="$3" display_name="$4"
213
+ if command -v "$cmd" &>/dev/null; then
214
+ success "$display_name already installed"
215
+ else
216
+ info "Installing $display_name..."
217
+ if eval "$install_cmd"; then
218
+ success "$display_name installed"
219
+ [ -n "$post_msg" ] && info "$post_msg"
220
+ else
221
+ warn "$display_name installation failed — install manually: $install_cmd"
222
+ fi
223
+ fi
224
+ }
package/lib/loading.sh ADDED
@@ -0,0 +1,190 @@
1
+ #!/bin/bash
2
+ # Loading screen with ASCII art, tool-specific color palettes, and animation.
3
+
4
+ # Print the loading screen ASCII art to stdout.
5
+ get_loading_art() {
6
+ cat << 'ART'
7
+ +--------------------------------------------------------------------------------------+
8
+ | |
9
+ | .d8888b. 888 888 88888888888 888 |
10
+ | d88P Y88b888 888 888 888 |
11
+ | 888 888888 888 888 888 |
12
+ | 888 88888b. .d88b. .d8888b 888888 888 8888b. 88888b. |
13
+ | 888 88888888 "88bd88""88b88K 888 888 "88b888 "88b |
14
+ | 888 888888 888888 888"Y8888b.888 888 .d888888888 888 |
15
+ | Y88b d88P888 888Y88..88P X88Y88b. 888 888 888888 d88P |
16
+ | "Y8888P88888 888 "Y88P" 88888P' "Y888 888 "Y88888888888P" |
17
+ | |
18
+ +--------------------------------------------------------------------------------------+
19
+ ART
20
+ }
21
+
22
+ # Get color palette for a given AI tool. Prints space-separated 256-color codes.
23
+ # Falls back to Claude palette for unknown or empty tool names.
24
+ get_tool_palette() {
25
+ case "${1:-}" in
26
+ codex) echo "22 28 29 34 35 41 42 47" ;; # green
27
+ copilot) echo "54 56 93 99 135 141 177 183" ;; # purple (GitHub Copilot brand)
28
+ opencode) echo "240 242 244 246 248 250 252 254" ;; # gray/silver (OpenCode mascot)
29
+ *) echo "130 166 172 208 209 214 215 220" ;; # orange/amber (claude default)
30
+ esac
31
+ }
32
+
33
+ # Render a single frame of the loading screen.
34
+ # Args: tool_name frame_number term_cols term_rows
35
+ render_loading_frame() {
36
+ local tool="$1" frame="$2"
37
+ local cols="${3:-80}" rows="${4:-24}"
38
+
39
+ # Get art lines into array
40
+ local art
41
+ art="$(get_loading_art)"
42
+ local -a lines=()
43
+ while IFS= read -r line; do
44
+ lines+=("$line")
45
+ done <<< "$art"
46
+
47
+ # Get palette
48
+ local -a palette
49
+ read -ra palette <<< "$(get_tool_palette "$tool")"
50
+ local pal_len=${#palette[@]}
51
+
52
+ # Calculate art dimensions
53
+ local art_height=${#lines[@]}
54
+ local art_width=0
55
+ for line in "${lines[@]}"; do
56
+ local len=${#line}
57
+ if (( len > art_width )); then
58
+ art_width=$len
59
+ fi
60
+ done
61
+
62
+ # Center position
63
+ local start_row=$(( (rows - art_height) / 2 ))
64
+ local start_col=$(( (cols - art_width) / 2 ))
65
+ if (( start_row < 1 )); then start_row=1; fi
66
+ if (( start_col < 1 )); then start_col=1; fi
67
+
68
+ # Draw each line with gradient color shifted by frame
69
+ local i
70
+ for i in "${!lines[@]}"; do
71
+ local color_idx=$(( (i + frame) % pal_len ))
72
+ local color="${palette[$color_idx]}"
73
+ printf '\033[%d;%dH\033[38;5;%dm%s' \
74
+ "$((start_row + i))" "$start_col" "$color" "${lines[$i]}"
75
+ done
76
+
77
+ printf '\033[0m'
78
+ }
79
+
80
+ # PID of the background animation process.
81
+ _LOADING_SCREEN_PID=""
82
+
83
+ # Detect terminal dimensions reliably. Prints "rows cols" to stdout.
84
+ _detect_term_size() {
85
+ local _r _c
86
+
87
+ # Method 1: stty size via /dev/tty (most reliable in pty context)
88
+ local _size
89
+ if _size=$( (stty size </dev/tty) 2>/dev/null ) && read -r _r _c <<< "$_size"; then
90
+ if (( _r > 0 && _c > 0 )); then
91
+ echo "$_r $_c"
92
+ return
93
+ fi
94
+ fi
95
+
96
+ # Method 2: stty size from stdin
97
+ if read -r _r _c < <(stty size 2>/dev/null); then
98
+ if (( _r > 0 && _c > 0 )); then
99
+ echo "$_r $_c"
100
+ return
101
+ fi
102
+ fi
103
+
104
+ # Method 3: tput (uses terminfo + ioctl)
105
+ _c=$(tput cols 2>/dev/null || echo 0)
106
+ _r=$(tput lines 2>/dev/null || echo 0)
107
+ if (( _r > 0 && _c > 0 )); then
108
+ echo "$_r $_c"
109
+ return
110
+ fi
111
+
112
+ # Fallback
113
+ echo "24 80"
114
+ }
115
+
116
+ # Show animated loading screen with tool-specific colors.
117
+ # Args: [tool_name] — defaults to claude if omitted.
118
+ # Sets _LOADING_SCREEN_PID for the caller to stop later.
119
+ show_loading_screen() {
120
+ local tool="${1:-claude}"
121
+
122
+ # Clear screen, hide cursor (instant dark feedback)
123
+ printf '\033[2J\033[H\033[?25l'
124
+
125
+ # Symbols for floating particles
126
+ local symbols=('·' '•' '°' '∘' '⋅' '∙')
127
+
128
+ # Start animation in background
129
+ (
130
+ trap 'printf "\033[?25h\033[0m"; exit 0' TERM INT HUP
131
+
132
+ # Brief delay so terminal reports its final size after window opens
133
+ sleep 0.1
134
+
135
+ local frame=0
136
+ local rows cols
137
+ local -a prev_sym_positions=()
138
+ local prev_rows=0 prev_cols=0
139
+
140
+ while true; do
141
+ # Detect terminal size each frame (handles late window resizes)
142
+ read -r rows cols <<< "$(_detect_term_size)"
143
+ if (( rows != prev_rows || cols != prev_cols )); then
144
+ printf '\033[2J' # Clear screen on resize to avoid ghost artifacts
145
+ prev_rows="$rows" prev_cols="$cols"
146
+ prev_sym_positions=()
147
+ fi
148
+
149
+ # Redraw art with shifted colors
150
+ render_loading_frame "$tool" "$frame" "$cols" "$rows"
151
+
152
+ # Clear previous floating symbols
153
+ for pos in "${prev_sym_positions[@]}"; do
154
+ local sr sc
155
+ IFS=';' read -r sr sc <<< "$pos"
156
+ printf '\033[%d;%dH ' "$sr" "$sc"
157
+ done
158
+ prev_sym_positions=()
159
+
160
+ # Draw new floating symbols
161
+ local -a palette
162
+ read -ra palette <<< "$(get_tool_palette "$tool")"
163
+ local pal_len=${#palette[@]}
164
+ local _s
165
+ for _s in 0 1 2; do
166
+ local sym_row=$(( RANDOM % rows + 1 ))
167
+ local sym_col=$(( RANDOM % cols + 1 ))
168
+ local sym_color="${palette[$(( RANDOM % pal_len ))]}"
169
+ local sym="${symbols[$(( RANDOM % ${#symbols[@]} ))]}"
170
+ printf '\033[%d;%dH\033[2m\033[38;5;%dm%s\033[0m' \
171
+ "$sym_row" "$sym_col" "$sym_color" "$sym"
172
+ prev_sym_positions+=("${sym_row};${sym_col}")
173
+ done
174
+
175
+ frame=$(( (frame + 1) % pal_len ))
176
+ sleep 0.15
177
+ done
178
+ ) &
179
+ _LOADING_SCREEN_PID=$!
180
+ }
181
+
182
+ # Stop loading screen animation and restore terminal.
183
+ stop_loading_screen() {
184
+ if [ -n "${_LOADING_SCREEN_PID:-}" ]; then
185
+ kill "$_LOADING_SCREEN_PID" 2>/dev/null
186
+ wait "$_LOADING_SCREEN_PID" 2>/dev/null
187
+ _LOADING_SCREEN_PID=""
188
+ fi
189
+ printf '\033[?25h\033[0m'
190
+ }