refactor man and completion with clap derive api

This commit is contained in:
Clément DOUIN 2023-12-05 22:38:08 +01:00
parent 7a10a7fc25
commit d2308221d7
No known key found for this signature in database
GPG key ID: 353E4A18EE0FAB72
22 changed files with 270 additions and 233 deletions

30
Cargo.lock generated
View file

@ -470,6 +470,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2275f18819641850fa26c89acc84d465c1bf91ce57bc2748b28c420473352f64" checksum = "2275f18819641850fa26c89acc84d465c1bf91ce57bc2748b28c420473352f64"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive",
] ]
[[package]] [[package]]
@ -493,6 +494,18 @@ dependencies = [
"clap", "clap",
] ]
[[package]]
name = "clap_derive"
version = "4.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.39",
]
[[package]] [[package]]
name = "clap_lex" name = "clap_lex"
version = "0.6.0" version = "0.6.0"
@ -1063,12 +1076,12 @@ dependencies = [
[[package]] [[package]]
name = "env_logger" name = "env_logger"
version = "0.8.4" version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece"
dependencies = [ dependencies = [
"atty",
"humantime", "humantime",
"is-terminal",
"log", "log",
"regex", "regex",
"termcolor", "termcolor",
@ -2371,6 +2384,17 @@ version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
[[package]]
name = "is-terminal"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
dependencies = [
"hermit-abi 0.3.3",
"rustix",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.9" version = "1.0.9"

View file

@ -60,6 +60,7 @@ version = "0.4.24"
[dependencies.clap] [dependencies.clap]
version = "4.0" version = "4.0"
features = ["derive"]
[dependencies.clap_complete] [dependencies.clap_complete]
version = "4.0" version = "4.0"
@ -80,7 +81,7 @@ version = "4.0.0"
version = "0.2.4" version = "0.2.4"
[dependencies.env_logger] [dependencies.env_logger]
version = "0.8" version = "0.10"
[dependencies.erased-serde] [dependencies.erased-serde]
version = "0.3" version = "0.3"

View file

@ -33,12 +33,20 @@ pub enum Cmd {
/// Represents the account command matcher. /// Represents the account command matcher.
pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> { pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
let cmd = if let Some(m) = m.subcommand_matches(CMD_ACCOUNT) { let cmd = if let Some(m) = m.subcommand_matches(CMD_ACCOUNT) {
if let Some(m) = m.subcommand_matches(CMD_SYNC) { if let Some(m) = m.subcommand_matches(CMD_CONFIGURE) {
info!("configure account subcommand matched");
let reset = parse_reset_flag(m);
Some(Cmd::Configure(reset))
} else if let Some(m) = m.subcommand_matches(CMD_LIST) {
info!("list accounts subcommand matched");
let max_table_width = table::args::parse_max_width(m);
Some(Cmd::List(max_table_width))
} else if let Some(m) = m.subcommand_matches(CMD_SYNC) {
info!("sync account subcommand matched"); info!("sync account subcommand matched");
let dry_run = parse_dry_run_arg(m); let dry_run = parse_dry_run_arg(m);
let include = folder::args::parse_include_arg(m); let include = folder::args::parse_include_arg(m);
let exclude = folder::args::parse_exclude_arg(m); let exclude = folder::args::parse_exclude_arg(m);
let folders_strategy = if let Some(folder) = folder::args::parse_source_arg(m) { let folders_strategy = if let Some(folder) = folder::args::parse_global_source_arg(m) {
Some(FolderSyncStrategy::Include(HashSet::from_iter([ Some(FolderSyncStrategy::Include(HashSet::from_iter([
folder.to_owned() folder.to_owned()
]))) ])))
@ -52,17 +60,8 @@ pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
None None
}; };
Some(Cmd::Sync(folders_strategy, dry_run)) Some(Cmd::Sync(folders_strategy, dry_run))
} else if let Some(m) = m.subcommand_matches(CMD_LIST) {
info!("list accounts subcommand matched");
let max_table_width = table::args::parse_max_width(m);
Some(Cmd::List(max_table_width))
} else if let Some(m) = m.subcommand_matches(CMD_CONFIGURE) {
info!("configure account subcommand matched");
let reset = parse_reset_flag(m);
Some(Cmd::Configure(reset))
} else { } else {
info!("no account subcommand matched, falling back to subcommand list"); None
Some(Cmd::List(None))
} }
} else { } else {
None None
@ -75,10 +74,26 @@ pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
pub fn subcmd() -> Command { pub fn subcmd() -> Command {
Command::new(CMD_ACCOUNT) Command::new(CMD_ACCOUNT)
.about("Subcommand to manage accounts") .about("Subcommand to manage accounts")
.subcommands([ .long_about("Subcommand to manage accounts like configure, list or sync")
.aliases(["accounts", "acc"])
.subcommand_required(true)
.arg_required_else_help(true)
.subcommand(
Command::new(CMD_CONFIGURE)
.about("Configure the given account")
.aliases(["config", "conf", "cfg"])
.arg(reset_flag())
.arg(folder::args::source_arg(
"Define the account to be configured",
)),
)
.subcommand(
Command::new(CMD_LIST) Command::new(CMD_LIST)
.about("List all accounts from the config file") .about("List all accounts")
.long_about("List all accounts that are set up in the configuration file")
.arg(table::args::max_width()), .arg(table::args::max_width()),
)
.subcommand(
Command::new(CMD_SYNC) Command::new(CMD_SYNC)
.about("Synchronize the given account locally") .about("Synchronize the given account locally")
.arg(folder::args::all_arg("Synchronize all folders")) .arg(folder::args::all_arg("Synchronize all folders"))
@ -89,26 +104,27 @@ pub fn subcmd() -> Command {
"Synchronize all folders except the given ones", "Synchronize all folders except the given ones",
)) ))
.arg(dry_run()), .arg(dry_run()),
Command::new(CMD_CONFIGURE) )
.about("Configure the current selected account")
.aliases(["config", "conf", "cfg"])
.arg(reset_flag()),
])
} }
/// Represents the user account name argument. This argument allows /// Represents the user account name argument. This argument allows
/// the user to select a different account than the default one. /// the user to select a different account than the default one.
pub fn arg() -> Arg { pub fn global_args() -> impl IntoIterator<Item = Arg> {
Arg::new(ARG_ACCOUNT) [Arg::new(ARG_ACCOUNT)
.help("Set the account") .help("Override the default account")
.long_help(
"Override the default account
The given account will be used by default for all other commands (when applicable).",
)
.long("account") .long("account")
.short('a') .short('a')
.global(true) .global(true)
.value_name("STRING") .value_name("name")]
} }
/// Represents the user account name argument parser. /// Represents the user account name argument parser.
pub fn parse_arg(matches: &ArgMatches) -> Option<&str> { pub fn parse_global_arg(matches: &ArgMatches) -> Option<&str> {
matches.get_one::<String>(ARG_ACCOUNT).map(String::as_str) matches.get_one::<String>(ARG_ACCOUNT).map(String::as_str)
} }

17
src/cache/args.rs vendored
View file

@ -6,19 +6,24 @@ const ARG_DISABLE_CACHE: &str = "disable-cache";
/// Represents the disable cache flag argument. This argument allows /// Represents the disable cache flag argument. This argument allows
/// the user to disable any sort of cache. /// the user to disable any sort of cache.
pub fn arg() -> Arg { pub fn global_args() -> impl IntoIterator<Item = Arg> {
Arg::new(ARG_DISABLE_CACHE) [Arg::new(ARG_DISABLE_CACHE)
.help("Disable any sort of cache") .help("Disable any sort of cache")
.long_help( .long_help(
"Disable any sort of cache. The action depends on "Disable any sort of cache.
the command it applies on.",
The action depends on commands it apply on. For example, when listing
envelopes using the IMAP backend, this flag will ensure that envelopes
are fetched from the IMAP server and not from the synchronized local
Maildir.",
) )
.long("disable-cache") .long("disable-cache")
.alias("no-cache")
.global(true) .global(true)
.action(ArgAction::SetTrue) .action(ArgAction::SetTrue)]
} }
/// Represents the disable cache flag parser. /// Represents the disable cache flag parser.
pub fn parse_disable_cache_flag(m: &ArgMatches) -> bool { pub fn parse_disable_cache_arg(m: &ArgMatches) -> bool {
m.get_flag(ARG_DISABLE_CACHE) m.get_flag(ARG_DISABLE_CACHE)
} }

View file

@ -1,39 +0,0 @@
//! Module related to completion CLI.
//!
//! This module provides subcommands and a command matcher related to completion.
use anyhow::Result;
use clap::{value_parser, Arg, ArgMatches, Command};
use clap_complete::Shell;
use log::debug;
const ARG_SHELL: &str = "shell";
const CMD_COMPLETION: &str = "completion";
type SomeShell = Shell;
/// Completion commands.
pub enum Cmd {
/// Generate completion script for the given shell.
Generate(SomeShell),
}
/// Completion command matcher.
pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
if let Some(m) = m.subcommand_matches(CMD_COMPLETION) {
let shell = m.get_one::<Shell>(ARG_SHELL).cloned().unwrap();
debug!("shell: {:?}", shell);
return Ok(Some(Cmd::Generate(shell)));
};
Ok(None)
}
/// Completion subcommands.
pub fn subcmd() -> Command {
Command::new(CMD_COMPLETION)
.about("Generate the completion script for the given shell")
.args(&[Arg::new(ARG_SHELL)
.value_parser(value_parser!(Shell))
.required(true)])
}

10
src/completion/command.rs Normal file
View file

@ -0,0 +1,10 @@
use clap::{value_parser, Parser};
use clap_complete::Shell;
/// Print completion script for the given shell to stdout
#[derive(Debug, Parser)]
pub struct Generate {
/// Shell that completion script should be generated for
#[arg(value_parser = value_parser!(Shell))]
pub shell: Shell,
}

18
src/completion/handler.rs Normal file
View file

@ -0,0 +1,18 @@
use anyhow::Result;
use clap::Command;
use clap_complete::Shell;
use std::io::stdout;
use crate::printer::Printer;
pub fn generate(printer: &mut impl Printer, mut cmd: Command, shell: Shell) -> Result<()> {
let name = cmd.get_name().to_string();
clap_complete::generate(shell, &mut cmd, name, &mut stdout());
printer.print(format!(
"Shell script successfully generated for shell {shell}!"
))?;
Ok(())
}

View file

@ -1,15 +0,0 @@
//! Module related to completion handling.
//!
//! This module gathers all completion commands.
use anyhow::Result;
use clap::Command;
use clap_complete::Shell;
use std::io::stdout;
/// Generates completion script from the given [`clap::App`] for the given shell slice.
pub fn generate<'a>(mut cmd: Command, shell: Shell) -> Result<()> {
let name = cmd.get_name().to_string();
clap_complete::generate(shell, &mut cmd, name, &mut stdout());
Ok(())
}

View file

@ -1,8 +1,2 @@
//! Module related to shell completion. pub mod command;
//! pub mod handler;
//! This module allows users to generate autocompletion scripts for
//! their shells. You can see the list of available shells directly on
//! the clap's [docs.rs](https://docs.rs/clap/2.33.3/clap/enum.Shell.html).
pub mod args;
pub mod handlers;

View file

@ -6,16 +6,21 @@ const ARG_CONFIG: &str = "config";
/// Represents the config file path argument. This argument allows the /// Represents the config file path argument. This argument allows the
/// user to customize the config file path. /// user to customize the config file path.
pub fn arg() -> Arg { pub fn global_args() -> impl IntoIterator<Item = Arg> {
Arg::new(ARG_CONFIG) [Arg::new(ARG_CONFIG)
.help("Set a custom configuration file path") .help("Override the configuration file path")
.long_help(
"Override the configuration file path
If the file under the given path does not exist, the wizard will propose to create it.",
)
.long("config") .long("config")
.short('c') .short('c')
.global(true) .global(true)
.value_name("PATH") .value_name("path")]
} }
/// Represents the config file path argument parser. /// Represents the config file path argument parser.
pub fn parse_arg(matches: &ArgMatches) -> Option<&str> { pub fn parse_global_arg(matches: &ArgMatches) -> Option<&str> {
matches.get_one::<String>(ARG_CONFIG).map(String::as_str) matches.get_one::<String>(ARG_CONFIG).map(String::as_str)
} }

View file

@ -43,7 +43,8 @@ pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
/// Represents the envelope subcommand. /// Represents the envelope subcommand.
pub fn subcmd() -> Command { pub fn subcmd() -> Command {
Command::new(CMD_ENVELOPE) Command::new(CMD_ENVELOPE)
.about("Manage envelopes") .about("Subcommand to manage envelopes")
.long_about("Subcommand to manage envelopes like list")
.subcommands([Command::new(CMD_LIST) .subcommands([Command::new(CMD_LIST)
.alias("lst") .alias("lst")
.about("List envelopes") .about("List envelopes")

View file

@ -57,7 +57,8 @@ pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
/// Represents the flag subcommand. /// Represents the flag subcommand.
pub fn subcmd() -> Command { pub fn subcmd() -> Command {
Command::new(CMD_FLAG) Command::new(CMD_FLAG)
.about("Manage flags") .about("Subcommand to manage flags")
.long_about("Subcommand to manage flags like add, set or remove")
.subcommand_required(true) .subcommand_required(true)
.arg_required_else_help(true) .arg_required_else_help(true)
.subcommand( .subcommand(

View file

@ -120,7 +120,8 @@ pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
/// Represents the email subcommands. /// Represents the email subcommands.
pub fn subcmd() -> Command { pub fn subcmd() -> Command {
Command::new(CMD_MESSAGE) Command::new(CMD_MESSAGE)
.about("Manage messages") .about("Subcommand to manage messages")
.long_about("Subcommand to manage messages like read, write, reply or send")
.aliases(["msg"]) .aliases(["msg"])
.subcommand_required(true) .subcommand_required(true)
.arg_required_else_help(true) .arg_required_else_help(true)

View file

@ -73,7 +73,8 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
pub fn subcmd() -> Command { pub fn subcmd() -> Command {
Command::new(CMD_TPL) Command::new(CMD_TPL)
.alias("tpl") .alias("tpl")
.about("Manage templates") .about("Subcommand to manage templates")
.long_about("Subcommand to manage templates like write, reply, send or save")
.subcommand_required(true) .subcommand_required(true)
.arg_required_else_help(true) .arg_required_else_help(true)
.subcommand( .subcommand(

View file

@ -14,6 +14,7 @@ use crate::ui::table;
const ARG_ALL: &str = "all"; const ARG_ALL: &str = "all";
const ARG_EXCLUDE: &str = "exclude"; const ARG_EXCLUDE: &str = "exclude";
const ARG_INCLUDE: &str = "include"; const ARG_INCLUDE: &str = "include";
const ARG_GLOBAL_SOURCE: &str = "global-source";
const ARG_SOURCE: &str = "source"; const ARG_SOURCE: &str = "source";
const ARG_TARGET: &str = "target"; const ARG_TARGET: &str = "target";
const CMD_CREATE: &str = "create"; const CMD_CREATE: &str = "create";
@ -61,7 +62,8 @@ pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
/// Represents the folder subcommand. /// Represents the folder subcommand.
pub fn subcmd() -> Command { pub fn subcmd() -> Command {
Command::new(CMD_FOLDER) Command::new(CMD_FOLDER)
.about("Manage folders") .about("Subcommand to manage folders")
.long_about("Subcommand to manage folders like list, expunge or delete")
.subcommands([ .subcommands([
Command::new(CMD_EXPUNGE).about("Delete emails marked for deletion"), Command::new(CMD_EXPUNGE).about("Delete emails marked for deletion"),
Command::new(CMD_CREATE) Command::new(CMD_CREATE)
@ -77,16 +79,31 @@ pub fn subcmd() -> Command {
} }
/// Represents the source folder argument. /// Represents the source folder argument.
pub fn source_arg() -> Arg { pub fn global_args() -> impl IntoIterator<Item = Arg> {
Arg::new(ARG_SOURCE) [Arg::new(ARG_GLOBAL_SOURCE)
.help("Set the source folder") .help("Override the default INBOX folder")
.long_help(
"Override the default INBOX folder.
The given folder will be used by default for all other commands (when
applicable).",
)
.long("folder") .long("folder")
.short('f') .short('f')
.global(true) .global(true)
.value_name("SOURCE") .value_name("name")]
}
pub fn parse_global_source_arg(matches: &ArgMatches) -> Option<&str> {
matches
.get_one::<String>(ARG_GLOBAL_SOURCE)
.map(String::as_str)
}
pub fn source_arg(help: &'static str) -> Arg {
Arg::new(ARG_SOURCE).help(help).value_name("name")
} }
/// Represents the source folder argument parser.
pub fn parse_source_arg(matches: &ArgMatches) -> Option<&str> { pub fn parse_source_arg(matches: &ArgMatches) -> Option<&str> {
matches.get_one::<String>(ARG_SOURCE).map(String::as_str) matches.get_one::<String>(ARG_SOURCE).map(String::as_str)
} }

View file

@ -1,6 +1,7 @@
use ::email::account::{config::DEFAULT_INBOX_FOLDER, sync::AccountSyncBuilder}; use ::email::account::{config::DEFAULT_INBOX_FOLDER, sync::AccountSyncBuilder};
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use clap::Command; use clap::{Command, CommandFactory, Parser, Subcommand};
use env_logger::{Builder as LoggerBuilder, Env, DEFAULT_FILTER_ENV};
use log::{debug, warn}; use log::{debug, warn};
use std::env; use std::env;
use url::Url; use url::Url;
@ -15,20 +16,18 @@ use himalaya::{
template, template,
}; };
fn create_app() -> Command { fn _create_app() -> Command {
Command::new(env!("CARGO_PKG_NAME")) Command::new(env!("CARGO_PKG_NAME"))
.version(env!("CARGO_PKG_VERSION")) .version(env!("CARGO_PKG_VERSION"))
.about(env!("CARGO_PKG_DESCRIPTION")) .about(env!("CARGO_PKG_DESCRIPTION"))
.author(env!("CARGO_PKG_AUTHORS")) .author(env!("CARGO_PKG_AUTHORS"))
.propagate_version(true) .propagate_version(true)
.infer_subcommands(true) .infer_subcommands(true)
.arg(config::args::arg()) .args(config::args::global_args())
.arg(account::args::arg()) .args(account::args::global_args())
.arg(cache::args::arg()) .args(folder::args::global_args())
.args(output::args::args()) .args(cache::args::global_args())
.arg(folder::args::source_arg()) .args(output::args::global_args())
.subcommand(completion::args::subcmd())
.subcommand(man::args::subcmd())
.subcommand(account::args::subcmd()) .subcommand(account::args::subcmd())
.subcommand(folder::args::subcmd()) .subcommand(folder::args::subcmd())
.subcommand(envelope::args::subcmd()) .subcommand(envelope::args::subcmd())
@ -38,7 +37,7 @@ fn create_app() -> Command {
} }
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn _old_main() -> Result<()> {
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
if let Err((_, err)) = coredump::register_panic_handler() { if let Err((_, err)) = coredump::register_panic_handler() {
warn!("cannot register custom panic handler: {err}"); warn!("cannot register custom panic handler: {err}");
@ -63,32 +62,13 @@ async fn main() -> Result<()> {
return message::handlers::mailto(&account_config, &backend, &mut printer, &url).await; return message::handlers::mailto(&account_config, &backend, &mut printer, &url).await;
} }
let app = create_app(); let app = _create_app();
let m = app.get_matches(); let m = app.get_matches();
// check completionetion command before configs let some_config_path = config::args::parse_global_arg(&m);
// https://github.com/soywod/himalaya/issues/115 let some_account_name = account::args::parse_global_arg(&m);
#[allow(clippy::single_match)] let disable_cache = cache::args::parse_disable_cache_arg(&m);
match completion::args::matches(&m)? { let folder = folder::args::parse_global_source_arg(&m);
Some(completion::args::Cmd::Generate(shell)) => {
return completion::handlers::generate(create_app(), shell);
}
_ => (),
}
// check also man command before configs
#[allow(clippy::single_match)]
match man::args::matches(&m)? {
Some(man::args::Cmd::GenerateAll(dir)) => {
return man::handlers::generate(dir, create_app());
}
_ => (),
}
let some_config_path = config::args::parse_arg(&m);
let some_account_name = account::args::parse_arg(&m);
let disable_cache = cache::args::parse_disable_cache_flag(&m);
let folder = folder::args::parse_source_arg(&m);
let toml_config = TomlConfig::from_some_path_or_default(some_config_path).await?; let toml_config = TomlConfig::from_some_path_or_default(some_config_path).await?;
@ -362,3 +342,34 @@ async fn main() -> Result<()> {
Ok(()) Ok(())
} }
#[derive(Parser, Debug)]
#[command(name= "himalaya", author, version, about, long_about = None, propagate_version = true)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand, Debug)]
enum Commands {
Man(man::command::Generate),
#[command(aliases = ["completions", "compl", "comp"])]
Completion(completion::command::Generate),
}
#[tokio::main]
async fn main() -> Result<()> {
LoggerBuilder::new()
.parse_env(Env::new().filter_or(DEFAULT_FILTER_ENV, "warn"))
.format_timestamp(None)
.init();
let mut printer = StdoutPrinter::default();
match Cli::parse().command {
Commands::Man(cmd) => man::handler::generate(&mut printer, Cli::command(), cmd.dir),
Commands::Completion(cmd) => {
completion::handler::generate(&mut printer, Cli::command(), cmd.shell)
}
}
}

View file

@ -1,43 +0,0 @@
//! Module related to man CLI.
//!
//! This module provides subcommands and a command matcher related to
//! man.
use anyhow::Result;
use clap::{Arg, ArgMatches, Command};
use log::debug;
const ARG_DIR: &str = "dir";
const CMD_MAN: &str = "man";
/// Man commands.
pub enum Cmd<'a> {
/// Generates all man pages to the specified directory.
GenerateAll(&'a str),
}
/// Man command matcher.
pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
if let Some(m) = m.subcommand_matches(CMD_MAN) {
let dir = m.get_one::<String>(ARG_DIR).map(String::as_str).unwrap();
debug!("directory: {}", dir);
return Ok(Some(Cmd::GenerateAll(dir)));
};
Ok(None)
}
/// Man subcommands.
pub fn subcmd() -> Command {
Command::new(CMD_MAN)
.about("Generate all man pages to the given directory")
.arg(
Arg::new(ARG_DIR)
.help("Directory to generate man files in")
.long_help(
"Represents the directory where all man files of
all commands and subcommands should be generated in.",
)
.required(true),
)
}

22
src/man/command.rs Normal file
View file

@ -0,0 +1,22 @@
use anyhow::Result;
use clap::Parser;
use shellexpand_utils::{canonicalize, expand};
use std::path::PathBuf;
/// Generate all man pages to the given directory
#[derive(Debug, Parser)]
pub struct Generate {
/// Directory where man files should be generated in
#[arg(value_parser = dir_parser)]
pub dir: PathBuf,
}
/// Parse the given [`str`] as [`PathBuf`].
///
/// The path is first shell expanded, then canonicalized (if
/// applicable).
fn dir_parser(path: &str) -> Result<PathBuf, String> {
expand::try_path(path)
.map(canonicalize::path)
.map_err(|err| err.to_string())
}

35
src/man/handler.rs Normal file
View file

@ -0,0 +1,35 @@
use anyhow::Result;
use clap::Command;
use clap_mangen::Man;
use std::{fs, path::PathBuf};
use crate::printer::Printer;
pub fn generate(printer: &mut impl Printer, cmd: Command, dir: PathBuf) -> Result<()> {
let cmd_name = cmd.get_name().to_string();
let subcmds = cmd.get_subcommands().cloned().collect::<Vec<_>>();
let subcmds_len = subcmds.len() + 1;
let mut buffer = Vec::new();
Man::new(cmd).render(&mut buffer)?;
fs::create_dir_all(&dir)?;
printer.print_log(format!("Generating man page for command {cmd_name}"))?;
fs::write(dir.join(format!("{}.1", cmd_name)), buffer)?;
for subcmd in subcmds {
let subcmd_name = subcmd.get_name().to_string();
let mut buffer = Vec::new();
Man::new(subcmd).render(&mut buffer)?;
printer.print_log(format!("Generating man page for subcommand {subcmd_name}"))?;
fs::write(dir.join(format!("{}-{}.1", cmd_name, subcmd_name)), buffer)?;
}
printer.print(format!(
"Successfully generated {subcmds_len} man page(s) in {dir:?}!"
))?;
Ok(())
}

View file

@ -1,29 +0,0 @@
//! Module related to man handling.
//!
//! This module gathers all man commands.
use anyhow::Result;
use clap::Command;
use clap_mangen::Man;
use std::{fs, path::PathBuf};
/// Generates all man pages of all subcommands in the given directory.
pub fn generate(dir: &str, cmd: Command) -> Result<()> {
let mut buffer = Vec::new();
let cmd_name = cmd.get_name().to_string();
let subcmds = cmd.get_subcommands().cloned().collect::<Vec<_>>();
Man::new(cmd).render(&mut buffer)?;
fs::write(PathBuf::from(dir).join(format!("{}.1", cmd_name)), buffer)?;
for subcmd in subcmds {
let mut buffer = Vec::new();
let subcmd_name = subcmd.get_name().to_string();
Man::new(subcmd).render(&mut buffer)?;
fs::write(
PathBuf::from(dir).join(format!("{}-{}.1", cmd_name, subcmd_name)),
buffer,
)?;
}
Ok(())
}

View file

@ -1,2 +1,2 @@
pub mod args; pub mod command;
pub mod handlers; pub mod handler;

View file

@ -8,27 +8,28 @@ pub(crate) const ARG_COLOR: &str = "color";
pub(crate) const ARG_OUTPUT: &str = "output"; pub(crate) const ARG_OUTPUT: &str = "output";
/// Output arguments. /// Output arguments.
pub fn args() -> Vec<Arg> { pub fn global_args() -> impl IntoIterator<Item = Arg> {
vec![ [
Arg::new(ARG_OUTPUT) Arg::new(ARG_OUTPUT)
.help("Set the output format") .help("Define the output format")
.long("output") .long("output")
.short('o') .short('o')
.global(true) .global(true)
.value_name("FMT") .value_name("format")
.value_parser(["plain", "json"]) .value_parser(["plain", "json"])
.default_value("plain"), .default_value("plain"),
Arg::new(ARG_COLOR) Arg::new(ARG_COLOR)
.help("Control when to use colors.") .help("Control when to use colors")
.long_help( .long_help(
"This flag controls when to use colors. The default "Control when to use colors.
setting is 'auto', which means himalaya will try to guess when to use
colors. For example, if himalaya is printing to a terminal, then it The default setting is 'auto', which means himalaya will try to guess
will use colors, but if it is redirected to a file or a pipe, then it when to use colors. For example, if himalaya is printing to a
will suppress color output. himalaya will suppress color output in terminal, then it will use colors, but if it is redirected to a file
some other circumstances as well. For example, if the TERM environment or a pipe, then it will suppress color output. himalaya will suppress
variable is not set or set to 'dumb', then himalaya will not use color output in some other circumstances as well. For example, if the
colors. TERM environment variable is not set or set to 'dumb', then himalaya
will not use colors.
The possible values for this flag are: The possible values for this flag are:
@ -42,6 +43,6 @@ ansi Like 'always', but emits ANSI escapes (even in a Windows console).",
.global(true) .global(true)
.value_parser(["never", "auto", "always", "ansi"]) .value_parser(["never", "auto", "always", "ansi"])
.default_value("auto") .default_value("auto")
.value_name("WHEN"), .value_name("mode"),
] ]
} }