forked from Mirrors/bubbletea
240 lines
6.2 KiB
Go
240 lines
6.2 KiB
Go
//go:build windows
|
|
// +build windows
|
|
|
|
package tea
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"syscall"
|
|
"time"
|
|
"unicode/utf16"
|
|
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
var fileShareValidFlags uint32 = 0x00000007
|
|
|
|
// newCancelReader returns a reader and a cancel function. If the input reader
|
|
// is an *os.File with the same file descriptor as os.Stdin, the cancel function
|
|
// can be used to interrupt a blocking call read call. In this case, the cancel
|
|
// function returns true if the call was cancelled successfully. If the input
|
|
// reader is not a *os.File with the same file descriptor as os.Stdin, the
|
|
// cancel function does nothing and always returns false. The Windows
|
|
// implementation is based on WaitForMultipleObject with overlapping reads from
|
|
// CONIN$.
|
|
func newCancelReader(reader io.Reader) (cancelReader, error) {
|
|
if f, ok := reader.(*os.File); !ok || f.Fd() != os.Stdin.Fd() {
|
|
return newFallbackCancelReader(reader)
|
|
}
|
|
|
|
// it is neccessary to open CONIN$ (NOT windows.STD_INPUT_HANDLE) in
|
|
// overlapped mode to be able to use it with WaitForMultipleObjects.
|
|
conin, err := windows.CreateFile(
|
|
&(utf16.Encode([]rune("CONIN$\x00"))[0]), windows.GENERIC_READ|windows.GENERIC_WRITE,
|
|
fileShareValidFlags, nil, windows.OPEN_EXISTING, windows.FILE_FLAG_OVERLAPPED, 0)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open CONIN$ in overlapping mode: %w", err)
|
|
}
|
|
|
|
resetConsole, err := prepareConsole(conin)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("prepare console: %w", err)
|
|
}
|
|
|
|
// flush input, otherwise it can contain events which trigger
|
|
// WaitForMultipleObjects but which ReadFile cannot read, resulting in an
|
|
// un-cancelable read
|
|
err = flushConsoleInputBuffer(conin)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("flush console input buffer: %w", err)
|
|
}
|
|
|
|
cancelEvent, err := windows.CreateEvent(nil, 0, 0, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create stop event: %w", err)
|
|
}
|
|
|
|
return &winCancelReader{
|
|
conin: conin,
|
|
cancelEvent: cancelEvent,
|
|
resetConsole: resetConsole,
|
|
blockingReadSignal: make(chan struct{}, 1),
|
|
}, nil
|
|
}
|
|
|
|
type winCancelReader struct {
|
|
conin windows.Handle
|
|
cancelEvent windows.Handle
|
|
cancelMixin
|
|
|
|
resetConsole func() error
|
|
blockingReadSignal chan struct{}
|
|
}
|
|
|
|
func (r *winCancelReader) Read(data []byte) (int, error) {
|
|
if r.isCancelled() {
|
|
return 0, errCanceled
|
|
}
|
|
|
|
err := r.wait()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
if r.isCancelled() {
|
|
return 0, errCanceled
|
|
}
|
|
|
|
// windows.Read does not work on overlapping windows.Handles
|
|
return r.readAsync(data)
|
|
}
|
|
|
|
// Cancel cancels ongoing and future Read() calls and returns true if the
|
|
// cancelation of the ongoing Read() was successful. On Windows Terminal,
|
|
// WaitForMultipleObjects sometimes immediately returns without input being
|
|
// available. In this case, graceful cancelation is not possible and Cancel()
|
|
// returns false.
|
|
func (r *winCancelReader) Cancel() bool {
|
|
r.setCancelled()
|
|
|
|
select {
|
|
case r.blockingReadSignal <- struct{}{}:
|
|
err := windows.SetEvent(r.cancelEvent)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
<-r.blockingReadSignal
|
|
case <-time.After(100 * time.Millisecond):
|
|
// Read() hangs in a GetOverlappedResult which is likely due to
|
|
// WaitForMultipleObjects returning without input being available
|
|
// so we cannot cancel this ongoing read.
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (r *winCancelReader) Close() error {
|
|
err := windows.CloseHandle(r.cancelEvent)
|
|
if err != nil {
|
|
return fmt.Errorf("closing cancel event handle: %w", err)
|
|
}
|
|
|
|
err = r.resetConsole()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = windows.Close(r.conin)
|
|
if err != nil {
|
|
return fmt.Errorf("closing CONIN$")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *winCancelReader) wait() error {
|
|
event, err := windows.WaitForMultipleObjects([]windows.Handle{r.conin, r.cancelEvent}, false, windows.INFINITE)
|
|
switch {
|
|
case windows.WAIT_OBJECT_0 <= event && event < windows.WAIT_OBJECT_0+2:
|
|
if event == windows.WAIT_OBJECT_0+1 {
|
|
return 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", error(err))
|
|
}
|
|
}
|
|
|
|
// readAsync is neccessary to read from a windows.Handle in overlapping mode.
|
|
func (r *winCancelReader) readAsync(data []byte) (int, error) {
|
|
hevent, err := windows.CreateEvent(nil, 0, 0, nil)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("create event: %w", err)
|
|
}
|
|
|
|
overlapped := windows.Overlapped{
|
|
HEvent: hevent,
|
|
}
|
|
|
|
var n uint32
|
|
|
|
err = windows.ReadFile(r.conin, data, &n, &overlapped)
|
|
if err != nil && err != windows.ERROR_IO_PENDING {
|
|
return int(n), err
|
|
}
|
|
|
|
r.blockingReadSignal <- struct{}{}
|
|
err = windows.GetOverlappedResult(r.conin, &overlapped, &n, true)
|
|
if err != nil {
|
|
return int(n), nil
|
|
}
|
|
<-r.blockingReadSignal
|
|
|
|
return int(n), nil
|
|
}
|
|
|
|
func prepareConsole(input windows.Handle) (reset func() error, err error) {
|
|
var originalMode uint32
|
|
|
|
err = windows.GetConsoleMode(input, &originalMode)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get console mode: %w", err)
|
|
}
|
|
|
|
var newMode uint32
|
|
newMode &^= windows.ENABLE_ECHO_INPUT
|
|
newMode &^= windows.ENABLE_LINE_INPUT
|
|
newMode &^= windows.ENABLE_MOUSE_INPUT
|
|
newMode &^= windows.ENABLE_WINDOW_INPUT
|
|
newMode &^= windows.ENABLE_PROCESSED_INPUT
|
|
|
|
newMode |= windows.ENABLE_EXTENDED_FLAGS
|
|
newMode |= windows.ENABLE_INSERT_MODE
|
|
newMode |= windows.ENABLE_QUICK_EDIT_MODE
|
|
|
|
newMode &^= windows.ENABLE_VIRTUAL_TERMINAL_INPUT
|
|
|
|
err = windows.SetConsoleMode(input, newMode)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("set console mode: %w", err)
|
|
}
|
|
|
|
return func() error {
|
|
err := windows.SetConsoleMode(input, originalMode)
|
|
if err != nil {
|
|
return fmt.Errorf("reset console mode: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}, nil
|
|
}
|
|
|
|
var (
|
|
modkernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
|
procFlushConsoleInputBuffer = modkernel32.NewProc("FlushConsoleInputBuffer")
|
|
)
|
|
|
|
func flushConsoleInputBuffer(consoleInput windows.Handle) error {
|
|
r, _, e := syscall.Syscall(procFlushConsoleInputBuffer.Addr(), 1,
|
|
uintptr(consoleInput), 0, 0)
|
|
if r == 0 {
|
|
return error(e)
|
|
}
|
|
|
|
return nil
|
|
}
|