okami.mom/ultraviolet
git clone https://git.nkp.pet/okami.mom/ultraviolet
7bfbaab
main
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// An OSC cut off after the ESC of its ST may still be 958// arriving, so input that ends there flushes as unknown. 959 []Event {UnknownEvent ("\x1b]11;#123456\x1b" )}, 960 }, 961 { 962"ignored apc" , 963 []byte ("\x9f\x9c\x1b_hello\x1b\x1b_hello\x18\x1b_abc\x1b\\\x1ba" ), 964 []Event { 965UnknownApcEvent ("\x9f\x9c" ), 966UnknownApcEvent ("\x1b_abc\x1b\\" ), 967KeyPressEvent {Code :'a' ,Mod :ModAlt }, 968 }, 969 }, 970 { 971"alt+] alt+'" , 972 []byte ("\x1b]\x1b'" ), 973 []Event { 974KeyPressEvent {Code :']' ,Mod :ModAlt }, 975KeyPressEvent {Code :'\'' ,Mod :ModAlt }, 976 }, 977 }, 978 { 979"alt+^ alt+&" , 980 []byte ("\x1b^\x1b&" ), 981 []Event { 982KeyPressEvent {Code :'^' ,Mod :ModAlt }, 983KeyPressEvent {Code :'&' ,Mod :ModAlt }, 984 }, 985 }, 986 { 987"a" , 988 []byte {'a' }, 989 []Event { 990KeyPressEvent {Code :'a' ,Text :"a" }, 991 }, 992 }, 993 { 994"space" , 995 []byte {' ' }, 996 []Event { 997KeyPressEvent {Code :KeySpace ,Text :" " }, 998 }, 999 }, 1000 { 1001"a alt+a" , 1002 []byte {'a' ,'\x1b' ,'a' }, 1003 []Event { 1004KeyPressEvent {Code :'a' ,Text :"a" }, 1005KeyPressEvent {Code :'a' ,Mod :ModAlt }, 1006 }, 1007 }, 1008 { 1009"a alt+a a" , 1010 []byte {'a' ,'\x1b' ,'a' ,'a' }, 1011 []Event { 1012KeyPressEvent {Code :'a' ,Text :"a" }, 1013KeyPressEvent {Code :'a' ,Mod :ModAlt }, 1014KeyPressEvent {Code :'a' ,Text :"a" }, 1015 }, 1016 }, 1017 { 1018"ctrl+a" , 1019 []byte {byte (ansi .SOH )}, 1020 []Event { 1021KeyPressEvent {Code :'a' ,Mod :ModCtrl }, 1022 }, 1023 }, 1024 { 1025"ctrl+a ctrl+b" , 1026 []byte {byte (ansi .SOH ),byte (ansi .STX )}, 1027 []Event { 1028KeyPressEvent {Code :'a' ,Mod :ModCtrl }, 1029KeyPressEvent {Code :'b' ,Mod :ModCtrl }, 1030 }, 1031 }, 1032 { 1033"alt+a" , 1034 []byte {byte (0x1b ),'a' }, 1035 []Event { 1036KeyPressEvent {Code :'a' ,Mod :ModAlt }, 1037 }, 1038 }, 1039 { 1040"a b c d" , 1041 []byte {'a' ,'b' ,'c' ,'d' }, 1042 []Event { 1043KeyPressEvent {Code :'a' ,Text :"a" }, 1044KeyPressEvent {Code :'b' ,Text :"b" }, 1045KeyPressEvent {Code :'c' ,Text :"c" }, 1046KeyPressEvent {Code :'d' ,Text :"d" }, 1047 }, 1048 }, 1049 { 1050"up" , 1051 []byte ("\x1b[A" ), 1052 []Event { 1053KeyPressEvent {Code :KeyUp }, 1054 }, 1055 }, 1056 { 1057"wheel up" , 1058 []byte {'\x1b' ,'[' ,'M' ,byte (32 )+ 0b0100_0000 ,byte (65 ),byte (49 )}, 1059 []Event { 1060MouseWheelEvent {X :32 ,Y :16 ,Button :MouseWheelUp }, 1061 }, 1062 }, 1063 { 1064"left motion release" , 1065 []byte { 1066'\x1b' ,'[' ,'M' ,byte (32 )+ 0b0010_0000 ,byte (32 + 33 ),byte (16 + 33 ), 1067'\x1b' ,'[' ,'M' ,byte (32 )+ 0b0000_0011 ,byte (64 + 33 ),byte (32 + 33 ), 1068 }, 1069 []Event { 1070MouseMotionEvent {X :32 ,Y :16 ,Button :MouseLeft }, 1071MouseReleaseEvent {X :64 ,Y :32 ,Button :MouseNone }, 1072 }, 1073 }, 1074 { 1075"shift+tab" , 1076 []byte {'\x1b' ,'[' ,'Z' }, 1077 []Event { 1078KeyPressEvent {Code :KeyTab ,Mod :ModShift }, 1079 }, 1080 }, 1081 { 1082"enter" , 1083 []byte {'\r' }, 1084 []Event {KeyPressEvent {Code :KeyEnter }}, 1085 }, 1086 { 1087"alt+enter" , 1088 []byte {'\x1b' ,'\r' }, 1089 []Event { 1090KeyPressEvent {Code :KeyEnter ,Mod :ModAlt }, 1091 }, 1092 }, 1093 { 1094"insert" , 1095 []byte {'\x1b' ,'[' ,'2' ,'~' }, 1096 []Event { 1097KeyPressEvent {Code :KeyInsert }, 1098 }, 1099 }, 1100 { 1101"ctrl+alt+a" , 1102 []byte {'\x1b' ,byte (ansi .SOH )}, 1103 []Event { 1104KeyPressEvent {Code :'a' ,Mod :ModCtrl | ModAlt }, 1105 }, 1106 }, 1107 { 1108"CSI?----X?" , 1109 []byte {'\x1b' ,'[' ,'-' ,'-' ,'-' ,'-' ,'X' }, 1110 []Event {UnknownCsiEvent ([]byte {'\x1b' ,'[' ,'-' ,'-' ,'-' ,'-' ,'X' })}, 1111 }, 1112// Powershell sequences. 1113 { 1114"up" , 1115 []byte {'\x1b' ,'O' ,'A' }, 1116 []Event {KeyPressEvent {Code :KeyUp }}, 1117 }, 1118 { 1119"down" , 1120 []byte {'\x1b' ,'O' ,'B' }, 1121 []Event {KeyPressEvent {Code :KeyDown }}, 1122 }, 1123 { 1124"right" , 1125 []byte {'\x1b' ,'O' ,'C' }, 1126 []Event {KeyPressEvent {Code :KeyRight }}, 1127 }, 1128 { 1129"left" , 1130 []byte {'\x1b' ,'O' ,'D' }, 1131 []Event {KeyPressEvent {Code :KeyLeft }}, 1132 }, 1133 { 1134"alt+enter" , 1135 []byte {'\x1b' ,'\x0d' }, 1136 []Event {KeyPressEvent {Code :KeyEnter ,Mod :ModAlt }}, 1137 }, 1138 { 1139"alt+backspace" , 1140 []byte {'\x1b' ,'\x7f' }, 1141 []Event {KeyPressEvent {Code :KeyBackspace ,Mod :ModAlt }}, 1142 }, 1143 { 1144"ctrl+space" , 1145 []byte {'\x00' }, 1146 []Event {KeyPressEvent {Code :KeySpace ,Mod :ModCtrl }}, 1147 }, 1148 { 1149"ctrl+alt+space" , 1150 []byte {'\x1b' ,'\x00' }, 1151 []Event {KeyPressEvent {Code :KeySpace ,Mod :ModCtrl | ModAlt }}, 1152 }, 1153 { 1154"esc" , 1155 []byte {'\x1b' }, 1156 []Event {KeyPressEvent {Code :KeyEscape }}, 1157 }, 1158 { 1159"alt+esc" , 1160 []byte {'\x1b' ,'\x1b' }, 1161 []Event {KeyPressEvent {Code :KeyEscape ,Mod :ModAlt }}, 1162 }, 1163 { 1164"a b o" , 1165 []byte { 1166'\x1b' ,'[' ,'2' ,'0' ,'0' ,'~' , 1167'a' ,' ' ,'b' , 1168'\x1b' ,'[' ,'2' ,'0' ,'1' ,'~' , 1169'o' , 1170 }, 1171 []Event { 1172PasteStartEvent {}, 1173PasteEvent {"a b" }, 1174PasteEndEvent {}, 1175KeyPressEvent {Code :'o' ,Text :"o" }, 1176 }, 1177 }, 1178 { 1179"a\x03\nb" , 1180 []byte { 1181'\x1b' ,'[' ,'2' ,'0' ,'0' ,'~' , 1182'a' ,'\x03' ,'\n' ,'b' , 1183'\x1b' ,'[' ,'2' ,'0' ,'1' ,'~' , 1184 }, 1185 []Event { 1186PasteStartEvent {}, 1187PasteEvent {"a\x03\nb" }, 1188PasteEndEvent {}, 1189 }, 1190 }, 1191 { 1192"?0xfe?" , 1193 []byte {'\xfe' }, 1194 []Event { 1195UnknownEvent (rune (0xfe )), 1196 }, 1197 }, 1198 { 1199"a ?0xfe? b" , 1200 []byte {'a' ,'\xfe' ,' ' ,'b' }, 1201 []Event { 1202KeyPressEvent {Code :'a' ,Text :"a" }, 1203UnknownEvent (rune (0xfe )), 1204KeyPressEvent {Code :KeySpace ,Text :" " }, 1205KeyPressEvent {Code :'b' ,Text :"b" }, 1206 }, 1207 }, 1208 } 1209 1210for i ,td := range testData { 1211t .Run (fmt .Sprintf ("%d: %s" ,i ,td .keyname ),func (t * testing.T ) { 1212events := testReadInputs (t ,bytes .NewReader (td .in )) 1213var buf strings.Builder 1214for i ,event := range events { 1215if i > 0 { 1216buf .WriteByte (' ' ) 1217 } 1218if s ,ok := event .(fmt.Stringer );ok { 1219buf .WriteString (s .String ()) 1220 }else { 1221fmt .Fprintf (& buf ,"%#v:%T" ,event ,event ) 1222 } 1223 } 1224 1225if len (events )!= len (td .out ) { 1226t .Fatalf ("unexpected message list length: got %d, expected %d\n got: %#v\n expected: %#v\n" ,len (events ),len (td .out ),events ,td .out ) 1227 } 1228 1229if len (td .out )!= len (events ) { 1230t .Fatalf ("expected %d events, got %d: %s" ,len (td .out ),len (events ),buf .String ()) 1231 } 1232for i ,e := range events { 1233if ! reflect .DeepEqual (td .out [i ],e ) { 1234t .Errorf ("expected event %d to be %T %v, got %T %v" ,i ,td .out [i ],td .out [i ],e ,e ) 1235 } 1236 } 1237if ! reflect .DeepEqual (td .out ,events ) { 1238t .Fatalf ("expected:\n%#v\ngot:\n%#v" ,td .out ,events ) 1239 } 1240 }) 1241 } 1242} 1243 1244func testReadInputs (t * testing.T ,input io.Reader ) []Event { 1245drv := NewTerminalReader (input ,"dumb" ) 1246drv .SetLogger (TLogger {t }) 1247 1248eventc := make (chan Event ) 1249go func (t testing.TB ) { 1250defer close (eventc ) 1251if err := drv .StreamEvents (context .TODO (),eventc );err != nil { 1252t .Errorf ("error streaming events: %v" ,err ) 1253 } 1254 }(t ) 1255 1256var events []Event 1257for ev := range eventc { 1258events = append (events ,ev ) 1259 } 1260 1261return events 1262} 1263 1264// randTest defines the test input and expected output for a sequence 1265// of interleaved control sequences and control characters. 1266type randTest struct { 1267data []byte 1268lengths []int 1269names []string 1270} 1271 1272// seed is the random seed to randomize the input. This helps check 1273// that all the sequences get ultimately exercised. 1274var seed = flag .Int64 ("seed" ,0 ,"random seed (0 to autoselect)" ) 1275 1276// genRandomData generates a randomized test, with a random seed unless 1277// the seed flag was set. 1278func genRandomData (logfn func (int64 ),length int )randTest { 1279// We'll use a random source. However, we give the user the option 1280// to override it to a specific value for reproduceability. 1281s := * seed 1282if s == 0 { 1283s = time .Now ().UnixNano () 1284 } 1285// Inform the user so they know what to reuse to get the same data. 1286logfn (s ) 1287return genRandomDataWithSeed (s ,length ) 1288} 1289 1290// genRandomDataWithSeed generates a randomized test with a fixed seed. 1291func genRandomDataWithSeed (s int64 ,length int )randTest { 1292src := rand .NewSource (s ) 1293r := rand .New (src ) 1294 1295// allseqs contains all the sequences, in sorted order. We sort 1296// to make the test deterministic (when the seed is also fixed). 1297type seqpair struct { 1298seq string 1299name string 1300 } 1301var allseqs []seqpair 1302for seq ,key := range sequences { 1303allseqs = append (allseqs ,seqpair {seq ,key .String ()}) 1304 } 1305sort .Slice (allseqs ,func (i ,j int )bool {return allseqs [i ].seq < allseqs [j ].seq }) 1306 1307// res contains the computed test. 1308var res randTest 1309 1310for len (res .data )< length { 1311alt := r .Intn (2 ) 1312prefix := "" 1313esclen := 0 1314if alt == 1 { 1315prefix = "alt+" 1316esclen = 1 1317 } 1318kind := r .Intn (3 ) 1319switch kind { 1320case 0 : 1321// A control character. 1322if alt == 1 { 1323res .data = append (res .data ,'\x1b' ) 1324 } 1325res .data = append (res .data ,1 ) 1326res .names = append (res .names ,"ctrl+" + prefix + "a" ) 1327res .lengths = append (res .lengths ,1 + esclen ) 1328 1329case 1 ,2 : 1330// A sequence. 1331seqi := r .Intn (len (allseqs )) 1332s := allseqs [seqi ] 1333if strings .Contains (s .name ,"alt+" )|| strings .Contains (s .name ,"meta+" ) { 1334esclen = 0 1335prefix = "" 1336alt = 0 1337 } 1338if alt == 1 { 1339res .data = append (res .data ,'\x1b' ) 1340 } 1341res .data = append (res .data ,s .seq ... ) 1342if strings .HasPrefix (s .name ,"ctrl+" ) { 1343prefix = "ctrl+" + prefix 1344 } 1345name := prefix + strings .TrimPrefix (s .name ,"ctrl+" ) 1346res .names = append (res .names ,name ) 1347res .lengths = append (res .lengths ,len (s .seq )+ esclen ) 1348 } 1349 } 1350return res 1351} 1352 1353func FuzzParseSequence (f * testing.F ) { 1354var p EventDecoder 1355for seq := range sequences { 1356f .Add (seq ) 1357 } 1358f .Add ("\x1b]52;?\x07" )// OSC 52 1359f .Add ("\x1b]11;rgb:0000/0000/0000\x1b\\" )// OSC 11 1360f .Add ("\x1bP>|charm terminal(0.1.2)\x1b\\" )// DCS (XTVERSION) 1361f .Add ("\x1b_Gi=123\x1b\\" )// APC 1362f .Fuzz (func (t * testing.T ,seq string ) { 1363n ,_ := p .Decode ([]byte (seq )) 1364if n == 0 && seq != "" { 1365t .Errorf ("expected a non-zero width for %q" ,seq ) 1366 } 1367 }) 1368} 1369 1370// BenchmarkDetectSequenceMap benchmarks the map-based sequence 1371// detector. 1372func BenchmarkDetectSequenceMap (b * testing.B ) { 1373var p EventDecoder 1374td := genRandomDataWithSeed (123 ,10000 ) 1375for i := 0 ;i < b .N ;i ++ { 1376for j ,w := 0 ,0 ;j < len (td .data );j += w { 1377w ,_ = p .Decode (td .data [j :]) 1378 } 1379 } 1380} 1381 1382func TestMouseEvent_String (t * testing.T ) { 1383tt := []struct { 1384name string 1385event Event 1386expected string 1387 }{ 1388 { 1389name :"unknown" , 1390event :MouseClickEvent {Button :MouseButton (0xff )}, 1391expected :"unknown" , 1392 }, 1393 { 1394name :"left" , 1395event :MouseClickEvent {Button :MouseLeft }, 1396expected :"left" , 1397 }, 1398 { 1399name :"right" , 1400event :MouseClickEvent {Button :MouseRight }, 1401expected :"right" , 1402 }, 1403 { 1404name :"middle" , 1405event :MouseClickEvent {Button :MouseMiddle }, 1406expected :"middle" , 1407 }, 1408 { 1409name :"release" , 1410event :MouseReleaseEvent {Button :MouseNone }, 1411expected :"" , 1412 }, 1413 { 1414name :"wheelup" , 1415event :MouseWheelEvent {Button :MouseWheelUp }, 1416expected :"wheelup" , 1417 }, 1418 { 1419name :"wheeldown" , 1420event :MouseWheelEvent {Button :MouseWheelDown }, 1421expected :"wheeldown" , 1422 }, 1423 { 1424name :"wheelleft" , 1425event :MouseWheelEvent {Button :MouseWheelLeft }, 1426expected :"wheelleft" , 1427 }, 1428 { 1429name :"wheelright" , 1430event :MouseWheelEvent {Button :MouseWheelRight }, 1431expected :"wheelright" , 1432 }, 1433 { 1434name :"motion" , 1435event :MouseMotionEvent {Button :MouseNone }, 1436expected :"motion" , 1437 }, 1438 { 1439name :"shift+left" , 1440event :MouseReleaseEvent {Button :MouseLeft ,Mod :ModShift }, 1441expected :"shift+left" , 1442 }, 1443 { 1444name :"shift+left" ,event :MouseClickEvent {Button :MouseLeft ,Mod :ModShift }, 1445expected :"shift+left" , 1446 }, 1447 { 1448name :"ctrl+shift+left" , 1449event :MouseClickEvent {Button :MouseLeft ,Mod :ModCtrl | ModShift }, 1450expected :"ctrl+shift+left" , 1451 }, 1452 { 1453name :"alt+left" , 1454event :MouseClickEvent {Button :MouseLeft ,Mod :ModAlt }, 1455expected :"alt+left" , 1456 }, 1457 { 1458name :"ctrl+left" , 1459event :MouseClickEvent {Button :MouseLeft ,Mod :ModCtrl }, 1460expected :"ctrl+left" , 1461 }, 1462 { 1463name :"ctrl+alt+left" , 1464event :MouseClickEvent {Button :MouseLeft ,Mod :ModAlt | ModCtrl }, 1465expected :"ctrl+alt+left" , 1466 }, 1467 { 1468name :"ctrl+alt+shift+left" , 1469event :MouseClickEvent {Button :MouseLeft ,Mod :ModAlt | ModCtrl | ModShift }, 1470expected :"ctrl+alt+shift+left" , 1471 }, 1472 { 1473name :"ignore coordinates" , 1474event :MouseClickEvent {X :100 ,Y :200 ,Button :MouseLeft }, 1475expected :"left" , 1476 }, 1477 { 1478name :"broken type" , 1479event :MouseClickEvent {Button :MouseButton (120 )}, 1480expected :"unknown" , 1481 }, 1482 } 1483 1484for i := range tt { 1485tc := tt [i ] 1486 1487t .Run (tc .name ,func (t * testing.T ) { 1488actual := fmt .Sprint (tc .event ) 1489 1490if tc .expected != actual { 1491t .Fatalf ("expected %q but got %q" , 1492tc .expected , 1493actual , 1494 ) 1495 } 1496 }) 1497 } 1498} 1499 1500func TestParseX10MouseDownEvent (t * testing.T ) { 1501encode := func (b byte ,x ,y int ) []byte { 1502return []byte { 1503'\x1b' , 1504'[' , 1505'M' , 1506byte (32 )+ b , 1507byte (x + 32 + 1 ), 1508byte (y + 32 + 1 ), 1509 } 1510 } 1511 1512tt := []struct { 1513name string 1514buf []byte 1515expected Event 1516 }{ 1517// Position. 1518 { 1519name :"zero position" , 1520buf :encode (0b0000_0000 ,0 ,0 ), 1521expected :MouseClickEvent {X :0 ,Y :0 ,Button :MouseLeft }, 1522 }, 1523 { 1524name :"max position" , 1525buf :encode (0b0000_0000 ,222 ,222 ),// Because 255 (max int8) - 32 - 1. 1526expected :MouseClickEvent {X :222 ,Y :222 ,Button :MouseLeft }, 1527 }, 1528// Simple. 1529 { 1530name :"left" , 1531buf :encode (0b0000_0000 ,32 ,16 ), 1532expected :MouseClickEvent {X :32 ,Y :16 ,Button :MouseLeft }, 1533 }, 1534 { 1535name :"left in motion" , 1536buf :encode (0b0010_0000 ,32 ,16 ), 1537expected :MouseMotionEvent {X :32 ,Y :16 ,Button :MouseLeft }, 1538 }, 1539 { 1540name :"middle" , 1541buf :encode (0b0000_0001 ,32 ,16 ), 1542expected :MouseClickEvent {X :32 ,Y :16 ,Button :MouseMiddle }, 1543 }, 1544 { 1545name :"middle in motion" , 1546buf :encode (0b0010_0001 ,32 ,16 ), 1547expected :MouseMotionEvent {X :32 ,Y :16 ,Button :MouseMiddle }, 1548 }, 1549 { 1550name :"right" , 1551buf :encode (0b0000_0010 ,32 ,16 ), 1552expected :MouseClickEvent {X :32 ,Y :16 ,Button :MouseRight }, 1553 }, 1554 { 1555name :"right in motion" , 1556buf :encode (0b0010_0010 ,32 ,16 ), 1557expected :MouseMotionEvent {X :32 ,Y :16 ,Button :MouseRight }, 1558 }, 1559 { 1560name :"motion" , 1561buf :encode (0b0010_0011 ,32 ,16 ), 1562expected :MouseMotionEvent {X :32 ,Y :16 ,Button :MouseNone }, 1563 }, 1564 { 1565name :"wheel up" , 1566buf :encode (0b0100_0000 ,32 ,16 ), 1567expected :MouseWheelEvent {X :32 ,Y :16 ,Button :MouseWheelUp }, 1568 }, 1569 { 1570name :"wheel down" , 1571buf :encode (0b0100_0001 ,32 ,16 ), 1572expected :MouseWheelEvent {X :32 ,Y :16 ,Button :MouseWheelDown }, 1573 }, 1574 { 1575name :"wheel left" , 1576buf :encode (0b0100_0010 ,32 ,16 ), 1577expected :MouseWheelEvent {X :32 ,Y :16 ,Button :MouseWheelLeft }, 1578 }, 1579 { 1580name :"wheel right" , 1581buf :encode (0b0100_0011 ,32 ,16 ), 1582expected :MouseWheelEvent {X :32 ,Y :16 ,Button :MouseWheelRight }, 1583 }, 1584 { 1585name :"release" , 1586buf :encode (0b0000_0011 ,32 ,16 ), 1587expected :MouseReleaseEvent {X :32 ,Y :16 ,Button :MouseNone }, 1588 }, 1589 { 1590name :"backward" , 1591buf :encode (0b1000_0000 ,32 ,16 ), 1592expected :MouseClickEvent {X :32 ,Y :16 ,Button :MouseBackward }, 1593 }, 1594 { 1595name :"forward" , 1596buf :encode (0b1000_0001 ,32 ,16 ), 1597expected :MouseClickEvent {X :32 ,Y :16 ,Button :MouseForward }, 1598 }, 1599 { 1600name :"button 10" , 1601buf :encode (0b1000_0010 ,32 ,16 ), 1602expected :MouseClickEvent {X :32 ,Y :16 ,Button :MouseButton10 }, 1603 }, 1604 { 1605name :"button 11" , 1606buf :encode (0b1000_0011 ,32 ,16 ), 1607expected :MouseClickEvent {X :32 ,Y :16 ,Button :MouseButton11 }, 1608 }, 1609// Combinations. 1610 { 1611name :"alt+right" , 1612buf :encode (0b0000_1010 ,32 ,16 ), 1613expected :MouseClickEvent {X :32 ,Y :16 ,Mod :ModAlt ,Button :MouseRight }, 1614 }, 1615 { 1616name :"ctrl+right" , 1617buf :encode (0b0001_0010 ,32 ,16 ), 1618expected :MouseClickEvent {X :32 ,Y :16 ,Mod :ModCtrl ,Button :MouseRight }, 1619 }, 1620 { 1621name :"left in motion" , 1622buf :encode (0b0010_0000 ,32 ,16 ), 1623expected :MouseMotionEvent {X :32 ,Y :16 ,Button :MouseLeft }, 1624 }, 1625 { 1626name :"alt+right in motion" , 1627buf :encode (0b0010_1010 ,32 ,16 ), 1628expected :MouseMotionEvent {X :32 ,Y :16 ,Mod :ModAlt ,Button :MouseRight }, 1629 }, 1630 { 1631name :"ctrl+right in motion" , 1632buf :encode (0b0011_0010 ,32 ,16 ), 1633expected :MouseMotionEvent {X :32 ,Y :16 ,Mod :ModCtrl ,Button :MouseRight }, 1634 }, 1635 { 1636name :"ctrl+alt+right" , 1637buf :encode (0b0001_1010 ,32 ,16 ), 1638expected :MouseClickEvent {X :32 ,Y :16 ,Mod :ModAlt | ModCtrl ,Button :MouseRight }, 1639 }, 1640 { 1641name :"ctrl+wheel up" , 1642buf :encode (0b0101_0000 ,32 ,16 ), 1643expected :MouseWheelEvent {X :32 ,Y :16 ,Mod :ModCtrl ,Button :MouseWheelUp }, 1644 }, 1645 { 1646name :"alt+wheel down" , 1647buf :encode (0b0100_1001 ,32 ,16 ), 1648expected :MouseWheelEvent {X :32 ,Y :16 ,Mod :ModAlt ,Button :MouseWheelDown }, 1649 }, 1650 { 1651name :"ctrl+alt+wheel down" , 1652buf :encode (0b0101_1001 ,32 ,16 ), 1653expected :MouseWheelEvent {X :32 ,Y :16 ,Mod :ModAlt | ModCtrl ,Button :MouseWheelDown }, 1654 }, 1655// Overflow position. 1656 { 1657name :"overflow position" , 1658buf :encode (0b0010_0000 ,250 ,223 ),// Because 255 (max int8) - 32 - 1. 1659expected :MouseMotionEvent {X :- 6 ,Y :- 33 ,Button :MouseLeft }, 1660 }, 1661 } 1662 1663for i := range tt { 1664tc := tt [i ] 1665 1666t .Run (tc .name ,func (t * testing.T ) { 1667actual := parseX10MouseEvent (tc .buf ) 1668 1669if tc .expected != actual { 1670t .Fatalf ("expected %#v but got %#v" , 1671tc .expected , 1672actual , 1673 ) 1674 } 1675 }) 1676 } 1677} 1678 1679func TestParseSGRMouseEvent (t * testing.T ) { 1680type csiSequence struct { 1681params []ansi.Param 1682cmd ansi.Cmd 1683 } 1684encode := func (b ,x ,y int ,r bool )* csiSequence { 1685re := 'M' 1686if r { 1687re = 'm' 1688 } 1689return & csiSequence { 1690params : []ansi.Param { 1691ansi .Param (b ), 1692ansi .Param (x + 1 ), 1693ansi .Param (y + 1 ), 1694 }, 1695cmd :ansi .Cmd (re )| ('<' << parser .PrefixShift ), 1696 } 1697 } 1698 1699tt := []struct { 1700name string 1701buf * csiSequence 1702expected Event 1703 }{ 1704// Position. 1705 { 1706name :"zero position" , 1707buf :encode (0 ,0 ,0 ,false ), 1708expected :MouseClickEvent {X :0 ,Y :0 ,Button :MouseLeft }, 1709 }, 1710 { 1711name :"225 position" , 1712buf :encode (0 ,225 ,225 ,false ), 1713expected :MouseClickEvent {X :225 ,Y :225 ,Button :MouseLeft }, 1714 }, 1715// Simple. 1716 { 1717name :"left" , 1718buf :encode (0 ,32 ,16 ,false ), 1719expected :MouseClickEvent {X :32 ,Y :16 ,Button :MouseLeft }, 1720 }, 1721 { 1722name :"left in motion" , 1723buf :encode (32 ,32 ,16 ,false ), 1724expected :MouseMotionEvent {X :32 ,Y :16 ,Button :MouseLeft }, 1725 }, 1726 { 1727name :"left" , 1728buf :encode (0 ,32 ,16 ,true ), 1729expected :MouseReleaseEvent {X :32 ,Y :16 ,Button :MouseLeft }, 1730 }, 1731 { 1732name :"middle" , 1733buf :encode (1 ,32 ,16 ,false ), 1734expected :MouseClickEvent {X :32 ,Y :16 ,Button :MouseMiddle }, 1735 }, 1736 { 1737name :"middle in motion" , 1738buf :encode (33 ,32 ,16 ,false ), 1739expected :MouseMotionEvent {X :32 ,Y :16 ,Button :MouseMiddle }, 1740 }, 1741 { 1742name :"middle" , 1743buf :encode (1 ,32 ,16 ,true ), 1744expected :MouseReleaseEvent {X :32 ,Y :16 ,Button :MouseMiddle }, 1745 }, 1746 { 1747name :"right" , 1748buf :encode (2 ,32 ,16 ,false ), 1749expected :MouseClickEvent {X :32 ,Y :16 ,Button :MouseRight }, 1750 }, 1751 { 1752name :"right" , 1753buf :encode (2 ,32 ,16 ,true ), 1754expected :MouseReleaseEvent {X :32 ,Y :16 ,Button :MouseRight }, 1755 }, 1756 { 1757name :"motion" , 1758buf :encode (35 ,32 ,16 ,false ), 1759expected :MouseMotionEvent {X :32 ,Y :16 ,Button :MouseNone }, 1760 }, 1761 { 1762name :"wheel up" , 1763buf :encode (64 ,32 ,16 ,false ), 1764expected :MouseWheelEvent {X :32 ,Y :16 ,Button :MouseWheelUp }, 1765 }, 1766 { 1767name :"wheel down" , 1768buf :encode (65 ,32 ,16 ,false ), 1769expected :MouseWheelEvent {X :32 ,Y :16 ,Button :MouseWheelDown }, 1770 }, 1771 { 1772name :"wheel left" , 1773buf :encode (66 ,32 ,16 ,false ), 1774expected :MouseWheelEvent {X :32 ,Y :16 ,Button :MouseWheelLeft }, 1775 }, 1776 { 1777name :"wheel right" , 1778buf :encode (67 ,32 ,16 ,false ), 1779expected :MouseWheelEvent {X :32 ,Y :16 ,Button :MouseWheelRight }, 1780 }, 1781 { 1782name :"backward" , 1783buf :encode (128 ,32 ,16 ,false ), 1784expected :MouseClickEvent {X :32 ,Y :16 ,Button :MouseBackward }, 1785 }, 1786 { 1787name :"backward in motion" , 1788buf :encode (160 ,32 ,16 ,false ), 1789expected :MouseMotionEvent {X :32 ,Y :16 ,Button :MouseBackward }, 1790 }, 1791 { 1792name :"forward" , 1793buf :encode (129 ,32 ,16 ,false ), 1794expected :MouseClickEvent {X :32 ,Y :16 ,Button :MouseForward }, 1795 }, 1796 { 1797name :"forward in motion" , 1798buf :encode (161 ,32 ,16 ,false ), 1799expected :MouseMotionEvent {X :32 ,Y :16 ,Button :MouseForward }, 1800 }, 1801// Combinations. 1802 { 1803name :"alt+right" , 1804buf :encode (10 ,32 ,16 ,false ), 1805expected :MouseClickEvent {X :32 ,Y :16 ,Mod :ModAlt ,Button :MouseRight }, 1806 }, 1807 { 1808name :"ctrl+right" , 1809buf :encode (18 ,32 ,16 ,false ), 1810expected :MouseClickEvent {X :32 ,Y :16 ,Mod :ModCtrl ,Button :MouseRight }, 1811 }, 1812 { 1813name :"ctrl+alt+right" , 1814buf :encode (26 ,32 ,16 ,false ), 1815expected :MouseClickEvent {X :32 ,Y :16 ,Mod :ModAlt | ModCtrl ,Button :MouseRight }, 1816 }, 1817 { 1818name :"alt+wheel" , 1819buf :encode (73 ,32 ,16 ,false ), 1820expected :MouseWheelEvent {X :32 ,Y :16 ,Mod :ModAlt ,Button :MouseWheelDown }, 1821 }, 1822 { 1823name :"ctrl+wheel" , 1824buf :encode (81 ,32 ,16 ,false ), 1825expected :MouseWheelEvent {X :32 ,Y :16 ,Mod :ModCtrl ,Button :MouseWheelDown }, 1826 }, 1827 { 1828name :"ctrl+alt+wheel" , 1829buf :encode (89 ,32 ,16 ,false ), 1830expected :MouseWheelEvent {X :32 ,Y :16 ,Mod :ModAlt | ModCtrl ,Button :MouseWheelDown }, 1831 }, 1832 { 1833name :"ctrl+alt+shift+wheel" , 1834buf :encode (93 ,32 ,16 ,false ), 1835expected :MouseWheelEvent {X :32 ,Y :16 ,Mod :ModAlt | ModShift | ModCtrl ,Button :MouseWheelDown }, 1836 }, 1837 } 1838 1839for i := range tt { 1840tc := tt [i ] 1841 1842t .Run (tc .name ,func (t * testing.T ) { 1843actual := parseSGRMouseEvent (tc .buf .cmd ,tc .buf .params ) 1844if tc .expected != actual { 1845t .Fatalf ("expected %#v but got %#v" , 1846tc .expected , 1847actual , 1848 ) 1849 } 1850 }) 1851 } 1852} 1853 1854func TestMousePixelToCell (t * testing.T ) { 1855tt := []struct { 1856name string 1857mouse Mouse 1858ws * Winsize 1859expected Mouse 1860 }{ 1861 { 1862name :"standard 80x24 terminal" , 1863mouse :Mouse {X :480 ,Y :312 ,Button :MouseLeft }, 1864ws :& Winsize {Row :24 ,Col :80 ,Xpixel :560 ,Ypixel :312 }, 1865expected :Mouse {X :68 ,Y :24 ,Button :MouseLeft }, 1866 }, 1867 { 1868name :"top-left pixel" , 1869mouse :Mouse {X :0 ,Y :0 ,Button :MouseLeft }, 1870ws :& Winsize {Row :24 ,Col :80 ,Xpixel :560 ,Ypixel :312 }, 1871expected :Mouse {X :0 ,Y :0 ,Button :MouseLeft }, 1872 }, 1873 { 1874name :"bottom-right pixel" , 1875mouse :Mouse {X :559 ,Y :311 ,Button :MouseRight }, 1876ws :& Winsize {Row :24 ,Col :80 ,Xpixel :560 ,Ypixel :312 }, 1877expected :Mouse {X :79 ,Y :23 ,Button :MouseRight }, 1878 }, 1879 { 1880name :"exact cell boundary pixel" , 1881mouse :Mouse {X :6 ,Y :12 ,Button :MouseLeft }, 1882ws :& Winsize {Row :24 ,Col :80 ,Xpixel :480 ,Ypixel :288 }, 1883expected :Mouse {X :1 ,Y :1 ,Button :MouseLeft }, 1884 }, 1885 { 1886name :"preserves modifiers" , 1887mouse :Mouse {X :240 ,Y :156 ,Button :MouseMiddle ,Mod :ModAlt }, 1888ws :& Winsize {Row :24 ,Col :80 ,Xpixel :560 ,Ypixel :312 }, 1889expected :Mouse {X :34 ,Y :12 ,Button :MouseMiddle ,Mod :ModAlt }, 1890 }, 1891 { 1892name :"zero pixel dimensions returns zero cell" , 1893mouse :Mouse {X :480 ,Y :312 ,Button :MouseLeft }, 1894ws :& Winsize {Row :24 ,Col :80 ,Xpixel :0 ,Ypixel :0 }, 1895expected :Mouse {X :0 ,Y :0 ,Button :MouseLeft }, 1896 }, 1897 { 1898name :"pixel position larger than window" , 1899mouse :Mouse {X :1200 ,Y :800 ,Button :MouseLeft }, 1900ws :& Winsize {Row :24 ,Col :80 ,Xpixel :560 ,Ypixel :312 }, 1901expected :Mouse {X :171 ,Y :61 ,Button :MouseLeft }, 1902 }, 1903 } 1904 1905for i := range tt { 1906tc := tt [i ] 1907 1908t .Run (tc .name ,func (t * testing.T ) { 1909actual := MousePixelToCell (tc .mouse ,tc .ws ) 1910if tc .expected != actual { 1911t .Fatalf ("expected %#v but got %#v" , 1912tc .expected , 1913actual , 1914 ) 1915 } 1916 }) 1917 } 1918} 1919 1920// TestMatchStrings tests the MatchStrings method 1921func TestMatchStrings (t * testing.T ) { 1922tests := []struct { 1923name string 1924key Key 1925inputs []string 1926want bool 1927 }{ 1928 { 1929name :"matches first string" , 1930key :Key {Code :'a' ,Mod :ModCtrl }, 1931inputs : []string {"ctrl+a" ,"ctrl+b" ,"ctrl+c" }, 1932want :true , 1933 }, 1934 { 1935name :"matches middle string" , 1936key :Key {Code :'b' ,Mod :ModCtrl }, 1937inputs : []string {"ctrl+a" ,"ctrl+b" ,"ctrl+c" }, 1938want :true , 1939 }, 1940 { 1941name :"matches last string" , 1942key :Key {Code :'c' ,Mod :ModCtrl }, 1943inputs : []string {"ctrl+a" ,"ctrl+b" ,"ctrl+c" }, 1944want :true , 1945 }, 1946 { 1947name :"no match" , 1948key :Key {Code :'d' ,Mod :ModCtrl }, 1949inputs : []string {"ctrl+a" ,"ctrl+b" ,"ctrl+c" }, 1950want :false , 1951 }, 1952 { 1953name :"empty inputs" , 1954key :Key {Code :'a' ,Mod :ModCtrl }, 1955inputs : []string {}, 1956want :false , 1957 }, 1958 } 1959 1960for _ ,tt := range tests { 1961t .Run (tt .name ,func (t * testing.T ) { 1962got := tt .key .MatchString (tt .inputs ... ) 1963if got != tt .want { 1964t .Errorf ("MatchStrings() = %v, want %v" ,got ,tt .want ) 1965 } 1966 }) 1967 } 1968} 1969 1970func TestKeyMatchString (t * testing.T ) { 1971cases := []struct { 1972name string 1973key Key 1974input string 1975want bool 1976 }{ 1977 { 1978name :"ctrl+a" , 1979key :Key {Code :'a' ,Mod :ModCtrl }, 1980input :"ctrl+a" , 1981want :true , 1982 }, 1983 { 1984name :"ctrl+alt+a" , 1985key :Key {Code :'a' ,Mod :ModCtrl | ModAlt }, 1986input :"ctrl+alt+a" , 1987want :true , 1988 }, 1989 { 1990name :"ctrl+alt+shift+a" , 1991key :Key {Code :'a' ,Mod :ModCtrl | ModAlt | ModShift }, 1992input :"ctrl+alt+shift+a" , 1993want :true , 1994 }, 1995 { 1996name :"H" , 1997key :Key {Code :'H' ,Text :"H" }, 1998input :"H" , 1999want :true , 2000 }, 2001 { 2002name :"shift+h" , 2003key :Key {Code :'h' ,Mod :ModShift ,Text :"H" }, 2004input :"H" , 2005want :true , 2006 }, 2007 { 2008name :"?" , 2009key :Key {Code :'/' ,Mod :ModShift ,Text :"?" }, 2010input :"?" , 2011want :true , 2012 }, 2013 { 2014name :"shift+/" , 2015key :Key {Code :'/' ,Mod :ModShift ,Text :"?" }, 2016input :"shift+/" , 2017want :true , 2018 }, 2019 { 2020name :"capslock+a" , 2021key :Key {Code :'a' ,Mod :ModCapsLock ,Text :"A" }, 2022input :"A" , 2023want :true , 2024 }, 2025 { 2026name :"ctrl+capslock+a" , 2027key :Key {Code :'a' ,Mod :ModCtrl | ModCapsLock }, 2028input :"ctrl+a" , 2029want :false , 2030 }, 2031 { 2032name :"space" , 2033key :Key {Code :KeySpace ,Text :" " }, 2034input :"space" , 2035want :true , 2036 }, 2037 { 2038name :"whitespace" , 2039key :Key {Code :KeySpace ,Text :" " }, 2040input :" " , 2041want :true , 2042 }, 2043 { 2044name :"ctrl+space" , 2045key :Key {Code :KeySpace ,Mod :ModCtrl }, 2046input :"ctrl+space" , 2047want :true , 2048 }, 2049 { 2050name :"shift+whitespace" , 2051key :Key {Code :KeySpace ,Mod :ModShift ,Text :" " }, 2052input :" " , 2053want :true , 2054 }, 2055 { 2056name :"shift+space" , 2057key :Key {Code :KeySpace ,Mod :ModShift ,Text :" " }, 2058input :"shift+space" , 2059want :true , 2060 }, 2061 { 2062name :"meta modifier" , 2063key :Key {Code :'a' ,Mod :ModMeta }, 2064input :"meta+a" , 2065want :true , 2066 }, 2067 { 2068name :"hyper modifier" , 2069key :Key {Code :'a' ,Mod :ModHyper }, 2070input :"hyper+a" , 2071want :true , 2072 }, 2073 { 2074name :"super modifier" , 2075key :Key {Code :'a' ,Mod :ModSuper }, 2076input :"super+a" , 2077want :true , 2078 }, 2079 { 2080name :"scrolllock modifier" , 2081key :Key {Code :'a' ,Mod :ModScrollLock }, 2082input :"scrolllock+a" , 2083want :true , 2084 }, 2085 { 2086name :"numlock modifier" , 2087key :Key {Code :'a' ,Mod :ModNumLock }, 2088input :"numlock+a" , 2089want :true , 2090 }, 2091 { 2092name :"multi-rune key" , 2093key :Key {Code :KeyExtended ,Text :"hello" }, 2094input :"hello" , 2095want :true , 2096 }, 2097 { 2098name :"enter key" , 2099key :Key {Code :KeyEnter }, 2100input :"enter" , 2101want :true , 2102 }, 2103 { 2104name :"tab key" , 2105key :Key {Code :KeyTab }, 2106input :"tab" , 2107want :true , 2108 }, 2109 { 2110name :"escape key" , 2111key :Key {Code :KeyEscape }, 2112input :"esc" , 2113want :true , 2114 }, 2115 { 2116name :"f1 key" , 2117key :Key {Code :KeyF1 }, 2118input :"f1" , 2119want :true , 2120 }, 2121 { 2122name :"backspace key" , 2123key :Key {Code :KeyBackspace }, 2124input :"backspace" , 2125want :true , 2126 }, 2127 { 2128name :"delete key" , 2129key :Key {Code :KeyDelete }, 2130input :"delete" , 2131want :true , 2132 }, 2133 { 2134name :"home key" , 2135key :Key {Code :KeyHome }, 2136input :"home" , 2137want :true , 2138 }, 2139 { 2140name :"end key" , 2141key :Key {Code :KeyEnd }, 2142input :"end" , 2143want :true , 2144 }, 2145 { 2146name :"pgup key" , 2147key :Key {Code :KeyPgUp }, 2148input :"pgup" , 2149want :true , 2150 }, 2151 { 2152name :"pgdown key" , 2153key :Key {Code :KeyPgDown }, 2154input :"pgdown" , 2155want :true , 2156 }, 2157 { 2158name :"up arrow" , 2159key :Key {Code :KeyUp }, 2160input :"up" , 2161want :true , 2162 }, 2163 { 2164name :"down arrow" , 2165key :Key {Code :KeyDown }, 2166input :"down" , 2167want :true , 2168 }, 2169 { 2170name :"left arrow" , 2171key :Key {Code :KeyLeft }, 2172input :"left" , 2173want :true , 2174 }, 2175 { 2176name :"right arrow" , 2177key :Key {Code :KeyRight }, 2178input :"right" , 2179want :true , 2180 }, 2181 { 2182name :"insert key" , 2183key :Key {Code :KeyInsert }, 2184input :"insert" , 2185want :true , 2186 }, 2187 { 2188name :"single printable character" , 2189key :Key {Code :'1' ,Text :"1" }, 2190input :"1" , 2191want :true , 2192 }, 2193 { 2194name :"uppercase letter without shift" , 2195key :Key {Code :'A' ,Text :"A" }, 2196input :"A" , 2197want :true , 2198 }, 2199 { 2200name :"no match different key" , 2201key :Key {Code :'a' ,Mod :ModCtrl }, 2202input :"ctrl+b" , 2203want :false , 2204 }, 2205 { 2206name :"no match different modifier" , 2207key :Key {Code :'a' ,Mod :ModCtrl }, 2208input :"alt+a" , 2209want :false , 2210 }, 2211 { 2212name :"unknown key name" , 2213key :Key {Code :'x' }, 2214input :"unknownkey" , 2215want :false , 2216 }, 2217 { 2218name :"multi-rune string that doesn't match" , 2219key :Key {Code :'a' }, 2220input :"hello" , 2221want :false , 2222 }, 2223 { 2224name :"printable character with ctrl modifier" , 2225key :Key {Code :'a' ,Mod :ModCtrl }, 2226input :"a" , 2227want :false , 2228 }, 2229 { 2230name :"lowercase letter with shift" , 2231key :Key {Code :'h' ,Mod :ModShift }, 2232input :"shift+h" , 2233want :true , 2234 }, 2235 { 2236name :"uppercase letter with capslock" , 2237key :Key {Code :'h' ,Mod :ModCapsLock }, 2238input :"capslock+h" , 2239want :true , 2240 }, 2241 } 2242 2243for i ,tc := range cases { 2244t .Run (fmt .Sprintf ("%d: %s" ,i ,tc .name ),func (t * testing.T ) { 2245got := tc .key .MatchString (tc .input ) 2246if got != tc .want { 2247t .Errorf ("expected %v but got %v" ,tc .want ,got ) 2248 } 2249 }) 2250 } 2251} 2252 2253// TestSplitSequences tests that string-terminated sequences work correctly 2254// when split across multiple read() calls. 2255func TestSplitSequences (t * testing.T ) { 2256tests := []struct { 2257name string 2258chunks [][]byte 2259want []Event 2260delay time.Duration 2261limit int // limit the number of bytes read at once 2262 }{ 2263 { 2264name :"OSC 11 background color with ST terminator" , 2265chunks : [][]byte { 2266 []byte ("\x1b]11;rgb:1a1a/1b1b/2c2c" ), 2267 []byte ("\x1b\\" ), 2268 }, 2269want : []Event { 2270BackgroundColorEvent {Color :ansi .XParseColor ("rgb:1a1a/1b1b/2c2c" )}, 2271 }, 2272 }, 2273 { 2274name :"OSC 11 background color with BEL terminator" , 2275chunks : [][]byte { 2276 []byte ("\x1b]11;rgb:1a1a/1b1b/2c2c" ), 2277 []byte ("\x07" ), 2278 }, 2279want : []Event { 2280BackgroundColorEvent {Color :ansi .XParseColor ("rgb:1a1a/1b1b/2c2c" )}, 2281 }, 2282 }, 2283 { 2284name :"OSC 10 foreground color split" , 2285chunks : [][]byte { 2286 []byte ("\x1b]10;rgb:ffff/0000/" ), 2287 []byte ("0000\x1b\\" ), 2288 }, 2289want : []Event { 2290ForegroundColorEvent {Color :ansi .XParseColor ("rgb:ffff/0000/0000" )}, 2291 }, 2292 }, 2293 { 2294name :"OSC 12 cursor color split" , 2295chunks : [][]byte { 2296 []byte ("\x1b]12;rgb:" ), 2297 []byte ("8080/8080/8080\x07" ), 2298 }, 2299want : []Event { 2300CursorColorEvent {Color :ansi .XParseColor ("rgb:8080/8080/8080" )}, 2301 }, 2302 }, 2303 { 2304name :"DCS sequence split" , 2305chunks : [][]byte { 2306 []byte ("\x1bP1$r" ), 2307 []byte ("test\x1b\\" ), 2308 }, 2309want : []Event { 2310UnknownDcsEvent ("\x1bP1$rtest\x1b\\" ), 2311 }, 2312 }, 2313 { 2314name :"long DCS sequence split" , 2315chunks : [][]byte { 2316 []byte ("\x1bP1$raaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabcdef" ), 2317 []byte ("test\x1b\\" ), 2318 }, 2319want : []Event { 2320UnknownDcsEvent ("\x1bP1$raaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabcdeftest\x1b\\" ), 2321 }, 2322limit :256 , 2323 }, 2324 { 2325name :"APC sequence split" , 2326chunks : [][]byte { 2327 []byte ("\x1b_T" ), 2328 []byte ("test\x1b\\" ), 2329 }, 2330want : []Event { 2331UnknownApcEvent ("\x1b_Ttest\x1b\\" ), 2332 }, 2333 }, 2334 { 2335name :"Multiple chunks OSC" , 2336chunks : [][]byte { 2337 []byte ("\x1b]11;" ), 2338 []byte ("rgb:1234/" ), 2339 []byte ("5678/9abc\x07" ), 2340 }, 2341want : []Event { 2342BackgroundColorEvent {Color :ansi .XParseColor ("rgb:1234/5678/9abc" )}, 2343 }, 2344 }, 2345 { 2346name :"OSC followed by regular key" , 2347chunks : [][]byte { 2348 []byte ("\x1b]11;rgb:1111/2222/3333" ), 2349 []byte ("\x07a" ), 2350 }, 2351want : []Event { 2352BackgroundColorEvent {Color :ansi .XParseColor ("rgb:1111/2222/3333" )}, 2353KeyPressEvent {Code :'a' ,Text :"a" }, 2354 }, 2355 }, 2356 { 2357name :"unknown sequence after timeout" , 2358chunks : [][]byte { 2359 []byte ("\x1b]11;rgb:1111/2222/3333" ), 2360 []byte ("abc" ), 2361 []byte ("x" ), 2362 []byte ("x" ), 2363 []byte ("x" ), 2364 []byte ("x" ), 2365 }, 2366want : []Event { 2367UnknownEvent ("\x1b]11;rgb:1111/2222/3333" ), 2368KeyPressEvent {Code :'a' ,Text :"a" }, 2369KeyPressEvent {Code :'b' ,Text :"b" }, 2370KeyPressEvent {Code :'c' ,Text :"c" }, 2371KeyPressEvent {Code :'x' ,Text :"x" }, 2372KeyPressEvent {Code :'x' ,Text :"x" }, 2373KeyPressEvent {Code :'x' ,Text :"x" }, 2374KeyPressEvent {Code :'x' ,Text :"x" }, 2375 }, 2376delay :60 * time .Millisecond ,// Ensure the timeout is triggered. 2377 }, 2378 { 2379name :"multiple broken down sequences" , 2380chunks : [][]byte { 2381 []byte ("\x1b[B" ), 2382 []byte ("\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B" ), 2383 []byte ("\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[" ), 2384 []byte ("B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b" ), 2385 []byte ("[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B" ), 2386 }, 2387want : []Event { 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 }, 2429KeyPressEvent {Code :KeyDown }, 2430KeyPressEvent {Code :KeyDown }, 2431 }, 2432 }, 2433 } 2434 2435for _ ,tt := range tests { 2436t .Run (tt .name ,func (t * testing.T ) { 2437rds := make ([]io.Reader ,len (tt .chunks )) 2438for i ,chunk := range tt .chunks { 2439rds [i ]= bytes .NewReader (chunk ) 2440 } 2441var r io.Reader 2442limit := 32 2443if tt .limit > 0 { 2444limit = tt .limit 2445 } 2446if tt .delay > 0 { 2447r = DelayedLimitedReader (io .MultiReader (rds ... ),limit ,tt .delay ) 2448 }else { 2449r = LimitedReader (io .MultiReader (rds ... ),limit ) 2450 } 2451ir := NewTerminalReader (r ,"xterm-256color" ) 2452ir .SetLogger (TLogger {TB :t }) 2453 2454eventc := make (chan Event ) 2455go func (t testing.TB ) { 2456defer close (eventc ) 2457if err := ir .StreamEvents (t .Context (),eventc );err != nil { 2458t .Errorf ("error streaming events: %v" ,err ) 2459 } 2460 }(t ) 2461 2462var events []Event 2463for ev := range eventc { 2464events = append (events ,ev ) 2465 } 2466 2467if len (events )!= len (tt .want ) { 2468t .Fatalf ("got %d events, want %d: %#v" ,len (events ),len (tt .want ),events ) 2469 } 2470 2471for i ,want := range tt .want { 2472if ! reflect .DeepEqual (events [i ],want ) { 2473t .Errorf ("event %d: got %#v, want %#v" ,i ,events [i ],want ) 2474 } 2475 } 2476 }) 2477 } 2478} 2479 2480// limitedSleepReader is a simple io.Reader that limits the number of bytes 2481// read on each Read call and simulates a delay to mimic terminal input 2482// behavior. 2483type limitedSleepReader struct { 2484r io.Reader 2485n int 2486d time.Duration 2487i int 2488} 2489 2490func DelayedLimitedReader (r io.Reader ,n int ,d time.Duration ) io.Reader { 2491return & limitedSleepReader {r :r ,n :n ,d :d } 2492} 2493 2494func LimitedReader (r io.Reader ,n int ) io.Reader { 2495return & limitedSleepReader {r :r ,n :n } 2496} 2497 2498func (r * limitedSleepReader )Read (p []byte ) (n int ,err error ) { 2499if r .i > 0 && r .d > 0 { 2500time .Sleep (r .d ) 2501 } 2502if r .n <= 0 { 2503return 0 ,io .EOF 2504 } 2505if len (p )> r .n { 2506p = p [0 :r .n ] 2507 } 2508n ,err = r .r .Read (p ) 2509r .i ++ 2510return n ,err 2511} 2512 2513type TLogger struct { testing.TB } 2514 2515func (t TLogger )Printf (format string ,args ... interface {}) { 2516t .Helper () 2517if t .TB == nil { 2518return 2519 } 2520t .Logf (format ,args ... ) 2521} 2522 2523// TestKeystroke tests the Keystroke method 2524func TestKeystroke (t * testing.T ) { 2525tests := []struct { 2526name string 2527key Key 2528want string 2529 }{ 2530 { 2531name :"simple key" , 2532key :Key {Code :'a' }, 2533want :"a" , 2534 }, 2535 { 2536name :"ctrl+a" , 2537key :Key {Code :'a' ,Mod :ModCtrl }, 2538want :"ctrl+a" , 2539 }, 2540 { 2541name :"alt+a" , 2542key :Key {Code :'a' ,Mod :ModAlt }, 2543want :"alt+a" , 2544 }, 2545 { 2546name :"shift+a" , 2547key :Key {Code :'a' ,Mod :ModShift }, 2548want :"shift+a" , 2549 }, 2550 { 2551name :"meta+a" , 2552key :Key {Code :'a' ,Mod :ModMeta }, 2553want :"meta+a" , 2554 }, 2555 { 2556name :"hyper+a" , 2557key :Key {Code :'a' ,Mod :ModHyper }, 2558want :"hyper+a" , 2559 }, 2560 { 2561name :"super+a" , 2562key :Key {Code :'a' ,Mod :ModSuper }, 2563want :"super+a" , 2564 }, 2565 { 2566name :"ctrl+alt+shift+a" , 2567key :Key {Code :'a' ,Mod :ModCtrl | ModAlt | ModShift }, 2568want :"ctrl+alt+shift+a" , 2569 }, 2570 { 2571name :"all modifiers" , 2572key :Key {Code :'a' ,Mod :ModCtrl | ModAlt | ModShift | ModMeta | ModHyper | ModSuper }, 2573want :"ctrl+alt+shift+meta+hyper+super+a" , 2574 }, 2575 { 2576name :"space key" , 2577key :Key {Code :KeySpace }, 2578want :"space" , 2579 }, 2580 { 2581name :"extended key with text" , 2582key :Key {Code :KeyExtended ,Text :"hello" }, 2583want :"hello" , 2584 }, 2585 { 2586name :"enter key" , 2587key :Key {Code :KeyEnter }, 2588want :"enter" , 2589 }, 2590 { 2591name :"tab key" , 2592key :Key {Code :KeyTab }, 2593want :"tab" , 2594 }, 2595 { 2596name :"escape key" , 2597key :Key {Code :KeyEscape }, 2598want :"esc" , 2599 }, 2600 { 2601name :"f1 key" , 2602key :Key {Code :KeyF1 }, 2603want :"f1" , 2604 }, 2605 { 2606name :"backspace key" , 2607key :Key {Code :KeyBackspace }, 2608want :"backspace" , 2609 }, 2610 { 2611name :"left ctrl key alone" , 2612key :Key {Code :KeyLeftCtrl ,Mod :ModCtrl }, 2613want :"leftctrl" , 2614 }, 2615 { 2616name :"right ctrl key alone" , 2617key :Key {Code :KeyRightCtrl ,Mod :ModCtrl }, 2618want :"rightctrl" , 2619 }, 2620 { 2621name :"left alt key alone" , 2622key :Key {Code :KeyLeftAlt ,Mod :ModAlt }, 2623want :"leftalt" , 2624 }, 2625 { 2626name :"right alt key alone" , 2627key :Key {Code :KeyRightAlt ,Mod :ModAlt }, 2628want :"rightalt" , 2629 }, 2630 { 2631name :"left shift key alone" , 2632key :Key {Code :KeyLeftShift ,Mod :ModShift }, 2633want :"leftshift" , 2634 }, 2635 { 2636name :"right shift key alone" , 2637key :Key {Code :KeyRightShift ,Mod :ModShift }, 2638want :"rightshift" , 2639 }, 2640 { 2641name :"left meta key alone" , 2642key :Key {Code :KeyLeftMeta ,Mod :ModMeta }, 2643want :"leftmeta" , 2644 }, 2645 { 2646name :"right meta key alone" , 2647key :Key {Code :KeyRightMeta ,Mod :ModMeta }, 2648want :"rightmeta" , 2649 }, 2650 { 2651name :"left hyper key alone" , 2652key :Key {Code :KeyLeftHyper ,Mod :ModHyper }, 2653want :"lefthyper" , 2654 }, 2655 { 2656name :"right hyper key alone" , 2657key :Key {Code :KeyRightHyper ,Mod :ModHyper }, 2658want :"righthyper" , 2659 }, 2660 { 2661name :"left super key alone" , 2662key :Key {Code :KeyLeftSuper ,Mod :ModSuper }, 2663want :"leftsuper" , 2664 }, 2665 { 2666name :"right super key alone" , 2667key :Key {Code :KeyRightSuper ,Mod :ModSuper }, 2668want :"rightsuper" , 2669 }, 2670 { 2671name :"key with base code" , 2672key :Key {Code :'A' ,BaseCode :'a' }, 2673want :"a" , 2674 }, 2675 { 2676name :"unknown key with base code" , 2677key :Key {Code :99999 ,BaseCode :'x' }, 2678want :"x" , 2679 }, 2680 { 2681name :"printable rune" , 2682key :Key {Code :'€' }, 2683want :"€" , 2684 }, 2685 { 2686name :"unknown key without base code" , 2687key :Key {Code :99999 }, 2688want :"𘚟" , 2689 }, 2690 } 2691 2692for _ ,tt := range tests { 2693t .Run (tt .name ,func (t * testing.T ) { 2694got := tt .key .Keystroke () 2695if got != tt .want { 2696t .Errorf ("Keystroke() = %q, want %q" ,got ,tt .want ) 2697 } 2698 }) 2699 } 2700} 2701 2702// TestKeystrokeCoverage tests edge cases for Keystroke method 2703func TestKeystrokeCoverage (t * testing.T ) { 2704// Test a key that's not in keyTypeString and is KeySpace 2705// This is actually impossible since KeySpace is in keyTypeString 2706// But we can test a key that's not in keyTypeString and has BaseCode of KeySpace 2707k := Key {Code :999999 ,BaseCode :KeySpace } 2708if got := k .Keystroke ();got != "space" { 2709t .Errorf ("Keystroke() = %q, want %q" ,got ,"space" ) 2710 } 2711 2712// Test a key that's not in keyTypeString, has no BaseCode, and is KeySpace 2713// This would require KeySpace to not be in keyTypeString, which it is 2714// So this branch might be unreachable 2715} 2716 2717func TestKeyStringMore (t * testing.T ) { 2718tests := []struct { 2719name string 2720key Key 2721want string 2722 }{ 2723 { 2724name :"space character" , 2725key :Key {Code :KeySpace ,Text :" " }, 2726want :"space" , 2727 }, 2728 { 2729name :"empty text" , 2730key :Key {Code :'a' ,Text :"" }, 2731want :"a" , 2732 }, 2733 { 2734name :"text with multiple characters" , 2735key :Key {Code :KeyExtended ,Text :"hello" }, 2736want :"hello" , 2737 }, 2738 } 2739 2740for _ ,tt := range tests { 2741t .Run (tt .name ,func (t * testing.T ) { 2742got := tt .key .String () 2743if got != tt .want { 2744t .Errorf ("String() = %q, want %q" ,got ,tt .want ) 2745 } 2746 }) 2747 } 2748}