mirror of
https://github.com/taigrr/bubbletea.git
synced 2026-04-02 02:59:09 -07:00
There are drawbacks to both sides of this, but in the end, it seems to make more sense to treat spaces as regular runes.
279 lines
6.9 KiB
Go
279 lines
6.9 KiB
Go
package boba
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
// KeyPressMsg contains information about a keypress
|
|
type KeyMsg Key
|
|
|
|
// String returns a friendly name for a key
|
|
func (k *KeyMsg) String() (str string) {
|
|
if k.Alt {
|
|
str += "alt+"
|
|
}
|
|
if k.Type == KeyRune {
|
|
str += string(k.Rune)
|
|
return str
|
|
} else if s, ok := keyNames[int(k.Type)]; ok {
|
|
str += s
|
|
return str
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// IsRune returns weather or not the key is a rune
|
|
func (k *KeyMsg) IsRune() bool {
|
|
return k.Type == KeyRune
|
|
}
|
|
|
|
// Key contains information about a keypress
|
|
type Key struct {
|
|
Type KeyType
|
|
Rune rune
|
|
Alt bool
|
|
}
|
|
|
|
// KeyType indicates the key pressed
|
|
type KeyType int
|
|
|
|
// Control keys. I know we could do this with an iota, but the values are very
|
|
// specific, so we set the values explicitly to avoid any confusion.
|
|
//
|
|
// See also:
|
|
// https://en.wikipedia.org/wiki/C0_and_C1_control_codes
|
|
const (
|
|
keyNUL = 0 // null, \0
|
|
keySOH = 1 // start of heading
|
|
keySTX = 2 // start of text
|
|
keyETX = 3 // break, ctrl+c
|
|
keyEOT = 4 // end of transmission
|
|
keyENQ = 5 // enquiry
|
|
keyACK = 6 // acknowledge
|
|
keyBEL = 7 // bell, \a
|
|
keyBS = 8 // backspace
|
|
keyHT = 9 // horizontal tabulation, \t
|
|
keyLF = 10 // line feed, \n
|
|
keyVT = 11 // vertical tabulation \v
|
|
keyFF = 12 // form feed \f
|
|
keyCR = 13 // carriage return, \r
|
|
keySO = 14 // shift out
|
|
keySI = 15 // shift in
|
|
keyDLE = 16 // data link escape
|
|
keyDC1 = 17 // device control one
|
|
keyDC2 = 18 // device control two
|
|
keyDC3 = 19 // device control three
|
|
keyDC4 = 20 // device control four
|
|
keyNAK = 21 // negative acknowledge
|
|
keySYN = 22 // synchronous idle
|
|
keyETB = 23 // end of transmission block
|
|
keyCAN = 24 // cancel
|
|
keyEM = 25 // end of medium
|
|
keySUB = 26 // substitution
|
|
keyESC = 27 // escape, \e
|
|
keyFS = 28 // file separator
|
|
keyGS = 29 // group separator
|
|
keyRS = 30 // record separator
|
|
keyUS = 31 // unit separator
|
|
keySP = 32 // space
|
|
keyDEL = 127 // delete. on most systems this is mapped to backspace, I hear
|
|
)
|
|
|
|
// Aliases
|
|
const (
|
|
KeyNull = keyNUL
|
|
KeyBreak = keyETX
|
|
KeyEnter = keyCR
|
|
KeyBackspace = keyBS
|
|
KeyTab = keyHT
|
|
KeySpace = keySP
|
|
KeyEsc = keyESC
|
|
KeyEscape = keyESC
|
|
KeyDelete = keyDEL
|
|
|
|
KeyCtrlAt = keyNUL // ctrl+@
|
|
KeyCtrlA = keySOH
|
|
KeyCtrlB = keySTX
|
|
KeyCtrlC = keyETX
|
|
KeyCtrlD = keyEOT
|
|
KeyCtrlE = keyENQ
|
|
KeyCtrlF = keyACK
|
|
KeyCtrlG = keyBEL
|
|
KeyCtrlH = keyBS
|
|
KeyCtrlI = keyHT
|
|
KeyCtrlJ = keyLF
|
|
KeyCtrlK = keyVT
|
|
KeyCtrlL = keyFF
|
|
KeyCtrlM = keyCR
|
|
KeyCtrlN = keySO
|
|
KeyCtrlO = keySI
|
|
KeyCtrlP = keyDLE
|
|
KeyCtrlQ = keyDC1
|
|
KeyCtrlR = keyDC2
|
|
KeyCtrlS = keyDC3
|
|
KeyCtrlT = keyDC4
|
|
KeyCtrlU = keyNAK
|
|
KeyCtrlV = keyETB
|
|
KeyCtrlW = keyETB
|
|
KeyCtrlX = keyCAN
|
|
KeyCtrlY = keyEM
|
|
KeyCtrlZ = keySUB
|
|
KeyCtrlOpenBracket = keyESC // ctrl+[
|
|
KeyCtrlBackslash = keyFS // ctrl+\
|
|
KeyCtrlCloseBracket = keyGS // ctrl+]
|
|
KeyCtrlCaret = keyRS // ctrl+^
|
|
KeyCtrlUnderscore = keyUS // ctrl+_
|
|
KeyCtrlQuestionMark = keyDEL // ctrl+?
|
|
)
|
|
|
|
// Other keys we track
|
|
const (
|
|
KeyRune = -(iota + 1)
|
|
KeyUp
|
|
KeyDown
|
|
KeyRight
|
|
KeyLeft
|
|
KeyShiftTab
|
|
KeyHome
|
|
KeyEnd
|
|
KeyPgUp
|
|
KeyPgDown
|
|
)
|
|
|
|
// Mapping for control keys to friendly consts
|
|
var keyNames = map[int]string{
|
|
keyNUL: "ctrl+@", // also ctrl+`
|
|
keySOH: "ctrl+a",
|
|
keySTX: "ctrl+b",
|
|
keyETX: "ctrl+c",
|
|
keyEOT: "ctrl+d",
|
|
keyENQ: "ctrl+e",
|
|
keyACK: "ctrl+f",
|
|
keyBEL: "ctrl+g",
|
|
keyBS: "backspace", // also ctrl+h
|
|
keyHT: "tab", // also ctrl+i
|
|
keyLF: "ctrl+j",
|
|
keyVT: "ctrl+k",
|
|
keyFF: "ctrl+l",
|
|
keyCR: "enter",
|
|
keySO: "ctrl+n",
|
|
keySI: "ctrl+o",
|
|
keyDLE: "ctrl+p",
|
|
keyDC1: "ctrl+q",
|
|
keyDC2: "ctrl+r",
|
|
keyDC3: "ctrl+s",
|
|
keyDC4: "ctrl+t",
|
|
keyNAK: "ctrl+u",
|
|
keySYN: "ctrl+v",
|
|
keyETB: "ctrl+w",
|
|
keyCAN: "ctrl+x",
|
|
keyEM: "ctrl+y",
|
|
keySUB: "ctrl+z",
|
|
keyESC: "esc",
|
|
keyFS: "ctrl+\\",
|
|
keyGS: "ctrl+]",
|
|
keyRS: "ctrl+^",
|
|
keyUS: "ctrl+_",
|
|
keySP: "space",
|
|
keyDEL: "delete",
|
|
|
|
KeyRune: "rune",
|
|
KeyUp: "up",
|
|
KeyDown: "down",
|
|
KeyRight: "right",
|
|
KeyLeft: "left",
|
|
KeyShiftTab: "shift+tab",
|
|
KeyHome: "home",
|
|
KeyEnd: "end",
|
|
KeyPgUp: "pgup",
|
|
KeyPgDown: "pgdown",
|
|
}
|
|
|
|
// Mapping for sequences to consts
|
|
// TODO: should we just move this into the hex table?
|
|
var sequences = map[string]KeyType{
|
|
"\x1b[A": KeyUp,
|
|
"\x1b[B": KeyDown,
|
|
"\x1b[C": KeyRight,
|
|
"\x1b[D": KeyLeft,
|
|
}
|
|
|
|
// Mapping for hex codes to consts. Unclear why these won't register as
|
|
// sequences.
|
|
var hexes = map[string]Key{
|
|
"1b5b5a": Key{Type: KeyShiftTab},
|
|
"1b0d": Key{Type: KeyEnter, Alt: true},
|
|
"1b7f": Key{Type: KeyDelete, Alt: true},
|
|
"1b5b48": Key{Type: KeyHome},
|
|
"1b5b377e": Key{Type: KeyHome}, // urxvt
|
|
"1b5b313b3348": Key{Type: KeyHome, Alt: true},
|
|
"1b1b5b377e": Key{Type: KeyHome, Alt: true}, // ursvt
|
|
"1b5b46": Key{Type: KeyEnd},
|
|
"1b5b387e": Key{Type: KeyEnd}, // urxvt
|
|
"1b5b313b3346": Key{Type: KeyEnd, Alt: true},
|
|
"1b1b5b387e": Key{Type: KeyEnd, Alt: true}, // urxvt
|
|
"1b5b357e": Key{Type: KeyPgUp},
|
|
"1b5b353b337e": Key{Type: KeyPgUp, Alt: true},
|
|
"1b1b5b357e": Key{Type: KeyPgUp, Alt: true}, // urxvt
|
|
"1b5b367e": Key{Type: KeyPgDown},
|
|
"1b5b363b337e": Key{Type: KeyPgDown, Alt: true},
|
|
"1b1b5b367e": Key{Type: KeyPgDown, Alt: true}, // urxvt
|
|
"1b5b313b3341": Key{Type: KeyUp, Alt: true},
|
|
"1b5b313b3342": Key{Type: KeyDown, Alt: true},
|
|
"1b5b313b3343": Key{Type: KeyRight, Alt: true},
|
|
"1b5b313b3344": Key{Type: KeyLeft, Alt: true},
|
|
}
|
|
|
|
// ReadKey reads keypress input from a TTY and returns a string representation
|
|
// of a key
|
|
func ReadKey(r io.Reader) (Key, error) {
|
|
var buf [256]byte
|
|
|
|
// Read and block
|
|
numBytes, err := r.Read(buf[:])
|
|
if err != nil {
|
|
return Key{}, err
|
|
}
|
|
|
|
hex := fmt.Sprintf("%x", buf[:numBytes])
|
|
|
|
// Some of these need special handling
|
|
if k, ok := hexes[hex]; ok {
|
|
return k, nil
|
|
}
|
|
|
|
// Get unicode value
|
|
char, _ := utf8.DecodeRune(buf[:])
|
|
if char == utf8.RuneError {
|
|
return Key{}, errors.New("could not decode rune")
|
|
}
|
|
|
|
// Is it a control character?
|
|
if numBytes == 1 && char <= keyUS || char == keyDEL {
|
|
return Key{Type: KeyType(char)}, nil
|
|
}
|
|
|
|
// Is it a special sequence, like an arrow key?
|
|
if k, ok := sequences[string(buf[:numBytes])]; ok {
|
|
return Key{Type: k}, nil
|
|
}
|
|
|
|
// Is the alt key pressed? The buffer will be prefixed with an escape
|
|
// sequence if so
|
|
if numBytes > 1 && buf[0] == 0x1b {
|
|
// Now remove the initial escape sequence and re-process to get the
|
|
// character.
|
|
c, _ := utf8.DecodeRune(buf[1:])
|
|
if c == utf8.RuneError {
|
|
return Key{}, errors.New("could not decode rune after removing initial escape")
|
|
}
|
|
return Key{Alt: true, Type: KeyRune, Rune: c}, nil
|
|
}
|
|
|
|
// Just a regular, ol' rune
|
|
return Key{Type: KeyRune, Rune: char}, nil
|
|
}
|