From 629d9c711d05339418876199707ca746535d06eb Mon Sep 17 00:00:00 2001 From: u Date: Thu, 15 Aug 2024 22:31:43 -0400 Subject: automatically install dependencies moved source to src/ --- .gitignore | 1 + Makefile | 17 ++- src/twopane.bash | 424 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ twopane.bash | 424 ------------------------------------------------------- 4 files changed, 439 insertions(+), 427 deletions(-) create mode 100644 .gitignore create mode 100755 src/twopane.bash delete mode 100755 twopane.bash diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..52173d6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +apt-install.stamp diff --git a/Makefile b/Makefile index 847eb68..1668ddc 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,16 @@ sudo != [ $$(id -u) = 0 ] || groups | grep -wo sudo +screen != command -v screen +apt = $(sudo) apt install = $(sudo) install -.PHONY: install -install: - $(install) -v -T -- twopane.bash /usr/local/bin/twopane +ifeq (,$(screen)) +prereqs += screen +endif +.PHONY: install deps +install: deps + $(install) -v -T -- src/twopane.bash /usr/local/bin/twopane +deps: apt-install.stamp +apt-install.stamp: Makefile +ifneq (,$(prereqs)) + $(apt) install --no-upgrade $(prereqs) +endif + touch $@ diff --git a/src/twopane.bash b/src/twopane.bash new file mode 100755 index 0000000..39a6bfb --- /dev/null +++ b/src/twopane.bash @@ -0,0 +1,424 @@ +#!/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=background + 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=twopane.${TWOPANE##*/} + +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_top() +{ + screen -p1 -Q info >/dev/null +} + +kill_top() +{ + while check_top + do + screen -p1 -X kill + done +} + +start_top() +{ + if check_top + 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 -p1 -X exec .!. sh -c 'exec socat UNIX-LISTEN:"$TWOPANE"/socket STDIN,cfmakeraw!!STDOUT' +} + +restart_top() +{ + kill_top + start_top "$@" +} + +socat_connect() +{ + socat STDIN!!STDOUT UNIX-CONNECT:"$TWOPANE"/socket,forever +} + +# 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 | 5 ) + declare -n pid="$1" std0="$2" std1="$3" + shift 3 + ;; + 2 | 4 ) + local pid + declare -n std0="$1" std1="$2" + shift 2 + ;; + 0 ) + local std0 std1 pid + ;; + * ) + 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 "$@" +} + +background() +{ + start_top "$@" + focus bottom + connect stdin stdout +} + +foreground_loop() +{ + while true + do + start_top "$@" + 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_top + 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 "$@" diff --git a/twopane.bash b/twopane.bash deleted file mode 100755 index 39a6bfb..0000000 --- a/twopane.bash +++ /dev/null @@ -1,424 +0,0 @@ -#!/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=background - 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=twopane.${TWOPANE##*/} - -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_top() -{ - screen -p1 -Q info >/dev/null -} - -kill_top() -{ - while check_top - do - screen -p1 -X kill - done -} - -start_top() -{ - if check_top - 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 -p1 -X exec .!. sh -c 'exec socat UNIX-LISTEN:"$TWOPANE"/socket STDIN,cfmakeraw!!STDOUT' -} - -restart_top() -{ - kill_top - start_top "$@" -} - -socat_connect() -{ - socat STDIN!!STDOUT UNIX-CONNECT:"$TWOPANE"/socket,forever -} - -# 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 | 5 ) - declare -n pid="$1" std0="$2" std1="$3" - shift 3 - ;; - 2 | 4 ) - local pid - declare -n std0="$1" std1="$2" - shift 2 - ;; - 0 ) - local std0 std1 pid - ;; - * ) - 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 "$@" -} - -background() -{ - start_top "$@" - focus bottom - connect stdin stdout -} - -foreground_loop() -{ - while true - do - start_top "$@" - 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_top - 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 "$@" -- cgit v1.2.3