Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ throws `RangeError` before reaching Ghostty, including for suppressed events.
Ordinary negative and off-screen positions remain supported.

When UTF-8 mouse mode (1005) is set, the binding also rejects clamped cell
coordinates whose encoded value would be a Unicode surrogate, or whose row
would overflow the pinned encoder's 16-bit arithmetic. This conservative guard
applies while 1005 remains set even if another mouse format was selected later.
coordinates whose encoded value would be a Unicode surrogate. This conservative
guard applies while 1005 remains set even if another mouse format was selected
later.

With the pinned Ghostty version, any non-empty `feed()` resets motion
deduplication when the next mouse event refreshes negotiated modes. Geometry
Expand Down
2 changes: 1 addition & 1 deletion native/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ C++ declarations:
- `encodeMouse` throws `TypeError("encodeMouse expects event and options objects")` unless it gets exactly two objects, and `TypeError("mouse geometry must be an object")` for a non-object `geometry` [native/terminal.cc:TerminalWrap::EncodeMouse]. `screenWidth`, `screenHeight`, `cellWidth`, and `cellHeight` must be integers from 1 to 2^32-1, and the four paddings integers from 0 to 2^32-1: a non-number throws `TypeError("<name> must be a positive integer")` or `"... non-negative integer"`, and any other bad value `RangeError("<name> must be a positive 32-bit integer")` or `"... non-negative 32-bit integer"` [native/terminal.cc:PositiveUint32] [native/terminal.cc:NonNegativeUint32].
- `encodeMouse` geometry limits, the same calculation that Ghostty makes before its unchecked casts: the padding sum of an axis, added in 64 bits, must not exceed the screen size (`RangeError("mouse geometry padding exceeds screenWidth")`, or `screenHeight`), and `f32(screen - padding) / f32(cell)` must be below 65536 (`RangeError("mouse geometry.screenWidth exceeds the 65535-cell limit")`, or `screenHeight`) [native/terminal.cc:ValidateMouseAxis].
- `encodeMouse` coordinates: A non-number `x` or `y` throws `TypeError("mouse x must be a finite number")`, and a value that is not finite or is outside the f32 range throws `RangeError("mouse x must be a finite 32-bit float")`. After the f32 rounding and the padding subtraction, the value must fit a signed 32-bit integer, and the value divided by the cell size must be below 65536. Otherwise it throws `RangeError("mouse x is outside Ghostty's cell or pixel coordinate range")`. The check runs for every event, also one that the mode drops [native/terminal.cc:MouseCoordinate].
- `encodeMouse` with mode 1005 (UTF-8) set: it computes the clamped cell as Ghostty does and throws `RangeError("mouse x is unsafe for Ghostty's UTF-8 mouse encoding")` when `cell + 33` is a UTF-16 surrogate (0xD800 to 0xDFFF), or for `y` also above 65535. It checks while 1005 is set, even when a later mode selected another format [native/terminal.cc:ValidateUtf8MouseCoordinate].
- `encodeMouse` with mode 1005 (UTF-8) set: it computes the clamped cell as Ghostty does and throws `RangeError("mouse x is unsafe for Ghostty's UTF-8 mouse encoding")` when `cell + 33` is a UTF-16 surrogate (0xD800 to 0xDFFF). It checks while 1005 is set, even when a later mode selected another format [native/terminal.cc:ValidateUtf8MouseCoordinate].
- `encodeMouse` `action` must be `press`, `release`, or `motion`, and `button` (when not `undefined`) one of `left`, `right`, `middle`, `four` to `eleven`. A non-string throws `TypeError("mouse action must be a string")` or `"mouse button must be a string"`, an unknown string `RangeError("mouse action is invalid")` or `"mouse button is invalid"`. `modifiers`, when defined, must be an object (`TypeError("mouse modifiers must be an object")`), and `shift`, `ctrl`, `alt`, `anyButtonPressed`, and `trackLastCell` count only when they are boolean `true` [native/terminal.cc:ParseMouseAction] [native/terminal.cc:ParseMouseButton] [native/terminal.cc:ParseMouseModifiers].
- Mouse mode cache: a non-empty `feed` sets `mouse_modes_dirty_`. The next `encodeMouse` that passes its checks then reloads the tracking mode and format with `ghostty_mouse_encoder_setopt_from_terminal`. The pinned Ghostty also resets its same-cell motion deduplication there (see "Mouse encoding" in the root README). A call that throws leaves `mouse_modes_dirty_` set. `encodeMouse` sets the size option only when the geometry differs from the last one [native/terminal.cc:TerminalWrap::Feed] [native/terminal.cc:TerminalWrap::EncodeMouse].
- `feed` throws `TypeError` for an argument count other than 1 (`"feed expects exactly one argument"`), for a typed array that is not a `Uint8Array` (`"feed typed array must be Uint8Array"`), and for any other value (`"feed data must be a string, Buffer, or Uint8Array"`). Zero-length input returns without a libghostty-vt call [native/terminal.cc:TerminalWrap::Feed].
Expand Down
13 changes: 6 additions & 7 deletions native/terminal.cc
Original file line number Diff line number Diff line change
Expand Up @@ -106,17 +106,16 @@ float MouseCoordinate(Napi::Env env, const Napi::Value& value, const char* name,

void ValidateUtf8MouseCoordinate(Napi::Env env, float position, uint32_t screen,
uint32_t cell, uint32_t before, uint32_t after,
const char* name, bool is_row) {
const char* name) {
// Match Ghostty's f32 grid size and f64 position conversion, including
// viewport clamping. The pinned UTF-8 encoder assumes Unicode scalars and
// adds 33 to rows in u16 arithmetic before widening.
// viewport clamping. The pinned UTF-8 encoder treats a surrogate code point
// as unreachable.
const auto cells = std::max<uint32_t>(1, static_cast<uint32_t>(
static_cast<float>(screen - before - after) / static_cast<float>(cell)));
const auto index = static_cast<uint32_t>(
std::max(0.0, static_cast<double>(position) - before) / cell);
const auto codepoint = std::min(index, cells - 1) + 33;
if ((codepoint >= 0xd800 && codepoint <= 0xdfff) ||
(is_row && codepoint > 65535)) {
if (codepoint >= 0xd800 && codepoint <= 0xdfff) {
throw Napi::RangeError::New(env, std::string(name) + " is unsafe for Ghostty's UTF-8 mouse encoding");
}
}
Expand Down Expand Up @@ -395,9 +394,9 @@ Napi::Value TerminalWrap::EncodeMouse(const Napi::CallbackInfo& info) {
// these exceptional cells whenever 1005 is set, even if another format was
// selected later. Ordinary geometry and SGR-only large grids are unchanged.
ValidateUtf8MouseCoordinate(env, position.x, size.screen_width, size.cell_width,
size.padding_left, size.padding_right, "mouse x", false);
size.padding_left, size.padding_right, "mouse x");
ValidateUtf8MouseCoordinate(env, position.y, size.screen_height, size.cell_height,
size.padding_top, size.padding_bottom, "mouse y", true);
size.padding_top, size.padding_bottom, "mouse y");
}

if (mouse_modes_dirty_) {
Expand Down
6 changes: 5 additions & 1 deletion test/mouse-safety.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,13 @@ describe.each([
const term = createTerminal({ cols: 80, rows: 24 });
term.feed("\\x1b[?1000h\\x1b[?1005h");
const options = { geometry: ${JSON.stringify({ ...geometry, screenWidth: 65535, screenHeight: 65535 })} };
for (const [x, y] of [[55263, 0], [0, 55263], [57310, 0], [0, 65503]]) {
for (const [x, y] of [[55263, 0], [0, 55263], [57310, 0]]) {
assert.throws(() => term.encodeMouse({ action: "press", button: "left", x, y }, options), RangeError);
}
for (const [y, row] of [[65503, "\\xf0\\x90\\x80\\x80"], [65534, "\\xf0\\x90\\x80\\x9f"]]) {
assert.deepEqual(term.encodeMouse({ action: "press", button: "left", x: 0, y }, options),
Buffer.from("\\x1b[M\\x20\\x21" + row, "latin1"));
}
// Clamping happens before UTF-8 conversion: a distant release on a small
// viewport is safe even when the original coordinate was problematic.
const bytes = term.encodeMouse({ action: "release", button: "left", x: 55263, y: 65503 },
Expand Down
Loading