NAME

Term::Fabulous - Full-screen terminal user interfaces with layouts, widgets, keyboard and mouse

SYNOPSIS

use v5.32;
use warnings;
use feature 'signatures';
no warnings 'experimental::signatures';

use Clay::XS qw(sizing_grow CLAY_TOP_TO_BOTTOM);
use Term::Fabulous;
use Term::Fabulous::Widget::Box;
use Term::Fabulous::Widget::Text;
use Term::Fabulous::Widget::TextField;

my $root = Term::Fabulous::Widget::Box->new(
        layout => {
                layout_direction => CLAY_TOP_TO_BOTTOM,
                sizing           => { width => sizing_grow(), height => sizing_grow() },
                padding          => { left => 2, right => 2, top => 1, bottom => 1 },
                child_gap        => 1,
        },
);

my $greeting = Term::Fabulous::Widget::Text->new(
        text       => 'What is your name? (Enter to greet, Ctrl+C to quit)',
        text_color => [ 230, 230, 230, 255 ],
);
my $name = Term::Fabulous::Widget::TextField->new( placeholder => 'Your name' );
$root->add_child( $greeting, $name );

$name->on(
        Submit => sub ($event) {
                $greeting->text( 'Hello, ' . $event->value . '!' );
                return;
        }
);

my $ui = Term::Fabulous->new( root => $root, width => 80, height => 24 );
$ui->interaction->set_focused_widget($name);
$ui->run;    # returns after Ctrl+C, SIGINT, SIGTERM or SIGHUP

The picture shows examples/showcase.pl, a demo program of the distribution (see Term::Fabulous::Examples):

A Term::Fabulous program: a sign-up form with text fields, radio buttons, a dropdown, a slider, a check box and buttons, a chart of requests per second with a translucent notification, an event log and text in several scripts

DESCRIPTION

Term::Fabulous builds full-screen terminal applications in Perl. You describe the screen as a tree of widgets (boxes, text, buttons, input fields, tables, charts, scrollable areas and canvases), in Perl code or in a layout file written in KDL, a small configuration language (https://kdl.dev). Term::Fabulous sizes and positions the widgets with the Clay layout engine, draws them with 24-bit colors through the termbox2 library, and turns key presses, mouse clicks and terminal resizes into events your code reacts to.

Highlights:

This class is the application object: it owns the widget tree, opens the terminal, runs the event loop, draws a frame whenever something changed (checking 30 times per second) and dispatches input events. It is a subclass of Clay::UI. It reaches the terminal through a terminal object (Term::Fabulous::Role::Terminal): the real one by default, or Term::Fabulous::Terminal::Memory in tests, which "step" drives without an event loop.

DOCUMENTATION

The documentation has four parts. If you are new to Term::Fabulous, start with the manual's first page and its first program.

REQUIREMENTS

Perl 5.32.1 or later, a C compiler to build Term::Fabulous::Termbox (termbox2 is compiled into the distribution), a terminal with 24-bit colors and a UTF-8 locale. Imager is recommended, for Term::Fabulous::Widget::Image, and Imager::File::SIXEL with it, for Term::Fabulous::Widget::Sixel. See "REQUIREMENTS" in Term::Fabulous::Manual.

CONSTRUCTOR

new

my $ui = Term::Fabulous->new(
        root   => $root_widget,
        width  => 80,
        height => 24,
        mouse  => 1,
);

Creates the application object. The terminal is not touched until "run" (or "step"), so you can create the object, set the focus and add timers first. Unknown parameters die (Unrecognised parameters for Term::Fabulous constructor: 'colour').

METHODS

run

$ui->run;

Opens the terminal in full-screen mode (or inline, see "INLINE MODE"), runs the event loop until it is stopped, and restores the terminal. It returns nothing. A terminal that "step" opened is used as it is, without a second Start, and closed when run returns.

While it runs:

The loop stops, and run returns, when:

While run is active it handles these three signals itself; when it returns or dies, %SIG holds again what the program had set for them before, except for a signal that an IO::Async::Signal the program added to the loop still watches.

If code running inside the loop dies (a listener, a timer), run restores the terminal first and then dies with the same error, so the message is readable on the normal screen. The terminal is also restored when code inside the loop calls exit. When the terminal input ends without a SIGHUP reaching the process (a terminal that went away while the process is not in its session, or input from a pipe), run dies with Term::Fabulous: the terminal was closed. After run has returned or died, the object can be used again and run can be called again. While run is active, a second call (from a listener or a timer inside the loop) dies with Term::Fabulous: run is active already; it cannot be called again until it returns and leaves the active run as it was, just as "step" refuses to run inside it.

run dies with the terminal's error when the terminal cannot be opened. For the real terminal, these start with Term::Fabulous::Terminal::Termbox:, for example when the process has no controlling terminal (tb_init failed: No such device or address, or tf_init_inline failed: ... in inline mode), when the terminal reports a size of 0 columns or rows, and in inline mode when the terminal does not report its cursor position within a second (the terminal did not report its cursor position; ...); see "open" in Term::Fabulous::Terminal::Termbox.

When the locale's character set is not UTF-8, run warns (at every call): Term::Fabulous: the locale's character set is not UTF-8; wide characters will be misaligned.

step

my $frames = $ui->step;
my $frames = $ui->step( paced => 1 );

One turn of "run" without an event loop, for tests: usually with a Term::Fabulous::Terminal::Memory as the "terminal", whose input methods queue keys, clicks and resizes. step

  1. opens the terminal if it is not open yet, sets "width" and "height" to its size and fires Start, as run does (but there is no loop: "loop" is the one of the last run, or undef);
  2. reads every event that waits and dispatches it exactly as run does: a key to the focused widget (after which Tab and Shift+Tab also move the focus), a mouse event to the widget under the pointer (a press focuses it first), a wheel notch to the scroll box under the pointer; see "EVENTS" and "KEYBOARD AND FOCUS". Ctrl+C fires its KeyPress but has no loop to stop;
  3. applies a resize at once, firing the Resize pair, instead of waiting for the size to settle;
  4. draws frames as long as one is due: for a click, one frame for the press and one for the release, and another one when a frame changed widgets (hover and press events fire while a frame is drawn). Without paced, a frame that only shows pointer motion is drawn at once; with paced => 1, it waits like in run (see there), measured with the clock of "new".

Returns the number of frames it drew, 0 when nothing was due. The terminal stays open; run closes it, or close it with $ui->terminal->close. The real terminal (Term::Fabulous::Terminal::Termbox) also closes itself when it is destroyed while open, so a program that steps and then dies or ends gets its shell back as it was. Unknown options die, and so does a call from inside run. When frames keep being due after 100 rounds, because a widget changes in every frame, step dies. When the terminal input has ended ("end_input" in Term::Fabulous::Terminal::Memory), step dies like run.

terminal

my $terminal = $ui->terminal;
$ui->terminal->press_key('Enter');

Returns the terminal object given to "new", or the Term::Fabulous::Terminal::Termbox created by default. Read only.

loop

my $loop = $ui->loop;
$ui->loop->stop;

Returns the IO::Async::Loop of the most recent "run", or undef before the first run; it is set before the Start event fires. Call $ui->loop->stop from a listener or timer to end run. The loop is IO::Async's process-wide loop: the same object that IO::Async::Loop->new returns, which is why notifiers added to IO::Async::Loop->new before run run inside it.

interaction

my $tracker = $ui->interaction;
$ui->interaction->set_focused_widget($widget);
my $focused = $ui->interaction->get_focused_widget;

Returns the Clay::UI::Interaction object of this UI. It holds the keyboard focus and the hover and press state of the widgets. Use it to move the focus from code (set_focused_widget, focus_next, focus_previous) and to ask which widget has it (get_focused_widget). See "FOCUS" in Term::Fabulous::Manual::Events. Inherited from Clay::UI.

root

my $root = $ui->root;

Returns the root widget given to "new". Read only.

width

my $columns = $ui->width;
$ui->width(100);

Accessor. Returns the current layout width in columns: the terminal width while "run" is active. Writing sets the layout width from the next frame on and returns the new value; a value that is not a positive number dies. run sets the width to the terminal width when it starts and after every terminal resize, so a written value lasts only until then. Inherited from Clay::UI.

height

my $rows = $ui->height;
$ui->height(40);

Accessor. Returns the current layout height in rows: the terminal height while "run" is active, or the rows of the inline region in inline mode. Writing works like for "width". Inherited from Clay::UI.

inline

my $rows = $ui->inline;    # undef for the full screen

Returns the inline constructor parameter. Read only.

mouse

my $enabled = $ui->mouse;

Returns whether the mouse is reported: the mouse constructor parameter, or its default (1, or 0 in inline mode). Read only.

kitty_keyboard

my $wanted = $ui->kitty_keyboard;

Returns the kitty_keyboard constructor parameter, or its default (1). Read only.

stop_on_ctrl_c

my $stops = $ui->stop_on_ctrl_c;

Returns the stop_on_ctrl_c constructor parameter as 1 or 0, or its default (1). Read only.

kitty_keyboard_active

my $in_use = $ui->kitty_keyboard_active;

Returns 1 while the open terminal uses the kitty keyboard protocol: from the start of "run" (or the first "step"), before Start fires, until the terminal is closed, when the terminal speaks the protocol and kitty_keyboard is 1. Returns 0 otherwise, and always while the terminal is closed. Read only.

output_mode

my $mode = $ui->output_mode;    # TB_OUTPUT_TRUECOLOR

Returns the output_mode constructor parameter, always TB_OUTPUT_TRUECOLOR. Read only.

pointer_state

my $pointer = $ui->pointer_state;    # { x => 12, y => 3, down => 0 } or undef

Returns where the mouse pointer was last reported: a new hash reference with the cell coordinates x and y and down, which is 1 while the left button is held and 0 otherwise. Returns undef until the first mouse report. The terminal reports every move, so this is the live mouse position as of the last report. Term::Fabulous passes it to Clay with every frame, which derives the hover and press state of the widgets from it. When the button went down and up again between two frames, each state gets a frame of its own, so a click is never too fast to press a widget. down follows the left button only: the release of another button does not end a press.

theme

my $theme = $ui->theme;
$ui->theme('light');
$ui->theme( Term::Fabulous::Theme->from_file('ocean.kdl') );

Accessor for the theme (see "new"). Without an argument it returns the Term::Fabulous::Theme object; with one it sets the theme, a theme object or a built-in name, makes every widget read its colors and border styles again, and draws a frame, screen background included. Anything else dies. See "THEMES" in Term::Fabulous::Manual::Looks.

invalidate

$ui->invalidate;

Asks for a frame: the screen is laid out and drawn again at the next tick of the frame timer, even if Term::Fabulous saw no change. Returns the object. Frames are drawn by themselves whenever a widget was changed through its methods, input arrived or the terminal was resized, so most programs never need this; call it when something the frame depends on changed behind Term::Fabulous's back, for example state a custom widget reads while it draws without calling mark_changed (see telling Term::Fabulous that something changed).

now

my $seconds = $ui->now;

The current time in seconds on the application's clock: the clock of "new", by default Time::HiRes::time. Widgets that animate read it instead of the system clock, so a test or the screenshot harness can move it; see "request_frame_at".

request_frame_at

$ui->request_frame_at( $ui->now + 0.1 );

Asks for a frame at a time on the clock (see "now"): at the first tick of the frame timer at or after it, a frame is drawn as if "invalidate" had been called, and step draws one when the time has come. Several requests keep the earliest time. Every frame forgets the request, so something that animates asks again from the frame it is drawn in. Returns the object. Dies unless the argument is a number.

This is how the widgets that move by themselves (a Term::Fabulous::Widget::Spinner, an indeterminate Term::Fabulous::Widget::ProgressBar) are drawn without timers of their own; see "ANIMATION" in Term::Fabulous::Widget::Display to write one.

find_by_id

my $field = $ui->find_by_id('name');

Returns the widget with the given id, searching the whole tree from the root, or undef when there is none; see "find_by_id" in Term::Fabulous::Widget. Dies when the root widget has no find_by_id method (every Term::Fabulous widget has one).

termbox_draw_interval

my $seconds = $ui->termbox_draw_interval;    # 1/30

Returns the time between two checks for a due frame in seconds: 1/30. A frame is drawn at a tick only when something changed since the last one. Read only.

termbox_resize_debounce_interval

my $seconds = $ui->termbox_resize_debounce_interval;    # 1/10

Returns how long, in seconds, the terminal size must stay unchanged before a resize is applied and the Resize events are fired: 1/10. While a resize is pending, no frames are drawn. Read only.

draw

$ui->draw;

Lays out and draws one frame immediately, into the cell target of the "terminal". run calls it whenever a frame is due, so programs do not need it; see "invalidate" to ask for a frame instead. With the real terminal, it only has a visible effect while the terminal is open. See "draw" in Term::Fabulous::Render.

bounding_box, scroll_state, scroll_to

my $box   = $ui->bounding_box($widget);       # { x, y, width, height } in cells, or undef
my $state = $ui->scroll_state($scroll_box);   # { position, viewport, content }
$ui->scroll_to( $scroll_box, { y => -10 } );  # ten rows down from the top

bounding_box returns where the last frame placed a widget. scroll_state and scroll_to read and set the scroll position of a scroll container such as a Term::Fabulous::Widget::ScrollBox, in cells: 0 at the top and left, negative when scrolled down or right. Inherited from Clay::UI; see "bounding_box" in Clay::UI, "scroll_state" in Clay::UI and "scroll_to" in Clay::UI, and the recipe Scroll a ScrollBox from code.

after_draw

$ui->after_draw( sub { ... } );

Queues a code reference to call once after the next frame has been drawn, for work that needs the layout of that frame. See "after_draw" in Term::Fabulous::Render.

Other inherited methods

The class inherits further methods from Clay::UI (render, widget_for, measure_text, max_element_count, max_measure_text_cache_word_count, laid_out_revision), from Term::Fabulous::Render (last_frame, the Term::Fabulous::Render::Frame of the last frame, and clip_rect) and from Term::Fabulous::Render::Canvas (invalidate_canvases). cell_target returns the cell target of the "terminal". Applications rarely need them; they are documented on those pages.

EVENTS

"run" and "step" fire these events. Each one bubbles from the widget it is fired on to the root, as described in "Return values and bubbling" in Term::Fabulous::Manual::Events.

Widgets fire further events themselves: Change and ValidityChange from the input widgets, Submit from Term::Fabulous::Widget::TextField, Change and Submit from Term::Fabulous::Widget::ColorPicker, Activate from Term::Fabulous::Widget::Button, Close from Term::Fabulous::Widget::Dialog, Answer from Term::Fabulous::Widget::Prompt and Term::Fabulous::Widget::FileDialog, Choose from Term::Fabulous::Widget::Menu, Select from accordions, tabs and Term::Fabulous::Widget::DocumentTabs, which also fires TabClose and TabAdd, CanvasResize from canvases, SeriesHover from charts, the table events (CursorMove, SelectionChange, RowActivate, SortChange, FilterChange, PageChange, Expand, Collapse, ColumnsChange) from Term::Fabulous::Widget::Table, and Clay::UI's OnPress, OnRelease, OnHoverStart, OnHoverStopped, OnFocus, OnBlur and OnScroll. The complete list is in "Event reference" in Term::Fabulous::Manual::Events.

KEYBOARD AND FOCUS

Three keys have a fixed meaning. Their KeyPress is fired first, like for any other key, and then:

A focused widget that has a captures_tab method returning true keeps Tab and Shift+Tab for itself: the focus does not move, and the widget's own key handling decides what the keys do (a code editor indents with them). See "Keys Term::Fabulous handles itself" in Term::Fabulous::Manual::Events.

Listeners cannot prevent these actions.

When the left mouse button is pressed (not dragged), the widget under the pointer (on text, the Text widget) gets the focus, or its nearest ancestor that can take it. When there is none, the focus is cleared, so clicking an empty area leaves a text field and closes an open dropdown. This happens before the Mouse event is fired. A widget that has a keeps_focus_on_click method returning true, or that is inside such a widget, leaves the focus where it is when it is pressed: a click on the title of a Term::Fabulous::Widget::MenuBar opens the menu without taking the focus from the text field the user was typing in, so the menu can give it back when it closes.

See "KEYBOARD" in Term::Fabulous::Manual::Events and "FOCUS" in Term::Fabulous::Manual::Events.

INLINE MODE

my $ui = Term::Fabulous->new( root => $root, width => 80, height => 3, inline => 3 );
$ui->run;
say 'Done.';    # printed below the region

An inline prompt in the three rows below a shell's earlier output: a question, a text field holding Ada Lovelace and a help line

The picture shows the recipe Ask for input below the shell's output, a complete program that asks for a name in three rows below the shell's output.

With inline set to a number of rows, "run" leaves the screen as it is and draws the user interface into that many rows, starting at the line of the cursor (the line below it when text precedes the cursor on its line). The layout is as wide as the terminal and as high as the region; "height" and the Start and Resize events report the region's rows. A region taller than the terminal gets the terminal's height.

Like in full-screen mode, printing to STDOUT while run is active writes over the user interface.

MOUSE WHEEL SCROLLING

Each notch of the mouse wheel scrolls the scroll box under the pointer (for example a Term::Fabulous::Widget::ScrollBox) by three rows, and each notch of a horizontal wheel (or a sideways tilt of the wheel) by three columns. Notches that arrive between two frames are added up and applied when the next frame is drawn. A Mouse event is fired for every notch, before the notch is counted: when a listener calls use_wheel on it ("use_wheel" in Term::Fabulous::Event::Mouse), the notch scrolls no scroll box; what the listener returns does not matter for this. Widgets that scroll themselves, like Term::Fabulous::Widget::TextArea, use the notches they scroll by, so the scroll box around them stays put while they can still scroll.

MODULES

Every module has its own page. They are grouped here by purpose; the modules marked "used internally" are documented for people who extend Term::Fabulous, and programs do not use them directly.

Application

Widgets

Input widgets

Feedback widgets

Tables

Charts

Events

Colors, borders and text

Themes

Extending Term::Fabulous

These modules matter only if you write widget classes that can be built from layout files, or your own terminal or output class.

LIMITATIONS

SEE ALSO

Term::Fabulous::Manual, Term::Fabulous::Cookbook, Term::Fabulous::Examples, Clay::UI, Clay::XS, Term::Fabulous::Termbox, IO::Async, Object::Pad.

BUGS

Please report bugs at https://github.com/davenonymous/perl-term-fabulous/issues.

AUTHOR

davenonymous perl@davenonymous.com

COPYRIGHT AND LICENSE

Copyright 2026 davenonymous

This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.