use std::time::Duration;
use anyhow::Result;
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseEventKind};
use ratatui::DefaultTerminal;
use crate::{
attached::{Attached, Polled},
config::Config,
console::ConsoleEvent,
control::Control,
draw::draw,
ui::Ui,
};
pub fn run(terminal: &mut DefaultTerminal, attached: &mut dyn Attached, control: &Control) -> Result<()> {
let mut config = Config::load();
let mut ui = Ui {
history: config.history.clone(),
..Default::default()
};
ui.push_rx(attached.backlog().as_bytes());
let title = attached.title();
let mut dirty = true;
loop {
if control.stopping() {
return Ok(());
}
loop {
match attached.poll() {
Polled::Event(ConsoleEvent::Rx(bytes)) => ui.push_rx(&bytes),
Polled::Event(ConsoleEvent::Echo { origin, text }) => ui.push_echo(origin, &text),
Polled::Event(ConsoleEvent::System(text)) => ui.push_system(&text),
Polled::Event(ConsoleEvent::Connected) => {}
Polled::Idle => break,
Polled::Closed => return Ok(()),
}
dirty = true;
}
if dirty {
draw(terminal, &mut ui, &title, attached.connected())?;
dirty = false;
}
if !event::poll(POLL)? {
continue;
}
let key = match event::read()? {
Event::Key(key) if key.kind == KeyEventKind::Press => key,
Event::Mouse(mouse) => {
match mouse.kind {
MouseEventKind::ScrollUp => ui.scroll_up(WHEEL_LINES),
MouseEventKind::ScrollDown => ui.scroll_down(WHEEL_LINES),
_ => continue,
}
dirty = true;
continue;
}
Event::Resize(_, _) => {
dirty = true;
continue;
}
_ => continue,
};
if is_quit(&key) {
return Ok(());
}
dirty = true;
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
match key.code {
KeyCode::Char(c) if ctrl => {
if let Err(e) = attached.send_ctrl(c) {
ui.push_system(&e);
}
}
KeyCode::Char(c) => ui.insert(c),
KeyCode::Tab => ui.accept_suggestion(),
KeyCode::Enter => {
let text = ui.take_input();
if !text.is_empty() {
config.record_command(&text);
if let Err(e) = config.save() {
ui.push_system(&format!("config save failed: {e}"));
}
ui.history = config.history.clone();
}
if let Err(e) = attached.send_line(&text) {
ui.push_system(&e);
}
}
KeyCode::Backspace => ui.backspace(),
KeyCode::Delete => ui.delete(),
KeyCode::Left => ui.cursor = ui.cursor.saturating_sub(1),
KeyCode::Right if ui.cursor == ui.input.len() && ui.suggestion.is_some() => {
ui.accept_suggestion()
}
KeyCode::Right => ui.cursor = (ui.cursor + 1).min(ui.input.len()),
KeyCode::Home => ui.cursor = 0,
KeyCode::End => ui.cursor = ui.input.len(),
KeyCode::Up => ui.history_prev(),
KeyCode::Down => ui.history_next(),
KeyCode::Esc => ui.clear_input(),
_ => {}
}
}
}
const POLL: Duration = Duration::from_millis(16);
const WHEEL_LINES: usize = 3;
fn is_quit(key: &KeyEvent) -> bool {
key.modifiers.contains(KeyModifiers::CONTROL)
&& matches!(key.code, KeyCode::Char('q') | KeyCode::Char('Q'))
}
#[cfg(test)]
mod tests {
use crossterm::event::KeyEvent;
use super::*;
fn key(code: KeyCode, mods: KeyModifiers) -> KeyEvent {
KeyEvent::new(code, mods)
}
#[test]
fn ctrl_q_quits() {
assert!(is_quit(&key(KeyCode::Char('q'), KeyModifiers::CONTROL)));
assert!(is_quit(&key(KeyCode::Char('Q'), KeyModifiers::CONTROL)));
}
#[test]
fn plain_q_does_not_quit() {
assert!(!is_quit(&key(KeyCode::Char('q'), KeyModifiers::NONE)));
}
#[test]
fn other_control_keys_pass_through_not_quit() {
assert!(!is_quit(&key(KeyCode::Char('c'), KeyModifiers::CONTROL)));
assert!(!is_quit(&key(KeyCode::Char(']'), KeyModifiers::CONTROL)));
}
}