use anyhow::Result;
use crate::keymap::Action;
use crate::tui::app::App;
use crate::tui::interaction::Interaction;
use crate::tui::views::calc::Mode;
impl App {
pub(super) fn enter_history(&mut self) {
let total = self.service.history().len();
if total == 0 {
return;
}
self.mode = Mode::History;
self.selected = Some(total - 1);
}
pub(super) fn leave_history(&mut self) {
self.mode = Mode::Input;
self.selected = None;
}
pub(super) fn apply_history_action(
&mut self,
action: Action,
io: &mut dyn Interaction,
) -> Result<bool> {
let Some(index) = self.selected else {
self.mode = Mode::Input;
return Ok(false);
};
let total = self.service.history().len();
let last = total.saturating_sub(1);
let page = self.metrics.get().view_height.max(1);
match action {
Action::Up => self.selected = Some(index.saturating_sub(1)),
Action::Down => {
if index < last {
self.selected = Some(index + 1);
} else {
self.leave_history();
}
}
Action::Top => self.selected = Some(0),
Action::Bottom => self.selected = Some(last),
Action::PageUp => self.selected = Some(index.saturating_sub(page)),
Action::PageDown => self.selected = Some((index + page).min(last)),
Action::MoveUp => self.move_selected(index, -1),
Action::MoveDown => self.move_selected(index, 1),
Action::CopyPlain => self.copy_selected(index, false),
Action::CopyDisplay => self.copy_selected(index, true),
Action::EditEntry => self.start_edit(index),
Action::InsertBelow => self.insert_line(index + 1),
Action::InsertAbove => self.insert_line(index),
Action::DeleteEntry => self.delete_entry(index, io)?,
Action::ClearHistory => self.clear_history(io)?,
Action::Back => self.leave_history(),
_ => return Ok(false),
}
Ok(true)
}
fn move_selected(&mut self, index: usize, delta: isize) {
self.selected = Some(self.service.move_entry(index, delta));
}
fn insert_line(&mut self, at: usize) {
self.service.insert_entry(at);
self.selected = Some(at);
self.start_edit(at);
self.editing_inserted = true;
}
fn start_edit(&mut self, index: usize) {
let Some(entry) = self.service.history().entries().get(index) else {
return;
};
self.input = entry.input.clone();
self.cursor =
crate::tui::text_edit::TextCursor::at(self.input.chars().count());
self.mode = Mode::Edit(index);
self.editing_inserted = false;
}
fn delete_entry(
&mut self,
index: usize,
io: &mut dyn Interaction,
) -> Result<()> {
if !self.confirm(io, " Delete this line? ")? {
return Ok(());
}
self.service.delete_entry(index);
let total = self.service.history().len();
if total == 0 {
self.leave_history();
} else {
self.selected = Some(index.min(total - 1));
}
self.report("line deleted".to_string());
Ok(())
}
pub(super) fn clear_history(
&mut self,
io: &mut dyn Interaction,
) -> Result<()> {
if self.service.history().is_empty() {
self.report("history is already empty".to_string());
return Ok(());
}
if !self.confirm(io, " Clear the entire history? ")? {
return Ok(());
}
self.service.clear_history();
self.leave_history();
self.report("history cleared".to_string());
Ok(())
}
fn copy_selected(&mut self, index: usize, as_displayed: bool) {
let value = self
.service
.history()
.entries()
.get(index)
.and_then(|entry| entry.value.clone());
match value {
Some(value) if as_displayed => self.copy_display(&value),
Some(value) => self.copy_plain(&value),
None => self.report("no value to copy".to_string()),
}
}
}
#[cfg(test)]
mod tests {
use crossterm::event::{KeyCode, KeyModifiers};
use super::*;
use crate::config::Config;
use crate::tui::app::testing::{
app_with, chord, key, press, render_to_string, submit, test_app,
value_at,
};
use crate::tui::interaction::Headless;
use crate::tui::views::ViewId;
#[test]
fn up_enters_history_and_an_edit_recomputes_the_chain() {
let mut app = test_app();
submit(&mut app, "10");
submit(&mut app, "ans+5");
render_to_string(&app, 80, 24);
press(&mut app, key(KeyCode::Up));
assert_eq!(app.mode, Mode::History);
press(&mut app, key(KeyCode::Home));
press(&mut app, key(KeyCode::Enter));
assert_eq!(app.mode, Mode::Edit(0));
for _ in 0..2 {
press(&mut app, key(KeyCode::Backspace));
}
for character in "20".chars() {
press(&mut app, key(KeyCode::Char(character)));
}
press(&mut app, key(KeyCode::Enter));
assert_eq!(value_at(&app, 1), Some(25.0));
}
#[test]
fn history_reorder_delete_and_clear_via_keys() {
let mut app = test_app();
submit(&mut app, "10");
submit(&mut app, "ans+5");
submit(&mut app, "ans*2");
render_to_string(&app, 80, 24);
press(&mut app, key(KeyCode::Up));
assert_eq!(app.selected, Some(2));
press(&mut app, chord(KeyCode::Up, KeyModifiers::ALT));
assert_eq!(app.selected, Some(1));
assert_eq!(value_at(&app, 1), Some(20.0));
assert_eq!(value_at(&app, 2), Some(25.0));
press(&mut app, key(KeyCode::Char('d')));
assert_eq!(app.service().history().len(), 2);
press(&mut app, key(KeyCode::Char('D')));
assert_eq!(app.service().history().len(), 0);
}
#[test]
fn a_declined_confirmation_keeps_the_entry() {
let mut app = test_app();
submit(&mut app, "10");
render_to_string(&app, 80, 24);
press(&mut app, key(KeyCode::Up));
let mut declining = Headless::declining();
app.dispatch(key(KeyCode::Char('d')), &mut declining)
.unwrap();
assert_eq!(declining.asked, 1);
assert_eq!(app.service().history().len(), 1);
}
#[test]
fn confirm_delete_off_deletes_without_asking() {
let config = Config {
confirm_delete: false,
..Config::default()
};
let mut app = app_with(&config);
submit(&mut app, "10");
render_to_string(&app, 80, 24);
press(&mut app, key(KeyCode::Up));
let mut declining = Headless::declining();
app.dispatch(key(KeyCode::Char('d')), &mut declining)
.unwrap();
assert_eq!(declining.asked, 0, "no dialog when confirm_delete is off");
assert_eq!(app.service().history().len(), 0);
}
#[test]
fn inserting_a_line_enters_edit_and_recomputes() {
let mut app = test_app();
submit(&mut app, "10");
submit(&mut app, "ans+5");
render_to_string(&app, 80, 24);
press(&mut app, key(KeyCode::Up));
press(&mut app, key(KeyCode::Home));
press(&mut app, key(KeyCode::Char('o')));
assert_eq!(app.mode, Mode::Edit(1));
for character in "ans*3".chars() {
press(&mut app, key(KeyCode::Char(character)));
}
press(&mut app, key(KeyCode::Enter));
assert_eq!(value_at(&app, 1), Some(30.0));
assert_eq!(value_at(&app, 2), Some(35.0));
}
#[test]
fn cancelling_an_inserted_line_removes_it() {
let mut app = test_app();
submit(&mut app, "10");
render_to_string(&app, 80, 24);
press(&mut app, key(KeyCode::Up));
press(&mut app, key(KeyCode::Char('o')));
assert_eq!(app.service().history().len(), 2);
press(&mut app, key(KeyCode::Esc));
assert_eq!(app.service().history().len(), 1);
assert_eq!(app.mode, Mode::History);
}
#[test]
fn an_in_place_edit_locks_the_tab_bar() {
let mut app = test_app();
submit(&mut app, "10");
render_to_string(&app, 80, 24);
press(&mut app, key(KeyCode::Up));
press(&mut app, key(KeyCode::Enter));
assert_eq!(app.mode, Mode::Edit(0));
press(&mut app, chord(KeyCode::Char('2'), KeyModifiers::ALT));
assert_eq!(app.active, ViewId::Calc, "the tab switch is locked");
assert_eq!(app.status.as_deref(), Some("finish the edit first"));
}
}