Skip to main content

gitv_tui/app/
cli.rs

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    /// Top-level CLI arguments controlling repository selection and runtime behavior.
15    #[clap(flatten)]
16    pub args: Args,
17}
18
19#[derive(clap::Args, Clone)]
20pub struct Args {
21    /// GitHub repository owner or organization (for example: `rust-lang`).
22    ///
23    /// This is required unless `--print-log-dir` or `--set-token` is provided.
24    #[clap(required_unless_present_any = [ "print_log_dir", "set_token", "generate_man" ])]
25    pub owner: Option<String>,
26    /// GitHub repository name under `owner` (for example: `rust`).
27    ///
28    /// This is required unless `--print-log-dir` or `--set-token` is provided.
29    #[clap(required_unless_present_any = [ "print_log_dir", "set_token", "generate_man" ])]
30    pub repo: Option<String>,
31    /// Global logging verbosity used by the application logger.
32    ///
33    /// Defaults to `info`.
34    #[clap(long, short, default_value_t = LogLevel::Info)]
35    pub log_level: LogLevel,
36    /// Prints the directory where log files are written and exits.
37    #[clap(long, short)]
38    pub print_log_dir: bool,
39    /// Stores/updates the GitHub token in the configured credential store.
40    ///
41    /// When provided, this command updates the saved token value.
42    #[clap(long, short)]
43    pub set_token: Option<String>,
44    /// Generate man pages using clap-mangen and exit.
45    #[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
80// Source - https://stackoverflow.com/a/76916424
81// Posted by Praveen Perera, modified by community. See post 'Timeline' for change history
82// Retrieved 2026-02-15, License - CC BY-SA 4.0
83
84pub 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 current_exe_path = PathBuf::from(clap::crate_name!()).display().to_string();
137    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    // Write them to the correct directories
168
169    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}