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 { 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 . + */ +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), +} + +#[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, + mode: ViewMode, + + initialized: bool, + theme_default: ThemeAttribute, + highlight_theme: ThemeAttribute, + + dirty: bool, + + movement: Option, + 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::>() { + 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, - pub(crate) mailboxes_order: Vec, - tree: Vec, - sent_mailbox: Option, - pub(crate) collection: Collection, - pub(crate) address_book: AddressBook, - pub(crate) settings: AccountConf, - pub(crate) backend: Arc>>, + pub mailbox_entries: IndexMap, + pub mailboxes_order: Vec, + pub tree: Vec, + pub sent_mailbox: Option, + pub collection: Collection, + pub address_book: AddressBook, + pub settings: AccountConf, + pub backend: Arc>>, pub job_executor: Arc, pub active_jobs: HashMap, pub active_job_instants: BTreeMap, - sender: Sender, - event_queue: VecDeque<(MailboxHash, RefreshEvent)>, + pub sender: Sender, + pub event_queue: VecDeque<(MailboxHash, RefreshEvent)>, pub backend_capabilities: MailBackendCapabilities, }