Skip to content

stefan-500/terminal-buffer

Repository files navigation

Terminal Emulator Text Buffer

This project implements a terminal text buffer. Text buffer is a core data structure a terminal emulator uses to store and manipulate displayed text.

The project features a terminal text buffer as a fixed size grid of cells with a scrollback history which contains lines that scrolled off the top of the screen.

Project structure

terminal-buffer/
├── build.gradle.kts
├── gradlew
├── settings.gradle.kts
├── docs/
├── src/
│   ├── main/java/      # logic implementation
│   └── test/java/      # JUnit 5 tests
└── README.md

Quick start

Run the build and tests.

Linux/MAC:

./gradlew build

Windows:

gradlew.bat build

Tech stack

  • JDK 21
  • JUnit 5
  • Gradle (Kotlin DSL)

Architecture

These are the main components:

  • TerminalBuffer, an entry point for users and tests. It owns:

    • screen (fixed height)
    • scrollback (constrained by maxScrollbackLines)
    • cursor and current text attributes (TextAttributes)
    • editing operations and content accessors
  • Line, an internal storage of fixed width, contains character array and attributes per cell.

    • It supports line fill and cell insertion.
  • TextAttributes, TerminalColor, StyleFlag; these are the value types for cell styling.


Task #1

Why are you interested in this project and how do you see its implementation?

I’m interested in this project because it is at the boundary between systems behavior and developer experience.
A terminal looks simple on the surface, but functionality depends on many small and solid rules like for cursor movement, scrolling, wrapping, history, etc.
Implementing the text buffer is interesting to me because it is a clean core component. If the buffer is correct and well tested, everything built on top of it (rendering, escape-sequence parsing, integrations) is much more reliable.
To better understand how JetBrains approaches this problem, using Java/Kotlin, I checked out the JediTerm repository and its TerminalTextBuffer implementation.

It is interesting to me how some Terminal emulator / text editor programs like Vim behave differently from others.

How I see the implementation:

I see this work as building the core internal model that any emulator integration depends on (a screen text grid and scrollback, cursor, cell attributes, with a well defined behavior and design decisions).
For the integration direction described, I would treat the external emulator as the source of truth for emulation standards (parsing, decoding, output interpretation) and design the IntelliJ side around a clean boundary.
I would implement and verify the solution incrementally with tests as behavior documentation, focusing first on correctness and stability (wrapping, scrolling, screen bounds, history, etc.) because those are the failure points that typically cause rendering issues in CLI applications after updates and general changes.

I am drawn to the project because it combines data structure design with real world behavior requirements, and it is directly relevant to improving the IntelliJ terminal experience.


Solution

I implemented a terminal text buffer as a fixed size grid of cells and a constrained scrollback history.

  • The main entry point is TerminalBuffer, an entrance point which owns the screen, scrollback, cursor position, and text attributes.
  • Internally, a Line object stores cell characters and attributes in arrays of fixed width.
  • Value types (TextAttributes, TerminalColor, StyleFlag) represent text styling and attributes.

Key design decisions and trade-offs

0-based coordinates and cursor clamping.

The cursor is always kept within screen bounds (0..width-1 and 0..height-1).

Wrap at line-end

Writing or inserting text at the last column advances to the next row. Wrapping from the last row scrolls the screen, the top line moves to scrollback, and a new blank line appears at the bottom. The cursor moves to the next line.
This avoids losing characters and keeps relative model simplicity at the same time.

Empty cells stored as NULL and rendered as spaces

Internally, unwritten cells are stored as '\0'. String producing functions render '\0' as a space (' ') for output. This eases the detection of cells which were not written and cells with a space.

Cell access returns internal values.

getCharAt() returns the internal value ('\0') intentionally, not a rendered space. It is an exception to the rule above. This is documented and tested.
Rendering rules apply only to methods that produce strings.

Fill semantics

Filling a line with empty clears cells and resets to default attributes.
Filling a line with a visible character uses current attributes.

Clear semantics

clearScreen() clears only the screen, preserves scrollback, and resets the cursor to start (0,0).
clearAll() clears both screen and scrollback and resets the cursor.
This behavior is okay because for example ANSI code parsing is out of scope for this project.

Scrollback and screen indexing

Screen rows are represented with positive indices (row >= 0).
Scrollback rows are represented with negtive indices (row < 0).
This is a Jediterm style approach, requires careful checking of bounds in tests, and it is documented.

  • row = -1 is the most recent scrollback line.
  • row = -scrollbackSize is the oldest scrollback line.

Line to string trimming

When getting a line as a string, trailing spaces are removed.
This keeps tests readable while still reflecting visible text.

Text insert behavior - drop overflow

insertText() shifts content to right within the current line. Overflow of shifted content is dropped, while inserted text wraps normally.

Trade-off:

  • This is not exactly the usual expected terminal behavior.
  • Carry overflow would preserve content better, yet it is significantly more complex to implement.
  • Carry overflow can be implemented in the future.

Resize policy

Height shrinking preserves the newest lines.

  • The bottom-most newHeight screen lines are preserved.
  • Dropped top lines are moved into scrollback.
  • This approach keeps the newest content visible.

Width shrinking clips the right side of the screen.

  • Width shrinking clips the right side of the screen, while scrollback lines preserve the original width.
  • Scrollback lines are rendered up to min(scrollbackLineWidth, currentWidth).
  • This approach respects the definition of the buffer as a grid of cells, and introduces minimal complexity to this design model.
  • Trade-off:
    • Reflowing clipped text would be the more natural terminal behavior option, but it is also more complex, and text would have to be treated as a stream.
    • Reflow can be implemented in the future.

Wide character support (CJK and emoji)

Wide characters are supported using a minimal rule set.

Heuristics for detecting wide characters:

  • Emoji and selected CJK ranges are treated as characters of width 2.
  • All other code points are treated as width 1.
  • This is intentionally incomplete and documented as a trade-off.

The representation of wide characters:

  • A wide character is stored as two parts: a lead cell and a continuation cell.
  • The lead cell stores the code point and attributes of the cell.
  • The continuation cell is marked as a cell that must not be rendered as a separate character.

Cursor behavior rules:

  • The cursor is cell-based, it always points to a cell in the grid.
  • Writing of wide characters advances the cursor by 2 cells.
  • If the cursor is at a continuation cell, it snaps left to its lead cell before editing.

Editing rules:

  • If a wide character is being written at the last column of a line, the buffer wraps before writing and scrolls if needed.
  • If an editing operation touches either half of a wide character, both halves of it are cleared first to prevent broken wide characters.

String APIs render lead cells normally and skip continuation cells.
Functions getCharAt() and getAttributesAt() return '\0' and default text attributes for continuation cells.

Trade-offs:

  • The width heuristics are minimal, not a full Unicode implementation, so some characters are misinterpreted.
  • Combining marks and grapheme clusters are not fully supported. Behavior is defined at the cell/code-point level.

Improvements I would implement next:

  • Carry overflow insertion for more terminal-like natural insert behavior.
  • More advanced resize strategies including optional reflow strategy with wrap metadata.
  • Potential memory optimizations if the buffer is to be scaled to large scrollbacks.
  • ANSI/VT command parsing and tests, a big logical/functionality update.
  • Visual rendering, which would take a lot of time, but it is a good option for learning about backend terminal logic.

Useful links

Author

Stefan Vujović, 2026.

About

Simple Text Buffer Implementation for Terminal Emulators

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages