Something went wrong. Try again.
small gleam coding and persistent agent daemon with a detachable cli and a canvas ui forked from okami.mom/albedo
Something went wrong. Try again.
14 kB · 427 lines
at density
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428ultraviolet v0.0.0-20261007174847 (tangled.org/okami.mom/ultraviolet), with40-byte cells instead of 112-byte ones.
a cell was a 16-byte content string, a 56-byte Style holding three colorinterfaces, a 32-byte Link holding two strings, and the width. the screen is acell per column and row, so at 200x50 the screen is 10000 cells: 1.1MB whileonly a handful of styles appear on it, and bubbletea's frame buffer (sized tothe content, not the screen) was 3.1MB by itself.
a cell now holds its style and link by pointer, one record per distinct style:a parse interns one record per distinct style, a border shares one per side, and anil pointer is the zero style, so a cell with no style is still the zero Cell.cells are 40 bytes: the 200x50 screen is 400KB.
a parse interns the records it hands out, so a frame allocates one smallrecord per distinct style it uses (about ten for a chat frame, 1.2KB) where itallocated none before: that is the price of the 72 bytes it saves per cell.
what a renderer writes is unchanged. the pen in renderLine and inTerminalRenderer is still a value, and a zero style is still written as thereset it always was. cli/internal/terminal/ultraviolet_render_test.go renders ascripted sequence of frames through this build and compares the bytes withgoldens taken from the unpatched one. one thing to know when reading the code:`Cell == Cell` compares the style pointers, so compare cells with Cell.Equal;Style.IsZero and Link.IsZero take a nil pointer as the zero value.
to bump ultraviolet: copy the new version over third_party/ultraviolet, dropits tests and CI files, and apply this patch (patch -p1 -d third_party/ultraviolet< third_party/ultraviolet.patch). the fork's own tests need a `&` on any cellliteral that sets Style or Link; they were run that way against this diff.
--- a/border.go+++ b/border.go@@ -190,26 +190,33 @@ // Draw draws the border around the given component. func (b *Border) Draw(scr Screen, area Rectangle) {+ // A side is the same cell along its whole run, and a screen copies the+ // cells it is given, so one cell per side is enough.+ top, bottom := borderCell(scr, &b.Top), borderCell(scr, &b.Bottom)+ left, right := borderCell(scr, &b.Left), borderCell(scr, &b.Right)+ topLeft, topRight := borderCell(scr, &b.TopLeft), borderCell(scr, &b.TopRight)+ bottomLeft, bottomRight := borderCell(scr, &b.BottomLeft), borderCell(scr, &b.BottomRight)+ for y := area.Min.Y; y < area.Max.Y; y++ { for x := area.Min.X; x < area.Max.X; x++ { var cell *Cell switch { case y == area.Min.Y && x == area.Min.X:- cell = borderCell(scr, &b.TopLeft)+ cell = topLeft case y == area.Min.Y && x == area.Max.X-1:- cell = borderCell(scr, &b.TopRight)+ cell = topRight case y == area.Max.Y-1 && x == area.Min.X:- cell = borderCell(scr, &b.BottomLeft)+ cell = bottomLeft case y == area.Max.Y-1 && x == area.Max.X-1:- cell = borderCell(scr, &b.BottomRight)+ cell = bottomRight case y == area.Min.Y:- cell = borderCell(scr, &b.Top)+ cell = top case y == area.Max.Y-1:- cell = borderCell(scr, &b.Bottom)+ cell = bottom case x == area.Min.X:- cell = borderCell(scr, &b.Left)+ cell = left case x == area.Max.X-1:- cell = borderCell(scr, &b.Right)+ cell = right default: continue }@@ -224,8 +231,8 @@ func borderCell(scr Screen, b *Side) *Cell { c := NewCell(scr.WidthMethod(), b.Content) if c != nil {- c.Style = b.Style- c.Link = b.Link+ c.Style = StyleRef(b.Style)+ c.Link = LinkRef(b.Link) } return c }--- a/buffer.go+++ b/buffer.go@@ -164,24 +164,30 @@ pending.Reset() } - if c.Style.IsZero() && !pen.IsZero() {+ // A cell shares its style and link records with every cell that has+ // the same ones, so render from a copy: the pen is a value, and the+ // sequence written is the one a cell's own style and link always were.+ style := styleValue(c.Style)+ cellLink := linkValue(c.Link)++ if style.IsZero() && !pen.IsZero() { _, _ = buf.WriteString(ansi.ResetStyle) pen = Style{} }- if !c.Style.Equal(&pen) {- seq := c.Style.Diff(&pen)+ if !style.Equal(&pen) {+ seq := style.Diff(&pen) _, _ = buf.WriteString(seq)- pen = c.Style+ pen = style } // Write the URL escape sequence- if c.Link != link && link.URL != "" {+ if cellLink != link && link.URL != "" { _, _ = buf.WriteString(ansi.ResetHyperlink()) link = Link{} }- if c.Link != link {- _, _ = buf.WriteString(ansi.SetHyperlink(c.Link.URL, c.Link.Params))- link = c.Link+ if cellLink != link {+ _, _ = buf.WriteString(ansi.SetHyperlink(cellLink.URL, cellLink.Params))+ link = cellLink } _, _ = buf.WriteString(c.String())--- a/cell.go+++ b/cell.go@@ -12,23 +12,62 @@ var EmptyCell = Cell{Content: " ", Width: 1} // Cell represents a single cell in the terminal screen.+//+// A screen is a cell per column and row, so what a cell costs is what a screen+// costs. Style and Link are therefore shared records rather than values: every+// cell that has the same style points at the same one, and a nil pointer is+// the zero style or link, which is what most cells have. type Cell struct { // Content is the [Cell]'s content, which consists of a single grapheme // cluster. Most of the time, this will be a single rune as well, but it // can also be a combination of runes that form a grapheme cluster. Content string - // The style of the cell. Nil style means no style. Zero value prints a- // reset sequence.- Style Style+ // The style of the cell, shared with every cell that has the same one. A+ // nil style means no style. Zero value prints a reset sequence.+ Style *Style - // Link is the hyperlink of the cell.- Link Link+ // Link is the hyperlink of the cell, shared like [Cell.Style].+ Link *Link // Width is the mono-spaced width of the grapheme cluster. Width int } +// StyleRef returns the style record for a cell: nil for the zero style, so a+// cell with no style stays the zero [Cell], and a fresh record otherwise.+// Cells that have the same style can share one record.+func StyleRef(style Style) *Style {+ if style.IsZero() {+ return nil+ }+ return &style+}++// LinkRef returns the link record for a cell, like [StyleRef].+func LinkRef(link Link) *Link {+ if link.IsZero() {+ return nil+ }+ return &link+}++// styleValue is the style a cell holds, with a nil style as the zero one.+func styleValue(style *Style) Style {+ if style == nil {+ return Style{}+ }+ return *style+}++// linkValue is the link a cell holds, with a nil link as the zero one.+func linkValue(link *Link) Link {+ if link == nil {+ return Link{}+ }+ return *link+}+ // NewCell creates a new cell from the given string grapheme. It will only use // the first grapheme in the string and ignore the rest. The width of the cell // is determined using the given width method.@@ -56,13 +95,13 @@ return o != nil && c.Width == o.Width && c.Content == o.Content &&- c.Style.Equal(&o.Style) &&- c.Link.Equal(&o.Link)+ c.Style.Equal(o.Style) &&+ c.Link.Equal(o.Link) } // IsZero returns whether the cell is an empty cell. func (c *Cell) IsZero() bool {- return *c == Cell{}+ return c.Width == 0 && c.Content == "" && c.Style.IsZero() && c.Link.IsZero() } // isWidePlaceholder reports whether the cell is the continuation column of a@@ -101,17 +140,27 @@ // String returns a string representation of the hyperlink. func (h *Link) String() string {+ if h == nil {+ return ""+ } return h.URL } -// Equal returns whether the hyperlink is equal to the other hyperlink.+// Equal returns whether the hyperlink is equal to the other hyperlink. A nil+// link is the zero link. func (h *Link) Equal(o *Link) bool {- return o != nil && *h == *o+ if h == o {+ return true+ }+ if h == nil || o == nil {+ return h.IsZero() && o.IsZero()+ }+ return *h == *o } -// IsZero returns whether the hyperlink is empty.+// IsZero returns whether the hyperlink is empty. A nil link is empty. func (h *Link) IsZero() bool {- return *h == Link{}+ return h == nil || *h == Link{} } // These are the available text attributes that can be combined to create@@ -175,8 +224,12 @@ Attrs uint8 } -// Equal returns true if the style is equal to the other style.+// Equal returns true if the style is equal to the other style. A nil style is+// the zero style. func (s *Style) Equal(o *Style) bool {+ if s == nil || o == nil {+ return s.IsZero() && o.IsZero()+ } return s.Attrs == o.Attrs && s.Underline == o.Underline && colorEqual(s.Fg, o.Fg) &&@@ -425,9 +478,9 @@ return cr == or && cg == og && cb == ob && ca == oa } -// IsZero returns true if the style is empty.+// IsZero returns true if the style is empty. A nil style is empty. func (s *Style) IsZero() bool {- return *s == Style{}+ return s == nil || *s == Style{} } // ConvertStyle converts a style to respect the given color profile.--- a/screen/context.go+++ b/screen/context.go@@ -336,6 +336,8 @@ return x, y } + // The cells share one style and one link record for the whole draw.+ stylep, linkp := uv.StyleRef(style), uv.LinkRef(link) wm := scr.WidthMethod() for grs.Next() { gr := string(grs.Value())@@ -364,8 +366,8 @@ scr.SetCell(x, y, &uv.Cell{ Content: gr, Width: w,- Style: style,- Link: link,+ Style: stylep,+ Link: linkp, }) x += w--- a/styled.go+++ b/styled.go@@ -145,6 +145,48 @@ return n >= 2 && seq[n-1] == '\\' && seq[n-2] == ansi.ESC } +// cellAttrs shares the style and link records a parse hands out. A string+// repeats the same few styles across its cells, so a parse allocates one+// record per style rather than per cell, and a string with no escapes+// allocates none at all.+type cellAttrs struct {+ styles map[Style]*Style+ links map[Link]*Link+}++// style returns the shared record for style, creating it the first time this+// parse sees it.+func (a *cellAttrs) style(style Style) *Style {+ if style.IsZero() {+ return nil+ }+ if a.styles == nil {+ a.styles = make(map[Style]*Style)+ }+ if shared, ok := a.styles[style]; ok {+ return shared+ }+ shared := StyleRef(style)+ a.styles[style] = shared+ return shared+}++// link returns the shared record for link, like [cellAttrs.style].+func (a *cellAttrs) link(link Link) *Link {+ if link.IsZero() {+ return nil+ }+ if a.links == nil {+ a.links = make(map[Link]*Link)+ }+ if shared, ok := a.links[link]; ok {+ return shared+ }+ shared := LinkRef(link)+ a.links[link] = shared+ return shared+}+ func printString[T []byte | string]( s Screen, m WidthMethod,@@ -174,6 +216,9 @@ var cell Cell var style Style var link Link+ var attrs cellAttrs+ var stylep *Style+ var linkp *Link var state byte lastX, lastY := -1, -1 // last cell written, for folding in combining marks var pending []byte // pass-through sequences awaiting a cell to ride on@@ -205,8 +250,8 @@ cell.Content = string(pending) pending = pending[:0] }- cell.Style = style- cell.Link = link+ cell.Style = stylep+ cell.Link = linkp if s == nil { // Building lines: unwrapped, no bounds@@ -229,8 +274,8 @@ if truncate && tailc.Width > 0 && x+cell.Width > bounds.Max.X-tailc.Width { // Truncate the string and append the tail if any. cell = tailc- cell.Style = style- cell.Link = link+ cell.Style = stylep+ cell.Link = linkp s.SetCell(x, y, &cell) lastX, lastY = x, y x += tailc.Width@@ -252,9 +297,11 @@ case ansi.HasCsiPrefix(seq) && p.Command() == 'm': // SGR - Select Graphic Rendition ReadStyle(p.Params(), &style)+ stylep = attrs.style(style) case ansi.HasOscPrefix(seq) && p.Command() == 8: // Hyperlinks ReadLink(p.Data(), &link)+ linkp = attrs.link(link) case ansi.Equal(seq, T("\n")): if s == nil { // When building lines, we need to ensure empty lines are represented.--- a/terminal_renderer.go+++ b/terminal_renderer.go@@ -601,10 +601,10 @@ // Downsample pen when we don't have a [colorprofile.TrueColor], // otherwise, use the original style.- newStyle := ConvertStyle(cell.Style, s.profile)- newLink := ConvertLink(cell.Link, s.profile)- oldStyle := ConvertStyle(s.cur.Style, s.profile)- oldLink := ConvertLink(s.cur.Link, s.profile)+ newStyle := ConvertStyle(styleValue(cell.Style), s.profile)+ newLink := ConvertLink(linkValue(cell.Link), s.profile)+ oldStyle := ConvertStyle(styleValue(s.cur.Style), s.profile)+ oldLink := ConvertLink(linkValue(s.cur.Link), s.profile) if !newStyle.Equal(&oldStyle) { seq := newStyle.Diff(&oldStyle)@@ -633,8 +633,9 @@ // NOTE: This assumes that the terminal supports bce terminfo capability // which all xterm-compatible terminals and terminals that use xterm* // terminal types do.- return c.Style.Underline == UnderlineNone &&- c.Style.Attrs&^(AttrBold|AttrFaint|AttrItalic|AttrBlink|AttrRapidBlink) == 0 &&+ style := styleValue(c.Style)+ return style.Underline == UnderlineNone &&+ style.Attrs&^(AttrBold|AttrFaint|AttrItalic|AttrBlink|AttrRapidBlink) == 0 && c.Link.IsZero() } @@ -1713,7 +1714,7 @@ cell := newbuf.CellAt(fx+i, ty) if cell != nil && cell.Width > 0 { i += cell.Width - 1- if !cell.Style.Equal(&s.cur.Style) || !cell.Link.Equal(&s.cur.Link) {+ if !cell.Style.Equal(s.cur.Style) || !cell.Link.Equal(s.cur.Link) { overwrite = false break }