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
- 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

View file

@ -7,18 +7,26 @@ use anyhow::Result;
use clap;
use log::trace;
use crate::ui::table_arg;
type MaxTableWidth = Option<usize>;
/// 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<Option<Cmd>> {
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::<usize>().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<Option<Cmd>> {
pub fn subcmds<'a>() -> Vec<clap::App<'a, 'a>> {
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]

View file

@ -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<usize>,
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<T: Serialize + 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<usize> {
self.content
.push_str(&String::from_utf8(buf.to_vec()).unwrap());
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
self.content = String::default();
Ok(())
}
}
impl termcolor::WriteColor for StringWritter {
fn supports_color(&self) -> bool {
false
}
fn set_color(&mut self, _spec: &ColorSpec) -> io::Result<()> {
io::Result::Ok(())
}
fn reset(&mut self) -> io::Result<()> {
io::Result::Ok(())
}
}
impl WriteColor for StringWritter {}
#[derive(Debug, Default)]
struct PrinterServiceTest {
pub writter: StringWritter,
}
impl PrinterService for PrinterServiceTest {
fn print_table<T: Debug + PrintTable + 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 {
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<Envelopes> {
unimplemented!()
}
fn fetch_envelopes_with(&mut self, _: &str, _: &usize, _: &usize) -> Result<Envelopes> {
unimplemented!()
}
fn find_msg(&mut self, _: &str) -> Result<Msg> {
unimplemented!()
}
fn find_raw_msg(&mut self, _: &str) -> Result<Vec<u8>> {
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
);
}
}

View file

@ -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<W: WriteWithColor>(&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(())
}
}

View file

@ -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;

View file

@ -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<W: WriteWithColor>(&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(())
}
}

View file

@ -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
))

View file

@ -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<usize>;
/// 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<PageSize>, Page),
List(MaxTableWidth, Option<PageSize>, Page),
Move(Seq<'a>, Mbox<'a>),
Read(Seq<'a>, TextMime<'a>, Raw),
Reply(Seq<'a>, All, AttachmentsPaths<'a>),
Save(RawMsg<'a>),
Search(Query, Option<PageSize>, Page),
Search(Query, MaxTableWidth, Option<PageSize>, Page),
Send(RawMsg<'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") {
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());
trace!(r#"page size: "{:?}""#, page_size);
let page = m
@ -87,7 +95,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
.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<Option<Command<'a>>> {
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::<usize>().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<Option<Command<'a>>> {
.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<Option<Command<'a>>> {
}
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<App<'a, 'a>> {
.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")

View file

@ -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) => {

View file

@ -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<usize>,
page_size: Option<usize>,
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<usize>,
page_size: Option<usize>,
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)
}

View file

@ -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)
}

View file

@ -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);
}
_ => (),
},

View file

@ -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::*;

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

View file

@ -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<W: WriteWithColor>(&self, writter: &mut W) -> Result<()>;
fn println<W: WriteWithColor>(&self, writter: &mut W) -> Result<()> {
println!();
self.print(writter)
}
fn print(&self, writter: &mut dyn WriteColor) -> Result<()>;
}
impl Print for &str {
fn print<W: WriteWithColor>(&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<W: WriteWithColor>(&self, writter: &mut W) -> Result<()> {
fn print(&self, writter: &mut dyn WriteColor) -> Result<()> {
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.
pub mod choice;
pub mod editor;
pub mod table_arg;
pub mod table;
pub use table::*;
pub mod choice;
pub mod editor;

View file

@ -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<W: WriteWithColor>(&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<W: WriteWithColor>(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<usize> =
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);
}

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