use clap::ValueEnum;
use miette::IntoDiagnostic as _;
use crate::{ch,
enter_event_loop_async,
fg_green,
get_size,
inline_string,
usize,
CalculateResizeHint,
CaretVerticalViewportLocation,
EventLoopResult,
Header,
Height,
InputDevice,
InputEvent,
ItemsOwned,
Key,
KeyPress,
KeyState,
LineStateControlSignal,
ModifierKeysMask,
OutputDevice,
SelectComponent,
SharedWriter,
SpecialKey,
State,
StyleSheet,
Width,
DEVELOPMENT_MODE};
pub const DEFAULT_HEIGHT: usize = 5;
pub struct DefaultIoDevices {
pub output_device: OutputDevice,
pub input_device: InputDevice,
pub maybe_shared_writer: Option<SharedWriter>,
}
impl Default for DefaultIoDevices {
fn default() -> Self {
let output_device = OutputDevice::new_stdout();
let input_device = InputDevice::new_event_stream();
DefaultIoDevices {
output_device,
input_device,
maybe_shared_writer: None,
}
}
}
impl DefaultIoDevices {
pub fn as_mut_tuple(
&mut self,
) -> (&mut OutputDevice, &mut InputDevice, Option<SharedWriter>) {
(
&mut self.output_device,
&mut self.input_device,
self.maybe_shared_writer.clone(),
)
}
}
pub async fn choose<'a>(
arg_header: impl Into<Header>,
arg_options_to_choose_from: impl Into<ItemsOwned>,
maybe_max_height: Option<Height>,
maybe_max_width: Option<Width>,
how: HowToChoose,
stylesheet: StyleSheet,
io: (
&'a mut OutputDevice,
&'a mut InputDevice,
Option<SharedWriter>,
),
) -> miette::Result<ItemsOwned> {
let from = arg_options_to_choose_from.into();
let (od, id, msw) = io;
if let Some(ref sw) = msw {
sw.line_state_control_channel_sender
.send(LineStateControlSignal::Pause)
.await
.into_diagnostic()?;
}
let max_display_height = ch({
match maybe_max_height {
None => DEFAULT_HEIGHT,
Some(row_height) => {
let row_height = row_height.as_usize();
if row_height == 0 {
DEFAULT_HEIGHT
} else {
std::cmp::min(row_height, from.len())
}
}
}
});
let max_display_width = ch(match maybe_max_width {
None => 0,
Some(col_width) => col_width.as_usize(),
});
let mut state = State {
max_display_height,
max_display_width,
items: from,
header: arg_header.into(),
selection_mode: how,
..Default::default()
};
let mut fc = SelectComponent {
output_device: od.clone(),
style: stylesheet,
};
if let Ok(size) = get_size() {
state.set_size(size);
}
let res_user_input =
enter_event_loop_async(&mut state, &mut fc, keypress_handler, id).await;
if let Some(ref sw) = msw {
sw.line_state_control_channel_sender
.send(LineStateControlSignal::Resume)
.await
.into_diagnostic()?;
}
match res_user_input {
Ok(EventLoopResult::ExitWithResult(it)) => Ok(it),
_ => Ok(ItemsOwned::default()),
}
}
fn keypress_handler(state: &mut State, ie: InputEvent) -> EventLoopResult {
DEVELOPMENT_MODE.then(|| {
tracing::debug!(
message = "🔆🔆🔆 *before* keypress: locate_cursor_in_viewport()",
cursor_location_in_viewport = ?state.locate_cursor_in_viewport()
);
});
let selection_mode = state.selection_mode;
let return_it = match ie {
InputEvent::Resize(size) => {
DEVELOPMENT_MODE.then(|| {
tracing::debug! {
message = "🍎🍎🍎 keypress_handler() resize",
details = %inline_string!(
"New size width:{w} x height:{h}",
w = fg_green(&inline_string!("{:?}", size.col_width)),
h = fg_green(&inline_string!("{:?}", size.row_height)),
)
};
});
state.set_resize_hint(size);
EventLoopResult::ContinueAndRerenderAndClear
}
InputEvent::Keyboard(KeyPress::Plain {
key: Key::SpecialKey(SpecialKey::Down),
}) => {
DEVELOPMENT_MODE.then(|| {
tracing::debug!(message = "Down");
});
let caret_location = state.locate_cursor_in_viewport();
match caret_location {
CaretVerticalViewportLocation::AtAbsoluteTop
| CaretVerticalViewportLocation::AboveTopOfViewport
| CaretVerticalViewportLocation::AtTopOfViewport
| CaretVerticalViewportLocation::InMiddleOfViewport => {
state.raw_caret_row_index += 1;
}
CaretVerticalViewportLocation::AtBottomOfViewport
| CaretVerticalViewportLocation::BelowBottomOfViewport => {
state.scroll_offset_row_index += 1;
}
CaretVerticalViewportLocation::AtAbsoluteBottom
| CaretVerticalViewportLocation::NotFound => {
}
}
DEVELOPMENT_MODE.then(|| {
tracing::debug!(
message = "enter_event_loop()::state",
state = ?state
);
});
EventLoopResult::ContinueAndRerender
}
InputEvent::Keyboard(KeyPress::Plain {
key: Key::SpecialKey(SpecialKey::Up),
}) => {
DEVELOPMENT_MODE.then(|| {
tracing::debug!(message = "Up");
});
match state.locate_cursor_in_viewport() {
CaretVerticalViewportLocation::NotFound
| CaretVerticalViewportLocation::AtAbsoluteTop => {
}
CaretVerticalViewportLocation::AboveTopOfViewport
| CaretVerticalViewportLocation::AtTopOfViewport => {
state.scroll_offset_row_index -= 1;
}
CaretVerticalViewportLocation::InMiddleOfViewport => {
state.raw_caret_row_index -= 1;
}
CaretVerticalViewportLocation::AtBottomOfViewport
| CaretVerticalViewportLocation::BelowBottomOfViewport
| CaretVerticalViewportLocation::AtAbsoluteBottom => {
state.raw_caret_row_index -= 1;
}
}
EventLoopResult::ContinueAndRerender
}
InputEvent::Keyboard(KeyPress::Plain {
key: Key::SpecialKey(SpecialKey::Enter),
}) if selection_mode == HowToChoose::Multiple => {
DEVELOPMENT_MODE.then(|| {
tracing::debug!(
message = "Enter on multi-select",
selected_items = ?state.selected_items
);
});
if state.selected_items.is_empty() {
EventLoopResult::ExitWithoutResult
} else {
EventLoopResult::ExitWithResult(state.selected_items.clone())
}
}
InputEvent::Keyboard(KeyPress::Plain {
key: Key::SpecialKey(SpecialKey::Enter),
}) => {
DEVELOPMENT_MODE.then(|| {
tracing::debug!(
message = "Enter",
focused_index = ?state.get_focused_index()
);
});
let selection_index = usize(state.get_focused_index());
let maybe_item = state.items.get(selection_index);
match maybe_item {
Some(it) => EventLoopResult::ExitWithResult(it.into()),
None => EventLoopResult::ExitWithoutResult,
}
}
InputEvent::Keyboard(KeyPress::Plain {
key: Key::SpecialKey(SpecialKey::Esc),
})
| InputEvent::Keyboard(KeyPress::WithModifiers {
key: Key::Character('c'),
mask:
ModifierKeysMask {
ctrl_key_state: KeyState::Pressed,
shift_key_state: KeyState::NotPressed,
alt_key_state: KeyState::NotPressed,
},
}) => {
DEVELOPMENT_MODE.then(|| {
tracing::debug!(message = "Esc");
});
EventLoopResult::ExitWithoutResult
}
InputEvent::Keyboard(KeyPress::Plain {
key: Key::Character(' '),
}) if selection_mode == HowToChoose::Multiple => {
DEVELOPMENT_MODE.then(|| {
tracing::debug!(
message = "Space on multi-select",
focused_index = ?state.get_focused_index()
);
});
let selection_index = usize(state.get_focused_index());
let maybe_item = state.items.get(selection_index);
let maybe_index = state
.selected_items
.iter()
.position(|item| Some(item) == maybe_item);
match (maybe_item, maybe_index) {
(None, _) => (),
(Some(_), Some(it)) => {
state.selected_items.remove(it);
}
(Some(it), None) => state.selected_items.push(it.clone()),
};
EventLoopResult::ContinueAndRerender
}
InputEvent::Keyboard(KeyPress::Plain {
key: Key::Character(' '),
}) => {
DEVELOPMENT_MODE.then(|| {
tracing::debug!(message = "Space");
});
EventLoopResult::Continue
}
_ => {
DEVELOPMENT_MODE.then(|| {
tracing::debug!(message = "Ignore key event");
});
EventLoopResult::Continue
}
};
DEVELOPMENT_MODE.then(|| {
tracing::debug!(
message = "👉 *after* keypress: locate_cursor_in_viewport()",
cursor_location_in_viewport = ?state.locate_cursor_in_viewport()
);
});
return_it
}
#[derive(
Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Default, Hash,
)]
pub enum HowToChoose {
#[default]
Single,
Multiple,
}
#[cfg(test)]
mod test_choose_async {
use std::{io::Write as _, time::Duration};
use smallvec::smallvec;
use super::*;
use crate::{CrosstermEventResult,
InlineVec,
InputDevice,
InputDeviceExtMock,
OutputDevice,
OutputDeviceExt,
SharedWriter};
fn generated_key_events() -> InlineVec<CrosstermEventResult> {
let generator_vec: InlineVec<CrosstermEventResult> = smallvec![
Ok(crossterm::event::Event::Key(
crossterm::event::KeyEvent::new(
crossterm::event::KeyCode::Down,
crossterm::event::KeyModifiers::empty(),
),
)),
Ok(crossterm::event::Event::Key(
crossterm::event::KeyEvent::new(
crossterm::event::KeyCode::Down,
crossterm::event::KeyModifiers::empty(),
),
)),
Ok(crossterm::event::Event::Key(
crossterm::event::KeyEvent::new(
crossterm::event::KeyCode::Enter,
crossterm::event::KeyModifiers::empty(),
),
)),
];
generator_vec
}
#[tokio::test]
async fn test_shared_writer_pause_works() {
let (mut line_receiver, shared_writer) = SharedWriter::new_mock();
let (mut output_device, stdout_mock) = OutputDevice::new_mock();
let mut input_device = InputDevice::new_mock_with_delay(
generated_key_events(),
Duration::from_millis(10),
);
let mut sw_1 = shared_writer.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(10)).await;
sw_1.write_all(b"data after 10ms delay\n").unwrap();
tokio::time::sleep(Duration::from_millis(100)).await;
sw_1.write_all(b"data after 100ms delay\n").unwrap();
});
assert_eq!(shared_writer.buffer, "");
assert_eq!(stdout_mock.get_copy_of_buffer_as_string(), "");
assert!(line_receiver.is_empty());
_ = choose(
Header::SingleLine("Choose one:".into()),
&["one", "two", "three"],
None,
None,
HowToChoose::Single,
StyleSheet::default(),
(
&mut output_device,
&mut input_device,
Some(shared_writer.clone()),
),
)
.await
.unwrap();
let mut acc = vec![];
line_receiver.close();
while let Some(line) = line_receiver.recv().await {
acc.push(line);
}
assert!(matches!(
acc.first().unwrap(),
LineStateControlSignal::Pause
));
assert!(matches!(
acc.last().unwrap(),
LineStateControlSignal::Resume
));
}
}