Skip to content

Atrac613/terminal_core

Repository files navigation

terminal_core

CI License: MIT

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.

Status

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

Highlights

  • 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

Getting Started

Add the package from pub.dev:

dart pub add terminal_core

Or declare it in pubspec.yaml:

dependencies:
  terminal_core: ^0.2.0

If 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.git

Then import the package:

import 'package:terminal_core/terminal_core.dart';

Quick Start

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.

Public API

The primary public types are:

  • Terminal: parser, terminal state, resize, notifications, and visible buffer
  • TerminalBuffer: read-only access to visible rows and scrollback
  • TerminalCell: a single cell with character text, TerminalStyle, width, and continuation metadata
  • TerminalStyle: bold / italic / underline / inverse and foreground / background colors
  • TerminalCursor: current cursor row, column, and visibility
  • TerminalInputEncoder: encodes key presses and paste payloads into bytes
  • TerminalKey / TerminalKeyEvent: special-key model for input encoding

Useful methods and properties:

  • terminal.writeBytes(bytes) for incremental transport input
  • terminal.write(text) for tests or already-decoded input
  • terminal.resize(cols, rows) to resize the viewport
  • terminal.visibleText() and terminal.debugDump() for testing/debugging
  • terminal.changes, terminal.titles, and terminal.bells for notifications
  • terminal.inputState to mirror terminal-controlled input modes into the encoder
  • terminal.ambiguousWidthIsWide to choose narrow vs. wide rendering for East Asian ambiguous characters
  • terminal.originModeEnabled, terminal.wraparoundModeEnabled, and terminal.tabStops for DEC-mode-aware terminal state inspection
  • terminal.applicationCursorKeysEnabled and terminal.applicationKeypadModeEnabled for low-level mode checks
  • terminal.bracketedPasteModeEnabled to drive paste encoding

Supported Sequences

C0 Controls

  • LF
  • CR
  • BS
  • TAB
  • BEL
  • ESC

Escape / CSI / OSC

  • ESC 7 / ESC 8 cursor save / restore
  • ESC H set a horizontal tab stop at the current column
  • ESC = / ESC > keypad application / numeric mode
  • CSI s / CSI u cursor save / restore
  • CSI A / B / C / D cursor move
  • CSI H / f cursor position
  • CSI @ insert blank characters
  • CSI g clear the current tab stop or all tab stops
  • CSI J erase in display
  • CSI K erase in line
  • CSI L insert lines
  • CSI M delete lines
  • CSI P delete characters
  • CSI S scroll up
  • CSI T scroll down
  • CSI X erase characters
  • CSI m SGR
  • CSI r scroll region
  • CSI ?6h / CSI ?6l origin mode
  • CSI ?7h / CSI ?7l wraparound mode
  • CSI ?1h / CSI ?1l application cursor keys
  • CSI ?1049h / CSI ?1049l alternate screen
  • CSI ?2004h / CSI ?2004l bracketed paste mode
  • CSI ?25h / CSI ?25l cursor visible state
  • CSI ?66h / CSI ?66l keypad application / numeric mode alias
  • OSC 0 title update
  • OSC 2 title update

SGR

  • 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.

Unicode Handling

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 Behavior

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.

Minimal ssh_core Example

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.

Development

Run the example locally:

dart run example/basic.dart

Run the standard verification set:

dart format .
dart analyze
dart test
dart run example/basic.dart

If you are preparing a release, also verify:

dart pub publish --dry-run

Unsupported In This MVP

  • Full xterm compatibility
  • Mouse reporting
  • Selection and search
  • Hyperlinks
  • Sixels, images, and graphics protocols
  • IME integration
  • Rendering, widgets, or accessibility semantics

Roadmap

Likely next areas of expansion are:

  • Additional DEC / xterm compatibility
  • Deeper Unicode edge-case coverage
  • A renderer package such as terminal_flutter

Contributing

Issues and pull requests are welcome. For local contributor workflow details, see AGENTS.md.

License

This project is available under the MIT License.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages