1mod api;
2mod assets;
3
4use crate::error::{ChronicleError, Result};
5use crate::git::CliOps;
6
7pub fn serve(git_ops: CliOps, port: Option<u16>, open_browser: bool) -> Result<()> {
8 let bind_port = port.unwrap_or(0);
9 let listener = std::net::TcpListener::bind(("127.0.0.1", bind_port)).map_err(|e| {
10 ChronicleError::Config {
11 message: format!("failed to bind to port {bind_port}: {e}"),
12 location: snafu::Location::default(),
13 }
14 })?;
15 let actual_port = listener
16 .local_addr()
17 .map_err(|e| ChronicleError::Config {
18 message: format!("failed to get local address: {e}"),
19 location: snafu::Location::default(),
20 })?
21 .port();
22
23 let server =
24 tiny_http::Server::from_listener(listener, None).map_err(|e| ChronicleError::Config {
25 message: format!("failed to start web server: {e}"),
26 location: snafu::Location::default(),
27 })?;
28
29 let url = format!("http://localhost:{actual_port}");
30 eprintln!("\n Chronicle web viewer running at {url}\n Press Ctrl+C to stop\n");
31 if open_browser {
32 open::that(&url).ok();
33 }
34
35 for request in server.incoming_requests() {
36 let req_url = request.url().to_string();
37 let result = if req_url.starts_with("/api/") {
38 api::handle(&git_ops, &req_url)
39 } else {
40 Ok(assets::handle(&req_url))
41 };
42
43 match result {
44 Ok(response) => {
45 request.respond(response).ok();
46 }
47 Err(e) => {
48 eprintln!("[chronicle-web] ERROR {req_url}: {e}");
49 let body = serde_json::json!({ "error": e.to_string() }).to_string();
50 let response = tiny_http::Response::from_string(body)
51 .with_header(
52 tiny_http::Header::from_bytes(
53 &b"Content-Type"[..],
54 &b"application/json"[..],
55 )
56 .unwrap(),
57 )
58 .with_status_code(500);
59 request.respond(response).ok();
60 }
61 }
62 }
63
64 Ok(())
65}