diff --git a/CHANGELOG.md b/CHANGELOG.md index 038b26f..de55536 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,11 +23,6 @@ listed under a **Changed** or **Removed** heading. ### Changed -- **`Screen::row_text` rejects an out-of-bounds row instead of returning an - ambiguous empty string.** Callers with a potentially invalid index should - bounds-check against `rows()` first, or use `cell(row, 0)` and treat `None` - as absent. (#260) - ### Fixed - **docs.rs labels APIs gated by the `decode` and `insta` features.** Optional diff --git a/crates/termlens/src/screen.rs b/crates/termlens/src/screen.rs index f9c2773..f2a290d 100644 --- a/crates/termlens/src/screen.rs +++ b/crates/termlens/src/screen.rs @@ -1018,6 +1018,8 @@ impl Screen { /// Locate the first occurrence of `needle` scanning rows top to bottom; /// returns the `(row, col)` of its first character. + /// Mouse methods such as [`Terminal::drag`](crate::Terminal::drag) take + /// columns first, so destructure this pair before using it as mouse input. /// /// The **visible screen only**, like [`contains`](Self::contains): a /// needle that has scrolled into history is not found here, however diff --git a/crates/termlens/src/terminal.rs b/crates/termlens/src/terminal.rs index 9ff375a..ca84c81 100644 --- a/crates/termlens/src/terminal.rs +++ b/crates/termlens/src/terminal.rs @@ -2510,8 +2510,12 @@ impl Terminal { self.write_input(&bytes, "a mouse click") } - /// Drag from one cell to another: press, a motion report **per cell - /// crossed**, release. + /// Drag from `(from_col, from_row)` to `(to_col, to_row)`: press, a + /// motion report **per cell crossed**, release. + /// + /// Coordinates are columns first, matching [`click`](Self::click) and + /// the other mouse methods. [`Screen::find`](crate::Screen::find) returns + /// `(row, col)` instead, so destructure its result before passing it here. /// /// The path is a straight line, interpolated on both axes for a diagonal /// drag. A real pointer's exact path is not reproducible and does not @@ -2543,8 +2547,10 @@ impl Terminal { pub fn drag( &mut self, button: impl Into, - from: (u16, u16), - to: (u16, u16), + from_col: u16, + from_row: u16, + to_col: u16, + to_row: u16, ) -> Result<()> { self.ensure_deliverable("a mouse drag")?; let chord = button.into(); @@ -2562,6 +2568,8 @@ impl Terminal { MouseMode::ButtonMotion | MouseMode::AnyMotion => true, }; let (cols, rows) = self.screen().size(); + let from = (from_col, from_row); + let to = (to_col, to_row); // Endpoints only — see the Errors section above. check_mouse_in_grid(from.0, from.1, cols, rows)?; check_mouse_in_grid(to.0, to.1, cols, rows)?; diff --git a/crates/termlens/tests/input.rs b/crates/termlens/tests/input.rs index 568b9d7..1554bc9 100644 --- a/crates/termlens/tests/input.rs +++ b/crates/termlens/tests/input.rs @@ -234,7 +234,7 @@ fn buttons_modifiers_drag_and_horizontal_wheel_reach_the_wire() -> termlens::Res t.scroll_with(termlens::Scroll::Up.ctrl(), 5, 5)?; t.scroll_with(termlens::Scroll::Down.shift(), 5, 5)?; // Drag left button from (2,2) to (6,3): press, motion (0+32), release. - t.drag(termlens::MouseButton::Left, (2, 2), (6, 3))?; + t.drag(termlens::MouseButton::Left, 2, 2, 6, 3)?; t.wait_until(|s| s.row_text(0).contains("|"))?; let text = t.screen().row_text(0); @@ -279,7 +279,7 @@ fn a_drag_reports_one_motion_per_cell_crossed() -> termlens::Result<()> { t.wait_until(|s| s.contains("READY"))?; // Seven cells crossed, from column 5 to column 12 on row 4. - t.drag(termlens::MouseButton::Left, (5, 4), (12, 4))?; + t.drag(termlens::MouseButton::Left, 5, 4, 12, 4)?; t.wait_until(|s| s.row_text(0).contains("|"))?; let text = t.screen().row_text(0); @@ -317,7 +317,7 @@ fn press_release_tracking_still_gets_no_motion_at_all() -> termlens::Result<()> ]) .spawn("/bin/sh")?; t.wait_until(|s| s.contains("READY"))?; - t.drag(termlens::MouseButton::Left, (5, 4), (12, 4))?; + t.drag(termlens::MouseButton::Left, 5, 4, 12, 4)?; t.wait_until(|s| s.row_text(0).contains("|"))?; let text = t.screen().row_text(0); assert!(!text.contains("[<32;"), "no motion under ?1000:\n{text}"); @@ -339,7 +339,7 @@ fn drag_is_refused_when_the_mode_cannot_express_it() -> termlens::Result<()> { t.wait_until(|s| s.contains("READY"))?; let err = t - .drag(termlens::MouseButton::Left, (1, 1), (4, 4)) + .drag(termlens::MouseButton::Left, 1, 1, 4, 4) .expect_err("X10 reports presses only"); assert!(matches!(err, Error::Input(_)), "got: {err}"); assert!(err.to_string().contains("X10"), "unhelpful: {err}"); @@ -428,7 +428,7 @@ fn mouse_events_outside_the_grid_are_refused() -> termlens::Result<()> { assert!(err.to_string().contains("20x5"), "{err}"); let err = t - .drag(termlens::MouseButton::Left, (0, 0), (20, 0)) + .drag(termlens::MouseButton::Left, 0, 0, 20, 0) .expect_err("off-grid drag endpoint"); assert!(matches!(err, Error::Input(_)), "got: {err}"); assert!(err.to_string().contains("(20, 0)"), "{err}"); @@ -550,8 +550,7 @@ fn a_mouse_click_at_a_departed_child_blames_the_child() -> termlens::Result<()> for err in [ t.click(1, 1).unwrap_err(), t.scroll(1, 1, Scroll::Up).unwrap_err(), - t.drag(termlens::MouseButton::Left, (1, 1), (2, 2)) - .unwrap_err(), + t.drag(termlens::MouseButton::Left, 1, 1, 2, 2).unwrap_err(), ] { assert!(matches!(err, Error::Write { .. }), "got: {err}"); assert!( diff --git a/skills/termlens/SKILL.md b/skills/termlens/SKILL.md index 311e12a..516115c 100644 --- a/skills/termlens/SKILL.md +++ b/skills/termlens/SKILL.md @@ -96,9 +96,9 @@ your test ── send(Key) · click · paste · resize ──▶ PTY └─ col)`, `row_text(row)`, `cursor()` → `(row, col, visible)`. Everything that speaks of terminal geometry or a pointer is **column-first**: `size()` → `(cols, rows)`, `resize(cols, rows)`, `click(col, row)`, - `scroll(col, row, …)`, `drag(button, (col, row), (col, row))`. Never - pass a `find` result straight into `drag` — the tuple types match and - the axes do not. + `scroll(col, row, …)`, `drag(button, from_col, from_row, to_col, + to_row)`. The four drag arguments make a transposed `find` result + unwritable without explicitly choosing the coordinate order. 7. **Always finish the process.** Send the quit key, `wait_exit()?` and assert on the `ExitStatus` (`success()`, `code()`, `signal()`), then @@ -366,8 +366,8 @@ Read the first line for the cause: **Drive**: `send(Key)`, `send_str("text")` (no Enter — send `Key::Enter` yourself; `"\n"` would send LF, not CR), `paste("text")` (bracketed if the app enabled it), `send_after(delay, Key)`, `click(col, row)`, -`click_with(MouseButton::Right, col, row)`, `drag(MouseButton::Left, (c, r), -(c, r))`, `scroll(col, row, Scroll::Down)`, `resize(cols, rows)`, +`click_with(MouseButton::Right, col, row)`, `drag(MouseButton::Left, from_c, +from_r, to_c, to_r)`, `scroll(col, row, Scroll::Down)`, `resize(cols, rows)`, `focus_in()` / `focus_out()`, `signal(Signal::Term)` (Unix), `pid()`. **Keys**: `Key::Char('j')`, `Enter`, `Esc`, `Tab`, `BackTab`, `Backspace`, @@ -411,7 +411,7 @@ the embedded screen when there is one. | `t.wait_until(a)?; assert!(t.screen().b);` | `t.wait_until(\|s\| a(s) && b(s))?;` | | `.size(0, 0)` / `.size(1, 1)` | leave the 80x24 default, or `.size(cols, rows)` with both in 2..=1000 | | `Terminal::builder().spawn("myapp")` | `termlens::bin!("myapp")?` — a name on `PATH` is not your binary, and under `env_clear` there is no `PATH` | -| `t.click(row, col)` / `t.drag(b, s.find("x").unwrap(), …)` | `t.click(col, row)`; destructure the `find` result and swap | +| `t.click(row, col)` / passing `s.find("x")` coordinates to `t.drag(b, …)` unchanged | `t.click(col, row)`; destructure the `find` result and pass each coordinate in column-first order | | `t.send_str("quit\n")` | `t.send_str("quit")?; t.send(Key::Enter)?;` | | `t.wait_frame(…)` against a ratatui app | `t.snapshot_after(…)` unless the app emits synchronized updates | | `t.click(3, 4)?` as the first thing after spawn | `t.wait_until(\|s\| s.mouse_mode() != MouseMode::None)?;` first |