From 42b0bc7e255e7f67c1254cb257eb982530331444 Mon Sep 17 00:00:00 2001 From: Johan Walles Date: Mon, 12 Aug 2024 18:36:49 +0200 Subject: [PATCH 1/9] Improve naming --- moar.go | 28 +++++++++---------- moar_test.go | 2 +- twin/colors.go | 66 ++++++++++++++++++++++----------------------- twin/colors_test.go | 10 +++---- twin/screen.go | 10 +++---- twin/screen_test.go | 20 +++++++------- twin/styles.go | 2 +- twin/styles_test.go | 2 +- 8 files changed, 70 insertions(+), 70 deletions(-) diff --git a/moar.go b/moar.go index 763f0972..b31095a6 100644 --- a/moar.go +++ b/moar.go @@ -38,7 +38,7 @@ const defaultLightTheme = "tango" var versionString = "Should be set when building, please use build.sh to build" -func renderLessTermcapEnvVar(envVarName string, description string, colors twin.ColorType) string { +func renderLessTermcapEnvVar(envVarName string, description string, colors twin.ColorCount) string { value := os.Getenv(envVarName) if len(value) == 0 { return "" @@ -67,7 +67,7 @@ func renderLessTermcapEnvVar(envVarName string, description string, colors twin. ) } -func renderPagerEnvVar(name string, colors twin.ColorType) string { +func renderPagerEnvVar(name string, colors twin.ColorCount) string { bold := twin.StyleDefault.WithAttr(twin.AttrBold).RenderUpdateFrom(twin.StyleDefault, colors) notBold := twin.StyleDefault.RenderUpdateFrom(twin.StyleDefault.WithAttr(twin.AttrBold), colors) @@ -129,14 +129,14 @@ func printCommandline(output io.Writer) { fmt.Fprintln(output) } -func heading(text string, colors twin.ColorType) string { +func heading(text string, colors twin.ColorCount) string { style := twin.StyleDefault.WithAttr(twin.AttrItalic) prefix := style.RenderUpdateFrom(twin.StyleDefault, colors) suffix := twin.StyleDefault.RenderUpdateFrom(style, colors) return prefix + text + suffix } -func printUsage(flagSet *flag.FlagSet, colors twin.ColorType) { +func printUsage(flagSet *flag.FlagSet, colors twin.ColorCount) { // This controls where PrintDefaults() prints, see below flagSet.SetOutput(os.Stdout) @@ -315,7 +315,7 @@ func parseStyleOption(styleOption string) (*chroma.Style, error) { return style, nil } -func parseColorsOption(colorsOption string) (twin.ColorType, error) { +func parseColorsOption(colorsOption string) (twin.ColorCount, error) { if strings.ToLower(colorsOption) == "auto" { colorsOption = "16M" if os.Getenv("COLORTERM") != "truecolor" && strings.Contains(os.Getenv("TERM"), "256") { @@ -326,16 +326,16 @@ func parseColorsOption(colorsOption string) (twin.ColorType, error) { switch strings.ToUpper(colorsOption) { case "8": - return twin.ColorType8, nil + return twin.ColorCount8, nil case "16": - return twin.ColorType16, nil + return twin.ColorCount16, nil case "256": - return twin.ColorType256, nil + return twin.ColorCount256, nil case "16M": - return twin.ColorType24bit, nil + return twin.ColorCount24bit, nil } - var noColor twin.ColorType + var noColor twin.ColorCount return noColor, fmt.Errorf("Valid counts are 8, 16, 256, 16M or auto") } @@ -519,7 +519,7 @@ func noLineNumbersDefault() bool { // Can return a nil pager on --help or --version, or if pumping to stdout. func pagerFromArgs( args []string, - newScreen func(mouseMode twin.MouseMode, terminalColorCount twin.ColorType) (twin.Screen, error), + newScreen func(mouseMode twin.MouseMode, terminalColorCount twin.ColorCount) (twin.Screen, error), stdinIsRedirected bool, stdoutIsRedirected bool, ) ( @@ -674,11 +674,11 @@ func pagerFromArgs( } formatter := formatters.TTY256 - if *terminalColorsCount == twin.ColorType8 { + if *terminalColorsCount == twin.ColorCount8 { formatter = formatters.TTY8 - } else if *terminalColorsCount == twin.ColorType16 { + } else if *terminalColorsCount == twin.ColorCount16 { formatter = formatters.TTY16 - } else if *terminalColorsCount == twin.ColorType24bit { + } else if *terminalColorsCount == twin.ColorCount24bit { formatter = formatters.TTY16m } diff --git a/moar_test.go b/moar_test.go index dede093b..e6fc8a26 100644 --- a/moar_test.go +++ b/moar_test.go @@ -19,7 +19,7 @@ func TestParseScrollHint(t *testing.T) { func TestPageOneInputFile(t *testing.T) { pager, screen, _, formatter, err := pagerFromArgs( []string{"", "moar_test.go"}, - func(_ twin.MouseMode, _ twin.ColorType) (twin.Screen, error) { + func(_ twin.MouseMode, _ twin.ColorCount) (twin.Screen, error) { return twin.NewFakeScreen(80, 24), nil }, false, // stdin is redirected diff --git a/twin/colors.go b/twin/colors.go index c17593ef..e385001b 100644 --- a/twin/colors.go +++ b/twin/colors.go @@ -10,30 +10,30 @@ import ( // Create using NewColor16(), NewColor256 or NewColor24Bit(), or use // ColorDefault. type Color uint32 -type ColorType uint8 +type ColorCount uint8 const ( // Default foreground / background color - ColorTypeDefault ColorType = iota + ColorCountDefault ColorCount = iota // https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit // // Note that this type is only used for output, on input we store 3 bit // colors as 4 bit colors since they map to the same values. - ColorType8 + ColorCount8 // https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit - ColorType16 + ColorCount16 // https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit - ColorType256 + ColorCount256 // RGB: https://en.wikipedia.org/wiki/ANSI_escape_code#24-bit - ColorType24bit + ColorCount24bit ) // Reset to default foreground / background color -var ColorDefault = newColor(ColorTypeDefault, 0) +var ColorDefault = newColor(ColorCountDefault, 0) // From: https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit var colorNames16 = map[int]string{ @@ -55,30 +55,30 @@ var colorNames16 = map[int]string{ 15: "15 bright white", } -func newColor(colorType ColorType, value uint32) Color { +func newColor(colorType ColorCount, value uint32) Color { return Color(value | (uint32(colorType) << 24)) } // Four bit colors as defined here: // https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit func NewColor16(colorNumber0to15 int) Color { - return newColor(ColorType16, uint32(colorNumber0to15)) + return newColor(ColorCount16, uint32(colorNumber0to15)) } func NewColor256(colorNumber uint8) Color { - return newColor(ColorType256, uint32(colorNumber)) + return newColor(ColorCount256, uint32(colorNumber)) } func NewColor24Bit(red uint8, green uint8, blue uint8) Color { - return newColor(ColorType24bit, (uint32(red)<<16)+(uint32(green)<<8)+(uint32(blue)<<0)) + return newColor(ColorCount24bit, (uint32(red)<<16)+(uint32(green)<<8)+(uint32(blue)<<0)) } func NewColorHex(rgb uint32) Color { - return newColor(ColorType24bit, rgb) + return newColor(ColorCount24bit, rgb) } -func (color Color) ColorType() ColorType { - return ColorType(color >> 24) +func (color Color) ColorType() ColorCount { + return ColorCount(color >> 24) } func (color Color) colorValue() uint32 { @@ -88,19 +88,19 @@ 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, terminalColorCount ColorType) string { +func (color Color) ansiString(foreground bool, terminalColorCount ColorCount) string { fgBgMarker := "3" if !foreground { fgBgMarker = "4" } - if color.ColorType() == ColorTypeDefault { + if color.ColorType() == ColorCountDefault { return fmt.Sprint("\x1b[", fgBgMarker, "9m") } color = color.downsampleTo(terminalColorCount) - if color.ColorType() == ColorType16 { + if color.ColorType() == ColorCount16 { value := color.colorValue() if value < 8 { return fmt.Sprint("\x1b[", fgBgMarker, value, "m") @@ -113,14 +113,14 @@ func (color Color) ansiString(foreground bool, terminalColorCount ColorType) str } } - if color.ColorType() == ColorType256 { + if color.ColorType() == ColorCount256 { value := color.colorValue() if value <= 255 { return fmt.Sprint("\x1b[", fgBgMarker, "8;5;", value, "m") } } - if color.ColorType() == ColorType24bit { + if color.ColorType() == ColorCount24bit { value := color.colorValue() red := (value & 0xff0000) >> 16 green := (value & 0xff00) >> 8 @@ -132,31 +132,31 @@ func (color Color) ansiString(foreground bool, terminalColorCount ColorType) str panic(fmt.Errorf("unhandled color type=%d %s", color.ColorType(), color.String())) } -func (color Color) ForegroundAnsiString(terminalColorCount ColorType) string { +func (color Color) ForegroundAnsiString(terminalColorCount ColorCount) string { // FIXME: Test this function with all different color types. return color.ansiString(true, terminalColorCount) } -func (color Color) BackgroundAnsiString(terminalColorCount ColorType) string { +func (color Color) BackgroundAnsiString(terminalColorCount ColorCount) string { // FIXME: Test this function with all different color types. return color.ansiString(false, terminalColorCount) } func (color Color) String() string { switch color.ColorType() { - case ColorTypeDefault: + case ColorCountDefault: return "Default color" - case ColorType16: + case ColorCount16: return colorNames16[int(color.colorValue())] - case ColorType256: + case ColorCount256: if color.colorValue() < 16 { return colorNames16[int(color.colorValue())] } return fmt.Sprintf("#%02x", color.colorValue()) - case ColorType24bit: + case ColorCount24bit: return fmt.Sprintf("#%06x", color.colorValue()) } @@ -164,11 +164,11 @@ func (color Color) String() string { } func (color Color) to24Bit() Color { - if color.ColorType() == ColorType24bit { + if color.ColorType() == ColorCount24bit { return color } - if color.ColorType() == ColorType8 || color.ColorType() == ColorType16 || color.ColorType() == ColorType256 { + if color.ColorType() == ColorCount8 || color.ColorType() == ColorCount16 || color.ColorType() == ColorCount256 { r0, g0, b0 := color256ToRGB(uint8(color.colorValue())) return NewColor24Bit(r0, g0, b0) } @@ -176,8 +176,8 @@ func (color Color) to24Bit() Color { panic(fmt.Errorf("unhandled color type %d", color.ColorType())) } -func (color Color) downsampleTo(terminalColorCount ColorType) Color { - if color.ColorType() == ColorTypeDefault || terminalColorCount == ColorTypeDefault { +func (color Color) downsampleTo(terminalColorCount ColorCount) Color { + if color.ColorType() == ColorCountDefault || terminalColorCount == ColorCountDefault { panic(fmt.Errorf("downsampling to or from default color not supported, %s -> %#v", color.String(), terminalColorCount)) } @@ -192,13 +192,13 @@ func (color Color) downsampleTo(terminalColorCount ColorType) Color { var scanFirst int var scanLast int switch terminalColorCount { - case ColorType8: + case ColorCount8: scanFirst = 0 scanLast = 7 - case ColorType16: + case ColorCount16: scanFirst = 0 scanLast = 15 - case ColorType256: + case ColorCount256: // Colors 0-15 can be customized by the user, so we skip them and use // only the well defined ones scanFirst = 16 @@ -234,7 +234,7 @@ func (color Color) downsampleTo(terminalColorCount ColorType) Color { // The result from this function has been scaled to 0.0-1.0, where 1.0 is the // distance between black and white. func (color Color) Distance(other Color) float64 { - if color.ColorType() != ColorType24bit { + if color.ColorType() != ColorCount24bit { panic(fmt.Errorf("contrast only supported for 24 bit colors, got %s vs %s", color.String(), other.String())) } diff --git a/twin/colors_test.go b/twin/colors_test.go index 9c9ade46..bab5177e 100644 --- a/twin/colors_test.go +++ b/twin/colors_test.go @@ -8,14 +8,14 @@ import ( func TestDownsample24BitsTo16Colors(t *testing.T) { assert.Equal(t, - NewColor24Bit(255, 255, 255).downsampleTo(ColorType16), + NewColor24Bit(255, 255, 255).downsampleTo(ColorCount16), NewColor16(15), ) } func TestDownsample24BitsTo256Colors(t *testing.T) { assert.Equal(t, - NewColor24Bit(255, 255, 255).downsampleTo(ColorType256), + NewColor24Bit(255, 255, 255).downsampleTo(ColorCount256), // From https://jonasjacek.github.io/colors/ NewColor256(231), @@ -24,21 +24,21 @@ func TestDownsample24BitsTo256Colors(t *testing.T) { func TestRealWorldDownsampling(t *testing.T) { assert.Equal(t, - NewColor24Bit(0xd0, 0xd0, 0xd0).downsampleTo(ColorType256), + NewColor24Bit(0xd0, 0xd0, 0xd0).downsampleTo(ColorCount256), NewColor256(252), // From https://jonasjacek.github.io/colors/ ) } func TestAnsiStringWithDownSampling(t *testing.T) { assert.Equal(t, - NewColor24Bit(0xd0, 0xd0, 0xd0).ansiString(true, ColorType256), + NewColor24Bit(0xd0, 0xd0, 0xd0).ansiString(true, ColorCount256), "\x1b[38;5;252m", ) } func TestAnsiStringDefault(t *testing.T) { assert.Equal(t, - ColorDefault.ansiString(true, ColorType16), + ColorDefault.ansiString(true, ColorCount16), "\x1b[39m", ) } diff --git a/twin/screen.go b/twin/screen.go index 30e179d6..54183eb5 100644 --- a/twin/screen.go +++ b/twin/screen.go @@ -96,7 +96,7 @@ type UnixScreen struct { ttyOut *os.File oldTtyOutMode uint32 //nolint Windows only - terminalColorCount ColorType + terminalColorCount ColorCount } // Example event: "\x1b[<65;127;41M" @@ -121,15 +121,15 @@ func NewScreen() (Screen, error) { } func NewScreenWithMouseMode(mouseMode MouseMode) (Screen, error) { - terminalColorCount := ColorType24bit + terminalColorCount := ColorCount24bit if os.Getenv("COLORTERM") != "truecolor" && strings.Contains(os.Getenv("TERM"), "256") { // Covers "xterm-256color" as used by the macOS Terminal - terminalColorCount = ColorType256 + terminalColorCount = ColorCount256 } return NewScreenWithMouseModeAndColorType(mouseMode, terminalColorCount) } -func NewScreenWithMouseModeAndColorType(mouseMode MouseMode, terminalColorCount ColorType) (Screen, error) { +func NewScreenWithMouseModeAndColorType(mouseMode MouseMode, terminalColorCount ColorCount) (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()) } @@ -657,7 +657,7 @@ func (screen *UnixScreen) Clear() { // Returns the rendered line, plus how many information carrying cells went into // it -func renderLine(row []Cell, terminalColorCount ColorType) (string, int) { +func renderLine(row []Cell, terminalColorCount ColorCount) (string, int) { // Strip trailing whitespace lastSignificantCellIndex := len(row) - 1 for ; lastSignificantCellIndex >= 0; lastSignificantCellIndex-- { diff --git a/twin/screen_test.go b/twin/screen_test.go index 4f1957ef..98ea1c07 100644 --- a/twin/screen_test.go +++ b/twin/screen_test.go @@ -64,7 +64,7 @@ func TestRenderLine(t *testing.T) { }, } - rendered, count := renderLine(row, ColorType16) + rendered, count := renderLine(row, ColorCount16) assert.Equal(t, count, 2) reset := "" reversed := "" @@ -79,7 +79,7 @@ func TestRenderLine(t *testing.T) { func TestRenderLineEmpty(t *testing.T) { row := []Cell{} - rendered, count := renderLine(row, ColorType16) + rendered, count := renderLine(row, ColorCount16) assert.Equal(t, count, 0) // All lines are expected to stand on their own, so we always need to clear @@ -95,7 +95,7 @@ func TestRenderLineLastReversed(t *testing.T) { }, } - rendered, count := renderLine(row, ColorType16) + rendered, count := renderLine(row, ColorCount16) assert.Equal(t, count, 1) reset := "" reversed := "" @@ -113,7 +113,7 @@ func TestRenderLineLastNonSpace(t *testing.T) { }, } - rendered, count := renderLine(row, ColorType16) + rendered, count := renderLine(row, ColorCount16) assert.Equal(t, count, 1) reset := "" clearToEol := "" @@ -134,7 +134,7 @@ func TestRenderLineLastReversedPlusTrailingSpace(t *testing.T) { }, } - rendered, count := renderLine(row, ColorType16) + rendered, count := renderLine(row, ColorCount16) assert.Equal(t, count, 1) reset := "" reversed := "" @@ -156,7 +156,7 @@ func TestRenderLineOnlyTrailingSpaces(t *testing.T) { }, } - rendered, count := renderLine(row, ColorType16) + rendered, count := renderLine(row, ColorCount16) assert.Equal(t, count, 0) // All lines are expected to stand on their own, so we always need to clear @@ -172,7 +172,7 @@ func TestRenderLineLastReversedSpaces(t *testing.T) { }, } - rendered, count := renderLine(row, ColorType16) + rendered, count := renderLine(row, ColorCount16) assert.Equal(t, count, 1) reset := "" reversed := "" @@ -189,7 +189,7 @@ func TestRenderLineNonPrintable(t *testing.T) { }, } - rendered, count := renderLine(row, ColorType16) + rendered, count := renderLine(row, ColorCount16) assert.Equal(t, count, 1) reset := "" white := "" @@ -210,7 +210,7 @@ func TestRenderHyperlinkAtEndOfLine(t *testing.T) { }, } - rendered, count := renderLine(row, ColorType16) + rendered, count := renderLine(row, ColorCount16) assert.Equal(t, count, 1) assert.Equal(t, @@ -235,7 +235,7 @@ func TestMultiCharHyperlink(t *testing.T) { }, } - rendered, count := renderLine(row, ColorType16) + rendered, count := renderLine(row, ColorCount16) assert.Equal(t, count, 3) assert.Equal(t, diff --git a/twin/styles.go b/twin/styles.go index d97644bf..313defc7 100644 --- a/twin/styles.go +++ b/twin/styles.go @@ -139,7 +139,7 @@ func (style Style) WithForeground(color Color) Style { // one. // //revive:disable-next-line:receiver-naming -func (current Style) RenderUpdateFrom(previous Style, terminalColorCount ColorType) string { +func (current Style) RenderUpdateFrom(previous Style, terminalColorCount ColorCount) string { if current == previous { // Shortcut for the common case return "" diff --git a/twin/styles_test.go b/twin/styles_test.go index 0cea0fb1..c3d34e49 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, ColorType16), "", "ESC"), + strings.ReplaceAll(StyleDefault.RenderUpdateFrom(style, ColorCount16), "", "ESC"), "ESC]8;;ESC\\") } From 44136f65248dfdb29a7b7ceb9c6703f8d3a41df8 Mon Sep 17 00:00:00 2001 From: Johan Walles Date: Mon, 12 Aug 2024 18:53:44 +0200 Subject: [PATCH 2/9] More renaming, forgot some things --- moar.go | 2 +- twin/colors.go | 32 ++++++++++++++++---------------- twin/screen.go | 8 ++++---- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/moar.go b/moar.go index b31095a6..83e2dd1c 100644 --- a/moar.go +++ b/moar.go @@ -803,7 +803,7 @@ func main() { pager, screen, style, formatter, err := pagerFromArgs( os.Args, - twin.NewScreenWithMouseModeAndColorType, + twin.NewScreenWithMouseModeAndColorCount, stdinIsRedirected, stdoutIsRedirected, ) diff --git a/twin/colors.go b/twin/colors.go index e385001b..54ca2825 100644 --- a/twin/colors.go +++ b/twin/colors.go @@ -55,8 +55,8 @@ var colorNames16 = map[int]string{ 15: "15 bright white", } -func newColor(colorType ColorCount, value uint32) Color { - return Color(value | (uint32(colorType) << 24)) +func newColor(colorCount ColorCount, value uint32) Color { + return Color(value | (uint32(colorCount) << 24)) } // Four bit colors as defined here: @@ -77,7 +77,7 @@ func NewColorHex(rgb uint32) Color { return newColor(ColorCount24bit, rgb) } -func (color Color) ColorType() ColorCount { +func (color Color) ColorCount() ColorCount { return ColorCount(color >> 24) } @@ -94,13 +94,13 @@ func (color Color) ansiString(foreground bool, terminalColorCount ColorCount) st fgBgMarker = "4" } - if color.ColorType() == ColorCountDefault { + if color.ColorCount() == ColorCountDefault { return fmt.Sprint("\x1b[", fgBgMarker, "9m") } color = color.downsampleTo(terminalColorCount) - if color.ColorType() == ColorCount16 { + if color.ColorCount() == ColorCount16 { value := color.colorValue() if value < 8 { return fmt.Sprint("\x1b[", fgBgMarker, value, "m") @@ -113,14 +113,14 @@ func (color Color) ansiString(foreground bool, terminalColorCount ColorCount) st } } - if color.ColorType() == ColorCount256 { + if color.ColorCount() == ColorCount256 { value := color.colorValue() if value <= 255 { return fmt.Sprint("\x1b[", fgBgMarker, "8;5;", value, "m") } } - if color.ColorType() == ColorCount24bit { + if color.ColorCount() == ColorCount24bit { value := color.colorValue() red := (value & 0xff0000) >> 16 green := (value & 0xff00) >> 8 @@ -129,7 +129,7 @@ func (color Color) ansiString(foreground bool, terminalColorCount ColorCount) st return fmt.Sprint("\x1b[", fgBgMarker, "8;2;", red, ";", green, ";", blue, "m") } - panic(fmt.Errorf("unhandled color type=%d %s", color.ColorType(), color.String())) + panic(fmt.Errorf("unhandled color type=%d %s", color.ColorCount(), color.String())) } func (color Color) ForegroundAnsiString(terminalColorCount ColorCount) string { @@ -143,7 +143,7 @@ func (color Color) BackgroundAnsiString(terminalColorCount ColorCount) string { } func (color Color) String() string { - switch color.ColorType() { + switch color.ColorCount() { case ColorCountDefault: return "Default color" @@ -160,28 +160,28 @@ func (color Color) String() string { return fmt.Sprintf("#%06x", color.colorValue()) } - panic(fmt.Errorf("unhandled color type %d", color.ColorType())) + panic(fmt.Errorf("unhandled color type %d", color.ColorCount())) } func (color Color) to24Bit() Color { - if color.ColorType() == ColorCount24bit { + if color.ColorCount() == ColorCount24bit { return color } - if color.ColorType() == ColorCount8 || color.ColorType() == ColorCount16 || color.ColorType() == ColorCount256 { + if color.ColorCount() == ColorCount8 || color.ColorCount() == ColorCount16 || color.ColorCount() == ColorCount256 { r0, g0, b0 := color256ToRGB(uint8(color.colorValue())) return NewColor24Bit(r0, g0, b0) } - panic(fmt.Errorf("unhandled color type %d", color.ColorType())) + panic(fmt.Errorf("unhandled color type %d", color.ColorCount())) } func (color Color) downsampleTo(terminalColorCount ColorCount) Color { - if color.ColorType() == ColorCountDefault || terminalColorCount == ColorCountDefault { + if color.ColorCount() == ColorCountDefault || terminalColorCount == ColorCountDefault { panic(fmt.Errorf("downsampling to or from default color not supported, %s -> %#v", color.String(), terminalColorCount)) } - if color.ColorType() <= terminalColorCount { + if color.ColorCount() <= terminalColorCount { // Already low enough return color } @@ -234,7 +234,7 @@ func (color Color) downsampleTo(terminalColorCount ColorCount) Color { // The result from this function has been scaled to 0.0-1.0, where 1.0 is the // distance between black and white. func (color Color) Distance(other Color) float64 { - if color.ColorType() != ColorCount24bit { + if color.ColorCount() != ColorCount24bit { panic(fmt.Errorf("contrast only supported for 24 bit colors, got %s vs %s", color.String(), other.String())) } diff --git a/twin/screen.go b/twin/screen.go index 54183eb5..006dbd57 100644 --- a/twin/screen.go +++ b/twin/screen.go @@ -126,10 +126,10 @@ func NewScreenWithMouseMode(mouseMode MouseMode) (Screen, error) { // Covers "xterm-256color" as used by the macOS Terminal terminalColorCount = ColorCount256 } - return NewScreenWithMouseModeAndColorType(mouseMode, terminalColorCount) + return NewScreenWithMouseModeAndColorCount(mouseMode, terminalColorCount) } -func NewScreenWithMouseModeAndColorType(mouseMode MouseMode, terminalColorCount ColorCount) (Screen, error) { +func NewScreenWithMouseModeAndColorCount(mouseMode MouseMode, terminalColorCount ColorCount) (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()) } @@ -181,7 +181,7 @@ func NewScreenWithMouseModeAndColorType(mouseMode MouseMode, terminalColorCount go func() { defer func() { - panicHandler("NewScreenWithMouseModeAndColorType()/mainLoop()", recover()) + panicHandler("NewScreenWithMouseModeAndColorCount()/mainLoop()", recover()) }() screen.mainLoop() @@ -413,7 +413,7 @@ func (screen *UnixScreen) mainLoop() { } // We only expect this on entry, it's requested right before we start - // the main loop in NewScreenWithMouseModeAndColorType(). + // the main loop in NewScreenWithMouseModeAndColorCount(). expectingTerminalBackgroundColor = false if count > maxBytesRead { From f26ae4ff9a5f90e4d2c20707f9f6b5fd30b5a216 Mon Sep 17 00:00:00 2001 From: Johan Walles Date: Mon, 12 Aug 2024 20:25:53 +0200 Subject: [PATCH 3/9] Introduce colorType to ansiString --- twin/colors.go | 52 +++++++++++++++++++++++++++++++++------------ twin/colors_test.go | 15 +++++++++---- 2 files changed, 49 insertions(+), 18 deletions(-) diff --git a/twin/colors.go b/twin/colors.go index 54ca2825..4ad3c0ba 100644 --- a/twin/colors.go +++ b/twin/colors.go @@ -10,6 +10,7 @@ import ( // Create using NewColor16(), NewColor256 or NewColor24Bit(), or use // ColorDefault. type Color uint32 + type ColorCount uint8 const ( @@ -32,6 +33,14 @@ const ( ColorCount24bit ) +type colorType uint8 + +const ( + colorTypeForeground colorType = iota + colorTypeBackground + colorTypeUnderline +) + // Reset to default foreground / background color var ColorDefault = newColor(ColorCountDefault, 0) @@ -88,35 +97,50 @@ 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, terminalColorCount ColorCount) string { - fgBgMarker := "3" - if !foreground { - fgBgMarker = "4" +func (color Color) ansiString(cType colorType, terminalColorCount ColorCount) string { + var typeMarker string + if cType == colorTypeForeground { + typeMarker = "3" + } else if cType == colorTypeBackground { + typeMarker = "4" + } else if cType == colorTypeUnderline { + typeMarker = "5" + } else { + panic(fmt.Errorf("unhandled color type %d", cType)) } if color.ColorCount() == ColorCountDefault { - return fmt.Sprint("\x1b[", fgBgMarker, "9m") + return fmt.Sprint("\x1b[", typeMarker, "9m") } color = color.downsampleTo(terminalColorCount) + // We never create any ColorCount8 colors, but we store them as + // ColorCount16. So this if() statement will cover both. if color.ColorCount() == ColorCount16 { + if cType == colorTypeUnderline { + // Only 256 and 24 bit colors supported for underline color + return "" + } + value := color.colorValue() if value < 8 { - return fmt.Sprint("\x1b[", fgBgMarker, value, "m") + return fmt.Sprint("\x1b[", typeMarker, value, "m") } else if value <= 15 { - fgBgMarker := "9" - if !foreground { - fgBgMarker = "10" + typeMarker := "9" + if cType == colorTypeBackground { + typeMarker = "10" } - return fmt.Sprint("\x1b[", fgBgMarker, value-8, "m") + return fmt.Sprint("\x1b[", typeMarker, value-8, "m") } + + panic(fmt.Errorf("unhandled color16 value %d", value)) } if color.ColorCount() == ColorCount256 { value := color.colorValue() if value <= 255 { - return fmt.Sprint("\x1b[", fgBgMarker, "8;5;", value, "m") + return fmt.Sprint("\x1b[", typeMarker, "8;5;", value, "m") } } @@ -126,7 +150,7 @@ func (color Color) ansiString(foreground bool, terminalColorCount ColorCount) st green := (value & 0xff00) >> 8 blue := value & 0xff - return fmt.Sprint("\x1b[", fgBgMarker, "8;2;", red, ";", green, ";", blue, "m") + return fmt.Sprint("\x1b[", typeMarker, "8;2;", red, ";", green, ";", blue, "m") } panic(fmt.Errorf("unhandled color type=%d %s", color.ColorCount(), color.String())) @@ -134,12 +158,12 @@ func (color Color) ansiString(foreground bool, terminalColorCount ColorCount) st func (color Color) ForegroundAnsiString(terminalColorCount ColorCount) string { // FIXME: Test this function with all different color types. - return color.ansiString(true, terminalColorCount) + return color.ansiString(colorTypeForeground, terminalColorCount) } func (color Color) BackgroundAnsiString(terminalColorCount ColorCount) string { // FIXME: Test this function with all different color types. - return color.ansiString(false, terminalColorCount) + return color.ansiString(colorTypeBackground, terminalColorCount) } func (color Color) String() string { diff --git a/twin/colors_test.go b/twin/colors_test.go index bab5177e..a47f1fcb 100644 --- a/twin/colors_test.go +++ b/twin/colors_test.go @@ -1,6 +1,7 @@ package twin import ( + "strings" "testing" "gotest.tools/v3/assert" @@ -30,16 +31,22 @@ func TestRealWorldDownsampling(t *testing.T) { } func TestAnsiStringWithDownSampling(t *testing.T) { + actual := NewColor24Bit(0xd0, 0xd0, 0xd0).ansiString(colorTypeForeground, ColorCount256) + actual = strings.ReplaceAll(actual, "\x1b", "ESC") + expected := "ESC[38;5;252m" assert.Equal(t, - NewColor24Bit(0xd0, 0xd0, 0xd0).ansiString(true, ColorCount256), - "\x1b[38;5;252m", + actual, + expected, ) } func TestAnsiStringDefault(t *testing.T) { + actual := ColorDefault.ansiString(colorTypeBackground, ColorCount16) + actual = strings.ReplaceAll(actual, "\x1b", "ESC") + expected := "ESC[49m" assert.Equal(t, - ColorDefault.ansiString(true, ColorCount16), - "\x1b[39m", + actual, + expected, ) } From 43f17ebe7bfdc3929e65427a9d68f4bd3ea20627 Mon Sep 17 00:00:00 2001 From: Johan Walles Date: Mon, 12 Aug 2024 20:31:11 +0200 Subject: [PATCH 4/9] Remove no-longer-needed code --- twin/colors.go | 10 ---------- twin/styles.go | 4 ++-- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/twin/colors.go b/twin/colors.go index 4ad3c0ba..b49ca0e7 100644 --- a/twin/colors.go +++ b/twin/colors.go @@ -156,16 +156,6 @@ func (color Color) ansiString(cType colorType, terminalColorCount ColorCount) st panic(fmt.Errorf("unhandled color type=%d %s", color.ColorCount(), color.String())) } -func (color Color) ForegroundAnsiString(terminalColorCount ColorCount) string { - // FIXME: Test this function with all different color types. - return color.ansiString(colorTypeForeground, terminalColorCount) -} - -func (color Color) BackgroundAnsiString(terminalColorCount ColorCount) string { - // FIXME: Test this function with all different color types. - return color.ansiString(colorTypeBackground, terminalColorCount) -} - func (color Color) String() string { switch color.ColorCount() { case ColorCountDefault: diff --git a/twin/styles.go b/twin/styles.go index 313defc7..2b549a38 100644 --- a/twin/styles.go +++ b/twin/styles.go @@ -152,11 +152,11 @@ func (current Style) RenderUpdateFrom(previous Style, terminalColorCount ColorCo var builder strings.Builder if current.fg != previous.fg { - builder.WriteString(current.fg.ForegroundAnsiString(terminalColorCount)) + builder.WriteString(current.fg.ansiString(colorTypeForeground, terminalColorCount)) } if current.bg != previous.bg { - builder.WriteString(current.bg.BackgroundAnsiString(terminalColorCount)) + builder.WriteString(current.bg.ansiString(colorTypeBackground, terminalColorCount)) } // Handle AttrDim / AttrBold changes From 2c9f16fa2af3008816a6fa825709dac1ae2b62ed Mon Sep 17 00:00:00 2001 From: Johan Walles Date: Mon, 12 Aug 2024 20:39:48 +0200 Subject: [PATCH 5/9] Add underline color support to Style --- twin/styles.go | 71 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 46 insertions(+), 25 deletions(-) diff --git a/twin/styles.go b/twin/styles.go index 2b549a38..8be69f2c 100644 --- a/twin/styles.go +++ b/twin/styles.go @@ -19,9 +19,10 @@ const ( ) type Style struct { - fg Color - bg Color - attrs AttrMask + fg Color + bg Color + underlineColor Color + attrs AttrMask // This hyperlinkURL is a URL for in-terminal hyperlinks. // @@ -37,8 +38,13 @@ type Style struct { var StyleDefault Style func (style Style) String() string { + undelineSuffix := "" + if style.underlineColor != ColorDefault { + undelineSuffix = fmt.Sprintf(" underlined with %v", style.underlineColor) + } + if style.attrs == AttrNone { - return fmt.Sprint(style.fg, " on ", style.bg) + return fmt.Sprint(style.fg, " on ", style.bg, undelineSuffix) } attrNames := make([]string, 0) @@ -67,15 +73,16 @@ func (style Style) String() string { attrNames = append(attrNames, "\""+*style.hyperlinkURL+"\"") } - return fmt.Sprint(strings.Join(attrNames, " "), " ", style.fg, " on ", style.bg) + return fmt.Sprint(strings.Join(attrNames, " "), " ", style.fg, " on ", style.bg, undelineSuffix) } func (style Style) WithAttr(attr AttrMask) Style { result := Style{ - fg: style.fg, - bg: style.bg, - attrs: style.attrs | attr, - hyperlinkURL: style.hyperlinkURL, + fg: style.fg, + bg: style.bg, + underlineColor: style.underlineColor, + attrs: style.attrs | attr, + hyperlinkURL: style.hyperlinkURL, } // Bold and dim are mutually exclusive @@ -97,19 +104,21 @@ func (style Style) WithHyperlink(hyperlinkURL *string) Style { } return Style{ - fg: style.fg, - bg: style.bg, - attrs: style.attrs, - hyperlinkURL: hyperlinkURL, + fg: style.fg, + bg: style.bg, + underlineColor: style.underlineColor, + attrs: style.attrs, + hyperlinkURL: hyperlinkURL, } } func (style Style) WithoutAttr(attr AttrMask) Style { return Style{ - fg: style.fg, - bg: style.bg, - attrs: style.attrs & ^attr, - hyperlinkURL: style.hyperlinkURL, + fg: style.fg, + bg: style.bg, + underlineColor: style.underlineColor, + attrs: style.attrs & ^attr, + hyperlinkURL: style.hyperlinkURL, } } @@ -119,19 +128,31 @@ func (attr AttrMask) has(attrs AttrMask) bool { func (style Style) WithBackground(color Color) Style { return Style{ - fg: style.fg, - bg: color, - attrs: style.attrs, - hyperlinkURL: style.hyperlinkURL, + fg: style.fg, + bg: color, + underlineColor: style.underlineColor, + attrs: style.attrs, + hyperlinkURL: style.hyperlinkURL, } } func (style Style) WithForeground(color Color) Style { return Style{ - fg: color, - bg: style.bg, - attrs: style.attrs, - hyperlinkURL: style.hyperlinkURL, + fg: color, + bg: style.bg, + underlineColor: style.underlineColor, + attrs: style.attrs, + hyperlinkURL: style.hyperlinkURL, + } +} + +func (style Style) WithUnderlineColor(color Color) Style { + return Style{ + fg: style.fg, + bg: style.bg, + underlineColor: color, + attrs: style.attrs, + hyperlinkURL: style.hyperlinkURL, } } From ae0ae422418410623ed2889adc55deea960fe730 Mon Sep 17 00:00:00 2001 From: Johan Walles Date: Mon, 12 Aug 2024 20:43:10 +0200 Subject: [PATCH 6/9] Support parsing underline color escape sequences --- m/textstyles/ansiTokenizer.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/m/textstyles/ansiTokenizer.go b/m/textstyles/ansiTokenizer.go index acc93a0f..00f2b750 100644 --- a/m/textstyles/ansiTokenizer.go +++ b/m/textstyles/ansiTokenizer.go @@ -511,6 +511,18 @@ func rawUpdateStyle(style twin.Style, escapeSequenceWithoutHeader string, number case 49: style = style.WithBackground(twin.ColorDefault) + case 58: + var err error + var color *twin.Color + index, color, err = consumeCompositeColor(numbersBuffer, index-1) + if err != nil { + return style, numbersBuffer, fmt.Errorf("Underline: %w", err) + } + style = style.WithUnderlineColor(*color) + + case 59: + style = style.WithUnderlineColor(twin.ColorDefault) + // Bright foreground colors: see https://pkg.go.dev/github.com/gdamore/Color // // After testing vs less and cat on iTerm2 3.3.9 / macOS Catalina From d9b3e16e003cda3fea892351a212d8f6fae646f0 Mon Sep 17 00:00:00 2001 From: Johan Walles Date: Mon, 12 Aug 2024 20:44:40 +0200 Subject: [PATCH 7/9] Render underline color changes --- twin/styles.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/twin/styles.go b/twin/styles.go index 8be69f2c..b375788d 100644 --- a/twin/styles.go +++ b/twin/styles.go @@ -180,6 +180,10 @@ func (current Style) RenderUpdateFrom(previous Style, terminalColorCount ColorCo builder.WriteString(current.bg.ansiString(colorTypeBackground, terminalColorCount)) } + if current.underlineColor != previous.underlineColor { + builder.WriteString(current.underlineColor.ansiString(colorTypeUnderline, terminalColorCount)) + } + // Handle AttrDim / AttrBold changes previousBoldDim := previous.attrs & (AttrBold | AttrDim) currentBoldDim := current.attrs & (AttrBold | AttrDim) From 065b95a653c92394344d9fcdc5f98dfa6b6eb81d Mon Sep 17 00:00:00 2001 From: Johan Walles Date: Mon, 12 Aug 2024 20:50:57 +0200 Subject: [PATCH 8/9] Add a colored underline test case --- sample-files/colored-underlines.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 sample-files/colored-underlines.txt diff --git a/sample-files/colored-underlines.txt b/sample-files/colored-underlines.txt new file mode 100644 index 00000000..c06e1eb9 --- /dev/null +++ b/sample-files/colored-underlines.txt @@ -0,0 +1 @@ +[58:5:196mRed underline Default colored underline From f69e58d9b3bcb232db6401a9b544d556e827e630 Mon Sep 17 00:00:00 2001 From: Johan Walles Date: Mon, 12 Aug 2024 20:53:16 +0200 Subject: [PATCH 9/9] Handle reading underline color --- m/textstyles/ansiTokenizer.go | 4 ++-- m/textstyles/ansiTokenizer_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/m/textstyles/ansiTokenizer.go b/m/textstyles/ansiTokenizer.go index 00f2b750..ca13f1e3 100644 --- a/m/textstyles/ansiTokenizer.go +++ b/m/textstyles/ansiTokenizer.go @@ -585,9 +585,9 @@ func joinUints(ints []uint) string { // * A color value that can be applied to a style func consumeCompositeColor(numbers []uint, index int) (int, *twin.Color, error) { baseIndex := index - if numbers[index] != 38 && numbers[index] != 48 { + if numbers[index] != 38 && numbers[index] != 48 && numbers[index] != 58 { err := fmt.Errorf( - "unknown start of color sequence <%d>, expected 38 (foreground) or 48 (background): ", + "unknown start of color sequence <%d>, expected 38 (foreground), 48 (background) or 58 (underline): ", numbers[index], joinUints(numbers[baseIndex:])) return -1, nil, err diff --git a/m/textstyles/ansiTokenizer_test.go b/m/textstyles/ansiTokenizer_test.go index b5e03cdc..99eb196a 100644 --- a/m/textstyles/ansiTokenizer_test.go +++ b/m/textstyles/ansiTokenizer_test.go @@ -216,7 +216,7 @@ func TestConsumeCompositeColorBadPrefix(t *testing.T) { // 8 bit color // Example from: https://github.com/walles/moar/issues/14 _, color, err := consumeCompositeColor([]uint{29}, 0) - assert.Equal(t, err.Error(), "unknown start of color sequence <29>, expected 38 (foreground) or 48 (background): ") + assert.Equal(t, err.Error(), "unknown start of color sequence <29>, expected 38 (foreground), 48 (background) or 58 (underline): ") assert.Assert(t, color == nil) }