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 <noreply@google.com> Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
38765962f8
commit
fa3a7bcd83
6 changed files with 89 additions and 5 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -23,3 +23,4 @@ local.test
|
|||
|
||||
patch_cov.*
|
||||
go.work.sum
|
||||
.kb
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue