From b5cc2a095f0268bb90cab150e903b0bbaffe1479 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Sat, 2 Dec 2023 15:55:24 +0200 Subject: [PATCH] Upgrade MailboxManager component to new TUI API Signed-off-by: Manos Pitsidianakis --- meli/src/lib.rs | 4 +- meli/src/mail/listing.rs | 10 +- meli/src/mailbox_management.rs | 375 ++++++++++++++++++++++++--------- 3 files changed, 282 insertions(+), 107 deletions(-) diff --git a/meli/src/lib.rs b/meli/src/lib.rs index 2dcc0d3f..5fb3cd1d 100644 --- a/meli/src/lib.rs +++ b/meli/src/lib.rs @@ -85,8 +85,8 @@ pub use crate::mail::*; pub mod notifications; -//pub mod mailbox_management; -//pub use mailbox_management::*; +pub mod mailbox_management; +pub use mailbox_management::*; pub mod jobs_view; pub use jobs_view::*; diff --git a/meli/src/mail/listing.rs b/meli/src/mail/listing.rs index 79b1793f..35dd524f 100644 --- a/meli/src/mail/listing.rs +++ b/meli/src/mail/listing.rs @@ -2294,11 +2294,11 @@ impl Component for Listing { return true; } UIEvent::Action(Action::Tab(ManageMailboxes)) => { - //let account_pos = self.cursor_pos.account; - //let mgr = MailboxManager::new(context, account_pos); - //context - // .replies - // .push_back(UIEvent::Action(Tab(New(Some(Box::new(mgr)))))); + let account_pos = self.cursor_pos.account; + let mgr = MailboxManager::new(context, account_pos); + context + .replies + .push_back(UIEvent::Action(Tab(New(Some(Box::new(mgr)))))); return true; } UIEvent::Action(Action::Tab(ManageJobs)) => { diff --git a/meli/src/mailbox_management.rs b/meli/src/mailbox_management.rs index 14e4a861..05c41be6 100644 --- a/meli/src/mailbox_management.rs +++ b/meli/src/mailbox_management.rs @@ -21,7 +21,7 @@ use std::cmp; use indexmap::IndexMap; -use melib::backends::AccountHash; +use melib::{backends::AccountHash, SortOrder}; use super::*; use crate::{accounts::MailboxEntry, melib::text_processing::TextProcessing}; @@ -41,6 +41,23 @@ enum ViewMode { Action(UIDialog), } +#[derive(Debug, Copy, Clone, PartialEq)] +#[repr(u8)] +enum Column { + _0 = 0, + _1, + _2, + _3, +} + +const fn _assert_len() { + if MailboxManager::HEADERS.len() != Column::_3 as usize + 1 { + panic!("MailboxManager::HEADERS length changed, please update Column enum accordingly."); + } +} + +const _: () = _assert_len(); + #[derive(Debug)] pub struct MailboxManager { cursor_pos: usize, @@ -48,7 +65,10 @@ pub struct MailboxManager { account_pos: usize, account_hash: AccountHash, length: usize, - data_columns: DataColumns<5>, + data_columns: DataColumns<4>, + min_width: [usize; 4], + sort_col: Column, + sort_order: SortOrder, entries: IndexMap, mode: ViewMode, @@ -69,6 +89,8 @@ impl std::fmt::Display for MailboxManager { } impl MailboxManager { + const HEADERS: [&'static str; 4] = ["name", "path", "size", "subscribed"]; + 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"); @@ -83,6 +105,9 @@ impl MailboxManager { length: 0, account_pos, data_columns, + sort_col: Column::_1, + sort_order: SortOrder::Asc, + min_width: [0; 4], theme_default, highlight_theme: crate::conf::value(context, "highlight"), initialized: false, @@ -95,113 +120,157 @@ impl MailboxManager { 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())); + let mut entries = account.mailbox_entries.clone(); + entries.sort_by(|_, a, _, b| match (self.sort_col, self.sort_order) { + (Column::_0, SortOrder::Asc) => a.ref_mailbox.name().cmp(b.ref_mailbox.name()), + (Column::_0, SortOrder::Desc) => b.ref_mailbox.name().cmp(a.ref_mailbox.name()), + (Column::_1, SortOrder::Asc) => a.ref_mailbox.path().cmp(b.ref_mailbox.path()), + (Column::_1, SortOrder::Desc) => b.ref_mailbox.path().cmp(a.ref_mailbox.path()), + (Column::_2, SortOrder::Asc) => { + let (_, a) = a.ref_mailbox.count().ok().unwrap_or((0, 0)); + let (_, b) = b.ref_mailbox.count().ok().unwrap_or((0, 0)); + a.cmp(&b) + } + (Column::_2, SortOrder::Desc) => { + let (_, a) = a.ref_mailbox.count().ok().unwrap_or((0, 0)); + let (_, b) = b.ref_mailbox.count().ok().unwrap_or((0, 0)); + b.cmp(&a) + } + (Column::_3, SortOrder::Asc) + if a.ref_mailbox.is_subscribed() && b.ref_mailbox.is_subscribed() => + { + std::cmp::Ordering::Equal + } + (Column::_3, SortOrder::Asc) if a.ref_mailbox.is_subscribed() => { + std::cmp::Ordering::Greater + } + (Column::_3, SortOrder::Desc) if a.ref_mailbox.is_subscribed() => { + std::cmp::Ordering::Less + } + (Column::_3, SortOrder::Asc) => std::cmp::Ordering::Less, + (Column::_3, SortOrder::Desc) => std::cmp::Ordering::Greater, + }); + self.entries = entries; + macro_rules! hdr { + ($idx:literal) => {{ + Self::HEADERS[$idx].len() + if self.sort_col as u8 == $idx { 1 } else { 0 } + }}; + } self.set_dirty(true); - let mut min_width = ( - "name".len(), - "path".len(), - "size".len(), - "subscribed".len(), - 0, - 0, - ); + let mut min_width = [hdr!(0), hdr!(1), hdr!(2), hdr!(3)]; 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()); + // 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); + // name column + _ = self.data_columns.columns[0].resize_with_context(min_width[0], self.length, context); + self.data_columns.columns[0].grid_mut().clear(None); + // path column + _ = self.data_columns.columns[1].resize_with_context(min_width[1], self.length, context); + self.data_columns.columns[1].grid_mut().clear(None); + // size column + _ = self.data_columns.columns[2].resize_with_context(min_width[2], self.length, context); + self.data_columns.columns[2].grid_mut().clear(None); + // subscribed column + _ = self.data_columns.columns[3].resize_with_context(min_width[3], self.length, context); + self.data_columns.columns[3].grid_mut().clear(None); for (idx, e) in self.entries.values().enumerate() { - self.data_columns.columns[0].write_string( - e.name(), - self.theme_default.fg, - self.theme_default.bg, - self.theme_default.attrs, - ((0, idx), (min_width.0, idx)), - None, - ); + { + let area = self.data_columns.columns[0].area().nth_row(idx); + self.data_columns.columns[0].grid_mut().write_string( + e.name(), + self.theme_default.fg, + self.theme_default.bg, + self.theme_default.attrs, + area, + None, + ); + } - self.data_columns.columns[1].write_string( - e.ref_mailbox.path(), - self.theme_default.fg, - self.theme_default.bg, - self.theme_default.attrs, - ((0, idx), (min_width.1, idx)), - None, - ); + { + let area = self.data_columns.columns[1].area().nth_row(idx); + self.data_columns.columns[1].grid_mut().write_string( + e.ref_mailbox.path(), + self.theme_default.fg, + self.theme_default.bg, + self.theme_default.attrs, + area, + None, + ); + } - let (_unseen, total) = e.ref_mailbox.count().ok().unwrap_or((0, 0)); - self.data_columns.columns[2].write_string( - &total.to_string(), - self.theme_default.fg, - self.theme_default.bg, - self.theme_default.attrs, - ((0, idx), (min_width.2, idx)), - None, - ); + { + let area = self.data_columns.columns[2].area().nth_row(idx); + let (_unseen, total) = e.ref_mailbox.count().ok().unwrap_or((0, 0)); + self.data_columns.columns[2].grid_mut().write_string( + &total.to_string(), + self.theme_default.fg, + self.theme_default.bg, + self.theme_default.attrs, + area, + None, + ); + } - self.data_columns.columns[3].write_string( - if e.ref_mailbox.is_subscribed() { - "yes" - } else { - "no" - }, - self.theme_default.fg, - self.theme_default.bg, - self.theme_default.attrs, - ((0, idx), (min_width.3, idx)), - None, - ); + { + let area = self.data_columns.columns[3].area().nth_row(idx); + self.data_columns.columns[3].grid_mut().write_string( + if e.ref_mailbox.is_subscribed() { + "yes" + } else { + "no" + }, + self.theme_default.fg, + self.theme_default.bg, + self.theme_default.attrs, + area, + 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); - self.data_columns.columns[0].write_string( - &message, - self.theme_default.fg, - self.theme_default.bg, - self.theme_default.attrs, - ((0, 0), (message.len() - 1, 0)), - None, - ); + if self.data_columns.columns[0].resize_with_context(message.len(), self.length, context) + { + let area = self.data_columns.columns[0].area(); + self.data_columns.columns[0].grid_mut().write_string( + &message, + self.theme_default.fg, + self.theme_default.bg, + self.theme_default.attrs, + area, + None, + ); + } } + + self.min_width = min_width; } fn draw_list(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { - let (upper_left, bottom_right) = area; + let rows = area.height(); + if rows < 2 { + return; + } if self.length == 0 { grid.clear_area(area, self.theme_default); grid.copy_area( - &self.data_columns.columns[0], + self.data_columns.columns[0].grid(), area, - ((0, 0), pos_dec(self.data_columns.columns[0].size(), (1, 1))), + self.data_columns.columns[0].area(), ); 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 { @@ -228,7 +297,20 @@ impl MailboxManager { self.new_cursor_pos = (self.length / rows) * rows; } } - PageMovement::Right(_) | PageMovement::Left(_) => {} + PageMovement::Right(amount) => { + self.data_columns.x_offset += amount; + self.data_columns.x_offset = self.data_columns.x_offset.min( + self.data_columns + .widths + .iter() + .map(|w| w + 2) + .sum::() + .saturating_sub(2), + ); + } + PageMovement::Left(amount) => { + self.data_columns.x_offset = self.data_columns.x_offset.saturating_sub(amount); + } PageMovement::Home => { self.new_cursor_pos = 0; } @@ -264,8 +346,8 @@ impl MailboxManager { ))); } - /* If cursor position has changed, remove the highlight from the previous - * position and apply it in the new one. */ + // 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; @@ -292,27 +374,21 @@ impl MailboxManager { self.new_cursor_pos = self.length - 1; self.cursor_pos = self.new_cursor_pos; } - /* Page_no has changed, so draw new page */ + // Page_no has changed, so draw new page _ = self .data_columns .recalc_widths((area.width(), area.height()), top_idx); grid.clear_area(area, self.theme_default); - /* copy table columns */ + // copy table columns self.data_columns .draw(grid, top_idx, self.cursor_pos, grid.bounds_iter(area)); - /* highlight cursor */ + // highlight cursor grid.change_theme(area.nth_row(self.cursor_pos % rows), self.highlight_theme); - /* clear gap if available height is more than count of entries */ + // clear gap if available height is more than count of entries if top_idx + rows > self.length { - grid.clear_area( - ( - pos_inc(upper_left, (0, self.length - top_idx)), - bottom_right, - ), - self.theme_default, - ); + grid.change_theme(area.skip_rows(self.length - top_idx), self.theme_default); } context.dirty_areas.push_back(area); } @@ -326,7 +402,42 @@ impl Component for MailboxManager { if !self.initialized { self.initialize(context); } - + if self.dirty { + let area = area.nth_row(0); + // Draw column headers. + grid.clear_area(area, self.theme_default); + let mut x_offset = 0; + for (i, (h, w)) in Self::HEADERS.iter().zip(self.min_width).enumerate() { + grid.write_string( + h, + self.theme_default.fg, + self.theme_default.bg, + self.theme_default.attrs | Attr::BOLD, + area.skip_cols(x_offset), + None, + ); + if self.sort_col as usize == i { + use SortOrder::*; + let arrow = match (grid.ascii_drawing, self.sort_order) { + (true, Asc) => DataColumns::<4>::ARROW_UP_ASCII, + (true, Desc) => DataColumns::<4>::ARROW_DOWN_ASCII, + (false, Asc) => DataColumns::<4>::ARROW_UP, + (false, Desc) => DataColumns::<4>::ARROW_DOWN, + }; + grid.write_string( + arrow, + self.theme_default.fg, + self.theme_default.bg, + self.theme_default.attrs, + area.skip_cols(x_offset + h.len()), + None, + ); + } + x_offset += w + 2; + } + context.dirty_areas.push_back(area); + } + let area = area.skip_rows(1); self.draw_list(grid, area, context); if let ViewMode::Action(ref mut s) = self.mode { s.draw(grid, area, context); @@ -373,7 +484,14 @@ impl Component for MailboxManager { .to_string(), )) { - context.replies.push_back(UIEvent::Notification { title: None, source: None, body: err.to_string().into(), kind: Some(crate::types::NotificationType::Error(err.kind)), }); + context.replies.push_back(UIEvent::Notification { + title: None, + source: None, + body: err.to_string().into(), + kind: Some(crate::types::NotificationType::Error( + err.kind, + )), + }); } } MailboxAction::Unsubscribe => { @@ -385,7 +503,14 @@ impl Component for MailboxManager { .to_string(), )) { - context.replies.push_back(UIEvent::Notification { title: None, source: None, body: err.to_string().into(), kind: Some(crate::types::NotificationType::Error(err.kind)), }); + context.replies.push_back(UIEvent::Notification { + title: None, + source: None, + body: err.to_string().into(), + kind: Some(crate::types::NotificationType::Error( + err.kind, + )), + }); } } } @@ -406,7 +531,6 @@ impl Component for MailboxManager { self.initialize(context); self.set_dirty(true); - //self.menu_content.empty(); context .replies .push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(match msg { @@ -414,6 +538,53 @@ impl Component for MailboxManager { None => self.status(context), }))); } + UIEvent::Action(Action::SortColumn(column, order)) => { + let column = match *column { + 0 => Column::_0, + 1 => Column::_1, + 2 => Column::_2, + 3 => Column::_3, + other => { + context.replies.push_back(UIEvent::StatusEvent( + StatusEvent::DisplayMessage(format!( + "Invalid column index `{}`: there are {} columns.", + other, + Self::HEADERS.len() + )), + )); + + return true; + } + }; + if (self.sort_col, self.sort_order) != (column, *order) { + self.sort_col = column; + self.sort_order = *order; + self.initialized = false; + self.set_dirty(true); + } + return true; + } + UIEvent::Input(Key::Char(ref c)) if c.is_ascii_digit() => { + let n = *c as u8 - b'0'; // safe cast because of is_ascii_digit() check; + let column = match n { + 1 => Column::_0, + 2 => Column::_1, + 3 => Column::_2, + 4 => Column::_3, + _ => { + return false; + } + }; + if self.sort_col == column { + self.sort_order = !self.sort_order; + } else { + self.sort_col = column; + self.sort_order = SortOrder::default(); + } + self.initialized = false; + self.set_dirty(true); + return true; + } UIEvent::Input(ref key) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_up"]) => { @@ -528,7 +699,11 @@ impl Component for MailboxManager { true } - fn status(&self, _context: &Context) -> String { - format!("{} entries", self.entries.len()) + fn status(&self, context: &Context) -> String { + format!( + "{} {} entries", + context.accounts[&self.account_hash].name(), + self.entries.len() + ) } }