okami.mom/ultraviolet
git clone https://git.nkp.pet/okami.mom/ultraviolet
ae99b73
upstream
1package uv 2 3import ( 4"image/color" 5"strings" 6"testing" 7 8"github.com/charmbracelet/x/ansi" 9) 10 11func TestStyledString (t * testing.T ) { 12cases := []struct { 13name string 14input string 15expected * Buffer 16expectedWidth int 17expectedHeight int 18 }{ 19 { 20name :"single line" , 21input :"Hello, World!" , 22expectedWidth :13 , 23expectedHeight :1 , 24expected :& Buffer { 25Lines : []Line { 26 { 27newWcCell ("H" ,nil ,nil ), 28newWcCell ("e" ,nil ,nil ), 29newWcCell ("l" ,nil ,nil ), 30newWcCell ("l" ,nil ,nil ), 31newWcCell ("o" ,nil ,nil ), 32newWcCell ("," ,nil ,nil ), 33newWcCell (" " ,nil ,nil ), 34newWcCell ("W" ,nil ,nil ), 35newWcCell ("o" ,nil ,nil ), 36newWcCell ("r" ,nil ,nil ), 37newWcCell ("l" ,nil ,nil ), 38newWcCell ("d" ,nil ,nil ), 39newWcCell ("!" ,nil ,nil ), 40 }, 41 }, 42 }, 43 }, 44 { 45name :"multiple lines" , 46input :"Hello,\nWorld!" , 47expectedWidth :6 , 48expectedHeight :2 , 49expected :& Buffer { 50Lines : []Line { 51 { 52newWcCell ("H" ,nil ,nil ), 53newWcCell ("e" ,nil ,nil ), 54newWcCell ("l" ,nil ,nil ), 55newWcCell ("l" ,nil ,nil ), 56newWcCell ("o" ,nil ,nil ), 57newWcCell ("," ,nil ,nil ), 58 }, 59 { 60newWcCell ("W" ,nil ,nil ), 61newWcCell ("o" ,nil ,nil ), 62newWcCell ("r" ,nil ,nil ), 63newWcCell ("l" ,nil ,nil ), 64newWcCell ("d" ,nil ,nil ), 65newWcCell ("!" ,nil ,nil ), 66 }, 67 }, 68 }, 69 }, 70 { 71name :"empty string" , 72input :"" , 73expectedWidth :0 , 74expectedHeight :1 , 75expected :& Buffer { 76Lines : []Line {{}}, 77 }, 78 }, 79 { 80name :"multiple lines different width" , 81input :"Hello,\nWorld!\nThis is a test." , 82expectedWidth :15 , 83expectedHeight :3 , 84expected :& Buffer { 85Lines : []Line { 86 { 87newWcCell ("H" ,nil ,nil ), 88newWcCell ("e" ,nil ,nil ), 89newWcCell ("l" ,nil ,nil ), 90newWcCell ("l" ,nil ,nil ), 91newWcCell ("o" ,nil ,nil ), 92newWcCell ("," ,nil ,nil ), 93newWcCell (" " ,nil ,nil ), 94newWcCell (" " ,nil ,nil ), 95newWcCell (" " ,nil ,nil ), 96newWcCell (" " ,nil ,nil ), 97newWcCell (" " ,nil ,nil ), 98newWcCell (" " ,nil ,nil ), 99newWcCell (" " ,nil ,nil ), 100newWcCell (" " ,nil ,nil ), 101newWcCell (" " ,nil ,nil ), 102 }, 103 { 104newWcCell ("W" ,nil ,nil ), 105newWcCell ("o" ,nil ,nil ), 106newWcCell ("r" ,nil ,nil ), 107newWcCell ("l" ,nil ,nil ), 108newWcCell ("d" ,nil ,nil ), 109newWcCell ("!" ,nil ,nil ), 110newWcCell (" " ,nil ,nil ), 111newWcCell (" " ,nil ,nil ), 112newWcCell (" " ,nil ,nil ), 113newWcCell (" " ,nil ,nil ), 114newWcCell (" " ,nil ,nil ), 115newWcCell (" " ,nil ,nil ), 116newWcCell (" " ,nil ,nil ), 117newWcCell (" " ,nil ,nil ), 118newWcCell (" " ,nil ,nil ), 119 }, 120 { 121newWcCell ("T" ,nil ,nil ), 122newWcCell ("h" ,nil ,nil ), 123newWcCell ("i" ,nil ,nil ), 124newWcCell ("s" ,nil ,nil ), 125newWcCell (" " ,nil ,nil ), 126newWcCell ("i" ,nil ,nil ), 127newWcCell ("s" ,nil ,nil ), 128newWcCell (" " ,nil ,nil ), 129newWcCell ("a" ,nil ,nil ), 130newWcCell (" " ,nil ,nil ), 131newWcCell ("t" ,nil ,nil ), 132newWcCell ("e" ,nil ,nil ), 133newWcCell ("s" ,nil ,nil ), 134newWcCell ("t" ,nil ,nil ), 135newWcCell ("." ,nil ,nil ), 136 }, 137 }, 138 }, 139 }, 140 { 141name :"unicode characters" , 142input :"Hello, 世界!" , 143expectedWidth :12 , 144expectedHeight :1 , 145expected :& Buffer { 146Lines : []Line { 147 { 148newWcCell ("H" ,nil ,nil ), 149newWcCell ("e" ,nil ,nil ), 150newWcCell ("l" ,nil ,nil ), 151newWcCell ("l" ,nil ,nil ), 152newWcCell ("o" ,nil ,nil ), 153newWcCell ("," ,nil ,nil ), 154newWcCell (" " ,nil ,nil ), 155newWcCell ("世" ,nil ,nil ), 156Cell {}, 157newWcCell ("界" ,nil ,nil ), 158Cell {}, 159newWcCell ("!" ,nil ,nil ), 160 }, 161 }, 162 }, 163 }, 164 { 165name :"styled hello world string" , 166input :"\x1b[31;1;4mHello, \x1b[32;22;4mWorld!\x1b[0m" , 167expectedWidth :13 , 168expectedHeight :1 , 169expected :& Buffer { 170Lines : []Line { 171 { 172newWcCell ("H" ,& Style {Fg :ansi .Red ,Underline :UnderlineStyleSingle ,Attrs :AttrBold },nil ), 173newWcCell ("e" ,& Style {Fg :ansi .Red ,Underline :UnderlineStyleSingle ,Attrs :AttrBold },nil ), 174newWcCell ("l" ,& Style {Fg :ansi .Red ,Underline :UnderlineStyleSingle ,Attrs :AttrBold },nil ), 175newWcCell ("l" ,& Style {Fg :ansi .Red ,Underline :UnderlineStyleSingle ,Attrs :AttrBold },nil ), 176newWcCell ("o" ,& Style {Fg :ansi .Red ,Underline :UnderlineStyleSingle ,Attrs :AttrBold },nil ), 177newWcCell ("," ,& Style {Fg :ansi .Red ,Underline :UnderlineStyleSingle ,Attrs :AttrBold },nil ), 178newWcCell (" " ,& Style {Fg :ansi .Red ,Underline :UnderlineStyleSingle ,Attrs :AttrBold },nil ), 179newWcCell ("W" ,& Style {Fg :ansi .Green ,Underline :UnderlineStyleSingle },nil ), 180newWcCell ("o" ,& Style {Fg :ansi .Green ,Underline :UnderlineStyleSingle },nil ), 181newWcCell ("r" ,& Style {Fg :ansi .Green ,Underline :UnderlineStyleSingle },nil ), 182newWcCell ("l" ,& Style {Fg :ansi .Green ,Underline :UnderlineStyleSingle },nil ), 183newWcCell ("d" ,& Style {Fg :ansi .Green ,Underline :UnderlineStyleSingle },nil ), 184newWcCell ("!" ,& Style {Fg :ansi .Green ,Underline :UnderlineStyleSingle },nil ), 185 }, 186 }, 187 }, 188 }, 189 { 190name :"complex styling with multiple SGR sequences" , 191input :"\x1b[31;1;2;4mR\x1b[22;1med\x1b[0m \x1b[32;3mGreen\x1b[0m \x1b[34;9mBlue\x1b[0m \x1b[33;7mYellow\x1b[0m \x1b[35;5mPurple\x1b[0m" , 192expectedWidth :28 , 193expectedHeight :1 , 194expected :& Buffer { 195Lines : []Line { 196 { 197newWcCell ("R" ,& Style {Fg :ansi .Red ,Underline :UnderlineStyleSingle ,Attrs :AttrBold | AttrFaint },nil ), 198newWcCell ("e" ,& Style {Fg :ansi .Red ,Underline :UnderlineStyleSingle ,Attrs :AttrBold },nil ), 199newWcCell ("d" ,& Style {Fg :ansi .Red ,Underline :UnderlineStyleSingle ,Attrs :AttrBold },nil ), 200newWcCell (" " ,nil ,nil ), 201newWcCell ("G" ,& Style {Fg :ansi .Green ,Attrs :AttrItalic },nil ), 202newWcCell ("r" ,& Style {Fg :ansi .Green ,Attrs :AttrItalic },nil ), 203newWcCell ("e" ,& Style {Fg :ansi .Green ,Attrs :AttrItalic },nil ), 204newWcCell ("e" ,& Style {Fg :ansi .Green ,Attrs :AttrItalic },nil ), 205newWcCell ("n" ,& Style {Fg :ansi .Green ,Attrs :AttrItalic },nil ), 206newWcCell (" " ,nil ,nil ), 207newWcCell ("B" ,& Style {Fg :ansi .Blue ,Attrs :AttrStrikethrough },nil ), 208newWcCell ("l" ,& Style {Fg :ansi .Blue ,Attrs :AttrStrikethrough },nil ), 209newWcCell ("u" ,& Style {Fg :ansi .Blue ,Attrs :AttrStrikethrough },nil ), 210newWcCell ("e" ,& Style {Fg :ansi .Blue ,Attrs :AttrStrikethrough },nil ), 211newWcCell (" " ,nil ,nil ), 212newWcCell ("Y" ,& Style {Fg :ansi .Yellow ,Attrs :AttrReverse },nil ), 213newWcCell ("e" ,& Style {Fg :ansi .Yellow ,Attrs :AttrReverse },nil ), 214newWcCell ("l" ,& Style {Fg :ansi .Yellow ,Attrs :AttrReverse },nil ), 215newWcCell ("l" ,& Style {Fg :ansi .Yellow ,Attrs :AttrReverse },nil ), 216newWcCell ("o" ,& Style {Fg :ansi .Yellow ,Attrs :AttrReverse },nil ), 217newWcCell ("w" ,& Style {Fg :ansi .Yellow ,Attrs :AttrReverse },nil ), 218newWcCell (" " ,nil ,nil ), 219newWcCell ("P" ,& Style {Fg :ansi .Magenta ,Attrs :AttrBlink },nil ), 220newWcCell ("u" ,& Style {Fg :ansi .Magenta ,Attrs :AttrBlink },nil ), 221newWcCell ("r" ,& Style {Fg :ansi .Magenta ,Attrs :AttrBlink },nil ), 222newWcCell ("p" ,& Style {Fg :ansi .Magenta ,Attrs :AttrBlink },nil ), 223newWcCell ("l" ,& Style {Fg :ansi .Magenta ,Attrs :AttrBlink },nil ), 224newWcCell ("e" ,& Style {Fg :ansi .Magenta ,Attrs :AttrBlink },nil ), 225 }, 226 }, 227 }, 228 }, 229 { 230name :"different underline styles" , 231input :"\x1b[4:1mSingle\x1b[0m \x1b[4:2mDouble\x1b[0m \x1b[4:3mCurly\x1b[0m \x1b[4:4mDotted\x1b[0m \x1b[4:5mDashed\x1b[0m" , 232expectedWidth :33 , 233expectedHeight :1 , 234expected :& Buffer { 235Lines : []Line { 236 { 237newWcCell ("S" ,& Style {Underline :UnderlineStyleSingle },nil ), 238newWcCell ("i" ,& Style {Underline :UnderlineStyleSingle },nil ), 239newWcCell ("n" ,& Style {Underline :UnderlineStyleSingle },nil ), 240newWcCell ("g" ,& Style {Underline :UnderlineStyleSingle },nil ), 241newWcCell ("l" ,& Style {Underline :UnderlineStyleSingle },nil ), 242newWcCell ("e" ,& Style {Underline :UnderlineStyleSingle },nil ), 243newWcCell (" " ,nil ,nil ), 244newWcCell ("D" ,& Style {Underline :UnderlineStyleDouble },nil ), 245newWcCell ("o" ,& Style {Underline :UnderlineStyleDouble },nil ), 246newWcCell ("u" ,& Style {Underline :UnderlineStyleDouble },nil ), 247newWcCell ("b" ,& Style {Underline :UnderlineStyleDouble },nil ), 248newWcCell ("l" ,& Style {Underline :UnderlineStyleDouble },nil ), 249newWcCell ("e" ,& Style {Underline :UnderlineStyleDouble },nil ), 250newWcCell (" " ,nil ,nil ), 251newWcCell ("C" ,& Style {Underline :UnderlineStyleCurly },nil ), 252newWcCell ("u" ,& Style {Underline :UnderlineStyleCurly },nil ), 253newWcCell ("r" ,& Style {Underline :UnderlineStyleCurly },nil ), 254newWcCell ("l" ,& Style {Underline :UnderlineStyleCurly },nil ), 255newWcCell ("y" ,& Style {Underline :UnderlineStyleCurly },nil ), 256newWcCell (" " ,nil ,nil ), 257newWcCell ("D" ,& Style {Underline :UnderlineStyleDotted },nil ), 258newWcCell ("o" ,& Style {Underline :UnderlineStyleDotted },nil ), 259newWcCell ("t" ,& Style {Underline :UnderlineStyleDotted },nil ), 260newWcCell ("t" ,& Style {Underline :UnderlineStyleDotted },nil ), 261newWcCell ("e" ,& Style {Underline :UnderlineStyleDotted },nil ), 262newWcCell ("d" ,& Style {Underline :UnderlineStyleDotted },nil ), 263newWcCell (" " ,nil ,nil ), 264newWcCell ("D" ,& Style {Underline :UnderlineStyleDashed },nil ), 265newWcCell ("a" ,& Style {Underline :UnderlineStyleDashed },nil ), 266newWcCell ("s" ,& Style {Underline :UnderlineStyleDashed },nil ), 267newWcCell ("h" ,& Style {Underline :UnderlineStyleDashed },nil ), 268newWcCell ("e" ,& Style {Underline :UnderlineStyleDashed },nil ), 269newWcCell ("d" ,& Style {Underline :UnderlineStyleDashed },nil ), 270 }, 271 }, 272 }, 273 }, 274 { 275name :"truecolor and 256 color support" , 276input :"\x1b[38;2;255;0;0mRGB Red\x1b[0m \x1b[48;2;0;255;0mRGB Green BG\x1b[0m \x1b[38;5;33m256 Blue\x1b[0m" , 277expectedWidth :29 , 278expectedHeight :1 , 279expected :& Buffer { 280Lines : []Line { 281 { 282newWcCell ("R" ,& Style {Fg : color.RGBA {R :255 ,G :0 ,B :0 ,A :255 }},nil ), 283newWcCell ("G" ,& Style {Fg : color.RGBA {R :255 ,G :0 ,B :0 ,A :255 }},nil ), 284newWcCell ("B" ,& Style {Fg : color.RGBA {R :255 ,G :0 ,B :0 ,A :255 }},nil ), 285newWcCell (" " ,& Style {Fg : color.RGBA {R :255 ,G :0 ,B :0 ,A :255 }},nil ), 286newWcCell ("R" ,& Style {Fg : color.RGBA {R :255 ,G :0 ,B :0 ,A :255 }},nil ), 287newWcCell ("e" ,& Style {Fg : color.RGBA {R :255 ,G :0 ,B :0 ,A :255 }},nil ), 288newWcCell ("d" ,& Style {Fg : color.RGBA {R :255 ,G :0 ,B :0 ,A :255 }},nil ), 289newWcCell (" " ,nil ,nil ), 290newWcCell ("R" ,& Style {Bg : color.RGBA {R :0 ,G :255 ,B :0 ,A :255 }},nil ), 291newWcCell ("G" ,& Style {Bg : color.RGBA {R :0 ,G :255 ,B :0 ,A :255 }},nil ), 292newWcCell ("B" ,& Style {Bg : color.RGBA {R :0 ,G :255 ,B :0 ,A :255 }},nil ), 293newWcCell (" " ,& Style {Bg : color.RGBA {R :0 ,G :255 ,B :0 ,A :255 }},nil ), 294newWcCell ("G" ,& Style {Bg : color.RGBA {R :0 ,G :255 ,B :0 ,A :255 }},nil ), 295newWcCell ("r" ,& Style {Bg : color.RGBA {R :0 ,G :255 ,B :0 ,A :255 }},nil ), 296newWcCell ("e" ,& Style {Bg : color.RGBA {R :0 ,G :255 ,B :0 ,A :255 }},nil ), 297newWcCell ("e" ,& Style {Bg : color.RGBA {R :0 ,G :255 ,B :0 ,A :255 }},nil ), 298newWcCell ("n" ,& Style {Bg : color.RGBA {R :0 ,G :255 ,B :0 ,A :255 }},nil ), 299newWcCell (" " ,& Style {Bg : color.RGBA {R :0 ,G :255 ,B :0 ,A :255 }},nil ), 300newWcCell ("B" ,& Style {Bg : color.RGBA {R :0 ,G :255 ,B :0 ,A :255 }},nil ), 301newWcCell ("G" ,& Style {Bg : color.RGBA {R :0 ,G :255 ,B :0 ,A :255 }},nil ), 302newWcCell (" " ,nil ,nil ), 303newWcCell ("2" ,& Style {Fg :ansi .IndexedColor (33 )},nil ), 304newWcCell ("5" ,& Style {Fg :ansi .IndexedColor (33 )},nil ), 305newWcCell ("6" ,& Style {Fg :ansi .IndexedColor (33 )},nil ), 306newWcCell (" " ,& Style {Fg :ansi .IndexedColor (33 )},nil ), 307newWcCell ("B" ,& Style {Fg :ansi .IndexedColor (33 )},nil ), 308newWcCell ("l" ,& Style {Fg :ansi .IndexedColor (33 )},nil ), 309newWcCell ("u" ,& Style {Fg :ansi .IndexedColor (33 )},nil ), 310newWcCell ("e" ,& Style {Fg :ansi .IndexedColor (33 )},nil ), 311newWcCell ("e" ,& Style {Fg :ansi .IndexedColor (33 )},nil ), 312 }, 313 }, 314 }, 315 }, 316 { 317name :"hyperlink support" , 318input :"Normal \x1b]8;;https://charm.sh\x1b\\Charm\x1b]8;;\x1b\\ Text \x1b]8;;https://github.com/charmbracelet\x1b\\GitHub\x1b]8;;\x1b\\" , 319expectedWidth :24 , 320expectedHeight :1 , 321expected :& Buffer { 322Lines : []Line { 323 { 324newWcCell ("N" ,nil ,nil ), 325newWcCell ("o" ,nil ,nil ), 326newWcCell ("r" ,nil ,nil ), 327newWcCell ("m" ,nil ,nil ), 328newWcCell ("a" ,nil ,nil ), 329newWcCell ("l" ,nil ,nil ), 330newWcCell (" " ,nil ,nil ), 331newWcCell ("C" ,nil ,& Link {URL :"https://charm.sh" }), 332newWcCell ("h" ,nil ,& Link {URL :"https://charm.sh" }), 333newWcCell ("a" ,nil ,& Link {URL :"https://charm.sh" }), 334newWcCell ("r" ,nil ,& Link {URL :"https://charm.sh" }), 335newWcCell ("m" ,nil ,& Link {URL :"https://charm.sh" }), 336newWcCell (" " ,nil ,nil ), 337newWcCell ("T" ,nil ,nil ), 338newWcCell ("e" ,nil ,nil ), 339newWcCell ("x" ,nil ,nil ), 340newWcCell ("t" ,nil ,nil ), 341newWcCell (" " ,nil ,nil ), 342newWcCell ("G" ,nil ,& Link {URL :"https://github.com/charmbracelet" }), 343newWcCell ("i" ,nil ,& Link {URL :"https://github.com/charmbracelet" }), 344newWcCell ("t" ,nil ,& Link {URL :"https://github.com/charmbracelet" }), 345newWcCell ("H" ,nil ,& Link {URL :"https://github.com/charmbracelet" }), 346newWcCell ("u" ,nil ,& Link {URL :"https://github.com/charmbracelet" }), 347newWcCell ("b" ,nil ,& Link {URL :"https://github.com/charmbracelet" }), 348 }, 349 }, 350 }, 351 }, 352 { 353name :"complex mixed styling with hyperlinks" , 354input :"\x1b[31;1;2;3mR\x1b[22;23;1med \x1b]8;;https://charm.sh\x1b\\\x1b[4mCharm\x1b]8;;\x1b\\\x1b[0m \x1b[38;5;33;48;2;0;100;0m\x1b]8;;https://github.com\x1b\\GitHub\x1b]8;;\x1b\\\x1b[0m" , 355expectedWidth :16 , 356expectedHeight :1 , 357expected :& Buffer { 358Lines : []Line { 359 { 360newWcCell ("R" ,& Style {Fg :ansi .Red ,Attrs :AttrBold | AttrFaint | AttrItalic },nil ), 361newWcCell ("e" ,& Style {Fg :ansi .Red ,Attrs :AttrBold },nil ), 362newWcCell ("d" ,& Style {Fg :ansi .Red ,Attrs :AttrBold },nil ), 363newWcCell (" " ,& Style {Fg :ansi .Red ,Attrs :AttrBold },nil ), 364newWcCell ("C" ,& Style {Fg :ansi .Red ,Underline :UnderlineStyleSingle ,Attrs :AttrBold },& Link {URL :"https://charm.sh" }), 365newWcCell ("h" ,& Style {Fg :ansi .Red ,Underline :UnderlineStyleSingle ,Attrs :AttrBold },& Link {URL :"https://charm.sh" }), 366newWcCell ("a" ,& Style {Fg :ansi .Red ,Underline :UnderlineStyleSingle ,Attrs :AttrBold },& Link {URL :"https://charm.sh" }), 367newWcCell ("r" ,& Style {Fg :ansi .Red ,Underline :UnderlineStyleSingle ,Attrs :AttrBold },& Link {URL :"https://charm.sh" }), 368newWcCell ("m" ,& Style {Fg :ansi .Red ,Underline :UnderlineStyleSingle ,Attrs :AttrBold },& Link {URL :"https://charm.sh" }), 369newWcCell (" " ,nil ,nil ), 370newWcCell ("G" ,& Style {Fg :ansi .IndexedColor (33 ),Bg : color.RGBA {R :0 ,G :100 ,B :0 ,A :255 }},& Link {URL :"https://github.com" }), 371newWcCell ("i" ,& Style {Fg :ansi .IndexedColor (33 ),Bg : color.RGBA {R :0 ,G :100 ,B :0 ,A :255 }},& Link {URL :"https://github.com" }), 372newWcCell ("t" ,& Style {Fg :ansi .IndexedColor (33 ),Bg : color.RGBA {R :0 ,G :100 ,B :0 ,A :255 }},& Link {URL :"https://github.com" }), 373newWcCell ("H" ,& Style {Fg :ansi .IndexedColor (33 ),Bg : color.RGBA {R :0 ,G :100 ,B :0 ,A :255 }},& Link {URL :"https://github.com" }), 374newWcCell ("u" ,& Style {Fg :ansi .IndexedColor (33 ),Bg : color.RGBA {R :0 ,G :100 ,B :0 ,A :255 }},& Link {URL :"https://github.com" }), 375newWcCell ("b" ,& Style {Fg :ansi .IndexedColor (33 ),Bg : color.RGBA {R :0 ,G :100 ,B :0 ,A :255 }},& Link {URL :"https://github.com" }), 376 }, 377 }, 378 }, 379 }, 380 { 381// A sequence that carries data rather than driving the cursor 382// rides in the content of the cell that follows it, so it reaches 383// the terminal when the cell is painted. See #95. 384name :"APC sequence rides the next cell" , 385input :"\x1b_foo\x1b\\bar" , 386expectedWidth :3 , 387expectedHeight :1 , 388expected :& Buffer { 389Lines : []Line { 390 { 391newWcCell ("\x1b_foo\x1b\\b" ,nil ,nil ), 392newWcCell ("a" ,nil ,nil ), 393newWcCell ("r" ,nil ,nil ), 394 }, 395 }, 396 }, 397 }, 398 { 399// At the end of the string there is no following cell, so it folds 400// into the last one. A width-0 cell one past the content would sit 401// outside the bounds whenever the string filled them. 402name :"trailing APC folds into the last cell" , 403input :"bar\x1b_foo\x1b\\" , 404expectedWidth :3 , 405expectedHeight :1 , 406expected :& Buffer { 407Lines : []Line { 408 { 409newWcCell ("b" ,nil ,nil ), 410newWcCell ("a" ,nil ,nil ), 411newWcCell ("r\x1b_foo\x1b\\" ,nil ,nil ), 412 }, 413 }, 414 }, 415 }, 416 { 417// Cursor movement is not carried. Replaying it from inside a cell 418// would move the real cursor somewhere the renderer's model cannot 419// see, so it stays dropped. 420name :"cursor movement is not carried" , 421input :"a\x1b[5Cb" , 422expectedWidth :2 , 423expectedHeight :1 , 424expected :& Buffer { 425Lines : []Line { 426 { 427newWcCell ("a" ,nil ,nil ), 428newWcCell ("b" ,nil ,nil ), 429 }, 430 }, 431 }, 432 }, 433 } 434 435for i ,tc := range cases { 436t .Run (tc .name ,func (t * testing.T ) { 437t .Logf ("Running case %d: %s for %q" ,i + 1 ,tc .name ,tc .input ) 438ss := NewStyledString (tc .input ) 439area := ss .Bounds () 440buf := NewScreenBuffer (area .Dx (),area .Dy ()) 441ss .Draw (buf ,area ) 442if buf .Width ()!= tc .expectedWidth { 443t .Errorf ("case %d expected width %d, got %d" ,i + 1 ,tc .expectedWidth ,buf .Width ()) 444 } 445if buf .Height ()!= tc .expectedHeight { 446t .Errorf ("case %d expected height %d, got %d" ,i + 1 ,tc .expectedHeight ,buf .Height ()) 447 } 448for y ,line := range buf .Lines { 449for x ,cell := range line { 450if ! cellEqual (tc .expected .CellAt (x ,y ),& cell ) { 451t .Errorf ("case %d expected cell (%d, %d) %#v, got %#v" ,y + 1 ,x ,y ,tc .expected .CellAt (x ,y ),& cell ) 452 } 453 } 454 } 455 }) 456 } 457} 458 459func TestStyledStringEmptyLines (t * testing.T ) { 460// This test uses an input that results in empty lines when drawn to a smaller 461// screen buffer. 462input := "\x1b[31;1;4mHello, \x1b[32;22;4mWorld!\x1b[0m" 463ss := NewStyledString (input ) 464scr := NewScreenBuffer (5 ,3 ) 465ss .Draw (scr ,scr .Bounds ()) 466expected := & Buffer { 467Lines : []Line { 468 { 469newWcCell ("H" ,& Style {Fg :ansi .Red ,Underline :UnderlineStyleSingle ,Attrs :AttrBold },nil ), 470newWcCell ("e" ,& Style {Fg :ansi .Red ,Underline :UnderlineStyleSingle ,Attrs :AttrBold },nil ), 471newWcCell ("l" ,& Style {Fg :ansi .Red ,Underline :UnderlineStyleSingle ,Attrs :AttrBold },nil ), 472newWcCell ("l" ,& Style {Fg :ansi .Red ,Underline :UnderlineStyleSingle ,Attrs :AttrBold },nil ), 473newWcCell ("o" ,& Style {Fg :ansi .Red ,Underline :UnderlineStyleSingle ,Attrs :AttrBold },nil ), 474 }, 475NewLine (5 ), 476NewLine (5 ), 477 }, 478 } 479for y ,line := range scr .Lines { 480for x ,cell := range line { 481if ! cellEqual (expected .CellAt (x ,y ),& cell ) { 482t .Errorf ("expected cell (%d, %d) %#v, got %#v" ,x ,y ,expected .CellAt (x ,y ),& cell ) 483 } 484 } 485 } 486} 487 488func TestStyledStringLinesUnboundedContent (t * testing.T ) { 489const destination = "https://example.com" 490input := "\x1b[31mab\x1b[0m\n" + ansi .SetHyperlink (destination ,"" )+ "界" + ansi .ResetHyperlink () 491lines := NewStyledString (input ).Lines (ansi .GraphemeWidth ) 492 493if got := Lines (lines ).String ();got != "ab\n界" { 494t .Fatalf ("Lines().String() = %q, want %q" ,got ,"ab\n界" ) 495 } 496if cell := lines [1 ][0 ];cell .Width != 2 || cell .Link .URL != destination { 497t .Fatalf ("linked cell = %#v, want width 2 and link %q" ,cell ,destination ) 498 } 499} 500 501func newWcCell (s string ,style * Style ,link * Link )Cell { 502c := NewCell (ansi .WcWidth ,s ) 503if style != nil { 504c .Style = * style 505 } 506if link != nil { 507c .Link = * link 508 } 509return * c 510} 511 512func TestReadLinkAllowsSemicolonsInURL (t * testing.T ) { 513var link Link 514ReadLink ([]byte ("8;id=123;Other;Guide.md" ),& link ) 515 516if link .Params != "id=123" || link .URL != "Other;Guide.md" { 517t .Fatalf ("ReadLink() = %#v, want params %q and URL %q" ,link ,"id=123" ,"Other;Guide.md" ) 518 } 519} 520 521// ASCII-heavy line: the common case. Guards the printString re-decode 522// pre-filter (str[1] >= 0xc0) from regressing plain-text draws. 523func BenchmarkPrintStringASCII (b * testing.B ) { 524line := strings .Repeat ("The quick brown fox jumps over the lazy dog. " ,4 ) 525buf := NewScreenBuffer (200 ,4 ) 526b .ResetTimer () 527for i := 0 ;i < b .N ;i ++ { 528NewStyledString (line ).Draw (buf ,Rect (0 ,0 ,200 ,1 )) 529 } 530} 531 532// GraphemeWidth variant of the ASCII draw, since the decoder selection 533// differs. 534func BenchmarkPrintStringASCIIGrapheme (b * testing.B ) { 535line := strings .Repeat ("The quick brown fox jumps over the lazy dog. " ,4 ) 536buf := NewScreenBuffer (200 ,4 ) 537buf .Method = ansi .GraphemeWidth 538b .ResetTimer () 539for i := 0 ;i < b .N ;i ++ { 540NewStyledString (line ).Draw (buf ,Rect (0 ,0 ,200 ,1 )) 541 } 542} 543 544// Cluster-heavy line: keycaps, VS16 emoji, combining marks. Exercises the 545// FirstGraphemeCluster re-decode path. 546func BenchmarkPrintStringClusters (b * testing.B ) { 547line := strings .Repeat ("1️⃣❤️é☹️" ,20 ) 548buf := NewScreenBuffer (200 ,4 ) 549b .ResetTimer () 550for i := 0 ;i < b .N ;i ++ { 551NewStyledString (line ).Draw (buf ,Rect (0 ,0 ,200 ,1 )) 552 } 553} 554 555func TestStyledStringCombiningMarks (t * testing.T ) { 556cases := []struct { 557name string 558in string 559want []string 560 }{ 561 {"combining mark folds into its base" ,"a\u0301b" , []string {"a\u0301" ,"b" }}, 562 {"keycap stays in one cell" ,"1\ufe0f\u20e3x" , []string {"1\ufe0f\u20e3" ,"x" }}, 563 {"style change between cells" ,"a\u0301\x1b[31mR" , []string {"a\u0301" ,"R" }}, 564// The re-decode cannot fold this one, since the escape sequence sits 565// between the base and the mark. The mark still belongs to the cell 566// before it, and dropping it would lose the glyph. 567 {"style change between base and mark" ,"a\x1b[31m\u0301b" , []string {"a\u0301" ,"b" }}, 568 } 569 570for _ ,tc := range cases { 571t .Run (tc .name ,func (t * testing.T ) { 572buf := NewScreenBuffer (10 ,1 ) 573NewStyledString (tc .in ).Draw (buf ,Rect (0 ,0 ,10 ,1 )) 574for x ,want := range tc .want { 575if got := buf .CellAt (x ,0 ).Content ;got != want { 576t .Errorf ("cell %d = %q, want %q" ,x ,got ,want ) 577 } 578 } 579 }) 580 } 581} 582 583// A string-type sequence is only carried when it actually ended, and only when 584// it is one of the kinds a cell has any business replaying. 585func TestPassThroughTerminators (t * testing.T ) { 586for name ,tc := range map [string ]struct { 587input string 588carried bool 589 }{ 590"APC ends with ST" : {"\x1b_x\x1b\\a" ,true }, 591"DCS ends with ST" : {"\x1bP1$r0m\x1b\\a" ,true }, 592"SOS ends with ST" : {"\x1bXfoo\x1b\\a" ,true }, 593"PM ends with ST" : {"\x1b^bar\x1b\\a" ,true }, 594 595// The parser returns what it has when the input runs out. An 596// unterminated introducer in a cell would have the terminal swallow 597// everything painted after it. 598"APC never terminated" : {"a\x1b_x" ,false }, 599"OSC never terminated" : {"a\x1b]0;t" ,false }, 600"DCS never terminated" : {"a\x1bP1$r0m" ,false }, 601 602// 0x9c is a UTF-8 continuation byte as well as C1 ST, so a sequence 603// that ran out partway through a character can end in a byte that 604// looks like a terminator. U+071C encodes as dc 9c. 605"APC cut off mid-character" : {"a\x1b_x\u071c" ,false }, 606"OSC cut off mid-character" : {"a\x1b]0;\u071c" ,false }, 607"APC ends with C1 ST" : {"\x1b_x\x9ca" ,false }, 608 609// An OSC carries data but a cell is painted again on every repaint. A 610// title survives that; a clipboard write or a notification should not 611// fire again on each resize. 612"OSC ends with BEL" : {"\x1b]0;t\x07a" ,false }, 613"OSC ends with ST" : {"\x1b]0;t\x1b\\a" ,false }, 614 } { 615t .Run (name ,func (t * testing.T ) { 616ss := NewStyledString (tc .input ) 617area := ss .Bounds () 618buf := NewScreenBuffer (area .Dx (),area .Dy ()) 619ss .Draw (buf ,area ) 620 621var content string 622for x := 0 ;x < buf .Width ();x ++ { 623if c := buf .CellAt (x ,0 );c != nil { 624content += c .Content 625 } 626 } 627// Every input above introduces its sequence with ESC, so an ESC 628// surviving into the cells is the sequence riding along. 629if carried := strings .ContainsRune (content ,ansi .ESC );carried != tc .carried { 630t .Errorf ("carried = %v, want %v (cells hold %q)" ,carried ,tc .carried ,content ) 631 } 632 }) 633 } 634} 635 636// A carried sequence must not change how wide its cell measures. 637// [lineHasDrift] works the width out from the content, so a sequence that 638// counted would have the renderer treat every line holding one as drift-prone 639// and repaint it whole. 640func TestPassThroughDoesNotAffectWidth (t * testing.T ) { 641ss := NewStyledString ("\x1b_x\x1b\\abc" ) 642area := ss .Bounds () 643buf := NewScreenBuffer (area .Dx (),area .Dy ()) 644ss .Draw (buf ,area ) 645 646if c := buf .CellAt (0 ,0 );c == nil || c .Width != 1 { 647t .Errorf ("cell holding a sequence has width %v, want 1" ,c ) 648 } 649if lineHasDrift (ansi .WcWidth ,buf .Line (0 )) { 650t .Error ("a line holding a pass-through sequence was flagged drift-prone" ) 651 } 652}