Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions internal/tty/tty.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,26 @@ import (
func IsStderrTerminal() bool {
return term.IsTerminal(int(os.Stderr.Fd()))
}

// IsStdoutTerminal returns true if stdout is connected to a terminal.
func IsStdoutTerminal() bool {
return term.IsTerminal(int(os.Stdout.Fd()))
}

// StderrTerminalWidth returns the width of the terminal connected to stderr
// and true if successful. Returns 0 and false if stderr is not a terminal or
// the width cannot be determined.
func StderrTerminalWidth() (int, bool) {
width, _, err := term.GetSize(int(os.Stderr.Fd()))
if err != nil || width <= 0 {
return 0, false
}
return width, true
}

// IsInteractiveTerminal returns true if the process is running in an
// interactive terminal context: stderr is a terminal and the process is not
// running inside a container.
func IsInteractiveTerminal() bool {
return IsStderrTerminal() && !IsRunningInContainer()
}
63 changes: 63 additions & 0 deletions internal/tty/tty_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/term"
)

Expand Down Expand Up @@ -33,3 +34,65 @@ func TestIsStderrTerminal_NotATerminalInCI(t *testing.T) {
}
assert.False(t, IsStderrTerminal(), "stderr should not be a terminal in CI")
}

// TestIsStdoutTerminal verifies the function agrees with the underlying
// term.IsTerminal check for os.Stdout.
func TestIsStdoutTerminal(t *testing.T) {
expected := term.IsTerminal(int(os.Stdout.Fd()))
result := IsStdoutTerminal()
assert.Equal(t, expected, result, "IsStdoutTerminal should match term.IsTerminal(stdout)")
}

// TestTermIsTerminal_PipeIsNotTerminal verifies that the underlying
// term.IsTerminal correctly identifies a pipe as not a terminal. This
// documents the invariant that IsStdoutTerminal and IsStderrTerminal rely on.
func TestTermIsTerminal_PipeIsNotTerminal(t *testing.T) {
r, w, err := os.Pipe()
require.NoError(t, err)
defer r.Close()
defer w.Close()
assert.False(t, term.IsTerminal(int(r.Fd())), "pipe file descriptor should not be a terminal")
assert.False(t, term.IsTerminal(int(w.Fd())), "pipe write-end should not be a terminal")
}

// TestStderrTerminalWidth verifies that StderrTerminalWidth returns consistent
// results and only reports success when stderr is a terminal.
func TestStderrTerminalWidth(t *testing.T) {
width, ok := StderrTerminalWidth()
isTerminal := term.IsTerminal(int(os.Stderr.Fd()))
if isTerminal {
assert.True(t, ok, "should succeed when stderr is a terminal")
assert.Greater(t, width, 0, "terminal width should be positive")
} else {
assert.False(t, ok, "should fail when stderr is not a terminal")
assert.Equal(t, 0, width, "width should be 0 when not a terminal")
}
}

// TestStderrTerminalWidth_NotATerminalInCI verifies that width detection
// returns false in CI where stderr is not a terminal.
func TestStderrTerminalWidth_NotATerminalInCI(t *testing.T) {
if os.Getenv("CI") == "" && os.Getenv("GITHUB_ACTIONS") == "" {
t.Skip("Skipping CI-specific assertion: not running in a CI environment")
}
width, ok := StderrTerminalWidth()
assert.False(t, ok, "should not detect terminal width in CI")
assert.Equal(t, 0, width, "width should be 0 in CI")
Comment on lines +75 to +80
}

// TestIsInteractiveTerminal verifies that IsInteractiveTerminal returns true
// only when both stderr is a terminal and the process is not in a container.
func TestIsInteractiveTerminal(t *testing.T) {
expected := IsStderrTerminal() && !IsRunningInContainer()
assert.Equal(t, expected, IsInteractiveTerminal(),
"IsInteractiveTerminal should be IsStderrTerminal() && !IsRunningInContainer()")
}

// TestIsInteractiveTerminal_NotInteractiveInCI verifies the expected false
// result in CI where stderr is a pipe, not a terminal.
func TestIsInteractiveTerminal_NotInteractiveInCI(t *testing.T) {
if os.Getenv("CI") == "" && os.Getenv("GITHUB_ACTIONS") == "" {
t.Skip("Skipping CI-specific assertion: not running in a CI environment")
}
assert.False(t, IsInteractiveTerminal(), "should not be interactive in CI")
}
Loading