Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cellbuf new acurses renderer #299

Merged
merged 44 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
5a97c01
feat(cellbuf): define window/screen types and implement efficient cur…
aymanbagabas Nov 29, 2024
ce6b817
feat(cellbuf): efficient screen rendering
aymanbagabas Nov 29, 2024
90991c7
fix(cellbuf): cursor movement optimization
aymanbagabas Nov 29, 2024
66fb974
fix(cellbuf): hyperlink support
aymanbagabas Nov 29, 2024
30c28fa
feat(cellbuf): optimize cursor movement using LF and RI
aymanbagabas Nov 29, 2024
491bf3d
fix(cellbuf): combining runes and ansi.REP optimization handling
aymanbagabas Nov 30, 2024
6524069
refactor(cellbuf): use rune and combining runes for cell content
aymanbagabas Nov 30, 2024
001fc77
feat(cellbuf): clean up and implement closer
aymanbagabas Nov 30, 2024
9b1d29f
fix(cellbuf): off-by-one error in window resizing
aymanbagabas Dec 3, 2024
af7a4b7
refactor(cellbuf): make buffer line access safer
aymanbagabas Dec 3, 2024
a9c4ef2
feat(cellbuf): export Cursor type
aymanbagabas Dec 4, 2024
b788605
feat(cellbuf): add window interface and subwindow type
aymanbagabas Dec 4, 2024
436f5c3
fix(cellbuf): compare overwrite cursor movement with existing one
aymanbagabas Dec 4, 2024
997735d
feat(cellbuf): screen relative cursor movements
aymanbagabas Dec 4, 2024
0ef84fd
fix(cellbuf): tidy and guard against out-of-bounds access
aymanbagabas Dec 4, 2024
7da7ab2
feat(cellbuf): add String method to Buffer
aymanbagabas Dec 4, 2024
42564e2
feat(cellbuf): add alternate screen buffer mode support
aymanbagabas Dec 4, 2024
6ba7bc9
feat(cellbuf): add xterm-like optimizations
aymanbagabas Dec 4, 2024
be55c27
feat(cellbuf): support color profiles
aymanbagabas Dec 4, 2024
e3de813
fix(cellbuf): remove debug log
aymanbagabas Dec 4, 2024
65af016
fix(cellbuf): map locking, rep char count, and divide by zero
aymanbagabas Dec 4, 2024
121bc95
fix(cellbuf): remove extraneous DCH and guard transform line copy
aymanbagabas Dec 4, 2024
6f3276a
fix(cellbuf): scroll when necessary in inline mode and ensure we copy
aymanbagabas Dec 5, 2024
aa86a09
fix(cellbuf): tidy up and don't render if no changes
aymanbagabas Dec 5, 2024
a4addb3
fix(cellbuf): only support ASCII in emitRange repeat characters
aymanbagabas Dec 5, 2024
7e99e6c
fix(cellbuf): standout glitch using the wrong blank cell
aymanbagabas Dec 5, 2024
30386fa
feat(cellbuf): support altscreen and cursor visibility
aymanbagabas Dec 5, 2024
d4567a2
fix(cellbuf): cache screen width and height after resize
aymanbagabas Dec 5, 2024
22cb4af
feat(cellbuf): add Screen.InsertAbove method
aymanbagabas Dec 5, 2024
4b38b35
fix(cellbuf): guard InsertAbove with mutex
aymanbagabas Dec 5, 2024
3d818a7
fix(cellbuf): mark screen as ready to clear on Clear()
aymanbagabas Dec 5, 2024
35e781a
fix(cellbuf): ensure cursor is hidden while rendering
aymanbagabas Dec 5, 2024
5d7933a
fix(cellbuf): do not insert above when alternate screen mode is enabled
aymanbagabas Dec 5, 2024
6588069
refactor(cellbuf): change Cell.Content to Cell.String
aymanbagabas Dec 5, 2024
5b2105a
fix(cellbuf): touched cells calculation
aymanbagabas Dec 5, 2024
3dbda97
fix(cellbuf): clear the rest of the lines after painting a new frame
aymanbagabas Dec 5, 2024
21006f5
fix(cellbuf): writing to wide cell placeholders
aymanbagabas Dec 9, 2024
839d5f9
fix(cellbuf): clear the rest of the line within the rect
aymanbagabas Dec 9, 2024
96bc4bb
refactor(cellbuf): rename Paint to PaintRect
aymanbagabas Dec 9, 2024
6874aa7
feat(examples): add cellbuf examples
aymanbagabas Dec 9, 2024
b5aefa9
fix(cellbuf): clear the last line when writing to the screen
aymanbagabas Dec 9, 2024
df67bb2
refactor(cellbuf): simplify Screen methods with io.Writer
aymanbagabas Dec 9, 2024
88fec18
feat(cellbuf): add MoveTo method
aymanbagabas Dec 9, 2024
a2181d9
chore(examples): go mod tidy
aymanbagabas Dec 9, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
496 changes: 382 additions & 114 deletions cellbuf/buffer.go

Large diffs are not rendered by default.

518 changes: 509 additions & 9 deletions cellbuf/cell.go

Large diffs are not rendered by default.

39 changes: 34 additions & 5 deletions cellbuf/geom.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,50 @@
package cellbuf

import (
"github.com/charmbracelet/x/vt"
"image"
)

// Position represents an x, y position.
type Position = vt.Position
type Position = image.Point

// Pos is a shorthand for Position{X: x, Y: y}.
func Pos(x, y int) Position {
return vt.Pos(x, y)
return image.Pt(x, y)
}

// Rectange represents a rectangle.
type Rectangle = vt.Rectangle
type Rectangle struct {
image.Rectangle
}

// Contains reports whether the rectangle contains the given point.
func (r Rectangle) Contains(p Position) bool {
return p.In(r.Bounds())
}

// Width returns the width of the rectangle.
func (r Rectangle) Width() int {
return r.Rectangle.Dx()
}

// Height returns the height of the rectangle.
func (r Rectangle) Height() int {
return r.Rectangle.Dy()
}

// X returns the starting x position of the rectangle.
// This is equivalent to Min.X.
func (r Rectangle) X() int {
return r.Min.X
}

// Y returns the starting y position of the rectangle.
// This is equivalent to Min.Y.
func (r Rectangle) Y() int {
return r.Min.Y
}

// Rect is a shorthand for Rectangle.
func Rect(x, y, w, h int) Rectangle {
return vt.Rect(x, y, w, h)
return Rectangle{image.Rect(x, y, x+w, y+h)}
}
5 changes: 2 additions & 3 deletions cellbuf/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@ go 1.18
require (
github.com/charmbracelet/colorprofile v0.1.9
github.com/charmbracelet/x/ansi v0.5.2
github.com/charmbracelet/x/vt v0.0.0-20241113152101-0af7d04e9f32
github.com/charmbracelet/x/term v0.2.1
github.com/charmbracelet/x/wcwidth v0.0.0-20241011142426-46044092ad91
github.com/rivo/uniseg v0.4.7
)

require (
github.com/charmbracelet/x/term v0.2.1 // 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.27.0 // indirect
golang.org/x/text v0.20.0 // indirect
Expand Down
2 changes: 0 additions & 2 deletions cellbuf/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ github.com/charmbracelet/x/ansi v0.5.2 h1:dEa1x2qdOZXD/6439s+wF7xjV+kZLu/iN00GuX
github.com/charmbracelet/x/ansi v0.5.2/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/charmbracelet/x/vt v0.0.0-20241113152101-0af7d04e9f32 h1:F6G/LwhlSj/oQgnNKkELI934e/oao0MM67rst7MExDY=
github.com/charmbracelet/x/vt v0.0.0-20241113152101-0af7d04e9f32/go.mod h1:+CYC0tzYqYMtIryA0lcGQgCUaAiRLaS7Rxi9R+PFii8=
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=
Expand Down
4 changes: 0 additions & 4 deletions cellbuf/link.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,8 @@ package cellbuf

import (
"github.com/charmbracelet/colorprofile"
"github.com/charmbracelet/x/vt"
)

// Link represents a hyperlink in the terminal screen.
type Link = vt.Link

// Convert converts a hyperlink to respect the given color profile.
func ConvertLink(h Link, p colorprofile.Profile) Link {
if p == colorprofile.NoTTY {
Expand Down
238 changes: 238 additions & 0 deletions cellbuf/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
package cellbuf

import (
"bytes"

"github.com/charmbracelet/colorprofile"
"github.com/charmbracelet/x/ansi"
"github.com/charmbracelet/x/wcwidth"
)

// Options are options for manipulating the buffer.
type Options struct {
// Parser is the parser to use when writing to the buffer.
Parser *ansi.Parser
// Area is the area to write to.
Area Rectangle
// Profile is the profile to use when writing to the buffer.
Profile colorprofile.Profile
// Method is the width calculation method to use when writing to the buffer.
Method Method
// AutoWrap is whether to automatically wrap text when it reaches the end
// of the line.
AutoWrap bool
// NewLine whether to automatically insert a carriage returns [ansi.CR]
// when a linefeed [ansi.LF] is encountered.
NewLine bool
}

// SetString sets the string at the given x, y position. It returns the new x
// and y position after writing the string. If the string is wider than the
// buffer and auto-wrap is enabled, it will wrap to the next line. Otherwise,
// it will be truncated.
func (opt Options) SetString(b *Buffer, x, y int, s string) (int, int) {
if opt.Area.Empty() {
opt.Area = b.Bounds()
}

p := opt.Parser
if p == nil {
p = ansi.GetParser()
defer ansi.PutParser(p)
}

var pen Style
var link Link
var state byte

handleCell := func(content string, width int) {
var r rune
var comb []rune
for i, c := range content {
if i == 0 {
r = c
} else {
comb = append(comb, c)
}
}

c := &Cell{
Rune: r,
Comb: comb,
Width: width,
Style: pen,
Link: link,
}

b.SetCell(x, y, c)
if x+width >= opt.Area.Max.X && opt.AutoWrap {
x = opt.Area.Min.X
y++
} else {
x += width
}
}

blankCell := func() *Cell {
if pen.Bg != nil {
return &Cell{
Rune: ' ',
Width: 1,
Style: pen,
}
}
return nil
}

for len(s) > 0 {
seq, width, n, newState := ansi.DecodeSequence(s, state, p)
switch width {
case 1, 2, 3, 4: // wide cells can go up to 4 cells wide
switch opt.Method {
case WcWidth:
for _, r := range seq {
width = wcwidth.RuneWidth(r)
handleCell(string(r), width)
}
case GraphemeWidth:
handleCell(seq, width)
}
case 0:
switch {
case ansi.HasCsiPrefix(seq):
switch p.Cmd() {
case 'm': // Select Graphic Rendition [ansi.SGR]
handleSgr(p, &pen)
case 'L': // Insert Line [ansi.IL]
count := 1
if n, ok := p.Param(0, 1); ok && n > 0 {
count = n
}

b.InsertLine(y, count, blankCell())
case 'M': // Delete Line [ansi.DL]
count := 1
if n, ok := p.Param(0, 1); ok && n > 0 {
count = n
}

b.DeleteLine(y, count, blankCell())
}
case ansi.HasOscPrefix(seq):
switch p.Cmd() {
case 8: // Hyperlinks
handleHyperlinks(p, &link)
}
case ansi.HasEscPrefix(seq):
switch p.Cmd() {
case 'M': // Reverse Index [ansi.RI]
// Move the cursor up one line in the same column. If the
// cursor is at the top margin, the screen performs a scroll-up.
if y > opt.Area.Min.Y {
y--
}
}
case seq == "\n", seq == "\v", seq == "\f":
if opt.NewLine {
x = opt.Area.Min.X
}
y++
case seq == "\r":
x = opt.Area.Min.X
}
default:
// Should never happen
panic("invalid cell width")
}

s = s[n:]
state = newState
}

return x, y
}

// Render renders the buffer to a string.
func (opt Options) Render(b *Buffer) string {
var buf bytes.Buffer
height := b.Height()
for y := 0; y < height; y++ {
_, line := renderLine(b, y, opt)
buf.WriteString(line)
if y < height-1 {
buf.WriteString("\r\n")
}
}
return buf.String()
}

// RenderLine renders a single line of the buffer to a string.
// It returns the width of the line and the rendered string.
func (opt Options) RenderLine(b *Buffer, y int) (int, string) {
return renderLine(b, y, opt)
}

// Span represents a span of cells with the same style and link.
type Span struct {
// Segment is the content of the span.
Segment
// Position is the starting position of the span.
Position
}

// Diff computes the diff between two buffers as a slice of affected cells. It
// only returns affected cells withing the given rectangle.
func (opt Options) Diff(b, prev *Buffer) (diff []Span) {
if prev == nil {
return nil
}

area := opt.Area
if area.Empty() {
area = b.Bounds()
}

for y := area.Min.Y; y < area.Max.Y; y++ {
var span *Span
for x := area.Min.X; x < area.Max.X; x++ {
cellA := b.Cell(x, y)
cellB := prev.Cell(x, y)

if cellA.Equal(cellB) {
continue
}

if cellB == nil {
cellB = &BlankCell
}

if span == nil {
span = &Span{
Position: Pos(x, y),
Segment: cellB.Segment(),
}
continue
}

if span.X+span.Width == x &&
span.Style.Equal(cellB.Style) &&
span.Link == cellB.Link {
span.Content += cellB.String()
span.Width += cellB.Width
continue
}

diff = append(diff, *span)
span = &Span{
Position: Pos(x, y),
Segment: cellB.Segment(),
}
}

if span != nil {
diff = append(diff, *span)
}
}

return
}
Loading
Loading