use std::{io::{self, Write},
sync::Arc,
time::Duration};
use crossterm::{cursor,
terminal::{self, disable_raw_mode, Clear},
ExecutableCommand, QueueableCommand};
use miette::Report as ErrorReport;
use thiserror::Error;
use tokio::{select, spawn,
sync::{broadcast,
mpsc::{self, UnboundedReceiver, UnboundedSender}},
task::JoinHandle,
time::sleep};
use crate::{execute_commands_no_lock, join, lock_output_device_as_mut,
CommonResultWithError, History, InputDevice, LineState,
LineStateControlSignal, LineStateLiveness, OutputDevice, PauseBuffer,
SafeHistory, SafeLineState, SafePauseBuffer, SendRawTerminal, SharedWriter,
StdMutex, CHANNEL_CAPACITY};
pub const READLINE_ASYNC_INITIAL_PROMPT_DISPLAY_CURSOR_SHOW_DELAY: Duration =
Duration::from_millis(66);
const CTRL_C: crossterm::event::Event =
crossterm::event::Event::Key(crossterm::event::KeyEvent::new(
crossterm::event::KeyCode::Char('c'),
crossterm::event::KeyModifiers::CONTROL,
));
const CTRL_D: crossterm::event::Event =
crossterm::event::Event::Key(crossterm::event::KeyEvent::new(
crossterm::event::KeyCode::Char('d'),
crossterm::event::KeyModifiers::CONTROL,
));
#[allow(missing_debug_implementations)]
pub struct Readline {
pub output_device: OutputDevice,
pub input_device: InputDevice,
pub safe_line_state: SafeLineState,
pub history_sender: UnboundedSender<String>,
pub history_receiver: UnboundedReceiver<String>,
pub safe_history: SafeHistory,
pub safe_is_paused_buffer: SafePauseBuffer,
pub safe_spinner_is_active: Arc<StdMutex<Option<broadcast::Sender<()>>>>,
shutdown_complete_sender: broadcast::Sender<()>,
}
#[derive(Debug, Error)]
pub enum ReadlineError {
#[error(transparent)]
IO(#[from] io::Error),
#[error("line writers closed")]
Closed,
}
impl From<ErrorReport> for ReadlineError {
fn from(report: ErrorReport) -> Self {
ReadlineError::IO(io::Error::other(format!("{report}")))
}
}
#[derive(Debug, PartialEq, Clone)]
pub enum ReadlineEvent {
Line(String),
Eof,
Interrupted,
Resized,
}
#[derive(Debug, PartialEq, Clone)]
pub enum ControlFlowExtended<T, E> {
ReturnOk(T),
ReturnError(E),
Continue,
}
#[derive(Debug, PartialEq, Clone)]
pub enum ControlFlowLimited<E> {
ReturnError(E),
Continue,
}
pub mod manage_shared_writer_output {
use super::{broadcast, io, join, lock_output_device_as_mut, mpsc, spawn, Arc,
CommonResultWithError, ControlFlowLimited, JoinHandle, LineState,
LineStateControlSignal, LineStateLiveness, OutputDevice, PauseBuffer,
ReadlineError, SafeLineState, SafePauseBuffer, SendRawTerminal, StdMutex};
pub fn spawn_task_to_monitor_line_control_channel(
mut line_control_channel_receiver: mpsc::Receiver<LineStateControlSignal>,
safe_line_state: SafeLineState,
output_device: OutputDevice,
safe_is_paused_buffer: SafePauseBuffer,
safe_spinner_is_active: Arc<StdMutex<Option<broadcast::Sender<()>>>>,
shutdown_complete_sender: broadcast::Sender<()>,
) -> JoinHandle<()> {
spawn(async move {
loop {
let maybe_line_control_signal = line_control_channel_receiver.recv();
#[allow(clippy::single_match_else)]
match maybe_line_control_signal.await {
Some(maybe_line_control_signal) => {
let control_flow = process_line_control_signal(
maybe_line_control_signal,
safe_is_paused_buffer.clone(),
safe_line_state.clone(),
output_device.clone(),
safe_spinner_is_active.clone(),
);
match control_flow {
ControlFlowLimited::ReturnError(_) => {
shutdown_complete_sender.send(()).ok();
break;
}
ControlFlowLimited::Continue => {
}
}
}
_ => {
shutdown_complete_sender.send(()).ok();
break;
}
}
}
})
}
#[allow(clippy::needless_pass_by_value)]
pub fn process_line_control_signal(
line_control_signal: LineStateControlSignal,
self_safe_is_paused_buffer: SafePauseBuffer,
self_safe_line_state: SafeLineState,
output_device: OutputDevice,
self_safe_spinner_is_active: Arc<StdMutex<Option<broadcast::Sender<()>>>>,
) -> ControlFlowLimited<ReadlineError> {
match line_control_signal {
LineStateControlSignal::ExitReadlineLoop => {
return ControlFlowLimited::ReturnError(ReadlineError::Closed);
}
LineStateControlSignal::Line(buf) => {
let mut line_state = self_safe_line_state.lock().unwrap();
if line_state.is_paused.is_paused() {
let pause_buffer = &mut *self_safe_is_paused_buffer.lock().unwrap();
pause_buffer.push(buf);
return ControlFlowLimited::Continue;
}
let term = lock_output_device_as_mut!(output_device);
if let Err(err) = line_state.print_data_and_flush(buf.as_ref(), term) {
return ControlFlowLimited::ReturnError(err);
}
if let Err(err) = term.flush() {
return ControlFlowLimited::ReturnError(err.into());
}
}
LineStateControlSignal::Flush => {
let is_paused = self_safe_line_state.lock().unwrap().is_paused;
let term = lock_output_device_as_mut!(output_device);
let line_state = self_safe_line_state.lock().unwrap();
flush_internal(&self_safe_is_paused_buffer, is_paused, line_state, term)
.ok();
}
LineStateControlSignal::Pause => {
let new_value = LineStateLiveness::Paused;
let term = lock_output_device_as_mut!(output_device);
let mut line_state = self_safe_line_state.lock().unwrap();
if line_state.set_paused(new_value, term).is_err() {
return ControlFlowLimited::ReturnError(ReadlineError::IO(
io::Error::other("failed to pause terminal"),
));
}
}
LineStateControlSignal::Resume => {
let new_value = LineStateLiveness::NotPaused;
let mut line_state = self_safe_line_state.lock().unwrap();
let term = lock_output_device_as_mut!(output_device);
if line_state.set_paused(new_value, term).is_err() {
return ControlFlowLimited::ReturnError(ReadlineError::IO(
io::Error::other("failed to resume terminal"),
));
}
flush_internal(&self_safe_is_paused_buffer, new_value, line_state, term)
.ok();
}
LineStateControlSignal::SpinnerActive(spinner_shutdown_sender) => {
let mut spinner_is_active = self_safe_spinner_is_active.lock().unwrap();
*spinner_is_active = Some(spinner_shutdown_sender);
}
LineStateControlSignal::SpinnerInactive => {
let mut spinner_is_active = self_safe_spinner_is_active.lock().unwrap();
let _unused: Option<_> = spinner_is_active.take();
}
}
ControlFlowLimited::Continue
}
pub fn flush_internal(
self_safe_is_paused_buffer: &SafePauseBuffer,
is_paused: LineStateLiveness,
mut line_state: std::sync::MutexGuard<'_, LineState>,
term: &mut SendRawTerminal,
) -> CommonResultWithError<(), ReadlineError> {
if is_paused.is_paused() {
return Ok(());
}
let is_paused_buffer = {
let paused_text_buffer: PauseBuffer = self_safe_is_paused_buffer
.lock()
.unwrap()
.drain(..)
.collect();
join!(
from: paused_text_buffer,
each: text,
delim: "",
format: "{text}"
)
};
line_state.print_data_and_flush(is_paused_buffer.as_bytes(), term)?;
line_state.clear_and_render_and_flush(term)?;
Ok(())
}
}
impl Drop for Readline {
fn drop(&mut self) {
let term = lock_output_device_as_mut!(self.output_device);
self.safe_line_state.lock().unwrap().exit(term).ok();
disable_raw_mode().ok();
}
}
impl Readline {
#[allow(clippy::unwrap_in_result)]
#[allow(clippy::needless_pass_by_value)]
pub fn try_new(
prompt: String,
output_device: OutputDevice,
input_device: InputDevice,
shutdown_complete_sender: broadcast::Sender<()>,
) -> CommonResultWithError<(Self, SharedWriter), ReadlineError> {
{
let writer = lock_output_device_as_mut!(output_device);
execute_commands_no_lock!(writer, cursor::Hide);
execute_commands_no_lock!(writer, terminal::EnableLineWrap);
}
terminal::enable_raw_mode()?;
let line_state_control_channel =
mpsc::channel::<LineStateControlSignal>(CHANNEL_CAPACITY);
let (line_control_channel_sender, line_state_control_channel_receiver) =
line_state_control_channel;
let (history, history_receiver) = History::new();
let history_sender = history.sender.clone();
let safe_history = Arc::new(StdMutex::new(history));
let line_state = LineState::new(prompt, terminal::size()?);
let safe_line_state = Arc::new(StdMutex::new(line_state));
let is_paused_buffer = PauseBuffer::new();
let safe_is_paused_buffer = Arc::new(StdMutex::new(is_paused_buffer));
let safe_spinner_is_active = Arc::new(StdMutex::new(None));
manage_shared_writer_output::spawn_task_to_monitor_line_control_channel(
line_state_control_channel_receiver,
safe_line_state.clone(),
output_device.clone(),
safe_is_paused_buffer.clone(),
safe_spinner_is_active.clone(),
shutdown_complete_sender.clone(),
);
let readline = Readline {
output_device: output_device.clone(),
input_device,
safe_line_state: safe_line_state.clone(),
history_sender,
history_receiver,
safe_history,
safe_is_paused_buffer,
safe_spinner_is_active,
shutdown_complete_sender,
};
{
let term = lock_output_device_as_mut!(output_device);
readline
.safe_line_state
.lock()
.unwrap()
.render_and_flush(term)?;
}
let output_device_clone = output_device.clone();
spawn({
async move {
sleep(READLINE_ASYNC_INITIAL_PROMPT_DISPLAY_CURSOR_SHOW_DELAY).await;
let term = lock_output_device_as_mut!(output_device_clone);
term.execute(cursor::Show).ok();
}
});
let shared_writer = SharedWriter::new(line_control_channel_sender);
Ok((readline, shared_writer))
}
#[allow(clippy::unwrap_in_result)]
pub fn update_prompt(
&mut self,
prompt: &str,
) -> CommonResultWithError<(), ReadlineError> {
let term = lock_output_device_as_mut!(self.output_device);
self.safe_line_state
.lock()
.unwrap()
.update_prompt(prompt, term)?;
Ok(())
}
#[allow(clippy::unwrap_in_result)]
pub fn clear(&mut self) -> CommonResultWithError<(), ReadlineError> {
let term = lock_output_device_as_mut!(self.output_device);
term.queue(Clear(terminal::ClearType::All))?;
self.safe_line_state
.lock()
.unwrap()
.clear_and_render_and_flush(term)?;
term.flush()?;
Ok(())
}
pub fn set_max_history(&mut self, max_size: usize) {
let mut history = self.safe_history.lock().unwrap();
history.max_size = max_size;
history.entries.truncate(max_size);
}
pub fn should_print_line_on(&mut self, enter: bool, control_c: bool) {
let mut line_state = self.safe_line_state.lock().unwrap();
line_state.should_print_line_on_enter = enter;
line_state.should_print_line_on_control_c = control_c;
}
pub async fn readline(
&mut self,
) -> CommonResultWithError<ReadlineEvent, ReadlineError> {
let mut shutdown_complete_receiver = self.shutdown_complete_sender.subscribe();
loop {
select! {
result_crossterm_event = self.input_device.next() => {
match readline_internal::apply_event_to_line_state_and_render(
result_crossterm_event,
&self.safe_line_state,
lock_output_device_as_mut!(self.output_device),
&self.safe_history,
&self.safe_spinner_is_active,
) {
ControlFlowExtended::ReturnOk(ok_value) => {
return Ok(ok_value);
},
ControlFlowExtended::ReturnError(err_value) => {
return Err(err_value);
},
ControlFlowExtended::Continue => {}
}
},
maybe_line = self.history_receiver.recv() => {
self.safe_history.lock().unwrap().update(maybe_line);
},
_ = shutdown_complete_receiver.recv() => {
return Err(ReadlineError::Closed);
}
}
}
}
pub fn add_history_entry(&mut self, entry: String) -> Option<()> {
self.history_sender.send(entry).ok()
}
}
pub mod readline_internal {
use super::{broadcast, Arc, ControlFlowExtended, ReadlineError, ReadlineEvent,
SafeHistory, SafeLineState, StdMutex, Write, CTRL_C, CTRL_D};
pub fn apply_event_to_line_state_and_render(
result_crossterm_event: miette::Result<crossterm::event::Event>,
self_line_state: &SafeLineState,
term: &mut dyn Write,
self_safe_history: &SafeHistory,
self_safe_is_spinner_active: &Arc<StdMutex<Option<broadcast::Sender<()>>>>,
) -> ControlFlowExtended<ReadlineEvent, ReadlineError> {
match result_crossterm_event {
Ok(crossterm_event) => {
let mut line_state = self_line_state.lock().unwrap();
let is_spinner_active =
self_safe_is_spinner_active.lock().unwrap().take();
if (crossterm_event == CTRL_C || crossterm_event == CTRL_D)
&& let Some(spinner_shutdown_sender) = is_spinner_active {
spinner_shutdown_sender.send(()).ok();
return ControlFlowExtended::Continue;
}
let result_maybe_readline_event = line_state.apply_event_and_render(
&crossterm_event,
term,
self_safe_history,
);
match result_maybe_readline_event {
Ok(maybe_readline_event) => {
if let Some(readline_event) = maybe_readline_event {
return ControlFlowExtended::ReturnOk(readline_event);
}
}
Err(e) => return ControlFlowExtended::ReturnError(e),
}
}
Err(report) => {
return ControlFlowExtended::ReturnError(ReadlineError::IO(
std::io::Error::other(format!("{report}")),
));
}
}
ControlFlowExtended::Continue
}
}
#[cfg(test)]
pub mod readline_test_fixtures {
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use smallvec::smallvec;
use crate::{CrosstermEventResult, InlineVec};
pub(super) fn get_input_vec() -> InlineVec<CrosstermEventResult> {
smallvec![
Ok(Event::Key(KeyEvent::new(
KeyCode::Char('a'),
KeyModifiers::NONE,
))),
Ok(Event::Key(KeyEvent::new(
KeyCode::Char('b'),
KeyModifiers::NONE,
))),
Ok(Event::Key(KeyEvent::new(
KeyCode::Char('c'),
KeyModifiers::NONE,
))),
Ok(Event::Key(KeyEvent::new(
KeyCode::Enter,
KeyModifiers::NONE,
))),
]
}
}
#[cfg(test)]
mod test_readline {
use super::{broadcast, lock_output_device_as_mut, readline_internal,
readline_test_fixtures::get_input_vec, sleep, Arc, ControlFlowExtended,
Duration, History, InputDevice, LineStateControlSignal,
LineStateLiveness, OutputDevice, Readline, ReadlineEvent, StdMutex};
use crate::{return_if_not_interactive_terminal, InputDeviceExtMock, OutputDeviceExt,
TTYResult};
#[tokio::test]
#[allow(clippy::needless_return)]
async fn test_readline_internal_process_event_and_terminal_output() {
let vec = get_input_vec();
let mut iter = vec.iter();
let prompt_str = "> ";
return_if_not_interactive_terminal!();
let (output_device, stdout_mock) = OutputDevice::new_mock();
let input_device = InputDevice::new_mock(get_input_vec());
let (shutdown_sender, _) = broadcast::channel::<()>(1);
let (readline, _) = Readline::try_new(
prompt_str.into(),
output_device.clone(),
input_device,
shutdown_sender,
)
.unwrap();
let safe_is_spinner_active = Arc::new(StdMutex::new(None));
let history = History::new();
let safe_history = Arc::new(StdMutex::new(history.0));
let Some(Ok(event)) = iter.next() else {
panic!();
};
let control_flow = readline_internal::apply_event_to_line_state_and_render(
Ok(event.clone()),
&readline.safe_line_state,
lock_output_device_as_mut!(output_device),
&safe_history,
&safe_is_spinner_active,
);
assert!(matches!(control_flow, ControlFlowExtended::Continue));
assert_eq!(readline.safe_line_state.lock().unwrap().line, "a");
let output_buffer_data = stdout_mock.get_copy_of_buffer_as_string_strip_ansi();
assert!(output_buffer_data.contains("> a"));
}
#[tokio::test]
#[allow(clippy::needless_return)]
async fn test_readline() {
let prompt_str = "> ";
return_if_not_interactive_terminal!();
let (output_device, stdout_mock) = OutputDevice::new_mock();
let input_device = InputDevice::new_mock(get_input_vec());
let (shutdown_sender, _) = broadcast::channel::<()>(1);
let (mut readline, _) = Readline::try_new(
prompt_str.into(),
output_device.clone(),
input_device,
shutdown_sender,
)
.unwrap();
let result = readline.readline().await;
assert!(matches!(result, Ok(ReadlineEvent::Line(_))));
pretty_assertions::assert_eq!(
result.unwrap(),
ReadlineEvent::Line("abc".to_string())
);
pretty_assertions::assert_eq!(readline.safe_line_state.lock().unwrap().line, "");
let output_buffer_data = stdout_mock.get_copy_of_buffer_as_string_strip_ansi();
assert!(output_buffer_data.contains("> abc"));
}
#[tokio::test]
#[allow(clippy::needless_return)]
async fn test_pause_resume() {
let prompt_str = "> ";
return_if_not_interactive_terminal!();
let (output_device, _) = OutputDevice::new_mock();
let input_device = InputDevice::new_mock(get_input_vec());
let (shutdown_sender, _) = broadcast::channel::<()>(1);
let (readline, shared_writer) = Readline::try_new(
prompt_str.into(),
output_device.clone(),
input_device,
shutdown_sender,
)
.unwrap();
shared_writer
.line_state_control_channel_sender
.send(LineStateControlSignal::Pause)
.await
.unwrap();
sleep(Duration::from_millis(1)).await;
assert_eq!(
readline.safe_line_state.lock().unwrap().is_paused,
LineStateLiveness::Paused
);
shared_writer
.line_state_control_channel_sender
.send(LineStateControlSignal::Resume)
.await
.unwrap();
sleep(Duration::from_millis(1)).await;
assert_eq!(
readline.safe_line_state.lock().unwrap().is_paused,
LineStateLiveness::NotPaused
);
}
#[tokio::test]
#[allow(clippy::needless_return)]
async fn test_pause_resume_with_output() {
let prompt_str = "> ";
return_if_not_interactive_terminal!();
let (output_device, _) = OutputDevice::new_mock();
let input_device = InputDevice::new_mock(get_input_vec());
let (shutdown_sender, _) = broadcast::channel::<()>(1);
let (readline, shared_writer) = Readline::try_new(
prompt_str.into(),
output_device.clone(),
input_device,
shutdown_sender,
)
.unwrap();
shared_writer
.line_state_control_channel_sender
.send(LineStateControlSignal::Pause)
.await
.unwrap();
sleep(Duration::from_millis(1)).await;
assert_eq!(
readline.safe_line_state.lock().unwrap().is_paused,
LineStateLiveness::Paused
);
shared_writer
.line_state_control_channel_sender
.send(LineStateControlSignal::Line("abc".into()))
.await
.unwrap();
sleep(Duration::from_millis(1)).await;
let pause_buffer = readline.safe_is_paused_buffer.lock().unwrap().clone();
assert_eq!(pause_buffer.len(), 1);
assert_eq!(pause_buffer[0], "abc");
shared_writer
.line_state_control_channel_sender
.send(LineStateControlSignal::Resume)
.await
.unwrap();
sleep(Duration::from_millis(1)).await;
assert_eq!(
readline.safe_line_state.lock().unwrap().is_paused,
LineStateLiveness::NotPaused
);
}
}
#[cfg(test)]
mod test_streams {
use test_streams::readline_test_fixtures::get_input_vec;
use super::*;
use crate::core::test_fixtures::gen_input_stream;
#[tokio::test]
#[allow(clippy::needless_return)]
async fn test_generate_event_stream_pinned() {
use futures_util::StreamExt;
let mut count = 0;
let mut it = gen_input_stream(get_input_vec());
while let Some(event) = it.next().await {
let lhs = event.unwrap();
let rhs = get_input_vec()[count].as_ref().unwrap().clone();
assert_eq!(lhs, rhs);
count += 1;
}
}
}
#[cfg(test)]
mod test_pause_and_resume_support {
use std::sync::Mutex;
use manage_shared_writer_output::flush_internal;
use super::*;
use crate::core::test_fixtures::StdoutMock;
#[test]
fn test_flush_internal_paused() {
let safe_line_state =
Arc::new(Mutex::new(LineState::new("> ".to_string(), (100, 100))));
let mut pause_buffer = PauseBuffer::new();
pause_buffer.push("Paused line 1".into());
pause_buffer.push("Paused line 2".into());
let safe_is_paused_buffer = Arc::new(Mutex::new(pause_buffer));
let mut stdout_mock = StdoutMock::default();
let line_state = safe_line_state.lock().unwrap();
let result = flush_internal(
&safe_is_paused_buffer,
LineStateLiveness::Paused,
line_state,
&mut stdout_mock,
);
assert!(result.is_ok());
assert_eq!(stdout_mock.get_copy_of_buffer_as_string_strip_ansi(), "");
}
#[test]
fn test_flush_internal_not_paused() {
let safe_line_state =
Arc::new(Mutex::new(LineState::new("> ".to_string(), (100, 100))));
let mut pause_buffer = PauseBuffer::new();
pause_buffer.push("Paused line 1".into());
pause_buffer.push("Paused line 2".into());
let safe_is_paused_buffer = Arc::new(Mutex::new(pause_buffer));
let mut stdout_mock = StdoutMock::default();
let line_state = safe_line_state.lock().unwrap();
let result = flush_internal(
&safe_is_paused_buffer,
LineStateLiveness::NotPaused,
line_state,
&mut stdout_mock,
);
assert!(result.is_ok());
assert_eq!(
stdout_mock.get_copy_of_buffer_as_string_strip_ansi(),
"Paused line 1Paused line 2\n> > "
);
}
}