From eaab95e3efc8d30923f0c23f64f68078559fb70d Mon Sep 17 00:00:00 2001 From: Johan Walles Date: Sun, 17 Dec 2023 16:34:34 +0100 Subject: [PATCH] Inform ansiString() about terminal color count So that it can downsample colors when needed. --- moar.go | 2 +- twin/colors.go | 15 +++++++++------ twin/screen.go | 27 ++++++++++++++++++++------- twin/screen_test.go | 20 ++++++++++---------- twin/styles.go | 6 +++--- twin/styles_test.go | 2 +- 6 files changed, 44 insertions(+), 28 deletions(-) diff --git a/moar.go b/moar.go index 8e6103b..b78ab2f 100644 --- a/moar.go +++ b/moar.go @@ -500,7 +500,7 @@ func main() { panic("Invariant broken: stdout is not a terminal") } - screen, err := twin.NewScreenWithMouseMode(*mouseMode) + screen, err := twin.NewScreenWithMouseModeAndColorType(*mouseMode, *terminalColorsCount) if err != nil { // Ref: https://github.com/walles/moar/issues/149 log.Debug("Failed to set up screen for paging, pumping to stdout instead: ", err) diff --git a/twin/colors.go b/twin/colors.go index 47b8203..ee62999 100644 --- a/twin/colors.go +++ b/twin/colors.go @@ -1,6 +1,8 @@ package twin -import "fmt" +import ( + "fmt" +) // Create using NewColor16(), NewColor256 or NewColor24Bit(), or use // ColorDefault. @@ -83,7 +85,8 @@ func (color Color) colorValue() uint32 { // Render color into an ANSI string. // // Ref: https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters -func (color Color) ansiString(foreground bool) string { +func (color Color) ansiString(foreground bool, terminalColorCount ColorType) string { + // FIXME: Downsample colors to at most terminalColorCount colors as needed. value := color.colorValue() fgBgMarker := "3" @@ -124,14 +127,14 @@ func (color Color) ansiString(foreground bool) string { panic(fmt.Errorf("unhandled color type=%d value=%#x", color.colorType(), value)) } -func (color Color) ForegroundAnsiString() string { +func (color Color) ForegroundAnsiString(terminalColorCount ColorType) string { // FIXME: Test this function with all different color types. - return color.ansiString(true) + return color.ansiString(true, terminalColorCount) } -func (color Color) BackgroundAnsiString() string { +func (color Color) BackgroundAnsiString(terminalColorCount ColorType) string { // FIXME: Test this function with all different color types. - return color.ansiString(false) + return color.ansiString(false, terminalColorCount) } func (color Color) String() string { diff --git a/twin/screen.go b/twin/screen.go index 4899187..c91916f 100644 --- a/twin/screen.go +++ b/twin/screen.go @@ -77,6 +77,8 @@ type UnixScreen struct { ttyOut *os.File oldTtyOutMode uint32 //nolint Windows only + + terminalColorCount ColorType } // Example event: "\x1b[<65;127;41M" @@ -94,18 +96,29 @@ type UnixScreen struct { // * "M" marks the end of the mouse event. var MOUSE_EVENT_REGEX = regexp.MustCompile("^\x1b\\[<([0-9]+);([0-9]+);([0-9]+)M") +// NewScreen() requires Close() to be called after you are done with your new +// screen, most likely somewhere in your shutdown code. func NewScreen() (Screen, error) { return NewScreenWithMouseMode(MouseModeAuto) } -// NewScreen() requires Close() to be called after you are done with your new -// screen, most likely somewhere in your shutdown code. func NewScreenWithMouseMode(mouseMode MouseMode) (Screen, error) { + terminalColorCount := ColorType24bit + if strings.Contains(os.Getenv("TERM"), "256") { + // Covers "xterm-256color" as used by the macOS Terminal + terminalColorCount = ColorType256 + } + return NewScreenWithMouseModeAndColorType(mouseMode, terminalColorCount) +} + +func NewScreenWithMouseModeAndColorType(mouseMode MouseMode, terminalColorCount ColorType) (Screen, error) { if !term.IsTerminal(int(os.Stdout.Fd())) { return nil, fmt.Errorf("stdout (fd=%d) must be a terminal for paging to work", os.Stdout.Fd()) } - screen := UnixScreen{} + screen := UnixScreen{ + terminalColorCount: terminalColorCount, + } // The number "80" here is from manual testing on my MacBook: // @@ -505,7 +518,7 @@ func (screen *UnixScreen) Clear() { // Returns the rendered line, plus how many information carrying cells went into // it -func renderLine(row []Cell) (string, int) { +func renderLine(row []Cell, terminalColorCount ColorType) (string, int) { // Strip trailing whitespace lastSignificantCellIndex := len(row) - 1 for ; lastSignificantCellIndex >= 0; lastSignificantCellIndex-- { @@ -538,7 +551,7 @@ func renderLine(row []Cell) (string, int) { } if style != lastStyle { - builder.WriteString(style.RenderUpdateFrom(lastStyle)) + builder.WriteString(style.RenderUpdateFrom(lastStyle, terminalColorCount)) lastStyle = style } @@ -547,7 +560,7 @@ func renderLine(row []Cell) (string, int) { // Clear to end of line // https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_(Control_Sequence_Introducer)_sequences - builder.WriteString(StyleDefault.RenderUpdateFrom(lastStyle)) + builder.WriteString(StyleDefault.RenderUpdateFrom(lastStyle, terminalColorCount)) builder.WriteString("\x1b[K") return builder.String(), len(row) @@ -572,7 +585,7 @@ func (screen *UnixScreen) showNLines(height int, clearFirst bool) { } for row := 0; row < height; row++ { - rendered, lineLength := renderLine(screen.cells[row]) + rendered, lineLength := renderLine(screen.cells[row], screen.terminalColorCount) builder.WriteString(rendered) wasLastLine := row == (height - 1) diff --git a/twin/screen_test.go b/twin/screen_test.go index c59d523..7c27579 100644 --- a/twin/screen_test.go +++ b/twin/screen_test.go @@ -61,7 +61,7 @@ func TestRenderLine(t *testing.T) { }, } - rendered, count := renderLine(row) + rendered, count := renderLine(row, ColorType16) assert.Equal(t, count, 2) reset := "" reversed := "" @@ -76,7 +76,7 @@ func TestRenderLine(t *testing.T) { func TestRenderLineEmpty(t *testing.T) { row := []Cell{} - rendered, count := renderLine(row) + rendered, count := renderLine(row, ColorType16) assert.Equal(t, count, 0) // All lines are expected to stand on their own, so we always need to clear @@ -92,7 +92,7 @@ func TestRenderLineLastReversed(t *testing.T) { }, } - rendered, count := renderLine(row) + rendered, count := renderLine(row, ColorType16) assert.Equal(t, count, 1) reset := "" reversed := "" @@ -110,7 +110,7 @@ func TestRenderLineLastNonSpace(t *testing.T) { }, } - rendered, count := renderLine(row) + rendered, count := renderLine(row, ColorType16) assert.Equal(t, count, 1) reset := "" clearToEol := "" @@ -131,7 +131,7 @@ func TestRenderLineLastReversedPlusTrailingSpace(t *testing.T) { }, } - rendered, count := renderLine(row) + rendered, count := renderLine(row, ColorType16) assert.Equal(t, count, 1) reset := "" reversed := "" @@ -153,7 +153,7 @@ func TestRenderLineOnlyTrailingSpaces(t *testing.T) { }, } - rendered, count := renderLine(row) + rendered, count := renderLine(row, ColorType16) assert.Equal(t, count, 0) // All lines are expected to stand on their own, so we always need to clear @@ -169,7 +169,7 @@ func TestRenderLineLastReversedSpaces(t *testing.T) { }, } - rendered, count := renderLine(row) + rendered, count := renderLine(row, ColorType16) assert.Equal(t, count, 1) reset := "" reversed := "" @@ -186,7 +186,7 @@ func TestRenderLineNonPrintable(t *testing.T) { }, } - rendered, count := renderLine(row) + rendered, count := renderLine(row, ColorType16) assert.Equal(t, count, 1) reset := "" white := "" @@ -207,7 +207,7 @@ func TestRenderHyperlinkAtEndOfLine(t *testing.T) { }, } - rendered, count := renderLine(row) + rendered, count := renderLine(row, ColorType16) assert.Equal(t, count, 1) assert.Equal(t, @@ -232,7 +232,7 @@ func TestMultiCharHyperlink(t *testing.T) { }, } - rendered, count := renderLine(row) + rendered, count := renderLine(row, ColorType16) assert.Equal(t, count, 3) assert.Equal(t, diff --git a/twin/styles.go b/twin/styles.go index 1eee07c..72817a5 100644 --- a/twin/styles.go +++ b/twin/styles.go @@ -137,7 +137,7 @@ func (style Style) Foreground(color Color) Style { // Emit an ANSI escape sequence switching from a previous style to the current // one. -func (current Style) RenderUpdateFrom(previous Style) string { +func (current Style) RenderUpdateFrom(previous Style, terminalColorCount ColorType) string { if current == previous { // Shortcut for the common case return "" @@ -150,11 +150,11 @@ func (current Style) RenderUpdateFrom(previous Style) string { var builder strings.Builder if current.fg != previous.fg { - builder.WriteString(current.fg.ForegroundAnsiString()) + builder.WriteString(current.fg.ForegroundAnsiString(terminalColorCount)) } if current.bg != previous.bg { - builder.WriteString(current.bg.BackgroundAnsiString()) + builder.WriteString(current.bg.BackgroundAnsiString(terminalColorCount)) } // Handle AttrDim / AttrBold changes diff --git a/twin/styles_test.go b/twin/styles_test.go index a4cdb88..0cea0fb 100644 --- a/twin/styles_test.go +++ b/twin/styles_test.go @@ -12,6 +12,6 @@ func TestHyperlinkToNormal(t *testing.T) { style := StyleDefault.WithHyperlink(&url) assert.Equal(t, - strings.ReplaceAll(StyleDefault.RenderUpdateFrom(style), "", "ESC"), + strings.ReplaceAll(StyleDefault.RenderUpdateFrom(style, ColorType16), "", "ESC"), "ESC]8;;ESC\\") }