From af8a6370307e65cff9b0a3913482ecf5569b5582 Mon Sep 17 00:00:00 2001 From: Arijit Basu Date: Sat, 3 Apr 2021 11:31:40 +0530 Subject: [PATCH] Logging, testing and other improvements --- Cargo.lock | 44 +++++++- Cargo.toml | 1 + src/app.rs | 139 ++++++++++++++++++++---- src/config.rs | 253 +++++++++++++++++++++++++++++++++++++++----- src/event_reader.rs | 17 ++- src/main.rs | 31 ++++++ src/pipe_reader.rs | 16 ++- src/ui.rs | 117 ++++++++------------ tests/test_task.rs | 29 +++++ 9 files changed, 517 insertions(+), 130 deletions(-) create mode 100644 tests/test_task.rs diff --git a/Cargo.lock b/Cargo.lock index 70e0c34..8e12807 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -132,6 +132,20 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "serde", + "time", + "winapi", +] + [[package]] name = "clap" version = "2.33.3" @@ -341,7 +355,7 @@ checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.9.0+wasi-snapshot-preview1", ] [[package]] @@ -519,6 +533,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.14" @@ -940,6 +964,17 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -1026,6 +1061,12 @@ version = "0.9.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + [[package]] name = "wasm-bindgen" version = "0.2.73" @@ -1126,6 +1167,7 @@ name = "xplr" version = "0.2.14" dependencies = [ "anyhow", + "chrono", "criterion", "crossterm", "dirs", diff --git a/Cargo.toml b/Cargo.toml index 653c1bd..d146342 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ serde_yaml = "0.8" handlebars = "3.5" mime_guess = "2.0.3" anyhow = "1.0" +chrono = { version = "0.4", features = ["serde"] } [dev-dependencies] criterion = "0.3" diff --git a/src/app.rs b/src/app.rs index 8752d48..670efd0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2,6 +2,7 @@ use crate::config::Config; use crate::config::Mode; use crate::input::Key; use anyhow::{bail, Result}; +use chrono::{DateTime, Utc}; use mime_guess; use serde::{Deserialize, Serialize}; use std::cmp::Ordering; @@ -162,7 +163,7 @@ pub enum InternalMsg { HandleKey(Key), } -#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)] pub enum NodeFilter { RelativePathIs, RelativePathIsNot, @@ -376,7 +377,7 @@ impl NodeFilterApplicable { } #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] -pub struct NodeFilterFromInputString { +pub struct NodeFilterFromInput { filter: NodeFilter, #[serde(default)] case_sensitive: bool, @@ -407,14 +408,16 @@ pub enum ExternalMsg { FocusFirst, FocusLast, FocusPath(String), + FocusPathFromInput, FocusByIndex(usize), FocusByIndexFromInput, FocusByFileName(String), ChangeDirectory(String), Enter, Back, - BufferString(String), - BufferStringFromKey, + BufferInput(String), + BufferInputFromKey, + SetInputBuffer(String), ResetInputBuffer, SwitchMode(String), Call(Command), @@ -425,8 +428,11 @@ pub enum ExternalMsg { AddNodeFilter(NodeFilterApplicable), RemoveNodeFilter(NodeFilterApplicable), ToggleNodeFilter(NodeFilterApplicable), - AddNodeFilterFromInputString(NodeFilterFromInputString), + AddNodeFilterFromInput(NodeFilterFromInput), ResetNodeFilters, + LogInfo(String), + LogSuccess(String), + LogError(String), PrintResultAndQuit, PrintAppStateAndQuit, Debug(String), @@ -463,11 +469,17 @@ pub struct Task { priority: usize, msg: MsgIn, key: Option, + created_at: DateTime, } impl Task { pub fn new(priority: usize, msg: MsgIn, key: Option) -> Self { - Self { priority, msg, key } + Self { + priority, + msg, + key, + created_at: Utc::now(), + } } } @@ -476,7 +488,10 @@ impl Ord for Task { // Notice that the we flip the ordering on costs. // In case of a tie we compare positions - this step is necessary // to make implementations of `PartialEq` and `Ord` consistent. - other.priority.cmp(&self.priority) + other + .priority + .cmp(&self.priority) + .then_with(|| other.created_at.cmp(&self.created_at)) } } impl PartialOrd for Task { @@ -485,6 +500,48 @@ impl PartialOrd for Task { } } +#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub enum LogLevel { + Info, + Success, + Error, +} + +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +pub struct Log { + pub level: LogLevel, + pub message: String, + pub created_at: DateTime, +} + +impl Log { + pub fn new(level: LogLevel, message: String) -> Self { + Self { + level, + message, + created_at: Utc::now(), + } + } +} + +impl std::fmt::Display for Log { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let level_str = match self.level { + LogLevel::Info => "INFO ", + LogLevel::Success => "SUCCESS", + LogLevel::Error => "ERROR ", + }; + write!(f, "[{}] {} {}", &self.created_at, level_str, &self.message) + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +pub enum HelpMenuLine { + KeyMap(String, String), + Paragraph(String), +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct App { config: Config, @@ -499,6 +556,7 @@ pub struct App { session_path: String, pipe: Pipe, explorer_config: ExplorerConfig, + logs: Vec, } impl App { @@ -569,6 +627,7 @@ impl App { session_path: session_path.clone(), pipe: Pipe::from_session_path(&session_path), explorer_config, + logs: Default::default(), }) } } @@ -621,14 +680,16 @@ impl App { self.focus_next_by_relative_index_from_input() } ExternalMsg::FocusPath(p) => self.focus_path(&p), + ExternalMsg::FocusPathFromInput => self.focus_path_from_input(), ExternalMsg::FocusByIndex(i) => self.focus_by_index(i), ExternalMsg::FocusByIndexFromInput => self.focus_by_index_from_input(), ExternalMsg::FocusByFileName(n) => self.focus_by_file_name(&n), ExternalMsg::ChangeDirectory(dir) => self.change_directory(&dir), ExternalMsg::Enter => self.enter(), ExternalMsg::Back => self.back(), - ExternalMsg::BufferString(input) => self.buffer_string(&input), - ExternalMsg::BufferStringFromKey => self.buffer_string_from_key(key), + ExternalMsg::BufferInput(input) => self.buffer_input(&input), + ExternalMsg::BufferInputFromKey => self.buffer_input_from_key(key), + ExternalMsg::SetInputBuffer(input) => self.set_input_buffer(input), ExternalMsg::ResetInputBuffer => self.reset_input_buffer(), ExternalMsg::SwitchMode(mode) => self.switch_mode(&mode), ExternalMsg::Call(cmd) => self.call(cmd), @@ -637,12 +698,13 @@ impl App { ExternalMsg::ToggleSelection => self.toggle_selection(), ExternalMsg::ClearSelection => self.clear_selection(), ExternalMsg::AddNodeFilter(f) => self.add_node_filter(f), - ExternalMsg::AddNodeFilterFromInputString(f) => { - self.add_node_filter_from_input_string(f) - } + ExternalMsg::AddNodeFilterFromInput(f) => self.add_node_filter_from_input(f), ExternalMsg::RemoveNodeFilter(f) => self.remove_node_filter(f), ExternalMsg::ToggleNodeFilter(f) => self.toggle_node_filter(f), ExternalMsg::ResetNodeFilters => self.reset_node_filters(), + ExternalMsg::LogInfo(l) => self.log_info(l), + ExternalMsg::LogSuccess(l) => self.log_success(l), + ExternalMsg::LogError(l) => self.log_error(l), ExternalMsg::PrintResultAndQuit => self.print_result_and_quit(), ExternalMsg::PrintAppStateAndQuit => self.print_app_state_and_quit(), ExternalMsg::Debug(path) => self.debug(&path), @@ -781,7 +843,7 @@ impl App { .unwrap_or(Ok(self)) } - fn buffer_string(mut self, input: &String) -> Result { + fn buffer_input(mut self, input: &String) -> Result { if let Some(buf) = self.input_buffer.as_mut() { buf.extend(input.chars()); } else { @@ -791,14 +853,20 @@ impl App { Ok(self) } - fn buffer_string_from_key(self, key: Option) -> Result { + fn buffer_input_from_key(self, key: Option) -> Result { if let Some(c) = key.and_then(|k| k.to_char()) { - self.buffer_string(&c.to_string()) + self.buffer_input(&c.to_string()) } else { Ok(self) } } + fn set_input_buffer(mut self, string: String) -> Result { + self.input_buffer = Some(string); + self.msg_out.push_back(MsgOut::Refresh); + Ok(self) + } + fn reset_input_buffer(mut self) -> Result { self.input_buffer = None; self.msg_out.push_back(MsgOut::Refresh); @@ -852,6 +920,14 @@ impl App { } } + fn focus_path_from_input(self) -> Result { + if let Some(p) = self.input_buffer() { + self.focus_path(&p) + } else { + Ok(self) + } + } + fn switch_mode(mut self, mode: &String) -> Result { if let Some(mode) = self.config.modes.get(mode) { self.input_buffer = None; @@ -912,21 +988,18 @@ impl App { fn add_node_filter(mut self, filter: NodeFilterApplicable) -> Result { self.explorer_config.filters.push(filter); - self.msg_out.push_back(MsgOut::Explore); + self.msg_out.push_back(MsgOut::Refresh); Ok(self) } - fn add_node_filter_from_input_string( - mut self, - filter: NodeFilterFromInputString, - ) -> Result { + fn add_node_filter_from_input(mut self, filter: NodeFilterFromInput) -> Result { if let Some(input) = self.input_buffer() { self.explorer_config.filters.push(NodeFilterApplicable::new( filter.filter, input, filter.case_sensitive, )); - self.msg_out.push_back(MsgOut::Explore); + self.msg_out.push_back(MsgOut::Refresh); }; Ok(self) } @@ -938,7 +1011,7 @@ impl App { .into_iter() .filter(|f| f != &filter) .collect(); - self.msg_out.push_back(MsgOut::Explore); + self.msg_out.push_back(MsgOut::Refresh); Ok(self) } @@ -960,8 +1033,23 @@ impl App { Default::default(), )); }; - self.msg_out.push_back(MsgOut::Explore); + self.msg_out.push_back(MsgOut::Refresh); + + Ok(self) + } + + fn log_info(mut self, message: String) -> Result { + self.logs.push(Log::new(LogLevel::Info, message)); + Ok(self) + } + + fn log_success(mut self, message: String) -> Result { + self.logs.push(Log::new(LogLevel::Success, message)); + Ok(self) + } + fn log_error(mut self, message: String) -> Result { + self.logs.push(Log::new(LogLevel::Error, message)); Ok(self) } @@ -1068,4 +1156,9 @@ impl App { pub fn explorer_config(&self) -> &ExplorerConfig { &self.explorer_config } + + /// Get a reference to the app's logs. + pub fn logs(&self) -> &Vec { + &self.logs + } } diff --git a/src/config.rs b/src/config.rs index ad8671d..0b7da77 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,5 @@ use crate::app::ExternalMsg; +use crate::app::HelpMenuLine; use crate::app::VERSION; use serde::{Deserialize, Serialize}; use serde_yaml; @@ -293,11 +294,17 @@ impl Default for KeyBindings { ctrl-f: help: search [/] messages: + - ResetNodeFilters - SwitchMode: search + - SetInputBuffer: "" + - Explore /: messages: + - ResetNodeFilters - SwitchMode: search + - SetInputBuffer: "" + - Explore d: help: delete @@ -326,6 +333,7 @@ impl Default for KeyBindings { - ToggleNodeFilter: filter: RelativePathDoesNotStartWith input: . + - Explore enter: help: quit with result @@ -336,6 +344,18 @@ impl Default for KeyBindings { messages: - PrintAppStateAndQuit + "?": + help: global help menu + messages: + - Call: + command: bash + args: + - -c + - | + echo -e "${XPLR_GLOBAL_HELP_MENU}" + echo + read -p "[enter to continue]" + ctrl-c: help: cancel & quit [q|esc] messages: @@ -360,8 +380,9 @@ impl Default for KeyBindings { let on_number = Some(Action { help: Some("input".to_string()), messages: vec![ - ExternalMsg::BufferStringFromKey, + ExternalMsg::ResetInputBuffer, ExternalMsg::SwitchMode("number".into()), + ExternalMsg::BufferInputFromKey, ], }); @@ -389,6 +410,71 @@ pub struct Mode { pub key_bindings: KeyBindings, } +impl Mode { + pub fn help_menu(&self) -> Vec { + let extra_help_lines = self.extra_help.clone().map(|e| { + e.lines() + .map(|l| HelpMenuLine::Paragraph(l.into())) + .collect::>() + }); + + self.help + .clone() + .map(|h| { + h.lines() + .map(|l| HelpMenuLine::Paragraph(l.into())) + .collect() + }) + .unwrap_or_else(|| { + extra_help_lines + .unwrap_or_default() + .into_iter() + .chain(self.key_bindings.on_key.iter().filter_map(|(k, a)| { + a.help + .clone() + .map(|h| HelpMenuLine::KeyMap(k.into(), h.into())) + })) + .chain( + self.key_bindings + .on_alphabet + .iter() + .map(|a| ("[a-Z]", a.help.clone())) + .filter_map(|(k, mh)| { + mh.map(|h| HelpMenuLine::KeyMap(k.into(), h.into())) + }), + ) + .chain( + self.key_bindings + .on_number + .iter() + .map(|a| ("[0-9]", a.help.clone())) + .filter_map(|(k, mh)| { + mh.map(|h| HelpMenuLine::KeyMap(k.into(), h.into())) + }), + ) + .chain( + self.key_bindings + .on_special_character + .iter() + .map(|a| ("[spcl chars]", a.help.clone())) + .filter_map(|(k, mh)| { + mh.map(|h| HelpMenuLine::KeyMap(k.into(), h.into())) + }), + ) + .chain( + self.key_bindings + .default + .iter() + .map(|a| ("[default]", a.help.clone())) + .filter_map(|(k, mh)| { + mh.map(|h| HelpMenuLine::KeyMap(k.into(), h.into())) + }), + ) + .collect() + }) + } +} + impl Default for Mode { fn default() -> Self { Self { @@ -426,6 +512,7 @@ impl Default for Config { messages: - ResetNodeFilters - SwitchMode: default + - Explore up: help: up @@ -440,29 +527,32 @@ impl Default for Config { right: help: enter messages: - - Enter - - ResetInputBuffer - ResetNodeFilters + - Enter - SwitchMode: default + - Explore left: help: back messages: + - ResetNodeFilters - Back - - ResetInputBuffer - SwitchMode: default + - Explore esc: help: cancel messages: - ResetNodeFilters - SwitchMode: default + - Explore backspace: help: clear messages: - - ResetInputBuffer + - SetInputBuffer: "" - ResetNodeFilters + - Explore ctrl-c: help: cancel & quit @@ -471,10 +561,12 @@ impl Default for Config { default: messages: - - BufferStringFromKey - - AddNodeFilterFromInputString: + - BufferInputFromKey + - ResetNodeFilters + - AddNodeFilterFromInput: filter: RelativePathDoesContain case_sensitive: false + - Explore "###, ) .unwrap(); @@ -526,8 +618,8 @@ impl Default for Config { - Explore - SwitchMode: default - n: - help: create new + c: + help: create messages: - SwitchMode: create @@ -536,6 +628,18 @@ impl Default for Config { messages: - SwitchMode: selection ops + l: + help: logs + messages: + - Call: + command: bash + args: + - -c + - | + echo -e "$XPLR_LOGS" + read -p "[enter to continue]" + - SwitchMode: default + ctrl-c: help: cancel & quit [q] messages: @@ -566,11 +670,15 @@ impl Default for Config { - -c - | (while IFS= read -r line; do - cp -v "${line:?}" ./ + if cp -v "${line:?}" ./; then + echo "LogSuccess: $line copied to $PWD" >> "${XPLR_PIPE_MSG_IN:?}" + else + echo "LogError: failed to copy $line to $PWD" >> "${XPLR_PIPE_MSG_IN:?}" + fi done <<< "${XPLR_SELECTION:?}") + echo Explore >> "${XPLR_PIPE_MSG_IN:?}" + echo ClearSelection >> "${XPLR_PIPE_MSG_IN:?}" read -p "[enter to continue]" - - ClearSelection - - Explore - SwitchMode: default m: @@ -582,10 +690,14 @@ impl Default for Config { - -c - | (while IFS= read -r line; do - mv -v "${line:?}" ./ + if mv -v "${line:?}" ./; then + echo "LogSuccess: $line moved to $PWD" >> "${XPLR_PIPE_MSG_IN:?}" + else + echo "LogError: failed to move $line to $PWD" >> "${XPLR_PIPE_MSG_IN:?}" + fi done <<< "${XPLR_SELECTION:?}") + echo Explore >> "${XPLR_PIPE_MSG_IN:?}" read -p "[enter to continue]" - - Explore - SwitchMode: default ctrl-c: @@ -646,7 +758,7 @@ impl Default for Config { on_number: help: input messages: - - BufferStringFromKey + - BufferInputFromKey default: messages: @@ -658,6 +770,40 @@ impl Default for Config { let create_mode: Mode = serde_yaml::from_str( r###" name: create + key_bindings: + on_key: + f: + help: create file + messages: + - SwitchMode: create file + - SetInputBuffer: "" + + d: + help: create directory + messages: + - SwitchMode: create directory + - SetInputBuffer: "" + + esc: + help: cancel + messages: + - SwitchMode: default + + ctrl-c: + help: cancel & quit + messages: + - Terminate + + default: + messages: + - SwitchMode: default + "###, + ) + .unwrap(); + + let create_file_mode: Mode = serde_yaml::from_str( + r###" + name: create file key_bindings: on_key: enter: @@ -668,11 +814,44 @@ impl Default for Config { args: - -c - | - touch "${XPLR_INPUT_BUFFER:?}" + PTH="${XPLR_INPUT_BUFFER:?}" + if touch "${PTH:?}"; then + echo "LogSuccess: $PTH created" >> "${XPLR_PIPE_MSG_IN:?}" + echo Explore >> "${XPLR_PIPE_MSG_IN:?}" + else + echo "LogError: failed to create $PTH" >> "${XPLR_PIPE_MSG_IN:?}" + echo Refresh >> "${XPLR_PIPE_MSG_IN:?}" + fi - SwitchMode: default - - Explore - ctrl-d: + backspace: + help: clear + messages: + - SetInputBuffer: "" + + esc: + help: cancel + messages: + - SwitchMode: default + + ctrl-c: + help: cancel & quit + messages: + - Terminate + + default: + messages: + - BufferInputFromKey + "###, + ) + .unwrap(); + + let create_dir_mode: Mode = serde_yaml::from_str( + r###" + name: create directory + key_bindings: + on_key: + enter: help: create directory messages: - Call: @@ -680,14 +859,19 @@ impl Default for Config { args: - -c - | - mkdir -p "${XPLR_INPUT_BUFFER:?}" + PTH="${XPLR_INPUT_BUFFER:?}" + if mkdir -p "$PTH"; then + echo Explore >> "${XPLR_PIPE_MSG_IN:?}" + echo "LogSuccess: $PTH created" >> "${XPLR_PIPE_MSG_IN:?}" + else + echo "LogError: failed to create $PTH" >> "${XPLR_PIPE_MSG_IN:?}" + fi - SwitchMode: default - - Explore backspace: help: clear messages: - - ResetInputBuffer + - SetInputBuffer: "" esc: help: cancel @@ -701,7 +885,7 @@ impl Default for Config { default: messages: - - BufferStringFromKey + - BufferInputFromKey "###, ) .unwrap(); @@ -721,14 +905,22 @@ impl Default for Config { - | (while IFS= read -r line; do if [ -d "$line" ]; then - rmdir -v "${line:?}" + if rmdir -v "${line:?}"; then + echo "LogSuccess: $line deleted" >> "${XPLR_PIPE_MSG_IN:?}" + else + echo "LogError: failed to delete $line" >> "${XPLR_PIPE_MSG_IN:?}" + fi else - rm -v "${line:?}" + if rm -v "${line:?}"; then + echo "LogSuccess: $line deleted" >> "${XPLR_PIPE_MSG_IN:?}" + else + echo "LogError: failed to delete $line" >> "${XPLR_PIPE_MSG_IN:?}" + fi fi done <<< "${XPLR_RESULT:?}") + echo Explore >> "${XPLR_PIPE_MSG_IN:?}" read -p "[enter to continue]" - SwitchMode: default - - Explore D: help: force delete @@ -738,7 +930,14 @@ impl Default for Config { args: - -c - | - (echo -e "${XPLR_RESULT:?}" | xargs -l rm -rfv) + (while IFS= read -r line; do + if rm -rfv "${line:?}"; then + echo "LogSuccess: $line deleted" >> "${XPLR_PIPE_MSG_IN:?}" + else + echo "LogError: failed to delete $line" >> "${XPLR_PIPE_MSG_IN:?}" + fi + done <<< "${XPLR_RESULT:?}") + echo Explore >> "${XPLR_PIPE_MSG_IN:?}" read -p "[enter to continue]" - SwitchMode: default - Explore @@ -760,6 +959,8 @@ impl Default for Config { modes.insert("go to".into(), goto_mode); modes.insert("number".into(), number_mode); modes.insert("create".into(), create_mode); + modes.insert("create file".into(), create_file_mode); + modes.insert("create directory".into(), create_dir_mode); modes.insert("delete".into(), delete_mode); modes.insert("action".into(), action_mode); modes.insert("search".into(), search_mode); diff --git a/src/event_reader.rs b/src/event_reader.rs index e6adb25..9708385 100644 --- a/src/event_reader.rs +++ b/src/event_reader.rs @@ -15,18 +15,27 @@ pub fn keep_reading(tx_msg_in: Sender, rx_event_reader: Receiver) { if !is_paused { if event::poll(std::time::Duration::from_millis(1)).unwrap() { - match event::read().unwrap() { - Event::Key(key) => { + match event::read() { + Ok(Event::Key(key)) => { let key = Key::from_event(key); let msg = MsgIn::Internal(InternalMsg::HandleKey(key)); tx_msg_in.send(Task::new(0, msg, Some(key))).unwrap(); } - Event::Resize(_, _) => { + Ok(Event::Resize(_, _)) => { let msg = MsgIn::External(ExternalMsg::Refresh); tx_msg_in.send(Task::new(0, msg, None)).unwrap(); } - _ => {} + Ok(_) => {} + Err(e) => { + tx_msg_in + .send(Task::new( + 0, + MsgIn::External(ExternalMsg::LogError(e.to_string())), + None, + )) + .unwrap(); + } } } } diff --git a/src/main.rs b/src/main.rs index e559901..8494408 100644 --- a/src/main.rs +++ b/src/main.rs @@ -170,6 +170,35 @@ fn main() -> Result<()> { }) .unwrap_or_default(); + let logs = app + .logs() + .iter() + .map(|l| l.to_string()) + .collect::>() + .join("\n"); + + let help_menu = app + .config() + .modes + .iter() + .map(|(name, mode)| { + let help = mode + .help_menu() + .iter() + .map(|l| match l { + app::HelpMenuLine::Paragraph(p) => format!("\t{}", p), + app::HelpMenuLine::KeyMap(k, h) => { + format!(" {:15} | {}", k, h) + } + }) + .collect::>() + .join("\n"); + + format!("### {}\n\n key | action\n --------------- | ------\n{}", name, help) + }) + .collect::>() + .join("\n\n\n"); + let pipe_msg_in = app.pipe().msg_in.clone(); let pipe_focus_out = app.pipe().focus_out.clone(); let pipe_selection_out = app.pipe().selection_out.clone(); @@ -191,7 +220,9 @@ fn main() -> Result<()> { .env("XPLR_PIPE_FOCUS_OUT", pipe_focus_out) .env("XPLR_APP_YAML", app_yaml) .env("XPLR_RESULT", result) + .env("XPLR_GLOBAL_HELP_MENU", help_menu) .env("XPLR_DIRECTORY_NODES", directory_nodes) + .env("XPLR_LOGS", logs) .args(cmd.args.clone()) .status(); diff --git a/src/pipe_reader.rs b/src/pipe_reader.rs index c984819..2af63e2 100644 --- a/src/pipe_reader.rs +++ b/src/pipe_reader.rs @@ -12,10 +12,20 @@ pub fn keep_reading(pipe: String, tx: Sender) { if !in_str.is_empty() { let msgs = in_str .lines() - .filter_map(|s| serde_yaml::from_str::(s.trim()).ok()); + .map(|s| serde_yaml::from_str::(s.trim())); - msgs.for_each(|msg| { - tx.send(Task::new(2, MsgIn::External(msg), None)).unwrap(); + msgs.for_each(|msg| match msg { + Ok(m) => { + tx.send(Task::new(2, MsgIn::External(m), None)).unwrap(); + } + Err(e) => { + tx.send(Task::new( + 0, + MsgIn::External(ExternalMsg::LogError(e.to_string())), + None, + )) + .unwrap(); + } }); fs::write(&pipe, "").unwrap(); }; diff --git a/src/ui.rs b/src/ui.rs index 6f74da0..e761edd 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,10 +1,12 @@ use crate::app; +use crate::app::HelpMenuLine; use crate::app::Node; use handlebars::Handlebars; use serde::{Deserialize, Serialize}; use tui::backend::Backend; use tui::layout::Rect; use tui::layout::{Constraint as TUIConstraint, Direction, Layout}; +use tui::style::{Color, Style}; use tui::widgets::{ Block, Borders, Cell, List, ListItem, ListState, Paragraph, Row, Table, TableState, }; @@ -275,83 +277,21 @@ fn draw_selection(f: &mut Frame, rect: Rect, app: &app::App, _: & } fn draw_help_menu(f: &mut Frame, rect: Rect, app: &app::App, _: &Handlebars) { - // Help menu - let mode = app.mode(); - let extra_help_lines = mode - .extra_help - .clone() - .map(|e| e.lines().map(|l| l.to_string()).collect::>()); - - let help_menu_rows: Vec = mode - .help - .clone() - .map(|h| { - h.lines() - .map(|l| Row::new(vec![Cell::from(l.to_string())])) - .collect() + let help_menu_rows = app + .mode() + .help_menu() + .into_iter() + .map(|l| match l { + HelpMenuLine::Paragraph(p) => Row::new([Cell::from(p)].to_vec()), + HelpMenuLine::KeyMap(k, h) => Row::new([Cell::from(k), Cell::from(h)].to_vec()), }) - .unwrap_or_else(|| { - extra_help_lines - .unwrap_or_default() - .into_iter() - .map(|l| Row::new(vec![Cell::from(l)])) - .chain(mode.key_bindings.on_key.iter().filter_map(|(k, a)| { - a.help.clone().map(|h| { - Row::new(vec![Cell::from(k.to_string()), Cell::from(h.to_string())]) - }) - })) - .chain( - mode.key_bindings - .on_alphabet - .iter() - .map(|a| ("a-Z", a.help.clone())) - .filter_map(|(k, mh)| { - mh.map(|h| { - Row::new(vec![Cell::from(k.to_string()), Cell::from(h.to_string())]) - }) - }), - ) - .chain( - mode.key_bindings - .on_number - .iter() - .map(|a| ("0-9", a.help.clone())) - .filter_map(|(k, mh)| { - mh.map(|h| { - Row::new(vec![Cell::from(k.to_string()), Cell::from(h.to_string())]) - }) - }), - ) - .chain( - mode.key_bindings - .on_special_character - .iter() - .map(|a| ("spcl chars", a.help.clone())) - .filter_map(|(k, mh)| { - mh.map(|h| { - Row::new(vec![Cell::from(k.to_string()), Cell::from(h.to_string())]) - }) - }), - ) - .chain( - mode.key_bindings - .default - .iter() - .map(|a| ("default", a.help.clone())) - .filter_map(|(k, mh)| { - mh.map(|h| { - Row::new(vec![Cell::from(k.to_string()), Cell::from(h.to_string())]) - }) - }), - ) - .collect::>() - }); + .collect::>(); let help_menu = Table::new(help_menu_rows) .block( Block::default() .borders(Borders::ALL) - .title(format!(" Help [{}] ", &mode.name)), + .title(format!(" Help [{}] ", &app.mode().name)), ) .widths(&[TUIConstraint::Percentage(30), TUIConstraint::Percentage(70)]); f.render_widget(help_menu, rect); @@ -363,6 +303,31 @@ fn draw_input_buffer(f: &mut Frame, rect: Rect, app: &app::App, _ f.render_widget(input_buf, rect); } +fn draw_logs(f: &mut Frame, rect: Rect, app: &app::App, _: &Handlebars) { + let logs = app + .logs() + .iter() + .rev() + .take(1) + .rev() + .map(|l| match &l.level { + app::LogLevel::Info => { + ListItem::new(l.to_string()).style(Style::default().fg(Color::Gray)) + } + app::LogLevel::Success => { + ListItem::new(l.to_string()).style(Style::default().fg(Color::Green)) + } + app::LogLevel::Error => { + ListItem::new(l.to_string()).style(Style::default().fg(Color::Red)) + } + }) + .collect::>(); + + let logs_list = List::new(logs).block(Block::default().borders(Borders::ALL).title(" Logs ")); + + f.render_widget(logs_list, rect); +} + pub fn draw(f: &mut Frame, app: &app::App, hb: &Handlebars) { let rect = f.size(); @@ -382,13 +347,19 @@ pub fn draw(f: &mut Frame, app: &app::App, hb: &Handlebars) { ) .split(chunks[0]); + draw_table(f, left_chunks[0], app, hb); + + if app.input_buffer().is_some() { + draw_input_buffer(f, left_chunks[1], app, hb); + } else { + draw_logs(f, left_chunks[1], app, hb); + }; + let right_chunks = Layout::default() .direction(Direction::Vertical) .constraints([TUIConstraint::Percentage(50), TUIConstraint::Percentage(50)].as_ref()) .split(chunks[1]); - draw_table(f, left_chunks[0], app, hb); - draw_input_buffer(f, left_chunks[1], app, hb); draw_selection(f, right_chunks[0], app, hb); draw_help_menu(f, right_chunks[1], app, hb); } diff --git a/tests/test_task.rs b/tests/test_task.rs new file mode 100644 index 0000000..a67f5cc --- /dev/null +++ b/tests/test_task.rs @@ -0,0 +1,29 @@ +use std::collections::BinaryHeap; +use xplr::*; + +#[test] +fn test_task_priority() { + let task1 = app::Task::new(2, app::MsgIn::External(app::ExternalMsg::Refresh), None); + let task2 = app::Task::new(2, app::MsgIn::External(app::ExternalMsg::Refresh), None); + let task3 = app::Task::new(1, app::MsgIn::External(app::ExternalMsg::Refresh), None); + let task4 = app::Task::new(1, app::MsgIn::External(app::ExternalMsg::Refresh), None); + let task5 = app::Task::new(3, app::MsgIn::External(app::ExternalMsg::Refresh), None); + let task6 = app::Task::new(3, app::MsgIn::External(app::ExternalMsg::Refresh), None); + + let mut heap = BinaryHeap::new(); + + heap.push(task1.clone()); + heap.push(task2.clone()); + heap.push(task3.clone()); + heap.push(task4.clone()); + heap.push(task5.clone()); + heap.push(task6.clone()); + + assert_eq!(heap.pop(), Some(task3)); + assert_eq!(heap.pop(), Some(task4)); + assert_eq!(heap.pop(), Some(task1)); + assert_eq!(heap.pop(), Some(task2)); + assert_eq!(heap.pop(), Some(task5)); + assert_eq!(heap.pop(), Some(task6)); + assert_eq!(heap.pop(), None); +}