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.*
|
patch_cov.*
|
||||||
go.work.sum
|
go.work.sum
|
||||||
|
.kb
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue