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 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 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 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}