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