From fdf67b2cb7a911ca44a55acd357c01705136a470 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Wed, 29 Jul 2026 14:53:56 +1000 Subject: [PATCH 01/14] Sorted use statements alphabetically across src, tests and playground. The dominant convention was already case-insensitive alphabetical ordering in 190 of 235 files with two or more imports. The 45 outliers hoisted a Model import above an alphabetically earlier namespace, most often placing Model above Input. --- playground/themes/OceanTheme.php | 4 ++-- src/Builder/FieldBuilder.php | 6 +++--- src/Builder/Form.php | 2 +- src/Engine/Engine.php | 6 +++--- src/Render/PanelController.php | 16 ++++++++-------- src/Schema/SchemaGenerator.php | 4 ++-- src/Schema/SchemaValidator.php | 2 +- src/Testing/TuiTester.php | 2 +- src/Theme/DefaultTheme.php | 10 +++++----- src/Tui.php | 4 ++-- src/Widget/CalendarWidget.php | 6 +++--- src/Widget/Capability/SelectionCapableTrait.php | 4 ++-- src/Widget/ConfirmWidget.php | 2 +- src/Widget/FilePickerWidget.php | 8 ++++---- src/Widget/NumberWidget.php | 4 ++-- src/Widget/PasswordWidget.php | 2 +- src/Widget/PauseWidget.php | 2 +- src/Widget/ReorderWidget.php | 6 +++--- src/Widget/SearchWidget.php | 4 ++-- src/Widget/SuggestWidget.php | 2 +- src/Widget/TextWidget.php | 2 +- src/Widget/TextareaWidget.php | 2 +- src/Widget/ToggleWidget.php | 2 +- src/Widget/WidgetFactory.php | 4 ++-- tests/phpunit/Unit/Builder/FormTest.php | 6 +++--- tests/phpunit/Unit/Derive/DeriveTest.php | 2 +- .../Unit/Engine/EngineConditionalTest.php | 4 ++-- tests/phpunit/Unit/Input/KeyMapTest.php | 2 +- tests/phpunit/Unit/Model/DateBoundsTest.php | 2 +- tests/phpunit/Unit/ProgressableTest.php | 2 +- .../phpunit/Unit/Render/PanelControllerTest.php | 2 +- .../phpunit/Unit/Schema/SchemaGeneratorTest.php | 2 +- .../phpunit/Unit/Testing/AllWidgetsFormTest.php | 2 +- tests/phpunit/Unit/Theme/ScaleRenderTest.php | 4 ++-- tests/phpunit/Unit/Theme/ThemeOptionsTest.php | 6 +++--- tests/phpunit/Unit/Theme/ThemeRenderTest.php | 10 +++++----- .../Unit/Translation/TranslationRenderTest.php | 4 ++-- tests/phpunit/Unit/TuiTest.php | 2 +- tests/phpunit/Unit/Widget/CalendarWidgetTest.php | 6 +++--- .../phpunit/Unit/Widget/FilePickerWidgetTest.php | 8 ++++---- tests/phpunit/Unit/Widget/NumberWidgetTest.php | 4 ++-- tests/phpunit/Unit/Widget/PasswordWidgetTest.php | 2 +- tests/phpunit/Unit/Widget/SelectWidgetTest.php | 8 ++++---- tests/phpunit/Unit/Widget/TextareaWidgetTest.php | 2 +- tests/phpunit/Unit/Widget/WidgetFactoryTest.php | 10 +++++----- 45 files changed, 98 insertions(+), 98 deletions(-) diff --git a/playground/themes/OceanTheme.php b/playground/themes/OceanTheme.php index e673267c..cee1f173 100644 --- a/playground/themes/OceanTheme.php +++ b/playground/themes/OceanTheme.php @@ -5,10 +5,10 @@ namespace Playground\Themes; use DrevOps\Tui\Answers\Answers; -use DrevOps\Tui\Model\Field; -use DrevOps\Tui\Model\Panel; use DrevOps\Tui\Input\Hint; use DrevOps\Tui\Input\ScopedKeyMap; +use DrevOps\Tui\Model\Field; +use DrevOps\Tui\Model\Panel; use DrevOps\Tui\Render\Ansi; use DrevOps\Tui\Render\Navigator; use DrevOps\Tui\Theme\DefaultTheme; diff --git a/src/Builder/FieldBuilder.php b/src/Builder/FieldBuilder.php index 2094dc6e..d69b722c 100644 --- a/src/Builder/FieldBuilder.php +++ b/src/Builder/FieldBuilder.php @@ -5,12 +5,14 @@ namespace DrevOps\Tui\Builder; use DrevOps\Tui\Condition\ConditionInterface; -use DrevOps\Tui\Model\FormException; +use DrevOps\Tui\Derive\Derive; +use DrevOps\Tui\Discovery\DiscoverInterface; use DrevOps\Tui\Model\DateBounds; use DrevOps\Tui\Model\Field; use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Model\FilePickerConstraints; use DrevOps\Tui\Model\FilePickerMode; +use DrevOps\Tui\Model\FormException; use DrevOps\Tui\Model\NumberBounds; use DrevOps\Tui\Model\Option; use DrevOps\Tui\Model\OptionKind; @@ -19,8 +21,6 @@ use DrevOps\Tui\Model\TableSpec; use DrevOps\Tui\Model\Template; use DrevOps\Tui\Model\Weekday; -use DrevOps\Tui\Derive\Derive; -use DrevOps\Tui\Discovery\DiscoverInterface; /** * A fluent builder for a single Field. diff --git a/src/Builder/Form.php b/src/Builder/Form.php index 6b01d4a2..fdd69841 100644 --- a/src/Builder/Form.php +++ b/src/Builder/Form.php @@ -6,9 +6,9 @@ use DrevOps\Tui\Model\Buttons; use DrevOps\Tui\Model\FieldType; +use DrevOps\Tui\Model\Fixup; use DrevOps\Tui\Model\FormDefinition; use DrevOps\Tui\Model\FormException; -use DrevOps\Tui\Model\Fixup; use DrevOps\Tui\Model\Option; use DrevOps\Tui\Model\Panel; diff --git a/src/Engine/Engine.php b/src/Engine/Engine.php index c85b3f93..3bcdb754 100644 --- a/src/Engine/Engine.php +++ b/src/Engine/Engine.php @@ -7,13 +7,13 @@ use DrevOps\Tui\Answers\Answers; use DrevOps\Tui\Answers\Provenance; use DrevOps\Tui\Condition\ConditionInterface; -use DrevOps\Tui\Model\FormDefinition; -use DrevOps\Tui\Model\Field; -use DrevOps\Tui\Model\Option; use DrevOps\Tui\Derive\Deriver; use DrevOps\Tui\Discovery\DiscoverInterface; use DrevOps\Tui\Handler\Context; use DrevOps\Tui\Handler\HandlerRegistry; +use DrevOps\Tui\Model\Field; +use DrevOps\Tui\Model\FormDefinition; +use DrevOps\Tui\Model\Option; use DrevOps\Tui\Translation\Translator; /** diff --git a/src/Render/PanelController.php b/src/Render/PanelController.php index 4d922063..ee9caabe 100644 --- a/src/Render/PanelController.php +++ b/src/Render/PanelController.php @@ -4,19 +4,12 @@ namespace DrevOps\Tui\Render; -use DrevOps\Tui\Derive\Derive; use DrevOps\Tui\Answers\Answers; use DrevOps\Tui\Answers\Provenance; +use DrevOps\Tui\Derive\Derive; use DrevOps\Tui\Engine\Engine; use DrevOps\Tui\Handler\Context; use DrevOps\Tui\Handler\HandlerRegistry; -use DrevOps\Tui\Model\Field; -use DrevOps\Tui\Model\FieldType; -use DrevOps\Tui\Model\FormDefinition; -use DrevOps\Tui\Model\Option; -use DrevOps\Tui\Model\Panel; -use DrevOps\Tui\Model\RenderMode; -use DrevOps\Tui\Primitive\ProgressReporter; use DrevOps\Tui\Input\Action; use DrevOps\Tui\Input\Hint; use DrevOps\Tui\Input\Key; @@ -25,6 +18,13 @@ use DrevOps\Tui\Input\KeyName; use DrevOps\Tui\Input\KeyParser; use DrevOps\Tui\Input\ScopedKeyMap; +use DrevOps\Tui\Model\Field; +use DrevOps\Tui\Model\FieldType; +use DrevOps\Tui\Model\FormDefinition; +use DrevOps\Tui\Model\Option; +use DrevOps\Tui\Model\Panel; +use DrevOps\Tui\Model\RenderMode; +use DrevOps\Tui\Primitive\ProgressReporter; use DrevOps\Tui\Theme\DefaultTheme; use DrevOps\Tui\Translation\Translator; use DrevOps\Tui\Widget\Capability\ExternalEditCapableInterface; diff --git a/src/Schema/SchemaGenerator.php b/src/Schema/SchemaGenerator.php index 3d77afff..1c9f07db 100644 --- a/src/Schema/SchemaGenerator.php +++ b/src/Schema/SchemaGenerator.php @@ -4,10 +4,10 @@ namespace DrevOps\Tui\Schema; +use DrevOps\Tui\Discovery\DiscoverInterface; use DrevOps\Tui\Handler\Context; -use DrevOps\Tui\Model\FormDefinition; use DrevOps\Tui\Model\Field; -use DrevOps\Tui\Discovery\DiscoverInterface; +use DrevOps\Tui\Model\FormDefinition; use DrevOps\Tui\Resolver\EnvNameResolver; /** diff --git a/src/Schema/SchemaValidator.php b/src/Schema/SchemaValidator.php index e4e9b240..d68bccf2 100644 --- a/src/Schema/SchemaValidator.php +++ b/src/Schema/SchemaValidator.php @@ -5,8 +5,8 @@ namespace DrevOps\Tui\Schema; use DrevOps\Tui\Handler\Context; -use DrevOps\Tui\Model\FormDefinition; use DrevOps\Tui\Model\Field; +use DrevOps\Tui\Model\FormDefinition; use DrevOps\Tui\Translation\Translator; /** diff --git a/src/Testing/TuiTester.php b/src/Testing/TuiTester.php index 4c2f54d9..e0ba7ad7 100644 --- a/src/Testing/TuiTester.php +++ b/src/Testing/TuiTester.php @@ -6,8 +6,8 @@ use DrevOps\Tui\Answers\Answers; use DrevOps\Tui\Builder\Form; -use DrevOps\Tui\Model\FormDefinition; use DrevOps\Tui\Input\Key; +use DrevOps\Tui\Model\FormDefinition; use DrevOps\Tui\Render\Ansi; use DrevOps\Tui\Theme\Mode; use DrevOps\Tui\Tui; diff --git a/src/Theme/DefaultTheme.php b/src/Theme/DefaultTheme.php index 1ae472ae..9bb13860 100644 --- a/src/Theme/DefaultTheme.php +++ b/src/Theme/DefaultTheme.php @@ -7,17 +7,17 @@ use DrevOps\Tui\Answers\Answers; use DrevOps\Tui\Answers\Provenance; use DrevOps\Tui\Answers\ValueFormatter; +use DrevOps\Tui\Input\Action; +use DrevOps\Tui\Input\Hint; +use DrevOps\Tui\Input\Key; +use DrevOps\Tui\Input\KeyName; +use DrevOps\Tui\Input\ScopedKeyMap; use DrevOps\Tui\Model\Field; use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Model\FormDefinition; use DrevOps\Tui\Model\Modal; use DrevOps\Tui\Model\Panel; use DrevOps\Tui\Model\TableSpec; -use DrevOps\Tui\Input\Action; -use DrevOps\Tui\Input\Hint; -use DrevOps\Tui\Input\Key; -use DrevOps\Tui\Input\KeyName; -use DrevOps\Tui\Input\ScopedKeyMap; use DrevOps\Tui\Primitive\Status; use DrevOps\Tui\Render\Ansi; use DrevOps\Tui\Render\Box; diff --git a/src/Tui.php b/src/Tui.php index 692a78e5..f443d9a1 100644 --- a/src/Tui.php +++ b/src/Tui.php @@ -14,12 +14,12 @@ use DrevOps\Tui\Model\FormDefinition; use DrevOps\Tui\Primitive\Output; use DrevOps\Tui\Primitive\Progress; +use DrevOps\Tui\Render\PanelController; +use DrevOps\Tui\Render\Terminal; use DrevOps\Tui\Resolver\InputResolver; use DrevOps\Tui\Schema\AgentHelp; use DrevOps\Tui\Schema\SchemaGenerator; use DrevOps\Tui\Schema\SchemaValidator; -use DrevOps\Tui\Render\PanelController; -use DrevOps\Tui\Render\Terminal; use DrevOps\Tui\Theme\DefaultTheme; use DrevOps\Tui\Theme\Mode; use DrevOps\Tui\Theme\ThemeManager; diff --git a/src/Widget/CalendarWidget.php b/src/Widget/CalendarWidget.php index 0a1bd862..1173a49e 100644 --- a/src/Widget/CalendarWidget.php +++ b/src/Widget/CalendarWidget.php @@ -4,15 +4,15 @@ namespace DrevOps\Tui\Widget; -use DrevOps\Tui\Model\DateBounds; -use DrevOps\Tui\Model\FieldType; -use DrevOps\Tui\Model\Weekday; use DrevOps\Tui\Input\Action; use DrevOps\Tui\Input\Hint; use DrevOps\Tui\Input\Key; use DrevOps\Tui\Input\KeyName; use DrevOps\Tui\Input\Scope; use DrevOps\Tui\Input\ScopedKeyMap; +use DrevOps\Tui\Model\DateBounds; +use DrevOps\Tui\Model\FieldType; +use DrevOps\Tui\Model\Weekday; use DrevOps\Tui\Theme\ThemeInterface; use DrevOps\Tui\Translation\Translator; use DrevOps\Tui\Utils\Strings; diff --git a/src/Widget/Capability/SelectionCapableTrait.php b/src/Widget/Capability/SelectionCapableTrait.php index b407d143..b49bbd4b 100644 --- a/src/Widget/Capability/SelectionCapableTrait.php +++ b/src/Widget/Capability/SelectionCapableTrait.php @@ -4,12 +4,12 @@ namespace DrevOps\Tui\Widget\Capability; -use DrevOps\Tui\Model\FieldType; -use DrevOps\Tui\Model\Option; use DrevOps\Tui\Input\Action; use DrevOps\Tui\Input\Hint; use DrevOps\Tui\Input\Key; use DrevOps\Tui\Input\Scope; +use DrevOps\Tui\Model\FieldType; +use DrevOps\Tui\Model\Option; use DrevOps\Tui\Theme\ThemeInterface; /** diff --git a/src/Widget/ConfirmWidget.php b/src/Widget/ConfirmWidget.php index 35a588bb..ccc054ae 100644 --- a/src/Widget/ConfirmWidget.php +++ b/src/Widget/ConfirmWidget.php @@ -4,11 +4,11 @@ namespace DrevOps\Tui\Widget; -use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Input\Action; use DrevOps\Tui\Input\Hint; use DrevOps\Tui\Input\Key; use DrevOps\Tui\Input\Scope; +use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Theme\ThemeInterface; use DrevOps\Tui\Translation\Translator; use DrevOps\Tui\Widget\Capability\StepCapableInterface; diff --git a/src/Widget/FilePickerWidget.php b/src/Widget/FilePickerWidget.php index 1dd3200b..34b18727 100644 --- a/src/Widget/FilePickerWidget.php +++ b/src/Widget/FilePickerWidget.php @@ -4,15 +4,15 @@ namespace DrevOps\Tui\Widget; +use DrevOps\Tui\Input\Action; +use DrevOps\Tui\Input\Hint; +use DrevOps\Tui\Input\Key; +use DrevOps\Tui\Input\Scope; use DrevOps\Tui\Model\Field; use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Model\FilePickerConstraints; use DrevOps\Tui\Model\FilePickerMode; use DrevOps\Tui\Model\SelectionBounds; -use DrevOps\Tui\Input\Action; -use DrevOps\Tui\Input\Hint; -use DrevOps\Tui\Input\Key; -use DrevOps\Tui\Input\Scope; use DrevOps\Tui\Render\Ansi; use DrevOps\Tui\Theme\ThemeInterface; use DrevOps\Tui\Translation\Translator; diff --git a/src/Widget/NumberWidget.php b/src/Widget/NumberWidget.php index 22636204..db76a322 100644 --- a/src/Widget/NumberWidget.php +++ b/src/Widget/NumberWidget.php @@ -4,12 +4,12 @@ namespace DrevOps\Tui\Widget; -use DrevOps\Tui\Model\FieldType; -use DrevOps\Tui\Model\NumberBounds; use DrevOps\Tui\Input\Action; use DrevOps\Tui\Input\Hint; use DrevOps\Tui\Input\Key; use DrevOps\Tui\Input\Scope; +use DrevOps\Tui\Model\FieldType; +use DrevOps\Tui\Model\NumberBounds; use DrevOps\Tui\Theme\ThemeInterface; use DrevOps\Tui\Translation\Translator; use DrevOps\Tui\Utils\Strings; diff --git a/src/Widget/PasswordWidget.php b/src/Widget/PasswordWidget.php index 293f7d79..37d8e137 100644 --- a/src/Widget/PasswordWidget.php +++ b/src/Widget/PasswordWidget.php @@ -4,11 +4,11 @@ namespace DrevOps\Tui\Widget; -use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Input\Action; use DrevOps\Tui\Input\Hint; use DrevOps\Tui\Input\Key; use DrevOps\Tui\Input\Scope; +use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Theme\ThemeInterface; use DrevOps\Tui\Translation\Translator; use DrevOps\Tui\Utils\Strings; diff --git a/src/Widget/PauseWidget.php b/src/Widget/PauseWidget.php index 9263c482..6c64ab35 100644 --- a/src/Widget/PauseWidget.php +++ b/src/Widget/PauseWidget.php @@ -4,11 +4,11 @@ namespace DrevOps\Tui\Widget; -use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Input\Action; use DrevOps\Tui\Input\Hint; use DrevOps\Tui\Input\Key; use DrevOps\Tui\Input\Scope; +use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Theme\ThemeInterface; use DrevOps\Tui\Translation\Translator; diff --git a/src/Widget/ReorderWidget.php b/src/Widget/ReorderWidget.php index cbc06256..cdb7c823 100644 --- a/src/Widget/ReorderWidget.php +++ b/src/Widget/ReorderWidget.php @@ -4,13 +4,13 @@ namespace DrevOps\Tui\Widget; -use DrevOps\Tui\Model\Field; -use DrevOps\Tui\Model\FieldType; -use DrevOps\Tui\Model\Option; use DrevOps\Tui\Input\Action; use DrevOps\Tui\Input\Hint; use DrevOps\Tui\Input\Key; use DrevOps\Tui\Input\Scope; +use DrevOps\Tui\Model\Field; +use DrevOps\Tui\Model\FieldType; +use DrevOps\Tui\Model\Option; use DrevOps\Tui\Theme\ThemeInterface; use DrevOps\Tui\Widget\Capability\OptionsCapableInterface; use DrevOps\Tui\Widget\Capability\PagingCapableInterface; diff --git a/src/Widget/SearchWidget.php b/src/Widget/SearchWidget.php index 069bb27b..17c3b56f 100644 --- a/src/Widget/SearchWidget.php +++ b/src/Widget/SearchWidget.php @@ -4,10 +4,10 @@ namespace DrevOps\Tui\Widget; -use DrevOps\Tui\Model\FieldType; -use DrevOps\Tui\Model\SelectionBounds; use DrevOps\Tui\Input\Action; use DrevOps\Tui\Input\Key; +use DrevOps\Tui\Model\FieldType; +use DrevOps\Tui\Model\SelectionBounds; use DrevOps\Tui\Theme\ThemeInterface; use DrevOps\Tui\Widget\Capability\FilterCapableInterface; use DrevOps\Tui\Widget\Capability\FilterCapableTrait; diff --git a/src/Widget/SuggestWidget.php b/src/Widget/SuggestWidget.php index 32087d8d..fff538d6 100644 --- a/src/Widget/SuggestWidget.php +++ b/src/Widget/SuggestWidget.php @@ -4,11 +4,11 @@ namespace DrevOps\Tui\Widget; -use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Input\Action; use DrevOps\Tui\Input\Hint; use DrevOps\Tui\Input\Key; use DrevOps\Tui\Input\Scope; +use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Model\Option; use DrevOps\Tui\Theme\ThemeInterface; use DrevOps\Tui\Utils\Strings; diff --git a/src/Widget/TextWidget.php b/src/Widget/TextWidget.php index 47b3433c..6ce7ea16 100644 --- a/src/Widget/TextWidget.php +++ b/src/Widget/TextWidget.php @@ -4,10 +4,10 @@ namespace DrevOps\Tui\Widget; -use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Input\Action; use DrevOps\Tui\Input\Key; use DrevOps\Tui\Input\Scope; +use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Theme\ThemeInterface; use DrevOps\Tui\Widget\Capability\CompletionCapableInterface; use DrevOps\Tui\Widget\Capability\CompletionCapableTrait; diff --git a/src/Widget/TextareaWidget.php b/src/Widget/TextareaWidget.php index 64c9a8f6..324fbe4f 100644 --- a/src/Widget/TextareaWidget.php +++ b/src/Widget/TextareaWidget.php @@ -4,11 +4,11 @@ namespace DrevOps\Tui\Widget; -use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Input\Action; use DrevOps\Tui\Input\Hint; use DrevOps\Tui\Input\Key; use DrevOps\Tui\Input\Scope; +use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Theme\ThemeInterface; use DrevOps\Tui\Utils\Strings; use DrevOps\Tui\Widget\Capability\ExternalEditCapableInterface; diff --git a/src/Widget/ToggleWidget.php b/src/Widget/ToggleWidget.php index 92449ef5..334612f1 100644 --- a/src/Widget/ToggleWidget.php +++ b/src/Widget/ToggleWidget.php @@ -4,11 +4,11 @@ namespace DrevOps\Tui\Widget; -use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Input\Action; use DrevOps\Tui\Input\Hint; use DrevOps\Tui\Input\Key; use DrevOps\Tui\Input\Scope; +use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Theme\ThemeInterface; use DrevOps\Tui\Utils\Strings; use DrevOps\Tui\Widget\Capability\StepCapableInterface; diff --git a/src/Widget/WidgetFactory.php b/src/Widget/WidgetFactory.php index 8cc72bad..28e4fd40 100644 --- a/src/Widget/WidgetFactory.php +++ b/src/Widget/WidgetFactory.php @@ -5,13 +5,13 @@ namespace DrevOps\Tui\Widget; use DrevOps\Tui\Handler\HandlerRegistry; +use DrevOps\Tui\Input\KeyMap; +use DrevOps\Tui\Input\KeyMapManager; use DrevOps\Tui\Model\Field; use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Model\NumberBounds; use DrevOps\Tui\Model\Option; use DrevOps\Tui\Model\Template; -use DrevOps\Tui\Input\KeyMap; -use DrevOps\Tui\Input\KeyMapManager; use DrevOps\Tui\Translation\Translator; use DrevOps\Tui\Widget\Capability\PlaceholderCapableInterface; use DrevOps\Tui\Widget\Capability\QueryOptionsCapableInterface; diff --git a/tests/phpunit/Unit/Builder/FormTest.php b/tests/phpunit/Unit/Builder/FormTest.php index 41d41193..0b2a0f5a 100644 --- a/tests/phpunit/Unit/Builder/FormTest.php +++ b/tests/phpunit/Unit/Builder/FormTest.php @@ -9,12 +9,14 @@ use DrevOps\Tui\Builder\LayoutGuard; use DrevOps\Tui\Builder\PanelBuilder; use DrevOps\Tui\Condition\Condition; -use DrevOps\Tui\Model\FormException; +use DrevOps\Tui\Derive\Derive; +use DrevOps\Tui\Discovery\Dotenv; use DrevOps\Tui\Model\DateBounds; use DrevOps\Tui\Model\Field; use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Model\FilePickerMode; use DrevOps\Tui\Model\Fixup; +use DrevOps\Tui\Model\FormException; use DrevOps\Tui\Model\Modal; use DrevOps\Tui\Model\NumberBounds; use DrevOps\Tui\Model\OptionKind; @@ -23,8 +25,6 @@ use DrevOps\Tui\Model\TableSpec; use DrevOps\Tui\Model\Template; use DrevOps\Tui\Model\Weekday; -use DrevOps\Tui\Derive\Derive; -use DrevOps\Tui\Discovery\Dotenv; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; diff --git a/tests/phpunit/Unit/Derive/DeriveTest.php b/tests/phpunit/Unit/Derive/DeriveTest.php index ccdfdf9a..e2dc7475 100644 --- a/tests/phpunit/Unit/Derive/DeriveTest.php +++ b/tests/phpunit/Unit/Derive/DeriveTest.php @@ -4,8 +4,8 @@ namespace DrevOps\Tui\Tests\Unit\Derive; -use DrevOps\Tui\Model\FormException; use DrevOps\Tui\Derive\Derive; +use DrevOps\Tui\Model\FormException; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; diff --git a/tests/phpunit/Unit/Engine/EngineConditionalTest.php b/tests/phpunit/Unit/Engine/EngineConditionalTest.php index a3267d3c..ec949703 100644 --- a/tests/phpunit/Unit/Engine/EngineConditionalTest.php +++ b/tests/phpunit/Unit/Engine/EngineConditionalTest.php @@ -8,11 +8,11 @@ use DrevOps\Tui\Builder\Form; use DrevOps\Tui\Builder\PanelBuilder; use DrevOps\Tui\Condition\Condition; -use DrevOps\Tui\Model\FormDefinition; -use DrevOps\Tui\Model\Fixup; use DrevOps\Tui\Engine\Engine; use DrevOps\Tui\Handler\Context; use DrevOps\Tui\Handler\HandlerRegistry; +use DrevOps\Tui\Model\Fixup; +use DrevOps\Tui\Model\FormDefinition; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; diff --git a/tests/phpunit/Unit/Input/KeyMapTest.php b/tests/phpunit/Unit/Input/KeyMapTest.php index ea0eb888..72fad473 100644 --- a/tests/phpunit/Unit/Input/KeyMapTest.php +++ b/tests/phpunit/Unit/Input/KeyMapTest.php @@ -4,7 +4,6 @@ namespace DrevOps\Tui\Tests\Unit\Input; -use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Input\Action; use DrevOps\Tui\Input\Binding; use DrevOps\Tui\Input\DefaultKeyMap; @@ -15,6 +14,7 @@ use DrevOps\Tui\Input\Scope; use DrevOps\Tui\Input\ScopedKeyMap; use DrevOps\Tui\Input\VimKeyMap; +use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Tests\Traits\ResetsRegistriesTrait; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; diff --git a/tests/phpunit/Unit/Model/DateBoundsTest.php b/tests/phpunit/Unit/Model/DateBoundsTest.php index 2605f09e..70f146ba 100644 --- a/tests/phpunit/Unit/Model/DateBoundsTest.php +++ b/tests/phpunit/Unit/Model/DateBoundsTest.php @@ -4,8 +4,8 @@ namespace DrevOps\Tui\Tests\Unit\Model; -use DrevOps\Tui\Model\FormException; use DrevOps\Tui\Model\DateBounds; +use DrevOps\Tui\Model\FormException; use DrevOps\Tui\Model\Weekday; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; diff --git a/tests/phpunit/Unit/ProgressableTest.php b/tests/phpunit/Unit/ProgressableTest.php index 75000acc..f76d8665 100644 --- a/tests/phpunit/Unit/ProgressableTest.php +++ b/tests/phpunit/Unit/ProgressableTest.php @@ -7,12 +7,12 @@ use DrevOps\Tui\Builder\FieldBuilder; use DrevOps\Tui\Builder\Form; use DrevOps\Tui\Builder\PanelBuilder; -use DrevOps\Tui\Model\Panel; use DrevOps\Tui\Engine\Engine; use DrevOps\Tui\Engine\EngineException; use DrevOps\Tui\Input\Key; use DrevOps\Tui\Input\KeyName; use DrevOps\Tui\Model\Field; +use DrevOps\Tui\Model\Panel; use DrevOps\Tui\Render\PanelController; use DrevOps\Tui\Testing\TuiTester; use DrevOps\Tui\Theme\DefaultTheme; diff --git a/tests/phpunit/Unit/Render/PanelControllerTest.php b/tests/phpunit/Unit/Render/PanelControllerTest.php index bf2b52e0..55ae25a7 100644 --- a/tests/phpunit/Unit/Render/PanelControllerTest.php +++ b/tests/phpunit/Unit/Render/PanelControllerTest.php @@ -11,9 +11,9 @@ use DrevOps\Tui\Condition\Condition; use DrevOps\Tui\Derive\Derive; use DrevOps\Tui\Handler\HandlerRegistry; -use DrevOps\Tui\Model\Fixup; use DrevOps\Tui\Input\Key; use DrevOps\Tui\Input\KeyName; +use DrevOps\Tui\Model\Fixup; use DrevOps\Tui\Render\Ansi; use DrevOps\Tui\Render\ExternalEditor; use DrevOps\Tui\Render\PanelController; diff --git a/tests/phpunit/Unit/Schema/SchemaGeneratorTest.php b/tests/phpunit/Unit/Schema/SchemaGeneratorTest.php index d8aaf881..e9f03c5a 100644 --- a/tests/phpunit/Unit/Schema/SchemaGeneratorTest.php +++ b/tests/phpunit/Unit/Schema/SchemaGeneratorTest.php @@ -7,9 +7,9 @@ use DrevOps\Tui\Builder\Form; use DrevOps\Tui\Builder\PanelBuilder; use DrevOps\Tui\Condition\Condition; +use DrevOps\Tui\Derive\Derive; use DrevOps\Tui\Handler\Context; use DrevOps\Tui\Model\Weekday; -use DrevOps\Tui\Derive\Derive; use DrevOps\Tui\Schema\SchemaGenerator; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Group; diff --git a/tests/phpunit/Unit/Testing/AllWidgetsFormTest.php b/tests/phpunit/Unit/Testing/AllWidgetsFormTest.php index 57aa9584..c026b630 100644 --- a/tests/phpunit/Unit/Testing/AllWidgetsFormTest.php +++ b/tests/phpunit/Unit/Testing/AllWidgetsFormTest.php @@ -4,9 +4,9 @@ namespace DrevOps\Tui\Tests\Unit\Testing; -use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Input\Key; use DrevOps\Tui\Input\KeyName; +use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Testing\TuiTester; use DrevOps\Tui\Tests\Fixtures\Form\AllWidgetsForm; use DrevOps\Tui\Tests\Fixtures\Theme\OceanTheme; diff --git a/tests/phpunit/Unit/Theme/ScaleRenderTest.php b/tests/phpunit/Unit/Theme/ScaleRenderTest.php index 5cb267c6..b9089717 100644 --- a/tests/phpunit/Unit/Theme/ScaleRenderTest.php +++ b/tests/phpunit/Unit/Theme/ScaleRenderTest.php @@ -4,10 +4,10 @@ namespace DrevOps\Tui\Tests\Unit\Theme; -use DrevOps\Tui\Render\Ansi; -use DrevOps\Tui\Testing\TuiTester; use DrevOps\Tui\Builder\Form; use DrevOps\Tui\Builder\PanelBuilder; +use DrevOps\Tui\Render\Ansi; +use DrevOps\Tui\Testing\TuiTester; use DrevOps\Tui\Theme\DefaultTheme; use DrevOps\Tui\Theme\Mode; use DrevOps\Tui\Theme\ThemeManager; diff --git a/tests/phpunit/Unit/Theme/ThemeOptionsTest.php b/tests/phpunit/Unit/Theme/ThemeOptionsTest.php index ef43ecc7..a0d8ff45 100644 --- a/tests/phpunit/Unit/Theme/ThemeOptionsTest.php +++ b/tests/phpunit/Unit/Theme/ThemeOptionsTest.php @@ -5,12 +5,12 @@ namespace DrevOps\Tui\Tests\Unit\Theme; use DrevOps\Tui\Answers\Answers; -use DrevOps\Tui\Model\Field; -use DrevOps\Tui\Model\FieldType; -use DrevOps\Tui\Model\Panel; use DrevOps\Tui\Input\Action; use DrevOps\Tui\Input\Hint; use DrevOps\Tui\Input\KeyMapManager; +use DrevOps\Tui\Model\Field; +use DrevOps\Tui\Model\FieldType; +use DrevOps\Tui\Model\Panel; use DrevOps\Tui\Render\Ansi; use DrevOps\Tui\Render\Viewport; use DrevOps\Tui\Tests\Fixtures\Theme\AccentOptionTheme; diff --git a/tests/phpunit/Unit/Theme/ThemeRenderTest.php b/tests/phpunit/Unit/Theme/ThemeRenderTest.php index eec7a5a2..e637374d 100644 --- a/tests/phpunit/Unit/Theme/ThemeRenderTest.php +++ b/tests/phpunit/Unit/Theme/ThemeRenderTest.php @@ -6,17 +6,17 @@ use DrevOps\Tui\Answers\Answers; use DrevOps\Tui\Answers\Provenance; +use DrevOps\Tui\Input\Action; +use DrevOps\Tui\Input\Hint; +use DrevOps\Tui\Input\Key; +use DrevOps\Tui\Input\KeyMapManager; +use DrevOps\Tui\Input\KeyName; use DrevOps\Tui\Model\Buttons; use DrevOps\Tui\Model\Field; use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Model\Modal; use DrevOps\Tui\Model\Panel; use DrevOps\Tui\Model\TableSpec; -use DrevOps\Tui\Input\Action; -use DrevOps\Tui\Input\Hint; -use DrevOps\Tui\Input\Key; -use DrevOps\Tui\Input\KeyMapManager; -use DrevOps\Tui\Input\KeyName; use DrevOps\Tui\Render\Ansi; use DrevOps\Tui\Render\HelpSection; use DrevOps\Tui\Render\Navigator; diff --git a/tests/phpunit/Unit/Translation/TranslationRenderTest.php b/tests/phpunit/Unit/Translation/TranslationRenderTest.php index dcb42903..a0d68091 100644 --- a/tests/phpunit/Unit/Translation/TranslationRenderTest.php +++ b/tests/phpunit/Unit/Translation/TranslationRenderTest.php @@ -9,10 +9,10 @@ use DrevOps\Tui\Answers\SummaryFormatter; use DrevOps\Tui\Builder\Form; use DrevOps\Tui\Builder\PanelBuilder; -use DrevOps\Tui\Model\Field; -use DrevOps\Tui\Model\FormDefinition; use DrevOps\Tui\Input\Key; use DrevOps\Tui\Input\KeyName; +use DrevOps\Tui\Model\Field; +use DrevOps\Tui\Model\FormDefinition; use DrevOps\Tui\Render\Ansi; use DrevOps\Tui\Render\PanelController; use DrevOps\Tui\Schema\AgentHelp; diff --git a/tests/phpunit/Unit/TuiTest.php b/tests/phpunit/Unit/TuiTest.php index 93581d56..77903955 100644 --- a/tests/phpunit/Unit/TuiTest.php +++ b/tests/phpunit/Unit/TuiTest.php @@ -13,7 +13,6 @@ use DrevOps\Tui\Discovery\Dotenv; use DrevOps\Tui\Discovery\JsonValue; use DrevOps\Tui\Engine\Engine; -use DrevOps\Tui\Primitive\Progress; use DrevOps\Tui\Handler\Context; use DrevOps\Tui\Handler\HandlerRegistry; use DrevOps\Tui\Input\Action; @@ -23,6 +22,7 @@ use DrevOps\Tui\Input\KeyName; use DrevOps\Tui\Input\Scope; use DrevOps\Tui\InterruptException; +use DrevOps\Tui\Primitive\Progress; use DrevOps\Tui\Render\Ansi; use DrevOps\Tui\Render\PanelController; use DrevOps\Tui\Render\Terminal; diff --git a/tests/phpunit/Unit/Widget/CalendarWidgetTest.php b/tests/phpunit/Unit/Widget/CalendarWidgetTest.php index 5604c32b..1f75ae1f 100644 --- a/tests/phpunit/Unit/Widget/CalendarWidgetTest.php +++ b/tests/phpunit/Unit/Widget/CalendarWidgetTest.php @@ -4,13 +4,13 @@ namespace DrevOps\Tui\Tests\Unit\Widget; -use DrevOps\Tui\Model\DateBounds; -use DrevOps\Tui\Model\FieldType; -use DrevOps\Tui\Model\Weekday; use DrevOps\Tui\Input\Hint; use DrevOps\Tui\Input\Key; use DrevOps\Tui\Input\KeyMapManager; use DrevOps\Tui\Input\KeyName; +use DrevOps\Tui\Model\DateBounds; +use DrevOps\Tui\Model\FieldType; +use DrevOps\Tui\Model\Weekday; use DrevOps\Tui\Render\Ansi; use DrevOps\Tui\Testing\ArrayKeyStream; use DrevOps\Tui\Testing\WidgetRunner; diff --git a/tests/phpunit/Unit/Widget/FilePickerWidgetTest.php b/tests/phpunit/Unit/Widget/FilePickerWidgetTest.php index 4d893f2c..5766cc92 100644 --- a/tests/phpunit/Unit/Widget/FilePickerWidgetTest.php +++ b/tests/phpunit/Unit/Widget/FilePickerWidgetTest.php @@ -4,21 +4,21 @@ namespace DrevOps\Tui\Tests\Unit\Widget; +use DrevOps\Tui\Input\Key; +use DrevOps\Tui\Input\KeyMapManager; +use DrevOps\Tui\Input\KeyName; use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Model\FilePickerConstraints; use DrevOps\Tui\Model\FilePickerMode; use DrevOps\Tui\Model\SelectionBounds; -use DrevOps\Tui\Input\Key; -use DrevOps\Tui\Input\KeyMapManager; -use DrevOps\Tui\Input\KeyName; use DrevOps\Tui\Render\Ansi; use DrevOps\Tui\Testing\ArrayKeyStream; use DrevOps\Tui\Testing\WidgetRunner; use DrevOps\Tui\Theme\DefaultTheme; use DrevOps\Tui\Widget\AbstractWidget; -use DrevOps\Tui\Widget\FilePickerWidget; use DrevOps\Tui\Widget\Capability\PagingCapableTrait; use DrevOps\Tui\Widget\Capability\SelectionBoundedTrait; +use DrevOps\Tui\Widget\FilePickerWidget; use org\bovigo\vfs\vfsStream; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\CoversTrait; diff --git a/tests/phpunit/Unit/Widget/NumberWidgetTest.php b/tests/phpunit/Unit/Widget/NumberWidgetTest.php index 7cb2eec9..8f370ad6 100644 --- a/tests/phpunit/Unit/Widget/NumberWidgetTest.php +++ b/tests/phpunit/Unit/Widget/NumberWidgetTest.php @@ -4,17 +4,17 @@ namespace DrevOps\Tui\Tests\Unit\Widget; -use DrevOps\Tui\Model\NumberBounds; use DrevOps\Tui\Input\Action; use DrevOps\Tui\Input\Hint; use DrevOps\Tui\Input\Key; use DrevOps\Tui\Input\KeyName; +use DrevOps\Tui\Model\NumberBounds; use DrevOps\Tui\Testing\ArrayKeyStream; use DrevOps\Tui\Testing\WidgetRunner; use DrevOps\Tui\Theme\DefaultTheme; -use DrevOps\Tui\Widget\NumberWidget; use DrevOps\Tui\Widget\Capability\PlaceholderCapableTrait; use DrevOps\Tui\Widget\Capability\TextEditCapableTrait; +use DrevOps\Tui\Widget\NumberWidget; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\CoversTrait; use PHPUnit\Framework\Attributes\Group; diff --git a/tests/phpunit/Unit/Widget/PasswordWidgetTest.php b/tests/phpunit/Unit/Widget/PasswordWidgetTest.php index c945878b..ec312066 100644 --- a/tests/phpunit/Unit/Widget/PasswordWidgetTest.php +++ b/tests/phpunit/Unit/Widget/PasswordWidgetTest.php @@ -10,9 +10,9 @@ use DrevOps\Tui\Testing\ArrayKeyStream; use DrevOps\Tui\Testing\WidgetRunner; use DrevOps\Tui\Theme\DefaultTheme; -use DrevOps\Tui\Widget\PasswordWidget; use DrevOps\Tui\Widget\Capability\PlaceholderCapableTrait; use DrevOps\Tui\Widget\Capability\TextEditCapableTrait; +use DrevOps\Tui\Widget\PasswordWidget; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\CoversTrait; use PHPUnit\Framework\Attributes\Group; diff --git a/tests/phpunit/Unit/Widget/SelectWidgetTest.php b/tests/phpunit/Unit/Widget/SelectWidgetTest.php index 8daffe04..b1062aed 100644 --- a/tests/phpunit/Unit/Widget/SelectWidgetTest.php +++ b/tests/phpunit/Unit/Widget/SelectWidgetTest.php @@ -4,15 +4,15 @@ namespace DrevOps\Tui\Tests\Unit\Widget; -use DrevOps\Tui\Model\FieldType; -use DrevOps\Tui\Model\Option; -use DrevOps\Tui\Model\OptionKind; -use DrevOps\Tui\Model\SelectionBounds; use DrevOps\Tui\Input\Action; use DrevOps\Tui\Input\Hint; use DrevOps\Tui\Input\Key; use DrevOps\Tui\Input\KeyMapManager; use DrevOps\Tui\Input\KeyName; +use DrevOps\Tui\Model\FieldType; +use DrevOps\Tui\Model\Option; +use DrevOps\Tui\Model\OptionKind; +use DrevOps\Tui\Model\SelectionBounds; use DrevOps\Tui\Render\Ansi; use DrevOps\Tui\Testing\ArrayKeyStream; use DrevOps\Tui\Testing\WidgetRunner; diff --git a/tests/phpunit/Unit/Widget/TextareaWidgetTest.php b/tests/phpunit/Unit/Widget/TextareaWidgetTest.php index 2826b23c..fd026d20 100644 --- a/tests/phpunit/Unit/Widget/TextareaWidgetTest.php +++ b/tests/phpunit/Unit/Widget/TextareaWidgetTest.php @@ -10,9 +10,9 @@ use DrevOps\Tui\Testing\ArrayKeyStream; use DrevOps\Tui\Testing\WidgetRunner; use DrevOps\Tui\Theme\DefaultTheme; -use DrevOps\Tui\Widget\TextareaWidget; use DrevOps\Tui\Widget\Capability\PlaceholderCapableTrait; use DrevOps\Tui\Widget\Capability\TextEditCapableTrait; +use DrevOps\Tui\Widget\TextareaWidget; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\CoversTrait; use PHPUnit\Framework\Attributes\Group; diff --git a/tests/phpunit/Unit/Widget/WidgetFactoryTest.php b/tests/phpunit/Unit/Widget/WidgetFactoryTest.php index f2330951..b52f3a16 100644 --- a/tests/phpunit/Unit/Widget/WidgetFactoryTest.php +++ b/tests/phpunit/Unit/Widget/WidgetFactoryTest.php @@ -5,6 +5,10 @@ namespace DrevOps\Tui\Tests\Unit\Widget; use DrevOps\Tui\Handler\HandlerRegistry; +use DrevOps\Tui\Input\Hint; +use DrevOps\Tui\Input\Key; +use DrevOps\Tui\Input\KeyMapManager; +use DrevOps\Tui\Input\KeyName; use DrevOps\Tui\Model\DateBounds; use DrevOps\Tui\Model\Field; use DrevOps\Tui\Model\FieldType; @@ -12,14 +16,10 @@ use DrevOps\Tui\Model\Option; use DrevOps\Tui\Model\OptionKind; use DrevOps\Tui\Model\Template; -use DrevOps\Tui\Input\Hint; -use DrevOps\Tui\Input\Key; -use DrevOps\Tui\Input\KeyMapManager; -use DrevOps\Tui\Input\KeyName; use DrevOps\Tui\Render\Ansi; use DrevOps\Tui\Theme\DefaultTheme; -use DrevOps\Tui\Widget\ConfirmWidget; use DrevOps\Tui\Widget\CalendarWidget; +use DrevOps\Tui\Widget\ConfirmWidget; use DrevOps\Tui\Widget\FilePickerWidget; use DrevOps\Tui\Widget\NumberWidget; use DrevOps\Tui\Widget\PasswordWidget; From b917492b17e6acaea3dca6314849995ea90730f3 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Wed, 29 Jul 2026 14:57:12 +1000 Subject: [PATCH 02/14] Named the data provider rows in the two providers yielding unkeyed cases. Every other data provider in the suite yields string-keyed rows so a failing case names itself. Keying the last two also lets the annotated one narrow its return key type from the (int|string) union to string, matching every documented provider. --- tests/phpunit/Unit/Render/MarkupTest.php | 8 ++++---- tests/phpunit/Unit/Widget/PasswordDisplayTest.php | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/phpunit/Unit/Render/MarkupTest.php b/tests/phpunit/Unit/Render/MarkupTest.php index f043e6dd..9ca0d059 100644 --- a/tests/phpunit/Unit/Render/MarkupTest.php +++ b/tests/phpunit/Unit/Render/MarkupTest.php @@ -70,10 +70,10 @@ public function testParseMarkdownMarkersAreLiteralWhenOff(string $source): void } public static function dataProviderParseMarkdownMarkersAreLiteralWhenOff(): \Iterator { - yield ['**bold**']; - yield ['*emphasis*']; - yield ['`code`']; - yield ['- bullet']; + yield 'bold' => ['**bold**']; + yield 'emphasis' => ['*emphasis*']; + yield 'code' => ['`code`']; + yield 'bullet' => ['- bullet']; } public function testParseBold(): void { diff --git a/tests/phpunit/Unit/Widget/PasswordDisplayTest.php b/tests/phpunit/Unit/Widget/PasswordDisplayTest.php index 33507913..4be1ebe3 100644 --- a/tests/phpunit/Unit/Widget/PasswordDisplayTest.php +++ b/tests/phpunit/Unit/Widget/PasswordDisplayTest.php @@ -25,13 +25,13 @@ public function testNext(PasswordDisplay $from, PasswordDisplay $to): void { /** * Data provider for testNext(). * - * @return \Iterator<(int | string), array{\DrevOps\Tui\Widget\PasswordDisplay, \DrevOps\Tui\Widget\PasswordDisplay}> + * @return \Iterator * The current display and the one that follows it. */ public static function dataProviderNext(): \Iterator { - yield [PasswordDisplay::Hidden, PasswordDisplay::Masked]; - yield [PasswordDisplay::Masked, PasswordDisplay::Plaintext]; - yield [PasswordDisplay::Plaintext, PasswordDisplay::Hidden]; + yield 'hidden to masked' => [PasswordDisplay::Hidden, PasswordDisplay::Masked]; + yield 'masked to plaintext' => [PasswordDisplay::Masked, PasswordDisplay::Plaintext]; + yield 'plaintext to hidden' => [PasswordDisplay::Plaintext, PasswordDisplay::Hidden]; } } From e2a005d85b52235a765a8963b13f75109cc8cbcf Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Wed, 29 Jul 2026 14:57:20 +1000 Subject: [PATCH 03/14] Named the consuming test in the two bare 'Data provider.' docblocks. The other 21 documented providers in the suite open with "Data provider for testX()." naming the test they feed. --- tests/phpunit/Unit/Theme/OutputRenderTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/Unit/Theme/OutputRenderTest.php b/tests/phpunit/Unit/Theme/OutputRenderTest.php index bbbc82f8..bae3106d 100644 --- a/tests/phpunit/Unit/Theme/OutputRenderTest.php +++ b/tests/phpunit/Unit/Theme/OutputRenderTest.php @@ -232,7 +232,7 @@ public function testStatusLeadsWithItsOwnGlyph(Status $status, string $unicode, } /** - * Data provider. + * Data provider for testStatusLeadsWithItsOwnGlyph(). * * @return \Iterator * The status, its Unicode glyph and its ASCII glyph. @@ -261,7 +261,7 @@ public function testStatusCarriesItsOwnColour(Status $status, string $sgr): void } /** - * Data provider. + * Data provider for testStatusCarriesItsOwnColour(). * * @return \Iterator * The status and the SGR the default dark theme paints it with. From bdc59ac3da721df89821246f9263c2f8e33f0893 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Wed, 29 Jul 2026 14:57:29 +1000 Subject: [PATCH 04/14] Explained the interrupt exit in the three demos whose catch block was bare. The other 60 demos that catch an interrupt carry the same one-line explanation above their exit code. --- playground/16-loading-data.php | 1 + playground/17-query-options.php | 1 + playground/19-dynamic-options.php | 1 + 3 files changed, 3 insertions(+) diff --git a/playground/16-loading-data.php b/playground/16-loading-data.php index 27c7de28..2a1ce8d8 100644 --- a/playground/16-loading-data.php +++ b/playground/16-loading-data.php @@ -44,5 +44,6 @@ echo (new Tui($form))->run()->toJson() . PHP_EOL; } catch (InterruptException) { + // Leave quietly on Ctrl-C. exit(130); } diff --git a/playground/17-query-options.php b/playground/17-query-options.php index 450ff931..e695d209 100644 --- a/playground/17-query-options.php +++ b/playground/17-query-options.php @@ -57,5 +57,6 @@ echo (new Tui($form))->run()->toJson() . PHP_EOL; } catch (InterruptException) { + // Leave quietly on Ctrl-C. exit(130); } diff --git a/playground/19-dynamic-options.php b/playground/19-dynamic-options.php index 7664ca84..8be50769 100644 --- a/playground/19-dynamic-options.php +++ b/playground/19-dynamic-options.php @@ -58,5 +58,6 @@ echo (new Tui($form))->run()->toJson() . PHP_EOL; } catch (InterruptException) { + // Leave quietly on Ctrl-C. exit(130); } From 2a8e03c5e01b3c152e444fa02cf6fb76aea6c91b Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Wed, 29 Jul 2026 14:57:40 +1000 Subject: [PATCH 05/14] Adopted the demo newline conventions in the two scripts that diverged. Demo scripts terminate output with PHP_EOL and spell newlines inside single-quoted sample data as chr(10), which keeps the single-quote rule. The glyph gallery echoed a raw newline instead, and the output-text demo carried a double-quoted markdown sample. Line splitting and joining of rendered views keeps its literal newline, since a view is joined that way regardless of platform. --- playground/11-display-modes-glyph-gallery.php | 10 +++++----- playground/18-output-text.php | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/playground/11-display-modes-glyph-gallery.php b/playground/11-display-modes-glyph-gallery.php index b3a9cdf5..b43f8487 100644 --- a/playground/11-display-modes-glyph-gallery.php +++ b/playground/11-display-modes-glyph-gallery.php @@ -110,11 +110,11 @@ return implode("\n", $rows); }; -echo "\n"; -echo $columns('UNICODE', 'TEXTUAL (ASCII)') . "\n"; -echo str_repeat('-', 60) . "\n\n"; +echo PHP_EOL; +echo $columns('UNICODE', 'TEXTUAL (ASCII)') . PHP_EOL; +echo str_repeat('-', 60) . PHP_EOL . PHP_EOL; foreach ($widgets as $name => $make) { - echo $name . "\n"; - echo $columns($make()->view($unicode), $make()->view($ascii)) . "\n\n"; + echo $name . PHP_EOL; + echo $columns($make()->view($unicode), $make()->view($ascii)) . PHP_EOL . PHP_EOL; } diff --git a/playground/18-output-text.php b/playground/18-output-text.php index 1079aeab..561aa88a 100644 --- a/playground/18-output-text.php +++ b/playground/18-output-text.php @@ -45,6 +45,6 @@ $out->text(''); // The markdown subset: bold, emphasis, inline code and bullet lists. -$out->text("**Before you order:**\n\n- Pick a *delivery day* between Monday and Saturday\n- Leave a note if the gate code is not `1234`\n- Cancel any time before seven the evening before"); +$out->text('**Before you order:**' . chr(10) . chr(10) . '- Pick a *delivery day* between Monday and Saturday' . chr(10) . '- Leave a note if the gate code is not `1234`' . chr(10) . '- Cancel any time before seven the evening before'); $out->rule(); From 13f1f82c5eebd64a144c6818db6117f58917aa01 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Wed, 29 Jul 2026 15:50:13 +1000 Subject: [PATCH 06/14] Named every widget constructor's seed value 'default'. The nine widget constructors spelled the same seed value five ways: buffer, default, current, value and point. They all name one thing, the value a widget opens with, and every caller passes it positionally. Confirm and Suggest give up constructor promotion so the parameter can be named for what a caller passes while the property stays named for the live state it holds, and Rector is told to leave that pairing alone in Confirm. --- rector.php | 7 +++++++ src/Widget/CalendarWidget.php | 6 +++--- src/Widget/ConfirmWidget.php | 10 ++++++++-- src/Widget/NumberWidget.php | 6 +++--- src/Widget/PasswordWidget.php | 6 +++--- src/Widget/RatingWidget.php | 6 +++--- src/Widget/SuggestWidget.php | 10 ++++++++-- src/Widget/TemplateWidget.php | 6 +++--- src/Widget/TextWidget.php | 6 +++--- src/Widget/TextareaWidget.php | 6 +++--- 10 files changed, 44 insertions(+), 25 deletions(-) diff --git a/rector.php b/rector.php index a40e6c4d..4afa86d9 100644 --- a/rector.php +++ b/rector.php @@ -25,6 +25,7 @@ use Rector\Naming\Rector\Foreach_\RenameForeachValueVariableToMatchExprVariableRector; use Rector\Naming\Rector\Foreach_\RenameForeachValueVariableToMatchMethodCallReturnTypeRector; use Rector\Naming\Rector\ClassMethod\RenameParamToMatchTypeRector; +use Rector\Php80\Rector\Class_\ClassPropertyAssignToConstructorPromotionRector; use Rector\Php80\Rector\Switch_\ChangeSwitchToMatchRector; use Rector\Php81\Rector\FuncCall\NullToStrictStringFuncCallArgRector; use Rector\PHPUnit\Set\PHPUnitSetList; @@ -56,6 +57,12 @@ // Rules added by Rector's rule sets. CatchExceptionNameMatchingTypeRector::class, ChangeSwitchToMatchRector::class, + // Promotion ties the constructor parameter name to the property name, but + // the two are named for different things here: the parameter for the seed + // value a caller passes, the property for the live answer it becomes. + ClassPropertyAssignToConstructorPromotionRector::class => [ + __DIR__ . '/src/Widget/ConfirmWidget.php', + ], CompleteDynamicPropertiesRector::class, CountArrayToEmptyArrayComparisonRector::class, DisallowedEmptyRuleFixerRector::class, diff --git a/src/Widget/CalendarWidget.php b/src/Widget/CalendarWidget.php index 1173a49e..45cdddcc 100644 --- a/src/Widget/CalendarWidget.php +++ b/src/Widget/CalendarWidget.php @@ -55,15 +55,15 @@ class CalendarWidget extends AbstractWidget implements StepCapableInterface { /** * Construct a calendar widget. * - * @param string $value + * @param string $default * The initial date as an ISO `Y-m-d` string; empty opens on today. * @param \DrevOps\Tui\Model\DateBounds|null $bounds * Optional min/max range and week-start day; NULL for an open range that * starts the week on Monday. */ - public function __construct(string $value = '', ?DateBounds $bounds = NULL) { + public function __construct(string $default = '', ?DateBounds $bounds = NULL) { $this->bounds = $bounds ?? new DateBounds(); - $seed = DateBounds::parse($value) ?? new \DateTimeImmutable('today'); + $seed = DateBounds::parse($default) ?? new \DateTimeImmutable('today'); $this->cursor = $this->bounds->clamp($seed); } diff --git a/src/Widget/ConfirmWidget.php b/src/Widget/ConfirmWidget.php index ccc054ae..16af2a9b 100644 --- a/src/Widget/ConfirmWidget.php +++ b/src/Widget/ConfirmWidget.php @@ -20,13 +20,19 @@ */ class ConfirmWidget extends AbstractWidget implements StepCapableInterface { + /** + * The chosen answer. + */ + protected bool $current; + /** * Construct a confirm widget. * - * @param bool $current + * @param bool $default * The initial choice. */ - public function __construct(protected bool $current = FALSE) { + public function __construct(bool $default = FALSE) { + $this->current = $default; } /** diff --git a/src/Widget/NumberWidget.php b/src/Widget/NumberWidget.php index db76a322..d9b4ceca 100644 --- a/src/Widget/NumberWidget.php +++ b/src/Widget/NumberWidget.php @@ -38,13 +38,13 @@ class NumberWidget extends AbstractWidget implements TextEditCapableInterface, S /** * Construct a number widget. * - * @param string $buffer + * @param string $default * The initial value (and live input buffer). * @param \DrevOps\Tui\Model\NumberBounds|null $bounds * Optional bounds and step; NULL for a plain integer entry. */ - public function __construct(string $buffer = '', protected ?NumberBounds $bounds = NULL) { - $this->initTextBuffer($buffer); + public function __construct(string $default = '', protected ?NumberBounds $bounds = NULL) { + $this->initTextBuffer($default); } /** diff --git a/src/Widget/PasswordWidget.php b/src/Widget/PasswordWidget.php index 37d8e137..f78be192 100644 --- a/src/Widget/PasswordWidget.php +++ b/src/Widget/PasswordWidget.php @@ -46,15 +46,15 @@ class PasswordWidget extends AbstractWidget implements TextEditCapableInterface, /** * Construct a password widget. * - * @param string $buffer + * @param string $default * The initial value (and live input buffer). * @param bool $revealable * Whether the reveal toggle is enabled. * @param bool $confirm * Whether confirmation mode is enabled. */ - public function __construct(string $buffer = '', protected bool $revealable = FALSE, protected bool $confirm = FALSE) { - $this->initTextBuffer($buffer); + public function __construct(string $default = '', protected bool $revealable = FALSE, protected bool $confirm = FALSE) { + $this->initTextBuffer($default); } /** diff --git a/src/Widget/RatingWidget.php b/src/Widget/RatingWidget.php index bf25e01f..ac58d6ea 100644 --- a/src/Widget/RatingWidget.php +++ b/src/Widget/RatingWidget.php @@ -35,7 +35,7 @@ class RatingWidget extends AbstractWidget implements StepCapableInterface { /** * Construct a rating widget. * - * @param int $point + * @param int $default * The initially chosen point; clamped onto the scale. * @param int $min * The lowest point of the scale. @@ -44,8 +44,8 @@ class RatingWidget extends AbstractWidget implements StepCapableInterface { * @param array $captions * The caption of a point, keyed by the point; points may be uncaptioned. */ - public function __construct(int $point, protected int $min = 1, protected int $max = 5, protected array $captions = []) { - $this->point = $this->clamp($point); + public function __construct(int $default, protected int $min = 1, protected int $max = 5, protected array $captions = []) { + $this->point = $this->clamp($default); } /** diff --git a/src/Widget/SuggestWidget.php b/src/Widget/SuggestWidget.php index fff538d6..211666be 100644 --- a/src/Widget/SuggestWidget.php +++ b/src/Widget/SuggestWidget.php @@ -40,6 +40,11 @@ class SuggestWidget extends AbstractWidget implements SearchCapableInterface, Te */ protected int $cursor = -1; + /** + * The live input buffer. + */ + protected string $buffer = ''; + /** * The buffer the memoized ranking was computed for, or NULL for none yet. */ @@ -57,7 +62,7 @@ class SuggestWidget extends AbstractWidget implements SearchCapableInterface, Te * * @param list $values * The suggestion values. - * @param string $buffer + * @param string $default * The initial input. * @param int|null $page_size * The number of suggestions shown at once before the list pages; NULL uses @@ -69,7 +74,8 @@ class SuggestWidget extends AbstractWidget implements SearchCapableInterface, Te * Whether the leading prefix match is previewed as inline ghost-text after * the caret; FALSE leaves the ranked list as the only completion. */ - public function __construct(protected array $values, protected string $buffer = '', ?int $page_size = NULL, protected array $descriptions = [], protected bool $ghost = FALSE) { + public function __construct(protected array $values, string $default = '', ?int $page_size = NULL, protected array $descriptions = [], protected bool $ghost = FALSE) { + $this->buffer = $default; $this->pageSize = $this->resolvePageSize($page_size); } diff --git a/src/Widget/TemplateWidget.php b/src/Widget/TemplateWidget.php index a78dae4d..c0f29c76 100644 --- a/src/Widget/TemplateWidget.php +++ b/src/Widget/TemplateWidget.php @@ -54,13 +54,13 @@ class TemplateWidget extends AbstractWidget implements TextEditCapableInterface * * @param \DrevOps\Tui\Model\Template $template * The shape to fill in. - * @param string $value + * @param string $default * The initial assembled value; a value that does not have the shape leaves * every slot empty. */ - public function __construct(protected Template $template, string $value = '') { + public function __construct(protected Template $template, string $default = '') { $this->names = $this->template->placeholders(); - $extracted = $this->template->extract($value); + $extracted = $this->template->extract($default); foreach ($this->names as $name) { $this->parts[$name] = $extracted[$name] ?? ''; diff --git a/src/Widget/TextWidget.php b/src/Widget/TextWidget.php index 6ce7ea16..f9a033a4 100644 --- a/src/Widget/TextWidget.php +++ b/src/Widget/TextWidget.php @@ -30,14 +30,14 @@ class TextWidget extends AbstractWidget implements TextEditCapableInterface, Com /** * Construct a text widget. * - * @param string $buffer + * @param string $default * The initial value (and live input buffer). * @param list $completions * Inline ghost-text candidates: the buffer is completed to the first * candidate it is a prefix of. Empty leaves a plain text field. */ - public function __construct(string $buffer = '', protected array $completions = []) { - $this->initTextBuffer($buffer); + public function __construct(string $default = '', protected array $completions = []) { + $this->initTextBuffer($default); } /** diff --git a/src/Widget/TextareaWidget.php b/src/Widget/TextareaWidget.php index 324fbe4f..de9fae5c 100644 --- a/src/Widget/TextareaWidget.php +++ b/src/Widget/TextareaWidget.php @@ -35,13 +35,13 @@ class TextareaWidget extends AbstractWidget implements TextEditCapableInterface, /** * Construct a textarea widget. * - * @param string $buffer + * @param string $default * The initial value (and live input buffer). * @param bool $externalEdit * Whether the external-editor handoff is offered (an available $EDITOR). */ - public function __construct(string $buffer = '', protected bool $externalEdit = FALSE) { - $this->initTextBuffer($buffer); + public function __construct(string $default = '', protected bool $externalEdit = FALSE) { + $this->initTextBuffer($default); } /** From ececf75f958a03b01e77c82f4b6270f7fab47ea7 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Wed, 29 Jul 2026 15:51:36 +1000 Subject: [PATCH 07/14] Threw 'FormException' from 'Modal' instead of the SPL exception. Modal was the one Model class rejecting a declaration with an SPL exception; the other six throw the library's own 'FormException'. The constructor now documents the throw, as five of those siblings already do. --- src/Model/Modal.php | 5 ++++- tests/phpunit/Unit/Model/ModalTest.php | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Model/Modal.php b/src/Model/Modal.php index b0dcfed8..6d1e6f30 100644 --- a/src/Model/Modal.php +++ b/src/Model/Modal.php @@ -22,6 +22,9 @@ * * @param \DrevOps\Tui\Model\Buttons $buttons * The dialog's submit/cancel buttons. + * + * @throws \DrevOps\Tui\Model\FormException + * When the buttons are hidden. */ public function __construct( public Buttons $buttons = new Buttons(), @@ -29,7 +32,7 @@ public function __construct( // The buttons are a modal's only on-screen way out, so hiding them would // strand the dialog; the constructor rejects that rather than ignoring it. if (!$this->buttons->show) { - throw new \InvalidArgumentException('A modal dialog must show its buttons.'); + throw new FormException('A modal dialog must show its buttons.'); } } diff --git a/tests/phpunit/Unit/Model/ModalTest.php b/tests/phpunit/Unit/Model/ModalTest.php index c3ebe7c1..2a0f09de 100644 --- a/tests/phpunit/Unit/Model/ModalTest.php +++ b/tests/phpunit/Unit/Model/ModalTest.php @@ -5,6 +5,7 @@ namespace DrevOps\Tui\Tests\Unit\Model; use DrevOps\Tui\Model\Buttons; +use DrevOps\Tui\Model\FormException; use DrevOps\Tui\Model\Modal; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Group; @@ -33,7 +34,7 @@ public function testCustomButtons(): void { } public function testHiddenButtonsRejected(): void { - $this->expectException(\InvalidArgumentException::class); + $this->expectException(FormException::class); $this->expectExceptionMessage('A modal dialog must show its buttons.'); new Modal(new Buttons(FALSE)); From 0a1ba551a505cc8432969cbf3173815900cb4a82 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Wed, 29 Jul 2026 15:54:44 +1000 Subject: [PATCH 08/14] Folded case through 'Strings' when filtering and sorting picked files. The file picker matched and ordered user-visible entry names with byte-level 'strtolower' and 'strcasecmp', which leave anything outside ASCII untouched, so a lowercase accented query never matched its uppercase entry. Every other widget folds the same kind of text through the mbstring-aware helper. A regression test covers a non-ASCII name. --- src/Widget/FilePickerWidget.php | 6 +++--- tests/phpunit/Unit/Widget/FilePickerWidgetTest.php | 12 ++++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Widget/FilePickerWidget.php b/src/Widget/FilePickerWidget.php index 34b18727..e8188d81 100644 --- a/src/Widget/FilePickerWidget.php +++ b/src/Widget/FilePickerWidget.php @@ -569,11 +569,11 @@ protected function entries(): array { */ protected function sortFilter(array $names): array { if ($this->filter !== '') { - $needle = strtolower($this->filter); - $names = array_filter($names, static fn(string $name): bool => str_contains(strtolower($name), $needle)); + $needle = Strings::lower($this->filter); + $names = array_filter($names, static fn(string $name): bool => str_contains(Strings::lower($name), $needle)); } - usort($names, strcasecmp(...)); + usort($names, static fn(string $a, string $b): int => strcmp(Strings::lower($a), Strings::lower($b))); return $names; } diff --git a/tests/phpunit/Unit/Widget/FilePickerWidgetTest.php b/tests/phpunit/Unit/Widget/FilePickerWidgetTest.php index 5766cc92..9d7b801c 100644 --- a/tests/phpunit/Unit/Widget/FilePickerWidgetTest.php +++ b/tests/phpunit/Unit/Widget/FilePickerWidgetTest.php @@ -206,6 +206,18 @@ public function testTypeToFilterNarrowsEntries(): void { $this->assertSame($this->root . '/docs', $widget->value()); } + public function testTypeToFilterFoldsCaseBeyondAscii(): void { + vfsStream::setup('accents', NULL, ['Äpfel.md' => '', 'pears.md' => '']); + $widget = new FilePickerWidget(vfsStream::url('accents')); + + $widget->handle(Key::char('ä')); + + // A lowercase non-ASCII query matches its uppercase entry, which a + // byte-level fold would miss. + $this->assertSame(vfsStream::url('accents') . '/Äpfel.md', $widget->value()); + $this->assertStringNotContainsString('pears.md', $this->render($widget)); + } + public function testBackspaceAscendsWhenFilterEmpty(): void { $widget = new FilePickerWidget($this->root); From 2ac40d086bc5c7b6107b2f4ff27192f0da545830 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Wed, 29 Jul 2026 15:58:01 +1000 Subject: [PATCH 09/14] Kept the truncation ellipsis out of ASCII output. The grid preview and the summary line both emitted the Unicode ellipsis whatever the display mode, so a terminal without Unicode was handed a glyph it cannot draw. The grid preview now falls back to three dots, and the summary line clips to the full width instead, which is what a table cell already does when it cannot spend a column on the marker. A layout test asserted the glyph while building an ASCII theme, so it was encoding the bug. --- src/Theme/DefaultTheme.php | 13 ++++++++++-- tests/phpunit/Unit/Theme/ThemeLayoutTest.php | 21 +++++++++++++++++--- tests/phpunit/Unit/Theme/ThemeRenderTest.php | 10 ++++++++++ 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/Theme/DefaultTheme.php b/src/Theme/DefaultTheme.php index 9bb13860..cc5b3377 100644 --- a/src/Theme/DefaultTheme.php +++ b/src/Theme/DefaultTheme.php @@ -1583,7 +1583,8 @@ protected function renderColumnBlock(Panel $panel, Answers $answers, bool $selec // A grid cell is one physical row, so a multi-line value previews as // its first line - an embedded newline would desync the column zip. $value_lines = explode("\n", $this->normalizeLines($this->renderFieldValue($field, $answers->value($field->id)))); - $value = $value_lines[0] . (count($value_lines) > 1 ? '…' : ''); + $more = $this->unicode ? '…' : '...'; + $value = $value_lines[0] . (count($value_lines) > 1 ? $more : ''); $lines[] = $indent . ' ' . $this->description(Translator::t($field->label), $selected) . ' ' . $this->value($value, $selected); } @@ -1988,7 +1989,15 @@ public function summarizePanel(Panel $panel, Answers $answers): string { */ public function renderSummaryLine(string $summary, bool $selected): string { $max = max(1, $this->width - 4); - $clipped = Strings::length($summary) > $max ? Strings::substr($summary, 0, $max - 1) . '…' : $summary; + + if (Strings::length($summary) > $max) { + // Only the Unicode marker fits the budget in one column; ASCII clips to + // the full width instead, as a table cell does. + $clipped = $this->unicode ? Strings::substr($summary, 0, $max - 1) . '…' : Strings::substr($summary, 0, $max); + } + else { + $clipped = $summary; + } return ' ' . $this->value($clipped, $selected); } diff --git a/tests/phpunit/Unit/Theme/ThemeLayoutTest.php b/tests/phpunit/Unit/Theme/ThemeLayoutTest.php index ad783ecb..6860c161 100644 --- a/tests/phpunit/Unit/Theme/ThemeLayoutTest.php +++ b/tests/phpunit/Unit/Theme/ThemeLayoutTest.php @@ -148,15 +148,30 @@ public function testLayoutPreviewsMultiLineValuesAsTheirFirstLine(): void { [$lines] = $theme->renderBody($panel, new Answers(['notes' => "Crisp and sweet\nHint of citrus", 'two' => 'x'], []), 0); // A grid cell is one physical row: the multi-line value previews as its - // first line with an ellipsis, and no entry carries an embedded newline - // that would desync the column zip. + // first line with a there-is-more marker, and no entry carries an embedded + // newline that would desync the column zip. $body = implode('|', $lines); - $this->assertStringContainsString('Crisp and sweet…', $body); + $this->assertStringContainsString('Crisp and sweet...', $body); $this->assertStringNotContainsString('Hint of citrus', $body); $this->assertStringNotContainsString("\n", $body); $this->assertCount(2, $lines); } + public function testLayoutPreviewMarkerFollowsTheDisplayMode(): void { + $panel = new Panel('p', 'P', '', [], [ + new Panel('a', 'A', '', [new Field('notes', 'Notes', '', FieldType::Textarea, '')]), + new Panel('b', 'B', '', [new Field('two', 'Two', '', FieldType::Text, '')]), + ], NULL, [2]); + $answers = new Answers(['notes' => "Crisp and sweet\nHint of citrus", 'two' => 'x'], []); + + [$unicode_lines] = (new DefaultTheme(60, ['color' => FALSE, 'unicode' => TRUE]))->renderBody($panel, $answers, 0); + [$ascii_lines] = (new DefaultTheme(60, ['color' => FALSE, 'unicode' => FALSE]))->renderBody($panel, $answers, 0); + + // A terminal without Unicode must never be handed the glyph. + $this->assertStringContainsString('Crisp and sweet…', implode('|', $unicode_lines)); + $this->assertStringNotContainsString('…', implode('|', $ascii_lines)); + } + public function testMeasureUsesTheWidestValueLine(): void { $form = Form::create('T') ->buttons(FALSE) diff --git a/tests/phpunit/Unit/Theme/ThemeRenderTest.php b/tests/phpunit/Unit/Theme/ThemeRenderTest.php index e637374d..b2529672 100644 --- a/tests/phpunit/Unit/Theme/ThemeRenderTest.php +++ b/tests/phpunit/Unit/Theme/ThemeRenderTest.php @@ -530,6 +530,16 @@ public function testSummaryLineClipsToWidth(): void { $this->assertStringContainsString('…', $line); } + public function testSummaryLineClipsWithoutTheGlyphInAsciiMode(): void { + $theme = new DefaultTheme(40, ['color' => FALSE, 'unicode' => FALSE]); + + $line = Ansi::strip($theme->renderSummaryLine(str_repeat('x', 100), FALSE)); + + // ASCII spends no column on a marker, so the clip still fits the width. + $this->assertLessThanOrEqual(40, mb_strlen($line)); + $this->assertStringNotContainsString('…', $line); + } + public function testSelectedItemIsBold(): void { $theme = new DefaultTheme(40); $field = new Field('name', 'Name', '', FieldType::Text, ''); From e746d310a36221b66550b303c6ed7c49bbe28b9f Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Wed, 29 Jul 2026 15:59:56 +1000 Subject: [PATCH 10/14] Named every test's group after the directory it lives in. The group attribute followed two rival conventions: a directory name on 62 classes and a blanket 'tui' on the rest, which meant no group could select a subsystem. The directory name now decides it everywhere, so the theme, render, translation, primitive and remaining model suites are selectable on their own. Tests at the root of Unit keep 'tui', matching the facade they cover at the root of src. No configuration filtered on these names. --- tests/phpunit/Unit/Builder/FormTest.php | 2 +- tests/phpunit/Unit/Input/HintTest.php | 2 +- tests/phpunit/Unit/Model/ButtonsTest.php | 2 +- tests/phpunit/Unit/Model/ModalTest.php | 2 +- tests/phpunit/Unit/Model/PanelTest.php | 2 +- tests/phpunit/Unit/Model/TableSpecTest.php | 2 +- tests/phpunit/Unit/Primitive/OutputTest.php | 2 +- tests/phpunit/Unit/Primitive/ProgressTest.php | 2 +- tests/phpunit/Unit/Render/AnsiTest.php | 2 +- tests/phpunit/Unit/Render/BoxTest.php | 2 +- tests/phpunit/Unit/Render/ExternalEditorTest.php | 2 +- tests/phpunit/Unit/Render/NavigatorTest.php | 2 +- tests/phpunit/Unit/Render/OverlayTest.php | 2 +- tests/phpunit/Unit/Render/PanelControllerTest.php | 2 +- tests/phpunit/Unit/Render/ScrollerTest.php | 2 +- tests/phpunit/Unit/Render/TableTest.php | 2 +- tests/phpunit/Unit/Render/TerminalControlTest.php | 2 +- tests/phpunit/Unit/Render/TerminalTest.php | 2 +- tests/phpunit/Unit/Testing/KeyEncoderTest.php | 2 +- tests/phpunit/Unit/Testing/KeyStreamTest.php | 2 +- tests/phpunit/Unit/Theme/BuiltinThemesTest.php | 2 +- tests/phpunit/Unit/Theme/OutputRenderTest.php | 2 +- tests/phpunit/Unit/Theme/ProgressRenderTest.php | 2 +- tests/phpunit/Unit/Theme/ScaleRenderTest.php | 2 +- tests/phpunit/Unit/Theme/ThemeConditionalIndentTest.php | 2 +- tests/phpunit/Unit/Theme/ThemeFullscreenTest.php | 2 +- tests/phpunit/Unit/Theme/ThemeLayoutTest.php | 2 +- tests/phpunit/Unit/Theme/ThemeManagerTest.php | 2 +- tests/phpunit/Unit/Theme/ThemeOptionsTest.php | 2 +- tests/phpunit/Unit/Theme/ThemeRenderTest.php | 2 +- tests/phpunit/Unit/Theme/ThemeTest.php | 2 +- tests/phpunit/Unit/Translation/ChromeCatalogTest.php | 2 +- tests/phpunit/Unit/Translation/TranslationRenderTest.php | 2 +- tests/phpunit/Unit/Translation/TranslatorTest.php | 2 +- 34 files changed, 34 insertions(+), 34 deletions(-) diff --git a/tests/phpunit/Unit/Builder/FormTest.php b/tests/phpunit/Unit/Builder/FormTest.php index 0b2a0f5a..eb424155 100644 --- a/tests/phpunit/Unit/Builder/FormTest.php +++ b/tests/phpunit/Unit/Builder/FormTest.php @@ -37,7 +37,7 @@ #[CoversClass(LayoutGuard::class)] #[CoversClass(PanelBuilder::class)] #[CoversClass(FieldBuilder::class)] -#[Group('model')] +#[Group('builder')] final class FormTest extends TestCase { public function testBuildsExpectedForm(): void { diff --git a/tests/phpunit/Unit/Input/HintTest.php b/tests/phpunit/Unit/Input/HintTest.php index e030dc21..73c87432 100644 --- a/tests/phpunit/Unit/Input/HintTest.php +++ b/tests/phpunit/Unit/Input/HintTest.php @@ -16,7 +16,7 @@ * Tests the key-hint fragment. */ #[CoversClass(Hint::class)] -#[Group('tui')] +#[Group('input')] final class HintTest extends TestCase { use ResetsTranslatorTrait; diff --git a/tests/phpunit/Unit/Model/ButtonsTest.php b/tests/phpunit/Unit/Model/ButtonsTest.php index 001562e9..ca914fb8 100644 --- a/tests/phpunit/Unit/Model/ButtonsTest.php +++ b/tests/phpunit/Unit/Model/ButtonsTest.php @@ -13,7 +13,7 @@ * Tests the submit/cancel button pair value object. */ #[CoversClass(Buttons::class)] -#[Group('tui')] +#[Group('model')] final class ButtonsTest extends TestCase { public function testDefaults(): void { diff --git a/tests/phpunit/Unit/Model/ModalTest.php b/tests/phpunit/Unit/Model/ModalTest.php index 2a0f09de..6b7a02df 100644 --- a/tests/phpunit/Unit/Model/ModalTest.php +++ b/tests/phpunit/Unit/Model/ModalTest.php @@ -15,7 +15,7 @@ * Tests the modal presentation config value object. */ #[CoversClass(Modal::class)] -#[Group('tui')] +#[Group('model')] final class ModalTest extends TestCase { public function testDefaultButtons(): void { diff --git a/tests/phpunit/Unit/Model/PanelTest.php b/tests/phpunit/Unit/Model/PanelTest.php index 60c8a893..eaeb1fb7 100644 --- a/tests/phpunit/Unit/Model/PanelTest.php +++ b/tests/phpunit/Unit/Model/PanelTest.php @@ -16,7 +16,7 @@ * Tests the panel model. */ #[CoversClass(Panel::class)] -#[Group('tui')] +#[Group('model')] final class PanelTest extends TestCase { public function testItemCountSumsFieldsAndPanels(): void { diff --git a/tests/phpunit/Unit/Model/TableSpecTest.php b/tests/phpunit/Unit/Model/TableSpecTest.php index a0333e62..98b1a70e 100644 --- a/tests/phpunit/Unit/Model/TableSpecTest.php +++ b/tests/phpunit/Unit/Model/TableSpecTest.php @@ -13,7 +13,7 @@ * Tests the presentational table value object. */ #[CoversClass(TableSpec::class)] -#[Group('tui')] +#[Group('model')] final class TableSpecTest extends TestCase { public function testKeepsStringCells(): void { diff --git a/tests/phpunit/Unit/Primitive/OutputTest.php b/tests/phpunit/Unit/Primitive/OutputTest.php index b1f828d3..643e1c4f 100644 --- a/tests/phpunit/Unit/Primitive/OutputTest.php +++ b/tests/phpunit/Unit/Primitive/OutputTest.php @@ -19,7 +19,7 @@ * Tests the standalone output primitive. */ #[CoversClass(Output::class)] -#[Group('tui')] +#[Group('primitive')] final class OutputTest extends TestCase { public function testBoxWritesTheFramedLines(): void { diff --git a/tests/phpunit/Unit/Primitive/ProgressTest.php b/tests/phpunit/Unit/Primitive/ProgressTest.php index b093bdf3..740bb0b8 100644 --- a/tests/phpunit/Unit/Primitive/ProgressTest.php +++ b/tests/phpunit/Unit/Primitive/ProgressTest.php @@ -17,7 +17,7 @@ * Tests the progress primitive (spinner and determinate bar). */ #[CoversClass(Progress::class)] -#[Group('tui')] +#[Group('primitive')] final class ProgressTest extends TestCase { public function testReturnsTheCallbackResult(): void { diff --git a/tests/phpunit/Unit/Render/AnsiTest.php b/tests/phpunit/Unit/Render/AnsiTest.php index 63d37862..63b941e9 100644 --- a/tests/phpunit/Unit/Render/AnsiTest.php +++ b/tests/phpunit/Unit/Render/AnsiTest.php @@ -13,7 +13,7 @@ * Tests the ANSI helpers. */ #[CoversClass(Ansi::class)] -#[Group('tui')] +#[Group('render')] final class AnsiTest extends TestCase { public function testStyle(): void { diff --git a/tests/phpunit/Unit/Render/BoxTest.php b/tests/phpunit/Unit/Render/BoxTest.php index 02ccfda0..58729613 100644 --- a/tests/phpunit/Unit/Render/BoxTest.php +++ b/tests/phpunit/Unit/Render/BoxTest.php @@ -16,7 +16,7 @@ * Tests the pure box-drawing geometry helper. */ #[CoversClass(Box::class)] -#[Group('tui')] +#[Group('render')] final class BoxTest extends TestCase { #[DataProvider('dataProviderChars')] diff --git a/tests/phpunit/Unit/Render/ExternalEditorTest.php b/tests/phpunit/Unit/Render/ExternalEditorTest.php index 7409c06a..33e56024 100644 --- a/tests/phpunit/Unit/Render/ExternalEditorTest.php +++ b/tests/phpunit/Unit/Render/ExternalEditorTest.php @@ -17,7 +17,7 @@ * Tests the external editor service. */ #[CoversClass(ExternalEditor::class)] -#[Group('tui')] +#[Group('render')] final class ExternalEditorTest extends TestCase { use IsolatesEnvTrait; diff --git a/tests/phpunit/Unit/Render/NavigatorTest.php b/tests/phpunit/Unit/Render/NavigatorTest.php index ea8aee40..44421223 100644 --- a/tests/phpunit/Unit/Render/NavigatorTest.php +++ b/tests/phpunit/Unit/Render/NavigatorTest.php @@ -14,7 +14,7 @@ * Tests the panel navigator. */ #[CoversClass(Navigator::class)] -#[Group('tui')] +#[Group('render')] final class NavigatorTest extends TestCase { public function testNavigation(): void { diff --git a/tests/phpunit/Unit/Render/OverlayTest.php b/tests/phpunit/Unit/Render/OverlayTest.php index ad3ab5c9..2a89b693 100644 --- a/tests/phpunit/Unit/Render/OverlayTest.php +++ b/tests/phpunit/Unit/Render/OverlayTest.php @@ -16,7 +16,7 @@ * Tests the line-compositor that overlays a box on a backdrop. */ #[CoversClass(Overlay::class)] -#[Group('tui')] +#[Group('render')] final class OverlayTest extends TestCase { public function testCenterPositionsWithEqualPadding(): void { diff --git a/tests/phpunit/Unit/Render/PanelControllerTest.php b/tests/phpunit/Unit/Render/PanelControllerTest.php index 55ae25a7..30b53acd 100644 --- a/tests/phpunit/Unit/Render/PanelControllerTest.php +++ b/tests/phpunit/Unit/Render/PanelControllerTest.php @@ -35,7 +35,7 @@ * Tests the interactive panel controller. */ #[CoversClass(PanelController::class)] -#[Group('tui')] +#[Group('render')] final class PanelControllerTest extends TestCase { public function testDrillIntoPanelAndBack(): void { diff --git a/tests/phpunit/Unit/Render/ScrollerTest.php b/tests/phpunit/Unit/Render/ScrollerTest.php index 3fe9cf81..a0cb46dd 100644 --- a/tests/phpunit/Unit/Render/ScrollerTest.php +++ b/tests/phpunit/Unit/Render/ScrollerTest.php @@ -15,7 +15,7 @@ */ #[CoversClass(Scroller::class)] #[CoversClass(Viewport::class)] -#[Group('tui')] +#[Group('render')] final class ScrollerTest extends TestCase { public function testCursorAtTop(): void { diff --git a/tests/phpunit/Unit/Render/TableTest.php b/tests/phpunit/Unit/Render/TableTest.php index d07c24d6..5369d7d5 100644 --- a/tests/phpunit/Unit/Render/TableTest.php +++ b/tests/phpunit/Unit/Render/TableTest.php @@ -14,7 +14,7 @@ * Tests the pure table geometry helper. */ #[CoversClass(Table::class)] -#[Group('tui')] +#[Group('render')] final class TableTest extends TestCase { public function testRendersAlignedGrid(): void { diff --git a/tests/phpunit/Unit/Render/TerminalControlTest.php b/tests/phpunit/Unit/Render/TerminalControlTest.php index 256d12a1..98546a04 100644 --- a/tests/phpunit/Unit/Render/TerminalControlTest.php +++ b/tests/phpunit/Unit/Render/TerminalControlTest.php @@ -13,7 +13,7 @@ * Tests the terminal control sequences. */ #[CoversClass(TerminalControl::class)] -#[Group('tui')] +#[Group('render')] final class TerminalControlTest extends TestCase { public function testSequences(): void { diff --git a/tests/phpunit/Unit/Render/TerminalTest.php b/tests/phpunit/Unit/Render/TerminalTest.php index 4b0317c4..5d36ae5d 100644 --- a/tests/phpunit/Unit/Render/TerminalTest.php +++ b/tests/phpunit/Unit/Render/TerminalTest.php @@ -18,7 +18,7 @@ * Tests the terminal output and capability detection. */ #[CoversClass(Terminal::class)] -#[Group('tui')] +#[Group('render')] final class TerminalTest extends TestCase { use IsolatesEnvTrait; diff --git a/tests/phpunit/Unit/Testing/KeyEncoderTest.php b/tests/phpunit/Unit/Testing/KeyEncoderTest.php index b76c413d..82cc7495 100644 --- a/tests/phpunit/Unit/Testing/KeyEncoderTest.php +++ b/tests/phpunit/Unit/Testing/KeyEncoderTest.php @@ -17,7 +17,7 @@ * Tests encoding a key back into the bytes a parser decodes into it. */ #[CoversClass(KeyEncoder::class)] -#[Group('input')] +#[Group('testing')] final class KeyEncoderTest extends TestCase { #[DataProvider('dataProviderRoundTrip')] diff --git a/tests/phpunit/Unit/Testing/KeyStreamTest.php b/tests/phpunit/Unit/Testing/KeyStreamTest.php index e6e8381f..e8352e63 100644 --- a/tests/phpunit/Unit/Testing/KeyStreamTest.php +++ b/tests/phpunit/Unit/Testing/KeyStreamTest.php @@ -16,7 +16,7 @@ */ #[CoversClass(Key::class)] #[CoversClass(ArrayKeyStream::class)] -#[Group('input')] +#[Group('testing')] final class KeyStreamTest extends TestCase { public function testCharAndNamed(): void { diff --git a/tests/phpunit/Unit/Theme/BuiltinThemesTest.php b/tests/phpunit/Unit/Theme/BuiltinThemesTest.php index ce535e69..4846ea7a 100644 --- a/tests/phpunit/Unit/Theme/BuiltinThemesTest.php +++ b/tests/phpunit/Unit/Theme/BuiltinThemesTest.php @@ -28,7 +28,7 @@ #[CoversClass(MonoTheme::class)] #[CoversClass(DosTheme::class)] #[CoversClass(Sgr::class)] -#[Group('tui')] +#[Group('theme')] final class BuiltinThemesTest extends TestCase { /** diff --git a/tests/phpunit/Unit/Theme/OutputRenderTest.php b/tests/phpunit/Unit/Theme/OutputRenderTest.php index bae3106d..9e48b18a 100644 --- a/tests/phpunit/Unit/Theme/OutputRenderTest.php +++ b/tests/phpunit/Unit/Theme/OutputRenderTest.php @@ -21,7 +21,7 @@ */ #[CoversClass(DefaultTheme::class)] #[CoversClass(EmberTheme::class)] -#[Group('tui')] +#[Group('theme')] final class OutputRenderTest extends TestCase { public function testBoxDrawsTitledFrameSizedToItsContent(): void { diff --git a/tests/phpunit/Unit/Theme/ProgressRenderTest.php b/tests/phpunit/Unit/Theme/ProgressRenderTest.php index 6a398e97..12303219 100644 --- a/tests/phpunit/Unit/Theme/ProgressRenderTest.php +++ b/tests/phpunit/Unit/Theme/ProgressRenderTest.php @@ -17,7 +17,7 @@ */ #[CoversClass(DefaultTheme::class)] #[CoversClass(EmberTheme::class)] -#[Group('tui')] +#[Group('theme')] final class ProgressRenderTest extends TestCase { public function testSpinnerCyclesFramesInTheAccent(): void { diff --git a/tests/phpunit/Unit/Theme/ScaleRenderTest.php b/tests/phpunit/Unit/Theme/ScaleRenderTest.php index b9089717..78296e25 100644 --- a/tests/phpunit/Unit/Theme/ScaleRenderTest.php +++ b/tests/phpunit/Unit/Theme/ScaleRenderTest.php @@ -20,7 +20,7 @@ * Tests the theme's rating-scale rendering. */ #[CoversClass(DefaultTheme::class)] -#[Group('tui')] +#[Group('theme')] final class ScaleRenderTest extends TestCase { public function testScaleFillsUpToTheChosenPoint(): void { diff --git a/tests/phpunit/Unit/Theme/ThemeConditionalIndentTest.php b/tests/phpunit/Unit/Theme/ThemeConditionalIndentTest.php index 424be2a1..d1bc18c5 100644 --- a/tests/phpunit/Unit/Theme/ThemeConditionalIndentTest.php +++ b/tests/phpunit/Unit/Theme/ThemeConditionalIndentTest.php @@ -25,7 +25,7 @@ * Tests the opt-in indentation of conditional fields. */ #[CoversClass(DefaultTheme::class)] -#[Group('tui')] +#[Group('theme')] final class ThemeConditionalIndentTest extends TestCase { /** diff --git a/tests/phpunit/Unit/Theme/ThemeFullscreenTest.php b/tests/phpunit/Unit/Theme/ThemeFullscreenTest.php index 3589ee85..27b27309 100644 --- a/tests/phpunit/Unit/Theme/ThemeFullscreenTest.php +++ b/tests/phpunit/Unit/Theme/ThemeFullscreenTest.php @@ -22,7 +22,7 @@ * Tests the fullscreen frame stretch, block alignment and content measuring. */ #[CoversClass(DefaultTheme::class)] -#[Group('tui')] +#[Group('theme')] final class ThemeFullscreenTest extends TestCase { #[DataProvider('dataProviderChromeHeight')] diff --git a/tests/phpunit/Unit/Theme/ThemeLayoutTest.php b/tests/phpunit/Unit/Theme/ThemeLayoutTest.php index 6860c161..8c97e535 100644 --- a/tests/phpunit/Unit/Theme/ThemeLayoutTest.php +++ b/tests/phpunit/Unit/Theme/ThemeLayoutTest.php @@ -23,7 +23,7 @@ * Tests the panel-grid layout rendering and measurement. */ #[CoversClass(DefaultTheme::class)] -#[Group('tui')] +#[Group('theme')] final class ThemeLayoutTest extends TestCase { public function testLayoutZipsPanelsSideBySide(): void { diff --git a/tests/phpunit/Unit/Theme/ThemeManagerTest.php b/tests/phpunit/Unit/Theme/ThemeManagerTest.php index c469a3f1..c35774ea 100644 --- a/tests/phpunit/Unit/Theme/ThemeManagerTest.php +++ b/tests/phpunit/Unit/Theme/ThemeManagerTest.php @@ -18,7 +18,7 @@ * Tests the theme registry and factory. */ #[CoversClass(ThemeManager::class)] -#[Group('tui')] +#[Group('theme')] final class ThemeManagerTest extends TestCase { use ResetsRegistriesTrait; diff --git a/tests/phpunit/Unit/Theme/ThemeOptionsTest.php b/tests/phpunit/Unit/Theme/ThemeOptionsTest.php index a0d8ff45..662ee3dd 100644 --- a/tests/phpunit/Unit/Theme/ThemeOptionsTest.php +++ b/tests/phpunit/Unit/Theme/ThemeOptionsTest.php @@ -30,7 +30,7 @@ * Tests the theme spacing, border and custom display options. */ #[CoversClass(DefaultTheme::class)] -#[Group('tui')] +#[Group('theme')] final class ThemeOptionsTest extends TestCase { public function testCompactSpacingDropsDescriptionsAndSummary(): void { diff --git a/tests/phpunit/Unit/Theme/ThemeRenderTest.php b/tests/phpunit/Unit/Theme/ThemeRenderTest.php index b2529672..b32ffdf2 100644 --- a/tests/phpunit/Unit/Theme/ThemeRenderTest.php +++ b/tests/phpunit/Unit/Theme/ThemeRenderTest.php @@ -34,7 +34,7 @@ */ #[CoversClass(DefaultTheme::class)] #[CoversClass(HelpSection::class)] -#[Group('tui')] +#[Group('theme')] final class ThemeRenderTest extends TestCase { public function testFieldLineSelectedRightAlignsBadge(): void { diff --git a/tests/phpunit/Unit/Theme/ThemeTest.php b/tests/phpunit/Unit/Theme/ThemeTest.php index 71cb05c4..f1b7f9b5 100644 --- a/tests/phpunit/Unit/Theme/ThemeTest.php +++ b/tests/phpunit/Unit/Theme/ThemeTest.php @@ -17,7 +17,7 @@ * Tests the theme's semantic styler and symbol methods. */ #[CoversClass(DefaultTheme::class)] -#[Group('tui')] +#[Group('theme')] final class ThemeTest extends TestCase { #[DataProvider('dataProviderStyler')] diff --git a/tests/phpunit/Unit/Translation/ChromeCatalogTest.php b/tests/phpunit/Unit/Translation/ChromeCatalogTest.php index 5bfe01f8..b784b59c 100644 --- a/tests/phpunit/Unit/Translation/ChromeCatalogTest.php +++ b/tests/phpunit/Unit/Translation/ChromeCatalogTest.php @@ -18,7 +18,7 @@ * chrome stays complete and honest. */ #[CoversNothing] -#[Group('tui')] +#[Group('translation')] final class ChromeCatalogTest extends TestCase { public function testTemplateMatchesSourceLiterals(): void { diff --git a/tests/phpunit/Unit/Translation/TranslationRenderTest.php b/tests/phpunit/Unit/Translation/TranslationRenderTest.php index a0d68091..632afbab 100644 --- a/tests/phpunit/Unit/Translation/TranslationRenderTest.php +++ b/tests/phpunit/Unit/Translation/TranslationRenderTest.php @@ -34,7 +34,7 @@ #[CoversClass(SummaryFormatter::class)] #[CoversClass(SchemaValidator::class)] #[CoversClass(AgentHelp::class)] -#[Group('tui')] +#[Group('translation')] final class TranslationRenderTest extends TestCase { use ResetsTranslatorTrait; diff --git a/tests/phpunit/Unit/Translation/TranslatorTest.php b/tests/phpunit/Unit/Translation/TranslatorTest.php index 988b2e60..597a1fd1 100644 --- a/tests/phpunit/Unit/Translation/TranslatorTest.php +++ b/tests/phpunit/Unit/Translation/TranslatorTest.php @@ -16,7 +16,7 @@ * Tests the translator and its static t() bridge. */ #[CoversClass(Translator::class)] -#[Group('tui')] +#[Group('translation')] final class TranslatorTest extends TestCase { use IsolatesEnvTrait; From d97d724bd29c5ab67c052a6e9d85a056d39a7f11 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Wed, 29 Jul 2026 16:07:16 +1000 Subject: [PATCH 11/14] Shared the test themes through a 'BuildsThemesTrait'. One helper name meant three different themes across the suite, and its body was copied into eight classes. Two of those copies had drifted apart: the output and progress primitive tests defaulted colour opposite ways, so the same call produced different themes. The trait now owns both shared themes, the output test states the colour it relies on rather than leaning on a default, and the conditional-indent helper is renamed for the one thing it varies. --- tests/phpunit/Traits/BuildsThemesTrait.php | 46 ++++++++++ tests/phpunit/Unit/Primitive/OutputTest.php | 51 ++++------ tests/phpunit/Unit/Primitive/ProgressTest.php | 21 +---- .../Unit/Render/PanelControllerTest.php | 13 +-- tests/phpunit/Unit/Theme/OutputRenderTest.php | 18 +--- .../phpunit/Unit/Theme/ProgressRenderTest.php | 18 +--- tests/phpunit/Unit/Theme/ScaleRenderTest.php | 18 +--- .../Unit/Theme/ThemeConditionalIndentTest.php | 26 +++--- tests/phpunit/Unit/Theme/ThemeRenderTest.php | 92 +++++++++---------- 9 files changed, 133 insertions(+), 170 deletions(-) create mode 100644 tests/phpunit/Traits/BuildsThemesTrait.php diff --git a/tests/phpunit/Traits/BuildsThemesTrait.php b/tests/phpunit/Traits/BuildsThemesTrait.php new file mode 100644 index 00000000..867ae833 --- /dev/null +++ b/tests/phpunit/Traits/BuildsThemesTrait.php @@ -0,0 +1,46 @@ + $color, 'unicode' => $unicode, 'mode' => Mode::Dark]); + } + + /** + * A narrow, unstyled, borderless theme for frame and layout assertions. + * + * Independent of the theme's bordered-and-padded defaults, so an assertion + * reads against the content alone. + * + * @return \DrevOps\Tui\Theme\DefaultTheme + * The theme. + */ + protected function plainTheme(): DefaultTheme { + return new DefaultTheme(40, ['color' => FALSE, 'border' => Border::None, 'spacing' => Spacing::Normal]); + } + +} diff --git a/tests/phpunit/Unit/Primitive/OutputTest.php b/tests/phpunit/Unit/Primitive/OutputTest.php index 643e1c4f..c9166248 100644 --- a/tests/phpunit/Unit/Primitive/OutputTest.php +++ b/tests/phpunit/Unit/Primitive/OutputTest.php @@ -7,9 +7,7 @@ use DrevOps\Tui\Primitive\Output; use DrevOps\Tui\Primitive\Status; use DrevOps\Tui\Testing\BufferedTerminal; -use DrevOps\Tui\Theme\DefaultTheme; -use DrevOps\Tui\Theme\Mode; -use DrevOps\Tui\Theme\ThemeManager; +use DrevOps\Tui\Tests\Traits\BuildsThemesTrait; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; @@ -22,10 +20,12 @@ #[Group('primitive')] final class OutputTest extends TestCase { + use BuildsThemesTrait; + public function testBoxWritesTheFramedLines(): void { $terminal = new BufferedTerminal(); - (new Output($terminal, $this->theme()))->box('Pick your fruit.', 'Welcome'); + (new Output($terminal, $this->theme(color: FALSE)))->box('Pick your fruit.', 'Welcome'); $output = $terminal->output(); @@ -38,7 +38,7 @@ public function testBoxWritesTheFramedLines(): void { public function testBoxAcceptsLineList(): void { $terminal = new BufferedTerminal(); - (new Output($terminal, $this->theme()))->box(['Apples', 'Pears']); + (new Output($terminal, $this->theme(color: FALSE)))->box(['Apples', 'Pears']); $lines = explode("\n", trim($terminal->output(), "\n")); @@ -50,7 +50,7 @@ public function testBoxAcceptsLineList(): void { public function testAnEmptyBoxWritesNothing(): void { $terminal = new BufferedTerminal(); - (new Output($terminal, $this->theme()))->box(''); + (new Output($terminal, $this->theme(color: FALSE)))->box(''); $this->assertSame('', $terminal->output()); } @@ -59,7 +59,7 @@ public function testAnEmptyBoxWritesNothing(): void { public function testEachStatusMethodWritesItsOwnLine(string $method, string $glyph): void { $terminal = new BufferedTerminal(); - (new Output($terminal, $this->theme()))->{$method}('Preserves are ready'); + (new Output($terminal, $this->theme(color: FALSE)))->{$method}('Preserves are ready'); $this->assertSame($glyph . ' Preserves are ready' . "\n", $terminal->output()); } @@ -81,7 +81,7 @@ public static function dataProviderEachStatusMethodWritesItsOwnLine(): \Iterator public function testCardWritesItsGridInsideTheBox(): void { $terminal = new BufferedTerminal(); - (new Output($terminal, $this->theme()))->card('Summary', 'Your order is packed.', ['Item', 'Count'], [['Apricot', '12']]); + (new Output($terminal, $this->theme(color: FALSE)))->card('Summary', 'Your order is packed.', ['Item', 'Count'], [['Apricot', '12']]); $output = $terminal->output(); @@ -94,7 +94,7 @@ public function testCardWritesItsGridInsideTheBox(): void { public function testUnborderedCardWritesIndentedLines(): void { $terminal = new BufferedTerminal(); - (new Output($terminal, $this->theme()))->card('Summary', 'Packed.', bordered: FALSE); + (new Output($terminal, $this->theme(color: FALSE)))->card('Summary', 'Packed.', bordered: FALSE); $this->assertSame(" Summary\n Packed.\n", $terminal->output()); } @@ -102,7 +102,7 @@ public function testUnborderedCardWritesIndentedLines(): void { public function testTableWritesTheGridAlone(): void { $terminal = new BufferedTerminal(); - (new Output($terminal, $this->theme()))->table(['Item'], [['Apricot']]); + (new Output($terminal, $this->theme(color: FALSE)))->table(['Item'], [['Apricot']]); $lines = explode("\n", trim($terminal->output(), "\n")); @@ -114,7 +114,7 @@ public function testTableWritesTheGridAlone(): void { public function testAnEmptyTableWritesNothing(): void { $terminal = new BufferedTerminal(); - (new Output($terminal, $this->theme()))->table([], []); + (new Output($terminal, $this->theme(color: FALSE)))->table([], []); $this->assertSame('', $terminal->output()); } @@ -122,14 +122,14 @@ public function testAnEmptyTableWritesNothing(): void { public function testTextWritesWrappedParagraph(): void { $terminal = new BufferedTerminal(); - (new Output($terminal, $this->theme()))->text('Everything is picked the morning it ships.'); + (new Output($terminal, $this->theme(color: FALSE)))->text('Everything is picked the morning it ships.'); $this->assertSame("Everything is picked the morning it ships.\n", $terminal->output()); } public function testRuleWritesSpanningLine(): void { $terminal = new BufferedTerminal(); - $theme = $this->theme(); + $theme = $this->theme(color: FALSE); (new Output($terminal, $theme))->rule(); @@ -139,7 +139,7 @@ public function testRuleWritesSpanningLine(): void { public function testBannerWritesTheLogoAndVersion(): void { $terminal = new BufferedTerminal(); - (new Output($terminal, $this->theme()))->banner('Produce Box', '1.2.3'); + (new Output($terminal, $this->theme(color: FALSE)))->banner('Produce Box', '1.2.3'); $output = $terminal->output(); @@ -151,7 +151,7 @@ public function testBannerWritesTheLogoAndVersion(): void { public function testStatusTakesTheKindDirectly(): void { $terminal = new BufferedTerminal(); - (new Output($terminal, $this->theme()))->status(Status::Warning, 'Short on jars'); + (new Output($terminal, $this->theme(color: FALSE)))->status(Status::Warning, 'Short on jars'); $this->assertSame("! Short on jars\n", $terminal->output()); } @@ -159,7 +159,7 @@ public function testStatusTakesTheKindDirectly(): void { public function testDefinitionsWriteAnAlignedList(): void { $terminal = new BufferedTerminal(); - (new Output($terminal, $this->theme()))->definitions(['Jars' => '12', 'Fruit' => 'Apricot']); + (new Output($terminal, $this->theme(color: FALSE)))->definitions(['Jars' => '12', 'Fruit' => 'Apricot']); $this->assertSame(" Jars 12\n Fruit Apricot\n", $terminal->output()); } @@ -167,14 +167,14 @@ public function testDefinitionsWriteAnAlignedList(): void { public function testEmptyDefinitionsWriteNothing(): void { $terminal = new BufferedTerminal(); - (new Output($terminal, $this->theme()))->definitions([]); + (new Output($terminal, $this->theme(color: FALSE)))->definitions([]); $this->assertSame('', $terminal->output()); } public function testCallsChainInOrder(): void { $terminal = new BufferedTerminal(); - $output = new Output($terminal, $this->theme()); + $output = new Output($terminal, $this->theme(color: FALSE)); $this->assertSame($output, $output->success('Packed')->note('Sealed')); $this->assertSame("✓ Packed\n• Sealed\n", $terminal->output()); @@ -207,19 +207,4 @@ public function testColourWrapsEveryPiece(): void { $this->assertStringContainsString("\033[32m", $output); } - /** - * A default theme in the given display modes, fixed to dark. - * - * @param bool $color - * Whether colour is on. - * @param bool $unicode - * Whether Unicode glyphs are on. - * - * @return \DrevOps\Tui\Theme\DefaultTheme - * The theme. - */ - protected function theme(bool $color = FALSE, bool $unicode = TRUE): DefaultTheme { - return ThemeManager::create('default', DefaultTheme::DEFAULT_WIDTH, ['color' => $color, 'unicode' => $unicode, 'mode' => Mode::Dark]); - } - } diff --git a/tests/phpunit/Unit/Primitive/ProgressTest.php b/tests/phpunit/Unit/Primitive/ProgressTest.php index 740bb0b8..6437b640 100644 --- a/tests/phpunit/Unit/Primitive/ProgressTest.php +++ b/tests/phpunit/Unit/Primitive/ProgressTest.php @@ -6,9 +6,7 @@ use DrevOps\Tui\Primitive\Progress; use DrevOps\Tui\Testing\BufferedTerminal; -use DrevOps\Tui\Theme\DefaultTheme; -use DrevOps\Tui\Theme\Mode; -use DrevOps\Tui\Theme\ThemeManager; +use DrevOps\Tui\Tests\Traits\BuildsThemesTrait; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; @@ -20,6 +18,8 @@ #[Group('primitive')] final class ProgressTest extends TestCase { + use BuildsThemesTrait; + public function testReturnsTheCallbackResult(): void { $progress = new Progress(new BufferedTerminal(), $this->theme(), TRUE, NULL, 'Scanning'); @@ -150,19 +150,4 @@ public function testCleansUpWhenTheWorkThrows(): void { $this->assertStringContainsString("\033[?25h", $terminal->output()); } - /** - * A default theme in the given display modes, fixed to dark. - * - * @param bool $color - * Whether colour is on. - * @param bool $unicode - * Whether Unicode glyphs are on. - * - * @return \DrevOps\Tui\Theme\DefaultTheme - * The theme. - */ - protected function theme(bool $color = TRUE, bool $unicode = TRUE): DefaultTheme { - return ThemeManager::create('default', DefaultTheme::DEFAULT_WIDTH, ['color' => $color, 'unicode' => $unicode, 'mode' => Mode::Dark]); - } - } diff --git a/tests/phpunit/Unit/Render/PanelControllerTest.php b/tests/phpunit/Unit/Render/PanelControllerTest.php index 30b53acd..46a9b405 100644 --- a/tests/phpunit/Unit/Render/PanelControllerTest.php +++ b/tests/phpunit/Unit/Render/PanelControllerTest.php @@ -21,6 +21,7 @@ use DrevOps\Tui\Render\TerminalControl; use DrevOps\Tui\Testing\BufferedTerminal; use DrevOps\Tui\Testing\KeyEncoder; +use DrevOps\Tui\Tests\Traits\BuildsThemesTrait; use DrevOps\Tui\Theme\Border; use DrevOps\Tui\Theme\DefaultTheme; use DrevOps\Tui\Theme\DosTheme; @@ -38,6 +39,8 @@ #[Group('render')] final class PanelControllerTest extends TestCase { + use BuildsThemesTrait; + public function testDrillIntoPanelAndBack(): void { $controller = $this->controller(); @@ -1425,16 +1428,6 @@ protected function requiredController(array $values): PanelController { return new PanelController($form, $this->plainTheme(), values: $values); } - /** - * An unstyled, borderless theme for frame assertions. - * - * @return \DrevOps\Tui\Theme\DefaultTheme - * The theme. - */ - protected function plainTheme(): DefaultTheme { - return new DefaultTheme(40, ['color' => FALSE, 'border' => Border::None, 'spacing' => Spacing::Normal]); - } - public function testEditEnforcesHandlerBehaviour(): void { $form = Form::create('Demo') ->panel('stall', 'Stall', function (PanelBuilder $p): void { diff --git a/tests/phpunit/Unit/Theme/OutputRenderTest.php b/tests/phpunit/Unit/Theme/OutputRenderTest.php index 9e48b18a..a4df21ba 100644 --- a/tests/phpunit/Unit/Theme/OutputRenderTest.php +++ b/tests/phpunit/Unit/Theme/OutputRenderTest.php @@ -6,6 +6,7 @@ use DrevOps\Tui\Primitive\Status; use DrevOps\Tui\Render\Ansi; +use DrevOps\Tui\Tests\Traits\BuildsThemesTrait; use DrevOps\Tui\Theme\DefaultTheme; use DrevOps\Tui\Theme\EmberTheme; use DrevOps\Tui\Theme\Mode; @@ -24,6 +25,8 @@ #[Group('theme')] final class OutputRenderTest extends TestCase { + use BuildsThemesTrait; + public function testBoxDrawsTitledFrameSizedToItsContent(): void { $lines = $this->theme(color: FALSE)->renderCard('Welcome', ['Pick your fruit.']); @@ -354,19 +357,4 @@ public function testDefinitionsStyleLabelsAndValuesApart(): void { $this->assertStringContainsString('Jars', $lines[0]); } - /** - * A default theme in the given display modes, fixed to dark. - * - * @param bool $color - * Whether colour is on. - * @param bool $unicode - * Whether Unicode glyphs are on. - * - * @return \DrevOps\Tui\Theme\DefaultTheme - * The theme. - */ - protected function theme(bool $color = TRUE, bool $unicode = TRUE): DefaultTheme { - return ThemeManager::create('default', DefaultTheme::DEFAULT_WIDTH, ['color' => $color, 'unicode' => $unicode, 'mode' => Mode::Dark]); - } - } diff --git a/tests/phpunit/Unit/Theme/ProgressRenderTest.php b/tests/phpunit/Unit/Theme/ProgressRenderTest.php index 12303219..4feb0639 100644 --- a/tests/phpunit/Unit/Theme/ProgressRenderTest.php +++ b/tests/phpunit/Unit/Theme/ProgressRenderTest.php @@ -4,6 +4,7 @@ namespace DrevOps\Tui\Tests\Unit\Theme; +use DrevOps\Tui\Tests\Traits\BuildsThemesTrait; use DrevOps\Tui\Theme\DefaultTheme; use DrevOps\Tui\Theme\EmberTheme; use DrevOps\Tui\Theme\Mode; @@ -20,6 +21,8 @@ #[Group('theme')] final class ProgressRenderTest extends TestCase { + use BuildsThemesTrait; + public function testSpinnerCyclesFramesInTheAccent(): void { $theme = $this->theme(); @@ -110,19 +113,4 @@ public function testLoadingAsciiFallbackAndEmptyCaption(): void { $this->assertStringContainsString('Loading ...', $theme->renderLoading('Loading')); } - /** - * A default theme in the given display modes, fixed to dark. - * - * @param bool $color - * Whether colour is on. - * @param bool $unicode - * Whether Unicode glyphs are on. - * - * @return \DrevOps\Tui\Theme\DefaultTheme - * The theme. - */ - protected function theme(bool $color = TRUE, bool $unicode = TRUE): DefaultTheme { - return ThemeManager::create('default', DefaultTheme::DEFAULT_WIDTH, ['color' => $color, 'unicode' => $unicode, 'mode' => Mode::Dark]); - } - } diff --git a/tests/phpunit/Unit/Theme/ScaleRenderTest.php b/tests/phpunit/Unit/Theme/ScaleRenderTest.php index 78296e25..a73ebb0c 100644 --- a/tests/phpunit/Unit/Theme/ScaleRenderTest.php +++ b/tests/phpunit/Unit/Theme/ScaleRenderTest.php @@ -8,6 +8,7 @@ use DrevOps\Tui\Builder\PanelBuilder; use DrevOps\Tui\Render\Ansi; use DrevOps\Tui\Testing\TuiTester; +use DrevOps\Tui\Tests\Traits\BuildsThemesTrait; use DrevOps\Tui\Theme\DefaultTheme; use DrevOps\Tui\Theme\Mode; use DrevOps\Tui\Theme\ThemeManager; @@ -23,6 +24,8 @@ #[Group('theme')] final class ScaleRenderTest extends TestCase { + use BuildsThemesTrait; + public function testScaleFillsUpToTheChosenPoint(): void { $line = $this->theme(color: FALSE)->renderScale(3, 1, 5, 'Fair'); @@ -124,19 +127,4 @@ protected function ratingForm(): Form { }); } - /** - * A default theme in the given display modes, fixed to dark. - * - * @param bool $color - * Whether colour is on. - * @param bool $unicode - * Whether Unicode glyphs are on. - * - * @return \DrevOps\Tui\Theme\DefaultTheme - * The theme. - */ - protected function theme(bool $color = TRUE, bool $unicode = TRUE): DefaultTheme { - return ThemeManager::create('default', DefaultTheme::DEFAULT_WIDTH, ['color' => $color, 'unicode' => $unicode, 'mode' => Mode::Dark]); - } - } diff --git a/tests/phpunit/Unit/Theme/ThemeConditionalIndentTest.php b/tests/phpunit/Unit/Theme/ThemeConditionalIndentTest.php index d1bc18c5..a017aeae 100644 --- a/tests/phpunit/Unit/Theme/ThemeConditionalIndentTest.php +++ b/tests/phpunit/Unit/Theme/ThemeConditionalIndentTest.php @@ -36,7 +36,7 @@ final class ThemeConditionalIndentTest extends TestCase { public function testOptionIsOffByDefault(): void { $panel = $this->chainPanel(); - [$lines] = $this->theme(FALSE)->renderBody($panel, new Answers(), 0); + [$lines] = $this->indentTheme(FALSE)->renderBody($panel, new Answers(), 0); // The cursor row leads with the marker glyph; every other row leads with // the two blank columns that stand in for it, and nothing more. @@ -47,7 +47,7 @@ public function testOptionIsOffByDefault(): void { public function testEachConditionStepsTheRowInFurther(): void { $panel = $this->chainPanel(); - [$lines] = $this->theme()->renderBody($panel, new Answers(), 0); + [$lines] = $this->indentTheme()->renderBody($panel, new Answers(), 0); // The cursor sits on the unconditional root, so its marker shows; the // conditional rows carry the blank marker plus one step per condition. @@ -63,7 +63,7 @@ public function testIndentedRowKeepsBadgeAtFrameEdge(): void { $field = $this->fieldsOf($this->chainPanel())['first']; $answers = new Answers(['first' => 'Acme'], ['first' => Provenance::Edited]); - $lines = $this->theme()->renderFieldLine($field, $answers, FALSE); + $lines = $this->indentTheme()->renderFieldLine($field, $answers, FALSE); $this->assertStringContainsString('First Acme', Ansi::strip($lines[0])); $this->assertStringContainsString('edited', Ansi::strip($lines[0])); @@ -80,7 +80,7 @@ public function testValueContinuationLinesFollowTheIndentedColumn(): void { $form = new FormDefinition('T', 'S', [$panel]); $answers = new Answers(['notes' => "Crisp and sweet\nHint of citrus"], []); - $lines = $this->theme()->renderFieldLine($this->fieldsOf($form->panels[0])['notes'], $answers, FALSE); + $lines = $this->indentTheme()->renderFieldLine($this->fieldsOf($form->panels[0])['notes'], $answers, FALSE); $this->assertSame(' Notes Crisp and sweet', Ansi::strip($lines[0])); // The second line aligns under the value column, which the gutter moved. @@ -94,7 +94,7 @@ public function testDescriptionStepsInWithItsField(): void { ]); new FormDefinition('T', 'S', [$panel]); - [$lines] = $this->theme()->renderBody($panel, new Answers(), 0); + [$lines] = $this->indentTheme()->renderBody($panel, new Answers(), 0); $rows = array_map(Ansi::strip(...), $lines); // A description already sits four columns in; the conditional one carries @@ -106,7 +106,7 @@ public function testDescriptionStepsInWithItsField(): void { public function testInlineEditorOpensAtTheIndentedColumn(): void { $field = $this->fieldsOf($this->chainPanel())['second']; - $lines = $this->theme()->renderInlineEditor($field, "line one\nline two", TRUE); + $lines = $this->indentTheme()->renderInlineEditor($field, "line one\nline two", TRUE); $this->assertSame(' ❯ Second line one', Ansi::strip($lines[0])); $this->assertSame($this->columnOf($lines[0], 'line one'), $this->columnOf($lines[1], 'line two')); @@ -119,7 +119,7 @@ public function testPlainNoteCardStepsInWithItsCondition(): void { ]); new FormDefinition('T', 'S', [$panel]); - $lines = array_map(Ansi::strip(...), $this->theme()->renderNoteLines($this->fieldsOf($panel)['hint'], new Answers())); + $lines = array_map(Ansi::strip(...), $this->indentTheme()->renderNoteLines($this->fieldsOf($panel)['hint'], new Answers())); // A card already sits in a two-column gutter of its own. $this->assertSame(' Storage', $lines[0]); @@ -134,8 +134,8 @@ public function testBorderedNoteNarrowsToStayInsideTheFrame(): void { new FormDefinition('T', 'S', [$panel]); $note = $this->fieldsOf($panel)['hint']; - $flush = $this->theme(FALSE)->renderNoteLines($note, new Answers()); - $stepped = $this->theme()->renderNoteLines($note, new Answers()); + $flush = $this->indentTheme(FALSE)->renderNoteLines($note, new Answers()); + $stepped = $this->indentTheme()->renderNoteLines($note, new Answers()); // The body wraps to the room the card's own chrome leaves, so an indented // card narrows by the columns its gutter takes and its right edge still @@ -160,8 +160,8 @@ public function testMeasuredWidthCoversTheIndent(): void { ]); $form = new FormDefinition('T', 'S', [$panel], buttons: new Buttons(show: FALSE)); - $flush = $this->theme(FALSE)->measureContentWidth($form, new Answers()); - $stepped = $this->theme()->measureContentWidth($form, new Answers()); + $flush = $this->indentTheme(FALSE)->measureContentWidth($form, new Answers()); + $stepped = $this->indentTheme()->measureContentWidth($form, new Answers()); $this->assertSame($flush + self::STEP, $stepped); } @@ -174,7 +174,7 @@ public function testGridColumnPreviewStepsInToo(): void { $panel = new Panel('p', 'P', '', [], [$sub], layout: [1]); new FormDefinition('T', 'S', [$panel]); - [$lines] = $this->theme()->renderBody($panel, new Answers(), -1); + [$lines] = $this->indentTheme()->renderBody($panel, new Answers(), -1); $rows = array_map(Ansi::strip(...), $lines); $this->assertSame(2, $this->leadingSpaces($rows[1]), 'An unconditional preview row keeps the block gutter.'); @@ -290,7 +290,7 @@ protected function columnOf(string $line, string $needle): int { * @return \DrevOps\Tui\Theme\DefaultTheme * The theme. */ - protected function theme(bool $indent = TRUE): DefaultTheme { + protected function indentTheme(bool $indent = TRUE): DefaultTheme { return new DefaultTheme(40, [ 'color' => FALSE, 'border' => Border::None, diff --git a/tests/phpunit/Unit/Theme/ThemeRenderTest.php b/tests/phpunit/Unit/Theme/ThemeRenderTest.php index b32ffdf2..a49c4ccd 100644 --- a/tests/phpunit/Unit/Theme/ThemeRenderTest.php +++ b/tests/phpunit/Unit/Theme/ThemeRenderTest.php @@ -21,6 +21,7 @@ use DrevOps\Tui\Render\HelpSection; use DrevOps\Tui\Render\Navigator; use DrevOps\Tui\Render\Viewport; +use DrevOps\Tui\Tests\Traits\BuildsThemesTrait; use DrevOps\Tui\Theme\Border; use DrevOps\Tui\Theme\DefaultTheme; use DrevOps\Tui\Theme\Spacing; @@ -37,8 +38,10 @@ #[Group('theme')] final class ThemeRenderTest extends TestCase { + use BuildsThemesTrait; + public function testFieldLineSelectedRightAlignsBadge(): void { - $lines = $this->theme()->renderFieldLine(new Field('name', 'Name', '', FieldType::Text, ''), new Answers(['name' => 'Acme'], ['name' => Provenance::Edited]), TRUE); + $lines = $this->plainTheme()->renderFieldLine(new Field('name', 'Name', '', FieldType::Text, ''), new Answers(['name' => 'Acme'], ['name' => Provenance::Edited]), TRUE); // A single-line value is one row. $this->assertCount(1, $lines); @@ -48,14 +51,14 @@ public function testFieldLineSelectedRightAlignsBadge(): void { } public function testFieldLineDefaultHasNoBadge(): void { - $lines = $this->theme()->renderFieldLine(new Field('name', 'Name', '', FieldType::Text, ''), new Answers(['name' => 'Acme'], ['name' => Provenance::Default]), FALSE); + $lines = $this->plainTheme()->renderFieldLine(new Field('name', 'Name', '', FieldType::Text, ''), new Answers(['name' => 'Acme'], ['name' => Provenance::Default]), FALSE); $this->assertStringNotContainsString('default', $lines[0]); $this->assertStringContainsString('Name Acme', Ansi::strip($lines[0])); } public function testFieldLineRendersValues(): void { - $theme = $this->theme(); + $theme = $this->plainTheme(); $bool = Ansi::strip($theme->renderFieldLine(new Field('b', 'B', '', FieldType::Confirm, FALSE), new Answers(['b' => TRUE], ['b' => Provenance::Default]), FALSE)[0]); $this->assertStringContainsString('B yes', $bool); @@ -67,20 +70,20 @@ public function testFieldLineRendersValues(): void { public function testFieldLineMasksPasswordValue(): void { $field = new Field('token', 'Token', '', FieldType::Password, ''); - $line = Ansi::strip($this->theme()->renderFieldLine($field, new Answers(['token' => 's3cret-long'], ['token' => Provenance::Edited]), FALSE)[0]); + $line = Ansi::strip($this->plainTheme()->renderFieldLine($field, new Answers(['token' => 's3cret-long'], ['token' => Provenance::Edited]), FALSE)[0]); $this->assertStringNotContainsString('s3cret-long', $line); // The mask has a fixed length so it does not leak the value's length. $this->assertStringContainsString('Token ••••••••', $line); - $empty = Ansi::strip($this->theme()->renderFieldLine($field, new Answers(['token' => ''], ['token' => Provenance::Default]), FALSE)[0]); + $empty = Ansi::strip($this->plainTheme()->renderFieldLine($field, new Answers(['token' => ''], ['token' => Provenance::Default]), FALSE)[0]); $this->assertStringNotContainsString('•', $empty); } public function testRenderInlineEditorPutsViewInPlaceOfValue(): void { $field = new Field('cdn', 'CDN', '', FieldType::Confirm, FALSE); - $lines = $this->theme()->renderInlineEditor($field, "line one\nline two", TRUE); + $lines = $this->plainTheme()->renderInlineEditor($field, "line one\nline two", TRUE); // The view's first line sits on the label row where the value would be; a // further line aligns under that value column. @@ -93,7 +96,7 @@ public function testBodyExpandsMultiLineValueAcrossRows(): void { $panel = new Panel('p', 'P', '', [new Field('notes', 'Notes', '', FieldType::Textarea, '')]); $answers = new Answers(['notes' => "Crisp and sweet\nHint of citrus"], ['notes' => Provenance::Edited]); - [$lines] = $this->theme()->renderBody($panel, $answers, 0); + [$lines] = $this->plainTheme()->renderBody($panel, $answers, 0); // Each body entry is one physical row: an embedded newline would desync the // box border, the badge alignment and the scroll maths. @@ -118,7 +121,7 @@ public function testBodyExpandsInlineEditorMultiLineView(): void { $panel = new Panel('p', 'P', '', [$field]); // The editor hands back a multi-line caret view for the field being edited. - [$lines] = $this->theme()->renderBody($panel, new Answers(), 0, $field, "Crisp and sweet\nHint of citrus"); + [$lines] = $this->plainTheme()->renderBody($panel, new Answers(), 0, $field, "Crisp and sweet\nHint of citrus"); foreach ($lines as $line) { $this->assertStringNotContainsString("\n", $line); @@ -145,7 +148,7 @@ public function testBodyRendersHintUnderDescription(): void { $field = new Field('name', 'Name', 'The grower of record', FieldType::Text, '', hint: 'Type a few letters to filter.'); $panel = new Panel('p', 'P', '', [$field]); - [$lines] = $this->theme()->renderBody($panel, new Answers(), 0); + [$lines] = $this->plainTheme()->renderBody($panel, new Answers(), 0); $stripped = array_map(Ansi::strip(...), $lines); // Guidance stacks under the row in declaration order: what is being asked, @@ -158,7 +161,7 @@ public function testBodyRendersHintWithoutDescription(): void { $field = new Field('name', 'Name', '', FieldType::Text, '', hint: 'Type a few letters to filter.'); $panel = new Panel('p', 'P', '', [$field]); - [$lines] = $this->theme()->renderBody($panel, new Answers(), 0); + [$lines] = $this->plainTheme()->renderBody($panel, new Answers(), 0); $stripped = array_map(Ansi::strip(...), $lines); $this->assertSame(' Type a few letters to filter.', $stripped[1]); @@ -169,7 +172,7 @@ public function testBodyRendersHintUnderInlineEditor(): void { $field = new Field('name', 'Name', '', FieldType::Text, '', hint: 'Type a few letters to filter.'); $panel = new Panel('p', 'P', '', [$field]); - [$lines] = $this->theme()->renderBody($panel, new Answers(), 0, $field, 'Acme'); + [$lines] = $this->plainTheme()->renderBody($panel, new Answers(), 0, $field, 'Acme'); $stripped = array_map(Ansi::strip(...), $lines); $this->assertStringContainsString('Name Acme', $stripped[0]); @@ -224,7 +227,7 @@ public function testPanelSummaryCollapsesMultiLineValue(): void { $panel = new Panel('sub', 'Sub', '', [new Field('notes', 'Notes', '', FieldType::Textarea, '')]); $answers = new Answers(['notes' => "Crisp and sweet\nHint of citrus"], []); - $summary = $this->theme()->summarizePanel($panel, $answers); + $summary = $this->plainTheme()->summarizePanel($panel, $answers); // A summary is a single line: newlines collapse so a multi-line value does // not break the row it sits on. @@ -238,7 +241,7 @@ public function testBodyNormalizesLineEndingsInMultiLineValue(string $value): vo $panel = new Panel('p', 'P', '', [new Field('notes', 'Notes', '', FieldType::Textarea, '')]); $answers = new Answers(['notes' => $value], []); - [$lines] = $this->theme()->renderBody($panel, $answers, 0); + [$lines] = $this->plainTheme()->renderBody($panel, $answers, 0); // A carriage return would send the terminal cursor back to the row start // and overprint the row, so every line ending an external editor's save @@ -264,7 +267,7 @@ public function testPanelSummaryNormalizesLineEndings(string $value): void { $panel = new Panel('sub', 'Sub', '', [new Field('notes', 'Notes', '', FieldType::Textarea, '')]); $answers = new Answers(['notes' => $value], []); - $summary = $this->theme()->summarizePanel($panel, $answers); + $summary = $this->plainTheme()->summarizePanel($panel, $answers); $this->assertStringNotContainsString("\r", $summary); $this->assertStringNotContainsString("\n", $summary); @@ -278,7 +281,7 @@ public static function dataProviderPanelSummaryNormalizesLineEndings(): \Iterato } public function testPanelLineShowsDrillIndicator(): void { - $line = Ansi::strip($this->theme()->renderPanelLine(new Panel('adv', 'Advanced', ''), TRUE)); + $line = Ansi::strip($this->plainTheme()->renderPanelLine(new Panel('adv', 'Advanced', ''), TRUE)); $this->assertStringContainsString('❯ Advanced', $line); $this->assertStringContainsString('›', $line); @@ -290,7 +293,7 @@ public function testBodyReportsCursorLine(): void { new Field('b', 'B', '', FieldType::Text, ''), ]); - [$lines, $cursor_line] = $this->theme()->renderBody($panel, new Answers(), 1); + [$lines, $cursor_line] = $this->plainTheme()->renderBody($panel, new Answers(), 1); $this->assertSame(2, $cursor_line); $this->assertStringContainsString('❯ B', Ansi::strip($lines[2])); @@ -304,7 +307,7 @@ public function testBodyRendersNoteCardAndSkipsItInTheCursorCount(): void { ]); // Cursor index 1 is the second navigable field; the note is not counted. - [$lines, $cursor_line] = $this->theme()->renderBody($panel, new Answers(['name' => 'Acme', 'agree' => FALSE], []), 1); + [$lines, $cursor_line] = $this->plainTheme()->renderBody($panel, new Answers(['name' => 'Acme', 'agree' => FALSE], []), 1); $body = Ansi::strip(implode("\n", $lines)); $this->assertStringContainsString('Getting started', $body); @@ -321,17 +324,17 @@ public function testBodySkipsEmptyNote(): void { new Field('blank', '', '', FieldType::Note, ''), ]); - [$lines] = $this->theme()->renderBody($panel, new Answers(['name' => 'Acme'], []), 0); + [$lines] = $this->plainTheme()->renderBody($panel, new Answers(['name' => 'Acme'], []), 0); // A note with neither title nor body contributes no lines. $this->assertStringContainsString('Name', Ansi::strip(implode("\n", $lines))); - $this->assertSame([], $this->theme()->renderNoteLines(new Field('blank', '', '', FieldType::Note, ''), new Answers())); + $this->assertSame([], $this->plainTheme()->renderNoteLines(new Field('blank', '', '', FieldType::Note, ''), new Answers())); } public function testNoteInterpolatesAnswersInTitleAndBody(): void { $note = new Field('echo', 'Hello {{name}}', 'You picked {{fruit}}.', FieldType::Note, ''); - $lines = Ansi::strip(implode("\n", $this->theme()->renderNoteLines($note, new Answers(['name' => 'Ada', 'fruit' => 'pear'], [])))); + $lines = Ansi::strip(implode("\n", $this->plainTheme()->renderNoteLines($note, new Answers(['name' => 'Ada', 'fruit' => 'pear'], [])))); $this->assertStringContainsString('Hello Ada', $lines); $this->assertStringContainsString('You picked pear.', $lines); @@ -384,7 +387,7 @@ public function testPaddedSpacingSeparatesNoteFromTheFieldAbove(): void { public function testRenderNoteLinesBoxesBorderedNote(): void { // The theme frame is borderless, so an opt-in note border falls back to the // single-line box; its glyphs come only from the note. - $lines = $this->theme()->renderNoteLines(new Field('boxed', 'Boxed', 'In a box.', FieldType::Note, '', bordered: TRUE), new Answers()); + $lines = $this->plainTheme()->renderNoteLines(new Field('boxed', 'Boxed', 'In a box.', FieldType::Note, '', bordered: TRUE), new Answers()); $joined = Ansi::strip(implode("\n", $lines)); $this->assertStringContainsString('Boxed', $joined); @@ -406,7 +409,7 @@ public function testRenderTableDrawsAlignedGrid(): void { '└───────┴────────┘', ]; - $this->assertSame($expected, $this->theme()->renderTable(['Fruit', 'Colour'], [['Apple', 'Red']])); + $this->assertSame($expected, $this->plainTheme()->renderTable(['Fruit', 'Colour'], [['Apple', 'Red']])); } public function testRenderTableColorsCellsAndBorders(): void { @@ -441,7 +444,7 @@ public function testRenderTableCapsAtFrameWidth(): void { public function testNoteRendersTableBeneathTitleAndBody(): void { $field = new Field('stock', 'Stock', 'Current basket:', FieldType::Note, '', table: new TableSpec(['Fruit', 'Qty'], [['Apple', '3']])); - $joined = Ansi::strip(implode("\n", $this->theme()->renderNoteLines($field, new Answers()))); + $joined = Ansi::strip(implode("\n", $this->plainTheme()->renderNoteLines($field, new Answers()))); $this->assertStringContainsString('Stock', $joined); $this->assertStringContainsString('Current basket:', $joined); @@ -451,7 +454,7 @@ public function testNoteRendersTableBeneathTitleAndBody(): void { public function testNoteInterpolatesTableCells(): void { $field = new Field('echo', '', '', FieldType::Note, '', table: new TableSpec(['Item'], [['{{fruit}}']])); - $joined = Ansi::strip(implode("\n", $this->theme()->renderNoteLines($field, new Answers(['fruit' => 'pear'], [])))); + $joined = Ansi::strip(implode("\n", $this->plainTheme()->renderNoteLines($field, new Answers(['fruit' => 'pear'], [])))); $this->assertStringContainsString('pear', $joined); $this->assertStringNotContainsString('{{fruit}}', $joined); @@ -461,7 +464,7 @@ public function testNoteTableFoldsInterpolatedNewlines(): void { // An answer carrying newlines - a textarea value - interpolated into a cell // folds to one row so it never splits the grid. $field = new Field('memo', '', '', FieldType::Note, '', table: new TableSpec(['Memo'], [['{{memo}}']])); - $lines = $this->theme()->renderNoteLines($field, new Answers(['memo' => "first\nsecond"], [])); + $lines = $this->plainTheme()->renderNoteLines($field, new Answers(['memo' => "first\nsecond"], [])); foreach ($lines as $line) { $this->assertStringNotContainsString("\n", $line); @@ -472,7 +475,7 @@ public function testNoteTableFoldsInterpolatedNewlines(): void { public function testBorderedNoteBoxesItsTable(): void { $field = new Field('stock', 'Stock', '', FieldType::Note, '', bordered: TRUE, table: new TableSpec(['Fruit'], [['Apple']])); - $joined = Ansi::strip(implode("\n", $this->theme()->renderNoteLines($field, new Answers()))); + $joined = Ansi::strip(implode("\n", $this->plainTheme()->renderNoteLines($field, new Answers()))); $this->assertStringContainsString('Stock', $joined); $this->assertStringContainsString('Apple', $joined); @@ -485,7 +488,7 @@ public function testBodyIncludesSubPanels(): void { new Panel('sub', 'Sub', 'sub desc'), ]); - [$lines, $cursor_line] = $this->theme()->renderBody($panel, new Answers(), 1); + [$lines, $cursor_line] = $this->plainTheme()->renderBody($panel, new Answers(), 1); // The cursor is on the sub-panel (index 1, after the single field). $this->assertSame(1, $cursor_line); @@ -498,7 +501,7 @@ public function testBodyIncludesPanelSummary(): void { new Panel('general', 'General', 'the general panel', [new Field('name', 'Name', '', FieldType::Text, '')]), ]); - [$lines] = $this->theme()->renderBody($hub, new Answers(['name' => 'Acme'], []), 0); + [$lines] = $this->plainTheme()->renderBody($hub, new Answers(['name' => 'Acme'], []), 0); // The hub shows the sub-panel's title, description and value summary. $body = Ansi::strip(implode("\n", $lines)); @@ -520,11 +523,11 @@ public function testPanelSummaryJoinsActiveValues(): void { // "gated" is skipped (no answer), the multiselect condenses to a pluralized // count, and only the first four active values appear ("Delta" is dropped). - $this->assertSame('Acme · Beta · 4 items selected · Gamma', $this->theme()->summarizePanel($panel, $answers)); + $this->assertSame('Acme · Beta · 4 items selected · Gamma', $this->plainTheme()->summarizePanel($panel, $answers)); } public function testSummaryLineClipsToWidth(): void { - $line = Ansi::strip($this->theme()->renderSummaryLine(str_repeat('x', 100), FALSE)); + $line = Ansi::strip($this->plainTheme()->renderSummaryLine(str_repeat('x', 100), FALSE)); $this->assertLessThanOrEqual(40, mb_strlen($line)); $this->assertStringContainsString('…', $line); @@ -558,7 +561,7 @@ public function testSelectedItemIsBold(): void { public function testFrameShowsIndicatorsAndWindow(): void { $body = array_map(static fn(int $i): string => 'line' . $i, range(0, 9)); - $frame = $this->theme()->renderFrame(['HEAD'], $body, ['FOOT'], new Viewport(3, TRUE, TRUE), 4); + $frame = $this->plainTheme()->renderFrame(['HEAD'], $body, ['FOOT'], new Viewport(3, TRUE, TRUE), 4); $this->assertStringContainsString('▲', $frame); $this->assertStringContainsString('▼', $frame); @@ -571,16 +574,16 @@ public function testFrameShowsIndicatorsAndWindow(): void { public function testBreadcrumbLine(): void { $navigator = new Navigator(new Panel('hub', 'Hub', '', [], [new Panel('d', 'Drupal', '')])); - $this->assertSame('Hub', Ansi::strip($this->theme()->renderBreadcrumbLine($navigator))); + $this->assertSame('Hub', Ansi::strip($this->plainTheme()->renderBreadcrumbLine($navigator))); } public function testBanner(): void { - $banner = Ansi::strip($this->theme()->renderBanner("LOGO\nline", '1.2.3')); + $banner = Ansi::strip($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->theme()->renderBanner('LOGO', ''))); + $this->assertStringNotContainsString('Version', Ansi::strip($this->plainTheme()->renderBanner('LOGO', ''))); } public function testHintsLineIsThemed(): void { @@ -740,11 +743,11 @@ public function testPanelError(): void { public function testDimRecedesText(): void { // With colour, dim wraps the text; with colour off, it is left untouched. $this->assertSame("\033[2mx\033[0m", (new DefaultTheme(40))->dim('x')); - $this->assertSame('x', $this->theme()->dim('x')); + $this->assertSame('x', $this->plainTheme()->dim('x')); } public function testRenderModalCentersDialogOverBackdrop(): void { - $theme = $this->theme(); + $theme = $this->plainTheme(); $modal = new Panel('c', 'Confirm', 'Proceed with care.', [ new Field('opt', 'Option', '', FieldType::Text, 'val'), ], [], new Modal(new Buttons(TRUE, 'Yes', 'No'))); @@ -763,7 +766,7 @@ public function testRenderModalCentersDialogOverBackdrop(): void { } public function testRenderModalWithoutFields(): void { - $theme = $this->theme(); + $theme = $this->plainTheme(); $modal = new Panel('n', 'Notice', 'Saved successfully.', [], [], new Modal()); $backdrop = "aaaaaa\nbbbbbb\ncccccc\ndddddd\neeeeee\nffffff\ngggggg\nhhhhhh"; @@ -790,7 +793,7 @@ public function testRenderModalForcesBorderOnBorderlessTheme(): void { } public function testRenderModalScrollsBodyAndPinsButtonsWhenTall(): void { - $theme = $this->theme(); + $theme = $this->plainTheme(); $fields = []; $values = []; for ($i = 1; $i <= 10; $i++) { @@ -811,17 +814,4 @@ public function testRenderModalScrollsBodyAndPinsButtonsWhenTall(): void { $this->assertStringNotContainsString('Field 10', $out); } - /** - * A colourless borderless theme of fixed width. - * - * The explicit border and spacing keep these layout assertions on the - * plain look, independent of the theme's bordered-and-padded defaults. - * - * @return \DrevOps\Tui\Theme\DefaultTheme - * The theme. - */ - protected function theme(): DefaultTheme { - return new DefaultTheme(40, ['color' => FALSE, 'border' => Border::None, 'spacing' => Spacing::Normal]); - } - } From 30e3d89ccad50111fe1e4babd8d2380c26bdd082 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Wed, 29 Jul 2026 16:12:27 +1000 Subject: [PATCH 12/14] Drew the demo fruit from the documented vocabulary. The table demo listed a blackberry and the option-group demo a rhubarb, neither of which is in the reference vocabulary, and the first is also a brand name the reference bans outright. They become a plum and a cherry, which the reference already names as canonical select options. The screenshot specs, the pty recording anchor and the documentation code blocks carry the same values, so all 24 affected SVGs were regenerated and audited. --- docs/assets/widget-select-groups-dark-static-ascii-no-ansi.svg | 2 +- docs/assets/widget-select-groups-dark-static-ascii.svg | 2 +- docs/assets/widget-select-groups-dark-static-no-ansi.svg | 2 +- docs/assets/widget-select-groups-dark-static.svg | 2 +- docs/assets/widget-select-groups-light-static-ascii-no-ansi.svg | 2 +- docs/assets/widget-select-groups-light-static-ascii.svg | 2 +- docs/assets/widget-select-groups-light-static-no-ansi.svg | 2 +- docs/assets/widget-select-groups-light-static.svg | 2 +- docs/assets/widget-table-dark-animated-ascii-no-ansi.svg | 2 +- docs/assets/widget-table-dark-animated-ascii.svg | 2 +- docs/assets/widget-table-dark-animated-no-ansi.svg | 2 +- docs/assets/widget-table-dark-animated.svg | 2 +- docs/assets/widget-table-dark-static-ascii-no-ansi.svg | 2 +- docs/assets/widget-table-dark-static-ascii.svg | 2 +- docs/assets/widget-table-dark-static-no-ansi.svg | 2 +- docs/assets/widget-table-dark-static.svg | 2 +- docs/assets/widget-table-light-animated-ascii-no-ansi.svg | 2 +- docs/assets/widget-table-light-animated-ascii.svg | 2 +- docs/assets/widget-table-light-animated-no-ansi.svg | 2 +- docs/assets/widget-table-light-animated.svg | 2 +- docs/assets/widget-table-light-static-ascii-no-ansi.svg | 2 +- docs/assets/widget-table-light-static-ascii.svg | 2 +- docs/assets/widget-table-light-static-no-ansi.svg | 2 +- docs/assets/widget-table-light-static.svg | 2 +- docs/content/widgets/option-groups.mdx | 2 +- docs/content/widgets/table.mdx | 2 +- docs/util/render-widget-svgs.php | 2 +- docs/util/update-assets.php | 2 +- playground/02-widgets-select-groups.php | 2 +- playground/02-widgets-table.php | 2 +- 30 files changed, 30 insertions(+), 30 deletions(-) diff --git a/docs/assets/widget-select-groups-dark-static-ascii-no-ansi.svg b/docs/assets/widget-select-groups-dark-static-ascii-no-ansi.svg index 9a9028d0..b85d8489 100644 --- a/docs/assets/widget-select-groups-dark-static-ascii-no-ansi.svg +++ b/docs/assets/widget-select-groups-dark-static-ascii-no-ansi.svg @@ -1 +1 @@ -+------------------------------------------+|||Selectwithgroups>Select||>FruitFruit||(*)Apple||()Banana||-------------------------------||()Rhubarb(outofseason)||^/vmove*<accept*esccancel| \ No newline at end of file ++------------------------------------------+|||Selectwithgroups>Select||>FruitFruit||(*)Apple||()Banana||-------------------------------||()Cherry(outofseason)||^/vmove*<accept*esccancel| \ No newline at end of file diff --git a/docs/assets/widget-select-groups-dark-static-ascii.svg b/docs/assets/widget-select-groups-dark-static-ascii.svg index 5c4bbbe3..4a83e980 100644 --- a/docs/assets/widget-select-groups-dark-static-ascii.svg +++ b/docs/assets/widget-select-groups-dark-static-ascii.svg @@ -1 +1 @@ -+------------------------------------------+|||Selectwithgroups>Select||>FruitFruit||(*)Apple||()Banana||-------------------------------||()Rhubarb(outofseason)||^/vmove*<accept*esccancel| \ No newline at end of file ++------------------------------------------+|||Selectwithgroups>Select||>FruitFruit||(*)Apple||()Banana||-------------------------------||()Cherry(outofseason)||^/vmove*<accept*esccancel| \ No newline at end of file diff --git a/docs/assets/widget-select-groups-dark-static-no-ansi.svg b/docs/assets/widget-select-groups-dark-static-no-ansi.svg index 3cdaf868..9d7aeae7 100644 --- a/docs/assets/widget-select-groups-dark-static-no-ansi.svg +++ b/docs/assets/widget-select-groups-dark-static-no-ansi.svg @@ -1 +1 @@ -├──────────────────────────────────────────┤╭──────────────────────────────────────────╮SelectwithgroupsSelectFruitFruitAppleBanana───────────────────────────────Rhubarb(outofseason)↑/↓move·accept·esccancel╰──────────────────────────────────────────╯ \ No newline at end of file +├──────────────────────────────────────────┤╭──────────────────────────────────────────╮SelectwithgroupsSelectFruitFruitAppleBanana───────────────────────────────Cherry(outofseason)↑/↓move·accept·esccancel╰──────────────────────────────────────────╯ \ No newline at end of file diff --git a/docs/assets/widget-select-groups-dark-static.svg b/docs/assets/widget-select-groups-dark-static.svg index 8722fef3..70311547 100644 --- a/docs/assets/widget-select-groups-dark-static.svg +++ b/docs/assets/widget-select-groups-dark-static.svg @@ -1 +1 @@ -├──────────────────────────────────────────┤╭──────────────────────────────────────────╮SelectwithgroupsSelectFruitFruitAppleBanana───────────────────────────────Rhubarb(outofseason)↑/↓move·accept·esccancel╰──────────────────────────────────────────╯ \ No newline at end of file +├──────────────────────────────────────────┤╭──────────────────────────────────────────╮SelectwithgroupsSelectFruitFruitAppleBanana───────────────────────────────Cherry(outofseason)↑/↓move·accept·esccancel╰──────────────────────────────────────────╯ \ No newline at end of file diff --git a/docs/assets/widget-select-groups-light-static-ascii-no-ansi.svg b/docs/assets/widget-select-groups-light-static-ascii-no-ansi.svg index 12cb0c7e..d34b59a6 100644 --- a/docs/assets/widget-select-groups-light-static-ascii-no-ansi.svg +++ b/docs/assets/widget-select-groups-light-static-ascii-no-ansi.svg @@ -1 +1 @@ -+------------------------------------------+|||Selectwithgroups>Select||>FruitFruit||(*)Apple||()Banana||-------------------------------||()Rhubarb(outofseason)||^/vmove*<accept*esccancel| \ No newline at end of file ++------------------------------------------+|||Selectwithgroups>Select||>FruitFruit||(*)Apple||()Banana||-------------------------------||()Cherry(outofseason)||^/vmove*<accept*esccancel| \ No newline at end of file diff --git a/docs/assets/widget-select-groups-light-static-ascii.svg b/docs/assets/widget-select-groups-light-static-ascii.svg index 00b5159f..3c70b31b 100644 --- a/docs/assets/widget-select-groups-light-static-ascii.svg +++ b/docs/assets/widget-select-groups-light-static-ascii.svg @@ -1 +1 @@ -+------------------------------------------+|||Selectwithgroups>Select||>FruitFruit||(*)Apple||()Banana||-------------------------------||()Rhubarb(outofseason)||^/vmove*<accept*esccancel| \ No newline at end of file ++------------------------------------------+|||Selectwithgroups>Select||>FruitFruit||(*)Apple||()Banana||-------------------------------||()Cherry(outofseason)||^/vmove*<accept*esccancel| \ No newline at end of file diff --git a/docs/assets/widget-select-groups-light-static-no-ansi.svg b/docs/assets/widget-select-groups-light-static-no-ansi.svg index 6e440c37..e1c83eab 100644 --- a/docs/assets/widget-select-groups-light-static-no-ansi.svg +++ b/docs/assets/widget-select-groups-light-static-no-ansi.svg @@ -1 +1 @@ -├──────────────────────────────────────────┤╭──────────────────────────────────────────╮SelectwithgroupsSelectFruitFruitAppleBanana───────────────────────────────Rhubarb(outofseason)↑/↓move·accept·esccancel╰──────────────────────────────────────────╯ \ No newline at end of file +├──────────────────────────────────────────┤╭──────────────────────────────────────────╮SelectwithgroupsSelectFruitFruitAppleBanana───────────────────────────────Cherry(outofseason)↑/↓move·accept·esccancel╰──────────────────────────────────────────╯ \ No newline at end of file diff --git a/docs/assets/widget-select-groups-light-static.svg b/docs/assets/widget-select-groups-light-static.svg index d77f45bc..62554965 100644 --- a/docs/assets/widget-select-groups-light-static.svg +++ b/docs/assets/widget-select-groups-light-static.svg @@ -1 +1 @@ -├──────────────────────────────────────────┤╭──────────────────────────────────────────╮SelectwithgroupsSelectFruitFruitAppleBanana───────────────────────────────Rhubarb(outofseason)↑/↓move·accept·esccancel╰──────────────────────────────────────────╯ \ No newline at end of file +├──────────────────────────────────────────┤╭──────────────────────────────────────────╮SelectwithgroupsSelectFruitFruitAppleBanana───────────────────────────────Cherry(outofseason)↑/↓move·accept·esccancel╰──────────────────────────────────────────╯ \ No newline at end of file diff --git a/docs/assets/widget-table-dark-animated-ascii-no-ansi.svg b/docs/assets/widget-table-dark-animated-ascii-no-ansi.svg index 04bdc807..b7ca3947 100644 --- a/docs/assets/widget-table-dark-animated-ascii-no-ansi.svg +++ b/docs/assets/widget-table-dark-animated-ascii-no-ansi.svg @@ -1 +1 @@ -+--------------------------------------------------------------------------+|||^/vmove*<select*escback*qquit*?help||Tablewidget>Stock||Basketcontents||Everythingpickedsofar:||+------------+--------+----------+|||Fruit|Colour|Instock||||Apple|Red|12||||Pear|Green|5||||Blackberry|Purple|120|||Tablewidget||>Stock>||[Submit][Cancel]| \ No newline at end of file ++--------------------------------------------------------------------------+|||^/vmove*<select*escback*qquit*?help||Tablewidget>Stock||Basketcontents||Everythingpickedsofar:||+-------+--------+----------+|||Fruit|Colour|Instock||||Apple|Red|12||||Pear|Green|5||||Plum|Purple|120|||Tablewidget||>Stock>||[Submit][Cancel]| \ No newline at end of file diff --git a/docs/assets/widget-table-dark-animated-ascii.svg b/docs/assets/widget-table-dark-animated-ascii.svg index b7bf3e94..b8973bd4 100644 --- a/docs/assets/widget-table-dark-animated-ascii.svg +++ b/docs/assets/widget-table-dark-animated-ascii.svg @@ -1 +1 @@ -+--------------------------------------------------------------------------+|||^/vmove*<select*escback*qquit*?help||Tablewidget>Stock||Basketcontents||Everythingpickedsofar:||+------------+--------+----------+|||Fruit|Colour|Instock||||Apple|Red|12||||Pear|Green|5||||Blackberry|Purple|120|||Tablewidget||>Stock>||[Submit][Cancel]| \ No newline at end of file ++--------------------------------------------------------------------------+|||^/vmove*<select*escback*qquit*?help||Tablewidget>Stock||Basketcontents||Everythingpickedsofar:||+-------+--------+----------+|||Fruit|Colour|Instock||||Apple|Red|12||||Pear|Green|5||||Plum|Purple|120|||Tablewidget||>Stock>||[Submit][Cancel]| \ No newline at end of file diff --git a/docs/assets/widget-table-dark-animated-no-ansi.svg b/docs/assets/widget-table-dark-animated-no-ansi.svg index 6d45c1f6..72114cff 100644 --- a/docs/assets/widget-table-dark-animated-no-ansi.svg +++ b/docs/assets/widget-table-dark-animated-no-ansi.svg @@ -1 +1 @@ -╭──────────────────────────────────────────────────────────────────────────╮├──────────────────────────────────────────────────────────────────────────┤↑/↓move·select·escback·qquit·?help╰──────────────────────────────────────────────────────────────────────────╯TablewidgetStockBasketcontentsEverythingpickedsofar:╭────────────┬────────┬──────────╮FruitColourInstock├────────────┼────────┼──────────┤AppleRed12PearGreen5BlackberryPurple120╰────────────┴────────┴──────────╯TablewidgetStock[Submit][Cancel] \ No newline at end of file +╭──────────────────────────────────────────────────────────────────────────╮├──────────────────────────────────────────────────────────────────────────┤↑/↓move·select·escback·qquit·?help╰──────────────────────────────────────────────────────────────────────────╯TablewidgetStockBasketcontentsEverythingpickedsofar:╭───────┬────────┬──────────╮FruitColourInstock├───────┼────────┼──────────┤AppleRed12PearGreen5PlumPurple120╰───────┴────────┴──────────╯TablewidgetStock[Submit][Cancel] \ No newline at end of file diff --git a/docs/assets/widget-table-dark-animated.svg b/docs/assets/widget-table-dark-animated.svg index 38fcc5f5..0134c70b 100644 --- a/docs/assets/widget-table-dark-animated.svg +++ b/docs/assets/widget-table-dark-animated.svg @@ -1 +1 @@ -╭──────────────────────────────────────────────────────────────────────────╮├──────────────────────────────────────────────────────────────────────────┤↑/↓move·select·escback·qquit·?help╰──────────────────────────────────────────────────────────────────────────╯TablewidgetStockBasketcontentsEverythingpickedsofar:╭────────────┬────────┬──────────╮FruitColourInstock├────────────┼────────┼──────────┤AppleRed12PearGreen5BlackberryPurple120╰────────────┴────────┴──────────╯TablewidgetStock[Submit][Cancel] \ No newline at end of file +╭──────────────────────────────────────────────────────────────────────────╮├──────────────────────────────────────────────────────────────────────────┤↑/↓move·select·escback·qquit·?help╰──────────────────────────────────────────────────────────────────────────╯TablewidgetStockBasketcontentsEverythingpickedsofar:╭───────┬────────┬──────────╮FruitColourInstock├───────┼────────┼──────────┤AppleRed12PearGreen5PlumPurple120╰───────┴────────┴──────────╯TablewidgetStock[Submit][Cancel] \ No newline at end of file diff --git a/docs/assets/widget-table-dark-static-ascii-no-ansi.svg b/docs/assets/widget-table-dark-static-ascii-no-ansi.svg index b7b329c0..fdda2cf3 100644 --- a/docs/assets/widget-table-dark-static-ascii-no-ansi.svg +++ b/docs/assets/widget-table-dark-static-ascii-no-ansi.svg @@ -1 +1 @@ -+--------------------------------------------------------------------------+|||+------------+--------+----------+||Tablewidget>Stock||Basketcontents||Everythingpickedsofar:|||Fruit|Colour|Instock||||Apple|Red|12||||Pear|Green|5||||Blackberry|Purple|120|||^/vmove*<select*escback*qquit*?help| \ No newline at end of file ++--------------------------------------------------------------------------+|||+-------+--------+----------+||Tablewidget>Stock||Basketcontents||Everythingpickedsofar:|||Fruit|Colour|Instock||||Apple|Red|12||||Pear|Green|5||||Plum|Purple|120|||^/vmove*<select*escback*qquit*?help| \ No newline at end of file diff --git a/docs/assets/widget-table-dark-static-ascii.svg b/docs/assets/widget-table-dark-static-ascii.svg index fcf3fa41..9735b91c 100644 --- a/docs/assets/widget-table-dark-static-ascii.svg +++ b/docs/assets/widget-table-dark-static-ascii.svg @@ -1 +1 @@ -+--------------------------------------------------------------------------+|||+------------+--------+----------+||Tablewidget>Stock||Basketcontents||Everythingpickedsofar:|||Fruit|Colour|Instock||||Apple|Red|12||||Pear|Green|5||||Blackberry|Purple|120|||^/vmove*<select*escback*qquit*?help| \ No newline at end of file ++--------------------------------------------------------------------------+|||+-------+--------+----------+||Tablewidget>Stock||Basketcontents||Everythingpickedsofar:|||Fruit|Colour|Instock||||Apple|Red|12||||Pear|Green|5||||Plum|Purple|120|||^/vmove*<select*escback*qquit*?help| \ No newline at end of file diff --git a/docs/assets/widget-table-dark-static-no-ansi.svg b/docs/assets/widget-table-dark-static-no-ansi.svg index 772ee6d4..e4454614 100644 --- a/docs/assets/widget-table-dark-static-no-ansi.svg +++ b/docs/assets/widget-table-dark-static-no-ansi.svg @@ -1 +1 @@ -├──────────────────────────────────────────────────────────────────────────┤╭──────────────────────────────────────────────────────────────────────────╮TablewidgetStockBasketcontentsEverythingpickedsofar:╭────────────┬────────┬──────────╮FruitColourInstock├────────────┼────────┼──────────┤AppleRed12PearGreen5BlackberryPurple120╰────────────┴────────┴──────────╯↑/↓move·select·escback·qquit·?help╰──────────────────────────────────────────────────────────────────────────╯ \ No newline at end of file +├──────────────────────────────────────────────────────────────────────────┤╭──────────────────────────────────────────────────────────────────────────╮TablewidgetStockBasketcontentsEverythingpickedsofar:╭───────┬────────┬──────────╮FruitColourInstock├───────┼────────┼──────────┤AppleRed12PearGreen5PlumPurple120╰───────┴────────┴──────────╯↑/↓move·select·escback·qquit·?help╰──────────────────────────────────────────────────────────────────────────╯ \ No newline at end of file diff --git a/docs/assets/widget-table-dark-static.svg b/docs/assets/widget-table-dark-static.svg index 1a7d1fc1..d78480df 100644 --- a/docs/assets/widget-table-dark-static.svg +++ b/docs/assets/widget-table-dark-static.svg @@ -1 +1 @@ -├──────────────────────────────────────────────────────────────────────────┤╭──────────────────────────────────────────────────────────────────────────╮TablewidgetStockBasketcontentsEverythingpickedsofar:╭────────────┬────────┬──────────╮FruitColourInstock├────────────┼────────┼──────────┤AppleRed12PearGreen5BlackberryPurple120╰────────────┴────────┴──────────╯↑/↓move·select·escback·qquit·?help╰──────────────────────────────────────────────────────────────────────────╯ \ No newline at end of file +├──────────────────────────────────────────────────────────────────────────┤╭──────────────────────────────────────────────────────────────────────────╮TablewidgetStockBasketcontentsEverythingpickedsofar:╭───────┬────────┬──────────╮FruitColourInstock├───────┼────────┼──────────┤AppleRed12PearGreen5PlumPurple120╰───────┴────────┴──────────╯↑/↓move·select·escback·qquit·?help╰──────────────────────────────────────────────────────────────────────────╯ \ No newline at end of file diff --git a/docs/assets/widget-table-light-animated-ascii-no-ansi.svg b/docs/assets/widget-table-light-animated-ascii-no-ansi.svg index c1c190b1..005e8b97 100644 --- a/docs/assets/widget-table-light-animated-ascii-no-ansi.svg +++ b/docs/assets/widget-table-light-animated-ascii-no-ansi.svg @@ -1 +1 @@ -+--------------------------------------------------------------------------+|||^/vmove*<select*escback*qquit*?help||Tablewidget>Stock||Basketcontents||Everythingpickedsofar:||+------------+--------+----------+|||Fruit|Colour|Instock||||Apple|Red|12||||Pear|Green|5||||Blackberry|Purple|120|||Tablewidget||>Stock>||[Submit][Cancel]| \ No newline at end of file ++--------------------------------------------------------------------------+|||^/vmove*<select*escback*qquit*?help||Tablewidget>Stock||Basketcontents||Everythingpickedsofar:||+-------+--------+----------+|||Fruit|Colour|Instock||||Apple|Red|12||||Pear|Green|5||||Plum|Purple|120|||Tablewidget||>Stock>||[Submit][Cancel]| \ No newline at end of file diff --git a/docs/assets/widget-table-light-animated-ascii.svg b/docs/assets/widget-table-light-animated-ascii.svg index aebc6a6a..96056dde 100644 --- a/docs/assets/widget-table-light-animated-ascii.svg +++ b/docs/assets/widget-table-light-animated-ascii.svg @@ -1 +1 @@ -+--------------------------------------------------------------------------+|||^/vmove*<select*escback*qquit*?help||Tablewidget>Stock||Basketcontents||Everythingpickedsofar:||+------------+--------+----------+|||Fruit|Colour|Instock||||Apple|Red|12||||Pear|Green|5||||Blackberry|Purple|120|||Tablewidget||>Stock>||[Submit][Cancel]| \ No newline at end of file ++--------------------------------------------------------------------------+|||^/vmove*<select*escback*qquit*?help||Tablewidget>Stock||Basketcontents||Everythingpickedsofar:||+-------+--------+----------+|||Fruit|Colour|Instock||||Apple|Red|12||||Pear|Green|5||||Plum|Purple|120|||Tablewidget||>Stock>||[Submit][Cancel]| \ No newline at end of file diff --git a/docs/assets/widget-table-light-animated-no-ansi.svg b/docs/assets/widget-table-light-animated-no-ansi.svg index c67932c1..42d3776f 100644 --- a/docs/assets/widget-table-light-animated-no-ansi.svg +++ b/docs/assets/widget-table-light-animated-no-ansi.svg @@ -1 +1 @@ -╭──────────────────────────────────────────────────────────────────────────╮├──────────────────────────────────────────────────────────────────────────┤↑/↓move·select·escback·qquit·?help╰──────────────────────────────────────────────────────────────────────────╯TablewidgetStockBasketcontentsEverythingpickedsofar:╭────────────┬────────┬──────────╮FruitColourInstock├────────────┼────────┼──────────┤AppleRed12PearGreen5BlackberryPurple120╰────────────┴────────┴──────────╯TablewidgetStock[Submit][Cancel] \ No newline at end of file +╭──────────────────────────────────────────────────────────────────────────╮├──────────────────────────────────────────────────────────────────────────┤↑/↓move·select·escback·qquit·?help╰──────────────────────────────────────────────────────────────────────────╯TablewidgetStockBasketcontentsEverythingpickedsofar:╭───────┬────────┬──────────╮FruitColourInstock├───────┼────────┼──────────┤AppleRed12PearGreen5PlumPurple120╰───────┴────────┴──────────╯TablewidgetStock[Submit][Cancel] \ No newline at end of file diff --git a/docs/assets/widget-table-light-animated.svg b/docs/assets/widget-table-light-animated.svg index 4da66f5f..e23a844d 100644 --- a/docs/assets/widget-table-light-animated.svg +++ b/docs/assets/widget-table-light-animated.svg @@ -1 +1 @@ -╭──────────────────────────────────────────────────────────────────────────╮├──────────────────────────────────────────────────────────────────────────┤↑/↓move·select·escback·qquit·?help╰──────────────────────────────────────────────────────────────────────────╯TablewidgetStockBasketcontentsEverythingpickedsofar:╭────────────┬────────┬──────────╮FruitColourInstock├────────────┼────────┼──────────┤AppleRed12PearGreen5BlackberryPurple120╰────────────┴────────┴──────────╯TablewidgetStock[Submit][Cancel] \ No newline at end of file +╭──────────────────────────────────────────────────────────────────────────╮├──────────────────────────────────────────────────────────────────────────┤↑/↓move·select·escback·qquit·?help╰──────────────────────────────────────────────────────────────────────────╯TablewidgetStockBasketcontentsEverythingpickedsofar:╭───────┬────────┬──────────╮FruitColourInstock├───────┼────────┼──────────┤AppleRed12PearGreen5PlumPurple120╰───────┴────────┴──────────╯TablewidgetStock[Submit][Cancel] \ No newline at end of file diff --git a/docs/assets/widget-table-light-static-ascii-no-ansi.svg b/docs/assets/widget-table-light-static-ascii-no-ansi.svg index b74f5be8..ccf37f76 100644 --- a/docs/assets/widget-table-light-static-ascii-no-ansi.svg +++ b/docs/assets/widget-table-light-static-ascii-no-ansi.svg @@ -1 +1 @@ -+--------------------------------------------------------------------------+|||+------------+--------+----------+||Tablewidget>Stock||Basketcontents||Everythingpickedsofar:|||Fruit|Colour|Instock||||Apple|Red|12||||Pear|Green|5||||Blackberry|Purple|120|||^/vmove*<select*escback*qquit*?help| \ No newline at end of file ++--------------------------------------------------------------------------+|||+-------+--------+----------+||Tablewidget>Stock||Basketcontents||Everythingpickedsofar:|||Fruit|Colour|Instock||||Apple|Red|12||||Pear|Green|5||||Plum|Purple|120|||^/vmove*<select*escback*qquit*?help| \ No newline at end of file diff --git a/docs/assets/widget-table-light-static-ascii.svg b/docs/assets/widget-table-light-static-ascii.svg index d3af670b..a6776726 100644 --- a/docs/assets/widget-table-light-static-ascii.svg +++ b/docs/assets/widget-table-light-static-ascii.svg @@ -1 +1 @@ -+--------------------------------------------------------------------------+|||+------------+--------+----------+||Tablewidget>Stock||Basketcontents||Everythingpickedsofar:|||Fruit|Colour|Instock||||Apple|Red|12||||Pear|Green|5||||Blackberry|Purple|120|||^/vmove*<select*escback*qquit*?help| \ No newline at end of file ++--------------------------------------------------------------------------+|||+-------+--------+----------+||Tablewidget>Stock||Basketcontents||Everythingpickedsofar:|||Fruit|Colour|Instock||||Apple|Red|12||||Pear|Green|5||||Plum|Purple|120|||^/vmove*<select*escback*qquit*?help| \ No newline at end of file diff --git a/docs/assets/widget-table-light-static-no-ansi.svg b/docs/assets/widget-table-light-static-no-ansi.svg index 751d762b..7585ec9b 100644 --- a/docs/assets/widget-table-light-static-no-ansi.svg +++ b/docs/assets/widget-table-light-static-no-ansi.svg @@ -1 +1 @@ -├──────────────────────────────────────────────────────────────────────────┤╭──────────────────────────────────────────────────────────────────────────╮TablewidgetStockBasketcontentsEverythingpickedsofar:╭────────────┬────────┬──────────╮FruitColourInstock├────────────┼────────┼──────────┤AppleRed12PearGreen5BlackberryPurple120╰────────────┴────────┴──────────╯↑/↓move·select·escback·qquit·?help╰──────────────────────────────────────────────────────────────────────────╯ \ No newline at end of file +├──────────────────────────────────────────────────────────────────────────┤╭──────────────────────────────────────────────────────────────────────────╮TablewidgetStockBasketcontentsEverythingpickedsofar:╭───────┬────────┬──────────╮FruitColourInstock├───────┼────────┼──────────┤AppleRed12PearGreen5PlumPurple120╰───────┴────────┴──────────╯↑/↓move·select·escback·qquit·?help╰──────────────────────────────────────────────────────────────────────────╯ \ No newline at end of file diff --git a/docs/assets/widget-table-light-static.svg b/docs/assets/widget-table-light-static.svg index d74ed079..7dd07c9c 100644 --- a/docs/assets/widget-table-light-static.svg +++ b/docs/assets/widget-table-light-static.svg @@ -1 +1 @@ -├──────────────────────────────────────────────────────────────────────────┤╭──────────────────────────────────────────────────────────────────────────╮TablewidgetStockBasketcontentsEverythingpickedsofar:╭────────────┬────────┬──────────╮FruitColourInstock├────────────┼────────┼──────────┤AppleRed12PearGreen5BlackberryPurple120╰────────────┴────────┴──────────╯↑/↓move·select·escback·qquit·?help╰──────────────────────────────────────────────────────────────────────────╯ \ No newline at end of file +├──────────────────────────────────────────────────────────────────────────┤╭──────────────────────────────────────────────────────────────────────────╮TablewidgetStockBasketcontentsEverythingpickedsofar:╭───────┬────────┬──────────╮FruitColourInstock├───────┼────────┼──────────┤AppleRed12PearGreen5PlumPurple120╰───────┴────────┴──────────╯↑/↓move·select·escback·qquit·?help╰──────────────────────────────────────────────────────────────────────────╯ \ No newline at end of file diff --git a/docs/content/widgets/option-groups.mdx b/docs/content/widgets/option-groups.mdx index 73c6d881..feec605e 100644 --- a/docs/content/widgets/option-groups.mdx +++ b/docs/content/widgets/option-groups.mdx @@ -19,7 +19,7 @@ $p->select('item', 'Item') ->separator() // A non-selectable divider row. ->heading('Vegetable') ->option('carrot', 'Carrot') - ->option('rhubarb', 'Rhubarb', disabled: TRUE, disabled_reason: 'out of season'); + ->option('cherry', 'Cherry', disabled: TRUE, disabled_reason: 'out of season'); ``` Runnable scripts: [`playground/02-widgets-select-groups.php`](https://github.com/drevops/tui/blob/main/playground/02-widgets-select-groups.php) and [`select-multiple-groups.php`](https://github.com/drevops/tui/blob/main/playground/02-widgets-select-multiple-groups.php). diff --git a/docs/content/widgets/table.mdx b/docs/content/widgets/table.mdx index 8921498f..9d9c2634 100644 --- a/docs/content/widgets/table.mdx +++ b/docs/content/widgets/table.mdx @@ -21,7 +21,7 @@ $p->note('stock', 'Basket contents') ->table(['Fruit', 'Colour', 'In stock'], [ ['Apple', 'Red', '12'], ['Pear', 'Green', '5'], - ['Blackberry', 'Purple', '120'], + ['Plum', 'Purple', '120'], ]); ``` diff --git a/docs/util/render-widget-svgs.php b/docs/util/render-widget-svgs.php index 53263ffe..cff53633 100644 --- a/docs/util/render-widget-svgs.php +++ b/docs/util/render-widget-svgs.php @@ -238,7 +238,7 @@ function widgetSpecs(string $tree): array { $p->note('stock', 'Basket contents')->description('Everything picked so far:')->table(['Fruit', 'Colour', 'In stock'], [ ['Apple', 'Red', '12'], ['Pear', 'Green', '5'], - ['Blackberry', 'Purple', '120'], + ['Plum', 'Purple', '120'], ]); }), 'keys' => [$enter], diff --git a/docs/util/update-assets.php b/docs/util/update-assets.php index ed95553e..7707ebb1 100644 --- a/docs/util/update-assets.php +++ b/docs/util/update-assets.php @@ -1282,7 +1282,7 @@ function getJobs(string $project_dir): array { // static frame is anchored to an option only visible once the list is open // (the hub shows the form title too, so gating on it would capture the hub). $group_demos = [ - 'select-groups' => ['gate' => 'Select with groups', 'needle' => 'Rhubarb', 'rows' => 18], + 'select-groups' => ['gate' => 'Select with groups', 'needle' => 'Cherry', 'rows' => 18], 'select-multiple-groups' => ['gate' => 'MultiSelect with groups', 'needle' => 'Leek', 'rows' => 21], ]; diff --git a/playground/02-widgets-select-groups.php b/playground/02-widgets-select-groups.php index 1f33a5d6..0160002d 100644 --- a/playground/02-widgets-select-groups.php +++ b/playground/02-widgets-select-groups.php @@ -30,7 +30,7 @@ ->option('apple', 'Apple') ->option('banana', 'Banana') ->separator() - ->option('rhubarb', 'Rhubarb', disabled: TRUE, disabled_reason: 'out of season'); + ->option('cherry', 'Cherry', disabled: TRUE, disabled_reason: 'out of season'); }); try { diff --git a/playground/02-widgets-table.php b/playground/02-widgets-table.php index 2107ab15..2d0328d1 100644 --- a/playground/02-widgets-table.php +++ b/playground/02-widgets-table.php @@ -28,7 +28,7 @@ $rows = [ ['Apple', 'Red', '12'], ['Pear', 'Green', '5'], - ['Blackberry', 'Purple', '120'], + ['Plum', 'Purple', '120'], ]; $form = Form::create('Table') From 3e1ca296922365bbf464ffc4d76bb7b5022e858b Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Wed, 29 Jul 2026 16:26:14 +1000 Subject: [PATCH 13/14] Sized a grid column to include its multi-line marker. A grid cell renders one line plus a there-is-more marker, but the column was measured from the raw value lines, so a value whose first line is its widest was sized short and clipped the marker. Rendering and measuring now share one preview method, which cannot drift. The gap predates the ASCII fallback, which only widened it from one column to three. --- src/Theme/DefaultTheme.php | 32 ++++++++++++++++---- tests/phpunit/Unit/Theme/ThemeLayoutTest.php | 20 ++++++++++++ 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/src/Theme/DefaultTheme.php b/src/Theme/DefaultTheme.php index cc5b3377..d78ac237 100644 --- a/src/Theme/DefaultTheme.php +++ b/src/Theme/DefaultTheme.php @@ -1580,11 +1580,7 @@ protected function renderColumnBlock(Panel $panel, Answers $answers, bool $selec continue; } - // A grid cell is one physical row, so a multi-line value previews as - // its first line - an embedded newline would desync the column zip. - $value_lines = explode("\n", $this->normalizeLines($this->renderFieldValue($field, $answers->value($field->id)))); - $more = $this->unicode ? '…' : '...'; - $value = $value_lines[0] . (count($value_lines) > 1 ? $more : ''); + $value = $this->columnValuePreview($field, $answers); $lines[] = $indent . ' ' . $this->description(Translator::t($field->label), $selected) . ' ' . $this->value($value, $selected); } @@ -1595,6 +1591,30 @@ protected function renderColumnBlock(Panel $panel, Answers $answers, bool $selec return $lines; } + /** + * A field's value as one grid cell: first line, marked when there is more. + * + * A grid cell is one physical row, so a multi-line value previews as its + * first line - an embedded newline would desync the column zip - followed by + * a marker so the cell does not read as the whole value. Rendering and + * measuring both route through here, so a column can never be sized without + * the room its marker needs. + * + * @param \DrevOps\Tui\Model\Field $field + * The field to preview. + * @param \DrevOps\Tui\Answers\Answers $answers + * The collected answers. + * + * @return string + * The previewed value. + */ + protected function columnValuePreview(Field $field, Answers $answers): string { + $value_lines = explode("\n", $this->normalizeLines($this->renderFieldValue($field, $answers->value($field->id)))); + $more = $this->unicode ? '…' : '...'; + + return $value_lines[0] . (count($value_lines) > 1 ? $more : ''); + } + /** * Render a field row, one entry per physical line. * @@ -2178,7 +2198,7 @@ protected function measureColumnBlock(Panel $panel, Answers $answers): int { continue; } - $width = max($width, $indent + 4 + Markup::width(Translator::t($field->label), FALSE, $this->color) + $this->measureValueWidth($field, $answers)); + $width = max($width, $indent + 4 + Markup::width(Translator::t($field->label), FALSE, $this->color) + Strings::length($this->columnValuePreview($field, $answers))); } foreach ($panel->panels as $subpanel) { diff --git a/tests/phpunit/Unit/Theme/ThemeLayoutTest.php b/tests/phpunit/Unit/Theme/ThemeLayoutTest.php index 8c97e535..da4cd4f3 100644 --- a/tests/phpunit/Unit/Theme/ThemeLayoutTest.php +++ b/tests/phpunit/Unit/Theme/ThemeLayoutTest.php @@ -199,6 +199,26 @@ public function testMeasureContentWidthCoversTheGrid(): void { $this->assertSame(56, (new DefaultTheme(40, ['color' => FALSE, 'border' => Border::None, 'spacing' => Spacing::Compact]))->measureContentWidth($form, $answers)); } + public function testMeasureReservesRoomForTheMultiLineMarker(): void { + $form = Form::create('T') + ->buttons(FALSE) + ->layout(2) + ->panel('a', 'A', fn(PanelBuilder $p): FieldBuilder => $p->textarea('notes', 'A')->default("Crisp and sweet\nshort")) + ->panel('b', 'B', fn(PanelBuilder $p): FieldBuilder => $p->text('two', 'B')) + ->build(); + $answers = new Answers(['notes' => "Crisp and sweet\nshort"], []); + $options = ['color' => FALSE, 'border' => Border::None, 'spacing' => Spacing::Compact]; + + $unicode = (new DefaultTheme(40, $options + ['unicode' => TRUE]))->measureContentWidth($form, $answers); + $ascii = (new DefaultTheme(40, $options + ['unicode' => FALSE]))->measureContentWidth($form, $answers); + + // The first line is the widest, so a grid cell is that line plus its + // marker. Measuring the raw lines alone would size both modes the same and + // clip the marker off the end of the cell. + $this->assertSame(44, $unicode); + $this->assertSame(48, $ascii); + } + public function testLayoutPreviewsNoteAsItsTitle(): void { $theme = new DefaultTheme(60, ['color' => FALSE, 'unicode' => FALSE]); $panel = new Panel('p', 'P', '', [], [ From 06178c7374dd6a06532f55c3d00758a2339e7cd3 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Wed, 29 Jul 2026 16:33:40 +1000 Subject: [PATCH 14/14] Measured a field value in columns rather than bytes. A progress, rating or loading value is painted, so measuring its raw string counted every escape sequence as a column: a rating measured 30 wide where it draws 9. Both the grid cell and the label/value row now measure visible width. The row path carried the same fault before the grid cell shared its preview. --- src/Theme/DefaultTheme.php | 4 ++-- tests/phpunit/Unit/Theme/ThemeLayoutTest.php | 25 ++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/Theme/DefaultTheme.php b/src/Theme/DefaultTheme.php index d78ac237..14bcefda 100644 --- a/src/Theme/DefaultTheme.php +++ b/src/Theme/DefaultTheme.php @@ -2198,7 +2198,7 @@ protected function measureColumnBlock(Panel $panel, Answers $answers): int { continue; } - $width = max($width, $indent + 4 + Markup::width(Translator::t($field->label), FALSE, $this->color) + Strings::length($this->columnValuePreview($field, $answers))); + $width = max($width, $indent + 4 + Markup::width(Translator::t($field->label), FALSE, $this->color) + Ansi::width($this->columnValuePreview($field, $answers))); } foreach ($panel->panels as $subpanel) { @@ -2227,7 +2227,7 @@ protected function measureValueWidth(Field $field, Answers $answers): int { $width = 0; foreach (explode("\n", $this->normalizeLines($this->renderFieldValue($field, $answers->value($field->id)))) as $line) { - $width = max($width, Strings::length($line)); + $width = max($width, Ansi::width($line)); } return $width; diff --git a/tests/phpunit/Unit/Theme/ThemeLayoutTest.php b/tests/phpunit/Unit/Theme/ThemeLayoutTest.php index da4cd4f3..10f6f160 100644 --- a/tests/phpunit/Unit/Theme/ThemeLayoutTest.php +++ b/tests/phpunit/Unit/Theme/ThemeLayoutTest.php @@ -219,6 +219,31 @@ public function testMeasureReservesRoomForTheMultiLineMarker(): void { $this->assertSame(48, $ascii); } + public function testMeasureCountsColumnsNotEscapeSequences(): void { + $grid = Form::create('T') + ->buttons(FALSE) + ->layout(2) + ->panel('a', 'A', fn(PanelBuilder $p): FieldBuilder => $p->rating('grade', 'A')) + ->panel('b', 'B', fn(PanelBuilder $p): FieldBuilder => $p->text('two', 'B')) + ->build(); + $linear = Form::create('T') + ->buttons(FALSE) + ->panel('a', 'A', fn(PanelBuilder $p): FieldBuilder => $p->rating('grade', 'A')) + ->build(); + $answers = new Answers(['grade' => 3], []); + $options = ['border' => Border::None, 'spacing' => Spacing::Compact]; + + // A rating renders painted, so measuring its raw string would count the + // escape sequences as columns and oversize the frame. Colour is invisible, + // so it must not move the measurement either in a grid cell or in a row. + foreach ([$grid, $linear] as $form) { + $plain = (new DefaultTheme(40, $options + ['color' => FALSE]))->measureContentWidth($form, $answers); + $painted = (new DefaultTheme(40, $options + ['color' => TRUE]))->measureContentWidth($form, $answers); + + $this->assertSame($plain, $painted); + } + } + public function testLayoutPreviewsNoteAsItsTitle(): void { $theme = new DefaultTheme(60, ['color' => FALSE, 'unicode' => FALSE]); $panel = new Panel('p', 'P', '', [], [