diff --git a/Cargo.lock b/Cargo.lock index 08d59a401..9294cc9cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,14 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi 0.3.8", +] + [[package]] name = "arc-swap" version = "0.4.7" @@ -27,6 +36,17 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi 0.3.8", +] + [[package]] name = "autocfg" version = "1.0.0" @@ -119,6 +139,21 @@ dependencies = [ "time", ] +[[package]] +name = "clap" +version = "2.33.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdfa80d47f954d53a35a64987ca1422f495b8d6483c0fe9f7117b36c2a792129" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -489,6 +524,15 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "heck" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "hermit-abi" version = "0.1.13" @@ -784,6 +828,7 @@ dependencies = [ "signal-hook", "signal-hook-registry", "smallvec", + "structopt", "termion", "toml", "unicode-segmentation", @@ -1170,6 +1215,32 @@ version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "237a5ed80e274dbc66f86bd59c1e25edc039660be53194b5fe0a482e0f2612ea" +[[package]] +name = "proc-macro-error" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98e9e4b82e0ef281812565ea4751049f1bdcdfccda7d3f459f2e138a40c08678" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f5444ead4e9935abd7f27dc51f7e852a0569ac888096d5ec2499470794e2e53" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "syn-mid", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.18" @@ -1542,6 +1613,36 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f3eb36b47e512f8f1c9e3d10c2c1965bc992bd9cdb024fa581e2194501c83d3" +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "structopt" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863246aaf5ddd0d6928dfeb1a9ca65f505599e4e1b399935ef7e75107516b4ef" +dependencies = [ + "clap", + "lazy_static", + "structopt-derive", +] + +[[package]] +name = "structopt-derive" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d239ca4b13aee7a2142e6795cbd69e457665ff8037aed33b3effdc430d2f927a" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "syn" version = "1.0.30" @@ -1553,6 +1654,17 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "syn-mid" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tempfile" version = "3.1.0" @@ -1586,6 +1698,15 @@ dependencies = [ "melib", ] +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + [[package]] name = "thread_local" version = "1.0.1" @@ -1701,6 +1822,12 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" +[[package]] +name = "unicode-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" + [[package]] name = "unicode-xid" version = "0.2.0" @@ -1734,6 +1861,12 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55d1e41d56121e07f1e223db0a4def204e45c85425f6a16d462fd07c8d10d74c" +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + [[package]] name = "version_check" version = "0.9.2" diff --git a/Cargo.toml b/Cargo.toml index 33da999e9..a1dc25851 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,8 @@ rmp-serde = "^0.14.0" smallvec = { version = "1.1.0", features = ["serde", ] } bitflags = "1.0" pcre2 = { version = "0.2.3", optional = true } +structopt = { version = "0.3.14" } + [profile.release] lto = true diff --git a/meli.1 b/meli.1 index 3527b9c1b..ff0a908d9 100644 --- a/meli.1 +++ b/meli.1 @@ -27,28 +27,26 @@ .Nm .Op Fl -help | h .Op Fl -version | v -.Op Fl -create-config Op Ar path -.Op Fl -test-config Op Ar path .Op Fl -config Ar path -.Op Fl -print-default-theme -.Op Fl -print-loaded-themes .Bl -tag -width flag -offset indent .It Fl -help | h Show help message and exit. .It Fl -version | v Show version and exit. -.It Fl -create-config Op Ar path +.It Fl -config Ar path +Start meli with given configuration file. +.It Cm create-config Op Ar path Create configuration file in .Pa path if given, or at .Pa $XDG_CONFIG_HOME/meli/config.toml -.It Fl -test-config Op Ar path +.It Cm test-config Op Ar path Test a configuration file for syntax issues or missing options. -.It Fl -config Ar path -Start meli with given configuration file. -.It Fl -print-default-theme +.It Cm man Op Ar page +Print documentation page and exit (Piping to a pager is recommended.) +.It Cm print-default-theme Print default theme keys and values in TOML syntax, to be used as a blueprint. -.It Fl -print-loaded-themes +.It Cm print-loaded-themes Print all loaded themes in TOML syntax. .El .Sh DESCRIPTION diff --git a/src/bin.rs b/src/bin.rs index c69949f22..fccb80503 100644 --- a/src/bin.rs +++ b/src/bin.rs @@ -29,7 +29,7 @@ use std::alloc::System; use std::collections::VecDeque; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; extern crate notify_rust; extern crate xdg_utils; #[macro_use] @@ -86,8 +86,6 @@ pub mod plugins; use nix; use std::os::raw::c_int; -use xdg; - fn notify( signals: &[c_int], sender: crossbeam::channel::Sender, @@ -143,29 +141,75 @@ fn notify( Ok(r) } -macro_rules! error_and_exit { - ($($err:expr),*) => {{ - return Err(MeliError::new(format!($($err),*))); - }} +fn parse_manpage(src: &str) -> Result { + match src { + "" | "meli" | "main" => Ok(ManPages::Main), + "meli.conf" | "conf" | "config" | "configuration" => Ok(ManPages::Conf), + "meli-themes" | "themes" | "theming" | "theme" => Ok(ManPages::Themes), + _ => Err(MeliError::new(format!( + "Invalid documentation page: {}", + src + ))), + } } +use structopt::StructOpt; + +#[derive(Debug)] +/// Choose manpage enum ManPages { + /// meli(1) Main = 0, + /// meli.conf(5) Conf = 1, + /// meli-themes(5) Themes = 2, } -struct CommandLineArguments { - print_manpage: Option, - create_config: Option, - test_config: Option, - config: Option, - help: bool, - version: bool, +#[derive(Debug, StructOpt)] +#[structopt(name = "meli", about = "terminal mail client", version_short = "v")] +struct Opt { + /// use specified configuration file + #[structopt(short, long, parse(from_os_str))] + config: Option, + + #[structopt(subcommand)] + subcommand: Option, +} + +#[derive(Debug, StructOpt)] +enum SubCommand { + /// print default theme in full to stdout and exit. + PrintDefaultTheme, + /// print loaded themes in full to stdout and exit. + PrintLoadedThemes, + /// create a sample configuration file with available configuration options. If PATH is not specified, meli will try to create it in $XDG_CONFIG_HOME/meli/config.toml + #[structopt(display_order = 1)] + CreateConfig { + #[structopt(value_name = "NEW_CONFIG_PATH", parse(from_os_str))] + path: Option, + }, + /// test a configuration file for syntax issues or missing options. + #[structopt(display_order = 2)] + TestConfig { + #[structopt(value_name = "CONFIG_PATH", parse(from_os_str))] + path: Option, + }, + #[structopt(visible_alias="docs", aliases=&["docs", "manpage", "manpages"])] + #[structopt(display_order = 3)] + /// print documentation page and exit (Piping to a pager is recommended.). + Man(ManOpt), +} + +#[derive(Debug, StructOpt)] +struct ManOpt { + #[structopt(default_value = "meli", possible_values=&["meli", "conf", "themes"], value_name="PAGE", parse(try_from_str = parse_manpage))] + page: ManPages, } fn main() { - ::std::process::exit(match run_app() { + let opt = Opt::from_args(); + ::std::process::exit(match run_app(opt) { Ok(()) => 0, Err(err) => { eprintln!("{}", err); @@ -174,184 +218,39 @@ fn main() { }); } -fn run_app() -> Result<()> { - enum CommandLineFlags { - PrintManPage, - CreateConfig, - TestConfig, - Config, +fn run_app(opt: Opt) -> Result<()> { + if let Some(config_location) = opt.config.as_ref() { + std::env::set_var("MELI_CONFIG", config_location); } - use CommandLineFlags::*; - let mut prev: Option = None; - let mut args = CommandLineArguments { - print_manpage: None, - create_config: None, - test_config: None, - config: None, - help: false, - version: false, - }; - for i in std::env::args().skip(1) { - match i.as_str() { - "--test-config" => match prev { - None => prev = Some(TestConfig), - Some(CreateConfig) => error_and_exit!("invalid value for flag `--create-config`"), - Some(Config) => error_and_exit!("invalid value for flag `--config`"), - Some(TestConfig) => error_and_exit!("invalid value for flag `--test-config`"), - Some(PrintManPage) => { - error_and_exit!("invalid value for flag `--print-documentation`") - } - }, - "--create-config" => match prev { - None => prev = Some(CreateConfig), - Some(CreateConfig) => error_and_exit!("invalid value for flag `--create-config`"), - Some(TestConfig) => error_and_exit!("invalid value for flag `--test-config`"), - Some(Config) => error_and_exit!("invalid value for flag `--config`"), - Some(PrintManPage) => { - error_and_exit!("invalid value for flag `--print-documentation`") - } - }, - "--config" | "-c" => match prev { - None => prev = Some(Config), - Some(CreateConfig) if args.create_config.is_none() => { - args.config = Some(String::new()); - prev = Some(Config); - } - Some(CreateConfig) => error_and_exit!("invalid value for flag `--create-config`"), - Some(Config) => error_and_exit!("invalid value for flag `--config`"), - Some(TestConfig) => error_and_exit!("invalid value for flag `--test-config`"), - Some(PrintManPage) => { - error_and_exit!("invalid value for flag `--print-documentation`") - } - }, - "--help" | "-h" => { - args.help = true; - } - "--version" | "-v" => { - args.version = true; - } - "--print-loaded-themes" => { - let s = conf::FileSettings::new()?; - print!("{}", s.terminal.themes.to_string()); - return Ok(()); - } - "--print-default-theme" => { - print!("{}", conf::Themes::default().key_to_string("dark", false)); - return Ok(()); - } - "--print-documentation" => { - if args.print_manpage.is_some() { - error_and_exit!("Multiple invocations of --print-documentation"); - } - prev = Some(PrintManPage); - args.print_manpage = Some(ManPages::Main); - } - e => match prev { - None => error_and_exit!("error: value without command {}", e), - Some(CreateConfig) if args.create_config.is_none() => { - args.create_config = Some(i); - prev = None; - } - Some(Config) if args.config.is_none() => { - args.config = Some(i); - prev = None; - } - Some(TestConfig) if args.test_config.is_none() => { - args.test_config = Some(i); - prev = None; - } - Some(PrintManPage) => { - match e { - "meli" | "main" => { /* This is the default */ } - "meli.conf" | "conf" | "config" | "configuration" => { - args.print_manpage = Some(ManPages::Conf); - } - "meli-themes" | "themes" | "theming" | "theme" => { - args.print_manpage = Some(ManPages::Themes); - } - _ => error_and_exit!("Invalid documentation page: {}", e), - } - } - Some(TestConfig) => error_and_exit!("Duplicate value for flag `--test-config`"), - Some(CreateConfig) => error_and_exit!("Duplicate value for flag `--create-config`"), - Some(Config) => error_and_exit!("Duplicate value for flag `--config`"), - }, + match opt.subcommand { + Some(SubCommand::TestConfig { path }) => { + let config_path = if let Some(path) = path { + path + } else { + crate::conf::get_config_file()? + }; + conf::FileSettings::validate(config_path)?; + return Ok(()); + } + Some(SubCommand::CreateConfig { path }) => { + let config_path = if let Some(path) = path { + path + } else { + crate::conf::get_config_file()? + }; + if config_path.exists() { + return Err(MeliError::new(format!( + "File `{}` already exists.\nMaybe you meant to specify another path?", + config_path.display() + ))); + } + conf::create_config_file(&config_path)?; + return Ok(()); } - } - - if args.help { - println!("usage:\tmeli [--config PATH|-c PATH]"); - println!("\tmeli --help"); - println!("\tmeli --version"); - println!(""); - println!("Other options:"); - println!("\t--help, -h\t\tshow this message and exit"); - println!("\t--version, -v\t\tprint version and exit"); - println!("\t--create-config[ PATH]\tcreate a sample configuration file with available configuration options. If PATH is not specified, meli will try to create it in $XDG_CONFIG_HOME/meli/config.toml"); - println!( - "\t--test-config PATH\ttest a configuration file for syntax issues or missing options." - ); - println!("\t--config PATH, -c PATH\tuse specified configuration file"); - println!("\t--print-loaded-themes\tprint loaded themes in full to stdout and exit."); - println!("\t--print-default-theme\tprint default theme in full to stdout and exit."); #[cfg(feature = "cli-docs")] - { - println!("\t--print-documentation [meli conf themes]\n\t\t\t\tprint documentation page and exit (Piping to a pager is recommended.)."); - } - return Ok(()); - } - - if args.version { - println!("meli {}", option_env!("CARGO_PKG_VERSION").unwrap_or("0.0")); - return Ok(()); - } - - match prev { - None => {} - Some(CreateConfig) if args.create_config.is_none() => args.create_config = Some("".into()), - Some(CreateConfig) => error_and_exit!("Duplicate value for flag `--create-config`"), - Some(Config) => error_and_exit!("error: flag without value: `--config`"), - Some(TestConfig) => error_and_exit!("error: flag without value: `--test-config`"), - Some(PrintManPage) => {} - }; - - if (args.print_manpage.is_some() - ^ args.test_config.is_some() - ^ args.create_config.is_some() - ^ args.config.is_some()) - && !(args.print_manpage.is_some() - || args.test_config.is_some() - || args.create_config.is_some() - || args.config.is_some()) - { - error_and_exit!("error: illegal command-line flag combination"); - } - - if let Some(config_path) = args.test_config.as_ref() { - conf::FileSettings::validate(config_path)?; - return Ok(()); - } else if let Some(config_path) = args.create_config.as_mut() { - let config_path: PathBuf = if config_path.is_empty() { - let xdg_dirs = xdg::BaseDirectories::with_prefix("meli").unwrap(); - xdg_dirs.place_config_file("config.toml").map_err(|e| { - MeliError::new(format!( - "Cannot create configuration directory in {}:\n{}", - xdg_dirs.get_config_home().display(), - e - )) - })? - } else { - Path::new(config_path).to_path_buf() - }; - if config_path.exists() { - return Err(MeliError::new(format!("File `{}` already exists.\nMaybe you meant to specify another path with --create-config=PATH", config_path.display()))); - } - conf::create_config_file(&config_path)?; - return Ok(()); - } else if let Some(_page) = args.print_manpage { - #[cfg(feature = "cli-docs")] - { + Some(SubCommand::Man(manopt)) => { + let _page = manopt.page; const MANPAGES: [&'static str; 3] = [ include_str!(concat!(env!("OUT_DIR"), "/meli.txt")), include_str!(concat!(env!("OUT_DIR"), "/meli.conf.txt")), @@ -361,13 +260,19 @@ fn run_app() -> Result<()> { return Ok(()); } #[cfg(not(feature = "cli-docs"))] - { - error_and_exit!("error: this version of meli was not build with embedded documentation. You might have it installed as manpages (eg `man meli`), otherwise check https://meli.delivery"); + Some(SubCommand::Man(_manopt)) => { + return Err(MeliError::new("error: this version of meli was not build with embedded documentation. You might have it installed as manpages (eg `man meli`), otherwise check https://meli.delivery")); } - } - - if let Some(config_location) = args.config.as_ref() { - std::env::set_var("MELI_CONFIG", config_location); + Some(SubCommand::PrintLoadedThemes) => { + let s = conf::FileSettings::new()?; + print!("{}", s.terminal.themes.to_string()); + return Ok(()); + } + Some(SubCommand::PrintDefaultTheme) => { + print!("{}", conf::Themes::default().key_to_string("dark", false)); + return Ok(()); + } + None => {} } /* Create a channel to communicate with other threads. The main process is the sole receiver. diff --git a/src/conf.rs b/src/conf.rs index f13922fda..3da36cdbf 100644 --- a/src/conf.rs +++ b/src/conf.rs @@ -313,17 +313,30 @@ pub struct Settings { pub log: LogSettings, } +pub fn get_config_file() -> Result { + let xdg_dirs = xdg::BaseDirectories::with_prefix("meli").map_err(|err| { + MeliError::new(format!( + "Could not detect XDG directories for user: {}", + err + )) + .set_source(Some(std::sync::Arc::new(Box::new(err)))) + })?; + match env::var("MELI_CONFIG") { + Ok(path) => Ok(PathBuf::from(path)), + Err(_) => Ok(xdg_dirs + .place_config_file("config.toml") + .chain_err_summary(|| { + format!( + "Cannot create configuration directory in {}", + xdg_dirs.get_config_home().display() + ) + })?), + } +} + impl FileSettings { pub fn new() -> Result { - let xdg_dirs = xdg::BaseDirectories::with_prefix("meli"); - let config_path = match env::var("MELI_CONFIG") { - Ok(path) => PathBuf::from(path), - Err(_) => xdg_dirs - .as_ref() - .unwrap() - .place_config_file("config.toml") - .expect("cannot create configuration directory"), - }; + let config_path = get_config_file()?; if !config_path.exists() { println!( "No configuration found. Would you like to generate one in {}? [Y/n]", @@ -359,18 +372,15 @@ impl FileSettings { } } - let path = config_path - .to_str() - .expect("Configuration file path was not valid UTF-8"); - FileSettings::validate(path) + FileSettings::validate(config_path) } - pub fn validate(path: &str) -> Result { - let s = pp::pp(path)?; + pub fn validate(path: PathBuf) -> Result { + let s = pp::pp(&path)?; let mut s: FileSettings = toml::from_str(&s).map_err(|e| { MeliError::new(format!( "{}:\nConfig file contains errors: {}", - path, + path.display(), e.to_string() )) })?;