use std::{
num::{NonZero, NonZeroU16},
time::Duration,
};
use ratatui::{
crossterm::event::{self, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseEventKind},
layout::Size,
};
use crate::{
Error,
cursor::{Cursor, CursorPointer},
document::{LineExtra, SectionContent},
model::{CursorPositioning, InputQueue, Model},
};
pub enum PollResult {
None,
HadInput,
SkipRender,
Quit,
}
pub fn poll(had_events: bool, model: &mut Model) -> Result<PollResult, Error> {
if event::poll(if had_events {
Duration::ZERO
} else {
Duration::from_millis(100)
})? {
match event::read()? {
event::Event::Key(key) => {
if key.kind == KeyEventKind::Press {
return match_keycode(key, model);
}
}
event::Event::Resize(new_width, new_height) => {
log::debug!("Resize {new_width},{new_height}");
if model.screen_size.width != new_width || model.screen_size.height != new_height {
let screen_size = Size::new(new_width, new_height);
model.reload(screen_size)?;
return Ok(PollResult::SkipRender);
}
return Ok(PollResult::None);
}
event::Event::Mouse(mouse) => match mouse.kind {
MouseEventKind::ScrollUp => {
model.scroll_by(-2);
return Ok(PollResult::HadInput);
}
MouseEventKind::ScrollDown => {
model.scroll_by(2);
return Ok(PollResult::HadInput);
}
_ => {}
},
_ => {}
}
}
Ok(PollResult::None)
}
fn match_keycode(key: KeyEvent, model: &mut Model) -> Result<PollResult, Error> {
let page_scroll_count = model.inner_height() as i32 - 1;
match key.code {
KeyCode::Char(c) if matches!(model.input_queue, InputQueue::Search(_)) => {
let InputQueue::Search(needle) = &mut model.input_queue else {
panic!("invariant InputQueue::Search");
};
needle.push(c);
let clone = needle.clone();
model.add_searches(Some(&clone));
model.cursor = Cursor::Search(clone, None);
}
KeyCode::Char(c) if matches!(model.input_queue, InputQueue::Command(_)) => {
let InputQueue::Command(command) = &mut model.input_queue else {
panic!("invariant InputQueue::Command");
};
command.push(c);
}
KeyCode::Char(x)
if x.is_ascii_digit() && (model.input_queue != InputQueue::None || x != '0') =>
{
let x = x as u16 - '0' as u16;
match &mut model.input_queue {
InputQueue::None => {
model.input_queue =
InputQueue::MovementCount(NonZero::new(x).expect("is_ascii_digit"));
}
InputQueue::MovementCount(count) => {
*count = count
.saturating_mul(NonZero::new(10).expect("10 != 0"))
.saturating_add(x);
}
InputQueue::CursorPositioningCommands => {
model.input_queue = InputQueue::None;
}
InputQueue::Search(_) | InputQueue::Command(_) => {
panic!("invariant is_ascii_digit while in invalid InputQueue mode");
}
}
}
KeyCode::Char(x)
if x == 'z'
&& model.input_queue == InputQueue::None
&& model.cursor != Cursor::None =>
{
model.input_queue = InputQueue::CursorPositioningCommands;
}
KeyCode::Char(x)
if (x == 'z' || x == 't' || x == 'b')
&& model.input_queue == InputQueue::CursorPositioningCommands =>
{
model.position_cursor(CursorPositioning::from(x));
model.input_queue = InputQueue::None;
}
KeyCode::Char('q') => {
return Ok(PollResult::Quit);
}
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
return Ok(PollResult::Quit);
}
KeyCode::Char('j') | KeyCode::Down => {
let count = model.input_queue.take_count_or_unit_i32();
if !model.scroll_by(count) {
return Ok(PollResult::None);
}
}
KeyCode::Char('k') | KeyCode::Up => {
let count = model.input_queue.take_count_or_unit_i32();
if !model.scroll_by(-count) {
return Ok(PollResult::None);
}
}
KeyCode::Char('d') => {
let count = model.input_queue.take_count_or_unit_i32();
if !model.scroll_by(((page_scroll_count + 1) / 2).saturating_mul(count)) {
return Ok(PollResult::None);
}
}
KeyCode::Char('u') => {
let count = model.input_queue.take_count_or_unit_i32();
if !model.scroll_by((-(page_scroll_count + 1) / 2).saturating_mul(count)) {
return Ok(PollResult::None);
}
}
KeyCode::Char('f' | ' ') | KeyCode::PageDown => {
let count = model.input_queue.take_count_or_unit_i32();
if !model.scroll_by(page_scroll_count.saturating_mul(count)) {
return Ok(PollResult::None);
}
}
KeyCode::Char('b') | KeyCode::PageUp => {
let count = model.input_queue.take_count_or_unit_i32();
if !model.scroll_by((-page_scroll_count).saturating_mul(count)) {
return Ok(PollResult::None);
}
}
KeyCode::Char('g') => {
let scroll = if let InputQueue::MovementCount(count) = model.input_queue {
model.input_queue = InputQueue::None;
count.get()
} else {
0
};
if scroll == model.scroll {
return Ok(PollResult::None);
}
model.scroll = scroll;
}
KeyCode::Char('G') => {
let scroll = if let InputQueue::MovementCount(count) = model.input_queue {
model.input_queue = InputQueue::None;
count.get()
} else {
model.total_lines().saturating_sub(
page_scroll_count as u16 + 1, )
};
if scroll == model.scroll {
return Ok(PollResult::None);
}
model.scroll = scroll;
}
KeyCode::Char('n') => {
let count = model.input_queue.take_count_or_unit_u16();
model.cursor_next(count);
}
KeyCode::Char('N') => {
let count = model.input_queue.take_count_or_unit_u16();
model.cursor_prev(count);
}
KeyCode::Char('r') => {
model.reload(model.screen_size)?;
model.input_queue = InputQueue::None;
return Ok(PollResult::SkipRender);
}
KeyCode::Char('/') => {
model.input_queue = InputQueue::Search(String::new());
model.cursor = Cursor::Search(String::new(), None);
}
KeyCode::Char(':') => {
model.input_queue = InputQueue::Command(String::new());
}
KeyCode::Enter if matches!(model.input_queue, InputQueue::Search(_)) => {
model.input_queue = InputQueue::None;
model.cursor_next(1);
}
KeyCode::Enter if matches!(model.input_queue, InputQueue::Command(_)) => {
let InputQueue::Command(command) =
std::mem::replace(&mut model.input_queue, InputQueue::None)
else {
panic!("invariant InputQueue::Command");
};
if let Err(err) = model.user_command_str(command) {
log::error!("{err}");
}
}
KeyCode::Enter => {
if let Cursor::Links(CursorPointer { id, index }) = model.cursor {
let url = model.sections().find_map(|section| {
if section.id == id {
let SectionContent::Lines(lines) = §ion.content else {
return None;
};
let mut remaining = index;
for (_, extras) in lines {
if remaining < extras.len() {
return match &extras[remaining] {
LineExtra::Link { source: url, .. } => Some(url.clone()),
_ => None,
};
}
remaining -= extras.len();
}
None
} else {
None
}
});
if let Some(url) = url {
log::debug!("open link_cursor {}", *url);
if let Err(err) = model.open_link(url.to_string()) {
log::error!("open_link failed: {err}");
}
}
}
}
KeyCode::Esc if model.is_help_screen()? => {
model.history_pop()?;
}
KeyCode::Esc => match model.input_queue {
InputQueue::None => {
match &model.cursor {
Cursor::Search(_, _) | Cursor::Links(_) => {
model.cursor = Cursor::None;
}
_ => {}
}
}
InputQueue::Search(_) => {
model.input_queue = InputQueue::None;
model.cursor = Cursor::None;
}
InputQueue::MovementCount(_)
| InputQueue::CursorPositioningCommands
| InputQueue::Command(_) => {
model.input_queue = InputQueue::None;
}
},
KeyCode::Backspace => match &mut model.input_queue {
InputQueue::None | InputQueue::CursorPositioningCommands => {}
InputQueue::MovementCount(count) => {
let value = count.get();
if value > 10 {
*count = NonZeroU16::new(count.get() / 10).expect("checked >10");
}
}
InputQueue::Search(needle) => {
if needle.is_empty() {
model.input_queue = InputQueue::None;
model.cursor = Cursor::None;
} else {
needle.pop();
let clone = needle.clone();
model.add_searches(Some(&clone));
}
}
InputQueue::Command(command) => {
if command.is_empty() {
model.input_queue = InputQueue::None;
} else {
command.pop();
}
}
},
_ => {
return Ok(PollResult::None);
}
}
Ok(PollResult::HadInput)
}