From 5b6196ebe67cf954bae8212c1a33b869da723e11 Mon Sep 17 00:00:00 2001 From: raven Date: Fri, 20 Feb 2026 13:42:12 -0600 Subject: support builtin terminfo copy termfo into the repository and modify it to embed an xterm terminfo to as a fallback --- tui/termfo/keys/key.go | 169 ++++++++++++++++++++++++++++++++++++++++++++ tui/termfo/keys/key_test.go | 31 ++++++++ tui/termfo/keys/keys.go | 120 +++++++++++++++++++++++++++++++ 3 files changed, 320 insertions(+) create mode 100644 tui/termfo/keys/key.go create mode 100644 tui/termfo/keys/key_test.go create mode 100644 tui/termfo/keys/keys.go (limited to 'tui/termfo/keys') diff --git a/tui/termfo/keys/key.go b/tui/termfo/keys/key.go new file mode 100644 index 0000000..a4e454f --- /dev/null +++ b/tui/termfo/keys/key.go @@ -0,0 +1,169 @@ +package keys + +import ( + "fmt" + "strings" + "unicode/utf8" +) + +// Modifiers keys. +const ( + Shift = 1 << 63 + Ctrl = 1 << 62 + Alt = 1 << 61 + + modmask = Shift | Ctrl | Alt +) + +// Common control sequences better known by their name than letter+Ctrl +// combination. +const ( + Tab = 'i' | Ctrl + Escape = '[' | Ctrl +) + +// Key represents a keypress. This is formatted as follows: +// +// - First 32 bits → rune (int32) +// - Next 16 bits → Named key constant. +// - Bits 49-61 → Currently unused. +// - Bit 62 → Alt +// - Bit 63 → Ctrl +// - Bit 64 → Shift +// +// The upshot of this is that you can now use a single value to test for all +// combinations: +// +// switch Key(0x61) { +// case 'a': // 'a' w/o modifiers +// case 'a' | keys.Ctrl: // 'a' with control +// case 'a' | keys.Ctrl | keys.Shift: // 'a' with shift and control +// +// case keys.Up: // Arrow up +// case keys.Up | keys.Ctrl: // Arrow up with control +// } +// +// Which is nicer than using two or three different variables to signal various +// things. +// +// Letters are always in lower-case; use keys.Shift to test for upper case. +// +// Control sequences (0x00-0x1f, 0x1f), are used sent as key + Ctrl. So this: +// +// switch k { +// case 0x09: +// } +// +// Won't work. you need to use: +// +// switch k { +// case 'i' | key.Ctrl: +// } +// +// Or better yet: +// +// ti := termfo.New("") +// +// ... +// +// switch k { +// case ti.Keys[keys.Tab]: +// } +// +// This just makes more sense, because people press "" not "Start of +// heading". +// +// It's better to use the variables from the terminfo, in case it's something +// different. Especially with things like Shift and Ctrl modifiers this can +// differ. +// +// Note that support for multiple modifier keys is flaky across terminals. +type Key uint64 + +// Shift reports if the Shift modifier is set. +func (k Key) Shift() bool { return k&Shift != 0 } + +// Ctrl reports if the Ctrl modifier is set. +func (k Key) Ctrl() bool { return k&Ctrl != 0 } + +// Alt reports if the Alt modifier is set. +func (k Key) Alt() bool { return k&Alt != 0 } + +// WithoutMods returns a copy of k without any modifier keys set. +func (k Key) WithoutMods() Key { return k &^ modmask } + +// Valid reports if this key is valid. +func (k Key) Valid() bool { return k&^modmask <= 1<<31 || k.Named() } + +// Named reports if this is a named key. +func (k Key) Named() bool { + _, ok := keyNames[k&^modmask] + return ok +} + +// Name gets the key name. This doesn't show if any modifiers are set; use +// String() for that. +func (k Key) Name() string { + k &^= modmask + + n, ok := keyNames[k] + if ok { + return n + } + if !k.Valid() { + return fmt.Sprintf("Unknown key: 0x%x", uint64(k)) + } + if rune(k) == utf8.RuneError { + return fmt.Sprintf("Invalid UTF-8: 0x%x", rune(k)) + } + + // TODO: maybe also other spaces like nbsp etc? + switch k { + case ' ': + return "Space" + case '\t': + return "Tab" + case Escape: + return "Esc" + } + return fmt.Sprintf("%c", rune(k)) +} + +func (k Key) String() string { + var ( + hasMod = k.Ctrl() || k.Shift() || k.Alt() + name = k.Name() + named = utf8.RuneCountInString(name) > 1 + b strings.Builder + ) + + b.Grow(8) + + if hasMod || named { + b.WriteRune('<') + } + + if k.Shift() { + b.WriteString("S-") + } + if k.Alt() { + b.WriteString("A-") + } + + switch k { + case Tab: + b.WriteString("Tab") + case Escape: + b.WriteString("Esc") + default: + if k.Ctrl() { + b.WriteString("C-") + } + b.WriteString(k.Name()) + } + + if hasMod || named { + b.WriteRune('>') + } + return b.String() +} diff --git a/tui/termfo/keys/key_test.go b/tui/termfo/keys/key_test.go new file mode 100644 index 0000000..2536ddf --- /dev/null +++ b/tui/termfo/keys/key_test.go @@ -0,0 +1,31 @@ +package keys + +import ( + "testing" +) + +func TestKey(t *testing.T) { + t.Skip() + tests := []struct { + k Key + want string + }{ + {'a', ""}, + {'a' | Shift, ""}, + {'a' | Ctrl | Shift, ""}, + {'a' | Ctrl | Shift | Alt, ""}, + {Tab, ""}, + {Tab | Ctrl, ""}, + {Up, ""}, + {Up | Ctrl, ""}, + } + + for _, tt := range tests { + t.Run("", func(t *testing.T) { + h := tt.k.String() + if h != tt.want { + t.Errorf("\nwant: %s\nhave: %s", tt.want, h) + } + }) + } +} diff --git a/tui/termfo/keys/keys.go b/tui/termfo/keys/keys.go new file mode 100644 index 0000000..fe2424c --- /dev/null +++ b/tui/termfo/keys/keys.go @@ -0,0 +1,120 @@ +// Code generated by term.h.zsh; DO NOT EDIT. + +package keys + +import "citrons.xyz/talk/tui/termfo/caps" + +// CursesVersion is the version of curses this data was generated with, as [implementation]-[version]. +const CursesVersion = `ncurses-6.5.20240511` + +// Keys maps caps.Cap to Key constants +var Keys = map[*caps.Cap]Key{ + caps.TableStrs[55]: Backspace, + caps.TableStrs[59]: Delete, + caps.TableStrs[61]: Down, + caps.TableStrs[66]: F1, + caps.TableStrs[67]: F10, + caps.TableStrs[68]: F2, + caps.TableStrs[69]: F3, + caps.TableStrs[70]: F4, + caps.TableStrs[71]: F5, + caps.TableStrs[72]: F6, + caps.TableStrs[73]: F7, + caps.TableStrs[74]: F8, + caps.TableStrs[75]: F9, + caps.TableStrs[76]: Home, + caps.TableStrs[77]: Insert, + caps.TableStrs[79]: Left, + caps.TableStrs[81]: PageDown, + caps.TableStrs[82]: PageUp, + caps.TableStrs[83]: Right, + caps.TableStrs[87]: Up, + caps.TableStrs[148]: BackTab, + caps.TableStrs[164]: End, + caps.TableStrs[165]: Enter, + caps.TableStrs[191]: ShiftDelete, + caps.TableStrs[194]: ShiftEnd, + caps.TableStrs[199]: ShiftHome, + caps.TableStrs[200]: ShiftInsert, + caps.TableStrs[201]: ShiftLeft, + caps.TableStrs[210]: ShiftRight, + caps.TableStrs[216]: F11, + caps.TableStrs[217]: F12, + caps.TableStrs[355]: Mouse, +} + +// List of all key sequences we know about. This excludes most obscure ones not +// present on modern devices. +const ( + // Special key used to signal errors. + UnknownSequence Key = iota + (1 << 32) + + ShiftLeft + PageUp + Insert + ShiftInsert + Up + Right + F1 + ShiftRight + F2 + ShiftHome + F3 + Delete + PageDown + F4 + F10 + F11 + ShiftDelete + F5 + Down + Mouse + F12 + ShiftEnd + F6 + Enter + Left + F7 + End + F8 + F9 + Backspace + BackTab + Home +) + +// Names of named key constants. +var keyNames = map[Key]string{ + ShiftLeft: `ShiftLeft`, + PageUp: `PageUp`, + Insert: `Insert`, + ShiftInsert: `ShiftInsert`, + Up: `Up`, + Right: `Right`, + F1: `F1`, + ShiftRight: `ShiftRight`, + F2: `F2`, + ShiftHome: `ShiftHome`, + F3: `F3`, + Delete: `Delete`, + PageDown: `PageDown`, + F4: `F4`, + F10: `F10`, + F11: `F11`, + ShiftDelete: `ShiftDelete`, + F5: `F5`, + Down: `Down`, + Mouse: `Mouse`, + F12: `F12`, + ShiftEnd: `ShiftEnd`, + F6: `F6`, + Enter: `Enter`, + Left: `Left`, + F7: `F7`, + End: `End`, + F8: `F8`, + F9: `F9`, + Backspace: `Backspace`, + BackTab: `BackTab`, + Home: `Home`, +} -- cgit v1.2.3