// Package termio provides helpers and functions to work with // terminal input and output. package termio import ( "bytes" "context" "errors" "io" ) // LineReader is an unbuffered line reader. type LineReader struct { r io.Reader ctx context.Context //nolint:containedctx } // NewReader creates a new line reader. func NewReader(ctx context.Context, r io.Reader) *LineReader { return &LineReader{r: r, ctx: ctx} } // Read implements io.Reader. func (lr LineReader) Read(p []byte) (int, error) { return lr.r.Read(p) //nolint:wrapcheck } // rr is a composite value to transport the result of Read through a channel. type rr struct { n int err error } // ReadLine reads one line w/o buffering. func (lr LineReader) ReadLine() (string, error) { out := &bytes.Buffer{} buf := make([]byte, 1) // important: we must only read one byte at a time! for { // we wait for the user input in the background so we can use the // select statement below to be able to immediately quit when the // user presses Ctrl+C msg := make(chan rr, 1) go func() { n, err := lr.r.Read(buf) msg <- rr{n, err} }() var n int var err error // wait for a user input (or a signal to abort) select { case <-lr.ctx.Done(): return "", ErrAborted case s := <-msg: n = s.n err = s.err } // process the user input for i := range n { if buf[i] == '\n' { return out.String(), nil } // err is always nil _ = out.WriteByte(buf[i]) } // Callers should always process the n > 0 bytes returned before considering the error err. if err != nil { if errors.Is(err, io.EOF) { return out.String(), nil } return out.String(), err } } }