Skip to content

Migrate readline from rustyline to reedline#30

Merged
maxholman merged 21 commits intomainfrom
feat/reedline
Feb 23, 2026
Merged

Migrate readline from rustyline to reedline#30
maxholman merged 21 commits intomainfrom
feat/reedline

Conversation

@maxholman
Copy link
Copy Markdown
Contributor

Migrate readline from rustyline to reedline

Branch

feat/reedline — based on feat/version-and-startup-ux

Scope

crates/cli/Cargo.toml, src/entry.rs, src/exit.rs, src/repl_common.rs

The #[cfg(not(feature = "repl"))] non-REPL path must continue to work unchanged.
The feature flag is named repl (renamed from readline as part of this branch).

Out of scope

Core wallhack logic, transports, netstack, protobuf, website, bench.
Do not change command parsing, REPL command set, or output formatting.

Why

Problems with the current rustyline implementation

The current readline implementation is broken. No REPL commands produce visible output.
The root cause is architectural: rustyline's ExternalPrinter only works correctly while
rl.readline() is actively blocking for input. Command responses are generated after
readline() returns (the command is sent to an async task, which processes it and sends
responses back through the print channel). By that point, readline() is no longer active,
so ExternalPrinter buffers the responses and only flushes them when readline() is called
again for the next prompt — after the prompt has already been drawn.

Concrete symptoms observed:

  • Commands such as peers, info, stats produce no visible output
  • Connection event messages ([+] Peer connected: ...) interleave with command responses
    in unpredictable order
  • The prompt appears before command responses, making the terminal look broken

The workaround that was attempted and failed

A Done sentinel (PrintMsg::Done) was introduced to signal command completion.
The readline thread waits with done_rx.recv_timeout(500ms) after sending a command,
hoping all response messages are queued in ExternalPrinter before the next
readline() call draws the prompt. This does not fix the problem because:

  • ExternalPrinter still buffers when readline() is not active — messages arrive
    during the 500ms window but are not printed until the next readline() call
  • The 500ms is a heuristic; fast commands signal Done before the user sees anything
  • Background async events (peer connect/disconnect) arrive independently and race
    against the command response window
  • The DoneGuard / done_rx machinery adds complexity and latency for zero benefit

Why reedline fixes this

reedline (the Nushell readline library) uses a fundamentally different model:

  • At the start of each read_line() call, reedline flushes all pending
    ExternalPrinter messages before drawing the prompt
    . This means command responses
    sent to the printer after read_line() returned will always appear correctly above the
    next prompt, regardless of async timing.
  • The ExternalPrinter channel is decoupled from the event loop — messages sent at any
    time are buffered and printed at the correct moment.
  • Designed explicitly for async-friendly REPLs (Nushell is async throughout).
  • Signal handling (Ctrl-C, Ctrl-D) is first-class with a typed Signal return value.
  • Active development; rustyline is comparatively stagnant.

Goals

  1. Binary size check first — before any other work. Measure the size delta of
    adding reedline vs rustyline. If the delta is unacceptable the migration may be
    abandoned or a lighter alternative chosen. Do not proceed to goal 2 until this
    is confirmed acceptable.
  2. Replace rustyline with reedline in the repl feature path.
  3. All existing REPL commands produce correct output in the correct order.
  4. Background async events (peer connect/disconnect) print cleanly without corrupting
    the prompt line.
  5. The non-REPL (#[cfg(not(feature = "repl"))]) path is unchanged.
  6. Document the binary size delta in this file once measured. ✓

Binary size delta (measured 2026-02-23)

Variant rustyline reedline delta
default-glibc 6,453,664 B (6.15M) 6,606,736 B (6.30M) +153,072 B (+2.37%)
slim-glibc 4,950,144 B (4.72M) 4,950,144 B (4.72M) 0 (reedline not compiled)

The increase comes from reedline's external_printer feature pulling in crossbeam channel
primitives. default-features = false is used; only external_printer is enabled.
The slim build is unaffected (reedline is gated behind the repl feature, excluded from slim).

Known challenges

Binary size

reedline pulls in more dependencies than rustyline. The slim build (--features slim,
no readline) must be unaffected. The full build will grow; the question is how much.
cargo bloat --release --crates can give per-crate size breakdown.

PrintMsg / DoneGuard compatibility

The current PrintMsg { Text(String), Done } enum and DoneGuard RAII were designed
as a rustyline workaround. With reedline the Done sentinel may become unnecessary for
command responses. However, the non-readline path also uses PrintMsg and must continue
to work. Changing PrintMsg affects both paths.

Printer channel type

Printer wraps mpsc::UnboundedSender<PrintMsg>. reedline's ExternalPrinter accepts
String not PrintMsg. The channel and Printer abstraction will need to bridge these.

Single vs two printers

Currently one Printer is used for both REPL command responses and background async
events. With reedline's model, it may be necessary or desirable to separate these two
concerns so each can be routed differently.

REPL feature gate

The repl feature (renamed from readline in this branch) is referenced in Cargo.toml
and guarded with #[cfg(feature = "repl")] in entry.rs and exit.rs. reedline must
slot into the same feature gate.

Blocking thread model

The readline loop runs in a spawn_blocking thread to avoid blocking the async runtime.
reedline's read_line() is synchronous, so this model is preserved. Verify that
reedline does not spawn its own tokio runtime or conflict with the existing one.

History, completions, hints

rustyline history (add_history_entry) is used today. reedline has its own history API.
Completions and hints are not implemented today — preserve the same capability gap; do
not add them as part of this migration.

maxholman and others added 21 commits February 23, 2026 12:28
Adds a human-readable protocol name method to the Server trait, implemented
as "QUIC" and "WebSocket" on the respective server types.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Default --version prints one line: "wallhack <version>".
Full build metadata (time, git hash, features) is behind --verbose.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
format_level() now returns the plain level string (e.g. "[+]") when
use_color is false, avoiding escape codes in piped or non-TTY output.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…_name()

Entry and relay now accept --name/-n (exit already had it). All three node
types share a single generate_node_name() helper producing a random 8-char
hex ID, replacing the inline logic that was duplicated in ExitCommand.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…our early

- Remove redundant "Starting as <role> node" log lines (each run() now
  prints its own wallhack <version>  <name> header)
- --version now dispatches to print_version_short or print_version_verbose
- Colour output is initialised at startup based on stderr IsTerminal
- Add name: None to default EntryCommand struct literal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…warn

- Print wallhack <version>  <name> header at startup (consistent with entry/exit)
- Show "Connecting to <addr>..." before DNS resolve
- Show "Resolved <host> as <ip>" only when DNS was actually performed
- Demote retry log from info to warn
- Remove redundant "Connected to upstream" messages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…t_help()

- PrintMsg enum (Text/Done) and DoneGuard RAII for readline output sync
- uptime() replaces inline print_ping() which mixed version and uptime
- print_version_info() prints version only (uptime moved to info command)
- Unified print_help() replaces separate entry/exit help functions;
  command set and descriptions are now identical across all node types

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Print wallhack <version>  <name> header at startup
- Switch print channel to UnboundedSender<PrintMsg>; drain until Done
  before drawing next readline prompt
- Add DoneGuard at REPL dispatch site to cover all exit paths
- Replace print_entry_help with repl_common::print_help
- Fix connect/listen help descriptions (were "Not available on entry nodes")

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…sync

- Print wallhack <version>  <name> header at startup
- Switch print channel to UnboundedSender<PrintMsg>; readline path waits
  for Done before drawing next prompt; non-readline path drains directly
- Add DoneGuard at all REPL dispatch sites (7 call sites)
- Drop state: field from info output; presence/absence of connect:/listen:
  lines conveys the same information without inventing a state abstraction
- Replace print_exit_help with repl_common::print_help
- Route commands available on all node types (consistent CLI)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The feature controls the interactive REPL (history, completion, prompt),
not readline specifically. "repl" is more accurate and protocol-agnostic
now that we are migrating from rustyline to reedline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nces

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…oilerplate

- Replace rustyline dep with reedline 0.45 (external_printer feature)
- Delete dead readline.rs; extract generic run_repl_input<T,F> to repl_common
- entry.rs + exit.rs both use shared run_repl_input — no duplicate setup
- Fix bench/bench.just musl build: add --no-default-features to prevent
  reedline/crossterm from compiling in the cross docker environment
- Update bloat thresholds (+153 KB default, slim unchanged)
- Purge all rustyline/readline terminology from comments

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Entry node REPL was absent in connect mode (--connect flag). Restructure
run_entry_connect to mirror exit.rs: REPL thread starts in run() before
dispatch; run_entry_connect_quic/ws use tokio::select! so REPL commands
(quit/info/stats/peers/help) work during connect + session.

Peer name showing IP address in peers list: run_ws_relay_capability had
name: None in its WsClient config — exit node never sent ExitNodeHello name.
Fixed to name: Some(node_name.to_string()).

Debug logs corrupting reedline display: subscriber.rs used eprintln! which
bypasses ExternalPrinter and writes raw bytes to the TTY mid-prompt. Add
LOG_SINK channel in repl_common; install_log_sink() wires it to the printer
channel when stdin is interactive. emit_log() replaces eprintln! in subscriber.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Routes startup messages through Printer to fix raw-mode newline corruption
when reedline is active. Introduces EntryResources{metrics,peers,routes,sessions}
to reduce argument counts below clippy::too_many_arguments threshold.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- AGENTS.md: prohibit "reverse tunnel", "connect mode", "listen mode" —
  nodes have a transport direction (--connect / --listen), not a mode
- Remove redundant inline comments from usage examples
- Fix entry startup messages to use route_info!/route_warn! so they
  appear on stderr in headless (non-TTY) use, consistent with exit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
session.rs was not declared in lib.rs — completely unreachable dead code.
OutputFormat::Json printed the literal string "{ json_output }" — removed
the variant and the stub. Closes oopsies #14 and #15.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@maxholman maxholman merged commit fd0bb88 into main Feb 23, 2026
4 checks passed
@maxholman maxholman deleted the feat/reedline branch February 23, 2026 08:12
maxholman added a commit that referenced this pull request May 6, 2026
Sweep of website/ deps to latest within ranges, plus a vite downgrade
from 8 -> 7 to match astro's transitive vite (7.3.2) and avoid a
rolldown regression with @tailwindcss/vite 4.2.4.

Closes alerts #28 #29 #30 #31 #33 #34 #35 #36 #37 #38 #39 #40 #44 #48
covering vite, picomatch, postcss, yaml, astro, smol-toml.

- vite ^8.0.1 -> ^7.3.2 (drops the now-redundant vite 8 lineage; astro
  pulls 7.3.2 transitively, which is the patched version)
- astro 6.0.6 -> 6.2.2 (#44)
- @tailwindcss/vite 4.2.2 -> 4.2.4
- smol-toml: lockfile bump to 1.6.1 (#28)
- postcss: lockfile bump to 8.5.14 (#48)
- picomatch: lockfile bumps to 2.3.2 + 4.0.4 (#29 #30 #39 #40)
- yaml is now omitted entirely (it was an optional vite peer)

Verified: pnpm build succeeds; no @tailwindcss/vite peer-dep warnings.
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.

1 participant