Skip to content

Commit

Permalink
chore: merge pull request #299 from charmbracelet/cellbuf/window2
Browse files Browse the repository at this point in the history
Cellbuf new acurses renderer
  • Loading branch information
aymanbagabas authored Dec 9, 2024
2 parents 10120d0 + a2181d9 commit cdc294c
Show file tree
Hide file tree
Showing 16 changed files with 3,206 additions and 345 deletions.
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

0 comments on commit cdc294c

Please sign in to comment.