use axum::extract::State; use axum::response::IntoResponse; use axum::routing::get; use axum::{http::StatusCode, response::Html, serve, Router}; use minijinja::{context, Environment}; use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher}; use std::sync::Arc; use std::{path::PathBuf, str::FromStr, sync::OnceLock}; use tokio::sync::mpsc::channel; use tokio::sync::Mutex; use tower_http::services::ServeDir; use tower_livereload::{LiveReloadLayer, Reloader}; pub use tracing::{debug, error, info, warn}; #[derive(Debug)] struct Berr(Box); type Besult = std::result::Result; mod observe { pub fn setup_logging() { color_eyre::install().expect("Failed to install color_eyre"); let filter = tracing_subscriber::EnvFilter::builder() .with_default_directive(::from( tracing::level_filters::LevelFilter::TRACE, )) .parse_lossy("info,lyrs=trace"); tracing_subscriber::fmt().with_env_filter(filter).init(); } } fn static_file_dir() -> &'static PathBuf { static STATIC_FILE_DIR: OnceLock = OnceLock::new(); STATIC_FILE_DIR.get_or_init(|| PathBuf::from_str("static").unwrap()) } #[derive(Clone)] struct AppState { templates: Arc>>, } #[tokio::main] async fn main() -> Besult<()> { // load configuration? observe::setup_logging(); // TODO: only start tailwind if in dev mode? tokio::spawn(async move { info!("Starting tailwind..."); let tw = tokio::process::Command::new("tailwindcss") .args(["-i", "src/style.css", "-o", "static/style.css", "--watch"]) .spawn(); info!("Tailwind spawned. {tw:#?}"); }); let templates = Arc::new(Mutex::new(Environment::new())); while let Some(d) = tokio::fs::read_dir("templates").await?.next_entry().await? { templates.clone().lock().await.add_template_owned( d.file_name().into_string().unwrap(), std::fs::read_to_string(d.path())?, )?; } // pulling the watcher into main's scope lets it live until the program dies let (rl_layer, _watcher) = live_reload_layer()?; let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); info!("Listening on {listener:#?}"); let router = Router::new() .route("/", get(index)) .nest_service("/static", ServeDir::new(static_file_dir())) .layer(rl_layer) .with_state(AppState { templates }); Ok(serve(listener, router).await?) } async fn index(State(state): State) -> Besult> { Ok(Html( state .templates .lock() .await .get_template("page.html")? .render(context!())?, )) } fn live_reload_layer() -> Besult<(LiveReloadLayer, RecommendedWatcher)> { let rl_layer = LiveReloadLayer::new(); let rl = rl_layer.reloader(); let watcher = static_file_watcher(rl)?; Ok((rl_layer, watcher)) } fn static_file_watcher( reloader: Reloader, ) -> Result> { info!("Creating async watcher..."); let (tx, mut rx) = channel(1); info!("Creating watcher..."); // watcher needs to move out of scope to live long enough to watch let mut watcher = RecommendedWatcher::new( move |res| { info!("Res from watcher: {res:#?}"); futures::executor::block_on(async { tx.send(res).await.unwrap(); }) }, Config::default(), )?; info!("Created watcher"); watcher.watch(static_file_dir(), RecursiveMode::Recursive)?; tokio::spawn(async move { info!("Recieving..."); while let Some(res) = rx.recv().await { info!("Recieved! {res:#?}"); match res { Ok(event) => { info!("fs event: {event:#?}"); reloader.reload() } Err(e) => println!("watch error: {:?}", e), } } }); Ok(watcher) } impl IntoResponse for Berr { fn into_response(self) -> axum::http::Response { error!("webserver error: {:?}", self.0); ( StatusCode::INTERNAL_SERVER_ERROR, format!("internal server error: {}", self.0), ) .into_response() } } // This enables using `?` on functions that return `Result<_, anyhow::Error>` // to turn them into `Result<_, AppError>`. That way you don't need to do that // manually. impl From for Berr where E: Into>, { fn from(err: E) -> Self { Self(err.into()) } }