terminfo.go (20110B)
1 // Copyright 2024 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 15 package terminfo 16 17 import ( 18 "bytes" 19 "errors" 20 "fmt" 21 "io" 22 "os" 23 "strconv" 24 "strings" 25 "sync" 26 "time" 27 ) 28 29 var ( 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. 43 type 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 CursorDefault string 224 CursorBlinkingBlock string 225 CursorSteadyBlock string 226 CursorBlinkingUnderline string 227 CursorSteadyUnderline string 228 CursorBlinkingBar string 229 CursorSteadyBar string 230 CursorColor string // nothing uses it yet 231 CursorColorRGB string // Cs (but not really because Cs uses X11 color string) 232 CursorColorReset string // Cr 233 EnterUrl string 234 ExitUrl string 235 SetWindowSize string 236 SetWindowTitle string // no terminfo extension 237 EnableFocusReporting string 238 DisableFocusReporting string 239 DisableAutoMargin string // smam 240 EnableAutoMargin string // rmam 241 DoubleUnderline string // Smulx with param 2 242 CurlyUnderline string // Smulx with param 3 243 DottedUnderline string // Smulx with param 4 244 DashedUnderline string // Smulx with param 5 245 UnderlineColor string // Setuc1 246 UnderlineColorRGB string // Setulc 247 UnderlineColorReset string // ol 248 XTermLike bool // (XT) has XTerm extensions 249 } 250 251 const ( 252 ModifiersNone = 0 253 ModifiersXTerm = 1 254 ) 255 256 type stack []interface{} 257 258 func (st stack) Push(v interface{}) stack { 259 if b, ok := v.(bool); ok { 260 if b { 261 return append(st, 1) 262 } else { 263 return append(st, 0) 264 } 265 } 266 return append(st, v) 267 } 268 269 func (st stack) PopString() (string, stack) { 270 if len(st) > 0 { 271 e := st[len(st)-1] 272 var s string 273 switch v := e.(type) { 274 case int: 275 s = strconv.Itoa(v) 276 case string: 277 s = v 278 } 279 return s, st[:len(st)-1] 280 } 281 return "", st 282 283 } 284 func (st stack) PopInt() (int, stack) { 285 if len(st) > 0 { 286 e := st[len(st)-1] 287 var i int 288 switch v := e.(type) { 289 case int: 290 i = v 291 case string: 292 i, _ = strconv.Atoi(v) 293 } 294 return i, st[:len(st)-1] 295 } 296 return 0, st 297 } 298 299 // static vars 300 var svars [26]string 301 302 type paramsBuffer struct { 303 out bytes.Buffer 304 buf bytes.Buffer 305 } 306 307 // Start initializes the params buffer with the initial string data. 308 // It also locks the paramsBuffer. The caller must call End() when 309 // finished. 310 func (pb *paramsBuffer) Start(s string) { 311 pb.out.Reset() 312 pb.buf.Reset() 313 pb.buf.WriteString(s) 314 } 315 316 // End returns the final output from TParam, but it also releases the lock. 317 func (pb *paramsBuffer) End() string { 318 s := pb.out.String() 319 return s 320 } 321 322 // NextCh returns the next input character to the expander. 323 func (pb *paramsBuffer) NextCh() (byte, error) { 324 return pb.buf.ReadByte() 325 } 326 327 // PutCh "emits" (rather schedules for output) a single byte character. 328 func (pb *paramsBuffer) PutCh(ch byte) { 329 pb.out.WriteByte(ch) 330 } 331 332 // PutString schedules a string for output. 333 func (pb *paramsBuffer) PutString(s string) { 334 pb.out.WriteString(s) 335 } 336 337 // TParm takes a terminfo parameterized string, such as setaf or cup, and 338 // evaluates the string, and returns the result with the parameter 339 // applied. 340 func (t *Terminfo) TParm(s string, p ...interface{}) string { 341 var stk stack 342 var a string 343 var ai, bi int 344 var dvars [26]string 345 var params [9]interface{} 346 var pb = ¶msBuffer{} 347 348 pb.Start(s) 349 350 // make sure we always have 9 parameters -- makes it easier 351 // later to skip checks 352 for i := 0; i < len(params) && i < len(p); i++ { 353 params[i] = p[i] 354 } 355 356 const ( 357 emit = iota 358 toEnd 359 toElse 360 ) 361 362 skip := emit 363 364 for { 365 366 ch, err := pb.NextCh() 367 if err != nil { 368 break 369 } 370 371 if ch != '%' { 372 if skip == emit { 373 pb.PutCh(ch) 374 } 375 continue 376 } 377 378 ch, err = pb.NextCh() 379 if err != nil { 380 // XXX Error 381 break 382 } 383 if skip == toEnd { 384 if ch == ';' { 385 skip = emit 386 } 387 continue 388 } else if skip == toElse { 389 if ch == 'e' || ch == ';' { 390 skip = emit 391 } 392 continue 393 } 394 395 switch ch { 396 case '%': // quoted % 397 pb.PutCh(ch) 398 399 case 'i': // increment both parameters (ANSI cup support) 400 if i, ok := params[0].(int); ok { 401 params[0] = i + 1 402 } 403 if i, ok := params[1].(int); ok { 404 params[1] = i + 1 405 } 406 407 case 's': 408 // NB: 's', 'c', and 'd' below are special cased for 409 // efficiency. They could be handled by the richer 410 // format support below, less efficiently. 411 a, stk = stk.PopString() 412 pb.PutString(a) 413 414 case 'c': 415 // Integer as special character. 416 ai, stk = stk.PopInt() 417 pb.PutCh(byte(ai)) 418 419 case 'd': 420 ai, stk = stk.PopInt() 421 pb.PutString(strconv.Itoa(ai)) 422 423 case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '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 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 's': 446 a, stk = stk.PopString() 447 pb.PutString(fmt.Sprintf(f, a)) 448 case 'c': 449 ai, stk = stk.PopInt() 450 pb.PutString(fmt.Sprintf(f, ai)) 451 } 452 453 case 'p': // push parameter 454 ch, _ = pb.NextCh() 455 ai = int(ch - '1') 456 if ai >= 0 && ai < len(params) { 457 stk = stk.Push(params[ai]) 458 } else { 459 stk = stk.Push(0) 460 } 461 462 case 'P': // pop & store variable 463 ch, _ = pb.NextCh() 464 if ch >= 'A' && ch <= 'Z' { 465 svars[int(ch-'A')], stk = stk.PopString() 466 } else if ch >= 'a' && ch <= 'z' { 467 dvars[int(ch-'a')], stk = stk.PopString() 468 } 469 470 case 'g': // recall & push variable 471 ch, _ = pb.NextCh() 472 if ch >= 'A' && ch <= 'Z' { 473 stk = stk.Push(svars[int(ch-'A')]) 474 } else if ch >= 'a' && ch <= 'z' { 475 stk = stk.Push(dvars[int(ch-'a')]) 476 } 477 478 case '\'': // push(char) - the integer value of it 479 ch, _ = pb.NextCh() 480 _, _ = pb.NextCh() // must be ' but we don't check 481 stk = stk.Push(int(ch)) 482 483 case '{': // push(int) 484 ai = 0 485 ch, _ = pb.NextCh() 486 for ch >= '0' && ch <= '9' { 487 ai *= 10 488 ai += int(ch - '0') 489 ch, _ = pb.NextCh() 490 } 491 // ch must be '}' but no verification 492 stk = stk.Push(ai) 493 494 case 'l': // push(strlen(pop)) 495 a, stk = stk.PopString() 496 stk = stk.Push(len(a)) 497 498 case '+': 499 bi, stk = stk.PopInt() 500 ai, stk = stk.PopInt() 501 stk = stk.Push(ai + bi) 502 503 case '-': 504 bi, stk = stk.PopInt() 505 ai, stk = stk.PopInt() 506 stk = stk.Push(ai - bi) 507 508 case '*': 509 bi, stk = stk.PopInt() 510 ai, stk = stk.PopInt() 511 stk = stk.Push(ai * bi) 512 513 case '/': 514 bi, stk = stk.PopInt() 515 ai, stk = stk.PopInt() 516 if bi != 0 { 517 stk = stk.Push(ai / bi) 518 } else { 519 stk = stk.Push(0) 520 } 521 522 case 'm': // push(pop mod pop) 523 bi, stk = stk.PopInt() 524 ai, stk = stk.PopInt() 525 if bi != 0 { 526 stk = stk.Push(ai % bi) 527 } else { 528 stk = stk.Push(0) 529 } 530 531 case '&': // AND 532 bi, stk = stk.PopInt() 533 ai, stk = stk.PopInt() 534 stk = stk.Push(ai & bi) 535 536 case '|': // OR 537 bi, stk = stk.PopInt() 538 ai, stk = stk.PopInt() 539 stk = stk.Push(ai | bi) 540 541 case '^': // XOR 542 bi, stk = stk.PopInt() 543 ai, stk = stk.PopInt() 544 stk = stk.Push(ai ^ bi) 545 546 case '~': // bit complement 547 ai, stk = stk.PopInt() 548 stk = stk.Push(ai ^ -1) 549 550 case '!': // logical NOT 551 ai, stk = stk.PopInt() 552 stk = stk.Push(ai == 0) 553 554 case '=': // numeric compare 555 bi, stk = stk.PopInt() 556 ai, stk = stk.PopInt() 557 stk = stk.Push(ai == bi) 558 559 case '>': // greater than, numeric 560 bi, stk = stk.PopInt() 561 ai, stk = stk.PopInt() 562 stk = stk.Push(ai > bi) 563 564 case '<': // less than, numeric 565 bi, stk = stk.PopInt() 566 ai, stk = stk.PopInt() 567 stk = stk.Push(ai < bi) 568 569 case '?': // start conditional 570 571 case ';': 572 skip = emit 573 574 case 't': 575 ai, stk = stk.PopInt() 576 if ai == 0 { 577 skip = toElse 578 } 579 580 case 'e': 581 skip = toEnd 582 583 default: 584 pb.PutString("%" + string(ch)) 585 } 586 } 587 588 return pb.End() 589 } 590 591 // TPuts emits the string to the writer, but expands inline padding 592 // indications (of the form $<[delay]> where [delay] is msec) to 593 // a suitable time (unless the terminfo string indicates this isn't needed 594 // by specifying npc - no padding). All Terminfo based strings should be 595 // emitted using this function. 596 func (t *Terminfo) TPuts(w io.Writer, s string) { 597 for { 598 beg := strings.Index(s, "$<") 599 if beg < 0 { 600 // Most strings don't need padding, which is good news! 601 _, _ = io.WriteString(w, s) 602 return 603 } 604 _, _ = io.WriteString(w, s[:beg]) 605 s = s[beg+2:] 606 end := strings.Index(s, ">") 607 if end < 0 { 608 // unterminated.. just emit bytes unadulterated 609 _, _ = io.WriteString(w, "$<"+s) 610 return 611 } 612 val := s[:end] 613 s = s[end+1:] 614 padus := 0 615 unit := time.Millisecond 616 dot := false 617 loop: 618 for i := range val { 619 switch val[i] { 620 case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': 621 padus *= 10 622 padus += int(val[i] - '0') 623 if dot { 624 unit /= 10 625 } 626 case '.': 627 if !dot { 628 dot = true 629 } else { 630 break loop 631 } 632 default: 633 break loop 634 } 635 } 636 637 // Curses historically uses padding to achieve "fine grained" 638 // delays. We have much better clocks these days, and so we 639 // do not rely on padding but simply sleep a bit. 640 if len(t.PadChar) > 0 { 641 time.Sleep(unit * time.Duration(padus)) 642 } 643 } 644 } 645 646 // TGoto returns a string suitable for addressing the cursor at the given 647 // row and column. The origin 0, 0 is in the upper left corner of the screen. 648 func (t *Terminfo) TGoto(col, row int) string { 649 return t.TParm(t.SetCursor, row, col) 650 } 651 652 // TColor returns a string corresponding to the given foreground and background 653 // colors. Either fg or bg can be set to -1 to elide. 654 func (t *Terminfo) TColor(fi, bi int) string { 655 rv := "" 656 // As a special case, we map bright colors to lower versions if the 657 // color table only holds 8. For the remaining 240 colors, the user 658 // is out of luck. Someday we could create a mapping table, but its 659 // not worth it. 660 if t.Colors == 8 { 661 if fi > 7 && fi < 16 { 662 fi -= 8 663 } 664 if bi > 7 && bi < 16 { 665 bi -= 8 666 } 667 } 668 if t.Colors > fi && fi >= 0 { 669 rv += t.TParm(t.SetFg, fi) 670 } 671 if t.Colors > bi && bi >= 0 { 672 rv += t.TParm(t.SetBg, bi) 673 } 674 return rv 675 } 676 677 var ( 678 dblock sync.Mutex 679 terminfos = make(map[string]*Terminfo) 680 ) 681 682 // AddTerminfo can be called to register a new Terminfo entry. 683 func AddTerminfo(t *Terminfo) { 684 dblock.Lock() 685 terminfos[t.Name] = t 686 for _, x := range t.Aliases { 687 terminfos[x] = t 688 } 689 dblock.Unlock() 690 } 691 692 // LookupTerminfo attempts to find a definition for the named $TERM. 693 func LookupTerminfo(name string) (*Terminfo, error) { 694 if name == "" { 695 // else on windows: index out of bounds 696 // on the name[0] reference below 697 return nil, ErrTermNotFound 698 } 699 700 addtruecolor := false 701 add256color := false 702 switch os.Getenv("COLORTERM") { 703 case "truecolor", "24bit", "24-bit": 704 addtruecolor = true 705 } 706 dblock.Lock() 707 t := terminfos[name] 708 dblock.Unlock() 709 710 // If the name ends in -truecolor, then fabricate an entry 711 // from the corresponding -256color, -color, or bare terminal. 712 if t != nil && t.TrueColor { 713 addtruecolor = true 714 } else if t == nil && strings.HasSuffix(name, "-truecolor") { 715 716 suffixes := []string{ 717 "-256color", 718 "-88color", 719 "-color", 720 "", 721 } 722 base := name[:len(name)-len("-truecolor")] 723 for _, s := range suffixes { 724 if t, _ = LookupTerminfo(base + s); t != nil { 725 addtruecolor = true 726 break 727 } 728 } 729 } 730 731 // If the name ends in -256color, maybe fabricate using the xterm 256 color sequences 732 if t == nil && strings.HasSuffix(name, "-256color") { 733 suffixes := []string{ 734 "-88color", 735 "-color", 736 } 737 base := name[:len(name)-len("-256color")] 738 for _, s := range suffixes { 739 if t, _ = LookupTerminfo(base + s); t != nil { 740 add256color = true 741 break 742 } 743 } 744 } 745 746 if t == nil { 747 return nil, ErrTermNotFound 748 } 749 750 switch os.Getenv("TCELL_TRUECOLOR") { 751 case "": 752 case "disable": 753 addtruecolor = false 754 default: 755 addtruecolor = true 756 } 757 758 // If the user has requested 24-bit color with $COLORTERM, then 759 // amend the value (unless already present). This means we don't 760 // need to have a value present. 761 if addtruecolor && 762 t.SetFgBgRGB == "" && 763 t.SetFgRGB == "" && 764 t.SetBgRGB == "" { 765 766 // Supply vanilla ISO 8613-6:1994 24-bit color sequences. 767 t.SetFgRGB = "\x1b[38;2;%p1%d;%p2%d;%p3%dm" 768 t.SetBgRGB = "\x1b[48;2;%p1%d;%p2%d;%p3%dm" 769 t.SetFgBgRGB = "\x1b[38;2;%p1%d;%p2%d;%p3%d;" + 770 "48;2;%p4%d;%p5%d;%p6%dm" 771 } 772 773 if add256color { 774 t.Colors = 256 775 t.SetFg = "\x1b[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m" 776 t.SetBg = "\x1b[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m" 777 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" 778 t.ResetFgBg = "\x1b[39;49m" 779 } 780 return t, nil 781 }