Skip to main content

fleury 0.1.1 copy "fleury: ^0.1.1" to clipboard
fleury: ^0.1.1 copied to clipboard

A Dart-native terminal UI framework with Flutter ergonomics and terminal-native internals.

Fleury #

Flutter ergonomics, terminal truth.

A Dart-native terminal UI framework that brings Flutter's familiar authoring model — widgets, state, context, keys, constraints, inherited dependencies, hot-reload-friendly rebuilds — to terminal UIs, while implementing the runtime with terminal-native best practices: cell buffers, ANSI diffing, raw-mode lifecycle management, grapheme-aware text, capability detection, and safe input handling.

Quick start #

Install the CLI and create a tested application scaffold:

dart pub global activate fleury
fleury create my_app
cd my_app

Open the directory in VS Code and press F5, or run dart run bin/run_app.dart from an interactive terminal. For the fastest start use dart run fleury run (fleury run once activated globally): the same save-to-reload session, but the app is compiled once instead of twice (see the hot-reload guide). The generated VS Code configuration uses the integrated terminal so input, rendering, breakpoints, and Dart hot reload share one normal debug session. For unreleased changes, use the Git installation.

A smaller handwritten application looks like this:

import 'package:fleury/fleury.dart';

void main() => runApp(const CounterApp());

class CounterApp extends StatefulWidget {
  const CounterApp({super.key});

  @override
  State<CounterApp> createState() => _CounterAppState();
}

class _CounterAppState extends State<CounterApp> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return KeyBindings(
      bindings: [
        KeyBinding(.space, onTrigger: (_) => setState(() => _count++)),
      ],
      child: Center(
        child: Text('count: $_count   (space to increment, Ctrl+C to quit)'),
      ),
    );
  }
}

A runnable counterpart lives at example/counter_quickstart.dart and is covered by test/example/counter_quickstart_test.dart; the test mounts the app and exercises the space-to-increment behavior.

runApp installs the binding, enters raw mode + the alternate screen, detects terminal capabilities (color depth, image protocol, multiplexer), wires input dispatch and the frame scheduler, and renders your widget tree with a diffing ANSI renderer. Unhandled Ctrl+C exits by default.

For a command-sized UI below the shell prompt, pass mode: const TerminalMode.inline(rows: 14). Earlier output stays in the main buffer; Fleury clears its live region on exit so you can print a result. See inline terminal commands or run dart run example/inline_picker.dart.

For a small application without companion packages, run dart run example/core_editor.dart: search a list, edit a value, save with Ctrl+S, and open a compact delete confirmation. Tab/Shift+Tab traverse the controls; arrows connect search and the list. Button is part of core. See sensitive input for masking and lifetime choices.

Button content #

Supply exactly one of text and child. Use text for a plain label and child for composed content; both keep the same bracketed frame, focus and hover styling, disabled behavior, and Enter/Space/click activation.

Button(text: 'Copy', onPressed: copy);

Button(
  onPressed: copy,
  semanticLabel: 'Copy',
  child: Row(
    mainAxisSize: MainAxisSize.min,
    children: [
      Text('[c] ', style: CellStyle(bold: true)),
      Text('Copy'),
    ],
  ),
);

semanticLabel names the action independently of decorative content. With text, it defaults to that text. Without an explicit name, a composed child's semantics remain available. Keep nested interactive controls outside buttons. Use appearance: ButtonAppearance.plain for an unframed action. It keeps the same interaction and accessibility behavior with left-aligned content. Its hover/focus style covers the allocated bounds; wrap it in Align(alignment: Alignment.centerLeft, child: ...) when a stretching layout should keep the hit target at the content width.

What's in the box #

  • Framework: Widget / StatefulWidget / State, BuildContext, Scope, keys + reconciliation, MediaQuery, layout constraints, the RenderObject tree, and a swappable TerminalDriver. The launch support boundary is a modern UTF-8, xterm-compatible POSIX terminal; the native Windows driver is preview-only until real-Windows acceptance lands. fleury_web renders the same tree to a browser DOM cell grid — selectable text and a real accessibility tree, not a canvas bitmap.
  • Layout: Row / Column / Flex, Stack, Align / Center, Padding, SizedBox, Expanded / Flexible, intrinsic sizing, LayoutBuilder.
  • Input + focus: key bindings that fire once per physical press, and held-key state sampled per frame for games (Keyboard.of — key releases negotiated with the terminal automatically, no flags), focus traversal (Tab / directional), dialog focus traps, mouse + pointer routing, paste handling.
  • Navigation: Navigator with routes + an Overlay for modals, menus, tooltips, and toasts.
  • State: setState for local fields, Scope(value, child: ...) for values shared through the tree, and Notifier.notify() for models used across widgets and services. Consume shared state with ScopeBuilder / NotifierBuilder or context.scope<T>() / context.listen(model) in build. See the state-management guide.
  • Animation: AnimationBuilder + Animation (spring-based, value-driven) and FrameBuilder / FrameTicker (frame-indexed), sharing one per-runtime scheduler. See below.
  • Hot reload: state-preserving reload wired for VS Code — see the hot-reload guide.
  • Widgets: the bundled catalog includes inputs, selects, tables, trees, charts, an image widget with Kitty/iTerm2 progressive enhancement, experimental Sixel, and portable glyph rendering, and more.

Design rationale and the phased delivery plan live in the RFC: Fleury framework RFC 0007.

Testing #

The generated scaffold already includes fleury_test. For an existing app, add it as a dev dependency using the same hosted, Git, or path source as the rest of your Fleury packages, then use FleuryTester (the testWidgets analog) from package:fleury_test/fleury_test.dart:

dev_dependencies:
  fleury_test: ^0.1.0
  test: ^1.26.3

For a Git checkout, use the matching sources described in Install from Git.

With the counter above saved as example/counter_quickstart.dart, this test goes in test/counter_quickstart_test.dart:

import 'package:fleury/fleury.dart';
import 'package:fleury_test/fleury_test.dart';
import 'package:test/test.dart';

import '../example/counter_quickstart.dart';

void main() {
  testWidgets('space increments the counter', (tester) {
    tester.pumpWidget(const CounterApp());
    tester.sendKey(const KeyEvent(KeyCode.char(' ')));
    tester.pump();
    expect(tester.renderToString(), contains('count: 1'));
  });
}

For whole-screen regression, assert against a golden:

expect(tester.renderToString(), matchesGolden('counter/initial.txt'));

Missing goldens fail by default. Create or update them with FLEURY_UPDATE_GOLDENS=1 dart test, then review the diff before committing.

Animation #

The animation system lives in two coordinated lanes — value-driven springs (AnimationBuilder + Animation, which retarget from the live value AND velocity so an interruption mid-flight stays smooth instead of snapping) and discrete frame-indexed primitives (FrameTicker / FrameBuilder). Both share a single per-runtime TickerScheduler owned by the TuiBinding that runApp installs at the root; idle apps burn no scheduler CPU.

See animation infrastructure RFC 0010 for the design rationale.

Ready-made widgets #

Spinner()                          // braille (default) or ascii
Spinner(label: 'Connecting')
BlinkingCursor()                   // ~500 ms on/off block cursor

Drop them in anywhere — they're built on FrameBuilder and share the scheduler with every other animation in the app.

Custom animations #

Most animations are one widget. AnimationBuilder<T> owns an Animation, retargets whenever value changes, and disposes itself — no StatefulWidget, no didUpdateWidget, no dispose. The engine is a spring, so a retarget mid-flight continues from the live value and velocity instead of restarting:

// A number that springs toward its target whenever `value` changes.
AnimationBuilder<int>(value, builder: (context, v, child) => Text('$v'))

// A panel that springs open/closed — toggle rapidly and it stays smooth.
AnimationBuilder<int>(
  open ? 30 : 0,
  spring: Spring.snappy,
  child: child,
  builder: (context, width, child) => SizedBox(width: width, child: child),
)

For chained motion, loops, or an animation you drive imperatively, own an Animation<T> in your State and append targets and delays directly:

final _x = Animation<int>(28);

// A toast: slide in, delay, slide out. The returned future fires at the end.
await _x
    .to(0, spring: Spring.snappy)
    .delay(const Duration(seconds: 2))
    .to(
      28,
      curve: Curves.easeIn,
      duration: const Duration(milliseconds: 200),
    );

// A pulsing dot: ping-pong forever; settles to nothing when off-screen.
final _c = Animation<RgbColor>(const RgbColor(120, 0, 0))
  ..loop(
    between: (const RgbColor(120, 0, 0), const RgbColor(255, 60, 60)),
    period: const Duration(milliseconds: 700),
    curve: Curves.easeInOut,
  );

example/animation_recipes.dart is a runnable gallery of these patterns — counter, interruptible panel, toast, badge flash, selection highlight, health color — each mirrored by a widget test in test/example/animation_recipes_test.dart.

For low-frequency frame-indexed animations (spinners, cursor blink, typing indicators), FrameBuilder is more direct:

FrameBuilder(
  interval: const Duration(milliseconds: 80),
  builder: (ctx, frame, elapsed, delta) {
    final frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
    return Text(frames[frame % frames.length]);
  },
)

Animation control #

  • TickerMode(enabled: false, child: ...) — mute tickers in a subtree (hidden tab, modal background). Internal elapsed-time state continues to advance so re-enabling resumes at the correct value; no replay of missed frames.
  • AnimationPolicy.disabled on the binding — show final animation states immediately in tests and non-interactive hosts.

Examples #

  • example/animation_showcase.dart — every animated widget demonstrated together.
  • example/chat_demo.dart — a Spinner integrated into the message-pane header of the full chat layout.

Color #

rgbColorLerp(a, b, t) interpolates RgbColor channels linearly; feed the result to an Animation<RgbColor> (or read it inside an AnimationBuilder<RgbColor>) to glide a color toward a target. Indexed ANSI colors aren't on a meaningful number line, so animate the RGB value rather than interpolating palette indices.

Bounded text fields #

Use a controller policy for short names, search terms, or confirmation fields:

// Declare this top-level or static: a const policy can't refer to a local
// function (in build()) or an instance method (on your State).
bool printableAscii(int point) => point >= 32 && point <= 126;

final controller = TextEditingController(
  editPolicy: const TextEditPolicy(
    maxCodeUnits: 120,
    allowCodePoint: printableAscii,
  ),
  onEditRejected: (reason) {
    // Show optional feedback without clearing the accepted text.
  },
);
final field = TextInput(controller: controller);

The limit counts UTF-16 code units. It applies before normalization to typing, replacement, paste, IME updates/commits, semantic edits, and assignments. Invalid edits are rejected as a whole; text, selection and undo/redo are preserved. Deletion and empty text remain valid; validate required fields on submission. A segmented paste waits for its final marker and becomes one undo step. An interrupted or invalid paste is discarded completely, with no partial prefix. Unconstrained controllers keep their normal incremental paste behavior. This policy does not replace secret byte limits or erasure requirements.

0
likes
160
points
1.65k
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A Dart-native terminal UI framework with Flutter ergonomics and terminal-native internals.

Repository (GitHub)
View/report issues

Topics

#terminal #tui #ui #framework #cli

License

MIT (license)

Dependencies

characters, ffi, image, meta, stdio, vm_service, watcher

More

Packages that depend on fleury