use crate::{ChannelCapacity, CommonResultWithError, CursorBoundsCheck,
CursorPositionBoundsStatus, GCStringOwned, History, InputDevice, InputEvent,
KeyPress, LineState, LineStateControlSignal, LineStateLiveness,
ModifierKeysMask, OutputDevice, PauseBuffer, SafeHistory, SafeLineState,
SafePauseBuffer, SegIndex, SendRawTerminal, SharedWriter, Size, StdMutex,
execute_commands_no_lock, join, key_press, lock_output_device_as_mut};
use crossterm::{ExecutableCommand, QueueableCommand, cursor,
terminal::{self, Clear}};
use miette::Report as ErrorReport;
use std::{io::{self, Write},
num::NonZeroU8,
sync::Arc,
time::Duration};
use thiserror::Error;
use tokio::{select, spawn,
sync::{broadcast,
mpsc::{self, UnboundedReceiver, UnboundedSender}},
task::JoinHandle,
time::sleep};
pub const READLINE_ASYNC_INITIAL_PROMPT_DISPLAY_CURSOR_SHOW_DELAY: Duration =
Duration::from_millis(66);
#[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,
Tab,
BackTab,
PageUp,
PageDown,
Insert,
FnKey(NonZeroU8),
UnhandledKey(KeyPress),
Resized(Size),
}
#[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::{Arc, CommonResultWithError, ControlFlowLimited, JoinHandle, LineState,
LineStateControlSignal, LineStateLiveness, OutputDevice, PauseBuffer,
ReadlineError, SafeLineState, SafePauseBuffer, SendRawTerminal,
StdMutex, broadcast, io, join, lock_output_device_as_mut, mpsc, spawn};
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
}
#[allow(clippy::unwrap_in_result)]
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();
crate::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<()>,
channel_capacity: ChannelCapacity,
) -> 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);
}
crate::enable_raw_mode()?;
let line_state_control_channel =
mpsc::channel::<LineStateControlSignal>(channel_capacity.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! {
maybe_input_event = self.input_device.next() => {
if let Some(input_event) = maybe_input_event {
match readline_internal::apply_event_to_line_state_and_render(
input_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()
}
#[must_use]
pub fn get_buffer(&self) -> GCStringOwned {
self.safe_line_state.lock().unwrap().line.clone()
}
#[must_use]
pub fn get_cursor_position(&self) -> SegIndex {
self.safe_line_state.lock().unwrap().line_cursor_grapheme
}
#[must_use]
pub fn get_cursor_position_status(&self) -> CursorPositionBoundsStatus {
let line_state = self.safe_line_state.lock().unwrap();
line_state
.line
.segment_count()
.check_cursor_position_bounds(line_state.line_cursor_grapheme)
}
}
pub mod readline_internal {
#[allow(clippy::wildcard_imports)]
use super::*;
pub fn apply_event_to_line_state_and_render(
input_event: InputEvent,
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> {
let is_ctrl_c_or_d = input_event.matches_any_of_these_keypresses(&[
key_press!(@char ModifierKeysMask::new().with_ctrl(), 'c'),
key_press!(@char ModifierKeysMask::new().with_ctrl(), 'd'),
]);
let mut line_state = self_line_state.lock().unwrap();
let is_spinner_active = self_safe_is_spinner_active.lock().unwrap().take();
if is_ctrl_c_or_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(&input_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),
}
ControlFlowExtended::Continue
}
#[must_use]
fn convert_key_code_to_key(code: crossterm::event::KeyCode) -> Option<crate::Key> {
use crate::{FunctionKey, Key, SpecialKey};
use crossterm::event::KeyCode;
match code {
KeyCode::Char(c) => Some(Key::Character(c)),
KeyCode::F(n) => {
let fn_key = match n {
1 => FunctionKey::F1,
2 => FunctionKey::F2,
3 => FunctionKey::F3,
4 => FunctionKey::F4,
5 => FunctionKey::F5,
6 => FunctionKey::F6,
7 => FunctionKey::F7,
8 => FunctionKey::F8,
9 => FunctionKey::F9,
10 => FunctionKey::F10,
11 => FunctionKey::F11,
12 => FunctionKey::F12,
_ => return None,
};
Some(Key::FunctionKey(fn_key))
}
KeyCode::Up => Some(Key::SpecialKey(SpecialKey::Up)),
KeyCode::Down => Some(Key::SpecialKey(SpecialKey::Down)),
KeyCode::Left => Some(Key::SpecialKey(SpecialKey::Left)),
KeyCode::Right => Some(Key::SpecialKey(SpecialKey::Right)),
KeyCode::Home => Some(Key::SpecialKey(SpecialKey::Home)),
KeyCode::End => Some(Key::SpecialKey(SpecialKey::End)),
KeyCode::PageUp => Some(Key::SpecialKey(SpecialKey::PageUp)),
KeyCode::PageDown => Some(Key::SpecialKey(SpecialKey::PageDown)),
KeyCode::Tab => Some(Key::SpecialKey(SpecialKey::Tab)),
KeyCode::BackTab => Some(Key::SpecialKey(SpecialKey::BackTab)),
KeyCode::Delete => Some(Key::SpecialKey(SpecialKey::Delete)),
KeyCode::Insert => Some(Key::SpecialKey(SpecialKey::Insert)),
KeyCode::Enter => Some(Key::SpecialKey(SpecialKey::Enter)),
KeyCode::Backspace => Some(Key::SpecialKey(SpecialKey::Backspace)),
KeyCode::Esc => Some(Key::SpecialKey(SpecialKey::Esc)),
_ => None,
}
}
#[must_use]
fn convert_modifier_keys(
modifiers: crossterm::event::KeyModifiers,
) -> crate::ModifierKeysMask {
use crate::KeyState;
use crossterm::event::KeyModifiers;
crate::ModifierKeysMask {
shift_key_state: if modifiers.contains(KeyModifiers::SHIFT) {
KeyState::Pressed
} else {
KeyState::NotPressed
},
ctrl_key_state: if modifiers.contains(KeyModifiers::CONTROL) {
KeyState::Pressed
} else {
KeyState::NotPressed
},
alt_key_state: if modifiers.contains(KeyModifiers::ALT) {
KeyState::Pressed
} else {
KeyState::NotPressed
},
}
}
#[must_use]
fn convert_mouse_button(button: crossterm::event::MouseButton) -> crate::Button {
use crate::Button;
use crossterm::event::MouseButton;
match button {
MouseButton::Left => Button::Left,
MouseButton::Right => Button::Right,
MouseButton::Middle => Button::Middle,
}
}
#[must_use]
pub fn convert_crossterm_event_to_input_event(
event: crossterm::event::Event,
) -> Option<InputEvent> {
use crate::{KeyState, MouseInputKind};
use crossterm::event::{Event, KeyEvent, MouseEvent, MouseEventKind};
match event {
Event::Key(KeyEvent {
code, modifiers, ..
}) => {
let key = convert_key_code_to_key(code)?;
let mask = convert_modifier_keys(modifiers);
let keypress = if mask.shift_key_state == KeyState::NotPressed
&& mask.ctrl_key_state == KeyState::NotPressed
&& mask.alt_key_state == KeyState::NotPressed
{
KeyPress::Plain { key }
} else {
KeyPress::WithModifiers { key, mask }
};
Some(InputEvent::Keyboard(keypress))
}
Event::Mouse(MouseEvent {
kind,
column,
row,
modifiers,
}) => {
let modifiers_mask = convert_modifier_keys(modifiers);
let mouse_input = crate::MouseInput {
pos: crate::Pos {
col_index: crate::ColIndex::from(i32::from(column)),
row_index: crate::RowIndex::from(i32::from(row)),
},
kind: match kind {
MouseEventKind::Down(btn) => {
MouseInputKind::MouseDown(convert_mouse_button(btn))
}
MouseEventKind::Up(btn) => {
MouseInputKind::MouseUp(convert_mouse_button(btn))
}
MouseEventKind::Drag(btn) => {
MouseInputKind::MouseDrag(convert_mouse_button(btn))
}
MouseEventKind::Moved => MouseInputKind::MouseMove,
MouseEventKind::ScrollUp => MouseInputKind::ScrollUp,
MouseEventKind::ScrollDown => MouseInputKind::ScrollDown,
MouseEventKind::ScrollLeft => MouseInputKind::ScrollLeft,
MouseEventKind::ScrollRight => MouseInputKind::ScrollRight,
},
maybe_modifier_keys: if modifiers_mask.shift_key_state
== KeyState::NotPressed
&& modifiers_mask.ctrl_key_state == KeyState::NotPressed
&& modifiers_mask.alt_key_state == KeyState::NotPressed
{
None
} else {
Some(modifiers_mask)
},
};
Some(InputEvent::Mouse(mouse_input))
}
Event::Resize(width, height) => Some(InputEvent::Resize(crate::Size {
col_width: crate::ColWidth::from(width),
row_height: crate::RowHeight::from(height),
})),
_ => None,
}
}
}
#[cfg(test)]
pub mod readline_test_fixtures {
use crate::{CrosstermEventResult, InlineVec};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use smallvec::smallvec;
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::{Arc, ChannelCapacity, ControlFlowExtended, CursorPositionBoundsStatus,
Duration, History, InputDevice, LineStateControlSignal,
LineStateLiveness, OutputDevice, Readline, ReadlineEvent, StdMutex,
broadcast, lock_output_device_as_mut, readline_internal,
readline_test_fixtures::get_input_vec, sleep};
use crate::{OutputDeviceExt, TTYResult, is_output_interactive};
#[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 = "> ";
if let TTYResult::IsNotInteractive = is_output_interactive() {
return;
}
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,
ChannelCapacity::Minimal, )
.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 Some(input_event) =
readline_internal::convert_crossterm_event_to_input_event(event.clone())
else {
panic!("Failed to convert event");
};
let control_flow = readline_internal::apply_event_to_line_state_and_render(
input_event,
&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.as_str(), "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 = "> ";
if let TTYResult::IsNotInteractive = is_output_interactive() {
return;
}
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,
ChannelCapacity::Minimal, )
.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.as_str(),
""
);
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 = "> ";
if let TTYResult::IsNotInteractive = is_output_interactive() {
return;
}
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,
ChannelCapacity::Minimal, )
.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 = "> ";
if let TTYResult::IsNotInteractive = is_output_interactive() {
return;
}
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,
ChannelCapacity::Minimal, )
.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
);
}
#[tokio::test]
#[allow(clippy::needless_return)]
async fn test_editor_state_empty_buffer() {
use crate::seg_index;
let prompt_str = "> ";
if let TTYResult::IsNotInteractive = is_output_interactive() {
return;
}
let (output_device, _) = OutputDevice::new_mock();
let input_device = InputDevice::new_mock(smallvec::smallvec![]);
let (shutdown_sender, _) = broadcast::channel::<()>(1);
let (readline, _) = Readline::try_new(
prompt_str.into(),
output_device,
input_device,
shutdown_sender,
ChannelCapacity::Minimal,
)
.unwrap();
assert!(readline.get_buffer().is_empty());
assert_eq!(
readline.get_cursor_position_status(),
CursorPositionBoundsStatus::AtStart
);
assert_eq!(readline.get_cursor_position(), seg_index(0));
assert_eq!(readline.get_buffer().as_str(), "");
}
#[tokio::test]
#[allow(clippy::needless_return)]
async fn test_editor_state_with_content() {
use crate::{GCStringOwned, seg_index};
let prompt_str = "> ";
if let TTYResult::IsNotInteractive = is_output_interactive() {
return;
}
let (output_device, _) = OutputDevice::new_mock();
let input_device = InputDevice::new_mock(smallvec::smallvec![]);
let (shutdown_sender, _) = broadcast::channel::<()>(1);
let (readline, _) = Readline::try_new(
prompt_str.into(),
output_device,
input_device,
shutdown_sender,
ChannelCapacity::Minimal,
)
.unwrap();
{
let mut line_state = readline.safe_line_state.lock().unwrap();
line_state.line = GCStringOwned::new("hello");
line_state.line_cursor_grapheme = seg_index(5);
}
assert!(!readline.get_buffer().is_empty());
assert_eq!(
readline.get_cursor_position_status(),
CursorPositionBoundsStatus::AtEnd
);
assert_eq!(readline.get_cursor_position(), seg_index(5));
assert_eq!(readline.get_buffer().as_str(), "hello");
}
#[tokio::test]
#[allow(clippy::needless_return)]
async fn test_editor_state_cursor_at_start_with_content() {
use crate::{GCStringOwned, seg_index};
let prompt_str = "> ";
if let TTYResult::IsNotInteractive = is_output_interactive() {
return;
}
let (output_device, _) = OutputDevice::new_mock();
let input_device = InputDevice::new_mock(smallvec::smallvec![]);
let (shutdown_sender, _) = broadcast::channel::<()>(1);
let (readline, _) = Readline::try_new(
prompt_str.into(),
output_device,
input_device,
shutdown_sender,
ChannelCapacity::Minimal,
)
.unwrap();
{
let mut line_state = readline.safe_line_state.lock().unwrap();
line_state.line = GCStringOwned::new("hello");
line_state.line_cursor_grapheme = seg_index(0);
}
assert!(!readline.get_buffer().is_empty());
assert_eq!(
readline.get_cursor_position_status(),
CursorPositionBoundsStatus::AtStart
);
assert_eq!(readline.get_cursor_position(), seg_index(0));
assert_eq!(readline.get_buffer().as_str(), "hello");
}
}
#[cfg(test)]
mod test_streams {
use super::*;
use crate::core::test_fixtures::gen_input_stream;
use test_streams::readline_test_fixtures::get_input_vec;
#[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 super::*;
use crate::core::test_fixtures::StdoutMock;
use manage_shared_writer_output::flush_internal;
use std::sync::Mutex;
#[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> > "
);
}
}