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