forked from Mirrors/bubbletea
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
This commit is contained in:
parent
b6695477b4
commit
9563bafdf5
|
@ -25,6 +25,7 @@ require (
|
||||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
|
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
|
||||||
github.com/dlclark/regexp2 v1.4.0 // indirect
|
github.com/dlclark/regexp2 v1.4.0 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // 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/gorilla/css v1.0.0 // indirect
|
||||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||||
|
|
|
@ -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/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 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
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 h1:VRIbnDWRmAh5yBdz+J6yFMF5vso1It6vn+WmM/5l7MA=
|
||||||
github.com/fogleman/ease v0.0.0-20170301025033-8da417bf1776/go.mod h1:9wvnDu3YOfxzWM9Cst40msBF1C2UdQgDv962oTxSuMs=
|
github.com/fogleman/ease v0.0.0-20170301025033-8da417bf1776/go.mod h1:9wvnDu3YOfxzWM9Cst40msBF1C2UdQgDv962oTxSuMs=
|
||||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
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-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-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-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-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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
|
3
go.mod
3
go.mod
|
@ -4,6 +4,7 @@ go 1.17
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81
|
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-isatty v0.0.18
|
||||||
github.com/mattn/go-localereader v0.0.1
|
github.com/mattn/go-localereader v0.0.1
|
||||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b
|
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b
|
||||||
|
@ -11,6 +12,7 @@ require (
|
||||||
github.com/muesli/reflow v0.3.0
|
github.com/muesli/reflow v0.3.0
|
||||||
github.com/muesli/termenv v0.15.2
|
github.com/muesli/termenv v0.15.2
|
||||||
golang.org/x/sync v0.1.0
|
golang.org/x/sync v0.1.0
|
||||||
|
golang.org/x/sys v0.7.0
|
||||||
golang.org/x/term v0.6.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/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||||
github.com/rivo/uniseg v0.2.0 // 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
|
golang.org/x/text v0.3.8 // indirect
|
||||||
)
|
)
|
||||||
|
|
3
go.sum
3
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/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 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
|
||||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
|
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 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
|
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-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-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-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-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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
key.go
4
key.go
|
@ -538,9 +538,9 @@ func (u unknownCSISequenceMsg) String() string {
|
||||||
|
|
||||||
var spaceRunes = []rune{' '}
|
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.
|
// 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 buf [256]byte
|
||||||
|
|
||||||
var leftOverFromPrevIteration []byte
|
var leftOverFromPrevIteration []byte
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -526,7 +526,7 @@ func testReadInputs(t *testing.T, input io.Reader) []Msg {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
inputErr = readInputs(ctx, msgsC, input)
|
inputErr = readAnsiInputs(ctx, msgsC, input)
|
||||||
msgsC <- nil
|
msgsC <- nil
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
6
tty.go
6
tty.go
|
@ -8,7 +8,6 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
isatty "github.com/mattn/go-isatty"
|
isatty "github.com/mattn/go-isatty"
|
||||||
localereader "github.com/mattn/go-localereader"
|
|
||||||
"github.com/muesli/cancelreader"
|
"github.com/muesli/cancelreader"
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
@ -58,7 +57,7 @@ func (p *Program) restoreTerminalState() error {
|
||||||
// initCancelReader (re)commences reading inputs.
|
// initCancelReader (re)commences reading inputs.
|
||||||
func (p *Program) initCancelReader() error {
|
func (p *Program) initCancelReader() error {
|
||||||
var err error
|
var err error
|
||||||
p.cancelReader, err = cancelreader.NewReader(p.input)
|
p.cancelReader, err = newInputReader(p.input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error creating cancelreader: %w", err)
|
return fmt.Errorf("error creating cancelreader: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -72,8 +71,7 @@ func (p *Program) initCancelReader() error {
|
||||||
func (p *Program) readLoop() {
|
func (p *Program) readLoop() {
|
||||||
defer close(p.readLoopDone)
|
defer close(p.readLoopDone)
|
||||||
|
|
||||||
input := localereader.NewReader(p.cancelReader)
|
err := readInputs(p.ctx, p.msgs, p.cancelReader)
|
||||||
err := readInputs(p.ctx, p.msgs, input)
|
|
||||||
if !errors.Is(err, io.EOF) && !errors.Is(err, cancelreader.ErrCanceled) {
|
if !errors.Is(err, io.EOF) && !errors.Is(err, cancelreader.ErrCanceled) {
|
||||||
select {
|
select {
|
||||||
case <-p.ctx.Done():
|
case <-p.ctx.Done():
|
||||||
|
|
|
@ -7,6 +7,7 @@ require github.com/charmbracelet/bubbletea v0.23.2
|
||||||
require (
|
require (
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // 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/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.18 // indirect
|
github.com/mattn/go-isatty v0.0.18 // indirect
|
||||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
|
|
|
@ -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/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 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
|
||||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
|
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 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
|
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-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-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-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-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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
|
Loading…
Reference in New Issue