chore: bump version to 0.1538

This commit is contained in:
arkon
2026-02-18 12:51:35 +01:00
parent c548c2d6ee
commit 88422cab57
40 changed files with 369 additions and 3547 deletions
-719
View File
@@ -1,719 +0,0 @@
#!/bin/bash
# ============================================================================
# Claudeman Screens - Mobile-friendly Screen 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:
# screen-chooser # Interactive chooser
# screen-chooser 1 # Quick attach to session 1
# screen-chooser -l # List only (non-interactive)
# screen-chooser -h # Help
#
# Alias (added by installer): alias sc='screen-chooser'
# Then: sc (interactive)
# sc 2 (attach session 2)
#
# ============================================================================
set -e
# ============================================================================
# Configuration
# ============================================================================
CLAUDEMAN_STATE="$HOME/.claudeman/state.json"
CLAUDEMAN_SCREENS="$HOME/.claudeman/screens.json"
# 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 if terminal likely supports nerd fonts
# Termius generally doesn't, so default to ASCII
detect_icons() {
# Check TERM and common indicators
if [[ "$TERM_PROGRAM" == "iTerm"* ]] || \
[[ "$TERM" == "xterm-kitty" ]] || \
[[ -n "$WEZTERM_PANE" ]] || \
[[ "$LC_TERMINAL" == "iTerm2" ]]; then
ICON_SCREEN="󰆍"
ICON_ATTACHED="●"
ICON_DETACHED="○"
ICON_UNKNOWN="◌"
else
# ASCII fallback for Termius/basic terminals
ICON_SCREEN="[S]"
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' # Selection background
# ============================================================================
# Utilities
# ============================================================================
# Truncate string with ellipsis, keeping end if it looks like a path
truncate() {
local str="$1"
local max="$2"
local len=${#str}
if [ "$len" -le "$max" ]; then
echo "$str"
return
fi
# If it's a path-like string, keep the end
if [[ "$str" == *"/"* ]]; then
echo "..${str: -$((max-2))}"
else
echo "${str:0:$((max-1))}…"
fi
}
# Find full session ID from short ID (screen names use first 8 chars)
find_full_session_id() {
local short_id="$1"
# Try state.json first
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
# Try screens.json array
if [ -f "$CLAUDEMAN_SCREENS" ]; then
local full_id
full_id=$(jq -r --arg short "$short_id" '
.[] | select(.sessionId | startswith($short)) | .sessionId
' "$CLAUDEMAN_SCREENS" 2>/dev/null | head -1)
if [ -n "$full_id" ]; then
echo "$full_id"
return
fi
fi
echo "$short_id"
}
# Get session name from Claudeman state or screens.json
# Falls back to working directory basename if name is empty
get_session_name() {
local session_id="$1"
local name=""
local workdir=""
# First, try screens.json (array format) - it has both name and workingDir
if [ -f "$CLAUDEMAN_SCREENS" ]; then
local result
result=$(jq -r --arg id "$session_id" '
.[] | select(.sessionId | startswith($id)) | "\(.name // "")\t\(.workingDir // "")"
' "$CLAUDEMAN_SCREENS" 2>/dev/null | head -1)
if [ -n "$result" ]; then
name="${result%% *}"
workdir="${result#* }"
fi
fi
# If no name yet, try state.json
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 we have a name, use it
if [ -n "$name" ]; then
echo "$name"
return
fi
# Fallback to working directory basename
if [ -n "$workdir" ]; then
echo "${workdir##*/}"
return
fi
# Last resort: use short ID
echo "${session_id:0:8}"
}
# Get working directory from state or screens.json
get_working_dir() {
local session_id="$1"
# Try screens.json first (array format)
if [ -f "$CLAUDEMAN_SCREENS" ]; then
local dir
dir=$(jq -r --arg id "$session_id" '
.[] | select(.sessionId | startswith($id)) | .workingDir // empty
' "$CLAUDEMAN_SCREENS" 2>/dev/null | head -1)
if [ -n "$dir" ] && [ "$dir" != "null" ]; then
echo "${dir/#$HOME/~}"
return
fi
fi
# Try state.json
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 token count from state
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 from screens.json (array format)
get_respawn_status() {
local session_id="$1"
if [ -f "$CLAUDEMAN_SCREENS" ]; then
local respawn_enabled
respawn_enabled=$(jq -r --arg id "$session_id" '
.[] | select(.sessionId | startswith($id)) | .respawnConfig.enabled // false
' "$CLAUDEMAN_SCREENS" 2>/dev/null | head -1)
if [ "$respawn_enabled" = "true" ]; then
echo "R" # Respawn active
return
fi
fi
echo ""
}
# Check if jq is available
check_deps() {
if ! command -v jq &>/dev/null; then
echo -e "${YELLOW}Note: Install jq for session names${R}"
echo ""
fi
}
# ============================================================================
# Screen List Parser
# ============================================================================
declare -a SCREEN_PIDS
declare -a SCREEN_NAMES
declare -a SCREEN_STATES
declare -a SESSION_IDS
declare -a DISPLAY_NAMES
declare -a WORKING_DIRS
declare -a TOKEN_COUNTS
declare -a RESPAWN_STATUS
parse_screens() {
SCREEN_PIDS=()
SCREEN_NAMES=()
SCREEN_STATES=()
SESSION_IDS=()
DISPLAY_NAMES=()
WORKING_DIRS=()
TOKEN_COUNTS=()
RESPAWN_STATUS=()
local i=0
# Regex for screen -ls lines: " 12345.claudeman-abc123 (date) (Attached)"
# We need to capture: PID, name, and the LAST parenthesized group (state)
# Modern screen -ls shows: PID.name (date) (Attached|Detached)
local screen_regex='^[[:space:]]*([0-9]+)\.([^[:space:]]+).*\((Attached|Detached|Multi)\)'
# Parse screen -ls output
while IFS= read -r line; do
if [[ "$line" =~ $screen_regex ]]; then
local pid="${BASH_REMATCH[1]}"
local name="${BASH_REMATCH[2]}"
local state="${BASH_REMATCH[3]}"
SCREEN_PIDS+=("$pid")
SCREEN_NAMES+=("$name")
SCREEN_STATES+=("$state")
# Extract session ID from claudeman screen name
local session_id=""
local cm_regex='^claudeman-(.+)$'
if [[ "$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+=("$name")
WORKING_DIRS+=("")
TOKEN_COUNTS+=("")
RESPAWN_STATUS+=("")
fi
i=$((i + 1))
fi
done < <(screen -ls 2>/dev/null || true)
}
# ============================================================================
# Display Functions
# ============================================================================
clear_screen() {
printf '\033[2J\033[H'
}
# Print header
print_header() {
local count=${#SCREEN_PIDS[@]}
echo -e "${B}${CYAN}Claudeman Screens${R} ${D}($count)${R}"
echo -e "${D}$(printf '%.0s─' {1..32})${R}"
}
# Print a session entry
print_entry() {
local idx="$1"
local num=$((idx + 1))
local name="${DISPLAY_NAMES[$idx]}"
local state="${SCREEN_STATES[$idx]}"
local dir="${WORKING_DIRS[$idx]}"
local tokens="${TOKEN_COUNTS[$idx]}"
local respawn="${RESPAWN_STATUS[$idx]}"
# Truncate name (leave room for indicators)
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)
# Status indicator and color
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
# Build the line
# Format: "1) name ○ R 45k"
local num_str="${B}${WHITE}${num})${R}"
local name_str="${B}${WHITE}${name}${R}"
local status_str="${status_color}${status_icon}${R}"
# Respawn indicator (green R if active)
local respawn_str=""
if [ -n "$respawn" ]; then
respawn_str=" ${GREEN}${respawn}${R}"
fi
# Token display
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}"
# Show directory on second line if present (dimmed, indented)
if [ -n "$dir" ]; then
dir=$(truncate "$dir" $((MAX_NAME_LEN - 2)))
echo -e " ${D}${dir}${R}"
fi
}
# Print footer with commands
print_footer() {
local page="$1"
local total_pages="$2"
echo ""
echo -e "${D}────────────────────────────────${R}"
# Pagination indicator
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
# Commands - most used first, thumb-friendly
echo -e " ${GRAY}[${WHITE}1-9${GRAY}]attach [${WHITE}r${GRAY}]efresh [${WHITE}q${GRAY}]uit${R}"
}
# Print no screens message
print_no_screens() {
clear_screen
echo -e "${B}${CYAN}Claudeman Screens${R}"
echo -e "${D}$(printf '%.0s─' {1..32})${R}"
echo ""
echo -e " ${YELLOW}No screen 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_screens
local count=${#SCREEN_PIDS[@]}
if [ "$count" -eq 0 ]; then
print_no_screens
return
fi
local total_pages=$(( (count + PAGE_SIZE - 1) / PAGE_SIZE ))
# Clamp page
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 to a screen
attach_screen() {
local idx="$1"
local pid="${SCREEN_PIDS[$idx]}"
local name="${SCREEN_NAMES[$idx]}"
local state="${SCREEN_STATES[$idx]}"
if [ -z "$pid" ]; then
return 1
fi
clear_screen
echo -e "${GREEN}Attaching to ${B}${DISPLAY_NAMES[$idx]}${R}${GREEN}...${R}"
echo -e "${D}(Ctrl+A D to detach)${R}"
sleep 0.3
# IMPORTANT: Claudeman attaches to screen via PTY with -x for streaming.
# Using -rd here would detach Claudeman and break terminal output.
#
# For mobile access without squeezing the desktop view:
# - Use the web UI from mobile Safari (responsive, works on iPhone)
# - The web UI at http://yourserver:3000 works great on mobile browsers
#
# If you attach via screen -x, the terminal will resize to the smallest
# attached client (your mobile). Use Ctrl+A D to detach when done.
if [[ "$state" == "Attached" ]] || [[ "$state" == "Multi" ]]; then
screen -x "$pid.$name"
else
screen -r "$pid.$name"
fi
# After detach, return to chooser
return 0
}
# ============================================================================
# Input Handler
# ============================================================================
handle_input() {
local key="$1"
local count=${#SCREEN_PIDS[@]}
local total_pages=$(( (count + PAGE_SIZE - 1) / PAGE_SIZE ))
case "$key" in
# Number selection (1-9)
[1-9])
local idx=$((key - 1))
if [ "$idx" -lt "$count" ]; then
attach_screen "$idx"
return 0
fi
;;
# Escape sequence (arrow keys)
$'\e')
# Read the rest of the escape sequence
read -rsn2 -t 0.1 seq 2>/dev/null || true
case "$seq" in
'[A'|'[D') # Up or Left arrow
if [ "$total_pages" -gt 1 ]; then
current_page=$(( (current_page - 1 + total_pages) % total_pages ))
fi
;;
'[B'|'[C') # Down or Right arrow
if [ "$total_pages" -gt 1 ]; then
current_page=$(( (current_page + 1) % total_pages ))
fi
;;
esac
;;
# Navigation (vim-style and standard)
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
;;
# Refresh
r|R)
# Just re-render
;;
# Quit
q|Q)
clear_screen
exit 0
;;
# Enter with no selection - attach first if only one
'')
if [ "$count" -eq 1 ]; then
attach_screen 0
return 0
fi
;;
esac
return 0
}
# ============================================================================
# List Mode (non-interactive, for scripting)
# ============================================================================
list_mode() {
parse_screens
local count=${#SCREEN_PIDS[@]}
if [ "$count" -eq 0 ]; then
echo "No screen sessions"
exit 0
fi
for ((i = 0; i < count; i++)); do
local num=$((i + 1))
local name="${DISPLAY_NAMES[$i]}"
local state="${SCREEN_STATES[$i]}"
local respawn="${RESPAWN_STATUS[$i]}"
local indicator="-"
[[ "$state" == *"Attached"* ]] && indicator="*"
[ -n "$respawn" ] && indicator="${indicator}R"
echo "$num) $name [$indicator]"
done
}
# ============================================================================
# Quick Attach (by number)
# ============================================================================
quick_attach() {
local num="$1"
parse_screens
local count=${#SCREEN_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_screen "$idx"
}
# ============================================================================
# Help
# ============================================================================
show_help() {
cat << 'EOF'
Claudeman Screens - Mobile-friendly Screen 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 screen: Ctrl+A D
- Session names from Claudeman state
- Optimized for Termius/iPhone
EOF
}
# ============================================================================
# Main
# ============================================================================
main() {
# Argument parsing
case "${1:-}" in
-h|--help)
show_help
exit 0
;;
-l|--list)
list_mode
exit 0
;;
[1-9]|[1-9][0-9])
# Quick attach by number
quick_attach "$1"
exit $?
;;
esac
# Check dependencies
check_deps
# Initial render
render
# Input loop
while true; do
# Read single character without enter
# -s: silent, -n1: one char, -t: timeout for auto-refresh
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
# Re-render after input or timeout (auto-refresh)
render
done
}
# Handle Ctrl+C gracefully
trap 'clear_screen; exit 0' INT
# Run
main "$@"
-494
View File
@@ -1,494 +0,0 @@
#!/bin/bash
#
# Claudeman Screen Session Manager
# Interactive tool with arrow key navigation
# Reads from ~/.claudeman/screens.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/screen-manager.ts)
SCREENS_FILE="${HOME}/.claudeman/screens.json"
# 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 screen &> /dev/null; then
echo -e "${RED}Error: 'screen' is required. Install with: sudo apt install screen${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 "$SCREENS_FILE" ]]; then
CACHED_JSON=$(cat "$SCREENS_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
echo "$CACHED_JSON" | jq -r ".[$index].$field // \"unknown\""
}
# 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 screen alive (cached per draw cycle)
declare -A ALIVE_CACHE
check_alive() {
local screen_name=$1
if [[ -z "${ALIVE_CACHE[$screen_name]+x}" ]]; then
if screen -ls 2>/dev/null | grep -q "$screen_name"; then
ALIVE_CACHE[$screen_name]=1
else
ALIVE_CACHE[$screen_name]=0
fi
fi
[[ "${ALIVE_CACHE[$screen_name]}" -eq 1 ]]
}
clear_alive_cache() {
ALIVE_CACHE=()
}
# Kill session by index
kill_session() {
local idx=$1
local screen_name=$(get_session_field $idx "screenName")
local pid=$(get_session_field $idx "pid")
pkill -TERM -P $pid 2>/dev/null
kill -TERM -$pid 2>/dev/null
screen -S "$screen_name" -X quit 2>/dev/null
kill -KILL $pid 2>/dev/null
# Remove from JSON
CACHED_JSON=$(echo "$CACHED_JSON" | jq "del(.[$idx])")
echo "$CACHED_JSON" > "$SCREENS_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 Screen 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 screen_name=$(echo "$CACHED_JSON" | jq -r ".[$i].screenName")
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 "$screen_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: $SCREENS_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 screen_name=$(get_session_field $idx "screenName")
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}Screen:${NC} $screen_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 "$screen_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 screen_name=$(get_session_field $selected "screenName")
if check_alive "$screen_name"; then
tput cnorm
clear
echo -e "${CYAN}Attaching... (Ctrl+A D to detach)${NC}"
sleep 0.3
screen -r "$screen_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 ($SCREENS_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 screen_name=$(echo "$CACHED_JSON" | jq -r ".[$i].screenName")
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 "$screen_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 Screen Session Manager
Reads from: $SCREENS_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 screen_name=$(get_session_field $(($2-1)) "screenName")
check_alive "$screen_name" && screen -r "$screen_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 "$@"
-4
View File
@@ -32,10 +32,6 @@ set -e
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
-4
View File
@@ -20,10 +20,6 @@ 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=""