summaryrefslogtreecommitdiff
path: root/tui/termfo/param.go
diff options
context:
space:
mode:
authorraven <citrons@mondecitronne.com>2026-02-20 13:42:12 -0600
committerraven <citrons@mondecitronne.com>2026-02-20 13:46:59 -0600
commit5b6196ebe67cf954bae8212c1a33b869da723e11 (patch)
treedce33c06621847c3862e64bda914b1e8a450317d /tui/termfo/param.go
parent05c068749740f9430d1fda7698c433697eef1652 (diff)
support builtin terminfo
copy termfo into the repository and modify it to embed an xterm terminfo to as a fallback
Diffstat (limited to 'tui/termfo/param.go')
-rw-r--r--tui/termfo/param.go506
1 files changed, 506 insertions, 0 deletions
diff --git a/tui/termfo/param.go b/tui/termfo/param.go
new file mode 100644
index 0000000..dff556c
--- /dev/null
+++ b/tui/termfo/param.go
@@ -0,0 +1,506 @@
+package termfo
+
+import (
+ "fmt"
+ "reflect"
+ "runtime"
+ "strconv"
+ "strings"
+)
+
+// TODO: we can actually simplify a lot of this. This uses a two-stage "lex +
+// parse", but that's actually not really needed with this stack-based
+// mini-language since we don't really need to know all that much about syntax,
+// so we can just skip the entire lexing.
+//
+// Kind of obvious really but I didn't realize this until I implemented it.
+// Sometimes you need to implement something at least once to fully understand
+// it.
+//
+// Now I can't really be bothered to replace it. I didn't really *want* to write
+// a terminfo implementation, it was just needed for an application. Maybe
+// later.
+//
+// It also doesn't handle the named "dynamic" and "static" variables for now.
+// None of the common terminfos seem to use it so we can probably get away with
+// it. These are annoying as they persist across escape-string evaluations.
+// Something of a misfeature IMO.
+
+type item struct {
+ typ itemType
+ val string
+ pos int
+}
+
+type lexer struct {
+ input string
+ start, pos int
+ items chan item
+ state stateFn
+}
+
+func replaceParams(input string, args ...int) string {
+ l := &lexer{input: input, items: make(chan item, 2), state: lexTop}
+ var items []item
+ for {
+ select {
+ case item := <-l.items:
+ if item.typ == itemEOF {
+ return parseParams(items, args...)
+ }
+ items = append(items, item)
+ default:
+ l.state = l.state(l)
+ }
+ }
+}
+
+func printParams(input string, args ...int) string {
+ l := &lexer{input: input, items: make(chan item, 2), state: lexTop}
+ var items []item
+ for {
+ select {
+ case item := <-l.items:
+ if item.typ == itemEOF {
+ pnl := false
+ b := new(strings.Builder)
+ for _, c := range items {
+ if c.typ == itemIf && !pnl {
+ fmt.Fprint(b, "\n ")
+ }
+ pnl = false
+ fmt.Fprintf(b, "%s(%q) ", c.typ, c.val)
+ if c.typ == itemEndif {
+ fmt.Fprint(b, "\n ")
+ pnl = true
+ }
+ }
+ fmt.Fprint(b, "\n\n\n")
+ return b.String()
+ }
+ items = append(items, item)
+ default:
+ l.state = l.state(l)
+ }
+ }
+}
+
+func (l *lexer) peek() byte {
+ if l.pos+1 >= len(l.input) {
+ return 0
+ }
+ return l.input[l.pos+1]
+}
+func (l *lexer) backup() { l.pos-- }
+func (l *lexer) next() byte {
+ if l.pos >= len(l.input) {
+ return 0
+ }
+ l.pos++
+ return l.input[l.pos-1]
+}
+func (l *lexer) until(anyOf ...byte) {
+ for {
+ b := l.next()
+ for _, a := range anyOf {
+ if b == a {
+ return
+ }
+ }
+ }
+}
+
+func (l *lexer) emit(typ itemType) {
+ l.items <- item{typ: typ, pos: l.start, val: l.input[l.start:l.pos]}
+ l.start = l.pos
+}
+
+func lexTop(l *lexer) stateFn {
+ switch b := l.next(); b {
+ default:
+ return lexTop
+ case eof:
+ if l.pos > l.start {
+ l.emit(itemStr)
+ }
+ l.emit(itemEOF)
+ return lexTop
+ case '%':
+ l.backup()
+ if l.pos > l.start {
+ l.emit(itemStr)
+ }
+ l.next()
+ return lexPercent
+ }
+}
+
+// This doesn't do a lot of error checking; it sort-of assumes the terminfo is
+// sane.
+func lexPercent(l *lexer) stateFn {
+ switch b := l.next(); b {
+ case eof:
+ l.emit(itemEOF)
+ case '%':
+ l.emit(itemPercent)
+
+ // %[[:]flags][width[.precision]][doxXs]
+ case 'd', 'o', 'x', 'X', 's', 'c':
+ l.emit(itemPrint)
+ case ':', '1', '2', '3', '4', '5', '6', '7', '8', '9':
+ l.until('d', 'o', 'x', 'X', 's')
+ l.emit(itemPrint)
+ // %p[1-9]
+ case 'p':
+ l.next()
+ l.emit(itemPush)
+ // %P[a-z]
+ // %P[A-Z]
+ case 'P':
+ n := l.next()
+ if n >= 'a' && n <= 'z' {
+ l.emit(itemSetDynamic)
+ } else {
+ l.emit(itemSetStatic)
+ }
+ // %g[a-z]/
+ // %g[A-Z]
+ // The manpage lists this with a / at the end, but that seems a typo. It's
+ // not present in the actual terminfo files.
+ case 'g':
+ n := l.next()
+ if n >= 'a' && n <= 'z' {
+ l.emit(itemGetDynamic)
+ } else {
+ l.emit(itemGetStatic)
+ }
+ // %'c'
+ case '\'':
+ l.next()
+ l.next()
+ l.emit(itemCharConstant)
+ // %{nn}
+ case '{':
+ l.until('}')
+ l.emit(itemIntConstant)
+ // %l push strlen(pop)
+ case 'l':
+ l.emit(itemStrlen)
+ // %+, %-, %*, %/, %m
+ case '+':
+ l.emit(itemAdd)
+ case '-':
+ l.emit(itemSub)
+ case '*':
+ l.emit(itemMult)
+ case '/':
+ l.emit(itemDiv)
+ case 'm':
+ l.emit(itemMod)
+ // %&, %|, %^
+ case '&':
+ l.emit(itemAnd)
+ case '|':
+ l.emit(itemOr)
+ case '^':
+ l.emit(itemXor)
+ // %=, %>, %<
+ case '=':
+ l.emit(itemEq)
+ case '>':
+ l.emit(itemGt)
+ case '<':
+ l.emit(itemLt)
+ // %A, %O
+ case 'A':
+ l.emit(itemLogicalAnd)
+ case 'O':
+ l.emit(itemLogicalOr)
+ // %!, %~
+ case '!':
+ l.emit(itemBang)
+ case '~':
+ l.emit(itemTilde)
+ // %i
+ case 'i':
+ l.emit(itemIncParams)
+ // %? expr %t thenpart %e elsepart %;
+ case '?':
+ l.emit(itemIf)
+ case 't':
+ l.emit(itemThen)
+ case 'e':
+ l.emit(itemElse)
+ case ';':
+ l.emit(itemEndif)
+ }
+ return lexTop
+}
+
+func parseParams(items []item, args ...int) string {
+ params := make([]int, 9)
+ for i := range args {
+ params[i] = args[i]
+ }
+
+ var (
+ stack []int
+ doInc = 0
+ b = new(strings.Builder)
+ )
+
+ push := func(v int) { stack = append(stack, v) }
+ pop := func() int {
+ if len(stack) == 0 {
+ // Increment first two pops even if the stack is empty; a common
+ // "trick" is to use:
+ // \E[%i%d;%dR
+ // to print 0 for non-ANSI terminals and 1 for ANSI.
+ if doInc > 0 {
+ doInc--
+ return 1
+ }
+ return 0
+ }
+ pop := stack[len(stack)-1]
+ stack = stack[:len(stack)-1]
+ return pop
+ }
+ pushBool := func(v bool) {
+ if v {
+ stack = append(stack, 1)
+ } else {
+ stack = append(stack, 0)
+ }
+ }
+
+ cond := false
+ state := ""
+ for _, item := range items {
+ //fmt.Printf("%s -> %t\n", state, cond)
+ if item.typ == itemEndif {
+ state = ""
+ }
+ if state == "then" && !cond {
+ if item.typ == itemElse {
+ state = "else"
+ }
+ continue
+ }
+ if state == "else" && cond {
+ continue
+ }
+
+ switch item.typ {
+ case itemStr:
+ b.WriteString(item.val)
+ case itemPercent:
+ b.WriteByte('%')
+ case itemPush:
+ a, _ := strconv.Atoi(item.val[2:])
+ push(params[a-1])
+ case itemPrint:
+ p := pop()
+ as := item.val[len(item.val)-1]
+ if as == 's' {
+ as = 'v'
+ }
+ fmt.Fprintf(b, "%"+
+ strings.TrimLeft(item.val[1:len(item.val)-1], ":")+
+ string(as), p)
+ // from lib_tparam.c:
+ // Increment the first two parameters -- if they are numbers rather than
+ // strings. As a side effect, assign into the stack; if this is
+ // termcap, then the stack was populated using the termcap hack above
+ // rather than via the terminfo 'p' case.
+ case itemIncParams:
+ doInc = 2
+ params[0]++
+ params[1]++
+ case itemIntConstant:
+ n, _ := strconv.Atoi(item.val[2 : len(item.val)-1])
+ push(n)
+ case itemCharConstant: // %'c'
+ push(int(item.val[2]))
+ case itemStrlen:
+ push(len(strconv.Itoa(pop())))
+ case itemAdd:
+ a, b := pop(), pop()
+ push(b + a)
+ case itemSub:
+ a, b := pop(), pop()
+ push(b - a)
+ case itemMult:
+ a, b := pop(), pop()
+ push(b * a)
+ case itemDiv:
+ a, b := pop(), pop()
+ push(b / a)
+ case itemMod:
+ a, b := pop(), pop()
+ push(b % a)
+ case itemAnd:
+ a, b := pop(), pop()
+ push(b & a)
+ case itemOr:
+ a, b := pop(), pop()
+ push(b | a)
+ case itemXor:
+ a, b := pop(), pop()
+ push(b ^ a)
+ case itemTilde:
+ push(^pop())
+ case itemLogicalAnd:
+ a, b := pop() > 0, pop() > 0
+ pushBool(b && a)
+ case itemLogicalOr:
+ a, b := pop() > 0, pop() > 0
+ pushBool(b || a)
+ case itemEq:
+ a, b := pop(), pop()
+ pushBool(b == a)
+ case itemGt:
+ a, b := pop(), pop()
+ pushBool(b > a)
+ case itemLt:
+ a, b := pop(), pop()
+ pushBool(b < a)
+ case itemBang:
+ pushBool(!(pop() > 0))
+
+ case itemIf, itemEndif:
+ // Handled at start.
+ case itemElse:
+ state = "else"
+ case itemThen:
+ cond = pop() > 0
+ state = "then"
+ }
+ }
+ return b.String()
+}
+
+type stateFn func(l *lexer) stateFn
+
+func (s stateFn) String() string {
+ name := runtime.FuncForPC(reflect.ValueOf(s).Pointer()).Name()
+ if i := strings.LastIndexByte(name, '.'); i > -1 {
+ name = name[i+1:]
+ }
+ if s == nil {
+ name = "<nil>"
+ }
+ return name + "()"
+}
+
+type itemType int
+
+const (
+ itemEOF itemType = iota
+ itemStr
+ itemPrint
+ itemPush
+ itemCharConstant
+ itemIntConstant
+ itemPercent
+ itemIncParams
+ itemAdd
+ itemSub
+ itemMult
+ itemDiv
+ itemMod
+ itemAnd
+ itemOr
+ itemXor
+ itemLogicalAnd
+ itemLogicalOr
+ itemStrlen
+ itemIf
+ itemThen
+ itemElse
+ itemEndif
+ itemEq
+ itemGt
+ itemLt
+ itemBang
+ itemTilde
+
+ // Unhandled:
+ itemGetDynamic
+ itemGetStatic
+ itemSetDynamic
+ itemSetStatic
+)
+
+const eof = 0
+
+func (i itemType) String() string {
+ switch i {
+ default:
+ return "XXX"
+ case itemEOF:
+ return "EOF"
+ case itemStr:
+ return "str"
+ case itemPrint:
+ return "print"
+ case itemPush:
+ return "push"
+ case itemPercent:
+ return "%"
+ case itemSetDynamic:
+ return "setDyn"
+ case itemSetStatic:
+ return "setStat"
+ case itemGetDynamic:
+ return "getDyn"
+ case itemGetStatic:
+ return "getStat"
+ case itemCharConstant:
+ return "charConstant"
+ case itemIntConstant:
+ return "intConstant"
+ case itemStrlen:
+ return "strlen"
+ case itemAdd:
+ return "add"
+ case itemSub:
+ return "sub"
+ case itemMult:
+ return "mult"
+ case itemDiv:
+ return "div"
+ case itemMod:
+ return "mod"
+ case itemAnd:
+ return "and"
+ case itemOr:
+ return "or"
+ case itemXor:
+ return "xor"
+ case itemEq:
+ return "eq"
+ case itemGt:
+ return "gt"
+ case itemLt:
+ return "lt"
+ case itemLogicalAnd:
+ return "logicalAnd"
+ case itemLogicalOr:
+ return "logicalOr"
+ case itemBang:
+ return "bang"
+ case itemTilde:
+ return "tilde"
+ case itemIncParams:
+ return "incParams"
+ case itemIf:
+ return "if"
+ case itemThen:
+ return "then"
+ case itemElse:
+ return "else"
+ case itemEndif:
+ return "endif"
+ }
+}