Fix arrow keys over iOS SSH (split escape sequences + stale probe bytes)#56
Merged
Conversation
Two changes that together resolve broken/slow arrow keys when connecting from iOS SSH clients (Termius, Shelly) reported in #53: 1. Use a 5ms minimum poll timeout instead of 0 when a render is pending. The previous zero-timeout poll gave crossterm no window to reassemble multi-byte escape sequences. iOS terminals deliver \x1b[A across separate reads, so the leading \x1b was reported as a standalone Escape and the trailing [A surfaced as literal characters. 5ms is imperceptible to a human and reliably catches the trailing bytes. Applied to both the main poll and the drain loop. 2. Drain stale input at event-loop entry. The OSC 11 background query and Kitty/Sixel image-protocol probes can leave bytes in the TTY buffer when a terminal doesn't understand them — those bytes would otherwise surface as spurious key events on first user interaction. Closes #53.
5 tasks
CI was using `dtolnay/rust-toolchain@stable`, so every new stable release that added clippy lints could fail CI without anyone changing code. Pin to 1.95.0 in both ci.yml and release.yml, and add rust-toolchain.toml so local rustup picks up the same version automatically. To bump in the future, update all three values together.
Pinning the toolchain surfaces new lints that were never run against the existing code: - input.rs: collapse nested `if` guards into match-arm guards (collapsible_match) - model.rs: replace `sort_by` with `sort_by_key` (unnecessary_sort_by) - types.rs: drop redundant `.into_iter()` in `.zip()` (useless_conversion) - watcher/mod.rs: remove a trailing comma in `format!` args (unnecessary_trailing_comma)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Resolves #53 — arrow keys intermittently fail when connecting to markless over SSH from iOS clients (Termius, Shelly on iOS/iPadOS). Two independent root causes; both addressed here.
1. Split escape sequences (
src/app/event_loop.rs)The main
event::pollusedDuration::from_millis(0)whenever a render was pending, and the drain loop always used 0. iOS terminals deliver\x1b[Aacross separate reads, so crossterm saw a bare\x1b, reported it as a standalone Escape keypress, and the trailing[Acame through on the next read as literal text.Both the main poll and the drain now use a 5ms minimum. The selection logic is extracted into a
const fn select_poll_msso it's unit-testable. 5ms is imperceptible to a human and gives crossterm enough time to reassemble the sequence.2. Stale probe bytes (
src/app/event_loop.rs)When a terminal doesn't understand the OSC 11 background query or the Kitty/Sixel image-protocol probes, response bytes (or the echoed-back payload) can sit in the TTY input buffer. crossterm reads them later as spurious key events.
Added
drain_stale_input()called at event-loop entry — aftertry_init()puts the terminal in raw mode, before the main loop. Drains up to 256 events with a 20ms poll window so late-arriving probe responses are also caught. Logsevent.drain.startupwhen anything is discarded so it surfaces in perf logs.Test plan
cargo fmt --checkcargo clippy -- -D warningscargo test— 604 tests pass (3 new tests forselect_poll_mscovering all three branches)--no-imagesmodesNotes / follow-ups
src/main.rs:129spawns a reader thread that's never joined. If the terminal never responds (likely on iOS Termius), that thread blocks inread()on a/dev/ttyhandle and could compete with crossterm for keystrokes in the main loop. Not addressed here since it requires more invasive changes; worth a follow-up if iOS testing reveals lingering issues after this fix.