From 3b3da4b2b15b58ed5e4dca32bcbc6088dff1cf99 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Fri, 14 Apr 2023 16:10:29 -0400 Subject: [PATCH] fix(output): export output writer Termenv output can be a buffer, ssh session, a file, or anything that implements the io.Writer interface. In the case of an ssh session, the std ssh library returns a io.ReadWriter for ssh sessions and the current Termenv implementation makes it impossible to read terminal status reports since it expects a file in return. In addition, the TTY() function is confusing since it implies that the returned value is a TTY. Which is not always true until we check that using the isTTY() function. Deprecate TTY() in favor of Writer() which simply returns the underlying writer. The caller then can infer the type of the writer and decide what to do with it. This also deprecate parts of commit 669c9abfb65169a7a146968e38702d3d80327a42 https://github.com/muesli/termenv/commit/669c9abfb65169a7a146968e38702d3d80327a42 --- output.go | 26 +++++++++----- screen.go | 88 +++++++++++++++++++++++----------------------- screen_test.go | 2 +- termenv.go | 7 ++-- termenv_windows.go | 5 +-- 5 files changed, 70 insertions(+), 58 deletions(-) diff --git a/output.go b/output.go index e22d369..ebe48fe 100644 --- a/output.go +++ b/output.go @@ -12,6 +12,8 @@ var ( ) // File represents a file descriptor. +// +// Deprecated: Use *os.File instead. type File interface { io.ReadWriter Fd() uintptr @@ -23,7 +25,7 @@ type OutputOption = func(*Output) // Output is a terminal output. type Output struct { Profile - tty io.Writer + w io.Writer environ Environ assumeTTY bool @@ -61,10 +63,10 @@ func SetDefaultOutput(o *Output) { output = o } -// NewOutput returns a new Output for the given file descriptor. -func NewOutput(tty io.Writer, opts ...OutputOption) *Output { +// NewOutput returns a new Output for the given writer. +func NewOutput(w io.Writer, opts ...OutputOption) *Output { o := &Output{ - tty: tty, + w: w, environ: &osEnviron{}, Profile: -1, fgSync: &sync.Once{}, @@ -73,8 +75,8 @@ func NewOutput(tty io.Writer, opts ...OutputOption) *Output { bgColor: NoColor{}, } - if o.tty == nil { - o.tty = os.Stdout + if o.w == nil { + o.w = os.Stdout } for _, opt := range opts { opt(o) @@ -180,15 +182,23 @@ func (o *Output) HasDarkBackground() bool { // TTY returns the terminal's file descriptor. This may be nil if the output is // not a terminal. +// +// Deprecated: Use Writer() instead. func (o Output) TTY() File { - if f, ok := o.tty.(File); ok { + if f, ok := o.w.(File); ok { return f } return nil } +// Writer returns the underlying writer. This may be of type io.Writer, +// io.ReadWriter, or *os.File. +func (o Output) Writer() io.Writer { + return o.w +} + func (o Output) Write(p []byte) (int, error) { - return o.tty.Write(p) + return o.w.Write(p) } // WriteString writes the given string to the output. diff --git a/screen.go b/screen.go index a71181b..19f28dc 100644 --- a/screen.go +++ b/screen.go @@ -71,234 +71,234 @@ const ( // Reset the terminal to its default style, removing any active styles. func (o Output) Reset() { - fmt.Fprint(o.tty, CSI+ResetSeq+"m") + fmt.Fprint(o.w, CSI+ResetSeq+"m") } // SetForegroundColor sets the default foreground color. func (o Output) SetForegroundColor(color Color) { - fmt.Fprintf(o.tty, OSC+SetForegroundColorSeq, color) + fmt.Fprintf(o.w, OSC+SetForegroundColorSeq, color) } // SetBackgroundColor sets the default background color. func (o Output) SetBackgroundColor(color Color) { - fmt.Fprintf(o.tty, OSC+SetBackgroundColorSeq, color) + fmt.Fprintf(o.w, OSC+SetBackgroundColorSeq, color) } // SetCursorColor sets the cursor color. func (o Output) SetCursorColor(color Color) { - fmt.Fprintf(o.tty, OSC+SetCursorColorSeq, color) + fmt.Fprintf(o.w, OSC+SetCursorColorSeq, color) } // RestoreScreen restores a previously saved screen state. func (o Output) RestoreScreen() { - fmt.Fprint(o.tty, CSI+RestoreScreenSeq) + fmt.Fprint(o.w, CSI+RestoreScreenSeq) } // SaveScreen saves the screen state. func (o Output) SaveScreen() { - fmt.Fprint(o.tty, CSI+SaveScreenSeq) + fmt.Fprint(o.w, CSI+SaveScreenSeq) } // AltScreen switches to the alternate screen buffer. The former view can be // restored with ExitAltScreen(). func (o Output) AltScreen() { - fmt.Fprint(o.tty, CSI+AltScreenSeq) + fmt.Fprint(o.w, CSI+AltScreenSeq) } // ExitAltScreen exits the alternate screen buffer and returns to the former // terminal view. func (o Output) ExitAltScreen() { - fmt.Fprint(o.tty, CSI+ExitAltScreenSeq) + fmt.Fprint(o.w, CSI+ExitAltScreenSeq) } // ClearScreen clears the visible portion of the terminal. func (o Output) ClearScreen() { - fmt.Fprintf(o.tty, CSI+EraseDisplaySeq, 2) + fmt.Fprintf(o.w, CSI+EraseDisplaySeq, 2) o.MoveCursor(1, 1) } // MoveCursor moves the cursor to a given position. func (o Output) MoveCursor(row int, column int) { - fmt.Fprintf(o.tty, CSI+CursorPositionSeq, row, column) + fmt.Fprintf(o.w, CSI+CursorPositionSeq, row, column) } // HideCursor hides the cursor. func (o Output) HideCursor() { - fmt.Fprint(o.tty, CSI+HideCursorSeq) + fmt.Fprint(o.w, CSI+HideCursorSeq) } // ShowCursor shows the cursor. func (o Output) ShowCursor() { - fmt.Fprint(o.tty, CSI+ShowCursorSeq) + fmt.Fprint(o.w, CSI+ShowCursorSeq) } // SaveCursorPosition saves the cursor position. func (o Output) SaveCursorPosition() { - fmt.Fprint(o.tty, CSI+SaveCursorPositionSeq) + fmt.Fprint(o.w, CSI+SaveCursorPositionSeq) } // RestoreCursorPosition restores a saved cursor position. func (o Output) RestoreCursorPosition() { - fmt.Fprint(o.tty, CSI+RestoreCursorPositionSeq) + fmt.Fprint(o.w, CSI+RestoreCursorPositionSeq) } // CursorUp moves the cursor up a given number of lines. func (o Output) CursorUp(n int) { - fmt.Fprintf(o.tty, CSI+CursorUpSeq, n) + fmt.Fprintf(o.w, CSI+CursorUpSeq, n) } // CursorDown moves the cursor down a given number of lines. func (o Output) CursorDown(n int) { - fmt.Fprintf(o.tty, CSI+CursorDownSeq, n) + fmt.Fprintf(o.w, CSI+CursorDownSeq, n) } // CursorForward moves the cursor up a given number of lines. func (o Output) CursorForward(n int) { - fmt.Fprintf(o.tty, CSI+CursorForwardSeq, n) + fmt.Fprintf(o.w, CSI+CursorForwardSeq, n) } // CursorBack moves the cursor backwards a given number of cells. func (o Output) CursorBack(n int) { - fmt.Fprintf(o.tty, CSI+CursorBackSeq, n) + fmt.Fprintf(o.w, CSI+CursorBackSeq, n) } // CursorNextLine moves the cursor down a given number of lines and places it at // the beginning of the line. func (o Output) CursorNextLine(n int) { - fmt.Fprintf(o.tty, CSI+CursorNextLineSeq, n) + fmt.Fprintf(o.w, CSI+CursorNextLineSeq, n) } // CursorPrevLine moves the cursor up a given number of lines and places it at // the beginning of the line. func (o Output) CursorPrevLine(n int) { - fmt.Fprintf(o.tty, CSI+CursorPreviousLineSeq, n) + fmt.Fprintf(o.w, CSI+CursorPreviousLineSeq, n) } // ClearLine clears the current line. func (o Output) ClearLine() { - fmt.Fprint(o.tty, CSI+EraseEntireLineSeq) + fmt.Fprint(o.w, CSI+EraseEntireLineSeq) } // ClearLineLeft clears the line to the left of the cursor. func (o Output) ClearLineLeft() { - fmt.Fprint(o.tty, CSI+EraseLineLeftSeq) + fmt.Fprint(o.w, CSI+EraseLineLeftSeq) } // ClearLineRight clears the line to the right of the cursor. func (o Output) ClearLineRight() { - fmt.Fprint(o.tty, CSI+EraseLineRightSeq) + fmt.Fprint(o.w, CSI+EraseLineRightSeq) } // ClearLines clears a given number of lines. func (o Output) ClearLines(n int) { clearLine := fmt.Sprintf(CSI+EraseLineSeq, 2) cursorUp := fmt.Sprintf(CSI+CursorUpSeq, 1) - fmt.Fprint(o.tty, clearLine+strings.Repeat(cursorUp+clearLine, n)) + fmt.Fprint(o.w, clearLine+strings.Repeat(cursorUp+clearLine, n)) } // ChangeScrollingRegion sets the scrolling region of the terminal. func (o Output) ChangeScrollingRegion(top, bottom int) { - fmt.Fprintf(o.tty, CSI+ChangeScrollingRegionSeq, top, bottom) + fmt.Fprintf(o.w, CSI+ChangeScrollingRegionSeq, top, bottom) } // InsertLines inserts the given number of lines at the top of the scrollable // region, pushing lines below down. func (o Output) InsertLines(n int) { - fmt.Fprintf(o.tty, CSI+InsertLineSeq, n) + fmt.Fprintf(o.w, CSI+InsertLineSeq, n) } // DeleteLines deletes the given number of lines, pulling any lines in // the scrollable region below up. func (o Output) DeleteLines(n int) { - fmt.Fprintf(o.tty, CSI+DeleteLineSeq, n) + fmt.Fprintf(o.w, CSI+DeleteLineSeq, n) } // EnableMousePress enables X10 mouse mode. Button press events are sent only. func (o Output) EnableMousePress() { - fmt.Fprint(o.tty, CSI+EnableMousePressSeq) + fmt.Fprint(o.w, CSI+EnableMousePressSeq) } // DisableMousePress disables X10 mouse mode. func (o Output) DisableMousePress() { - fmt.Fprint(o.tty, CSI+DisableMousePressSeq) + fmt.Fprint(o.w, CSI+DisableMousePressSeq) } // EnableMouse enables Mouse Tracking mode. func (o Output) EnableMouse() { - fmt.Fprint(o.tty, CSI+EnableMouseSeq) + fmt.Fprint(o.w, CSI+EnableMouseSeq) } // DisableMouse disables Mouse Tracking mode. func (o Output) DisableMouse() { - fmt.Fprint(o.tty, CSI+DisableMouseSeq) + fmt.Fprint(o.w, CSI+DisableMouseSeq) } // EnableMouseHilite enables Hilite Mouse Tracking mode. func (o Output) EnableMouseHilite() { - fmt.Fprint(o.tty, CSI+EnableMouseHiliteSeq) + fmt.Fprint(o.w, CSI+EnableMouseHiliteSeq) } // DisableMouseHilite disables Hilite Mouse Tracking mode. func (o Output) DisableMouseHilite() { - fmt.Fprint(o.tty, CSI+DisableMouseHiliteSeq) + fmt.Fprint(o.w, CSI+DisableMouseHiliteSeq) } // EnableMouseCellMotion enables Cell Motion Mouse Tracking mode. func (o Output) EnableMouseCellMotion() { - fmt.Fprint(o.tty, CSI+EnableMouseCellMotionSeq) + fmt.Fprint(o.w, CSI+EnableMouseCellMotionSeq) } // DisableMouseCellMotion disables Cell Motion Mouse Tracking mode. func (o Output) DisableMouseCellMotion() { - fmt.Fprint(o.tty, CSI+DisableMouseCellMotionSeq) + fmt.Fprint(o.w, CSI+DisableMouseCellMotionSeq) } // EnableMouseAllMotion enables All Motion Mouse mode. func (o Output) EnableMouseAllMotion() { - fmt.Fprint(o.tty, CSI+EnableMouseAllMotionSeq) + fmt.Fprint(o.w, CSI+EnableMouseAllMotionSeq) } // DisableMouseAllMotion disables All Motion Mouse mode. func (o Output) DisableMouseAllMotion() { - fmt.Fprint(o.tty, CSI+DisableMouseAllMotionSeq) + fmt.Fprint(o.w, CSI+DisableMouseAllMotionSeq) } // EnableMouseExtendedMotion enables Extended Mouse mode (SGR). This should be // enabled in conjunction with EnableMouseCellMotion, and EnableMouseAllMotion. func (o Output) EnableMouseExtendedMode() { - fmt.Fprint(o.tty, CSI+EnableMouseExtendedModeSeq) + fmt.Fprint(o.w, CSI+EnableMouseExtendedModeSeq) } // DisableMouseExtendedMotion disables Extended Mouse mode (SGR). func (o Output) DisableMouseExtendedMode() { - fmt.Fprint(o.tty, CSI+DisableMouseExtendedModeSeq) + fmt.Fprint(o.w, CSI+DisableMouseExtendedModeSeq) } // EnableMousePixelsMotion enables Pixel Motion Mouse mode (SGR-Pixels). This // should be enabled in conjunction with EnableMouseCellMotion, and // EnableMouseAllMotion. func (o Output) EnableMousePixelsMode() { - fmt.Fprint(o.tty, CSI+EnableMousePixelsModeSeq) + fmt.Fprint(o.w, CSI+EnableMousePixelsModeSeq) } // DisableMousePixelsMotion disables Pixel Motion Mouse mode (SGR-Pixels). func (o Output) DisableMousePixelsMode() { - fmt.Fprint(o.tty, CSI+DisableMousePixelsModeSeq) + fmt.Fprint(o.w, CSI+DisableMousePixelsModeSeq) } // SetWindowTitle sets the terminal window title. func (o Output) SetWindowTitle(title string) { - fmt.Fprintf(o.tty, OSC+SetWindowTitleSeq, title) + fmt.Fprintf(o.w, OSC+SetWindowTitleSeq, title) } // EnableBracketedPaste enables bracketed paste. func (o Output) EnableBracketedPaste() { - fmt.Fprintf(o.tty, CSI+EnableBracketedPasteSeq) + fmt.Fprintf(o.w, CSI+EnableBracketedPasteSeq) } // DisableBracketedPaste disables bracketed paste. func (o Output) DisableBracketedPaste() { - fmt.Fprintf(o.tty, CSI+DisableBracketedPasteSeq) + fmt.Fprintf(o.w, CSI+DisableBracketedPasteSeq) } // Legacy functions. diff --git a/screen_test.go b/screen_test.go index aa40595..7e2a845 100644 --- a/screen_test.go +++ b/screen_test.go @@ -34,7 +34,7 @@ func tempOutput(t *testing.T) *Output { func verify(t *testing.T, o *Output, exp string) { t.Helper() - tty := o.tty.(*os.File) + tty := o.w.(*os.File) if _, err := tty.Seek(0, 0); err != nil { t.Fatal(err) diff --git a/termenv.go b/termenv.go index 4ceb271..84a82c2 100644 --- a/termenv.go +++ b/termenv.go @@ -2,6 +2,7 @@ package termenv import ( "errors" + "os" "github.com/mattn/go-isatty" ) @@ -31,11 +32,11 @@ func (o *Output) isTTY() bool { if len(o.environ.Getenv("CI")) > 0 { return false } - if o.TTY() == nil { - return false + if f, ok := o.Writer().(*os.File); ok { + return isatty.IsTerminal(f.Fd()) } - return isatty.IsTerminal(o.TTY().Fd()) + return false } // ColorProfile returns the supported color profile: diff --git a/termenv_windows.go b/termenv_windows.go index 1d9c618..f9b1def 100644 --- a/termenv_windows.go +++ b/termenv_windows.go @@ -5,6 +5,7 @@ package termenv import ( "fmt" + "os" "strconv" "golang.org/x/sys/windows" @@ -103,8 +104,8 @@ func EnableVirtualTerminalProcessing(o *Output) (restoreFunc func() error, err e } // If o is not a tty, then there is nothing to do. - tty := o.TTY() - if tty == nil { + tty, ok := o.Writer().(*os.File) + if tty == nil || !ok { return }