Context: I am trying to write a small tool in Golang, which spawns a vim
editor and tries to record all the keystrokes input by the user.
package main
import (
"bufio"
"fmt"
"os"
"os/exec"
)
func main() {
cmd := exec.Command("vim", "test.txt")
rdr := bufio.NewReader(os.Stdin)
cmd.Stdin = rdr
cmd.Stdout = os.Stdout
if err := cmd.Run(); err != nil {
fmt.Println("err", err)
return
}
}
The above piece of code works as expected, but I want to record the keystrokes into a temporary buffer, for this I am using an io.TeeReader
which writes the os.Stdin
contents into a bytes.Buffer
. This seems to break the tty, vim starts behaving weirdly and doesn't differentiate between the insert mode and command mode. Also The Read
method returned by the io.TeeReader
hangs up and doesn't seem to return after vim
is exited.
Example code:
package main
import (
"bytes"
"fmt"
"io"
"os"
"os/exec"
)
func main() {
cmd := exec.Command("vim", "test.txt")
vimBuff := bytes.Buffer{}
rdr := io.TeeReader(os.Stdin, &vimBuff)
cmd.Stdin = rdr
cmd.Stdout = os.Stdout
if err := cmd.Run(); err != nil {
fmt.Println("err", err)
return
}
}
The reason is that Vim is a program which expects a real terminal — which might be a hardware device or an emulator such as xterm
, GNOME Terminal, screen
, tmux
etc in order to work interactively.
Normally, when you start Vim in your shell, these days with 99% confidence you're doing that in the shell running in some terminal emulator window. So, when Vim starts, its stdin and stdout are connected to a real terminal, and Vim checks that.
In your code, you actually add a layer between the terminal and Vim, and so Vim is no longer connected to a terminal, but rather to a plain data stream.
A little demonstration using bash
in a GNOME Termnial running on Linux:
$ stat -L -c %F /dev/fd/0
character special file
$ stat -L -c %F /dev/tty
character special file
$ echo test | stat -L -c %F /dev/fd/0
fifo
$ stat -L -c %F /dev/fd/0 <<EOF
> test
> EOF
fifo
$ f=$(mktemp); stat -L -c %F /dev/fd/0 <"$f"; rm "$f"
regular empty file
Observe that in the first two cases we asked stat
running right in the shell to figure out the type of the file its stdin is connected to, and it's a "character special" file — a terminal, in this case.
When we ask it to do the same when its stdin is connected to a pipe (the "here document" in the penultimate example is handled by bash
in the same way: it's presented to the tagret program as a pipe), it reports the type of the file is fifo.
Ultimately, we connect its stdin to a real file, and it figures that out, too.
In your code, Vim's stdin will be connected to pipe (fifo), and it won't be able to work with it properly as it's not a terminal.
You need to run Vim on a terminal. To achieve that, you should employ the so-called pseudo terminal machinery (exploited, among others, by SSH and terminal multiplexors such as screen
and tmux
): basically, you create a PTY for Vim, make it run on that PTY and then interact with that subsystem.
The go-to Go package implementing PTY support is probably this but see this in general.
Also see this.