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