replace globalOptions.stdout with termstatus.OutputWriter

This commit is contained in:
Michael Eischer
2025-09-20 23:06:28 +02:00
parent c293736841
commit 76b2cdd4fb
29 changed files with 79 additions and 85 deletions

View File

@@ -161,7 +161,7 @@ func runCache(opts CacheOptions, gopts GlobalOptions, args []string, term ui.Ter
}) })
} }
_ = tab.Write(gopts.stdout) _ = tab.Write(gopts.term.OutputWriter())
printer.S("%d cache dirs in %s", len(dirs), cachedir) printer.S("%d cache dirs in %s", len(dirs), cachedir)
return nil return nil

View File

@@ -23,8 +23,7 @@ func testRunCheckMustFail(t testing.TB, gopts GlobalOptions) {
} }
func testRunCheckOutput(t testing.TB, gopts GlobalOptions, checkUnused bool) (string, error) { func testRunCheckOutput(t testing.TB, gopts GlobalOptions, checkUnused bool) (string, error) {
buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error { buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
opts := CheckOptions{ opts := CheckOptions{
ReadData: true, ReadData: true,
CheckUnused: checkUnused, CheckUnused: checkUnused,
@@ -32,6 +31,5 @@ func testRunCheckOutput(t testing.TB, gopts GlobalOptions, checkUnused bool) (st
_, err := runCheck(context.TODO(), opts, gopts, nil, gopts.term) _, err := runCheck(context.TODO(), opts, gopts, nil, gopts.term)
return err return err
}) })
})
return buf.String(), err return buf.String(), err
} }

View File

@@ -200,20 +200,20 @@ func runDebugDump(ctx context.Context, gopts GlobalOptions, args []string, term
switch tpe { switch tpe {
case "indexes": case "indexes":
return dumpIndexes(ctx, repo, gopts.stdout, printer) return dumpIndexes(ctx, repo, gopts.term.OutputWriter(), printer)
case "snapshots": case "snapshots":
return debugPrintSnapshots(ctx, repo, gopts.stdout) return debugPrintSnapshots(ctx, repo, gopts.term.OutputWriter())
case "packs": case "packs":
return printPacks(ctx, repo, gopts.stdout, printer) return printPacks(ctx, repo, gopts.term.OutputWriter(), printer)
case "all": case "all":
printer.S("snapshots:") printer.S("snapshots:")
err := debugPrintSnapshots(ctx, repo, gopts.stdout) err := debugPrintSnapshots(ctx, repo, gopts.term.OutputWriter())
if err != nil { if err != nil {
return err return err
} }
printer.S("indexes:") printer.S("indexes:")
err = dumpIndexes(ctx, repo, gopts.stdout, printer) err = dumpIndexes(ctx, repo, gopts.term.OutputWriter(), printer)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -424,7 +424,7 @@ func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args []
} }
if gopts.JSON { if gopts.JSON {
enc := json.NewEncoder(gopts.stdout) enc := json.NewEncoder(gopts.term.OutputWriter())
c.printChange = func(change *Change) { c.printChange = func(change *Change) {
err := enc.Encode(change) err := enc.Encode(change)
if err != nil { if err != nil {
@@ -458,7 +458,7 @@ func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args []
updateBlobs(repo, stats.BlobsAfter.Sub(both).Sub(stats.BlobsCommon), &stats.Added, printer.E) updateBlobs(repo, stats.BlobsAfter.Sub(both).Sub(stats.BlobsCommon), &stats.Added, printer.E)
if gopts.JSON { if gopts.JSON {
err := json.NewEncoder(gopts.stdout).Encode(stats) err := json.NewEncoder(gopts.term.OutputWriter()).Encode(stats)
if err != nil { if err != nil {
printer.E("JSON encode failed: %v", err) printer.E("JSON encode failed: %v", err)
} }

View File

@@ -15,14 +15,12 @@ import (
) )
func testRunDiffOutput(t testing.TB, gopts GlobalOptions, firstSnapshotID string, secondSnapshotID string) (string, error) { func testRunDiffOutput(t testing.TB, gopts GlobalOptions, firstSnapshotID string, secondSnapshotID string) (string, error) {
buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error { buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
opts := DiffOptions{ opts := DiffOptions{
ShowMetadata: false, ShowMetadata: false,
} }
return withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runDiff(ctx, opts, gopts, []string{firstSnapshotID, secondSnapshotID}, gopts.term) return runDiff(ctx, opts, gopts, []string{firstSnapshotID, secondSnapshotID}, gopts.term)
}) })
})
return buf.String(), err return buf.String(), err
} }

View File

@@ -49,7 +49,7 @@ Exit status is 1 if there was any error.
for _, flag := range flags { for _, flag := range flags {
tab.AddRow(flag) tab.AddRow(flag)
} }
return tab.Write(globalOptions.stdout) return tab.Write(globalOptions.term.OutputWriter())
}, },
} }

View File

@@ -11,13 +11,11 @@ import (
) )
func testRunFind(t testing.TB, wantJSON bool, opts FindOptions, gopts GlobalOptions, pattern string) []byte { func testRunFind(t testing.TB, wantJSON bool, opts FindOptions, gopts GlobalOptions, pattern string) []byte {
buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error { buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
gopts.JSON = wantJSON gopts.JSON = wantJSON
return withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runFind(ctx, opts, gopts, []string{pattern}, gopts.term) return runFind(ctx, opts, gopts, []string{pattern}, gopts.term)
}) })
})
rtest.OK(t, err) rtest.OK(t, err)
return buf.Bytes() return buf.Bytes()
} }

View File

@@ -251,7 +251,7 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
} }
if gopts.Verbose >= 1 && !gopts.JSON { if gopts.Verbose >= 1 && !gopts.JSON {
err = PrintSnapshotGroupHeader(gopts.stdout, k) err = PrintSnapshotGroupHeader(gopts.term.OutputWriter(), k)
if err != nil { if err != nil {
return err return err
} }
@@ -274,7 +274,7 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
} }
if len(keep) != 0 && !gopts.Quiet && !gopts.JSON { if len(keep) != 0 && !gopts.Quiet && !gopts.JSON {
printer.P("keep %d snapshots:\n", len(keep)) printer.P("keep %d snapshots:\n", len(keep))
if err := PrintSnapshots(gopts.stdout, keep, reasons, opts.Compact); err != nil { if err := PrintSnapshots(gopts.term.OutputWriter(), keep, reasons, opts.Compact); err != nil {
return err return err
} }
printer.P("\n") printer.P("\n")
@@ -283,7 +283,7 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
if len(remove) != 0 && !gopts.Quiet && !gopts.JSON { if len(remove) != 0 && !gopts.Quiet && !gopts.JSON {
printer.P("remove %d snapshots:\n", len(remove)) printer.P("remove %d snapshots:\n", len(remove))
if err := PrintSnapshots(gopts.stdout, remove, nil, opts.Compact); err != nil { if err := PrintSnapshots(gopts.term.OutputWriter(), remove, nil, opts.Compact); err != nil {
return err return err
} }
printer.P("\n") printer.P("\n")
@@ -328,7 +328,7 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
} }
if gopts.JSON && len(jsonGroups) > 0 { if gopts.JSON && len(jsonGroups) > 0 {
err = printJSONForget(gopts.stdout, jsonGroups) err = printJSONForget(gopts.term.OutputWriter(), jsonGroups)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -84,7 +84,7 @@ func writeCompletion(filename string, shell string, generate func(w io.Writer) e
defer func() { err = outFile.Close() }() defer func() { err = outFile.Close() }()
outWriter = outFile outWriter = outFile
} else { } else {
outWriter = gopts.stdout outWriter = gopts.term.OutputWriter()
} }
err = generate(outWriter) err = generate(outWriter)

View File

@@ -9,11 +9,9 @@ import (
) )
func testRunGenerate(t testing.TB, gopts GlobalOptions, opts generateOptions) ([]byte, error) { func testRunGenerate(t testing.TB, gopts GlobalOptions, opts generateOptions) ([]byte, error) {
buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error { buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runGenerate(opts, gopts, []string{}, gopts.term) return runGenerate(opts, gopts, []string{}, gopts.term)
}) })
})
return buf.Bytes(), err return buf.Bytes(), err
} }

View File

@@ -132,7 +132,7 @@ func runInit(ctx context.Context, opts InitOptions, gopts GlobalOptions, args []
ID: s.Config().ID, ID: s.Config().ID,
Repository: location.StripPassword(gopts.backends, gopts.Repo), Repository: location.StripPassword(gopts.backends, gopts.Repo),
} }
return json.NewEncoder(gopts.stdout).Encode(status) return json.NewEncoder(gopts.term.OutputWriter()).Encode(status)
} }
return nil return nil

View File

@@ -16,11 +16,9 @@ import (
) )
func testRunKeyListOtherIDs(t testing.TB, gopts GlobalOptions) []string { func testRunKeyListOtherIDs(t testing.TB, gopts GlobalOptions) []string {
buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error { buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runKeyList(ctx, gopts, []string{}, gopts.term) return runKeyList(ctx, gopts, []string{}, gopts.term)
}) })
})
rtest.OK(t, err) rtest.OK(t, err)
scanner := bufio.NewScanner(buf) scanner := bufio.NewScanner(buf)

View File

@@ -95,7 +95,7 @@ func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions
} }
if gopts.JSON { if gopts.JSON {
return json.NewEncoder(gopts.stdout).Encode(keys) return json.NewEncoder(gopts.term.OutputWriter()).Encode(keys)
} }
tab := table.New() tab := table.New()
@@ -108,5 +108,5 @@ func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions
tab.AddRow(key) tab.AddRow(key)
} }
return tab.Write(gopts.stdout) return tab.Write(gopts.term.OutputWriter())
} }

View File

@@ -11,11 +11,9 @@ import (
) )
func testRunList(t testing.TB, gopts GlobalOptions, tpe string) restic.IDs { func testRunList(t testing.TB, gopts GlobalOptions, tpe string) restic.IDs {
buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error { buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runList(ctx, gopts, []string{tpe}, gopts.term) return runList(ctx, gopts, []string{tpe}, gopts.term)
}) })
})
rtest.OK(t, err) rtest.OK(t, err)
return parseIDsFromReader(t, buf) return parseIDsFromReader(t, buf)
} }

View File

@@ -382,11 +382,11 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri
if gopts.JSON { if gopts.JSON {
printer = &jsonLsPrinter{ printer = &jsonLsPrinter{
enc: json.NewEncoder(gopts.stdout), enc: json.NewEncoder(gopts.term.OutputWriter()),
} }
} else if opts.Ncdu { } else if opts.Ncdu {
printer = &ncduLsPrinter{ printer = &ncduLsPrinter{
out: gopts.stdout, out: gopts.term.OutputWriter(),
} }
} else { } else {
printer = &textLsPrinter{ printer = &textLsPrinter{

View File

@@ -13,12 +13,10 @@ import (
) )
func testRunLsWithOpts(t testing.TB, gopts GlobalOptions, opts LsOptions, args []string) []byte { func testRunLsWithOpts(t testing.TB, gopts GlobalOptions, opts LsOptions, args []string) []byte {
buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error { buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
gopts.Quiet = true gopts.Quiet = true
return withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runLs(context.TODO(), opts, gopts, args, gopts.term) return runLs(context.TODO(), opts, gopts, args, gopts.term)
}) })
})
rtest.OK(t, err) rtest.OK(t, err)
return buf.Bytes() return buf.Bytes()
} }

View File

@@ -89,7 +89,7 @@ func createPrunableRepo(t *testing.T, env *testEnvironment) {
} }
func testRunForgetJSON(t testing.TB, gopts GlobalOptions, args ...string) { func testRunForgetJSON(t testing.TB, gopts GlobalOptions, args ...string) {
buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error { buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
gopts.JSON = true gopts.JSON = true
opts := ForgetOptions{ opts := ForgetOptions{
DryRun: true, DryRun: true,
@@ -98,10 +98,8 @@ func testRunForgetJSON(t testing.TB, gopts GlobalOptions, args ...string) {
pruneOpts := PruneOptions{ pruneOpts := PruneOptions{
MaxUnused: "5%", MaxUnused: "5%",
} }
return withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runForget(context.TODO(), opts, pruneOpts, gopts, gopts.term, args) return runForget(context.TODO(), opts, pruneOpts, gopts, gopts.term, args)
}) })
})
rtest.OK(t, err) rtest.OK(t, err)
var forgets []*ForgetGroup var forgets []*ForgetGroup

View File

@@ -17,7 +17,7 @@ import (
func testRunRebuildIndex(t testing.TB, gopts GlobalOptions) { func testRunRebuildIndex(t testing.TB, gopts GlobalOptions) {
rtest.OK(t, withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error { rtest.OK(t, withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
gopts.stdout = io.Discard gopts.Quiet = true
return runRebuildIndex(context.TODO(), RepairIndexOptions{}, gopts, gopts.term) return runRebuildIndex(context.TODO(), RepairIndexOptions{}, gopts, gopts.term)
})) }))
} }
@@ -129,7 +129,7 @@ func TestRebuildIndexFailsOnAppendOnly(t *testing.T) {
return &appendOnlyBackend{r}, nil return &appendOnlyBackend{r}, nil
} }
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error { err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error {
gopts.stdout = io.Discard gopts.Quiet = true
return runRebuildIndex(context.TODO(), RepairIndexOptions{}, gopts, gopts.term) return runRebuildIndex(context.TODO(), RepairIndexOptions{}, gopts, gopts.term)
}) })

View File

@@ -103,7 +103,7 @@ func runSnapshots(ctx context.Context, opts SnapshotOptions, gopts GlobalOptions
} }
if gopts.JSON { if gopts.JSON {
err := printSnapshotGroupJSON(gopts.stdout, snapshotGroups, grouped) err := printSnapshotGroupJSON(gopts.term.OutputWriter(), snapshotGroups, grouped)
if err != nil { if err != nil {
printer.E("error printing snapshots: %v", err) printer.E("error printing snapshots: %v", err)
} }
@@ -116,12 +116,12 @@ func runSnapshots(ctx context.Context, opts SnapshotOptions, gopts GlobalOptions
} }
if grouped { if grouped {
err := PrintSnapshotGroupHeader(gopts.stdout, k) err := PrintSnapshotGroupHeader(gopts.term.OutputWriter(), k)
if err != nil { if err != nil {
return err return err
} }
} }
err := PrintSnapshots(gopts.stdout, list, nil, opts.Compact) err := PrintSnapshots(gopts.term.OutputWriter(), list, nil, opts.Compact)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -10,14 +10,12 @@ import (
) )
func testRunSnapshots(t testing.TB, gopts GlobalOptions) (newest *Snapshot, snapmap map[restic.ID]Snapshot) { func testRunSnapshots(t testing.TB, gopts GlobalOptions) (newest *Snapshot, snapmap map[restic.ID]Snapshot) {
buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error { buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
gopts.JSON = true gopts.JSON = true
opts := SnapshotOptions{} opts := SnapshotOptions{}
return withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runSnapshots(ctx, opts, gopts, []string{}, gopts.term) return runSnapshots(ctx, opts, gopts, []string{}, gopts.term)
}) })
})
rtest.OK(t, err) rtest.OK(t, err)
snapshots := []Snapshot{} snapshots := []Snapshot{}

View File

@@ -169,7 +169,7 @@ func runStats(ctx context.Context, opts StatsOptions, gopts GlobalOptions, args
} }
if gopts.JSON { if gopts.JSON {
err = json.NewEncoder(gopts.stdout).Encode(stats) err = json.NewEncoder(gopts.term.OutputWriter()).Encode(stats)
if err != nil { if err != nil {
return fmt.Errorf("encoding output: %v", err) return fmt.Errorf("encoding output: %v", err)
} }

View File

@@ -43,7 +43,7 @@ Exit status is 1 if there was any error.
GoArch: runtime.GOARCH, GoArch: runtime.GOARCH,
} }
err := json.NewEncoder(globalOptions.stdout).Encode(jsonS) err := json.NewEncoder(globalOptions.term.OutputWriter()).Encode(jsonS)
if err != nil { if err != nil {
printer.E("JSON encode failed: %v\n", err) printer.E("JSON encode failed: %v\n", err)
return return

View File

@@ -3,7 +3,6 @@ package main
import ( import (
"context" "context"
"fmt" "fmt"
"io"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@@ -74,7 +73,6 @@ type GlobalOptions struct {
limiter.Limits limiter.Limits
password string password string
stdout io.Writer
term ui.Terminal term ui.Terminal
backends *location.Registry backends *location.Registry

View File

@@ -212,10 +212,6 @@ func withTestEnvironment(t testing.TB) (env *testEnvironment, cleanup func()) {
Quiet: true, Quiet: true,
CacheDir: env.cache, CacheDir: env.cache,
password: rtest.TestPassword, password: rtest.TestPassword,
// stdout and stderr are written to by Warnf etc. That is the written data
// usually consists of one or multiple lines and therefore can be handled well
// by t.Log.
stdout: &logOutputter{t},
extended: make(options.Options), extended: make(options.Options),
// replace this hook with "nil" if listing a filetype more than once is necessary // replace this hook with "nil" if listing a filetype more than once is necessary
@@ -416,18 +412,24 @@ func testFileSize(filename string, size int64) error {
return nil return nil
} }
func withCaptureStdout(gopts GlobalOptions, inner func(gopts GlobalOptions) error) (*bytes.Buffer, error) { func withCaptureStdout(t testing.TB, gopts GlobalOptions, callback func(ctx context.Context, gopts GlobalOptions) error) (*bytes.Buffer, error) {
buf := bytes.NewBuffer(nil) buf := bytes.NewBuffer(nil)
gopts.stdout = buf err := withTermStatusRaw(os.Stdin, buf, &logOutputter{t: t}, gopts, callback)
err := inner(gopts)
return buf, err return buf, err
} }
func withTermStatus(t testing.TB, gopts GlobalOptions, callback func(ctx context.Context, gopts GlobalOptions) error) error { func withTermStatus(t testing.TB, gopts GlobalOptions, callback func(ctx context.Context, gopts GlobalOptions) error) error {
// stdout and stderr are written to by printer functions etc. That is the written data
// usually consists of one or multiple lines and therefore can be handled well
// by t.Log.
return withTermStatusRaw(os.Stdin, &logOutputter{t: t}, &logOutputter{t: t}, gopts, callback)
}
func withTermStatusRaw(stdin io.ReadCloser, stdout, stderr io.Writer, gopts GlobalOptions, callback func(ctx context.Context, gopts GlobalOptions) error) error {
ctx, cancel := context.WithCancel(context.TODO()) ctx, cancel := context.WithCancel(context.TODO())
var wg sync.WaitGroup var wg sync.WaitGroup
term := termstatus.New(os.Stdin, gopts.stdout, &logOutputter{t: t}, gopts.Quiet) term := termstatus.New(stdin, stdout, stderr, gopts.Quiet)
gopts.term = term gopts.term = term
wg.Add(1) wg.Add(1)
go func() { go func() {

View File

@@ -173,13 +173,11 @@ func main() {
version, runtime.Version(), runtime.GOOS, runtime.GOARCH) version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
globalOptions := GlobalOptions{ globalOptions := GlobalOptions{
stdout: os.Stdout,
backends: collectBackends(), backends: collectBackends(),
} }
func() { func() {
term, cancel := termstatus.Setup(os.Stdin, os.Stdout, os.Stderr, globalOptions.Quiet) term, cancel := termstatus.Setup(os.Stdin, os.Stdout, os.Stderr, globalOptions.Quiet)
defer cancel() defer cancel()
globalOptions.stdout = termstatus.WrapStdout(term)
globalOptions.term = term globalOptions.term = term
ctx := createGlobalContext(os.Stderr) ctx := createGlobalContext(os.Stderr)
err = newRootCommand(&globalOptions).ExecuteContext(ctx) err = newRootCommand(&globalOptions).ExecuteContext(ctx)

View File

@@ -40,6 +40,10 @@ func (m *MockTerminal) ReadPassword(_ context.Context, _ string) (string, error)
return "password", nil return "password", nil
} }
func (m *MockTerminal) OutputWriter() io.Writer {
return nil
}
func (m *MockTerminal) OutputRaw() io.Writer { func (m *MockTerminal) OutputRaw() io.Writer {
return nil return nil
} }

View File

@@ -16,9 +16,12 @@ type Terminal interface {
SetStatus(lines []string) SetStatus(lines []string)
// CanUpdateStatus returns true if the terminal can update the status lines. // CanUpdateStatus returns true if the terminal can update the status lines.
CanUpdateStatus() bool CanUpdateStatus() bool
InputRaw() io.ReadCloser InputRaw() io.ReadCloser
InputIsTerminal() bool InputIsTerminal() bool
ReadPassword(ctx context.Context, prompt string) (string, error) ReadPassword(ctx context.Context, prompt string) (string, error)
OutputWriter() io.Writer
// OutputRaw returns the output writer. Should only be used if there is no // OutputRaw returns the output writer. Should only be used if there is no
// other option. Must not be used in combination with Print, Error, SetStatus // other option. Must not be used in combination with Print, Error, SetStatus
// or any other method that writes to the terminal. // or any other method that writes to the terminal.

View File

@@ -30,6 +30,9 @@ type Terminal struct {
outputIsTerminal bool outputIsTerminal bool
canUpdateStatus bool canUpdateStatus bool
outputWriter io.WriteCloser
outputWriterOnce sync.Once
// will be closed when the goroutine which runs Run() terminates, so it'll // will be closed when the goroutine which runs Run() terminates, so it'll
// yield a default value immediately // yield a default value immediately
closed chan struct{} closed chan struct{}
@@ -73,6 +76,9 @@ func Setup(stdin io.ReadCloser, stdout, stderr io.Writer, quiet bool) (*Terminal
}() }()
return term, func() { return term, func() {
if term.outputWriter != nil {
_ = term.outputWriter.Close()
}
// shutdown termstatus // shutdown termstatus
cancel() cancel()
wg.Wait() wg.Wait()
@@ -158,6 +164,13 @@ func (t *Terminal) CanUpdateStatus() bool {
return t.canUpdateStatus return t.canUpdateStatus
} }
func (t *Terminal) OutputWriter() io.Writer {
t.outputWriterOnce.Do(func() {
t.outputWriter = newLineWriter(t.Print)
})
return t.outputWriter
}
// OutputRaw returns the output writer. Should only be used if there is no // OutputRaw returns the output writer. Should only be used if there is no
// other option. Must not be used in combination with Print, Error, SetStatus // other option. Must not be used in combination with Print, Error, SetStatus
// or any other method that writes to the terminal. // or any other method that writes to the terminal.

View File

@@ -6,12 +6,6 @@ import (
"sync" "sync"
) )
// WrapStdout returns line-buffering replacements for os.Stdout.
// On Close, the remaining bytes are written, followed by a line break.
func WrapStdout(term *Terminal) (stdout io.WriteCloser) {
return newLineWriter(term.Print)
}
type lineWriter struct { type lineWriter struct {
m sync.Mutex m sync.Mutex
buf bytes.Buffer buf bytes.Buffer