1//go:build windows
2
3package tui
4
5import (
6 "os"
7 "time"
8 "unsafe"
9
10 "golang.org/x/sys/windows"
11)
12
13// resizePollInterval bounds how long a size change goes unnoticed.
14const resizePollInterval = 100 * time.Millisecond
15
16// Windows has no SIGWINCH. Console resize events arrive as input records,
17// but stdin is read as a VT byte stream, so those records never reach us.
18// Poll the console size instead and fire the callbacks on every change.
19// This matters most under ConPTY hosts such as multiplexers, where the
20// pane width changes without any user input.
21func (p *ProcTerm) installResizeHandler() {
22 p.resizeOnce.Do(func() {
23 go func() {
24 lastW, lastH := p.Size()
25 ticker := time.NewTicker(resizePollInterval)
26 defer ticker.Stop()
27 for range ticker.C {
28 w, h := p.Size()
29 if w == lastW && h == lastH {
30 continue
31 }
32 lastW, lastH = w, h
33 for _, cb := range p.resizeCBs {
34 cb()
35 }
36 }
37 }()
38 })
39}
40
41func (p *ProcTerm) SetNonblock(enable bool) error { return nil }
42
43var (
44 kernel32 = windows.NewLazySystemDLL("kernel32.dll")
45 procPeekConsoleInputW = kernel32.NewProc("PeekConsoleInputW")
46 procReadConsoleInputW = kernel32.NewProc("ReadConsoleInputW")
47 procGetNumberOfConsoleE = kernel32.NewProc("GetNumberOfConsoleInputEvents")
48)
49
50// inputRecord mirrors the Win32 INPUT_RECORD layout (20 bytes). Only
51// key events are decoded; the union is kept as raw bytes.
52type inputRecord struct {
53 eventType uint16
54 _ uint16
55 event [16]byte
56}
57
58const keyEvent = 0x0001
59
60// yieldsByte reports whether a key-down can produce input in VT mode.
61// Navigation and function keys emit escape sequences even when their
62// UnicodeChar is zero. Key-ups and non-key records produce no bytes.
63func (r *inputRecord) yieldsByte() bool {
64 if r.eventType != keyEvent {
65 return false
66 }
67 keyDown := *(*int32)(unsafe.Pointer(&r.event[0]))
68 if keyDown == 0 {
69 return false
70 }
71 char := *(*uint16)(unsafe.Pointer(&r.event[10]))
72 if char != 0 {
73 return true
74 }
75 key := *(*uint16)(unsafe.Pointer(&r.event[4]))
76 // Win32 virtual-key codes for PageUp/Down, End/Home, arrows,
77 // Insert/Delete and F1-F24. ReadConsole translates these into VT
78 // sequences, despite the input record having no Unicode character.
79 return key >= 0x21 && key <= 0x28 || key == 0x2d || key == 0x2e || key >= 0x70 && key <= 0x87
80}
81
82// consoleHasByte checks pending console input without consuming bytes.
83// Leading records that would not produce a byte are discarded so the
84// handle stops signalling for them; otherwise a lone focus event would
85// make every wait return immediately while ReadFile still blocks.
86func consoleHasByte(h windows.Handle) (bool, error) {
87 for {
88 var count uint32
89 if r, _, err := procGetNumberOfConsoleE.Call(uintptr(h), uintptr(unsafe.Pointer(&count))); r == 0 {
90 return false, err
91 }
92 if count == 0 {
93 return false, nil
94 }
95 records := make([]inputRecord, count)
96 var read uint32
97 if r, _, err := procPeekConsoleInputW.Call(uintptr(h), uintptr(unsafe.Pointer(&records[0])), uintptr(count), uintptr(unsafe.Pointer(&read))); r == 0 {
98 return false, err
99 }
100 skip := uint32(0)
101 for skip < read && !records[skip].yieldsByte() {
102 skip++
103 }
104 if skip < read {
105 return true, nil
106 }
107 if skip == 0 {
108 return false, nil
109 }
110 if r, _, err := procReadConsoleInputW.Call(uintptr(h), uintptr(unsafe.Pointer(&records[0])), uintptr(skip), uintptr(unsafe.Pointer(&read))); r == 0 {
111 return false, err
112 }
113 }
114}
115
116// peekStdin waits up to d for one byte of console input. It uses the
117// console handle's wait state plus PeekConsoleInput, so a missing reply
118// (for example an OSC 11 background query the host never answers) or a
119// bare Esc times out instead of blocking until the next keystroke.
120// Non-console stdin falls back to a blocking read.
121func peekStdin(in *os.File, d time.Duration) (byte, bool, error) {
122 h := windows.Handle(in.Fd())
123 var mode uint32
124 if windows.GetConsoleMode(h, &mode) != nil {
125 var b [1]byte
126 if _, err := in.Read(b[:]); err != nil {
127 return 0, false, err
128 }
129 return b[0], true, nil
130 }
131 deadline := time.Now().Add(d)
132 for {
133 ready, err := consoleHasByte(h)
134 if err != nil {
135 return 0, false, err
136 }
137 if ready {
138 var b [1]byte
139 if _, err := in.Read(b[:]); err != nil {
140 return 0, false, err
141 }
142 return b[0], true, nil
143 }
144 remaining := time.Until(deadline)
145 if remaining <= 0 {
146 return 0, false, nil
147 }
148 event, err := windows.WaitForSingleObject(h, uint32(remaining.Milliseconds())+1)
149 if err != nil {
150 return 0, false, err
151 }
152 if event == uint32(windows.WAIT_TIMEOUT) {
153 return 0, false, nil
154 }
155 }
156}