feat(cli): add Go 1.26 iterators and modernise idioms
Some checks failed
Deploy / build (push) Failing after 4s
Security Scan / security (push) Successful in 16s

- 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 <noreply@google.com>
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-02-23 04:57:24 +00:00
parent 38765962f8
commit fa3a7bcd83
6 changed files with 89 additions and 5 deletions

1
.gitignore vendored
View file

@ -23,3 +23,4 @@ local.test
patch_cov.* patch_cov.*
go.work.sum go.work.sum
.kb

View file

@ -3,6 +3,7 @@ package cli
import ( import (
"context" "context"
"iter"
"sync" "sync"
"forge.lthn.ai/core/go/pkg/framework" "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. // attachRegisteredCommands calls all registered command functions.
// Called by Init() after creating the root command. // Called by Init() after creating the root command.
func attachRegisteredCommands(root *cobra.Command) { func attachRegisteredCommands(root *cobra.Command) {
@ -76,3 +90,4 @@ func attachRegisteredCommands(root *cobra.Command) {
} }
commandsAttached = true commandsAttached = true
} }

View file

@ -1,6 +1,9 @@
package cli package cli
import "fmt" import (
"fmt"
"iter"
)
// Region represents one of the 5 HLCRF regions. // Region represents one of the 5 HLCRF regions.
type Region rune type Region rune
@ -26,6 +29,28 @@ type Composite struct {
parent *Composite 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. // Slot holds content for a region.
type Slot struct { type Slot struct {
region Region region Region

View file

@ -3,6 +3,7 @@ package cli
import ( import (
"fmt" "fmt"
"io" "io"
"iter"
"os" "os"
"strings" "strings"
"sync" "sync"
@ -83,6 +84,33 @@ type TaskTracker struct {
started bool 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. // NewTaskTracker creates a new parallel task tracker.
func NewTaskTracker() *TaskTracker { func NewTaskTracker() *TaskTracker {
return &TaskTracker{out: os.Stdout} return &TaskTracker{out: os.Stdout}

View file

@ -2,6 +2,7 @@ package cli
import ( import (
"fmt" "fmt"
"iter"
"strings" "strings"
) )
@ -55,6 +56,17 @@ func (n *TreeNode) WithStyle(style *AnsiStyle) *TreeNode {
return n 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. // String renders the tree with box-drawing characters.
// Implements fmt.Stringer. // Implements fmt.Stringer.
func (n *TreeNode) String() string { func (n *TreeNode) String() string {

View file

@ -427,12 +427,14 @@ func ChooseMulti[T any](prompt string, items []T, opts ...ChooseOption[T]) []T {
// Returns 0-based indices. // Returns 0-based indices.
func parseMultiSelection(input string, maxItems int) ([]int, error) { func parseMultiSelection(input string, maxItems int) ([]int, error) {
selected := make(map[int]bool) 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") // Check for range (e.g., "1-3")
if strings.Contains(part, "-") { 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 { if len(rangeParts) != 2 {
return nil, fmt.Errorf("invalid range: %s", part) 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 // Convert map to sorted slice
result := make([]int, 0, len(selected)) result := make([]int, 0, len(selected))
for i := 0; i < maxItems; i++ { for i := range maxItems {
if selected[i] { if selected[i] {
result = append(result, i) result = append(result, i)
} }
@ -472,6 +474,7 @@ func parseMultiSelection(input string, maxItems int) ([]int, error) {
return result, nil return result, nil
} }
// ChooseMultiAction prompts for multiple selections using grammar composition. // ChooseMultiAction prompts for multiple selections using grammar composition.
// //
// files := ChooseMultiAction("select", "files", files) // files := ChooseMultiAction("select", "files", files)