Compare commits

...

3 Commits

Author SHA1 Message Date
Manos Pitsidianakis 792fcee954 Add notification history view 2022-10-16 20:21:38 +03:00
Manos Pitsidianakis 4085622a1c components.rs: add perform() method in Component trait 2022-10-16 20:21:38 +03:00
Manos Pitsidianakis 96f9aa8072 conf/shortcuts.rs: add key_slice() method to shortcut structs
Add a new method that returns a static slice of included shortcuts.
2022-10-16 20:21:38 +03:00
30 changed files with 1121 additions and 476 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

@ -23,6 +23,7 @@
Notification handling components.
*/
use std::process::{Command, Stdio};
use std::sync::{Arc, Mutex};
use super::*;
@ -138,6 +139,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 {
@ -177,6 +181,19 @@ impl NotificationCommand {
pub fn new() -> Self {
NotificationCommand {}
}
fn update_xbiff(path: &str) -> Result<()> {
let mut file = std::fs::OpenOptions::new()
.append(true) /* writes will append to a file instead of overwriting previous contents */
.create(true) /* a new file will be created if the file does not yet already exist.*/
.open(path)?;
if file.metadata()?.len() > 128 {
file.set_len(0)?;
} else {
std::io::Write::write_all(&mut file, b"z")?;
}
Ok(())
}
}
impl fmt::Display for NotificationCommand {
@ -193,7 +210,7 @@ impl Component for NotificationCommand {
if context.settings.notifications.enable {
if *kind == Some(NotificationType::NewMail) {
if let Some(ref path) = context.settings.notifications.xbiff_file_path {
if let Err(err) = update_xbiff(path) {
if let Err(err) = Self::update_xbiff(path) {
debug!("Could not update xbiff file: {:?}", &err);
melib::log(format!("Could not update xbiff file: {}.", err), ERROR);
}
@ -274,17 +291,291 @@ 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<()> {
let mut file = std::fs::OpenOptions::new()
.append(true) /* writes will append to a file instead of overwriting previous contents */
.create(true) /* a new file will be created if the file does not yet already exist.*/
.open(path)?;
if file.metadata()?.len() > 128 {
file.set_len(0)?;
} else {
std::io::Write::write_all(&mut file, b"z")?;
}
Ok(())
#[derive(Debug)]
struct NotificationLog {
title: Option<String>,
body: String,
kind: Option<NotificationType>,
}
/// Notification history
#[derive(Debug)]
pub struct NotificationHistory {
history: Arc<Mutex<IndexMap<std::time::Instant, NotificationLog>>>,
last_update: Arc<Mutex<std::time::Instant>>,
id: ComponentId,
}
/// Notification history view
#[derive(Debug)]
pub struct NotificationHistoryView {
theme_default: ThemeAttribute,
history: Arc<Mutex<IndexMap<std::time::Instant, NotificationLog>>>,
last_update: Arc<Mutex<std::time::Instant>>,
my_last_update: std::time::Instant,
cursor_pos: usize,
dirty: bool,
id: ComponentId,
}
impl Default for NotificationHistory {
fn default() -> Self {
Self::new()
}
}
impl NotificationHistory {
pub fn new() -> Self {
NotificationHistory {
history: Arc::new(Mutex::new(IndexMap::default())),
last_update: Arc::new(Mutex::new(std::time::Instant::now())),
id: ComponentId::new_v4(),
}
}
fn new_view(&self, context: &Context) -> NotificationHistoryView {
NotificationHistoryView {
theme_default: crate::conf::value(context, "theme_default"),
history: self.history.clone(),
last_update: self.last_update.clone(),
my_last_update: std::time::Instant::now(),
cursor_pos: 0,
dirty: true,
id: ComponentId::new_v4(),
}
}
}
impl fmt::Display for NotificationHistory {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "")
}
}
impl fmt::Display for NotificationHistoryView {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "notifications")
}
}
impl Component for NotificationHistory {
fn draw(&mut self, _grid: &mut CellBuffer, _area: Area, _context: &mut Context) {}
fn process_event(&mut self, event: &mut UIEvent, _context: &mut Context) -> bool {
if let UIEvent::Notification(ref title, ref body, ref kind) = event {
self.history.lock().unwrap().insert(
std::time::Instant::now(),
NotificationLog {
title: title.clone(),
body: body.to_string(),
kind: *kind,
},
);
*self.last_update.lock().unwrap() = std::time::Instant::now();
}
false
}
fn id(&self) -> ComponentId {
self.id
}
fn is_dirty(&self) -> bool {
false
}
fn set_dirty(&mut self, _value: bool) {}
fn set_id(&mut self, id: ComponentId) {
self.id = id;
}
fn perform(&mut self, action: &str, context: &mut Context) -> Result<()> {
match action {
"clear_history" => {
self.history.lock().unwrap().clear();
*self.last_update.lock().unwrap() = std::time::Instant::now();
Ok(())
}
"open_notification_log" => {
context
.replies
.push_back(UIEvent::Action(Tab(New(Some(Box::new(
self.new_view(context),
))))));
Ok(())
}
_ => Err("No actions available.".into()),
}
}
}
impl Component for NotificationHistoryView {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
if !self.is_dirty() {
return;
}
self.set_dirty(false);
self.my_last_update = std::time::Instant::now();
clear_area(grid, area, self.theme_default);
context.dirty_areas.push_back(area);
/* reserve top row for column headers */
let upper_left = pos_inc(upper_left!(area), (0, 1));
let bottom_right = bottom_right!(area);
if get_y(bottom_right) < get_y(upper_left) {
return;
}
let rows = get_y(bottom_right) - get_y(upper_left) + 1;
let page_no = (self.cursor_pos).wrapping_div(rows);
let top_idx = page_no * rows;
for (i, (instant, log)) in self
.history
.lock()
.unwrap()
.iter()
.rev()
.skip(top_idx)
.enumerate()
{
let (x, _) = write_string_to_grid(
&i.to_string(),
grid,
self.theme_default.fg,
self.theme_default.bg,
self.theme_default.attrs,
(pos_inc(upper_left, (0, i)), bottom_right),
None,
);
let (x, _) = write_string_to_grid(
&format!("{:#?}", instant),
grid,
self.theme_default.fg,
self.theme_default.bg,
self.theme_default.attrs,
(pos_inc(upper_left, (x + 2, i)), bottom_right),
None,
);
let (x, _) = write_string_to_grid(
&format!("{:?}", log.kind),
grid,
self.theme_default.fg,
self.theme_default.bg,
self.theme_default.attrs,
(pos_inc(upper_left, (x + 2, i)), bottom_right),
None,
);
let (x, _) = write_string_to_grid(
log.title.as_deref().unwrap_or_default(),
grid,
self.theme_default.fg,
self.theme_default.bg,
self.theme_default.attrs,
(pos_inc(upper_left, (x + 2, i)), bottom_right),
None,
);
write_string_to_grid(
&log.body,
grid,
self.theme_default.fg,
self.theme_default.bg,
self.theme_default.attrs,
(pos_inc(upper_left, (x + 2, i)), bottom_right),
None,
);
}
}
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
let shortcuts = self.get_shortcuts(context);
match event {
UIEvent::ConfigReload { old_settings: _ } => {
self.theme_default = crate::conf::value(context, "theme_default");
self.set_dirty(true);
}
UIEvent::Input(ref key) if shortcut!(key == shortcuts["general"]["scroll_up"]) => {
let _ret = self.perform("scroll_up", context);
debug_assert!(_ret.is_ok());
return true;
}
UIEvent::Input(ref key) if shortcut!(key == shortcuts["general"]["scroll_down"]) => {
let _ret = self.perform("scroll_down", context);
debug_assert!(_ret.is_ok());
return true;
}
UIEvent::Input(ref key) if shortcut!(key == shortcuts["general"]["scroll_right"]) => {
let _ret = self.perform("scroll_right", context);
debug_assert!(_ret.is_ok());
return true;
}
UIEvent::Input(ref key) if shortcut!(key == shortcuts["general"]["scroll_left"]) => {
let _ret = self.perform("scroll_left", context);
debug_assert!(_ret.is_ok());
return true;
}
_ => {}
}
false
}
fn get_shortcuts(&self, context: &Context) -> ShortcutMaps {
let mut map: ShortcutMaps = Default::default();
let config_map = context.settings.shortcuts.general.key_values();
map.insert("general", config_map);
map
}
fn id(&self) -> ComponentId {
self.id
}
fn is_dirty(&self) -> bool {
*self.last_update.lock().unwrap() > self.my_last_update || self.dirty
}
fn set_dirty(&mut self, value: bool) {
self.dirty = value;
if value {
self.my_last_update = *self.last_update.lock().unwrap();
}
}
fn set_id(&mut self, id: ComponentId) {
self.id = id;
}
fn kill(&mut self, uuid: Uuid, context: &mut Context) {
debug_assert!(uuid == self.id);
context.replies.push_back(UIEvent::Action(Tab(Kill(uuid))));
}
fn perform(&mut self, action: &str, _context: &mut Context) -> Result<()> {
match action {
"scroll_up" | "scroll_down" | "scroll_right" | "scroll_left" => {
if action == "scroll_up" {
self.cursor_pos = self.cursor_pos.saturating_sub(1);
} else if action == "scroll_down" {
self.cursor_pos = std::cmp::min(
self.cursor_pos + 1,
self.history.lock().unwrap().len().saturating_sub(1),
);
}
self.set_dirty(true);
Ok(())
}
_ => Err("No actions available.".into()),
}
}
}

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

@ -86,8 +86,7 @@ macro_rules! shortcut_key_values {
pub struct $name:ident { $($fname:ident |> $fdesc:literal |> $default:expr),* }) => {
$(#[$outer])*
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
#[serde(rename = $cname)]
#[serde(default, deny_unknown_fields, rename = $cname)]
pub struct $name {
$(pub $fname : Key),*
}
@ -100,12 +99,28 @@ macro_rules! shortcut_key_values {
_ => unreachable!()
}
}
/// Returns a hashmap of all shortcuts and their values
pub fn key_values(&self) -> IndexMap<&'static str, Key> {
[
$((stringify!($fname),(self.$fname).clone()),)*
].iter().cloned().collect()
}
/// Returns a slice of all shortcuts.
pub fn key_slice(&self) -> &'static [&'static str] {
use std::sync::Once;
static mut VAL: Vec<&'static str> = vec![];
static INIT: Once = Once::new();
unsafe {
INIT.call_once(|| {
$(VAL.push(stringify!($fname));)*
});
VAL.as_ref()
}
}
}
impl Default for $name {

View File

@ -344,6 +344,9 @@ fn run_app(opt: Opt) -> Result<()> {
state.register_component(Box::new(
components::notifications::NotificationCommand::new(),
));
state.register_component(Box::new(
components::notifications::NotificationHistory::new(),
));
}
let enter_command_mode: Key = state
.context

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