diff options
Diffstat (limited to 'tui/termfo/termfo.go')
| -rw-r--r-- | tui/termfo/termfo.go | 287 |
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 + } + +} |
