terminal_reader_windows.go

  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}