// Copyright (c) 2026, Pim van Pelt package main import ( "context" "errors" "fmt" "io" "strings" "github.com/chzyer/readline" "git.ipng.ch/ipng/vpp-maglev/internal/grpcapi" ) // errQuit is a sentinel returned by runQuit to exit the REPL. var errQuit = errors.New("quit") // runShell runs the interactive REPL until the user types quit/exit or EOF. func runShell(ctx context.Context, client grpcapi.MaglevClient) error { root := buildTree() comp := &Completer{root: root, client: client} ql := &questionListener{root: root, client: client} cfg := &readline.Config{ Prompt: "maglev> ", AutoComplete: comp, InterruptPrompt: "^C", EOFPrompt: "exit", Listener: ql, } rl, err := readline.NewEx(cfg) if err != nil { return fmt.Errorf("readline init: %w", err) } ql.rl = rl defer func() { _ = rl.Close() }() for { line, err := rl.Readline() if err == readline.ErrInterrupt { continue } if err == io.EOF { return nil } if err != nil { return err } tokens := splitTokens(line) if len(tokens) == 0 { continue } if err := dispatch(ctx, root, client, tokens); err != nil { if errors.Is(err, errQuit) { return nil } _, _ = fmt.Fprintf(rl.Stderr(), "%s\n", formatError(err)) } } } // dispatch walks the tree and executes the matched command. func dispatch(ctx context.Context, root *Node, client grpcapi.MaglevClient, tokens []string) error { node, args, remaining := Walk(root, tokens) if len(remaining) > 0 { // One or more tokens couldn't be matched. Report the first // offending token with the consumed prefix for context; don't // dump the full command tree prefixed with garbage, which is // what the previous code did and what prompted this fix. consumed := tokens[:len(tokens)-len(remaining)] return unknownCommandError(consumed, remaining[0]) } if node.Run == nil { showHelpAt(node, strings.Join(tokens, " ")) return nil } return node.Run(ctx, client, args) } // unknownCommandError builds the error returned by dispatch when the // tree walk couldn't consume the full token list. The format differs // slightly depending on whether any tokens were consumed, so the // message always points at the first unknown token and its context. func unknownCommandError(consumed []string, bad string) error { if len(consumed) == 0 { return fmt.Errorf("unknown command: %s", bad) } return fmt.Errorf("unknown subcommand %q after %q", bad, strings.Join(consumed, " ")) } // showHelpAt prints the reachable leaves below node, each displayed // with the given prefix. Split from dispatch so the caller can decide // which node to anchor the help at without re-walking the tree. func showHelpAt(node *Node, prefix string) { lines := expandPaths(node, prefix, make(map[*Node]bool)) maxLen := 0 for _, l := range lines { if len(l.path) > maxLen { maxLen = len(l.path) } } if len(lines) == 0 { fmt.Println(" ") return } for _, l := range lines { if l.help != "" { fmt.Printf("%-*s %s\n", maxLen+2, l.path, l.help) } else { fmt.Printf("%s\n", l.path) } } }