['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; } }