Coming from Flutter
Fleury is deliberately familiar if you know Flutter: apps are widget trees,
local widget state lives in State, build returns widgets, and setState
schedules a rebuild. The difference is the surface. Flutter lays out pixels and usually
leans on Material/Cupertino; Fleury lays out terminal-style cells and can
paint the same tree to a real terminal, a browser embed, or a served browser
session.
This is not a Flutter renderer or a Material porting layer. It is the same programming model, tuned for keyboard-first tools, dashboards, agent UIs, and structured terminal/browser surfaces.
The short version
Section titled “The short version”| Flutter instinct | Fleury answer |
|---|---|
Widget, State, BuildContext, setState | Same model, same names. |
runApp(const MyApp()) | Same name for terminal/native apps. |
MaterialApp / WidgetsApp | FleuryApp(title:, home:, theme:) is the lightweight app shell. |
| logical pixels | integer cells (EdgeInsets.all(1), not 8.0). |
| Material controls | Fleury includes primitives, controls, tables, charts, forms, and agent surfaces in one package. |
| Flutter web build | mountApp(() => const FleuryApp(title: 'My app', home: MyHomeScreen()), into: host) in a client-side dart2js bundle. (fleury serve previews a native app in a browser during development, on macOS and Linux; it isn’t a web build.) |
flutter run, then r / R | fleury run hot reloads when you save a file (macOS and Linux). For a hot restart, press Ctrl+G to open the debug shell, then F5; see Hot reload. |
The same counter, twice
Section titled “The same counter, twice”Here is the classic Flutter counter next to its Fleury port. The highlighted lines are the whole diff:
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: Counter()));
class Counter extends StatefulWidget { const Counter({super.key});
@override State<Counter> createState() => _CounterState();}
class _CounterState extends State<Counter> { int _count = 0;
@override Widget build(BuildContext context) { return Scaffold( body: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text('Count: $_count'), const SizedBox(height: 16), ElevatedButton( onPressed: () => setState(() => _count++), child: const Text('Increment'), ), ], ), ), ); }}import 'package:fleury/fleury.dart';
void main(List<String> args) => runApp( const FleuryApp(title: 'Counter', home: Counter()), args: args, mode: const TerminalMode(mouse: true),);
class Counter extends StatefulWidget { const Counter({super.key});
@override State<Counter> createState() => _CounterState();}
class _CounterState extends State<Counter> { int _count = 0;
@override Widget build(BuildContext context) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text('Count: $_count'), const SizedBox(height: 1), Button( text: 'Increment', autofocus: true, onPressed: () => setState(() => _count++), ), ], ), ); }}The Fleury version runs in your browser. Press Enter, or click Increment. Then change the code, say the label or the step, and press Run:
What changed:
MaterialAppbecomesFleuryApp, imported frompackage:fleury/fleury.dart.- There is no
Scaffold. A terminal app composes its own chrome, soCenteris the screen’s root. SizedBox(height: 16)in pixels becomesheight: 1: one row of cells.ElevatedButton(child: Text(...))becomesButton(text: ...).TerminalMode(mouse: true)enables terminal clicks, as in a generatedfleury createapp. The entrypoint also forwards command-lineargsto the host.autofocus: truegives the button keyboard focus, so Enter presses it. Terminal apps are keyboard-first; see Input is keyboard-first.
Everything else, StatefulWidget, State, setState, build, and Column,
is the same code.
Imports and entrypoints
Section titled “Imports and entrypoints”Most Flutter developers expect one app root and one rendering target. Fleury has the same widget tree, but different host entrypoints:
| Use case | Import | Entrypoint |
|---|---|---|
| Native terminal app | package:fleury/fleury.dart | runApp(const FleuryApp(title: 'My app', home: MyHomeScreen())) |
| Client-side browser embed | package:fleury/fleury_core.dart, package:fleury_web/fleury_web.dart | mountApp(() => const FleuryApp(title: 'My app', home: MyHomeScreen()), into: host) |
| Local browser preview of a native app, during development (macOS and Linux) | terminal app imports | fleury serve --spawn dart --enable-vm-service=0 run bin/run_app.dart |
MyHomeScreen below is the first screen inside the shell. In a project made
with fleury create my_app, MyApp already builds FleuryApp: pass
const MyApp() directly to runApp, or () => const MyApp() to mountApp,
without another shell.
For a browser embed, the entrypoint is a tiny web file:
import 'package:fleury/fleury_core.dart';import 'package:fleury_web/fleury_web.dart';import 'package:web/web.dart' as web;
Future<void> main() async { final host = web.document.getElementById('app')!; await mountApp( () => const FleuryApp(title: 'My app', home: MyHomeScreen()), into: host, );}Client-side browser bundles can only use web-safe code: dart2js compiles
dart:io imports, but that code throws when it runs in the browser. Import
package:fleury/fleury_core.dart for shared UI, including every widget.
Explicit local filesystem access uses package:fleury/fleury.dart. FileBrowser and
FilePicker work with a browser-safe FileSource, such as MemoryFileSource;
LogRegion displays log entries supplied by your app. LocalFileSource and
code that reads host files or launches processes need a native app. With
fleury serve, that work runs on the machine hosting the app; the browser
displays the app host’s files and logs.
Same API, same mental model
Section titled “Same API, same mental model”These transfer with little or no adjustment:
| Area | Carries over |
|---|---|
| Core model | Widget, StatelessWidget, StatefulWidget, State, build, setState, BuildContext |
| Keys | Key, ValueKey, UniqueKey, GlobalKey |
| Layout | Column, Row, Expanded, Flexible, Spacer, Stack, Positioned, Padding, Center, Align, Container, ConstrainedBox, AspectRatio, SizedBox, Wrap, IntrinsicWidth, IntrinsicHeight, LayoutBuilder |
| Async | FutureBuilder, StreamBuilder, AsyncSnapshot, ConnectionState |
| Navigation guards | PopScope |
| Focus and pointer input | FocusNode, Focus, FocusScope, GestureDetector, MouseRegion |
| Inherited data | Theme.of, MediaQuery.of, DefaultTextStyle |
| Text | Text, RichText, TextSpan |
The table is intentionally boring: most of the muscle memory is valid.
Renamed or simplified
Section titled “Renamed or simplified”| Flutter | Fleury | Why |
|---|---|---|
TextStyle | CellStyle | A cell has foreground/background color and terminal attributes such as bold, dim, underline, and inverse (reverse-video). It does not have fonts. |
BoxConstraints | CellConstraints | Constraints are integer cells; null represents unbounded. |
Offset / Size | CellOffset / CellSize | Coordinates and dimensions are whole cells. |
ChangeNotifier / notifyListeners() | Notifier / notify() | An ordinary Dart model announces changes to its consumers. |
Navigator.push(context, MaterialPageRoute(...)), pop, popUntil(predicate) | context.push(screen), context.pop(result), context.popUntil<HomeScreen>() | Routes are widgets: you push the screen itself, with no Route object or route names. popUntil takes the widget type of the screen to stop at. |
await tester.pumpWidget(...), find.text(...), findsOneWidget | tester.pumpWidget(...), tester.exists(text(...)), tester.button('Save') | testWidgets comes from package:fleury_test and runs on a fake clock, so mounting is synchronous: no await. Find controls by role and label, or assert on the rendered cells with tester.renderToString(). |
AnimatedBuilder / ListenableBuilder | NotifierBuilder | Rebuild from a typed notifier or any Listenable, including an animation. |
ValueListenableBuilder | NotifierBuilder or context.listen | Read a ValueNotifier through the same APIs as other notifiers. |
TweenAnimationBuilder | AnimationBuilder | Animate a value toward a new target when it changes. |
SingleChildScrollView | ScrollView | A scrollable viewport around one child. |
Shortcuts / Actions / Intent | KeyBindings / KeySequence, or AppCommand in a CommandScope | A key sequence maps directly to a callback; no Intent layer. For a named action that buttons, shortcuts, and the command palette share, declare an AppCommand with its shortcuts in a CommandScope or FleuryApp(commands:); see Commands. |
InheritedWidget / InheritedNotifier | Scope<T> | One tree-local primitive: the type argument is the key, context.scope<T>() or ScopeBuilder<T> reads it, and a Notifier value notifies readers through the scope. Scope<T>.create lets the scope own the object. |
For state, keep setState for a widget’s own fields. Use Scope to share values
with descendants, read through ScopeBuilder<T> or context.scope<T>().
Models used by widgets and services extend Notifier; widgets observe them
with NotifierBuilder or context.listen(model). ValueNotifier<T> handles
notification automatically when its value changes. The
State management guide demonstrates all
three levels.
EdgeInsets keeps the familiar constructors (all, symmetric, only), but
the values are cells:
Padding( padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 1), child: Text('two columns, one row'),)Where Flutter instincts need adjustment
Section titled “Where Flutter instincts need adjustment”Cells are not pixels
Section titled “Cells are not pixels”Fleury layout is integer cell layout. Width is columns; height is rows. A
terminal cell is usually taller than it is wide, so a visually square box often
uses an AspectRatio around 2.0, not 1.0:
That sounds small, but it changes what “polish” means. You tune information density, alignment, wrapping, keyboard flow, and semantic structure before you think about pixel-perfect spacing.
Lists include a row cursor
Section titled “Lists include a row cursor”ListView.builder requires a finite itemCount and an
itemBuilder: (context, index, highlighted) => ... that returns a widget for
each index. The extra highlighted argument identifies the current row;
Fleury applies the default highlight automatically. Only visible rows are
mounted, so it remains suitable for long lists.
By default, selectable: true gives the list a row cursor. Arrow keys move it
and report onFocusedItemChanged; Enter or a completed row click calls
onSelect. Moving the cursor or scrolling does not select an item. Set
selectable: false for scrolling without a row cursor; controls inside rows
still handle their own input. Use ScrollView for one child rather than a
collection. Lists & scrolling demonstrates
these interactions and controllers.
FleuryApp is deliberately smaller than MaterialApp
Section titled “FleuryApp is deliberately smaller than MaterialApp”You do not port MaterialApp, CupertinoApp, or WidgetsApp wholesale.
runApp installs terminal host services such as MediaQuery, focus, pointer
routing, the root Overlay, log capture, and scheduling. FleuryApp owns the
application concerns: its theme and command/status/extension scopes sit above
its Navigator, so pushed routes keep the same app context.
void main(List<String> args) => runApp( FleuryApp( title: 'My app', theme: ThemeData( colorScheme: const ColorScheme(primary: RgbColor(0x3D, 0xDC, 0x97)), ), home: const MyHomeScreen(), ), args: args, mode: const TerminalMode(mouse: true),);title is required, but it only labels the app’s node in the semantic tree
that tests and agents read. It doesn’t set a terminal or browser window title.
Small one-screen tools can still pass a bare widget to runApp. For a custom
navigation topology, use FleuryApp(child: ...) and place explicit Navigator
widgets in that shell. child does not create an implicit route stack.
Input is keyboard-first, pointer-aware
Section titled “Input is keyboard-first, pointer-aware”Flutter’s Shortcuts / Actions stack is intentionally simpler in Fleury: a
key sequence maps straight to a callback. Type in the field, then press Ctrl+S
or Esc. The bindings sit above the focused field, so its keys reach them, and
their labels feed the hint bar:
Pointer input uses GestureDetector and MouseRegion. Native clicks need
TerminalMode(mouse: true); hover needs TerminalMode(mouseMotion: true),
which also enables clicks. Browser hosts already deliver pointer motion. Keep
every important workflow reachable from the keyboard too.
Animation is value-first
Section titled “Animation is value-first”Flutter’s Animation<T> is usually a read-only listenable driven by an
AnimationController. Fleury’s Animation<T> is the mutable value you retarget:
final fill = Animation(0.0);
fill.to(0.8, spring: Spring.snappy);fill.loop(between: (0.3, 1.0));For the common “animate when this state value changes” case, use
AnimationBuilder, the counterpart of TweenAnimationBuilder. Press Enter to
flip selected and watch the value ease toward its new target:
For entrance/exit effects, use Animate or AnimatedVisibility:
Text('Saved').animate().fadeIn().slideIn();AnimatedVisibility( visible: open, enter: Effects.expand(), child: Panel(title: 'Details', child: details),);Routes are widgets, not route names
Section titled “Routes are widgets, not route names”With FleuryApp(home: ...) there is no MaterialPageRoute and no named-route
table. Push the widget you want to show, pop with an optional result, or pop
back to a screen by its type:
Use PopScope for the same kind of “can this route close?” guard you would use
in Flutter.
Not there, and what to use instead
Section titled “Not there, and what to use instead”| Flutter | Use instead |
|---|---|
InkWell / ripples | GestureDetector, MouseRegion, focus styles, and key hints. |
Scaffold, AppBar, Material layout chrome | Compose Fleury widgets directly; terminal apps usually want denser app-specific chrome. |
CustomScrollView / slivers / GridView | ListView, ListView.builder, ScrollView, Wrap, or purpose-built table/tree widgets. |
Plain Builder | A small StatelessWidget. |
FittedBox / FractionallySizedBox / OverflowBox | LayoutBuilder, ConstrainedBox, explicit cell sizing, and wrapping/clipping behavior. |
Hero / route-shared element transitions | AnimatedVisibility, route transitions, or simpler terminal-native motion. |
Porting checklist
Section titled “Porting checklist”- Start with the app’s state and widget structure; most
StatefulWidget/setStatecode ports directly. - Replace
MaterialAppwithFleuryApp(title:, home:, theme:); keeprunAppfocused on the terminal host. - Translate dimensions from pixels to cells. Remove
doublespacing habits. - Replace Material controls with Fleury widgets or focused app-specific widgets.
- Add keyboard paths first, then pointer affordances.
- Decide the host: terminal
runApp, or a static browser bundle withmountApp.fleury servepreviews a native app in a browser during development on macOS and Linux; it isn’t a way to host one.
The fastest way in is the tutorial, then Widgets & state and App entry points.