Skip to content

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.

Live

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.

  • The Dart SDK, version 3.10.4 or later. Check with dart --version.
  • Dart’s global package directory on your PATH, so the fleury command you install in step 1 can be found. That is ~/.pub-cache/bin on macOS and Linux; dart pub global activate prints the exact path if it’s missing.

Until the packages are published, activate the CLI and scaffold against the current Git repository:

Terminal window
dart pub global activate --source git \
https://github.com/danReynolds/fleury.git \
--git-path packages/fleury
fleury create my_app --dependency-source=git
cd my_app

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

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:

Terminal window
fleury run

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

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:

lib/app.dart
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:

test/app_test.dart
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.

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:

lib/app.dart
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.

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:

Terminal window
fleury serve --spawn dart --enable-vm-service=0 run bin/run_app.dart

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

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

Then 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:

lib/app.dart
import 'package:fleury/fleury_core.dart';

The browser entrypoint mounts the same MyApp into a page element:

web/main.dart
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:

web/index.html
<!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:

Terminal window
dart compile js web/main.dart -o web/app.js -O2

The output is static files you can host anywhere. Deployment & distribution covers hosting, and when a local serve preview fits better than a bundle.

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