4

I opened an issue in creack/pty for this question, but I actually think it probably belongs here as it's probably more to do with my usage of the library than anything wrong with the library.

I am using a websocket api that sends stdin messages and receives stdout and stderr output from the command as well as exit codes.

For example, this API is used in a web UI, with the following string of messages sent and received

This web UI uses xterm.js to provide a terminal-like input ui and to interpret the responses, including ansi escape sequences, into terminal output.

I am building a terminal application that would like to leverage this same API, so a "terminal-in-a-terminal" like thing, where stdin is sent to the API and responses received are rendered in my application.

I would like to use creack/pty as the response interpreter, handling ansi escape sequences and the like, and holding a view of the terminal session that I can read into a string and render to the screen of my application.

So the flow is roughly like:

  • stdin sent to websocket connection
  • response received
  • write response to pty
  • read entire pty to string
  • render string to screen
  • repeat

If I use creack/pty this way, I don't actually have a command to start - it's just a nice box that interprets ansi escape sequences for me and allows me to retrieve the current "string view" of the terminal.

Here is my attempt to get a command-less pty, write to it, and read from it:

package main

import (
    "bytes"
    "fmt"
    "github.com/creack/pty"
    "io"
    "os"
)

func getPtyWithoutCommand() (*os.File, error) {
    // this function just pty.StartWithAttrs with command-specific stuff commented out
    pty, tty, err := pty.Open()
    if err != nil {
        return nil, err
    }
    defer func() { _ = tty.Close() }() // Best effort.

    // if sz != nil {
    //  if err := Setsize(pty, sz); err != nil {
    //      _ = pty.Close() // Best effort.
    //      return nil, err
    //  }
    // }
    // if c.Stdout == nil {
    //  c.Stdout = tty
    // }
    // if c.Stderr == nil {
    //  c.Stderr = tty
    // }
    // if c.Stdin == nil {
    //  c.Stdin = tty
    // }
    //
    // c.SysProcAttr = attrs
    //
    // if err := c.Start(); err != nil {
    //  _ = pty.Close() // Best effort.
    //  return nil, err
    // }
    return pty, err
}

func main() {
    myPty, err := getPtyWithoutCommand()
    if err != nil {
        panic(err)
    }

    _, err = myPty.Write([]byte("test\n"))
    if err != nil {
        panic(err)
    }
    _, err = myPty.Write([]byte{4}) // EOT
    if err != nil {
        panic(err)
    }

    buf := new(bytes.Buffer)
    _, err = io.Copy(buf, myPty)
    if err != nil {
        panic(err)
    }
    fmt.Println(buf.String())
}

I get the following error

❯ go run test.go
panic: write /dev/ptmx: input/output error

goroutine 1 [running]:
main.main()
        test.go:52 +0x19c
exit status 2

Is what I'm trying to do sane at all? Is there a better way to achieve my goal here?

robinovitch61
  • 167
  • 2
  • 9

1 Answers1

3

I was tackling a similar problem and creack's pty might be a bit too abstracted for what you want. I just used the golang standard xterm terminal emulator https://pkg.go.dev/golang.org/x/term and a pipe.

Here's a partial snippet as an example of what I mean. Your stdin stream is fed into the stdin_writer and your stdout is written to the writer, don't forget to Flush()!

stdin_reader, stdin_writer := io.Pipe()
reader := bufio.NewReader(stdin_reader)

stdout_writer := bytes.Buffer{}
writer := bufio.NewWriter(&stdout_writer)

rw := bufio.NewReadWriter(reader, writer)
t := term.NewTerminal(rw, prompt)

// constantly be reading lines
go func() {
    for {
        line, err := t.ReadLine()
        if err == io.EOF {
            log.Printf("got EOF")
        }
        if err != nil {
            log.Printf("got err")
        }
        if line == "" {
            continue
        }
        log.Printf("LINE: %s", line)
    }
}()
moleperson
  • 46
  • 1