// Copyright (c) 2026, Pim van Pelt package main import ( "context" "errors" "fmt" "io" "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 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(), "error: %v\n", 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 := Walk(root, tokens) if node.Run == nil { showHelp(root, tokens) return nil } return node.Run(ctx, client, args) } // showHelp prints available completions for the given token path. func showHelp(root *Node, tokens []string) { node, _ := Walk(root, tokens) candidates := filterFixedChildren(node.Children, "") if len(candidates) == 0 { fmt.Println(" ") return } for _, c := range candidates { if c.Help != "" { fmt.Printf(" %-20s %s\n", c.Word, c.Help) } else { fmt.Printf(" %s\n", c.Word) } } }