docs: initial wiki — pkg/cli API reference and usage guides
commit
197f05cc69
9 changed files with 630 additions and 0 deletions
102
Command-Builders.md
Normal file
102
Command-Builders.md
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
# Command Builders & Flag Helpers
|
||||
|
||||
## Command Types
|
||||
|
||||
### `NewCommand` — Standard command (returns error)
|
||||
|
||||
```go
|
||||
cmd := cli.NewCommand("build", "Build the project", "", func(cmd *cli.Command, args []string) error {
|
||||
// Business logic here
|
||||
return nil // or return fmt.Errorf("failed: %w", err)
|
||||
})
|
||||
```
|
||||
|
||||
### `NewGroup` — Parent command (no handler, only subcommands)
|
||||
|
||||
```go
|
||||
scoreCmd := cli.NewGroup("score", "Scoring commands", "")
|
||||
scoreCmd.AddCommand(grammarCmd, attentionCmd, tierCmd)
|
||||
root.AddCommand(scoreCmd)
|
||||
```
|
||||
|
||||
### `NewRun` — Simple command (no error return)
|
||||
|
||||
```go
|
||||
cmd := cli.NewRun("version", "Show version", "", func(cmd *cli.Command, args []string) {
|
||||
cli.Println("v1.0.0")
|
||||
})
|
||||
```
|
||||
|
||||
## Flag Helpers
|
||||
|
||||
All flag helpers follow the same signature: `(cmd, ptr, name, short, default, usage)`.
|
||||
|
||||
```go
|
||||
var cfg struct {
|
||||
Model string
|
||||
Verbose bool
|
||||
Count int
|
||||
Score float64
|
||||
Seed int64
|
||||
Timeout time.Duration
|
||||
Tags []string
|
||||
}
|
||||
|
||||
cli.StringFlag(cmd, &cfg.Model, "model", "m", "", "Model path")
|
||||
cli.BoolFlag(cmd, &cfg.Verbose, "verbose", "v", false, "Verbose output")
|
||||
cli.IntFlag(cmd, &cfg.Count, "count", "n", 10, "Item count")
|
||||
cli.Float64Flag(cmd, &cfg.Score, "score", "s", 0.0, "Min score")
|
||||
cli.Int64Flag(cmd, &cfg.Seed, "seed", "", 0, "Random seed")
|
||||
cli.DurationFlag(cmd, &cfg.Timeout, "timeout", "t", 30*time.Second, "Timeout")
|
||||
cli.StringSliceFlag(cmd, &cfg.Tags, "tag", "", nil, "Tags")
|
||||
```
|
||||
|
||||
### Persistent Flags (inherited by subcommands)
|
||||
|
||||
```go
|
||||
cli.PersistentStringFlag(parentCmd, &dbPath, "db", "d", "", "Database path")
|
||||
cli.PersistentBoolFlag(parentCmd, &debug, "debug", "", false, "Debug mode")
|
||||
```
|
||||
|
||||
## Args Validation
|
||||
|
||||
```go
|
||||
cmd := cli.NewCommand("deploy", "Deploy to env", "", deployFn)
|
||||
cli.WithArgs(cmd, cli.ExactArgs(1)) // Exactly 1 arg
|
||||
cli.WithArgs(cmd, cli.MinimumNArgs(1)) // At least 1
|
||||
cli.WithArgs(cmd, cli.NoArgs()) // No args allowed
|
||||
cli.WithArgs(cmd, cli.ArbitraryArgs()) // Any args
|
||||
```
|
||||
|
||||
## Re-exports
|
||||
|
||||
- `cli.Command` = `cobra.Command` (no need to import cobra)
|
||||
- `cli.PositionalArgs` = `cobra.PositionalArgs`
|
||||
|
||||
## Pattern: Config Struct + Flags
|
||||
|
||||
The idiomatic pattern for commands with many flags:
|
||||
|
||||
```go
|
||||
type DistillOpts struct {
|
||||
Model string
|
||||
Probes string
|
||||
Runs int
|
||||
DryRun bool
|
||||
}
|
||||
|
||||
func addDistillCommand(parent *cli.Command) {
|
||||
var cfg DistillOpts
|
||||
|
||||
cmd := cli.NewCommand("distill", "Run distillation", "", func(cmd *cli.Command, args []string) error {
|
||||
return RunDistill(cfg)
|
||||
})
|
||||
|
||||
cli.StringFlag(cmd, &cfg.Model, "model", "m", "", "Model config path")
|
||||
cli.StringFlag(cmd, &cfg.Probes, "probes", "p", "", "Probe set name")
|
||||
cli.IntFlag(cmd, &cfg.Runs, "runs", "r", 3, "Runs per probe")
|
||||
cli.BoolFlag(cmd, &cfg.DryRun, "dry-run", "", false, "Preview without executing")
|
||||
|
||||
parent.AddCommand(cmd)
|
||||
}
|
||||
```
|
||||
78
Daemon-Mode.md
Normal file
78
Daemon-Mode.md
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
# Daemon Mode
|
||||
|
||||
## Execution Modes
|
||||
|
||||
```go
|
||||
mode := cli.DetectMode()
|
||||
// cli.ModeInteractive — TTY attached, colours enabled
|
||||
// cli.ModePipe — stdout piped, colours disabled
|
||||
// cli.ModeDaemon — CORE_DAEMON=1, log-only output
|
||||
|
||||
cli.IsTTY() // stdout is terminal?
|
||||
cli.IsStdinTTY() // stdin is terminal?
|
||||
```
|
||||
|
||||
## Simple Daemon
|
||||
|
||||
```go
|
||||
func runDaemon(cmd *cli.Command, args []string) error {
|
||||
ctx := cli.Context() // Cancelled on SIGINT/SIGTERM
|
||||
// ... start work ...
|
||||
return cli.Run(ctx) // Blocks until signal
|
||||
}
|
||||
```
|
||||
|
||||
## Full Daemon with Health Checks
|
||||
|
||||
```go
|
||||
daemon := cli.NewDaemon(cli.DaemonOptions{
|
||||
PIDFile: "/var/run/myapp.pid",
|
||||
ShutdownTimeout: 30 * time.Second,
|
||||
HealthAddr: ":9090",
|
||||
HealthChecks: []cli.HealthCheck{checkDB},
|
||||
})
|
||||
|
||||
if err := daemon.Start(); err != nil {
|
||||
return err // "another instance is running (PID 1234)"
|
||||
}
|
||||
|
||||
daemon.SetReady(true)
|
||||
return daemon.Run(cli.Context()) // Blocks, handles shutdown
|
||||
```
|
||||
|
||||
### Health Endpoints
|
||||
|
||||
- `GET /health` — Liveness (200 if server up, 503 if checks fail)
|
||||
- `GET /ready` — Readiness (200 if ready, 503 if not)
|
||||
|
||||
## PID File
|
||||
|
||||
```go
|
||||
pid := cli.NewPIDFile("/tmp/myapp.pid")
|
||||
if err := pid.Acquire(); err != nil {
|
||||
// "another instance is running (PID 1234)"
|
||||
}
|
||||
defer pid.Release()
|
||||
```
|
||||
|
||||
## Shutdown with Timeout
|
||||
|
||||
```go
|
||||
cli.Init(cli.Options{AppName: "worker"})
|
||||
shutdown := cli.RunWithTimeout(30 * time.Second)
|
||||
defer shutdown() // Replaces cli.Shutdown()
|
||||
cli.Run(cli.Context())
|
||||
```
|
||||
|
||||
## Signal Handling
|
||||
|
||||
Built into the runtime — SIGINT/SIGTERM cancel `cli.Context()`. Optional SIGHUP for config reload:
|
||||
|
||||
```go
|
||||
cli.Init(cli.Options{
|
||||
AppName: "daemon",
|
||||
OnReload: func() error {
|
||||
return reloadConfig()
|
||||
},
|
||||
})
|
||||
```
|
||||
72
Error-Handling.md
Normal file
72
Error-Handling.md
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# Error Handling
|
||||
|
||||
## Error Creation
|
||||
|
||||
```go
|
||||
// Simple error (replaces fmt.Errorf)
|
||||
return cli.Err("invalid model: %s", name)
|
||||
|
||||
// Wrap with context (nil-safe)
|
||||
return cli.Wrap(err, "load config") // "load config: <original>"
|
||||
|
||||
// Wrap with i18n grammar
|
||||
return cli.WrapVerb(err, "load", "config") // "Failed to load config: <original>"
|
||||
return cli.WrapAction(err, "connect") // "Failed to connect: <original>"
|
||||
```
|
||||
|
||||
## Error Inspection
|
||||
|
||||
Re-exports of `errors` package for convenience:
|
||||
|
||||
```go
|
||||
if cli.Is(err, os.ErrNotExist) { ... }
|
||||
|
||||
var exitErr *cli.ExitError
|
||||
if cli.As(err, &exitErr) {
|
||||
os.Exit(exitErr.Code)
|
||||
}
|
||||
|
||||
combined := cli.Join(err1, err2, err3)
|
||||
```
|
||||
|
||||
## Exit Codes
|
||||
|
||||
```go
|
||||
// Return specific exit code from a command
|
||||
return cli.Exit(2, fmt.Errorf("validation failed"))
|
||||
```
|
||||
|
||||
The `ExitError` type is checked in `Main()` — commands that return `*ExitError` cause the process to exit with that code.
|
||||
|
||||
## Fatal Functions (Deprecated)
|
||||
|
||||
These exist for legacy code but should not be used in new commands. Return errors from `RunE` instead.
|
||||
|
||||
```go
|
||||
// DON'T use these in new code:
|
||||
cli.Fatal(err) // prints + os.Exit(1)
|
||||
cli.Fatalf("bad: %v", err) // prints + os.Exit(1)
|
||||
cli.FatalWrap(err, "load config") // prints + os.Exit(1)
|
||||
cli.FatalWrapVerb(err, "load", "x") // prints + os.Exit(1)
|
||||
```
|
||||
|
||||
## Pattern: Commands Return Errors
|
||||
|
||||
```go
|
||||
// GOOD: return error, let Main() handle exit
|
||||
func runBuild(cmd *cli.Command, args []string) error {
|
||||
if err := compile(); err != nil {
|
||||
return cli.WrapVerb(err, "compile", "project")
|
||||
}
|
||||
cli.Success("Build complete")
|
||||
return nil
|
||||
}
|
||||
|
||||
// BAD: calling os.Exit from library code
|
||||
func runBuild(cmd *cli.Command, args []string) error {
|
||||
if err := compile(); err != nil {
|
||||
cli.Fatal(err) // DON'T DO THIS
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
120
Framework-Integration.md
Normal file
120
Framework-Integration.md
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
# Framework Integration
|
||||
|
||||
## How Commands Register
|
||||
|
||||
Commands register through the Core framework lifecycle using `WithCommands`:
|
||||
|
||||
```go
|
||||
// In main.go
|
||||
cli.Main(
|
||||
cli.WithCommands("score", score.AddScoreCommands),
|
||||
cli.WithCommands("gen", gen.AddGenCommands),
|
||||
)
|
||||
```
|
||||
|
||||
Internally, `WithCommands` wraps the registration function in a `commandService` that implements `Startable.OnStartup()`. During startup, it casts `Core.App` to `*cobra.Command` and calls the registration function.
|
||||
|
||||
**Startup order:**
|
||||
1. Core services start (i18n, log, crypt, workspace)
|
||||
2. Command services start (your `WithCommands` functions)
|
||||
3. `Execute()` runs the matched command
|
||||
|
||||
## Registration Function Pattern
|
||||
|
||||
```go
|
||||
// score/commands.go
|
||||
package score
|
||||
|
||||
import "forge.lthn.ai/core/cli/pkg/cli"
|
||||
|
||||
func AddScoreCommands(root *cli.Command) {
|
||||
scoreCmd := cli.NewGroup("score", "Scoring commands", "")
|
||||
|
||||
grammarCmd := cli.NewCommand("grammar", "Grammar analysis", "", runGrammar)
|
||||
cli.StringFlag(grammarCmd, &inputPath, "input", "i", "", "Input file")
|
||||
scoreCmd.AddCommand(grammarCmd)
|
||||
|
||||
root.AddCommand(scoreCmd)
|
||||
}
|
||||
```
|
||||
|
||||
## Accessing Core Services
|
||||
|
||||
```go
|
||||
func runMyCommand(cmd *cli.Command, args []string) error {
|
||||
ctx := cli.Context() // Root context (cancelled on signal)
|
||||
core := cli.Core() // Framework Core instance
|
||||
root := cli.RootCmd() // Root cobra command
|
||||
|
||||
// Type-safe service retrieval
|
||||
ws, err := framework.ServiceFor[*workspace.Service](core)
|
||||
if err != nil {
|
||||
return cli.WrapVerb(err, "get", "workspace service")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## Built-in Services
|
||||
|
||||
| Service | Name | Purpose |
|
||||
|---------|------|---------|
|
||||
| i18n | `i18n` | Internationalisation, grammar |
|
||||
| log | `log` | Structured logging (slog) |
|
||||
| crypt | `crypt` | OpenPGP encryption |
|
||||
| workspace | `workspace` | Project root detection |
|
||||
| signal | `signal` | SIGINT/SIGTERM/SIGHUP handling |
|
||||
|
||||
## `Main()` Lifecycle
|
||||
|
||||
1. Recovery defer (catches panics)
|
||||
2. Register core services (i18n, log, crypt, workspace)
|
||||
3. Append user command services
|
||||
4. `Init()` — creates cobra root, signals, starts all services
|
||||
5. Add completion command
|
||||
6. `Execute()` — runs matched command
|
||||
7. `Shutdown()` — stops all services in reverse order
|
||||
|
||||
## Legacy: `RegisterCommands`
|
||||
|
||||
For packages that need `init()`-time registration (not recommended):
|
||||
|
||||
```go
|
||||
func init() {
|
||||
cli.RegisterCommands(func(root *cobra.Command) {
|
||||
root.AddCommand(myCmd)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Prefer `WithCommands` — it's explicit and doesn't rely on import side effects.
|
||||
|
||||
## Building a CLI Binary
|
||||
|
||||
```go
|
||||
// cmd/lem/main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
"forge.lthn.ai/lthn/lem/cmd/lemcmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cli.WithAppName("lem")
|
||||
cli.Main(lemcmd.Commands()...)
|
||||
}
|
||||
```
|
||||
|
||||
Where `Commands()` returns `[]framework.Option`:
|
||||
|
||||
```go
|
||||
func Commands() []framework.Option {
|
||||
return []framework.Option{
|
||||
cli.WithCommands("score", addScoreCommands),
|
||||
cli.WithCommands("gen", addGenCommands),
|
||||
cli.WithCommands("data", addDataCommands),
|
||||
}
|
||||
}
|
||||
```
|
||||
41
Home.md
Normal file
41
Home.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Core CLI Framework (`pkg/cli`)
|
||||
|
||||
Go 1.26 CLI framework built on cobra + bubbletea + lipgloss. Provides command registration, flag helpers, styled output, streaming, daemon mode, and TUI components.
|
||||
|
||||
**Import:** `forge.lthn.ai/core/cli/pkg/cli`
|
||||
|
||||
## Quick Start
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import "forge.lthn.ai/core/cli/pkg/cli"
|
||||
|
||||
func main() {
|
||||
cli.WithAppName("myapp")
|
||||
cli.Main(
|
||||
cli.WithCommands("greet", addGreetCommands),
|
||||
)
|
||||
}
|
||||
|
||||
func addGreetCommands(root *cli.Command) {
|
||||
cmd := cli.NewCommand("greet", "Say hello", "", func(cmd *cli.Command, args []string) error {
|
||||
cli.Success("Hello, world!")
|
||||
return nil
|
||||
})
|
||||
root.AddCommand(cmd)
|
||||
}
|
||||
```
|
||||
|
||||
## Pages
|
||||
|
||||
- [[Command-Builders]] — NewCommand, NewGroup, NewRun, flag helpers
|
||||
- [[Output-Functions]] — Success, Error, Warn, Info, Progress, Section, Label
|
||||
- [[Error-Handling]] — Err, Wrap, WrapVerb, ExitError, Is/As/Join
|
||||
- [[Streaming]] — Stream for token-by-token rendering
|
||||
- [[Daemon-Mode]] — PIDFile, HealthServer, Daemon runner
|
||||
- [[Prompt-Input]] — Prompt, Select, MultiSelect
|
||||
- [[Runtime]] — Init, Main, Shutdown, Context, signal handling
|
||||
- [[Framework-Integration]] — WithCommands, Core lifecycle, services
|
||||
- [[Styles-and-Glyphs]] — ANSI styles, glyph shortcodes, colours
|
||||
- [[Frame-TUI]] — Bubbletea AppShell with focus, navigation, keymaps
|
||||
63
Output-Functions.md
Normal file
63
Output-Functions.md
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
# Output Functions
|
||||
|
||||
All output functions support glyph shortcodes (`:check:`, `:cross:`, `:warn:`, `:info:`) which are auto-converted to Unicode symbols.
|
||||
|
||||
## Styled Output
|
||||
|
||||
```go
|
||||
cli.Success("All tests passed") // ✓ All tests passed (green)
|
||||
cli.Successf("Built %d files", n) // ✓ Built 5 files (green)
|
||||
cli.Error("Connection refused") // ✗ Connection refused (red, stderr)
|
||||
cli.Errorf("Port %d in use", port) // ✗ Port 8080 in use (red, stderr)
|
||||
cli.Warn("Deprecated flag used") // ⚠ Deprecated flag used (amber, stderr)
|
||||
cli.Warnf("Skipping %s", name) // ⚠ Skipping foo (amber, stderr)
|
||||
cli.Info("Using default config") // ℹ Using default config (blue)
|
||||
cli.Infof("Found %d items", n) // ℹ Found 42 items (blue)
|
||||
cli.Dim("Optional detail") // Optional detail (gray)
|
||||
```
|
||||
|
||||
## Plain Output
|
||||
|
||||
```go
|
||||
cli.Println("Hello %s", name) // fmt.Sprintf + glyph conversion + newline
|
||||
cli.Print("Loading...") // No newline
|
||||
cli.Text("raw", "text") // Like fmt.Println but with glyphs
|
||||
cli.Blank() // Empty line
|
||||
cli.Echo("key.label", args...) // i18n.T translation + newline
|
||||
```
|
||||
|
||||
## Structured Output
|
||||
|
||||
```go
|
||||
cli.Label("version", "1.2.0") // Version: 1.2.0 (styled key)
|
||||
cli.Task("php", "Running tests") // [php] Running tests
|
||||
cli.Section("audit") // ── AUDIT ──
|
||||
cli.Hint("fix", "go mod tidy") // fix: go mod tidy
|
||||
cli.Result(passed, "Tests passed") // ✓ or ✗ based on bool
|
||||
```
|
||||
|
||||
## Progress
|
||||
|
||||
```go
|
||||
for i, item := range items {
|
||||
cli.Progress("check", i+1, len(items), item.Name) // Overwrites line
|
||||
}
|
||||
cli.ProgressDone() // Clears progress line
|
||||
```
|
||||
|
||||
## Severity
|
||||
|
||||
```go
|
||||
cli.Severity("critical", "SQL injection found") // [critical] red+bold
|
||||
cli.Severity("high", "XSS vulnerability") // [high] orange+bold
|
||||
cli.Severity("medium", "Missing CSRF token") // [medium] amber
|
||||
cli.Severity("low", "Debug mode enabled") // [low] gray
|
||||
```
|
||||
|
||||
## Error Wrapping for Output
|
||||
|
||||
```go
|
||||
cli.ErrorWrap(err, "load config") // ✗ load config: <error>
|
||||
cli.ErrorWrapVerb(err, "load", "config") // ✗ Failed to load config: <error>
|
||||
cli.ErrorWrapAction(err, "connect") // ✗ Failed to connect: <error>
|
||||
```
|
||||
42
Prompt-Input.md
Normal file
42
Prompt-Input.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# Prompt & Input
|
||||
|
||||
## Text Prompt
|
||||
|
||||
```go
|
||||
name, err := cli.Prompt("Project name", "my-app")
|
||||
// Project name [my-app]: _
|
||||
// Returns default if user presses Enter
|
||||
```
|
||||
|
||||
## Single Select
|
||||
|
||||
```go
|
||||
choice, err := cli.Select("Choose backend:", []string{"metal", "rocm", "cpu"})
|
||||
// Choose backend:
|
||||
// 1. metal
|
||||
// 2. rocm
|
||||
// 3. cpu
|
||||
// Choose [1-3]: _
|
||||
```
|
||||
|
||||
## Multi Select
|
||||
|
||||
```go
|
||||
tags, err := cli.MultiSelect("Enable features:", []string{"auth", "api", "admin", "mcp"})
|
||||
// Enable features:
|
||||
// 1. auth
|
||||
// 2. api
|
||||
// 3. admin
|
||||
// 4. mcp
|
||||
// Choose (space-separated) [1-4]: _
|
||||
// Returns []string of selected items
|
||||
```
|
||||
|
||||
## Confirm (via Prompt)
|
||||
|
||||
```go
|
||||
answer, _ := cli.Prompt("Proceed? (y/n)", "y")
|
||||
if answer == "y" || answer == "Y" {
|
||||
// proceed
|
||||
}
|
||||
```
|
||||
60
Runtime.md
Normal file
60
Runtime.md
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
# Runtime
|
||||
|
||||
## Initialisation
|
||||
|
||||
```go
|
||||
// Simple (recommended)
|
||||
cli.WithAppName("myapp")
|
||||
cli.Main(
|
||||
cli.WithCommands("cmd", addCommands),
|
||||
)
|
||||
|
||||
// Manual (advanced)
|
||||
cli.Init(cli.Options{
|
||||
AppName: "myapp",
|
||||
Version: "1.0.0",
|
||||
Services: []framework.Option{...},
|
||||
OnReload: func() error { return reloadConfig() },
|
||||
})
|
||||
defer cli.Shutdown()
|
||||
cli.Execute()
|
||||
```
|
||||
|
||||
## Global Accessors
|
||||
|
||||
| Function | Returns | Description |
|
||||
|----------|---------|-------------|
|
||||
| `cli.Core()` | `*framework.Core` | Framework container |
|
||||
| `cli.RootCmd()` | `*cobra.Command` | Root cobra command |
|
||||
| `cli.Context()` | `context.Context` | Cancelled on SIGINT/SIGTERM |
|
||||
| `cli.Execute()` | `error` | Run matched command |
|
||||
| `cli.Shutdown()` | — | Stop all services |
|
||||
|
||||
## Version Info
|
||||
|
||||
Set via ldflags at build time:
|
||||
|
||||
```go
|
||||
cli.AppVersion // "1.2.0"
|
||||
cli.BuildCommit // "df94c24"
|
||||
cli.BuildDate // "2026-02-06"
|
||||
cli.BuildPreRelease // "dev.8"
|
||||
cli.SemVer() // "1.2.0-dev.8+df94c24.20260206"
|
||||
```
|
||||
|
||||
Build command:
|
||||
|
||||
```bash
|
||||
go build -ldflags="-X forge.lthn.ai/core/cli/pkg/cli.AppVersion=1.2.0 \
|
||||
-X forge.lthn.ai/core/cli/pkg/cli.BuildCommit=$(git rev-parse --short HEAD) \
|
||||
-X forge.lthn.ai/core/cli/pkg/cli.BuildDate=$(date +%Y-%m-%d)"
|
||||
```
|
||||
|
||||
## Signal Handling
|
||||
|
||||
Automatic via the `signalService` registered during `Init()`:
|
||||
|
||||
- **SIGINT/SIGTERM** → cancels `cli.Context()`
|
||||
- **SIGHUP** → calls `OnReload` if configured
|
||||
|
||||
No manual signal handling needed in commands — use `cli.Context()` for cancellation.
|
||||
52
Streaming.md
Normal file
52
Streaming.md
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# Streaming Text Output
|
||||
|
||||
The `Stream` type renders growing text as tokens arrive, with optional word-wrap. Thread-safe for a single producer goroutine.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```go
|
||||
stream := cli.NewStream()
|
||||
go func() {
|
||||
for token := range tokens {
|
||||
stream.Write(token)
|
||||
}
|
||||
stream.Done()
|
||||
}()
|
||||
stream.Wait()
|
||||
```
|
||||
|
||||
## Word Wrap
|
||||
|
||||
```go
|
||||
stream := cli.NewStream(cli.WithWordWrap(80))
|
||||
```
|
||||
|
||||
## Custom Output
|
||||
|
||||
```go
|
||||
var buf strings.Builder
|
||||
stream := cli.NewStream(cli.WithStreamOutput(&buf))
|
||||
// ... write tokens ...
|
||||
stream.Done()
|
||||
result := stream.Captured() // or buf.String()
|
||||
```
|
||||
|
||||
## Reading from io.Reader
|
||||
|
||||
```go
|
||||
stream := cli.NewStream(cli.WithWordWrap(120))
|
||||
err := stream.WriteFrom(resp.Body)
|
||||
stream.Done()
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `NewStream(opts...)` | Create stream with options |
|
||||
| `Write(text)` | Append text (thread-safe) |
|
||||
| `WriteFrom(r)` | Stream from io.Reader until EOF |
|
||||
| `Done()` | Signal completion (adds trailing newline if needed) |
|
||||
| `Wait()` | Block until Done is called |
|
||||
| `Column()` | Current column position |
|
||||
| `Captured()` | Get output as string (when using Builder) |
|
||||
Loading…
Add table
Reference in a new issue