This commit is contained in:
Dheepak Krishnamurthy 2023-09-04 14:17:38 -04:00
parent 0d89f657d5
commit 951939efb8
8 changed files with 1024 additions and 1088 deletions

298
Cargo.lock generated
View file

@ -17,17 +17,6 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "ahash"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
dependencies = [
"getrandom",
"once_cell",
"version_check",
]
[[package]]
name = "aho-corasick"
version = "1.0.5"
@ -101,15 +90,10 @@ dependencies = [
]
[[package]]
name = "async-trait"
version = "0.1.73"
name = "atomic"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.29",
]
checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba"
[[package]]
name = "autocfg"
@ -132,12 +116,6 @@ dependencies = [
"rustc-demangle",
]
[[package]]
name = "base64"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "bitflags"
version = "1.3.2"
@ -153,15 +131,6 @@ dependencies = [
"serde",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "bumpalo"
version = "3.13.0"
@ -312,40 +281,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "config"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d379af7f68bfc21714c6c7dea883544201741d2ce8274bb12fa54f89507f52a7"
dependencies = [
"async-trait",
"json5",
"lazy_static",
"nom",
"pathdiff",
"ron",
"rust-ini",
"serde",
"serde_json",
"toml 0.5.11",
"yaml-rust",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
[[package]]
name = "cpufeatures"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1"
dependencies = [
"libc",
]
[[package]]
name = "crossterm"
version = "0.27.0"
@ -373,16 +314,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "darling"
version = "0.14.4"
@ -455,16 +386,6 @@ version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "directories"
version = "5.0.1"
@ -495,12 +416,6 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "dlv-list"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257"
[[package]]
name = "either"
version = "1.9.0"
@ -571,6 +486,20 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "figment"
version = "0.10.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4547e226f4c9ab860571e070a9034192b3175580ecea38da34fcdb53a018c9a5"
dependencies = [
"atomic",
"pear",
"serde",
"toml",
"uncased",
"version_check",
]
[[package]]
name = "fnv"
version = "1.0.7"
@ -675,16 +604,6 @@ dependencies = [
"byteorder",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.10"
@ -702,15 +621,6 @@ version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
dependencies = [
"ahash",
]
[[package]]
name = "hashbrown"
version = "0.14.0"
@ -750,7 +660,7 @@ dependencies = [
"os_info",
"serde",
"serde_derive",
"toml 0.7.6",
"toml",
"uuid",
]
@ -796,7 +706,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
dependencies = [
"equivalent",
"hashbrown 0.14.0",
"hashbrown",
]
[[package]]
@ -805,6 +715,12 @@ version = "2.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c785eefb63ebd0e33416dfcb8d6da0bf27ce752843a45632a67bf10d4d4b5c4"
[[package]]
name = "inlinable_string"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
[[package]]
name = "io-lifetimes"
version = "1.0.11"
@ -840,17 +756,6 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "json5"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1"
dependencies = [
"pest",
"pest_derive",
"serde",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
@ -863,12 +768,6 @@ version = "0.2.147"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
[[package]]
name = "linked-hash-map"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linux-raw-sys"
version = "0.3.8"
@ -1020,16 +919,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "ordered-multimap"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a"
dependencies = [
"dlv-list",
"hashbrown 0.12.3",
]
[[package]]
name = "os_info"
version = "3.7.0"
@ -1089,56 +978,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17359afc20d7ab31fdb42bb844c8b3bb1dabd7dcf7e68428492da7f16966fcef"
[[package]]
name = "pathdiff"
version = "0.2.1"
name = "pear"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
[[package]]
name = "pest"
version = "2.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7a4d085fd991ac8d5b05a147b437791b4260b76326baf0fc60cf7c9c27ecd33"
checksum = "61a386cd715229d399604b50d1361683fe687066f42d56f54be995bc6868f71c"
dependencies = [
"memchr",
"thiserror",
"ucd-trie",
"inlinable_string",
"pear_codegen",
"yansi 1.0.0-rc.1",
]
[[package]]
name = "pest_derive"
version = "2.7.3"
name = "pear_codegen"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2bee7be22ce7918f641a33f08e3f43388c7656772244e2bbb2477f44cc9021a"
checksum = "da9f0f13dac8069c139e8300a6510e3f4143ecf5259c60b116a9b271b4ca0d54"
dependencies = [
"pest",
"pest_generator",
]
[[package]]
name = "pest_generator"
version = "2.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1511785c5e98d79a05e8a6bc34b4ac2168a0e3e92161862030ad84daa223141"
dependencies = [
"pest",
"pest_meta",
"proc-macro2",
"proc-macro2-diagnostics",
"quote",
"syn 2.0.29",
]
[[package]]
name = "pest_meta"
version = "2.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42f0394d3123e33353ca5e1e89092e533d2cc490389f2bd6131c43c634ebc5f"
dependencies = [
"once_cell",
"pest",
"sha2",
]
[[package]]
name = "pin-project-lite"
version = "0.2.12"
@ -1164,7 +1025,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66"
dependencies = [
"diff",
"yansi",
"yansi 0.5.1",
]
[[package]]
@ -1176,6 +1037,19 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "proc-macro2-diagnostics"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.29",
"version_check",
"yansi 1.0.0-rc.1",
]
[[package]]
name = "quote"
version = "1.0.33"
@ -1329,27 +1203,6 @@ version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
[[package]]
name = "ron"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a"
dependencies = [
"base64",
"bitflags 1.3.2",
"serde",
]
[[package]]
name = "rust-ini"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df"
dependencies = [
"cfg-if",
"ordered-multimap",
]
[[package]]
name = "rustc-demangle"
version = "0.1.23"
@ -1476,17 +1329,6 @@ dependencies = [
"serde",
]
[[package]]
name = "sha2"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "sharded-slab"
version = "0.1.4"
@ -1661,9 +1503,9 @@ dependencies = [
"clap",
"clap_complete",
"color-eyre",
"config",
"crossterm",
"directories",
"figment",
"futures",
"human-panic",
"itertools",
@ -1686,6 +1528,7 @@ dependencies = [
"task-hookrs",
"tokio",
"tokio-util",
"toml",
"tracing",
"tracing-error",
"tracing-subscriber",
@ -1792,15 +1635,6 @@ dependencies = [
"tokio",
]
[[package]]
name = "toml"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
dependencies = [
"serde",
]
[[package]]
name = "toml"
version = "0.7.6"
@ -1934,16 +1768,13 @@ dependencies = [
]
[[package]]
name = "typenum"
version = "1.16.0"
name = "uncased"
version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
[[package]]
name = "ucd-trie"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9"
checksum = "9b9bc53168a4be7402ab86c3aad243a84dd7381d09be0eddc81280c1da95ca68"
dependencies = [
"version_check",
]
[[package]]
name = "unicase"
@ -2211,17 +2042,14 @@ dependencies = [
"memchr",
]
[[package]]
name = "yaml-rust"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
dependencies = [
"linked-hash-map",
]
[[package]]
name = "yansi"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
[[package]]
name = "yansi"
version = "1.0.0-rc.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1367295b8f788d371ce2dbc842c7b709c73ee1364d30351dd300ec2203b12377"

View file

@ -17,9 +17,9 @@ cassowary = "0.3.0"
chrono = "0.4.28"
clap = { version = "4.4.2", features = ["std", "color", "help", "usage", "error-context", "suggestions", "derive", "cargo", "wrap_help", "unicode", "string", "unstable-styles"] }
color-eyre = "0.6.2"
config = "0.13.3"
crossterm = { version = "0.27.0", features = ["event-stream", "serde"] }
directories = "5.0.1"
figment = { version = "0.10.10", features = ["toml", "env"] }
futures = "0.3.28"
human-panic = "1.2.0"
itertools = "0.11.0"
@ -42,6 +42,7 @@ strip-ansi-escapes = "0.2.0"
task-hookrs = "0.9.0"
tokio = { version = "1.32.0", features = ["full"] }
tokio-util = "0.7.8"
toml = "0.7.6"
tracing = "0.1.37"
tracing-error = "0.2.0"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }

View file

@ -1,5 +1,32 @@
#[derive(Clone, PartialEq, Eq, Debug, Copy)]
use serde_derive::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Action {
Quit,
Refresh,
GotoBottom,
GotoTop,
GotoPageBottom,
GotoPageTop,
Down,
Up,
PageDown,
PageUp,
Delete,
Done,
ToggleStartStop,
QuickTag,
Select,
SelectAll,
Undo,
Edit,
Shell,
Help,
ToggleZoom,
Context,
Next,
Previous,
Shortcut(usize),
Report,
Filter,
Add,

View file

@ -13,7 +13,7 @@ use std::{
use chrono::{DateTime, Datelike, FixedOffset, Local, NaiveDate, NaiveDateTime, TimeZone, Timelike};
use color_eyre::eyre::{anyhow, Context as AnyhowContext, Result};
use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture, KeyCode, KeyModifiers},
event::{DisableMouseCapture, EnableMouseCapture, KeyCode, KeyEvent, KeyModifiers},
execute,
style::style,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
@ -211,7 +211,7 @@ pub struct TaskwarriorTui {
}
impl TaskwarriorTui {
pub async fn new(report: &str, init_event_loop: bool) -> Result<Self> {
pub async fn new(report: &str) -> Result<Self> {
let output = std::process::Command::new("task")
.arg("rc.color=off")
.arg("rc._forcecolor=off")
@ -360,7 +360,7 @@ impl TaskwarriorTui {
match event {
Event::Key(keyevent) => {
debug!("Received input = {:?}", keyevent);
self.handle_input(keyevent.code).await?;
self.handle_input(keyevent).await?;
}
Event::Mouse(mouseevent) => {
debug!("tui mouseevent")
@ -2405,7 +2405,7 @@ impl TaskwarriorTui {
es
}
pub async fn handle_input(&mut self, input: KeyCode) -> Result<()> {
pub async fn handle_input(&mut self, input: KeyEvent) -> Result<()> {
match self.mode {
Mode::Tasks(_) => {
self.handle_input_by_task_mode(input).await?;
@ -2462,7 +2462,7 @@ impl TaskwarriorTui {
Ok(())
}
async fn handle_input_by_task_mode(&mut self, input: KeyCode) -> Result<()> {
async fn handle_input_by_task_mode(&mut self, input: KeyEvent) -> Result<()> {
if let Mode::Tasks(task_mode) = &self.mode {
match task_mode {
Action::Report => {
@ -2923,7 +2923,7 @@ impl TaskwarriorTui {
}
_ => {
self.command_history.reset();
handle_movement(&mut self.modify, input, &mut self.changes);
handle_movement(&mut self.modify, input);
self.update_input_for_completion();
}
},
@ -2950,7 +2950,7 @@ impl TaskwarriorTui {
self.reset_command();
self.mode = Mode::Tasks(Action::Report);
}
_ => handle_movement(&mut self.command, input, &mut self.changes),
_ => handle_movement(&mut self.command, input),
},
Action::Log => match input {
KeyCode::Esc => {
@ -3056,7 +3056,7 @@ impl TaskwarriorTui {
}
_ => {
self.command_history.reset();
handle_movement(&mut self.command, input, &mut self.changes);
handle_movement(&mut self.command, input);
self.update_input_for_completion();
}
},
@ -3164,7 +3164,7 @@ impl TaskwarriorTui {
_ => {
self.command_history.reset();
handle_movement(&mut self.command, input, &mut self.changes);
handle_movement(&mut self.command, input);
self.update_input_for_completion();
}
},
@ -3192,7 +3192,7 @@ impl TaskwarriorTui {
self.reset_command();
self.mode = Mode::Tasks(Action::Report);
}
_ => handle_movement(&mut self.command, input, &mut self.changes),
_ => handle_movement(&mut self.command, input),
},
Action::Add => match input {
KeyCode::Esc => {
@ -3298,7 +3298,7 @@ impl TaskwarriorTui {
}
_ => {
self.command_history.reset();
handle_movement(&mut self.command, input, &mut self.changes);
handle_movement(&mut self.command, input);
self.update_input_for_completion();
}
},
@ -3412,7 +3412,7 @@ impl TaskwarriorTui {
// self.dirty = true;
// }
_ => {
handle_movement(&mut self.filter, input, &mut self.changes);
handle_movement(&mut self.filter, input);
self.update_input_for_completion();
self.dirty = true;
}
@ -3437,7 +3437,7 @@ impl TaskwarriorTui {
} else if input == self.keyconfig.quit || input == KeyCode::Esc {
self.mode = Mode::Tasks(Action::Report);
} else {
handle_movement(&mut self.command, input, &mut self.changes);
handle_movement(&mut self.command, input);
}
}
Action::DeletePrompt => {
@ -3460,7 +3460,7 @@ impl TaskwarriorTui {
} else if input == self.keyconfig.quit || input == KeyCode::Esc {
self.mode = Mode::Tasks(Action::Report);
} else {
handle_movement(&mut self.command, input, &mut self.changes);
handle_movement(&mut self.command, input);
}
}
Action::UndoPrompt => {
@ -3483,7 +3483,7 @@ impl TaskwarriorTui {
} else if input == self.keyconfig.quit || input == KeyCode::Esc {
self.mode = Mode::Tasks(Action::Report);
} else {
handle_movement(&mut self.command, input, &mut self.changes);
handle_movement(&mut self.command, input);
}
}
Action::Error => {
@ -3639,11 +3639,8 @@ impl TaskwarriorTui {
}
}
pub fn handle_movement(linebuffer: &mut Input, input: KeyCode, changes: &mut utils::Changeset) {
linebuffer.handle_event(&crossterm::event::Event::Key(crossterm::event::KeyEvent::new(
input,
KeyModifiers::empty(),
)));
pub fn handle_movement(linebuffer: &mut Input, input: KeyEvent) {
linebuffer.handle_event(&crossterm::event::Event::Key(input));
}
pub fn add_tag(task: &mut Task, tag: String) {

File diff suppressed because it is too large Load diff

279
src/keyevent.rs Normal file
View file

@ -0,0 +1,279 @@
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MediaKeyCode, ModifierKeyCode};
fn parse_key_event(raw: &str) -> Result<KeyEvent, String> {
let raw_lower = raw.to_ascii_lowercase();
let (remaining, modifiers) = extract_modifiers(&raw_lower);
parse_key_code_with_modifiers(remaining, modifiers)
}
fn extract_modifiers(raw: &str) -> (&str, KeyModifiers) {
let mut modifiers = KeyModifiers::empty();
let mut current = raw;
loop {
match current {
rest if rest.starts_with("ctrl-") => {
modifiers.insert(KeyModifiers::CONTROL);
current = &rest[5..];
}
rest if rest.starts_with("alt-") => {
modifiers.insert(KeyModifiers::ALT);
current = &rest[4..];
}
rest if rest.starts_with("shift-") => {
modifiers.insert(KeyModifiers::SHIFT);
current = &rest[6..];
}
_ => break, // break out of the loop if no known prefix is detected
};
}
(current, modifiers)
}
fn parse_key_code_with_modifiers(raw: &str, mut modifiers: KeyModifiers) -> Result<KeyEvent, String> {
let c = match raw {
"esc" => KeyCode::Esc,
"enter" => KeyCode::Enter,
"left" => KeyCode::Left,
"right" => KeyCode::Right,
"up" => KeyCode::Up,
"down" => KeyCode::Down,
"home" => KeyCode::Home,
"end" => KeyCode::End,
"pageup" => KeyCode::PageUp,
"pagedown" => KeyCode::PageDown,
"backtab" => {
modifiers.insert(KeyModifiers::SHIFT);
KeyCode::BackTab
}
"backspace" => KeyCode::Backspace,
"delete" => KeyCode::Delete,
"insert" => KeyCode::Insert,
"f1" => KeyCode::F(1),
"f2" => KeyCode::F(2),
"f3" => KeyCode::F(3),
"f4" => KeyCode::F(4),
"f5" => KeyCode::F(5),
"f6" => KeyCode::F(6),
"f7" => KeyCode::F(7),
"f8" => KeyCode::F(8),
"f9" => KeyCode::F(9),
"f10" => KeyCode::F(10),
"f11" => KeyCode::F(11),
"f12" => KeyCode::F(12),
"space" => KeyCode::Char(' '),
"tab" => KeyCode::Tab,
c if c.len() == 1 => {
let mut c = c.chars().next().unwrap();
if modifiers.contains(KeyModifiers::SHIFT) {
c = c.to_ascii_uppercase();
}
KeyCode::Char(c)
}
_ => return Err(format!("Unable to parse {raw}")),
};
Ok(KeyEvent::new(c, modifiers))
}
pub fn parse_key_sequence(raw: &str) -> Result<Vec<KeyEvent>, String> {
if raw.chars().filter(|c| *c == '>').count() != raw.chars().filter(|c| *c == '<').count() {
return Err(format!("Unable to parse `{}`", raw));
}
let raw = if !raw.contains("><") {
let raw = raw.strip_prefix("<").unwrap_or(raw);
let raw = raw.strip_prefix(">").unwrap_or(raw);
raw
} else {
raw
};
let sequences = raw
.split("><")
.map(|seq| {
if seq.starts_with('<') {
&seq[1..]
} else if seq.ends_with('>') {
&seq[..seq.len() - 1]
} else {
seq
}
})
.collect::<Vec<_>>();
sequences.into_iter().map(parse_key_event).collect()
}
pub fn key_event_to_string(event: KeyEvent) -> String {
let mut result = String::new();
result.push('<');
// Add modifiers
if event.modifiers.contains(KeyModifiers::CONTROL) {
result.push_str("ctrl-");
}
if event.modifiers.contains(KeyModifiers::ALT) {
result.push_str("alt-");
}
if event.modifiers.contains(KeyModifiers::SHIFT) {
result.push_str("shift-");
}
match event.code {
KeyCode::Char(' ') => result.push_str("space"),
KeyCode::Char(c) => result.push(c),
KeyCode::Enter => result.push_str("enter"),
KeyCode::Esc => result.push_str("esc"),
KeyCode::Left => result.push_str("left"),
KeyCode::Right => result.push_str("right"),
KeyCode::Up => result.push_str("up"),
KeyCode::Down => result.push_str("down"),
KeyCode::Home => result.push_str("home"),
KeyCode::End => result.push_str("end"),
KeyCode::PageUp => result.push_str("pageup"),
KeyCode::PageDown => result.push_str("pagedown"),
KeyCode::BackTab => result.push_str("backtab"),
KeyCode::Delete => result.push_str("delete"),
KeyCode::Insert => result.push_str("insert"),
KeyCode::F(n) => result.push_str(&format!("f{}", n)),
KeyCode::Backspace => result.push_str("backspace"),
KeyCode::Tab => result.push_str("tab"),
KeyCode::Null => result.push_str("null"),
KeyCode::CapsLock => result.push_str("capslock"),
KeyCode::ScrollLock => result.push_str("scrolllock"),
KeyCode::NumLock => result.push_str("numlock"),
KeyCode::PrintScreen => result.push_str("printscreen"),
KeyCode::Pause => result.push_str("pause"),
KeyCode::Menu => result.push_str("menu"),
KeyCode::KeypadBegin => result.push_str("keypadbegin"),
KeyCode::Media(media) => match media {
MediaKeyCode::Play => result.push_str("play"),
MediaKeyCode::Pause => result.push_str("pause"),
MediaKeyCode::PlayPause => result.push_str("playpause"),
MediaKeyCode::Reverse => result.push_str("reverse"),
MediaKeyCode::Stop => result.push_str("stop"),
MediaKeyCode::FastForward => result.push_str("fastforward"),
MediaKeyCode::Rewind => result.push_str("rewind"),
MediaKeyCode::TrackNext => result.push_str("tracknext"),
MediaKeyCode::TrackPrevious => result.push_str("trackprevious"),
MediaKeyCode::Record => result.push_str("record"),
MediaKeyCode::LowerVolume => result.push_str("lowervolume"),
MediaKeyCode::RaiseVolume => result.push_str("raisevolume"),
MediaKeyCode::MuteVolume => result.push_str("mutevolume"),
},
KeyCode::Modifier(keycode) => match keycode {
ModifierKeyCode::LeftShift => result.push_str("leftshift"),
ModifierKeyCode::LeftControl => result.push_str("leftcontrol"),
ModifierKeyCode::LeftAlt => result.push_str("leftalt"),
ModifierKeyCode::LeftSuper => result.push_str("leftsuper"),
ModifierKeyCode::LeftHyper => result.push_str("lefthyper"),
ModifierKeyCode::LeftMeta => result.push_str("leftmeta"),
ModifierKeyCode::RightShift => result.push_str("rightshift"),
ModifierKeyCode::RightControl => result.push_str("rightcontrol"),
ModifierKeyCode::RightAlt => result.push_str("rightalt"),
ModifierKeyCode::RightSuper => result.push_str("rightsuper"),
ModifierKeyCode::RightHyper => result.push_str("righthyper"),
ModifierKeyCode::RightMeta => result.push_str("rightmeta"),
ModifierKeyCode::IsoLevel3Shift => result.push_str("isolevel3shift"),
ModifierKeyCode::IsoLevel5Shift => result.push_str("isolevel5shift"),
},
}
result.push('>');
result
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
fn test_event_to_string() {
let event = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL);
println!("{}", key_event_to_string(event)); // Outputs: ctrl-a
}
#[test]
fn test_single_key_sequence() {
let result = parse_key_sequence("a");
assert_eq!(result.unwrap(), vec![KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty())]);
let result = parse_key_sequence("<a><b>");
assert_eq!(
result.unwrap(),
vec![
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()),
KeyEvent::new(KeyCode::Char('b'), KeyModifiers::empty())
]
);
let result = parse_key_sequence("<Ctrl-a><Alt-b>");
assert_eq!(
result.unwrap(),
vec![
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL),
KeyEvent::new(KeyCode::Char('b'), KeyModifiers::ALT)
]
);
let result = parse_key_sequence("<Ctrl-a>");
assert_eq!(result.unwrap(), vec![KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL),]);
let result = parse_key_sequence("<Ctrl-Alt-a>");
assert_eq!(
result.unwrap(),
vec![KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL | KeyModifiers::ALT),]
);
assert!(parse_key_sequence("Ctrl-a>").is_err());
assert!(parse_key_sequence("<Ctrl-a").is_err());
}
#[test]
fn test_simple_keys() {
assert_eq!(parse_key_event("a").unwrap(), KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()));
assert_eq!(parse_key_event("enter").unwrap(), KeyEvent::new(KeyCode::Enter, KeyModifiers::empty()));
assert_eq!(parse_key_event("esc").unwrap(), KeyEvent::new(KeyCode::Esc, KeyModifiers::empty()));
}
#[test]
fn test_with_modifiers() {
assert_eq!(
parse_key_event("ctrl-a").unwrap(),
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL)
);
assert_eq!(parse_key_event("alt-enter").unwrap(), KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT));
assert_eq!(parse_key_event("shift-esc").unwrap(), KeyEvent::new(KeyCode::Esc, KeyModifiers::SHIFT));
}
#[test]
fn test_multiple_modifiers() {
assert_eq!(
parse_key_event("ctrl-alt-a").unwrap(),
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL | KeyModifiers::ALT)
);
assert_eq!(
parse_key_event("ctrl-shift-enter").unwrap(),
KeyEvent::new(KeyCode::Enter, KeyModifiers::CONTROL | KeyModifiers::SHIFT)
);
}
#[test]
fn test_invalid_keys() {
assert!(parse_key_event("invalid-key").is_err());
assert!(parse_key_event("ctrl-invalid-key").is_err());
}
#[test]
fn test_case_insensitivity() {
assert_eq!(
parse_key_event("CTRL-a").unwrap(),
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL)
);
assert_eq!(parse_key_event("AlT-eNtEr").unwrap(), KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT));
}
}

196
src/keymap.rs Normal file
View file

@ -0,0 +1,196 @@
use std::ops::{Deref, DerefMut};
use std::{collections::HashMap, error::Error, str};
use color_eyre::eyre::{eyre, Context, Result};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use serde::de::{self, Deserialize, Deserializer, MapAccess, Visitor};
use serde::ser::{self, Serialize, SerializeMap, Serializer};
use crate::keyevent::key_event_to_string;
use crate::{action::Action, keyevent::parse_key_sequence};
#[derive(Clone, Debug, Default)]
pub struct KeyMap(pub std::collections::HashMap<Vec<KeyEvent>, Action>);
impl Deref for KeyMap {
type Target = std::collections::HashMap<Vec<KeyEvent>, Action>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for KeyMap {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl KeyMap {
pub fn validate(&self) -> Result<(), String> {
let mut sorted_sequences: Vec<_> = self.keys().collect();
sorted_sequences.sort_by_key(|seq| seq.len());
for i in 0..sorted_sequences.len() {
for j in i + 1..sorted_sequences.len() {
if sorted_sequences[j].starts_with(sorted_sequences[i]) {
return Err(format!(
"Conflict detected: Sequence {:?} is a prefix of sequence {:?}",
sorted_sequences[i], sorted_sequences[j]
));
}
}
}
Ok(())
}
}
impl Serialize for KeyMap {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
// Begin serializing a map.
let mut map = serializer.serialize_map(Some(self.0.len()))?;
for (key_sequence, action) in &self.0 {
let key_string = key_sequence
.iter()
.map(|key_event| key_event_to_string(*key_event))
.collect::<Vec<_>>()
.join("");
map.serialize_entry(&key_string, action)?;
}
// End serialization.
map.end()
}
}
impl<'de> Deserialize<'de> for KeyMap {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct KeyMapVisitor;
impl<'de> Visitor<'de> for KeyMapVisitor {
type Value = KeyMap;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a keymap with string representation of KeyEvent as key and Action as value")
}
fn visit_map<M>(self, mut access: M) -> Result<KeyMap, M::Error>
where
M: MapAccess<'de>,
{
let mut keymap = std::collections::HashMap::new();
// While there are entries in the map, read them
while let Some((key_sequence_str, action)) = access.next_entry::<String, Action>()? {
let key_sequence = parse_key_sequence(&key_sequence_str).map_err(de::Error::custom)?;
if let Some(old_action) = keymap.insert(key_sequence, action.clone()) {
if old_action != action {
return Err(format!("Found a {:?} for both {:?} and {:?}", key_sequence_str, old_action, action)).map_err(de::Error::custom);
}
}
}
Ok(KeyMap(keymap))
}
}
deserializer.deserialize_map(KeyMapVisitor)
}
}
#[cfg(test)]
mod validate_tests {
use super::*;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
#[test]
fn test_no_conflict() {
let mut map = std::collections::HashMap::new();
map.insert(vec![KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty())], Action::Report);
map.insert(vec![KeyEvent::new(KeyCode::Char('b'), KeyModifiers::empty())], Action::Report);
let keymap = KeyMap(map);
assert!(keymap.validate().is_ok());
}
#[test]
fn test_conflict_prefix() {
let mut map = std::collections::HashMap::new();
map.insert(vec![KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty())], Action::Report);
map.insert(
vec![
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()),
KeyEvent::new(KeyCode::Char('b'), KeyModifiers::empty()),
],
Action::Report,
);
let keymap = KeyMap(map);
assert!(keymap.validate().is_err());
}
#[test]
fn test_no_conflict_different_modifiers() {
let mut map = std::collections::HashMap::new();
map.insert(vec![KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL)], Action::Report);
map.insert(vec![KeyEvent::new(KeyCode::Char('a'), KeyModifiers::ALT)], Action::Report);
let keymap = KeyMap(map);
assert!(keymap.validate().is_ok());
}
#[test]
fn test_no_conflict_multiple_keys() {
let mut map = std::collections::HashMap::new();
map.insert(
vec![
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()),
KeyEvent::new(KeyCode::Char('b'), KeyModifiers::empty()),
],
Action::Report,
);
map.insert(
vec![
KeyEvent::new(KeyCode::Char('b'), KeyModifiers::empty()),
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()),
],
Action::Report,
);
let keymap = KeyMap(map);
assert!(keymap.validate().is_ok());
}
#[test]
fn test_conflict_three_keys() {
let mut map = std::collections::HashMap::new();
map.insert(
vec![
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()),
KeyEvent::new(KeyCode::Char('b'), KeyModifiers::empty()),
KeyEvent::new(KeyCode::Char('c'), KeyModifiers::empty()),
],
Action::Report,
);
map.insert(
vec![
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()),
KeyEvent::new(KeyCode::Char('b'), KeyModifiers::empty()),
],
Action::Report,
);
let keymap = KeyMap(map);
assert!(keymap.validate().is_err());
}
}

View file

@ -3,21 +3,24 @@
#![allow(unused_variables)]
#![allow(clippy::too_many_arguments)]
// mod app;
// mod calendar;
// mod cli;
// mod completion;
// mod config;
// mod help;
// mod history;
// mod keyconfig;
// mod pane;
// mod scrollbar;
// mod table;
// mod task_report;
// mod ui;
mod action;
mod app;
mod calendar;
mod cli;
mod completion;
mod config;
mod help;
mod history;
mod keyconfig;
mod pane;
mod scrollbar;
mod table;
mod task_report;
mod keyevent;
mod keymap;
mod tui;
mod ui;
mod utils;
use std::{
@ -29,73 +32,73 @@ use std::{
time::Duration,
};
use app::{Mode, TaskwarriorTui};
// use app::{Mode, TaskwarriorTui};
use color_eyre::eyre::Result;
use crossterm::{
cursor,
event::{DisableMouseCapture, EnableMouseCapture, EventStream},
execute,
terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
};
use futures::stream::{FuturesUnordered, StreamExt};
use log::{debug, error, info, log_enabled, trace, warn, Level, LevelFilter};
use ratatui::{backend::CrosstermBackend, Terminal};
use utils::{get_config_dir, get_data_dir};
// use crossterm::{
// cursor,
// event::{DisableMouseCapture, EnableMouseCapture, EventStream},
// execute,
// terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
// };
// use futures::stream::{FuturesUnordered, StreamExt};
// use log::{debug, error, info, log_enabled, trace, warn, Level, LevelFilter};
// use ratatui::{backend::CrosstermBackend, Terminal};
// use utils::{get_config_dir, get_data_dir};
use crate::{
action::Action,
keyconfig::KeyConfig,
utils::{initialize_logging, initialize_panic_handler},
};
const LOG_PATTERN: &str = "{d(%Y-%m-%d %H:%M:%S)} | {l} | {f}:{L} | {m}{n}";
// use crate::{
// action::Action,
// keyconfig::KeyConfig,
// utils::{initialize_logging, initialize_panic_handler},
// };
//
// const LOG_PATTERN: &str = "{d(%Y-%m-%d %H:%M:%S)} | {l} | {f}:{L} | {m}{n}";
#[tokio::main]
async fn main() -> Result<()> {
let matches = cli::generate_cli_app().get_matches();
let config = matches.get_one::<String>("config");
let data = matches.get_one::<String>("data");
let taskrc = matches.get_one::<String>("taskrc");
let taskdata = matches.get_one::<String>("taskdata");
let binding = String::from("next");
let report = matches.get_one::<String>("report").unwrap_or(&binding);
let config_dir = config.map(PathBuf::from).unwrap_or_else(get_config_dir);
let data_dir = data.map(PathBuf::from).unwrap_or_else(get_data_dir);
// if let Some(e) = taskrc {
// if env::var("TASKRC").is_err() {
// // if environment variable is not set, this env::var returns an error
// env::set_var("TASKRC", absolute_path(PathBuf::from(e)).expect("Unable to get path for taskrc"))
// } else {
// warn!("TASKRC environment variable cannot be set.")
// }
// }
// let matches = cli::generate_cli_app().get_matches();
//
// if let Some(e) = taskdata {
// if env::var("TASKDATA").is_err() {
// // if environment variable is not set, this env::var returns an error
// env::set_var("TASKDATA", absolute_path(PathBuf::from(e)).expect("Unable to get path for taskdata"))
// } else {
// warn!("TASKDATA environment variable cannot be set.")
// let config = matches.get_one::<String>("config");
// let data = matches.get_one::<String>("data");
// let taskrc = matches.get_one::<String>("taskrc");
// let taskdata = matches.get_one::<String>("taskdata");
// let binding = String::from("next");
// let report = matches.get_one::<String>("report").unwrap_or(&binding);
//
// let config_dir = config.map(PathBuf::from).unwrap_or_else(get_config_dir);
// let data_dir = data.map(PathBuf::from).unwrap_or_else(get_data_dir);
//
// // if let Some(e) = taskrc {
// // if env::var("TASKRC").is_err() {
// // // if environment variable is not set, this env::var returns an error
// // env::set_var("TASKRC", absolute_path(PathBuf::from(e)).expect("Unable to get path for taskrc"))
// // } else {
// // warn!("TASKRC environment variable cannot be set.")
// // }
// // }
// //
// // if let Some(e) = taskdata {
// // if env::var("TASKDATA").is_err() {
// // // if environment variable is not set, this env::var returns an error
// // env::set_var("TASKDATA", absolute_path(PathBuf::from(e)).expect("Unable to get path for taskdata"))
// // } else {
// // warn!("TASKDATA environment variable cannot be set.")
// // }
// // }
//
// initialize_logging(data_dir)?;
// initialize_panic_handler()?;
//
// log::info!("getting matches from clap...");
// debug!("report = {:?}", &report);
// debug!("config = {:?}", &config);
//
// let mut app = app::TaskwarriorTui::new(report).await?;
//
// let r = app.run().await;
//
// if let Err(err) = r {
// eprintln!("\x1b[0;31m[taskwarrior-tui error]\x1b[0m: {}\n\nIf you need additional help, please report as a github issue on https://github.com/kdheepak/taskwarrior-tui", err);
// std::process::exit(1);
// }
// }
initialize_logging(data_dir)?;
initialize_panic_handler()?;
debug!("getting matches from clap...");
debug!("report = {:?}", &report);
debug!("config = {:?}", &config);
let mut app = app::TaskwarriorTui::new(report, true).await?;
let r = app.run().await;
if let Err(err) = r {
eprintln!("\x1b[0;31m[taskwarrior-tui error]\x1b[0m: {}\n\nIf you need additional help, please report as a github issue on https://github.com/kdheepak/taskwarrior-tui", err);
std::process::exit(1);
}
Ok(())
}