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