123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432 |
- //go:build !windows && !plan9
- package term
- import (
- "os"
- "time"
- "src.elv.sh/pkg/ui"
- )
- // reader reads terminal escape sequences and decodes them into events.
- type reader struct {
- fr fileReader
- }
- func newReader(f *os.File) *reader {
- fr, err := newFileReader(f)
- if err != nil {
- // TODO(xiaq): Do not panic.
- panic(err)
- }
- return &reader{fr}
- }
- func (rd *reader) ReadEvent() (Event, error) {
- return readEvent(rd.fr)
- }
- func (rd *reader) ReadRawEvent() (Event, error) {
- r, err := readRune(rd.fr, -1)
- return K(r), err
- }
- func (rd *reader) Close() {
- rd.fr.Stop()
- rd.fr.Close()
- }
- // Used by readRune in readOne to signal end of current sequence.
- const runeEndOfSeq rune = -1
- // Timeout for bytes in escape sequences. Modern terminal emulators send escape
- // sequences very fast, so 10ms is more than sufficient. SSH connections on a
- // slow link might be problematic though.
- var keySeqTimeout = 10 * time.Millisecond
- func readEvent(rd byteReaderWithTimeout) (event Event, err error) {
- var r rune
- r, err = readRune(rd, -1)
- if err != nil {
- return
- }
- currentSeq := string(r)
- // Attempts to read a rune within a timeout of keySeqTimeout. It returns
- // runeEndOfSeq if there is any error; the caller should terminate the
- // current sequence when it sees that value.
- readRune :=
- func() rune {
- r, e := readRune(rd, keySeqTimeout)
- if e != nil {
- return runeEndOfSeq
- }
- currentSeq += string(r)
- return r
- }
- badSeq := func(msg string) {
- err = seqError{msg, currentSeq}
- }
- switch r {
- case 0x1b: // ^[ Escape
- r2 := readRune()
- // According to https://unix.stackexchange.com/a/73697, rxvt and derivatives
- // prepend another ESC to a CSI-style or G3-style sequence to signal Alt.
- // If that happens, remember this now; it will be later picked up when parsing
- // those two kinds of sequences.
- //
- // issue #181
- hasTwoLeadingESC := false
- if r2 == 0x1b {
- hasTwoLeadingESC = true
- r2 = readRune()
- }
- if r2 == runeEndOfSeq {
- // TODO(xiaq): Error is swallowed.
- // Nothing follows. Taken as a lone Escape.
- event = KeyEvent{'[', ui.Ctrl}
- break
- }
- switch r2 {
- case '[':
- // A '[' follows. CSI style function key sequence.
- r = readRune()
- if r == runeEndOfSeq {
- event = KeyEvent{'[', ui.Alt}
- return
- }
- nums := make([]int, 0, 2)
- var starter rune
- // Read an optional starter.
- switch r {
- case '<':
- starter = r
- r = readRune()
- case 'M':
- // Mouse event.
- cb := readRune()
- if cb == runeEndOfSeq {
- badSeq("incomplete mouse event")
- return
- }
- cx := readRune()
- if cx == runeEndOfSeq {
- badSeq("incomplete mouse event")
- return
- }
- cy := readRune()
- if cy == runeEndOfSeq {
- badSeq("incomplete mouse event")
- return
- }
- down := true
- button := int(cb & 3)
- if button == 3 {
- down = false
- button = -1
- }
- mod := mouseModify(int(cb))
- event = MouseEvent{
- Pos{int(cy) - 32, int(cx) - 32}, down, button, mod}
- return
- }
- CSISeq:
- for {
- switch {
- case r == ';':
- nums = append(nums, 0)
- case '0' <= r && r <= '9':
- if len(nums) == 0 {
- nums = append(nums, 0)
- }
- cur := len(nums) - 1
- nums[cur] = nums[cur]*10 + int(r-'0')
- case r == runeEndOfSeq:
- // Incomplete CSI.
- badSeq("incomplete CSI")
- return
- default: // Treat as a terminator.
- break CSISeq
- }
- r = readRune()
- }
- if starter == 0 && r == 'R' {
- // Cursor position report.
- if len(nums) != 2 {
- badSeq("bad CPR")
- return
- }
- event = CursorPosition{nums[0], nums[1]}
- } else if starter == '<' && (r == 'm' || r == 'M') {
- // SGR-style mouse event.
- if len(nums) != 3 {
- badSeq("bad SGR mouse event")
- return
- }
- down := r == 'M'
- button := nums[0] & 3
- mod := mouseModify(nums[0])
- event = MouseEvent{Pos{nums[2], nums[1]}, down, button, mod}
- } else if r == '~' && len(nums) == 1 && (nums[0] == 200 || nums[0] == 201) {
- b := nums[0] == 200
- event = PasteSetting(b)
- } else {
- k := parseCSI(nums, r, currentSeq)
- if k == (ui.Key{}) {
- badSeq("bad CSI")
- } else {
- if hasTwoLeadingESC {
- k.Mod |= ui.Alt
- }
- event = KeyEvent(k)
- }
- }
- case 'O':
- // An 'O' follows. G3 style function key sequence: read one rune.
- r = readRune()
- if r == runeEndOfSeq {
- // Nothing follows after 'O'. Taken as Alt-O.
- event = KeyEvent{'O', ui.Alt}
- return
- }
- k, ok := g3Seq[r]
- if ok {
- if hasTwoLeadingESC {
- k.Mod |= ui.Alt
- }
- event = KeyEvent(k)
- } else {
- badSeq("bad G3")
- }
- default:
- // Something other than '[' or 'O' follows. Taken as an
- // Alt-modified key, possibly also modified by Ctrl.
- k := ctrlModify(r2)
- k.Mod |= ui.Alt
- event = KeyEvent(k)
- }
- default:
- event = KeyEvent(ctrlModify(r))
- }
- return
- }
- // Determines whether a rune corresponds to a Ctrl-modified key and returns the
- // ui.Key the rune represents.
- func ctrlModify(r rune) ui.Key {
- switch r {
- // TODO(xiaq): Are the following special cases universal?
- case 0x0:
- return ui.K('`', ui.Ctrl) // ^@
- case 0x1e:
- return ui.K('6', ui.Ctrl) // ^^
- case 0x1f:
- return ui.K('/', ui.Ctrl) // ^_
- case ui.Tab, ui.Enter, ui.Backspace: // ^I ^J ^?
- // Ambiguous Ctrl keys; prefer the non-Ctrl form as they are more likely.
- return ui.K(r)
- default:
- // Regular ui.Ctrl sequences.
- if 0x1 <= r && r <= 0x1d {
- return ui.K(r+0x40, ui.Ctrl)
- }
- }
- return ui.K(r)
- }
- // Tables for key sequences. Comments document which terminal emulators are
- // known to generate which sequences. The terminal emulators tested are
- // categorized into xterm (including actual xterm, libvte-based terminals,
- // Konsole and Terminal.app unless otherwise noted), urxvt, tmux.
- // G3-style key sequences: \eO followed by exactly one character. For instance,
- // \eOP is F1. These are pretty limited in that they cannot be extended to
- // support modifier keys, other than a leading \e for Alt (e.g. \e\eOP is
- // Alt-F1). Terminals that send G3-style key sequences typically switch to
- // sending a CSI-style key sequence when a non-Alt modifier key is pressed.
- var g3Seq = map[rune]ui.Key{
- // xterm, tmux -- only in Vim, depends on termios setting?
- // NOTE(xiaq): According to urxvt's manpage, \eO[ABCD] sequences are used for
- // Ctrl-Shift-modified arrow keys; however, this doesn't seem to be true for
- // urxvt 9.22 packaged by Debian; those keys simply send the same sequence
- // as Ctrl-modified keys (\eO[abcd]).
- 'A': ui.K(ui.Up), 'B': ui.K(ui.Down), 'C': ui.K(ui.Right), 'D': ui.K(ui.Left),
- 'H': ui.K(ui.Home), 'F': ui.K(ui.End), 'M': ui.K(ui.Insert),
- // urxvt
- 'a': ui.K(ui.Up, ui.Ctrl), 'b': ui.K(ui.Down, ui.Ctrl),
- 'c': ui.K(ui.Right, ui.Ctrl), 'd': ui.K(ui.Left, ui.Ctrl),
- // xterm, urxvt, tmux
- 'P': ui.K(ui.F1), 'Q': ui.K(ui.F2), 'R': ui.K(ui.F3), 'S': ui.K(ui.F4),
- }
- // Tables for CSI-style key sequences. A CSI sequence is \e[ followed by zero or
- // more numerical arguments (separated by semicolons), ending in a non-numeric,
- // non-semicolon rune. They are used for many purposes, and CSI-style key
- // sequences are a subset of them.
- //
- // There are several variants of CSI-style key sequences; see comments above the
- // respective tables. In all variants, modifier keys are encoded in numerical
- // arguments; see xtermModify. Note that although the set of possible sequences
- // make it possible to express a very complete set of key combinations, they are
- // not always sent by terminals. For instance, many (if not most) terminals will
- // send the same sequence for Up when Shift-Up is pressed, even if Shift-Up is
- // expressible using the escape sequences described below.
- // CSI-style key sequences identified by the last rune. For instance, \e[A is
- // Up. When modified, two numerical arguments are added, the first always beging
- // 1 and the second identifying the modifier. For instance, \e[1;5A is Ctrl-Up.
- var csiSeqByLast = map[rune]ui.Key{
- // xterm, urxvt, tmux
- 'A': ui.K(ui.Up), 'B': ui.K(ui.Down), 'C': ui.K(ui.Right), 'D': ui.K(ui.Left),
- // urxvt
- 'a': ui.K(ui.Up, ui.Shift), 'b': ui.K(ui.Down, ui.Shift),
- 'c': ui.K(ui.Right, ui.Shift), 'd': ui.K(ui.Left, ui.Shift),
- // xterm (Terminal.app only sends those in alternate screen)
- 'H': ui.K(ui.Home), 'F': ui.K(ui.End),
- // xterm, urxvt, tmux
- 'Z': ui.K(ui.Tab, ui.Shift),
- }
- // CSI-style key sequences ending with '~' with by one or two numerical
- // arguments. The first argument identifies the key, and the optional second
- // argument identifies the modifier. For instance, \e[3~ is Delete, and \e[3;5~
- // is Ctrl-Delete.
- //
- // An alternative encoding of the modifier key, only known to be used by urxvt
- // (or for that matter, likely also rxvt) is to change the last rune: '$' for
- // Shift, '^' for Ctrl, and '@' for Ctrl+Shift. The numeric argument is kept
- // unchanged. For instance, \e[3^ is Ctrl-Delete.
- var csiSeqTilde = map[int]rune{
- // tmux (NOTE: urxvt uses the pair for Find/Select)
- 1: ui.Home, 4: ui.End,
- // xterm (Terminal.app sends ^M for Fn+Enter), urxvt, tmux
- 2: ui.Insert,
- // xterm, urxvt, tmux
- 3: ui.Delete,
- // xterm (Terminal.app only sends those in alternate screen), urxvt, tmux
- // NOTE: called Prior/Next in urxvt manpage
- 5: ui.PageUp, 6: ui.PageDown,
- // urxvt
- 7: ui.Home, 8: ui.End,
- // urxvt
- 11: ui.F1, 12: ui.F2, 13: ui.F3, 14: ui.F4,
- // xterm, urxvt, tmux
- // NOTE: 16 and 22 are unused
- 15: ui.F5, 17: ui.F6, 18: ui.F7, 19: ui.F8,
- 20: ui.F9, 21: ui.F10, 23: ui.F11, 24: ui.F12,
- }
- // CSI-style key sequences ending with '~', with the first argument always 27,
- // the second argument identifying the modifier, and the third argument
- // identifying the key. For instance, \e[27;5;9~ is Ctrl-Tab.
- //
- // NOTE(xiaq): The list is taken blindly from xterm-keys.c in the tmux source
- // tree. I do not have a keyboard-terminal combination that generate such
- // sequences, but assumably they are generated by some terminals for numpad
- // inputs.
- var csiSeqTilde27 = map[int]rune{
- 9: '\t', 13: '\r',
- 33: '!', 35: '#', 39: '\'', 40: '(', 41: ')', 43: '+', 44: ',', 45: '-',
- 46: '.',
- 48: '0', 49: '1', 50: '2', 51: '3', 52: '4', 53: '5', 54: '6', 55: '7',
- 56: '8', 57: '9',
- 58: ':', 59: ';', 60: '<', 61: '=', 62: '>', 63: ';',
- }
- // parseCSI parses a CSI-style key sequence. See comments above for all the 3
- // variants this function handles.
- func parseCSI(nums []int, last rune, seq string) ui.Key {
- if k, ok := csiSeqByLast[last]; ok {
- if len(nums) == 0 {
- // Unmodified: \e[A (Up)
- return k
- } else if len(nums) == 2 && nums[0] == 1 {
- // Modified: \e[1;5A (Ctrl-Up)
- return xtermModify(k, nums[1], seq)
- } else {
- return ui.Key{}
- }
- }
- switch last {
- case '~':
- if len(nums) == 1 || len(nums) == 2 {
- if r, ok := csiSeqTilde[nums[0]]; ok {
- k := ui.K(r)
- if len(nums) == 1 {
- // Unmodified: \e[5~ (e.g. PageUp)
- return k
- }
- // Modified: \e[5;5~ (e.g. Ctrl-PageUp)
- return xtermModify(k, nums[1], seq)
- }
- } else if len(nums) == 3 && nums[0] == 27 {
- if r, ok := csiSeqTilde27[nums[2]]; ok {
- k := ui.K(r)
- return xtermModify(k, nums[1], seq)
- }
- }
- case '$', '^', '@':
- // Modified by urxvt; see comment above csiSeqTilde.
- if len(nums) == 1 {
- if r, ok := csiSeqTilde[nums[0]]; ok {
- var mod ui.Mod
- switch last {
- case '$':
- mod = ui.Shift
- case '^':
- mod = ui.Ctrl
- case '@':
- mod = ui.Shift | ui.Ctrl
- }
- return ui.K(r, mod)
- }
- }
- }
- return ui.Key{}
- }
- func xtermModify(k ui.Key, mod int, seq string) ui.Key {
- if mod < 0 || mod > 16 {
- // Out of range
- return ui.Key{}
- }
- if mod == 0 {
- return k
- }
- modFlags := mod - 1
- if modFlags&0x1 != 0 {
- k.Mod |= ui.Shift
- }
- if modFlags&0x2 != 0 {
- k.Mod |= ui.Alt
- }
- if modFlags&0x4 != 0 {
- k.Mod |= ui.Ctrl
- }
- if modFlags&0x8 != 0 {
- // This should be Meta, but we currently conflate Meta and Alt.
- k.Mod |= ui.Alt
- }
- return k
- }
- func mouseModify(n int) ui.Mod {
- var mod ui.Mod
- if n&4 != 0 {
- mod |= ui.Shift
- }
- if n&8 != 0 {
- mod |= ui.Alt
- }
- if n&16 != 0 {
- mod |= ui.Ctrl
- }
- return mod
- }
|