Files
relspecgo/vendor/github.com/gdamore/tcell/v2/terminfo/terminfo.go
T
2026-05-20 22:52:20 +02:00

629 lines
14 KiB
Go

// 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 = &paramsBuffer{}
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
}