#!/bin/bash BASH_ARGV0=twopane.bash echo -n "$0" >/proc/$BASHPID/comm set -e set -f set -o pipefail shopt -s lastpipe . read_chars.bash kill_tty_forward() { read pid < "$TWOPANE"/tty_forward.pid /bin/kill "${1:-'-INT'}" -- -$pid 2>/dev/null } BOT_SIZE=8 BOT_TITLE=input if [ $# = 0 ] then TOP_CMD="$SHELL -i" BOT_CMD=start #TOP_EXIT=quit #TOP_EXIT=prompt TOP_EXIT=restart elif [ "$TWOPANE" -a "$*" = detach ] then kill_tty_forward -TSTP 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() { [ $# -gt 0 ] || set -- COPROC declare -n coproc="$1" shift [ $# -gt 0 ] || set -- "${!coproc}" declare -n std1="${!coproc}_STDOUT" declare -n std0="${!coproc}_STDIN" declare -n opid="${!coproc}_PID" if ! [ "$opid" ] then i "coproc starting: ${!coproc}" local STDERR { coproc "${!coproc}" \ { "$@" } 2>&$STDERR {STDERR}>&- disown "%coproc ${!coproc} " } {STDERR}>&2 2>/dev/null else i "coproc already running: ${!coproc}" 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() { [ $# -gt 0 ] || set -- COPROC declare -n coproc="$1" declare -n std0="${!coproc}_STDIN" declare -n std1="${!coproc}_STDOUT" 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() { connect_coproc TOP_PANE } disconnect() { : "${TOP_PANE_STDIN@A} ${TOP_PANE_STDOUT@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[1]} } send() { [ "${TOP_PANE[0]}" ] || connect printf '%s\n' "$*" >&${TOP_PANE[1]} } restart() { start "$@" } start() { foreground "$@" } 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) x kill_tty_forward -STOP case "$TOP_EXIT" in restart ) # x kill -USR1 $$ x kill -INT $$ # with_screen_pane 1 start_screen_pane "$@" ;; quit ) kill -INT $$ quit ;; prompt | * ) focus bottom ;; esac } connect_sink() { [ "$1" ] || return declare -n fd="$1" declare -n pid="${!fd}_PID" disconnect_sink "$fd" i "sink starting: ${!fd}" exec {fd}> >("${@:2}") pid=$! } disconnect_sink() { [ "$1" ] || return 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() { (exec -a echo_sender socat - fd:${TOP_PANE_STDIN?$0: Internal error}!!-) | tee >(write-tty) >&${TOP_PANE_STDOUT?$0: Internal error} } background() { with_screen_pane 1 start_screen_pane "$@" focus bottom disconnect_sink ECHOSEND connect connect_sink ECHOSEND echo_sender kill %tty_forward 2>/dev/null %tty_forward & disown %tty_forward i 'starting tty_forward' quiet_bg tty_forward >&$ECHOSEND echo $! > "$TWOPANE"/tty_forward.pid } foreground() { while true do background "$@" focus top set -x %tty_forward >/dev/null 2>&1 if [ "$TOP_EXIT" != restart ] || jobs %tty_forward >/dev/null 2>&1 then focus bottom set +x break fi set +x done } twopane() { start "$@" } quit() { screen -X quit } focus() { screen -X focus "$@" } resize() { screen -X resize "$@" } attach() { if jobs %tty_forward >/dev/null 2>&1 then %tty_forward else foreground fi } SIGUSR1() { i "SIGUSR1: ${BASH_COMMAND@A}" } SIGCHLD() { i "SIGCHILD: ${BASH_COMMAND@A}" } prompt_command() { set -- 'unset PROMPT_COMMAND; restart' exec {FINALLY_0}<&0 {FINALLY_1}>&1 {FINALLY_2}>&2 if ! [ "$DEBUG" ] then exec &>/dev/null fi cmd="history -d -1; exec <&$FINALLY_0 >&$FINALLY_1 >&$FINALLY_2; $*" exec <<< "$cmd" } SIGINT() { i INT "${BASH_COMMAND@A}" kill $TOP_PANE_PID $ECHOSEND_PID 2>/dev/null 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 "$@"