Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/rpc-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,12 @@ Built by `Rpc\JsonRpcRequest::payload()`:
| `trace` | array of `{file, line}` | backtrace, `DEBUG_BACKTRACE_IGNORE_ARGS`, first 2 frames skipped |
| `rootDir` | string\|null | resolved via Composer's `ClassLoader` file location |
| `code` | array of `{number, line, active}` | ±3 lines of source around the caller line |
| `kind` | string\|null | `null`/absent for a normal value; `"error"` when `tp()` was called with a `Throwable` — in that case `message` is a preformatted string (not JSON-encoded) and `caller`/`trace`/`code` describe where the exception was thrown, not the `tp()` call site |

Server response: `{"jsonrpc":"2.0","result":"ok","id":<id>}`.

Consecutive `log`/`wait` calls with identical `kind`+`message`+`caller` are coalesced server-side (`AppState::appendLog()`) into one entry with an incrementing `LogItem::$repeatCount`, instead of appending a new row per call — this is what a UI-facing "×N" counter renders from.

### `wait` params

Same shape as `log`. Server behavior differs: it still calls `AppState::appendLog()` immediately (so the "paused" message shows up right away), but **does not reply immediately**. Instead it registers a listener on `EventBus` for `KeyCode::Enter` that writes the deferred reply the next time Enter is pressed anywhere in the TUI:
Expand Down
12 changes: 12 additions & 0 deletions examples/DebugMenu.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@
$line = fgets(STDIN);
tp(trim($line ?: ''));
}],
'9' => ['label' => 'Exception (tp($throwable))', 'run' => function () {
try {
throw new \RuntimeException('Something went wrong while processing the request');
} catch (\Throwable $e) {
tp($e);
}
}],
'10' => ['label' => 'Repeated identical log (×N counter test)', 'run' => function () {
for ($i = 0; $i < 5; $i++) {
tp('Same message every time');
}
}],
];

echo "Tapper dev menu — make sure `php bin/tapper` is running in another terminal.\n";
Expand Down
10 changes: 7 additions & 3 deletions src/Console/Components/Details.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use PhpTui\Term\KeyModifiers;
use PhpTui\Term\MouseButton;
use PhpTui\Term\MouseEventKind;
use PhpTui\Tui\Color\RgbColor;
use PhpTui\Tui\Display\Area;
use PhpTui\Tui\Extension\Core\Widget\CompositeWidget;
use PhpTui\Tui\Extension\Core\Widget\List\ListItem;
Expand All @@ -25,6 +26,7 @@
use Tapper\Console\CommandAttributes\Mouse;
use Tapper\Console\Component;
use Tapper\Console\MessageFormatter;
use Tapper\Console\Palette;
use Tapper\Console\Support\Scroll;

class Details extends Component
Expand Down Expand Up @@ -137,9 +139,11 @@ protected function view(Area $area): Widget
];
$infoListItems = array_map(fn ($line) => ListItem::new(Text::fromLine($line)), $info);

$formattedMessage = $log->kind === 'wait'
? [Line::fromSpan(Span::styled($log->message, Style::default()->yellow()))]
: MessageFormatter::colorizeFormattedJson($log->message);
$formattedMessage = match ($log->kind) {
'wait' => [Line::fromSpan(Span::styled($log->message, Style::default()->yellow()))],
'error' => [Line::fromSpan(Span::styled($log->message, Style::default()->fg(RgbColor::fromHex(Palette::ERROR))))],
default => MessageFormatter::colorizeFormattedJson($log->message),
};
$formattedListItems = array_map(fn ($line) => ListItem::new(Text::fromLine($line)), $formattedMessage);

$allItems = [
Expand Down
20 changes: 16 additions & 4 deletions src/Console/Components/LogItem.php
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,23 @@ protected function view(Area $area): Widget
Span::styled($time, Style::default()->fg(RgbColor::fromHex(Palette::ACCENT))),
);

$messageSpans = $this->log->kind === 'wait'
? [Span::styled($message, Style::default()->yellow())]
: MessageFormatter::colorizeInlineJson($message);
$messageSpans = match ($this->log->kind) {
'wait' => [Span::styled($message, Style::default()->yellow())],
'error' => [Span::styled($message, Style::default()->fg(RgbColor::fromHex(Palette::ERROR)))],
default => MessageFormatter::colorizeInlineJson($message),
};

$wMess = ParagraphWidget::fromSpans(...$this->truncateSpans($messageSpans, $messageWidth, $darkGray));
$repeatBadge = $this->log->repeatCount > 1
? Span::styled(sprintf(' ×%d', $this->log->repeatCount), Style::default()->fg(RgbColor::fromHex(Palette::ACCENT)))
: null;

$messageSpans = $this->truncateSpans($messageSpans, $messageWidth - ($repeatBadge?->width() ?? 0), $darkGray);

if ($repeatBadge) {
$messageSpans[] = $repeatBadge;
}

$wMess = ParagraphWidget::fromSpans(...$messageSpans);
$wFile = ParagraphWidget::fromSpans(
...$this->truncateSpans([Span::styled(sprintf('↪ %s', $this->log->caller), $darkGray)], $messageWidth, $darkGray),
);
Expand Down
21 changes: 19 additions & 2 deletions src/Console/State/AppState.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,24 @@ public function setOnChange(callable $change): void
$this->change = $change;
}

public function appendLog(LogItem $logItem): void
/**
* @return bool true if a new entry was appended, false if it was merged
* into the previous identical entry (its repeat counter bumped)
*/
public function appendLog(LogItem $logItem): bool
{
$this->logs[] = $logItem;
$last = $this->logs[count($this->logs) - 1] ?? null;
$isRepeat = $last
&& $last->kind === $logItem->kind
&& $last->message === $logItem->message
&& $last->caller === $logItem->caller;

if ($isRepeat) {
$last->repeatCount++;
} else {
$this->logs[] = $logItem;
}

if (! $this->batching) {
$this->notifyChange();
$this->callObservers('logs');
Expand All @@ -76,6 +91,8 @@ public function appendLog(LogItem $logItem): void
if ($this->batching) {
$this->changed[] = 'logs';
}

return ! $isRepeat;
}

public function observe(string $name, callable $callable): void
Expand Down
1 change: 1 addition & 0 deletions src/Console/State/LogItem.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ public function __construct(
public string $rootDir,
public array $code,
public string $kind = 'log',
public int $repeatCount = 1,
) {}
}
31 changes: 30 additions & 1 deletion src/Runtime/Tapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,16 @@ class Tapper

private string $type = 'log';

private ?string $kind = null;

private array $trace = [];

private ?string $rootDir = null;

private array $callerToGet;

private ?array $code = null;

public function __construct()
{
if (self::$client === null) {
Expand All @@ -36,11 +40,35 @@ public function __construct()

public function tap(mixed $message): self
{
if ($message instanceof \Throwable) {
return $this->tapThrowable($message);
}

$this->message = $message;

return $this;
}

private function tapThrowable(\Throwable $throwable): self
{
$file = $throwable->getFile();
$line = $throwable->getLine();

$this->kind = 'error';
$this->message = sprintf('✕ %s: %s', get_class($throwable), $throwable->getMessage());
$this->caller = sprintf('%s:%s', basename($file), $line);
$this->trace = [
['file' => $file, 'line' => $line],
...array_map(fn (array $frame): array => [
'file' => $frame['file'] ?? null,
'line' => $frame['line'] ?? null,
], $throwable->getTrace()),
];
$this->code = $this->getCodeExcerpt($file, $line);

return $this;
}

public function wait(): self
{
$this->type = 'wait';
Expand Down Expand Up @@ -119,7 +147,8 @@ private function send(): void
'microtime' => $this->microtime,
'trace' => $this->trace,
'rootDir' => $this->rootDir,
'code' => $this->getCodeExcerpt($this->callerToGet['file'], $this->callerToGet['line']),
'code' => $this->code ?? $this->getCodeExcerpt($this->callerToGet['file'], $this->callerToGet['line']),
'kind' => $this->kind,
];

$request = new JsonRpcRequest(
Expand Down
17 changes: 12 additions & 5 deletions src/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,16 @@ public function run(): void

switch ($method) {
case 'log':
$this->appState->appendLog(new LogItem(
$kind = $params['kind'] ?? 'log';
$isAppended = $this->appState->appendLog(new LogItem(
self::$id,
$params['microtime'],
json_encode($params['message'], JSON_UNESCAPED_UNICODE),
$kind === 'error' ? $params['message'] : json_encode($params['message'], JSON_UNESCAPED_UNICODE),
$params['caller'],
$params['trace'],
$params['rootDir'],
$params['code'],
kind: $kind,
));

$encoder->write([
Expand All @@ -68,11 +70,13 @@ public function run(): void
'id' => $id,
]);

self::$id++;
if ($isAppended) {
self::$id++;
}
break;

case 'wait':
$this->appState->appendLog(new LogItem(
$isAppended = $this->appState->appendLog(new LogItem(
self::$id,
$params['microtime'],
"⏸ {$params['message']} — press ENTER to continue",
Expand All @@ -83,7 +87,10 @@ public function run(): void
kind: 'wait',
));

self::$id++;
if ($isAppended) {
self::$id++;
}

$this->appState->pendingWaits++;

$this->eventBus->listen(KeyCode::Enter, function () use ($encoder, $id) {
Expand Down
Loading