1// Copyright 2020 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 MouseMode string // XM 154 AltChars string // acsc 155 EnterAcs string // smacs 156 ExitAcs string // rmacs 157 EnableAcs string // enacs 158 KeyShfRight string // kRIT 159 KeyShfLeft string // kLFT 160 KeyShfHome string // kHOM 161 KeyShfEnd string // kEND 162 KeyShfInsert string // kIC 163 KeyShfDelete string // kDC 164 165 // These are non-standard extensions to terminfo. This includes 166 // true color support, and some additional keys. Its kind of bizarre 167 // that shifted variants of left and right exist, but not up and down. 168 // Terminal support for these are going to vary amongst XTerm 169 // emulations, so don't depend too much on them in your application. 170 171 StrikeThrough string // smxx 172 SetFgBg string // setfgbg 173 SetFgBgRGB string // setfgbgrgb 174 SetFgRGB string // setfrgb 175 SetBgRGB string // setbrgb 176 KeyShfUp string // shift-up 177 KeyShfDown string // shift-down 178 KeyShfPgUp string // shift-kpp 179 KeyShfPgDn string // shift-knp 180 KeyCtrlUp string // ctrl-up 181 KeyCtrlDown string // ctrl-left 182 KeyCtrlRight string // ctrl-right 183 KeyCtrlLeft string // ctrl-left 184 KeyMetaUp string // meta-up 185 KeyMetaDown string // meta-left 186 KeyMetaRight string // meta-right 187 KeyMetaLeft string // meta-left 188 KeyAltUp string // alt-up 189 KeyAltDown string // alt-left 190 KeyAltRight string // alt-right 191 KeyAltLeft string // alt-left 192 KeyCtrlHome string 193 KeyCtrlEnd string 194 KeyMetaHome string 195 KeyMetaEnd string 196 KeyAltHome string 197 KeyAltEnd string 198 KeyAltShfUp string 199 KeyAltShfDown string 200 KeyAltShfLeft string 201 KeyAltShfRight string 202 KeyMetaShfUp string 203 KeyMetaShfDown string 204 KeyMetaShfLeft string 205 KeyMetaShfRight string 206 KeyCtrlShfUp string 207 KeyCtrlShfDown string 208 KeyCtrlShfLeft string 209 KeyCtrlShfRight string 210 KeyCtrlShfHome string 211 KeyCtrlShfEnd string 212 KeyAltShfHome string 213 KeyAltShfEnd string 214 KeyMetaShfHome string 215 KeyMetaShfEnd string 216 EnablePaste string // bracketed paste mode 217 DisablePaste string 218 PasteStart string 219 PasteEnd string 220 Modifiers int 221 TrueColor bool // true if the terminal supports direct color 222} 223 224const ( 225 ModifiersNone = 0 226 ModifiersXTerm = 1 227) 228 229type stackElem struct { 230 s string 231 i int 232 isStr bool 233 isInt bool 234} 235 236type stack []stackElem 237 238func (st stack) Push(v string) stack { 239 e := stackElem{ 240 s: v, 241 isStr: true, 242 } 243 return append(st, e) 244} 245 246func (st stack) Pop() (string, stack) { 247 v := "" 248 if len(st) > 0 { 249 e := st[len(st)-1] 250 st = st[:len(st)-1] 251 if e.isStr { 252 v = e.s 253 } else { 254 v = strconv.Itoa(e.i) 255 } 256 } 257 return v, st 258} 259 260func (st stack) PopInt() (int, stack) { 261 if len(st) > 0 { 262 e := st[len(st)-1] 263 st = st[:len(st)-1] 264 if e.isInt { 265 return e.i, st 266 } else if e.isStr { 267 // If the string that was pushed was the representation 268 // of a number e.g. '123', then return the number. If the 269 // conversion doesn't work, assume the string pushed was 270 // intended to return, as an int, the ascii representation 271 // of the (one and only) character. 272 i, err := strconv.Atoi(e.s) 273 if err == nil { 274 return i, st 275 } else if len(e.s) >= 1 { 276 return int(e.s[0]), st 277 } 278 } 279 } 280 return 0, st 281} 282 283func (st stack) PopBool() (bool, stack) { 284 if len(st) > 0 { 285 e := st[len(st)-1] 286 st = st[:len(st)-1] 287 if e.isStr { 288 if e.s == "1" { 289 return true, st 290 } 291 return false, st 292 } else if e.i == 1 { 293 return true, st 294 } else { 295 return false, st 296 } 297 } 298 return false, st 299} 300 301func (st stack) PushInt(i int) stack { 302 e := stackElem{ 303 i: i, 304 isInt: true, 305 } 306 return append(st, e) 307} 308 309func (st stack) PushBool(i bool) stack { 310 if i { 311 return st.PushInt(1) 312 } 313 return st.PushInt(0) 314} 315 316// static vars 317var svars [26]string 318 319// paramsBuffer handles some persistent state for TParam. Technically we 320// could probably dispense with this, but caching buffer arrays gives us 321// a nice little performance boost. Furthermore, we know that TParam is 322// rarely (never?) called re-entrantly, so we can just reuse the same 323// buffers, making it thread-safe by stashing a lock. 324type paramsBuffer struct { 325 out bytes.Buffer 326 buf bytes.Buffer 327 lk sync.Mutex 328} 329 330// Start initializes the params buffer with the initial string data. 331// It also locks the paramsBuffer. The caller must call End() when 332// finished. 333func (pb *paramsBuffer) Start(s string) { 334 pb.lk.Lock() 335 pb.out.Reset() 336 pb.buf.Reset() 337 pb.buf.WriteString(s) 338} 339 340// End returns the final output from TParam, but it also releases the lock. 341func (pb *paramsBuffer) End() string { 342 s := pb.out.String() 343 pb.lk.Unlock() 344 return s 345} 346 347// NextCh returns the next input character to the expander. 348func (pb *paramsBuffer) NextCh() (byte, error) { 349 return pb.buf.ReadByte() 350} 351 352// PutCh "emits" (rather schedules for output) a single byte character. 353func (pb *paramsBuffer) PutCh(ch byte) { 354 pb.out.WriteByte(ch) 355} 356 357// PutString schedules a string for output. 358func (pb *paramsBuffer) PutString(s string) { 359 pb.out.WriteString(s) 360} 361 362var pb = ¶msBuffer{} 363 364// TParm takes a terminfo parameterized string, such as setaf or cup, and 365// evaluates the string, and returns the result with the parameter 366// applied. 367func (t *Terminfo) TParm(s string, p ...int) string { 368 var stk stack 369 var a, b string 370 var ai, bi int 371 var ab bool 372 var dvars [26]string 373 var params [9]int 374 375 pb.Start(s) 376 377 // make sure we always have 9 parameters -- makes it easier 378 // later to skip checks 379 for i := 0; i < len(params) && i < len(p); i++ { 380 params[i] = p[i] 381 } 382 383 nest := 0 384 385 for { 386 387 ch, err := pb.NextCh() 388 if err != nil { 389 break 390 } 391 392 if ch != '%' { 393 pb.PutCh(ch) 394 continue 395 } 396 397 ch, err = pb.NextCh() 398 if err != nil { 399 // XXX Error 400 break 401 } 402 403 switch ch { 404 case '%': // quoted % 405 pb.PutCh(ch) 406 407 case 'i': // increment both parameters (ANSI cup support) 408 params[0]++ 409 params[1]++ 410 411 case 'c', 's': 412 // NB: these, and 'd' below are special cased for 413 // efficiency. They could be handled by the richer 414 // format support below, less efficiently. 415 a, stk = stk.Pop() 416 pb.PutString(a) 417 418 case 'd': 419 ai, stk = stk.PopInt() 420 pb.PutString(strconv.Itoa(ai)) 421 422 case '0', '1', '2', '3', '4', 'x', 'X', 'o', ':': 423 // This is pretty suboptimal, but this is rarely used. 424 // None of the mainstream terminals use any of this, 425 // and it would surprise me if this code is ever 426 // executed outside of test cases. 427 f := "%" 428 if ch == ':' { 429 ch, _ = pb.NextCh() 430 } 431 f += string(ch) 432 for ch == '+' || ch == '-' || ch == '#' || ch == ' ' { 433 ch, _ = pb.NextCh() 434 f += string(ch) 435 } 436 for (ch >= '0' && ch <= '9') || ch == '.' { 437 ch, _ = pb.NextCh() 438 f += string(ch) 439 } 440 switch ch { 441 case 'd', 'x', 'X', 'o': 442 ai, stk = stk.PopInt() 443 pb.PutString(fmt.Sprintf(f, ai)) 444 case 'c', 's': 445 a, stk = stk.Pop() 446 pb.PutString(fmt.Sprintf(f, a)) 447 } 448 449 case 'p': // push parameter 450 ch, _ = pb.NextCh() 451 ai = int(ch - '1') 452 if ai >= 0 && ai < len(params) { 453 stk = stk.PushInt(params[ai]) 454 } else { 455 stk = stk.PushInt(0) 456 } 457 458 case 'P': // pop & store variable 459 ch, _ = pb.NextCh() 460 if ch >= 'A' && ch <= 'Z' { 461 svars[int(ch-'A')], stk = stk.Pop() 462 } else if ch >= 'a' && ch <= 'z' { 463 dvars[int(ch-'a')], stk = stk.Pop() 464 } 465 466 case 'g': // recall & push variable 467 ch, _ = pb.NextCh() 468 if ch >= 'A' && ch <= 'Z' { 469 stk = stk.Push(svars[int(ch-'A')]) 470 } else if ch >= 'a' && ch <= 'z' { 471 stk = stk.Push(dvars[int(ch-'a')]) 472 } 473 474 case '\'': // push(char) 475 ch, _ = pb.NextCh() 476 pb.NextCh() // must be ' but we don't check 477 stk = stk.Push(string(ch)) 478 479 case '{': // push(int) 480 ai = 0 481 ch, _ = pb.NextCh() 482 for ch >= '0' && ch <= '9' { 483 ai *= 10 484 ai += int(ch - '0') 485 ch, _ = pb.NextCh() 486 } 487 // ch must be '}' but no verification 488 stk = stk.PushInt(ai) 489 490 case 'l': // push(strlen(pop)) 491 a, stk = stk.Pop() 492 stk = stk.PushInt(len(a)) 493 494 case '+': 495 bi, stk = stk.PopInt() 496 ai, stk = stk.PopInt() 497 stk = stk.PushInt(ai + bi) 498 499 case '-': 500 bi, stk = stk.PopInt() 501 ai, stk = stk.PopInt() 502 stk = stk.PushInt(ai - bi) 503 504 case '*': 505 bi, stk = stk.PopInt() 506 ai, stk = stk.PopInt() 507 stk = stk.PushInt(ai * bi) 508 509 case '/': 510 bi, stk = stk.PopInt() 511 ai, stk = stk.PopInt() 512 if bi != 0 { 513 stk = stk.PushInt(ai / bi) 514 } else { 515 stk = stk.PushInt(0) 516 } 517 518 case 'm': // push(pop mod pop) 519 bi, stk = stk.PopInt() 520 ai, stk = stk.PopInt() 521 if bi != 0 { 522 stk = stk.PushInt(ai % bi) 523 } else { 524 stk = stk.PushInt(0) 525 } 526 527 case '&': // AND 528 bi, stk = stk.PopInt() 529 ai, stk = stk.PopInt() 530 stk = stk.PushInt(ai & bi) 531 532 case '|': // OR 533 bi, stk = stk.PopInt() 534 ai, stk = stk.PopInt() 535 stk = stk.PushInt(ai | bi) 536 537 case '^': // XOR 538 bi, stk = stk.PopInt() 539 ai, stk = stk.PopInt() 540 stk = stk.PushInt(ai ^ bi) 541 542 case '~': // bit complement 543 ai, stk = stk.PopInt() 544 stk = stk.PushInt(ai ^ -1) 545 546 case '!': // logical NOT 547 ai, stk = stk.PopInt() 548 stk = stk.PushBool(ai != 0) 549 550 case '=': // numeric compare or string compare 551 b, stk = stk.Pop() 552 a, stk = stk.Pop() 553 stk = stk.PushBool(a == b) 554 555 case '>': // greater than, numeric 556 bi, stk = stk.PopInt() 557 ai, stk = stk.PopInt() 558 stk = stk.PushBool(ai > bi) 559 560 case '<': // less than, numeric 561 bi, stk = stk.PopInt() 562 ai, stk = stk.PopInt() 563 stk = stk.PushBool(ai < bi) 564 565 case '?': // start conditional 566 567 case 't': 568 ab, stk = stk.PopBool() 569 if ab { 570 // just keep going 571 break 572 } 573 nest = 0 574 ifloop: 575 // this loop consumes everything until we hit our else, 576 // or the end of the conditional 577 for { 578 ch, err = pb.NextCh() 579 if err != nil { 580 break 581 } 582 if ch != '%' { 583 continue 584 } 585 ch, _ = pb.NextCh() 586 switch ch { 587 case ';': 588 if nest == 0 { 589 break ifloop 590 } 591 nest-- 592 case '?': 593 nest++ 594 case 'e': 595 if nest == 0 { 596 break ifloop 597 } 598 } 599 } 600 601 case 'e': 602 // if we got here, it means we didn't use the else 603 // in the 't' case above, and we should skip until 604 // the end of the conditional 605 nest = 0 606 elloop: 607 for { 608 ch, err = pb.NextCh() 609 if err != nil { 610 break 611 } 612 if ch != '%' { 613 continue 614 } 615 ch, _ = pb.NextCh() 616 switch ch { 617 case ';': 618 if nest == 0 { 619 break elloop 620 } 621 nest-- 622 case '?': 623 nest++ 624 } 625 } 626 627 case ';': // endif 628 629 } 630 } 631 632 return pb.End() 633} 634 635// TPuts emits the string to the writer, but expands inline padding 636// indications (of the form $<[delay]> where [delay] is msec) to 637// a suitable time (unless the terminfo string indicates this isn't needed 638// by specifying npc - no padding). All Terminfo based strings should be 639// emitted using this function. 640func (t *Terminfo) TPuts(w io.Writer, s string) { 641 for { 642 beg := strings.Index(s, "$<") 643 if beg < 0 { 644 // Most strings don't need padding, which is good news! 645 io.WriteString(w, s) 646 return 647 } 648 io.WriteString(w, s[:beg]) 649 s = s[beg+2:] 650 end := strings.Index(s, ">") 651 if end < 0 { 652 // unterminated.. just emit bytes unadulterated 653 io.WriteString(w, "$<"+s) 654 return 655 } 656 val := s[:end] 657 s = s[end+1:] 658 padus := 0 659 unit := time.Millisecond 660 dot := false 661 loop: 662 for i := range val { 663 switch val[i] { 664 case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': 665 padus *= 10 666 padus += int(val[i] - '0') 667 if dot { 668 unit /= 10 669 } 670 case '.': 671 if !dot { 672 dot = true 673 } else { 674 break loop 675 } 676 default: 677 break loop 678 } 679 } 680 681 // Curses historically uses padding to achieve "fine grained" 682 // delays. We have much better clocks these days, and so we 683 // do not rely on padding but simply sleep a bit. 684 if len(t.PadChar) > 0 { 685 time.Sleep(unit * time.Duration(padus)) 686 } 687 } 688} 689 690// TGoto returns a string suitable for addressing the cursor at the given 691// row and column. The origin 0, 0 is in the upper left corner of the screen. 692func (t *Terminfo) TGoto(col, row int) string { 693 return t.TParm(t.SetCursor, row, col) 694} 695 696// TColor returns a string corresponding to the given foreground and background 697// colors. Either fg or bg can be set to -1 to elide. 698func (t *Terminfo) TColor(fi, bi int) string { 699 rv := "" 700 // As a special case, we map bright colors to lower versions if the 701 // color table only holds 8. For the remaining 240 colors, the user 702 // is out of luck. Someday we could create a mapping table, but its 703 // not worth it. 704 if t.Colors == 8 { 705 if fi > 7 && fi < 16 { 706 fi -= 8 707 } 708 if bi > 7 && bi < 16 { 709 bi -= 8 710 } 711 } 712 if t.Colors > fi && fi >= 0 { 713 rv += t.TParm(t.SetFg, fi) 714 } 715 if t.Colors > bi && bi >= 0 { 716 rv += t.TParm(t.SetBg, bi) 717 } 718 return rv 719} 720 721var ( 722 dblock sync.Mutex 723 terminfos = make(map[string]*Terminfo) 724 aliases = make(map[string]string) 725) 726 727// AddTerminfo can be called to register a new Terminfo entry. 728func AddTerminfo(t *Terminfo) { 729 dblock.Lock() 730 terminfos[t.Name] = t 731 for _, x := range t.Aliases { 732 terminfos[x] = t 733 } 734 dblock.Unlock() 735} 736 737// LookupTerminfo attempts to find a definition for the named $TERM. 738func LookupTerminfo(name string) (*Terminfo, error) { 739 if name == "" { 740 // else on windows: index out of bounds 741 // on the name[0] reference below 742 return nil, ErrTermNotFound 743 } 744 745 addtruecolor := false 746 switch os.Getenv("COLORTERM") { 747 case "truecolor", "24bit", "24-bit": 748 addtruecolor = true 749 } 750 dblock.Lock() 751 t := terminfos[name] 752 dblock.Unlock() 753 754 // If the name ends in -truecolor, then fabricate an entry 755 // from the corresponding -256color, -color, or bare terminal. 756 if t != nil && t.TrueColor { 757 addtruecolor = true 758 } else if t == nil && strings.HasSuffix(name, "-truecolor") { 759 760 suffixes := []string{ 761 "-256color", 762 "-88color", 763 "-color", 764 "", 765 } 766 base := name[:len(name)-len("-truecolor")] 767 for _, s := range suffixes { 768 if t, _ = LookupTerminfo(base + s); t != nil { 769 addtruecolor = true 770 break 771 } 772 } 773 } 774 775 if t == nil { 776 return nil, ErrTermNotFound 777 } 778 779 switch os.Getenv("TCELL_TRUECOLOR") { 780 case "": 781 case "disable": 782 addtruecolor = false 783 default: 784 addtruecolor = true 785 } 786 787 // If the user has requested 24-bit color with $COLORTERM, then 788 // amend the value (unless already present). This means we don't 789 // need to have a value present. 790 if addtruecolor && 791 t.SetFgBgRGB == "" && 792 t.SetFgRGB == "" && 793 t.SetBgRGB == "" { 794 795 // Supply vanilla ISO 8613-6:1994 24-bit color sequences. 796 t.SetFgRGB = "\x1b[38;2;%p1%d;%p2%d;%p3%dm" 797 t.SetBgRGB = "\x1b[48;2;%p1%d;%p2%d;%p3%dm" 798 t.SetFgBgRGB = "\x1b[38;2;%p1%d;%p2%d;%p3%d;" + 799 "48;2;%p4%d;%p5%d;%p6%dm" 800 } 801 802 return t, nil 803} 804