okami.mom/ultraviolet
git clone https://git.nkp.pet/okami.mom/ultraviolet
6d3a341
main
1//go:build windows 2// +build windows 3 4package uv 5 6import ( 7"bytes" 8"context" 9"fmt" 10"strings" 11"time" 12"unicode/utf16" 13 14"github.com/charmbracelet/x/ansi" 15 xwindows"github.com/charmbracelet/x/windows" 16"github.com/muesli/cancelreader" 17"golang.org/x/sys/windows" 18) 19 20// streamData sends data from the input stream to the event channel. 21func (d * TerminalReader )streamData (ctx context.Context ,readc chan []byte )error { 22cc ,ok := d .r .(* conInputReader ) 23if ! ok { 24d .logf ("streamData: reader is not a conInputReader, falling back to default implementation" ) 25return d .sendBytes (ctx ,readc ) 26 } 27 28// Store the value of VT Input Mode for later use. 29d .vtInput = cc .newMode & windows .ENABLE_VIRTUAL_TERMINAL_INPUT != 0 30 31var buf bytes.Buffer 32var records []xwindows.InputRecord 33var err error 34for { 35for { 36records ,err = peekNConsoleInputs (cc .conin ,readBufSize ) 37if cc .isCanceled () { 38return cancelreader .ErrCanceled 39 } 40if err != nil { 41return err 42 } 43if len (records )> 0 { 44break 45 } 46 47// Sleep for a bit to avoid busy waiting. 48time .Sleep (10 * time .Millisecond ) 49 } 50 51records ,err = readNConsoleInputs (cc .conin ,uint32 (len (records )))//nolint:gosec 52if cc .isCanceled () { 53return cancelreader .ErrCanceled 54 } 55if err != nil { 56return err 57 } 58 59// We convert Windows Input Records to VT input sequences for easier 60// processing especially when dealing with UTF-16 decoding and 61// Win32-Input-Mode processing. 62d .serializeWin32InputRecords (records ,& buf ) 63 64select { 65case <- ctx .Done (): 66return nil 67case readc <- buf .Bytes (): 68 } 69 70buf .Reset () 71 } 72} 73 74// serializeWin32InputRecords serializes the Win32 input events converting them 75// to valid VT input sequences. It will also encode any UTF-16 pairs that might 76// be present in the input buffer. The resulting byte slice can be sent to the 77// terminal as input. 78func (d * TerminalReader )serializeWin32InputRecords (records []xwindows.InputRecord ,buf * bytes.Buffer ) { 79for _ ,record := range records { 80switch record .EventType { 81case xwindows .KEY_EVENT : 82kevent := record .KeyEvent () 83// d.logf("key event: %s", keyEventString(kevent.VirtualKeyCode, kevent.VirtualScanCode, kevent.Char, kevent.KeyDown, kevent.ControlKeyState, kevent.RepeatCount)) 84 85var kd int 86if kevent .KeyDown { 87kd = 1 88 } 89if d .vtInput {//nolint:nestif 90// In VT Input Mode, we only capture the Unicode characters 91// decoding them along the way. 92// This is similar to [TerminalReader.storeGraphemeRune] except 93// that we need to write the events directly to the buffer. 94if d .utf16Half [kd ] { 95// We have a half pair that needs to be decoded. 96d .utf16Half [kd ]= false 97d .utf16Buf [kd ][1 ]= kevent .Char 98r := utf16 .DecodeRune (d .utf16Buf [kd ][0 ],d .utf16Buf [kd ][1 ]) 99buf .WriteRune (r ) 100 }else if utf16 .IsSurrogate (kevent .Char ) { 101// This is the first half of a UTF-16 surrogate pair. 102d .utf16Half [kd ]= true 103d .utf16Buf [kd ][0 ]= kevent .Char 104 }else if kevent .KeyDown { 105// Just a regular key press character encoded in VT. 106buf .WriteRune (kevent .Char ) 107 } 108 }else { 109// We encode the key to Win32 Input Mode if it is a known key. 110if kevent .VirtualKeyCode == 0 { 111d .eventScanner .storeGraphemeRune (kd ,kevent .Char ) 112 }else { 113buf .Write (d .eventScanner .encodeGraphemeBufs ()) 114fmt .Fprintf (buf , 115"\x1b[%d;%d;%d;%d;%d;%d_" , 116kevent .VirtualKeyCode , 117kevent .VirtualScanCode , 118kevent .Char , 119kd , 120kevent .ControlKeyState , 121kevent .RepeatCount ) 122 } 123 } 124 125case xwindows .MOUSE_EVENT : 126if d .MouseMode == nil || * d .MouseMode == 0 { 127continue 128 } 129mouseMode := * d .MouseMode 130mevent := record .MouseEvent () 131 132var isRelease bool 133var isMotion bool 134var button MouseButton 135alt := mevent .ControlKeyState & (xwindows .LEFT_ALT_PRESSED | xwindows .RIGHT_ALT_PRESSED )!= 0 136ctrl := mevent .ControlKeyState & (xwindows .LEFT_CTRL_PRESSED | xwindows .RIGHT_CTRL_PRESSED )!= 0 137shift := mevent .ControlKeyState & xwindows .SHIFT_PRESSED != 0 138wheelDirection := int16 (highWord (mevent .ButtonState ))//nolint:gosec 139switch mevent .EventFlags { 140case 0 ,xwindows .DOUBLE_CLICK : 141button ,isRelease = mouseEventButton (d .lastMouseBtns ,mevent .ButtonState ) 142case xwindows .MOUSE_WHEELED : 143if wheelDirection > 0 { 144button = MouseWheelUp 145 }else { 146button = MouseWheelDown 147 } 148case xwindows .MOUSE_HWHEELED : 149if wheelDirection > 0 { 150button = MouseWheelRight 151 }else { 152button = MouseWheelLeft 153 } 154case xwindows .MOUSE_MOVED : 155button ,_ = mouseEventButton (d .lastMouseBtns ,mevent .ButtonState ) 156isMotion = true 157 } 158 159// We emulate mouse mode levels on Windows. This is because Windows 160// doesn't have a concept of different mouse modes. We use the mouse mode to determine 161if button == MouseNone && mouseMode & MouseModeMotion == 0 || 162 (button != MouseNone && mouseMode & MouseModeDrag == 0 ) { 163continue 164 } 165 166// Encode mouse events as SGR mouse sequences that can be read by [EventDecoder]. 167buf .WriteString (ansi .MouseSgr ( 168ansi .EncodeMouseButton (button ,isMotion ,shift ,alt ,ctrl ), 169int (mevent .MousePositon .X ),int (mevent .MousePositon .Y ),isRelease , 170 )) 171 172d .lastMouseBtns = mevent .ButtonState 173 174case xwindows .WINDOW_BUFFER_SIZE_EVENT : 175wevent := record .WindowBufferSizeEvent () 176if wevent .Size .X != d .lastWinsizeX || wevent .Size .Y != d .lastWinsizeY { 177d .lastWinsizeX ,d .lastWinsizeY = wevent .Size .X ,wevent .Size .Y 178// We encode window resize events as CSI 4 ; height ; width t 179// sequence which the [EventDecoder] understands. 180buf .WriteString ( 181ansi .WindowOp ( 1828 ,// Terminal window size in cells 183int (wevent .Size .Y ),// height 184int (wevent .Size .X ),// width 185 ), 186 ) 187 } 188 189case xwindows .FOCUS_EVENT : 190fevent := record .FocusEvent () 191if fevent .SetFocus { 192buf .WriteString (ansi .Focus ) 193 }else { 194buf .WriteString (ansi .Blur ) 195 } 196 197case xwindows .MENU_EVENT : 198// ignore 199 } 200 } 201 202// Flush any remaining grapheme buffers. 203buf .Write (d .eventScanner .encodeGraphemeBufs ()) 204} 205 206func mouseEventButton (p ,s uint32 ) (MouseButton ,bool ) { 207var isRelease bool 208button := MouseNone 209btn := p ^ s 210if btn & s == 0 { 211isRelease = true 212 } 213 214if btn == 0 { 215switch { 216case s & xwindows .FROM_LEFT_1ST_BUTTON_PRESSED > 0 : 217button = MouseLeft 218case s & xwindows .FROM_LEFT_2ND_BUTTON_PRESSED > 0 : 219button = MouseMiddle 220case s & xwindows .RIGHTMOST_BUTTON_PRESSED > 0 : 221button = MouseRight 222case s & xwindows .FROM_LEFT_3RD_BUTTON_PRESSED > 0 : 223button = MouseBackward 224case s & xwindows .FROM_LEFT_4TH_BUTTON_PRESSED > 0 : 225button = MouseForward 226 } 227return button ,isRelease 228 } 229 230switch btn { 231case xwindows .FROM_LEFT_1ST_BUTTON_PRESSED :// left button 232button = MouseLeft 233case xwindows .RIGHTMOST_BUTTON_PRESSED :// right button 234button = MouseRight 235case xwindows .FROM_LEFT_2ND_BUTTON_PRESSED :// middle button 236button = MouseMiddle 237case xwindows .FROM_LEFT_3RD_BUTTON_PRESSED :// unknown (possibly mouse backward) 238button = MouseBackward 239case xwindows .FROM_LEFT_4TH_BUTTON_PRESSED :// unknown (possibly mouse forward) 240button = MouseForward 241 } 242 243return button ,isRelease 244} 245 246func highWord (data uint32 )uint16 { 247return uint16 ((data & 0xFFFF0000 )>> 16 )//nolint:gosec 248} 249 250func readNConsoleInputs (console windows.Handle ,maxEvents uint32 ) ([]xwindows.InputRecord ,error ) { 251if maxEvents == 0 { 252return nil ,fmt .Errorf ("maxEvents cannot be zero" ) 253 } 254 255records := make ([]xwindows.InputRecord ,maxEvents ) 256n ,err := readConsoleInput (console ,records ) 257return records [:n ],err 258} 259 260func readConsoleInput (console windows.Handle ,inputRecords []xwindows.InputRecord ) (uint32 ,error ) { 261if len (inputRecords )== 0 { 262return 0 ,fmt .Errorf ("size of input record buffer cannot be zero" ) 263 } 264 265var read uint32 266 267err := xwindows .ReadConsoleInput (console ,& inputRecords [0 ],uint32 (len (inputRecords )),& read )//nolint:gosec 268 269return read ,err //nolint:wrapcheck 270} 271 272func peekConsoleInput (console windows.Handle ,inputRecords []xwindows.InputRecord ) (uint32 ,error ) { 273if len (inputRecords )== 0 { 274return 0 ,fmt .Errorf ("size of input record buffer cannot be zero" ) 275 } 276 277var read uint32 278 279err := xwindows .PeekConsoleInput (console ,& inputRecords [0 ],uint32 (len (inputRecords )),& read )//nolint:gosec 280 281return read ,err //nolint:wrapcheck 282} 283 284func peekNConsoleInputs (console windows.Handle ,maxEvents uint32 ) ([]xwindows.InputRecord ,error ) { 285if maxEvents == 0 { 286return nil ,fmt .Errorf ("maxEvents cannot be zero" ) 287 } 288 289records := make ([]xwindows.InputRecord ,maxEvents ) 290n ,err := peekConsoleInput (console ,records ) 291return records [:n ],err 292} 293 294//nolint:unused 295func keyEventString (vkc ,sc uint16 ,r rune ,keyDown bool ,cks uint32 ,repeatCount uint16 )string { 296var s strings.Builder 297s .WriteString ("vkc: " ) 298fmt .Fprintf (& s ,"%d, 0x%02x" ,vkc ,vkc ) 299s .WriteString (", sc: " ) 300fmt .Fprintf (& s ,"%d, 0x%02x" ,sc ,sc ) 301s .WriteString (", r: " ) 302fmt .Fprintf (& s ,"%q 0x%x" ,r ,r ) 303s .WriteString (", down: " ) 304fmt .Fprintf (& s ,"%v" ,keyDown ) 305s .WriteString (", cks: [" ) 306if cks & xwindows .LEFT_ALT_PRESSED != 0 { 307s .WriteString ("left alt, " ) 308 } 309if cks & xwindows .RIGHT_ALT_PRESSED != 0 { 310s .WriteString ("right alt, " ) 311 } 312if cks & xwindows .LEFT_CTRL_PRESSED != 0 { 313s .WriteString ("left ctrl, " ) 314 } 315if cks & xwindows .RIGHT_CTRL_PRESSED != 0 { 316s .WriteString ("right ctrl, " ) 317 } 318if cks & xwindows .SHIFT_PRESSED != 0 { 319s .WriteString ("shift, " ) 320 } 321if cks & xwindows .CAPSLOCK_ON != 0 { 322s .WriteString ("caps lock, " ) 323 } 324if cks & xwindows .NUMLOCK_ON != 0 { 325s .WriteString ("num lock, " ) 326 } 327if cks & xwindows .SCROLLLOCK_ON != 0 { 328s .WriteString ("scroll lock, " ) 329 } 330if cks & xwindows .ENHANCED_KEY != 0 { 331s .WriteString ("enhanced key, " ) 332 } 333s .WriteString ("], repeat count: " ) 334fmt .Fprintf (& s ,"%d" ,repeatCount ) 335return s .String () 336}