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.*
go.work.sum
.kb

View file

@ -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
}

View file

@ -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

View file

@ -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}

View file

@ -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 {

View file

@ -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)