native: state the input ABI's REPLIES and overlap rules

Two gaps in the ot_session_input_* contract survived independent
refutation:
- REPLIES also decides how a complete CSI 1 ; N R is classified: a
  cursor report, or modified F3. The header described only its timeout
  effect, and did not say that setup and resume queries count as
  outstanding.
- feed copies paste bytes from the input into payload in the same call,
  so the buffers must not overlap.
Documentation only; the behavior is unchanged.

The comptime check compared one constant per family. Check every
parser constant against its OT_INPUT_* twin by name instead, which
needs key.kp0 renamed to kp_0. The busy-Context test now tells one
rejected ESC from a parsed one by its expiry.
This commit is contained in:
Simon Klee committed 2026-10-09 06:47:06 +02:00
1 parent b1ae566191
commit 4c3a53ecd8
5 files changed
+48 -41

No files matched your search

+7 -5
View File
@@ -961,7 +961,7 @@ typedef struct ot_input_drain {
* at least OT_INPUT_EVENTS_MIN and payload_capacity at least
* OT_INPUT_PAYLOAD_BYTES_MIN; the call consumes input until either runs low and
* reports consumed. Records and payload are borrowed for the call and never
* retained. Accepted in any open session state; closing, closed, failed, and
* retained. bytes, records, payload, and out_drain must not overlap. Accepted in any open session state; closing, closed, failed, and
* cancelled sessions reject. No allocation, no I/O. */
ot_status ot_session_input_feed(
ot_context *context,
@@ -976,10 +976,12 @@ ot_status ot_session_input_feed(
ot_input_drain *out_drain);
/* flags is a combination of OT_INPUT_EXPECT_* bits; other bits must be zero.
* REPLIES keeps a partial CSI reply waiting past the timeout while the host
* awaits one. KITTY_KEYBOARD does the same for Kitty keys; the session also
* applies it while its terminal has Kitty keyboard enabled. Changing flags
* emits nothing; the next feed resolves a unit that may no longer wait. */
* Set REPLIES while any query is outstanding, including the capability queries
* that setup and resume publish. It keeps a partial CSI reply waiting past the
* timeout, and it makes a complete CSI 1 ; N R (N >= 2) a CURSOR_POSITION reply
* instead of a modified F3 key. KITTY_KEYBOARD keeps a partial Kitty key waiting;
* the session also applies it while its terminal has Kitty keyboard enabled.
* Changing flags emits nothing; the next feed resolves a unit that may no longer wait. */
ot_status ot_session_input_expect(ot_context *context, const ot_handle *session, uint32_t flags);
/* Drop any partial unit and mouse button state. Expectations are kept. */
+7 -4
View File
@@ -2834,6 +2834,7 @@ ot_status ot_session_pump(
* OT_INPUT_PAYLOAD_BYTES_MIN; the call consumes input until either runs low and
* reports consumed. A bracketed paste arrives as one or more PASTE records from
* START to END. Records and payload are borrowed for the call and never retained.
* bytes, records, payload, and out_drain must not overlap.
* Accepted in any open session state; closing, closed, failed, and cancelled
* sessions reject. A rejected call leaves out_drain, the records, and parser
* state unchanged. No allocation, no I/O. */
@@ -2850,10 +2851,12 @@ ot_status ot_session_input_feed(
ot_input_drain *out_drain);
/* flags is a combination of OT_INPUT_EXPECT_* bits; other bits must be zero.
* REPLIES keeps a partial CSI reply waiting past the timeout while the host
* awaits one. KITTY_KEYBOARD does the same for Kitty keys; the session also
* applies it while its terminal has Kitty keyboard enabled. Changing flags
* emits nothing; the next feed resolves a unit that may no longer wait. */
* Set REPLIES while any query is outstanding, including the capability queries
* that setup and resume publish. It keeps a partial CSI reply waiting past the
* timeout, and it makes a complete CSI 1 ; N R (N >= 2) a CURSOR_POSITION reply
* instead of a modified F3 key. KITTY_KEYBOARD keeps a partial Kitty key waiting;
* the session also applies it while its terminal has Kitty keyboard enabled.
* Changing flags emits nothing; the next feed resolves a unit that may no longer wait. */
ot_status ot_session_input_expect(ot_context *context, const ot_handle *session, uint32_t flags);
/* Drop any partial unit and mouse button state. Expectations are kept. */
+28 -26
View File
@@ -13,31 +13,31 @@ comptime {
if (@offsetOf(Event, field.name) != @offsetOf(c.ot_input_event, field.name))
@compileError("ot_input_event differs from input_parser.Event: " ++ field.name);
}
const Kind = input_parser.Kind;
const flags = input_parser.flags;
const checks = .{
.{ c.OT_INPUT_KEY, @intFromEnum(Kind.key) },
.{ c.OT_INPUT_MOUSE, @intFromEnum(Kind.mouse) },
.{ c.OT_INPUT_PASTE, @intFromEnum(Kind.paste) },
.{ c.OT_INPUT_FOCUS, @intFromEnum(Kind.focus) },
.{ c.OT_INPUT_REPLY, @intFromEnum(Kind.reply) },
.{ c.OT_INPUT_KEY_RELEASE, @intFromEnum(input_parser.KeyAction.release) },
.{ c.OT_INPUT_MOUSE_SCROLL, @intFromEnum(input_parser.MouseAction.scroll) },
.{ c.OT_INPUT_MOUSE_BUTTON_NONE, input_parser.mouse_button_none },
.{ c.OT_INPUT_MOD_NUM_LOCK, @as(u8, @bitCast(input_parser.Modifiers{ .num_lock = true })) },
.{ c.OT_INPUT_KEY_TEXT_TRUNCATED, flags.key_text_truncated },
.{ c.OT_INPUT_PASTE_END, flags.paste_end },
.{ c.OT_INPUT_REPLY_CURSOR_POSITION, flags.reply_cursor_position },
.{ c.OT_INPUT_REPLY_APC, @intFromEnum(input_parser.Protocol.apc) },
.{ c.OT_INPUT_KEY_KP_BEGIN, input_parser.key.kp_begin },
.{ c.OT_INPUT_KEY_F35, input_parser.key.f(35) },
.{ c.OT_INPUT_KEY_ISO_LEVEL5_SHIFT, input_parser.key.functional_last },
.{ c.OT_INPUT_EXPECT_KITTY_KEYBOARD, @as(u8, @bitCast(input_parser.Expectations{ .kitty_keyboard = true })) },
.{ c.OT_INPUT_EVENTS_MIN, input_parser.events_per_byte_max },
.{ c.OT_INPUT_PAYLOAD_BYTES_MIN, input_parser.payload_per_byte_max },
.{ c.OT_INPUT_TIMEOUT_NS, input_parser.timeout_ns },
};
for (checks) |check| if (check[0] != check[1]) @compileError("opentui.h input constant differs from input_parser");
@setEvalBranchQuota(20_000);
// Every parser constant has an equal header twin named OT_INPUT_<prefix><NAME>.
for (std.meta.tags(input_parser.Kind)) |tag| checkTwin("", @tagName(tag), @intFromEnum(tag));
for (std.meta.tags(input_parser.KeyAction)) |tag| checkTwin("KEY_", @tagName(tag), @intFromEnum(tag));
for (std.meta.tags(input_parser.MouseAction)) |tag| checkTwin("MOUSE_", @tagName(tag), @intFromEnum(tag));
for (std.meta.tags(input_parser.Protocol)) |tag| checkTwin("REPLY_", @tagName(tag), @intFromEnum(tag));
for (std.meta.fields(input_parser.Modifiers), 0..) |field, bit| checkTwin("MOD_", field.name, 1 << bit);
for (std.meta.fields(input_parser.Expectations)[0..2], 0..) |field, bit| checkTwin("EXPECT_", field.name, 1 << bit);
for (@typeInfo(input_parser.flags).@"struct".decls) |decl| checkTwin("", decl.name, @field(input_parser.flags, decl.name));
for (@typeInfo(input_parser.key).@"struct".decls) |decl| {
// Range bounds and the F(n) helper have no twin of their own.
if (std.mem.startsWith(u8, decl.name, "functional_") or std.mem.eql(u8, decl.name, "f")) continue;
checkTwin("KEY_", decl.name, @field(input_parser.key, decl.name));
}
checkTwin("", "mouse_button_none", input_parser.mouse_button_none);
checkTwin("", "events_min", input_parser.events_per_byte_max);
checkTwin("", "payload_bytes_min", input_parser.payload_per_byte_max);
checkTwin("", "timeout_ns", input_parser.timeout_ns);
}
fn checkTwin(comptime prefix: []const u8, comptime name: []const u8, comptime value: u64) void {
comptime var upper: [name.len]u8 = undefined;
_ = std.ascii.upperString(&upper, name);
const twin = "OT_INPUT_" ++ prefix ++ upper;
if (!@hasDecl(c, twin) or @field(c, twin) != value) @compileError(twin ++ " differs from input_parser");
}
const expect_flags: u32 = c.OT_INPUT_EXPECT_REPLIES | c.OT_INPUT_EXPECT_KITTY_KEYBOARD;
@@ -227,7 +227,9 @@ test "Context input ABI rejects malformed calls, foreign handles, and closed ses
try std.testing.expectEqual(c.OT_CONTEXT_BUSY, f.feed("\x1b", 1));
context.core.mutating = false;
try std.testing.expectEqual(c.OT_OK, f.feed("\x1b", 1));
try std.testing.expectEqual(@as(u32, 0), f.drain.count);
try std.testing.expectEqual(c.OT_OK, f.feed("", 1 + c.OT_INPUT_TIMEOUT_NS));
// One plain Escape: a parsed rejected ESC would make this Alt+Escape.
try std.testing.expectEqual(@as(u8, 0), f.records[0].modifiers);
const thread = try std.Thread.spawn(.{}, struct {
fn run(owner: *abi.ContextHandle, session: *const c.ot_handle) void {
std.testing.expectEqual(c.OT_WRONG_THREAD, ot_session_input_feed(owner, session, null, 0, 0, null, 0, null, 0, null)) catch unreachable;
+2 -2
View File
@@ -75,7 +75,7 @@ pub const key = struct {
pub const end: u32 = 57357;
pub const menu: u32 = 57363;
pub const f1: u32 = 57364;
pub const kp0: u32 = 57399;
pub const kp_0: u32 = 57399;
pub const kp_begin: u32 = 57427;
pub const functional_first: u32 = 57344;
pub const functional_last: u32 = 57454;
@@ -1041,7 +1041,7 @@ fn functionalKey(code: u32) ?u32 {
/// What a printable keypad key inserts.
fn keypadText(code: u32) ?u8 {
return switch (code) {
key.kp0...key.kp0 + 9 => @as(u8, @intCast('0' + code - key.kp0)),
key.kp_0...key.kp_0 + 9 => @as(u8, @intCast('0' + code - key.kp_0)),
57409 => '.',
57410 => '/',
57411 => '*',
@@ -569,7 +569,7 @@ test "input parser modifyOtherKeys" {
.{ "\x1b[27;2;127~", k(K.backspace, SHIFT) },
.{ "\x1b[27;5;8~", k(K.backspace, CTRL) },
.{ "\x1b[27;2;53~", k('5', SHIFT) },
.{ "\x1b[27;5;57399~", kt(K.kp0, CTRL, "0") },
.{ "\x1b[27;5;57399~", kt(K.kp_0, CTRL, "0") },
.{ "\x1b[27;5;3~", reply("\x1b[27;5;3~") },
.{ "\x1b[27;5~", reply("\x1b[27;5~") },
});
@@ -674,9 +674,9 @@ test "input parser Kitty keys report modifiers, events, and text" {
.{ "\x1b[57349;5u", k(K.delete, CTRL) },
.{ "\x1b[57376u", k(K.f(13), NONE) },
.{ "\x1b[57414u", kt(57414, NONE, "") },
.{ "\x1b[57399u", kt(K.kp0, NONE, "0") },
.{ "\x1b[57400;5u", kt(K.kp0 + 1, CTRL, "1") },
.{ "\x1b[57400;1;120u", kt(K.kp0 + 1, NONE, "x") },
.{ "\x1b[57399u", kt(K.kp_0, NONE, "0") },
.{ "\x1b[57400;5u", kt(K.kp_0 + 1, CTRL, "1") },
.{ "\x1b[57400;1;120u", kt(K.kp_0 + 1, NONE, "x") },
.{ "\x1b[57409u", kt(57409, NONE, ".") },
.{ "\x1b[57416u", kt(57416, NONE, ",") },
.{ "\x1b[57441u", k(57441, NONE) },