Encode keys where the keyboard modes are authoritative #323
No reviewers
Labels
No labels
priority/P0
priority/P1
priority/P2
release/v0.1.0
status/blocked
status/planned
type/bug
type/design
type/test-gap
type/tracker
No milestone
No project
No assignees
1 participant
Due date
No due date set.
Dependencies
No dependencies set
Reference
natsukium/felis!323
Loading…
Reference in a new issue
No description provided.
Delete branch "daemon-key-encoding"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #309.
Stacks on #319 (
connection-mode-minor, issue #312) and must merge after it. This branch is cut fromorigin/connection-mode-minor, which holdsPROTOCOL_MINOR11; this one takes 12. The number is provisional: twosibling branches (#307, #310) also take 12/13, and whoever merges later renumbers on rebase. It is a two-line change —
MINOR_STRUCTURED_KEYincrates/felis-protocol/src/preface.rsand its ledger row indocs/reference/ipc.md— plusthe
since=on the arm's// arm:line, whichjust protoand the arm-table test keep honest.The race
A keystroke's bytes depend on keyboard modes the daemon parses and the client only mirrors, through
GridMsg::ModeFlagsand
KittyKbdFlags. A producer can enable a mode and read input in the window between the daemon parsing it and thatgrid frame reaching the client; an occluded, pull-paced window holds the window open indefinitely. Pushing mode frames
eagerly does not close it, because nothing orders the daemon's next frame against the user's next key.
Mouse input already avoids this by sending the event and letting the daemon encode it. Keyboard input now does the same.
Wire shape
NamedKey,FKey,KeyLocation,KeyandKeyModsmove intofelis-protocolas wire vocabulary;felis-client-core's keymap re-exports them (KeyModscarries aCONTROLalias so the chord grammar keeps itsspelling).
InputModsis untouched: the mouse arm still speaks xterm's shift/alt/ctrl triple, andKeyModsis wider bySUPERalone.On the proto side:
InputKeyEventat field 10 of theInputMsgoneof, withKeyId(a oneof over theNamedKeyenum,an F-key index, a character string, and
KeyOther),optional string text(so an empty composition stays distinct froman absent one),
KeyEventKindandKeyLocation.KeyByteskeeps its meaning for literal bytes:sessions send --raw,send_string, a committed IME string, a filedrop.
Pasteis unchanged.Where the encoder lives
crates/felis-client-core/src/key_encode.rs→crates/felis-daemon/src/serve/key_encode.rs, with its 102 unit testsand 11 proptests (the test module keeps a local press/release shim so the existing cases read unchanged). It is still
exactly one encoder — that is what the rewritten rejection record in
control-surfaces.mdnow says, with the stale-moderace as the reason instead of encoder duplication.
handle_admitted_inputencodes undersession.lock_core(), offgrid.kitty_kbd_flags()+grid.mode_snapshot()—the same lock
InputMsg::Mousereads — then writes throughstreaming::write_pty_reserved, carrying the permit. AKeypromotes input ownership only if it encodes to bytes, likeMouse.Limits
MAX_KEY_CHARACTER_BYTESKey::Character, refused at admission withInputOverLimitMAX_KEY_TEXT_BYTESKeyEvent.text, sameMAX_KEY_REPORT_BYTESinput_reservation_bytesfor everyKeyMAX_KEY_REPORT_BYTES = KEY_REPORT_FIXED_BYTES + 8 * MAX_KEY_TEXT_BYTES = 24 + 8 × 32. The widest form any modeproduces is Kitty CSI u with alternate keys and associated text. Fixed part:
ESC [(2), two keycodes of at most 7decimal digits each (a Unicode scalar tops out at 1114111 and every Kitty functional code is smaller) plus their
:(15),
;plus a two-digit modifier field (3),:plus a one-digit event type (2), the;opening the text field (1),the final
u(1) = 24. Each text codepoint costs at most 7 digits plus one separator, and a codepoint occupies at leastone UTF-8 byte, so the 32-byte text cap admits at most 32 of them. The derivation is written out in
docs/reference/protocols/key-encoding.md"Report size" and cited from the constant.Repeat
New behavior, not a move. A held key reports Kitty event type 2 while bit 2 (event types) is active; without it, and
under every other encoding, a repeat encodes as a press. win32-input-mode reports both edges through
Kdand keepsRc = 1, since each repeat arrives as its own event.Mixed minor
A daemon below minor 12 cannot be typed at at all: the encoding reads modes only it owns, so there is no client-side
fallback left.
check_key_input_minorrefuses the landing indial::land_on(every landing ends attached to a sessionsurface) rather than letting the send gate drop each key with a warning, and
sessions send --keyexits2underprotocol_mismatch. Both are documented in the ledger row and incli.md.Tests added
serve::session_task::tests::a_key_encodes_against_a_mode_the_subscriber_has_not_been_told_about— the requiredstale-mode race test. Advances
?1hstraight into the grid so nothing is fanned out, sends aKey, and asserts thechild received
ESC O A; then drains the subscriber's queue and asserts theModeFlagsmirror was still sittingundelivered when the key was encoded.
serve::tests::a_key_parked_on_a_full_budget_holds_back_the_roster_query_behind_it— the saturated-budget barriertest. A budget-filling paste, then a
Key, thenOps::Liston the same connection: the roster must not be answeredwithin a second, and must be answered once the child drains. This is the barrier
felis sessions sendrelies on toknow its input was not dropped.
serve::key_encode::tests::no_mode_encodes_a_capped_key_past_the_reserved_report_size— proptest over every named keyand F-key, capped character and text, all 16 modifier sets, all three event kinds, all four locations, every Kitty
flag subset, all three
modifyOtherKeyslevels, DECCKM, DECKPAM and win32.serve::session_task::tests::a_repeat_reports_kitty_event_type_two_only_while_event_types_are_onserve::tests::a_key_reserves_the_worst_case_report_and_refuses_an_oversized_oneconvert::input::tests::{an_f_key_index_outside_the_supported_range_is_rejected, an_absent_or_unspecified_named_key_is_rejected, a_key_event_without_a_key_is_rejected}messages::input::tests::key_mods_remap_every_bit_and_drop_super(moved from the keymap withto_input_mods)input::tests::{a_held_key_reports_repeat_and_a_release_reports_release, a_key_event_carries_the_facts_the_daemon_encodes_from}cli_sessions::tests::a_chord_becomes_the_key_event_a_keystroke_would, and the frame-inspection test is nowkey_enter_rides_its_own_structured_frame_after_the_payloadfuzz/seeds/ipc_body/proto-input-key-kitty-text.binGates
just proto,just proto-compat(wire-compatible),cargo nextest run --workspace --all-features,cargo clippy --workspace --all-targets,just prose-check,just docs-links.Two
prose-checkcomment-length hits remain in the PR range and are not from this branch:felis.proto:162andconnector.rs'scheck_mode_minorare both lines #319 adds. They clear when this rebases onto a merged #319.origin/mainmerged in at60419785(merge commit, no rebase, no force-push).The stacking note at the top of the description is now stale: #319 landed as a squash (
48fdbce8,PROTOCOL_MINOR11), so main is no longer a descendant of this branch's base and the histories had to be merged rather than rebased.Resolutions:
CHANGELOG.mdkeeps both entries, this branch's minor-12 entry first.PROTOCOL_MINORstays at the provisional 12. Main's merged 11 (MINOR_UNKNOWN_MODE) keeps its ledger row, itsRequiresgate and itssince_minor, andInput::Keyremains the only row above it. Renumbering to 14 after #321 and #322 land is still the same two-line change.serve.rstakes main's reviewed Hello mode peek and keeps this branch'sInput::Keyreservation arm.connector.rskeepscheck_key_input_minorbeside main's rewordedcheck_mode_minor.docs/explanation/architecture/ipc.md) are taken from main verbatim, and the generated proto is regenerated.Green after the merge: clippy clean across the workspace, 778 tests in felis-protocol + felis-daemon, 1112 in felis-client, felis-client-core, felis-cli and felis-transport,
just proto-compatwire-compatible with the merge base,just docs-linksandprose_check --range main...HEADclean.#317 landed on main after this merge. It touches only the transport driver, so it does not conflict and is not merged in here.
origin/main(57d894ea) merged in at877aae1e, and the provisional minor renumbered from 12 to 14.#321 took 12 (
GridMsg::CycleEnd) and #322 took 13 (attach by prefix), soInputMsg::Keysits above them. The renumber touched:MINOR_STRUCTURED_KEY, itsMINOR_LEDGERrow after main's 13, the ledger row indocs/reference/ipc.md, thePROTOCOL_MINORheader comment infelis.proto, thesince=on theInput::Keyarm line, both frozen preface byte arrays (0x0Cto0x0E), theKey(KeyEvent)and older-peer prose indocs/reference/ipc.md,docs/reference/cli.md, and the CHANGELOG heading.just protoregenerated.Conflict resolutions:
CHANGELOG.mdkeeps every entry, this branch's (now minor 14) first.crates/felis-cli/src/cli_sessions.rs: main folded the empty-payload short circuit and the attach intoattach_by_prefix, so only the--keyminor gate stays from this side.crates/felis-protocol/src/messages/limits.rsimports both main'sAttachTargetand this branch'sKey.preface.rs,felis.protoand the ipc.md ledger keep main's rows 12 and 13 and add this branch's row at 14.Green: clippy clean across the workspace; 793 tests in felis-protocol + felis-daemon, 799 in felis-client-core + felis-cli;
just proto-compatwire-compatible with the merge base;just docs-links0 errors;prose_check --range origin/main...HEAD0 hits. All pre-commit hooks ran (no--no-verify).#318 is still open; expect one more small CHANGELOG re-merge once it lands.