Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ab3e9d8
Named the cursor-move amount '$delta' in 'Reorder' and 'Template'.
AlexSkrypnyk Aug 11, 2026
635f55d
Gave the field validity checks the interface-anchored 'Violation' suf…
AlexSkrypnyk Aug 11, 2026
b0cc783
Gave the prefix-less boolean accessors the dominant 'is' prefix.
AlexSkrypnyk Aug 11, 2026
4e95b26
Cased the KeyMap property as '$keyMap' and its argument as '$key_map'.
AlexSkrypnyk Aug 11, 2026
e5f9d43
Paired the region's head-packed property with its '$tail' as '$head'.
AlexSkrypnyk Aug 11, 2026
6385aed
Named the narrowed theme argument '$elements' in 'entryLine()' like i…
AlexSkrypnyk Aug 11, 2026
96b874a
Renamed 'statusSymbol()' to 'statusGlyph()' for the theme's glyph voc…
AlexSkrypnyk Aug 11, 2026
810218a
Brought 12 files onto the project's blank-line rhythm around blocks a…
AlexSkrypnyk Aug 11, 2026
8ea53ac
Applied the 160-character line rule to three declarations that broke …
AlexSkrypnyk Aug 11, 2026
d30814f
Marked '#[\Override]' only where a concrete inherited method is overr…
AlexSkrypnyk Aug 11, 2026
45f236a
Returned 'static' from the last two fluent setters in the Block family.
AlexSkrypnyk Aug 11, 2026
5d8e99a
Ordered 'AgentHelp' constructor arguments as (root, context, envPrefi…
AlexSkrypnyk Aug 11, 2026
dafbc42
Converged source comments onto the technical register across 55 files.
AlexSkrypnyk Aug 11, 2026
98430f9
Addressed code review: corrected nine documentation claims to match t…
AlexSkrypnyk Aug 11, 2026
e23b776
Named windows and panels in the grid capacity error, matching the code.
AlexSkrypnyk Aug 11, 2026
ca0a44d
Threw 'FormException' for the sixteen consumer-declaration mistakes.
AlexSkrypnyk Aug 11, 2026
89b880e
Settled the cursor noun on 'current', the enum suffix on 'Type' and t…
AlexSkrypnyk Aug 11, 2026
bcd6250
Converged the choice-list row vocabulary on 'option', including the t…
AlexSkrypnyk Aug 11, 2026
3c35972
Spoke 'columns' and 'rows' for terminal extent everywhere, retiring t…
AlexSkrypnyk Aug 11, 2026
3823f85
Aligned the public primitive and testing surfaces on their siblings' …
AlexSkrypnyk Aug 11, 2026
d1eabf0
Routed the renderer's scroll window through 'Scroller', leaving one h…
AlexSkrypnyk Aug 11, 2026
88b7eea
Addressed code review: let the facade take a built theme, and deferre…
AlexSkrypnyk Aug 11, 2026
c72afdb
Corrected the four documentation claims that misdescribed what the co…
AlexSkrypnyk Aug 11, 2026
373b2dd
Required the 'is' prefix for state predicates, exempting commands tha…
AlexSkrypnyk Aug 11, 2026
38adef3
Finished the option rename, sparing the catalog, directory, binding a…
AlexSkrypnyk Aug 11, 2026
7ea0deb
Converged the 'has' predicates onto the 'is' prefix, leaving the comm…
AlexSkrypnyk Aug 11, 2026
6f54886
Stated the predicate rule as one spelling now that no 'has' form rema…
AlexSkrypnyk Aug 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,22 @@ composer install
- All files must end with a newline character
- Local variables/method arguments: `snake_case`
- Method names/class properties: `camelCase`
- **A method that answers a yes/no question about state is named `is*`.** The
prefix is what marks a return as boolean, so a reader never has to open the
method to find out - `isRequired()`, `isMultiple()`, `isScrolling()`,
`isSelectable()`, `isQueryDriven()`, `isUnicode()`, `isGhost()`. There is no
`has*` form: possession is state, so `has*` and `is*` were one group and
`is*` is the one spelling.

Two things are not state predicates and keep their own names:

- A **command that reports its own outcome**. Its job is to do something and
its boolean says whether that happened - `accept()`, `capture()`,
`activate()`, `load()`, `leave()`, `prepare()`. An `is` prefix would
misname the work. A method that both acts and answers is a command.
- A **lookup taking what it is asked about** - `Answers::has(string $id)`,
`Key::is(KeyName $name)`, `Bounds::contains($value)`. These ask about an
argument rather than about the object's own state, so they read as verbs.
- **Never model a closed set of values as string literals.** Any value that is
one-of-a-fixed-set (a kind, a state, a mode, a source) is a backed or pure
enum, and every property, parameter and return that carries it is typed with
Expand Down
6 changes: 3 additions & 3 deletions docs/content/fields/anatomy.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -380,10 +380,10 @@ A terminal may have no color, no Unicode, or a background the theme should read.

| Declaration | Grants | For |
| ----------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| `ColorSchemeCapableInterface` | `hasColor()`, `isDark()` | painting at all, and picking a palette for a dark or light terminal |
| `UnicodeCapableInterface` | `hasUnicode()` | choosing between a glyph and its ASCII stand-in |
| `ColorSchemeCapableInterface` | `isColor()`, `isDark()` | painting at all, and picking a palette for a dark or light terminal |
| `UnicodeCapableInterface` | `isUnicode()` | choosing between a glyph and its ASCII stand-in |
| `DimCapableInterface` | `dim()` | pushing back what a dialog is drawn over |
| `MarkdownCapableInterface` | `hasMarkdown()` | drawing the [markdown subset](/markdown) rather than its markers |
| `MarkdownCapableInterface` | `isMarkdown()` | drawing the [markdown subset](/markdown) rather than its markers |
| `OccupyCapableInterface` | `isFullscreen()`, `halign()`, `valign()`, the min/max sizes, `borderStyle()`, `spacing()`, `background()` | saying how much of the terminal the frame takes, and where it anchors |
| `OverrideCapableInterface` | `overrides()` | taking the glyphs and styles a consumer states without a subclass |

Expand Down
6 changes: 3 additions & 3 deletions docs/content/output.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ A form is rarely the whole program. A market-stall order opens with a welcome bo
```php
$out = $tui->output();

$out->box('Everything below is picked the morning it ships.', 'Welcome');
$out->box('Welcome', 'Everything below is picked the morning it ships.');
Comment thread
coderabbitai[bot] marked this conversation as resolved.

$answers = $tui->run();

Expand All @@ -34,11 +34,11 @@ $out->success('Preserves are ready')
</p>

```php
$out->box([
$out->box('Welcome to the produce box', [
'Everything below is picked the morning it ships.',
'',
'Nothing is charged until the box leaves the packing shed.',
], 'Welcome to the produce box');
]);
```

Long lines wrap inside the border rather than being clipped by it, so you can hand `box()` a paragraph and let it fit itself to the terminal.
Expand Down
6 changes: 3 additions & 3 deletions docs/content/specification.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -990,10 +990,10 @@ Six capabilities exist, and that is the whole set:

| Declaration | Grants | For |
| ----------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| `ColorSchemeCapableInterface` | `hasColor()`, `isDark()` | painting at all, and picking a palette for a dark or light terminal |
| `UnicodeCapableInterface` | `hasUnicode()` | choosing between a glyph and its ASCII stand-in |
| `ColorSchemeCapableInterface` | `isColor()`, `isDark()` | painting at all, and picking a palette for a dark or light terminal |
| `UnicodeCapableInterface` | `isUnicode()` | choosing between a glyph and its ASCII stand-in |
| `DimCapableInterface` | `dim()` | pushing back what a dialog is drawn over |
| `MarkdownCapableInterface` | `hasMarkdown()` | drawing the markdown subset rather than its markers |
| `MarkdownCapableInterface` | `isMarkdown()` | drawing the markdown subset rather than its markers |
| `OccupyCapableInterface` | `isFullscreen()`, `halign()`, `valign()`, the min/max sizes, `borderStyle()`, `spacing()`, `background()` | saying how much of the terminal the frame takes, and where it anchors |
| `OverrideCapableInterface` | `overrides()` | taking the elements a consumer states differently |

Expand Down
6 changes: 3 additions & 3 deletions docs/content/themes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -230,10 +230,10 @@ A terminal may have no color, no Unicode, or a background the theme should read.

| Declaration | Grants | For |
| ----------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| `ColorSchemeCapableInterface` | `hasColor()`, `isDark()` | painting at all, and picking a palette for a dark or light terminal |
| `UnicodeCapableInterface` | `hasUnicode()` | choosing between a glyph and its ASCII stand-in |
| `ColorSchemeCapableInterface` | `isColor()`, `isDark()` | painting at all, and picking a palette for a dark or light terminal |
| `UnicodeCapableInterface` | `isUnicode()` | choosing between a glyph and its ASCII stand-in |
| `DimCapableInterface` | `dim()` | pushing back what a [modal](/panels#modal-panels) is drawn over |
| `MarkdownCapableInterface` | `hasMarkdown()` | drawing the [markdown subset](/markdown) rather than its markers |
| `MarkdownCapableInterface` | `isMarkdown()` | drawing the [markdown subset](/markdown) rather than its markers |
| `OccupyCapableInterface` | `isFullscreen()`, `halign()`, `valign()`, the min/max sizes, `borderStyle()`, `spacing()`, `background()` | saying how much of the terminal the frame takes, and where it anchors |
| `OverrideCapableInterface` | `overrides()` | taking the elements a consumer states differently |

Expand Down
10 changes: 5 additions & 5 deletions playground/09-themes-elements.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* order.
*
* It is a patch, not a replacement: every element nobody names keeps the
* selected theme's own answer, which is why the unpicked entries below still
* selected theme's own answer, which is why the unpicked options below still
* carry the mark the theme draws for them.
*
* Usage:
Expand Down Expand Up @@ -67,12 +67,12 @@
->separator('•', '|')
->key(Sgr::Bold, Sgr::BrightCyan))
->field(static fn(FieldOverrides $f): FieldOverrides => $f
// The mark saying which row has the cursor, and which entry inside an
// The mark saying which row has the cursor, and which option inside an
// open one has it - two different marks, so two calls.
->selector('▶', '=>')
->entrySelector('▸', '->')
// The mark an entry carries once it is picked.
->entryMarker('▣', '[x]')
->optionSelector('▸', '->')
// The mark an option carries once it is picked.
->optionMarker('▣', '[x]')
// The mark showing where the next keystroke lands.
->caret('▎', '|')
// Text rather than a glyph: one argument, no stand-in to state.
Expand Down
6 changes: 3 additions & 3 deletions playground/18-output-box.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@

// A titled box: the title heads the frame, the body wraps inside it. An empty
// line in the list stays blank, so the content can be spaced out.
$out->box([
$out->box('Welcome to the produce box', [
'Everything below is picked the morning it ships.',
'',
'Nothing is charged until the box leaves the packing shed.',
], 'Welcome to the produce box');
]);

// A box with no title is a bare frame around its body.
$out->box('Pick a fruit, add vegetables, and confirm the quantity.');
$out->box('', 'Pick a fruit, add vegetables, and confirm the quantity.');
24 changes: 12 additions & 12 deletions playground/themes/OceanTheme.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ protected function indicator(string $text): string {
*/
#[\Override]
protected function marker(bool $selected): string {
return $selected ? $this->paint($this->accent(), $this->hasUnicode() ? '➤' : '>') : ' ';
return $selected ? $this->paint($this->accent(), $this->isUnicode() ? '➤' : '>') : ' ';
}

/**
Expand All @@ -89,7 +89,7 @@ protected function divider(): string {
* The mark.
*/
protected function lead(): string {
return $this->hasUnicode() ? '•' : '*';
return $this->isUnicode() ? '•' : '*';
}

/**
Expand All @@ -98,7 +98,7 @@ protected function lead(): string {
#[\Override]
public function keyGlyph(KeyName|string $key): string {
if ($key === KeyName::Enter) {
return $this->hasUnicode() ? '⏎' : '<';
return $this->isUnicode() ? '⏎' : '<';
}

return parent::keyGlyph($key);
Expand All @@ -109,7 +109,7 @@ public function keyGlyph(KeyName|string $key): string {
*/
#[\Override]
public function chromeOverflowMarker(bool $above): string {
return $this->indicator($above ? ($this->hasUnicode() ? '▴' : '^') : ($this->hasUnicode() ? '▾' : 'v'));
return $this->indicator($above ? ($this->isUnicode() ? '▴' : '^') : ($this->isUnicode() ? '▾' : 'v'));
}

/**
Expand All @@ -125,19 +125,19 @@ public function fieldBadge(string $text): string {
*/
#[\Override]
public function fieldCaret(): string {
return $this->paint($this->accent(), $this->hasUnicode() ? '▎' : '|');
return $this->paint($this->accent(), $this->isUnicode() ? '▎' : '|');
}

/**
* {@inheritdoc}
*/
#[\Override]
public function fieldEntryMarker(bool $chosen, bool $exclusive = FALSE): string {
public function fieldOptionMarker(bool $chosen, bool $exclusive = FALSE): string {
if ($exclusive) {
return $chosen ? $this->paint($this->accent(), $this->hasUnicode() ? '◉' : '(o)') : ($this->hasUnicode() ? '◯' : '( )');
return $chosen ? $this->paint($this->accent(), $this->isUnicode() ? '◉' : '(o)') : ($this->isUnicode() ? '◯' : '( )');
}

return $chosen ? $this->fieldValue($this->hasUnicode() ? '▣' : '[x]') : ($this->hasUnicode() ? '▢' : '[ ]');
return $chosen ? $this->fieldValue($this->isUnicode() ? '▣' : '[x]') : ($this->isUnicode() ? '▢' : '[ ]');
}

/**
Expand Down Expand Up @@ -185,7 +185,7 @@ public function actionSeparator(): string {
*/
#[\Override]
public function panelDescend(): string {
return $this->description($this->hasUnicode() ? '»' : '>');
return $this->description($this->isUnicode() ? '»' : '>');
}

/**
Expand All @@ -201,7 +201,7 @@ public function panelDescription(string $text): string {
*/
#[\Override]
public function panelSummary(string $text): string {
return $this->description(($this->hasUnicode() ? '»' : '>') . ' ' . $text);
return $this->description(($this->isUnicode() ? '»' : '>') . ' ' . $text);
}

/**
Expand Down Expand Up @@ -240,7 +240,7 @@ public function legendSeparator(): string {
* {@inheritdoc}
*/
#[\Override]
public function renderBanner(string $logo, string $version): string {
public function renderBanner(string $logo, string $version): array {
$lines = [];

foreach (explode("\n", $logo) as $line) {
Expand All @@ -252,7 +252,7 @@ public function renderBanner(string $logo, string $version): string {
$lines[] = $this->footer('≈ ' . $version . ' ≈');
}

return implode("\n", $lines);
return $lines;
}

}
8 changes: 4 additions & 4 deletions src/Answers/Answers.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* override.
*
* An answer set is self-describing: each answer carries a snapshot of its
* question (label, kind, panel trail) in items(), so summaries and processing
* question (label, kind, panel trail) in $items, so summaries and processing
* need no form configuration.
*
* @package DrevOps\Tui\Answers
Expand Down Expand Up @@ -44,9 +44,9 @@ public function __construct(
/**
* Build a self-describing answer set from a declared block tree.
*
* The tree's root is the form itself rather than a panel somebody declared,
* so it contributes no heading: the trail each answer carries starts at the
* panel it was asked in.
* The tree's root is the form itself rather than a declared panel, so it
* contributes no heading: the trail each answer carries starts at the panel
* it was asked in.
*
* @param \DrevOps\Tui\Block\Panel $root
* The panel every declared panel hangs from.
Expand Down
2 changes: 1 addition & 1 deletion src/Answers/Provenance.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ enum Provenance: string {
/**
* The badge label in the active language.
*
* A literal per case, rather than translating the backing value, so each
* Each case translates a literal rather than the backing value, so each
* badge string is a discoverable chrome key in the catalog template.
*
* @return string
Expand Down
1 change: 1 addition & 0 deletions src/Answers/SummaryFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ public function format(Answers $answers): string {
*/
protected function openPanels(array $trail, array $panels): array {
$common = 0;

while ($common < count($trail) && isset($panels[$common]) && $trail[$common] === $panels[$common]) {
$common++;
}
Expand Down
6 changes: 3 additions & 3 deletions src/Answers/ValueFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
* A boolean reads as a translated yes/no, a list joins its scalar items with
* commas, a scalar casts to its string, and anything else renders empty. The
* panel rows, the grid previews and the answer summary all route through this
* one rendering, so a value never reads differently between surfaces. Secret
* masking rides along: a fixed-length mask conceals both a secret's value and
* its length, whatever glyph the surface masks with.
* one rendering, so a value never reads differently between surfaces. The
* fixed-length mask conceals both a secret's value and its length, whatever
* glyph the surface masks with.
*
* @package DrevOps\Tui\Answers
*/
Expand Down
18 changes: 8 additions & 10 deletions src/Block/AbstractBlock.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,15 @@
use DrevOps\Tui\Theme\ThemeInterface;

/**
* What every block has in common: it draws through a theme, or it cannot draw.
* Behaviour every block shares: drawing through a theme's elements.
*
* A block fills the space it is given, and the region it sits in knows nothing
* else about it. Order and spacing within its own output belong to the block;
* colour and glyph belong to the theme. So the one thing every kind shares is
* the elements it reaches for - and a theme that declares none cannot draw it,
* which is a type error rather than a blank line.
* A block draws with the elements it declares, and {@see elements()} narrows
* the theme to them: a theme that does not implement them throws
* \InvalidArgumentException rather than drawing a blank line.
*
* Every block may also declare edges. What it occupies is known where it is
* drawn rather than here, so the declaration carries no geometry and the
* renderer sizes the box.
* Every block may also declare edges. The declaration carries no geometry -
* what a block occupies is known where it is drawn - so the renderer sizes
* the box.
*
* @package DrevOps\Tui\Block
*/
Expand All @@ -35,7 +33,7 @@ abstract class AbstractBlock implements BlockInterface, BorderCapableInterface {
* @param class-string<T> $elements
* The elements interface this block declares.
* @param string $subject
* What could not be drawn, as the phrase the failure names it by.
* The phrase the exception message uses for what could not be drawn.
*
* @return T
* The theme, able to draw this block.
Expand Down
6 changes: 5 additions & 1 deletion src/Block/Actions.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use DrevOps\Tui\Block\Capability\FocusCapableTrait;
use DrevOps\Tui\Block\Capability\RejectCapableInterface;
use DrevOps\Tui\Block\Element\ActionsElementsInterface;
use DrevOps\Tui\FormException;
use DrevOps\Tui\Terminal\Ansi;
use DrevOps\Tui\Theme\ThemeInterface;
use DrevOps\Tui\Translation\Translator;
Expand Down Expand Up @@ -87,10 +88,13 @@ public function names(): array {
*
* @return static
* The block.
*
* @throws \DrevOps\Tui\FormException
* When the action name is unknown.
*/
public function select(string $name): static {
if (!isset($this->buttons[$name])) {
throw new \InvalidArgumentException(sprintf('Unknown action "%s". This block declares: %s.', $name, implode(', ', $this->names())));
throw new FormException(sprintf('Unknown action "%s". This block declares: %s.', $name, implode(', ', $this->names())));
}

$this->selected = $name;
Expand Down
8 changes: 4 additions & 4 deletions src/Block/BlockInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
/**
* Anything drawn in a region.
*
* That is the whole definition: a block fills the space it is given, and the
* region knows nothing else about it. Order and spacing within its own output
* belong to the block; colour and glyph belong to the theme, which is why
* render() reaches the theme for elements rather than choosing either itself.
* A block fills the space it is given, and the region knows nothing else
* about it. Order and spacing within its own output belong to the block;
* colour and glyph belong to the theme, so render() takes the elements from
* the theme rather than choosing either itself.
*
* @package DrevOps\Tui\Block
*/
Expand Down
Loading
Loading