factor out app state and make keybinds remappable via hashmap.
This commit is contained in:
parent
f35d1f353e
commit
f48eb23f0d
63
src/app.rs
Normal file
63
src/app.rs
Normal 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
114
src/control.rs
Normal 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
|
||||
}
|
||||
}
|
||||
76
src/main.rs
76
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::<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 {
|
||||
// clean up terminal state even on panics
|
||||
exec_on_drop: || {
|
||||
{
|
||||
// needs two braces to function properly
|
||||
let _ = Ui::init(&mut io::stdout(), Document::default()).deinit();
|
||||
}
|
||||
.into()
|
||||
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(())
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user