1//go:build windows
2
3package tui
4
5import (
6 "encoding/binary"
7 "os"
8 "time"
9 "unsafe"
10
11 "golang.org/x/sys/windows"
12)
13
14// Windows doesn't have SIGWINCH; resize events are signaled via input
15// records. For v1 we just ignore resize and rely on the next render
16// cycle to pick up the new size via term.GetSize.
17func (p *ProcTerm) installResizeHandler() {}
18
19func (p *ProcTerm) SetNonblock(enable bool) error { return nil }
20
21// peekStdin waits for one byte on a supported Windows stdin handle without
22// consuming anything unless a byte is available. Console input handles can be
23// waited on directly. Pipes are checked with PeekNamedPipe, which does not
24// consume data; polling that non-consuming check keeps the wait bounded.
25//
26// Disk files and other character handles are deliberately unsupported. A
27// speculative os.File.Read on either can block (or consume a byte that cannot
28// be put back safely), so those handles report no data instead of corrupting
29// the escape parser. In particular, do not replace this with a blocking Read
30// fallback: an isolated Esc must remain observable within d.
31func peekStdin(in *os.File, d time.Duration) (byte, bool, error) {
32 handle := windows.Handle(in.Fd())
33 fileType, err := windows.GetFileType(handle)
34 if err != nil {
35 return 0, false, err
36 }
37
38 switch fileType {
39 case windows.FILE_TYPE_CHAR:
40 // FILE_TYPE_CHAR also includes devices such as NUL. GetConsoleMode
41 // distinguishes an actual console input handle from those devices.
42 var mode uint32
43 if err := windows.GetConsoleMode(handle, &mode); err != nil {
44 return 0, false, windows.ERROR_INVALID_FUNCTION
45 }
46 return peekConsole(in, handle, d)
47 case windows.FILE_TYPE_PIPE:
48 return peekPipe(in, handle, d)
49 case windows.FILE_TYPE_DISK:
50 return 0, false, nil
51 default:
52 return 0, false, nil
53 }
54}
55
56const consoleKeyEvent = 0x0001
57
58type consoleInputRecord struct {
59 eventType uint16
60 _ uint16
61 event [16]byte
62}
63
64var (
65 peekConsoleInputProc = windows.NewLazySystemDLL("kernel32.dll").NewProc("PeekConsoleInputW")
66 readConsoleInputProc = windows.NewLazySystemDLL("kernel32.dll").NewProc("ReadConsoleInputW")
67)
68
69func consoleInputRecordCall(proc *windows.LazyProc, handle windows.Handle, record *consoleInputRecord) (uint32, error) {
70 if err := proc.Find(); err != nil {
71 return 0, err
72 }
73 var read uint32
74 r1, _, callErr := proc.Call(
75 uintptr(handle),
76 uintptr(unsafe.Pointer(record)),
77 1,
78 uintptr(unsafe.Pointer(&read)),
79 )
80 if r1 == 0 {
81 if callErr != nil && callErr != windows.ERROR_SUCCESS {
82 return 0, callErr
83 }
84 return 0, windows.ERROR_INVALID_FUNCTION
85 }
86 return read, nil
87}
88
89func peekConsole(in *os.File, handle windows.Handle, d time.Duration) (byte, bool, error) {
90 deadline := time.Now().Add(d)
91 for {
92 remaining := d
93 if d > 0 {
94 remaining = time.Until(deadline)
95 if remaining <= 0 {
96 return 0, false, nil
97 }
98 }
99 event, err := windows.WaitForSingleObject(handle, waitMilliseconds(remaining))
100 if err != nil {
101 return 0, false, err
102 }
103 switch event {
104 case uint32(windows.WAIT_TIMEOUT):
105 return 0, false, nil
106 case windows.WAIT_OBJECT_0:
107 var record consoleInputRecord
108 count, err := consoleInputRecordCall(peekConsoleInputProc, handle, &record)
109 if err != nil {
110 return 0, false, err
111 }
112 if count == 0 {
113 if d <= 0 {
114 return 0, false, nil
115 }
116 continue
117 }
118 if record.eventType == consoleKeyEvent {
119 keyDown := binary.LittleEndian.Uint32(record.event[0:4]) != 0
120 char := binary.LittleEndian.Uint16(record.event[10:12])
121 if keyDown && char != 0 {
122 // The callback returns one byte, so leave non-ASCII
123 // characters queued for the normal reader rather than
124 // consuming them and truncating their encoding.
125 if char > 0xff {
126 return 0, false, nil
127 }
128 count, err := consoleInputRecordCall(readConsoleInputProc, handle, &record)
129 if err != nil {
130 return 0, false, err
131 }
132 if count != 0 {
133 return byte(char), true, nil
134 }
135 continue
136 }
137 }
138 // Consume non-character, key-up, and non-printing records so
139 // a ready console handle cannot make the next wait spin.
140 if _, err := consoleInputRecordCall(readConsoleInputProc, handle, &record); err != nil {
141 return 0, false, err
142 }
143 if d <= 0 || time.Until(deadline) <= 0 {
144 return 0, false, nil
145 }
146 continue
147 default:
148 return 0, false, windows.ERROR_INVALID_FUNCTION
149 }
150 }
151}
152
153func waitMilliseconds(d time.Duration) uint32 {
154 if d <= 0 {
155 return 0
156 }
157 milliseconds := uint64(d / time.Millisecond)
158 if d%time.Millisecond != 0 {
159 milliseconds++
160 }
161 const maxWaitMilliseconds = uint64(^uint32(0) - 1) // reserve INFINITE
162 if milliseconds > maxWaitMilliseconds {
163 return uint32(maxWaitMilliseconds)
164 }
165 return uint32(milliseconds)
166}
167
168var peekNamedPipe = windows.NewLazySystemDLL("kernel32.dll").NewProc("PeekNamedPipe")
169
170func peekPipe(in *os.File, handle windows.Handle, d time.Duration) (byte, bool, error) {
171 if err := peekNamedPipe.Find(); err != nil {
172 return 0, false, err
173 }
174 deadline := time.Now().Add(d)
175 for {
176 var available uint32
177 ok, _, callErr := peekNamedPipe.Call(
178 uintptr(handle),
179 0, 0, 0,
180 uintptr(unsafe.Pointer(&available)),
181 0,
182 )
183 if ok == 0 {
184 if callErr != nil && callErr != windows.ERROR_SUCCESS {
185 return 0, false, callErr
186 }
187 return 0, false, windows.ERROR_INVALID_FUNCTION
188 }
189 if available != 0 {
190 var b [1]byte
191 n, err := in.Read(b[:])
192 if err != nil {
193 return 0, false, err
194 }
195 if n != len(b) {
196 return 0, false, windows.ERROR_INVALID_FUNCTION
197 }
198 return b[0], true, nil
199 }
200 if d <= 0 {
201 return 0, false, nil
202 }
203 remaining := time.Until(deadline)
204 if remaining <= 0 {
205 return 0, false, nil
206 }
207 // PeekNamedPipe is a non-consuming availability check. A short
208 // sleep avoids a busy loop while the deadline remains authoritative.
209 if remaining > time.Millisecond {
210 remaining = time.Millisecond
211 }
212 time.Sleep(remaining)
213 }
214}