From b95be54187d3798470ffe0dcfd492ba6cf629c87 Mon Sep 17 00:00:00 2001 From: Tai Groot Date: Wed, 4 Mar 2026 02:08:41 -0500 Subject: [PATCH 1/4] add damage tracking to vt --- vt/csi_mode.go | 2 +- vt/screen.go | 54 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/vt/csi_mode.go b/vt/csi_mode.go index 4402905a..65387779 100644 --- a/vt/csi_mode.go +++ b/vt/csi_mode.go @@ -44,7 +44,7 @@ func (e *Emulator) setAltScreenMode(on bool) { e.scr = &e.scrs[1] e.scrs[1].cur = e.scrs[0].cur e.scr.Clear() - e.scr.buf.Touched = nil + e.scr.ClearTouched() e.setCursor(0, 0) } else { e.scr = &e.scrs[0] diff --git a/vt/screen.go b/vt/screen.go index a2eae20b..bf2fb101 100644 --- a/vt/screen.go +++ b/vt/screen.go @@ -15,6 +15,8 @@ type Screen struct { cur, saved Cursor // scroll is the scroll region. scroll uv.Rectangle + // touched tracks which lines have been modified. + touched []*uv.LineData } // NewScreen creates a new screen. @@ -32,6 +34,7 @@ func (s *Screen) Reset() { s.cur = Cursor{} s.saved = Cursor{} s.scroll = s.buf.Bounds() + s.touchAll() } // Bounds returns the bounds of the screen. @@ -41,7 +44,12 @@ func (s *Screen) Bounds() uv.Rectangle { // Touched returns touched lines in the screen buffer. func (s *Screen) Touched() []*uv.LineData { - return s.buf.Touched + return s.touched +} + +// ClearTouched clears the touched state. +func (s *Screen) ClearTouched() { + s.touched = make([]*uv.LineData, s.buf.Height()) } // CellAt returns the cell at the given x, y position. @@ -52,6 +60,7 @@ func (s *Screen) CellAt(x int, y int) *uv.Cell { // SetCell sets the cell at the given x, y position. func (s *Screen) SetCell(x, y int, c *uv.Cell) { s.buf.SetCell(x, y, c) + s.touchLine(x, y, 1) } // Height returns the height of the screen. @@ -63,6 +72,8 @@ func (s *Screen) Height() int { func (s *Screen) Resize(width int, height int) { s.buf.Resize(width, height) s.scroll = s.buf.Bounds() + s.touched = make([]*uv.LineData, height) + s.touchAll() } // Width returns the width of the screen. @@ -78,6 +89,7 @@ func (s *Screen) Clear() { // ClearArea clears the given area. func (s *Screen) ClearArea(area uv.Rectangle) { s.buf.ClearArea(area) + s.touchArea(area) } // Fill fills the screen or part of it. @@ -88,6 +100,7 @@ func (s *Screen) Fill(c *uv.Cell) { // FillArea fills the given area with the given cell. func (s *Screen) FillArea(c *uv.Cell, area uv.Rectangle) { s.buf.FillArea(c, area) + s.touchArea(area) } // setHorizontalMargins sets the horizontal margins. @@ -237,6 +250,7 @@ func (s *Screen) InsertCell(n int) { x, y := s.cur.X, s.cur.Y s.buf.InsertCellArea(x, y, n, s.blankCell(), s.scroll) + s.touchLine(x, y, s.scroll.Max.X-x) } // DeleteCell deletes n cells at the cursor position moving cells to the left. @@ -248,6 +262,7 @@ func (s *Screen) DeleteCell(n int) { x, y := s.cur.X, s.cur.Y s.buf.DeleteCellArea(x, y, n, s.blankCell(), s.scroll) + s.touchLine(x, y, s.scroll.Max.X-x) } // ScrollUp scrolls the content up n lines within the given region. Lines @@ -288,6 +303,7 @@ func (s *Screen) InsertLine(n int) bool { } s.buf.InsertLineArea(y, n, s.blankCell(), s.scroll) + s.touchArea(uv.Rect(s.scroll.Min.X, y, s.scroll.Max.X, s.scroll.Max.Y)) return true } @@ -311,6 +327,7 @@ func (s *Screen) DeleteLine(n int) bool { } s.buf.DeleteLineArea(y, n, s.blankCell(), scroll) + s.touchArea(uv.Rect(scroll.Min.X, y, scroll.Max.X, scroll.Max.Y)) return true } @@ -327,3 +344,38 @@ func (s *Screen) blankCell() *uv.Cell { c.Style.Bg = s.cur.Pen.Bg return &c } + +// touchLine marks a line as touched at the given x position for n cells. +func (s *Screen) touchLine(x, y, n int) { + if y < 0 || y >= len(s.touched) { + return + } + if s.touched[y] == nil { + s.touched[y] = &uv.LineData{FirstCell: x, LastCell: x + n} + return + } + if x < s.touched[y].FirstCell { + s.touched[y].FirstCell = x + } + if x+n > s.touched[y].LastCell { + s.touched[y].LastCell = x + n + } +} + +// touchAll marks all lines as touched. +func (s *Screen) touchAll() { + w := s.buf.Width() + for i := range s.touched { + s.touched[i] = &uv.LineData{FirstCell: 0, LastCell: w} + } +} + +// touchArea marks all lines in the given area as touched. +func (s *Screen) touchArea(area uv.Rectangle) { + for y := area.Min.Y; y < area.Max.Y && y < len(s.touched); y++ { + if y < 0 { + continue + } + s.touchLine(area.Min.X, y, area.Max.X-area.Min.X) + } +} From e5f03149441b26dbe61164f5a93969307fb66442 Mon Sep 17 00:00:00 2001 From: Tai Groot Date: Wed, 4 Mar 2026 03:23:50 -0500 Subject: [PATCH 2/4] use RenderBuffer --- vt/screen.go | 57 ++++++++++++++++------------------------------------ 1 file changed, 17 insertions(+), 40 deletions(-) diff --git a/vt/screen.go b/vt/screen.go index bf2fb101..0199984c 100644 --- a/vt/screen.go +++ b/vt/screen.go @@ -10,19 +10,20 @@ type Screen struct { // cb is the callbacks struct to use. cb *Callbacks // The buffer of the screen. - buf uv.Buffer + buf *uv.RenderBuffer // The cur of the screen. cur, saved Cursor // scroll is the scroll region. scroll uv.Rectangle - // touched tracks which lines have been modified. - touched []*uv.LineData } // NewScreen creates a new screen. func NewScreen(w, h int) *Screen { - s := Screen{} - s.Resize(w, h) + s := Screen{ + buf: uv.NewRenderBuffer(w, h), + } + s.scroll = s.buf.Bounds() + s.touchAll() return &s } @@ -44,12 +45,12 @@ func (s *Screen) Bounds() uv.Rectangle { // Touched returns touched lines in the screen buffer. func (s *Screen) Touched() []*uv.LineData { - return s.touched + return s.buf.Touched } // ClearTouched clears the touched state. func (s *Screen) ClearTouched() { - s.touched = make([]*uv.LineData, s.buf.Height()) + s.buf.Touched = make([]*uv.LineData, s.buf.Height()) } // CellAt returns the cell at the given x, y position. @@ -60,7 +61,6 @@ func (s *Screen) CellAt(x int, y int) *uv.Cell { // SetCell sets the cell at the given x, y position. func (s *Screen) SetCell(x, y int, c *uv.Cell) { s.buf.SetCell(x, y, c) - s.touchLine(x, y, 1) } // Height returns the height of the screen. @@ -70,9 +70,13 @@ func (s *Screen) Height() int { // Resize resizes the screen. func (s *Screen) Resize(width int, height int) { - s.buf.Resize(width, height) + if s.buf == nil { + s.buf = uv.NewRenderBuffer(width, height) + } else { + s.buf.Resize(width, height) + s.buf.Touched = make([]*uv.LineData, height) + } s.scroll = s.buf.Bounds() - s.touched = make([]*uv.LineData, height) s.touchAll() } @@ -250,7 +254,6 @@ func (s *Screen) InsertCell(n int) { x, y := s.cur.X, s.cur.Y s.buf.InsertCellArea(x, y, n, s.blankCell(), s.scroll) - s.touchLine(x, y, s.scroll.Max.X-x) } // DeleteCell deletes n cells at the cursor position moving cells to the left. @@ -262,7 +265,6 @@ func (s *Screen) DeleteCell(n int) { x, y := s.cur.X, s.cur.Y s.buf.DeleteCellArea(x, y, n, s.blankCell(), s.scroll) - s.touchLine(x, y, s.scroll.Max.X-x) } // ScrollUp scrolls the content up n lines within the given region. Lines @@ -303,7 +305,6 @@ func (s *Screen) InsertLine(n int) bool { } s.buf.InsertLineArea(y, n, s.blankCell(), s.scroll) - s.touchArea(uv.Rect(s.scroll.Min.X, y, s.scroll.Max.X, s.scroll.Max.Y)) return true } @@ -327,7 +328,6 @@ func (s *Screen) DeleteLine(n int) bool { } s.buf.DeleteLineArea(y, n, s.blankCell(), scroll) - s.touchArea(uv.Rect(scroll.Min.X, y, scroll.Max.X, scroll.Max.Y)) return true } @@ -345,37 +345,14 @@ func (s *Screen) blankCell() *uv.Cell { return &c } -// touchLine marks a line as touched at the given x position for n cells. -func (s *Screen) touchLine(x, y, n int) { - if y < 0 || y >= len(s.touched) { - return - } - if s.touched[y] == nil { - s.touched[y] = &uv.LineData{FirstCell: x, LastCell: x + n} - return - } - if x < s.touched[y].FirstCell { - s.touched[y].FirstCell = x - } - if x+n > s.touched[y].LastCell { - s.touched[y].LastCell = x + n - } -} - // touchAll marks all lines as touched. func (s *Screen) touchAll() { - w := s.buf.Width() - for i := range s.touched { - s.touched[i] = &uv.LineData{FirstCell: 0, LastCell: w} - } + s.touchArea(s.buf.Bounds()) } // touchArea marks all lines in the given area as touched. func (s *Screen) touchArea(area uv.Rectangle) { - for y := area.Min.Y; y < area.Max.Y && y < len(s.touched); y++ { - if y < 0 { - continue - } - s.touchLine(area.Min.X, y, area.Max.X-area.Min.X) + for y := area.Min.Y; y < area.Max.Y; y++ { + s.buf.TouchLine(area.Min.X, y, area.Max.X-area.Min.X) } } From 47b8a1dd2cb28005c345b4e035062d60a70831a0 Mon Sep 17 00:00:00 2001 From: Tai Groot Date: Wed, 4 Mar 2026 03:26:52 -0500 Subject: [PATCH 3/4] bump ultraviolet --- vt/go.mod | 10 +++++----- vt/go.sum | 10 ++++++++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/vt/go.mod b/vt/go.mod index 5f389817..7e370eb4 100644 --- a/vt/go.mod +++ b/vt/go.mod @@ -3,25 +3,25 @@ module github.com/charmbracelet/x/vt go 1.24.2 require ( - github.com/charmbracelet/ultraviolet v0.0.0-20251106193841-7889546fc720 + github.com/charmbracelet/ultraviolet v0.0.0-20260303162955-0b88c25f3fff github.com/charmbracelet/x/ansi v0.11.6 github.com/charmbracelet/x/exp/ordered v0.1.0 ) require ( - github.com/charmbracelet/colorprofile v0.3.3 // indirect + github.com/charmbracelet/colorprofile v0.4.2 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect github.com/charmbracelet/x/termios v0.1.1 // indirect github.com/charmbracelet/x/windows v0.2.2 // indirect github.com/clipperhouse/displaywidth v0.9.0 // indirect github.com/clipperhouse/stringish v0.1.1 // indirect - github.com/clipperhouse/uax29/v2 v2.5.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-runewidth v0.0.19 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect - golang.org/x/sync v0.17.0 // indirect - golang.org/x/sys v0.37.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.41.0 // indirect ) diff --git a/vt/go.sum b/vt/go.sum index b4174ba8..92af3a0e 100644 --- a/vt/go.sum +++ b/vt/go.sum @@ -1,7 +1,11 @@ github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI= github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4= +github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY= +github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8= github.com/charmbracelet/ultraviolet v0.0.0-20251106193841-7889546fc720 h1:Pny/vp+ySKst82CWEME1oP6YEFs/17tlH+QOjqW7VUY= github.com/charmbracelet/ultraviolet v0.0.0-20251106193841-7889546fc720/go.mod h1:Y8B4DzWeTb0ama8l3+KyopZtkE8fZjwRQ3aEAPEXHE0= +github.com/charmbracelet/ultraviolet v0.0.0-20260303162955-0b88c25f3fff h1:uY7A6hTokHPJBHfq7rj9Y/wm+IAjOghZTxKfVW6QLvw= +github.com/charmbracelet/ultraviolet v0.0.0-20260303162955-0b88c25f3fff/go.mod h1:E6/0abq9uG2SnM8IbLB9Y5SW09uIgfaFETk8aRzgXUQ= github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= github.com/charmbracelet/x/exp/ordered v0.1.0 h1:55/qLwjIh0gL0Vni+QAWk7T/qRVP6sBf+2agPBgnOFE= @@ -18,6 +22,8 @@ github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfa github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U= github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= @@ -32,5 +38,9 @@ golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= From 742b12b7fcafe427eab39a77d384cac367c07755 Mon Sep 17 00:00:00 2001 From: Tai Groot Date: Wed, 4 Mar 2026 03:37:33 -0500 Subject: [PATCH 4/4] fixup: apply suggestions from ayman --- vt/screen.go | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/vt/screen.go b/vt/screen.go index 0199984c..a4efe92c 100644 --- a/vt/screen.go +++ b/vt/screen.go @@ -23,7 +23,6 @@ func NewScreen(w, h int) *Screen { buf: uv.NewRenderBuffer(w, h), } s.scroll = s.buf.Bounds() - s.touchAll() return &s } @@ -35,7 +34,7 @@ func (s *Screen) Reset() { s.cur = Cursor{} s.saved = Cursor{} s.scroll = s.buf.Bounds() - s.touchAll() + s.buf.Touched = nil } // Bounds returns the bounds of the screen. @@ -50,7 +49,7 @@ func (s *Screen) Touched() []*uv.LineData { // ClearTouched clears the touched state. func (s *Screen) ClearTouched() { - s.buf.Touched = make([]*uv.LineData, s.buf.Height()) + s.buf.Touched = nil } // CellAt returns the cell at the given x, y position. @@ -74,10 +73,9 @@ func (s *Screen) Resize(width int, height int) { s.buf = uv.NewRenderBuffer(width, height) } else { s.buf.Resize(width, height) - s.buf.Touched = make([]*uv.LineData, height) + s.buf.Touched = nil } s.scroll = s.buf.Bounds() - s.touchAll() } // Width returns the width of the screen. @@ -345,11 +343,6 @@ func (s *Screen) blankCell() *uv.Cell { return &c } -// touchAll marks all lines as touched. -func (s *Screen) touchAll() { - s.touchArea(s.buf.Bounds()) -} - // touchArea marks all lines in the given area as touched. func (s *Screen) touchArea(area uv.Rectangle) { for y := area.Min.Y; y < area.Max.Y; y++ {