package uv import ( "bytes" "encoding/base64" "encoding/hex" "fmt" "image/color" "math" "slices" "strings" "unicode" "unicode/utf8" "github.com/charmbracelet/x/ansi" "github.com/charmbracelet/x/ansi/parser" xwindows "github.com/charmbracelet/x/windows" "github.com/rivo/uniseg" ) // Flags to control the behavior of the parser. const ( // When this flag is set, the driver will treat both Ctrl+Space and Ctrl+@ // as the same key sequence. // // Historically, the ANSI specs generate NUL (0x00) on both the Ctrl+Space // and Ctrl+@ key sequences. This flag allows the driver to treat both as // the same key sequence. flagCtrlAt = 1 << iota // When this flag is set, the driver will treat the Tab key and Ctrl+I as // the same key sequence. // // Historically, the ANSI specs generate HT (0x09) on both the Tab key and // Ctrl+I. This flag allows the driver to treat both as the same key // sequence. flagCtrlI // When this flag is set, the driver will treat the Enter key and Ctrl+M as // the same key sequence. // // Historically, the ANSI specs generate CR (0x0D) on both the Enter key // and Ctrl+M. This flag allows the driver to treat both as the same key. flagCtrlM // When this flag is set, the driver will treat Escape and Ctrl+[ as // the same key sequence. // // Historically, the ANSI specs generate ESC (0x1B) on both the Escape key // and Ctrl+[. This flag allows the driver to treat both as the same key // sequence. flagCtrlOpenBracket // When this flag is set, the driver will send a BS (0x08 byte) character // instead of a DEL (0x7F byte) character when the Backspace key is // pressed. // // The VT100 terminal has both a Backspace and a Delete key. The VT220 // terminal dropped the Backspace key and replaced it with the Delete key. // Both terminals send a DEL character when the Delete key is pressed. // Modern terminals and PCs later readded the Delete key but used a // different key sequence, and the Backspace key was standardized to send a // DEL character. flagBackspace // When this flag is set, the driver will recognize the Find key instead of // treating it as a Home key. // // The Find key was part of the VT220 keyboard, and is no longer used in // modern day PCs. flagFind // When this flag is set, the driver will recognize the Select key instead // of treating it as a End key. // // The Symbol key was part of the VT220 keyboard, and is no longer used in // modern day PCs. flagSelect // When this flag is set, the driver will preserve function keys (F13-F63) // as symbols. // // Since these keys are not part of today's standard 20th century keyboard, // we treat them as F1-F12 modifier keys i.e. ctrl/shift/alt + Fn combos. // Key definitions come from Terminfo, this flag is only useful when // FlagTerminfo is not set. flagFKeys ) // LegacyKeyEncoding is a set of flags that control the behavior of legacy // terminal key encodings. Historically, ctrl+ input events produce // control characters (0x00-0x1F) that collide with some special keys like Tab, // Enter, and Escape. This controls the expected behavior of encoding these // keys. // // This type has the following default values: // - CtrlAt maps 0x00 [ansi.NUL] to ctrl+space instead of ctrl+@. // - CtrlI maps 0x09 [ansi.HT] to the tab key instead of ctrl+i. // - CtrlM maps 0x0D [ansi.CR] to the enter key instead of ctrl+m. // - CtrlOpenBracket maps 0x1B [ansi.ESC] to the escape key instead of ctrl+[. // - Backspace maps the backspace key to 0x08 [ansi.BS] instead of 0x7F [ansi.DEL]. // - Find maps the legacy find key to the home key. // - Select maps the legacy select key to the end key. // - FKeys maps high function keys instead of treating them as Function+. type LegacyKeyEncoding uint32 // CtrlAt returns a [LegacyKeyEncoding] with whether [ansi.NUL] (0x00) // is mapped to ctrl+at instead of ctrl+space. func (l LegacyKeyEncoding) CtrlAt(v bool) LegacyKeyEncoding { if v { l |= flagCtrlAt } else { l &^= flagCtrlAt } return l } // CtrlI returns a [LegacyKeyEncoding] with whether [ansi.HT] (0x09) // is mapped to ctrl+i instead of the tab key. func (l LegacyKeyEncoding) CtrlI(v bool) LegacyKeyEncoding { if v { l |= flagCtrlI } else { l &^= flagCtrlI } return l } // CtrlM returns a [LegacyKeyEncoding] with whether [ansi.CR] (0x0D) // is mapped to ctrl+m instead of the enter key. func (l LegacyKeyEncoding) CtrlM(v bool) LegacyKeyEncoding { if v { l |= flagCtrlM } else { l &^= flagCtrlM } return l } // CtrlOpenBracket returns a [LegacyKeyEncoding] with whether [ansi.ESC] (0x1B) // is mapped to ctrl+[ instead of the escape key. func (l LegacyKeyEncoding) CtrlOpenBracket(v bool) LegacyKeyEncoding { if v { l |= flagCtrlOpenBracket } else { l &^= flagCtrlOpenBracket } return l } // Backspace returns a [LegacyKeyEncoding] with whether the backspace key is // mapped to [ansi.BS] (0x08) instead of [ansi.DEL] (0x7F). func (l LegacyKeyEncoding) Backspace(v bool) LegacyKeyEncoding { if v { l |= flagBackspace } else { l &^= flagBackspace } return l } // Find returns a [LegacyKeyEncoding] with whether the legacy find key // is mapped to the home key. func (l LegacyKeyEncoding) Find(v bool) LegacyKeyEncoding { if v { l |= flagFind } else { l &^= flagFind } return l } // Select returns a [LegacyKeyEncoding] with whether the legacy select key // is mapped to the end key. func (l LegacyKeyEncoding) Select(v bool) LegacyKeyEncoding { if v { l |= flagSelect } else { l &^= flagSelect } return l } // FKeys returns a [LegacyKeyEncoding] with whether high function keys are // mapped to high function keys (beyond F20) instead of treating them as // Function+ keys. func (l LegacyKeyEncoding) FKeys(v bool) LegacyKeyEncoding { if v { l |= flagFKeys } else { l &^= flagFKeys } return l } // EventDecoder decodes terminal input events from a byte buffer. Terminal // input events are typically encoded as Unicode or ASCII characters, control // codes, or ANSI escape sequences. type EventDecoder struct { // Legacy is the legacy key encoding flags. These flags control the // behavior of legacy terminal key encodings. See [LegacyKeyEncoding] for // more details. Legacy LegacyKeyEncoding // UseTerminfo is a flag that controls whether to use the terminal type // Terminfo database to map escape sequences to key events. This will // override the default key sequences handled by the parser. UseTerminfo bool lastCks uint32 // the last control key state for the previous key event record. } // Decode finds the first recognized event sequence and returns it along // with its length. // // It will return zero and nil no sequence is recognized or when the buffer is // empty. If a sequence is not supported, an [UnknownEvent] is returned. // // Example: // // ```go // var decoder EventDecoder // var events []Event // buf := []byte("\x00" + // Ctrl+Space // "\x1b[A" + // Up arrow // "Hello") // Text input // for len(buf) > 0 { // n, ev := decoder.Decode(buf) // if ev != nil { // events = append(events, ev) // } // buf = buf[n:] // } // ``` func (p *EventDecoder) Decode(buf []byte) (n int, Event Event) { if len(buf) == 0 { return 0, nil } switch b := buf[0]; b { case ansi.ESC: if len(buf) == 1 { // Escape key return 1, KeyPressEvent{Code: KeyEscape} } switch bPrime := buf[1]; bPrime { case 'O': // Esc-prefixed SS3 return p.parseSs3(buf) case 'P': // Esc-prefixed DCS return p.parseDcs(buf) case '[': // Esc-prefixed CSI return p.parseCsi(buf) case ']': // Esc-prefixed OSC return p.parseOsc(buf) case '_': // Esc-prefixed APC return p.parseApc(buf) case '^': // Esc-prefixed PM return p.parseStTerminated(ansi.PM, '^', nil)(buf) case 'X': // Esc-prefixed SOS return p.parseStTerminated(ansi.SOS, 'X', nil)(buf) default: n, e := p.Decode(buf[1:]) if k, ok := e.(KeyPressEvent); ok { k.Text = "" k.Mod |= ModAlt return n + 1, k } // Not a key sequence, nor an alt modified key sequence. In that // case, just report a single escape key. return 1, KeyPressEvent{Code: KeyEscape} } case ansi.SS3: return p.parseSs3(buf) case ansi.DCS: return p.parseDcs(buf) case ansi.CSI: return p.parseCsi(buf) case ansi.OSC: return p.parseOsc(buf) case ansi.APC: return p.parseApc(buf) case ansi.PM: return p.parseStTerminated(ansi.PM, '^', nil)(buf) case ansi.SOS: return p.parseStTerminated(ansi.SOS, 'X', nil)(buf) default: if b <= ansi.US || b == ansi.DEL || b == ansi.SP { return 1, p.parseControl(b) } else if b >= ansi.PAD && b <= ansi.APC { // C1 control code // UTF-8 never starts with a C1 control code // Encode these as Ctrl+Alt+ code := rune(b) - 0x40 return 1, KeyPressEvent{Code: code, Mod: ModCtrl | ModAlt} } return p.parseUtf8(buf) } } func (p *EventDecoder) parseCsi(b []byte) (int, Event) { if len(b) == 2 && b[0] == ansi.ESC { // short cut if this is an alt+[ key return 2, KeyPressEvent{Code: rune(b[1]), Mod: ModAlt} } var cmd ansi.Cmd var params [parser.MaxParamsSize]ansi.Param var paramsLen int var i int if b[i] == ansi.CSI || b[i] == ansi.ESC { i++ } if i < len(b) && b[i-1] == ansi.ESC && b[i] == '[' { i++ } // Initial CSI byte if i < len(b) && b[i] >= '<' && b[i] <= '?' { cmd |= ansi.Cmd(b[i]) << parser.PrefixShift } // Scan parameter bytes in the range 0x30-0x3F var j int for j = 0; i < len(b) && paramsLen < len(params) && b[i] >= 0x30 && b[i] <= 0x3F; i, j = i+1, j+1 { if b[i] >= '0' && b[i] <= '9' { if params[paramsLen] == parser.MissingParam { params[paramsLen] = 0 } params[paramsLen] *= 10 params[paramsLen] += ansi.Param(b[i]) - '0' } if b[i] == ':' { params[paramsLen] |= parser.HasMoreFlag } if b[i] == ';' || b[i] == ':' { paramsLen++ if paramsLen < len(params) { // Don't overflow the params slice params[paramsLen] = parser.MissingParam } } } if j > 0 && paramsLen < len(params) { // has parameters paramsLen++ } // Scan intermediate bytes in the range 0x20-0x2F var intermed byte for ; i < len(b) && b[i] >= 0x20 && b[i] <= 0x2F; i++ { intermed = b[i] } // Set the intermediate byte cmd |= ansi.Cmd(intermed) << parser.IntermedShift // Scan final byte in the range 0x40-0x7E if i >= len(b) || b[i] < 0x40 || b[i] > 0x7E { // Special case for URxvt keys // CSI $ is an invalid sequence, but URxvt uses it for // shift modified keys. if intermed == '$' && b[i-1] == '$' { buf := slices.Clone(b[:i-1]) n, ev := p.parseCsi(append(buf, '~')) if k, ok := ev.(KeyPressEvent); ok { k.Mod |= ModShift return n, k } } return i, UnknownEvent(b[:i]) } // Add the final byte cmd |= ansi.Cmd(b[i]) i++ pa := ansi.Params(params[:paramsLen]) switch cmd { case 'y' | '?'<'<'< R (which is modified F3) when the cursor is at the // row 1. In this case, we report both messages. // // For a non ambiguous cursor position report, use // [ansi.RequestExtendedCursorPosition] (DECXCPR) instead. return i, MultiEvent{KeyPressEvent{Code: KeyF3, Mod: KeyMod(col - 1)}, m} } return i, m } if paramsLen != 0 { break } // Unmodified key F3 (CSI R) fallthrough case 'a', 'b', 'c', 'd', 'A', 'B', 'C', 'D', 'E', 'F', 'H', 'P', 'Q', 'S', 'Z': var k KeyPressEvent switch cmd { case 'a', 'b', 'c', 'd': k = KeyPressEvent{Code: KeyUp + rune(cmd-'a'), Mod: ModShift} case 'A', 'B', 'C', 'D': k = KeyPressEvent{Code: KeyUp + rune(cmd-'A')} case 'E': k = KeyPressEvent{Code: KeyBegin} case 'F': k = KeyPressEvent{Code: KeyEnd} case 'H': k = KeyPressEvent{Code: KeyHome} case 'P', 'Q', 'R', 'S': k = KeyPressEvent{Code: KeyF1 + rune(cmd-'P')} case 'Z': k = KeyPressEvent{Code: KeyTab, Mod: ModShift} } id, _, _ := pa.Param(0, 1) mod, _, _ := pa.Param(1, 1) if paramsLen > 2 && !pa[1].HasMore() || id != 1 { break } if paramsLen > 1 && id == 1 && mod != -1 { // CSI 1 ; A k.Mod |= KeyMod(mod - 1) } // Don't forget to handle Kitty keyboard protocol return i, parseKittyKeyboardExt(pa, k) case 'M': // Handle X10 mouse if i+3 > len(b) { return i, UnknownCsiEvent(b[:i]) } return i + 3, parseX10MouseEvent(append(b[:i], b[i:i+3]...)) case 'y' | '$'< 1 && mod != -1 { k.Mod |= KeyMod(mod - 1) } // Handle URxvt weird keys switch cmd { case '~': // Don't forget to handle Kitty keyboard protocol return i, parseKittyKeyboardExt(pa, k) case '^': k.Mod |= ModCtrl case '@': k.Mod |= ModCtrl | ModShift } return i, k } case 't': param, _, ok := pa.Param(0, 0) if !ok { break } switch param { case 4: // Report Terminal window size in pixels. if paramsLen == 3 { height, _, hOk := pa.Param(1, 0) width, _, wOk := pa.Param(2, 0) if !hOk || !wOk { break } return i, PixelSizeEvent{Width: width, Height: height} } case 6: // Report Terminal character cell size. if paramsLen == 3 { height, _, hOk := pa.Param(1, 0) width, _, wOk := pa.Param(2, 0) if !hOk || !wOk { break } return i, CellSizeEvent{Width: width, Height: height} } case 8: // Report Terminal Window size in cells. if paramsLen == 3 { height, _, hOk := pa.Param(1, 0) width, _, wOk := pa.Param(2, 0) if !hOk || !wOk { break } return i, WindowSizeEvent{Width: width, Height: height} } case 48: // In band terminal size report. if paramsLen == 5 { cellHeight, _, chOk := pa.Param(1, 0) cellWidth, _, cwOk := pa.Param(2, 0) pixelHeight, _, phOk := pa.Param(3, 0) pixelWidth, _, pwOk := pa.Param(4, 0) if !chOk || !cwOk || !phOk || !pwOk { break } return i, MultiEvent{ WindowSizeEvent{Width: cellWidth, Height: cellHeight}, PixelSizeEvent{Width: pixelWidth, Height: pixelHeight}, } } } // Any other window operation event. var winop WindowOpEvent winop.Op = param for j := 1; j < paramsLen; j++ { val, _, ok := pa.Param(j, 0) if ok { winop.Args = append(winop.Args, val) } } return i, winop } return i, UnknownCsiEvent(b[:i]) } // parseSs3 parses a SS3 sequence. // See https://vt100.net/docs/vt220-rm/chapter4.html#S4.4.4.2 func (p *EventDecoder) parseSs3(b []byte) (int, Event) { if len(b) == 2 && b[0] == ansi.ESC { // short cut if this is an alt+O key return 2, KeyPressEvent{Code: unicode.ToLower(rune(b[1])), Mod: ModShift | ModAlt} } var i int if b[i] == ansi.SS3 || b[i] == ansi.ESC { i++ } if i < len(b) && b[i-1] == ansi.ESC && b[i] == 'O' { i++ } // Scan numbers from 0-9 var mod int for ; i < len(b) && b[i] >= '0' && b[i] <= '9'; i++ { mod *= 10 mod += int(b[i]) - '0' } // Scan a GL character // A GL character is a single byte in the range 0x21-0x7E // See https://vt100.net/docs/vt220-rm/chapter2.html#S2.3.2 if i >= len(b) || b[i] < 0x21 || b[i] > 0x7E { return i, UnknownEvent(b[:i]) } // GL character(s) gl := b[i] i++ var k KeyPressEvent switch gl { case 'a', 'b', 'c', 'd': k = KeyPressEvent{Code: KeyUp + rune(gl-'a'), Mod: ModCtrl} case 'A', 'B', 'C', 'D': k = KeyPressEvent{Code: KeyUp + rune(gl-'A')} case 'E': k = KeyPressEvent{Code: KeyBegin} case 'F': k = KeyPressEvent{Code: KeyEnd} case 'H': k = KeyPressEvent{Code: KeyHome} case 'P', 'Q', 'R', 'S': k = KeyPressEvent{Code: KeyF1 + rune(gl-'P')} case 'M': k = KeyPressEvent{Code: KeyKpEnter} case 'X': k = KeyPressEvent{Code: KeyKpEqual} case 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y': k = KeyPressEvent{Code: KeyKpMultiply + rune(gl-'j')} default: return i, UnknownSs3Event(b[:i]) } // Handle weird SS3 Func if mod > 0 { k.Mod |= KeyMod(mod - 1) } return i, k } func (p *EventDecoder) parseOsc(b []byte) (int, Event) { defaultKey := func() KeyPressEvent { return KeyPressEvent{Code: rune(b[1]), Mod: ModAlt} } if len(b) == 2 && b[0] == ansi.ESC { // short cut if this is an alt+] key return 2, defaultKey() } var i int if b[i] == ansi.OSC || b[i] == ansi.ESC { i++ } if i < len(b) && b[i-1] == ansi.ESC && b[i] == ']' { i++ } // Parse OSC command // An OSC sequence is terminated by a BEL, ESC, or ST character var start, end int cmd := -1 for ; i < len(b) && b[i] >= '0' && b[i] <= '9'; i++ { if cmd == -1 { cmd = 0 } else { cmd *= 10 } cmd += int(b[i]) - '0' } if i < len(b) && b[i] == ';' { // mark the start of the sequence data i++ start = i } for ; i < len(b); i++ { // advance to the end of the sequence if slices.Contains([]byte{ansi.BEL, ansi.ESC, ansi.ST, ansi.CAN, ansi.SUB}, b[i]) { break } } if i >= len(b) { return i, UnknownEvent(b[:i]) } end = i // end of the sequence data i++ // Check 7-bit ST (string terminator) character switch b[i-1] { case ansi.CAN, ansi.SUB: return i, ignoredEvent(b[:i]) case ansi.ESC: if i >= len(b) || b[i] != '\\' { if cmd == -1 || (start == 0 && end == 2) { return 2, defaultKey() } // If we don't have a valid ST terminator, then this is a // cancelled sequence and should be ignored. return i, ignoredEvent(b[:i]) } i++ } if end <= start { return i, UnknownEvent(b[:i]) } data := string(b[start:end]) switch cmd { case 10: return i, ForegroundColorEvent{ansi.XParseColor(data)} case 11: return i, BackgroundColorEvent{ansi.XParseColor(data)} case 12: return i, CursorColorEvent{ansi.XParseColor(data)} case 52: parts := strings.Split(data, ";") if len(parts) != 2 || len(parts[0]) < 1 { return i, ClipboardEvent{} } b64 := parts[1] bts, err := base64.StdEncoding.DecodeString(b64) if err != nil { return i, ClipboardEvent{Content: parts[1]} } sel := ClipboardSelection(parts[0][0]) //nolint:unconvert return i, ClipboardEvent{Selection: sel, Content: string(bts)} } return i, UnknownOscEvent(b[:i]) } // parseStTerminated parses a control sequence that gets terminated by a ST character. func (p *EventDecoder) parseStTerminated(intro8, intro7 byte, fn func([]byte) Event) func([]byte) (int, Event) { defaultKey := func(b []byte) (int, Event) { switch intro8 { case ansi.SOS: return 2, KeyPressEvent{Code: unicode.ToLower(rune(b[1])), Mod: ModShift | ModAlt} case ansi.PM, ansi.APC: return 2, KeyPressEvent{Code: rune(b[1]), Mod: ModAlt} } return 0, nil } return func(b []byte) (int, Event) { if len(b) == 2 && b[0] == ansi.ESC { return defaultKey(b) } var i int if b[i] == intro8 || b[i] == ansi.ESC { i++ } if i < len(b) && b[i-1] == ansi.ESC && b[i] == intro7 { i++ } // Scan control sequence // Most common control sequence is terminated by a ST character // ST is a 7-bit string terminator character is (ESC \) start := i for ; i < len(b); i++ { if slices.Contains([]byte{ansi.ESC, ansi.ST, ansi.CAN, ansi.SUB}, b[i]) { break } } if i >= len(b) { return i, UnknownEvent(b[:i]) } end := i // end of the sequence data i++ // Check 7-bit ST (string terminator) character switch b[i-1] { case ansi.CAN, ansi.SUB: return i, ignoredEvent(b[:i]) case ansi.ESC: if i >= len(b) || b[i] != '\\' { if start == end { return defaultKey(b) } // If we don't have a valid ST terminator, then this is a // cancelled sequence and should be ignored. return i, ignoredEvent(b[:i]) } i++ } // Call the function to parse the sequence and return the result if fn != nil { if e := fn(b[start:end]); e != nil { return i, e } } switch intro8 { case ansi.PM: return i, UnknownPmEvent(b[:i]) case ansi.SOS: return i, UnknownSosEvent(b[:i]) case ansi.APC: return i, UnknownApcEvent(b[:i]) default: return i, UnknownEvent(b[:i]) } } } func (p *EventDecoder) parseDcs(b []byte) (int, Event) { if len(b) == 2 && b[0] == ansi.ESC { // short cut if this is an alt+P key return 2, KeyPressEvent{Code: unicode.ToLower(rune(b[1])), Mod: ModShift | ModAlt} } var params [16]ansi.Param var paramsLen int var cmd ansi.Cmd // DCS sequences are introduced by DCS (0x90) or ESC P (0x1b 0x50) var i int if b[i] == ansi.DCS || b[i] == ansi.ESC { i++ } if i < len(b) && b[i-1] == ansi.ESC && b[i] == 'P' { i++ } // initial DCS byte if i < len(b) && b[i] >= '<' && b[i] <= '?' { cmd |= ansi.Cmd(b[i]) << parser.PrefixShift } // Scan parameter bytes in the range 0x30-0x3F var j int for j = 0; i < len(b) && paramsLen < len(params) && b[i] >= 0x30 && b[i] <= 0x3F; i, j = i+1, j+1 { if b[i] >= '0' && b[i] <= '9' { if params[paramsLen] == parser.MissingParam { params[paramsLen] = 0 } params[paramsLen] *= 10 params[paramsLen] += ansi.Param(b[i]) - '0' } if b[i] == ':' { params[paramsLen] |= parser.HasMoreFlag } if b[i] == ';' || b[i] == ':' { paramsLen++ if paramsLen < len(params) { // Don't overflow the params slice params[paramsLen] = parser.MissingParam } } } if j > 0 && paramsLen < len(params) { // has parameters paramsLen++ } // Scan intermediate bytes in the range 0x20-0x2F var intermed byte for j := 0; i < len(b) && b[i] >= 0x20 && b[i] <= 0x2F; i, j = i+1, j+1 { intermed = b[i] } // set intermediate byte cmd |= ansi.Cmd(intermed) << parser.IntermedShift // Scan final byte in the range 0x40-0x7E if i >= len(b) || b[i] < 0x40 || b[i] > 0x7E { return i, UnknownEvent(b[:i]) } // Add the final byte cmd |= ansi.Cmd(b[i]) i++ start := i // start of the sequence data for ; i < len(b); i++ { if b[i] == ansi.ST || b[i] == ansi.ESC { break } } if i >= len(b) { return i, UnknownEvent(b[:i]) } end := i // end of the sequence data i++ // Check 7-bit ST (string terminator) character if i < len(b) && b[i-1] == ansi.ESC && b[i] == '\\' { i++ } pa := ansi.Params(params[:paramsLen]) switch cmd { case 'r' | '+'<'< 1 { g.Payload = parts[1] } return g } return nil })(b) } func (p *EventDecoder) parseUtf8(b []byte) (int, Event) { if len(b) == 0 { return 0, nil } c := b[0] if c <= ansi.US || c == ansi.DEL { // Control codes get handled by parseControl return 1, p.parseControl(c) } else if c > ansi.US && c < ansi.DEL { // ASCII printable characters code := rune(c) k := KeyPressEvent{Code: code, Text: string(code)} if unicode.IsUpper(code) { // Convert upper case letters to lower case + shift modifier k.Code = unicode.ToLower(code) k.ShiftedCode = code k.Mod |= ModShift } return 1, k } code, _ := utf8.DecodeRune(b) if code == utf8.RuneError { return 1, UnknownEvent(b[0]) } cluster, _, _, _ := uniseg.FirstGraphemeCluster(b, -1) text := string(cluster) for i := range text { if i > 0 { // Use [KeyExtended] for multi-rune graphemes code = KeyExtended break } } return len(cluster), KeyPressEvent{Code: code, Text: text} } func (p *EventDecoder) parseControl(b byte) Event { switch b { case ansi.NUL: if p.Legacy&flagCtrlAt != 0 { return KeyPressEvent{Code: '@', Mod: ModCtrl} } return KeyPressEvent{Code: KeySpace, Mod: ModCtrl} case ansi.BS: return KeyPressEvent{Code: 'h', Mod: ModCtrl} case ansi.HT: if p.Legacy&flagCtrlI != 0 { return KeyPressEvent{Code: 'i', Mod: ModCtrl} } return KeyPressEvent{Code: KeyTab} case ansi.CR: if p.Legacy&flagCtrlM != 0 { return KeyPressEvent{Code: 'm', Mod: ModCtrl} } return KeyPressEvent{Code: KeyEnter} case ansi.ESC: if p.Legacy&flagCtrlOpenBracket != 0 { return KeyPressEvent{Code: '[', Mod: ModCtrl} } return KeyPressEvent{Code: KeyEscape} case ansi.DEL: if p.Legacy&flagBackspace != 0 { return KeyPressEvent{Code: KeyDelete} } return KeyPressEvent{Code: KeyBackspace} case ansi.SP: return KeyPressEvent{Code: KeySpace, Text: " "} default: if b >= ansi.SOH && b <= ansi.SUB { // Use lower case letters for control codes code := rune(b + 0x60) return KeyPressEvent{Code: code, Mod: ModCtrl} } else if b >= ansi.FS && b <= ansi.US { code := rune(b + 0x40) return KeyPressEvent{Code: code, Mod: ModCtrl} } return UnknownEvent(b) } } func parseXTermModifyOtherKeys(params ansi.Params) Event { // XTerm modify other keys starts with ESC [ 27 ; ; ~ xmod, _, _ := params.Param(1, 1) xrune, _, _ := params.Param(2, 1) mod := KeyMod(xmod - 1) r := rune(xrune) switch r { case ansi.BS: return KeyPressEvent{Mod: mod, Code: KeyBackspace} case ansi.HT: return KeyPressEvent{Mod: mod, Code: KeyTab} case ansi.CR: return KeyPressEvent{Mod: mod, Code: KeyEnter} case ansi.ESC: return KeyPressEvent{Mod: mod, Code: KeyEscape} case ansi.DEL: return KeyPressEvent{Mod: mod, Code: KeyBackspace} } // CSI 27 ; ; ~ keys defined in XTerm modifyOtherKeys k := KeyPressEvent{Code: r, Mod: mod} if k.Mod <= ModShift { k.Text = string(r) } return k } // Kitty Clipboard Control Sequences. var kittyKeyMap = map[int]Key{ ansi.BS: {Code: KeyBackspace}, ansi.HT: {Code: KeyTab}, ansi.CR: {Code: KeyEnter}, ansi.ESC: {Code: KeyEscape}, ansi.DEL: {Code: KeyBackspace}, 57344: {Code: KeyEscape}, 57345: {Code: KeyEnter}, 57346: {Code: KeyTab}, 57347: {Code: KeyBackspace}, 57348: {Code: KeyInsert}, 57349: {Code: KeyDelete}, 57350: {Code: KeyLeft}, 57351: {Code: KeyRight}, 57352: {Code: KeyUp}, 57353: {Code: KeyDown}, 57354: {Code: KeyPgUp}, 57355: {Code: KeyPgDown}, 57356: {Code: KeyHome}, 57357: {Code: KeyEnd}, 57358: {Code: KeyCapsLock}, 57359: {Code: KeyScrollLock}, 57360: {Code: KeyNumLock}, 57361: {Code: KeyPrintScreen}, 57362: {Code: KeyPause}, 57363: {Code: KeyMenu}, 57364: {Code: KeyF1}, 57365: {Code: KeyF2}, 57366: {Code: KeyF3}, 57367: {Code: KeyF4}, 57368: {Code: KeyF5}, 57369: {Code: KeyF6}, 57370: {Code: KeyF7}, 57371: {Code: KeyF8}, 57372: {Code: KeyF9}, 57373: {Code: KeyF10}, 57374: {Code: KeyF11}, 57375: {Code: KeyF12}, 57376: {Code: KeyF13}, 57377: {Code: KeyF14}, 57378: {Code: KeyF15}, 57379: {Code: KeyF16}, 57380: {Code: KeyF17}, 57381: {Code: KeyF18}, 57382: {Code: KeyF19}, 57383: {Code: KeyF20}, 57384: {Code: KeyF21}, 57385: {Code: KeyF22}, 57386: {Code: KeyF23}, 57387: {Code: KeyF24}, 57388: {Code: KeyF25}, 57389: {Code: KeyF26}, 57390: {Code: KeyF27}, 57391: {Code: KeyF28}, 57392: {Code: KeyF29}, 57393: {Code: KeyF30}, 57394: {Code: KeyF31}, 57395: {Code: KeyF32}, 57396: {Code: KeyF33}, 57397: {Code: KeyF34}, 57398: {Code: KeyF35}, 57399: {Code: KeyKp0}, 57400: {Code: KeyKp1}, 57401: {Code: KeyKp2}, 57402: {Code: KeyKp3}, 57403: {Code: KeyKp4}, 57404: {Code: KeyKp5}, 57405: {Code: KeyKp6}, 57406: {Code: KeyKp7}, 57407: {Code: KeyKp8}, 57408: {Code: KeyKp9}, 57409: {Code: KeyKpDecimal}, 57410: {Code: KeyKpDivide}, 57411: {Code: KeyKpMultiply}, 57412: {Code: KeyKpMinus}, 57413: {Code: KeyKpPlus}, 57414: {Code: KeyKpEnter}, 57415: {Code: KeyKpEqual}, 57416: {Code: KeyKpSep}, 57417: {Code: KeyKpLeft}, 57418: {Code: KeyKpRight}, 57419: {Code: KeyKpUp}, 57420: {Code: KeyKpDown}, 57421: {Code: KeyKpPgUp}, 57422: {Code: KeyKpPgDown}, 57423: {Code: KeyKpHome}, 57424: {Code: KeyKpEnd}, 57425: {Code: KeyKpInsert}, 57426: {Code: KeyKpDelete}, 57427: {Code: KeyKpBegin}, 57428: {Code: KeyMediaPlay}, 57429: {Code: KeyMediaPause}, 57430: {Code: KeyMediaPlayPause}, 57431: {Code: KeyMediaReverse}, 57432: {Code: KeyMediaStop}, 57433: {Code: KeyMediaFastForward}, 57434: {Code: KeyMediaRewind}, 57435: {Code: KeyMediaNext}, 57436: {Code: KeyMediaPrev}, 57437: {Code: KeyMediaRecord}, 57438: {Code: KeyLowerVol}, 57439: {Code: KeyRaiseVol}, 57440: {Code: KeyMute}, 57441: {Code: KeyLeftShift}, 57442: {Code: KeyLeftCtrl}, 57443: {Code: KeyLeftAlt}, 57444: {Code: KeyLeftSuper}, 57445: {Code: KeyLeftHyper}, 57446: {Code: KeyLeftMeta}, 57447: {Code: KeyRightShift}, 57448: {Code: KeyRightCtrl}, 57449: {Code: KeyRightAlt}, 57450: {Code: KeyRightSuper}, 57451: {Code: KeyRightHyper}, 57452: {Code: KeyRightMeta}, 57453: {Code: KeyIsoLevel3Shift}, 57454: {Code: KeyIsoLevel5Shift}, } func init() { // These are some faulty C0 mappings some terminals such as WezTerm have // and doesn't follow the specs. kittyKeyMap[ansi.NUL] = Key{Code: KeySpace, Mod: ModCtrl} for i := ansi.SOH; i <= ansi.SUB; i++ { if _, ok := kittyKeyMap[i]; !ok { kittyKeyMap[i] = Key{Code: rune(i + 0x60), Mod: ModCtrl} } } for i := ansi.FS; i <= ansi.US; i++ { if _, ok := kittyKeyMap[i]; !ok { kittyKeyMap[i] = Key{Code: rune(i + 0x40), Mod: ModCtrl} } } } const ( kittyShift = 1 << iota kittyAlt kittyCtrl kittySuper kittyHyper kittyMeta kittyCapsLock kittyNumLock ) func fromKittyMod(mod int) KeyMod { var m KeyMod if mod&kittyShift != 0 { m |= ModShift } if mod&kittyAlt != 0 { m |= ModAlt } if mod&kittyCtrl != 0 { m |= ModCtrl } if mod&kittySuper != 0 { m |= ModSuper } if mod&kittyHyper != 0 { m |= ModHyper } if mod&kittyMeta != 0 { m |= ModMeta } if mod&kittyCapsLock != 0 { m |= ModCapsLock } if mod&kittyNumLock != 0 { m |= ModNumLock } return m } // parseKittyKeyboard parses a Kitty Keyboard Protocol sequence. // // In `CSI u`, this is parsed as: // // CSI codepoint ; modifiers u // codepoint: ASCII Dec value // // The Kitty Keyboard Protocol extends this with optional components that can be // enabled progressively. The full sequence is parsed as: // // CSI unicode-key-code:alternate-key-codes ; modifiers:event-type ; text-as-codepoints u // // See https://sw.kovidgoyal.net/kitty/keyboard-protocol/ func parseKittyKeyboard(params ansi.Params) (Event Event) { var isRelease bool var key Key // The index of parameters separated by semicolons ';'. Sub parameters are // separated by colons ':'. var paramIdx int var sudIdx int // The sub parameter index for _, p := range params { // Kitty Keyboard Protocol has 3 optional components. switch paramIdx { case 0: switch sudIdx { case 0: var foundKey bool code := p.Param(1) // CSI u has a default value of 1 key, foundKey = kittyKeyMap[code] if !foundKey { r := rune(code) if !utf8.ValidRune(r) { r = utf8.RuneError } key.Code = r } case 2: // shifted key + base key if b := rune(p.Param(1)); unicode.IsPrint(b) { // XXX: When alternate key reporting is enabled, the protocol // can return 3 things, the unicode codepoint of the key, // the shifted codepoint of the key, and the standard // PC-101 key layout codepoint. // This is useful to create an unambiguous mapping of keys // when using a different language layout. key.BaseCode = b } fallthrough case 1: // shifted key if s := rune(p.Param(1)); unicode.IsPrint(s) { // XXX: We swap keys here because we want the shifted key // to be the Rune that is returned by the event. // For example, shift+a should produce "A" not "a". // In such a case, we set AltRune to the original key "a" // and Rune to "A". key.ShiftedCode = s } } case 1: switch sudIdx { case 0: mod := p.Param(1) if mod > 1 { key.Mod = fromKittyMod(mod - 1) if key.Mod > ModShift { // XXX: We need to clear the text if we have a modifier key // other than a [ModShift] key. key.Text = "" } } case 1: switch p.Param(1) { case 2: key.IsRepeat = true case 3: isRelease = true } case 2: } case 2: if code := p.Param(0); code != 0 { key.Text += string(rune(code)) } } sudIdx++ if !p.HasMore() { paramIdx++ sudIdx = 0 } } keyMod := key.Mod // Remove these lock modifiers from now on since they don't affect the text. keyMod &^= ModNumLock // keyMod &^= ModScrollLock // Kitty doesn't support scroll lock printMod := keyMod <= ModShift || keyMod == ModCapsLock || keyMod == (ModShift|ModCapsLock) printKeyPad := key.Code >= KeyKpEqual && key.Code <= KeyKpSep if len(key.Text) == 0 && printKeyPad && printMod { switch { case key.Code >= KeyKp0 && key.Code <= KeyKp9: key.Text = string('0' + key.Code - KeyKp0) case key.Code == KeyKpEqual: key.Text = "=" case key.Code == KeyKpMultiply: key.Text = "*" case key.Code == KeyKpPlus: key.Text = "+" case key.Code == KeyKpMinus: key.Text = "-" case key.Code == KeyKpDecimal: key.Text = "." case key.Code == KeyKpDivide: key.Text = "/" case key.Code == KeyKpSep: key.Text = "," } } //nolint:nestif if len(key.Text) == 0 && unicode.IsPrint(key.Code) && printMod { if keyMod == 0 { key.Text = string(key.Code) } else { desiredCase := unicode.ToLower if keyMod.Contains(ModShift) || keyMod.Contains(ModCapsLock) { desiredCase = unicode.ToUpper } if key.ShiftedCode != 0 { key.Text = string(key.ShiftedCode) } else { key.Text = string(desiredCase(key.Code)) } } } if isRelease { return KeyReleaseEvent(key) } return KeyPressEvent(key) } // parseKittyKeyboardExt parses a Kitty Keyboard Protocol sequence extensions // for non CSI u sequences. This includes things like CSI A, SS3 A and others, // and CSI ~. func parseKittyKeyboardExt(params ansi.Params, k KeyPressEvent) Event { // Handle Kitty keyboard protocol if len(params) > 2 && // We have at least 3 parameters params[1].HasMore() { // The second parameter is a subparameter (separated by a ":") switch params[2].Param(1) { // The third parameter is the event type (defaults to 1) case 2: k.IsRepeat = true case 3: return KeyReleaseEvent(k) } } return k } func parsePrimaryDevAttrs(params ansi.Params) Event { // Primary Device Attributes da1 := make(PrimaryDeviceAttributesEvent, len(params)) for i, p := range params { if !p.HasMore() { da1[i] = p.Param(0) } } return da1 } func parseSecondaryDevAttrs(params ansi.Params) Event { // Secondary Device Attributes da2 := make(SecondaryDeviceAttributesEvent, len(params)) for i, p := range params { if !p.HasMore() { da2[i] = p.Param(0) } } return da2 } func parseTertiaryDevAttrs(b []byte) Event { // Tertiary Device Attributes // The response is a 4-digit hexadecimal number. bts, err := hex.DecodeString(string(b)) if err != nil { return UnknownDcsEvent(fmt.Sprintf("\x1bP!|%s\x1b\\", b)) } return TertiaryDeviceAttributesEvent(bts) } // Parse SGR-encoded mouse events; SGR extended mouse events. SGR mouse events // look like: // // ESC [ < Cb ; Cx ; Cy (M or m) // // where: // // Cb is the encoded button code // Cx is the x-coordinate of the mouse // Cy is the y-coordinate of the mouse // M is for button press, m is for button release // // https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates func parseSGRMouseEvent(cmd ansi.Cmd, params ansi.Params) Event { x, _, ok := params.Param(1, 1) if !ok { x = 1 } y, _, ok := params.Param(2, 1) if !ok { y = 1 } release := cmd.Final() == 'm' b, _, _ := params.Param(0, 0) mod, btn, _, isMotion := parseMouseButton(b) // (1,1) is the upper left. We subtract 1 to normalize it to (0,0). x-- y-- m := Mouse{X: x, Y: y, Button: btn, Mod: mod} // Wheel buttons don't have release events // Motion can be reported as a release event in some terminals (Windows Terminal) if isWheel(m.Button) { return MouseWheelEvent(m) } else if !isMotion && release { return MouseReleaseEvent(m) } else if isMotion { return MouseMotionEvent(m) } return MouseClickEvent(m) } const x10MouseByteOffset = 32 // Parse X10-encoded mouse events; the simplest kind. The last release of X10 // was December 1986, by the way. The original X10 mouse protocol limits the Cx // and Cy coordinates to 223 (=255-032). // // X10 mouse events look like: // // ESC [M Cb Cx Cy // // See: http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking func parseX10MouseEvent(buf []byte) Event { v := buf[3:6] b := int(v[0]) if b >= x10MouseByteOffset { // XXX: b < 32 should be impossible, but we're being defensive. b -= x10MouseByteOffset } mod, btn, isRelease, isMotion := parseMouseButton(b) // (1,1) is the upper left. We subtract 1 to normalize it to (0,0). x := int(v[1]) - x10MouseByteOffset - 1 y := int(v[2]) - x10MouseByteOffset - 1 m := Mouse{X: x, Y: y, Button: btn, Mod: mod} if isWheel(m.Button) { return MouseWheelEvent(m) } else if isMotion { return MouseMotionEvent(m) } else if isRelease { return MouseReleaseEvent(m) } return MouseClickEvent(m) } // See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates func parseMouseButton(b int) (mod KeyMod, btn MouseButton, isRelease bool, isMotion bool) { // mouse bit shifts const ( bitShift = 0b0000_0100 bitAlt = 0b0000_1000 bitCtrl = 0b0001_0000 bitMotion = 0b0010_0000 bitWheel = 0b0100_0000 bitAdd = 0b1000_0000 // additional buttons 8-11 bitsMask = 0b0000_0011 ) // Modifiers if b&bitAlt != 0 { mod |= ModAlt } if b&bitCtrl != 0 { mod |= ModCtrl } if b&bitShift != 0 { mod |= ModShift } if b&bitAdd != 0 { btn = MouseBackward + MouseButton(b&bitsMask) } else if b&bitWheel != 0 { btn = MouseWheelUp + MouseButton(b&bitsMask) } else { btn = MouseLeft + MouseButton(b&bitsMask) // X10 reports a button release as 0b0000_0011 (3) if b&bitsMask == bitsMask { btn = MouseNone isRelease = true } } // Motion bit doesn't get reported for wheel events. if b&bitMotion != 0 && !isWheel(btn) { isMotion = true } return //nolint:nakedret } // isWheel returns true if the mouse event is a wheel event. func isWheel(btn MouseButton) bool { return btn >= MouseWheelUp && btn <= MouseWheelRight } type shiftable interface { ~uint | ~uint16 | ~uint32 | ~uint64 } func shift[T shiftable](x T) T { if x > 0xff { x >>= 8 } return x } func colorToHex(c color.Color) string { if c == nil { return "" } r, g, b, _ := c.RGBA() return fmt.Sprintf("#%02x%02x%02x", shift(r), shift(g), shift(b)) } func getMaxMin(a, b, c float64) (ma, mi float64) { if a > b { ma = a mi = b } else { ma = b mi = a } if c > ma { ma = c } else if c < mi { mi = c } return ma, mi } func round(x float64) float64 { return math.Round(x*1000) / 1000 } // rgbToHSL converts an RGB triple to an HSL triple. func rgbToHSL(r, g, b uint8) (h, s, l float64) { // convert uint32 pre-multiplied value to uint8 // The r,g,b values are divided by 255 to change the range from 0..255 to 0..1: Rnot := float64(r) / 255 Gnot := float64(g) / 255 Bnot := float64(b) / 255 Cmax, Cmin := getMaxMin(Rnot, Gnot, Bnot) Δ := Cmax - Cmin // Lightness calculation: l = (Cmax + Cmin) / 2 // Hue and Saturation Calculation: if Δ == 0 { h = 0 s = 0 } else { switch Cmax { case Rnot: h = 60 * math.Mod((Gnot-Bnot)/Δ, 6) case Gnot: h = 60 * (((Bnot - Rnot) / Δ) + 2) case Bnot: h = 60 * (((Rnot - Gnot) / Δ) + 4) } if h < 0 { h += 360 } s = Δ / (1 - math.Abs((2*l)-1)) } return h, round(s), round(l) } // isDarkColor returns whether the given color is dark. func isDarkColor(c color.Color) bool { if c == nil { return true } r, g, b, _ := c.RGBA() _, _, l := rgbToHSL(uint8(r>>8), uint8(g>>8), uint8(b>>8)) //nolint:gosec return l < 0.5 } func parseTermcap(data []byte) CapabilityEvent { // XTGETTCAP if len(data) == 0 { return CapabilityEvent{""} } var tc strings.Builder split := bytes.Split(data, []byte{';'}) for _, s := range split { parts := bytes.SplitN(s, []byte{'='}, 2) if len(parts) == 0 { return CapabilityEvent{""} } name, err := hex.DecodeString(string(parts[0])) if err != nil || len(name) == 0 { continue } var value []byte if len(parts) > 1 { value, err = hex.DecodeString(string(parts[1])) if err != nil { continue } } if tc.Len() > 0 { tc.WriteByte(';') } tc.WriteString(string(name)) if len(value) > 0 { tc.WriteByte('=') tc.WriteString(string(value)) } } return CapabilityEvent{tc.String()} } // parseWin32InputKeyEvent converts a Windows Input Record Key Event into a // KeyPressEvent, KeyReleaseEvent, or MultiEvent including multiple of the // former type. // A special case is when the key is either part of VtInputMode which produces // vkc == 0, or when a key is a UTF-16 surrogate pair. In both of these cases, // we need to handle key encoding and properly parse the key event. func (p *EventDecoder) parseWin32InputKeyEvent(vkc uint16, _ uint16, r rune, keyDown bool, cks uint32, repeatCount uint16) (event Event) { defer func() { // Respect the repeat count. if repeatCount > 1 { var multi MultiEvent for i := 0; i < int(repeatCount); i++ { multi = append(multi, event) } event = multi } }() defer func() { if vkc != 0 { p.lastCks = cks } }() var key Key switch { case vkc == 0: // This is either a UTF-16 encoded pair, or an escape sequence waiting // to be decoded. if keyDown { return KeyPressEvent{Code: 0, BaseCode: r, Mod: translateControlKeyState(cks)} } return KeyReleaseEvent{Code: 0, BaseCode: r, Mod: translateControlKeyState(cks)} case vkc == xwindows.VK_BACK: key.BaseCode = KeyBackspace case vkc == xwindows.VK_TAB: key.BaseCode = KeyTab case vkc == xwindows.VK_RETURN: key.BaseCode = KeyEnter case vkc == xwindows.VK_SHIFT: //nolint:nestif if cks&xwindows.SHIFT_PRESSED != 0 { if cks&xwindows.ENHANCED_KEY != 0 { key.BaseCode = KeyRightShift } else { key.BaseCode = KeyLeftShift } } else if p.lastCks&xwindows.SHIFT_PRESSED != 0 { if p.lastCks&xwindows.ENHANCED_KEY != 0 { key.BaseCode = KeyRightShift } else { key.BaseCode = KeyLeftShift } } case vkc == xwindows.VK_CONTROL: if cks&xwindows.LEFT_CTRL_PRESSED != 0 { key.BaseCode = KeyLeftCtrl } else if cks&xwindows.RIGHT_CTRL_PRESSED != 0 { key.BaseCode = KeyRightCtrl } else if p.lastCks&xwindows.LEFT_CTRL_PRESSED != 0 { key.BaseCode = KeyLeftCtrl } else if p.lastCks&xwindows.RIGHT_CTRL_PRESSED != 0 { key.BaseCode = KeyRightCtrl } case vkc == xwindows.VK_MENU: if cks&xwindows.LEFT_ALT_PRESSED != 0 { key.BaseCode = KeyLeftAlt } else if cks&xwindows.RIGHT_ALT_PRESSED != 0 { key.BaseCode = KeyRightAlt } else if p.lastCks&xwindows.LEFT_ALT_PRESSED != 0 { key.BaseCode = KeyLeftAlt } else if p.lastCks&xwindows.RIGHT_ALT_PRESSED != 0 { key.BaseCode = KeyRightAlt } case vkc == xwindows.VK_PAUSE: key.BaseCode = KeyPause case vkc == xwindows.VK_CAPITAL: key.BaseCode = KeyCapsLock case vkc == xwindows.VK_ESCAPE: key.BaseCode = KeyEscape case vkc == xwindows.VK_SPACE: key.BaseCode = KeySpace case vkc == xwindows.VK_PRIOR: key.BaseCode = KeyPgUp case vkc == xwindows.VK_NEXT: key.BaseCode = KeyPgDown case vkc == xwindows.VK_END: key.BaseCode = KeyEnd case vkc == xwindows.VK_HOME: key.BaseCode = KeyHome case vkc == xwindows.VK_LEFT: key.BaseCode = KeyLeft case vkc == xwindows.VK_UP: key.BaseCode = KeyUp case vkc == xwindows.VK_RIGHT: key.BaseCode = KeyRight case vkc == xwindows.VK_DOWN: key.BaseCode = KeyDown case vkc == xwindows.VK_SELECT: key.BaseCode = KeySelect case vkc == xwindows.VK_SNAPSHOT: key.BaseCode = KeyPrintScreen case vkc == xwindows.VK_INSERT: key.BaseCode = KeyInsert case vkc == xwindows.VK_DELETE: key.BaseCode = KeyDelete case vkc >= '0' && vkc <= '9': key.BaseCode = rune(vkc) case vkc >= 'A' && vkc <= 'Z': // Convert to lowercase. key.BaseCode = rune(vkc) + 32 case vkc == xwindows.VK_LWIN: key.BaseCode = KeyLeftSuper case vkc == xwindows.VK_RWIN: key.BaseCode = KeyRightSuper case vkc == xwindows.VK_APPS: key.BaseCode = KeyMenu case vkc >= xwindows.VK_NUMPAD0 && vkc <= xwindows.VK_NUMPAD9: key.BaseCode = rune(vkc-xwindows.VK_NUMPAD0) + KeyKp0 key.Text = string('0' + (rune(vkc) - xwindows.VK_NUMPAD0)) case vkc == xwindows.VK_MULTIPLY: key.BaseCode = KeyKpMultiply key.Text = "*" case vkc == xwindows.VK_ADD: key.BaseCode = KeyKpPlus key.Text = "+" case vkc == xwindows.VK_SEPARATOR: key.BaseCode = KeyKpComma key.Text = "," case vkc == xwindows.VK_SUBTRACT: key.BaseCode = KeyKpMinus key.Text = "-" case vkc == xwindows.VK_DECIMAL: key.BaseCode = KeyKpDecimal key.Text = "." case vkc == xwindows.VK_DIVIDE: key.BaseCode = KeyKpDivide key.Text = "/" case vkc >= xwindows.VK_F1 && vkc <= xwindows.VK_F24: key.BaseCode = rune(vkc-xwindows.VK_F1) + KeyF1 case vkc == xwindows.VK_NUMLOCK: key.BaseCode = KeyNumLock case vkc == xwindows.VK_SCROLL: key.BaseCode = KeyScrollLock case vkc == xwindows.VK_LSHIFT: key.BaseCode = KeyLeftShift case vkc == xwindows.VK_RSHIFT: key.BaseCode = KeyRightShift case vkc == xwindows.VK_LCONTROL: key.BaseCode = KeyLeftCtrl case vkc == xwindows.VK_RCONTROL: key.BaseCode = KeyRightCtrl case vkc == xwindows.VK_LMENU: key.BaseCode = KeyLeftAlt case vkc == xwindows.VK_RMENU: key.BaseCode = KeyRightAlt case vkc == xwindows.VK_VOLUME_MUTE: key.BaseCode = KeyMute case vkc == xwindows.VK_VOLUME_DOWN: key.BaseCode = KeyLowerVol case vkc == xwindows.VK_VOLUME_UP: key.BaseCode = KeyRaiseVol case vkc == xwindows.VK_MEDIA_NEXT_TRACK: key.BaseCode = KeyMediaNext case vkc == xwindows.VK_MEDIA_PREV_TRACK: key.BaseCode = KeyMediaPrev case vkc == xwindows.VK_MEDIA_STOP: key.BaseCode = KeyMediaStop case vkc == xwindows.VK_MEDIA_PLAY_PAUSE: key.BaseCode = KeyMediaPlayPause case vkc == xwindows.VK_OEM_1: key.BaseCode = ';' case vkc == xwindows.VK_OEM_PLUS: key.BaseCode = '+' case vkc == xwindows.VK_OEM_COMMA: key.BaseCode = ',' case vkc == xwindows.VK_OEM_MINUS: key.BaseCode = '-' case vkc == xwindows.VK_OEM_PERIOD: key.BaseCode = '.' case vkc == xwindows.VK_OEM_2: key.BaseCode = '/' case vkc == xwindows.VK_OEM_3: key.BaseCode = '`' case vkc == xwindows.VK_OEM_4: key.BaseCode = '[' case vkc == xwindows.VK_OEM_5: key.BaseCode = '\\' case vkc == xwindows.VK_OEM_6: key.BaseCode = ']' case vkc == xwindows.VK_OEM_7: key.BaseCode = '\'' } // AltGr is left ctrl + right alt. On non-US keyboards, this is used to type // special characters and produce printable events. // XXX: Should this be a KeyMod? const altGrPressed = xwindows.LEFT_CTRL_PRESSED | xwindows.RIGHT_ALT_PRESSED altGr := cks&altGrPressed == altGrPressed // Remove these lock keys from the control key state from now on. cks &^= xwindows.NUMLOCK_ON cks &^= xwindows.SCROLLLOCK_ON key.Code = key.BaseCode if !unicode.IsControl(r) { key.Code = r if unicode.IsPrint(key.Code) && (cks == 0 || // no modifiers pressed cks == xwindows.SHIFT_PRESSED || // shift pressed cks == xwindows.CAPSLOCK_ON || // caps lock on cks == (xwindows.SHIFT_PRESSED|xwindows.CAPSLOCK_ON) || // Shift + caps lock pressed altGr) { // AltGr pressed // If the control key state is 0, shift is pressed, or caps lock // then the key event is a printable event i.e. [text] is not empty. key.Text = string(key.Code) } } key.Mod = translateControlKeyState(cks) key = ensureKeyCase(key, cks) if keyDown { return KeyPressEvent(key) } return KeyReleaseEvent(key) } // ensureKeyCase ensures that the key's text is in the correct case based on the // control key state. func ensureKeyCase(key Key, cks uint32) Key { if len(key.Text) == 0 { return key } hasShift := cks&xwindows.SHIFT_PRESSED != 0 hasCaps := cks&xwindows.CAPSLOCK_ON != 0 if hasShift || hasCaps { if unicode.IsLower(key.Code) { key.ShiftedCode = unicode.ToUpper(key.Code) key.Text = string(key.ShiftedCode) } } else { if unicode.IsUpper(key.Code) { key.ShiftedCode = unicode.ToLower(key.Code) key.Text = string(key.ShiftedCode) } } return key } // translateControlKeyState translates the control key state from the Windows // Console API into a Mod bitmask. func translateControlKeyState(cks uint32) (m KeyMod) { if cks&xwindows.LEFT_CTRL_PRESSED != 0 || cks&xwindows.RIGHT_CTRL_PRESSED != 0 { m |= ModCtrl } if cks&xwindows.LEFT_ALT_PRESSED != 0 || cks&xwindows.RIGHT_ALT_PRESSED != 0 { m |= ModAlt } if cks&xwindows.SHIFT_PRESSED != 0 { m |= ModShift } if cks&xwindows.CAPSLOCK_ON != 0 { m |= ModCapsLock } if cks&xwindows.NUMLOCK_ON != 0 { m |= ModNumLock } if cks&xwindows.SCROLLLOCK_ON != 0 { m |= ModScrollLock } return }