wijjit_ssh.input
Async byte-level input decoding for SSH sessions.
A local Wijjit app reads the keyboard through
wijjit.terminal.input.InputHandler, which polls prompt_toolkit on a
background thread. That is fine for one foreground app, but wrong at server
scale: it costs an OS thread (and a prompt_toolkit pipe) per connection, when
the bytes are already being delivered to us on the event loop by asyncssh.
This module replaces that path for remote sessions with two pieces:
KeyDecoderA pure, resumable state machine turning raw terminal bytes into Wijjit
KeyandMouseEventobjects. It buffers incomplete trailing sequences across calls, so a keystroke split across TCP packets (or a UTF-8 rune split mid-character) decodes correctly. No I/O, no threads, no clock - which also makes it exhaustively unit-testable.ChannelInputSourceThe event loop’s input handler for a session. It owns a decoder, pushes decoded events onto an
asyncio.Queue, and satisfies the duck-typed surface the loop calls (read_input_async,mouse_enabled,enable/disable_mouse_tracking,close,restore_terminal).
The one place a timer is unavoidable is the lone-ESC ambiguity: a bare ESC
byte is either the Escape key or the first byte of a sequence still in flight,
and nothing in the byte stream distinguishes them. ChannelInputSource
resolves it by scheduling KeyDecoder.flush() a few tens of milliseconds
after the last byte (see ESCAPE_TIMEOUT). SSH almost always delivers a
full sequence in one packet, so this rarely fires.
- class wijjit_ssh.input.KeyDecoder(mouse_parser=None)[source]
Bases:
objectResumable byte-to-event decoder for terminal input.
Feed it whatever bytes arrive on the wire; it returns the events it can fully decode and retains any incomplete trailing sequence for the next call. It is a pure state machine: no I/O, no threads, and no clock (the one time-dependent decision, the lone-ESC ambiguity, is delegated to the caller via
pending_escape()/flush()).- Parameters:
mouse_parser (MouseEventParser, optional) – Parser used for mouse sequences. Supply one to share click-synthesis state; by default a fresh parser is created.
- Variables:
mouse_parser (MouseEventParser) – The parser used for SGR and legacy mouse reports. It carries the press/release state that synthesizes CLICK and DOUBLE_CLICK events, so it must persist across calls.
- feed(data)[source]
Decode a chunk of raw terminal bytes.
- Parameters:
data (bytes) – Bytes as received from the channel. May contain any number of whole events, and may end mid-sequence or mid-rune.
- Returns:
Every event that could be fully decoded, in arrival order. An incomplete trailing sequence is retained internally for the next call and is not reported here.
- Return type:
list of Key or MouseEvent
- pending_escape()[source]
Whether the buffer holds a bare ESC awaiting disambiguation.
- Returns:
True when the only thing buffered is a single
ESCbyte, which is either the Escape key or the start of a sequence still in flight. The caller resolves this with a timer (seeESCAPE_TIMEOUT) and then callsflush().- Return type:
Notes
Deliberately narrow: a partial sequence (
ESC [with no final byte) is not reported, because the rest is almost certainly in the next packet and flushing it as Escape would corrupt a real keypress.
- flush()[source]
Resolve a pending lone ESC as the Escape key.
Called by the session once
ESCAPE_TIMEOUThas elapsed with no further bytes.- Returns:
[Keys.ESCAPE]if a bare ESC was buffered, else an empty list (bytes arrived in the meantime and already decoded).- Return type:
list of Key or MouseEvent
- class wijjit_ssh.input.ChannelInputSource(writer, *, enable_mouse=False, mouse_tracking_mode=None)[source]
Bases:
objectEvent-loop input handler backed by an SSH channel’s byte stream.
Implements the duck-typed surface Wijjit’s event loop expects of an input handler, but sourced from
feed()(called by the SSH session as bytes arrive) rather than from a thread polling a tty. Decoded events queue up and are handed to the loop one at a time byread_input_async().- Parameters:
writer (TextIO) – Stream that mouse-tracking escape sequences are written to - the SSH channel, so the sequences reach the client’s terminal rather than the server’s stdout.
enable_mouse (bool, optional) – Whether the app wants mouse tracking (default False). Tracking is not turned on here; the event loop calls
enable_mouse_tracking().mouse_tracking_mode (MouseTrackingMode, optional) – Tracking granularity to request (default
MouseTrackingMode.BUTTON_EVENT).
- Variables:
mouse_enabled (bool) – Whether mouse tracking is currently active on the client terminal (i.e. the enable sequences have been sent).
- feed(data)[source]
Decode inbound channel bytes and queue the resulting events.
- Parameters:
data (bytes) – Raw bytes received on the channel.
- Return type:
None
- async read_input_async(timeout=None)[source]
Wait for the next decoded input event.
- Parameters:
timeout (float or None, optional) – Maximum time to wait, in seconds.
Nonewaits indefinitely.- Returns:
The next event, or
Noneif the timeout expired first - which the event loop treats as a quiet frame (it uses the timeout to drive animations and pending re-renders).- Return type:
Key, MouseEvent, or None
- enable_mouse_tracking(mode=None)[source]
Turn on mouse reporting in the client’s terminal.
- Parameters:
mode (MouseTrackingMode, optional) – Tracking mode to request; defaults to the one given at construction.
- Return type:
None
- disable_mouse_tracking()[source]
Turn off mouse reporting in the client’s terminal.
- Return type:
None