feat: CI/CD pipeline setup complete - Ansible playbooks updated, secrets configured, workflow ready
This commit is contained in:
@@ -10,6 +10,7 @@ use App\Framework\Console\ConsoleColor;
|
||||
use App\Framework\Console\ConsoleOutputInterface;
|
||||
use App\Framework\Console\ExitCode;
|
||||
use App\Framework\Console\Screen\CursorControlCode;
|
||||
use App\Framework\Console\Screen\MouseControlCode;
|
||||
use App\Framework\Console\Screen\ScreenControlCode;
|
||||
use App\Framework\Console\SimpleWorkflowExecutor;
|
||||
use App\Framework\Console\TuiView;
|
||||
@@ -22,6 +23,8 @@ use App\Framework\Discovery\Results\DiscoveryRegistry;
|
||||
*/
|
||||
final readonly class ConsoleTUI
|
||||
{
|
||||
private InputParser $inputParser;
|
||||
|
||||
public function __construct(
|
||||
private ConsoleOutputInterface $output,
|
||||
private Container $container,
|
||||
@@ -34,6 +37,7 @@ final readonly class ConsoleTUI
|
||||
private CommandGroupRegistry $groupRegistry,
|
||||
private SimpleWorkflowExecutor $workflowExecutor
|
||||
) {
|
||||
$this->inputParser = new InputParser();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,12 +68,59 @@ final readonly class ConsoleTUI
|
||||
*/
|
||||
private function mainLoop(): void
|
||||
{
|
||||
$needsRender = true;
|
||||
|
||||
while ($this->state->isRunning()) {
|
||||
$this->renderCurrentView();
|
||||
$this->handleUserInput();
|
||||
// Only render when needed
|
||||
if ($needsRender) {
|
||||
$this->renderCurrentView();
|
||||
$needsRender = false;
|
||||
}
|
||||
|
||||
// Handle input (non-blocking)
|
||||
$event = $this->inputParser->readEvent();
|
||||
|
||||
if ($event === null) {
|
||||
// No input available, sleep briefly to reduce CPU usage
|
||||
usleep(50000); // 50ms
|
||||
continue;
|
||||
}
|
||||
|
||||
// Process event and mark for re-render
|
||||
$needsRender = $this->processEvent($event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process input event and return whether re-render is needed
|
||||
*/
|
||||
private function processEvent(MouseEvent|KeyEvent $event): bool
|
||||
{
|
||||
// Handle form mode specially
|
||||
if ($this->state->getCurrentView() === TuiView::FORM) {
|
||||
$this->handleFormMode();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Dispatch event to input handler
|
||||
if ($event instanceof MouseEvent) {
|
||||
$this->inputHandler->handleMouseEvent($event, $this->state);
|
||||
return true; // Mouse events always need re-render
|
||||
} elseif ($event instanceof KeyEvent) {
|
||||
// Handle Ctrl+C
|
||||
if ($event->ctrl && $event->key === 'C') {
|
||||
$this->state->setRunning(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use new KeyEvent handler (supports Alt+Letter shortcuts)
|
||||
$this->inputHandler->handleKeyEvent($event, $this->state, $this->commandHistory);
|
||||
return true; // Key events always need re-render
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup and restore terminal
|
||||
*/
|
||||
@@ -89,6 +140,13 @@ final readonly class ConsoleTUI
|
||||
$this->setRawMode(true);
|
||||
$this->output->write(CursorControlCode::HIDE->format());
|
||||
|
||||
// Enable mouse reporting (SGR format)
|
||||
$this->output->write(MouseControlCode::ENABLE_ALL->format());
|
||||
$this->output->write(MouseControlCode::ENABLE_SGR->format());
|
||||
|
||||
// Optional: Enable alternate screen buffer
|
||||
$this->output->write(ScreenControlCode::ALTERNATE_BUFFER->format());
|
||||
|
||||
// Welcome message
|
||||
$this->output->write(ScreenControlCode::CLEAR_ALL->format());
|
||||
$this->output->write(CursorControlCode::POSITION->format(1, 1));
|
||||
@@ -99,8 +157,24 @@ final readonly class ConsoleTUI
|
||||
*/
|
||||
private function restoreTerminal(): void
|
||||
{
|
||||
$this->setRawMode(false);
|
||||
// Disable mouse reporting
|
||||
$this->output->write(MouseControlCode::DISABLE_SGR->format());
|
||||
$this->output->write(MouseControlCode::DISABLE_ALL->format());
|
||||
|
||||
// Show cursor
|
||||
$this->output->write(CursorControlCode::SHOW->format());
|
||||
|
||||
// Deactivate alternate screen buffer
|
||||
$this->output->write(ScreenControlCode::MAIN_BUFFER->format());
|
||||
|
||||
// Restore terminal mode
|
||||
$this->setRawMode(false);
|
||||
|
||||
// Reset terminal (stty sane)
|
||||
if (function_exists('shell_exec')) {
|
||||
shell_exec('stty sane 2>/dev/null');
|
||||
}
|
||||
|
||||
$this->output->screen->setInteractiveMode(false);
|
||||
}
|
||||
|
||||
@@ -126,24 +200,7 @@ final readonly class ConsoleTUI
|
||||
$this->renderer->render($this->state, $this->commandHistory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle user input
|
||||
*/
|
||||
private function handleUserInput(): void
|
||||
{
|
||||
$key = $this->readKey();
|
||||
|
||||
if ($key !== '') {
|
||||
// Handle form mode specially
|
||||
if ($this->state->getCurrentView() === TuiView::FORM) {
|
||||
$this->handleFormMode();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->inputHandler->handleInput($key, $this->state, $this->commandHistory);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle form mode interaction
|
||||
@@ -243,7 +300,13 @@ final readonly class ConsoleTUI
|
||||
$this->output->writeLine('Press any key to continue...', ConsoleColor::YELLOW);
|
||||
|
||||
// Wait for user input
|
||||
$this->readKey();
|
||||
while (true) {
|
||||
$event = $this->inputParser->readEvent();
|
||||
if ($event !== null) {
|
||||
break;
|
||||
}
|
||||
usleep(10000); // 10ms sleep
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
63
src/Framework/Console/Components/EventDispatcher.php
Normal file
63
src/Framework/Console/Components/EventDispatcher.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Console\Components;
|
||||
|
||||
/**
|
||||
* Dispatches events to UI components
|
||||
*/
|
||||
final class EventDispatcher
|
||||
{
|
||||
/** @var array<callable(MouseEvent, TuiState): void> */
|
||||
private array $mouseListeners = [];
|
||||
|
||||
/** @var array<callable(KeyEvent, TuiState): void> */
|
||||
private array $keyListeners = [];
|
||||
|
||||
/**
|
||||
* Register a mouse event listener
|
||||
*/
|
||||
public function onMouseEvent(callable $listener): void
|
||||
{
|
||||
$this->mouseListeners[] = $listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a keyboard event listener
|
||||
*/
|
||||
public function onKeyEvent(callable $listener): void
|
||||
{
|
||||
$this->keyListeners[] = $listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a mouse event
|
||||
*/
|
||||
public function dispatchMouseEvent(MouseEvent $event, TuiState $state): void
|
||||
{
|
||||
foreach ($this->mouseListeners as $listener) {
|
||||
$listener($event, $state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a keyboard event
|
||||
*/
|
||||
public function dispatchKeyEvent(KeyEvent $event, TuiState $state): void
|
||||
{
|
||||
foreach ($this->keyListeners as $listener) {
|
||||
$listener($event, $state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all listeners
|
||||
*/
|
||||
public function clear(): void
|
||||
{
|
||||
$this->mouseListeners = [];
|
||||
$this->keyListeners = [];
|
||||
}
|
||||
}
|
||||
|
||||
242
src/Framework/Console/Components/InputParser.php
Normal file
242
src/Framework/Console/Components/InputParser.php
Normal file
@@ -0,0 +1,242 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Console\Components;
|
||||
|
||||
/**
|
||||
* Parses ANSI escape sequences for mouse and keyboard events
|
||||
*/
|
||||
final class InputParser
|
||||
{
|
||||
/**
|
||||
* Read and parse input from STDIN using stream_select()
|
||||
*
|
||||
* @return MouseEvent|KeyEvent|null Returns parsed event or null if no input available
|
||||
*/
|
||||
public function readEvent(): MouseEvent|KeyEvent|null
|
||||
{
|
||||
// Use stream_select for non-blocking I/O
|
||||
$read = [STDIN];
|
||||
$write = null;
|
||||
$except = null;
|
||||
|
||||
// Wait up to 0.01 seconds (10ms) for input
|
||||
$result = stream_select($read, $write, $except, 0, 10000);
|
||||
|
||||
if ($result === false || $result === 0 || !in_array(STDIN, $read, true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$originalBlocking = stream_get_meta_data(STDIN)['blocked'] ?? true;
|
||||
stream_set_blocking(STDIN, false);
|
||||
|
||||
try {
|
||||
$firstChar = fgetc(STDIN);
|
||||
if ($firstChar === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check for escape sequence
|
||||
if ($firstChar === "\033") {
|
||||
return $this->parseEscapeSequence($firstChar);
|
||||
}
|
||||
|
||||
// Check for Ctrl+C (ASCII 3)
|
||||
if ($firstChar === "\003") {
|
||||
stream_set_blocking(STDIN, $originalBlocking);
|
||||
return new KeyEvent(key: 'C', ctrl: true);
|
||||
}
|
||||
|
||||
// Regular character
|
||||
stream_set_blocking(STDIN, $originalBlocking);
|
||||
return new KeyEvent(key: $firstChar);
|
||||
} finally {
|
||||
stream_set_blocking(STDIN, $originalBlocking);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse escape sequence (mouse or keyboard)
|
||||
*/
|
||||
private function parseEscapeSequence(string $firstChar): MouseEvent|KeyEvent|null
|
||||
{
|
||||
$sequence = $firstChar;
|
||||
|
||||
// Read next character with timeout
|
||||
$next = $this->readCharWithTimeout();
|
||||
if ($next === null) {
|
||||
return new KeyEvent(key: "\033");
|
||||
}
|
||||
|
||||
$sequence .= $next;
|
||||
|
||||
// Mouse events start with \e[<
|
||||
if ($next === '[') {
|
||||
$third = $this->readCharWithTimeout();
|
||||
if ($third === '<') {
|
||||
return $this->parseMouseEvent($sequence . $third);
|
||||
}
|
||||
|
||||
// Keyboard escape sequence like \e[A (arrow up)
|
||||
if ($third !== null) {
|
||||
$sequence .= $third;
|
||||
return $this->parseKeyboardSequence($sequence);
|
||||
}
|
||||
}
|
||||
|
||||
// Just escape key
|
||||
return new KeyEvent(key: "\033");
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse SGR mouse event: \e[<b;x;yM (press) or \e[<b;x;ym (release)
|
||||
*/
|
||||
private function parseMouseEvent(string $prefix): MouseEvent|null
|
||||
{
|
||||
$buffer = '';
|
||||
$timeout = 10000; // 10ms
|
||||
$startTime = microtime(true) * 1000;
|
||||
|
||||
// Read until we get 'M' or 'm'
|
||||
while (true) {
|
||||
$char = fgetc(STDIN);
|
||||
if ($char === false) {
|
||||
// Check timeout
|
||||
$elapsed = (microtime(true) * 1000) - $startTime;
|
||||
if ($elapsed > $timeout) {
|
||||
return null;
|
||||
}
|
||||
usleep(1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
$buffer .= $char;
|
||||
|
||||
// Mouse event ends with 'M' (press) or 'm' (release)
|
||||
if ($char === 'M' || $char === 'm') {
|
||||
break;
|
||||
}
|
||||
|
||||
// Safety: limit buffer size
|
||||
if (strlen($buffer) > 20) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse format: b;x;y where b is button code, x and y are coordinates
|
||||
// Remove the final M/m
|
||||
$data = substr($buffer, 0, -1);
|
||||
$parts = explode(';', $data);
|
||||
|
||||
if (count($parts) < 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$buttonCode = (int) $parts[0];
|
||||
$x = (int) $parts[1];
|
||||
$y = (int) $parts[2];
|
||||
|
||||
// Decode button and modifiers
|
||||
// Bit flags in button code:
|
||||
// Bit 0-1: Button (0=left, 1=middle, 2=right, 3=release)
|
||||
// Bit 2: Shift
|
||||
// Bit 3: Meta/Alt
|
||||
// Bit 4: Ctrl
|
||||
// Bit 5-6: Polarity (64=scroll up, 65=scroll down)
|
||||
|
||||
$button = $buttonCode & 0x03;
|
||||
$shift = ($buttonCode & 0x04) !== 0;
|
||||
$alt = ($buttonCode & 0x08) !== 0;
|
||||
$ctrl = ($buttonCode & 0x10) !== 0;
|
||||
|
||||
// Handle scroll events (button codes 64 and 65)
|
||||
if ($buttonCode >= 64 && $buttonCode <= 65) {
|
||||
$button = $buttonCode;
|
||||
}
|
||||
|
||||
$pressed = $buffer[-1] === 'M';
|
||||
|
||||
return new MouseEvent(
|
||||
x: $x,
|
||||
y: $y,
|
||||
button: $button,
|
||||
pressed: $pressed,
|
||||
shift: $shift,
|
||||
ctrl: $ctrl,
|
||||
alt: $alt
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse keyboard escape sequence (arrow keys, function keys, etc.)
|
||||
*/
|
||||
private function parseKeyboardSequence(string $sequence): KeyEvent
|
||||
{
|
||||
// Map common escape sequences
|
||||
$keyMap = [
|
||||
"\033[A" => 'ArrowUp',
|
||||
"\033[B" => 'ArrowDown',
|
||||
"\033[C" => 'ArrowRight',
|
||||
"\033[D" => 'ArrowLeft',
|
||||
"\033[H" => 'Home',
|
||||
"\033[F" => 'End',
|
||||
"\033[5~" => 'PageUp',
|
||||
"\033[6~" => 'PageDown',
|
||||
"\033[3~" => 'Delete',
|
||||
"\n" => 'Enter',
|
||||
"\r" => 'Enter',
|
||||
"\033" => 'Escape',
|
||||
"\t" => 'Tab',
|
||||
];
|
||||
|
||||
// Check if we need to read more characters
|
||||
if (strlen($sequence) >= 3 && in_array($sequence[2], ['5', '6', '3'], true)) {
|
||||
$fourth = $this->readCharWithTimeout();
|
||||
if ($fourth !== null) {
|
||||
$sequence .= $fourth;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Enter key
|
||||
if ($sequence === "\n" || $sequence === "\r") {
|
||||
return new KeyEvent(key: 'Enter', code: "\n");
|
||||
}
|
||||
|
||||
// Check for Escape
|
||||
if ($sequence === "\033") {
|
||||
return new KeyEvent(key: 'Escape', code: "\033");
|
||||
}
|
||||
|
||||
// Map to known key
|
||||
if (isset($keyMap[$sequence])) {
|
||||
return new KeyEvent(key: $keyMap[$sequence], code: $sequence);
|
||||
}
|
||||
|
||||
// Unknown sequence, return as-is
|
||||
return new KeyEvent(key: $sequence, code: $sequence);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a single character with small timeout
|
||||
*/
|
||||
private function readCharWithTimeout(int $timeoutMs = 10): ?string
|
||||
{
|
||||
$startTime = microtime(true) * 1000;
|
||||
|
||||
while (true) {
|
||||
$char = fgetc(STDIN);
|
||||
if ($char !== false) {
|
||||
return $char;
|
||||
}
|
||||
|
||||
$elapsed = (microtime(true) * 1000) - $startTime;
|
||||
if ($elapsed > $timeoutMs) {
|
||||
return null;
|
||||
}
|
||||
|
||||
usleep(1000); // 1ms
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
54
src/Framework/Console/Components/KeyEvent.php
Normal file
54
src/Framework/Console/Components/KeyEvent.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Console\Components;
|
||||
|
||||
/**
|
||||
* Keyboard event value object
|
||||
*/
|
||||
final readonly class KeyEvent
|
||||
{
|
||||
public function __construct(
|
||||
public string $key,
|
||||
public string $code = '',
|
||||
public bool $shift = false,
|
||||
public bool $ctrl = false,
|
||||
public bool $alt = false,
|
||||
public bool $meta = false
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get human-readable description
|
||||
*/
|
||||
public function getDescription(): string
|
||||
{
|
||||
$modifiers = [];
|
||||
if ($this->ctrl) {
|
||||
$modifiers[] = 'Ctrl';
|
||||
}
|
||||
if ($this->alt) {
|
||||
$modifiers[] = 'Alt';
|
||||
}
|
||||
if ($this->shift) {
|
||||
$modifiers[] = 'Shift';
|
||||
}
|
||||
if ($this->meta) {
|
||||
$modifiers[] = 'Meta';
|
||||
}
|
||||
|
||||
$modifierStr = ! empty($modifiers) ? implode('+', $modifiers) . '+' : '';
|
||||
|
||||
return $modifierStr . $this->key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a control character (like Ctrl+C)
|
||||
*/
|
||||
public function isControlChar(): bool
|
||||
{
|
||||
return $this->ctrl && strlen($this->key) === 1 && ord($this->key) < 32;
|
||||
}
|
||||
}
|
||||
|
||||
117
src/Framework/Console/Components/MenuBar.php
Normal file
117
src/Framework/Console/Components/MenuBar.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Console\Components;
|
||||
|
||||
use App\Framework\Console\ConsoleColor;
|
||||
use App\Framework\Console\ConsoleOutputInterface;
|
||||
|
||||
/**
|
||||
* Menu bar with clickable entries
|
||||
*/
|
||||
final readonly class MenuBar
|
||||
{
|
||||
/**
|
||||
* @param array<string> $items Menu items (e.g. ['Datei', 'Bearbeiten', 'Ansicht', 'Hilfe'])
|
||||
*/
|
||||
public function __construct(
|
||||
private array $items
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the menu bar at the top of the screen
|
||||
*/
|
||||
public function render(ConsoleOutputInterface $output, ?string $activeMenu = null, int $screenWidth = 80): void
|
||||
{
|
||||
$menuLine = '';
|
||||
$separator = ' | ';
|
||||
|
||||
foreach ($this->items as $index => $item) {
|
||||
if ($index > 0) {
|
||||
$menuLine .= $separator;
|
||||
}
|
||||
|
||||
$isActive = $item === $activeMenu;
|
||||
$color = $isActive ? ConsoleColor::BRIGHT_WHITE : ConsoleColor::WHITE;
|
||||
$prefix = $isActive ? '▶ ' : ' ';
|
||||
|
||||
$menuLine .= "{$prefix}{$item}";
|
||||
}
|
||||
|
||||
// Pad to screen width
|
||||
$padding = max(0, $screenWidth - mb_strlen($menuLine));
|
||||
$menuLine .= str_repeat(' ', $padding);
|
||||
|
||||
$output->writeLine($menuLine, ConsoleColor::BRIGHT_WHITE);
|
||||
$output->writeLine(str_repeat('═', $screenWidth), ConsoleColor::GRAY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a mouse click is within the menu bar area
|
||||
*/
|
||||
public function isClickInMenuArea(int $x, int $y): bool
|
||||
{
|
||||
// Menu bar is always at y=1 (first line)
|
||||
return $y === 1 && $x >= 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get menu item at click position
|
||||
*/
|
||||
public function getItemAtPosition(int $x): ?string
|
||||
{
|
||||
// Simple calculation: approximate position based on item lengths
|
||||
// This is a simplified version - in a real implementation, you'd track exact positions
|
||||
$currentPos = 3; // Start after initial padding
|
||||
|
||||
foreach ($this->items as $item) {
|
||||
$itemLength = mb_strlen($item) + 4; // Include prefix and spacing
|
||||
if ($x >= $currentPos && $x < $currentPos + $itemLength) {
|
||||
return $item;
|
||||
}
|
||||
$currentPos += $itemLength + 3; // Add separator length
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hotkey for a menu item (first letter)
|
||||
*/
|
||||
public function getHotkeyForItem(string $item): ?string
|
||||
{
|
||||
$firstChar = mb_strtoupper(mb_substr($item, 0, 1));
|
||||
if ($firstChar === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $firstChar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find menu item by hotkey
|
||||
*/
|
||||
public function getItemByHotkey(string $hotkey): ?string
|
||||
{
|
||||
$hotkeyUpper = mb_strtoupper($hotkey);
|
||||
|
||||
foreach ($this->items as $item) {
|
||||
if (mb_strtoupper(mb_substr($item, 0, 1)) === $hotkeyUpper) {
|
||||
return $item;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all menu items
|
||||
*/
|
||||
public function getItems(): array
|
||||
{
|
||||
return $this->items;
|
||||
}
|
||||
}
|
||||
|
||||
46
src/Framework/Console/Components/MouseEvent.php
Normal file
46
src/Framework/Console/Components/MouseEvent.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Console\Components;
|
||||
|
||||
/**
|
||||
* Mouse event value object
|
||||
*/
|
||||
final readonly class MouseEvent
|
||||
{
|
||||
public function __construct(
|
||||
public int $x,
|
||||
public int $y,
|
||||
public int $button,
|
||||
public bool $pressed,
|
||||
public bool $shift = false,
|
||||
public bool $ctrl = false,
|
||||
public bool $alt = false
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get human-readable button name
|
||||
*/
|
||||
public function getButtonName(): string
|
||||
{
|
||||
return match ($this->button) {
|
||||
0 => 'Left',
|
||||
1 => 'Middle',
|
||||
2 => 'Right',
|
||||
64 => 'Scroll Up',
|
||||
65 => 'Scroll Down',
|
||||
default => 'Unknown',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a scroll event
|
||||
*/
|
||||
public function isScrollEvent(): bool
|
||||
{
|
||||
return in_array($this->button, [64, 65], true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@ use App\Framework\Discovery\ValueObjects\DiscoveredAttribute;
|
||||
final readonly class TuiInputHandler
|
||||
{
|
||||
public function __construct(
|
||||
private TuiCommandExecutor $commandExecutor
|
||||
private TuiCommandExecutor $commandExecutor,
|
||||
private ?MenuBar $menuBar = null
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -489,4 +490,86 @@ final readonly class TuiInputHandler
|
||||
HistoryTab::FAVORITES => $history->getFavorites(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle mouse events
|
||||
*/
|
||||
public function handleMouseEvent(MouseEvent $event, TuiState $state): void
|
||||
{
|
||||
// Only handle button presses, not releases
|
||||
if (! $event->pressed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only update status for menu clicks, not all mouse movements
|
||||
// This reduces flickering
|
||||
|
||||
// Handle menu bar clicks
|
||||
if ($this->menuBar !== null && $this->menuBar->isClickInMenuArea($event->x, $event->y)) {
|
||||
$menuItem = $this->menuBar->getItemAtPosition($event->x);
|
||||
if ($menuItem !== null && $event->button === 0) {
|
||||
// Left click on menu item
|
||||
$state->setActiveMenu($menuItem);
|
||||
$state->setStatus("Menü geöffnet: {$menuItem}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle keyboard events (for Alt+Letter menu shortcuts)
|
||||
*/
|
||||
public function handleKeyEvent(KeyEvent $event, TuiState $state, CommandHistory $history): void
|
||||
{
|
||||
// Handle Alt+Letter shortcuts for menu
|
||||
if ($event->alt && $this->menuBar !== null && strlen($event->key) === 1) {
|
||||
$menuItem = $this->menuBar->getItemByHotkey($event->key);
|
||||
if ($menuItem !== null) {
|
||||
$state->setActiveMenu($menuItem);
|
||||
$state->setStatus("Menü geöffnet: {$menuItem}");
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to legacy key string format first
|
||||
$keyString = $this->keyEventToString($event);
|
||||
if ($keyString === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only update status for non-navigation keys to reduce flickering
|
||||
$isNavigationKey = in_array($event->key, ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Enter', 'Escape', 'Tab'], true);
|
||||
|
||||
if (!$isNavigationKey) {
|
||||
$keyDesc = $event->getDescription();
|
||||
$state->setStatus("Taste: {$keyDesc}");
|
||||
}
|
||||
// Don't update status for navigation keys - let navigation happen without status flicker
|
||||
|
||||
// Process input - this handles navigation
|
||||
$this->handleInput($keyString, $state, $history);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert KeyEvent to legacy key string format
|
||||
*/
|
||||
private function keyEventToString(KeyEvent $event): string
|
||||
{
|
||||
// If code is available, use it directly (contains escape sequence)
|
||||
if ($event->code !== '') {
|
||||
return $event->code;
|
||||
}
|
||||
|
||||
// Otherwise map known keys to escape sequences
|
||||
return match ($event->key) {
|
||||
'ArrowUp' => "\033[A",
|
||||
'ArrowDown' => "\033[B",
|
||||
'ArrowRight' => "\033[C",
|
||||
'ArrowLeft' => "\033[D",
|
||||
'Enter' => "\n",
|
||||
'Escape' => "\033",
|
||||
'Tab' => "\t",
|
||||
default => $event->key,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ 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;
|
||||
@@ -18,9 +19,13 @@ use App\Framework\Discovery\ValueObjects\DiscoveredAttribute;
|
||||
*/
|
||||
final readonly class TuiRenderer
|
||||
{
|
||||
private MenuBar $menuBar;
|
||||
|
||||
public function __construct(
|
||||
private ConsoleOutputInterface $output
|
||||
) {
|
||||
// Initialize menu bar with default items
|
||||
$this->menuBar = new MenuBar(['Datei', 'Bearbeiten', 'Ansicht', 'Hilfe']);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,8 +34,17 @@ final readonly class TuiRenderer
|
||||
public function render(TuiState $state, CommandHistory $history): void
|
||||
{
|
||||
$this->clearScreen();
|
||||
$this->renderHeader();
|
||||
|
||||
// Get terminal size for layout
|
||||
$terminalSize = TerminalSize::detect();
|
||||
|
||||
// Render menu bar at top
|
||||
$this->renderMenuBar($state, $terminalSize->width);
|
||||
|
||||
// Render header (or skip if menu bar replaces it)
|
||||
// $this->renderHeader();
|
||||
|
||||
// Render main content
|
||||
match ($state->getCurrentView()) {
|
||||
TuiView::CATEGORIES => $this->renderCategories($state),
|
||||
TuiView::COMMANDS => $this->renderCommands($state),
|
||||
@@ -40,6 +54,9 @@ final readonly class TuiRenderer
|
||||
TuiView::DASHBOARD => $this->renderDashboard($state),
|
||||
TuiView::HELP => $this->renderHelp($state),
|
||||
};
|
||||
|
||||
// Render status line at bottom
|
||||
$this->renderStatusLine($state, $terminalSize->width);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -443,4 +460,41 @@ final readonly class TuiRenderer
|
||||
|
||||
return floor($diff / 86400) . "d ago";
|
||||
}
|
||||
|
||||
/**
|
||||
* Render menu bar at the top
|
||||
*/
|
||||
private function renderMenuBar(TuiState $state, int $screenWidth): void
|
||||
{
|
||||
$this->menuBar->render($this->output, $state->getActiveMenu(), $screenWidth);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,11 @@ final class TuiState
|
||||
|
||||
private bool $formMode = false;
|
||||
|
||||
// Menu and status line
|
||||
private string $statusText = '';
|
||||
|
||||
private ?string $activeMenu = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
@@ -318,4 +323,26 @@ final class TuiState
|
||||
$this->showWorkflows = false;
|
||||
$this->workflowProgress = [];
|
||||
}
|
||||
|
||||
// Status line
|
||||
public function getStatus(): string
|
||||
{
|
||||
return $this->statusText;
|
||||
}
|
||||
|
||||
public function setStatus(string $text): void
|
||||
{
|
||||
$this->statusText = $text;
|
||||
}
|
||||
|
||||
// Active menu
|
||||
public function getActiveMenu(): ?string
|
||||
{
|
||||
return $this->activeMenu;
|
||||
}
|
||||
|
||||
public function setActiveMenu(?string $menu): void
|
||||
{
|
||||
$this->activeMenu = $menu;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,7 +269,10 @@ final class ConsoleApplication
|
||||
$this->output->writeWindowTitle("{$this->scriptName} - {$commandName}");
|
||||
|
||||
// Execute command via registry
|
||||
return $this->commandRegistry->executeCommand($commandName, $arguments, $this->output);
|
||||
$result = $this->commandRegistry->executeCommand($commandName, $arguments, $this->output);
|
||||
|
||||
// Handle ConsoleResult (new) or ExitCode (legacy)
|
||||
return $this->processCommandResult($result);
|
||||
|
||||
} catch (CommandNotFoundException $e) {
|
||||
return $this->errorHandler->handleCommandNotFound($commandName, $this->output);
|
||||
@@ -291,6 +294,42 @@ final class ConsoleApplication
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process command result - supports both ConsoleResult and ExitCode
|
||||
*/
|
||||
private function processCommandResult(mixed $result): ExitCode
|
||||
{
|
||||
// New ConsoleResult pattern
|
||||
if ($result instanceof \App\Framework\Console\Result\ConsoleResult) {
|
||||
// Render result to output
|
||||
$result->render($this->output);
|
||||
|
||||
// Return exit code from result
|
||||
return $result->exitCode;
|
||||
}
|
||||
|
||||
// Legacy ExitCode pattern
|
||||
if ($result instanceof ExitCode) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Legacy int pattern (for backwards compatibility)
|
||||
if (is_int($result)) {
|
||||
return ExitCode::from($result);
|
||||
}
|
||||
|
||||
// Invalid return type - log warning and return error
|
||||
if ($this->container->has(Logger::class)) {
|
||||
$logger = $this->container->get(Logger::class);
|
||||
$logger->warning('Command returned invalid result type', LogContext::withData([
|
||||
'result_type' => get_debug_type($result),
|
||||
'component' => 'ConsoleApplication',
|
||||
]));
|
||||
}
|
||||
|
||||
return ExitCode::GENERAL_ERROR;
|
||||
}
|
||||
|
||||
private function showCommandUsage(string $commandName): void
|
||||
{
|
||||
try {
|
||||
@@ -527,6 +566,7 @@ final class ConsoleApplication
|
||||
// Create TUI components
|
||||
$state = new TuiState();
|
||||
$renderer = new TuiRenderer($this->output);
|
||||
$menuBar = $renderer->getMenuBar();
|
||||
$commandExecutor = new TuiCommandExecutor(
|
||||
$this->output,
|
||||
$this->commandRegistry,
|
||||
@@ -537,7 +577,7 @@ final class ConsoleApplication
|
||||
new CommandHelpGenerator(new ParameterInspector()),
|
||||
$this->scriptName
|
||||
);
|
||||
$inputHandler = new TuiInputHandler($commandExecutor);
|
||||
$inputHandler = new TuiInputHandler($commandExecutor, $menuBar);
|
||||
|
||||
// Erstelle TUI Instanz
|
||||
$tui = new ConsoleTUI(
|
||||
|
||||
@@ -51,6 +51,12 @@ final readonly class ConsoleOutput implements ConsoleOutputInterface
|
||||
*/
|
||||
public function writeWindowTitle(string $title, int $mode = 0): void
|
||||
{
|
||||
// Only emit terminal escape sequences when output goes to a real terminal
|
||||
// Skip for pipes, redirects, MCP mode, Claude Code execution, and any programmatic use
|
||||
if (!$this->isTerminal()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->writeRaw("\033]$mode;{$title}\007");
|
||||
}
|
||||
|
||||
|
||||
84
src/Framework/Console/Result/CompositeResult.php
Normal file
84
src/Framework/Console/Result/CompositeResult.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Console\Result;
|
||||
|
||||
use App\Framework\Console\ConsoleOutputInterface;
|
||||
use App\Framework\Console\ExitCode;
|
||||
|
||||
/**
|
||||
* Composite Result - Combines multiple results
|
||||
*
|
||||
* Allows combining multiple console results into a single result.
|
||||
* Useful for complex commands that need to display multiple sections.
|
||||
*
|
||||
* Exit code follows "worst wins" strategy:
|
||||
* - If any result has FAILURE, composite has FAILURE
|
||||
* - Otherwise SUCCESS
|
||||
*
|
||||
* Example:
|
||||
* return new CompositeResult(
|
||||
* TextResult::info("Starting process..."),
|
||||
* TableResult::create(['Step', 'Status'], $steps),
|
||||
* TextResult::success("Process completed!")
|
||||
* );
|
||||
*/
|
||||
final readonly class CompositeResult implements ConsoleResult
|
||||
{
|
||||
/** @var array<ConsoleResult> */
|
||||
private array $results;
|
||||
|
||||
public readonly ExitCode $exitCode;
|
||||
public readonly array $data;
|
||||
|
||||
/**
|
||||
* Create composite result with variadic constructor
|
||||
*/
|
||||
public function __construct(ConsoleResult ...$results)
|
||||
{
|
||||
$this->results = $results;
|
||||
$this->exitCode = $this->aggregateExitCode($results);
|
||||
$this->data = [
|
||||
'result_count' => count($results),
|
||||
'results' => array_map(fn(ConsoleResult $r) => $r->data, $results),
|
||||
'exit_code' => $this->exitCode->value,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all results
|
||||
*
|
||||
* @return array<ConsoleResult>
|
||||
*/
|
||||
public function getResults(): array
|
||||
{
|
||||
return $this->results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render all results sequentially
|
||||
*/
|
||||
public function render(ConsoleOutputInterface $output): void
|
||||
{
|
||||
foreach ($this->results as $result) {
|
||||
$result->render($output);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregate exit codes - "worst wins" strategy
|
||||
*
|
||||
* @param array<ConsoleResult> $results
|
||||
*/
|
||||
private function aggregateExitCode(array $results): ExitCode
|
||||
{
|
||||
foreach ($results as $result) {
|
||||
if ($result->exitCode === ExitCode::FAILURE) {
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
}
|
||||
46
src/Framework/Console/Result/ConsoleResult.php
Normal file
46
src/Framework/Console/Result/ConsoleResult.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Console\Result;
|
||||
|
||||
use App\Framework\Console\ConsoleOutputInterface;
|
||||
use App\Framework\Console\ExitCode;
|
||||
|
||||
/**
|
||||
* Console Result Interface
|
||||
*
|
||||
* Represents the result of a console command execution.
|
||||
* Similar to HTTP ActionResult, but for console output.
|
||||
*
|
||||
* Results are value objects that encapsulate:
|
||||
* - Exit code (success/failure)
|
||||
* - Rendering logic
|
||||
* - Metadata for testing/introspection
|
||||
*/
|
||||
interface ConsoleResult
|
||||
{
|
||||
/**
|
||||
* Exit code for this result
|
||||
*
|
||||
* Determines the process exit code (0 = success, >0 = failure)
|
||||
*/
|
||||
public ExitCode $exitCode { get; }
|
||||
|
||||
/**
|
||||
* Metadata for testing/introspection
|
||||
*
|
||||
* Returns array with result data that can be inspected in tests.
|
||||
* Useful for asserting on result contents without rendering.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public array $data { get; }
|
||||
|
||||
/**
|
||||
* Render result to console output
|
||||
*
|
||||
* Implementations should write to the provided output interface.
|
||||
*/
|
||||
public function render(ConsoleOutputInterface $output): void;
|
||||
}
|
||||
188
src/Framework/Console/Result/ConsoleResultBuilder.php
Normal file
188
src/Framework/Console/Result/ConsoleResultBuilder.php
Normal file
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Console\Result;
|
||||
|
||||
use App\Framework\Console\ConsoleStyle;
|
||||
use App\Framework\Console\ExitCode;
|
||||
|
||||
/**
|
||||
* Console Result Builder - Fluent API
|
||||
*
|
||||
* Provides a convenient fluent interface for building complex console results.
|
||||
* Automatically creates CompositeResult when multiple components are added.
|
||||
*
|
||||
* Example:
|
||||
* return ConsoleResultBuilder::create()
|
||||
* ->addText('System Report', ConsoleStyle::SUCCESS)
|
||||
* ->addDivider()
|
||||
* ->addTable(['Metric', 'Value'], [
|
||||
* ['CPU', '45%'],
|
||||
* ['Memory', '2.3GB']
|
||||
* ])
|
||||
* ->addNewLine()
|
||||
* ->addText('Generated: ' . date('Y-m-d H:i:s'))
|
||||
* ->withExitCode(ExitCode::SUCCESS)
|
||||
* ->build();
|
||||
*/
|
||||
final class ConsoleResultBuilder
|
||||
{
|
||||
/** @var array<ConsoleResult> */
|
||||
private array $results = [];
|
||||
|
||||
private ExitCode $exitCode = ExitCode::SUCCESS;
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new builder instance
|
||||
*/
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add text result
|
||||
*/
|
||||
public function addText(
|
||||
string $message,
|
||||
ConsoleStyle $style = ConsoleStyle::INFO
|
||||
): self {
|
||||
$this->results[] = new TextResult($message, $style, $this->exitCode);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add success text
|
||||
*/
|
||||
public function addSuccess(string $message): self
|
||||
{
|
||||
$this->results[] = TextResult::success($message);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add error text
|
||||
*/
|
||||
public function addError(string $message): self
|
||||
{
|
||||
$this->results[] = TextResult::error($message);
|
||||
$this->exitCode = ExitCode::FAILURE;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add warning text
|
||||
*/
|
||||
public function addWarning(string $message): self
|
||||
{
|
||||
$this->results[] = TextResult::warning($message);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add info text
|
||||
*/
|
||||
public function addInfo(string $message): self
|
||||
{
|
||||
$this->results[] = TextResult::info($message);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add plain text
|
||||
*/
|
||||
public function addPlain(string $message): self
|
||||
{
|
||||
$this->results[] = TextResult::plain($message);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add table result
|
||||
*
|
||||
* @param array<string> $headers
|
||||
* @param array<array<string>> $rows
|
||||
*/
|
||||
public function addTable(
|
||||
array $headers,
|
||||
array $rows,
|
||||
?string $title = null
|
||||
): self {
|
||||
$this->results[] = TableResult::create($headers, $rows, $title, $this->exitCode);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add divider (horizontal line)
|
||||
*/
|
||||
public function addDivider(string $char = '─', int $length = 80): self
|
||||
{
|
||||
$this->results[] = TextResult::plain(str_repeat($char, $length));
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add empty line
|
||||
*/
|
||||
public function addNewLine(int $count = 1): self
|
||||
{
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$this->results[] = TextResult::plain('');
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add existing result
|
||||
*/
|
||||
public function addResult(ConsoleResult $result): self
|
||||
{
|
||||
$this->results[] = $result;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set exit code for builder
|
||||
*
|
||||
* Note: Individual results may override this
|
||||
*/
|
||||
public function withExitCode(ExitCode $exitCode): self
|
||||
{
|
||||
$this->exitCode = $exitCode;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build final result
|
||||
*
|
||||
* Returns single result if only one component, CompositeResult otherwise
|
||||
*/
|
||||
public function build(): ConsoleResult
|
||||
{
|
||||
if (empty($this->results)) {
|
||||
// Empty builder - return success with no output
|
||||
return TextResult::plain('');
|
||||
}
|
||||
|
||||
if (count($this->results) === 1) {
|
||||
// Single result - return directly
|
||||
return $this->results[0];
|
||||
}
|
||||
|
||||
// Multiple results - wrap in CompositeResult
|
||||
return new CompositeResult(...$this->results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience: Build and render immediately
|
||||
*/
|
||||
public function render(\App\Framework\Console\ConsoleOutputInterface $output): void
|
||||
{
|
||||
$this->build()->render($output);
|
||||
}
|
||||
}
|
||||
91
src/Framework/Console/Result/TableResult.php
Normal file
91
src/Framework/Console/Result/TableResult.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Console\Result;
|
||||
|
||||
use App\Framework\Console\Components\Table;
|
||||
use App\Framework\Console\ConsoleOutputInterface;
|
||||
use App\Framework\Console\ExitCode;
|
||||
|
||||
/**
|
||||
* Table Result - Tabular data output
|
||||
*
|
||||
* Displays data in a formatted table with headers and rows.
|
||||
* Integrates with the existing Table component.
|
||||
*
|
||||
* Example:
|
||||
* return TableResult::create(
|
||||
* headers: ['ID', 'Name', 'Email'],
|
||||
* rows: [['1', 'John', 'john@example.com']],
|
||||
* title: 'Users'
|
||||
* );
|
||||
*/
|
||||
final readonly class TableResult implements ConsoleResult
|
||||
{
|
||||
public readonly array $data;
|
||||
|
||||
/**
|
||||
* Create table result
|
||||
*
|
||||
* @param array<string> $headers Table headers
|
||||
* @param array<array<string>> $rows Table rows
|
||||
* @param string|null $title Optional table title
|
||||
* @param ExitCode $exitCode Exit code (default: SUCCESS)
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly array $headers,
|
||||
public readonly array $rows,
|
||||
public readonly ?string $title = null,
|
||||
public readonly ExitCode $exitCode = ExitCode::SUCCESS,
|
||||
) {
|
||||
$this->data = [
|
||||
'headers' => $this->headers,
|
||||
'rows' => $this->rows,
|
||||
'title' => $this->title,
|
||||
'row_count' => count($this->rows),
|
||||
'exit_code' => $this->exitCode->value,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create table result (convenience factory)
|
||||
*
|
||||
* @param array<string> $headers
|
||||
* @param array<array<string>> $rows
|
||||
*/
|
||||
public static function create(
|
||||
array $headers,
|
||||
array $rows,
|
||||
?string $title = null,
|
||||
ExitCode $exitCode = ExitCode::SUCCESS
|
||||
): self {
|
||||
return new self($headers, $rows, $title, $exitCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create empty table with headers only
|
||||
*
|
||||
* @param array<string> $headers
|
||||
*/
|
||||
public static function empty(array $headers, ?string $title = null): self
|
||||
{
|
||||
return new self($headers, [], $title);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render table to console output
|
||||
*/
|
||||
public function render(ConsoleOutputInterface $output): void
|
||||
{
|
||||
$table = new Table($output);
|
||||
|
||||
if ($this->title !== null) {
|
||||
$table->setTitle($this->title);
|
||||
}
|
||||
|
||||
$table->setHeaders($this->headers);
|
||||
$table->setRows($this->rows);
|
||||
$table->render();
|
||||
}
|
||||
}
|
||||
89
src/Framework/Console/Result/TextResult.php
Normal file
89
src/Framework/Console/Result/TextResult.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Console\Result;
|
||||
|
||||
use App\Framework\Console\ConsoleOutputInterface;
|
||||
use App\Framework\Console\ConsoleStyle;
|
||||
use App\Framework\Console\ExitCode;
|
||||
|
||||
/**
|
||||
* Text Result - Simple text output with styling
|
||||
*
|
||||
* The most basic console result type for displaying text messages.
|
||||
* Supports different styles (success, error, warning, info) via factory methods.
|
||||
*
|
||||
* Example:
|
||||
* return TextResult::success("User created successfully!");
|
||||
* return TextResult::error("Failed to connect to database");
|
||||
*/
|
||||
final readonly class TextResult implements ConsoleResult
|
||||
{
|
||||
public readonly array $data;
|
||||
|
||||
/**
|
||||
* Create text result
|
||||
*
|
||||
* Use factory methods (success/error/warning/info) for convenience.
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $message,
|
||||
public readonly ConsoleStyle $style = ConsoleStyle::INFO,
|
||||
public readonly ExitCode $exitCode = ExitCode::SUCCESS,
|
||||
) {
|
||||
$this->data = [
|
||||
'message' => $this->message,
|
||||
'style' => $this->style->value,
|
||||
'exit_code' => $this->exitCode->value,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create success result (green text, exit code 0)
|
||||
*/
|
||||
public static function success(string $message): self
|
||||
{
|
||||
return new self($message, ConsoleStyle::SUCCESS, ExitCode::SUCCESS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create error result (red text, exit code 1)
|
||||
*/
|
||||
public static function error(string $message): self
|
||||
{
|
||||
return new self($message, ConsoleStyle::ERROR, ExitCode::FAILURE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create warning result (yellow text, exit code 0)
|
||||
*/
|
||||
public static function warning(string $message): self
|
||||
{
|
||||
return new self($message, ConsoleStyle::WARNING, ExitCode::SUCCESS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create info result (blue text, exit code 0)
|
||||
*/
|
||||
public static function info(string $message): self
|
||||
{
|
||||
return new self($message, ConsoleStyle::INFO, ExitCode::SUCCESS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create plain result (no styling, exit code 0)
|
||||
*/
|
||||
public static function plain(string $message): self
|
||||
{
|
||||
return new self($message, ConsoleStyle::NONE, ExitCode::SUCCESS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render text to console output
|
||||
*/
|
||||
public function render(ConsoleOutputInterface $output): void
|
||||
{
|
||||
$output->writeLine($this->message, $this->style);
|
||||
}
|
||||
}
|
||||
26
src/Framework/Console/Screen/MouseControlCode.php
Normal file
26
src/Framework/Console/Screen/MouseControlCode.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Console\Screen;
|
||||
|
||||
/**
|
||||
* Mouse control codes for ANSI terminals (SGR mouse reports)
|
||||
*/
|
||||
enum MouseControlCode: string
|
||||
{
|
||||
// Enable/disable mouse reporting
|
||||
case ENABLE_ALL = '?1000h'; // Enable mouse tracking
|
||||
case DISABLE_ALL = '?1000l'; // Disable mouse tracking
|
||||
case ENABLE_SGR = '?1006h'; // Enable SGR (Sixel Graphics Raster) mouse reports
|
||||
case DISABLE_SGR = '?1006l'; // Disable SGR mouse reports
|
||||
|
||||
/**
|
||||
* Format the mouse control code as ANSI sequence
|
||||
*/
|
||||
public function format(): string
|
||||
{
|
||||
return "\033[{$this->value}";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user