mirror of
https://github.com/soywod/himalaya.git
synced 2025-04-22 17:23:27 +00:00
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:
parent
192445d7e4
commit
e154481c5b
23 changed files with 457 additions and 346 deletions
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
))
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
76
src/main.rs
76
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);
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
|
|
|
@ -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::*;
|
||||
|
|
57
src/output/output_entity.rs
Normal file
57
src/output/output_entity.rs
Normal 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 }
|
||||
}
|
||||
}
|
|
@ -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 })
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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
15
src/output/print_table.rs
Normal 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>,
|
||||
}
|
78
src/output/printer_service.rs
Normal file
78
src/output/printer_service.rs
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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
10
src/ui/table_arg.rs
Normal 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
2
wiki
|
@ -1 +1 @@
|
|||
Subproject commit 8cf79989facecaf4210db6d1eaa9f090975f5e25
|
||||
Subproject commit 9fbd490bd4f42524cb0099e9914144375ea5514a
|
Loading…
Add table
Reference in a new issue