1use std::{env, fs};
2use std::{fmt::Display, path::PathBuf, str::FromStr};
3
4use anyhow::anyhow;
5use clap::{CommandFactory, Parser};
6use tracing_subscriber::filter::{self, Directive};
7
8use crate::errors::AppError;
9use crate::logging::{PROJECT_NAME, get_data_dir};
10
11#[derive(Parser)]
12#[clap(author, version = version(), about, long_about = None, styles = get_styles())]
13pub struct Cli {
14 #[clap(flatten)]
16 pub args: Args,
17}
18
19#[derive(clap::Args, Clone)]
20pub struct Args {
21 #[clap(required_unless_present_any = [ "print_log_dir", "set_token", "generate_man" ])]
25 pub owner: Option<String>,
26 #[clap(required_unless_present_any = [ "print_log_dir", "set_token", "generate_man" ])]
30 pub repo: Option<String>,
31 #[clap(long, short, default_value_t = LogLevel::Info)]
35 pub log_level: LogLevel,
36 #[clap(long, short)]
38 pub print_log_dir: bool,
39 #[clap(long, short)]
43 pub set_token: Option<String>,
44 #[clap(long)]
46 pub generate_man: bool,
47
48 #[clap(short, long)]
50 pub env: bool,
51}
52
53#[derive(clap::ValueEnum, Clone, Debug)]
54pub enum LogLevel {
55 Trace,
56 Debug,
57 Info,
58 Warn,
59 Error,
60 None,
61}
62
63impl Display for LogLevel {
64 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65 let s = match self {
66 LogLevel::Trace => "trace",
67 LogLevel::Debug => "debug",
68 LogLevel::Info => "info",
69 LogLevel::Warn => "warn",
70 LogLevel::Error => "error",
71 LogLevel::None => "none",
72 };
73 write!(f, "{s}")
74 }
75}
76
77impl TryFrom<LogLevel> for Directive {
78 type Error = filter::ParseError;
79 fn try_from(value: LogLevel) -> Result<Self, Self::Error> {
80 Directive::from_str(&value.to_string())
81 }
82}
83
84pub fn get_styles() -> clap::builder::Styles {
89 use clap::builder::styling::{AnsiColor, Color, Style};
90 clap::builder::Styles::styled()
91 .usage(
92 Style::new()
93 .bold()
94 .fg_color(Some(Color::Ansi(AnsiColor::Cyan))),
95 )
96 .header(
97 Style::new()
98 .bold()
99 .fg_color(Some(Color::Ansi(AnsiColor::Green))),
100 )
101 .literal(
102 Style::new()
103 .bold()
104 .fg_color(Some(Color::Ansi(AnsiColor::Cyan))),
105 )
106 .invalid(
107 Style::new()
108 .bold()
109 .fg_color(Some(Color::Ansi(AnsiColor::Red))),
110 )
111 .error(
112 Style::new()
113 .bold()
114 .fg_color(Some(Color::Ansi(AnsiColor::Red))),
115 )
116 .valid(
117 Style::new()
118 .bold()
119 .fg_color(Some(Color::Ansi(AnsiColor::Cyan))),
120 )
121 .placeholder(
122 Style::new()
123 .bold()
124 .fg_color(Some(Color::Ansi(AnsiColor::BrightBlue))),
125 )
126}
127
128pub const VERSION_MESSAGE: &str = concat!(
129 env!("CARGO_PKG_VERSION"),
130 "-",
131 env!("VERGEN_GIT_DESCRIBE"),
132 " (",
133 env!("VERGEN_BUILD_DATE"),
134 ")"
135);
136
137pub fn version() -> String {
138 let author = clap::crate_authors!();
139
140 let data_dir_path = get_data_dir().display().to_string();
142
143 format!(
144 "\
145{VERSION_MESSAGE}
146
147Author: {author}
148
149Data directory: {data_dir_path}"
150 )
151}
152
153pub fn generate_man_pages() -> Result<PathBuf, AppError> {
154 if cfg!(windows) {
155 return Err(AppError::Other(anyhow!(
156 "man page generation is not supported on Windows"
157 )));
158 }
159
160 let cmd = Cli::command();
161
162 let prefix = env::var("PREFIX").unwrap_or("/usr/local".to_string());
163
164 let man1_dir = PathBuf::from(&prefix).join("share/man/man1");
165
166 fs::create_dir_all(&man1_dir)?;
167
168 let man1_file = format!("{}.1", &*PROJECT_NAME).to_lowercase();
169 let mut man1_fd = fs::File::create(man1_dir.join(&man1_file))?;
170
171 clap_mangen::Man::new(cmd).render(&mut man1_fd)?;
174 println!("Installed manpages:");
175 println!(" {}/share/man/man1/{}", prefix, man1_file);
176
177 Ok(man1_dir.join(man1_file))
178}