pub(crate) mod modal;
pub(crate) mod mouse;
pub(crate) mod orders;
pub(crate) mod positions;
pub(crate) mod search;
pub(crate) mod validation;
pub(crate) mod watchlist;
pub(crate) use modal::handle_modal_key;
pub(crate) use mouse::handle_mouse;
pub(crate) use orders::handle_orders_key;
pub(crate) use positions::handle_positions_key;
pub(crate) use search::handle_search_key;
pub(crate) use validation::validate;
pub(crate) use watchlist::handle_watchlist_key;
use std::time::{Duration, Instant};
use crossterm::event::KeyCode;
use ratatui::widgets::{ListState, TableState};
use tokio::sync::mpsc::error::TrySendError;
use crate::app::{App, StatusMessage};
use crate::commands::Command;
pub(crate) const GG_TIMEOUT: Duration = Duration::from_millis(500);
pub(crate) trait SelectionState {
fn selected(&self) -> Option<usize>;
fn select(&mut self, index: Option<usize>);
}
impl SelectionState for ListState {
fn selected(&self) -> Option<usize> {
ListState::selected(self)
}
fn select(&mut self, index: Option<usize>) {
ListState::select(self, index);
}
}
impl SelectionState for TableState {
fn selected(&self) -> Option<usize> {
TableState::selected(self)
}
fn select(&mut self, index: Option<usize>) {
TableState::select(self, index);
}
}
pub(crate) fn handle_nav_key(
key: KeyCode,
len: usize,
state: &mut impl SelectionState,
pending_g: &mut Option<Instant>,
) {
let was_pending = *pending_g;
*pending_g = None;
match key {
KeyCode::Char('j') | KeyCode::Down if len > 0 => {
let i = state.selected().unwrap_or(0);
state.select(Some((i + 1).min(len - 1)));
}
KeyCode::Char('k') | KeyCode::Up => {
let i = state.selected().unwrap_or(0);
state.select(Some(i.saturating_sub(1)));
}
KeyCode::Char('g') => {
if was_pending
.map(|t| t.elapsed() < GG_TIMEOUT)
.unwrap_or(false)
{
state.select(Some(0));
} else {
*pending_g = Some(Instant::now());
}
}
KeyCode::Char('G') if len > 0 => {
state.select(Some(len - 1));
}
_ => {}
}
}
pub(crate) fn send_command(app: &mut App, cmd: Command, success_msg: impl Into<String>) {
match app.command_tx.try_send(cmd) {
Ok(()) => app.push_transient_status(success_msg),
Err(TrySendError::Full(_)) => {
app.push_transient_status("System busy — please retry");
}
Err(TrySendError::Closed(_)) => {
tracing::error!("command channel closed; command handler has stopped");
app.push_status(StatusMessage::persistent(
"Command handler stopped — restart app",
));
}
}
}