mirror of
https://github.com/walles/moar.git
synced 2024-09-11 12:15:43 +03:00
Compare commits
15 Commits
978b51ea3a
...
4fa9cca7d6
Author | SHA1 | Date | |
---|---|---|---|
|
4fa9cca7d6 | ||
|
e68547a413 | ||
|
c379459982 | ||
|
a67c887554 | ||
|
27e9c0d68b | ||
|
f69e58d9b3 | ||
|
065b95a653 | ||
|
d9b3e16e00 | ||
|
ae0ae42241 | ||
|
2c9f16fa2a | ||
|
43f17ebe7b | ||
|
f26ae4ff9a | ||
|
44136f6524 | ||
|
42b0bc7e25 | ||
|
d22af869d0 |
3
go.mod
3
go.mod
@ -4,12 +4,13 @@ go 1.20
|
||||
|
||||
require (
|
||||
github.com/alecthomas/chroma/v2 v2.12.0
|
||||
github.com/fsnotify/fsnotify v1.7.0
|
||||
github.com/google/go-cmp v0.5.9
|
||||
github.com/klauspost/compress v1.17.4
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/ulikunitz/xz v0.5.11
|
||||
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc
|
||||
golang.org/x/sys v0.1.0
|
||||
golang.org/x/sys v0.4.0
|
||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56
|
||||
gotest.tools/v3 v3.3.0
|
||||
)
|
||||
|
6
go.sum
6
go.sum
@ -7,6 +7,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
||||
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
@ -41,8 +43,8 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
|
||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w=
|
||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
@ -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 {
|
||||
|
@ -219,6 +219,24 @@ func TestCodeHighlight_compressed(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Regression test for:
|
||||
// https://github.com/walles/moar/issues/236#issuecomment-2282677792
|
||||
//
|
||||
// Sample file sysctl.h from:
|
||||
// https://github.com/fastfetch-cli/fastfetch/blob/f9597eba39d6afd278eeca2f2972f73a7e54f111/src/common/sysctl.h
|
||||
func TestCodeHighlightingIncludes(t *testing.T) {
|
||||
reader, err := NewReaderFromFilename("../sample-files/sysctl.h", *styles.Get("native"), formatters.TTY16m, nil)
|
||||
assert.NilError(t, err)
|
||||
assert.NilError(t, reader._wait())
|
||||
|
||||
screen := startPaging(t, reader)
|
||||
firstIncludeLine := screen.GetRow(2)
|
||||
secondIncludeLine := screen.GetRow(3)
|
||||
|
||||
// Both should start with "#include" colored the same way
|
||||
assertCellsEqual(t, firstIncludeLine[0], secondIncludeLine[0])
|
||||
}
|
||||
|
||||
func TestUnicodePrivateUse(t *testing.T) {
|
||||
// This character lives in a Private Use Area:
|
||||
// https://codepoints.net/U+f244
|
||||
|
@ -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)
|
||||
}
|
||||
|
40
m/reader.go
40
m/reader.go
@ -7,11 +7,13 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/walles/moar/m/linenumbers"
|
||||
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
@ -239,11 +241,39 @@ func (reader *Reader) tailFile() error {
|
||||
|
||||
log.Debugf("Tailing file %s", *fileName)
|
||||
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
log.Debugf("Failed to create watcher for %s, giving up: %s", *fileName, err.Error())
|
||||
return nil
|
||||
}
|
||||
err = watcher.Add(*fileName)
|
||||
if err != nil {
|
||||
log.Debugf("Failed to add %s to watcher for tailing, giving up: %s", *fileName, err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
// Make sure to not miss any events while we were setting up the watcher
|
||||
firstLap := true
|
||||
|
||||
for {
|
||||
// NOTE: We could use something like
|
||||
// https://github.com/fsnotify/fsnotify instead of sleeping and polling
|
||||
// here.
|
||||
time.Sleep(1 * time.Second)
|
||||
if !firstLap {
|
||||
select {
|
||||
case event, ok := <-watcher.Events:
|
||||
if !ok {
|
||||
log.Debug("Watcher closed, giving up on tailing")
|
||||
return nil
|
||||
}
|
||||
log.Trace("File event received: ", event.String())
|
||||
case err, ok := <-watcher.Errors:
|
||||
if !ok {
|
||||
log.Debug("Watcher closed, giving up on tailing")
|
||||
return nil
|
||||
}
|
||||
log.Debugf("Watcher error, giving up on tailing: %s", err.Error())
|
||||
return nil
|
||||
}
|
||||
}
|
||||
firstLap = false
|
||||
|
||||
fileStats, err := os.Stat(*fileName)
|
||||
if err != nil {
|
||||
@ -366,7 +396,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)
|
||||
|
@ -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)
|
||||
|
@ -119,10 +119,10 @@ func styleUI(chromaStyle *chroma.Style, chromaFormatter *chroma.Formatter, statu
|
||||
|
||||
chromaLineNumbers := twinStyleFromChroma(chromaStyle, chromaFormatter, chroma.LineNumbers, true)
|
||||
if chromaLineNumbers != nil {
|
||||
// If somebody can provide an example where not-dimmed line numbers
|
||||
// looks good I'll change this, but until then they will be dimmed no
|
||||
// matter what the theme authors think.
|
||||
lineNumbersStyle = chromaLineNumbers.WithAttr(twin.AttrDim)
|
||||
// NOTE: We used to dim line numbers here, but Johan found them too hard
|
||||
// to read. If line numbers should look some other way for some Chroma
|
||||
// style, go fix that in Chroma!
|
||||
lineNumbersStyle = *chromaLineNumbers
|
||||
}
|
||||
|
||||
if standoutStyle != nil {
|
||||
|
@ -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
|
12
sample-files/sysctl.h
Normal file
12
sample-files/sysctl.h
Normal file
@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "fastfetch.h"
|
||||
#include "util/FFcheckmacros.h"
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <sys/sysctl.h>
|
||||
|
||||
const char* ffSysctlGetString(const char* propName, FFstrbuf* result);
|
||||
FF_C_NODISCARD int ffSysctlGetInt(const char* propName, int defaultValue);
|
||||
FF_C_NODISCARD int64_t ffSysctlGetInt64(const char* propName, int64_t defaultValue);
|
||||
FF_C_NODISCARD void* ffSysctlGetData(int* request, u_int requestLength, size_t* resultLength);
|
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,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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-- {
|
||||
|
@ -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 := "[m"
|
||||
reversed := "[7m"
|
||||
@ -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 := "[m"
|
||||
reversed := "[7m"
|
||||
@ -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 := "[m"
|
||||
clearToEol := "[K"
|
||||
@ -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 := "[m"
|
||||
reversed := "[7m"
|
||||
@ -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 := "[m"
|
||||
reversed := "[7m"
|
||||
@ -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 := "[m"
|
||||
white := "[37m"
|
||||
@ -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)
|
||||
|
@ -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