1// Copyright 2021 The TCell Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use file except in compliance with the License. 5// You may obtain a copy of the license at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package terminfo 16 17import ( 18 "bytes" 19 "errors" 20 "fmt" 21 "io" 22 "os" 23 "strconv" 24 "strings" 25 "sync" 26 "time" 27) 28 29var ( 30 // ErrTermNotFound indicates that a suitable terminal entry could 31 // not be found. This can result from either not having TERM set, 32 // or from the TERM failing to support certain minimal functionality, 33 // in particular absolute cursor addressability (the cup capability) 34 // is required. For example, legacy "adm3" lacks this capability, 35 // whereas the slightly newer "adm3a" supports it. This failure 36 // occurs most often with "dumb". 37 ErrTermNotFound = errors.New("terminal entry not found") 38) 39 40// Terminfo represents a terminfo entry. Note that we use friendly names 41// in Go, but when we write out JSON, we use the same names as terminfo. 42// The name, aliases and smous, rmous fields do not come from terminfo directly. 43type Terminfo struct { 44 Name string 45 Aliases []string 46 Columns int // cols 47 Lines int // lines 48 Colors int // colors 49 Bell string // bell 50 Clear string // clear 51 EnterCA string // smcup 52 ExitCA string // rmcup 53 ShowCursor string // cnorm 54 HideCursor string // civis 55 AttrOff string // sgr0 56 Underline string // smul 57 Bold string // bold 58 Blink string // blink 59 Reverse string // rev 60 Dim string // dim 61 Italic string // sitm 62 EnterKeypad string // smkx 63 ExitKeypad string // rmkx 64 SetFg string // setaf 65 SetBg string // setab 66 ResetFgBg string // op 67 SetCursor string // cup 68 CursorBack1 string // cub1 69 CursorUp1 string // cuu1 70 PadChar string // pad 71 KeyBackspace string // kbs 72 KeyF1 string // kf1 73 KeyF2 string // kf2 74 KeyF3 string // kf3 75 KeyF4 string // kf4 76 KeyF5 string // kf5 77 KeyF6 string // kf6 78 KeyF7 string // kf7 79 KeyF8 string // kf8 80 KeyF9 string // kf9 81 KeyF10 string // kf10 82 KeyF11 string // kf11 83 KeyF12 string // kf12 84 KeyF13 string // kf13 85 KeyF14 string // kf14 86 KeyF15 string // kf15 87 KeyF16 string // kf16 88 KeyF17 string // kf17 89 KeyF18 string // kf18 90 KeyF19 string // kf19 91 KeyF20 string // kf20 92 KeyF21 string // kf21 93 KeyF22 string // kf22 94 KeyF23 string // kf23 95 KeyF24 string // kf24 96 KeyF25 string // kf25 97 KeyF26 string // kf26 98 KeyF27 string // kf27 99 KeyF28 string // kf28 100 KeyF29 string // kf29 101 KeyF30 string // kf30 102 KeyF31 string // kf31 103 KeyF32 string // kf32 104 KeyF33 string // kf33 105 KeyF34 string // kf34 106 KeyF35 string // kf35 107 KeyF36 string // kf36 108 KeyF37 string // kf37 109 KeyF38 string // kf38 110 KeyF39 string // kf39 111 KeyF40 string // kf40 112 KeyF41 string // kf41 113 KeyF42 string // kf42 114 KeyF43 string // kf43 115 KeyF44 string // kf44 116 KeyF45 string // kf45 117 KeyF46 string // kf46 118 KeyF47 string // kf47 119 KeyF48 string // kf48 120 KeyF49 string // kf49 121 KeyF50 string // kf50 122 KeyF51 string // kf51 123 KeyF52 string // kf52 124 KeyF53 string // kf53 125 KeyF54 string // kf54 126 KeyF55 string // kf55 127 KeyF56 string // kf56 128 KeyF57 string // kf57 129 KeyF58 string // kf58 130 KeyF59 string // kf59 131 KeyF60 string // kf60 132 KeyF61 string // kf61 133 KeyF62 string // kf62 134 KeyF63 string // kf63 135 KeyF64 string // kf64 136 KeyInsert string // kich1 137 KeyDelete string // kdch1 138 KeyHome string // khome 139 KeyEnd string // kend 140 KeyHelp string // khlp 141 KeyPgUp string // kpp 142 KeyPgDn string // knp 143 KeyUp string // kcuu1 144 KeyDown string // kcud1 145 KeyLeft string // kcub1 146 KeyRight string // kcuf1 147 KeyBacktab string // kcbt 148 KeyExit string // kext 149 KeyClear string // kclr 150 KeyPrint string // kprt 151 KeyCancel string // kcan 152 Mouse string // kmous 153 AltChars string // acsc 154 EnterAcs string // smacs 155 ExitAcs string // rmacs 156 EnableAcs string // enacs 157 KeyShfRight string // kRIT 158 KeyShfLeft string // kLFT 159 KeyShfHome string // kHOM 160 KeyShfEnd string // kEND 161 KeyShfInsert string // kIC 162 KeyShfDelete string // kDC 163 164 // These are non-standard extensions to terminfo. This includes 165 // true color support, and some additional keys. Its kind of bizarre 166 // that shifted variants of left and right exist, but not up and down. 167 // Terminal support for these are going to vary amongst XTerm 168 // emulations, so don't depend too much on them in your application. 169 170 StrikeThrough string // smxx 171 SetFgBg string // setfgbg 172 SetFgBgRGB string // setfgbgrgb 173 SetFgRGB string // setfrgb 174 SetBgRGB string // setbrgb 175 KeyShfUp string // shift-up 176 KeyShfDown string // shift-down 177 KeyShfPgUp string // shift-kpp 178 KeyShfPgDn string // shift-knp 179 KeyCtrlUp string // ctrl-up 180 KeyCtrlDown string // ctrl-left 181 KeyCtrlRight string // ctrl-right 182 KeyCtrlLeft string // ctrl-left 183 KeyMetaUp string // meta-up 184 KeyMetaDown string // meta-left 185 KeyMetaRight string // meta-right 186 KeyMetaLeft string // meta-left 187 KeyAltUp string // alt-up 188 KeyAltDown string // alt-left 189 KeyAltRight string // alt-right 190 KeyAltLeft string // alt-left 191 KeyCtrlHome string 192 KeyCtrlEnd string 193 KeyMetaHome string 194 KeyMetaEnd string 195 KeyAltHome string 196 KeyAltEnd string 197 KeyAltShfUp string 198 KeyAltShfDown string 199 KeyAltShfLeft string 200 KeyAltShfRight string 201 KeyMetaShfUp string 202 KeyMetaShfDown string 203 KeyMetaShfLeft string 204 KeyMetaShfRight string 205 KeyCtrlShfUp string 206 KeyCtrlShfDown string 207 KeyCtrlShfLeft string 208 KeyCtrlShfRight string 209 KeyCtrlShfHome string 210 KeyCtrlShfEnd string 211 KeyAltShfHome string 212 KeyAltShfEnd string 213 KeyMetaShfHome string 214 KeyMetaShfEnd string 215 EnablePaste string // bracketed paste mode 216 DisablePaste string 217 PasteStart string 218 PasteEnd string 219 Modifiers int 220 InsertChar string // string to insert a character (ich1) 221 AutoMargin bool // true if writing to last cell in line advances 222 TrueColor bool // true if the terminal supports direct color 223} 224 225const ( 226 ModifiersNone = 0 227 ModifiersXTerm = 1 228) 229 230type stackElem struct { 231 s string 232 i int 233 isStr bool 234 isInt bool 235} 236 237type stack []stackElem 238 239func (st stack) Push(v string) stack { 240 e := stackElem{ 241 s: v, 242 isStr: true, 243 } 244 return append(st, e) 245} 246 247func (st stack) Pop() (string, stack) { 248 v := "" 249 if len(st) > 0 { 250 e := st[len(st)-1] 251 st = st[:len(st)-1] 252 if e.isStr { 253 v = e.s 254 } else { 255 v = strconv.Itoa(e.i) 256 } 257 } 258 return v, st 259} 260 261func (st stack) PopInt() (int, stack) { 262 if len(st) > 0 { 263 e := st[len(st)-1] 264 st = st[:len(st)-1] 265 if e.isInt { 266 return e.i, st 267 } else if e.isStr { 268 // If the string that was pushed was the representation 269 // of a number e.g. '123', then return the number. If the 270 // conversion doesn't work, assume the string pushed was 271 // intended to return, as an int, the ascii representation 272 // of the (one and only) character. 273 i, err := strconv.Atoi(e.s) 274 if err == nil { 275 return i, st 276 } else if len(e.s) >= 1 { 277 return int(e.s[0]), st 278 } 279 } 280 } 281 return 0, st 282} 283 284func (st stack) PopBool() (bool, stack) { 285 if len(st) > 0 { 286 e := st[len(st)-1] 287 st = st[:len(st)-1] 288 if e.isStr { 289 if e.s == "1" { 290 return true, st 291 } 292 return false, st 293 } else if e.i == 1 { 294 return true, st 295 } else { 296 return false, st 297 } 298 } 299 return false, st 300} 301 302func (st stack) PushInt(i int) stack { 303 e := stackElem{ 304 i: i, 305 isInt: true, 306 } 307 return append(st, e) 308} 309 310func (st stack) PushBool(i bool) stack { 311 if i { 312 return st.PushInt(1) 313 } 314 return st.PushInt(0) 315} 316 317// static vars 318var svars [26]string 319 320// paramsBuffer handles some persistent state for TParam. Technically we 321// could probably dispense with this, but caching buffer arrays gives us 322// a nice little performance boost. Furthermore, we know that TParam is 323// rarely (never?) called re-entrantly, so we can just reuse the same 324// buffers, making it thread-safe by stashing a lock. 325type paramsBuffer struct { 326 out bytes.Buffer 327 buf bytes.Buffer 328 lk sync.Mutex 329} 330 331// Start initializes the params buffer with the initial string data. 332// It also locks the paramsBuffer. The caller must call End() when 333// finished. 334func (pb *paramsBuffer) Start(s string) { 335 pb.lk.Lock() 336 pb.out.Reset() 337 pb.buf.Reset() 338 pb.buf.WriteString(s) 339} 340 341// End returns the final output from TParam, but it also releases the lock. 342func (pb *paramsBuffer) End() string { 343 s := pb.out.String() 344 pb.lk.Unlock() 345 return s 346} 347 348// NextCh returns the next input character to the expander. 349func (pb *paramsBuffer) NextCh() (byte, error) { 350 return pb.buf.ReadByte() 351} 352 353// PutCh "emits" (rather schedules for output) a single byte character. 354func (pb *paramsBuffer) PutCh(ch byte) { 355 pb.out.WriteByte(ch) 356} 357 358// PutString schedules a string for output. 359func (pb *paramsBuffer) PutString(s string) { 360 pb.out.WriteString(s) 361} 362 363var pb = ¶msBuffer{} 364 365// TParm takes a terminfo parameterized string, such as setaf or cup, and 366// evaluates the string, and returns the result with the parameter 367// applied. 368func (t *Terminfo) TParm(s string, p ...int) string { 369 var stk stack 370 var a, b string 371 var ai, bi int 372 var ab bool 373 var dvars [26]string 374 var params [9]int 375 376 pb.Start(s) 377 378 // make sure we always have 9 parameters -- makes it easier 379 // later to skip checks 380 for i := 0; i < len(params) && i < len(p); i++ { 381 params[i] = p[i] 382 } 383 384 nest := 0 385 386 for { 387 388 ch, err := pb.NextCh() 389 if err != nil { 390 break 391 } 392 393 if ch != '%' { 394 pb.PutCh(ch) 395 continue 396 } 397 398 ch, err = pb.NextCh() 399 if err != nil { 400 // XXX Error 401 break 402 } 403 404 switch ch { 405 case '%': // quoted % 406 pb.PutCh(ch) 407 408 case 'i': // increment both parameters (ANSI cup support) 409 params[0]++ 410 params[1]++ 411 412 case 'c', 's': 413 // NB: these, and 'd' below are special cased for 414 // efficiency. They could be handled by the richer 415 // format support below, less efficiently. 416 a, stk = stk.Pop() 417 pb.PutString(a) 418 419 case 'd': 420 ai, stk = stk.PopInt() 421 pb.PutString(strconv.Itoa(ai)) 422 423 case '0', '1', '2', '3', '4', 'x', 'X', 'o', ':': 424 // This is pretty suboptimal, but this is rarely used. 425 // None of the mainstream terminals use any of this, 426 // and it would surprise me if this code is ever 427 // executed outside of test cases. 428 f := "%" 429 if ch == ':' { 430 ch, _ = pb.NextCh() 431 } 432 f += string(ch) 433 for ch == '+' || ch == '-' || ch == '#' || ch == ' ' { 434 ch, _ = pb.NextCh() 435 f += string(ch) 436 } 437 for (ch >= '0' && ch <= '9') || ch == '.' { 438 ch, _ = pb.NextCh() 439 f += string(ch) 440 } 441 switch ch { 442 case 'd', 'x', 'X', 'o': 443 ai, stk = stk.PopInt() 444 pb.PutString(fmt.Sprintf(f, ai)) 445 case 'c', 's': 446 a, stk = stk.Pop() 447 pb.PutString(fmt.Sprintf(f, a)) 448 } 449 450 case 'p': // push parameter 451 ch, _ = pb.NextCh() 452 ai = int(ch - '1') 453 if ai >= 0 && ai < len(params) { 454 stk = stk.PushInt(params[ai]) 455 } else { 456 stk = stk.PushInt(0) 457 } 458 459 case 'P': // pop & store variable 460 ch, _ = pb.NextCh() 461 if ch >= 'A' && ch <= 'Z' { 462 svars[int(ch-'A')], stk = stk.Pop() 463 } else if ch >= 'a' && ch <= 'z' { 464 dvars[int(ch-'a')], stk = stk.Pop() 465 } 466 467 case 'g': // recall & push variable 468 ch, _ = pb.NextCh() 469 if ch >= 'A' && ch <= 'Z' { 470 stk = stk.Push(svars[int(ch-'A')]) 471 } else if ch >= 'a' && ch <= 'z' { 472 stk = stk.Push(dvars[int(ch-'a')]) 473 } 474 475 case '\'': // push(char) 476 ch, _ = pb.NextCh() 477 pb.NextCh() // must be ' but we don't check 478 stk = stk.Push(string(ch)) 479 480 case '{': // push(int) 481 ai = 0 482 ch, _ = pb.NextCh() 483 for ch >= '0' && ch <= '9' { 484 ai *= 10 485 ai += int(ch - '0') 486 ch, _ = pb.NextCh() 487 } 488 // ch must be '}' but no verification 489 stk = stk.PushInt(ai) 490 491 case 'l': // push(strlen(pop)) 492 a, stk = stk.Pop() 493 stk = stk.PushInt(len(a)) 494 495 case '+': 496 bi, stk = stk.PopInt() 497 ai, stk = stk.PopInt() 498 stk = stk.PushInt(ai + bi) 499 500 case '-': 501 bi, stk = stk.PopInt() 502 ai, stk = stk.PopInt() 503 stk = stk.PushInt(ai - bi) 504 505 case '*': 506 bi, stk = stk.PopInt() 507 ai, stk = stk.PopInt() 508 stk = stk.PushInt(ai * bi) 509 510 case '/': 511 bi, stk = stk.PopInt() 512 ai, stk = stk.PopInt() 513 if bi != 0 { 514 stk = stk.PushInt(ai / bi) 515 } else { 516 stk = stk.PushInt(0) 517 } 518 519 case 'm': // push(pop mod pop) 520 bi, stk = stk.PopInt() 521 ai, stk = stk.PopInt() 522 if bi != 0 { 523 stk = stk.PushInt(ai % bi) 524 } else { 525 stk = stk.PushInt(0) 526 } 527 528 case '&': // AND 529 bi, stk = stk.PopInt() 530 ai, stk = stk.PopInt() 531 stk = stk.PushInt(ai & bi) 532 533 case '|': // OR 534 bi, stk = stk.PopInt() 535 ai, stk = stk.PopInt() 536 stk = stk.PushInt(ai | bi) 537 538 case '^': // XOR 539 bi, stk = stk.PopInt() 540 ai, stk = stk.PopInt() 541 stk = stk.PushInt(ai ^ bi) 542 543 case '~': // bit complement 544 ai, stk = stk.PopInt() 545 stk = stk.PushInt(ai ^ -1) 546 547 case '!': // logical NOT 548 ai, stk = stk.PopInt() 549 stk = stk.PushBool(ai != 0) 550 551 case '=': // numeric compare or string compare 552 b, stk = stk.Pop() 553 a, stk = stk.Pop() 554 stk = stk.PushBool(a == b) 555 556 case '>': // greater than, numeric 557 bi, stk = stk.PopInt() 558 ai, stk = stk.PopInt() 559 stk = stk.PushBool(ai > bi) 560 561 case '<': // less than, numeric 562 bi, stk = stk.PopInt() 563 ai, stk = stk.PopInt() 564 stk = stk.PushBool(ai < bi) 565 566 case '?': // start conditional 567 568 case 't': 569 ab, stk = stk.PopBool() 570 if ab { 571 // just keep going 572 break 573 } 574 nest = 0 575 ifloop: 576 // this loop consumes everything until we hit our else, 577 // or the end of the conditional 578 for { 579 ch, err = pb.NextCh() 580 if err != nil { 581 break 582 } 583 if ch != '%' { 584 continue 585 } 586 ch, _ = pb.NextCh() 587 switch ch { 588 case ';': 589 if nest == 0 { 590 break ifloop 591 } 592 nest-- 593 case '?': 594 nest++ 595 case 'e': 596 if nest == 0 { 597 break ifloop 598 } 599 } 600 } 601 602 case 'e': 603 // if we got here, it means we didn't use the else 604 // in the 't' case above, and we should skip until 605 // the end of the conditional 606 nest = 0 607 elloop: 608 for { 609 ch, err = pb.NextCh() 610 if err != nil { 611 break 612 } 613 if ch != '%' { 614 continue 615 } 616 ch, _ = pb.NextCh() 617 switch ch { 618 case ';': 619 if nest == 0 { 620 break elloop 621 } 622 nest-- 623 case '?': 624 nest++ 625 } 626 } 627 628 case ';': // endif 629 630 } 631 } 632 633 return pb.End() 634} 635 636// TPuts emits the string to the writer, but expands inline padding 637// indications (of the form $<[delay]> where [delay] is msec) to 638// a suitable time (unless the terminfo string indicates this isn't needed 639// by specifying npc - no padding). All Terminfo based strings should be 640// emitted using this function. 641func (t *Terminfo) TPuts(w io.Writer, s string) { 642 for { 643 beg := strings.Index(s, "$<") 644 if beg < 0 { 645 // Most strings don't need padding, which is good news! 646 io.WriteString(w, s) 647 return 648 } 649 io.WriteString(w, s[:beg]) 650 s = s[beg+2:] 651 end := strings.Index(s, ">") 652 if end < 0 { 653 // unterminated.. just emit bytes unadulterated 654 io.WriteString(w, "$<"+s) 655 return 656 } 657 val := s[:end] 658 s = s[end+1:] 659 padus := 0 660 unit := time.Millisecond 661 dot := false 662 loop: 663 for i := range val { 664 switch val[i] { 665 case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': 666 padus *= 10 667 padus += int(val[i] - '0') 668 if dot { 669 unit /= 10 670 } 671 case '.': 672 if !dot { 673 dot = true 674 } else { 675 break loop 676 } 677 default: 678 break loop 679 } 680 } 681 682 // Curses historically uses padding to achieve "fine grained" 683 // delays. We have much better clocks these days, and so we 684 // do not rely on padding but simply sleep a bit. 685 if len(t.PadChar) > 0 { 686 time.Sleep(unit * time.Duration(padus)) 687 } 688 } 689} 690 691// TGoto returns a string suitable for addressing the cursor at the given 692// row and column. The origin 0, 0 is in the upper left corner of the screen. 693func (t *Terminfo) TGoto(col, row int) string { 694 return t.TParm(t.SetCursor, row, col) 695} 696 697// TColor returns a string corresponding to the given foreground and background 698// colors. Either fg or bg can be set to -1 to elide. 699func (t *Terminfo) TColor(fi, bi int) string { 700 rv := "" 701 // As a special case, we map bright colors to lower versions if the 702 // color table only holds 8. For the remaining 240 colors, the user 703 // is out of luck. Someday we could create a mapping table, but its 704 // not worth it. 705 if t.Colors == 8 { 706 if fi > 7 && fi < 16 { 707 fi -= 8 708 } 709 if bi > 7 && bi < 16 { 710 bi -= 8 711 } 712 } 713 if t.Colors > fi && fi >= 0 { 714 rv += t.TParm(t.SetFg, fi) 715 } 716 if t.Colors > bi && bi >= 0 { 717 rv += t.TParm(t.SetBg, bi) 718 } 719 return rv 720} 721 722var ( 723 dblock sync.Mutex 724 terminfos = make(map[string]*Terminfo) 725 aliases = make(map[string]string) 726) 727 728// AddTerminfo can be called to register a new Terminfo entry. 729func AddTerminfo(t *Terminfo) { 730 dblock.Lock() 731 terminfos[t.Name] = t 732 for _, x := range t.Aliases { 733 terminfos[x] = t 734 } 735 dblock.Unlock() 736} 737 738// LookupTerminfo attempts to find a definition for the named $TERM. 739func LookupTerminfo(name string) (*Terminfo, error) { 740 if name == "" { 741 // else on windows: index out of bounds 742 // on the name[0] reference below 743 return nil, ErrTermNotFound 744 } 745 746 addtruecolor := false 747 add256color := false 748 switch os.Getenv("COLORTERM") { 749 case "truecolor", "24bit", "24-bit": 750 addtruecolor = true 751 } 752 dblock.Lock() 753 t := terminfos[name] 754 dblock.Unlock() 755 756 // If the name ends in -truecolor, then fabricate an entry 757 // from the corresponding -256color, -color, or bare terminal. 758 if t != nil && t.TrueColor { 759 addtruecolor = true 760 } else if t == nil && strings.HasSuffix(name, "-truecolor") { 761 762 suffixes := []string{ 763 "-256color", 764 "-88color", 765 "-color", 766 "", 767 } 768 base := name[:len(name)-len("-truecolor")] 769 for _, s := range suffixes { 770 if t, _ = LookupTerminfo(base + s); t != nil { 771 addtruecolor = true 772 break 773 } 774 } 775 } 776 777 // If the name ends in -256color, maybe fabricate using the xterm 256 color sequences 778 if t == nil && strings.HasSuffix(name, "-256color") { 779 suffixes := []string{ 780 "-88color", 781 "-color", 782 } 783 base := name[:len(name)-len("-256color")] 784 for _, s := range suffixes { 785 if t, _ = LookupTerminfo(base + s); t != nil { 786 add256color = true 787 break 788 } 789 } 790 } 791 792 if t == nil { 793 return nil, ErrTermNotFound 794 } 795 796 switch os.Getenv("TCELL_TRUECOLOR") { 797 case "": 798 case "disable": 799 addtruecolor = false 800 default: 801 addtruecolor = true 802 } 803 804 // If the user has requested 24-bit color with $COLORTERM, then 805 // amend the value (unless already present). This means we don't 806 // need to have a value present. 807 if addtruecolor && 808 t.SetFgBgRGB == "" && 809 t.SetFgRGB == "" && 810 t.SetBgRGB == "" { 811 812 // Supply vanilla ISO 8613-6:1994 24-bit color sequences. 813 t.SetFgRGB = "\x1b[38;2;%p1%d;%p2%d;%p3%dm" 814 t.SetBgRGB = "\x1b[48;2;%p1%d;%p2%d;%p3%dm" 815 t.SetFgBgRGB = "\x1b[38;2;%p1%d;%p2%d;%p3%d;" + 816 "48;2;%p4%d;%p5%d;%p6%dm" 817 } 818 819 if add256color { 820 t.Colors = 256 821 t.SetFg = "\x1b[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m" 822 t.SetBg = "\x1b[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m" 823 t.SetFgBg = "\x1b[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;;%?%p2%{8}%<%t4%p2%d%e%p2%{16}%<%t10%p2%{8}%-%d%e48;5;%p2%d%;m" 824 t.ResetFgBg = "\x1b[39;49m" 825 } 826 return t, nil 827} 828