diff options
Diffstat (limited to 'src')
-rwxr-xr-x | src/twopane.bash | 424 |
1 files changed, 424 insertions, 0 deletions
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 @@ | |||
1 | #!/bin/bash | ||
2 | BASH_ARGV0=twopane.bash | ||
3 | echo -n "$0" >/proc/$$/comm | ||
4 | set -e | ||
5 | set -f | ||
6 | set -o pipefail | ||
7 | shopt -s lastpipe | ||
8 | |||
9 | BOT_SIZE=8 | ||
10 | BOT_TITLE=input | ||
11 | |||
12 | if [ $# = 0 ] | ||
13 | then | ||
14 | TOP_CMD="$SHELL -i" | ||
15 | BOT_CMD=background | ||
16 | TOP_EXIT=prompt | ||
17 | #BOT_CMD=start | ||
18 | #TOP_EXIT=restart | ||
19 | else | ||
20 | TOP_CMD=$* | ||
21 | BOT_CMD=start | ||
22 | TOP_EXIT=quit | ||
23 | fi | ||
24 | TOP_TITLE="Command: $TOP_CMD" | ||
25 | |||
26 | TWOPANE=$(mktemp -d) | ||
27 | export TWOPANE TOP_CMD BOT_CMD TOP_TITLE BOT_TITLE BOT_SIZE | ||
28 | trap 'rm -r "$TWOPANE"' EXIT | ||
29 | STY=twopane.${TWOPANE##*/} | ||
30 | |||
31 | save_file() | ||
32 | { | ||
33 | cat > "$TWOPANE"/"${1:?$0: Error: filename cannot be empty string}" | ||
34 | } | ||
35 | |||
36 | save_screenrc() | ||
37 | { | ||
38 | save_file screenrc"${1:+.$1}" | ||
39 | } | ||
40 | |||
41 | save_screenrc <<'.' | ||
42 | # Disable keybindings. | ||
43 | unbindall | ||
44 | escape \0\0 | ||
45 | # Disable messages. This is needed for screen -X/-Q to work reasonably. | ||
46 | msgwait 0 | ||
47 | msgminwait 0 | ||
48 | # Try to disable blocking the terminal on ^S. | ||
49 | # Doesn't seem to work here. | ||
50 | # More is needed. Xterm? | ||
51 | nonblock on | ||
52 | |||
53 | caption string '%t' | ||
54 | layout new | ||
55 | split | ||
56 | focus bottom | ||
57 | resize $BOT_SIZE | ||
58 | screen -ln -t "$BOT_TITLE" 0 bash --noprofile --rcfile "$TWOPANE"/bashrc -i | ||
59 | layout save 0 | ||
60 | . | ||
61 | |||
62 | check_top() | ||
63 | { | ||
64 | screen -p1 -Q info >/dev/null | ||
65 | } | ||
66 | |||
67 | kill_top() | ||
68 | { | ||
69 | while check_top | ||
70 | do | ||
71 | screen -p1 -X kill | ||
72 | done | ||
73 | } | ||
74 | |||
75 | start_top() | ||
76 | { | ||
77 | if check_top | ||
78 | then | ||
79 | return | ||
80 | fi | ||
81 | if [ $# = 0 ] | ||
82 | then | ||
83 | set -f | ||
84 | set -- ${TOP_CMD:-bash -i} | ||
85 | fi | ||
86 | TOP_TITLE="Command: ${*@Q}" | ||
87 | screen -X focus top | ||
88 | screen -X screen -ln -t "$TOP_TITLE" 1 "$@" | ||
89 | screen -p1 -X exec .!. sh -c 'exec socat UNIX-LISTEN:"$TWOPANE"/socket STDIN,cfmakeraw!!STDOUT' | ||
90 | } | ||
91 | |||
92 | restart_top() | ||
93 | { | ||
94 | kill_top | ||
95 | start_top "$@" | ||
96 | } | ||
97 | |||
98 | socat_connect() | ||
99 | { | ||
100 | socat STDIN!!STDOUT UNIX-CONNECT:"$TWOPANE"/socket,forever | ||
101 | } | ||
102 | |||
103 | # Start SOCAT if necessary. | ||
104 | # Connect the running SOCAT to file descriptors. | ||
105 | # Optionally assign the file descriptors to the specified variables. | ||
106 | # Optionally assign the socat PID to the specified variable. | ||
107 | # | ||
108 | # The copied file descriptors (unlike the original coprocess file | ||
109 | # descriptors, in ${SOCAT[0]} and ${SOCAT[1]}) can be passed to external | ||
110 | # processes (e.g.: other socat(1) instances) and used in subshells. | ||
111 | connect() | ||
112 | { | ||
113 | case $# in | ||
114 | 3 | 5 ) | ||
115 | declare -n pid="$1" std0="$2" std1="$3" | ||
116 | shift 3 | ||
117 | ;; | ||
118 | 2 | 4 ) | ||
119 | local pid | ||
120 | declare -n std0="$1" std1="$2" | ||
121 | shift 2 | ||
122 | ;; | ||
123 | 0 ) | ||
124 | local std0 std1 pid | ||
125 | ;; | ||
126 | * ) | ||
127 | return 1 | ||
128 | ;; | ||
129 | esac | ||
130 | if ! [ "${SOCAT[0]}" ] | ||
131 | then | ||
132 | local STDERR | ||
133 | exec {STDERR}>&2 | ||
134 | { | ||
135 | coproc SOCAT { socat_connect; } 2>&$STDERR | ||
136 | } 2>/dev/null | ||
137 | eval "exec $STDERR>&-" | ||
138 | fi | ||
139 | pid=${SOCAT_PID} | ||
140 | exec {std0}<&${SOCAT[0]} {std1}>&${SOCAT[1]} | ||
141 | disown | ||
142 | } | ||
143 | |||
144 | sendc() | ||
145 | { | ||
146 | [ "${SOCAT[0]}" ] || connect | ||
147 | printf '%s' "$*" >&${SOCAT[1]} | ||
148 | } | ||
149 | |||
150 | send() | ||
151 | { | ||
152 | [ "${SOCAT[0]}" ] || connect | ||
153 | printf '%s\n' "$*" >&${SOCAT[1]} | ||
154 | } | ||
155 | |||
156 | disconnect() | ||
157 | { | ||
158 | if [ $# = 2 ] | ||
159 | then | ||
160 | declare -i -n std0="$1" std1="$2" | ||
161 | eval "exec $std0<&- $std1>&-" | ||
162 | unset std0 std1 | ||
163 | fi | ||
164 | wait -f "$SOCAT_PID" 2>/dev/null | ||
165 | } | ||
166 | |||
167 | restart() | ||
168 | { | ||
169 | start "$@" | ||
170 | } | ||
171 | |||
172 | start() | ||
173 | { | ||
174 | foreground "$@" | ||
175 | } | ||
176 | |||
177 | background() | ||
178 | { | ||
179 | start_top "$@" | ||
180 | focus bottom | ||
181 | connect stdin stdout | ||
182 | } | ||
183 | |||
184 | foreground_loop() | ||
185 | { | ||
186 | while true | ||
187 | do | ||
188 | start_top "$@" | ||
189 | connect stdin stdout | ||
190 | forward | ||
191 | disconnect stdin stdout | ||
192 | |||
193 | case "$TOP_EXIT" in | ||
194 | restart ) | ||
195 | continue ;; | ||
196 | quit ) | ||
197 | exit ;; | ||
198 | prompt | * ) | ||
199 | focus bottom | ||
200 | break | ||
201 | ;; | ||
202 | esac | ||
203 | done | ||
204 | } | ||
205 | |||
206 | forwarding() | ||
207 | { | ||
208 | [ "$FORWARD_PID" ] || return | ||
209 | FORWARD_JOBSPEC=$(jobs -sl | pid_to_jobspec "$FORWARD_PID") | ||
210 | [ "$FORWARD_JOBSPEC" ] | ||
211 | } | ||
212 | |||
213 | forward() | ||
214 | { | ||
215 | declare -g FORWARD_PID | ||
216 | if ! check_top | ||
217 | then | ||
218 | echo "$0: Warning: Nothing to forward. Starting anew." >&2 | ||
219 | background "$@" | ||
220 | elif forwarding | ||
221 | then | ||
222 | resume_forward | ||
223 | return | ||
224 | fi | ||
225 | |||
226 | focus top | ||
227 | old_stty=$(stty -g) | ||
228 | # Lowercase $stdin/$stdout are the SOCAT coprocess connected to | ||
229 | # the other pane's terminal. Uppercase $STDIN/$STDOUT are the | ||
230 | # real stdin/stdout of this function, connected to the lower | ||
231 | # pane's terminal. Socat here merges inputs from both sources. | ||
232 | exec {STDIN}<&0 {STDOUT}>&1 {STDERR}>&2 | ||
233 | |||
234 | # The input is put out raw back over the socket. The input is | ||
235 | # copied to stdout after being filtered (to display control | ||
236 | # characters with carrot-encoding like '^[' etc). | ||
237 | exec {TEE}> >(tee >(output_filter >&$STDOUT) >&$stdout) | ||
238 | stty=cfmakeraw,opost=1,onlcr=1 | ||
239 | { | ||
240 | socat FD:$STDIN,$stty!!STDOUT - <&$stdin >&$TEE 2>&$STDERR & | ||
241 | } 2>/dev/null | ||
242 | FORWARD_PID=$! | ||
243 | printf '%s\n' "#!/bin/bash" "kill $!" "screen -X focus bottom" > "$TWOPANE"/unforward | ||
244 | chmod +x "$TWOPANE"/unforward | ||
245 | fg >/dev/null | ||
246 | stty "$old_stty" | ||
247 | focus bottom | ||
248 | echo | ||
249 | } | ||
250 | |||
251 | cfmakeraw() | ||
252 | { | ||
253 | cmd=(stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl | ||
254 | -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8) | ||
255 | "${cmd[@]}" "$@" | ||
256 | } | ||
257 | |||
258 | pid_to_jobspec() | ||
259 | { | ||
260 | while read jspec pid status cmd | ||
261 | do | ||
262 | [ "$pid" = "$1" ] || continue | ||
263 | jspec=${jspec%]*} | ||
264 | jspec=%${jspec#[} | ||
265 | echo $jspec | ||
266 | return | ||
267 | done | ||
268 | false | ||
269 | } | ||
270 | |||
271 | resume_forward() | ||
272 | { | ||
273 | old_stty=$(stty -g) | ||
274 | cfmakeraw opost onlcr | ||
275 | focus top | ||
276 | fg "$FORWARD_JOBSPEC" >/dev/null | ||
277 | stty "$old_stty" | ||
278 | focus bottom | ||
279 | } | ||
280 | |||
281 | foreground() | ||
282 | { | ||
283 | if forwarding | ||
284 | then | ||
285 | resume_forward | ||
286 | else | ||
287 | foreground_loop "$@" | ||
288 | fi | ||
289 | } | ||
290 | |||
291 | twopane() | ||
292 | { | ||
293 | start "$@" | ||
294 | } | ||
295 | |||
296 | quit() | ||
297 | { | ||
298 | screen -X quit | ||
299 | } | ||
300 | |||
301 | focus() | ||
302 | { | ||
303 | screen -X focus "$@" | ||
304 | } | ||
305 | |||
306 | output_filter() | ||
307 | { | ||
308 | tokenize | colorize | soft_cursor | ||
309 | } | ||
310 | |||
311 | # if (show_nonprinting) { | ||
312 | # while (true) | ||
313 | # { | ||
314 | # if (ch >= 32) { | ||
315 | # if (ch < 127) *bpout++ = ch; | ||
316 | # else if (ch == 127) {*bpout++ = '^'; *bpout++ = '?';} | ||
317 | # else { | ||
318 | # *bpout++ = 'M'; | ||
319 | # *bpout++ = '-'; | ||
320 | # if (ch >= 128 + 32) { | ||
321 | # if (ch < 128 + 127) *bpout++ = ch - 128; | ||
322 | # else {*bpout++ = '^'; *bpout++ = '?';}} | ||
323 | # else {*bpout++ = '^'; *bpout++ = ch - 128 + 64;}}} | ||
324 | # else if (ch == '\t' && !show_tabs) *bpout++ = '\t'; | ||
325 | # else if (ch == '\n') {newlines = -1; break;} | ||
326 | # else {*bpout++ = '^'; *bpout++ = ch + 64;} | ||
327 | # ch = *bpin++;}} | ||
328 | |||
329 | chr() | ||
330 | { | ||
331 | declare -i n="$*" | ||
332 | printf "\\$(printf %o "$n")" | ||
333 | } | ||
334 | |||
335 | colorize() | ||
336 | { | ||
337 | while read -r | ||
338 | do | ||
339 | case "$REPLY" in | ||
340 | \\[0-7][0-7][0-7] ) ;; | ||
341 | * ) | ||
342 | printf '%s\n' "$REPLY" | ||
343 | continue | ||
344 | ;; | ||
345 | esac | ||
346 | declare -i c=8#"${REPLY#?}" | ||
347 | if (( c > 128 + 127 )) | ||
348 | then | ||
349 | : | ||
350 | elif (( c > 128 + 32 )) | ||
351 | then | ||
352 | printf -v REPLY "M-$(chr c - 128)" | ||
353 | elif (( c > 127 )) | ||
354 | then | ||
355 | printf -v REPLY "M-^$(chr c - 128 + 64)" | ||
356 | elif (( c < 32 )) | ||
357 | then | ||
358 | printf -v REPLY "^$(chr c + 64)" | ||
359 | fi | ||
360 | printf $'\e[1m%s\e[m\n' "$REPLY" | ||
361 | done | ||
362 | } | ||
363 | |||
364 | tokenize() | ||
365 | { | ||
366 | while read -r -N1 | ||
367 | do | ||
368 | if [[ "$REPLY" =~ [[:print:]] ]] | ||
369 | then | ||
370 | # Output one printable character per line. It may be a | ||
371 | # multibyte unicode character. | ||
372 | printf '%s\n' "$REPLY" | ||
373 | continue | ||
374 | else | ||
375 | # If it is a non-printable, then we output a | ||
376 | # multi-character line. In this case we colorize it | ||
377 | # later so that it won't be confused with multiple | ||
378 | # printable characters. | ||
379 | printf '\\%.3o\n' "'$REPLY" | ||
380 | continue | ||
381 | fi | ||
382 | done | ||
383 | } | ||
384 | |||
385 | soft_cursor() | ||
386 | { | ||
387 | FMT=$'%s \b\e[%sm \e[m\b' | ||
388 | REPLY= | ||
389 | while printf "$FMT" "$REPLY" $(( 101 + RANDOM % 7 )) | ||
390 | do | ||
391 | read -r || break | ||
392 | done | ||
393 | } | ||
394 | |||
395 | our_bashrc_main() | ||
396 | { | ||
397 | set -f | ||
398 | set -o pipefail | ||
399 | trap "screen -X quit" EXIT | ||
400 | export PS1="$BOT_TITLE\\\$ " | ||
401 | } | ||
402 | |||
403 | save_file bashrc <<. | ||
404 | BASH_ARGV0=twopane | ||
405 | echo -n "\$0" >/proc/\$\$/comm | ||
406 | ${TOP_CMD@A} | ||
407 | ${BOT_CMD@A} | ||
408 | ${TOP_TITLE@A} | ||
409 | ${BOT_TITLE@A} | ||
410 | |||
411 | ${TOP_EXIT@A} | ||
412 | |||
413 | $(declare -f) | ||
414 | our_bashrc_main | ||
415 | |||
416 | $BOT_CMD | ||
417 | . | ||
418 | |||
419 | main() | ||
420 | { | ||
421 | screen -c "$TWOPANE"/screenrc -m -S "$STY" -ln | ||
422 | } | ||
423 | |||
424 | main "$@" | ||