From 4c6de79e209b4a71d5156b85d020b470523a0e68 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Tue, 29 Oct 2024 15:26:23 -0400 Subject: [PATCH] feat(cellbuf): add color profile support --- cellbuf/go.mod | 5 +++++ cellbuf/go.sum | 11 +++++++++++ cellbuf/link.go | 11 +++++++++++ cellbuf/screen.go | 36 ++++++++++++++++++++++++++---------- cellbuf/style.go | 27 +++++++++++++++++++++++++++ 5 files changed, 80 insertions(+), 10 deletions(-) diff --git a/cellbuf/go.mod b/cellbuf/go.mod index f327154b..8d91edff 100644 --- a/cellbuf/go.mod +++ b/cellbuf/go.mod @@ -3,11 +3,16 @@ module github.com/charmbracelet/x/cellbuf go 1.18 require ( + github.com/charmbracelet/colorprofile v0.1.4 github.com/charmbracelet/x/ansi v0.4.0 github.com/charmbracelet/x/wcwidth v0.0.0-20241011142426-46044092ad91 ) require ( + github.com/charmbracelet/x/term v0.2.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/sys v0.24.0 // indirect golang.org/x/text v0.19.0 // indirect ) diff --git a/cellbuf/go.sum b/cellbuf/go.sum index 4a3cddc5..ce6e71d9 100644 --- a/cellbuf/go.sum +++ b/cellbuf/go.sum @@ -1,8 +1,19 @@ +github.com/charmbracelet/colorprofile v0.1.4 h1:UZm1273VW+rD7gfH6NqfTWi9TGyPsFvGDNcIFlZw/As= +github.com/charmbracelet/colorprofile v0.1.4/go.mod h1:zZrJB+Qq2tyGEprGJwY1u05me+TmRqfECZOxWTxyoVU= github.com/charmbracelet/x/ansi v0.4.0 h1:NqwHA4B23VwsDn4H3VcNX1W1tOmgnvY1NDx5tOXdnOU= github.com/charmbracelet/x/ansi v0.4.0/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= +github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= github.com/charmbracelet/x/wcwidth v0.0.0-20241011142426-46044092ad91 h1:D5OO0lVavz7A+Swdhp62F9gbkibxmz9B2hZ/jVdMPf0= github.com/charmbracelet/x/wcwidth v0.0.0-20241011142426-46044092ad91/go.mod h1:Ey8PFmYwH+/td9bpiEx07Fdx9ZVkxfIjWXxBluxF4Nw= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= diff --git a/cellbuf/link.go b/cellbuf/link.go index 6dd2b91d..001da73c 100644 --- a/cellbuf/link.go +++ b/cellbuf/link.go @@ -1,5 +1,7 @@ package cellbuf +import "github.com/charmbracelet/colorprofile" + // Link represents a hyperlink in the terminal screen. type Link struct { URL string @@ -26,3 +28,12 @@ func (h Link) Equal(o Link) bool { func (h Link) Empty() bool { return h.URL == "" && h.URLID == "" } + +// Convert converts a hyperlink to respect the given color profile. +func (h Link) Convert(p colorprofile.Profile) Link { + if p == colorprofile.NoTTY { + return Link{} + } + + return h +} diff --git a/cellbuf/screen.go b/cellbuf/screen.go index 05359a46..9b2ea1a2 100644 --- a/cellbuf/screen.go +++ b/cellbuf/screen.go @@ -4,6 +4,7 @@ import ( "bytes" "strings" + "github.com/charmbracelet/colorprofile" "github.com/charmbracelet/x/ansi" ) @@ -37,12 +38,17 @@ func SetContent(d Screen, m Method, content string) []int { } // Render returns a string representation of the grid with ANSI escape sequences. -// Use [ansi.Strip] to remove them. func Render(d Screen) string { + return RenderWithProfile(d, colorprofile.TrueColor) +} + +// RenderWithProfile returns a string representation of the grid with ANSI escape +// sequences converting styles and colors to the given color profile. +func RenderWithProfile(d Screen, p colorprofile.Profile) string { var buf bytes.Buffer height := d.Height() for y := 0; y < height; y++ { - _, line := RenderLine(d, y) + _, line := RenderLineWithProfile(d, y, p) buf.WriteString(line) if y < height-1 { buf.WriteString("\r\n") @@ -54,6 +60,13 @@ func Render(d Screen) string { // RenderLine returns a string representation of the yth line of the grid along // with the width of the line. func RenderLine(d Screen, n int) (w int, line string) { + return RenderLineWithProfile(d, n, colorprofile.TrueColor) +} + +// RenderLineWithProfile returns a string representation of the nth line of the +// grid along with the width of the line converting styles and colors to the +// given color profile. +func RenderLineWithProfile(d Screen, n int, p colorprofile.Profile) (w int, line string) { var pen Style var link Link var buf bytes.Buffer @@ -73,28 +86,31 @@ func RenderLine(d Screen, n int) (w int, line string) { for x := 0; x < d.Width(); x++ { if cell, ok := d.Cell(x, n); ok && cell.Width > 0 { - if cell.Style.Empty() && !pen.Empty() { + // Convert the cell's style and link to the given color profile. + cellStyle := cell.Style.Convert(p) + cellLink := cell.Link.Convert(p) + if cellStyle.Empty() && !pen.Empty() { writePending() buf.WriteString(ansi.ResetStyle) //nolint:errcheck pen.Reset() } - if !cell.Style.Equal(pen) { + if !cellStyle.Equal(pen) { writePending() - seq := cell.Style.DiffSequence(pen) + seq := cellStyle.DiffSequence(pen) buf.WriteString(seq) // nolint:errcheck - pen = cell.Style + pen = cellStyle } // Write the URL escape sequence - if cell.Link != link && link.URL != "" { + if cellLink != link && link.URL != "" { writePending() buf.WriteString(ansi.ResetHyperlink()) //nolint:errcheck link.Reset() } - if cell.Link != link { + if cellLink != link { writePending() - buf.WriteString(ansi.SetHyperlink(cell.Link.URL, cell.Link.URLID)) //nolint:errcheck - link = cell.Link + buf.WriteString(ansi.SetHyperlink(cellLink.URL, cellLink.URLID)) //nolint:errcheck + link = cellLink } // We only write the cell content if it's not empty. If it is, we diff --git a/cellbuf/style.go b/cellbuf/style.go index bd97c8e0..8366e104 100644 --- a/cellbuf/style.go +++ b/cellbuf/style.go @@ -1,6 +1,7 @@ package cellbuf import ( + "github.com/charmbracelet/colorprofile" "github.com/charmbracelet/x/ansi" ) @@ -370,3 +371,29 @@ func (s *Style) Reset() *Style { func (s *Style) Empty() bool { return s.Fg == nil && s.Bg == nil && s.Ul == nil && s.Attrs == ResetAttr && s.UlStyle == NoUnderline } + +// Convert converts a style to respect the given color profile. +func (s Style) Convert(p colorprofile.Profile) Style { + switch p { + case colorprofile.TrueColor: + return s + case colorprofile.Ascii: + s.Fg = nil + s.Bg = nil + s.Ul = nil + case colorprofile.NoTTY: + return Style{} + } + + if s.Fg != nil { + s.Fg = p.Convert(s.Fg) + } + if s.Bg != nil { + s.Bg = p.Convert(s.Bg) + } + if s.Ul != nil { + s.Ul = p.Convert(s.Ul) + } + + return s +}