From f48eb23f0d67ea9ec493edc8f5596c23bac8d712 Mon Sep 17 00:00:00 2001 From: mxhagen Date: Mon, 30 Sep 2024 17:46:45 +0200 Subject: [PATCH] factor out app state and make keybinds remappable via hashmap. --- src/app.rs | 63 +++++++++++++++++++++++++++ src/control.rs | 114 +++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 78 +++++++++++---------------------- 3 files changed, 203 insertions(+), 52 deletions(-) create mode 100644 src/app.rs create mode 100644 src/control.rs diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..39ad5ca --- /dev/null +++ b/src/app.rs @@ -0,0 +1,63 @@ +use crate::*; + +use std::io::Stdout; + +#[derive()] +pub struct App { + pub ui: Ui, + pub md_file: String, + pub running: bool, + pub keymap: Keymap, + pub mode: Mode, +} + +impl App { + pub fn init() -> anyhow::Result { + let cli = cli::new(); + let args = cli.clone().get_matches(); + + let md_file = match args.get_one::("file") { + Some(path) => path.into(), + _ => { + Log::warn("No filename specified -- falling back to `todo.md`"); + "todo.md".to_string() + } + }; + + if !path::Path::new(&md_file).exists() { + Log::error_exit_with( + ErrorCode::IO, + format!("Markdown file `{md_file}` not found. Exiting..."), + ); + } + + let md = fs::read_to_string(&md_file).unwrap(); + let document = Document::from_md(md)?; + + let ui = Ui::init(io::stdout(), document); + + Ok(Self { + ui, + md_file, + running: true, + keymap: Keymap::default(), + mode: Mode::Normal, + }) + } + + pub fn handle_input(&mut self) -> anyhow::Result<()> { + if poll(time::Duration::from_millis(250)).unwrap_or(false) { + if let Ok(Key(k)) = read() { + let a = self.keymap.map.get(&(self.mode.clone(), k)); + if let Some(callback) = a { + (callback.clone())(self); + } + } + } + Ok(()) + } + + pub fn quit(&mut self) { + self.running = false; + } +} diff --git a/src/control.rs b/src/control.rs new file mode 100644 index 0000000..2de173b --- /dev/null +++ b/src/control.rs @@ -0,0 +1,114 @@ +#![allow(dead_code, unused)] // todo +use crate::*; + +use std::collections::HashMap; + +use anyhow::*; +use crossterm::event::KeyEvent; + +/// Mode of the TUI +#[derive(Default, Debug, PartialEq, Eq, Hash, Clone)] +pub enum Mode { + #[default] + Normal, + Insert(EditMode), + Datetime, + Visual, +} + +/// Mode for the line editor +#[derive(Default, Debug, PartialEq, Eq, Hash, Clone)] +pub enum EditMode { + #[default] + Normal, + Insert, + Visual, + Replace, +} + +type KeymapCallback = Box fn(&'a mut App)>; + +#[derive(Debug)] +pub struct Keymap { + pub map: HashMap<(Mode, KeyEvent), KeymapCallback>, +} + +impl Keymap { + pub fn handle(&self, key: KeyEvent, app: &mut App) -> anyhow::Result<()> { + self.map + .get(&(app.mode.clone(), key)) + .map(|f| f(app)) + .ok_or(anyhow!("Associated mapping not found.")) + } + + pub fn register(&mut self, mode: Mode, key: KeyEvent, f: KeymapCallback) { + self.map.insert((mode, key), f); + } +} + +impl Default for Keymap { + fn default() -> Self { + let mut map = Self { + map: HashMap::new(), + }; + use Mode::*; + map.register(Normal, Char('q').into(), Box::new(App::quit)); + map.register( + Normal, + Char('Q').into(), + Box::new(|app: &mut App| { + app.ui.dont_save_on_quit(); + app.quit() + }), + ); + map.register( + Normal, + Char('j').into(), + Box::new(|app: &mut App| app.ui.move_selection(Down).unwrap()), + ); + map.register( + Normal, + Char('k').into(), + Box::new(|app: &mut App| app.ui.move_selection(Up).unwrap()), + ); + map.register( + Normal, + Char('J').into(), + Box::new(|app: &mut App| app.ui.move_selected_entry(Down)), + ); + map.register( + Normal, + Char('K').into(), + Box::new(|app: &mut App| app.ui.move_selected_entry(Up)), + ); + map.register( + Normal, + Char('G').into(), + Box::new(|app: &mut App| app.ui.move_selection_to_top()), + ); + map.register( + Normal, + Char('g').into(), + Box::new(|app: &mut App| app.ui.move_selection_to_bottom()), + ); + map.register( + Normal, + Char(' ').into(), + Box::new(|app: &mut App| app.ui.toggle_active_entry()), + ); + map.register( + Normal, + Char('s').into(), + Box::new(|app: &mut App| app.ui.cycle_sort_mode()), + ); + map.register( + Normal, + Char('-').into(), + Box::new(|app: &mut App| { + Log::info("Debug panic keybind '-' invoked -- panicking..."); + panic!(); + }), + ); + map + } +} diff --git a/src/main.rs b/src/main.rs index 424a4ab..ad78c90 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use std::{ +pub use std::{ fs, io::{self, Write}, path, process, time, @@ -6,6 +6,12 @@ use std::{ use crossterm::event::{poll, read, Event::*, KeyCode::Char}; +mod app; +use app::*; + +mod control; +use control::*; + mod md; use md::*; @@ -20,74 +26,42 @@ pub use log::*; mod tests; fn main() -> anyhow::Result<()> { - let command = cli::new(); - let args = command.clone().get_matches(); + let mut app = App::init()?; - let md_file = match args.get_one::("file") { - Some(path) => path, - _ => { - Log::warn("No filename specified -- falling back to `todo.md`"); - &"todo.md".to_string() - } - }; - - if !path::Path::new(md_file).exists() { - Log::error_exit_with( - ErrorCode::IO, - format!("Markdown file `{md_file}` not found. Exiting..."), - ); - } - - let md = fs::read_to_string(md_file).unwrap(); - let document = Document::from_md(md)?; - - let mut ui = Ui::init(io::stdout(), document); let _guard = DropGuard { + // clean up terminal state even on panics exec_on_drop: || { - { - let _ = Ui::init(&mut io::stdout(), Document::default()).deinit(); - } - .into() + // needs two braces to function properly + let _ = Ui::init(&mut io::stdout(), Document::default()).deinit(); + Log::flush(); }, }; - loop { - ui.draw()?; - if poll(time::Duration::from_millis(250)).unwrap_or(false) { - if let Ok(Key(k)) = read() { - match k.code { - Char('q') => break, - Char('j') => ui.move_selection(Down)?, - Char('k') => ui.move_selection(Up)?, - Char('J') => ui.move_selected_entry(Down), - Char('K') => ui.move_selected_entry(Up), - Char('G') => ui.move_selection_to_bottom(), - Char('g') => ui.move_selection_to_top(), - Char(' ') => ui.toggle_active_entry(), - Char('s') => ui.cycle_sort_mode(), - Char('Q') => { ui.dont_save_on_quit(); break; }, - _ => {} - } - } - } + while app.running { + app.ui.draw()?; + app.handle_input()?; } - if ui.save_on_quit { + if app.ui.save_on_quit { Log::info(format!( - "Writing updated markdown to file `{md_file}` and exiting..." + "Writing updated markdown to file `{}` and exiting...", + app.md_file )); - let file = fs::File::create(md_file); + let file = fs::File::create(&app.md_file); if file.is_err() { - Log::error(format!("Failed to open file for writing: `{md_file}`")); + Log::error(format!( + "Failed to open file for writing: `{}`", + app.md_file + )); process::exit(ErrorCode::IO.into()); } - write!(file.unwrap(), "{}", ui.document.to_md()).unwrap_or_else(|e| { - Log::error(format!("Failed to write to file: `{md_file}`: {e}")); + write!(file.unwrap(), "{}", app.ui.document.to_md()).unwrap_or_else(|e| { + Log::error(format!("Failed to write to file: `{}`: {}", app.md_file, e)); process::exit(ErrorCode::IO.into()); }); } - ui.deinit()?; + app.ui.deinit()?; Log::flush(); Ok(()) }