// Copyright 2025 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 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 PadChar string // pad Mouse string // kmous AltChars string // acsc EnterAcs string // smacs ExitAcs string // rmacs EnableAcs string // enacs // 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 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 DisableAutoMargin string // smam EnableAutoMargin string // rmam XTermLike bool // (XT) has XTerm extensions } type stack []any func (st stack) Push(v any) 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 ...any) string { var stk stack var a string var ai, bi int var dvars [26]string var params [9]any 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 } func TerminfoNames() []string { res := make([]string, 0, len(terminfos)) for m := range terminfos { res = append(res, m) } return res }