restore semantic bold rendering in terminal panes (T-73)

Bold attributes from terminal escapes now render in a real bold
weight instead of being silently flattened.

- pubspec.yaml: register JetBrainsMono Bold + BoldItalic at
  weight 700 under family JetBrainsMono. Files already shipped on
  disk; only the registration was missing.
- assets/licenses.yaml: bump JetBrainsMono weights_bundled to
  [Regular, Italic, Bold, BoldItalic] per D-42 (the entry must
  match what is actually wired into the family).
- lib/src/terminal/src/ui/painter.dart: revert the `bold: false`
  override and drop the workaround comment. Bold now flows from
  CellFlags.bold to TextStyle.fontWeight.
- test/terminal/painter_bold_metrics_test.dart: load Regular and
  Bold via FontLoader and assert paragraph maxIntrinsicWidth is
  identical (cell-grid drift = 0). JetBrainsMono Bold's monospace
  by spec; this test is the canary for the day someone swaps the
  font.
- test/goldens/goldens/{ci,linux}/clide_button.png: regenerate.
  ClideButton's label renders slightly heavier on the bold variant
  (expected — 0.28% pixel diff before regen).

Earlier perception of over-bolding in the Claude pane was
synthetic-bold smearing (Flutter overpaints when no Bold.ttf is
registered for the family), not legitimate bold rendering. Visual
A/B confirms a real Bold face renders crisp emphasis without the
smear, so no per-pane renderer config is needed.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-05-06 22:29:23 +02:00
co-authored by Claude
parent 699bd40423
commit a67a768592
8 changed files with 70 additions and 11 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
{
"exported_at": "2026-05-06T20:29:05Z",
"exported_at": "2026-05-06T20:29:23Z",
"decisions": [
{
"id": "D-1",
+5
View File
@@ -39,6 +39,11 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
### Changed
- Terminal panes now render bold attributes with a real bold weight —
bundled JetBrainsMono Bold + BoldItalic are registered with the
`JetBrainsMono` family at `weight: 700`. The painter's bold
suppression workaround (added when only Regular + Italic were wired
and Flutter's synthetic bold drifted advance widths) is gone.
- Claude pane uses `MultitabPane` for primary + secondaries — drops
~100 lines of bespoke tab-strip code, gains drag-to-reorder.
- UI spacing constants live in `lib/widgets/src/spacing.dart`
+1 -1
View File
@@ -53,7 +53,7 @@ dependencies:
purpose: >-
Monospace face for terminal panes, diff views, code editors, and
any other monospace surface.
weights_bundled: [Regular, Italic]
weights_bundled: [Regular, Italic, Bold, BoldItalic]
- name: Josefin Sans
kind: font
+3 -9
View File
@@ -126,8 +126,7 @@ class TerminalPainter {
@pragma('vm:prefer-inline')
void paintHighlight(Canvas canvas, Offset offset, int length, Color color) {
final endOffset =
offset.translate(length * _cellSize.width, _cellSize.height);
final endOffset = offset.translate(length * _cellSize.width, _cellSize.height);
final paint = Paint()
..color = color
@@ -182,20 +181,15 @@ class TerminalPainter {
if (paragraph == null) {
final cellFlags = cellData.flags;
var color = cellFlags & CellFlags.inverse == 0
? resolveForegroundColor(cellData.foreground)
: resolveBackgroundColor(cellData.background);
var color = cellFlags & CellFlags.inverse == 0 ? resolveForegroundColor(cellData.foreground) : resolveBackgroundColor(cellData.background);
if (cellData.flags & CellFlags.faint != 0) {
color = color.withOpacity(0.5);
}
// Skip bold rendering — Flutter's synthetic bold (no Bold.ttf
// registered) drifts glyph advance widths slightly, breaking
// the cell grid. Color is enough to convey emphasis in TUIs.
final style = _textStyle.toTextStyle(
color: color,
bold: false,
bold: cellFlags & CellFlags.bold != 0,
italic: cellFlags & CellFlags.italic != 0,
underline: cellFlags & CellFlags.underline != 0,
);
+5
View File
@@ -95,3 +95,8 @@ flutter:
- asset: assets/fonts/jetbrains_mono/JetBrainsMono-Regular.ttf
- asset: assets/fonts/jetbrains_mono/JetBrainsMono-Italic.ttf
style: italic
- asset: assets/fonts/jetbrains_mono/JetBrainsMono-Bold.ttf
weight: 700
- asset: assets/fonts/jetbrains_mono/JetBrainsMono-BoldItalic.ttf
weight: 700
style: italic
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

@@ -0,0 +1,55 @@
/// Verifies that the bundled JetBrainsMono Bold face has identical
/// advance widths to Regular at our render size — required for the
/// terminal cell grid to stay stable when bold attributes flip on.
///
/// If this fails, the workaround in painter.dart was deactivated
/// against a font that drifts; pick one of the alternatives in T-73
/// (variable JetBrainsMono / Berkeley Mono / IBM Plex Mono).
library;
import 'dart:io';
import 'dart:ui' as ui;
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
const _family = 'JetBrainsMonoTest';
const _fontSize = 14.0;
const _sample = 'mmmmmmmmmm';
void main() {
setUpAll(() async {
final regular = await File('assets/fonts/jetbrains_mono/JetBrainsMono-Regular.ttf').readAsBytes();
final bold = await File('assets/fonts/jetbrains_mono/JetBrainsMono-Bold.ttf').readAsBytes();
final loader = FontLoader(_family)
..addFont(Future.value(ByteData.sublistView(regular)))
..addFont(Future.value(ByteData.sublistView(bold)));
await loader.load();
});
test('JetBrainsMono Bold advance width matches Regular (cell drift = 0)', () {
final regularWidth = _measure(FontWeight.normal);
final boldWidth = _measure(FontWeight.bold);
expect(boldWidth, regularWidth,
reason: 'Bold advance width must equal Regular at $_fontSize px '
'or the terminal cell grid drifts when bold flips on.');
});
}
double _measure(FontWeight weight) {
final builder = ui.ParagraphBuilder(ui.ParagraphStyle(
fontFamily: _family,
fontSize: _fontSize,
))
..pushStyle(ui.TextStyle(
fontFamily: _family,
fontWeight: weight,
fontSize: _fontSize,
))
..addText(_sample);
final paragraph = builder.build()..layout(const ui.ParagraphConstraints(width: double.infinity));
final width = paragraph.maxIntrinsicWidth;
paragraph.dispose();
return width;
}