From e154481c5bc822c7bb37171ef071fb4c796cedf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sun, 24 Oct 2021 21:02:02 +0200 Subject: [PATCH] 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 --- CHANGELOG.md | 4 +- src/domain/mbox/mbox_arg.rs | 23 +++-- src/domain/mbox/mbox_handler.rs | 104 ++++++++++++++++------- src/domain/mbox/mboxes_entity.rs | 10 ++- src/domain/msg/envelope_entity.rs | 2 +- src/domain/msg/envelopes_entity.rs | 12 +-- src/domain/msg/flag_handler.rs | 20 ++--- src/domain/msg/msg_arg.rs | 37 ++++++-- src/domain/msg/msg_entity.rs | 12 +-- src/domain/msg/msg_handler.rs | 88 ++++++++++--------- src/domain/msg/tpl_handler.rs | 20 ++--- src/main.rs | 76 +++++++++++------ src/output/mod.rs | 10 ++- src/output/output_entity.rs | 57 +++++++++++++ src/output/output_service.rs | 132 ----------------------------- src/output/output_utils.rs | 1 + src/output/print.rs | 23 ++--- src/output/print_table.rs | 15 ++++ src/output/printer_service.rs | 78 +++++++++++++++++ src/ui/mod.rs | 6 +- src/ui/table.rs | 61 ++++--------- src/ui/table_arg.rs | 10 +++ wiki | 2 +- 23 files changed, 457 insertions(+), 346 deletions(-) create mode 100644 src/output/output_entity.rs delete mode 100644 src/output/output_service.rs create mode 100644 src/output/print_table.rs create mode 100644 src/output/printer_service.rs create mode 100644 src/ui/table_arg.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index c3bdd0e..98c2815 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Disable color support [#185] +- Disable color feature [#185] +- `--max-width|-w` argument to restrict listing table width [#220] ### 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 [#205]: https://github.com/soywod/himalaya/issues/205 [#215]: https://github.com/soywod/himalaya/issues/215 +[#220]: https://github.com/soywod/himalaya/issues/220 [#228]: https://github.com/soywod/himalaya/issues/228 [#229]: https://github.com/soywod/himalaya/issues/229 diff --git a/src/domain/mbox/mbox_arg.rs b/src/domain/mbox/mbox_arg.rs index 23ace1e..5469934 100644 --- a/src/domain/mbox/mbox_arg.rs +++ b/src/domain/mbox/mbox_arg.rs @@ -7,18 +7,26 @@ use anyhow::Result; use clap; use log::trace; +use crate::ui::table_arg; + +type MaxTableWidth = Option; + /// Represents the mailbox commands. #[derive(Debug, PartialEq, Eq)] pub enum Cmd { /// Represents the list mailboxes command. - List, + List(MaxTableWidth), } /// Defines the mailbox command matcher. pub fn matches(m: &clap::ArgMatches) -> Result> { - if let Some(_) = m.subcommand_matches("mailboxes") { + if let Some(m) = m.subcommand_matches("mailboxes") { trace!("mailboxes subcommand matched"); - return Ok(Some(Cmd::List)); + let max_table_width = m + .value_of("max-table-width") + .and_then(|width| width.parse::().ok()); + trace!(r#"max table width: "{:?}""#, max_table_width); + return Ok(Some(Cmd::List(max_table_width))); } Ok(None) @@ -28,7 +36,8 @@ pub fn matches(m: &clap::ArgMatches) -> Result> { pub fn subcmds<'a>() -> Vec> { vec![clap::SubCommand::with_name("mailboxes") .aliases(&["mailbox", "mboxes", "mbox", "mb", "m"]) - .about("Lists mailboxes")] + .about("Lists mailboxes") + .arg(table_arg::max_width())] } /// Defines the source mailbox argument. @@ -58,8 +67,12 @@ mod tests { let arg = clap::App::new("himalaya") .subcommands(subcmds()) .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] diff --git a/src/domain/mbox/mbox_handler.rs b/src/domain/mbox/mbox_handler.rs index 0111949..ceb6db9 100644 --- a/src/domain/mbox/mbox_handler.rs +++ b/src/domain/mbox/mbox_handler.rs @@ -5,43 +5,89 @@ use anyhow::Result; use log::trace; -use crate::{domain::ImapServiceInterface, output::OutputServiceInterface}; +use crate::{ + domain::ImapServiceInterface, + output::{PrintTableOpts, PrinterService}, +}; -/// List all mailboxes. -pub fn list<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>( - output: &OutputService, +/// Lists all mailboxes. +pub fn list<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>( + max_width: Option, + printer: &mut Printer, imap: &'a mut ImapService, ) -> Result<()> { let mboxes = imap.fetch_mboxes()?; trace!("mailboxes: {:#?}", mboxes); - output.print(mboxes) + printer.print_table(mboxes, PrintTableOpts { max_width }) } #[cfg(test)] mod tests { use serde::Serialize; + use std::{fmt::Debug, io}; + use termcolor::ColorSpec; - use super::*; use crate::{ config::Config, domain::{AttrRemote, Attrs, Envelopes, Flags, Mbox, Mboxes, Msg}, - output::{OutputJson, Print}, + output::{Print, PrintTable, WriteColor}, }; + use super::*; + #[test] fn it_should_list_mboxes() { - struct OutputServiceTest; + #[derive(Debug, Default, Clone)] + struct StringWritter { + content: String, + } - impl OutputServiceInterface for OutputServiceTest { - fn print(&self, data: T) -> Result<()> { - let data = serde_json::to_string(&OutputJson::new(data))?; - assert_eq!( - data, - r#"{"response":[{"delim":"/","name":"INBOX","attrs":["NoSelect"]},{"delim":"/","name":"Sent","attrs":["NoInferiors",{"Custom":"HasNoChildren"}]}]}"# - ); - Ok(()) + impl io::Write for StringWritter { + fn write(&mut self, buf: &[u8]) -> io::Result { + 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( + &mut self, + data: T, + opts: PrintTableOpts, + ) -> Result<()> { + data.print_table(&mut self.writter, opts)?; + Ok(()) + } + fn print(&mut self, _data: T) -> Result<()> { + unimplemented!() + } fn is_json(&self) -> bool { unimplemented!() } @@ -71,59 +117,57 @@ mod tests { fn notify(&mut self, _: &Config, _: u64) -> Result<()> { unimplemented!() } - fn watch(&mut self, _: u64) -> Result<()> { unimplemented!() } - fn fetch_envelopes(&mut self, _: &usize, _: &usize) -> Result { unimplemented!() } - fn fetch_envelopes_with(&mut self, _: &str, _: &usize, _: &usize) -> Result { unimplemented!() } - fn find_msg(&mut self, _: &str) -> Result { unimplemented!() } - fn find_raw_msg(&mut self, _: &str) -> Result> { unimplemented!() } - fn append_msg(&mut self, _: &Mbox, _: Msg) -> Result<()> { unimplemented!() } - fn append_raw_msg_with_flags(&mut self, _: &Mbox, _: &[u8], _: Flags) -> Result<()> { unimplemented!() } - fn expunge(&mut self) -> Result<()> { unimplemented!() } - fn logout(&mut self) -> Result<()> { unimplemented!() } - fn add_flags(&mut self, _: &str, _: &Flags) -> Result<()> { unimplemented!() } - fn set_flags(&mut self, _: &str, _: &Flags) -> Result<()> { unimplemented!() } - fn remove_flags(&mut self, _: &str, _: &Flags) -> Result<()> { unimplemented!() } } - let output = OutputServiceTest {}; + let mut printer = PrinterServiceTest::default(); 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 + ); } } diff --git a/src/domain/mbox/mboxes_entity.rs b/src/domain/mbox/mboxes_entity.rs index f554d73..4c437f0 100644 --- a/src/domain/mbox/mboxes_entity.rs +++ b/src/domain/mbox/mboxes_entity.rs @@ -8,7 +8,7 @@ use std::ops::Deref; use crate::{ domain::{Mbox, RawMbox}, - output::{Print, WriteWithColor}, + output::{PrintTable, PrintTableOpts, WriteColor}, ui::Table, }; @@ -29,10 +29,12 @@ impl<'a> Deref for Mboxes<'a> { } /// Makes the mailboxes printable. -impl<'a> Print for Mboxes<'a> { - fn print(&self, writter: &mut W) -> Result<()> { +impl<'a> PrintTable for Mboxes<'a> { + fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { writeln!(writter)?; - Table::println(writter, &self) + Table::print(writter, &self, opts)?; + writeln!(writter)?; + Ok(()) } } diff --git a/src/domain/msg/envelope_entity.rs b/src/domain/msg/envelope_entity.rs index a5af8f3..013e760 100644 --- a/src/domain/msg/envelope_entity.rs +++ b/src/domain/msg/envelope_entity.rs @@ -4,7 +4,7 @@ use std::{borrow::Cow, convert::TryFrom}; use crate::{ domain::msg::{Flag, Flags}, - ui::table::{Cell, Row, Table}, + ui::{Cell, Row, Table}, }; pub type RawEnvelope = imap::types::Fetch; diff --git a/src/domain/msg/envelopes_entity.rs b/src/domain/msg/envelopes_entity.rs index 9cd2600..6402317 100644 --- a/src/domain/msg/envelopes_entity.rs +++ b/src/domain/msg/envelopes_entity.rs @@ -4,7 +4,7 @@ use std::{convert::TryFrom, ops::Deref}; use crate::{ domain::{msg::Envelope, RawEnvelope}, - output::{Print, WriteWithColor}, + output::{PrintTable, PrintTableOpts, WriteColor}, ui::Table, }; @@ -36,9 +36,11 @@ impl<'a> TryFrom<&'a RawEnvelopes> for Envelopes<'a> { } } -impl<'a> Print for Envelopes<'a> { - fn print(&self, writter: &mut W) -> Result<()> { - println!(); - Table::println(writter, &self) +impl<'a> PrintTable for Envelopes<'a> { + fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { + writeln!(writter)?; + Table::print(writter, &self, opts)?; + writeln!(writter)?; + Ok(()) } } diff --git a/src/domain/msg/flag_handler.rs b/src/domain/msg/flag_handler.rs index c14d114..4d7ad39 100644 --- a/src/domain/msg/flag_handler.rs +++ b/src/domain/msg/flag_handler.rs @@ -6,20 +6,20 @@ use anyhow::Result; use crate::{ domain::{Flags, ImapServiceInterface}, - output::OutputServiceInterface, + output::PrinterService, }; /// Adds flags to all messages matching the given sequence range. /// 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, flags: Vec<&'a str>, - output: &'a OutputService, + printer: &'a mut Printer, imap: &'a mut ImapService, ) -> Result<()> { let flags = Flags::from(flags); imap.add_flags(seq_range, &flags)?; - output.print(format!( + printer.print(format!( r#"Flag(s) "{}" successfully added to message(s) "{}""#, 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. /// 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, flags: Vec<&'a str>, - output: &'a OutputService, + printer: &'a mut Printer, imap: &'a mut ImapService, ) -> Result<()> { let flags = Flags::from(flags); imap.remove_flags(seq_range, &flags)?; - output.print(format!( + printer.print(format!( r#"Flag(s) "{}" successfully removed from message(s) "{}""#, 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. /// 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, flags: Vec<&'a str>, - output: &'a OutputService, + printer: &'a mut Printer, imap: &'a mut ImapService, ) -> Result<()> { let flags = Flags::from(flags); imap.set_flags(seq_range, &flags)?; - output.print(format!( + printer.print(format!( r#"Flag(s) "{}" successfully set for message(s) "{}""#, flags, seq_range )) diff --git a/src/domain/msg/msg_arg.rs b/src/domain/msg/msg_arg.rs index 6d9b003..989a8f4 100644 --- a/src/domain/msg/msg_arg.rs +++ b/src/domain/msg/msg_arg.rs @@ -6,9 +6,12 @@ use anyhow::Result; use clap::{self, App, Arg, ArgMatches, SubCommand}; use log::{debug, trace}; -use crate::domain::{ - mbox::mbox_arg, - msg::{flag_arg, msg_arg, tpl_arg}, +use crate::{ + domain::{ + mbox::mbox_arg, + msg::{flag_arg, msg_arg, tpl_arg}, + }, + ui::table_arg, }; type Seq<'a> = &'a str; @@ -21,6 +24,7 @@ type All = bool; type RawMsg<'a> = &'a str; type Query = String; type AttachmentsPaths<'a> = Vec<&'a str>; +type MaxTableWidth = Option; /// Message commands. pub enum Command<'a> { @@ -28,12 +32,12 @@ pub enum Command<'a> { Copy(Seq<'a>, Mbox<'a>), Delete(Seq<'a>), Forward(Seq<'a>, AttachmentsPaths<'a>), - List(Option, Page), + List(MaxTableWidth, Option, Page), Move(Seq<'a>, Mbox<'a>), Read(Seq<'a>, TextMime<'a>, Raw), Reply(Seq<'a>, All, AttachmentsPaths<'a>), Save(RawMsg<'a>), - Search(Query, Option, Page), + Search(Query, MaxTableWidth, Option, Page), Send(RawMsg<'a>), Write(AttachmentsPaths<'a>), @@ -77,6 +81,10 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { if let Some(m) = m.subcommand_matches("list") { debug!("list command matched"); + let max_table_width = m + .value_of("max-table-width") + .and_then(|width| width.parse::().ok()); + trace!(r#"max table width: "{:?}""#, max_table_width); let page_size = m.value_of("page-size").and_then(|s| s.parse().ok()); trace!(r#"page size: "{:?}""#, page_size); let page = m @@ -87,7 +95,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { .map(|page| 1.max(page) - 1) .unwrap_or_default(); 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") { @@ -130,6 +138,10 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { if let Some(m) = m.subcommand_matches("search") { debug!("search command matched"); + let max_table_width = m + .value_of("max-table-width") + .and_then(|width| width.parse::().ok()); + trace!(r#"max table width: "{:?}""#, max_table_width); let page_size = m.value_of("page-size").and_then(|s| s.parse().ok()); trace!(r#"page size: "{:?}""#, page_size); let page = m @@ -165,7 +177,12 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { .1 .join(" "); 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") { @@ -191,7 +208,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { } debug!("default list command matched"); - Ok(Some(Command::List(None, 0))) + Ok(Some(Command::List(None, None, 0))) } /// Message sequence number argument. @@ -262,12 +279,14 @@ pub fn subcmds<'a>() -> Vec> { .aliases(&["lst", "l"]) .about("Lists all messages") .arg(page_size_arg()) - .arg(page_arg()), + .arg(page_arg()) + .arg(table_arg::max_width()), SubCommand::with_name("search") .aliases(&["s", "query", "q"]) .about("Lists messages matching the given IMAP query") .arg(page_size_arg()) .arg(page_arg()) + .arg(table_arg::max_width()) .arg( Arg::with_name("query") .help("IMAP query") diff --git a/src/domain/msg/msg_entity.rs b/src/domain/msg/msg_entity.rs index a1dca68..edc6072 100644 --- a/src/domain/msg/msg_entity.rs +++ b/src/domain/msg/msg_entity.rs @@ -22,7 +22,7 @@ use crate::{ msg::{msg_utils, BinaryPart, Flags, Part, Parts, TextPlainPart, TplOverride}, smtp::SmtpServiceInterface, }, - output::OutputServiceInterface, + output::PrinterService, ui::{ choice::{self, PostEditChoice, PreEditChoice}, editor, @@ -298,13 +298,13 @@ impl Msg { pub fn edit_with_editor< 'a, - OutputService: OutputServiceInterface, + Printer: PrinterService, ImapService: ImapServiceInterface<'a>, SmtpService: SmtpServiceInterface, >( mut self, account: &Account, - output: &OutputService, + printer: &mut Printer, imap: &mut ImapService, smtp: &mut SmtpService, ) -> Result<()> { @@ -342,7 +342,7 @@ impl Msg { let flags = Flags::try_from(vec![Flag::Seen])?; imap.append_raw_msg_with_flags(&mbox, &sent_msg.formatted(), flags)?; msg_utils::remove_local_draft()?; - output.print("Message successfully sent")?; + printer.print("Message successfully sent")?; break; } Ok(PostEditChoice::Edit) => { @@ -350,7 +350,7 @@ impl Msg { continue; } Ok(PostEditChoice::LocalDraft) => { - output.print("Message successfully saved locally")?; + printer.print("Message successfully saved locally")?; break; } Ok(PostEditChoice::RemoteDraft) => { @@ -359,7 +359,7 @@ impl Msg { let tpl = self.to_tpl(TplOverride::default(), account); imap.append_raw_msg_with_flags(&mbox, tpl.as_bytes(), flags)?; msg_utils::remove_local_draft()?; - output.print("Message successfully saved to Drafts")?; + printer.print("Message successfully saved to Drafts")?; break; } Ok(PostEditChoice::Discard) => { diff --git a/src/domain/msg/msg_handler.rs b/src/domain/msg/msg_handler.rs index 8195f5b..88b152c 100644 --- a/src/domain/msg/msg_handler.rs +++ b/src/domain/msg/msg_handler.rs @@ -22,18 +22,14 @@ use crate::{ msg::{Flags, Msg, Part, TextPlainPart}, smtp::SmtpServiceInterface, }, - output::OutputServiceInterface, + output::{PrintTableOpts, PrinterService}, }; /// Download all message attachments to the user account downloads directory. -pub fn attachments< - 'a, - OutputService: OutputServiceInterface, - ImapService: ImapServiceInterface<'a>, ->( +pub fn attachments<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>( seq: &str, account: &Account, - output: &OutputService, + printer: &mut Printer, imap: &mut ImapService, ) -> Result<()> { let attachments = imap.find_msg(&seq)?.attachments(); @@ -50,75 +46,76 @@ pub fn attachments< .context(format!("cannot download attachment {:?}", filepath))?; } - output.print(format!( + printer.print(format!( "{} attachment(s) successfully downloaded to {:?}", attachments_len, account.downloads_dir )) } /// 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, mbox: &str, - output: &OutputService, + printer: &mut Printer, imap: &mut ImapService, ) -> Result<()> { let mbox = Mbox::new(mbox); let msg = imap.find_raw_msg(&seq)?; let flags = Flags::try_from(vec![Flag::Seen])?; imap.append_raw_msg_with_flags(&mbox, &msg, flags)?; - output.print(format!( + printer.print(format!( r#"Message {} successfully copied to folder "{}""#, seq, mbox )) } /// 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, - output: &OutputService, + printer: &mut Printer, imap: &mut ImapService, ) -> Result<()> { let flags = Flags::try_from(vec![Flag::Seen, Flag::Deleted])?; imap.add_flags(seq, &flags)?; 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. pub fn forward< 'a, - OutputService: OutputServiceInterface, + Printer: PrinterService, ImapService: ImapServiceInterface<'a>, SmtpService: SmtpServiceInterface, >( seq: &str, attachments_paths: Vec<&str>, account: &Account, - output: &OutputService, + printer: &mut Printer, imap: &mut ImapService, smtp: &mut SmtpService, ) -> Result<()> { imap.find_msg(seq)? .into_forward(account)? .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. -pub fn list<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>( +pub fn list<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>( + max_width: Option, page_size: Option, page: usize, account: &Account, - output: &OutputService, - imap: &mut ImapService, + printer: &mut Printer, + imap: &'a mut ImapService, ) -> Result<()> { let page_size = page_size.unwrap_or(account.default_page_size); trace!("page size: {}", page_size); let msgs = imap.fetch_envelopes(&page_size, &page)?; trace!("messages: {:#?}", msgs); - output.print(msgs) + printer.print_table(msgs, PrintTableOpts { max_width }) } /// 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 pub fn mailto< 'a, - OutputService: OutputServiceInterface, + Printer: PrinterService, ImapService: ImapServiceInterface<'a>, SmtpService: SmtpServiceInterface, >( url: &Url, account: &Account, - output: &OutputService, + printer: &mut Printer, imap: &mut ImapService, smtp: &mut SmtpService, ) -> Result<()> { @@ -174,16 +171,16 @@ pub fn mailto< msg.parts.push(Part::TextPlain(TextPlainPart { 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. -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 seq: &str, // The mailbox to move the message in mbox: &str, - output: &OutputService, + printer: &mut Printer, imap: &mut ImapService, ) -> Result<()> { // Copy the message to targetted mailbox @@ -197,18 +194,18 @@ pub fn move_<'a, OutputService: OutputServiceInterface, ImapService: ImapService imap.add_flags(seq, &flags)?; imap.expunge()?; - output.print(format!( + printer.print(format!( r#"Message {} successfully moved to folder "{}""#, seq, mbox )) } /// 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, text_mime: &str, raw: bool, - output: &OutputService, + printer: &mut Printer, imap: &mut ImapService, ) -> Result<()> { 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) }; - output.print(msg) + printer.print(msg) } /// Reply to the given message UID. pub fn reply< 'a, - OutputService: OutputServiceInterface, + Printer: PrinterService, ImapService: ImapServiceInterface<'a>, SmtpService: SmtpServiceInterface, >( @@ -232,26 +229,26 @@ pub fn reply< all: bool, attachments_paths: Vec<&str>, account: &Account, - output: &OutputService, + printer: &mut Printer, imap: &mut ImapService, smtp: &mut SmtpService, ) -> Result<()> { imap.find_msg(seq)? .into_reply(all, account)? .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])?; imap.add_flags(seq, &flags) } /// 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, raw_msg: &str, - output: &OutputService, + printer: &mut Printer, imap: &mut ImapService, ) -> 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") } else { io::stdin() @@ -268,12 +265,13 @@ pub fn save<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceI } /// 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, + max_width: Option, page_size: Option, page: usize, account: &Account, - output: &OutputService, + printer: &mut Printer, imap: &'a mut ImapService, ) -> Result<()> { 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)?; trace!("messages: {:#?}", msgs); - output.print(msgs) + printer.print_table(msgs, PrintTableOpts { max_width }) } /// Send a raw message. pub fn send< 'a, - OutputService: OutputServiceInterface, + Printer: PrinterService, ImapService: ImapServiceInterface<'a>, SmtpService: SmtpServiceInterface, >( raw_msg: &str, - output: &OutputService, + printer: &mut Printer, imap: &mut ImapService, smtp: &mut SmtpService, ) -> 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") } else { io::stdin() @@ -322,17 +320,17 @@ pub fn send< /// Compose a new message. pub fn write< 'a, - OutputService: OutputServiceInterface, + Printer: PrinterService, ImapService: ImapServiceInterface<'a>, SmtpService: SmtpServiceInterface, >( attachments_paths: Vec<&str>, account: &Account, - output: &OutputService, + printer: &mut Printer, imap: &mut ImapService, smtp: &mut SmtpService, ) -> Result<()> { Msg::default() .add_attachments(attachments_paths)? - .edit_with_editor(account, output, imap, smtp) + .edit_with_editor(account, printer, imap, smtp) } diff --git a/src/domain/msg/tpl_handler.rs b/src/domain/msg/tpl_handler.rs index 26105cf..d700bbe 100644 --- a/src/domain/msg/tpl_handler.rs +++ b/src/domain/msg/tpl_handler.rs @@ -10,46 +10,46 @@ use crate::{ imap::ImapServiceInterface, msg::{Msg, TplOverride}, }, - output::OutputServiceInterface, + output::PrinterService, }; /// Generate a new message template. -pub fn new<'a, OutputService: OutputServiceInterface>( +pub fn new<'a, Printer: PrinterService>( opts: TplOverride<'a>, account: &'a Account, - output: &'a OutputService, + printer: &'a mut Printer, ) -> Result<()> { let tpl = Msg::default().to_tpl(opts, account); - output.print(tpl) + printer.print(tpl) } /// 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, all: bool, opts: TplOverride<'a>, account: &'a Account, - output: &'a OutputService, + printer: &'a mut Printer, imap: &'a mut ImapService, ) -> Result<()> { let tpl = imap .find_msg(seq)? .into_reply(all, account)? .to_tpl(opts, account); - output.print(tpl) + printer.print(tpl) } /// 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, opts: TplOverride<'a>, account: &'a Account, - output: &'a OutputService, + printer: &'a mut Printer, imap: &'a mut ImapService, ) -> Result<()> { let tpl = imap .find_msg(seq)? .into_forward(account)? .to_tpl(opts, account); - output.print(tpl) + printer.print(tpl) } diff --git a/src/main.rs b/src/main.rs index 4d8e564..2217de1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ use anyhow::Result; use clap; use env_logger; +use output::StdoutPrinter; use std::{convert::TryFrom, env}; use url::Url; @@ -18,7 +19,7 @@ use domain::{ msg::{flag_arg, flag_handler, msg_arg, msg_handler, tpl_arg, tpl_handler}, smtp::SmtpService, }; -use output::{output_arg, OutputService}; +use output::{output_arg, OutputFmt}; fn create_app<'a>() -> clap::App<'a, 'a> { clap::App::new(env!("CARGO_PKG_NAME")) @@ -47,11 +48,11 @@ fn main() -> Result<()> { let mbox = Mbox::new("INBOX"); let config = Config::try_from(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 mut imap = ImapService::from((&account, &mbox)); 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(); @@ -70,7 +71,7 @@ fn main() -> Result<()> { let mbox = Mbox::new(m.value_of("mbox-source").unwrap()); let config = Config::try_from(m.value_of("config"))?; 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 smtp = SmtpService::from(&account); @@ -87,8 +88,8 @@ fn main() -> Result<()> { // Check mailbox commands. match mbox_arg::matches(&m)? { - Some(mbox_arg::Cmd::List) => { - return mbox_handler::list(&output, &mut imap); + Some(mbox_arg::Cmd::List(max_width)) => { + return mbox_handler::list(max_width, &mut printer, &mut imap); } _ => (), } @@ -96,62 +97,85 @@ fn main() -> Result<()> { // Check message commands. match msg_arg::matches(&m)? { 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)) => { - 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)) => { - return msg_handler::delete(seq, &output, &mut imap); + return msg_handler::delete(seq, &mut printer, &mut imap); } 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)) => { - return msg_handler::list(page_size, page, &account, &output, &mut imap); + Some(msg_arg::Command::List(max_width, page_size, page)) => { + return msg_handler::list( + max_width, + page_size, + page, + &account, + &mut printer, + &mut imap, + ); } 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)) => { - 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)) => { - 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)) => { - 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)) => { - return msg_handler::search(query, page_size, page, &account, &output, &mut imap); + Some(msg_arg::Command::Search(query, max_width, page_size, page)) => { + return msg_handler::search( + query, + max_width, + page_size, + page, + &account, + &mut printer, + &mut imap, + ); } 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)) => { - 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(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)) => { - 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)) => { - 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(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)) => { - 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)) => { - return tpl_handler::forward(seq, tpl, &account, &output, &mut imap); + return tpl_handler::forward(seq, tpl, &account, &mut printer, &mut imap); } _ => (), }, diff --git a/src/output/mod.rs b/src/output/mod.rs index 3435281..2955eae 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -5,8 +5,14 @@ pub mod output_arg; pub mod output_utils; pub use output_utils::*; -pub mod output_service; -pub use output_service::*; +pub mod output_entity; +pub use output_entity::*; pub mod print; pub use print::*; + +pub mod print_table; +pub use print_table::*; + +pub mod printer_service; +pub use printer_service::*; diff --git a/src/output/output_entity.rs b/src/output/output_entity.rs new file mode 100644 index 0000000..e92b488 --- /dev/null +++ b/src/output/output_entity.rs @@ -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> for OutputFmt { + type Error = Error; + + fn try_from(fmt: Option<&str>) -> Result { + 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 { + response: T, +} + +impl OutputJson { + pub fn new(response: T) -> Self { + Self { response } + } +} diff --git a/src/output/output_service.rs b/src/output/output_service.rs deleted file mode 100644 index 2a908bc..0000000 --- a/src/output/output_service.rs +++ /dev/null @@ -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> for OutputFmt { - type Error = Error; - - fn try_from(fmt: Option<&str>) -> Result { - 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 { - response: T, -} - -impl OutputJson { - pub fn new(response: T) -> Self { - Self { response } - } -} - -pub trait OutputServiceInterface { - fn 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(&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> for OutputService { - type Error = Error; - - fn try_from(fmt: Option<&str>) -> Result { - debug!("init output service"); - debug!("output: `{:?}`", fmt); - let fmt = fmt.try_into()?; - Ok(Self { fmt }) - } -} diff --git a/src/output/output_utils.rs b/src/output/output_utils.rs index cedc17d..4f44494 100644 --- a/src/output/output_utils.rs +++ b/src/output/output_utils.rs @@ -1,6 +1,7 @@ use anyhow::Result; use std::process::Command; +/// TODO: move this in a more approriate place. pub fn run_cmd(cmd: &str) -> Result { let output = if cfg!(target_os = "windows") { Command::new("cmd").args(&["/C", cmd]).output() diff --git a/src/output/print.rs b/src/output/print.rs index 261c263..023a678 100644 --- a/src/output/print.rs +++ b/src/output/print.rs @@ -1,28 +1,23 @@ use anyhow::{Context, Result}; -use std::io; -use termcolor::{StandardStream, WriteColor}; +use log::error; -pub trait WriteWithColor: io::Write + WriteColor {} - -impl WriteWithColor for StandardStream {} +use crate::output::WriteColor; pub trait Print { - fn print(&self, writter: &mut W) -> Result<()>; - - fn println(&self, writter: &mut W) -> Result<()> { - println!(); - self.print(writter) - } + fn print(&self, writter: &mut dyn WriteColor) -> Result<()>; } impl Print for &str { - fn print(&self, writter: &mut W) -> Result<()> { - write!(writter, "{}", self).context(format!(r#"cannot print string "{}""#, self)) + fn print(&self, writter: &mut dyn WriteColor) -> Result<()> { + write!(writter, "{}", self).with_context(|| { + error!(r#"cannot write string to writter: "{}""#, self); + "cannot write string to writter" + }) } } impl Print for String { - fn print(&self, writter: &mut W) -> Result<()> { + fn print(&self, writter: &mut dyn WriteColor) -> Result<()> { self.as_str().print(writter) } } diff --git a/src/output/print_table.rs b/src/output/print_table.rs new file mode 100644 index 0000000..e3dacd6 --- /dev/null +++ b/src/output/print_table.rs @@ -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, +} diff --git a/src/output/printer_service.rs b/src/output/printer_service.rs new file mode 100644 index 0000000..c875c94 --- /dev/null +++ b/src/output/printer_service.rs @@ -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(&mut self, data: T) -> Result<()>; + fn print_table( + &mut self, + data: T, + opts: PrintTableOpts, + ) -> Result<()>; + fn is_json(&self) -> bool; +} + +pub struct StdoutPrinter { + pub writter: Box, + pub fmt: OutputFmt, +} + +impl PrinterService for StdoutPrinter { + fn print(&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( + &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 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> for StdoutPrinter { + type Error = Error; + + fn try_from(fmt: Option<&str>) -> Result { + Ok(Self { + fmt: OutputFmt::try_from(fmt)?, + ..Self::from(OutputFmt::Plain) + }) + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 7016534..4210d06 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,7 +1,9 @@ //! Module related to User Interface. -pub mod choice; -pub mod editor; +pub mod table_arg; pub mod table; pub use table::*; + +pub mod choice; +pub mod editor; diff --git a/src/ui/table.rs b/src/ui/table.rs index 6765ca0..b1739d7 100644 --- a/src/ui/table.rs +++ b/src/ui/table.rs @@ -10,7 +10,7 @@ use termcolor::{Color, ColorSpec}; use terminal_size; use unicode_width::UnicodeWidthStr; -use crate::output::{Print, WriteWithColor}; +use crate::output::{Print, PrintTableOpts, WriteColor}; /// Defines the default terminal size. /// This is used when the size cannot be determined by the `terminal_size` crate. @@ -117,20 +117,7 @@ impl Cell { /// Makes the cell printable. impl Print for Cell { - fn print(&self, writter: &mut W) -> 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 - //}; - + fn print(&self, writter: &mut dyn WriteColor) -> Result<()> { // Applies colors to the cell writter .set_color(&self.style) @@ -170,16 +157,12 @@ where /// Defines the row template. fn row(&self) -> Row; - /// Determines the max width of the table. - /// The default implementation takes the terminal width as the maximum width of the table. - fn max_width() -> usize { - terminal_size::terminal_size() - .map(|(w, _)| w.0 as usize) - .unwrap_or(DEFAULT_TERM_WIDTH) - } - - /// Prints the table. - fn println(writter: &mut W, items: &[Self]) -> Result<()> { + /// Writes the table to the writter. + fn print(writter: &mut dyn WriteColor, items: &[Self], opts: PrintTableOpts) -> Result<()> { + let max_width = opts + .max_width + .or_else(|| terminal_size::terminal_size().map(|(w, _)| w.0 as usize)) + .unwrap_or(DEFAULT_TERM_WIDTH); let mut table = vec![Self::head()]; let mut cell_widths: Vec = table[0].0.iter().map(|cell| cell.unicode_width()).collect(); @@ -206,11 +189,11 @@ where for (i, cell) in row.0.iter_mut().enumerate() { 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() { 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); let cell_width = if shrink_width + MAX_SHRINK_WIDTH < cell_widths[i] { cell_widths[i] - shrink_width @@ -265,8 +248,6 @@ where } writeln!(writter)?; } - - writeln!(writter)?; Ok(()) } } @@ -274,7 +255,6 @@ where #[cfg(test)] mod tests { use std::io; - use termcolor::WriteColor; use super::*; @@ -296,7 +276,7 @@ mod tests { } } - impl WriteColor for StringWritter { + impl termcolor::WriteColor for StringWritter { fn supports_color(&self) -> bool { false } @@ -310,7 +290,7 @@ mod tests { } } - impl WriteWithColor for StringWritter {} + impl WriteColor for StringWritter {} struct Item { id: u16, @@ -342,16 +322,11 @@ mod tests { .cell(Cell::new(self.name.as_str()).shrinkable()) .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 { ($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", "1 │a │aa \n", "2 │b │bb \n", - "3 │c │cc \n\n" + "3 │c │cc \n", ]; assert_eq!(expected, writter.content); } @@ -388,7 +363,7 @@ mod tests { "ID │NAME │DESC \n", "1 │a │aa \n", "2222 │bbbbb │bbbbb \n", - "3 │c │cc \n\n", + "3 │c │cc \n", ]; assert_eq!(expected, writter.content); @@ -404,7 +379,7 @@ mod tests { "ID │NAME │DESC \n", "1 │a │aa \n", "2222 │bbbbb │bbbbb \n", - "3 │cccccc │cc \n\n", + "3 │cccccc │cc \n", ]; assert_eq!(expected, writter.content); } @@ -433,7 +408,7 @@ mod tests { "5 │shriiiii… │desc \n", "6 │😍😍😍😍 │desc \n", "7 │😍😍😍😍… │desc \n", - "8 │!😍😍😍… │desc \n\n", + "8 │!😍😍😍… │desc \n", ]; assert_eq!(expected, writter.content); } @@ -450,7 +425,7 @@ mod tests { let expected = concat![ "ID │NAME │DESC \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); } diff --git a/src/ui/table_arg.rs b/src/ui/table_arg.rs new file mode 100644 index 0000000..29ec40b --- /dev/null +++ b/src/ui/table_arg.rs @@ -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") +} diff --git a/wiki b/wiki index 8cf7998..9fbd490 160000 --- a/wiki +++ b/wiki @@ -1 +1 @@ -Subproject commit 8cf79989facecaf4210db6d1eaa9f090975f5e25 +Subproject commit 9fbd490bd4f42524cb0099e9914144375ea5514a