summaryrefslogtreecommitdiff
path: root/src/p4.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/p4.zig')
-rw-r--r--src/p4.zig69
1 files changed, 67 insertions, 2 deletions
diff --git a/src/p4.zig b/src/p4.zig
index bf9d1c7d..cb8a5790 100644
--- a/src/p4.zig
+++ b/src/p4.zig
@@ -259,6 +259,21 @@ export fn pardes_p4_init(
// works over a serial line: the replies arrive as ordinary input and are parsed like any key.
vx.enterAltScreen(&out) catch |err| return errCode(err);
vx.queryTerminalSend(&out) catch |err| return errCode(err);
+
+ // MOUSE REPORTING, spelled out here rather than taken from `vx.setMouseMode`.
+ //
+ // vaxis enables `1002;1003;1004;1006`, and 1003 is ANY-MOTION tracking: the terminal reports
+ // every cell the pointer crosses with no button held. On a 115200 line that is unaffordable -
+ // one sweep across this grid is dozens of reports of ~15 bytes each, and each one arrives as
+ // input that the editor must parse while it is trying to paint. Worse, it arrives whether or not
+ // anybody wants it, so moving the mouse over the window would starve typing.
+ //
+ // 1002 reports presses, releases and motion WHILE A BUTTON IS HELD, which is exactly the set a
+ // click and a drag-select need. 1004 is focus in/out, which `apply` already handles. 1006 is the
+ // SGR encoding: unlike the original X10 form it is not limited to column 223, which a grid this
+ // small does not need today but costs nothing to have and cannot be added later without the
+ // terminal disagreeing with the editor about where the pointer is.
+ out.writeAll("\x1b[?1002;1004;1006h") catch |err| return errCode(err);
out.flush() catch |err| return errCode(err);
// The CLAMPED geometry, because the core and vaxis must agree on the grid and vaxis was just
@@ -293,8 +308,33 @@ export fn pardes_p4_input(ptr: [*]const u8, len: usize) callconv(.c) void {
in_len += take;
}
+ drainInput(c, false);
+}
+
+/// Parse what has accumulated, applying every event it yields.
+///
+/// THE LONE ESCAPE IS AMBIGUOUS, and on this transport it is ambiguous constantly. `vaxis.Parser`
+/// resolves a buffer containing nothing but `0x1b` as the Escape KEY - deliberately, and correctly
+/// for a real terminal, where the kernel hands over a whole escape sequence in one read so a solitary
+/// ESC really does mean the key. A 115200 serial line hands over one byte at a time: 87 us apart,
+/// which is an eternity to this loop. So the first byte of EVERY escape sequence arrived alone and
+/// was resolved as Escape, and the rest arrived as ordinary keys.
+///
+/// That is not a mouse bug, though it is why the mouse did not work: a click report came through as
+/// ten key presses - Escape, `[`, `<`, `0`, ... - and the `0` among them is "go to column zero" in
+/// normal mode, which is exactly where the cursor kept landing. Arrow keys, function keys and the
+/// host's in-band resize reports were all being shredded the same way.
+///
+/// Longer partial sequences were never affected: the CSI scanner returns `n == 0` for "no final byte
+/// yet", and the loop below keeps those bytes. Only the one-byte case needed an answer, because it is
+/// the only one the parser answers wrongly instead of declining.
+///
+/// `force` is how a real Escape keypress still works: `pardes_p4_tick` calls with it set once the
+/// hold has lasted longer than any serial line would take to deliver the next byte.
+fn drainInput(c: *pardes.Pardes, force: bool) void {
var off: usize = 0;
while (off < in_len) {
+ if (!force and in_len - off == 1 and in_buf[off] == 0x1b) break;
const res = parser.parse(in_buf[off..in_len], gpa()) catch break;
if (res.n == 0) break; // incomplete: wait for more bytes
off += res.n;
@@ -305,6 +345,11 @@ export fn pardes_p4_input(ptr: [*]const u8, len: usize) callconv(.c) void {
std.mem.copyForwards(u8, in_buf[0 .. in_len - off], in_buf[off..in_len]);
in_len -= off;
}
+ // Start or clear the hold. `esc_held_at` is only ever set for a buffer that is exactly one ESC,
+ // so a partial CSI - which the parser already declines - does not start a timer it does not need.
+ if (in_len == 1 and in_buf[0] == 0x1b) {
+ if (esc_held_at == null) esc_held_at = last_now_ms;
+ } else esc_held_at = null;
}
/// One parsed vaxis event applied to the core. Mirrors the tty shell's `apply`
@@ -475,16 +520,36 @@ fn mapKey(cp: u21) u21 {
else => cp,
};
}
-
export fn pardes_p4_tick(now_ms: u64) callconv(.c) void {
const c = core orelse return;
- _ = now_ms;
+ last_now_ms = now_ms;
+ // The held Escape, released. Anything still waiting after this long is a key the human pressed,
+ // not the head of a sequence: the next byte of a real sequence is 87 us behind on this line, and
+ // even a slow terminal emulator answers a query in well under a millisecond. Ten is generous by
+ // two orders of magnitude and imperceptible to the person pressing it - the same trade every
+ // terminal editor makes for the same reason.
+ if (esc_held_at) |at| {
+ if (now_ms -% at >= esc_hold_ms) {
+ drainInput(c, true);
+ dirty = true;
+ }
+ }
if (c.animationActive()) {
c.update(.tick);
dirty = true;
}
}
+/// How long a lone ESC waits for a second byte before it counts as the Escape key.
+const esc_hold_ms = 10;
+
+/// The last timestamp `pardes_p4_tick` was given, so `drainInput` can date a hold without needing a
+/// clock of its own - there is no clock on this side of the ABI.
+var last_now_ms: u64 = 0;
+
+/// When the buffer became a lone ESC, or null when it is not holding one.
+var esc_held_at: ?u64 = null;
+
export fn pardes_p4_wants_frame() callconv(.c) bool {
const c = core orelse return false;
return dirty or c.animationActive();