// Copyright 2024 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 CursorColor string // nothing uses it yet CursorColorRGB string // Cs (but not really because Cs uses X11 color string) CursorColorReset string // Cr EnterUrl string ExitUrl string SetWindowSize string SetWindowTitle string // no terminfo extension EnableFocusReporting string DisableFocusReporting string DisableAutoMargin string // smam EnableAutoMargin string // rmam DoubleUnderline string // Smulx with param 2 CurlyUnderline string // Smulx with param 3 DottedUnderline string // Smulx with param 4 DashedUnderline string // Smulx with param 5 UnderlineColor string // Setuc1 UnderlineColorRGB string // Setulc UnderlineColorReset string // ol XTermLike bool // (XT) has XTerm extensions } 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 }