summaryrefslogtreecommitdiff
path: root/tui/termfo/keys/key.go
blob: a4e454f826fa7c3e3ed94675a5f9ab919511717b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
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 "<C-a>" 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()
}