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 messageHash = Object.hashAll(
    messageEntries.map(
      (entry) => Object.hash(
        entry.timestamp,
        entry.messageType,
        entry.summary,
        entry.processingTime,
      ),
    ),
  );
  final outputHash = Object.hashAll(
    outputEntries.map(
      (entry) => Object.hash(entry.timestamp, entry.source, entry.line),
    ),
  );
  final cacheKey = Object.hash(
    currentFrame,
    m?.isIdle,
    panelWidth,
    terminalWidthOverride ?? terminalWidth,
    terminalHeight,
    title,
    rendererLabel,
    maxDisplayMessages,
    maxDisplayOutput,
    customHash,
    mode,
    messageHash,
    outputHash,
  );

  // 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();
  final tabs = DebugOverlayMode.values.map(_tabLabel).join(' ');
  content
    ..writeln(dim.render(tabs))
    ..writeln();

  // --- Metrics section ---
  if (mode == DebugOverlayMode.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;
    final fpsText = m == null
        ? 'pending'
        : m.isIdle
        ? 'idle'
        : avgFrameTimeUs == 0
        ? 'pending'
        : avgFps.toStringAsFixed(1);

    content
      ..writeln(
        '${label.render('FPS:')} $fpsText '
        '(${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) {
    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) {
    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',
  };

  final rendered = PanelComponent(
    title: panelTitle,
    content: content.toString().trimRight(),
    width: panelWidth,
    renderConfig: RenderConfig(
      // Keep panel construction valid in small terminals; composition
      // clips the completed panel to the actual viewport.
      terminalWidth: mathMax(
        tabs.length + 4,
        mathMax(panelWidth, terminalWidthOverride ?? terminalWidth),
      ),
    ),
  ).render();

  // Cache the result.
  _cachedPanel = rendered;
  _cachedCacheKey = cacheKey;
  final lines = rendered.split('\n');
  _cachedPanelHeight = lines.length;
  _cachedPanelWidth = lines.fold(
    0,
    (width, line) => mathMax(width, Style.visibleLength(line)),
  );

  return rendered;
}