2024-05-07 21:13:46 -05:00
|
|
|
use axum::extract::State;
|
|
|
|
use axum::response::IntoResponse;
|
|
|
|
use axum::routing::get;
|
|
|
|
use axum::{http::StatusCode, response::Html, serve, Router};
|
|
|
|
use minijinja::{context, Environment};
|
2024-05-06 15:39:21 -05:00
|
|
|
use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
|
2024-05-07 21:13:46 -05:00
|
|
|
use std::sync::Arc;
|
2024-05-07 19:49:04 -05:00
|
|
|
use std::{path::PathBuf, str::FromStr, sync::OnceLock};
|
2024-05-05 11:22:54 -05:00
|
|
|
use tokio::sync::mpsc::channel;
|
2024-05-07 21:13:46 -05:00
|
|
|
use tokio::sync::Mutex;
|
2024-05-06 15:39:21 -05:00
|
|
|
use tower_http::services::ServeDir;
|
|
|
|
use tower_livereload::{LiveReloadLayer, Reloader};
|
|
|
|
pub use tracing::{debug, error, info, warn};
|
2024-05-05 11:22:54 -05:00
|
|
|
|
2024-05-07 21:13:46 -05:00
|
|
|
#[derive(Debug)]
|
|
|
|
struct Berr(Box<dyn std::error::Error>);
|
|
|
|
type Besult<T> = std::result::Result<T, Berr>;
|
2024-05-06 13:05:04 -05:00
|
|
|
|
|
|
|
mod observe {
|
|
|
|
pub fn setup_logging() {
|
|
|
|
color_eyre::install().expect("Failed to install color_eyre");
|
|
|
|
|
2024-05-06 15:39:21 -05:00
|
|
|
let filter = tracing_subscriber::EnvFilter::builder()
|
|
|
|
.with_default_directive(<tracing_subscriber::filter::Directive>::from(
|
|
|
|
tracing::level_filters::LevelFilter::TRACE,
|
|
|
|
))
|
|
|
|
.parse_lossy("info,lyrs=trace");
|
2024-05-06 13:05:04 -05:00
|
|
|
|
|
|
|
tracing_subscriber::fmt().with_env_filter(filter).init();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-06 15:39:21 -05:00
|
|
|
fn static_file_dir() -> &'static PathBuf {
|
|
|
|
static STATIC_FILE_DIR: OnceLock<PathBuf> = OnceLock::new();
|
|
|
|
STATIC_FILE_DIR.get_or_init(|| PathBuf::from_str("static").unwrap())
|
2024-05-06 13:05:04 -05:00
|
|
|
}
|
2024-05-05 11:22:54 -05:00
|
|
|
|
2024-05-07 21:13:46 -05:00
|
|
|
#[derive(Clone)]
|
|
|
|
struct AppState {
|
|
|
|
templates: Arc<Mutex<Environment<'static>>>,
|
|
|
|
}
|
|
|
|
|
2024-05-06 15:39:21 -05:00
|
|
|
#[tokio::main]
|
|
|
|
async fn main() -> Besult<()> {
|
|
|
|
// load configuration?
|
|
|
|
observe::setup_logging();
|
2024-05-05 11:22:54 -05:00
|
|
|
|
2024-05-07 21:13:46 -05:00
|
|
|
// 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 mut 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(
|
|
|
|
d.path().to_string_lossy().as_ref(),
|
|
|
|
&std::fs::read_to_string(d.path())?,
|
|
|
|
)?;
|
|
|
|
}
|
|
|
|
|
2024-05-07 19:49:04 -05:00
|
|
|
// pulling the watcher into main's scope lets it live until the program dies
|
|
|
|
let (rl_layer, _watcher) = live_reload_layer()?;
|
2024-05-05 11:22:54 -05:00
|
|
|
|
2024-05-06 15:39:21 -05:00
|
|
|
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
|
|
|
info!("Listening on {listener:#?}");
|
2024-05-07 19:49:04 -05:00
|
|
|
let router = Router::new()
|
2024-05-07 21:13:46 -05:00
|
|
|
.route("/", get(index))
|
2024-05-07 19:49:04 -05:00
|
|
|
.nest_service("/static", ServeDir::new(static_file_dir()))
|
2024-05-07 21:13:46 -05:00
|
|
|
.layer(rl_layer)
|
|
|
|
.with_state(AppState { templates });
|
2024-05-07 19:49:04 -05:00
|
|
|
|
|
|
|
Ok(serve(listener, router).await?)
|
2024-05-05 11:22:54 -05:00
|
|
|
}
|
|
|
|
|
2024-05-07 21:13:46 -05:00
|
|
|
async fn index(State(state): State<AppState>) -> Besult<Html<String>> {
|
|
|
|
Ok(Html(
|
|
|
|
state
|
|
|
|
.templates
|
|
|
|
.lock()
|
|
|
|
.await
|
|
|
|
.get_template("page.html")?
|
|
|
|
.render(context!())?,
|
|
|
|
))
|
|
|
|
}
|
|
|
|
|
2024-05-07 19:49:04 -05:00
|
|
|
fn live_reload_layer() -> Besult<(LiveReloadLayer, RecommendedWatcher)> {
|
2024-05-06 15:39:21 -05:00
|
|
|
let rl_layer = LiveReloadLayer::new();
|
|
|
|
let rl = rl_layer.reloader();
|
2024-05-07 19:49:04 -05:00
|
|
|
let watcher = static_file_watcher(rl)?;
|
|
|
|
Ok((rl_layer, watcher))
|
2024-05-06 15:39:21 -05:00
|
|
|
}
|
2024-05-05 11:22:54 -05:00
|
|
|
|
2024-05-07 19:49:04 -05:00
|
|
|
fn static_file_watcher(
|
|
|
|
reloader: Reloader,
|
|
|
|
) -> Result<RecommendedWatcher, Box<dyn std::error::Error>> {
|
2024-05-05 11:22:54 -05:00
|
|
|
info!("Creating async watcher...");
|
|
|
|
|
|
|
|
let (tx, mut rx) = channel(1);
|
|
|
|
|
|
|
|
info!("Creating watcher...");
|
2024-05-07 19:49:04 -05:00
|
|
|
// watcher needs to move out of scope to live long enough to watch
|
2024-05-05 11:22:54 -05:00
|
|
|
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:#?}");
|
2024-05-06 15:39:21 -05:00
|
|
|
reloader.reload()
|
2024-05-05 11:22:54 -05:00
|
|
|
}
|
|
|
|
Err(e) => println!("watch error: {:?}", e),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2024-05-07 19:49:04 -05:00
|
|
|
Ok(watcher)
|
2024-05-05 09:47:13 -05:00
|
|
|
}
|
2024-05-07 21:13:46 -05:00
|
|
|
|
|
|
|
impl IntoResponse for Berr {
|
|
|
|
fn into_response(self) -> axum::http::Response<axum::body::Body> {
|
|
|
|
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<E> From<E> for Berr
|
|
|
|
where
|
|
|
|
E: Into<Box<dyn std::error::Error>>,
|
|
|
|
{
|
|
|
|
fn from(err: E) -> Self {
|
|
|
|
Self(err.into())
|
|
|
|
}
|
|
|
|
}
|