Getting started
Fleury is a Dart UI framework for terminal apps that also run in a browser. If
you know Flutter, the model is the same: a tree of widgets, build, and
setState, painted to a grid of character cells instead of pixels.
The status panel below is live: a real Fleury app compiled to JavaScript and running in your browser, from the same Dart you’ll run in a terminal.
In about five minutes you’ll build this panel, run it in your terminal, and open it in a browser.
Prefer to try it before installing anything? Fleury Pad runs a Dart editor and a live app with hot reload in your browser.
Before you start
Section titled “Before you start”- The Dart SDK, version 3.10.4 or later. Check
with
dart --version. - Dart’s global package directory on your
PATH, so thefleurycommand you install in step 1 can be found. That is~/.pub-cache/binon macOS and Linux;dart pub global activateprints the exact path if it’s missing.
1. Install the CLI and create a project
Section titled “1. Install the CLI and create a project”Until the packages are published, activate the CLI and scaffold against the current Git repository:
dart pub global activate --source git \ https://github.com/danReynolds/fleury.git \ --git-path packages/fleuryfleury create my_app --dependency-source=gitcd my_appfleury create writes a small application, its first widget tests, and VS Code
settings, then runs dart pub get. To use an existing empty directory, run
fleury create . --dependency-source=git from inside it.
The app has one UI dependency: fleury. It includes layout, text editing,
focus, navigation, theming, and the full catalog of controls, tables, charts,
tabs, and dialogs. fleury_test is a development dependency for the generated
tests. Core and targets explains the
optional browser and tooling companions.
2. Run the app
Section titled “2. Run the app”A Fleury program is a widget tree handed to runApp. FleuryApp is the
lightweight app shell: its home screen gets app-wide navigation and theming.
You build everything beneath it with the familiar Widget / build /
setState model.
From the project directory, run:
fleury runYou can also use dart run fleury run without a globally activated CLI.
Press Enter or click Increment and watch the counter change. The
application lives in lib/app.dart; bin/run_app.dart is the small native
entrypoint that fleury run starts.
Leave it running and try a hot reload: in lib/app.dart, change the Count:
label to Clicks: and save. The running app updates in place, and the count
keeps its value. Press Ctrl+C in the terminal to quit.
In VS Code with the Dart extension,
you can press F5 instead: the generated launch configuration runs the app in an
integrated terminal under the debugger, and saving reloads it the same way.
Save-to-reload under fleury run needs macOS or Linux; on Windows, launch
through VS Code’s debugger. A plain dart run bin/run_app.dart also works; Hot reload
explains the difference.
3. Drive state over time
Section titled “3. Drive state over time”The scaffold already demonstrates event-driven state with its counter. Replace
it with time-driven state: a status screen that ticks once per second.
setState marks the widget dirty; Fleury rebuilds only that path,
re-lays-out, and diffs the new cell grid against the old, so an idle frame costs
nothing.
Replace lib/app.dart with:
import 'dart:async';
import 'package:fleury/fleury.dart';
class MyApp extends StatelessWidget { const MyApp({super.key});
@override Widget build(BuildContext context) { return const FleuryApp(title: 'Status monitor', home: StatusApp()); }}
class StatusApp extends StatefulWidget { const StatusApp({super.key});
@override State<StatusApp> createState() => _StatusAppState();}
class _StatusAppState extends State<StatusApp> { var _tick = 0; Timer? _timer;
@override void initState() { super.initState(); _timer = Timer.periodic(const Duration(seconds: 1), (_) { setState(() => _tick++); }); }
@override void dispose() { _timer?.cancel(); super.dispose(); }
@override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(1), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text('uptime: ${_tick}s'), const SizedBox(height: 1), ProgressBar(value: (_tick % 60) / 60), ], ), ); }}This edit changes the app’s structure and starts a timer in initState, which a
reload doesn’t rerun, so restart instead: in a fleury run session, press
Ctrl+G to open the debug shell, then F5. If F5 doesn’t reach the app, as on
a Mac laptop’s top row or in VS Code’s integrated terminal, see
When F5 doesn’t restart.
If you launched with VS Code’s F5, use the editor’s Restart action instead.
The uptime line ticks every second, and the progress bar fills over the minute.
The scaffold’s test describes the counter you just replaced, so update it at the same time:
import 'package:fleury_test/fleury_test.dart';import 'package:my_app/app.dart';import 'package:test/test.dart';
void main() { testWidgets('shows the status screen', (tester) { tester.pumpWidget(const MyApp()); expect( tester.renderToString(emptyMark: ' '), contains('uptime: 0s'), ); });}Then run dart test.
4. Compose widgets
Section titled “4. Compose widgets”The widget library is broad — browse the reference for
every widget’s API and, for most, a live example. Charts, meters, and the rest
compose like any other widget. Replace lib/app.dart with the stateless panel behind
the live example at the top of this page — the whole file: MyApp stays, the
timer and its dart:async import go:
import 'package:fleury/fleury.dart';
class MyApp extends StatelessWidget { const MyApp({super.key});
@override Widget build(BuildContext context) { return const FleuryApp(title: 'Status monitor', home: StatusApp()); }}
class StatusApp extends StatelessWidget { const StatusApp({super.key});
@override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(1), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ Gauge(value: 0.62, label: 'CPU'), Gauge(value: 0.81, label: 'MEM'), Gauge(value: 0.34, label: 'DISK'), const SizedBox(height: 1), Sparkline(data: const [3, 5, 4, 8, 6, 9, 7, 5, 8, 6]), ], ), ); }}Restart once more using the same method, since StatusApp is now stateless.
From here, edits reload on save: change the CPU value from 0.62 to 0.95 and
save, and the gauge updates in place.
Change the test expectation from uptime: 0s to CPU, then run dart test
again. Those are fixed sample values. To make the panel live, feed them from
state and call setState — exactly the way step 3 drove the progress bar from
_tick. State, lifecycle, composition: the same three moves, however rich the
screen gets.
5. Open it in a browser
Section titled “5. Open it in a browser”On macOS and Linux, you can preview the same app in a browser tab without any
changes. fleury serve --spawn starts the native app and streams its frames to
the page:
fleury serve --spawn dart --enable-vm-service=0 run bin/run_app.dartOpen the printed URL. That’s your panel, in a browser, streamed from the native
process. Saving a file reloads it here too: --enable-vm-service=0 is what lets
the served app reload, and without it the session runs but never reloads.
Stop the preview with Ctrl+C in the terminal running serve.
6. Optional: ship a browser bundle
Section titled “6. Optional: ship a browser bundle”serve needs the native process running. To ship the app as a static page with
no backend, compile it to JavaScript instead. The app code in the bundle must
use the web-safe fleury_core.dart library, which includes the entire
widget catalog, instead of the native fleury.dart library: dart2js compiles
code that imports dart:io, but that code throws when it runs in the browser.
Add fleury_web and web to pubspec.yaml, merging these entries into the
existing dependencies: section (do not add a second section):
dependencies: fleury_web: git: url: https://github.com/danReynolds/fleury.git path: packages/fleury_web web: ^1.1.1Then run dart pub upgrade, so every Fleury package resolves from the same
commit.
lib/app.dart uses no native-only APIs, so it becomes web-safe by switching its
Fleury import to fleury_core.dart, which includes the whole widget catalog.
Nothing else in the file changes, and
bin/run_app.dart keeps running it in the terminal:
import 'package:fleury/fleury_core.dart';The browser entrypoint mounts the same MyApp into a page element:
import 'package:fleury_web/fleury_web.dart';import 'package:my_app/app.dart';import 'package:web/web.dart' as web;
Future<void> main() async { await mountApp( () => const MyApp(), into: web.document.getElementById('app')!, );}The page gives that element an explicit size and a monospace font:
<!doctype html><html> <head> <meta charset="utf-8"> <title>Status monitor</title> </head> <body> <div id="app" style="width: 40ch; height: 12em; font-family: monospace"></div> <script src="app.js"></script> </body></html>Compile, then open web/index.html in a browser:
dart compile js web/main.dart -o web/app.js -O2The output is static files you can host anywhere.
Deployment & distribution covers hosting, and when
a local serve preview fits better than a bundle.
Where to next
Section titled “Where to next”- Tutorial: a filterable list — build an interactive app with a text field, live filtering, and tests.
- Guides — task-focused guides with live examples: layout, lists, forms, navigation, key handling, testing, and more.
- Widgets & state — how
build,setState, and the widget lifecycle fit together. - Widget reference — every widget, with generated API tables and, for most, a live example.
- Hot reload — what survives a reload, and when to restart.
- Built for agents — the semantic tree that lets tests and AI agents drive the UI.