summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile2
-rwxr-xr-xsrc/finally.bash51
-rwxr-xr-xsrc/twopane.bash220
3 files changed, 192 insertions, 81 deletions
diff --git a/Makefile b/Makefile
index cda8018..472af80 100644
--- a/Makefile
+++ b/Makefile
@@ -10,7 +10,7 @@ ifeq (,$(socat))
10prereqs += socat 10prereqs += socat
11endif 11endif
12.PHONY: install deps 12.PHONY: install deps
13executables = read-tty write-tty read_chars.bash 13executables = read-tty write-tty read_chars.bash finally.bash
14executables_sources = $(addprefix src/,$(executables)) 14executables_sources = $(addprefix src/,$(executables))
15install: deps 15install: deps
16 $(install) -v -T -- src/twopane.bash /usr/local/bin/twopane 16 $(install) -v -T -- src/twopane.bash /usr/local/bin/twopane
diff --git a/src/finally.bash b/src/finally.bash
new file mode 100755
index 0000000..7700c98
--- /dev/null
+++ b/src/finally.bash
@@ -0,0 +1,51 @@
1#!/bin/bash
2#
3# Usage:
4#
5# finally <eval-string> <cmd> [args...]
6#
7# What it does:
8#
9# (1) First, the <cmd> is executed with its arguments if any.
10#
11# (2) Then <eval-string> is executed as it would be by eval in
12# the most global scope of the shell. (It is actually sent to
13# the bash prompt's standard input as a list of quoted words.)
14#
15# Reason to exist:
16#
17# If execution of <cmd> is interrupted by signal, the
18# <eval-string> will still run.
19
20finally()
21{
22 exec \
23 {FINALLY_0}<&0 \
24 {FINALLY_1}>&1 \
25 {FINALLY_2}>&2 \
26 0< <(finally_unwind "$1") \
27 &> /dev/null # silence prompt
28 # note that finally_unwind still has un-redirected
29 # stdin/stderr
30 {
31 "${@:2}"
32 } <&$FINALLY_0 >&$FINALLY_1 2>&$FINALLY_2
33}
34
35finally_unwind()
36{
37 case $# in
38 0 )
39 exec \
40 0<&$FINALLY_0 \
41 1>&$FINALLY_1 \
42 2>&$FINALLY_2 \
43 {FINALLY_0}<&- \
44 {FINALLY_1}>&- \
45 {FINALLY_2}>&-
46 ;;
47 1 )
48 echo "history -d -1; finally_unwind; $1"
49 ;;
50 esac
51}
diff --git a/src/twopane.bash b/src/twopane.bash
index 327d246..c266828 100755
--- a/src/twopane.bash
+++ b/src/twopane.bash
@@ -6,7 +6,10 @@ set -f
6set -o pipefail 6set -o pipefail
7shopt -s lastpipe 7shopt -s lastpipe
8 8
9PS4='+ \t$LINENO\t '
10
9. read_chars.bash 11. read_chars.bash
12. finally.bash
10 13
11kill_tty_forward() 14kill_tty_forward()
12{ 15{
@@ -17,17 +20,39 @@ kill_tty_forward()
17BOT_SIZE=8 20BOT_SIZE=8
18BOT_TITLE=input 21BOT_TITLE=input
19 22
23DO_RESTART=y
24DO_RESTART=
25DO_START=
26DO_START=y
27
20if [ $# = 0 ] 28if [ $# = 0 ]
21then 29then
22 TOP_CMD="$SHELL -i" 30 TOP_CMD="$SHELL -i"
23 BOT_CMD=start 31 if [ "$DO_RESTART" ]
24 #TOP_EXIT=quit 32 then
25 #TOP_EXIT=prompt 33 SIG=INT
26 TOP_EXIT=restart 34 BOT_CMD=PROMPT_COMMAND=prompt_command
35 TOP_EXIT=restart
36 elif [ "$DO_START" ]
37 then
38 BOT_CMD=start
39 TOP_EXIT=quit
40 else
41 BOT_CMD=PROMPT_COMMAND=prompt_command
42 TOP_EXIT=prompt
43 fi
27elif [ "$TWOPANE" -a "$*" = detach ] 44elif [ "$TWOPANE" -a "$*" = detach ]
28then 45then
29 kill_tty_forward -TSTP 46 kill_tty_forward -TSTP
30 exit 47 exit
48elif [ "$TWOPANE" -a "$*" = focus ]
49then
50 screen -X focus
51 exit
52elif [ "$TWOPANE" -a "$*" = quit ]
53then
54 screen -X quit
55 exit
31else 56else
32 TOP_CMD=$* 57 TOP_CMD=$*
33 BOT_CMD=start 58 BOT_CMD=start
@@ -156,33 +181,42 @@ restart_top_pane()
156 with_screen_pane 1 restart_pane "$@" 181 with_screen_pane 1 restart_pane "$@"
157} 182}
158 183
184# connect_coproc() -- start a coprocess and connect it to copied file
185# descriptors named {NAME}_IN and {NAME}_OUT where {NAME} is the
186# supplied first argument or default "COPROC". {NAME} is the name of
187# the created bash coprocess.
188
189# The copied file descriptors (unlike the original coprocess
190# file descriptors, in ${NAME[0]} and ${NAME[1]}) can be
191# passed to external processes (e.g.: socat(1) instances) and
192# used in subshells.
193
194# The created coprocess is silently disowned from job control before
195# bash can make noise about it.
159connect_coproc() 196connect_coproc()
160{ 197{
161 [ $# -gt 0 ] || set -- COPROC 198 [ $# -gt 0 ] || set -- COPROC
162 declare -n coproc="$1" 199 declare -n coproc="$1"
163 shift 200 shift
164 [ $# -gt 0 ] || set -- "${!coproc}" 201 [ $# -gt 0 ] || set -- "${!coproc}"
165 declare -n std1="${!coproc}_STDOUT" 202 declare -n std0="${!coproc}_IN"
166 declare -n std0="${!coproc}_STDIN" 203 declare -n std1="${!coproc}_OUT"
167 declare -n opid="${!coproc}_PID" 204 declare -n std2="${!coproc}_ERR"
168 if ! [ "$opid" ] 205 declare -n pid="${!coproc}_PID"
206 if ! [ "$pid" ]
169 then 207 then
170 i "coproc starting: ${!coproc}" 208 i "coproc starting: ${!coproc}"
171 local STDERR
172 { 209 {
173 coproc "${!coproc}" \ 210 coproc "${!coproc}" \
174 { 211 {
175 "$@" 212 "$@"
176 } 2>&$STDERR {STDERR}>&- 213 } 2>&$std2 {std2}>&-
214 unset std2
177 disown "%coproc ${!coproc} " 215 disown "%coproc ${!coproc} "
178 } {STDERR}>&2 2>/dev/null 216 } {std2}>&2 2>/dev/null
179 else 217 else
180 i "coproc already running: ${!coproc}" 218 i "coproc already running: ${!coproc}"
181 fi 219 fi
182 # The copied file descriptors (unlike the original coprocess
183 # file descriptors, in ${coproc[0]} and ${coproc[1]}) can be
184 # passed to external processes (e.g.: socat(1) instances) and
185 # used in subshells.
186 { 220 {
187 exec {std0}<&0 {std1}>&1 221 exec {std0}<&0 {std1}>&1
188 } <&${coproc[0]} >&${coproc[1]} 222 } <&${coproc[0]} >&${coproc[1]}
@@ -192,8 +226,8 @@ disconnect_coproc()
192{ 226{
193 [ $# -gt 0 ] || set -- COPROC 227 [ $# -gt 0 ] || set -- COPROC
194 declare -n coproc="$1" 228 declare -n coproc="$1"
195 declare -n std0="${!coproc}_STDIN" 229 declare -n std0="${!coproc}_IN"
196 declare -n std1="${!coproc}_STDOUT" 230 declare -n std1="${!coproc}_OUT"
197 declare -n pid="${!coproc}_PID" 231 declare -n pid="${!coproc}_PID"
198 if [ "$std0" -o "$std1" ] 232 if [ "$std0" -o "$std1" ]
199 then 233 then
@@ -209,12 +243,17 @@ disconnect_coproc()
209 243
210connect() 244connect()
211{ 245{
212 connect_coproc TOP_PANE 246 if ! [ "$TOP_PANE_PID" ]
247 then
248 disconnect_sink ECHOSEND
249 connect_coproc TOP_PANE
250 fi
251 connect_sink ECHOSEND echo_sender
213} 252}
214 253
215disconnect() 254disconnect()
216{ 255{
217 : "${TOP_PANE_STDIN@A} ${TOP_PANE_STDOUT@A} ${TOP_PANE_PID@A} ${TOP_PANE[@]@A}" 256 : "${TOP_PANE_IN@A} ${TOP_PANE_OUT@A} ${TOP_PANE_PID@A} ${TOP_PANE[@]@A}"
218 disconnect_coproc TOP_PANE 257 disconnect_coproc TOP_PANE
219 : "${ECHOSEND@A} ${ECHOSEND_PID@A}" 258 : "${ECHOSEND@A} ${ECHOSEND_PID@A}"
220 disconnect_sink ECHOSEND 259 disconnect_sink ECHOSEND
@@ -223,23 +262,13 @@ disconnect()
223sendc() 262sendc()
224{ 263{
225 [ "${TOP_PANE[0]}" ] || connect 264 [ "${TOP_PANE[0]}" ] || connect
226 printf '%s' "$*" >&${TOP_PANE[1]} 265 printf '%s' "$*" >&${TOP_PANE_OUT?Internal error}
227} 266}
228 267
229send() 268send()
230{ 269{
231 [ "${TOP_PANE[0]}" ] || connect 270 [ "${TOP_PANE[0]}" ] || connect
232 printf '%s\n' "$*" >&${TOP_PANE[1]} 271 printf '%s\n' "$*" >&${TOP_PANE_OUT?Internal error}
233}
234
235restart()
236{
237 start "$@"
238}
239
240start()
241{
242 foreground "$@"
243} 272}
244 273
245tty_forward() 274tty_forward()
@@ -252,18 +281,16 @@ tty_forward()
252TOP_PANE() 281TOP_PANE()
253{ 282{
254 (exec -a bottom-pane-tty-forward socat - UNIX-CONNECT:"$TWOPANE"/socket,forever) 283 (exec -a bottom-pane-tty-forward socat - UNIX-CONNECT:"$TWOPANE"/socket,forever)
255 x kill_tty_forward -STOP
256 case "$TOP_EXIT" in 284 case "$TOP_EXIT" in
257 restart ) 285 restart )
258 # x kill -USR1 $$ 286 x kill_tty_forward -STOP
259 x kill -INT $$ 287 x kill -INT $$
260 # with_screen_pane 1 start_screen_pane "$@"
261 ;; 288 ;;
262 quit ) 289 quit )
263 kill -INT $$
264 quit 290 quit
265 ;; 291 ;;
266 prompt | * ) 292 prompt | * )
293 x kill_tty_forward -STOP
267 focus bottom 294 focus bottom
268 ;; 295 ;;
269 esac 296 esac
@@ -271,10 +298,14 @@ TOP_PANE()
271 298
272connect_sink() 299connect_sink()
273{ 300{
274 [ "$1" ] || return 301 (( $# > 0 )) || set -- SINK
275 declare -n fd="$1" 302 declare -n fd="$1"
276 declare -n pid="${!fd}_PID" 303 declare -n pid="${!fd}_PID"
277 disconnect_sink "$fd" 304 if [ "$pid" ]
305 then
306 i "sink already running: ${!fd}"
307 return
308 fi
278 i "sink starting: ${!fd}" 309 i "sink starting: ${!fd}"
279 exec {fd}> >("${@:2}") 310 exec {fd}> >("${@:2}")
280 pid=$! 311 pid=$!
@@ -282,7 +313,7 @@ connect_sink()
282 313
283disconnect_sink() 314disconnect_sink()
284{ 315{
285 [ "$1" ] || return 316 (( $# > 0 )) || set -- SINK
286 declare -n fd="$1" 317 declare -n fd="$1"
287 [ "$fd" ] || return 318 [ "$fd" ] || return
288 declare -n pid="${!fd}_PID" 319 declare -n pid="${!fd}_PID"
@@ -300,51 +331,66 @@ quiet_bg()
300 } {STDERR}>&2 2>/dev/null 331 } {STDERR}>&2 2>/dev/null
301} 332}
302 333
334# echo_sender() reads from two inputs and writes to two outputs.
335#
336# Read from both:
337# read from stdin
338# read from $TOP_PANE_IN
339# Write to both:
340# write to stdout
341# write to $TOP_PANE_OUT
342# I.e.:
343# read from /dev/tty
344# read from the socat-coproc-forwarded /dev/tty in the top pane.
345# write to /dev/tty
346# write to the socat-coproc-forwarded /dev/tty in the top pane.
303echo_sender() 347echo_sender()
304{ 348{
305 (exec -a echo_sender socat - fd:${TOP_PANE_STDIN?$0: Internal error}!!-) | 349 (exec -a echo_sender socat - fd:${TOP_PANE_IN?$0: Internal error}!!-) |
306 tee >(write-tty) >&${TOP_PANE_STDOUT?$0: Internal error} 350 tee >(write-tty) >&${TOP_PANE_OUT?$0: Internal error}
307} 351}
308 352
309background() 353restart_tty_forward()
310{ 354{
355 {
356 kill -INT %tty_forward
357 disown %tty_forward
358 } &>/dev/null
359
311 with_screen_pane 1 start_screen_pane "$@" 360 with_screen_pane 1 start_screen_pane "$@"
312 focus bottom 361 focus bottom
313
314 disconnect_sink ECHOSEND
315 connect 362 connect
316 connect_sink ECHOSEND echo_sender
317 kill %tty_forward 2>/dev/null
318 %tty_forward &
319 disown %tty_forward
320 i 'starting tty_forward' 363 i 'starting tty_forward'
321 quiet_bg tty_forward >&$ECHOSEND 364 quiet_bg tty_forward >&$ECHOSEND
322 echo $! > "$TWOPANE"/tty_forward.pid 365 echo $! > "$TWOPANE"/tty_forward.pid
323} 366}
324 367
325foreground() 368attach()
326{ 369{
327 while true 370 if ! jobs %tty_forward &>/dev/null
328 do 371 then
329 background "$@" 372 restart_tty_forward "$@"
330 focus top 373 fi
331 set -x 374 focus top
332 %tty_forward >/dev/null 2>&1 375 { %tty_forward; } &>/dev/null
333 if [ "$TOP_EXIT" != restart ] || jobs %tty_forward >/dev/null 2>&1 376 focus bottom
334 then
335 focus bottom
336 set +x
337 break
338 fi
339 set +x
340 done
341} 377}
342 378
343twopane() 379detach()
344{ 380{
345 start "$@" 381 {
382 kill -INT %tty_forward
383 disown %tty_forward
384 } &>/dev/null
385 focus bottom
346} 386}
347 387
388restart() { attach "$@"; }
389start() { attach "$@"; }
390foreground() { attach "$@"; }
391
392twopane() { attach "$@"; }
393
348quit() 394quit()
349{ 395{
350 screen -X quit 396 screen -X quit
@@ -360,16 +406,6 @@ resize()
360 screen -X resize "$@" 406 screen -X resize "$@"
361} 407}
362 408
363attach()
364{
365 if jobs %tty_forward >/dev/null 2>&1
366 then
367 %tty_forward
368 else
369 foreground
370 fi
371}
372
373SIGUSR1() 409SIGUSR1()
374{ 410{
375 i "SIGUSR1: ${BASH_COMMAND@A}" 411 i "SIGUSR1: ${BASH_COMMAND@A}"
@@ -382,21 +418,45 @@ SIGCHLD()
382 418
383prompt_command() 419prompt_command()
384{ 420{
385 set -- 'unset PROMPT_COMMAND; restart' 421 [ "$TOP_EXIT" = 'restart' ] || return 0
422
423 if [ "$SIG" = INT ]
424 then
425 finally 'unset SIG; start' detach
426 return
427 fi
428
429 local job jobnew
430 if job=$(jobs -n %tty_forward 2>/dev/null)
431 then
432 jobnew=y
433 else
434 job=$(jobs %tty_forward 2>/dev/null)
435 jobnew=
436 fi
437
438 if [ ! "$job" ]
439 then
440 finally 'start' :
441 return
442 fi
386 443
387 exec {FINALLY_0}<&0 {FINALLY_1}>&1 {FINALLY_2}>&2 444 if [ "$jobnew" ]
388 if ! [ "$DEBUG" ]
389 then 445 then
390 exec &>/dev/null 446 read _ jobstatus _ <<< "$job"
447 if [ "$jobstatus" = 'Running' ]
448 then
449 jobs -x finally 'attach' kill -CONT %tty_forward
450 fi
451 return
391 fi 452 fi
392 cmd="history -d -1; exec <&$FINALLY_0 >&$FINALLY_1 >&$FINALLY_2; $*"
393 exec <<< "$cmd"
394} 453}
395 454
396SIGINT() 455SIGINT()
397{ 456{
398 i INT "${BASH_COMMAND@A}" 457 i INT "${BASH_COMMAND@A}"
399 kill $TOP_PANE_PID $ECHOSEND_PID 2>/dev/null 458 kill $TOP_PANE_PID $ECHOSEND_PID 2>/dev/null
459 SIG=INT
400 PROMPT_COMMAND=prompt_command 460 PROMPT_COMMAND=prompt_command
401} 461}
402 462