Version 1.0.6 terminalphone.sh

This commit is contained in:
Here ForAwhile 2026-02-19 16:57:26 +00:00
parent bcae3f6b37
commit 00df2f6b57

View file

@ -9,7 +9,7 @@ set -euo pipefail
# CONFIGURATION # CONFIGURATION
#============================================================================= #=============================================================================
APP_NAME="TerminalPhone" APP_NAME="TerminalPhone"
VERSION="1.0.5" VERSION="1.0.6"
BASE_DIR="$(dirname "$(readlink -f "$0")")" BASE_DIR="$(dirname "$(readlink -f "$0")")"
DATA_DIR="$BASE_DIR/.terminalphone" DATA_DIR="$BASE_DIR/.terminalphone"
TOR_DIR="$DATA_DIR/tor_data" TOR_DIR="$DATA_DIR/tor_data"
@ -24,6 +24,8 @@ CONNECTED_FLAG="$DATA_DIR/run/connected_$$"
RECV_PIPE="$DATA_DIR/run/recv_$$" RECV_PIPE="$DATA_DIR/run/recv_$$"
SEND_PIPE="$DATA_DIR/run/send_$$" SEND_PIPE="$DATA_DIR/run/send_$$"
CIPHER_RUNTIME_FILE="$DATA_DIR/run/cipher_$$" CIPHER_RUNTIME_FILE="$DATA_DIR/run/cipher_$$"
AUTO_LISTEN_FLAG="$DATA_DIR/run/autolisten_$$"
AUTO_LISTEN_PID=""
# Defaults # Defaults
@ -36,6 +38,7 @@ PTT_KEY=" " # spacebar
CHUNK_DURATION=1 # seconds per audio chunk CHUNK_DURATION=1 # seconds per audio chunk
CIPHER="aes-256-cbc" # OpenSSL cipher for encryption CIPHER="aes-256-cbc" # OpenSSL cipher for encryption
SNOWFLAKE_ENABLED=0 # Snowflake bridge (off by default) SNOWFLAKE_ENABLED=0 # Snowflake bridge (off by default)
AUTO_LISTEN=0 # Auto-listen after Tor starts (off by default)
# ANSI Colors # ANSI Colors
RED='\033[0;31m' RED='\033[0;31m'
@ -160,6 +163,7 @@ OPUS_FRAMESIZE=$OPUS_FRAMESIZE
PTT_KEY="$PTT_KEY" PTT_KEY="$PTT_KEY"
CIPHER="$CIPHER" CIPHER="$CIPHER"
SNOWFLAKE_ENABLED=$SNOWFLAKE_ENABLED SNOWFLAKE_ENABLED=$SNOWFLAKE_ENABLED
AUTO_LISTEN=$AUTO_LISTEN
EOF EOF
} }
@ -419,13 +423,15 @@ start_tor() {
} }
stop_tor() { stop_tor() {
stop_auto_listener
if [ -n "$TOR_PID" ] && kill -0 "$TOR_PID" 2>/dev/null; then if [ -n "$TOR_PID" ] && kill -0 "$TOR_PID" 2>/dev/null; then
kill "$TOR_PID" 2>/dev/null || true kill "$TOR_PID" 2>/dev/null || true
wait "$TOR_PID" 2>/dev/null || true wait "$TOR_PID" 2>/dev/null || true
TOR_PID="" TOR_PID=""
log_ok "Tor stopped" log_ok "Tor stopped"
else
log_info "Tor is not running"
fi fi
} }
get_onion() { get_onion() {
@ -581,7 +587,6 @@ stop_and_send() {
# Encode → encrypt → send # Encode → encrypt → send
if [ -s "$raw_file" ]; then if [ -s "$raw_file" ]; then
echo -ne "\r ${DIM}Sending...${NC} " >&2
opusenc --raw --raw-rate "$SAMPLE_RATE" --raw-chan 1 \ opusenc --raw --raw-rate "$SAMPLE_RATE" --raw-chan 1 \
--bitrate "$OPUS_BITRATE" --framesize "$OPUS_FRAMESIZE" \ --bitrate "$OPUS_BITRATE" --framesize "$OPUS_FRAMESIZE" \
--speech --quiet \ --speech --quiet \
@ -590,9 +595,16 @@ stop_and_send() {
if [ -s "$opus_file" ]; then if [ -s "$opus_file" ]; then
encrypt_file "$opus_file" "$enc_file" 2>/dev/null encrypt_file "$opus_file" "$enc_file" 2>/dev/null
if [ -s "$enc_file" ]; then if [ -s "$enc_file" ]; then
local enc_size
enc_size=$(stat -c%s "$enc_file" 2>/dev/null || echo 0)
local size_kb=$(( enc_size * 10 / 1024 ))
local size_whole=$(( size_kb / 10 ))
local size_frac=$(( size_kb % 10 ))
local b64 local b64
b64=$(base64 -w 0 "$enc_file" 2>/dev/null) b64=$(base64 -w 0 "$enc_file" 2>/dev/null)
echo "AUDIO:${b64}" >&4 2>/dev/null || true echo "AUDIO:${b64}" >&4 2>/dev/null || true
LAST_SENT_INFO="${size_whole}.${size_frac}KB"
fi fi
fi fi
fi fi
@ -695,8 +707,55 @@ cleanup_call() {
CALL_ACTIVE=0 CALL_ACTIVE=0
REC_PID="" REC_PID=""
} }
#=============================================================================
# AUTO-LISTEN (BACKGROUND LISTENER)
#=============================================================================
# Start listening for incoming calls start_auto_listener() {
# Only start if auto-listen is enabled and Tor is running
if [ "$AUTO_LISTEN" -ne 1 ]; then return 0; fi
if [ -z "$SHARED_SECRET" ]; then return 0; fi
if [ -z "$TOR_PID" ] || ! kill -0 "$TOR_PID" 2>/dev/null; then return 0; fi
# Stop any existing listener first
stop_auto_listener
mkdir -p "$AUDIO_DIR" "$DATA_DIR/run"
rm -f "$RECV_PIPE" "$SEND_PIPE" "$AUTO_LISTEN_FLAG"
mkfifo "$RECV_PIPE" "$SEND_PIPE"
socat "TCP-LISTEN:$LISTEN_PORT,reuseaddr" \
"SYSTEM:touch $AUTO_LISTEN_FLAG; cat $SEND_PIPE & cat > $RECV_PIPE" &
AUTO_LISTEN_PID=$!
save_pid "socat" "$AUTO_LISTEN_PID"
}
stop_auto_listener() {
if [ -n "$AUTO_LISTEN_PID" ]; then
kill "$AUTO_LISTEN_PID" 2>/dev/null || true
kill -9 "$AUTO_LISTEN_PID" 2>/dev/null || true
AUTO_LISTEN_PID=""
fi
rm -f "$AUTO_LISTEN_FLAG" "$RECV_PIPE" "$SEND_PIPE"
}
# Check if an incoming call arrived on the background listener
check_auto_listen() {
if [ -f "$AUTO_LISTEN_FLAG" ]; then
rm -f "$AUTO_LISTEN_FLAG"
touch "$CONNECTED_FLAG"
echo -e "\n ${GREEN}${BOLD}Incoming call detected!${NC}" >&2
sleep 0.5
in_call_session "$RECV_PIPE" "$SEND_PIPE" ""
cleanup_call
# Restart listener for next call
start_auto_listener
return 0
fi
return 1
}
# Start listening for incoming calls (manual, blocking)
listen_for_call() { listen_for_call() {
if [ -z "$SHARED_SECRET" ]; then if [ -z "$SHARED_SECRET" ]; then
log_err "No shared secret set! Use option 4 first." log_err "No shared secret set! Use option 4 first."
@ -705,6 +764,9 @@ listen_for_call() {
start_tor || return 1 start_tor || return 1
# Stop auto-listener if running (we'll do manual listen)
stop_auto_listener
local onion local onion
onion=$(get_onion) onion=$(get_onion)
echo -e "\n${BOLD}${CYAN}═══ Listening for Calls ═══${NC}\n" echo -e "\n${BOLD}${CYAN}═══ Listening for Calls ═══${NC}\n"
@ -714,29 +776,25 @@ listen_for_call() {
echo -e " ${DIM}Press Ctrl+C to stop listening.${NC}\n" echo -e " ${DIM}Press Ctrl+C to stop listening.${NC}\n"
mkdir -p "$AUDIO_DIR" mkdir -p "$AUDIO_DIR"
# Use socat to accept a TCP connection, then handle it
log_info "Waiting for incoming connection..." log_info "Waiting for incoming connection..."
# Create named pipes for bidirectional communication
rm -f "$RECV_PIPE" "$SEND_PIPE" rm -f "$RECV_PIPE" "$SEND_PIPE"
mkfifo "$RECV_PIPE" "$SEND_PIPE" mkfifo "$RECV_PIPE" "$SEND_PIPE"
# Flag file that socat will create when a connection arrives
local incoming_flag="$DATA_DIR/run/incoming_$$" local incoming_flag="$DATA_DIR/run/incoming_$$"
rm -f "$incoming_flag" rm -f "$incoming_flag"
# Start socat in background — it touches the flag when someone connects
socat "TCP-LISTEN:$LISTEN_PORT,reuseaddr" \ socat "TCP-LISTEN:$LISTEN_PORT,reuseaddr" \
"SYSTEM:touch $incoming_flag; cat $SEND_PIPE & cat > $RECV_PIPE" & "SYSTEM:touch $incoming_flag; cat $SEND_PIPE & cat > $RECV_PIPE" &
local socat_pid=$! local socat_pid=$!
save_pid "socat" "$socat_pid" save_pid "socat" "$socat_pid"
# Wait for an incoming connection (poll for the flag file)
while [ ! -f "$incoming_flag" ]; do while [ ! -f "$incoming_flag" ]; do
if ! kill -0 "$socat_pid" 2>/dev/null; then if ! kill -0 "$socat_pid" 2>/dev/null; then
log_err "Listener stopped unexpectedly" log_err "Listener stopped unexpectedly"
rm -f "$RECV_PIPE" "$SEND_PIPE" "$incoming_flag" rm -f "$RECV_PIPE" "$SEND_PIPE" "$incoming_flag"
# Restart auto-listener if enabled
start_auto_listener
return 1 return 1
fi fi
sleep 0.5 sleep 0.5
@ -745,9 +803,10 @@ listen_for_call() {
touch "$CONNECTED_FLAG" touch "$CONNECTED_FLAG"
log_ok "Call connected!" log_ok "Call connected!"
in_call_session "$RECV_PIPE" "$SEND_PIPE" "" in_call_session "$RECV_PIPE" "$SEND_PIPE" ""
# Full cleanup after call ends
cleanup_call cleanup_call
# Restart auto-listener if enabled
start_auto_listener
} }
# Call a remote .onion address # Call a remote .onion address
@ -788,13 +847,29 @@ call_remote() {
local socat_pid=$! local socat_pid=$!
save_pid "socat_call" "$socat_pid" save_pid "socat_call" "$socat_pid"
# Animated connecting indicator while socat establishes connection
(
local dots=""
while true; do
for dots in "." ".." "..." " "; do
echo -ne "\r ${CYAN}${BOLD}Connecting${dots}${NC} " >&2
sleep 0.3
done
done
) &
local spinner_pid=$!
# Give socat a moment to connect # Give socat a moment to connect
sleep 2 sleep 2
if kill -0 "$socat_pid" 2>/dev/null; then if kill -0 "$socat_pid" 2>/dev/null; then
log_ok "Connected to ${remote_onion}!" in_call_session "$RECV_PIPE" "$SEND_PIPE" "$remote_onion" "$spinner_pid"
in_call_session "$RECV_PIPE" "$SEND_PIPE" "$remote_onion"
else else
# Kill spinner and show error
kill "$spinner_pid" 2>/dev/null || true
wait "$spinner_pid" 2>/dev/null || true
echo -ne "\r " >&2
echo "" >&2
log_err "Failed to connect. Check the address and ensure Tor is running." log_err "Failed to connect. Check the address and ensure Tor is running."
fi fi
@ -811,13 +886,18 @@ draw_call_header() {
local _remote="${1:-}" local _remote="${1:-}"
local _rcipher="${2:-}" local _rcipher="${2:-}"
clear >&2 clear >&2
# Row counter for ANSI cursor positioning (clear sets cursor to row 1)
local _r=1
if [ -n "$_remote" ]; then if [ -n "$_remote" ]; then
echo -e "\n${BOLD}${BG_GREEN}${WHITE} CALL CONNECTED ${NC} ${CYAN}${_remote}${NC}\n" >&2 echo -e "\n${BOLD}${BG_GREEN}${WHITE} CALL CONNECTED ${NC} ${CYAN}${_remote}${NC}\n" >&2
else else
echo -e "\n${BOLD}${BG_GREEN}${WHITE} CALL CONNECTED ${NC}\n" >&2 echo -e "\n${BOLD}${BG_GREEN}${WHITE} CALL CONNECTED ${NC}\n" >&2
fi fi
_r=4 # \n(row1) + header(row2) + \n(row3) + echo-newline → cursor at row 4
# Show cipher info — always show both local and remote # Cipher info
CIPHER_ROW=$_r
local cipher_upper="${CIPHER^^}" local cipher_upper="${CIPHER^^}"
if [ -n "$_rcipher" ]; then if [ -n "$_rcipher" ]; then
local rcipher_upper="${_rcipher^^}" local rcipher_upper="${_rcipher^^}"
@ -832,14 +912,14 @@ draw_call_header() {
echo -e " ${GREEN}${NC} Local cipher: ${WHITE}${cipher_upper}${NC}" >&2 echo -e " ${GREEN}${NC} Local cipher: ${WHITE}${cipher_upper}${NC}" >&2
echo -e " ${DIM}${NC} Remote cipher: ${DIM}waiting...${NC}" >&2 echo -e " ${DIM}${NC} Remote cipher: ${DIM}waiting...${NC}" >&2
fi fi
_r=$((_r + 2))
# Show Snowflake bridge info if enabled # Snowflake bridge info
if [ "$SNOWFLAKE_ENABLED" -eq 1 ]; then if [ "$SNOWFLAKE_ENABLED" -eq 1 ]; then
local tor_log="$TOR_DIR/tor.log" local tor_log="$TOR_DIR/tor.log"
echo "" >&2 echo "" >&2; _r=$((_r + 1))
echo -e " ${TOR_PURPLE}${NC} ${BOLD}Snowflake bridge${NC}" >&2 echo -e " ${TOR_PURPLE}${NC} ${BOLD}Snowflake bridge${NC}" >&2; _r=$((_r + 1))
if [ -f "$tor_log" ]; then if [ -f "$tor_log" ]; then
# Parse bridge descriptor: "new bridge descriptor 'NAME' (fresh): $FINGERPRINT"
local bridge_line="" local bridge_line=""
bridge_line=$(grep "new bridge descriptor" "$tor_log" 2>/dev/null | tail -1 || true) bridge_line=$(grep "new bridge descriptor" "$tor_log" 2>/dev/null | tail -1 || true)
if [ -n "$bridge_line" ]; then if [ -n "$bridge_line" ]; then
@ -848,15 +928,14 @@ draw_call_header() {
local bridge_fp="" local bridge_fp=""
bridge_fp=$(echo "$bridge_line" | sed -n 's/.*(\(fresh\|stale\)): \(.*\)/\2/p' || true) bridge_fp=$(echo "$bridge_line" | sed -n 's/.*(\(fresh\|stale\)): \(.*\)/\2/p' || true)
if [ -n "$bridge_name" ]; then if [ -n "$bridge_name" ]; then
# Truncate fingerprint to fit terminal
local fp_display="$bridge_fp" local fp_display="$bridge_fp"
if [ ${#fp_display} -gt 40 ]; then if [ ${#fp_display} -gt 40 ]; then
fp_display="${fp_display:0:40}..." fp_display="${fp_display:0:40}..."
fi fi
echo -e " ${DIM}descriptor:${NC} ${WHITE}${bridge_name}${NC} ${DIM}${fp_display}${NC}" >&2 echo -e " ${DIM}descriptor:${NC} ${WHITE}${bridge_name}${NC} ${DIM}${fp_display}${NC}" >&2
_r=$((_r + 1))
fi fi
fi fi
# Show managed proxy status
local proxy_line="" local proxy_line=""
proxy_line=$(grep 'Managed proxy.*snowflake' "$tor_log" 2>/dev/null | tail -1 || true) proxy_line=$(grep 'Managed proxy.*snowflake' "$tor_log" 2>/dev/null | tail -1 || true)
if [ -n "$proxy_line" ]; then if [ -n "$proxy_line" ]; then
@ -865,23 +944,40 @@ draw_call_header() {
else else
echo -e " ${DIM}transport:${NC} ${YELLOW}connecting...${NC}" >&2 echo -e " ${DIM}transport:${NC} ${YELLOW}connecting...${NC}" >&2
fi fi
_r=$((_r + 1))
fi fi
fi fi
fi fi
echo "" >&2 echo "" >&2; _r=$((_r + 1))
echo -e " ${BOLD}Controls:${NC}" >&2 # Static placeholders — updated in-place via ANSI positioning
echo -e " ${GREEN}[SPACE]${NC} -- Push-to-Talk" >&2 echo -e " ${DIM}Last sent: --${NC}" >&2
echo -e " ${CYAN}[T]${NC} -- Send text message" >&2 SENT_INFO_ROW=$_r
echo -e " ${YELLOW}[S]${NC} -- Settings" >&2 _r=$((_r + 1))
echo -e " ${RED}[Q]${NC} -- Hang up" >&2
echo -e "" >&2 echo -e " ${DIM}Last recv: --${NC}" >&2
RECV_INFO_ROW=$_r
_r=$((_r + 1))
echo "" >&2; _r=$((_r + 1))
# Static status bar
if [ $IS_TERMUX -eq 1 ]; then
echo -ne " ${GREEN}${BOLD} Ready ${NC} ${DIM}[SPACE]=Talk [T]=Chat [S]=Settings [Q]=Hang up${NC} " >&2
else
echo -ne " ${GREEN}${BOLD} Ready ${NC} ${DIM}[SPACE]=Hold to Talk [T]=Chat [S]=Settings [Q]=Hang up${NC} " >&2
fi
STATUS_ROW=$_r
echo "" >&2
echo "" >&2
} }
in_call_session() { in_call_session() {
local recv_pipe="$1" local recv_pipe="$1"
local send_pipe="$2" local send_pipe="$2"
local known_remote="${3:-}" local known_remote="${3:-}"
local spinner_pid="${4:-}"
CALL_ACTIVE=1 CALL_ACTIVE=1
rm -f "$PTT_FLAG" rm -f "$PTT_FLAG"
@ -938,7 +1034,11 @@ in_call_session() {
echo "$remote_cipher" > "$remote_cipher_file" echo "$remote_cipher" > "$remote_cipher_file"
fi fi
# Draw call header # Kill connecting spinner and draw call header
if [ -n "$spinner_pid" ]; then
kill "$spinner_pid" 2>/dev/null || true
wait "$spinner_pid" 2>/dev/null || true
fi
draw_call_header "$remote_display" "$remote_cipher" draw_call_header "$remote_display" "$remote_cipher"
# Start receive handler in background # Start receive handler in background
@ -950,13 +1050,16 @@ in_call_session() {
if read -r line <&3 2>/dev/null; then if read -r line <&3 2>/dev/null; then
case "$line" in case "$line" in
PTT_START) PTT_START)
echo -ne "\r ${BG_GREEN}${WHITE}${BOLD} REMOTE TALKING ${NC} " printf '\033[s' >&2
printf '\033[%d;1H\033[K' "$STATUS_ROW" >&2
printf ' \033[42;1;37m REMOTE TALKING \033[0m ' >&2
printf '\033[u' >&2
;; ;;
PTT_STOP) PTT_STOP)
echo -ne "\r ${DIM} Remote idle ${NC} " # silent — Last recv row provides feedback
;; ;;
PING) PING)
echo -ne "\r ${DIM} Ping received ${NC} " # silent — no display update
;; ;;
ID:*) ID:*)
# Caller ID received (save but don't print — already in header) # Caller ID received (save but don't print — already in header)
@ -980,7 +1083,7 @@ in_call_session() {
_dot_color="$RED" _dot_color="$RED"
fi fi
printf '\033[s' >&2 printf '\033[s' >&2
printf '\033[4;1H\033[K' >&2 printf '\033[%d;1H\033[K' "$CIPHER_ROW" >&2
printf ' %b●%b Local cipher: %b%s%b\r\n' "$_dot_color" "$NC" "$WHITE" "$_cu" "$NC" >&2 printf ' %b●%b Local cipher: %b%s%b\r\n' "$_dot_color" "$NC" "$WHITE" "$_cu" "$NC" >&2
printf '\033[K' >&2 printf '\033[K' >&2
printf ' %b●%b Remote cipher: %b%s%b' "$_dot_color" "$NC" "$WHITE" "$_ru" "$NC" >&2 printf ' %b●%b Remote cipher: %b%s%b' "$_dot_color" "$NC" "$WHITE" "$_ru" "$NC" >&2
@ -997,7 +1100,7 @@ in_call_session() {
if decrypt_file "$msg_enc" "$msg_dec" 2>/dev/null; then if decrypt_file "$msg_enc" "$msg_dec" 2>/dev/null; then
local msg_text local msg_text
msg_text=$(cat "$msg_dec" 2>/dev/null) msg_text=$(cat "$msg_dec" 2>/dev/null)
echo -e "\r\n ${MAGENTA}${BOLD}[MSG]${NC} ${WHITE}${msg_text}${NC}" >&2 echo -e "\n ${MAGENTA}${BOLD}[MSG]${NC} ${WHITE}${msg_text}${NC}" >&2
fi fi
fi fi
rm -f "$msg_enc" "$msg_dec" 2>/dev/null rm -f "$msg_enc" "$msg_dec" 2>/dev/null
@ -1012,6 +1115,19 @@ in_call_session() {
echo "$b64_data" | base64 -d > "$enc_file" 2>/dev/null || true echo "$b64_data" | base64 -d > "$enc_file" 2>/dev/null || true
if [ -s "$enc_file" ]; then if [ -s "$enc_file" ]; then
if decrypt_file "$enc_file" "$dec_file" 2>/dev/null; then if decrypt_file "$enc_file" "$dec_file" 2>/dev/null; then
# Calculate recv size
local _enc_sz=0
_enc_sz=$(stat -c%s "$enc_file" 2>/dev/null || echo 0)
local _sz_kb=$(( _enc_sz * 10 / 1024 ))
local _sz_w=$(( _sz_kb / 10 ))
local _sz_f=$(( _sz_kb % 10 ))
local _recv_info="${_sz_w}.${_sz_f}KB"
# Update static "Last recv" row via ANSI positioning
printf '\033[s' >&2
printf '\033[%d;1H\033[K' "$RECV_INFO_ROW" >&2
printf ' \033[2mLast recv: \033[0m\033[1;37m%s\033[0m' "$_recv_info" >&2
printf '\033[u' >&2
play_chunk "$dec_file" 2>/dev/null || true play_chunk "$dec_file" 2>/dev/null || true
fi fi
fi fi
@ -1041,36 +1157,44 @@ in_call_session() {
REC_PID="" REC_PID=""
REC_FILE="" REC_FILE=""
LAST_SENT_INFO=""
local ptt_active=0 local ptt_active=0
if [ $IS_TERMUX -eq 1 ]; then # Status bar is already drawn by draw_call_header
echo -ne "\r ${GREEN}${BOLD} Ready ${NC} ${DIM}[SPACE]=Talk [T]=Chat [S]=Settings [Q]=Hang up${NC} " >&2
else
echo -ne "\r ${GREEN}${BOLD} Ready ${NC} ${DIM}[SPACE]=Hold to Talk [T]=Chat [S]=Settings [Q]=Hang up${NC} " >&2
fi
while [ -f "$CONNECTED_FLAG" ]; do while [ -f "$CONNECTED_FLAG" ]; do
local key="" local key=""
key=$(dd bs=1 count=1 2>/dev/null) || true key=$(dd bs=1 count=1 2>/dev/null) || true
if [ "$key" = " " ]; then if [ "$key" = "$PTT_KEY" ]; then
if [ $IS_TERMUX -eq 1 ]; then if [ $IS_TERMUX -eq 1 ]; then
# TERMUX: Toggle mode # TERMUX: Toggle mode
if [ $ptt_active -eq 0 ]; then if [ $ptt_active -eq 0 ]; then
ptt_active=1 ptt_active=1
echo -ne "\r ${BG_RED}${WHITE}${BOLD} ● RECORDING ${NC} ${DIM}[SPACE]=Send${NC} " >&2 printf '\033[s' >&2; printf '\033[%d;1H\033[K' "$STATUS_ROW" >&2
printf ' \033[41;1;37m \u25cf RECORDING \033[0m \033[2m[SPACE]=Send\033[0m ' >&2
printf '\033[u' >&2
start_recording start_recording
else else
ptt_active=0 ptt_active=0
stop_and_send stop_and_send
echo "PTT_STOP" >&4 2>/dev/null || true echo "PTT_STOP" >&4 2>/dev/null || true
echo -ne "\r ${GREEN}${BOLD} Sent! ${NC} ${DIM}[SPACE]=Talk [T]=Chat [S]=Settings [Q]=Hang up${NC} " >&2 # Update Last sent + status bar
printf '\033[s' >&2
printf '\033[%d;1H\033[K' "$SENT_INFO_ROW" >&2
printf ' \033[2mLast sent: \033[0m\033[1;37m%s\033[0m' "$LAST_SENT_INFO" >&2
printf '\033[%d;1H\033[K' "$STATUS_ROW" >&2
printf ' \033[1;32m Sent! \033[0m \033[2m[SPACE]=Talk [T]=Chat [S]=Settings [Q]=Hang up\033[0m ' >&2
printf '\033[u' >&2
fi fi
else else
# LINUX: Hold-to-talk # LINUX: Hold-to-talk
if [ $ptt_active -eq 0 ]; then if [ $ptt_active -eq 0 ]; then
ptt_active=1 ptt_active=1
echo -ne "\r ${BG_RED}${WHITE}${BOLD}${BLINK} ● RECORDING ${NC} " >&2 printf '\033[s' >&2; printf '\033[%d;1H\033[K' "$STATUS_ROW" >&2
printf ' \033[41;1;37;5m \u25cf RECORDING \033[0m ' >&2
printf '\033[u' >&2
stty time 5 # longer timeout to span keyboard repeat delay
start_recording start_recording
fi fi
fi fi
@ -1095,17 +1219,24 @@ in_call_session() {
elif [ -z "$key" ]; then elif [ -z "$key" ]; then
# No key pressed (timeout) — on Linux, release = stop and send # No key pressed (timeout) — on Linux, release = stop and send
if [ $IS_TERMUX -eq 0 ] && [ $ptt_active -eq 1 ]; then if [ $IS_TERMUX -eq 0 ] && [ $ptt_active -eq 1 ]; then
stty time 1 # restore fast timeout for key detection
ptt_active=0 ptt_active=0
stop_and_send stop_and_send
echo "PTT_STOP" >&4 2>/dev/null || true echo "PTT_STOP" >&4 2>/dev/null || true
echo -ne "\r ${GREEN}${BOLD} Sent! ${NC} ${DIM}[SPACE]=Hold to Talk [T]=Chat [S]=Settings [Q]=Hang up${NC} " >&2 # Update Last sent + status bar
printf '\033[s' >&2
printf '\033[%d;1H\033[K' "$SENT_INFO_ROW" >&2
printf ' \033[2mLast sent: \033[0m\033[1;37m%s\033[0m' "$LAST_SENT_INFO" >&2
printf '\033[%d;1H\033[K' "$STATUS_ROW" >&2
printf ' \033[1;32m Sent! \033[0m \033[2m[SPACE]=Hold to Talk [T]=Chat [S]=Settings [Q]=Hang up\033[0m ' >&2
printf '\033[u' >&2
fi fi
elif [ "$key" = "t" ] || [ "$key" = "T" ]; then elif [ "$key" = "t" ] || [ "$key" = "T" ]; then
# Text chat mode # Text chat mode
# Switch to cooked mode for text input # Switch to cooked mode for text input
stty "$ORIGINAL_STTY" 2>/dev/null || stty sane stty "$ORIGINAL_STTY" 2>/dev/null || stty sane
echo -e "\r " >&2 echo "" >&2
echo -ne " ${CYAN}${BOLD}MSG>${NC} " >&2 echo -ne " ${CYAN}${BOLD}MSG>${NC} " >&2
local chat_msg="" local chat_msg=""
read -r chat_msg read -r chat_msg
@ -1126,12 +1257,15 @@ in_call_session() {
fi fi
# Switch back to raw mode for PTT # Switch back to raw mode for PTT
stty raw -echo -icanon min 0 time 1 stty raw -echo -icanon min 0 time 1
echo "" >&2 # Restore status bar to Ready
printf '\033[s' >&2
printf '\033[%d;1H\033[K' "$STATUS_ROW" >&2
if [ $IS_TERMUX -eq 1 ]; then if [ $IS_TERMUX -eq 1 ]; then
echo -ne " ${GREEN}${BOLD} Ready ${NC} ${DIM}[SPACE]=Talk [T]=Chat [S]=Settings [Q]=Hang up${NC} " >&2 printf ' \033[1;32m Ready \033[0m \033[2m[SPACE]=Talk [T]=Chat [S]=Settings [Q]=Hang up\033[0m ' >&2
else else
echo -ne " ${GREEN}${BOLD} Ready ${NC} ${DIM}[SPACE]=Hold to Talk [T]=Chat [S]=Settings [Q]=Hang up${NC} " >&2 printf ' \033[1;32m Ready \033[0m \033[2m[SPACE]=Hold to Talk [T]=Chat [S]=Settings [Q]=Hang up\033[0m ' >&2
fi fi
printf '\033[u' >&2
elif [ "$key" = "s" ] || [ "$key" = "S" ]; then elif [ "$key" = "s" ] || [ "$key" = "S" ]; then
# Mid-call settings # Mid-call settings
@ -1146,11 +1280,15 @@ in_call_session() {
[ -f "$remote_cipher_file" ] && _rc=$(cat "$remote_cipher_file" 2>/dev/null) [ -f "$remote_cipher_file" ] && _rc=$(cat "$remote_cipher_file" 2>/dev/null)
draw_call_header "$_rd" "$_rc" draw_call_header "$_rd" "$_rc"
stty raw -echo -icanon min 0 time 1 stty raw -echo -icanon min 0 time 1
# Restore status bar to Ready (header was redrawn, STATUS_ROW is fresh)
printf '\033[s' >&2
printf '\033[%d;1H\033[K' "$STATUS_ROW" >&2
if [ $IS_TERMUX -eq 1 ]; then if [ $IS_TERMUX -eq 1 ]; then
echo -ne " ${GREEN}${BOLD} Ready ${NC} ${DIM}[SPACE]=Talk [T]=Chat [S]=Settings [Q]=Hang up${NC} " >&2 printf ' \033[1;32m Ready \033[0m \033[2m[SPACE]=Talk [T]=Chat [S]=Settings [Q]=Hang up\033[0m ' >&2
else else
echo -ne " ${GREEN}${BOLD} Ready ${NC} ${DIM}[SPACE]=Hold to Talk [T]=Chat [S]=Settings [Q]=Hang up${NC} " >&2 printf ' \033[1;32m Ready \033[0m \033[2m[SPACE]=Hold to Talk [T]=Chat [S]=Settings [Q]=Hang up\033[0m ' >&2
fi fi
printf '\033[u' >&2
fi fi
done done
@ -1320,11 +1458,23 @@ settings_menu() {
if [ "$SNOWFLAKE_ENABLED" -eq 1 ]; then if [ "$SNOWFLAKE_ENABLED" -eq 1 ]; then
sf_label="${GREEN}enabled${NC}" sf_label="${GREEN}enabled${NC}"
fi fi
echo -e " ${DIM}Snowflake bridge: ${NC}${sf_label}\n" echo -e " ${DIM}Snowflake bridge: ${NC}${sf_label}"
local al_label="${RED}disabled${NC}"
if [ "$AUTO_LISTEN" -eq 1 ]; then
al_label="${GREEN}enabled${NC}"
fi
echo -e " ${DIM}Auto-listen: ${NC}${al_label}"
local ptt_display="SPACE"
[ "$PTT_KEY" != " " ] && ptt_display="$PTT_KEY"
echo -e " ${DIM}PTT key: ${NC}${WHITE}${ptt_display}${NC}\n"
echo -e " ${BOLD}${WHITE}1${NC} ${CYAN}${NC} Change encryption cipher" echo -e " ${BOLD}${WHITE}1${NC} ${CYAN}${NC} Change encryption cipher"
echo -e " ${BOLD}${WHITE}2${NC} ${CYAN}${NC} Change Opus encoding quality" echo -e " ${BOLD}${WHITE}2${NC} ${CYAN}${NC} Change Opus encoding quality"
echo -e " ${BOLD}${WHITE}3${NC} ${CYAN}${NC} Snowflake bridge (censorship circumvention)" echo -e " ${BOLD}${WHITE}3${NC} ${CYAN}${NC} Snowflake bridge (censorship circumvention)"
echo -e " ${BOLD}${WHITE}4${NC} ${CYAN}${NC} Auto-listen (listen for calls automatically once Tor starts)"
echo -e " ${BOLD}${WHITE}5${NC} ${CYAN}${NC} Change PTT (push-to-talk) key"
echo -e " ${BOLD}${WHITE}0${NC} ${CYAN}${NC} ${DIM}Back to main menu${NC}" echo -e " ${BOLD}${WHITE}0${NC} ${CYAN}${NC} ${DIM}Back to main menu${NC}"
echo "" echo ""
echo -ne " ${BOLD}Select: ${NC}" echo -ne " ${BOLD}Select: ${NC}"
@ -1334,6 +1484,40 @@ settings_menu() {
1) settings_cipher ;; 1) settings_cipher ;;
2) settings_opus ;; 2) settings_opus ;;
3) settings_snowflake ;; 3) settings_snowflake ;;
4)
if [ "$AUTO_LISTEN" -eq 1 ]; then
AUTO_LISTEN=0
stop_auto_listener
log_ok "Auto-listen disabled"
else
AUTO_LISTEN=1
log_ok "Auto-listen enabled"
start_auto_listener
fi
save_config
sleep 1
;;
5)
local _pd="SPACE"
[ "$PTT_KEY" != " " ] && _pd="$PTT_KEY"
echo -e "\n ${DIM}Current PTT key: ${NC}${WHITE}${_pd}${NC}"
echo -ne " ${BOLD}Press the key you want to use for PTT: ${NC}"
# Read a single character in raw mode
local _old_stty
_old_stty=$(stty -g)
stty raw -echo
local _newkey
_newkey=$(dd bs=1 count=1 2>/dev/null) || true
stty "$_old_stty"
if [ -n "$_newkey" ]; then
PTT_KEY="$_newkey"
save_config
local _nd="SPACE"
[ "$PTT_KEY" != " " ] && _nd="$PTT_KEY"
log_ok "PTT key set to: ${_nd}"
fi
sleep 1
;;
0|q|Q) return ;; 0|q|Q) return ;;
*) *)
echo -e "\n ${RED}Invalid choice${NC}" echo -e "\n ${RED}Invalid choice${NC}"
@ -1613,8 +1797,14 @@ main_menu() {
if [ "$SNOWFLAKE_ENABLED" -eq 1 ]; then if [ "$SNOWFLAKE_ENABLED" -eq 1 ]; then
sf_status="${GREEN}${NC}" sf_status="${GREEN}${NC}"
fi fi
local al_status="${RED}${NC}"
if [ "$AUTO_LISTEN" -eq 1 ]; then
al_status="${GREEN}${NC}"
fi
local _ptt_d="SPACE"
[ "$PTT_KEY" != " " ] && _ptt_d="$PTT_KEY"
echo -e " ${DIM}Tor:${NC} $tor_status ${DIM}Secret:${NC} $secret_status ${DIM}SF:${NC} $sf_status ${DIM}PTT:${NC} ${GREEN}[SPACE]${NC}\n" echo -e " ${DIM}Tor:${NC} $tor_status ${DIM}Secret:${NC} $secret_status ${DIM}SF:${NC} $sf_status ${DIM}AL:${NC} $al_status ${DIM}PTT:${NC} ${GREEN}[${_ptt_d}]${NC}\n"
echo -e " ${BOLD}${WHITE}1${NC} ${CYAN}${NC} Listen for calls" echo -e " ${BOLD}${WHITE}1${NC} ${CYAN}${NC} Listen for calls"
echo -e " ${BOLD}${WHITE}2${NC} ${CYAN}${NC} Call an onion address" echo -e " ${BOLD}${WHITE}2${NC} ${CYAN}${NC} Call an onion address"
@ -1632,7 +1822,26 @@ main_menu() {
echo "" echo ""
echo -ne " ${BOLD}Select: ${NC}" echo -ne " ${BOLD}Select: ${NC}"
read -r choice # If auto-listen is active, poll for incoming calls without redrawing
local choice=""
if [ "$AUTO_LISTEN" -eq 1 ] && [ -n "$AUTO_LISTEN_PID" ]; then
echo -ne "${DIM}[Auto-listening...]${NC} " >&2
while true; do
# Check for incoming call
if check_auto_listen; then
choice=""
break
fi
# Try to read user input with short timeout
if read -r -t 1 choice 2>/dev/null; then
break # user typed something
fi
done
else
read -r choice
fi
[ -z "$choice" ] && continue
case "$choice" in case "$choice" in
1) listen_for_call ;; 1) listen_for_call ;;
@ -1663,6 +1872,7 @@ main_menu() {
;; ;;
8) 8)
start_tor start_tor
start_auto_listener
echo -ne "\n ${DIM}Press Enter to continue...${NC}" echo -ne "\n ${DIM}Press Enter to continue...${NC}"
read -r read -r
;; ;;
@ -1674,6 +1884,7 @@ main_menu() {
10) 10)
stop_tor stop_tor
start_tor start_tor
start_auto_listener
echo -ne "\n ${DIM}Press Enter to continue...${NC}" echo -ne "\n ${DIM}Press Enter to continue...${NC}"
read -r read -r
;; ;;