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/tui.sh
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Shared TUI helpers — colors, logging, cursor utilities.
|
|
3
|
+
|
|
4
|
+
# Basic colors (with terminal capability fallback)
|
|
5
|
+
if [ -t 1 ] && [ "$(tput colors 2>/dev/null)" -ge 8 ] 2>/dev/null; then
|
|
6
|
+
_GREEN='\033[0;32m'
|
|
7
|
+
_YELLOW='\033[0;33m'
|
|
8
|
+
_RED='\033[0;31m'
|
|
9
|
+
_BLUE='\033[0;34m'
|
|
10
|
+
_BOLD='\033[1m'
|
|
11
|
+
_NC='\033[0m'
|
|
12
|
+
else
|
|
13
|
+
_GREEN='' _YELLOW='' _RED='' _BLUE='' _BOLD='' _NC=''
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
# Logging helpers
|
|
17
|
+
success() { echo -e "${_GREEN}✓${_NC} $1"; }
|
|
18
|
+
warn() { echo -e "${_YELLOW}!${_NC} $1"; }
|
|
19
|
+
error() { echo -e "${_RED}✗${_NC} $1"; }
|
|
20
|
+
info() { echo -e "${_BLUE}→${_NC} $1"; }
|
|
21
|
+
header() { echo -e "\n${_BOLD}$1${_NC}"; }
|
|
22
|
+
|
|
23
|
+
# Set terminal/tab title. With tool: "project · tool", without: "project"
|
|
24
|
+
set_tab_title() {
|
|
25
|
+
local project="$1"
|
|
26
|
+
local tool="${2:-}"
|
|
27
|
+
if [ -n "$tool" ]; then
|
|
28
|
+
printf '\033]0;%s · %s\007' "$project" "$tool"
|
|
29
|
+
else
|
|
30
|
+
printf '\033]0;%s\007' "$project"
|
|
31
|
+
fi
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Set terminal/tab title with ● prefix (waiting state).
|
|
35
|
+
# Same format as set_tab_title but prepends "● ".
|
|
36
|
+
set_tab_title_waiting() {
|
|
37
|
+
local project="$1"
|
|
38
|
+
local tool="${2:-}"
|
|
39
|
+
if [ -n "$tool" ]; then
|
|
40
|
+
printf '\033]0;● %s · %s\007' "$project" "$tool"
|
|
41
|
+
else
|
|
42
|
+
printf '\033]0;● %s\007' "$project"
|
|
43
|
+
fi
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Extended TUI variables for interactive full-screen UIs.
|
|
47
|
+
# Call this before using any of the extended variables.
|
|
48
|
+
tui_init_interactive() {
|
|
49
|
+
_CYAN=$'\033[0;36m'
|
|
50
|
+
_DIM=$'\033[2m'
|
|
51
|
+
_INVERSE=$'\033[7m'
|
|
52
|
+
_BG_BLUE=$'\033[48;5;27m'
|
|
53
|
+
_BG_RED=$'\033[48;5;160m'
|
|
54
|
+
_WHITE=$'\033[1;37m'
|
|
55
|
+
_HIDE_CURSOR=$'\033[?25l'
|
|
56
|
+
_SHOW_CURSOR=$'\033[?25h'
|
|
57
|
+
_MOUSE_ON=$'\033[?1000h\033[?1006h'
|
|
58
|
+
_MOUSE_OFF=$'\033[?1000l\033[?1006l'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Move cursor to row;col
|
|
62
|
+
moveto() { printf '\033[%d;%dH' "$1" "$2"; }
|
|
63
|
+
|
|
64
|
+
# Print N spaces
|
|
65
|
+
pad() { printf "%*s" "$1" ""; }
|
|
66
|
+
|
|
67
|
+
# Draw logo using ghost-tab-tui binary
|
|
68
|
+
# Usage: draw_logo tool_name [row] [col]
|
|
69
|
+
# If row/col provided, they are ignored (for compatibility)
|
|
70
|
+
draw_logo() {
|
|
71
|
+
# shellcheck disable=SC2034 # tool parameter reserved for future use
|
|
72
|
+
local tool="$1"
|
|
73
|
+
|
|
74
|
+
if command -v ghost-tab-tui &>/dev/null; then
|
|
75
|
+
ghost-tab-tui show-logo 2>/dev/null || true
|
|
76
|
+
else
|
|
77
|
+
# Fallback: no logo if binary missing
|
|
78
|
+
return 0
|
|
79
|
+
fi
|
|
80
|
+
}
|
package/lib/update.sh
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Git-based auto-update for ghost-tab.
|
|
3
|
+
|
|
4
|
+
# Show update notification if a previous background update wrote a flag file.
|
|
5
|
+
# Deletes the flag after displaying.
|
|
6
|
+
notify_if_updated() {
|
|
7
|
+
local config_home="${XDG_CONFIG_HOME:-$HOME/.config}"
|
|
8
|
+
local flag="${config_home}/ghost-tab/updated"
|
|
9
|
+
[ -f "$flag" ] || return 0
|
|
10
|
+
|
|
11
|
+
local version
|
|
12
|
+
version="$(cat "$flag")"
|
|
13
|
+
rm -f "$flag"
|
|
14
|
+
echo " ↑ Updated to v${version}"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
# Run a background git fetch + pull in share_dir.
|
|
18
|
+
# If a new version is pulled, downloads the ghost-tab-tui binary and writes a flag file.
|
|
19
|
+
# Args: share_dir
|
|
20
|
+
check_for_update() {
|
|
21
|
+
local share_dir="$1"
|
|
22
|
+
local config_home="${XDG_CONFIG_HOME:-$HOME/.config}"
|
|
23
|
+
local flag="${config_home}/ghost-tab/updated"
|
|
24
|
+
|
|
25
|
+
# Only works if share_dir is a git repo
|
|
26
|
+
[ -d "$share_dir/.git" ] || return 0
|
|
27
|
+
|
|
28
|
+
(
|
|
29
|
+
local local_ref remote_ref
|
|
30
|
+
git -C "$share_dir" fetch origin main --quiet 2>/dev/null || return
|
|
31
|
+
|
|
32
|
+
local_ref="$(git -C "$share_dir" rev-parse HEAD 2>/dev/null)"
|
|
33
|
+
remote_ref="$(git -C "$share_dir" rev-parse origin/main 2>/dev/null)"
|
|
34
|
+
[ "$local_ref" = "$remote_ref" ] && return
|
|
35
|
+
|
|
36
|
+
git -C "$share_dir" pull --rebase --quiet origin main 2>/dev/null || return
|
|
37
|
+
|
|
38
|
+
local new_version arch
|
|
39
|
+
new_version="$(tr -d '[:space:]' < "$share_dir/VERSION" 2>/dev/null)" || return
|
|
40
|
+
[ -n "$new_version" ] || return
|
|
41
|
+
|
|
42
|
+
arch="$(uname -m)"
|
|
43
|
+
local bin_url="https://github.com/JackUait/ghost-tab/releases/download/v${new_version}/ghost-tab-tui-darwin-${arch}"
|
|
44
|
+
mkdir -p "$HOME/.local/bin"
|
|
45
|
+
curl -fsSL -o "$HOME/.local/bin/ghost-tab-tui" "$bin_url" 2>/dev/null && \
|
|
46
|
+
chmod +x "$HOME/.local/bin/ghost-tab-tui" || true
|
|
47
|
+
|
|
48
|
+
mkdir -p "${config_home}/ghost-tab"
|
|
49
|
+
echo "$new_version" > "$flag"
|
|
50
|
+
) &
|
|
51
|
+
disown
|
|
52
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ghost-tab",
|
|
3
|
+
"version": "2.6.0",
|
|
4
|
+
"description": "Terminal + tmux wrapper for AI coding tools (Claude Code, Codex CLI, Copilot CLI, OpenCode)",
|
|
5
|
+
"bin": {
|
|
6
|
+
"ghost-tab": "bin/npx-ghost-tab.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin/ghost-tab",
|
|
10
|
+
"bin/npx-ghost-tab.js",
|
|
11
|
+
"lib/",
|
|
12
|
+
"templates/",
|
|
13
|
+
"ghostty/",
|
|
14
|
+
"terminals/",
|
|
15
|
+
"wrapper.sh",
|
|
16
|
+
"VERSION"
|
|
17
|
+
],
|
|
18
|
+
"os": [
|
|
19
|
+
"darwin"
|
|
20
|
+
],
|
|
21
|
+
"keywords": [
|
|
22
|
+
"ghost-tab",
|
|
23
|
+
"tmux",
|
|
24
|
+
"terminal",
|
|
25
|
+
"claude",
|
|
26
|
+
"codex",
|
|
27
|
+
"copilot",
|
|
28
|
+
"opencode",
|
|
29
|
+
"ai",
|
|
30
|
+
"coding"
|
|
31
|
+
],
|
|
32
|
+
"author": "JackUait",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/JackUait/ghost-tab.git"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/JackUait/ghost-tab",
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=16"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"lines": [
|
|
4
|
+
[
|
|
5
|
+
{
|
|
6
|
+
"id": "1",
|
|
7
|
+
"type": "context-percentage",
|
|
8
|
+
"color": "yellow",
|
|
9
|
+
"bold": true,
|
|
10
|
+
"rawValue": true
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
[],
|
|
14
|
+
[]
|
|
15
|
+
],
|
|
16
|
+
"flexMode": "full-minus-40",
|
|
17
|
+
"compactThreshold": 60,
|
|
18
|
+
"colorLevel": 2,
|
|
19
|
+
"inheritSeparatorColors": false,
|
|
20
|
+
"globalBold": false,
|
|
21
|
+
"powerline": {
|
|
22
|
+
"enabled": false,
|
|
23
|
+
"separators": [""],
|
|
24
|
+
"separatorInvertBackground": [false],
|
|
25
|
+
"startCaps": [],
|
|
26
|
+
"endCaps": [],
|
|
27
|
+
"autoAlign": false
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
input=$(cat)
|
|
3
|
+
cwd=$(echo "$input" | sed -n 's/.*"current_dir":"\([^"]*\)".*/\1/p')
|
|
4
|
+
|
|
5
|
+
if git -C "$cwd" rev-parse --git-dir > /dev/null 2>&1; then
|
|
6
|
+
repo_name=$(basename "$cwd")
|
|
7
|
+
branch=$(git -C "$cwd" --no-optional-locks rev-parse --abbrev-ref HEAD 2>/dev/null)
|
|
8
|
+
|
|
9
|
+
# Session line diff: compare working tree against baseline SHA
|
|
10
|
+
baseline_file="${GHOST_TAB_BASELINE_FILE:-}"
|
|
11
|
+
if [ -n "$baseline_file" ] && [ -f "$baseline_file" ]; then
|
|
12
|
+
baseline_sha=$(head -1 "$baseline_file" 2>/dev/null)
|
|
13
|
+
if [ -n "$baseline_sha" ]; then
|
|
14
|
+
diff_stats=$(git -C "$cwd" --no-optional-locks diff "$baseline_sha" --numstat 2>/dev/null \
|
|
15
|
+
| awk '{a+=$1; d+=$2} END {print a+0, d+0}')
|
|
16
|
+
added=$(echo "$diff_stats" | cut -d' ' -f1)
|
|
17
|
+
deleted=$(echo "$diff_stats" | cut -d' ' -f2)
|
|
18
|
+
fi
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
if [ -n "${added:-}" ]; then
|
|
22
|
+
printf '\033[01;36m%s\033[00m | \033[01;32m%s\033[00m | \033[01;32m+%s\033[00m / \033[01;31m-%s\033[00m' \
|
|
23
|
+
"$repo_name" "$branch" "$added" "$deleted"
|
|
24
|
+
else
|
|
25
|
+
printf '\033[01;36m%s\033[00m | \033[01;32m%s\033[00m' \
|
|
26
|
+
"$repo_name" "$branch"
|
|
27
|
+
fi
|
|
28
|
+
else
|
|
29
|
+
printf '\033[01;36m%s\033[00m' "$(basename "$cwd")"
|
|
30
|
+
fi
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# shellcheck source=../lib/statusline.sh
|
|
3
|
+
source "$(dirname "$0")/../lib/statusline.sh" 2>/dev/null \
|
|
4
|
+
|| source ~/.claude/statusline-helpers.sh 2>/dev/null \
|
|
5
|
+
|| true
|
|
6
|
+
|
|
7
|
+
input=$(cat)
|
|
8
|
+
git_info=$(echo "$input" | bash ~/.claude/statusline-command.sh)
|
|
9
|
+
context_pct=$(echo "$input" | npx ccstatusline 2>/dev/null)
|
|
10
|
+
|
|
11
|
+
# Find parent Claude Code process and get total tree memory usage
|
|
12
|
+
pid=$PPID
|
|
13
|
+
mem_label=""
|
|
14
|
+
while [ -n "$pid" ] && [ "$pid" != "1" ]; do
|
|
15
|
+
comm=$(ps -o comm= -p "$pid" 2>/dev/null | xargs basename 2>/dev/null)
|
|
16
|
+
if [ "$comm" = "claude" ]; then
|
|
17
|
+
if type get_tree_rss_kb &>/dev/null; then
|
|
18
|
+
mem_kb=$(get_tree_rss_kb "$pid")
|
|
19
|
+
else
|
|
20
|
+
mem_kb=$(ps -o rss= -p "$pid" 2>/dev/null | tr -d ' ')
|
|
21
|
+
fi
|
|
22
|
+
if [ -n "$mem_kb" ] && [ "$mem_kb" -gt 0 ] 2>/dev/null; then
|
|
23
|
+
mem_mb=$((mem_kb / 1024))
|
|
24
|
+
if [ "$mem_mb" -ge 1024 ]; then
|
|
25
|
+
mem_gb=$(echo "scale=1; $mem_mb / 1024" | bc)
|
|
26
|
+
mem_label="${mem_gb}G"
|
|
27
|
+
else
|
|
28
|
+
mem_label="${mem_mb}M"
|
|
29
|
+
fi
|
|
30
|
+
fi
|
|
31
|
+
break
|
|
32
|
+
fi
|
|
33
|
+
pid=$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d ' ')
|
|
34
|
+
done
|
|
35
|
+
|
|
36
|
+
if [ -n "$mem_label" ]; then
|
|
37
|
+
printf '%s | %s | \033[01;35m%s\033[00m' "$git_info" "$context_pct" "$mem_label"
|
|
38
|
+
else
|
|
39
|
+
printf '%s | %s' "$git_info" "$context_pct"
|
|
40
|
+
fi
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
shell ~/.config/ghost-tab/wrapper.sh
|
package/wrapper.sh
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"
|
|
3
|
+
|
|
4
|
+
SHARE_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/ghost-tab"
|
|
5
|
+
|
|
6
|
+
# shellcheck source=/dev/null
|
|
7
|
+
[ -f "$SHARE_DIR/lib/update.sh" ] && source "$SHARE_DIR/lib/update.sh"
|
|
8
|
+
|
|
9
|
+
notify_if_updated
|
|
10
|
+
check_for_update "$SHARE_DIR"
|
|
11
|
+
|
|
12
|
+
# Show animated loading screen immediately in interactive mode (no args)
|
|
13
|
+
_wrapper_dir_early="$(cd "$(dirname "$0")" && pwd)"
|
|
14
|
+
if [ -z "$1" ] && [ -f "$_wrapper_dir_early/lib/loading.sh" ]; then
|
|
15
|
+
# shellcheck disable=SC1091 # Dynamic path
|
|
16
|
+
source "$_wrapper_dir_early/lib/loading.sh"
|
|
17
|
+
# Mirrors AI_TOOL_PREF_FILE (defined after libs load); duplicated here because loading.sh runs before modules
|
|
18
|
+
_ai_tool="$(cat "${XDG_CONFIG_HOME:-$HOME/.config}/ghost-tab/ai-tool" 2>/dev/null | tr -d '[:space:]')"
|
|
19
|
+
show_loading_screen "${_ai_tool:-}"
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
# Check if ghost-tab-tui binary is available
|
|
23
|
+
if ! command -v ghost-tab-tui &>/dev/null; then
|
|
24
|
+
printf '\033[31mError:\033[0m ghost-tab-tui binary not found.\n' >&2
|
|
25
|
+
printf 'Run \033[1mghost-tab\033[0m to reinstall.\n' >&2
|
|
26
|
+
printf 'Press any key to exit...\n' >&2
|
|
27
|
+
read -rsn1
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# Load shared library functions
|
|
32
|
+
_WRAPPER_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
33
|
+
|
|
34
|
+
if [ ! -d "$_WRAPPER_DIR/lib" ]; then
|
|
35
|
+
printf '\033[31mError:\033[0m Ghost Tab libraries not found at %s/lib\n' "$_WRAPPER_DIR" >&2
|
|
36
|
+
printf 'Run \033[1mghost-tab\033[0m to reinstall.\n' >&2
|
|
37
|
+
printf 'Press any key to exit...\n' >&2
|
|
38
|
+
read -rsn1
|
|
39
|
+
exit 1
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
_gt_libs=(ai-tools projects process input tui menu-tui project-actions project-actions-tui tmux-session settings-menu-tui settings-json notification-setup tab-title-watcher)
|
|
43
|
+
for _gt_lib in "${_gt_libs[@]}"; do
|
|
44
|
+
if [ ! -f "$_WRAPPER_DIR/lib/${_gt_lib}.sh" ]; then
|
|
45
|
+
printf '\033[31mError:\033[0m Missing library %s/lib/%s.sh\n' "$_WRAPPER_DIR" "$_gt_lib" >&2
|
|
46
|
+
printf 'Run \033[1mghost-tab\033[0m to reinstall.\n' >&2
|
|
47
|
+
printf 'Press any key to exit...\n' >&2
|
|
48
|
+
read -rsn1
|
|
49
|
+
exit 1
|
|
50
|
+
fi
|
|
51
|
+
# shellcheck disable=SC1090 # Dynamic module loading
|
|
52
|
+
source "$_WRAPPER_DIR/lib/${_gt_lib}.sh"
|
|
53
|
+
done
|
|
54
|
+
unset _gt_libs _gt_lib
|
|
55
|
+
|
|
56
|
+
TMUX_CMD="$(command -v tmux)"
|
|
57
|
+
LAZYGIT_CMD="$(command -v lazygit)"
|
|
58
|
+
BROOT_CMD="$(command -v broot)"
|
|
59
|
+
CLAUDE_CMD="$(command -v claude)"
|
|
60
|
+
CODEX_CMD="$(command -v codex)"
|
|
61
|
+
COPILOT_CMD="$(command -v copilot)"
|
|
62
|
+
OPENCODE_CMD="$(command -v opencode)"
|
|
63
|
+
|
|
64
|
+
# AI tool preference
|
|
65
|
+
AI_TOOL_PREF_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/ghost-tab/ai-tool"
|
|
66
|
+
AI_TOOLS_AVAILABLE=()
|
|
67
|
+
[ -n "$CLAUDE_CMD" ] && AI_TOOLS_AVAILABLE+=("claude")
|
|
68
|
+
[ -n "$CODEX_CMD" ] && AI_TOOLS_AVAILABLE+=("codex")
|
|
69
|
+
[ -n "$COPILOT_CMD" ] && AI_TOOLS_AVAILABLE+=("copilot")
|
|
70
|
+
[ -n "$OPENCODE_CMD" ] && AI_TOOLS_AVAILABLE+=("opencode")
|
|
71
|
+
|
|
72
|
+
# Read saved preference, default to first available
|
|
73
|
+
SELECTED_AI_TOOL=""
|
|
74
|
+
if [ -f "$AI_TOOL_PREF_FILE" ]; then
|
|
75
|
+
SELECTED_AI_TOOL="$(cat "$AI_TOOL_PREF_FILE" 2>/dev/null | tr -d '[:space:]')"
|
|
76
|
+
fi
|
|
77
|
+
# Validate saved preference is still installed
|
|
78
|
+
validate_ai_tool "$AI_TOOL_PREF_FILE"
|
|
79
|
+
|
|
80
|
+
# Load user projects from config file if it exists
|
|
81
|
+
PROJECTS_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/ghost-tab/projects"
|
|
82
|
+
|
|
83
|
+
# Select working directory
|
|
84
|
+
if [ -n "$1" ] && [ -d "$1" ]; then
|
|
85
|
+
cd "$1" || exit 1
|
|
86
|
+
shift
|
|
87
|
+
elif [ -z "$1" ]; then
|
|
88
|
+
# Use TUI for project selection
|
|
89
|
+
printf '\033]0;👻 Ghost Tab\007'
|
|
90
|
+
|
|
91
|
+
# Stop loading animation before TUI takes over
|
|
92
|
+
type stop_loading_screen &>/dev/null && stop_loading_screen
|
|
93
|
+
|
|
94
|
+
while true; do
|
|
95
|
+
if select_project_interactive "$PROJECTS_FILE"; then
|
|
96
|
+
# Update AI tool if user cycled it in the menu (for all actions)
|
|
97
|
+
if [[ -n "${_selected_ai_tool:-}" ]]; then
|
|
98
|
+
SELECTED_AI_TOOL="$_selected_ai_tool"
|
|
99
|
+
fi
|
|
100
|
+
# shellcheck disable=SC2154
|
|
101
|
+
case "$_selected_project_action" in
|
|
102
|
+
select-project|open-once)
|
|
103
|
+
PROJECT_NAME="$_selected_project_name"
|
|
104
|
+
# shellcheck disable=SC2154
|
|
105
|
+
cd "$_selected_project_path" || exit 1
|
|
106
|
+
break
|
|
107
|
+
;;
|
|
108
|
+
plain-terminal)
|
|
109
|
+
exec "$SHELL"
|
|
110
|
+
;;
|
|
111
|
+
add-worktree)
|
|
112
|
+
# Loop back to menu — worktrees refresh on reload
|
|
113
|
+
continue
|
|
114
|
+
;;
|
|
115
|
+
*)
|
|
116
|
+
# settings or unknown — loop back to menu
|
|
117
|
+
continue
|
|
118
|
+
;;
|
|
119
|
+
esac
|
|
120
|
+
else
|
|
121
|
+
# User quit (ESC/Ctrl-C)
|
|
122
|
+
exit 0
|
|
123
|
+
fi
|
|
124
|
+
done
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
PROJECT_DIR="$(pwd)"
|
|
128
|
+
export PROJECT_DIR
|
|
129
|
+
export PROJECT_NAME="${PROJECT_NAME:-$(basename "$PROJECT_DIR")}"
|
|
130
|
+
SESSION_NAME="dev-${PROJECT_NAME}-$$"
|
|
131
|
+
|
|
132
|
+
# Capture session baseline for line diff tracking
|
|
133
|
+
GHOST_TAB_BASELINE_FILE="/tmp/ghost-tab-baseline-${SESSION_NAME}"
|
|
134
|
+
if git rev-parse --git-dir > /dev/null 2>&1; then
|
|
135
|
+
git rev-parse HEAD > "$GHOST_TAB_BASELINE_FILE" 2>/dev/null
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
# Set terminal/tab title based on tab_title setting
|
|
139
|
+
_tab_title_setting="full"
|
|
140
|
+
_settings_file="${XDG_CONFIG_HOME:-$HOME/.config}/ghost-tab/settings"
|
|
141
|
+
if [ -f "$_settings_file" ]; then
|
|
142
|
+
_saved_tab_title=$(grep '^tab_title=' "$_settings_file" 2>/dev/null | cut -d= -f2)
|
|
143
|
+
if [ -n "$_saved_tab_title" ]; then
|
|
144
|
+
_tab_title_setting="$_saved_tab_title"
|
|
145
|
+
fi
|
|
146
|
+
fi
|
|
147
|
+
if [ "$_tab_title_setting" = "full" ]; then
|
|
148
|
+
set_tab_title "$PROJECT_NAME" "$SELECTED_AI_TOOL"
|
|
149
|
+
else
|
|
150
|
+
set_tab_title "$PROJECT_NAME"
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
# Tab title waiting indicator
|
|
154
|
+
GHOST_TAB_MARKER_FILE="/tmp/ghost-tab-waiting-$$"
|
|
155
|
+
if [ "$SELECTED_AI_TOOL" = "claude" ]; then
|
|
156
|
+
_claude_settings="${HOME}/.claude/settings.json"
|
|
157
|
+
add_waiting_indicator_hooks "$_claude_settings" >/dev/null
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
# Background watcher: switch to Claude pane once it's ready
|
|
161
|
+
(
|
|
162
|
+
while true; do
|
|
163
|
+
sleep 0.5
|
|
164
|
+
content=$("$TMUX_CMD" capture-pane -t "$SESSION_NAME:0.1" -p 2>/dev/null)
|
|
165
|
+
# All three tools show a prompt character when ready
|
|
166
|
+
if echo "$content" | grep -qE '[>$❯]'; then
|
|
167
|
+
"$TMUX_CMD" select-pane -t "$SESSION_NAME:0.1"
|
|
168
|
+
break
|
|
169
|
+
fi
|
|
170
|
+
done
|
|
171
|
+
) &
|
|
172
|
+
WATCHER_PID=$!
|
|
173
|
+
|
|
174
|
+
cleanup() {
|
|
175
|
+
stop_tab_title_watcher "$GHOST_TAB_MARKER_FILE"
|
|
176
|
+
# Remove waiting indicator hooks if no other Ghost Tab sessions are running
|
|
177
|
+
if [ "$SELECTED_AI_TOOL" = "claude" ]; then
|
|
178
|
+
# Clean up orphaned markers from dead sessions (e.g., after SIGKILL)
|
|
179
|
+
for marker in /tmp/ghost-tab-waiting-*; do
|
|
180
|
+
[ -f "$marker" ] || continue
|
|
181
|
+
local pid="${marker##*-}"
|
|
182
|
+
if ! kill -0 "$pid" 2>/dev/null; then
|
|
183
|
+
rm -f "$marker"
|
|
184
|
+
fi
|
|
185
|
+
done
|
|
186
|
+
if ! ls /tmp/ghost-tab-waiting-* &>/dev/null; then
|
|
187
|
+
remove_waiting_indicator_hooks "${HOME}/.claude/settings.json" >/dev/null 2>&1 || true
|
|
188
|
+
fi
|
|
189
|
+
fi
|
|
190
|
+
cleanup_tmux_session "$SESSION_NAME" "$WATCHER_PID" "$TMUX_CMD"
|
|
191
|
+
rm -f "$GHOST_TAB_BASELINE_FILE"
|
|
192
|
+
}
|
|
193
|
+
trap cleanup EXIT HUP TERM INT
|
|
194
|
+
|
|
195
|
+
# Build the AI tool launch command
|
|
196
|
+
case "$SELECTED_AI_TOOL" in
|
|
197
|
+
codex|opencode)
|
|
198
|
+
AI_LAUNCH_CMD="$(build_ai_launch_cmd "$SELECTED_AI_TOOL" "$CLAUDE_CMD" "$CODEX_CMD" "$COPILOT_CMD" "$OPENCODE_CMD" "$PROJECT_DIR")"
|
|
199
|
+
;;
|
|
200
|
+
*)
|
|
201
|
+
AI_LAUNCH_CMD="$(build_ai_launch_cmd "$SELECTED_AI_TOOL" "$CLAUDE_CMD" "$CODEX_CMD" "$COPILOT_CMD" "$OPENCODE_CMD" "$*")"
|
|
202
|
+
;;
|
|
203
|
+
esac
|
|
204
|
+
|
|
205
|
+
# Start tab title watcher before tmux (which blocks until session ends)
|
|
206
|
+
start_tab_title_watcher "$SESSION_NAME" "$SELECTED_AI_TOOL" "$PROJECT_NAME" "$_tab_title_setting" "$TMUX_CMD" "$GHOST_TAB_MARKER_FILE" "${XDG_CONFIG_HOME:-$HOME/.config}/ghost-tab"
|
|
207
|
+
|
|
208
|
+
"$TMUX_CMD" new-session -s "$SESSION_NAME" -e "PATH=$PATH" -e "GHOST_TAB_BASELINE_FILE=$GHOST_TAB_BASELINE_FILE" -e "GHOST_TAB_MARKER_FILE=$GHOST_TAB_MARKER_FILE" -c "$PROJECT_DIR" \
|
|
209
|
+
"$LAZYGIT_CMD; exec bash" \; \
|
|
210
|
+
set-option status-left " ⬡ ${PROJECT_NAME} " \; \
|
|
211
|
+
set-option status-left-style "fg=white,bg=colour236,bold" \; \
|
|
212
|
+
set-option status-style "bg=colour235" \; \
|
|
213
|
+
set-option status-right "" \; \
|
|
214
|
+
set-option set-titles off \; \
|
|
215
|
+
set-option exit-unattached on \; \
|
|
216
|
+
split-window -h -p 50 -c "$PROJECT_DIR" \
|
|
217
|
+
"$AI_LAUNCH_CMD; exec bash" \; \
|
|
218
|
+
select-pane -t 0 \; \
|
|
219
|
+
split-window -v -p 50 -c "$PROJECT_DIR" \
|
|
220
|
+
"trap exit TERM; while true; do $BROOT_CMD $PROJECT_DIR; done" \; \
|
|
221
|
+
split-window -v -p 30 -c "$PROJECT_DIR" \; \
|
|
222
|
+
select-pane -t 3
|