Here, the eipe2 script wraps around emacsclient to enable myself to intercept the traffic of a bash pipeline and edit it mid-command. It’s modelled after vipe.

But that is only half the trick. I then do the same for tcl/expect. On its own, expect can’t edit the stream, but by placing an eipe2 within the expect script I can complete the pipeline, automating emacs with expect.

But that is not the most awesome part. Although expect could fully automate emacs, I hand over control to the user before exiting expect, allowing the user to make some final adjustments to the data before continuing the pipeline.

The final command and an explanation

This commands pipes the html through expect, which spawns emacs and runs a few automated key presses to prepare the user for editing.

Command is then given to the user to do some editing.

When the buffer is killed, the emacs frame is also killed and the contents of the killed buffer is piped into vim.

Sadly I can’t explain it all here as there are so many components at work, but the result makes semi-automated stream editing using an interactive TUI possible and very easy to use.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
curl -s "https://mullikine.github.io/css/magit.css" |
    x \
        -sh "eipe2 -aft"                       ` # run emacs with language detection            ` \
        -e "When done with this frame"         ` # wait for this string to apper                ` \
        -m s -s o -s "\\bbold\\b" -c m         ` # start occur mode. search for 'bold'          ` \
        -m l -m 1                              ` # make it the only window                      ` \
        -m n                                   ` # go to the first/next occurrence              ` \
        -m l -m ';'                            ` # go to the other window                       ` \
        -m i                                   ` # enter 'iedit' mode                           ` \
        -m r                                   ` # select the word under the cursor (bold)      ` \
        -s normal                              ` # replace 'bold' with 'normal'                 ` \
        -i                                     ` # hand over interaction to the user            ` |
    v

Demonstration

asciinema recording

The generated expect script

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#!/usr/bin/expect -f
#trap sigwinch and pass it to the child we spawned
trap {
    set rows [stty rows]
    set cols [stty columns]
    stty rows $rows columns $cols < $spawn_out(slave,name)
} WINCH

proc getctrl {char} {
    set ctrl [expr ("$char" & 01xF)]
    return $ctrl
}

set force_conservative 0
if {$force_conservative} {
    set send_slow {1 .1}
    proc send {ignore arg} {
        sleep .1
        exp_send -s -- $arg
    }
}

# For send -h
set send_human {.4 .4 .2 .5 100}
set ::env(PATH) "/home/shane/scripts:/home/shane/var/smulliga/source/git/google-cloud-sdk/google-cloud-sdk/bin:/home/shane/.cargo/bin:/usr/local/toolchains/clang+llvm-9.0.0-x86_64-linux-gnu-ubuntu-16.04/bin:/usr/local/toolchains/clang/bin:/usr/local/toolchains/boost/bin:/home/shane/.nix-profile/bin:/home/shane/.cask/bin:/home/shane/.opam/system/bin:/home/shane/notes2018/ws/codelingo/scripts:/home/shane/scripts:scripts:/home/shane/.pyenv/bin:/opt/apache-maven-3.5.3/bin:/home/shane/bin:/usr/local/racket/bin:/home/shane/local/emacs26/bin:/home/shane/local/bin:/usr/sbin:/sbin:/usr/local/sbin:/usr/local/bin:/usr/sbin/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/usr/lib/jvm/java-8-oracle/bin:/usr/lib/jvm/java-8-oracle/db/bin:/usr/lib/jvm/java-8-oracle/jre/bin:/home/shane/source/git/fzf/bin/fzf:/home/shane/go/bin:/home/shane/go1.6.2/bin:/downloads/node-v9.9.0-linux-x64/bin:/home/shane/node_modules/.bin/:/home/shane/.cabal/bin:/home/shane/.local/bin:/home/shane/.config/composer/vendor/bin:/usr/local/go/bin:/home/shane/dump/downloads/node-v9.9.0-linux-x64/bin:/home/shane/.config/composer/vendor/bin/home/shane/source/git/fzf/bin/fzf/home/shane/source/git/fzf/bin/fzf/home/shane/source/git/fzf/bin/fzf:/usr/bin"
# This is not what you want.
# set ::env(TTY) "/dev/pts/508"
# I must set the TTY to expect's TTY
# set ::env(TTY) "/dev/pts/508"
# puts "$::env(TTY)"
unset ::env(TTY)
set timeout -1
match_max 100000
set SHELL "zsh"
spawn "/tmp/file_new_script_cx9Zkd_rand-30891_pid-20249.sh"
expect -exact "When done with this frame"
send -- \033s
send -- "o"
send -- "cloudflare.com"
send -- \015
send -- \033l
send -- \0331
send -- \033n
send -- \033l
send -- \033\;
interact
expect eof
close

How it works - script snippets

I’m just going to copy out some of the important parts of the following scripts.

Explaining it all will take too long.

eipe2

1
2
3
4
5
6
#!/bin/bash
export TTY

( hs "$(basename "$0")" "$@" "#" "<==" "$(ps -o comm= $PPID)" 0</dev/null ) &>/dev/null

orspe "$@" +ooq "(buffer-string)"

sp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
+ooq) {
    # This assumes stdin and a buffer which is killed
    # I need a way to write on frame kill, rather than buffer kill

    ON_DELFRAME_EVAL_TO_STDOUT="$2"
    tf_buffer_path="$(odn ux tf path || echo /dev/null)"
    elisp+="(on-kill-write-and-close-frame $(aqf "$tf_buffer_path" "$ON_DELFRAME_EVAL_TO_STDOUT"))"
    shift
    shift
}
;;

my-frame.el

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
(defun on-frame-quit-write (path outvalue)
  (let* ((framename (frame-parameter termframe 'name))
         ;; (fun (str2sym (concat (slugify path) "-frame-quit-write-fun")))
         (fun (str2sym (concat (slugify framename) "-frame-quit-write-fun"))))
    (eval `(progn (defun ,fun (frame)
                    (interactive)
                    (message "%s" (concat "Running: " (sym2str ',fun)))
                    (write-string-to-file (str (eval-string ,outvalue)) ,path)
                    (remove-hook 'delete-frame-hook ',fun)
                    (fmakunbound ',fun))
                  (add-hook 'delete-frame-hook ',fun)))))

(defun on-kill-write-and-close-frame (path &optional outvalue)
  (write-to-file-on-buffer-exit path outvalue)
  (close-frame-on-buffer-exit))

(defun write-termfile ()
  ;; (interactive)

  (let ((outvalue (if (and (variable-p 'outvalue-local)
                           outvalue-local)
                      (str (eval-string outvalue-local))
                    (buffer-string))))
    (if (and (variable-p 'termfile-local)
             termfile-local)
        (write-string-to-file outvalue termfile-local))))

(defun write-to-file-on-buffer-exit (path &optional outvalue)
  (interactive)
  (defset-local termfile-local path)
  (defset-local outvalue-local outvalue)

  (add-hook 'kill-buffer-hook 'write-termfile))

x

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
if test -n "$tfstdin" && test -n "$INIT_SPAWN"; then
    # INIT_SPAWN="$(nsfa -E "cat $tfstdin | $INIT_SPAWN")"
    INIT_SPAWN="cat $tfstdin | $INIT_SPAWN"
fi

if is_stdout_pipe; then
    tfstdout="$(odn tf txt)"
    # INIT_SPAWN="$(nsfa -E "cat $tfstdin | $INIT_SPAWN")"
    INIT_SPAWN="$INIT_SPAWN > $tfstdout"
fi

INIT_SPAWN="$(nsfa -E "$INIT_SPAWN")"

# script_append "spawn \"\$SHELL\""
# This is required if I want to spawn this way:
# x -cd "$(pwd)" -sh "racket -iI racket" -e ">" -i
script_append "spawn $(aqf "$INIT_SPAWN")"

# script_append "expect -exact \"\r\""

Generated expect code

1
unset ::env(TTY)

TTY must be unset so the spawned scripts can determine the TTY from the /dev/tty command.

nsfa

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#!/bin/bash
export TTY

## Example
## -------
## 726edcf master 20:34 shane/notes2018 δ » new-script-from-args echo hi
## /tmp/file_new_script_8W7iI5_rand-11510_pid-21032.sh
## 726edcf master 20:34 shane/notes2018 δ » /tmp/file_new_script_8W7iI5_rand-11510_pid-21032.sh
## hi

while [ $# -gt 0 ]; do opt="$1"; case "$opt" in
    -E) {
        DO_EXEC=y
        shift
    }
    ;;

    *) break;
esac; done

if test "$DO_EXEC" = "y"; then
    CMD="$1"
else
    CMD="$(cmd "$@")"
fi

tf_new_script="$(ux tf new_script sh || echo /dev/null)"
# trap "rm \"$tf_new_script\" 2>/dev/null" 0

IFS= read -r -d '' scriptcode <<'HEREDOC'
#!/bin/bash
: ${TTY:="$(tm-tty)"}
export TTY
stty stop undef 2>/dev/null; stty start undef 2>/dev/null;
HEREDOC
printf -- "%s\n" "$scriptcode" >> "$tf_new_script"
export PATH=$HOME/scripts:$PATH

printf -- "%s\n" "$CMD" >> "$tf_new_script"
chmod a+x "$tf_new_script"
echo -n "$tf_new_script"

This part is where the magic happens.

1
2
: ${TTY:="$(tm-tty)"}
export TTY

tm-tty

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#!/bin/bash

# Whoa, this was really bad. It broke the tty command.
# exec 0</dev/null

# I also haven't yet tried simply specifying /dev/tty, although I'm
# unsure how to test it, and tty might do it

{
    test -n "$TTY" && echo "$TTY"
} || {
    ttymaybe="$(tty 2>&1)"
    # echo "$ttymaybe $?" 1>&2
    if [[ "${ttymaybe:0:1}" == "/" ]]; then
        printf -- "%s\n" "$ttymaybe"
        :
    else
        false
    fi
} || {
   # Not sure if this always succeeds
   # It does work though, when it works, even if tty above has forgotten
   # If I have problems then remove it I guess, but then
   # test on:
   # term-mode cr $NOTES/ws/problog/scratch/scratch.problog
   # eipe2 is broken now
    {
       exec 5>/dev/tty && echo /dev/tty
       # This properly hides the error and the fallback works correctly
       # Test from "buffer-name": "*slime-repl sbcl*"
       # (b sph eww "https://blog.miguelgrinberg.com/post/how-to-make-python-wait")
    } 2>/dev/null
} || {
    TTY="$(tmux display-message -p '#{pane_tty}')"
    echo "$TTY"
}