okami.mom/ultraviolet
git clone https://git.nkp.pet/okami.mom/ultraviolet
006e29f
upstream
1package uv 2 3import ( 4"bytes" 5"context" 6"flag" 7"fmt" 8"image/color" 9"io" 10"math/rand" 11"reflect" 12"regexp" 13"sort" 14"strings" 15"testing" 16"time" 17 18"github.com/charmbracelet/x/ansi" 19"github.com/charmbracelet/x/ansi/kitty" 20"github.com/charmbracelet/x/ansi/parser" 21"github.com/lucasb-eyer/go-colorful" 22) 23 24var sequences = buildKeysTable (LegacyKeyEncoding (0 ),"dumb" ,true ) 25 26func TestKeyString (t * testing.T ) { 27t .Run ("alt+space" ,func (t * testing.T ) { 28k := KeyPressEvent {Code :KeySpace ,Mod :ModAlt } 29if got := k .String ();got != "alt+space" { 30t .Fatalf (`expected a "alt+space", got %q` ,got ) 31 } 32 }) 33 34t .Run ("runes" ,func (t * testing.T ) { 35k := KeyPressEvent {Code :'a' ,Text :"a" } 36if got := k .String ();got != "a" { 37t .Fatalf (`expected an "a", got %q` ,got ) 38 } 39 }) 40 41t .Run ("invalid" ,func (t * testing.T ) { 42k := KeyPressEvent {Code :99999 } 43if got := k .String ();got != "𘚟" { 44t .Fatalf (`expected a "unknown", got %q` ,got ) 45 } 46 }) 47} 48 49type seqTest struct { 50seq []byte 51Events []Event 52} 53 54var f3CurPosRegexp = regexp .MustCompile (`\x1b\[1;(\d+)R` ) 55 56// buildBaseSeqTests returns sequence tests that are valid for the 57// detectSequence() function. 58func buildBaseSeqTests () []seqTest { 59td := []seqTest {} 60for seq ,key := range sequences { 61k := KeyPressEvent (key ) 62st := seqTest {seq : []byte (seq ),Events : []Event {k }} 63 64// XXX: This is a special case to handle F3 key sequence and cursor 65// position report having the same sequence. See [parseCsi] for more 66// information. 67if f3CurPosRegexp .MatchString (seq ) { 68st .Events = []Event {k ,CursorPositionEvent {Y :0 ,X :int (key .Mod )}} 69 } 70td = append (td ,st ) 71 } 72 73// Additional special cases. 74td = append (td , 75// Unrecognized CSI sequence. 76seqTest { 77 []byte {'\x1b' ,'[' ,'-' ,'-' ,'-' ,'-' ,'X' }, 78 []Event { 79UnknownCsiEvent ([]byte {'\x1b' ,'[' ,'-' ,'-' ,'-' ,'-' ,'X' }), 80 }, 81 }, 82// A lone space character. 83seqTest { 84 []byte {' ' }, 85 []Event { 86KeyPressEvent {Code :KeySpace ,Text :" " }, 87 }, 88 }, 89// An escape character with the alt modifier. 90seqTest { 91 []byte {'\x1b' ,' ' }, 92 []Event { 93KeyPressEvent {Code :KeySpace ,Mod :ModAlt }, 94 }, 95 }, 96 ) 97return td 98} 99 100func TestFocus (t * testing.T ) { 101var p EventDecoder 102_ ,e := p .Decode ([]byte ("\x1b[I" )) 103switch e .(type ) { 104case FocusEvent : 105// ok 106default : 107t .Error ("invalid sequence" ) 108 } 109} 110 111func TestBlur (t * testing.T ) { 112var p EventDecoder 113_ ,e := p .Decode ([]byte ("\x1b[O" )) 114switch e .(type ) { 115case BlurEvent : 116// ok 117default : 118t .Error ("invalid sequence" ) 119 } 120} 121 122func TestParseSequence (t * testing.T ) { 123td := buildBaseSeqTests () 124td = append (td , 125// OSC 11 response 126seqTest { 127 []byte ("\x1b]11;rgb:ffff/0000/ffff\x07" ), 128 []Event { 129BackgroundColorEvent {ansi .XParseColor ("rgb:ff/00/ff" )}, 130 }, 131 }, 132 133// Teritary Device Attributes (DA3) 134seqTest { 135 []byte ("\x1bP!|4368726d\x1b\\" ), 136 []Event { 137TertiaryDeviceAttributesEvent ("Chrm" ), 138 }, 139 }, 140 141// XTGETTCAP response 142seqTest { 143 []byte ("\x1bP1+r524742\x1b\\" ), 144 []Event { 145CapabilityEvent {"RGB" }, 146 }, 147 }, 148 149// Unknown sequences. 150seqTest { 151 []byte ("\x1b[z\x1bOz\x1bO2 \x1bP?1;2:3+zABC\x1b\\" ), 152 []Event { 153UnknownCsiEvent ("\x1b[z" ), 154UnknownSs3Event ("\x1bOz" ), 155UnknownEvent ("\x1bO2" ), 156KeyPressEvent {Code :KeySpace ,Text :" " }, 157UnknownDcsEvent ("\x1bP?1;2:3+zABC\x1b\\" ), 158 }, 159 }, 160 161// OSC 52 read clipboard 162seqTest { 163 []byte ("\x1b]52\x1b\\\x1b]52;c;!\x1b\\\x1b]52;c;aGk=\x1b\\" ), 164 []Event { 165ClipboardEvent {}, 166ClipboardEvent {Content :"!" }, 167ClipboardEvent {Content :"hi" ,Selection :'c' }, 168 }, 169 }, 170 171// Invalid Xterm modifyOtherKeys key sequence 172seqTest { 173 []byte ("\x1b[27;3~" ), 174 []Event {UnknownCsiEvent ("\x1b[27;3~" )}, 175 }, 176 177// Empty @ ^ ~ 178seqTest { 179 []byte ("\x1b[@\x1b[^\x1b[~" ), 180 []Event { 181UnknownCsiEvent ("\x1b[@" ), 182UnknownCsiEvent ("\x1b[^" ), 183UnknownCsiEvent ("\x1b[~" ), 184 }, 185 }, 186 187// Win32 input mode key sequences 188seqTest { 189 []byte ("\x1b[65;0;97;1;0;1_\x1b[0;0;0_" ), 190 []Event { 191KeyPressEvent {Code :'a' ,BaseCode :'a' ,Text :"a" }, 192UnknownCsiEvent ("\x1b[0;0;0_" ), 193 }, 194 }, 195 196// Report mode responses 197seqTest { 198 []byte ("\x1b[2;1$y\x1b[$y\x1b[2$y\x1b[2;$y" ), 199 []Event { 200ModeReportEvent {Mode :ansi .ModeKeyboardAction ,Value :ansi .ModeSet }, 201UnknownCsiEvent ("\x1b[$y" ), 202UnknownCsiEvent ("\x1b[2$y" ), 203ModeReportEvent {Mode :ansi .ModeKeyboardAction ,Value :ansi .ModeNotRecognized }, 204 }, 205 }, 206 207// Short X10 mouse input 208seqTest { 209 []byte ("\x1b[M !" ), 210 []Event { 211UnknownCsiEvent ("\x1b[M" ), 212KeyPressEvent {Code :' ' ,Text :" " }, 213KeyPressEvent {Code :'!' ,Text :"!" }, 214 }, 215 }, 216 217// Invalid report mode responses 218seqTest { 219 []byte ("\x1b[?$y\x1b[?1049$y\x1b[?1049;$y" ), 220 []Event { 221UnknownCsiEvent ("\x1b[?$y" ), 222UnknownCsiEvent ("\x1b[?1049$y" ), 223ModeReportEvent {Mode :ansi .ModeAltScreenSaveCursor ,Value :ansi .ModeNotRecognized }, 224 }, 225 }, 226 227// Xterm modifyOtherKeys response 228seqTest { 229 []byte ("\x1b[>4;1m\x1b[>4m\x1b[>3m" ), 230 []Event { 231ModifyOtherKeysEvent {1 }, 232UnknownCsiEvent ("\x1b[>4m" ), 233UnknownCsiEvent ("\x1b[>3m" ), 234 }, 235 }, 236 237// F3 with modifier key press or cursor position report 238seqTest { 239 []byte ("\x1b[1;5R\x1b[1;5;7R" ), 240 []Event { 241KeyPressEvent {Code :KeyF3 ,Mod :ModCtrl }, 242CursorPositionEvent {Y :0 ,X :4 }, 243UnknownCsiEvent ("\x1b[1;5;7R" ), 244 }, 245 }, 246 247// Cursor position report 248seqTest { 249 []byte ("\x1b[?12;34R\x1b[?14R" ), 250 []Event { 251CursorPositionEvent {Y :11 ,X :33 }, 252UnknownCsiEvent ("\x1b[?14R" ), 253 }, 254 }, 255 256// Unknown CSI sequence 257seqTest { 258 []byte ("\x1b[10;2;3c" ), 259 []Event {UnknownCsiEvent ([]byte ("\x1b[10;2;3c" ))}, 260 }, 261 262// Kitty Keyboard response 263seqTest { 264 []byte ("\x1b[?16u\x1b[?u" ), 265 []Event { 266KeyboardEnhancementsEvent {16 }, 267KeyboardEnhancementsEvent {0 }, 268 }, 269 }, 270 271// Secondary Device Attributes (DA2) 272seqTest { 273 []byte ("\x1b[>1;2;3c" ), 274 []Event {SecondaryDeviceAttributesEvent {1 ,2 ,3 }}, 275 }, 276 277// Primary Device Attributes (DA1) 278seqTest { 279 []byte ("\x1b[?1;2;3c" ), 280 []Event {PrimaryDeviceAttributesEvent {1 ,2 ,3 }}, 281 }, 282 283// esc followed by non-key event sequence 284seqTest { 285 []byte ("\x1b\x1b[?2004;1$y" ), 286 []Event { 287KeyPressEvent {Code :KeyEscape }, 288ModeReportEvent {Mode :ansi .ModeBracketedPaste ,Value :ansi .ModeSet }, 289 }, 290 }, 291 292// 8-bit sequences 293seqTest { 294 []byte ( 295"\x9bA" + // CSI A 296"\x8fA" + // SS3 A 297"\x90>|Ultraviolet\x1b\\" + // DCS >|Ultraviolet ST 298"\x9d11;#123456\x9c" + // OSC 11 ; #123456 ST 299"\x98hi\x9c" + // SOS hi ST 300"\x9fhello\x9c" + // APC hello ST 301"\x9ebye\x9c" ,// PM bye ST 302 ), 303 []Event { 304KeyPressEvent {Code :KeyUp }, 305KeyPressEvent {Code :KeyUp }, 306TerminalVersionEvent {"Ultraviolet" }, 307BackgroundColorEvent {ansi .XParseColor ("#123456" )}, 308UnknownSosEvent ("\x98hi\x9c" ), 309UnknownApcEvent ("\x9fhello\x9c" ), 310UnknownPmEvent ("\x9ebye\x9c" ), 311 }, 312 }, 313 314// Empty input 315seqTest { 316 []byte ("" ), 317 []Event (nil ), 318 }, 319 320// Broken escape sequence introducers. 321seqTest { 322 []byte ("\x1b[" ),// CSI 323 []Event {KeyPressEvent {Code :'[' ,Mod :ModAlt }}, 324 }, 325seqTest { 326 []byte ("\x1b]" ),// OSC 327 []Event {KeyPressEvent {Code :']' ,Mod :ModAlt }}, 328 }, 329seqTest { 330 []byte ("\x1b^" ),// PM 331 []Event {KeyPressEvent {Code :'^' ,Mod :ModAlt }}, 332 }, 333seqTest { 334 []byte ("\x1b_" ),// APC 335 []Event {KeyPressEvent {Code :'_' ,Mod :ModAlt }}, 336 }, 337seqTest { 338 []byte ("\x1bP" ),// DCS 339 []Event {KeyPressEvent {Code :'p' ,Mod :ModShift | ModAlt }}, 340 }, 341seqTest { 342 []byte ("\x1bX" ),// SOS 343 []Event {KeyPressEvent {Code :'x' ,Mod :ModShift | ModAlt }}, 344 }, 345seqTest { 346 []byte ("\x1bO" ),// SS3 347 []Event {KeyPressEvent {Code :'o' ,Mod :ModShift | ModAlt }}, 348 }, 349seqTest { 350 []byte ("\x1b" ),// ESC 351 []Event {KeyPressEvent {Code :KeyEscape }}, 352 }, 353 354// Kitty invalid key sequence 355seqTest { 356 []byte ("\x1b[u" ), 357 []Event {UnknownCsiEvent ("\x1b[u" )}, 358 }, 359 360// Kitty printable keys with lock modifiers. 361seqTest { 362 []byte ("\x1b[97;65u" + // caps lock on 363"\x1b[97;2u" + // shift pressed 364"\x1b[97;65u" + // caps lock on 365"\x1b[97;66u" + // caps lock on and shift pressed 366"\x1b[97;129u" + // num lock on 367"\x1b[97;130u" + // num lock on and shift pressed 368"\x1b[97;194u" ),// num lock on and caps lock on and shift pressed 369 []Event { 370KeyPressEvent {Code :'a' ,Text :"A" ,Mod :ModCapsLock }, 371KeyPressEvent {Code :'a' ,Text :"A" ,Mod :ModShift }, 372KeyPressEvent {Code :'a' ,Text :"A" ,Mod :ModCapsLock }, 373KeyPressEvent {Code :'a' ,Text :"A" ,Mod :ModCapsLock | ModShift }, 374KeyPressEvent {Code :'a' ,Text :"a" ,Mod :ModNumLock }, 375KeyPressEvent {Code :'a' ,Text :"A" ,Mod :ModNumLock | ModShift }, 376KeyPressEvent {Code :'a' ,Text :"A" ,Mod :ModNumLock | ModCapsLock | ModShift }, 377 }, 378 }, 379 380// Kitty NumPad keys. 381seqTest { 382 []byte ("\x1b[57399u" + 383"\x1b[57400u" + 384"\x1b[57401u" + 385"\x1b[57402u" + 386"\x1b[57403u" + 387"\x1b[57404u" + 388"\x1b[57405u" + 389"\x1b[57406u" + 390"\x1b[57407u" + 391"\x1b[57408u" + 392"\x1b[57409u" + 393"\x1b[57410u" + 394"\x1b[57411u" + 395"\x1b[57412u" + 396"\x1b[57413u" + 397"\x1b[57414u" + 398"\x1b[57415u" + 399"\x1b[57416u" + 400"\x1b[57417u" + 401"\x1b[57418u" + 402"\x1b[57419u" + 403"\x1b[57420u" + 404"\x1b[57421u" + 405"\x1b[57422u" + 406"\x1b[57423u" + 407"\x1b[57424u" + 408"\x1b[57425u" ), 409 []Event { 410KeyPressEvent {Code :KeyKp0 ,Text :"0" }, 411KeyPressEvent {Code :KeyKp1 ,Text :"1" }, 412KeyPressEvent {Code :KeyKp2 ,Text :"2" }, 413KeyPressEvent {Code :KeyKp3 ,Text :"3" }, 414KeyPressEvent {Code :KeyKp4 ,Text :"4" }, 415KeyPressEvent {Code :KeyKp5 ,Text :"5" }, 416KeyPressEvent {Code :KeyKp6 ,Text :"6" }, 417KeyPressEvent {Code :KeyKp7 ,Text :"7" }, 418KeyPressEvent {Code :KeyKp8 ,Text :"8" }, 419KeyPressEvent {Code :KeyKp9 ,Text :"9" }, 420KeyPressEvent {Code :KeyKpDecimal ,Text :"." }, 421KeyPressEvent {Code :KeyKpDivide ,Text :"/" }, 422KeyPressEvent {Code :KeyKpMultiply ,Text :"*" }, 423KeyPressEvent {Code :KeyKpMinus ,Text :"-" }, 424KeyPressEvent {Code :KeyKpPlus ,Text :"+" }, 425KeyPressEvent {Code :KeyKpEnter }, 426KeyPressEvent {Code :KeyKpEqual ,Text :"=" }, 427KeyPressEvent {Code :KeyKpSep ,Text :"," }, 428KeyPressEvent {Code :KeyKpLeft }, 429KeyPressEvent {Code :KeyKpRight }, 430KeyPressEvent {Code :KeyKpUp }, 431KeyPressEvent {Code :KeyKpDown }, 432KeyPressEvent {Code :KeyKpPgUp }, 433KeyPressEvent {Code :KeyKpPgDown }, 434KeyPressEvent {Code :KeyKpHome }, 435KeyPressEvent {Code :KeyKpEnd }, 436KeyPressEvent {Code :KeyKpInsert }, 437 }, 438 }, 439 440// Invalid CSI sequence. 441seqTest { 442 []byte ("\x1b[?2004;1$y" ), 443 []Event {ModeReportEvent {Mode :ansi .ModeBracketedPaste ,Value :ansi .ModeSet }}, 444 }, 445 446// Invalid CSI sequence. 447seqTest { 448 []byte ("\x1b[?2004;1$" ), 449 []Event {UnknownEvent ("\x1b[?2004;1$" )}, 450 }, 451 452// Light/dark color scheme reports. 453seqTest { 454 []byte ("\x1b[?997;1n" ), 455 []Event {DarkColorSchemeEvent {}}, 456 }, 457seqTest { 458 []byte ("\x1b[?997;2n" ), 459 []Event {LightColorSchemeEvent {}}, 460 }, 461 462// ESC [ [ansi.CSI] 463seqTest { 464 []byte ("\x1b[" ), 465 []Event {KeyPressEvent {Code :'[' ,Mod :ModAlt }}, 466 }, 467// ESC ] [ansi.OSC] 468seqTest { 469 []byte ("\x1b]" ), 470 []Event {KeyPressEvent {Code :']' ,Mod :ModAlt }}, 471 }, 472// ESC ^ [ansi.PM] 473seqTest { 474 []byte ("\x1b^" ), 475 []Event {KeyPressEvent {Code :'^' ,Mod :ModAlt }}, 476 }, 477// ESC _ [ansi.APC] 478seqTest { 479 []byte ("\x1b_" ), 480 []Event {KeyPressEvent {Code :'_' ,Mod :ModAlt }}, 481 }, 482// ESC p 483seqTest { 484 []byte ("\x1bp" ), 485 []Event {KeyPressEvent {Code :'p' ,Mod :ModAlt }}, 486 }, 487// ESC P [ansi.DCS] 488seqTest { 489 []byte ("\x1bP" ), 490 []Event {KeyPressEvent {Code :'p' ,Mod :ModShift | ModAlt }}, 491 }, 492// ESC x 493seqTest { 494 []byte ("\x1bx" ), 495 []Event {KeyPressEvent {Code :'x' ,Mod :ModAlt }}, 496 }, 497// ESC X [ansi.SOS] 498seqTest { 499 []byte ("\x1bX" ), 500 []Event {KeyPressEvent {Code :'x' ,Mod :ModShift | ModAlt }}, 501 }, 502 503// OSC 11 with ST termination. 504seqTest { 505 []byte ("\x1b]11;#123456\x1b\\" ), 506 []Event {BackgroundColorEvent { 507Color :func () color.Color { 508c ,_ := colorful .Hex ("#123456" ) 509return c 510 }(), 511 }}, 512 }, 513 514// Kitty Graphics response. 515seqTest { 516 []byte ("\x1b_Ga=t;OK\x1b\\" ), 517 []Event {KittyGraphicsEvent { 518Options : kitty.Options {Action :kitty .Transmit }, 519Payload : []byte ("OK" ), 520 }}, 521 }, 522seqTest { 523 []byte ("\x1b_Gi=99,I=13;OK\x1b\\" ), 524 []Event {KittyGraphicsEvent { 525Options : kitty.Options {ID :99 ,Number :13 }, 526Payload : []byte ("OK" ), 527 }}, 528 }, 529seqTest { 530 []byte ("\x1b_Gi=1337,q=1;EINVAL:your face\x1b\\" ), 531 []Event {KittyGraphicsEvent { 532Options : kitty.Options {ID :1337 ,Quiet :1 }, 533Payload : []byte ("EINVAL:your face" ), 534 }}, 535 }, 536 537// Xterm modifyOtherKeys CSI 27 ; <modifier> ; <code> ~ 538seqTest { 539 []byte ("\x1b[27;3;20320~" ), 540 []Event {KeyPressEvent {Code :'你' ,Mod :ModAlt }}, 541 }, 542seqTest { 543 []byte ("\x1b[27;3;65~" ), 544 []Event {KeyPressEvent {Code :'A' ,Mod :ModAlt }}, 545 }, 546seqTest { 547 []byte ("\x1b[27;3;8~" ), 548 []Event {KeyPressEvent {Code :KeyBackspace ,Mod :ModAlt }}, 549 }, 550seqTest { 551 []byte ("\x1b[27;3;27~" ), 552 []Event {KeyPressEvent {Code :KeyEscape ,Mod :ModAlt }}, 553 }, 554seqTest { 555 []byte ("\x1b[27;3;127~" ), 556 []Event {KeyPressEvent {Code :KeyBackspace ,Mod :ModAlt }}, 557 }, 558 559// Xterm report window text area size in pixels. 560seqTest { 561 []byte ("\x1b[4;24;80t" + // window pixel size 562"\x1b[6;13;7t" + // single cell size 563"\x1b[8;24;80t" + // window cells area size 564"\x1b[48;24;80;312;560t" + // in-band resize response 565"\x1b[t" + // invalid 566"\x1b[999t" + // invalid 567"\x1b[999;1t" + // invalid 568"" ), 569 []Event { 570PixelSizeEvent {Width :80 ,Height :24 }, 571CellSizeEvent {Width :7 ,Height :13 }, 572WindowSizeEvent {Width :80 ,Height :24 }, 573WindowSizeEvent {Width :80 ,Height :24 }, 574PixelSizeEvent {Width :560 ,Height :312 }, 575UnknownCsiEvent ("\x1b[t" ), 576WindowOpEvent {Op :999 }, 577WindowOpEvent {Op :999 ,Args : []int {1 }}, 578 }, 579 }, 580 581// Kitty keyboard / CSI u (fixterms) 582seqTest { 583 []byte ("\x1b[1B" ), 584 []Event {KeyPressEvent {Code :KeyDown }}, 585 }, 586seqTest { 587 []byte ("\x1b[1;B" ), 588 []Event {KeyPressEvent {Code :KeyDown }}, 589 }, 590seqTest { 591 []byte ("\x1b[1;4B" ), 592 []Event {KeyPressEvent {Mod :ModShift | ModAlt ,Code :KeyDown }}, 593 }, 594seqTest { 595 []byte ("\x1b[1;4:1B" ), 596 []Event {KeyPressEvent {Mod :ModShift | ModAlt ,Code :KeyDown }}, 597 }, 598seqTest { 599 []byte ("\x1b[1;4:2B" ), 600 []Event {KeyPressEvent {Mod :ModShift | ModAlt ,Code :KeyDown ,IsRepeat :true }}, 601 }, 602seqTest { 603 []byte ("\x1b[1;4:3B" ), 604 []Event {KeyReleaseEvent {Mod :ModShift | ModAlt ,Code :KeyDown }}, 605 }, 606seqTest { 607 []byte ("\x1b[8~" ), 608 []Event {KeyPressEvent {Code :KeyEnd }}, 609 }, 610seqTest { 611 []byte ("\x1b[8;~" ), 612 []Event {KeyPressEvent {Code :KeyEnd }}, 613 }, 614seqTest { 615 []byte ("\x1b[8;10~" ), 616 []Event {KeyPressEvent {Mod :ModShift | ModMeta ,Code :KeyEnd }}, 617 }, 618seqTest { 619 []byte ("\x1b[6;6:2~" ), 620 []Event {KeyPressEvent {Mod :ModShift | ModCtrl ,Code :KeyPgDown ,IsRepeat :true }}, 621 }, 622seqTest { 623 []byte ("\x1b[6;6:3~" ), 624 []Event {KeyReleaseEvent {Mod :ModShift | ModCtrl ,Code :KeyPgDown }}, 625 }, 626seqTest { 627 []byte ("\x1b[27;4u" ), 628 []Event {KeyPressEvent {Mod :ModShift | ModAlt ,Code :KeyEscape }}, 629 }, 630seqTest { 631 []byte ("\x1b[127;4u" ), 632 []Event {KeyPressEvent {Mod :ModShift | ModAlt ,Code :KeyBackspace }}, 633 }, 634seqTest { 635 []byte ("\x1b[57358;4u" ), 636 []Event {KeyPressEvent {Mod :ModShift | ModAlt ,Code :KeyCapsLock }}, 637 }, 638seqTest { 639 []byte ("\x1b[9;2u" ), 640 []Event {KeyPressEvent {Mod :ModShift ,Code :KeyTab }}, 641 }, 642seqTest { 643 []byte ("\x1b[195;u" ), 644 []Event {KeyPressEvent {Text :"Ã" ,Code :'Ã' }}, 645 }, 646seqTest { 647 []byte ("\x1b[20320;2u" ), 648 []Event {KeyPressEvent {Text :"你" ,Mod :ModShift ,Code :'你' }}, 649 }, 650seqTest { 651 []byte ("\x1b[195;:1u" ), 652 []Event {KeyPressEvent {Text :"Ã" ,Code :'Ã' }}, 653 }, 654seqTest { 655 []byte ("\x1b[195;2:3u" ), 656 []Event {KeyReleaseEvent {Code :'Ã' ,Text :"Ã" ,Mod :ModShift }}, 657 }, 658seqTest { 659 []byte ("\x1b[195;2:2u" ), 660 []Event {KeyPressEvent {Code :'Ã' ,Text :"Ã" ,IsRepeat :true ,Mod :ModShift }}, 661 }, 662seqTest { 663 []byte ("\x1b[195;2:1u" ), 664 []Event {KeyPressEvent {Code :'Ã' ,Text :"Ã" ,Mod :ModShift }}, 665 }, 666seqTest { 667 []byte ("\x1b[195;2:3u" ), 668 []Event {KeyReleaseEvent {Code :'Ã' ,Text :"Ã" ,Mod :ModShift }}, 669 }, 670seqTest { 671 []byte ("\x1b[97;2;65u" ), 672 []Event {KeyPressEvent {Code :'a' ,Text :"A" ,Mod :ModShift }}, 673 }, 674seqTest { 675 []byte ("\x1b[97;;229u" ), 676 []Event {KeyPressEvent {Code :'a' ,Text :"å" }}, 677 }, 678 679// focus/blur 680seqTest { 681 []byte {'\x1b' ,'[' ,'I' }, 682 []Event { 683FocusEvent {}, 684 }, 685 }, 686seqTest { 687 []byte {'\x1b' ,'[' ,'O' }, 688 []Event { 689BlurEvent {}, 690 }, 691 }, 692// Mouse event. 693seqTest { 694 []byte {'\x1b' ,'[' ,'M' ,byte (32 )+ 0b0100_0000 ,byte (65 ),byte (49 )}, 695 []Event { 696MouseWheelEvent {X :32 ,Y :16 ,Button :MouseWheelUp }, 697 }, 698 }, 699// SGR Mouse event. 700seqTest { 701 []byte ("\x1b[<0;33;17M" ), 702 []Event { 703MouseClickEvent {X :32 ,Y :16 ,Button :MouseLeft }, 704 }, 705 }, 706// Runes. 707seqTest { 708 []byte {'a' }, 709 []Event { 710KeyPressEvent {Code :'a' ,Text :"a" }, 711 }, 712 }, 713seqTest { 714 []byte {'\x1b' ,'a' }, 715 []Event { 716KeyPressEvent {Code :'a' ,Mod :ModAlt }, 717 }, 718 }, 719seqTest { 720 []byte {'a' ,'a' ,'a' }, 721 []Event { 722KeyPressEvent {Code :'a' ,Text :"a" }, 723KeyPressEvent {Code :'a' ,Text :"a" }, 724KeyPressEvent {Code :'a' ,Text :"a" }, 725 }, 726 }, 727// Multi-byte rune. 728seqTest { 729 []byte ("☃" ), 730 []Event { 731KeyPressEvent {Code :'☃' ,Text :"☃" }, 732 }, 733 }, 734seqTest { 735 []byte ("\x1b☃" ), 736 []Event { 737KeyPressEvent {Code :'☃' ,Mod :ModAlt }, 738 }, 739 }, 740// Standalone control characters. 741seqTest { 742 []byte {'\x1b' }, 743 []Event { 744KeyPressEvent {Code :KeyEscape }, 745 }, 746 }, 747seqTest { 748 []byte {ansi .SOH }, 749 []Event { 750KeyPressEvent {Code :'a' ,Mod :ModCtrl }, 751 }, 752 }, 753seqTest { 754 []byte {'\x1b' ,ansi .SOH }, 755 []Event { 756KeyPressEvent {Code :'a' ,Mod :ModCtrl | ModAlt }, 757 }, 758 }, 759seqTest { 760 []byte {ansi .NUL }, 761 []Event { 762KeyPressEvent {Code :KeySpace ,Mod :ModCtrl }, 763 }, 764 }, 765seqTest { 766 []byte {'\x1b' ,ansi .NUL }, 767 []Event { 768KeyPressEvent {Code :KeySpace ,Mod :ModCtrl | ModAlt }, 769 }, 770 }, 771// C1 control characters. 772seqTest { 773 []byte {'\x80' }, 774 []Event { 775KeyPressEvent {Code :rune (0x80 - '@' ),Mod :ModCtrl | ModAlt }, 776 }, 777 }, 778 ) 779 780if ! isWindows { 781// Sadly, utf8.DecodeRune([]byte(0xfe)) returns a valid rune on windows. 782// This is incorrect, but it makes our test fail if we try it out. 783td = append (td ,seqTest { 784 []byte {'\xfe' }, 785 []Event { 786UnknownEvent (rune (0xfe )), 787 }, 788 }) 789 } 790 791var p EventDecoder 792for _ ,tc := range td { 793t .Run (fmt .Sprintf ("%q" ,string (tc .seq )),func (t * testing.T ) { 794var events []Event 795buf := tc .seq 796for len (buf )> 0 { 797width ,Event := p .Decode (buf ) 798switch Event := Event .(type ) { 799case MultiEvent : 800events = append (events ,Event ... ) 801default : 802events = append (events ,Event ) 803 } 804buf = buf [width :] 805 } 806if len (tc .Events )!= len (events ) { 807t .Fatalf ("\nexpected %d events for %q:\n %#v\ngot %d:\n %#v" ,len (tc .Events ),tc .seq ,tc .Events ,len (events ),events ) 808 } 809for i := range tc .Events { 810if ! reflect .DeepEqual (tc .Events [i ],events [i ]) { 811t .Errorf ("\nexpected event %d for %q:\n %#v\ngot:\n %#v" ,i ,tc .seq ,tc .Events [i ],events [i ]) 812 } 813 } 814 }) 815 } 816} 817 818func TestEmptyBufferDecode (t * testing.T ) { 819var p EventDecoder 820if n ,e := p .Decode ([]byte {});n != 0 || e != nil { 821t .Errorf ("expected (0, nil), got (%d, %v)" ,n ,e ) 822 } 823} 824 825func TestSplitReads (t * testing.T ) { 826expect := []Event { 827KeyPressEvent {Code :'a' ,Text :"a" }, 828KeyPressEvent {Code :'b' ,Text :"b" }, 829KeyPressEvent {Code :'c' ,Text :"c" }, 830KeyPressEvent {Code :KeyUp }, 831MouseClickEvent {X :32 ,Y :16 ,Button :MouseLeft }, 832FocusEvent {}, 833MouseClickEvent {X :32 ,Y :16 ,Button :MouseLeft }, 834BlurEvent {}, 835MouseClickEvent {X :32 ,Y :16 ,Button :MouseLeft }, 836KeyPressEvent {Code :KeyUp }, 837MouseClickEvent {X :32 ,Y :16 ,Button :MouseLeft }, 838MouseClickEvent {X :32 ,Y :16 ,Button :MouseLeft }, 839FocusEvent {}, 840UnknownEvent ("\x1b[12;34;9" ), 841 } 842inputs := []string { 843"abc" , 844"\x1b[A" , 845"\x1b[<0;33" , 846";17M" , 847"\x1b[I" , 848"\x1b" , 849"[" , 850"<" , 851"0" , 852";" , 853"3" , 854"3" , 855";" , 856"1" , 857"7" , 858"M" , 859"\x1b[O" , 860"\x1b" , 861"]" , 862"2" , 863";" , 864"a" , 865"b" , 866"c" , 867"\x1b" , 868"\x1b[" , 869"<0;3" , 870"3;17M" , 871"\x1b[A\x1b[" , 872"<0;33;17M\x1b[" , 873"<0;33;17M\x1b[I" , 874"\x1b[12;34;9" , 875 } 876 877r := LimitedReader (strings .NewReader (strings .Join (inputs ,"" )),8 ) 878drv := NewTerminalReader (r ,"dumb" ) 879drv .SetLogger (TLogger {t }) 880 881eventc := make (chan Event ) 882go func (t testing.TB ) { 883defer close (eventc ) 884if err := drv .StreamEvents (t .Context (),eventc );err != nil { 885t .Errorf ("error streaming events: %v" ,err ) 886 } 887 }(t ) 888 889var events []Event 890for ev := range eventc { 891events = append (events ,ev ) 892 } 893 894if ! reflect .DeepEqual (expect ,events ) { 895t .Errorf ("unexpected messages, expected:\n %+v\ngot:\n %+v" ,expect ,events ) 896 } 897} 898 899func TestReadLongInput (t * testing.T ) { 900expect := make ([]Event ,1000 ) 901for i := 0 ;i < 1000 ;i ++ { 902expect [i ]= KeyPressEvent {Code :'a' ,Text :"a" } 903 } 904input := strings .Repeat ("a" ,1000 ) 905rdr := strings .NewReader (input ) 906drv := NewTerminalReader (rdr ,"dumb" ) 907 908eventc := make (chan Event ) 909go func (t testing.TB ) { 910defer close (eventc ) 911if err := drv .StreamEvents (context .TODO (),eventc );err != nil { 912t .Errorf ("error streaming events: %v" ,err ) 913 } 914 }(t ) 915 916var events []Event 917for ev := range eventc { 918events = append (events ,ev ) 919 } 920 921if ! reflect .DeepEqual (expect ,events ) { 922t .Errorf ("unexpected messages, expected:\n %+v\ngot:\n %+v" ,expect ,events ) 923 } 924} 925 926func TestReadInput (t * testing.T ) { 927type test struct { 928keyname string 929in []byte 930out []Event 931 } 932testData := []test { 933 { 934"non-serialized single win32 esc" , 935 []byte ("\x1b" ), 936 []Event { 937KeyPressEvent {Code :KeyEscape }, 938 }, 939 }, 940 941 { 942"serialized win32 esc" , 943 []byte ("\x1b[27;0;27;1;0;1_abc\x1b[0;0;55357;1;0;1_\x1b[0;0;56835;1;0;1_ " ), 944 []Event { 945KeyPressEvent {Code :KeyEscape ,BaseCode :KeyEscape }, 946KeyPressEvent {Code :'a' ,Text :"a" }, 947KeyPressEvent {Code :'b' ,Text :"b" }, 948KeyPressEvent {Code :'c' ,Text :"c" }, 949KeyPressEvent {Code :128515 ,Text :"😃" }, 950KeyPressEvent {Code :KeySpace ,Text :" " }, 951 }, 952 }, 953 954 { 955"ignored osc" , 956 []byte ("\x1b]11;#123456\x18\x1b]11;#123456\x1a\x1b]11;#123456\x1b" ), 957 []Event (nil ), 958 }, 959 { 960"ignored apc" , 961 []byte ("\x9f\x9c\x1b_hello\x1b\x1b_hello\x18\x1b_abc\x1b\\\x1ba" ), 962 []Event { 963UnknownApcEvent ("\x9f\x9c" ), 964UnknownApcEvent ("\x1b_abc\x1b\\" ), 965KeyPressEvent {Code :'a' ,Mod :ModAlt }, 966 }, 967 }, 968 { 969"alt+] alt+'" , 970 []byte ("\x1b]\x1b'" ), 971 []Event { 972KeyPressEvent {Code :']' ,Mod :ModAlt }, 973KeyPressEvent {Code :'\'' ,Mod :ModAlt }, 974 }, 975 }, 976 { 977"alt+^ alt+&" , 978 []byte ("\x1b^\x1b&" ), 979 []Event { 980KeyPressEvent {Code :'^' ,Mod :ModAlt }, 981KeyPressEvent {Code :'&' ,Mod :ModAlt }, 982 }, 983 }, 984 { 985"a" , 986 []byte {'a' }, 987 []Event { 988KeyPressEvent {Code :'a' ,Text :"a" }, 989 }, 990 }, 991 { 992"space" , 993 []byte {' ' }, 994 []Event { 995KeyPressEvent {Code :KeySpace ,Text :" " }, 996 }, 997 }, 998 { 999"a alt+a" , 1000 []byte {'a' ,'\x1b' ,'a' }, 1001 []Event { 1002KeyPressEvent {Code :'a' ,Text :"a" }, 1003KeyPressEvent {Code :'a' ,Mod :ModAlt }, 1004 }, 1005 }, 1006 { 1007"a alt+a a" , 1008 []byte {'a' ,'\x1b' ,'a' ,'a' }, 1009 []Event { 1010KeyPressEvent {Code :'a' ,Text :"a" }, 1011KeyPressEvent {Code :'a' ,Mod :ModAlt }, 1012KeyPressEvent {Code :'a' ,Text :"a" }, 1013 }, 1014 }, 1015 { 1016"ctrl+a" , 1017 []byte {byte (ansi .SOH )}, 1018 []Event { 1019KeyPressEvent {Code :'a' ,Mod :ModCtrl }, 1020 }, 1021 }, 1022 { 1023"ctrl+a ctrl+b" , 1024 []byte {byte (ansi .SOH ),byte (ansi .STX )}, 1025 []Event { 1026KeyPressEvent {Code :'a' ,Mod :ModCtrl }, 1027KeyPressEvent {Code :'b' ,Mod :ModCtrl }, 1028 }, 1029 }, 1030 { 1031"alt+a" , 1032 []byte {byte (0x1b ),'a' }, 1033 []Event { 1034KeyPressEvent {Code :'a' ,Mod :ModAlt }, 1035 }, 1036 }, 1037 { 1038"a b c d" , 1039 []byte {'a' ,'b' ,'c' ,'d' }, 1040 []Event { 1041KeyPressEvent {Code :'a' ,Text :"a" }, 1042KeyPressEvent {Code :'b' ,Text :"b" }, 1043KeyPressEvent {Code :'c' ,Text :"c" }, 1044KeyPressEvent {Code :'d' ,Text :"d" }, 1045 }, 1046 }, 1047 { 1048"up" , 1049 []byte ("\x1b[A" ), 1050 []Event { 1051KeyPressEvent {Code :KeyUp }, 1052 }, 1053 }, 1054 { 1055"wheel up" , 1056 []byte {'\x1b' ,'[' ,'M' ,byte (32 )+ 0b0100_0000 ,byte (65 ),byte (49 )}, 1057 []Event { 1058MouseWheelEvent {X :32 ,Y :16 ,Button :MouseWheelUp }, 1059 }, 1060 }, 1061 { 1062"left motion release" , 1063 []byte { 1064'\x1b' ,'[' ,'M' ,byte (32 )+ 0b0010_0000 ,byte (32 + 33 ),byte (16 + 33 ), 1065'\x1b' ,'[' ,'M' ,byte (32 )+ 0b0000_0011 ,byte (64 + 33 ),byte (32 + 33 ), 1066 }, 1067 []Event { 1068MouseMotionEvent {X :32 ,Y :16 ,Button :MouseLeft }, 1069MouseReleaseEvent {X :64 ,Y :32 ,Button :MouseNone }, 1070 }, 1071 }, 1072 { 1073"shift+tab" , 1074 []byte {'\x1b' ,'[' ,'Z' }, 1075 []Event { 1076KeyPressEvent {Code :KeyTab ,Mod :ModShift }, 1077 }, 1078 }, 1079 { 1080"enter" , 1081 []byte {'\r' }, 1082 []Event {KeyPressEvent {Code :KeyEnter }}, 1083 }, 1084 { 1085"alt+enter" , 1086 []byte {'\x1b' ,'\r' }, 1087 []Event { 1088KeyPressEvent {Code :KeyEnter ,Mod :ModAlt }, 1089 }, 1090 }, 1091 { 1092"insert" , 1093 []byte {'\x1b' ,'[' ,'2' ,'~' }, 1094 []Event { 1095KeyPressEvent {Code :KeyInsert }, 1096 }, 1097 }, 1098 { 1099"ctrl+alt+a" , 1100 []byte {'\x1b' ,byte (ansi .SOH )}, 1101 []Event { 1102KeyPressEvent {Code :'a' ,Mod :ModCtrl | ModAlt }, 1103 }, 1104 }, 1105 { 1106"CSI?----X?" , 1107 []byte {'\x1b' ,'[' ,'-' ,'-' ,'-' ,'-' ,'X' }, 1108 []Event {UnknownCsiEvent ([]byte {'\x1b' ,'[' ,'-' ,'-' ,'-' ,'-' ,'X' })}, 1109 }, 1110// Powershell sequences. 1111 { 1112"up" , 1113 []byte {'\x1b' ,'O' ,'A' }, 1114 []Event {KeyPressEvent {Code :KeyUp }}, 1115 }, 1116 { 1117"down" , 1118 []byte {'\x1b' ,'O' ,'B' }, 1119 []Event {KeyPressEvent {Code :KeyDown }}, 1120 }, 1121 { 1122"right" , 1123 []byte {'\x1b' ,'O' ,'C' }, 1124 []Event {KeyPressEvent {Code :KeyRight }}, 1125 }, 1126 { 1127"left" , 1128 []byte {'\x1b' ,'O' ,'D' }, 1129 []Event {KeyPressEvent {Code :KeyLeft }}, 1130 }, 1131 { 1132"alt+enter" , 1133 []byte {'\x1b' ,'\x0d' }, 1134 []Event {KeyPressEvent {Code :KeyEnter ,Mod :ModAlt }}, 1135 }, 1136 { 1137"alt+backspace" , 1138 []byte {'\x1b' ,'\x7f' }, 1139 []Event {KeyPressEvent {Code :KeyBackspace ,Mod :ModAlt }}, 1140 }, 1141 { 1142"ctrl+space" , 1143 []byte {'\x00' }, 1144 []Event {KeyPressEvent {Code :KeySpace ,Mod :ModCtrl }}, 1145 }, 1146 { 1147"ctrl+alt+space" , 1148 []byte {'\x1b' ,'\x00' }, 1149 []Event {KeyPressEvent {Code :KeySpace ,Mod :ModCtrl | ModAlt }}, 1150 }, 1151 { 1152"esc" , 1153 []byte {'\x1b' }, 1154 []Event {KeyPressEvent {Code :KeyEscape }}, 1155 }, 1156 { 1157"alt+esc" , 1158 []byte {'\x1b' ,'\x1b' }, 1159 []Event {KeyPressEvent {Code :KeyEscape ,Mod :ModAlt }}, 1160 }, 1161 { 1162"a b o" , 1163 []byte { 1164'\x1b' ,'[' ,'2' ,'0' ,'0' ,'~' , 1165'a' ,' ' ,'b' , 1166'\x1b' ,'[' ,'2' ,'0' ,'1' ,'~' , 1167'o' , 1168 }, 1169 []Event { 1170PasteStartEvent {}, 1171PasteEvent {"a b" }, 1172PasteEndEvent {}, 1173KeyPressEvent {Code :'o' ,Text :"o" }, 1174 }, 1175 }, 1176 { 1177"a\x03\nb" , 1178 []byte { 1179'\x1b' ,'[' ,'2' ,'0' ,'0' ,'~' , 1180'a' ,'\x03' ,'\n' ,'b' , 1181'\x1b' ,'[' ,'2' ,'0' ,'1' ,'~' , 1182 }, 1183 []Event { 1184PasteStartEvent {}, 1185PasteEvent {"a\x03\nb" }, 1186PasteEndEvent {}, 1187 }, 1188 }, 1189 { 1190"?0xfe?" , 1191 []byte {'\xfe' }, 1192 []Event { 1193UnknownEvent (rune (0xfe )), 1194 }, 1195 }, 1196 { 1197"a ?0xfe? b" , 1198 []byte {'a' ,'\xfe' ,' ' ,'b' }, 1199 []Event { 1200KeyPressEvent {Code :'a' ,Text :"a" }, 1201UnknownEvent (rune (0xfe )), 1202KeyPressEvent {Code :KeySpace ,Text :" " }, 1203KeyPressEvent {Code :'b' ,Text :"b" }, 1204 }, 1205 }, 1206 } 1207 1208for i ,td := range testData { 1209t .Run (fmt .Sprintf ("%d: %s" ,i ,td .keyname ),func (t * testing.T ) { 1210events := testReadInputs (t ,bytes .NewReader (td .in )) 1211var buf strings.Builder 1212for i ,event := range events { 1213if i > 0 { 1214buf .WriteByte (' ' ) 1215 } 1216if s ,ok := event .(fmt.Stringer );ok { 1217buf .WriteString (s .String ()) 1218 }else { 1219fmt .Fprintf (& buf ,"%#v:%T" ,event ,event ) 1220 } 1221 } 1222 1223if len (events )!= len (td .out ) { 1224t .Fatalf ("unexpected message list length: got %d, expected %d\n got: %#v\n expected: %#v\n" ,len (events ),len (td .out ),events ,td .out ) 1225 } 1226 1227if len (td .out )!= len (events ) { 1228t .Fatalf ("expected %d events, got %d: %s" ,len (td .out ),len (events ),buf .String ()) 1229 } 1230for i ,e := range events { 1231if ! reflect .DeepEqual (td .out [i ],e ) { 1232t .Errorf ("expected event %d to be %T %v, got %T %v" ,i ,td .out [i ],td .out [i ],e ,e ) 1233 } 1234 } 1235if ! reflect .DeepEqual (td .out ,events ) { 1236t .Fatalf ("expected:\n%#v\ngot:\n%#v" ,td .out ,events ) 1237 } 1238 }) 1239 } 1240} 1241 1242func testReadInputs (t * testing.T ,input io.Reader ) []Event { 1243drv := NewTerminalReader (input ,"dumb" ) 1244drv .SetLogger (TLogger {t }) 1245 1246eventc := make (chan Event ) 1247go func (t testing.TB ) { 1248defer close (eventc ) 1249if err := drv .StreamEvents (context .TODO (),eventc );err != nil { 1250t .Errorf ("error streaming events: %v" ,err ) 1251 } 1252 }(t ) 1253 1254var events []Event 1255for ev := range eventc { 1256events = append (events ,ev ) 1257 } 1258 1259return events 1260} 1261 1262// randTest defines the test input and expected output for a sequence 1263// of interleaved control sequences and control characters. 1264type randTest struct { 1265data []byte 1266lengths []int 1267names []string 1268} 1269 1270// seed is the random seed to randomize the input. This helps check 1271// that all the sequences get ultimately exercised. 1272var seed = flag .Int64 ("seed" ,0 ,"random seed (0 to autoselect)" ) 1273 1274// genRandomData generates a randomized test, with a random seed unless 1275// the seed flag was set. 1276func genRandomData (logfn func (int64 ),length int )randTest { 1277// We'll use a random source. However, we give the user the option 1278// to override it to a specific value for reproduceability. 1279s := * seed 1280if s == 0 { 1281s = time .Now ().UnixNano () 1282 } 1283// Inform the user so they know what to reuse to get the same data. 1284logfn (s ) 1285return genRandomDataWithSeed (s ,length ) 1286} 1287 1288// genRandomDataWithSeed generates a randomized test with a fixed seed. 1289func genRandomDataWithSeed (s int64 ,length int )randTest { 1290src := rand .NewSource (s ) 1291r := rand .New (src ) 1292 1293// allseqs contains all the sequences, in sorted order. We sort 1294// to make the test deterministic (when the seed is also fixed). 1295type seqpair struct { 1296seq string 1297name string 1298 } 1299var allseqs []seqpair 1300for seq ,key := range sequences { 1301allseqs = append (allseqs ,seqpair {seq ,key .String ()}) 1302 } 1303sort .Slice (allseqs ,func (i ,j int )bool {return allseqs [i ].seq < allseqs [j ].seq }) 1304 1305// res contains the computed test. 1306var res randTest 1307 1308for len (res .data )< length { 1309alt := r .Intn (2 ) 1310prefix := "" 1311esclen := 0 1312if alt == 1 { 1313prefix = "alt+" 1314esclen = 1 1315 } 1316kind := r .Intn (3 ) 1317switch kind { 1318case 0 : 1319// A control character. 1320if alt == 1 { 1321res .data = append (res .data ,'\x1b' ) 1322 } 1323res .data = append (res .data ,1 ) 1324res .names = append (res .names ,"ctrl+" + prefix + "a" ) 1325res .lengths = append (res .lengths ,1 + esclen ) 1326 1327case 1 ,2 : 1328// A sequence. 1329seqi := r .Intn (len (allseqs )) 1330s := allseqs [seqi ] 1331if strings .Contains (s .name ,"alt+" )|| strings .Contains (s .name ,"meta+" ) { 1332esclen = 0 1333prefix = "" 1334alt = 0 1335 } 1336if alt == 1 { 1337res .data = append (res .data ,'\x1b' ) 1338 } 1339res .data = append (res .data ,s .seq ... ) 1340if strings .HasPrefix (s .name ,"ctrl+" ) { 1341prefix = "ctrl+" + prefix 1342 } 1343name := prefix + strings .TrimPrefix (s .name ,"ctrl+" ) 1344res .names = append (res .names ,name ) 1345res .lengths = append (res .lengths ,len (s .seq )+ esclen ) 1346 } 1347 } 1348return res 1349} 1350 1351func FuzzParseSequence (f * testing.F ) { 1352var p EventDecoder 1353for seq := range sequences { 1354f .Add (seq ) 1355 } 1356f .Add ("\x1b]52;?\x07" )// OSC 52 1357f .Add ("\x1b]11;rgb:0000/0000/0000\x1b\\" )// OSC 11 1358f .Add ("\x1bP>|charm terminal(0.1.2)\x1b\\" )// DCS (XTVERSION) 1359f .Add ("\x1b_Gi=123\x1b\\" )// APC 1360f .Fuzz (func (t * testing.T ,seq string ) { 1361n ,_ := p .Decode ([]byte (seq )) 1362if n == 0 && seq != "" { 1363t .Errorf ("expected a non-zero width for %q" ,seq ) 1364 } 1365 }) 1366} 1367 1368// BenchmarkDetectSequenceMap benchmarks the map-based sequence 1369// detector. 1370func BenchmarkDetectSequenceMap (b * testing.B ) { 1371var p EventDecoder 1372td := genRandomDataWithSeed (123 ,10000 ) 1373for i := 0 ;i < b .N ;i ++ { 1374for j ,w := 0 ,0 ;j < len (td .data );j += w { 1375w ,_ = p .Decode (td .data [j :]) 1376 } 1377 } 1378} 1379 1380func TestMouseEvent_String (t * testing.T ) { 1381tt := []struct { 1382name string 1383event Event 1384expected string 1385 }{ 1386 { 1387name :"unknown" , 1388event :MouseClickEvent {Button :MouseButton (0xff )}, 1389expected :"unknown" , 1390 }, 1391 { 1392name :"left" , 1393event :MouseClickEvent {Button :MouseLeft }, 1394expected :"left" , 1395 }, 1396 { 1397name :"right" , 1398event :MouseClickEvent {Button :MouseRight }, 1399expected :"right" , 1400 }, 1401 { 1402name :"middle" , 1403event :MouseClickEvent {Button :MouseMiddle }, 1404expected :"middle" , 1405 }, 1406 { 1407name :"release" , 1408event :MouseReleaseEvent {Button :MouseNone }, 1409expected :"" , 1410 }, 1411 { 1412name :"wheelup" , 1413event :MouseWheelEvent {Button :MouseWheelUp }, 1414expected :"wheelup" , 1415 }, 1416 { 1417name :"wheeldown" , 1418event :MouseWheelEvent {Button :MouseWheelDown }, 1419expected :"wheeldown" , 1420 }, 1421 { 1422name :"wheelleft" , 1423event :MouseWheelEvent {Button :MouseWheelLeft }, 1424expected :"wheelleft" , 1425 }, 1426 { 1427name :"wheelright" , 1428event :MouseWheelEvent {Button :MouseWheelRight }, 1429expected :"wheelright" , 1430 }, 1431 { 1432name :"motion" , 1433event :MouseMotionEvent {Button :MouseNone }, 1434expected :"motion" , 1435 }, 1436 { 1437name :"shift+left" , 1438event :MouseReleaseEvent {Button :MouseLeft ,Mod :ModShift }, 1439expected :"shift+left" , 1440 }, 1441 { 1442name :"shift+left" ,event :MouseClickEvent {Button :MouseLeft ,Mod :ModShift }, 1443expected :"shift+left" , 1444 }, 1445 { 1446name :"ctrl+shift+left" , 1447event :MouseClickEvent {Button :MouseLeft ,Mod :ModCtrl | ModShift }, 1448expected :"ctrl+shift+left" , 1449 }, 1450 { 1451name :"alt+left" , 1452event :MouseClickEvent {Button :MouseLeft ,Mod :ModAlt }, 1453expected :"alt+left" , 1454 }, 1455 { 1456name :"ctrl+left" , 1457event :MouseClickEvent {Button :MouseLeft ,Mod :ModCtrl }, 1458expected :"ctrl+left" , 1459 }, 1460 { 1461name :"ctrl+alt+left" , 1462event :MouseClickEvent {Button :MouseLeft ,Mod :ModAlt | ModCtrl }, 1463expected :"ctrl+alt+left" , 1464 }, 1465 { 1466name :"ctrl+alt+shift+left" , 1467event :MouseClickEvent {Button :MouseLeft ,Mod :ModAlt | ModCtrl | ModShift }, 1468expected :"ctrl+alt+shift+left" , 1469 }, 1470 { 1471name :"ignore coordinates" , 1472event :MouseClickEvent {X :100 ,Y :200 ,Button :MouseLeft }, 1473expected :"left" , 1474 }, 1475 { 1476name :"broken type" , 1477event :MouseClickEvent {Button :MouseButton (120 )}, 1478expected :"unknown" , 1479 }, 1480 } 1481 1482for i := range tt { 1483tc := tt [i ] 1484 1485t .Run (tc .name ,func (t * testing.T ) { 1486actual := fmt .Sprint (tc .event ) 1487 1488if tc .expected != actual { 1489t .Fatalf ("expected %q but got %q" , 1490tc .expected , 1491actual , 1492 ) 1493 } 1494 }) 1495 } 1496} 1497 1498func TestParseX10MouseDownEvent (t * testing.T ) { 1499encode := func (b byte ,x ,y int ) []byte { 1500return []byte { 1501'\x1b' , 1502'[' , 1503'M' , 1504byte (32 )+ b , 1505byte (x + 32 + 1 ), 1506byte (y + 32 + 1 ), 1507 } 1508 } 1509 1510tt := []struct { 1511name string 1512buf []byte 1513expected Event 1514 }{ 1515// Position. 1516 { 1517name :"zero position" , 1518buf :encode (0b0000_0000 ,0 ,0 ), 1519expected :MouseClickEvent {X :0 ,Y :0 ,Button :MouseLeft }, 1520 }, 1521 { 1522name :"max position" , 1523buf :encode (0b0000_0000 ,222 ,222 ),// Because 255 (max int8) - 32 - 1. 1524expected :MouseClickEvent {X :222 ,Y :222 ,Button :MouseLeft }, 1525 }, 1526// Simple. 1527 { 1528name :"left" , 1529buf :encode (0b0000_0000 ,32 ,16 ), 1530expected :MouseClickEvent {X :32 ,Y :16 ,Button :MouseLeft }, 1531 }, 1532 { 1533name :"left in motion" , 1534buf :encode (0b0010_0000 ,32 ,16 ), 1535expected :MouseMotionEvent {X :32 ,Y :16 ,Button :MouseLeft }, 1536 }, 1537 { 1538name :"middle" , 1539buf :encode (0b0000_0001 ,32 ,16 ), 1540expected :MouseClickEvent {X :32 ,Y :16 ,Button :MouseMiddle }, 1541 }, 1542 { 1543name :"middle in motion" , 1544buf :encode (0b0010_0001 ,32 ,16 ), 1545expected :MouseMotionEvent {X :32 ,Y :16 ,Button :MouseMiddle }, 1546 }, 1547 { 1548name :"right" , 1549buf :encode (0b0000_0010 ,32 ,16 ), 1550expected :MouseClickEvent {X :32 ,Y :16 ,Button :MouseRight }, 1551 }, 1552 { 1553name :"right in motion" , 1554buf :encode (0b0010_0010 ,32 ,16 ), 1555expected :MouseMotionEvent {X :32 ,Y :16 ,Button :MouseRight }, 1556 }, 1557 { 1558name :"motion" , 1559buf :encode (0b0010_0011 ,32 ,16 ), 1560expected :MouseMotionEvent {X :32 ,Y :16 ,Button :MouseNone }, 1561 }, 1562 { 1563name :"wheel up" , 1564buf :encode (0b0100_0000 ,32 ,16 ), 1565expected :MouseWheelEvent {X :32 ,Y :16 ,Button :MouseWheelUp }, 1566 }, 1567 { 1568name :"wheel down" , 1569buf :encode (0b0100_0001 ,32 ,16 ), 1570expected :MouseWheelEvent {X :32 ,Y :16 ,Button :MouseWheelDown }, 1571 }, 1572 { 1573name :"wheel left" , 1574buf :encode (0b0100_0010 ,32 ,16 ), 1575expected :MouseWheelEvent {X :32 ,Y :16 ,Button :MouseWheelLeft }, 1576 }, 1577 { 1578name :"wheel right" , 1579buf :encode (0b0100_0011 ,32 ,16 ), 1580expected :MouseWheelEvent {X :32 ,Y :16 ,Button :MouseWheelRight }, 1581 }, 1582 { 1583name :"release" , 1584buf :encode (0b0000_0011 ,32 ,16 ), 1585expected :MouseReleaseEvent {X :32 ,Y :16 ,Button :MouseNone }, 1586 }, 1587 { 1588name :"backward" , 1589buf :encode (0b1000_0000 ,32 ,16 ), 1590expected :MouseClickEvent {X :32 ,Y :16 ,Button :MouseBackward }, 1591 }, 1592 { 1593name :"forward" , 1594buf :encode (0b1000_0001 ,32 ,16 ), 1595expected :MouseClickEvent {X :32 ,Y :16 ,Button :MouseForward }, 1596 }, 1597 { 1598name :"button 10" , 1599buf :encode (0b1000_0010 ,32 ,16 ), 1600expected :MouseClickEvent {X :32 ,Y :16 ,Button :MouseButton10 }, 1601 }, 1602 { 1603name :"button 11" , 1604buf :encode (0b1000_0011 ,32 ,16 ), 1605expected :MouseClickEvent {X :32 ,Y :16 ,Button :MouseButton11 }, 1606 }, 1607// Combinations. 1608 { 1609name :"alt+right" , 1610buf :encode (0b0000_1010 ,32 ,16 ), 1611expected :MouseClickEvent {X :32 ,Y :16 ,Mod :ModAlt ,Button :MouseRight }, 1612 }, 1613 { 1614name :"ctrl+right" , 1615buf :encode (0b0001_0010 ,32 ,16 ), 1616expected :MouseClickEvent {X :32 ,Y :16 ,Mod :ModCtrl ,Button :MouseRight }, 1617 }, 1618 { 1619name :"left in motion" , 1620buf :encode (0b0010_0000 ,32 ,16 ), 1621expected :MouseMotionEvent {X :32 ,Y :16 ,Button :MouseLeft }, 1622 }, 1623 { 1624name :"alt+right in motion" , 1625buf :encode (0b0010_1010 ,32 ,16 ), 1626expected :MouseMotionEvent {X :32 ,Y :16 ,Mod :ModAlt ,Button :MouseRight }, 1627 }, 1628 { 1629name :"ctrl+right in motion" , 1630buf :encode (0b0011_0010 ,32 ,16 ), 1631expected :MouseMotionEvent {X :32 ,Y :16 ,Mod :ModCtrl ,Button :MouseRight }, 1632 }, 1633 { 1634name :"ctrl+alt+right" , 1635buf :encode (0b0001_1010 ,32 ,16 ), 1636expected :MouseClickEvent {X :32 ,Y :16 ,Mod :ModAlt | ModCtrl ,Button :MouseRight }, 1637 }, 1638 { 1639name :"ctrl+wheel up" , 1640buf :encode (0b0101_0000 ,32 ,16 ), 1641expected :MouseWheelEvent {X :32 ,Y :16 ,Mod :ModCtrl ,Button :MouseWheelUp }, 1642 }, 1643 { 1644name :"alt+wheel down" , 1645buf :encode (0b0100_1001 ,32 ,16 ), 1646expected :MouseWheelEvent {X :32 ,Y :16 ,Mod :ModAlt ,Button :MouseWheelDown }, 1647 }, 1648 { 1649name :"ctrl+alt+wheel down" , 1650buf :encode (0b0101_1001 ,32 ,16 ), 1651expected :MouseWheelEvent {X :32 ,Y :16 ,Mod :ModAlt | ModCtrl ,Button :MouseWheelDown }, 1652 }, 1653// Overflow position. 1654 { 1655name :"overflow position" , 1656buf :encode (0b0010_0000 ,250 ,223 ),// Because 255 (max int8) - 32 - 1. 1657expected :MouseMotionEvent {X :- 6 ,Y :- 33 ,Button :MouseLeft }, 1658 }, 1659 } 1660 1661for i := range tt { 1662tc := tt [i ] 1663 1664t .Run (tc .name ,func (t * testing.T ) { 1665actual := parseX10MouseEvent (tc .buf ) 1666 1667if tc .expected != actual { 1668t .Fatalf ("expected %#v but got %#v" , 1669tc .expected , 1670actual , 1671 ) 1672 } 1673 }) 1674 } 1675} 1676 1677func TestParseSGRMouseEvent (t * testing.T ) { 1678type csiSequence struct { 1679params []ansi.Param 1680cmd ansi.Cmd 1681 } 1682encode := func (b ,x ,y int ,r bool )* csiSequence { 1683re := 'M' 1684if r { 1685re = 'm' 1686 } 1687return & csiSequence { 1688params : []ansi.Param { 1689ansi .Param (b ), 1690ansi .Param (x + 1 ), 1691ansi .Param (y + 1 ), 1692 }, 1693cmd :ansi .Cmd (re )| ('<' << parser .PrefixShift ), 1694 } 1695 } 1696 1697tt := []struct { 1698name string 1699buf * csiSequence 1700expected Event 1701 }{ 1702// Position. 1703 { 1704name :"zero position" , 1705buf :encode (0 ,0 ,0 ,false ), 1706expected :MouseClickEvent {X :0 ,Y :0 ,Button :MouseLeft }, 1707 }, 1708 { 1709name :"225 position" , 1710buf :encode (0 ,225 ,225 ,false ), 1711expected :MouseClickEvent {X :225 ,Y :225 ,Button :MouseLeft }, 1712 }, 1713// Simple. 1714 { 1715name :"left" , 1716buf :encode (0 ,32 ,16 ,false ), 1717expected :MouseClickEvent {X :32 ,Y :16 ,Button :MouseLeft }, 1718 }, 1719 { 1720name :"left in motion" , 1721buf :encode (32 ,32 ,16 ,false ), 1722expected :MouseMotionEvent {X :32 ,Y :16 ,Button :MouseLeft }, 1723 }, 1724 { 1725name :"left" , 1726buf :encode (0 ,32 ,16 ,true ), 1727expected :MouseReleaseEvent {X :32 ,Y :16 ,Button :MouseLeft }, 1728 }, 1729 { 1730name :"middle" , 1731buf :encode (1 ,32 ,16 ,false ), 1732expected :MouseClickEvent {X :32 ,Y :16 ,Button :MouseMiddle }, 1733 }, 1734 { 1735name :"middle in motion" , 1736buf :encode (33 ,32 ,16 ,false ), 1737expected :MouseMotionEvent {X :32 ,Y :16 ,Button :MouseMiddle }, 1738 }, 1739 { 1740name :"middle" , 1741buf :encode (1 ,32 ,16 ,true ), 1742expected :MouseReleaseEvent {X :32 ,Y :16 ,Button :MouseMiddle }, 1743 }, 1744 { 1745name :"right" , 1746buf :encode (2 ,32 ,16 ,false ), 1747expected :MouseClickEvent {X :32 ,Y :16 ,Button :MouseRight }, 1748 }, 1749 { 1750name :"right" , 1751buf :encode (2 ,32 ,16 ,true ), 1752expected :MouseReleaseEvent {X :32 ,Y :16 ,Button :MouseRight }, 1753 }, 1754 { 1755name :"motion" , 1756buf :encode (35 ,32 ,16 ,false ), 1757expected :MouseMotionEvent {X :32 ,Y :16 ,Button :MouseNone }, 1758 }, 1759 { 1760name :"wheel up" , 1761buf :encode (64 ,32 ,16 ,false ), 1762expected :MouseWheelEvent {X :32 ,Y :16 ,Button :MouseWheelUp }, 1763 }, 1764 { 1765name :"wheel down" , 1766buf :encode (65 ,32 ,16 ,false ), 1767expected :MouseWheelEvent {X :32 ,Y :16 ,Button :MouseWheelDown }, 1768 }, 1769 { 1770name :"wheel left" , 1771buf :encode (66 ,32 ,16 ,false ), 1772expected :MouseWheelEvent {X :32 ,Y :16 ,Button :MouseWheelLeft }, 1773 }, 1774 { 1775name :"wheel right" , 1776buf :encode (67 ,32 ,16 ,false ), 1777expected :MouseWheelEvent {X :32 ,Y :16 ,Button :MouseWheelRight }, 1778 }, 1779 { 1780name :"backward" , 1781buf :encode (128 ,32 ,16 ,false ), 1782expected :MouseClickEvent {X :32 ,Y :16 ,Button :MouseBackward }, 1783 }, 1784 { 1785name :"backward in motion" , 1786buf :encode (160 ,32 ,16 ,false ), 1787expected :MouseMotionEvent {X :32 ,Y :16 ,Button :MouseBackward }, 1788 }, 1789 { 1790name :"forward" , 1791buf :encode (129 ,32 ,16 ,false ), 1792expected :MouseClickEvent {X :32 ,Y :16 ,Button :MouseForward }, 1793 }, 1794 { 1795name :"forward in motion" , 1796buf :encode (161 ,32 ,16 ,false ), 1797expected :MouseMotionEvent {X :32 ,Y :16 ,Button :MouseForward }, 1798 }, 1799// Combinations. 1800 { 1801name :"alt+right" , 1802buf :encode (10 ,32 ,16 ,false ), 1803expected :MouseClickEvent {X :32 ,Y :16 ,Mod :ModAlt ,Button :MouseRight }, 1804 }, 1805 { 1806name :"ctrl+right" , 1807buf :encode (18 ,32 ,16 ,false ), 1808expected :MouseClickEvent {X :32 ,Y :16 ,Mod :ModCtrl ,Button :MouseRight }, 1809 }, 1810 { 1811name :"ctrl+alt+right" , 1812buf :encode (26 ,32 ,16 ,false ), 1813expected :MouseClickEvent {X :32 ,Y :16 ,Mod :ModAlt | ModCtrl ,Button :MouseRight }, 1814 }, 1815 { 1816name :"alt+wheel" , 1817buf :encode (73 ,32 ,16 ,false ), 1818expected :MouseWheelEvent {X :32 ,Y :16 ,Mod :ModAlt ,Button :MouseWheelDown }, 1819 }, 1820 { 1821name :"ctrl+wheel" , 1822buf :encode (81 ,32 ,16 ,false ), 1823expected :MouseWheelEvent {X :32 ,Y :16 ,Mod :ModCtrl ,Button :MouseWheelDown }, 1824 }, 1825 { 1826name :"ctrl+alt+wheel" , 1827buf :encode (89 ,32 ,16 ,false ), 1828expected :MouseWheelEvent {X :32 ,Y :16 ,Mod :ModAlt | ModCtrl ,Button :MouseWheelDown }, 1829 }, 1830 { 1831name :"ctrl+alt+shift+wheel" , 1832buf :encode (93 ,32 ,16 ,false ), 1833expected :MouseWheelEvent {X :32 ,Y :16 ,Mod :ModAlt | ModShift | ModCtrl ,Button :MouseWheelDown }, 1834 }, 1835 } 1836 1837for i := range tt { 1838tc := tt [i ] 1839 1840t .Run (tc .name ,func (t * testing.T ) { 1841actual := parseSGRMouseEvent (tc .buf .cmd ,tc .buf .params ) 1842if tc .expected != actual { 1843t .Fatalf ("expected %#v but got %#v" , 1844tc .expected , 1845actual , 1846 ) 1847 } 1848 }) 1849 } 1850} 1851 1852func TestMousePixelToCell (t * testing.T ) { 1853tt := []struct { 1854name string 1855mouse Mouse 1856ws * Winsize 1857expected Mouse 1858 }{ 1859 { 1860name :"standard 80x24 terminal" , 1861mouse :Mouse {X :480 ,Y :312 ,Button :MouseLeft }, 1862ws :& Winsize {Row :24 ,Col :80 ,Xpixel :560 ,Ypixel :312 }, 1863expected :Mouse {X :68 ,Y :24 ,Button :MouseLeft }, 1864 }, 1865 { 1866name :"top-left pixel" , 1867mouse :Mouse {X :0 ,Y :0 ,Button :MouseLeft }, 1868ws :& Winsize {Row :24 ,Col :80 ,Xpixel :560 ,Ypixel :312 }, 1869expected :Mouse {X :0 ,Y :0 ,Button :MouseLeft }, 1870 }, 1871 { 1872name :"bottom-right pixel" , 1873mouse :Mouse {X :559 ,Y :311 ,Button :MouseRight }, 1874ws :& Winsize {Row :24 ,Col :80 ,Xpixel :560 ,Ypixel :312 }, 1875expected :Mouse {X :79 ,Y :23 ,Button :MouseRight }, 1876 }, 1877 { 1878name :"exact cell boundary pixel" , 1879mouse :Mouse {X :6 ,Y :12 ,Button :MouseLeft }, 1880ws :& Winsize {Row :24 ,Col :80 ,Xpixel :480 ,Ypixel :288 }, 1881expected :Mouse {X :1 ,Y :1 ,Button :MouseLeft }, 1882 }, 1883 { 1884name :"preserves modifiers" , 1885mouse :Mouse {X :240 ,Y :156 ,Button :MouseMiddle ,Mod :ModAlt }, 1886ws :& Winsize {Row :24 ,Col :80 ,Xpixel :560 ,Ypixel :312 }, 1887expected :Mouse {X :34 ,Y :12 ,Button :MouseMiddle ,Mod :ModAlt }, 1888 }, 1889 { 1890name :"zero pixel dimensions returns zero cell" , 1891mouse :Mouse {X :480 ,Y :312 ,Button :MouseLeft }, 1892ws :& Winsize {Row :24 ,Col :80 ,Xpixel :0 ,Ypixel :0 }, 1893expected :Mouse {X :0 ,Y :0 ,Button :MouseLeft }, 1894 }, 1895 { 1896name :"pixel position larger than window" , 1897mouse :Mouse {X :1200 ,Y :800 ,Button :MouseLeft }, 1898ws :& Winsize {Row :24 ,Col :80 ,Xpixel :560 ,Ypixel :312 }, 1899expected :Mouse {X :171 ,Y :61 ,Button :MouseLeft }, 1900 }, 1901 } 1902 1903for i := range tt { 1904tc := tt [i ] 1905 1906t .Run (tc .name ,func (t * testing.T ) { 1907actual := MousePixelToCell (tc .mouse ,tc .ws ) 1908if tc .expected != actual { 1909t .Fatalf ("expected %#v but got %#v" , 1910tc .expected , 1911actual , 1912 ) 1913 } 1914 }) 1915 } 1916} 1917 1918// TestMatchStrings tests the MatchStrings method 1919func TestMatchStrings (t * testing.T ) { 1920tests := []struct { 1921name string 1922key Key 1923inputs []string 1924want bool 1925 }{ 1926 { 1927name :"matches first string" , 1928key :Key {Code :'a' ,Mod :ModCtrl }, 1929inputs : []string {"ctrl+a" ,"ctrl+b" ,"ctrl+c" }, 1930want :true , 1931 }, 1932 { 1933name :"matches middle string" , 1934key :Key {Code :'b' ,Mod :ModCtrl }, 1935inputs : []string {"ctrl+a" ,"ctrl+b" ,"ctrl+c" }, 1936want :true , 1937 }, 1938 { 1939name :"matches last string" , 1940key :Key {Code :'c' ,Mod :ModCtrl }, 1941inputs : []string {"ctrl+a" ,"ctrl+b" ,"ctrl+c" }, 1942want :true , 1943 }, 1944 { 1945name :"no match" , 1946key :Key {Code :'d' ,Mod :ModCtrl }, 1947inputs : []string {"ctrl+a" ,"ctrl+b" ,"ctrl+c" }, 1948want :false , 1949 }, 1950 { 1951name :"empty inputs" , 1952key :Key {Code :'a' ,Mod :ModCtrl }, 1953inputs : []string {}, 1954want :false , 1955 }, 1956 } 1957 1958for _ ,tt := range tests { 1959t .Run (tt .name ,func (t * testing.T ) { 1960got := tt .key .MatchString (tt .inputs ... ) 1961if got != tt .want { 1962t .Errorf ("MatchStrings() = %v, want %v" ,got ,tt .want ) 1963 } 1964 }) 1965 } 1966} 1967 1968func TestKeyMatchString (t * testing.T ) { 1969cases := []struct { 1970name string 1971key Key 1972input string 1973want bool 1974 }{ 1975 { 1976name :"ctrl+a" , 1977key :Key {Code :'a' ,Mod :ModCtrl }, 1978input :"ctrl+a" , 1979want :true , 1980 }, 1981 { 1982name :"ctrl+alt+a" , 1983key :Key {Code :'a' ,Mod :ModCtrl | ModAlt }, 1984input :"ctrl+alt+a" , 1985want :true , 1986 }, 1987 { 1988name :"ctrl+alt+shift+a" , 1989key :Key {Code :'a' ,Mod :ModCtrl | ModAlt | ModShift }, 1990input :"ctrl+alt+shift+a" , 1991want :true , 1992 }, 1993 { 1994name :"H" , 1995key :Key {Code :'H' ,Text :"H" }, 1996input :"H" , 1997want :true , 1998 }, 1999 { 2000name :"shift+h" , 2001key :Key {Code :'h' ,Mod :ModShift ,Text :"H" }, 2002input :"H" , 2003want :true , 2004 }, 2005 { 2006name :"?" , 2007key :Key {Code :'/' ,Mod :ModShift ,Text :"?" }, 2008input :"?" , 2009want :true , 2010 }, 2011 { 2012name :"shift+/" , 2013key :Key {Code :'/' ,Mod :ModShift ,Text :"?" }, 2014input :"shift+/" , 2015want :true , 2016 }, 2017 { 2018name :"capslock+a" , 2019key :Key {Code :'a' ,Mod :ModCapsLock ,Text :"A" }, 2020input :"A" , 2021want :true , 2022 }, 2023 { 2024name :"ctrl+capslock+a" , 2025key :Key {Code :'a' ,Mod :ModCtrl | ModCapsLock }, 2026input :"ctrl+a" , 2027want :false , 2028 }, 2029 { 2030name :"space" , 2031key :Key {Code :KeySpace ,Text :" " }, 2032input :"space" , 2033want :true , 2034 }, 2035 { 2036name :"whitespace" , 2037key :Key {Code :KeySpace ,Text :" " }, 2038input :" " , 2039want :true , 2040 }, 2041 { 2042name :"ctrl+space" , 2043key :Key {Code :KeySpace ,Mod :ModCtrl }, 2044input :"ctrl+space" , 2045want :true , 2046 }, 2047 { 2048name :"shift+whitespace" , 2049key :Key {Code :KeySpace ,Mod :ModShift ,Text :" " }, 2050input :" " , 2051want :true , 2052 }, 2053 { 2054name :"shift+space" , 2055key :Key {Code :KeySpace ,Mod :ModShift ,Text :" " }, 2056input :"shift+space" , 2057want :true , 2058 }, 2059 { 2060name :"meta modifier" , 2061key :Key {Code :'a' ,Mod :ModMeta }, 2062input :"meta+a" , 2063want :true , 2064 }, 2065 { 2066name :"hyper modifier" , 2067key :Key {Code :'a' ,Mod :ModHyper }, 2068input :"hyper+a" , 2069want :true , 2070 }, 2071 { 2072name :"super modifier" , 2073key :Key {Code :'a' ,Mod :ModSuper }, 2074input :"super+a" , 2075want :true , 2076 }, 2077 { 2078name :"scrolllock modifier" , 2079key :Key {Code :'a' ,Mod :ModScrollLock }, 2080input :"scrolllock+a" , 2081want :true , 2082 }, 2083 { 2084name :"numlock modifier" , 2085key :Key {Code :'a' ,Mod :ModNumLock }, 2086input :"numlock+a" , 2087want :true , 2088 }, 2089 { 2090name :"multi-rune key" , 2091key :Key {Code :KeyExtended ,Text :"hello" }, 2092input :"hello" , 2093want :true , 2094 }, 2095 { 2096name :"enter key" , 2097key :Key {Code :KeyEnter }, 2098input :"enter" , 2099want :true , 2100 }, 2101 { 2102name :"tab key" , 2103key :Key {Code :KeyTab }, 2104input :"tab" , 2105want :true , 2106 }, 2107 { 2108name :"escape key" , 2109key :Key {Code :KeyEscape }, 2110input :"esc" , 2111want :true , 2112 }, 2113 { 2114name :"f1 key" , 2115key :Key {Code :KeyF1 }, 2116input :"f1" , 2117want :true , 2118 }, 2119 { 2120name :"backspace key" , 2121key :Key {Code :KeyBackspace }, 2122input :"backspace" , 2123want :true , 2124 }, 2125 { 2126name :"delete key" , 2127key :Key {Code :KeyDelete }, 2128input :"delete" , 2129want :true , 2130 }, 2131 { 2132name :"home key" , 2133key :Key {Code :KeyHome }, 2134input :"home" , 2135want :true , 2136 }, 2137 { 2138name :"end key" , 2139key :Key {Code :KeyEnd }, 2140input :"end" , 2141want :true , 2142 }, 2143 { 2144name :"pgup key" , 2145key :Key {Code :KeyPgUp }, 2146input :"pgup" , 2147want :true , 2148 }, 2149 { 2150name :"pgdown key" , 2151key :Key {Code :KeyPgDown }, 2152input :"pgdown" , 2153want :true , 2154 }, 2155 { 2156name :"up arrow" , 2157key :Key {Code :KeyUp }, 2158input :"up" , 2159want :true , 2160 }, 2161 { 2162name :"down arrow" , 2163key :Key {Code :KeyDown }, 2164input :"down" , 2165want :true , 2166 }, 2167 { 2168name :"left arrow" , 2169key :Key {Code :KeyLeft }, 2170input :"left" , 2171want :true , 2172 }, 2173 { 2174name :"right arrow" , 2175key :Key {Code :KeyRight }, 2176input :"right" , 2177want :true , 2178 }, 2179 { 2180name :"insert key" , 2181key :Key {Code :KeyInsert }, 2182input :"insert" , 2183want :true , 2184 }, 2185 { 2186name :"single printable character" , 2187key :Key {Code :'1' ,Text :"1" }, 2188input :"1" , 2189want :true , 2190 }, 2191 { 2192name :"uppercase letter without shift" , 2193key :Key {Code :'A' ,Text :"A" }, 2194input :"A" , 2195want :true , 2196 }, 2197 { 2198name :"no match different key" , 2199key :Key {Code :'a' ,Mod :ModCtrl }, 2200input :"ctrl+b" , 2201want :false , 2202 }, 2203 { 2204name :"no match different modifier" , 2205key :Key {Code :'a' ,Mod :ModCtrl }, 2206input :"alt+a" , 2207want :false , 2208 }, 2209 { 2210name :"unknown key name" , 2211key :Key {Code :'x' }, 2212input :"unknownkey" , 2213want :false , 2214 }, 2215 { 2216name :"multi-rune string that doesn't match" , 2217key :Key {Code :'a' }, 2218input :"hello" , 2219want :false , 2220 }, 2221 { 2222name :"printable character with ctrl modifier" , 2223key :Key {Code :'a' ,Mod :ModCtrl }, 2224input :"a" , 2225want :false , 2226 }, 2227 { 2228name :"lowercase letter with shift" , 2229key :Key {Code :'h' ,Mod :ModShift }, 2230input :"shift+h" , 2231want :true , 2232 }, 2233 { 2234name :"uppercase letter with capslock" , 2235key :Key {Code :'h' ,Mod :ModCapsLock }, 2236input :"capslock+h" , 2237want :true , 2238 }, 2239 } 2240 2241for i ,tc := range cases { 2242t .Run (fmt .Sprintf ("%d: %s" ,i ,tc .name ),func (t * testing.T ) { 2243got := tc .key .MatchString (tc .input ) 2244if got != tc .want { 2245t .Errorf ("expected %v but got %v" ,tc .want ,got ) 2246 } 2247 }) 2248 } 2249} 2250 2251// TestSplitSequences tests that string-terminated sequences work correctly 2252// when split across multiple read() calls. 2253func TestSplitSequences (t * testing.T ) { 2254tests := []struct { 2255name string 2256chunks [][]byte 2257want []Event 2258delay time.Duration 2259limit int // limit the number of bytes read at once 2260 }{ 2261 { 2262name :"OSC 11 background color with ST terminator" , 2263chunks : [][]byte { 2264 []byte ("\x1b]11;rgb:1a1a/1b1b/2c2c" ), 2265 []byte ("\x1b\\" ), 2266 }, 2267want : []Event { 2268BackgroundColorEvent {Color :ansi .XParseColor ("rgb:1a1a/1b1b/2c2c" )}, 2269 }, 2270 }, 2271 { 2272name :"OSC 11 background color with BEL terminator" , 2273chunks : [][]byte { 2274 []byte ("\x1b]11;rgb:1a1a/1b1b/2c2c" ), 2275 []byte ("\x07" ), 2276 }, 2277want : []Event { 2278BackgroundColorEvent {Color :ansi .XParseColor ("rgb:1a1a/1b1b/2c2c" )}, 2279 }, 2280 }, 2281 { 2282name :"OSC 10 foreground color split" , 2283chunks : [][]byte { 2284 []byte ("\x1b]10;rgb:ffff/0000/" ), 2285 []byte ("0000\x1b\\" ), 2286 }, 2287want : []Event { 2288ForegroundColorEvent {Color :ansi .XParseColor ("rgb:ffff/0000/0000" )}, 2289 }, 2290 }, 2291 { 2292name :"OSC 12 cursor color split" , 2293chunks : [][]byte { 2294 []byte ("\x1b]12;rgb:" ), 2295 []byte ("8080/8080/8080\x07" ), 2296 }, 2297want : []Event { 2298CursorColorEvent {Color :ansi .XParseColor ("rgb:8080/8080/8080" )}, 2299 }, 2300 }, 2301 { 2302name :"DCS sequence split" , 2303chunks : [][]byte { 2304 []byte ("\x1bP1$r" ), 2305 []byte ("test\x1b\\" ), 2306 }, 2307want : []Event { 2308UnknownDcsEvent ("\x1bP1$rtest\x1b\\" ), 2309 }, 2310 }, 2311 { 2312name :"long DCS sequence split" , 2313chunks : [][]byte { 2314 []byte ("\x1bP1$raaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabcdef" ), 2315 []byte ("test\x1b\\" ), 2316 }, 2317want : []Event { 2318UnknownDcsEvent ("\x1bP1$raaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabcdeftest\x1b\\" ), 2319 }, 2320limit :256 , 2321 }, 2322 { 2323name :"APC sequence split" , 2324chunks : [][]byte { 2325 []byte ("\x1b_T" ), 2326 []byte ("test\x1b\\" ), 2327 }, 2328want : []Event { 2329UnknownApcEvent ("\x1b_Ttest\x1b\\" ), 2330 }, 2331 }, 2332 { 2333name :"Multiple chunks OSC" , 2334chunks : [][]byte { 2335 []byte ("\x1b]11;" ), 2336 []byte ("rgb:1234/" ), 2337 []byte ("5678/9abc\x07" ), 2338 }, 2339want : []Event { 2340BackgroundColorEvent {Color :ansi .XParseColor ("rgb:1234/5678/9abc" )}, 2341 }, 2342 }, 2343 { 2344name :"OSC followed by regular key" , 2345chunks : [][]byte { 2346 []byte ("\x1b]11;rgb:1111/2222/3333" ), 2347 []byte ("\x07a" ), 2348 }, 2349want : []Event { 2350BackgroundColorEvent {Color :ansi .XParseColor ("rgb:1111/2222/3333" )}, 2351KeyPressEvent {Code :'a' ,Text :"a" }, 2352 }, 2353 }, 2354 { 2355name :"unknown sequence after timeout" , 2356chunks : [][]byte { 2357 []byte ("\x1b]11;rgb:1111/2222/3333" ), 2358 []byte ("abc" ), 2359 []byte ("x" ), 2360 []byte ("x" ), 2361 []byte ("x" ), 2362 []byte ("x" ), 2363 }, 2364want : []Event { 2365UnknownEvent ("\x1b]11;rgb:1111/2222/3333" ), 2366KeyPressEvent {Code :'a' ,Text :"a" }, 2367KeyPressEvent {Code :'b' ,Text :"b" }, 2368KeyPressEvent {Code :'c' ,Text :"c" }, 2369KeyPressEvent {Code :'x' ,Text :"x" }, 2370KeyPressEvent {Code :'x' ,Text :"x" }, 2371KeyPressEvent {Code :'x' ,Text :"x" }, 2372KeyPressEvent {Code :'x' ,Text :"x" }, 2373 }, 2374delay :60 * time .Millisecond ,// Ensure the timeout is triggered. 2375 }, 2376 { 2377name :"multiple broken down sequences" , 2378chunks : [][]byte { 2379 []byte ("\x1b[B" ), 2380 []byte ("\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B" ), 2381 []byte ("\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[" ), 2382 []byte ("B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b" ), 2383 []byte ("[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B" ), 2384 }, 2385want : []Event { 2386KeyPressEvent {Code :KeyDown }, 2387KeyPressEvent {Code :KeyDown }, 2388KeyPressEvent {Code :KeyDown }, 2389KeyPressEvent {Code :KeyDown }, 2390KeyPressEvent {Code :KeyDown }, 2391KeyPressEvent {Code :KeyDown }, 2392KeyPressEvent {Code :KeyDown }, 2393KeyPressEvent {Code :KeyDown }, 2394KeyPressEvent {Code :KeyDown }, 2395KeyPressEvent {Code :KeyDown }, 2396KeyPressEvent {Code :KeyDown }, 2397KeyPressEvent {Code :KeyDown }, 2398KeyPressEvent {Code :KeyDown }, 2399KeyPressEvent {Code :KeyDown }, 2400KeyPressEvent {Code :KeyDown }, 2401KeyPressEvent {Code :KeyDown }, 2402KeyPressEvent {Code :KeyDown }, 2403KeyPressEvent {Code :KeyDown }, 2404KeyPressEvent {Code :KeyDown }, 2405KeyPressEvent {Code :KeyDown }, 2406KeyPressEvent {Code :KeyDown }, 2407KeyPressEvent {Code :KeyDown }, 2408KeyPressEvent {Code :KeyDown }, 2409KeyPressEvent {Code :KeyDown }, 2410KeyPressEvent {Code :KeyDown }, 2411KeyPressEvent {Code :KeyDown }, 2412KeyPressEvent {Code :KeyDown }, 2413KeyPressEvent {Code :KeyDown }, 2414KeyPressEvent {Code :KeyDown }, 2415KeyPressEvent {Code :KeyDown }, 2416KeyPressEvent {Code :KeyDown }, 2417KeyPressEvent {Code :KeyDown }, 2418KeyPressEvent {Code :KeyDown }, 2419KeyPressEvent {Code :KeyDown }, 2420KeyPressEvent {Code :KeyDown }, 2421KeyPressEvent {Code :KeyDown }, 2422KeyPressEvent {Code :KeyDown }, 2423KeyPressEvent {Code :KeyDown }, 2424KeyPressEvent {Code :KeyDown }, 2425KeyPressEvent {Code :KeyDown }, 2426KeyPressEvent {Code :KeyDown }, 2427KeyPressEvent {Code :KeyDown }, 2428KeyPressEvent {Code :KeyDown }, 2429 }, 2430 }, 2431 } 2432 2433for _ ,tt := range tests { 2434t .Run (tt .name ,func (t * testing.T ) { 2435rds := make ([]io.Reader ,len (tt .chunks )) 2436for i ,chunk := range tt .chunks { 2437rds [i ]= bytes .NewReader (chunk ) 2438 } 2439var r io.Reader 2440limit := 32 2441if tt .limit > 0 { 2442limit = tt .limit 2443 } 2444if tt .delay > 0 { 2445r = DelayedLimitedReader (io .MultiReader (rds ... ),limit ,tt .delay ) 2446 }else { 2447r = LimitedReader (io .MultiReader (rds ... ),limit ) 2448 } 2449ir := NewTerminalReader (r ,"xterm-256color" ) 2450ir .SetLogger (TLogger {TB :t }) 2451 2452eventc := make (chan Event ) 2453go func (t testing.TB ) { 2454defer close (eventc ) 2455if err := ir .StreamEvents (t .Context (),eventc );err != nil { 2456t .Errorf ("error streaming events: %v" ,err ) 2457 } 2458 }(t ) 2459 2460var events []Event 2461for ev := range eventc { 2462events = append (events ,ev ) 2463 } 2464 2465if len (events )!= len (tt .want ) { 2466t .Fatalf ("got %d events, want %d: %#v" ,len (events ),len (tt .want ),events ) 2467 } 2468 2469for i ,want := range tt .want { 2470if ! reflect .DeepEqual (events [i ],want ) { 2471t .Errorf ("event %d: got %#v, want %#v" ,i ,events [i ],want ) 2472 } 2473 } 2474 }) 2475 } 2476} 2477 2478// limitedSleepReader is a simple io.Reader that limits the number of bytes 2479// read on each Read call and simulates a delay to mimic terminal input 2480// behavior. 2481type limitedSleepReader struct { 2482r io.Reader 2483n int 2484d time.Duration 2485i int 2486} 2487 2488func DelayedLimitedReader (r io.Reader ,n int ,d time.Duration ) io.Reader { 2489return & limitedSleepReader {r :r ,n :n ,d :d } 2490} 2491 2492func LimitedReader (r io.Reader ,n int ) io.Reader { 2493return & limitedSleepReader {r :r ,n :n } 2494} 2495 2496func (r * limitedSleepReader )Read (p []byte ) (n int ,err error ) { 2497if r .i > 0 && r .d > 0 { 2498time .Sleep (r .d ) 2499 } 2500if r .n <= 0 { 2501return 0 ,io .EOF 2502 } 2503if len (p )> r .n { 2504p = p [0 :r .n ] 2505 } 2506n ,err = r .r .Read (p ) 2507r .i ++ 2508return n ,err 2509} 2510 2511type TLogger struct { testing.TB } 2512 2513func (t TLogger )Printf (format string ,args ... interface {}) { 2514t .Helper () 2515if t .TB == nil { 2516return 2517 } 2518t .Logf (format ,args ... ) 2519} 2520 2521// TestKeystroke tests the Keystroke method 2522func TestKeystroke (t * testing.T ) { 2523tests := []struct { 2524name string 2525key Key 2526want string 2527 }{ 2528 { 2529name :"simple key" , 2530key :Key {Code :'a' }, 2531want :"a" , 2532 }, 2533 { 2534name :"ctrl+a" , 2535key :Key {Code :'a' ,Mod :ModCtrl }, 2536want :"ctrl+a" , 2537 }, 2538 { 2539name :"alt+a" , 2540key :Key {Code :'a' ,Mod :ModAlt }, 2541want :"alt+a" , 2542 }, 2543 { 2544name :"shift+a" , 2545key :Key {Code :'a' ,Mod :ModShift }, 2546want :"shift+a" , 2547 }, 2548 { 2549name :"meta+a" , 2550key :Key {Code :'a' ,Mod :ModMeta }, 2551want :"meta+a" , 2552 }, 2553 { 2554name :"hyper+a" , 2555key :Key {Code :'a' ,Mod :ModHyper }, 2556want :"hyper+a" , 2557 }, 2558 { 2559name :"super+a" , 2560key :Key {Code :'a' ,Mod :ModSuper }, 2561want :"super+a" , 2562 }, 2563 { 2564name :"ctrl+alt+shift+a" , 2565key :Key {Code :'a' ,Mod :ModCtrl | ModAlt | ModShift }, 2566want :"ctrl+alt+shift+a" , 2567 }, 2568 { 2569name :"all modifiers" , 2570key :Key {Code :'a' ,Mod :ModCtrl | ModAlt | ModShift | ModMeta | ModHyper | ModSuper }, 2571want :"ctrl+alt+shift+meta+hyper+super+a" , 2572 }, 2573 { 2574name :"space key" , 2575key :Key {Code :KeySpace }, 2576want :"space" , 2577 }, 2578 { 2579name :"extended key with text" , 2580key :Key {Code :KeyExtended ,Text :"hello" }, 2581want :"hello" , 2582 }, 2583 { 2584name :"enter key" , 2585key :Key {Code :KeyEnter }, 2586want :"enter" , 2587 }, 2588 { 2589name :"tab key" , 2590key :Key {Code :KeyTab }, 2591want :"tab" , 2592 }, 2593 { 2594name :"escape key" , 2595key :Key {Code :KeyEscape }, 2596want :"esc" , 2597 }, 2598 { 2599name :"f1 key" , 2600key :Key {Code :KeyF1 }, 2601want :"f1" , 2602 }, 2603 { 2604name :"backspace key" , 2605key :Key {Code :KeyBackspace }, 2606want :"backspace" , 2607 }, 2608 { 2609name :"left ctrl key alone" , 2610key :Key {Code :KeyLeftCtrl ,Mod :ModCtrl }, 2611want :"leftctrl" , 2612 }, 2613 { 2614name :"right ctrl key alone" , 2615key :Key {Code :KeyRightCtrl ,Mod :ModCtrl }, 2616want :"rightctrl" , 2617 }, 2618 { 2619name :"left alt key alone" , 2620key :Key {Code :KeyLeftAlt ,Mod :ModAlt }, 2621want :"leftalt" , 2622 }, 2623 { 2624name :"right alt key alone" , 2625key :Key {Code :KeyRightAlt ,Mod :ModAlt }, 2626want :"rightalt" , 2627 }, 2628 { 2629name :"left shift key alone" , 2630key :Key {Code :KeyLeftShift ,Mod :ModShift }, 2631want :"leftshift" , 2632 }, 2633 { 2634name :"right shift key alone" , 2635key :Key {Code :KeyRightShift ,Mod :ModShift }, 2636want :"rightshift" , 2637 }, 2638 { 2639name :"left meta key alone" , 2640key :Key {Code :KeyLeftMeta ,Mod :ModMeta }, 2641want :"leftmeta" , 2642 }, 2643 { 2644name :"right meta key alone" , 2645key :Key {Code :KeyRightMeta ,Mod :ModMeta }, 2646want :"rightmeta" , 2647 }, 2648 { 2649name :"left hyper key alone" , 2650key :Key {Code :KeyLeftHyper ,Mod :ModHyper }, 2651want :"lefthyper" , 2652 }, 2653 { 2654name :"right hyper key alone" , 2655key :Key {Code :KeyRightHyper ,Mod :ModHyper }, 2656want :"righthyper" , 2657 }, 2658 { 2659name :"left super key alone" , 2660key :Key {Code :KeyLeftSuper ,Mod :ModSuper }, 2661want :"leftsuper" , 2662 }, 2663 { 2664name :"right super key alone" , 2665key :Key {Code :KeyRightSuper ,Mod :ModSuper }, 2666want :"rightsuper" , 2667 }, 2668 { 2669name :"key with base code" , 2670key :Key {Code :'A' ,BaseCode :'a' }, 2671want :"a" , 2672 }, 2673 { 2674name :"unknown key with base code" , 2675key :Key {Code :99999 ,BaseCode :'x' }, 2676want :"x" , 2677 }, 2678 { 2679name :"printable rune" , 2680key :Key {Code :'€' }, 2681want :"€" , 2682 }, 2683 { 2684name :"unknown key without base code" , 2685key :Key {Code :99999 }, 2686want :"𘚟" , 2687 }, 2688 } 2689 2690for _ ,tt := range tests { 2691t .Run (tt .name ,func (t * testing.T ) { 2692got := tt .key .Keystroke () 2693if got != tt .want { 2694t .Errorf ("Keystroke() = %q, want %q" ,got ,tt .want ) 2695 } 2696 }) 2697 } 2698} 2699 2700// TestKeystrokeCoverage tests edge cases for Keystroke method 2701func TestKeystrokeCoverage (t * testing.T ) { 2702// Test a key that's not in keyTypeString and is KeySpace 2703// This is actually impossible since KeySpace is in keyTypeString 2704// But we can test a key that's not in keyTypeString and has BaseCode of KeySpace 2705k := Key {Code :999999 ,BaseCode :KeySpace } 2706if got := k .Keystroke ();got != "space" { 2707t .Errorf ("Keystroke() = %q, want %q" ,got ,"space" ) 2708 } 2709 2710// Test a key that's not in keyTypeString, has no BaseCode, and is KeySpace 2711// This would require KeySpace to not be in keyTypeString, which it is 2712// So this branch might be unreachable 2713} 2714 2715func TestKeyStringMore (t * testing.T ) { 2716tests := []struct { 2717name string 2718key Key 2719want string 2720 }{ 2721 { 2722name :"space character" , 2723key :Key {Code :KeySpace ,Text :" " }, 2724want :"space" , 2725 }, 2726 { 2727name :"empty text" , 2728key :Key {Code :'a' ,Text :"" }, 2729want :"a" , 2730 }, 2731 { 2732name :"text with multiple characters" , 2733key :Key {Code :KeyExtended ,Text :"hello" }, 2734want :"hello" , 2735 }, 2736 } 2737 2738for _ ,tt := range tests { 2739t .Run (tt .name ,func (t * testing.T ) { 2740got := tt .key .String () 2741if got != tt .want { 2742t .Errorf ("String() = %q, want %q" ,got ,tt .want ) 2743 } 2744 }) 2745 } 2746}