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
|
||||||
|
}
|
||||||
|
}
|
||||||
78
src/main.rs
78
src/main.rs
@ -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(())
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user