byob-go-cli

IOStreams wraps In/Out/ErrOut + TTY flags; commands never touch os.Std*

byob-iostreams.1 iostreams

Problem: fmt.Println, os.Stdout.Write, and friends scattered across a codebase are impossible to capture in tests and impossible to redirect consistently (e.g., to suppress output in a JSON mode).

Idea: define a small IOStreams struct with In io.Reader, Out io.Writer, ErrOut io.Writer, plus TTY flags for all three streams. Every command writes through its *IOStreams. The only place in the codebase that touches os.Stdin / os.Stdout / os.Stderr is iostreams.System(), called once in main().

All three streams carry their own TTY flag. Stdin's flag isn't just symmetry: the Prompter (byob-prompter) consults IsStdinTTY() to decide whether to prompt at all, so this decision has to expose it alongside the output-stream flags.

Tradeoffs: you pay a tiny indirection. You also gain the ability to swap buffers in tests, suppress chatter in JSON mode, and ask IsStdoutTTY() without sprinkling isatty checks across the codebase.

Design

type IOStreams struct {
    In     io.Reader
    Out    io.Writer
    ErrOut io.Writer

    stdinIsTTY  bool
    stdoutIsTTY bool
    stderrIsTTY bool
    colorScheme *ColorScheme
}

func (s *IOStreams) IsStdinTTY() bool  { return s.stdinIsTTY }
func (s *IOStreams) IsStdoutTTY() bool { return s.stdoutIsTTY }
func (s *IOStreams) IsStderrTTY() bool { return s.stderrIsTTY }
func (s *IOStreams) ColorScheme() *ColorScheme { return s.colorScheme }

func System() *IOStreams {
    return &IOStreams{
        In:  os.Stdin,
        Out: os.Stdout,
        ErrOut: os.Stderr,
        stdinIsTTY:  isatty.IsTerminal(os.Stdin.Fd()),
        stdoutIsTTY: isatty.IsTerminal(os.Stdout.Fd()),
        stderrIsTTY: isatty.IsTerminal(os.Stderr.Fd()),
    }
}