mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-04 22:49:41 +02:00
chore: bump version to 0.1481
This commit is contained in:
Executable
+659
@@ -0,0 +1,659 @@
|
||||
#!/bin/bash
|
||||
# ============================================================================
|
||||
# Claudeman Sessions - Mobile-friendly Tmux Session Chooser
|
||||
# Optimized for iPhone/Termius (portrait ~45 chars, landscape ~95 chars)
|
||||
# ============================================================================
|
||||
#
|
||||
# Design principles:
|
||||
# - Single-digit selection (1-9) for fast thumb typing
|
||||
# - Compact display, no wasted space
|
||||
# - Color-coded status for quick scanning
|
||||
# - Names pulled from Claudeman state.json
|
||||
# - Minimal keystrokes to attach
|
||||
#
|
||||
# Usage:
|
||||
# tmux-chooser # Interactive chooser
|
||||
# tmux-chooser 1 # Quick attach to session 1
|
||||
# tmux-chooser -l # List only (non-interactive)
|
||||
# tmux-chooser -h # Help
|
||||
#
|
||||
# Alias: alias sc='tmux-chooser'
|
||||
# Then: sc (interactive)
|
||||
# sc 2 (attach session 2)
|
||||
#
|
||||
# ============================================================================
|
||||
|
||||
set -e
|
||||
|
||||
# ============================================================================
|
||||
# Configuration
|
||||
# ============================================================================
|
||||
|
||||
CLAUDEMAN_STATE="$HOME/.claudeman/state.json"
|
||||
CLAUDEMAN_SESSIONS="$HOME/.claudeman/mux-sessions.json"
|
||||
|
||||
# Fallback to legacy screens.json
|
||||
if [[ ! -f "$CLAUDEMAN_SESSIONS" ]] && [[ -f "$HOME/.claudeman/screens.json" ]]; then
|
||||
CLAUDEMAN_SESSIONS="$HOME/.claudeman/screens.json"
|
||||
fi
|
||||
|
||||
# iPhone 17 Pro portrait width (conservative)
|
||||
MAX_WIDTH=44
|
||||
MAX_NAME_LEN=28
|
||||
|
||||
# Page size for pagination (leave room for header/footer)
|
||||
PAGE_SIZE=7
|
||||
|
||||
# Auto-refresh timeout (seconds) - 0 to disable
|
||||
AUTO_REFRESH=60
|
||||
|
||||
# ============================================================================
|
||||
# Icon Detection (Nerd Fonts vs ASCII)
|
||||
# ============================================================================
|
||||
|
||||
detect_icons() {
|
||||
if [[ "$TERM_PROGRAM" == "iTerm"* ]] || \
|
||||
[[ "$TERM" == "xterm-kitty" ]] || \
|
||||
[[ -n "$WEZTERM_PANE" ]] || \
|
||||
[[ "$LC_TERMINAL" == "iTerm2" ]]; then
|
||||
ICON_SESSION=""
|
||||
ICON_ATTACHED="●"
|
||||
ICON_DETACHED="○"
|
||||
ICON_UNKNOWN="◌"
|
||||
else
|
||||
ICON_SESSION="[T]"
|
||||
ICON_ATTACHED="*"
|
||||
ICON_DETACHED="-"
|
||||
ICON_UNKNOWN="?"
|
||||
fi
|
||||
}
|
||||
|
||||
detect_icons
|
||||
|
||||
# ============================================================================
|
||||
# Colors - ANSI 256 for better Termius compatibility
|
||||
# ============================================================================
|
||||
|
||||
R='\033[0m' # Reset
|
||||
B='\033[1m' # Bold
|
||||
D='\033[2m' # Dim
|
||||
GREEN='\033[38;5;82m'
|
||||
YELLOW='\033[38;5;220m'
|
||||
BLUE='\033[38;5;75m'
|
||||
CYAN='\033[38;5;87m'
|
||||
RED='\033[38;5;203m'
|
||||
GRAY='\033[38;5;245m'
|
||||
WHITE='\033[38;5;255m'
|
||||
BG_SEL='\033[48;5;236m'
|
||||
|
||||
# ============================================================================
|
||||
# Utilities
|
||||
# ============================================================================
|
||||
|
||||
truncate() {
|
||||
local str="$1"
|
||||
local max="$2"
|
||||
local len=${#str}
|
||||
|
||||
if [ "$len" -le "$max" ]; then
|
||||
echo "$str"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ "$str" == *"/"* ]]; then
|
||||
echo "..${str: -$((max-2))}"
|
||||
else
|
||||
echo "${str:0:$((max-1))}…"
|
||||
fi
|
||||
}
|
||||
|
||||
find_full_session_id() {
|
||||
local short_id="$1"
|
||||
|
||||
if [ -f "$CLAUDEMAN_STATE" ]; then
|
||||
local full_id
|
||||
full_id=$(jq -r --arg short "$short_id" '
|
||||
.sessions | keys[] | select(startswith($short))
|
||||
' "$CLAUDEMAN_STATE" 2>/dev/null | head -1)
|
||||
if [ -n "$full_id" ]; then
|
||||
echo "$full_id"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f "$CLAUDEMAN_SESSIONS" ]; then
|
||||
local full_id
|
||||
full_id=$(jq -r --arg short "$short_id" '
|
||||
.[] | select(.sessionId | startswith($short)) | .sessionId
|
||||
' "$CLAUDEMAN_SESSIONS" 2>/dev/null | head -1)
|
||||
if [ -n "$full_id" ]; then
|
||||
echo "$full_id"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "$short_id"
|
||||
}
|
||||
|
||||
get_session_name() {
|
||||
local session_id="$1"
|
||||
local name=""
|
||||
local workdir=""
|
||||
|
||||
if [ -f "$CLAUDEMAN_SESSIONS" ]; then
|
||||
local result
|
||||
result=$(jq -r --arg id "$session_id" '
|
||||
.[] | select(.sessionId | startswith($id)) | "\(.name // "")\t\(.workingDir // "")"
|
||||
' "$CLAUDEMAN_SESSIONS" 2>/dev/null | head -1)
|
||||
if [ -n "$result" ]; then
|
||||
name="${result%% *}"
|
||||
workdir="${result#* }"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$name" ] && [ -f "$CLAUDEMAN_STATE" ]; then
|
||||
local result
|
||||
result=$(jq -r --arg id "$session_id" '
|
||||
.sessions | to_entries[] | select(.key | startswith($id)) | "\(.value.name // "")\t\(.value.workingDir // "")"
|
||||
' "$CLAUDEMAN_STATE" 2>/dev/null | head -1)
|
||||
if [ -n "$result" ]; then
|
||||
name="${result%% *}"
|
||||
[ -z "$workdir" ] && workdir="${result#* }"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$name" ]; then
|
||||
echo "$name"
|
||||
return
|
||||
fi
|
||||
|
||||
if [ -n "$workdir" ]; then
|
||||
echo "${workdir##*/}"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "${session_id:0:8}"
|
||||
}
|
||||
|
||||
get_working_dir() {
|
||||
local session_id="$1"
|
||||
|
||||
if [ -f "$CLAUDEMAN_SESSIONS" ]; then
|
||||
local dir
|
||||
dir=$(jq -r --arg id "$session_id" '
|
||||
.[] | select(.sessionId | startswith($id)) | .workingDir // empty
|
||||
' "$CLAUDEMAN_SESSIONS" 2>/dev/null | head -1)
|
||||
if [ -n "$dir" ] && [ "$dir" != "null" ]; then
|
||||
echo "${dir/#$HOME/~}"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f "$CLAUDEMAN_STATE" ]; then
|
||||
local dir
|
||||
dir=$(jq -r --arg id "$session_id" '
|
||||
.sessions | to_entries[] | select(.key | startswith($id)) | .value.workingDir // empty
|
||||
' "$CLAUDEMAN_STATE" 2>/dev/null | head -1)
|
||||
if [ -n "$dir" ] && [ "$dir" != "null" ]; then
|
||||
echo "${dir/#$HOME/~}"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
get_tokens() {
|
||||
local session_id="$1"
|
||||
|
||||
if [ -f "$CLAUDEMAN_STATE" ]; then
|
||||
local tokens
|
||||
tokens=$(jq -r --arg id "$session_id" '
|
||||
.sessions | to_entries[] | select(.key | startswith($id)) |
|
||||
((.value.inputTokens // 0) + (.value.outputTokens // 0))
|
||||
' "$CLAUDEMAN_STATE" 2>/dev/null | head -1)
|
||||
|
||||
if [ -n "$tokens" ] && [ "$tokens" != "null" ] && [ "$tokens" -gt 0 ] 2>/dev/null; then
|
||||
if [ "$tokens" -gt 1000 ]; then
|
||||
echo "$((tokens / 1000))k"
|
||||
else
|
||||
echo "${tokens}"
|
||||
fi
|
||||
return
|
||||
fi
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
get_respawn_status() {
|
||||
local session_id="$1"
|
||||
|
||||
if [ -f "$CLAUDEMAN_SESSIONS" ]; then
|
||||
local respawn_enabled
|
||||
respawn_enabled=$(jq -r --arg id "$session_id" '
|
||||
.[] | select(.sessionId | startswith($id)) | .respawnConfig.enabled // false
|
||||
' "$CLAUDEMAN_SESSIONS" 2>/dev/null | head -1)
|
||||
|
||||
if [ "$respawn_enabled" = "true" ]; then
|
||||
echo "R"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
check_deps() {
|
||||
if ! command -v jq &>/dev/null; then
|
||||
echo -e "${YELLOW}Note: Install jq for session names${R}"
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Tmux Session Parser
|
||||
# ============================================================================
|
||||
|
||||
declare -a SESSION_PIDS
|
||||
declare -a MUX_NAMES
|
||||
declare -a SESSION_STATES
|
||||
declare -a SESSION_IDS
|
||||
declare -a DISPLAY_NAMES
|
||||
declare -a WORKING_DIRS
|
||||
declare -a TOKEN_COUNTS
|
||||
declare -a RESPAWN_STATUS
|
||||
|
||||
parse_sessions() {
|
||||
SESSION_PIDS=()
|
||||
MUX_NAMES=()
|
||||
SESSION_STATES=()
|
||||
SESSION_IDS=()
|
||||
DISPLAY_NAMES=()
|
||||
WORKING_DIRS=()
|
||||
TOKEN_COUNTS=()
|
||||
RESPAWN_STATUS=()
|
||||
|
||||
local i=0
|
||||
|
||||
# Parse tmux list-sessions output
|
||||
while IFS= read -r line; do
|
||||
local session_name="${line%%:*}"
|
||||
|
||||
# Only show claudeman sessions
|
||||
if [[ "$session_name" != claudeman-* ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Check if attached
|
||||
local state="Detached"
|
||||
if [[ "$line" == *"(attached)"* ]]; then
|
||||
state="Attached"
|
||||
fi
|
||||
|
||||
# Get PID from tmux
|
||||
local pid
|
||||
pid=$(tmux display-message -t "$session_name" -p '#{pane_pid}' 2>/dev/null || echo "0")
|
||||
|
||||
SESSION_PIDS+=("$pid")
|
||||
MUX_NAMES+=("$session_name")
|
||||
SESSION_STATES+=("$state")
|
||||
|
||||
# Extract session ID from claudeman session name
|
||||
local session_id=""
|
||||
local cm_regex='^claudeman-(.+)$'
|
||||
if [[ "$session_name" =~ $cm_regex ]]; then
|
||||
session_id="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
SESSION_IDS+=("$session_id")
|
||||
|
||||
# Get display name and metadata
|
||||
if [ -n "$session_id" ]; then
|
||||
DISPLAY_NAMES+=("$(get_session_name "$session_id")")
|
||||
WORKING_DIRS+=("$(get_working_dir "$session_id")")
|
||||
TOKEN_COUNTS+=("$(get_tokens "$session_id")")
|
||||
RESPAWN_STATUS+=("$(get_respawn_status "$session_id")")
|
||||
else
|
||||
DISPLAY_NAMES+=("$session_name")
|
||||
WORKING_DIRS+=("")
|
||||
TOKEN_COUNTS+=("")
|
||||
RESPAWN_STATUS+=("")
|
||||
fi
|
||||
|
||||
i=$((i + 1))
|
||||
done < <(tmux list-sessions 2>/dev/null || true)
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Display Functions
|
||||
# ============================================================================
|
||||
|
||||
clear_screen() {
|
||||
printf '\033[2J\033[H'
|
||||
}
|
||||
|
||||
print_header() {
|
||||
local count=${#SESSION_PIDS[@]}
|
||||
echo -e "${B}${CYAN}Claudeman Sessions${R} ${D}($count)${R}"
|
||||
echo -e "${D}$(printf '%.0s─' {1..32})${R}"
|
||||
}
|
||||
|
||||
print_entry() {
|
||||
local idx="$1"
|
||||
local num=$((idx + 1))
|
||||
local name="${DISPLAY_NAMES[$idx]}"
|
||||
local state="${SESSION_STATES[$idx]}"
|
||||
local dir="${WORKING_DIRS[$idx]}"
|
||||
local tokens="${TOKEN_COUNTS[$idx]}"
|
||||
local respawn="${RESPAWN_STATUS[$idx]}"
|
||||
|
||||
local name_max=$MAX_NAME_LEN
|
||||
[ -n "$respawn" ] && name_max=$((name_max - 2))
|
||||
[ -n "$tokens" ] && name_max=$((name_max - 4))
|
||||
name=$(truncate "$name" $name_max)
|
||||
|
||||
local status_icon status_color
|
||||
if [[ "$state" == *"Attached"* ]]; then
|
||||
status_icon="$ICON_ATTACHED"
|
||||
status_color="$GREEN"
|
||||
elif [[ "$state" == *"Detached"* ]]; then
|
||||
status_icon="$ICON_DETACHED"
|
||||
status_color="$GRAY"
|
||||
else
|
||||
status_icon="$ICON_UNKNOWN"
|
||||
status_color="$YELLOW"
|
||||
fi
|
||||
|
||||
local num_str="${B}${WHITE}${num})${R}"
|
||||
local name_str="${B}${WHITE}${name}${R}"
|
||||
local status_str="${status_color}${status_icon}${R}"
|
||||
|
||||
local respawn_str=""
|
||||
if [ -n "$respawn" ]; then
|
||||
respawn_str=" ${GREEN}${respawn}${R}"
|
||||
fi
|
||||
|
||||
local token_str=""
|
||||
if [ -n "$tokens" ]; then
|
||||
token_str=" ${D}${tokens}${R}"
|
||||
fi
|
||||
|
||||
echo -e " ${num_str} ${name_str} ${status_str}${respawn_str}${token_str}"
|
||||
|
||||
if [ -n "$dir" ]; then
|
||||
dir=$(truncate "$dir" $((MAX_NAME_LEN - 2)))
|
||||
echo -e " ${D}${dir}${R}"
|
||||
fi
|
||||
}
|
||||
|
||||
print_footer() {
|
||||
local page="$1"
|
||||
local total_pages="$2"
|
||||
|
||||
echo ""
|
||||
echo -e "${D}────────────────────────────────${R}"
|
||||
|
||||
if [ "$total_pages" -gt 1 ]; then
|
||||
echo -e " ${D}Page $((page+1))/$total_pages${R} ${GRAY}[${WHITE}n${GRAY}]ext [${WHITE}p${GRAY}]rev${R}"
|
||||
fi
|
||||
|
||||
echo -e " ${GRAY}[${WHITE}1-9${GRAY}]attach [${WHITE}r${GRAY}]efresh [${WHITE}q${GRAY}]uit${R}"
|
||||
}
|
||||
|
||||
print_no_sessions() {
|
||||
clear_screen
|
||||
echo -e "${B}${CYAN}Claudeman Sessions${R}"
|
||||
echo -e "${D}$(printf '%.0s─' {1..32})${R}"
|
||||
echo ""
|
||||
echo -e " ${YELLOW}No tmux sessions found${R}"
|
||||
echo ""
|
||||
echo -e " ${D}Start one with:${R}"
|
||||
echo -e " ${WHITE}claudeman web${R}"
|
||||
echo ""
|
||||
echo -e "${D}$(printf '%.0s─' {1..32})${R}"
|
||||
echo -e " ${GRAY}[${WHITE}r${GRAY}]efresh [${WHITE}q${GRAY}]uit${R}"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Main Display Loop
|
||||
# ============================================================================
|
||||
|
||||
current_page=0
|
||||
|
||||
render() {
|
||||
clear_screen
|
||||
parse_sessions
|
||||
|
||||
local count=${#SESSION_PIDS[@]}
|
||||
|
||||
if [ "$count" -eq 0 ]; then
|
||||
print_no_sessions
|
||||
return
|
||||
fi
|
||||
|
||||
local total_pages=$(( (count + PAGE_SIZE - 1) / PAGE_SIZE ))
|
||||
|
||||
if [ "$current_page" -ge "$total_pages" ]; then
|
||||
current_page=$((total_pages - 1))
|
||||
fi
|
||||
if [ "$current_page" -lt 0 ]; then
|
||||
current_page=0
|
||||
fi
|
||||
|
||||
local start=$((current_page * PAGE_SIZE))
|
||||
local end=$((start + PAGE_SIZE))
|
||||
if [ "$end" -gt "$count" ]; then
|
||||
end=$count
|
||||
fi
|
||||
|
||||
print_header
|
||||
echo ""
|
||||
|
||||
for ((i = start; i < end; i++)); do
|
||||
print_entry $i
|
||||
done
|
||||
|
||||
print_footer $current_page $total_pages
|
||||
}
|
||||
|
||||
attach_session() {
|
||||
local idx="$1"
|
||||
local mux_name="${MUX_NAMES[$idx]}"
|
||||
|
||||
if [ -z "$mux_name" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
clear_screen
|
||||
echo -e "${GREEN}Attaching to ${B}${DISPLAY_NAMES[$idx]}${R}${GREEN}...${R}"
|
||||
echo -e "${D}(Ctrl+B D to detach)${R}"
|
||||
sleep 0.3
|
||||
|
||||
tmux attach-session -t "$mux_name"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Input Handler
|
||||
# ============================================================================
|
||||
|
||||
handle_input() {
|
||||
local key="$1"
|
||||
local count=${#SESSION_PIDS[@]}
|
||||
local total_pages=$(( (count + PAGE_SIZE - 1) / PAGE_SIZE ))
|
||||
|
||||
case "$key" in
|
||||
[1-9])
|
||||
local idx=$((key - 1))
|
||||
if [ "$idx" -lt "$count" ]; then
|
||||
attach_session "$idx"
|
||||
return 0
|
||||
fi
|
||||
;;
|
||||
|
||||
$'\e')
|
||||
read -rsn2 -t 0.1 seq 2>/dev/null || true
|
||||
case "$seq" in
|
||||
'[A'|'[D')
|
||||
if [ "$total_pages" -gt 1 ]; then
|
||||
current_page=$(( (current_page - 1 + total_pages) % total_pages ))
|
||||
fi
|
||||
;;
|
||||
'[B'|'[C')
|
||||
if [ "$total_pages" -gt 1 ]; then
|
||||
current_page=$(( (current_page + 1) % total_pages ))
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
|
||||
n|N|j|J)
|
||||
if [ "$total_pages" -gt 1 ]; then
|
||||
current_page=$(( (current_page + 1) % total_pages ))
|
||||
fi
|
||||
;;
|
||||
|
||||
p|P|k|K)
|
||||
if [ "$total_pages" -gt 1 ]; then
|
||||
current_page=$(( (current_page - 1 + total_pages) % total_pages ))
|
||||
fi
|
||||
;;
|
||||
|
||||
r|R)
|
||||
;;
|
||||
|
||||
q|Q)
|
||||
clear_screen
|
||||
exit 0
|
||||
;;
|
||||
|
||||
'')
|
||||
if [ "$count" -eq 1 ]; then
|
||||
attach_session 0
|
||||
return 0
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# List Mode
|
||||
# ============================================================================
|
||||
|
||||
list_mode() {
|
||||
parse_sessions
|
||||
local count=${#SESSION_PIDS[@]}
|
||||
|
||||
if [ "$count" -eq 0 ]; then
|
||||
echo "No tmux sessions"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for ((i = 0; i < count; i++)); do
|
||||
local num=$((i + 1))
|
||||
local name="${DISPLAY_NAMES[$i]}"
|
||||
local state="${SESSION_STATES[$i]}"
|
||||
local respawn="${RESPAWN_STATUS[$i]}"
|
||||
local indicator="-"
|
||||
[[ "$state" == *"Attached"* ]] && indicator="*"
|
||||
[ -n "$respawn" ] && indicator="${indicator}R"
|
||||
|
||||
echo "$num) $name [$indicator]"
|
||||
done
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Quick Attach
|
||||
# ============================================================================
|
||||
|
||||
quick_attach() {
|
||||
local num="$1"
|
||||
parse_sessions
|
||||
|
||||
local count=${#SESSION_PIDS[@]}
|
||||
local idx=$((num - 1))
|
||||
|
||||
if [ "$idx" -lt 0 ] || [ "$idx" -ge "$count" ]; then
|
||||
echo -e "${RED}Invalid session: $num${R}"
|
||||
echo "Available: 1-$count"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
attach_session "$idx"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Help
|
||||
# ============================================================================
|
||||
|
||||
show_help() {
|
||||
cat << 'EOF'
|
||||
Claudeman Sessions - Mobile-friendly Tmux Session Chooser
|
||||
|
||||
USAGE:
|
||||
sc Interactive chooser
|
||||
sc <number> Quick attach to session N
|
||||
sc -l List sessions (non-interactive)
|
||||
sc -h Show this help
|
||||
|
||||
INTERACTIVE KEYS:
|
||||
1-9 Attach to session
|
||||
n/j/↓ Next page
|
||||
p/k/↑ Previous page
|
||||
r Refresh
|
||||
q Quit
|
||||
|
||||
INDICATORS:
|
||||
* / ● Attached (someone connected)
|
||||
- / ○ Detached (available)
|
||||
R Respawn enabled
|
||||
45k Token count
|
||||
|
||||
TIPS:
|
||||
- Detach from tmux: Ctrl+B D
|
||||
- Session names from Claudeman state
|
||||
- Optimized for Termius/iPhone
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Main
|
||||
# ============================================================================
|
||||
|
||||
main() {
|
||||
case "${1:-}" in
|
||||
-h|--help)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
-l|--list)
|
||||
list_mode
|
||||
exit 0
|
||||
;;
|
||||
[1-9]|[1-9][0-9])
|
||||
quick_attach "$1"
|
||||
exit $?
|
||||
;;
|
||||
esac
|
||||
|
||||
check_deps
|
||||
|
||||
render
|
||||
|
||||
while true; do
|
||||
local timeout_opt=""
|
||||
if [ "$AUTO_REFRESH" -gt 0 ]; then
|
||||
timeout_opt="-t $AUTO_REFRESH"
|
||||
fi
|
||||
|
||||
if read -rsn1 $timeout_opt key 2>/dev/null; then
|
||||
handle_input "$key"
|
||||
fi
|
||||
render
|
||||
done
|
||||
}
|
||||
|
||||
trap 'clear_screen; exit 0' INT
|
||||
|
||||
main "$@"
|
||||
Executable
+505
@@ -0,0 +1,505 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Claudeman Tmux Session Manager
|
||||
# Interactive tool with arrow key navigation
|
||||
# Reads from ~/.claudeman/mux-sessions.json
|
||||
#
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
MAGENTA='\033[0;35m'
|
||||
BOLD='\033[1m'
|
||||
DIM='\033[2m'
|
||||
NC='\033[0m'
|
||||
REVERSE='\033[7m'
|
||||
|
||||
# Use the same path as claudeman (src/tmux-manager.ts)
|
||||
SESSIONS_FILE="${HOME}/.claudeman/mux-sessions.json"
|
||||
|
||||
# Fallback to legacy screens.json if mux-sessions.json doesn't exist
|
||||
if [[ ! -f "$SESSIONS_FILE" ]] && [[ -f "${HOME}/.claudeman/screens.json" ]]; then
|
||||
SESSIONS_FILE="${HOME}/.claudeman/screens.json"
|
||||
fi
|
||||
|
||||
# Cached data
|
||||
CACHED_JSON=""
|
||||
CACHED_COUNT=0
|
||||
LAST_REFRESH=0
|
||||
REFRESH_INTERVAL=5 # Refresh data every 5 seconds
|
||||
|
||||
# Cleanup on exit
|
||||
cleanup() {
|
||||
tput cnorm 2>/dev/null
|
||||
stty echo 2>/dev/null
|
||||
echo ""
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# Check dependencies
|
||||
check_dependencies() {
|
||||
if ! command -v jq &> /dev/null; then
|
||||
echo -e "${RED}Error: 'jq' is required. Install with: sudo apt install jq${NC}"
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v tmux &> /dev/null; then
|
||||
echo -e "${RED}Error: 'tmux' is required. Install with: sudo apt install tmux${NC}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Refresh cached data if needed
|
||||
refresh_cache() {
|
||||
local now=$(date +%s)
|
||||
if [[ $((now - LAST_REFRESH)) -ge $REFRESH_INTERVAL ]] || [[ -z "$CACHED_JSON" ]]; then
|
||||
if [[ -f "$SESSIONS_FILE" ]]; then
|
||||
CACHED_JSON=$(cat "$SESSIONS_FILE")
|
||||
else
|
||||
CACHED_JSON="[]"
|
||||
fi
|
||||
CACHED_COUNT=$(echo "$CACHED_JSON" | jq 'length')
|
||||
LAST_REFRESH=$now
|
||||
fi
|
||||
}
|
||||
|
||||
# Force refresh
|
||||
force_refresh() {
|
||||
LAST_REFRESH=0
|
||||
refresh_cache
|
||||
}
|
||||
|
||||
get_session_field() {
|
||||
local index=$1 field=$2
|
||||
# Support both muxName (tmux) and screenName (legacy) fields
|
||||
if [[ "$field" == "muxName" ]]; then
|
||||
local val=$(echo "$CACHED_JSON" | jq -r ".[$index].muxName // .[$index].screenName // \"unknown\"")
|
||||
echo "$val"
|
||||
else
|
||||
echo "$CACHED_JSON" | jq -r ".[$index].$field // \"unknown\""
|
||||
fi
|
||||
}
|
||||
|
||||
# Format duration
|
||||
format_duration() {
|
||||
local s=$1 d=$((s/86400)) h=$(((s%86400)/3600)) m=$(((s%3600)/60))
|
||||
if [[ $d -gt 0 ]]; then printf "%dd %dh" $d $h
|
||||
elif [[ $h -gt 0 ]]; then printf "%dh %dm" $h $m
|
||||
elif [[ $m -gt 0 ]]; then printf "%dm" $m
|
||||
else printf "%ds" $s; fi
|
||||
}
|
||||
|
||||
# Check if tmux session alive (cached per draw cycle)
|
||||
declare -A ALIVE_CACHE
|
||||
check_alive() {
|
||||
local mux_name=$1
|
||||
if [[ -z "${ALIVE_CACHE[$mux_name]+x}" ]]; then
|
||||
if tmux has-session -t "$mux_name" 2>/dev/null; then
|
||||
ALIVE_CACHE[$mux_name]=1
|
||||
else
|
||||
ALIVE_CACHE[$mux_name]=0
|
||||
fi
|
||||
fi
|
||||
[[ "${ALIVE_CACHE[$mux_name]}" -eq 1 ]]
|
||||
}
|
||||
|
||||
clear_alive_cache() {
|
||||
ALIVE_CACHE=()
|
||||
}
|
||||
|
||||
# Kill session by index
|
||||
kill_session() {
|
||||
local idx=$1
|
||||
local mux_name=$(get_session_field $idx "muxName")
|
||||
local pid=$(get_session_field $idx "pid")
|
||||
|
||||
pkill -TERM -P $pid 2>/dev/null
|
||||
kill -TERM -$pid 2>/dev/null
|
||||
tmux kill-session -t "$mux_name" 2>/dev/null
|
||||
kill -KILL $pid 2>/dev/null
|
||||
|
||||
# Remove from JSON
|
||||
CACHED_JSON=$(echo "$CACHED_JSON" | jq "del(.[$idx])")
|
||||
echo "$CACHED_JSON" > "$SESSIONS_FILE"
|
||||
CACHED_COUNT=$(echo "$CACHED_JSON" | jq 'length')
|
||||
clear_alive_cache
|
||||
}
|
||||
|
||||
# Draw header (only once)
|
||||
draw_header() {
|
||||
echo -e "${BOLD}${CYAN}╔══════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${BOLD}${CYAN}║ Claudeman Tmux Session Manager ║${NC}"
|
||||
echo -e "${BOLD}${CYAN}║ ${DIM}Press q or Esc to exit${CYAN} ║${NC}"
|
||||
echo -e "${BOLD}${CYAN}╚══════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Draw a single session row (no_newline=1 for partial updates)
|
||||
draw_row() {
|
||||
local i=$1
|
||||
local selected=$2
|
||||
local no_newline=${3:-0}
|
||||
local now=$(date +%s)
|
||||
|
||||
local name=$(echo "$CACHED_JSON" | jq -r ".[$i].name // \"unnamed\"")
|
||||
local mux_name=$(get_session_field $i "muxName")
|
||||
local created_at=$(echo "$CACHED_JSON" | jq -r ".[$i].createdAt")
|
||||
local mode=$(echo "$CACHED_JSON" | jq -r ".[$i].mode")
|
||||
|
||||
local created_s=$((created_at / 1000))
|
||||
local elapsed=$((now - created_s))
|
||||
local running=$(format_duration $elapsed)
|
||||
|
||||
local status status_color
|
||||
if check_alive "$mux_name"; then
|
||||
status="● alive"
|
||||
status_color="${GREEN}"
|
||||
else
|
||||
status="○ dead"
|
||||
status_color="${RED}"
|
||||
fi
|
||||
|
||||
[[ ${#name} -gt 20 ]] && name="${name:0:17}..."
|
||||
|
||||
local line_end=$'\n'
|
||||
[[ $no_newline -eq 1 ]] && line_end=""
|
||||
|
||||
if [[ $i -eq $selected ]]; then
|
||||
printf " ${REVERSE}${BOLD}▶ %-20s %-14s ${status_color}%-12s${NC}${REVERSE} %-8s ${NC}%s" \
|
||||
"$name" "$running" "$status" "$mode" "$line_end"
|
||||
else
|
||||
printf " %-20s %-14s ${status_color}%-12s${NC} %-8s%s" \
|
||||
"$name" "$running" "$status" "$mode" "$line_end"
|
||||
fi
|
||||
}
|
||||
|
||||
# Draw footer
|
||||
draw_footer() {
|
||||
echo ""
|
||||
echo -e " ${DIM}──────────────────────────────────────────────────────────────${NC}"
|
||||
echo -e " ${GREEN}↑/↓${NC} Navigate ${GREEN}Enter${NC} Attach ${YELLOW}d${NC} Delete ${RED}D${NC} Delete All ${BLUE}i${NC} Info ${DIM}q${NC} Quit"
|
||||
}
|
||||
|
||||
# Full redraw
|
||||
full_redraw() {
|
||||
local selected=$1
|
||||
clear
|
||||
draw_header
|
||||
|
||||
if [[ $CACHED_COUNT -eq 0 ]]; then
|
||||
echo -e " ${YELLOW}No sessions found${NC}"
|
||||
echo -e " ${DIM}(checking: $SESSIONS_FILE)${NC}"
|
||||
echo ""
|
||||
echo -e " ${DIM}Press q to quit${NC}"
|
||||
return
|
||||
fi
|
||||
|
||||
printf " ${BOLD}%-22s %-14s %-12s %-8s${NC}\n" "NAME" "RUNNING" "STATUS" "MODE"
|
||||
echo -e " ${DIM}──────────────────────────────────────────────────────────────${NC}"
|
||||
|
||||
for ((i=0; i<CACHED_COUNT; i++)); do
|
||||
draw_row $i $selected
|
||||
done
|
||||
|
||||
draw_footer
|
||||
}
|
||||
|
||||
# Update just the selection (fast, no flicker)
|
||||
update_selection() {
|
||||
local old_sel=$1
|
||||
local new_sel=$2
|
||||
local header_lines=7 # Lines before session list
|
||||
|
||||
# Move to old selection, clear line, redraw as unselected
|
||||
tput cup $((header_lines + old_sel)) 0
|
||||
tput el # Clear to end of line
|
||||
draw_row $old_sel -1 1 # -1 means not selected, 1 means no newline
|
||||
|
||||
# Move to new selection, clear line, redraw as selected
|
||||
tput cup $((header_lines + new_sel)) 0
|
||||
tput el # Clear to end of line
|
||||
draw_row $new_sel $new_sel 1 # selected, no newline
|
||||
}
|
||||
|
||||
# Show session info
|
||||
show_info() {
|
||||
local idx=$1
|
||||
local now=$(date +%s)
|
||||
|
||||
local name=$(get_session_field $idx "name")
|
||||
local session_id=$(get_session_field $idx "sessionId")
|
||||
local mux_name=$(get_session_field $idx "muxName")
|
||||
local pid=$(get_session_field $idx "pid")
|
||||
local created_at=$(get_session_field $idx "createdAt")
|
||||
local mode=$(get_session_field $idx "mode")
|
||||
local working_dir=$(get_session_field $idx "workingDir")
|
||||
|
||||
local created_s=$((created_at / 1000))
|
||||
local elapsed=$((now - created_s))
|
||||
local started=$(date -d "@$created_s" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "unknown")
|
||||
|
||||
clear
|
||||
echo -e "${BOLD}${CYAN}Session Details${NC}"
|
||||
echo -e "${DIM}────────────────────────────────────────${NC}"
|
||||
echo -e " ${BOLD}Name:${NC} $name"
|
||||
echo -e " ${BOLD}Session ID:${NC} $session_id"
|
||||
echo -e " ${BOLD}Tmux:${NC} $mux_name"
|
||||
echo -e " ${BOLD}PID:${NC} $pid"
|
||||
echo -e " ${BOLD}Mode:${NC} $mode"
|
||||
echo -e " ${BOLD}Directory:${NC} $working_dir"
|
||||
echo -e " ${BOLD}Started:${NC} $started"
|
||||
echo -e " ${BOLD}Running:${NC} $(format_duration $elapsed)"
|
||||
|
||||
if check_alive "$mux_name"; then
|
||||
echo -e " ${BOLD}Status:${NC} ${GREEN}● alive${NC}"
|
||||
local mem=$(ps -o rss= -p $pid 2>/dev/null | tr -d ' ')
|
||||
[[ -n "$mem" ]] && echo -e " ${BOLD}Memory:${NC} $(echo "scale=1; $mem/1024" | bc) MB"
|
||||
else
|
||||
echo -e " ${BOLD}Status:${NC} ${RED}○ dead${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${DIM}Press any key to continue...${NC}"
|
||||
read -rsn1
|
||||
}
|
||||
|
||||
# Confirm dialog
|
||||
confirm() {
|
||||
local msg=$1
|
||||
tput sc # Save cursor
|
||||
tput cup $((CACHED_COUNT + 12)) 0
|
||||
echo -en " ${YELLOW}$msg (y/n):${NC} "
|
||||
read -rsn1 answer
|
||||
tput rc # Restore cursor
|
||||
[[ "$answer" == "y" || "$answer" == "Y" ]]
|
||||
}
|
||||
|
||||
# Read single keypress (handles arrow keys)
|
||||
read_key() {
|
||||
local key
|
||||
IFS= read -rsn1 key
|
||||
|
||||
if [[ "$key" == $'\x1b' ]]; then
|
||||
read -rsn2 -t 0.1 key
|
||||
case "$key" in
|
||||
'[A') echo "UP" ;;
|
||||
'[B') echo "DOWN" ;;
|
||||
*) echo "ESC" ;;
|
||||
esac
|
||||
elif [[ "$key" == "" ]]; then
|
||||
echo "ENTER"
|
||||
else
|
||||
echo "$key"
|
||||
fi
|
||||
}
|
||||
|
||||
# Main interactive loop
|
||||
interactive_menu() {
|
||||
local selected=0
|
||||
local need_full_redraw=1
|
||||
|
||||
tput civis # Hide cursor
|
||||
force_refresh
|
||||
clear_alive_cache
|
||||
full_redraw $selected
|
||||
need_full_redraw=0
|
||||
|
||||
while true; do
|
||||
# Adjust selection if out of bounds
|
||||
[[ $selected -ge $CACHED_COUNT ]] && selected=$((CACHED_COUNT > 0 ? CACHED_COUNT - 1 : 0))
|
||||
[[ $selected -lt 0 ]] && selected=0
|
||||
|
||||
# Redraw if needed
|
||||
if [[ $need_full_redraw -eq 1 ]]; then
|
||||
clear_alive_cache
|
||||
full_redraw $selected
|
||||
need_full_redraw=0
|
||||
fi
|
||||
|
||||
local key=$(read_key)
|
||||
local old_selected=$selected
|
||||
|
||||
case "$key" in
|
||||
UP|k)
|
||||
if [[ $selected -gt 0 ]]; then
|
||||
((selected--))
|
||||
[[ $CACHED_COUNT -gt 0 ]] && update_selection $old_selected $selected
|
||||
fi
|
||||
;;
|
||||
DOWN|j)
|
||||
if [[ $selected -lt $((CACHED_COUNT - 1)) ]]; then
|
||||
((selected++))
|
||||
[[ $CACHED_COUNT -gt 0 ]] && update_selection $old_selected $selected
|
||||
fi
|
||||
;;
|
||||
ENTER)
|
||||
if [[ $CACHED_COUNT -gt 0 ]]; then
|
||||
local mux_name=$(get_session_field $selected "muxName")
|
||||
if check_alive "$mux_name"; then
|
||||
tput cnorm
|
||||
clear
|
||||
echo -e "${CYAN}Attaching... (Ctrl+B D to detach)${NC}"
|
||||
sleep 0.3
|
||||
tmux attach-session -t "$mux_name"
|
||||
tput civis
|
||||
need_full_redraw=1
|
||||
force_refresh
|
||||
else
|
||||
tput cup $((CACHED_COUNT + 12)) 0
|
||||
echo -e " ${RED}Session is dead${NC} "
|
||||
sleep 1
|
||||
need_full_redraw=1
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
d|x)
|
||||
if [[ $CACHED_COUNT -gt 0 ]]; then
|
||||
local name=$(get_session_field $selected "name")
|
||||
if confirm "Kill '$name'?"; then
|
||||
kill_session $selected
|
||||
need_full_redraw=1
|
||||
else
|
||||
need_full_redraw=1
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
D|X)
|
||||
if [[ $CACHED_COUNT -gt 0 ]]; then
|
||||
if confirm "Kill ALL $CACHED_COUNT sessions?"; then
|
||||
for ((i=CACHED_COUNT-1; i>=0; i--)); do
|
||||
kill_session $i
|
||||
done
|
||||
fi
|
||||
need_full_redraw=1
|
||||
fi
|
||||
;;
|
||||
i)
|
||||
if [[ $CACHED_COUNT -gt 0 ]]; then
|
||||
show_info $selected
|
||||
need_full_redraw=1
|
||||
fi
|
||||
;;
|
||||
q|Q|ESC)
|
||||
clear
|
||||
echo -e "${DIM}Goodbye!${NC}"
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
tput cnorm
|
||||
}
|
||||
|
||||
# Quick list (non-interactive)
|
||||
quick_list() {
|
||||
force_refresh
|
||||
local now=$(date +%s)
|
||||
|
||||
if [[ $CACHED_COUNT -eq 0 ]]; then
|
||||
echo "No claudeman sessions found."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Claudeman Sessions ($SESSIONS_FILE):"
|
||||
echo ""
|
||||
printf "%-4s %-22s %-14s %-10s %-8s\n" "#" "NAME" "RUNNING" "STATUS" "MODE"
|
||||
echo "─────────────────────────────────────────────────────────────────"
|
||||
|
||||
for ((i=0; i<CACHED_COUNT; i++)); do
|
||||
local name=$(echo "$CACHED_JSON" | jq -r ".[$i].name // \"unnamed\"")
|
||||
local mux_name=$(get_session_field $i "muxName")
|
||||
local created_at=$(echo "$CACHED_JSON" | jq -r ".[$i].createdAt")
|
||||
local mode=$(echo "$CACHED_JSON" | jq -r ".[$i].mode")
|
||||
|
||||
local created_s=$((created_at / 1000))
|
||||
local elapsed=$((now - created_s))
|
||||
local running=$(format_duration $elapsed)
|
||||
|
||||
local status="alive"
|
||||
check_alive "$mux_name" || status="dead"
|
||||
|
||||
[[ ${#name} -gt 20 ]] && name="${name:0:17}..."
|
||||
|
||||
printf "%-4s %-22s %-14s %-10s %-8s\n" "[$((i+1))]" "$name" "$running" "$status" "$mode"
|
||||
done
|
||||
}
|
||||
|
||||
# Usage
|
||||
usage() {
|
||||
cat << EOF
|
||||
Claudeman Tmux Session Manager
|
||||
|
||||
Reads from: $SESSIONS_FILE
|
||||
|
||||
Usage: $0 [command]
|
||||
|
||||
Commands:
|
||||
(none) Interactive mode with arrow key navigation
|
||||
list, ls List all sessions
|
||||
attach N Attach to session #N
|
||||
kill N Kill session #N (or N,M or N-M)
|
||||
kill-all Kill all sessions
|
||||
info N Show session #N details
|
||||
help Show this help
|
||||
|
||||
Interactive Controls:
|
||||
↑/↓ or j/k Navigate
|
||||
Enter Attach to selected session
|
||||
d Delete selected session
|
||||
D Delete ALL sessions
|
||||
i Show session info
|
||||
q/Esc Quit
|
||||
|
||||
Examples:
|
||||
$0 # Interactive mode
|
||||
$0 list # List sessions
|
||||
$0 attach 1 # Attach to session 1
|
||||
$0 kill 2,3 # Kill sessions 2 and 3
|
||||
EOF
|
||||
}
|
||||
|
||||
# Main
|
||||
main() {
|
||||
check_dependencies
|
||||
|
||||
case "${1:-}" in
|
||||
"") interactive_menu ;;
|
||||
list|ls) quick_list ;;
|
||||
attach)
|
||||
[[ -z "${2:-}" ]] && { echo "Usage: $0 attach <N>"; exit 1; }
|
||||
force_refresh
|
||||
local mux_name=$(get_session_field $(($2-1)) "muxName")
|
||||
check_alive "$mux_name" && tmux attach-session -t "$mux_name" || echo "Session dead or not found"
|
||||
;;
|
||||
kill)
|
||||
[[ -z "${2:-}" ]] && { echo "Usage: $0 kill <N|N,M|N-M>"; exit 1; }
|
||||
force_refresh
|
||||
if [[ "$2" =~ ^[0-9]+-[0-9]+$ ]]; then
|
||||
local start="${2%-*}" end="${2#*-}"
|
||||
for ((i=end-1; i>=start-1; i--)); do kill_session $i 2>/dev/null; done
|
||||
else
|
||||
IFS=',' read -ra nums <<< "$2"
|
||||
for num in $(echo "${nums[*]}" | tr ' ' '\n' | sort -rn); do
|
||||
kill_session $((num-1)) 2>/dev/null
|
||||
done
|
||||
fi
|
||||
echo "Done"
|
||||
;;
|
||||
kill-all)
|
||||
force_refresh
|
||||
for ((i=CACHED_COUNT-1; i>=0; i--)); do kill_session $i; done
|
||||
echo "All sessions killed"
|
||||
;;
|
||||
info)
|
||||
[[ -z "${2:-}" ]] && { echo "Usage: $0 info <N>"; exit 1; }
|
||||
force_refresh
|
||||
show_info $(($2-1))
|
||||
;;
|
||||
help|--help|-h) usage ;;
|
||||
*) echo "Unknown: $1"; usage; exit 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user