| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765 |
- // Copyright 2022 The TCell Authors
- //
- // Licensed under the Apache License, Version 2.0 (the "License");
- // you may not use file except in compliance with the License.
- // You may obtain a copy of the license at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS IS" BASIS,
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- // See the License for the specific language governing permissions and
- // limitations under the License.
- package terminfo
- import (
- "bytes"
- "errors"
- "fmt"
- "io"
- "os"
- "strconv"
- "strings"
- "sync"
- "time"
- )
- var (
- // ErrTermNotFound indicates that a suitable terminal entry could
- // not be found. This can result from either not having TERM set,
- // or from the TERM failing to support certain minimal functionality,
- // in particular absolute cursor addressability (the cup capability)
- // is required. For example, legacy "adm3" lacks this capability,
- // whereas the slightly newer "adm3a" supports it. This failure
- // occurs most often with "dumb".
- ErrTermNotFound = errors.New("terminal entry not found")
- )
- // Terminfo represents a terminfo entry. Note that we use friendly names
- // in Go, but when we write out JSON, we use the same names as terminfo.
- // The name, aliases and smous, rmous fields do not come from terminfo directly.
- type Terminfo struct {
- Name string
- Aliases []string
- Columns int // cols
- Lines int // lines
- Colors int // colors
- Bell string // bell
- Clear string // clear
- EnterCA string // smcup
- ExitCA string // rmcup
- ShowCursor string // cnorm
- HideCursor string // civis
- AttrOff string // sgr0
- Underline string // smul
- Bold string // bold
- Blink string // blink
- Reverse string // rev
- Dim string // dim
- Italic string // sitm
- EnterKeypad string // smkx
- ExitKeypad string // rmkx
- SetFg string // setaf
- SetBg string // setab
- ResetFgBg string // op
- SetCursor string // cup
- CursorBack1 string // cub1
- CursorUp1 string // cuu1
- PadChar string // pad
- KeyBackspace string // kbs
- KeyF1 string // kf1
- KeyF2 string // kf2
- KeyF3 string // kf3
- KeyF4 string // kf4
- KeyF5 string // kf5
- KeyF6 string // kf6
- KeyF7 string // kf7
- KeyF8 string // kf8
- KeyF9 string // kf9
- KeyF10 string // kf10
- KeyF11 string // kf11
- KeyF12 string // kf12
- KeyF13 string // kf13
- KeyF14 string // kf14
- KeyF15 string // kf15
- KeyF16 string // kf16
- KeyF17 string // kf17
- KeyF18 string // kf18
- KeyF19 string // kf19
- KeyF20 string // kf20
- KeyF21 string // kf21
- KeyF22 string // kf22
- KeyF23 string // kf23
- KeyF24 string // kf24
- KeyF25 string // kf25
- KeyF26 string // kf26
- KeyF27 string // kf27
- KeyF28 string // kf28
- KeyF29 string // kf29
- KeyF30 string // kf30
- KeyF31 string // kf31
- KeyF32 string // kf32
- KeyF33 string // kf33
- KeyF34 string // kf34
- KeyF35 string // kf35
- KeyF36 string // kf36
- KeyF37 string // kf37
- KeyF38 string // kf38
- KeyF39 string // kf39
- KeyF40 string // kf40
- KeyF41 string // kf41
- KeyF42 string // kf42
- KeyF43 string // kf43
- KeyF44 string // kf44
- KeyF45 string // kf45
- KeyF46 string // kf46
- KeyF47 string // kf47
- KeyF48 string // kf48
- KeyF49 string // kf49
- KeyF50 string // kf50
- KeyF51 string // kf51
- KeyF52 string // kf52
- KeyF53 string // kf53
- KeyF54 string // kf54
- KeyF55 string // kf55
- KeyF56 string // kf56
- KeyF57 string // kf57
- KeyF58 string // kf58
- KeyF59 string // kf59
- KeyF60 string // kf60
- KeyF61 string // kf61
- KeyF62 string // kf62
- KeyF63 string // kf63
- KeyF64 string // kf64
- KeyInsert string // kich1
- KeyDelete string // kdch1
- KeyHome string // khome
- KeyEnd string // kend
- KeyHelp string // khlp
- KeyPgUp string // kpp
- KeyPgDn string // knp
- KeyUp string // kcuu1
- KeyDown string // kcud1
- KeyLeft string // kcub1
- KeyRight string // kcuf1
- KeyBacktab string // kcbt
- KeyExit string // kext
- KeyClear string // kclr
- KeyPrint string // kprt
- KeyCancel string // kcan
- Mouse string // kmous
- AltChars string // acsc
- EnterAcs string // smacs
- ExitAcs string // rmacs
- EnableAcs string // enacs
- KeyShfRight string // kRIT
- KeyShfLeft string // kLFT
- KeyShfHome string // kHOM
- KeyShfEnd string // kEND
- KeyShfInsert string // kIC
- KeyShfDelete string // kDC
- // These are non-standard extensions to terminfo. This includes
- // true color support, and some additional keys. Its kind of bizarre
- // that shifted variants of left and right exist, but not up and down.
- // Terminal support for these are going to vary amongst XTerm
- // emulations, so don't depend too much on them in your application.
- StrikeThrough string // smxx
- SetFgBg string // setfgbg
- SetFgBgRGB string // setfgbgrgb
- SetFgRGB string // setfrgb
- SetBgRGB string // setbrgb
- KeyShfUp string // shift-up
- KeyShfDown string // shift-down
- KeyShfPgUp string // shift-kpp
- KeyShfPgDn string // shift-knp
- KeyCtrlUp string // ctrl-up
- KeyCtrlDown string // ctrl-left
- KeyCtrlRight string // ctrl-right
- KeyCtrlLeft string // ctrl-left
- KeyMetaUp string // meta-up
- KeyMetaDown string // meta-left
- KeyMetaRight string // meta-right
- KeyMetaLeft string // meta-left
- KeyAltUp string // alt-up
- KeyAltDown string // alt-left
- KeyAltRight string // alt-right
- KeyAltLeft string // alt-left
- KeyCtrlHome string
- KeyCtrlEnd string
- KeyMetaHome string
- KeyMetaEnd string
- KeyAltHome string
- KeyAltEnd string
- KeyAltShfUp string
- KeyAltShfDown string
- KeyAltShfLeft string
- KeyAltShfRight string
- KeyMetaShfUp string
- KeyMetaShfDown string
- KeyMetaShfLeft string
- KeyMetaShfRight string
- KeyCtrlShfUp string
- KeyCtrlShfDown string
- KeyCtrlShfLeft string
- KeyCtrlShfRight string
- KeyCtrlShfHome string
- KeyCtrlShfEnd string
- KeyAltShfHome string
- KeyAltShfEnd string
- KeyMetaShfHome string
- KeyMetaShfEnd string
- EnablePaste string // bracketed paste mode
- DisablePaste string
- PasteStart string
- PasteEnd string
- Modifiers int
- InsertChar string // string to insert a character (ich1)
- AutoMargin bool // true if writing to last cell in line advances
- TrueColor bool // true if the terminal supports direct color
- CursorDefault string
- CursorBlinkingBlock string
- CursorSteadyBlock string
- CursorBlinkingUnderline string
- CursorSteadyUnderline string
- CursorBlinkingBar string
- CursorSteadyBar string
- EnterUrl string
- ExitUrl string
- SetWindowSize string
- }
- const (
- ModifiersNone = 0
- ModifiersXTerm = 1
- )
- type stack []interface{}
- func (st stack) Push(v interface{}) stack {
- if b, ok := v.(bool); ok {
- if b {
- return append(st, 1)
- } else {
- return append(st, 0)
- }
- }
- return append(st, v)
- }
- func (st stack) PopString() (string, stack) {
- if len(st) > 0 {
- e := st[len(st)-1]
- var s string
- switch v := e.(type) {
- case int:
- s = strconv.Itoa(v)
- case string:
- s = v
- }
- return s, st[:len(st)-1]
- }
- return "", st
- }
- func (st stack) PopInt() (int, stack) {
- if len(st) > 0 {
- e := st[len(st)-1]
- var i int
- switch v := e.(type) {
- case int:
- i = v
- case string:
- i, _ = strconv.Atoi(v)
- }
- return i, st[:len(st)-1]
- }
- return 0, st
- }
- // static vars
- var svars [26]string
- type paramsBuffer struct {
- out bytes.Buffer
- buf bytes.Buffer
- }
- // Start initializes the params buffer with the initial string data.
- // It also locks the paramsBuffer. The caller must call End() when
- // finished.
- func (pb *paramsBuffer) Start(s string) {
- pb.out.Reset()
- pb.buf.Reset()
- pb.buf.WriteString(s)
- }
- // End returns the final output from TParam, but it also releases the lock.
- func (pb *paramsBuffer) End() string {
- s := pb.out.String()
- return s
- }
- // NextCh returns the next input character to the expander.
- func (pb *paramsBuffer) NextCh() (byte, error) {
- return pb.buf.ReadByte()
- }
- // PutCh "emits" (rather schedules for output) a single byte character.
- func (pb *paramsBuffer) PutCh(ch byte) {
- pb.out.WriteByte(ch)
- }
- // PutString schedules a string for output.
- func (pb *paramsBuffer) PutString(s string) {
- pb.out.WriteString(s)
- }
- // TParm takes a terminfo parameterized string, such as setaf or cup, and
- // evaluates the string, and returns the result with the parameter
- // applied.
- func (t *Terminfo) TParm(s string, p ...interface{}) string {
- var stk stack
- var a string
- var ai, bi int
- var dvars [26]string
- var params [9]interface{}
- var pb = ¶msBuffer{}
- pb.Start(s)
- // make sure we always have 9 parameters -- makes it easier
- // later to skip checks
- for i := 0; i < len(params) && i < len(p); i++ {
- params[i] = p[i]
- }
- const (
- emit = iota
- toEnd
- toElse
- )
- skip := emit
- for {
- ch, err := pb.NextCh()
- if err != nil {
- break
- }
- if ch != '%' {
- if skip == emit {
- pb.PutCh(ch)
- }
- continue
- }
- ch, err = pb.NextCh()
- if err != nil {
- // XXX Error
- break
- }
- if skip == toEnd {
- if ch == ';' {
- skip = emit
- }
- continue
- } else if skip == toElse {
- if ch == 'e' || ch == ';' {
- skip = emit
- }
- continue
- }
- switch ch {
- case '%': // quoted %
- pb.PutCh(ch)
- case 'i': // increment both parameters (ANSI cup support)
- if i, ok := params[0].(int); ok {
- params[0] = i + 1
- }
- if i, ok := params[1].(int); ok {
- params[1] = i + 1
- }
- case 's':
- // NB: 's', 'c', and 'd' below are special cased for
- // efficiency. They could be handled by the richer
- // format support below, less efficiently.
- a, stk = stk.PopString()
- pb.PutString(a)
- case 'c':
- // Integer as special character.
- ai, stk = stk.PopInt()
- pb.PutCh(byte(ai))
- case 'd':
- ai, stk = stk.PopInt()
- pb.PutString(strconv.Itoa(ai))
- case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'x', 'X', 'o', ':':
- // This is pretty suboptimal, but this is rarely used.
- // None of the mainstream terminals use any of this,
- // and it would surprise me if this code is ever
- // executed outside test cases.
- f := "%"
- if ch == ':' {
- ch, _ = pb.NextCh()
- }
- f += string(ch)
- for ch == '+' || ch == '-' || ch == '#' || ch == ' ' {
- ch, _ = pb.NextCh()
- f += string(ch)
- }
- for (ch >= '0' && ch <= '9') || ch == '.' {
- ch, _ = pb.NextCh()
- f += string(ch)
- }
- switch ch {
- case 'd', 'x', 'X', 'o':
- ai, stk = stk.PopInt()
- pb.PutString(fmt.Sprintf(f, ai))
- case 's':
- a, stk = stk.PopString()
- pb.PutString(fmt.Sprintf(f, a))
- case 'c':
- ai, stk = stk.PopInt()
- pb.PutString(fmt.Sprintf(f, ai))
- }
- case 'p': // push parameter
- ch, _ = pb.NextCh()
- ai = int(ch - '1')
- if ai >= 0 && ai < len(params) {
- stk = stk.Push(params[ai])
- } else {
- stk = stk.Push(0)
- }
- case 'P': // pop & store variable
- ch, _ = pb.NextCh()
- if ch >= 'A' && ch <= 'Z' {
- svars[int(ch-'A')], stk = stk.PopString()
- } else if ch >= 'a' && ch <= 'z' {
- dvars[int(ch-'a')], stk = stk.PopString()
- }
- case 'g': // recall & push variable
- ch, _ = pb.NextCh()
- if ch >= 'A' && ch <= 'Z' {
- stk = stk.Push(svars[int(ch-'A')])
- } else if ch >= 'a' && ch <= 'z' {
- stk = stk.Push(dvars[int(ch-'a')])
- }
- case '\'': // push(char) - the integer value of it
- ch, _ = pb.NextCh()
- _, _ = pb.NextCh() // must be ' but we don't check
- stk = stk.Push(int(ch))
- case '{': // push(int)
- ai = 0
- ch, _ = pb.NextCh()
- for ch >= '0' && ch <= '9' {
- ai *= 10
- ai += int(ch - '0')
- ch, _ = pb.NextCh()
- }
- // ch must be '}' but no verification
- stk = stk.Push(ai)
- case 'l': // push(strlen(pop))
- a, stk = stk.PopString()
- stk = stk.Push(len(a))
- case '+':
- bi, stk = stk.PopInt()
- ai, stk = stk.PopInt()
- stk = stk.Push(ai + bi)
- case '-':
- bi, stk = stk.PopInt()
- ai, stk = stk.PopInt()
- stk = stk.Push(ai - bi)
- case '*':
- bi, stk = stk.PopInt()
- ai, stk = stk.PopInt()
- stk = stk.Push(ai * bi)
- case '/':
- bi, stk = stk.PopInt()
- ai, stk = stk.PopInt()
- if bi != 0 {
- stk = stk.Push(ai / bi)
- } else {
- stk = stk.Push(0)
- }
- case 'm': // push(pop mod pop)
- bi, stk = stk.PopInt()
- ai, stk = stk.PopInt()
- if bi != 0 {
- stk = stk.Push(ai % bi)
- } else {
- stk = stk.Push(0)
- }
- case '&': // AND
- bi, stk = stk.PopInt()
- ai, stk = stk.PopInt()
- stk = stk.Push(ai & bi)
- case '|': // OR
- bi, stk = stk.PopInt()
- ai, stk = stk.PopInt()
- stk = stk.Push(ai | bi)
- case '^': // XOR
- bi, stk = stk.PopInt()
- ai, stk = stk.PopInt()
- stk = stk.Push(ai ^ bi)
- case '~': // bit complement
- ai, stk = stk.PopInt()
- stk = stk.Push(ai ^ -1)
- case '!': // logical NOT
- ai, stk = stk.PopInt()
- stk = stk.Push(ai == 0)
- case '=': // numeric compare
- bi, stk = stk.PopInt()
- ai, stk = stk.PopInt()
- stk = stk.Push(ai == bi)
- case '>': // greater than, numeric
- bi, stk = stk.PopInt()
- ai, stk = stk.PopInt()
- stk = stk.Push(ai > bi)
- case '<': // less than, numeric
- bi, stk = stk.PopInt()
- ai, stk = stk.PopInt()
- stk = stk.Push(ai < bi)
- case '?': // start conditional
- case ';':
- skip = emit
- case 't':
- ai, stk = stk.PopInt()
- if ai == 0 {
- skip = toElse
- }
- case 'e':
- skip = toEnd
- default:
- pb.PutString("%" + string(ch))
- }
- }
- return pb.End()
- }
- // TPuts emits the string to the writer, but expands inline padding
- // indications (of the form $<[delay]> where [delay] is msec) to
- // a suitable time (unless the terminfo string indicates this isn't needed
- // by specifying npc - no padding). All Terminfo based strings should be
- // emitted using this function.
- func (t *Terminfo) TPuts(w io.Writer, s string) {
- for {
- beg := strings.Index(s, "$<")
- if beg < 0 {
- // Most strings don't need padding, which is good news!
- _, _ = io.WriteString(w, s)
- return
- }
- _, _ = io.WriteString(w, s[:beg])
- s = s[beg+2:]
- end := strings.Index(s, ">")
- if end < 0 {
- // unterminated.. just emit bytes unadulterated
- _, _ = io.WriteString(w, "$<"+s)
- return
- }
- val := s[:end]
- s = s[end+1:]
- padus := 0
- unit := time.Millisecond
- dot := false
- loop:
- for i := range val {
- switch val[i] {
- case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
- padus *= 10
- padus += int(val[i] - '0')
- if dot {
- unit /= 10
- }
- case '.':
- if !dot {
- dot = true
- } else {
- break loop
- }
- default:
- break loop
- }
- }
- // Curses historically uses padding to achieve "fine grained"
- // delays. We have much better clocks these days, and so we
- // do not rely on padding but simply sleep a bit.
- if len(t.PadChar) > 0 {
- time.Sleep(unit * time.Duration(padus))
- }
- }
- }
- // TGoto returns a string suitable for addressing the cursor at the given
- // row and column. The origin 0, 0 is in the upper left corner of the screen.
- func (t *Terminfo) TGoto(col, row int) string {
- return t.TParm(t.SetCursor, row, col)
- }
- // TColor returns a string corresponding to the given foreground and background
- // colors. Either fg or bg can be set to -1 to elide.
- func (t *Terminfo) TColor(fi, bi int) string {
- rv := ""
- // As a special case, we map bright colors to lower versions if the
- // color table only holds 8. For the remaining 240 colors, the user
- // is out of luck. Someday we could create a mapping table, but its
- // not worth it.
- if t.Colors == 8 {
- if fi > 7 && fi < 16 {
- fi -= 8
- }
- if bi > 7 && bi < 16 {
- bi -= 8
- }
- }
- if t.Colors > fi && fi >= 0 {
- rv += t.TParm(t.SetFg, fi)
- }
- if t.Colors > bi && bi >= 0 {
- rv += t.TParm(t.SetBg, bi)
- }
- return rv
- }
- var (
- dblock sync.Mutex
- terminfos = make(map[string]*Terminfo)
- )
- // AddTerminfo can be called to register a new Terminfo entry.
- func AddTerminfo(t *Terminfo) {
- dblock.Lock()
- terminfos[t.Name] = t
- for _, x := range t.Aliases {
- terminfos[x] = t
- }
- dblock.Unlock()
- }
- // LookupTerminfo attempts to find a definition for the named $TERM.
- func LookupTerminfo(name string) (*Terminfo, error) {
- if name == "" {
- // else on windows: index out of bounds
- // on the name[0] reference below
- return nil, ErrTermNotFound
- }
- addtruecolor := false
- add256color := false
- switch os.Getenv("COLORTERM") {
- case "truecolor", "24bit", "24-bit":
- addtruecolor = true
- }
- dblock.Lock()
- t := terminfos[name]
- dblock.Unlock()
- // If the name ends in -truecolor, then fabricate an entry
- // from the corresponding -256color, -color, or bare terminal.
- if t != nil && t.TrueColor {
- addtruecolor = true
- } else if t == nil && strings.HasSuffix(name, "-truecolor") {
- suffixes := []string{
- "-256color",
- "-88color",
- "-color",
- "",
- }
- base := name[:len(name)-len("-truecolor")]
- for _, s := range suffixes {
- if t, _ = LookupTerminfo(base + s); t != nil {
- addtruecolor = true
- break
- }
- }
- }
- // If the name ends in -256color, maybe fabricate using the xterm 256 color sequences
- if t == nil && strings.HasSuffix(name, "-256color") {
- suffixes := []string{
- "-88color",
- "-color",
- }
- base := name[:len(name)-len("-256color")]
- for _, s := range suffixes {
- if t, _ = LookupTerminfo(base + s); t != nil {
- add256color = true
- break
- }
- }
- }
- if t == nil {
- return nil, ErrTermNotFound
- }
- switch os.Getenv("TCELL_TRUECOLOR") {
- case "":
- case "disable":
- addtruecolor = false
- default:
- addtruecolor = true
- }
- // If the user has requested 24-bit color with $COLORTERM, then
- // amend the value (unless already present). This means we don't
- // need to have a value present.
- if addtruecolor &&
- t.SetFgBgRGB == "" &&
- t.SetFgRGB == "" &&
- t.SetBgRGB == "" {
- // Supply vanilla ISO 8613-6:1994 24-bit color sequences.
- t.SetFgRGB = "\x1b[38;2;%p1%d;%p2%d;%p3%dm"
- t.SetBgRGB = "\x1b[48;2;%p1%d;%p2%d;%p3%dm"
- t.SetFgBgRGB = "\x1b[38;2;%p1%d;%p2%d;%p3%d;" +
- "48;2;%p4%d;%p5%d;%p6%dm"
- }
- if add256color {
- t.Colors = 256
- t.SetFg = "\x1b[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m"
- t.SetBg = "\x1b[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m"
- 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"
- t.ResetFgBg = "\x1b[39;49m"
- }
- return t, nil
- }
|