184 lines
6.0 KiB
Go
184 lines
6.0 KiB
Go
// 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(), " <no completions>\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)
|
|
}
|