terminal_core is a pure Dart terminal emulation core for byte streams coming
from SSH channels, PTYs, or any other terminal-like transport.
It focuses on the terminal state/model layer so that another package such as
terminal_flutter can handle rendering.
This repository is public and usable today as a practical terminal core MVP.
What it is:
- A UI-agnostic terminal parser and screen model
- Incremental and chunk-boundary-safe for transport streams
- Designed to pair cleanly with an SSH or PTY package
What it is not:
- A Flutter widget or renderer
- An SSH client, PTY controller, or networking package
- A full xterm implementation
- Incremental
writeBytes(List<int>)parsing - UTF-8 decoding across chunk boundaries
- Grapheme-cluster-aware Unicode handling for combining marks, emoji modifiers, flags, and common ZWJ emoji sequences
- Configurable ambiguous-width handling for East Asian ambiguous characters
- Main buffer and alternate buffer
- Cursor state, scroll region, DEC origin/wrap modes, and main-buffer scrollback
- Horizontal tab stops with default 8-column stops plus HTS/TBC support
- Dirty-row tracking for UI layers
- Title and bell notifications
- A terminal input encoder that produces bytes ready to send to
ssh_core
Add the package from pub.dev:
dart pub add terminal_coreOr declare it in pubspec.yaml:
dependencies:
terminal_core: ^0.2.0If you need unreleased commits before the next publish, you can depend on the GitHub repository directly:
dependencies:
terminal_core:
git:
url: https://github.com/Atrac613/terminal_core.gitThen import the package:
import 'package:terminal_core/terminal_core.dart';import 'dart:convert';
import 'package:terminal_core/terminal_core.dart';
void main() {
final terminal = Terminal(columns: 20, rows: 5);
terminal.titles.listen((title) => print('title: $title'));
terminal.bells.listen((_) => print('bell'));
terminal.writeBytes(utf8.encode('hello\r\n'));
terminal.writeBytes(utf8.encode('\x1b[31mred\x1b[0m text'));
print(terminal.visibleText());
}For a runnable feed-and-inspect demo, see
example/basic.dart.
The primary public types are:
Terminal: parser, terminal state, resize, notifications, and visible bufferTerminalBuffer: read-only access to visible rows and scrollbackTerminalCell: a single cell with character text,TerminalStyle, width, and continuation metadataTerminalStyle: bold / italic / underline / inverse and foreground / background colorsTerminalCursor: current cursor row, column, and visibilityTerminalInputEncoder: encodes key presses and paste payloads into bytesTerminalKey/TerminalKeyEvent: special-key model for input encoding
Useful methods and properties:
terminal.writeBytes(bytes)for incremental transport inputterminal.write(text)for tests or already-decoded inputterminal.resize(cols, rows)to resize the viewportterminal.visibleText()andterminal.debugDump()for testing/debuggingterminal.changes,terminal.titles, andterminal.bellsfor notificationsterminal.inputStateto mirror terminal-controlled input modes into the encoderterminal.ambiguousWidthIsWideto choose narrow vs. wide rendering for East Asian ambiguous charactersterminal.originModeEnabled,terminal.wraparoundModeEnabled, andterminal.tabStopsfor DEC-mode-aware terminal state inspectionterminal.applicationCursorKeysEnabledandterminal.applicationKeypadModeEnabledfor low-level mode checksterminal.bracketedPasteModeEnabledto drive paste encoding
LFCRBSTABBELESC
ESC 7/ESC 8cursor save / restoreESC Hset a horizontal tab stop at the current columnESC =/ESC >keypad application / numeric modeCSI s/CSI ucursor save / restoreCSI A/B/C/Dcursor moveCSI H/fcursor positionCSI @insert blank charactersCSI gclear the current tab stop or all tab stopsCSI Jerase in displayCSI Kerase in lineCSI Linsert linesCSI Mdelete linesCSI Pdelete charactersCSI Sscroll upCSI Tscroll downCSI Xerase charactersCSI mSGRCSI rscroll regionCSI ?6h/CSI ?6lorigin modeCSI ?7h/CSI ?7lwraparound modeCSI ?1h/CSI ?1lapplication cursor keysCSI ?1049h/CSI ?1049lalternate screenCSI ?2004h/CSI ?2004lbracketed paste modeCSI ?25h/CSI ?25lcursor visible stateCSI ?66h/CSI ?66lkeypad application / numeric mode aliasOSC 0title updateOSC 2title update
- Reset
- Bold
- Italic
- Underline
- Inverse
- 16 colors
- Bright 16 colors
- 256 colors
- Truecolor via
38;2/48;2
Unsupported or unknown escape sequences are ignored safely instead of crashing.
This package handles the most important terminal-oriented Unicode cases:
- ASCII and other single-cell characters render as width
1 - East Asian wide/fullwidth characters render as width
2 - Common emoji code points render as width
2 - Combining marks, emoji modifiers, and variation selectors extend the previous visible cluster when possible
- Regional-indicator flags and common ZWJ emoji families are treated as a single display cluster
- Double-width clusters are stored as a lead cell plus a continuation cell
- Ambiguous-width characters can be treated as narrow or wide by setting
Terminal(ambiguousWidthIsWide: true)
Current scope is intentionally practical rather than exhaustive:
- If resize or editing would leave half of a double-width cluster behind, the broken cluster is blanked safely
- The tested focus is on terminal-relevant clusters rather than exhaustive Unicode conformance for every edge case
resize(cols, rows) keeps the existing screen contents without reflowing lines.
- Column changes truncate or pad each stored line in place
- When rows shrink, the viewport is bottom-anchored
- Lines removed from the top of the main buffer move into scrollback
- When rows grow, the main buffer reveals scrollback above the viewport when available; otherwise blank rows are inserted above
- If a column shrink cuts through a double-width character, the incomplete glyph is blanked instead of leaving a broken half-cell
- Existing tab stops are preserved when possible; stops past the new width are dropped, and newly exposed columns gain default 8-column stops
- The cursor is clamped to the new viewport
- The scroll region resets to the full viewport after resize
This behavior is covered by tests and intended to be stable for UI consumers.
import 'package:terminal_core/terminal_core.dart';
Future<void> attachTerminal(SshChannel channel) async {
final terminal = Terminal(columns: 80, rows: 24);
channel.stdout.listen(terminal.writeBytes);
channel.write(
TerminalInputEncoder.fromState(
terminal.inputState,
).encodeKey(TerminalKey.arrowUp),
);
channel.write(
TerminalInputEncoder.fromState(
terminal.inputState,
).encodePaste(
'ls -la\n',
bracketedPasteMode: terminal.bracketedPasteModeEnabled,
),
);
}SshChannel is intentionally not defined here; the point is that
terminal_core consumes bytes from the remote side and emits bytes for local
input without knowing anything about transport or rendering.
Run the example locally:
dart run example/basic.dartRun the standard verification set:
dart format .
dart analyze
dart test
dart run example/basic.dartIf you are preparing a release, also verify:
dart pub publish --dry-run- Full xterm compatibility
- Mouse reporting
- Selection and search
- Hyperlinks
- Sixels, images, and graphics protocols
- IME integration
- Rendering, widgets, or accessibility semantics
Likely next areas of expansion are:
- Additional DEC / xterm compatibility
- Deeper Unicode edge-case coverage
- A renderer package such as
terminal_flutter
Issues and pull requests are welcome. For local contributor workflow details,
see AGENTS.md.
This project is available under the MIT License.