From 9563bafdf5db0dcc9ab732e0c6a547eb09029122 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Sun, 3 Dec 2023 18:11:18 -0500 Subject: [PATCH] feat: support windows console input buffer This adds support to the Windows Console Input Buffer API which access the console API directly without the need for virtual terminal input (i.e. the current mode that emulates unix inputs). Since this uses the console input api, we can finally read window size events. This is mearly based on the awesome work of @erikgeiser in #140. Fixes: https://github.com/charmbracelet/bubbletea/issues/538 Fixes: https://github.com/charmbracelet/bubbletea/issues/121 --- examples/go.mod | 1 + examples/go.sum | 3 + go.mod | 3 +- go.sum | 3 + inputreader_other.go | 14 +++ inputreader_windows.go | 154 ++++++++++++++++++++++++ key.go | 4 +- key_other.go | 13 +++ key_test.go | 2 +- key_windows.go | 259 +++++++++++++++++++++++++++++++++++++++++ tty.go | 6 +- tutorials/go.mod | 1 + tutorials/go.sum | 3 + 13 files changed, 458 insertions(+), 8 deletions(-) create mode 100644 inputreader_other.go create mode 100644 inputreader_windows.go create mode 100644 key_other.go create mode 100644 key_windows.go diff --git a/examples/go.mod b/examples/go.mod index 48f1c3012e..96de302419 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -25,6 +25,7 @@ require ( github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/dlclark/regexp2 v1.4.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/gorilla/css v1.0.0 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect diff --git a/examples/go.sum b/examples/go.sum index c1562876cc..1b636845e9 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -30,6 +30,8 @@ github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/fogleman/ease v0.0.0-20170301025033-8da417bf1776 h1:VRIbnDWRmAh5yBdz+J6yFMF5vso1It6vn+WmM/5l7MA= github.com/fogleman/ease v0.0.0-20170301025033-8da417bf1776/go.mod h1:9wvnDu3YOfxzWM9Cst40msBF1C2UdQgDv962oTxSuMs= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= @@ -100,6 +102,7 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/go.mod b/go.mod index b5e9e98cd9..c46bcf9856 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.17 require ( github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f github.com/mattn/go-isatty v0.0.18 github.com/mattn/go-localereader v0.0.1 github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b @@ -11,6 +12,7 @@ require ( github.com/muesli/reflow v0.3.0 github.com/muesli/termenv v0.15.2 golang.org/x/sync v0.1.0 + golang.org/x/sys v0.7.0 golang.org/x/term v0.6.0 ) @@ -19,6 +21,5 @@ require ( github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect github.com/rivo/uniseg v0.2.0 // indirect - golang.org/x/sys v0.7.0 // indirect golang.org/x/text v0.3.8 // indirect ) diff --git a/go.sum b/go.sum index 9bc66d6357..ce379f59c4 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= @@ -37,6 +39,7 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/inputreader_other.go b/inputreader_other.go new file mode 100644 index 0000000000..8e63a87dc6 --- /dev/null +++ b/inputreader_other.go @@ -0,0 +1,14 @@ +//go:build !windows +// +build !windows + +package tea + +import ( + "io" + + "github.com/muesli/cancelreader" +) + +func newInputReader(r io.Reader) (cancelreader.CancelReader, error) { + return cancelreader.NewReader(r) +} diff --git a/inputreader_windows.go b/inputreader_windows.go new file mode 100644 index 0000000000..8daa6e60c8 --- /dev/null +++ b/inputreader_windows.go @@ -0,0 +1,154 @@ +//go:build windows +// +build windows + +package tea + +import ( + "fmt" + "io" + "os" + "sync" + + "github.com/erikgeiser/coninput" + "github.com/muesli/cancelreader" + "golang.org/x/sys/windows" +) + +type conInputReader struct { + cancelMixin + + conin windows.Handle + cancelEvent windows.Handle + + originalMode uint32 + + // inputEvent holds the input event that was read in order to avoid + // unneccessary allocations. This re-use is possible because + // InputRecord.Unwarp which is called inparseInputMsgFromInputRecord + // returns an data structure that is independent of the passed InputRecord. + inputEvent []coninput.InputRecord +} + +var _ cancelreader.CancelReader = &conInputReader{} + +func newInputReader(r io.Reader) (cancelreader.CancelReader, error) { + fallback := func(io.Reader) (cancelreader.CancelReader, error) { + return cancelreader.NewReader(r) + } + if f, ok := r.(*os.File); !ok || f.Fd() != os.Stdin.Fd() { + return fallback(r) + } + + conin, err := coninput.NewStdinHandle() + if err != nil { + return fallback(r) + } + + originalMode, err := prepareConsole(conin, + windows.ENABLE_MOUSE_INPUT, + windows.ENABLE_WINDOW_INPUT, + windows.ENABLE_PROCESSED_INPUT, + windows.ENABLE_EXTENDED_FLAGS, + ) + if err != nil { + return nil, fmt.Errorf("failed to prepare console input: %w", err) + } + + cancelEvent, err := windows.CreateEvent(nil, 0, 0, nil) + if err != nil { + return nil, fmt.Errorf("create stop event: %w", err) + } + + return &conInputReader{ + conin: conin, + cancelEvent: cancelEvent, + originalMode: originalMode, + }, nil +} + +// Cancel implements cancelreader.CancelReader. +func (r *conInputReader) Cancel() bool { + r.setCanceled() + + err := windows.SetEvent(r.cancelEvent) + if err != nil { + return false + } + + return true +} + +// Close implements cancelreader.CancelReader. +func (r *conInputReader) Close() error { + err := windows.CloseHandle(r.cancelEvent) + if err != nil { + return fmt.Errorf("closing cancel event handle: %w", err) + } + + if r.originalMode != 0 { + err := windows.SetConsoleMode(r.conin, r.originalMode) + if err != nil { + return fmt.Errorf("reset console mode: %w", err) + } + } + + return nil +} + +// Read implements cancelreader.CancelReader. +func (*conInputReader) Read(_ []byte) (n int, err error) { + return 0, nil +} + +func prepareConsole(input windows.Handle, modes ...uint32) (originalMode uint32, err error) { + err = windows.GetConsoleMode(input, &originalMode) + if err != nil { + return 0, fmt.Errorf("get console mode: %w", err) + } + + newMode := coninput.AddInputModes(0, modes...) + + err = windows.SetConsoleMode(input, newMode) + if err != nil { + return 0, fmt.Errorf("set console mode: %w", err) + } + + return originalMode, nil +} + +func waitForInput(conin, cancel windows.Handle) error { + event, err := windows.WaitForMultipleObjects([]windows.Handle{conin, cancel}, false, windows.INFINITE) + switch { + case windows.WAIT_OBJECT_0 <= event && event < windows.WAIT_OBJECT_0+2: + if event == windows.WAIT_OBJECT_0+1 { + return cancelreader.ErrCanceled + } + + if event == windows.WAIT_OBJECT_0 { + return nil + } + + return fmt.Errorf("unexpected wait object is ready: %d", event-windows.WAIT_OBJECT_0) + case windows.WAIT_ABANDONED <= event && event < windows.WAIT_ABANDONED+2: + return fmt.Errorf("abandoned") + case event == uint32(windows.WAIT_TIMEOUT): + return fmt.Errorf("timeout") + case event == windows.WAIT_FAILED: + return fmt.Errorf("failed") + default: + return fmt.Errorf("unexpected error: %w", err) + } +} + +// cancelMixin represents a goroutine-safe cancelation status. +type cancelMixin struct { + unsafeCanceled bool + lock sync.Mutex +} + +func (c *cancelMixin) setCanceled() { + c.lock.Lock() + defer c.lock.Unlock() + + c.unsafeCanceled = true +} diff --git a/key.go b/key.go index f851490a0d..0ebe7c9b7e 100644 --- a/key.go +++ b/key.go @@ -538,9 +538,9 @@ func (u unknownCSISequenceMsg) String() string { var spaceRunes = []rune{' '} -// readInputs reads keypress and mouse inputs from a TTY and produces messages +// readAnsiInputs reads keypress and mouse inputs from a TTY and produces messages // containing information about the key or mouse events accordingly. -func readInputs(ctx context.Context, msgs chan<- Msg, input io.Reader) error { +func readAnsiInputs(ctx context.Context, msgs chan<- Msg, input io.Reader) error { var buf [256]byte var leftOverFromPrevIteration []byte diff --git a/key_other.go b/key_other.go new file mode 100644 index 0000000000..b8c46082f8 --- /dev/null +++ b/key_other.go @@ -0,0 +1,13 @@ +//go:build !windows +// +build !windows + +package tea + +import ( + "context" + "io" +) + +func readInputs(ctx context.Context, msgs chan<- Msg, input io.Reader) error { + return readAnsiInputs(ctx, msgs, input) +} diff --git a/key_test.go b/key_test.go index 0b1112a222..042ade1ba0 100644 --- a/key_test.go +++ b/key_test.go @@ -526,7 +526,7 @@ func testReadInputs(t *testing.T, input io.Reader) []Msg { wg.Add(1) go func() { defer wg.Done() - inputErr = readInputs(ctx, msgsC, input) + inputErr = readAnsiInputs(ctx, msgsC, input) msgsC <- nil }() diff --git a/key_windows.go b/key_windows.go new file mode 100644 index 0000000000..1fa46506aa --- /dev/null +++ b/key_windows.go @@ -0,0 +1,259 @@ +//go:build windows +// +build windows + +package tea + +import ( + "context" + "fmt" + "io" + + "github.com/erikgeiser/coninput" + localereader "github.com/mattn/go-localereader" + "golang.org/x/sys/windows" +) + +func readInputs(ctx context.Context, msgs chan<- Msg, input io.Reader) error { + if coninReader, ok := input.(*conInputReader); ok { + return readConInputs(ctx, msgs, coninReader.conin) + } + + return readAnsiInputs(ctx, msgs, localereader.NewReader(input)) +} + +func readConInputs(ctx context.Context, msgsch chan<- Msg, con windows.Handle) error { + var ps coninput.ButtonState // keep track of previous mouse state + for { + events, err := coninput.ReadNConsoleInputs(con, 16) + if err != nil { + return fmt.Errorf("read coninput events: %w", err) + } + + for _, event := range events { + var msgs []Msg + switch e := event.Unwrap().(type) { + case coninput.KeyEventRecord: + if !e.KeyDown || e.VirtualKeyCode == coninput.VK_SHIFT { + continue + } + + for i := 0; i < int(e.RepeatCount); i++ { + msgs = append(msgs, KeyMsg{ + Type: keyType(e), + Runes: []rune{e.Char}, + Alt: e.ControlKeyState.Contains(coninput.LEFT_ALT_PRESSED | coninput.RIGHT_ALT_PRESSED), + }) + } + case coninput.WindowBufferSizeEventRecord: + msgs = append(msgs, WindowSizeMsg{ + Width: int(e.Size.X), + Height: int(e.Size.Y), + }) + case coninput.MouseEventRecord: + event := mouseEvent(ps, e) + msgs = append(msgs, event) + ps = e.ButtonState + case coninput.FocusEventRecord, coninput.MenuEventRecord: + // ignore + default: // unknown event + continue + } + + // Send all messages to the channel + for _, msg := range msgs { + select { + case msgsch <- msg: + case <-ctx.Done(): + err := ctx.Err() + if err != nil { + return fmt.Errorf("coninput context error: %w", err) + } + return err + } + } + } + } +} + +func mouseEventButton(p, s coninput.ButtonState) (button MouseButton, action MouseAction) { + btn := p ^ s + action = MouseActionPress + if btn&s == 0 { + action = MouseActionRelease + } + + switch btn { + case coninput.FROM_LEFT_1ST_BUTTON_PRESSED: // left button + button = MouseButtonLeft + case coninput.RIGHTMOST_BUTTON_PRESSED: // right button + button = MouseButtonRight + case coninput.FROM_LEFT_2ND_BUTTON_PRESSED: // middle button + button = MouseButtonMiddle + case coninput.FROM_LEFT_3RD_BUTTON_PRESSED: // unknown (possibly mouse backward) + button = MouseButtonBackward + case coninput.FROM_LEFT_4TH_BUTTON_PRESSED: // unknown (possibly mouse forward) + button = MouseButtonForward + } + + return button, action +} + +func mouseEvent(p coninput.ButtonState, e coninput.MouseEventRecord) MouseMsg { + ev := MouseMsg{ + X: int(e.MousePositon.X), + Y: int(e.MousePositon.Y), + Alt: e.ControlKeyState.Contains(coninput.LEFT_ALT_PRESSED | coninput.RIGHT_ALT_PRESSED), + Ctrl: e.ControlKeyState.Contains(coninput.LEFT_CTRL_PRESSED | coninput.RIGHT_CTRL_PRESSED), + Shift: e.ControlKeyState.Contains(coninput.SHIFT_PRESSED), + } + switch e.EventFlags { + case coninput.CLICK, coninput.DOUBLE_CLICK: + ev.Button, ev.Action = mouseEventButton(p, e.ButtonState) + if ev.Action == MouseActionRelease { + ev.Type = MouseRelease + } + switch ev.Button { + case MouseButtonLeft: + ev.Type = MouseLeft + case MouseButtonMiddle: + ev.Type = MouseMiddle + case MouseButtonRight: + ev.Type = MouseRight + case MouseButtonBackward: + ev.Type = MouseBackward + case MouseButtonForward: + ev.Type = MouseForward + } + case coninput.MOUSE_WHEELED: + if e.WheelDirection > 0 { + ev.Button = MouseButtonWheelUp + ev.Type = MouseWheelUp + } else { + ev.Button = MouseButtonWheelDown + ev.Type = MouseWheelDown + } + case coninput.MOUSE_HWHEELED: + if e.WheelDirection > 0 { + ev.Button = MouseButtonWheelRight + ev.Type = MouseWheelRight + } else { + ev.Button = MouseButtonWheelLeft + ev.Type = MouseWheelLeft + } + case coninput.MOUSE_MOVED: + ev.Button, _ = mouseEventButton(0, e.ButtonState) + ev.Action = MouseActionMotion + ev.Type = MouseMotion + } + + return ev +} + +func keyType(e coninput.KeyEventRecord) KeyType { + code := e.VirtualKeyCode + + switch code { + case coninput.VK_RETURN: + return KeyEnter + case coninput.VK_BACK: + return KeyBackspace + case coninput.VK_TAB: + return KeyTab + case coninput.VK_SPACE: + return KeyRunes // this could be KeySpace but on unix space also produces KeyRunes + case coninput.VK_ESCAPE: + return KeyEscape + case coninput.VK_UP: + return KeyUp + case coninput.VK_DOWN: + return KeyDown + case coninput.VK_RIGHT: + return KeyRight + case coninput.VK_LEFT: + return KeyLeft + case coninput.VK_HOME: + return KeyHome + case coninput.VK_END: + return KeyEnd + case coninput.VK_PRIOR: + return KeyPgUp + case coninput.VK_NEXT: + return KeyPgDown + case coninput.VK_DELETE: + return KeyDelete + default: + if e.ControlKeyState&(coninput.LEFT_CTRL_PRESSED|coninput.RIGHT_CTRL_PRESSED) == 0 { + return KeyRunes + } + + switch e.Char { + case '@': + return KeyCtrlAt + case '\x01': + return KeyCtrlA + case '\x02': + return KeyCtrlB + case '\x03': + return KeyCtrlC + case '\x04': + return KeyCtrlD + case '\x05': + return KeyCtrlE + case '\x06': + return KeyCtrlF + case '\a': + return KeyCtrlG + case '\b': + return KeyCtrlH + case '\t': + return KeyCtrlI + case '\n': + return KeyCtrlJ + case '\v': + return KeyCtrlK + case '\f': + return KeyCtrlL + case '\r': + return KeyCtrlM + case '\x0e': + return KeyCtrlN + case '\x0f': + return KeyCtrlO + case '\x10': + return KeyCtrlP + case '\x11': + return KeyCtrlQ + case '\x12': + return KeyCtrlR + case '\x13': + return KeyCtrlS + case '\x14': + return KeyCtrlT + case '\x15': + return KeyCtrlU + case '\x16': + return KeyCtrlV + case '\x17': + return KeyCtrlW + case '\x18': + return KeyCtrlX + case '\x19': + return KeyCtrlY + case '\x1a': + return KeyCtrlZ + case '\x1b': + return KeyCtrlCloseBracket + case '\x1c': + return KeyCtrlBackslash + case '\x1f': + return KeyCtrlUnderscore + } + + switch code { + case coninput.VK_OEM_4: + return KeyCtrlOpenBracket + } + + return KeyRunes + } +} diff --git a/tty.go b/tty.go index 01f084d438..5e1bf0ce7d 100644 --- a/tty.go +++ b/tty.go @@ -8,7 +8,6 @@ import ( "time" isatty "github.com/mattn/go-isatty" - localereader "github.com/mattn/go-localereader" "github.com/muesli/cancelreader" "golang.org/x/term" ) @@ -58,7 +57,7 @@ func (p *Program) restoreTerminalState() error { // initCancelReader (re)commences reading inputs. func (p *Program) initCancelReader() error { var err error - p.cancelReader, err = cancelreader.NewReader(p.input) + p.cancelReader, err = newInputReader(p.input) if err != nil { return fmt.Errorf("error creating cancelreader: %w", err) } @@ -72,8 +71,7 @@ func (p *Program) initCancelReader() error { func (p *Program) readLoop() { defer close(p.readLoopDone) - input := localereader.NewReader(p.cancelReader) - err := readInputs(p.ctx, p.msgs, input) + err := readInputs(p.ctx, p.msgs, p.cancelReader) if !errors.Is(err, io.EOF) && !errors.Is(err, cancelreader.ErrCanceled) { select { case <-p.ctx.Done(): diff --git a/tutorials/go.mod b/tutorials/go.mod index 085b9d4560..db5897e9db 100644 --- a/tutorials/go.mod +++ b/tutorials/go.mod @@ -7,6 +7,7 @@ require github.com/charmbracelet/bubbletea v0.23.2 require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.18 // indirect github.com/mattn/go-localereader v0.0.1 // indirect diff --git a/tutorials/go.sum b/tutorials/go.sum index 9bc66d6357..ce379f59c4 100644 --- a/tutorials/go.sum +++ b/tutorials/go.sum @@ -2,6 +2,8 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= @@ -37,6 +39,7 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=