From a67a768592c7617063204914c362098051c52ecc Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 6 May 2026 22:29:23 +0200 Subject: [PATCH] restore semantic bold rendering in terminal panes (T-73) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .pql/pql-plan.json | 2 +- CHANGELOG.md | 5 ++ assets/licenses.yaml | 2 +- lib/src/terminal/src/ui/painter.dart | 12 +--- pubspec.yaml | 5 ++ test/goldens/goldens/ci/clide_button.png | Bin 2455 -> 2391 bytes test/goldens/goldens/linux/clide_button.png | Bin 16540 -> 16495 bytes test/terminal/painter_bold_metrics_test.dart | 55 +++++++++++++++++++ 8 files changed, 70 insertions(+), 11 deletions(-) create mode 100644 test/terminal/painter_bold_metrics_test.dart diff --git a/.pql/pql-plan.json b/.pql/pql-plan.json index 9d1dab87..fd1df1e9 100644 --- a/.pql/pql-plan.json +++ b/.pql/pql-plan.json @@ -1,5 +1,5 @@ { - "exported_at": "2026-05-06T20:29:05Z", + "exported_at": "2026-05-06T20:29:23Z", "decisions": [ { "id": "D-1", diff --git a/CHANGELOG.md b/CHANGELOG.md index f35ca2e3..6e83cc6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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` — diff --git a/assets/licenses.yaml b/assets/licenses.yaml index 70284287..ba27e0ff 100644 --- a/assets/licenses.yaml +++ b/assets/licenses.yaml @@ -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 diff --git a/lib/src/terminal/src/ui/painter.dart b/lib/src/terminal/src/ui/painter.dart index 500f3221..a4376257 100644 --- a/lib/src/terminal/src/ui/painter.dart +++ b/lib/src/terminal/src/ui/painter.dart @@ -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, ); diff --git a/pubspec.yaml b/pubspec.yaml index dc56967d..d76df55b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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 diff --git a/test/goldens/goldens/ci/clide_button.png b/test/goldens/goldens/ci/clide_button.png index 008f3e4e41220be5302c7855f1982ca389c27bbe..7c8b9051b40db11c530e8ab91d650eba36fb6c75 100644 GIT binary patch delta 1866 zcma)-c{tnI8plJ3dy6q9YAmr#O^2d-RXVmB+G4I@q=X<8wNG^{wM->6lo_>!PHyAY z7NtajTFOL4#HgiJTW+HiNlY_JV~t3oXfnU_xzBU^=RME!{&D_z-*evc{eIq44xRn1 z>>N(P!}Xls^{S0U&%}^egHfTZ%0A^RA$c9XOU(N&fzpIg{r=g+rR+SVK7}E3O`K z4OZCmM8%D1yn^lw5PV>4zldUbSegOL5*^F$cqf_l)litT9mQwCt$t)`)-$TvNYLBw zZ(~5fTC^mTD(DdK3c4SKp7FKl_rq*`kRJ6Y*Cc1b<3$JNl!HLJt^eriyPO@ zK=KXo$r33W8UDp^swt{XdUjrn@`~ZJQ-Ev+?C~agU2KGKQr7lOSJCeXT_3*7Cr7kC zM=lBA!*u3E_u!m2NY&Pt@aKshZ7KO4IDnKEL|E{3*`X8&{n?!x| z+Ae>bbxkl@ zg~cd;dM}JZ9ynk4v;_~X4iy-{7tv%RLQpjZ0&CAW+nH)6{@dy8i_5?NT(qn_ zX(=?!xf-Lmm~JyE1%f}CiyLmHCz5LvRTnC0uju7gBX{1|iM8|ugR8!WO9JOea!R7$M0?qbQRenw zUkk0y#$f2`Hy(gd%+b-s3;B`09DPVoYJ91m&{CjX-kqsKuG@%N}wB+@O7oTPZx5^IAvLY?$T)E?5tU;OFwhWl?_F`I;Wa+V8&@z z2q85oS48}<+!2V*SSN;&7vEvkAmx#NN)#d43%A0YA%uSA6CcSUmj_~wT4SBc!qs4u zTtwsi)k^2W4QQCRlTtXZrB6(l3wj^uMRi0X0J6OaQ_4C(3KVyz^`%*Ft}3gU7IAvP zQ~nl34Rfvd;kb8x<3$Mq`=nZjdZyc%!wX&H03_nxy~d5lJ`$sn{}G8P$7x41j<){0 zvE~AJqA;`=YgE9H^5woB+YC2(QN*xIIrM8u+owpTT`KvPt2q5SD*&nVom8MYuC_M- zryF3!gkrH+Ytj^Q>fU8^E6VcZCH%}A7xu3u4e`v9Kel~C^g@eXUL|5aQTOxrZ{b?w zGgw8vl5m0jc7I_gpjVYI`nr6aqup{3DI^Z~)S6Z6vPjIEa>$vJ#31!rKWP&I{>j*Cf3ke@=-7 zEtdVi_^~Ma^5(qL%z)+s>2VD;E|9U#;*5n@;|#O(W-Kr|WBRPQ9*m!V=?(laNO6EM zYZ-@xjnk(AhhLBTSC y6)&^jf4>|zY-TBj>BXUTBNnm70&cFnuvAsibH)ok-;gT*nmpXRT_2nezxfvu4!CCk delta 1896 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 literal 16540 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 diff --git a/test/terminal/painter_bold_metrics_test.dart b/test/terminal/painter_bold_metrics_test.dart new file mode 100644 index 00000000..80d8b5f1 --- /dev/null +++ b/test/terminal/painter_bold_metrics_test.dart @@ -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; +}