cancelreader_windows.go

  1//go:build windows
  2// +build windows
  3
  4package uv
  5
  6import (
  7	"fmt"
  8	"io"
  9	"os"
 10	"sync"
 11
 12	xwindows "github.com/charmbracelet/x/windows"
 13	"github.com/muesli/cancelreader"
 14	"golang.org/x/sys/windows"
 15)
 16
 17type conInputReader struct {
 18	cancelMixin
 19	conin        windows.Handle
 20	originalMode uint32
 21}
 22
 23var _ cancelreader.CancelReader = &conInputReader{}
 24
 25func newCancelreader(r io.Reader) (cancelreader.CancelReader, error) {
 26	fallback := func(io.Reader) (cancelreader.CancelReader, error) {
 27		return cancelreader.NewReader(r)
 28	}
 29
 30	var dummy uint32
 31	if f, ok := r.(cancelreader.File); !ok || f.Fd() != os.Stdin.Fd() ||
 32		// If data was piped to the standard input, it does not emit events
 33		// anymore. We can detect this if the console mode cannot be set anymore,
 34		// in this case, we fallback to the default cancelreader implementation.
 35		windows.GetConsoleMode(windows.Handle(f.Fd()), &dummy) != nil {
 36		return fallback(r)
 37	}
 38
 39	conin, err := windows.GetStdHandle(windows.STD_INPUT_HANDLE)
 40	if err != nil {
 41		return fallback(r)
 42	}
 43
 44	// Discard any pending input events.
 45	if err := xwindows.FlushConsoleInputBuffer(conin); err != nil {
 46		return fallback(r)
 47	}
 48
 49	modes := []uint32{
 50		windows.ENABLE_WINDOW_INPUT,
 51		windows.ENABLE_EXTENDED_FLAGS,
 52	}
 53
 54	originalMode, err := prepareConsole(conin, modes...)
 55	if err != nil {
 56		return nil, fmt.Errorf("failed to prepare console input: %w", err)
 57	}
 58
 59	return &conInputReader{
 60		conin:        conin,
 61		originalMode: originalMode,
 62	}, nil
 63}
 64
 65// Cancel implements cancelreader.CancelReader.
 66func (r *conInputReader) Cancel() bool {
 67	r.setCanceled()
 68
 69	return windows.CancelIoEx(r.conin, nil) == nil || windows.CancelIo(r.conin) == nil
 70}
 71
 72// Close implements cancelreader.CancelReader.
 73func (r *conInputReader) Close() error {
 74	if r.originalMode != 0 {
 75		err := windows.SetConsoleMode(r.conin, r.originalMode)
 76		if err != nil {
 77			return fmt.Errorf("reset console mode: %w", err)
 78		}
 79	}
 80
 81	return nil
 82}
 83
 84// Read implements cancelreader.CancelReader.
 85func (r *conInputReader) Read(data []byte) (int, error) {
 86	if r.isCanceled() {
 87		return 0, cancelreader.ErrCanceled
 88	}
 89
 90	var n uint32
 91	if err := windows.ReadFile(r.conin, data, &n, nil); err != nil {
 92		return int(n), fmt.Errorf("read console input: %w", err)
 93	}
 94
 95	return int(n), nil
 96}
 97
 98func prepareConsole(input windows.Handle, modes ...uint32) (originalMode uint32, err error) {
 99	err = windows.GetConsoleMode(input, &originalMode)
100	if err != nil {
101		return 0, fmt.Errorf("get console mode: %w", err)
102	}
103
104	var newMode uint32
105	for _, mode := range modes {
106		newMode |= mode
107	}
108
109	err = windows.SetConsoleMode(input, newMode)
110	if err != nil {
111		return 0, fmt.Errorf("set console mode: %w", err)
112	}
113
114	return originalMode, nil
115}
116
117// cancelMixin represents a goroutine-safe cancelation status.
118type cancelMixin struct {
119	unsafeCanceled bool
120	lock           sync.Mutex
121}
122
123func (c *cancelMixin) setCanceled() {
124	c.lock.Lock()
125	defer c.lock.Unlock()
126
127	c.unsafeCanceled = true
128}
129
130func (c *cancelMixin) isCanceled() bool {
131	c.lock.Lock()
132	defer c.lock.Unlock()
133
134	return c.unsafeCanceled
135}