Merge pull request #124 from kdheepak/key-binding

This commit is contained in:
Dheepak Krishnamurthy 2021-02-28 23:10:58 -07:00 committed by GitHub
commit 59aaa82891
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 287 additions and 94 deletions

View file

@ -3,6 +3,7 @@ use crate::config;
use crate::config::Config; use crate::config::Config;
use crate::context::Context; use crate::context::Context;
use crate::help::Help; use crate::help::Help;
use crate::keyconfig::KeyConfig;
use crate::table::{Row, Table, TableState}; use crate::table::{Row, Table, TableState};
use crate::task_report::TaskReportTable; use crate::task_report::TaskReportTable;
use crate::util::Key; use crate::util::Key;
@ -157,11 +158,14 @@ pub struct TTApp {
pub help_popup: Help, pub help_popup: Help,
pub contexts: Vec<Context>, pub contexts: Vec<Context>,
pub last_export: Option<SystemTime>, pub last_export: Option<SystemTime>,
pub keyconfig: KeyConfig,
} }
impl TTApp { impl TTApp {
pub fn new() -> Result<Self, Box<dyn Error>> { pub fn new() -> Result<Self, Box<dyn Error>> {
let c = Config::default()?; let c = Config::default()?;
let mut kc = KeyConfig::default();
kc.update()?;
let mut app = Self { let mut app = Self {
should_quit: false, should_quit: false,
task_table_state: TableState::default(), task_table_state: TableState::default(),
@ -182,6 +186,7 @@ impl TTApp {
help_popup: Help::new(), help_popup: Help::new(),
contexts: vec![], contexts: vec![],
last_export: None, last_export: None,
keyconfig: kc,
}; };
for c in app.config.filter.chars() { for c in app.config.filter.chars() {
app.filter.insert(c, 1); app.filter.insert(c, 1);
@ -1204,7 +1209,7 @@ impl TTApp {
} }
} }
pub fn task_start_or_stop(&mut self) -> Result<(), String> { pub fn task_start_stop(&mut self) -> Result<(), String> {
if self.tasks.lock().unwrap().is_empty() { if self.tasks.lock().unwrap().is_empty() {
return Ok(()); return Ok(());
} }
@ -1410,7 +1415,6 @@ impl TTApp {
let reference = TimeZone::from_utc_datetime(now.offset(), d); let reference = TimeZone::from_utc_datetime(now.offset(), d);
let now = TimeZone::from_utc_datetime(now.offset(), &now.naive_utc()); let now = TimeZone::from_utc_datetime(now.offset(), &now.naive_utc());
let d = d.clone(); let d = d.clone();
dbg!(reference, now);
if (reference - chrono::Duration::nanoseconds(1)).month() == now.month() { if (reference - chrono::Duration::nanoseconds(1)).month() == now.month() {
add_tag(&mut task, "MONTH".to_string()); add_tag(&mut task, "MONTH".to_string());
} }
@ -1460,49 +1464,56 @@ impl TTApp {
events: &Events, events: &Events,
) -> Result<(), Box<dyn Error>> { ) -> Result<(), Box<dyn Error>> {
match self.mode { match self.mode {
AppMode::TaskReport => match input { AppMode::TaskReport => {
Key::Ctrl('c') | Key::Char('q') => self.should_quit = true, if input == self.keyconfig.quit || input == Key::Ctrl('c') {
Key::Char('r') => self.update(true)?, self.should_quit = true;
Key::End | Key::Char('G') => self.task_report_bottom(), } else if input == self.keyconfig.refresh {
Key::Home => self.task_report_top(), self.update(true)?;
Key::Char('g') => { } else if input == self.keyconfig.go_to_bottom || input == Key::End {
if let Event::Input(Key::Char('g')) = events.next()? { self.task_report_bottom();
self.task_report_top() } else if input == self.keyconfig.go_to_top || input == Key::Home {
self.task_report_top();
} else if input == Key::Down || input == self.keyconfig.down {
self.task_report_next();
} else if input == Key::Up || input == self.keyconfig.up {
self.task_report_previous();
} else if input == Key::PageDown || input == self.keyconfig.page_down {
self.task_report_next_page();
} else if input == Key::PageUp || input == self.keyconfig.page_up {
self.task_report_previous_page();
} else if input == self.keyconfig.done {
match self.task_done() {
Ok(_) => self.update(true)?,
Err(e) => {
self.mode = AppMode::TaskError;
self.error = e;
}
} }
} } else if input == self.keyconfig.delete {
Key::Down | Key::Char('j') => self.task_report_next(), match self.task_delete() {
Key::Up | Key::Char('k') => self.task_report_previous(), Ok(_) => self.update(true)?,
Key::PageDown | Key::Char('J') => self.task_report_next_page(), Err(e) => {
Key::PageUp | Key::Char('K') => self.task_report_previous_page(), self.mode = AppMode::TaskError;
Key::Char('d') => match self.task_done() { self.error = e;
Ok(_) => self.update(true)?, }
Err(e) => {
self.mode = AppMode::TaskError;
self.error = e;
} }
}, } else if input == self.keyconfig.start_stop {
Key::Char('x') => match self.task_delete() { match self.task_start_stop() {
Ok(_) => self.update(true)?, Ok(_) => self.update(true)?,
Err(e) => { Err(e) => {
self.mode = AppMode::TaskError; self.mode = AppMode::TaskError;
self.error = e; self.error = e;
}
} }
}, } else if input == self.keyconfig.undo {
Key::Char('s') => match self.task_start_or_stop() { match self.task_undo() {
Ok(_) => self.update(true)?, Ok(_) => self.update(true)?,
Err(e) => { Err(e) => {
self.mode = AppMode::TaskError; self.mode = AppMode::TaskError;
self.error = e; self.error = e;
}
} }
}, } else if input == self.keyconfig.edit {
Key::Char('u') => match self.task_undo() {
Ok(_) => self.update(true)?,
Err(e) => {
self.mode = AppMode::TaskError;
self.error = e;
}
},
Key::Char('e') => {
events.pause_key_capture(terminal); events.pause_key_capture(terminal);
let r = self.task_edit(); let r = self.task_edit();
events.resume_key_capture(terminal); events.resume_key_capture(terminal);
@ -1513,8 +1524,7 @@ impl TTApp {
self.error = e; self.error = e;
} }
} }
} } else if input == self.keyconfig.modify {
Key::Char('m') => {
self.mode = AppMode::TaskModify; self.mode = AppMode::TaskModify;
match self.task_current() { match self.task_current() {
Some(t) => { Some(t) => {
@ -1523,64 +1533,51 @@ impl TTApp {
} }
None => self.modify.update("", 0), None => self.modify.update("", 0),
} }
} } else if input == self.keyconfig.shell {
Key::Char('!') => {
self.mode = AppMode::TaskSubprocess; self.mode = AppMode::TaskSubprocess;
} } else if input == self.keyconfig.log {
Key::Char('l') => {
self.mode = AppMode::TaskLog; self.mode = AppMode::TaskLog;
} } else if input == self.keyconfig.add {
Key::Char('a') => {
self.mode = AppMode::TaskAdd; self.mode = AppMode::TaskAdd;
} } else if input == self.keyconfig.annotate {
Key::Char('A') => {
self.mode = AppMode::TaskAnnotate; self.mode = AppMode::TaskAnnotate;
} } else if input == self.keyconfig.help {
Key::Char('?') => {
self.mode = AppMode::TaskHelpPopup; self.mode = AppMode::TaskHelpPopup;
} } else if input == self.keyconfig.filter {
Key::Char('/') => {
self.mode = AppMode::TaskFilter; self.mode = AppMode::TaskFilter;
} } else if input == self.keyconfig.zoom {
Key::Char('z') => {
self.task_report_show_info = !self.task_report_show_info; self.task_report_show_info = !self.task_report_show_info;
} } else if input == self.keyconfig.context_menu {
Key::Char('c') => {
self.mode = AppMode::TaskContextMenu; self.mode = AppMode::TaskContextMenu;
} } else if input == self.keyconfig.next_tab {
Key::Char(']') => {
self.mode = AppMode::Calendar; self.mode = AppMode::Calendar;
} }
_ => {} }
}, AppMode::TaskContextMenu => {
AppMode::TaskContextMenu => match input { if input == self.keyconfig.quit || input == Key::Esc {
Key::Esc | Key::Char('q') => {
self.mode = AppMode::TaskReport; self.mode = AppMode::TaskReport;
} } else if input == Key::Down || input == self.keyconfig.down {
Key::Down | Key::Char('j') => self.context_next(), self.context_next();
Key::Up | Key::Char('k') => self.context_previous(), } else if input == Key::Up || input == self.keyconfig.up {
Key::Char('\n') => { self.context_previous();
} else if input == Key::Char('\n') {
self.context_select(); self.context_select();
self.get_context()?; self.get_context()?;
} }
_ => {} }
}, AppMode::TaskHelpPopup => {
AppMode::TaskHelpPopup => match input { if input == self.keyconfig.quit || input == Key::Esc {
Key::Esc | Key::Char('q') => {
self.mode = AppMode::TaskReport; self.mode = AppMode::TaskReport;
} } else if input == self.keyconfig.down {
Key::Char('j') => {
self.help_popup.scroll = self.help_popup.scroll.checked_add(1).unwrap_or(0); self.help_popup.scroll = self.help_popup.scroll.checked_add(1).unwrap_or(0);
let th = (self.help_popup.text_height as u16).saturating_sub(1); let th = (self.help_popup.text_height as u16).saturating_sub(1);
if self.help_popup.scroll > th { if self.help_popup.scroll > th {
self.help_popup.scroll = th self.help_popup.scroll = th
} }
} } else if input == self.keyconfig.up {
Key::Char('k') => {
self.help_popup.scroll = self.help_popup.scroll.saturating_sub(1); self.help_popup.scroll = self.help_popup.scroll.saturating_sub(1);
} }
_ => {} }
},
AppMode::TaskModify => match input { AppMode::TaskModify => match input {
Key::Char('\n') => match self.task_modify() { Key::Char('\n') => match self.task_modify() {
Ok(_) => { Ok(_) => {
@ -1674,25 +1671,25 @@ impl TTApp {
_ => handle_movement(&mut self.filter, input), _ => handle_movement(&mut self.filter, input),
}, },
AppMode::TaskError => self.mode = AppMode::TaskReport, AppMode::TaskError => self.mode = AppMode::TaskReport,
AppMode::Calendar => match input { AppMode::Calendar => {
Key::Ctrl('c') | Key::Char('q') => self.should_quit = true, if input == self.keyconfig.quit || input == Key::Ctrl('c') {
Key::Char('[') => { self.should_quit = true;
} else if input == self.keyconfig.previous_tab {
self.mode = AppMode::TaskReport; self.mode = AppMode::TaskReport;
} } else if input == Key::Up || input == self.keyconfig.up {
Key::Up | Key::Char('k') => {
if self.calendar_year > 0 { if self.calendar_year > 0 {
self.calendar_year -= 1 self.calendar_year -= 1;
} }
} } else if input == Key::Down || input == self.keyconfig.down {
Key::Down | Key::Char('j') => self.calendar_year += 1, self.calendar_year += 1;
Key::PageUp | Key::Char('K') => { } else if input == Key::PageUp || input == self.keyconfig.page_up {
if self.calendar_year > 0 { if self.calendar_year > 0 {
self.calendar_year -= 10 self.calendar_year -= 10
} }
} else if input == Key::PageDown || input == self.keyconfig.page_down {
self.calendar_year += 10
} }
Key::PageDown | Key::Char('J') => self.calendar_year += 10, }
_ => {}
},
} }
Ok(()) Ok(())
} }
@ -1949,7 +1946,6 @@ mod tests {
let caps = re.captures(&s); let caps = re.captures(&s);
if caps.is_none() { if caps.is_none() {
let s = String::from_utf8_lossy(&output.stderr); let s = String::from_utf8_lossy(&output.stderr);
dbg!(s);
assert!(false); assert!(false);
} }
let caps = re.captures(&s).unwrap(); let caps = re.captures(&s).unwrap();
@ -2036,7 +2032,6 @@ mod tests {
"UNBLOCKED", "UNBLOCKED",
"YEAR", "YEAR",
] { ] {
dbg!(s, task.tags());
assert!(task.tags().unwrap().contains(&s.to_string())); assert!(task.tags().unwrap().contains(&s.to_string()));
} }

195
src/keyconfig.rs Normal file
View file

@ -0,0 +1,195 @@
use crate::util::Key;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::error::Error;
use std::hash::Hash;
use std::process::Command;
#[derive(Serialize, Deserialize, Debug)]
pub struct KeyConfig {
pub quit: Key,
pub refresh: Key,
pub go_to_bottom: Key,
pub go_to_top: Key,
pub down: Key,
pub up: Key,
pub page_down: Key,
pub page_up: Key,
pub delete: Key,
pub done: Key,
pub start_stop: Key,
pub undo: Key,
pub edit: Key,
pub modify: Key,
pub shell: Key,
pub log: Key,
pub add: Key,
pub annotate: Key,
pub help: Key,
pub filter: Key,
pub zoom: Key,
pub context_menu: Key,
pub next_tab: Key,
pub previous_tab: Key,
}
impl Default for KeyConfig {
fn default() -> Self {
Self {
quit: Key::Char('q'),
refresh: Key::Char('r'),
go_to_bottom: Key::End,
go_to_top: Key::Home,
down: Key::Char('j'),
up: Key::Char('k'),
page_down: Key::Char('J'),
page_up: Key::Char('K'),
delete: Key::Char('d'),
done: Key::Char('x'),
start_stop: Key::Char('s'),
undo: Key::Char('u'),
edit: Key::Char('e'),
modify: Key::Char('m'),
shell: Key::Char('!'),
log: Key::Char('l'),
add: Key::Char('a'),
annotate: Key::Char('A'),
help: Key::Char('?'),
filter: Key::Char('/'),
zoom: Key::Char('z'),
context_menu: Key::Char('c'),
next_tab: Key::Char(']'),
previous_tab: Key::Char('['),
}
}
}
impl KeyConfig {
pub fn update(&mut self) -> Result<(), Box<dyn Error>> {
self.quit = self.get_config("taskwarrior-tui.keyconfig.quit").unwrap_or(self.quit);
self.refresh = self
.get_config("taskwarrior-tui.keyconfig.refresh")
.unwrap_or(self.refresh);
self.go_to_bottom = self
.get_config("taskwarrior-tui.keyconfig.go-to-bottom")
.unwrap_or(self.go_to_bottom);
self.go_to_top = self
.get_config("taskwarrior-tui.keyconfig.go-to-top")
.unwrap_or(self.go_to_top);
self.down = self.get_config("taskwarrior-tui.keyconfig.down").unwrap_or(self.down);
self.up = self.get_config("taskwarrior-tui.keyconfig.up").unwrap_or(self.up);
self.page_down = self
.get_config("taskwarrior-tui.keyconfig.page-down")
.unwrap_or(self.page_down);
self.page_up = self
.get_config("taskwarrior-tui.keyconfig.page-up")
.unwrap_or(self.page_up);
self.delete = self
.get_config("taskwarrior-tui.keyconfig.delete")
.unwrap_or(self.delete);
self.done = self.get_config("taskwarrior-tui.keyconfig.done").unwrap_or(self.done);
self.start_stop = self
.get_config("taskwarrior-tui.keyconfig.start-stop")
.unwrap_or(self.start_stop);
self.undo = self.get_config("taskwarrior-tui.keyconfig.undo").unwrap_or(self.undo);
self.edit = self.get_config("taskwarrior-tui.keyconfig.edit").unwrap_or(self.edit);
self.modify = self
.get_config("taskwarrior-tui.keyconfig.modify")
.unwrap_or(self.modify);
self.shell = self.get_config("taskwarrior-tui.keyconfig.shell").unwrap_or(self.shell);
self.log = self.get_config("taskwarrior-tui.keyconfig.log").unwrap_or(self.log);
self.add = self.get_config("taskwarrior-tui.keyconfig.add").unwrap_or(self.add);
self.annotate = self
.get_config("taskwarrior-tui.keyconfig.annotate")
.unwrap_or(self.annotate);
self.filter = self
.get_config("taskwarrior-tui.keyconfig.filter")
.unwrap_or(self.filter);
self.zoom = self.get_config("taskwarrior-tui.keyconfig.zoom").unwrap_or(self.zoom);
self.context_menu = self
.get_config("taskwarrior-tui.keyconfig.context-menu")
.unwrap_or(self.context_menu);
self.next_tab = self
.get_config("taskwarrior-tui.keyconfig.next-tab")
.unwrap_or(self.next_tab);
self.previous_tab = self
.get_config("taskwarrior-tui.keyconfig.previous-tab")
.unwrap_or(self.previous_tab);
self.check()
}
pub fn check(&self) -> Result<(), Box<dyn Error>> {
let mut elements = vec![
&self.quit,
&self.refresh,
&self.go_to_bottom,
&self.go_to_top,
&self.down,
&self.up,
&self.page_down,
&self.page_up,
&self.delete,
&self.done,
&self.start_stop,
&self.undo,
&self.edit,
&self.modify,
&self.shell,
&self.log,
&self.add,
&self.annotate,
&self.help,
&self.filter,
&self.zoom,
&self.context_menu,
&self.next_tab,
&self.previous_tab,
];
let l = elements.len();
elements.dedup();
if l == elements.len() {
Ok(())
} else {
Err("Duplicate keys found in key config".into())
}
}
fn get_config(&mut self, config: &str) -> Option<Key> {
let output = Command::new("task")
.arg("rc.color=off")
.arg("show")
.arg(config)
.output()
.expect("Unable to run `task show`");
let data = String::from_utf8_lossy(&output.stdout);
for line in data.split('\n') {
if line.starts_with(config) {
let line = line.trim_start_matches(config).trim_start().trim_end().to_string();
if line.len() == 1 {
return Some(Key::Char(line.chars().next().unwrap()));
} else {
return None;
}
} else if line.starts_with(&config.replace('-', "_")) {
let line = line
.trim_start_matches(&config.replace('-', "_"))
.trim_start()
.trim_end()
.to_string();
if line.len() == 1 {
return Some(Key::Char(line.chars().next().unwrap()));
} else {
return None;
}
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
}

View file

@ -7,6 +7,7 @@ mod calendar;
mod config; mod config;
mod context; mod context;
mod help; mod help;
mod keyconfig;
mod table; mod table;
mod task_report; mod task_report;
mod util; mod util;

View file

@ -11,7 +11,9 @@ use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::{sync::mpsc, thread, time::Duration, time::Instant}; use std::{sync::mpsc, thread, time::Duration, time::Instant};
#[derive(Debug, Clone, Copy)] use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, PartialOrd)]
pub enum Key { pub enum Key {
Backspace, Backspace,
Left, Left,