opencode/packages/tui/input/cancelreader_windows.go

143 lines
3.3 KiB
Go

//go:build windows
// +build windows
package input
import (
"fmt"
"io"
"os"
"sync"
xwindows "github.com/charmbracelet/x/windows"
"github.com/muesli/cancelreader"
"golang.org/x/sys/windows"
)
type conInputReader struct {
cancelMixin
conin windows.Handle
originalMode uint32
}
var _ cancelreader.CancelReader = &conInputReader{}
func newCancelreader(r io.Reader, flags int) (cancelreader.CancelReader, error) {
fallback := func(io.Reader) (cancelreader.CancelReader, error) {
return cancelreader.NewReader(r)
}
var dummy uint32
if f, ok := r.(cancelreader.File); !ok || f.Fd() != os.Stdin.Fd() ||
// If data was piped to the standard input, it does not emit events
// anymore. We can detect this if the console mode cannot be set anymore,
// in this case, we fallback to the default cancelreader implementation.
windows.GetConsoleMode(windows.Handle(f.Fd()), &dummy) != nil {
return fallback(r)
}
conin, err := windows.GetStdHandle(windows.STD_INPUT_HANDLE)
if err != nil {
return fallback(r)
}
// Discard any pending input events.
if err := xwindows.FlushConsoleInputBuffer(conin); err != nil {
return fallback(r)
}
modes := []uint32{
windows.ENABLE_WINDOW_INPUT,
windows.ENABLE_EXTENDED_FLAGS,
}
// Enabling mouse mode implicitly blocks console text selection. Thus, we
// need to enable it only if the mouse mode is requested.
// In order to toggle mouse mode, the caller must recreate the reader with
// the appropriate flag toggled.
if flags&FlagMouseMode != 0 {
modes = append(modes, windows.ENABLE_MOUSE_INPUT)
}
originalMode, err := prepareConsole(conin, modes...)
if err != nil {
return nil, fmt.Errorf("failed to prepare console input: %w", err)
}
return &conInputReader{
conin: conin,
originalMode: originalMode,
}, nil
}
// Cancel implements cancelreader.CancelReader.
func (r *conInputReader) Cancel() bool {
r.setCanceled()
return windows.CancelIoEx(r.conin, nil) == nil || windows.CancelIo(r.conin) == nil
}
// Close implements cancelreader.CancelReader.
func (r *conInputReader) Close() error {
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 (r *conInputReader) Read(data []byte) (int, error) {
if r.isCanceled() {
return 0, cancelreader.ErrCanceled
}
var n uint32
if err := windows.ReadFile(r.conin, data, &n, nil); err != nil {
return int(n), fmt.Errorf("read console input: %w", err)
}
return int(n), 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)
}
var newMode uint32
for _, mode := range modes {
newMode |= mode
}
err = windows.SetConsoleMode(input, newMode)
if err != nil {
return 0, fmt.Errorf("set console mode: %w", err)
}
return originalMode, nil
}
// 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
}
func (c *cancelMixin) isCanceled() bool {
c.lock.Lock()
defer c.lock.Unlock()
return c.unsafeCanceled
}