termstatus: centralize OutputIsTerminal checks

This commit is contained in:
Michael Eischer
2025-09-14 17:58:52 +02:00
parent c745e4221e
commit 1ae2d08d1b
14 changed files with 85 additions and 68 deletions

View File

@@ -10,18 +10,11 @@ func StdinIsTerminal() bool {
return term.IsTerminal(int(os.Stdin.Fd()))
}
func StdoutIsTerminal() bool {
func OutputIsTerminal(fd uintptr) bool {
// mintty on windows can use pipes which behave like a posix terminal,
// but which are not a terminal handle
return term.IsTerminal(int(os.Stdout.Fd())) || StdoutCanUpdateStatus()
}
func StdoutCanUpdateStatus() bool {
return CanUpdateStatus(os.Stdout.Fd())
}
func StdoutWidth() int {
return Width(os.Stdout.Fd())
// but which are not a terminal handle. Thus also check `CanUpdateStatus`,
// which is able to detect such pipes.
return term.IsTerminal(int(fd)) || CanUpdateStatus(fd)
}
func Width(fd uintptr) int {

View File

@@ -30,8 +30,17 @@ func (m *Message) S(msg string, args ...interface{}) {
m.term.Print(fmt.Sprintf(msg, args...))
}
// P prints a message if verbosity >= 1 (neither --quiet nor --verbose is specified),
// this is used for normal messages which are not errors.
// PT prints a message if verbosity >= 1 (neither --quiet nor --verbose is specified)
// and stdout points to a terminal.
// This is used for informational messages.
func (m *Message) PT(msg string, args ...interface{}) {
if m.term.OutputIsTerminal() && m.v >= 1 {
m.term.Print(fmt.Sprintf(msg, args...))
}
}
// P prints a message if verbosity >= 1 (neither --quiet nor --verbose is specified).
// This is used for normal messages which are not errors.
func (m *Message) P(msg string, args ...interface{}) {
if m.v >= 1 {
m.term.Print(fmt.Sprintf(msg, args...))

View File

@@ -28,3 +28,7 @@ func (m *MockTerminal) CanUpdateStatus() bool {
func (m *MockTerminal) OutputRaw() io.Writer {
return nil
}
func (m *MockTerminal) OutputIsTerminal() bool {
return true
}

View File

@@ -19,6 +19,10 @@ type Printer interface {
// that are not errors. The message is even printed if --quiet is specified.
// Appends a newline if not present.
S(msg string, args ...interface{})
// PT prints a message if verbosity >= 1 (neither --quiet nor --verbose is specified)
// and stdout points to a terminal.
// This is used for informational messages.
PT(msg string, args ...interface{})
// P prints a message if verbosity >= 1 (neither --quiet nor --verbose is specified),
// this is used for normal messages which are not errors. Appends a newline if not present.
P(msg string, args ...interface{})
@@ -47,6 +51,8 @@ func (*NoopPrinter) E(_ string, _ ...interface{}) {}
func (*NoopPrinter) S(_ string, _ ...interface{}) {}
func (*NoopPrinter) PT(_ string, _ ...interface{}) {}
func (*NoopPrinter) P(_ string, _ ...interface{}) {}
func (*NoopPrinter) V(_ string, _ ...interface{}) {}
@@ -82,6 +88,10 @@ func (p *TestPrinter) S(msg string, args ...interface{}) {
p.t.Logf("stdout: "+msg, args...)
}
func (p *TestPrinter) PT(msg string, args ...interface{}) {
p.t.Logf("stdout(terminal): "+msg, args...)
}
func (p *TestPrinter) P(msg string, args ...interface{}) {
p.t.Logf("print: "+msg, args...)
}

View File

@@ -17,4 +17,5 @@ type Terminal interface {
// other option. Must not be used in combination with Print, Error, SetStatus
// or any other method that writes to the terminal.
OutputRaw() io.Writer
OutputIsTerminal() bool
}

View File

@@ -16,13 +16,14 @@ var _ ui.Terminal = &Terminal{}
// updated. When the output is redirected to a file, the status lines are not
// printed.
type Terminal struct {
wr io.Writer
fd uintptr
errWriter io.Writer
msg chan message
status chan status
canUpdateStatus bool
lastStatusLen int
wr io.Writer
fd uintptr
errWriter io.Writer
msg chan message
status chan status
outputIsTerminal bool
canUpdateStatus bool
lastStatusLen int
// will be closed when the goroutine which runs Run() terminates, so it'll
// yield a default value immediately
@@ -65,12 +66,17 @@ func New(wr io.Writer, errWriter io.Writer, disableStatus bool) *Terminal {
return t
}
if d, ok := wr.(fder); ok && terminal.CanUpdateStatus(d.Fd()) {
// only use the fancy status code when we're running on a real terminal.
t.canUpdateStatus = true
t.fd = d.Fd()
t.clearCurrentLine = terminal.ClearCurrentLine(t.fd)
t.moveCursorUp = terminal.MoveCursorUp(t.fd)
if d, ok := wr.(fder); ok {
if terminal.CanUpdateStatus(d.Fd()) {
// only use the fancy status code when we're running on a real terminal.
t.canUpdateStatus = true
t.fd = d.Fd()
t.clearCurrentLine = terminal.ClearCurrentLine(t.fd)
t.moveCursorUp = terminal.MoveCursorUp(t.fd)
}
if terminal.OutputIsTerminal(d.Fd()) {
t.outputIsTerminal = true
}
}
return t
@@ -88,6 +94,11 @@ func (t *Terminal) OutputRaw() io.Writer {
return t.wr
}
// OutputIsTerminal returns whether the output is a terminal.
func (t *Terminal) OutputIsTerminal() bool {
return t.outputIsTerminal
}
// Run updates the screen. It should be run in a separate goroutine. When
// ctx is cancelled, the status lines are cleanly removed.
func (t *Terminal) Run(ctx context.Context) {