Skip to main content

nano_web/
cli.rs

1use crate::init_logging;
2
3use anyhow::Result;
4use clap::{CommandFactory, Parser, Subcommand};
5use std::path::PathBuf;
6
7const DEFAULT_PORT: u16 = 3000;
8
9#[derive(Parser)]
10#[command(name = "nano-web")]
11#[command(about = "Static file server built with Rust")]
12#[command(
13    long_about = "Static file server built with Rust\nRepository: https://github.com/radiosilence/nano-web"
14)]
15#[command(version)]
16pub struct Cli {
17    #[command(subcommand)]
18    pub command: Option<Commands>,
19
20    #[arg(long = "dir", default_value = "public")]
21    #[arg(help = "Directory to serve")]
22    pub dir: PathBuf,
23
24    #[arg(short = 'p', long = "port", default_value_t = DEFAULT_PORT)]
25    #[arg(help = "Port to listen on")]
26    pub port: u16,
27
28    #[arg(short = 'd', long = "dev")]
29    #[arg(help = "Check/reload files if modified")]
30    pub dev: bool,
31
32    #[arg(long = "spa")]
33    #[arg(help = "Enable SPA mode (serve index.html for all routes)")]
34    pub spa: bool,
35
36    #[arg(long = "config-prefix", default_value = "VITE_")]
37    #[arg(help = "Environment variable prefix for config injection")]
38    pub config_prefix: String,
39
40    #[arg(long = "log-level", default_value = "info")]
41    #[arg(help = "Log level (debug, info, warn, error)")]
42    pub log_level: String,
43
44    #[arg(long = "log-format", default_value = "console")]
45    #[arg(help = "Log format (json, console)")]
46    pub log_format: String,
47
48    #[arg(long = "log-requests")]
49    #[arg(help = "Log HTTP requests")]
50    pub log_requests: bool,
51}
52
53#[derive(Subcommand)]
54pub enum Commands {
55    #[command(about = "Start the web server")]
56    Serve {
57        #[arg(help = "Directory to serve")]
58        directory: Option<PathBuf>,
59
60        #[arg(short = 'p', long = "port")]
61        #[arg(help = "Port to listen on")]
62        port: Option<u16>,
63
64        #[arg(short = 'd', long = "dev")]
65        #[arg(help = "Check/reload files if modified")]
66        dev: bool,
67
68        #[arg(long = "spa")]
69        #[arg(help = "Enable SPA mode (serve index.html for all routes)")]
70        spa: bool,
71
72        #[arg(long = "config-prefix")]
73        #[arg(help = "Environment variable prefix for config injection")]
74        config_prefix: Option<String>,
75
76        #[arg(long = "log-level")]
77        #[arg(help = "Log level (debug, info, warn, error)")]
78        log_level: Option<String>,
79
80        #[arg(long = "log-format")]
81        #[arg(help = "Log format (json, console)")]
82        log_format: Option<String>,
83
84        #[arg(long = "log-requests")]
85        #[arg(help = "Log HTTP requests")]
86        log_requests: bool,
87    },
88    #[command(about = "Show version information")]
89    Version,
90    #[command(about = "Generate completion script")]
91    Completion {
92        #[arg(value_enum)]
93        shell: clap_complete::Shell,
94    },
95}
96
97impl Cli {
98    pub async fn run(self) -> Result<()> {
99        // Initialize logging with defaults for non-serve commands
100        if self.command.is_none() || !matches!(self.command, Some(Commands::Serve { .. })) {
101            init_logging(&self.log_level, &self.log_format);
102        }
103
104        match self.command {
105            Some(Commands::Serve {
106                ref directory,
107                port,
108                dev,
109                spa,
110                config_prefix,
111                log_level,
112                log_format,
113                log_requests,
114            }) => {
115                let public_dir = self.dir.clone();
116                let serve_dir = directory.clone().unwrap_or(public_dir);
117
118                // Use subcommand values or fall back to global defaults
119                let final_log_level = log_level.unwrap_or(self.log_level);
120                let final_log_format = log_format.unwrap_or(self.log_format);
121
122                // Initialize logging with final values
123                init_logging(&final_log_level, &final_log_format);
124
125                crate::server::start_server(crate::server::ServeConfig {
126                    public_dir: serve_dir,
127                    port: port.unwrap_or(self.port),
128                    dev: dev || self.dev,
129                    spa_mode: spa || self.spa,
130                    config_prefix: config_prefix.unwrap_or(self.config_prefix),
131                    log_requests: log_requests || self.log_requests,
132                })
133                .await
134            }
135            Some(Commands::Version) => {
136                println!("{}", full_version());
137                println!("Static file server built with Rust");
138                println!("Repository: https://github.com/radiosilence/nano-web");
139                Ok(())
140            }
141            Some(Commands::Completion { shell }) => {
142                generate_completion(shell);
143                Ok(())
144            }
145            None => {
146                // Show help when no subcommand is provided
147                let mut cmd = Self::command();
148                cmd.print_help()?;
149                Ok(())
150            }
151        }
152    }
153}
154
155fn full_version() -> String {
156    format!("nano-web v{}", env!("CARGO_PKG_VERSION"))
157}
158
159fn generate_completion(shell: clap_complete::Shell) {
160    use clap_complete::generate;
161    use std::io;
162
163    let mut cmd = Cli::command();
164    generate(shell, &mut cmd, "nano-web", &mut io::stdout());
165}