summaryrefslogtreecommitdiff
path: root/tui/termfo/termfo.go
diff options
context:
space:
mode:
Diffstat (limited to 'tui/termfo/termfo.go')
-rw-r--r--tui/termfo/termfo.go287
1 files changed, 287 insertions, 0 deletions
diff --git a/tui/termfo/termfo.go b/tui/termfo/termfo.go
new file mode 100644
index 0000000..849af07
--- /dev/null
+++ b/tui/termfo/termfo.go
@@ -0,0 +1,287 @@
+//go:generate zsh term.h.zsh
+
+package termfo
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "unicode/utf8"
+
+ "citrons.xyz/talk/tui/termfo/caps"
+ "citrons.xyz/talk/tui/termfo/keys"
+)
+
+// Terminfo describes the terminfo database for a single terminal.
+type Terminfo struct {
+ Name string // Main name as listed in the terminfo file.
+ Desc string // Some textual description.
+ Aliases []string // Aliases for this terminal.
+ Location string // Where it was loaded from; path or "builtin".
+
+ Bools map[*caps.Cap]struct{} // Boolean capabilities.
+ Numbers map[*caps.Cap]int32 // Number capabilities.
+ Strings map[*caps.Cap]string // String capabilities.
+
+ // Capabilities listed in the "extended" section. The values are in the
+ // Bools, Numbers, and Strings maps.
+ Extended []*caps.Cap
+
+ // The default format uses int16, but the "extended number format" uses
+ // int32. This lists the integer size as 2 or 4.
+ IntSize int
+
+ // List of keys, as sequence → Key mapping. e.g. "\x1b[OP" → KeyF1.
+ //
+ // This contains all key_* capabilities, plus a few generated ones for
+ // modifier keys and such.
+ Keys map[string]keys.Key
+}
+
+// New reads the terminfo for term. If term is an empty string then the value of
+// the TERM environment variable is used.
+//
+// It tries to load a terminfo file according to these rules:
+//
+// 1. Use the path in TERMINFO if it's set and don't search any other
+// locations.
+//
+// 2. Try built-in ones unless set NO_BUILTIN_TERMINFO is set.
+//
+// 3. Try ~/.terminfo/ as the database path.
+//
+// 4. Look in the paths listed in TERMINFO_DIRS.
+//
+// 5. Look in /lib/terminfo/
+//
+// 6. Look in /usr/share/terminfo/
+//
+// These are the same rules as ncurses, except that step 2 was added.
+//
+// TODO: curses allows setting a different path at compile-time; we can use
+// infocmp -D to get this. Probably want to add this as step 7(?)
+func New(term string) (*Terminfo, error) {
+ if term == "" {
+ term = os.Getenv("TERM")
+ if term == "" {
+ return nil, errors.New("terminfo: TERM not set")
+ }
+ }
+ ti, err := loadTerminfo(term)
+ if err != nil {
+ return nil, err
+ }
+
+ // Add all the keys.
+ for o, k := range keys.Keys {
+ seq, ok := ti.Strings[o]
+ if ok {
+ ti.Keys[seq] = k
+ }
+ }
+
+ // From tcell:
+ //
+ // Sadly, xterm handling of keycodes is somewhat erratic. In particular,
+ // different codes are sent depending on application mode is in use or
+ // not, and the entries for many of these are simply absent from terminfo
+ // on many systems. So we insert a number of escape sequences if they are
+ // not already used, in order to have the widest correct usage. Note that
+ // prepareKey will not inject codes if the escape sequence is already
+ // known. We also only do this for terminals that have the application
+ // mode present.
+ if _, ok := ti.Strings[caps.KeypadXmit]; ok {
+ ti.Keys["\x1b[A"] = keys.Up
+ ti.Keys["\x1b[B"] = keys.Down
+ ti.Keys["\x1b[C"] = keys.Right
+ ti.Keys["\x1b[D"] = keys.Left
+ ti.Keys["\x1b[F"] = keys.End
+ ti.Keys["\x1b[H"] = keys.Home
+ ti.Keys["\x1b[3~"] = keys.Delete
+ ti.Keys["\x1b[1~"] = keys.Home
+ ti.Keys["\x1b[4~"] = keys.End
+ ti.Keys["\x1b[5~"] = keys.PageUp
+ ti.Keys["\x1b[6~"] = keys.PageDown
+ // Application mode
+ ti.Keys["\x1bOA"] = keys.Up
+ ti.Keys["\x1bOB"] = keys.Down
+ ti.Keys["\x1bOC"] = keys.Right
+ ti.Keys["\x1bOD"] = keys.Left
+ ti.Keys["\x1bOH"] = keys.Home
+ }
+
+ for seq, k := range ti.Keys {
+ addModifierKeys(ti, seq, k)
+ }
+ return ti, nil
+}
+
+func (ti Terminfo) String() string {
+ return fmt.Sprintf("Terminfo file for %q from %q with %d properties", ti.Name, ti.Location,
+ len(ti.Bools)+len(ti.Numbers)+len(ti.Strings))
+}
+
+// Supports reports if this terminal supports the given capability.
+func (ti Terminfo) Supports(c *caps.Cap) bool {
+ if _, ok := ti.Bools[c]; ok {
+ return true
+ }
+ if _, ok := ti.Numbers[c]; ok {
+ return true
+ }
+ if v := ti.Strings[c]; v != "" {
+ return true
+ }
+
+ return false
+}
+
+// Get a capability.
+func (ti Terminfo) Get(c *caps.Cap, args ...int) string {
+ v, ok := ti.Strings[c]
+ if !ok {
+ return ""
+ }
+ return replaceParams(v, args...)
+}
+
+func (ti Terminfo) Put(w io.Writer, c *caps.Cap, args ...int) {
+ w.Write([]byte(ti.Get(c, args...)))
+}
+
+// Event sent by FindKeys.
+type Event struct {
+ Key keys.Key // Processed key that was pressed.
+ Seq []byte // Unprocessed text; only usedful for debugging really.
+ Err error // Error; only set for read errors.
+}
+
+// FindKeys finds all keys in the given reader (usually stdin) and sends them in
+// the channel.
+//
+// Any read error will send an Event with Err set and it will stop reading keys.
+func (ti Terminfo) FindKeys(fp io.Reader) <-chan Event {
+ var (
+ ch = make(chan Event)
+ pbuf []byte
+ )
+ go func() {
+ for {
+ buf := make([]byte, 32)
+ n, err := fp.Read(buf)
+ if err != nil {
+ ch <- Event{Err: err}
+ break
+ }
+ buf = buf[:n]
+ if pbuf != nil {
+ buf = append(pbuf, buf...)
+ pbuf = nil
+ }
+
+ for {
+ k, n := ti.FindKey(buf)
+ if n == 0 {
+ break
+ }
+
+ // Possible the buffer just ran out in the middle of a multibyte
+ // character, so try again.
+ if k == utf8.RuneError && len(buf) < 4 {
+ pbuf = buf
+ break
+ }
+
+ seq := buf[:n]
+ buf = buf[n:]
+ ch <- Event{Key: k, Seq: seq}
+ }
+ }
+ }()
+ return ch
+}
+
+// Find the first valid keypress in s.
+//
+// Returns the key and number of bytes processed. On errors it will return
+// UnknownSequence and the length of the string.
+func (ti Terminfo) FindKey(b []byte) (keys.Key, int) {
+ // TODO: this only works for ASCII; not entirely sure how non-ASCII input is
+ // done wrt. Control key etc.
+ // TODO: doesn't deal with characters consisting of multiple codepoints.
+ // Maybe want to add: https://github.com/arp242/termtext
+ //
+ // TODO: on my system <C-Tab> sends \E[Z, which isn't recognized(?)
+ // Also: <C-End>
+ // <S-End> is "<Send>"?
+ if len(b) == 0 {
+ return 0, 0
+ }
+
+ // No escape sequence.
+ if b[0] != 0x1b {
+ return toKey(b)
+ }
+
+ // Single \E
+ if len(b) == 1 {
+ return keys.Escape, 1
+ }
+
+ // Exact match.
+ k, ok := ti.Keys[string(b)]
+ if ok {
+ return k, len(b)
+ }
+
+ // Find first matching.
+ for seq, k := range ti.Keys {
+ if bytes.HasPrefix(b, []byte(seq)) {
+ return k, len(seq)
+ }
+ }
+
+ // Alt keys are sent as \Ek.
+ // TODO: I think this depends on the "mode"? Xterm has settings for it anyway.
+ if len(b) == 2 {
+ k, _ := toKey(b[1:])
+ return k | keys.Alt, 2
+ }
+ return keys.UnknownSequence, len(b)
+}
+
+func toKey(b []byte) (keys.Key, int) {
+ // TODO: we probably want to use rivo/uniseg here; otherwise something like:
+ //
+ // U+1F3F4 (🏴) U+200D U+2620 (☠️)
+ //
+ // will be sent as three characters, rather than one. It should be the
+ // "pirate flag" emoji.
+ //
+ // Actually, this kinda sucks because this is where my clever "encode
+ // everything in a uint64!"-scheme kind of breaks down, since this can't be
+ // represented by that.
+ //
+ // Perhaps change the return signature to (Key, string, int), and then send
+ // a special MultiCodepoint as the Key? I don't know...
+ //
+ // Or maybe just don't support it. Many applications will work just fine
+ // anyway; e.g. if you print text it will just output those three bytes and
+ // it's all grand, and modifiers aren't sent with that in the first place.
+ r, n := utf8.DecodeRune(b)
+ switch {
+ case r == 0x7f:
+ return keys.Backspace, n
+ case r == 0x0d:
+ return keys.Enter, n
+ case r < 0x1f:
+ return keys.Key(r) | 0x20 | 0x40 | keys.Ctrl, n
+ case r >= 'A' && r <= 'Z':
+ return keys.Key(r) ^ 0x20 | keys.Shift, n
+ default:
+ return keys.Key(r), n
+ }
+
+}