#!/bin/bash BASH_ARGV0=twopane.bash echo -n "$0" >/proc/$BASHPID/comm set -e set -f set -o pipefail shopt -s lastpipe PS4='+ \t$LINENO\t ' . read_chars.bash . finally.bash kill_tty_forward() { read pid < "$TWOPANE"/tty_forward.pid /bin/kill "${1:-'-INT'}" -- -$pid 2>/dev/null } BOT_SIZE=8 BOT_TITLE=input DO_RESTART= DO_START= DO_START=y DO_RESTART=y if [ $# = 0 ] then TOP_CMD="$SHELL -i" if [ "$DO_RESTART" ] then SIG=INT BOT_CMD=PROMPT_COMMAND=prompt_command TOP_EXIT=restart elif [ "$DO_START" ] then BOT_CMD=start TOP_EXIT=quit else BOT_CMD=PROMPT_COMMAND=prompt_command TOP_EXIT=prompt fi elif [ "$TWOPANE" -a "$*" = detach ] then kill_tty_forward -TSTP exit elif [ "$TWOPANE" -a "$*" = focus ] then screen -X focus exit elif [ "$TWOPANE" -a "$*" = quit ] then screen -X quit exit 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 -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 . with_screen_pane() { (( $# > 1 )) || return declare -i PANE="$1" LOCK (( PANE > 0 )) || return ( flock $LOCK "$2" "$1" "${@:3}" ) {LOCK}>"$TWOPANE"/pane$PANE } check_screen_pane() { (( PANE > 0 )) || return screen -p "$PANE" -Q info >/dev/null } kill_screen_pane() { (( PANE > 0 )) || return while check_screen_pane "$PANE" do screen -p "$PANE" -X kill done } start_screen_pane() { (( PANE > 0 )) || return if check_screen_pane "$PANE" then i "pane already running: $PANE" return else i "starting pane: $PANE" 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 "$@" start_screen_pane_subprocess } start_screen_pane_subprocess() { (( PANE > 0 )) || return subproc_command=( exec -a top-pane-tty-forward socat UNIX-LISTEN:"$TWOPANE"/socket STDIN,cfmakeraw,opost=1,onlcr=1!!STDOUT ) screen -p "$PANE" -X exec .!. bash -c "${subproc_command[*]}" } restart_pane() { (( PANE > 0 )) || return kill_screen_pane start_screen_pane "$@" } restart_top_pane() { with_screen_pane 1 restart_pane "$@" } # connect_coproc() -- start a coprocess and connect it to copied file # descriptors named {NAME}_IN and {NAME}_OUT where {NAME} is the # supplied first argument or default "COPROC". {NAME} is the name of # the created bash coprocess. # The copied file descriptors (unlike the original coprocess # file descriptors, in ${NAME[0]} and ${NAME[1]}) can be # passed to external processes (e.g.: socat(1) instances) and # used in subshells. # The created coprocess is silently disowned from job control before # bash can make noise about it. connect_coproc() { [ $# -gt 0 ] || set -- COPROC declare -n coproc="$1" shift [ $# -gt 0 ] || set -- "${!coproc}" declare -n std0="${!coproc}_IN" declare -n std1="${!coproc}_OUT" declare -n std2="${!coproc}_ERR" declare -n pid="${!coproc}_PID" if ! [ "$pid" ] then i "coproc starting: ${!coproc}" { coproc "${!coproc}" \ { "$@" } 2>&$std2 {std2}>&- unset std2 disown "%coproc ${!coproc} " } {std2}>&2 2>/dev/null else i "coproc already running: ${!coproc}" fi { exec {std0}<&0 {std1}>&1 } <&${coproc[0]} >&${coproc[1]} } disconnect_coproc() { [ $# -gt 0 ] || set -- COPROC declare -n coproc="$1" declare -n std0="${!coproc}_IN" declare -n std1="${!coproc}_OUT" declare -n pid="${!coproc}_PID" if [ "$std0" -o "$std1" ] then exec {std0}<&- {std1}>&- fi if [ "$pid" ] then kill "$pid" 2>/dev/null wait -f "$pid" 2>/dev/null fi unset coproc std0 std1 pid } connect() { if ! [ "$TOP_PANE_PID" ] then disconnect_sink ECHOSEND connect_coproc TOP_PANE fi connect_sink ECHOSEND echo_sender } disconnect() { : "${TOP_PANE_IN@A} ${TOP_PANE_OUT@A} ${TOP_PANE_PID@A} ${TOP_PANE[@]@A}" disconnect_coproc TOP_PANE : "${ECHOSEND@A} ${ECHOSEND_PID@A}" disconnect_sink ECHOSEND } sendc() { [ "${TOP_PANE[0]}" ] || connect printf '%s' "$*" >&${TOP_PANE_OUT?Internal error} } send() { [ "${TOP_PANE[0]}" ] || connect printf '%s\n' "$*" >&${TOP_PANE_OUT?Internal error} } tty_forward() { printf '%d\n' "$BASHPID" > "$TWOPANE"/tty_forward.pid trap 'kill -INT $$' INT read-tty } TOP_PANE() { (exec -a bottom-pane-tty-forward socat - UNIX-CONNECT:"$TWOPANE"/socket,forever) case "$TOP_EXIT" in restart ) x kill_tty_forward -STOP x kill -INT $$ ;; quit ) quit ;; prompt | * ) x kill_tty_forward -STOP focus bottom ;; esac } connect_sink() { (( $# > 0 )) || set -- SINK declare -n fd="$1" declare -n pid="${!fd}_PID" if [ "$pid" ] then i "sink already running: ${!fd}" return fi i "sink starting: ${!fd}" exec {fd}> >("${@:2}") pid=$! } disconnect_sink() { (( $# > 0 )) || set -- SINK declare -n fd="$1" [ "$fd" ] || return declare -n pid="${!fd}_PID" i "sink stopping: ${!fd}" exec {fd}>&- kill $pid 2>/dev/null unset fd pid } quiet_bg() { local STDERR { eval "$(printf '%q ' "$@") & 2>&\$STDERR {STDERR}>&-" } {STDERR}>&2 2>/dev/null } # echo_sender() reads from two inputs and writes to two outputs. # # Read from both: # read from stdin # read from $TOP_PANE_IN # Write to both: # write to stdout # write to $TOP_PANE_OUT # I.e.: # read from /dev/tty # read from the socat-coproc-forwarded /dev/tty in the top pane. # write to /dev/tty # write to the socat-coproc-forwarded /dev/tty in the top pane. echo_sender() { (exec -a echo_sender socat - fd:${TOP_PANE_IN?$0: Internal error}!!-) | tee >(write-tty) >&${TOP_PANE_OUT?$0: Internal error} } restart_tty_forward() { detach with_screen_pane 1 start_screen_pane "$@" focus bottom connect i 'starting tty_forward' quiet_bg tty_forward >&$ECHOSEND echo $! > "$TWOPANE"/tty_forward.pid } attach() { if ! jobs %tty_forward &>/dev/null then restart_tty_forward "$@" fi focus top { %tty_forward; } &>/dev/null focus bottom } detach() { { kill -INT %tty_forward disown %tty_forward } &>/dev/null } restart() { attach "$@"; } start() { attach "$@"; } foreground() { attach "$@"; } twopane() { attach "$@"; } forward() { attach "$@"; } quit() { screen -X quit } focus() { screen -X focus "$@" } resize() { screen -X resize "$@" } SIGUSR1() { i "SIGUSR1: ${BASH_COMMAND@A}" } SIGCHLD() { i "SIGCHILD: ${BASH_COMMAND@A}" } prompt_command() { [ "$TOP_EXIT" = 'restart' ] || return 0 if [ "$SIG" = INT ] then finally 'unset SIG; start' detach return fi local job if ! { job=$(jobs %tty_forward 2>/dev/null) && [ "$job" ]; } then finally 'start' : return fi read _ jobstatus _ <<< "${job?Internal error: line $LINENO}" case "${jobstatus?Internal error: line $LINENO}" in Running ) jobs -x finally 'attach' kill -CONT %tty_forward ;; Terminated | Interrupt ) finally 'start' : ;; Stopped ) ;; esac } SIGINT() { i INT "${BASH_COMMAND@A}" kill $TOP_PANE_PID $ECHOSEND_PID 2>/dev/null SIG=INT PROMPT_COMMAND=prompt_command } our_bashrc_main() { BASH_ARGV0=twopane echo -n "$0" >/proc/$BASHPID/comm set -m set -f set -o pipefail trap 'SIGUSR1' USR1 # trap 'SIGCHLD' CHLD trap 'SIGINT' INT trap '! [ "$DEBUG" ] || read -p "Exit> "; quit' EXIT export PS1="$BOT_TITLE\\\$ " } save_file bashrc <<. ${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 "$@"