From dcc203931ac2582ba14336fa37d64b72fd70c507 Mon Sep 17 00:00:00 2001 From: Luis Alberto Santos Date: Tue, 16 May 2023 18:28:41 +0200 Subject: [PATCH] allow to edit commands, including alias --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 6 +- intelli-shell.ps1 | 2 +- intelli-shell.sh | 4 +- src/common/process.rs | 12 +- src/common/widget/command.rs | 13 +- src/common/widget/label.rs | 4 +- src/common/widget/text.rs | 9 +- src/lib.rs | 1 + src/main.rs | 26 ++-- src/process/edit.rs | 247 +++++++++++++++++++++++++++++++++++ src/process/label.rs | 4 + src/process/mod.rs | 4 +- src/process/save.rs | 160 ----------------------- src/process/search.rs | 23 +++- src/theme.rs | 15 ++- 17 files changed, 333 insertions(+), 201 deletions(-) create mode 100644 src/process/edit.rs delete mode 100644 src/process/save.rs diff --git a/Cargo.lock b/Cargo.lock index 675953a..18750c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -418,7 +418,7 @@ dependencies = [ [[package]] name = "intelli-shell" -version = "0.2.3" +version = "0.2.4" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 022db9d..496f78a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "intelli-shell" description = "Like IntelliSense, but for shells" -version = "0.2.3" +version = "0.2.4" edition = "2021" license = "Apache-2.0" readme = "README.md" diff --git a/README.md b/README.md index 249d0ca..7854089 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ It currently works on Bash, Zsh and Fish and should be compatible with most Linu - Autocomplete currently typed command - Full Text Search in both command and description with hashtag support on descriptions - Find & replace labels of currently typed command +- Edit bookmarked commands and provide aliases - Non-intrusive (inline) and full-screen interfaces - Fetch command to parse and store [tldr](https://github.com/tldr-pages/tldr) pages (Thanks to them!) - Portability. You can use bookmarked commands in any supported shell, as well as exporting and importing elsewhere. @@ -133,14 +134,15 @@ You can view supported actions by running `intelli-shell -h`. Most used standalo - `ctrl + l` replace labels of currently typed command - `esc` clean current line, this binding can be skipped if `INTELLI_SKIP_ESC_BIND=1` -**Note:** When navigating items, selected suggestion can be deleted with `ctrl + d` +**Note:** When navigating items, selected suggestion can be deleted with `ctrl + d` or edited with any of: `ctrl + e`, +`ctrl + u` or `F2` You can customize key bindings using environment variables: `INTELLI_BOOKMARK_HOTKEY`, `INTELLI_SEARCH_HOTKEY` and `INTELLI_LABEL_HOTKEY` ## Wishlist - [x] Labels support to store most used labels and select them using a dedicated UI -- [ ] Usability improvements to manage stored commands (including aliases) +- [x] Usability improvements to manage stored commands (including aliases) - [x] Support for more terminals - [x] [Fish](https://fishshell.com/) - [x] PowerShell diff --git a/intelli-shell.ps1 b/intelli-shell.ps1 index c384126..3168fce 100644 --- a/intelli-shell.ps1 +++ b/intelli-shell.ps1 @@ -29,7 +29,7 @@ Set-PSReadLineKeyHandler -Chord $IntelliBookmarkChord -BriefDescription "Intelli $TempFile = New-TemporaryFile $line = $line -replace '"','""""""""""""' - $Command = 'intelli-shell.exe --file-output=""""' + $TempFile.FullName + '"""" save """"' + $line + '""""' + $Command = 'intelli-shell.exe --file-output=""""' + $TempFile.FullName + '"""" new -c """"' + $line + '""""' Start-Process powershell.exe -Wait -NoNewWindow -ArgumentList "-command ""$Command""" -RedirectStandardError "NUL" $IntelliOutput = Get-Content -Raw $TempFile Remove-Item $TempFile diff --git a/intelli-shell.sh b/intelli-shell.sh index 398eaad..8f62b96 100755 --- a/intelli-shell.sh +++ b/intelli-shell.sh @@ -34,7 +34,7 @@ if [[ -n "$ZSH_VERSION" ]]; then } function _intelli_save { - _intelli_exec save "$BUFFER" + _intelli_exec new -c "$BUFFER" } function _intelli_label { @@ -78,7 +78,7 @@ elif [[ -n "$BASH" ]]; then } function _intelli_save { - _intelli_exec save "$READLINE_LINE" + _intelli_exec new -c "$READLINE_LINE" } function _intelli_label { diff --git a/src/common/process.rs b/src/common/process.rs index 69ed592..a38701b 100644 --- a/src/common/process.rs +++ b/src/common/process.rs @@ -113,12 +113,8 @@ pub trait InteractiveProcess: Process { // `ctrl + d` - Delete KeyCode::Char(c) if has_ctrl && c == 'd' => self.delete_current()?, // `ctrl + u` | `ctrl + e` | F2 - Edit / Update - KeyCode::F(f) if f == 2 => { - // TODO edit - delegate to process? - } - KeyCode::Char(c) if has_ctrl && (c == 'e' || c == 'u') => { - // TODO edit - } + KeyCode::F(f) if f == 2 => self.edit_current()?, + KeyCode::Char(c) if has_ctrl && (c == 'e' || c == 'u') => self.edit_current()?, // Selection KeyCode::Home => self.home(), KeyCode::End => self.end(), @@ -171,8 +167,10 @@ pub trait InteractiveProcess: Process { /// Removes a character from the currently selected input, if any fn delete_char(&mut self, backspace: bool) -> Result<()>; - /// Deleted the currently selected item, if any + /// Deletes the currently selected item, if any fn delete_current(&mut self) -> Result<()>; + /// Edits the currently selected item, if any + fn edit_current(&mut self) -> Result<()>; /// Accepts the currently selected item, if any fn accept_current(&mut self) -> Result>; /// Exits with the current state diff --git a/src/common/widget/command.rs b/src/common/widget/command.rs index 2b685b0..7e77200 100644 --- a/src/common/widget/command.rs +++ b/src/common/widget/command.rs @@ -9,11 +9,14 @@ use crate::{model::Command, theme::Theme}; impl<'a> IntoWidget> for &'a Command { fn into_widget(self, theme: Theme) -> ListItem<'a> { - let content = Spans::from(vec![ + let mut content = vec![ Span::raw(&self.cmd), - Span::styled(" # ", Style::default().fg(theme.secondary)), - Span::styled(&self.description, Style::default().fg(theme.secondary)), - ]); - ListItem::new(content) + Span::styled(" # ", Style::default().fg(theme.description)), + Span::styled(&self.description, Style::default().fg(theme.description)), + ]; + if let Some(alias) = &self.alias { + content.insert(0, Span::styled(format!("[{alias}] "), Style::default().fg(theme.alias))) + } + ListItem::new(Spans::from(content)) } } diff --git a/src/common/widget/label.rs b/src/common/widget/label.rs index a3ae652..9106290 100644 --- a/src/common/widget/label.rs +++ b/src/common/widget/label.rs @@ -46,7 +46,7 @@ impl<'a> IntoCursorWidget> for &'a LabeledCommand { .map(|p| { let span = match p { CommandPart::Text(t) | CommandPart::LabelValue(t) => { - Span::styled(t, Style::default().fg(theme.disabled)) + Span::styled(t, Style::default().fg(theme.secondary)) } CommandPart::Label(l) => { let style = if !first_label_found { @@ -54,7 +54,7 @@ impl<'a> IntoCursorWidget> for &'a LabeledCommand { first_label_width = l.len_chars() as u16 + 4; Style::default().fg(theme.main).add_modifier(Modifier::BOLD) } else { - Style::default().fg(theme.disabled) + Style::default().fg(theme.secondary) }; Span::styled(format!("{{{{{l}}}}}"), style) } diff --git a/src/common/widget/text.rs b/src/common/widget/text.rs index 6be9977..721cb79 100644 --- a/src/common/widget/text.rs +++ b/src/common/widget/text.rs @@ -62,6 +62,11 @@ where self } + pub fn set_focus(&mut self, focus: bool) -> &mut Self { + self.focus = focus; + self + } + pub fn inner(&self) -> &T { &self.text } @@ -136,7 +141,9 @@ where paragraph = paragraph.block(block); // Remove borders from max width & height max_width -= 2; - max_height -= 2; + if max_height > 2 { + max_height -= 2; + } // Shift offset because of borders if let (Some(cursor), Some(end_offset)) = (cursor.as_mut(), end_offset.as_mut()) { cursor.x += 1; diff --git a/src/lib.rs b/src/lib.rs index a7eea92..957b40c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,7 @@ //! - Autocomplete currently typed command //! - Full Text Search in both command and description with hashtag support on descriptions //! - Find & replace labels of currently typed command +//! - Edit bookmarked commands and provide aliases //! - Non-intrusive (inline) and full-screen interfaces //! - Fetch command to parse and store [tldr](https://github.com/tldr-pages/tldr) pages (Thanks to them!) //! - Portability. You can use bookmarked commands in any supported shell, as well as exporting and importing elsewhere. diff --git a/src/main.rs b/src/main.rs index 923fd05..84ee839 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,8 +15,8 @@ use crossterm::{ QueueableCommand, }; use intelli_shell::{ - model::AsLabeledCommand, - process::{LabelProcess, SaveCommandProcess, SearchProcess}, + model::{AsLabeledCommand, Command}, + process::{EditCommandProcess, LabelProcess, SearchProcess}, remove_newlines, storage::{SqliteStorage, USER_CATEGORY}, theme, ExecutionContext, Process, ProcessOutput, @@ -49,10 +49,11 @@ struct Args { #[derive(Subcommand)] #[cfg_attr(debug_assertions, derive(Debug))] enum Actions { - /// Saves a new user command - Save { + /// Stores a new user command + New { /// Command to be stored - command: String, + #[arg(short, long)] + command: Option, #[arg(short, long)] /// Description of the command @@ -123,11 +124,16 @@ fn run(cli: Args) -> Result<()> { // Execute command let res = match cli.action { - Actions::Save { command, description } => exec( - cli.inline, - cli.inline_extra_line, - SaveCommandProcess::new(&storage, remove_newlines(command), description, context), - ), + Actions::New { command, description } => { + let cmd = command.map(remove_newlines); + let description = description.map(remove_newlines); + let command = Command::new(USER_CATEGORY, cmd.unwrap_or_default(), description.unwrap_or_default()); + exec( + cli.inline, + cli.inline_extra_line, + EditCommandProcess::new(&storage, command, context)?, + ) + } Actions::Search { filter } => exec( cli.inline, cli.inline_extra_line, diff --git a/src/process/edit.rs b/src/process/edit.rs new file mode 100644 index 0000000..47b843b --- /dev/null +++ b/src/process/edit.rs @@ -0,0 +1,247 @@ +use anyhow::Result; +use crossterm::event::Event; +use tui::{ + backend::Backend, + layout::{Constraint, Direction, Layout, Rect}, + style::Style, + Frame, +}; + +use crate::{ + common::{ + widget::{CustomParagraph, CustomWidget, TextInput}, + ExecutionContext, InteractiveProcess, + }, + model::Command, + storage::SqliteStorage, + Process, ProcessOutput, +}; + +/// Process to edit a [Command] +pub struct EditCommandProcess<'s> { + /// Storage + storage: &'s SqliteStorage, + /// Initial command + command: Command, + /// Command alias + alias: CustomParagraph, + /// Command itself + cmd: CustomParagraph, + /// Command description + description: CustomParagraph, + /// Kind of field currently active + active_field_kind: ActiveFieldKind, + /// Execution context + ctx: ExecutionContext, +} + +pub enum ActiveFieldKind { + Alias, + Command, + Description, +} + +impl<'s> EditCommandProcess<'s> { + pub fn new(storage: &'s SqliteStorage, command: Command, ctx: ExecutionContext) -> Result { + let active_field_kind = if !command.cmd.is_empty() && command.description.is_empty() { + ActiveFieldKind::Description + } else { + ActiveFieldKind::Command + }; + + let mut alias = CustomParagraph::new(TextInput::new(command.alias.as_deref().unwrap_or_default())) + .inline(ctx.inline) + .inline_title("(alias)") + .block_title("Alias") + .style(Style::default().fg(ctx.theme.secondary)); + + let mut cmd = CustomParagraph::new(TextInput::new(&command.cmd)) + .inline(ctx.inline) + .inline_title("Command:") + .block_title("Command") + .style(Style::default().fg(ctx.theme.main)); + + let mut description = CustomParagraph::new(TextInput::new(&command.description)) + .inline(ctx.inline) + .inline_title("Description:") + .block_title("Description") + .style(Style::default().fg(ctx.theme.main)); + + match active_field_kind { + ActiveFieldKind::Alias => alias.set_focus(true), + ActiveFieldKind::Command => cmd.set_focus(true), + ActiveFieldKind::Description => description.set_focus(true), + }; + + Ok(Self { + storage, + command, + alias, + cmd, + description, + active_field_kind, + ctx, + }) + } + + fn active_input(&mut self) -> &mut CustomParagraph { + match self.active_field_kind { + ActiveFieldKind::Alias => &mut self.alias, + ActiveFieldKind::Command => &mut self.cmd, + ActiveFieldKind::Description => &mut self.description, + } + } + + fn update_focus(&mut self) { + self.alias.set_focus(false); + self.cmd.set_focus(false); + self.description.set_focus(false); + + self.active_input().set_focus(true); + } + + fn finish(&mut self) -> Result { + // Edit command + self.command.alias = if self.alias.inner().as_str().is_empty() { + None + } else { + Some(self.alias.inner().as_str().to_owned()) + }; + self.command.cmd = self.cmd.inner().as_str().to_owned(); + self.command.description = self.description.inner().as_str().to_owned(); + + // Insert / update + Ok(if self.command.is_persisted() { + match self.storage.update_command(&self.command)? { + true => ProcessOutput::new(" -> Command was updated successfully", &self.command.cmd), + false => ProcessOutput::new(" -> Error: Command didn't exist", &self.command.cmd), + } + } else { + match self.storage.insert_command(&mut self.command)? { + true => ProcessOutput::new(" -> Command was saved successfully", &self.command.cmd), + false => ProcessOutput::new(" -> Command already existed, so it was updated", &self.command.cmd), + } + }) + } +} + +impl<'s> Process for EditCommandProcess<'s> { + fn min_height(&self) -> usize { + (self.alias.min_size().height + self.cmd.min_size().height + self.description.min_size().height) as usize + } + + fn peek(&mut self) -> Result> { + if !self.command.is_persisted() && !self.command.cmd.is_empty() && !self.command.description.is_empty() { + Ok(Some(self.finish()?)) + } else { + Ok(None) + } + } + + fn render(&mut self, frame: &mut Frame, area: Rect) { + // Prepare main layout + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(!self.ctx.inline as u16) + .constraints([ + Constraint::Length(self.alias.min_size().height), + Constraint::Length(self.cmd.min_size().height), + Constraint::Length(self.description.min_size().height), + ]) + .split(area); + + let alias_area = chunks[0]; + let command_area = chunks[1]; + let description_area = chunks[2]; + + // Render components + self.alias.render_in(frame, alias_area, self.ctx.theme); + self.cmd.render_in(frame, command_area, self.ctx.theme); + self.description.render_in(frame, description_area, self.ctx.theme); + } + + fn process_raw_event(&mut self, event: Event) -> Result> { + self.process_event(event) + } +} + +impl<'s> InteractiveProcess for EditCommandProcess<'s> { + fn move_up(&mut self) { + self.active_field_kind = match self.active_field_kind { + ActiveFieldKind::Alias => ActiveFieldKind::Description, + ActiveFieldKind::Command => ActiveFieldKind::Alias, + ActiveFieldKind::Description => ActiveFieldKind::Command, + }; + self.update_focus(); + } + + fn move_down(&mut self) { + self.active_field_kind = match self.active_field_kind { + ActiveFieldKind::Alias => ActiveFieldKind::Command, + ActiveFieldKind::Command => ActiveFieldKind::Description, + ActiveFieldKind::Description => ActiveFieldKind::Alias, + }; + self.update_focus(); + } + + fn move_left(&mut self) { + self.active_input().inner_mut().move_left() + } + + fn move_right(&mut self) { + self.active_input().inner_mut().move_right() + } + + fn prev(&mut self) { + self.move_up() + } + + fn next(&mut self) { + self.move_down() + } + + fn home(&mut self) { + self.active_input().inner_mut().move_beginning() + } + + fn end(&mut self) { + self.active_input().inner_mut().move_end() + } + + fn insert_text(&mut self, text: String) -> Result<()> { + self.active_input().inner_mut().insert_text(text); + Ok(()) + } + + fn insert_char(&mut self, c: char) -> Result<()> { + self.active_input().inner_mut().insert_char(c); + Ok(()) + } + + fn delete_char(&mut self, backspace: bool) -> Result<()> { + self.active_input().inner_mut().delete_char(backspace); + Ok(()) + } + + fn edit_current(&mut self) -> Result<()> { + Ok(()) + } + + fn delete_current(&mut self) -> Result<()> { + Ok(()) + } + + fn accept_current(&mut self) -> Result> { + if !self.cmd.inner().as_str().is_empty() && !self.description.inner().as_str().is_empty() { + // Exit after saving the command + Ok(Some(self.finish()?)) + } else { + // Keep waiting for input + Ok(None) + } + } + + fn exit(&mut self) -> Result { + Ok(ProcessOutput::output(self.cmd.inner().as_str())) + } +} diff --git a/src/process/label.rs b/src/process/label.rs index 2454721..e6104fc 100644 --- a/src/process/label.rs +++ b/src/process/label.rs @@ -222,6 +222,10 @@ impl<'s> InteractiveProcess for LabelProcess<'s> { Ok(()) } + fn edit_current(&mut self) -> Result<()> { + Ok(()) + } + fn delete_current(&mut self) -> Result<()> { if let Some(LabelSuggestionItem::Persisted(_)) = self.suggestions.current() { if let Some(LabelSuggestionItem::Persisted(suggestion)) = self.suggestions.delete_current() { diff --git a/src/process/mod.rs b/src/process/mod.rs index 0acae6a..2206b57 100644 --- a/src/process/mod.rs +++ b/src/process/mod.rs @@ -1,11 +1,11 @@ +mod edit; #[cfg(feature = "tldr")] mod fetch; mod label; -mod save; mod search; +pub use edit::*; #[cfg(feature = "tldr")] pub use fetch::*; pub use label::*; -pub use save::*; pub use search::*; diff --git a/src/process/save.rs b/src/process/save.rs deleted file mode 100644 index 479bf7b..0000000 --- a/src/process/save.rs +++ /dev/null @@ -1,160 +0,0 @@ -use anyhow::Result; -use crossterm::event::Event; -use tui::{backend::Backend, layout::Rect, style::Style, Frame}; - -use crate::{ - common::{ - widget::{CustomParagraph, CustomWidget, TextInput}, - ExecutionContext, InteractiveProcess, - }, - model::Command, - storage::{SqliteStorage, USER_CATEGORY}, - Process, ProcessOutput, -}; - -/// Process to save a new [Command] -/// -/// If both command and description are provided upon initialization, this process will show no UI. -/// If the description is missing, it will ask for it. -pub struct SaveCommandProcess<'s> { - /// Storage - storage: &'s SqliteStorage, - /// Command to save - command: String, - /// Provided description of the command - description: Option, - /// Current command description - current_description: CustomParagraph, - // Execution context - ctx: ExecutionContext, -} - -impl<'s> SaveCommandProcess<'s> { - pub fn new( - storage: &'s SqliteStorage, - command: String, - description: Option, - ctx: ExecutionContext, - ) -> Self { - let current_description = CustomParagraph::new(TextInput::default()) - .inline(ctx.inline) - .focus(true) - .inline_title("Description:") - .block_title("Description") - .style(Style::default().fg(ctx.theme.main)); - - Self { - storage, - command, - description, - current_description, - ctx, - } - } - - /// Inserts a new [Command] with provided fields on [USER_CATEGORY] - fn insert_command( - storage: &SqliteStorage, - command: impl Into, - description: impl Into, - ) -> Result { - let cmd = command.into(); - let mut command = Command::new(USER_CATEGORY, cmd, description); - Ok(match storage.insert_command(&mut command)? { - true => ProcessOutput::new(" -> Command was saved successfully", command.cmd), - false => ProcessOutput::new(" -> Command already existed, so it was updated", command.cmd), - }) - } -} - -impl<'s> Process for SaveCommandProcess<'s> { - fn min_height(&self) -> usize { - 5 - } - - fn peek(&mut self) -> Result> { - if self.command.is_empty() { - Ok(Some(ProcessOutput::message(" -> A command must be typed first!"))) - } else { - match &self.description { - Some(d) => Ok(Some(Self::insert_command(self.storage, &self.command, d)?)), - None => Ok(None), - } - } - } - - fn render(&mut self, frame: &mut Frame, area: Rect) { - self.current_description.render_in(frame, area, self.ctx.theme); - } - - fn process_raw_event(&mut self, event: Event) -> Result> { - self.process_event(event) - } -} - -impl<'s> InteractiveProcess for SaveCommandProcess<'s> { - fn move_up(&mut self) { - self.current_description.inner_mut().move_up() - } - - fn move_down(&mut self) { - self.current_description.inner_mut().move_down() - } - - fn move_left(&mut self) { - self.current_description.inner_mut().move_left() - } - - fn move_right(&mut self) { - self.current_description.inner_mut().move_right() - } - - fn prev(&mut self) {} - - fn next(&mut self) {} - - fn home(&mut self) { - self.current_description.inner_mut().move_beginning() - } - - fn end(&mut self) { - self.current_description.inner_mut().move_end() - } - - fn insert_text(&mut self, text: String) -> Result<()> { - self.current_description.inner_mut().insert_text(text); - Ok(()) - } - - fn insert_char(&mut self, c: char) -> Result<()> { - self.current_description.inner_mut().insert_char(c); - Ok(()) - } - - fn delete_char(&mut self, backspace: bool) -> Result<()> { - self.current_description.inner_mut().delete_char(backspace); - Ok(()) - } - - fn delete_current(&mut self) -> Result<()> { - Ok(()) - } - - fn accept_current(&mut self) -> Result> { - if !self.current_description.inner().as_str().is_empty() { - // Exit after saving the command - Ok(Some(Self::insert_command( - self.storage, - &self.command, - self.current_description.inner().as_str(), - )?)) - } else { - // Keep waiting for input - Ok(None) - } - } - - fn exit(&mut self) -> Result { - Ok(ProcessOutput::output(self.command.clone())) - } -} diff --git a/src/process/search.rs b/src/process/search.rs index c65ed2f..b3c49e3 100644 --- a/src/process/search.rs +++ b/src/process/search.rs @@ -7,7 +7,7 @@ use tui::{ Frame, }; -use super::LabelProcess; +use super::{EditCommandProcess, LabelProcess}; use crate::{ common::{ widget::{ @@ -31,6 +31,8 @@ pub struct SearchProcess<'s> { commands: CustomStatefulList, /// Delegate label widget delegate_label: Option>, + /// Delegate edit widget + delegate_edit: Option>, // Execution context ctx: ExecutionContext, } @@ -62,6 +64,7 @@ impl<'s> SearchProcess<'s> { filter, storage, delegate_label: None, + delegate_edit: None, ctx, }) } @@ -107,6 +110,10 @@ impl<'s> Process for SearchProcess<'s> { fn render(&mut self, frame: &mut Frame, area: Rect) { // If there's a delegate active, forward to it + if let Some(delegate) = &mut self.delegate_edit { + delegate.render(frame, area); + return; + } if let Some(delegate) = &mut self.delegate_label { delegate.render(frame, area); return; @@ -133,6 +140,13 @@ impl<'s> Process for SearchProcess<'s> { // If there's a delegate active, forward to it if let Some(delegate) = &mut self.delegate_label { delegate.process_event(event) + } else if let Some(delegate) = &mut self.delegate_edit { + if delegate.process_event(event)?.is_some() { + self.delegate_edit = None; + self.commands + .update_items(self.storage.find_commands(self.filter.inner().as_str())?); + } + Ok(None) } else { self.process_event(event) } @@ -194,6 +208,13 @@ impl<'s> InteractiveProcess for SearchProcess<'s> { Ok(()) } + fn edit_current(&mut self) -> Result<()> { + if let Some(command) = self.commands.current() { + self.delegate_edit = Some(EditCommandProcess::new(self.storage, command.clone(), self.ctx)?); + } + Ok(()) + } + fn delete_current(&mut self) -> Result<()> { if let Some(command) = self.commands.delete_current() { self.storage.delete_command(command.id)?; diff --git a/src/theme.rs b/src/theme.rs index 3e9b60c..d608a7f 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -2,22 +2,25 @@ use tui::style::Color; pub const LIGHT: Theme = Theme { main: Color::Black, - disabled: Color::Gray, + secondary: Color::Gray, selected_background: Color::Gray, - secondary: Color::Rgb(0, 128, 0), + alias: Color::Yellow, + description: Color::Rgb(0, 128, 0), }; pub const DARK: Theme = Theme { main: Color::White, - disabled: Color::Rgb(154, 154, 154), + secondary: Color::Rgb(154, 154, 154), selected_background: Color::Rgb(154, 154, 154), - secondary: Color::Rgb(71, 105, 56), + alias: Color::Yellow, + description: Color::Rgb(71, 105, 56), }; #[derive(Clone, Copy)] pub struct Theme { pub main: Color, - pub disabled: Color, - pub selected_background: Color, pub secondary: Color, + pub selected_background: Color, + pub alias: Color, + pub description: Color, }