1//go:build windows
2// +build windows
3
4package uv
5
6import (
7 "context"
8 "errors"
9 "fmt"
10 "strings"
11 "time"
12 "unicode"
13 "unicode/utf16"
14 "unicode/utf8"
15
16 "github.com/charmbracelet/x/ansi"
17 xwindows "github.com/charmbracelet/x/windows"
18 "github.com/muesli/cancelreader"
19 "golang.org/x/sys/windows"
20)
21
22// ReceiveEvents reads input events from the terminal and sends them to the
23// given event channel.
24func (d *TerminalReader) ReceiveEvents(ctx context.Context, events chan<- Event) error {
25 for {
26 evs, err := d.handleConInput()
27 if errors.Is(err, errNotConInputReader) {
28 return d.receiveEvents(ctx, events)
29 }
30 if err != nil {
31 return fmt.Errorf("read coninput events: %w", err)
32 }
33 for _, ev := range evs {
34 select {
35 case <-ctx.Done():
36 return nil
37 case events <- ev:
38 }
39 }
40 }
41}
42
43var errNotConInputReader = fmt.Errorf("handleConInput: not a conInputReader")
44
45func (d *TerminalReader) handleConInput() ([]Event, error) {
46 cc, ok := d.rd.(*conInputReader)
47 if !ok {
48 return nil, errNotConInputReader
49 }
50
51 var (
52 events []xwindows.InputRecord
53 err error
54 )
55 for {
56 // Peek up to 256 events, this is to allow for sequences events reported as
57 // key events.
58 events, err = peekNConsoleInputs(cc.conin, 256)
59 if cc.isCanceled() {
60 return nil, cancelreader.ErrCanceled
61 }
62 if err != nil {
63 return nil, fmt.Errorf("peek coninput events: %w", err)
64 }
65 if len(events) > 0 {
66 break
67 }
68
69 // Sleep for a bit to avoid busy waiting.
70 time.Sleep(10 * time.Millisecond)
71 }
72
73 events, err = readNConsoleInputs(cc.conin, uint32(len(events)))
74 if cc.isCanceled() {
75 return nil, cancelreader.ErrCanceled
76 }
77 if err != nil {
78 return nil, fmt.Errorf("read coninput events: %w", err)
79 }
80
81 var evs []Event
82 for _, event := range events {
83 if e := d.SequenceParser.parseConInputEvent(event, &d.keyState, d.MouseMode, d.logger); e != nil {
84 if multi, ok := e.(MultiEvent); ok {
85 if d.logger != nil {
86 for _, ev := range multi {
87 d.logf("input: %T %v", ev, ev)
88 }
89 }
90 evs = append(evs, multi...)
91 } else {
92 d.logf("input: %T %v", e, e)
93 evs = append(evs, e)
94 }
95 }
96 }
97
98 return evs, nil
99}
100
101func (p *SequenceParser) parseConInputEvent(event xwindows.InputRecord, keyState *win32InputState, mouseMode *MouseMode, logger Logger) Event {
102 switch event.EventType {
103 case xwindows.KEY_EVENT:
104 kevent := event.KeyEvent()
105 return p.parseWin32InputKeyEvent(keyState, kevent.VirtualKeyCode, kevent.VirtualScanCode,
106 kevent.Char, kevent.KeyDown, kevent.ControlKeyState, kevent.RepeatCount, logger)
107
108 case xwindows.WINDOW_BUFFER_SIZE_EVENT:
109 wevent := event.WindowBufferSizeEvent()
110 if wevent.Size.X != keyState.lastWinsizeX || wevent.Size.Y != keyState.lastWinsizeY {
111 keyState.lastWinsizeX, keyState.lastWinsizeY = wevent.Size.X, wevent.Size.Y
112 return WindowSizeEvent{
113 Width: int(wevent.Size.X),
114 Height: int(wevent.Size.Y),
115 }
116 }
117 case xwindows.MOUSE_EVENT:
118 if mouseMode == nil || *mouseMode == 0 {
119 return nil
120 }
121 mevent := event.MouseEvent()
122 event := mouseEvent(keyState.lastMouseBtns, mevent)
123 // We emulate mouse mode levels on Windows. This is because Windows
124 // doesn't have a concept of different mouse modes. We use the mouse mode to determine
125 switch m := event.(type) {
126 case MouseMotionEvent:
127 if m.Button == MouseNone && (*mouseMode)&AllMouseMode == 0 {
128 return nil
129 }
130 if m.Button != MouseNone && (*mouseMode)&DragMouseMode == 0 {
131 return nil
132 }
133 }
134 keyState.lastMouseBtns = mevent.ButtonState
135 return event
136 case xwindows.FOCUS_EVENT:
137 fevent := event.FocusEvent()
138 if fevent.SetFocus {
139 return FocusEvent{}
140 }
141 return BlurEvent{}
142 case xwindows.MENU_EVENT:
143 // ignore
144 }
145 return nil
146}
147
148func mouseEventButton(p, s uint32) (MouseButton, bool) {
149 var isRelease bool
150 button := MouseNone
151 btn := p ^ s
152 if btn&s == 0 {
153 isRelease = true
154 }
155
156 if btn == 0 {
157 switch {
158 case s&xwindows.FROM_LEFT_1ST_BUTTON_PRESSED > 0:
159 button = MouseLeft
160 case s&xwindows.FROM_LEFT_2ND_BUTTON_PRESSED > 0:
161 button = MouseMiddle
162 case s&xwindows.RIGHTMOST_BUTTON_PRESSED > 0:
163 button = MouseRight
164 case s&xwindows.FROM_LEFT_3RD_BUTTON_PRESSED > 0:
165 button = MouseBackward
166 case s&xwindows.FROM_LEFT_4TH_BUTTON_PRESSED > 0:
167 button = MouseForward
168 }
169 return button, isRelease
170 }
171
172 switch btn {
173 case xwindows.FROM_LEFT_1ST_BUTTON_PRESSED: // left button
174 button = MouseLeft
175 case xwindows.RIGHTMOST_BUTTON_PRESSED: // right button
176 button = MouseRight
177 case xwindows.FROM_LEFT_2ND_BUTTON_PRESSED: // middle button
178 button = MouseMiddle
179 case xwindows.FROM_LEFT_3RD_BUTTON_PRESSED: // unknown (possibly mouse backward)
180 button = MouseBackward
181 case xwindows.FROM_LEFT_4TH_BUTTON_PRESSED: // unknown (possibly mouse forward)
182 button = MouseForward
183 }
184
185 return button, isRelease
186}
187
188func mouseEvent(p uint32, e xwindows.MouseEventRecord) (ev Event) {
189 var mod KeyMod
190 var isRelease bool
191 if e.ControlKeyState&(xwindows.LEFT_ALT_PRESSED|xwindows.RIGHT_ALT_PRESSED) != 0 {
192 mod |= ModAlt
193 }
194 if e.ControlKeyState&(xwindows.LEFT_CTRL_PRESSED|xwindows.RIGHT_CTRL_PRESSED) != 0 {
195 mod |= ModCtrl
196 }
197 if e.ControlKeyState&(xwindows.SHIFT_PRESSED) != 0 {
198 mod |= ModShift
199 }
200
201 m := Mouse{
202 X: int(e.MousePositon.X),
203 Y: int(e.MousePositon.Y),
204 Mod: mod,
205 }
206
207 wheelDirection := int16(highWord(e.ButtonState)) //nolint:gosec
208 switch e.EventFlags {
209 case 0, xwindows.DOUBLE_CLICK:
210 m.Button, isRelease = mouseEventButton(p, e.ButtonState)
211 case xwindows.MOUSE_WHEELED:
212 if wheelDirection > 0 {
213 m.Button = MouseWheelUp
214 } else {
215 m.Button = MouseWheelDown
216 }
217 case xwindows.MOUSE_HWHEELED:
218 if wheelDirection > 0 {
219 m.Button = MouseWheelRight
220 } else {
221 m.Button = MouseWheelLeft
222 }
223 case xwindows.MOUSE_MOVED:
224 m.Button, _ = mouseEventButton(p, e.ButtonState)
225 return MouseMotionEvent(m)
226 }
227
228 if isWheel(m.Button) {
229 return MouseWheelEvent(m)
230 } else if isRelease {
231 return MouseReleaseEvent(m)
232 }
233
234 return MouseClickEvent(m)
235}
236
237func highWord(data uint32) uint16 {
238 return uint16((data & 0xFFFF0000) >> 16) //nolint:gosec
239}
240
241func readNConsoleInputs(console windows.Handle, maxEvents uint32) ([]xwindows.InputRecord, error) {
242 if maxEvents == 0 {
243 return nil, fmt.Errorf("maxEvents cannot be zero")
244 }
245
246 records := make([]xwindows.InputRecord, maxEvents)
247 n, err := readConsoleInput(console, records)
248 return records[:n], err
249}
250
251func readConsoleInput(console windows.Handle, inputRecords []xwindows.InputRecord) (uint32, error) {
252 if len(inputRecords) == 0 {
253 return 0, fmt.Errorf("size of input record buffer cannot be zero")
254 }
255
256 var read uint32
257
258 err := xwindows.ReadConsoleInput(console, &inputRecords[0], uint32(len(inputRecords)), &read) //nolint:gosec
259
260 return read, err //nolint:wrapcheck
261}
262
263func peekConsoleInput(console windows.Handle, inputRecords []xwindows.InputRecord) (uint32, error) {
264 if len(inputRecords) == 0 {
265 return 0, fmt.Errorf("size of input record buffer cannot be zero")
266 }
267
268 var read uint32
269
270 err := xwindows.PeekConsoleInput(console, &inputRecords[0], uint32(len(inputRecords)), &read) //nolint:gosec
271
272 return read, err //nolint:wrapcheck
273}
274
275func peekNConsoleInputs(console windows.Handle, maxEvents uint32) ([]xwindows.InputRecord, error) {
276 if maxEvents == 0 {
277 return nil, fmt.Errorf("maxEvents cannot be zero")
278 }
279
280 records := make([]xwindows.InputRecord, maxEvents)
281 n, err := peekConsoleInput(console, records)
282 return records[:n], err
283}
284
285// parseWin32InputKeyEvent parses a single key event from either the Windows
286// Console API or win32-input-mode events. When state is nil, it means this is
287// an event from win32-input-mode. Otherwise, it's a key event from the Windows
288// Console API and needs a state to decode ANSI escape sequences and utf16
289// runes.
290func (p *SequenceParser) parseWin32InputKeyEvent(state *win32InputState, vkc uint16, _ uint16, r rune, keyDown bool, cks uint32, repeatCount uint16, logger Logger) (event Event) {
291 defer func() {
292 // Respect the repeat count.
293 if repeatCount > 1 {
294 var multi MultiEvent
295 for i := 0; i < int(repeatCount); i++ {
296 multi = append(multi, event)
297 }
298 event = multi
299 }
300 }()
301 if state != nil {
302 defer func() {
303 state.lastCks = cks
304 }()
305 }
306
307 var utf8Buf [utf8.UTFMax]byte
308 var key Key
309 if state != nil && state.utf16Half {
310 state.utf16Half = false
311 state.utf16Buf[1] = r
312 codepoint := utf16.DecodeRune(state.utf16Buf[0], state.utf16Buf[1])
313 rw := utf8.EncodeRune(utf8Buf[:], codepoint)
314 r, _ = utf8.DecodeRune(utf8Buf[:rw])
315 key.Code = r
316 key.Text = string(r)
317 key.Mod = translateControlKeyState(cks)
318 key = ensureKeyCase(key, cks)
319 if keyDown {
320 return KeyPressEvent(key)
321 }
322 return KeyReleaseEvent(key)
323 }
324
325 var baseCode rune
326 switch {
327 case vkc == 0:
328 // Zero means this event is either an escape code or a unicode
329 // codepoint.
330 if state != nil && state.ansiIdx == 0 && r != ansi.ESC {
331 if logger != nil {
332 logger.Printf("input: received unicode codepoint instead of sequence %q", r)
333 }
334 // This is a unicode codepoint.
335 baseCode = r
336 break
337 }
338
339 if state != nil {
340 // Collect ANSI escape code.
341 state.ansiBuf[state.ansiIdx] = byte(r)
342 state.ansiIdx++
343 if state.ansiIdx <= 2 {
344 // We haven't received enough bytes to determine if this is an
345 // ANSI escape code.
346 return nil
347 }
348 if r == ansi.ESC {
349 // We're expecting a closing String Terminator [ansi.ST].
350 return nil
351 }
352
353 n, event := p.parseSequence(state.ansiBuf[:state.ansiIdx])
354 if n == 0 {
355 return nil
356 }
357 if _, ok := event.(UnknownEvent); ok {
358 return nil
359 }
360
361 if logger != nil {
362 logger.Printf("input: parsed sequence %q, %d bytes", state.ansiBuf[:n], n)
363 }
364
365 state.ansiIdx = 0
366 return event
367 }
368 case vkc == xwindows.VK_BACK:
369 baseCode = KeyBackspace
370 case vkc == xwindows.VK_TAB:
371 baseCode = KeyTab
372 case vkc == xwindows.VK_RETURN:
373 baseCode = KeyEnter
374 case vkc == xwindows.VK_SHIFT:
375 //nolint:nestif
376 if cks&xwindows.SHIFT_PRESSED != 0 {
377 if cks&xwindows.ENHANCED_KEY != 0 {
378 baseCode = KeyRightShift
379 } else {
380 baseCode = KeyLeftShift
381 }
382 } else if state != nil {
383 if state.lastCks&xwindows.SHIFT_PRESSED != 0 {
384 if state.lastCks&xwindows.ENHANCED_KEY != 0 {
385 baseCode = KeyRightShift
386 } else {
387 baseCode = KeyLeftShift
388 }
389 }
390 }
391 case vkc == xwindows.VK_CONTROL:
392 if cks&xwindows.LEFT_CTRL_PRESSED != 0 {
393 baseCode = KeyLeftCtrl
394 } else if cks&xwindows.RIGHT_CTRL_PRESSED != 0 {
395 baseCode = KeyRightCtrl
396 } else if state != nil {
397 if state.lastCks&xwindows.LEFT_CTRL_PRESSED != 0 {
398 baseCode = KeyLeftCtrl
399 } else if state.lastCks&xwindows.RIGHT_CTRL_PRESSED != 0 {
400 baseCode = KeyRightCtrl
401 }
402 }
403 case vkc == xwindows.VK_MENU:
404 if cks&xwindows.LEFT_ALT_PRESSED != 0 {
405 baseCode = KeyLeftAlt
406 } else if cks&xwindows.RIGHT_ALT_PRESSED != 0 {
407 baseCode = KeyRightAlt
408 } else if state != nil {
409 if state.lastCks&xwindows.LEFT_ALT_PRESSED != 0 {
410 baseCode = KeyLeftAlt
411 } else if state.lastCks&xwindows.RIGHT_ALT_PRESSED != 0 {
412 baseCode = KeyRightAlt
413 }
414 }
415 case vkc == xwindows.VK_PAUSE:
416 baseCode = KeyPause
417 case vkc == xwindows.VK_CAPITAL:
418 baseCode = KeyCapsLock
419 case vkc == xwindows.VK_ESCAPE:
420 baseCode = KeyEscape
421 case vkc == xwindows.VK_SPACE:
422 baseCode = KeySpace
423 case vkc == xwindows.VK_PRIOR:
424 baseCode = KeyPgUp
425 case vkc == xwindows.VK_NEXT:
426 baseCode = KeyPgDown
427 case vkc == xwindows.VK_END:
428 baseCode = KeyEnd
429 case vkc == xwindows.VK_HOME:
430 baseCode = KeyHome
431 case vkc == xwindows.VK_LEFT:
432 baseCode = KeyLeft
433 case vkc == xwindows.VK_UP:
434 baseCode = KeyUp
435 case vkc == xwindows.VK_RIGHT:
436 baseCode = KeyRight
437 case vkc == xwindows.VK_DOWN:
438 baseCode = KeyDown
439 case vkc == xwindows.VK_SELECT:
440 baseCode = KeySelect
441 case vkc == xwindows.VK_SNAPSHOT:
442 baseCode = KeyPrintScreen
443 case vkc == xwindows.VK_INSERT:
444 baseCode = KeyInsert
445 case vkc == xwindows.VK_DELETE:
446 baseCode = KeyDelete
447 case vkc >= '0' && vkc <= '9':
448 baseCode = rune(vkc)
449 case vkc >= 'A' && vkc <= 'Z':
450 // Convert to lowercase.
451 baseCode = rune(vkc) + 32
452 case vkc == xwindows.VK_LWIN:
453 baseCode = KeyLeftSuper
454 case vkc == xwindows.VK_RWIN:
455 baseCode = KeyRightSuper
456 case vkc == xwindows.VK_APPS:
457 baseCode = KeyMenu
458 case vkc >= xwindows.VK_NUMPAD0 && vkc <= xwindows.VK_NUMPAD9:
459 baseCode = rune(vkc-xwindows.VK_NUMPAD0) + KeyKp0
460 case vkc == xwindows.VK_MULTIPLY:
461 baseCode = KeyKpMultiply
462 case vkc == xwindows.VK_ADD:
463 baseCode = KeyKpPlus
464 case vkc == xwindows.VK_SEPARATOR:
465 baseCode = KeyKpComma
466 case vkc == xwindows.VK_SUBTRACT:
467 baseCode = KeyKpMinus
468 case vkc == xwindows.VK_DECIMAL:
469 baseCode = KeyKpDecimal
470 case vkc == xwindows.VK_DIVIDE:
471 baseCode = KeyKpDivide
472 case vkc >= xwindows.VK_F1 && vkc <= xwindows.VK_F24:
473 baseCode = rune(vkc-xwindows.VK_F1) + KeyF1
474 case vkc == xwindows.VK_NUMLOCK:
475 baseCode = KeyNumLock
476 case vkc == xwindows.VK_SCROLL:
477 baseCode = KeyScrollLock
478 case vkc == xwindows.VK_LSHIFT:
479 baseCode = KeyLeftShift
480 case vkc == xwindows.VK_RSHIFT:
481 baseCode = KeyRightShift
482 case vkc == xwindows.VK_LCONTROL:
483 baseCode = KeyLeftCtrl
484 case vkc == xwindows.VK_RCONTROL:
485 baseCode = KeyRightCtrl
486 case vkc == xwindows.VK_LMENU:
487 baseCode = KeyLeftAlt
488 case vkc == xwindows.VK_RMENU:
489 baseCode = KeyRightAlt
490 case vkc == xwindows.VK_VOLUME_MUTE:
491 baseCode = KeyMute
492 case vkc == xwindows.VK_VOLUME_DOWN:
493 baseCode = KeyLowerVol
494 case vkc == xwindows.VK_VOLUME_UP:
495 baseCode = KeyRaiseVol
496 case vkc == xwindows.VK_MEDIA_NEXT_TRACK:
497 baseCode = KeyMediaNext
498 case vkc == xwindows.VK_MEDIA_PREV_TRACK:
499 baseCode = KeyMediaPrev
500 case vkc == xwindows.VK_MEDIA_STOP:
501 baseCode = KeyMediaStop
502 case vkc == xwindows.VK_MEDIA_PLAY_PAUSE:
503 baseCode = KeyMediaPlayPause
504 case vkc == xwindows.VK_OEM_1:
505 baseCode = ';'
506 case vkc == xwindows.VK_OEM_PLUS:
507 baseCode = '+'
508 case vkc == xwindows.VK_OEM_COMMA:
509 baseCode = ','
510 case vkc == xwindows.VK_OEM_MINUS:
511 baseCode = '-'
512 case vkc == xwindows.VK_OEM_PERIOD:
513 baseCode = '.'
514 case vkc == xwindows.VK_OEM_2:
515 baseCode = '/'
516 case vkc == xwindows.VK_OEM_3:
517 baseCode = '`'
518 case vkc == xwindows.VK_OEM_4:
519 baseCode = '['
520 case vkc == xwindows.VK_OEM_5:
521 baseCode = '\\'
522 case vkc == xwindows.VK_OEM_6:
523 baseCode = ']'
524 case vkc == xwindows.VK_OEM_7:
525 baseCode = '\''
526 }
527
528 if utf16.IsSurrogate(r) {
529 if state != nil {
530 state.utf16Buf[0] = r
531 state.utf16Half = true
532 }
533 return nil
534 }
535
536 // AltGr is left ctrl + right alt. On non-US keyboards, this is used to type
537 // special characters and produce printable events.
538 // XXX: Should this be a KeyMod?
539 altGr := cks&(xwindows.LEFT_CTRL_PRESSED|xwindows.RIGHT_ALT_PRESSED) == xwindows.LEFT_CTRL_PRESSED|xwindows.RIGHT_ALT_PRESSED
540
541 var text string
542 keyCode := baseCode
543 if isCc := unicode.IsControl(r); vkc == 0 && isCc {
544 return p.parseControl(byte(r))
545 } else if !isCc {
546 rw := utf8.EncodeRune(utf8Buf[:], r)
547 keyCode, _ = utf8.DecodeRune(utf8Buf[:rw])
548 if unicode.IsPrint(keyCode) && (cks == 0 ||
549 cks == xwindows.SHIFT_PRESSED ||
550 cks == xwindows.CAPSLOCK_ON ||
551 altGr) {
552 // If the control key state is 0, shift is pressed, or caps lock
553 // then the key event is a printable event i.e. [text] is not empty.
554 text = string(keyCode)
555 }
556 }
557
558 key.Code = keyCode
559 key.Text = text
560 key.Mod = translateControlKeyState(cks)
561 key.BaseCode = baseCode
562 key = ensureKeyCase(key, cks)
563 if keyDown {
564 return KeyPressEvent(key)
565 }
566
567 return KeyReleaseEvent(key)
568}
569
570// ensureKeyCase ensures that the key's text is in the correct case based on the
571// control key state.
572func ensureKeyCase(key Key, cks uint32) Key {
573 if len(key.Text) == 0 {
574 return key
575 }
576
577 hasShift := cks&xwindows.SHIFT_PRESSED != 0
578 hasCaps := cks&xwindows.CAPSLOCK_ON != 0
579 if hasShift || hasCaps {
580 if unicode.IsLower(key.Code) {
581 key.ShiftedCode = unicode.ToUpper(key.Code)
582 key.Text = string(key.ShiftedCode)
583 }
584 } else {
585 if unicode.IsUpper(key.Code) {
586 key.ShiftedCode = unicode.ToLower(key.Code)
587 key.Text = string(key.ShiftedCode)
588 }
589 }
590
591 return key
592}
593
594// translateControlKeyState translates the control key state from the Windows
595// Console API into a Mod bitmask.
596func translateControlKeyState(cks uint32) (m KeyMod) {
597 if cks&xwindows.LEFT_CTRL_PRESSED != 0 || cks&xwindows.RIGHT_CTRL_PRESSED != 0 {
598 m |= ModCtrl
599 }
600 if cks&xwindows.LEFT_ALT_PRESSED != 0 || cks&xwindows.RIGHT_ALT_PRESSED != 0 {
601 m |= ModAlt
602 }
603 if cks&xwindows.SHIFT_PRESSED != 0 {
604 m |= ModShift
605 }
606 if cks&xwindows.CAPSLOCK_ON != 0 {
607 m |= ModCapsLock
608 }
609 if cks&xwindows.NUMLOCK_ON != 0 {
610 m |= ModNumLock
611 }
612 if cks&xwindows.SCROLLLOCK_ON != 0 {
613 m |= ModScrollLock
614 }
615 return
616}
617
618//nolint:unused
619func keyEventString(vkc, sc uint16, r rune, keyDown bool, cks uint32, repeatCount uint16) string {
620 var s strings.Builder
621 s.WriteString("vkc: ")
622 s.WriteString(fmt.Sprintf("%d, 0x%02x", vkc, vkc))
623 s.WriteString(", sc: ")
624 s.WriteString(fmt.Sprintf("%d, 0x%02x", sc, sc))
625 s.WriteString(", r: ")
626 s.WriteString(fmt.Sprintf("%q", r))
627 s.WriteString(", down: ")
628 s.WriteString(fmt.Sprintf("%v", keyDown))
629 s.WriteString(", cks: [")
630 if cks&xwindows.LEFT_ALT_PRESSED != 0 {
631 s.WriteString("left alt, ")
632 }
633 if cks&xwindows.RIGHT_ALT_PRESSED != 0 {
634 s.WriteString("right alt, ")
635 }
636 if cks&xwindows.LEFT_CTRL_PRESSED != 0 {
637 s.WriteString("left ctrl, ")
638 }
639 if cks&xwindows.RIGHT_CTRL_PRESSED != 0 {
640 s.WriteString("right ctrl, ")
641 }
642 if cks&xwindows.SHIFT_PRESSED != 0 {
643 s.WriteString("shift, ")
644 }
645 if cks&xwindows.CAPSLOCK_ON != 0 {
646 s.WriteString("caps lock, ")
647 }
648 if cks&xwindows.NUMLOCK_ON != 0 {
649 s.WriteString("num lock, ")
650 }
651 if cks&xwindows.SCROLLLOCK_ON != 0 {
652 s.WriteString("scroll lock, ")
653 }
654 if cks&xwindows.ENHANCED_KEY != 0 {
655 s.WriteString("enhanced key, ")
656 }
657 s.WriteString("], repeat count: ")
658 s.WriteString(fmt.Sprintf("%d", repeatCount))
659 return s.String()
660}