1// package multibar provides a combined Vi-like statusbar and input field.
2//
3// Multibar has three modes:
4//   * NORMAL	statusbar text is shown
5//   * COMMAND	acts as a command input box
6//   * SEARCH	acts as a search input box
7
8package multibar
9
10import (
11	"strings"
12	"unicode"
13	"unicode/utf8"
14
15	"github.com/ambientsound/visp/log"
16
17	"github.com/ambientsound/visp/input/lexer"
18	"github.com/ambientsound/visp/message"
19	"github.com/ambientsound/visp/utils"
20
21	"github.com/gdamore/tcell/v2"
22)
23
24type TabCompleter interface {
25	Scan() (string, error)
26}
27
28type TabCompleterFactory func(input string) TabCompleter
29
30// Multibar implements a Vi-like combined statusbar and input box.
31type Multibar struct {
32	buffer      []rune
33	commands    chan string
34	cursor      int
35	history     []*history
36	mode        InputMode
37	msg         message.Message
38	orig        []rune
39	searches    chan string
40	tabComplete TabCompleter
41	tcf         TabCompleterFactory
42}
43
44func New(tcf TabCompleterFactory) *Multibar {
45	hist := make([]*history, 3)
46	for i := range hist {
47		hist[i] = NewHistory()
48	}
49	return &Multibar{
50		history:  hist,
51		buffer:   make([]rune, 0),
52		orig:     make([]rune, 0),
53		commands: make(chan string, 1),
54		searches: make(chan string, 1),
55		tcf:      tcf,
56	}
57}
58
59// Input is called on keyboard events.
60func (m *Multibar) Input(event tcell.Event) bool {
61	ev, ok := event.(*tcell.EventKey)
62	if !ok {
63		return false
64	}
65
66	if m.mode == ModeNormal {
67		return false
68	}
69
70	log.Debugf("multibar keypress: name=%v key=%v modifiers=%v", ev.Name(), ev.Key(), ev.Modifiers())
71
72	switch ev.Key() {
73
74	// Alt keys has to be handled a bit differently than Ctrl keys.
75	case tcell.KeyRune:
76		modifiers := ev.Modifiers()
77		if modifiers&tcell.ModAlt == 0 {
78			// Pass the rune on to the text handling function if the alt modifier was not used.
79			m.inputRune(ev.Rune())
80		} else {
81			switch ev.Rune() {
82			case 'b':
83				m.wordJump(-1)
84			case 'f':
85				m.wordJump(1)
86			}
87		}
88
89	case tcell.KeyCtrlU:
90		m.truncate()
91	case tcell.KeyEnter:
92		m.finish()
93	case tcell.KeyTab:
94		if m.Mode() == ModeInput {
95			m.tab()
96		}
97	case tcell.KeyLeft, tcell.KeyCtrlB:
98		m.moveCursor(-1)
99	case tcell.KeyRight, tcell.KeyCtrlF:
100		m.moveCursor(1)
101	case tcell.KeyUp, tcell.KeyCtrlP:
102		m.moveHistory(-1)
103	case tcell.KeyDown, tcell.KeyCtrlN:
104		m.moveHistory(1)
105	case tcell.KeyCtrlG, tcell.KeyCtrlC:
106		if m.tabComplete == nil {
107			m.abort()
108		} else {
109			m.untab()
110		}
111	case tcell.KeyCtrlA, tcell.KeyHome:
112		m.moveCursor(-len(m.buffer))
113	case tcell.KeyCtrlE, tcell.KeyEnd:
114		m.moveCursor(len(m.buffer))
115	case tcell.KeyBS, tcell.KeyDEL:
116		m.backspace()
117	case tcell.KeyCtrlW:
118		m.deleteWord()
119
120	default:
121		log.Debugf("Unhandled text input event in Multibar: %v", ev.Key())
122		return false
123	}
124
125	return true
126}
127
128// History returns the input history of the current input mode.
129func (m *Multibar) History() *history {
130	return m.history[m.mode]
131}
132
133// Clear the statusbar text
134func (m *Multibar) Clear() {
135	m.SetMessage(message.Message{
136		Severity: message.Info,
137		Text:     "",
138	})
139}
140
141// Set an error in the statusbar
142func (m *Multibar) Error(err error) {
143	m.SetMessage(message.Message{
144		Severity: message.Error,
145		Text:     err.Error(),
146	})
147}
148
149func (m *Multibar) Message() message.Message {
150	return m.msg
151}
152
153func (m *Multibar) SetMessage(msg message.Message) {
154	m.msg = msg
155}
156
157func (m *Multibar) SetMode(mode InputMode) {
158	log.Debugf("Switching input mode from %s to %s", m.mode, mode)
159	m.mode = mode
160	m.setRunes(make([]rune, 0))
161	m.History().Reset("")
162}
163
164func (m *Multibar) Mode() InputMode {
165	return m.mode
166}
167
168func (m *Multibar) String() string {
169	return string(m.buffer)
170}
171
172func (m *Multibar) Len() int {
173	return len(m.buffer)
174}
175
176// Cursor returns the cursor position.
177func (m *Multibar) Cursor() int {
178	return m.cursor
179}
180
181// Commands returns a channel sending any commands entered in input mode.
182func (m *Multibar) Commands() <-chan string {
183	return m.commands
184}
185
186// Searches returns a channel sending any search terms.
187func (m *Multibar) Searches() <-chan string {
188	return m.searches
189}
190
191func (m *Multibar) setRunes(r []rune) {
192	m.buffer = r
193	m.validateCursor()
194}
195
196// validateCursor makes sure the cursor stays within boundaries.
197func (m *Multibar) validateCursor() {
198	if m.cursor > len(m.buffer) {
199		m.cursor = len(m.buffer)
200	}
201	if m.cursor < 0 {
202		m.cursor = 0
203	}
204}
205
206func (m *Multibar) truncate() {
207	m.tabComplete = nil
208	m.setRunes(make([]rune, 0))
209	m.History().Reset(m.String())
210}
211
212// inputRune inserts a literal rune at the cursor position.
213func (m *Multibar) inputRune(r rune) {
214	m.tabComplete = nil
215	runes := make([]rune, len(m.buffer)+1)
216	copy(runes, m.buffer[:m.cursor])
217	copy(runes[m.cursor+1:], m.buffer[m.cursor:])
218	runes[m.cursor] = r
219	m.setRunes(runes)
220
221	m.cursor++
222	m.History().Reset(m.String())
223}
224
225// backspace deletes a literal rune behind the cursor position.
226func (m *Multibar) backspace() {
227
228	m.tabComplete = nil
229
230	// Backspace on an empty string returns to normal mode.
231	if len(m.buffer) == 0 {
232		m.abort()
233		return
234	}
235
236	// Copy all runes except the deleted rune
237	runes := deleteBackwards(m.buffer, m.cursor, 1)
238	m.cursor--
239	m.setRunes(runes)
240
241	m.History().Reset(m.String())
242}
243
244// deleteWord deletes the previous word, along with all the backspace
245// succeeding it.
246func (m *Multibar) deleteWord() {
247
248	m.tabComplete = nil
249
250	// We don't use the lexer here because it is too smart when it comes to
251	// quoted strings.
252	cursor := m.cursor - 1
253
254	// Scan backwards until a non-space character is found.
255	for ; cursor >= 0; cursor-- {
256		if !unicode.IsSpace(m.buffer[cursor]) {
257			break
258		}
259	}
260
261	// Scan backwards until a space character is found.
262	for ; cursor >= 0; cursor-- {
263		if unicode.IsSpace(m.buffer[cursor]) {
264			cursor++
265			break
266		}
267	}
268
269	// Delete backwards.
270	runes := deleteBackwards(m.buffer, m.cursor, m.cursor-cursor)
271	m.cursor = cursor
272	m.setRunes(runes)
273
274	m.History().Reset(m.String())
275}
276
277func (m *Multibar) finish() {
278	input := m.String()
279	m.tabComplete = nil
280	m.History().Add(input)
281
282	mode := m.mode
283	m.SetMode(ModeNormal)
284
285	switch mode {
286	case ModeInput:
287		m.commands <- input
288	case ModeSearch:
289		m.searches <- input
290	}
291}
292
293func (m *Multibar) abort() {
294	m.setRunes(make([]rune, 0))
295	m.finish()
296}
297
298func (m *Multibar) moveHistory(offset int) {
299	m.tabComplete = nil
300	s := m.History().Navigate(offset)
301	m.setRunes([]rune(s))
302	m.cursor = len(m.buffer)
303}
304
305func (m *Multibar) moveCursor(offset int) {
306	m.tabComplete = nil
307	m.cursor += offset
308	m.validateCursor()
309}
310
311// wordJump moves the cursor forward to the start of the next word or
312// backwards to the start of the previous word.
313func (m *Multibar) wordJump(offset int) {
314	m.tabComplete = nil
315	m.cursor += nextWord(m.buffer, m.cursor, offset)
316	m.validateCursor()
317}
318
319// tab invokes tab completion.
320func (m *Multibar) tab() {
321
322	// Ignore event if cursor is not at the end
323	if m.cursor != len(m.buffer) {
324		return
325	}
326
327	// Initialize tabcomplete
328	if m.tabComplete == nil {
329		m.orig = make([]rune, len(m.buffer))
330		copy(m.orig, m.buffer)
331		m.tabComplete = m.tcf(m.String())
332	}
333
334	// Get next sentence, and abort on any errors.
335	sentence, err := m.tabComplete.Scan()
336	if err != nil {
337		log.Debugf("Autocomplete: %s", err)
338		return
339	}
340
341	// Replace current text.
342	m.setRunes([]rune(sentence))
343	m.cursor = len(m.buffer)
344}
345
346// untab cancels tab completion, restoring the buffer to its original contents.
347func (m *Multibar) untab() {
348	m.buffer = make([]rune, len(m.orig))
349	copy(m.buffer, m.orig)
350	m.cursor = len(m.buffer)
351	m.tabComplete = nil
352}
353
354// deleteBackwards returns a new rune slice with a part cut out. If the deleted
355// part is bigger than the string contains, deleteBackwards removes as much as
356// possible.
357func deleteBackwards(src []rune, cursor int, length int) []rune {
358	if cursor < length {
359		length = cursor
360	}
361	runes := make([]rune, len(src)-length)
362	index := copy(runes, src[:cursor-length])
363	copy(runes[index:], src[cursor:])
364	return runes
365}
366
367// nextWord returns the distance to the next word in a rune slice.
368func nextWord(runes []rune, cursor, offset int) int {
369	var s string
370
371	switch {
372	// Move backwards
373	case offset < 0:
374		rev := utils.ReverseRunes(runes)
375		revIndex := len(runes) - cursor
376		runes := rev[revIndex:]
377		s = string(runes)
378
379	// Move forwards
380	case offset > 0:
381		runes := runes[cursor:]
382		s = string(runes)
383
384	default:
385		return 0
386	}
387
388	reader := strings.NewReader(s)
389	scanner := lexer.NewScanner(reader)
390
391	// Strip any whitespace, and count the total length of the whitespace and
392	// the next token.
393	tok, lit := scanner.Scan()
394	skip := utf8.RuneCountInString(lit)
395	if tok == lexer.TokenWhitespace {
396		_, lit = scanner.Scan()
397		skip += utf8.RuneCountInString(lit)
398	}
399
400	return offset * skip
401}
402