make tables more customizable

All tables can customize the color of their column, and the envelopes
table can customize its flag chars.
This commit is contained in:
Clément DOUIN 2024-08-20 10:53:21 +02:00
parent daf2c7c87a
commit 8ccabf1fc0
No known key found for this signature in database
GPG key ID: 353E4A18EE0FAB72
14 changed files with 535 additions and 166 deletions

View file

@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Added
- Added `account.list.table.preset` global config option, `accounts.<name>.folder.list.table.preset` and `accounts.<name>.envelope.list.table.preset` account config options.
These options customize the shape of tables, see examples at [`comfy_table::presets`](https://docs.rs/comfy-table/latest/comfy_table/presets/index.html). Defaults to `"|| |-||| "`, which corresponds to [`comfy_table::presets::ASCII_MARKDOWN`](https://docs.rs/comfy-table/latest/comfy_table/presets/constant.ASCII_MARKDOWN.html).
- Added `account.list.table.name-color` config option to customize the color used for the accounts' `NAME` column (defaults to `green`).
- Added `account.list.table.backends-color` config option to customize the color used for the folders' `BACKENDS` column (defaults to `blue`).
- Added `account.list.table.default-color` config option to customize the color used for the folders' `DEFAULT` column (defaults to `reset`).
- Added `accounts.<name>.folder.list.table.name-color` account config option to customize the color used for the folders' `NAME` column (defaults to `blue`).
- Added `accounts.<name>.folder.list.table.desc-color` account config option to customize the color used for the folders' `DESC` column (defaults to `green`).
- Added `accounts.<name>.envelope.list.table.id-color` account config option to customize the color used for the envelopes' `ID` column (defaults to `red`).
- Added `accounts.<name>.envelope.list.table.flags-color` account config option to customize the color used for the envelopes' `FLAGS` column (defaults to `reset`).
- Added `accounts.<name>.envelope.list.table.subject-color` account config option to customize the color used for the envelopes' `SUBJECT` column (defaults to `green`).
- Added `accounts.<name>.envelope.list.table.sender-color` account config option to customize the color used for the envelopes' `FROM` column (defaults to `blue`).
- Added `accounts.<name>.envelope.list.table.date-color` account config option to customize the color used for the envelopes' `DATE` column (defaults to `dark_yellow`).
- Added `accounts.<name>.envelope.list.table.unseen-char` account config option to customize the char used for unseen envelopes (defaults to `*`).
- Added `accounts.<name>.envelope.list.table.replied-char` account config option to customize the char used for replied envelopes (defaults to `R`).
- Added `accounts.<name>.envelope.list.table.flagged-char` account config option to customize the char used for flagged envelopes (defaults to `!`).
- Added `accounts.<name>.envelope.list.table.attachment-char` account config option to customize the char used for envelopes with at least one attachment (defaults to `@`).
## [1.0.0-beta.4] - 2024-04-16 ## [1.0.0-beta.4] - 2024-04-16
### Added ### Added

4
Cargo.lock generated
View file

@ -418,6 +418,9 @@ name = "bitflags"
version = "2.6.0" version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "block-buffer" name = "block-buffer"
@ -885,6 +888,7 @@ dependencies = [
"libc", "libc",
"mio 0.8.11", "mio 0.8.11",
"parking_lot 0.12.3", "parking_lot 0.12.3",
"serde",
"signal-hook", "signal-hook",
"signal-hook-mio", "signal-hook-mio",
"winapi", "winapi",

View file

@ -56,7 +56,7 @@ clap_mangen = "0.2"
color-eyre = "0.6.3" color-eyre = "0.6.3"
comfy-table = "7.1.1" comfy-table = "7.1.1"
console = "0.15.2" console = "0.15.2"
crossterm = "0.27" crossterm = { version = "0.27", features = ["serde"] }
dirs = "4" dirs = "4"
email-lib = { version = "=0.25.0", default-features = false, features = ["derive", "thread", "tracing"] } email-lib = { version = "=0.25.0", default-features = false, features = ["derive", "thread", "tracing"] }
email_address = "0.2.4" email_address = "0.2.4"

View file

@ -28,7 +28,12 @@ impl AccountListCommand {
info!("executing list accounts command"); info!("executing list accounts command");
let accounts = Accounts::from(config.accounts.iter()); let accounts = Accounts::from(config.accounts.iter());
let table = AccountsTable::from(accounts).with_some_width(self.table_max_width); let table = AccountsTable::from(accounts)
.with_some_width(self.table_max_width)
.with_some_preset(config.account_list_table_preset())
.with_some_name_color(config.account_list_table_name_color())
.with_some_backends_color(config.account_list_table_backends_color())
.with_some_default_color(config.account_list_table_default_color());
printer.out(table)?; printer.out(table)?;
Ok(()) Ok(())

View file

@ -3,6 +3,8 @@
//! This module contains the raw deserialized representation of an //! This module contains the raw deserialized representation of an
//! account in the accounts section of the user configuration file. //! account in the accounts section of the user configuration file.
use comfy_table::presets;
use crossterm::style::Color;
#[cfg(feature = "pgp")] #[cfg(feature = "pgp")]
use email::account::config::pgp::PgpConfig; use email::account::config::pgp::PgpConfig;
#[cfg(feature = "imap")] #[cfg(feature = "imap")]
@ -21,7 +23,7 @@ use std::{collections::HashSet, path::PathBuf};
use crate::{ use crate::{
backend::BackendKind, envelope::config::EnvelopeConfig, flag::config::FlagConfig, backend::BackendKind, envelope::config::EnvelopeConfig, flag::config::FlagConfig,
folder::config::FolderConfig, message::config::MessageConfig, folder::config::FolderConfig, message::config::MessageConfig, ui::map_color,
}; };
/// Represents all existing kind of account config. /// Represents all existing kind of account config.
@ -58,6 +60,110 @@ pub struct TomlAccountConfig {
} }
impl TomlAccountConfig { impl TomlAccountConfig {
pub fn folder_list_table_preset(&self) -> Option<String> {
self.folder
.as_ref()
.and_then(|folder| folder.list.as_ref())
.and_then(|list| list.table.as_ref())
.and_then(|table| table.preset.clone())
}
pub fn folder_list_table_name_color(&self) -> Option<Color> {
self.folder
.as_ref()
.and_then(|folder| folder.list.as_ref())
.and_then(|list| list.table.as_ref())
.and_then(|table| table.name_color)
}
pub fn folder_list_table_desc_color(&self) -> Option<Color> {
self.folder
.as_ref()
.and_then(|folder| folder.list.as_ref())
.and_then(|list| list.table.as_ref())
.and_then(|table| table.desc_color)
}
pub fn envelope_list_table_preset(&self) -> Option<String> {
self.envelope
.as_ref()
.and_then(|env| env.list.as_ref())
.and_then(|list| list.table.as_ref())
.and_then(|table| table.preset.clone())
}
pub fn envelope_list_table_unseen_char(&self) -> Option<char> {
self.envelope
.as_ref()
.and_then(|env| env.list.as_ref())
.and_then(|list| list.table.as_ref())
.and_then(|table| table.unseen_char)
}
pub fn envelope_list_table_replied_char(&self) -> Option<char> {
self.envelope
.as_ref()
.and_then(|env| env.list.as_ref())
.and_then(|list| list.table.as_ref())
.and_then(|table| table.replied_char)
}
pub fn envelope_list_table_flagged_char(&self) -> Option<char> {
self.envelope
.as_ref()
.and_then(|env| env.list.as_ref())
.and_then(|list| list.table.as_ref())
.and_then(|table| table.flagged_char)
}
pub fn envelope_list_table_attachment_char(&self) -> Option<char> {
self.envelope
.as_ref()
.and_then(|env| env.list.as_ref())
.and_then(|list| list.table.as_ref())
.and_then(|table| table.attachment_char)
}
pub fn envelope_list_table_id_color(&self) -> Option<Color> {
self.envelope
.as_ref()
.and_then(|env| env.list.as_ref())
.and_then(|list| list.table.as_ref())
.and_then(|table| table.id_color)
}
pub fn envelope_list_table_flags_color(&self) -> Option<Color> {
self.envelope
.as_ref()
.and_then(|env| env.list.as_ref())
.and_then(|list| list.table.as_ref())
.and_then(|table| table.flags_color)
}
pub fn envelope_list_table_subject_color(&self) -> Option<Color> {
self.envelope
.as_ref()
.and_then(|env| env.list.as_ref())
.and_then(|list| list.table.as_ref())
.and_then(|table| table.subject_color)
}
pub fn envelope_list_table_sender_color(&self) -> Option<Color> {
self.envelope
.as_ref()
.and_then(|env| env.list.as_ref())
.and_then(|list| list.table.as_ref())
.and_then(|table| table.sender_color)
}
pub fn envelope_list_table_date_color(&self) -> Option<Color> {
self.envelope
.as_ref()
.and_then(|env| env.list.as_ref())
.and_then(|list| list.table.as_ref())
.and_then(|table| table.date_color)
}
pub fn add_folder_kind(&self) -> Option<&BackendKind> { pub fn add_folder_kind(&self) -> Option<&BackendKind> {
self.folder self.folder
.as_ref() .as_ref()
@ -228,3 +334,30 @@ impl TomlAccountConfig {
used_backends used_backends
} }
} }
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct ListAccountsTableConfig {
pub preset: Option<String>,
pub name_color: Option<Color>,
pub backends_color: Option<Color>,
pub default_color: Option<Color>,
}
impl ListAccountsTableConfig {
pub fn preset(&self) -> &str {
self.preset.as_deref().unwrap_or(presets::ASCII_MARKDOWN)
}
pub fn name_color(&self) -> comfy_table::Color {
map_color(self.name_color.unwrap_or(Color::Green))
}
pub fn backends_color(&self) -> comfy_table::Color {
map_color(self.backends_color.unwrap_or(Color::Blue))
}
pub fn default_color(&self) -> comfy_table::Color {
map_color(self.default_color.unwrap_or(Color::Reset))
}
}

View file

@ -4,11 +4,12 @@ pub mod config;
#[cfg(feature = "wizard")] #[cfg(feature = "wizard")]
pub(crate) mod wizard; pub(crate) mod wizard;
use comfy_table::{presets, Attribute, Cell, Color, ContentArrangement, Row, Table}; use comfy_table::{Cell, ContentArrangement, Row, Table};
use crossterm::style::Color;
use serde::{Serialize, Serializer}; use serde::{Serialize, Serializer};
use std::{collections::hash_map::Iter, fmt, ops::Deref}; use std::{collections::hash_map::Iter, fmt, ops::Deref};
use self::config::TomlAccountConfig; use self::config::{ListAccountsTableConfig, TomlAccountConfig};
/// Represents the printable account. /// Represents the printable account.
#[derive(Debug, Default, PartialEq, Eq, Serialize)] #[derive(Debug, Default, PartialEq, Eq, Serialize)]
@ -30,12 +31,12 @@ impl Account {
} }
} }
pub fn to_row(&self) -> Row { pub fn to_row(&self, config: &ListAccountsTableConfig) -> Row {
let mut row = Row::new(); let mut row = Row::new();
row.add_cell(Cell::new(&self.name).fg(Color::Green)); row.add_cell(Cell::new(&self.name).fg(config.name_color()));
row.add_cell(Cell::new(&self.backend).fg(Color::Blue)); row.add_cell(Cell::new(&self.backend).fg(config.backends_color()));
row.add_cell(Cell::new(if self.default { "yes" } else { "" }).fg(Color::White)); row.add_cell(Cell::new(if self.default { "yes" } else { "" }).fg(config.default_color()));
row row
} }
@ -51,24 +52,6 @@ impl fmt::Display for Account {
#[derive(Debug, Default, Serialize)] #[derive(Debug, Default, Serialize)]
pub struct Accounts(Vec<Account>); pub struct Accounts(Vec<Account>);
impl Accounts {
pub fn to_table(&self) -> Table {
let mut table = Table::new();
table
.load_preset(presets::NOTHING)
.set_content_arrangement(ContentArrangement::DynamicFullWidth)
.set_header(Row::from([
Cell::new("NAME").add_attribute(Attribute::Reverse),
Cell::new("BACKENDS").add_attribute(Attribute::Reverse),
Cell::new("DEFAULT").add_attribute(Attribute::Reverse),
]))
.add_rows(self.iter().map(Account::to_row));
table
}
}
impl Deref for Accounts { impl Deref for Accounts {
type Target = Vec<Account>; type Target = Vec<Account>;
@ -98,7 +81,7 @@ impl From<Iter<'_, String, TomlAccountConfig>> for Accounts {
} }
#[cfg(feature = "notmuch")] #[cfg(feature = "notmuch")]
if account.imap.is_some() { if account.notmuch.is_some() {
if !backends.is_empty() { if !backends.is_empty() {
backends.push_str(", ") backends.push_str(", ")
} }
@ -135,6 +118,7 @@ impl From<Iter<'_, String, TomlAccountConfig>> for Accounts {
pub struct AccountsTable { pub struct AccountsTable {
accounts: Accounts, accounts: Accounts,
width: Option<u16>, width: Option<u16>,
config: ListAccountsTableConfig,
} }
impl AccountsTable { impl AccountsTable {
@ -142,6 +126,26 @@ impl AccountsTable {
self.width = width; self.width = width;
self self
} }
pub fn with_some_preset(mut self, preset: Option<String>) -> Self {
self.config.preset = preset;
self
}
pub fn with_some_name_color(mut self, color: Option<Color>) -> Self {
self.config.name_color = color;
self
}
pub fn with_some_backends_color(mut self, color: Option<Color>) -> Self {
self.config.backends_color = color;
self
}
pub fn with_some_default_color(mut self, color: Option<Color>) -> Self {
self.config.default_color = color;
self
}
} }
impl From<Accounts> for AccountsTable { impl From<Accounts> for AccountsTable {
@ -149,13 +153,28 @@ impl From<Accounts> for AccountsTable {
Self { Self {
accounts, accounts,
width: None, width: None,
config: Default::default(),
} }
} }
} }
impl fmt::Display for AccountsTable { impl fmt::Display for AccountsTable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut table = self.accounts.to_table(); let mut table = Table::new();
table
.load_preset(self.config.preset())
.set_content_arrangement(ContentArrangement::DynamicFullWidth)
.set_header(Row::from([
Cell::new("NAME"),
Cell::new("BACKENDS"),
Cell::new("DEFAULT"),
]))
.add_rows(
self.accounts
.iter()
.map(|account| account.to_row(&self.config)),
);
if let Some(width) = self.width { if let Some(width) = self.width {
table.set_width(width); table.set_width(width);

View file

@ -5,6 +5,7 @@ use color_eyre::{
eyre::{bail, eyre, Context}, eyre::{bail, eyre, Context},
Result, Result,
}; };
use crossterm::style::Color;
use dirs::{config_dir, home_dir}; use dirs::{config_dir, home_dir};
use email::{ use email::{
account::config::AccountConfig, config::Config, envelope::config::EnvelopeConfig, account::config::AccountConfig, config::Config, envelope::config::EnvelopeConfig,
@ -17,7 +18,7 @@ use std::{collections::HashMap, fs, path::PathBuf, sync::Arc};
use toml::{self, Value}; use toml::{self, Value};
use tracing::debug; use tracing::debug;
use crate::account::config::TomlAccountConfig; use crate::account::config::{ListAccountsTableConfig, TomlAccountConfig};
#[cfg(feature = "wizard")] #[cfg(feature = "wizard")]
use crate::wizard_warn; use crate::wizard_warn;
@ -31,9 +32,42 @@ pub struct TomlConfig {
pub signature_delim: Option<String>, pub signature_delim: Option<String>,
pub downloads_dir: Option<PathBuf>, pub downloads_dir: Option<PathBuf>,
pub accounts: HashMap<String, TomlAccountConfig>, pub accounts: HashMap<String, TomlAccountConfig>,
pub account: Option<AccountsConfig>,
} }
impl TomlConfig { impl TomlConfig {
pub fn account_list_table_preset(&self) -> Option<String> {
self.account
.as_ref()
.and_then(|account| account.list.as_ref())
.and_then(|list| list.table.as_ref())
.and_then(|table| table.preset.clone())
}
pub fn account_list_table_name_color(&self) -> Option<Color> {
self.account
.as_ref()
.and_then(|account| account.list.as_ref())
.and_then(|list| list.table.as_ref())
.and_then(|table| table.name_color)
}
pub fn account_list_table_backends_color(&self) -> Option<Color> {
self.account
.as_ref()
.and_then(|account| account.list.as_ref())
.and_then(|list| list.table.as_ref())
.and_then(|table| table.backends_color)
}
pub fn account_list_table_default_color(&self) -> Option<Color> {
self.account
.as_ref()
.and_then(|account| account.list.as_ref())
.and_then(|list| list.table.as_ref())
.and_then(|table| table.default_color)
}
/// Read and parse the TOML configuration at the given paths. /// Read and parse the TOML configuration at the given paths.
/// ///
/// Returns an error if a configuration file cannot be read or if /// Returns an error if a configuration file cannot be read or if
@ -266,3 +300,15 @@ pub fn path_parser(path: &str) -> Result<PathBuf, String> {
.map(canonicalize::path) .map(canonicalize::path)
.map_err(|err| err.to_string()) .map_err(|err| err.to_string())
} }
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct AccountsConfig {
pub list: Option<ListAccountsConfig>,
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct ListAccountsConfig {
pub table: Option<ListAccountsTableConfig>,
}

View file

@ -188,7 +188,18 @@ impl ListEnvelopesCommand {
}; };
let envelopes = backend.list_envelopes(folder, opts).await?; let envelopes = backend.list_envelopes(folder, opts).await?;
let table = EnvelopesTable::from(envelopes).with_some_width(self.table_max_width); let table = EnvelopesTable::from(envelopes)
.with_some_width(self.table_max_width)
.with_some_preset(toml_account_config.envelope_list_table_preset())
.with_some_unseen_char(toml_account_config.envelope_list_table_unseen_char())
.with_some_replied_char(toml_account_config.envelope_list_table_replied_char())
.with_some_flagged_char(toml_account_config.envelope_list_table_flagged_char())
.with_some_attachment_char(toml_account_config.envelope_list_table_attachment_char())
.with_some_id_color(toml_account_config.envelope_list_table_id_color())
.with_some_flags_color(toml_account_config.envelope_list_table_flags_color())
.with_some_subject_color(toml_account_config.envelope_list_table_subject_color())
.with_some_sender_color(toml_account_config.envelope_list_table_sender_color())
.with_some_date_color(toml_account_config.envelope_list_table_date_color());
printer.out(table)?; printer.out(table)?;
Ok(()) Ok(())

View file

@ -1,7 +1,9 @@
use comfy_table::presets;
use crossterm::style::Color;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashSet; use std::collections::HashSet;
use crate::backend::BackendKind; use crate::{backend::BackendKind, ui::map_color};
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct EnvelopeConfig { pub struct EnvelopeConfig {
@ -27,8 +29,10 @@ impl EnvelopeConfig {
} }
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct ListEnvelopesConfig { pub struct ListEnvelopesConfig {
pub backend: Option<BackendKind>, pub backend: Option<BackendKind>,
pub table: Option<ListEnvelopesTableConfig>,
#[serde(flatten)] #[serde(flatten)]
pub remote: email::envelope::list::config::EnvelopeListConfig, pub remote: email::envelope::list::config::EnvelopeListConfig,
@ -46,6 +50,81 @@ impl ListEnvelopesConfig {
} }
} }
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct ListEnvelopesTableConfig {
pub preset: Option<String>,
pub unseen_char: Option<char>,
pub replied_char: Option<char>,
pub flagged_char: Option<char>,
pub attachment_char: Option<char>,
pub id_color: Option<Color>,
pub flags_color: Option<Color>,
pub subject_color: Option<Color>,
pub sender_color: Option<Color>,
pub date_color: Option<Color>,
}
impl ListEnvelopesTableConfig {
pub fn preset(&self) -> &str {
self.preset.as_deref().unwrap_or(presets::ASCII_MARKDOWN)
}
pub fn replied_char(&self, replied: bool) -> char {
if replied {
self.replied_char.unwrap_or('R')
} else {
' '
}
}
pub fn flagged_char(&self, flagged: bool) -> char {
if flagged {
self.flagged_char.unwrap_or('!')
} else {
' '
}
}
pub fn attachment_char(&self, attachment: bool) -> char {
if attachment {
self.attachment_char.unwrap_or('@')
} else {
' '
}
}
pub fn unseen_char(&self, unseen: bool) -> char {
if unseen {
self.unseen_char.unwrap_or('*')
} else {
' '
}
}
pub fn id_color(&self) -> comfy_table::Color {
map_color(self.id_color.unwrap_or(Color::Red))
}
pub fn flags_color(&self) -> comfy_table::Color {
map_color(self.flags_color.unwrap_or(Color::Reset))
}
pub fn subject_color(&self) -> comfy_table::Color {
map_color(self.subject_color.unwrap_or(Color::Green))
}
pub fn sender_color(&self) -> comfy_table::Color {
map_color(self.sender_color.unwrap_or(Color::Blue))
}
pub fn date_color(&self) -> comfy_table::Color {
map_color(self.date_color.unwrap_or(Color::DarkYellow))
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct ThreadEnvelopesConfig { pub struct ThreadEnvelopesConfig {
pub backend: Option<BackendKind>, pub backend: Option<BackendKind>,

View file

@ -4,8 +4,12 @@ pub mod config;
pub mod flag; pub mod flag;
use color_eyre::Result; use color_eyre::Result;
use comfy_table::{presets, Attribute, Cell, ContentArrangement, Row, Table}; use comfy_table::{Attribute, Cell, ContentArrangement, Row, Table};
use crossterm::{cursor, style::Stylize, terminal}; use crossterm::{
cursor,
style::{Color, Stylize},
terminal,
};
use email::{account::config::AccountConfig, envelope::ThreadedEnvelope}; use email::{account::config::AccountConfig, envelope::ThreadedEnvelope};
use petgraph::graphmap::DiGraphMap; use petgraph::graphmap::DiGraphMap;
use serde::{Serialize, Serializer}; use serde::{Serialize, Serializer};
@ -16,6 +20,8 @@ use crate::{
flag::{Flag, Flags}, flag::{Flag, Flags},
}; };
use self::config::ListEnvelopesTableConfig;
#[derive(Clone, Debug, Default, Serialize)] #[derive(Clone, Debug, Default, Serialize)]
pub struct Mailbox { pub struct Mailbox {
pub name: Option<String>, pub name: Option<String>,
@ -33,72 +39,11 @@ pub struct Envelope {
pub has_attachment: bool, pub has_attachment: bool,
} }
impl From<Envelope> for Row { impl Envelope {
fn from(envelope: Envelope) -> Self { fn to_row(&self, config: &ListEnvelopesTableConfig) -> Row {
let mut all_attributes = vec![]; let mut all_attributes = vec![];
let unseen = !envelope.flags.contains(&Flag::Seen); let unseen = !self.flags.contains(&Flag::Seen);
if unseen {
all_attributes.push(Attribute::Bold)
}
let flags = {
let mut flags = String::new();
flags.push(if !unseen { ' ' } else { '✷' });
flags.push(if envelope.flags.contains(&Flag::Answered) {
'↵'
} else {
' '
});
flags.push(if envelope.flags.contains(&Flag::Flagged) {
'⚑'
} else {
' '
});
flags
};
let mut row = Row::new();
row.add_cell(
Cell::new(envelope.id)
.add_attributes(all_attributes.clone())
.fg(comfy_table::Color::Red),
)
.add_cell(
Cell::new(flags)
.add_attributes(all_attributes.clone())
.fg(comfy_table::Color::White),
)
.add_cell(
Cell::new(envelope.subject)
.add_attributes(all_attributes.clone())
.fg(comfy_table::Color::Green),
)
.add_cell(
Cell::new(if let Some(name) = envelope.from.name {
name
} else {
envelope.from.addr
})
.add_attributes(all_attributes.clone())
.fg(comfy_table::Color::Blue),
)
.add_cell(
Cell::new(envelope.date)
.add_attributes(all_attributes)
.fg(comfy_table::Color::Yellow),
);
row
}
}
impl From<&Envelope> for Row {
fn from(envelope: &Envelope) -> Self {
let mut all_attributes = vec![];
let unseen = !envelope.flags.contains(&Flag::Seen);
if unseen { if unseen {
all_attributes.push(Attribute::Bold) all_attributes.push(Attribute::Bold)
} }
@ -106,55 +51,45 @@ impl From<&Envelope> for Row {
let flags = { let flags = {
let mut flags = String::new(); let mut flags = String::new();
flags.push(if !unseen { ' ' } else { '✷' }); flags.push(config.unseen_char(unseen));
flags.push(config.replied_char(self.flags.contains(&Flag::Answered)));
flags.push(if envelope.flags.contains(&Flag::Answered) { flags.push(config.flagged_char(self.flags.contains(&Flag::Flagged)));
'↵' flags.push(config.attachment_char(self.has_attachment));
} else {
' '
});
flags.push(if envelope.flags.contains(&Flag::Flagged) {
'⚑'
} else {
' '
});
flags.push(if envelope.has_attachment { '📎' } else { ' ' });
flags flags
}; };
let mut row = Row::new(); let mut row = Row::new();
row.max_height(1);
row.add_cell( row.add_cell(
Cell::new(&envelope.id) Cell::new(&self.id)
.add_attributes(all_attributes.clone()) .add_attributes(all_attributes.clone())
.fg(comfy_table::Color::Red), .fg(config.id_color()),
) )
.add_cell( .add_cell(
Cell::new(flags) Cell::new(flags)
.add_attributes(all_attributes.clone()) .add_attributes(all_attributes.clone())
.fg(comfy_table::Color::White), .fg(config.flags_color()),
) )
.add_cell( .add_cell(
Cell::new(&envelope.subject) Cell::new(&self.subject)
.add_attributes(all_attributes.clone()) .add_attributes(all_attributes.clone())
.fg(comfy_table::Color::Green), .fg(config.subject_color()),
) )
.add_cell( .add_cell(
Cell::new(if let Some(name) = &envelope.from.name { Cell::new(if let Some(name) = &self.from.name {
name name
} else { } else {
&envelope.from.addr &self.from.addr
}) })
.add_attributes(all_attributes.clone()) .add_attributes(all_attributes.clone())
.fg(comfy_table::Color::Blue), .fg(config.sender_color()),
) )
.add_cell( .add_cell(
Cell::new(&envelope.date) Cell::new(&self.date)
.add_attributes(all_attributes) .add_attributes(all_attributes)
.fg(comfy_table::Color::Yellow), .fg(config.date_color()),
); );
row row
@ -193,24 +128,6 @@ impl Envelopes {
Ok(Envelopes(envelopes)) Ok(Envelopes(envelopes))
} }
pub fn to_table(&self) -> Table {
let mut table = Table::new();
table
.load_preset(presets::NOTHING)
.set_content_arrangement(ContentArrangement::DynamicFullWidth)
.set_header(Row::from([
Cell::new("ID").add_attribute(Attribute::Reverse),
Cell::new("FLAGS").add_attribute(Attribute::Reverse),
Cell::new("SUBJECT").add_attribute(Attribute::Reverse),
Cell::new("FROM").add_attribute(Attribute::Reverse),
Cell::new("DATE").add_attribute(Attribute::Reverse),
]))
.add_rows(self.iter().map(Row::from));
table
}
} }
impl Deref for Envelopes { impl Deref for Envelopes {
@ -224,6 +141,7 @@ impl Deref for Envelopes {
pub struct EnvelopesTable { pub struct EnvelopesTable {
envelopes: Envelopes, envelopes: Envelopes,
width: Option<u16>, width: Option<u16>,
config: ListEnvelopesTableConfig,
} }
impl EnvelopesTable { impl EnvelopesTable {
@ -231,6 +149,56 @@ impl EnvelopesTable {
self.width = width; self.width = width;
self self
} }
pub fn with_some_preset(mut self, preset: Option<String>) -> Self {
self.config.preset = preset;
self
}
pub fn with_some_unseen_char(mut self, char: Option<char>) -> Self {
self.config.unseen_char = char;
self
}
pub fn with_some_replied_char(mut self, char: Option<char>) -> Self {
self.config.replied_char = char;
self
}
pub fn with_some_flagged_char(mut self, char: Option<char>) -> Self {
self.config.flagged_char = char;
self
}
pub fn with_some_attachment_char(mut self, char: Option<char>) -> Self {
self.config.attachment_char = char;
self
}
pub fn with_some_id_color(mut self, color: Option<Color>) -> Self {
self.config.id_color = color;
self
}
pub fn with_some_flags_color(mut self, color: Option<Color>) -> Self {
self.config.flags_color = color;
self
}
pub fn with_some_subject_color(mut self, color: Option<Color>) -> Self {
self.config.subject_color = color;
self
}
pub fn with_some_sender_color(mut self, color: Option<Color>) -> Self {
self.config.sender_color = color;
self
}
pub fn with_some_date_color(mut self, color: Option<Color>) -> Self {
self.config.date_color = color;
self
}
} }
impl From<Envelopes> for EnvelopesTable { impl From<Envelopes> for EnvelopesTable {
@ -238,13 +206,26 @@ impl From<Envelopes> for EnvelopesTable {
Self { Self {
envelopes, envelopes,
width: None, width: None,
config: Default::default(),
} }
} }
} }
impl fmt::Display for EnvelopesTable { impl fmt::Display for EnvelopesTable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut table = self.envelopes.to_table(); let mut table = Table::new();
table
.load_preset(self.config.preset())
.set_content_arrangement(ContentArrangement::DynamicFullWidth)
.set_header(Row::from([
Cell::new("ID"),
Cell::new("FLAGS"),
Cell::new("SUBJECT"),
Cell::new("FROM"),
Cell::new("DATE"),
]))
.add_rows(self.envelopes.iter().map(|env| env.to_row(&self.config)));
if let Some(width) = self.width { if let Some(width) = self.width {
table.set_width(width); table.set_width(width);

View file

@ -47,7 +47,11 @@ impl FolderListCommand {
.await?; .await?;
let folders = Folders::from(backend.list_folders().await?); let folders = Folders::from(backend.list_folders().await?);
let table = FoldersTable::from(folders).with_some_width(self.table_max_width); let table = FoldersTable::from(folders)
.with_some_width(self.table_max_width)
.with_some_preset(toml_account_config.folder_list_table_preset())
.with_some_name_color(toml_account_config.folder_list_table_name_color())
.with_some_desc_color(toml_account_config.folder_list_table_desc_color());
printer.log(table)?; printer.log(table)?;
Ok(()) Ok(())

View file

@ -1,7 +1,9 @@
use comfy_table::presets;
use crossterm::style::Color;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use crate::backend::BackendKind; use crate::{backend::BackendKind, ui::map_color};
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct FolderConfig { pub struct FolderConfig {
@ -60,8 +62,10 @@ impl FolderAddConfig {
} }
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct FolderListConfig { pub struct FolderListConfig {
pub backend: Option<BackendKind>, pub backend: Option<BackendKind>,
pub table: Option<ListFoldersTableConfig>,
#[serde(flatten)] #[serde(flatten)]
pub remote: email::folder::list::config::FolderListConfig, pub remote: email::folder::list::config::FolderListConfig,
@ -79,6 +83,28 @@ impl FolderListConfig {
} }
} }
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct ListFoldersTableConfig {
pub preset: Option<String>,
pub name_color: Option<Color>,
pub desc_color: Option<Color>,
}
impl ListFoldersTableConfig {
pub fn preset(&self) -> &str {
self.preset.as_deref().unwrap_or(presets::ASCII_MARKDOWN)
}
pub fn name_color(&self) -> comfy_table::Color {
map_color(self.name_color.unwrap_or(Color::Blue))
}
pub fn desc_color(&self) -> comfy_table::Color {
map_color(self.desc_color.unwrap_or(Color::Green))
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct FolderExpungeConfig { pub struct FolderExpungeConfig {
pub backend: Option<BackendKind>, pub backend: Option<BackendKind>,

View file

@ -2,10 +2,13 @@ pub mod arg;
pub mod command; pub mod command;
pub mod config; pub mod config;
use comfy_table::{presets, Attribute, Cell, ContentArrangement, Row, Table}; use comfy_table::{Cell, ContentArrangement, Row, Table};
use crossterm::style::Color;
use serde::{Serialize, Serializer}; use serde::{Serialize, Serializer};
use std::{fmt, ops::Deref}; use std::{fmt, ops::Deref};
use self::config::ListFoldersTableConfig;
#[derive(Clone, Debug, Default, Serialize)] #[derive(Clone, Debug, Default, Serialize)]
pub struct Folder { pub struct Folder {
pub name: String, pub name: String,
@ -13,11 +16,12 @@ pub struct Folder {
} }
impl Folder { impl Folder {
pub fn to_row(&self) -> Row { pub fn to_row(&self, config: &ListFoldersTableConfig) -> Row {
let mut row = Row::new(); let mut row = Row::new();
row.max_height(1);
row.add_cell(Cell::new(&self.name).fg(comfy_table::Color::Blue)); row.add_cell(Cell::new(&self.name).fg(config.name_color()));
row.add_cell(Cell::new(&self.desc).fg(comfy_table::Color::Green)); row.add_cell(Cell::new(&self.desc).fg(config.desc_color()));
row row
} }
@ -35,23 +39,6 @@ impl From<email::folder::Folder> for Folder {
#[derive(Clone, Debug, Default, Serialize)] #[derive(Clone, Debug, Default, Serialize)]
pub struct Folders(Vec<Folder>); pub struct Folders(Vec<Folder>);
impl Folders {
pub fn to_table(&self) -> Table {
let mut table = Table::new();
table
.load_preset(presets::NOTHING)
.set_content_arrangement(ContentArrangement::DynamicFullWidth)
.set_header(Row::from([
Cell::new("NAME").add_attribute(Attribute::Reverse),
Cell::new("DESC").add_attribute(Attribute::Reverse),
]))
.add_rows(self.iter().map(Folder::to_row));
table
}
}
impl Deref for Folders { impl Deref for Folders {
type Target = Vec<Folder>; type Target = Vec<Folder>;
@ -69,6 +56,7 @@ impl From<email::folder::Folders> for Folders {
pub struct FoldersTable { pub struct FoldersTable {
folders: Folders, folders: Folders,
width: Option<u16>, width: Option<u16>,
config: ListFoldersTableConfig,
} }
impl FoldersTable { impl FoldersTable {
@ -76,6 +64,21 @@ impl FoldersTable {
self.width = width; self.width = width;
self self
} }
pub fn with_some_preset(mut self, preset: Option<String>) -> Self {
self.config.preset = preset;
self
}
pub fn with_some_name_color(mut self, color: Option<Color>) -> Self {
self.config.name_color = color;
self
}
pub fn with_some_desc_color(mut self, color: Option<Color>) -> Self {
self.config.desc_color = color;
self
}
} }
impl From<Folders> for FoldersTable { impl From<Folders> for FoldersTable {
@ -83,13 +86,24 @@ impl From<Folders> for FoldersTable {
Self { Self {
folders, folders,
width: None, width: None,
config: Default::default(),
} }
} }
} }
impl fmt::Display for FoldersTable { impl fmt::Display for FoldersTable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut table = self.folders.to_table(); let mut table = Table::new();
table
.load_preset(self.config.preset())
.set_content_arrangement(ContentArrangement::DynamicFullWidth)
.set_header(Row::from([Cell::new("NAME"), Cell::new("DESC")]))
.add_rows(
self.folders
.iter()
.map(|folder| folder.to_row(&self.config)),
);
if let Some(width) = self.width { if let Some(width) = self.width {
table.set_width(width); table.set_width(width);

View file

@ -1,3 +1,29 @@
use crossterm::style::Color;
pub mod choice; pub mod choice;
pub mod editor; pub mod editor;
pub(crate) mod prompt; pub(crate) mod prompt;
pub(crate) fn map_color(color: Color) -> comfy_table::Color {
match color {
Color::Reset => comfy_table::Color::Reset,
Color::Black => comfy_table::Color::Black,
Color::DarkGrey => comfy_table::Color::DarkGrey,
Color::Red => comfy_table::Color::Red,
Color::DarkRed => comfy_table::Color::DarkRed,
Color::Green => comfy_table::Color::Green,
Color::DarkGreen => comfy_table::Color::DarkGreen,
Color::Yellow => comfy_table::Color::Yellow,
Color::DarkYellow => comfy_table::Color::DarkYellow,
Color::Blue => comfy_table::Color::Blue,
Color::DarkBlue => comfy_table::Color::DarkBlue,
Color::Magenta => comfy_table::Color::Magenta,
Color::DarkMagenta => comfy_table::Color::DarkMagenta,
Color::Cyan => comfy_table::Color::Cyan,
Color::DarkCyan => comfy_table::Color::DarkCyan,
Color::White => comfy_table::Color::White,
Color::Grey => comfy_table::Color::Grey,
Color::Rgb { r, g, b } => comfy_table::Color::Rgb { r, g, b },
Color::AnsiValue(n) => comfy_table::Color::AnsiValue(n),
}
}