ghostty_vte_flutter 0.1.1-dev.1 copy "ghostty_vte_flutter: ^0.1.1-dev.1" to clipboard
ghostty_vte_flutter: ^0.1.1-dev.1 copied to clipboard

Flutter terminal UI widgets powered by Ghostty's VT engine. Provides GhosttyTerminalView, GhosttyTerminalController, and automatic wasm initialisation for web targets.

ghostty_vte_flutter #

CI pub package License: MIT

Flutter terminal UI widgets powered by Ghostty's VT engine. Drop-in GhosttyTerminalView and GhosttyTerminalController for embedding a terminal in any Flutter app — on desktop, mobile, and the web.

Features #

Widget / Class Description
GhosttyTerminalView CustomPaint-based terminal renderer with keyboard input, text selection, hyperlink detection, and mouse reporting
GhosttyTerminalController ChangeNotifier that manages a shell subprocess (native PTY or Process) or remote transport (web)
GhosttyTerminalSnapshot Parsed styled terminal output with selection, word-boundary, and hyperlink support
GhosttyTerminalRenderSnapshot High-fidelity cell-level render data from Ghostty's native render-state API
initializeGhosttyVteWeb() One-liner that loads ghostty-vt.wasm from Flutter assets on web

This package re-exports all of ghostty_vte, so you only need a single import.

Platform support #

Platform Native shell Web (wasm)
Linux yes yes
macOS yes yes
Windows yes yes
Android yes yes
iOS -- yes

Installation #

dependencies:
  ghostty_vte_flutter: ^0.1.0+1

No separate ghostty_vte dependency is needed — it's re-exported automatically.

Quick start #

A minimal terminal app with a live shell session:

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:ghostty_vte_flutter/ghostty_vte_flutter.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await initializeGhosttyVteWeb(); // no-op on native, loads wasm on web
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) => const MaterialApp(home: TerminalPage());
}

class TerminalPage extends StatefulWidget {
  const TerminalPage({super.key});
  @override
  State<TerminalPage> createState() => _TerminalPageState();
}

class _TerminalPageState extends State<TerminalPage> {
  final _ctrl = GhosttyTerminalController();
  String? _error;

  @override
  void initState() {
    super.initState();
    _startTerminal();
  }

  Future<void> _startTerminal() async {
    try {
      if (kIsWeb) {
        await _ctrl.start();
        _ctrl.appendDebugOutput(
          '\x1b]2;Ghostty VT Demo\x07'
          '\x1b[32mweb demo backend attached\x1b[0m\r\n'
          'Connect a backend and feed bytes with appendDebugOutput().\r\n',
        );
        return;
      }

      final launch = await _ctrl.startShellProfile(
        profile: GhosttyTerminalShellProfile.auto,
        platformEnvironment: ghosttyTerminalPlatformEnvironment(),
      );
      if (launch == null) {
        await _ctrl.start(
          environment: ghosttyTerminalShellEnvironment(
            platformEnvironment: ghosttyTerminalPlatformEnvironment(),
          ),
        );
      }
    } catch (error) {
      if (!mounted) return;
      setState(() => _error = error.toString());
    }
  }

  @override
  void dispose() {
    _ctrl.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Terminal')),
      body: Column(
        children: [
          if (_error != null)
            Padding(
              padding: const EdgeInsets.all(12),
              child: Text(
                _error!,
                style: const TextStyle(color: Colors.redAccent),
              ),
            ),
          Expanded(
            child: ColoredBox(
              color: Colors.black,
              child: GhosttyTerminalView(controller: _ctrl, autofocus: true),
            ),
          ),
        ],
      ),
    );
  }
}

On native platforms this starts a real shell session. On web it starts the VT engine and shows a demo banner until you connect your own backend and stream bytes into appendDebugOutput().

Controller #

Creating a controller #

final controller = GhosttyTerminalController(
  maxLines: 2000,          // max retained lines in formatted snapshot
  maxScrollback: 10000,    // scrollback depth in the VT terminal
  initialCols: 80,         // initial grid width before layout
  initialRows: 24,         // initial grid height before layout
  preferPty: true,         // prefer native PTY over Process.start
  defaultShell: '/bin/bash',
);

Starting a shell #

// Simple start with defaults
await controller.start();

// Custom shell, arguments, and environment
await controller.start(
  shell: '/bin/zsh',
  arguments: ['-l'],
  environment: {'TERM': 'xterm-256color', 'LANG': 'en_US.UTF-8'},
);

Shell profiles #

Use the built-in shell profile resolver for common configurations:

final launch = await controller.startShellProfile(
  profile: GhosttyTerminalShellProfile.cleanBash,
  platformEnvironment: ghosttyTerminalPlatformEnvironment(),
  environmentOverrides: const {'TERM': 'xterm-256color'},
);

print(controller.activeShellLaunch?.commandLine);
print(controller.activeShellLaunch?.environment?['TERM']);

Available profiles: auto, cleanBash, cleanZsh, userShell.

Shell environment helper #

Build a usable native shell environment with sane defaults:

await controller.start(
  environment: ghosttyTerminalShellEnvironment(
    platformEnvironment: ghosttyTerminalPlatformEnvironment(),
    overrides: const {'TERM': 'xterm-256color'},
  ),
);

ghosttyTerminalShellEnvironment() preserves the caller's base environment, sets TERM, fills HOME-derived XDG_* paths, and ensures a UTF-8 locale when the input environment omitted one.

Launch plans #

For full control, create and start a resolved launch plan:

final launch = GhosttyTerminalShellLaunch(
  label: 'dev-shell',
  shell: '/bin/bash',
  arguments: ['--rcfile', '/path/to/custom.bashrc'],
  environment: {'TERM': 'xterm-256color'},
  setupCommand: 'cd ~/projects\n',
);

await controller.startLaunch(launch);

// Restart with the same launch plan
await controller.restartLaunch(launch);

Writing input #

// Write text to stdin (with optional paste safety check)
controller.write('ls -la\n');
controller.write('rm -rf /\n', sanitizePaste: true);  // rejected: unsafe

// Write raw bytes
controller.writeBytes(utf8.encode('hello'));

Sending key events #

// Send Ctrl+C
controller.sendKey(
  key: GhosttyKey.GHOSTTY_KEY_C,
  mods: GhosttyModsMask.ctrl,
  utf8Text: 'c',
  unshiftedCodepoint: 0x63,
);

// Send Enter
controller.sendKey(
  key: GhosttyKey.GHOSTTY_KEY_ENTER,
  utf8Text: '\r',
);

// Send arrow keys
controller.sendKey(key: GhosttyKey.GHOSTTY_KEY_ARROW_UP);

Sending mouse events #

controller.sendMouse(
  action: GhosttyMouseAction.GHOSTTY_MOUSE_ACTION_PRESS,
  button: GhosttyMouseButton.GHOSTTY_MOUSE_BUTTON_LEFT,
  position: VtMousePosition(col: 10, row: 5),
  size: VtMouseEncoderSize(cols: controller.cols, rows: controller.rows),
);

Reading terminal state #

print(controller.title);       // window title from OSC 0/2
print(controller.isRunning);   // subprocess alive?
print(controller.lines);       // buffered output lines
print(controller.lineCount);   // number of buffered lines
print(controller.plainText);   // full plain-text snapshot
print(controller.cols);        // current grid width
print(controller.rows);        // current grid height
print(controller.revision);    // monotonic change counter

Styled snapshot #

The controller exposes a GhosttyTerminalSnapshot parsed from VT formatter output. This contains styled lines with full SGR attributes, hyperlink detection, and selection support:

final snapshot = controller.snapshot;
for (final line in snapshot.lines) {
  for (final run in line.runs) {
    print('${run.text} bold=${run.style.bold} fg=${run.style.foreground}');
  }
}

// Text selection
final selection = GhosttyTerminalSelection(
  base: GhosttyTerminalCellPosition(row: 0, col: 0),
  extent: GhosttyTerminalCellPosition(row: 2, col: 10),
);
final selectedText = snapshot.textForSelection(selection);

// Word selection at a cell position
final wordSel = snapshot.wordSelectionAt(
  GhosttyTerminalCellPosition(row: 1, col: 5),
);

// Hyperlink detection
final link = snapshot.hyperlinkAt(
  GhosttyTerminalCellPosition(row: 3, col: 12),
);

Native render-state snapshot #

On native platforms, the controller also exposes a high-fidelity GhosttyTerminalRenderSnapshot derived from Ghostty's incremental render-state API:

final renderSnap = controller.renderSnapshot;
if (renderSnap != null) {
  print(renderSnap.cols);             // viewport width
  print(renderSnap.rows);             // viewport height
  print(renderSnap.dirty);            // dirty state
  print(renderSnap.cursor.visible);   // cursor visibility
  print(renderSnap.cursor.row);       // cursor row

  for (final row in renderSnap.rowsData) {
    for (final cell in row.cells) {
      // cell.text, cell.style.foreground, cell.style.bold, etc.
    }
  }
}

Direct VT terminal access #

The controller exposes the underlying VtTerminal for advanced use cases:

final terminal = controller.terminal;
print(terminal.cursorPosition);
print(terminal.isPrimaryScreen);
print(terminal.mouseProtocolState.enabled);

// Query terminal modes
final bracketedPaste = terminal.getMode(VtModes.bracketedPaste);

// Grid introspection
final cell = terminal.activeCell(0, 0);
print(cell.graphemeText);
print(cell.style);

Custom formatted output #

Generate terminal output in different formats on demand:

// Plain text with trimming
final plain = controller.formatTerminal();

// VT sequences with styles and cursor
final vt = controller.formatTerminal(
  emit: GhosttyFormatterFormat.GHOSTTY_FORMATTER_FORMAT_VT,
  trim: false,
  extra: const VtFormatterTerminalExtra.all(),
);

Stopping and cleanup #

await controller.stop();  // kill subprocess
controller.clear();       // reset terminal, clear scrollback
controller.dispose();     // release all resources

View #

GhosttyTerminalView #

A CustomPaint widget that renders terminal output, handles keyboard events through the Ghostty key encoder, and supports text selection, hyperlinks, and mouse reporting.

GhosttyTerminalView(
  controller: myController,
  autofocus: true,
  backgroundColor: const Color(0xFF0A0F14),
  foregroundColor: const Color(0xFFE6EDF3),
  fontSize: 14,
  lineHeight: 1.35,
  fontFamily: 'JetBrainsMono Nerd Font',
  cellWidthScale: 1.0,
  padding: const EdgeInsets.all(12),
  palette: GhosttyTerminalPalette.xterm,
  cursorColor: const Color(0xFF9AD1C0),
  selectionColor: const Color(0x665DA9FF),
  hyperlinkColor: const Color(0xFF61AFEF),
  renderer: GhosttyTerminalRendererMode.formatter,
  interactionPolicy: GhosttyTerminalInteractionPolicy.auto,
  onSelectionChanged: (selection) { /* ... */ },
  onCopySelection: (content) { /* ... */ },
  onPasteRequest: () async => clipboardText,
  onOpenHyperlink: (uri) { /* ... */ },
)
Property Type Default Description
controller GhosttyTerminalController required Terminal session to render
autofocus bool false Request focus on mount
focusNode FocusNode? null Custom focus node
backgroundColor Color #0A0F14 Canvas background
foregroundColor Color #E6EDF3 Text color
chromeColor Color #121A24 Terminal chrome accent color
fontSize double 14 Monospace font size
lineHeight double 1.35 Line height multiplier
fontFamily String? null Override the terminal font family
fontFamilyFallback List<String>? null Fallback fonts for terminal glyphs
fontPackage String? null Package that provides fontFamily
letterSpacing double 0 Additional character spacing
cellWidthScale double 1 Manual terminal cell width tuning for prompt glyph alignment
padding EdgeInsets all(12) Content padding
palette GhosttyTerminalPalette xterm Color palette for indexed ANSI colors
cursorColor Color #9AD1C0 Cursor fill color
selectionColor Color #665DA9FF Selection highlight color
hyperlinkColor Color #61AFEF Detected hyperlink text color
renderer GhosttyTerminalRendererMode formatter Choose formatter or native render-state painting
interactionPolicy GhosttyTerminalInteractionPolicy auto Resolve conflicts between text selection and terminal mouse reporting
onSelectionChanged callback null Called when text selection changes
onCopySelection callback null Called with selection content for clipboard copy
onPasteRequest callback null Called to retrieve clipboard text for paste
onOpenHyperlink callback null Called when a hyperlink is activated

Renderer modes #

// Formatter mode (default): snapshot-driven, best for scrollback and dense TUIs
GhosttyTerminalView(
  controller: ctrl,
  renderer: GhosttyTerminalRendererMode.formatter,
)

// Render-state mode: native cell-level data, incremental dirty tracking
GhosttyTerminalView(
  controller: ctrl,
  renderer: GhosttyTerminalRendererMode.renderState,
)

Interaction policies #

Control how the view handles conflicts between text selection and terminal mouse reporting:

// Auto (default): prefer text selection unless the running program enables
// mouse reporting
GhosttyTerminalView(
  controller: ctrl,
  interactionPolicy: GhosttyTerminalInteractionPolicy.auto,
)

// Always prefer text selection
GhosttyTerminalView(
  controller: ctrl,
  interactionPolicy: GhosttyTerminalInteractionPolicy.selectionFirst,
)

// Always forward to terminal mouse reporting
GhosttyTerminalView(
  controller: ctrl,
  interactionPolicy: GhosttyTerminalInteractionPolicy.terminalMouseFirst,
)

Selection and clipboard #

Wire up selection and clipboard callbacks for copy/paste support:

GhosttyTerminalView(
  controller: ctrl,
  copyOptions: const GhosttyTerminalCopyOptions(
    trimTrailingSpaces: true,
    joinWrappedLines: false,
  ),
  wordBoundaryPolicy: const GhosttyTerminalWordBoundaryPolicy(
    extraWordCharacters: '._/~:@%#?&=+-',
    treatNonAsciiAsWord: true,
  ),
  onCopySelection: (content) {
    Clipboard.setData(ClipboardData(text: content.text));
  },
  onPasteRequest: () async {
    final data = await Clipboard.getData(Clipboard.kTextPlain);
    return data?.text;
  },
  onOpenHyperlink: (uri) {
    launchUrl(Uri.parse(uri));
  },
)

Complete example #

A full terminal app with shell profile selection, clipboard, and theming:

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:ghostty_vte_flutter/ghostty_vte_flutter.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await initializeGhosttyVteWeb();
  runApp(const TerminalApp());
}

class TerminalApp extends StatelessWidget {
  const TerminalApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData.dark(),
      home: const TerminalScreen(),
    );
  }
}

class TerminalScreen extends StatefulWidget {
  const TerminalScreen({super.key});
  @override
  State<TerminalScreen> createState() => _TerminalScreenState();
}

class _TerminalScreenState extends State<TerminalScreen> {
  final _ctrl = GhosttyTerminalController(
    maxScrollback: 10000,
    preferPty: true,
  );

  @override
  void initState() {
    super.initState();
    _startShell();
  }

  Future<void> _startShell() async {
    await _ctrl.startShellProfile(
      profile: GhosttyTerminalShellProfile.auto,
      platformEnvironment: ghosttyTerminalPlatformEnvironment(),
    );
  }

  @override
  void dispose() {
    _ctrl.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: ListenableBuilder(
          listenable: _ctrl,
          builder: (context, _) => Text(_ctrl.title),
        ),
        actions: [
          IconButton(
            icon: const Icon(Icons.refresh),
            onPressed: () async {
              final launch = _ctrl.activeShellLaunch;
              if (launch != null) {
                await _ctrl.restartLaunch(launch);
              }
            },
          ),
        ],
      ),
      body: GhosttyTerminalView(
        controller: _ctrl,
        autofocus: true,
        backgroundColor: const Color(0xFF0A0F14),
        foregroundColor: const Color(0xFFE6EDF3),
        fontSize: 14,
        fontFamily: 'JetBrainsMono Nerd Font',
        fontFamilyFallback: const ['monospace'],
        onCopySelection: (content) {
          Clipboard.setData(ClipboardData(text: content.text));
        },
        onPasteRequest: () async {
          final data = await Clipboard.getData(Clipboard.kTextPlain);
          return data?.text;
        },
      ),
    );
  }
}

Web setup #

  1. Build the wasm module:

    cd pkgs/vte/ghostty_vte
    dart run tool/build_wasm.dart
    

    This produces ghostty-vt.wasm in the Flutter assets directory.

  2. Initialise before runApp:

    await initializeGhosttyVteWeb();
    

    This is a no-op on native platforms.

  3. Build for web:

    flutter build web --wasm
    

Native setup #

No manual steps needed. The ghostty_vte build hook runs automatically during flutter run and flutter build, producing the correct native library for your target. Just make sure Zig and the Ghostty source are available — see the ghostty_vte README for details.

Or download prebuilt libraries to skip the Zig requirement entirely.

Package Description
ghostty_vte Core Dart FFI bindings (re-exported by this package)
portable_pty Cross-platform PTY subprocess control
portable_pty_flutter Flutter controller for PTY sessions

License #

MIT — see LICENSE.

1
likes
0
points
151
downloads

Publisher

unverified uploader

Weekly Downloads

Flutter terminal UI widgets powered by Ghostty's VT engine. Provides GhosttyTerminalView, GhosttyTerminalController, and automatic wasm initialisation for web targets.

Homepage
Repository (GitHub)
View/report issues

Topics

#terminal #flutter #widget #ghostty

License

unknown (license)

Dependencies

characters, flutter, ghostty_vte, portable_pty, url_launcher

More

Packages that depend on ghostty_vte_flutter