Skip to content

Refactor shell I/O handling with unified stdin reader#8

Merged
mariotaku merged 2 commits into
mainfrom
claude/ares-shell-pty-raw
Jun 14, 2026
Merged

Refactor shell I/O handling with unified stdin reader#8
mariotaku merged 2 commits into
mainfrom
claude/ares-shell-pty-raw

Conversation

@mariotaku

Copy link
Copy Markdown
Member

Summary

This PR refactors the shell I/O handling in both PTY and dumb modes to use a unified, cleaner approach for reading stdin and managing the event loop. The changes extract common I/O patterns into a new io module and simplify the main shell loops.

Key Changes

  • New io module: Extracted spawn_stdin_reader() function that reads stdin on a background thread and forwards bytes through a channel, replacing the previous per-mode event handling threads

  • Simplified PTY mode (pty.rs):

    • Removed complex EventThread struct and crossterm event handling
    • Replaced with simple stdin reader + ticker-based polling
    • Introduced RawMode RAII guard to ensure terminal is restored even on early return
    • Consolidated remote output reading into drain() helper function
    • Increased buffer size from 1KB to 8KB for better throughput
  • Simplified dumb mode (dumb.rs):

    • Removed EventThread struct in favor of shared spawn_stdin_reader()
    • Added crlf_to_lf() function to normalize line endings (CR/CRLF → LF) since dumb mode lacks remote line discipline
    • Includes comprehensive tests for line ending conversion, including edge cases with splits across chunks
    • Consolidated remote output reading into drain() helper function
    • Increased buffer size from 1KB to 8KB
  • Unified polling: Both modes now use crossbeam_channel::tick() for periodic polling with a 10ms interval, replacing ad-hoc sleep calls

  • Error handling: Extracted io_error() helper for consistent error conversion from libssh errors

  • Terminal handling: Improved TERM environment variable handling in main.rs

Implementation Details

  • The stdin reader thread runs indefinitely, reading into an 8KB buffer and sending chunks through an unbounded channel
  • Both shell modes now use select! to multiplex between stdin input and a periodic ticker
  • When local stdin reaches EOF, send_eof() is called on the remote channel to signal completion
  • The dumb mode's crlf_to_lf() function maintains state across chunk boundaries to handle CRLF pairs split across reads
  • All remote output is drained non-blockingly before checking for input, ensuring responsive output display

https://claude.ai/code/session_017vAGzSSYNcgdQzJRLWfX47

claude added 2 commits June 14, 2026 09:43
Make the interactive shell behave like plain `ssh`.

PTY input was reconstructed byte-by-byte from parsed crossterm key
events, which mistranslated a lot of input: Ctrl+<letter> was off by
one (Ctrl+C sent 0x02, so it never interrupted), non-ASCII/UTF-8 keys
were truncated to a single byte, Backspace sent a BS-SP-BS erase
sequence instead of DEL, and function keys used a bogus encoding.

Instead, put the terminal in raw mode and forward stdin bytes verbatim
to the channel, letting the remote PTY interpret them — control keys,
UTF-8 and escape sequences now pass through correctly. Local terminal
resizes are mirrored to the remote PTY, and raw mode is restored via an
RAII guard on every exit path.

Both shells previously spun in a 1ms sleep loop calling read_nonblocking.
Replace that with a select! over the stdin channel and a 10ms ticker:
the loop parks until there is input or it is time to drain output, so an
idle session no longer burns CPU while input stays responsive. Output is
now fully drained each wake, and the dumb shell reads both stdout and
stderr every cycle instead of alternating between them.

Also send the local $TERM when requesting the PTY and exit non-zero when
the shell loop fails.

https://claude.ai/code/session_017vAGzSSYNcgdQzJRLWfX47
Dumb mode has no remote pseudo-terminal, so there is no line discipline
on the far end to translate carriage returns. On platforms whose console
sends Enter as CR or CRLF (e.g. Windows), a typed `ls` arrives at the
remote shell as `ls\r`, which fails with `ls\r: not found` — and the
embedded CR rewinds the cursor so it shows up as just ": not found".

Translate CR and CRLF in the forwarded input to LF, mirroring what a
terminal line discipline would do, with carry-over state so a `\r\n`
split across reads still collapses to a single `\n`. PTY mode is
unaffected: it sends CR and lets the remote PTY do the translation.

https://claude.ai/code/session_017vAGzSSYNcgdQzJRLWfX47
@mariotaku mariotaku merged commit 65bb855 into main Jun 14, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants