fix(cli): add safe stream capture helper

Co-authored-by: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-02 13:07:28 +00:00
parent 4e9b42e7d0
commit be2e2db845
3 changed files with 22 additions and 11 deletions

View file

@ -41,10 +41,11 @@ var buf strings.Builder
stream := cli.NewStream(cli.WithStreamOutput(&buf)) stream := cli.NewStream(cli.WithStreamOutput(&buf))
// ... write tokens ... // ... write tokens ...
stream.Done() stream.Done()
result := stream.Captured() // or buf.String() result, ok := stream.CapturedOK() // or buf.String()
``` ```
`Captured()` returns the output as a string when using a `*strings.Builder` or any `fmt.Stringer`. `Captured()` returns the output as a string when using a `*strings.Builder` or any `fmt.Stringer`.
`CapturedOK()` reports whether capture is supported by the configured writer.
## Reading from `io.Reader` ## Reading from `io.Reader`
@ -68,7 +69,8 @@ stream.Done()
| `Done()` | Signal completion (adds trailing newline if needed) | | `Done()` | Signal completion (adds trailing newline if needed) |
| `Wait()` | Block until `Done` is called | | `Wait()` | Block until `Done` is called |
| `Column()` | Current column position | | `Column()` | Current column position |
| `Captured()` | Get output as string (requires `*strings.Builder` or `fmt.Stringer` writer) | | `Captured()` | Get output as string (returns `""` if capture is unsupported) |
| `CapturedOK()` | Get output and support status |
## Options ## Options

View file

@ -132,16 +132,24 @@ func (s *Stream) Column() int {
return s.col return s.col
} }
// Captured returns the stream output as a string when using a stringable writer. // Captured returns the stream output as a string when the output writer is
// Panics if the output writer is not a *strings.Builder or fmt.Stringer. // capture-capable. If the writer cannot be captured, it returns an empty string.
// Use CapturedOK when you need to distinguish that case.
func (s *Stream) Captured() string { func (s *Stream) Captured() string {
out, _ := s.CapturedOK()
return out
}
// CapturedOK returns the stream output and whether the configured writer
// supports capture.
func (s *Stream) CapturedOK() (string, bool) {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
if sb, ok := s.out.(*strings.Builder); ok { if sb, ok := s.out.(*strings.Builder); ok {
return sb.String() return sb.String(), true
} }
if st, ok := s.out.(fmt.Stringer); ok { if st, ok := s.out.(fmt.Stringer); ok {
return st.String() return st.String(), true
} }
panic("stream output writer does not support Capture") return "", false
} }

View file

@ -189,11 +189,12 @@ func TestStream_Bad(t *testing.T) {
assert.Equal(t, "", buf.String()) assert.Equal(t, "", buf.String())
}) })
t.Run("Captured panics when output is not stringable", func(t *testing.T) { t.Run("CapturedOK reports unsupported writers", func(t *testing.T) {
s := NewStream(WithStreamOutput(writerOnly{})) s := NewStream(WithStreamOutput(writerOnly{}))
assert.Panics(t, func() { got, ok := s.CapturedOK()
_ = s.Captured() assert.False(t, ok)
}) assert.Equal(t, "", got)
assert.Equal(t, "", s.Captured())
}) })
} }