diff options
| author | raven <citrons@mondecitronne.com> | 2026-02-20 13:42:12 -0600 |
|---|---|---|
| committer | raven <citrons@mondecitronne.com> | 2026-02-20 13:46:59 -0600 |
| commit | 5b6196ebe67cf954bae8212c1a33b869da723e11 (patch) | |
| tree | dce33c06621847c3862e64bda914b1e8a450317d /tui/termfo/param.go | |
| parent | 05c068749740f9430d1fda7698c433697eef1652 (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.go | 506 |
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" + } +} |
