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    /// When provided, this command will read the GitHub token from the environment variable
49    #[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
84// Source - https://stackoverflow.com/a/76916424
85// Posted by Praveen Perera, modified by community. See post 'Timeline' for change history
86// Retrieved 2026-02-15, License - CC BY-SA 4.0
87
88pub 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 current_exe_path = PathBuf::from(clap::crate_name!()).display().to_string();
141    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    // Write them to the correct directories
172
173    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}