Add interactive prompts and components
Use Bubbles when you need an input, picker, list, table, spinner, progress bar, or another interactive piece without building its state machine from scratch. Simple programs can call a prompt helper; larger TEA apps can compose the underlying models.
Choose how to use a component
There are two primary ways to use Bubbles:
- Prompt helpers for simple, inline prompts.
- Full TUI programs using
Model,Msg, andProgram.
Quick Start
import 'package:artisanal/bubbles.dart';
import 'package:artisanal/terminal.dart';
Future<void> main() async {
final terminal = StdioTerminal();
final model = TextInputModel(
prompt: 'Name: ',
placeholder: 'Ada Lovelace',
);
final value = await runTextInputPrompt(model, terminal);
terminal.writeln('Hello ${value ?? 'anonymous'}');
}
Prompt Helpers
Prompt helpers run a single bubble and return a value. They are designed to run inline so they work well inside CLI command flows.
Select and Multi-Select
import 'package:artisanal/bubbles.dart';
import 'package:artisanal/terminal.dart';
Future<void> main() async {
final terminal = StdioTerminal();
final choice = await runSelectPrompt(
SelectModel<String>(
items: ['alpha', 'beta', 'gamma'],
title: 'Choose one',
),
terminal,
);
final multi = await runMultiSelectPrompt(
MultiSelectModel<String>(
items: ['red', 'green', 'blue'],
title: 'Choose many',
),
terminal,
);
terminal.writeln('choice=$choice');
terminal.writeln('multi=$multi');
}
Search and Anticipate
import 'package:artisanal/bubbles.dart';
import 'package:artisanal/terminal.dart';
Future<void> main() async {
final terminal = StdioTerminal();
final result = await runSearchPrompt(
SearchModel<String>(
items: ['apple', 'banana', 'cherry'],
title: 'Search fruit',
),
terminal,
);
final anticipated = await runAnticipatePrompt(
AnticipateModel(
prompt: 'City: ',
suggestions: ['Paris', 'London', 'Tokyo'],
),
terminal,
);
terminal.writeln('search=$result');
terminal.writeln('anticipate=$anticipated');
}
Text Area
Text areas run in full-screen mode by default, with ctrl+s to submit and esc to cancel.
import 'package:artisanal/bubbles.dart';
import 'package:artisanal/terminal.dart';
Future<void> main() async {
final terminal = StdioTerminal();
final model = TextAreaModel(placeholder: 'Notes...');
final value = await runTextAreaPrompt(
model,
terminal,
options: textareaPromptOptions,
);
terminal.writeln(value ?? 'No input');
}
TextAreaModel and TextInputModel now share the same mouse-selection
semantics used by the widget editors:
- single click places the cursor
- double click selects the current word
- triple click selects the current logical line
Under the hood, editor-core text navigation uses TextDocument, which now
caches line-start offsets so repeated offset-to-position and
position-to-offset mapping stays cheap even as the buffer changes.
Password Prompts
import 'package:artisanal/bubbles.dart';
import 'package:artisanal/terminal.dart';
Future<void> main() async {
final terminal = StdioTerminal();
final password = await runPasswordPrompt(
PasswordModel(prompt: 'Password: '),
terminal,
);
final confirmed = await runPasswordConfirmPrompt(
PasswordConfirmModel(prompt: 'Confirm: '),
terminal,
);
terminal.writeln('password=$password');
terminal.writeln('confirm=$confirmed');
}
Wizard
import 'package:artisanal/bubbles.dart';
import 'package:artisanal/terminal.dart';
Future<void> main() async {
final terminal = StdioTerminal();
final wizard = WizardModel(steps: [
WizardStep.textInput(key: 'name', prompt: 'Name: '),
WizardStep.confirm(key: 'confirm', prompt: 'Continue?'),
]);
final result = await runWizardPrompt(wizard, terminal);
terminal.writeln('result=$result');
}
Compose in a Program
import 'package:artisanal/tui.dart';
import 'package:artisanal/bubbles.dart';
class DemoModel with ComponentHost implements Model {
TextInputModel input;
SpinnerModel spinner;
DemoModel({TextInputModel? input, SpinnerModel? spinner})
: input = input ?? TextInputModel(prompt: 'Search: '),
spinner = spinner ?? SpinnerModel();
Cmd? init() => spinner.tick();
(Model, Cmd?) update(Msg msg) {
final (_, inputCmd) = updateComponent(
input,
msg,
(next) => input = next,
);
final (_, spinnerCmd) = updateComponent(
spinner,
msg,
(next) => spinner = next,
);
return (
this,
Cmd.batch([
if (inputCmd != null) inputCmd,
if (spinnerCmd != null) spinnerCmd,
]),
);
}
String view() => '${spinner.view()} ${input.view()}';
}
Future<void> main() async {
await runProgram(DemoModel());
}
Display Components
Display components are non-interactive renderers (tables, lists, blocks). Use them directly or through Console.components.
import 'package:artisanal/bubbles.dart';
import 'package:artisanal/artisanal.dart';
void main() {
final io = Console();
final component = BulletList(
items: ['one', 'two', 'three'],
bullet: '-',
renderConfig: io.renderConfig,
);
component.writelnTo(io);
}
Model Catalog
- Input:
TextInputModel,TextAreaModel,PasswordModel,PasswordConfirmModel,AnticipateModel - Selection:
SelectModel,MultiSelectModel,ListModel,SearchModel,ConfirmModel - Navigation:
ViewportModel,TableModel,PaginatorModel,HelpModel - Progress/Time:
SpinnerModel,ProgressModel,TimerModel,StopwatchModel,CountdownModel - File system:
FilePickerModel - Flow:
WizardModel
Prompt Options
Prompt helpers accept optional ProgramOptions:
import 'package:artisanal/bubbles.dart';
import 'package:artisanal/terminal.dart';
import 'package:artisanal/tui.dart';
Future<void> main() async {
final terminal = StdioTerminal();
final options = ProgramOptions(altScreen: false, fps: 30);
final value = await runTextInputPrompt(
TextInputModel(prompt: 'Value: '),
terminal,
options: options,
);
terminal.writeln('value=$value');
}
Use textareaPromptOptions for full-screen editing and promptProgramOptions for inline prompts.
Things to keep in mind
- Prompt helpers return
nullon cancel; handlenullexplicitly. - Text area prompts use
ctrl+sto submit andescto cancel by default. RenderConfigshould match terminal width for display components.
Where to go next
- docs_index.md - Full documentation index
- tui.md
- io_components.md