use std::io::Stdout;
use std::time::Instant;
use anyhow::Result;
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use ratatui::Terminal;
use ratatui::backend::CrosstermBackend;
use super::{App, Overlay, PromptKind, Screen, TriageState, capture, open_in_editor};
type Term = Terminal<CrosstermBackend<Stdout>>;
pub(super) fn handle_key(terminal: &mut Term, app: &mut App, key: KeyEvent) -> Result<()> {
if key.kind == KeyEventKind::Release {
return Ok(());
}
if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
app.quit = true;
return Ok(());
}
match &app.overlay {
Overlay::None => {}
Overlay::Capture(_) => return capture::handle_key(terminal, app, key),
Overlay::Edit(_) => {
if super::edit::handle_key(app, key)? {
open_selected(terminal, app)?;
}
return Ok(());
}
_ => return handle_overlay(app, key),
}
if app.pane {
return pane_key(terminal, app, key);
}
match app.screen {
Screen::Triage => triage_key(app, key),
Screen::Review => review_key(terminal, app, key),
_ => list_key(terminal, app, key),
}
}
fn common_key(app: &mut App, key: KeyEvent) -> Result<bool> {
match key.code {
KeyCode::Char('j') | KeyCode::Down => app.move_selection(1),
KeyCode::Char('k') | KeyCode::Up => app.move_selection(-1),
KeyCode::PageDown => app.move_selection(10),
KeyCode::PageUp => app.move_selection(-10),
KeyCode::Home => app.selected = 0,
KeyCode::End => app.selected = app.row_count().saturating_sub(1),
KeyCode::Char('?') => app.overlay = Overlay::Help,
KeyCode::Char('c') => app.overlay = Overlay::Capture(capture::Draft::default()),
KeyCode::Char('g') => {
app.index = org_core::WorkspaceIndex::load(app.settings.clone())?;
app.reload_rows()?;
app.set_status("refreshed");
}
KeyCode::Tab => next_tab(app, 1)?,
KeyCode::BackTab => next_tab(app, -1)?,
KeyCode::Char('1') => app.switch_screen(Screen::Today)?,
KeyCode::Char('2') => app.switch_screen(Screen::Week)?,
KeyCode::Char('3') => app.switch_screen(Screen::Inbox)?,
KeyCode::Char('4') | KeyCode::Char('A') => {
app.review_diff = false;
app.switch_screen(Screen::Review)?;
}
_ => return Ok(false),
}
Ok(true)
}
fn next_tab(app: &mut App, delta: isize) -> Result<()> {
let tabs = [Screen::Today, Screen::Week, Screen::Inbox, Screen::Review];
let current = tabs.iter().position(|tab| *tab == app.screen).unwrap_or(0) as isize;
let next = (current + delta).rem_euclid(tabs.len() as isize) as usize;
app.switch_screen(tabs[next].clone())
}
fn is_editor_key(key: KeyEvent) -> bool {
key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('e')
}
fn list_key(terminal: &mut Term, app: &mut App, key: KeyEvent) -> Result<()> {
if is_editor_key(key) {
return open_selected(terminal, app);
}
if common_key(app, key)? {
return Ok(());
}
let bulk = !app.marks.is_empty();
match key.code {
KeyCode::Char('q') => app.quit = true,
KeyCode::Esc => {
if app.peek {
app.peek = false;
} else if bulk {
app.marks.clear();
} else if !app.filter.is_empty() {
app.filter.clear();
app.reload_rows()?;
} else if app.screen != Screen::Today {
let back = app.back.clone();
app.switch_screen(if back == app.screen {
Screen::Today
} else {
back
})?;
}
}
KeyCode::Enter | KeyCode::Char('e') => {
if let Some(form) = super::edit::EditForm::open(app) {
app.overlay = Overlay::Edit(Box::new(form));
}
}
KeyCode::Char(' ') => app.toggle_mark(),
KeyCode::Char('.') if bulk => app.schedule("today")?,
KeyCode::Char('m') if bulk => app.schedule("+1d")?,
KeyCode::Char('w') if bulk => app.schedule("+1w")?,
KeyCode::Char('s') if bulk => app.someday()?,
KeyCode::Char('x') => app.mark_done()?,
KeyCode::Char('t') => app.cycle_state()?,
KeyCode::Char('s') => prompt(app, PromptKind::Schedule),
KeyCode::Char('d') => prompt(app, PromptKind::Deadline),
KeyCode::Char(',') => prompt(app, PromptKind::Priority),
KeyCode::Char('+') => app.shift_priority(true)?,
KeyCode::Char('-') => app.shift_priority(false)?,
KeyCode::Char(':') => prompt(app, PromptKind::Tags),
KeyCode::Char('r') => app.open_refile(),
KeyCode::Char('u') => app.undo()?,
KeyCode::Char('y') => app.copy_link()?,
KeyCode::Char('p') => app.peek = !app.peek,
KeyCode::Char('T') => start_triage(app)?,
KeyCode::Char('a') => app.overlay = Overlay::Views { selected: 0 },
KeyCode::Char('/') => {
app.overlay = Overlay::Prompt {
kind: PromptKind::Filter,
input: app.filter.clone(),
}
}
KeyCode::Char('<') => {
let categories = app.categories();
let index = app
.context
.as_ref()
.and_then(|context| categories.iter().position(|category| category == context));
app.context = match index {
None => categories.first().cloned(),
Some(index) => categories.get(index + 1).cloned(),
};
app.reload_rows()?;
}
KeyCode::Char('v') => {
app.classic = !app.classic;
app.reload_rows()?;
}
KeyCode::Char('l') => {
app.log_mode = !app.log_mode;
app.reload_rows()?;
}
KeyCode::Char('f') => {
app.day += period(app);
app.reload_rows()?;
}
KeyCode::Char('b') => {
app.day -= period(app);
app.reload_rows()?;
}
KeyCode::Char('.') => {
app.day = app.today;
app.reload_rows()?;
}
_ => {}
}
Ok(())
}
fn period(app: &App) -> chrono::Duration {
if app.screen == Screen::Week {
chrono::Duration::weeks(1)
} else {
chrono::Duration::days(1)
}
}
fn prompt(app: &mut App, kind: PromptKind) {
if app.selected_row().is_some() || !app.marks.is_empty() {
app.overlay = Overlay::Prompt {
kind,
input: String::new(),
};
}
}
fn start_triage(app: &mut App) -> Result<()> {
app.triage = TriageState {
started: Some(Instant::now()),
..TriageState::default()
};
app.switch_screen(Screen::Triage)
}
fn open_selected(terminal: &mut Term, app: &mut App) -> Result<()> {
let Some(row) = app.selected_row() else {
return Ok(());
};
let (file, line) = (row.item.file.clone(), row.item.line);
open_in_editor(terminal, app, &file, line)
}
fn triage_key(app: &mut App, key: KeyEvent) -> Result<()> {
if app.rows.is_empty() {
return match key.code {
KeyCode::Char('u') => app.undo(),
KeyCode::Enter | KeyCode::Esc | KeyCode::Char('q') => app.switch_screen(Screen::Today),
_ => Ok(()),
};
}
match key.code {
KeyCode::Esc | KeyCode::Char('q') => {
let back = app.back.clone();
app.switch_screen(back)?;
}
KeyCode::Char('.') => app.schedule("today")?,
KeyCode::Char('m') => app.schedule("+1d")?,
KeyCode::Char('w') => app.schedule("+1w")?,
KeyCode::Char('p') => prompt(app, PromptKind::Schedule),
KeyCode::Char('x') => app.mark_done()?,
KeyCode::Char('n') => app.set_state("NEXT")?,
KeyCode::Char('s') => app.someday()?,
KeyCode::Char('k') => app.cancel()?,
KeyCode::Char('r') => app.open_refile(),
KeyCode::Char('u') => app.undo()?,
KeyCode::Char('j') | KeyCode::Down | KeyCode::Tab => app.move_selection(1),
KeyCode::Up | KeyCode::BackTab => app.move_selection(-1),
KeyCode::Char('?') => app.overlay = Overlay::Help,
_ => {}
}
Ok(())
}
fn review_key(terminal: &mut Term, app: &mut App, key: KeyEvent) -> Result<()> {
let key = if is_editor_key(key) {
KeyEvent::from(KeyCode::Enter)
} else {
key
};
if common_key(app, key)? {
return Ok(());
}
match key.code {
KeyCode::Esc | KeyCode::Char('q') => {
if app.review_diff {
app.review_diff = false;
} else {
let back = app.back.clone();
app.switch_screen(if back == Screen::Review {
Screen::Today
} else {
back
})?;
}
}
KeyCode::Char('a') => app.accept_selected()?,
KeyCode::Char('A') => app.accept_all()?,
KeyCode::Char('u') => app.undo_selected_change()?,
KeyCode::Char('d') => app.review_diff = !app.review_diff,
KeyCode::Char('f') => app.cycle_agent_filter()?,
KeyCode::Enter => {
if let Some(record) = app.journals.get(app.selected) {
let file = record
.file
.split(',')
.next()
.unwrap_or_default()
.to_string();
let line = super::review::changed_line(record);
open_in_editor(terminal, app, &file, line)?;
}
}
_ => {}
}
Ok(())
}
fn pane_key(terminal: &mut Term, app: &mut App, key: KeyEvent) -> Result<()> {
if is_editor_key(key) {
return open_selected(terminal, app);
}
match key.code {
KeyCode::Char('j') | KeyCode::Down => app.move_selection(1),
KeyCode::Char('k') | KeyCode::Up => app.move_selection(-1),
KeyCode::Char('x') => app.mark_done()?,
KeyCode::Char('u') => app.undo()?,
KeyCode::Char('c') => app.overlay = Overlay::Capture(capture::Draft::default()),
KeyCode::Char('T') => {
app.pane = false;
start_triage(app)?;
}
KeyCode::Enter => {
app.pane = false;
app.reload_rows()?;
}
KeyCode::Char('q') => app.quit = true,
_ => {}
}
Ok(())
}
fn handle_overlay(app: &mut App, key: KeyEvent) -> Result<()> {
match &mut app.overlay {
Overlay::Help => app.overlay = Overlay::None,
Overlay::Prompt { input, .. } => match key.code {
KeyCode::Esc => app.overlay = Overlay::None,
KeyCode::Backspace => {
input.pop();
}
KeyCode::Enter => app.commit_prompt()?,
KeyCode::Char(character) => input.push(character),
_ => {}
},
Overlay::Refile(picker) => match key.code {
KeyCode::Esc => app.overlay = Overlay::None,
KeyCode::Enter => app.commit_refile()?,
KeyCode::Up => picker.selected = picker.selected.saturating_sub(1),
KeyCode::Down => {
let count = picker.matches().len();
picker.selected = (picker.selected + 1).min(count.saturating_sub(1));
}
KeyCode::Backspace => {
picker.input.pop();
picker.selected = 0;
}
KeyCode::Char(character) => {
picker.input.push(character);
picker.selected = 0;
}
_ => {}
},
Overlay::Views { selected } => {
let keys: Vec<String> = app
.settings
.views
.iter()
.map(|view| view.key.clone())
.collect();
match key.code {
KeyCode::Esc => app.overlay = Overlay::None,
KeyCode::Up | KeyCode::Char('k') => *selected = selected.saturating_sub(1),
KeyCode::Down | KeyCode::Char('j') => {
*selected = (*selected + 1).min(keys.len().saturating_sub(1));
}
KeyCode::Enter => {
if let Some(key) = keys.get(*selected).cloned() {
app.overlay = Overlay::None;
app.switch_screen(Screen::View(key))?;
}
}
KeyCode::Char(character) => {
if let Some(key) = keys.iter().find(|key| **key == character.to_string()) {
let key = key.clone();
app.overlay = Overlay::None;
app.switch_screen(Screen::View(key))?;
}
}
_ => {}
}
}
Overlay::None | Overlay::Capture(_) | Overlay::Edit(_) => {}
}
Ok(())
}