Refactor CLI: birdc-style help, collapsed nouns, ReloadConfig, bug fixes
maglevc - Rewrite '?' handler (birdc-style): show full command paths from current position to every leaf, right-aligned help column, dynamic slot values displayed as an indented block when cursor is at a slot position. - Collapse show frontends/frontend, backends/backend, healthchecks/healthcheck into single plural-noun nodes with an optional <name> slot. Allows 'sh ba' (list all) and 'sh ba nginx0' (show one) without ambiguity. - Add 'config reload' command. - Fix tabwriter ANSI alignment: continuation lines in transition output now carry the same label() byte overhead as the header line. - Fix broken Walk for 'set frontend' command: setFrontendPoolName and setWeightValue were fixed-word nodes that couldn't capture user input; mark them as slot nodes with dynNone. - Add tree_test.go covering expandPaths, cycle detection, prefix matching, and the full weight-command walk. gRPC / proto - Add ReloadConfig RPC: checks config then applies it to the running checker, returning ok/parse_error/semantic_error/reload_error. - Add logging to CheckConfig (config-check-start/config-check-done at INFO level). maglevd - SIGHUP handler now calls maglevServer.TriggerReload(), sharing the same code path as the gRPC ReloadConfig RPC. docs - Collapse show command documentation to use [<name>] optional syntax. - Remove developer-facing 'Command tree and parser' section. - Document 'config reload'.
This commit is contained in:
@@ -67,15 +67,16 @@ func (ql *questionListener) OnChange(line []rune, pos int, key rune) (newLine []
|
||||
return line, pos, false
|
||||
}
|
||||
|
||||
// line[:pos] includes the '?' just typed — strip it before tokenizing.
|
||||
// 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)
|
||||
|
||||
var partial string
|
||||
// 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 = ""
|
||||
@@ -84,25 +85,65 @@ func (ql *questionListener) OnChange(line []rune, pos int, key rune) (newLine []
|
||||
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, _ := Walk(ql.root, prefix)
|
||||
displayPrefix := strings.Join(prefix, " ")
|
||||
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
|
||||
}
|
||||
|
||||
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(), " <no completions>\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)
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the '?' from the line and return with cursor one step back.
|
||||
// Emit output. Raw terminal mode requires \r\n.
|
||||
fmt.Fprintf(ql.rl.Stderr(), "\r\n")
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user