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.
Quick links: Architecture | Design decisions | Quick start | Planned improvements
terminal-buffer/
├── build.gradle.kts
├── gradlew
├── settings.gradle.kts
├── docs/
├── src/
│ ├── main/java/ # logic implementation
│ └── test/java/ # JUnit 5 tests
└── README.md
Run the build and tests.
Linux/MAC:
./gradlew buildWindows:
gradlew.bat build- JDK 21
- JUnit 5
- Gradle (Kotlin DSL)
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
- screen (fixed
-
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.
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.
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.
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
Lineobject stores cell characters and attributes in arrays of fixed width. - Value types (
TextAttributes,TerminalColor,StyleFlag) represent text styling and attributes.
The cursor is always kept within screen bounds (0..width-1 and 0..height-1).
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.
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.
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.
Filling a line with empty clears cells and resets to default attributes.
Filling a line with a visible character uses current attributes.
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.
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 = -1is the most recent scrollback line.row = -scrollbackSizeis the oldest scrollback line.
When getting a line as a string, trailing spaces are removed.
This keeps tests readable while still reflecting visible text.
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.
Height shrinking preserves the newest lines.
- The bottom-most
newHeightscreen 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 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
2cells. - 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.
- 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.
- https://poor.dev/blog/terminal-anatomy/
- https://github.com/JetBrains/jediterm
- https://learn.microsoft.com/en-us/windows/console/console-screen-buffers
- https://www.youtube.com/watch?v=4GBp_WTG66Q
Stefan Vujović, 2026.