From ab3e9d8b5ec856928d4f574e49c7b6bc933631d3 Mon Sep 17 00:00:00 2001
From: Alex Skrypnyk
Date: Tue, 11 Aug 2026 15:25:03 +1000
Subject: [PATCH 01/27] Named the cursor-move amount '$delta' in 'Reorder' and
'Template'.
---
src/Field/Reorder.php | 6 +++---
src/Field/Template.php | 6 +++---
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/src/Field/Reorder.php b/src/Field/Reorder.php
index 0c0ecc1a..5000aef7 100644
--- a/src/Field/Reorder.php
+++ b/src/Field/Reorder.php
@@ -124,11 +124,11 @@ public function handle(Key $key): void {
/**
* Move the cursor, carrying the held item when one is grabbed.
*
- * @param int $dir
+ * @param int $delta
* The direction: -1 up, +1 down.
*/
- protected function move(int $dir): void {
- $target = $this->cursor + $dir;
+ protected function move(int $delta): void {
+ $target = $this->cursor + $delta;
if ($target < 0 || $target >= count($this->items)) {
return;
diff --git a/src/Field/Template.php b/src/Field/Template.php
index f39b5664..9e7ade62 100644
--- a/src/Field/Template.php
+++ b/src/Field/Template.php
@@ -158,15 +158,15 @@ protected function activeName(): string {
* error but does not hold the caret, so a slot filled in the wrong order can
* still be reached and corrected.
*
- * @param int $direction
+ * @param int $delta
* The number of slots to move by: 1 forward, -1 back.
*/
- protected function move(int $direction): void {
+ protected function move(int $delta): void {
$count = count($this->names);
$this->parts[$this->activeName()] = $this->buffer;
$this->error = $this->template->partError($this->activeName(), $this->buffer);
- $this->focus((($this->active + $direction) % $count + $count) % $count);
+ $this->focus((($this->active + $delta) % $count + $count) % $count);
}
/**
From 635f55dc64a5fde0c2131738417bef7dba5bf2e3 Mon Sep 17 00:00:00 2001
From: Alex Skrypnyk
Date: Tue, 11 Aug 2026 15:29:16 +1000
Subject: [PATCH 02/27] Gave the field validity checks the interface-anchored
'Violation' suffix.
---
src/Block/Field.php | 22 +++++++++----------
src/Schema/SchemaValidator.php | 2 +-
src/Screen/Collector.php | 4 ++--
tests/phpunit/Unit/Block/EntryTest.php | 2 +-
tests/phpunit/Unit/Block/FieldBlockTest.php | 10 ++++-----
.../Unit/Block/FieldDeclarationTest.php | 4 ++--
6 files changed, 22 insertions(+), 22 deletions(-)
diff --git a/src/Block/Field.php b/src/Block/Field.php
index b6b835c3..3f30f5e1 100644
--- a/src/Block/Field.php
+++ b/src/Block/Field.php
@@ -1391,7 +1391,7 @@ public function template(): ?Template {
* @return string|null
* The message, or NULL when the value fits or no shape is declared.
*/
- public function templateError(mixed $value): ?string {
+ public function templateViolation(mixed $value): ?string {
if (!$this->template instanceof Template || !is_string($value) || $value === '') {
return NULL;
}
@@ -1464,7 +1464,7 @@ public function ratingCaptions(): array {
* The fragment, or NULL when nothing constrains the value or every item is
* among the entries.
*/
- public function entryError(mixed $value): ?string {
+ public function entryViolation(mixed $value): ?string {
// A field that declares no entries constrains nothing - but one whose
// entries follow a query or the answers is constrained by whatever they
// resolved to, and resolving to nothing means the value does not exist.
@@ -1473,7 +1473,7 @@ public function entryError(mixed $value): ?string {
}
if (!$this->isMultiChoice()) {
- return $this->scalarEntryError(is_scalar($value) ? (string) $value : '');
+ return $this->scalarEntryViolation(is_scalar($value) ? (string) $value : '');
}
if (!is_array($value)) {
@@ -1481,14 +1481,14 @@ public function entryError(mixed $value): ?string {
}
foreach ($value as $item) {
- $error = $this->scalarEntryError(is_scalar($item) ? (string) $item : '');
+ $error = $this->scalarEntryViolation(is_scalar($item) ? (string) $item : '');
if ($error !== NULL) {
return $error;
}
}
- return $this->fieldType === FieldType::Reorder ? $this->rankingError($value) : NULL;
+ return $this->fieldType === FieldType::Reorder ? $this->rankingViolation($value) : NULL;
}
/**
@@ -1919,7 +1919,7 @@ public function refuses(mixed $value, ?\Closure $reusable = NULL): ?string {
return $missing;
}
- $outside = $this->limitRefusal($value);
+ $outside = $this->limitViolation($value);
if ($outside !== NULL) {
return $outside;
@@ -1927,7 +1927,7 @@ public function refuses(mixed $value, ?\Closure $reusable = NULL): ?string {
// The shape is checked before the field's own validator, which reads the
// value as an assembled template and would otherwise see a foreign string.
- $misshapen = $this->templateError($value);
+ $misshapen = $this->templateViolation($value);
if ($misshapen !== NULL) {
return $misshapen;
@@ -1938,7 +1938,7 @@ public function refuses(mixed $value, ?\Closure $reusable = NULL): ?string {
// A validator that answers with nothing has not said why, and a refusal
// nobody can read is no refusal at all.
- return is_string($refusal) && $refusal !== '' ? $refusal : $this->entryError($value);
+ return is_string($refusal) && $refusal !== '' ? $refusal : $this->entryViolation($value);
}
/**
@@ -1954,7 +1954,7 @@ public function refuses(mixed $value, ?\Closure $reusable = NULL): ?string {
* @return string|null
* The reason, or NULL when every declared limit is met.
*/
- protected function limitRefusal(mixed $value): ?string {
+ protected function limitViolation(mixed $value): ?string {
$number = $this->bounds?->violation($value);
if ($number !== NULL) {
@@ -2055,7 +2055,7 @@ public static function canonicalOrder(array $allowed, array $desired): array {
* The fragment naming the value when it is disabled or unknown, or NULL
* when it can be picked.
*/
- protected function scalarEntryError(string $value): ?string {
+ protected function scalarEntryViolation(string $value): ?string {
if (in_array($value, $this->selectableValues(), TRUE)) {
return NULL;
}
@@ -2098,7 +2098,7 @@ protected function scalarEntryError(string $value): ?string {
* @return string|null
* The fragment, or NULL when the ranking covers every entry once.
*/
- protected function rankingError(array $items): ?string {
+ protected function rankingViolation(array $items): ?string {
$selectable = $this->selectableValues();
$seen = [];
diff --git a/src/Schema/SchemaValidator.php b/src/Schema/SchemaValidator.php
index b74dfafe..bbfeda32 100644
--- a/src/Schema/SchemaValidator.php
+++ b/src/Schema/SchemaValidator.php
@@ -208,7 +208,7 @@ protected function constraintMessage(Field $field, string $constraint): string {
* An error, or NULL when valid.
*/
protected function checkOptions(Field $field, mixed $value): ?string {
- $error = $field->entryError($value);
+ $error = $field->entryViolation($value);
return $error === NULL ? NULL : Translator::t('Question "@id": @error.', ['@id' => $field->id(), '@error' => $error]);
}
diff --git a/src/Screen/Collector.php b/src/Screen/Collector.php
index 371e065f..f1b01d31 100644
--- a/src/Screen/Collector.php
+++ b/src/Screen/Collector.php
@@ -462,8 +462,8 @@ protected function acceptsDetected(Field $field, mixed $value): bool {
&& $field->requiredViolation($value) === NULL
&& $field->boundsViolation($value) === NULL
&& $field->pickerViolation($value) === NULL
- && $field->templateError($value) === NULL
- && $field->entryError($value) === NULL;
+ && $field->templateViolation($value) === NULL
+ && $field->entryViolation($value) === NULL;
}
/**
diff --git a/tests/phpunit/Unit/Block/EntryTest.php b/tests/phpunit/Unit/Block/EntryTest.php
index 9c58ae7e..a39baae4 100644
--- a/tests/phpunit/Unit/Block/EntryTest.php
+++ b/tests/phpunit/Unit/Block/EntryTest.php
@@ -196,7 +196,7 @@ public function testSelectableValues(): void {
#[DataProvider('dataProviderOptionError')]
public function testOptionError(FieldType $type, bool $multiple, array $options, mixed $value, ?string $expected): void {
- $this->assertSame($expected, self::offering($type, $multiple, $options)->entryError($value));
+ $this->assertSame($expected, self::offering($type, $multiple, $options)->entryViolation($value));
}
public static function dataProviderOptionError(): \Iterator {
diff --git a/tests/phpunit/Unit/Block/FieldBlockTest.php b/tests/phpunit/Unit/Block/FieldBlockTest.php
index 903e3e31..dae286c4 100644
--- a/tests/phpunit/Unit/Block/FieldBlockTest.php
+++ b/tests/phpunit/Unit/Block/FieldBlockTest.php
@@ -602,7 +602,7 @@ public function testEntryWithNoLabelDrawsItsOwnValue(): void {
#[DataProvider('dataProviderValueOutsideTheEntriesIsRefused')]
public function testValueOutsideTheEntriesIsRefused(Field $field, mixed $value, ?string $error): void {
- $this->assertSame($error, $field->entryError($value));
+ $this->assertSame($error, $field->entryViolation($value));
}
public static function dataProviderValueOutsideTheEntriesIsRefused(): \Iterator {
@@ -654,7 +654,7 @@ public function testValueIsRefusedWhenTheQueryResolvedToNothingThatCarriesIt():
// to, so resolving to nothing means the value does not exist.
$field = (new Field('basket', 'Basket contents', FieldType::Search))->query(static fn(): array => []);
- $this->assertSame('value "plum" was not found', $field->entryError('plum'));
+ $this->assertSame('value "plum" was not found', $field->entryViolation('plum'));
}
#[DataProvider('dataProviderEntriesAreUnsettledWhileSomethingOwesThem')]
@@ -703,9 +703,9 @@ public function testEntriesThatAreNotMapOfLabelsSettleToNone(): void {
public function testValueThatDoesNotFitTheDeclaredShapeIsRefused(): void {
$field = (new Field('crate', 'Crate code', FieldType::Template))->pattern(new Template('{{orchard}}-{{fruit}}'));
- $this->assertNull($field->templateError('valley-apple'));
- $this->assertNull($field->templateError(''));
- $this->assertNull((new Field('courier', 'Courier'))->templateError('anything'));
+ $this->assertNull($field->templateViolation('valley-apple'));
+ $this->assertNull($field->templateViolation(''));
+ $this->assertNull((new Field('courier', 'Courier'))->templateViolation('anything'));
$this->assertFalse($field->accept('valley'));
$this->assertStringContainsString('does not match the template', (string) $field->refusal());
diff --git a/tests/phpunit/Unit/Block/FieldDeclarationTest.php b/tests/phpunit/Unit/Block/FieldDeclarationTest.php
index c267edcd..5f74a348 100644
--- a/tests/phpunit/Unit/Block/FieldDeclarationTest.php
+++ b/tests/phpunit/Unit/Block/FieldDeclarationTest.php
@@ -209,7 +209,7 @@ public function testTemplateError(mixed $value, ?string $expected): void {
'b' => static fn(string $part): ?string => $part === 'ok' ? NULL : 'must be ok',
]));
- $this->assertSame($expected, $field->templateError($value));
+ $this->assertSame($expected, $field->templateViolation($value));
}
/**
@@ -229,7 +229,7 @@ public static function dataProviderTemplateError(): \Iterator {
}
public function testTemplateErrorIsNullWithoutShape(): void {
- $this->assertNull((new Field('name', 'Name'))->templateError('anything'));
+ $this->assertNull((new Field('name', 'Name'))->templateViolation('anything'));
}
/**
From b0cc78371bf840a576c4cbaf7f9c13b2fdb9cf0a Mon Sep 17 00:00:00 2001
From: Alex Skrypnyk
Date: Tue, 11 Aug 2026 15:34:47 +1000
Subject: [PATCH 03/27] Gave the prefix-less boolean accessors the dominant
'is' prefix.
---
src/Block/Option.php | 4 ++--
src/Builder/Form.php | 2 +-
src/Field/Capability/CompletionCapableTrait.php | 4 ++--
src/Field/Capability/OptionsCapableTrait.php | 8 ++++----
src/Field/Capability/SelectionCapableInterface.php | 2 +-
src/Field/Capability/SelectionCapableTrait.php | 14 +++++++-------
src/Field/FieldFactory.php | 4 ++--
src/Field/Reorder.php | 2 +-
src/Field/Suggest.php | 4 ++--
src/Schema/SchemaGenerator.php | 2 +-
src/Screen/Layout/LayoutManager.php | 6 +++---
tests/phpunit/Unit/Block/EntryTest.php | 4 ++--
12 files changed, 28 insertions(+), 28 deletions(-)
diff --git a/src/Block/Option.php b/src/Block/Option.php
index f7c7b18b..be2a3ad5 100644
--- a/src/Block/Option.php
+++ b/src/Block/Option.php
@@ -80,7 +80,7 @@ public function __construct(
* TRUE for an enabled Option row; FALSE for separators, headings and
* disabled options.
*/
- public function selectable(): bool {
+ public function isSelectable(): bool {
return $this->kind === OptionKind::Option && !$this->disabled;
}
@@ -150,7 +150,7 @@ public static function selectableValues(array $options): array {
$out = [];
foreach ($options as $option) {
- if ($option->selectable()) {
+ if ($option->isSelectable()) {
$out[] = $option->value;
}
}
diff --git a/src/Builder/Form.php b/src/Builder/Form.php
index 74457d75..2b73a32c 100644
--- a/src/Builder/Form.php
+++ b/src/Builder/Form.php
@@ -578,7 +578,7 @@ protected function assertReorderEntries(Panel $root): void {
$entries = $field->entries();
foreach ($entries as $entry) {
- if (!$entry->selectable()) {
+ if (!$entry->isSelectable()) {
throw new FormException(sprintf('Reorder field "%s" allows only plain options - no headings, separators or disabled rows.', $field->id()));
}
}
diff --git a/src/Field/Capability/CompletionCapableTrait.php b/src/Field/Capability/CompletionCapableTrait.php
index 0067e324..5808a46e 100644
--- a/src/Field/Capability/CompletionCapableTrait.php
+++ b/src/Field/Capability/CompletionCapableTrait.php
@@ -31,7 +31,7 @@ trait CompletionCapableTrait {
* The full candidate string, or NULL.
*/
public function bestMatch(): ?string {
- if ($this->buffer === '' || !$this->completionAvailable()) {
+ if ($this->buffer === '' || !$this->isCompletionAvailable()) {
return NULL;
}
@@ -56,7 +56,7 @@ public function bestMatch(): ?string {
* TRUE when the caret sits at the end of the buffer, so the ghost text
* continues what is being typed rather than interrupting it.
*/
- protected function completionAvailable(): bool {
+ protected function isCompletionAvailable(): bool {
return $this->cursor === Strings::length($this->buffer);
}
diff --git a/src/Field/Capability/OptionsCapableTrait.php b/src/Field/Capability/OptionsCapableTrait.php
index b333ac0c..790bef99 100644
--- a/src/Field/Capability/OptionsCapableTrait.php
+++ b/src/Field/Capability/OptionsCapableTrait.php
@@ -49,7 +49,7 @@ protected function initOptions(array $options): void {
*/
protected function firstSelectable(array $rows): int {
foreach ($rows as $index => $row) {
- if ($row->selectable()) {
+ if ($row->isSelectable()) {
return $index;
}
}
@@ -70,7 +70,7 @@ protected function firstSelectable(array $rows): int {
*/
protected function cursorForDefault(array $rows, string $default): int {
foreach ($rows as $index => $row) {
- if ($row->selectable() && $row->value === $default) {
+ if ($row->isSelectable() && $row->value === $default) {
return $index;
}
}
@@ -95,7 +95,7 @@ protected function stepCursor(array $rows, int $from, int $dir): int {
$index = $from + $dir;
while ($index >= 0 && $index < count($rows)) {
- if ($rows[$index]->selectable()) {
+ if ($rows[$index]->isSelectable()) {
return $index;
}
@@ -153,7 +153,7 @@ protected function renderChoiceList(ThemeInterface $theme): string {
protected function highlightedDescription(): string {
$option = $this->visible()[$this->cursor] ?? NULL;
- return $option instanceof Option && $option->selectable() ? $option->description : '';
+ return $option instanceof Option && $option->isSelectable() ? $option->description : '';
}
/**
diff --git a/src/Field/Capability/SelectionCapableInterface.php b/src/Field/Capability/SelectionCapableInterface.php
index add16a6b..277cdb97 100644
--- a/src/Field/Capability/SelectionCapableInterface.php
+++ b/src/Field/Capability/SelectionCapableInterface.php
@@ -19,6 +19,6 @@ interface SelectionCapableInterface {
* @return bool
* TRUE when the cursor rests on a selectable option.
*/
- public function currentSelectable(): bool;
+ public function isCurrentSelectable(): bool;
}
diff --git a/src/Field/Capability/SelectionCapableTrait.php b/src/Field/Capability/SelectionCapableTrait.php
index df7ac5f7..0cbaef88 100644
--- a/src/Field/Capability/SelectionCapableTrait.php
+++ b/src/Field/Capability/SelectionCapableTrait.php
@@ -156,7 +156,7 @@ protected function handleSingleChoiceKey(Key $key): void {
return;
}
- if ($keys->matches($key, Action::Accept) && $this->currentSelectable()) {
+ if ($keys->matches($key, Action::Accept) && $this->isCurrentSelectable()) {
$this->accept($this->liveValue());
}
}
@@ -220,8 +220,8 @@ protected function handleMultiChoiceKey(Key $key): bool {
* @return bool
* TRUE when the cursor rests on a selectable option.
*/
- public function currentSelectable(): bool {
- return ($this->visible()[$this->cursor] ?? NULL)?->selectable() ?? FALSE;
+ public function isCurrentSelectable(): bool {
+ return ($this->visible()[$this->cursor] ?? NULL)?->isSelectable() ?? FALSE;
}
/**
@@ -240,7 +240,7 @@ public function selectableValues(): array {
public function toggleCurrent(): void {
$option = $this->visible()[$this->cursor] ?? NULL;
- if (!$option instanceof Option || !$option->selectable()) {
+ if (!$option instanceof Option || !$option->isSelectable()) {
return;
}
@@ -260,7 +260,7 @@ public function toggleCurrent(): void {
*/
public function setAllVisible(bool $selected): void {
foreach ($this->visible() as $option) {
- if (!$option->selectable()) {
+ if (!$option->isSelectable()) {
continue;
}
@@ -278,13 +278,13 @@ public function setAllVisible(bool $selected): void {
*/
protected function liveValue(): mixed {
if (!$this->multiple) {
- return $this->currentSelectable() ? $this->visible()[$this->cursor]->value : '';
+ return $this->isCurrentSelectable() ? $this->visible()[$this->cursor]->value : '';
}
$out = [];
foreach ($this->options as $option) {
- if ($option->selectable() && isset($this->selected[$option->value])) {
+ if ($option->isSelectable() && isset($this->selected[$option->value])) {
$out[] = $option->value;
}
}
diff --git a/src/Field/FieldFactory.php b/src/Field/FieldFactory.php
index 818ed97c..fb8ab1f0 100644
--- a/src/Field/FieldFactory.php
+++ b/src/Field/FieldFactory.php
@@ -225,7 +225,7 @@ protected function entryLabels(array $options): array {
$out = [];
foreach ($options as $option) {
- if ($option->selectable()) {
+ if ($option->isSelectable()) {
$out[$option->value] = $option->label;
}
}
@@ -273,7 +273,7 @@ protected function entryDescriptions(array $options): array {
$out = [];
foreach ($options as $option) {
- if (!$option->selectable()) {
+ if (!$option->isSelectable()) {
continue;
}
diff --git a/src/Field/Reorder.php b/src/Field/Reorder.php
index 5000aef7..b9ee9e6f 100644
--- a/src/Field/Reorder.php
+++ b/src/Field/Reorder.php
@@ -190,7 +190,7 @@ protected function highlightedDescription(): string {
$current = $this->items[$this->cursor];
- return $current->selectable() ? $current->description : '';
+ return $current->isSelectable() ? $current->description : '';
}
/**
diff --git a/src/Field/Suggest.php b/src/Field/Suggest.php
index 746c55f2..c0cdd65a 100644
--- a/src/Field/Suggest.php
+++ b/src/Field/Suggest.php
@@ -192,7 +192,7 @@ protected function resetFilterCursor(): void {
* @return bool
* TRUE when the ghost-text preview applies.
*/
- protected function completionAvailable(): bool {
+ protected function isCompletionAvailable(): bool {
return $this->ghost && $this->cursor < 0 && !$this->queryLoading;
}
@@ -271,7 +271,7 @@ protected function adoptQueryRows(array $rows): void {
$this->descriptions = [];
foreach ($rows as $row) {
- if ($row->selectable()) {
+ if ($row->isSelectable()) {
$this->descriptions[$row->value] = $row->description;
}
}
diff --git a/src/Schema/SchemaGenerator.php b/src/Schema/SchemaGenerator.php
index 34b78f20..cb3e1a06 100644
--- a/src/Schema/SchemaGenerator.php
+++ b/src/Schema/SchemaGenerator.php
@@ -118,7 +118,7 @@ protected function options(Field $field): array {
$out = [];
foreach ($field->entries() as $option) {
- if (!$option->selectable()) {
+ if (!$option->isSelectable()) {
continue;
}
diff --git a/src/Screen/Layout/LayoutManager.php b/src/Screen/Layout/LayoutManager.php
index 5952fe3b..ccd75400 100644
--- a/src/Screen/Layout/LayoutManager.php
+++ b/src/Screen/Layout/LayoutManager.php
@@ -121,7 +121,7 @@ protected static function shipped(): array {
// An arrangement nobody can build from its name alone is not one a form
// can pick by name.
- if (!self::nameable($class)) {
+ if (!self::isNameable($class)) {
continue;
}
@@ -167,7 +167,7 @@ protected static function vouch(string $class): string {
// An abstract layout, or one built from something a name cannot carry,
// passes the type check and then fatals on the first create(): refusing it
// here names the class rather than the call site.
- if (!self::nameable($class)) {
+ if (!self::isNameable($class)) {
throw new \InvalidArgumentException(sprintf('Layout class "%s" cannot be built from a name alone.', $class));
}
@@ -188,7 +188,7 @@ protected static function vouch(string $class): string {
* @return bool
* TRUE when it can.
*/
- protected static function nameable(string $class): bool {
+ protected static function isNameable(string $class): bool {
$reflection = new \ReflectionClass($class);
return $reflection->isInstantiable() && ($reflection->getConstructor()?->getNumberOfParameters() ?? 0) === 0;
diff --git a/tests/phpunit/Unit/Block/EntryTest.php b/tests/phpunit/Unit/Block/EntryTest.php
index a39baae4..3d1d3ddc 100644
--- a/tests/phpunit/Unit/Block/EntryTest.php
+++ b/tests/phpunit/Unit/Block/EntryTest.php
@@ -33,7 +33,7 @@ public function testListFromMap(): void {
$this->assertSame('a', $options[0]->value);
$this->assertSame('Apple', $options[0]->label);
$this->assertSame(OptionKind::Option, $options[0]->kind);
- $this->assertTrue($options[0]->selectable());
+ $this->assertTrue($options[0]->isSelectable());
}
public function testListLabelDefaultsToValue(): void {
@@ -60,7 +60,7 @@ public function testListMixed(): void {
#[DataProvider('dataProviderSelectable')]
public function testSelectable(Option $option, bool $expected): void {
- $this->assertSame($expected, $option->selectable());
+ $this->assertSame($expected, $option->isSelectable());
}
public static function dataProviderSelectable(): \Iterator {
From 4e95b267d025e63d5e175751530486b20f94518d Mon Sep 17 00:00:00 2001
From: Alex Skrypnyk
Date: Tue, 11 Aug 2026 15:36:02 +1000
Subject: [PATCH 04/27] Cased the KeyMap property as '$keyMap' and its argument
as '$key_map'.
---
src/Field/FieldFactory.php | 10 +++++-----
src/Tui.php | 6 +++---
tests/phpunit/Unit/TuiTest.php | 2 +-
3 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/src/Field/FieldFactory.php b/src/Field/FieldFactory.php
index fb8ab1f0..ee706638 100644
--- a/src/Field/FieldFactory.php
+++ b/src/Field/FieldFactory.php
@@ -30,19 +30,19 @@ class FieldFactory {
/**
* The resolved key bindings to inject into each field.
*/
- protected KeyMap $keymap;
+ protected KeyMap $keyMap;
/**
* Construct a field factory.
*
- * @param \DrevOps\Tui\Input\KeyMap|null $keymap
+ * @param \DrevOps\Tui\Input\KeyMap|null $key_map
* The resolved key bindings; NULL uses the default preset.
* @param bool $externalEditorAvailable
* Whether an external editor is launchable here. A textarea field opts in
* per-field; the handoff shows only when one is also available.
*/
- public function __construct(?KeyMap $keymap = NULL, protected bool $externalEditorAvailable = FALSE) {
- $this->keymap = $keymap ?? KeyMapManager::create();
+ public function __construct(?KeyMap $key_map = NULL, protected bool $externalEditorAvailable = FALSE) {
+ $this->keyMap = $key_map ?? KeyMapManager::create();
}
/**
@@ -91,7 +91,7 @@ public function open(Field $block, mixed $current = NULL, array $answers = []):
$field->setPlaceholder($block->placeholderText());
}
- return $field->setKeys($this->keymap->forField($block->type(), $block->isMultiple()));
+ return $field->setKeys($this->keyMap->forField($block->type(), $block->isMultiple()));
}
/**
diff --git a/src/Tui.php b/src/Tui.php
index c607c176..1c6f622d 100644
--- a/src/Tui.php
+++ b/src/Tui.php
@@ -104,7 +104,7 @@ final class Tui {
/**
* The resolved key bindings; NULL uses the default preset.
*/
- protected ?KeyMap $keymap = NULL;
+ protected ?KeyMap $keyMap = NULL;
/**
* Force ANSI colour on/off; NULL auto-detects.
@@ -348,7 +348,7 @@ protected function assertRegions(): void {
* The facade.
*/
public function keys(string $preset = '', array $overrides = []): self {
- $this->keymap = KeyMapManager::create($preset, $overrides);
+ $this->keyMap = KeyMapManager::create($preset, $overrides);
return $this;
}
@@ -716,7 +716,7 @@ public function controller(array $options, string $theme = '', string $banner =
$this->root(),
$drawn,
[],
- $this->keymap ?? KeyMapManager::create(),
+ $this->keyMap ?? KeyMapManager::create(),
new Collector($this->registry, $this->form->currentFixups()),
$this->context($directory, $update, $version),
// The frame the theme was told to lay its rows out to is the frame that
diff --git a/tests/phpunit/Unit/TuiTest.php b/tests/phpunit/Unit/TuiTest.php
index 0128dccf..52e6119b 100644
--- a/tests/phpunit/Unit/TuiTest.php
+++ b/tests/phpunit/Unit/TuiTest.php
@@ -95,7 +95,7 @@ public function testEachOperationRestoresItsOwnTranslator(): void {
public function testKeysResolvesPresetAndOverrides(): void {
$tui = (new Tui($this->demoForm()))->keys('vim', [new Binding(Scope::navigation(), Action::Quit, 'x')]);
- $keymap = (new \ReflectionProperty($tui, 'keymap'))->getValue($tui);
+ $keymap = (new \ReflectionProperty($tui, 'keyMap'))->getValue($tui);
$this->assertInstanceOf(KeyMap::class, $keymap);
$nav = $keymap->navigation();
// The vim preset supplies "j" for MoveDown; the override binds "x" to Quit.
From e5f9d43afd44aa6484d9db3756d7cb5cd685d12c Mon Sep 17 00:00:00 2001
From: Alex Skrypnyk
Date: Tue, 11 Aug 2026 15:37:35 +1000
Subject: [PATCH 05/27] Paired the region's head-packed property with its
'$tail' as '$head'.
---
src/Screen/Region.php | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/Screen/Region.php b/src/Screen/Region.php
index 2dabcbff..424b0d8b 100644
--- a/src/Screen/Region.php
+++ b/src/Screen/Region.php
@@ -59,7 +59,7 @@ final class Region implements BorderCapableInterface, ScrollCapableInterface {
*
* @var list<\DrevOps\Tui\Block\BlockInterface>
*/
- protected array $blocks = [];
+ protected array $head = [];
/**
* The blocks packed from the end of its flow, in the order they were added.
@@ -238,7 +238,7 @@ public function isPreviewing(): bool {
* The region.
*/
public function add(BlockInterface $block): self {
- $this->blocks[] = $block;
+ $this->head[] = $block;
return $this;
}
@@ -257,7 +257,7 @@ public function add(BlockInterface $block): self {
* The region.
*/
public function prepend(BlockInterface $block): self {
- array_unshift($this->blocks, $block);
+ array_unshift($this->head, $block);
return $this;
}
@@ -289,7 +289,7 @@ public function tail(BlockInterface $block): self {
* The blocks packed from the start, then the ones packed from the end.
*/
public function blocks(): array {
- return [...$this->blocks, ...$this->tail];
+ return [...$this->head, ...$this->tail];
}
/**
@@ -299,7 +299,7 @@ public function blocks(): array {
* The blocks.
*/
public function headBlocks(): array {
- return $this->blocks;
+ return $this->head;
}
/**
From 6385aed915066057d7cd425f456e2e235e940814 Mon Sep 17 00:00:00 2001
From: Alex Skrypnyk
Date: Tue, 11 Aug 2026 15:37:42 +1000
Subject: [PATCH 06/27] Named the narrowed theme argument '$elements' in
'entryLine()' like its siblings.
---
src/Block/Field.php | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/src/Block/Field.php b/src/Block/Field.php
index 3f30f5e1..8c15bb9e 100644
--- a/src/Block/Field.php
+++ b/src/Block/Field.php
@@ -2338,7 +2338,7 @@ protected function valueRegion(ThemeInterface $theme, FieldElementsInterface $el
/**
* One entry as it is drawn.
*
- * @param \DrevOps\Tui\Block\Element\FieldElementsInterface $theme
+ * @param \DrevOps\Tui\Block\Element\FieldElementsInterface $elements
* The theme.
* @param \DrevOps\Tui\Block\Option $entry
* The entry.
@@ -2346,23 +2346,23 @@ protected function valueRegion(ThemeInterface $theme, FieldElementsInterface $el
* @return string
* The drawn entry; empty for a divider, which is a gap and nothing else.
*/
- protected function entryLine(FieldElementsInterface $theme, Option $entry): string {
+ protected function entryLine(FieldElementsInterface $elements, Option $entry): string {
if ($entry->kind === OptionKind::Heading) {
- return $theme->fieldCaption($entry->label);
+ return $elements->fieldCaption($entry->label);
}
if ($entry->kind === OptionKind::Separator) {
- return $theme->fieldEntrySeparator();
+ return $elements->fieldEntrySeparator();
}
// Marking and naming are two elements: the mark records what was picked
// and the text says what it was, so a theme can restyle either alone.
$chosen = $this->isChosen($entry);
- $line = $theme->fieldEntryMarker($chosen, !$this->multiple) . ' ' . $theme->fieldEntry($entry->label, $chosen);
+ $line = $elements->fieldEntryMarker($chosen, !$this->multiple) . ' ' . $elements->fieldEntry($entry->label, $chosen);
// Why an entry cannot be picked belongs beside it, or a row that is drawn
// and refuses the cursor reads as a fault rather than a decision.
- return $entry->disabled && $entry->disabledReason !== '' ? $line . ' ' . $theme->fieldEntryNote($entry->disabledReason) : $line;
+ return $entry->disabled && $entry->disabledReason !== '' ? $line . ' ' . $elements->fieldEntryNote($entry->disabledReason) : $line;
}
/**
From 96b874adc8ce49e40dfe7c39ef73db974649edd2 Mon Sep 17 00:00:00 2001
From: Alex Skrypnyk
Date: Tue, 11 Aug 2026 15:37:49 +1000
Subject: [PATCH 07/27] Renamed 'statusSymbol()' to 'statusGlyph()' for the
theme's glyph vocabulary.
---
src/Theme/DefaultTheme.php | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/Theme/DefaultTheme.php b/src/Theme/DefaultTheme.php
index 8b38e783..56b49da1 100644
--- a/src/Theme/DefaultTheme.php
+++ b/src/Theme/DefaultTheme.php
@@ -1514,7 +1514,7 @@ public function renderBanner(string $logo, string $version): string {
public function renderStatus(Status $status, string $text): string {
// The glyph and the message share one colour so the line reads as a single
// statement, and the glyph alone still tells the five apart without it.
- $line = rtrim($this->statusSymbol($status) . ' ' . $this->linkify($this->oneLine($text)));
+ $line = rtrim($this->statusGlyph($status) . ' ' . $this->linkify($this->oneLine($text)));
return match ($status) {
Status::Note => $this->description($line),
@@ -1572,7 +1572,7 @@ public function renderDefinitions(array $pairs): array {
* @return string
* The glyph, respecting the theme's Unicode mode.
*/
- protected function statusSymbol(Status $status): string {
+ protected function statusGlyph(Status $status): string {
// Every glyph is one column wide in any terminal - none has an emoji
// presentation or an East Asian width - so a run of status lines aligns.
return match ($status) {
From 810218a1365760609d103afd2b9e67abc31969ed Mon Sep 17 00:00:00 2001
From: Alex Skrypnyk
Date: Tue, 11 Aug 2026 15:41:10 +1000
Subject: [PATCH 08/27] Brought 12 files onto the project's blank-line rhythm
around blocks and guards.
---
src/Answers/SummaryFormatter.php | 1 +
src/Discovery/Dotenv.php | 3 +++
src/Discovery/Scan.php | 3 +++
src/Handler/HandlerRegistry.php | 2 ++
src/Input/KeyMap.php | 1 +
src/Input/KeyParser.php | 4 ++++
src/Resolver/InputResolver.php | 2 ++
src/Screen/Region.php | 1 -
src/Terminal/Terminal.php | 3 +++
src/Testing/TuiTester.php | 1 +
src/Translation/Translator.php | 3 +++
src/Tui.php | 1 -
12 files changed, 23 insertions(+), 2 deletions(-)
diff --git a/src/Answers/SummaryFormatter.php b/src/Answers/SummaryFormatter.php
index 6a427477..827862ce 100644
--- a/src/Answers/SummaryFormatter.php
+++ b/src/Answers/SummaryFormatter.php
@@ -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++;
}
diff --git a/src/Discovery/Dotenv.php b/src/Discovery/Dotenv.php
index 9dec7cd8..004641ab 100644
--- a/src/Discovery/Dotenv.php
+++ b/src/Discovery/Dotenv.php
@@ -38,14 +38,17 @@ public function discover(string $directory): mixed {
// @codeCoverageIgnoreEnd
foreach ($lines as $line) {
$line = trim($line);
+
if ($line === '') {
continue;
}
+
if (str_starts_with($line, '#')) {
continue;
}
$pos = strpos($line, '=');
+
if ($pos === FALSE) {
continue;
}
diff --git a/src/Discovery/Scan.php b/src/Discovery/Scan.php
index bae18f17..4497f0be 100644
--- a/src/Discovery/Scan.php
+++ b/src/Discovery/Scan.php
@@ -48,10 +48,13 @@ public function discover(string $directory): mixed {
if ($entry === '.') {
continue;
}
+
if ($entry === '..') {
continue;
}
+
$path = $full . '/' . $entry;
+
if ($this->type === ScanType::Dir && !is_dir($path)) {
continue;
}
diff --git a/src/Handler/HandlerRegistry.php b/src/Handler/HandlerRegistry.php
index 0520e02b..d2a4cb1f 100644
--- a/src/Handler/HandlerRegistry.php
+++ b/src/Handler/HandlerRegistry.php
@@ -70,8 +70,10 @@ public function resolve(string $field_id): ?string {
}
$class = $this->classNameFor($field_id);
+
foreach ($this->namespaces as $namespace) {
$fqcn = $namespace . '\\' . $class;
+
if (class_exists($fqcn)) {
return $this->cache[$field_id] = $fqcn;
}
diff --git a/src/Input/KeyMap.php b/src/Input/KeyMap.php
index ee643edb..373c94d1 100644
--- a/src/Input/KeyMap.php
+++ b/src/Input/KeyMap.php
@@ -152,6 +152,7 @@ protected function buildScope(array $base_inverted, array $layers, Scope $scope)
$scope_inverted = $this->invert($layers[$scope->token()] ?? [], $scope);
$effective = $base_inverted;
+
foreach ($scope_inverted as $token => $entry) {
$effective[$token] = $entry;
}
diff --git a/src/Input/KeyParser.php b/src/Input/KeyParser.php
index 2ad8bfd5..69196cfe 100644
--- a/src/Input/KeyParser.php
+++ b/src/Input/KeyParser.php
@@ -35,6 +35,7 @@ public function parse(string $bytes): array {
while ($i < $length) {
if ($bytes[$i] === "\033") {
[$key, $consumed] = $this->parseEscape($bytes, $i);
+
if ($key instanceof Key) {
$keys[] = $key;
}
@@ -45,6 +46,7 @@ public function parse(string $bytes): array {
// A multi-byte UTF-8 sequence is one typed character, not several keys.
$span = $this->utf8Span($bytes, $i);
+
if ($span > 1) {
$keys[] = Key::char(substr($bytes, $i, $span));
$i += $span;
@@ -158,6 +160,7 @@ protected function parseEscape(string $bytes, int $start): array {
// rather than leaking its tail as typed characters.
$j = $start + 2;
$params = '';
+
while ($j < $length && ord($bytes[$j]) >= 0x30 && ord($bytes[$j]) <= 0x3F) {
$params .= $bytes[$j];
$j++;
@@ -233,6 +236,7 @@ protected function parseMouse(string $bytes, int $start): array {
$length = strlen($bytes);
$j = $start + 3;
$data = '';
+
while ($j < $length && $bytes[$j] !== 'M' && $bytes[$j] !== 'm') {
$data .= $bytes[$j];
$j++;
diff --git a/src/Resolver/InputResolver.php b/src/Resolver/InputResolver.php
index 1ef3a6f3..6ed35779 100644
--- a/src/Resolver/InputResolver.php
+++ b/src/Resolver/InputResolver.php
@@ -132,11 +132,13 @@ protected function parsePrompts(string $prompts): array {
$json = is_file($prompts) ? (string) file_get_contents($prompts) : $prompts;
$data = json_decode($json, TRUE);
+
if (!is_array($data)) {
throw new \InvalidArgumentException(Translator::t('The --prompts value is neither a JSON object nor a path to one.'));
}
$out = [];
+
foreach ($data as $key => $value) {
$out[(string) $key] = $value;
}
diff --git a/src/Screen/Region.php b/src/Screen/Region.php
index 424b0d8b..a7bb42df 100644
--- a/src/Screen/Region.php
+++ b/src/Screen/Region.php
@@ -53,7 +53,6 @@ final class Region implements BorderCapableInterface, ScrollCapableInterface {
*/
protected bool $previews = FALSE;
-
/**
* The blocks packed from the start of its flow, in the order they were added.
*
diff --git a/src/Terminal/Terminal.php b/src/Terminal/Terminal.php
index 413be620..3fa83f53 100644
--- a/src/Terminal/Terminal.php
+++ b/src/Terminal/Terminal.php
@@ -371,9 +371,11 @@ public function queryBackground(): ?string {
}
$chunk = fread($this->input, 64);
+
if (!is_string($chunk)) {
continue;
}
+
if ($chunk === '') {
continue;
}
@@ -405,6 +407,7 @@ public function queryBackground(): ?string {
public static function detectUnicode(): bool {
foreach (['LC_ALL', 'LC_CTYPE', 'LANG'] as $var) {
$value = getenv($var);
+
if (is_string($value) && $value !== '') {
return stripos($value, 'utf') !== FALSE;
}
diff --git a/src/Testing/TuiTester.php b/src/Testing/TuiTester.php
index bb0b644d..8806c502 100644
--- a/src/Testing/TuiTester.php
+++ b/src/Testing/TuiTester.php
@@ -291,6 +291,7 @@ public function update(bool $update = TRUE): self {
*/
public function run(string|Key ...$items): Answers {
$keystrokes = [];
+
foreach ($items as $item) {
$keystrokes[] = $item instanceof Key ? KeyEncoder::encode($item) : $item;
}
diff --git a/src/Translation/Translator.php b/src/Translation/Translator.php
index 1d826377..2f631f2c 100644
--- a/src/Translation/Translator.php
+++ b/src/Translation/Translator.php
@@ -252,6 +252,7 @@ protected static function interpolate(string $message, array $args = []): string
}
$map = [];
+
foreach ($args as $placeholder => $value) {
$map[$placeholder] = (string) $value;
}
@@ -272,9 +273,11 @@ protected static function interpolate(string $message, array $args = []): string
public static function detectLanguage(): string {
foreach (['LC_ALL', 'LC_MESSAGES', 'LANG'] as $var) {
$value = getenv($var);
+
if (!is_string($value)) {
continue;
}
+
if ($value === '') {
continue;
}
diff --git a/src/Tui.php b/src/Tui.php
index 1c6f622d..4b52c628 100644
--- a/src/Tui.php
+++ b/src/Tui.php
@@ -100,7 +100,6 @@ final class Tui {
*/
protected array $flows = [];
-
/**
* The resolved key bindings; NULL uses the default preset.
*/
From 8ea53ac3f8570f22546e46417b14830e2446bd2e Mon Sep 17 00:00:00 2001
From: Alex Skrypnyk
Date: Tue, 11 Aug 2026 15:42:43 +1000
Subject: [PATCH 09/27] Applied the 160-character line rule to three
declarations that broke it both ways.
---
src/Field/Suggest.php | 8 +++++++-
src/Screen/KeyRouter.php | 4 +---
src/Screen/Layout/AbstractLayout.php | 4 +---
3 files changed, 9 insertions(+), 7 deletions(-)
diff --git a/src/Field/Suggest.php b/src/Field/Suggest.php
index c0cdd65a..1c4cea81 100644
--- a/src/Field/Suggest.php
+++ b/src/Field/Suggest.php
@@ -28,7 +28,13 @@
*
* @package DrevOps\Tui\Field
*/
-class Suggest extends AbstractField implements SearchCapableInterface, TextEditCapableInterface, QueryOptionsCapableInterface, PagingCapableInterface, PlaceholderCapableInterface, CompletionCapableInterface {
+class Suggest extends AbstractField implements
+ SearchCapableInterface,
+ TextEditCapableInterface,
+ QueryOptionsCapableInterface,
+ PagingCapableInterface,
+ PlaceholderCapableInterface,
+ CompletionCapableInterface {
use PagingCapableTrait;
use QueryOptionsCapableTrait;
diff --git a/src/Screen/KeyRouter.php b/src/Screen/KeyRouter.php
index 10b191ea..5f5fef2e 100644
--- a/src/Screen/KeyRouter.php
+++ b/src/Screen/KeyRouter.php
@@ -71,9 +71,7 @@ final class KeyRouter {
* @param \DrevOps\Tui\Block\Panel $panel
* The panel it moves around, which is the panel you are in.
*/
- public function __construct(
- protected Panel $panel,
- ) {
+ public function __construct(protected Panel $panel) {
$this->root = $panel;
// One declaration outlives the session driving it, so a session opens on a
diff --git a/src/Screen/Layout/AbstractLayout.php b/src/Screen/Layout/AbstractLayout.php
index 5df8b24f..845434a2 100644
--- a/src/Screen/Layout/AbstractLayout.php
+++ b/src/Screen/Layout/AbstractLayout.php
@@ -56,9 +56,7 @@ abstract class AbstractLayout implements LayoutInterface {
* @param \DrevOps\Tui\Screen\Axis $axis
* The direction its regions run.
*/
- public function __construct(
- protected Axis $axis,
- ) {
+ public function __construct(protected Axis $axis) {
}
/**
From d30814fea5522ccf54b58bee121b1a46a81e9e48 Mon Sep 17 00:00:00 2001
From: Alex Skrypnyk
Date: Tue, 11 Aug 2026 15:42:49 +1000
Subject: [PATCH 10/27] Marked '#[\Override]' only where a concrete inherited
method is overridden.
---
src/Field/Capability/SelectionBoundedTrait.php | 1 +
src/Field/Template.php | 1 -
2 files changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Field/Capability/SelectionBoundedTrait.php b/src/Field/Capability/SelectionBoundedTrait.php
index dd87534a..8fced6df 100644
--- a/src/Field/Capability/SelectionBoundedTrait.php
+++ b/src/Field/Capability/SelectionBoundedTrait.php
@@ -52,6 +52,7 @@ protected function selectionHint(ThemeInterface $theme): string {
/**
* {@inheritdoc}
*/
+ #[\Override]
protected function renderConstraint(ThemeInterface $theme): string {
return $this->selectionHint($theme);
}
diff --git a/src/Field/Template.php b/src/Field/Template.php
index 9e7ade62..fd6e8f22 100644
--- a/src/Field/Template.php
+++ b/src/Field/Template.php
@@ -113,7 +113,6 @@ public function handle(Key $key): void {
*
* The assembled string, with the live buffer standing in for its slot.
*/
- #[\Override]
protected function liveValue(): mixed {
return $this->template->assemble($this->values());
}
From 45f236a8336f2372e326ef35f20574ea81169146 Mon Sep 17 00:00:00 2001
From: Alex Skrypnyk
Date: Tue, 11 Aug 2026 15:42:55 +1000
Subject: [PATCH 11/27] Returned 'static' from the last two fluent setters in
the Block family.
---
src/Block/Breadcrumb.php | 4 ++--
src/Block/Legend.php | 8 ++++----
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/src/Block/Breadcrumb.php b/src/Block/Breadcrumb.php
index 776d7a65..66464f4d 100644
--- a/src/Block/Breadcrumb.php
+++ b/src/Block/Breadcrumb.php
@@ -42,10 +42,10 @@ public function __construct(string ...$segments) {
* @param string ...$segments
* The panel titles, from the root to where you are.
*
- * @return $this
+ * @return static
* The block.
*/
- public function trail(string ...$segments): self {
+ public function trail(string ...$segments): static {
$this->segments = array_map(Ansi::sanitize(...), array_values($segments));
return $this;
diff --git a/src/Block/Legend.php b/src/Block/Legend.php
index 45dc9422..606aed6d 100644
--- a/src/Block/Legend.php
+++ b/src/Block/Legend.php
@@ -48,10 +48,10 @@ final class Legend extends AbstractBlock {
* @param \DrevOps\Tui\Input\Hint ...$hints
* What those keys do, each naming the actions whose keys illustrate it.
*
- * @return $this
+ * @return static
* The block.
*/
- public function advertise(ScopedKeyMap $keys, Hint ...$hints): self {
+ public function advertise(ScopedKeyMap $keys, Hint ...$hints): static {
$this->clear();
$this->keyMap = $keys;
$this->hints = array_values($hints);
@@ -62,10 +62,10 @@ public function advertise(ScopedKeyMap $keys, Hint ...$hints): self {
/**
* Forget every key advertised so far.
*
- * @return $this
+ * @return static
* The block.
*/
- public function clear(): self {
+ public function clear(): static {
$this->keyMap = NULL;
$this->hints = [];
From 5d8e99a184ce2e60fab93c695f1c74d0da7fb341 Mon Sep 17 00:00:00 2001
From: Alex Skrypnyk
Date: Tue, 11 Aug 2026 15:44:03 +1000
Subject: [PATCH 12/27] Ordered 'AgentHelp' constructor arguments as (root,
context, envPrefix) like its siblings.
---
src/Schema/AgentHelp.php | 8 ++++----
src/Tui.php | 2 +-
tests/phpunit/Unit/Schema/AgentHelpTest.php | 8 ++++----
tests/phpunit/Unit/Translation/TranslationRenderTest.php | 2 +-
4 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/src/Schema/AgentHelp.php b/src/Schema/AgentHelp.php
index 3657bc9f..b09a9790 100644
--- a/src/Schema/AgentHelp.php
+++ b/src/Schema/AgentHelp.php
@@ -59,15 +59,15 @@ class AgentHelp {
* @param \DrevOps\Tui\Block\Panel $root
* The declared tree to describe, read from the panel every declared panel
* hangs from.
+ * @param \DrevOps\Tui\Handler\Context $context
+ * The context a closure default is evaluated against; defaults to an empty
+ * context carrying no prior answers.
* @param string $envPrefix
* The prefix for per-question env variable names (e.g. "APP_"); under an
* empty prefix only a field naming its own variable carries the `env`
* annotation.
- * @param \DrevOps\Tui\Handler\Context $context
- * The context a closure default is evaluated against; defaults to an empty
- * context carrying no prior answers.
*/
- public function __construct(protected Panel $root, protected string $envPrefix = '', protected Context $context = new Context()) {
+ public function __construct(protected Panel $root, protected Context $context = new Context(), protected string $envPrefix = '') {
$this->names = new EnvNameResolver($envPrefix);
}
diff --git a/src/Tui.php b/src/Tui.php
index 4b52c628..6460e498 100644
--- a/src/Tui.php
+++ b/src/Tui.php
@@ -783,7 +783,7 @@ public function schema(?Context $context = NULL): array {
* The help text.
*/
public function agentHelp(?Context $context = NULL): string {
- return (new AgentHelp($this->root(), $this->envPrefix, $context ?? new Context()))->generate();
+ return (new AgentHelp($this->root(), $context ?? new Context(), $this->envPrefix))->generate();
}
/**
diff --git a/tests/phpunit/Unit/Schema/AgentHelpTest.php b/tests/phpunit/Unit/Schema/AgentHelpTest.php
index 2950011c..9a9ce4ff 100644
--- a/tests/phpunit/Unit/Schema/AgentHelpTest.php
+++ b/tests/phpunit/Unit/Schema/AgentHelpTest.php
@@ -34,7 +34,7 @@ public function testGenerate(): void {
})
->root();
- $help = (new AgentHelp($form, 'APP_'))->generate();
+ $help = (new AgentHelp($form, envPrefix: 'APP_'))->generate();
$this->assertNotNull(json_decode($help), 'output is valid JSON');
$this->assertStringContainsString('"$schema": "https://json-schema.org/draft/2020-12/schema"', $help);
@@ -177,7 +177,7 @@ static function (PanelBuilder $p): void {
public function testAdvertisesEnvironmentVariables(\Closure $declare, string $prefix, array $contains, array $absent, array $matches): void {
$form = Form::create('T')->panel('p', 'p', $declare)->root();
- $this->assertHelp((new AgentHelp($form, $prefix))->generate(), $contains, $absent, $matches);
+ $this->assertHelp((new AgentHelp($form, envPrefix: $prefix))->generate(), $contains, $absent, $matches);
}
/**
@@ -258,7 +258,7 @@ static function (PanelBuilder $p): void {
public function testResolvesDefault(\Closure $declare, Context $context, array $contains, array $absent): void {
$form = Form::create('T')->panel('p', 'p', $declare)->root();
- $this->assertHelp((new AgentHelp($form, '', $context))->generate(), $contains, $absent, []);
+ $this->assertHelp((new AgentHelp($form, $context))->generate(), $contains, $absent, []);
}
/**
@@ -313,7 +313,7 @@ public function testSkipsNonAnsweringField(\Closure $declare, string $absent): v
$form = Form::create('T')->panel('p', 'p', $declare)->root();
// A field that carries no answer is not one an agent is asked to provide.
- $this->assertHelp((new AgentHelp($form, 'APP_'))->generate(), ['"name"'], [$absent], []);
+ $this->assertHelp((new AgentHelp($form, envPrefix: 'APP_'))->generate(), ['"name"'], [$absent], []);
}
/**
diff --git a/tests/phpunit/Unit/Translation/TranslationRenderTest.php b/tests/phpunit/Unit/Translation/TranslationRenderTest.php
index 804af87e..e345f45e 100644
--- a/tests/phpunit/Unit/Translation/TranslationRenderTest.php
+++ b/tests/phpunit/Unit/Translation/TranslationRenderTest.php
@@ -143,7 +143,7 @@ public function testHeadlessMessagesTranslated(): void {
// A headless validation error and the agent help both localize.
$this->assertContains('Falta la pregunta obligatoria "name".', (new SchemaValidator($form))->validate([]));
- $this->assertStringContainsString('Nombre del sitio', (new AgentHelp($form, 'TUI_'))->generate());
+ $this->assertStringContainsString('Nombre del sitio', (new AgentHelp($form, envPrefix: 'TUI_'))->generate());
}
public function testUkrainianLegendNamesEveryKeyItAdvertises(): void {
From dafbc42e83a52cbc3ad39fdd66dc77b9df53a394 Mon Sep 17 00:00:00 2001
From: Alex Skrypnyk
Date: Tue, 11 Aug 2026 19:25:00 +1000
Subject: [PATCH 13/27] Converged source comments onto the technical register
across 55 files.
---
src/Answers/Answers.php | 6 +-
src/Answers/Provenance.php | 2 +-
src/Answers/ValueFormatter.php | 6 +-
src/Block/AbstractBlock.php | 18 +-
src/Block/BlockInterface.php | 8 +-
.../Capability/ActivateCapableInterface.php | 7 +-
src/Block/FieldType.php | 26 +--
src/Block/Markup.php | 16 +-
src/Block/Option.php | 15 +-
src/Block/Panel.php | 193 +++++++++---------
src/Block/Progress.php | 38 ++--
src/Block/TableSpec.php | 14 +-
src/Block/Tree.php | 77 ++++---
src/Builder/FieldBuilder.php | 67 +++---
src/Builder/Form.php | 95 +++++----
src/Builder/PanelBuilder.php | 54 +++--
src/CancelException.php | 9 +-
src/CollectException.php | 11 +-
src/Field/AbstractField.php | 54 +++--
src/Field/Calendar.php | 29 +--
.../Capability/CompletionCapableTrait.php | 14 +-
.../Capability/FilterCapableInterface.php | 2 +-
src/Field/Capability/FilterCapableTrait.php | 12 +-
.../Capability/OptionsCapableInterface.php | 4 +-
src/Field/Capability/OptionsCapableTrait.php | 8 +-
src/Field/Capability/PagingCapableTrait.php | 8 +-
.../Capability/PlaceholderCapableTrait.php | 9 +-
.../QueryOptionsCapableInterface.php | 25 +--
.../Capability/QueryOptionsCapableTrait.php | 28 +--
.../Capability/SelectionBoundedTrait.php | 20 +-
.../Capability/SelectionCapableTrait.php | 28 ++-
.../Capability/TextEditCapableInterface.php | 2 +-
src/Field/Capability/TextEditCapableTrait.php | 2 +-
src/Field/FieldFactory.php | 20 +-
src/Field/FilePicker.php | 48 +++--
src/Field/Rating.php | 19 +-
src/Field/Reorder.php | 14 +-
src/Field/Search.php | 8 +-
src/Field/Suggest.php | 56 +++--
src/Field/Template.php | 34 ++-
src/FormException.php | 6 +-
src/Input/Action.php | 11 +-
src/Input/Binding.php | 14 +-
.../Capability/BorderCapableInterface.php | 9 +-
src/Screen/Capability/BorderCapableTrait.php | 6 +-
.../Capability/ScrollCapableInterface.php | 23 +--
src/Screen/Capability/ScrollCapableTrait.php | 6 +-
src/Screen/ExternalEditor.php | 6 +-
src/Screen/Overlay.php | 4 +-
src/Screen/Region.php | 71 +++----
src/Screen/Scroller.php | 8 +-
src/Terminal/Ansi.php | 25 +--
src/Translation/Translator.php | 34 ++-
src/Tui.php | 105 +++++-----
src/Utils/Strings.php | 6 +-
55 files changed, 681 insertions(+), 759 deletions(-)
diff --git a/src/Answers/Answers.php b/src/Answers/Answers.php
index d975bd91..f854bbed 100644
--- a/src/Answers/Answers.php
+++ b/src/Answers/Answers.php
@@ -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.
diff --git a/src/Answers/Provenance.php b/src/Answers/Provenance.php
index 31296f1c..7f6487d4 100644
--- a/src/Answers/Provenance.php
+++ b/src/Answers/Provenance.php
@@ -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
diff --git a/src/Answers/ValueFormatter.php b/src/Answers/ValueFormatter.php
index e5bca42e..28a149e4 100644
--- a/src/Answers/ValueFormatter.php
+++ b/src/Answers/ValueFormatter.php
@@ -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
*/
diff --git a/src/Block/AbstractBlock.php b/src/Block/AbstractBlock.php
index 5b72f522..6b7e8586 100644
--- a/src/Block/AbstractBlock.php
+++ b/src/Block/AbstractBlock.php
@@ -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 fails as a type
+ * error 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
*/
@@ -35,7 +33,7 @@ abstract class AbstractBlock implements BlockInterface, BorderCapableInterface {
* @param class-string $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.
diff --git a/src/Block/BlockInterface.php b/src/Block/BlockInterface.php
index d2ee0c78..217d66ae 100644
--- a/src/Block/BlockInterface.php
+++ b/src/Block/BlockInterface.php
@@ -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
*/
diff --git a/src/Block/Capability/ActivateCapableInterface.php b/src/Block/Capability/ActivateCapableInterface.php
index bd0ccaa0..f84bf013 100644
--- a/src/Block/Capability/ActivateCapableInterface.php
+++ b/src/Block/Capability/ActivateCapableInterface.php
@@ -7,16 +7,15 @@
/**
* A block that does something when it is activated.
*
- * Activating it acts rather than reveals: work runs, or the form ends. Nothing
- * it does reaches the collected result, which is what separates it from the
- * block that holds a value.
+ * Activating runs work or ends the form; it reveals nothing. Nothing it does
+ * reaches the collected result, so holding a value is a separate capability.
*
* @package DrevOps\Tui\Block\Capability
*/
interface ActivateCapableInterface {
/**
- * Do what activating this block does.
+ * Perform this block's action.
*
* @return bool
* Whether anything was done.
diff --git a/src/Block/FieldType.php b/src/Block/FieldType.php
index 7bc59411..68f5341c 100644
--- a/src/Block/FieldType.php
+++ b/src/Block/FieldType.php
@@ -7,9 +7,9 @@
/**
* The kinds of answer a field collects.
*
- * Every case names something a reader can be asked for, because a field is the
- * only block that collects: a block that merely shows content or runs work is a
- * kind of its own rather than a kind of answer.
+ * A field is the only block that collects, so every case names an answer a
+ * reader can supply. A block that shows content or runs work is a block kind
+ * of its own, not a kind of answer.
*
* @package DrevOps\Tui\Block
*/
@@ -34,11 +34,6 @@ enum FieldType: string {
/**
* Whether the field's answer is a whole number rather than text.
*
- * The two integer types reach the same shape from opposite directions: a
- * number is typed digit by digit, a rating is stepped along a scale - but
- * both answer with an int, so the type check, the environment coercion and
- * the machine schemas treat them alike.
- *
* @return bool
* TRUE for the integer-valued types.
*/
@@ -96,11 +91,12 @@ public function supportsMultiple(): bool {
/**
* Whether a field of this type may source its options from a live query.
*
- * Only the types whose interaction *is* a query qualify, because a source
- * that follows the query has to show the query it followed: a select filters
- * a known set without displaying the filter, a reorder and a toggle need the
- * whole set at once, and a text field's ghost completion has no list to
- * resolve into.
+ * Only the types whose interaction is a query qualify: query-sourced
+ * options must be shown beside the query that produced them.
+ *
+ * A select filters a known set without displaying the filter. A reorder and
+ * a toggle need the whole set at once, and a text field's ghost completion
+ * has no list to resolve into.
*
* @return bool
* TRUE for the query-driven choice types.
@@ -112,8 +108,8 @@ public function supportsQuerySource(): bool {
/**
* Whether a field of this type can show ghost text in an empty input.
*
- * Template is excluded: it ghosts each empty slot with that slot's own label,
- * so a field-level placeholder would compete with it.
+ * Template is excluded: each empty slot shows its own label as ghost text,
+ * so a field-level placeholder would conflict with it.
*
* @return bool
* TRUE for the types that edit a text buffer or a query line.
diff --git a/src/Block/Markup.php b/src/Block/Markup.php
index 78bbd597..fd5b00e5 100644
--- a/src/Block/Markup.php
+++ b/src/Block/Markup.php
@@ -16,15 +16,15 @@
/**
* Formatted content, and nothing else.
*
- * How it is laid out on the page - prose, a bordered card, a table - is a
- * presentation choice rather than a capability, so all three are this one block
- * drawn three ways. What an earlier answer can change is whether it is there at
- * all, which is why a standing warning needs no block of its own.
+ * Prose, a bordered card and a table are one block drawn three ways: the
+ * layout is a presentation choice rather than a capability. An earlier
+ * answer can change only whether the block is there at all, so a standing
+ * warning needs no block of its own.
*
- * Prose is composed line by line from the theme's markup elements. A card and a
- * grid are laid out rather than styled - they measure their content, wrap it
- * and align it - so both go to the theme's card renderer, the one that already
- * draws a card wherever else the library draws one.
+ * Prose is composed line by line from the theme's markup elements. A card
+ * and a grid are laid out rather than styled - they measure their content,
+ * wrap it and align it - so both go to the theme's card renderer, the same
+ * one that draws a card everywhere else in the library.
*
* @package DrevOps\Tui\Block
*/
diff --git a/src/Block/Option.php b/src/Block/Option.php
index be2a3ad5..4d9da6fd 100644
--- a/src/Block/Option.php
+++ b/src/Block/Option.php
@@ -87,8 +87,8 @@ public function isSelectable(): bool {
/**
* Normalize a value => label map or a list of options into a list of options.
*
- * The map form (`['standard' => 'Standard']`) is the ergonomic shorthand for
- * simple selectable options; richer rows (separators, headings, disabled
+ * The map form (`['standard' => 'Standard']`) is the shorthand for simple
+ * selectable options; richer rows (separators, headings, disabled
* options) are passed as {@see Option} instances. A label defaults to its
* value when empty.
*
@@ -115,12 +115,12 @@ public static function list(array $options): array {
}
/**
- * Normalize what a callable returning options handed back.
+ * Normalize the return value of an option-returning callable.
*
* An option loader and a query source are each declared to return a
- * value => label map, but both are consumer code running mid-session, so a
- * mistyped one degrades to no options rather than erroring where there is no
- * good way to report it.
+ * value => label map, but both are consumer code running mid-session. A
+ * mistyped result degrades to no options rather than erroring where there
+ * is no good way to report it.
*
* @param mixed $result
* The callable's return value.
@@ -136,9 +136,6 @@ public static function resolved(mixed $result): array {
/**
* The values of the selectable rows, in display order.
*
- * The one filtering every collection surface shares, so the field model, the
- * choice fields and the schema generators agree on what is selectable.
- *
* @param list<\DrevOps\Tui\Block\Option> $options
* The option rows.
*
diff --git a/src/Block/Panel.php b/src/Block/Panel.php
index 5008d6e5..b762217a 100644
--- a/src/Block/Panel.php
+++ b/src/Block/Panel.php
@@ -31,20 +31,19 @@
use DrevOps\Tui\Translation\Translator;
/**
- * A destination you can go into and come back from.
+ * A block that navigation enters and leaves.
*
- * It is the only block that nests a layout, and the only one you navigate into:
- * entering it replaces the screen and grows the trail; leaving restores both.
- * A region can hold blocks and a layout can arrange them, but neither is
- * somewhere you go.
+ * It is the only block that nests a layout and the only one navigation
+ * enters: entering replaces the screen and adds a trail segment; leaving
+ * restores both. A region holds blocks and a layout arranges them, but
+ * neither is entered.
*
- * Nested inside another panel it draws a row you select. Once entered it draws
- * nothing of its own, because its blocks do.
+ * Nested inside another panel it draws as a selectable row. Once entered it
+ * draws nothing of its own; its blocks draw instead.
*
- * A whole section can come and go with the answers, exactly as one row can: the
- * condition decides whether the section is there at all, and a section that is
- * not there takes everything it holds with it - so a question inside one is
- * never asked, drawn or navigated into.
+ * A condition decides whether a whole section is there, exactly as one
+ * decides a single row. An absent section takes everything it holds with it,
+ * so a question inside one is never asked, drawn or entered.
*
* @package DrevOps\Tui\Block
*/
@@ -55,57 +54,57 @@ final class Panel extends AbstractBlock implements BindCapableInterface, DependC
use FocusCapableTrait;
/**
- * The region a layout names for the rows a panel holds itself.
+ * The default name of the region holding a panel's own rows.
*/
protected const string ROWS = 'content';
/**
- * The gutter the rows under a panel's title are stepped in by.
+ * The indent the rows under a panel's title are stepped in by.
*/
protected const string INDENT = ' ';
/**
- * How many answers a panel says it is holding before it stops counting.
+ * The maximum number of answers the summary line lists.
*/
protected const int SUMMARY_ANSWERS = 4;
/**
- * How many picks a panel spells out before it says how many there were.
+ * The maximum number of picks listed before a count replaces them.
*/
protected const int SUMMARY_ITEMS = 3;
/**
- * The layout arranging this panel's blocks, once one is given.
+ * The layout arranging this panel's blocks, or NULL before one is set.
*/
protected ?LayoutInterface $layout = NULL;
/**
- * Whether it draws over what is behind it rather than replacing it.
+ * Whether the panel draws over what is behind it rather than replacing it.
*/
protected bool $modal = FALSE;
/**
- * Whether this is the panel you are currently in.
+ * Whether this panel is the entered one.
*/
protected bool $entered = FALSE;
/**
- * The title it carries into the trail.
+ * The title shown in the trail.
*/
protected string $title;
/**
- * The standing text under its title.
+ * The description shown under the title.
*/
protected string $description = '';
/**
- * The way out of it, once it draws over what is behind it.
+ * The button pair that closes a modal panel.
*/
protected Buttons $buttons;
/**
- * What prepares it before it is first entered, until that has been done.
+ * The preparation run before the panel is first entered, until it has run.
*/
protected ?\Closure $preload = NULL;
@@ -115,7 +114,7 @@ final class Panel extends AbstractBlock implements BindCapableInterface, DependC
* @param string $id
* The id it is addressed by.
* @param string $title
- * The title it carries into the trail.
+ * The title shown in the trail.
*/
public function __construct(
protected string $id,
@@ -136,7 +135,7 @@ public function id(): string {
}
/**
- * The title this panel carries into the trail.
+ * The title shown in the trail.
*
* @return string
* The title.
@@ -146,7 +145,7 @@ public function title(): string {
}
/**
- * Set the standing text under this panel's title.
+ * Set the description shown under this panel's title.
*
* @param string $description
* The description.
@@ -161,17 +160,17 @@ public function description(string $description): static {
}
/**
- * The standing text under this panel's title.
+ * The description shown under this panel's title.
*
* @return string
- * The description, empty when it carries none.
+ * The description, empty when none is set.
*/
public function descriptionText(): string {
return $this->description;
}
/**
- * Label the way out of this panel.
+ * Set the button pair that closes this panel.
*
* @param \DrevOps\Tui\Block\Buttons $buttons
* The pair that closes it.
@@ -180,8 +179,8 @@ public function descriptionText(): string {
* The panel.
*
* @throws \DrevOps\Tui\FormException
- * When the pair is hidden on a panel that draws over what is behind it,
- * which would strand it with no way out.
+ * When the pair is hidden on a modal panel, whose buttons are its only
+ * way out.
*/
public function buttons(Buttons $buttons): static {
$this->assertWayOut($this->modal, $buttons);
@@ -191,7 +190,7 @@ public function buttons(Buttons $buttons): static {
}
/**
- * The way out of this panel.
+ * The button pair that closes this panel.
*
* @return \DrevOps\Tui\Block\Buttons
* The pair that closes it.
@@ -204,8 +203,8 @@ public function currentButtons(): Buttons {
* Prepare this panel before it is first entered.
*
* @param \Closure $work
- * An `fn (): void` doing the preparation, such as one fetch several of the
- * panel's fields then read.
+ * An `fn (): void` doing the preparation, for example one fetch several
+ * of the panel's fields then read.
*
* @return static
* The panel.
@@ -217,21 +216,21 @@ public function preload(\Closure $work): static {
}
/**
- * What prepares this panel before it is first entered.
+ * The preparation run before this panel is first entered.
*
* @return \Closure|null
- * The preparation, or NULL when there is none left to do.
+ * The preparation, or NULL when none remains.
*/
public function preparation(): ?\Closure {
return $this->preload;
}
/**
- * Do what was to be done before this panel is first entered.
+ * Run the preparation before this panel is first entered.
*
* @return bool
- * Whether anything was done. Preparation happens once, so every call after
- * the first answers FALSE.
+ * Whether anything ran. Preparation runs once, so every call after the
+ * first returns FALSE.
*/
public function prepare(): bool {
if (!$this->preload instanceof \Closure) {
@@ -315,8 +314,8 @@ public function isEntered(): bool {
* {@inheritdoc}
*
* @throws \DrevOps\Tui\FormException
- * When its buttons are hidden, which would leave it drawn over everything
- * with no way out.
+ * When its buttons are hidden; a modal panel's buttons are its only way
+ * out.
*/
public function modal(): static {
$this->assertWayOut(TRUE, $this->buttons);
@@ -335,9 +334,8 @@ public function isModal(): bool {
/**
* {@inheritdoc}
*
- * A nested panel is a row you select rather than somewhere you are, so it
- * takes no key until you have gone into it - which is what leaves the keys
- * that move the cursor past it reaching the panel it sits in.
+ * An un-entered panel binds no keys, so the keys that move the cursor past
+ * it reach the panel it sits in.
*/
public function binds(Key $key): bool {
return $this->entered && $this->boundAction($key) instanceof Action;
@@ -346,15 +344,14 @@ public function binds(Key $key): bool {
/**
* {@inheritdoc}
*
- * These are the keys you have while you are in a panel rather than inside
- * anything it holds, which is why moving the cursor, going into a nested
- * panel and coming back out again all resolve here.
+ * These keys apply inside a panel itself rather than inside a block it
+ * holds: moving the cursor, entering a nested panel and going back all
+ * resolve here.
*/
public function hints(): array {
- // Anything drawn beside something else is moved between in two directions
- // rather than one, so what the keys do depends on how they are arranged.
- // A grid says so with several regions on one line; an arrangement running
- // across says so with its axis, every region of it being drawn abreast.
+ // Regions drawn side by side are moved between in two directions, so the
+ // move hint depends on the arrangement: a Columns axis draws every region
+ // abreast, and a grid places several regions on one line.
$abreast = $this->layout instanceof LayoutInterface
&& ($this->layout->axis() === Axis::Columns || array_filter($this->layout->lines(), static fn(array $line): bool => count($line) > 1) !== []);
$move = $abreast
@@ -371,8 +368,8 @@ public function hints(): array {
/**
* The fields this panel holds, in the order they were placed.
*
- * Its own only: a nested panel is somewhere you go rather than something this
- * one holds, so what it asks belongs to it.
+ * Its own only: a nested panel's fields belong to that panel and are not
+ * included.
*
* @return list<\DrevOps\Tui\Block\Field>
* The fields.
@@ -392,9 +389,10 @@ public function fields(): array {
/**
* The ids of the rows this panel holds, in the order they were placed.
*
- * Every row that carries one, whether it collects an answer or only shows
- * something: an id that shows is still an id the form knows, which is what
- * tells a stray answer apart from one meant for a row that takes none.
+ * Every row that carries an id is included, whether it collects an answer
+ * or only shows something. A display-only id is still an id the form
+ * knows, so a stray answer can be told from one aimed at a row that takes
+ * none.
*
* @return list
* The ids.
@@ -412,7 +410,7 @@ public function ids(): array {
}
/**
- * The panels you can descend into from this one.
+ * The panels that can be entered from this one.
*
* @return list<\DrevOps\Tui\Block\Panel>
* The sub-panels, in the order they were placed.
@@ -454,8 +452,8 @@ public function blocks(): array {
/**
* {@inheritdoc}
*
- * A row you select: the way in, what it is called, and enough of what is
- * behind it to decide whether to go there.
+ * Draws the panel as a selectable row: its headline, its description and a
+ * summary of the answers it holds.
*/
public function render(ThemeInterface $theme): string {
$elements = $this->guard($theme);
@@ -475,37 +473,34 @@ public function render(ThemeInterface $theme): string {
}
/**
- * This panel drawn as the bare way into it.
+ * This panel drawn as its headline row only.
*
* @param \DrevOps\Tui\Theme\ThemeInterface $theme
* The theme.
*
* @return string
- * The row: where the cursor is, what the panel is called, and the mark
- * saying it leads somewhere.
+ * The row: the selector, the title and the descend mark.
*/
public function wayIn(ThemeInterface $theme): string {
return $this->stepped($this->headline($this->guard($theme)), $this->gutter($theme));
}
/**
- * This panel drawn as a window into it rather than as a row.
+ * This panel drawn as a preview of its rows rather than as one row.
*
- * Where a row says what is behind it in one line, a window shows the rows
- * themselves - which is what a panel sitting beside its siblings has the
- * space for, and what makes a grid of them read as several views of the same
- * form rather than as a list turned sideways.
+ * A preview shows the panel's rows themselves where {@see render()} shows
+ * one summary line, so a panel drawn beside its siblings in a grid reads
+ * as a view of the form.
*
- * A window is a column previewing one panel, so a way into another panel is
- * drawn as the line that says so and nothing more: the row spelling out a
- * description and a summary of what is behind it belongs to a list, where
- * there is a whole width to spend on it.
+ * A preview is one column, so a nested panel inside it is drawn as its
+ * headline line only: the full row with a description and a summary
+ * belongs to a list, which has a whole width to spend on it.
*
* @param \DrevOps\Tui\Theme\ThemeInterface $theme
* The theme.
*
* @return string
- * The window; newlines separate rows.
+ * The preview; newlines separate rows.
*/
public function preview(ThemeInterface $theme): string {
$elements = $this->guard($theme);
@@ -537,9 +532,9 @@ public function preview(ThemeInterface $theme): string {
/**
* The region this panel's own rows are drawn in.
*
- * Where the form goes on a screen and where a panel's rows go inside it are
- * the same question asked of two layouts, so both read the same answer off
- * the layout rather than each going looking for a name.
+ * The screen layout and a panel layout answer this the same way, so the
+ * region is read off the layout's Body furnishing rather than found by a
+ * fixed name.
*
* @return \DrevOps\Tui\Screen\Region
* The region.
@@ -549,7 +544,7 @@ public function place(): Region {
}
/**
- * The theme this panel draws through, refusing to draw an entered one.
+ * The theme this panel draws through, rejecting an entered panel.
*
* @param \DrevOps\Tui\Theme\ThemeInterface $theme
* The theme.
@@ -558,11 +553,11 @@ public function place(): Region {
* The theme, narrowed to the elements a panel draws with.
*
* @throws \LogicException
- * When the panel is the one you are in, which draws nothing of its own.
+ * When the panel is entered; an entered panel draws nothing of its own.
*/
protected function guard(ThemeInterface $theme): PanelElementsInterface {
- // Show and Focus are a nested panel's, not an entered one's: once you are
- // in it, its blocks draw and it draws nothing of its own.
+ // Drawing and focus belong to a nested panel, not an entered one: once
+ // entered, its blocks draw and the panel draws nothing of its own.
if ($this->entered) {
throw new \LogicException(sprintf('Panel "%s" is entered, so its blocks draw rather than the panel itself.', $this->id));
}
@@ -571,20 +566,20 @@ protected function guard(ThemeInterface $theme): PanelElementsInterface {
}
/**
- * The gutter this panel's rows are laid out after.
+ * The gutter this panel's rows are stepped in by.
*
* @param \DrevOps\Tui\Theme\ThemeInterface $theme
* The theme.
*
* @return string
- * The gutter, empty when nothing steps the section in.
+ * The gutter, empty when the section is not stepped in.
*/
protected function gutter(ThemeInterface $theme): string {
return $this->elements($theme, ChromeElementsInterface::class, 'a conditional section')->chromeIndent($this->depth);
}
/**
- * The row that names this panel and says it leads somewhere.
+ * The row joining the selector, the title and the descend mark.
*
* @param \DrevOps\Tui\Block\Element\PanelElementsInterface $elements
* The theme, narrowed to the elements a panel draws with.
@@ -597,7 +592,7 @@ protected function headline(PanelElementsInterface $elements): string {
}
/**
- * The rows this panel's standing text comes to, as they are drawn.
+ * This panel's description, drawn as rows.
*
* @param \DrevOps\Tui\Theme\ThemeInterface $theme
* The theme.
@@ -605,9 +600,9 @@ protected function headline(PanelElementsInterface $elements): string {
* The same theme, narrowed to the elements a panel draws with.
*
* @return list
- * The rows, none when it carries no standing text or there is no room for
- * it: the text is secondary to the rows it introduces, so a theme with
- * nothing to spare drops it.
+ * The rows; none when there is no description or the theme is compact.
+ * The description is secondary to the rows it introduces, so a compact
+ * theme drops it.
*/
protected function guidance(ThemeInterface $theme, PanelElementsInterface $elements): array {
if ($this->description === '' || $this->terse($theme)) {
@@ -620,7 +615,7 @@ protected function guidance(ThemeInterface $theme, PanelElementsInterface $eleme
}
/**
- * What this panel is holding, as one line of answers.
+ * The answers this panel holds, as one line.
*
* @param \DrevOps\Tui\Theme\ThemeInterface $theme
* The theme.
@@ -628,8 +623,8 @@ protected function guidance(ThemeInterface $theme, PanelElementsInterface $eleme
* The same theme, narrowed to the elements a panel draws with.
*
* @return string
- * The answers, empty when it holds none, when none of them is there, or
- * when the theme has no room to say them.
+ * The answers, empty when the panel holds none, when every field is
+ * hidden, or when the theme is compact.
*/
protected function summary(ThemeInterface $theme, PanelElementsInterface $elements): string {
if ($this->terse($theme)) {
@@ -639,16 +634,12 @@ protected function summary(ThemeInterface $theme, PanelElementsInterface $elemen
$answers = [];
foreach ($this->fields() as $field) {
- // A row the answers took off the screen says nothing about the panel,
- // because it is not there to say it.
if ($field->isHidden()) {
continue;
}
$answers[] = $this->held($theme, $field);
- // A row summarizes rather than lists, so it stops well before it would
- // be read as the panel itself.
if (count($answers) >= self::SUMMARY_ANSWERS) {
break;
}
@@ -658,11 +649,11 @@ protected function summary(ThemeInterface $theme, PanelElementsInterface $elemen
}
/**
- * One answer, as the row that stands for a whole panel says it.
+ * One field's answer, as the summary line shows it.
*
- * A handful of picks reads as the picks themselves; more than that would be
- * the panel's whole content spelled out on the line meant to stand for it,
- * so past a handful the line says how many were picked instead.
+ * A value of up to SUMMARY_ITEMS picks is listed as the picks themselves;
+ * a longer one is shown as a count, so the summary line stays a summary
+ * rather than the panel's whole content.
*
* @param \DrevOps\Tui\Theme\ThemeInterface $theme
* The theme.
@@ -670,7 +661,7 @@ protected function summary(ThemeInterface $theme, PanelElementsInterface $elemen
* The field holding the answer.
*
* @return string
- * The answer as it reads on the panel's own row.
+ * The answer as it reads on the summary line.
*/
protected function held(ThemeInterface $theme, Field $field): string {
$value = $field->value();
@@ -683,13 +674,13 @@ protected function held(ThemeInterface $theme, Field $field): string {
}
/**
- * Whether the theme has no room for anything but the rows themselves.
+ * Whether the theme is set to compact spacing.
*
* @param \DrevOps\Tui\Theme\ThemeInterface $theme
* The theme.
*
* @return bool
- * TRUE when it says so.
+ * TRUE when the spacing is compact.
*/
protected function terse(ThemeInterface $theme): bool {
return $theme instanceof OccupyCapableInterface && $theme->spacing() === Spacing::Compact;
@@ -703,10 +694,10 @@ protected function keyScope(): Scope {
}
/**
- * Reject a panel that would draw over everything with no way out.
+ * Reject a modal panel with hidden buttons, which would have no way out.
*
* @param bool $modal
- * Whether it draws over what is behind it.
+ * Whether the panel draws over what is behind it.
* @param \DrevOps\Tui\Block\Buttons $buttons
* The pair that closes it.
*
diff --git a/src/Block/Progress.php b/src/Block/Progress.php
index baa095f1..f02ab589 100644
--- a/src/Block/Progress.php
+++ b/src/Block/Progress.php
@@ -19,8 +19,7 @@
/**
* Work that runs when activated, with an indicator while it does.
*
- * It is the block that separates taking the cursor from reaching the result: it
- * focuses and it acts, and nothing it does becomes an answer.
+ * The block takes focus and acts, and nothing it does becomes an answer.
*
* @package DrevOps\Tui\Block
*/
@@ -30,12 +29,12 @@ final class Progress extends AbstractBlock implements ActivateCapableInterface,
use FocusCapableTrait;
/**
- * The cells a bar's run takes.
+ * The width of the bar's track, in cells.
*/
protected const int TRACK_WIDTH = 10;
/**
- * The steps there are, or NULL when the length is unknown.
+ * The total number of steps, or NULL when the length is unknown.
*/
protected ?int $total = NULL;
@@ -57,7 +56,7 @@ final class Progress extends AbstractBlock implements ActivateCapableInterface,
protected ?\Closure $work = NULL;
/**
- * What the work says it is doing, shown after the indicator.
+ * The text describing what the work is doing, shown after the indicator.
*/
protected string $label = '';
@@ -102,7 +101,7 @@ public function caption(): string {
}
/**
- * Say how many steps the work has, which is what earns it a bar.
+ * Declare how many steps the work has; a known total draws a bar.
*
* @param int $total
* The steps.
@@ -124,7 +123,7 @@ public function steps(int $total): static {
}
/**
- * The steps the work has, which is what earns it a bar.
+ * The number of steps the work has; a known total draws a bar.
*
* @return int|null
* The steps, or NULL when the length is unknown.
@@ -144,7 +143,7 @@ public function current(): int {
}
/**
- * Say what the work is doing right now.
+ * Set the text describing what the work is doing.
*
* @param string $label
* The label, shown after the bar or the spinner.
@@ -159,10 +158,10 @@ public function label(string $label): static {
}
/**
- * What the work says it is doing.
+ * The text describing what the work is doing.
*
* @return string
- * The label, empty when it says nothing.
+ * The label, empty when none is set.
*/
public function labelText(): string {
return $this->label;
@@ -200,15 +199,14 @@ public function workload(): ?\Closure {
* @param int $steps
* The steps done since the last report.
* @param string|null $label
- * What is being done now, or NULL to leave the label as it stands.
+ * The new label, or NULL to leave the label unchanged.
*
* @return static
* The block.
*/
public function advance(int $steps = 1, ?string $label = NULL): static {
- // Work can report a step backwards, and a spinner frame is an index: both
- // are clamped here so no caller can drive the block into a state it could
- // not draw.
+ // A report may step backwards and the spinner frame is an index, so both
+ // are clamped to keep the block in a drawable state.
$this->current = max(0, $this->current + $steps);
$this->frame = max(0, $this->frame + $steps);
@@ -231,8 +229,8 @@ public function activate(): bool {
return FALSE;
}
- // The work is handed a reporter rather than the block, so it says only that
- // one more step is done and never reaches the state behind the indicator.
+ // The work receives a reporter rather than the block, so it can only
+ // report a step and never reaches the state behind the indicator.
($this->work)(new ProgressReporter(function (?string $label): void {
$this->advance(1, $label);
}));
@@ -247,11 +245,11 @@ public function render(ThemeInterface $theme): string {
$elements = $this->elements($theme, ProgressElementsInterface::class, 'progress');
$gutter = $this->elements($theme, ChromeElementsInterface::class, 'a conditional row')->chromeIndent($this->depth);
$caption = $elements->progressCaption($this->caption);
- // The caption names the work and stays put; the label is what that work is
- // doing right now, so it trails the indicator and changes under it.
+ // The caption names the work and is fixed; the label describes the
+ // current activity, so it follows the indicator and changes with it.
$label = $this->label === '' ? '' : ' ' . $elements->progressCaption($this->label);
- // The row takes the cursor and starting the work is a key press away from
- // there, so it says where the cursor is the way every other row does.
+ // The row takes focus and activation starts the work, so it draws a
+ // selector mark the way every other row does.
$mark = $elements->progressSelector($this->isFocused()) . ' ';
if ($this->total === NULL) {
diff --git a/src/Block/TableSpec.php b/src/Block/TableSpec.php
index 0c8ec4ff..11bda683 100644
--- a/src/Block/TableSpec.php
+++ b/src/Block/TableSpec.php
@@ -9,9 +9,8 @@
/**
* A presentational table: header cells and body rows, coerced to strings.
*
- * A note carries one to render an aligned grid beneath its title and body. The
- * cells are stored as plain strings; how they are styled and interpolated is
- * the renderer's concern, not this value object's.
+ * The cells are stored as plain strings; how they are styled and interpolated
+ * is the renderer's concern, not this value object's.
*
* @package DrevOps\Tui\Block
*/
@@ -34,10 +33,11 @@
/**
* Construct a table spec, coercing every cell to a string.
*
- * A scalar cell (a number, a bool) is stringified so tabular data need not be
- * pre-formatted, and a non-scalar cell becomes an empty string, so the
- * renderer only ever handles strings. An empty header list renders the grid
- * with no header row.
+ * A scalar cell (a number, a bool) is stringified so tabular data need not
+ * be pre-formatted. A non-scalar cell becomes an empty string, so the
+ * renderer only ever handles strings.
+ *
+ * An empty header list renders the grid with no header row.
*
* @param list $headers
* The header cells.
diff --git a/src/Block/Tree.php b/src/Block/Tree.php
index b3c0362a..ba04afe6 100644
--- a/src/Block/Tree.php
+++ b/src/Block/Tree.php
@@ -11,11 +11,9 @@
/**
* A declared block tree, read as a flat list.
*
- * A panel knows what it holds and which panels hang from it, and nothing more:
- * flattening the two into the order a form is declared in is a question about
- * the whole tree rather than about any one panel. It lives here so the headless
- * collection and the machine-readable descriptions read one order rather than
- * each walking their own.
+ * A panel knows only its own blocks and its child panels. Flattening the
+ * tree lives here so the headless collection and the machine-readable
+ * descriptions read one order rather than each walking their own.
*
* The order is declaration order: a panel's own rows first, then everything
* beneath it, panel by panel.
@@ -65,14 +63,13 @@ public static function panels(Panel $panel): array {
/**
* Which blocks a panel and the panels beneath it leave on the form.
*
- * A block decides for itself whether it is there, but a section carries what
- * it holds: a block inside a section the answers took off the form is not
- * there either, however its own rule reads. Composing the two is a question
- * about the whole tree rather than about any one block, which is why it is
- * answered here rather than by whoever asks.
+ * A block's own condition decides its presence, and an absent section
+ * removes every block it holds, however that block's own rule reads.
+ * Composing the two is a question about the whole tree, so it is answered
+ * here once rather than by each caller.
*
- * Keyed by object rather than by id, because a section and a row it holds may
- * go by the same name and only one set of ids is ever checked for collisions.
+ * Keyed by object rather than by id: a section and a row it holds may
+ * share a name, and only one set of ids is ever checked for collisions.
*
* @param \DrevOps\Tui\Block\Panel $panel
* The panel to walk.
@@ -102,14 +99,13 @@ public static function within(Panel $panel, array $answers, bool $inside = TRUE)
}
/**
- * Which blocks are there once the answers nobody was asked for stop counting.
+ * Which blocks are there once answers for absent blocks stop counting.
*
- * The companion to {@see within()} for a whole answer set read in one go. A
- * value belonging to a block the answers took off the form is a value the
- * form never asked for, so it cannot be what puts another block on it -
- * measuring the conditions against the raw set would let one do exactly that.
- * Dropping a value can take a further block away, so the reading is repeated
- * until nothing moves.
+ * The companion to {@see within()} for a whole answer set read in one go.
+ * A value belonging to an absent block was never asked for, so it must not
+ * put another block on the form; measuring the conditions against the raw
+ * set would allow exactly that. Dropping a value can remove a further
+ * block, so the reading is repeated until nothing changes.
*
* Keyed by object rather than by id, for the reason {@see within()} is.
*
@@ -123,9 +119,9 @@ public static function within(Panel $panel, array $answers, bool $inside = TRUE)
*/
public static function settled(Panel $panel, array $answers): array {
$there = self::within($panel, $answers);
- // A settled reading exits below, so the bound only guards a set that never
- // settles: one pass per field covers the longest chain of blocks that can
- // take each other away, plus one to confirm nothing moved.
+ // A settled reading returns inside the loop, so the bound only guards a
+ // set that never settles: one pass per field covers the longest removal
+ // chain, plus one pass to confirm nothing changed.
$limit = count(self::fields($panel)) + 1;
for ($pass = 0; $pass < $limit; $pass++) {
@@ -142,7 +138,7 @@ public static function settled(Panel $panel, array $answers): array {
}
/**
- * The answers left once the ones nobody was asked for are dropped.
+ * The answers left once those belonging to absent blocks are dropped.
*
* @param \DrevOps\Tui\Block\Panel $panel
* The panel to walk.
@@ -152,9 +148,9 @@ public static function settled(Panel $panel, array $answers): array {
* Which blocks are there, keyed by object id.
*
* @return array
- * The answers, less the ones belonging to a block that is not there. A
- * value under an id the tree does not know is left alone, because whether
- * it belongs to the form at all is somebody else's question.
+ * The answers, less the ones belonging to an absent block. A value under
+ * an id the tree does not know is left alone; whether it belongs to the
+ * form at all is out of scope here.
*/
public static function held(Panel $panel, array $answers, array $there): array {
foreach (self::fields($panel) as $field) {
@@ -167,15 +163,15 @@ public static function held(Panel $panel, array $answers, array $there): array {
}
/**
- * The one rule deciding whether each block is there, as it can be read.
+ * The one rule deciding whether each block is there, in readable form.
*
* The companion to {@see within()}: where that measures the composed rule
- * against one answer set, this hands the rule itself over, so anything
- * describing the form can publish what a block waits on rather than only
- * whether it is there right now.
+ * against one answer set, this returns the rule itself, so a description
+ * of the form can publish what a block depends on rather than only whether
+ * it is there right now.
*
- * A rule a block decides for itself cannot be read, only asked, so it counts
- * as no rule here - the same reading {@see DependCapableTrait::rule()} takes.
+ * A closure rule can be evaluated but not read, so it counts as no rule
+ * here - the same reading {@see DependCapableTrait::rule()} takes.
*
* Keyed by object rather than by id, for the reason {@see within()} is.
*
@@ -209,18 +205,17 @@ public static function gates(Panel $panel, ?ConditionInterface $inherited = NULL
/**
* Whether anything at all decides that each block is there.
*
- * The question {@see gates()} cannot answer. A rule a block decides for
- * itself is a real rule that simply cannot be read, so gates() hands back
- * nothing for it - and anything that took "no rule to publish" for "asked on
- * every run" would state something false about the form. This says only
- * whether a block waits on anything, which is answerable either way.
+ * The question {@see gates()} cannot answer: a closure rule is a real rule
+ * that cannot be read, so gates() returns NULL for it, and a NULL gate
+ * must not be taken for "asked on every run". This reports only whether a
+ * block depends on anything, which both kinds of rule can answer.
*
* Keyed by object rather than by id, for the reason {@see within()} is.
*
* @param \DrevOps\Tui\Block\Panel $panel
* The panel to walk.
* @param bool $inside
- * Whether the sections holding this one already wait on something.
+ * Whether the sections holding this one already depend on something.
*
* @return array
* TRUE for each block the answers can take off the form, keyed by its
@@ -252,9 +247,9 @@ public static function gated(Panel $panel, bool $inside = FALSE): array {
* The block's own rule, or NULL when it declares none.
*
* @return \DrevOps\Tui\Condition\ConditionInterface|null
- * Both rules combined, the one that exists, or NULL when neither does. One
- * rule is handed back as it is rather than wrapped, so a reader is never
- * given an "all" of a single condition to unpick.
+ * Both rules combined, the one that exists, or NULL when neither does. A
+ * single rule is returned as it is rather than wrapped, so a reader is
+ * never given an "all" of one condition to unpick.
*/
protected static function both(?ConditionInterface $outer, ?ConditionInterface $own): ?ConditionInterface {
if (!$outer instanceof ConditionInterface) {
diff --git a/src/Builder/FieldBuilder.php b/src/Builder/FieldBuilder.php
index 9f15bf0a..a067ca94 100644
--- a/src/Builder/FieldBuilder.php
+++ b/src/Builder/FieldBuilder.php
@@ -21,15 +21,14 @@
/**
* A fluent builder for a single field: the block that collects.
*
- * What is declared lands on the field as it is written. A declaration stated in
- * parts - a range, a shape, a set of limits - is assembled when the declaration
- * is finished, because only a finished one can be measured for the
- * contradictions it must not hold.
+ * A declaration is written onto the field as it is made. A declaration
+ * stated in parts - a range, a shape, a set of limits - is assembled when
+ * the declaration is finished, because only a finished one can be checked
+ * for contradictions.
*
- * Most of what can be declared belongs to some kinds of answer and not to
- * others, and a declaration the kind has nowhere to put is refused where it was
- * written rather than quietly dropped - so a mistake is read at the line that
- * made it.
+ * Most declarations apply to some kinds of answer and not to others, and one
+ * the kind has nowhere to put is rejected where it was written rather than
+ * quietly dropped, so the error points at the line that made it.
*
* @package DrevOps\Tui\Builder
*/
@@ -212,8 +211,8 @@ public function description(string $description): self {
/**
* Set the help: the long-form text behind the field's help key.
*
- * Where a description has to fit under the row, help opens on its own page,
- * it can run to paragraphs and carry the detail a row has no space for.
+ * A description has to fit under the row; help opens on its own page, so it
+ * can run to paragraphs and hold the detail a row has no space for.
*
* @param string $help
* The help text; blank lines separate paragraphs.
@@ -254,9 +253,9 @@ public function placeholder(string $placeholder): self {
*
* @param mixed $default
* The default value, or a `fn (Context): mixed` closure computing a
- * dynamic default from the run context. A closure that cannot be evaluated
- * without answers reads as null in machine-readable output unless a
- * {@see schemaDefault()} stands in for it.
+ * dynamic default from the run context. A closure that cannot be
+ * evaluated without answers reads as null in machine-readable output
+ * unless a {@see schemaDefault()} replaces it there.
*
* @return $this
* The builder.
@@ -290,10 +289,11 @@ public function schemaDefault(mixed $default): self {
/**
* Set the environment variable that answers the field.
*
- * The name is absolute: the form's prefix is not applied to it, so a variable
- * name published elsewhere can be reproduced exactly. It replaces the
- * mechanical `` name rather than adding to it - declare the
- * mechanical name through {@see envAliases()} to keep honouring it too.
+ * The name is absolute: the form's prefix is not applied to it, so a
+ * variable name published elsewhere can be reproduced exactly. It replaces
+ * the mechanical `` name rather than adding to it;
+ * declare the mechanical name through {@see envAliases()} so it still
+ * answers the field.
*
* @param string $name
* The variable name.
@@ -308,7 +308,7 @@ public function env(string $name): self {
}
/**
- * Set the further environment variables the field also answers to.
+ * Set further environment variables that also answer the field.
*
* Each is absolute, like {@see env()}, and they are consulted in order after
* the canonical name - so the canonical name wins when both are set, and a
@@ -431,9 +431,9 @@ public function externalEditor(bool $enabled = TRUE): self {
*
* A field is edited inline by default - its editor expands in place on the
* panel when activated, and collapses back on accept or cancel. Declaring it
- * standalone opens that same editor full-screen instead: the better fit for a
- * field that wants the whole viewport, such as a long option list, a month
- * calendar or a multi-line textarea.
+ * standalone opens that same editor full-screen instead: the better fit for
+ * a field that needs the whole viewport, such as a long option list, a
+ * month calendar or a multi-line textarea.
*
* @param bool $standalone
* TRUE for the full-screen editor; FALSE to restore inline editing.
@@ -499,8 +499,8 @@ public function max(int $max): self {
* are already its steps.
*/
public function step(int $step): self {
- // Named ahead of the kind check: a scale does count through numbers, so
- // what is wrong here is the increment rather than the kind.
+ // Checked before the kind check: a scale does count through numbers, so
+ // the error here is the increment rather than the kind.
if ($this->fieldType === FieldType::Rating) {
throw new FormException(sprintf('Field "%s" declares a step of %d on a scale whose points are its steps; remove ->step() and set the ends with ->min() and ->max().', $this->id, $step));
}
@@ -1032,10 +1032,9 @@ public function heading(string $label): self {
/**
* Add several options from a value => label map, or a callback for them.
*
- * A callback's own signature says when it runs. One that asks for the run
+ * The callback's signature determines when it runs. One that takes the run
* context follows the collected answers: it is called again whenever they
- * change, so one field's choices can narrow by another's answer - a basket
- * that stops offering what the chosen category does not hold. It runs as
+ * change, so one field's choices can narrow by another's answer. It runs as
* part of the form settling, before anything is drawn or validated, so the
* narrowed set is the one every surface sees: the panel, headless
* collection, the schema and the validator. A value the narrowed set no
@@ -1044,7 +1043,7 @@ public function heading(string $label): self {
* was supplied headlessly, which is reported instead. Keep such a callback
* cheap: it runs for the whole form, not once per panel.
*
- * A callback that asks for nothing loads one list, once, lazily when the
+ * A callback with no parameters loads one list, once, lazily when the
* field's panel opens - showing a themed "Loading…" beside the field until
* it returns, and running after the panel's `->preload()` so it can read
* what preload prepared.
@@ -1066,8 +1065,8 @@ public function options(array|\Closure $options): self {
$this->scoped(self::lists(), 'shows no options', 'options');
if ($options instanceof \Closure) {
- // Reading the signature here, rather than at every call, keeps the two
- // lifecycles apart without a reflection call mid-session.
+ // The signature is read here rather than at every call, so the
+ // lifecycle is chosen without a reflection call mid-session.
if ((new \ReflectionFunction($options))->getNumberOfParameters() > 0) {
$this->block->resolve($options);
}
@@ -1094,8 +1093,8 @@ public function options(array|\Closure $options): self {
* index. The field shows a themed "Loading…" while a call is in flight, and
* the result replaces the list wholesale, so the local filter is turned off.
*
- * Repeats are free: a query already answered in this editor session is served
- * from a cache, and a burst of typing resolves once, not once per character.
+ * A query already answered in this editor session is served from a cache,
+ * and a burst of typing resolves once, not once per character.
*
* @param \Closure $source
* An
@@ -1120,9 +1119,9 @@ public function optionsFrom(\Closure $source): self {
/**
* Set the query length below which the query source is not called.
*
- * A remote source asked for the empty query has to answer with everything, so
- * a floor keeps the field quiet until the query is worth sending. Below it
- * the field shows a prompt to keep typing instead of a list.
+ * A remote source asked for the empty query has to answer with everything,
+ * so no query shorter than the floor is sent. Below the floor the field
+ * shows a prompt to keep typing instead of a list.
*
* @param int $length
* The minimum number of characters, at least one.
diff --git a/src/Builder/Form.php b/src/Builder/Form.php
index 2b73a32c..20462eec 100644
--- a/src/Builder/Form.php
+++ b/src/Builder/Form.php
@@ -30,7 +30,7 @@
final class Form {
/**
- * The region a panel goes in when the arrangement keeps no window for it.
+ * The region a panel goes in when no grid window is declared for it.
*/
protected const string CONTENT = 'content';
@@ -79,7 +79,7 @@ final class Form {
protected ?GridLayout $layout = NULL;
/**
- * The panel every declared panel hangs from, once the form is finished.
+ * The root panel holding every declared panel, once the form is finished.
*/
protected ?Panel $root = NULL;
@@ -231,10 +231,10 @@ public function layout(int ...$rows): self {
/**
* The block tree this form declares.
*
- * The panels hang from one root, so the whole declaration is reachable from a
- * single block - the panel a screen starts in, and the one a headless
- * collection walks. The tree is the declaration rather than a view of it, so
- * it is written once and handed back as it stands.
+ * Every panel is a child of one root, so the whole declaration is reachable
+ * from a single block - the panel a screen starts in, and the one headless
+ * collection traverses. The tree is built once; later calls return the same
+ * instance.
*
* @return \DrevOps\Tui\Block\Panel
* The root panel, carrying the form's own name.
@@ -249,15 +249,15 @@ public function root(): Panel {
$this->layout?->assertDeals(count($this->panels), $this->title);
- // The root is the form itself rather than a panel somebody declared, so it
- // is addressed by the name the form goes by.
+ // The root is the form itself rather than a declared panel, so its id is
+ // the form's title.
$root = (new Panel($this->title, $this->title))->layout($this->layout ?? new PanelLayout());
$root->buttons(new Buttons($this->buttons, $this->submitLabel, $this->cancelLabel));
foreach ($this->panels as $index => $panel) {
$panel->seal();
// A grid draws each panel in a window of its own, and a window is a
- // region, so the panel is placed in the one that names it.
+ // region, so each panel is placed in the window at its index.
$root->in($this->layout instanceof GridLayout ? $this->layout->windows()[$index] : self::CONTENT)->add($panel->block());
}
@@ -305,11 +305,10 @@ public function currentFixups(): array {
}
/**
- * Assert that nothing the tree is built from is declared after it is built.
+ * Assert that the tree has not been built yet.
*
- * The tree is written once and handed back as it stands, so a declaration
- * arriving after it reaches nothing - and a panel nobody can see is worse
- * than a form that refuses to take it.
+ * The tree is built once and reused, so a declaration arriving after the
+ * build would never reach it; the form throws instead of dropping it.
*
* @param string $declaration
* What is being declared, as a fragment naming it.
@@ -327,7 +326,7 @@ protected function assertUnbuilt(string $declaration): void {
* Assert that every field id is unique across the panel tree.
*
* @param \DrevOps\Tui\Block\Panel $root
- * The panel every declared panel hangs from.
+ * The root panel holding every declared panel.
*/
protected function assertUniqueFieldIds(Panel $root): void {
$seen = [];
@@ -342,15 +341,14 @@ protected function assertUniqueFieldIds(Panel $root): void {
}
/**
- * Say how many answers each block waits on before it is there at all.
+ * Set the nesting depth of every conditionally visible block.
*
- * A rule may name a field on any panel, and a section carries what it holds,
- * so this is a fact about the whole form rather than about a panel or a block
- * on its own: it can only be worked out once every panel has been placed,
- * which is here.
+ * A rule may name a field on any panel, and a section's rule covers what it
+ * holds, so a block's depth is a fact about the whole form: it can only be
+ * computed once every panel has been placed.
*
* @param \DrevOps\Tui\Block\Panel $root
- * The panel every declared panel hangs from.
+ * The root panel holding every declared panel.
*/
protected function nestConditionals(Panel $root): void {
$holders = [];
@@ -369,13 +367,13 @@ protected function nestConditionals(Panel $root): void {
}
/**
- * Every block a tree holds that can come and go, and what holds each.
+ * Every block a condition can hide, and the panel holding each.
*
* @param \DrevOps\Tui\Block\Panel $panel
* The panel to walk.
* @param array $holders
- * The section each block sits in, keyed by the block's object id, written
- * here as the walk goes.
+ * The section each block sits in, keyed by the block's object id,
+ * populated as the walk proceeds.
*
* @return list<\DrevOps\Tui\Block\Capability\DependCapableInterface>
* The blocks, in declaration order.
@@ -400,14 +398,14 @@ protected function conditionals(Panel $panel, array &$holders): array {
}
/**
- * How deep one block sits: one step per rule in the chain leading to it.
+ * The nesting depth of one block: one step per rule in the chain to it.
*
- * The chain runs through the sections as well as through the rules: a section
- * only there behind an answer is one such rule, so everything it holds counts
- * it once, and a block with a rule of its own is the next step in from there.
+ * The chain runs through the sections as well as through the rules: a
+ * section gated by a rule adds one step counted by everything it holds, and
+ * a block with a rule of its own adds another.
*
- * Measured by object rather than by id, because a section and a row it holds
- * may go by the same name.
+ * Measured by object rather than by id, because a section and a row it
+ * holds may share the same id.
*
* @param \DrevOps\Tui\Block\Capability\DependCapableInterface $block
* The block to measure.
@@ -416,7 +414,8 @@ protected function conditionals(Panel $panel, array &$holders): array {
* @param array $by_id
* Every field in the form, keyed by id.
* @param array $resolved
- * The depths measured so far, so a block several rules name is walked once.
+ * The depths measured so far, so a block named by several rules is walked
+ * once.
* @param array $walking
* The blocks on the current walk, keyed by object id.
*
@@ -430,9 +429,9 @@ protected function nesting(DependCapableInterface $block, array $holders, array
return $resolved[$at];
}
- // A reference leading back to a block already on the walk closes a cycle.
- // Such a rule can never hold a stable depth, so the back edge contributes
- // none and the walk ends rather than deepening forever.
+ // A reference back to a block already on the walk closes a cycle. A
+ // cyclic rule has no stable depth, so the back edge contributes nothing
+ // and the recursion stops.
if (isset($walking[$at])) {
return 0;
}
@@ -441,8 +440,8 @@ protected function nesting(DependCapableInterface $block, array $holders, array
$holder = $holders[$at] ?? NULL;
$base = $holder instanceof Panel ? $this->nesting($holder, $holders, $by_id, $resolved, $walking) : 0;
- // A rule the block decides for itself names no question, so nothing can be
- // said about what it waits on and it sits where its section's rows do.
+ // A closure rule names no field, so nothing is known about what it
+ // depends on and the block takes its section's depth.
if (!$block->condition() instanceof ConditionInterface) {
return $resolved[$at] = $base;
}
@@ -462,7 +461,7 @@ protected function nesting(DependCapableInterface $block, array $holders, array
* Assert that nothing is declared on a field that never draws it.
*
* @param \DrevOps\Tui\Block\Panel $root
- * The panel every declared panel hangs from.
+ * The root panel holding every declared panel.
*/
protected function assertFieldSurfaces(Panel $root): void {
foreach (Tree::fields($root) as $field) {
@@ -479,13 +478,13 @@ protected function assertFieldSurfaces(Panel $root): void {
/**
* Assert that every field is offered exactly one set of rows it can hold.
*
- * A field's rows may stand as declared, arrive from a loader, follow the
- * answers or follow a query, and each of the four replaces the others - so a
+ * A field's rows come from one of four sources - declared entries, a
+ * loader, a resolver or a query source - and each replaces the others. A
* field offered two of them has no one set, and a field offered any of them
* on a kind that shows no list has nowhere to put them.
*
* @param \DrevOps\Tui\Block\Panel $root
- * The panel every declared panel hangs from.
+ * The root panel holding every declared panel.
*/
protected function assertEntrySources(Panel $root): void {
foreach (Tree::fields($root) as $field) {
@@ -518,7 +517,7 @@ protected function assertEntrySources(Panel $root): void {
* Assert that every toggle field declares exactly two entries.
*
* @param \DrevOps\Tui\Block\Panel $root
- * The panel every declared panel hangs from.
+ * The root panel holding every declared panel.
*/
protected function assertToggleEntries(Panel $root): void {
foreach (Tree::fields($root) as $field) {
@@ -526,8 +525,6 @@ protected function assertToggleEntries(Panel $root): void {
continue;
}
- // Rows that arrive later cannot be counted here, and the default they
- // would be checked against is the one they will settle on.
if (!$this->settled($field)) {
continue;
}
@@ -559,10 +556,10 @@ protected function assertToggleEntries(Panel $root): void {
* Assert that every reorder field declares at least two plain entries.
*
* A ranking arranges a flat list, so headings, separators and disabled rows
- * have no place in it, and fewer than two items is nothing to reorder.
+ * are not allowed in it, and fewer than two items leaves nothing to reorder.
*
* @param \DrevOps\Tui\Block\Panel $root
- * The panel every declared panel hangs from.
+ * The root panel holding every declared panel.
*/
protected function assertReorderEntries(Panel $root): void {
foreach (Tree::fields($root) as $field) {
@@ -570,7 +567,6 @@ protected function assertReorderEntries(Panel $root): void {
continue;
}
- // Rows that arrive later are not there to be counted or vetted here.
if (!$this->settled($field)) {
continue;
}
@@ -593,7 +589,7 @@ protected function assertReorderEntries(Panel $root): void {
* Assert that every template field declares the shape it fills in.
*
* @param \DrevOps\Tui\Block\Panel $root
- * The panel every declared panel hangs from.
+ * The root panel holding every declared panel.
*/
protected function assertTemplateShapes(Panel $root): void {
foreach (Tree::fields($root) as $field) {
@@ -615,7 +611,7 @@ protected function assertTemplateShapes(Panel $root): void {
* has no defined layout.
*
* @param \DrevOps\Tui\Block\Panel $root
- * The panel every declared panel hangs from.
+ * The root panel holding every declared panel.
*/
protected function assertModalPanels(Panel $root): void {
foreach (Tree::panels($root) as $panel) {
@@ -632,8 +628,9 @@ protected function assertModalPanels(Panel $root): void {
* The field.
*
* @return bool
- * FALSE while a loader, a resolver or a query source still owes the field
- * its rows, so there is nothing yet to count or to check a default against.
+ * FALSE while a loader, a resolver or a query source has not yet supplied
+ * the rows, so there is nothing yet to count or to check a default
+ * against.
*/
protected function settled(Field $field): bool {
return !$field->loader() instanceof \Closure && !$field->resolver() instanceof \Closure && !$field->source() instanceof \Closure;
diff --git a/src/Builder/PanelBuilder.php b/src/Builder/PanelBuilder.php
index 056671c1..dc44af53 100644
--- a/src/Builder/PanelBuilder.php
+++ b/src/Builder/PanelBuilder.php
@@ -21,10 +21,10 @@
* A fluent builder for a panel: what it holds, and how it is arranged.
*
* Every level of the hierarchy has a default, so a three-field panel names no
- * layout and no region: it takes the one region a panel wants most of the time
- * and the fields go in it in the order they are written. Naming a layout is
- * what a panel does only when it needs one, and then a block says which region
- * it belongs to rather than depending on the order it was declared in.
+ * layout and no region: the fields go into the default region in the order
+ * they are written. A panel names a layout only when it needs one, and a
+ * block then names the region it goes in rather than depending on the order
+ * it was declared in.
*
* @package DrevOps\Tui\Builder
*/
@@ -123,9 +123,9 @@ public function description(string $description): self {
/**
* Set the conditional-visibility rule.
*
- * A section comes and goes exactly as a field does, and takes everything it
- * holds with it: while the condition does not hold, its questions are not
- * asked, not drawn and not in the answers.
+ * A section is shown and hidden exactly as a field is, and the rule covers
+ * everything it holds: while the condition does not hold, its questions are
+ * not asked, not drawn and not in the answers.
*
* @param \DrevOps\Tui\Condition\ConditionInterface $condition
* The condition gating the panel, evaluated as the answers settle.
@@ -170,8 +170,8 @@ public function modal(string $submit_label = 'Submit', string $cancel_label = 'C
* The builder.
*/
public function in(string $name): self {
- // Reached now rather than when a block arrives, so a name that was never
- // declared is caught where it was written.
+ // Called now rather than when a block is added, so an undeclared name
+ // fails at the line that declared it.
$this->panel->in($name);
$this->region = $name;
@@ -420,9 +420,9 @@ public function pause(string $id, string $label = ''): FieldBuilder {
/**
* Add a titled note: markup written title first.
*
- * Sugar over {@see markup()} for the common shape of a card, where the title
- * is what you write first and the body follows through `->body()`. It builds
- * the same block, so every markup call is available on it.
+ * Sugar over {@see markup()} for the common shape of a card: the title
+ * comes first and the body follows through `->body()`. It builds the same
+ * block, so every markup call is available on it.
*
* @param string $id
* The block id.
@@ -437,12 +437,12 @@ public function note(string $id, string $title = ''): Markup {
}
/**
- * Add markup: formatted content, and nothing else.
+ * Add markup: formatted content only.
*
* Chain `->border()` to draw it inside a card and `->table()` to lay it out
* as a grid; both are presentation choices over the same block. `->when()`
- * gates it on an earlier answer, which is what lets a warning appear only
- * when one calls for it.
+ * gates it on an earlier answer, so a warning can appear only when an
+ * answer calls for it.
*
* @param string $id
* The block id.
@@ -564,9 +564,9 @@ public function layout(int|string ...$rows): self {
/**
* Run a hook once before the panel first opens.
*
- * The panel reads as loading while the hook runs, so a drill-in that has to
- * prepare shared data - one fetch feeding several of the panel's fields -
- * shows feedback instead of freezing. Runs once, then never again.
+ * The panel shows a loading state while the hook runs, so a drill-in that
+ * has to prepare shared data - one fetch feeding several of the panel's
+ * fields - shows feedback instead of freezing. The hook runs once.
*
* @param \Closure $work
* An `fn(): void` doing the preparation.
@@ -581,10 +581,7 @@ public function preload(\Closure $work): self {
}
/**
- * Add a block to the region in hand.
- *
- * A region never knows which kind it was given, so anything drawn goes in the
- * same way a field does.
+ * Add a block to the current region.
*
* @param \DrevOps\Tui\Block\BlockInterface $block
* The block.
@@ -600,11 +597,11 @@ public function add(BlockInterface $block): self {
}
/**
- * Put a sub-panel where the arrangement keeps it.
+ * Place a sub-panel in the region its layout assigns.
*
- * A grid draws each of its sub-panels in a window of its own, and a window is
- * a region, so which one a sub-panel goes in is settled here rather than left
- * to something further down to work out from the order they arrived in.
+ * A grid draws each of its sub-panels in a window of its own, and a window
+ * is a region, so the region a sub-panel goes in is settled here rather
+ * than derived later from declaration order.
*
* @param \DrevOps\Tui\Block\Panel $panel
* The sub-panel.
@@ -630,9 +627,8 @@ protected function descend(Panel $panel): void {
$this->panel->in($window)->add($panel);
$this->placed = TRUE;
- // Where a row sits is which region it is in, and where it was written is
- // what says which: from here on a row is one written after the windows, so
- // it goes below them rather than above.
+ // A block declared after the sub-panels goes below the windows, so the
+ // current region becomes the trailing one.
$this->region = $layout->trailing();
}
diff --git a/src/CancelException.php b/src/CancelException.php
index 790889a3..0090a49c 100644
--- a/src/CancelException.php
+++ b/src/CancelException.php
@@ -7,11 +7,10 @@
/**
* Thrown when the user dismisses an interactive session via the cancel button.
*
- * A cancel is the button-driven twin of the Ctrl-C abort: the session ends
- * without a submit, so the answers edited before it must never be mistaken for
- * a completed form. It extends {@see InterruptException} so a caller that only
- * tells aborted from submitted catches one exception; a caller that reacts
- * differently to an explicit cancel catches this class first.
+ * A cancel ends the session without a submit, like the Ctrl-C abort, so the
+ * answers edited before it are not a completed form. It extends
+ * {@see InterruptException} so one catch covers both aborts; a caller that
+ * treats an explicit cancel differently catches this class first.
*
* @package DrevOps\Tui
*/
diff --git a/src/CollectException.php b/src/CollectException.php
index df006f74..0e05b8f4 100644
--- a/src/CollectException.php
+++ b/src/CollectException.php
@@ -7,11 +7,12 @@
/**
* Thrown when a collection cannot take the answers it was given.
*
- * With no screen there is nobody to retype a value the form refuses, and no row
- * to say so on, so the whole collection fails rather than handing back answers
- * one of which was never accepted. It sits beside {@see InterruptException} and
- * {@see CancelException} because all three are ways a collection ends without a
- * complete set of answers, so one import covers every ending.
+ * A run with no screen has no way to retype a rejected value and no row to
+ * report it on, so the whole collection fails rather than returning a set
+ * with an answer that was never accepted. It shares a namespace with
+ * {@see InterruptException} and {@see CancelException}: all three end a
+ * collection without a complete answer set, so one import covers every
+ * ending.
*
* @package DrevOps\Tui
*/
diff --git a/src/Field/AbstractField.php b/src/Field/AbstractField.php
index 1d73a473..174aa58c 100644
--- a/src/Field/AbstractField.php
+++ b/src/Field/AbstractField.php
@@ -24,8 +24,8 @@ abstract class AbstractField implements FieldInterface {
/**
* The resolved key bindings for this field's scope.
*
- * Injected by the field factory; when a field is constructed directly (for
- * a test or a one-off), it falls back to the default preset for its scope.
+ * When none are injected, the field falls back to the default preset for
+ * its scope.
*/
protected ?ScopedKeyMap $scoped = NULL;
@@ -118,8 +118,7 @@ abstract protected function liveValue(): mixed;
/**
* The scope whose default bindings apply when none are injected.
*
- * Fields whose bindings differ from the base defaults override this; the
- * base scope is the right fallback for the rest.
+ * Fields whose bindings differ from the base defaults override this.
*
* @return \DrevOps\Tui\Input\Scope
* The field's binding scope.
@@ -161,7 +160,7 @@ protected function handleCancel(Key $key): bool {
* The key to test.
*
* @return bool
- * TRUE when the key triggered the accept, so it travels no further.
+ * TRUE when the key triggered the accept and was consumed.
*/
protected function handleAccept(Key $key): bool {
if ($this->keys()->matches($key, Action::Accept)) {
@@ -226,8 +225,8 @@ protected function entryLabel(ThemeInterface $theme, string $label, bool $curren
* The rendered row.
*/
protected function renderExclusiveRow(ThemeInterface $theme, string $label, bool $current): string {
- // Moving the cursor is what picks in an exclusive list, so the mark and the
- // cursor say the same thing and the row draws only the mark.
+ // Moving the cursor picks in an exclusive list, so the mark and the
+ // cursor state coincide and the row draws only the mark.
return $this->elements($theme)->fieldEntryMarker($current, TRUE) . ' ' . $this->entryLabel($theme, $label, $current);
}
@@ -245,9 +244,8 @@ protected function matcher(): Matcher {
* Style an option label, emphasising the query-matched characters.
*
* The label is split into runs of matched and unmatched characters, each run
- * styled on its own so no SGR code nests inside another: matched runs get the
- * match colour, and the rest is drawn as the entry it belongs to. With no
- * matched positions this is exactly {@see entryLabel()}.
+ * styled on its own, so no SGR code nests inside another. With no matched
+ * positions this is exactly {@see entryLabel()}.
*
* @param \DrevOps\Tui\Theme\ThemeInterface $theme
* The theme.
@@ -289,7 +287,7 @@ protected function renderMatchedLabel(ThemeInterface $theme, string $label, arra
}
/**
- * Style one run of same-kind characters for {@see renderMatchedLabel()}.
+ * Style one run of matched or unmatched characters.
*
* @param \DrevOps\Tui\Theme\ThemeInterface $theme
* The theme.
@@ -316,13 +314,12 @@ protected function styleRun(ThemeInterface $theme, string $run, bool $matched, b
/**
* {@inheritdoc}
*
- * The frame every field shares, stacked so that each line sits nearest what
- * it belongs to: the field's own body, the highlighted option's detail
- * (choice fields only), then what the field expects of an answer, then why
- * the last one was refused. The detail leads because it changes as the
- * highlight moves - a line that follows the cursor belongs against the list
- * it follows, not below a constraint that never moves. A field renders only
- * its body via {@see renderBody()} and states its expectation via
+ * The frame every field shares: the field's body, the highlighted option's
+ * description (choice fields only), the constraint, then the error. The
+ * description changes as the highlight moves, so it renders directly under
+ * the list rather than below the constraint, which never moves.
+ *
+ * A field supplies its body via {@see renderBody()} and its constraint via
* {@see renderConstraint()}.
*/
public function view(ThemeInterface $theme): string {
@@ -346,7 +343,7 @@ public function view(ThemeInterface $theme): string {
}
/**
- * What the field expects of an answer, before anything is refused.
+ * What the field expects of an answer.
*
* @param \DrevOps\Tui\Theme\ThemeInterface $theme
* The theme.
@@ -373,9 +370,8 @@ abstract protected function renderBody(ThemeInterface $theme): string;
/**
* The highlighted option's description; empty for fields without one.
*
- * The choice fields override this (directly or via a capability trait) to
- * surface the highlighted option's description; every other field inherits
- * the empty default, so the shared frame adds no description line for it.
+ * Choice fields override this, directly or via a capability trait; the
+ * empty default adds no description line to the frame.
*
* @return string
* The description shown beneath the body, or an empty string.
@@ -387,8 +383,8 @@ protected function highlightedDescription(): string {
/**
* The narrowest content width at which an option description is still shown.
*
- * Below this the panel is too narrow to render a readable description, so it
- * is dropped rather than wrapped into unreadable fragments.
+ * Below this width a wrapped description breaks into unreadable fragments,
+ * so it is dropped.
*/
protected const int MIN_DESCRIPTION_WIDTH = 8;
@@ -405,8 +401,8 @@ protected function highlightedDescription(): string {
* description or the panel is too narrow to show one.
*/
protected function renderOptionDescription(ThemeInterface $theme, string $description): string {
- // Indented to start where an entry's own text starts, so it reads as
- // belonging to the entry above it rather than to the list as a whole.
+ // Indent to the column where an entry's own text starts, so the
+ // description aligns with the entry above it.
$indent = str_repeat(' ', $this->entryTextOffset($theme));
$width = $theme->contentWidth() - Strings::length($indent);
@@ -435,9 +431,9 @@ protected function entryTextOffset(ThemeInterface $theme): int {
/**
* Offer a value as the answer, finishing the collection.
*
- * Nothing is measured here: a field offers what it collected and whatever
- * holds the answer decides whether it stands, so the offer always goes
- * through and whatever was being said about the last one is cleared.
+ * The field does not validate: whatever holds the answer decides whether an
+ * offered value stands. The offer always succeeds and clears any prior
+ * error.
*
* @param mixed $value
* The collected value.
diff --git a/src/Field/Calendar.php b/src/Field/Calendar.php
index 01097d54..8fd9da02 100644
--- a/src/Field/Calendar.php
+++ b/src/Field/Calendar.php
@@ -21,11 +21,13 @@
/**
* A navigable month calendar returning a normalized ISO `Y-m-d` string.
*
- * The move actions - bound to the arrow keys by default, and to h/j/k/l too
- * under the vim preset - move the cursor by day and by week; the page keys
- * change month, and Home/End jump to the first/last day of the visible month.
- * Every motion is clamped to the declared min/max range, so the cursor never
- * settles on an out-of-range day; days outside the range stay visible but
+ * The move actions move the cursor by day and by week; they bind to the
+ * arrow keys by default and to h/j/k/l under the vim preset. The page keys
+ * change month, and Home/End jump to the first/last day of the visible
+ * month.
+ *
+ * Every motion is clamped to the declared min/max range, so the cursor is
+ * never on an out-of-range day; days outside the range stay visible but
* dimmed.
*
* @package DrevOps\Tui\Field
@@ -99,8 +101,9 @@ public function handle(Key $key): void {
* The date a navigation key moves to before clamping, or NULL for no move.
*
* Day and week movement resolve through the injected key bindings, so the
- * arrow keys, the vim preset and any consumer remap all reach them; the month
- * and month-edge jumps have no action of their own and stay on their keys.
+ * arrow keys, the vim preset and any consumer remap all apply. The month
+ * and month-edge jumps have no action of their own, so they match fixed
+ * keys.
*
* @param \DrevOps\Tui\Input\Key $key
* The key to interpret.
@@ -127,9 +130,9 @@ protected function move(Key $key, ScopedKeyMap $keys): ?\DateTimeImmutable {
/**
* The cursor moved by whole months, kept on a valid day-of-month.
*
- * Anchoring on the first of the month before shifting avoids the day-of-month
- * overflow that a naive "+1 month" produces (e.g. Jan 31 becoming Mar 3); the
- * day is then re-applied, capped to the shorter month's length.
+ * Anchoring on the first of the month before shifting avoids the
+ * day-of-month overflow a naive "+1 month" produces (e.g. Jan 31 becoming
+ * Mar 3). The day is then re-applied, capped to the shorter month's length.
*
* @param int $months
* The signed number of months to move.
@@ -173,7 +176,7 @@ protected function renderBody(ThemeInterface $theme): string {
* {@inheritdoc}
*
* Month (PgUp/PgDn) and month-edge (Home/End) jumps have no action of their
- * own, so the footer advertises the binding-driven day/week motion.
+ * own, so the footer shows the binding-driven day/week motion.
*/
#[\Override]
public function hints(): array {
@@ -245,8 +248,8 @@ protected function weekRows(ThemeInterface $theme): array {
/**
* Render one day cell: bracketed at the cursor, dimmed when out of range.
*
- * The cursor cell carries literal brackets so it stays distinguishable even
- * with colour off, mirroring how the radio glyph marks a selection in ASCII.
+ * The cursor cell uses literal brackets so it stays distinguishable even
+ * with colour off.
*
* @param \DrevOps\Tui\Theme\ThemeInterface $theme
* The theme.
diff --git a/src/Field/Capability/CompletionCapableTrait.php b/src/Field/Capability/CompletionCapableTrait.php
index 5808a46e..fcc006c0 100644
--- a/src/Field/Capability/CompletionCapableTrait.php
+++ b/src/Field/Capability/CompletionCapableTrait.php
@@ -10,10 +10,10 @@
* Inline ghost-text completion over a character buffer.
*
* Composes with {@see TextEditCapableTrait}: the buffer is completed to the
- * first candidate it is a case-insensitive prefix of. Which candidates are
- * offered, when they are offered at all, and how the accepted one lands in the
- * buffer are each overridable, so a field whose buffer is not a plain caret
- * line reuses the matching rule without inheriting the caret arithmetic.
+ * first candidate it is a case-insensitive prefix of. The candidate list, the
+ * availability check and the acceptance step are each overridable. A field
+ * whose buffer is not a plain caret line reuses the matching rule without
+ * inheriting the caret arithmetic.
*
* @package DrevOps\Tui\Field\Capability
*/
@@ -50,11 +50,11 @@ public function bestMatch(): ?string {
}
/**
- * Whether the field's current state offers a completion at all.
+ * Whether the field's current state offers a completion.
*
* @return bool
* TRUE when the caret sits at the end of the buffer, so the ghost text
- * continues what is being typed rather than interrupting it.
+ * continues the typed text instead of interrupting it.
*/
protected function isCompletionAvailable(): bool {
return $this->cursor === Strings::length($this->buffer);
@@ -69,7 +69,7 @@ protected function isCompletionAvailable(): bool {
abstract protected function completionCandidates(): array;
/**
- * Land an accepted candidate in the buffer.
+ * Set the buffer to an accepted candidate.
*
* @param string $match
* The candidate to complete the buffer to.
diff --git a/src/Field/Capability/FilterCapableInterface.php b/src/Field/Capability/FilterCapableInterface.php
index f21c27c5..c18b4277 100644
--- a/src/Field/Capability/FilterCapableInterface.php
+++ b/src/Field/Capability/FilterCapableInterface.php
@@ -24,7 +24,7 @@ interface FilterCapableInterface {
public function filter(): string;
/**
- * Land the cursor on the first match and reset paging on a query change.
+ * Move the cursor to the first match and reset paging on a query change.
*/
public function resetFilterCursor(): void;
diff --git a/src/Field/Capability/FilterCapableTrait.php b/src/Field/Capability/FilterCapableTrait.php
index 08283b7d..4b144ba1 100644
--- a/src/Field/Capability/FilterCapableTrait.php
+++ b/src/Field/Capability/FilterCapableTrait.php
@@ -73,7 +73,7 @@ protected function handleFilterKey(Key $key): bool {
}
/**
- * Land the cursor on the first match and reset paging on a query change.
+ * Move the cursor to the first match and reset paging on a query change.
*/
public function resetFilterCursor(): void {
$this->cursor = $this->firstSelectable($this->visible());
@@ -83,11 +83,11 @@ public function resetFilterCursor(): void {
/**
* The rows currently visible under the filter.
*
- * With no filter every row shows in declared order; once filtering, only
- * matching options show - structural headings and separators drop away so
- * the result reads as a flat list. Rows that came from a query source are
- * already the answer to the query, so filtering them again locally would drop
- * the ones whose labels do not literally match it.
+ * With no filter every row shows in declared order. Under a filter only
+ * matching options show; structural headings and separators are excluded,
+ * so the result is a flat list. A query source has already matched its rows
+ * against the query, so filtering them again locally would drop rows whose
+ * labels do not literally match it.
*
* @return list<\DrevOps\Tui\Block\Option>
* The visible rows.
diff --git a/src/Field/Capability/OptionsCapableInterface.php b/src/Field/Capability/OptionsCapableInterface.php
index 79e85ce2..b325e4c7 100644
--- a/src/Field/Capability/OptionsCapableInterface.php
+++ b/src/Field/Capability/OptionsCapableInterface.php
@@ -11,8 +11,8 @@
* A field that presents a list of option rows.
*
* The rows are {@see Option} objects, so headings, separators and disabled
- * options travel with the selectable ones; {@see OptionsCapableTrait} carries
- * the default implementation.
+ * options share the list with the selectable ones; {@see OptionsCapableTrait}
+ * carries the default implementation.
*
* @package DrevOps\Tui\Field\Capability
*/
diff --git a/src/Field/Capability/OptionsCapableTrait.php b/src/Field/Capability/OptionsCapableTrait.php
index 790bef99..56f5e882 100644
--- a/src/Field/Capability/OptionsCapableTrait.php
+++ b/src/Field/Capability/OptionsCapableTrait.php
@@ -12,10 +12,10 @@
/**
* Shared option-list behaviour for the choice fields.
*
- * Holds the ordered option rows and centralizes the two things every choice
- * field must agree on: the cursor only ever rests on a selectable row (so
- * separators, headings and disabled options are skipped), and those
- * non-selectable rows render as visual-only structure.
+ * Holds the ordered option rows and the two invariants every choice field
+ * shares. The cursor is only ever placed on a selectable row, skipping
+ * separators, headings and disabled options. Non-selectable rows render as
+ * visual-only structure.
*
* @package DrevOps\Tui\Field\Capability
*/
diff --git a/src/Field/Capability/PagingCapableTrait.php b/src/Field/Capability/PagingCapableTrait.php
index ede3ccf6..6dc73bd2 100644
--- a/src/Field/Capability/PagingCapableTrait.php
+++ b/src/Field/Capability/PagingCapableTrait.php
@@ -115,11 +115,11 @@ protected function wrapScrolled(ThemeInterface $theme, array $rows, Viewport $vi
}
/**
- * The theme, narrowed to the mark that says a list runs past its page.
+ * The theme, narrowed to the elements that draw the overflow mark.
*
- * The field's own mark rather than the chrome's: a field draws only what it
- * owns, so the page it windows a list to is marked with an element of its
- * own, and a theme that wants the two to read alike says so once in each.
+ * The mark is the field's own element, not the chrome's, because a field
+ * draws only what it owns. A theme that styles the two marks alike declares
+ * the style in both elements.
*
* @param \DrevOps\Tui\Theme\ThemeInterface $theme
* The theme.
diff --git a/src/Field/Capability/PlaceholderCapableTrait.php b/src/Field/Capability/PlaceholderCapableTrait.php
index ebbd0ffc..68ce9049 100644
--- a/src/Field/Capability/PlaceholderCapableTrait.php
+++ b/src/Field/Capability/PlaceholderCapableTrait.php
@@ -10,9 +10,9 @@
/**
* Ghost text over an empty input, in the theme's ghost style.
*
- * The placeholder and an inline completion suffix share one visual channel and
- * never apply at once: a completion needs something typed, a placeholder needs
- * nothing typed.
+ * The placeholder and an inline completion suffix share one visual channel
+ * and never apply at once: a completion requires typed input, a placeholder
+ * requires none.
*
* @package DrevOps\Tui\Field\Capability
*/
@@ -60,7 +60,8 @@ protected function placeholderText(string $current): string {
*
* @return string
* The styled placeholder, or an empty string when none applies - including
- * in no-colour mode, where ghost text would read as a typed value.
+ * in no-colour mode, where ghost text is indistinguishable from a typed
+ * value.
*/
protected function placeholderGhost(ThemeInterface $theme, string $current): string {
$text = $this->placeholderText($current);
diff --git a/src/Field/Capability/QueryOptionsCapableInterface.php b/src/Field/Capability/QueryOptionsCapableInterface.php
index af9b346b..8f1f1cfa 100644
--- a/src/Field/Capability/QueryOptionsCapableInterface.php
+++ b/src/Field/Capability/QueryOptionsCapableInterface.php
@@ -7,26 +7,27 @@
/**
* A field whose candidate list is resolved from its live query.
*
- * The field owns the query, the cache and the displayed state; it never
- * resolves anything itself, because a resolution blocks and only the panel loop
- * may block and repaint. The loop asks {@see pendingQuery()} what still needs
- * answering, calls the field's source, and hands the result back.
+ * The field owns the query, the cache and the displayed state. It never
+ * resolves anything itself: a resolution blocks, and only the panel loop may
+ * block and repaint. The loop reads {@see pendingQuery()} for the query still
+ * needing resolution, calls the field's source, and passes the result back.
*
* @package DrevOps\Tui\Field\Capability
*/
interface QueryOptionsCapableInterface {
/**
- * Turn the field's candidates over to a query source.
+ * Drive the field's candidate list from a query source.
*
* @param int $min_length
- * The query length below which the source is not called, so a backend is
- * not asked to list everything; zero calls it for the empty query too.
+ * The query length below which the source is not called, so a short query
+ * does not request the backend's full listing; zero calls the source for
+ * the empty query too.
*/
public function driveByQuery(int $min_length = 0): void;
/**
- * Whether the field's candidates come from a query source at all.
+ * Whether the field's candidates come from a query source.
*
* @return bool
* TRUE when a source is driving the list.
@@ -42,10 +43,10 @@ public function isQueryDriven(): bool;
public function query(): string;
/**
- * The query still waiting on a call to the source, if any.
+ * The query that still requires a call to the source, if any.
*
- * Settles everything that needs no call - an unchanged query, a query below
- * the minimum length, one already answered in this session - so a NULL return
+ * A query requiring no call is settled directly: one unchanged, one below
+ * the minimum length, one already resolved in this session. A NULL return
* means the displayed list is up to date.
*
* @return string|null
@@ -72,7 +73,7 @@ public function applyQuery(string $query, array $rows): void;
* Record that a query could not be resolved and leave the loading state.
*
* @param string $query
- * The query that failed - remembered, so the same failing call is not
+ * The query that failed, recorded so the same failing call is not
* repeated on every frame.
* @param string $message
* The message shown in place of the list.
diff --git a/src/Field/Capability/QueryOptionsCapableTrait.php b/src/Field/Capability/QueryOptionsCapableTrait.php
index 15a573d9..b1ca5272 100644
--- a/src/Field/Capability/QueryOptionsCapableTrait.php
+++ b/src/Field/Capability/QueryOptionsCapableTrait.php
@@ -11,10 +11,10 @@
/**
* Candidate rows resolved from the live query rather than filtered locally.
*
- * Holds everything about a query-sourced list that is not I/O: which query the
- * displayed rows answer, whether one is in flight, what a failed or too-short
- * query shows instead of a list, and the per-query cache that makes a repeat -
- * a backspace, a retyped prefix - free.
+ * Holds everything about a query-sourced list that is not I/O. That covers
+ * the resolved query, an in-flight flag, and what a failed or too-short query
+ * shows instead of a list. The per-query cache serves a repeated query - a
+ * backspace, a retyped prefix - without another call.
*
* The resolution itself belongs to the panel loop, which is the only place that
* may block and repaint.
@@ -26,8 +26,8 @@ trait QueryOptionsCapableTrait {
/**
* How many resolved queries are kept before the oldest is dropped.
*
- * A session's queries are short-lived and short, so the cap is about never
- * growing without bound rather than about memory pressure.
+ * A session's queries are short-lived and short, so the cap stops unbounded
+ * growth; memory pressure is not the concern.
*/
public const int QUERY_CACHE_SIZE = 50;
@@ -42,7 +42,9 @@ trait QueryOptionsCapableTrait {
protected int $queryMinLength = 0;
/**
- * The query the displayed rows answer, or NULL before the first resolution.
+ * The query the displayed rows were resolved for.
+ *
+ * NULL before the first resolution.
*/
protected ?string $resolvedQuery = NULL;
@@ -64,7 +66,7 @@ trait QueryOptionsCapableTrait {
protected array $queryCache = [];
/**
- * Turn the field's rows over to a query source.
+ * Drive the field's rows from a query source.
*
* @param int $min_length
* The query length below which the source is not called.
@@ -142,7 +144,7 @@ public function failQuery(string $query, string $message): void {
* Show a query's rows and leave the loading state.
*
* @param string $query
- * The query the rows answer.
+ * The query the rows were resolved for.
* @param list<\DrevOps\Tui\Block\Option> $rows
* The rows to show.
*/
@@ -154,7 +156,7 @@ protected function settle(string $query, array $rows): void {
}
/**
- * Take on a resolved query's rows as the field's own candidates.
+ * Adopt a resolved query's rows as the field's candidates.
*
* @param list<\DrevOps\Tui\Block\Option> $rows
* The rows.
@@ -179,8 +181,6 @@ protected function queryStateLine(ThemeInterface $theme): ?string {
$elements = $this->elements($theme);
if ($this->queryLoading) {
- // The same mark a lazily loaded field shows in its panel row, so waiting
- // reads the same wherever it happens.
return $elements->fieldLoading();
}
@@ -189,8 +189,8 @@ protected function queryStateLine(ThemeInterface $theme): ?string {
}
if (Strings::length($this->query()) < $this->queryMinLength) {
- // The guidance voice, not the description's: this line shares its row
- // with the error above, and states what the field expects.
+ // Rendered as a constraint, not a description: the line shares its row
+ // with the error above and states what the field expects.
return $elements->fieldConstraint(Translator::formatPlural($this->queryMinLength, 'Type 1 character to search.', 'Type @count characters to search.'));
}
diff --git a/src/Field/Capability/SelectionBoundedTrait.php b/src/Field/Capability/SelectionBoundedTrait.php
index 8fced6df..c5ed0df0 100644
--- a/src/Field/Capability/SelectionBoundedTrait.php
+++ b/src/Field/Capability/SelectionBoundedTrait.php
@@ -9,11 +9,11 @@
use DrevOps\Tui\Translation\Translator;
/**
- * Says what a multi-value field's declared selection counts ask for.
+ * Presents a multi-value field's declared selection-count bounds.
*
- * The bound is surfaced as a persistent hint so it is visible before it is
- * reached; refusing a count that misses it belongs to the block holding the
- * answer, which measures every offered value once.
+ * The bound is shown as a persistent hint, so it is visible before it is
+ * reached. Rejecting a count outside the bounds belongs to the block holding
+ * the answer, which measures every offered value once.
*
* @package DrevOps\Tui\Field\Capability
*/
@@ -27,9 +27,9 @@ trait SelectionBoundedTrait {
/**
* The themed selection-count hint line, or an empty string when not shown.
*
- * Worded as the refusal is, so the persistent guidance and the reason a
- * count was refused read the same; the hint gives way to the error line
- * while one is showing, so the two never stack.
+ * The hint uses the refusal's wording, so the persistent guidance and the
+ * rejection message match. While an error shows, the hint is suppressed, so
+ * the two lines never stack.
*
* @param \DrevOps\Tui\Theme\ThemeInterface $theme
* The theme.
@@ -43,9 +43,9 @@ protected function selectionHint(ThemeInterface $theme): string {
return '';
}
- // The guidance voice, not the description's: this line states what the
- // field expects, and drawn as a description it is indistinguishable from
- // the highlighted option's own text sitting directly above it.
+ // Rendered as a constraint, not a description: the line states what the
+ // field expects. Drawn as a description it is indistinguishable from the
+ // highlighted option's own text directly above it.
return $this->elements($theme)->fieldConstraint(Translator::t('Select @constraint.', ['@constraint' => $this->selectionBounds->describe()]));
}
diff --git a/src/Field/Capability/SelectionCapableTrait.php b/src/Field/Capability/SelectionCapableTrait.php
index 0cbaef88..dee99729 100644
--- a/src/Field/Capability/SelectionCapableTrait.php
+++ b/src/Field/Capability/SelectionCapableTrait.php
@@ -17,10 +17,10 @@
* Choice behaviour over the option rows, single-value or multiple-value.
*
* Composes with {@see OptionsCapableTrait}: the cursor moves over the visible
- * rows, only ever resting on a selectable one. A single-value field commits
- * the highlighted option; a multiple-value field toggles a selection set -
- * Space toggles the highlighted option, Left/Right deselect or select every
- * visible option - and commits the selected values in display order.
+ * rows and stops only on a selectable one. A single-value field commits the
+ * highlighted option. A multiple-value field toggles a selection set - Space
+ * toggles the highlighted option, Left/Right deselect or select every visible
+ * option - and commits the selected values in display order.
*
* @package DrevOps\Tui\Field\Capability
*/
@@ -289,10 +289,9 @@ protected function liveValue(): mixed {
}
}
- // The rows are only the display order, and a list that changes under the
- // selection - one resolved from a query - no longer holds everything that
- // was selected from an earlier one. Those selections are still the user's,
- // so they follow the ordered ones rather than being dropped.
+ // The options hold only the current display order; a list resolved from a
+ // query can lack values selected from an earlier one. Those selections are
+ // still the user's, so they are appended after the ordered ones.
foreach (array_keys($this->selected) as $value) {
if (!in_array($value, $out, TRUE)) {
$out[] = $value;
@@ -332,17 +331,17 @@ public function renderOptionRow(ThemeInterface $theme, Option $option, bool $cur
return $elements->fieldEntryMarker(FALSE, TRUE) . ' ' . $this->renderDisabledLabel($theme, $option);
}
- // Moving the cursor is what picks in an exclusive list, so the mark and the
- // cursor say the same thing and the row draws only the mark.
+ // Moving the cursor is the selection in an exclusive list, so the mark
+ // mirrors the cursor and the row draws only the mark.
return $elements->fieldEntryMarker($current, TRUE) . ' ' . $this->renderMatchedLabel($theme, $option->label, $this->matchPositions($option->label), $current);
}
/**
* {@inheritdoc}
*
- * Measured from the glyphs the row actually draws rather than assumed: the
- * leading run differs between the two modes, and again between a themed
- * glyph and its textual stand-in.
+ * Measured from the glyphs the row actually draws, not assumed: the leading
+ * run differs between the two modes, and between a themed glyph and its
+ * textual stand-in.
*/
#[\Override]
protected function entryTextOffset(ThemeInterface $theme): int {
@@ -359,8 +358,7 @@ protected function entryTextOffset(ThemeInterface $theme): int {
* {@inheritdoc}
*
* In multiple mode Toggle is the non-obvious action - nothing else signals
- * that a key toggles the highlighted option - so it leads, followed by the
- * rest.
+ * that a key toggles the highlighted option - so its hint is listed first.
*/
#[\Override]
public function hints(): array {
diff --git a/src/Field/Capability/TextEditCapableInterface.php b/src/Field/Capability/TextEditCapableInterface.php
index 2b6ce802..f91dd309 100644
--- a/src/Field/Capability/TextEditCapableInterface.php
+++ b/src/Field/Capability/TextEditCapableInterface.php
@@ -8,7 +8,7 @@
* A field that edits a character buffer.
*
* {@see TextEditCapableTrait} carries the default cursor-based implementation;
- * an append-only field may implement the vocabulary directly.
+ * an append-only field may implement the methods directly.
*
* @package DrevOps\Tui\Field\Capability
*/
diff --git a/src/Field/Capability/TextEditCapableTrait.php b/src/Field/Capability/TextEditCapableTrait.php
index 507b7e90..3bb4d760 100644
--- a/src/Field/Capability/TextEditCapableTrait.php
+++ b/src/Field/Capability/TextEditCapableTrait.php
@@ -31,7 +31,7 @@ trait TextEditCapableTrait {
protected int $cursor = 0;
/**
- * Seed the buffer and land the cursor at its end.
+ * Seed the buffer and place the cursor at its end.
*
* @param string $buffer
* The initial value (and live input buffer).
diff --git a/src/Field/FieldFactory.php b/src/Field/FieldFactory.php
index ee706638..0e390a91 100644
--- a/src/Field/FieldFactory.php
+++ b/src/Field/FieldFactory.php
@@ -17,11 +17,10 @@
use DrevOps\Tui\Translation\Translator;
/**
- * Builds the editor a field opens onto, seeded with the value it holds.
+ * Builds the interactive field for a block, seeded with the value it holds.
*
- * The kind of answer a field collects is what it turns on, and nothing it
- * builds refuses anything: what a field will not take is the field's own to
- * refuse, so an offered value is measured where the answer is held.
+ * The block's field type selects the field class. A built field does not
+ * validate: an offered value is validated where the answer is held.
*
* @package DrevOps\Tui\Field
*/
@@ -46,7 +45,7 @@ public function __construct(?KeyMap $key_map = NULL, protected bool $externalEdi
}
/**
- * Build the field a block opens onto, seeded with the value it holds.
+ * Build the interactive field for a block, seeded with the value it holds.
*
* @param \DrevOps\Tui\Block\Field $block
* The block being opened.
@@ -59,8 +58,7 @@ public function __construct(?KeyMap $key_map = NULL, protected bool $externalEdi
* The field.
*
* @throws \LogicException
- * When the block's kind needs a declaration it was not given, so there is
- * nothing complete enough to open onto.
+ * When the block's type requires a declaration the block does not carry.
*/
public function open(Field $block, mixed $current = NULL, array $answers = []): FieldInterface {
$entries = $this->translate($block->entries());
@@ -134,7 +132,7 @@ protected function rating(Field $block, mixed $current): Rating {
}
/**
- * The shape a block's template field fills in.
+ * The template a block's template field fills in.
*
* @param \DrevOps\Tui\Block\Field $block
* The block.
@@ -199,8 +197,8 @@ protected function number(mixed $current): string {
/**
* A rating's captions, localized to the active language.
*
- * Translated once here rather than at each draw, the way the option labels
- * are, so the caption a field shows is the caption the panel row shows.
+ * Translated once here rather than at each draw, so the caption the field
+ * shows is the caption the panel row shows.
*
* @param array $captions
* The caption of each captioned point, keyed by the point.
@@ -219,7 +217,7 @@ protected function localized(array $captions): array {
* The localized options.
*
* @return array
- * The labels keyed by value, for fields that take a flat option map.
+ * The labels keyed by value.
*/
protected function entryLabels(array $options): array {
$out = [];
diff --git a/src/Field/FilePicker.php b/src/Field/FilePicker.php
index 8bbd49dd..715b906e 100644
--- a/src/Field/FilePicker.php
+++ b/src/Field/FilePicker.php
@@ -27,17 +27,18 @@
* A filesystem browser that selects a path, or several in multiple mode.
*
* Navigation walks directories from a start directory that also bounds the
- * browse - it is a floor the browser cannot ascend above. The constraints
- * govern which entries may be selected (any, files or directories) and which
- * files pass the extension filter; directories stay navigable regardless, so
- * files beneath them remain reachable. Printable characters filter the current
- * directory; Tab reveals or hides dot-entries. In multiple mode Space toggles
- * the highlighted selectable entry and selections accumulate across
- * directories, so the value is the chosen path (single) or the list of chosen
- * paths (multiple). The type and extension limits gate what may be browsed
- * onto and picked; the size limit is weighed against the final value by the
- * block holding the answer, so an oversized file is offered here rather than
- * refused.
+ * browse: the browser never ascends above it. Printable characters filter
+ * the current directory; Tab reveals or hides dot-entries.
+ *
+ * The constraints govern which entries may be selected (any, files or
+ * directories) and which files pass the extension filter. Directories stay
+ * navigable regardless, so files beneath them remain reachable.
+ *
+ * In multiple mode Space toggles the highlighted selectable entry and
+ * selections accumulate across directories, so the value is the chosen path
+ * (single) or the list of chosen paths (multiple). The size limit is checked
+ * against the final value by the block holding the answer, so an oversized
+ * file can still be picked here.
*
* @package DrevOps\Tui\Field
*/
@@ -47,7 +48,7 @@ class FilePicker extends AbstractField implements FilterCapableInterface, Reveal
use SelectionBoundedTrait;
/**
- * The start directory: where the browser opens and the floor it cannot pass.
+ * The start directory: the browser opens here and cannot ascend above it.
*/
protected string $root;
@@ -168,7 +169,6 @@ public function handle(Key $key): void {
return;
}
- // Reveal doubles as the show-hidden toggle, mirroring the password reveal.
if ($keys->matches($key, Action::Reveal)) {
$this->toggleReveal();
@@ -252,10 +252,9 @@ protected function renderBody(ThemeInterface $theme): string {
/**
* The themed constraint hint line, or an empty string when not shown.
*
- * Mirrors the selection-count hint: the active type, extension and size
- * limits are surfaced as a persistent line so they are visible before a pick
- * breaks one, giving way to the inline error while a violation is showing so
- * the two never stack.
+ * The active type, extension and size limits render as one persistent line,
+ * so they are visible before a pick violates them. While an error is
+ * showing the line is suppressed, so the hint and the error never stack.
*
* @param \DrevOps\Tui\Theme\ThemeInterface $theme
* The theme.
@@ -270,16 +269,14 @@ protected function constraintHint(ThemeInterface $theme): string {
return '';
}
- // The guidance voice, not the description's: this line states what the
- // field expects, and shares its row with the error that replaces it.
return $this->elements($theme)->fieldConstraint($describe);
}
/**
* {@inheritdoc}
*
- * A picker states two limits at once - what may be picked, and how many -
- * and either may stand alone.
+ * The picker has two limit lines - what may be picked, and how many - and
+ * either may appear without the other.
*/
#[\Override]
protected function renderConstraint(ThemeInterface $theme): string {
@@ -289,8 +286,9 @@ protected function renderConstraint(ThemeInterface $theme): string {
/**
* {@inheritdoc}
*
- * The Toggle fragment resolves only in multiple mode, where Space is bound to
- * it; Accept reads "select" for a single pick and "accept" for multiple.
+ * The Toggle hint renders only in multiple mode, where Space is bound to
+ * it. The Accept label reads "select" for a single pick and "accept" for
+ * multiple.
*/
#[\Override]
public function hints(): array {
@@ -433,8 +431,8 @@ protected function ascend(): void {
/**
* {@inheritdoc}
*
- * Toggles whether dot-entries are shown, landing back at the top of the
- * refreshed listing.
+ * Toggles whether dot-entries are shown and resets the highlight to the top
+ * of the refreshed listing.
*/
public function toggleReveal(): void {
$this->showHidden = !$this->showHidden;
diff --git a/src/Field/Rating.php b/src/Field/Rating.php
index 8dbbae13..59589db1 100644
--- a/src/Field/Rating.php
+++ b/src/Field/Rating.php
@@ -15,13 +15,8 @@
/**
* A graded answer: a point chosen from a scale, accepted as an int.
*
- * The arrows walk the scale one point at a time and stop at either end - a
- * grade has a floor and a ceiling, so unlike a two-state switch the ends do
- * not wrap round. A digit selects its point outright when the scale reaches it.
- *
- * The ends are plain integers rather than a bounds object because a scale is
- * closed by construction: there is no point to draw beyond either end, so
- * neither may be left open.
+ * The arrows step one point at a time and stop at either end; the ends do
+ * not wrap. A typed digit selects its point when the scale includes it.
*
* @package DrevOps\Tui\Field
*/
@@ -90,7 +85,7 @@ public function handle(Key $key): void {
/**
* {@inheritdoc}
*
- * Each position is one point, and the scale stops at the end it reaches.
+ * Each position is one point, clamped at the ends of the scale.
*/
public function stepBy(int $delta): void {
$this->point = $this->clamp($this->point + $delta);
@@ -99,9 +94,8 @@ public function stepBy(int $delta): void {
/**
* Jump to the point a typed digit names.
*
- * A digit the scale does not reach leaves the choice alone, so typing on a
- * scale that starts above nine - or runs well past it - is inert rather than
- * surprising.
+ * A digit the scale does not include is ignored, so typing on a scale that
+ * starts above nine, or runs well past it, is inert.
*
* @param string $char
* The typed character.
@@ -146,9 +140,6 @@ protected function renderBody(ThemeInterface $theme): string {
/**
* {@inheritdoc}
- *
- * The stepping keys lead: nothing about a row of points says which keys move
- * along it.
*/
#[\Override]
public function hints(): array {
diff --git a/src/Field/Reorder.php b/src/Field/Reorder.php
index b9ee9e6f..6054de0a 100644
--- a/src/Field/Reorder.php
+++ b/src/Field/Reorder.php
@@ -92,8 +92,8 @@ public function handle(Key $key): void {
}
if ($keys->matches($key, Action::Accept)) {
- // A held item drops on Accept, mirroring Space; nothing is committed
- // while an item is held, so Enter never accepts mid-move.
+ // Accept drops a held item, as Space does, so nothing is committed
+ // mid-move.
if ($this->grabbed) {
$this->grabbed = FALSE;
}
@@ -225,8 +225,8 @@ public function renderOptionRow(ThemeInterface $theme, Option $option, bool $cur
* The two-column marker cell.
*/
protected function marker(ThemeInterface $theme, bool $current): string {
- // The keys that move it, so the mark says what to press rather than
- // inventing a second vocabulary for the same two directions.
+ // The glyphs are the keys that move the item, so the mark shows what to
+ // press.
if ($current && $this->grabbed) {
return $theme->keyGlyph(KeyName::Up) . $theme->keyGlyph(KeyName::Down);
}
@@ -237,9 +237,9 @@ protected function marker(ThemeInterface $theme, bool $current): string {
/**
* {@inheritdoc}
*
- * A held item flips the labels to "reorder"/"drop" and cannot be accepted
- * mid-move, so the accept hint is dropped until it lands; otherwise "move"
- * and "grab" lead the base accept/cancel fragments.
+ * While an item is held the labels read "reorder" and "drop", and the
+ * accept hint is omitted - a held item cannot be accepted mid-move.
+ * Otherwise "move" and "grab" precede the base accept/cancel hints.
*/
#[\Override]
public function hints(): array {
diff --git a/src/Field/Search.php b/src/Field/Search.php
index 7c77f567..589c8646 100644
--- a/src/Field/Search.php
+++ b/src/Field/Search.php
@@ -90,8 +90,8 @@ protected function choiceType(): FieldType {
/**
* {@inheritdoc}
*
- * Space is part of the query in single mode, so it cannot double as a select
- * key there; multiple mode binds Space to toggle the highlighted option.
+ * Space is part of the query in single mode, so it cannot also select;
+ * multiple mode binds Space to toggle the highlighted option.
*/
protected function handleSingleMode(Key $key): void {
if ($this->keys()->matches($key, Action::InsertSpace)) {
@@ -133,8 +133,8 @@ protected function renderBody(ThemeInterface $theme): string {
/**
* {@inheritdoc}
*
- * A query that has not run yet stands in for the list, and a count limit on
- * a list nobody can see yet is noise.
+ * While the query-state line replaces the list, the selection-count hint is
+ * suppressed.
*/
#[\Override]
protected function renderConstraint(ThemeInterface $theme): string {
diff --git a/src/Field/Suggest.php b/src/Field/Suggest.php
index 1c4cea81..18926ed4 100644
--- a/src/Field/Suggest.php
+++ b/src/Field/Suggest.php
@@ -113,8 +113,8 @@ public function handle(Key $key): void {
return;
}
- // Right accepts the ghost-text like Tab; with nothing to complete it is
- // inert, as it is for a suggest field that never opted into ghost-text.
+ // Right applies the completion like Tab; with nothing to complete, or
+ // with ghost-text off, it does nothing.
if ($keys->matches($key, Action::MoveRight) && $this->bestMatch() !== NULL) {
$this->applyCompletion();
@@ -160,8 +160,8 @@ public function buffer(): string {
/**
* {@inheritdoc}
*
- * The buffer is append-only - the query grows at its end - so the text is
- * added there and the suggestion highlight resets.
+ * The buffer is append-only, so the text is appended and the suggestion
+ * highlight resets.
*/
public function insert(string $text): void {
$this->buffer .= $text;
@@ -187,13 +187,12 @@ protected function resetFilterCursor(): void {
/**
* Whether a completion is offered in the field's current state.
*
- * The buffer is append-only, so the caret is always at its end; what gates a
- * completion here is what the rest of the editor is saying. Once a suggestion
- * is highlighted it, not the buffer, is the live value, so previewing a
- * completion of the buffer would contradict it. While a query is in flight
- * the candidates still held are the previous query's, and the list they came
- * from has already been replaced by the loading indicator - previewing one of
- * them would put back the very answer the field is withdrawing.
+ * The buffer is append-only, so the caret is always at its end and never
+ * gates the preview. A highlighted suggestion, not the buffer, is the live
+ * value, so a completion of the buffer would contradict it.
+ *
+ * While a query is in flight the candidates are the previous query's and
+ * their list has been replaced by the loading indicator; none is previewed.
*
* @return bool
* TRUE when the ghost-text preview applies.
@@ -205,10 +204,9 @@ protected function isCompletionAvailable(): bool {
/**
* The candidates the buffer is completed against.
*
- * Drawn from the displayed list rather than the declared order, so the
- * previewed completion is always the leading prefix match of the very list
- * shown beneath it - whether that order came from local ranking or from a
- * query source.
+ * The candidates are the displayed list, not the declared order. The
+ * preview is always the leading prefix match of the list shown beneath it,
+ * whether the order came from local ranking or a query source.
*
* @return list
* The suggestion values in display order.
@@ -218,10 +216,10 @@ protected function completionCandidates(): array {
}
/**
- * Land an accepted completion in the query.
+ * Apply an accepted completion to the query.
*
- * The completion is a new query, not a selection: the list re-filters around
- * it and stays open, with nothing highlighted.
+ * The completion is a new query, not a selection: the list re-filters
+ * against it, stays open and highlights nothing.
*
* @param string $match
* The candidate to complete the query to.
@@ -234,16 +232,16 @@ protected function completeBuffer(string $match): void {
/**
* The suggestions matching the current buffer, ranked by fuzzy relevance.
*
- * Suggestions that came from a query source are already the answer to the
- * buffer, so ranking them again locally would drop the ones that do not
- * literally match it.
+ * Suggestions from a query source already answer the buffer, so ranking
+ * them locally would drop the ones that do not literally match it.
+ *
+ * Only the locally ranked path is memoized: the values are fixed for the
+ * field's life, so the buffer alone determines the ranking. One ranking
+ * covers the several reads a frame makes - the list, the highlighted
+ * description, the live value and the ghost-text preview.
*
- * Only the locally ranked path is memoized, and deliberately so: there the
- * values are fixed for the field's life, so the query alone determines the
- * ranking and one pass serves the several reads a frame makes - the list, the
- * highlighted description, the live value and the ghost-text preview. A query
- * source replaces the values as each query settles, which a query-keyed
- * memo could not see, so that path reads them directly every time.
+ * A query source replaces the values as queries settle, which a query-keyed
+ * memo would not see, so that path reads them directly.
*
* @return list
* The matching suggestion values, most relevant first.
@@ -341,8 +339,8 @@ protected function highlightedDescription(): string {
* {@inheritdoc}
*
* The completion suffix and the placeholder share the one ghost slot after
- * the caret: the former needs a typed query to complete, the latter an empty
- * one, so at most one of them is ever set.
+ * the caret. The completion needs a typed query and the placeholder an
+ * empty one, so at most one of them is ever set.
*/
public function queryLine(ThemeInterface $theme): string {
$completion = $this->ghostSuffix();
diff --git a/src/Field/Template.php b/src/Field/Template.php
index fd6e8f22..a9c8dd67 100644
--- a/src/Field/Template.php
+++ b/src/Field/Template.php
@@ -55,8 +55,8 @@ class Template extends AbstractField implements TextEditCapableInterface {
* @param \DrevOps\Tui\Block\Template $template
* The shape to fill in.
* @param string $default
- * The initial assembled value; a value that does not have the shape leaves
- * every slot empty.
+ * The initial assembled value; a value that does not match the shape
+ * leaves every slot empty.
*/
public function __construct(protected TemplateModel $template, string $default = '') {
$this->names = $this->template->placeholders();
@@ -111,7 +111,7 @@ public function handle(Key $key): void {
/**
* {@inheritdoc}
*
- * The assembled string, with the live buffer standing in for its slot.
+ * The assembled string, with the live buffer supplying the active slot.
*/
protected function liveValue(): mixed {
return $this->template->assemble($this->values());
@@ -119,8 +119,6 @@ protected function liveValue(): mixed {
/**
* {@inheritdoc}
- *
- * Moving between slots is the action a reader will not guess, so it leads.
*/
#[\Override]
public function hints(): array {
@@ -128,7 +126,7 @@ public function hints(): array {
}
/**
- * The value of every slot, with the live buffer standing in for its slot.
+ * The value of every slot, with the live buffer supplying the active slot.
*
* @return array
* The slot values keyed by slot name, in shape order.
@@ -153,9 +151,9 @@ protected function activeName(): string {
/**
* Move the caret to another slot, wrapping around the ends.
*
- * The slot being left is validated on the way out: a rejected value shows its
- * error but does not hold the caret, so a slot filled in the wrong order can
- * still be reached and corrected.
+ * The slot being left is validated. A rejected value sets the error but
+ * does not block the move, so slots can be filled and corrected in any
+ * order.
*
* @param int $delta
* The number of slots to move by: 1 forward, -1 back.
@@ -182,8 +180,8 @@ protected function focus(int $index): void {
/**
* Accept the assembled value once every slot passes its own validator.
*
- * A rejected slot takes the caret, so the shown error names the slot the user
- * is looking at.
+ * A rejected slot receives the caret, so the shown error names the focused
+ * slot.
*/
protected function submit(): void {
$values = $this->values();
@@ -207,14 +205,14 @@ protected function submit(): void {
}
/**
- * Refuse a slot whose value would be misread once the shape is assembled.
+ * Reject a slot whose value would be ambiguous once the shape is assembled.
*
* @param array $values
* The value of each slot, keyed by slot name.
*
* @return bool
- * TRUE when every slot survives assembly; FALSE when one was rejected, the
- * caret moved to it and the error set.
+ * TRUE when every slot passes; FALSE when one was rejected, the caret
+ * moved to it and the error set.
*/
protected function rejectAmbiguous(array $values): bool {
$name = $this->template->ambiguousSlot($values);
@@ -256,8 +254,8 @@ protected function renderBody(ThemeInterface $theme): string {
* The chunk position.
*
* @return string
- * The rendered chunk; an absent chunk styles to nothing rather than to a
- * bare pair of styling codes.
+ * The rendered chunk; an absent chunk renders as an empty string, not as
+ * a bare pair of styling codes.
*/
protected function renderLiteral(ThemeInterface $theme, int $index): string {
$literal = $this->template->literalAt($index);
@@ -285,8 +283,8 @@ protected function renderSlot(ThemeInterface $theme, int $index, string $name):
$value = $this->parts[$name] ?? '';
- // An empty slot would collapse the shape into its fixed text alone, so it
- // shows its label instead - dimmed, to read as a hint and not a value.
+ // An empty slot would collapse the shape into its fixed text, so its
+ // label renders in its place. The dimming marks it as a hint, not a value.
return $value === '' ? $this->elements($theme)->fieldDescription($this->template->labelOf($name)) : $value;
}
diff --git a/src/FormException.php b/src/FormException.php
index 58d1af2b..c7017c2e 100644
--- a/src/FormException.php
+++ b/src/FormException.php
@@ -7,9 +7,9 @@
/**
* Thrown when a form declaration is invalid.
*
- * A declaration mistake is an argument a caller got wrong, so it lands in the
- * family a caller already catches for one: whichever surface refuses - a
- * builder, a block or a limit - one catch covers the lot.
+ * A declaration mistake is an argument the caller got wrong, so the class
+ * extends the exception family a caller already catches for one. Whichever
+ * surface throws it - a builder, a block or a limit - one catch covers all.
*
* @package DrevOps\Tui
*/
diff --git a/src/Input/Action.php b/src/Input/Action.php
index aa2c5c17..53accd82 100644
--- a/src/Input/Action.php
+++ b/src/Input/Action.php
@@ -7,12 +7,11 @@
/**
* A semantic input action, decoupled from the physical key that triggers it.
*
- * Fields and the panel controller ask a {@see ScopedKeyMap} whether a key
- * press means a given action ("is this Accept?"), rather than testing a raw
- * {@see KeyName}. The map binds each action to one or more keys, per scope, so
- * the same action can be reached by a different key in a different context (or
- * after a consumer remap). These are the fixed set of intents the fields
- * understand; the bindings behind them are configurable, the intents are not.
+ * Fields and the panel controller test a key press against an action via
+ * {@see ScopedKeyMap}, not against a raw {@see KeyName}. The map binds each
+ * action to one or more keys per scope, so a different context or a consumer
+ * remap can bind a different key to the same action. The set of actions is
+ * fixed; the bindings behind them are configurable.
*
* @package DrevOps\Tui\Input
*/
diff --git a/src/Input/Binding.php b/src/Input/Binding.php
index 94c8f7c1..5f89b0a3 100644
--- a/src/Input/Binding.php
+++ b/src/Input/Binding.php
@@ -5,14 +5,14 @@
namespace DrevOps\Tui\Input;
/**
- * One authored binding: an action reachable by a set of keys within a scope.
+ * One authored binding: an action and the keys that trigger it in a scope.
*
- * This is the declaration unit a preset ships and a consumer overrides with.
- * Keys are given in their most convenient form - a {@see KeyName} for a named
- * key, a single-character string for a printable key, or a ready {@see Key} -
- * and {@see KeyMap} normalizes them to {@see Key} when it resolves. Two
- * bindings for the same scope and action do not merge: the later one wins, so a
- * consumer replaces a preset's binding by re-declaring it.
+ * Presets and consumer overrides both declare their bindings in this form.
+ * A key may be a {@see KeyName} for a named key, a single-character string
+ * for a printable key, or a {@see Key}; {@see KeyMap} normalizes all three
+ * forms to {@see Key} when it resolves. Two bindings for the same scope and
+ * action do not merge: the later one wins, so a consumer replaces a preset's
+ * binding by re-declaring it.
*
* @package DrevOps\Tui\Input
*/
diff --git a/src/Screen/Capability/BorderCapableInterface.php b/src/Screen/Capability/BorderCapableInterface.php
index 72eb50c8..5047576a 100644
--- a/src/Screen/Capability/BorderCapableInterface.php
+++ b/src/Screen/Capability/BorderCapableInterface.php
@@ -10,8 +10,9 @@
/**
* Declares that something draws edges around the space it occupies.
*
- * Claimed by everything that occupies a rectangle - the screen, a region of a
- * layout, and any block - so one call means the same thing at every level.
+ * Implemented by everything that occupies a rectangle - the screen, a region
+ * of a layout, and any block - so one call means the same thing at every
+ * level.
*
* The declaration carries no geometry. What a thing occupies is known where it
* is drawn rather than where it is declared, so the renderer sizes the box and
@@ -46,8 +47,8 @@ public function border(int $sides = BorderSide::ALL, ?Border $style = NULL, stri
* Whether edges are drawn around this.
*
* @return bool
- * TRUE when they are. A style of {@see Border::None} is a refusal, so it
- * answers FALSE.
+ * TRUE when they are. A style of {@see Border::None} means no border, so
+ * it returns FALSE.
*/
public function isBordered(): bool;
diff --git a/src/Screen/Capability/BorderCapableTrait.php b/src/Screen/Capability/BorderCapableTrait.php
index 3bcfc16e..90f0236e 100644
--- a/src/Screen/Capability/BorderCapableTrait.php
+++ b/src/Screen/Capability/BorderCapableTrait.php
@@ -15,7 +15,7 @@
trait BorderCapableTrait {
/**
- * Whether edges are drawn, once one way or the other has been stated.
+ * Whether edges are drawn; NULL before any declaration.
*/
protected ?bool $bordered = NULL;
@@ -38,8 +38,8 @@ trait BorderCapableTrait {
* {@inheritdoc}
*/
public function border(int $sides = BorderSide::ALL, ?Border $style = NULL, string $title = ''): static {
- // Naming no side draws nothing, which is the same refusal as naming no
- // style: both leave the thing unboxed rather than boxed with no edges.
+ // Naming no side and naming a style of None both mean no border at all,
+ // not a border with no edges.
$this->bordered = $sides !== BorderSide::NONE && $style !== Border::None;
$this->borderSides = $sides;
$this->borderStyle = $style;
diff --git a/src/Screen/Capability/ScrollCapableInterface.php b/src/Screen/Capability/ScrollCapableInterface.php
index f23fa27b..7b4f8513 100644
--- a/src/Screen/Capability/ScrollCapableInterface.php
+++ b/src/Screen/Capability/ScrollCapableInterface.php
@@ -5,24 +5,23 @@
namespace DrevOps\Tui\Screen\Capability;
/**
- * A surface whose contents may outrun it, and be moved through.
+ * A surface whose contents may overflow it and be scrolled through.
*
- * Two things can be one: a region, whose contents are the blocks it holds, and
- * an arrangement whose lines move together, whose contents are every line at
- * once. The second is not a region's to do, because no region sees its
- * siblings - which is the same reason sizing belongs to the arrangement.
+ * A region can be one, with the blocks it holds as the contents, and so can
+ * an arrangement whose lines move together, with every line at once as the
+ * contents. No region sees its siblings, so scrolling the whole belongs to
+ * the arrangement, as sizing does.
*
- * Either way the surface only holds the offset and clamps it. What the offset
- * is an offset into is measured where things are drawn, and moving it is the
- * driver's, so the rule that keeps the cursor in sight lives in one place
- * whatever it is moving.
+ * Either way the surface only holds the offset and clamps it. The extent the
+ * offset indexes into is measured where things are drawn, and the driver
+ * moves it, so one rule keeps the cursor in sight whatever is moved.
*
* @package DrevOps\Tui\Screen\Capability
*/
interface ScrollCapableInterface {
/**
- * Let this surface's contents outrun it.
+ * Allow this surface's contents to overflow it.
*
* @return static
* The surface.
@@ -30,7 +29,7 @@ interface ScrollCapableInterface {
public function scrolls(): static;
/**
- * Whether this surface's contents may outrun it.
+ * Whether this surface's contents may overflow it.
*
* @return bool
* TRUE when they may.
@@ -57,7 +56,7 @@ public function scrollTo(int $row): static;
* The rows it was given.
*
* @return int
- * The offset, never far enough to scroll the contents off their own end.
+ * The offset, clamped so the contents cannot scroll past their own end.
*/
public function offset(int $content, int $visible): int;
diff --git a/src/Screen/Capability/ScrollCapableTrait.php b/src/Screen/Capability/ScrollCapableTrait.php
index ed9b73ce..d38fd62b 100644
--- a/src/Screen/Capability/ScrollCapableTrait.php
+++ b/src/Screen/Capability/ScrollCapableTrait.php
@@ -5,14 +5,14 @@
namespace DrevOps\Tui\Screen\Capability;
/**
- * Holds a surface's offset, and refuses to move one that does not scroll.
+ * Holds a surface's offset; scrollTo() throws for one that does not scroll.
*
* @package DrevOps\Tui\Screen\Capability
*/
trait ScrollCapableTrait {
/**
- * Whether this surface's contents may outrun it.
+ * Whether this surface's contents may overflow it.
*/
protected bool $scrolls = FALSE;
@@ -58,7 +58,7 @@ public function offset(int $content, int $visible): int {
}
/**
- * What this surface is called, for a refusal that names it.
+ * The name of this surface, used in the scrollTo() exception message.
*
* @return string
* The name.
diff --git a/src/Screen/ExternalEditor.php b/src/Screen/ExternalEditor.php
index ed1b908d..dbfe69d3 100644
--- a/src/Screen/ExternalEditor.php
+++ b/src/Screen/ExternalEditor.php
@@ -89,8 +89,8 @@ public function edit(string $initial, ?Terminal $terminal = NULL): ?string {
$code = $this->spawn($command, $file);
}
finally {
- // Restore the TUI even if the spawn throws, so a failed launch never
- // strands the terminal in raw mode.
+ // Restore the TUI even when the spawn throws, so a failed launch
+ // cannot leave the terminal in raw mode.
$terminal?->setup();
}
@@ -112,7 +112,7 @@ public function edit(string $initial, ?Terminal $terminal = NULL): ?string {
}
/**
- * Condition the saved buffer into a value the field can hold.
+ * Normalize the saved buffer.
*
* Drops a single trailing newline the editor appended by convention, and
* filters the control bytes an externally written file can contain.
diff --git a/src/Screen/Overlay.php b/src/Screen/Overlay.php
index b9eca684..f417b3f3 100644
--- a/src/Screen/Overlay.php
+++ b/src/Screen/Overlay.php
@@ -11,8 +11,8 @@
/**
* Pure line-compositor: splice a box of lines over a backdrop, centered.
*
- * Theme-agnostic - it knows nothing about colour. It places a rendered box over
- * a rectangular backdrop and splices each box line into the backdrop by visible
+ * Theme-agnostic: it handles no colour. It places a rendered box over a
+ * rectangular backdrop and splices each box line into the backdrop by visible
* column, so the backdrop shows through the padding on every side. Only the
* backdrop is sliced - it is plain text the caller has already flattened and
* padded to the composite width - while the styled box lines are placed
diff --git a/src/Screen/Region.php b/src/Screen/Region.php
index a7bb42df..edee4ff4 100644
--- a/src/Screen/Region.php
+++ b/src/Screen/Region.php
@@ -13,18 +13,17 @@
/**
* A named container inside a layout.
*
- * A region is a name, some blocks, and whether you can scroll them. Its name is
- * how a block says where it goes, so nothing depends on the order things were
- * declared in.
+ * A block addresses a region by name, so nothing depends on the order things
+ * were declared in.
*
- * The blocks run along one axis and are packed from either end of it, which is
- * placement rather than sizing: a block takes the space it drew whichever end
- * it was packed from.
+ * The blocks run along one axis and are packed from either end of it, which
+ * is placement rather than sizing: a block takes the space it drew whichever
+ * end it was packed from.
*
- * It declares a size without computing one: the arithmetic belongs to the
- * layout, which is the only thing that sees every sibling. Even a region as
- * deep as what it holds only says so - what it holds is counted where blocks
- * are drawn, and the layout is handed the number.
+ * A region declares a size without computing one: the layout is the only
+ * thing that sees every sibling, so the arithmetic belongs to it. A
+ * content-sized region's extent is counted where blocks are drawn, and the
+ * layout is handed the number.
*
* @package DrevOps\Tui\Screen
*/
@@ -34,12 +33,12 @@ final class Region implements BorderCapableInterface, ScrollCapableInterface {
use ScrollCapableTrait;
/**
- * How it asks for its share of the axis.
+ * How this region's share of the axis is determined.
*/
protected Sizing $sizing = Sizing::Flex;
/**
- * The cells it asked for, or the share it takes; nothing when it is measured.
+ * The cell count or flex share; 0 when the region is content-sized.
*/
protected int $size = 1;
@@ -49,7 +48,7 @@ final class Region implements BorderCapableInterface, ScrollCapableInterface {
protected Axis $flow = Axis::Rows;
/**
- * Whether a panel in it shows what is behind it rather than a row.
+ * Whether a panel in it draws its preview rather than its row.
*/
protected bool $previews = FALSE;
@@ -91,9 +90,6 @@ public function name(): string {
/**
* Take a fixed number of cells of the axis.
*
- * A header is one line whatever the terminal height, and no proportion can
- * say that, which is what this is for.
- *
* @param int $cells
* The cells to take.
*
@@ -112,10 +108,10 @@ public function fixed(int $cells): self {
}
/**
- * Take a share of whatever the fixed regions leave behind.
+ * Take a share of the cells the fixed regions leave.
*
- * Shares do not sum to anything in particular, so 30, 40, 30 and 3, 4, 3 mean
- * the same thing.
+ * Shares do not sum to anything in particular, so 30, 40, 30 and 3, 4, 3
+ * mean the same thing.
*
* @param int $share
* The share to take.
@@ -135,12 +131,10 @@ public function flex(int $share): self {
}
/**
- * Take as much of the axis as what this region holds comes to.
+ * Take as much of the axis as this region's contents come to.
*
- * A window is as deep as the rows behind it, and neither a count of cells nor
- * a share of the remainder can say that. What it comes to is measured where
- * blocks are drawn and handed to the layout, so this stays a declaration like
- * the other two rather than a measurement.
+ * The extent is counted where blocks are drawn and handed to the layout, so
+ * this stays a declaration, like the other two, rather than a measurement.
*
* @return $this
* The region.
@@ -153,7 +147,7 @@ public function content(): self {
}
/**
- * How this region asks for its share of the axis.
+ * How this region's share of the axis is determined.
*
* @return \DrevOps\Tui\Screen\Sizing
* The kind.
@@ -163,10 +157,10 @@ public function sizing(): Sizing {
}
/**
- * The cells this region asked for, or the share it takes.
+ * The cells this region declared, or the share it takes.
*
* @return int
- * The number, which is nothing at all when it is measured instead.
+ * The number, or 0 when the region is content-sized.
*/
public function size(): int {
return $this->size;
@@ -200,10 +194,10 @@ public function flowAxis(): Axis {
/**
* Show a panel in this region as a window onto it rather than as a row.
*
- * A row has one line to say what is behind a panel and a window has the depth
- * to show it, so which of the two a panel draws is a question about the space
- * rather than about the panel. Only the arrangement knows which the space is,
- * which is why it is declared here and not on the block.
+ * A row is one line and a window has depth, so which of the two a panel
+ * draws is a question about the space rather than about the panel. The
+ * space is known only to the arrangement, so the choice is declared here
+ * and not on the block.
*
* @return $this
* The region.
@@ -215,7 +209,7 @@ public function previews(): self {
}
/**
- * Whether a panel in this region shows what is behind it rather than a row.
+ * Whether a panel in this region draws its preview rather than its row.
*
* @return bool
* TRUE when it does.
@@ -227,9 +221,6 @@ public function isPreviewing(): bool {
/**
* Draw a block in this region.
*
- * A region never knows which kind it was given, which is why a breadcrumb can
- * go wherever a field can.
- *
* @param \DrevOps\Tui\Block\BlockInterface $block
* The block.
*
@@ -245,10 +236,6 @@ public function add(BlockInterface $block): self {
/**
* Draw a block before everything already in this region.
*
- * What a region holds is normally in the order it was declared in, so this is
- * for the standing text a driver puts above rows that were placed before it
- * knew there would be any.
- *
* @param \DrevOps\Tui\Block\BlockInterface $block
* The block.
*
@@ -265,9 +252,7 @@ public function prepend(BlockInterface $block): self {
* Draw a block at the far end of this region's flow.
*
* Where {@see self::add()} packs from the start of the axis the blocks run
- * along, this packs from the end of it - so a footer flowing across keeps its
- * key hints at the left and a version string at the right, and one flowing
- * down keeps a standing note on its last row.
+ * along, this packs from the end of it.
*
* @param \DrevOps\Tui\Block\BlockInterface $block
* The block.
@@ -305,7 +290,7 @@ public function headBlocks(): array {
* The blocks packed from the end of this region's flow.
*
* @return list<\DrevOps\Tui\Block\BlockInterface>
- * The blocks, none when everything it holds runs from the start.
+ * The blocks; empty when none were packed from the end.
*/
public function tailBlocks(): array {
return $this->tail;
diff --git a/src/Screen/Scroller.php b/src/Screen/Scroller.php
index 8971708d..2a285d56 100644
--- a/src/Screen/Scroller.php
+++ b/src/Screen/Scroller.php
@@ -7,9 +7,9 @@
/**
* Computes the visible window of a scrolling list.
*
- * `follow()` keeps the cursor inside the viewport (a key press re-engages
- * cursor-follow) and `viewport()` resolves a window for an offset alone. Both
- * clamp to the valid range, and a viewport reports ▲/▼.
+ * `follow()` keeps the cursor inside the viewport and `viewport()` resolves a
+ * window for an offset alone. Both clamp to the valid range, and a viewport
+ * reports ▲/▼.
*
* @package DrevOps\Tui\Screen
*/
@@ -50,7 +50,7 @@ public function follow(int $total, int $height, int $cursor, int $offset): Viewp
/**
* The viewport for an offset, clamped, with the scrolled-off flags resolved.
*
- * The single home of the "when do the scroll indicators show" rule.
+ * The one place the scroll-indicator flags are decided.
*
* @param int $offset
* The desired first-visible-line index.
diff --git a/src/Terminal/Ansi.php b/src/Terminal/Ansi.php
index e9b8619a..407d6832 100644
--- a/src/Terminal/Ansi.php
+++ b/src/Terminal/Ansi.php
@@ -29,7 +29,7 @@ final class Ansi {
protected const string LINK_SEQUENCE = '\033\]8;[^\007\033]*(?:\007|\033\\\\)';
/**
- * A CSI sequence, which is what carries the styling.
+ * A CSI sequence; CSI sequences carry the styling.
*/
protected const string STYLE_SEQUENCE = '\033\[[0-9;?<>=]*[A-Za-z]';
@@ -64,9 +64,9 @@ public static function style(string $text, string $sgr): string {
* The hyperlinked text.
*/
public static function link(string $text, string $url): string {
- // A raw ESC or BEL in either part would break out of the escape wrapper -
- // truncating the sequence or injecting arbitrary control codes downstream -
- // so every control byte is dropped before the URL and text are embedded.
+ // A raw ESC or BEL in either part would truncate the wrapper or inject
+ // arbitrary control codes downstream, so every control byte is dropped
+ // before the URL and text are embedded.
$text = self::stripControl($text);
$url = self::stripControl($url);
@@ -94,22 +94,21 @@ public static function stripControl(string $text): string {
* text and are kept; every other C0 control and DEL is removed.
*
* A carriage return folds to a newline instead of being dropped, because it
- * is a line break written on another platform.
+ * is another platform's line break.
*
* The C1 controls are removed as the two bytes UTF-8 encodes them as: a
* terminal in 8-bit mode reads U+009B as a CSI introducer. Their lead byte
* never appears inside another character, so multi-byte text is unaffected.
*
* Invalid UTF-8 is filtered a second time over the bare C1 range. Valid text
- * cannot hold a bare C1 byte, so the extra pass only reaches input that is
- * already malformed, and it closes the bypass of encoding a control byte
- * that way deliberately.
+ * cannot hold a bare C1 byte, so the extra pass only reaches malformed
+ * input. A control byte deliberately encoded as a bare C1 is still removed.
*
* @param string $text
* The text.
*
* @return string
- * The text a terminal can only print.
+ * The text with the control bytes a terminal acts on removed.
*/
public static function sanitize(string $text): string {
$text = (string) preg_replace('/\r\n?/', "\n", $text);
@@ -125,14 +124,12 @@ public static function sanitize(string $text): string {
/**
* Sanitize every string a value holds, whatever shape the value has.
*
- * An answer is a string, a list of strings, or a value that is not text.
* Arrays are walked to their leaves and a non-string value is returned
* unchanged.
*
- * Keys are left alone. A key addresses an entry rather than being read, the
- * same way an id addresses a field, and filtering two keys into one would
- * drop an entry. Where a key is drawn it is a label, and it is filtered by
- * whoever draws it.
+ * Keys are left alone: a key addresses an entry rather than being read, and
+ * filtering two keys into one would drop an entry. A key that is drawn is a
+ * label, filtered by the code that draws it.
*
* @param mixed $value
* The value.
diff --git a/src/Translation/Translator.php b/src/Translation/Translator.php
index 2f631f2c..8ffbb4de 100644
--- a/src/Translation/Translator.php
+++ b/src/Translation/Translator.php
@@ -139,8 +139,8 @@ public static function t(string $message, array $args = []): string {
* @param string $singular
* The English singular source, a catalog key and the one-form fallback.
* @param string $plural
- * The English plural source: the key a translation's forms hang from, and
- * the fallback form for any count that is not one.
+ * The English plural source: the key a translation's forms are listed
+ * under, and the fallback form for any count that is not one.
* @param array $args
* Replacements for the @name placeholders; @count is added automatically.
*
@@ -180,18 +180,18 @@ public function translate(string $source, array $args = []): string {
* A translation lists the forms for the plural source, and the catalog's own
* rule - or the default one-versus-other when it supplies none - selects
* among them. Without a translation the two English source forms and the
- * default rule stand in, so a language's rule never applies to the English
- * wording, whose singular reads for a count of exactly one. An index the
- * chosen forms do not cover falls back to the plural source, so a rendering
- * is always defined.
+ * default rule are used, so a language's rule never applies to the English
+ * wording and the English singular is used only for a count of one. An index
+ * the chosen forms do not cover falls back to the plural source, so a
+ * rendering is always defined.
*
* @param int $count
* The item count the form is chosen for.
* @param string $singular
* The English singular source, the one-form fallback.
* @param string $plural
- * The English plural source: the key the catalog's forms hang from, and the
- * fallback form.
+ * The English plural source: the key the catalog's forms are listed under,
+ * and the fallback form.
* @param array $args
* Replacements for the @name placeholders; @count is added automatically.
*
@@ -228,15 +228,13 @@ protected static function defaultPluralRule(): \Closure {
/**
* Substitute @name placeholders in a message.
*
- * The one substitution routine, shared by the instance path and the t()
- * fallback path so a translated and an untranslated string interpolate
- * identically.
+ * One substitution routine serves the instance path and the t() fallback
+ * path, so a translated and an untranslated string interpolate identically.
*
- * A placeholder value may itself be an already-translated phrase (as the
- * bounds `describe()` methods and the schema validator compose one message
- * inside another); this concatenation cannot honour every locale's word order
- * or grammatical agreement, so a language needing those would supply
- * full-sentence catalog keys rather than relying on composition.
+ * A placeholder value may itself be an already-translated phrase; the
+ * concatenation cannot honour every locale's word order or grammatical
+ * agreement, so a language needing those supplies full-sentence catalog
+ * keys rather than relying on composition.
*
* @param string $message
* The message, possibly carrying @name placeholders.
@@ -415,8 +413,8 @@ protected function readCatalog(string $file): array {
*
* Returns the string => string pairs; the plural-form lists and a plural-rule
* closure are read as side effects into $this->plurals and $this->pluralRule,
- * so the caller merges only the returned string map. The one normalisation
- * routine, shared by file catalogs and inline maps so both shapes honour the
+ * so the caller merges only the returned string map. One normalisation
+ * routine serves file catalogs and inline maps, so both shapes honour the
* same entry kinds.
*
* @param array $data
diff --git a/src/Tui.php b/src/Tui.php
index 6460e498..aab03e6b 100644
--- a/src/Tui.php
+++ b/src/Tui.php
@@ -60,7 +60,7 @@ final class Tui {
protected string $envPrefix;
/**
- * What collects the answers with no screen at all, once one is asked for.
+ * The headless collector, created on first use.
*/
protected ?Collector $collector = NULL;
@@ -77,7 +77,7 @@ final class Tui {
protected array $themeOptions = [];
/**
- * What the consumer states differently, on whatever theme is selected.
+ * The consumer's overrides, applied to whichever theme is selected.
*/
protected ?Overrides $themeOverrides = NULL;
@@ -161,13 +161,12 @@ public function __construct(protected Form $form, array $handler_namespaces = []
/**
* Select the theme, or state what it draws differently.
*
- * Two things a consumer wants at two different sizes, so one call answers
- * both. A name picks the theme and its display options. A closure is handed a
- * {@see \DrevOps\Tui\Theme\ThemeBuilder} and states the elements whatever
- * theme is selected should draw differently - anything it does not name keeps
- * the theme's own answer, which is what makes it a patch rather than a
- * replacement. Reach for a subclass when changing a palette; reach for the
- * closure when changing a handful of glyphs.
+ * A name picks the theme and its display options. A closure receives a
+ * {@see \DrevOps\Tui\Theme\ThemeBuilder} and states the elements the
+ * selected theme draws differently; an element it does not name keeps the
+ * theme's own value, so the closure is a patch rather than a replacement.
+ * A palette change suits a theme subclass; a handful of glyphs suits the
+ * closure.
*
* @code
* $tui->theme('mono')
@@ -205,7 +204,7 @@ public function theme(string|\Closure $theme, array $options = []): self {
}
/**
- * Build the selected theme, carrying anything stated differently.
+ * Build the selected theme and apply the consumer's overrides.
*
* @param string $name
* The theme name or class; empty falls back to the facade's theme.
@@ -220,8 +219,8 @@ public function theme(string|\Closure $theme, array $options = []): self {
protected function buildTheme(string $name, int $width, array $options): ThemeInterface {
$theme = ThemeManager::create($this->resolveTheme($name), $width, $options);
- // A patch is a facility a theme declares, so a theme with nowhere to put
- // one keeps its own answers rather than the patch deciding it cannot draw.
+ // Overrides apply only through a theme's own override capability, so a
+ // theme without it is used unchanged rather than rejected.
if (!$this->themeOverrides instanceof Overrides || !$theme instanceof OverrideCapableInterface) {
return $theme;
}
@@ -251,18 +250,18 @@ public function layout(string $layout): self {
}
/**
- * Put a block of your own in one of the screen's regions.
+ * Put a consumer block in one of the screen's regions.
*
- * The regions around the form are the session's rather than the form's, so
- * what stands in them is stated here rather than declared on a panel: a
- * standing note beside the trail, a version string beside the key hints. The
- * standard furniture is placed first, so a block lands after the trail or the
- * hints its region already holds.
+ * The regions around the form belong to the session rather than the form,
+ * so their extra blocks are placed here rather than declared on a panel: a
+ * note beside the trail, a version string beside the key hints. The built-in
+ * blocks are placed first, so a placed block follows the trail or the hints
+ * its region already holds.
*
- * Which end of the region's run it packs from is the block's own to say.
- * Packing from the end is what puts something against the far edge - the
- * right of a region running across, the last row of one running down - and
- * where the two runs meet, what packs from the start keeps its space.
+ * The $tail flag selects which end of the region's run the block packs
+ * from. Packing from the end puts a block against the far edge - the right
+ * of a region running across, the last row of one running down - and where
+ * the two runs meet, a block packed from the start keeps its space.
*
* @code
* $tui->layout('market')
@@ -291,11 +290,11 @@ public function place(string $region, BlockInterface $block, bool $tail = FALSE)
/**
* Run one region's blocks across it rather than down it.
*
- * A region a layout declared running one way is turned the other way here,
- * which is what saves a form from arranging a layout of its own every time
- * two things belong on the same line. A region running across gives each
- * block the width it drew; one running down gives each a row. Either way a
- * region that was not declared to scroll clips what outruns it.
+ * A region the layout declared running one way is turned the other way
+ * here, so a form needs no layout of its own every time two blocks belong
+ * on the same line. A region running across gives each block the width it
+ * drew; one running down gives each a row. On either axis, a region that
+ * was not declared to scroll clips what overflows it.
*
* @param string $region
* The region name, as the layout declares it.
@@ -313,11 +312,11 @@ public function flow(string $region, Axis $axis): self {
}
/**
- * Check the layout answers to every region name stated so far.
+ * Check that the layout declares every region name stated so far.
*
- * Re-checked from every setter that can invalidate the answer, so a name
- * nothing answers to throws where it was written whichever order the layout
- * and the regions were named in.
+ * Every setter that can invalidate the check re-runs it, so an unknown name
+ * throws at the call that stated it, whichever order the layout and the
+ * regions were named in.
*
* @throws \InvalidArgumentException
* When the layout declares no region of a stated name.
@@ -335,7 +334,7 @@ protected function assertRegions(): void {
*
* The preset names the base bindings ("default", "vim", a registered name, or
* a preset class); each override is a {@see \DrevOps\Tui\Input\Binding}
- * naming a scope, an action and its keys, retuning it on top of the preset.
+ * naming a scope, an action and its keys, applied on top of the preset.
* Conflicting, un-typeable or malformed bindings throw here, not mid-session.
*
* @param string $preset
@@ -491,9 +490,9 @@ public function translator(Translator $translator): self {
* TRUE/FALSE to force the mode; NULL auto-detects from the prompts and
* the standard-input TTY.
* @param bool $update
- * Whether to enable discovery against an existing project. It reaches the
- * chosen mode's initial state, so the panels open pre-filled interactively
- * just as the headless answers resolve them.
+ * Whether to enable discovery against an existing project. Discovery
+ * feeds the chosen mode's initial state: the panels open pre-filled
+ * interactively, and the headless answers resolve to the same values.
*
* @return \DrevOps\Tui\Answers\Answers
* The collected answers.
@@ -591,7 +590,7 @@ public function progress(?int $total, string $caption, callable $work, ?Terminal
* The terminal to write to (defaults to a real one on standard error).
*
* @return \DrevOps\Tui\Primitive\Output
- * The output primitive; hold onto it to write more than one piece.
+ * The output primitive; one instance writes any number of pieces.
*/
public function output(?Terminal $terminal = NULL): Output {
// Restore this facade's language at the operation boundary (see collect()).
@@ -608,9 +607,9 @@ public function output(?Terminal $terminal = NULL): Output {
/**
* The theme, narrowed to the finished pieces a primitive writes.
*
- * A card, a grid, a status line and a bar are composed rather than styled, so
- * a theme that answers for none of them says so by name here rather than
- * failing on the first line it is asked to write.
+ * A card, a grid, a status line and a bar are composed rather than styled,
+ * so a theme that implements none of them fails here with a named error
+ * rather than at the first line it writes.
*
* @param \DrevOps\Tui\Theme\ThemeInterface $theme
* The theme.
@@ -667,8 +666,6 @@ public function interact(string $theme = '', string $banner = '', string $versio
// @codeCoverageIgnoreEnd
}
- // The theme's display options (colour, Unicode, mode) come from the facade
- // when set, otherwise they are auto-detected from the terminal.
$options = $this->resolveThemeOptions($terminal);
return $this->controller($options, $theme, $banner, $version, $directory, self::frameWidth($options, $terminal->width()), $update)->run($terminal);
@@ -677,10 +674,9 @@ public function interact(string $theme = '', string $banner = '', string $versio
/**
* Build the session that drives the form for the resolved display options.
*
- * Shared by interact() and the test harness: it builds the tree the form
- * declares, resolves the theme and banner and wires the session - so a caller
- * that supplies its own terminal (a real one, or a scripted one for tests)
- * can run it against that.
+ * Builds the tree the form declares, resolves the theme and banner and
+ * wires the session, so a caller that supplies its own terminal (a real
+ * one, or a scripted one for tests) can run the session against it.
*
* @param array $options
* The resolved theme display options (colour, Unicode, mode).
@@ -718,10 +714,9 @@ public function controller(array $options, string $theme = '', string $banner =
$this->keyMap ?? KeyMapManager::create(),
new Collector($this->registry, $this->form->currentFixups()),
$this->context($directory, $update, $version),
- // The frame the theme was told to lay its rows out to is the frame that
- // has to be drawn around them, so the border is read back off it rather
- // than resolved a second time here. A theme that says nothing about the
- // room it takes gets no edge drawn around it.
+ // The border must match the frame the theme laid its rows out to, so it
+ // is read back off the built theme rather than resolved a second time.
+ // A theme without the occupy capability gets no border.
layout: $this->layout,
border: $drawn instanceof OccupyCapableInterface ? $drawn->borderStyle() : Border::None,
clearOnExit: $this->clearOnExit,
@@ -815,10 +810,10 @@ public function registry(): HandlerRegistry {
/**
* The declared block tree: the panel every declared panel hangs from.
*
- * The rows a form asks about are settled state - a set of entries that
- * arrives from somewhere else settles onto the block holding it - so one tree
- * carries the declaration and what has become of it, and every operation on
- * this facade reads that one.
+ * The rows a form asks about are state: a set of entries supplied from
+ * elsewhere is stored on the block holding it, so one tree carries the
+ * declaration and its state. Every operation on this facade reads that one
+ * tree.
*
* @return \DrevOps\Tui\Block\Panel
* The root panel.
@@ -909,8 +904,8 @@ protected function resolvedUnicode(): bool {
/**
* A real terminal that draws a primitive's output on standard error.
*
- * A primitive is chrome, not data, so it stays off standard output where a
- * consumer's own results are written.
+ * A primitive is chrome, not data, so it writes to standard error and
+ * leaves standard output to a consumer's own results.
*
* @return \DrevOps\Tui\Terminal\Terminal
* The terminal.
diff --git a/src/Utils/Strings.php b/src/Utils/Strings.php
index 4d66ec0f..49fba640 100644
--- a/src/Utils/Strings.php
+++ b/src/Utils/Strings.php
@@ -10,9 +10,9 @@
* UTF-8 string helpers backed by mbstring when available.
*
* The mbstring detection, the byte-level fallbacks behind it and the case
- * folding are the base class's; what is added here is the text handling a
- * terminal needs and a name formatter does not - measuring, slicing and
- * wrapping a line, and filling a template in.
+ * folding come from the base class. This class adds the text handling a
+ * terminal needs and a name formatter does not: measuring, slicing and
+ * wrapping a line, and filling a template.
*
* @package DrevOps\Tui\Utils
*/
From 98430f9f31bebd593e340d4416182e7c0170b7ae Mon Sep 17 00:00:00 2001
From: Alex Skrypnyk
Date: Tue, 11 Aug 2026 19:54:35 +1000
Subject: [PATCH 14/27] Addressed code review: corrected nine documentation
claims to match the code.
---
src/Block/AbstractBlock.php | 4 ++--
src/Block/Field.php | 2 +-
src/Block/Panel.php | 6 +++---
src/Builder/FieldBuilder.php | 6 +++---
src/Field/Capability/OptionsCapableTrait.php | 6 +++---
src/Field/Rating.php | 5 +++--
src/Field/Reorder.php | 7 +++----
src/Screen/Capability/ScrollCapableInterface.php | 3 +++
src/Terminal/Ansi.php | 2 +-
9 files changed, 22 insertions(+), 19 deletions(-)
diff --git a/src/Block/AbstractBlock.php b/src/Block/AbstractBlock.php
index 6b7e8586..4031c164 100644
--- a/src/Block/AbstractBlock.php
+++ b/src/Block/AbstractBlock.php
@@ -12,8 +12,8 @@
* Behaviour every block shares: drawing through a theme's elements.
*
* A block draws with the elements it declares, and {@see elements()} narrows
- * the theme to them: a theme that does not implement them fails as a type
- * error rather than drawing a blank line.
+ * 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. The declaration carries no geometry -
* what a block occupies is known where it is drawn - so the renderer sizes
diff --git a/src/Block/Field.php b/src/Block/Field.php
index 8c15bb9e..495b6589 100644
--- a/src/Block/Field.php
+++ b/src/Block/Field.php
@@ -2339,7 +2339,7 @@ protected function valueRegion(ThemeInterface $theme, FieldElementsInterface $el
* One entry as it is drawn.
*
* @param \DrevOps\Tui\Block\Element\FieldElementsInterface $elements
- * The theme.
+ * The theme narrowed to the field elements.
* @param \DrevOps\Tui\Block\Option $entry
* The entry.
*
diff --git a/src/Block/Panel.php b/src/Block/Panel.php
index b762217a..a134ace0 100644
--- a/src/Block/Panel.php
+++ b/src/Block/Panel.php
@@ -389,10 +389,10 @@ public function fields(): array {
/**
* The ids of the rows this panel holds, in the order they were placed.
*
- * Every row that carries an id is included, whether it collects an answer
- * or only shows something. A display-only id is still an id the form
+ * Field, markup and progress rows are included, whether they collect an
+ * answer or only show something. A display-only id is still an id the form
* knows, so a stray answer can be told from one aimed at a row that takes
- * none.
+ * none. The ids of nested panels are not included.
*
* @return list
* The ids.
diff --git a/src/Builder/FieldBuilder.php b/src/Builder/FieldBuilder.php
index a067ca94..17fa6218 100644
--- a/src/Builder/FieldBuilder.php
+++ b/src/Builder/FieldBuilder.php
@@ -26,9 +26,9 @@
* the declaration is finished, because only a finished one can be checked
* for contradictions.
*
- * Most declarations apply to some kinds of answer and not to others, and one
- * the kind has nowhere to put is rejected where it was written rather than
- * quietly dropped, so the error points at the line that made it.
+ * Most declarations apply to some kinds of answer and not to others. A
+ * declaration the kind has nowhere to put is rejected where it was written
+ * rather than quietly dropped, so the error points at the line that made it.
*
* @package DrevOps\Tui\Builder
*/
diff --git a/src/Field/Capability/OptionsCapableTrait.php b/src/Field/Capability/OptionsCapableTrait.php
index 56f5e882..85a1403c 100644
--- a/src/Field/Capability/OptionsCapableTrait.php
+++ b/src/Field/Capability/OptionsCapableTrait.php
@@ -13,9 +13,9 @@
* Shared option-list behaviour for the choice fields.
*
* Holds the ordered option rows and the two invariants every choice field
- * shares. The cursor is only ever placed on a selectable row, skipping
- * separators, headings and disabled options. Non-selectable rows render as
- * visual-only structure.
+ * shares. Where a selectable row exists the cursor is placed on one, skipping
+ * separators, headings and disabled options; where none is selectable it
+ * falls back to index 0. Non-selectable rows render as visual-only structure.
*
* @package DrevOps\Tui\Field\Capability
*/
diff --git a/src/Field/Rating.php b/src/Field/Rating.php
index 59589db1..dd5b67fa 100644
--- a/src/Field/Rating.php
+++ b/src/Field/Rating.php
@@ -94,8 +94,9 @@ public function stepBy(int $delta): void {
/**
* Jump to the point a typed digit names.
*
- * A digit the scale does not include is ignored, so typing on a scale that
- * starts above nine, or runs well past it, is inert.
+ * A digit the scale does not include is ignored. One digit names one point,
+ * so only the points 0 to 9 are reachable by typing; the rest are reached
+ * with the movement keys.
*
* @param string $char
* The typed character.
diff --git a/src/Field/Reorder.php b/src/Field/Reorder.php
index 6054de0a..03279904 100644
--- a/src/Field/Reorder.php
+++ b/src/Field/Reorder.php
@@ -92,8 +92,8 @@ public function handle(Key $key): void {
}
if ($keys->matches($key, Action::Accept)) {
- // Accept drops a held item, as Space does, so nothing is committed
- // mid-move.
+ // Accept drops a held item, as the grab action does, so nothing is
+ // committed mid-move.
if ($this->grabbed) {
$this->grabbed = FALSE;
}
@@ -225,8 +225,7 @@ public function renderOptionRow(ThemeInterface $theme, Option $option, bool $cur
* The two-column marker cell.
*/
protected function marker(ThemeInterface $theme, bool $current): string {
- // The glyphs are the keys that move the item, so the mark shows what to
- // press.
+ // The glyphs name the two directions the item moves in.
if ($current && $this->grabbed) {
return $theme->keyGlyph(KeyName::Up) . $theme->keyGlyph(KeyName::Down);
}
diff --git a/src/Screen/Capability/ScrollCapableInterface.php b/src/Screen/Capability/ScrollCapableInterface.php
index 7b4f8513..4ab7b402 100644
--- a/src/Screen/Capability/ScrollCapableInterface.php
+++ b/src/Screen/Capability/ScrollCapableInterface.php
@@ -44,6 +44,9 @@ public function isScrolling(): bool;
*
* @return static
* The surface.
+ *
+ * @throws \LogicException
+ * When the surface does not scroll.
*/
public function scrollTo(int $row): static;
diff --git a/src/Terminal/Ansi.php b/src/Terminal/Ansi.php
index 407d6832..c66cccb6 100644
--- a/src/Terminal/Ansi.php
+++ b/src/Terminal/Ansi.php
@@ -65,7 +65,7 @@ public static function style(string $text, string $sgr): string {
*/
public static function link(string $text, string $url): string {
// A raw ESC or BEL in either part would truncate the wrapper or inject
- // arbitrary control codes downstream, so every control byte is dropped
+ // arbitrary control codes downstream, so the C0 bytes and DEL are dropped
// before the URL and text are embedded.
$text = self::stripControl($text);
$url = self::stripControl($url);
From e23b77679e6e33eed96acb07f0afb8c2df7ec637 Mon Sep 17 00:00:00 2001
From: Alex Skrypnyk
Date: Tue, 11 Aug 2026 21:09:46 +1000
Subject: [PATCH 15/27] Named windows and panels in the grid capacity error,
matching the code.
---
src/Builder/PanelBuilder.php | 2 +-
src/Screen/Layout/GridLayout.php | 12 ++++++------
tests/phpunit/Unit/Builder/FormTest.php | 6 +++---
tests/phpunit/Unit/Screen/Layout/GridLayoutTest.php | 2 +-
4 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/src/Builder/PanelBuilder.php b/src/Builder/PanelBuilder.php
index dc44af53..108a52d1 100644
--- a/src/Builder/PanelBuilder.php
+++ b/src/Builder/PanelBuilder.php
@@ -621,7 +621,7 @@ protected function descend(Panel $panel): void {
$window = $layout->windows()[count($this->panels) - 1] ?? NULL;
if ($window === NULL) {
- throw new FormException(sprintf('The grid of "%s" declares %d slot(s) for %d window(s).', $this->id, count($layout->windows()), count($this->panels)));
+ throw new FormException(sprintf('The grid of "%s" declares %d window(s) for %d panel(s).', $this->id, count($layout->windows()), count($this->panels)));
}
$this->panel->in($window)->add($panel);
diff --git a/src/Screen/Layout/GridLayout.php b/src/Screen/Layout/GridLayout.php
index e562b486..9cae824c 100644
--- a/src/Screen/Layout/GridLayout.php
+++ b/src/Screen/Layout/GridLayout.php
@@ -170,21 +170,21 @@ public function windows(): array {
/**
* Assert this grid's slots cover exactly the windows dealt into it.
*
- * @param int $windows
- * How many windows there are to deal.
+ * @param int $panels
+ * How many panels there are to deal.
* @param string $owner
* What declared the grid, as the name it goes by.
*
* @throws \DrevOps\Tui\FormException
- * When the slots do not cover the windows, which would leave one of them
+ * When the windows do not cover the panels, which would leave one of them
* undrawn or a row of the grid empty.
*/
- public function assertDeals(int $windows, string $owner): void {
- if (count($this->windows) === $windows) {
+ public function assertDeals(int $panels, string $owner): void {
+ if (count($this->windows) === $panels) {
return;
}
- throw new FormException(sprintf('The grid of "%s" declares %d slot(s) for %d window(s).', $owner, count($this->windows), $windows));
+ throw new FormException(sprintf('The grid of "%s" declares %d window(s) for %d panel(s).', $owner, count($this->windows), $panels));
}
/**
diff --git a/tests/phpunit/Unit/Builder/FormTest.php b/tests/phpunit/Unit/Builder/FormTest.php
index 945a1505..e6c25b11 100644
--- a/tests/phpunit/Unit/Builder/FormTest.php
+++ b/tests/phpunit/Unit/Builder/FormTest.php
@@ -972,7 +972,7 @@ static function (): void {
->panel('b', 'B', fn(PanelBuilder $p): FieldBuilder => $p->text('two', 'Two'))
->root();
},
- 'The grid of "Demo" declares 1 slot(s) for 2 window(s).',
+ 'The grid of "Demo" declares 1 window(s) for 2 panel(s).',
];
yield 'form slots above the panels' => [
@@ -983,7 +983,7 @@ static function (): void {
->panel('b', 'B', fn(PanelBuilder $p): FieldBuilder => $p->text('two', 'Two'))
->root();
},
- 'The grid of "Demo" declares 4 slot(s) for 2 window(s).',
+ 'The grid of "Demo" declares 4 window(s) for 2 panel(s).',
];
yield 'panel slots mismatch its children' => [
@@ -995,7 +995,7 @@ static function (): void {
})
->root();
},
- 'The grid of "a" declares 2 slot(s) for 1 window(s).',
+ 'The grid of "a" declares 2 window(s) for 1 panel(s).',
];
yield 'zero-width row' => [
diff --git a/tests/phpunit/Unit/Screen/Layout/GridLayoutTest.php b/tests/phpunit/Unit/Screen/Layout/GridLayoutTest.php
index 9b39f862..f92f0fd4 100644
--- a/tests/phpunit/Unit/Screen/Layout/GridLayoutTest.php
+++ b/tests/phpunit/Unit/Screen/Layout/GridLayoutTest.php
@@ -160,7 +160,7 @@ public function testShapeWithRowNothingCouldBeDealtIntoIsRefused(): void {
public function testSlotsThatDoNotCoverTheWindowsAreRefusedNamingTheOwner(): void {
$this->expectException(FormException::class);
- $this->expectExceptionMessage('The grid of "Market stall" declares 3 slot(s) for 2 window(s).');
+ $this->expectExceptionMessage('The grid of "Market stall" declares 3 window(s) for 2 panel(s).');
(new GridLayout(1, 2))->assertDeals(2, 'Market stall');
}
From ca0a44d178ce1c2ea00d2659d1d1531174818dc0 Mon Sep 17 00:00:00 2001
From: Alex Skrypnyk
Date: Tue, 11 Aug 2026 21:09:52 +1000
Subject: [PATCH 16/27] Threw 'FormException' for the sixteen
consumer-declaration mistakes.
---
src/Block/Actions.php | 6 +++++-
src/Field/Capability/PagingCapableTrait.php | 5 +++--
src/Input/KeyMap.php | 15 ++++++++-------
src/Resolver/InputResolver.php | 5 +++--
src/Screen/Layout/AbstractLayout.php | 11 +++++++++--
src/Screen/Region.php | 11 +++++++++--
src/Theme/DefaultTheme.php | 11 ++++++-----
src/Translation/Translator.php | 7 ++++++-
8 files changed, 49 insertions(+), 22 deletions(-)
diff --git a/src/Block/Actions.php b/src/Block/Actions.php
index 44cd02f6..3619c2f5 100644
--- a/src/Block/Actions.php
+++ b/src/Block/Actions.php
@@ -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;
@@ -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;
diff --git a/src/Field/Capability/PagingCapableTrait.php b/src/Field/Capability/PagingCapableTrait.php
index 6dc73bd2..3f9f66ab 100644
--- a/src/Field/Capability/PagingCapableTrait.php
+++ b/src/Field/Capability/PagingCapableTrait.php
@@ -5,6 +5,7 @@
namespace DrevOps\Tui\Field\Capability;
use DrevOps\Tui\Block\Element\FieldElementsInterface;
+use DrevOps\Tui\FormException;
use DrevOps\Tui\Screen\Scroller;
use DrevOps\Tui\Screen\Viewport;
use DrevOps\Tui\Theme\ThemeInterface;
@@ -54,12 +55,12 @@ public function pageSize(): int {
* @return int
* The effective page size.
*
- * @throws \InvalidArgumentException
+ * @throws \DrevOps\Tui\FormException
* When a declared page size is not positive.
*/
protected function resolvePageSize(?int $page_size): int {
if ($page_size !== NULL && $page_size < 1) {
- throw new \InvalidArgumentException(Translator::t('Page size must be a positive integer, @size given.', [
+ throw new FormException(Translator::t('Page size must be a positive integer, @size given.', [
'@size' => $page_size,
]));
}
diff --git a/src/Input/KeyMap.php b/src/Input/KeyMap.php
index 373c94d1..ec7d6433 100644
--- a/src/Input/KeyMap.php
+++ b/src/Input/KeyMap.php
@@ -5,6 +5,7 @@
namespace DrevOps\Tui\Input;
use DrevOps\Tui\Block\FieldType;
+use DrevOps\Tui\FormException;
/**
* The resolved, validated key bindings for a whole form.
@@ -175,7 +176,7 @@ protected function buildScope(array $base_inverted, array $layers, Scope $scope)
* @return array
* The inverted map.
*
- * @throws \InvalidArgumentException
+ * @throws \DrevOps\Tui\FormException
* When one key is bound to two different actions in the layer.
*/
protected function invert(array $layer, Scope $scope): array {
@@ -186,7 +187,7 @@ protected function invert(array $layer, Scope $scope): array {
$token = $key->token();
if (isset($inverted[$token]) && $inverted[$token]['action'] !== $entry['action']) {
- throw new \InvalidArgumentException(sprintf('Key "%s" is bound to both %s and %s in the %s scope.', $key->label(), $inverted[$token]['action']->name, $entry['action']->name, $scope->label()));
+ throw new FormException(sprintf('Key "%s" is bound to both %s and %s in the %s scope.', $key->label(), $inverted[$token]['action']->name, $entry['action']->name, $scope->label()));
}
$inverted[$token] = ['key' => $key, 'action' => $entry['action']];
@@ -225,7 +226,7 @@ protected function toScoped(array $inverted): ScopedKeyMap {
* @param \DrevOps\Tui\Input\Scope $scope
* The scope being checked.
*
- * @throws \InvalidArgumentException
+ * @throws \DrevOps\Tui\FormException
* When a printable character is bound in the base or a text-entry scope.
*/
protected function assertTypeable(array $inverted, Scope $scope): void {
@@ -241,10 +242,10 @@ protected function assertTypeable(array $inverted, Scope $scope): void {
}
if ($scope->fieldType instanceof FieldType) {
- throw new \InvalidArgumentException(sprintf('The %s scope consumes typed characters, so the printable character "%s" cannot be bound to an action there.', $scope->label(), $entry['key']->label()));
+ throw new FormException(sprintf('The %s scope consumes typed characters, so the printable character "%s" cannot be bound to an action there.', $scope->label(), $entry['key']->label()));
}
- throw new \InvalidArgumentException(sprintf('The base scope may not bind the printable character "%s"; it would be un-typeable in text fields. Bind it in a specific non-text scope instead.', $entry['key']->label()));
+ throw new FormException(sprintf('The base scope may not bind the printable character "%s"; it would be un-typeable in text fields. Bind it in a specific non-text scope instead.', $entry['key']->label()));
}
}
@@ -272,7 +273,7 @@ protected function isControl(string $char): bool {
* @return list<\DrevOps\Tui\Input\Key>
* The normalized keys.
*
- * @throws \InvalidArgumentException
+ * @throws \DrevOps\Tui\FormException
* When a character binding is not exactly one character.
*/
protected function normalize(array $keys, Scope $scope): array {
@@ -289,7 +290,7 @@ protected function normalize(array $keys, Scope $scope): array {
$out[] = Key::char($key);
}
else {
- throw new \InvalidArgumentException(sprintf('A character binding in the %s scope must be a single character, got "%s".', $scope->label(), $key));
+ throw new FormException(sprintf('A character binding in the %s scope must be a single character, got "%s".', $scope->label(), $key));
}
}
diff --git a/src/Resolver/InputResolver.php b/src/Resolver/InputResolver.php
index 6ed35779..6b98b90b 100644
--- a/src/Resolver/InputResolver.php
+++ b/src/Resolver/InputResolver.php
@@ -6,6 +6,7 @@
use DrevOps\Tui\Block\Field;
use DrevOps\Tui\Block\FieldType;
+use DrevOps\Tui\FormException;
use DrevOps\Tui\Translation\Translator;
/**
@@ -121,7 +122,7 @@ protected function splitList(string $value): array {
* @return array
* The decoded map keyed by field id.
*
- * @throws \InvalidArgumentException
+ * @throws \DrevOps\Tui\FormException
* When the operand decodes to anything but a JSON object - failing loudly
* instead of silently discarding every supplied answer.
*/
@@ -134,7 +135,7 @@ protected function parsePrompts(string $prompts): array {
$data = json_decode($json, TRUE);
if (!is_array($data)) {
- throw new \InvalidArgumentException(Translator::t('The --prompts value is neither a JSON object nor a path to one.'));
+ throw new FormException(Translator::t('The --prompts value is neither a JSON object nor a path to one.'));
}
$out = [];
diff --git a/src/Screen/Layout/AbstractLayout.php b/src/Screen/Layout/AbstractLayout.php
index 845434a2..d263141a 100644
--- a/src/Screen/Layout/AbstractLayout.php
+++ b/src/Screen/Layout/AbstractLayout.php
@@ -5,6 +5,7 @@
namespace DrevOps\Tui\Screen\Layout;
use DrevOps\Tui\Block\Element\ChromeElementsInterface;
+use DrevOps\Tui\FormException;
use DrevOps\Tui\Screen\Axis;
use DrevOps\Tui\Screen\Capability\ScrollCapableTrait;
use DrevOps\Tui\Screen\Furniture;
@@ -75,10 +76,13 @@ public function names(): array {
/**
* {@inheritdoc}
+ *
+ * @throws \DrevOps\Tui\FormException
+ * When the region name is unknown.
*/
public function in(string $name): Region {
if (!isset($this->regions[$name])) {
- throw new \InvalidArgumentException(sprintf('Unknown region "%s". This layout declares: %s.', $name, implode(', ', $this->names())));
+ throw new FormException(sprintf('Unknown region "%s". This layout declares: %s.', $name, implode(', ', $this->names())));
}
return $this->regions[$name];
@@ -213,10 +217,13 @@ public function share(int $available, int $count, ChromeElementsInterface $chrom
*
* @return \DrevOps\Tui\Screen\Region
* The region, for declaring its size, flow and scrolling.
+ *
+ * @throws \DrevOps\Tui\FormException
+ * When the region name is already declared.
*/
protected function region(string $name): Region {
if (isset($this->regions[$name])) {
- throw new \InvalidArgumentException(sprintf('Region "%s" is already declared on this layout.', $name));
+ throw new FormException(sprintf('Region "%s" is already declared on this layout.', $name));
}
return $this->regions[$name] = new Region($name);
diff --git a/src/Screen/Region.php b/src/Screen/Region.php
index edee4ff4..45c5697c 100644
--- a/src/Screen/Region.php
+++ b/src/Screen/Region.php
@@ -5,6 +5,7 @@
namespace DrevOps\Tui\Screen;
use DrevOps\Tui\Block\BlockInterface;
+use DrevOps\Tui\FormException;
use DrevOps\Tui\Screen\Capability\BorderCapableInterface;
use DrevOps\Tui\Screen\Capability\BorderCapableTrait;
use DrevOps\Tui\Screen\Capability\ScrollCapableInterface;
@@ -95,10 +96,13 @@ public function name(): string {
*
* @return $this
* The region.
+ *
+ * @throws \DrevOps\Tui\FormException
+ * When the cell count is 0 or negative.
*/
public function fixed(int $cells): self {
if ($cells < 1) {
- throw new \InvalidArgumentException('A fixed size is a count of cells, so it cannot be 0.');
+ throw new FormException('A fixed size is a count of cells, so it cannot be 0.');
}
$this->sizing = Sizing::Fixed;
@@ -118,10 +122,13 @@ public function fixed(int $cells): self {
*
* @return $this
* The region.
+ *
+ * @throws \DrevOps\Tui\FormException
+ * When the share is 0 or negative.
*/
public function flex(int $share): self {
if ($share < 1) {
- throw new \InvalidArgumentException('A flex share divides the remainder, so it cannot be 0.');
+ throw new FormException('A flex share divides the remainder, so it cannot be 0.');
}
$this->sizing = Sizing::Flex;
diff --git a/src/Theme/DefaultTheme.php b/src/Theme/DefaultTheme.php
index 56b49da1..2ea887e7 100644
--- a/src/Theme/DefaultTheme.php
+++ b/src/Theme/DefaultTheme.php
@@ -4,6 +4,7 @@
namespace DrevOps\Tui\Theme;
+use DrevOps\Tui\FormException;
use DrevOps\Tui\Input\KeyName;
use DrevOps\Tui\Primitive\Element\PrimitiveElementsInterface;
use DrevOps\Tui\Primitive\Status;
@@ -170,7 +171,7 @@ public function __construct(int $width = ThemeInterface::DEFAULT_WIDTH, array $o
/**
* Validate the options against optionSchema(), failing loudly on a mistake.
*
- * @throws \InvalidArgumentException
+ * @throws \DrevOps\Tui\FormException
* When an option key is unknown or its value is not allowed.
*/
protected function validateOptions(): void {
@@ -180,7 +181,7 @@ protected function validateOptions(): void {
foreach ($this->options as $key => $value) {
if (in_array($key, $integers, TRUE)) {
if (!is_int($value) || $value < 0) {
- throw new \InvalidArgumentException(Translator::t('@value is not a valid "@key". Use a non-negative integer.', [
+ throw new FormException(Translator::t('@value is not a valid "@key". Use a non-negative integer.', [
'@value' => $this->showValue($value),
'@key' => $key,
]));
@@ -190,7 +191,7 @@ protected function validateOptions(): void {
}
if (!array_key_exists($key, $schema)) {
- throw new \InvalidArgumentException(Translator::t('Unknown theme option "@key". Known: @known.', [
+ throw new FormException(Translator::t('Unknown theme option "@key". Known: @known.', [
'@key' => $key,
'@known' => implode(', ', [...array_keys($schema), ...$integers]),
]));
@@ -200,7 +201,7 @@ protected function validateOptions(): void {
$candidate = $value instanceof \BackedEnum ? $value->value : $value;
if (!in_array($candidate, $schema[$key], TRUE)) {
- throw new \InvalidArgumentException(Translator::t('@value is not a valid "@key". Allowed: @allowed.', [
+ throw new FormException(Translator::t('@value is not a valid "@key". Allowed: @allowed.', [
'@value' => $this->showValue($candidate),
'@key' => $key,
'@allowed' => implode(', ', array_map($this->showValue(...), $schema[$key])),
@@ -213,7 +214,7 @@ protected function validateOptions(): void {
// unresolvable resize notice.
foreach ([['min_width', 'max_width'], ['min_height', 'max_height']] as [$min_key, $max_key]) {
if (array_key_exists($min_key, $this->options) && $this->intOption($max_key, 0) > 0 && $this->intOption($min_key, 0) > $this->intOption($max_key, 0)) {
- throw new \InvalidArgumentException(Translator::t('"@min" must not exceed "@max".', [
+ throw new FormException(Translator::t('"@min" must not exceed "@max".', [
'@min' => $min_key,
'@max' => $max_key,
]));
diff --git a/src/Translation/Translator.php b/src/Translation/Translator.php
index 8ffbb4de..8bba4152 100644
--- a/src/Translation/Translator.php
+++ b/src/Translation/Translator.php
@@ -4,6 +4,8 @@
namespace DrevOps\Tui\Translation;
+use DrevOps\Tui\FormException;
+
/**
* Resolves user-facing strings to a target language, English as the fallback.
*
@@ -350,6 +352,9 @@ protected function load(string $language): array {
*
* @return array
* The source => translation entries the source contributes.
+ *
+ * @throws \DrevOps\Tui\FormException
+ * When the source is neither a directory, file, nor array.
*/
protected function loadSource(string|array $source, array $candidates): array {
if (is_array($source)) {
@@ -380,7 +385,7 @@ protected function loadSource(string|array $source, array $candidates): array {
return in_array(pathinfo($source, PATHINFO_FILENAME), $candidates, TRUE) ? $this->readCatalog($source) : [];
}
- throw new \InvalidArgumentException(sprintf('The translation source "%s" is neither a directory nor a catalog file.', $source));
+ throw new FormException(sprintf('The translation source "%s" is neither a directory nor a catalog file.', $source));
}
/**
From 89b880eef69adead7982d993742ce29dd8d7a666 Mon Sep 17 00:00:00 2001
From: Alex Skrypnyk
Date: Tue, 11 Aug 2026 21:15:41 +1000
Subject: [PATCH 17/27] Settled the cursor noun on 'current', the enum suffix
on 'Type' and the move verb on 'moveCursor'.
---
src/Block/Field.php | 16 ++++-----
src/Block/Option.php | 8 ++---
src/Block/{OptionKind.php => OptionType.php} | 2 +-
src/Block/Prose.php | 12 +++----
src/Field/AbstractField.php | 4 +--
src/Field/Capability/OptionsCapableTrait.php | 8 ++---
src/Field/Matcher.php | 4 +--
src/Field/Reorder.php | 8 ++---
src/Field/Select.php | 4 +--
src/Field/Suggest.php | 2 +-
src/Field/Template.php | 28 +++++++--------
src/Schema/SchemaValidator.php | 2 +-
src/Screen/Collector.php | 2 +-
src/Screen/ScreenController.php | 4 +--
src/Terminal/Markup.php | 16 ++++-----
src/Terminal/MarkupSegment.php | 4 +--
.../{MarkupKind.php => MarkupType.php} | 2 +-
src/Theme/DefaultTheme.php | 12 +++----
tests/phpunit/Traits/MixedOptionsTrait.php | 8 ++---
tests/phpunit/Unit/Block/EntryTest.php | 36 +++++++++----------
tests/phpunit/Unit/Block/FieldBlockTest.php | 6 ++--
tests/phpunit/Unit/Builder/FormTest.php | 10 +++---
tests/phpunit/Unit/Field/MatcherTest.php | 6 ++--
tests/phpunit/Unit/Field/ReorderTest.php | 4 +--
tests/phpunit/Unit/Field/SelectTest.php | 6 ++--
tests/phpunit/Unit/Terminal/MarkupTest.php | 20 +++++------
26 files changed, 117 insertions(+), 117 deletions(-)
rename src/Block/{OptionKind.php => OptionType.php} (93%)
rename src/Terminal/{MarkupKind.php => MarkupType.php} (93%)
diff --git a/src/Block/Field.php b/src/Block/Field.php
index 495b6589..8b152684 100644
--- a/src/Block/Field.php
+++ b/src/Block/Field.php
@@ -687,7 +687,7 @@ public function acceptsValue(mixed $value): bool {
* @return string
* The fragment (e.g. "a string", "a list"), translated.
*/
- public function valueKind(): string {
+ public function valueType(): string {
return match (TRUE) {
$this->fieldType === FieldType::Confirm, $this->fieldType === FieldType::Pause => Translator::t('a boolean'),
$this->collectsList() => Translator::t('a list'),
@@ -943,12 +943,12 @@ public function hasSchemaDefault(): bool {
* The field.
*/
public function entry(string $value, string $label = '', string $description = '', bool $disabled = FALSE, string $disabled_reason = ''): static {
- $entry = new Option($value, $label === '' ? $value : $label, $description, OptionKind::Option, $disabled, $disabled_reason);
+ $entry = new Option($value, $label === '' ? $value : $label, $description, OptionType::Option, $disabled, $disabled_reason);
foreach ($this->entries as $index => $existing) {
// Option filters the value it is given, so the match is made against the
// filtered form rather than the argument.
- if ($existing->kind === OptionKind::Option && $existing->value === $entry->value) {
+ if ($existing->kind === OptionType::Option && $existing->value === $entry->value) {
$this->entries[$index] = $entry;
return $this;
@@ -970,7 +970,7 @@ public function entry(string $value, string $label = '', string $description = '
* The field.
*/
public function heading(string $label): static {
- $this->entries[] = new Option('', $label, '', OptionKind::Heading);
+ $this->entries[] = new Option('', $label, '', OptionType::Heading);
return $this;
}
@@ -982,7 +982,7 @@ public function heading(string $label): static {
* The field.
*/
public function separator(): static {
- $this->entries[] = new Option('', '', '', OptionKind::Separator);
+ $this->entries[] = new Option('', '', '', OptionType::Separator);
return $this;
}
@@ -1014,7 +1014,7 @@ public function entryOf(string $value): ?Option {
$value = Ansi::sanitize($value);
foreach ($this->entries as $entry) {
- if ($entry->kind === OptionKind::Option && $entry->value === $value) {
+ if ($entry->kind === OptionType::Option && $entry->value === $value) {
return $entry;
}
}
@@ -2347,11 +2347,11 @@ protected function valueRegion(ThemeInterface $theme, FieldElementsInterface $el
* The drawn entry; empty for a divider, which is a gap and nothing else.
*/
protected function entryLine(FieldElementsInterface $elements, Option $entry): string {
- if ($entry->kind === OptionKind::Heading) {
+ if ($entry->kind === OptionType::Heading) {
return $elements->fieldCaption($entry->label);
}
- if ($entry->kind === OptionKind::Separator) {
+ if ($entry->kind === OptionType::Separator) {
return $elements->fieldEntrySeparator();
}
diff --git a/src/Block/Option.php b/src/Block/Option.php
index 4d9da6fd..48029c33 100644
--- a/src/Block/Option.php
+++ b/src/Block/Option.php
@@ -9,7 +9,7 @@
/**
* A single row in a select, search or suggest option list.
*
- * A row is an Option, a Separator or a Heading (see {@see OptionKind}). Only
+ * A row is an Option, a Separator or a Heading (see {@see OptionType}). Only
* an Option row is selectable, and only when it is not disabled; Separator and
* Heading rows, and disabled Option rows, are visual structure that navigation
* skips and collection never returns.
@@ -52,7 +52,7 @@
* @param string $description
* The option's description. Shown for the highlighted option as a secondary
* line beneath the choice list, and carried into the machine schema.
- * @param \DrevOps\Tui\Block\OptionKind $kind
+ * @param \DrevOps\Tui\Block\OptionType $kind
* The row kind.
* @param bool $disabled
* Whether a selectable Option row is shown but cannot be selected.
@@ -63,7 +63,7 @@ public function __construct(
string $value,
string $label,
string $description = '',
- public OptionKind $kind = OptionKind::Option,
+ public OptionType $kind = OptionType::Option,
public bool $disabled = FALSE,
string $disabled_reason = '',
) {
@@ -81,7 +81,7 @@ public function __construct(
* disabled options.
*/
public function isSelectable(): bool {
- return $this->kind === OptionKind::Option && !$this->disabled;
+ return $this->kind === OptionType::Option && !$this->disabled;
}
/**
diff --git a/src/Block/OptionKind.php b/src/Block/OptionType.php
similarity index 93%
rename from src/Block/OptionKind.php
rename to src/Block/OptionType.php
index 1d9aa02e..ee0683e0 100644
--- a/src/Block/OptionKind.php
+++ b/src/Block/OptionType.php
@@ -12,7 +12,7 @@
*
* @package DrevOps\Tui\Block
*/
-enum OptionKind: string {
+enum OptionType: string {
case Option = 'option';
case Separator = 'separator';
diff --git a/src/Block/Prose.php b/src/Block/Prose.php
index e0ad5a0b..58a69714 100644
--- a/src/Block/Prose.php
+++ b/src/Block/Prose.php
@@ -6,7 +6,7 @@
use DrevOps\Tui\Block\Element\MarkupElementsInterface;
use DrevOps\Tui\Terminal\Markup as Parser;
-use DrevOps\Tui\Terminal\MarkupKind;
+use DrevOps\Tui\Terminal\MarkupType;
use DrevOps\Tui\Terminal\MarkupSegment;
use DrevOps\Tui\Theme\Capability\MarkdownCapableInterface;
@@ -74,13 +74,13 @@ public static function lines(string $source, MarkupElementsInterface $theme, ?\C
*/
protected static function span(MarkupSegment $segment, MarkupElementsInterface $theme, \Closure $plain): string {
return match ($segment->kind) {
- MarkupKind::Bold => $theme->markupStrong($segment->text),
- MarkupKind::Emphasis => $theme->markupEmphasis($segment->text),
- MarkupKind::Code => $theme->markupCode($segment->text),
- MarkupKind::Link => $theme->markupLink($segment->text, $segment->url),
+ MarkupType::Bold => $theme->markupStrong($segment->text),
+ MarkupType::Emphasis => $theme->markupEmphasis($segment->text),
+ MarkupType::Code => $theme->markupCode($segment->text),
+ MarkupType::Link => $theme->markupLink($segment->text, $segment->url),
// Every link is already a span of its own, so the styling left to do here
// is whatever the surrounding text is drawn in.
- MarkupKind::Text => $plain($segment->text),
+ MarkupType::Text => $plain($segment->text),
};
}
diff --git a/src/Field/AbstractField.php b/src/Field/AbstractField.php
index 174aa58c..159f94eb 100644
--- a/src/Field/AbstractField.php
+++ b/src/Field/AbstractField.php
@@ -325,7 +325,7 @@ protected function styleRun(ThemeInterface $theme, string $run, bool $matched, b
public function view(ThemeInterface $theme): string {
$lines = [$this->renderBody($theme)];
- $detail = $this->renderOptionDescription($theme, $this->highlightedDescription());
+ $detail = $this->renderOptionDescription($theme, $this->currentDescription());
if ($detail !== '') {
$lines[] = $detail;
}
@@ -376,7 +376,7 @@ abstract protected function renderBody(ThemeInterface $theme): string;
* @return string
* The description shown beneath the body, or an empty string.
*/
- protected function highlightedDescription(): string {
+ protected function currentDescription(): string {
return '';
}
diff --git a/src/Field/Capability/OptionsCapableTrait.php b/src/Field/Capability/OptionsCapableTrait.php
index 85a1403c..a03b7449 100644
--- a/src/Field/Capability/OptionsCapableTrait.php
+++ b/src/Field/Capability/OptionsCapableTrait.php
@@ -5,7 +5,7 @@
namespace DrevOps\Tui\Field\Capability;
use DrevOps\Tui\Block\Option;
-use DrevOps\Tui\Block\OptionKind;
+use DrevOps\Tui\Block\OptionType;
use DrevOps\Tui\Screen\Viewport;
use DrevOps\Tui\Theme\ThemeInterface;
@@ -150,7 +150,7 @@ protected function renderChoiceList(ThemeInterface $theme): string {
* @return string
* The highlighted option's description.
*/
- protected function highlightedDescription(): string {
+ protected function currentDescription(): string {
$option = $this->visible()[$this->cursor] ?? NULL;
return $option instanceof Option && $option->isSelectable() ? $option->description : '';
@@ -179,13 +179,13 @@ protected function renderListRows(ThemeInterface $theme, array $rows, Viewport $
$lines = [];
foreach (array_slice($rows, $viewport->offset, $this->pageSize) as $slot => $option) {
- if ($option->kind === OptionKind::Heading) {
+ if ($option->kind === OptionType::Heading) {
$lines[] = $this->renderHeadingRow($theme, $option);
continue;
}
- if ($option->kind === OptionKind::Separator) {
+ if ($option->kind === OptionType::Separator) {
$lines[] = $this->renderSeparatorRow($theme);
continue;
diff --git a/src/Field/Matcher.php b/src/Field/Matcher.php
index 70a0e063..ec79baae 100644
--- a/src/Field/Matcher.php
+++ b/src/Field/Matcher.php
@@ -5,7 +5,7 @@
namespace DrevOps\Tui\Field;
use DrevOps\Tui\Block\Option;
-use DrevOps\Tui\Block\OptionKind;
+use DrevOps\Tui\Block\OptionType;
use DrevOps\Tui\Utils\Strings;
/**
@@ -139,7 +139,7 @@ public function rankValues(array $values, string $needle): array {
* The matching options, most relevant first; ties keep their input order.
*/
public function rankOptions(array $options, string $needle): array {
- return $this->rank($options, static fn(Option $option): ?string => $option->kind === OptionKind::Option ? $option->label : NULL, $needle);
+ return $this->rank($options, static fn(Option $option): ?string => $option->kind === OptionType::Option ? $option->label : NULL, $needle);
}
/**
diff --git a/src/Field/Reorder.php b/src/Field/Reorder.php
index 03279904..7c826a26 100644
--- a/src/Field/Reorder.php
+++ b/src/Field/Reorder.php
@@ -111,13 +111,13 @@ public function handle(Key $key): void {
}
if ($keys->matches($key, Action::MoveUp)) {
- $this->move(-1);
+ $this->moveCursor(-1);
return;
}
if ($keys->matches($key, Action::MoveDown)) {
- $this->move(1);
+ $this->moveCursor(1);
}
}
@@ -127,7 +127,7 @@ public function handle(Key $key): void {
* @param int $delta
* The direction: -1 up, +1 down.
*/
- protected function move(int $delta): void {
+ protected function moveCursor(int $delta): void {
$target = $this->cursor + $delta;
if ($target < 0 || $target >= count($this->items)) {
@@ -183,7 +183,7 @@ protected function renderBody(ThemeInterface $theme): string {
* The highlighted item's description.
*/
#[\Override]
- protected function highlightedDescription(): string {
+ protected function currentDescription(): string {
if ($this->items === []) {
return '';
}
diff --git a/src/Field/Select.php b/src/Field/Select.php
index 1f59d04e..1ae67e88 100644
--- a/src/Field/Select.php
+++ b/src/Field/Select.php
@@ -6,7 +6,7 @@
use DrevOps\Tui\Block\FieldType;
use DrevOps\Tui\Block\Option;
-use DrevOps\Tui\Block\OptionKind;
+use DrevOps\Tui\Block\OptionType;
use DrevOps\Tui\Block\SelectionBounds;
use DrevOps\Tui\Field\Capability\FilterCapableInterface;
use DrevOps\Tui\Field\Capability\FilterCapableTrait;
@@ -81,7 +81,7 @@ protected function choiceType(): FieldType {
protected function filterOptions(string $needle): array {
$lower = Strings::lower($needle);
- return array_values(array_filter($this->options, static fn(Option $option): bool => $option->kind === OptionKind::Option && str_contains(Strings::lower($option->label), $lower)));
+ return array_values(array_filter($this->options, static fn(Option $option): bool => $option->kind === OptionType::Option && str_contains(Strings::lower($option->label), $lower)));
}
/**
diff --git a/src/Field/Suggest.php b/src/Field/Suggest.php
index 18926ed4..320a341b 100644
--- a/src/Field/Suggest.php
+++ b/src/Field/Suggest.php
@@ -325,7 +325,7 @@ protected function renderBody(ThemeInterface $theme): string {
* The highlighted suggestion's description.
*/
#[\Override]
- protected function highlightedDescription(): string {
+ protected function currentDescription(): string {
if ($this->cursor < 0) {
return '';
}
diff --git a/src/Field/Template.php b/src/Field/Template.php
index a9c8dd67..63520f55 100644
--- a/src/Field/Template.php
+++ b/src/Field/Template.php
@@ -47,7 +47,7 @@ class Template extends AbstractField implements TextEditCapableInterface {
/**
* The index of the slot holding the caret.
*/
- protected int $active = 0;
+ protected int $current = 0;
/**
* Construct a template field.
@@ -88,13 +88,13 @@ public function handle(Key $key): void {
}
if ($keys->matches($key, Action::MoveDown)) {
- $this->move(1);
+ $this->moveCursor(1);
return;
}
if ($keys->matches($key, Action::MoveUp)) {
- $this->move(-1);
+ $this->moveCursor(-1);
return;
}
@@ -133,7 +133,7 @@ public function hints(): array {
*/
protected function values(): array {
$values = $this->parts;
- $values[$this->activeName()] = $this->buffer;
+ $values[$this->currentName()] = $this->buffer;
return $values;
}
@@ -144,8 +144,8 @@ protected function values(): array {
* @return string
* The slot name.
*/
- protected function activeName(): string {
- return $this->names[$this->active] ?? '';
+ protected function currentName(): string {
+ return $this->names[$this->current] ?? '';
}
/**
@@ -158,12 +158,12 @@ protected function activeName(): string {
* @param int $delta
* The number of slots to move by: 1 forward, -1 back.
*/
- protected function move(int $delta): void {
+ protected function moveCursor(int $delta): void {
$count = count($this->names);
- $this->parts[$this->activeName()] = $this->buffer;
- $this->error = $this->template->partError($this->activeName(), $this->buffer);
+ $this->parts[$this->currentName()] = $this->buffer;
+ $this->error = $this->template->partError($this->currentName(), $this->buffer);
- $this->focus((($this->active + $delta) % $count + $count) % $count);
+ $this->focus((($this->current + $delta) % $count + $count) % $count);
}
/**
@@ -173,8 +173,8 @@ protected function move(int $delta): void {
* The slot index.
*/
protected function focus(int $index): void {
- $this->active = $index;
- $this->initTextBuffer($this->parts[$this->activeName()] ?? '');
+ $this->current = $index;
+ $this->initTextBuffer($this->parts[$this->currentName()] ?? '');
}
/**
@@ -242,7 +242,7 @@ protected function renderBody(ThemeInterface $theme): string {
$shape .= $this->renderLiteral($theme, count($this->names));
- return $shape . "\n" . $this->elements($theme)->fieldState(Translator::t('filling in @label', ['@label' => $this->template->labelOf($this->activeName())]));
+ return $shape . "\n" . $this->elements($theme)->fieldState(Translator::t('filling in @label', ['@label' => $this->template->labelOf($this->currentName())]));
}
/**
@@ -277,7 +277,7 @@ protected function renderLiteral(ThemeInterface $theme, int $index): string {
* The rendered slot.
*/
protected function renderSlot(ThemeInterface $theme, int $index, string $name): string {
- if ($index === $this->active) {
+ if ($index === $this->current) {
return $this->renderCaretLine($theme);
}
diff --git a/src/Schema/SchemaValidator.php b/src/Schema/SchemaValidator.php
index bbfeda32..b05c09ae 100644
--- a/src/Schema/SchemaValidator.php
+++ b/src/Schema/SchemaValidator.php
@@ -127,7 +127,7 @@ protected function validateValue(Field $field, mixed $value): ?string {
}
if (!$field->acceptsValue($value)) {
- return $this->constraintMessage($field, $field->valueKind());
+ return $this->constraintMessage($field, $field->valueType());
}
$bounds_error = $this->checkBounds($field, $value);
diff --git a/src/Screen/Collector.php b/src/Screen/Collector.php
index f1b01d31..1c66e6ce 100644
--- a/src/Screen/Collector.php
+++ b/src/Screen/Collector.php
@@ -913,7 +913,7 @@ protected function rejects(Field $field, mixed $value): ?string {
}
if (!$field->acceptsValue($value)) {
- return Translator::t('must be @constraint.', ['@constraint' => $field->valueKind()]);
+ return Translator::t('must be @constraint.', ['@constraint' => $field->valueType()]);
}
return $field->refuses($value, $this->handlers->validator($field->id()));
diff --git a/src/Screen/ScreenController.php b/src/Screen/ScreenController.php
index ce08ab8c..748e3831 100644
--- a/src/Screen/ScreenController.php
+++ b/src/Screen/ScreenController.php
@@ -15,7 +15,7 @@
use DrevOps\Tui\Block\Markup;
use DrevOps\Tui\Block\Mode;
use DrevOps\Tui\Block\Option;
-use DrevOps\Tui\Block\OptionKind;
+use DrevOps\Tui\Block\OptionType;
use DrevOps\Tui\Block\Panel;
use DrevOps\Tui\Block\Progress;
use DrevOps\Tui\Block\RenderMode;
@@ -1124,7 +1124,7 @@ protected function offered(Field $field, array $rows): array {
$offered = [];
foreach ([...$field->entries(), ...$rows] as $row) {
- if ($row->kind === OptionKind::Option) {
+ if ($row->kind === OptionType::Option) {
$offered[$row->value] = $row->label;
}
}
diff --git a/src/Terminal/Markup.php b/src/Terminal/Markup.php
index 76e3864f..3c149a8e 100644
--- a/src/Terminal/Markup.php
+++ b/src/Terminal/Markup.php
@@ -79,7 +79,7 @@ public static function links(string $text, bool $color): string {
$rendered = '';
foreach ($line->segments as $segment) {
- $rendered .= $segment->kind === MarkupKind::Link ? self::hyperlink($segment->text, $segment->url, $color) : $segment->text;
+ $rendered .= $segment->kind === MarkupType::Link ? self::hyperlink($segment->text, $segment->url, $color) : $segment->text;
}
$out[] = $rendered;
@@ -160,7 +160,7 @@ public static function width(string $source, bool $markdown, bool $color): int {
* The visible text.
*/
protected static function visible(MarkupSegment $segment, bool $color): string {
- if ($segment->kind !== MarkupKind::Link) {
+ if ($segment->kind !== MarkupType::Link) {
return $segment->text;
}
@@ -230,7 +230,7 @@ protected static function parseInline(string $text, bool $markdown): array {
// Close the run of plain text that led up to this span before it.
if ($index > $start) {
- $segments[] = new MarkupSegment(MarkupKind::Text, substr($text, $start, $index - $start));
+ $segments[] = new MarkupSegment(MarkupType::Text, substr($text, $start, $index - $start));
}
[$segment, $consumed] = $span;
@@ -240,7 +240,7 @@ protected static function parseInline(string $text, bool $markdown): array {
}
if ($index > $start) {
- $segments[] = new MarkupSegment(MarkupKind::Text, substr($text, $start));
+ $segments[] = new MarkupSegment(MarkupType::Text, substr($text, $start));
}
return $segments;
@@ -264,7 +264,7 @@ protected static function matchSpan(string $text, int $index, bool $markdown): ?
$char = $text[$index];
if ($markdown && $char === '`' && preg_match('/\G`([^`]+)`/', $text, $matches, 0, $index) === 1) {
- return [new MarkupSegment(MarkupKind::Code, $matches[1]), strlen($matches[0])];
+ return [new MarkupSegment(MarkupType::Code, $matches[1]), strlen($matches[0])];
}
if ($char === '[' && preg_match('/\G\[([^\]]*)\]\(([^)]+)\)/', $text, $matches, 0, $index) === 1 && self::looksLikeUrl($matches[2])) {
@@ -272,15 +272,15 @@ protected static function matchSpan(string $text, int $index, bool $markdown): ?
$label = Ansi::stripControl($matches[1]);
$url = Ansi::stripControl($matches[2]);
- return [new MarkupSegment(MarkupKind::Link, $label, $url), strlen($matches[0])];
+ return [new MarkupSegment(MarkupType::Link, $label, $url), strlen($matches[0])];
}
if ($markdown && $char === '*' && preg_match('/\G\*\*(\S(?:.*?\S)?)\*\*/', $text, $matches, 0, $index) === 1) {
- return [new MarkupSegment(MarkupKind::Bold, $matches[1]), strlen($matches[0])];
+ return [new MarkupSegment(MarkupType::Bold, $matches[1]), strlen($matches[0])];
}
if ($markdown && $char === '*' && preg_match('/\G\*([^\s*](?:[^*]*[^\s*])?)\*/', $text, $matches, 0, $index) === 1) {
- return [new MarkupSegment(MarkupKind::Emphasis, $matches[1]), strlen($matches[0])];
+ return [new MarkupSegment(MarkupType::Emphasis, $matches[1]), strlen($matches[0])];
}
return NULL;
diff --git a/src/Terminal/MarkupSegment.php b/src/Terminal/MarkupSegment.php
index f3b5bb33..16565343 100644
--- a/src/Terminal/MarkupSegment.php
+++ b/src/Terminal/MarkupSegment.php
@@ -14,7 +14,7 @@
/**
* Construct a segment.
*
- * @param \DrevOps\Tui\Terminal\MarkupKind $kind
+ * @param \DrevOps\Tui\Terminal\MarkupType $kind
* The span kind.
* @param string $text
* The visible text (the inner text of a styled span, or a link's label).
@@ -22,7 +22,7 @@
* The link target, for a Link span; empty otherwise.
*/
public function __construct(
- public MarkupKind $kind,
+ public MarkupType $kind,
public string $text,
public string $url = '',
) {
diff --git a/src/Terminal/MarkupKind.php b/src/Terminal/MarkupType.php
similarity index 93%
rename from src/Terminal/MarkupKind.php
rename to src/Terminal/MarkupType.php
index a52bf162..4eeebbcc 100644
--- a/src/Terminal/MarkupKind.php
+++ b/src/Terminal/MarkupType.php
@@ -9,7 +9,7 @@
*
* @package DrevOps\Tui\Terminal
*/
-enum MarkupKind {
+enum MarkupType {
case Text;
case Bold;
diff --git a/src/Theme/DefaultTheme.php b/src/Theme/DefaultTheme.php
index 2ea887e7..f6820668 100644
--- a/src/Theme/DefaultTheme.php
+++ b/src/Theme/DefaultTheme.php
@@ -11,7 +11,7 @@
use DrevOps\Tui\Terminal\Ansi;
use DrevOps\Tui\Terminal\Box;
use DrevOps\Tui\Terminal\Markup;
-use DrevOps\Tui\Terminal\MarkupKind;
+use DrevOps\Tui\Terminal\MarkupType;
use DrevOps\Tui\Terminal\MarkupSegment;
use DrevOps\Tui\Terminal\Table;
use DrevOps\Tui\Theme\Capability\ColorSchemeCapableInterface;
@@ -1841,13 +1841,13 @@ protected function markupBody(string $source): array {
*/
protected function markupSegment(MarkupSegment $segment): string {
return match ($segment->kind) {
- MarkupKind::Bold => $this->markupStrong($segment->text),
- MarkupKind::Emphasis => $this->markupEmphasis($segment->text),
- MarkupKind::Code => $this->markupCode($segment->text),
- MarkupKind::Link => $this->markupLink($segment->text, $segment->url),
+ MarkupType::Bold => $this->markupStrong($segment->text),
+ MarkupType::Emphasis => $this->markupEmphasis($segment->text),
+ MarkupType::Code => $this->markupCode($segment->text),
+ MarkupType::Link => $this->markupLink($segment->text, $segment->url),
// The parser has already split every link into its own span, so the
// element's own link resolution finds nothing left to do here.
- MarkupKind::Text => $this->markupLine($segment->text),
+ MarkupType::Text => $this->markupLine($segment->text),
};
}
diff --git a/tests/phpunit/Traits/MixedOptionsTrait.php b/tests/phpunit/Traits/MixedOptionsTrait.php
index 91293385..36c4c1ba 100644
--- a/tests/phpunit/Traits/MixedOptionsTrait.php
+++ b/tests/phpunit/Traits/MixedOptionsTrait.php
@@ -5,7 +5,7 @@
namespace DrevOps\Tui\Tests\Traits;
use DrevOps\Tui\Block\Option;
-use DrevOps\Tui\Block\OptionKind;
+use DrevOps\Tui\Block\OptionType;
/**
* Provides a choice-list fixture mixing every option kind.
@@ -22,10 +22,10 @@ trait MixedOptionsTrait {
protected function mixedOptions(): array {
return [
new Option('a', 'Apple'),
- new Option('', 'Fruits', '', OptionKind::Heading),
+ new Option('', 'Fruits', '', OptionType::Heading),
new Option('b', 'Banana'),
- new Option('', '', '', OptionKind::Separator),
- new Option('c', 'Cherry', '', OptionKind::Option, TRUE, 'out of stock'),
+ new Option('', '', '', OptionType::Separator),
+ new Option('c', 'Cherry', '', OptionType::Option, TRUE, 'out of stock'),
new Option('d', 'Date'),
];
}
diff --git a/tests/phpunit/Unit/Block/EntryTest.php b/tests/phpunit/Unit/Block/EntryTest.php
index 3d1d3ddc..8be95b4e 100644
--- a/tests/phpunit/Unit/Block/EntryTest.php
+++ b/tests/phpunit/Unit/Block/EntryTest.php
@@ -7,7 +7,7 @@
use DrevOps\Tui\Block\Field;
use DrevOps\Tui\Block\FieldType;
use DrevOps\Tui\Block\Option;
-use DrevOps\Tui\Block\OptionKind;
+use DrevOps\Tui\Block\OptionType;
use DrevOps\Tui\Builder\FieldBuilder;
use DrevOps\Tui\FormException;
use PHPUnit\Framework\Attributes\CoversClass;
@@ -19,7 +19,7 @@
* Tests the option model, option kinds and the entry helpers a field offers.
*/
#[CoversClass(Option::class)]
-#[CoversClass(OptionKind::class)]
+#[CoversClass(OptionType::class)]
#[CoversClass(FieldType::class)]
#[CoversClass(Field::class)]
#[CoversClass(FieldBuilder::class)]
@@ -32,7 +32,7 @@ public function testListFromMap(): void {
$this->assertCount(2, $options);
$this->assertSame('a', $options[0]->value);
$this->assertSame('Apple', $options[0]->label);
- $this->assertSame(OptionKind::Option, $options[0]->kind);
+ $this->assertSame(OptionType::Option, $options[0]->kind);
$this->assertTrue($options[0]->isSelectable());
}
@@ -43,7 +43,7 @@ public function testListLabelDefaultsToValue(): void {
}
public function testListFromOptionsPassesThrough(): void {
- $sep = new Option('', '', '', OptionKind::Separator);
+ $sep = new Option('', '', '', OptionType::Separator);
$options = Option::list([new Option('a', 'Apple'), $sep]);
$this->assertSame('Apple', $options[0]->label);
@@ -51,7 +51,7 @@ public function testListFromOptionsPassesThrough(): void {
}
public function testListMixed(): void {
- $options = Option::list(['a' => 'Apple', new Option('b', 'Banana', '', OptionKind::Option, TRUE, 'nope')]);
+ $options = Option::list(['a' => 'Apple', new Option('b', 'Banana', '', OptionType::Option, TRUE, 'nope')]);
$this->assertSame('a', $options[0]->value);
$this->assertTrue($options[1]->disabled);
@@ -65,9 +65,9 @@ public function testSelectable(Option $option, bool $expected): void {
public static function dataProviderSelectable(): \Iterator {
yield 'plain option' => [new Option('a', 'A'), TRUE];
- yield 'disabled option' => [new Option('a', 'A', '', OptionKind::Option, TRUE), FALSE];
- yield 'separator' => [new Option('', '', '', OptionKind::Separator), FALSE];
- yield 'heading' => [new Option('', 'Group', '', OptionKind::Heading), FALSE];
+ yield 'disabled option' => [new Option('a', 'A', '', OptionType::Option, TRUE), FALSE];
+ yield 'separator' => [new Option('', '', '', OptionType::Separator), FALSE];
+ yield 'heading' => [new Option('', 'Group', '', OptionType::Heading), FALSE];
}
#[DataProvider('dataProviderConstrainsToOptions')]
@@ -137,7 +137,7 @@ public static function dataProviderAcceptsValue(): \Iterator {
#[DataProvider('dataProviderValueKind')]
public function testValueKind(FieldType $type, bool $multiple, string $expected): void {
- $this->assertSame($expected, (new Field('f', 'F', $type))->multiple($multiple)->valueKind());
+ $this->assertSame($expected, (new Field('f', 'F', $type))->multiple($multiple)->valueType());
}
public static function dataProviderValueKind(): \Iterator {
@@ -203,9 +203,9 @@ public static function dataProviderOptionError(): \Iterator {
$options = [
new Option('standard', 'Standard'),
new Option('minimal', 'Minimal'),
- new Option('demo', 'Demo', '', OptionKind::Option, TRUE, 'unavailable'),
- new Option('legacy', 'Legacy', '', OptionKind::Option, TRUE),
- new Option('', '', '', OptionKind::Separator),
+ new Option('demo', 'Demo', '', OptionType::Option, TRUE, 'unavailable'),
+ new Option('legacy', 'Legacy', '', OptionType::Option, TRUE),
+ new Option('', '', '', OptionType::Separator),
];
yield 'selectable value' => [FieldType::Select, FALSE, $options, 'standard', NULL];
yield 'disabled with reason' => [FieldType::Select, FALSE, $options, 'demo', 'option "demo" is disabled: unavailable'];
@@ -259,10 +259,10 @@ public static function dataProviderCanonicalOrder(): \Iterator {
protected function selectField(): Field {
return self::offering(FieldType::Select, FALSE, [
new Option('standard', 'Standard'),
- new Option('', 'Group', '', OptionKind::Heading),
+ new Option('', 'Group', '', OptionType::Heading),
new Option('minimal', 'Minimal'),
- new Option('', '', '', OptionKind::Separator),
- new Option('demo', 'Demo', '', OptionKind::Option, TRUE, 'unavailable'),
+ new Option('', '', '', OptionType::Separator),
+ new Option('demo', 'Demo', '', OptionType::Option, TRUE, 'unavailable'),
]);
}
@@ -284,9 +284,9 @@ protected static function offering(FieldType $type, bool $multiple, array $optio
foreach ($options as $option) {
match ($option->kind) {
- OptionKind::Heading => $field->heading($option->label),
- OptionKind::Separator => $field->separator(),
- OptionKind::Option => $field->entry($option->value, $option->label, $option->description, $option->disabled, $option->disabledReason),
+ OptionType::Heading => $field->heading($option->label),
+ OptionType::Separator => $field->separator(),
+ OptionType::Option => $field->entry($option->value, $option->label, $option->description, $option->disabled, $option->disabledReason),
};
}
diff --git a/tests/phpunit/Unit/Block/FieldBlockTest.php b/tests/phpunit/Unit/Block/FieldBlockTest.php
index dae286c4..7d283ed5 100644
--- a/tests/phpunit/Unit/Block/FieldBlockTest.php
+++ b/tests/phpunit/Unit/Block/FieldBlockTest.php
@@ -12,7 +12,7 @@
use DrevOps\Tui\Block\Mode;
use DrevOps\Tui\Block\NumberBounds;
use DrevOps\Tui\Block\Option;
-use DrevOps\Tui\Block\OptionKind;
+use DrevOps\Tui\Block\OptionType;
use DrevOps\Tui\Block\RenderMode;
use DrevOps\Tui\Block\SelectionBounds;
use DrevOps\Tui\Block\Template;
@@ -564,9 +564,9 @@ public function testEntriesAreDrawnInTheOrderTheyWereDeclared(): void {
->heading('Vegetables')
->entry('carrot', 'Carrot');
- $kinds = array_map(static fn(object $entry): OptionKind => $entry->kind, $field->entries());
+ $kinds = array_map(static fn(object $entry): OptionType => $entry->kind, $field->entries());
- $expected = [OptionKind::Heading, OptionKind::Option, OptionKind::Separator, OptionKind::Heading, OptionKind::Option];
+ $expected = [OptionType::Heading, OptionType::Option, OptionType::Separator, OptionType::Heading, OptionType::Option];
$this->assertSame($expected, $kinds);
$this->assertSame(['apple', 'carrot'], $field->selectableValues());
}
diff --git a/tests/phpunit/Unit/Builder/FormTest.php b/tests/phpunit/Unit/Builder/FormTest.php
index e6c25b11..d8982521 100644
--- a/tests/phpunit/Unit/Builder/FormTest.php
+++ b/tests/phpunit/Unit/Builder/FormTest.php
@@ -10,7 +10,7 @@
use DrevOps\Tui\Block\FilePickerMode;
use DrevOps\Tui\Block\Markup;
use DrevOps\Tui\Block\NumberBounds;
-use DrevOps\Tui\Block\OptionKind;
+use DrevOps\Tui\Block\OptionType;
use DrevOps\Tui\Block\Panel;
use DrevOps\Tui\Block\Progress;
use DrevOps\Tui\Block\RenderMode;
@@ -642,7 +642,7 @@ public function testFilePickerOptions(): void {
$this->assertSame(FilePickerMode::Directory, $assets->pickerConstraints()->mode);
}
- public function testOptionKindsAndDisabled(): void {
+ public function testOptionTypesAndDisabled(): void {
$form = Form::create('T')
->panel('p', 'P', function (PanelBuilder $p): void {
$p->select('profile')
@@ -658,10 +658,10 @@ public function testOptionKindsAndDisabled(): void {
$options = $profile->entries();
$this->assertCount(4, $options);
- $this->assertSame(OptionKind::Heading, $options[0]->kind);
+ $this->assertSame(OptionType::Heading, $options[0]->kind);
$this->assertSame('Recommended', $options[0]->label);
- $this->assertSame(OptionKind::Option, $options[1]->kind);
- $this->assertSame(OptionKind::Separator, $options[2]->kind);
+ $this->assertSame(OptionType::Option, $options[1]->kind);
+ $this->assertSame(OptionType::Separator, $options[2]->kind);
$this->assertTrue($options[3]->disabled);
$this->assertSame('requires PHP 8.4', $options[3]->disabledReason);
diff --git a/tests/phpunit/Unit/Field/MatcherTest.php b/tests/phpunit/Unit/Field/MatcherTest.php
index 9fa21932..86e500f2 100644
--- a/tests/phpunit/Unit/Field/MatcherTest.php
+++ b/tests/phpunit/Unit/Field/MatcherTest.php
@@ -5,7 +5,7 @@
namespace DrevOps\Tui\Tests\Unit\Field;
use DrevOps\Tui\Block\Option;
-use DrevOps\Tui\Block\OptionKind;
+use DrevOps\Tui\Block\OptionType;
use DrevOps\Tui\Field\Matcher;
use DrevOps\Tui\Field\MatchResult;
use DrevOps\Tui\Field\MatchTier;
@@ -125,9 +125,9 @@ public function testRankOptionsRanksAndDropsStructuralRows(): void {
$options = [
new Option('a', 'Apple'),
- new Option('', 'Fruits', '', OptionKind::Heading),
+ new Option('', 'Fruits', '', OptionType::Heading),
new Option('p', 'Pineapple'),
- new Option('', '', '', OptionKind::Separator),
+ new Option('', '', '', OptionType::Separator),
new Option('g', 'Grape'),
];
diff --git a/tests/phpunit/Unit/Field/ReorderTest.php b/tests/phpunit/Unit/Field/ReorderTest.php
index 7e9b0781..8aab6b25 100644
--- a/tests/phpunit/Unit/Field/ReorderTest.php
+++ b/tests/phpunit/Unit/Field/ReorderTest.php
@@ -5,7 +5,7 @@
namespace DrevOps\Tui\Tests\Unit\Field;
use DrevOps\Tui\Block\Option;
-use DrevOps\Tui\Block\OptionKind;
+use DrevOps\Tui\Block\OptionType;
use DrevOps\Tui\Field\AbstractField;
use DrevOps\Tui\Field\Capability\PagingCapableTrait;
use DrevOps\Tui\Field\Reorder;
@@ -52,7 +52,7 @@ public function testNonSelectableItemShowsNoDescription(): void {
// The cursor starts on the non-selectable heading, so its description
// never renders beneath the list.
$field = new Reorder([
- new Option('', 'Group', 'group note', OptionKind::Heading),
+ new Option('', 'Group', 'group note', OptionType::Heading),
new Option('a', 'Apple', 'Crisp and sweet.'),
]);
diff --git a/tests/phpunit/Unit/Field/SelectTest.php b/tests/phpunit/Unit/Field/SelectTest.php
index 70921d04..a46361ca 100644
--- a/tests/phpunit/Unit/Field/SelectTest.php
+++ b/tests/phpunit/Unit/Field/SelectTest.php
@@ -6,7 +6,7 @@
use DrevOps\Tui\Block\FieldType;
use DrevOps\Tui\Block\Option;
-use DrevOps\Tui\Block\OptionKind;
+use DrevOps\Tui\Block\OptionType;
use DrevOps\Tui\Block\SelectionBounds;
use DrevOps\Tui\Field\AbstractField;
use DrevOps\Tui\Field\Capability\FilterCapableTrait;
@@ -209,13 +209,13 @@ public function testOmitsDescriptionWhenPanelTooNarrow(): void {
public function testNonSelectableRowDescriptionNeverShows(): void {
// With no selectable option the cursor parks on the heading; its
// description must not render as an option description.
- $field = new Select([new Option('', 'Fruit', 'group note', OptionKind::Heading)]);
+ $field = new Select([new Option('', 'Fruit', 'group note', OptionType::Heading)]);
$this->assertStringNotContainsString('group note', Ansi::strip($field->view(new DefaultTheme())));
}
public function testNoSelectableRowYieldsNoValue(): void {
- $field = new Select([new Option('', 'Group', '', OptionKind::Heading)]);
+ $field = new Select([new Option('', 'Group', '', OptionType::Heading)]);
$field->handle(Key::named(KeyName::Enter));
diff --git a/tests/phpunit/Unit/Terminal/MarkupTest.php b/tests/phpunit/Unit/Terminal/MarkupTest.php
index a75d1adc..fb6e2bf0 100644
--- a/tests/phpunit/Unit/Terminal/MarkupTest.php
+++ b/tests/phpunit/Unit/Terminal/MarkupTest.php
@@ -6,7 +6,7 @@
use DrevOps\Tui\Terminal\Ansi;
use DrevOps\Tui\Terminal\Markup;
-use DrevOps\Tui\Terminal\MarkupKind;
+use DrevOps\Tui\Terminal\MarkupType;
use DrevOps\Tui\Terminal\MarkupSegment;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
@@ -26,7 +26,7 @@ public function testParsePlainText(): void {
$this->assertCount(1, $lines);
$this->assertFalse($lines[0]->bullet);
- $this->assertEquals([new MarkupSegment(MarkupKind::Text, 'just text')], $lines[0]->segments);
+ $this->assertEquals([new MarkupSegment(MarkupType::Text, 'just text')], $lines[0]->segments);
}
public function testParseSplitsPhysicalLines(): void {
@@ -43,9 +43,9 @@ public function testParseLinkInBothModes(): void {
$segments = $lines[0]->segments;
$this->assertCount(3, $segments);
- $this->assertSame(MarkupKind::Text, $segments[0]->kind);
+ $this->assertSame(MarkupType::Text, $segments[0]->kind);
$this->assertSame('see ', $segments[0]->text);
- $this->assertSame(MarkupKind::Link, $segments[1]->kind);
+ $this->assertSame(MarkupType::Link, $segments[1]->kind);
$this->assertSame('Orchard', $segments[1]->text);
$this->assertSame('https://example.com/orchard', $segments[1]->url);
$this->assertSame(' now', $segments[2]->text);
@@ -56,7 +56,7 @@ public function testParseLeavesNonUrlBracketsLiteral(): void {
$lines = Markup::parse('pack it [see step 3](later)', TRUE);
$this->assertCount(1, $lines[0]->segments);
- $this->assertSame(MarkupKind::Text, $lines[0]->segments[0]->kind);
+ $this->assertSame(MarkupType::Text, $lines[0]->segments[0]->kind);
$this->assertSame('pack it [see step 3](later)', $lines[0]->segments[0]->text);
}
@@ -65,7 +65,7 @@ public function testParseMarkdownMarkersAreLiteralWhenOff(string $source): void
$lines = Markup::parse($source, FALSE);
$this->assertCount(1, $lines[0]->segments);
- $this->assertSame(MarkupKind::Text, $lines[0]->segments[0]->kind);
+ $this->assertSame(MarkupType::Text, $lines[0]->segments[0]->kind);
$this->assertSame($source, $lines[0]->segments[0]->text);
}
@@ -79,14 +79,14 @@ public static function dataProviderParseMarkdownMarkersAreLiteralWhenOff(): \Ite
public function testParseBold(): void {
$segments = Markup::parse('a **two words** b', TRUE)[0]->segments;
- $this->assertSame(MarkupKind::Bold, $segments[1]->kind);
+ $this->assertSame(MarkupType::Bold, $segments[1]->kind);
$this->assertSame('two words', $segments[1]->text);
}
public function testParseEmphasis(): void {
$segments = Markup::parse('a *ripe* pear', TRUE)[0]->segments;
- $this->assertSame(MarkupKind::Emphasis, $segments[1]->kind);
+ $this->assertSame(MarkupType::Emphasis, $segments[1]->kind);
$this->assertSame('ripe', $segments[1]->text);
}
@@ -100,7 +100,7 @@ public function testParseEmphasisIgnoresSpacedAsterisks(): void {
public function testParseInlineCode(): void {
$segments = Markup::parse('run `harvest` today', TRUE)[0]->segments;
- $this->assertSame(MarkupKind::Code, $segments[1]->kind);
+ $this->assertSame(MarkupType::Code, $segments[1]->kind);
$this->assertSame('harvest', $segments[1]->text);
}
@@ -117,7 +117,7 @@ public function testParseBulletKeepsInlineMarkup(): void {
$line = Markup::parse('* **Ripe** fruit', TRUE)[0];
$this->assertTrue($line->bullet);
- $this->assertSame(MarkupKind::Bold, $line->segments[0]->kind);
+ $this->assertSame(MarkupType::Bold, $line->segments[0]->kind);
$this->assertSame('Ripe', $line->segments[0]->text);
}
From bcd6250c2469b81e392409db4beca6023d3d7614 Mon Sep 17 00:00:00 2001
From: Alex Skrypnyk
Date: Tue, 11 Aug 2026 21:29:57 +1000
Subject: [PATCH 18/27] Converged the choice-list row vocabulary on 'option',
including the theme elements.
---
playground/themes/OceanTheme.php | 2 +-
src/Block/Element/FieldElementsInterface.php | 54 +++++++++----------
src/Block/Field.php | 24 ++++-----
src/Builder/FieldBuilder.php | 2 +-
src/Builder/Form.php | 6 +--
src/Field/AbstractField.php | 42 +++++++--------
src/Field/Calendar.php | 4 +-
src/Field/Capability/OptionsCapableTrait.php | 4 +-
.../Capability/SelectionCapableTrait.php | 14 ++---
src/Field/FieldFactory.php | 10 ++--
src/Field/FilePicker.php | 12 ++---
src/Field/Pause.php | 2 +-
src/Field/Reorder.php | 4 +-
src/Field/Suggest.php | 2 +-
src/Schema/SchemaGenerator.php | 2 +-
src/Schema/SchemaValidator.php | 2 +-
src/Screen/Collector.php | 12 ++---
src/Screen/ScreenController.php | 2 +-
src/Theme/AbstractTheme.php | 14 ++---
src/Theme/DefaultTheme.php | 18 +++----
src/Theme/DosTheme.php | 2 +-
src/Theme/EmberTheme.php | 2 +-
src/Theme/FrostTheme.php | 2 +-
src/Theme/MidnightTheme.php | 2 +-
src/Theme/MonoTheme.php | 2 +-
src/Theme/Override/FieldOverrides.php | 4 +-
src/Theme/Override/ThemeElement.php | 4 +-
tests/phpunit/Fixtures/Theme/CapableTheme.php | 2 +-
tests/phpunit/Unit/Block/FieldBlockTest.php | 30 +++++------
.../Unit/Block/FieldDeclarationTest.php | 4 +-
.../Block/{EntryTest.php => OptionTest.php} | 6 +--
tests/phpunit/Unit/Builder/FormTest.php | 14 ++---
tests/phpunit/Unit/ControlBytesTest.php | 8 +--
tests/phpunit/Unit/Field/CalendarTest.php | 6 +--
tests/phpunit/Unit/Field/SearchTest.php | 6 +--
tests/phpunit/Unit/Field/SelectTest.php | 4 +-
tests/phpunit/Unit/Field/SuggestTest.php | 2 +-
.../phpunit/Unit/Theme/AbstractThemeTest.php | 26 ++++-----
.../phpunit/Unit/Theme/BuiltinThemesTest.php | 6 +--
.../Unit/Theme/ElementDelegationTest.php | 10 ++--
tests/phpunit/Unit/Theme/SupportTest.php | 4 +-
tests/phpunit/Unit/Theme/ThemeBuilderTest.php | 24 ++++-----
tests/phpunit/Unit/Theme/ThemeTest.php | 30 +++++------
43 files changed, 216 insertions(+), 216 deletions(-)
rename tests/phpunit/Unit/Block/{EntryTest.php => OptionTest.php} (98%)
diff --git a/playground/themes/OceanTheme.php b/playground/themes/OceanTheme.php
index a7394ec7..a33ff5b3 100644
--- a/playground/themes/OceanTheme.php
+++ b/playground/themes/OceanTheme.php
@@ -132,7 +132,7 @@ public function fieldCaret(): string {
* {@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() ? '◯' : '( )');
}
diff --git a/src/Block/Element/FieldElementsInterface.php b/src/Block/Element/FieldElementsInterface.php
index 9559c75c..3abfb92a 100644
--- a/src/Block/Element/FieldElementsInterface.php
+++ b/src/Block/Element/FieldElementsInterface.php
@@ -103,27 +103,27 @@ public function fieldBadge(string $text): string;
public function fieldDescription(string $text): string;
/**
- * Style one entry of a list a field opens onto.
+ * Style one option of a list a field opens onto.
*
- * Being picked and being where the cursor rests are two different facts about
- * an entry, and an entry can be either, both or neither: a settled row draws
- * what was picked with no cursor anywhere in it, and a cursor moving down an
- * open list picks nothing.
+ * Being picked and being where the cursor rests are two different facts
+ * about an option, and an option can be either, both or neither: a settled
+ * row draws what was picked with no cursor anywhere in it, and a cursor
+ * moving down an open list picks nothing.
*
* @param string $text
- * The entry label.
+ * The option label.
* @param bool $chosen
* Whether it is picked.
* @param bool $focused
* Whether the cursor rests on it.
*
* @return string
- * The styled entry.
+ * The styled option.
*/
- public function fieldEntry(string $text, bool $chosen, bool $focused = FALSE): string;
+ public function fieldOption(string $text, bool $chosen, bool $focused = FALSE): string;
/**
- * Style the run of an entry's label that answers what was typed.
+ * Style the run of an option's label that answers what was typed.
*
* @param string $text
* The matched run.
@@ -131,45 +131,45 @@ public function fieldEntry(string $text, bool $chosen, bool $focused = FALSE): s
* @return string
* The styled run.
*/
- public function fieldEntryMatch(string $text): string;
+ public function fieldOptionMatch(string $text): string;
/**
- * The mark saying which entry has focus.
+ * The mark saying which option has focus.
*
* Its own element rather than a reuse of the field selector: one says which
- * field you are on and the other which entry within it, so a theme can
+ * field you are on and the other which option within it, so a theme can
* restyle either without touching the other.
*
* @param bool $selected
- * Whether this is the entry the cursor is on.
+ * Whether this is the option the cursor is on.
*
* @return string
* The styled mark, or the gap standing in its place.
*/
- public function fieldEntrySelector(bool $selected): string;
+ public function fieldOptionSelector(bool $selected): string;
/**
- * The mark recording that an entry was picked.
+ * The mark recording that an option was picked.
*
* Selecting and marking are different things: a selector says where you are,
- * and this says what you decided. Whether picking one entry unpicks the last
+ * and this says what you decided. Whether picking one option unpicks the last
* is a fact about the question rather than about the mark, so the mark is
* told - a question that takes one answer and a question that takes several
* are worth telling apart before anything has been picked at all.
*
* @param bool $chosen
- * Whether the entry is picked.
+ * Whether the option is picked.
* @param bool $exclusive
- * Whether picking this entry gives up every other, rather than adding to
+ * Whether picking this option gives up every other, rather than adding to
* what is already picked.
*
* @return string
* The styled mark.
*/
- public function fieldEntryMarker(bool $chosen, bool $exclusive = FALSE): string;
+ public function fieldOptionMarker(bool $chosen, bool $exclusive = FALSE): string;
/**
- * Style a qualifier on an entry, such as why it is unavailable.
+ * Style a qualifier on an option, such as why it is unavailable.
*
* @param string $text
* The note.
@@ -177,10 +177,10 @@ public function fieldEntryMarker(bool $chosen, bool $exclusive = FALSE): string;
* @return string
* The styled note.
*/
- public function fieldEntryNote(string $text): string;
+ public function fieldOptionNote(string $text): string;
/**
- * Style the focused entry's own explanatory text.
+ * Style the focused option's own explanatory text.
*
* @param string $text
* The description.
@@ -188,15 +188,15 @@ public function fieldEntryNote(string $text): string;
* @return string
* The styled description.
*/
- public function fieldEntryDescription(string $text): string;
+ public function fieldOptionDescription(string $text): string;
/**
- * The mark standing between two runs of entries.
+ * The mark standing between two runs of options.
*
* @return string
* The styled mark.
*/
- public function fieldEntrySeparator(): string;
+ public function fieldOptionSeparator(): string;
/**
* The mark saying a list runs past the page it is windowed to.
@@ -206,7 +206,7 @@ public function fieldEntrySeparator(): string;
* was given, so a theme can restyle either without touching the other.
*
* @param bool $above
- * Whether the entries it points at are above rather than below.
+ * Whether the options it points at are above rather than below.
*
* @return string
* The styled mark.
@@ -217,7 +217,7 @@ public function fieldOverflowMarker(bool $above): string;
* Style what a field will accept, before anything is refused.
*
* The guidance voice, and the one line that has to survive a surface with
- * nothing to spend: it sits directly under an entry's own explanatory text,
+ * nothing to spend: it sits directly under an option's own explanatory text,
* so with no colour and no dependable italic it needs a mark of its own or a
* reader cannot tell an expectation from prose.
*
diff --git a/src/Block/Field.php b/src/Block/Field.php
index 8b152684..cc5fc5a0 100644
--- a/src/Block/Field.php
+++ b/src/Block/Field.php
@@ -993,7 +993,7 @@ public function separator(): static {
* @return list<\DrevOps\Tui\Block\Option>
* The rows, in the order they were declared.
*/
- public function entries(): array {
+ public function options(): array {
return $this->entries;
}
@@ -1010,7 +1010,7 @@ public function entries(): array {
* The entry, or NULL when nothing carries that value. Headings and
* separators carry none and are never returned.
*/
- public function entryOf(string $value): ?Option {
+ public function optionOf(string $value): ?Option {
$value = Ansi::sanitize($value);
foreach ($this->entries as $entry) {
@@ -1173,7 +1173,7 @@ public function settle(mixed $entries): static {
* FALSE while a loader, a resolver or a query source still owes them, so
* there is nothing yet to count or to check a value against.
*/
- public function hasSettledEntries(): bool {
+ public function hasSettledOptions(): bool {
return !$this->loader instanceof \Closure && !$this->resolver instanceof \Closure && !$this->source instanceof \Closure;
}
@@ -1464,7 +1464,7 @@ public function ratingCaptions(): array {
* The fragment, or NULL when nothing constrains the value or every item is
* among the entries.
*/
- public function entryViolation(mixed $value): ?string {
+ public function optionViolation(mixed $value): ?string {
// A field that declares no entries constrains nothing - but one whose
// entries follow a query or the answers is constrained by whatever they
// resolved to, and resolving to nothing means the value does not exist.
@@ -1938,7 +1938,7 @@ public function refuses(mixed $value, ?\Closure $reusable = NULL): ?string {
// A validator that answers with nothing has not said why, and a refusal
// nobody can read is no refusal at all.
- return is_string($refusal) && $refusal !== '' ? $refusal : $this->entryViolation($value);
+ return is_string($refusal) && $refusal !== '' ? $refusal : $this->optionViolation($value);
}
/**
@@ -2067,7 +2067,7 @@ protected function scalarEntryViolation(string $value): ?string {
return Translator::t('value "@value" was not found', ['@value' => $value]);
}
- $entry = $this->entryOf($value);
+ $entry = $this->optionOf($value);
if ($entry instanceof Option && $entry->disabled) {
if ($entry->disabledReason === '') {
@@ -2332,7 +2332,7 @@ protected function valueRegion(ThemeInterface $theme, FieldElementsInterface $el
return [$elements->fieldValue($this->readable($theme, $elements))];
}
- return array_map(fn(Option $entry): string => $this->entryLine($elements, $entry), $this->entries);
+ return array_map(fn(Option $entry): string => $this->optionLine($elements, $entry), $this->entries);
}
/**
@@ -2346,23 +2346,23 @@ protected function valueRegion(ThemeInterface $theme, FieldElementsInterface $el
* @return string
* The drawn entry; empty for a divider, which is a gap and nothing else.
*/
- protected function entryLine(FieldElementsInterface $elements, Option $entry): string {
+ protected function optionLine(FieldElementsInterface $elements, Option $entry): string {
if ($entry->kind === OptionType::Heading) {
return $elements->fieldCaption($entry->label);
}
if ($entry->kind === OptionType::Separator) {
- return $elements->fieldEntrySeparator();
+ return $elements->fieldOptionSeparator();
}
// Marking and naming are two elements: the mark records what was picked
// and the text says what it was, so a theme can restyle either alone.
$chosen = $this->isChosen($entry);
- $line = $elements->fieldEntryMarker($chosen, !$this->multiple) . ' ' . $elements->fieldEntry($entry->label, $chosen);
+ $line = $elements->fieldOptionMarker($chosen, !$this->multiple) . ' ' . $elements->fieldOption($entry->label, $chosen);
- // Why an entry cannot be picked belongs beside it, or a row that is drawn
+ // Why an option cannot be picked belongs beside it, or a row that is drawn
// and refuses the cursor reads as a fault rather than a decision.
- return $entry->disabled && $entry->disabledReason !== '' ? $line . ' ' . $elements->fieldEntryNote($entry->disabledReason) : $line;
+ return $entry->disabled && $entry->disabledReason !== '' ? $line . ' ' . $elements->fieldOptionNote($entry->disabledReason) : $line;
}
/**
diff --git a/src/Builder/FieldBuilder.php b/src/Builder/FieldBuilder.php
index 17fa6218..a2fca0db 100644
--- a/src/Builder/FieldBuilder.php
+++ b/src/Builder/FieldBuilder.php
@@ -1269,7 +1269,7 @@ protected function resolveDefault(): mixed {
// option's value rather than an empty value that would not match either.
// The value is read off the option, not its array key, so a numeric-string
// value like "0" is not coerced to an int.
- $entries = $this->block->entries();
+ $entries = $this->block->options();
if ($this->fieldType === FieldType::Toggle && $entries !== []) {
return reset($entries)->value;
diff --git a/src/Builder/Form.php b/src/Builder/Form.php
index 20462eec..6360e422 100644
--- a/src/Builder/Form.php
+++ b/src/Builder/Form.php
@@ -489,7 +489,7 @@ protected function assertFieldSurfaces(Panel $root): void {
protected function assertEntrySources(Panel $root): void {
foreach (Tree::fields($root) as $field) {
$offered = [
- $field->entries() !== [],
+ $field->options() !== [],
$field->loader() instanceof \Closure,
$field->resolver() instanceof \Closure,
$field->source() instanceof \Closure,
@@ -529,7 +529,7 @@ protected function assertToggleEntries(Panel $root): void {
continue;
}
- $entries = $field->entries();
+ $entries = $field->options();
if (count($entries) !== 2) {
throw new FormException(sprintf('Toggle field "%s" must have exactly two options, %d given.', $field->id(), count($entries)));
@@ -571,7 +571,7 @@ protected function assertReorderEntries(Panel $root): void {
continue;
}
- $entries = $field->entries();
+ $entries = $field->options();
foreach ($entries as $entry) {
if (!$entry->isSelectable()) {
diff --git a/src/Field/AbstractField.php b/src/Field/AbstractField.php
index 159f94eb..5d39b93c 100644
--- a/src/Field/AbstractField.php
+++ b/src/Field/AbstractField.php
@@ -193,33 +193,33 @@ protected function elements(ThemeInterface $theme): FieldElementsInterface {
}
/**
- * Style one entry's label.
+ * Style one option's label.
*
* @param \DrevOps\Tui\Theme\ThemeInterface $theme
* The theme.
* @param string $label
- * The entry label.
+ * The option label.
* @param bool $current
- * Whether the entry's row holds the cursor.
+ * Whether the option's row holds the cursor.
* @param bool $chosen
- * Whether the entry is picked.
+ * Whether the option is picked.
*
* @return string
* The styled label.
*/
- protected function entryLabel(ThemeInterface $theme, string $label, bool $current, bool $chosen = FALSE): string {
- return $this->elements($theme)->fieldEntry($label, $chosen, $current);
+ protected function optionLabel(ThemeInterface $theme, string $label, bool $current, bool $chosen = FALSE): string {
+ return $this->elements($theme)->fieldOption($label, $chosen, $current);
}
/**
- * Render an exclusive entry row: the mark and the label beside it.
+ * Render an exclusive option row: the mark and the label beside it.
*
* @param \DrevOps\Tui\Theme\ThemeInterface $theme
* The theme.
* @param string $label
- * The entry label.
+ * The option label.
* @param bool $current
- * Whether the entry's row holds the cursor.
+ * Whether the option's row holds the cursor.
*
* @return string
* The rendered row.
@@ -227,7 +227,7 @@ protected function entryLabel(ThemeInterface $theme, string $label, bool $curren
protected function renderExclusiveRow(ThemeInterface $theme, string $label, bool $current): string {
// Moving the cursor picks in an exclusive list, so the mark and the
// cursor state coincide and the row draws only the mark.
- return $this->elements($theme)->fieldEntryMarker($current, TRUE) . ' ' . $this->entryLabel($theme, $label, $current);
+ return $this->elements($theme)->fieldOptionMarker($current, TRUE) . ' ' . $this->optionLabel($theme, $label, $current);
}
/**
@@ -245,7 +245,7 @@ protected function matcher(): Matcher {
*
* The label is split into runs of matched and unmatched characters, each run
* styled on its own, so no SGR code nests inside another. With no matched
- * positions this is exactly {@see entryLabel()}.
+ * positions this is exactly {@see optionLabel()}.
*
* @param \DrevOps\Tui\Theme\ThemeInterface $theme
* The theme.
@@ -263,7 +263,7 @@ protected function matcher(): Matcher {
*/
protected function renderMatchedLabel(ThemeInterface $theme, string $label, array $positions, bool $current, bool $chosen = FALSE): string {
if ($positions === []) {
- return $this->entryLabel($theme, $label, $current, $chosen);
+ return $this->optionLabel($theme, $label, $current, $chosen);
}
$matched = array_fill_keys($positions, TRUE);
@@ -305,10 +305,10 @@ protected function renderMatchedLabel(ThemeInterface $theme, string $label, arra
*/
protected function styleRun(ThemeInterface $theme, string $run, bool $matched, bool $current, bool $chosen): string {
if ($matched) {
- return $this->elements($theme)->fieldEntryMatch($run);
+ return $this->elements($theme)->fieldOptionMatch($run);
}
- return $this->entryLabel($theme, $run, $current, $chosen);
+ return $this->optionLabel($theme, $run, $current, $chosen);
}
/**
@@ -401,9 +401,9 @@ protected function currentDescription(): string {
* description or the panel is too narrow to show one.
*/
protected function renderOptionDescription(ThemeInterface $theme, string $description): string {
- // Indent to the column where an entry's own text starts, so the
- // description aligns with the entry above it.
- $indent = str_repeat(' ', $this->entryTextOffset($theme));
+ // Indent to the column where an option's own text starts, so the
+ // description aligns with the option above it.
+ $indent = str_repeat(' ', $this->optionTextOffset($theme));
$width = $theme->contentWidth() - Strings::length($indent);
if ($description === '' || $width < self::MIN_DESCRIPTION_WIDTH) {
@@ -412,19 +412,19 @@ protected function renderOptionDescription(ThemeInterface $theme, string $descri
$elements = $this->elements($theme);
- return implode("\n", array_map(static fn(string $line): string => $indent . $elements->fieldEntryDescription($line), Strings::wrap($description, $width)));
+ return implode("\n", array_map(static fn(string $line): string => $indent . $elements->fieldOptionDescription($line), Strings::wrap($description, $width)));
}
/**
- * The column an entry's own text starts at, within the field's view.
+ * The column an option's own text starts at, within the field's view.
*
* @param \DrevOps\Tui\Theme\ThemeInterface $theme
* The theme.
*
* @return int
- * The offset; zero for a field whose entries carry no leading glyphs.
+ * The offset; zero for a field whose options carry no leading glyphs.
*/
- protected function entryTextOffset(ThemeInterface $theme): int {
+ protected function optionTextOffset(ThemeInterface $theme): int {
return 0;
}
diff --git a/src/Field/Calendar.php b/src/Field/Calendar.php
index 8fd9da02..6e45a310 100644
--- a/src/Field/Calendar.php
+++ b/src/Field/Calendar.php
@@ -263,12 +263,12 @@ protected function weekRows(ThemeInterface $theme): array {
*/
protected function dayCell(ThemeInterface $theme, \DateTimeImmutable $date, int $day): string {
if ($date->format('Y-m-d') === $this->cursor->format('Y-m-d')) {
- return $this->entryLabel($theme, sprintf('[%2d]', $day), TRUE);
+ return $this->optionLabel($theme, sprintf('[%2d]', $day), TRUE);
}
$cell = sprintf(' %2d ', $day);
- return $this->bounds->contains($date) ? $cell : $this->elements($theme)->fieldEntryNote($cell);
+ return $this->bounds->contains($date) ? $cell : $this->elements($theme)->fieldOptionNote($cell);
}
}
diff --git a/src/Field/Capability/OptionsCapableTrait.php b/src/Field/Capability/OptionsCapableTrait.php
index a03b7449..596515b6 100644
--- a/src/Field/Capability/OptionsCapableTrait.php
+++ b/src/Field/Capability/OptionsCapableTrait.php
@@ -222,7 +222,7 @@ protected function renderHeadingRow(ThemeInterface $theme, Option $option): stri
* The rendered row.
*/
protected function renderSeparatorRow(ThemeInterface $theme): string {
- return $this->elements($theme)->fieldEntrySeparator();
+ return $this->elements($theme)->fieldOptionSeparator();
}
/**
@@ -243,7 +243,7 @@ protected function renderDisabledLabel(ThemeInterface $theme, Option $option): s
$text .= ' (' . $option->disabledReason . ')';
}
- return $this->elements($theme)->fieldEntryNote($text);
+ return $this->elements($theme)->fieldOptionNote($text);
}
}
diff --git a/src/Field/Capability/SelectionCapableTrait.php b/src/Field/Capability/SelectionCapableTrait.php
index dee99729..d746132c 100644
--- a/src/Field/Capability/SelectionCapableTrait.php
+++ b/src/Field/Capability/SelectionCapableTrait.php
@@ -319,21 +319,21 @@ public function renderOptionRow(ThemeInterface $theme, Option $option, bool $cur
if ($this->multiple) {
if ($option->disabled) {
- return $elements->fieldEntrySelector(FALSE) . ' ' . $elements->fieldEntryMarker(FALSE) . ' ' . $this->renderDisabledLabel($theme, $option);
+ return $elements->fieldOptionSelector(FALSE) . ' ' . $elements->fieldOptionMarker(FALSE) . ' ' . $this->renderDisabledLabel($theme, $option);
}
$chosen = isset($this->selected[$option->value]);
- return $elements->fieldEntrySelector($current) . ' ' . $elements->fieldEntryMarker($chosen) . ' ' . $this->renderMatchedLabel($theme, $option->label, $this->matchPositions($option->label), $current, $chosen);
+ return $elements->fieldOptionSelector($current) . ' ' . $elements->fieldOptionMarker($chosen) . ' ' . $this->renderMatchedLabel($theme, $option->label, $this->matchPositions($option->label), $current, $chosen);
}
if ($option->disabled) {
- return $elements->fieldEntryMarker(FALSE, TRUE) . ' ' . $this->renderDisabledLabel($theme, $option);
+ return $elements->fieldOptionMarker(FALSE, TRUE) . ' ' . $this->renderDisabledLabel($theme, $option);
}
// Moving the cursor is the selection in an exclusive list, so the mark
// mirrors the cursor and the row draws only the mark.
- return $elements->fieldEntryMarker($current, TRUE) . ' ' . $this->renderMatchedLabel($theme, $option->label, $this->matchPositions($option->label), $current);
+ return $elements->fieldOptionMarker($current, TRUE) . ' ' . $this->renderMatchedLabel($theme, $option->label, $this->matchPositions($option->label), $current);
}
/**
@@ -344,12 +344,12 @@ public function renderOptionRow(ThemeInterface $theme, Option $option, bool $cur
* textual stand-in.
*/
#[\Override]
- protected function entryTextOffset(ThemeInterface $theme): int {
+ protected function optionTextOffset(ThemeInterface $theme): int {
$elements = $this->elements($theme);
$prefix = $this->multiple
- ? $elements->fieldEntrySelector(TRUE) . ' ' . $elements->fieldEntryMarker(FALSE) . ' '
- : $elements->fieldEntryMarker(FALSE, TRUE) . ' ';
+ ? $elements->fieldOptionSelector(TRUE) . ' ' . $elements->fieldOptionMarker(FALSE) . ' '
+ : $elements->fieldOptionMarker(FALSE, TRUE) . ' ';
return Ansi::width($prefix);
}
diff --git a/src/Field/FieldFactory.php b/src/Field/FieldFactory.php
index 0e390a91..0449e986 100644
--- a/src/Field/FieldFactory.php
+++ b/src/Field/FieldFactory.php
@@ -61,14 +61,14 @@ public function __construct(?KeyMap $key_map = NULL, protected bool $externalEdi
* When the block's type requires a declaration the block does not carry.
*/
public function open(Field $block, mixed $current = NULL, array $answers = []): FieldInterface {
- $entries = $this->translate($block->entries());
+ $entries = $this->translate($block->options());
$field = match ($block->type()) {
FieldType::Confirm => new Confirm((bool) $current),
- FieldType::Toggle => new Toggle($this->entryLabels($entries), $this->text($current)),
+ FieldType::Toggle => new Toggle($this->optionLabels($entries), $this->text($current)),
FieldType::Select => new Select($entries, $this->seed($block, $current), $block->isMultiple(), $block->pageSize(), $block->selectionBounds()),
FieldType::Reorder => new Reorder($entries, Field::stringList($current), $block->pageSize()),
- FieldType::Suggest => new Suggest($block->selectableValues(), $this->text($current), $block->pageSize(), $this->entryDescriptions($entries), $block->hasGhost()),
+ FieldType::Suggest => new Suggest($block->selectableValues(), $this->text($current), $block->pageSize(), $this->optionDescriptions($entries), $block->hasGhost()),
FieldType::Search => new Search($entries, $this->seed($block, $current), $block->isMultiple(), $block->pageSize(), $block->selectionBounds()),
FieldType::FilePicker => new FilePicker($block->pickerStart(), $this->seed($block, $current), $block->pickerConstraints(), $block->showsHidden(), $block->isMultiple(), $block->pageSize(), $block->selectionBounds()),
FieldType::Number => new Number($this->number($current), $block->numberBounds()),
@@ -219,7 +219,7 @@ protected function localized(array $captions): array {
* @return array
* The labels keyed by value.
*/
- protected function entryLabels(array $options): array {
+ protected function optionLabels(array $options): array {
$out = [];
foreach ($options as $option) {
@@ -267,7 +267,7 @@ protected function translate(array $options): array {
* @return array
* The description for each selectable option value.
*/
- protected function entryDescriptions(array $options): array {
+ protected function optionDescriptions(array $options): array {
$out = [];
foreach ($options as $option) {
diff --git a/src/Field/FilePicker.php b/src/Field/FilePicker.php
index 715b906e..f226ef98 100644
--- a/src/Field/FilePicker.php
+++ b/src/Field/FilePicker.php
@@ -235,7 +235,7 @@ protected function renderBody(ThemeInterface $theme): string {
$entries = $this->entries();
if ($entries === []) {
- $lines[] = $elements->fieldEntryNote(Translator::t('(empty)'));
+ $lines[] = $elements->fieldOptionNote(Translator::t('(empty)'));
}
$viewport = $this->pageViewport(count($entries), $this->cursor);
@@ -601,20 +601,20 @@ protected function isDir(string $name): bool {
protected function renderRow(ThemeInterface $theme, string $name, bool $current): string {
$label = $this->isDir($name) ? $name . '/' : $name;
$elements = $this->elements($theme);
- $row = $elements->fieldEntrySelector($current) . ' ';
+ $row = $elements->fieldOptionSelector($current) . ' ';
$chosen = FALSE;
if ($this->multiple) {
$chosen = isset($this->selected[$this->join($name)]);
- $box = $this->isSelectable($name) ? $elements->fieldEntryMarker($chosen) : $this->blankBox($theme);
+ $box = $this->isSelectable($name) ? $elements->fieldOptionMarker($chosen) : $this->blankBox($theme);
$row .= $box . ' ';
}
- return $row . $this->entryLabel($theme, $label, $current, $chosen);
+ return $row . $this->optionLabel($theme, $label, $current, $chosen);
}
/**
- * A spacer the width of a checkbox, for entries that cannot be selected.
+ * A spacer the width of a checkbox, for options that cannot be selected.
*
* @param \DrevOps\Tui\Theme\ThemeInterface $theme
* The theme.
@@ -623,7 +623,7 @@ protected function renderRow(ThemeInterface $theme, string $name, bool $current)
* The spacer.
*/
protected function blankBox(ThemeInterface $theme): string {
- return str_repeat(' ', Strings::length(Ansi::strip($this->elements($theme)->fieldEntryMarker(FALSE))));
+ return str_repeat(' ', Strings::length(Ansi::strip($this->elements($theme)->fieldOptionMarker(FALSE))));
}
/**
diff --git a/src/Field/Pause.php b/src/Field/Pause.php
index 9b02958d..befa70bd 100644
--- a/src/Field/Pause.php
+++ b/src/Field/Pause.php
@@ -59,7 +59,7 @@ protected function renderBody(ThemeInterface $theme): string {
// nothing an input layer holds a key in reaches the theme.
$glyph = $theme->keyGlyph($key->name ?? $key->label());
- return Translator::t('Press @key to continue', ['@key' => $this->entryLabel($theme, $glyph, TRUE)]);
+ return Translator::t('Press @key to continue', ['@key' => $this->optionLabel($theme, $glyph, TRUE)]);
}
/**
diff --git a/src/Field/Reorder.php b/src/Field/Reorder.php
index 7c826a26..2e47f9f9 100644
--- a/src/Field/Reorder.php
+++ b/src/Field/Reorder.php
@@ -207,7 +207,7 @@ protected function currentDescription(): string {
* The rendered row.
*/
public function renderOptionRow(ThemeInterface $theme, Option $option, bool $current): string {
- return $this->marker($theme, $current) . ' ' . $this->entryLabel($theme, $option->label, $current);
+ return $this->marker($theme, $current) . ' ' . $this->optionLabel($theme, $option->label, $current);
}
/**
@@ -230,7 +230,7 @@ protected function marker(ThemeInterface $theme, bool $current): string {
return $theme->keyGlyph(KeyName::Up) . $theme->keyGlyph(KeyName::Down);
}
- return $this->elements($theme)->fieldEntrySelector($current) . ' ';
+ return $this->elements($theme)->fieldOptionSelector($current) . ' ';
}
/**
diff --git a/src/Field/Suggest.php b/src/Field/Suggest.php
index 320a341b..aa5e010a 100644
--- a/src/Field/Suggest.php
+++ b/src/Field/Suggest.php
@@ -312,7 +312,7 @@ protected function renderBody(ThemeInterface $theme): string {
foreach (array_slice($visible, $viewport->offset, $this->pageSize) as $slot => $value) {
$current = $viewport->offset + $slot === $this->cursor;
- $rows[] = $this->elements($theme)->fieldEntrySelector($current) . ' ' . $this->renderMatchedLabel($theme, $value, $this->matchPositions($value), $current);
+ $rows[] = $this->elements($theme)->fieldOptionSelector($current) . ' ' . $this->renderMatchedLabel($theme, $value, $this->matchPositions($value), $current);
}
return implode("\n", [$this->queryLine($theme), ...$this->wrapScrolled($theme, $rows, $viewport)]);
diff --git a/src/Schema/SchemaGenerator.php b/src/Schema/SchemaGenerator.php
index cb3e1a06..cc11e23b 100644
--- a/src/Schema/SchemaGenerator.php
+++ b/src/Schema/SchemaGenerator.php
@@ -117,7 +117,7 @@ protected function options(Field $field): array {
$out = [];
- foreach ($field->entries() as $option) {
+ foreach ($field->options() as $option) {
if (!$option->isSelectable()) {
continue;
}
diff --git a/src/Schema/SchemaValidator.php b/src/Schema/SchemaValidator.php
index b05c09ae..29b7311b 100644
--- a/src/Schema/SchemaValidator.php
+++ b/src/Schema/SchemaValidator.php
@@ -208,7 +208,7 @@ protected function constraintMessage(Field $field, string $constraint): string {
* An error, or NULL when valid.
*/
protected function checkOptions(Field $field, mixed $value): ?string {
- $error = $field->entryViolation($value);
+ $error = $field->optionViolation($value);
return $error === NULL ? NULL : Translator::t('Question "@id": @error.', ['@id' => $field->id(), '@error' => $error]);
}
diff --git a/src/Screen/Collector.php b/src/Screen/Collector.php
index 1c66e6ce..8ba0c05f 100644
--- a/src/Screen/Collector.php
+++ b/src/Screen/Collector.php
@@ -463,7 +463,7 @@ protected function acceptsDetected(Field $field, mixed $value): bool {
&& $field->boundsViolation($value) === NULL
&& $field->pickerViolation($value) === NULL
&& $field->templateViolation($value) === NULL
- && $field->entryViolation($value) === NULL;
+ && $field->optionViolation($value) === NULL;
}
/**
@@ -685,7 +685,7 @@ protected function resolveEntries(array $fields, array $values, array $active, C
$memo = $this->memo[$field->id()] ?? NULL;
- if ($memo !== NULL && $memo['answers'] === $answers && $memo['run'] === $run && $memo['rows'] === $field->entries()) {
+ if ($memo !== NULL && $memo['answers'] === $answers && $memo['run'] === $run && $memo['rows'] === $field->options()) {
continue;
}
@@ -693,10 +693,10 @@ protected function resolveEntries(array $fields, array $values, array $active, C
$field->settle($resolver($resolved));
}
catch (\Throwable $throwable) {
- throw $this->entriesError($field, $throwable);
+ throw $this->optionsError($field, $throwable);
}
- $this->memo[$field->id()] = ['answers' => $answers, 'run' => $run, 'rows' => $field->entries()];
+ $this->memo[$field->id()] = ['answers' => $answers, 'run' => $run, 'rows' => $field->options()];
if ($supplied[$field->id()] ?? FALSE) {
continue;
@@ -759,7 +759,7 @@ protected function loadQueryEntries(array $fields, array $values, array $active)
// On screen a source that cannot answer degrades to a message in the
// field, but with no screen there is nobody to retype the query, so
// the collection fails instead.
- throw $this->entriesError($field, $throwable);
+ throw $this->optionsError($field, $throwable);
}
foreach ($resolved as $row) {
@@ -806,7 +806,7 @@ protected function queriesFor(Field $field, mixed $value): array {
* @return \DrevOps\Tui\CollectException
* The error naming the field.
*/
- protected function entriesError(Field $field, \Throwable $throwable): CollectException {
+ protected function optionsError(Field $field, \Throwable $throwable): CollectException {
// Not every code is an integer - a database driver's SQLSTATE is a string -
// and consumer code decides which exception arrives here, so it is coerced
// rather than allowed to fail the report instead of making it.
diff --git a/src/Screen/ScreenController.php b/src/Screen/ScreenController.php
index 748e3831..6c095157 100644
--- a/src/Screen/ScreenController.php
+++ b/src/Screen/ScreenController.php
@@ -1123,7 +1123,7 @@ protected function query(): void {
protected function offered(Field $field, array $rows): array {
$offered = [];
- foreach ([...$field->entries(), ...$rows] as $row) {
+ foreach ([...$field->options(), ...$rows] as $row) {
if ($row->kind === OptionType::Option) {
$offered[$row->value] = $row->label;
}
diff --git a/src/Theme/AbstractTheme.php b/src/Theme/AbstractTheme.php
index c343bf87..ed8dc859 100644
--- a/src/Theme/AbstractTheme.php
+++ b/src/Theme/AbstractTheme.php
@@ -199,28 +199,28 @@ public function fieldDescription(string $text): string {
/**
* {@inheritdoc}
*/
- public function fieldEntry(string $text, bool $chosen, bool $focused = FALSE): string {
+ public function fieldOption(string $text, bool $chosen, bool $focused = FALSE): string {
return $text;
}
/**
* {@inheritdoc}
*/
- public function fieldEntryMatch(string $text): string {
+ public function fieldOptionMatch(string $text): string {
return $text;
}
/**
* {@inheritdoc}
*/
- public function fieldEntrySelector(bool $selected): string {
+ public function fieldOptionSelector(bool $selected): string {
return $selected ? '>' : ' ';
}
/**
* {@inheritdoc}
*/
- public function fieldEntryMarker(bool $chosen, bool $exclusive = FALSE): string {
+ public function fieldOptionMarker(bool $chosen, bool $exclusive = FALSE): string {
if ($exclusive) {
return $chosen ? '(*)' : '( )';
}
@@ -231,21 +231,21 @@ public function fieldEntryMarker(bool $chosen, bool $exclusive = FALSE): string
/**
* {@inheritdoc}
*/
- public function fieldEntryNote(string $text): string {
+ public function fieldOptionNote(string $text): string {
return $text;
}
/**
* {@inheritdoc}
*/
- public function fieldEntryDescription(string $text): string {
+ public function fieldOptionDescription(string $text): string {
return $text;
}
/**
* {@inheritdoc}
*/
- public function fieldEntrySeparator(): string {
+ public function fieldOptionSeparator(): string {
return str_repeat('-', max(1, $this->contentWidth()));
}
diff --git a/src/Theme/DefaultTheme.php b/src/Theme/DefaultTheme.php
index f6820668..1b1042a8 100644
--- a/src/Theme/DefaultTheme.php
+++ b/src/Theme/DefaultTheme.php
@@ -858,7 +858,7 @@ public function fieldDescription(string $text): string {
* {@inheritdoc}
*/
#[\Override]
- public function fieldEntry(string $text, bool $chosen, bool $focused = FALSE): string {
+ public function fieldOption(string $text, bool $chosen, bool $focused = FALSE): string {
// Where the cursor rests is the louder of the two facts, and the only one
// that moves, so it takes the accent and picking takes weight.
if ($focused) {
@@ -872,7 +872,7 @@ public function fieldEntry(string $text, bool $chosen, bool $focused = FALSE): s
* {@inheritdoc}
*/
#[\Override]
- public function fieldEntryMatch(string $text): string {
+ public function fieldOptionMatch(string $text): string {
return $this->paint($this->isDark ? Sgr::of(Sgr::Bold, Sgr::Yellow) : Sgr::of(Sgr::Bold, Sgr::Magenta), $text);
}
@@ -880,8 +880,8 @@ public function fieldEntryMatch(string $text): string {
* {@inheritdoc}
*/
#[\Override]
- public function fieldEntrySelector(bool $selected): string {
- $glyph = $this->overriddenGlyph(ThemeElement::FieldEntrySelector);
+ public function fieldOptionSelector(bool $selected): string {
+ $glyph = $this->overriddenGlyph(ThemeElement::FieldOptionSelector);
if ($glyph === NULL || !$selected) {
return $this->marker($selected);
@@ -894,8 +894,8 @@ public function fieldEntrySelector(bool $selected): string {
* {@inheritdoc}
*/
#[\Override]
- public function fieldEntryMarker(bool $chosen, bool $exclusive = FALSE): string {
- $glyph = $this->overriddenGlyph(ThemeElement::FieldEntryMarker);
+ public function fieldOptionMarker(bool $chosen, bool $exclusive = FALSE): string {
+ $glyph = $this->overriddenGlyph(ThemeElement::FieldOptionMarker);
// Only the picked state is stated, so an entry nobody picked keeps the
// mark the theme draws for it and the patch stays a patch.
@@ -917,7 +917,7 @@ public function fieldEntryMarker(bool $chosen, bool $exclusive = FALSE): string
* {@inheritdoc}
*/
#[\Override]
- public function fieldEntryNote(string $text): string {
+ public function fieldOptionNote(string $text): string {
return $this->paint(Sgr::of(Sgr::Grey), $text);
}
@@ -925,7 +925,7 @@ public function fieldEntryNote(string $text): string {
* {@inheritdoc}
*/
#[\Override]
- public function fieldEntryDescription(string $text): string {
+ public function fieldOptionDescription(string $text): string {
// Slanted against the description's grey: it says the same kind of thing
// about a smaller subject, and the slant marks it as belonging to the entry
// above rather than to the field.
@@ -936,7 +936,7 @@ public function fieldEntryDescription(string $text): string {
* {@inheritdoc}
*/
#[\Override]
- public function fieldEntrySeparator(): string {
+ public function fieldOptionSeparator(): string {
return $this->renderRule();
}
diff --git a/src/Theme/DosTheme.php b/src/Theme/DosTheme.php
index a0324599..51b1d72d 100644
--- a/src/Theme/DosTheme.php
+++ b/src/Theme/DosTheme.php
@@ -90,7 +90,7 @@ protected function border(string $text): string {
* {@inheritdoc}
*/
#[\Override]
- public function fieldEntryMatch(string $text): string {
+ public function fieldOptionMatch(string $text): string {
return $this->paint(Sgr::of(Sgr::BrightYellow), $text);
}
diff --git a/src/Theme/EmberTheme.php b/src/Theme/EmberTheme.php
index 8aac5a36..338465f3 100644
--- a/src/Theme/EmberTheme.php
+++ b/src/Theme/EmberTheme.php
@@ -51,7 +51,7 @@ protected function border(string $text): string {
* {@inheritdoc}
*/
#[\Override]
- public function fieldEntryMatch(string $text): string {
+ public function fieldOptionMatch(string $text): string {
return $this->paint($this->isDark ? Sgr::of(Sgr::Gold) : Sgr::of(Sgr::Bronze), $text);
}
diff --git a/src/Theme/FrostTheme.php b/src/Theme/FrostTheme.php
index 9bea82af..a9d5fa39 100644
--- a/src/Theme/FrostTheme.php
+++ b/src/Theme/FrostTheme.php
@@ -51,7 +51,7 @@ protected function border(string $text): string {
* {@inheritdoc}
*/
#[\Override]
- public function fieldEntryMatch(string $text): string {
+ public function fieldOptionMatch(string $text): string {
return $this->paint($this->isDark ? Sgr::of(Sgr::Sand) : Sgr::of(Sgr::Ochre), $text);
}
diff --git a/src/Theme/MidnightTheme.php b/src/Theme/MidnightTheme.php
index 4c52ff69..ce596ff7 100644
--- a/src/Theme/MidnightTheme.php
+++ b/src/Theme/MidnightTheme.php
@@ -51,7 +51,7 @@ protected function border(string $text): string {
* {@inheritdoc}
*/
#[\Override]
- public function fieldEntryMatch(string $text): string {
+ public function fieldOptionMatch(string $text): string {
return $this->paint($this->isDark ? Sgr::of(Sgr::Pink) : Sgr::of(Sgr::Fuchsia), $text);
}
diff --git a/src/Theme/MonoTheme.php b/src/Theme/MonoTheme.php
index 6209ce93..92b022a4 100644
--- a/src/Theme/MonoTheme.php
+++ b/src/Theme/MonoTheme.php
@@ -64,7 +64,7 @@ protected function border(string $text): string {
* {@inheritdoc}
*/
#[\Override]
- public function fieldEntryMatch(string $text): string {
+ public function fieldOptionMatch(string $text): string {
return $this->paint(Sgr::of(Sgr::Reverse), $text);
}
diff --git a/src/Theme/Override/FieldOverrides.php b/src/Theme/Override/FieldOverrides.php
index 8d2d2d88..68900685 100644
--- a/src/Theme/Override/FieldOverrides.php
+++ b/src/Theme/Override/FieldOverrides.php
@@ -90,7 +90,7 @@ public function valueSeparator(string $text): self {
* The group.
*/
public function entrySelector(string $glyph, string $ascii): self {
- $this->overrides->setGlyph(ThemeElement::FieldEntrySelector, $glyph, $ascii);
+ $this->overrides->setGlyph(ThemeElement::FieldOptionSelector, $glyph, $ascii);
return $this;
}
@@ -110,7 +110,7 @@ public function entrySelector(string $glyph, string $ascii): self {
* The group.
*/
public function entryMarker(string $glyph, string $ascii): self {
- $this->overrides->setGlyph(ThemeElement::FieldEntryMarker, $glyph, $ascii);
+ $this->overrides->setGlyph(ThemeElement::FieldOptionMarker, $glyph, $ascii);
return $this;
}
diff --git a/src/Theme/Override/ThemeElement.php b/src/Theme/Override/ThemeElement.php
index 15b1024d..19d03130 100644
--- a/src/Theme/Override/ThemeElement.php
+++ b/src/Theme/Override/ThemeElement.php
@@ -26,9 +26,9 @@ enum ThemeElement: string {
case FieldValueSeparator = 'fieldValueSeparator';
- case FieldEntrySelector = 'fieldEntrySelector';
+ case FieldOptionSelector = 'fieldOptionSelector';
- case FieldEntryMarker = 'fieldEntryMarker';
+ case FieldOptionMarker = 'fieldOptionMarker';
case FieldCaret = 'fieldCaret';
diff --git a/tests/phpunit/Fixtures/Theme/CapableTheme.php b/tests/phpunit/Fixtures/Theme/CapableTheme.php
index 24c3aaf5..e891e59f 100644
--- a/tests/phpunit/Fixtures/Theme/CapableTheme.php
+++ b/tests/phpunit/Fixtures/Theme/CapableTheme.php
@@ -61,7 +61,7 @@ public function breadcrumbSeparator(): string {
* {@inheritdoc}
*/
#[\Override]
- public function fieldEntry(string $text, bool $chosen, bool $focused = FALSE): string {
+ public function fieldOption(string $text, bool $chosen, bool $focused = FALSE): string {
return $this->paint($this->emphasize(Sgr::of(Sgr::Green), $chosen || $focused), $text);
}
diff --git a/tests/phpunit/Unit/Block/FieldBlockTest.php b/tests/phpunit/Unit/Block/FieldBlockTest.php
index 7d283ed5..c8912375 100644
--- a/tests/phpunit/Unit/Block/FieldBlockTest.php
+++ b/tests/phpunit/Unit/Block/FieldBlockTest.php
@@ -564,7 +564,7 @@ public function testEntriesAreDrawnInTheOrderTheyWereDeclared(): void {
->heading('Vegetables')
->entry('carrot', 'Carrot');
- $kinds = array_map(static fn(object $entry): OptionType => $entry->kind, $field->entries());
+ $kinds = array_map(static fn(object $entry): OptionType => $entry->kind, $field->options());
$expected = [OptionType::Heading, OptionType::Option, OptionType::Separator, OptionType::Heading, OptionType::Option];
$this->assertSame($expected, $kinds);
@@ -577,7 +577,7 @@ public function testEntryValueStaysTheStringItWasDeclaredAs(): void {
$field = (new Field('grade', 'Grade', FieldType::Select))->entry('0', 'Unripe')->entry('1', 'Ripe');
$this->assertSame(['0', '1'], $field->selectableValues());
- $this->assertSame('Unripe', $field->entryOf('0')?->label);
+ $this->assertSame('Unripe', $field->optionOf('0')?->label);
$this->assertTrue($field->accept('0'));
$this->assertSame('0', $field->value());
}
@@ -588,21 +588,21 @@ public function testDeclaringValueTwiceReplacesTheEntryWhereItAlreadySits(): voi
->entry('carrot', 'Carrot')
->entry('apple', 'Golden Apple');
- $this->assertCount(2, $field->entries());
- $this->assertSame('Golden Apple', $field->entryOf('apple')?->label);
+ $this->assertCount(2, $field->options());
+ $this->assertSame('Golden Apple', $field->optionOf('apple')?->label);
$this->assertSame(['apple', 'carrot'], $field->selectableValues());
}
public function testEntryWithNoLabelDrawsItsOwnValue(): void {
$field = (new Field('basket', 'Basket contents', FieldType::Select))->entry('apple');
- $this->assertSame('apple', $field->entryOf('apple')?->label);
- $this->assertNotInstanceOf(Option::class, $field->entryOf('carrot'));
+ $this->assertSame('apple', $field->optionOf('apple')?->label);
+ $this->assertNotInstanceOf(Option::class, $field->optionOf('carrot'));
}
#[DataProvider('dataProviderValueOutsideTheEntriesIsRefused')]
public function testValueOutsideTheEntriesIsRefused(Field $field, mixed $value, ?string $error): void {
- $this->assertSame($error, $field->entryViolation($value));
+ $this->assertSame($error, $field->optionViolation($value));
}
public static function dataProviderValueOutsideTheEntriesIsRefused(): \Iterator {
@@ -654,12 +654,12 @@ public function testValueIsRefusedWhenTheQueryResolvedToNothingThatCarriesIt():
// to, so resolving to nothing means the value does not exist.
$field = (new Field('basket', 'Basket contents', FieldType::Search))->query(static fn(): array => []);
- $this->assertSame('value "plum" was not found', $field->entryViolation('plum'));
+ $this->assertSame('value "plum" was not found', $field->optionViolation('plum'));
}
#[DataProvider('dataProviderEntriesAreUnsettledWhileSomethingOwesThem')]
public function testEntriesAreUnsettledWhileSomethingOwesThem(Field $field, bool $settled, bool $dynamic): void {
- $this->assertSame($settled, $field->hasSettledEntries());
+ $this->assertSame($settled, $field->hasSettledOptions());
$this->assertSame($dynamic, $field->hasDynamicEntries());
}
@@ -691,13 +691,13 @@ public function testSettlingTheEntriesRetiresTheLoaderThatOwedThem(): void {
$this->assertSame(['apple', 'carrot'], $field->selectableValues());
$this->assertNotInstanceOf(\Closure::class, $field->loader());
- $this->assertTrue($field->hasSettledEntries());
+ $this->assertTrue($field->hasSettledOptions());
}
public function testEntriesThatAreNotMapOfLabelsSettleToNone(): void {
$field = (new Field('basket', 'Basket contents', FieldType::Select))->entry('apple', 'Apple');
- $this->assertSame([], $field->settle('not a map')->entries());
+ $this->assertSame([], $field->settle('not a map')->options());
}
public function testValueThatDoesNotFitTheDeclaredShapeIsRefused(): void {
@@ -836,8 +836,8 @@ public function testChosenEntryIsMarkedAcrossOneValueAndSeveral(): void {
// A single choice is a radio list and several is a checkbox list, which is
// the editor's shape rather than the row's: the field hands the region over
// and the kind decides what fills it.
- $this->assertSame(['○ Apple', '● Carrot'], $this->entryLines($one));
- $this->assertSame(['❯ ◼ Apple', '◼ Carrot', '◻ Plum'], $this->entryLines($several));
+ $this->assertSame(['○ Apple', '● Carrot'], $this->optionLines($one));
+ $this->assertSame(['❯ ◼ Apple', '◼ Carrot', '◻ Plum'], $this->optionLines($several));
}
public function testSeveralAnswersReadAsOneLineWhileTheFieldIsSettled(): void {
@@ -890,7 +890,7 @@ public function testOpenFieldSaysWhatItAcceptsUntilSomethingIsRefused(): void {
}
/**
- * The entry rows an open field draws, with the label prefix taken off.
+ * The option rows an open field draws, with the label prefix taken off.
*
* @param \DrevOps\Tui\Block\Field $field
* The field.
@@ -898,7 +898,7 @@ public function testOpenFieldSaysWhatItAcceptsUntilSomethingIsRefused(): void {
* @return list
* The rows.
*/
- protected function entryLines(Field $field): array {
+ protected function optionLines(Field $field): array {
$lines = explode("\n", $field->open()->render($this->theme()));
return array_map(static fn(string $line): string => trim(str_replace('Basket contents', '', $line)), $lines);
diff --git a/tests/phpunit/Unit/Block/FieldDeclarationTest.php b/tests/phpunit/Unit/Block/FieldDeclarationTest.php
index 5f74a348..f8dedbc6 100644
--- a/tests/phpunit/Unit/Block/FieldDeclarationTest.php
+++ b/tests/phpunit/Unit/Block/FieldDeclarationTest.php
@@ -55,8 +55,8 @@ public function testTreeHoldsEveryPanelAndFieldItWasDeclaredWith(): void {
$orchard = $root->children()[1];
$basket = $orchard->fields()[0];
$this->assertSame(FieldType::Select, $basket->type());
- $this->assertSame('Standard', $basket->entryOf('standard')?->label);
- $this->assertNotInstanceOf(Option::class, $basket->entryOf('missing'));
+ $this->assertSame('Standard', $basket->optionOf('standard')?->label);
+ $this->assertNotInstanceOf(Option::class, $basket->optionOf('missing'));
// The trail reaches every panel and every field beneath the root.
$this->assertCount(1, $orchard->children());
diff --git a/tests/phpunit/Unit/Block/EntryTest.php b/tests/phpunit/Unit/Block/OptionTest.php
similarity index 98%
rename from tests/phpunit/Unit/Block/EntryTest.php
rename to tests/phpunit/Unit/Block/OptionTest.php
index 8be95b4e..07206193 100644
--- a/tests/phpunit/Unit/Block/EntryTest.php
+++ b/tests/phpunit/Unit/Block/OptionTest.php
@@ -16,7 +16,7 @@
use PHPUnit\Framework\TestCase;
/**
- * Tests the option model, option kinds and the entry helpers a field offers.
+ * Tests the option model, option kinds and the option helpers a field offers.
*/
#[CoversClass(Option::class)]
#[CoversClass(OptionType::class)]
@@ -24,7 +24,7 @@
#[CoversClass(Field::class)]
#[CoversClass(FieldBuilder::class)]
#[Group('block')]
-final class EntryTest extends TestCase {
+final class OptionTest extends TestCase {
public function testListFromMap(): void {
$options = Option::list(['a' => 'Apple', 'b' => 'Banana']);
@@ -196,7 +196,7 @@ public function testSelectableValues(): void {
#[DataProvider('dataProviderOptionError')]
public function testOptionError(FieldType $type, bool $multiple, array $options, mixed $value, ?string $expected): void {
- $this->assertSame($expected, self::offering($type, $multiple, $options)->entryViolation($value));
+ $this->assertSame($expected, self::offering($type, $multiple, $options)->optionViolation($value));
}
public static function dataProviderOptionError(): \Iterator {
diff --git a/tests/phpunit/Unit/Builder/FormTest.php b/tests/phpunit/Unit/Builder/FormTest.php
index d8982521..1dead20c 100644
--- a/tests/phpunit/Unit/Builder/FormTest.php
+++ b/tests/phpunit/Unit/Builder/FormTest.php
@@ -93,13 +93,13 @@ public function testBuildsExpectedForm(): void {
$this->assertInstanceOf(Field::class, $profile);
$this->assertSame(FieldType::Select, $profile->type());
$this->assertSame('standard', $profile->value());
- $this->assertSame('Standard', $profile->entryOf('standard')?->label);
+ $this->assertSame('Standard', $profile->optionOf('standard')?->label);
$services = self::fieldOf($form, 'services');
$this->assertInstanceOf(Field::class, $services);
$this->assertSame(FieldType::Select, $services->type());
$this->assertTrue($services->isMultiple());
- $this->assertSame('Search', $services->entryOf('solr')?->description);
+ $this->assertSame('Search', $services->optionOf('solr')?->description);
$docs = self::fieldOf($form, 'docs');
$this->assertInstanceOf(Field::class, $docs);
@@ -113,7 +113,7 @@ public function testBuildsExpectedForm(): void {
$this->assertInstanceOf(Field::class, $visibility);
$this->assertSame(FieldType::Toggle, $visibility->type());
$this->assertSame('private', $visibility->value());
- $this->assertSame('Public', $visibility->entryOf('public')?->label);
+ $this->assertSame('Public', $visibility->optionOf('public')?->label);
$secret = self::fieldOf($form, 'secret');
$this->assertInstanceOf(Field::class, $secret);
@@ -199,7 +199,7 @@ public function testDefaultsAndFallbacks(): void {
// Label and option-label fall back to the id/value.
$this->assertSame('t', self::fieldOf($form, 't')?->label());
- $this->assertSame('a', self::fieldOf($form, 's')?->entryOf('a')?->label);
+ $this->assertSame('a', self::fieldOf($form, 's')?->optionOf('a')?->label);
// Form-level defaults (the global TUI runtime is tested on the Tui facade).
$this->assertTrue($form->currentButtons()->show);
@@ -656,7 +656,7 @@ public function testOptionTypesAndDisabled(): void {
$profile = self::fieldOf($form, 'profile');
$this->assertInstanceOf(Field::class, $profile);
- $options = $profile->entries();
+ $options = $profile->options();
$this->assertCount(4, $options);
$this->assertSame(OptionType::Heading, $options[0]->kind);
$this->assertSame('Recommended', $options[0]->label);
@@ -679,8 +679,8 @@ public function testRepeatedOptionValueOverridesInPlace(): void {
$this->assertInstanceOf(Field::class, $field);
// The second declaration overrides the first in place; the separator stays.
- $this->assertCount(2, $field->entries());
- $this->assertSame('Second', $field->entryOf('a')?->label);
+ $this->assertCount(2, $field->options());
+ $this->assertSame('Second', $field->optionOf('a')?->label);
$this->assertSame(['a'], $field->selectableValues());
}
diff --git a/tests/phpunit/Unit/ControlBytesTest.php b/tests/phpunit/Unit/ControlBytesTest.php
index 9be86edf..481eeacb 100644
--- a/tests/phpunit/Unit/ControlBytesTest.php
+++ b/tests/phpunit/Unit/ControlBytesTest.php
@@ -125,8 +125,8 @@ public static function dataProviderDeclaredTextIsFiltered(): \Iterator {
yield 'an option value' => [static fn(string $b): string => (new Option('figs' . $b, 'Figs'))->value, 'figs[2J'];
yield 'an option description' => [static fn(string $b): string => (new Option('v', 'V', 'Figs' . $b))->description, 'Figs[2J'];
yield 'a disabled reason' => [static fn(string $b): string => (new Option('v', 'V', '', disabled: TRUE, disabled_reason: 'Figs' . $b))->disabledReason, 'Figs[2J'];
- yield 'an entry declared on a field' => [static fn(string $b): string => (string) (new Field('f', 'F', FieldType::Select))->entry('v', 'Figs' . $b)->entryOf('v')?->label, 'Figs[2J'];
- yield 'a heading between entries' => [static fn(string $b): string => (new Field('f', 'F', FieldType::Select))->heading('Figs' . $b)->entries()[0]->label, 'Figs[2J'];
+ yield 'an entry declared on a field' => [static fn(string $b): string => (string) (new Field('f', 'F', FieldType::Select))->entry('v', 'Figs' . $b)->optionOf('v')?->label, 'Figs[2J'];
+ yield 'a heading between entries' => [static fn(string $b): string => (new Field('f', 'F', FieldType::Select))->heading('Figs' . $b)->options()[0]->label, 'Figs[2J'];
yield 'a panel title' => [static fn(string $b): string => (new Panel('p', 'Figs' . $b))->title(), 'Figs[2J'];
yield 'a panel description' => [static fn(string $b): string => (new Panel('p', 'P'))->description('Figs' . $b)->descriptionText(), 'Figs[2J'];
yield 'a breadcrumb segment' => [static fn(string $b): string => (new Breadcrumb('Figs' . $b))->render(new DefaultTheme(80, ['color' => FALSE])), 'Figs[2J'];
@@ -245,8 +245,8 @@ public function testAnEntryDeclaredTwiceReplacesTheRowAfterFiltering(): void {
->entry("apple\x00", 'Bruised apple');
// The set stays unique: the filtered value matches the row already there.
- $this->assertCount(1, $field->entries());
- $this->assertSame('Bruised apple', $field->entryOf('apple')?->label);
+ $this->assertCount(1, $field->options());
+ $this->assertSame('Bruised apple', $field->optionOf('apple')?->label);
}
public function testMultiLineMarkupKeepsItsLines(): void {
diff --git a/tests/phpunit/Unit/Field/CalendarTest.php b/tests/phpunit/Unit/Field/CalendarTest.php
index c9e26eb8..2467fe86 100644
--- a/tests/phpunit/Unit/Field/CalendarTest.php
+++ b/tests/phpunit/Unit/Field/CalendarTest.php
@@ -228,9 +228,9 @@ public function testDimsOutOfRangeDays(): void {
$view = $field->view($theme);
// A day before the minimum is rendered dimmed, not plain.
- $this->assertStringContainsString($theme->fieldEntryNote(sprintf(' %2d ', 5)), $view);
+ $this->assertStringContainsString($theme->fieldOptionNote(sprintf(' %2d ', 5)), $view);
// The cursor day stays bracketed and highlighted.
- $this->assertStringContainsString($theme->fieldEntry('[15]', FALSE, TRUE), $view);
+ $this->assertStringContainsString($theme->fieldOption('[15]', FALSE, TRUE), $view);
}
public function testDimsDaysPastMaximum(): void {
@@ -241,7 +241,7 @@ public function testDimsDaysPastMaximum(): void {
$view = $field->view($theme);
// A day after the maximum is dimmed too, guarding the upper bound.
- $this->assertStringContainsString($theme->fieldEntryNote(sprintf(' %2d ', 25)), $view);
+ $this->assertStringContainsString($theme->fieldOptionNote(sprintf(' %2d ', 25)), $view);
}
}
diff --git a/tests/phpunit/Unit/Field/SearchTest.php b/tests/phpunit/Unit/Field/SearchTest.php
index f75ae876..83e16aa0 100644
--- a/tests/phpunit/Unit/Field/SearchTest.php
+++ b/tests/phpunit/Unit/Field/SearchTest.php
@@ -277,7 +277,7 @@ public function testHighlightsMatchedCharacters(): void {
$field->handle(Key::char('a'));
$view = $field->view($theme);
- $this->assertStringContainsString($theme->fieldEntryMatch('Pa'), $view);
+ $this->assertStringContainsString($theme->fieldOptionMatch('Pa'), $view);
$this->assertStringContainsString('Palace', Ansi::strip($view));
}
@@ -371,8 +371,8 @@ public function testMultipleHighlightsMatchedCharacters(): void {
// The non-contiguous match highlights each hit character on its own,
// leaving the intervening characters unstyled.
- $this->assertStringContainsString($theme->fieldEntryMatch('B'), $view);
- $this->assertStringContainsString($theme->fieldEntryMatch('n'), $view);
+ $this->assertStringContainsString($theme->fieldOptionMatch('B'), $view);
+ $this->assertStringContainsString($theme->fieldOptionMatch('n'), $view);
$this->assertStringContainsString('Banana', Ansi::strip($view));
}
diff --git a/tests/phpunit/Unit/Field/SelectTest.php b/tests/phpunit/Unit/Field/SelectTest.php
index a46361ca..6e243a86 100644
--- a/tests/phpunit/Unit/Field/SelectTest.php
+++ b/tests/phpunit/Unit/Field/SelectTest.php
@@ -513,8 +513,8 @@ public function testMultipleSelectionHintReadsApartFromAnOptionDescription(): vo
// the field is stating cannot be told from prose about the highlighted
// option.
$this->assertStringContainsString($this->styleOf($theme->fieldConstraint(...)) . 'Select between 1 and 2 items.', $view);
- $this->assertStringContainsString($this->styleOf($theme->fieldEntryDescription(...)) . 'Crisp and sweet, the everyday choice.', $view);
- $this->assertNotSame($this->styleOf($theme->fieldConstraint(...)), $this->styleOf($theme->fieldEntryDescription(...)));
+ $this->assertStringContainsString($this->styleOf($theme->fieldOptionDescription(...)) . 'Crisp and sweet, the everyday choice.', $view);
+ $this->assertNotSame($this->styleOf($theme->fieldConstraint(...)), $this->styleOf($theme->fieldOptionDescription(...)));
}
/**
diff --git a/tests/phpunit/Unit/Field/SuggestTest.php b/tests/phpunit/Unit/Field/SuggestTest.php
index 28f777f5..bebc02a5 100644
--- a/tests/phpunit/Unit/Field/SuggestTest.php
+++ b/tests/phpunit/Unit/Field/SuggestTest.php
@@ -136,7 +136,7 @@ public function testHighlightsMatchedCharacters(): void {
// The matched "Pa" prefix is themed as a match run; the label is intact
// once the styling is stripped.
- $this->assertStringContainsString($theme->fieldEntryMatch('Pa'), $view);
+ $this->assertStringContainsString($theme->fieldOptionMatch('Pa'), $view);
$this->assertStringContainsString('Palace', Ansi::strip($view));
}
diff --git a/tests/phpunit/Unit/Theme/AbstractThemeTest.php b/tests/phpunit/Unit/Theme/AbstractThemeTest.php
index 450133ab..2696c10e 100644
--- a/tests/phpunit/Unit/Theme/AbstractThemeTest.php
+++ b/tests/phpunit/Unit/Theme/AbstractThemeTest.php
@@ -33,8 +33,8 @@ public static function dataProviderEveryStyledElementHandsBackTheStringItWasGive
yield 'field value' => [static fn(FloorTheme $t): string => $t->fieldValue('Orchard')];
yield 'field badge' => [static fn(FloorTheme $t): string => $t->fieldBadge('Orchard')];
yield 'field description' => [static fn(FloorTheme $t): string => $t->fieldDescription('Orchard')];
- yield 'field entry note' => [static fn(FloorTheme $t): string => $t->fieldEntryNote('Orchard')];
- yield 'field entry description' => [static fn(FloorTheme $t): string => $t->fieldEntryDescription('Orchard')];
+ yield 'field entry note' => [static fn(FloorTheme $t): string => $t->fieldOptionNote('Orchard')];
+ yield 'field entry description' => [static fn(FloorTheme $t): string => $t->fieldOptionDescription('Orchard')];
yield 'field error' => [static fn(FloorTheme $t): string => $t->fieldError('Orchard')];
yield 'field draft' => [static fn(FloorTheme $t): string => $t->fieldDraft('Orchard')];
yield 'field state' => [static fn(FloorTheme $t): string => $t->fieldState('Orchard')];
@@ -63,11 +63,11 @@ public static function dataProviderEveryGlyphFallsBackToWhatAsciiCanDraw(): \Ite
yield 'legend separator' => [static fn(FloorTheme $t): string => $t->legendSeparator()];
yield 'field selector' => [static fn(FloorTheme $t): string => $t->fieldSelector(TRUE)];
yield 'field help marker' => [static fn(FloorTheme $t): string => $t->fieldHelpMarker()];
- yield 'field entry selector' => [static fn(FloorTheme $t): string => $t->fieldEntrySelector(TRUE)];
- yield 'field entry marker chosen' => [static fn(FloorTheme $t): string => $t->fieldEntryMarker(TRUE)];
- yield 'field entry marker unchosen' => [static fn(FloorTheme $t): string => $t->fieldEntryMarker(FALSE)];
- yield 'field entry marker exclusive' => [static fn(FloorTheme $t): string => $t->fieldEntryMarker(TRUE, TRUE)];
- yield 'field entry separator' => [static fn(FloorTheme $t): string => $t->fieldEntrySeparator()];
+ yield 'field entry selector' => [static fn(FloorTheme $t): string => $t->fieldOptionSelector(TRUE)];
+ yield 'field entry marker chosen' => [static fn(FloorTheme $t): string => $t->fieldOptionMarker(TRUE)];
+ yield 'field entry marker unchosen' => [static fn(FloorTheme $t): string => $t->fieldOptionMarker(FALSE)];
+ yield 'field entry marker exclusive' => [static fn(FloorTheme $t): string => $t->fieldOptionMarker(TRUE, TRUE)];
+ yield 'field entry separator' => [static fn(FloorTheme $t): string => $t->fieldOptionSeparator()];
yield 'field caret' => [static fn(FloorTheme $t): string => $t->fieldCaret()];
yield 'field mask' => [static fn(FloorTheme $t): string => $t->fieldMask()];
yield 'field loading' => [static fn(FloorTheme $t): string => $t->fieldLoading()];
@@ -94,7 +94,7 @@ public static function dataProviderEveryPassageSpanHandsBackItsText(): \Iterator
yield 'code' => [static fn(FloorTheme $t): string => $t->markupCode('Orchard')];
yield 'panel description' => [static fn(FloorTheme $t): string => $t->panelDescription('Orchard')];
yield 'panel summary' => [static fn(FloorTheme $t): string => $t->panelSummary('Orchard')];
- yield 'entry match' => [static fn(FloorTheme $t): string => $t->fieldEntryMatch('Orchard')];
+ yield 'entry match' => [static fn(FloorTheme $t): string => $t->fieldOptionMatch('Orchard')];
}
public function testFloorWritesOutTargetItCannotFollow(): void {
@@ -127,15 +127,15 @@ public function testGuidanceOpensWithMarkNothingCanStrip(): void {
$floor = new FloorTheme();
$this->assertSame('> Orchard', $floor->fieldConstraint('Orchard'));
- $this->assertNotSame($floor->fieldEntryDescription('Orchard'), $floor->fieldConstraint('Orchard'));
+ $this->assertNotSame($floor->fieldOptionDescription('Orchard'), $floor->fieldConstraint('Orchard'));
}
public function testMarkOnlyAppearsWhereThereIsSomethingToMark(): void {
$floor = new FloorTheme();
$this->assertSame(' ', $floor->fieldSelector(FALSE));
- $this->assertSame(' ', $floor->fieldEntrySelector(FALSE));
- $this->assertNotSame($floor->fieldEntryMarker(TRUE), $floor->fieldEntryMarker(FALSE));
+ $this->assertSame(' ', $floor->fieldOptionSelector(FALSE));
+ $this->assertNotSame($floor->fieldOptionMarker(TRUE), $floor->fieldOptionMarker(FALSE));
}
public function testEntryIsItsOwnTextAndTheMarkBesideItIsNot(): void {
@@ -143,8 +143,8 @@ public function testEntryIsItsOwnTextAndTheMarkBesideItIsNot(): void {
// it is, and the mark beside it says whether it was picked.
$floor = new FloorTheme();
- $this->assertSame('Apple', $floor->fieldEntry('Apple', TRUE));
- $this->assertSame('Apple', $floor->fieldEntry('Apple', FALSE));
+ $this->assertSame('Apple', $floor->fieldOption('Apple', TRUE));
+ $this->assertSame('Apple', $floor->fieldOption('Apple', FALSE));
}
public function testFramingButtonBelongsToTheElementRatherThanTheBlock(): void {
diff --git a/tests/phpunit/Unit/Theme/BuiltinThemesTest.php b/tests/phpunit/Unit/Theme/BuiltinThemesTest.php
index b8d61016..d34d54ce 100644
--- a/tests/phpunit/Unit/Theme/BuiltinThemesTest.php
+++ b/tests/phpunit/Unit/Theme/BuiltinThemesTest.php
@@ -44,10 +44,10 @@ public function testPalette(string $name, Mode $mode, array $expected): void {
// A hue is stated once and every element drawn from it follows, so each
// role is read back through an element rather than through the palette.
$this->assertSame(Ansi::style('X', $expected['accent']), $theme->markupTitle('X'));
- $this->assertSame(Ansi::style('X', $expected['accent']), $theme->fieldEntry('X', FALSE, TRUE));
+ $this->assertSame(Ansi::style('X', $expected['accent']), $theme->fieldOption('X', FALSE, TRUE));
$this->assertSame(Ansi::style('X', $expected['value']), $theme->fieldValue('X'));
$this->assertSame(Ansi::style('▲', $expected['indicator']), $theme->chromeOverflowMarker(TRUE));
- $this->assertSame(Ansi::style('X', $expected['match']), $theme->fieldEntryMatch('X'));
+ $this->assertSame(Ansi::style('X', $expected['match']), $theme->fieldOptionMatch('X'));
$this->assertSame(Ansi::style('X', $expected['border']), $theme->chromeBorder('X'));
}
@@ -93,7 +93,7 @@ public function testColourOffStripsPalette(string $name): void {
$this->assertSame('Setup', $theme->markupTitle('Setup'));
$this->assertSame('X', $theme->fieldValue('X'));
$this->assertSame('▲', $theme->chromeOverflowMarker(TRUE));
- $this->assertSame('X', $theme->fieldEntryMatch('X'));
+ $this->assertSame('X', $theme->fieldOptionMatch('X'));
$this->assertSame('X', $theme->chromeBorder('X'));
}
diff --git a/tests/phpunit/Unit/Theme/ElementDelegationTest.php b/tests/phpunit/Unit/Theme/ElementDelegationTest.php
index 31828489..e3e88993 100644
--- a/tests/phpunit/Unit/Theme/ElementDelegationTest.php
+++ b/tests/phpunit/Unit/Theme/ElementDelegationTest.php
@@ -43,7 +43,7 @@ public static function dataProviderElementsSharingOneHueAreDrawnAlike(): \Iterat
];
yield 'the field and entry selectors' => [
static fn(DefaultTheme $t): string => $t->fieldSelector(TRUE),
- static fn(DefaultTheme $t): string => $t->fieldEntrySelector(TRUE),
+ static fn(DefaultTheme $t): string => $t->fieldOptionSelector(TRUE),
];
yield 'a value and a summary of values' => [
static fn(DefaultTheme $t): string => $t->fieldValue('apple'),
@@ -58,12 +58,12 @@ public static function dataProviderElementsSharingOneHueAreDrawnAlike(): \Iterat
static fn(DefaultTheme $t): string => $t->markupLine('Pick the produce.'),
];
yield 'the focused entry and the caret' => [
- static fn(DefaultTheme $t): string => $t->fieldEntry('█', FALSE, TRUE),
+ static fn(DefaultTheme $t): string => $t->fieldOption('█', FALSE, TRUE),
static fn(DefaultTheme $t): string => $t->fieldCaret(),
];
yield 'the rule and the entry separator' => [
static fn(DefaultTheme $t): string => $t->renderRule(),
- static fn(DefaultTheme $t): string => $t->fieldEntrySeparator(),
+ static fn(DefaultTheme $t): string => $t->fieldOptionSeparator(),
];
}
@@ -79,10 +79,10 @@ public function testRepaintingOneHueMovesEveryElementDrawnFromIt(\Closure $eleme
public static function dataProviderRepaintingOneHueMovesEveryElementDrawnFromIt(): \Iterator {
yield 'field selector' => [static fn(DefaultTheme $t): string => $t->fieldSelector(TRUE)];
- yield 'field entry selector' => [static fn(DefaultTheme $t): string => $t->fieldEntrySelector(TRUE)];
+ yield 'field entry selector' => [static fn(DefaultTheme $t): string => $t->fieldOptionSelector(TRUE)];
yield 'field value' => [static fn(DefaultTheme $t): string => $t->fieldValue('apple')];
yield 'field caret' => [static fn(DefaultTheme $t): string => $t->fieldCaret()];
- yield 'field entry marker' => [static fn(DefaultTheme $t): string => $t->fieldEntryMarker(TRUE, TRUE)];
+ yield 'field entry marker' => [static fn(DefaultTheme $t): string => $t->fieldOptionMarker(TRUE, TRUE)];
yield 'chrome border' => [static fn(DefaultTheme $t): string => $t->chromeBorder('----')];
yield 'chrome overflow marker' => [static fn(DefaultTheme $t): string => $t->chromeOverflowMarker(TRUE)];
yield 'panel title' => [static fn(DefaultTheme $t): string => $t->panelTitle('Delivery')];
diff --git a/tests/phpunit/Unit/Theme/SupportTest.php b/tests/phpunit/Unit/Theme/SupportTest.php
index 0cc940bc..91af6f79 100644
--- a/tests/phpunit/Unit/Theme/SupportTest.php
+++ b/tests/phpunit/Unit/Theme/SupportTest.php
@@ -68,8 +68,8 @@ public function testTurningColourOffHandsBackWhatTheElementWasGiven(): void {
public function testSelectingAnItemAddsWeightRatherThanReplacingItsColour(): void {
$theme = new CapableTheme();
- $this->assertSame("\033[32mApple\033[0m", $theme->fieldEntry('Apple', FALSE));
- $this->assertSame("\033[1;32mApple\033[0m", $theme->fieldEntry('Apple', TRUE));
+ $this->assertSame("\033[32mApple\033[0m", $theme->fieldOption('Apple', FALSE));
+ $this->assertSame("\033[1;32mApple\033[0m", $theme->fieldOption('Apple', TRUE));
}
public function testAnElementPicksItsGlyphFromWhatTheThemeSupports(): void {
diff --git a/tests/phpunit/Unit/Theme/ThemeBuilderTest.php b/tests/phpunit/Unit/Theme/ThemeBuilderTest.php
index acc824f8..d2372013 100644
--- a/tests/phpunit/Unit/Theme/ThemeBuilderTest.php
+++ b/tests/phpunit/Unit/Theme/ThemeBuilderTest.php
@@ -71,11 +71,11 @@ public static function dataProviderOverrideChangesItsElementAndNothingElse(): \I
],
'field entry selector' => [
(new ThemeBuilder())->field(static fn(FieldOverrides $f): FieldOverrides => $f->entrySelector('→', '=>'))->overrides(),
- 'fieldEntrySelector',
+ 'fieldOptionSelector',
],
'field entry marker' => [
(new ThemeBuilder())->field(static fn(FieldOverrides $f): FieldOverrides => $f->entryMarker('★', '(*)'))->overrides(),
- 'fieldEntryMarker',
+ 'fieldOptionMarker',
],
'field caret' => [
(new ThemeBuilder())->field(static fn(FieldOverrides $f): FieldOverrides => $f->caret('▌', '!'))->overrides(),
@@ -106,7 +106,7 @@ public function testOverrideNamesTheMarkAndTheThemeGoesOnPaintingIt(): void {
(new ThemeBuilder())->field(static fn(FieldOverrides $f): FieldOverrides => $f->selector('→', '=>'))->overrides()
);
- $this->assertSame($theme->fieldEntry('→', FALSE, TRUE), $theme->fieldSelector(TRUE));
+ $this->assertSame($theme->fieldOption('→', FALSE, TRUE), $theme->fieldSelector(TRUE));
}
public function testTwoSelectorsComeApartOnceEitherIsOverridden(): void {
@@ -117,7 +117,7 @@ public function testTwoSelectorsComeApartOnceEitherIsOverridden(): void {
);
$this->assertSame('→', $theme->fieldSelector(TRUE));
- $this->assertSame('❯', $theme->fieldEntrySelector(TRUE));
+ $this->assertSame('❯', $theme->fieldOptionSelector(TRUE));
}
public function testAnUnmarkedStateKeepsWhatTheThemeDrawsForIt(): void {
@@ -126,8 +126,8 @@ public function testAnUnmarkedStateKeepsWhatTheThemeDrawsForIt(): void {
(new ThemeBuilder())->field(static fn(FieldOverrides $f): FieldOverrides => $f->entryMarker('★', '(*)'))->overrides()
);
- $this->assertSame('★', $theme->fieldEntryMarker(TRUE));
- $this->assertSame($plain->fieldEntryMarker(FALSE), $theme->fieldEntryMarker(FALSE));
+ $this->assertSame('★', $theme->fieldOptionMarker(TRUE));
+ $this->assertSame($plain->fieldOptionMarker(FALSE), $theme->fieldOptionMarker(FALSE));
$this->assertSame(' ', $theme->fieldSelector(FALSE));
}
@@ -146,7 +146,7 @@ public function testOneGroupCanStateSeveralElementsAtOnce(): void {
$this->assertSame('→', $theme->fieldSelector(TRUE));
$this->assertSame('?', $theme->fieldHelpMarker());
$this->assertSame(' | ', $theme->fieldValueSeparator());
- $this->assertSame('★', $theme->fieldEntryMarker(TRUE));
+ $this->assertSame('★', $theme->fieldOptionMarker(TRUE));
$this->assertSame('▌', $theme->fieldCaret());
}
@@ -193,11 +193,11 @@ protected function elements(DefaultTheme $theme): array {
'fieldValueSeparator' => $theme->fieldValueSeparator(),
'fieldBadge' => $theme->fieldBadge('edited'),
'fieldDescription' => $theme->fieldDescription('Pick the produce.'),
- 'fieldEntry' => $theme->fieldEntry('Apple', TRUE),
- 'fieldEntrySelector' => $theme->fieldEntrySelector(TRUE),
- 'fieldEntryMarker' => $theme->fieldEntryMarker(TRUE),
- 'fieldEntryNote' => $theme->fieldEntryNote('out of season'),
- 'fieldEntryDescription' => $theme->fieldEntryDescription('Stays crisp.'),
+ 'fieldOption' => $theme->fieldOption('Apple', TRUE),
+ 'fieldOptionSelector' => $theme->fieldOptionSelector(TRUE),
+ 'fieldOptionMarker' => $theme->fieldOptionMarker(TRUE),
+ 'fieldOptionNote' => $theme->fieldOptionNote('out of season'),
+ 'fieldOptionDescription' => $theme->fieldOptionDescription('Stays crisp.'),
'fieldConstraint' => $theme->fieldConstraint('Pick two.'),
'fieldError' => $theme->fieldError('Pick at least two.'),
'fieldCaret' => $theme->fieldCaret(),
diff --git a/tests/phpunit/Unit/Theme/ThemeTest.php b/tests/phpunit/Unit/Theme/ThemeTest.php
index 5a5c9b66..c23e03d8 100644
--- a/tests/phpunit/Unit/Theme/ThemeTest.php
+++ b/tests/phpunit/Unit/Theme/ThemeTest.php
@@ -33,10 +33,10 @@ public static function dataProviderElementPaint(): \Iterator {
yield 'dark title' => [static fn(): string => (new DefaultTheme())->markupTitle('X'), '1;36'];
yield 'dark value' => [static fn(): string => (new DefaultTheme())->fieldValue('X'), '32'];
yield 'dark border' => [static fn(): string => (new DefaultTheme())->chromeBorder('X'), '36'];
- yield 'dark match' => [static fn(): string => (new DefaultTheme())->fieldEntryMatch('X'), '1;33'];
+ yield 'dark match' => [static fn(): string => (new DefaultTheme())->fieldOptionMatch('X'), '1;33'];
yield 'light title' => [static fn(): string => self::light()->markupTitle('X'), '1;34'];
yield 'light border' => [static fn(): string => self::light()->chromeBorder('X'), '34'];
- yield 'light match' => [static fn(): string => self::light()->fieldEntryMatch('X'), '1;35'];
+ yield 'light match' => [static fn(): string => self::light()->fieldOptionMatch('X'), '1;35'];
// These roles are mode-independent: dimmed chrome and the red error.
yield 'description' => [static fn(): string => (new DefaultTheme())->fieldDescription('X'), '90'];
// Guidance on how to answer steps along the grey ramp rather than taking a
@@ -45,7 +45,7 @@ public static function dataProviderElementPaint(): \Iterator {
yield 'constraint' => [static fn(): string => (new DefaultTheme())->fieldConstraint('X'), '3;38;5;246'];
yield 'error' => [static fn(): string => (new DefaultTheme())->fieldError('X'), '31'];
yield 'breadcrumb' => [static fn(): string => self::light()->breadcrumbLabel('X'), '90'];
- yield 'entry note' => [static fn(): string => (new DefaultTheme())->fieldEntryNote('X'), '90'];
+ yield 'entry note' => [static fn(): string => (new DefaultTheme())->fieldOptionNote('X'), '90'];
yield 'state' => [static fn(): string => (new DefaultTheme())->fieldState('X'), '90'];
yield 'caption' => [static fn(): string => (new DefaultTheme())->fieldCaption('X'), '1;38;5;109'];
// Inline ghost-text is dimmed gray, the same as the other dimmed chrome.
@@ -169,15 +169,15 @@ public function testRule(): void {
$this->assertStringContainsString("\033[90m", (new DefaultTheme(10))->renderRule());
// One rule wherever it appears: what stands between two runs of entries is
// what stands between two blocks of standalone output.
- $this->assertSame((new DefaultTheme(10))->renderRule(), (new DefaultTheme(10))->fieldEntrySeparator());
+ $this->assertSame((new DefaultTheme(10))->renderRule(), (new DefaultTheme(10))->fieldOptionSeparator());
}
public function testPickedEntryTakesWeightAndFocusTakesTheAccent(): void {
$theme = new DefaultTheme();
- $this->assertStringContainsString("\033[1", $theme->fieldEntry('X', TRUE));
- $this->assertStringNotContainsString("\033[1", $theme->fieldEntry('X', FALSE));
- $this->assertSame(Ansi::style('X', '1;36'), $theme->fieldEntry('X', FALSE, TRUE));
+ $this->assertStringContainsString("\033[1", $theme->fieldOption('X', TRUE));
+ $this->assertStringNotContainsString("\033[1", $theme->fieldOption('X', FALSE));
+ $this->assertSame(Ansi::style('X', '1;36'), $theme->fieldOption('X', FALSE, TRUE));
}
public function testColourOffLeavesTextPlain(): void {
@@ -217,22 +217,22 @@ public function testSelectorAndMarkerGlyphs(): void {
$this->assertSame(' ', $theme->fieldSelector(FALSE));
// A round mark for a question that takes one answer, a square one for a
// question that takes several.
- $this->assertSame('●', $theme->fieldEntryMarker(TRUE, TRUE));
- $this->assertSame('○', $theme->fieldEntryMarker(FALSE, TRUE));
- $this->assertSame('◼', $theme->fieldEntryMarker(TRUE));
- $this->assertSame('◻', $theme->fieldEntryMarker(FALSE));
+ $this->assertSame('●', $theme->fieldOptionMarker(TRUE, TRUE));
+ $this->assertSame('○', $theme->fieldOptionMarker(FALSE, TRUE));
+ $this->assertSame('◼', $theme->fieldOptionMarker(TRUE));
+ $this->assertSame('◻', $theme->fieldOptionMarker(FALSE));
$ascii = new DefaultTheme(76, ['unicode' => FALSE, 'color' => FALSE]);
$this->assertSame('>', $ascii->fieldSelector(TRUE));
- $this->assertSame('(*)', $ascii->fieldEntryMarker(TRUE, TRUE));
- $this->assertSame('[ ]', $ascii->fieldEntryMarker(FALSE));
+ $this->assertSame('(*)', $ascii->fieldOptionMarker(TRUE, TRUE));
+ $this->assertSame('[ ]', $ascii->fieldOptionMarker(FALSE));
}
public function testCursorAccentIsShared(): void {
// The selector, caret and exclusive mark all carry the cursor accent.
$this->assertStringContainsString("\033[1;36m", (new DefaultTheme())->fieldSelector(TRUE));
$this->assertStringContainsString("\033[1;36m", (new DefaultTheme())->fieldCaret());
- $this->assertStringContainsString("\033[1;36m", (new DefaultTheme())->fieldEntryMarker(TRUE, TRUE));
+ $this->assertStringContainsString("\033[1;36m", (new DefaultTheme())->fieldOptionMarker(TRUE, TRUE));
$this->assertStringContainsString("\033[1;34m", self::light()->fieldSelector(TRUE));
}
@@ -248,7 +248,7 @@ protected function accent(): string {
// Everything drawn from the accent follows; everything else stays default.
$this->assertSame(Ansi::style('X', '1;95'), $theme->markupTitle('X'));
- $this->assertSame(Ansi::style('X', '1;95'), $theme->fieldEntry('X', FALSE, TRUE));
+ $this->assertSame(Ansi::style('X', '1;95'), $theme->fieldOption('X', FALSE, TRUE));
$this->assertSame(Ansi::style('X', '32'), $theme->fieldValue('X'));
}
From 3c35972ece1adba207f66107216e7c684bd2fc64 Mon Sep 17 00:00:00 2001
From: Alex Skrypnyk
Date: Tue, 11 Aug 2026 21:36:30 +1000
Subject: [PATCH 19/27] Spoke 'columns' and 'rows' for terminal extent
everywhere, retiring the width/height dialect.
---
src/Screen/Overlay.php | 22 ++++++------
src/Screen/ScreenController.php | 22 ++++++------
src/Screen/ScreenRenderer.php | 14 ++++----
src/Screen/Scroller.php | 34 +++++++++----------
src/Terminal/Terminal.php | 8 ++---
src/Testing/BufferedTerminal.php | 4 +--
src/Tui.php | 4 +--
tests/phpunit/Unit/Terminal/TerminalTest.php | 20 +++++------
.../Unit/Testing/BufferedTerminalTest.php | 12 +++----
9 files changed, 70 insertions(+), 70 deletions(-)
diff --git a/src/Screen/Overlay.php b/src/Screen/Overlay.php
index f417b3f3..b7f82a64 100644
--- a/src/Screen/Overlay.php
+++ b/src/Screen/Overlay.php
@@ -26,9 +26,9 @@ final class Overlay {
/**
* The top-left offset that centers a box within an area.
*
- * @param int $area_width
+ * @param int $area_columns
* The area's width in columns.
- * @param int $area_height
+ * @param int $area_rows
* The area's height in rows.
* @param int $box_width
* The box's width in columns.
@@ -38,16 +38,16 @@ final class Overlay {
* @return array{int,int}
* The [top, left] offsets, never negative.
*/
- public static function center(int $area_width, int $area_height, int $box_width, int $box_height): array {
- return self::place($area_width, $area_height, $box_width, $box_height, HAlign::Center, VAlign::Middle);
+ public static function center(int $area_columns, int $area_rows, int $box_width, int $box_height): array {
+ return self::place($area_columns, $area_rows, $box_width, $box_height, HAlign::Center, VAlign::Middle);
}
/**
* The top-left offset that places a box within an area by alignment.
*
- * @param int $area_width
+ * @param int $area_columns
* The area's width in columns.
- * @param int $area_height
+ * @param int $area_rows
* The area's height in rows.
* @param int $box_width
* The box's width in columns.
@@ -61,17 +61,17 @@ public static function center(int $area_width, int $area_height, int $box_width,
* @return array{int,int}
* The [top, left] offsets, never negative.
*/
- public static function place(int $area_width, int $area_height, int $box_width, int $box_height, HAlign $halign, VAlign $valign): array {
+ public static function place(int $area_columns, int $area_rows, int $box_width, int $box_height, HAlign $halign, VAlign $valign): array {
$top = match ($valign) {
VAlign::Top => 0,
- VAlign::Middle => intdiv(max(0, $area_height - $box_height), 2),
- VAlign::Bottom => max(0, $area_height - $box_height),
+ VAlign::Middle => intdiv(max(0, $area_rows - $box_height), 2),
+ VAlign::Bottom => max(0, $area_rows - $box_height),
};
$left = match ($halign) {
HAlign::Left => 0,
- HAlign::Center => intdiv(max(0, $area_width - $box_width), 2),
- HAlign::Right => max(0, $area_width - $box_width),
+ HAlign::Center => intdiv(max(0, $area_columns - $box_width), 2),
+ HAlign::Right => max(0, $area_columns - $box_width),
};
return [$top, $left];
diff --git a/src/Screen/ScreenController.php b/src/Screen/ScreenController.php
index 6c095157..b9cdb35e 100644
--- a/src/Screen/ScreenController.php
+++ b/src/Screen/ScreenController.php
@@ -546,7 +546,7 @@ protected function guard(Terminal $terminal): string {
$columns = $this->narrowest($occupancy);
$rows = $this->shortest($occupancy);
- if ($terminal->width() >= $columns && $terminal->height() >= $rows) {
+ if ($terminal->columns() >= $columns && $terminal->rows() >= $rows) {
return '';
}
@@ -555,15 +555,15 @@ protected function guard(Terminal $terminal): string {
Translator::t('Need at least @width x @height - have @w x @h.', [
'@width' => (string) $columns,
'@height' => (string) $rows,
- '@w' => (string) $terminal->width(),
- '@h' => (string) $terminal->height(),
+ '@w' => (string) $terminal->columns(),
+ '@h' => (string) $terminal->rows(),
]),
(new Legend())->advertise($this->router->bindings(), new Hint('quit', Action::Quit))->render($this->theme),
];
$width = Ansi::blockWidth($lines);
- [$top, $left] = Overlay::center($terminal->width(), $terminal->height(), $width, count($lines));
- $backdrop = array_fill(0, max(count($lines), $terminal->height()), str_repeat(' ', max($width, $terminal->width())));
+ [$top, $left] = Overlay::center($terminal->columns(), $terminal->rows(), $width, count($lines));
+ $backdrop = array_fill(0, max(count($lines), $terminal->rows()), str_repeat(' ', max($width, $terminal->columns())));
return implode("\n", Overlay::composite($backdrop, $lines, $width, $top, $left));
}
@@ -592,16 +592,16 @@ protected function chrome(string $frame, Terminal $terminal): string {
}
$lines = explode("\n", $frame);
- $area_width = $terminal->width();
- $area_height = $terminal->height();
+ $area_columns = $terminal->columns();
+ $area_rows = $terminal->rows();
$width = Ansi::blockWidth($lines);
- if (count($lines) >= $area_height && $width >= $area_width) {
+ if (count($lines) >= $area_rows && $width >= $area_columns) {
return $frame;
}
- [$top, $left] = Overlay::place($area_width, $area_height, $width, count($lines), $occupancy->halign(), $occupancy->valign());
- $backdrop = array_fill(0, $area_height, str_repeat(' ', $area_width));
+ [$top, $left] = Overlay::place($area_columns, $area_rows, $width, count($lines), $occupancy->halign(), $occupancy->valign());
+ $backdrop = array_fill(0, $area_rows, str_repeat(' ', $area_columns));
return implode("\n", Overlay::composite($backdrop, $lines, $width, $top, $left));
}
@@ -725,7 +725,7 @@ protected function rows(Terminal $terminal): int {
$occupancy = $this->occupancy();
$tallest = $occupancy instanceof OccupyCapableInterface ? $occupancy->maxHeight() : 0;
- return $tallest > 0 ? min($terminal->height(), $tallest) : $terminal->height();
+ return $tallest > 0 ? min($terminal->rows(), $tallest) : $terminal->rows();
}
/**
diff --git a/src/Screen/ScreenRenderer.php b/src/Screen/ScreenRenderer.php
index c43f123e..84c0a80c 100644
--- a/src/Screen/ScreenRenderer.php
+++ b/src/Screen/ScreenRenderer.php
@@ -396,7 +396,7 @@ public function extent(LayoutInterface $layout, string $name, ?BlockInterface $o
$row = $total + $piece['offsets'][$at];
}
- $total += $piece['height'];
+ $total += $piece['rows'];
}
return [$total, $row];
@@ -408,7 +408,7 @@ public function extent(LayoutInterface $layout, string $name, ?BlockInterface $o
* @param \DrevOps\Tui\Screen\Region $region
* The region.
*
- * @return list,offsets:list}>
+ * @return list,offsets:list}>
* Each piece: the rows it comes to, the blocks drawn in it, and the row
* each of those starts on within it.
*/
@@ -427,7 +427,7 @@ protected function pieces(Region $region): array {
* @param bool $previews
* Whether a panel among them shows what is behind it rather than a row.
*
- * @return list,offsets:list}>
+ * @return list,offsets:list}>
* The pieces.
*/
protected function stacked(array $blocks, bool $previews = FALSE): array {
@@ -437,7 +437,7 @@ protected function stacked(array $blocks, bool $previews = FALSE): array {
// The panel you are in draws its own layout in place of its row, so what
// it comes to is what that layout comes to.
if ($block instanceof Panel && $block->isEntered()) {
- $pieces[] = ['height' => $this->height($block->currentLayout()), 'blocks' => [$block], 'offsets' => [0]];
+ $pieces[] = ['rows' => $this->rows($block->currentLayout()), 'blocks' => [$block], 'offsets' => [0]];
continue;
}
@@ -449,7 +449,7 @@ protected function stacked(array $blocks, bool $previews = FALSE): array {
}
$pieces[] = [
- 'height' => substr_count($rendered, "\n") + 1,
+ 'rows' => substr_count($rendered, "\n") + 1,
'blocks' => [$block],
'offsets' => [0],
];
@@ -467,7 +467,7 @@ protected function stacked(array $blocks, bool $previews = FALSE): array {
* @return int
* The rows.
*/
- protected function height(LayoutInterface $layout): int {
+ protected function rows(LayoutInterface $layout): int {
return $layout->natural($this->measured($layout));
}
@@ -992,7 +992,7 @@ protected function fit(Panel $panel, array $beside, int $rows): int {
// panel is one of them.
$spent += $this->spaced() ? count($beside) : 0;
- return max(0, min($rows - $spent, $this->height($panel->currentLayout())));
+ return max(0, min($rows - $spent, $this->rows($panel->currentLayout())));
}
/**
diff --git a/src/Screen/Scroller.php b/src/Screen/Scroller.php
index 2a285d56..554fb153 100644
--- a/src/Screen/Scroller.php
+++ b/src/Screen/Scroller.php
@@ -20,7 +20,7 @@ class Scroller {
*
* @param int $total
* The total number of lines.
- * @param int $height
+ * @param int $rows
* The viewport height.
* @param int $cursor
* The cursor line index.
@@ -30,8 +30,8 @@ class Scroller {
* @return \DrevOps\Tui\Screen\Viewport
* The computed viewport.
*/
- public function follow(int $total, int $height, int $cursor, int $offset): Viewport {
- if ($height <= 0 || $total <= 0) {
+ public function follow(int $total, int $rows, int $cursor, int $offset): Viewport {
+ if ($rows <= 0 || $total <= 0) {
return new Viewport(0, FALSE, FALSE);
}
@@ -40,11 +40,11 @@ public function follow(int $total, int $height, int $cursor, int $offset): Viewp
if ($cursor < $offset) {
$offset = $cursor;
}
- elseif ($cursor >= $offset + $height) {
- $offset = $cursor - $height + 1;
+ elseif ($cursor >= $offset + $rows) {
+ $offset = $cursor - $rows + 1;
}
- return $this->viewport($offset, $total, $height);
+ return $this->viewport($offset, $total, $rows);
}
/**
@@ -56,20 +56,20 @@ public function follow(int $total, int $height, int $cursor, int $offset): Viewp
* The desired first-visible-line index.
* @param int $total
* The total number of lines.
- * @param int $height
+ * @param int $rows
* The viewport height.
*
* @return \DrevOps\Tui\Screen\Viewport
* The resolved viewport.
*/
- public function viewport(int $offset, int $total, int $height): Viewport {
- if ($height <= 0 || $total <= 0) {
+ public function viewport(int $offset, int $total, int $rows): Viewport {
+ if ($rows <= 0 || $total <= 0) {
return new Viewport(0, FALSE, FALSE);
}
- $offset = $this->clamp($offset, $total, $height);
+ $offset = $this->clamp($offset, $total, $rows);
- return new Viewport($offset, $offset > 0, $offset + $height < $total);
+ return new Viewport($offset, $offset > 0, $offset + $rows < $total);
}
/**
@@ -79,14 +79,14 @@ public function viewport(int $offset, int $total, int $height): Viewport {
* The lines.
* @param int $offset
* The first-visible-line index.
- * @param int $height
+ * @param int $rows
* The viewport height.
*
* @return list
* The visible lines.
*/
- public function slice(array $lines, int $offset, int $height): array {
- return array_slice($lines, max(0, $offset), max(0, $height));
+ public function slice(array $lines, int $offset, int $rows): array {
+ return array_slice($lines, max(0, $offset), max(0, $rows));
}
/**
@@ -96,14 +96,14 @@ public function slice(array $lines, int $offset, int $height): array {
* The offset.
* @param int $total
* The total number of lines.
- * @param int $height
+ * @param int $rows
* The viewport height.
*
* @return int
* The clamped offset.
*/
- protected function clamp(int $offset, int $total, int $height): int {
- return max(0, min(max(0, $total - $height), $offset));
+ protected function clamp(int $offset, int $total, int $rows): int {
+ return max(0, min(max(0, $total - $rows), $offset));
}
}
diff --git a/src/Terminal/Terminal.php b/src/Terminal/Terminal.php
index 3fa83f53..5b5bccd2 100644
--- a/src/Terminal/Terminal.php
+++ b/src/Terminal/Terminal.php
@@ -193,7 +193,7 @@ public function read(int $bytes = 32): string {
}
/**
- * The terminal height in rows.
+ * The terminal rows.
*
* A LINES environment override wins; otherwise the size is probed from the
* terminal once per instance, falling back to the classic 24 rows.
@@ -201,12 +201,12 @@ public function read(int $bytes = 32): string {
* @return int
* The number of rows available for rendering.
*/
- public function height(): int {
+ public function rows(): int {
return $this->envDimension('LINES') ?? $this->size()[1];
}
/**
- * The terminal width in columns.
+ * The terminal columns.
*
* A COLUMNS environment override wins; otherwise the size is probed from the
* terminal once per instance, falling back to the classic 80 columns.
@@ -214,7 +214,7 @@ public function height(): int {
* @return int
* The number of columns available for rendering.
*/
- public function width(): int {
+ public function columns(): int {
return $this->envDimension('COLUMNS') ?? $this->size()[0];
}
diff --git a/src/Testing/BufferedTerminal.php b/src/Testing/BufferedTerminal.php
index f32701b0..cb724260 100644
--- a/src/Testing/BufferedTerminal.php
+++ b/src/Testing/BufferedTerminal.php
@@ -76,7 +76,7 @@ public function read(int $bytes = 32): string {
* {@inheritdoc}
*/
#[\Override]
- public function height(): int {
+ public function rows(): int {
return $this->rows;
}
@@ -84,7 +84,7 @@ public function height(): int {
* {@inheritdoc}
*/
#[\Override]
- public function width(): int {
+ public function columns(): int {
return $this->cols;
}
diff --git a/src/Tui.php b/src/Tui.php
index aab03e6b..39779bb7 100644
--- a/src/Tui.php
+++ b/src/Tui.php
@@ -599,7 +599,7 @@ public function output(?Terminal $terminal = NULL): Output {
$terminal ??= self::primitiveTerminal();
$options = $this->primitiveThemeOptions($terminal->isOutputTty());
- $theme = $this->buildTheme('', self::frameWidth($options, $terminal->width()), $options);
+ $theme = $this->buildTheme('', self::frameWidth($options, $terminal->columns()), $options);
return new Output($terminal, self::pieces($theme));
}
@@ -668,7 +668,7 @@ public function interact(string $theme = '', string $banner = '', string $versio
$options = $this->resolveThemeOptions($terminal);
- return $this->controller($options, $theme, $banner, $version, $directory, self::frameWidth($options, $terminal->width()), $update)->run($terminal);
+ return $this->controller($options, $theme, $banner, $version, $directory, self::frameWidth($options, $terminal->columns()), $update)->run($terminal);
}
/**
diff --git a/tests/phpunit/Unit/Terminal/TerminalTest.php b/tests/phpunit/Unit/Terminal/TerminalTest.php
index bd2eb4d0..5f6ff181 100644
--- a/tests/phpunit/Unit/Terminal/TerminalTest.php
+++ b/tests/phpunit/Unit/Terminal/TerminalTest.php
@@ -82,12 +82,12 @@ public function testRenderWashesTheBackground(): void {
$this->assertStringContainsString("\033[2J", $contents);
}
- public function testHeight(): void {
- $this->assertGreaterThan(0, (new Terminal())->height());
+ public function testRows(): void {
+ $this->assertGreaterThan(0, (new Terminal())->rows());
}
- public function testWidth(): void {
- $this->assertGreaterThan(0, (new Terminal())->width());
+ public function testColumns(): void {
+ $this->assertGreaterThan(0, (new Terminal())->columns());
}
public function testSizeFromEnvironmentOverrides(): void {
@@ -96,8 +96,8 @@ public function testSizeFromEnvironmentOverrides(): void {
$terminal = new ProbeTerminal();
- $this->assertSame(120, $terminal->width());
- $this->assertSame(40, $terminal->height());
+ $this->assertSame(120, $terminal->columns());
+ $this->assertSame(40, $terminal->rows());
}
public function testSizeIgnoresNonPositiveEnvironment(): void {
@@ -106,8 +106,8 @@ public function testSizeIgnoresNonPositiveEnvironment(): void {
$terminal = new ProbeTerminal("34 132\n");
- $this->assertSame(132, $terminal->width());
- $this->assertSame(34, $terminal->height());
+ $this->assertSame(132, $terminal->columns());
+ $this->assertSame(34, $terminal->rows());
}
#[DataProvider('dataProviderSizeFromProbe')]
@@ -117,8 +117,8 @@ public function testSizeFromProbe(?string $reply, bool $windows, int $width, int
$terminal = new ProbeTerminal($reply, $windows);
- $this->assertSame($width, $terminal->width());
- $this->assertSame($height, $terminal->height());
+ $this->assertSame($width, $terminal->columns());
+ $this->assertSame($height, $terminal->rows());
}
public static function dataProviderSizeFromProbe(): \Iterator {
diff --git a/tests/phpunit/Unit/Testing/BufferedTerminalTest.php b/tests/phpunit/Unit/Testing/BufferedTerminalTest.php
index 21f16e63..e150a030 100644
--- a/tests/phpunit/Unit/Testing/BufferedTerminalTest.php
+++ b/tests/phpunit/Unit/Testing/BufferedTerminalTest.php
@@ -26,14 +26,14 @@ public function testReadDequeuesOnePerCallThenEof(): void {
$this->assertSame('', $terminal->read());
}
- public function testHeightIsFixed(): void {
- $this->assertSame(24, (new BufferedTerminal())->height());
- $this->assertSame(30, (new BufferedTerminal([], 30))->height());
+ public function testRowsIsFixed(): void {
+ $this->assertSame(24, (new BufferedTerminal())->rows());
+ $this->assertSame(30, (new BufferedTerminal([], 30))->rows());
}
- public function testWidthIsFixed(): void {
- $this->assertSame(80, (new BufferedTerminal())->width());
- $this->assertSame(120, (new BufferedTerminal([], 24, 120))->width());
+ public function testColumnsIsFixed(): void {
+ $this->assertSame(80, (new BufferedTerminal())->columns());
+ $this->assertSame(120, (new BufferedTerminal([], 24, 120))->columns());
}
public function testSetupAndRestoreProduceNoOutput(): void {
From 3823f85937e7426657590851398cad2661599a6b Mon Sep 17 00:00:00 2001
From: Alex Skrypnyk
Date: Tue, 11 Aug 2026 21:49:28 +1000
Subject: [PATCH 20/27] Aligned the public primitive and testing surfaces on
their siblings' shapes.
---
docs/content/output.mdx | 6 +-
playground/18-output-box.php | 6 +-
playground/themes/OceanTheme.php | 4 +-
.../Element/PrimitiveElementsInterface.php | 6 +-
src/Primitive/Output.php | 10 ++--
src/Primitive/Progress.php | 4 +-
src/Screen/ScreenController.php | 2 +-
src/Testing/ScreenTester.php | 15 +++--
src/Testing/TuiTester.php | 58 +++++++++++++++++--
src/Theme/DefaultTheme.php | 4 +-
src/Tui.php | 4 +-
tests/phpunit/Unit/ControlBytesTest.php | 2 +-
tests/phpunit/Unit/Primitive/OutputTest.php | 8 +--
tests/phpunit/Unit/Theme/OutputRenderTest.php | 4 +-
tests/phpunit/Unit/Theme/ThemeRenderTest.php | 4 +-
15 files changed, 95 insertions(+), 42 deletions(-)
diff --git a/docs/content/output.mdx b/docs/content/output.mdx
index 319b5a97..7e24b289 100644
--- a/docs/content/output.mdx
+++ b/docs/content/output.mdx
@@ -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.');
$answers = $tui->run();
@@ -34,11 +34,11 @@ $out->success('Preserves are ready')
```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.
diff --git a/playground/18-output-box.php b/playground/18-output-box.php
index 8603e49f..fef40807 100644
--- a/playground/18-output-box.php
+++ b/playground/18-output-box.php
@@ -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.');
diff --git a/playground/themes/OceanTheme.php b/playground/themes/OceanTheme.php
index a33ff5b3..177170eb 100644
--- a/playground/themes/OceanTheme.php
+++ b/playground/themes/OceanTheme.php
@@ -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) {
@@ -252,7 +252,7 @@ public function renderBanner(string $logo, string $version): string {
$lines[] = $this->footer('≈ ' . $version . ' ≈');
}
- return implode("\n", $lines);
+ return $lines;
}
}
diff --git a/src/Primitive/Element/PrimitiveElementsInterface.php b/src/Primitive/Element/PrimitiveElementsInterface.php
index 9854236e..3730ce27 100644
--- a/src/Primitive/Element/PrimitiveElementsInterface.php
+++ b/src/Primitive/Element/PrimitiveElementsInterface.php
@@ -95,10 +95,10 @@ public function renderRule(): string;
* @param string $version
* The version shown below the logo, or an empty string for none.
*
- * @return string
- * The composed banner.
+ * @return list
+ * The banner's physical lines.
*/
- public function renderBanner(string $logo, string $version): string;
+ public function renderBanner(string $logo, string $version): array;
/**
* Draw a status line: the kind's glyph and the message, in its colour.
diff --git a/src/Primitive/Output.php b/src/Primitive/Output.php
index 5b43b926..dd3757d4 100644
--- a/src/Primitive/Output.php
+++ b/src/Primitive/Output.php
@@ -23,7 +23,7 @@
*
* @code
* $out = $tui->output();
- * $out->box('Everything below is optional.', 'Welcome');
+ * $out->box('Welcome', 'Everything below is optional.');
* $out->success('Preserves are ready');
* $out->definitions(['Jars' => '12', 'Fruit' => 'Apricot']);
* @endcode
@@ -46,17 +46,17 @@ public function __construct(protected Terminal $terminal, protected PrimitiveEle
/**
* Write a bordered box with an optional title.
*
+ * @param string $title
+ * The heading shown as the box's first line; empty for a bare box.
* @param string|list $body
* The body: one string (its newlines splitting it into lines), or a list of
* lines. Long lines wrap to the box's inner width, and an empty entry of a
* list stays a blank line so the content can be spaced out.
- * @param string $title
- * The heading shown as the box's first line; empty for a bare box.
*
* @return $this
* The primitive.
*/
- public function box(string|array $body, string $title = ''): self {
+ public function box(string $title = '', string|array $body = ''): self {
return $this->writeLines($this->theme->renderCard(Ansi::sanitize($title), self::toLines($body)));
}
@@ -131,7 +131,7 @@ public function rule(): self {
* The primitive.
*/
public function banner(string $logo, string $version = ''): self {
- return $this->writeLines(explode("\n", $this->theme->renderBanner(Ansi::sanitize($logo), Ansi::sanitize($version))));
+ return $this->writeLines($this->theme->renderBanner(Ansi::sanitize($logo), Ansi::sanitize($version)));
}
/**
diff --git a/src/Primitive/Progress.php b/src/Primitive/Progress.php
index d9bc1688..4d3ada27 100644
--- a/src/Primitive/Progress.php
+++ b/src/Primitive/Progress.php
@@ -83,7 +83,7 @@ public function __construct(
/**
* Run the callback, showing progress while it works.
*
- * @param callable(self): TReturn $work
+ * @param \Closure(self): TReturn $work
* The work to run; it receives this primitive so it can drive the updates.
*
* @return TReturn
@@ -91,7 +91,7 @@ public function __construct(
*
* @template TReturn
*/
- public function run(callable $work): mixed {
+ public function run(\Closure $work): mixed {
if (!$this->active) {
// Off a TTY the indicator is invisible chrome, so a single plain caption
// line is the whole trace. Flush it so a block-buffered stream shows the
diff --git a/src/Screen/ScreenController.php b/src/Screen/ScreenController.php
index b9cdb35e..468e5a10 100644
--- a/src/Screen/ScreenController.php
+++ b/src/Screen/ScreenController.php
@@ -518,7 +518,7 @@ protected function opening(): string {
return '';
}
- return $this->pieces()->renderBanner($this->banner, $this->version) . "\n\n" . Translator::t('Press any key to continue...');
+ return implode("\n", $this->pieces()->renderBanner($this->banner, $this->version)) . "\n\n" . Translator::t('Press any key to continue...');
}
/**
diff --git a/src/Testing/ScreenTester.php b/src/Testing/ScreenTester.php
index 2d8904da..47b7959f 100644
--- a/src/Testing/ScreenTester.php
+++ b/src/Testing/ScreenTester.php
@@ -18,6 +18,7 @@
use DrevOps\Tui\Theme\DefaultTheme;
use DrevOps\Tui\Theme\Mode;
use DrevOps\Tui\Theme\ThemeInterface;
+use DrevOps\Tui\Theme\ThemeManager;
use DrevOps\Tui\Tui;
/**
@@ -151,14 +152,20 @@ public function __construct(protected Panel $panel) {
/**
* Set the theme the blocks draw through.
*
- * @param \DrevOps\Tui\Theme\ThemeInterface $theme
- * The theme.
+ * @param \DrevOps\Tui\Theme\ThemeInterface|string $theme
+ * The theme instance or name.
*
* @return $this
* The tester.
*/
- public function theme(ThemeInterface $theme): self {
- $this->theme = $theme;
+ public function theme(ThemeInterface|string $theme): self {
+ if (is_string($theme)) {
+ $options = $this->themeOptions();
+ $this->theme = ThemeManager::create($theme, Tui::frameWidth($options, $this->cols), $options);
+ }
+ else {
+ $this->theme = $theme;
+ }
return $this;
}
diff --git a/src/Testing/TuiTester.php b/src/Testing/TuiTester.php
index 8806c502..4838c577 100644
--- a/src/Testing/TuiTester.php
+++ b/src/Testing/TuiTester.php
@@ -8,11 +8,16 @@
use DrevOps\Tui\Block\BlockInterface;
use DrevOps\Tui\Builder\Form;
use DrevOps\Tui\CancelException;
+use DrevOps\Tui\Handler\Context;
use DrevOps\Tui\Input\Key;
use DrevOps\Tui\InterruptException;
use DrevOps\Tui\Screen\Axis;
+use DrevOps\Tui\Screen\Collector;
+use DrevOps\Tui\Screen\ScreenController;
use DrevOps\Tui\Terminal\Ansi;
+use DrevOps\Tui\Theme\Border;
use DrevOps\Tui\Theme\Mode;
+use DrevOps\Tui\Theme\ThemeInterface;
use DrevOps\Tui\Tui;
/**
@@ -56,6 +61,11 @@ final class TuiTester {
*/
protected string $theme = '';
+ /**
+ * The theme instance, when one was passed directly.
+ */
+ protected ?ThemeInterface $themeInstance = NULL;
+
/**
* The reported terminal height.
*/
@@ -116,16 +126,23 @@ public function __construct(Form $form, array $handler_namespaces = [], string $
}
/**
- * Set the theme name or class the form is rendered with.
+ * Set the theme the form is rendered with.
*
- * @param string $theme
- * The theme name or class.
+ * @param \DrevOps\Tui\Theme\ThemeInterface|string $theme
+ * The theme instance, name or class.
*
* @return $this
* The tester.
*/
- public function theme(string $theme): self {
- $this->theme = $theme;
+ public function theme(ThemeInterface|string $theme): self {
+ if (is_string($theme)) {
+ $this->theme = $theme;
+ $this->themeInstance = NULL;
+ }
+ else {
+ $this->themeInstance = $theme;
+ $this->theme = '';
+ }
return $this;
}
@@ -302,7 +319,36 @@ public function run(string|Key ...$items): Answers {
// terminal's columns.
$width = Tui::frameWidth($this->options, $this->cols);
- $controller = $this->tui->controller($this->options, $this->theme, '', $this->version, $this->directory, $width, $this->update);
+ if ($this->themeInstance instanceof ThemeInterface) {
+ // Build the controller manually when a ThemeInterface instance was
+ // passed.
+ $collector = new Collector($this->tui->registry(), []);
+ $controller = new ScreenController(
+ $this->tui->root(),
+ $this->themeInstance,
+ [],
+ NULL,
+ $collector,
+ new Context($this->directory, [], $this->update, $this->version),
+ 'default',
+ Border::None,
+ TRUE,
+ TRUE,
+ '',
+ $this->version,
+ );
+ }
+ else {
+ $controller = $this->tui->controller(
+ $this->options,
+ $this->theme,
+ '',
+ $this->version,
+ $this->directory,
+ $width,
+ $this->update
+ );
+ }
$this->cancelled = FALSE;
$this->interrupted = FALSE;
diff --git a/src/Theme/DefaultTheme.php b/src/Theme/DefaultTheme.php
index 1b1042a8..2c5a4446 100644
--- a/src/Theme/DefaultTheme.php
+++ b/src/Theme/DefaultTheme.php
@@ -1494,7 +1494,7 @@ public function renderRule(): string {
/**
* {@inheritdoc}
*/
- public function renderBanner(string $logo, string $version): string {
+ public function renderBanner(string $logo, string $version): array {
$lines = [];
foreach (explode("\n", $logo) as $line) {
@@ -1506,7 +1506,7 @@ public function renderBanner(string $logo, string $version): string {
$lines[] = $this->footer(Translator::t('Version: @version', ['@version' => $version]));
}
- return implode("\n", $lines);
+ return $lines;
}
/**
diff --git a/src/Tui.php b/src/Tui.php
index 39779bb7..59bdee96 100644
--- a/src/Tui.php
+++ b/src/Tui.php
@@ -554,7 +554,7 @@ public function collect(string $prompts = '', string $directory = '', bool $upda
* spinner.
* @param string $caption
* The caption shown beside the indicator.
- * @param callable(\DrevOps\Tui\Primitive\Progress): TReturn $work
+ * @param \Closure(\DrevOps\Tui\Primitive\Progress): TReturn $work
* The work to run; it receives the progress primitive and its result is
* returned.
* @param \DrevOps\Tui\Terminal\Terminal|null $terminal
@@ -565,7 +565,7 @@ public function collect(string $prompts = '', string $directory = '', bool $upda
*
* @template TReturn
*/
- public function progress(?int $total, string $caption, callable $work, ?Terminal $terminal = NULL): mixed {
+ public function progress(?int $total, string $caption, \Closure $work, ?Terminal $terminal = NULL): mixed {
// Restore this facade's language at the operation boundary (see collect()).
Translator::setShared($this->translator);
diff --git a/tests/phpunit/Unit/ControlBytesTest.php b/tests/phpunit/Unit/ControlBytesTest.php
index 481eeacb..128a9909 100644
--- a/tests/phpunit/Unit/ControlBytesTest.php
+++ b/tests/phpunit/Unit/ControlBytesTest.php
@@ -373,7 +373,7 @@ public function testTheOutputPrimitiveWritesFilteredText(\Closure $write, string
*/
public static function dataProviderTheOutputPrimitiveWritesFilteredText(): \Iterator {
yield 'a box body' => [static fn(Output $out, string $b): Output => $out->box('Figs' . $b), 'Figs'];
- yield 'a box body as a list' => [static fn(Output $out, string $b): Output => $out->box(['Figs' . $b, 'Plums']), 'Plums'];
+ yield 'a box body as a list' => [static fn(Output $out, string $b): Output => $out->box('', ['Figs' . $b, 'Plums']), 'Plums'];
yield 'a box title' => [static fn(Output $out, string $b): Output => $out->box('Plums', 'Figs' . $b), 'Figs'];
yield 'a card title' => [static fn(Output $out, string $b): Output => $out->card('Figs' . $b, 'Plums'), 'Figs'];
yield 'a card grid cell' => [static fn(Output $out, string $b): Output => $out->card('Crates', '', ['Produce'], [['Figs' . $b]]), 'Figs'];
diff --git a/tests/phpunit/Unit/Primitive/OutputTest.php b/tests/phpunit/Unit/Primitive/OutputTest.php
index c9166248..725a1e0b 100644
--- a/tests/phpunit/Unit/Primitive/OutputTest.php
+++ b/tests/phpunit/Unit/Primitive/OutputTest.php
@@ -25,7 +25,7 @@ final class OutputTest extends TestCase {
public function testBoxWritesTheFramedLines(): void {
$terminal = new BufferedTerminal();
- (new Output($terminal, $this->theme(color: FALSE)))->box('Pick your fruit.', 'Welcome');
+ (new Output($terminal, $this->theme(color: FALSE)))->box('Welcome', 'Pick your fruit.');
$output = $terminal->output();
@@ -38,7 +38,7 @@ public function testBoxWritesTheFramedLines(): void {
public function testBoxAcceptsLineList(): void {
$terminal = new BufferedTerminal();
- (new Output($terminal, $this->theme(color: FALSE)))->box(['Apples', 'Pears']);
+ (new Output($terminal, $this->theme(color: FALSE)))->box('', ['Apples', 'Pears']);
$lines = explode("\n", trim($terminal->output(), "\n"));
@@ -184,7 +184,7 @@ public function testOutputCarriesNoControlSequencesWithoutColour(): void {
$terminal = new BufferedTerminal();
(new Output($terminal, $this->theme(color: FALSE)))
- ->box('Pick your fruit.', 'Welcome')
+ ->box('Welcome', 'Pick your fruit.')
->success('Preserves are ready')
->definitions(['Jars' => '12']);
@@ -195,7 +195,7 @@ public function testColourWrapsEveryPiece(): void {
$terminal = new BufferedTerminal();
(new Output($terminal, $this->theme(color: TRUE)))
- ->box('Pick your fruit.', 'Welcome')
+ ->box('Welcome', 'Pick your fruit.')
->success('Preserves are ready')
->definitions(['Jars' => '12']);
diff --git a/tests/phpunit/Unit/Theme/OutputRenderTest.php b/tests/phpunit/Unit/Theme/OutputRenderTest.php
index 097eff1e..85bd0921 100644
--- a/tests/phpunit/Unit/Theme/OutputRenderTest.php
+++ b/tests/phpunit/Unit/Theme/OutputRenderTest.php
@@ -171,7 +171,7 @@ public function testRuleSpansTheRowWidth(): void {
}
public function testBannerStacksTheLogoAboveItsVersion(): void {
- $lines = explode("\n", $this->theme(color: FALSE)->renderBanner("Produce\nBox", '1.2.3'));
+ $lines = $this->theme(color: FALSE)->renderBanner("Produce\nBox", '1.2.3');
$this->assertSame('Produce', $lines[0]);
$this->assertSame('Box', $lines[1]);
@@ -180,7 +180,7 @@ public function testBannerStacksTheLogoAboveItsVersion(): void {
}
public function testBannerWithoutVersionIsTheLogoAlone(): void {
- $this->assertSame('Produce', $this->theme(color: FALSE)->renderBanner('Produce', ''));
+ $this->assertSame(['Produce'], $this->theme(color: FALSE)->renderBanner('Produce', ''));
}
public function testBoxWrapsLongBodyTextInsideTheBorder(): void {
diff --git a/tests/phpunit/Unit/Theme/ThemeRenderTest.php b/tests/phpunit/Unit/Theme/ThemeRenderTest.php
index da167b5e..3af416a8 100644
--- a/tests/phpunit/Unit/Theme/ThemeRenderTest.php
+++ b/tests/phpunit/Unit/Theme/ThemeRenderTest.php
@@ -75,12 +75,12 @@ public function testRenderTableCapsAtFrameWidth(): void {
}
public function testBanner(): void {
- $banner = Ansi::strip($this->plainTheme()->renderBanner("LOGO\nline", '1.2.3'));
+ $banner = Ansi::strip(implode("\n", $this->plainTheme()->renderBanner("LOGO\nline", '1.2.3')));
$this->assertStringContainsString('LOGO', $banner);
$this->assertStringContainsString('Version: 1.2.3', $banner);
- $this->assertStringNotContainsString('Version', Ansi::strip($this->plainTheme()->renderBanner('LOGO', '')));
+ $this->assertStringNotContainsString('Version', Ansi::strip(implode("\n", $this->plainTheme()->renderBanner('LOGO', ''))));
}
#[DataProvider('dataProviderKeyGlyph')]
From d1eabf0273806e99e3fed499f1c18698c1dd132b Mon Sep 17 00:00:00 2001
From: Alex Skrypnyk
Date: Tue, 11 Aug 2026 21:51:10 +1000
Subject: [PATCH 21/27] Routed the renderer's scroll window through 'Scroller',
leaving one home for the indicator rule.
---
src/Screen/ScreenRenderer.php | 15 +++++++++------
1 file changed, 9 insertions(+), 6 deletions(-)
diff --git a/src/Screen/ScreenRenderer.php b/src/Screen/ScreenRenderer.php
index 84c0a80c..0ae3efb4 100644
--- a/src/Screen/ScreenRenderer.php
+++ b/src/Screen/ScreenRenderer.php
@@ -55,10 +55,13 @@ final class ScreenRenderer {
* @param \DrevOps\Tui\Theme\Border $border
* The frame drawn around every region at once; none by default, which
* leaves the rows exactly as the layout arranged them.
+ * @param \DrevOps\Tui\Screen\Scroller $scroller
+ * Resolves the window over content that outran the space it was given.
*/
public function __construct(
protected ThemeInterface $theme,
protected Border $border = Border::None,
+ protected Scroller $scroller = new Scroller(),
) {
}
@@ -653,10 +656,10 @@ protected function present(array $line, array $measured): array {
*/
protected function moved(LayoutInterface $layout, array $lines, int $rows, int $columns): array {
$content = count($lines);
- $from = $layout->offset($content, $rows);
- $shown = array_slice($lines, $from, max(0, $rows));
+ $window = $this->scroller->viewport($layout->offset($content, $rows), $content, $rows);
+ $shown = $this->scroller->slice($lines, $window->offset, $rows);
- return $this->marked($shown, $columns, $from > 0, $from + $rows < $content);
+ return $this->marked($shown, $columns, $window->hasAbove, $window->hasBelow);
}
/**
@@ -790,8 +793,8 @@ protected function packed(Region $region, int $rows, int $columns, bool $furnish
// declared to, and clips if it was not. Either way it hands back the rows
// it was given, so the frame stays the shape the layout worked out.
$content = count($lines);
- $from = $region->isScrolling() ? $region->offset($content, $rows) : 0;
- $lines = array_slice($lines, $from, max(0, $rows));
+ $window = $this->scroller->viewport($region->isScrolling() ? $region->offset($content, $rows) : 0, $content, $rows);
+ $lines = $this->scroller->slice($lines, $window->offset, $rows);
// What packs from the end takes the cells the start left, so where the two
// meet in the middle the head keeps its rows and the tail is the one cut.
@@ -813,7 +816,7 @@ protected function packed(Region $region, int $rows, int $columns, bool $furnish
return $lines;
}
- return $this->marked($lines, $columns, $from > 0, $from + $rows < $content);
+ return $this->marked($lines, $columns, $window->hasAbove, $window->hasBelow);
}
/**
From 88b7eea487bc74552e8248906ad581b527e1400c Mon Sep 17 00:00:00 2001
From: Alex Skrypnyk
Date: Tue, 11 Aug 2026 22:14:05 +1000
Subject: [PATCH 22/27] Addressed code review: let the facade take a built
theme, and deferred the tester's named-theme build.
---
src/Field/FieldFactory.php | 12 ++---
src/Testing/ScreenTester.php | 42 ++++++++++++++---
src/Testing/TuiTester.php | 46 +++++--------------
src/Tui.php | 16 ++++---
.../phpunit/Unit/Theme/AbstractThemeTest.php | 22 ++++-----
5 files changed, 74 insertions(+), 64 deletions(-)
diff --git a/src/Field/FieldFactory.php b/src/Field/FieldFactory.php
index 0449e986..58703b09 100644
--- a/src/Field/FieldFactory.php
+++ b/src/Field/FieldFactory.php
@@ -61,15 +61,15 @@ public function __construct(?KeyMap $key_map = NULL, protected bool $externalEdi
* When the block's type requires a declaration the block does not carry.
*/
public function open(Field $block, mixed $current = NULL, array $answers = []): FieldInterface {
- $entries = $this->translate($block->options());
+ $options = $this->translate($block->options());
$field = match ($block->type()) {
FieldType::Confirm => new Confirm((bool) $current),
- FieldType::Toggle => new Toggle($this->optionLabels($entries), $this->text($current)),
- FieldType::Select => new Select($entries, $this->seed($block, $current), $block->isMultiple(), $block->pageSize(), $block->selectionBounds()),
- FieldType::Reorder => new Reorder($entries, Field::stringList($current), $block->pageSize()),
- FieldType::Suggest => new Suggest($block->selectableValues(), $this->text($current), $block->pageSize(), $this->optionDescriptions($entries), $block->hasGhost()),
- FieldType::Search => new Search($entries, $this->seed($block, $current), $block->isMultiple(), $block->pageSize(), $block->selectionBounds()),
+ FieldType::Toggle => new Toggle($this->optionLabels($options), $this->text($current)),
+ FieldType::Select => new Select($options, $this->seed($block, $current), $block->isMultiple(), $block->pageSize(), $block->selectionBounds()),
+ FieldType::Reorder => new Reorder($options, Field::stringList($current), $block->pageSize()),
+ FieldType::Suggest => new Suggest($block->selectableValues(), $this->text($current), $block->pageSize(), $this->optionDescriptions($options), $block->hasGhost()),
+ FieldType::Search => new Search($options, $this->seed($block, $current), $block->isMultiple(), $block->pageSize(), $block->selectionBounds()),
FieldType::FilePicker => new FilePicker($block->pickerStart(), $this->seed($block, $current), $block->pickerConstraints(), $block->showsHidden(), $block->isMultiple(), $block->pageSize(), $block->selectionBounds()),
FieldType::Number => new Number($this->number($current), $block->numberBounds()),
FieldType::Rating => $this->rating($block, $current),
diff --git a/src/Testing/ScreenTester.php b/src/Testing/ScreenTester.php
index 47b7959f..0de1fed4 100644
--- a/src/Testing/ScreenTester.php
+++ b/src/Testing/ScreenTester.php
@@ -62,6 +62,15 @@ final class ScreenTester {
*/
protected ?ThemeInterface $theme = NULL;
+ /**
+ * The name of the theme to build, when one was named rather than given.
+ *
+ * A named theme is built in controller() rather than here, so the options
+ * and column count it reads are the ones the tester ends up configured with
+ * whatever order the fluent calls were made in.
+ */
+ protected string $themeName = '';
+
/**
* The bindings the screen answers to, once some are given.
*/
@@ -159,14 +168,16 @@ public function __construct(protected Panel $panel) {
* The tester.
*/
public function theme(ThemeInterface|string $theme): self {
- if (is_string($theme)) {
- $options = $this->themeOptions();
- $this->theme = ThemeManager::create($theme, Tui::frameWidth($options, $this->cols), $options);
- }
- else {
+ if ($theme instanceof ThemeInterface) {
$this->theme = $theme;
+ $this->themeName = '';
+
+ return $this;
}
+ $this->theme = NULL;
+ $this->themeName = $theme;
+
return $this;
}
@@ -512,7 +523,7 @@ protected function controller(): ScreenController {
// The same width resolution the facade applies, against the scripted
// terminal's columns: the theme is what every width is read off, so it
// is the one place a terminal's size is turned into a frame's.
- $this->theme ?? new DefaultTheme(Tui::frameWidth($options, $this->cols), $options),
+ $this->theme ?? $this->buildTheme($options),
$this->supplied,
$this->keys,
$this->collector,
@@ -527,6 +538,25 @@ protected function controller(): ScreenController {
);
}
+ /**
+ * Build the theme a name asked for, or the default when none was named.
+ *
+ * @param array $options
+ * The resolved display options.
+ *
+ * @return \DrevOps\Tui\Theme\ThemeInterface
+ * The theme, laid out to the scripted terminal's frame width.
+ */
+ protected function buildTheme(array $options): ThemeInterface {
+ $width = Tui::frameWidth($options, $this->cols);
+
+ if ($this->themeName === '') {
+ return new DefaultTheme($width, $options);
+ }
+
+ return ThemeManager::create($this->themeName, $width, $options);
+ }
+
/**
* The display options the theme is built from.
*
diff --git a/src/Testing/TuiTester.php b/src/Testing/TuiTester.php
index 4838c577..39825223 100644
--- a/src/Testing/TuiTester.php
+++ b/src/Testing/TuiTester.php
@@ -8,14 +8,10 @@
use DrevOps\Tui\Block\BlockInterface;
use DrevOps\Tui\Builder\Form;
use DrevOps\Tui\CancelException;
-use DrevOps\Tui\Handler\Context;
use DrevOps\Tui\Input\Key;
use DrevOps\Tui\InterruptException;
use DrevOps\Tui\Screen\Axis;
-use DrevOps\Tui\Screen\Collector;
-use DrevOps\Tui\Screen\ScreenController;
use DrevOps\Tui\Terminal\Ansi;
-use DrevOps\Tui\Theme\Border;
use DrevOps\Tui\Theme\Mode;
use DrevOps\Tui\Theme\ThemeInterface;
use DrevOps\Tui\Tui;
@@ -319,36 +315,18 @@ public function run(string|Key ...$items): Answers {
// terminal's columns.
$width = Tui::frameWidth($this->options, $this->cols);
- if ($this->themeInstance instanceof ThemeInterface) {
- // Build the controller manually when a ThemeInterface instance was
- // passed.
- $collector = new Collector($this->tui->registry(), []);
- $controller = new ScreenController(
- $this->tui->root(),
- $this->themeInstance,
- [],
- NULL,
- $collector,
- new Context($this->directory, [], $this->update, $this->version),
- 'default',
- Border::None,
- TRUE,
- TRUE,
- '',
- $this->version,
- );
- }
- else {
- $controller = $this->tui->controller(
- $this->options,
- $this->theme,
- '',
- $this->version,
- $this->directory,
- $width,
- $this->update
- );
- }
+ // A built theme is handed to the facade as it stands, so a tester passing
+ // an instance still gets the key map, fixups, layout, border and banner the
+ // facade wires for a named one.
+ $controller = $this->tui->controller(
+ $this->options,
+ $this->themeInstance ?? $this->theme,
+ '',
+ $this->version,
+ $this->directory,
+ $width,
+ $this->update
+ );
$this->cancelled = FALSE;
$this->interrupted = FALSE;
diff --git a/src/Tui.php b/src/Tui.php
index 59bdee96..3f209074 100644
--- a/src/Tui.php
+++ b/src/Tui.php
@@ -206,8 +206,9 @@ public function theme(string|\Closure $theme, array $options = []): self {
/**
* Build the selected theme and apply the consumer's overrides.
*
- * @param string $name
- * The theme name or class; empty falls back to the facade's theme.
+ * @param \DrevOps\Tui\Theme\ThemeInterface|string $name
+ * A built theme, used as it stands; or the theme name or class to build,
+ * where empty falls back to the facade's theme.
* @param int $width
* The frame width.
* @param array $options
@@ -216,8 +217,8 @@ public function theme(string|\Closure $theme, array $options = []): self {
* @return \DrevOps\Tui\Theme\ThemeInterface
* The theme.
*/
- protected function buildTheme(string $name, int $width, array $options): ThemeInterface {
- $theme = ThemeManager::create($this->resolveTheme($name), $width, $options);
+ protected function buildTheme(ThemeInterface|string $name, int $width, array $options): ThemeInterface {
+ $theme = $name instanceof ThemeInterface ? $name : ThemeManager::create($this->resolveTheme($name), $width, $options);
// Overrides apply only through a theme's own override capability, so a
// theme without it is used unchanged rather than rejected.
@@ -680,8 +681,9 @@ public function interact(string $theme = '', string $banner = '', string $versio
*
* @param array $options
* The resolved theme display options (colour, Unicode, mode).
- * @param string $theme
- * The theme name or class; empty falls back to the facade's theme.
+ * @param \DrevOps\Tui\Theme\ThemeInterface|string $theme
+ * A built theme, used as it stands; or the theme name or class to build,
+ * where empty falls back to the facade's theme.
* @param string $banner
* An optional start banner; empty falls back to the form's banner.
* @param string $version
@@ -701,7 +703,7 @@ public function interact(string $theme = '', string $banner = '', string $versio
* Public for the {@see \DrevOps\Tui\Testing\TuiTester} harness; consumers
* collect through run(), collect() or interact().
*/
- public function controller(array $options, string $theme = '', string $banner = '', string $version = '', string $directory = '', int $width = ThemeInterface::DEFAULT_WIDTH, bool $update = FALSE): ScreenController {
+ public function controller(array $options, ThemeInterface|string $theme = '', string $banner = '', string $version = '', string $directory = '', int $width = ThemeInterface::DEFAULT_WIDTH, bool $update = FALSE): ScreenController {
// Restore this facade's language before rendering (see collect()).
Translator::setShared($this->translator);
diff --git a/tests/phpunit/Unit/Theme/AbstractThemeTest.php b/tests/phpunit/Unit/Theme/AbstractThemeTest.php
index 2696c10e..b679623c 100644
--- a/tests/phpunit/Unit/Theme/AbstractThemeTest.php
+++ b/tests/phpunit/Unit/Theme/AbstractThemeTest.php
@@ -33,8 +33,8 @@ public static function dataProviderEveryStyledElementHandsBackTheStringItWasGive
yield 'field value' => [static fn(FloorTheme $t): string => $t->fieldValue('Orchard')];
yield 'field badge' => [static fn(FloorTheme $t): string => $t->fieldBadge('Orchard')];
yield 'field description' => [static fn(FloorTheme $t): string => $t->fieldDescription('Orchard')];
- yield 'field entry note' => [static fn(FloorTheme $t): string => $t->fieldOptionNote('Orchard')];
- yield 'field entry description' => [static fn(FloorTheme $t): string => $t->fieldOptionDescription('Orchard')];
+ yield 'field option note' => [static fn(FloorTheme $t): string => $t->fieldOptionNote('Orchard')];
+ yield 'field option description' => [static fn(FloorTheme $t): string => $t->fieldOptionDescription('Orchard')];
yield 'field error' => [static fn(FloorTheme $t): string => $t->fieldError('Orchard')];
yield 'field draft' => [static fn(FloorTheme $t): string => $t->fieldDraft('Orchard')];
yield 'field state' => [static fn(FloorTheme $t): string => $t->fieldState('Orchard')];
@@ -63,11 +63,11 @@ public static function dataProviderEveryGlyphFallsBackToWhatAsciiCanDraw(): \Ite
yield 'legend separator' => [static fn(FloorTheme $t): string => $t->legendSeparator()];
yield 'field selector' => [static fn(FloorTheme $t): string => $t->fieldSelector(TRUE)];
yield 'field help marker' => [static fn(FloorTheme $t): string => $t->fieldHelpMarker()];
- yield 'field entry selector' => [static fn(FloorTheme $t): string => $t->fieldOptionSelector(TRUE)];
- yield 'field entry marker chosen' => [static fn(FloorTheme $t): string => $t->fieldOptionMarker(TRUE)];
- yield 'field entry marker unchosen' => [static fn(FloorTheme $t): string => $t->fieldOptionMarker(FALSE)];
- yield 'field entry marker exclusive' => [static fn(FloorTheme $t): string => $t->fieldOptionMarker(TRUE, TRUE)];
- yield 'field entry separator' => [static fn(FloorTheme $t): string => $t->fieldOptionSeparator()];
+ yield 'field option selector' => [static fn(FloorTheme $t): string => $t->fieldOptionSelector(TRUE)];
+ yield 'field option marker chosen' => [static fn(FloorTheme $t): string => $t->fieldOptionMarker(TRUE)];
+ yield 'field option marker unchosen' => [static fn(FloorTheme $t): string => $t->fieldOptionMarker(FALSE)];
+ yield 'field option marker exclusive' => [static fn(FloorTheme $t): string => $t->fieldOptionMarker(TRUE, TRUE)];
+ yield 'field option separator' => [static fn(FloorTheme $t): string => $t->fieldOptionSeparator()];
yield 'field caret' => [static fn(FloorTheme $t): string => $t->fieldCaret()];
yield 'field mask' => [static fn(FloorTheme $t): string => $t->fieldMask()];
yield 'field loading' => [static fn(FloorTheme $t): string => $t->fieldLoading()];
@@ -94,7 +94,7 @@ public static function dataProviderEveryPassageSpanHandsBackItsText(): \Iterator
yield 'code' => [static fn(FloorTheme $t): string => $t->markupCode('Orchard')];
yield 'panel description' => [static fn(FloorTheme $t): string => $t->panelDescription('Orchard')];
yield 'panel summary' => [static fn(FloorTheme $t): string => $t->panelSummary('Orchard')];
- yield 'entry match' => [static fn(FloorTheme $t): string => $t->fieldOptionMatch('Orchard')];
+ yield 'option match' => [static fn(FloorTheme $t): string => $t->fieldOptionMatch('Orchard')];
}
public function testFloorWritesOutTargetItCannotFollow(): void {
@@ -123,7 +123,7 @@ public function testFloorKeepsOneColumnBetweenTwoThingsDrawnSideBySide(): void {
public function testGuidanceOpensWithMarkNothingCanStrip(): void {
// Neither hue nor slant survives here, and a constraint sits directly under
- // an entry's own text, so a mark is the one cue left to tell them apart.
+ // an option's own text, so a mark is the one cue left to tell them apart.
$floor = new FloorTheme();
$this->assertSame('> Orchard', $floor->fieldConstraint('Orchard'));
@@ -138,8 +138,8 @@ public function testMarkOnlyAppearsWhereThereIsSomethingToMark(): void {
$this->assertNotSame($floor->fieldOptionMarker(TRUE), $floor->fieldOptionMarker(FALSE));
}
- public function testEntryIsItsOwnTextAndTheMarkBesideItIsNot(): void {
- // Selecting and marking come apart at the floor too: the entry says what
+ public function testOptionIsItsOwnTextAndTheMarkBesideItIsNot(): void {
+ // Selecting and marking come apart at the floor too: the option says what
// it is, and the mark beside it says whether it was picked.
$floor = new FloorTheme();
From c72afdb1fd0edf032a8642df2da92763800ebffd Mon Sep 17 00:00:00 2001
From: Alex Skrypnyk
Date: Wed, 12 Aug 2026 09:28:39 +1000
Subject: [PATCH 23/27] Corrected the four documentation claims that
misdescribed what the code does.
---
src/Answers/Answers.php | 2 +-
src/Field/Suggest.php | 6 +++++-
src/Translation/Translator.php | 2 +-
src/Tui.php | 4 ++--
4 files changed, 9 insertions(+), 5 deletions(-)
diff --git a/src/Answers/Answers.php b/src/Answers/Answers.php
index f854bbed..8030c130 100644
--- a/src/Answers/Answers.php
+++ b/src/Answers/Answers.php
@@ -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
diff --git a/src/Field/Suggest.php b/src/Field/Suggest.php
index aa5e010a..325aa796 100644
--- a/src/Field/Suggest.php
+++ b/src/Field/Suggest.php
@@ -24,7 +24,11 @@
use DrevOps\Tui\Utils\Strings;
/**
- * An autocomplete text input fuzzy-filtering a fixed option set.
+ * An autocomplete text input over a set of candidate values.
+ *
+ * A declared set is ranked locally against the buffer. A query source replaces
+ * the values as its queries settle, and those arrive already answering the
+ * buffer, so they are shown in the order given.
*
* @package DrevOps\Tui\Field
*/
diff --git a/src/Translation/Translator.php b/src/Translation/Translator.php
index 8bba4152..defb4a6e 100644
--- a/src/Translation/Translator.php
+++ b/src/Translation/Translator.php
@@ -323,7 +323,7 @@ protected function load(string $language): array {
// The bundled defaults are the implicit first source, so any consumer
// source overrides them. A missing bundled directory (a trimmed archive)
- // degrades to English rather than erroring on every construction.
+ // degrades to English rather than failing at the first translation.
$bundled = self::bundledDirectory();
$sources = is_dir($bundled) ? [$bundled, ...$this->sources] : $this->sources;
diff --git a/src/Tui.php b/src/Tui.php
index 3f209074..eb053790 100644
--- a/src/Tui.php
+++ b/src/Tui.php
@@ -527,8 +527,8 @@ public function run(string $prompts = '', string $version = '', string $director
* The collected answers.
*/
public function collect(string $prompts = '', string $directory = '', bool $update = FALSE, string $version = ''): Answers {
- // Restore this facade's language at the operation boundary: another facade
- // constructed or configured meanwhile may have replaced the shared one.
+ // Restore this facade's language at the operation boundary: another
+ // facade's operation may have replaced the shared one.
Translator::setShared($this->translator);
$root = $this->root();
$inputs = (new InputResolver($this->envPrefix))->resolve(Tree::fields($root), $prompts, getenv());
From 373b2dd362dc84bc3c1bcc089e948240e6dd6f9d Mon Sep 17 00:00:00 2001
From: Alex Skrypnyk
Date: Wed, 12 Aug 2026 09:28:46 +1000
Subject: [PATCH 24/27] Required the 'is' prefix for state predicates,
exempting commands that report an outcome.
---
AGENTS.md | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/AGENTS.md b/AGENTS.md
index 36f103ed..755b52b3 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -127,6 +127,18 @@ 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 - existing examples: `isRequired()`, `isMultiple()`,
+ `isScrolling()`, `isSelectable()`, `isQueryDriven()`. This covers the
+ `has*` possession predicates too: prefer `is*` for a new one.
+
+ The single exception is a **command that reports its own outcome**. A method
+ whose job is to do something, and whose boolean says whether it happened,
+ keeps its verb - `accept()`, `capture()`, `activate()`, `load()`, `leave()`,
+ `prepare()`. Those are not questions, and an `is` prefix would misname the
+ work they do. If a method both acts and answers, it is a command, not a
+ predicate.
- **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
From 38adef309c161bae25591dbec5b7ab6344ed8aeb Mon Sep 17 00:00:00 2001
From: Alex Skrypnyk
Date: Wed, 12 Aug 2026 09:48:28 +1000
Subject: [PATCH 25/27] Finished the option rename, sparing the catalog,
directory, binding and legend entries.
---
playground/09-themes-elements.php | 10 +-
src/Block/Element/FieldElementsInterface.php | 2 +-
src/Block/Field.php | 154 +++++++++---------
src/Builder/FieldBuilder.php | 8 +-
src/Builder/Form.php | 38 ++---
src/Schema/SchemaGenerator.php | 2 +-
src/Screen/Collector.php | 14 +-
src/Theme/DefaultTheme.php | 8 +-
src/Theme/Override/FieldOverrides.php | 14 +-
src/Theme/ThemeBuilder.php | 2 +-
src/Tui.php | 2 +-
tests/phpunit/Unit/Block/CapabilityTest.php | 2 +-
tests/phpunit/Unit/Block/FieldBlockTest.php | 106 ++++++------
.../Unit/Block/FieldDeclarationTest.php | 2 +-
tests/phpunit/Unit/Block/OptionTest.php | 2 +-
tests/phpunit/Unit/ControlBytesTest.php | 10 +-
tests/phpunit/Unit/Field/FieldFactoryTest.php | 48 +++---
tests/phpunit/Unit/Screen/CollectorTest.php | 4 +-
tests/phpunit/Unit/Screen/KeyRouterTest.php | 6 +-
.../Unit/Screen/ScreenControllerTest.php | 6 +-
.../phpunit/Unit/Screen/ScreenParityTest.php | 8 +-
.../phpunit/Unit/Theme/BuiltinThemesTest.php | 10 +-
.../Unit/Theme/ElementDelegationTest.php | 10 +-
tests/phpunit/Unit/Theme/ThemeBuilderTest.php | 12 +-
tests/phpunit/Unit/Theme/ThemeTest.php | 6 +-
25 files changed, 243 insertions(+), 243 deletions(-)
diff --git a/playground/09-themes-elements.php b/playground/09-themes-elements.php
index 29d56607..6104ca32 100644
--- a/playground/09-themes-elements.php
+++ b/playground/09-themes-elements.php
@@ -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:
@@ -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.
diff --git a/src/Block/Element/FieldElementsInterface.php b/src/Block/Element/FieldElementsInterface.php
index 3abfb92a..ed57ebc9 100644
--- a/src/Block/Element/FieldElementsInterface.php
+++ b/src/Block/Element/FieldElementsInterface.php
@@ -10,7 +10,7 @@
* One field owns both of its modes, so one interface names both: the first
* group is the single line a field draws until it is opened, and the second is
* what it draws in the region it takes over once it is. A pair such as the
- * value and the draft, or the description and the entry description, is two
+ * value and the draft, or the description and the option description, is two
* elements rather than one because each says a different thing, at a different
* moment or about a different subject.
*
diff --git a/src/Block/Field.php b/src/Block/Field.php
index cc5fc5a0..77d016bb 100644
--- a/src/Block/Field.php
+++ b/src/Block/Field.php
@@ -104,7 +104,7 @@ final class Field extends AbstractBlock implements
*
* @var list<\DrevOps\Tui\Block\Option>
*/
- protected array $entries = [];
+ protected array $options = [];
/**
* What owes the field one set of rows, resolved once.
@@ -699,7 +699,7 @@ public function valueType(): string {
}
/**
- * A value restated against the entries as they now stand.
+ * A value restated against the options as they now stand.
*
* A choice outlives the rows it was picked from: a set that follows the
* answers narrows as they change, leaving a value that is no longer offered,
@@ -715,7 +715,7 @@ public function valueType(): string {
* The current value.
*
* @return mixed
- * The value the current entries can carry.
+ * The value the current options can carry.
*/
public function reconcileValue(mixed $value): mixed {
if (!$this->fieldType->supportsOptions() || $this->fieldType === FieldType::Suggest) {
@@ -923,7 +923,7 @@ public function hasSchemaDefault(): bool {
}
/**
- * Offer an entry for edit mode to open onto.
+ * Offer an option for edit mode to open onto.
*
* Declaring a value twice replaces the row in place, so the set stays unique
* and stays in the order it was first declared in.
@@ -933,35 +933,35 @@ public function hasSchemaDefault(): bool {
* @param string $label
* The label it draws; empty draws the value.
* @param string $description
- * What the entry means, shown beside the list.
+ * What the option means, shown beside the list.
* @param bool $disabled
- * Whether the entry is drawn but cannot be picked.
+ * Whether the option is drawn but cannot be picked.
* @param string $disabled_reason
* Why it cannot be picked.
*
* @return static
* The field.
*/
- public function entry(string $value, string $label = '', string $description = '', bool $disabled = FALSE, string $disabled_reason = ''): static {
- $entry = new Option($value, $label === '' ? $value : $label, $description, OptionType::Option, $disabled, $disabled_reason);
+ public function option(string $value, string $label = '', string $description = '', bool $disabled = FALSE, string $disabled_reason = ''): static {
+ $option = new Option($value, $label === '' ? $value : $label, $description, OptionType::Option, $disabled, $disabled_reason);
- foreach ($this->entries as $index => $existing) {
+ foreach ($this->options as $index => $existing) {
// Option filters the value it is given, so the match is made against the
// filtered form rather than the argument.
- if ($existing->kind === OptionType::Option && $existing->value === $entry->value) {
- $this->entries[$index] = $entry;
+ if ($existing->kind === OptionType::Option && $existing->value === $option->value) {
+ $this->options[$index] = $option;
return $this;
}
}
- $this->entries[] = $entry;
+ $this->options[] = $option;
return $this;
}
/**
- * Head the entries that follow with a group label.
+ * Head the options that follow with a group label.
*
* @param string $label
* The heading.
@@ -970,19 +970,19 @@ public function entry(string $value, string $label = '', string $description = '
* The field.
*/
public function heading(string $label): static {
- $this->entries[] = new Option('', $label, '', OptionType::Heading);
+ $this->options[] = new Option('', $label, '', OptionType::Heading);
return $this;
}
/**
- * Divide the entries either side of it.
+ * Divide the options either side of it.
*
* @return static
* The field.
*/
public function separator(): static {
- $this->entries[] = new Option('', '', '', OptionType::Separator);
+ $this->options[] = new Option('', '', '', OptionType::Separator);
return $this;
}
@@ -994,11 +994,11 @@ public function separator(): static {
* The rows, in the order they were declared.
*/
public function options(): array {
- return $this->entries;
+ return $this->options;
}
/**
- * The entry standing for a value.
+ * The option standing for a value.
*
* A row is found by the value it carries rather than by where it sits, so a
* numeric-looking value stays the string it was declared as.
@@ -1007,15 +1007,15 @@ public function options(): array {
* The value.
*
* @return \DrevOps\Tui\Block\Option|null
- * The entry, or NULL when nothing carries that value. Headings and
+ * The option, or NULL when nothing carries that value. Headings and
* separators carry none and are never returned.
*/
public function optionOf(string $value): ?Option {
$value = Ansi::sanitize($value);
- foreach ($this->entries as $entry) {
- if ($entry->kind === OptionType::Option && $entry->value === $value) {
- return $entry;
+ foreach ($this->options as $option) {
+ if ($option->kind === OptionType::Option && $option->value === $value) {
+ return $option;
}
}
@@ -1026,14 +1026,14 @@ public function optionOf(string $value): ?Option {
* The values that can be picked, in the order they are drawn.
*
* @return list
- * The values, excluding headings, separators and disabled entries.
+ * The values, excluding headings, separators and disabled options.
*/
public function selectableValues(): array {
- return Option::selectableValues($this->entries);
+ return Option::selectableValues($this->options);
}
/**
- * Load the entries on demand, once.
+ * Load the options on demand, once.
*
* @param \Closure $loader
* An `fn (): array` returning the value => label map.
@@ -1048,17 +1048,17 @@ public function load(\Closure $loader): static {
}
/**
- * What owes the field one set of entries.
+ * What owes the field one set of options.
*
* @return \Closure|null
- * The loader, or NULL when the entries stand as declared.
+ * The loader, or NULL when the options stand as declared.
*/
public function loader(): ?\Closure {
return $this->loader;
}
/**
- * Resolve the entries from the answers, again whenever they change.
+ * Resolve the options from the answers, again whenever they change.
*
* @param \Closure $resolver
* An `fn (\DrevOps\Tui\Handler\Context $context): array`
@@ -1075,17 +1075,17 @@ public function resolve(\Closure $resolver): static {
}
/**
- * What resolves the entries from the answers.
+ * What resolves the options from the answers.
*
* @return \Closure|null
- * The resolver, or NULL when the entries do not follow the answers.
+ * The resolver, or NULL when the options do not follow the answers.
*/
public function resolver(): ?\Closure {
return $this->resolver;
}
/**
- * Resolve the entries from a live query, again whenever it changes.
+ * Resolve the options from a live query, again whenever it changes.
*
* Unlike a loader it is asked again as the query changes, so the candidates
* can come from a backend that filters for itself.
@@ -1105,10 +1105,10 @@ public function query(\Closure $source): static {
}
/**
- * What resolves the entries from a live query.
+ * What resolves the options from a live query.
*
* @return \Closure|null
- * The source, or NULL when the entries do not follow a query.
+ * The source, or NULL when the options do not follow a query.
*/
public function source(): ?\Closure {
return $this->source;
@@ -1147,18 +1147,18 @@ public function queryMinLength(): int {
}
/**
- * Replace the entries with a set a loader, resolver or query resolved to.
+ * Replace the options with a set a loader, resolver or query resolved to.
*
- * @param mixed $entries
+ * @param mixed $options
* What the callable returned; anything but a map of strings settles to no
- * entries, because consumer code running mid-session has no good way to
+ * options, because consumer code running mid-session has no good way to
* report a mistake.
*
* @return static
* The field.
*/
- public function settle(mixed $entries): static {
- $this->entries = Option::resolved($entries);
+ public function settle(mixed $options): static {
+ $this->options = Option::resolved($options);
// A loader owes the field one list and has now given it; a resolver and a
// query source answer again as their input changes, so they stand.
$this->loader = NULL;
@@ -1167,7 +1167,7 @@ public function settle(mixed $entries): static {
}
/**
- * Whether the entries stand as declared.
+ * Whether the options stand as declared.
*
* @return bool
* FALSE while a loader, a resolver or a query source still owes them, so
@@ -1178,13 +1178,13 @@ public function hasSettledOptions(): bool {
}
/**
- * Whether the entries follow the answers rather than standing.
+ * Whether the options follow the answers rather than standing.
*
* @return bool
* TRUE when they are resolved from the answers or from a live query, so no
* one list describes the field.
*/
- public function hasDynamicEntries(): bool {
+ public function hasDynamicOptions(): bool {
return $this->resolver instanceof \Closure || $this->source instanceof \Closure;
}
@@ -1453,7 +1453,7 @@ public function ratingCaptions(): array {
}
/**
- * The reason a value is not among the entries, as a fragment, else NULL.
+ * The reason a value is not among the options, as a fragment, else NULL.
*
* A fragment rather than a sentence, so each caller frames it its own way.
*
@@ -1462,18 +1462,18 @@ public function ratingCaptions(): array {
*
* @return string|null
* The fragment, or NULL when nothing constrains the value or every item is
- * among the entries.
+ * among the options.
*/
public function optionViolation(mixed $value): ?string {
- // A field that declares no entries constrains nothing - but one whose
- // entries follow a query or the answers is constrained by whatever they
+ // A field that declares no options constrains nothing - but one whose
+ // options follow a query or the answers is constrained by whatever they
// resolved to, and resolving to nothing means the value does not exist.
- if (!$this->fieldType->constrainsToOptions() || ($this->entries === [] && !$this->hasDynamicEntries())) {
+ if (!$this->fieldType->constrainsToOptions() || ($this->options === [] && !$this->hasDynamicOptions())) {
return NULL;
}
if (!$this->isMultiChoice()) {
- return $this->scalarEntryViolation(is_scalar($value) ? (string) $value : '');
+ return $this->scalarOptionViolation(is_scalar($value) ? (string) $value : '');
}
if (!is_array($value)) {
@@ -1481,7 +1481,7 @@ public function optionViolation(mixed $value): ?string {
}
foreach ($value as $item) {
- $error = $this->scalarEntryViolation(is_scalar($item) ? (string) $item : '');
+ $error = $this->scalarOptionViolation(is_scalar($item) ? (string) $item : '');
if ($error !== NULL) {
return $error;
@@ -2046,7 +2046,7 @@ public static function canonicalOrder(array $allowed, array $desired): array {
}
/**
- * The reason one value is not among the entries, as a fragment, else NULL.
+ * The reason one value is not among the options, as a fragment, else NULL.
*
* @param string $value
* The candidate value.
@@ -2055,7 +2055,7 @@ public static function canonicalOrder(array $allowed, array $desired): array {
* The fragment naming the value when it is disabled or unknown, or NULL
* when it can be picked.
*/
- protected function scalarEntryViolation(string $value): ?string {
+ protected function scalarOptionViolation(string $value): ?string {
if (in_array($value, $this->selectableValues(), TRUE)) {
return NULL;
}
@@ -2063,20 +2063,20 @@ protected function scalarEntryViolation(string $value): ?string {
// Listing what is allowed is what makes the message useful, so when a query
// found nothing there is no list to offer and naming the value is all that
// can honestly be said.
- if ($this->entries === []) {
+ if ($this->options === []) {
return Translator::t('value "@value" was not found', ['@value' => $value]);
}
- $entry = $this->optionOf($value);
+ $option = $this->optionOf($value);
- if ($entry instanceof Option && $entry->disabled) {
- if ($entry->disabledReason === '') {
+ if ($option instanceof Option && $option->disabled) {
+ if ($option->disabledReason === '') {
return Translator::t('option "@value" is disabled', ['@value' => $value]);
}
return Translator::t('option "@value" is disabled: @reason', [
'@value' => $value,
- '@reason' => $entry->disabledReason,
+ '@reason' => $option->disabledReason,
]);
}
@@ -2087,16 +2087,16 @@ protected function scalarEntryViolation(string $value): ?string {
}
/**
- * The reason a ranking is not a full ordering of the entries, else NULL.
+ * The reason a ranking is not a full ordering of the options, else NULL.
*
* Membership is checked by the caller, so a ranking that is a full ordering
- * has as many items as there are entries, with no repeats.
+ * has as many items as there are options, with no repeats.
*
* @param array $items
* The ranking, already known to hold only values that can be picked.
*
* @return string|null
- * The fragment, or NULL when the ranking covers every entry once.
+ * The fragment, or NULL when the ranking covers every option once.
*/
protected function rankingViolation(array $items): ?string {
$selectable = $this->selectableValues();
@@ -2328,61 +2328,61 @@ protected function valueRegion(ThemeInterface $theme, FieldElementsInterface $el
return explode("\n", $this->editor->view($theme));
}
- if ($this->entries === []) {
+ if ($this->options === []) {
return [$elements->fieldValue($this->readable($theme, $elements))];
}
- return array_map(fn(Option $entry): string => $this->optionLine($elements, $entry), $this->entries);
+ return array_map(fn(Option $option): string => $this->optionLine($elements, $option), $this->options);
}
/**
- * One entry as it is drawn.
+ * One option as it is drawn.
*
* @param \DrevOps\Tui\Block\Element\FieldElementsInterface $elements
* The theme narrowed to the field elements.
- * @param \DrevOps\Tui\Block\Option $entry
- * The entry.
+ * @param \DrevOps\Tui\Block\Option $option
+ * The option.
*
* @return string
- * The drawn entry; empty for a divider, which is a gap and nothing else.
+ * The drawn option; empty for a divider, which is a gap and nothing else.
*/
- protected function optionLine(FieldElementsInterface $elements, Option $entry): string {
- if ($entry->kind === OptionType::Heading) {
- return $elements->fieldCaption($entry->label);
+ protected function optionLine(FieldElementsInterface $elements, Option $option): string {
+ if ($option->kind === OptionType::Heading) {
+ return $elements->fieldCaption($option->label);
}
- if ($entry->kind === OptionType::Separator) {
+ if ($option->kind === OptionType::Separator) {
return $elements->fieldOptionSeparator();
}
// Marking and naming are two elements: the mark records what was picked
// and the text says what it was, so a theme can restyle either alone.
- $chosen = $this->isChosen($entry);
- $line = $elements->fieldOptionMarker($chosen, !$this->multiple) . ' ' . $elements->fieldOption($entry->label, $chosen);
+ $chosen = $this->isChosen($option);
+ $line = $elements->fieldOptionMarker($chosen, !$this->multiple) . ' ' . $elements->fieldOption($option->label, $chosen);
// Why an option cannot be picked belongs beside it, or a row that is drawn
// and refuses the cursor reads as a fault rather than a decision.
- return $entry->disabled && $entry->disabledReason !== '' ? $line . ' ' . $elements->fieldOptionNote($entry->disabledReason) : $line;
+ return $option->disabled && $option->disabledReason !== '' ? $line . ' ' . $elements->fieldOptionNote($option->disabledReason) : $line;
}
/**
- * Whether an entry is the one picked.
+ * Whether an option is the one picked.
*
- * @param \DrevOps\Tui\Block\Option $entry
- * The entry.
+ * @param \DrevOps\Tui\Block\Option $option
+ * The option.
*
* @return bool
* TRUE when the answer being drawn holds its value.
*/
- protected function isChosen(Option $entry): bool {
+ protected function isChosen(Option $option): bool {
$shown = $this->mode === Mode::Edit ? $this->draft : $this->value;
if (!is_array($shown)) {
- return is_scalar($shown) && (string) $shown === $entry->value;
+ return is_scalar($shown) && (string) $shown === $option->value;
}
foreach ($shown as $item) {
- if (is_scalar($item) && (string) $item === $entry->value) {
+ if (is_scalar($item) && (string) $item === $option->value) {
return TRUE;
}
}
diff --git a/src/Builder/FieldBuilder.php b/src/Builder/FieldBuilder.php
index a2fca0db..f3d27b7a 100644
--- a/src/Builder/FieldBuilder.php
+++ b/src/Builder/FieldBuilder.php
@@ -989,7 +989,7 @@ public function slot(string $name, string $label = '', ?\Closure $validate = NUL
*/
public function option(string $value, string $label = '', string $description = '', bool $disabled = FALSE, string $disabled_reason = ''): self {
$this->scoped(self::lists(), 'shows no options', 'option');
- $this->block->entry($value, $label, $description, $disabled, $disabled_reason);
+ $this->block->option($value, $label, $description, $disabled, $disabled_reason);
return $this;
}
@@ -1269,10 +1269,10 @@ protected function resolveDefault(): mixed {
// option's value rather than an empty value that would not match either.
// The value is read off the option, not its array key, so a numeric-string
// value like "0" is not coerced to an int.
- $entries = $this->block->options();
+ $options = $this->block->options();
- if ($this->fieldType === FieldType::Toggle && $entries !== []) {
- return reset($entries)->value;
+ if ($this->fieldType === FieldType::Toggle && $options !== []) {
+ return reset($options)->value;
}
return $this->defaultFor($this->fieldType);
diff --git a/src/Builder/Form.php b/src/Builder/Form.php
index 6360e422..1ff3cb39 100644
--- a/src/Builder/Form.php
+++ b/src/Builder/Form.php
@@ -263,9 +263,9 @@ public function root(): Panel {
$this->assertUniqueFieldIds($root);
$this->assertFieldSurfaces($root);
- $this->assertEntrySources($root);
- $this->assertToggleEntries($root);
- $this->assertReorderEntries($root);
+ $this->assertOptionSources($root);
+ $this->assertToggleOptions($root);
+ $this->assertReorderOptions($root);
$this->assertTemplateShapes($root);
$this->assertModalPanels($root);
@@ -478,7 +478,7 @@ protected function assertFieldSurfaces(Panel $root): void {
/**
* Assert that every field is offered exactly one set of rows it can hold.
*
- * A field's rows come from one of four sources - declared entries, a
+ * A field's rows come from one of four sources - declared options, a
* loader, a resolver or a query source - and each replaces the others. A
* field offered two of them has no one set, and a field offered any of them
* on a kind that shows no list has nowhere to put them.
@@ -486,7 +486,7 @@ protected function assertFieldSurfaces(Panel $root): void {
* @param \DrevOps\Tui\Block\Panel $root
* The root panel holding every declared panel.
*/
- protected function assertEntrySources(Panel $root): void {
+ protected function assertOptionSources(Panel $root): void {
foreach (Tree::fields($root) as $field) {
$offered = [
$field->options() !== [],
@@ -514,12 +514,12 @@ protected function assertEntrySources(Panel $root): void {
}
/**
- * Assert that every toggle field declares exactly two entries.
+ * Assert that every toggle field declares exactly two options.
*
* @param \DrevOps\Tui\Block\Panel $root
* The root panel holding every declared panel.
*/
- protected function assertToggleEntries(Panel $root): void {
+ protected function assertToggleOptions(Panel $root): void {
foreach (Tree::fields($root) as $field) {
if ($field->type() !== FieldType::Toggle) {
continue;
@@ -529,10 +529,10 @@ protected function assertToggleEntries(Panel $root): void {
continue;
}
- $entries = $field->options();
+ $options = $field->options();
- if (count($entries) !== 2) {
- throw new FormException(sprintf('Toggle field "%s" must have exactly two options, %d given.', $field->id(), count($entries)));
+ if (count($options) !== 2) {
+ throw new FormException(sprintf('Toggle field "%s" must have exactly two options, %d given.', $field->id(), count($options)));
}
$default = $field->value();
@@ -544,7 +544,7 @@ protected function assertToggleEntries(Panel $root): void {
continue;
}
- $values = array_map(static fn(Option $entry): string => $entry->value, $entries);
+ $values = array_map(static fn(Option $option): string => $option->value, $options);
if (!is_string($default) || !in_array($default, $values, TRUE)) {
throw new FormException(sprintf('Toggle field "%s" default must be one of: %s.', $field->id(), implode(', ', $values)));
@@ -553,7 +553,7 @@ protected function assertToggleEntries(Panel $root): void {
}
/**
- * Assert that every reorder field declares at least two plain entries.
+ * Assert that every reorder field declares at least two plain options.
*
* A ranking arranges a flat list, so headings, separators and disabled rows
* are not allowed in it, and fewer than two items leaves nothing to reorder.
@@ -561,7 +561,7 @@ protected function assertToggleEntries(Panel $root): void {
* @param \DrevOps\Tui\Block\Panel $root
* The root panel holding every declared panel.
*/
- protected function assertReorderEntries(Panel $root): void {
+ protected function assertReorderOptions(Panel $root): void {
foreach (Tree::fields($root) as $field) {
if ($field->type() !== FieldType::Reorder) {
continue;
@@ -571,16 +571,16 @@ protected function assertReorderEntries(Panel $root): void {
continue;
}
- $entries = $field->options();
+ $options = $field->options();
- foreach ($entries as $entry) {
- if (!$entry->isSelectable()) {
+ foreach ($options as $option) {
+ if (!$option->isSelectable()) {
throw new FormException(sprintf('Reorder field "%s" allows only plain options - no headings, separators or disabled rows.', $field->id()));
}
}
- if (count($entries) < 2) {
- throw new FormException(sprintf('Reorder field "%s" must have at least two options, %d given.', $field->id(), count($entries)));
+ if (count($options) < 2) {
+ throw new FormException(sprintf('Reorder field "%s" must have at least two options, %d given.', $field->id(), count($options)));
}
}
}
@@ -622,7 +622,7 @@ protected function assertModalPanels(Panel $root): void {
}
/**
- * Whether a field's entries stand as declared.
+ * Whether a field's options stand as declared.
*
* @param \DrevOps\Tui\Block\Field $field
* The field.
diff --git a/src/Schema/SchemaGenerator.php b/src/Schema/SchemaGenerator.php
index cc11e23b..5556efb4 100644
--- a/src/Schema/SchemaGenerator.php
+++ b/src/Schema/SchemaGenerator.php
@@ -76,7 +76,7 @@ public function generate(): array {
'help' => $field->helpText(),
'placeholder' => $field->placeholderText(),
'options' => $this->options($field),
- 'options_dynamic' => $field->hasDynamicEntries(),
+ 'options_dynamic' => $field->hasDynamicOptions(),
'default' => DefaultResolver::resolve($field, $this->context),
'required' => $field->isRequired(),
'env' => $names->isAdvertisable($field) ? $names->canonical($field) : NULL,
diff --git a/src/Screen/Collector.php b/src/Screen/Collector.php
index 8ba0c05f..3685886f 100644
--- a/src/Screen/Collector.php
+++ b/src/Screen/Collector.php
@@ -259,7 +259,7 @@ public function load(Panel $panel, ?\Closure $waiting = NULL): bool {
$waiting();
}
- $this->loadEntries($owed);
+ $this->loadOptions($owed);
return TRUE;
}
@@ -298,11 +298,11 @@ public function reusable(string $id): array {
protected function fetched(Panel $panel, array $supplied, Context $context): array {
// With no cursor to open a field, nothing would ever ask a loader for its
// rows - and a value cannot be measured against rows that never arrive.
- $this->loadEntries(Tree::fields($panel));
+ $this->loadOptions(Tree::fields($panel));
[$fields, $values, $sources, $active] = $this->settle($panel, $supplied, $context);
- $this->loadQueryEntries($fields, $values, $active);
+ $this->loadQueryOptions($fields, $values, $active);
return [$fields, $values, $sources, $active];
}
@@ -594,7 +594,7 @@ protected function stabilize(Panel $panel, array $shows, array $fields, array $v
for ($pass = 0; $pass <= $limit; $pass++) {
// Rows resolve first: a set that follows the answers decides what the
// conditions below then see, and what a value is still allowed to be.
- $values = $this->resolveEntries($fields, $values, $active, $context, $supplied);
+ $values = $this->resolveOptions($fields, $values, $active, $context, $supplied);
$derived = $this->deriver->derive($rules, $values, $pinned);
@@ -629,7 +629,7 @@ protected function stabilize(Panel $panel, array $shows, array $fields, array $v
* @param list<\DrevOps\Tui\Block\Field> $fields
* The fields, in declaration order.
*/
- protected function loadEntries(array $fields): void {
+ protected function loadOptions(array $fields): void {
foreach ($fields as $field) {
$loader = $field->loader();
@@ -667,7 +667,7 @@ protected function loadEntries(array $fields): void {
* @throws \DrevOps\Tui\CollectException
* When a resolver cannot answer.
*/
- protected function resolveEntries(array $fields, array $values, array $active, Context $context, array $supplied): array {
+ protected function resolveOptions(array $fields, array $values, array $active, Context $context, array $supplied): array {
$answers = $this->activeAnswers($fields, $values, $active);
$resolved = new Context($context->directory, $answers, $context->update, $context->version);
@@ -733,7 +733,7 @@ protected function resolveEntries(array $fields, array $values, array $active, C
* @throws \DrevOps\Tui\CollectException
* When a source cannot answer.
*/
- protected function loadQueryEntries(array $fields, array $values, array $active): void {
+ protected function loadQueryOptions(array $fields, array $values, array $active): void {
foreach ($fields as $field) {
$source = $field->source();
diff --git a/src/Theme/DefaultTheme.php b/src/Theme/DefaultTheme.php
index 2c5a4446..e4582b18 100644
--- a/src/Theme/DefaultTheme.php
+++ b/src/Theme/DefaultTheme.php
@@ -897,7 +897,7 @@ public function fieldOptionSelector(bool $selected): string {
public function fieldOptionMarker(bool $chosen, bool $exclusive = FALSE): string {
$glyph = $this->overriddenGlyph(ThemeElement::FieldOptionMarker);
- // Only the picked state is stated, so an entry nobody picked keeps the
+ // Only the picked state is stated, so an option nobody picked keeps the
// mark the theme draws for it and the patch stays a patch.
if ($glyph !== NULL && $chosen) {
return $exclusive ? $this->paint($this->accent(), $glyph) : $this->value($glyph);
@@ -927,8 +927,8 @@ public function fieldOptionNote(string $text): string {
#[\Override]
public function fieldOptionDescription(string $text): string {
// Slanted against the description's grey: it says the same kind of thing
- // about a smaller subject, and the slant marks it as belonging to the entry
- // above rather than to the field.
+ // about a smaller subject, and the slant marks it as belonging to the
+ // option above rather than to the field.
return $this->paint(Sgr::of(Sgr::Italic, Sgr::Grey), $this->linkify($text));
}
@@ -954,7 +954,7 @@ public function fieldOverflowMarker(bool $above): string {
#[\Override]
public function fieldConstraint(string $text): string {
// Guidance on how to answer is never louder than the question itself, but
- // still reads as its own voice - and it sits directly beneath an entry's
+ // still reads as its own voice - and it sits directly beneath an option's
// own description, so the two must not be mistaken for each other on any
// surface. Slant reinforces the hue where the surface honours it, and where
// neither survives the voice falls back to a mark, which nothing can strip.
diff --git a/src/Theme/Override/FieldOverrides.php b/src/Theme/Override/FieldOverrides.php
index 68900685..1fb27de9 100644
--- a/src/Theme/Override/FieldOverrides.php
+++ b/src/Theme/Override/FieldOverrides.php
@@ -8,7 +8,7 @@
* The field's elements, as a consumer patches them.
*
* The block's prefix is implied by the group, so `selector()` here is the
- * field's selector and nothing else's - and the entry's is a call of its own,
+ * field's selector and nothing else's - and the option's is a call of its own,
* because the two are different marks.
*
* @package DrevOps\Tui\Theme\Override
@@ -79,7 +79,7 @@ public function valueSeparator(string $text): self {
}
/**
- * Draw the mark saying which entry has focus with this glyph.
+ * Draw the mark saying which option has focus with this glyph.
*
* @param string $glyph
* The glyph.
@@ -89,17 +89,17 @@ public function valueSeparator(string $text): self {
* @return $this
* The group.
*/
- public function entrySelector(string $glyph, string $ascii): self {
+ public function optionSelector(string $glyph, string $ascii): self {
$this->overrides->setGlyph(ThemeElement::FieldOptionSelector, $glyph, $ascii);
return $this;
}
/**
- * Draw the mark recording a picked entry with this glyph.
+ * Draw the mark recording a picked option with this glyph.
*
- * The mark an entry carries once it is picked; an entry nobody picked keeps
- * whatever the theme draws for it, so a patch stays a patch.
+ * The mark an option carries once it is picked; an option nobody picked
+ * keeps whatever the theme draws for it, so a patch stays a patch.
*
* @param string $glyph
* The glyph.
@@ -109,7 +109,7 @@ public function entrySelector(string $glyph, string $ascii): self {
* @return $this
* The group.
*/
- public function entryMarker(string $glyph, string $ascii): self {
+ public function optionMarker(string $glyph, string $ascii): self {
$this->overrides->setGlyph(ThemeElement::FieldOptionMarker, $glyph, $ascii);
return $this;
diff --git a/src/Theme/ThemeBuilder.php b/src/Theme/ThemeBuilder.php
index 0cd9ea60..070141a9 100644
--- a/src/Theme/ThemeBuilder.php
+++ b/src/Theme/ThemeBuilder.php
@@ -28,7 +28,7 @@
* ->selector('❯', '>')
* ->helpMarker('ⁱ', '[?]')
* ->valueSeparator(', ')
- * ->entryMarker('◼', '[x]')
+ * ->optionMarker('◼', '[x]')
* ->caret('█', '|'))
* ->overrides();
* @endcode
diff --git a/src/Tui.php b/src/Tui.php
index eb053790..2e5db931 100644
--- a/src/Tui.php
+++ b/src/Tui.php
@@ -812,7 +812,7 @@ public function registry(): HandlerRegistry {
/**
* The declared block tree: the panel every declared panel hangs from.
*
- * The rows a form asks about are state: a set of entries supplied from
+ * The rows a form asks about are state: a set of options supplied from
* elsewhere is stored on the block holding it, so one tree carries the
* declaration and its state. Every operation on this facade reads that one
* tree.
diff --git a/tests/phpunit/Unit/Block/CapabilityTest.php b/tests/phpunit/Unit/Block/CapabilityTest.php
index b1b17bd9..2765cd18 100644
--- a/tests/phpunit/Unit/Block/CapabilityTest.php
+++ b/tests/phpunit/Unit/Block/CapabilityTest.php
@@ -221,7 +221,7 @@ public function testOpenFieldBindsEveryPrintableKeyAndClosedOneBindsNone(): void
}
public function testOpenFieldTakesPrintableKeyOnlyWhereItsKindTakesTypedInput(): void {
- $basket = (new Field('basket', 'Basket contents', FieldType::Select))->entry('apple', 'Apple')->open();
+ $basket = (new Field('basket', 'Basket contents', FieldType::Select))->option('apple', 'Apple')->open();
// A single choice is walked with the cursor rather than typed into, so a
// printable key is not something being typed and travels outward.
diff --git a/tests/phpunit/Unit/Block/FieldBlockTest.php b/tests/phpunit/Unit/Block/FieldBlockTest.php
index c8912375..eb4ff935 100644
--- a/tests/phpunit/Unit/Block/FieldBlockTest.php
+++ b/tests/phpunit/Unit/Block/FieldBlockTest.php
@@ -57,14 +57,14 @@ public function testOpeningFieldSwitchesItToEditMode(): void {
}
public function testTheLabelStaysPutAcrossBothModes(): void {
- $field = (new Field('basket', 'Basket', FieldType::Select))->entry('apple', 'Apple');
+ $field = (new Field('basket', 'Basket', FieldType::Select))->option('apple', 'Apple');
$this->assertStringStartsWith(' Basket', $field->render($this->theme()));
$this->assertStringStartsWith(' Basket', $field->open()->render($this->theme()));
}
- public function testEditModeOpensOntoTheEntriesItWasGiven(): void {
- $field = (new Field('basket', 'Basket', FieldType::Select))->entry('apple', 'Apple')->entry('carrot', 'Carrot');
+ public function testEditModeOpensOntoTheOptionsItWasGiven(): void {
+ $field = (new Field('basket', 'Basket', FieldType::Select))->option('apple', 'Apple')->option('carrot', 'Carrot');
$rendered = $field->open()->render($this->theme());
@@ -74,7 +74,7 @@ public function testEditModeOpensOntoTheEntriesItWasGiven(): void {
public function testOpeningFieldHandsTheValueRegionToTheEditorItsKindOpensOnto(): void {
$courier = (new Field('courier', 'Courier'))->default('Valley Runs');
- $basket = (new Field('basket', 'Basket', FieldType::Select))->entry('apple', 'Apple');
+ $basket = (new Field('basket', 'Basket', FieldType::Select))->option('apple', 'Apple');
$this->assertNotInstanceOf(FieldInterface::class, $courier->editor());
@@ -134,11 +134,11 @@ public function testTypingIntoAnOpenFieldIsWhatFillsTheValueRegion(): void {
$this->assertSame(Mode::View, $field->mode());
}
- public function testSpaceTogglesAnEntryOfAnOpenMultipleSelect(): void {
+ public function testSpaceTogglesAnOptionOfAnOpenMultipleSelect(): void {
$field = (new Field('basket', 'Basket contents', FieldType::Select))
->multiple()
- ->entry('apple', 'Apple')
- ->entry('carrot', 'Carrot')
+ ->option('apple', 'Apple')
+ ->option('carrot', 'Carrot')
->open();
// Space belongs to the kind rather than to whatever sends the key: the
@@ -556,25 +556,25 @@ public static function dataProviderAnswerIsListWhenTheFieldCollectsOne(): \Itera
yield 'a ranking' => [new Field('order', 'Order', FieldType::Reorder), TRUE, TRUE];
}
- public function testEntriesAreDrawnInTheOrderTheyWereDeclared(): void {
+ public function testOptionsAreDrawnInTheOrderTheyWereDeclared(): void {
$field = (new Field('basket', 'Basket contents', FieldType::Select))
->heading('Fruit')
- ->entry('apple', 'Apple')
+ ->option('apple', 'Apple')
->separator()
->heading('Vegetables')
- ->entry('carrot', 'Carrot');
+ ->option('carrot', 'Carrot');
- $kinds = array_map(static fn(object $entry): OptionType => $entry->kind, $field->options());
+ $kinds = array_map(static fn(object $option): OptionType => $option->kind, $field->options());
$expected = [OptionType::Heading, OptionType::Option, OptionType::Separator, OptionType::Heading, OptionType::Option];
$this->assertSame($expected, $kinds);
$this->assertSame(['apple', 'carrot'], $field->selectableValues());
}
- public function testEntryValueStaysTheStringItWasDeclaredAs(): void {
+ public function testOptionValueStaysTheStringItWasDeclaredAs(): void {
// Rows are held as a list carrying their own value, so a numeric-looking
// value is never coerced the way an array key would be.
- $field = (new Field('grade', 'Grade', FieldType::Select))->entry('0', 'Unripe')->entry('1', 'Ripe');
+ $field = (new Field('grade', 'Grade', FieldType::Select))->option('0', 'Unripe')->option('1', 'Ripe');
$this->assertSame(['0', '1'], $field->selectableValues());
$this->assertSame('Unripe', $field->optionOf('0')?->label);
@@ -582,97 +582,97 @@ public function testEntryValueStaysTheStringItWasDeclaredAs(): void {
$this->assertSame('0', $field->value());
}
- public function testDeclaringValueTwiceReplacesTheEntryWhereItAlreadySits(): void {
+ public function testDeclaringValueTwiceReplacesTheOptionWhereItAlreadySits(): void {
$field = (new Field('basket', 'Basket contents', FieldType::Select))
- ->entry('apple', 'Apple')
- ->entry('carrot', 'Carrot')
- ->entry('apple', 'Golden Apple');
+ ->option('apple', 'Apple')
+ ->option('carrot', 'Carrot')
+ ->option('apple', 'Golden Apple');
$this->assertCount(2, $field->options());
$this->assertSame('Golden Apple', $field->optionOf('apple')?->label);
$this->assertSame(['apple', 'carrot'], $field->selectableValues());
}
- public function testEntryWithNoLabelDrawsItsOwnValue(): void {
- $field = (new Field('basket', 'Basket contents', FieldType::Select))->entry('apple');
+ public function testOptionWithNoLabelDrawsItsOwnValue(): void {
+ $field = (new Field('basket', 'Basket contents', FieldType::Select))->option('apple');
$this->assertSame('apple', $field->optionOf('apple')?->label);
$this->assertNotInstanceOf(Option::class, $field->optionOf('carrot'));
}
- #[DataProvider('dataProviderValueOutsideTheEntriesIsRefused')]
- public function testValueOutsideTheEntriesIsRefused(Field $field, mixed $value, ?string $error): void {
+ #[DataProvider('dataProviderValueOutsideTheOptionsIsRefused')]
+ public function testValueOutsideTheOptionsIsRefused(Field $field, mixed $value, ?string $error): void {
$this->assertSame($error, $field->optionViolation($value));
}
- public static function dataProviderValueOutsideTheEntriesIsRefused(): \Iterator {
+ public static function dataProviderValueOutsideTheOptionsIsRefused(): \Iterator {
$basket = static fn(): Field => (new Field('basket', 'Basket contents', FieldType::Select))
- ->entry('apple', 'Apple')
- ->entry('carrot', 'Carrot', disabled: TRUE, disabled_reason: 'Out of season.')
- ->entry('turnip', 'Turnip', disabled: TRUE);
+ ->option('apple', 'Apple')
+ ->option('carrot', 'Carrot', disabled: TRUE, disabled_reason: 'Out of season.')
+ ->option('turnip', 'Turnip', disabled: TRUE);
yield 'a listed value' => [$basket(), 'apple', NULL];
yield 'an unlisted value' => [$basket(), 'plum', 'value "plum" is not one of: apple'];
yield 'a disabled value' => [$basket(), 'carrot', 'option "carrot" is disabled: Out of season.'];
yield 'a disabled value with no reason' => [$basket(), 'turnip', 'option "turnip" is disabled'];
- yield 'no entries at all' => [new Field('basket', 'Basket', FieldType::Select), 'plum', NULL];
+ yield 'no options at all' => [new Field('basket', 'Basket', FieldType::Select), 'plum', NULL];
yield 'nothing to constrain' => [new Field('courier', 'Courier'), 'Valley Runs', NULL];
yield 'one value where a list is owed' => [
- (new Field('basket', 'Basket', FieldType::Select))->multiple()->entry('apple', 'Apple'),
+ (new Field('basket', 'Basket', FieldType::Select))->multiple()->option('apple', 'Apple'),
'apple',
'value must be a list',
];
- yield 'a ranking that misses an entry' => [
- (new Field('order', 'Order', FieldType::Reorder))->entry('apple')->entry('carrot'),
+ yield 'a ranking that misses an option' => [
+ (new Field('order', 'Order', FieldType::Reorder))->option('apple')->option('carrot'),
['apple'],
'must rank every option exactly once (apple, carrot)',
];
yield 'a full ranking' => [
- (new Field('order', 'Order', FieldType::Reorder))->entry('apple')->entry('carrot'),
+ (new Field('order', 'Order', FieldType::Reorder))->option('apple')->option('carrot'),
['apple', 'carrot'],
NULL,
];
yield 'a list of listed values' => [
- (new Field('basket', 'Basket', FieldType::Select))->multiple()->entry('apple')->entry('carrot'),
+ (new Field('basket', 'Basket', FieldType::Select))->multiple()->option('apple')->option('carrot'),
['apple', 'carrot'],
NULL,
];
yield 'a list carrying an unlisted value' => [
- (new Field('basket', 'Basket', FieldType::Select))->multiple()->entry('apple')->entry('carrot'),
+ (new Field('basket', 'Basket', FieldType::Select))->multiple()->option('apple')->option('carrot'),
['apple', 'plum'],
'value "plum" is not one of: apple, carrot',
];
}
public function testValueIsRefusedWhenTheQueryResolvedToNothingThatCarriesIt(): void {
- // Entries that follow a query constrain the value to whatever they resolved
+ // Options that follow a query constrain the value to whatever they resolved
// to, so resolving to nothing means the value does not exist.
$field = (new Field('basket', 'Basket contents', FieldType::Search))->query(static fn(): array => []);
$this->assertSame('value "plum" was not found', $field->optionViolation('plum'));
}
- #[DataProvider('dataProviderEntriesAreUnsettledWhileSomethingOwesThem')]
- public function testEntriesAreUnsettledWhileSomethingOwesThem(Field $field, bool $settled, bool $dynamic): void {
+ #[DataProvider('dataProviderOptionsAreUnsettledWhileSomethingOwesThem')]
+ public function testOptionsAreUnsettledWhileSomethingOwesThem(Field $field, bool $settled, bool $dynamic): void {
$this->assertSame($settled, $field->hasSettledOptions());
- $this->assertSame($dynamic, $field->hasDynamicEntries());
+ $this->assertSame($dynamic, $field->hasDynamicOptions());
}
- public static function dataProviderEntriesAreUnsettledWhileSomethingOwesThem(): \Iterator {
+ public static function dataProviderOptionsAreUnsettledWhileSomethingOwesThem(): \Iterator {
$field = static fn(): Field => new Field('basket', 'Basket contents', FieldType::Select);
- yield 'declared' => [$field()->entry('apple', 'Apple'), TRUE, FALSE];
+ yield 'declared' => [$field()->option('apple', 'Apple'), TRUE, FALSE];
yield 'loaded once' => [$field()->load(static fn(): array => []), FALSE, FALSE];
yield 'resolved from the answers' => [$field()->resolve(static fn(array $answers): array => []), FALSE, TRUE];
yield 'resolved from a query' => [$field()->query(static fn(): array => []), FALSE, TRUE];
}
- public function testWhatOwesTheEntriesIsReadBackAsItWasDeclared(): void {
+ public function testWhatOwesTheOptionsIsReadBackAsItWasDeclared(): void {
$loader = static fn(): array => ['apple' => 'Apple'];
$resolver = static fn(array $answers): array => ['carrot' => 'Carrot'];
$source = static fn(string $query, array $answers): array => ['plum' => 'Plum'];
@@ -684,7 +684,7 @@ public function testWhatOwesTheEntriesIsReadBackAsItWasDeclared(): void {
$this->assertSame($source, $field->source());
}
- public function testSettlingTheEntriesRetiresTheLoaderThatOwedThem(): void {
+ public function testSettlingTheOptionsRetiresTheLoaderThatOwedThem(): void {
$field = (new Field('basket', 'Basket contents', FieldType::Select))->load(static fn(): array => ['apple' => 'Apple']);
$field->settle(['apple' => 'Apple', 'carrot' => 'Carrot']);
@@ -694,8 +694,8 @@ public function testSettlingTheEntriesRetiresTheLoaderThatOwedThem(): void {
$this->assertTrue($field->hasSettledOptions());
}
- public function testEntriesThatAreNotMapOfLabelsSettleToNone(): void {
- $field = (new Field('basket', 'Basket contents', FieldType::Select))->entry('apple', 'Apple');
+ public function testOptionsThatAreNotMapOfLabelsSettleToNone(): void {
+ $field = (new Field('basket', 'Basket contents', FieldType::Select))->option('apple', 'Apple');
$this->assertSame([], $field->settle('not a map')->options());
}
@@ -790,12 +790,12 @@ public static function dataProviderDeclarationThatCouldNotBeHonouredIsRefused():
];
}
- public function testEditModeDrawsGroupedDisabledAndDividedEntries(): void {
+ public function testEditModeDrawsGroupedDisabledAndDividedOptions(): void {
$field = (new Field('basket', 'Basket contents', FieldType::Select))
->heading('Fruit')
- ->entry('apple', 'Apple')
+ ->option('apple', 'Apple')
->separator()
- ->entry('carrot', 'Carrot', disabled: TRUE, disabled_reason: 'Out of season.')
+ ->option('carrot', 'Carrot', disabled: TRUE, disabled_reason: 'Out of season.')
->default('apple');
$lines = explode("\n", $field->open()->render($this->theme()));
@@ -824,13 +824,13 @@ public function testDescriptionUnderSettledRowLinesUpWithTheAnswer(): void {
$this->assertSame([' Basket contents apple', ' Pick the produce.'], explode("\n", $field->render($this->theme())));
}
- public function testChosenEntryIsMarkedAcrossOneValueAndSeveral(): void {
- $one = (new Field('basket', 'Basket contents', FieldType::Select))->entry('apple', 'Apple')->entry('carrot', 'Carrot')->default('carrot');
+ public function testChosenOptionIsMarkedAcrossOneValueAndSeveral(): void {
+ $one = (new Field('basket', 'Basket contents', FieldType::Select))->option('apple', 'Apple')->option('carrot', 'Carrot')->default('carrot');
$several = (new Field('basket', 'Basket contents', FieldType::Select))
->multiple()
- ->entry('apple', 'Apple')
- ->entry('carrot', 'Carrot')
- ->entry('plum', 'Plum')
+ ->option('apple', 'Apple')
+ ->option('carrot', 'Carrot')
+ ->option('plum', 'Plum')
->default(['apple', 'carrot']);
// A single choice is a radio list and several is a checkbox list, which is
@@ -843,14 +843,14 @@ public function testChosenEntryIsMarkedAcrossOneValueAndSeveral(): void {
public function testSeveralAnswersReadAsOneLineWhileTheFieldIsSettled(): void {
$field = (new Field('basket', 'Basket contents', FieldType::Select))
->multiple()
- ->entry('apple', 'Apple')
- ->entry('carrot', 'Carrot')
+ ->option('apple', 'Apple')
+ ->option('carrot', 'Carrot')
->default(['apple', 'carrot']);
$this->assertSame(' Basket contents apple, carrot', $field->render($this->theme()));
}
- public function testRowSaysItsEntriesAreStillComingRatherThanReadingAsEmpty(): void {
+ public function testRowSaysItsOptionsAreStillComingRatherThanReadingAsEmpty(): void {
$field = (new Field('basket', 'Basket contents', FieldType::Select))->load(static fn(): array => ['apple' => 'Apple']);
// Nothing has asked the loader yet, which is what a reader meets while a
diff --git a/tests/phpunit/Unit/Block/FieldDeclarationTest.php b/tests/phpunit/Unit/Block/FieldDeclarationTest.php
index f8dedbc6..7fea0f72 100644
--- a/tests/phpunit/Unit/Block/FieldDeclarationTest.php
+++ b/tests/phpunit/Unit/Block/FieldDeclarationTest.php
@@ -298,7 +298,7 @@ public function testOptionsOnFieldWithNoListThrow(\Closure $declare, string $mes
*/
public static function dataProviderOptionsOnFieldWithNoListThrow(): \Iterator {
yield 'a declared row' => [
- static fn(Field $field): Field => $field->entry('apple', 'Apple'),
+ static fn(Field $field): Field => $field->option('apple', 'Apple'),
'Field "f" of type "text" shows no options; only select, search, suggest, toggle and reorder fields have a list.',
];
yield 'a query source' => [
diff --git a/tests/phpunit/Unit/Block/OptionTest.php b/tests/phpunit/Unit/Block/OptionTest.php
index 07206193..30cdb274 100644
--- a/tests/phpunit/Unit/Block/OptionTest.php
+++ b/tests/phpunit/Unit/Block/OptionTest.php
@@ -286,7 +286,7 @@ protected static function offering(FieldType $type, bool $multiple, array $optio
match ($option->kind) {
OptionType::Heading => $field->heading($option->label),
OptionType::Separator => $field->separator(),
- OptionType::Option => $field->entry($option->value, $option->label, $option->description, $option->disabled, $option->disabledReason),
+ OptionType::Option => $field->option($option->value, $option->label, $option->description, $option->disabled, $option->disabledReason),
};
}
diff --git a/tests/phpunit/Unit/ControlBytesTest.php b/tests/phpunit/Unit/ControlBytesTest.php
index 128a9909..8ed4ce1e 100644
--- a/tests/phpunit/Unit/ControlBytesTest.php
+++ b/tests/phpunit/Unit/ControlBytesTest.php
@@ -125,8 +125,8 @@ public static function dataProviderDeclaredTextIsFiltered(): \Iterator {
yield 'an option value' => [static fn(string $b): string => (new Option('figs' . $b, 'Figs'))->value, 'figs[2J'];
yield 'an option description' => [static fn(string $b): string => (new Option('v', 'V', 'Figs' . $b))->description, 'Figs[2J'];
yield 'a disabled reason' => [static fn(string $b): string => (new Option('v', 'V', '', disabled: TRUE, disabled_reason: 'Figs' . $b))->disabledReason, 'Figs[2J'];
- yield 'an entry declared on a field' => [static fn(string $b): string => (string) (new Field('f', 'F', FieldType::Select))->entry('v', 'Figs' . $b)->optionOf('v')?->label, 'Figs[2J'];
- yield 'a heading between entries' => [static fn(string $b): string => (new Field('f', 'F', FieldType::Select))->heading('Figs' . $b)->options()[0]->label, 'Figs[2J'];
+ yield 'an option declared on a field' => [static fn(string $b): string => (string) (new Field('f', 'F', FieldType::Select))->option('v', 'Figs' . $b)->optionOf('v')?->label, 'Figs[2J'];
+ yield 'a heading between options' => [static fn(string $b): string => (new Field('f', 'F', FieldType::Select))->heading('Figs' . $b)->options()[0]->label, 'Figs[2J'];
yield 'a panel title' => [static fn(string $b): string => (new Panel('p', 'Figs' . $b))->title(), 'Figs[2J'];
yield 'a panel description' => [static fn(string $b): string => (new Panel('p', 'P'))->description('Figs' . $b)->descriptionText(), 'Figs[2J'];
yield 'a breadcrumb segment' => [static fn(string $b): string => (new Breadcrumb('Figs' . $b))->render(new DefaultTheme(80, ['color' => FALSE])), 'Figs[2J'];
@@ -239,10 +239,10 @@ public function testMultiLineValueKeepsItsNewlinesAndTabs(): void {
$this->assertSame("Figs\tand plums\nleave at dawn.[2J", $field->value());
}
- public function testAnEntryDeclaredTwiceReplacesTheRowAfterFiltering(): void {
+ public function testAnOptionDeclaredTwiceReplacesTheRowAfterFiltering(): void {
$field = (new Field('basket', 'Basket', FieldType::Select))
- ->entry('apple', 'Apple')
- ->entry("apple\x00", 'Bruised apple');
+ ->option('apple', 'Apple')
+ ->option("apple\x00", 'Bruised apple');
// The set stays unique: the filtered value matches the row already there.
$this->assertCount(1, $field->options());
diff --git a/tests/phpunit/Unit/Field/FieldFactoryTest.php b/tests/phpunit/Unit/Field/FieldFactoryTest.php
index 141c0bee..0e10d434 100644
--- a/tests/phpunit/Unit/Field/FieldFactoryTest.php
+++ b/tests/phpunit/Unit/Field/FieldFactoryTest.php
@@ -74,13 +74,13 @@ public static function dataProviderSeedsValueFromCurrent(): \Iterator {
yield 'template from the assembled value' => [self::templateBlock(), 'one-two', 'one-two'];
yield 'template from a non-string' => [self::templateBlock(), 42, '-'];
// The seed order flows through: the given value first, the remaining
- // entry appended to complete the ranking.
- yield 'reorder completes the ranking' => [self::blockWithEntries(FieldType::Reorder), ['b'], ['b', 'a']];
- yield 'multiple from a non-list' => [self::blockWithEntries(FieldType::Select)->multiple(), 'notalist', []];
+ // option appended to complete the ranking.
+ yield 'reorder completes the ranking' => [self::blockWithOptions(FieldType::Reorder), ['b'], ['b', 'a']];
+ yield 'multiple from a non-list' => [self::blockWithOptions(FieldType::Select)->multiple(), 'notalist', []];
// A multiple choice block opens on the list value, proving the multiple
// flag reaches the select and search fields.
- yield 'multiple select' => [self::blockWithEntries(FieldType::Select)->multiple(), ['a', 'b'], ['a', 'b']];
- yield 'multiple search' => [self::blockWithEntries(FieldType::Search)->multiple(), ['a'], ['a']];
+ yield 'multiple select' => [self::blockWithOptions(FieldType::Select)->multiple(), ['a', 'b'], ['a', 'b']];
+ yield 'multiple search' => [self::blockWithOptions(FieldType::Search)->multiple(), ['a'], ['a']];
}
public function testFilePickerSeedsValueFromCurrent(): void {
@@ -190,7 +190,7 @@ public static function dataProviderTextareaExternalEditorHandoff(): \Iterator {
public function testInjectsScopedKeymapIntoField(): void {
// The vim preset binds j to move-down in the select scope, so the injected
// field responds to j where a default-preset field would not.
- $field = (new FieldFactory(KeyMapManager::create('vim')))->open(self::blockWithEntries(FieldType::Select), 'a');
+ $field = (new FieldFactory(KeyMapManager::create('vim')))->open(self::blockWithOptions(FieldType::Select), 'a');
$field->handle(Key::char('j'));
@@ -199,8 +199,8 @@ public function testInjectsScopedKeymapIntoField(): void {
public function testSuggestReceivesSelectableValuesOnly(): void {
$block = (new BlockField('tz', 'TZ', FieldType::Suggest))
- ->entry('utc', 'UTC')
- ->entry('gmt', 'GMT', '', TRUE)
+ ->option('utc', 'UTC')
+ ->option('gmt', 'GMT', '', TRUE)
->separator();
$view = (new FieldFactory())->open($block, '')->view(new DefaultTheme());
@@ -211,8 +211,8 @@ public function testSuggestReceivesSelectableValuesOnly(): void {
public function testPerOptionDescriptionReachesChoiceField(): void {
$block = (new BlockField('f', 'F', FieldType::Select))
- ->entry('a', 'Apple', 'Crisp and sweet.')
- ->entry('b', 'Banana', 'Rich in potassium.');
+ ->option('a', 'Apple', 'Crisp and sweet.')
+ ->option('b', 'Banana', 'Rich in potassium.');
$view = Ansi::strip((new FieldFactory())->open($block, 'a')->view(new DefaultTheme()));
@@ -220,7 +220,7 @@ public function testPerOptionDescriptionReachesChoiceField(): void {
}
public function testPerOptionDescriptionReachesSuggest(): void {
- $block = (new BlockField('f', 'F', FieldType::Suggest))->entry('apple', 'Apple', 'Crisp and sweet.');
+ $block = (new BlockField('f', 'F', FieldType::Suggest))->option('apple', 'Apple', 'Crisp and sweet.');
$field = (new FieldFactory())->open($block, '');
$field->handle(Key::named(KeyName::Down));
@@ -238,10 +238,10 @@ public function testTextCompletionStaticListReachesField(): void {
}
public function testSuggestGhostFlagReachesField(): void {
- $off = (new BlockField('fruit', 'Fruit', FieldType::Suggest))->entry('Apple', 'Apple')->entry('Apricot', 'Apricot');
+ $off = (new BlockField('fruit', 'Fruit', FieldType::Suggest))->option('Apple', 'Apple')->option('Apricot', 'Apricot');
$this->assertStringNotContainsString("\033[90m", (new FieldFactory())->open($off, 'ap')->view(new DefaultTheme()));
- $on = (new BlockField('fruit', 'Fruit', FieldType::Suggest))->entry('Apple', 'Apple')->entry('Apricot', 'Apricot')->ghost();
+ $on = (new BlockField('fruit', 'Fruit', FieldType::Suggest))->option('Apple', 'Apple')->option('Apricot', 'Apricot')->ghost();
$view = (new FieldFactory())->open($on, 'ap')->view(new DefaultTheme());
// The opted-in field previews the leading candidate's remaining suffix.
@@ -316,12 +316,12 @@ public function testOpensBlockByKind(BlockField $block, mixed $current, string $
public static function dataProviderOpensBlockByKind(): \Iterator {
yield 'text' => [new BlockField('f', 'F'), 'x', Text::class];
yield 'confirm' => [new BlockField('f', 'F', FieldType::Confirm), TRUE, Confirm::class];
- yield 'toggle' => [self::blockWithEntries(FieldType::Toggle), 'a', Toggle::class];
- yield 'select' => [self::blockWithEntries(FieldType::Select), 'a', Select::class];
- yield 'multiple select' => [self::blockWithEntries(FieldType::Select)->multiple(), ['a'], Select::class];
- yield 'search' => [self::blockWithEntries(FieldType::Search), 'a', Search::class];
- yield 'suggest' => [self::blockWithEntries(FieldType::Suggest), 'a', Suggest::class];
- yield 'reorder' => [self::blockWithEntries(FieldType::Reorder), ['a', 'b'], Reorder::class];
+ yield 'toggle' => [self::blockWithOptions(FieldType::Toggle), 'a', Toggle::class];
+ yield 'select' => [self::blockWithOptions(FieldType::Select), 'a', Select::class];
+ yield 'multiple select' => [self::blockWithOptions(FieldType::Select)->multiple(), ['a'], Select::class];
+ yield 'search' => [self::blockWithOptions(FieldType::Search), 'a', Search::class];
+ yield 'suggest' => [self::blockWithOptions(FieldType::Suggest), 'a', Suggest::class];
+ yield 'reorder' => [self::blockWithOptions(FieldType::Reorder), ['a', 'b'], Reorder::class];
yield 'file picker' => [new BlockField('f', 'F', FieldType::FilePicker), '', FilePicker::class];
yield 'number' => [new BlockField('f', 'F', FieldType::Number), 42, Number::class];
yield 'rating' => [(new BlockField('f', 'F', FieldType::Rating))->bounds(new NumberBounds(1, 5)), 3, Rating::class];
@@ -335,8 +335,8 @@ public static function dataProviderOpensBlockByKind(): \Iterator {
public function testOpeningTheBlockCarriesItsDeclarationOntoTheField(): void {
$block = (new BlockField('f', 'F', FieldType::Select))
->multiple()
- ->entry('a', 'A')
- ->entry('b', 'B')
+ ->option('a', 'A')
+ ->option('b', 'B')
->paginate(1)
->placeholder('Pick some produce');
@@ -398,7 +398,7 @@ public function testOpeningTheTextBlockResolvesCompletionAgainstTheAnswers(): vo
}
/**
- * A block of the given kind with two entries.
+ * A block of the given kind with two options.
*
* @param \DrevOps\Tui\Block\FieldType $type
* The kind.
@@ -406,8 +406,8 @@ public function testOpeningTheTextBlockResolvesCompletionAgainstTheAnswers(): vo
* @return \DrevOps\Tui\Block\Field
* The block.
*/
- protected static function blockWithEntries(FieldType $type): BlockField {
- return (new BlockField('f', 'F', $type))->entry('a', 'A')->entry('b', 'B');
+ protected static function blockWithOptions(FieldType $type): BlockField {
+ return (new BlockField('f', 'F', $type))->option('a', 'A')->option('b', 'B');
}
/**
diff --git a/tests/phpunit/Unit/Screen/CollectorTest.php b/tests/phpunit/Unit/Screen/CollectorTest.php
index cba9e83d..9fa731fa 100644
--- a/tests/phpunit/Unit/Screen/CollectorTest.php
+++ b/tests/phpunit/Unit/Screen/CollectorTest.php
@@ -218,9 +218,9 @@ public function testSuppliedValueIsNormalizedBeforeItIsCollected(): void {
$this->assertSame(['courier' => 'Valley Runs'], (new Collector())->collect($panel, ['courier' => ' Valley Runs ']));
}
- public function testSuppliedValueIsMeasuredAgainstTheEntriesItMayPickFrom(): void {
+ public function testSuppliedValueIsMeasuredAgainstTheOptionsItMayPickFrom(): void {
$panel = $this->panel(
- (new Field('basket', 'Basket contents', FieldType::Select))->entry('apple', 'Apple')->entry('carrot', 'Carrot')->default('apple'),
+ (new Field('basket', 'Basket contents', FieldType::Select))->option('apple', 'Apple')->option('carrot', 'Carrot')->default('apple'),
);
$this->assertSame(['basket' => 'carrot'], (new Collector())->collect($panel, ['basket' => 'carrot']));
diff --git a/tests/phpunit/Unit/Screen/KeyRouterTest.php b/tests/phpunit/Unit/Screen/KeyRouterTest.php
index 482e58e7..56f58382 100644
--- a/tests/phpunit/Unit/Screen/KeyRouterTest.php
+++ b/tests/phpunit/Unit/Screen/KeyRouterTest.php
@@ -145,8 +145,8 @@ public function testAcceptingAnOpenFieldTakesWhatWasTypedIntoIt(): void {
public function testSpaceTogglesInsideAnOpenListBecauseTheEditorBindsIt(): void {
$basket = (new Field('basket', 'Basket contents', FieldType::Select))
->multiple()
- ->entry('apple', 'Apple')
- ->entry('carrot', 'Carrot');
+ ->option('apple', 'Apple')
+ ->option('carrot', 'Carrot');
$router = $this->router($basket);
$router->handle(Key::named(KeyName::Enter));
@@ -157,7 +157,7 @@ public function testSpaceTogglesInsideAnOpenListBecauseTheEditorBindsIt(): void
}
public function testCursorKeysReachAnOpenListRatherThanMovingBetweenFields(): void {
- $basket = (new Field('basket', 'Basket contents', FieldType::Select))->entry('apple', 'Apple')->entry('carrot', 'Carrot');
+ $basket = (new Field('basket', 'Basket contents', FieldType::Select))->option('apple', 'Apple')->option('carrot', 'Carrot');
$router = $this->router($basket, new Field('weight', 'Weight'));
$router->handle(Key::named(KeyName::Enter));
diff --git a/tests/phpunit/Unit/Screen/ScreenControllerTest.php b/tests/phpunit/Unit/Screen/ScreenControllerTest.php
index 5e270e6b..6b25380b 100644
--- a/tests/phpunit/Unit/Screen/ScreenControllerTest.php
+++ b/tests/phpunit/Unit/Screen/ScreenControllerTest.php
@@ -115,8 +115,8 @@ public function testTypingReachesAnOpenFieldAndAcceptingTakesIt(): void {
public function testTheListKeysReachAnOpenListRatherThanMovingBetweenRows(): void {
$basket = (new Field('basket', 'Basket contents', FieldType::Select))
->multiple()
- ->entry('apple', 'Apple')
- ->entry('carrot', 'Carrot');
+ ->option('apple', 'Apple')
+ ->option('carrot', 'Carrot');
$tester = $this->tester($this->panel($basket, new Field('courier', 'Courier')));
@@ -187,7 +187,7 @@ public function testFieldRefusesTheNumberItsBoundsRuleOutAndStaysOpenOnIt(): voi
public function testFieldRefusesTooFewPicksAndStaysOpenOnThem(): void {
$basket = (new Field('basket', 'Basket', FieldType::Select))->multiple()->selections(new SelectionBounds(2))->default([]);
- $basket->entry('pear', 'Pear')->entry('plum', 'Plum');
+ $basket->option('pear', 'Pear')->option('plum', 'Plum');
$tester = $this->tester($this->panel($basket));
$answers = $tester->run(
diff --git a/tests/phpunit/Unit/Screen/ScreenParityTest.php b/tests/phpunit/Unit/Screen/ScreenParityTest.php
index d817a1ec..d5919806 100644
--- a/tests/phpunit/Unit/Screen/ScreenParityTest.php
+++ b/tests/phpunit/Unit/Screen/ScreenParityTest.php
@@ -234,8 +234,8 @@ public function testRowSetThatFollowsTheAnswersNarrowsBeforeTheNextFrame(): void
$category = (new Field('category', 'Category', FieldType::Select))
->default('fruit')
- ->entry('fruit', 'Fruit')
- ->entry('vegetable', 'Vegetable');
+ ->option('fruit', 'Fruit')
+ ->option('vegetable', 'Vegetable');
$item = (new Field('item', 'Item', FieldType::Select))->resolve(static function (Context $context) use ($catalog): array {
$category = $context->answers['category'] ?? '';
@@ -538,8 +538,8 @@ public static function dataProviderSettledRowReadsTheAnswerRatherThanHoldsIt():
yield 'several answers read as one run' => [
(new Field('basket', 'Basket contents', FieldType::Select))
->multiple()
- ->entry('apple', 'Apple')
- ->entry('carrot', 'Carrot')
+ ->option('apple', 'Apple')
+ ->option('carrot', 'Carrot')
->default(['apple', 'carrot']),
'❯ Basket contents apple, carrot',
];
diff --git a/tests/phpunit/Unit/Theme/BuiltinThemesTest.php b/tests/phpunit/Unit/Theme/BuiltinThemesTest.php
index d34d54ce..e90770fd 100644
--- a/tests/phpunit/Unit/Theme/BuiltinThemesTest.php
+++ b/tests/phpunit/Unit/Theme/BuiltinThemesTest.php
@@ -65,16 +65,16 @@ public static function dataProviderPalette(): \Iterator {
}
/**
- * A picked entry keeps the palette hue and gains weight.
+ * A picked option keeps the palette hue and gains weight.
*/
- #[DataProvider('dataProviderPickedEntryIsBold')]
- public function testPickedEntryIsBold(string $name, string $expected): void {
+ #[DataProvider('dataProviderPickedOptionIsBold')]
+ public function testPickedOptionIsBold(string $name, string $expected): void {
$theme = $this->builtin($name, 76, ['mode' => Mode::Dark]);
$this->assertSame(Ansi::style('X', $expected), $theme->panelSummary('X'));
}
- public static function dataProviderPickedEntryIsBold(): \Iterator {
+ public static function dataProviderPickedOptionIsBold(): \Iterator {
yield 'midnight' => ['midnight', '38;5;114'];
yield 'frost' => ['frost', '38;5;150'];
yield 'ember' => ['ember', '38;5;142'];
@@ -172,7 +172,7 @@ public static function dataProviderDosConstraintTakesItsOwnColour(): \Iterator {
/**
* Every theme separates guidance from description by colour, not italic.
*
- * A constraint is drawn directly beneath the highlighted entry's own
+ * A constraint is drawn directly beneath the highlighted option's own
* description, so the two need a cue that survives the surface: an SVG
* render carries colour but drops italic entirely.
*/
diff --git a/tests/phpunit/Unit/Theme/ElementDelegationTest.php b/tests/phpunit/Unit/Theme/ElementDelegationTest.php
index e3e88993..743591f2 100644
--- a/tests/phpunit/Unit/Theme/ElementDelegationTest.php
+++ b/tests/phpunit/Unit/Theme/ElementDelegationTest.php
@@ -41,7 +41,7 @@ public static function dataProviderElementsSharingOneHueAreDrawnAlike(): \Iterat
static fn(DefaultTheme $t): string => $t->fieldSelector(TRUE),
static fn(DefaultTheme $t): string => $t->panelSelector(TRUE),
];
- yield 'the field and entry selectors' => [
+ yield 'the field and option selectors' => [
static fn(DefaultTheme $t): string => $t->fieldSelector(TRUE),
static fn(DefaultTheme $t): string => $t->fieldOptionSelector(TRUE),
];
@@ -57,11 +57,11 @@ public static function dataProviderElementsSharingOneHueAreDrawnAlike(): \Iterat
static fn(DefaultTheme $t): string => $t->panelDescription('Pick the produce.'),
static fn(DefaultTheme $t): string => $t->markupLine('Pick the produce.'),
];
- yield 'the focused entry and the caret' => [
+ yield 'the focused option and the caret' => [
static fn(DefaultTheme $t): string => $t->fieldOption('█', FALSE, TRUE),
static fn(DefaultTheme $t): string => $t->fieldCaret(),
];
- yield 'the rule and the entry separator' => [
+ yield 'the rule and the option separator' => [
static fn(DefaultTheme $t): string => $t->renderRule(),
static fn(DefaultTheme $t): string => $t->fieldOptionSeparator(),
];
@@ -79,10 +79,10 @@ public function testRepaintingOneHueMovesEveryElementDrawnFromIt(\Closure $eleme
public static function dataProviderRepaintingOneHueMovesEveryElementDrawnFromIt(): \Iterator {
yield 'field selector' => [static fn(DefaultTheme $t): string => $t->fieldSelector(TRUE)];
- yield 'field entry selector' => [static fn(DefaultTheme $t): string => $t->fieldOptionSelector(TRUE)];
+ yield 'field option selector' => [static fn(DefaultTheme $t): string => $t->fieldOptionSelector(TRUE)];
yield 'field value' => [static fn(DefaultTheme $t): string => $t->fieldValue('apple')];
yield 'field caret' => [static fn(DefaultTheme $t): string => $t->fieldCaret()];
- yield 'field entry marker' => [static fn(DefaultTheme $t): string => $t->fieldOptionMarker(TRUE, TRUE)];
+ yield 'field option marker' => [static fn(DefaultTheme $t): string => $t->fieldOptionMarker(TRUE, TRUE)];
yield 'chrome border' => [static fn(DefaultTheme $t): string => $t->chromeBorder('----')];
yield 'chrome overflow marker' => [static fn(DefaultTheme $t): string => $t->chromeOverflowMarker(TRUE)];
yield 'panel title' => [static fn(DefaultTheme $t): string => $t->panelTitle('Delivery')];
diff --git a/tests/phpunit/Unit/Theme/ThemeBuilderTest.php b/tests/phpunit/Unit/Theme/ThemeBuilderTest.php
index d2372013..61c4823d 100644
--- a/tests/phpunit/Unit/Theme/ThemeBuilderTest.php
+++ b/tests/phpunit/Unit/Theme/ThemeBuilderTest.php
@@ -69,12 +69,12 @@ public static function dataProviderOverrideChangesItsElementAndNothingElse(): \I
(new ThemeBuilder())->field(static fn(FieldOverrides $f): FieldOverrides => $f->valueSeparator(' | '))->overrides(),
'fieldValueSeparator',
],
- 'field entry selector' => [
- (new ThemeBuilder())->field(static fn(FieldOverrides $f): FieldOverrides => $f->entrySelector('→', '=>'))->overrides(),
+ 'field option selector' => [
+ (new ThemeBuilder())->field(static fn(FieldOverrides $f): FieldOverrides => $f->optionSelector('→', '=>'))->overrides(),
'fieldOptionSelector',
],
- 'field entry marker' => [
- (new ThemeBuilder())->field(static fn(FieldOverrides $f): FieldOverrides => $f->entryMarker('★', '(*)'))->overrides(),
+ 'field option marker' => [
+ (new ThemeBuilder())->field(static fn(FieldOverrides $f): FieldOverrides => $f->optionMarker('★', '(*)'))->overrides(),
'fieldOptionMarker',
],
'field caret' => [
@@ -123,7 +123,7 @@ public function testTwoSelectorsComeApartOnceEitherIsOverridden(): void {
public function testAnUnmarkedStateKeepsWhatTheThemeDrawsForIt(): void {
$plain = new DefaultTheme(80, ['color' => FALSE]);
$theme = (new DefaultTheme(80, ['color' => FALSE]))->overrides(
- (new ThemeBuilder())->field(static fn(FieldOverrides $f): FieldOverrides => $f->entryMarker('★', '(*)'))->overrides()
+ (new ThemeBuilder())->field(static fn(FieldOverrides $f): FieldOverrides => $f->optionMarker('★', '(*)'))->overrides()
);
$this->assertSame('★', $theme->fieldOptionMarker(TRUE));
@@ -137,7 +137,7 @@ public function testOneGroupCanStateSeveralElementsAtOnce(): void {
->selector('→', '=>')
->helpMarker('?', '(?)')
->valueSeparator(' | ')
- ->entryMarker('★', '(*)')
+ ->optionMarker('★', '(*)')
->caret('▌', '!'))
->overrides();
diff --git a/tests/phpunit/Unit/Theme/ThemeTest.php b/tests/phpunit/Unit/Theme/ThemeTest.php
index c23e03d8..5d887cd8 100644
--- a/tests/phpunit/Unit/Theme/ThemeTest.php
+++ b/tests/phpunit/Unit/Theme/ThemeTest.php
@@ -45,7 +45,7 @@ public static function dataProviderElementPaint(): \Iterator {
yield 'constraint' => [static fn(): string => (new DefaultTheme())->fieldConstraint('X'), '3;38;5;246'];
yield 'error' => [static fn(): string => (new DefaultTheme())->fieldError('X'), '31'];
yield 'breadcrumb' => [static fn(): string => self::light()->breadcrumbLabel('X'), '90'];
- yield 'entry note' => [static fn(): string => (new DefaultTheme())->fieldOptionNote('X'), '90'];
+ yield 'option note' => [static fn(): string => (new DefaultTheme())->fieldOptionNote('X'), '90'];
yield 'state' => [static fn(): string => (new DefaultTheme())->fieldState('X'), '90'];
yield 'caption' => [static fn(): string => (new DefaultTheme())->fieldCaption('X'), '1;38;5;109'];
// Inline ghost-text is dimmed gray, the same as the other dimmed chrome.
@@ -167,12 +167,12 @@ public function testRule(): void {
$this->assertSame('----------', (new DefaultTheme(10, ['unicode' => FALSE, 'color' => FALSE, 'border' => Border::None]))->renderRule());
// The rule is dimmed when colour is on.
$this->assertStringContainsString("\033[90m", (new DefaultTheme(10))->renderRule());
- // One rule wherever it appears: what stands between two runs of entries is
+ // One rule wherever it appears: what stands between two runs of options is
// what stands between two blocks of standalone output.
$this->assertSame((new DefaultTheme(10))->renderRule(), (new DefaultTheme(10))->fieldOptionSeparator());
}
- public function testPickedEntryTakesWeightAndFocusTakesTheAccent(): void {
+ public function testPickedOptionTakesWeightAndFocusTakesTheAccent(): void {
$theme = new DefaultTheme();
$this->assertStringContainsString("\033[1", $theme->fieldOption('X', TRUE));
From 7ea0deb120a09fdb746363f26064c9a755718af0 Mon Sep 17 00:00:00 2001
From: Alex Skrypnyk
Date: Wed, 12 Aug 2026 09:55:56 +1000
Subject: [PATCH 26/27] Converged the 'has' predicates onto the 'is' prefix,
leaving the commands alone.
---
docs/content/fields/anatomy.mdx | 6 +++---
docs/content/specification.mdx | 6 +++---
docs/content/themes.mdx | 6 +++---
playground/themes/OceanTheme.php | 18 +++++++++---------
src/Block/Field.php | 18 +++++++++---------
src/Block/Prose.php | 2 +-
src/Condition/Condition.php | 4 ++--
src/Field/FieldFactory.php | 6 +++---
src/Schema/DefaultResolver.php | 2 +-
src/Schema/SchemaGenerator.php | 2 +-
src/Screen/ScreenController.php | 2 +-
src/Screen/ScreenRenderer.php | 2 +-
.../Capability/ColorSchemeCapableInterface.php | 2 +-
.../Capability/ColorSchemeCapableTrait.php | 2 +-
.../Capability/MarkdownCapableInterface.php | 2 +-
.../Capability/UnicodeCapableInterface.php | 2 +-
src/Theme/Capability/UnicodeCapableTrait.php | 2 +-
src/Theme/DefaultTheme.php | 4 ++--
tests/phpunit/Unit/Block/FieldBlockTest.php | 16 ++++++++--------
tests/phpunit/Unit/Builder/FormTest.php | 14 +++++++-------
tests/phpunit/Unit/Screen/ScreenParityTest.php | 4 ++--
tests/phpunit/Unit/Theme/BuiltinThemesTest.php | 2 +-
tests/phpunit/Unit/Theme/SupportTest.php | 8 ++++----
tests/phpunit/Unit/Theme/ThemeManagerTest.php | 8 ++++----
tests/phpunit/Unit/Theme/ThemeTest.php | 6 +++---
25 files changed, 73 insertions(+), 73 deletions(-)
diff --git a/docs/content/fields/anatomy.mdx b/docs/content/fields/anatomy.mdx
index 70d3f2b2..182e2043 100644
--- a/docs/content/fields/anatomy.mdx
+++ b/docs/content/fields/anatomy.mdx
@@ -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 |
diff --git a/docs/content/specification.mdx b/docs/content/specification.mdx
index 9df7601c..fd50400d 100644
--- a/docs/content/specification.mdx
+++ b/docs/content/specification.mdx
@@ -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 |
diff --git a/docs/content/themes.mdx b/docs/content/themes.mdx
index ebbfb292..034eeefe 100644
--- a/docs/content/themes.mdx
+++ b/docs/content/themes.mdx
@@ -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 |
diff --git a/playground/themes/OceanTheme.php b/playground/themes/OceanTheme.php
index 177170eb..80e6f935 100644
--- a/playground/themes/OceanTheme.php
+++ b/playground/themes/OceanTheme.php
@@ -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() ? '➤' : '>') : ' ';
}
/**
@@ -89,7 +89,7 @@ protected function divider(): string {
* The mark.
*/
protected function lead(): string {
- return $this->hasUnicode() ? '•' : '*';
+ return $this->isUnicode() ? '•' : '*';
}
/**
@@ -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);
@@ -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'));
}
/**
@@ -125,7 +125,7 @@ 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() ? '▎' : '|');
}
/**
@@ -134,10 +134,10 @@ public function fieldCaret(): string {
#[\Override]
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() ? '▢' : '[ ]');
}
/**
@@ -185,7 +185,7 @@ public function actionSeparator(): string {
*/
#[\Override]
public function panelDescend(): string {
- return $this->description($this->hasUnicode() ? '»' : '>');
+ return $this->description($this->isUnicode() ? '»' : '>');
}
/**
@@ -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);
}
/**
diff --git a/src/Block/Field.php b/src/Block/Field.php
index 77d016bb..9f3090ec 100644
--- a/src/Block/Field.php
+++ b/src/Block/Field.php
@@ -510,7 +510,7 @@ public function accept(mixed $value = NULL): bool {
* @return bool
* TRUE when the last thing to happen here was an answer being taken.
*/
- public function hasAccepted(): bool {
+ public function isAccepted(): bool {
return $this->accepted;
}
@@ -918,7 +918,7 @@ public function schemaDefaultValue(): mixed {
* @return bool
* TRUE when it was.
*/
- public function hasSchemaDefault(): bool {
+ public function isSchemaDefault(): bool {
return $this->hasSchemaDefault;
}
@@ -1173,7 +1173,7 @@ public function settle(mixed $options): static {
* FALSE while a loader, a resolver or a query source still owes them, so
* there is nothing yet to count or to check a value against.
*/
- public function hasSettledOptions(): bool {
+ public function isSettledOptions(): bool {
return !$this->loader instanceof \Closure && !$this->resolver instanceof \Closure && !$this->source instanceof \Closure;
}
@@ -1184,7 +1184,7 @@ public function hasSettledOptions(): bool {
* TRUE when they are resolved from the answers or from a live query, so no
* one list describes the field.
*/
- public function hasDynamicOptions(): bool {
+ public function isDynamicOptions(): bool {
return $this->resolver instanceof \Closure || $this->source instanceof \Closure;
}
@@ -1468,7 +1468,7 @@ public function optionViolation(mixed $value): ?string {
// A field that declares no options constrains nothing - but one whose
// options follow a query or the answers is constrained by whatever they
// resolved to, and resolving to nothing means the value does not exist.
- if (!$this->fieldType->constrainsToOptions() || ($this->options === [] && !$this->hasDynamicOptions())) {
+ if (!$this->fieldType->constrainsToOptions() || ($this->options === [] && !$this->isDynamicOptions())) {
return NULL;
}
@@ -1649,7 +1649,7 @@ public function ghost(bool $ghost = TRUE): static {
* @return bool
* TRUE when it is.
*/
- public function hasGhost(): bool {
+ public function isGhost(): bool {
return $this->ghost;
}
@@ -1731,7 +1731,7 @@ public function confirmation(bool $confirm = TRUE): static {
* @return bool
* TRUE when it does.
*/
- public function hasConfirmation(): bool {
+ public function isConfirmation(): bool {
return $this->confirm;
}
@@ -1756,7 +1756,7 @@ public function externalEditor(bool $enabled = TRUE): static {
* @return bool
* TRUE when it may.
*/
- public function hasExternalEditor(): bool {
+ public function isExternalEditor(): bool {
return $this->externalEditor;
}
@@ -1784,7 +1784,7 @@ public function handoff(bool $available = TRUE): static {
* @return bool
* TRUE when one can be launched.
*/
- public function hasHandoff(): bool {
+ public function isHandoff(): bool {
return $this->handoff;
}
diff --git a/src/Block/Prose.php b/src/Block/Prose.php
index 58a69714..14ae638d 100644
--- a/src/Block/Prose.php
+++ b/src/Block/Prose.php
@@ -94,7 +94,7 @@ protected static function span(MarkupSegment $segment, MarkupElementsInterface $
* TRUE when it draws it.
*/
protected static function markdown(MarkupElementsInterface $theme): bool {
- return $theme instanceof MarkdownCapableInterface && $theme->hasMarkdown();
+ return $theme instanceof MarkdownCapableInterface && $theme->isMarkdown();
}
}
diff --git a/src/Condition/Condition.php b/src/Condition/Condition.php
index 4e6a15d2..df4866b7 100644
--- a/src/Condition/Condition.php
+++ b/src/Condition/Condition.php
@@ -98,7 +98,7 @@ public function matches(array $answers): bool {
}
if ($this->contains !== NULL) {
- return $this->hasContains($value, $this->contains);
+ return $this->isContaining($value, $this->contains);
}
return !in_array($value, [NULL, FALSE, '', []], TRUE);
@@ -184,7 +184,7 @@ protected function isIn(mixed $value, array $list): bool {
* @return bool
* TRUE when contained.
*/
- protected function hasContains(mixed $value, mixed $needle): bool {
+ protected function isContaining(mixed $value, mixed $needle): bool {
if (is_array($value)) {
foreach ($value as $item) {
if ($this->equals($item, $needle)) {
diff --git a/src/Field/FieldFactory.php b/src/Field/FieldFactory.php
index 58703b09..544ad6a3 100644
--- a/src/Field/FieldFactory.php
+++ b/src/Field/FieldFactory.php
@@ -68,14 +68,14 @@ public function open(Field $block, mixed $current = NULL, array $answers = []):
FieldType::Toggle => new Toggle($this->optionLabels($options), $this->text($current)),
FieldType::Select => new Select($options, $this->seed($block, $current), $block->isMultiple(), $block->pageSize(), $block->selectionBounds()),
FieldType::Reorder => new Reorder($options, Field::stringList($current), $block->pageSize()),
- FieldType::Suggest => new Suggest($block->selectableValues(), $this->text($current), $block->pageSize(), $this->optionDescriptions($options), $block->hasGhost()),
+ FieldType::Suggest => new Suggest($block->selectableValues(), $this->text($current), $block->pageSize(), $this->optionDescriptions($options), $block->isGhost()),
FieldType::Search => new Search($options, $this->seed($block, $current), $block->isMultiple(), $block->pageSize(), $block->selectionBounds()),
FieldType::FilePicker => new FilePicker($block->pickerStart(), $this->seed($block, $current), $block->pickerConstraints(), $block->showsHidden(), $block->isMultiple(), $block->pageSize(), $block->selectionBounds()),
FieldType::Number => new Number($this->number($current), $block->numberBounds()),
FieldType::Rating => $this->rating($block, $current),
FieldType::Calendar => new Calendar($this->text($current), $block->dateBounds()),
- FieldType::Textarea => new Textarea($this->text($current), $block->hasExternalEditor() && $this->externalEditorAvailable),
- FieldType::Password => new Password($this->text($current), $block->isRevealable(), $block->hasConfirmation()),
+ FieldType::Textarea => new Textarea($this->text($current), $block->isExternalEditor() && $this->externalEditorAvailable),
+ FieldType::Password => new Password($this->text($current), $block->isRevealable(), $block->isConfirmation()),
FieldType::Pause => new Pause(),
FieldType::Text => new Text($this->text($current), $this->completions($block, $answers)),
FieldType::Template => new Template($this->template($block), $this->text($current)),
diff --git a/src/Schema/DefaultResolver.php b/src/Schema/DefaultResolver.php
index 3b72830c..2b33e919 100644
--- a/src/Schema/DefaultResolver.php
+++ b/src/Schema/DefaultResolver.php
@@ -40,7 +40,7 @@ public static function resolve(Field $field, Context $context): mixed {
return $default;
}
- if ($field->hasSchemaDefault()) {
+ if ($field->isSchemaDefault()) {
return $field->schemaDefaultValue();
}
diff --git a/src/Schema/SchemaGenerator.php b/src/Schema/SchemaGenerator.php
index 5556efb4..5277776d 100644
--- a/src/Schema/SchemaGenerator.php
+++ b/src/Schema/SchemaGenerator.php
@@ -76,7 +76,7 @@ public function generate(): array {
'help' => $field->helpText(),
'placeholder' => $field->placeholderText(),
'options' => $this->options($field),
- 'options_dynamic' => $field->hasDynamicOptions(),
+ 'options_dynamic' => $field->isDynamicOptions(),
'default' => DefaultResolver::resolve($field, $this->context),
'required' => $field->isRequired(),
'env' => $names->isAdvertisable($field) ? $names->canonical($field) : NULL,
diff --git a/src/Screen/ScreenController.php b/src/Screen/ScreenController.php
index 468e5a10..06ef365c 100644
--- a/src/Screen/ScreenController.php
+++ b/src/Screen/ScreenController.php
@@ -1171,7 +1171,7 @@ protected function handoff(?Field $open): void {
* The field the key reached, if it reached one that was open.
*/
protected function stamp(?Field $open): void {
- if (!$open instanceof Field || !$open->hasAccepted()) {
+ if (!$open instanceof Field || !$open->isAccepted()) {
return;
}
diff --git a/src/Screen/ScreenRenderer.php b/src/Screen/ScreenRenderer.php
index 0ae3efb4..9de22762 100644
--- a/src/Screen/ScreenRenderer.php
+++ b/src/Screen/ScreenRenderer.php
@@ -546,7 +546,7 @@ protected function chrome(): ChromeElementsInterface {
* TRUE when the theme declared it handles them.
*/
protected function unicode(): bool {
- return $this->theme instanceof UnicodeCapableInterface && $this->theme->hasUnicode();
+ return $this->theme instanceof UnicodeCapableInterface && $this->theme->isUnicode();
}
/**
diff --git a/src/Theme/Capability/ColorSchemeCapableInterface.php b/src/Theme/Capability/ColorSchemeCapableInterface.php
index 431955f1..0e0e2ad5 100644
--- a/src/Theme/Capability/ColorSchemeCapableInterface.php
+++ b/src/Theme/Capability/ColorSchemeCapableInterface.php
@@ -25,7 +25,7 @@ interface ColorSchemeCapableInterface {
* @return bool
* TRUE when an element may paint.
*/
- public function hasColor(): bool;
+ public function isColor(): bool;
/**
* Whether the terminal's background is dark.
diff --git a/src/Theme/Capability/ColorSchemeCapableTrait.php b/src/Theme/Capability/ColorSchemeCapableTrait.php
index e7d332f4..6e06582c 100644
--- a/src/Theme/Capability/ColorSchemeCapableTrait.php
+++ b/src/Theme/Capability/ColorSchemeCapableTrait.php
@@ -31,7 +31,7 @@ trait ColorSchemeCapableTrait {
/**
* {@inheritdoc}
*/
- public function hasColor(): bool {
+ public function isColor(): bool {
return $this->color;
}
diff --git a/src/Theme/Capability/MarkdownCapableInterface.php b/src/Theme/Capability/MarkdownCapableInterface.php
index 44f4b127..f1bd7154 100644
--- a/src/Theme/Capability/MarkdownCapableInterface.php
+++ b/src/Theme/Capability/MarkdownCapableInterface.php
@@ -22,6 +22,6 @@ interface MarkdownCapableInterface {
* @return bool
* TRUE when it is drawn.
*/
- public function hasMarkdown(): bool;
+ public function isMarkdown(): bool;
}
diff --git a/src/Theme/Capability/UnicodeCapableInterface.php b/src/Theme/Capability/UnicodeCapableInterface.php
index 072bab27..988b17f9 100644
--- a/src/Theme/Capability/UnicodeCapableInterface.php
+++ b/src/Theme/Capability/UnicodeCapableInterface.php
@@ -20,6 +20,6 @@ interface UnicodeCapableInterface {
* @return bool
* TRUE when an element may reach for one.
*/
- public function hasUnicode(): bool;
+ public function isUnicode(): bool;
}
diff --git a/src/Theme/Capability/UnicodeCapableTrait.php b/src/Theme/Capability/UnicodeCapableTrait.php
index 27241540..1dc7f7a3 100644
--- a/src/Theme/Capability/UnicodeCapableTrait.php
+++ b/src/Theme/Capability/UnicodeCapableTrait.php
@@ -22,7 +22,7 @@ trait UnicodeCapableTrait {
/**
* {@inheritdoc}
*/
- public function hasUnicode(): bool {
+ public function isUnicode(): bool {
return $this->unicode;
}
diff --git a/src/Theme/DefaultTheme.php b/src/Theme/DefaultTheme.php
index e4582b18..8cd2944d 100644
--- a/src/Theme/DefaultTheme.php
+++ b/src/Theme/DefaultTheme.php
@@ -353,7 +353,7 @@ protected function mode(): Mode {
* @return bool
* TRUE when it is drawn rather than left as literal text.
*/
- public function hasMarkdown(): bool {
+ public function isMarkdown(): bool {
return $this->markdown;
}
@@ -958,7 +958,7 @@ public function fieldConstraint(string $text): string {
// own description, so the two must not be mistaken for each other on any
// surface. Slant reinforces the hue where the surface honours it, and where
// neither survives the voice falls back to a mark, which nothing can strip.
- $marked = $this->hasColor() ? $text : $this->glyph('› ', '> ') . $text;
+ $marked = $this->isColor() ? $text : $this->glyph('› ', '> ') . $text;
return $this->paint($this->guidance(), $this->linkify($marked));
}
diff --git a/tests/phpunit/Unit/Block/FieldBlockTest.php b/tests/phpunit/Unit/Block/FieldBlockTest.php
index eb4ff935..72742a70 100644
--- a/tests/phpunit/Unit/Block/FieldBlockTest.php
+++ b/tests/phpunit/Unit/Block/FieldBlockTest.php
@@ -288,7 +288,7 @@ public static function dataProviderDeclarationReadsBackAsItWasWritten(): \Iterat
yield 'ghost' => [
static fn(Field $field): Field => $field->ghost(),
- static fn(Field $field): bool => $field->hasGhost(),
+ static fn(Field $field): bool => $field->isGhost(),
TRUE,
];
@@ -318,13 +318,13 @@ public static function dataProviderDeclarationReadsBackAsItWasWritten(): \Iterat
yield 'confirmation' => [
static fn(Field $field): Field => $field->confirmation(),
- static fn(Field $field): bool => $field->hasConfirmation(),
+ static fn(Field $field): bool => $field->isConfirmation(),
TRUE,
];
yield 'external editor' => [
static fn(Field $field): Field => $field->externalEditor(),
- static fn(Field $field): bool => $field->hasExternalEditor(),
+ static fn(Field $field): bool => $field->isExternalEditor(),
TRUE,
];
@@ -410,8 +410,8 @@ public static function dataProviderDeclarationReadsBackAsItWasWritten(): \Iterat
public function testDeclaredNullDefaultIsStillDeclaredInMachineOutput(): void {
$field = new Field('basket', 'Basket contents');
- $this->assertFalse($field->hasSchemaDefault());
- $this->assertTrue($field->schemaDefault(NULL)->hasSchemaDefault());
+ $this->assertFalse($field->isSchemaDefault());
+ $this->assertTrue($field->schemaDefault(NULL)->isSchemaDefault());
$this->assertNull($field->schemaDefaultValue());
}
@@ -659,8 +659,8 @@ public function testValueIsRefusedWhenTheQueryResolvedToNothingThatCarriesIt():
#[DataProvider('dataProviderOptionsAreUnsettledWhileSomethingOwesThem')]
public function testOptionsAreUnsettledWhileSomethingOwesThem(Field $field, bool $settled, bool $dynamic): void {
- $this->assertSame($settled, $field->hasSettledOptions());
- $this->assertSame($dynamic, $field->hasDynamicOptions());
+ $this->assertSame($settled, $field->isSettledOptions());
+ $this->assertSame($dynamic, $field->isDynamicOptions());
}
public static function dataProviderOptionsAreUnsettledWhileSomethingOwesThem(): \Iterator {
@@ -691,7 +691,7 @@ public function testSettlingTheOptionsRetiresTheLoaderThatOwedThem(): void {
$this->assertSame(['apple', 'carrot'], $field->selectableValues());
$this->assertNotInstanceOf(\Closure::class, $field->loader());
- $this->assertTrue($field->hasSettledOptions());
+ $this->assertTrue($field->isSettledOptions());
}
public function testOptionsThatAreNotMapOfLabelsSettleToNone(): void {
diff --git a/tests/phpunit/Unit/Builder/FormTest.php b/tests/phpunit/Unit/Builder/FormTest.php
index 1dead20c..c36127a0 100644
--- a/tests/phpunit/Unit/Builder/FormTest.php
+++ b/tests/phpunit/Unit/Builder/FormTest.php
@@ -119,7 +119,7 @@ public function testBuildsExpectedForm(): void {
$this->assertInstanceOf(Field::class, $secret);
$this->assertSame(FieldType::Password, $secret->type());
$this->assertTrue($secret->isRevealable());
- $this->assertTrue($secret->hasConfirmation());
+ $this->assertTrue($secret->isConfirmation());
$timezone = self::fieldOf($form, 'timezone');
$this->assertInstanceOf(Field::class, $timezone);
@@ -177,7 +177,7 @@ public function testDefaultsAndFallbacks(): void {
$password = self::fieldOf($form, 'pw');
$this->assertInstanceOf(Field::class, $password);
$this->assertFalse($password->isRevealable());
- $this->assertFalse($password->hasConfirmation());
+ $this->assertFalse($password->isConfirmation());
$this->assertSame('', self::fieldOf($form, 'se')?->value());
$this->assertSame([], self::fieldOf($form, 'ms')?->value());
// A toggle defaults to its first option, since it always holds a value.
@@ -233,8 +233,8 @@ public function testExternalEditorFlag(): void {
})
->root();
- $this->assertTrue(self::fieldOf($form, 'notes')?->hasExternalEditor());
- $this->assertFalse(self::fieldOf($form, 'plain')?->hasExternalEditor());
+ $this->assertTrue(self::fieldOf($form, 'notes')?->isExternalEditor());
+ $this->assertFalse(self::fieldOf($form, 'plain')?->isExternalEditor());
}
public function testNoteField(): void {
@@ -373,10 +373,10 @@ public function testGhostTextOptInStored(): void {
})
->root();
- $this->assertTrue(self::fieldOf($form, 'fruit')?->hasGhost());
- $this->assertFalse(self::fieldOf($form, 'berry')?->hasGhost());
+ $this->assertTrue(self::fieldOf($form, 'fruit')?->isGhost());
+ $this->assertFalse(self::fieldOf($form, 'berry')?->isGhost());
// Ghost-text is opt-in, so a field that never asks for it stays without.
- $this->assertFalse(self::fieldOf($form, 'plain')?->hasGhost());
+ $this->assertFalse(self::fieldOf($form, 'plain')?->isGhost());
}
public function testTemplateAssembled(): void {
diff --git a/tests/phpunit/Unit/Screen/ScreenParityTest.php b/tests/phpunit/Unit/Screen/ScreenParityTest.php
index d5919806..0cb88e4c 100644
--- a/tests/phpunit/Unit/Screen/ScreenParityTest.php
+++ b/tests/phpunit/Unit/Screen/ScreenParityTest.php
@@ -329,7 +329,7 @@ public function testTextareaHandsItsBufferToTheEditorOfTheReadersOwn(): void {
// The session left the terminal to the editor and took it back, and what
// came back is what the row now holds.
- $this->assertTrue($notes->hasHandoff());
+ $this->assertTrue($notes->isHandoff());
$this->assertSame('Weighed at the bench', $answers->value('notes'));
$this->assertInstanceOf(Terminal::class, $editor->suspended);
}
@@ -355,7 +355,7 @@ public function testFieldOffersNoHandoffWhereThereIsNoEditorToHandOffTo(): void
$tester = $this->tester($this->panel($notes))->externalEditor(new EditorFixture(FALSE))->cols(90);
$tester->run(Key::named(KeyName::Enter));
- $this->assertFalse($notes->hasHandoff());
+ $this->assertFalse($notes->isHandoff());
$this->assertStringContainsString('to accept', $tester->frame());
$this->assertStringNotContainsString('CTRL', $tester->frame());
}
diff --git a/tests/phpunit/Unit/Theme/BuiltinThemesTest.php b/tests/phpunit/Unit/Theme/BuiltinThemesTest.php
index e90770fd..b505741e 100644
--- a/tests/phpunit/Unit/Theme/BuiltinThemesTest.php
+++ b/tests/phpunit/Unit/Theme/BuiltinThemesTest.php
@@ -89,7 +89,7 @@ public static function dataProviderPickedOptionIsBold(): \Iterator {
public function testColourOffStripsPalette(string $name): void {
$theme = $this->builtin($name, 76, ['color' => FALSE]);
- $this->assertFalse($theme->hasColor());
+ $this->assertFalse($theme->isColor());
$this->assertSame('Setup', $theme->markupTitle('Setup'));
$this->assertSame('X', $theme->fieldValue('X'));
$this->assertSame('▲', $theme->chromeOverflowMarker(TRUE));
diff --git a/tests/phpunit/Unit/Theme/SupportTest.php b/tests/phpunit/Unit/Theme/SupportTest.php
index 91af6f79..219570e5 100644
--- a/tests/phpunit/Unit/Theme/SupportTest.php
+++ b/tests/phpunit/Unit/Theme/SupportTest.php
@@ -35,14 +35,14 @@ public function testColourAndItsSchemeAreOneDeclaration(): void {
$this->assertInstanceOf(ColorSchemeCapableInterface::class, $dark);
$this->assertTrue($dark->isDark());
$this->assertFalse($light->isDark());
- $this->assertTrue((new DefaultTheme(80, ['color' => TRUE]))->hasColor());
- $this->assertFalse((new DefaultTheme(80, ['color' => FALSE]))->hasColor());
+ $this->assertTrue((new DefaultTheme(80, ['color' => TRUE]))->isColor());
+ $this->assertFalse((new DefaultTheme(80, ['color' => FALSE]))->isColor());
}
public function testUnicodeIsDeclaredAndCanBeTurnedOff(): void {
$this->assertInstanceOf(UnicodeCapableInterface::class, new DefaultTheme());
- $this->assertTrue((new DefaultTheme(80, ['unicode' => TRUE]))->hasUnicode());
- $this->assertFalse((new DefaultTheme(80, ['unicode' => FALSE]))->hasUnicode());
+ $this->assertTrue((new DefaultTheme(80, ['unicode' => TRUE]))->isUnicode());
+ $this->assertFalse((new DefaultTheme(80, ['unicode' => FALSE]))->isUnicode());
}
public function testThemeDeclaringNothingSupportsNothing(): void {
diff --git a/tests/phpunit/Unit/Theme/ThemeManagerTest.php b/tests/phpunit/Unit/Theme/ThemeManagerTest.php
index 1a9e338f..3ec14d45 100644
--- a/tests/phpunit/Unit/Theme/ThemeManagerTest.php
+++ b/tests/phpunit/Unit/Theme/ThemeManagerTest.php
@@ -229,10 +229,10 @@ public function testCreatePassesOptions(): void {
$this->assertInstanceOf(DefaultTheme::class, $theme);
$this->assertInstanceOf(DefaultTheme::class, $default);
- $this->assertFalse($theme->hasColor());
- $this->assertFalse($theme->hasUnicode());
- $this->assertTrue($default->hasColor());
- $this->assertTrue($default->hasUnicode());
+ $this->assertFalse($theme->isColor());
+ $this->assertFalse($theme->isUnicode());
+ $this->assertTrue($default->isColor());
+ $this->assertTrue($default->isUnicode());
}
}
diff --git a/tests/phpunit/Unit/Theme/ThemeTest.php b/tests/phpunit/Unit/Theme/ThemeTest.php
index 5d887cd8..d8dbddee 100644
--- a/tests/phpunit/Unit/Theme/ThemeTest.php
+++ b/tests/phpunit/Unit/Theme/ThemeTest.php
@@ -185,7 +185,7 @@ public function testColourOffLeavesTextPlain(): void {
$this->assertSame('Setup', $theme->markupTitle('Setup'));
$this->assertSame('X', $theme->fieldValue('X'));
- $this->assertFalse($theme->hasColor());
+ $this->assertFalse($theme->isColor());
}
#[DataProvider('dataProviderGlyph')]
@@ -253,8 +253,8 @@ protected function accent(): string {
}
public function testHasUnicode(): void {
- $this->assertTrue((new DefaultTheme())->hasUnicode());
- $this->assertFalse((new DefaultTheme(76, ['unicode' => FALSE]))->hasUnicode());
+ $this->assertTrue((new DefaultTheme())->isUnicode());
+ $this->assertFalse((new DefaultTheme(76, ['unicode' => FALSE]))->isUnicode());
}
public function testDefaultThemePaintsNoBackground(): void {
From 6f54886d8e907d4bbe238fe41239821da9519697 Mon Sep 17 00:00:00 2001
From: Alex Skrypnyk
Date: Wed, 12 Aug 2026 09:56:16 +1000
Subject: [PATCH 27/27] Stated the predicate rule as one spelling now that no
'has' form remains.
---
AGENTS.md | 24 ++++++++++++++----------
1 file changed, 14 insertions(+), 10 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
index 755b52b3..2e9ffc74 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -129,16 +129,20 @@ composer install
- 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 - existing examples: `isRequired()`, `isMultiple()`,
- `isScrolling()`, `isSelectable()`, `isQueryDriven()`. This covers the
- `has*` possession predicates too: prefer `is*` for a new one.
-
- The single exception is a **command that reports its own outcome**. A method
- whose job is to do something, and whose boolean says whether it happened,
- keeps its verb - `accept()`, `capture()`, `activate()`, `load()`, `leave()`,
- `prepare()`. Those are not questions, and an `is` prefix would misname the
- work they do. If a method both acts and answers, it is a command, not a
- predicate.
+ 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