add max table width arg, refactor printer (#237)

* printer: refactor output to pass down args from cli

* msg: add missing max width arg to search cmd

* output: rename printer service, merge print with output folder

* doc: update changelog and wiki

* table: rename print fn
This commit is contained in:
Clément DOUIN 2021-10-24 21:02:02 +02:00 committed by GitHub
parent 192445d7e4
commit e154481c5b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 457 additions and 346 deletions

View file

@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Disable color support [#185] - Disable color feature [#185]
- `--max-width|-w` argument to restrict listing table width [#220]
### Fixed ### Fixed
@ -346,5 +347,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#199]: https://github.com/soywod/himalaya/issues/199 [#199]: https://github.com/soywod/himalaya/issues/199
[#205]: https://github.com/soywod/himalaya/issues/205 [#205]: https://github.com/soywod/himalaya/issues/205
[#215]: https://github.com/soywod/himalaya/issues/215 [#215]: https://github.com/soywod/himalaya/issues/215
[#220]: https://github.com/soywod/himalaya/issues/220
[#228]: https://github.com/soywod/himalaya/issues/228 [#228]: https://github.com/soywod/himalaya/issues/228
[#229]: https://github.com/soywod/himalaya/issues/229 [#229]: https://github.com/soywod/himalaya/issues/229

View file

@ -7,18 +7,26 @@ use anyhow::Result;
use clap; use clap;
use log::trace; use log::trace;
use crate::ui::table_arg;
type MaxTableWidth = Option<usize>;
/// Represents the mailbox commands. /// Represents the mailbox commands.
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum Cmd { pub enum Cmd {
/// Represents the list mailboxes command. /// Represents the list mailboxes command.
List, List(MaxTableWidth),
} }
/// Defines the mailbox command matcher. /// Defines the mailbox command matcher.
pub fn matches(m: &clap::ArgMatches) -> Result<Option<Cmd>> { pub fn matches(m: &clap::ArgMatches) -> Result<Option<Cmd>> {
if let Some(_) = m.subcommand_matches("mailboxes") { if let Some(m) = m.subcommand_matches("mailboxes") {
trace!("mailboxes subcommand matched"); trace!("mailboxes subcommand matched");
return Ok(Some(Cmd::List)); let max_table_width = m
.value_of("max-table-width")
.and_then(|width| width.parse::<usize>().ok());
trace!(r#"max table width: "{:?}""#, max_table_width);
return Ok(Some(Cmd::List(max_table_width)));
} }
Ok(None) Ok(None)
@ -28,7 +36,8 @@ pub fn matches(m: &clap::ArgMatches) -> Result<Option<Cmd>> {
pub fn subcmds<'a>() -> Vec<clap::App<'a, 'a>> { pub fn subcmds<'a>() -> Vec<clap::App<'a, 'a>> {
vec![clap::SubCommand::with_name("mailboxes") vec![clap::SubCommand::with_name("mailboxes")
.aliases(&["mailbox", "mboxes", "mbox", "mb", "m"]) .aliases(&["mailbox", "mboxes", "mbox", "mb", "m"])
.about("Lists mailboxes")] .about("Lists mailboxes")
.arg(table_arg::max_width())]
} }
/// Defines the source mailbox argument. /// Defines the source mailbox argument.
@ -58,8 +67,12 @@ mod tests {
let arg = clap::App::new("himalaya") let arg = clap::App::new("himalaya")
.subcommands(subcmds()) .subcommands(subcmds())
.get_matches_from(&["himalaya", "mailboxes"]); .get_matches_from(&["himalaya", "mailboxes"]);
assert_eq!(Some(Cmd::List(None)), matches(&arg).unwrap());
assert_eq!(Some(Cmd::List), matches(&arg).unwrap()); let arg = clap::App::new("himalaya")
.subcommands(subcmds())
.get_matches_from(&["himalaya", "mailboxes", "--max-width", "20"]);
assert_eq!(Some(Cmd::List(Some(20))), matches(&arg).unwrap());
} }
#[test] #[test]

View file

@ -5,43 +5,89 @@
use anyhow::Result; use anyhow::Result;
use log::trace; use log::trace;
use crate::{domain::ImapServiceInterface, output::OutputServiceInterface}; use crate::{
domain::ImapServiceInterface,
output::{PrintTableOpts, PrinterService},
};
/// List all mailboxes. /// Lists all mailboxes.
pub fn list<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>( pub fn list<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
output: &OutputService, max_width: Option<usize>,
printer: &mut Printer,
imap: &'a mut ImapService, imap: &'a mut ImapService,
) -> Result<()> { ) -> Result<()> {
let mboxes = imap.fetch_mboxes()?; let mboxes = imap.fetch_mboxes()?;
trace!("mailboxes: {:#?}", mboxes); trace!("mailboxes: {:#?}", mboxes);
output.print(mboxes) printer.print_table(mboxes, PrintTableOpts { max_width })
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use serde::Serialize; use serde::Serialize;
use std::{fmt::Debug, io};
use termcolor::ColorSpec;
use super::*;
use crate::{ use crate::{
config::Config, config::Config,
domain::{AttrRemote, Attrs, Envelopes, Flags, Mbox, Mboxes, Msg}, domain::{AttrRemote, Attrs, Envelopes, Flags, Mbox, Mboxes, Msg},
output::{OutputJson, Print}, output::{Print, PrintTable, WriteColor},
}; };
use super::*;
#[test] #[test]
fn it_should_list_mboxes() { fn it_should_list_mboxes() {
struct OutputServiceTest; #[derive(Debug, Default, Clone)]
struct StringWritter {
content: String,
}
impl OutputServiceInterface for OutputServiceTest { impl io::Write for StringWritter {
fn print<T: Serialize + Print>(&self, data: T) -> Result<()> { fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let data = serde_json::to_string(&OutputJson::new(data))?; self.content
assert_eq!( .push_str(&String::from_utf8(buf.to_vec()).unwrap());
data, Ok(buf.len())
r#"{"response":[{"delim":"/","name":"INBOX","attrs":["NoSelect"]},{"delim":"/","name":"Sent","attrs":["NoInferiors",{"Custom":"HasNoChildren"}]}]}"#
);
Ok(())
} }
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 + Serialize>(
&mut self,
data: T,
opts: PrintTableOpts,
) -> Result<()> {
data.print_table(&mut self.writter, opts)?;
Ok(())
}
fn print<T: Serialize + Print>(&mut self, _data: T) -> Result<()> {
unimplemented!()
}
fn is_json(&self) -> bool { fn is_json(&self) -> bool {
unimplemented!() unimplemented!()
} }
@ -71,59 +117,57 @@ mod tests {
fn notify(&mut self, _: &Config, _: u64) -> Result<()> { fn notify(&mut self, _: &Config, _: u64) -> Result<()> {
unimplemented!() unimplemented!()
} }
fn watch(&mut self, _: u64) -> Result<()> { fn watch(&mut self, _: u64) -> Result<()> {
unimplemented!() unimplemented!()
} }
fn fetch_envelopes(&mut self, _: &usize, _: &usize) -> Result<Envelopes> { fn fetch_envelopes(&mut self, _: &usize, _: &usize) -> Result<Envelopes> {
unimplemented!() unimplemented!()
} }
fn fetch_envelopes_with(&mut self, _: &str, _: &usize, _: &usize) -> Result<Envelopes> { fn fetch_envelopes_with(&mut self, _: &str, _: &usize, _: &usize) -> Result<Envelopes> {
unimplemented!() unimplemented!()
} }
fn find_msg(&mut self, _: &str) -> Result<Msg> { fn find_msg(&mut self, _: &str) -> Result<Msg> {
unimplemented!() unimplemented!()
} }
fn find_raw_msg(&mut self, _: &str) -> Result<Vec<u8>> { fn find_raw_msg(&mut self, _: &str) -> Result<Vec<u8>> {
unimplemented!() unimplemented!()
} }
fn append_msg(&mut self, _: &Mbox, _: Msg) -> Result<()> { fn append_msg(&mut self, _: &Mbox, _: Msg) -> Result<()> {
unimplemented!() unimplemented!()
} }
fn append_raw_msg_with_flags(&mut self, _: &Mbox, _: &[u8], _: Flags) -> Result<()> { fn append_raw_msg_with_flags(&mut self, _: &Mbox, _: &[u8], _: Flags) -> Result<()> {
unimplemented!() unimplemented!()
} }
fn expunge(&mut self) -> Result<()> { fn expunge(&mut self) -> Result<()> {
unimplemented!() unimplemented!()
} }
fn logout(&mut self) -> Result<()> { fn logout(&mut self) -> Result<()> {
unimplemented!() unimplemented!()
} }
fn add_flags(&mut self, _: &str, _: &Flags) -> Result<()> { fn add_flags(&mut self, _: &str, _: &Flags) -> Result<()> {
unimplemented!() unimplemented!()
} }
fn set_flags(&mut self, _: &str, _: &Flags) -> Result<()> { fn set_flags(&mut self, _: &str, _: &Flags) -> Result<()> {
unimplemented!() unimplemented!()
} }
fn remove_flags(&mut self, _: &str, _: &Flags) -> Result<()> { fn remove_flags(&mut self, _: &str, _: &Flags) -> Result<()> {
unimplemented!() unimplemented!()
} }
} }
let output = OutputServiceTest {}; let mut printer = PrinterServiceTest::default();
let mut imap = ImapServiceTest {}; let mut imap = ImapServiceTest {};
assert!(list(&output, &mut imap).is_ok()); assert!(list(None, &mut printer, &mut imap).is_ok());
assert_eq!(
concat![
"\n",
"DELIM │NAME │ATTRIBUTES \n",
"/ │INBOX │NoSelect \n",
"/ │Sent │NoInferiors, HasNoChildren \n",
"\n"
],
printer.writter.content
);
} }
} }

View file

@ -8,7 +8,7 @@ use std::ops::Deref;
use crate::{ use crate::{
domain::{Mbox, RawMbox}, domain::{Mbox, RawMbox},
output::{Print, WriteWithColor}, output::{PrintTable, PrintTableOpts, WriteColor},
ui::Table, ui::Table,
}; };
@ -29,10 +29,12 @@ impl<'a> Deref for Mboxes<'a> {
} }
/// Makes the mailboxes printable. /// Makes the mailboxes printable.
impl<'a> Print for Mboxes<'a> { impl<'a> PrintTable for Mboxes<'a> {
fn print<W: WriteWithColor>(&self, writter: &mut W) -> Result<()> { fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
writeln!(writter)?; writeln!(writter)?;
Table::println(writter, &self) Table::print(writter, &self, opts)?;
writeln!(writter)?;
Ok(())
} }
} }

View file

@ -4,7 +4,7 @@ use std::{borrow::Cow, convert::TryFrom};
use crate::{ use crate::{
domain::msg::{Flag, Flags}, domain::msg::{Flag, Flags},
ui::table::{Cell, Row, Table}, ui::{Cell, Row, Table},
}; };
pub type RawEnvelope = imap::types::Fetch; pub type RawEnvelope = imap::types::Fetch;

View file

@ -4,7 +4,7 @@ use std::{convert::TryFrom, ops::Deref};
use crate::{ use crate::{
domain::{msg::Envelope, RawEnvelope}, domain::{msg::Envelope, RawEnvelope},
output::{Print, WriteWithColor}, output::{PrintTable, PrintTableOpts, WriteColor},
ui::Table, ui::Table,
}; };
@ -36,9 +36,11 @@ impl<'a> TryFrom<&'a RawEnvelopes> for Envelopes<'a> {
} }
} }
impl<'a> Print for Envelopes<'a> { impl<'a> PrintTable for Envelopes<'a> {
fn print<W: WriteWithColor>(&self, writter: &mut W) -> Result<()> { fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
println!(); writeln!(writter)?;
Table::println(writter, &self) Table::print(writter, &self, opts)?;
writeln!(writter)?;
Ok(())
} }
} }

View file

@ -6,20 +6,20 @@ use anyhow::Result;
use crate::{ use crate::{
domain::{Flags, ImapServiceInterface}, domain::{Flags, ImapServiceInterface},
output::OutputServiceInterface, output::PrinterService,
}; };
/// Adds flags to all messages matching the given sequence range. /// Adds flags to all messages matching the given sequence range.
/// Flags are case-insensitive, and they do not need to be prefixed with `\`. /// Flags are case-insensitive, and they do not need to be prefixed with `\`.
pub fn add<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>( pub fn add<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
seq_range: &'a str, seq_range: &'a str,
flags: Vec<&'a str>, flags: Vec<&'a str>,
output: &'a OutputService, printer: &'a mut Printer,
imap: &'a mut ImapService, imap: &'a mut ImapService,
) -> Result<()> { ) -> Result<()> {
let flags = Flags::from(flags); let flags = Flags::from(flags);
imap.add_flags(seq_range, &flags)?; imap.add_flags(seq_range, &flags)?;
output.print(format!( printer.print(format!(
r#"Flag(s) "{}" successfully added to message(s) "{}""#, r#"Flag(s) "{}" successfully added to message(s) "{}""#,
flags, seq_range flags, seq_range
)) ))
@ -27,15 +27,15 @@ pub fn add<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceIn
/// Removes flags from all messages matching the given sequence range. /// Removes flags from all messages matching the given sequence range.
/// Flags are case-insensitive, and they do not need to be prefixed with `\`. /// Flags are case-insensitive, and they do not need to be prefixed with `\`.
pub fn remove<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>( pub fn remove<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
seq_range: &'a str, seq_range: &'a str,
flags: Vec<&'a str>, flags: Vec<&'a str>,
output: &'a OutputService, printer: &'a mut Printer,
imap: &'a mut ImapService, imap: &'a mut ImapService,
) -> Result<()> { ) -> Result<()> {
let flags = Flags::from(flags); let flags = Flags::from(flags);
imap.remove_flags(seq_range, &flags)?; imap.remove_flags(seq_range, &flags)?;
output.print(format!( printer.print(format!(
r#"Flag(s) "{}" successfully removed from message(s) "{}""#, r#"Flag(s) "{}" successfully removed from message(s) "{}""#,
flags, seq_range flags, seq_range
)) ))
@ -43,15 +43,15 @@ pub fn remove<'a, OutputService: OutputServiceInterface, ImapService: ImapServic
/// Replaces flags of all messages matching the given sequence range. /// Replaces flags of all messages matching the given sequence range.
/// Flags are case-insensitive, and they do not need to be prefixed with `\`. /// Flags are case-insensitive, and they do not need to be prefixed with `\`.
pub fn set<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>( pub fn set<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
seq_range: &'a str, seq_range: &'a str,
flags: Vec<&'a str>, flags: Vec<&'a str>,
output: &'a OutputService, printer: &'a mut Printer,
imap: &'a mut ImapService, imap: &'a mut ImapService,
) -> Result<()> { ) -> Result<()> {
let flags = Flags::from(flags); let flags = Flags::from(flags);
imap.set_flags(seq_range, &flags)?; imap.set_flags(seq_range, &flags)?;
output.print(format!( printer.print(format!(
r#"Flag(s) "{}" successfully set for message(s) "{}""#, r#"Flag(s) "{}" successfully set for message(s) "{}""#,
flags, seq_range flags, seq_range
)) ))

View file

@ -6,9 +6,12 @@ use anyhow::Result;
use clap::{self, App, Arg, ArgMatches, SubCommand}; use clap::{self, App, Arg, ArgMatches, SubCommand};
use log::{debug, trace}; use log::{debug, trace};
use crate::domain::{ use crate::{
mbox::mbox_arg, domain::{
msg::{flag_arg, msg_arg, tpl_arg}, mbox::mbox_arg,
msg::{flag_arg, msg_arg, tpl_arg},
},
ui::table_arg,
}; };
type Seq<'a> = &'a str; type Seq<'a> = &'a str;
@ -21,6 +24,7 @@ type All = bool;
type RawMsg<'a> = &'a str; type RawMsg<'a> = &'a str;
type Query = String; type Query = String;
type AttachmentsPaths<'a> = Vec<&'a str>; type AttachmentsPaths<'a> = Vec<&'a str>;
type MaxTableWidth = Option<usize>;
/// Message commands. /// Message commands.
pub enum Command<'a> { pub enum Command<'a> {
@ -28,12 +32,12 @@ pub enum Command<'a> {
Copy(Seq<'a>, Mbox<'a>), Copy(Seq<'a>, Mbox<'a>),
Delete(Seq<'a>), Delete(Seq<'a>),
Forward(Seq<'a>, AttachmentsPaths<'a>), Forward(Seq<'a>, AttachmentsPaths<'a>),
List(Option<PageSize>, Page), List(MaxTableWidth, Option<PageSize>, Page),
Move(Seq<'a>, Mbox<'a>), Move(Seq<'a>, Mbox<'a>),
Read(Seq<'a>, TextMime<'a>, Raw), Read(Seq<'a>, TextMime<'a>, Raw),
Reply(Seq<'a>, All, AttachmentsPaths<'a>), Reply(Seq<'a>, All, AttachmentsPaths<'a>),
Save(RawMsg<'a>), Save(RawMsg<'a>),
Search(Query, Option<PageSize>, Page), Search(Query, MaxTableWidth, Option<PageSize>, Page),
Send(RawMsg<'a>), Send(RawMsg<'a>),
Write(AttachmentsPaths<'a>), Write(AttachmentsPaths<'a>),
@ -77,6 +81,10 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
if let Some(m) = m.subcommand_matches("list") { if let Some(m) = m.subcommand_matches("list") {
debug!("list command matched"); debug!("list command matched");
let max_table_width = m
.value_of("max-table-width")
.and_then(|width| width.parse::<usize>().ok());
trace!(r#"max table width: "{:?}""#, max_table_width);
let page_size = m.value_of("page-size").and_then(|s| s.parse().ok()); let page_size = m.value_of("page-size").and_then(|s| s.parse().ok());
trace!(r#"page size: "{:?}""#, page_size); trace!(r#"page size: "{:?}""#, page_size);
let page = m let page = m
@ -87,7 +95,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
.map(|page| 1.max(page) - 1) .map(|page| 1.max(page) - 1)
.unwrap_or_default(); .unwrap_or_default();
trace!(r#"page: "{:?}""#, page); trace!(r#"page: "{:?}""#, page);
return Ok(Some(Command::List(page_size, page))); return Ok(Some(Command::List(max_table_width, page_size, page)));
} }
if let Some(m) = m.subcommand_matches("move") { if let Some(m) = m.subcommand_matches("move") {
@ -130,6 +138,10 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
if let Some(m) = m.subcommand_matches("search") { if let Some(m) = m.subcommand_matches("search") {
debug!("search command matched"); debug!("search command matched");
let max_table_width = m
.value_of("max-table-width")
.and_then(|width| width.parse::<usize>().ok());
trace!(r#"max table width: "{:?}""#, max_table_width);
let page_size = m.value_of("page-size").and_then(|s| s.parse().ok()); let page_size = m.value_of("page-size").and_then(|s| s.parse().ok());
trace!(r#"page size: "{:?}""#, page_size); trace!(r#"page size: "{:?}""#, page_size);
let page = m let page = m
@ -165,7 +177,12 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
.1 .1
.join(" "); .join(" ");
trace!(r#"query: "{:?}""#, query); trace!(r#"query: "{:?}""#, query);
return Ok(Some(Command::Search(query, page_size, page))); return Ok(Some(Command::Search(
query,
max_table_width,
page_size,
page,
)));
} }
if let Some(m) = m.subcommand_matches("send") { if let Some(m) = m.subcommand_matches("send") {
@ -191,7 +208,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
} }
debug!("default list command matched"); debug!("default list command matched");
Ok(Some(Command::List(None, 0))) Ok(Some(Command::List(None, None, 0)))
} }
/// Message sequence number argument. /// Message sequence number argument.
@ -262,12 +279,14 @@ pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
.aliases(&["lst", "l"]) .aliases(&["lst", "l"])
.about("Lists all messages") .about("Lists all messages")
.arg(page_size_arg()) .arg(page_size_arg())
.arg(page_arg()), .arg(page_arg())
.arg(table_arg::max_width()),
SubCommand::with_name("search") SubCommand::with_name("search")
.aliases(&["s", "query", "q"]) .aliases(&["s", "query", "q"])
.about("Lists messages matching the given IMAP query") .about("Lists messages matching the given IMAP query")
.arg(page_size_arg()) .arg(page_size_arg())
.arg(page_arg()) .arg(page_arg())
.arg(table_arg::max_width())
.arg( .arg(
Arg::with_name("query") Arg::with_name("query")
.help("IMAP query") .help("IMAP query")

View file

@ -22,7 +22,7 @@ use crate::{
msg::{msg_utils, BinaryPart, Flags, Part, Parts, TextPlainPart, TplOverride}, msg::{msg_utils, BinaryPart, Flags, Part, Parts, TextPlainPart, TplOverride},
smtp::SmtpServiceInterface, smtp::SmtpServiceInterface,
}, },
output::OutputServiceInterface, output::PrinterService,
ui::{ ui::{
choice::{self, PostEditChoice, PreEditChoice}, choice::{self, PostEditChoice, PreEditChoice},
editor, editor,
@ -298,13 +298,13 @@ impl Msg {
pub fn edit_with_editor< pub fn edit_with_editor<
'a, 'a,
OutputService: OutputServiceInterface, Printer: PrinterService,
ImapService: ImapServiceInterface<'a>, ImapService: ImapServiceInterface<'a>,
SmtpService: SmtpServiceInterface, SmtpService: SmtpServiceInterface,
>( >(
mut self, mut self,
account: &Account, account: &Account,
output: &OutputService, printer: &mut Printer,
imap: &mut ImapService, imap: &mut ImapService,
smtp: &mut SmtpService, smtp: &mut SmtpService,
) -> Result<()> { ) -> Result<()> {
@ -342,7 +342,7 @@ impl Msg {
let flags = Flags::try_from(vec![Flag::Seen])?; let flags = Flags::try_from(vec![Flag::Seen])?;
imap.append_raw_msg_with_flags(&mbox, &sent_msg.formatted(), flags)?; imap.append_raw_msg_with_flags(&mbox, &sent_msg.formatted(), flags)?;
msg_utils::remove_local_draft()?; msg_utils::remove_local_draft()?;
output.print("Message successfully sent")?; printer.print("Message successfully sent")?;
break; break;
} }
Ok(PostEditChoice::Edit) => { Ok(PostEditChoice::Edit) => {
@ -350,7 +350,7 @@ impl Msg {
continue; continue;
} }
Ok(PostEditChoice::LocalDraft) => { Ok(PostEditChoice::LocalDraft) => {
output.print("Message successfully saved locally")?; printer.print("Message successfully saved locally")?;
break; break;
} }
Ok(PostEditChoice::RemoteDraft) => { Ok(PostEditChoice::RemoteDraft) => {
@ -359,7 +359,7 @@ impl Msg {
let tpl = self.to_tpl(TplOverride::default(), account); let tpl = self.to_tpl(TplOverride::default(), account);
imap.append_raw_msg_with_flags(&mbox, tpl.as_bytes(), flags)?; imap.append_raw_msg_with_flags(&mbox, tpl.as_bytes(), flags)?;
msg_utils::remove_local_draft()?; msg_utils::remove_local_draft()?;
output.print("Message successfully saved to Drafts")?; printer.print("Message successfully saved to Drafts")?;
break; break;
} }
Ok(PostEditChoice::Discard) => { Ok(PostEditChoice::Discard) => {

View file

@ -22,18 +22,14 @@ use crate::{
msg::{Flags, Msg, Part, TextPlainPart}, msg::{Flags, Msg, Part, TextPlainPart},
smtp::SmtpServiceInterface, smtp::SmtpServiceInterface,
}, },
output::OutputServiceInterface, output::{PrintTableOpts, PrinterService},
}; };
/// Download all message attachments to the user account downloads directory. /// Download all message attachments to the user account downloads directory.
pub fn attachments< pub fn attachments<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
'a,
OutputService: OutputServiceInterface,
ImapService: ImapServiceInterface<'a>,
>(
seq: &str, seq: &str,
account: &Account, account: &Account,
output: &OutputService, printer: &mut Printer,
imap: &mut ImapService, imap: &mut ImapService,
) -> Result<()> { ) -> Result<()> {
let attachments = imap.find_msg(&seq)?.attachments(); let attachments = imap.find_msg(&seq)?.attachments();
@ -50,75 +46,76 @@ pub fn attachments<
.context(format!("cannot download attachment {:?}", filepath))?; .context(format!("cannot download attachment {:?}", filepath))?;
} }
output.print(format!( printer.print(format!(
"{} attachment(s) successfully downloaded to {:?}", "{} attachment(s) successfully downloaded to {:?}",
attachments_len, account.downloads_dir attachments_len, account.downloads_dir
)) ))
} }
/// Copy a message from a mailbox to another. /// Copy a message from a mailbox to another.
pub fn copy<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>( pub fn copy<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
seq: &str, seq: &str,
mbox: &str, mbox: &str,
output: &OutputService, printer: &mut Printer,
imap: &mut ImapService, imap: &mut ImapService,
) -> Result<()> { ) -> Result<()> {
let mbox = Mbox::new(mbox); let mbox = Mbox::new(mbox);
let msg = imap.find_raw_msg(&seq)?; let msg = imap.find_raw_msg(&seq)?;
let flags = Flags::try_from(vec![Flag::Seen])?; let flags = Flags::try_from(vec![Flag::Seen])?;
imap.append_raw_msg_with_flags(&mbox, &msg, flags)?; imap.append_raw_msg_with_flags(&mbox, &msg, flags)?;
output.print(format!( printer.print(format!(
r#"Message {} successfully copied to folder "{}""#, r#"Message {} successfully copied to folder "{}""#,
seq, mbox seq, mbox
)) ))
} }
/// Delete messages matching the given sequence range. /// Delete messages matching the given sequence range.
pub fn delete<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>( pub fn delete<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
seq: &str, seq: &str,
output: &OutputService, printer: &mut Printer,
imap: &mut ImapService, imap: &mut ImapService,
) -> Result<()> { ) -> Result<()> {
let flags = Flags::try_from(vec![Flag::Seen, Flag::Deleted])?; let flags = Flags::try_from(vec![Flag::Seen, Flag::Deleted])?;
imap.add_flags(seq, &flags)?; imap.add_flags(seq, &flags)?;
imap.expunge()?; imap.expunge()?;
output.print(format!(r#"Message(s) {} successfully deleted"#, seq)) printer.print(format!(r#"Message(s) {} successfully deleted"#, seq))
} }
/// Forward the given message UID from the selected mailbox. /// Forward the given message UID from the selected mailbox.
pub fn forward< pub fn forward<
'a, 'a,
OutputService: OutputServiceInterface, Printer: PrinterService,
ImapService: ImapServiceInterface<'a>, ImapService: ImapServiceInterface<'a>,
SmtpService: SmtpServiceInterface, SmtpService: SmtpServiceInterface,
>( >(
seq: &str, seq: &str,
attachments_paths: Vec<&str>, attachments_paths: Vec<&str>,
account: &Account, account: &Account,
output: &OutputService, printer: &mut Printer,
imap: &mut ImapService, imap: &mut ImapService,
smtp: &mut SmtpService, smtp: &mut SmtpService,
) -> Result<()> { ) -> Result<()> {
imap.find_msg(seq)? imap.find_msg(seq)?
.into_forward(account)? .into_forward(account)?
.add_attachments(attachments_paths)? .add_attachments(attachments_paths)?
.edit_with_editor(account, output, imap, smtp) .edit_with_editor(account, printer, imap, smtp)
} }
/// List paginated messages from the selected mailbox. /// List paginated messages from the selected mailbox.
pub fn list<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>( pub fn list<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
max_width: Option<usize>,
page_size: Option<usize>, page_size: Option<usize>,
page: usize, page: usize,
account: &Account, account: &Account,
output: &OutputService, printer: &mut Printer,
imap: &mut ImapService, imap: &'a mut ImapService,
) -> Result<()> { ) -> Result<()> {
let page_size = page_size.unwrap_or(account.default_page_size); let page_size = page_size.unwrap_or(account.default_page_size);
trace!("page size: {}", page_size); trace!("page size: {}", page_size);
let msgs = imap.fetch_envelopes(&page_size, &page)?; let msgs = imap.fetch_envelopes(&page_size, &page)?;
trace!("messages: {:#?}", msgs); trace!("messages: {:#?}", msgs);
output.print(msgs) printer.print_table(msgs, PrintTableOpts { max_width })
} }
/// Parse and edit a message from a [mailto] URL string. /// Parse and edit a message from a [mailto] URL string.
@ -126,13 +123,13 @@ pub fn list<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceI
/// [mailto]: https://en.wikipedia.org/wiki/Mailto /// [mailto]: https://en.wikipedia.org/wiki/Mailto
pub fn mailto< pub fn mailto<
'a, 'a,
OutputService: OutputServiceInterface, Printer: PrinterService,
ImapService: ImapServiceInterface<'a>, ImapService: ImapServiceInterface<'a>,
SmtpService: SmtpServiceInterface, SmtpService: SmtpServiceInterface,
>( >(
url: &Url, url: &Url,
account: &Account, account: &Account,
output: &OutputService, printer: &mut Printer,
imap: &mut ImapService, imap: &mut ImapService,
smtp: &mut SmtpService, smtp: &mut SmtpService,
) -> Result<()> { ) -> Result<()> {
@ -174,16 +171,16 @@ pub fn mailto<
msg.parts.push(Part::TextPlain(TextPlainPart { msg.parts.push(Part::TextPlain(TextPlainPart {
content: body.into(), content: body.into(),
})); }));
msg.edit_with_editor(account, output, imap, smtp) msg.edit_with_editor(account, printer, imap, smtp)
} }
/// Move a message from a mailbox to another. /// Move a message from a mailbox to another.
pub fn move_<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>( pub fn move_<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
// The sequence number of the message to move // The sequence number of the message to move
seq: &str, seq: &str,
// The mailbox to move the message in // The mailbox to move the message in
mbox: &str, mbox: &str,
output: &OutputService, printer: &mut Printer,
imap: &mut ImapService, imap: &mut ImapService,
) -> Result<()> { ) -> Result<()> {
// Copy the message to targetted mailbox // Copy the message to targetted mailbox
@ -197,18 +194,18 @@ pub fn move_<'a, OutputService: OutputServiceInterface, ImapService: ImapService
imap.add_flags(seq, &flags)?; imap.add_flags(seq, &flags)?;
imap.expunge()?; imap.expunge()?;
output.print(format!( printer.print(format!(
r#"Message {} successfully moved to folder "{}""#, r#"Message {} successfully moved to folder "{}""#,
seq, mbox seq, mbox
)) ))
} }
/// Read a message by its sequence number. /// Read a message by its sequence number.
pub fn read<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>( pub fn read<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
seq: &str, seq: &str,
text_mime: &str, text_mime: &str,
raw: bool, raw: bool,
output: &OutputService, printer: &mut Printer,
imap: &mut ImapService, imap: &mut ImapService,
) -> Result<()> { ) -> Result<()> {
let msg = if raw { let msg = if raw {
@ -218,13 +215,13 @@ pub fn read<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceI
imap.find_msg(&seq)?.fold_text_parts(text_mime) imap.find_msg(&seq)?.fold_text_parts(text_mime)
}; };
output.print(msg) printer.print(msg)
} }
/// Reply to the given message UID. /// Reply to the given message UID.
pub fn reply< pub fn reply<
'a, 'a,
OutputService: OutputServiceInterface, Printer: PrinterService,
ImapService: ImapServiceInterface<'a>, ImapService: ImapServiceInterface<'a>,
SmtpService: SmtpServiceInterface, SmtpService: SmtpServiceInterface,
>( >(
@ -232,26 +229,26 @@ pub fn reply<
all: bool, all: bool,
attachments_paths: Vec<&str>, attachments_paths: Vec<&str>,
account: &Account, account: &Account,
output: &OutputService, printer: &mut Printer,
imap: &mut ImapService, imap: &mut ImapService,
smtp: &mut SmtpService, smtp: &mut SmtpService,
) -> Result<()> { ) -> Result<()> {
imap.find_msg(seq)? imap.find_msg(seq)?
.into_reply(all, account)? .into_reply(all, account)?
.add_attachments(attachments_paths)? .add_attachments(attachments_paths)?
.edit_with_editor(account, output, imap, smtp)?; .edit_with_editor(account, printer, imap, smtp)?;
let flags = Flags::try_from(vec![Flag::Answered])?; let flags = Flags::try_from(vec![Flag::Answered])?;
imap.add_flags(seq, &flags) imap.add_flags(seq, &flags)
} }
/// Save a raw message to the targetted mailbox. /// Save a raw message to the targetted mailbox.
pub fn save<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>( pub fn save<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
mbox: &Mbox, mbox: &Mbox,
raw_msg: &str, raw_msg: &str,
output: &OutputService, printer: &mut Printer,
imap: &mut ImapService, imap: &mut ImapService,
) -> Result<()> { ) -> Result<()> {
let raw_msg = if atty::is(Stream::Stdin) || output.is_json() { let raw_msg = if atty::is(Stream::Stdin) || printer.is_json() {
raw_msg.replace("\r", "").replace("\n", "\r\n") raw_msg.replace("\r", "").replace("\n", "\r\n")
} else { } else {
io::stdin() io::stdin()
@ -268,12 +265,13 @@ pub fn save<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceI
} }
/// Paginate messages from the selected mailbox matching the specified query. /// Paginate messages from the selected mailbox matching the specified query.
pub fn search<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>( pub fn search<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
query: String, query: String,
max_width: Option<usize>,
page_size: Option<usize>, page_size: Option<usize>,
page: usize, page: usize,
account: &Account, account: &Account,
output: &OutputService, printer: &mut Printer,
imap: &'a mut ImapService, imap: &'a mut ImapService,
) -> Result<()> { ) -> Result<()> {
let page_size = page_size.unwrap_or(account.default_page_size); let page_size = page_size.unwrap_or(account.default_page_size);
@ -281,22 +279,22 @@ pub fn search<'a, OutputService: OutputServiceInterface, ImapService: ImapServic
let msgs = imap.fetch_envelopes_with(&query, &page_size, &page)?; let msgs = imap.fetch_envelopes_with(&query, &page_size, &page)?;
trace!("messages: {:#?}", msgs); trace!("messages: {:#?}", msgs);
output.print(msgs) printer.print_table(msgs, PrintTableOpts { max_width })
} }
/// Send a raw message. /// Send a raw message.
pub fn send< pub fn send<
'a, 'a,
OutputService: OutputServiceInterface, Printer: PrinterService,
ImapService: ImapServiceInterface<'a>, ImapService: ImapServiceInterface<'a>,
SmtpService: SmtpServiceInterface, SmtpService: SmtpServiceInterface,
>( >(
raw_msg: &str, raw_msg: &str,
output: &OutputService, printer: &mut Printer,
imap: &mut ImapService, imap: &mut ImapService,
smtp: &mut SmtpService, smtp: &mut SmtpService,
) -> Result<()> { ) -> Result<()> {
let raw_msg = if atty::is(Stream::Stdin) || output.is_json() { let raw_msg = if atty::is(Stream::Stdin) || printer.is_json() {
raw_msg.replace("\r", "").replace("\n", "\r\n") raw_msg.replace("\r", "").replace("\n", "\r\n")
} else { } else {
io::stdin() io::stdin()
@ -322,17 +320,17 @@ pub fn send<
/// Compose a new message. /// Compose a new message.
pub fn write< pub fn write<
'a, 'a,
OutputService: OutputServiceInterface, Printer: PrinterService,
ImapService: ImapServiceInterface<'a>, ImapService: ImapServiceInterface<'a>,
SmtpService: SmtpServiceInterface, SmtpService: SmtpServiceInterface,
>( >(
attachments_paths: Vec<&str>, attachments_paths: Vec<&str>,
account: &Account, account: &Account,
output: &OutputService, printer: &mut Printer,
imap: &mut ImapService, imap: &mut ImapService,
smtp: &mut SmtpService, smtp: &mut SmtpService,
) -> Result<()> { ) -> Result<()> {
Msg::default() Msg::default()
.add_attachments(attachments_paths)? .add_attachments(attachments_paths)?
.edit_with_editor(account, output, imap, smtp) .edit_with_editor(account, printer, imap, smtp)
} }

View file

@ -10,46 +10,46 @@ use crate::{
imap::ImapServiceInterface, imap::ImapServiceInterface,
msg::{Msg, TplOverride}, msg::{Msg, TplOverride},
}, },
output::OutputServiceInterface, output::PrinterService,
}; };
/// Generate a new message template. /// Generate a new message template.
pub fn new<'a, OutputService: OutputServiceInterface>( pub fn new<'a, Printer: PrinterService>(
opts: TplOverride<'a>, opts: TplOverride<'a>,
account: &'a Account, account: &'a Account,
output: &'a OutputService, printer: &'a mut Printer,
) -> Result<()> { ) -> Result<()> {
let tpl = Msg::default().to_tpl(opts, account); let tpl = Msg::default().to_tpl(opts, account);
output.print(tpl) printer.print(tpl)
} }
/// Generate a reply message template. /// Generate a reply message template.
pub fn reply<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>( pub fn reply<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
seq: &str, seq: &str,
all: bool, all: bool,
opts: TplOverride<'a>, opts: TplOverride<'a>,
account: &'a Account, account: &'a Account,
output: &'a OutputService, printer: &'a mut Printer,
imap: &'a mut ImapService, imap: &'a mut ImapService,
) -> Result<()> { ) -> Result<()> {
let tpl = imap let tpl = imap
.find_msg(seq)? .find_msg(seq)?
.into_reply(all, account)? .into_reply(all, account)?
.to_tpl(opts, account); .to_tpl(opts, account);
output.print(tpl) printer.print(tpl)
} }
/// Generate a forward message template. /// Generate a forward message template.
pub fn forward<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>( pub fn forward<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
seq: &str, seq: &str,
opts: TplOverride<'a>, opts: TplOverride<'a>,
account: &'a Account, account: &'a Account,
output: &'a OutputService, printer: &'a mut Printer,
imap: &'a mut ImapService, imap: &'a mut ImapService,
) -> Result<()> { ) -> Result<()> {
let tpl = imap let tpl = imap
.find_msg(seq)? .find_msg(seq)?
.into_forward(account)? .into_forward(account)?
.to_tpl(opts, account); .to_tpl(opts, account);
output.print(tpl) printer.print(tpl)
} }

View file

@ -1,6 +1,7 @@
use anyhow::Result; use anyhow::Result;
use clap; use clap;
use env_logger; use env_logger;
use output::StdoutPrinter;
use std::{convert::TryFrom, env}; use std::{convert::TryFrom, env};
use url::Url; use url::Url;
@ -18,7 +19,7 @@ use domain::{
msg::{flag_arg, flag_handler, msg_arg, msg_handler, tpl_arg, tpl_handler}, msg::{flag_arg, flag_handler, msg_arg, msg_handler, tpl_arg, tpl_handler},
smtp::SmtpService, smtp::SmtpService,
}; };
use output::{output_arg, OutputService}; use output::{output_arg, OutputFmt};
fn create_app<'a>() -> clap::App<'a, 'a> { fn create_app<'a>() -> clap::App<'a, 'a> {
clap::App::new(env!("CARGO_PKG_NAME")) clap::App::new(env!("CARGO_PKG_NAME"))
@ -47,11 +48,11 @@ fn main() -> Result<()> {
let mbox = Mbox::new("INBOX"); let mbox = Mbox::new("INBOX");
let config = Config::try_from(None)?; let config = Config::try_from(None)?;
let account = Account::try_from((&config, None))?; let account = Account::try_from((&config, None))?;
let output = OutputService::from("plain"); let mut printer = StdoutPrinter::from(OutputFmt::Plain);
let url = Url::parse(&raw_args[1])?; let url = Url::parse(&raw_args[1])?;
let mut imap = ImapService::from((&account, &mbox)); let mut imap = ImapService::from((&account, &mbox));
let mut smtp = SmtpService::from(&account); let mut smtp = SmtpService::from(&account);
return msg_handler::mailto(&url, &account, &output, &mut imap, &mut smtp); return msg_handler::mailto(&url, &account, &mut printer, &mut imap, &mut smtp);
} }
let app = create_app(); let app = create_app();
@ -70,7 +71,7 @@ fn main() -> Result<()> {
let mbox = Mbox::new(m.value_of("mbox-source").unwrap()); let mbox = Mbox::new(m.value_of("mbox-source").unwrap());
let config = Config::try_from(m.value_of("config"))?; let config = Config::try_from(m.value_of("config"))?;
let account = Account::try_from((&config, m.value_of("account")))?; let account = Account::try_from((&config, m.value_of("account")))?;
let output = OutputService::try_from(m.value_of("output"))?; let mut printer = StdoutPrinter::try_from(m.value_of("output"))?;
let mut imap = ImapService::from((&account, &mbox)); let mut imap = ImapService::from((&account, &mbox));
let mut smtp = SmtpService::from(&account); let mut smtp = SmtpService::from(&account);
@ -87,8 +88,8 @@ fn main() -> Result<()> {
// Check mailbox commands. // Check mailbox commands.
match mbox_arg::matches(&m)? { match mbox_arg::matches(&m)? {
Some(mbox_arg::Cmd::List) => { Some(mbox_arg::Cmd::List(max_width)) => {
return mbox_handler::list(&output, &mut imap); return mbox_handler::list(max_width, &mut printer, &mut imap);
} }
_ => (), _ => (),
} }
@ -96,62 +97,85 @@ fn main() -> Result<()> {
// Check message commands. // Check message commands.
match msg_arg::matches(&m)? { match msg_arg::matches(&m)? {
Some(msg_arg::Command::Attachments(seq)) => { Some(msg_arg::Command::Attachments(seq)) => {
return msg_handler::attachments(seq, &account, &output, &mut imap); return msg_handler::attachments(seq, &account, &mut printer, &mut imap);
} }
Some(msg_arg::Command::Copy(seq, mbox)) => { Some(msg_arg::Command::Copy(seq, mbox)) => {
return msg_handler::copy(seq, mbox, &output, &mut imap); return msg_handler::copy(seq, mbox, &mut printer, &mut imap);
} }
Some(msg_arg::Command::Delete(seq)) => { Some(msg_arg::Command::Delete(seq)) => {
return msg_handler::delete(seq, &output, &mut imap); return msg_handler::delete(seq, &mut printer, &mut imap);
} }
Some(msg_arg::Command::Forward(seq, atts)) => { Some(msg_arg::Command::Forward(seq, atts)) => {
return msg_handler::forward(seq, atts, &account, &output, &mut imap, &mut smtp); return msg_handler::forward(seq, atts, &account, &mut printer, &mut imap, &mut smtp);
} }
Some(msg_arg::Command::List(page_size, page)) => { Some(msg_arg::Command::List(max_width, page_size, page)) => {
return msg_handler::list(page_size, page, &account, &output, &mut imap); return msg_handler::list(
max_width,
page_size,
page,
&account,
&mut printer,
&mut imap,
);
} }
Some(msg_arg::Command::Move(seq, mbox)) => { Some(msg_arg::Command::Move(seq, mbox)) => {
return msg_handler::move_(seq, mbox, &output, &mut imap); return msg_handler::move_(seq, mbox, &mut printer, &mut imap);
} }
Some(msg_arg::Command::Read(seq, text_mime, raw)) => { Some(msg_arg::Command::Read(seq, text_mime, raw)) => {
return msg_handler::read(seq, text_mime, raw, &output, &mut imap); return msg_handler::read(seq, text_mime, raw, &mut printer, &mut imap);
} }
Some(msg_arg::Command::Reply(seq, all, atts)) => { Some(msg_arg::Command::Reply(seq, all, atts)) => {
return msg_handler::reply(seq, all, atts, &account, &output, &mut imap, &mut smtp); return msg_handler::reply(
seq,
all,
atts,
&account,
&mut printer,
&mut imap,
&mut smtp,
);
} }
Some(msg_arg::Command::Save(raw_msg)) => { Some(msg_arg::Command::Save(raw_msg)) => {
return msg_handler::save(&mbox, raw_msg, &output, &mut imap); return msg_handler::save(&mbox, raw_msg, &mut printer, &mut imap);
} }
Some(msg_arg::Command::Search(query, page_size, page)) => { Some(msg_arg::Command::Search(query, max_width, page_size, page)) => {
return msg_handler::search(query, page_size, page, &account, &output, &mut imap); return msg_handler::search(
query,
max_width,
page_size,
page,
&account,
&mut printer,
&mut imap,
);
} }
Some(msg_arg::Command::Send(raw_msg)) => { Some(msg_arg::Command::Send(raw_msg)) => {
return msg_handler::send(raw_msg, &output, &mut imap, &mut smtp); return msg_handler::send(raw_msg, &mut printer, &mut imap, &mut smtp);
} }
Some(msg_arg::Command::Write(atts)) => { Some(msg_arg::Command::Write(atts)) => {
return msg_handler::write(atts, &account, &output, &mut imap, &mut smtp); return msg_handler::write(atts, &account, &mut printer, &mut imap, &mut smtp);
} }
Some(msg_arg::Command::Flag(m)) => match m { Some(msg_arg::Command::Flag(m)) => match m {
Some(flag_arg::Command::Set(seq_range, flags)) => { Some(flag_arg::Command::Set(seq_range, flags)) => {
return flag_handler::set(seq_range, flags, &output, &mut imap); return flag_handler::set(seq_range, flags, &mut printer, &mut imap);
} }
Some(flag_arg::Command::Add(seq_range, flags)) => { Some(flag_arg::Command::Add(seq_range, flags)) => {
return flag_handler::add(seq_range, flags, &output, &mut imap); return flag_handler::add(seq_range, flags, &mut printer, &mut imap);
} }
Some(flag_arg::Command::Remove(seq_range, flags)) => { Some(flag_arg::Command::Remove(seq_range, flags)) => {
return flag_handler::remove(seq_range, flags, &output, &mut imap); return flag_handler::remove(seq_range, flags, &mut printer, &mut imap);
} }
_ => (), _ => (),
}, },
Some(msg_arg::Command::Tpl(m)) => match m { Some(msg_arg::Command::Tpl(m)) => match m {
Some(tpl_arg::Command::New(tpl)) => { Some(tpl_arg::Command::New(tpl)) => {
return tpl_handler::new(tpl, &account, &output); return tpl_handler::new(tpl, &account, &mut printer);
} }
Some(tpl_arg::Command::Reply(seq, all, tpl)) => { Some(tpl_arg::Command::Reply(seq, all, tpl)) => {
return tpl_handler::reply(seq, all, tpl, &account, &output, &mut imap); return tpl_handler::reply(seq, all, tpl, &account, &mut printer, &mut imap);
} }
Some(tpl_arg::Command::Forward(seq, tpl)) => { Some(tpl_arg::Command::Forward(seq, tpl)) => {
return tpl_handler::forward(seq, tpl, &account, &output, &mut imap); return tpl_handler::forward(seq, tpl, &account, &mut printer, &mut imap);
} }
_ => (), _ => (),
}, },

View file

@ -5,8 +5,14 @@ pub mod output_arg;
pub mod output_utils; pub mod output_utils;
pub use output_utils::*; pub use output_utils::*;
pub mod output_service; pub mod output_entity;
pub use output_service::*; pub use output_entity::*;
pub mod print; pub mod print;
pub use print::*; pub use print::*;
pub mod print_table;
pub use print_table::*;
pub mod printer_service;
pub use printer_service::*;

View file

@ -0,0 +1,57 @@
use anyhow::{anyhow, Error, Result};
use serde::Serialize;
use std::{
convert::TryFrom,
fmt::{self, Display},
};
/// Represents the available output formats.
#[derive(Debug, PartialEq)]
pub enum OutputFmt {
Plain,
Json,
}
impl From<&str> for OutputFmt {
fn from(fmt: &str) -> Self {
match fmt {
slice if slice.eq_ignore_ascii_case("json") => Self::Json,
_ => Self::Plain,
}
}
}
impl TryFrom<Option<&str>> for OutputFmt {
type Error = Error;
fn try_from(fmt: Option<&str>) -> Result<Self, Self::Error> {
match fmt {
Some(fmt) if fmt.eq_ignore_ascii_case("json") => Ok(Self::Json),
Some(fmt) if fmt.eq_ignore_ascii_case("plain") => Ok(Self::Plain),
None => Ok(Self::Plain),
Some(fmt) => Err(anyhow!(r#"cannot parse output format "{}""#, fmt)),
}
}
}
impl Display for OutputFmt {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let fmt = match self {
&OutputFmt::Json => "JSON",
&OutputFmt::Plain => "Plain",
};
write!(f, "{}", fmt)
}
}
/// Defines a struct-wrapper to provide a JSON output.
#[derive(Debug, Serialize, Clone)]
pub struct OutputJson<T: Serialize> {
response: T,
}
impl<T: Serialize> OutputJson<T> {
pub fn new(response: T) -> Self {
Self { response }
}
}

View file

@ -1,132 +0,0 @@
use anyhow::{anyhow, Error, Result};
use atty::Stream;
use log::debug;
use serde::Serialize;
use std::{
convert::{TryFrom, TryInto},
fmt,
};
use termcolor::{ColorChoice, StandardStream};
use crate::output::Print;
#[derive(Debug, PartialEq)]
pub enum OutputFmt {
Plain,
Json,
}
impl From<&str> for OutputFmt {
fn from(fmt: &str) -> Self {
match fmt {
slice if slice.eq_ignore_ascii_case("json") => Self::Json,
_ => Self::Plain,
}
}
}
impl TryFrom<Option<&str>> for OutputFmt {
type Error = Error;
fn try_from(fmt: Option<&str>) -> Result<Self, Self::Error> {
match fmt {
Some(slice) if slice.eq_ignore_ascii_case("json") => Ok(Self::Json),
Some(slice) if slice.eq_ignore_ascii_case("plain") => Ok(Self::Plain),
None => Ok(Self::Plain),
Some(slice) => Err(anyhow!("cannot parse output `{}`", slice)),
}
}
}
impl fmt::Display for OutputFmt {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let slice = match self {
&OutputFmt::Json => "JSON",
&OutputFmt::Plain => "PLAIN",
};
write!(f, "{}", slice)
}
}
// JSON output helper
/// A little struct-wrapper to provide a JSON output.
#[derive(Debug, Serialize, Clone)]
pub struct OutputJson<T: Serialize> {
response: T,
}
impl<T: Serialize> OutputJson<T> {
pub fn new(response: T) -> Self {
Self { response }
}
}
pub trait OutputServiceInterface {
fn print<T: Serialize + Print>(&self, data: T) -> Result<()>;
fn is_json(&self) -> bool;
}
#[derive(Debug)]
pub struct OutputService {
fmt: OutputFmt,
}
impl OutputServiceInterface for OutputService {
/// Print the provided item out according to the formatting setting when you created this
/// struct.
fn print<T: Serialize + Print>(&self, data: T) -> Result<()> {
match self.fmt {
OutputFmt::Plain => {
data.print(&mut StandardStream::stdout(if atty::isnt(Stream::Stdin) {
// Colors should be deactivated if the terminal is not a tty.
ColorChoice::Never
} else {
// Otherwise let's `termcolor` decide by inspecting the environment. From the [doc]:
// - If `NO_COLOR` is set to any value, then colors will be suppressed.
// - If `TERM` is set to dumb, then colors will be suppressed.
// - In non-Windows environments, if `TERM` is not set, then colors will be suppressed.
//
// [doc]: https://github.com/BurntSushi/termcolor#automatic-color-selection
ColorChoice::Auto
}))?;
}
OutputFmt::Json => {
print!("{}", serde_json::to_string(&OutputJson::new(data))?)
}
};
Ok(())
}
/// Returns true, if the formatting should be json.
fn is_json(&self) -> bool {
self.fmt == OutputFmt::Json
}
}
impl Default for OutputService {
fn default() -> Self {
Self {
fmt: OutputFmt::Plain,
}
}
}
impl From<&str> for OutputService {
fn from(fmt: &str) -> Self {
debug!("init output service");
debug!("output: `{:?}`", fmt);
let fmt = fmt.into();
Self { fmt }
}
}
impl TryFrom<Option<&str>> for OutputService {
type Error = Error;
fn try_from(fmt: Option<&str>) -> Result<Self, Self::Error> {
debug!("init output service");
debug!("output: `{:?}`", fmt);
let fmt = fmt.try_into()?;
Ok(Self { fmt })
}
}

View file

@ -1,6 +1,7 @@
use anyhow::Result; use anyhow::Result;
use std::process::Command; use std::process::Command;
/// TODO: move this in a more approriate place.
pub fn run_cmd(cmd: &str) -> Result<String> { pub fn run_cmd(cmd: &str) -> Result<String> {
let output = if cfg!(target_os = "windows") { let output = if cfg!(target_os = "windows") {
Command::new("cmd").args(&["/C", cmd]).output() Command::new("cmd").args(&["/C", cmd]).output()

View file

@ -1,28 +1,23 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use std::io; use log::error;
use termcolor::{StandardStream, WriteColor};
pub trait WriteWithColor: io::Write + WriteColor {} use crate::output::WriteColor;
impl WriteWithColor for StandardStream {}
pub trait Print { pub trait Print {
fn print<W: WriteWithColor>(&self, writter: &mut W) -> Result<()>; fn print(&self, writter: &mut dyn WriteColor) -> Result<()>;
fn println<W: WriteWithColor>(&self, writter: &mut W) -> Result<()> {
println!();
self.print(writter)
}
} }
impl Print for &str { impl Print for &str {
fn print<W: WriteWithColor>(&self, writter: &mut W) -> Result<()> { fn print(&self, writter: &mut dyn WriteColor) -> Result<()> {
write!(writter, "{}", self).context(format!(r#"cannot print string "{}""#, self)) write!(writter, "{}", self).with_context(|| {
error!(r#"cannot write string to writter: "{}""#, self);
"cannot write string to writter"
})
} }
} }
impl Print for String { impl Print for String {
fn print<W: WriteWithColor>(&self, writter: &mut W) -> Result<()> { fn print(&self, writter: &mut dyn WriteColor) -> Result<()> {
self.as_str().print(writter) self.as_str().print(writter)
} }
} }

15
src/output/print_table.rs Normal file
View file

@ -0,0 +1,15 @@
use anyhow::Result;
use std::io;
use termcolor::{self, StandardStream};
pub trait WriteColor: io::Write + termcolor::WriteColor {}
impl WriteColor for StandardStream {}
pub trait PrintTable {
fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()>;
}
pub struct PrintTableOpts {
pub max_width: Option<usize>,
}

View file

@ -0,0 +1,78 @@
use anyhow::{Context, Error, Result};
use atty::Stream;
use serde::Serialize;
use std::{convert::TryFrom, fmt::Debug};
use termcolor::{ColorChoice, StandardStream};
use crate::output::{OutputFmt, OutputJson, Print, PrintTable, PrintTableOpts, WriteColor};
pub trait PrinterService {
fn print<T: Debug + Print + Serialize>(&mut self, data: T) -> Result<()>;
fn print_table<T: Debug + PrintTable + Serialize>(
&mut self,
data: T,
opts: PrintTableOpts,
) -> Result<()>;
fn is_json(&self) -> bool;
}
pub struct StdoutPrinter {
pub writter: Box<dyn WriteColor>,
pub fmt: OutputFmt,
}
impl PrinterService for StdoutPrinter {
fn print<T: Debug + Print + Serialize>(&mut self, data: T) -> Result<()> {
match self.fmt {
OutputFmt::Plain => data.print(self.writter.as_mut()),
OutputFmt::Json => serde_json::to_writer(self.writter.as_mut(), &OutputJson::new(data))
.context("cannot write JSON to writter"),
}
}
fn print_table<T: Debug + PrintTable + Serialize>(
&mut self,
data: T,
opts: PrintTableOpts,
) -> Result<()> {
match self.fmt {
OutputFmt::Plain => data.print_table(self.writter.as_mut(), opts),
OutputFmt::Json => serde_json::to_writer(self.writter.as_mut(), &OutputJson::new(data))
.context("cannot write JSON to writter"),
}
}
fn is_json(&self) -> bool {
self.fmt == OutputFmt::Json
}
}
impl From<OutputFmt> for StdoutPrinter {
fn from(fmt: OutputFmt) -> Self {
let writter = StandardStream::stdout(if atty::isnt(Stream::Stdin) {
// Colors should be deactivated if the terminal is not a tty.
ColorChoice::Never
} else {
// Otherwise let's `termcolor` decide by inspecting the environment. From the [doc]:
// - If `NO_COLOR` is set to any value, then colors will be suppressed.
// - If `TERM` is set to dumb, then colors will be suppressed.
// - In non-Windows environments, if `TERM` is not set, then colors will be suppressed.
//
// [doc]: https://github.com/BurntSushi/termcolor#automatic-color-selection
ColorChoice::Auto
});
let writter = Box::new(writter);
Self { writter, fmt }
}
}
impl TryFrom<Option<&str>> for StdoutPrinter {
type Error = Error;
fn try_from(fmt: Option<&str>) -> Result<Self> {
Ok(Self {
fmt: OutputFmt::try_from(fmt)?,
..Self::from(OutputFmt::Plain)
})
}
}

View file

@ -1,7 +1,9 @@
//! Module related to User Interface. //! Module related to User Interface.
pub mod choice; pub mod table_arg;
pub mod editor;
pub mod table; pub mod table;
pub use table::*; pub use table::*;
pub mod choice;
pub mod editor;

View file

@ -10,7 +10,7 @@ use termcolor::{Color, ColorSpec};
use terminal_size; use terminal_size;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use crate::output::{Print, WriteWithColor}; use crate::output::{Print, PrintTableOpts, WriteColor};
/// Defines the default terminal size. /// Defines the default terminal size.
/// This is used when the size cannot be determined by the `terminal_size` crate. /// This is used when the size cannot be determined by the `terminal_size` crate.
@ -117,20 +117,7 @@ impl Cell {
/// Makes the cell printable. /// Makes the cell printable.
impl Print for Cell { impl Print for Cell {
fn print<W: WriteWithColor>(&self, writter: &mut W) -> Result<()> { fn print(&self, writter: &mut dyn WriteColor) -> Result<()> {
//let color_choice = if atty::isnt(Stream::Stdin) {
// // Colors should be deactivated if the terminal is not a tty.
// ColorChoice::Never
//} else {
// // Otherwise let's `termcolor` decide by inspecting the environment. From the [doc]:
// // - If `NO_COLOR` is set to any value, then colors will be suppressed.
// // - If `TERM` is set to dumb, then colors will be suppressed.
// // - In non-Windows environments, if `TERM` is not set, then colors will be suppressed.
// //
// // [doc]: https://github.com/BurntSushi/termcolor#automatic-color-selection
// ColorChoice::Auto
//};
// Applies colors to the cell // Applies colors to the cell
writter writter
.set_color(&self.style) .set_color(&self.style)
@ -170,16 +157,12 @@ where
/// Defines the row template. /// Defines the row template.
fn row(&self) -> Row; fn row(&self) -> Row;
/// Determines the max width of the table. /// Writes the table to the writter.
/// The default implementation takes the terminal width as the maximum width of the table. fn print(writter: &mut dyn WriteColor, items: &[Self], opts: PrintTableOpts) -> Result<()> {
fn max_width() -> usize { let max_width = opts
terminal_size::terminal_size() .max_width
.map(|(w, _)| w.0 as usize) .or_else(|| terminal_size::terminal_size().map(|(w, _)| w.0 as usize))
.unwrap_or(DEFAULT_TERM_WIDTH) .unwrap_or(DEFAULT_TERM_WIDTH);
}
/// Prints the table.
fn println<W: WriteWithColor>(writter: &mut W, items: &[Self]) -> Result<()> {
let mut table = vec![Self::head()]; let mut table = vec![Self::head()];
let mut cell_widths: Vec<usize> = let mut cell_widths: Vec<usize> =
table[0].0.iter().map(|cell| cell.unicode_width()).collect(); table[0].0.iter().map(|cell| cell.unicode_width()).collect();
@ -206,11 +189,11 @@ where
for (i, cell) in row.0.iter_mut().enumerate() { for (i, cell) in row.0.iter_mut().enumerate() {
glue.print(writter)?; glue.print(writter)?;
let table_is_overflowing = table_width > Self::max_width(); let table_is_overflowing = table_width > max_width;
if table_is_overflowing && cell.is_shrinkable() { if table_is_overflowing && cell.is_shrinkable() {
trace!("table is overflowing and cell is shrinkable"); trace!("table is overflowing and cell is shrinkable");
let shrink_width = table_width - Self::max_width(); let shrink_width = table_width - max_width;
trace!("shrink width: {}", shrink_width); trace!("shrink width: {}", shrink_width);
let cell_width = if shrink_width + MAX_SHRINK_WIDTH < cell_widths[i] { let cell_width = if shrink_width + MAX_SHRINK_WIDTH < cell_widths[i] {
cell_widths[i] - shrink_width cell_widths[i] - shrink_width
@ -265,8 +248,6 @@ where
} }
writeln!(writter)?; writeln!(writter)?;
} }
writeln!(writter)?;
Ok(()) Ok(())
} }
} }
@ -274,7 +255,6 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::io; use std::io;
use termcolor::WriteColor;
use super::*; use super::*;
@ -296,7 +276,7 @@ mod tests {
} }
} }
impl WriteColor for StringWritter { impl termcolor::WriteColor for StringWritter {
fn supports_color(&self) -> bool { fn supports_color(&self) -> bool {
false false
} }
@ -310,7 +290,7 @@ mod tests {
} }
} }
impl WriteWithColor for StringWritter {} impl WriteColor for StringWritter {}
struct Item { struct Item {
id: u16, id: u16,
@ -342,16 +322,11 @@ mod tests {
.cell(Cell::new(self.name.as_str()).shrinkable()) .cell(Cell::new(self.name.as_str()).shrinkable())
.cell(Cell::new(self.desc.as_str())) .cell(Cell::new(self.desc.as_str()))
} }
// Defines a fixed max width instead of terminal size for testing.
fn max_width() -> usize {
20
}
} }
macro_rules! write_items { macro_rules! write_items {
($writter:expr, $($item:expr),*) => { ($writter:expr, $($item:expr),*) => {
Table::println($writter, &[$($item,)*]).unwrap(); Table::print($writter, &[$($item,)*], PrintTableOpts { max_width: Some(20) }).unwrap();
}; };
} }
@ -369,7 +344,7 @@ mod tests {
"ID │NAME │DESC \n", "ID │NAME │DESC \n",
"1 │a │aa \n", "1 │a │aa \n",
"2 │b │bb \n", "2 │b │bb \n",
"3 │c │cc \n\n" "3 │c │cc \n",
]; ];
assert_eq!(expected, writter.content); assert_eq!(expected, writter.content);
} }
@ -388,7 +363,7 @@ mod tests {
"ID │NAME │DESC \n", "ID │NAME │DESC \n",
"1 │a │aa \n", "1 │a │aa \n",
"2222 │bbbbb │bbbbb \n", "2222 │bbbbb │bbbbb \n",
"3 │c │cc \n\n", "3 │c │cc \n",
]; ];
assert_eq!(expected, writter.content); assert_eq!(expected, writter.content);
@ -404,7 +379,7 @@ mod tests {
"ID │NAME │DESC \n", "ID │NAME │DESC \n",
"1 │a │aa \n", "1 │a │aa \n",
"2222 │bbbbb │bbbbb \n", "2222 │bbbbb │bbbbb \n",
"3 │cccccc │cc \n\n", "3 │cccccc │cc \n",
]; ];
assert_eq!(expected, writter.content); assert_eq!(expected, writter.content);
} }
@ -433,7 +408,7 @@ mod tests {
"5 │shriiiii… │desc \n", "5 │shriiiii… │desc \n",
"6 │😍😍😍😍 │desc \n", "6 │😍😍😍😍 │desc \n",
"7 │😍😍😍😍… │desc \n", "7 │😍😍😍😍… │desc \n",
"8 │!😍😍😍… │desc \n\n", "8 │!😍😍😍… │desc \n",
]; ];
assert_eq!(expected, writter.content); assert_eq!(expected, writter.content);
} }
@ -450,7 +425,7 @@ mod tests {
let expected = concat![ let expected = concat![
"ID │NAME │DESC \n", "ID │NAME │DESC \n",
"1111 │shri… │desc very looong \n", "1111 │shri… │desc very looong \n",
"2222 │shri… │desc very loooooooooong \n\n", "2222 │shri… │desc very loooooooooong \n",
]; ];
assert_eq!(expected, writter.content); assert_eq!(expected, writter.content);
} }

10
src/ui/table_arg.rs Normal file
View file

@ -0,0 +1,10 @@
use clap::Arg;
/// Defines the max table width argument.
pub fn max_width<'a>() -> Arg<'a, 'a> {
Arg::with_name("max-table-width")
.help("Defines a maximum width for the table")
.short("w")
.long("max-width")
.value_name("INT")
}

2
wiki

@ -1 +1 @@
Subproject commit 8cf79989facecaf4210db6d1eaa9f090975f5e25 Subproject commit 9fbd490bd4f42524cb0099e9914144375ea5514a