package main import ( "errors" "strings" "github.com/nsf/termbox-go" ) type CliProc struct { History []string HistoryIdx int Suggestions []string // Suggestions are 'autocomplete' commands Proposals []string // Proposals are proposed 'next' commands Buffer string Cursor int Complete bool } func NewCLI() *CliProc { c := CliProc{} return &c } func (c *CliProc) handleEvent(event termbox.Event) error { if c.Complete { return nil } if event.Ch == 0 { switch event.Key { case termbox.KeyEnter: // Execute command c.Complete = true case termbox.KeySpace: c.putInBuffer(" ") case termbox.KeyBackspace, termbox.KeyBackspace2: if c.Cursor > 0 { c.Buffer = c.Buffer[:c.Cursor-1] + c.Buffer[c.Cursor:] c.Cursor = c.Cursor - 1 } case termbox.KeyDelete: if c.Cursor < len(c.Buffer) { c.Buffer = c.Buffer[:c.Cursor] + c.Buffer[c.Cursor+1:] } case termbox.KeyArrowLeft: if c.Cursor > 0 { c.Cursor = c.Cursor - 1 } case termbox.KeyArrowRight: if c.Cursor < len(c.Buffer) { c.Cursor = c.Cursor + 1 } else { // Complete suggestion } case termbox.KeyHome: c.Cursor = 0 case termbox.KeyEnd: c.Cursor = len(c.Buffer) case termbox.KeyArrowUp: val, err := c.HistoryBack() if err != nil { return err } c.Buffer = val c.Cursor = len(c.Buffer) case termbox.KeyArrowDown: val, err := c.HistoryForward() if err != nil { return err } c.Buffer = val c.Cursor = len(c.Buffer) case termbox.KeyTab: // Complete suggestion args := strings.Split(c.Buffer, " ") acText := strings.TrimPrefix(c.GetAutocompleteText(), args[len(args)-1]) if len(acText) > 0 { c.Buffer = c.Buffer + acText c.Cursor = len(c.Buffer) } case termbox.KeyCtrlU: // Delete everything from the cursor forward c.Buffer = c.Buffer[c.Cursor:] c.Cursor = 0 } } else { c.putInBuffer(string(event.Ch)) /* if c.Cursor == len(c.Buffer) { c.Buffer = c.Buffer + string(event.Ch) } else { c.Buffer = c.Buffer[:c.Cursor] + string(event.Ch) + c.Buffer[c.Cursor:] } c.Cursor = c.Cursor + 1 */ } return nil } func (c *CliProc) putInBuffer(v string) { c.Buffer = c.Buffer[:c.Cursor] + v + c.Buffer[c.Cursor:] c.Cursor = c.Cursor + 1 } func (c *CliProc) ClearBuffer() { c.Buffer = "" c.Cursor = 0 c.Complete = false } func (c *CliProc) GetBuffer() string { return c.Buffer } func (c *CliProc) HistoryBack() (string, error) { if len(c.History) < c.HistoryIdx+1 { return "", errors.New("Already at oldest command") } c.HistoryIdx = c.HistoryIdx + 1 return c.History[len(c.History)-c.HistoryIdx], nil } func (c *CliProc) HistoryForward() (string, error) { if c.HistoryIdx == 0 { return "", errors.New("Already at most recent command") } c.HistoryIdx = c.HistoryIdx - 1 if c.HistoryIdx == 0 { return "", nil } return c.History[len(c.History)-c.HistoryIdx], nil } func (c *CliProc) Suggest(cmd string) string { allSuggestions := c.GetAllSuggestions(cmd) if len(allSuggestions) == 0 { return "" } return allSuggestions[0] } func (c *CliProc) GetDisplaySuggestions() []string { var ret []string v := c.GetAllSuggestions(c.Buffer) if len(c.Buffer) > 0 { for k := range v { if strings.HasPrefix(v[k], c.Buffer) { ret = append(ret, v[k]) } } } else { // Normally, filter out 'drop's for k := range v { if !strings.HasPrefix(v[k], "drop ") { ret = append(ret, v[k]) } } } return ret } func (c *CliProc) GetAllSuggestions(cmd string) []string { var ret []string args := strings.Fields(cmd) if len(args) == 0 { // Return all commands for _, v := range c.Suggestions { ret = append(ret, v) } return ret } else if len(args) == 1 && !strings.HasSuffix(cmd, " ") { for _, v := range c.Suggestions { if strings.HasPrefix(v, args[0]) { ret = append(ret, v) } } } return ret } func (c *CliProc) Draw(x, y int, bg, fg termbox.Attribute) { DrawString("[ "+strings.Join(c.GetDisplaySuggestions(), ", ")+" ]", x, y, bg, fg) y = y + 1 prompt := '>' termbox.SetCell(x, y, prompt, bg, fg) x = x + 2 for k := range c.Buffer { useFg, useBg := bg, fg if k == c.Cursor { useFg, useBg = fg, bg } termbox.SetCell(x+k, y, rune(c.Buffer[k]), useFg, useBg) } args := strings.Split(c.Buffer, " ") acText := strings.TrimPrefix(c.GetAutocompleteText(), args[len(args)-1]) if len(acText) > 0 { for k := range acText { useFg, useBg := bg|termbox.AttrBold, fg if k == 0 && c.Cursor == len(c.Buffer) { useFg, useBg = fg|termbox.AttrBold, bg } termbox.SetCell(x+len(c.Buffer)+k, y, rune(acText[k]), useFg, useBg) } } else { if c.Cursor == len(c.Buffer) { termbox.SetCell(x+len(c.Buffer), y, ' ', fg, bg) } } } func (c *CliProc) GetAutocompleteText() string { suggestions := c.GetAllSuggestions(c.Buffer) if len(suggestions) == 1 { args := strings.Split(c.Buffer, " ") return strings.TrimPrefix(suggestions[0], args[len(args)-1]) } else if len(suggestions) > 1 { // Find how many characters we _can_ autocomplete var ret string for k := range suggestions[0] { allHave := true for wrk := range suggestions { if len(suggestions[wrk]) <= k || suggestions[0][k] != suggestions[wrk][k] { allHave = false break } } if allHave { ret = ret + string(suggestions[0][k]) } } return ret } return "" } func (c *CliProc) ClearSuggestions() { c.Suggestions = nil } func (c *CliProc) AddSuggestion(s string) { c.AddSuggestions([]string{s}) } func (c *CliProc) AddSuggestions(s []string) { c.Suggestions = append(c.Suggestions, s...) } func (c *CliProc) RemoveSuggestion(s string) { var i int for i = range c.Suggestions { if c.Suggestions[i] == s { break } } c.Suggestions = append(c.Suggestions[:i], c.Suggestions[i+1:]...) }