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
49#[derive(clap::ValueEnum, Clone, Debug)]
50pub enum LogLevel {
51 Trace,
52 Debug,
53 Info,
54 Warn,
55 Error,
56 None,
57}
58
59impl Display for LogLevel {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 let s = match self {
62 LogLevel::Trace => "trace",
63 LogLevel::Debug => "debug",
64 LogLevel::Info => "info",
65 LogLevel::Warn => "warn",
66 LogLevel::Error => "error",
67 LogLevel::None => "none",
68 };
69 write!(f, "{s}")
70 }
71}
72
73impl TryFrom<LogLevel> for Directive {
74 type Error = filter::ParseError;
75 fn try_from(value: LogLevel) -> Result<Self, Self::Error> {
76 Directive::from_str(&value.to_string())
77 }
78}
79
80pub fn get_styles() -> clap::builder::Styles {
85 use clap::builder::styling::{AnsiColor, Color, Style};
86 clap::builder::Styles::styled()
87 .usage(
88 Style::new()
89 .bold()
90 .fg_color(Some(Color::Ansi(AnsiColor::Cyan))),
91 )
92 .header(
93 Style::new()
94 .bold()
95 .fg_color(Some(Color::Ansi(AnsiColor::Green))),
96 )
97 .literal(
98 Style::new()
99 .bold()
100 .fg_color(Some(Color::Ansi(AnsiColor::Cyan))),
101 )
102 .invalid(
103 Style::new()
104 .bold()
105 .fg_color(Some(Color::Ansi(AnsiColor::Red))),
106 )
107 .error(
108 Style::new()
109 .bold()
110 .fg_color(Some(Color::Ansi(AnsiColor::Red))),
111 )
112 .valid(
113 Style::new()
114 .bold()
115 .fg_color(Some(Color::Ansi(AnsiColor::Cyan))),
116 )
117 .placeholder(
118 Style::new()
119 .bold()
120 .fg_color(Some(Color::Ansi(AnsiColor::BrightBlue))),
121 )
122}
123
124pub const VERSION_MESSAGE: &str = concat!(
125 env!("CARGO_PKG_VERSION"),
126 "-",
127 env!("VERGEN_GIT_DESCRIBE"),
128 " (",
129 env!("VERGEN_BUILD_DATE"),
130 ")"
131);
132
133pub fn version() -> String {
134 let author = clap::crate_authors!();
135
136 let data_dir_path = get_data_dir().display().to_string();
138
139 format!(
140 "\
141{VERSION_MESSAGE}
142
143Author: {author}
144
145Data directory: {data_dir_path}"
146 )
147}
148
149pub fn generate_man_pages() -> Result<PathBuf, AppError> {
150 if cfg!(windows) {
151 return Err(AppError::Other(anyhow!(
152 "man page generation is not supported on Windows"
153 )));
154 }
155
156 let cmd = Cli::command();
157
158 let prefix = env::var("PREFIX").unwrap_or("/usr/local".to_string());
159
160 let man1_dir = PathBuf::from(&prefix).join("share/man/man1");
161
162 fs::create_dir_all(&man1_dir)?;
163
164 let man1_file = format!("{}.1", &*PROJECT_NAME).to_lowercase();
165 let mut man1_fd = fs::File::create(man1_dir.join(&man1_file))?;
166
167 clap_mangen::Man::new(cmd).render(&mut man1_fd)?;
170 println!("Installed manpages:");
171 println!(" {}/share/man/man1/{}", prefix, man1_file);
172
173 Ok(man1_dir.join(man1_file))
174}