Skip to content

Commit

Permalink
Honor Ctrl+C while waiting for user input (#1805)
Browse files Browse the repository at this point in the history
Fixes #1800

RELEASE_NOTES=[BUGFIX] Honor Ctrl+C while waiting for user input

Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org>
  • Loading branch information
dominikschulz authored Feb 22, 2021
1 parent 4f87dec commit 49ad0ee
Show file tree
Hide file tree
Showing 4 changed files with 42 additions and 9 deletions.
1 change: 1 addition & 0 deletions internal/cui/cui.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ func GetSelection(ctx context.Context, prompt string, choices []string) (string,
if ctxutil.IsAlwaysYes(ctx) || !ctxutil.IsInteractive(ctx) {
return "impossible", 0
}

for i, c := range choices {
fmt.Printf("[% d] %s\n", i, c)
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/termio/ask.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func AskForString(ctx context.Context, text, def string) (string, error) {
}

fmt.Fprintf(Stdout, "%s [%s]: ", text, def)
input, err := NewReader(Stdin).ReadLine()
input, err := NewReader(ctx, Stdin).ReadLine()
if err != nil {
return "", fmt.Errorf("failed to read user input: %w", err)
}
Expand Down Expand Up @@ -95,7 +95,7 @@ func AskForInt(ctx context.Context, text string, def int) (int, error) {
return def, nil
}

str, err := AskForString(ctx, text, strconv.Itoa(def))
str, err := AskForString(ctx, text+" (q to abort)", strconv.Itoa(def))
if err != nil {
return 0, err
}
Expand Down
36 changes: 32 additions & 4 deletions pkg/termio/reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,58 @@ package termio

import (
"bytes"
"context"
"io"
)

// LineReader is an unbuffered line reader
type LineReader struct {
r io.Reader
r io.Reader
ctx context.Context
}

// NewReader creates a new line reader
func NewReader(r io.Reader) *LineReader {
return &LineReader{r: r}
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)
}

// 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 {
n, err := lr.r.Read(buf)
// 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 := 0; i < n; i++ {
if buf[i] == '\n' {
return out.String(), nil
Expand Down
10 changes: 7 additions & 3 deletions pkg/termio/reader_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package termio

import (
"context"
"io"
"strings"
"testing"
Expand All @@ -24,17 +25,19 @@ func TestReadLines(t *testing.T) {
}

func TestReadLineError(t *testing.T) {
ctx := context.Background()
stdin := strings.NewReader("fo")
lr := NewReader(iotest.TimeoutReader(stdin))
lr := NewReader(ctx, iotest.TimeoutReader(stdin))

line, err := lr.ReadLine()
assert.Error(t, err)
assert.Equal(t, "f", line)
}

func TestRead(t *testing.T) {
ctx := context.Background()
stdin := strings.NewReader(`foobarbazzabzabzab`)
lr := NewReader(stdin)
lr := NewReader(ctx, stdin)

b := make([]byte, 10)
n, err := lr.Read(b)
Expand All @@ -44,7 +47,8 @@ func TestRead(t *testing.T) {
}

func mustReadLine(r io.Reader) string {
line, err := NewReader(r).ReadLine()
ctx := context.Background()
line, err := NewReader(ctx, r).ReadLine()
if err != nil {
panic(err)
}
Expand Down

0 comments on commit 49ad0ee

Please sign in to comment.