Skip to Content
Your first app

Your first app

Build a counter you can change while it runs. You will create a project, add Noir, run the app, press a button, and edit the label without losing the count.

1. Create the project

Create a Dart console application and move into it:

dart create -t console noir_demo cd noir_demo

The template writes bin/noir_demo.dart. That is the one file you will edit.

2. Add Noir

dart pub add noir

3. Write the screen

Replace everything in bin/noir_demo.dart with this complete file:

bin/noir_demo.dart
import 'package:noir/noir.dart'; void main() => runTuiApp(const CounterApp(), enableMouse: true); class CounterApp extends StatefulWidget { const CounterApp({super.key}); @override State<CounterApp> createState() => _CounterAppState(); } class _CounterAppState extends State<CounterApp> { int _count = 0; void _increment() => setState(() => _count++); @override Widget build(BuildContext context) => Container( padding: const EdgeInsets.all(1), child: Column( crossAxisAlignment: CrossAxisAlignment.start, spacing: 1, children: [ Text('Count: $_count'), Button(autofocus: true, label: '+ Add one', onPressed: _increment), ], ), ); }

Three parts matter here:

  • runTuiApp mounts the widget tree and owns the terminal session. enableMouse: true lets the Button receive a click.
  • CounterApp is immutable configuration. Noir can rebuild it as often as it needs to.
  • _CounterAppState owns _count. setState changes that value and asks Noir to rebuild this widget.

4. Run it and press the button

Start the entry point through Noir’s development runner:

dart run noir:run bin/noir_demo.dart

The count appears above a focused button. Press Enter or Space, or click the button. Each activation adds one.

The first app after one activationView source

 Count: 1

  + Add one 

dart run noir:run bin/noir_demo.dartPress Space once on the focused button. Captured at 40×6 through NoirDriver (NOIR_DRIVE=1), headless.

If the keyboard does nothing, focus the terminal window. The Button has autofocus: true, so Noir gives it primary focus when the tree mounts. Press Ctrl+C to exit; Noir restores the terminal modes it owns.

For a normal launch without the source watcher, run dart run bin/noir_demo.dart instead.

5. Change the running app

Leave the runner open. Change one word in the Text widget and save the file:

bin/noir_demo.dart
- Text('Count: $_count'), + Text('Total: $_count'),

The label changes while the current count and the focus stay in place.

The same app after the label editView source

 Total: 1

  + Add one 

dart run noir:run bin/noir_demo.dartPress Space once, then change Count: to Total:. Captured at 40×6 through NoirDriver (NOIR_DRIVE=1), headless.

The widget declaration changed. The State object that owns the count did not, so the count survived. That boundary is the whole idea: State, identity, and ownership explains it in full.

The runner watches Dart files under lib/ and beside the entry point. It asks the Dart VM to swap the changed sources, then tells Noir to rebuild, lay out, and paint the retained tree. If an edit does not compile, the last good app keeps running: fix the source and save again. Reload does not rerun main() or initState(), and some structural changes need a restart. See the development runner for its options and restart boundaries.

What you built

You built a terminal counter that responds to Enter, Space, and mouse clicks. The widget declares the screen; its State owns the count, and setState requests a rebuild after each increment. Changing the label through hot reload keeps that same state and the current count.

Where to go next

  • Layout in terminal cells — compose a larger screen.
  • Build a task list — learn hooks and reactive state through a five-lesson exercise. It uses the noir_signals companion.
  • Examples — runnable apps, including a larger counter with arrow keys and explicit pointer handling.
Last updated on