From ecf8ee56af90d1880865d3eee2f9dc27d0235bc8 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Fri, 8 May 2026 11:28:17 +0200 Subject: [PATCH] test sweep: cover ui/ pure-Dart files (T-91) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds test/terminal/ui/ui_pure_test.dart — 39 unit tests covering the lib/src/terminal/src/ui/ files that don't need a Flutter widget harness. Files at 100%: - palette_builder.dart: 0/52 → 52/52 (named-colour mapping for indices 0..15, RGB cube walk for 16..231 with the 95-then-+40 step pattern, grayscale ramp 232..255, out-of-range clamp). - paragraph_cache.dart: 2/22 → 22/22 (LRU promotion on read, eviction at maxSize, miss returns null, clear empties). - terminal_text_style.dart: 7/19 → 19/19 (default ctor, fromTextStyle fontFamily fallback chain + size/height pass- through, toTextStyle attribute threading, copyWith with override + no-args clone). - input_map.dart: 8/20 → 20/20 (keyToTerminalKey hits + null on unmapped LogicalKeyboardKey id, charToTerminalKey case-fold + multi-char/unknown null). - terminal_size.dart: 4/8 → 8/8 (operator==/hashCode/toString, identity + type-mismatch branches). - pointer_input.dart: 1/3 → 3/3 (default ctor, .none(), .all()). - terminal_theme.dart: 1/1 unchanged. - char_metrics.dart: 0/10 → 10/10 (positive width/height, monotonic text-scaler scaling). - ui/controller.dart: 18/52 → 52/52 (TerminalController default state, setSelection + mode override, attached/detached anchor selection-getter behaviour, prior-anchor disposal on second setSelection, setSelectionMode no-op short-circuit, clearSelection, setPointerInputs / setSuspendPointerInput gating, highlight register/dispose lifecycle, TerminalHighlight.range null on detached anchors). Enums (selection_mode, cursor_type) and themes constants are covered transitively — they have no executable lines that lcov instruments. Coverage delta: - Total project: 67.06% → 68.80%; coverage_floor bumped 66 → 68. Co-Authored-By: Claude --- .pql/pql-plan.json | 230 ++++++++------- pubspec.yaml | 2 +- test/terminal/ui/ui_pure_test.dart | 438 +++++++++++++++++++++++++++++ 3 files changed, 562 insertions(+), 108 deletions(-) create mode 100644 test/terminal/ui/ui_pure_test.dart diff --git a/.pql/pql-plan.json b/.pql/pql-plan.json index 072a3b1c..690e2c48 100644 --- a/.pql/pql-plan.json +++ b/.pql/pql-plan.json @@ -1,5 +1,5 @@ { - "exported_at": "2026-05-07T19:05:16Z", + "exported_at": "2026-05-08T09:28:17Z", "decisions": [ { "id": "D-1", @@ -9,7 +9,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-3", @@ -19,7 +19,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-4", @@ -29,7 +29,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-5", @@ -39,7 +39,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-6", @@ -49,7 +49,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-7", @@ -59,7 +59,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-8", @@ -69,7 +69,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-9", @@ -79,7 +79,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-10", @@ -89,7 +89,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-11", @@ -99,7 +99,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-12", @@ -109,7 +109,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-13", @@ -119,7 +119,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-14", @@ -129,7 +129,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-15", @@ -139,7 +139,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-16", @@ -149,7 +149,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-17", @@ -159,7 +159,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-18", @@ -169,7 +169,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-19", @@ -179,7 +179,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-20", @@ -189,7 +189,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/accessibility.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-21", @@ -199,7 +199,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/accessibility.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-22", @@ -209,7 +209,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/accessibility.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-23", @@ -219,7 +219,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-24", @@ -229,7 +229,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-25", @@ -239,7 +239,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-26", @@ -249,7 +249,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-27", @@ -259,7 +259,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-28", @@ -269,7 +269,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-29", @@ -279,7 +279,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-30", @@ -289,7 +289,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-31", @@ -299,7 +299,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-32", @@ -309,7 +309,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-33", @@ -319,7 +319,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-34", @@ -329,7 +329,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-35", @@ -339,7 +339,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-36", @@ -349,7 +349,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-37", @@ -359,7 +359,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-38", @@ -369,7 +369,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-39", @@ -379,7 +379,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-40", @@ -389,7 +389,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-41", @@ -399,7 +399,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-42", @@ -409,7 +409,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-43", @@ -419,7 +419,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-44", @@ -429,7 +429,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-45", @@ -439,7 +439,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-46", @@ -449,7 +449,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/extensions.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-47", @@ -459,7 +459,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-48", @@ -469,7 +469,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-49", @@ -479,7 +479,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-50", @@ -489,7 +489,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-51", @@ -499,7 +499,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-52", @@ -509,7 +509,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-53", @@ -519,7 +519,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-54", @@ -529,7 +529,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-55", @@ -539,7 +539,7 @@ "status": "active", "date": "2026-04-23", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-56", @@ -549,7 +549,7 @@ "status": "active", "date": "2026-04-23", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-57", @@ -559,7 +559,7 @@ "status": "active", "date": "2026-04-23", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-58", @@ -569,7 +569,7 @@ "status": "active", "date": "2026-04-23", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-59", @@ -579,7 +579,7 @@ "status": "active", "date": "2026-04-25", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-60", @@ -589,7 +589,7 @@ "status": "active", "date": "2026-04-26", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-61", @@ -599,7 +599,7 @@ "status": "active", "date": "2026-04-26", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-62", @@ -609,7 +609,7 @@ "status": "active", "date": "2026-04-26", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-63", @@ -619,7 +619,7 @@ "status": "active", "date": "2026-04-26", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-64", @@ -629,7 +629,7 @@ "status": "active", "date": "2026-05-03", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-65", @@ -639,7 +639,7 @@ "status": "active", "date": "2026-04-26", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "D-66", @@ -649,7 +649,7 @@ "status": "active", "date": "2026-05-06", "file_path": "decisions/testing.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "Q-1", @@ -658,7 +658,7 @@ "title": "Authorisation granularity on the IPC socket", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "Q-2", @@ -667,7 +667,7 @@ "title": "Back-pressure on event streams", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "Q-3", @@ -676,7 +676,7 @@ "title": "Event persistence + audit/undo", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "Q-4", @@ -685,7 +685,7 @@ "title": "`.canvas` schema compatibility with Obsidian", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "Q-5", @@ -694,7 +694,7 @@ "title": "IPC wire-format stability + `schema_version:`", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "Q-6", @@ -703,7 +703,7 @@ "title": "Window chrome — native frame vs frameless custom", "status": "resolved", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "Q-7", @@ -712,7 +712,7 @@ "title": "macOS app bundle signing / notarisation", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "Q-8", @@ -721,7 +721,7 @@ "title": "Extension API shape — widgets, subcommands, both?", "status": "open", "file_path": "decisions/questions-extensions.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "Q-9", @@ -730,7 +730,7 @@ "title": "Lua runtime vendoring", "status": "open", "file_path": "decisions/questions-extensions.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "Q-10", @@ -739,7 +739,7 @@ "title": "Extension manifest `schema_version:`", "status": "open", "file_path": "decisions/questions-extensions.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "Q-11", @@ -748,7 +748,7 @@ "title": "Coverage gates — hard thresholds vs soft reporting", "status": "open", "file_path": "decisions/questions-testing.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "Q-12", @@ -757,7 +757,7 @@ "title": "Screen-reader automation (axe-core via Playwright)", "status": "open", "file_path": "decisions/questions-testing.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "Q-13", @@ -766,7 +766,7 @@ "title": "Web production-mode a11y", "status": "open", "file_path": "decisions/questions-accessibility.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "Q-14", @@ -775,7 +775,7 @@ "title": "i18n plurals / gender / date-format tooling", "status": "open", "file_path": "decisions/questions-accessibility.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "Q-15", @@ -784,7 +784,7 @@ "title": "Editor tab — full LSP vs tree-sitter-only highlight", "status": "open", "file_path": "decisions/questions-process.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "Q-16", @@ -793,7 +793,7 @@ "title": "`tree-sitter-dart` grammar maintenance", "status": "open", "file_path": "decisions/questions-process.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "Q-17", @@ -802,7 +802,7 @@ "title": "Icon set growth", "status": "open", "file_path": "decisions/questions-process.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "Q-18", @@ -811,7 +811,7 @@ "title": "Theme hot-reload in release builds", "status": "open", "file_path": "decisions/questions-process.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "Q-19", @@ -820,7 +820,7 @@ "title": "(withdrawn)", "status": "resolved", "file_path": "decisions/questions-process.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "Q-20", @@ -829,7 +829,7 @@ "title": "Kernel DB service — namespaced SQL access?", "status": "open", "file_path": "decisions/questions-process.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "Q-21", @@ -838,7 +838,7 @@ "title": "Pql absorbs planning vs keeps separate", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "Q-22", @@ -847,7 +847,7 @@ "title": "Ticket persistence strategy", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "Q-23", @@ -856,7 +856,7 @@ "title": "SSH-remote development — run clide against a remote workspace", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "Q-25", @@ -865,7 +865,7 @@ "title": "Body text face — mono everywhere vs Josefin Sans UI + mono code", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "Q-26", @@ -874,7 +874,7 @@ "title": "Small screen layout (\u003c 1000px)", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "Q-27", @@ -883,7 +883,7 @@ "title": "Two-editor split", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "Q-28", @@ -892,7 +892,7 @@ "title": "Terminal strip scope — shell only or logs/errors/tests", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "Q-29", @@ -901,7 +901,7 @@ "title": "Branch picker location", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "Q-30", @@ -910,7 +910,7 @@ "title": "Focus behavior when editor is dirty and viewer is peeked", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "Q-31", @@ -919,7 +919,7 @@ "title": "XWayland fallback for frameless — proper Wayland protocol needed", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "R-2", @@ -929,7 +929,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "R-3", @@ -939,7 +939,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "R-4", @@ -949,7 +949,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "R-5", @@ -959,7 +959,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "R-6", @@ -969,7 +969,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "R-7", @@ -979,7 +979,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "R-8", @@ -989,7 +989,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "R-9", @@ -999,7 +999,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "R-10", @@ -1009,7 +1009,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "R-11", @@ -1019,7 +1019,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" }, { "id": "R-12", @@ -1029,7 +1029,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-07 19:04:55" + "synced_at": "2026-05-08 09:27:22" } ], "decision_refs": [ @@ -2811,6 +2811,15 @@ "priority": "high", "created_at": "2026-05-07 06:33:47", "updated_at": "2026-05-07 19:04:55" + }, + { + "id": "T-94", + "type": "task", + "title": "wire pql plan merge as git custom merge driver", + "status": "cancelled", + "priority": "medium", + "created_at": "2026-05-07 19:08:29", + "updated_at": "2026-05-07 19:08:52" } ], "ticket_deps": null, @@ -4496,6 +4505,13 @@ "old_value": "in_progress", "new_value": "done", "changed_at": "2026-05-07 19:04:55" + }, + { + "ticket_id": "T-94", + "field": "status", + "old_value": "backlog", + "new_value": "cancelled", + "changed_at": "2026-05-07 19:08:52" } ] } diff --git a/pubspec.yaml b/pubspec.yaml index ed7f33aa..7df2fa10 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,7 +18,7 @@ repository: https://github.com/postmeridiem/clide # Pre-push line-coverage floor. Ratchets up only — see D-66. # Reading: `awk -F: '/^coverage_floor:/ {gsub(/ /,"",$2); print $2}' pubspec.yaml`. -coverage_floor: 66 +coverage_floor: 68 # Project metadata (was project.yaml, folded in per D-056). # version: above is the single source of truth. The Makefile reads diff --git a/test/terminal/ui/ui_pure_test.dart b/test/terminal/ui/ui_pure_test.dart new file mode 100644 index 00000000..931dcbd5 --- /dev/null +++ b/test/terminal/ui/ui_pure_test.dart @@ -0,0 +1,438 @@ +/// Pure-Dart tests for `lib/src/terminal/src/ui/` files that don't +/// require a Flutter widget harness — palette construction, paragraph +/// cache, text style, key/char mapping, controller state machine, +/// small enums + value classes, char-metrics and theme constants. +library; + +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:clide/src/terminal/src/core/buffer/cell_offset.dart'; +import 'package:clide/src/terminal/src/core/buffer/line.dart'; +import 'package:clide/src/terminal/src/core/buffer/range_block.dart'; +import 'package:clide/src/terminal/src/core/buffer/range_line.dart'; +import 'package:clide/src/terminal/src/core/input/keys.dart'; +import 'package:clide/src/terminal/src/utils/circular_buffer.dart'; +import 'package:clide/src/terminal/src/ui/char_metrics.dart'; +import 'package:clide/src/terminal/src/ui/controller.dart'; +import 'package:clide/src/terminal/src/ui/cursor_type.dart'; +import 'package:clide/src/terminal/src/ui/input_map.dart'; +import 'package:clide/src/terminal/src/ui/palette_builder.dart'; +import 'package:clide/src/terminal/src/ui/paragraph_cache.dart'; +import 'package:clide/src/terminal/src/ui/pointer_input.dart'; +import 'package:clide/src/terminal/src/ui/selection_mode.dart'; +import 'package:clide/src/terminal/src/ui/terminal_size.dart'; +import 'package:clide/src/terminal/src/ui/terminal_text_style.dart'; +import 'package:clide/src/terminal/src/ui/themes.dart'; + +void main() { + group('PaletteBuilder', () { + final builder = PaletteBuilder(TerminalThemes.defaultTheme); + + test('build() returns exactly 256 colors', () { + final palette = builder.build(); + expect(palette, hasLength(256)); + }); + + test('first 16 indices map directly to the named theme colors', () { + expect(builder.paletteColor(0), TerminalThemes.defaultTheme.black); + expect(builder.paletteColor(1), TerminalThemes.defaultTheme.red); + expect(builder.paletteColor(2), TerminalThemes.defaultTheme.green); + expect(builder.paletteColor(3), TerminalThemes.defaultTheme.yellow); + expect(builder.paletteColor(4), TerminalThemes.defaultTheme.blue); + expect(builder.paletteColor(5), TerminalThemes.defaultTheme.magenta); + expect(builder.paletteColor(6), TerminalThemes.defaultTheme.cyan); + expect(builder.paletteColor(7), TerminalThemes.defaultTheme.white); + expect(builder.paletteColor(8), TerminalThemes.defaultTheme.brightBlack); + expect(builder.paletteColor(9), TerminalThemes.defaultTheme.brightRed); + expect(builder.paletteColor(10), TerminalThemes.defaultTheme.brightGreen); + expect(builder.paletteColor(11), TerminalThemes.defaultTheme.brightYellow); + expect(builder.paletteColor(12), TerminalThemes.defaultTheme.brightBlue); + expect(builder.paletteColor(13), TerminalThemes.defaultTheme.brightMagenta); + expect(builder.paletteColor(14), TerminalThemes.defaultTheme.brightCyan); + expect(builder.paletteColor(15), TerminalThemes.defaultTheme.white); + }); + + test('index 16 is the start of the 6×6×6 RGB cube — pure black at (0,0,0)', () { + // The cube starts with r=g=b=0 and walks the b-axis first by 95, then 40. + expect(builder.paletteColor(16), const Color.fromARGB(0xFF, 0, 0, 0)); + }); + + test('cube walks blue → green → red, with first step 95 and subsequent +40', () { + // Index 17: b becomes 95. + expect(builder.paletteColor(17), const Color.fromARGB(0xFF, 0, 0, 95)); + // Index 18: b += 40 → 135. + expect(builder.paletteColor(18), const Color.fromARGB(0xFF, 0, 0, 135)); + }); + + test('231 is the cube terminus — pure white-ish corner', () { + // After a full 6-step walk on each axis, the 6³ cube ends at (255,255,255). + expect(builder.paletteColor(231), const Color.fromARGB(0xFF, 255, 255, 255)); + }); + + test('grayscale ramp at 232..255', () { + expect(builder.paletteColor(232), const Color(0xff080808)); + expect(builder.paletteColor(255), const Color(0xffeeeeee)); + }); + + test('out-of-range indices clamp to grayscale endpoints', () { + // The implementation falls through to grayscale with clamp(232, 255). + expect(builder.paletteColor(999), const Color(0xffeeeeee)); + }); + }); + + group('ParagraphCache', () { + test('caches and retrieves a paragraph by integer key', () { + final cache = ParagraphCache(8); + const style = TextStyle(fontSize: 14); + final p = cache.performAndCacheLayout('a', style, TextScaler.noScaling, 1); + expect(cache.getLayoutFromCache(1), same(p)); + expect(cache.length, 1); + }); + + test('moves recently-accessed entries to the end (LRU promotion)', () { + final cache = ParagraphCache(2); + const style = TextStyle(fontSize: 14); + cache.performAndCacheLayout('a', style, TextScaler.noScaling, 1); + cache.performAndCacheLayout('b', style, TextScaler.noScaling, 2); + // Touch key 1 — promotes it. + cache.getLayoutFromCache(1); + // Inserting a third entry evicts the LRU (key 2). + cache.performAndCacheLayout('c', style, TextScaler.noScaling, 3); + expect(cache.getLayoutFromCache(1), isNotNull); + expect(cache.getLayoutFromCache(2), isNull); + expect(cache.getLayoutFromCache(3), isNotNull); + }); + + test('clear empties the cache', () { + final cache = ParagraphCache(4); + const style = TextStyle(fontSize: 14); + cache.performAndCacheLayout('a', style, TextScaler.noScaling, 1); + expect(cache.length, 1); + cache.clear(); + expect(cache.length, 0); + expect(cache.getLayoutFromCache(1), isNull); + }); + + test('miss on a never-set key returns null', () { + final cache = ParagraphCache(2); + expect(cache.getLayoutFromCache(99), isNull); + }); + }); + + group('TerminalStyle', () { + test('default constructor uses the documented defaults', () { + const s = TerminalStyle(); + expect(s.fontSize, 13.0); + expect(s.height, 1.2); + expect(s.fontFamily, 'monospace'); + expect(s.fontFamilyFallback, contains('Menlo')); + }); + + test('fromTextStyle picks fontFamily, falling back through fontFamilyFallback', () { + final s1 = TerminalStyle.fromTextStyle(const TextStyle(fontFamily: 'Hack')); + expect(s1.fontFamily, 'Hack'); + + final s2 = TerminalStyle.fromTextStyle(const TextStyle(fontFamilyFallback: ['Mono', 'Courier'])); + expect(s2.fontFamily, 'Mono'); + + final s3 = TerminalStyle.fromTextStyle(const TextStyle()); + expect(s3.fontFamily, 'monospace'); + }); + + test('fromTextStyle picks fontSize / height / fontFamilyFallback through to TerminalStyle', () { + final s = TerminalStyle.fromTextStyle( + const TextStyle(fontSize: 18, height: 1.5, fontFamilyFallback: ['Mono']), + ); + expect(s.fontSize, 18); + expect(s.height, 1.5); + expect(s.fontFamilyFallback, ['Mono']); + }); + + test('toTextStyle threads attribute flags into the resulting TextStyle', () { + const s = TerminalStyle(fontSize: 16); + final t = s.toTextStyle( + color: const Color(0xFF112233), + bold: true, + italic: true, + underline: true, + ); + expect(t.fontSize, 16); + expect(t.fontWeight, FontWeight.bold); + expect(t.fontStyle, FontStyle.italic); + expect(t.decoration, TextDecoration.underline); + expect(t.color, const Color(0xFF112233)); + }); + + test('copyWith overrides only specified fields', () { + const s = TerminalStyle(fontSize: 14); + final copied = s.copyWith(fontSize: 20); + expect(copied.fontSize, 20); + expect(copied.fontFamily, s.fontFamily); // unchanged + }); + + test('copyWith with no arguments returns a value-equal clone', () { + // Hits the `?? this.X` right-hand side for every parameter. + const s = TerminalStyle(fontSize: 14); + final clone = s.copyWith(); + expect(clone.fontSize, s.fontSize); + expect(clone.height, s.height); + expect(clone.fontFamily, s.fontFamily); + expect(clone.fontFamilyFallback, s.fontFamilyFallback); + }); + }); + + group('input_map', () { + test('keyToTerminalKey maps logical keys 1:1', () { + expect(keyToTerminalKey(LogicalKeyboardKey.arrowUp), TerminalKey.arrowUp); + expect(keyToTerminalKey(LogicalKeyboardKey.keyA), TerminalKey.keyA); + expect(keyToTerminalKey(LogicalKeyboardKey.escape), TerminalKey.escape); + }); + + test('keyToTerminalKey returns null for an unmapped logical key', () { + // gameButtonStart is mapped; pick something that is not — there's no + // TerminalKey for `LogicalKeyboardKey.colon`, etc. Verify by walking + // a synthesized key id. + const synthetic = LogicalKeyboardKey(0xDEADBEEF); + expect(keyToTerminalKey(synthetic), isNull); + }); + + test('charToTerminalKey maps a single ASCII char (case-insensitive)', () { + expect(charToTerminalKey('a'), TerminalKey.keyA); + expect(charToTerminalKey('A'), TerminalKey.keyA); + expect(charToTerminalKey('1'), TerminalKey.digit1); + }); + + test('charToTerminalKey returns null for multi-char strings or unknown chars', () { + expect(charToTerminalKey('ab'), isNull); + expect(charToTerminalKey(''), isNull); + // Some printable characters don't have keytab mappings (e.g. ¥ ñ). + expect(charToTerminalKey('¥'), isNull); + }); + }); + + group('TerminalSize', () { + test('value semantics — operator== / hashCode / toString', () { + const a = TerminalSize(80, 24); + const b = TerminalSize(80, 24); + const c = TerminalSize(100, 24); + expect(a, b); + expect(a.hashCode, b.hashCode); + expect(a, isNot(c)); + expect(a.toString(), 'TerminalSize(80, 24)'); + // identity branch + expect(identical(a, a), isTrue); + // type-mismatch branch (typed Object so we don't trip the unrelated lint) + const Object notASize = 'not a size'; + expect(a == notASize, isFalse); + }); + }); + + group('PointerInputs', () { + test('default constructor wraps the supplied set', () { + const p = PointerInputs({PointerInput.tap, PointerInput.drag}); + expect(p.inputs, {PointerInput.tap, PointerInput.drag}); + }); + + test('.none() is empty; .all() contains every PointerInput value', () { + expect(const PointerInputs.none().inputs, isEmpty); + expect(const PointerInputs.all().inputs, PointerInput.values.toSet()); + }); + }); + + group('SelectionMode + TerminalCursorType enums', () { + test('all enum values exist and are distinct', () { + expect(SelectionMode.values, [SelectionMode.line, SelectionMode.block]); + expect(TerminalCursorType.values, [ + TerminalCursorType.block, + TerminalCursorType.underline, + TerminalCursorType.verticalBar, + ]); + }); + }); + + group('TerminalThemes (bundled themes are well-formed)', () { + test('defaultTheme exposes the documented core palette', () { + const t = TerminalThemes.defaultTheme; + expect(t.foreground, const Color(0XFFCCCCCC)); + expect(t.background, const Color(0XFF1E1E1E)); + expect(t.brightWhite, const Color(0XFFFFFFFF)); + }); + + test('whiteOnBlack and any other bundled themes are valid TerminalTheme instances', () { + // Touch every static field that PaletteBuilder needs so that any + // missing field would surface as an error here, before runtime. + const t = TerminalThemes.whiteOnBlack; + final palette = PaletteBuilder(t).build(); + expect(palette, hasLength(256)); + }); + }); + + group('calcCharSize (char_metrics)', () { + test('returns positive-width / positive-height for a default style', () { + const style = TerminalStyle(); + final size = calcCharSize(style, TextScaler.noScaling); + expect(size.width, greaterThan(0)); + expect(size.height, greaterThan(0)); + }); + + test('text-scaler scales the size monotonically', () { + const style = TerminalStyle(); + final small = calcCharSize(style, const TextScaler.linear(1.0)); + final big = calcCharSize(style, const TextScaler.linear(2.0)); + expect(big.width, greaterThan(small.width)); + expect(big.height, greaterThan(small.height)); + }); + }); + + group('TerminalController', () { + test('default state: no selection, no highlights, line mode, tap-only inputs', () { + final c = TerminalController(); + addTearDown(c.dispose); + expect(c.selection, isNull); + expect(c.highlights, isEmpty); + expect(c.selectionMode, SelectionMode.line); + expect(c.suspendedPointerInputs, isFalse); + expect(c.pointerInput.inputs, {PointerInput.tap}); + }); + + test('setSelection records anchors and notifies listeners', () { + final c = TerminalController(); + addTearDown(c.dispose); + var notifs = 0; + c.addListener(() => notifs++); + + // Build a pair of attached anchors via a ring + line. + final ring = IndexAwareCircularBuffer(4); + final line = BufferLine(8); + ring.push(line); + final base = line.createAnchor(0); + final extent = line.createAnchor(2); + + c.setSelection(base, extent); + expect(notifs, 1); + expect(c.selection, isA()); + expect(c.selection!.begin, const CellOffset(0, 0)); + expect(c.selection!.end, const CellOffset(2, 0)); + }); + + test('setSelection mode override switches to block range', () { + final c = TerminalController(); + addTearDown(c.dispose); + final ring = IndexAwareCircularBuffer(4); + final line = BufferLine(8); + ring.push(line); + final base = line.createAnchor(0); + final extent = line.createAnchor(3); + c.setSelection(base, extent, mode: SelectionMode.block); + expect(c.selectionMode, SelectionMode.block); + expect(c.selection, isA()); + }); + + test('selection getter returns null when an anchor is detached', () { + final c = TerminalController(); + addTearDown(c.dispose); + // Anchors not attached to a circular buffer are detached by definition. + final detachedLine = BufferLine(8); + final base = detachedLine.createAnchor(0); + final extent = detachedLine.createAnchor(2); + c.setSelection(base, extent); + expect(c.selection, isNull); + }); + + test('setSelection a second time disposes the previous anchors', () { + final c = TerminalController(); + addTearDown(c.dispose); + final ring = IndexAwareCircularBuffer(4); + final line = BufferLine(8); + ring.push(line); + final base1 = line.createAnchor(0); + final extent1 = line.createAnchor(2); + c.setSelection(base1, extent1); + expect(line.anchors.length, 2); + + // Replace with new anchors — old ones should be dropped from the line. + final base2 = line.createAnchor(3); + final extent2 = line.createAnchor(5); + c.setSelection(base2, extent2); + expect(line.anchors.length, 2); + expect(line.anchors.contains(base2), isTrue); + expect(line.anchors.contains(extent2), isTrue); + expect(line.anchors.contains(base1), isFalse); + }); + + test('setSelectionMode is a no-op when the new mode equals the old', () { + final c = TerminalController(); + addTearDown(c.dispose); + var notifs = 0; + c.addListener(() => notifs++); + c.setSelectionMode(SelectionMode.line); // same as default + expect(notifs, 0); + c.setSelectionMode(SelectionMode.block); + expect(notifs, 1); + expect(c.selectionMode, SelectionMode.block); + }); + + test('clearSelection drops anchors and notifies', () { + final c = TerminalController(); + addTearDown(c.dispose); + final ring = IndexAwareCircularBuffer(4); + final line = BufferLine(8); + ring.push(line); + c.setSelection(line.createAnchor(0), line.createAnchor(2)); + var notifs = 0; + c.addListener(() => notifs++); + c.clearSelection(); + expect(c.selection, isNull); + expect(notifs, 1); + }); + + test('setPointerInputs and setSuspendPointerInput notify and update the gate', () { + final c = TerminalController(); + addTearDown(c.dispose); + var notifs = 0; + c.addListener(() => notifs++); + + c.setPointerInputs(const PointerInputs({PointerInput.scroll, PointerInput.drag})); + expect(notifs, 1); + expect(c.shouldSendPointerInput(PointerInput.scroll), isTrue); + expect(c.shouldSendPointerInput(PointerInput.tap), isFalse); + + c.setSuspendPointerInput(true); + expect(c.suspendedPointerInputs, isTrue); + expect(notifs, 2); + // Suspended → all queries return false even if the input is in the set. + expect(c.shouldSendPointerInput(PointerInput.scroll), isFalse); + }); + + test('highlight registers + auto-removes via Disposable', () { + final c = TerminalController(); + addTearDown(c.dispose); + final ring = IndexAwareCircularBuffer(4); + final line = BufferLine(8); + ring.push(line); + final p1 = line.createAnchor(0); + final p2 = line.createAnchor(3); + + final h = c.highlight(p1: p1, p2: p2, color: const Color(0xFF112233)); + expect(c.highlights, contains(h)); + // Range from highlight resolves to the same span via attached anchors. + expect(h.range, isA()); + + h.dispose(); + expect(c.highlights, isNot(contains(h))); + }); + + test('TerminalHighlight.range returns null when its anchors are detached', () { + final c = TerminalController(); + addTearDown(c.dispose); + // Anchors on a non-attached line. + final freeLine = BufferLine(8); + final p1 = freeLine.createAnchor(0); + final p2 = freeLine.createAnchor(2); + final h = c.highlight(p1: p1, p2: p2, color: const Color(0xFFAA0000)); + expect(h.range, isNull); + }); + }); +}