Database time
This commit is contained in:
parent
27dd80830f
commit
1a1aee1195
19 changed files with 2006 additions and 406 deletions
1742
Cargo.lock
generated
1742
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -11,8 +11,8 @@ opt-level = 3
|
|||
|
||||
[profile.release]
|
||||
strip = true
|
||||
opt-level = "z"
|
||||
lto = true
|
||||
opt-level = "s"
|
||||
lto = "fat"
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
|
||||
|
@ -24,9 +24,11 @@ clap = { version = "4.5.4", features = ["derive", "env"] }
|
|||
color-eyre = "0.6.3"
|
||||
config = "0.14.0"
|
||||
futures = "0.3.30"
|
||||
minijinja = { version = "2.0.1", features = ["loader"] }
|
||||
maud = "0.26.0"
|
||||
notify = "6.1.1"
|
||||
pathdiff = "0.2.1"
|
||||
redact = { version = "0.1.10", features = ["serde"] }
|
||||
sea-orm = { version = "0.12.15", features = ["sqlx-postgres", "runtime-tokio-rustls", "debug-print"] }
|
||||
serde = "1.0.201"
|
||||
thiserror = "1.0.60"
|
||||
tokio = { version = "1.37.0", features = ["full"] }
|
||||
|
|
61
src/cli.rs
61
src/cli.rs
|
@ -1,10 +1,9 @@
|
|||
use crate::{observe, prelude::*, router::NewRouterError};
|
||||
use prelude::*;
|
||||
use thiserror::Error;
|
||||
pub mod prelude;
|
||||
|
||||
mod prelude {
|
||||
pub use clap::{Args, Parser, Subcommand};
|
||||
}
|
||||
mod run;
|
||||
|
||||
use crate::{observe, prelude::*};
|
||||
use prelude::*;
|
||||
|
||||
/// Web application for managing lyrics and live displays
|
||||
#[derive(Parser)]
|
||||
|
@ -21,61 +20,23 @@ pub struct Cli {
|
|||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Run the web application server
|
||||
Run(Run),
|
||||
}
|
||||
|
||||
/// Doc comment
|
||||
#[derive(Args)]
|
||||
struct Run {
|
||||
/// Whether or not to watch certain resource files for changes and reload accordingly
|
||||
#[arg(short, long, default_value = None)]
|
||||
pub watch: bool,
|
||||
|
||||
/// The address to bind to - you almost certainly want to use :: or 0.0.0.0 instead of the default
|
||||
#[arg(short = 'H', long, default_value = "::1")]
|
||||
pub host: String,
|
||||
|
||||
/// The port to bind to
|
||||
#[arg(short, long, default_value = "3000")]
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum RunError {
|
||||
#[error("router error: {0}")]
|
||||
Router(#[from] NewRouterError),
|
||||
|
||||
#[error("io error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
impl Run {
|
||||
pub async fn run(&self) -> Result<(), RunError> {
|
||||
let (router, _watchers) = crate::router::router(self.watch).await?;
|
||||
Ok(
|
||||
crate::webserver::webserver(router, self.watch, Some(&self.host), Some(self.port))
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cli() -> Cli {
|
||||
Cli::parse()
|
||||
Run(run::Run),
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ExecError {
|
||||
#[error("run error: {0}")]
|
||||
Run(#[from] RunError),
|
||||
Run(#[from] run::RunError),
|
||||
|
||||
#[error("{0}")]
|
||||
Eyre(#[from] color_eyre::Report),
|
||||
}
|
||||
|
||||
impl Cli {
|
||||
pub async fn exec(self) -> Result<(), ExecError> {
|
||||
observe::setup_logging(&self.log_env_filter)?;
|
||||
match self.command {
|
||||
pub async fn exec() -> Result<(), ExecError> {
|
||||
let cli = Cli::parse();
|
||||
observe::setup_logging(&cli.log_env_filter)?;
|
||||
match cli.command {
|
||||
Commands::Run(args) => Ok(args.run().await?),
|
||||
}
|
||||
}
|
||||
|
|
3
src/cli/prelude.rs
Normal file
3
src/cli/prelude.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
#![allow(unused_imports)]
|
||||
pub use clap::{Args, Parser, Subcommand};
|
||||
pub use thiserror::Error;
|
52
src/cli/run.rs
Normal file
52
src/cli/run.rs
Normal file
|
@ -0,0 +1,52 @@
|
|||
use super::prelude::*;
|
||||
use crate::{
|
||||
router::NewRouterError,
|
||||
state::{NewStateError, State},
|
||||
};
|
||||
|
||||
/// Doc comment
|
||||
#[derive(Args)]
|
||||
pub struct Run {
|
||||
/// Whether or not to watch certain resource files for changes and reload accordingly
|
||||
#[arg(short, long, default_value = None)]
|
||||
pub watch: bool,
|
||||
|
||||
/// The address to bind to - you almost certainly want to use :: or 0.0.0.0 instead of the default
|
||||
#[arg(short = 'H', long, default_value = "::1")]
|
||||
pub host: String,
|
||||
|
||||
/// The port to bind to
|
||||
#[arg(short, long, default_value = "3000")]
|
||||
pub port: u16,
|
||||
|
||||
/// The database connection string to use
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
default_value = "postgresql://lyrs?host=/var/run/postgresql"
|
||||
)]
|
||||
pub database_connection_string: String,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum RunError {
|
||||
#[error("router error: {0}")]
|
||||
Router(#[from] NewRouterError),
|
||||
|
||||
#[error("io error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("new state error: {0}")]
|
||||
State(#[from] NewStateError),
|
||||
}
|
||||
|
||||
impl Run {
|
||||
pub async fn run(&self) -> Result<(), RunError> {
|
||||
let app_state = State::try_new(&self.database_connection_string).await?;
|
||||
let (router, _watchers) = crate::router::router(app_state, self.watch).await?;
|
||||
Ok(
|
||||
crate::webserver::webserver(router, self.watch, Some(&self.host), Some(self.port))
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,5 +1,3 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
mod cli;
|
||||
mod file_watcher;
|
||||
mod observe;
|
||||
|
@ -8,10 +6,12 @@ mod router;
|
|||
mod state;
|
||||
mod static_files;
|
||||
mod tailwind;
|
||||
mod templates;
|
||||
mod webserver;
|
||||
|
||||
use crate::prelude::*;
|
||||
use cli::Cli;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> AnonResult<()> {
|
||||
Ok(cli::cli().exec().await?)
|
||||
Ok(Cli::exec().await?)
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ pub fn setup_logging(env_filter: &str) -> Result<(), color_eyre::Report> {
|
|||
))
|
||||
.parse_lossy(env_filter);
|
||||
|
||||
info!("{filter}");
|
||||
info!(%filter);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
239
src/router.rs
239
src/router.rs
|
@ -1,5 +1,3 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use crate::{
|
||||
file_watcher::FileWatcher,
|
||||
prelude::*,
|
||||
|
@ -7,34 +5,26 @@ use crate::{
|
|||
static_files,
|
||||
};
|
||||
use axum::{
|
||||
body::Bytes,
|
||||
extract::{MatchedPath, State},
|
||||
http::{HeaderMap, Request, Response, StatusCode},
|
||||
http::StatusCode,
|
||||
response::{Html, IntoResponse},
|
||||
routing::get,
|
||||
Router,
|
||||
routing::{get, post},
|
||||
Form, Router,
|
||||
};
|
||||
use minijinja::context;
|
||||
use tower_http::{classify::ServerErrorsFailureClass, trace::TraceLayer};
|
||||
use tower_livereload::LiveReloadLayer;
|
||||
|
||||
use maud::{html, Markup, PreEscaped, DOCTYPE};
|
||||
use redact::Secret;
|
||||
use serde::Deserialize;
|
||||
use thiserror::Error;
|
||||
use tracing::{info_span, Span};
|
||||
use tower_http::trace::TraceLayer;
|
||||
use tower_livereload::LiveReloadLayer;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum NewRouterError {
|
||||
#[error("new state error: {0}")]
|
||||
State(#[from] NewStateError),
|
||||
|
||||
#[error("watcher error: {0}")]
|
||||
Watcher(#[from] notify::Error),
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ReqError {
|
||||
#[error("template error: {0}")]
|
||||
Template(#[from] minijinja::Error),
|
||||
}
|
||||
pub enum ReqError {}
|
||||
|
||||
impl IntoResponse for ReqError {
|
||||
fn into_response(self) -> axum::http::Response<axum::body::Body> {
|
||||
|
@ -51,10 +41,9 @@ impl IntoResponse for ReqError {
|
|||
pub type ReqResult<T> = Result<T, ReqError>;
|
||||
|
||||
pub async fn router(
|
||||
state: AppState,
|
||||
with_watchers: bool,
|
||||
) -> Result<(Router, Vec<Option<FileWatcher>>), NewRouterError> {
|
||||
let state = AppState::try_new().await?;
|
||||
|
||||
let live_reload_layer: Option<LiveReloadLayer> = if with_watchers {
|
||||
Some(LiveReloadLayer::new())
|
||||
} else {
|
||||
|
@ -69,86 +58,174 @@ pub async fn router(
|
|||
}
|
||||
};
|
||||
|
||||
let template_file_watcher = state.clone().templates.start_watcher(orl()).await?;
|
||||
let (static_file_service, static_file_watcher) = static_files::router(orl())?;
|
||||
|
||||
let mut result = Router::new()
|
||||
.route("/", get(index))
|
||||
.route("/about", get(about))
|
||||
.route("/login", get(login))
|
||||
.route("/login", post(authenticate))
|
||||
.route("/register", get(register))
|
||||
.route("/register", post(create_user))
|
||||
.nest_service("/static", static_file_service)
|
||||
// `TraceLayer` is provided by tower-http so you have to add that as a dependency.
|
||||
// It provides good defaults but is also very customizable.
|
||||
//
|
||||
// See https://docs.rs/tower-http/0.1.1/tower_http/trace/index.html for more details.
|
||||
//
|
||||
// If you want to customize the behavior using closures here is how.
|
||||
.layer(
|
||||
TraceLayer::new_for_http()
|
||||
.make_span_with(|request: &Request<_>| {
|
||||
// Log the matched route's path (with placeholders not filled in).
|
||||
// Use request.uri() or OriginalUri if you want the real path.
|
||||
let matched_path = request
|
||||
.extensions()
|
||||
.get::<MatchedPath>()
|
||||
.map(MatchedPath::as_str);
|
||||
|
||||
info_span!(
|
||||
"http_request",
|
||||
method = ?request.method(),
|
||||
matched_path,
|
||||
some_other_field = tracing::field::Empty,
|
||||
)
|
||||
})
|
||||
.on_request(|_request: &Request<_>, _span: &Span| {
|
||||
// You can use `_span.record("some_other_field", value)` in one of these
|
||||
// closures to attach a value to the initially empty field in the info_span
|
||||
// created above.
|
||||
})
|
||||
.on_response(|response: &Response<_>, latency: Duration, _span: &Span| {
|
||||
trace!("on_response: {response:?} in {latency:?}");
|
||||
})
|
||||
.on_body_chunk(|_chunk: &Bytes, _latency: Duration, _span: &Span| {
|
||||
// ...
|
||||
})
|
||||
.on_eos(
|
||||
|_trailers: Option<&HeaderMap>, _stream_duration: Duration, _span: &Span| {
|
||||
// ...
|
||||
},
|
||||
)
|
||||
.on_failure(
|
||||
|error: ServerErrorsFailureClass, latency: Duration, _span: &Span| {
|
||||
error!("on_failure: {error:?} in {latency:?}");
|
||||
// ...
|
||||
},
|
||||
),
|
||||
)
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.with_state(state.clone());
|
||||
|
||||
if let Some(lr) = live_reload_layer {
|
||||
result = result.clone().layer(lr);
|
||||
}
|
||||
|
||||
let watchers = vec![template_file_watcher, static_file_watcher];
|
||||
let watchers = vec![static_file_watcher];
|
||||
|
||||
Ok((result, watchers))
|
||||
}
|
||||
|
||||
#[instrument(skip(state))]
|
||||
async fn index(State(state): State<AppState>) -> ReqResult<Html<String>> {
|
||||
fn stylesheet(url: &str) -> Markup {
|
||||
html! {
|
||||
link rel="stylesheet" type="text/css" href=(url);
|
||||
}
|
||||
}
|
||||
|
||||
fn head(page_title: &str) -> Markup {
|
||||
html! {
|
||||
(DOCTYPE)
|
||||
meta charset="utf-8" {}
|
||||
meta name="viewport" content="width=device-width, initial-scale=1" {}
|
||||
title { (page_title) " - lyrs" }
|
||||
(stylesheet("/static/style.css"));
|
||||
|
||||
script src="https://unpkg.com/htmx.org@1.9.12" integrity="sha384-ujb1lZYygJmzgSwoxRggbCHcjc0rB2XoQrxeTUQyRjrOnlCoYta87iKBWq3EsdM2" crossorigin="anonymous" defer {}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fn foot() -> Markup {
|
||||
html! {
|
||||
footer class="p-2 border-t-2 border-surface0 flex overflow-x-scroll" {
|
||||
section {
|
||||
(PreEscaped("© 2024 "))
|
||||
a .underline.text-mauve href="https://lyte.dev" { "lytedev" }
|
||||
}
|
||||
section .ml-auto {("Made with ❤️")}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn page(title: &str, content: Markup) -> ReqResult<Html<String>> {
|
||||
Ok(Html(
|
||||
state
|
||||
.templates
|
||||
.render("pages/index.jinja.html", context!())
|
||||
.await?,
|
||||
html! {
|
||||
(head(title))
|
||||
body .bg-bg.text-text.min-h-lvh.flex.flex-col.font-sans {
|
||||
header class="drop-shadow border-b-2 border-surface0 bg-blue-500 flex overflow-x-scroll" {
|
||||
a class="flex p-2 text-3xl font-mono text-mauve opacity-80 hover:bg-mauve hover:text-bg" href="/" { "lyrs" }
|
||||
nav class="flex flex-1 justify-start" {
|
||||
a class="flex items-center p-2 hover:bg-mauve hover:text-bg" href="/login" { "Login" }
|
||||
a class="flex items-center p-2 hover:bg-mauve hover:text-bg" href="/register" { "Register" }
|
||||
}
|
||||
}
|
||||
main class="flex flex-col flex-1 relative overflow-x-scroll bg-mantle" {
|
||||
(content)
|
||||
}
|
||||
}
|
||||
(foot())
|
||||
}.into_string()
|
||||
))
|
||||
}
|
||||
|
||||
#[instrument(skip(state))]
|
||||
async fn about(State(state): State<AppState>) -> ReqResult<Html<String>> {
|
||||
async fn index() -> ReqResult<Html<String>> {
|
||||
page("index", html! { "Index" })
|
||||
}
|
||||
|
||||
async fn about() -> ReqResult<Html<String>> {
|
||||
page("index", html! { "About" })
|
||||
}
|
||||
|
||||
fn center_hero_form(title: &str, content: Markup, subform: Markup) -> Markup {
|
||||
html! {
|
||||
section class="hero grow" {
|
||||
form class="flex flex-col gap-2 w-full max-w-sm" method="post" {
|
||||
header {
|
||||
h1 class="pb-2 text-center text-xl" { (title) }
|
||||
}
|
||||
(content)
|
||||
input class="bg-mauve" value="Submit" type="submit";
|
||||
}
|
||||
(subform)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn labelled_input(label: &str, input: Markup) -> Markup {
|
||||
html! {
|
||||
label class="flex flex-col" {
|
||||
(label)
|
||||
(input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn login() -> ReqResult<Html<String>> {
|
||||
let form = html! {
|
||||
(labelled_input("Username", html!{
|
||||
input class="input" type="text" name="username" autocomplete="username" required;
|
||||
}))
|
||||
(labelled_input("Password", html!{
|
||||
input class="input" type="password" name="password" autocomplete="current-password" required;
|
||||
}))
|
||||
};
|
||||
let subaction = html! {
|
||||
small class="mt-4" {
|
||||
"Need an account? "
|
||||
a href="/register" {"Get one"}
|
||||
"."
|
||||
}
|
||||
};
|
||||
page("login", center_hero_form("Login", form, subaction))
|
||||
}
|
||||
|
||||
async fn register() -> ReqResult<Html<String>> {
|
||||
let form = html! {
|
||||
(labelled_input("Username", html!{
|
||||
input class="input" type="text" name="username" required;
|
||||
}))
|
||||
(labelled_input("Password", html!{
|
||||
input class="input" type="password" name="password" autocomplete="new-password" required;
|
||||
}))
|
||||
};
|
||||
let subaction = html! {
|
||||
small class="mt-4" {
|
||||
"Already have an account? "
|
||||
a href="/login" {"Login"}
|
||||
"."
|
||||
}
|
||||
};
|
||||
page("login", center_hero_form("Register", form, subaction))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct Creds {
|
||||
username: String,
|
||||
password: Secret<String>,
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn authenticate(Form(creds): Form<Creds>) -> ReqResult<Html<String>> {
|
||||
info!("login attempt");
|
||||
Ok(Html(
|
||||
state
|
||||
.templates
|
||||
.render("pages/about.jinja.html", context!())
|
||||
.await?,
|
||||
html! {
|
||||
"no"
|
||||
}
|
||||
.into_string(),
|
||||
))
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn create_user(Form(creds): Form<Creds>) -> ReqResult<Html<String>> {
|
||||
info!("registration attempt");
|
||||
Ok(Html(
|
||||
html! {
|
||||
"no"
|
||||
}
|
||||
.into_string(),
|
||||
))
|
||||
}
|
||||
|
|
24
src/state.rs
24
src/state.rs
|
@ -1,26 +1,22 @@
|
|||
use crate::{
|
||||
prelude::*,
|
||||
templates::{TemplateLoadError, Templates},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use crate::prelude::*;
|
||||
use sea_orm::{Database, DatabaseConnection};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct State {
|
||||
pub templates: Arc<Templates>,
|
||||
db: DatabaseConnection,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub async fn try_new() -> Result<Self, NewStateError> {
|
||||
let templates = Arc::new(Templates::try_load("src/templates").await?);
|
||||
|
||||
Ok(Self { templates })
|
||||
pub async fn try_new(database_connection_string: &str) -> Result<Self, NewStateError> {
|
||||
Ok(Self {
|
||||
db: Database::connect(database_connection_string).await?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum NewStateError {
|
||||
#[error("template load error: {0}")]
|
||||
TemplateLoad(#[from] TemplateLoadError),
|
||||
#[error("database error: {0}")]
|
||||
Database(#[from] sea_orm::DbErr),
|
||||
}
|
||||
|
|
|
@ -1,7 +1,64 @@
|
|||
@tailwind base;
|
||||
|
||||
@layer base {
|
||||
|
||||
main a[href] {
|
||||
@apply text-mauve underline;
|
||||
}
|
||||
|
||||
input {
|
||||
@apply opacity-80 hover:opacity-100 p-2 border-2 bg-bg border-surface2 rounded;
|
||||
}
|
||||
|
||||
button,
|
||||
input[type=submit] {
|
||||
@apply opacity-80 hover:opacity-100 p-2 border-0 rounded cursor-pointer text-bg;
|
||||
}
|
||||
|
||||
.hero {
|
||||
@apply flex flex-col p-2 justify-center items-center relative;
|
||||
}
|
||||
|
||||
.card {
|
||||
@apply flex flex-col drop-shadow-xl border-2 border-surface2 rounded p-2 gap-2;
|
||||
}
|
||||
}
|
||||
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@font-face {
|
||||
font-family: iosevkalyte;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local("Iosevka"), url("/static/font/iosevkalyteweb-regular.subset.woff2");
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: iosevkalyte;
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: local("Iosevka"), url("/static/font/iosevkalyteweb-italic.subset.woff2");
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: iosevkalyte;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local("Iosevka"), url("/static/font/iosevkalyteweb-bold.subset.woff2");
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: iosevkalyte;
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: local("Iosevka"), url("/static/font/iosevkalyteweb-bolditalic.subset.woff2");
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
/* Catppuccin Mocha */
|
||||
--Rosewater: #f5e0dc;
|
||||
|
@ -62,4 +119,10 @@
|
|||
--Mantle: #e6e9ef;
|
||||
--Crust: #dce0e8;
|
||||
}
|
||||
}
|
||||
|
||||
:autofill,
|
||||
:-webkit-autofill {
|
||||
filter: none !important;
|
||||
background-color: #f00 !important;
|
||||
}
|
115
src/templates.rs
115
src/templates.rs
|
@ -1,115 +0,0 @@
|
|||
use crate::{file_watcher::prelude::*, prelude::*};
|
||||
use minijinja::Environment;
|
||||
use pathdiff::diff_paths;
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use thiserror::Error;
|
||||
use tokio::sync::Mutex;
|
||||
use tower_livereload::Reloader;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum TemplateLoadError {
|
||||
#[error("template error: {0}")]
|
||||
Minijinja(#[from] minijinja::Error),
|
||||
|
||||
#[error("io error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Templates {
|
||||
env: Arc<Mutex<Environment<'static>>>,
|
||||
dir: PathBuf,
|
||||
}
|
||||
|
||||
impl Templates {
|
||||
pub fn for_dir<P: Into<PathBuf>>(dir: P) -> Self {
|
||||
let env = Arc::new(Mutex::new(Environment::new()));
|
||||
Self {
|
||||
env,
|
||||
dir: dir.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn try_load<P: Into<PathBuf>>(dir: P) -> Result<Self, TemplateLoadError> {
|
||||
let result = Self::for_dir(dir);
|
||||
result.load_env().await?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn start_watcher(
|
||||
self: Arc<Self>,
|
||||
reloader: Option<Reloader>,
|
||||
) -> Result<Option<FileWatcher>, notify::Error> {
|
||||
if let Some(rl) = reloader {
|
||||
Ok(Some(self.watch(rl).await?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
async fn watch(self: Arc<Self>, reloader: Reloader) -> Result<FileWatcher, notify::Error> {
|
||||
let watcher = file_watcher(self.dir.clone(), move |ev| {
|
||||
futures::executor::block_on(async {
|
||||
if ev.kind.is_create() || ev.kind.is_modify() {
|
||||
for p in ev.paths {
|
||||
let _ = self.load_template(&p).await;
|
||||
}
|
||||
reloader.reload();
|
||||
}
|
||||
});
|
||||
})?;
|
||||
Ok(watcher)
|
||||
}
|
||||
|
||||
#[instrument(skip(self, p))]
|
||||
pub async fn load_template<P>(&self, p: P) -> Result<(), TemplateLoadError>
|
||||
where
|
||||
P: AsRef<Path> + Copy,
|
||||
{
|
||||
let p = p.as_ref();
|
||||
if p.is_dir() {
|
||||
return Ok(());
|
||||
}
|
||||
let filename: String = diff_paths(p, "src/templates")
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
if [".bck", ".tmp"].iter().any(|s| filename.ends_with(s)) {
|
||||
debug!("skipping temporary file");
|
||||
return Ok(());
|
||||
}
|
||||
info!(filename);
|
||||
Ok(self
|
||||
.env
|
||||
.lock()
|
||||
.await
|
||||
.add_template_owned(filename, std::fs::read_to_string(p)?)?)
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub async fn load_env(&self) -> Result<(), TemplateLoadError> {
|
||||
for d in walkdir::WalkDir::new(&self.dir) {
|
||||
match d {
|
||||
Ok(d) => self.load_template(d.path()).await?,
|
||||
Err(_) => todo!(),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn render<S: serde::ser::Serialize>(
|
||||
&self,
|
||||
template_name: &str,
|
||||
context: S,
|
||||
) -> Result<String, minijinja::Error> {
|
||||
Ok(self
|
||||
.env
|
||||
.lock()
|
||||
.await
|
||||
.get_template(template_name)?
|
||||
.render(context)?)
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head hx-preserve="true">
|
||||
<title>Sup</title>
|
||||
<link rel="stylesheet" type="text/css" href="/static/style.css" />
|
||||
<script hx-preserve="true" src="https://unpkg.com/htmx.org@1.9.12"
|
||||
integrity="sha384-ujb1lZYygJmzgSwoxRggbCHcjc0rB2XoQrxeTUQyRjrOnlCoYta87iKBWq3EsdM2"
|
||||
crossorigin="anonymous" defer></script>
|
||||
</head>
|
||||
|
||||
<body class="bg-base text-text">
|
||||
Page Template
|
||||
<nav>
|
||||
<a href="/">Index</a>
|
||||
<a href="/about">About</a>
|
||||
</nav>
|
||||
{% block body %}{% endblock %}
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,7 +0,0 @@
|
|||
{% extends "page.jinja.html" %}
|
||||
{% block body %}
|
||||
<h1>About</h1>
|
||||
<p class="important">
|
||||
Welcome to my awesome about page!
|
||||
</p>
|
||||
{% endblock %}
|
|
@ -1,7 +0,0 @@
|
|||
{% extends "page.jinja.html" %}
|
||||
{% block body %}
|
||||
<h1>Index</h1>
|
||||
<p class="important">
|
||||
Welcome to my awesome index!
|
||||
</p>
|
||||
{% endblock %}
|
BIN
static/font/iosevkalyteweb-bold.subset.woff2
Normal file
BIN
static/font/iosevkalyteweb-bold.subset.woff2
Normal file
Binary file not shown.
BIN
static/font/iosevkalyteweb-bolditalic.subset.woff2
Normal file
BIN
static/font/iosevkalyteweb-bolditalic.subset.woff2
Normal file
Binary file not shown.
BIN
static/font/iosevkalyteweb-italic.subset.woff2
Normal file
BIN
static/font/iosevkalyteweb-italic.subset.woff2
Normal file
Binary file not shown.
BIN
static/font/iosevkalyteweb-regular.subset.woff2
Normal file
BIN
static/font/iosevkalyteweb-regular.subset.woff2
Normal file
Binary file not shown.
|
@ -1,35 +1,39 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ["./src/**/*"], theme: {
|
||||
colors: {
|
||||
rosewater: "var(--Rosewater)",
|
||||
flamingo: "var(--Flamingo)",
|
||||
pink: "var(--Pink)",
|
||||
mauve: "var(--Mauve)",
|
||||
red: "var(--Red)",
|
||||
maroon: "var(--Maroon)",
|
||||
peach: "var(--Peach)",
|
||||
yellow: "var(--Yellow)",
|
||||
green: "var(--Green)",
|
||||
teal: "var(--Teal)",
|
||||
sky: "var(--Sky)",
|
||||
sapphire: "var(--Sapphire)",
|
||||
blue: "var(--Blue)",
|
||||
lavender: "var(--Lavender)",
|
||||
text: "var(--Text)",
|
||||
subtext1: "var(--Subtext1)",
|
||||
subtext0: "var(--Subtext0)",
|
||||
overlay2: "var(--Overlay2)",
|
||||
overlay1: "var(--Overlay1)",
|
||||
overlay0: "var(--Overlay0)",
|
||||
surface2: "var(--Surface2)",
|
||||
surface1: "var(--Surface1)",
|
||||
surface0: "var(--Surface0)",
|
||||
base: "var(--Base)",
|
||||
mantle: "var(--Mantle)",
|
||||
crust: "var(--Crust)",
|
||||
},
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['ui-sans-serif', 'system-ui', 'sans-serif', "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"],
|
||||
mono: ['iosevkalyte', 'ui-monospace', 'monospace'],
|
||||
},
|
||||
colors: {
|
||||
rosewater: "var(--Rosewater)",
|
||||
flamingo: "var(--Flamingo)",
|
||||
pink: "var(--Pink)",
|
||||
mauve: "var(--Mauve)",
|
||||
red: "var(--Red)",
|
||||
maroon: "var(--Maroon)",
|
||||
peach: "var(--Peach)",
|
||||
yellow: "var(--Yellow)",
|
||||
green: "var(--Green)",
|
||||
teal: "var(--Teal)",
|
||||
sky: "var(--Sky)",
|
||||
sapphire: "var(--Sapphire)",
|
||||
blue: "var(--Blue)",
|
||||
lavender: "var(--Lavender)",
|
||||
text: "var(--Text)",
|
||||
subtext1: "var(--Subtext1)",
|
||||
subtext0: "var(--Subtext0)",
|
||||
overlay2: "var(--Overlay2)",
|
||||
overlay1: "var(--Overlay1)",
|
||||
overlay0: "var(--Overlay0)",
|
||||
surface2: "var(--Surface2)",
|
||||
surface1: "var(--Surface1)",
|
||||
surface0: "var(--Surface0)",
|
||||
bg: "var(--Base)",
|
||||
mantle: "var(--Mantle)",
|
||||
crust: "var(--Crust)",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
|
|
Loading…
Reference in a new issue