1use crate::init_logging;
2use anyhow::Result;
3use clap::{CommandFactory, Parser, Subcommand};
4use std::path::PathBuf;
5
6const DEFAULT_PORT: u16 = 3000;
7const VERSION: &str = include_str!("../VERSION");
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 = 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 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 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 init_logging(&final_log_level, &final_log_format);
124
125 let final_config = FinalServeConfig {
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
134 final_config.serve().await
135 }
136 Some(Commands::Version) => {
137 println!("{}", full_version());
138 println!("Static file server built with Rust");
139 println!("Repository: https://github.com/radiosilence/nano-web");
140 Ok(())
141 }
142 Some(Commands::Completion { shell }) => {
143 generate_completion(shell);
144 Ok(())
145 }
146 None => {
147 let mut cmd = Self::command();
149 cmd.print_help()?;
150 Ok(())
151 }
152 }
153 }
154}
155
156struct FinalServeConfig {
157 public_dir: PathBuf,
158 port: u16,
159 dev: bool,
160 spa_mode: bool,
161 config_prefix: String,
162 log_requests: bool,
163}
164
165impl FinalServeConfig {
166 async fn serve(self) -> Result<()> {
167 let config = crate::server::AxumServeConfig {
169 public_dir: self.public_dir,
170 port: self.port,
171 dev: self.dev,
172 spa_mode: self.spa_mode,
173 config_prefix: self.config_prefix,
174 log_requests: self.log_requests,
175 };
176 crate::server::start_axum_server(config).await
177 }
178}
179
180fn full_version() -> String {
181 format!("nano-web v{}", VERSION.trim())
182}
183
184fn generate_completion(shell: clap_complete::Shell) {
185 use clap_complete::generate;
186 use std::io;
187
188 let mut cmd = Cli::command();
189 generate(shell, &mut cmd, "nano-web", &mut io::stdout());
190}