diff --git a/src/command.rs b/src/command.rs index 525d69f5..3f711608 100644 --- a/src/command.rs +++ b/src/command.rs @@ -851,6 +851,19 @@ Alternatives(&[to_stream!(One(Literal("add-attachment")), One(Filepath)), to_str } ) }, + { tags: ["do"], + desc: "perform a shortcut", + tokens: &[One(Literal("do"))], + parser:( + fn do_shortcut(input: &[u8]) -> IResult<&[u8], Action> { + let (input, _) = tag("do")(input.trim())?; + let (input, _) = is_a(" ")(input)?; + let (input, shortcut) = map_res(not_line_ending, std::str::from_utf8)(input.trim())?; + let (input, _) = eof(input.trim())?; + Ok((input, DoShortcut(shortcut.to_string()))) + } + ) + }, { tags: ["quit"], desc: "quit meli", tokens: &[One(Literal("quit"))], @@ -952,7 +965,7 @@ fn view(input: &[u8]) -> IResult<&[u8], Action> { ))(input) } -pub fn parse_command(input: &[u8]) -> Result { +fn split_parse_command_1(input: &[u8]) -> IResult<&[u8], Action> { alt(( goto, listing_action, @@ -964,6 +977,11 @@ pub fn parse_command(input: &[u8]) -> Result { printenv, view, compose_action, + ))(input) +} + +fn split_parse_command_2(input: &[u8]) -> IResult<&[u8], Action> { + alt(( create_mailbox, sub_mailbox, unsub_mailbox, @@ -973,11 +991,16 @@ pub fn parse_command(input: &[u8]) -> Result { account_action, print_setting, toggle_mouse, + do_shortcut, reload_config, quit, ))(input) - .map(|(_, v)| v) - .map_err(|err| err.into()) +} + +pub fn parse_command(input: &[u8]) -> Result { + alt((split_parse_command_1, split_parse_command_2))(input) + .map(|(_, v)| v) + .map_err(|err| err.into()) } #[test] diff --git a/src/command/actions.rs b/src/command/actions.rs index a4bdf02c..baf86dc1 100644 --- a/src/command/actions.rs +++ b/src/command/actions.rs @@ -125,34 +125,22 @@ pub enum Action { PrintSetting(String), ReloadConfiguration, ToggleMouse, + DoShortcut(String), Quit, } impl Action { pub fn needs_confirmation(&self) -> bool { - match self { - Action::Listing(ListingAction::Delete) => true, - Action::Listing(_) => false, - Action::ViewMailbox(_) => false, - Action::Sort(_, _) => false, - Action::SubSort(_, _) => false, - Action::Tab(_) => false, - Action::MailingListAction(_) => true, - Action::View(_) => false, - Action::SetEnv(_, _) => false, - Action::PrintEnv(_) => false, - Action::Compose(_) => false, - Action::Mailbox(_, _) => true, - Action::AccountAction(_, _) => false, - Action::PrintSetting(_) => false, - Action::ToggleMouse => false, - Action::ManageMailboxes => false, - Action::Quit => true, - Action::ReloadConfiguration => false, - } + matches!( + self, + Action::Listing(ListingAction::Delete) + | Action::MailingListAction(_) + | Action::Mailbox(_, _) + | Action::Quit + ) } } -type AccountName = String; -type MailboxPath = String; -type NewMailboxPath = String; +pub type AccountName = String; +pub type MailboxPath = String; +pub type NewMailboxPath = String; diff --git a/src/components.rs b/src/components.rs index acb3c7b5..697203a4 100644 --- a/src/components.rs +++ b/src/components.rs @@ -110,4 +110,6 @@ pub trait Component: Display + Debug + Send + Sync { fn get_status(&self, _context: &Context) -> String { String::new() } + + fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()>; } diff --git a/src/components/contacts.rs b/src/components/contacts.rs index e72ab3f2..443d8569 100644 --- a/src/components/contacts.rs +++ b/src/components/contacts.rs @@ -295,4 +295,8 @@ impl Component for ContactManager { self.set_dirty(true); false } + + fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> { + Err("No actions available.".into()) + } } diff --git a/src/components/contacts/contact_list.rs b/src/components/contacts/contact_list.rs index adf3717c..4e3f236f 100644 --- a/src/components/contacts/contact_list.rs +++ b/src/components/contacts/contact_list.rs @@ -637,18 +637,8 @@ impl Component for ContactList { UIEvent::Input(ref key) if shortcut!(key == shortcuts[Shortcuts::CONTACT_LIST]["create_contact"]) => { - let mut manager = ContactManager::new(context); - manager.set_parent_id(self.id); - manager.account_pos = self.account_pos; - - self.mode = ViewMode::View(manager.id()); - self.view = Some(manager); - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate( - ScrollUpdate::End(self.id), - ))); - + let _ret = self.perform("create_contact", context); + debug_assert!(_ret.is_ok()); return true; } @@ -656,114 +646,30 @@ impl Component for ContactList { if shortcut!(key == shortcuts[Shortcuts::CONTACT_LIST]["edit_contact"]) && self.length > 0 => { - let account = &mut context.accounts[self.account_pos]; - let book = &mut account.address_book; - let card = book[&self.id_positions[self.cursor_pos]].clone(); - let mut manager = ContactManager::new(context); - manager.set_parent_id(self.id); - manager.card = card; - manager.account_pos = self.account_pos; - - self.mode = ViewMode::View(manager.id()); - self.view = Some(manager); - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate( - ScrollUpdate::End(self.id), - ))); - + let _ret = self.perform("edit_contact", context); + debug_assert!(_ret.is_ok()); return true; } UIEvent::Input(ref key) if shortcut!(key == shortcuts[Shortcuts::CONTACT_LIST]["mail_contact"]) && self.length > 0 => { - let account = &context.accounts[self.account_pos]; - let account_hash = account.hash(); - let book = &account.address_book; - let card = &book[&self.id_positions[self.cursor_pos]]; - let mut draft: Draft = Draft::default(); - *draft.headers_mut().get_mut("To").unwrap() = - format!("{} <{}>", &card.name(), &card.email()); - let mut composer = Composer::with_account(account_hash, context); - composer.set_draft(draft); - context - .replies - .push_back(UIEvent::Action(Tab(New(Some(Box::new(composer)))))); - + let _ret = self.perform("mail_contact", context); + debug_assert!(_ret.is_ok()); return true; } UIEvent::Input(ref key) if shortcut!(key == shortcuts[Shortcuts::CONTACT_LIST]["next_account"]) => { - let amount = if self.cmd_buf.is_empty() { - 1 - } else if let Ok(amount) = self.cmd_buf.parse::() { - self.cmd_buf.clear(); - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); - amount - } else { - self.cmd_buf.clear(); - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); - return true; - }; - if self.accounts.is_empty() { - return true; - } - if self.account_pos + amount < self.accounts.len() { - self.account_pos += amount; - self.set_dirty(true); - self.initialized = false; - self.cursor_pos = 0; - self.new_cursor_pos = 0; - self.length = 0; - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus( - self.get_status(context), - ))); - } - + let _ret = self.perform("next_account", context); + debug_assert!(_ret.is_ok()); return true; } UIEvent::Input(ref key) if shortcut!(key == shortcuts[Shortcuts::CONTACT_LIST]["prev_account"]) => { - let amount = if self.cmd_buf.is_empty() { - 1 - } else if let Ok(amount) = self.cmd_buf.parse::() { - self.cmd_buf.clear(); - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); - amount - } else { - self.cmd_buf.clear(); - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); - return true; - }; - if self.accounts.is_empty() { - return true; - } - if self.account_pos >= amount { - self.account_pos -= amount; - self.set_dirty(true); - self.cursor_pos = 0; - self.new_cursor_pos = 0; - self.length = 0; - self.initialized = false; - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus( - self.get_status(context), - ))); - } + let _ret = self.perform("prev_account", context); + debug_assert!(_ret.is_ok()); return true; } UIEvent::Input(ref k) @@ -771,8 +677,9 @@ impl Component for ContactList { k == shortcuts[Shortcuts::CONTACT_LIST]["toggle_menu_visibility"] ) => { - self.menu_visibility = !self.menu_visibility; - self.set_dirty(true); + let _ret = self.perform("toggle_menu_visibility", context); + debug_assert!(_ret.is_ok()); + return true; } UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt('')) if !self.cmd_buf.is_empty() => @@ -972,4 +879,142 @@ impl Component for ContactList { context.accounts[self.account_pos].address_book.len() ) } + + fn perform(&mut self, mut action: &str, context: &mut Context) -> Result<()> { + if let Some(stripped) = action.strip_prefix("contact_list.") { + action = stripped; + } + match action { + "scroll_up" => Ok(()), + "scroll_down" => Ok(()), + "create_contact" => { + if self.view.is_none() { + let mut manager = ContactManager::new(context); + manager.set_parent_id(self.id); + manager.account_pos = self.account_pos; + + self.mode = ViewMode::View(manager.id()); + self.view = Some(manager); + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate( + ScrollUpdate::End(self.id), + ))); + } + Ok(()) + } + "edit_contact" => { + if self.length > 0 { + let account = &mut context.accounts[self.account_pos]; + let book = &mut account.address_book; + let card = book[&self.id_positions[self.cursor_pos]].clone(); + let mut manager = ContactManager::new(context); + manager.set_parent_id(self.id); + manager.card = card; + manager.account_pos = self.account_pos; + + self.mode = ViewMode::View(manager.id()); + self.view = Some(manager); + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate( + ScrollUpdate::End(self.id), + ))); + } + Ok(()) + } + "mail_contact" => { + if self.length > 0 { + let account = &context.accounts[self.account_pos]; + let account_hash = account.hash(); + let book = &account.address_book; + let card = &book[&self.id_positions[self.cursor_pos]]; + let mut draft: Draft = Draft::default(); + *draft.headers_mut().get_mut("To").unwrap() = + format!("{} <{}>", &card.name(), &card.email()); + let mut composer = Composer::with_account(account_hash, context); + composer.set_draft(draft); + context + .replies + .push_back(UIEvent::Action(Tab(New(Some(Box::new(composer)))))); + } + Ok(()) + } + "next_account" => { + let amount = if self.cmd_buf.is_empty() { + 1 + } else if let Ok(amount) = self.cmd_buf.parse::() { + self.cmd_buf.clear(); + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); + amount + } else { + self.cmd_buf.clear(); + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); + return Ok(()); + }; + if self.accounts.is_empty() { + return Ok(()); + } + if self.account_pos + amount < self.accounts.len() { + self.account_pos += amount; + self.set_dirty(true); + self.initialized = false; + self.cursor_pos = 0; + self.new_cursor_pos = 0; + self.length = 0; + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus( + self.get_status(context), + ))); + } + + Ok(()) + } + "prev_account" => { + let amount = if self.cmd_buf.is_empty() { + 1 + } else if let Ok(amount) = self.cmd_buf.parse::() { + self.cmd_buf.clear(); + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); + amount + } else { + self.cmd_buf.clear(); + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); + return Ok(()); + }; + if self.accounts.is_empty() { + return Ok(()); + } + if self.account_pos >= amount { + self.account_pos -= amount; + self.set_dirty(true); + self.cursor_pos = 0; + self.new_cursor_pos = 0; + self.length = 0; + self.initialized = false; + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus( + self.get_status(context), + ))); + } + Ok(()) + } + "toggle_menu_visibility" => { + self.menu_visibility = !self.menu_visibility; + self.set_dirty(true); + Ok(()) + } + other => Err(format!("`{}` is not a valid contact list shortcut.", other).into()), + } + } } diff --git a/src/components/mail/compose.rs b/src/components/mail/compose.rs index 62636668..728102be 100644 --- a/src/components/mail/compose.rs +++ b/src/components/mail/compose.rs @@ -2101,6 +2101,10 @@ impl Component for Composer { self.set_dirty(true); false } + + fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> { + Err("No actions available.".into()) + } } pub fn send_draft( diff --git a/src/components/mail/compose/edit_attachments.rs b/src/components/mail/compose/edit_attachments.rs index 9e1a61a8..fc59c94e 100644 --- a/src/components/mail/compose/edit_attachments.rs +++ b/src/components/mail/compose/edit_attachments.rs @@ -309,4 +309,8 @@ impl Component for EditAttachmentsRefMut<'_, '_> { fn set_id(&mut self, new_id: ComponentId) { self.inner.id = new_id; } + + fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> { + Err("No actions available.".into()) + } } diff --git a/src/components/mail/compose/gpg.rs b/src/components/mail/compose/gpg.rs index eaba98f7..68f4e946 100644 --- a/src/components/mail/compose/gpg.rs +++ b/src/components/mail/compose/gpg.rs @@ -264,6 +264,10 @@ impl Component for KeySelection { KeySelection::Loaded { ref mut widget, .. } => widget.set_id(new_id), } } + + fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> { + Err("No actions available.".into()) + } } #[derive(Debug, Clone)] diff --git a/src/components/mail/listing.rs b/src/components/mail/listing.rs index dc0115b5..e50291b8 100644 --- a/src/components/mail/listing.rs +++ b/src/components/mail/listing.rs @@ -1935,6 +1935,10 @@ impl Component for Listing { MailboxStatus::Failed(_) | MailboxStatus::None => account[&mailbox_hash].status(), } } + + fn perform(&mut self, action: &str, context: &mut Context) -> Result<()> { + self.component.perform(action, context) + } } impl Listing { diff --git a/src/components/mail/listing/compact.rs b/src/components/mail/listing/compact.rs index dc476586..bf997dae 100644 --- a/src/components/mail/listing/compact.rs +++ b/src/components/mail/listing/compact.rs @@ -1953,4 +1953,11 @@ impl Component for CompactListing { fn set_id(&mut self, id: ComponentId) { self.id = id; } + + fn perform(&mut self, action: &str, context: &mut Context) -> Result<()> { + if self.unfocused() { + return self.view.perform(action, context); + } + Ok(()) + } } diff --git a/src/components/mail/listing/conversations.rs b/src/components/mail/listing/conversations.rs index c6d8b6c9..7e87251f 100644 --- a/src/components/mail/listing/conversations.rs +++ b/src/components/mail/listing/conversations.rs @@ -1576,4 +1576,11 @@ impl Component for ConversationsListing { fn set_id(&mut self, id: ComponentId) { self.id = id; } + + fn perform(&mut self, action: &str, context: &mut Context) -> Result<()> { + if self.unfocused() { + return self.view.perform(action, context); + } + Err("No actions available.".into()) + } } diff --git a/src/components/mail/listing/offline.rs b/src/components/mail/listing/offline.rs index 3fdcebe7..438506eb 100644 --- a/src/components/mail/listing/offline.rs +++ b/src/components/mail/listing/offline.rs @@ -237,4 +237,8 @@ impl Component for OfflineListing { fn set_id(&mut self, id: ComponentId) { self.id = id; } + + fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> { + Err("No actions available.".into()) + } } diff --git a/src/components/mail/listing/plain.rs b/src/components/mail/listing/plain.rs index 01a0ff48..7bf1e252 100644 --- a/src/components/mail/listing/plain.rs +++ b/src/components/mail/listing/plain.rs @@ -1530,4 +1530,11 @@ impl Component for PlainListing { fn set_id(&mut self, id: ComponentId) { self.id = id; } + + fn perform(&mut self, action: &str, context: &mut Context) -> Result<()> { + if self.unfocused() { + return self.view.perform(action, context); + } + Ok(()) + } } diff --git a/src/components/mail/listing/thread.rs b/src/components/mail/listing/thread.rs index 2f89bc88..d990740a 100644 --- a/src/components/mail/listing/thread.rs +++ b/src/components/mail/listing/thread.rs @@ -1615,4 +1615,13 @@ impl Component for ThreadListing { fn set_id(&mut self, id: ComponentId) { self.id = id; } + + fn perform(&mut self, action: &str, context: &mut Context) -> Result<()> { + if self.unfocused() { + if let Some(p) = self.view.as_mut() { + return p.perform(action, context); + }; + } + Ok(()) + } } diff --git a/src/components/mail/status.rs b/src/components/mail/status.rs index 4748e92f..61fd6be6 100644 --- a/src/components/mail/status.rs +++ b/src/components/mail/status.rs @@ -469,4 +469,8 @@ impl Component for AccountStatus { fn set_id(&mut self, id: ComponentId) { self.id = id; } + + fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> { + Err("No actions available.".into()) + } } diff --git a/src/components/mail/view.rs b/src/components/mail/view.rs index e2090fb4..d9e8704b 100644 --- a/src/components/mail/view.rs +++ b/src/components/mail/view.rs @@ -1927,60 +1927,29 @@ impl Component for MailView { UIEvent::Input(ref key) if shortcut!(key == shortcuts[Shortcuts::ENVELOPE_VIEW]["reply"]) => { - self.perform_action(PendingReplyAction::Reply, context); + let _ret = self.perform("reply", context); + debug_assert!(_ret.is_ok()); return true; } UIEvent::Input(ref key) if shortcut!(key == shortcuts[Shortcuts::ENVELOPE_VIEW]["reply_to_all"]) => { - self.perform_action(PendingReplyAction::ReplyToAll, context); + let _ret = self.perform("reply_to_all", context); + debug_assert!(_ret.is_ok()); return true; } UIEvent::Input(ref key) if shortcut!(key == shortcuts[Shortcuts::ENVELOPE_VIEW]["reply_to_author"]) => { - self.perform_action(PendingReplyAction::ReplyToAuthor, context); + let _ret = self.perform("reply_to_author", context); + debug_assert!(_ret.is_ok()); return true; } UIEvent::Input(ref key) if shortcut!(key == shortcuts[Shortcuts::ENVELOPE_VIEW]["forward"]) => { - match mailbox_settings!( - context[self.coordinates.0][&self.coordinates.1] - .composing - .forward_as_attachment - ) { - f if f.is_ask() => { - let id = self.id; - context.replies.push_back(UIEvent::GlobalUIDialog(Box::new( - UIConfirmationDialog::new( - "How do you want the email to be forwarded?", - vec![ - (true, "inline".to_string()), - (false, "as attachment".to_string()), - ], - true, - Some(Box::new(move |_: ComponentId, result: bool| { - Some(UIEvent::FinishedUIDialog( - id, - Box::new(if result { - PendingReplyAction::ForwardInline - } else { - PendingReplyAction::ForwardAttachment - }), - )) - })), - context, - ), - ))); - } - f if f.is_true() => { - self.perform_action(PendingReplyAction::ForwardAttachment, context); - } - _ => { - self.perform_action(PendingReplyAction::ForwardInline, context); - } - } + let _ret = self.perform("forward", context); + debug_assert!(_ret.is_ok()); return true; } UIEvent::FinishedUIDialog(id, ref result) if id == self.id() => { @@ -1992,76 +1961,13 @@ impl Component for MailView { UIEvent::Input(ref key) if shortcut!(key == shortcuts[Shortcuts::ENVELOPE_VIEW]["edit"]) => { - let account_hash = self.coordinates.0; - let env_hash = self.coordinates.2; - let (sender, mut receiver) = crate::jobs::oneshot::channel(); - let operation = context.accounts[&account_hash].operation(env_hash); - let bytes_job = async move { - let _ = sender.send(operation?.as_bytes()?.await); - Ok(()) - }; - let handle = if context.accounts[&account_hash] - .backend_capabilities - .is_async - { - context.accounts[&account_hash] - .job_executor - .spawn_specialized(bytes_job) - } else { - context.accounts[&account_hash] - .job_executor - .spawn_blocking(bytes_job) - }; - context.accounts[&account_hash].insert_job( - handle.job_id, - crate::conf::accounts::JobRequest::Generic { - name: "fetch envelope".into(), - handle, - on_finish: Some(CallbackFn(Box::new(move |context: &mut Context| { - match receiver.try_recv() { - Err(_) => { /* Job was canceled */ } - Ok(None) => { /* something happened, perhaps a worker thread panicked */ - } - Ok(Some(result)) => { - match result.and_then(|bytes| { - Composer::edit(account_hash, env_hash, &bytes, context) - }) { - Ok(composer) => { - context.replies.push_back(UIEvent::Action(Tab(New(Some( - Box::new(composer), - ))))); - } - Err(err) => { - let err_string = format!( - "Failed to open envelope {}: {}", - context.accounts[&account_hash] - .collection - .envelopes - .read() - .unwrap() - .get(&env_hash) - .map(|env| env.message_id_display()) - .unwrap_or_else(|| "Not found".into()), - err - ); - log(&err_string, ERROR); - context.replies.push_back(UIEvent::Notification( - Some("Failed to open e-mail".to_string()), - err_string, - Some(NotificationType::Error(err.kind)), - )); - } - } - } - } - }))), - logging_level: melib::LoggingLevel::DEBUG, - }, - ); + let _ret = self.perform("edit", context); + debug_assert!(_ret.is_ok()); return true; } UIEvent::Action(View(ViewAction::AddAddressesToContacts)) => { - self.start_contact_selector(context); + let _ret = self.perform("add_addresses_to_contacts", context); + debug_assert!(_ret.is_ok()); return true; } UIEvent::Input(ref key) @@ -2070,7 +1976,8 @@ impl Component for MailView { key == shortcuts[Shortcuts::ENVELOPE_VIEW]["add_addresses_to_contacts"] ) => { - self.start_contact_selector(context); + let _ret = self.perform("add_addresses_to_contacts", context); + debug_assert!(_ret.is_ok()); return true; } UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt('')) @@ -2104,12 +2011,8 @@ impl Component for MailView { || self.mode == ViewMode::Source(Source::Raw)) && shortcut!(key == shortcuts[Shortcuts::ENVELOPE_VIEW]["view_raw_source"]) => { - self.mode = match self.mode { - ViewMode::Source(Source::Decoded) => ViewMode::Source(Source::Raw), - _ => ViewMode::Source(Source::Decoded), - }; - self.set_dirty(true); - self.initialised = false; + let _ret = self.perform("view_raw_source", context); + debug_assert!(_ret.is_ok()); return true; } UIEvent::Input(ref key) @@ -2123,9 +2026,8 @@ impl Component for MailView { key == shortcuts[Shortcuts::ENVELOPE_VIEW]["return_to_normal_view"] ) => { - self.mode = ViewMode::Normal; - self.set_dirty(true); - self.initialised = false; + let _ret = self.perform("return_to_normal_view", context); + debug_assert!(_ret.is_ok()); return true; } UIEvent::Input(ref key) @@ -2133,33 +2035,8 @@ impl Component for MailView { && !self.cmd_buf.is_empty() && shortcut!(key == shortcuts[Shortcuts::ENVELOPE_VIEW]["open_mailcap"]) => { - let lidx = self.cmd_buf.parse::().unwrap(); - self.cmd_buf.clear(); - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); - match self.state { - MailViewState::Error { .. } | MailViewState::LoadingBody { .. } => {} - MailViewState::Loaded { .. } => { - if let Some(attachment) = self.open_attachment(lidx, context) { - if let Ok(()) = - crate::mailcap::MailcapEntry::execute(attachment, context) - { - self.set_dirty(true); - } else { - context.replies.push_back(UIEvent::StatusEvent( - StatusEvent::DisplayMessage(format!( - "no mailcap entry found for {}", - attachment.content_type() - )), - )); - } - } - } - MailViewState::Init { .. } => { - self.init_futures(context); - } - } + let _ret = self.perform("open_mailcap", context); + debug_assert!(_ret.is_ok()); return true; } UIEvent::Input(ref key) @@ -2167,117 +2044,8 @@ impl Component for MailView { && !self.cmd_buf.is_empty() && (self.mode == ViewMode::Normal || self.mode == ViewMode::Subview) => { - let lidx = self.cmd_buf.parse::().unwrap(); - self.cmd_buf.clear(); - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); - match self.state { - MailViewState::Error { .. } | MailViewState::LoadingBody { .. } => {} - MailViewState::Loaded { .. } => { - if let Some(attachment) = self.open_attachment(lidx, context) { - match attachment.content_type() { - ContentType::MessageRfc822 => { - match Mail::new(attachment.body().to_vec(), Some(Flag::SEEN)) { - Ok(wrapper) => { - context.replies.push_back(UIEvent::Action(Tab(New( - Some(Box::new(EnvelopeView::new( - wrapper, - None, - None, - self.coordinates.0, - ))), - )))); - } - Err(e) => { - context.replies.push_back(UIEvent::StatusEvent( - StatusEvent::DisplayMessage(format!("{}", e)), - )); - } - } - } - - ContentType::Text { .. } - | ContentType::PGPSignature - | ContentType::CMSSignature => { - self.mode = ViewMode::Attachment(lidx); - self.initialised = false; - self.dirty = true; - } - ContentType::Multipart { .. } => { - context.replies.push_back(UIEvent::StatusEvent( - StatusEvent::DisplayMessage( - "Multipart attachments are not supported yet." - .to_string(), - ), - )); - } - ContentType::Other { .. } => { - let attachment_type = attachment.mime_type(); - let filename = attachment.filename(); - if let Ok(command) = query_default_app(&attachment_type) { - let p = create_temp_file( - &attachment.decode(Default::default()), - filename.as_deref(), - None, - true, - ); - let exec_cmd = desktop_exec_to_command( - &command, - p.path.display().to_string(), - false, - ); - match Command::new("sh") - .args(&["-c", &exec_cmd]) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - { - Ok(child) => { - context.temp_files.push(p); - context.children.push(child); - } - Err(err) => { - context.replies.push_back(UIEvent::StatusEvent( - StatusEvent::DisplayMessage(format!( - "Failed to start `{}`: {}", - &exec_cmd, err - )), - )); - } - } - } else { - context.replies.push_back(UIEvent::StatusEvent( - StatusEvent::DisplayMessage(if let Some(filename) = filename.as_ref() { - format!( - "Couldn't find a default application for file {} (type {})", - filename, - attachment_type - ) - } else { - format!( - "Couldn't find a default application for type {}", - attachment_type - ) - }), - )); - } - } - ContentType::OctetStream { ref name } => { - context.replies.push_back(UIEvent::StatusEvent( - StatusEvent::DisplayMessage(format!( - "Failed to open {}. application/octet-stream isn't supported yet", - name.as_ref().map(|n| n.as_str()).unwrap_or("file") - )), - )); - } - } - } - } - MailViewState::Init { .. } => { - self.init_futures(context); - } - } + let _ret = self.perform("open_attachment", context); + debug_assert!(_ret.is_ok()); return true; } UIEvent::Input(ref key) @@ -2286,8 +2054,8 @@ impl Component for MailView { key == shortcuts[Shortcuts::ENVELOPE_VIEW]["toggle_expand_headers"] ) => { - self.expand_headers = !self.expand_headers; - self.set_dirty(true); + let _ret = self.perform("toggle_expand_headers", context); + debug_assert!(_ret.is_ok()); return true; } UIEvent::Input(ref key) @@ -2295,90 +2063,16 @@ impl Component for MailView { && self.mode == ViewMode::Url && shortcut!(key == shortcuts[Shortcuts::ENVELOPE_VIEW]["go_to_url"]) => { - let lidx = self.cmd_buf.parse::().unwrap(); - self.cmd_buf.clear(); - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); - match self.state { - MailViewState::Init { .. } => { - self.init_futures(context); - } - MailViewState::Error { .. } | MailViewState::LoadingBody { .. } => {} - MailViewState::Loaded { - body: _, - bytes: _, - display: _, - env: _, - ref body_text, - ref links, - } => { - let (_kind, url) = { - if let Some(l) = links - .get(lidx) - .and_then(|l| Some((l.kind, body_text.get(l.start..l.end)?))) - { - l - } else { - context.replies.push_back(UIEvent::StatusEvent( - StatusEvent::DisplayMessage(format!( - "Link `{}` not found.", - lidx - )), - )); - return true; - } - }; - - let url_launcher = mailbox_settings!( - context[self.coordinates.0][&self.coordinates.1] - .pager - .url_launcher - ) - .as_ref() - .map(|s| s.as_str()) - .unwrap_or( - #[cfg(target_os = "macos")] - { - "open" - }, - #[cfg(not(target_os = "macos"))] - { - "xdg-open" - }, - ); - match Command::new(url_launcher) - .arg(url) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - { - Ok(child) => { - context.children.push(child); - } - Err(err) => { - context.replies.push_back(UIEvent::Notification( - Some(format!("Failed to launch {:?}", url_launcher)), - err.to_string(), - Some(NotificationType::Error(melib::ErrorKind::External)), - )); - } - } - } - } + let _ret = self.perform("go_to_url", context); + debug_assert!(_ret.is_ok()); return true; } UIEvent::Input(ref key) if (self.mode == ViewMode::Normal || self.mode == ViewMode::Url) && shortcut!(key == shortcuts[Shortcuts::ENVELOPE_VIEW]["toggle_url_mode"]) => { - match self.mode { - ViewMode::Normal => self.mode = ViewMode::Url, - ViewMode::Url => self.mode = ViewMode::Normal, - _ => {} - } - self.initialised = false; - self.dirty = true; + let _ret = self.perform("toggle_url_mode", context); + debug_assert!(_ret.is_ok()); return true; } UIEvent::EnvelopeRename(old_hash, new_hash) if self.coordinates.2 == old_hash => { @@ -2777,6 +2471,414 @@ impl Component for MailView { .push_back(UIEvent::Action(Tab(Kill(self.id)))); } } + + fn perform(&mut self, action: &str, context: &mut Context) -> Result<()> { + match action { + "reply" => { + self.perform_action(PendingReplyAction::Reply, context); + } + "reply_to_all" => { + self.perform_action(PendingReplyAction::ReplyToAll, context); + } + "reply_to_author" => { + self.perform_action(PendingReplyAction::ReplyToAuthor, context); + } + "forward" => { + match mailbox_settings!( + context[self.coordinates.0][&self.coordinates.1] + .composing + .forward_as_attachment + ) { + f if f.is_ask() => { + let id = self.id; + context.replies.push_back(UIEvent::GlobalUIDialog(Box::new( + UIConfirmationDialog::new( + "How do you want the email to be forwarded?", + vec![ + (true, "inline".to_string()), + (false, "as attachment".to_string()), + ], + true, + Some(Box::new(move |_: ComponentId, result: bool| { + Some(UIEvent::FinishedUIDialog( + id, + Box::new(if result { + PendingReplyAction::ForwardInline + } else { + PendingReplyAction::ForwardAttachment + }), + )) + })), + context, + ), + ))); + } + f if f.is_true() => { + self.perform_action(PendingReplyAction::ForwardAttachment, context); + } + _ => { + self.perform_action(PendingReplyAction::ForwardInline, context); + } + } + } + "edit" => { + let account_hash = self.coordinates.0; + let env_hash = self.coordinates.2; + let (sender, mut receiver) = crate::jobs::oneshot::channel(); + let operation = context.accounts[&account_hash].operation(env_hash); + let bytes_job = async move { + let _ = sender.send(operation?.as_bytes()?.await); + Ok(()) + }; + let handle = if context.accounts[&account_hash] + .backend_capabilities + .is_async + { + context.accounts[&account_hash] + .job_executor + .spawn_specialized(bytes_job) + } else { + context.accounts[&account_hash] + .job_executor + .spawn_blocking(bytes_job) + }; + context.accounts[&account_hash].insert_job( + handle.job_id, + crate::conf::accounts::JobRequest::Generic { + name: "fetch envelope".into(), + handle, + on_finish: Some(CallbackFn(Box::new(move |context: &mut Context| { + match receiver.try_recv() { + Err(_) => { /* Job was canceled */ } + Ok(None) => { /* something happened, perhaps a worker thread panicked */ + } + Ok(Some(result)) => { + match result.and_then(|bytes| { + Composer::edit(account_hash, env_hash, &bytes, context) + }) { + Ok(composer) => { + context.replies.push_back(UIEvent::Action(Tab(New(Some( + Box::new(composer), + ))))); + } + Err(err) => { + let err_string = format!( + "Failed to open envelope {}: {}", + context.accounts[&account_hash] + .collection + .envelopes + .read() + .unwrap() + .get(&env_hash) + .map(|env| env.message_id_display()) + .unwrap_or_else(|| "Not found".into()), + err + ); + log(&err_string, ERROR); + context.replies.push_back(UIEvent::Notification( + Some("Failed to open e-mail".to_string()), + err_string, + Some(NotificationType::Error(err.kind)), + )); + } + } + } + } + }))), + logging_level: melib::LoggingLevel::DEBUG, + }, + ); + } + "add_addresses_to_contacts" => { + if !self.mode.is_contact_selector() { + self.start_contact_selector(context); + } + } + "view_raw_source" => { + if matches!( + self.mode, + ViewMode::Normal + | ViewMode::Subview + | ViewMode::Source(Source::Decoded) + | ViewMode::Source(Source::Raw) + ) { + self.mode = match self.mode { + ViewMode::Source(Source::Decoded) => ViewMode::Source(Source::Raw), + _ => ViewMode::Source(Source::Decoded), + }; + self.set_dirty(true); + self.initialised = false; + } + } + "return_to_normal_view" => { + if self.mode.is_attachment() + || matches!( + self.mode, + ViewMode::Subview + | ViewMode::Url + | ViewMode::Source(Source::Decoded) + | ViewMode::Source(Source::Raw) + ) + { + self.mode = ViewMode::Normal; + self.set_dirty(true); + self.initialised = false; + } + } + "open_mailcap" => { + if (self.mode == ViewMode::Normal || self.mode == ViewMode::Subview) + && !self.cmd_buf.is_empty() + { + let lidx = self.cmd_buf.parse::().unwrap(); + self.cmd_buf.clear(); + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); + match self.state { + MailViewState::Error { .. } | MailViewState::LoadingBody { .. } => {} + MailViewState::Loaded { .. } => { + if let Some(attachment) = self.open_attachment(lidx, context) { + if let Ok(()) = + crate::mailcap::MailcapEntry::execute(attachment, context) + { + self.set_dirty(true); + } else { + context.replies.push_back(UIEvent::StatusEvent( + StatusEvent::DisplayMessage(format!( + "no mailcap entry found for {}", + attachment.content_type() + )), + )); + } + } + } + MailViewState::Init { .. } => { + self.init_futures(context); + } + } + } + } + "open_attachment" => { + if self.cmd_buf.is_empty() + && (self.mode == ViewMode::Normal || self.mode == ViewMode::Subview) + { + let lidx = self.cmd_buf.parse::().unwrap(); + self.cmd_buf.clear(); + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); + match self.state { + MailViewState::Error { .. } | MailViewState::LoadingBody { .. } => {} + MailViewState::Loaded { .. } => { + if let Some(attachment) = self.open_attachment(lidx, context) { + match attachment.content_type() { + ContentType::MessageRfc822 => { + match Mail::new( + attachment.body().to_vec(), + Some(Flag::SEEN), + ) { + Ok(wrapper) => { + context.replies.push_back(UIEvent::Action(Tab( + New(Some(Box::new(EnvelopeView::new( + wrapper, + None, + None, + self.coordinates.0, + )))), + ))); + } + Err(e) => { + context.replies.push_back(UIEvent::StatusEvent( + StatusEvent::DisplayMessage(format!("{}", e)), + )); + } + } + } + + ContentType::Text { .. } + | ContentType::PGPSignature + | ContentType::CMSSignature => { + self.mode = ViewMode::Attachment(lidx); + self.initialised = false; + self.dirty = true; + } + ContentType::Multipart { .. } => { + context.replies.push_back(UIEvent::StatusEvent( + StatusEvent::DisplayMessage( + "Multipart attachments are not supported yet." + .to_string(), + ), + )); + } + ContentType::Other { .. } => { + let attachment_type = attachment.mime_type(); + let filename = attachment.filename(); + if let Ok(command) = query_default_app(&attachment_type) { + let p = create_temp_file( + &attachment.decode(Default::default()), + filename.as_deref(), + None, + true, + ); + let exec_cmd = desktop_exec_to_command( + &command, + p.path.display().to_string(), + false, + ); + match Command::new("sh") + .args(&["-c", &exec_cmd]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + { + Ok(child) => { + context.temp_files.push(p); + context.children.push(child); + } + Err(err) => { + context.replies.push_back( + UIEvent::StatusEvent( + StatusEvent::DisplayMessage(format!( + "Failed to start `{}`: {}", + &exec_cmd, err + )), + ), + ); + } + } + } else { + context.replies.push_back(UIEvent::StatusEvent( + StatusEvent::DisplayMessage(if let Some(filename) = filename.as_ref() { + format!( + "Couldn't find a default application for file {} (type {})", + filename, + attachment_type + ) + } else { + format!( + "Couldn't find a default application for type {}", + attachment_type + ) + }), + )); + } + } + ContentType::OctetStream { ref name } => { + context.replies.push_back(UIEvent::StatusEvent( + StatusEvent::DisplayMessage(format!( + "Failed to open {}. application/octet-stream isn't supported yet", + name.as_ref().map(|n| n.as_str()).unwrap_or("file") + )), + )); + } + } + } + } + MailViewState::Init { .. } => { + self.init_futures(context); + } + } + } + } + "toggle_expand_headers" => { + if self.mode == ViewMode::Normal || self.mode == ViewMode::Url { + self.expand_headers = !self.expand_headers; + self.set_dirty(true); + } + } + "go_to_url" => { + if !self.cmd_buf.is_empty() && self.mode == ViewMode::Url { + let lidx = self.cmd_buf.parse::().unwrap(); + self.cmd_buf.clear(); + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); + match self.state { + MailViewState::Init { .. } => { + self.init_futures(context); + } + MailViewState::Error { .. } | MailViewState::LoadingBody { .. } => {} + MailViewState::Loaded { + body: _, + bytes: _, + display: _, + env: _, + ref body_text, + ref links, + } => { + let (_kind, url) = { + if let Some(l) = links + .get(lidx) + .and_then(|l| Some((l.kind, body_text.get(l.start..l.end)?))) + { + l + } else { + context.replies.push_back(UIEvent::StatusEvent( + StatusEvent::DisplayMessage(format!( + "Link `{}` not found.", + lidx + )), + )); + return Ok(()); + } + }; + + let url_launcher = mailbox_settings!( + context[self.coordinates.0][&self.coordinates.1] + .pager + .url_launcher + ) + .as_ref() + .map(|s| s.as_str()) + .unwrap_or( + #[cfg(target_os = "macos")] + { + "open" + }, + #[cfg(not(target_os = "macos"))] + { + "xdg-open" + }, + ); + match Command::new(url_launcher) + .arg(url) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + { + Ok(child) => { + context.children.push(child); + } + Err(err) => { + context.replies.push_back(UIEvent::Notification( + Some(format!("Failed to launch {:?}", url_launcher)), + err.to_string(), + Some(NotificationType::Error(melib::ErrorKind::External)), + )); + } + } + } + } + } + } + "toggle_url_mode" => { + if matches!(self.mode, ViewMode::Normal | ViewMode::Url) { + match self.mode { + ViewMode::Normal => self.mode = ViewMode::Url, + ViewMode::Url => self.mode = ViewMode::Normal, + _ => {} + } + self.initialised = false; + self.dirty = true; + } + } + other => { + return Err(format!("Envelope view doesn't have an `{}` action.", other).into()) + } + } + + Ok(()) + } } fn save_attachment(path: &std::path::Path, bytes: &[u8]) -> Result<()> { diff --git a/src/components/mail/view/envelope.rs b/src/components/mail/view/envelope.rs index bb968459..19b884a1 100644 --- a/src/components/mail/view/envelope.rs +++ b/src/components/mail/view/envelope.rs @@ -525,11 +525,13 @@ impl Component for EnvelopeView { } false } + fn is_dirty(&self) -> bool { self.dirty || self.pager.as_ref().map(|p| p.is_dirty()).unwrap_or(false) || self.subview.as_ref().map(|p| p.is_dirty()).unwrap_or(false) } + fn set_dirty(&mut self, value: bool) { self.dirty = value; } @@ -548,4 +550,8 @@ impl Component for EnvelopeView { fn set_id(&mut self, id: ComponentId) { self.id = id; } + + fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> { + Err("No actions available.".into()) + } } diff --git a/src/components/mail/view/html.rs b/src/components/mail/view/html.rs index d5334561..95f96dfd 100644 --- a/src/components/mail/view/html.rs +++ b/src/components/mail/view/html.rs @@ -135,6 +135,7 @@ impl Component for HtmlView { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { self.pager.draw(grid, area, context); } + fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool { if self.pager.process_event(event, context) { return true; @@ -183,12 +184,15 @@ impl Component for HtmlView { } false } + fn get_shortcuts(&self, context: &Context) -> ShortcutMaps { self.pager.get_shortcuts(context) } + fn is_dirty(&self) -> bool { self.pager.is_dirty() } + fn set_dirty(&mut self, value: bool) { self.pager.set_dirty(value); } @@ -196,7 +200,12 @@ impl Component for HtmlView { fn id(&self) -> ComponentId { self.id } + fn set_id(&mut self, id: ComponentId) { self.id = id; } + + fn perform(&mut self, action: &str, context: &mut Context) -> Result<()> { + self.pager.perform(action, context) + } } diff --git a/src/components/mail/view/thread.rs b/src/components/mail/view/thread.rs index d2ce3f4e..a6c8db25 100644 --- a/src/components/mail/view/thread.rs +++ b/src/components/mail/view/thread.rs @@ -1179,4 +1179,11 @@ impl Component for ThreadView { .replies .push_back(UIEvent::Action(Tab(Kill(self.id)))); } + + fn perform(&mut self, action: &str, context: &mut Context) -> Result<()> { + if self.show_mailview { + return self.mailview.perform(action, context); + } + Err("No actions available.".into()) + } } diff --git a/src/components/mailbox_management.rs b/src/components/mailbox_management.rs index 5e8cce5a..9a4cb512 100644 --- a/src/components/mailbox_management.rs +++ b/src/components/mailbox_management.rs @@ -433,53 +433,91 @@ impl Component for MailboxManager { UIEvent::Input(ref key) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_up"]) => { - let amount = 1; - self.movement = Some(PageMovement::Up(amount)); - self.set_dirty(true); + let _ret = self.perform("scroll_up", context); + debug_assert!(_ret.is_ok()); return true; } UIEvent::Input(ref key) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_down"]) && self.cursor_pos < self.length.saturating_sub(1) => { - let amount = 1; - self.set_dirty(true); - self.movement = Some(PageMovement::Down(amount)); + let _ret = self.perform("scroll_down", context); + debug_assert!(_ret.is_ok()); return true; } UIEvent::Input(ref key) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["prev_page"]) => { - let mult = 1; - self.set_dirty(true); - self.movement = Some(PageMovement::PageUp(mult)); + let _ret = self.perform("prev_page", context); + debug_assert!(_ret.is_ok()); return true; } UIEvent::Input(ref key) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["next_page"]) => { - let mult = 1; - self.set_dirty(true); - self.movement = Some(PageMovement::PageDown(mult)); + let _ret = self.perform("next_page", context); + debug_assert!(_ret.is_ok()); return true; } UIEvent::Input(ref key) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["home_page"]) => { - self.set_dirty(true); - self.movement = Some(PageMovement::Home); + let _ret = self.perform("home_page", context); + debug_assert!(_ret.is_ok()); return true; } UIEvent::Input(ref key) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["end_page"]) => { - self.set_dirty(true); - self.movement = Some(PageMovement::End); + let _ret = self.perform("end_page", context); + debug_assert!(_ret.is_ok()); return true; } UIEvent::Input(ref key) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["open_entry"]) => { + let _ret = self.perform("open_entry", context); + debug_assert!(_ret.is_ok()); + return true; + } + _ => {} + } + false + } + + fn perform(&mut self, action: &str, context: &mut Context) -> Result<()> { + match action { + "scroll_up" => { + let amount = 1; + self.movement = Some(PageMovement::Up(amount)); + self.set_dirty(true); + } + "scroll_down" => { + if self.cursor_pos < self.length.saturating_sub(1) { + let amount = 1; + self.set_dirty(true); + self.movement = Some(PageMovement::Down(amount)); + } + } + "prev_page" => { + let mult = 1; + self.set_dirty(true); + self.movement = Some(PageMovement::PageUp(mult)); + } + "next_page" => { + let mult = 1; + self.set_dirty(true); + self.movement = Some(PageMovement::PageDown(mult)); + } + "home_page" => { + self.set_dirty(true); + self.movement = Some(PageMovement::Home); + } + "end_page" => { + self.set_dirty(true); + self.movement = Some(PageMovement::End); + } + "open_entry" => { self.set_dirty(true); self.mode = ViewMode::Action(UIDialog::new( "select action", @@ -497,11 +535,13 @@ impl Component for MailboxManager { )), context, )); - return true; } - _ => {} + other => { + return Err(format!("Mailbox manager doesn't have an `{}` action.", other).into()) + } } - false + + Ok(()) } fn is_dirty(&self) -> bool { diff --git a/src/components/notifications.rs b/src/components/notifications.rs index 3d959a6a..213ef297 100644 --- a/src/components/notifications.rs +++ b/src/components/notifications.rs @@ -138,6 +138,9 @@ mod dbus { } fn set_id(&mut self, _id: ComponentId) {} + fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> { + Err("No actions available.".into()) + } } fn escape_str(s: &str) -> String { @@ -274,6 +277,9 @@ impl Component for NotificationCommand { } fn set_dirty(&mut self, _value: bool) {} fn set_id(&mut self, _id: ComponentId) {} + fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> { + Err("No actions available.".into()) + } } fn update_xbiff(path: &str) -> Result<()> { diff --git a/src/components/svg.rs b/src/components/svg.rs index 048867ff..4ddf0eb5 100644 --- a/src/components/svg.rs +++ b/src/components/svg.rs @@ -436,6 +436,10 @@ impl Component for SVGScreenshotFilter { ComponentId::nil() } fn set_id(&mut self, _id: ComponentId) {} + + fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> { + Err("No actions available.".into()) + } } const CSS_STYLE: &str = r#"#t{font-family:'DejaVu Sans Mono',monospace;font-style:normal;font-size:14px;} text {dominant-baseline: text-before-edge; white-space: pre;} .f{fill:#e5e5e5;} .b{fill:#000;} .c0 {fill:#000;} .c1 {fill:#cd0000;} .c2 {fill:#00cd00;} .c3 {fill:#cdcd00;} .c4 {fill:#00e;} .c5 {fill:#cd00cd;} .c6 {fill:#00cdcd;} .c7 {fill:#e5e5e5;} .c8 {fill:#7f7f7f;} .c9 {fill:#f00;} .c10 {fill:#0f0;} .c11 {fill:#ff0;} .c12 {fill:#5c5cff;} .c13 {fill:#f0f;} .c14 {fill:#0ff;} .c15 {fill:#fff;}"#; diff --git a/src/components/utilities.rs b/src/components/utilities.rs index 848770ed..1cdc4cbe 100644 --- a/src/components/utilities.rs +++ b/src/components/utilities.rs @@ -799,6 +799,10 @@ impl Component for StatusBar { fn can_quit_cleanly(&mut self, context: &Context) -> bool { self.container.can_quit_cleanly(context) } + + fn perform(&mut self, action: &str, context: &mut Context) -> Result<()> { + self.container.perform(action, context) + } } #[derive(Debug)] @@ -1559,6 +1563,14 @@ impl Component for Tabbed { } true } + + fn perform(&mut self, action: &str, context: &mut Context) -> Result<()> { + if !self.children.is_empty() { + self.children[self.cursor_pos].perform(action, context) + } else { + Ok(()) + } + } } #[derive(Debug, Clone, PartialEq, Eq)] @@ -1641,6 +1653,10 @@ impl Component for RawBuffer { fn id(&self) -> ComponentId { ComponentId::nil() } + + fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> { + Err("No actions available.".into()) + } } impl RawBuffer { diff --git a/src/components/utilities/dialogs.rs b/src/components/utilities/dialogs.rs index 2d2e1a69..d15e61a8 100644 --- a/src/components/utilities/dialogs.rs +++ b/src/components/utilities/dialogs.rs @@ -423,9 +423,14 @@ impl Component for UIDialo fn id(&self) -> ComponentId { self.id } + fn set_id(&mut self, id: ComponentId) { self.id = id; } + + fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> { + Err("No actions available.".into()) + } } impl Component for UIConfirmationDialog { @@ -756,6 +761,10 @@ impl Component for UIConfirmationDialog { fn set_id(&mut self, id: ComponentId) { self.id = id; } + + fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> { + Err("No actions available.".into()) + } } impl Selector { diff --git a/src/components/utilities/layouts.rs b/src/components/utilities/layouts.rs index c1b256b8..39e5e71c 100644 --- a/src/components/utilities/layouts.rs +++ b/src/components/utilities/layouts.rs @@ -110,9 +110,14 @@ impl Component for HSplit { fn id(&self) -> ComponentId { self.id } + fn set_id(&mut self, id: ComponentId) { self.id = id; } + + fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> { + Err("No actions available.".into()) + } } /// A vertically split in half container. @@ -250,7 +255,12 @@ impl Component for VSplit { fn id(&self) -> ComponentId { self.id } + fn set_id(&mut self, id: ComponentId) { self.id = id; } + + fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> { + Err("No actions available.".into()) + } } diff --git a/src/components/utilities/pager.rs b/src/components/utilities/pager.rs index cbda1394..425e02ad 100644 --- a/src/components/utilities/pager.rs +++ b/src/components/utilities/pager.rs @@ -843,4 +843,8 @@ impl Component for Pager { fn set_id(&mut self, id: ComponentId) { self.id = id; } + + fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> { + Err("No actions available.".into()) + } } diff --git a/src/components/utilities/widgets.rs b/src/components/utilities/widgets.rs index 682d6b12..60314f85 100644 --- a/src/components/utilities/widgets.rs +++ b/src/components/utilities/widgets.rs @@ -366,15 +366,22 @@ impl Component for Field { self.set_dirty(true); true } + fn is_dirty(&self) -> bool { false } + fn set_dirty(&mut self, _value: bool) {} fn id(&self) -> ComponentId { ComponentId::nil() } + fn set_id(&mut self, _id: ComponentId) {} + + fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> { + Err("No actions available.".into()) + } } impl fmt::Display for Field { @@ -714,9 +721,11 @@ impl Component for } false } + fn is_dirty(&self) -> bool { self.dirty || self.buttons.is_dirty() } + fn set_dirty(&mut self, value: bool) { self.dirty = value; self.buttons.set_dirty(value); @@ -725,9 +734,14 @@ impl Component for fn id(&self) -> ComponentId { self.id } + fn set_id(&mut self, id: ComponentId) { self.id = id; } + + fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> { + Err("No actions available.".into()) + } } #[derive(Debug, Default)] @@ -855,9 +869,11 @@ where } false } + fn is_dirty(&self) -> bool { self.dirty } + fn set_dirty(&mut self, value: bool) { self.dirty = value; } @@ -865,9 +881,14 @@ where fn id(&self) -> ComponentId { self.id } + fn set_id(&mut self, id: ComponentId) { self.id = id; } + + fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> { + Err("No actions available.".into()) + } } #[derive(Debug, PartialEq, Eq, Clone)] @@ -988,12 +1009,15 @@ impl Component for AutoComplete { } context.dirty_areas.push_back(area); } + fn process_event(&mut self, _event: &mut UIEvent, _context: &mut Context) -> bool { false } + fn is_dirty(&self) -> bool { self.dirty } + fn set_dirty(&mut self, value: bool) { self.dirty = value; } @@ -1001,9 +1025,14 @@ impl Component for AutoComplete { fn id(&self) -> ComponentId { self.id } + fn set_id(&mut self, id: ComponentId) { self.id = id; } + + fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> { + Err("No actions available.".into()) + } } impl AutoComplete { @@ -1454,4 +1483,8 @@ impl Component for ProgressSpinner { fn set_id(&mut self, id: ComponentId) { self.id = id; } + + fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> { + Err("No actions available.".into()) + } } diff --git a/src/state.rs b/src/state.rs index e72a4fba..4957bd56 100644 --- a/src/state.rs +++ b/src/state.rs @@ -971,6 +971,30 @@ impl State { ))) .unwrap(); } + DoShortcut(action) => { + let Self { + ref mut components, + ref mut context, + ref mut overlay, + .. + } = self; + let mut failure: Option = None; + for c in overlay.iter_mut().chain(components.iter_mut()) { + if let Err(err) = c.perform(action.as_str(), context) { + failure = Some(err); + } else { + failure = None; + break; + } + } + if let Some(err) = failure { + context.replies.push_back(UIEvent::Notification( + None, + err.to_string(), + Some(NotificationType::Error(ErrorKind::None)), + )); + } + } v => { self.rcv_event(UIEvent::Action(v)); } diff --git a/tools/src/embed.rs b/tools/src/embed.rs index b2a2299a..27aa0a57 100644 --- a/tools/src/embed.rs +++ b/tools/src/embed.rs @@ -389,6 +389,10 @@ impl Component for EmbedContainer { fn id(&self) -> ComponentId { self.id } + + fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> { + Err("No actions available.".into()) + } } fn main() -> std::io::Result<()> {