forked from manifoldco/promptui
-
Notifications
You must be signed in to change notification settings - Fork 1
/
cursor.go
232 lines (204 loc) · 5.56 KB
/
cursor.go
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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
package promptui
import (
"fmt"
"strings"
)
// Pointer is A specific type that translates a given set of runes into a given
// set of runes pointed at by the cursor.
type Pointer func(to []rune) []rune
func defaultCursor(ignored []rune) []rune {
return []rune("\u2588")
}
func blockCursor(input []rune) []rune {
return []rune(fmt.Sprintf("\\e[7m%s\\e[0m", string(input)))
}
func pipeCursor(input []rune) []rune {
marker := []rune("|")
out := []rune{}
out = append(out, marker...)
out = append(out, input...)
return out
}
var (
// DefaultCursor is a big square block character. Obscures whatever was
// input.
DefaultCursor Pointer = defaultCursor
// BlockCursor is a cursor which highlights a character by inverting colors
// on it.
BlockCursor Pointer = blockCursor
// PipeCursor is a pipe character "|" which appears before the input
// character.
PipeCursor Pointer = pipeCursor
)
// Cursor tracks the state associated with the movable cursor
// The strategy is to keep the prompt, input pristine except for requested
// modifications. The insertion of the cursor happens during a `format` call
// and we read in new input via an `Update` call
type Cursor struct {
// shows where the user inserts/updates text
Cursor Pointer
// what the user entered, and what we will echo back to them, after
// insertion of the cursor and prefixing with the prompt
input []rune
// Put the cursor before this slice
Position int
erase bool
}
// NewCursor create a new cursor, with the DefaultCursor, the specified input,
// and position at the end of the specified starting input.
func NewCursor(startinginput string, pointer Pointer, eraseDefault bool) Cursor {
if pointer == nil {
pointer = defaultCursor
}
cur := Cursor{Cursor: pointer, Position: len(startinginput), input: []rune(startinginput), erase: eraseDefault}
if eraseDefault {
cur.Start()
} else {
cur.End()
}
return cur
}
func (c *Cursor) String() string {
return fmt.Sprintf(
"Cursor: %s, input %s, Position %d",
string(c.Cursor([]rune(""))), string(c.input), c.Position)
}
// End is a convenience for c.Place(len(c.input)) so you don't have to know how I
// indexed.
func (c *Cursor) End() {
c.Place(len(c.input))
}
// Start is convenience for c.Place(0) so you don't have to know how I
// indexed.
func (c *Cursor) Start() {
c.Place(0)
}
// ensures we are in bounds.
func (c *Cursor) correctPosition() {
if c.Position > len(c.input) {
c.Position = len(c.input)
}
if c.Position < 0 {
c.Position = 0
}
}
// insert the cursor rune array into r before the provided index
func format(a []rune, c *Cursor) string {
i := c.Position
var b []rune
out := make([]rune, 0)
if i < len(a) {
b = c.Cursor(a[i : i+1])
out = append(out, a[:i]...) // does not include i
out = append(out, b...) // add the cursor
out = append(out, a[i+1:]...) // add the rest after i
} else {
b = c.Cursor([]rune{})
out = append(out, a...)
out = append(out, b...)
}
return string(out)
}
// Format renders the input with the Cursor appropriately positioned.
func (c *Cursor) Format() string {
r := c.input
// insert the cursor
return format(r, c)
}
// FormatMask replaces all input runes with the mask rune.
func (c *Cursor) FormatMask(mask rune) string {
if mask == ' ' {
return format([]rune{}, c)
}
r := make([]rune, len(c.input))
for i := range r {
r[i] = mask
}
return format(r, c)
}
// Update inserts newinput into the input []rune in the appropriate place.
// The cursor is moved to the end of the inputed sequence.
func (c *Cursor) Update(newinput string) {
a := c.input
b := []rune(newinput)
i := c.Position
a = append(a[:i], append(b, a[i:]...)...)
c.input = a
c.Move(len(b))
}
// Get returns a copy of the input
func (c *Cursor) Get() string {
return string(c.input)
}
// GetMask returns a mask string with length equal to the input
func (c *Cursor) GetMask(mask rune) string {
return strings.Repeat(string(mask), len(c.input))
}
// Replace replaces the previous input with whatever is specified, and moves the
// cursor to the end position
func (c *Cursor) Replace(input string) {
c.input = []rune(input)
c.End()
}
// Place moves the cursor to the absolute array index specified by position
func (c *Cursor) Place(position int) {
c.Position = position
c.correctPosition()
}
// Move moves the cursor over in relative terms, by shift indices.
func (c *Cursor) Move(shift int) {
// delete the current cursor
c.Position = c.Position + shift
c.correctPosition()
}
// Backspace removes the rune that precedes the cursor
//
// It handles being at the beginning or end of the row, and moves the cursor to
// the appropriate position.
func (c *Cursor) Backspace() {
a := c.input
i := c.Position
if i == 0 {
// Shrug
return
}
if i == len(a) {
c.input = a[:i-1]
} else {
c.input = append(a[:i-1], a[i:]...)
}
// now it's pointing to the i+1th element
c.Move(-1)
}
// Listen is a readline Listener that updates internal cursor state appropriately.
func (c *Cursor) Listen(line []rune, pos int, key rune) ([]rune, int, bool) {
if line != nil {
// no matter what, update our internal representation.
c.Update(string(line))
}
switch key {
case 0: // empty
case KeyEnter:
return []rune(c.Get()), c.Position, false
case KeyBackspace, KeyCtrlH:
if c.erase {
c.erase = false
c.Replace("")
}
c.Backspace()
case KeyForward:
// the user wants to edit the default, despite how we set it up. Let
// them.
c.erase = false
c.Move(1)
case KeyBackward:
c.Move(-1)
default:
if c.erase {
c.erase = false
c.Replace("")
c.Update(string(key))
}
}
return []rune(c.Get()), c.Position, true
}