Compare commits
3 Commits
master
...
feature/pe
Author | SHA1 | Date |
---|---|---|
Manos Pitsidianakis | 792fcee954 | |
Manos Pitsidianakis | 4085622a1c | |
Manos Pitsidianakis | 96f9aa8072 |
|
@ -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"],
|
{ tags: ["quit"],
|
||||||
desc: "quit meli",
|
desc: "quit meli",
|
||||||
tokens: &[One(Literal("quit"))],
|
tokens: &[One(Literal("quit"))],
|
||||||
|
@ -961,6 +974,7 @@ pub fn parse_command(input: &[u8]) -> Result<Action, MeliError> {
|
||||||
account_action,
|
account_action,
|
||||||
print_setting,
|
print_setting,
|
||||||
toggle_mouse,
|
toggle_mouse,
|
||||||
|
do_shortcut,
|
||||||
reload_config,
|
reload_config,
|
||||||
quit,
|
quit,
|
||||||
))(input)
|
))(input)
|
||||||
|
|
|
@ -124,33 +124,22 @@ pub enum Action {
|
||||||
PrintSetting(String),
|
PrintSetting(String),
|
||||||
ReloadConfiguration,
|
ReloadConfiguration,
|
||||||
ToggleMouse,
|
ToggleMouse,
|
||||||
|
DoShortcut(String),
|
||||||
Quit,
|
Quit,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Action {
|
impl Action {
|
||||||
pub fn needs_confirmation(&self) -> bool {
|
pub fn needs_confirmation(&self) -> bool {
|
||||||
match self {
|
matches!(
|
||||||
Action::Listing(ListingAction::Delete) => true,
|
self,
|
||||||
Action::Listing(_) => false,
|
Action::Listing(ListingAction::Delete)
|
||||||
Action::ViewMailbox(_) => false,
|
| Action::MailingListAction(_)
|
||||||
Action::Sort(_, _) => false,
|
| Action::Mailbox(_, _)
|
||||||
Action::SubSort(_, _) => false,
|
| Action::Quit
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type AccountName = String;
|
pub type AccountName = String;
|
||||||
type MailboxPath = String;
|
pub type MailboxPath = String;
|
||||||
type NewMailboxPath = String;
|
pub type NewMailboxPath = String;
|
||||||
|
|
|
@ -107,4 +107,6 @@ pub trait Component: Display + Debug + Send + Sync {
|
||||||
fn get_status(&self, _context: &Context) -> String {
|
fn get_status(&self, _context: &Context) -> String {
|
||||||
String::new()
|
String::new()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -295,4 +295,8 @@ impl Component for ContactManager {
|
||||||
self.set_dirty(true);
|
self.set_dirty(true);
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> {
|
||||||
|
Err("No actions available.".into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -638,18 +638,8 @@ impl Component for ContactList {
|
||||||
UIEvent::Input(ref key)
|
UIEvent::Input(ref key)
|
||||||
if shortcut!(key == shortcuts[Self::DESCRIPTION]["create_contact"]) =>
|
if shortcut!(key == shortcuts[Self::DESCRIPTION]["create_contact"]) =>
|
||||||
{
|
{
|
||||||
let mut manager = ContactManager::new(context);
|
let _ret = self.perform("create_contact", context);
|
||||||
manager.set_parent_id(self.id);
|
debug_assert!(_ret.is_ok());
|
||||||
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),
|
|
||||||
)));
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -657,121 +647,38 @@ impl Component for ContactList {
|
||||||
if shortcut!(key == shortcuts[Self::DESCRIPTION]["edit_contact"])
|
if shortcut!(key == shortcuts[Self::DESCRIPTION]["edit_contact"])
|
||||||
&& self.length > 0 =>
|
&& self.length > 0 =>
|
||||||
{
|
{
|
||||||
let account = &mut context.accounts[self.account_pos];
|
let _ret = self.perform("edit_contact", context);
|
||||||
let book = &mut account.address_book;
|
debug_assert!(_ret.is_ok());
|
||||||
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),
|
|
||||||
)));
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
UIEvent::Input(ref key)
|
UIEvent::Input(ref key)
|
||||||
if shortcut!(key == shortcuts[Self::DESCRIPTION]["mail_contact"])
|
if shortcut!(key == shortcuts[Self::DESCRIPTION]["mail_contact"])
|
||||||
&& self.length > 0 =>
|
&& self.length > 0 =>
|
||||||
{
|
{
|
||||||
let account = &context.accounts[self.account_pos];
|
let _ret = self.perform("mail_contact", context);
|
||||||
let account_hash = account.hash();
|
debug_assert!(_ret.is_ok());
|
||||||
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))))));
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
UIEvent::Input(ref key)
|
UIEvent::Input(ref key)
|
||||||
if shortcut!(key == shortcuts[Self::DESCRIPTION]["next_account"]) =>
|
if shortcut!(key == shortcuts[Self::DESCRIPTION]["next_account"]) =>
|
||||||
{
|
{
|
||||||
let amount = if self.cmd_buf.is_empty() {
|
let _ret = self.perform("next_account", context);
|
||||||
1
|
debug_assert!(_ret.is_ok());
|
||||||
} 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),
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
UIEvent::Input(ref key)
|
UIEvent::Input(ref key)
|
||||||
if shortcut!(key == shortcuts[Self::DESCRIPTION]["prev_account"]) =>
|
if shortcut!(key == shortcuts[Self::DESCRIPTION]["prev_account"]) =>
|
||||||
{
|
{
|
||||||
let amount = if self.cmd_buf.is_empty() {
|
let _ret = self.perform("prev_account", context);
|
||||||
1
|
debug_assert!(_ret.is_ok());
|
||||||
} 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),
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
UIEvent::Input(ref k)
|
UIEvent::Input(ref k)
|
||||||
if shortcut!(k == shortcuts[Self::DESCRIPTION]["toggle_menu_visibility"]) =>
|
if shortcut!(k == shortcuts[Self::DESCRIPTION]["toggle_menu_visibility"]) =>
|
||||||
{
|
{
|
||||||
self.menu_visibility = !self.menu_visibility;
|
let _ret = self.perform("toggle_menu_visibility", context);
|
||||||
self.set_dirty(true);
|
debug_assert!(_ret.is_ok());
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt(''))
|
UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt(''))
|
||||||
if !self.cmd_buf.is_empty() =>
|
if !self.cmd_buf.is_empty() =>
|
||||||
|
@ -957,4 +864,142 @@ impl Component for ContactList {
|
||||||
context.accounts[self.account_pos].address_book.len()
|
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()),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2102,6 +2102,10 @@ impl Component for Composer {
|
||||||
self.set_dirty(true);
|
self.set_dirty(true);
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> {
|
||||||
|
Err("No actions available.".into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_draft(
|
pub fn send_draft(
|
||||||
|
|
|
@ -309,4 +309,8 @@ impl Component for EditAttachmentsRefMut<'_, '_> {
|
||||||
fn set_id(&mut self, new_id: ComponentId) {
|
fn set_id(&mut self, new_id: ComponentId) {
|
||||||
self.inner.id = new_id;
|
self.inner.id = new_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> {
|
||||||
|
Err("No actions available.".into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -264,6 +264,10 @@ impl Component for KeySelection {
|
||||||
KeySelection::Loaded { ref mut widget, .. } => widget.set_id(new_id),
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
|
@ -1786,6 +1786,10 @@ impl Component for Listing {
|
||||||
MailboxStatus::Failed(_) | MailboxStatus::None => account[&mailbox_hash].status(),
|
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 {
|
impl Listing {
|
||||||
|
|
|
@ -2125,4 +2125,11 @@ impl Component for CompactListing {
|
||||||
fn set_id(&mut self, id: ComponentId) {
|
fn set_id(&mut self, id: ComponentId) {
|
||||||
self.id = id;
|
self.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn perform(&mut self, action: &str, context: &mut Context) -> Result<()> {
|
||||||
|
if self.unfocused() {
|
||||||
|
return self.view.perform(action, context);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1585,4 +1585,11 @@ impl Component for ConversationsListing {
|
||||||
fn set_id(&mut self, id: ComponentId) {
|
fn set_id(&mut self, id: ComponentId) {
|
||||||
self.id = id;
|
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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -229,4 +229,8 @@ impl Component for OfflineListing {
|
||||||
fn set_id(&mut self, id: ComponentId) {
|
fn set_id(&mut self, id: ComponentId) {
|
||||||
self.id = id;
|
self.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> {
|
||||||
|
Err("No actions available.".into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1490,4 +1490,11 @@ impl Component for PlainListing {
|
||||||
fn set_id(&mut self, id: ComponentId) {
|
fn set_id(&mut self, id: ComponentId) {
|
||||||
self.id = id;
|
self.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn perform(&mut self, action: &str, context: &mut Context) -> Result<()> {
|
||||||
|
if self.unfocused() {
|
||||||
|
return self.view.perform(action, context);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1519,4 +1519,13 @@ impl Component for ThreadListing {
|
||||||
fn set_id(&mut self, id: ComponentId) {
|
fn set_id(&mut self, id: ComponentId) {
|
||||||
self.id = id;
|
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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -466,4 +466,8 @@ impl Component for AccountStatus {
|
||||||
fn set_id(&mut self, id: ComponentId) {
|
fn set_id(&mut self, id: ComponentId) {
|
||||||
self.id = id;
|
self.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> {
|
||||||
|
Err("No actions available.".into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1928,60 +1928,29 @@ impl Component for MailView {
|
||||||
UIEvent::Input(ref key)
|
UIEvent::Input(ref key)
|
||||||
if shortcut!(key == shortcuts[MailView::DESCRIPTION]["reply"]) =>
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
UIEvent::Input(ref key)
|
UIEvent::Input(ref key)
|
||||||
if shortcut!(key == shortcuts[MailView::DESCRIPTION]["reply_to_all"]) =>
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
UIEvent::Input(ref key)
|
UIEvent::Input(ref key)
|
||||||
if shortcut!(key == shortcuts[MailView::DESCRIPTION]["reply_to_author"]) =>
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
UIEvent::Input(ref key)
|
UIEvent::Input(ref key)
|
||||||
if shortcut!(key == shortcuts[MailView::DESCRIPTION]["forward"]) =>
|
if shortcut!(key == shortcuts[MailView::DESCRIPTION]["forward"]) =>
|
||||||
{
|
{
|
||||||
match mailbox_settings!(
|
let _ret = self.perform("forward", context);
|
||||||
context[self.coordinates.0][&self.coordinates.1]
|
debug_assert!(_ret.is_ok());
|
||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
UIEvent::FinishedUIDialog(id, ref result) if id == self.id() => {
|
UIEvent::FinishedUIDialog(id, ref result) if id == self.id() => {
|
||||||
|
@ -1993,76 +1962,13 @@ impl Component for MailView {
|
||||||
UIEvent::Input(ref key)
|
UIEvent::Input(ref key)
|
||||||
if shortcut!(key == shortcuts[MailView::DESCRIPTION]["edit"]) =>
|
if shortcut!(key == shortcuts[MailView::DESCRIPTION]["edit"]) =>
|
||||||
{
|
{
|
||||||
let account_hash = self.coordinates.0;
|
let _ret = self.perform("edit", context);
|
||||||
let env_hash = self.coordinates.2;
|
debug_assert!(_ret.is_ok());
|
||||||
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,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
UIEvent::Action(View(ViewAction::AddAddressesToContacts)) => {
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
UIEvent::Input(ref key)
|
UIEvent::Input(ref key)
|
||||||
|
@ -2071,7 +1977,8 @@ impl Component for MailView {
|
||||||
key == shortcuts[MailView::DESCRIPTION]["add_addresses_to_contacts"]
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt(''))
|
UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt(''))
|
||||||
|
@ -2105,12 +2012,8 @@ impl Component for MailView {
|
||||||
|| self.mode == ViewMode::Source(Source::Raw))
|
|| self.mode == ViewMode::Source(Source::Raw))
|
||||||
&& shortcut!(key == shortcuts[MailView::DESCRIPTION]["view_raw_source"]) =>
|
&& shortcut!(key == shortcuts[MailView::DESCRIPTION]["view_raw_source"]) =>
|
||||||
{
|
{
|
||||||
self.mode = match self.mode {
|
let _ret = self.perform("view_raw_source", context);
|
||||||
ViewMode::Source(Source::Decoded) => ViewMode::Source(Source::Raw),
|
debug_assert!(_ret.is_ok());
|
||||||
_ => ViewMode::Source(Source::Decoded),
|
|
||||||
};
|
|
||||||
self.set_dirty(true);
|
|
||||||
self.initialised = false;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
UIEvent::Input(ref key)
|
UIEvent::Input(ref key)
|
||||||
|
@ -2124,9 +2027,8 @@ impl Component for MailView {
|
||||||
key == shortcuts[MailView::DESCRIPTION]["return_to_normal_view"]
|
key == shortcuts[MailView::DESCRIPTION]["return_to_normal_view"]
|
||||||
) =>
|
) =>
|
||||||
{
|
{
|
||||||
self.mode = ViewMode::Normal;
|
let _ret = self.perform("return_to_normal_view", context);
|
||||||
self.set_dirty(true);
|
debug_assert!(_ret.is_ok());
|
||||||
self.initialised = false;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
UIEvent::Input(ref key)
|
UIEvent::Input(ref key)
|
||||||
|
@ -2134,33 +2036,8 @@ impl Component for MailView {
|
||||||
&& !self.cmd_buf.is_empty()
|
&& !self.cmd_buf.is_empty()
|
||||||
&& shortcut!(key == shortcuts[MailView::DESCRIPTION]["open_mailcap"]) =>
|
&& shortcut!(key == shortcuts[MailView::DESCRIPTION]["open_mailcap"]) =>
|
||||||
{
|
{
|
||||||
let lidx = self.cmd_buf.parse::<usize>().unwrap();
|
let _ret = self.perform("open_mailcap", context);
|
||||||
self.cmd_buf.clear();
|
debug_assert!(_ret.is_ok());
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
UIEvent::Input(ref key)
|
UIEvent::Input(ref key)
|
||||||
|
@ -2168,117 +2045,8 @@ impl Component for MailView {
|
||||||
&& !self.cmd_buf.is_empty()
|
&& !self.cmd_buf.is_empty()
|
||||||
&& (self.mode == ViewMode::Normal || self.mode == ViewMode::Subview) =>
|
&& (self.mode == ViewMode::Normal || self.mode == ViewMode::Subview) =>
|
||||||
{
|
{
|
||||||
let lidx = self.cmd_buf.parse::<usize>().unwrap();
|
let _ret = self.perform("open_attachment", context);
|
||||||
self.cmd_buf.clear();
|
debug_assert!(_ret.is_ok());
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
UIEvent::Input(ref key)
|
UIEvent::Input(ref key)
|
||||||
|
@ -2287,8 +2055,8 @@ impl Component for MailView {
|
||||||
key == shortcuts[MailView::DESCRIPTION]["toggle_expand_headers"]
|
key == shortcuts[MailView::DESCRIPTION]["toggle_expand_headers"]
|
||||||
) =>
|
) =>
|
||||||
{
|
{
|
||||||
self.expand_headers = !self.expand_headers;
|
let _ret = self.perform("toggle_expand_headers", context);
|
||||||
self.set_dirty(true);
|
debug_assert!(_ret.is_ok());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
UIEvent::Input(ref key)
|
UIEvent::Input(ref key)
|
||||||
|
@ -2296,90 +2064,16 @@ impl Component for MailView {
|
||||||
&& self.mode == ViewMode::Url
|
&& self.mode == ViewMode::Url
|
||||||
&& shortcut!(key == shortcuts[MailView::DESCRIPTION]["go_to_url"]) =>
|
&& shortcut!(key == shortcuts[MailView::DESCRIPTION]["go_to_url"]) =>
|
||||||
{
|
{
|
||||||
let lidx = self.cmd_buf.parse::<usize>().unwrap();
|
let _ret = self.perform("go_to_url", context);
|
||||||
self.cmd_buf.clear();
|
debug_assert!(_ret.is_ok());
|
||||||
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)),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
UIEvent::Input(ref key)
|
UIEvent::Input(ref key)
|
||||||
if (self.mode == ViewMode::Normal || self.mode == ViewMode::Url)
|
if (self.mode == ViewMode::Normal || self.mode == ViewMode::Url)
|
||||||
&& shortcut!(key == shortcuts[MailView::DESCRIPTION]["toggle_url_mode"]) =>
|
&& shortcut!(key == shortcuts[MailView::DESCRIPTION]["toggle_url_mode"]) =>
|
||||||
{
|
{
|
||||||
match self.mode {
|
let _ret = self.perform("toggle_url_mode", context);
|
||||||
ViewMode::Normal => self.mode = ViewMode::Url,
|
debug_assert!(_ret.is_ok());
|
||||||
ViewMode::Url => self.mode = ViewMode::Normal,
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
self.initialised = false;
|
|
||||||
self.dirty = true;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
UIEvent::EnvelopeRename(old_hash, new_hash) if self.coordinates.2 == old_hash => {
|
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))));
|
.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<()> {
|
fn save_attachment(path: &std::path::Path, bytes: &[u8]) -> Result<()> {
|
||||||
|
|
|
@ -525,11 +525,13 @@ impl Component for EnvelopeView {
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_dirty(&self) -> bool {
|
fn is_dirty(&self) -> bool {
|
||||||
self.dirty
|
self.dirty
|
||||||
|| self.pager.as_ref().map(|p| p.is_dirty()).unwrap_or(false)
|
|| self.pager.as_ref().map(|p| p.is_dirty()).unwrap_or(false)
|
||||||
|| self.subview.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) {
|
fn set_dirty(&mut self, value: bool) {
|
||||||
self.dirty = value;
|
self.dirty = value;
|
||||||
}
|
}
|
||||||
|
@ -548,4 +550,8 @@ impl Component for EnvelopeView {
|
||||||
fn set_id(&mut self, id: ComponentId) {
|
fn set_id(&mut self, id: ComponentId) {
|
||||||
self.id = id;
|
self.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> {
|
||||||
|
Err("No actions available.".into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -135,6 +135,7 @@ impl Component for HtmlView {
|
||||||
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
|
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
|
||||||
self.pager.draw(grid, area, context);
|
self.pager.draw(grid, area, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
|
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
|
||||||
if self.pager.process_event(event, context) {
|
if self.pager.process_event(event, context) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -183,12 +184,15 @@ impl Component for HtmlView {
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_shortcuts(&self, context: &Context) -> ShortcutMaps {
|
fn get_shortcuts(&self, context: &Context) -> ShortcutMaps {
|
||||||
self.pager.get_shortcuts(context)
|
self.pager.get_shortcuts(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_dirty(&self) -> bool {
|
fn is_dirty(&self) -> bool {
|
||||||
self.pager.is_dirty()
|
self.pager.is_dirty()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_dirty(&mut self, value: bool) {
|
fn set_dirty(&mut self, value: bool) {
|
||||||
self.pager.set_dirty(value);
|
self.pager.set_dirty(value);
|
||||||
}
|
}
|
||||||
|
@ -196,7 +200,12 @@ impl Component for HtmlView {
|
||||||
fn id(&self) -> ComponentId {
|
fn id(&self) -> ComponentId {
|
||||||
self.id
|
self.id
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_id(&mut self, id: ComponentId) {
|
fn set_id(&mut self, id: ComponentId) {
|
||||||
self.id = id;
|
self.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn perform(&mut self, action: &str, context: &mut Context) -> Result<()> {
|
||||||
|
self.pager.perform(action, context)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1172,4 +1172,11 @@ impl Component for ThreadView {
|
||||||
.replies
|
.replies
|
||||||
.push_back(UIEvent::Action(Tab(Kill(self.id))));
|
.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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
Notification handling components.
|
Notification handling components.
|
||||||
*/
|
*/
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
@ -138,6 +139,9 @@ mod dbus {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_id(&mut self, _id: ComponentId) {}
|
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 {
|
fn escape_str(s: &str) -> String {
|
||||||
|
@ -177,6 +181,19 @@ impl NotificationCommand {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
NotificationCommand {}
|
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 {
|
impl fmt::Display for NotificationCommand {
|
||||||
|
@ -193,7 +210,7 @@ impl Component for NotificationCommand {
|
||||||
if context.settings.notifications.enable {
|
if context.settings.notifications.enable {
|
||||||
if *kind == Some(NotificationType::NewMail) {
|
if *kind == Some(NotificationType::NewMail) {
|
||||||
if let Some(ref path) = context.settings.notifications.xbiff_file_path {
|
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);
|
debug!("Could not update xbiff file: {:?}", &err);
|
||||||
melib::log(format!("Could not update xbiff file: {}.", err), ERROR);
|
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_dirty(&mut self, _value: bool) {}
|
||||||
fn set_id(&mut self, _id: ComponentId) {}
|
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<()> {
|
#[derive(Debug)]
|
||||||
let mut file = std::fs::OpenOptions::new()
|
struct NotificationLog {
|
||||||
.append(true) /* writes will append to a file instead of overwriting previous contents */
|
title: Option<String>,
|
||||||
.create(true) /* a new file will be created if the file does not yet already exist.*/
|
body: String,
|
||||||
.open(path)?;
|
kind: Option<NotificationType>,
|
||||||
if file.metadata()?.len() > 128 {
|
}
|
||||||
file.set_len(0)?;
|
|
||||||
} else {
|
/// Notification history
|
||||||
std::io::Write::write_all(&mut file, b"z")?;
|
#[derive(Debug)]
|
||||||
}
|
pub struct NotificationHistory {
|
||||||
Ok(())
|
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()),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -436,6 +436,10 @@ impl Component for SVGScreenshotFilter {
|
||||||
ComponentId::nil()
|
ComponentId::nil()
|
||||||
}
|
}
|
||||||
fn set_id(&mut self, _id: ComponentId) {}
|
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;}"#;
|
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;}"#;
|
||||||
|
|
|
@ -796,6 +796,10 @@ impl Component for StatusBar {
|
||||||
fn can_quit_cleanly(&mut self, context: &Context) -> bool {
|
fn can_quit_cleanly(&mut self, context: &Context) -> bool {
|
||||||
self.container.can_quit_cleanly(context)
|
self.container.can_quit_cleanly(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn perform(&mut self, action: &str, context: &mut Context) -> Result<()> {
|
||||||
|
self.container.perform(action, context)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -1545,6 +1549,14 @@ impl Component for Tabbed {
|
||||||
}
|
}
|
||||||
true
|
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)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
@ -1627,6 +1639,10 @@ impl Component for RawBuffer {
|
||||||
fn id(&self) -> ComponentId {
|
fn id(&self) -> ComponentId {
|
||||||
ComponentId::nil()
|
ComponentId::nil()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> {
|
||||||
|
Err("No actions available.".into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RawBuffer {
|
impl RawBuffer {
|
||||||
|
|
|
@ -419,9 +419,14 @@ impl<T: 'static + PartialEq + Debug + Clone + Sync + Send> Component for UIDialo
|
||||||
fn id(&self) -> ComponentId {
|
fn id(&self) -> ComponentId {
|
||||||
self.id
|
self.id
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_id(&mut self, id: ComponentId) {
|
fn set_id(&mut self, id: ComponentId) {
|
||||||
self.id = id;
|
self.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> {
|
||||||
|
Err("No actions available.".into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component for UIConfirmationDialog {
|
impl Component for UIConfirmationDialog {
|
||||||
|
@ -748,6 +753,10 @@ impl Component for UIConfirmationDialog {
|
||||||
fn set_id(&mut self, id: ComponentId) {
|
fn set_id(&mut self, id: ComponentId) {
|
||||||
self.id = id;
|
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> {
|
impl<T: PartialEq + Debug + Clone + Sync + Send, F: 'static + Sync + Send> Selector<T, F> {
|
||||||
|
|
|
@ -110,9 +110,14 @@ impl Component for HSplit {
|
||||||
fn id(&self) -> ComponentId {
|
fn id(&self) -> ComponentId {
|
||||||
self.id
|
self.id
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_id(&mut self, id: ComponentId) {
|
fn set_id(&mut self, id: ComponentId) {
|
||||||
self.id = id;
|
self.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> {
|
||||||
|
Err("No actions available.".into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A vertically split in half container.
|
/// A vertically split in half container.
|
||||||
|
@ -250,7 +255,12 @@ impl Component for VSplit {
|
||||||
fn id(&self) -> ComponentId {
|
fn id(&self) -> ComponentId {
|
||||||
self.id
|
self.id
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_id(&mut self, id: ComponentId) {
|
fn set_id(&mut self, id: ComponentId) {
|
||||||
self.id = id;
|
self.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> {
|
||||||
|
Err("No actions available.".into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -839,4 +839,8 @@ impl Component for Pager {
|
||||||
fn set_id(&mut self, id: ComponentId) {
|
fn set_id(&mut self, id: ComponentId) {
|
||||||
self.id = id;
|
self.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> {
|
||||||
|
Err("No actions available.".into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -366,15 +366,22 @@ impl Component for Field {
|
||||||
self.set_dirty(true);
|
self.set_dirty(true);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_dirty(&self) -> bool {
|
fn is_dirty(&self) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_dirty(&mut self, _value: bool) {}
|
fn set_dirty(&mut self, _value: bool) {}
|
||||||
|
|
||||||
fn id(&self) -> ComponentId {
|
fn id(&self) -> ComponentId {
|
||||||
ComponentId::nil()
|
ComponentId::nil()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_id(&mut self, _id: ComponentId) {}
|
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 {
|
impl fmt::Display for Field {
|
||||||
|
@ -714,9 +721,11 @@ impl<T: 'static + std::fmt::Debug + Copy + Default + Send + Sync> Component for
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_dirty(&self) -> bool {
|
fn is_dirty(&self) -> bool {
|
||||||
self.dirty || self.buttons.is_dirty()
|
self.dirty || self.buttons.is_dirty()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_dirty(&mut self, value: bool) {
|
fn set_dirty(&mut self, value: bool) {
|
||||||
self.dirty = value;
|
self.dirty = value;
|
||||||
self.buttons.set_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 {
|
fn id(&self) -> ComponentId {
|
||||||
self.id
|
self.id
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_id(&mut self, id: ComponentId) {
|
fn set_id(&mut self, id: ComponentId) {
|
||||||
self.id = id;
|
self.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> {
|
||||||
|
Err("No actions available.".into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
|
@ -855,9 +869,11 @@ where
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_dirty(&self) -> bool {
|
fn is_dirty(&self) -> bool {
|
||||||
self.dirty
|
self.dirty
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_dirty(&mut self, value: bool) {
|
fn set_dirty(&mut self, value: bool) {
|
||||||
self.dirty = value;
|
self.dirty = value;
|
||||||
}
|
}
|
||||||
|
@ -865,9 +881,14 @@ where
|
||||||
fn id(&self) -> ComponentId {
|
fn id(&self) -> ComponentId {
|
||||||
self.id
|
self.id
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_id(&mut self, id: ComponentId) {
|
fn set_id(&mut self, id: ComponentId) {
|
||||||
self.id = id;
|
self.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> {
|
||||||
|
Err("No actions available.".into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
|
@ -988,12 +1009,15 @@ impl Component for AutoComplete {
|
||||||
}
|
}
|
||||||
context.dirty_areas.push_back(area);
|
context.dirty_areas.push_back(area);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_event(&mut self, _event: &mut UIEvent, _context: &mut Context) -> bool {
|
fn process_event(&mut self, _event: &mut UIEvent, _context: &mut Context) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_dirty(&self) -> bool {
|
fn is_dirty(&self) -> bool {
|
||||||
self.dirty
|
self.dirty
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_dirty(&mut self, value: bool) {
|
fn set_dirty(&mut self, value: bool) {
|
||||||
self.dirty = value;
|
self.dirty = value;
|
||||||
}
|
}
|
||||||
|
@ -1001,9 +1025,14 @@ impl Component for AutoComplete {
|
||||||
fn id(&self) -> ComponentId {
|
fn id(&self) -> ComponentId {
|
||||||
self.id
|
self.id
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_id(&mut self, id: ComponentId) {
|
fn set_id(&mut self, id: ComponentId) {
|
||||||
self.id = id;
|
self.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> {
|
||||||
|
Err("No actions available.".into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AutoComplete {
|
impl AutoComplete {
|
||||||
|
@ -1454,4 +1483,8 @@ impl Component for ProgressSpinner {
|
||||||
fn set_id(&mut self, id: ComponentId) {
|
fn set_id(&mut self, id: ComponentId) {
|
||||||
self.id = id;
|
self.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> {
|
||||||
|
Err("No actions available.".into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,8 +86,7 @@ macro_rules! shortcut_key_values {
|
||||||
pub struct $name:ident { $($fname:ident |> $fdesc:literal |> $default:expr),* }) => {
|
pub struct $name:ident { $($fname:ident |> $fdesc:literal |> $default:expr),* }) => {
|
||||||
$(#[$outer])*
|
$(#[$outer])*
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(default)]
|
#[serde(default, deny_unknown_fields, rename = $cname)]
|
||||||
#[serde(rename = $cname)]
|
|
||||||
pub struct $name {
|
pub struct $name {
|
||||||
$(pub $fname : Key),*
|
$(pub $fname : Key),*
|
||||||
}
|
}
|
||||||
|
@ -100,12 +99,28 @@ macro_rules! shortcut_key_values {
|
||||||
_ => unreachable!()
|
_ => unreachable!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a hashmap of all shortcuts and their values
|
/// Returns a hashmap of all shortcuts and their values
|
||||||
pub fn key_values(&self) -> IndexMap<&'static str, Key> {
|
pub fn key_values(&self) -> IndexMap<&'static str, Key> {
|
||||||
[
|
[
|
||||||
$((stringify!($fname),(self.$fname).clone()),)*
|
$((stringify!($fname),(self.$fname).clone()),)*
|
||||||
].iter().cloned().collect()
|
].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 {
|
impl Default for $name {
|
||||||
|
|
|
@ -344,6 +344,9 @@ fn run_app(opt: Opt) -> Result<()> {
|
||||||
state.register_component(Box::new(
|
state.register_component(Box::new(
|
||||||
components::notifications::NotificationCommand::new(),
|
components::notifications::NotificationCommand::new(),
|
||||||
));
|
));
|
||||||
|
state.register_component(Box::new(
|
||||||
|
components::notifications::NotificationHistory::new(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
let enter_command_mode: Key = state
|
let enter_command_mode: Key = state
|
||||||
.context
|
.context
|
||||||
|
|
24
src/state.rs
24
src/state.rs
|
@ -971,6 +971,30 @@ impl State {
|
||||||
)))
|
)))
|
||||||
.unwrap();
|
.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 => {
|
v => {
|
||||||
self.rcv_event(UIEvent::Action(v));
|
self.rcv_event(UIEvent::Action(v));
|
||||||
}
|
}
|
||||||
|
|
|
@ -389,6 +389,10 @@ impl Component for EmbedContainer {
|
||||||
fn id(&self) -> ComponentId {
|
fn id(&self) -> ComponentId {
|
||||||
self.id
|
self.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> {
|
||||||
|
Err("No actions available.".into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> std::io::Result<()> {
|
fn main() -> std::io::Result<()> {
|
||||||
|
|
Loading…
Reference in New Issue