chore: update console components, logging, router and add subdomain support
This commit is contained in:
@@ -17,15 +17,22 @@ use App\Framework\Discovery\ValueObjects\DiscoveredAttribute;
|
||||
/**
|
||||
* Handles all rendering and display logic for the TUI
|
||||
*/
|
||||
final readonly class TuiRenderer
|
||||
final class TuiRenderer
|
||||
{
|
||||
private MenuBar $menuBar;
|
||||
private bool $menuBarInitialized = false;
|
||||
|
||||
public function __construct(
|
||||
private ConsoleOutputInterface $output
|
||||
) {
|
||||
// Initialize menu bar with default items
|
||||
$this->menuBar = new MenuBar(['Datei', 'Bearbeiten', 'Ansicht', 'Hilfe']);
|
||||
// Initialize menu bar with default items and submenus
|
||||
$submenus = [
|
||||
'Datei' => ['Neu', 'Öffnen', 'Speichern', '---', 'Beenden'],
|
||||
'Bearbeiten' => ['Rückgängig', 'Wiederholen', '---', 'Ausschneiden', 'Kopieren', 'Einfügen'],
|
||||
'Ansicht' => ['Vergrößern', 'Verkleinern', 'Normal', '---', 'Vollbild'],
|
||||
'Hilfe' => ['Hilfe anzeigen', 'Über'],
|
||||
];
|
||||
$this->menuBar = new MenuBar(['Datei', 'Bearbeiten', 'Ansicht', 'Hilfe'], $submenus);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -33,18 +40,28 @@ final readonly class TuiRenderer
|
||||
*/
|
||||
public function render(TuiState $state, CommandHistory $history): void
|
||||
{
|
||||
$this->clearScreen();
|
||||
|
||||
// Get terminal size for layout
|
||||
$terminalSize = TerminalSize::detect();
|
||||
|
||||
// Render menu bar at top
|
||||
// On first render, clear entire screen to remove any welcome screen content
|
||||
if (!$this->menuBarInitialized) {
|
||||
$this->clearScreen();
|
||||
$this->menuBarInitialized = true;
|
||||
} else {
|
||||
// On subsequent renders, clear ONLY the content area (preserve menu bar on lines 1-3)
|
||||
// Line 1: empty spacing
|
||||
// Lines 2-3: menu bar
|
||||
// Line 4+: content (this will be cleared and redrawn)
|
||||
$this->clearContentArea();
|
||||
}
|
||||
|
||||
// Render menu bar on lines 2-3 FIRST, before any content
|
||||
$this->renderMenuBar($state, $terminalSize->width);
|
||||
|
||||
// Render header (or skip if menu bar replaces it)
|
||||
// $this->renderHeader();
|
||||
// Position cursor at line 4, before rendering content
|
||||
$this->output->write(CursorControlCode::POSITION->format(4, 1));
|
||||
|
||||
// Render main content
|
||||
// Render main content starting at line 4 (after spacing + menu bar)
|
||||
match ($state->getCurrentView()) {
|
||||
TuiView::CATEGORIES => $this->renderCategories($state),
|
||||
TuiView::COMMANDS => $this->renderCommands($state),
|
||||
@@ -57,10 +74,24 @@ final readonly class TuiRenderer
|
||||
|
||||
// Render status line at bottom
|
||||
$this->renderStatusLine($state, $terminalSize->width);
|
||||
|
||||
// Render dropdown menu if a menu is active and has submenus
|
||||
$this->renderDropdownMenu($state, $terminalSize->width);
|
||||
|
||||
// Position cursor safely after rendering (below status line)
|
||||
$terminalSize = TerminalSize::detect();
|
||||
$cursorLine = min($terminalSize->height + 1, $terminalSize->height);
|
||||
$this->output->write(CursorControlCode::POSITION->format($cursorLine, 1));
|
||||
|
||||
// Final flush to ensure everything is displayed
|
||||
if (function_exists('fflush')) {
|
||||
fflush(STDOUT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear screen and reset cursor
|
||||
* Note: This clears the entire screen. Use clearContentArea() to preserve the menu bar.
|
||||
*/
|
||||
private function clearScreen(): void
|
||||
{
|
||||
@@ -68,6 +99,26 @@ final readonly class TuiRenderer
|
||||
$this->output->write(CursorControlCode::POSITION->format(1, 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear only the content area (from line 4 downwards), preserving the menu bar (lines 1-3)
|
||||
*/
|
||||
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));
|
||||
|
||||
// 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)
|
||||
$this->output->write(ScreenControlCode::CLEAR_BELOW->format());
|
||||
|
||||
// Position cursor back at line 4 for content rendering
|
||||
$this->output->write(CursorControlCode::POSITION->format(4, 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Render header with title
|
||||
*/
|
||||
@@ -90,7 +141,8 @@ final readonly class TuiRenderer
|
||||
$categories = $state->getCategories();
|
||||
foreach ($categories as $index => $category) {
|
||||
$isSelected = $index === $state->getSelectedCategory();
|
||||
$this->renderCategoryItem($category, $isSelected);
|
||||
$isHovered = $state->isContentItemHovered('category', $index);
|
||||
$this->renderCategoryItem($category, $isSelected, $isHovered);
|
||||
}
|
||||
|
||||
$this->output->newLine();
|
||||
@@ -108,14 +160,15 @@ final readonly class TuiRenderer
|
||||
/**
|
||||
* Render a category item
|
||||
*/
|
||||
private function renderCategoryItem(array $category, bool $isSelected): void
|
||||
private function renderCategoryItem(array $category, bool $isSelected, bool $isHovered = false): void
|
||||
{
|
||||
$icon = $category['icon'] ?? '📁';
|
||||
$name = $category['name'];
|
||||
$count = count($category['commands']);
|
||||
|
||||
$prefix = $isSelected ? '▶ ' : ' ';
|
||||
$color = $isSelected ? ConsoleColor::BRIGHT_WHITE : ConsoleColor::WHITE;
|
||||
// Determine color: selected > hovered > normal
|
||||
$color = $isSelected ? ConsoleColor::BRIGHT_WHITE : ($isHovered ? ConsoleColor::BRIGHT_CYAN : ConsoleColor::WHITE);
|
||||
|
||||
$this->output->writeLine(
|
||||
"{$prefix}{$icon} {$name} ({$count} commands)",
|
||||
@@ -139,7 +192,8 @@ final readonly class TuiRenderer
|
||||
|
||||
foreach ($category['commands'] as $index => $command) {
|
||||
$isSelected = $index === $state->getSelectedCommand();
|
||||
$this->renderCommandItem($command, $isSelected);
|
||||
$isHovered = $state->isContentItemHovered('command', $index);
|
||||
$this->renderCommandItem($command, $isSelected, $isHovered);
|
||||
}
|
||||
|
||||
$this->output->newLine();
|
||||
@@ -159,10 +213,11 @@ final readonly class TuiRenderer
|
||||
/**
|
||||
* Render a command item
|
||||
*/
|
||||
private function renderCommandItem(object $command, bool $isSelected): void
|
||||
private function renderCommandItem(object $command, bool $isSelected, bool $isHovered = false): void
|
||||
{
|
||||
$prefix = $isSelected ? '▶ ' : ' ';
|
||||
$color = $isSelected ? ConsoleColor::BRIGHT_WHITE : ConsoleColor::WHITE;
|
||||
// Determine color: selected > hovered > normal
|
||||
$color = $isSelected ? ConsoleColor::BRIGHT_WHITE : ($isHovered ? ConsoleColor::BRIGHT_CYAN : ConsoleColor::WHITE);
|
||||
|
||||
// Handle DiscoveredAttribute objects
|
||||
if ($command instanceof DiscoveredAttribute) {
|
||||
@@ -214,7 +269,8 @@ final readonly class TuiRenderer
|
||||
|
||||
foreach ($results as $index => $command) {
|
||||
$isSelected = $index === $state->getSelectedSearchResult();
|
||||
$this->renderCommandItem($command, $isSelected);
|
||||
$isHovered = $state->isContentItemHovered('search', $index);
|
||||
$this->renderCommandItem($command, $isSelected, $isHovered);
|
||||
}
|
||||
|
||||
$current = $state->getSelectedSearchResult() + 1;
|
||||
@@ -259,7 +315,8 @@ final readonly class TuiRenderer
|
||||
} else {
|
||||
foreach ($items as $index => $item) {
|
||||
$isSelected = $index === $state->getSelectedHistoryItem();
|
||||
$this->renderHistoryItem($item, $isSelected, $history);
|
||||
$isHovered = $state->isContentItemHovered('history', $index);
|
||||
$this->renderHistoryItem($item, $isSelected, $history, $isHovered);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,10 +369,11 @@ final readonly class TuiRenderer
|
||||
/**
|
||||
* Render history item
|
||||
*/
|
||||
private function renderHistoryItem(array $item, bool $isSelected, CommandHistory $history): void
|
||||
private function renderHistoryItem(array $item, bool $isSelected, CommandHistory $history, bool $isHovered = false): void
|
||||
{
|
||||
$prefix = $isSelected ? '▶ ' : ' ';
|
||||
$color = $isSelected ? ConsoleColor::BRIGHT_WHITE : ConsoleColor::WHITE;
|
||||
// Determine color: selected > hovered > normal
|
||||
$color = $isSelected ? ConsoleColor::BRIGHT_WHITE : ($isHovered ? ConsoleColor::BRIGHT_CYAN : ConsoleColor::WHITE);
|
||||
|
||||
$command = $item['command'];
|
||||
$isFavorite = $history->isFavorite($command);
|
||||
@@ -466,7 +524,121 @@ final readonly class TuiRenderer
|
||||
*/
|
||||
private function renderMenuBar(TuiState $state, int $screenWidth): void
|
||||
{
|
||||
$this->menuBar->render($this->output, $state->getActiveMenu(), $screenWidth);
|
||||
// Explicitly position cursor at line 2 before rendering menu bar
|
||||
// Line 1 is empty spacing, menu bar is at lines 2-3
|
||||
$this->output->write(CursorControlCode::POSITION->format(2, 1));
|
||||
|
||||
// Render menu bar (will write to lines 2-3)
|
||||
$this->menuBar->render($this->output, $state->getActiveMenu(), $screenWidth, $state->getHoveredMenuItem());
|
||||
|
||||
// Flush to ensure menu bar is immediately visible
|
||||
if (function_exists('fflush')) {
|
||||
fflush(STDOUT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render dropdown menu under active menu item
|
||||
*/
|
||||
private function renderDropdownMenu(TuiState $state, int $screenWidth): void
|
||||
{
|
||||
$activeMenu = $state->getActiveMenu();
|
||||
|
||||
// Only render dropdown if a menu is active and has submenus
|
||||
if ($activeMenu === null || !$this->menuBar->hasSubmenu($activeMenu)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$submenuItems = $this->menuBar->getSubmenu($activeMenu);
|
||||
if (empty($submenuItems)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the X position of the active menu item
|
||||
// We'll need to get it from the menu bar's tracked positions
|
||||
$menuItems = $this->menuBar->getItems();
|
||||
$activeIndex = array_search($activeMenu, $menuItems, true);
|
||||
|
||||
if ($activeIndex === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the X position of this menu item from tracked positions
|
||||
// Note: MenuBar must be rendered first to track positions, but we render dropdown before menu bar
|
||||
// So we'll use the approximate calculation
|
||||
$dropdownX = $this->menuBar->getItemXPosition($activeIndex) ?? $this->calculateDropdownXPosition($activeIndex, $activeMenu);
|
||||
|
||||
// Render dropdown starting at line 4, column dropdownX (after menu bar at 2-3)
|
||||
// Find the maximum width of submenu items
|
||||
$maxWidth = 0;
|
||||
foreach ($submenuItems as $item) {
|
||||
if ($item === '---') {
|
||||
continue; // Separator
|
||||
}
|
||||
$maxWidth = max($maxWidth, mb_strlen($item) + 2); // +2 for padding
|
||||
}
|
||||
$dropdownWidth = min($maxWidth, $screenWidth - $dropdownX + 1);
|
||||
|
||||
// Render dropdown box at line 4
|
||||
$this->output->write(CursorControlCode::POSITION->format(4, $dropdownX));
|
||||
|
||||
$selectedIndex = $state->getSelectedDropdownItem();
|
||||
$itemCount = count($submenuItems);
|
||||
|
||||
foreach ($submenuItems as $index => $item) {
|
||||
// Position at line 4 + index
|
||||
$line = 4 + $index;
|
||||
$this->output->write(CursorControlCode::POSITION->format($line, $dropdownX));
|
||||
|
||||
if ($item === '---') {
|
||||
// Render separator
|
||||
$this->output->write(str_repeat('─', $dropdownWidth - 2), ConsoleColor::GRAY);
|
||||
} else {
|
||||
$isSelected = $index === $selectedIndex;
|
||||
$isHovered = $index === $state->getHoveredDropdownItem();
|
||||
|
||||
// Render item with selection indicator
|
||||
$prefix = $isSelected ? '▶ ' : ' ';
|
||||
$text = $prefix . $item;
|
||||
|
||||
// Pad to dropdown width
|
||||
$padding = max(0, $dropdownWidth - mb_strlen($text) - 2);
|
||||
$text .= str_repeat(' ', $padding);
|
||||
|
||||
// Determine color: selected > hovered > normal
|
||||
$color = $isSelected ? ConsoleColor::BRIGHT_WHITE : ($isHovered ? ConsoleColor::BRIGHT_CYAN : ConsoleColor::WHITE);
|
||||
$this->output->writeLine($text, $color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate X position for dropdown based on menu item index
|
||||
*/
|
||||
private function calculateDropdownXPosition(int $menuIndex, string $menuItem): int
|
||||
{
|
||||
// Approximate calculation: we need to sum up widths of all previous items
|
||||
$items = $this->menuBar->getItems();
|
||||
$x = 1; // Start at column 1
|
||||
|
||||
for ($i = 0; $i < $menuIndex; $i++) {
|
||||
$item = $items[$i];
|
||||
// Approximate width: separator (3) + prefix (2) + item length
|
||||
$separatorWidth = $i > 0 ? 3 : 0; // " | " separator
|
||||
$prefixWidth = 2; // " " or "▶ "
|
||||
$itemWidth = mb_strlen($item);
|
||||
$x += $separatorWidth + $prefixWidth + $itemWidth;
|
||||
}
|
||||
|
||||
// Add separator for current item (if not first)
|
||||
if ($menuIndex > 0) {
|
||||
$x += 3; // " | "
|
||||
}
|
||||
|
||||
// Add prefix width for current item
|
||||
$x += 2; // " " or "▶ "
|
||||
|
||||
return $x;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user