1
1
mirror of https://github.com/walles/moar.git synced 2024-09-11 12:15:43 +03:00

Compare commits

...

11 Commits

Author SHA1 Message Date
Johan Walles
a67c887554 Include stack trace in panic logs 2024-08-12 21:47:46 +02:00
Johan Walles
27e9c0d68b Merge branch 'johan/color-underline'
Fixes #237.
2024-08-12 20:57:59 +02:00
Johan Walles
f69e58d9b3 Handle reading underline color 2024-08-12 20:57:44 +02:00
Johan Walles
065b95a653 Add a colored underline test case 2024-08-12 20:50:57 +02:00
Johan Walles
d9b3e16e00 Render underline color changes 2024-08-12 20:44:40 +02:00
Johan Walles
ae0ae42241 Support parsing underline color escape sequences 2024-08-12 20:43:10 +02:00
Johan Walles
2c9f16fa2a Add underline color support to Style 2024-08-12 20:39:48 +02:00
Johan Walles
43f17ebe7b Remove no-longer-needed code 2024-08-12 20:31:11 +02:00
Johan Walles
f26ae4ff9a Introduce colorType to ansiString 2024-08-12 20:25:53 +02:00
Johan Walles
44136f6524 More renaming, forgot some things 2024-08-12 18:53:44 +02:00
Johan Walles
42b0bc7e25 Improve naming 2024-08-12 18:36:49 +02:00
18 changed files with 211 additions and 143 deletions

View File

@ -3,6 +3,7 @@ package m
import (
"fmt"
"regexp"
"runtime/debug"
"strings"
"time"
@ -323,7 +324,7 @@ func (p *Pager) StartPaging(screen twin.Screen, chromaStyle *chroma.Style, chrom
go func() {
defer func() {
panicHandler("StartPaging()/moreLinesAvailable", recover())
panicHandler("StartPaging()/moreLinesAvailable", recover(), debug.Stack())
}()
for range p.reader.moreLinesAdded {
@ -342,7 +343,7 @@ func (p *Pager) StartPaging(screen twin.Screen, chromaStyle *chroma.Style, chrom
go func() {
defer func() {
panicHandler("StartPaging()/spinner", recover())
panicHandler("StartPaging()/spinner", recover(), debug.Stack())
}()
// Spin the spinner as long as contents is still loading
@ -368,7 +369,7 @@ func (p *Pager) StartPaging(screen twin.Screen, chromaStyle *chroma.Style, chrom
go func() {
defer func() {
panicHandler("StartPaging()/maybeDone", recover())
panicHandler("StartPaging()/maybeDone", recover(), debug.Stack())
}()
for range p.reader.maybeDone {

View File

@ -6,12 +6,13 @@ import (
log "github.com/sirupsen/logrus"
)
func panicHandler(goroutineName string, recoverResult any) {
func panicHandler(goroutineName string, recoverResult any, stackTrace []byte) {
if recoverResult == nil {
return
}
log.WithFields(log.Fields{
"recoverResult": recoverResult,
"panic": recoverResult,
"stackTrace": string(stackTrace),
}).Error("Goroutine panicked: " + goroutineName)
}

View File

@ -7,6 +7,7 @@ import (
"io"
"os"
"path"
"runtime/debug"
"strings"
"sync"
"sync/atomic"
@ -366,7 +367,7 @@ func newReaderFromStream(reader io.Reader, originalFileName *string, formatter c
go func() {
defer func() {
panicHandler("newReaderFromStream()/readStream()", recover())
panicHandler("newReaderFromStream()/readStream()", recover(), debug.Stack())
}()
returnMe.readStream(reader, formatter, lexer)

View File

@ -3,6 +3,7 @@ package m
import (
"fmt"
"runtime"
"runtime/debug"
"time"
log "github.com/sirupsen/logrus"
@ -125,7 +126,7 @@ func (p *Pager) findFirstHit(startPosition linenumbers.LineNumber, beforePositio
go func(i int, searchStart linenumbers.LineNumber, chunkBefore *linenumbers.LineNumber) {
defer func() {
panicHandler("findFirstHit()/chunkSearch", recover())
panicHandler("findFirstHit()/chunkSearch", recover(), debug.Stack())
}()
findings[i] <- p._findFirstHit(searchStart, chunkBefore, backwards)

View File

@ -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

View File

@ -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
View File

@ -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,
)

View File

@ -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

View File

@ -0,0 +1 @@
[58:5:196mRed underline Default colored underline

View File

@ -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()))
}

View File

@ -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,
)
}

View File

@ -6,12 +6,13 @@ import (
log "github.com/sirupsen/logrus"
)
func panicHandler(goroutineName string, recoverResult any) {
func panicHandler(goroutineName string, recoverResult any, stackTrace []byte) {
if recoverResult == nil {
return
}
log.WithFields(log.Fields{
"recoverResult": recoverResult,
"panic": recoverResult,
"stackTrace": string(stackTrace),
}).Error("Goroutine panicked: " + goroutineName)
}

View File

@ -7,6 +7,7 @@ import (
"io"
"os"
"os/signal"
"runtime/debug"
"sync/atomic"
"syscall"
@ -118,7 +119,7 @@ func (screen *UnixScreen) setupSigwinchNotification() {
signal.Notify(sigwinch, syscall.SIGWINCH)
go func() {
defer func() {
panicHandler("setupSigwinchNotification()/SIGWINCH", recover())
panicHandler("setupSigwinchNotification()/SIGWINCH", recover(), debug.Stack())
}()
for {

View File

@ -6,6 +6,7 @@ package twin
import (
"io"
"os"
"runtime/debug"
"testing"
"time"
@ -32,7 +33,7 @@ func TestInterruptableReader_blockedOnReadImmediate(t *testing.T) {
readResultChan := make(chan readResult)
go func() {
defer func() {
panicHandler("TestInterruptableReader_blockedOnReadImmediate()", recover())
panicHandler("TestInterruptableReader_blockedOnReadImmediate()", recover(), debug.Stack())
}()
buffer := make([]byte, 1)

View File

@ -5,6 +5,7 @@ import (
"fmt"
"os"
"regexp"
"runtime/debug"
"strconv"
"strings"
"unicode/utf8"
@ -96,7 +97,7 @@ type UnixScreen struct {
ttyOut *os.File
oldTtyOutMode uint32 //nolint Windows only
terminalColorCount ColorType
terminalColorCount ColorCount
}
// Example event: "\x1b[<65;127;41M"
@ -121,15 +122,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 +182,7 @@ func NewScreenWithMouseModeAndColorType(mouseMode MouseMode, terminalColorCount
go func() {
defer func() {
panicHandler("NewScreenWithMouseModeAndColorType()/mainLoop()", recover())
panicHandler("NewScreenWithMouseModeAndColorCount()/mainLoop()", recover(), debug.Stack())
}()
screen.mainLoop()
@ -413,7 +414,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 +658,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-- {

View File

@ -3,6 +3,7 @@ package twin
import (
"io"
"os"
"runtime/debug"
"strings"
"testing"
"time"
@ -64,7 +65,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 +80,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 +96,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 +114,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 +135,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 +157,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 +173,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 +190,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 +211,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 +236,7 @@ func TestMultiCharHyperlink(t *testing.T) {
},
}
rendered, count := renderLine(row, ColorType16)
rendered, count := renderLine(row, ColorCount16)
assert.Equal(t, count, 3)
assert.Equal(t,
@ -270,7 +271,7 @@ func TestInterruptableReader_blockedOnRead(t *testing.T) {
readResultChan := make(chan readResult)
go func() {
defer func() {
panicHandler("TestInterruptableReader_blockedOnRead()", recover())
panicHandler("TestInterruptableReader_blockedOnRead()", recover(), debug.Stack())
}()
buffer := make([]byte, 1)

View File

@ -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

View File

@ -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\\")
}