panel method

String panel({
  1. int? terminalWidthOverride,
})

Renders just the debug panel (cached for performance).

Implementation

String panel({int? terminalWidthOverride}) {
  final m = metrics;
  final currentFrame = m?.frameCount ?? 0;
  final customHash = Object.hashAll(
    customMetrics.entries.map((e) => Object.hash(e.key, e.value)),
  );

  // Include mode, message count, and output count in cache key.
  final msgCount = messageEntries.length;
  final outCount = outputEntries.length;
  final cacheKey = Object.hash(
    currentFrame,
    panelWidth,
    customHash,
    mode,
    msgCount,
    outCount,
  );

  // Use cached panel if nothing has changed.
  if (_cachedPanel != null && _cachedCacheKey == cacheKey) {
    return _cachedPanel!;
  }

  final label = Style().foreground(Colors.yellow).bold();
  final dim = Style().dim();
  final content = StringBuffer();

  // --- Metrics section ---
  if (mode == DebugOverlayMode.metrics || mode == DebugOverlayMode.all) {
    if (mode == DebugOverlayMode.all) {
      content.writeln(dim.render('── Metrics ──'));
    }
    final avgFps = m?.averageFps ?? 0.0;
    final avgFrameTimeUs = m?.averageFrameTime.inMicroseconds ?? 0;
    final avgRenderTimeUs = m?.averageRenderDuration.inMicroseconds ?? 0;
    final frameCount = m?.frameCount ?? 0;
    final skippedFrames = m?.skippedFrames ?? 0;
    final renderPct = m?.renderTimePercentage ?? 0.0;

    content
      ..writeln(
        '${label.render('FPS:')} ${avgFps.toStringAsFixed(1)} '
        '(${m?.minFps.toStringAsFixed(0) ?? 0}-${m?.maxFps.toStringAsFixed(0) ?? 0})',
      )
      ..writeln(
        '${label.render('Frame Time:')} ${(avgFrameTimeUs / 1000).toStringAsFixed(2)}ms',
      )
      ..writeln(
        '${label.render('Render Time:')} $avgRenderTimeUsµs '
        '(${renderPct.toStringAsFixed(1)}%)',
      )
      ..writeln(
        '${label.render('Frames:')} $frameCount (skipped: $skippedFrames)',
      )
      ..writeln('${label.render('Cells:')} ${terminalWidth * terminalHeight}')
      ..writeln('${label.render('Renderer:')} $rendererLabel');

    for (final entry in customMetrics.entries) {
      content.writeln('${label.render('${entry.key}:')} ${entry.value}');
    }
  }

  // --- Messages section ---
  if (mode == DebugOverlayMode.messages || mode == DebugOverlayMode.all) {
    if (content.isNotEmpty) content.writeln();
    if (mode == DebugOverlayMode.all) {
      content.writeln(dim.render('── Messages ──'));
    }
    if (messageEntries.isEmpty) {
      content.writeln(dim.render('(no messages)'));
    } else {
      final display = messageEntries.take(maxDisplayMessages);
      for (final entry in display) {
        final elapsed = entry.processingTime.inMicroseconds;
        final line = '${entry.summary}  ${dim.render('$elapsed\u00b5s')}';
        // Truncate to fit panel width (leaving room for borders + padding).
        final maxLen = panelWidth - 4;
        if (Style.visibleLength(line) > maxLen && maxLen > 3) {
          content.writeln(
            '${_truncateVisible(line, maxLen - 3)}${dim.render('...')}',
          );
        } else {
          content.writeln(line);
        }
      }
      if (messageEntries.length > maxDisplayMessages) {
        content.writeln(
          dim.render('(+${messageEntries.length - maxDisplayMessages} more)'),
        );
      }
    }
  }

  // --- Output section ---
  if (mode == DebugOverlayMode.output || mode == DebugOverlayMode.all) {
    if (content.isNotEmpty) content.writeln();
    if (mode == DebugOverlayMode.all) {
      content.writeln(dim.render('── Output ──'));
    }
    if (outputEntries.isEmpty) {
      content.writeln(dim.render('(no output)'));
    } else {
      final display = outputEntries.take(maxDisplayOutput);
      for (final entry in display) {
        final prefix = entry.source.name == 'stderr'
            ? Style().foreground(Colors.red).render('err')
            : '';
        final lineText = prefix.isNotEmpty
            ? '$prefix ${entry.line}'
            : entry.line;
        final maxLen = panelWidth - 4;
        if (Style.visibleLength(lineText) > maxLen && maxLen > 3) {
          content.writeln(
            '${_truncateVisible(lineText, maxLen - 3)}${dim.render('...')}',
          );
        } else {
          content.writeln(lineText);
        }
      }
      if (outputEntries.length > maxDisplayOutput) {
        content.writeln(
          dim.render('(+${outputEntries.length - maxDisplayOutput} more)'),
        );
      }
    }
  }

  // Determine panel title based on mode.
  final panelTitle = switch (mode) {
    DebugOverlayMode.metrics => title,
    DebugOverlayMode.messages => 'Messages',
    DebugOverlayMode.output => 'Captured Output',
    DebugOverlayMode.all => 'Debug',
  };

  final rendered = PanelComponent(
    title: panelTitle,
    content: content.toString().trimRight(),
    width: panelWidth,
    renderConfig: RenderConfig(
      terminalWidth: terminalWidthOverride ?? terminalWidth,
    ),
  ).render();

  // Cache the result.
  _cachedPanel = rendered;
  _cachedCacheKey = cacheKey;
  final lines = rendered.split('\n');
  _cachedPanelHeight = lines.length;

  return rendered;
}