// Copyright (c) 2026, Pim van Pelt package main import ( "context" "fmt" "strings" "time" "github.com/chzyer/readline" "git.ipng.ch/ipng/vpp-maglev/internal/grpcapi" ) const completeTimeout = 1 * time.Second // Completer implements readline.AutoCompleter for the command tree. type Completer struct { root *Node client grpcapi.MaglevClient } // Do implements readline.AutoCompleter. // line is the full current line; pos is the cursor position. // Returns (newLine [][]rune, length int) where length is how many rune bytes // before pos should be replaced by each candidate in newLine. func (co *Completer) Do(line []rune, pos int) (newLine [][]rune, length int) { before := string(line[:pos]) tokens := splitTokens(before) // Determine the partial token being completed. var partial string var prefix []string if len(tokens) == 0 || (len(before) > 0 && before[len(before)-1] == ' ') { // Cursor is after a space — completing a new token. prefix = tokens partial = "" } else { // Cursor is within the last token. prefix = tokens[:len(tokens)-1] partial = tokens[len(tokens)-1] } ctx, cancel := context.WithTimeout(context.Background(), completeTimeout) defer cancel() candidates := Candidates(co.root, prefix, partial, ctx, co.client) var suffixes [][]rune for _, c := range candidates { suffix := c.Word[len(partial):] suffixes = append(suffixes, []rune(suffix+" ")) } return suffixes, len([]rune(partial)) } // questionListener intercepts the '?' key and prints inline help. type questionListener struct { root *Node client grpcapi.MaglevClient rl *readline.Instance } func (ql *questionListener) OnChange(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) { if key != '?' { return line, pos, false } // line[:pos] includes the '?' just typed — strip it before tokenizing. before := string(line[:pos]) if len(before) > 0 && before[len(before)-1] == '?' { before = before[:len(before)-1] } tokens := splitTokens(before) var partial string var prefix []string if len(before) == 0 || before[len(before)-1] == ' ' { prefix = tokens partial = "" } else if len(tokens) > 0 { prefix = tokens[:len(tokens)-1] partial = tokens[len(tokens)-1] } ctx, cancel := context.WithTimeout(context.Background(), completeTimeout) defer cancel() candidates := Candidates(ql.root, prefix, partial, ctx, ql.client) fmt.Fprintf(ql.rl.Stderr(), "\r\n") if len(candidates) == 0 { fmt.Fprintf(ql.rl.Stderr(), " \r\n") } else { for _, c := range candidates { if c.Help != "" { fmt.Fprintf(ql.rl.Stderr(), " %-20s %s\r\n", c.Word, c.Help) } else { fmt.Fprintf(ql.rl.Stderr(), " %s\r\n", c.Word) } } } // Remove the '?' from the line and return with cursor one step back. newLine = append(append([]rune{}, line[:pos-1]...), line[pos:]...) return newLine, pos - 1, true } // splitTokens splits a string into whitespace-separated tokens. func splitTokens(s string) []string { return strings.Fields(s) }