a45ee4ed
fix: a click at the wall is bounded by the grid, as a zoomed one is
a73x 2026-08-22 13:13
Commit message
src/wallview.zig
| Old | New | ||
|---|---|---|---|
| @@ -341,6 +341,14 @@ const Window = struct { | |||
| 341 | /// The grid row the window's first content line came from | 341 | /// The grid row the window's first content line came from |
| 342 | /// (`paint.stripeWinStart`). | 342 | /// (`paint.stripeWinStart`). |
| 343 | win_start: u16 = 0, | 343 | win_start: u16 = 0, |
| 344 | /// The replica's grid at that paint, which is what bounds a click. | ||
| 345 | /// A stripe attaches at 0x0 and inherits whatever grid the daemon | ||
| 346 | /// holds, so a stripe is routinely TALLER and WIDER than the session | ||
| 347 | /// inside it — `renderStripe` clears the rows below the grid rather | ||
| 348 | /// than painting them, and DECAWM-off clips the columns past it. | ||
| 349 | /// Zero by default, so a tile that has not painted yet resolves no | ||
| 350 | /// click at all: there is nothing on the terminal to have clicked. | ||
| 351 | grid: proto.Size = .{ .cols = 0, .rows = 0 }, | ||
| 344 | }; | 352 | }; |
| 345 | 353 | ||
| 346 | const Tile = struct { | 354 | const Tile = struct { |
| @@ -545,10 +553,22 @@ fn hitTest(tiles: []Tile, present: []const bool, shared: *Shared, row: u16, col: | |||
| 545 | for (tiles, present) |*t, p| { | 553 | for (tiles, present) |*t, p| { |
| 546 | if (!p) continue; | 554 | if (!p) continue; |
| 547 | const off = stripeContentRow(t.stripe, row) orelse continue; | 555 | const off = stripeContentRow(t.stripe, row) orelse continue; |
| 556 | // Past the grid's last row this stripe shows a CLEARED row, not a | ||
| 557 | // session line — `renderStripe`'s second loop wrote it. Refused | ||
| 558 | // rather than answered, and refused here rather than left for the | ||
| 559 | // daemon: an out-of-range row makes `extractSelection` reply | ||
| 560 | // `.invalid`, which reaches the user as a drag that copied nothing | ||
| 561 | // and said nothing about why. Same rule and same reasoning as | ||
| 562 | // `interact.hitTest`, which the zoomed driver uses; the two answer | ||
| 563 | // one question and must not answer it differently. | ||
| 564 | const grid_row: u32 = @as(u32, t.win.win_start) + off; | ||
| 565 | if (grid_row >= t.win.grid.rows) return null; | ||
| 548 | return .{ | 566 | return .{ |
| 549 | .tile = t.idx, | 567 | .tile = t.idx, |
| 550 | .row = t.win.history_rows + t.win.win_start + off, | 568 | .row = t.win.history_rows + grid_row, |
| 551 | .col = col, | 569 | // Clamped, not refused: the right edge is where a hand |
| 570 | // overshoots, and a copy is better than a lesson. | ||
| 571 | .col = @min(col, t.win.grid.cols -| 1), | ||
| 552 | }; | 572 | }; |
| 553 | } | 573 | } |
| 554 | return null; | 574 | return null; |
| @@ -898,6 +918,7 @@ fn paintStripe(t: *Tile, alloc: std.mem.Allocator, eng: *Engine, history_rows: u | |||
| 898 | t.win = .{ | 918 | t.win = .{ |
| 899 | .history_rows = history_rows, | 919 | .history_rows = history_rows, |
| 900 | .win_start = paint.stripeWinStart(@intCast(eng.term.rows), eng.cursorPos().y, view.rows), | 920 | .win_start = paint.stripeWinStart(@intCast(eng.term.rows), eng.cursorPos().y, view.rows), |
| 921 | .grid = .{ .cols = @intCast(eng.term.cols), .rows = @intCast(eng.term.rows) }, | ||
| 901 | }; | 922 | }; |
| 902 | // The highlight is drawn HERE and never by the keyboard. A stripe is | 923 | // The highlight is drawn HERE and never by the keyboard. A stripe is |
| 903 | // repainted from its replica on every frame, so an inversion the | 924 | // repainted from its replica on every frame, so an inversion the |
| @@ -1768,6 +1789,18 @@ fn pumpTile(t: *Tile) void { | |||
| 1768 | // the delta that cannot fix us. | 1789 | // the delta that cannot fix us. |
| 1769 | .resync => { | 1790 | .resync => { |
| 1770 | core.rep.state_since_attach = false; | 1791 | core.rep.state_since_attach = false; |
| 1792 | // A resync renames the absolute row space, so a | ||
| 1793 | // held highlight now names rows nobody selected — | ||
| 1794 | // and on an EPOCH change `sel_range` still matches | ||
| 1795 | // it, so an in-flight reply would copy the new | ||
| 1796 | // session's text. `redial` drops both sides for | ||
| 1797 | // this reason; this path re-attaches without going | ||
| 1798 | // through it. Not `core.reattached()`: that also | ||
| 1799 | // flushes the overlay and forces a repaint, which | ||
| 1800 | // this arm has never done and is a separate | ||
| 1801 | // question from the drag. | ||
| 1802 | core.drag.clear(); | ||
| 1803 | dropDragOver(t); | ||
| 1771 | sendAttach(t, &transport, 0, 0) catch return; | 1804 | sendAttach(t, &transport, 0, 0) catch return; |
| 1772 | }, | 1805 | }, |
| 1773 | .not_mine => switch (frame.type) { | 1806 | .not_mine => switch (frame.type) { |
| @@ -3815,6 +3848,40 @@ fn drainWallPipe(fd: std.posix.fd_t, buf: []u8) []const u8 { | |||
| 3815 | return buf[0..n]; | 3848 | return buf[0..n]; |
| 3816 | } | 3849 | } |
| 3817 | 3850 | ||
| 3851 | test "a wall click is bounded by the session's grid, not by the stripe" { | ||
| 3852 | const p = try std.posix.pipe2(.{ .NONBLOCK = true }); | ||
| 3853 | defer std.posix.close(p[0]); | ||
| 3854 | defer std.posix.close(p[1]); | ||
| 3855 | var shared = Shared{ .out_fd = p[1], .size = .{ .cols = 80, .rows = 24 }, .is_tty = true }; | ||
| 3856 | var tiles = [_]Tile{ | ||
| 3857 | .{ .r = .{ .target = .{ .sock = "/s" }, .label = "a", .session = "a" }, .stripe = .{ .top = 0, .rows = 12 }, .shared = &shared, .idx = 0 }, | ||
| 3858 | }; | ||
| 3859 | var present = [_]bool{true}; | ||
| 3860 | // A stripe eleven content rows tall over a grid of four: routine, not | ||
| 3861 | // contrived — a stripe attaches at 0x0 and inherits whatever grid the | ||
| 3862 | // daemon holds, so the stripe is nearly always the bigger of the two. | ||
| 3863 | tiles[0].win = .{ .history_rows = 100, .win_start = 0, .grid = .{ .cols = 10, .rows = 4 } }; | ||
| 3864 | |||
| 3865 | // Inside the grid: the oldest retained row plus the offset. | ||
| 3866 | const in = hitTest(&tiles, &present, &shared, 1, 3).?; | ||
| 3867 | try std.testing.expectEqual(@as(u32, 100), in.row); | ||
| 3868 | try std.testing.expectEqual(@as(u16, 3), in.col); | ||
| 3869 | |||
| 3870 | // Right of the grid, clamped to its last column rather than refused — | ||
| 3871 | // the daemon would answer `.invalid` and the user would get silence. | ||
| 3872 | const wide = hitTest(&tiles, &present, &shared, 1, 40).?; | ||
| 3873 | try std.testing.expectEqual(@as(u16, 9), wide.col); | ||
| 3874 | |||
| 3875 | // Below the grid: `renderStripe` CLEARED that row, so it shows no | ||
| 3876 | // session line and there is nothing there to select. | ||
| 3877 | try std.testing.expect(hitTest(&tiles, &present, &shared, 5, 3) == null); | ||
| 3878 | |||
| 3879 | // A tile that has not painted yet has a zero grid, and so resolves | ||
| 3880 | // nothing at all. | ||
| 3881 | tiles[0].win = .{}; | ||
| 3882 | try std.testing.expect(hitTest(&tiles, &present, &shared, 1, 3) == null); | ||
| 3883 | } | ||
| 3884 | |||
| 3818 | test "a plain click selects the stripe under it, and a drag highlights instead" { | 3885 | test "a plain click selects the stripe under it, and a drag highlights instead" { |
| 3819 | const p = try std.posix.pipe2(.{ .NONBLOCK = true }); | 3886 | const p = try std.posix.pipe2(.{ .NONBLOCK = true }); |
| 3820 | defer std.posix.close(p[0]); | 3887 | defer std.posix.close(p[0]); |
| @@ -3826,8 +3893,8 @@ test "a plain click selects the stripe under it, and a drag highlights instead" | |||
| 3826 | }; | 3893 | }; |
| 3827 | var present = [_]bool{ true, true }; | 3894 | var present = [_]bool{ true, true }; |
| 3828 | // Both stripes have painted once, so a terminal row resolves to a line. | 3895 | // Both stripes have painted once, so a terminal row resolves to a line. |
| 3829 | tiles[0].win = .{ .history_rows = 100, .win_start = 0 }; | 3896 | tiles[0].win = .{ .history_rows = 100, .win_start = 0, .grid = .{ .cols = 80, .rows = 24 } }; |
| 3830 | tiles[1].win = .{ .history_rows = 200, .win_start = 3 }; | 3897 | tiles[1].win = .{ .history_rows = 200, .win_start = 3, .grid = .{ .cols = 80, .rows = 24 } }; |
| 3831 | 3898 | ||
| 3832 | const press: interact.MouseFilter.Event = .{ .kind = .press, .button = 0, .col = 4, .row = 15, .at = 0 }; | 3899 | const press: interact.MouseFilter.Event = .{ .kind = .press, .button = 0, .col = 4, .row = 15, .at = 0 }; |
| 3833 | const release: interact.MouseFilter.Event = .{ .kind = .release, .button = 0, .col = 4, .row = 15, .at = 0 }; | 3900 | const release: interact.MouseFilter.Event = .{ .kind = .release, .button = 0, .col = 4, .row = 15, .at = 0 }; |
| @@ -3899,8 +3966,8 @@ test "a drag at the wall stays in the stripe it started in, and rings for a repa | |||
| 3899 | std.posix.close(t.wake_r); | 3966 | std.posix.close(t.wake_r); |
| 3900 | std.posix.close(t.wake_w); | 3967 | std.posix.close(t.wake_w); |
| 3901 | }; | 3968 | }; |
| 3902 | tiles[0].win = .{ .history_rows = 100, .win_start = 0 }; | 3969 | tiles[0].win = .{ .history_rows = 100, .win_start = 0, .grid = .{ .cols = 80, .rows = 24 } }; |
| 3903 | tiles[1].win = .{ .history_rows = 200, .win_start = 3 }; | 3970 | tiles[1].win = .{ .history_rows = 200, .win_start = 3, .grid = .{ .cols = 80, .rows = 24 } }; |
| 3904 | 3971 | ||
| 3905 | const gen0 = shared.repaint_gen.load(.acquire); | 3972 | const gen0 = shared.repaint_gen.load(.acquire); |
| 3906 | wallMouse(&tiles, &present, &shared, .{ .kind = .press, .button = 0, .col = 4, .row = 3, .at = 0 }); | 3973 | wallMouse(&tiles, &present, &shared, .{ .kind = .press, .button = 0, .col = 4, .row = 3, .at = 0 }); |
| @@ -3959,8 +4026,8 @@ test "a wall drag posts its selection for the pump, because the keyboard owns no | |||
| 3959 | std.posix.close(t.wake_r); | 4026 | std.posix.close(t.wake_r); |
| 3960 | std.posix.close(t.wake_w); | 4027 | std.posix.close(t.wake_w); |
| 3961 | }; | 4028 | }; |
| 3962 | tiles[0].win = .{ .history_rows = 100, .win_start = 0 }; | 4029 | tiles[0].win = .{ .history_rows = 100, .win_start = 0, .grid = .{ .cols = 80, .rows = 24 } }; |
| 3963 | tiles[1].win = .{ .history_rows = 200, .win_start = 3 }; | 4030 | tiles[1].win = .{ .history_rows = 200, .win_start = 3, .grid = .{ .cols = 80, .rows = 24 } }; |
| 3964 | 4031 | ||
| 3965 | // A drag inside stripe b, released. | 4032 | // A drag inside stripe b, released. |
| 3966 | wallMouse(&tiles, &present, &shared, .{ .kind = .press, .button = 0, .col = 4, .row = 15, .at = 0 }); | 4033 | wallMouse(&tiles, &present, &shared, .{ .kind = .press, .button = 0, .col = 4, .row = 15, .at = 0 }); |
| @@ -4044,8 +4111,8 @@ test "a wall drag copies on release, and a stale answer copies nothing" { | |||
| 4044 | std.posix.close(t.wake_r); | 4111 | std.posix.close(t.wake_r); |
| 4045 | std.posix.close(t.wake_w); | 4112 | std.posix.close(t.wake_w); |
| 4046 | }; | 4113 | }; |
| 4047 | tiles[0].win = .{ .history_rows = 100, .win_start = 0 }; | 4114 | tiles[0].win = .{ .history_rows = 100, .win_start = 0, .grid = .{ .cols = 80, .rows = 24 } }; |
| 4048 | tiles[1].win = .{ .history_rows = 200, .win_start = 3 }; | 4115 | tiles[1].win = .{ .history_rows = 200, .win_start = 3, .grid = .{ .cols = 80, .rows = 24 } }; |
| 4049 | 4116 | ||
| 4050 | // The pump's half. A stripe's Core is DEMOTED — it holds no claim and | 4117 | // The pump's half. A stripe's Core is DEMOTED — it holds no claim and |
| 4051 | // paints nothing — which is exactly why the copy cannot go out through | 4118 | // paints nothing — which is exactly why the copy cannot go out through |
| @@ -4112,7 +4179,7 @@ test "a zoomed tile copies from its own drag, not the wall's" { | |||
| 4112 | tiles[0].wake_w = pipe[1]; | 4179 | tiles[0].wake_w = pipe[1]; |
| 4113 | defer std.posix.close(pipe[0]); | 4180 | defer std.posix.close(pipe[0]); |
| 4114 | defer std.posix.close(pipe[1]); | 4181 | defer std.posix.close(pipe[1]); |
| 4115 | tiles[0].win = .{ .history_rows = 0, .win_start = 0 }; | 4182 | tiles[0].win = .{ .history_rows = 0, .win_start = 0, .grid = .{ .cols = 80, .rows = 24 } }; |
| 4116 | shared.zoom.store(0, .release); | 4183 | shared.zoom.store(0, .release); |
| 4117 | 4184 | ||
| 4118 | var core = try interact.Core.initSized(alloc, -1, p[1], shared.size); | 4185 | var core = try interact.Core.initSized(alloc, -1, p[1], shared.size); |
| @@ -4169,7 +4236,7 @@ test "a copy too big for OSC 52 is said out loud rather than dropped" { | |||
| 4169 | tiles[0].wake_w = pipe[1]; | 4236 | tiles[0].wake_w = pipe[1]; |
| 4170 | defer std.posix.close(pipe[0]); | 4237 | defer std.posix.close(pipe[0]); |
| 4171 | defer std.posix.close(pipe[1]); | 4238 | defer std.posix.close(pipe[1]); |
| 4172 | tiles[0].win = .{ .history_rows = 0, .win_start = 0 }; | 4239 | tiles[0].win = .{ .history_rows = 0, .win_start = 0, .grid = .{ .cols = 80, .rows = 24 } }; |
| 4173 | 4240 | ||
| 4174 | var core = try interact.Core.initSized(alloc, -1, p[1], shared.size); | 4241 | var core = try interact.Core.initSized(alloc, -1, p[1], shared.size); |
| 4175 | defer core.deinit(); | 4242 | defer core.deinit(); |
| @@ -4218,8 +4285,8 @@ test "a stripe paints its own highlight and nobody else's" { | |||
| 4218 | 4285 | ||
| 4219 | // Both stripes have painted, so a click resolves. Tile a's window sits | 4286 | // Both stripes have painted, so a click resolves. Tile a's window sits |
| 4220 | // at grid row 0 with 100 rows of history behind it. | 4287 | // at grid row 0 with 100 rows of history behind it. |
| 4221 | tiles[0].win = .{ .history_rows = 100, .win_start = 0 }; | 4288 | tiles[0].win = .{ .history_rows = 100, .win_start = 0, .grid = .{ .cols = 80, .rows = 24 } }; |
| 4222 | tiles[1].win = .{ .history_rows = 100, .win_start = 0 }; | 4289 | tiles[1].win = .{ .history_rows = 100, .win_start = 0, .grid = .{ .cols = 80, .rows = 24 } }; |
| 4223 | // Drag across tile a's second content line, columns 2..5. | 4290 | // Drag across tile a's second content line, columns 2..5. |
| 4224 | wallMouse(&tiles, &present, &shared, .{ .kind = .press, .button = 0, .col = 2, .row = 2, .at = 0 }); | 4291 | wallMouse(&tiles, &present, &shared, .{ .kind = .press, .button = 0, .col = 2, .row = 2, .at = 0 }); |
| 4225 | wallMouse(&tiles, &present, &shared, .{ .kind = .motion, .button = 32, .col = 5, .row = 2, .at = 0 }); | 4292 | wallMouse(&tiles, &present, &shared, .{ .kind = .motion, .button = 32, .col = 5, .row = 2, .at = 0 }); |
| @@ -4251,7 +4318,7 @@ test "a reconnect drops the highlight over its own tile, and only its own" { | |||
| 4251 | .{ .r = .{ .target = .{ .sock = "/s" }, .label = "b", .session = "b" }, .stripe = .{ .top = 12, .rows = 12 }, .shared = &shared, .idx = 1 }, | 4318 | .{ .r = .{ .target = .{ .sock = "/s" }, .label = "b", .session = "b" }, .stripe = .{ .top = 12, .rows = 12 }, .shared = &shared, .idx = 1 }, |
| 4252 | }; | 4319 | }; |
| 4253 | var present = [_]bool{ true, true }; | 4320 | var present = [_]bool{ true, true }; |
| 4254 | tiles[0].win = .{ .history_rows = 100, .win_start = 0 }; | 4321 | tiles[0].win = .{ .history_rows = 100, .win_start = 0, .grid = .{ .cols = 80, .rows = 24 } }; |
| 4255 | 4322 | ||
| 4256 | wallMouse(&tiles, &present, &shared, .{ .kind = .press, .button = 0, .col = 4, .row = 3, .at = 0 }); | 4323 | wallMouse(&tiles, &present, &shared, .{ .kind = .press, .button = 0, .col = 4, .row = 3, .at = 0 }); |
| 4257 | wallMouse(&tiles, &present, &shared, .{ .kind = .motion, .button = 32, .col = 7, .row = 5, .at = 0 }); | 4324 | wallMouse(&tiles, &present, &shared, .{ .kind = .motion, .button = 32, .col = 7, .row = 5, .at = 0 }); |
| @@ -4280,7 +4347,7 @@ test "a relayout drops the highlight, because the stripes move under it" { | |||
| 4280 | .{ .r = .{ .target = .{ .sock = "/s" }, .label = "b", .session = "b" }, .stripe = .{ .top = 12, .rows = 12 }, .shared = &shared, .idx = 1 }, | 4347 | .{ .r = .{ .target = .{ .sock = "/s" }, .label = "b", .session = "b" }, .stripe = .{ .top = 12, .rows = 12 }, .shared = &shared, .idx = 1 }, |
| 4281 | }; | 4348 | }; |
| 4282 | var present = [_]bool{ true, true }; | 4349 | var present = [_]bool{ true, true }; |
| 4283 | tiles[0].win = .{ .history_rows = 100, .win_start = 0 }; | 4350 | tiles[0].win = .{ .history_rows = 100, .win_start = 0, .grid = .{ .cols = 80, .rows = 24 } }; |
| 4284 | 4351 | ||
| 4285 | wallMouse(&tiles, &present, &shared, .{ .kind = .press, .button = 0, .col = 4, .row = 3, .at = 0 }); | 4352 | wallMouse(&tiles, &present, &shared, .{ .kind = .press, .button = 0, .col = 4, .row = 3, .at = 0 }); |
| 4286 | wallMouse(&tiles, &present, &shared, .{ .kind = .motion, .button = 32, .col = 7, .row = 5, .at = 0 }); | 4353 | wallMouse(&tiles, &present, &shared, .{ .kind = .motion, .button = 32, .col = 7, .row = 5, .at = 0 }); |
| @@ -4320,7 +4387,7 @@ test "a zoom move drops what the wall's input filters are holding" { | |||
| 4320 | var mouse_out: [mailbox_max + interact.MouseFilter.max_held]u8 = undefined; | 4387 | var mouse_out: [mailbox_max + interact.MouseFilter.max_held]u8 = undefined; |
| 4321 | _ = input.mouse.feed("\x1b[<0;3", &mouse_out); | 4388 | _ = input.mouse.feed("\x1b[<0;3", &mouse_out); |
| 4322 | try std.testing.expect(input.mouse.len > 0); | 4389 | try std.testing.expect(input.mouse.len > 0); |
| 4323 | tiles[1].win = .{ .history_rows = 200, .win_start = 3 }; | 4390 | tiles[1].win = .{ .history_rows = 200, .win_start = 3, .grid = .{ .cols = 80, .rows = 24 } }; |
| 4324 | wallMouse(&tiles, &present, &shared, .{ .kind = .press, .button = 0, .col = 4, .row = 15, .at = 0 }); | 4391 | wallMouse(&tiles, &present, &shared, .{ .kind = .press, .button = 0, .col = 4, .row = 15, .at = 0 }); |
| 4325 | wallMouse(&tiles, &present, &shared, .{ .kind = .motion, .button = 32, .col = 9, .row = 17, .at = 0 }); | 4392 | wallMouse(&tiles, &present, &shared, .{ .kind = .motion, .button = 32, .col = 9, .row = 17, .at = 0 }); |
| 4326 | try std.testing.expect(shared.drag.range() != null); | 4393 | try std.testing.expect(shared.drag.range() != null); |
| @@ -4365,8 +4432,8 @@ test "hitTest names the absolute row under a click, and nothing at all off a str | |||
| 4365 | }; | 4432 | }; |
| 4366 | // What each pump last painted. The daemon holds 100 rows above tile | 4433 | // What each pump last painted. The daemon holds 100 rows above tile |
| 4367 | // b's viewport and the stripe was showing that grid from row 5. | 4434 | // b's viewport and the stripe was showing that grid from row 5. |
| 4368 | tiles[0].win = .{ .history_rows = 0, .win_start = 0 }; | 4435 | tiles[0].win = .{ .history_rows = 0, .win_start = 0, .grid = .{ .cols = 80, .rows = 24 } }; |
| 4369 | tiles[1].win = .{ .history_rows = 100, .win_start = 5 }; | 4436 | tiles[1].win = .{ .history_rows = 100, .win_start = 5, .grid = .{ .cols = 80, .rows = 24 } }; |
| 4370 | var present = [_]bool{ true, true }; | 4437 | var present = [_]bool{ true, true }; |
| 4371 | 4438 | ||
| 4372 | // Tile b's first content row: terminal row 13, grid row 5, and 100 | 4439 | // Tile b's first content row: terminal row 13, grid row 5, and 100 |
test/e2e.sh
| Old | New | ||
|---|---|---|---|
| @@ -6230,8 +6230,9 @@ grep -q "wallwheel" "$OUT.zwcapg" && { | |||
| 6230 | # the session's modes on the user's terminal for the rest of the wall's | 6230 | # the session's modes on the user's terminal for the rest of the wall's |
| 6231 | # life. What the demote then puts back is the WALL's own capture set, which | 6231 | # life. What the demote then puts back is the WALL's own capture set, which |
| 6232 | # is the wall reading mouse reports on its own account and not a leak; the | 6232 | # is the wall reading mouse reports on its own account and not a leak; the |
| 6233 | # exit's teardown is what has to leave the terminal clean. Counted with `grep -o`: these are escape sequences inside a | 6233 | # exit's teardown is what has to leave the terminal clean. Counted with |
| 6234 | # paint, with no newlines to make a line count mean anything. | 6234 | # `grep -o`: these are escape sequences inside a paint, with no newlines to |
| 6235 | # make a line count mean anything. | ||
| 6235 | _zw_off=$(grep -oa "$(printf '\033')\[?1000l" "$OUT.zwcap" | wc -l) | 6236 | _zw_off=$(grep -oa "$(printf '\033')\[?1000l" "$OUT.zwcap" | wc -l) |
| 6236 | [ "$_zw_off" -ge 2 ] || { | 6237 | [ "$_zw_off" -ge 2 ] || { |
| 6237 | echo "e2e FAIL: zoom wheel: mouse reporting was turned off $_zw_off time(s);" | 6238 | echo "e2e FAIL: zoom wheel: mouse reporting was turned off $_zw_off time(s);" |