chore: complete update

This commit is contained in:
2025-07-17 16:24:20 +02:00
parent 899227b0a4
commit 64a7051137
1300 changed files with 85570 additions and 2756 deletions

View File

@@ -0,0 +1,257 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Components;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleOutput;
/**
* Klasse für interaktive Menüs in der Konsole.
* Unterstützt sowohl einfache nummerierte Menüs als auch
* Menüs mit Pfeiltasten-Navigation.
*/
final class InteractiveMenu
{
private ConsoleOutput $output;
private array $menuItems = [];
private string $title = '';
private int $selectedIndex = 0;
private bool $showNumbers = true;
public function __construct(ConsoleOutput $output)
{
$this->output = $output;
}
/**
* Setzt den Titel des Menüs.
*/
public function setTitle(string $title): self
{
$this->title = $title;
return $this;
}
/**
* Fügt einen Menüpunkt hinzu.
*
* @param string $label Die Beschriftung des Menüpunkts
* @param callable|null $action Eine optionale Aktion, die bei Auswahl ausgeführt wird
* @param mixed $value Ein optionaler Wert, der zurückgegeben wird
*/
public function addItem(string $label, ?callable $action = null, mixed $value = null): self
{
$this->menuItems[] = [
'label' => $label,
'action' => $action,
'value' => $value ?? $label
];
return $this;
}
/**
* Fügt einen Separator zum Menü hinzu.
*/
public function addSeparator(): self
{
$this->menuItems[] = [
'label' => '---',
'action' => null,
'value' => null,
'separator' => true
];
return $this;
}
/**
* Steuert, ob Nummern angezeigt werden sollen.
*/
public function showNumbers(bool $show = true): self
{
$this->showNumbers = $show;
return $this;
}
/**
* Zeigt ein einfaches Menü mit Nummern-Auswahl.
*/
public function showSimple(): mixed
{
// Verwende den ScreenManager anstelle des direkten clearScreen()
$this->output->screen()->newMenu();
if ($this->title) {
$this->output->writeLine($this->title, ConsoleColor::BRIGHT_CYAN);
$this->output->writeLine(str_repeat('=', strlen($this->title)));
$this->output->writeLine('');
}
foreach ($this->menuItems as $index => $item) {
if (isset($item['separator'])) {
$this->output->writeLine($item['label'], ConsoleColor::GRAY);
continue;
}
$number = $this->showNumbers ? ($index + 1) . '. ' : '';
$this->output->writeLine($number . $item['label']);
}
$this->output->writeLine('');
$this->output->write('Ihre Wahl: ', ConsoleColor::BRIGHT_CYAN);
$input = trim(fgets(STDIN));
if (is_numeric($input)) {
$selectedIndex = (int)$input - 1;
if (isset($this->menuItems[$selectedIndex]) && !isset($this->menuItems[$selectedIndex]['separator'])) {
$item = $this->menuItems[$selectedIndex];
if ($item['action']) {
return $item['action']();
}
return $item['value'];
}
}
$this->output->writeError('Ungültige Auswahl!');
return $this->showSimple();
}
/**
* Zeigt ein interaktives Menü mit Pfeiltasten-Navigation.
*/
public function showInteractive(): mixed
{
$this->output->screen()->setInteractiveMode();
// Terminal in Raw-Modus setzen für Tastatur-Input
$this->setRawMode(true);
try {
while (true) {
$this->renderMenu();
$key = $this->readKey();
switch ($key) {
case "\033[A": // Pfeil hoch
$this->moveUp();
break;
case "\033[B": // Pfeil runter
$this->moveDown();
break;
case "\n": // Enter
case "\r":
return $this->selectCurrentItem();
case "\033": // ESC
return null;
case 'q':
case 'Q':
return null;
}
}
} finally {
$this->setRawMode(false);
}
}
/**
* Rendert das Menü auf dem Bildschirm.
*/
private function renderMenu(): void
{
// Verwende den ScreenManager anstelle des direkten clearScreen()
$this->output->screen()->newMenu();
if ($this->title) {
$this->output->writeLine($this->title, ConsoleColor::BRIGHT_CYAN);
$this->output->writeLine(str_repeat('=', strlen($this->title)));
$this->output->writeLine('');
}
foreach ($this->menuItems as $index => $item) {
if (isset($item['separator'])) {
$this->output->writeLine($item['label'], ConsoleColor::GRAY);
continue;
}
$isSelected = ($index === $this->selectedIndex);
$prefix = $isSelected ? '► ' : ' ';
$color = $isSelected ? ConsoleColor::BRIGHT_YELLOW : null;
$number = $this->showNumbers ? ($index + 1) . '. ' : '';
$this->output->writeLine($prefix . $number . $item['label'], $color);
}
$this->output->writeLine('');
$this->output->writeLine('Verwenden Sie ↑/↓ zum Navigieren, Enter zum Auswählen, ESC/Q zum Beenden', ConsoleColor::GRAY);
}
/**
* Bewegt die Auswahl nach oben.
*/
private function moveUp(): void
{
do {
$this->selectedIndex = ($this->selectedIndex - 1 + count($this->menuItems)) % count($this->menuItems);
} while (isset($this->menuItems[$this->selectedIndex]['separator']) && count($this->menuItems) > 1);
}
/**
* Bewegt die Auswahl nach unten.
*/
private function moveDown(): void
{
do {
$this->selectedIndex = ($this->selectedIndex + 1) % count($this->menuItems);
} while (isset($this->menuItems[$this->selectedIndex]['separator']) && count($this->menuItems) > 1);
}
/**
* Wählt den aktuellen Menüpunkt aus.
*/
private function selectCurrentItem(): mixed
{
$item = $this->menuItems[$this->selectedIndex];
if ($item['action']) {
return $item['action']();
}
return $item['value'];
}
/**
* Liest einen Tastendruck.
*/
private function readKey(): string
{
$key = fgetc(STDIN);
// Behandle Escape-Sequenzen
if ($key === "\033") {
$key .= fgetc(STDIN);
if ($key === "\033[") {
$key .= fgetc(STDIN);
}
}
return $key;
}
/**
* Setzt den Terminal-Modus.
*/
private function setRawMode(bool $enable): void
{
if ($enable) {
system('stty -echo -icanon min 1 time 0');
} else {
system('stty echo icanon');
}
}
// Die clearScreen-Methode wurde entfernt, da sie durch den ScreenManager ersetzt wurde.
// Verwende stattdessen $this->output->screen()->newScreen() oder $this->output->display()->clear().
}

View File

@@ -0,0 +1,205 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Components;
use App\Framework\Console\ConsoleStyle;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleFormat;
/**
* Rendert eine Tabelle in der Konsole.
*/
final class Table
{
private array $headers = [];
private array $rows = [];
private array $columnWidths = [];
private int $padding = 1;
public function __construct(
private ?ConsoleStyle $headerStyle = null,
private ?ConsoleStyle $rowStyle = null,
private ?ConsoleStyle $borderStyle = null,
private bool $showBorders = true
) {
$this->headerStyle ??= ConsoleStyle::create(color: ConsoleColor::BRIGHT_WHITE, format: ConsoleFormat::BOLD);
$this->rowStyle ??= ConsoleStyle::create();
$this->borderStyle ??= ConsoleStyle::create(color: ConsoleColor::GRAY);
}
/**
* Setzt die Spaltenüberschriften der Tabelle.
*/
public function setHeaders(array $headers): self
{
$this->headers = $headers;
$this->calculateColumnWidths();
return $this;
}
/**
* Fügt eine Zeile zur Tabelle hinzu.
*/
public function addRow(array $row): self
{
$this->rows[] = $row;
$this->calculateColumnWidths();
return $this;
}
/**
* Setzt alle Zeilen der Tabelle.
*/
public function setRows(array $rows): self
{
$this->rows = $rows;
$this->calculateColumnWidths();
return $this;
}
/**
* Setzt den Innenabstand der Zellen.
*/
public function setPadding(int $padding): self
{
$this->padding = max(0, $padding);
return $this;
}
/**
* Rendert die Tabelle und gibt den Text zurück.
*/
public function render(): string
{
if (empty($this->headers) && empty($this->rows)) {
return '';
}
$output = '';
if ($this->showBorders) {
$output .= $this->renderBorder('top') . "\n";
}
// Header
if (!empty($this->headers)) {
$output .= $this->renderRow($this->headers, $this->headerStyle) . "\n";
if ($this->showBorders) {
$output .= $this->renderBorder('middle') . "\n";
}
}
// Zeilen
foreach ($this->rows as $index => $row) {
$output .= $this->renderRow($row, $this->rowStyle) . "\n";
}
if ($this->showBorders) {
$output .= $this->renderBorder('bottom') . "\n";
}
return $output;
}
/**
* Rendert eine Zeile der Tabelle.
*/
private function renderRow(array $cells, ConsoleStyle $style): string
{
$output = '';
if ($this->showBorders) {
$output .= $this->borderStyle->apply('│');
}
foreach ($cells as $index => $cell) {
$width = $this->columnWidths[$index] ?? 10;
$cellText = (string)$cell;
// Linksbündige Ausrichtung mit exakter Breite
$paddedCell = str_pad($cellText, $width, ' ', STR_PAD_RIGHT);
// Padding hinzufügen
$padding = str_repeat(' ', $this->padding);
$finalCell = $padding . $paddedCell . $padding;
$output .= $style->apply($finalCell);
if ($this->showBorders) {
$output .= $this->borderStyle->apply('│');
}
}
return $output;
}
/**
* Rendert eine Trennlinie der Tabelle.
*/
private function renderBorder(string $type): string
{
$left = match($type) {
'top' => '┌',
'middle' => '├',
'row' => '├',
'bottom' => '└',
default => '│'
};
$right = match($type) {
'top' => '┐',
'middle' => '┤',
'row' => '┤',
'bottom' => '┘',
default => '│'
};
$horizontal = '─';
$junction = match($type) {
'top' => '┬',
'middle' => '┼',
'row' => '┼',
'bottom' => '┴',
default => '│'
};
$border = $left;
foreach ($this->columnWidths as $index => $width) {
$cellWidth = $width + ($this->padding * 2);
$border .= str_repeat($horizontal, $cellWidth);
if ($index < count($this->columnWidths) - 1) {
$border .= $junction;
}
}
$border .= $right;
return $this->borderStyle->apply($border);
}
/**
* Berechnet die Breite jeder Spalte basierend auf dem Inhalt.
*/
private function calculateColumnWidths(): void
{
$this->columnWidths = [];
// Header-Breiten
foreach ($this->headers as $index => $header) {
$this->columnWidths[$index] = mb_strlen((string)$header);
}
// Zeilen-Breiten
foreach ($this->rows as $row) {
foreach ($row as $index => $cell) {
$length = mb_strlen((string)$cell);
$this->columnWidths[$index] = max($this->columnWidths[$index] ?? 0, $length);
}
}
}
}

View File

@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Components;
use App\Framework\Console\ConsoleStyle;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleFormat;
/**
* Rendert eine Textbox in der Konsole.
*/
final readonly class TextBox
{
public function __construct(
private string $content,
private int $width = 80,
private int $padding = 1,
private ?ConsoleStyle $borderStyle = null,
private ?ConsoleStyle $contentStyle = null,
private ?string $title = null
) {}
public function render(): string
{
$borderStyle = $this->borderStyle ?? ConsoleStyle::create(color: ConsoleColor::GRAY);
$contentStyle = $this->contentStyle ?? ConsoleStyle::create();
$contentWidth = $this->width - ($this->padding * 2) - 2;
$lines = $this->wrapText($this->content, $contentWidth);
$output = '';
// Obere Linie
if ($this->title) {
$titleLength = mb_strlen($this->title);
$availableSpace = $this->width - $titleLength - 6; // Abzug für '[ ]' und Ränder
$leftPadding = max(0, (int)floor($availableSpace / 2));
$rightPadding = max(0, $availableSpace - $leftPadding);
$output .= $borderStyle->apply('┌' . str_repeat('─', $leftPadding) . '[ ' . $this->title . ' ]' . str_repeat('─', $rightPadding) . '┐') . "\n";
} else {
$output .= $borderStyle->apply('┌' . str_repeat('─', $this->width - 2) . '┐') . "\n";
}
// Padding oben
for ($i = 0; $i < $this->padding; $i++) {
$output .= $borderStyle->apply('│') . str_repeat(' ', $this->width - 2) . $borderStyle->apply('│') . "\n";
}
// Inhalt
foreach ($lines as $line) {
$lineLength = mb_strlen($line);
$spaces = $contentWidth - $lineLength;
$paddedLine = $line . str_repeat(' ', $spaces);
$output .= $borderStyle->apply('│') .
str_repeat(' ', $this->padding) .
$contentStyle->apply($paddedLine) .
str_repeat(' ', $this->padding) .
$borderStyle->apply('│') . "\n";
}
// Padding unten
for ($i = 0; $i < $this->padding; $i++) {
$output .= $borderStyle->apply('│') . str_repeat(' ', $this->width - 2) . $borderStyle->apply('│') . "\n";
}
// Untere Linie
$output .= $borderStyle->apply('└' . str_repeat('─', $this->width - 2) . '┘') . "\n";
return $output;
}
private function wrapText(string $text, int $width): array
{
$lines = explode("\n", $text);
$wrapped = [];
foreach ($lines as $line) {
if (mb_strlen($line) <= $width) {
$wrapped[] = $line;
} else {
$wrapped = array_merge($wrapped, $this->splitTextIntoLines($line, $width));
}
}
// Leere Zeile hinzufügen, falls keine Inhalte vorhanden
if (empty($wrapped)) {
$wrapped[] = '';
}
return $wrapped;
}
private function splitTextIntoLines(string $text, int $width): array
{
$lines = [];
$words = explode(' ', $text);
$currentLine = '';
foreach ($words as $word) {
$testLine = empty($currentLine) ? $word : $currentLine . ' ' . $word;
if (mb_strlen($testLine) <= $width) {
$currentLine = $testLine;
} else {
if (!empty($currentLine)) {
$lines[] = $currentLine;
$currentLine = $word;
} else {
// Wort ist länger als die Zeile - hart umbrechen
$lines[] = mb_substr($word, 0, $width);
$currentLine = mb_substr($word, $width);
}
}
}
if (!empty($currentLine)) {
$lines[] = $currentLine;
}
return $lines ?: [''];
}
}

View File

@@ -0,0 +1,215 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Components;
use App\Framework\Console\ConsoleStyle;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleFormat;
use App\Framework\Console\ConsoleOutput;
/**
* TreeHelper zum Anzeigen hierarchischer Baumstrukturen in der Konsole.
* Ähnlich dem Symfony TreeHelper, aber angepasst an unser Styling-System.
*/
final class TreeHelper
{
private string $prefix = '';
private bool $isLastElement = true;
private ?ConsoleStyle $nodeStyle = null;
private ?ConsoleStyle $leafStyle = null;
private ?ConsoleStyle $lineStyle = null;
/**
* @var array<array{title: string, node: ?self, isLeaf: bool}>
*/
private array $nodes = [];
public function __construct(
private string $title = '',
private ConsoleOutput $output = new ConsoleOutput(),
) {
$this->nodeStyle = ConsoleStyle::create(color: ConsoleColor::BRIGHT_YELLOW, format: ConsoleFormat::BOLD);
$this->leafStyle = ConsoleStyle::create(color: ConsoleColor::WHITE);
$this->lineStyle = ConsoleStyle::create(color: ConsoleColor::GRAY);
}
/**
* Setzt den Stil für Knotentitel (Verzeichnisse/Kategorien).
*/
public function setNodeStyle(?ConsoleStyle $style): self
{
$this->nodeStyle = $style;
return $this;
}
/**
* Setzt den Stil für Blätter/Endpunkte.
*/
public function setLeafStyle(?ConsoleStyle $style): self
{
$this->leafStyle = $style;
return $this;
}
/**
* Setzt den Stil für Baumlinien.
*/
public function setLineStyle(?ConsoleStyle $style): self
{
$this->lineStyle = $style;
return $this;
}
/**
* Setzt den Haupttitel des Baums.
*/
public function setTitle(string $title): self
{
$this->title = $title;
return $this;
}
/**
* Fügt einen Unterknoten (z.B. Unterverzeichnis) hinzu.
*/
public function addNode(string $title): self
{
$node = new self($title);
$node->nodeStyle = $this->nodeStyle;
$node->leafStyle = $this->leafStyle;
$node->lineStyle = $this->lineStyle;
$this->nodes[] = [
'title' => $title,
'node' => $node,
'isLeaf' => false
];
return $node;
}
/**
* Fügt einen Endpunkt (z.B. Datei) hinzu.
*/
public function addLeaf(string $title): self
{
$this->nodes[] = [
'title' => $title,
'node' => null,
'isLeaf' => true
];
return $this;
}
/**
* Zeigt die vollständige Baumstruktur an.
*/
public function display(): void
{
if (!empty($this->title)) {
$this->output->writeLine($this->title, $this->nodeStyle);
}
$this->displayTree();
}
/**
* Rendert die Baumstruktur und gibt den Text zurück.
*/
public function render(): string
{
$output = '';
if (!empty($this->title)) {
$output .= $this->nodeStyle->apply($this->title) . "\n";
}
$output .= $this->renderTree();
return $output;
}
/**
* Setzt den Präfix für die aktuelle Ebene.
* (Interne Methode für rekursives Rendern)
*/
private function setPrefix(string $prefix, bool $isLastElement): self
{
$this->prefix = $prefix;
$this->isLastElement = $isLastElement;
return $this;
}
/**
* Zeigt die Baumstruktur mit dem aktuellen Präfix an.
* (Interne Methode für rekursives Rendern)
*/
private function displayTree(): void
{
$count = count($this->nodes);
foreach ($this->nodes as $index => $item) {
$isLast = ($index === $count - 1);
$nodePrefix = $this->prefix . ($this->isLastElement ? ' ' : '│ ');
// Baumlinien und Verbindungen
$connector = $isLast ? '└── ' : '├── ';
$linePrefix = $this->prefix . $connector;
// Titel anzeigen
$style = $item['isLeaf'] ? $this->leafStyle : $this->nodeStyle;
$title = $linePrefix . $item['title'];
$this->output->writeLine(
$this->lineStyle->apply($linePrefix) .
$style->apply($item['title'])
);
// Unterelemente rekursiv anzeigen
if (!$item['isLeaf'] && $item['node'] !== null) {
$item['node']
->setPrefix($nodePrefix, $isLast)
->displayTree();
}
}
}
/**
* Rendert die Baumstruktur mit dem aktuellen Präfix und gibt den Text zurück.
* (Interne Methode für rekursives Rendern)
*/
private function renderTree(): string
{
$output = '';
$count = count($this->nodes);
foreach ($this->nodes as $index => $item) {
$isLast = ($index === $count - 1);
$nodePrefix = $this->prefix . ($this->isLastElement ? ' ' : '│ ');
// Baumlinien und Verbindungen
$connector = $isLast ? '└── ' : '├── ';
$linePrefix = $this->prefix . $connector;
// Titel formatieren
$style = $item['isLeaf'] ? $this->leafStyle : $this->nodeStyle;
$title = $item['title'];
$output .= $this->lineStyle->apply($linePrefix) .
$style->apply($title) . "\n";
// Unterelemente rekursiv rendern
if (!$item['isLeaf'] && $item['node'] !== null) {
$childOutput = $item['node']
->setPrefix($nodePrefix, $isLast)
->renderTree();
$output .= $childOutput;
}
}
return $output;
}
}