summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorManos Pitsidianakis <el13635@mail.ntua.gr>2022-11-25 15:15:02 +0200
committerManos Pitsidianakis <el13635@mail.ntua.gr>2022-11-28 15:44:12 +0200
commit7d9cabb023b510e6175fd6b2523f0414a6da1f3f (patch)
tree76fa49bdac28d7a2f9df78b36c032dba14845db1
parentee9d458b05ffa0214a4526daf1423916830526bc (diff)
downloadmeli-7d9cabb023b510e6175fd6b2523f0414a6da1f3f.zip
Add mailbox manager tab
-rw-r--r--src/command.rs12
-rw-r--r--src/command/actions.rs2
-rw-r--r--src/components.rs3
-rw-r--r--src/components/mail/listing.rs8
-rw-r--r--src/components/mailbox_management.rs541
-rw-r--r--src/conf/accounts.rs28
6 files changed, 580 insertions, 14 deletions
diff --git a/src/command.rs b/src/command.rs
index f219641c..525d69f5 100644
--- a/src/command.rs
+++ b/src/command.rs
@@ -840,6 +840,17 @@ Alternatives(&[to_stream!(One(Literal("add-attachment")), One(Filepath)), to_str
}
)
},
+ { tags: ["manage-mailboxes"],
+ desc: "view and manage mailbox preferences",
+ tokens: &[One(Literal("manage-mailboxes"))],
+ parser:(
+ fn manage_mailboxes(input: &[u8]) -> IResult<&[u8], Action> {
+ let (input, _) = tag("manage-mailboxes")(input.trim())?;
+ let (input, _) = eof(input)?;
+ Ok((input, ManageMailboxes))
+ }
+ )
+ },
{ tags: ["quit"],
desc: "quit meli",
tokens: &[One(Literal("quit"))],
@@ -958,6 +969,7 @@ pub fn parse_command(input: &[u8]) -> Result<Action, MeliError> {
unsub_mailbox,
delete_mailbox,
rename_mailbox,
+ manage_mailboxes,
account_action,
print_setting,
toggle_mouse,
diff --git a/src/command/actions.rs b/src/command/actions.rs
index e67441c5..a4bdf02c 100644
--- a/src/command/actions.rs
+++ b/src/command/actions.rs
@@ -120,6 +120,7 @@ pub enum Action {
PrintEnv(String),
Compose(ComposeAction),
Mailbox(AccountName, MailboxOperation),
+ ManageMailboxes,
AccountAction(AccountName, AccountAction),
PrintSetting(String),
ReloadConfiguration,
@@ -145,6 +146,7 @@ impl Action {
Action::AccountAction(_, _) => false,
Action::PrintSetting(_) => false,
Action::ToggleMouse => false,
+ Action::ManageMailboxes => false,
Action::Quit => true,
Action::ReloadConfiguration => false,
}
diff --git a/src/components.rs b/src/components.rs
index ba0b78e0..acb3c7b5 100644
--- a/src/components.rs
+++ b/src/components.rs
@@ -40,6 +40,9 @@ pub use self::utilities::*;
pub mod contacts;
pub use crate::contacts::*;
+pub mod mailbox_management;
+pub use self::mailbox_management::*;
+
#[cfg(feature = "svgscreenshot")]
pub mod svg;
diff --git a/src/components/mail/listing.rs b/src/components/mail/listing.rs
index 72ec4703..8ed9f615 100644
--- a/src/components/mail/listing.rs
+++ b/src/components/mail/listing.rs
@@ -1809,6 +1809,14 @@ impl Component for Listing {
.push_back(UIEvent::Action(Tab(New(Some(Box::new(composer))))));
return true;
}
+ UIEvent::Action(Action::ManageMailboxes) => {
+ let account_pos = self.cursor_pos.0;
+ let mgr = MailboxManager::new(context, account_pos);
+ context
+ .replies
+ .push_back(UIEvent::Action(Tab(New(Some(Box::new(mgr))))));
+ return true;
+ }
UIEvent::StartupCheck(_)
| UIEvent::MailboxUpdate(_)
| UIEvent::EnvelopeUpdate(_)
diff --git a/src/components/mailbox_management.rs b/src/components/mailbox_management.rs
new file mode 100644
index 00000000..ce81babc
--- /dev/null
+++ b/src/components/mailbox_management.rs
@@ -0,0 +1,541 @@
+/*
+ * meli
+ *
+ * Copyright 2019 Manos Pitsidianakis
+ *
+ * This file is part of meli.
+ *
+ * meli is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * meli is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with meli. If not, see <http://www.gnu.org/licenses/>.
+ */
+use super::*;
+use crate::conf::accounts::MailboxEntry;
+use crate::melib::text_processing::TextProcessing;
+use melib::backends::AccountHash;
+
+use std::cmp;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum MailboxAction {
+ Rename,
+ Move,
+ Subscribe,
+ Unsubscribe,
+}
+
+#[derive(Debug, Default, PartialEq)]
+enum ViewMode {
+ #[default]
+ List,
+ Action(UIDialog<MailboxAction>),
+}
+
+#[derive(Debug)]
+pub struct MailboxManager {
+ cursor_pos: usize,
+ new_cursor_pos: usize,
+ account_pos: usize,
+ account_hash: AccountHash,
+ length: usize,
+ data_columns: DataColumns<5>,
+ entries: IndexMap<MailboxHash, MailboxEntry>,
+ mode: ViewMode,
+
+ initialized: bool,
+ theme_default: ThemeAttribute,
+ highlight_theme: ThemeAttribute,
+
+ dirty: bool,
+
+ movement: Option<PageMovement>,
+ id: ComponentId,
+}
+
+impl fmt::Display for MailboxManager {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{}", MailboxManager::DESCRIPTION)
+ }
+}
+
+impl MailboxManager {
+ const DESCRIPTION: &'static str = "mailboxes";
+ pub fn new(context: &Context, account_pos: usize) -> Self {
+ let account_hash = context.accounts[account_pos].hash();
+ let theme_default = crate::conf::value(context, "theme_default");
+ let mut data_columns = DataColumns::default();
+ data_columns.theme_config.set_single_theme(theme_default);
+ MailboxManager {
+ cursor_pos: 0,
+ new_cursor_pos: 0,
+ account_hash,
+ mode: ViewMode::default(),
+ entries: IndexMap::default(),
+ length: 0,
+ account_pos,
+ data_columns,
+ theme_default,
+ highlight_theme: crate::conf::value(context, "highlight"),
+ initialized: false,
+ dirty: true,
+ movement: None,
+ id: ComponentId::new_v4(),
+ }
+ }
+
+ fn initialize(&mut self, context: &mut Context) {
+ let account = &context.accounts[self.account_pos];
+ self.length = account.mailbox_entries.len();
+ self.entries = account.mailbox_entries.clone();
+ self.entries
+ .sort_by(|_, a, _, b| a.ref_mailbox.path().cmp(&b.ref_mailbox.path()));
+
+ self.set_dirty(true);
+ let mut min_width = (
+ "name".len(),
+ "path".len(),
+ "size".len(),
+ "subscribed".len(),
+ 0,
+ 0,
+ );
+
+ for c in self.entries.values() {
+ /* title */
+ min_width.0 = cmp::max(min_width.0, c.name().split_graphemes().len());
+ /* path */
+ min_width.1 = cmp::max(min_width.1, c.ref_mailbox.path().len());
+ }
+
+ /* name column */
+ self.data_columns.columns[0] =
+ CellBuffer::new_with_context(min_width.0, self.length, None, context);
+ /* path column */
+ self.data_columns.columns[1] =
+ CellBuffer::new_with_context(min_width.1, self.length, None, context);
+ /* size column */
+ self.data_columns.columns[2] =
+ CellBuffer::new_with_context(min_width.2, self.length, None, context);
+ /* subscribed column */
+ self.data_columns.columns[3] =
+ CellBuffer::new_with_context(min_width.3, self.length, None, context);
+
+ for (idx, e) in self.entries.values().enumerate() {
+ write_string_to_grid(
+ e.name(),
+ &mut self.data_columns.columns[0],
+ self.theme_default.fg,
+ self.theme_default.bg,
+ self.theme_default.attrs,
+ ((0, idx), (min_width.0, idx)),
+ None,
+ );
+
+ write_string_to_grid(
+ e.ref_mailbox.path(),
+ &mut self.data_columns.columns[1],
+ self.theme_default.fg,
+ self.theme_default.bg,
+ self.theme_default.attrs,
+ ((0, idx), (min_width.1, idx)),
+ None,
+ );
+
+ let (_unseen, total) = e.ref_mailbox.count().ok().unwrap_or((0, 0));
+ write_string_to_grid(
+ &total.to_string(),
+ &mut self.data_columns.columns[2],
+ self.theme_default.fg,
+ self.theme_default.bg,
+ self.theme_default.attrs,
+ ((0, idx), (min_width.2, idx)),
+ None,
+ );
+
+ write_string_to_grid(
+ if e.ref_mailbox.is_subscribed() {
+ "yes"
+ } else {
+ "no"
+ },
+ &mut self.data_columns.columns[3],
+ self.theme_default.fg,
+ self.theme_default.bg,
+ self.theme_default.attrs,
+ ((0, idx), (min_width.3, idx)),
+ None,
+ );
+ }
+
+ if self.length == 0 {
+ let message = "No mailboxes.".to_string();
+ self.data_columns.columns[0] =
+ CellBuffer::new_with_context(message.len(), self.length, None, context);
+ write_string_to_grid(
+ &message,
+ &mut self.data_columns.columns[0],
+ self.theme_default.fg,
+ self.theme_default.bg,
+ self.theme_default.attrs,
+ ((0, 0), (message.len() - 1, 0)),
+ None,
+ );
+ }
+ }
+
+ fn draw_list(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
+ let (upper_left, bottom_right) = area;
+
+ if self.length == 0 {
+ clear_area(grid, area, self.theme_default);
+ copy_area(
+ grid,
+ &self.data_columns.columns[0],
+ area,
+ ((0, 0), pos_dec(self.data_columns.columns[0].size(), (1, 1))),
+ );
+ context.dirty_areas.push_back(area);
+ return;
+ }
+ let rows = get_y(bottom_right) - get_y(upper_left) + 1;
+
+ if let Some(mvm) = self.movement.take() {
+ match mvm {
+ PageMovement::Up(amount) => {
+ self.new_cursor_pos = self.new_cursor_pos.saturating_sub(amount);
+ }
+ PageMovement::PageUp(multiplier) => {
+ self.new_cursor_pos = self.new_cursor_pos.saturating_sub(rows * multiplier);
+ }
+ PageMovement::Down(amount) => {
+ if self.new_cursor_pos + amount < self.length {
+ self.new_cursor_pos += amount;
+ } else {
+ self.new_cursor_pos = self.length - 1;
+ }
+ }
+ PageMovement::PageDown(multiplier) => {
+ #[allow(clippy::comparison_chain)]
+ if self.new_cursor_pos + rows * multiplier < self.length {
+ self.new_cursor_pos += rows * multiplier;
+ } else if self.new_cursor_pos + rows * multiplier > self.length {
+ self.new_cursor_pos = self.length - 1;
+ } else {
+ self.new_cursor_pos = (self.length / rows) * rows;
+ }
+ }
+ PageMovement::Right(_) | PageMovement::Left(_) => {}
+ PageMovement::Home => {
+ self.new_cursor_pos = 0;
+ }
+ PageMovement::End => {
+ self.new_cursor_pos = self.length - 1;
+ }
+ }
+ }
+
+ let prev_page_no = (self.cursor_pos).wrapping_div(rows);
+ let page_no = (self.new_cursor_pos).wrapping_div(rows);
+
+ let top_idx = page_no * rows;
+
+ if self.length >= rows {
+ context
+ .replies
+ .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
+ ScrollUpdate::Update {
+ id: self.id,
+ context: ScrollContext {
+ shown_lines: top_idx + rows,
+ total_lines: self.length,
+ has_more_lines: false,
+ },
+ },
+ )));
+ } else {
+ context
+ .replies
+ .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
+ ScrollUpdate::End(self.id),
+ )));
+ }
+
+ /* If cursor position has changed, remove the highlight from the previous position and
+ * apply it in the new one. */
+ if self.cursor_pos != self.new_cursor_pos && prev_page_no == page_no {
+ let old_cursor_pos = self.cursor_pos;
+ self.cursor_pos = self.new_cursor_pos;
+ for &(idx, highlight) in &[(old_cursor_pos, false), (self.new_cursor_pos, true)] {
+ if idx >= self.length {
+ continue; //bounds check
+ }
+ let new_area = nth_row_area(area, idx % rows);
+ self.data_columns
+ .draw(grid, idx, self.cursor_pos, grid.bounds_iter(new_area));
+ let row_attr = if highlight {
+ self.highlight_theme
+ } else {
+ self.theme_default
+ };
+ change_colors(grid, new_area, row_attr.fg, row_attr.bg);
+ context.dirty_areas.push_back(new_area);
+ }
+ return;
+ } else if self.cursor_pos != self.new_cursor_pos {
+ self.cursor_pos = self.new_cursor_pos;
+ }
+ if self.new_cursor_pos >= self.length {
+ self.new_cursor_pos = self.length - 1;
+ self.cursor_pos = self.new_cursor_pos;
+ }
+ /* Page_no has changed, so draw new page */
+ _ = self
+ .data_columns
+ .recalc_widths((width!(area), height!(area)), top_idx);
+ clear_area(grid, area, self.theme_default);
+ /* copy table columns */
+ self.data_columns
+ .draw(grid, top_idx, self.cursor_pos, grid.bounds_iter(area));
+
+ /* highlight cursor */
+ change_colors(
+ grid,
+ nth_row_area(area, self.cursor_pos % rows),
+ self.highlight_theme.fg,
+ self.highlight_theme.bg,
+ );
+
+ /* clear gap if available height is more than count of entries */
+ if top_idx + rows > self.length {
+ clear_area(
+ grid,
+ (
+ pos_inc(upper_left, (0, self.length - top_idx)),
+ bottom_right,
+ ),
+ self.theme_default,
+ );
+ }
+ context.dirty_areas.push_back(area);
+ }
+}
+
+impl Component for MailboxManager {
+ fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
+ if !self.is_dirty() {
+ return;
+ }
+ if !self.initialized {
+ self.initialize(context);
+ }
+
+ self.draw_list(grid, area, context);
+ if let ViewMode::Action(ref mut s) = self.mode {
+ s.draw(grid, area, context);
+ }
+ self.dirty = false;
+ }
+
+ fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
+ if let UIEvent::ConfigReload { old_settings: _ } = event {
+ self.theme_default = crate::conf::value(context, "theme_default");
+ self.initialized = false;
+ self.set_dirty(true);
+ }
+ if let ViewMode::Action(ref mut s) = self.mode {
+ match &event {
+ UIEvent::FinishedUIDialog(id, result) if s.id() == *id => {
+ self.set_dirty(true);
+ self.mode = ViewMode::List;
+ if let Some(actions) = result.downcast_ref::<Vec<MailboxAction>>() {
+ if actions.len() == 1 {
+ use crate::actions::MailboxOperation;
+ match actions[0] {
+ MailboxAction::Move | MailboxAction::Rename => {
+ context.replies.push_back(UIEvent::CmdInput(Key::Paste(
+ format!(
+ "rename-mailbox \"{account_name}\" \"{mailbox_path_src}\" ",
+ account_name = context.accounts[&self.account_hash].name(),
+ mailbox_path_src =
+ self.entries[self.cursor_pos].ref_mailbox.path()
+ ),
+ )));
+ context
+ .replies
+ .push_back(UIEvent::ChangeMode(UIMode::Command));
+ }
+ MailboxAction::Subscribe => {
+ if let Err(err) = context.accounts[&self.account_hash]
+ .mailbox_operation(MailboxOperation::Subscribe(
+ self.entries[self.cursor_pos]
+ .ref_mailbox
+ .path()
+ .to_string(),
+ ))
+ {
+ context.replies.push_back(UIEvent::Notification(
+ None,
+ err.to_string(),
+ Some(crate::types::NotificationType::Error(err.kind)),
+ ));
+ }
+ }
+ MailboxAction::Unsubscribe => {
+ if let Err(err) = context.accounts[&self.account_hash]
+ .mailbox_operation(MailboxOperation::Unsubscribe(
+ self.entries[self.cursor_pos]
+ .ref_mailbox
+ .path()
+ .to_string(),
+ ))
+ {
+ context.replies.push_back(UIEvent::Notification(
+ None,
+ err.to_string(),
+ Some(crate::types::NotificationType::Error(err.kind)),
+ ));
+ }
+ }
+ }
+ }
+ }
+ return true;
+ }
+ _ => {}
+ }
+ return s.process_event(event, context);
+ }
+
+ let shortcuts = self.get_shortcuts(context);
+ match event {
+ UIEvent::AccountStatusChange(account_hash, msg)
+ if *account_hash == self.account_hash =>
+ {
+ self.initialize(context);
+
+ self.set_dirty(true);
+ //self.menu_content.empty();
+ context
+ .replies
+ .push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(match msg {
+ Some(msg) => format!("{} {}", self.get_status(context), msg),
+ None => self.get_status(context),
+ })));
+ }
+ UIEvent::Input(ref key) if shortcut!(key == shortcuts["general"]["scroll_up"]) => {
+ let amount = 1;
+ self.movement = Some(PageMovement::Up(amount));
+ self.set_dirty(true);
+ return true;
+ }
+ UIEvent::Input(ref key)
+ if shortcut!(key == shortcuts["general"]["scroll_down"])
+ && self.cursor_pos < self.length.saturating_sub(1) =>
+ {
+ let amount = 1;
+ self.set_dirty(true);
+ self.movement = Some(PageMovement::Down(amount));
+ return true;
+ }
+ UIEvent::Input(ref key) if shortcut!(key == shortcuts["general"]["prev_page"]) => {
+ let mult = 1;
+ self.set_dirty(true);
+ self.movement = Some(PageMovement::PageUp(mult));
+ return true;
+ }
+ UIEvent::Input(ref key) if shortcut!(key == shortcuts["general"]["next_page"]) => {
+ let mult = 1;
+ self.set_dirty(true);
+ self.movement = Some(PageMovement::PageDown(mult));
+ return true;
+ }
+ UIEvent::Input(ref key) if shortcut!(key == shortcuts["general"]["home_page"]) => {
+ self.set_dirty(true);
+ self.movement = Some(PageMovement::Home);
+ return true;
+ }
+ UIEvent::Input(ref key) if shortcut!(key == shortcuts["general"]["end_page"]) => {
+ self.set_dirty(true);
+ self.movement = Some(PageMovement::End);
+ return true;
+ }
+ UIEvent::Input(ref key) if shortcut!(key == shortcuts["general"]["open_entry"]) => {
+ self.set_dirty(true);
+ self.mode = ViewMode::Action(UIDialog::new(
+ "select action",
+ vec![
+ (MailboxAction::Rename, "rename".into()),
+ (MailboxAction::Move, "move".into()),
+ (MailboxAction::Subscribe, "subscribe".into()),
+ (MailboxAction::Unsubscribe, "unsubscribe".into()),
+ ],
+ true,
+ Some(Box::new(
+ move |id: ComponentId, results: &[MailboxAction]| {
+ Some(UIEvent::FinishedUIDialog(id, Box::new(results.to_vec())))
+ },
+ )),
+ context,
+ ));
+ return true;
+ }
+ _ => {}
+ }
+ false
+ }
+
+ fn is_dirty(&self) -> bool {
+ self.dirty
+ || if let ViewMode::Action(ref s) = self.mode {
+ s.is_dirty()
+ } else {
+ false
+ }
+ }
+
+ fn set_dirty(&mut self, value: bool) {
+ self.dirty = value;
+ if let ViewMode::Action(ref mut s) = self.mode {
+ s.set_dirty(value);
+ }
+ }
+
+ fn kill(&mut self, uuid: Uuid, context: &mut Context) {
+ debug_assert!(uuid == self.id);
+ context.replies.push_back(UIEvent::Action(Tab(Kill(uuid))));
+ }
+
+ fn get_shortcuts(&self, context: &Context) -> ShortcutMaps {
+ let mut map = ShortcutMaps::default();
+
+ let config_map = context.settings.shortcuts.general.key_values();
+ map.insert("general", config_map);
+
+ map
+ }
+
+ fn id(&self) -> ComponentId {
+ self.id
+ }
+
+ fn set_id(&mut self, id: ComponentId) {
+ self.id = id;
+ }
+
+ fn can_quit_cleanly(&mut self, _context: &Context) -> bool {
+ true
+ }
+
+ fn get_status(&self, _context: &Context) -> String {
+ format!("{} entries", self.entries.len())
+ }
+}
diff --git a/src/conf/accounts.rs b/src/conf/accounts.rs
index 86f30718..6945990f 100644
--- a/src/conf/accounts.rs
+++ b/src/conf/accounts.rs
@@ -72,7 +72,7 @@ macro_rules! try_recv_timeout {
}};
}
-#[derive(Debug)]
+#[derive(Debug, Clone)]
pub enum MailboxStatus {
Available,
Failed(MeliError),
@@ -97,7 +97,7 @@ impl MailboxStatus {
}
}
-#[derive(Debug)]
+#[derive(Debug, Clone)]
pub struct MailboxEntry {
pub status: MailboxStatus,
pub name: String,
@@ -132,23 +132,23 @@ impl MailboxEntry {
#[derive(Debug)]
pub struct Account {
- name: String,
- hash: AccountHash,
+ pub name: String,
+ pub hash: AccountHash,
pub is_online: Result<()>,
- pub(crate) mailbox_entries: IndexMap<MailboxHash, MailboxEntry>,
- pub(crate) mailboxes_order: Vec<MailboxHash>,
- tree: Vec<MailboxNode>,
- sent_mailbox: Option<MailboxHash>,
- pub(crate) collection: Collection,
- pub(crate) address_book: AddressBook,
- pub(crate) settings: AccountConf,
- pub(crate) backend: Arc<RwLock<Box<dyn MailBackend>>>,
+ pub mailbox_entries: IndexMap<MailboxHash, MailboxEntry>,
+ pub mailboxes_order: Vec<MailboxHash>,
+ pub tree: Vec<MailboxNode>,
+ pub sent_mailbox: Option<MailboxHash>,
+ pub collection: Collection,
+ pub address_book: AddressBook,
+ pub settings: AccountConf,
+ pub backend: Arc<RwLock<Box<dyn MailBackend>>>,
pub job_executor: Arc<JobExecutor>,
pub active_jobs: HashMap<JobId, JobRequest>,
pub active_job_instants: BTreeMap<std::time::Instant, JobId>,
- sender: Sender<ThreadEvent>,
- event_queue: VecDeque<(MailboxHash, RefreshEvent)>,
+ pub sender: Sender<ThreadEvent>,
+ pub event_queue: VecDeque<(MailboxHash, RefreshEvent)>,
pub backend_capabilities: MailBackendCapabilities,
}