factor out app state and make keybinds remappable via hashmap.

This commit is contained in:
mxhagen 2024-09-30 17:46:45 +02:00
parent f35d1f353e
commit f48eb23f0d
3 changed files with 203 additions and 52 deletions

63
src/app.rs Normal file
View File

@ -0,0 +1,63 @@
use crate::*;
use std::io::Stdout;
#[derive()]
pub struct App {
pub ui: Ui<Stdout>,
pub md_file: String,
pub running: bool,
pub keymap: Keymap,
pub mode: Mode,
}
impl App {
pub fn init() -> anyhow::Result<Self> {
let cli = cli::new();
let args = cli.clone().get_matches();
let md_file = match args.get_one::<String>("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;
}
}

114
src/control.rs Normal file
View File

@ -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<for<'a> 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
}
}

View File

@ -1,4 +1,4 @@
use std::{ pub use std::{
fs, fs,
io::{self, Write}, io::{self, Write},
path, process, time, path, process, time,
@ -6,6 +6,12 @@ use std::{
use crossterm::event::{poll, read, Event::*, KeyCode::Char}; use crossterm::event::{poll, read, Event::*, KeyCode::Char};
mod app;
use app::*;
mod control;
use control::*;
mod md; mod md;
use md::*; use md::*;
@ -20,74 +26,42 @@ pub use log::*;
mod tests; mod tests;
fn main() -> anyhow::Result<()> { fn main() -> anyhow::Result<()> {
let command = cli::new(); let mut app = App::init()?;
let args = command.clone().get_matches();
let md_file = match args.get_one::<String>("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 { let _guard = DropGuard {
// clean up terminal state even on panics
exec_on_drop: || { exec_on_drop: || {
{ // needs two braces to function properly
let _ = Ui::init(&mut io::stdout(), Document::default()).deinit(); let _ = Ui::init(&mut io::stdout(), Document::default()).deinit();
} Log::flush();
.into()
}, },
}; };
loop { while app.running {
ui.draw()?; app.ui.draw()?;
if poll(time::Duration::from_millis(250)).unwrap_or(false) { app.handle_input()?;
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; },
_ => {}
}
}
}
} }
if ui.save_on_quit { if app.ui.save_on_quit {
Log::info(format!( 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() { 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()); process::exit(ErrorCode::IO.into());
} }
write!(file.unwrap(), "{}", ui.document.to_md()).unwrap_or_else(|e| { write!(file.unwrap(), "{}", app.ui.document.to_md()).unwrap_or_else(|e| {
Log::error(format!("Failed to write to file: `{md_file}`: {e}")); Log::error(format!("Failed to write to file: `{}`: {}", app.md_file, e));
process::exit(ErrorCode::IO.into()); process::exit(ErrorCode::IO.into());
}); });
} }
ui.deinit()?; app.ui.deinit()?;
Log::flush(); Log::flush();
Ok(()) Ok(())
} }