diff --git a/term.go b/term.go index b21178f..82c3c04 100644 --- a/term.go +++ b/term.go @@ -23,8 +23,20 @@ func StdStreams() (stdIn io.ReadCloser, stdOut, stdErr io.Writer) { return stdStreams() } -// GetFdInfo returns the file descriptor for an os.File and indicates whether the file represents a terminal. -func GetFdInfo(in any) (fd uintptr, isTerminal bool) { +// terminalInfo provides the file descriptor and terminal status of a stream. +// It matches the relevant methods of github.com/docker/cli/cli/streams.Out. +type terminalInfo interface { + FD() uintptr + IsTerminal() bool +} + +// GetFdInfo returns the file descriptor and terminal status of the given stream. +// It supports *os.File and streams that provide their own file descriptor and +// terminal status. +func GetFdInfo(in any) (uintptr, bool) { + if t, ok := in.(terminalInfo); ok { + return t.FD(), t.IsTerminal() + } return getFdInfo(in) } diff --git a/term_test.go b/term_unix_test.go similarity index 67% rename from term_test.go rename to term_unix_test.go index 78ad528..01f30e0 100644 --- a/term_test.go +++ b/term_unix_test.go @@ -83,22 +83,63 @@ func TestSetWinsize(t *testing.T) { } } +type testTerminalInfo struct { + fd uintptr + isTerminal bool +} + +func (t testTerminalInfo) FD() uintptr { + return t.fd +} + +func (t testTerminalInfo) IsTerminal() bool { + return t.isTerminal +} + func TestGetFdInfo(t *testing.T) { tty := newTTYForTest(t) - inFd, isTerm := GetFdInfo(tty) - if inFd != tty.Fd() { - t.Errorf("expected: %d, got: %d", tty.Fd(), inFd) - } - if !isTerm { - t.Error("expected file-descriptor to be a terminal") - } tmpFile := newTempFile(t) - inFd, isTerm = GetFdInfo(tmpFile) - if inFd != tmpFile.Fd() { - t.Errorf("expected: %d, got: %d", tty.Fd(), inFd) - } - if isTerm { - t.Error("expected file-descriptor to not be a terminal") + + for _, tc := range []struct { + name string + stream any + wantFD uintptr + wantIsTerm bool + }{ + { + name: "tty", + stream: tty, + wantFD: tty.Fd(), + wantIsTerm: true, + }, + { + name: "file", + stream: tmpFile, + wantFD: tmpFile.Fd(), + wantIsTerm: false, + }, + // Stream wrappers, such as docker/cli's streams.Out, provide their + // own terminal status. Use a TTY descriptor to verify that the + // reported status takes precedence over OS-level detection. + { + name: "stream terminal", + stream: testTerminalInfo{fd: tty.Fd(), isTerminal: true}, + wantFD: tty.Fd(), + wantIsTerm: true, + }, + { + name: "stream non-terminal", + stream: testTerminalInfo{fd: tty.Fd(), isTerminal: false}, + wantFD: tty.Fd(), + wantIsTerm: false, + }, + } { + t.Run(tc.name, func(t *testing.T) { + fd, isTerm := GetFdInfo(tc.stream) + if fd != tc.wantFD || isTerm != tc.wantIsTerm { + t.Errorf("GetFdInfo() = (%d, %t), want (%d, %t)", fd, isTerm, tc.wantFD, tc.wantIsTerm) + } + }) } } diff --git a/term_windows_test.go b/term_windows_test.go new file mode 100644 index 0000000..9b7e52a --- /dev/null +++ b/term_windows_test.go @@ -0,0 +1,46 @@ +package term_test + +import ( + "testing" + + "github.com/moby/term" +) + +type testTerminalInfo struct { + fd uintptr + isTerminal bool +} + +func (t testTerminalInfo) FD() uintptr { + return t.fd +} + +func (t testTerminalInfo) IsTerminal() bool { + return t.isTerminal +} + +func TestGetFdInfoStream(t *testing.T) { + for _, tc := range []struct { + name string + isTerminal bool + }{ + {name: "terminal", isTerminal: true}, + {name: "non-terminal", isTerminal: false}, + } { + t.Run(tc.name, func(t *testing.T) { + // Stream wrappers, such as docker/cli's streams.Out, provide + // their own terminal status, regardless of the underlying + // descriptor. + stream := testTerminalInfo{ + fd: 123, + isTerminal: tc.isTerminal, + } + + fd, isTerm := term.GetFdInfo(stream) + if fd != stream.FD() || isTerm != stream.IsTerminal() { + t.Errorf("GetFdInfo() = (%d, %t), want (%d, %t)", + fd, isTerm, stream.FD(), stream.IsTerminal()) + } + }) + } +}