diff --git a/src/file_watcher.rs b/src/file_watcher.rs
new file mode 100644
index 0000000..6e16046
--- /dev/null
+++ b/src/file_watcher.rs
@@ -0,0 +1,42 @@
+use std::path::Path;
+
+use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher};
+use tokio::sync::mpsc::channel;
+use tracing::{error, info, instrument};
+
+use crate::Besult;
+
+pub type FileWatcher = RecommendedWatcher;
+
+pub mod prelude {
+ pub use super::{file_watcher, FileWatcher};
+}
+
+pub fn file_watcher
(dir: P, cb: F) -> Besult
+where
+ P: AsRef,
+ F: Fn(Event) -> () + std::marker::Send + 'static,
+{
+ let (tx, mut rx) = channel(1);
+ let mut watcher = RecommendedWatcher::new(
+ move |res| match res {
+ Ok(e) => futures::executor::block_on(async {
+ tx.send(e).await.unwrap();
+ }),
+ Err(e) => error!("Error from file_watcher: {e}"),
+ },
+ Config::default(),
+ )?;
+
+ info!("Watching directory '{}'", dir.as_ref().display());
+
+ tokio::spawn(async move {
+ while let Some(ev) = rx.recv().await {
+ cb(ev)
+ }
+ });
+
+ watcher.watch(dir.as_ref(), RecursiveMode::Recursive)?;
+
+ Ok(watcher)
+}
diff --git a/src/live_reload.rs b/src/live_reload.rs
new file mode 100644
index 0000000..573c55b
--- /dev/null
+++ b/src/live_reload.rs
@@ -0,0 +1,5 @@
+use tower_livereload::LiveReloadLayer;
+
+pub fn live_reload_layer() -> LiveReloadLayer {
+ LiveReloadLayer::new()
+}
diff --git a/src/main.rs b/src/main.rs
index 348c07e..6c3afa9 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -3,56 +3,51 @@ use axum::response::IntoResponse;
use axum::routing::get;
use axum::{http::StatusCode, response::Html, serve, Router};
use minijinja::context;
-use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
-use std::{path::PathBuf, str::FromStr, sync::OnceLock};
-use templates::Templates;
-use tokio::sync::mpsc::channel;
-use tower_http::services::ServeDir;
-use tower_livereload::{LiveReloadLayer, Reloader};
+use state::State as AppState;
pub use tracing::{debug, error, event, info, span, warn, Level};
+use crate::live_reload::live_reload_layer;
+
#[derive(Debug)]
struct Berr(Box);
type Besult = std::result::Result;
+mod file_watcher;
+mod live_reload;
mod observe;
+mod state;
+mod static_files;
mod tailwind;
mod templates;
-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: Templates,
-}
-
#[tokio::main]
async fn main() -> Besult<()> {
// load configuration?
let _setup_logging = observe::setup_logging();
- // TODO: reload templates when they change? separate watcher?
- let templates = Templates::try_load().await?;
- let mut tt = templates.clone();
- let templates_watcher = tt.start_watcher();
+ let state = AppState::try_new().await?;
// TODO: only start tailwind if in dev mode?
- tokio::spawn(async { tailwind::start_watcher() });
- tokio::spawn(async move { templates_watcher.await });
+ tokio::spawn(async move { tailwind::start_watcher() });
- // pulling the watcher into main's scope lets it live until the program dies
- let (rl_layer, _watcher) = live_reload_layer()?;
+ let lr = live_reload_layer();
+
+ {
+ // TODO: only start watcher for dev mode?
+ let state = state.clone();
+ let lr = lr.reloader();
+ tokio::spawn(async move { state.templates.start_watcher(Some(lr)) });
+ }
+
+ let (static_file_service, _static_file_watcher) = static_files::router(Some(lr.reloader()))?;
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
- info!("Listening on {listener:#?}");
+
+ 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 });
+ .nest_service("/static", static_file_service)
+ .with_state(state);
Ok(serve(listener, router).await?)
}
@@ -66,50 +61,6 @@ async fn index(State(state): State) -> Besult> {
))
}
-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 {
- while let Some(res) = rx.recv().await {
- 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);
diff --git a/src/state.rs b/src/state.rs
new file mode 100644
index 0000000..43b1a63
--- /dev/null
+++ b/src/state.rs
@@ -0,0 +1,16 @@
+use crate::templates::Templates;
+use crate::Besult;
+use std::sync::Arc;
+
+#[derive(Clone)]
+pub struct State {
+ pub templates: Arc,
+}
+
+impl State {
+ pub async fn try_new() -> Besult {
+ let templates = Arc::new(Templates::try_load("src/templates").await?);
+
+ Ok(Self { templates })
+ }
+}
diff --git a/src/static_files.rs b/src/static_files.rs
new file mode 100644
index 0000000..1abd792
--- /dev/null
+++ b/src/static_files.rs
@@ -0,0 +1,30 @@
+use crate::file_watcher::prelude::*;
+
+use crate::Besult;
+use axum::Router;
+use std::path::PathBuf;
+use std::str::FromStr;
+use std::sync::OnceLock;
+use tower_http::services::ServeDir;
+use tower_livereload::Reloader;
+use tracing::{info, instrument};
+
+fn static_file_dir() -> &'static PathBuf {
+ static STATIC_FILE_DIR: OnceLock = OnceLock::new();
+ STATIC_FILE_DIR.get_or_init(|| PathBuf::from_str("static").unwrap())
+}
+
+#[instrument]
+pub fn router(reloader: Option) -> Besult<(Router, Option)> {
+ let watcher = if let Some(rl) = reloader {
+ Some(file_watcher(static_file_dir(), move |ev| {
+ info!("Static File Watcher Event: {ev:#?}");
+ rl.reload()
+ })?)
+ } else {
+ None
+ };
+
+ let router = Router::new().nest_service("/", ServeDir::new(static_file_dir()));
+ Ok((router, watcher))
+}
diff --git a/src/templates.rs b/src/templates.rs
index b1b0c03..049303d 100644
--- a/src/templates.rs
+++ b/src/templates.rs
@@ -1,36 +1,72 @@
-use std::sync::Arc;
+use std::{path::PathBuf, sync::Arc};
use minijinja::Environment;
+use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
use pathdiff::diff_paths;
-use tokio::sync::Mutex;
-use tracing::info;
+use tokio::sync::{mpsc::channel, Mutex};
+use tower_livereload::Reloader;
+use tracing::{error, info};
+
+use crate::{file_watcher::file_watcher, Besult};
#[derive(Clone)]
pub struct Templates {
env: Arc>>,
+ dir: PathBuf,
}
pub type Error = Box;
impl Templates {
- pub fn empty() -> Self {
+ pub fn for_dir>(dir: P) -> Self {
let env = Arc::new(Mutex::new(Environment::new()));
- Self { env }
+ Self {
+ env,
+ dir: dir.into(),
+ }
}
- pub async fn try_load() -> Result {
- let mut result = Self::empty();
+ pub async fn try_load>(dir: P) -> Result {
+ let result = Self::for_dir(dir);
result.load_env().await?;
Ok(result)
}
- pub async fn start_watcher(&mut self) {
- info!("TODO: Implement template watcher");
+ pub async fn start_watcher(
+ self: Arc,
+ reloader: Option,
+ ) -> Besult