From fa3a7bcd8327f2e77fea31f9f57d5fd23e498da9 Mon Sep 17 00:00:00 2001 From: Snider Date: Mon, 23 Feb 2026 04:57:24 +0000 Subject: [PATCH] feat(cli): add Go 1.26 iterators and modernise idioms - Add Children() iter.Seq on TreeNode for range-based traversal - Add RegisteredCommands() iter.Seq on command registry (mutex-safe) - Add Regions()/Slots() iterators on Composite layout - Add Tasks()/Snapshots() iterators on TaskTracker (mutex-safe) - Use strings.FieldsSeq, strings.SplitSeq in parseMultiSelection - Use range-over-int where applicable Co-Authored-By: Gemini Co-Authored-By: Virgil --- .gitignore | 1 + pkg/cli/commands.go | 15 +++++++++++++++ pkg/cli/layout.go | 27 ++++++++++++++++++++++++++- pkg/cli/tracker.go | 28 ++++++++++++++++++++++++++++ pkg/cli/tree.go | 12 ++++++++++++ pkg/cli/utils.go | 11 +++++++---- 6 files changed, 89 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index d7119518..043e7aeb 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ local.test patch_cov.* go.work.sum +.kb diff --git a/pkg/cli/commands.go b/pkg/cli/commands.go index b6e9a5b8..083fdde4 100644 --- a/pkg/cli/commands.go +++ b/pkg/cli/commands.go @@ -3,6 +3,7 @@ package cli import ( "context" + "iter" "sync" "forge.lthn.ai/core/go/pkg/framework" @@ -65,6 +66,19 @@ func RegisterCommands(fn CommandRegistration) { } } +// RegisteredCommands returns an iterator over the registered command functions. +func RegisteredCommands() iter.Seq[CommandRegistration] { + return func(yield func(CommandRegistration) bool) { + registeredCommandsMu.Lock() + defer registeredCommandsMu.Unlock() + for _, fn := range registeredCommands { + if !yield(fn) { + return + } + } + } +} + // attachRegisteredCommands calls all registered command functions. // Called by Init() after creating the root command. func attachRegisteredCommands(root *cobra.Command) { @@ -76,3 +90,4 @@ func attachRegisteredCommands(root *cobra.Command) { } commandsAttached = true } + diff --git a/pkg/cli/layout.go b/pkg/cli/layout.go index a8aedbbe..e0acb488 100644 --- a/pkg/cli/layout.go +++ b/pkg/cli/layout.go @@ -1,6 +1,9 @@ package cli -import "fmt" +import ( + "fmt" + "iter" +) // Region represents one of the 5 HLCRF regions. type Region rune @@ -26,6 +29,28 @@ type Composite struct { parent *Composite } +// Regions returns an iterator over the regions in the composite. +func (c *Composite) Regions() iter.Seq[Region] { + return func(yield func(Region) bool) { + for r := range c.regions { + if !yield(r) { + return + } + } + } +} + +// Slots returns an iterator over the slots in the composite. +func (c *Composite) Slots() iter.Seq2[Region, *Slot] { + return func(yield func(Region, *Slot) bool) { + for r, s := range c.regions { + if !yield(r, s) { + return + } + } + } +} + // Slot holds content for a region. type Slot struct { region Region diff --git a/pkg/cli/tracker.go b/pkg/cli/tracker.go index b8e4192d..c64c2e73 100644 --- a/pkg/cli/tracker.go +++ b/pkg/cli/tracker.go @@ -3,6 +3,7 @@ package cli import ( "fmt" "io" + "iter" "os" "strings" "sync" @@ -83,6 +84,33 @@ type TaskTracker struct { started bool } +// Tasks returns an iterator over the tasks in the tracker. +func (tr *TaskTracker) Tasks() iter.Seq[*TrackedTask] { + return func(yield func(*TrackedTask) bool) { + tr.mu.Lock() + defer tr.mu.Unlock() + for _, t := range tr.tasks { + if !yield(t) { + return + } + } + } +} + +// Snapshots returns an iterator over snapshots of tasks in the tracker. +func (tr *TaskTracker) Snapshots() iter.Seq2[string, string] { + return func(yield func(string, string) bool) { + tr.mu.Lock() + defer tr.mu.Unlock() + for _, t := range tr.tasks { + name, status, _ := t.snapshot() + if !yield(name, status) { + return + } + } + } +} + // NewTaskTracker creates a new parallel task tracker. func NewTaskTracker() *TaskTracker { return &TaskTracker{out: os.Stdout} diff --git a/pkg/cli/tree.go b/pkg/cli/tree.go index 50b4c9a9..ead9195e 100644 --- a/pkg/cli/tree.go +++ b/pkg/cli/tree.go @@ -2,6 +2,7 @@ package cli import ( "fmt" + "iter" "strings" ) @@ -55,6 +56,17 @@ func (n *TreeNode) WithStyle(style *AnsiStyle) *TreeNode { return n } +// Children returns an iterator over the node's children. +func (n *TreeNode) Children() iter.Seq[*TreeNode] { + return func(yield func(*TreeNode) bool) { + for _, child := range n.children { + if !yield(child) { + return + } + } + } +} + // String renders the tree with box-drawing characters. // Implements fmt.Stringer. func (n *TreeNode) String() string { diff --git a/pkg/cli/utils.go b/pkg/cli/utils.go index ed012d2d..13d0471a 100644 --- a/pkg/cli/utils.go +++ b/pkg/cli/utils.go @@ -427,12 +427,14 @@ func ChooseMulti[T any](prompt string, items []T, opts ...ChooseOption[T]) []T { // Returns 0-based indices. func parseMultiSelection(input string, maxItems int) ([]int, error) { selected := make(map[int]bool) - parts := strings.Fields(input) - for _, part := range parts { + for part := range strings.FieldsSeq(input) { // Check for range (e.g., "1-3") if strings.Contains(part, "-") { - rangeParts := strings.Split(part, "-") + var rangeParts []string + for p := range strings.SplitSeq(part, "-") { + rangeParts = append(rangeParts, p) + } if len(rangeParts) != 2 { return nil, fmt.Errorf("invalid range: %s", part) } @@ -464,7 +466,7 @@ func parseMultiSelection(input string, maxItems int) ([]int, error) { // Convert map to sorted slice result := make([]int, 0, len(selected)) - for i := 0; i < maxItems; i++ { + for i := range maxItems { if selected[i] { result = append(result, i) } @@ -472,6 +474,7 @@ func parseMultiSelection(input string, maxItems int) ([]int, error) { return result, nil } + // ChooseMultiAction prompts for multiple selections using grammar composition. // // files := ChooseMultiAction("select", "files", files)