#!/bin/bash BASH_ARGV0=twopane.bash echo -n "$0" >/proc/$$/comm set -e set -f set -o pipefail shopt -s lastpipe . read_chars.bash 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 # Enable mouse focus defmousetrack on mousetrack 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 shift 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 .!. bash -c 'exec -a top-pane-tty-forward socat UNIX-LISTEN:"$TWOPANE"/socket STDIN,cfmakeraw!!STDOUT' } restart_screen_pane() { kill_screen_pane start_screen_pane "$@" } connect_coproc() { declare -n coproc="$1" shift declare -g -n std1="${!coproc}_STDOUT" declare -g -n std0="${!coproc}_STDIN" declare -g -n opid="${!coproc}_PID" if [ $# = 0 ] then set -- "${!coproc}" fi if ! [ "$opid" ] then local STDERR { coproc "${!coproc}" \ { "$@" } 2>&$STDERR {STDERR}>&- } {STDERR}>&2 2>/dev/null i "${!opid}=$opid" x disown fi # The copied file descriptors (unlike the original coprocess # file descriptors, in ${coproc[0]} and ${coproc[1]}) can be # passed to external processes (e.g.: socat(1) instances) and # used in subshells. { exec {std0}<&0 {std1}>&1 } <&${coproc[0]} >&${coproc[1]} } disconnect_coproc() { if [ $# = 1 ] then declare -n coproc="$1" declare -g -n std1="${!coproc}_STDOUT" declare -g -n std0="${!coproc}_STDIN" declare -g -n pid="${!coproc}_PID" [ "$std0" -a "$std1" ] || return exec {std0}<&- {std1}>&- unset std0 std1 x kill "$pid" x wait -f "$pid" fi } connect() { connect_coproc SOCAT i "SOCAT_PID=$SOCAT_PID" } disconnect() { i "SOCAT_STDIN=$SOCAT_STDIN SOCAT_STDOUT=$SOCAT_STDOUT SOCAT_PID=$SOCAT_PID" x disconnect_coproc SOCAT } sendc() { [ "${SOCAT[0]}" ] || connect printf '%s' "$*" >&${SOCAT[1]} } send() { [ "${SOCAT[0]}" ] || connect printf '%s\n' "$*" >&${SOCAT[1]} } restart() { start "$@" } start() { foreground "$@" } check_tty_reader() { [ "$TTY_READER_PID" ] || return TTY_READER_JOBSPEC=$(jobs -sl | pid_to_jobspec "$TTY_READER_PID") [ "$TTY_READER_JOBSPEC" ] } pid_to_jobspec() { while read jspec pid status cmd do [ "$pid" = "$1" ] || continue jspec=${jspec%]*} jspec=%${jspec#[} echo $jspec return done false } tty_forward() { declare -i pid="$1" read-tty x kill $pid } SOCAT() { (exec -a bottom-pane-tty-forward socat - UNIX-CONNECT:"$TWOPANE"/socket,forever) case "$TOP_EXIT" in restart ) start_screen_pane 1 "$@" ;; quit ) quit ;; prompt | * ) ;; esac x screen -X quit } echosend() { declare -n fd="$1" declare -n pid="${!fd}_PID" if ! [ "$fd" ] then connect_coproc SOCAT i "SOCAT_PID=$SOCAT_PID" exec {fd}> >( (exec -a echosend socat - fd:${SOCAT_STDIN?e}!!-) | tee >(output_filter) >&${SOCAT_STDOUT?e} ) pid=$! fi } quiet_bg() { local STDERR { eval "$(printf '%q ' "$@") & 2>&\$STDERR {STDERR}>&-" } {STDERR}>&2 2>/dev/null } background() { start_screen_pane 1 "$@" focus bottom echosend ECHOSEND { eval "tty_forward $ECHOSEND_PID &" } >&$ECHOSEND TTY_READER_PID=$! printf '%s\n' \ "#!/bin/bash" \ "kill -TSTP $TTY_READER_PID" \ "screen -X focus bottom" \ > "$TWOPANE"/unforward chmod +x "$TWOPANE"/unforward } foreground() { check_tty_reader || background focus top fg %tty_forward >/dev/null #disconnect focus bottom } foreground_loop() { while true do foreground x kill %tty_forward 2>/dev/null case "$TOP_EXIT" in restart ) x kill -INT $TTY_READER_PID $ECHOSEND_PID start_screen_pane "$@" continue ;; quit ) exit ;; prompt | * ) x kill -INT $TTY_READER_PID $ECHOSEND_PID break ;; esac done } twopane() { start "$@" } quit() { screen -X quit } focus() { screen -X focus "$@" } output_filter() { tokenize | colorize | soft_cursor } 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[106m%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 in the . printf '\\%.3o\n' "'$REPLY" continue fi done } soft_cursor() { FMT=$'%s \b\e[%sm \e[m\b' REPLY= color=101 while printf "$FMT" "$REPLY" "$color" do read -r || break let '++color <= 105' || color=101 done } resize() { screen -X resize "$@" } our_bashrc_main() { set -m set -f set -o pipefail #trap 'focus bottom; resize 90%; read -p "Exit> "; quit' EXIT #trap 'echo USR1 >&2; focus bottom; x kill -INT %tty_forward $TTY_READER_PID $ECHOSEND_PID' USR1 trap 'quit' USR1 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 "$@"