editline/lib.rs
1//! Platform-agnostic line editor with history and full editing capabilities.
2//!
3//! This library provides a flexible line editing system with complete separation between
4//! I/O operations and editing logic through the [`Terminal`] trait. This design enables
5//! usage across various platforms and I/O systems without modification to the core logic.
6//!
7//! # Features
8//!
9//! - **Full line editing**: Insert, delete, cursor movement
10//! - **Word-aware navigation**: Ctrl+Left/Right, Alt+Backspace, Ctrl+Delete
11//! - **Command history**: 50-entry circular buffer with up/down navigation
12//! - **Smart history**: Automatically skips duplicates and empty lines
13//! - **Cross-platform**: Unix (termios/ANSI) and Windows (Console API) implementations included
14//! - **Zero global state**: All state is explicitly managed
15//! - **Type-safe**: Strong typing with Result-based error handling
16//!
17//! # Quick Start
18//!
19//! ```no_run
20//! use editline::{LineEditor, terminals::StdioTerminal};
21//!
22//! let mut editor = LineEditor::new(1024, 50); // buffer size, history size
23//! let mut terminal = StdioTerminal::new();
24//!
25//! loop {
26//! print!("> ");
27//! std::io::Write::flush(&mut std::io::stdout()).unwrap();
28//!
29//! match editor.read_line(&mut terminal) {
30//! Ok(line) => {
31//! if line == "exit" {
32//! break;
33//! }
34//! println!("You typed: {}", line);
35//! }
36//! Err(e) => {
37//! eprintln!("Error: {}", e);
38//! break;
39//! }
40//! }
41//! }
42//! ```
43//!
44//! # Architecture
45//!
46//! The library is organized around three main components:
47//!
48//! - [`LineEditor`]: Main API that orchestrates editing operations
49//! - [`LineBuffer`]: Manages text buffer and cursor position
50//! - [`History`]: Circular buffer for command history
51//!
52//! All I/O is abstracted through the [`Terminal`] trait, which platform-specific
53//! implementations must provide.
54//!
55//! # Custom Terminal Implementation
56//!
57//! To use editline with custom I/O (UART, network, etc.), implement the [`Terminal`] trait:
58//!
59//! ```
60//! use editline::{Terminal, KeyEvent, Result};
61//!
62//! struct MyTerminal {
63//! // Your platform-specific fields
64//! }
65//!
66//! impl Terminal for MyTerminal {
67//! fn read_byte(&mut self) -> Result<u8> {
68//! // Read from your input source
69//! # Ok(b'x')
70//! }
71//!
72//! fn write(&mut self, data: &[u8]) -> Result<()> {
73//! // Write to your output
74//! # Ok(())
75//! }
76//!
77//! fn flush(&mut self) -> Result<()> {
78//! // Flush output
79//! # Ok(())
80//! }
81//!
82//! fn enter_raw_mode(&mut self) -> Result<()> {
83//! // Configure for character-by-character input
84//! # Ok(())
85//! }
86//!
87//! fn exit_raw_mode(&mut self) -> Result<()> {
88//! // Restore normal mode
89//! # Ok(())
90//! }
91//!
92//! fn cursor_left(&mut self) -> Result<()> {
93//! // Move cursor left one position
94//! # Ok(())
95//! }
96//!
97//! fn cursor_right(&mut self) -> Result<()> {
98//! // Move cursor right one position
99//! # Ok(())
100//! }
101//!
102//! fn clear_eol(&mut self) -> Result<()> {
103//! // Clear from cursor to end of line
104//! # Ok(())
105//! }
106//!
107//! fn parse_key_event(&mut self) -> Result<KeyEvent> {
108//! // Parse input bytes into key events
109//! # Ok(KeyEvent::Enter)
110//! }
111//! }
112//! ```
113
114#![cfg_attr(not(feature = "std"), no_std)]
115
116extern crate alloc;
117
118use alloc::string::{String, ToString};
119use alloc::vec::Vec;
120use core::fmt;
121
122// Import prelude types that are normally available via std::prelude
123#[cfg(not(feature = "std"))]
124use core::prelude::v1::*;
125#[cfg(not(feature = "std"))]
126use core::result::Result::{Ok, Err};
127
128/// Error type for editline operations
129#[derive(Debug)]
130pub enum Error {
131 /// I/O error occurred
132 Io(&'static str),
133 /// Invalid UTF-8 data
134 InvalidUtf8,
135 /// End of file
136 Eof,
137 /// Operation interrupted
138 Interrupted,
139}
140
141impl fmt::Display for Error {
142 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143 match self {
144 Error::Io(msg) => {
145 f.write_str("I/O error: ")?;
146 f.write_str(msg)
147 }
148 Error::InvalidUtf8 => f.write_str("Invalid UTF-8"),
149 Error::Eof => f.write_str("End of file"),
150 Error::Interrupted => f.write_str("Interrupted"),
151 }
152 }
153}
154
155#[cfg(feature = "std")]
156impl From<std::io::Error> for Error {
157 fn from(e: std::io::Error) -> Self {
158 use std::io::ErrorKind;
159 match e.kind() {
160 ErrorKind::UnexpectedEof => Error::Eof,
161 ErrorKind::Interrupted => Error::Interrupted,
162 _ => Error::Io("I/O error"),
163 }
164 }
165}
166
167#[cfg(feature = "std")]
168impl From<Error> for std::io::Error {
169 fn from(e: Error) -> Self {
170 use std::io::{Error as IoError, ErrorKind};
171 match e {
172 Error::Io(msg) => IoError::new(ErrorKind::Other, msg),
173 Error::InvalidUtf8 => IoError::new(ErrorKind::InvalidData, "Invalid UTF-8"),
174 Error::Eof => IoError::new(ErrorKind::UnexpectedEof, "End of file"),
175 Error::Interrupted => IoError::new(ErrorKind::Interrupted, "Interrupted"),
176 }
177 }
178}
179
180impl From<core::str::Utf8Error> for Error {
181 fn from(_: core::str::Utf8Error) -> Self {
182 Error::InvalidUtf8
183 }
184}
185
186/// Result type for editline operations
187pub type Result<T> = core::result::Result<T, Error>;
188
189/// Key events that can be processed by the line editor
190#[derive(Debug, Clone, Copy, PartialEq, Eq)]
191pub enum KeyEvent {
192 /// Normal printable character
193 Normal(char),
194 /// Left arrow
195 Left,
196 /// Right arrow
197 Right,
198 /// Up arrow (history previous)
199 Up,
200 /// Down arrow (history next)
201 Down,
202 /// Home key
203 Home,
204 /// End key
205 End,
206 /// Backspace
207 Backspace,
208 /// Delete
209 Delete,
210 /// Enter/Return
211 Enter,
212 /// Ctrl+Left (word left)
213 CtrlLeft,
214 /// Ctrl+Right (word right)
215 CtrlRight,
216 /// Ctrl+Delete (delete word right)
217 CtrlDelete,
218 /// Alt+Backspace (delete word left)
219 AltBackspace,
220}
221
222/// Terminal abstraction that enables platform-agnostic line editing.
223///
224/// Implement this trait to use editline with any I/O system: standard terminals,
225/// UART connections, network sockets, or custom devices.
226///
227/// # Platform Implementations
228///
229/// This library provides built-in implementations:
230/// - [`terminals::StdioTerminal`] for Unix (termios + ANSI)
231/// - [`terminals::StdioTerminal`] for Windows (Console API)
232///
233/// # Example
234///
235/// ```
236/// use editline::{Terminal, KeyEvent, Result};
237///
238/// struct MockTerminal {
239/// input: Vec<u8>,
240/// output: Vec<u8>,
241/// }
242///
243/// impl Terminal for MockTerminal {
244/// fn read_byte(&mut self) -> Result<u8> {
245/// self.input.pop().ok_or(editline::Error::Eof)
246/// }
247///
248/// fn write(&mut self, data: &[u8]) -> Result<()> {
249/// self.output.extend_from_slice(data);
250/// Ok(())
251/// }
252///
253/// // ... implement other methods
254/// # fn flush(&mut self) -> Result<()> { Ok(()) }
255/// # fn enter_raw_mode(&mut self) -> Result<()> { Ok(()) }
256/// # fn exit_raw_mode(&mut self) -> Result<()> { Ok(()) }
257/// # fn cursor_left(&mut self) -> Result<()> { Ok(()) }
258/// # fn cursor_right(&mut self) -> Result<()> { Ok(()) }
259/// # fn clear_eol(&mut self) -> Result<()> { Ok(()) }
260/// # fn parse_key_event(&mut self) -> Result<KeyEvent> { Ok(KeyEvent::Enter) }
261/// }
262/// ```
263pub trait Terminal {
264 /// Reads a single byte from the input source.
265 ///
266 /// This is called repeatedly to fetch user input. Should block until a byte is available.
267 fn read_byte(&mut self) -> Result<u8>;
268
269 /// Writes raw bytes to the output.
270 ///
271 /// Used to display typed characters and redraw the line during editing.
272 fn write(&mut self, data: &[u8]) -> Result<()>;
273
274 /// Flushes any buffered output.
275 ///
276 /// Called after each key event to ensure immediate visual feedback.
277 fn flush(&mut self) -> Result<()>;
278
279 /// Enters raw mode for character-by-character input.
280 ///
281 /// Should disable line buffering and echo. Called at the start of [`LineEditor::read_line`].
282 fn enter_raw_mode(&mut self) -> Result<()>;
283
284 /// Exits raw mode and restores normal terminal settings.
285 ///
286 /// Called at the end of [`LineEditor::read_line`] to restore the terminal state.
287 fn exit_raw_mode(&mut self) -> Result<()>;
288
289 /// Moves the cursor left by one position.
290 ///
291 /// Typically outputs an ANSI escape sequence like `\x1b[D` or calls a platform API.
292 fn cursor_left(&mut self) -> Result<()>;
293
294 /// Moves the cursor right by one position.
295 ///
296 /// Typically outputs an ANSI escape sequence like `\x1b[C` or calls a platform API.
297 fn cursor_right(&mut self) -> Result<()>;
298
299 /// Clears from the cursor position to the end of the line.
300 ///
301 /// Typically outputs an ANSI escape sequence like `\x1b[K` or calls a platform API.
302 fn clear_eol(&mut self) -> Result<()>;
303
304 /// Parses the next key event from input.
305 ///
306 /// Should handle multi-byte sequences (like ANSI escape codes) and return a single
307 /// [`KeyEvent`]. Called once per key press by [`LineEditor::read_line`].
308 fn parse_key_event(&mut self) -> Result<KeyEvent>;
309}
310
311/// Text buffer with cursor tracking for line editing operations.
312///
313/// Manages the actual text being edited and the cursor position within it.
314/// Supports UTF-8 text and provides methods for character/word manipulation.
315///
316/// This struct is typically not used directly - instead use [`LineEditor`] which
317/// provides the high-level editing interface.
318pub struct LineBuffer {
319 buffer: Vec<u8>,
320 cursor_pos: usize,
321}
322
323impl LineBuffer {
324 /// Creates a new line buffer with the specified capacity.
325 ///
326 /// # Arguments
327 ///
328 /// * `capacity` - Initial capacity for the internal buffer in bytes
329 ///
330 /// # Examples
331 ///
332 /// ```
333 /// use editline::LineBuffer;
334 ///
335 /// let buffer = LineBuffer::new(1024);
336 /// assert!(buffer.is_empty());
337 /// ```
338 pub fn new(capacity: usize) -> Self {
339 Self {
340 buffer: Vec::with_capacity(capacity),
341 cursor_pos: 0,
342 }
343 }
344
345 /// Clears the buffer and resets the cursor to the start.
346 pub fn clear(&mut self) {
347 self.buffer.clear();
348 self.cursor_pos = 0;
349 }
350
351 /// Returns the length of the buffer in bytes.
352 ///
353 /// Note: For UTF-8 text, this is the byte count, not the character count.
354 pub fn len(&self) -> usize {
355 self.buffer.len()
356 }
357
358 /// Returns `true` if the buffer is empty.
359 pub fn is_empty(&self) -> bool {
360 self.buffer.is_empty()
361 }
362
363 /// Returns the current cursor position in bytes from the start.
364 pub fn cursor_pos(&self) -> usize {
365 self.cursor_pos
366 }
367
368 /// Returns the buffer contents as a UTF-8 string slice.
369 ///
370 /// # Errors
371 ///
372 /// Returns `Err` if the buffer contains invalid UTF-8.
373 pub fn as_str(&self) -> Result<&str> {
374 core::str::from_utf8(&self.buffer).map_err(|_| Error::InvalidUtf8)
375 }
376
377 /// Returns the buffer contents as a byte slice.
378 pub fn as_bytes(&self) -> &[u8] {
379 &self.buffer
380 }
381
382 /// Inserts a character at the cursor position, moving the cursor forward.
383 ///
384 /// Supports UTF-8 characters. The cursor advances by the byte length of the character.
385 pub fn insert_char(&mut self, c: char) {
386 let mut buf = [0; 4];
387 let bytes = c.encode_utf8(&mut buf).as_bytes();
388
389 for &byte in bytes {
390 self.buffer.insert(self.cursor_pos, byte);
391 self.cursor_pos += 1;
392 }
393 }
394
395 /// Deletes the character before the cursor (backspace operation).
396 ///
397 /// Returns `true` if a character was deleted, `false` if the cursor is at the start.
398 pub fn delete_before_cursor(&mut self) -> bool {
399 if self.cursor_pos > 0 {
400 self.cursor_pos -= 1;
401 self.buffer.remove(self.cursor_pos);
402 true
403 } else {
404 false
405 }
406 }
407
408 /// Deletes the character at the cursor (delete key operation).
409 ///
410 /// Returns `true` if a character was deleted, `false` if the cursor is at the end.
411 pub fn delete_at_cursor(&mut self) -> bool {
412 if self.cursor_pos < self.buffer.len() {
413 self.buffer.remove(self.cursor_pos);
414 true
415 } else {
416 false
417 }
418 }
419
420 /// Moves the cursor one position to the left.
421 ///
422 /// Returns `true` if the cursor moved, `false` if already at the start.
423 pub fn move_cursor_left(&mut self) -> bool {
424 if self.cursor_pos > 0 {
425 self.cursor_pos -= 1;
426 true
427 } else {
428 false
429 }
430 }
431
432 /// Moves the cursor one position to the right.
433 ///
434 /// Returns `true` if the cursor moved, `false` if already at the end.
435 pub fn move_cursor_right(&mut self) -> bool {
436 if self.cursor_pos < self.buffer.len() {
437 self.cursor_pos += 1;
438 true
439 } else {
440 false
441 }
442 }
443
444 /// Moves the cursor to the start of the line.
445 ///
446 /// Returns the number of positions the cursor moved.
447 pub fn move_cursor_to_start(&mut self) -> usize {
448 let old_pos = self.cursor_pos;
449 self.cursor_pos = 0;
450 old_pos
451 }
452
453 /// Moves the cursor to the end of the line.
454 ///
455 /// Returns the number of positions the cursor moved.
456 pub fn move_cursor_to_end(&mut self) -> usize {
457 let old_pos = self.cursor_pos;
458 self.cursor_pos = self.buffer.len();
459 self.buffer.len() - old_pos
460 }
461
462 /// Find start of word to the left
463 fn find_word_start_left(&self) -> usize {
464 if self.cursor_pos == 0 {
465 return 0;
466 }
467
468 let mut pos = self.cursor_pos;
469
470 // If we're on a word char, skip to start of current word
471 if pos > 0 && is_word_char(self.buffer[pos - 1]) {
472 while pos > 0 && is_word_char(self.buffer[pos - 1]) {
473 pos -= 1;
474 }
475 } else {
476 // Skip non-word chars
477 while pos > 0 && !is_word_char(self.buffer[pos - 1]) {
478 pos -= 1;
479 }
480 // Then skip word chars
481 while pos > 0 && is_word_char(self.buffer[pos - 1]) {
482 pos -= 1;
483 }
484 }
485
486 pos
487 }
488
489 /// Find start of word to the right
490 fn find_word_start_right(&self) -> usize {
491 if self.cursor_pos >= self.buffer.len() {
492 return self.buffer.len();
493 }
494
495 let mut pos = self.cursor_pos;
496
497 // If on word char, skip to end of current word
498 if pos < self.buffer.len() && is_word_char(self.buffer[pos]) {
499 while pos < self.buffer.len() && is_word_char(self.buffer[pos]) {
500 pos += 1;
501 }
502 }
503
504 // Skip non-word chars
505 while pos < self.buffer.len() && !is_word_char(self.buffer[pos]) {
506 pos += 1;
507 }
508
509 pos
510 }
511
512 /// Moves the cursor to the start of the previous word.
513 ///
514 /// Words are defined as sequences of alphanumeric characters and underscores.
515 /// Returns the number of positions the cursor moved.
516 pub fn move_cursor_word_left(&mut self) -> usize {
517 let target = self.find_word_start_left();
518 let moved = self.cursor_pos - target;
519 self.cursor_pos = target;
520 moved
521 }
522
523 /// Moves the cursor to the start of the next word.
524 ///
525 /// Words are defined as sequences of alphanumeric characters and underscores.
526 /// Returns the number of positions the cursor moved.
527 pub fn move_cursor_word_right(&mut self) -> usize {
528 let target = self.find_word_start_right();
529 let moved = target - self.cursor_pos;
530 self.cursor_pos = target;
531 moved
532 }
533
534 /// Deletes the word to the left of the cursor (Alt+Backspace operation).
535 ///
536 /// Returns the number of bytes deleted.
537 pub fn delete_word_left(&mut self) -> usize {
538 let target = self.find_word_start_left();
539 let count = self.cursor_pos - target;
540
541 for _ in 0..count {
542 if self.cursor_pos > 0 {
543 self.cursor_pos -= 1;
544 self.buffer.remove(self.cursor_pos);
545 }
546 }
547
548 count
549 }
550
551 /// Deletes the word to the right of the cursor (Ctrl+Delete operation).
552 ///
553 /// Returns the number of bytes deleted.
554 pub fn delete_word_right(&mut self) -> usize {
555 let target = self.find_word_start_right();
556 let count = target - self.cursor_pos;
557
558 for _ in 0..count {
559 if self.cursor_pos < self.buffer.len() {
560 self.buffer.remove(self.cursor_pos);
561 }
562 }
563
564 count
565 }
566
567 /// Loads text into the buffer, replacing existing content.
568 ///
569 /// The cursor is positioned at the end of the loaded text.
570 /// Used internally for history navigation.
571 pub fn load(&mut self, text: &str) {
572 self.buffer.clear();
573 self.buffer.extend_from_slice(text.as_bytes());
574 self.cursor_pos = self.buffer.len();
575 }
576}
577
578/// Check if a byte is a word character
579fn is_word_char(c: u8) -> bool {
580 c.is_ascii_alphanumeric() || c == b'_'
581}
582
583/// Command history manager with circular buffer storage.
584///
585/// Maintains a fixed-size history of entered commands with automatic
586/// duplicate and empty-line filtering. Supports bidirectional navigation
587/// and preserves the current line when browsing history.
588///
589/// # Examples
590///
591/// ```
592/// use editline::History;
593///
594/// let mut hist = History::new(50);
595/// hist.add("first command");
596/// hist.add("second command");
597///
598/// // Navigate through history
599/// assert_eq!(hist.previous(""), Some("second command"));
600/// assert_eq!(hist.previous(""), Some("first command"));
601/// ```
602pub struct History {
603 entries: Vec<String>,
604 capacity: usize,
605 current_entry: usize,
606 viewing_entry: Option<usize>,
607 saved_line: Option<String>,
608}
609
610impl History {
611 /// Creates a new history buffer with the specified capacity.
612 ///
613 /// When the capacity is reached, the oldest entries are overwritten.
614 ///
615 /// # Arguments
616 ///
617 /// * `capacity` - Maximum number of history entries to store
618 pub fn new(capacity: usize) -> Self {
619 Self {
620 entries: Vec::with_capacity(capacity),
621 capacity,
622 current_entry: 0,
623 viewing_entry: None,
624 saved_line: None,
625 }
626 }
627
628 /// Adds a line to the history.
629 ///
630 /// Empty lines (including whitespace-only) and consecutive duplicates are automatically skipped.
631 /// When the buffer is full, the oldest entry is overwritten.
632 ///
633 /// # Arguments
634 ///
635 /// * `line` - The command line to add to history
636 pub fn add(&mut self, line: &str) {
637 let trimmed = line.trim();
638
639 // Skip empty or whitespace-only lines
640 if trimmed.is_empty() {
641 return;
642 }
643
644 // Skip if same as most recent (after trimming)
645 if let Some(last) = self.entries.last() {
646 if last == trimmed {
647 return;
648 }
649 }
650
651 if self.entries.len() < self.capacity {
652 self.entries.push(trimmed.to_string());
653 self.current_entry = self.entries.len() - 1;
654 } else {
655 // Circular buffer - overwrite oldest
656 self.current_entry = (self.current_entry + 1) % self.capacity;
657 self.entries[self.current_entry] = trimmed.to_string();
658 }
659
660 self.viewing_entry = None;
661 self.saved_line = None;
662 }
663
664 /// Navigates to the previous (older) history entry.
665 ///
666 /// On the first call, saves `current_line` so it can be restored when
667 /// navigating forward past the most recent entry.
668 ///
669 /// # Arguments
670 ///
671 /// * `current_line` - The current line content to save (only used on first call)
672 ///
673 /// # Returns
674 ///
675 /// `Some(&str)` with the previous history entry, or `None` if at the oldest entry.
676 pub fn previous(&mut self, current_line: &str) -> Option<&str> {
677 if self.entries.is_empty() {
678 return None;
679 }
680
681 match self.viewing_entry {
682 None => {
683 // First time - save current line and start at most recent
684 self.saved_line = Some(current_line.to_string());
685 self.viewing_entry = Some(self.current_entry);
686 Some(&self.entries[self.current_entry])
687 }
688 Some(idx) => {
689 // Go further back
690 if self.entries.len() < self.capacity {
691 // Haven't filled buffer yet
692 if idx > 0 {
693 let prev = idx - 1;
694 self.viewing_entry = Some(prev);
695 Some(&self.entries[prev])
696 } else {
697 None
698 }
699 } else {
700 // Buffer is full
701 let prev = (idx + self.capacity - 1) % self.capacity;
702 if prev == self.current_entry {
703 None
704 } else {
705 self.viewing_entry = Some(prev);
706 Some(&self.entries[prev])
707 }
708 }
709 }
710 }
711 }
712
713 /// Navigates to the next (newer) history entry.
714 ///
715 /// When reaching the end of history, returns the saved current line
716 /// that was passed to the first [`previous`](Self::previous) call.
717 ///
718 /// # Returns
719 ///
720 /// `Some(&str)` with the next history entry or saved line, or `None` if
721 /// not currently viewing history.
722 pub fn next_entry(&mut self) -> Option<&str> {
723 match self.viewing_entry {
724 None => None,
725 Some(idx) => {
726 if self.entries.len() < self.capacity {
727 // Haven't filled buffer yet
728 if idx < self.entries.len() - 1 {
729 let next = idx + 1;
730 self.viewing_entry = Some(next);
731 Some(&self.entries[next])
732 } else {
733 // Reached the end, return saved line
734 self.viewing_entry = None;
735 self.saved_line.as_deref()
736 }
737 } else {
738 // Buffer is full
739 let next = (idx + 1) % self.capacity;
740 if next == (self.current_entry + 1) % self.capacity {
741 // Reached the end, return saved line
742 self.viewing_entry = None;
743 self.saved_line.as_deref()
744 } else {
745 self.viewing_entry = Some(next);
746 Some(&self.entries[next])
747 }
748 }
749 }
750 }
751 }
752
753 /// Resets the history view to the current line.
754 ///
755 /// Called when the user starts typing to exit history browsing mode.
756 pub fn reset_view(&mut self) {
757 self.viewing_entry = None;
758 }
759}
760
761/// Main line editor interface with full editing and history support.
762///
763/// Provides a high-level API for reading edited lines from any [`Terminal`]
764/// implementation. Handles all keyboard input, cursor movement, text editing,
765/// and history navigation.
766///
767/// # Examples
768///
769/// ```no_run
770/// use editline::{LineEditor, terminals::StdioTerminal};
771///
772/// let mut editor = LineEditor::new(1024, 50);
773/// let mut terminal = StdioTerminal::new();
774///
775/// match editor.read_line(&mut terminal) {
776/// Ok(line) => println!("Got: {}", line),
777/// Err(e) => eprintln!("Error: {}", e),
778/// }
779/// ```
780///
781/// # Key Bindings
782///
783/// - **Arrow keys**: Move cursor left/right, navigate history up/down
784/// - **Home/End**: Jump to start/end of line
785/// - **Backspace/Delete**: Delete characters
786/// - **Ctrl+Left/Right**: Move by word
787/// - **Alt+Backspace**: Delete word left
788/// - **Ctrl+Delete**: Delete word right
789/// - **Enter**: Submit line
790pub struct LineEditor {
791 line: LineBuffer,
792 history: History,
793}
794
795impl LineEditor {
796 /// Creates a new line editor with the specified capacities.
797 ///
798 /// # Arguments
799 ///
800 /// * `buffer_capacity` - Initial capacity for the line buffer in bytes
801 /// * `history_capacity` - Maximum number of history entries to store
802 ///
803 /// # Examples
804 ///
805 /// ```
806 /// use editline::LineEditor;
807 ///
808 /// // 1024 byte buffer, 50 history entries
809 /// let editor = LineEditor::new(1024, 50);
810 /// ```
811 pub fn new(buffer_capacity: usize, history_capacity: usize) -> Self {
812 Self {
813 line: LineBuffer::new(buffer_capacity),
814 history: History::new(history_capacity),
815 }
816 }
817
818 /// Reads a line from the terminal with full editing support.
819 ///
820 /// Enters raw mode, processes key events until Enter is pressed, then returns
821 /// the edited line with leading and trailing whitespace removed. The trimmed
822 /// line is automatically added to history if non-empty.
823 ///
824 /// # Arguments
825 ///
826 /// * `terminal` - Any type implementing the [`Terminal`] trait
827 ///
828 /// # Returns
829 ///
830 /// `Ok(String)` with the trimmed entered line, or `Err` if an I/O error occurs.
831 ///
832 /// # Examples
833 ///
834 /// ```no_run
835 /// use editline::{LineEditor, terminals::StdioTerminal};
836 ///
837 /// let mut editor = LineEditor::new(1024, 50);
838 /// let mut terminal = StdioTerminal::new();
839 ///
840 /// print!("> ");
841 /// std::io::Write::flush(&mut std::io::stdout()).unwrap();
842 ///
843 /// let line = editor.read_line(&mut terminal)?;
844 /// println!("You entered: {}", line);
845 /// # Ok::<(), editline::Error>(())
846 /// ```
847 pub fn read_line<T: Terminal>(&mut self, terminal: &mut T) -> Result<String> {
848 self.line.clear();
849 terminal.enter_raw_mode()?;
850
851 // Use a closure to ensure we always exit raw mode, even on error
852 let result = (|| {
853 loop {
854 let event = terminal.parse_key_event()?;
855
856 if event == KeyEvent::Enter {
857 break;
858 }
859
860 self.handle_key_event(terminal, event)?;
861 }
862
863 terminal.write(b"\n")?;
864 terminal.flush()?;
865
866 let result = self.line.as_str()?
867 .trim()
868 .to_string();
869
870 // Add to history (History::add will check if empty and skip duplicates)
871 self.history.add(&result);
872 self.history.reset_view();
873
874 Ok(result)
875 })();
876
877 // Always exit raw mode, even if an error occurred
878 terminal.exit_raw_mode()?;
879
880 result
881 }
882
883 fn handle_key_event<T: Terminal>(&mut self, terminal: &mut T, event: KeyEvent) -> Result<()> {
884 match event {
885 KeyEvent::Normal(c) => {
886 self.history.reset_view();
887 self.line.insert_char(c);
888 terminal.write(c.to_string().as_bytes())?;
889 self.redraw_from_cursor(terminal)?;
890 }
891 KeyEvent::Left => {
892 if self.line.move_cursor_left() {
893 terminal.cursor_left()?;
894 }
895 }
896 KeyEvent::Right => {
897 if self.line.move_cursor_right() {
898 terminal.cursor_right()?;
899 }
900 }
901 KeyEvent::Up => {
902 let current = self.line.as_str().unwrap_or("").to_string();
903 if let Some(text) = self.history.previous(¤t) {
904 let text = text.to_string();
905 self.load_history_into_line(terminal, &text)?;
906 }
907 }
908 KeyEvent::Down => {
909 if let Some(text) = self.history.next_entry() {
910 let text = text.to_string();
911 self.load_history_into_line(terminal, &text)?;
912 }
913 // If None, we're not viewing history, so do nothing
914 }
915 KeyEvent::Home => {
916 let count = self.line.move_cursor_to_start();
917 for _ in 0..count {
918 terminal.cursor_left()?;
919 }
920 }
921 KeyEvent::End => {
922 let count = self.line.move_cursor_to_end();
923 for _ in 0..count {
924 terminal.cursor_right()?;
925 }
926 }
927 KeyEvent::Backspace => {
928 self.history.reset_view();
929 if self.line.delete_before_cursor() {
930 terminal.cursor_left()?;
931 self.redraw_from_cursor(terminal)?;
932 }
933 }
934 KeyEvent::Delete => {
935 self.history.reset_view();
936 if self.line.delete_at_cursor() {
937 self.redraw_from_cursor(terminal)?;
938 }
939 }
940 KeyEvent::CtrlLeft => {
941 let count = self.line.move_cursor_word_left();
942 for _ in 0..count {
943 terminal.cursor_left()?;
944 }
945 }
946 KeyEvent::CtrlRight => {
947 let count = self.line.move_cursor_word_right();
948 for _ in 0..count {
949 terminal.cursor_right()?;
950 }
951 }
952 KeyEvent::AltBackspace => {
953 self.history.reset_view();
954 let count = self.line.delete_word_left();
955 for _ in 0..count {
956 terminal.cursor_left()?;
957 }
958 self.redraw_from_cursor(terminal)?;
959 }
960 KeyEvent::CtrlDelete => {
961 self.history.reset_view();
962 self.line.delete_word_right();
963 self.redraw_from_cursor(terminal)?;
964 }
965 KeyEvent::Enter => {}
966 }
967
968 terminal.flush()?;
969 Ok(())
970 }
971
972 fn redraw_from_cursor<T: Terminal>(&self, terminal: &mut T) -> Result<()> {
973 terminal.clear_eol()?;
974
975 let cursor_pos = self.line.cursor_pos();
976 let remaining = &self.line.as_bytes()[cursor_pos..];
977 terminal.write(remaining)?;
978
979 // Move cursor back
980 for _ in 0..remaining.len() {
981 terminal.cursor_left()?;
982 }
983
984 Ok(())
985 }
986
987 fn clear_line_display<T: Terminal>(&self, terminal: &mut T) -> Result<()> {
988 for _ in 0..self.line.cursor_pos() {
989 terminal.cursor_left()?;
990 }
991 terminal.clear_eol()?;
992 Ok(())
993 }
994
995 fn load_history_into_line<T: Terminal>(&mut self, terminal: &mut T, text: &str) -> Result<()> {
996 self.clear_line_display(terminal)?;
997 self.line.load(text);
998 terminal.write(text.as_bytes())?;
999 Ok(())
1000 }
1001}
1002
1003// Re-export terminal implementations (only with std feature)
1004#[cfg(feature = "std")]
1005pub mod terminals;
1006
1007#[cfg(test)]
1008mod tests {
1009 use super::*;
1010
1011 // LineBuffer tests
1012 #[test]
1013 fn test_line_buffer_insert() {
1014 let mut buf = LineBuffer::new(100);
1015 buf.insert_char('h');
1016 buf.insert_char('i');
1017 assert_eq!(buf.as_str().unwrap(), "hi");
1018 assert_eq!(buf.cursor_pos(), 2);
1019 assert_eq!(buf.len(), 2);
1020 }
1021
1022 #[test]
1023 fn test_line_buffer_backspace() {
1024 let mut buf = LineBuffer::new(100);
1025 buf.insert_char('h');
1026 buf.insert_char('i');
1027 assert!(buf.delete_before_cursor());
1028 assert_eq!(buf.as_str().unwrap(), "h");
1029 assert_eq!(buf.cursor_pos(), 1);
1030 }
1031
1032 #[test]
1033 fn test_line_buffer_delete() {
1034 let mut buf = LineBuffer::new(100);
1035 buf.insert_char('h');
1036 buf.insert_char('i');
1037 buf.move_cursor_left();
1038 assert!(buf.delete_at_cursor());
1039 assert_eq!(buf.as_str().unwrap(), "h");
1040 assert_eq!(buf.cursor_pos(), 1);
1041 }
1042
1043 #[test]
1044 fn test_line_buffer_cursor_movement() {
1045 let mut buf = LineBuffer::new(100);
1046 buf.insert_char('h');
1047 buf.insert_char('e');
1048 buf.insert_char('y');
1049 assert_eq!(buf.cursor_pos(), 3);
1050
1051 assert!(buf.move_cursor_left());
1052 assert_eq!(buf.cursor_pos(), 2);
1053
1054 assert!(buf.move_cursor_right());
1055 assert_eq!(buf.cursor_pos(), 3);
1056
1057 assert!(!buf.move_cursor_right()); // at end
1058 }
1059
1060 #[test]
1061 fn test_line_buffer_home_end() {
1062 let mut buf = LineBuffer::new(100);
1063 buf.insert_char('h');
1064 buf.insert_char('e');
1065 buf.insert_char('y');
1066
1067 buf.move_cursor_to_start();
1068 assert_eq!(buf.cursor_pos(), 0);
1069
1070 buf.move_cursor_to_end();
1071 assert_eq!(buf.cursor_pos(), 3);
1072 }
1073
1074 #[test]
1075 fn test_line_buffer_word_navigation() {
1076 let mut buf = LineBuffer::new(100);
1077 for c in "hello world test".chars() {
1078 buf.insert_char(c);
1079 }
1080
1081 // At end: "hello world test|"
1082 buf.move_cursor_word_left();
1083 assert_eq!(buf.cursor_pos(), 12); // "hello world |test"
1084
1085 buf.move_cursor_word_left();
1086 assert_eq!(buf.cursor_pos(), 6); // "hello |world test"
1087
1088 buf.move_cursor_word_right();
1089 assert_eq!(buf.cursor_pos(), 12); // "hello world |test"
1090 }
1091
1092 #[test]
1093 fn test_line_buffer_delete_word() {
1094 let mut buf = LineBuffer::new(100);
1095 for c in "hello world".chars() {
1096 buf.insert_char(c);
1097 }
1098
1099 buf.delete_word_left();
1100 assert_eq!(buf.as_str().unwrap(), "hello ");
1101
1102 buf.delete_word_left();
1103 assert_eq!(buf.as_str().unwrap(), "");
1104 }
1105
1106 #[test]
1107 fn test_line_buffer_delete_word_right() {
1108 let mut buf = LineBuffer::new(100);
1109 for c in "hello world".chars() {
1110 buf.insert_char(c);
1111 }
1112 buf.move_cursor_to_start();
1113
1114 buf.delete_word_right();
1115 assert_eq!(buf.as_str().unwrap(), "world");
1116 }
1117
1118 #[test]
1119 fn test_line_buffer_insert_middle() {
1120 let mut buf = LineBuffer::new(100);
1121 buf.insert_char('h');
1122 buf.insert_char('e');
1123 buf.move_cursor_left();
1124 buf.insert_char('x');
1125 assert_eq!(buf.as_str().unwrap(), "hxe");
1126 assert_eq!(buf.cursor_pos(), 2);
1127 }
1128
1129 // History tests
1130 #[test]
1131 fn test_history_add() {
1132 let mut hist = History::new(10);
1133 hist.add("first");
1134 hist.add("second");
1135
1136 assert_eq!(hist.previous(""), Some("second"));
1137 assert_eq!(hist.previous(""), Some("first"));
1138 assert_eq!(hist.previous(""), None); // no more
1139 }
1140
1141 #[test]
1142 fn test_history_skip_empty() {
1143 let mut hist = History::new(10);
1144 hist.add("first");
1145 hist.add("");
1146 hist.add("second");
1147
1148 assert_eq!(hist.previous(""), Some("second"));
1149 assert_eq!(hist.previous(""), Some("first"));
1150 assert_eq!(hist.previous(""), None);
1151 }
1152
1153 #[test]
1154 fn test_history_skip_duplicates() {
1155 let mut hist = History::new(10);
1156 hist.add("test");
1157 hist.add("test"); // should be skipped
1158 hist.add("other");
1159
1160 assert_eq!(hist.previous(""), Some("other"));
1161 assert_eq!(hist.previous(""), Some("test"));
1162 assert_eq!(hist.previous(""), None);
1163 }
1164
1165 #[test]
1166 fn test_history_navigation() {
1167 let mut hist = History::new(10);
1168 hist.add("first");
1169 hist.add("second");
1170 hist.add("third");
1171
1172 // Go back through history
1173 assert_eq!(hist.previous(""), Some("third"));
1174 assert_eq!(hist.previous(""), Some("second"));
1175
1176 // Go forward
1177 assert_eq!(hist.next_entry(), Some("third"));
1178 assert_eq!(hist.next_entry(), Some("")); // returns saved line (empty string)
1179 }
1180
1181 #[test]
1182 fn test_history_saves_current_line() {
1183 let mut hist = History::new(10);
1184 hist.add("first");
1185 hist.add("second");
1186
1187 // Start typing something
1188 assert_eq!(hist.previous("hello"), Some("second"));
1189 assert_eq!(hist.previous("hello"), Some("first"));
1190
1191 // Navigate back forward
1192 assert_eq!(hist.next_entry(), Some("second"));
1193 assert_eq!(hist.next_entry(), Some("hello")); // restored!
1194 }
1195
1196 #[test]
1197 fn test_history_down_without_up() {
1198 let mut hist = History::new(10);
1199 hist.add("first");
1200
1201 // Down without going up first should do nothing
1202 assert_eq!(hist.next_entry(), None);
1203 }
1204
1205 #[test]
1206 fn test_history_circular_buffer() {
1207 let mut hist = History::new(3);
1208 hist.add("first");
1209 hist.add("second");
1210 hist.add("third");
1211 hist.add("fourth"); // overwrites "first"
1212
1213 assert_eq!(hist.previous(""), Some("fourth"));
1214 assert_eq!(hist.previous(""), Some("third"));
1215 assert_eq!(hist.previous(""), Some("second"));
1216 assert_eq!(hist.previous(""), None); // "first" was overwritten
1217 }
1218
1219 #[test]
1220 fn test_history_reset_view() {
1221 let mut hist = History::new(10);
1222 hist.add("first");
1223 hist.add("second");
1224
1225 assert_eq!(hist.previous(""), Some("second"));
1226 hist.reset_view();
1227
1228 // After reset, previous() should start from most recent again
1229 assert_eq!(hist.previous(""), Some("second"));
1230 }
1231
1232 #[test]
1233 fn test_line_buffer_utf8() {
1234 let mut buf = LineBuffer::new(100);
1235 buf.insert_char('ä');
1236 buf.insert_char('ö');
1237 buf.insert_char('ü');
1238 assert_eq!(buf.as_str().unwrap(), "äöü");
1239 assert_eq!(buf.len(), 6); // UTF-8 bytes
1240 }
1241
1242 #[test]
1243 fn test_line_buffer_load() {
1244 let mut buf = LineBuffer::new(100);
1245 buf.insert_char('x');
1246 buf.load("hello world");
1247 assert_eq!(buf.as_str().unwrap(), "hello world");
1248 assert_eq!(buf.cursor_pos(), 11);
1249 }
1250}