diff --git a/src/Console/Application.php b/src/Console/Application.php index dd2e188..86a2f34 100644 --- a/src/Console/Application.php +++ b/src/Console/Application.php @@ -107,7 +107,7 @@ private function draw(Area $area): void { $widgets = [$this->window->render($area)]; - if ($this->popup->isActive()) { + if ($this->appState->popupOpen) { $widgets[] = $this->popup->render($area); } diff --git a/src/Console/Components/Header.php b/src/Console/Components/Header.php index 9e75803..b9ea5bc 100644 --- a/src/Console/Components/Header.php +++ b/src/Console/Components/Header.php @@ -17,10 +17,17 @@ class Header extends Component { protected function view(Area $area): Widget { + $waiting = $this->appState->pendingWaits > 0; $live = $this->appState->live; $unread = $this->appState->unread > 0; + [$dot, $label] = match (true) { + $waiting => [Span::fromString('⏳')->yellow(), 'WAIT'], + $live => [Span::fromString('●')->red(), 'LIVE'], + default => [Span::fromString('⏸')->blue(), 'PAUSED'], + }; + return BlockWidget::default() ->borders(Borders::BOTTOM) @@ -28,9 +35,9 @@ protected function view(Area $area): Widget ->widget( ParagraphWidget::fromSpans( Span::fromString(' '), - $live ? Span::fromString('●')->red() : Span::fromString('⏸')->blue(), + $dot, Span::fromString(' '), - Span::fromString($live ? 'LIVE' : 'PAUSED'), + Span::fromString($label), $unread ? Span::fromString(sprintf(' (↓%s)', $this->appState->unread))->yellow() : Span::fromString(''), Span::fromString(' | '), Span::fromString(sprintf('port: %s', $this->appState->port)), diff --git a/src/Console/Components/LogList.php b/src/Console/Components/LogList.php index c4d9c9d..16388fe 100644 --- a/src/Console/Components/LogList.php +++ b/src/Console/Components/LogList.php @@ -102,6 +102,27 @@ public function select(): void $this->appState->previewLog = $this->appState->logs()[$this->appState->cursor]; } + #[KeyPressed(KeyCode::Enter)] + public function selectUnlessWaiting(): void + { + if ($this->appState->pendingWaits > 0) { + return; + } + + $this->select(); + } + + #[KeyPressed('l', KeyModifiers::CONTROL, global: true)] + public function clear(): void + { + $this->appState->previewLog = null; + $this->appState->live = true; + $this->appState->logs = []; + $this->appState->cursor = 0; + $this->appState->offset = 0; + $this->appState->unread = 0; + } + #[KeyPressed(KeyCode::Esc)] public function backToLive(): void { diff --git a/src/Console/Components/Navigation.php b/src/Console/Components/Navigation.php index e1810f4..4b93ac6 100644 --- a/src/Console/Components/Navigation.php +++ b/src/Console/Components/Navigation.php @@ -21,6 +21,8 @@ protected function view(Area $area): Widget { $this->area = $area; + $waiting = $this->appState->pendingWaits > 0; + $buttonStyle = Style::default()->fg(RgbColor::fromHex('7aa2f7')); $descStyle = Style::default()->fg(RgbColor::fromHex('7aa2f7')); $delimiter = Span::styled(' | ', $descStyle); @@ -36,20 +38,17 @@ protected function view(Area $area): Widget if ($this->appState->previewLog) { $instruction[] = Span::styled('Back: ', $descStyle); $instruction[] = Span::styled('backspace', $buttonStyle); - $instruction[] = $delimiter; + } elseif ($waiting) { + $instruction[] = Span::styled('Continue: ', $descStyle); + $instruction[] = Span::styled('enter', $buttonStyle); } else { $instruction[] = Span::styled('Details: ', $descStyle); - $instruction[] = Span::styled('space', $buttonStyle); - $instruction[] = $delimiter; - + $instruction[] = Span::styled('enter/space', $buttonStyle); } - $instruction[] = Span::styled('Continue: ', $descStyle); - $instruction[] = Span::styled('enter', $buttonStyle); $instruction[] = $delimiter; - - $instruction[] = Span::styled('Navigation: ', $descStyle); - $instruction[] = Span::styled('↑/↓', $buttonStyle); + $instruction[] = Span::styled('Shortcuts: ', $descStyle); + $instruction[] = Span::styled('?', $buttonStyle); $instruction[] = $delimiter; $instruction[] = Span::styled('Quit: ', $descStyle); diff --git a/src/Console/State/AppState.php b/src/Console/State/AppState.php index a9f9a09..182c94e 100644 --- a/src/Console/State/AppState.php +++ b/src/Console/State/AppState.php @@ -19,6 +19,8 @@ * @property array $logs * @property int $detailsCursor * @property int $detailsOffset + * @property int $pendingWaits + * @property bool $popupOpen */ class AppState { @@ -46,6 +48,8 @@ public function __construct( private array $logs = [], private int $detailsCursor = 0, private int $detailsOffset = 0, + private int $pendingWaits = 0, + private bool $popupOpen = false, ) {} /** diff --git a/src/Console/Windows/Main.php b/src/Console/Windows/Main.php index 026146b..5f8d2de 100644 --- a/src/Console/Windows/Main.php +++ b/src/Console/Windows/Main.php @@ -37,6 +37,10 @@ public function afterInit(): void $this->appState->observe('previewLog', function (?LogItem $log) { Loop::futureTick(function () use ($log) { + if ($this->appState->popupOpen) { + return; + } + if ($log) { $this->componentInstances[Details::class]->activate(); $this->componentInstances[LogList::class]->deactivate(); @@ -46,6 +50,23 @@ public function afterInit(): void } }); }); + + $this->appState->observe('popupOpen', function (bool $open) { + Loop::futureTick(function () use ($open) { + if ($open) { + $this->componentInstances[Details::class]->deactivate(); + $this->componentInstances[LogList::class]->deactivate(); + + return; + } + + if ($this->appState->previewLog) { + $this->componentInstances[Details::class]->activate(); + } else { + $this->componentInstances[LogList::class]->activate(); + } + }); + }); } protected function view(Area $area): Widget diff --git a/src/Console/Windows/Popup.php b/src/Console/Windows/Popup.php index 286f5a6..4c69800 100644 --- a/src/Console/Windows/Popup.php +++ b/src/Console/Windows/Popup.php @@ -4,15 +4,83 @@ namespace Tapper\Console\Windows; +use PhpTui\Term\KeyCode; +use PhpTui\Tui\Color\RgbColor; use PhpTui\Tui\Display\Area; use PhpTui\Tui\Extension\Core\Widget\BlockWidget; +use PhpTui\Tui\Extension\Core\Widget\Buffer\BufferContext; +use PhpTui\Tui\Extension\Core\Widget\BufferWidget; +use PhpTui\Tui\Extension\Core\Widget\ParagraphWidget; +use PhpTui\Tui\Style\Style; +use PhpTui\Tui\Text\Line; +use PhpTui\Tui\Text\Span; +use PhpTui\Tui\Text\Title; +use PhpTui\Tui\Widget\Borders; +use PhpTui\Tui\Widget\BorderType; +use PhpTui\Tui\Widget\HorizontalAlignment; use PhpTui\Tui\Widget\Widget; +use Tapper\Console\CommandAttributes\KeyPressed; use Tapper\Console\Component; class Popup extends Component { + #[KeyPressed('?', global: true)] + public function toggle(): void + { + $this->appState->popupOpen = ! $this->appState->popupOpen; + } + + #[KeyPressed(KeyCode::Esc, global: true)] + public function close(): void + { + if ($this->appState->popupOpen) { + $this->appState->popupOpen = false; + } + } + protected function view(Area $area): Widget { - return BlockWidget::default(); + $keyStyle = Style::default()->fg(RgbColor::fromHex('7aa2f7')); + $descStyle = Style::default()->fg(RgbColor::fromHex('c0caf5')); + + $shortcuts = [ + ['↑ / k', '↓ / j', 'Move cursor'], + ['Ctrl+u', 'Ctrl+d', 'Page up / down'], + ['space', 'enter', 'Open details'], + ['enter', '', 'Continue a paused wait'], + ['backspace', 'esc', 'Close details / back to live'], + ['ctrl+l', '', 'Clear all logs'], + ['?', '', 'Toggle this help'], + ['q', '', 'Quit'], + ]; + + $lines = array_map( + fn (array $row): Line => Line::fromSpans( + Span::styled(sprintf('%-10s', $row[0]), $keyStyle), + Span::styled(sprintf('%-10s', $row[1]), $keyStyle), + Span::styled($row[2], $descStyle), + ), + $shortcuts, + ); + + $width = min($area->width, 46); + $height = min($area->height, count($lines) + 2); + + $popupArea = Area::fromScalars( + $area->position->x + intdiv(max(0, $area->width - $width), 2), + $area->position->y + intdiv(max(0, $area->height - $height), 2), + $width, + $height, + ); + + $block = BlockWidget::default() + ->borders(Borders::ALL) + ->borderType(BorderType::Rounded) + ->titles(Title::fromString(' Shortcuts ')->horizontalAlignment(HorizontalAlignment::Center)) + ->widget(ParagraphWidget::fromLines(...$lines)); + + return BufferWidget::new(function (BufferContext $context) use ($block, $popupArea): void { + $context->draw($block, $popupArea); + }); } } diff --git a/src/Server.php b/src/Server.php index be83918..be9ae2c 100644 --- a/src/Server.php +++ b/src/Server.php @@ -83,13 +83,17 @@ public function run(): void )); self::$id++; + $this->appState->pendingWaits++; - $this->eventBus->listen(KeyCode::Enter, fn () => ($encoder->write([ - 'jsonrpc' => '2.0', - 'result' => 'continue', - 'id' => $id, - ]) - )); + $this->eventBus->listen(KeyCode::Enter, function () use ($encoder, $id) { + $encoder->write([ + 'jsonrpc' => '2.0', + 'result' => 'continue', + 'id' => $id, + ]); + + $this->appState->pendingWaits = max(0, $this->appState->pendingWaits - 1); + }); break;