mirror of
https://github.com/walles/moar.git
synced 2024-09-11 12:15:43 +03:00
commit
27e9c0d68b
@ -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
|
||||
@ -573,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): <CSI %sm>",
|
||||
"unknown start of color sequence <%d>, expected 38 (foreground), 48 (background) or 58 (underline): <CSI %sm>",
|
||||
numbers[index],
|
||||
joinUints(numbers[baseIndex:]))
|
||||
return -1, nil, err
|
||||
|
@ -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): <CSI 29m>")
|
||||
assert.Equal(t, err.Error(), "unknown start of color sequence <29>, expected 38 (foreground), 48 (background) or 58 (underline): <CSI 29m>")
|
||||
assert.Assert(t, color == nil)
|
||||
}
|
||||
|
||||
|
30
moar.go
30
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
|
||||
}
|
||||
|
||||
@ -803,7 +803,7 @@ func main() {
|
||||
|
||||
pager, screen, style, formatter, err := pagerFromArgs(
|
||||
os.Args,
|
||||
twin.NewScreenWithMouseModeAndColorType,
|
||||
twin.NewScreenWithMouseModeAndColorCount,
|
||||
stdinIsRedirected,
|
||||
stdoutIsRedirected,
|
||||
)
|
||||
|
@ -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
|
||||
|
1
sample-files/colored-underlines.txt
Normal file
1
sample-files/colored-underlines.txt
Normal file
@ -0,0 +1 @@
|
||||
[4m[58:5:196mRed underline[59m Default colored underline
|
130
twin/colors.go
130
twin/colors.go
@ -10,30 +10,39 @@ 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
|
||||
)
|
||||
|
||||
type colorType uint8
|
||||
|
||||
const (
|
||||
colorTypeForeground colorType = iota
|
||||
colorTypeBackground
|
||||
colorTypeUnderline
|
||||
)
|
||||
|
||||
// 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 +64,30 @@ var colorNames16 = map[int]string{
|
||||
15: "15 bright white",
|
||||
}
|
||||
|
||||
func newColor(colorType ColorType, 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:
|
||||
// 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) ColorCount() ColorCount {
|
||||
return ColorCount(color >> 24)
|
||||
}
|
||||
|
||||
func (color Color) colorValue() uint32 {
|
||||
@ -88,100 +97,105 @@ 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 {
|
||||
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.ColorType() == ColorTypeDefault {
|
||||
return fmt.Sprint("\x1b[", fgBgMarker, "9m")
|
||||
if color.ColorCount() == ColorCountDefault {
|
||||
return fmt.Sprint("\x1b[", typeMarker, "9m")
|
||||
}
|
||||
|
||||
color = color.downsampleTo(terminalColorCount)
|
||||
|
||||
if color.ColorType() == ColorType16 {
|
||||
// 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.ColorType() == ColorType256 {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
if color.ColorType() == ColorType24bit {
|
||||
if color.ColorCount() == ColorCount24bit {
|
||||
value := color.colorValue()
|
||||
red := (value & 0xff0000) >> 16
|
||||
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.ColorType(), color.String()))
|
||||
}
|
||||
|
||||
func (color Color) ForegroundAnsiString(terminalColorCount ColorType) string {
|
||||
// FIXME: Test this function with all different color types.
|
||||
return color.ansiString(true, terminalColorCount)
|
||||
}
|
||||
|
||||
func (color Color) BackgroundAnsiString(terminalColorCount ColorType) string {
|
||||
// FIXME: Test this function with all different color types.
|
||||
return color.ansiString(false, terminalColorCount)
|
||||
panic(fmt.Errorf("unhandled color type=%d %s", color.ColorCount(), color.String()))
|
||||
}
|
||||
|
||||
func (color Color) String() string {
|
||||
switch color.ColorType() {
|
||||
case ColorTypeDefault:
|
||||
switch color.ColorCount() {
|
||||
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())
|
||||
}
|
||||
|
||||
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() == ColorType24bit {
|
||||
if color.ColorCount() == ColorCount24bit {
|
||||
return color
|
||||
}
|
||||
|
||||
if color.ColorType() == ColorType8 || color.ColorType() == ColorType16 || color.ColorType() == ColorType256 {
|
||||
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 ColorType) Color {
|
||||
if color.ColorType() == ColorTypeDefault || terminalColorCount == ColorTypeDefault {
|
||||
func (color Color) downsampleTo(terminalColorCount ColorCount) Color {
|
||||
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
|
||||
}
|
||||
@ -192,13 +206,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 +248,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.ColorCount() != ColorCount24bit {
|
||||
panic(fmt.Errorf("contrast only supported for 24 bit colors, got %s vs %s", color.String(), other.String()))
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package twin
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
@ -8,14 +9,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,22 +25,28 @@ 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) {
|
||||
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, ColorType256),
|
||||
"\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, ColorType16),
|
||||
"\x1b[39m",
|
||||
actual,
|
||||
expected,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
return NewScreenWithMouseModeAndColorCount(mouseMode, terminalColorCount)
|
||||
}
|
||||
|
||||
func NewScreenWithMouseModeAndColorType(mouseMode MouseMode, terminalColorCount ColorType) (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 {
|
||||
@ -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-- {
|
||||
|
@ -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 := "[m"
|
||||
reversed := "[7m"
|
||||
@ -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 := "[m"
|
||||
reversed := "[7m"
|
||||
@ -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 := "[m"
|
||||
clearToEol := "[K"
|
||||
@ -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 := "[m"
|
||||
reversed := "[7m"
|
||||
@ -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 := "[m"
|
||||
reversed := "[7m"
|
||||
@ -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 := "[m"
|
||||
white := "[37m"
|
||||
@ -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,
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -139,7 +160,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 ""
|
||||
@ -152,11 +173,15 @@ func (current Style) RenderUpdateFrom(previous Style, terminalColorCount ColorTy
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
if current.underlineColor != previous.underlineColor {
|
||||
builder.WriteString(current.underlineColor.ansiString(colorTypeUnderline, terminalColorCount))
|
||||
}
|
||||
|
||||
// Handle AttrDim / AttrBold changes
|
||||
|
@ -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\\")
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user