rustyline_async/
lib.rs

1//! The `rustyline-async` library lets you read user input from the terminal
2//! line by line while concurrently writing lines to the same terminal.
3//!
4//! Usage
5//! =====
6//!
7//! - Call [`Readline::new()`] to create a [`Readline`] instance and associated
8//!   [`SharedWriter`].
9//!
10//! - Call [`Readline::readline()`] (most likely in a loop) to receive a line
11//!   of input from the terminal.  The user entering the line can edit their
12//!   input using the key bindings listed under "Input Editing" below.
13//!
14//! - After receiving a line from the user, if you wish to add it to the
15//!   history (so that the user can retrieve it while editing a later line),
16//!   call [`Readline::add_history_entry()`].
17//!
18//! - Lines written to the associated `SharedWriter` while `readline()` is in
19//!   progress will be output to the screen above the input line.
20//!
21//! - When done, call [`Readline::flush()`] to ensure that all lines written to
22//!   the `SharedWriter` are output.
23//!
24//! Input Editing
25//! =============
26//!
27//! While entering text, the user can edit and navigate through the current
28//! input line with the following key bindings:
29//!
30//! - Left, Right: Move cursor left/right
31//! - Up, Down: Scroll through input history
32//! - Ctrl-W: Erase the input from the cursor to the previous whitespace
33//! - Ctrl-U: Erase the input before the cursor
34//! - Ctrl-L: Clear the screen
35//! - Ctrl-Left / Ctrl-Right: Move to previous/next whitespace
36//! - Home: Jump to the start of the line
37//!     - When the "emacs" feature (on by default) is enabled, Ctrl-A has the
38//!       same effect.
39//! - End: Jump to the end of the line
40//!     - When the "emacs" feature (on by default) is enabled, Ctrl-E has the
41//!       same effect.
42//! - Ctrl-D: Send an `Eof` event
43//! - Ctrl-C: Send an `Interrupt` event
44
45use std::{
46	collections::VecDeque,
47	io::{self, stdout, Stdout, Write},
48	ops::DerefMut,
49	pin::Pin,
50	task::{Context, Poll},
51};
52
53use crossterm::{
54	event::EventStream,
55	terminal::{self, disable_raw_mode, Clear},
56	QueueableCommand,
57};
58use futures_util::{pin_mut, ready, select, AsyncWrite, FutureExt, StreamExt};
59use thingbuf::mpsc::{errors::TrySendError, Receiver, Sender};
60use thiserror::Error;
61
62mod history;
63mod line;
64use history::History;
65use line::LineState;
66
67/// Error returned from [`readline()`][Readline::readline].  Such errors
68/// generally require specific procedures to recover from.
69#[derive(Debug, Error)]
70pub enum ReadlineError {
71	/// An internal I/O error occurred
72	#[error(transparent)]
73	IO(#[from] io::Error),
74
75	/// `readline()` was called after the [`SharedWriter`] was dropped and
76	/// everything written to the `SharedWriter` was already output
77	#[error("line writers closed")]
78	Closed,
79}
80
81/// Events emitted by [`Readline::readline()`]
82#[derive(Debug)]
83pub enum ReadlineEvent {
84	/// The user entered a line of text
85	Line(String),
86	/// The user pressed Ctrl-D
87	Eof,
88	/// The user pressed Ctrl-C
89	Interrupted,
90}
91
92/// Clonable object that implements [`Write`][std::io::Write] and
93/// [`AsyncWrite`][futures::io::AsyncWrite] and allows for sending data to the
94/// terminal without messing up the readline.
95///
96/// A `SharedWriter` instance is obtained by calling [`Readline::new()`], which
97/// also returns a [`Readline`] instance associated with the writer.
98///
99/// Data written to a `SharedWriter` is only output when a line feed (`'\n'`)
100/// has been written and either [`Readline::readline()`] or
101/// [`Readline::flush()`] is executing on the associated `Readline` instance.
102#[pin_project::pin_project]
103pub struct SharedWriter {
104	#[pin]
105	buffer: Vec<u8>,
106	sender: Sender<Vec<u8>>,
107}
108impl Clone for SharedWriter {
109	fn clone(&self) -> Self {
110		Self {
111			buffer: Vec::new(),
112			sender: self.sender.clone(),
113		}
114	}
115}
116impl AsyncWrite for SharedWriter {
117	fn poll_write(
118		self: Pin<&mut Self>,
119		cx: &mut Context<'_>,
120		buf: &[u8],
121	) -> Poll<io::Result<usize>> {
122		let mut this = self.project();
123		this.buffer.extend_from_slice(buf);
124		if this.buffer.ends_with(b"\n") {
125			let fut = this.sender.send_ref();
126			pin_mut!(fut);
127			let mut send_buf = ready!(fut.poll_unpin(cx)).map_err(|_| {
128				io::Error::new(io::ErrorKind::Other, "thingbuf receiver has closed")
129			})?;
130			// Swap buffers
131			std::mem::swap(send_buf.deref_mut(), &mut this.buffer);
132			this.buffer.clear();
133			Poll::Ready(Ok(buf.len()))
134		} else {
135			Poll::Ready(Ok(buf.len()))
136		}
137	}
138	fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
139		let mut this = self.project();
140		let fut = this.sender.send_ref();
141		pin_mut!(fut);
142		let mut send_buf = ready!(fut.poll_unpin(cx))
143			.map_err(|_| io::Error::new(io::ErrorKind::Other, "thingbuf receiver has closed"))?;
144		// Swap buffers
145		std::mem::swap(send_buf.deref_mut(), &mut this.buffer);
146		this.buffer.clear();
147		Poll::Ready(Ok(()))
148	}
149	fn poll_close(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
150		Poll::Ready(Ok(()))
151	}
152}
153impl io::Write for SharedWriter {
154	fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
155		self.buffer.extend_from_slice(buf);
156		if self.buffer.ends_with(b"\n") {
157			match self.sender.try_send_ref() {
158				Ok(mut send_buf) => {
159					std::mem::swap(send_buf.deref_mut(), &mut self.buffer);
160					self.buffer.clear();
161				}
162				Err(TrySendError::Full(_)) => return Err(io::ErrorKind::WouldBlock.into()),
163				_ => {
164					return Err(io::Error::new(
165						io::ErrorKind::Other,
166						"thingbuf receiver has closed",
167					));
168				}
169			}
170		}
171		Ok(buf.len())
172	}
173	fn flush(&mut self) -> io::Result<()> {
174		Ok(())
175	}
176}
177
178/// Structure for reading lines of input from a terminal while lines are output
179/// to the terminal concurrently.
180///
181/// Terminal input is retrieved by calling [`Readline::readline()`], which
182/// returns each complete line of input once the user presses Enter.
183///
184/// Each `Readline` instance is associated with one or more [`SharedWriter`]
185/// instances.  Lines written to an associated `SharedWriter` are output while
186/// retrieving input with `readline()` or by calling
187/// [`flush()`][Readline::flush].
188pub struct Readline {
189	raw_term: Stdout,
190	event_stream: EventStream, // Stream of events
191	line_receiver: Receiver<Vec<u8>>,
192	line: LineState, // Current line
193}
194
195impl Readline {
196	/// Create a new `Readline` instance with an associated
197	/// [`SharedWriter`]
198	pub fn new(prompt: String) -> Result<(Self, SharedWriter), ReadlineError> {
199		let (sender, line_receiver) = thingbuf::mpsc::channel(500);
200		terminal::enable_raw_mode()?;
201
202		let line = LineState::new(prompt, terminal::size()?);
203
204		let mut readline = Readline {
205			raw_term: stdout(),
206			event_stream: EventStream::new(),
207			line_receiver,
208			line,
209		};
210		readline.line.render(&mut readline.raw_term)?;
211		readline.raw_term.queue(terminal::EnableLineWrap)?;
212		readline.raw_term.flush()?;
213		Ok((
214			readline,
215			SharedWriter {
216				sender,
217				buffer: Vec::new(),
218			},
219		))
220	}
221
222	/// Change the prompt
223	pub fn update_prompt(&mut self, prompt: &str) -> Result<(), ReadlineError> {
224		self.line.update_prompt(prompt, &mut self.raw_term)?;
225		Ok(())
226	}
227
228	/// Clear the screen
229	pub fn clear(&mut self) -> Result<(), ReadlineError> {
230		self.raw_term.queue(Clear(terminal::ClearType::All))?;
231		self.line.clear_and_render(&mut self.raw_term)?;
232		self.raw_term.flush()?;
233		Ok(())
234	}
235
236	/// Set maximum history length.  The default length is 1000.
237	pub fn set_max_history(&mut self, max_size: usize) {
238		self.line.history.set_max_size(max_size);
239	}
240
241	/// Set whether the input line should remain on the screen after
242	/// events.
243	///
244	/// If `enter` is true, then when the user presses "Enter", the prompt
245	/// and the text they entered will remain on the screen, and the cursor
246	/// will move to the next line.  If `enter` is false, the prompt &
247	/// input will be erased instead.
248	///
249	/// `control_c` similarly controls the behavior for when the user
250	/// presses Ctrl-C.
251	///
252	/// The default value for both settings is `true`.
253	pub fn should_print_line_on(&mut self, enter: bool, control_c: bool) {
254		self.line.should_print_line_on_enter = enter;
255		self.line.should_print_line_on_control_c = control_c;
256	}
257
258	/// Flush all writers to terminal and erase the prompt string
259	pub fn flush(&mut self) -> Result<(), ReadlineError> {
260		while let Ok(buf) = self.line_receiver.try_recv_ref() {
261			self.line.print_data(&buf, &mut self.raw_term)?;
262		}
263		self.line.clear(&mut self.raw_term)?;
264		self.raw_term.flush()?;
265		Ok(())
266	}
267
268	/// Polling function for readline, manages all input and output.
269	/// Returns either an Readline Event or an Error
270	pub async fn readline(&mut self) -> Result<ReadlineEvent, ReadlineError> {
271		loop {
272			select! {
273				event = self.event_stream.next().fuse() => match event {
274					Some(Ok(event)) => {
275						match self.line.handle_event(event, &mut self.raw_term) {
276							Ok(Some(event)) => {
277								self.raw_term.flush()?;
278								return Result::<_, ReadlineError>::Ok(event)
279							},
280							Err(e) => return Err(e),
281							Ok(None) => self.raw_term.flush()?,
282						}
283					}
284					Some(Err(e)) => return Err(e.into()),
285					None => {},
286				},
287				result = self.line_receiver.recv_ref().fuse() => match result {
288					Some(buf) => {
289						self.line.print_data(&buf, &mut self.raw_term)?;
290						self.raw_term.flush()?;
291					},
292					None => return Err(ReadlineError::Closed),
293				},
294			}
295		}
296	}
297
298	/// Add a line to the input history
299	pub fn add_history_entry(&mut self, entry: String) -> Option<()> {
300		self.line.history.add_entry(entry);
301		// Return value to keep compatibility with previous API.
302		Some(())
303	}
304
305	/// Returns the entries of the history in the order they were added in.
306	pub fn get_history_entries(&self) -> &VecDeque<String> {
307		self.line.history.get_entries()
308	}
309
310	/// Replaces the current history.
311	pub fn set_history_entries(&mut self, entries: impl IntoIterator<Item = String>) {
312		self.line.history.set_entries(entries);
313	}
314
315	/// Clears the current history.
316	pub fn clear_history(&mut self) {
317		self.set_history_entries([]);
318	}
319}
320
321impl Drop for Readline {
322	fn drop(&mut self) {
323		let _ = disable_raw_mode();
324	}
325}