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}