From 196940f97f4dabba18b0c626a6ed75e447a4db24 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Fri, 29 Apr 2022 14:15:25 -0400 Subject: [PATCH] feat: use interfaces and simplify Output --- output.go | 58 +++++++++++++++++++++++++++------- screen.go | 77 +++++++++++++++++++++++----------------------- screen_test.go | 7 +++-- termenv.go | 59 +++++++++++++++++------------------ termenv_test.go | 5 +-- termenv_unix.go | 25 +++++++++------ termenv_windows.go | 9 +++--- 7 files changed, 140 insertions(+), 100 deletions(-) diff --git a/output.go b/output.go index 9e8debb..342aa91 100644 --- a/output.go +++ b/output.go @@ -1,35 +1,71 @@ package termenv -import "os" +import ( + "io" + "os" +) + +var ( + // output is the default global output. + output = NewOutputWithProfile(os.Stdout, &osEnviron{}, ANSI) +) + +// File represents a file descriptor. +type File interface { + io.ReadWriter + Fd() uintptr +} // Output is a terminal output. type Output struct { Profile - tty *os.File + tty File + environ Environ +} + +// Environ is an interface for getting environment variables. +type Environ interface { + Environ() []string + Getenv(string) string +} + +type osEnviron struct{} + +func (oe *osEnviron) Environ() []string { + return os.Environ() +} + +func (oe *osEnviron) Getenv(key string) string { + return os.Getenv(key) +} + +// DefaultOutput returns the default global output. +func DefaultOutput() *Output { + return output } // NewOutput returns a new Output for the given file descriptor. -func NewOutput(tty *os.File) *Output { - p := Ascii - if isTTY(tty.Fd()) { - p = envColorProfile() +func NewOutput(tty File, environ Environ) *Output { + o := NewOutputWithProfile(tty, environ, Ascii) + if o.isTTY() { + o.Profile = o.EnvColorProfile() } - - return NewOutputWithProfile(tty, p) + return o } // NewOutputWithProfile returns a new Output for the given file descriptor and // profile. -func NewOutputWithProfile(tty *os.File, profile Profile) *Output { +func NewOutputWithProfile(tty File, environ Environ, profile Profile) *Output { return &Output{ Profile: profile, tty: tty, + environ: environ, } } // ForegroundColor returns the terminal's default foreground color. func (o Output) ForegroundColor() Color { - if !isTTY(o.tty.Fd()) { + if !o.isTTY() { return NoColor{} } @@ -38,7 +74,7 @@ func (o Output) ForegroundColor() Color { // BackgroundColor returns the terminal's default background color. func (o Output) BackgroundColor() Color { - if !isTTY(o.tty.Fd()) { + if !o.isTTY() { return NoColor{} } diff --git a/screen.go b/screen.go index 5d335fd..bc4340b 100644 --- a/screen.go +++ b/screen.go @@ -2,7 +2,6 @@ package termenv import ( "fmt" - "os" "strings" ) @@ -262,196 +261,196 @@ func (o Output) SetWindowTitle(title string) { // Reset the terminal to its default style, removing any active styles. func Reset() { - NewOutputWithProfile(os.Stdout, ANSI).Reset() + output.Reset() } // SetForegroundColor sets the default foreground color. func SetForegroundColor(color Color) { - NewOutputWithProfile(os.Stdout, ANSI).SetForegroundColor(color) + output.SetForegroundColor(color) } // SetBackgroundColor sets the default background color. func SetBackgroundColor(color Color) { - NewOutputWithProfile(os.Stdout, ANSI).SetBackgroundColor(color) + output.SetBackgroundColor(color) } // SetCursorColor sets the cursor color. func SetCursorColor(color Color) { - NewOutputWithProfile(os.Stdout, ANSI).SetCursorColor(color) + output.SetCursorColor(color) } // RestoreScreen restores a previously saved screen state. func RestoreScreen() { - NewOutputWithProfile(os.Stdout, ANSI).RestoreScreen() + output.RestoreScreen() } // SaveScreen saves the screen state. func SaveScreen() { - NewOutputWithProfile(os.Stdout, ANSI).SaveScreen() + output.SaveScreen() } // AltScreen switches to the alternate screen buffer. The former view can be // restored with ExitAltScreen(). func AltScreen() { - NewOutputWithProfile(os.Stdout, ANSI).AltScreen() + output.AltScreen() } // ExitAltScreen exits the alternate screen buffer and returns to the former // terminal view. func ExitAltScreen() { - NewOutputWithProfile(os.Stdout, ANSI).ExitAltScreen() + output.ExitAltScreen() } // ClearScreen clears the visible portion of the terminal. func ClearScreen() { - NewOutputWithProfile(os.Stdout, ANSI).ClearScreen() + output.ClearScreen() } // MoveCursor moves the cursor to a given position. func MoveCursor(row int, column int) { - NewOutputWithProfile(os.Stdout, ANSI).MoveCursor(row, column) + output.MoveCursor(row, column) } // HideCursor hides the cursor. func HideCursor() { - NewOutputWithProfile(os.Stdout, ANSI).HideCursor() + output.HideCursor() } // ShowCursor shows the cursor. func ShowCursor() { - NewOutputWithProfile(os.Stdout, ANSI).ShowCursor() + output.ShowCursor() } // SaveCursorPosition saves the cursor position. func SaveCursorPosition() { - NewOutputWithProfile(os.Stdout, ANSI).SaveCursorPosition() + output.SaveCursorPosition() } // RestoreCursorPosition restores a saved cursor position. func RestoreCursorPosition() { - NewOutputWithProfile(os.Stdout, ANSI).RestoreCursorPosition() + output.RestoreCursorPosition() } // CursorUp moves the cursor up a given number of lines. func CursorUp(n int) { - NewOutputWithProfile(os.Stdout, ANSI).CursorUp(n) + output.CursorUp(n) } // CursorDown moves the cursor down a given number of lines. func CursorDown(n int) { - NewOutputWithProfile(os.Stdout, ANSI).CursorDown(n) + output.CursorDown(n) } // CursorForward moves the cursor up a given number of lines. func CursorForward(n int) { - NewOutputWithProfile(os.Stdout, ANSI).CursorForward(n) + output.CursorForward(n) } // CursorBack moves the cursor backwards a given number of cells. func CursorBack(n int) { - NewOutputWithProfile(os.Stdout, ANSI).CursorBack(n) + output.CursorBack(n) } // CursorNextLine moves the cursor down a given number of lines and places it at // the beginning of the line. func CursorNextLine(n int) { - NewOutputWithProfile(os.Stdout, ANSI).CursorNextLine(n) + output.CursorNextLine(n) } // CursorPrevLine moves the cursor up a given number of lines and places it at // the beginning of the line. func CursorPrevLine(n int) { - NewOutputWithProfile(os.Stdout, ANSI).CursorPrevLine(n) + output.CursorPrevLine(n) } // ClearLine clears the current line. func ClearLine() { - NewOutputWithProfile(os.Stdout, ANSI).ClearLine() + output.ClearLine() } // ClearLineLeft clears the line to the left of the cursor. func ClearLineLeft() { - NewOutputWithProfile(os.Stdout, ANSI).ClearLineLeft() + output.ClearLineLeft() } // ClearLineRight clears the line to the right of the cursor. func ClearLineRight() { - NewOutputWithProfile(os.Stdout, ANSI).ClearLineRight() + output.ClearLineRight() } // ClearLines clears a given number of lines. func ClearLines(n int) { - NewOutputWithProfile(os.Stdout, ANSI).ClearLines(n) + output.ClearLines(n) } // ChangeScrollingRegion sets the scrolling region of the terminal. func ChangeScrollingRegion(top, bottom int) { - NewOutputWithProfile(os.Stdout, ANSI).ChangeScrollingRegion(top, bottom) + output.ChangeScrollingRegion(top, bottom) } // InsertLines inserts the given number of lines at the top of the scrollable // region, pushing lines below down. func InsertLines(n int) { - NewOutputWithProfile(os.Stdout, ANSI).InsertLines(n) + output.InsertLines(n) } // DeleteLines deletes the given number of lines, pulling any lines in // the scrollable region below up. func DeleteLines(n int) { - NewOutputWithProfile(os.Stdout, ANSI).DeleteLines(n) + output.DeleteLines(n) } // EnableMousePress enables X10 mouse mode. Button press events are sent only. func EnableMousePress() { - NewOutputWithProfile(os.Stdout, ANSI).EnableMousePress() + output.EnableMousePress() } // DisableMousePress disables X10 mouse mode. func DisableMousePress() { - NewOutputWithProfile(os.Stdout, ANSI).DisableMousePress() + output.DisableMousePress() } // EnableMouse enables Mouse Tracking mode. func EnableMouse() { - NewOutputWithProfile(os.Stdout, ANSI).EnableMouse() + output.EnableMouse() } // DisableMouse disables Mouse Tracking mode. func DisableMouse() { - NewOutputWithProfile(os.Stdout, ANSI).DisableMouse() + output.DisableMouse() } // EnableMouseHilite enables Hilite Mouse Tracking mode. func EnableMouseHilite() { - NewOutputWithProfile(os.Stdout, ANSI).EnableMouseHilite() + output.EnableMouseHilite() } // DisableMouseHilite disables Hilite Mouse Tracking mode. func DisableMouseHilite() { - NewOutputWithProfile(os.Stdout, ANSI).DisableMouseHilite() + output.DisableMouseHilite() } // EnableMouseCellMotion enables Cell Motion Mouse Tracking mode. func EnableMouseCellMotion() { - NewOutputWithProfile(os.Stdout, ANSI).EnableMouseCellMotion() + output.EnableMouseCellMotion() } // DisableMouseCellMotion disables Cell Motion Mouse Tracking mode. func DisableMouseCellMotion() { - NewOutputWithProfile(os.Stdout, ANSI).DisableMouseCellMotion() + output.DisableMouseCellMotion() } // EnableMouseAllMotion enables All Motion Mouse mode. func EnableMouseAllMotion() { - NewOutputWithProfile(os.Stdout, ANSI).EnableMouseAllMotion() + output.EnableMouseAllMotion() } // DisableMouseAllMotion disables All Motion Mouse mode. func DisableMouseAllMotion() { - NewOutputWithProfile(os.Stdout, ANSI).DisableMouseAllMotion() + output.DisableMouseAllMotion() } // SetWindowTitle sets the terminal window title. func SetWindowTitle(title string) { - NewOutputWithProfile(os.Stdout, ANSI).SetWindowTitle(title) + output.SetWindowTitle(title) } diff --git a/screen_test.go b/screen_test.go index 865460d..342b50e 100644 --- a/screen_test.go +++ b/screen_test.go @@ -16,13 +16,14 @@ func tempOutput(t *testing.T) *Output { t.Fatal(err) } - return NewOutputWithProfile(f, TrueColor) + return NewOutputWithProfile(f, &osEnviron{}, TrueColor) } func verify(t *testing.T, o *Output, exp string) { t.Helper() + tty := o.tty.(*os.File) - if _, err := o.tty.Seek(0, 0); err != nil { + if _, err := tty.Seek(0, 0); err != nil { t.Fatal(err) } @@ -37,7 +38,7 @@ func verify(t *testing.T, o *Output, exp string) { } // remove temp file - os.Remove(o.tty.Name()) + os.Remove(tty.Name()) } func TestReset(t *testing.T) { diff --git a/termenv.go b/termenv.go index 98d5a84..89fc04f 100644 --- a/termenv.go +++ b/termenv.go @@ -2,7 +2,6 @@ package termenv import ( "errors" - "os" "github.com/mattn/go-isatty" ) @@ -19,40 +18,42 @@ const ( OSC = "\x1b]" ) -func isTTY(fd uintptr) bool { - if len(os.Getenv("CI")) > 0 { +func (o *Output) isTTY() bool { + if len(o.environ.Getenv("CI")) > 0 { return false } - return isatty.IsTerminal(fd) + return isatty.IsTerminal(o.tty.Fd()) } // ColorProfile returns the supported color profile: // Ascii, ANSI, ANSI256, or TrueColor. func ColorProfile() Profile { - if !isTTY(os.Stdout.Fd()) { - return Ascii - } - - return colorProfile() + return output.ColorProfile() } // ForegroundColor returns the terminal's default foreground color. func ForegroundColor() Color { - o := NewOutputWithProfile(os.Stdout, TrueColor) - return o.ForegroundColor() + return output.ForegroundColor() } // BackgroundColor returns the terminal's default background color. func BackgroundColor() Color { - o := NewOutputWithProfile(os.Stdout, TrueColor) - return o.BackgroundColor() + return output.BackgroundColor() } // HasDarkBackground returns whether terminal uses a dark-ish background. func HasDarkBackground() bool { - o := NewOutputWithProfile(os.Stdout, TrueColor) - return o.HasDarkBackground() + return output.HasDarkBackground() +} + +// EnvNoColor returns true if the environment variables explicitly disable color output +// by setting NO_COLOR (https://no-color.org/) +// or CLICOLOR/CLICOLOR_FORCE (https://bixense.com/clicolors/) +// If NO_COLOR is set, this will return true, ignoring CLICOLOR/CLICOLOR_FORCE +// If CLICOLOR=="0", it will be true only if CLICOLOR_FORCE is also "0" or is unset. +func (o *Output) EnvNoColor() bool { + return o.environ.Getenv("NO_COLOR") != "" || (o.environ.Getenv("CLICOLOR") == "0" && !o.cliColorForced()) } // EnvNoColor returns true if the environment variables explicitly disable color output @@ -61,7 +62,7 @@ func HasDarkBackground() bool { // If NO_COLOR is set, this will return true, ignoring CLICOLOR/CLICOLOR_FORCE // If CLICOLOR=="0", it will be true only if CLICOLOR_FORCE is also "0" or is unset. func EnvNoColor() bool { - return os.Getenv("NO_COLOR") != "" || (os.Getenv("CLICOLOR") == "0" && !cliColorForced()) + return output.EnvNoColor() } // EnvColorProfile returns the color profile based on environment variables set @@ -72,29 +73,27 @@ func EnvNoColor() bool { // If the terminal does not support any colors, but CLICOLOR_FORCE is set and not "0" // then the ANSI color profile will be returned. func EnvColorProfile() Profile { - if EnvNoColor() { - return Ascii - } - p := ColorProfile() - if cliColorForced() && p == Ascii { - return ANSI - } - return p + return output.EnvColorProfile() } -func envColorProfile() Profile { - if EnvNoColor() { +// EnvNoColor returns true if the environment variables explicitly disable color output +// by setting NO_COLOR (https://no-color.org/) +// or CLICOLOR/CLICOLOR_FORCE (https://bixense.com/clicolors/) +// If NO_COLOR is set, this will return true, ignoring CLICOLOR/CLICOLOR_FORCE +// If CLICOLOR=="0", it will be true only if CLICOLOR_FORCE is also "0" or is unset. +func (o *Output) EnvColorProfile() Profile { + if o.EnvNoColor() { return Ascii } - p := colorProfile() - if cliColorForced() && p == Ascii { + p := o.ColorProfile() + if o.cliColorForced() && p == Ascii { return ANSI } return p } -func cliColorForced() bool { - if forced := os.Getenv("CLICOLOR_FORCE"); forced != "" { +func (o *Output) cliColorForced() bool { + if forced := o.environ.Getenv("CLICOLOR_FORCE"); forced != "" { return forced != "0" } return false diff --git a/termenv_test.go b/termenv_test.go index 40f3a03..d24d68f 100644 --- a/termenv_test.go +++ b/termenv_test.go @@ -34,7 +34,7 @@ func TestLegacyTermEnv(t *testing.T) { } func TestTermEnv(t *testing.T) { - o := NewOutput(os.Stdout) + o := NewOutput(os.Stdout, &osEnviron{}) if o.Profile != TrueColor && o.Profile != Ascii { t.Errorf("Expected %d, got %d", TrueColor, o.Profile) } @@ -347,7 +347,8 @@ func TestEnvNoColor(t *testing.T) { for i := 0; i < len(test.environ); i += 2 { os.Setenv(test.environ[i], test.environ[i+1]) } - actual := EnvNoColor() + out := NewOutput(os.Stdout, &osEnviron{}) + actual := out.EnvNoColor() if test.expected != actual { t.Errorf("expected %t but was %t", test.expected, actual) } diff --git a/termenv_unix.go b/termenv_unix.go index fecff4f..9eeca55 100644 --- a/termenv_unix.go +++ b/termenv_unix.go @@ -5,7 +5,6 @@ package termenv import ( "fmt" - "os" "strconv" "strings" "time" @@ -18,9 +17,15 @@ const ( OSCTimeout = 5 * time.Second ) -func colorProfile() Profile { - term := os.Getenv("TERM") - colorTerm := os.Getenv("COLORTERM") +// ColorProfile returns the supported color profile: +// Ascii, ANSI, ANSI256, or TrueColor. +func (o *Output) ColorProfile() Profile { + if !o.isTTY() { + return Ascii + } + + term := o.environ.Getenv("TERM") + colorTerm := o.environ.Getenv("COLORTERM") switch strings.ToLower(colorTerm) { case "24bit": @@ -28,7 +33,7 @@ func colorProfile() Profile { case "truecolor": if strings.HasPrefix(term, "screen") { // tmux supports TrueColor, screen only ANSI256 - if os.Getenv("TERM_PROGRAM") != "tmux" { + if o.environ.Getenv("TERM_PROGRAM") != "tmux" { return ANSI256 } } @@ -68,7 +73,7 @@ func (o Output) foregroundColor() Color { } } - colorFGBG := os.Getenv("COLORFGBG") + colorFGBG := o.environ.Getenv("COLORFGBG") if strings.Contains(colorFGBG, ";") { c := strings.Split(colorFGBG, ";") i, err := strconv.Atoi(c[0]) @@ -90,7 +95,7 @@ func (o Output) backgroundColor() Color { } } - colorFGBG := os.Getenv("COLORFGBG") + colorFGBG := o.environ.Getenv("COLORFGBG") if strings.Contains(colorFGBG, ";") { c := strings.Split(colorFGBG, ";") i, err := strconv.Atoi(c[len(c)-1]) @@ -126,7 +131,7 @@ func waitForData(fd uintptr, timeout time.Duration) error { return nil } -func readNextByte(f *os.File) (byte, error) { +func readNextByte(f File) (byte, error) { if err := waitForData(f.Fd(), OSCTimeout); err != nil { return 0, err } @@ -147,7 +152,7 @@ func readNextByte(f *os.File) (byte, error) { // readNextResponse reads either an OSC response or a cursor position response: // * OSC response: "\x1b]11;rgb:1111/1111/1111\x1b\\" // * cursor position response: "\x1b[42;1R" -func readNextResponse(fd *os.File) (response string, isOSC bool, err error) { +func readNextResponse(fd File) (response string, isOSC bool, err error) { start, err := readNextByte(fd) if err != nil { return "", false, err @@ -213,7 +218,7 @@ func readNextResponse(fd *os.File) (response string, isOSC bool, err error) { func (o Output) termStatusReport(sequence int) (string, error) { // screen/tmux can't support OSC, because they can be connected to multiple // terminals concurrently. - term := os.Getenv("TERM") + term := o.environ.Getenv("TERM") if strings.HasPrefix(term, "screen") { return "", ErrStatusReport } diff --git a/termenv_windows.go b/termenv_windows.go index 879c306..9758fd0 100644 --- a/termenv_windows.go +++ b/termenv_windows.go @@ -4,22 +4,21 @@ package termenv import ( - "os" "strconv" "golang.org/x/sys/windows" ) -func colorProfile() Profile { - if os.Getenv("ConEmuANSI") == "ON" { +func (o *Output) ColorProfile() Profile { + if o.environ.Getenv("ConEmuANSI") == "ON" { return TrueColor } winVersion, _, buildNumber := windows.RtlGetNtVersionNumbers() if buildNumber < 10586 || winVersion < 10 { // No ANSI support before Windows 10 build 10586. - if os.Getenv("ANSICON") != "" { - conVersion := os.Getenv("ANSICON_VER") + if o.environ.Getenv("ANSICON") != "" { + conVersion := o.environ.Getenv("ANSICON_VER") cv, err := strconv.ParseInt(conVersion, 10, 64) if err != nil || cv < 181 { // No 8 bit color support before v1.81 release.