Howdy CLI is a declarative terminal UI library for Dart. It provides a robust set of interactive widgets, terminal I/O management, and a flexible theming API to build beautiful command-line workflows.
NOTE
I was heavily inspired by and heavily ripped off Huh?. If you're language agnostic, that library is battle tested and likely more stable.
final results =Form.send(title:'Charmburger ordering', [
Note([
Text(
'\n🍔 Charmburger\n',
style:TextStyle(
bold:true,
foreground:Color.cyan,
),
),
Text('Welcome to _Charmburger™_\nHow may we take your order?\n'),
], next:true),
Page([
Select<String>(
'Choose your burger',
help:'At Charm we truly have a burger for everyone.',
options: [
Option(label:'Charmburger Classic', value:'Charmburger Classic'),
Option(label:'Chickwich', value:'Chickwich'),
Option(label:'Fishburger', value:'Fishburger'),
Option(
label:'Charmpossible™ Burger',
value:'Charmpossible™ Burger',
),
],
validator: (v) => v =='Chickwich'?'no chicken today, sorry':null,
key:'burger',
),
Multiselect<String>(
'Toppings',
help:'Choose up to 4.',
options: [
Option(label:'Lettuce', value:'Lettuce'),
Option(label:'Tomatoes', value:'Tomatoes'),
Option(label:'Charm Sauce', value:'Charm Sauce'),
Option(label:'Jalapeños', value:'Jalapeños'),
Option(label:'Cheese', value:'Cheese'),
Option(label:'Vegan Cheese', value:'Vegan Cheese'),
Option(label:'Nutella', value:'Nutella'),
],
defaultValue: ['Lettuce', 'Tomatoes'],
validator: (v) {
if (v.isEmpty) return'at least one topping is required';
if (v.length >4) return'Maximum 4 toppings allowed';
returnnull;
},
key:'toppings',
),
]),
// ...
]);- Extendible: Use built in widgets or write your own.
- Declarative Components: Easily build forms and interactive prompts.
- Theming System: Centralized styling for colors, borders, icons, and text.
- Low-Level Terminal Control: Exposed primitive terminal functions for cursors, escape sequences, and screen buffers.
Howdy CLI comes with a rich set of built-in widgets for both collecting input and displaying output.
Widgets that expect input back from the user.
A single-line text input field with support for an optional placeholder and inline validation.
A multi-line text input field that renders with a pipe-bordered region and supports word wrapping.
A single-choice list of options rendered as a scrollable menu with a highlighted cursor.
A multiple-choice list of options where users can toggle individual items before submitting.
A boolean yes/no confirmation prompt rendered as a single-line toggle.
An interactive directory browser that lets users navigate the filesystem and select a file.
Widgets that don't expect input back from the user.
A simple widget for rendering a single block of styled terminal text.
A bounded, styled box with configurable padding and automatic text wrapping for displaying information panels.
A widget for rendering tabular data with customizable borders, column widths, and cell alignment.
A spinner is an animated progress indicator for displaying indeterminate loading states inline.
A SpinnerTask is a convenience wrapper around Spinner that ties an async task to the spinner lifecycle, completing automatically when the task resolves.
A read-only display widget that renders a directory tree structure with icons and indentation.
A scrollable, keyboard-navigable list for displaying collections of items as output.
Forms are used to create multi-widget workflows that orchestrates multiple Page widgets, managing state, validation, and navigation between pages.
A lightweight inline annotation widget for displaying contextual hints or messages within a Form.
A container that groups a set of widgets into a single form page, managing focus traversal across its children.
A tappable button widget used to advance to the next step in a Form or multi-page flow.
You can easily build custom widgets by extending one of the three base classes provided by the framework.
InputWidget<T>: Extend this for interactive input widgets. You must override:T get value: Returns the widget's current value.KeyResult handleKey(KeyEvent event): Processes individual keystrokes. ReturnKeyResult.consumedif the key changed state (triggers a re-render),KeyResult.ignoredif it wasn't relevant, orKeyResult.donewhen the widget is finished collecting input.String build(IndentedStringBuffer buf): Renders the visual state intobufand returns it as a string.
DisplayWidget: Extend this for output-only widgets that display content without capturing input. Onlybuildneeds to be overridden.Widget<T>: The sealed base class both of the above extend. Implement this directly only if you need fully custom lifecycle behaviour.
classMyWidgetextendsInputWidget<String> {
MyWidget('My Widget title');
String _input ='';
@overrideStringget value => _input;
@overrideKeyResulthandleKey(KeyEvent event) {
if (event caseCharacterKey(:final char)) {
_input += char;
returnKeyResult.consumed;
}
if (event ==SpecialKey(Key.enter)) {
returnKeyResult.done;
}
returnKeyResult.ignored;
}
@overrideStringbuild(IndentedStringBuffer buf) {
buf.writeln('Enter something: $_input');
return buf.toString();
}
}Howdy CLI features a centralized hierarchical theming API. This ensures consistent styling across all widgets, managing focus and blur states automatically.
To use one of the included themes, set the static current field.
Theme.current =Theme.dracula();
Out of the box, the following themes are provided:
Theme.charm(): The default, vibrant theme.Theme.standard(): A minimal theme using standard base terminal colors.Theme.dracula(): Based on the popular Dracula color scheme.Theme.base16(): A 16-color ANSI palette theme.Theme.catppuccin(): Based on the Catppuccin Mocha color scheme.
You can create a custom theme by instantiating the Theme class and providing specific FieldStyles, GroupStyles, and FormStyles.
final myTheme =Theme(
focused:FieldStyles(
text:TextStyles(
prompt:TextStyle(foreground:Color.blue, bold:true),
),
// ...other styles
),
blurred:FieldStyles(/*...*/),
);Themes are composed using structural styling primitives:
- Colors: ANSI and RGB colors via the
Colorclass (e.g.,Color.redLight,Color.magenta). - Text Styles: Apply foreground/background colors, boldness, dimming, and italics to text using
TextStyle. Can be quickly applied using the.style(...)string extension. - Border Styles: Configure characters used for widget bounding boxes with
BorderType(e.g., standard, rounded). - Padding/Margin: Control spatial rules with
EdgeInsets(e.g.,EdgeInsets.all(1)). - Icons: Easily retrieve cross-platform terminal icons (like
Icon.dot,Icon.pointer,Icon.check) via theIconclass.
The framework exposes raw terminal manipulation via the Terminal singleton and helper methods for advanced command-line UI creation.
- Raw Mode (
enableRawMode,runRawMode): Bypasses terminal line buffering to read individual keystrokes (useful for custom interactive widgets). - Key Event Parsing (
readKeySync): Automatically translates complex ANSI escape sequences intoKeyEventobjects likeSpecialKey(Key.arrowUp). - Screen Buffer Management (
updateScreen,clearScreen): Effortlessly redraws the screen without flickering, automatically counting physical lines for efficient screen erasure. - Cursor Control: Directly control the cursor shape (
CursorShape.blinkingBar,CursorShape.steadyBlock) and position (cursorUp,cursorDown,cursorHide,cursorShow). - Erasing: Exposed constants and methods for clearing the screen (
eraseLine,eraseScreenDown, etc.). - Signal Handling: Robust teardown logic ensures that when the script receives a
SIGINT(Ctrl+C), raw mode is disabled and the cursor is cleanly restored.













