use std::process::Command;
use include_dir::{Dir, include_dir};
use tiny_http::{Header, Response, Server};
use crate::error::Error;
static SITE: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/docs/book/book");
pub fn run(port: u16, open: bool) -> Result<(), Error> {
if SITE.get_file("index.html").is_none() {
return Err(Error::DocsNotBuilt);
}
let bind = format!("127.0.0.1:{port}");
let server = Server::http(&bind).map_err(|e| Error::DocsBindFailed {
bind: bind.clone(),
reason: e.to_string(),
})?;
let addr = server.server_addr();
let url = format!("http://{addr}/");
println!("Serving docs at {url}");
println!("Press Ctrl-C to stop.");
if open && let Err(e) = open_url(&url) {
log::warn!("failed to open browser: {e}");
}
for req in server.incoming_requests() {
if let Err(e) = handle_request(req) {
log::warn!("request error: {e}");
}
}
Ok(())
}
fn handle_request(req: tiny_http::Request) -> std::io::Result<()> {
let url = req.url().to_string();
let raw_path = url
.split('?')
.next()
.unwrap_or(&url)
.trim_start_matches('/');
let candidate = if raw_path.is_empty() || raw_path.ends_with('/') {
format!("{raw_path}index.html")
} else {
raw_path.to_string()
};
let file = SITE
.get_file(&candidate)
.or_else(|| SITE.get_file(format!("{raw_path}/index.html")));
match file {
Some(f) => {
let mime = mime_for(&candidate);
let response = Response::from_data(f.contents()).with_header(
Header::from_bytes(&b"Content-Type"[..], mime.as_bytes())
.expect("static mime header is well-formed"),
);
req.respond(response)
}
None => match SITE.get_file("404.html") {
Some(page) => req.respond(
Response::from_data(page.contents())
.with_status_code(404)
.with_header(
Header::from_bytes(&b"Content-Type"[..], &b"text/html; charset=utf-8"[..])
.expect("static mime header is well-formed"),
),
),
None => {
let body = format!("404 Not Found: /{raw_path}");
req.respond(Response::from_string(body).with_status_code(404))
}
},
}
}
fn mime_for(path: &str) -> &'static str {
match path.rsplit_once('.').map(|(_, ext)| ext) {
Some("html") => "text/html; charset=utf-8",
Some("js") => "application/javascript; charset=utf-8",
Some("css") => "text/css; charset=utf-8",
Some("json") => "application/json; charset=utf-8",
Some("svg") => "image/svg+xml",
Some("png") => "image/png",
Some("woff") => "font/woff",
Some("woff2") => "font/woff2",
Some("ttf") => "font/ttf",
Some("ico") => "image/x-icon",
Some("txt") => "text/plain; charset=utf-8",
_ => "application/octet-stream",
}
}
fn open_url(url: &str) -> std::io::Result<()> {
let cmd = if cfg!(target_os = "macos") {
"open"
} else if cfg!(target_os = "linux") {
"xdg-open"
} else {
return Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"no known URL opener for this OS",
));
};
Command::new(cmd).arg(url).spawn().map(|_| ())
}