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"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
@ -493,6 +494,18 @@ dependencies = [
"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]]
name = "clap_lex"
version = "0.6.0"
@ -1063,12 +1076,12 @@ dependencies = [
[[package]]
name = "env_logger"
version = "0.8.4"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3"
checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece"
dependencies = [
"atty",
"humantime",
"is-terminal",
"log",
"regex",
"termcolor",
@ -2371,6 +2384,17 @@ version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "itoa"
version = "1.0.9"

View file

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

View file

@ -33,12 +33,20 @@ pub enum Cmd {
/// Represents the account command matcher.
pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
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");
let dry_run = parse_dry_run_arg(m);
let include = folder::args::parse_include_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([
folder.to_owned()
])))
@ -52,17 +60,8 @@ pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
None
};
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 {
info!("no account subcommand matched, falling back to subcommand list");
Some(Cmd::List(None))
None
}
} else {
None
@ -75,10 +74,26 @@ pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
pub fn subcmd() -> Command {
Command::new(CMD_ACCOUNT)
.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)
.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()),
)
.subcommand(
Command::new(CMD_SYNC)
.about("Synchronize the given account locally")
.arg(folder::args::all_arg("Synchronize all folders"))
@ -89,26 +104,27 @@ pub fn subcmd() -> Command {
"Synchronize all folders except the given ones",
))
.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
/// the user to select a different account than the default one.
pub fn arg() -> Arg {
Arg::new(ARG_ACCOUNT)
.help("Set the account")
pub fn global_args() -> impl IntoIterator<Item = Arg> {
[Arg::new(ARG_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")
.short('a')
.global(true)
.value_name("STRING")
.value_name("name")]
}
/// 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)
}

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
/// the user to disable any sort of cache.
pub fn arg() -> Arg {
Arg::new(ARG_DISABLE_CACHE)
pub fn global_args() -> impl IntoIterator<Item = Arg> {
[Arg::new(ARG_DISABLE_CACHE)
.help("Disable any sort of cache")
.long_help(
"Disable any sort of cache. The action depends on
the command it applies on.",
"Disable any sort of cache.
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")
.alias("no-cache")
.global(true)
.action(ArgAction::SetTrue)
.action(ArgAction::SetTrue)]
}
/// 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)
}

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.
//!
//! 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;
pub mod command;
pub mod handler;

View file

@ -6,16 +6,21 @@ const ARG_CONFIG: &str = "config";
/// Represents the config file path argument. This argument allows the
/// user to customize the config file path.
pub fn arg() -> Arg {
Arg::new(ARG_CONFIG)
.help("Set a custom configuration file path")
pub fn global_args() -> impl IntoIterator<Item = Arg> {
[Arg::new(ARG_CONFIG)
.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")
.short('c')
.global(true)
.value_name("PATH")
.value_name("path")]
}
/// 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)
}

View file

@ -43,7 +43,8 @@ pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
/// Represents the envelope subcommand.
pub fn subcmd() -> Command {
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)
.alias("lst")
.about("List envelopes")

View file

@ -57,7 +57,8 @@ pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
/// Represents the flag subcommand.
pub fn subcmd() -> Command {
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)
.arg_required_else_help(true)
.subcommand(

View file

@ -120,7 +120,8 @@ pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
/// Represents the email subcommands.
pub fn subcmd() -> Command {
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"])
.subcommand_required(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 {
Command::new(CMD_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)
.arg_required_else_help(true)
.subcommand(

View file

@ -14,6 +14,7 @@ use crate::ui::table;
const ARG_ALL: &str = "all";
const ARG_EXCLUDE: &str = "exclude";
const ARG_INCLUDE: &str = "include";
const ARG_GLOBAL_SOURCE: &str = "global-source";
const ARG_SOURCE: &str = "source";
const ARG_TARGET: &str = "target";
const CMD_CREATE: &str = "create";
@ -61,7 +62,8 @@ pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
/// Represents the folder subcommand.
pub fn subcmd() -> Command {
Command::new(CMD_FOLDER)
.about("Manage folders")
.about("Subcommand to manage folders")
.long_about("Subcommand to manage folders like list, expunge or delete")
.subcommands([
Command::new(CMD_EXPUNGE).about("Delete emails marked for deletion"),
Command::new(CMD_CREATE)
@ -77,16 +79,31 @@ pub fn subcmd() -> Command {
}
/// Represents the source folder argument.
pub fn source_arg() -> Arg {
Arg::new(ARG_SOURCE)
.help("Set the source folder")
pub fn global_args() -> impl IntoIterator<Item = Arg> {
[Arg::new(ARG_GLOBAL_SOURCE)
.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")
.short('f')
.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> {
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 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 std::env;
use url::Url;
@ -15,20 +16,18 @@ use himalaya::{
template,
};
fn create_app() -> Command {
fn _create_app() -> Command {
Command::new(env!("CARGO_PKG_NAME"))
.version(env!("CARGO_PKG_VERSION"))
.about(env!("CARGO_PKG_DESCRIPTION"))
.author(env!("CARGO_PKG_AUTHORS"))
.propagate_version(true)
.infer_subcommands(true)
.arg(config::args::arg())
.arg(account::args::arg())
.arg(cache::args::arg())
.args(output::args::args())
.arg(folder::args::source_arg())
.subcommand(completion::args::subcmd())
.subcommand(man::args::subcmd())
.args(config::args::global_args())
.args(account::args::global_args())
.args(folder::args::global_args())
.args(cache::args::global_args())
.args(output::args::global_args())
.subcommand(account::args::subcmd())
.subcommand(folder::args::subcmd())
.subcommand(envelope::args::subcmd())
@ -38,7 +37,7 @@ fn create_app() -> Command {
}
#[tokio::main]
async fn main() -> Result<()> {
async fn _old_main() -> Result<()> {
#[cfg(not(target_os = "windows"))]
if let Err((_, err)) = coredump::register_panic_handler() {
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;
}
let app = create_app();
let app = _create_app();
let m = app.get_matches();
// check completionetion command before configs
// https://github.com/soywod/himalaya/issues/115
#[allow(clippy::single_match)]
match completion::args::matches(&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 some_config_path = config::args::parse_global_arg(&m);
let some_account_name = account::args::parse_global_arg(&m);
let disable_cache = cache::args::parse_disable_cache_arg(&m);
let folder = folder::args::parse_global_source_arg(&m);
let toml_config = TomlConfig::from_some_path_or_default(some_config_path).await?;
@ -362,3 +342,34 @@ async fn main() -> Result<()> {
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 handlers;
pub mod command;
pub mod handler;

View file

@ -8,27 +8,28 @@ pub(crate) const ARG_COLOR: &str = "color";
pub(crate) const ARG_OUTPUT: &str = "output";
/// Output arguments.
pub fn args() -> Vec<Arg> {
vec![
pub fn global_args() -> impl IntoIterator<Item = Arg> {
[
Arg::new(ARG_OUTPUT)
.help("Set the output format")
.help("Define the output format")
.long("output")
.short('o')
.global(true)
.value_name("FMT")
.value_name("format")
.value_parser(["plain", "json"])
.default_value("plain"),
Arg::new(ARG_COLOR)
.help("Control when to use colors.")
.help("Control when to use colors")
.long_help(
"This flag controls when to use colors. The default
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
will use colors, but if it is redirected to a file or a pipe, then it
will suppress color output. himalaya will suppress color output in
some other circumstances as well. For example, if the TERM environment
variable is not set or set to 'dumb', then himalaya will not use
colors.
"Control when to use colors.
The default 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 will use colors, but if it is redirected to a file
or a pipe, then it will suppress color output. himalaya will suppress
color output in some other circumstances as well. For example, if the
TERM environment variable is not set or set to 'dumb', then himalaya
will not use colors.
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)
.value_parser(["never", "auto", "always", "ansi"])
.default_value("auto")
.value_name("WHEN"),
.value_name("mode"),
]
}