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
|
@@ -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
|
+
}
|
package/lib/ai-tools.sh
ADDED
|
@@ -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
|
+
}
|