From bcb5bf9cfe6cbba01906a960103971204b58ef50 Mon Sep 17 00:00:00 2001 From: Caleb Foust Date: Wed, 7 Aug 2024 05:54:14 +0800 Subject: [PATCH 1/9] feat: working (style/render) --- cmd/stories/animation.go | 4 +- pkg/cy/api/layout_test.janet | 6 -- pkg/cy/api/style.go | 12 +++ pkg/cy/api/style_test.janet | 6 ++ pkg/cy/janet.go | 1 + pkg/layout/borders/module.go | 3 + pkg/style/borders.go | 2 - pkg/style/color.go | 31 +++++++ pkg/style/style.go | 164 +++++++++++++++++++++++++++++++++++ 9 files changed, 219 insertions(+), 10 deletions(-) create mode 100644 pkg/cy/api/style.go create mode 100644 pkg/cy/api/style_test.janet create mode 100644 pkg/style/color.go create mode 100644 pkg/style/style.go diff --git a/cmd/stories/animation.go b/cmd/stories/animation.go index 62c692ef..f0a54a49 100644 --- a/cmd/stories/animation.go +++ b/cmd/stories/animation.go @@ -8,7 +8,7 @@ import ( "github.com/cfoust/cy/pkg/geom" "github.com/cfoust/cy/pkg/geom/image" "github.com/cfoust/cy/pkg/geom/tty" - "github.com/cfoust/cy/pkg/layout" + "github.com/cfoust/cy/pkg/layout/margins" "github.com/cfoust/cy/pkg/mux/screen/placeholder" "github.com/cfoust/cy/pkg/taro" "github.com/cfoust/cy/pkg/util" @@ -19,7 +19,7 @@ import ( func createInitial(size geom.Size) image.Image { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - outerLayers := layout.AddMargins(ctx, placeholder.New(ctx)) + outerLayers := margins.Add(ctx, placeholder.New(ctx)) outerLayers.Resize(size) return outerLayers.State().Image } diff --git a/pkg/cy/api/layout_test.janet b/pkg/cy/api/layout_test.janet index 5cbaa0f4..b1c91789 100644 --- a/pkg/cy/api/layout_test.janet +++ b/pkg/cy/api/layout_test.janet @@ -17,12 +17,6 @@ (layout/set layout) (assert (deep= (layout/get) layout))) -(pp (layout/new (split - (pane) - (vsplit - (margins (attach)) - (borders (pane)))))) - (test "borders" (do (def layout (layout/new (split (attach) (pane) diff --git a/pkg/cy/api/style.go b/pkg/cy/api/style.go new file mode 100644 index 00000000..4fc6606f --- /dev/null +++ b/pkg/cy/api/style.go @@ -0,0 +1,12 @@ +package api + +import ( + "github.com/cfoust/cy/pkg/style" +) + +type StyleModule struct { +} + +func (s *StyleModule) Render(style *style.Style, text string) string { + return style.Render(text) +} diff --git a/pkg/cy/api/style_test.janet b/pkg/cy/api/style_test.janet new file mode 100644 index 00000000..83e23a46 --- /dev/null +++ b/pkg/cy/api/style_test.janet @@ -0,0 +1,6 @@ +(test "render" + (style/render {:fg "#0000ff" + :bg "#ff0000" + :width 15 + :italic true + :align-horizontal :right} "test")) diff --git a/pkg/cy/janet.go b/pkg/cy/janet.go index 0f52727f..2caffb81 100644 --- a/pkg/cy/janet.go +++ b/pkg/cy/janet.go @@ -46,6 +46,7 @@ func (c *Cy) initJanet(ctx context.Context) (*janet.VM, error) { TimeBinds: c.timeBinds, CopyBinds: c.copyBinds, }, + "style": &api.StyleModule{}, "tree": &api.TreeModule{Tree: c.tree}, "viewport": &api.ViewportModule{}, } diff --git a/pkg/layout/borders/module.go b/pkg/layout/borders/module.go index 6cc1e445..72d40d9c 100644 --- a/pkg/layout/borders/module.go +++ b/pkg/layout/borders/module.go @@ -35,6 +35,9 @@ func (l *Borders) Apply(node L.NodeType) (bool, error) { return false, nil } + l.Lock() + defer l.Unlock() + l.borderStyle = config.Border if config.Title != nil { diff --git a/pkg/style/borders.go b/pkg/style/borders.go index 6b099b3e..34308537 100644 --- a/pkg/style/borders.go +++ b/pkg/style/borders.go @@ -7,7 +7,6 @@ import ( "github.com/cfoust/cy/pkg/janet" "github.com/charmbracelet/lipgloss" - "github.com/rs/zerolog/log" ) var ( @@ -136,7 +135,6 @@ func (b *Border) UnmarshalJanet(value *janet.Value) (err error) { var keyword janet.Keyword err = value.Unmarshal(&keyword) if err != nil { - log.Info().Msgf("Unmarshal border %s", err) return err } diff --git a/pkg/style/color.go b/pkg/style/color.go new file mode 100644 index 00000000..0a406e9c --- /dev/null +++ b/pkg/style/color.go @@ -0,0 +1,31 @@ +package style + +import ( + "github.com/cfoust/cy/pkg/janet" + + "github.com/charmbracelet/lipgloss" +) + +type Color struct { + lipgloss.Color +} + +var _ janet.Unmarshalable = (*Color)(nil) + +func (c *Color) UnmarshalJanet(value *janet.Value) (err error) { + var str string + err = value.Unmarshal(&str) + if err != nil { + return err + } + + // TODO(cfoust): 08/06/24 validate colors? + c.Color = lipgloss.Color(str) + return nil +} + +var _ janet.Marshalable = (*Color)(nil) + +func (c *Color) MarshalJanet() interface{} { + return string(c.Color) +} diff --git a/pkg/style/style.go b/pkg/style/style.go new file mode 100644 index 00000000..fa0365a0 --- /dev/null +++ b/pkg/style/style.go @@ -0,0 +1,164 @@ +package style + +import ( + "fmt" + + "github.com/cfoust/cy/pkg/emu" + "github.com/cfoust/cy/pkg/janet" + + "github.com/charmbracelet/lipgloss" + "github.com/muesli/termenv" +) + +var ( + KEYWORD_RIGHT = janet.Keyword("right") + KEYWORD_LEFT = janet.Keyword("left") + KEYWORD_CENTER = janet.Keyword("center") + KEYWORD_TOP = janet.Keyword("top") + KEYWORD_BOTTOM = janet.Keyword("bottom") +) + +// We use a common renderer, since this actually has no impact on how/where we +// can render this style (it's all virtual anyway.) +var renderer *lipgloss.Renderer = func() *lipgloss.Renderer { + renderer := lipgloss.NewRenderer(emu.New()) + renderer.SetColorProfile(termenv.TrueColor) + return renderer +}() + +type Style struct { + lipgloss.Style +} + +var _ janet.Unmarshalable = (*Style)(nil) + +type janetStyle struct { + Fg *janet.Value + Bg *janet.Value + Width *int + Height *int + AlignHorizontal *janet.Value + AlignVertical *janet.Value + Bold *bool + Italic *bool + Underline *bool + Strikethrough *bool + Reverse *bool + Blink *bool + Faint *bool +} + +func unmarshalPosition(value *janet.Value) ( + position lipgloss.Position, + err error, +) { + var keyword janet.Keyword + err = value.Unmarshal(&keyword) + if err != nil { + return + } + + switch keyword { + case KEYWORD_RIGHT: + return lipgloss.Right, nil + case KEYWORD_LEFT: + return lipgloss.Left, nil + case KEYWORD_CENTER: + return lipgloss.Center, nil + case KEYWORD_TOP: + return lipgloss.Top, nil + case KEYWORD_BOTTOM: + return lipgloss.Bottom, nil + } + + err = fmt.Errorf("unknown position: %s", keyword) + return +} + +func (s *Style) UnmarshalJanet(value *janet.Value) (err error) { + style := renderer.NewStyle() + + var v janetStyle + if err := value.Unmarshal(&v); err != nil { + return err + } + + if !v.Fg.Nil() { + var color Color + err = v.Fg.Unmarshal(&color) + if err != nil { + return err + } + style = style.Foreground(color.Color) + } + + if !v.Bg.Nil() { + var color Color + err = v.Bg.Unmarshal(&color) + if err != nil { + return err + } + style = style.Background(color.Color) + } + + if v.Width != nil { + style = style.Width(*v.Width) + } + + if v.Height != nil { + style = style.Height(*v.Height) + } + + if v.Height != nil { + style = style.Height(*v.Height) + } + + if !v.AlignHorizontal.Nil() { + var position lipgloss.Position + position, err = unmarshalPosition(v.AlignHorizontal) + if err != nil { + return err + } + style = style.AlignHorizontal(position) + } + + if !v.AlignVertical.Nil() { + var position lipgloss.Position + position, err = unmarshalPosition(v.AlignVertical) + if err != nil { + return err + } + style = style.AlignVertical(position) + } + + if v.Bold != nil { + style = style.Bold(*v.Bold) + } + + if v.Italic != nil { + style = style.Italic(*v.Italic) + } + + if v.Underline != nil { + style = style.Underline(*v.Underline) + } + + if v.Strikethrough != nil { + style = style.Strikethrough(*v.Strikethrough) + } + + if v.Reverse != nil { + style = style.Reverse(*v.Reverse) + } + + if v.Blink != nil { + style = style.Blink(*v.Blink) + } + + if v.Faint != nil { + style = style.Faint(*v.Faint) + } + + s.Style = style + return nil +} From 490f7a905f52d611286a665404eb82ebeae20a87 Mon Sep 17 00:00:00 2001 From: Caleb Foust Date: Wed, 7 Aug 2024 06:31:00 +0800 Subject: [PATCH 2/9] fix: italics rendering bug --- pkg/geom/tty/render.go | 4 ++- pkg/geom/tty/render_test.go | 61 +++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 pkg/geom/tty/render_test.go diff --git a/pkg/geom/tty/render.go b/pkg/geom/tty/render.go index 636be3fb..12dea72a 100644 --- a/pkg/geom/tty/render.go +++ b/pkg/geom/tty/render.go @@ -87,7 +87,9 @@ func swapImage( } if mode&emu.AttrItalic != 0 { - info.Fprintf(data, terminfo.EnterItalicsMode) + // TODO(cfoust): 08/07/24 why does this not work? + //info.Fprintf(data, terminfo.EnterItalicsMode) + data.Write([]byte("\033[3m")) } if mode&emu.AttrBlink != 0 { diff --git a/pkg/geom/tty/render_test.go b/pkg/geom/tty/render_test.go new file mode 100644 index 00000000..254c0c4a --- /dev/null +++ b/pkg/geom/tty/render_test.go @@ -0,0 +1,61 @@ +package tty + +import ( + "testing" + + "github.com/cfoust/cy/pkg/emu" + + "github.com/charmbracelet/lipgloss" + "github.com/muesli/termenv" + "github.com/stretchr/testify/require" + "github.com/xo/terminfo" +) + +func testBytes( + t *testing.T, + name string, + bytes []byte, +) { + info, _ := terminfo.Load("xterm-256color") + termA := emu.New() + termA.Write(bytes) + + termB := emu.New() + newBytes := swapImage( + info, + termB.Screen(), + termA.Screen(), + ) + termB.Write(newBytes) + + require.Equal( + t, + termA.Screen(), + termB.Screen(), + "style %s was not equal: %#v", + name, + string(newBytes), + ) + t.Logf("%s %+v", name, termB.Screen()[0][0]) +} + +func TestAttributes(t *testing.T) { + r := lipgloss.NewRenderer(emu.New()) + r.SetColorProfile(termenv.TrueColor) + + for name, style := range map[string]lipgloss.Style{ + "blink": r.NewStyle().Blink(true), + "bold": r.NewStyle().Bold(true), + "fg": r.NewStyle().Foreground(lipgloss.Color("#123456")), + "bg": r.NewStyle().Background(lipgloss.Color("#123456")), + "fg + bg": r.NewStyle(). + Background(lipgloss.Color("#123456")). + Foreground(lipgloss.Color("#123456")), + "italics": r.NewStyle().Italic(true), + "strikethrough": r.NewStyle().Strikethrough(true), + } { + testBytes(t, name, []byte(style.Render("foobar"))) + } + + testBytes(t, "style", []byte("\033[48;2;255;0;0m \033[0m\033[3;38;2;0;0;255;48;2;255;0;0mtest\033[0m")) +} From 9444cdab787c620dd2f973eef0088105ad43867c Mon Sep 17 00:00:00 2001 From: Caleb Foust Date: Wed, 7 Aug 2024 06:31:48 +0800 Subject: [PATCH 3/9] fix: remove errant log line --- pkg/geom/tty/render_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/geom/tty/render_test.go b/pkg/geom/tty/render_test.go index 254c0c4a..04b09304 100644 --- a/pkg/geom/tty/render_test.go +++ b/pkg/geom/tty/render_test.go @@ -36,7 +36,6 @@ func testBytes( name, string(newBytes), ) - t.Logf("%s %+v", name, termB.Screen()[0][0]) } func TestAttributes(t *testing.T) { From f7a14de1573c47fba8a55b765450d75e98054dcd Mon Sep 17 00:00:00 2001 From: Caleb Foust Date: Wed, 7 Aug 2024 16:14:44 +0800 Subject: [PATCH 4/9] fix: accurate color rendering --- pkg/cy/stories.go | 7 ++- pkg/emu/color.go | 81 ++++++++++++++++++++++++++++++---- pkg/emu/state.go | 16 +++---- pkg/emu/str.go | 53 ++++++++++------------ pkg/geom/tty/render.go | 18 +++----- pkg/geom/tty/render_test.go | 6 ++- pkg/replay/movement/testing.go | 2 +- pkg/taro/render.go | 6 +-- 8 files changed, 123 insertions(+), 66 deletions(-) diff --git a/pkg/cy/stories.go b/pkg/cy/stories.go index 8ae97ba4..d07099a4 100644 --- a/pkg/cy/stories.go +++ b/pkg/cy/stories.go @@ -578,7 +578,12 @@ func init() { (def cmd1 (shell/new)) (layout/set {:type :borders - :title ":title" + :title (style/render {:fg "#ff0000" + :italic true + :bold true + :bg "#123456" + #:strikethrough true + } ":title") :title-bottom ":title-bottom" :node {:type :pane :id cmd1 :attached true}}) `) diff --git a/pkg/emu/color.go b/pkg/emu/color.go index e9397083..bebbeed2 100644 --- a/pkg/emu/color.go +++ b/pkg/emu/color.go @@ -1,5 +1,36 @@ package emu +const ( + // We need to be able to distinguish colors that have never been + // explicitly set. + colorSet uint32 = 1 << 24 + // We also need to be able to distinguish between the background and + // foreground colors--but _only_ when they have not yet been set. + colorBG uint32 = 1 << 25 + // This flag determines whether the color should be interpreted as an + // RGB color. The old method meant that all RGB colors within + // 0x0000ff were represented incorrectly. + colorRGB uint32 = 1 << 26 + + colorAnsi uint32 = 0xf + colorXterm uint32 = 0xff +) + +// Color maps to the ANSI colors [0, 16) and the xterm colors [16, 256). +type Color uint32 + +func ANSIColor(color int) Color { + return Color(colorSet | (uint32(color) & colorAnsi)) +} + +func XTermColor(color int) Color { + return Color(colorSet | (uint32(color) & colorXterm)) +} + +func RGBColor(r, g, b int) Color { + return Color(colorSet | colorRGB | (uint32(r) << 16) | (uint32(g) << 8) | uint32(b)) +} + // ANSI color values const ( Black Color = iota @@ -24,15 +55,49 @@ const ( // For example, a transparent background. Otherwise, the simple case is to // map default colors to another color. const ( - DefaultFG Color = 1<<24 + iota - DefaultBG - DefaultCursor + DefaultFG Color = 0 + DefaultBG Color = Color(colorBG) ) -// Color maps to the ANSI colors [0, 16) and the xterm colors [16, 256). -type Color uint32 +// Default reports whether this color has been set. If false, it represents +// the default foreground or background color. +func (c Color) Default() bool { + return (uint32(c) & colorSet) == 0 +} + +// RGB reports whether Color is in the RGB color space. +func (c Color) RGB() (r, g, b int, ok bool) { + if c.Default() { + return + } + + r = int((c >> 16) & 0xff) + g = int((c >> 8) & 0xff) + b = int(c & 0xff) + ok = (uint32(c) & colorRGB) > 0 + return +} + +// ANSI reports whether Color is within [0, 16) and returns that color if +// it is. +func (c Color) ANSI() (color int, ok bool) { + if c.Default() || uint32(c)&colorXterm >= 16 { + return + } + + color = int(c & 0xf) + ok = (uint32(c) & colorRGB) == 0 + return +} + +// XTerm reports whether Color is within [0, 256) and returns that color if +// it is. +func (c Color) XTerm() (color int, ok bool) { + if c.Default() { + return + } -// ANSI returns true if Color is within [0, 16). -func (c Color) ANSI() bool { - return (c < 16) + color = int(c & 0xff) + ok = (uint32(c) & colorRGB) == 0 + return } diff --git a/pkg/emu/state.go b/pkg/emu/state.go index 586e2947..debc4fbe 100644 --- a/pkg/emu/state.go +++ b/pkg/emu/state.go @@ -736,7 +736,7 @@ func (t *State) setAttr(attr []int) { if i+2 < len(attr) && attr[i+1] == 5 { i += 2 if between(attr[i], 0, 255) { - t.cur.Attr.FG = Color(attr[i]) + t.cur.Attr.FG = XTermColor(attr[i]) } else { t.logf("bad fgcolor %d\n", attr[i]) } @@ -746,7 +746,7 @@ func (t *State) setAttr(attr []int) { if !between(r, 0, 255) || !between(g, 0, 255) || !between(b, 0, 255) { t.logf("bad fg rgb color (%d,%d,%d)\n", r, g, b) } else { - t.cur.Attr.FG = Color(r<<16 | g<<8 | b) + t.cur.Attr.FG = RGBColor(r, g, b) } } else { t.logf("gfx attr %d unknown\n", a) @@ -757,7 +757,7 @@ func (t *State) setAttr(attr []int) { if i+2 < len(attr) && attr[i+1] == 5 { i += 2 if between(attr[i], 0, 255) { - t.cur.Attr.BG = Color(attr[i]) + t.cur.Attr.BG = XTermColor(attr[i]) } else { t.logf("bad bgcolor %d\n", attr[i]) } @@ -767,7 +767,7 @@ func (t *State) setAttr(attr []int) { if !between(r, 0, 255) || !between(g, 0, 255) || !between(b, 0, 255) { t.logf("bad bg rgb color (%d,%d,%d)\n", r, g, b) } else { - t.cur.Attr.BG = Color(r<<16 | g<<8 | b) + t.cur.Attr.BG = RGBColor(r, g, b) } } else { t.logf("gfx attr %d unknown\n", a) @@ -776,13 +776,13 @@ func (t *State) setAttr(attr []int) { t.cur.Attr.BG = DefaultBG default: if between(a, 30, 37) { - t.cur.Attr.FG = Color(a - 30) + t.cur.Attr.FG = ANSIColor(a - 30) } else if between(a, 40, 47) { - t.cur.Attr.BG = Color(a - 40) + t.cur.Attr.BG = ANSIColor(a - 40) } else if between(a, 90, 97) { - t.cur.Attr.FG = Color(a - 90 + 8) + t.cur.Attr.FG = ANSIColor(a - 90 + 8) } else if between(a, 100, 107) { - t.cur.Attr.BG = Color(a - 100 + 8) + t.cur.Attr.BG = ANSIColor(a - 100 + 8) } else { t.logf("gfx attr %d unknown\n", a) } diff --git a/pkg/emu/str.go b/pkg/emu/str.go index d3c0aaa9..c5c1eed8 100644 --- a/pkg/emu/str.go +++ b/pkg/emu/str.go @@ -77,8 +77,8 @@ func (t *State) handleSTR() { c := s.argString(1, "") p := &c if p != nil && *p == "?" { - t.oscColorResponse(int(DefaultFG), 10) - } else if err := t.setColorName(int(DefaultFG), p); err != nil { + t.oscColorResponse(DefaultFG, 10) + } else if err := t.setColorName(DefaultFG, p); err != nil { t.logf("invalid foreground color: %s\n", maybe(p)) } else { // TODO: redraw @@ -91,8 +91,8 @@ func (t *State) handleSTR() { c := s.argString(1, "") p := &c if p != nil && *p == "?" { - t.oscColorResponse(int(DefaultBG), 11) - } else if err := t.setColorName(int(DefaultBG), p); err != nil { + t.oscColorResponse(DefaultBG, 11) + } else if err := t.setColorName(DefaultBG, p); err != nil { t.logf("invalid cursor color: %s\n", maybe(p)) } else { // TODO: redraw @@ -125,8 +125,8 @@ func (t *State) handleSTR() { j = s.arg(1, 0) } if p != nil && *p == "?" { // report - t.osc4ColorResponse(j) - } else if err := t.setColorName(j, p); err != nil { + t.osc4ColorResponse(XTermColor(j)) + } else if err := t.setColorName(XTermColor(j), p); err != nil { if !(d == 104 && len(s.args) <= 1) { t.logf("invalid color j=%d, p=%s\n", j, maybe(p)) } @@ -153,60 +153,53 @@ func (t *State) handleSTR() { } } -func (t *State) setColorName(j int, p *string) error { - if !between(j, 0, 1<<24) { - return fmt.Errorf("invalid color value %d", j) - } - +func (t *State) setColorName(j Color, p *string) error { if p == nil { // restore color - delete(t.colorOverride, Color(j)) + delete(t.colorOverride, j) } else { // set color r, g, b, err := parseColor(*p) if err != nil { return err } - t.colorOverride[Color(j)] = Color(r<<16 | g<<8 | b) + t.colorOverride[j] = RGBColor(r, g, b) } return nil } -func (t *State) oscColorResponse(j, num int) { - if j < 0 { - t.logf("failed to fetch osc color %d\n", j) - return - } - - k, ok := t.colorOverride[Color(j)] +func (t *State) oscColorResponse(j Color, num int) { + k, ok := t.colorOverride[j] if ok { - j = int(k) + j = k } - r, g, b := rgb(j) + r, g, b, ok := j.RGB() + if !ok { + return + } t.w.Write([]byte(fmt.Sprintf("\033]%d;rgb:%02x%02x/%02x%02x/%02x%02x\007", num, r, r, g, g, b, b))) } -func (t *State) osc4ColorResponse(j int) { +func (t *State) osc4ColorResponse(j Color) { if j < 0 { t.logf("failed to fetch osc4 color %d\n", j) return } - k, ok := t.colorOverride[Color(j)] + k, ok := t.colorOverride[j] if ok { - j = int(k) + j = k } - r, g, b := rgb(j) + r, g, b, ok := j.RGB() + if !ok { + return + } t.w.Write([]byte(fmt.Sprintf("\033]4;%d;rgb:%02x%02x/%02x%02x/%02x%02x\007", j, r, r, g, g, b, b))) } -func rgb(j int) (r, g, b int) { - return (j >> 16) & 0xff, (j >> 8) & 0xff, j & 0xff -} - var ( RGBPattern = regexp.MustCompile(`^([\da-f]{1})\/([\da-f]{1})\/([\da-f]{1})$|^([\da-f]{2})\/([\da-f]{2})\/([\da-f]{2})$|^([\da-f]{3})\/([\da-f]{3})\/([\da-f]{3})$|^([\da-f]{4})\/([\da-f]{4})\/([\da-f]{4})$`) HashPattern = regexp.MustCompile(`[\da-f]`) diff --git a/pkg/geom/tty/render.go b/pkg/geom/tty/render.go index 12dea72a..98307425 100644 --- a/pkg/geom/tty/render.go +++ b/pkg/geom/tty/render.go @@ -13,8 +13,6 @@ import ( func setColor(info *terminfo.Terminfo, color emu.Color, isBg bool) []byte { data := new(bytes.Buffer) - num := uint32(color) - maxColors := uint32(info.Nums[terminfo.MaxColors]) if (!isBg && color == emu.DefaultFG) || (isBg && color == emu.DefaultBG) { return make([]byte, 0) @@ -22,30 +20,24 @@ func setColor(info *terminfo.Terminfo, color emu.Color, isBg bool) []byte { // Special case for reversed text when still set to default if isBg && color == emu.DefaultFG { - num = 15 - color = 15 + color = emu.ANSIColor(15) } else if !isBg && color == emu.DefaultBG { - num = 0 - color = 0 + color = emu.ANSIColor(0) } - if num > maxColors { - r := color >> 16 - g := (color >> 8) & 0xff - b := color & 0xff - + if r, g, b, ok := color.RGB(); ok { if isBg { fmt.Fprintf(data, "\x1b[48;2;%d;%d;%dm", r, g, b) } else { fmt.Fprintf(data, "\x1b[38;2;%d;%d;%dm", r, g, b) } - } else { + } else if xterm, ok := color.XTerm(); ok { code := terminfo.SetABackground if !isBg { code = terminfo.SetAForeground } - info.Fprintf(data, code, int(color)) + info.Fprintf(data, code, xterm) } return data.Bytes() diff --git a/pkg/geom/tty/render_test.go b/pkg/geom/tty/render_test.go index 04b09304..280f44be 100644 --- a/pkg/geom/tty/render_test.go +++ b/pkg/geom/tty/render_test.go @@ -32,8 +32,9 @@ func testBytes( t, termA.Screen(), termB.Screen(), - "style %s was not equal: %#v", + "style %s was not equal: '%#v' '%#v'", name, + string(bytes), string(newBytes), ) } @@ -52,8 +53,9 @@ func TestAttributes(t *testing.T) { Foreground(lipgloss.Color("#123456")), "italics": r.NewStyle().Italic(true), "strikethrough": r.NewStyle().Strikethrough(true), + "bg 255": r.NewStyle().Foreground(lipgloss.Color("255")), } { - testBytes(t, name, []byte(style.Render("foobar"))) + testBytes(t, name, []byte(style.Render("f"))) } testBytes(t, "style", []byte("\033[48;2;255;0;0m \033[0m\033[3;38;2;0;0;255;48;2;255;0;0mtest\033[0m")) diff --git a/pkg/replay/movement/testing.go b/pkg/replay/movement/testing.go index 9835f465..d65e0451 100644 --- a/pkg/replay/movement/testing.go +++ b/pkg/replay/movement/testing.go @@ -17,7 +17,7 @@ func TestHighlight( highlights []Highlight, lines ...string, ) { - bg := emu.Color(1) + bg := emu.ANSIColor(1) for i := range highlights { highlights[i].BG = bg } diff --git a/pkg/taro/render.go b/pkg/taro/render.go index 03dd1581..7e7258ec 100644 --- a/pkg/taro/render.go +++ b/pkg/taro/render.go @@ -48,12 +48,12 @@ func (r *Renderer) RenderImage(value string) image.Image { func (r *Renderer) ConvertLipgloss(color lipgloss.Color) emu.Color { switch c := r.ColorProfile().Color(string(color)).(type) { case termenv.ANSIColor: - return emu.Color(c) + return emu.ANSIColor(int(c)) case termenv.ANSI256Color: - return emu.Color(c) + return emu.XTermColor(int(c)) case termenv.RGBColor: r, g, b, _ := termenv.ConvertToRGB(c).RGBA() - return emu.Color(r<<16 | g<<8 | b) + return emu.RGBColor(int(r), int(g), int(b)) } return emu.DefaultFG From 1ecf3f86df7a30940af1551c73ad01022e566520 Mon Sep 17 00:00:00 2001 From: Caleb Foust Date: Wed, 7 Aug 2024 16:20:13 +0800 Subject: [PATCH 5/9] feat: strikethrough support --- pkg/emu/module.go | 1 + pkg/emu/state.go | 5 +++++ pkg/geom/tty/render.go | 4 ++++ 3 files changed, 10 insertions(+) diff --git a/pkg/emu/module.go b/pkg/emu/module.go index fe2912ce..ffde8af8 100644 --- a/pkg/emu/module.go +++ b/pkg/emu/module.go @@ -17,6 +17,7 @@ const ( AttrBold AttrGfx AttrItalic + AttrStrikethrough AttrBlink AttrWrap AttrBlank diff --git a/pkg/emu/state.go b/pkg/emu/state.go index debc4fbe..91bd400e 100644 --- a/pkg/emu/state.go +++ b/pkg/emu/state.go @@ -21,6 +21,7 @@ const ( attrBold attrGfx attrItalic + attrStrikethrough attrBlink attrWrap attrBlank @@ -722,6 +723,8 @@ func (t *State) setAttr(attr []int) { t.cur.Attr.Mode |= attrBlink case 7: t.cur.Attr.Mode |= attrReverse + case 9: + t.cur.Attr.Mode |= attrStrikethrough case 21, 22: t.cur.Attr.Mode &^= attrBold case 23: @@ -732,6 +735,8 @@ func (t *State) setAttr(attr []int) { t.cur.Attr.Mode &^= attrBlink case 27: t.cur.Attr.Mode &^= attrReverse + case 29: + t.cur.Attr.Mode &^= attrStrikethrough case 38: if i+2 < len(attr) && attr[i+1] == 5 { i += 2 diff --git a/pkg/geom/tty/render.go b/pkg/geom/tty/render.go index 98307425..4b730689 100644 --- a/pkg/geom/tty/render.go +++ b/pkg/geom/tty/render.go @@ -78,6 +78,10 @@ func swapImage( info.Fprintf(data, terminfo.EnterUnderlineMode) } + if mode&emu.AttrStrikethrough != 0 { + data.Write([]byte("\033[9m")) + } + if mode&emu.AttrItalic != 0 { // TODO(cfoust): 08/07/24 why does this not work? //info.Fprintf(data, terminfo.EnterItalicsMode) From 8dae5c46a253d04d371f5faf1165ef79454ae7fb Mon Sep 17 00:00:00 2001 From: Caleb Foust Date: Wed, 7 Aug 2024 16:24:00 +0800 Subject: [PATCH 6/9] fix: reset strikethrough --- pkg/geom/tty/render.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/geom/tty/render.go b/pkg/geom/tty/render.go index 4b730689..3d6f515b 100644 --- a/pkg/geom/tty/render.go +++ b/pkg/geom/tty/render.go @@ -97,6 +97,11 @@ func swapImage( data.Write([]byte(string(srcCell.Char))) + // TODO(cfoust): 08/07/24 why does ExitAttributeMode not cover this in alacritty? + if mode&emu.AttrStrikethrough != 0 { + data.Write([]byte("\033[29m")) + } + info.Fprintf(data, terminfo.ExitAttributeMode) // CJK characters From a7a962d8d126ebeeafbc758dbe92311f2744ad8f Mon Sep 17 00:00:00 2001 From: Caleb Foust Date: Wed, 7 Aug 2024 16:29:17 +0800 Subject: [PATCH 7/9] fix: mode reset in emu --- pkg/emu/state.go | 2 +- pkg/geom/tty/render_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/emu/state.go b/pkg/emu/state.go index 91bd400e..6fc1065e 100644 --- a/pkg/emu/state.go +++ b/pkg/emu/state.go @@ -710,7 +710,7 @@ func (t *State) setAttr(attr []int) { a := attr[i] switch a { case 0: - t.cur.Attr.Mode &^= attrReverse | attrUnderline | attrBold | attrItalic | attrBlink + t.cur.Attr.Mode &^= attrReverse | attrStrikethrough | attrUnderline | attrBold | attrItalic | attrBlink t.cur.Attr.FG = DefaultFG t.cur.Attr.BG = DefaultBG case 1: diff --git a/pkg/geom/tty/render_test.go b/pkg/geom/tty/render_test.go index 280f44be..9d172cf6 100644 --- a/pkg/geom/tty/render_test.go +++ b/pkg/geom/tty/render_test.go @@ -55,7 +55,7 @@ func TestAttributes(t *testing.T) { "strikethrough": r.NewStyle().Strikethrough(true), "bg 255": r.NewStyle().Foreground(lipgloss.Color("255")), } { - testBytes(t, name, []byte(style.Render("f"))) + testBytes(t, name, []byte(style.Render("on")+" off")) } testBytes(t, "style", []byte("\033[48;2;255;0;0m \033[0m\033[3;38;2;0;0;255;48;2;255;0;0mtest\033[0m")) From 3fab494873c34f5a22a1088dfb2e85e4df0229cf Mon Sep 17 00:00:00 2001 From: Caleb Foust Date: Thu, 8 Aug 2024 05:41:35 +0800 Subject: [PATCH 8/9] feat: documentation for styles --- docs/src/api.md | 13 +++++++++++++ docs/src/layouts.md | 24 +++++++++++++++++++++++ pkg/cy/api/docs-style.md | 33 ++++++++++++++++++++++++++++++++ pkg/cy/api/docs.go | 9 +++++++++ pkg/cy/boot/style.janet | 41 ++++++++++++++++++++++++++++++++++++++++ pkg/cy/janet.go | 1 + pkg/cy/stories.go | 32 +++++++++++++++++++++++++------ 7 files changed, 147 insertions(+), 6 deletions(-) create mode 100644 pkg/cy/api/docs-style.md create mode 100644 pkg/cy/boot/style.janet diff --git a/docs/src/api.md b/docs/src/api.md index 41ed9325..5aa86880 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -25,6 +25,19 @@ For example: } ``` +#### Color + +Some API functions, such as {{api style/render}}, accept colors as input. `cy` supports RGB colors specified in hexadecimal and direct references to colors in the 256-color terminal color palette traditionally supported in terminal emulators like `xterm`. You can read more about color support in terminal emiulators [here](https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797#color-codes). + +Examples of valid colors: + +```janet +"#ffffff" +"#123456" +"255" +"0" +``` + #### NodeID Many API functions have a parameter of type `NodeID`, which can be one of two values: diff --git a/docs/src/layouts.md b/docs/src/layouts.md index 84e51c55..a7b6a162 100644 --- a/docs/src/layouts.md +++ b/docs/src/layouts.md @@ -215,3 +215,27 @@ Some nodes have a `:border` property that determines the appearance of the node' ### Frames The patterned background seen in the screenshot above is referred to as the **frame**. `cy` comes with a [range of different frames](/frames.md). You can choose between all of the available frames using the {{api action/choose-frame}} function, which is bound by default to {{bind :root ctrl+a F}}, and set the default frame on startup using the [`:default-frame`](/default-parameters.md#default-frame) parameter. + +### Styling + +{{story cast layout/styled}} + +All string layout properties accept text styled with {{api style/render}} (or {{api style/text}}). + +The layout shown in the asciicast above was generated with the following code: +```janet +(def cmd1 (shell/new)) +(def cmd2 (shell/new)) +(layout/set + (layout/new + (split + (borders + (attach :id cmd1) + :title (style/text "some pane" :bg "6") + :title-bottom (style/text "some subtitle" :bg "6")) + (borders + (pane :id cmd2) + :title (style/text "some pane" :italic true :bg "5") + :title-bottom (style/text "some subtitle" :italic true :bg "5")) + :border :none))) +``` diff --git a/pkg/cy/api/docs-style.md b/pkg/cy/api/docs-style.md new file mode 100644 index 00000000..ef03d384 --- /dev/null +++ b/pkg/cy/api/docs-style.md @@ -0,0 +1,33 @@ +# doc: Render + +(style/render style text) + +Apply styling effects to some text. This function generates a string containing ANSI escape sequences that will style the provided `text` according to `style`. + +All `cy` API functions that render text to the screen, such as {{api layout/set}} and {{api input/find}} accept input styled with {{api style/render}}. + +`style` is a struct with any of the following properties: + +- `:fg`: The foreground [color](/api.md#color) of the text. +- `:bg`: The background [color](/api.md#color) of the text. +- `:width`: The number of horizontal cells the text should occupy. Padding is added if this value exceeds the length of `text`. +- `:height`: The number of vertical cells the text should occupy. Padding is added if this value exceeds the height of `text`. +- `:align-horizontal`: One of `:left`, `:center`, or `:right`. If `:width` is greater than the length of the text, the text will be aligned according to this property. +- `:align-vertical`: One of `:top`, `:center`, or `:bottom`. If `:height` is greater than the height of the text, the text will be aligned according to this property. +- `:bold`: A boolean indicating whether the text should be bolded. +- `:italic`: A boolean indicating whether the text should be italic. +- `:underline`: A boolean indicating whether the text should be underlined. +- `:strikethrough`: A boolean indicating whether the text should be struck through. +- `:reverse`: A boolean indicating whether the foreground and background colorshould be reversed. +- `:blink`: A boolean indicating whether the text should blink. +- `:faint`: A boolean indicating whether the text should be faint. + +For example: + +```janet +(style/render + {:bg "4" + :bold true + :width 15 + } "some text") +``` diff --git a/pkg/cy/api/docs.go b/pkg/cy/api/docs.go index 3e7e5dc2..68104148 100644 --- a/pkg/cy/api/docs.go +++ b/pkg/cy/api/docs.go @@ -122,3 +122,12 @@ var _ janet.Documented = (*LayoutModule)(nil) func (l *LayoutModule) Documentation() string { return DOCS_LAYOUT } + +//go:embed docs-style.md +var DOCS_STYLE string + +var _ janet.Documented = (*StyleModule)(nil) + +func (s *StyleModule) Documentation() string { + return DOCS_STYLE +} diff --git a/pkg/cy/boot/style.janet b/pkg/cy/boot/style.janet new file mode 100644 index 00000000..ad7caf30 --- /dev/null +++ b/pkg/cy/boot/style.janet @@ -0,0 +1,41 @@ +(defn + style/text + ````Style the provided text with the attributes provided. This function is a convenient wrapper around {{api style/render}}; instead of providing a struct, you may pass any of the attributes {{api style/render}} supports as named parameters. + +For example: +```janet +(style/text "foobar" :bg "#00ff00") +(style/text "foobar" :italic true :bold true :width 15) +``` + ```` + [text + &named + fg + bg + width + height + align-horizontal + align-vertical + bold + italic + underline + strikethrough + reverse + blink + faint] + + (style/render + {:fg fg + :bg bg + :width width + :height height + :align-horizontal align-horizontal + :align-vertical align-vertical + :bold bold + :italic italic + :underline underline + :strikethrough strikethrough + :reverse reverse + :blink blink + :faint faint} + text)) diff --git a/pkg/cy/janet.go b/pkg/cy/janet.go index 2caffb81..43055822 100644 --- a/pkg/cy/janet.go +++ b/pkg/cy/janet.go @@ -62,6 +62,7 @@ func (c *Cy) initJanet(ctx context.Context) (*janet.VM, error) { // like 01_actions.janet, 02_layout.janet is ugly files := []string{ "actions.janet", + "style.janet", "layout.janet", "binds.janet", } diff --git a/pkg/cy/stories.go b/pkg/cy/stories.go index d07099a4..2b376175 100644 --- a/pkg/cy/stories.go +++ b/pkg/cy/stories.go @@ -578,15 +578,35 @@ func init() { (def cmd1 (shell/new)) (layout/set {:type :borders - :title (style/render {:fg "#ff0000" - :italic true - :bold true - :bg "#123456" - #:strikethrough true - } ":title") + :title ":title" :title-bottom ":title-bottom" :node {:type :pane :id cmd1 :attached true}}) `) return screen, err }, stories.Config{}) + + stories.Register("layout/styled", func(ctx context.Context) ( + mux.Screen, + error, + ) { + _, client, screen, err := createStory(ctx) + err = client.execute(` +(def cmd1 (shell/new)) +(def cmd2 (shell/new)) +(layout/set + (layout/new + (split + (borders + (attach :id cmd1) + :title (style/text "some pane" :bg "6") + :title-bottom (style/text "some subtitle" :bg "6")) + (borders + (pane :id cmd2) + :title (style/text "some pane" :italic true :bg "5") + :title-bottom (style/text "some subtitle" :italic true :bg "5")) + :border :none) + )) + `) + return screen, err + }, stories.Config{}) } From 110ffd05bdd09bb119f1d728206913fa5f786a85 Mon Sep 17 00:00:00 2001 From: Caleb Foust Date: Thu, 8 Aug 2024 06:20:19 +0800 Subject: [PATCH 9/9] feat: border colors --- docs/src/api.md | 12 ++++--- docs/src/layouts.md | 48 ++++++++++++++++++------- pkg/cy/api/layout_test.janet | 2 ++ pkg/cy/boot/layout.janet | 31 ++++++++++------ pkg/cy/stories.go | 28 ++++++++------- pkg/layout/borders/module.go | 46 ++++++++++++++++-------- pkg/layout/janet.go | 57 ++++++++++++++++++++++++++--- pkg/layout/margins/module.go | 70 ++++++++++++++++++++++++++---------- pkg/layout/module.go | 16 ++++++--- pkg/layout/split/module.go | 67 ++++++++++++++++++++++++---------- pkg/style/color.go | 16 +++++++++ 11 files changed, 294 insertions(+), 99 deletions(-) diff --git a/docs/src/api.md b/docs/src/api.md index 5aa86880..b478c0da 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -27,15 +27,17 @@ For example: #### Color -Some API functions, such as {{api style/render}}, accept colors as input. `cy` supports RGB colors specified in hexadecimal and direct references to colors in the 256-color terminal color palette traditionally supported in terminal emulators like `xterm`. You can read more about color support in terminal emiulators [here](https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797#color-codes). +Some API functions, such as {{api style/render}}, accept colors as input. `cy` supports True Color colors specified in hexadecimal along with ANSI 16 and ANSI-256 colors. Under the hood, `cy` uses [charmbracelet/lipgloss](https://github.com/charmbracelet/lipgloss?tab=readme-ov-file#colors) and thus supports its color references. + +You can read more about color support in terminal emiulators [here](https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797#color-codes). Examples of valid colors: ```janet -"#ffffff" -"#123456" -"255" -"0" +"#ffffff" # true color +"#123456" # true color +"255" # ANSI 256 +"0" # ANSI 16 ``` #### NodeID diff --git a/docs/src/layouts.md b/docs/src/layouts.md index a7b6a162..ebe61ba3 100644 --- a/docs/src/layouts.md +++ b/docs/src/layouts.md @@ -118,6 +118,8 @@ A `:margins` node puts transparent margins around its child allowing the current :cols 0 # number, optional :rows 0 # number, optional :border :rounded # border type, optional + :border-fg nil # color, optional + :border-bg nil # color, optional :node {} # a node } ``` @@ -130,6 +132,10 @@ These properties set the size of the node inside of the `:margins` node; they do The [border style](#border-styles) for the borders around the node. +`:border-fg` and `:border-bg` + +The foreground and background [color](/api.md#color) of the border. + `:node` A valid layout node. @@ -147,6 +153,8 @@ A `:split` node divides its visual space in two and gives it to two other nodes, :cells nil # int or nil, optional :percent nil # int or nil, optional :border :rounded # border type, optional + :border-fg nil # color, optional + :border-bg nil # color, optional :a {} # a node :b {} # a node } @@ -164,6 +172,10 @@ At most one of these can be defined. Both determine the amount of space given to The [border style](#border-styles) to use for the dividing line. +`:border-fg` and `:border-bg` + +The foreground and background [color](/api.md#color) of the border. + `:a` and `:b` Both must be valid layout nodes. @@ -180,6 +192,8 @@ A `:borders` node surrounds its child in borders and adds an optional title to t :title nil # string or nil, optional :title-bottom nil # string or nil, optional :border :rounded # border type, optional + :border-fg nil # color, optional + :border-bg nil # color, optional :node {} # a node } ``` @@ -192,6 +206,10 @@ These strings will be rendered on the top and the bottom of the window, respecti The [border style](#border-styles) for this node. `:none` is not supported. +`:border-fg` and `:border-bg` + +The foreground and background [color](/api.md#color) of the border. + `:node` A valid layout node. @@ -222,20 +240,26 @@ The patterned background seen in the screenshot above is referred to as the **fr All string layout properties accept text styled with {{api style/render}} (or {{api style/text}}). -The layout shown in the asciicast above was generated with the following code: +The layout shown in the monstrosity above was generated with the following code: + ```janet (def cmd1 (shell/new)) (def cmd2 (shell/new)) (layout/set - (layout/new - (split - (borders - (attach :id cmd1) - :title (style/text "some pane" :bg "6") - :title-bottom (style/text "some subtitle" :bg "6")) - (borders - (pane :id cmd2) - :title (style/text "some pane" :italic true :bg "5") - :title-bottom (style/text "some subtitle" :italic true :bg "5")) - :border :none))) + (layout/new + (margins + (split + (borders + (attach :id cmd1) + :border-fg "6" + :title (style/text "some pane" :fg "0" :bg "6") + :title-bottom (style/text "some subtitle" :fg "0" :bg "6")) + (borders + (pane :id cmd2) + :border-fg "5" + :title (style/text "some pane" :italic true :bg "5") + :title-bottom (style/text "some subtitle" :italic true :bg "5")) + :border-bg "3") + :cols 70 + :border-bg "4"))) ``` diff --git a/pkg/cy/api/layout_test.janet b/pkg/cy/api/layout_test.janet index b1c91789..07e31f7b 100644 --- a/pkg/cy/api/layout_test.janet +++ b/pkg/cy/api/layout_test.janet @@ -50,6 +50,8 @@ {:type :split :vertical true :percent 26 + :border-fg "7" + :border-bg "7" :a {:type :pane :attached true} :b {:type :pane}}) diff --git a/pkg/cy/boot/layout.janet b/pkg/cy/boot/layout.janet index 76b32aab..6b0ff095 100644 --- a/pkg/cy/boot/layout.janet +++ b/pkg/cy/boot/layout.janet @@ -52,7 +52,7 @@ For example: (defn layout/split ```Convenience function for creating a new split node.``` - [a b &named vertical cells percent border] + [a b &named vertical cells percent border border-fg border-bg] (default vertical false) {:type :split :a a @@ -60,47 +60,58 @@ For example: :vertical vertical :cells cells :percent percent - :border border}) + :border border + :border-fg border-fg + :border-bg border-bg}) (defn layout/vsplit ```Convenience function for creating a new vertical split node.``` - [a b &named cells percent border] + [a b &named cells percent border border-fg border-bg] (layout/split a b :vertical true :cells cells :percent percent - :border border)) + :border border + :border-fg border-fg + :border-bg border-bg)) (defn layout/hsplit ```Convenience function for creating a new horizontal split node.``` - [a b &named cells percent border] + [a b &named cells percent border border-fg border-bg] (layout/split a b :vertical false :cells cells :percent percent - :border border)) + :border border + :border-fg border-fg + :border-bg border-bg)) (defn layout/margins ```Convenience function for creating a new margins node.``` - [node &named cols rows border] + [node &named cols rows border border-fg border-bg] {:type :margins :node node :cols cols :rows rows - :border border}) + :border border + :border-fg border-fg + :border-bg border-bg}) (defn layout/borders ```Convenience function for creating a new borders node.``` - [node &named title title-bottom border] + [node &named title title-bottom border border-fg border-bg] {:type :borders :node node :title title :title-bottom title-bottom - :border border}) + :border border + :border-fg border-fg + :border-bg border-bg}) + (defmacro layout/new diff --git a/pkg/cy/stories.go b/pkg/cy/stories.go index 2b376175..4f0bedf9 100644 --- a/pkg/cy/stories.go +++ b/pkg/cy/stories.go @@ -594,18 +594,22 @@ func init() { (def cmd1 (shell/new)) (def cmd2 (shell/new)) (layout/set - (layout/new - (split - (borders - (attach :id cmd1) - :title (style/text "some pane" :bg "6") - :title-bottom (style/text "some subtitle" :bg "6")) - (borders - (pane :id cmd2) - :title (style/text "some pane" :italic true :bg "5") - :title-bottom (style/text "some subtitle" :italic true :bg "5")) - :border :none) - )) + (layout/new + (margins + (split + (borders + (attach :id cmd1) + :border-fg "6" + :title (style/text "some pane" :fg "0" :bg "6") + :title-bottom (style/text "some subtitle" :fg "0" :bg "6")) + (borders + (pane :id cmd2) + :border-fg "5" + :title (style/text "some pane" :italic true :bg "5") + :title-bottom (style/text "some subtitle" :italic true :bg "5")) + :border-bg "3") + :cols 70 + :border-bg "4"))) `) return screen, err }, stories.Config{}) diff --git a/pkg/layout/borders/module.go b/pkg/layout/borders/module.go index 72d40d9c..c7f69953 100644 --- a/pkg/layout/borders/module.go +++ b/pkg/layout/borders/module.go @@ -17,13 +17,14 @@ import ( type Borders struct { deadlock.RWMutex *mux.UpdatePublisher - render *taro.Renderer - screen mux.Screen - size geom.Size - inner geom.Rect - borderStyle *style.Border + render *taro.Renderer + screen mux.Screen + size geom.Size + inner geom.Rect title, titleBottom string + borderStyle *style.Border + borderFg, borderBg *style.Color } var _ mux.Screen = (*Borders)(nil) @@ -39,13 +40,19 @@ func (l *Borders) Apply(node L.NodeType) (bool, error) { defer l.Unlock() l.borderStyle = config.Border + l.borderBg = config.BorderBg + l.borderFg = config.BorderFg if config.Title != nil { l.title = *config.Title + } else { + l.title = "" } if config.TitleBottom != nil { l.titleBottom = *config.TitleBottom + } else { + l.titleBottom = "" } return true, nil @@ -61,11 +68,15 @@ func (l *Borders) Kill() { func (l *Borders) State() *tty.State { l.RLock() - inner := l.inner - size := l.size - borderStyle := l.borderStyle - title := l.title - titleBottom := l.titleBottom + var ( + inner = l.inner + size = l.size + borderStyle = l.borderStyle + title = l.title + titleBottom = l.titleBottom + borderFg = l.borderFg + borderBg = l.borderBg + ) l.RUnlock() innerState := l.screen.State() @@ -73,7 +84,7 @@ func (l *Borders) State() *tty.State { tty.Copy(inner.Position, state, innerState) - boxText := l.render.NewStyle(). + boxStyle := l.render.NewStyle(). Border(borderStyle.Border). BorderForeground(lipgloss.Color("7")). BorderTop(true). @@ -81,10 +92,17 @@ func (l *Borders) State() *tty.State { BorderRight(true). BorderBottom(true). Width(inner.Size.C). - Height(inner.Size.R). - Render("") + Height(inner.Size.R) + + if borderFg != nil { + boxStyle = boxStyle.BorderForeground(borderFg.Color) + } + + if borderBg != nil { + boxStyle = boxStyle.BorderBackground(borderBg.Color) + } - l.render.RenderAt(state.Image, 0, 0, boxText) + l.render.RenderAt(state.Image, 0, 0, boxStyle.Render("")) if len(title) > 0 { l.render.RenderAt( diff --git a/pkg/layout/janet.go b/pkg/layout/janet.go index 743841f0..5399ca04 100644 --- a/pkg/layout/janet.go +++ b/pkg/layout/janet.go @@ -44,6 +44,19 @@ func unmarshalBorder(value *janet.Value) (*style.Border, error) { return &border, nil } +func unmarshalColor(value *janet.Value) (*style.Color, error) { + if value == nil || value.Nil() { + return nil, nil + } + + var color style.Color + err := value.Unmarshal(&color) + if err != nil { + return nil, err + } + return &color, nil +} + func unmarshalNode(value *janet.Value) (NodeType, error) { n := nodeType{} err := value.Unmarshal(&n) @@ -84,6 +97,8 @@ func unmarshalNode(value *janet.Value) (NodeType, error) { Border *janet.Value A *janet.Value B *janet.Value + BorderBg *janet.Value + BorderFg *janet.Value } args := splitArgs{} err = value.Unmarshal(&args) @@ -119,6 +134,16 @@ func unmarshalNode(value *janet.Value) (NodeType, error) { return nil, err } + type_.BorderFg, err = unmarshalColor(args.BorderFg) + if err != nil { + return nil, err + } + + type_.BorderBg, err = unmarshalColor(args.BorderBg) + if err != nil { + return nil, err + } + if args.Vertical != nil { type_.Vertical = *args.Vertical } @@ -126,10 +151,12 @@ func unmarshalNode(value *janet.Value) (NodeType, error) { return type_, nil case KEYWORD_MARGINS: type marginsArgs struct { - Cols *int - Rows *int - Border *janet.Value - Node *janet.Value + Cols *int + Rows *int + Border *janet.Value + BorderBg *janet.Value + BorderFg *janet.Value + Node *janet.Value } args := marginsArgs{} err = value.Unmarshal(&args) @@ -159,6 +186,16 @@ func unmarshalNode(value *janet.Value) (NodeType, error) { return nil, err } + type_.BorderFg, err = unmarshalColor(args.BorderFg) + if err != nil { + return nil, err + } + + type_.BorderBg, err = unmarshalColor(args.BorderBg) + if err != nil { + return nil, err + } + return type_, nil case KEYWORD_BORDERS: type borderArgs struct { @@ -166,6 +203,8 @@ func unmarshalNode(value *janet.Value) (NodeType, error) { TitleBottom *string Border *janet.Value Node *janet.Value + BorderBg *janet.Value + BorderFg *janet.Value } args := borderArgs{} err = value.Unmarshal(&args) @@ -195,6 +234,16 @@ func unmarshalNode(value *janet.Value) (NodeType, error) { ) } + type_.BorderFg, err = unmarshalColor(args.BorderFg) + if err != nil { + return nil, err + } + + type_.BorderBg, err = unmarshalColor(args.BorderBg) + if err != nil { + return nil, err + } + return type_, nil } diff --git a/pkg/layout/margins/module.go b/pkg/layout/margins/module.go index b716f0e2..136fe45b 100644 --- a/pkg/layout/margins/module.go +++ b/pkg/layout/margins/module.go @@ -34,7 +34,8 @@ type Margins struct { outer geom.Size inner geom.Rect - borderStyle *style.Border + borderStyle *style.Border + borderFg, borderBg *style.Color } var _ mux.Screen = (*Margins)(nil) @@ -81,11 +82,21 @@ func (l *Margins) Apply(node L.NodeType) (bool, error) { l.setSize(newSize) } - if config.Border != nil { + if config.Border != l.borderStyle { l.borderStyle = config.Border changed = true } + if config.BorderFg != l.borderFg { + l.borderFg = config.BorderFg + changed = true + } + + if config.BorderBg != l.borderBg { + l.borderBg = config.BorderBg + changed = true + } + if !changed { return true, nil } @@ -103,9 +114,13 @@ func (l *Margins) Kill() { func (l *Margins) State() *tty.State { l.RLock() - inner := l.inner - outer := l.outer - borderStyle := l.borderStyle + var ( + inner = l.inner + outer = l.outer + borderStyle = l.borderStyle + borderFg = l.borderFg + borderBg = l.borderBg + ) l.RUnlock() innerState := l.screen.State() @@ -126,20 +141,37 @@ func (l *Margins) State() *tty.State { } } - if borderStyle != nil { - left := geom.Clamp(inner.Position.C-1, 0, size.C-1) - right := geom.Clamp( - inner.Position.C+inner.Size.C, - 0, - size.C-1, - ) - char := []rune(borderStyle.Left)[0] - for row := 0; row < size.R; row++ { - state.Image[row][left].Char = char - state.Image[row][left].Mode ^= emu.AttrTransparent - state.Image[row][right].Char = char - state.Image[row][right].Mode ^= emu.AttrTransparent - } + if borderStyle == nil { + return state + } + + fg := emu.DefaultFG + bg := emu.DefaultBG + + if borderFg != nil { + fg = borderFg.Emu() + } + + if borderBg != nil { + bg = borderBg.Emu() + } + + left := geom.Clamp(inner.Position.C-1, 0, size.C-1) + right := geom.Clamp( + inner.Position.C+inner.Size.C, + 0, + size.C-1, + ) + char := []rune(borderStyle.Left)[0] + for row := 0; row < size.R; row++ { + state.Image[row][left].Char = char + state.Image[row][left].Mode ^= emu.AttrTransparent + state.Image[row][left].FG = fg + state.Image[row][left].BG = bg + state.Image[row][right].Char = char + state.Image[row][right].Mode ^= emu.AttrTransparent + state.Image[row][right].FG = fg + state.Image[row][right].BG = bg } return state diff --git a/pkg/layout/module.go b/pkg/layout/module.go index 1725ec29..a04c94e9 100644 --- a/pkg/layout/module.go +++ b/pkg/layout/module.go @@ -21,22 +21,28 @@ type SplitType struct { Percent *int Cells *int Border *style.Border + BorderFg *style.Color + BorderBg *style.Color A NodeType B NodeType } type MarginsType struct { - Cols int - Rows int - Frame *string - Border *style.Border - Node NodeType + Cols int + Rows int + Frame *string + Border *style.Border + BorderFg *style.Color + BorderBg *style.Color + Node NodeType } type BorderType struct { Title *string TitleBottom *string Border *style.Border + BorderFg *style.Color + BorderBg *style.Color Node NodeType } diff --git a/pkg/layout/split/module.go b/pkg/layout/split/module.go index 8d079019..1a94c4d0 100644 --- a/pkg/layout/split/module.go +++ b/pkg/layout/split/module.go @@ -3,6 +3,7 @@ package split import ( "context" + "github.com/cfoust/cy/pkg/emu" "github.com/cfoust/cy/pkg/geom" "github.com/cfoust/cy/pkg/geom/image" "github.com/cfoust/cy/pkg/geom/tty" @@ -41,7 +42,8 @@ type Split struct { // screen A. This is calculated using `percent`. cells int - borderStyle *style.Border + borderStyle *style.Border + borderFg, borderBg *style.Color } var _ mux.Screen = (*Split)(nil) @@ -66,6 +68,8 @@ func (s *Split) State() *tty.State { positionB = s.positionB isVertical = s.isVertical borderStyle = s.borderStyle + borderFg = s.borderFg + borderBg = s.borderBg ) s.RUnlock() @@ -74,22 +78,6 @@ func (s *Split) State() *tty.State { stateA := s.screenA.State().Clone() image.CopyRaw(geom.Size{}, state.Image, stateA.Image) - if borderStyle != nil { - if !isVertical { - col := geom.Clamp(positionB.C-1, 0, size.C-1) - char := []rune(borderStyle.Left)[0] - for row := 0; row < size.R; row++ { - state.Image[row][col].Char = char - } - } else { - row := geom.Clamp(positionB.R-1, 0, size.R-1) - char := []rune(borderStyle.Top)[0] - for col := 0; col < size.C; col++ { - state.Image[row][col].Char = char - } - } - } - stateB := s.screenB.State().Clone() image.CopyRaw(positionB, state.Image, stateB.Image) @@ -106,6 +94,39 @@ func (s *Split) State() *tty.State { state.CursorVisible = false } + if borderStyle == nil { + return state + } + + fg := emu.DefaultFG + bg := emu.DefaultBG + + if borderFg != nil { + fg = borderFg.Emu() + } + + if borderBg != nil { + bg = borderBg.Emu() + } + + if !isVertical { + col := geom.Clamp(positionB.C-1, 0, size.C-1) + char := []rune(borderStyle.Left)[0] + for row := 0; row < size.R; row++ { + state.Image[row][col].Char = char + state.Image[row][col].FG = fg + state.Image[row][col].BG = bg + } + } else { + row := geom.Clamp(positionB.R-1, 0, size.R-1) + char := []rune(borderStyle.Top)[0] + for col := 0; col < size.C; col++ { + state.Image[row][col].Char = char + state.Image[row][col].FG = fg + state.Image[row][col].BG = bg + } + } + return state } @@ -129,11 +150,21 @@ func (s *Split) Apply(node L.NodeType) (bool, error) { changed = true } - if config.Border != nil { + if config.Border != s.borderStyle { s.borderStyle = config.Border changed = true } + if config.BorderFg != s.borderFg { + s.borderFg = config.BorderFg + changed = true + } + + if config.BorderBg != s.borderBg { + s.borderBg = config.BorderBg + changed = true + } + if !changed { return true, nil } diff --git a/pkg/style/color.go b/pkg/style/color.go index 0a406e9c..280fa848 100644 --- a/pkg/style/color.go +++ b/pkg/style/color.go @@ -1,9 +1,11 @@ package style import ( + "github.com/cfoust/cy/pkg/emu" "github.com/cfoust/cy/pkg/janet" "github.com/charmbracelet/lipgloss" + "github.com/muesli/termenv" ) type Color struct { @@ -29,3 +31,17 @@ var _ janet.Marshalable = (*Color)(nil) func (c *Color) MarshalJanet() interface{} { return string(c.Color) } + +func (c *Color) Emu() emu.Color { + switch c := renderer.ColorProfile().Color(string(c.Color)).(type) { + case termenv.ANSIColor: + return emu.ANSIColor(int(c)) + case termenv.ANSI256Color: + return emu.XTermColor(int(c)) + case termenv.RGBColor: + r, g, b, _ := termenv.ConvertToRGB(c).RGBA() + return emu.RGBColor(int(r), int(g), int(b)) + } + + return emu.DefaultFG +}