panel method
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;
}