diff --git a/helpers/levenshtein.go b/helpers/levenshtein.go new file mode 100644 index 0000000..5707750 --- /dev/null +++ b/helpers/levenshtein.go @@ -0,0 +1,99 @@ +/* +Copyright © Brian Buller + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +package helpers + +import "unicode/utf8" + +const levMinLngthThreshold = 32 + +func ComputeLevDistance(a, b string) int { + if len(a) == 0 { + return utf8.RuneCountInString(b) + } + if len(b) == 0 { + return utf8.RuneCountInString(a) + } + + if a == b { + return 0 + } + s1 := []rune(a) + s2 := []rune(b) + + if len(s1) > len(s2) { + s1, s2 = s2, s1 + } + + s1, s2 = trimLongestCommonSuffix(s1, s2) + s1, s2 = trimLongestCommonPrefix(s1, s2) + + lenS1 := len(s1) + lenS2 := len(s2) + + // Init the row + var x []uint16 + if lenS1+1 > levMinLngthThreshold { + x = make([]uint16, lenS1+1) + } else { + x = make([]uint16, levMinLngthThreshold) + x = x[:lenS1+1] + } + + for i := 1; i < len(x); i++ { + x[i] = uint16(i) + } + + _ = x[lenS1] + y := x[1:] + y = y[:lenS1] + for i := 0; i < lenS2; i++ { + prev := uint16(i + 1) + for j := 0; j < lenS1; j++ { + current := x[j] + if s2[i] != s1[j] { + current = min(x[j], prev, y[j]) + 1 + } + x[j] = prev + prev = current + } + x[lenS1] = prev + } + return int(x[lenS1]) +} + +func trimLongestCommonSuffix(a, b []rune) ([]rune, []rune) { + m := min(len(a), len(b)) + a2 := a[len(a)-m:] + b2 := b[len(b)-m:] + i := len(a2) + b2 = b2[:i] + for ; i > 0 && a2[i-1] == b2[i-1]; i-- { + } + return a[:len(a)-len(a2)+i], b[:len(b)-len(b2)+i] +} + +func trimLongestCommonPrefix(a, b []rune) ([]rune, []rune) { + var i int + for m := min(len(a), len(b)); i < m && a[i] == b[i]; i++ { + } + return a[i:], b[i:] +} diff --git a/wdgt_cli.go b/wdgt_cli.go index fcc6e0d..104bf37 100644 --- a/wdgt_cli.go +++ b/wdgt_cli.go @@ -23,7 +23,6 @@ package widgets import ( "fmt" - "sort" "strings" wh "git.bullercodeworks.com/brian/tcell-widgets/helpers" @@ -53,13 +52,12 @@ type Cli struct { history []string historyPosition int - commands []string - commandMap map[string]cliCommand - commandGuessMap map[string]func(args ...string) string + commands []*CliCommand keyMap KeyMap } +// TODO: Fix Command/SubCommand finding var _ Widget = (*Cli)(nil) func NewCli(id string, s tcell.Style) *Cli { @@ -146,20 +144,22 @@ func (w *Cli) Draw(screen tcell.Screen) { } x, y := w.x+1, w.y+1+w.h-3 - for i := 0; i < w.h-2; i++ { - if len(w.log) > (i + w.logPosition) { - idx := len(w.log) - (i + w.logPosition) - 1 - if idx < 0 { + if !w.minimized { + for i := 0; i < w.h-2; i++ { + if len(w.log) > (i + w.logPosition) { + idx := len(w.log) - (i + w.logPosition) - 1 + if idx < 0 { + y-- + wh.DrawText(x, y, wh.PadR("", w.w-2), dStyle, screen) + continue + } + line := w.log[idx] + if len(line) > w.w-2 { + line = line[:w.w-2] + } + wh.DrawText(x, y, wh.PadR(line, w.w-2), dStyle, screen) y-- - wh.DrawText(x, y, wh.PadR("", w.w-2), dStyle, screen) - continue } - line := w.log[idx] - if len(line) > w.w-2 { - line = line[:w.w-2] - } - wh.DrawText(x, y, wh.PadR(line, w.w-2), dStyle, screen) - y-- } } y = w.y + w.h - 1 @@ -258,26 +258,29 @@ func (w *Cli) initKeyMap() { tcell.KeyTab: func(ev *tcell.EventKey) bool { // Auto-complete guess := w.findBestGuess() - if guess != "" { - w.value = guess + if guess != nil { + wrk := strings.Split(w.value, " ") + wrk = append(wrk[:len(wrk)-1], guess.cmd) + w.value = strings.Join(wrk, " ") w.cursor = len(w.value) return true } return false }, tcell.KeyEnter: func(ev *tcell.EventKey) bool { - cmds := strings.Split(w.value, " ") - if v, ok := w.commandMap[cmds[0]]; ok { - w.history = append(w.history, w.value) - w.historyPosition = -1 - w.value = "" - w.cursor = 0 - if len(cmds) > 1 { - return v(cmds[1:]...) - } else { - return v() - } + args := strings.Split(w.value, " ") + w.historyPosition = -1 + w.value = "" + w.cursor = 0 + v := w.value + if len(args) > 0 { + v = args[0] } + if wrk := w.GetCommandFor(v); wrk != nil { + w.history = append(w.history, w.value) + return wrk.Run(args...) + } + w.history = append(w.history, fmt.Sprintf("%s: command not found", w.value)) return true }, tcell.KeyPgUp: func(ev *tcell.EventKey) bool { @@ -309,33 +312,56 @@ func (w *Cli) SetValue(val string) { w.cursor = len(val) } -func (w *Cli) AddComamnd(cmd string, do cliCommand) { - w.commands = append(w.commands, cmd) - sort.Strings(w.commands) - w.commandMap[cmd] = do +func (w *Cli) GetCommandFor(txt string) *CliCommand { + for i := range w.commands { + if w.commands[i].cmd == txt { + return w.commands[i] + } + } + return nil +} + +func (w *Cli) HasCommand(cmd *CliCommand) bool { + for i := range w.commands { + if w.commands[i].cmd == cmd.cmd { + return true + } + } + return false +} + +func (w *Cli) AddCommand(cmd *CliCommand) { + if !w.HasCommand(cmd) { + w.commands = append(w.commands, cmd) + } } func (w *Cli) RemoveCommand(cmd string) { var idx int for idx = range w.commands { - if w.commands[idx] == cmd { + if w.commands[idx].cmd == cmd { break } } w.commands = append(w.commands[:idx], w.commands[idx+1:]...) - delete(w.commandMap, cmd) } -func (w *Cli) findBestGuess() string { +func (w *Cli) findBestGuess() *CliCommand { if w.value == "" { - return "" + return nil + } + args := strings.Split(w.value, " ") + if len(args) > 1 { + if wrk := w.GetCommandFor(args[0]); wrk != nil { + return wrk.findBestGuess(args[1:]...) + } } for _, v := range w.commands { - if strings.HasPrefix(v, w.value) { + if strings.HasPrefix(v.cmd, w.value) { return v } } - return "" + return nil } func (w *Cli) Log(txt string, args ...any) { @@ -352,10 +378,71 @@ func (w *Cli) Clear() { func (w *Cli) SetMinimized(m bool) { w.minimized = m } func (w *Cli) IsMinimized() bool { return w.minimized } -type cliCommand func(args ...string) bool - -// TODO -func (c *cliCommand) findBestGuess(args ...string) string { - _ = args - return "" +type CliCommand struct { + cmd string + do func(args ...string) bool + subCommands []*CliCommand +} + +func NewCliCommand(nm string, do func(args ...string) bool, subs ...*CliCommand) *CliCommand { + c := CliCommand{ + cmd: nm, + do: do, + } + for i := range subs { + c.AddCommand(subs[i]) + } + return &c +} + +func (c *CliCommand) Run(args ...string) bool { + if len(c.subCommands) > 0 { + wrk := c.findBestGuess(args...) + if wrk != nil { + return wrk.Run(args...) + } + } + if c.do != nil { + return c.do(args...) + } + return false +} + +func (c *CliCommand) AddCommand(cc *CliCommand) error { + if c.HasCommand(cc.cmd) { + return fmt.Errorf("command already exists: %s", cc.cmd) + } + c.subCommands = append(c.subCommands, cc) + return nil +} + +func (c *CliCommand) HasCommand(cmd string) bool { + for i := range c.subCommands { + if c.subCommands[i].cmd == cmd { + return true + } + } + return false +} + +func (c *CliCommand) GetCommandFor(cmd string) *CliCommand { + for i := range c.subCommands { + if c.subCommands[i].cmd == cmd { + return c.subCommands[i] + } + } + return nil +} + +func (c *CliCommand) findBestGuess(args ...string) *CliCommand { + if len(args) == 0 { + return nil + } + // Find the best guess + for _, v := range c.subCommands { + if strings.HasPrefix(args[0], v.cmd) { + return v + } + } + return nil } diff --git a/wdgt_simple_list.go b/wdgt_simple_list.go index 1e10a0b..84307d0 100644 --- a/wdgt_simple_list.go +++ b/wdgt_simple_list.go @@ -212,8 +212,13 @@ func (w *SimpleList) MoveDown() bool { } func (w *SimpleList) SetTitle(ttl string) { w.title = ttl } func (w *SimpleList) SetList(l []string) { w.list = l } -func (w *SimpleList) Clear() { w.list = []string{} } -func (w *SimpleList) Add(l string) { w.list = append(w.list, l) } +func (w *SimpleList) Clear() { + w.list = []string{} + for k := range w.itemsStyle { + delete(w.itemsStyle, k) + } +} +func (w *SimpleList) Add(l string) { w.list = append(w.list, l) } func (w *SimpleList) Remove(l string) { var idx int var found bool @@ -242,7 +247,15 @@ func (w *SimpleList) SetItem(idx int, txt string) { w.list[idx] = txt } } -func (w *SimpleList) SelectedIndex() int { return w.cursor } +func (w *SimpleList) SelectedIndex() int { return w.cursor } +func (w *SimpleList) SetSelectedIndex(i int) { + if i < 0 { + i = 0 + } else if i >= len(w.list) { + i = len(w.list) - 1 + } + w.cursor = i +} func (w *SimpleList) ClearBorder() { w.border = []rune{} } func (w *SimpleList) SetOnSelect(s func(int, string) bool) { w.onSelect = s } func (w *SimpleList) SetVimMode(b bool) { w.vimMode = b }