diff --git a/.pql/changelog/ticket_history/2026-05.sql b/.pql/changelog/ticket_history/2026-05.sql index 2525dec0..f875671f 100644 --- a/.pql/changelog/ticket_history/2026-05.sql +++ b/.pql/changelog/ticket_history/2026-05.sql @@ -1821,3 +1821,5 @@ INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-107', 'status', 'in_progress', 'done', NULL, '2026-05-18 10:30:30', '2026-05-18 10:30:30', '2026-05-18 10:30:30', NULL, 'd5ecc2578b5b48991d07531eb2166529', 1) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-116', 'status', 'backlog', 'in_progress', NULL, '2026-05-18 10:35:47', '2026-05-18 10:35:47', '2026-05-18 10:35:47', NULL, '59b70e93c913952d4f13b9ad2320e060', 1) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-116', 'status', 'in_progress', 'done', NULL, '2026-05-18 11:22:46', '2026-05-18 11:22:46', '2026-05-18 11:22:46', NULL, '16fd285587d7a37090d2a21f4307c8e8', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-74', 'status', 'ready', 'in_progress', NULL, '2026-05-18 11:49:28', '2026-05-18 11:49:28', '2026-05-18 11:49:28', NULL, 'e25e13a5afcbc376b3c57312c6f320ea', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-74', 'status', 'in_progress', 'done', NULL, '2026-05-18 11:51:40', '2026-05-18 11:51:40', '2026-05-18 11:51:40', NULL, '4343b4758e9ffbf3e046d11d65dfeb1f', 1) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-05.sql b/.pql/changelog/tickets/2026-05.sql index 9242b6c5..38def1a8 100644 --- a/.pql/changelog/tickets/2026-05.sql +++ b/.pql/changelog/tickets/2026-05.sql @@ -1986,3 +1986,12 @@ Blocks T-103 from adding integration tests to `make push-check` (the other two i 3. T-103''s `push-check-full` can include integration tests without the theme-picker hanging. Source: surfaced 2026-05-17 during T-103 work.', 'done', 'medium', NULL, NULL, NULL, '2026-05-17 19:14:31', '2026-05-18 11:22:46', NULL, 'ff3b728310bd265a57a60778417d026f', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-74', 'task', NULL, 'forward real mouse events to TUI apps (not just PgUp/PgDown)', 'Mouse wheel scroll is currently translated to PgUp/PgDown key input in `lib/src/terminal/src/terminal_view.dart` as a pragmatic workaround — TUIs that capture mouse events without binding the wheel get a no-op otherwise. The downside: any TUI that does its own wheel handling (selection, click-to-position) loses the actual mouse coordinates because all we send is keyboard. + +**Acceptance:** +- The terminal forwards `PointerScrollEvent` and click/drag/release as proper xterm mouse-protocol escape sequences (CSI `M` / SGR variant per the report mode the inner program declared via `?1000h` / `?1006h`). +- Tested with `vim` (mouse mode `a`), `htop`, `less` — all should respond to mouse selection / click correctly. +- Falls back to PgUp/PgDown only when the inner program has not declared a mouse mode. +- The `mouseInput` API on `Terminal` is wired through `Listener.onPointerSignal` instead of being bypassed. + +**Why this isn`t the default already:** the bypass was the cheapest fix to get scroll working at all when the multitab/Scrollable ate scroll events. With those fixes landed, the right-shaped mouse forwarding is the proper next step.', 'done', 'medium', NULL, NULL, NULL, '2026-05-05 12:53:22', '2026-05-18 11:51:40', NULL, '20b66d93b9315ad51170a6f8762d4abe', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); diff --git a/CHANGELOG.md b/CHANGELOG.md index 20886d1a..e5e6df5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -97,6 +97,11 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. ### Changed +- Terminal mouse wheel forwards as proper xterm wheel-button escapes + when the inner program declares a mouse mode (?1000h / ?1002h / + ?1003h, optionally +?1006h SGR). Falls back to PgUp/PgDown only + when no mouse mode is active. vim mouse=a / htop / less mouse modes + now react to the wheel (T-74). - `lib/src/terminal/` cleaned to the project bar — commented-out `print()` debugging stubs stripped from `custom_text_edit.dart`, stale TODOs in `parser.dart` + `keytab.dart` replaced with clear diff --git a/lib/src/terminal/src/terminal_view.dart b/lib/src/terminal/src/terminal_view.dart index 80458b42..d9d59746 100644 --- a/lib/src/terminal/src/terminal_view.dart +++ b/lib/src/terminal/src/terminal_view.dart @@ -8,6 +8,8 @@ import 'package:flutter/services.dart'; import 'package:clide/src/terminal/src/core/buffer/cell_offset.dart'; import 'package:clide/src/terminal/src/core/input/keys.dart'; +import 'package:clide/src/terminal/src/core/mouse/button.dart'; +import 'package:clide/src/terminal/src/core/mouse/button_state.dart'; import 'package:clide/src/terminal/src/terminal.dart'; import 'package:clide/src/terminal/src/ui/controller.dart'; import 'package:clide/src/terminal/src/ui/cursor_type.dart'; @@ -155,13 +157,23 @@ class TerminalViewState extends State { final lh = renderTerminal.lineHeight; if (lh <= 0) return; final lines = (event.scrollDelta.dy / lh).round().clamp(-5, 5); - // Always send PgUp/PgDown for scroll — the mouse-escape-sequence - // path tends to be a no-op in TUI apps (claude, vim) that capture - // mouse for other purposes. PgUp/PgDown is the universal scroll. - for (var i = 0; i < lines.abs(); i++) { - widget.terminal.keyInput( - lines < 0 ? TerminalKey.pageUp : TerminalKey.pageDown, - ); + if (lines == 0) return; + final button = lines > 0 ? TerminalMouseButton.wheelDown : TerminalMouseButton.wheelUp; + final count = lines.abs(); + // If the inner program has declared a mouse mode that reports + // scroll (?1000h / ?1002h / ?1003h / +?1006h for SGR), forward + // proper xterm wheel-button escapes so the TUI can react (vim + // mouse=a scroll, less line-by-line, htop highlight). Otherwise + // fall back to PgUp/PgDown so plain shells still scroll (T-74). + if (widget.terminal.mouseMode.reportScroll) { + for (var i = 0; i < count; i++) { + renderTerminal.mouseEvent(button, TerminalMouseButtonState.down, event.localPosition); + } + return; + } + final key = lines < 0 ? TerminalKey.pageUp : TerminalKey.pageDown; + for (var i = 0; i < count; i++) { + widget.terminal.keyInput(key); } } diff --git a/test/terminal/terminal_view_test.dart b/test/terminal/terminal_view_test.dart index 50eb4e13..626f5d96 100644 --- a/test/terminal/terminal_view_test.dart +++ b/test/terminal/terminal_view_test.dart @@ -2,6 +2,7 @@ /// gesture / keyboard / scroll / render plumbing around a `Terminal`. library; +import 'package:clide/src/terminal/src/core/mouse/mode.dart'; import 'package:clide/src/terminal/src/terminal.dart'; import 'package:clide/src/terminal/src/terminal_view.dart'; import 'package:clide/src/terminal/src/ui/controller.dart'; @@ -97,6 +98,32 @@ void main() { expect(r.outputs, isNotEmpty); }); + testWidgets('PointerScrollEvent forwards as xterm wheel escapes when mouseMode.reportScroll (T-74)', (tester) async { + final r = _OutputRecorder(); + final t = r.build(); + // Have the inner program declare ?1000h (upDownScroll) so + // mouseMode.reportScroll becomes true. + t.setMouseMode(MouseMode.upDownScroll); + await tester.pumpWidget(_host(TerminalView(t))); + final viewCenter = tester.getCenter(find.byType(TerminalView)); + final testGesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await testGesture.addPointer(location: viewCenter); + await tester.sendEventToBinding(PointerScrollEvent( + position: viewCenter, + scrollDelta: const Offset(0, 100), + )); + await tester.pump(); + expect(r.outputs, isNotEmpty); + // Wheel-down id is 64+5=69; normal-mode reporter encodes button + // bytes as 32+id, but the reporter chunks across modes — the + // load-bearing assertion is "no PgDn key escape was emitted". + // PgDn under default keyboard sends ESC[6~. Scrolls in mouse + // mode must NOT contain that — they contain CSI M / SGR mouse + // sequences instead. + final combined = r.outputs.join(); + expect(combined.contains('\x1b[6~'), isFalse, reason: 'expected wheel forwarded as mouse, not PgDn'); + }); + testWidgets('non-scroll PointerSignalEvent is a no-op', (tester) async { final r = _OutputRecorder(); final t = r.build();