// SPDX-License-Identifier: Apache-2.0 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 } // Strip the '?' that was just appended to line[:pos]. before := string(line[:pos]) if len(before) > 0 && before[len(before)-1] == '?' { before = before[:len(before)-1] } tokens := splitTokens(before) // Split into confirmed prefix tokens and the partial token being typed. var prefix []string var partial 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] } // Walk the confirmed prefix to the current node, then try to advance one // more step using the partial token (via prefix-match or slot fallback). // This mirrors birdc: "sh?" expands "sh" to "show" and shows show's subtree. node, _, remaining := Walk(ql.root, prefix) displayPrefix := strings.Join(prefix, " ") var unknownMsg string if len(remaining) > 0 { // One of the confirmed prefix tokens was unknown. Show an // "unknown" banner, then list what's available at the deepest // node we *did* reach so the operator can see what they could // have typed instead. The partial at the cursor is irrelevant // once the left context is already broken — no downstream // branch reads it after we enter this branch, so we don't // bother clearing it. consumed := prefix[:len(prefix)-len(remaining)] bad := remaining[0] if len(consumed) == 0 { unknownMsg = fmt.Sprintf("unknown command: %s", bad) } else { unknownMsg = fmt.Sprintf("unknown subcommand %q after %q", bad, strings.Join(consumed, " ")) } displayPrefix = strings.Join(consumed, " ") } else if partial != "" { if next := matchFixedChild(node.Children, partial); next != nil { // Partial uniquely matched a fixed child — descend into it. node = next displayPrefix = strings.Join(tokens, " ") } else if slot := findSlotChild(node.Children); slot != nil { // Partial is filling a slot node. node = slot displayPrefix = strings.Join(tokens, " ") } // If partial matched nothing (ambiguous or dead end), stay at the // current node and show its subcommands with the confirmed prefix. } // Expand all leaf paths reachable from the current node. lines := expandPaths(node, displayPrefix, make(map[*Node]bool)) // If the cursor is at a position where the next input is a dynamic slot, // fetch live values now and show them below the syntax lines. ctx, cancel := context.WithTimeout(context.Background(), completeTimeout) defer cancel() var dynValues []string var dynWord string if slot := findSlotChild(node.Children); slot != nil && slot.Dynamic != nil { dynValues = slot.Dynamic(ctx, ql.client) dynWord = slot.Word } // Right-align the help column at the width of the longest path + 2. maxLen := 0 for _, l := range lines { if len(l.path) > maxLen { maxLen = len(l.path) } } // Emit output. Raw terminal mode requires \r\n. // // readline's wrapWriter wraps every Write in a clean-write-print // cycle: it erases the current input line, runs our closure, and // redraws the prompt+buffer afterwards. That means starting the // output with a bare "\r\n" leaves the original row blank, so the // operator loses sight of what they typed. Instead we echo the // full "maglev> show vpp lb ?" ourselves as the first write — // that lands on the just-cleaned row, birdc-style, and the // subsequent Fprintfs each redraw a fresh prompt below the help. _, _ = fmt.Fprintf(ql.rl.Stderr(), "%s%s\r\n", ql.rl.Config.Prompt, string(line)) if unknownMsg != "" { _, _ = fmt.Fprintf(ql.rl.Stderr(), " %s\r\n", unknownMsg) } if len(lines) == 0 { _, _ = fmt.Fprintf(ql.rl.Stderr(), " \r\n") } else { for _, l := range lines { if l.help != "" { _, _ = fmt.Fprintf(ql.rl.Stderr(), "%-*s %s\r\n", maxLen+2, l.path, l.help) } else { _, _ = fmt.Fprintf(ql.rl.Stderr(), "%s\r\n", l.path) } } if len(dynValues) > 0 { _, _ = fmt.Fprintf(ql.rl.Stderr(), " %s: %s\r\n", dynWord, strings.Join(dynValues, " ")) } } // Remove the '?' from the line and step cursor back one position. 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) }