24 releases
| 0.0.24 | Oct 26, 2025 |
|---|---|
| 0.0.23 | Oct 26, 2025 |
#248 in Command-line interface
Used in 2 crates
140KB
2K
SLoC
editline
A platform-agnostic line editor library for Rust with full editing capabilities, command history, and cross-platform terminal support.
Overview
editline provides a powerful, flexible line editing library with a clean separation between I/O and editing logic. Unlike traditional readline implementations that are tightly coupled to specific terminal APIs, editline uses a trait-based design that works with any byte-stream I/O.
Perfect for:
- Desktop CLIs and REPLs
- Embedded systems (UART, custom displays)
- Network services (telnet/SSH servers)
- Custom terminal emulators
- Testing with mock I/O
Why editline?
- Platform-agnostic core - editing logic has zero I/O dependencies
- No global state - create multiple independent editors
- Type-safe - Rust enums and Result types throughout
- Memory-safe - no manual memory management
- Full-featured - history, word navigation, editing operations
- Cross-platform - Unix (termios/ANSI) and Windows (Console API) included
Features
- Full line editing: Insert, delete, cursor movement
- Word-aware navigation: Ctrl+Left/Right, Alt+Backspace, Ctrl+Delete (treats symbols like
+,-as separate words) - Command history: 50-entry circular buffer with up/down navigation
- Smart history: Automatically skips duplicates and empty lines
- Cross-platform: Unix (termios/ANSI), Windows (Console API), and embedded systems
- Async support: AsyncLineEditor for Embassy and other async runtimes
- Zero global state: All state is explicitly managed
- Type-safe: Strong typing with Result-based error handling
Usage
Add to your Cargo.toml:
[dependencies]
editline = "0.0.22"
# For embedded platforms
[target.'cfg(target_os = "none")'.dependencies]
# micro:bit v2 (nRF52833):
editline = { version = "0.0.22", features = ["microbit"], default-features = false }
# Raspberry Pi Pico (RP2040) with USB CDC:
editline = { version = "0.0.22", features = ["rp_pico_usb"], default-features = false }
# Raspberry Pi Pico 2 (RP2350) with USB CDC:
editline = { version = "0.0.22", features = ["rp_pico2_usb"], default-features = false }
# STM32H753ZI with Embassy async USB CDC:
editline = { version = "0.0.22", features = ["stm32h753zi"], default-features = false }
Basic REPL Example
use editline::terminals::StdioTerminal;
use editline::LineEditor;
fn main() {
let mut editor = LineEditor::new(1024, 50); // buffer size, history size
let mut terminal = StdioTerminal::new();
loop {
print!("> ");
std::io::Write::flush(&mut std::io::stdout()).unwrap();
match editor.read_line(&mut terminal) {
Ok(line) => {
if line == "exit" {
break;
}
if !line.is_empty() {
println!("typed: {}", line);
}
}
Err(e) => {
// Handle Ctrl-C and Ctrl-D
match e.kind() {
std::io::ErrorKind::UnexpectedEof => {
// Ctrl-D pressed - exit gracefully
println!("\nGoodbye!");
break;
}
std::io::ErrorKind::Interrupted => {
// Ctrl-C pressed - show message and continue
println!("\nInterrupted. Type 'exit' or press Ctrl-D to quit.");
continue;
}
_ => {
eprintln!("\nError: {}", e);
break;
}
}
}
}
}
}
Async REPL with Real-Time Output
For async environments where background tasks need to display output while the user is typing, use read_line_with_async_output(). This is perfect for REPLs with spawned tasks:
use editline::{AsyncLineEditor, terminals::EmbassyUsbTerminal};
use embassy_sync::channel::Channel;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
// Shared channel for background tasks to send output
static OUTPUT_CHANNEL: Channel<CriticalSectionRawMutex, heapless::Vec<u8, 256>, 4> = Channel::new();
// In your REPL loop:
let mut editor = AsyncLineEditor::new(256, 10);
let mut terminal = EmbassyUsbTerminal::new(usb_class);
loop {
terminal.write(b"> ").await?;
terminal.flush().await?;
// Async output from background tasks will interrupt and display immediately
match editor.read_line_with_async_output(&mut terminal, || async {
Some(OUTPUT_CHANNEL.receive().await)
}).await {
Ok(line) => {
// Process the line...
}
Err(e) => break,
}
}
Background tasks can write to the channel:
// From a spawned task
let output = b"Background task output\r\n";
let mut buf = heapless::Vec::<u8, 256>::new();
buf.extend_from_slice(output).ok();
OUTPUT_CHANNEL.send(buf).await;
The output will appear immediately, interrupting the prompt. The current input line is automatically redrawn below the async output.
Custom Terminal Implementation
Implement the Terminal trait for your platform:
use editline::{Terminal, KeyEvent};
use std::io;
struct MyCustomTerminal {
// Your platform-specific fields
}
impl Terminal for MyCustomTerminal {
fn read_byte(&mut self) -> io::Result<u8> {
// Read from your input source
}
fn write(&mut self, data: &[u8]) -> io::Result<()> {
// Write to your output
}
fn flush(&mut self) -> io::Result<()> {
// Flush output
}
fn enter_raw_mode(&mut self) -> io::Result<()> {
// Configure for character-by-character input
}
fn exit_raw_mode(&mut self) -> io::Result<()> {
// Restore normal mode
}
fn cursor_left(&mut self) -> io::Result<()> {
// Move cursor left
}
fn cursor_right(&mut self) -> io::Result<()> {
// Move cursor right
}
fn clear_eol(&mut self) -> io::Result<()> {
// Clear from cursor to end of line
}
fn parse_key_event(&mut self) -> io::Result<KeyEvent> {
// Parse input bytes into key events
}
}
Running the Examples
Standard Terminal (Linux/Windows/macOS)
cargo run --example simple_repl
Embedded micro:bit Example
For embedded targets, you need to:
- Build with
--no-default-featuresto disable thestdfeature - Provide the appropriate target and build configuration
cargo build --example microbit_repl --no-default-features --target thumbv7em-none-eabihf -Z build-std=core,alloc
For convenience when developing embedded applications, create a .cargo/config.toml in your project:
[target.thumbv7em-none-eabihf]
runner = "probe-rs run --chip nRF52833_xxAA"
rustflags = ["-C", "link-arg=-Tlink.x"]
# Note: No [build] section with default target!
# This allows both Linux and embedded builds to work.
# For embedded builds, explicitly specify: --target thumbv7em-none-eabihf
[unstable]
build-std = ["core", "alloc"]
Then use editline in your Cargo.toml:
[dependencies]
editline = { version = "0.0.22", default-features = false }
Try these features:
- Arrow keys for cursor movement
- Home/End keys
- Up/Down for history
- Ctrl+Left/Right for word navigation
- Alt+Backspace to delete word left
- Ctrl+Delete to delete word right
- Ctrl-D to exit (EOF)
- Ctrl-C to interrupt current line (continues REPL)
Platform Support
Supported Platforms
- Linux/Unix: Uses termios for raw mode and ANSI escape sequences for cursor control
- Windows: Uses Windows Console API for native terminal control
- micro:bit v2: UART-based terminal with proper line endings (CRLF) for serial terminals
- Raspberry Pi Pico (RP2040): USB CDC (Communications Device Class) for virtual COM port over USB
- Raspberry Pi Pico 2 (RP2350): USB CDC with DTR-based connection detection for reliable operation
Platform-Specific Behavior
Line Endings:
- Unix/Linux/macOS:
\n(LF) - Embedded platforms (micro:bit, Raspberry Pi Pico):
\r\n(CRLF)
The library automatically handles platform-specific line endings through conditional compilation.
Building for Different Platforms
Desktop (Linux/Windows/macOS):
cargo build --release
cargo run --example simple_repl
micro:bit v2:
cargo build --example microbit_repl --target thumbv7em-none-eabihf \
--no-default-features --features microbit --release
# Flash to micro:bit (when mounted at /media/$USER/MICROBIT)
arm-none-eabi-objcopy -O ihex \
target/thumbv7em-none-eabihf/release/examples/microbit_repl \
/media/$USER/MICROBIT/microbit_repl.hex
# Connect via serial
picocom /dev/ttyACM0 -b 115200
Raspberry Pi Pico (RP2040 USB CDC):
cargo build --example rp_pico_usb_repl --target thumbv6m-none-eabi \
--no-default-features --features rp_pico_usb --release
# Convert to UF2 format
elf2uf2-rs target/thumbv6m-none-eabi/release/examples/rp_pico_usb_repl \
target/thumbv6m-none-eabi/release/examples/rp_pico_usb_repl.uf2
# Flash to Pico (when in BOOTSEL mode at /media/$USER/RPI-RP2)
cp target/thumbv6m-none-eabi/release/examples/rp_pico_usb_repl.uf2 \
/media/$USER/RPI-RP2/
# Connect via USB CDC
picocom /dev/ttyACM0 -b 115200
Raspberry Pi Pico 2 (RP2350 USB CDC):
cargo build --example rp_pico2_usb_repl --target thumbv8m.main-none-eabihf \
--no-default-features --features rp_pico2_usb --release
# Convert to UF2 format (requires picotool for correct family ID)
picotool uf2 convert --family rp2350-arm-s \
target/thumbv8m.main-none-eabihf/release/examples/rp_pico2_usb_repl \
target/thumbv8m.main-none-eabihf/release/examples/rp_pico2_usb_repl.uf2
# Flash to Pico 2 (when in BOOTSEL mode at /media/$USER/RP2350)
cp target/thumbv8m.main-none-eabihf/release/examples/rp_pico2_usb_repl.uf2 \
/media/$USER/RP2350/
# Connect via USB CDC
picocom /dev/ttyACM0 -b 115200
Architecture
┌───────────────────────────────────────┐
│ LineEditor (lib.rs) │
│ ┌───────────┐ ┌──────────────────┐ │
│ │LineBuffer │ │ History │ │
│ │ │ │ (circular buffer)│ │
│ └───────────┘ └──────────────────┘ │
└──────────────────┬────────────────────┘
│ Terminal trait
┌──────────┴──────────┐
│ │
┌───────▼────────┐ ┌────────▼─────────┐
│ Unix Terminal │ │ Windows Terminal │
│ (termios/ANSI) │ │ (Console API) │
└────────────────┘ └──────────────────┘
Contributing
Contributions are welcome! Areas for enhancement:
- Tab completion callback hooks
- Multi-line editing support
- Syntax highlighting callbacks
- Additional platform implementations
- More comprehensive tests
License
Licensed under either of:
at your option.
Dependencies
~0–41MB
~1.5M SLoC