Search by

mulertech / mterm

This application is a PHP CLI.

Maintainers

Package info

github.com/mulertech/mterm

pkg:composer/mulertech/mterm

Transparency log

Statistics

Installs: 98

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

v2.0.0 2026-09-02 10:01 UTC

This package is auto-updated.

Last update: 2026-09-02 10:05:58 UTC


README

Latest Version on Packagist GitHub Tests Action Status GitHub PHPStan Action Status Total Downloads Test Coverage

This class is a simple class to create a terminal interface for your application.

Installation

Two methods to install MTerm package with composer :

Add to your "composer.json" file into require section :

"mulertech/mterm": "^2.0"

and run the command :

php composer.phar update

Run the command :

php composer.phar require mulertech/mterm "^2.0"

Usage

MTerm provides a simple and elegant way to build interactive command-line interfaces in PHP. Below are the main classes and their methods with usage examples.

Basic Usage

use MulerTech\MTerm\Core\Color;
use MulerTech\MTerm\Core\Terminal;

$terminal = new Terminal();
$terminal->write('Hello, World!');
$terminal->writeLine('Hello with a new line!', Color::Green);

Terminal Class

The main class for interacting with the terminal. It writes through an output and reads through an input reader, both replaceable:

use MulerTech\MTerm\Core\Input\InputReader;
use MulerTech\MTerm\Core\Output\StreamOutput;
use MulerTech\MTerm\Core\Terminal;
use MulerTech\MTerm\Core\TerminalMode;

$terminal = new Terminal();                                  // standard streams
$terminal = new Terminal(new StreamOutput($file));           // into a file
$terminal = new Terminal(new StreamOutput(), new InputReader($stream), new TerminalMode());

Escape sequences are written only when the output is a terminal that accepts them, so a display redirected to a file carries text alone.

Method Reference

Here's a comprehensive guide to all public methods in the Terminal class:

Reading Input

read(string $prompt = null): string

Reads a line of input from the terminal.

$name = $terminal->read('Enter your name: ');
$input = $terminal->read(); // No prompt
readChar(string $prompt = null): string

Reads one whole character, however many bytes it takes.

$char = $terminal->readChar('Continue? (y/n): ');
if ($char === 'y') {
    // Process confirmation
}
readKey(string $prompt = null): KeyPress

Reads one key press. An escape sequence — an arrow, a function key — comes back as the key it names rather than as its bytes.

use MulerTech\MTerm\Core\Input\Key;

$press = $terminal->readKey();

if ($press->is(Key::Up)) {
    // Move the cursor up
}

if ($press->isCharacter()) {
    $typed = $press->character;
}

if ($press->isEndOfInput()) {
    // Nothing left to read
}

Output Methods

write(string $text, Color $color = null, bool $bold = false): void

Writes text to the terminal without a newline.

$terminal->write('Regular text ');
$terminal->write('Red text ', Color::Red);
$terminal->write('Bold blue ', Color::Blue, true);
writeLine(string $text = '', Color $color = null, bool $bold = false): void

Writes text to the terminal followed by a newline.

$terminal->writeLine('First line');
$terminal->writeLine('Success message', Color::Green);
$terminal->writeLine('Error message', Color::Red, true);
$terminal->writeLine(); // An empty line
getOutput(): OutputInterface and getInput(): InputReader

Return the output and the input reader the terminal was built with.

Terminal Control

Screen and cursor are driven by ANSI sequences, without forking a subprocess.

clear(): void

Erases the screen and puts the cursor back at its top left corner.

$terminal->clear();
clearLine(): void

Erases the line the cursor sits on, and returns to its first column.

$terminal->clearLine();
$terminal->write('Replaced content');
moveCursor(int $row, int $column): void

Places the cursor, counting rows and columns from one.

$terminal->moveCursor(1, 1); // Top left corner
hideCursor(): void and showCursor(): void

Hide the cursor while a page is being repainted, and show it again.

$terminal->hideCursor();
$terminal->showCursor();
enableRawMode(): void

Reads keys one by one, without echo. The previous state is saved and restored by a shutdown handler as well as by the signal handlers, so an exception or an interruption never leaves the terminal without echo. Catching the interruption needs ext-pcntl; without it only the shutdown handler stands.

$terminal->enableRawMode();
$press = $terminal->readKey();
$terminal->disableRawMode();
disableRawMode(): void and isRawMode(): bool

Put the terminal back as it was found, and tell whether it is in raw mode.

Utility Methods

supportsAnsi(): bool

Tells whether escape sequences written to the output are interpreted. It answers false when NO_COLOR is set, and when the output is not a terminal.

if ($terminal->supportsAnsi()) {
    $terminal->writeLine('Colors supported', Color::Green);
}

Color

The eight colors every ANSI terminal renders: Color::Black, Color::Red, Color::Green, Color::Yellow, Color::Blue, Color::Magenta, Color::Cyan, Color::White. Passing true as the third argument of write() and writeLine() makes the text bold.

Color::Green->sequence();     // "\033[0;32m"
Color::Green->sequence(true); // "\033[1;32m"
Color::RESET;                 // "\033[0m"

Output Classes

OutputInterface

Destination of everything the library displays: write(string $text): void and isDecorated(): bool.

StreamOutput

Writes to a stream, the standard output unless another one is given. Decoration is detected — NO_COLOR, a stream that is not a terminal — or forced:

$output = new StreamOutput();                       // Standard output
$output = new StreamOutput(fopen('report.txt', 'w')); // No escape sequence written
$output = new StreamOutput(STDOUT, true);           // Decoration forced on

BufferedOutput

Keeps everything in memory, which is how a display is asserted upon in a test.

$output = new BufferedOutput();
$terminal = new Terminal($output);
$terminal->writeLine('Hello');

$output->content(); // "Hello\n"
$output->fetch();   // "Hello\n", and empties the buffer

Input Classes

InputReader

Reads a stream by press rather than by byte: readLine(), readCharacter() — one whole character, accents and emoji included — and readKey().

Key

The keys that carry no printable character of their own: Up, Down, Right, Left, Enter, Escape, Backspace, Delete, Insert, Tab, Space, Home, End, PageUp, PageDown and F1 to F12.

KeyPress

One press: is(Key $key), isCharacter(), isEndOfInput(), and the key and character it carries.

TerminalMode

Raw mode and the guarantee that the terminal comes back from it. Terminal drives it; it is only built directly to be replaced in a test.

Command System

MTerm includes a robust command system for creating and managing terminal commands.

CommandInterface

This interface defines the basic structure for all commands.

class HelloCommand implements CommandInterface
{
    public function getName(): string
    {
        return 'hello';
    }
    
    public function getDescription(): string
    {
        return 'Greets a user';
    }
    
    public function execute(array $args = []): int
    {
        $name = $args[0] ?? 'World';
        $this->terminal->writeLine("Hello, $name!");
        return 0;
    }
}

AbstractCommand

A base class that implements basic functionality for CommandInterface.

class DateCommand extends AbstractCommand
{
    public function __construct(Terminal $terminal) 
    {
        parent::__construct($terminal);
        $this->name = 'date';
        $this->description = 'Shows date/time';
    }
    
    public function execute(array $args = []): int
    {
        $format = $args[0] ?? 'Y-m-d H:i:s';
        $this->terminal->writeLine(date($format));
        return 0;
    }
}

// Usage
$cmd = new DateCommand($terminal);
$cmd->execute(['Y-m-d']); // Shows date in specified format

CommandRegistry

Manages a collection of commands.

$registry = new CommandRegistry();

$registry->register(new HelloCommand($terminal));
$registry->has('hello'); // Check if exists
$command = $registry->get('date'); // Get specific command
$allCommands = $registry->getAll(); // Get all commands
$registry->execute('hello', ['User']); // Execute with arguments

Simple CLI Application

$terminal = new Terminal();
$registry = new CommandRegistry();

// Register commands
$registry->register(new HelloCommand($terminal));

// Main loop
while (true) {
    $input = $terminal->read('> ');
    $parts = explode(' ', $input);
    $commandName = array_shift($parts);
    
    if ($commandName === 'exit') break;
    
    if ($registry->has($commandName)) {
        $registry->execute($commandName, $parts);
    }
}

Application Class

The Application class implements a singleton pattern for managing terminal interactions.

Method Reference

getInstance(): Application

Gets the singleton instance of the Application class.

$app = Application::getInstance();

getTerminal(): Terminal

Returns the Terminal instance.

$terminal = $app->getTerminal();
$terminal->writeLine('Hello!');

getCommandRunner(): CommandRunner

Returns the CommandRunner instance.

$runner = $app->getCommandRunner();
$result = $runner->run('ls -la');

run(): void

Starts the application's main execution loop.

$app = Application::getInstance();
$app->run();

CommandRunner Class

The CommandRunner class provides methods to execute system commands.

Method Reference

run(string $command): array

Executes a command and returns output and return code.

$runner = new CommandRunner();
$result = $runner->run('echo "Hello"');
// Returns ['output' => ['Hello'], 'returnCode' => 0]

runWithStderr(string $command): array

Executes a command and returns stdout, stderr, and return code.

$result = $runner->runWithStderr('ls /nonexistent');
// Returns ['stdout' => '', 'stderr' => 'error message...', 'returnCode' => 1]

runDirect(string $command): int

Executes a command that writes to the terminal itself, and returns its exit code. Deployment logs and test suites are watched as they unfold; capturing their output to display it afterwards would trade that for a tidier summary.

$returnCode = $runner->runDirect('docker compose logs -f');

Combining Classes Example

$app = Application::getInstance();
$terminal = $app->getTerminal();
$runner = $app->getCommandRunner();

$command = $terminal->read('Command: ');
$result = $runner->runWithStderr($command);
$terminal->writeLine($result['stdout']);
if ($result['stderr']) {
    $terminal->writeLine($result['stderr'], Color::Red);
}

Form Classes

AbstractField

__construct(string $name, string $label)

Constructor for the AbstractField class.

$field = new AbstractField('username', 'Username');
$field->setDescription('Enter your username');
$field->setRequired(true);
$field->setDefault('guest');
$field->setTerminal(new Terminal());

getName(): string

Returns the name of the field.

$field->getName(); // Returns 'username'

getLabel(): string

Returns the label of the field.

$field->getLabel(); // Returns 'Username'

getDescription(): ?string

Returns the description of the field.

$field->getDescription(); // Returns 'Enter your username'

setDescription(string $description): self

Sets the description of the field.

$field->setDescription('Enter your username');

isRequired(): bool

Checks if the field is required.

$field->isRequired(); // Returns true

setRequired(bool $required = true): self

Sets whether the field is required.

$field->setRequired(true);

getDefault(): string|int|float|array|null

Returns the default value of the field.

$field->getDefault(); // Returns 'guest'

setDefault(string|int|float|array $defaultValue): self

Sets the default value of the field.

$field->setDefault('guest');

clearErrors(): void

Clears the errors of the field.

$field->clearErrors();

addValidator(ValidatorInterface $validator): self

Adds a validator to the field.

$validator = new NotEmptyValidator();
$field->addValidator($validator);

validate(string|int|float|array|null $value): array

Validates the field value.

$errors = $field->validate(''); // Returns array of errors

processInput(string $input): string|int|float|array

Processes the user input.

$value = $field->processInput('guest'); // Returns processed value

setTerminal(Terminal $terminal): self

Sets the terminal instance for the field.

$field->setTerminal(new Terminal());

CheckboxField

setCheckedValue(string $value): self

Sets the checked value for the checkbox.

$field = new CheckboxField('accept_terms', 'Accept Terms');
$field->setCheckedValue('yes');

setUncheckedValue(string $value): self

Sets the unchecked value for the checkbox.

$field->setUncheckedValue('no');

processInput(string $input): string|int|float|array

Processes the user input for the checkbox.

$value = $field->processInput('yes'); // Returns 'yes'

ColorField

validate(string|int|float|array|null $value): array

Validates the color field value.

$field = new ColorField('favorite_color', 'Favorite Color');
$errors = $field->validate('red'); // Returns array of errors

DateField

setFormat(string $format): self

Sets the date format.

$field = new DateField('birthdate', 'Birthdate');
$field->setFormat('Y-m-d');

getFormat(): string

Returns the date format.

$field->getFormat(); // Returns 'Y-m-d'

processInput(string $input): string|int|float|array

Processes the user input for the date field.

$value = $field->processInput('2022-01-01'); // Returns processed value

validate(string|int|float|array|null $value): array

Validates the date field value.

$errors = $field->validate('2022-01-01'); // Returns array of errors

EmailField

validate(string|int|float|array|null $value): array

Validates the email field value.

$field = new EmailField('email', 'Email');
$errors = $field->validate('user@example.com'); // Returns array of errors

FieldInterface

getName(): string

Returns the name of the field.

getLabel(): string

Returns the label of the field.

getDescription(): ?string

Returns the description of the field.

setDescription(string $description): self

Sets the description of the field.

isRequired(): bool

Checks if the field is required.

setRequired(bool $required = true): self

Sets whether the field is required.

clearErrors(): void

Clears the errors of the field.

setDefault(string|int|float|array $defaultValue): self

Sets the default value of the field.

getDefault(): string|int|float|array|null

Returns the default value of the field.

processInput(string $input): string|int|float|array

Processes the user input.

validate(string|int|float|array|null $value): array

Validates the field value.

setTerminal(Terminal $terminal): self

Sets the terminal instance for the field.

FileField

setAllowedExtensions(array $extensions): self

Sets the allowed file extensions.

$field = new FileField('profile_picture', 'Profile Picture');
$field->setAllowedExtensions(['jpg', 'png']);

setMaxSize(int $bytes): self

Sets the maximum file size.

$field->setMaxSize(1048576); // 1 MB

processInput(string $input): string|int|float|array

Processes the user input for the file field.

$value = $field->processInput('path/to/file.jpg'); // Returns processed value

validate(string|int|float|array|null $value): array

Validates the file field value.

$errors = $field->validate('path/to/file.jpg'); // Returns array of errors

NumberField

setMin(?float $min): self

Sets the minimum value for the number field.

$field = new NumberField('age', 'Age');
$field->setMin(18);

setMax(?float $max): self

Sets the maximum value for the number field.

$field->setMax(99);

setAllowFloat(bool $allowFloat): self

Sets whether to allow floating-point numbers.

$field->setAllowFloat(false);

processInput(string $input): string|int|float|array

Processes the user input for the number field.

$value = $field->processInput('25'); // Returns processed value

validate(string|int|float|array|null $value): array

Validates the number field value.

$errors = $field->validate('25'); // Returns array of errors

PasswordField

isMaskInput(): bool

Checks if the input should be masked.

$field = new PasswordField('password', 'Password');
$field->isMaskInput(); // Returns true

setMaskInput(bool $maskInput = true): self

Sets whether the input should be masked.

$field->setMaskInput(true);

getMaskChar(): string

Returns the mask character.

$field->getMaskChar(); // Returns '*'

setMaskChar(string $maskChar): self

Sets the mask character.

$field->setMaskChar('*');

parseInput(string $input): string

Parses the user input.

$value = $field->parseInput('password'); // Returns parsed value

processInput(string $input = ''): string

Processes the user input for the password field.

$value = $field->processInput('password'); // Returns processed value

RadioField

__construct(string $name, string $label)

Constructor for the RadioField class.

$field = new RadioField('gender', 'Gender');

setMultipleSelection(bool $multipleSelection = true): self

Sets whether multiple selection is allowed.

$field->setMultipleSelection(false);

RangeField

__construct(string $name, string $label)

Constructor for the RangeField class.

$field = new RangeField('rating', 'Rating');

setStep(int $step): self

Sets the step value for the range field.

$field->setStep(1);

getStep(): int

Returns the step value for the range field.

$field->getStep(); // Returns 1

validate(string|int|float|array|null $value): array

Validates the range field value.

$errors = $field->validate(5); // Returns array of errors

SelectField

__construct(string $name, string $label, bool $multipleSelection = false)

Constructor for the SelectField class.

$field = new SelectField('country', 'Country');

setOptions(array $options): self

Sets the options for the select field.

$field->setOptions(['USA', 'Canada', 'UK']);

setMultipleSelection(bool $multipleSelection = true): self

Sets whether multiple selection is allowed.

$field->setMultipleSelection(false);

isMultipleSelection(): bool

Checks if multiple selection is allowed.

$field->isMultipleSelection(); // Returns false

parseInput(string $input): string

Parses the user input.

$value = $field->parseInput('USA'); // Returns parsed value

processInput(string $input = ''): string|array

Processes the user input for the select field.

$value = $field->processInput('USA'); // Returns processed value

renderSelectMultipleField(Terminal $terminal): array

Renders the select field for multiple selection.

$options = $field->renderSelectMultipleField(new Terminal()); // Returns array of options

renderSelectSingleField(Terminal $terminal): string

Renders the select field for single selection.

$option = $field->renderSelectSingleField(new Terminal()); // Returns selected option

validate(string|int|float|array|null $value): array

Validates the select field value.

$errors = $field->validate('USA'); // Returns array of errors

getCurrentOption(): string

Returns the current selected option.

$field->getCurrentOption(); // Returns 'USA'

TextField

setMinLength(int $minLength): self

Sets the minimum length for the text field.

$field = new TextField('username', 'Username');
$field->setMinLength(3);

setMaxLength(?int $maxLength): self

Sets the maximum length for the text field.

$field->setMaxLength(20);

validate(string|int|float|array|null $value): array

Validates the text field value.

$errors = $field->validate('guest'); // Returns array of errors

UrlField

validate(string|int|float|array|null $value): array

Validates the URL field value.

$field = new UrlField('website', 'Website');
$errors = $field->validate('https://example.com'); // Returns array of errors

Form

__construct(Terminal $terminal)

Constructor for the Form class.

$form = new Form(new Terminal());

addField(FieldInterface $field): self

Adds a field to the form.

$field = new TextField('username', 'Username');
$form->addField($field);

handle(): void

Handles the form submission.

$form->handle();

isSubmitted(): bool

Checks if the form has been submitted.

$form->isSubmitted(); // Returns true or false

isValid(): bool

Checks if the form is valid.

$form->isValid(); // Returns true or false

getValues(): array

Returns all form values.

$values = $form->getValues(); // Returns array of values

getValue(string $fieldName): string|int|float|array|null

Returns a specific form value.

$value = $form->getValue('username'); // Returns value of 'username' field

FormRenderer

__construct(Terminal $terminal)

Constructor for the FormRenderer class.

$renderer = new FormRenderer(new Terminal());

renderField(FieldInterface $field): string|array

Renders a field.

$field = new TextField('username', 'Username');
$output = $renderer->renderField($field); // Returns rendered field

renderErrors(array $errors): void

Renders the errors.

$renderer->renderErrors(['Error 1', 'Error 2']);

clear(): void

Clears the terminal screen.

$renderer->clear();

ValidatorInterface

validate(mixed $value): ?string

Validates a value.

class CustomValidator implements ValidatorInterface
{
    public function validate($value): ?string
    {
        return $value === 'valid' ? null : 'Invalid value';
    }
}

AbstractValidator

__construct(string $errorMessage)

Constructor for the AbstractValidator class.

class CustomValidator extends AbstractValidator
{
    public function __construct()
    {
        parent::__construct('Invalid value');
    }
    
    public function validate($value): ?string
    {
        return $value === 'valid' ? null : $this->getErrorMessage();
    }
}

getErrorMessage(): string

Returns the error message.

$validator = new CustomValidator();
$errorMessage = $validator->getErrorMessage(); // Returns 'Invalid value'

ChoiceValidator

__construct(array $choices, bool $strict = true, string $errorMessage = "Selected value is not a valid choice.")

Constructor for the ChoiceValidator class.

$validator = new ChoiceValidator(['option1', 'option2']);

validate(mixed $value): ?string

Validates a value.

$error = $validator->validate('option1'); // Returns null (valid)
$error = $validator->validate('invalid'); // Returns error message (invalid)

DateValidator

__construct(string $format = 'Y-m-d', ?DateTimeInterface $minDate = null, ?DateTimeInterface $maxDate = null, ?string $errorMessage = null)

Constructor for the DateValidator class.

$validator = new DateValidator('Y-m-d');

validate(mixed $value): ?string

Validates a value.

$error = $validator->validate('2022-01-01'); // Returns null (valid)
$error = $validator->validate('invalid-date'); // Returns error message (invalid)

EmailValidator

__construct(string $errorMessage = "Please enter a valid email address.")

Constructor for the EmailValidator class.

$validator = new EmailValidator();

validate(mixed $value): ?string

Validates a value.

$error = $validator->validate('user@example.com'); // Returns null (valid)
$error = $validator->validate('invalid-email'); // Returns error message (invalid)

IpAddressValidator

__construct(bool $allowIPv4 = true, bool $allowIPv6 = true, bool $allowPrivate = true, bool $allowReserved = true, string $errorMessage = "Please enter a valid IP address.")

Constructor for the IpAddressValidator class.

$validator = new IpAddressValidator();

validate(mixed $value): ?string

Validates a value.

$error = $validator->validate('192.168.0.1'); // Returns null (valid)
$error = $validator->validate('invalid-ip'); // Returns error message (invalid)

LengthValidator

__construct(?int $min = null, ?int $max = null, ?string $errorMessage = null)

Constructor for the LengthValidator class.

$validator = new LengthValidator(3, 20);

validate(mixed $value): ?string

Validates a value.

$error = $validator->validate('valid'); // Returns null (valid)
$error = $validator->validate(''); // Returns error message (invalid)

NotEmptyValidator

__construct(string $errorMessage = "This value cannot be empty.")

Constructor for the NotEmptyValidator class.

$validator = new NotEmptyValidator();

validate(mixed $value): ?string

Validates a value.

$error = $validator->validate('valid'); // Returns null (valid)
$error = $validator->validate(''); // Returns error message (invalid)

NumericRangeValidator

__construct(?float $min = null, ?float $max = null, ?string $errorMessage = null)

Constructor for the NumericRangeValidator class.

$validator = new NumericRangeValidator(1, 100);

validate(mixed $value): ?string

Validates a value.

$error = $validator->validate(50); // Returns null (valid)
$error = $validator->validate(200); // Returns error message (invalid)

PatternValidator

__construct(string $pattern, string $errorMessage = "Value does not match required pattern.")

Constructor for the PatternValidator class.

$validator = new PatternValidator('/^[a-z]+$/');

validate(mixed $value): ?string

Validates a value.

$error = $validator->validate('valid'); // Returns null (valid)
$error = $validator->validate('123'); // Returns error message (invalid)

RegexValidator

__construct(string $pattern, string $errorMessage = "This value is not valid.")

Constructor for the RegexValidator class.

$validator = new RegexValidator('/^[a-z]+$/');

validate(mixed $value): ?string

Validates a value.

$error = $validator->validate('valid'); // Returns null (valid)
$error = $validator->validate('123'); // Returns error message (invalid)

Utils Classes

ProgressBar

__construct(Terminal $terminal, int $total = 100, int $width = 50, string $completeChar = '=', string $incompleteChar = '-', Color $color = Color::Green)

Constructor for the ProgressBar class.

$terminal = new Terminal();
$progressBar = new ProgressBar($terminal, 100, 50, '=', '-', Color::Green);

start(): void

Starts the progress bar.

$progressBar->start();

advance(int $step = 1): void

Advances the progress bar by a specific amount.

$progressBar->advance(10);

setProgress(int $current): void

Sets the progress to a specific value.

$progressBar->setProgress(50);

finish(): void

Finishes the progress bar.

$progressBar->finish();

TableFormatter

__construct(Terminal $terminal, Color $headerColor = Color::Green, Color $borderColor = Color::Blue, Color $cellColor = Color::White, int $padding = 1)

Constructor for the TableFormatter class.

$terminal = new Terminal();
$tableFormatter = new TableFormatter($terminal, Color::Green, Color::Blue, Color::White, 1);

renderTable(array $headers, array $rows): void

Formats and renders a table.

$headers = ['Name', 'Age', 'Country'];
$rows = [
    ['John', 25, 'USA'],
    ['Jane', 30, 'Canada'],
    ['Doe', 22, 'UK']
];

$tableFormatter->renderTable($headers, $rows);

Interface Classes

Two components for any CLI presenting states and choices.

Indicator

One checked item, its state, and what to do about it. Four states, each with a shape as well as a color, so a colorless output still tells them apart:

State Shape Color Meaning
Compliant green Conforms to what is expected
Watch yellow Works, but drifts towards a failure worth preventing
Failing red Does not conform, and says how to put it right
Unavailable · black Cannot be checked, so says nothing about conformity

A failing indicator without the label of its remedy is refused, so a panel cannot announce a problem while leaving the reader without a move.

use MulerTech\MTerm\Ui\Indicator;
use MulerTech\MTerm\Ui\IndicatorStatus;

Indicator::compliant('Containers running');
Indicator::watch('Disk at 82%', 'prune the unused images'); // The remedy is optional here
Indicator::failing('TLS certificate expired', 'renew it with certbot');
Indicator::unavailable('Backup age', 'host unreachable');

// When the state is only known at runtime
Indicator::of($status, 'Disk at 82%', 'prune the unused images');

Indicator::of(IndicatorStatus::Failing, 'TLS certificate expired'); // InvalidArgumentException

IndicatorRenderer

Displays indicators on a common alignment, remedies in their own column.

use MulerTech\MTerm\Ui\IndicatorRenderer;

(new IndicatorRenderer($terminal))->render(
    Indicator::compliant('Containers running'),
    Indicator::failing('TLS certificate expired', 'renew it with certbot'),
    Indicator::unavailable('Backup age', 'host unreachable'),
);
✔ Containers running
✘ TLS certificate expired  → renew it with certbot
· Backup age               → host unreachable

Menu

A list of choices, nestable, driven with the arrow keys: / move, ENTER selects, ESC — or q — goes back one level and leaves the menu at its top.

The menu carries the cycle around an action: confirm, run, report a failure, and hand the terminal back. An item states its confirmation rather than asking for one itself, so a destructive move cannot reach the user unconfirmed.

use MulerTech\MTerm\Ui\Menu;
use MulerTech\MTerm\Ui\MenuItem;

$containers = new Menu($terminal, 'Containers');
$containers->add(MenuItem::action('Restart', $restart, 'Restart every container?'));

$menu = new Menu($terminal, 'Production');
$menu->add(MenuItem::action('Status', $status))
    ->add(MenuItem::menu('Containers', $containers));

$menu->run();
Production › Containers

❯ Restart
  Prune images

↑/↓ move   ENTER select   ESC back

MenuItem

One line of a menu: either an action to run, or a menu to enter.

action(string $label, callable $action, string $confirmation = null): MenuItem

An action, and the question to answer before it runs.

menu(string $label, Menu $submenu): MenuItem

A submenu, entered with ENTER and left with ESC.