Full-screen & inline UIs
A terminal UI can become the user’s workspace or help them finish one step in a command. Fleury supports both: full-screen uses the terminal viewport; inline reserves a smaller region beneath the current cursor.
The examples below run a real Fleury form inside illustrated terminal buffers. In the full-screen tab, run the command to watch the form take over. Finish or cancel to return to the shell; Run again resets the browser illustration.
Notice how the earlier output stays above the form. Review grows the live region; finishing clears it and leaves a lasting summary.
Notice how the form replaces the shell while it runs. Finish or cancel: the earlier output returns, with the command’s result underneath.
The form uses the terminal’s own text and background colors, with accents and focus highlights. Inline mode sets the UI’s space; it doesn’t require a background fill or a particular color theme.
Choose for the workflow
Section titled “Choose for the workflow”| Full-screen | Inline | |
|---|---|---|
| Good fit | Editors, dashboards, file managers, multi-pane workspaces | Pickers, setup forms, confirmations, bounded progress displays |
| Space | The terminal’s width and height | Full width and an explicit number of rows, clamped to the terminal height |
| Earlier shell output | Hidden while the app runs; restored on exit | Remains in the main buffer; reserving space can move it into scrollback |
| When Fleury exits | Restores the previous screen | Clears the live region |
| Lasting result | Print after awaiting runApp | Print after awaiting runApp |
Choose full-screen when users need room to explore, navigate between views, or keep several things in view. Choose inline when the UI is one step in a shell workflow and the surrounding output provides useful context. Duration alone doesn’t decide it: a compact status display can run for a long time.
For ordinary command output, a report, or a single text prompt, plain CLI output may be enough. Before you design around inline mode, check what it doesn’t support yet.
What the terminal does
Section titled “What the terminal does”Most modern terminal emulators provide a main screen buffer, where your shell and command output live, and an alternate screen buffer for interactive apps. Entering the alternate screen hides the main screen; leaving it reveals the previous contents. This is the mechanism behind Fleury’s default full-screen mode. It fills the terminal viewport, not the desktop display.
Inline stays in the main buffer. Fleury asks the terminal for the cursor’s position, reserves rows there, and confines painting and clearing to that region. It translates mouse and text-caret positions into the widget tree’s coordinates. The widgets receive a smaller viewport and use the same layout and input APIs.
Earlier output
$ project-setupProject setup
[ Your interactive UI ]Earlier output
$ project-setup
Setup complete.
$ ▌Earlier output
$ project-setupEarlier output
$ project-setup
[ Your interactive UI ]Earlier output
$ project-setup
Setup complete.
$ ▌Screen placement is separate from input handling. Both modes use raw input so Fleury can react to individual keys, manage focus, and edit text. The shell waits while the app runs. Mouse reporting is optional in both modes; enabling it affects native terminal selection throughout the window. Terminal-specific selection modifiers and scrollback behavior still apply.
The alternate screen normally has no scrollback of its own. A ScrollView or
ListView scrolls content inside your UI, independently of terminal scrollback.
Redrawing frames does not create a durable history of the interaction.
Choose the mode at startup
Section titled “Choose the mode at startup”Use the same form in either mode. Inline asks for a row count; full-screen uses the terminal’s size. Mouse input is optional in both.
import 'package:fleury/fleury.dart';import 'package:fleury_samples/samples.dart';
Future<void> main() async { await runApp( FleuryApp( title: 'Project setup', home: InlineSetup(onComplete: (_) => exitApp()), ), mode: const TerminalMode.inline(rows: 21, mouse: true), enableHotReload: false, );}import 'package:fleury/fleury.dart';import 'package:fleury_samples/samples.dart';
Future<void> main() async { await runApp( FleuryApp( title: 'Project setup', home: InlineSetup(onComplete: (_) => exitApp()), ), mode: const TerminalMode.fullScreen(mouse: true), enableHotReload: false, );}InlineSetup is the sample form used above. These commands disable hot reload
so their startup and completion code run once
(why).
Finish back at the prompt
Section titled “Finish back at the prompt”The form calls exitApp() when it finishes. Fleury removes the UI, restores
terminal input, and completes runApp. Print the command’s result after awaiting
it so the summary stays in the terminal.
Shutdown and signals shows cleanup and interrupt handling, including how to preserve the command’s exit status.
Resize an inline UI
Section titled “Resize an inline UI”Choose Review →: the form requests 21 rows, up from 17. Choose Back to shrink it again. The earlier output stays above the live region.
runApp provides the terminal session automatically. Read it from the build
context, then request a new height from a callback:
View resize.dart
import 'package:fleury/fleury.dart';import 'package:fleury_samples/samples.dart';
Future<void> main() async { await runApp( const FleuryApp(title: 'Project setup', home: ResizingSetup()), mode: const TerminalMode.inline(rows: 17, mouse: true), enableHotReload: false, );}
class ResizingSetup extends StatelessWidget { const ResizingSetup({super.key});
@override Widget build(BuildContext context) { final session = context.scope<TerminalSession>(); return InlineSetup( onComplete: (_) => exitApp(), onStepChanged: (step) async { await session.resizeInline(step.rows); }, ); }}The browser resizes the host element around the real form. In a terminal,
session.resizeInline(rows) reallocates the live region and repaints it.
Mix UI and CLI steps
Section titled “Mix UI and CLI steps”Run setup, finish the form, then type y at the text prompt to open a fresh form. Complete that one and answer n to finish the command. Both summaries stay in the output.
Each form here is a separate Fleury browser mount. Between forms, the page owns
the text prompt. The native equivalent awaits runApp, reads ordinary CLI input,
then calls runApp again:
View repeat.dart
import 'dart:io';
import 'package:fleury/fleury.dart';import 'package:fleury_samples/samples.dart';
Future<void> main() => repeatSetup();
Future<void> repeatSetup() async { while (true) { InlineSetupResult? result; final outcome = await runApp( FleuryApp( title: 'Project setup', home: InlineSetup( onComplete: (value) { result = value; exitApp(); }, ), ), mode: const TerminalMode.inline(rows: 21, mouse: true), enableHotReload: false, );
if (outcome.signal case final signal?) { exitCode = switch (signal) { AppSignal.interrupt => 130, AppSignal.terminate => 143, AppSignal.hangup => 129, }; return; } if (result == null) return;
stdout.writeln(result!.summary); stdout.write('Open setup again? [y/N] '); await stdout.flush(); if (stdin.readLineSync()?.trim().toLowerCase() != 'y') return; }}Try the native flow from the packages/samples directory of a Fleury
checkout:
cd packages/samplesdart run bin/samples.dart inline --repeatAwait each UI before opening the next. Keep shared data in the command’s host; each UI starts fresh. To return to the same UI after another program runs, use handoff.
Output and subprocesses
Section titled “Output and subprocesses”On macOS and Linux, runApp captures stray output by default while it owns
the terminal and replays the retained output after exit. Calling print during the UI is
not a way to append permanent lines above it. Render live messages as widgets;
print the command’s final result after awaiting runApp.
Let another program use the terminal
Section titled “Let another program use the terminal”Suppose the setup form offers View in pager. Selecting it opens the generated
configuration in less, a separate terminal pager. Pressing q closes it and
brings you back to the same form, with your name, template, and review step preserved.
This is a recording of the real native app launching less, not a browser
simulation. Try it yourself from packages/samples:
dart run bin/samples.dart inline --handoffChoose Review →, then View in pager. The preview uses a temporary file
and removes it when the pager closes; no project files are created. Add
--full-screen to try the same handoff from the alternate screen.
runWithHandoff makes this work by temporarily giving the child control of the
terminal. Fleury pauses its painting and output capture, restores normal input,
and releases its screen. Without the handoff, the two programs would compete
for input and the child’s output could disappear into Fleury’s captured logs.
When the child exits, Fleury reclaims the terminal and redraws the existing UI.
await session.runWithHandoff(() async { final pager = await Process.start( 'less', ['--', previewFile.path], mode: ProcessStartMode.inheritStdio, ); await pager.exitCode;});The native host reads session from context.scope<TerminalSession>() and creates
previewFile before the handoff. The form itself needs no process or terminal
code. This also works for an editor or an interactive Git command. In inline
mode, child output can remain above the resumed UI; full-screen mode re-enters
the alternate screen. On macOS and Linux, a Ctrl+Z that nothing in the app
handles suspends the app through the same release-and-resume lifecycle, and
fg brings it back. That happens only when a job-control shell, such as your
interactive shell, started the app, since only the shell can bring it back. A
terminal profile, tmux pane, or ssh -t host app that runs the app directly
leaves Ctrl+Z an ordinary key. A few launchers look like a shell’s job
without being one, such as fish’s exec app in a macOS terminal tab; there a
suspended app stays stopped until you continue its process group from
another terminal: kill -CONT -- -<pid>, with the pid of the process the
launcher started.
Your code can ask for the same suspension with session.suspend(): for
example, from a key of its own when a focused text field takes every Ctrl+Z
for undo (see Key handling).
The returned future completes once fg has brought the app back. Where the app
isn’t a job the shell can suspend (no job-control shell started it, or it runs
under fleury serve, fleury shell, or Windows), session.supportsSuspend is
false and suspend() does nothing.
Try the native behavior
Section titled “Try the native behavior”The browser examples illustrate the buffers around a real shared Fleury form.
They cannot exercise a terminal emulator’s cursor reports, scrollback, or raw
input. From packages/samples in a Fleury checkout, compare the actual host
modes:
dart run bin/samples.dart inlinedart run bin/samples.dart inline --full-screenNeither command writes project files. Resize the terminal during review, go back, and finish. Earlier output should remain available and ordinary shell input should work again. This recording captures the inline run on a native PTY:
Current inline support
Section titled “Current inline support”Inline mode currently runs on macOS and Linux. It needs terminal input/output and cursor position reporting. If the terminal cannot report its cursor position, Fleury restores its modes and reports the error.
Inline mode does not yet support:
- Natural content height. The UI asks for an explicit number of rows and
can change it with
session.resizeInline. - Permanent lines above a running region, such as a transcript that grows
above a live prompt. Render live messages as widgets, and print lasting
output after
runAppreturns. - Keeping the final frame. Fleury clears the live region on exit, so print
the command’s result after awaiting
runApp. - Native image protocols. Images fall back to glyph rendering.
When exit overlaps a resize, Fleury gets a fresh cursor report before clearing the region. If the terminal stops answering, cleanup still restores terminal modes and leaves uncertain rows alone to protect earlier output.
Browser embeds take their size from the containing element. A served native preview also has a host-controlled viewport; neither demonstrates a native inline allocation. See Serving and embedding.
Next steps
Section titled “Next steps”- App entry points covers
runAppand the host services available to your widgets. - Layout and Lists & scrolling help your UI adapt to the available space.
- Deployment & distribution covers shipping your command.