components.rs: add perform() method in Component trait

feature/perform-shortcut
Manos Pitsidianakis 2022-09-22 16:09:30 +03:00
parent 96f9aa8072
commit 4085622a1c
28 changed files with 804 additions and 462 deletions

View File

@ -840,6 +840,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"))],
@ -961,6 +974,7 @@ pub fn parse_command(input: &[u8]) -> Result<Action, MeliError> {
account_action,
print_setting,
toggle_mouse,
do_shortcut,
reload_config,
quit,
))(input)

View File

@ -124,33 +124,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::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;

View File

@ -107,4 +107,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<()>;
}

View File

@ -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())
}
}

View File

@ -638,18 +638,8 @@ impl Component for ContactList {
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Self::DESCRIPTION]["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;
}
@ -657,121 +647,38 @@ impl Component for ContactList {
if shortcut!(key == shortcuts[Self::DESCRIPTION]["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[Self::DESCRIPTION]["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[Self::DESCRIPTION]["next_account"]) =>
{
let amount = if self.cmd_buf.is_empty() {
1
} else if let Ok(amount) = self.cmd_buf.parse::<usize>() {
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[Self::DESCRIPTION]["prev_account"]) =>
{
let amount = if self.cmd_buf.is_empty() {
1
} else if let Ok(amount) = self.cmd_buf.parse::<usize>() {
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)
if shortcut!(k == shortcuts[Self::DESCRIPTION]["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() =>
@ -957,4 +864,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::<usize>() {
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::<usize>() {
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()),
}
}
}

View File

@ -2102,6 +2102,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(

View File

@ -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())
}
}

View File

@ -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)]

View File

@ -1786,6 +1786,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 {

View File

@ -2125,4 +2125,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(())
}
}

View File

@ -1585,4 +1585,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())
}
}

View File

@ -229,4 +229,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())
}
}

View File

@ -1490,4 +1490,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(())
}
}

View File

@ -1519,4 +1519,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(())
}
}

View File

@ -466,4 +466,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())
}
}

View File

@ -1928,60 +1928,29 @@ impl Component for MailView {
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[MailView::DESCRIPTION]["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[MailView::DESCRIPTION]["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[MailView::DESCRIPTION]["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[MailView::DESCRIPTION]["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() => {
@ -1993,76 +1962,13 @@ impl Component for MailView {
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[MailView::DESCRIPTION]["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)
@ -2071,7 +1977,8 @@ impl Component for MailView {
key == shortcuts[MailView::DESCRIPTION]["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(''))
@ -2105,12 +2012,8 @@ impl Component for MailView {
|| self.mode == ViewMode::Source(Source::Raw))
&& shortcut!(key == shortcuts[MailView::DESCRIPTION]["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)
@ -2124,9 +2027,8 @@ impl Component for MailView {
key == shortcuts[MailView::DESCRIPTION]["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)
@ -2134,33 +2036,8 @@ impl Component for MailView {
&& !self.cmd_buf.is_empty()
&& shortcut!(key == shortcuts[MailView::DESCRIPTION]["open_mailcap"]) =>
{
let lidx = self.cmd_buf.parse::<usize>().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)
@ -2168,117 +2045,8 @@ impl Component for MailView {
&& !self.cmd_buf.is_empty()
&& (self.mode == ViewMode::Normal || self.mode == ViewMode::Subview) =>
{
let lidx = self.cmd_buf.parse::<usize>().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, argument) = desktop_exec_to_command(
&command,
p.path.display().to_string(),
false,
);
match Command::new(&exec_cmd)
.arg(&argument)
.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, &argument, 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)
@ -2287,8 +2055,8 @@ impl Component for MailView {
key == shortcuts[MailView::DESCRIPTION]["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)
@ -2296,90 +2064,16 @@ impl Component for MailView {
&& self.mode == ViewMode::Url
&& shortcut!(key == shortcuts[MailView::DESCRIPTION]["go_to_url"]) =>
{
let lidx = self.cmd_buf.parse::<usize>().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[MailView::DESCRIPTION]["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 => {
@ -2778,6 +2472,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::<usize>().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::<usize>().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, argument) = desktop_exec_to_command(
&command,
p.path.display().to_string(),
false,
);
match Command::new(&exec_cmd)
.arg(&argument)
.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, &argument, 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::<usize>().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<()> {

View File

@ -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())
}
}

View File

@ -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)
}
}

View File

@ -1172,4 +1172,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())
}
}

View File

@ -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<()> {

View File

@ -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;}"#;

View File

@ -796,6 +796,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)]
@ -1545,6 +1549,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)]
@ -1627,6 +1639,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 {

View File

@ -419,9 +419,14 @@ impl<T: 'static + PartialEq + Debug + Clone + Sync + Send> 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 {
@ -748,6 +753,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<T: PartialEq + Debug + Clone + Sync + Send, F: 'static + Sync + Send> Selector<T, F> {

View File

@ -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())
}
}

View File

@ -839,4 +839,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())
}
}

View File

@ -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<T: 'static + std::fmt::Debug + Copy + Default + Send + Sync> 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<T: 'static + std::fmt::Debug + Copy + Default + Send + Sync> 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())
}
}

View File

@ -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<MeliError> = 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));
}

View File

@ -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<()> {