Parent [>]
1//! Binary entry point. Parses CLI arguments, initialises global configuration,
2//! builds the CSS theme, constructs the axum router, and starts the HTTP server.
3
4mod cli;
5
6use clap::Parser;
7use teak::config;
8use teak::theme::{BASE_CSS, MARKDOWN_CSS};
9use teak::{error, routes};
10use tower_http::trace::TraceLayer;
11use tracing_subscriber::EnvFilter;
12
13async fn shutdown_signal() {
14    let ctrl_c = tokio::signal::ctrl_c();
15    #[cfg(unix)]
16    let sigterm = async {
17        tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
18            .expect("install SIGTERM handler")
19            .recv()
20            .await;
21    };
22    #[cfg(not(unix))]
23    let sigterm = std::future::pending::<()>();
24
25    tokio::select! {
26        _ = ctrl_c => {},
27        _ = sigterm => {},
28    }
29    tracing::info!("shutting down");
30}
31
32#[tokio::main]
33async fn main() -> error::Result<()> {
34    tracing_subscriber::fmt()
35        .with_env_filter(
36            EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")),
37        )
38        .init();
39
40    let args = cli::Args::parse();
41
42    config::set_hostname(args.hostname);
43    config::set_ssh_prefix(args.ssh_prefix);
44    config::set_base_url(args.base_url);
45
46    let (favicon_data, favicon_mime) = match args.favicon {
47        Some(path) => match std::fs::read(&path) {
48            Ok(data) => {
49                let mime = match path.extension().and_then(|e| e.to_str()) {
50                    Some("ico") => "image/x-icon",
51                    Some("png") => "image/png",
52                    Some("gif") => "image/gif",
53                    _ => "image/svg+xml",
54                };
55                (data, mime)
56            }
57            Err(e) => {
58                tracing::warn!("failed to read favicon {path:?}: {e}; using default");
59                (include_bytes!("../static/favicon.svg").to_vec(), "image/svg+xml")
60            }
61        },
62        None => (include_bytes!("../static/favicon.svg").to_vec(), "image/svg+xml"),
63    };
64    config::set_favicon(favicon_data, favicon_mime);
65
66    let theme_vars = match args.theme_file {
67        Some(path) => std::fs::read_to_string(path)?,
68        None => args.theme.css().to_owned(),
69    };
70    let css = format!("{theme_vars}\n{BASE_CSS}\n{MARKDOWN_CSS}");
71
72    let state = routes::AppState {
73        root: args.root,
74        css,
75    };
76    let router = routes::router(state).layer(TraceLayer::new_for_http());
77    let addr = format!("{}:{}", args.bind, args.port);
78
79    tracing::info!("listening on {addr}");
80
81    let socket = tokio::net::TcpListener::bind(&addr).await?;
82    axum::serve(socket, router)
83        .with_graceful_shutdown(shutdown_signal())
84        .await?;
85
86    Ok(())
87}