Skip to content

feat(vt): implement scrollback buffer support#605

Open
Gaurav-Gosain wants to merge 1 commit intocharmbracelet:mainfrom
Gaurav-Gosain:feature/scrollback-buffer
Open

feat(vt): implement scrollback buffer support#605
Gaurav-Gosain wants to merge 1 commit intocharmbracelet:mainfrom
Gaurav-Gosain:feature/scrollback-buffer

Conversation

@Gaurav-Gosain
Copy link
Copy Markdown
Contributor

Summary

This PR implements comprehensive scrollback buffer functionality for the VT terminal emulator. The scrollback buffer stores lines that scroll off the top of the visible screen, allowing applications to access terminal history.

Motivation

Scrollback support is a fundamental feature of modern terminal emulators. Several TODOs in the codebase indicated this functionality was missing:

  • handlers.go: ED 3 sequence (clear scrollback) was not implemented
  • cc.go: Scrollback support was marked as TODO
  • csi_mode.go: Scrollback-related modes needed implementation

This implementation resolves these TODOs and provides a robust foundation for terminal history management.

Implementation

Core Components

  1. Scrollback struct (scrollback.go):

    • Circular buffer storing scrolled lines
    • Configurable maximum size (default: 10,000 lines)
    • Efficient memory management with pre-allocation
    • Deep copying to prevent aliasing issues
  2. Screen integration (screen.go):

    • Automatic line capture during ScrollUp operations
    • Thread-safe access with sync.RWMutex
    • Only captures full-width scrolling from Y=0
    • Separate scrollback for main and alternate screens
  3. API Methods (emulator.go):

    • Scrollback() - Access scrollback buffer
    • ScrollbackLen() - Get number of stored lines
    • ScrollbackLine(index) - Retrieve specific line
    • ClearScrollback() - Clear history
    • SetScrollbackMaxLines(n) - Configure buffer size
  4. ANSI Sequence Support (handlers.go):

    • ED 3 (ESC[3J) - Clear scrollback buffer

Design Decisions

  • Full-width scrolling only: Scrollback only captures when scrolling the entire width from Y=0, not limited scroll regions. This matches behavior of standard terminal emulators.
  • Alternate screen: The alternate screen maintains its own separate scrollback buffer.
  • Thread safety: All scrollback operations use read/write mutexes for concurrent access.
  • Deep copying: Lines are deep copied when added to prevent modifications to live screen from affecting scrollback.

Usage Example

package main

import (
    "fmt"
    uv "github.com/charmbracelet/ultraviolet"
    "github.com/charmbracelet/x/vt"
)

func main() {
    // Create a terminal
    term := vt.NewEmulator(80, 24)
    
    // Configure scrollback size (optional, default is 10,000 lines)
    term.SetScrollbackMaxLines(1000)
    
    // Write content that scrolls off the screen
    for i := 0; i < 100; i++ {
        term.Write([]byte(fmt.Sprintf("Line %d\r\n", i)))
    }
    
    // Access scrollback
    numLines := term.ScrollbackLen()
    fmt.Printf("Scrollback has %d lines\n", numLines)
    
    // Get a specific line (index 0 is oldest)
    if line := term.ScrollbackLine(0); line != nil {
        fmt.Printf("Oldest line: %v\n", line)
    }
    
    // Clear scrollback when needed
    term.ClearScrollback()
    
    // Clear both screen and scrollback (ED 3 sequence)
    term.Write([]byte("\x1b[3J"))
}

Testing

Comprehensive test suite with 13 tests covering:

  • Basic push/retrieve operations
  • Buffer overflow and circular behavior
  • Line deep copying
  • Bounds checking
  • Integration with Screen and Emulator
  • ED 3 sequence handling
  • Separate main/alternate screen scrollback
  • Concurrent access patterns

All tests pass successfully.

Performance Considerations

  • Pre-allocates reasonable buffer capacity to minimize reallocations
  • Uses efficient slice operations for circular buffer management
  • Deep copying only when adding lines, not on retrieval
  • Read-write mutexes allow concurrent reads

Breaking Changes

None. This is purely additive functionality.

Related

Resolves TODOs in:

  • handlers.go (ED 3 sequence)
  • cc.go (scrollback support)
  • csi_mode.go (scrollback modes)

Adds comprehensive scrollback buffer functionality to the VT terminal emulator,
allowing applications to store and access lines that have scrolled off the top
of the visible screen.

Features:
- Scrollback buffer with configurable maximum lines (default 10,000)
- Automatic line capture during full-width scrolling operations
- Thread-safe access with read/write mutexes
- Deep copying of lines to prevent aliasing issues
- ED 3 (ESC[3J) sequence support for clearing scrollback
- Separate scrollback for main and alternate screens
- Comprehensive API for accessing and managing scrollback

API:
- Scrollback() - Get scrollback buffer reference
- ScrollbackLen() - Get number of lines in scrollback
- ScrollbackLine(index) - Retrieve line at index
- ClearScrollback() - Clear all scrollback history
- SetScrollbackMaxLines(n) - Configure buffer size

The scrollback only captures full-width scrolling from Y=0, not limited scroll
regions, following standard terminal emulator behavior.
Comment thread vt/scrollback.go
// scrolled off the top of the visible screen.
type Scrollback struct {
// lines stores the scrollback lines, with the oldest at index 0
lines [][]uv.Cell
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
lines [][]uv.Cell
lines []uv.Line

Comment thread vt/scrollback.go

// Lines returns a slice of all lines in the scrollback buffer, from oldest
// to newest. The returned slice should not be modified.
func (sb *Scrollback) Lines() [][]uv.Cell {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func (sb *Scrollback) Lines() [][]uv.Cell {
func (sb *Scrollback) Lines() []uv.Line {

Comment thread vt/scrollback.go
// number of lines. If maxLines is 0, a default of 10000 lines is used.
func NewScrollback(maxLines int) *Scrollback {
if maxLines <= 0 {
maxLines = 10000 // Default scrollback size
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this into a const DefaultScrollbackSize = 10000

Comment thread vt/scrollback.go
// are discarded to fit the new limit.
func (sb *Scrollback) SetMaxLines(maxLines int) {
if maxLines <= 0 {
maxLines = 10000 // Default scrollback size
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
maxLines = 10000 // Default scrollback size
maxLines = DefaultScrollbackSize

Comment thread vt/screen.go
// scrollback is the scrollback buffer for lines that have scrolled off the top.
scrollback *Scrollback
// mutex for the screen.
mu sync.RWMutex
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current VT implementation doesn't use any mutexes and doesn't guarantee thread safety. Caller should handle that and I think it should stay that way for now

aymanbagabas pushed a commit that referenced this pull request Mar 5, 2026
* feat: add vt scrollback

* fixup: address ayman's comments

* use slices package

Supersedes: #605
@aymanbagabas
Copy link
Copy Markdown
Contributor

Hi @Gaurav-Gosain, I've merged #793. Let me know if that works with your use cases 🙂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants