Skip to content

Commit

Permalink
feat: add cellbuf package
Browse files Browse the repository at this point in the history
  • Loading branch information
aymanbagabas committed Sep 11, 2024
1 parent 5807114 commit cae1634
Show file tree
Hide file tree
Showing 15 changed files with 1,230 additions and 0 deletions.
9 changes: 9 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ updates:
commit-message:
prefix: "chore"
include: "scope"
- package-ecosystem: "gomod"
directory: "/cellbuf"
schedule:
interval: "daily"
labels:
- "dependencies"
commit-message:
prefix: "chore"
include: "scope"
- package-ecosystem: "gomod"
directory: "/colors"
schedule:
Expand Down
30 changes: 30 additions & 0 deletions .github/workflows/cellbuf.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# auto-generated by scripts/dependabot. DO NOT EDIT.
name: cellbuf

on:
push:
branches:
- main
pull_request:
paths:
- cellbuf/**
- .github/workflows/cellbuf.yml

jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
defaults:
run:
working-directory: ./cellbuf
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: ./cellbuf/go.mod
cache: true
cache-dependency-path: ./cellbuf/go.sum
- run: go build -v ./...
- run: go test -race -v ./...
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ into other repositories.
Currently the following packages are available:

- [`ansi`](./ansi): ANSI escape sequence parser and definitions • [Docs](https://pkg.go.dev/github.com/charmbracelet/x/ansi)
- [`cellbuf`](./cellbuf): Cell-based terminal display parser • [Docs](https://pkg.go.dev/github.com/charmbracelet/x/cellbuf)
- [`conpty`](./conpty): Windows Console Pseudo-terminal library • [Docs](https://pkg.go.dev/github.com/charmbracelet/x/conpty)
- [`editor`](./editor): open files in text editors • [Docs](https://pkg.go.dev/github.com/charmbracelet/x/editor)
- [`errors`](./errors): `errors.Join` in older Go versions • [Docs](https://pkg.go.dev/github.com/charmbracelet/x/errors)
Expand Down
155 changes: 155 additions & 0 deletions cellbuf/buffer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package cellbuf

import (
"errors"
)

// Buffer is a 2D grid of cells representing a screen or terminal.
type Buffer struct {
cells []Cell
width, height int
method WidthMethod // Defaults to WcWidth
}

// NewBuffer creates a new Buffer with the given width and height.
func NewBuffer(width, height int) *Buffer {
cells := make([]Cell, width*height)
b := &Buffer{cells: cells, width: width, height: height}
b.Fill(spaceCell)
return b
}

// SetWidthMethod sets the display width calculation method.
func (b *Buffer) SetWidthMethod(method WidthMethod) {
b.method = method
}

// Reset resets the buffer to the default state.
//
// This is a syntactic sugar for Fill(spaceCell).
func (b *Buffer) Reset() {
b.Fill(spaceCell)
}

// Size returns the width and height of the buffer.
func (b *Buffer) Size() (width, height int) {
return b.width, b.height
}

// Resize resizes the buffer to the given width and height.
func (b *Buffer) Resize(width, height int) {
if width == b.width && height == b.height {
return
}

// Truncate or extend the buffer
area := width * height
if area > len(b.cells) {
newcells := make([]Cell, area-len(b.cells))
for i := range newcells {
newcells[i] = spaceCell
}
b.cells = append(b.cells, newcells...)
}

b.width, b.height = width, height
}

// Free frees extra memory used by the buffer.
func (b *Buffer) Free() {
area := b.width * b.height
if area < len(b.cells) {
b.cells = b.cells[:area]
}
}

// IsClear returns true if the buffer is empty with only space cells.
func (b *Buffer) IsClear() bool {
for j := 0; j < b.height; j++ {
for i := 0; i < b.width; i++ {
if c, err := b.At(i, j); err == nil && !c.Equal(spaceCell) {
return false
}
}
}
return true
}

// ErrOutOfBounds is returned when the given x, y position is out of bounds.
var ErrOutOfBounds = errors.New("out of bounds")

// At returns the cell at the given x, y position.
func (b *Buffer) At(x, y int) (Cell, error) {
if x < 0 || x >= b.width || y < 0 || y >= b.height {
return Cell{}, ErrOutOfBounds
}
idx := y*b.width + x
if idx < 0 || idx >= len(b.cells) {
return Cell{}, ErrOutOfBounds
}
return b.cells[idx], nil
}

// Fill fills the buffer with the given style and rune.
func (b *Buffer) Fill(c Cell) {
for j := 0; j < b.height; j++ {
for i := 0; i < b.width; i++ {
b.Set(i, j, c) //nolint:errcheck
}
}
}

// FillFunc fills the buffer with the given function.
func (b *Buffer) FillFunc(f func(c Cell) Cell) {
for j := 0; j < b.height; j++ {
for i := 0; i < b.width; i++ {
idx := j*b.width + i
if c, err := b.At(i, j); err == nil {
b.cells[idx] = f(c)
}
}
}
}

// FillRange fills the buffer with the given Cell in the given range.
func (b *Buffer) FillRange(x, y, w, h int, c Cell) {
for i := y; i < y+h; i++ {
for j := x; j < x+w; j++ {
b.Set(j, i, c) //nolint:errcheck
}
}
}

// FillRangeFunc fills the buffer with the given function in the given range.
func (b *Buffer) FillRangeFunc(x, y, w, h int, f func(c Cell) Cell) {
for j := y; j < y+h; j++ {
for i := x; i < x+w; i++ {
b.SetFunc(i, j, f) //nolint:errcheck
}
}
}

// Set sets the cell at the given x, y position.
func (b *Buffer) Set(x, y int, c Cell) error {
if x > b.width-1 || y > b.height-1 {
return ErrOutOfBounds
}
idx := y*b.width + x
if idx < 0 || idx >= len(b.cells) {
return ErrOutOfBounds
}

b.cells[idx] = c

return nil
}

// SetFunc sets the cell at the given x, y position using a function.
func (b *Buffer) SetFunc(x, y int, f func(c Cell) Cell) error {
c, err := b.At(x, y)
if err != nil {
return err
}
c = f(c)
return b.Set(x, y, c)
}
44 changes: 44 additions & 0 deletions cellbuf/buffer_string.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package cellbuf

import (
"bytes"

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

// Render returns a string representation of the buffer with ANSI escape
// sequences. Use [ansi.Strip] to remove them.
func (b *Buffer) Render() string {
var pen Style
var link Hyperlink
var buf bytes.Buffer
for y := 0; y < b.height; y++ {
for x := 0; x < b.width; x++ {
if cell, err := b.At(x, y); err == nil && cell.Width > 0 {
if cell.Style.IsEmpty() && !pen.IsEmpty() {
buf.WriteString(ansi.ResetStyle) //nolint:errcheck
pen.Reset()
}
if !cell.Style.Equal(pen) {
seq := cell.Style.DiffSequence(pen)
buf.WriteString(seq) // nolint:errcheck
pen = cell.Style
}

// Write the URL escape sequence
if cell.Link != link && link.URL != "" {
buf.WriteString(ansi.ResetHyperlink()) //nolint:errcheck
link.Reset()
}
if cell.Link != link {
buf.WriteString(ansi.SetHyperlink(cell.Link.URL, cell.Link.URLID)) //nolint:errcheck
link = cell.Link
}

buf.WriteString(cell.Content)
}
}
buf.WriteString("\r\n")
}
return buf.String()
}
Loading

0 comments on commit cae1634

Please sign in to comment.