Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
- Remove middleware reference from Gitea Traefik labels (caused routing issues) - Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s) - Add explicit service reference in Traefik labels - Fix intermittent 504 timeouts by improving PostgreSQL connection handling Fixes Gitea unreachability via git.michaelschiemer.de
687 lines
24 KiB
PHP
687 lines
24 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\Console\Components;
|
|
|
|
use App\Framework\Console\Animation\AnimationManager;
|
|
use App\Framework\Console\Animation\TuiAnimationRenderer;
|
|
use App\Framework\Console\CommandHistory;
|
|
use App\Framework\Console\ConsoleColor;
|
|
use App\Framework\Console\ConsoleOutputInterface;
|
|
use App\Framework\Console\HistoryTab;
|
|
use App\Framework\Console\Layout\TerminalSize;
|
|
use App\Framework\Console\Screen\CursorControlCode;
|
|
use App\Framework\Console\Screen\ScreenControlCode;
|
|
use App\Framework\Console\TuiView;
|
|
use App\Framework\Discovery\ValueObjects\DiscoveredAttribute;
|
|
|
|
/**
|
|
* Handles all rendering and display logic for the TUI
|
|
*/
|
|
final class TuiRenderer
|
|
{
|
|
private MenuBar $menuBar;
|
|
private bool $menuBarInitialized = false;
|
|
private ?TuiAnimationRenderer $animationRenderer = null;
|
|
|
|
public function __construct(
|
|
private ConsoleOutputInterface $output,
|
|
?AnimationManager $animationManager = null
|
|
) {
|
|
// 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);
|
|
|
|
// Initialize animation renderer if animation manager is provided
|
|
if ($animationManager !== null) {
|
|
$this->animationRenderer = new TuiAnimationRenderer($animationManager);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render the current view based on state
|
|
*/
|
|
public function render(TuiState $state, CommandHistory $history): void
|
|
{
|
|
// Update animations if animation renderer is available
|
|
if ($this->animationRenderer !== null) {
|
|
$this->animationRenderer->update(0.016); // ~60 FPS
|
|
}
|
|
|
|
// Get terminal size for layout
|
|
$terminalSize = TerminalSize::detect();
|
|
|
|
// 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);
|
|
|
|
// Position cursor at line 4, before rendering content
|
|
$this->output->write(CursorControlCode::POSITION->format(4, 1));
|
|
|
|
// Render main content starting at line 4 (after spacing + menu bar)
|
|
match ($state->getCurrentView()) {
|
|
TuiView::CATEGORIES => $this->renderCategories($state),
|
|
TuiView::COMMANDS => $this->renderCommands($state),
|
|
TuiView::SEARCH => $this->renderSearch($state),
|
|
TuiView::HISTORY => $this->renderHistory($state, $history),
|
|
TuiView::FORM => $this->renderForm($state),
|
|
TuiView::DASHBOARD => $this->renderDashboard($state),
|
|
TuiView::HELP => $this->renderHelp($state),
|
|
};
|
|
|
|
// 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
|
|
{
|
|
$this->output->write(ScreenControlCode::CLEAR_ALL->format());
|
|
$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
|
|
*/
|
|
private function renderHeader(): void
|
|
{
|
|
$this->output->writeLine('');
|
|
$this->output->writeLine('🚀 Interactive Console - Modern TUI', ConsoleColor::BRIGHT_CYAN);
|
|
$this->output->writeLine(str_repeat('═', 60), ConsoleColor::GRAY);
|
|
$this->output->writeLine('');
|
|
}
|
|
|
|
/**
|
|
* Render categories view
|
|
*/
|
|
private function renderCategories(TuiState $state): void
|
|
{
|
|
$this->output->writeLine('📂 Select Category:', ConsoleColor::BRIGHT_YELLOW);
|
|
$this->output->writeLine('');
|
|
|
|
$categories = $state->getCategories();
|
|
foreach ($categories as $index => $category) {
|
|
$isSelected = $index === $state->getSelectedCategory();
|
|
$isHovered = $state->isContentItemHovered('category', $index);
|
|
$this->renderCategoryItem($category, $isSelected, $isHovered);
|
|
}
|
|
|
|
$this->output->newLine();
|
|
$this->renderNavigationBar([
|
|
"↑/↓: Navigate",
|
|
"Enter: Select",
|
|
"/: Search",
|
|
"R: History",
|
|
"D: Dashboard",
|
|
"F1: Help",
|
|
"Q: Quit",
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Render a category item
|
|
*/
|
|
private function renderCategoryItem(array $category, bool $isSelected, bool $isHovered = false): void
|
|
{
|
|
$icon = $category['icon'] ?? '📁';
|
|
$name = $category['name'];
|
|
$count = count($category['commands']);
|
|
|
|
$prefix = $isSelected ? '▶ ' : ' ';
|
|
// 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
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Render commands view
|
|
*/
|
|
private function renderCommands(TuiState $state): void
|
|
{
|
|
$category = $state->getCurrentCategory();
|
|
if (! $category) {
|
|
return;
|
|
}
|
|
|
|
$icon = $category['icon'] ?? '📁';
|
|
$this->output->writeLine("📂 {$icon} {$category['name']} Commands:", ConsoleColor::BRIGHT_YELLOW);
|
|
$this->output->writeLine('');
|
|
|
|
foreach ($category['commands'] as $index => $command) {
|
|
$isSelected = $index === $state->getSelectedCommand();
|
|
$isHovered = $state->isContentItemHovered('command', $index);
|
|
$this->renderCommandItem($command, $isSelected, $isHovered);
|
|
}
|
|
|
|
$this->output->newLine();
|
|
$this->renderNavigationBar([
|
|
"↑/↓: Navigate",
|
|
"Enter: Execute",
|
|
"Space: Parameters",
|
|
"H: Help",
|
|
"V: Validate",
|
|
"/: Search",
|
|
"R: History",
|
|
"←: Back",
|
|
"Q: Quit",
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Render a command item
|
|
*/
|
|
private function renderCommandItem(object $command, bool $isSelected, bool $isHovered = false): void
|
|
{
|
|
$prefix = $isSelected ? '▶ ' : ' ';
|
|
// Determine color: selected > hovered > normal
|
|
$color = $isSelected ? ConsoleColor::BRIGHT_WHITE : ($isHovered ? ConsoleColor::BRIGHT_CYAN : ConsoleColor::WHITE);
|
|
|
|
// Handle DiscoveredAttribute objects
|
|
if ($command instanceof DiscoveredAttribute) {
|
|
$attribute = $command->createAttributeInstance();
|
|
if ($attribute === null) {
|
|
return;
|
|
}
|
|
$commandName = $attribute->name;
|
|
$commandDescription = $attribute->description ?? '';
|
|
} else {
|
|
// Fallback for legacy objects
|
|
$commandName = $command->name ?? 'Unknown Command';
|
|
$commandDescription = $command->description ?? '';
|
|
}
|
|
|
|
$this->output->writeLine("{$prefix}⚡ {$commandName}", $color);
|
|
|
|
if (! empty($commandDescription)) {
|
|
$descColor = ConsoleColor::GRAY;
|
|
$this->output->writeLine(" {$commandDescription}", $descColor);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render search view
|
|
*/
|
|
private function renderSearch(TuiState $state): void
|
|
{
|
|
$this->output->writeLine('🔍 Search Commands:', ConsoleColor::BRIGHT_YELLOW);
|
|
$this->output->writeLine('');
|
|
|
|
// Search box
|
|
$query = $state->getSearchQuery();
|
|
$this->output->writeLine("Search: {$query}_", ConsoleColor::BRIGHT_WHITE);
|
|
$this->output->writeLine('');
|
|
|
|
// Results
|
|
$results = $state->getSearchResults();
|
|
if (empty($results)) {
|
|
if (! empty($query)) {
|
|
$this->output->writeLine('No commands found matching your search.', ConsoleColor::YELLOW);
|
|
} else {
|
|
$this->output->writeLine('Start typing to search commands...', ConsoleColor::GRAY);
|
|
}
|
|
} else {
|
|
$resultCount = count($results);
|
|
$this->output->writeLine("Found {$resultCount} result(s):", ConsoleColor::WHITE);
|
|
$this->output->writeLine('');
|
|
|
|
foreach ($results as $index => $command) {
|
|
$isSelected = $index === $state->getSelectedSearchResult();
|
|
$isHovered = $state->isContentItemHovered('search', $index);
|
|
$this->renderCommandItem($command, $isSelected, $isHovered);
|
|
}
|
|
|
|
$current = $state->getSelectedSearchResult() + 1;
|
|
$total = count($results);
|
|
if ($total > 1) {
|
|
$this->output->writeLine('');
|
|
$this->output->writeLine(" Showing result $current of $total", ConsoleColor::GRAY);
|
|
}
|
|
}
|
|
|
|
// Navigation shortcuts
|
|
$shortcuts = [
|
|
'Type: Search',
|
|
'Enter: Execute',
|
|
'Space: Parameters',
|
|
'H: Help',
|
|
'V: Validate',
|
|
'R: History',
|
|
'Esc: Back',
|
|
'D: Dashboard',
|
|
'Q: Exit',
|
|
];
|
|
$this->renderNavigationBar($shortcuts);
|
|
}
|
|
|
|
/**
|
|
* Render history view
|
|
*/
|
|
private function renderHistory(TuiState $state, CommandHistory $history): void
|
|
{
|
|
$this->output->writeLine('📚 Command History:', ConsoleColor::BRIGHT_YELLOW);
|
|
$this->output->writeLine('');
|
|
|
|
// Tab navigation
|
|
$this->renderHistoryTabs($state->getHistoryTab());
|
|
|
|
// History items
|
|
$items = $this->getHistoryItems($state->getHistoryTab(), $history);
|
|
|
|
if (empty($items)) {
|
|
$this->output->writeLine('No history available.', ConsoleColor::YELLOW);
|
|
} else {
|
|
foreach ($items as $index => $item) {
|
|
$isSelected = $index === $state->getSelectedHistoryItem();
|
|
$isHovered = $state->isContentItemHovered('history', $index);
|
|
$this->renderHistoryItem($item, $isSelected, $history, $isHovered);
|
|
}
|
|
}
|
|
|
|
$this->output->writeLine('');
|
|
$this->renderNavigationHelp([
|
|
"↑/↓: Navigate",
|
|
"←/→: Switch tabs",
|
|
"Enter: Execute command",
|
|
"Space: Parameters",
|
|
"H: Help",
|
|
"V: Validate",
|
|
"F: Toggle favorite",
|
|
"C: Clear history",
|
|
"/: Search",
|
|
"Q: Quit",
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Render history tabs
|
|
*/
|
|
private function renderHistoryTabs(HistoryTab $activeTab): void
|
|
{
|
|
$tabs = [
|
|
HistoryTab::RECENT,
|
|
HistoryTab::FREQUENT,
|
|
HistoryTab::FAVORITES,
|
|
];
|
|
|
|
$tabDisplay = [];
|
|
foreach ($tabs as $tab) {
|
|
$isActive = $tab === $activeTab;
|
|
$color = $isActive ? ConsoleColor::BRIGHT_WHITE : ConsoleColor::GRAY;
|
|
$prefix = $isActive ? '[' : ' ';
|
|
$suffix = $isActive ? ']' : ' ';
|
|
|
|
$tabDisplay[] = [
|
|
'text' => "{$prefix}{$tab->getIcon()} {$tab->getTitle()}{$suffix}",
|
|
'color' => $color,
|
|
];
|
|
}
|
|
|
|
foreach ($tabDisplay as $tab) {
|
|
$this->output->write($tab['text'], $tab['color']);
|
|
}
|
|
$this->output->writeLine('');
|
|
$this->output->writeLine('');
|
|
}
|
|
|
|
/**
|
|
* Render history item
|
|
*/
|
|
private function renderHistoryItem(array $item, bool $isSelected, CommandHistory $history, bool $isHovered = false): void
|
|
{
|
|
$prefix = $isSelected ? '▶ ' : ' ';
|
|
// Determine color: selected > hovered > normal
|
|
$color = $isSelected ? ConsoleColor::BRIGHT_WHITE : ($isHovered ? ConsoleColor::BRIGHT_CYAN : ConsoleColor::WHITE);
|
|
|
|
$command = $item['command'];
|
|
$isFavorite = $history->isFavorite($command);
|
|
$favoriteIcon = $isFavorite ? '⭐' : ' ';
|
|
|
|
$this->output->writeLine("{$prefix}{$favoriteIcon} ⚡ {$command}", $color);
|
|
|
|
// Additional info based on type
|
|
$infoColor = ConsoleColor::GRAY;
|
|
if (isset($item['count'])) {
|
|
$this->output->writeLine(" Used {$item['count']} times", $infoColor);
|
|
}
|
|
if (isset($item['timestamp'])) {
|
|
$timeAgo = $this->timeAgo($item['timestamp']);
|
|
$this->output->writeLine(" {$timeAgo}", $infoColor);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render form view placeholder
|
|
*/
|
|
private function renderForm(TuiState $state): void
|
|
{
|
|
$this->output->writeLine('📝 Interactive Form', ConsoleColor::BRIGHT_YELLOW);
|
|
$this->output->writeLine('Form functionality handled by InteractiveForm class', ConsoleColor::GRAY);
|
|
}
|
|
|
|
/**
|
|
* Render dashboard view
|
|
*/
|
|
private function renderDashboard(TuiState $state): void
|
|
{
|
|
$this->output->writeLine('📊 System Dashboard', ConsoleColor::BRIGHT_YELLOW);
|
|
$this->output->writeLine('');
|
|
$this->output->writeLine('Console system status and metrics', ConsoleColor::WHITE);
|
|
$this->output->writeLine('');
|
|
|
|
$this->renderNavigationBar([
|
|
"Categories: C",
|
|
"Search: /",
|
|
"History: R",
|
|
"Help: F1",
|
|
"Back: Esc",
|
|
"Quit: Q",
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Render help view
|
|
*/
|
|
private function renderHelp(TuiState $state): void
|
|
{
|
|
$this->output->writeLine('❓ Help & Shortcuts', ConsoleColor::BRIGHT_YELLOW);
|
|
$this->output->writeLine('');
|
|
|
|
$helpSections = [
|
|
'Navigation' => [
|
|
'↑/↓ Arrow Keys' => 'Navigate up and down in lists',
|
|
'←/→ Arrow Keys' => 'Navigate left/right, switch tabs',
|
|
'Enter' => 'Execute selected command',
|
|
'Esc' => 'Go back to previous view',
|
|
],
|
|
'Commands' => [
|
|
'Space' => 'Open interactive parameter form',
|
|
'H' => 'Show detailed command help',
|
|
'V' => 'Validate command signature',
|
|
'F1' => 'Show all commands help overview',
|
|
],
|
|
'Views' => [
|
|
'/' => 'Search commands',
|
|
'R' => 'Command history',
|
|
'D' => 'Dashboard',
|
|
'C' => 'Categories (from other views)',
|
|
],
|
|
'History' => [
|
|
'F' => 'Toggle command as favorite',
|
|
'C' => 'Clear command history',
|
|
],
|
|
];
|
|
|
|
foreach ($helpSections as $section => $items) {
|
|
$this->output->writeLine($section . ':', ConsoleColor::BRIGHT_WHITE);
|
|
foreach ($items as $key => $description) {
|
|
$this->output->writeLine(" {$key} - {$description}", ConsoleColor::WHITE);
|
|
}
|
|
$this->output->writeLine('');
|
|
}
|
|
|
|
$this->renderNavigationBar([
|
|
"Back: Esc",
|
|
"Categories: C",
|
|
"Quit: Q",
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Render navigation bar
|
|
*/
|
|
private function renderNavigationBar(array $items): void
|
|
{
|
|
$this->output->writeLine(str_repeat('─', 60), ConsoleColor::GRAY);
|
|
$this->output->writeLine(implode(' | ', $items), ConsoleColor::BRIGHT_BLUE);
|
|
}
|
|
|
|
/**
|
|
* Render navigation help
|
|
*/
|
|
private function renderNavigationHelp(array $items): void
|
|
{
|
|
$this->output->writeLine(str_repeat('─', 60), ConsoleColor::GRAY);
|
|
$this->output->writeLine(implode(' | ', $items), ConsoleColor::BRIGHT_BLUE);
|
|
}
|
|
|
|
/**
|
|
* Get history items based on tab
|
|
*/
|
|
private function getHistoryItems(HistoryTab $tab, CommandHistory $history): array
|
|
{
|
|
return match ($tab) {
|
|
HistoryTab::RECENT => $history->getRecentHistory(10),
|
|
HistoryTab::FREQUENT => $history->getFrequentCommands(10),
|
|
HistoryTab::FAVORITES => $history->getFavorites(),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Calculate time ago
|
|
*/
|
|
private function timeAgo(int $timestamp): string
|
|
{
|
|
$diff = time() - $timestamp;
|
|
|
|
if ($diff < 60) {
|
|
return "{$diff}s ago";
|
|
}
|
|
if ($diff < 3600) {
|
|
return floor($diff / 60) . "m ago";
|
|
}
|
|
if ($diff < 86400) {
|
|
return floor($diff / 3600) . "h ago";
|
|
}
|
|
|
|
return floor($diff / 86400) . "d ago";
|
|
}
|
|
|
|
/**
|
|
* Render menu bar at the top
|
|
*/
|
|
private function renderMenuBar(TuiState $state, int $screenWidth): void
|
|
{
|
|
// 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;
|
|
}
|
|
|
|
/**
|
|
* Render status line at the bottom
|
|
*/
|
|
private function renderStatusLine(TuiState $state, int $screenWidth): void
|
|
{
|
|
$statusText = $state->getStatus();
|
|
|
|
// Default status if empty
|
|
if ($statusText === '') {
|
|
$statusText = 'Bereit';
|
|
}
|
|
|
|
// Move cursor to last line
|
|
$terminalSize = TerminalSize::detect();
|
|
$this->output->write(CursorControlCode::POSITION->format($terminalSize->height, 1));
|
|
|
|
// Render status line with separator
|
|
$this->output->writeLine(str_repeat('─', $screenWidth), ConsoleColor::GRAY);
|
|
$this->output->writeLine($statusText, ConsoleColor::BRIGHT_BLUE);
|
|
}
|
|
|
|
/**
|
|
* Get menu bar instance
|
|
*/
|
|
public function getMenuBar(): MenuBar
|
|
{
|
|
return $this->menuBar;
|
|
}
|
|
}
|