forked from Mirrors/bubbletea
155 lines
3.6 KiB
Go
155 lines
3.6 KiB
Go
//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
|
|
}
|