// Copyright (c) 2026, Pim van Pelt package main import ( "context" "strings" "git.ipng.ch/ipng/vpp-maglev/internal/grpcapi" ) // Node is one word in the command tree. Leaf nodes have a Run function. // Slot nodes have Dynamic set (and no fixed Word to match against); they // accept any single token as an argument and may have further Children. type Node struct { Word string Help string Dynamic func(context.Context, grpcapi.MaglevClient) []string // non-nil → slot node Children []*Node Run func(context.Context, grpcapi.MaglevClient, []string) error } // Walk descends the tree following tokens. At each step it tries fixed // children first (exact then prefix), then falls back to a slot child // (Dynamic != nil). Tokens consumed by slot children are collected as args. // Returns the deepest node reached and the args collected from slot nodes. func Walk(root *Node, tokens []string) (*Node, []string) { node := root var args []string for len(tokens) > 0 { tok := tokens[0] // Try fixed children (exact, then unique prefix). next := matchFixedChild(node.Children, tok) if next != nil { node = next tokens = tokens[1:] continue } // Try a slot child. slot := findSlotChild(node.Children) if slot != nil { args = append(args, tok) tokens = tokens[1:] node = slot continue } // Dead end — no match. break } return node, args } // matchFixedChild returns the child matching tok by exact then unique prefix, // considering only non-slot children. func matchFixedChild(children []*Node, tok string) *Node { var fixed []*Node for _, c := range children { if c.Dynamic == nil { fixed = append(fixed, c) } } // Exact match. for _, c := range fixed { if c.Word == tok { return c } } // Unique prefix match. var matches []*Node for _, c := range fixed { if strings.HasPrefix(c.Word, tok) { matches = append(matches, c) } } if len(matches) == 1 { return matches[0] } return nil } // findSlotChild returns the first child that is a slot node (Dynamic != nil). func findSlotChild(children []*Node) *Node { for _, c := range children { if c.Dynamic != nil { return c } } return nil } // Candidates returns the completable children at the current position given // the already-typed tokens and the partial token being completed. func Candidates(root *Node, tokens []string, partial string, ctx context.Context, client grpcapi.MaglevClient) []*Node { // Walk the already-confirmed tokens. node, _ := Walk(root, tokens) // Now look at what could follow at this node. // Check fixed children filtered by partial. fixedMatches := filterFixedChildren(node.Children, partial) // Check dynamic slot child if present. var dynMatches []*Node slot := findSlotChild(node.Children) if slot != nil && slot.Dynamic != nil { vals := slot.Dynamic(ctx, client) for _, v := range vals { if strings.HasPrefix(v, partial) { dynMatches = append(dynMatches, &Node{Word: v, Help: slot.Help}) } } } return append(fixedMatches, dynMatches...) } func filterFixedChildren(children []*Node, prefix string) []*Node { var out []*Node for _, c := range children { if c.Dynamic == nil && strings.HasPrefix(c.Word, prefix) { out = append(out, c) } } return out }