#!/bin/bash BASH_ARGV0=twopane.bash echo -n "$0" >/proc/$$/comm set -e set -f set -o pipefail shopt -s lastpipe BOT_SIZE=8 BOT_TITLE=input if [ $# = 0 ] then TOP_CMD="$SHELL -i" BOT_CMD=foreground TOP_EXIT=prompt #BOT_CMD=start #TOP_EXIT=restart else TOP_CMD=$* BOT_CMD=start TOP_EXIT=quit fi TOP_TITLE="Command: $TOP_CMD" TWOPANE=$(mktemp -d) export TWOPANE TOP_CMD BOT_CMD TOP_TITLE BOT_TITLE BOT_SIZE trap 'rm -r "$TWOPANE"' EXIT # STY=$(systemd-escape -p "$TWOPANE") systemd_escape_path() { declare -g -n path="$1" result="$2" result=$(realpath "$path") result=${result//-/--} result=${result//\//-} } systemd_escape_path TWOPANE STY STY=$0.$$.$STY save_file() { cat > "$TWOPANE"/"${1:?$0: Error: filename cannot be empty string}" } save_screenrc() { save_file screenrc"${1:+.$1}" } save_screenrc <<'.' # Disable keybindings. unbindall escape \0\0 # Disable messages. This is needed for screen -X/-Q to work reasonably. msgwait 0 msgminwait 0 # Try to disable blocking the terminal on ^S. # Doesn't seem to work here. # More is needed. Xterm? nonblock on caption string '%t' layout new split focus bottom resize $BOT_SIZE screen -ln -t "$BOT_TITLE" 0 bash --noprofile --rcfile "$TWOPANE"/bashrc -i layout save 0 . check_screen_pane() { screen -p "$pane" -Q info >/dev/null } kill_screen_pane() { while check_screen_pane do screen -p "$pane" -X kill done } start_screen_pane() { declare -g -a PANES declare -i pane="${1:-1}" (( pane > 0 )) || return PANES+=("$pane") if check_screen_pane "$pane" then return fi if [ $# = 0 ] then set -f set -- ${TOP_CMD:-bash -i} fi TOP_TITLE="Command: ${*@Q}" screen -X focus top screen -X screen -ln -t "$TOP_TITLE" 1 "$@" screen -p "$pane" -X exec .!. sh -c 'exec socat UNIX-LISTEN:"$TWOPANE"/socket STDIN,cfmakeraw!!STDOUT' } restart_screen_pane() { kill_screen_pane start_screen_pane "$@" } # Start SOCAT if necessary. # Connect the running SOCAT to file descriptors. # Optionally assign the file descriptors to the specified variables. # Optionally assign the socat PID to the specified variable. # # The copied file descriptors (unlike the original coprocess file # descriptors, in ${SOCAT[0]} and ${SOCAT[1]}) can be passed to external # processes (e.g.: other socat(1) instances) and used in subshells. connect() { case $# in 3 ) declare -n pid="$1" std0="$2" std1="$3" shift 3 ;; 2 ) local pid declare -n std0="$1" std1="$2" shift 2 ;; 0 ) local pid std0 std1 ;; * ) return 1 ;; esac if ! [ "${SOCAT[0]}" ] then local STDERR exec {STDERR}>&2 { coproc SOCAT { socat_connect; } 2>&$STDERR } 2>/dev/null eval "exec $STDERR>&-" fi pid=${SOCAT_PID} exec {std0}<&${SOCAT[0]} {std1}>&${SOCAT[1]} disown } sendc() { [ "${SOCAT[0]}" ] || connect printf '%s' "$*" >&${SOCAT[1]} } send() { [ "${SOCAT[0]}" ] || connect printf '%s\n' "$*" >&${SOCAT[1]} } disconnect() { if [ $# = 2 ] then declare -i -n std0="$1" std1="$2" eval "exec $std0<&- $std1>&-" unset std0 std1 fi wait -f "$SOCAT_PID" 2>/dev/null } restart() { start "$@" } start() { foreground "$@" } foreground_loop() { while true do start_screen_pane "$@" connect stdin stdout forward disconnect stdin stdout case "$TOP_EXIT" in restart ) continue ;; quit ) exit ;; prompt | * ) focus bottom break ;; esac done } forwarding() { [ "$FORWARD_PID" ] || return FORWARD_JOBSPEC=$(jobs -sl | pid_to_jobspec "$FORWARD_PID") [ "$FORWARD_JOBSPEC" ] } forward() { declare -g FORWARD_PID if ! check_screen_pane then echo "$0: Warning: Nothing to forward. Starting anew." >&2 background "$@" elif forwarding then resume_forward return fi focus top old_stty=$(stty -g) # Lowercase $stdin/$stdout are the SOCAT coprocess connected to # the other pane's terminal. Uppercase $STDIN/$STDOUT are the # real stdin/stdout of this function, connected to the lower # pane's terminal. Socat here merges inputs from both sources. exec {STDIN}<&0 {STDOUT}>&1 {STDERR}>&2 # The input is put out raw back over the socket. The input is # copied to stdout after being filtered (to display control # characters with carrot-encoding like '^[' etc). exec {TEE}> >(tee >(output_filter >&$STDOUT) >&$stdout) stty=cfmakeraw,opost=1,onlcr=1 { socat FD:$STDIN,$stty!!STDOUT - <&$stdin >&$TEE 2>&$STDERR & } 2>/dev/null FORWARD_PID=$! printf '%s\n' "#!/bin/bash" "kill $!" "screen -X focus bottom" > "$TWOPANE"/unforward chmod +x "$TWOPANE"/unforward fg >/dev/null stty "$old_stty" focus bottom echo } cfmakeraw() { cmd=(stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8) "${cmd[@]}" "$@" } pid_to_jobspec() { while read jspec pid status cmd do [ "$pid" = "$1" ] || continue jspec=${jspec%]*} jspec=%${jspec#[} echo $jspec return done false } resume_forward() { old_stty=$(stty -g) cfmakeraw opost onlcr focus top fg "$FORWARD_JOBSPEC" >/dev/null stty "$old_stty" focus bottom } foreground() { if forwarding then resume_forward else foreground_loop "$@" fi } twopane() { start "$@" } quit() { screen -X quit } focus() { screen -X focus "$@" } output_filter() { tokenize | colorize | soft_cursor } # if (show_nonprinting) { # while (true) # { # if (ch >= 32) { # if (ch < 127) *bpout++ = ch; # else if (ch == 127) {*bpout++ = '^'; *bpout++ = '?';} # else { # *bpout++ = 'M'; # *bpout++ = '-'; # if (ch >= 128 + 32) { # if (ch < 128 + 127) *bpout++ = ch - 128; # else {*bpout++ = '^'; *bpout++ = '?';}} # else {*bpout++ = '^'; *bpout++ = ch - 128 + 64;}}} # else if (ch == '\t' && !show_tabs) *bpout++ = '\t'; # else if (ch == '\n') {newlines = -1; break;} # else {*bpout++ = '^'; *bpout++ = ch + 64;} # ch = *bpin++;}} chr() { declare -i n="$*" printf "\\$(printf %o "$n")" } colorize() { while read -r do case "$REPLY" in \\[0-7][0-7][0-7] ) ;; * ) printf '%s\n' "$REPLY" continue ;; esac declare -i c=8#"${REPLY#?}" if (( c > 128 + 127 )) then : elif (( c > 128 + 32 )) then printf -v REPLY "M-$(chr c - 128)" elif (( c > 127 )) then printf -v REPLY "M-^$(chr c - 128 + 64)" elif (( c < 32 )) then printf -v REPLY "^$(chr c + 64)" fi printf $'\e[1m%s\e[m\n' "$REPLY" done } tokenize() { while read -r -N1 do if [[ "$REPLY" =~ [[:print:]] ]] then # Output one printable character per line. It may be a # multibyte unicode character. printf '%s\n' "$REPLY" continue else # If it is a non-printable, then we output a # multi-character line. In this case we colorize it # later so that it won't be confused with multiple # printable characters. printf '\\%.3o\n' "'$REPLY" continue fi done } soft_cursor() { FMT=$'%s \b\e[%sm \e[m\b' REPLY= while printf "$FMT" "$REPLY" $(( 101 + RANDOM % 7 )) do read -r || break done } our_bashrc_main() { set -f set -o pipefail trap "screen -X quit" EXIT export PS1="$BOT_TITLE\\\$ " } save_file bashrc <<. BASH_ARGV0=twopane echo -n "\$0" >/proc/\$\$/comm ${TOP_CMD@A} ${BOT_CMD@A} ${TOP_TITLE@A} ${BOT_TITLE@A} ${TOP_EXIT@A} $(declare -f) our_bashrc_main $BOT_CMD . main() { screen -c "$TWOPANE"/screenrc -m -S "$STY" -ln } main "$@"