nano_web/
cli.rs

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