Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support windows console input buffer #878

Merged
merged 1 commit into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions examples/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,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=
Expand Down Expand Up @@ -80,6 +82,7 @@ golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
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-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ go 1.18

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.20
github.com/mattn/go-localereader v0.0.1
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b
github.com/muesli/cancelreader v0.2.2
github.com/muesli/reflow v0.3.0
github.com/muesli/termenv v0.15.2
golang.org/x/sync v0.5.0
golang.org/x/sys v0.16.0
golang.org/x/term v0.16.0
)

Expand All @@ -19,6 +21,5 @@ require (
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/rivo/uniseg v0.4.6 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.3.8 // indirect
)
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
Expand All @@ -25,6 +27,7 @@ github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg=
github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
Expand Down
14 changes: 14 additions & 0 deletions inputreader_other.go
Original file line number Diff line number Diff line change
@@ -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)

Check failure on line 13 in inputreader_other.go

View workflow job for this annotation

GitHub Actions / lint-soft

error returned from external package is unwrapped: sig: func github.com/muesli/cancelreader.NewReader(reader io.Reader) (github.com/muesli/cancelreader.CancelReader, error) (wrapcheck)

Check failure on line 13 in inputreader_other.go

View workflow job for this annotation

GitHub Actions / lint-soft

error returned from external package is unwrapped: sig: func github.com/muesli/cancelreader.NewReader(reader io.Reader) (github.com/muesli/cancelreader.CancelReader, error) (wrapcheck)
}
154 changes: 154 additions & 0 deletions inputreader_windows.go
Original file line number Diff line number Diff line change
@@ -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
}
4 changes: 2 additions & 2 deletions key.go
Original file line number Diff line number Diff line change
Expand Up @@ -553,9 +553,9 @@

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
Expand Down Expand Up @@ -622,7 +622,7 @@
case '<':
if matchIndices := mouseSGRRegex.FindSubmatchIndex(b[3:]); matchIndices != nil {
// SGR mouse events length is the length of the match plus the length of the escape sequence
mouseEventSGRLen := matchIndices[1] + 3

Check failure on line 625 in key.go

View workflow job for this annotation

GitHub Actions / lint-soft

mnd: Magic number: 3, in <operation> detected (gomnd)
return mouseEventSGRLen, MouseMsg(parseSGRMouseEvent(b))
}
}
Expand All @@ -632,7 +632,7 @@
var foundbp bool
foundbp, w, msg = detectBracketedPaste(b)
if foundbp {
return

Check failure on line 635 in key.go

View workflow job for this annotation

GitHub Actions / lint-soft

naked return in func `detectOneMsg` with 94 lines of code (nakedret)
}

// Detect escape sequence and control characters other than NUL,
Expand All @@ -641,7 +641,7 @@
var foundSeq bool
foundSeq, w, msg = detectSequence(b)
if foundSeq {
return

Check failure on line 644 in key.go

View workflow job for this annotation

GitHub Actions / lint-soft

naked return in func `detectOneMsg` with 94 lines of code (nakedret)
}

// No non-NUL control character or escape sequence.
Expand Down
13 changes: 13 additions & 0 deletions key_other.go
Original file line number Diff line number Diff line change
@@ -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)
}
2 changes: 1 addition & 1 deletion key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,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
}()

Expand Down