Skip to main content

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:

  1. Prompt helpers for simple, inline prompts.
  2. Full TUI programs using Model, Msg, and Program.

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 null on cancel; handle null explicitly.
  • Text area prompts use ctrl+s to submit and esc to cancel by default.
  • RenderConfig should match terminal width for display components.

Where to go next