fix(console): improve TUI rendering - fix menu bar display and category formatting

- Fix TuiRenderer rendering: correct line positioning for categories
- Fix category item formatting: remove tabs, ensure consistent spacing
- Improve clearContentArea: preserve menu bar (lines 2-3) when clearing content
- Add ConsoleContext: mutable context container for readonly ConsoleOutput
- Add context awareness to ConsoleOutput: setContext/getContext/isInTuiContext
- Auto-detect TUI context in InteractivePrompter: automatically set LayoutAreas
- Set TUI context in TuiFactory and TuiCommandExecutor
- Add tests for TuiRenderer: menu bar preservation, category formatting

This fixes rendering issues where:
- Menu bar was not displayed or overwritten
- Category items had tab/space misalignment
- Content area clearing overwrote the menu bar
This commit is contained in:
2025-11-10 02:42:46 +01:00
parent 2d53270056
commit 6bc78f5540
7 changed files with 213 additions and 9 deletions

View File

@@ -58,6 +58,11 @@ final readonly class TuiCommandExecutor
*/
public function executeCommand(string $commandName): void
{
// Set TUI context so commands can use LayoutAreas automatically
if ($this->output instanceof \App\Framework\Console\ConsoleOutput) {
$this->output->setContext('TUI');
}
$this->commandHistory->addToHistory($commandName);
$this->exitInteractiveMode();
@@ -93,6 +98,11 @@ final readonly class TuiCommandExecutor
*/
public function executeCommandWithParameters(string $commandName, array $parameters): void
{
// Set TUI context so commands can use LayoutAreas automatically
if ($this->output instanceof \App\Framework\Console\ConsoleOutput) {
$this->output->setContext('TUI');
}
$this->commandHistory->addToHistory($commandName);
$this->exitInteractiveMode();

View File

@@ -12,6 +12,7 @@ use App\Framework\Console\CommandHistory;
use App\Framework\Console\CommandRegistry;
use App\Framework\Console\CommandValidator;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleOutput;
use App\Framework\Console\ConsoleOutputInterface;
use App\Framework\Console\ExitCode;
use App\Framework\Console\Help\ConsoleHelpRenderer;
@@ -63,6 +64,11 @@ final readonly class TuiFactory
// Create CommandHistory
$commandHistory = new CommandHistory();
// Set TUI context on output
if ($this->output instanceof ConsoleOutput) {
$this->output->setContext('TUI');
}
// Create new services
$groupRegistry = new CommandGroupRegistry($discoveryRegistry);
$workflowExecutor = new SimpleWorkflowExecutor($this->commandRegistry, $groupRegistry, $this->output);

View File

@@ -118,15 +118,17 @@ final class TuiRenderer
*/
private function clearContentArea(): void
{
// First, clear line 1 explicitly (spacing line)
$this->output->write(CursorControlCode::POSITION->format(1, 1));
$terminalSize = TerminalSize::detect();
$this->output->write(str_repeat(' ', $terminalSize->width));
// Clear line 1 (spacing line) explicitly
$this->output->write(CursorControlCode::POSITION->format(1, 1));
$this->output->write(ScreenControlCode::CLEAR_LINE->format());
// Position cursor at line 4 (start of content area)
$this->output->write(CursorControlCode::POSITION->format(4, 1));
// Clear everything from cursor downwards (preserves lines 1-3: spacing + menu bar)
// Clear everything from cursor downwards (preserves lines 2-3: menu bar)
// Note: CLEAR_BELOW clears from current cursor position to end of screen
$this->output->write(ScreenControlCode::CLEAR_BELOW->format());
// Position cursor back at line 4 for content rendering
@@ -149,14 +151,22 @@ final class TuiRenderer
*/
private function renderCategories(TuiState $state): void
{
// Ensure we're at the correct starting line (4) for content
$currentLine = 4;
$this->output->write(CursorControlCode::POSITION->format($currentLine, 1));
$this->output->writeLine('📂 Select Category:', ConsoleColor::BRIGHT_YELLOW);
$this->output->writeLine('');
$currentLine++;
$categories = $state->getCategories();
foreach ($categories as $index => $category) {
$isSelected = $index === $state->getSelectedCategory();
$isHovered = $state->isContentItemHovered('category', $index);
// Position cursor at correct line before rendering
$this->output->write(CursorControlCode::POSITION->format($currentLine, 1));
$this->renderCategoryItem($category, $isSelected, $isHovered);
$currentLine++;
}
$this->output->newLine();
@@ -184,10 +194,22 @@ final class TuiRenderer
// Determine color: selected > hovered > normal
$color = $isSelected ? ConsoleColor::BRIGHT_WHITE : ($isHovered ? ConsoleColor::BRIGHT_CYAN : ConsoleColor::WHITE);
$this->output->writeLine(
"{$prefix}{$icon} {$name} ({$count} commands)",
$color
);
// Clear the entire line first to remove any leftover characters
$this->output->write(ScreenControlCode::CLEAR_LINE->format());
$this->output->write("\r");
// Build the text without any tabs or extra spaces
$text = "{$prefix}{$icon} {$name} ({$count} commands)";
// Write the category item with color
if ($color !== null) {
$this->output->write($text, $color);
} else {
$this->output->write($text);
}
// Move to next line explicitly
$this->output->write("\n");
}
/**

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console;
/**
* Mutable context container for ConsoleOutput.
* Allows readonly ConsoleOutput to track execution context.
*/
final class ConsoleContext
{
private ?string $context = null;
public function set(string $context): void
{
$this->context = $context;
}
public function get(): ?string
{
return $this->context;
}
public function is(string $context): bool
{
return $this->context === $context;
}
public function clear(): void
{
$this->context = null;
}
}

View File

@@ -42,6 +42,8 @@ final readonly class ConsoleOutput implements ConsoleOutputInterface
private AnimationManager $animationManager;
private ConsoleContext $context;
public function __construct()
{
$this->capabilities = new TerminalCapabilities();
@@ -51,12 +53,37 @@ final readonly class ConsoleOutput implements ConsoleOutputInterface
$this->titleManager = new WindowTitleManager($this->ansiGenerator, $this->capabilities);
$this->linkFormatter = new LinkFormatter($this->ansiGenerator, $this->capabilities);
$this->prompter = new InteractivePrompter($this, $this->textWriter);
$this->context = new ConsoleContext();
$this->cursor = new Cursor($this);
$this->display = new Display($this);
$this->screen = new ScreenManager($this);
}
/**
* Set the execution context (TUI, Dialog, Standalone)
*/
public function setContext(string $context): void
{
$this->context->set($context);
}
/**
* Get the current execution context
*/
public function getContext(): ?string
{
return $this->context->get();
}
/**
* Check if running in TUI context
*/
public function isInTuiContext(): bool
{
return $this->context->is('TUI');
}
/**
* Schreibt Text mit optionalem Stil.
*/

View File

@@ -5,6 +5,8 @@ declare(strict_types=1);
namespace App\Framework\Console;
use App\Framework\Console\Components\InteractiveMenu;
use App\Framework\Console\Layout\TerminalSize;
use App\Framework\Console\Screen\LayoutAreas;
use App\Framework\Console\ValueObjects\ChoiceOptions;
use App\Framework\Console\ValueObjects\MenuOptions;
@@ -91,6 +93,12 @@ final readonly class InteractivePrompter
public function choiceFromOptions(string $question, MenuOptions $options, mixed $default = null): mixed
{
$menu = new InteractiveMenu($this->output);
// Automatically set LayoutAreas if in TUI context
if ($this->isInTuiContext()) {
$menu->setLayoutAreas(LayoutAreas::forTui());
}
$menu->setTitle($question);
foreach ($options as $option) {
@@ -119,6 +127,12 @@ final readonly class InteractivePrompter
public function menuFromOptions(string $title, MenuOptions $options): mixed
{
$menu = new InteractiveMenu($this->output);
// Automatically set LayoutAreas if in TUI context
if ($this->isInTuiContext()) {
$menu->setLayoutAreas(LayoutAreas::forTui());
}
$menu->setTitle($title);
foreach ($options as $option) {
@@ -171,5 +185,18 @@ final readonly class InteractivePrompter
return $options->selectByValues($selectedValues);
}
/**
* Check if running in TUI context
*/
private function isInTuiContext(): bool
{
// Check if output is ConsoleOutput and has context set
if ($this->output instanceof ConsoleOutput) {
return $this->output->isInTuiContext();
}
return false;
}
}