add list accounts command (#244)

This commit is contained in:
Clément DOUIN
2022-03-03 17:29:39 +01:00
parent e5164a2ce3
commit 736641bf77
21 changed files with 416 additions and 111 deletions
+107
View File
@@ -0,0 +1,107 @@
//! Account module.
//!
//! This module contains the definition of the printable account,
//! which is only used by the "accounts" command to list all available
//! accounts from the config file.
use anyhow::Result;
use serde::Serialize;
use std::{
collections::hash_map::Iter,
fmt::{self, Display},
ops::Deref,
};
use crate::{
config::DeserializedAccountConfig,
output::{PrintTable, PrintTableOpts, WriteColor},
ui::{Cell, Row, Table},
};
/// Represents the list of printable accounts.
#[derive(Debug, Default, Serialize)]
pub struct Accounts(pub Vec<Account>);
impl Deref for Accounts {
type Target = Vec<Account>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl PrintTable for Accounts {
fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
writeln!(writter)?;
Table::print(writter, self, opts)?;
writeln!(writter)?;
Ok(())
}
}
/// Represents the printable account.
#[derive(Debug, Default, PartialEq, Eq, Serialize)]
pub struct Account {
/// Represents the account name.
pub name: String,
/// Represents the backend name of the account.
pub backend: String,
/// Represents the defaultness state of the account.
pub default: bool,
}
impl Account {
pub fn new(name: &str, backend: &str, default: bool) -> Self {
Self {
name: name.into(),
backend: backend.into(),
default,
}
}
}
impl Display for Account {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.name)
}
}
impl Table for Account {
fn head() -> Row {
Row::new()
.cell(Cell::new("NAME").shrinkable().bold().underline().white())
.cell(Cell::new("BACKEND").bold().underline().white())
.cell(Cell::new("DEFAULT").bold().underline().white())
}
fn row(&self) -> Row {
let default = if self.default { "yes" } else { "" };
Row::new()
.cell(Cell::new(&self.name).shrinkable().green())
.cell(Cell::new(&self.backend).blue())
.cell(Cell::new(default).white())
}
}
impl From<Iter<'_, String, DeserializedAccountConfig>> for Accounts {
fn from(map: Iter<'_, String, DeserializedAccountConfig>) -> Self {
let mut accounts: Vec<_> = map
.map(|(name, config)| match config {
DeserializedAccountConfig::Imap(config) => {
Account::new(name, "imap", config.default.unwrap_or_default())
}
DeserializedAccountConfig::Maildir(config) => {
Account::new(name, "maildir", config.default.unwrap_or_default())
}
#[cfg(feature = "notmuch")]
DeserializedAccountConfig::Maildir(config) => {
Account::new(name, "notmuch", config.default.unwrap_or_default())
}
})
.collect();
accounts.sort_by(|a, b| b.name.partial_cmp(&a.name).unwrap());
Self(accounts)
}
}
+45 -2
View File
@@ -1,9 +1,52 @@
//! This module provides arguments related to the user account config.
use clap::Arg;
use anyhow::Result;
use clap::{App, Arg, ArgMatches, SubCommand};
use log::{debug, info};
use crate::ui::table_arg;
type MaxTableWidth = Option<usize>;
/// Represents the account commands.
#[derive(Debug, PartialEq, Eq)]
pub enum Cmd {
/// Represents the list accounts command.
List(MaxTableWidth),
}
/// Represents the account command matcher.
pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
info!(">> account command matcher");
let cmd = if let Some(m) = m.subcommand_matches("accounts") {
info!("accounts command matched");
let max_table_width = m
.value_of("max-table-width")
.and_then(|width| width.parse::<usize>().ok());
debug!("max table width: {:?}", max_table_width);
Some(Cmd::List(max_table_width))
} else {
None
};
info!("<< account command matcher");
Ok(cmd)
}
/// Represents the account subcommands.
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
vec![SubCommand::with_name("accounts")
.aliases(&["account", "acc", "a"])
.about("Lists accounts")
.arg(table_arg::max_width())]
}
/// Represents the user account name argument.
/// This argument allows the user to select a different account than the default one.
/// This argument allows the user to select a different account than
/// the default one.
pub fn name_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name("account")
.long("account")
+135
View File
@@ -0,0 +1,135 @@
//! Account handlers module.
//!
//! This module gathers all account actions triggered by the CLI.
use anyhow::Result;
use log::{info, trace};
use crate::{
config::{AccountConfig, Accounts, DeserializedConfig},
output::{PrintTableOpts, PrinterService},
};
/// Lists all accounts.
pub fn list<'a, P: PrinterService>(
max_width: Option<usize>,
config: &DeserializedConfig,
account_config: &AccountConfig,
printer: &mut P,
) -> Result<()> {
info!(">> account list handler");
let accounts: Accounts = config.accounts.iter().into();
trace!("accounts: {:?}", accounts);
printer.print_table(
Box::new(accounts),
PrintTableOpts {
format: &account_config.format,
max_width,
},
)?;
info!("<< account list handler");
Ok(())
}
#[cfg(test)]
mod tests {
use std::{collections::HashMap, fmt::Debug, io, iter::FromIterator};
use termcolor::ColorSpec;
use crate::{
config::{DeserializedAccountConfig, DeserializedImapAccountConfig},
output::{Print, PrintTable, WriteColor},
};
use super::*;
#[test]
fn it_should_match_cmds_accounts() {
#[derive(Debug, Default, Clone)]
struct StringWritter {
content: String,
}
impl io::Write for StringWritter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.content
.push_str(&String::from_utf8(buf.to_vec()).unwrap());
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
self.content = String::default();
Ok(())
}
}
impl termcolor::WriteColor for StringWritter {
fn supports_color(&self) -> bool {
false
}
fn set_color(&mut self, _spec: &ColorSpec) -> io::Result<()> {
io::Result::Ok(())
}
fn reset(&mut self) -> io::Result<()> {
io::Result::Ok(())
}
}
impl WriteColor for StringWritter {}
#[derive(Debug, Default)]
struct PrinterServiceTest {
pub writter: StringWritter,
}
impl PrinterService for PrinterServiceTest {
fn print_table<T: Debug + PrintTable + erased_serde::Serialize + ?Sized>(
&mut self,
data: Box<T>,
opts: PrintTableOpts,
) -> Result<()> {
data.print_table(&mut self.writter, opts)?;
Ok(())
}
fn print<T: serde::Serialize + Print>(&mut self, _data: T) -> Result<()> {
unimplemented!()
}
fn is_json(&self) -> bool {
unimplemented!()
}
}
struct TestBackend;
let config = DeserializedConfig {
accounts: HashMap::from_iter([(
"account-1".into(),
DeserializedAccountConfig::Imap(DeserializedImapAccountConfig {
default: Some(true),
..DeserializedImapAccountConfig::default()
}),
)]),
..DeserializedConfig::default()
};
let account_config = AccountConfig::default();
let mut printer = PrinterServiceTest::default();
let mut backend = TestBackend {};
assert!(list(None, &config, &account_config, &mut printer).is_ok());
assert_eq!(
concat![
"\n",
"NAME │BACKEND │DEFAULT \n",
"account-1 │imap │true \n",
"\n"
],
printer.writter.content
);
}
}