🚀 Init public commit

This commit is contained in:
Markus Scully 2025-07-27 00:42:40 +03:00
commit 62fbf6d17c
Signed by: mascully
GPG key ID: 93CA5814B698101C
42 changed files with 7433 additions and 0 deletions

3
.cargo/config.toml Normal file
View file

@ -0,0 +1,3 @@
[build]
target-dir = "target/cargo"
rustflags = ["-A", "unused_imports"]

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target/

5528
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

78
Cargo.toml Normal file
View file

@ -0,0 +1,78 @@
[workspace]
members = ["crates/*"]
resolver = "3"
edition = "2024"
[workspace.package]
version = "0.1.0"
edition = "2024"
name = "mascully_website"
license = "MIT"
[workspace.metadata.crane]
name = "mascully_website"
[[workspace.metadata.leptos]]
name = "mascully_website"
bin-package = "mascully_website_server"
lib-package = "mascully_website_frontend"
assets-dir = "crates/frontend/assets"
style-file = "crates/frontend/assets/css/styles.css"
site-root = "target/site/mascully_website"
server-fn-prefix = "/custom/prefix"
# disable-server-fn-hash = true
# server-fn-mod-path = true
site-addr = "127.0.0.1:3002"
# hash-files = true
# disable-erase-components = true
[workspace.dependencies]
async-trait = "0.1.74"
axum = "0.8.4"
comrak = "0.29.0"
console_error_panic_hook = { version = "0.1.7" }
console_log = { version = "1.0" }
dotenvy = { version = "0.15.6" }
encase = { version = "0.11.1", features = ["glam"] }
futures = { version = "0.3.31" }
glam = { version = "0.30.0" }
gloo = { version = "0.11.0", features = ["timers"] }
gloo-net = { version = "0.6", features = ["http"] }
js-sys = "0.3.77"
leptos = { version = "0.8.4", features = ["nightly"] }
leptos_axum = "0.8.4"
leptos_dom = { version = "0.8.4" }
leptos_meta = { version = "0.8.4" }
leptos_router = { version = "0.8.4" }
log = "0.4.17"
mime_guess = "2.0.4"
reqwest = { version = "0.12.8", features = [
"json",
"rustls-tls",
], default-features = false }
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1.0.128" }
simple_logger = { version = "5.0.0" }
tokio = { version = "1.33.0", features = ["full"] }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.17", features = ["env-filter", "json"] }
wasm-bindgen = { version = "=0.2.100" }
wasm-bindgen-futures = "0.4.50"
web-sys = { version = "0.3.77", features = [
"HtmlCanvasElement",
"Window",
"Document",
"Element",
"DomRect",
"Performance",
"WebGl2RenderingContext",
"WebGlBuffer",
"WebGlVertexArrayObject",
"WebGlProgram",
"WebGlShader",
"WebGlUniformLocation",
] }
wgpu = { version = "26.0.1", features = ["webgl"] }
tower = { version = "0.5.2", features = ["full"] }
tower-http = { version = "0.6.6", features = ["full"] }
indoc = "2.0.6"

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Markus Scully
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

39
README.md Normal file
View file

@ -0,0 +1,39 @@
# Mascully Website
My personal website. Hosted at [mascully.com](https://mascully.com). The currently hosted version may not always exactly reflect the contents of this repo.
Built using [Leptos](https://leptos.dev/) with [Axum](https://github.com/tokio-rs/axum).
## Running
### Nix
Run directly with
```bash
nix develop -c cargo-leptos serve
```
Develop with
```bash
nix develop
cargo-leptos watch
```
I'm still working on getting the flake working, so for now you can't use `nix run`. I also haven't figured out how to bundle it as a standalone binary yet.
### Other systems
- Install Rust nightly.
- Install [Cargo Leptos](https://github.com/leptos-rs/cargo-leptos).
- `cargo-leptos serve`
## Code structure
- `crates/app`: The actual website.
- `crates/frontend`: Images, fonts, CSS.
- `crates/server`: A minimal Axum server for serving pages.
- `flake.nix`: This doesn't work; ignore it.
## License
MIT

26
crates/app/Cargo.toml Normal file
View file

@ -0,0 +1,26 @@
[package]
name = "mascully_website_app"
version.workspace = true
edition.workspace = true
[features]
default = ["hydrate"]
hydrate = ["leptos/hydrate"]
ssr = ["leptos/ssr"]
[dependencies]
leptos.workspace = true
leptos_meta.workspace = true
leptos_router.workspace = true
leptos_axum = { workspace = true, optional = true }
wgpu.workspace = true
web-sys.workspace = true
encase.workspace = true
glam.workspace = true
log.workspace = true
gloo.workspace = true
wasm-bindgen.workspace = true
js-sys.workspace = true
wasm-bindgen-futures.workspace = true
serde.workspace = true
indoc.workspace = true

78
crates/app/src/article.rs Normal file
View file

@ -0,0 +1,78 @@
use leptos::prelude::*;
use leptos_router::{components::A, hooks::use_params_map};
use crate::wgpu_renderer::WgpuTriangle;
#[derive(Clone)]
pub struct ArticleMetadata {
pub title: &'static str,
pub slug: &'static str,
pub description: &'static str,
}
#[component]
pub fn ArticlePage() -> impl IntoView {
let params = use_params_map();
// let slug = move || params.with(|params| params.get("slug").unwrap_or_default());
// let article_content = move || match slug().as_str() {
// "creating-an-actually-perfect-grid-shader" => view! {
// <div class="article grid-shader-article">
// <h1>"Creating an actually perfect grid shader"</h1>
// <div class="article-metadata">
// <p class="date">"March 9, 2024"</p>
// </div>
// <p>"Grid shaders are essential for many visualization and design applications, but creating the perfect grid involves some nuanced considerations."</p>
// <h2>"Basic Triangle Rendering"</h2>
// <p>"Before we get to grids, let's start with a basic triangle rendering implementation using WebGL:"</p>
// <div class="visualization">
// <WgpuTriangle />
// </div>
// <h2>"Building the Grid Shader"</h2>
// <p>"The key to a perfect grid shader lies in the precision of the grid lines and the handling of coordinate spaces."</p>
// <h2>"Avoiding Common Pitfalls"</h2>
// <p>"Many grid implementations suffer from aliasing issues or precision problems at certain scales."</p>
// <h2>"Conclusion"</h2>
// <p>"With the right approach, you can create grid shaders that look perfect at any scale."</p>
// </div>
// },
// "minimal-example-article" => view! {
// <div class="article minimal-article">
// <h1>"Minimal Example Article"</h1>
// <div class="article-metadata">
// <p class="date">"March 9, 2024"</p>
// </div>
// <p>"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam auctor, nisl eget ultricies tincidunt, nisl nisl aliquam nisl, eget ultricies nisl nisl eget nisl."</p>
// <h2>"Section One"</h2>
// <p>"Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."</p>
// <h2>"Section Two"</h2>
// <p>"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."</p>
// </div>
// },
// _ => view! {
// <div class="article-not-found">
// <h1>"Article Not Found"</h1>
// <p>"The article you're looking for doesn't exist."</p>
// <A href="/">"Return to Home"</A>
// </div>
// },
// };
view! {
<div class="article-page">
<nav class="article-nav">
<A href="/">"← Back to Home"</A>
</nav>
// {article_content}
</div>
}
}

View file

@ -0,0 +1,6 @@
use leptos::prelude::*;
#[component]
pub fn CommandLine() -> impl IntoView {
view! { <input class="terminal-input" type="text" placeholder="$ " /> }
}

30
crates/app/src/contact.rs Normal file
View file

@ -0,0 +1,30 @@
#![allow(unused)]
#![warn(unused_must_use)]
use leptos::prelude::*;
#[component]
pub fn Contact() -> impl IntoView {
view! {
<section>
<h1>"Contact"</h1>
<section>
<span>"Email: "</span>
<a href="mailto:contact@mascully.com">"contact@mascully.com"</a>
</section>
<section>
<span>"GitHub: "</span>
<a href="https://github.com/mascully">"mascully"</a>
</section>
<section>
<span>"Matrix: "</span>
<a href="matrix:u/blue_flycatcher:matrix.org">"@blue_flycatcher:matrix.org"</a>
</section>
</section>
<section>
<span>"PGP:"</span>
<pre>{include_str!("security/pgp_master.txt")}</pre>
</section>
}
}

154
crates/app/src/lib.rs Normal file
View file

@ -0,0 +1,154 @@
#![allow(unused)]
#![warn(unused_must_use)]
use leptos::hydration::{AutoReload, HydrationScripts};
use leptos::prelude::*;
use leptos_meta::{Link, MetaTags, Stylesheet, Title, provide_meta_context};
use leptos_router::components::{A, FlatRoutes, Route, Router, Routes, RoutingProgress};
use leptos_router::{ParamSegment, StaticSegment, path};
use std::time::Duration;
use contact::Contact;
use posts::{Posts, plot_demo::PlotDemo};
use projects::Projects;
use theme_switcher::SiteHeader;
mod command_line;
mod contact;
mod plot;
mod posts;
mod projects;
mod sigil;
mod theme_switcher;
mod version;
pub fn shell(options: LeptosOptions) -> impl IntoView {
view! {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<AutoReload options=options.clone() />
<HydrationScripts options />
<MetaTags />
</head>
<body>
<App />
</body>
</html>
}
}
#[component]
pub fn App() -> impl IntoView {
provide_meta_context();
view! {
<Stylesheet href="/css/styles.css" />
<Link rel="icon" href="/icon_2.png" />
<Link
rel="preload"
href="/fonts/space_grotesk_variable.woff2"
as_="font"
type_="font/woff2"
crossorigin="anonymous"
/>
<Link
rel="preload"
href="/fonts/ibm_plex_sans_variable.ttf"
as_="font"
type_="font/ttf"
crossorigin="anonymous"
/>
<Link
rel="preload"
href="/fonts/zed_mono_variable.woff2"
as_="font"
type_="font/woff2"
crossorigin="anonymous"
/>
<Title text="mascully.com" />
<Root>
<SiteHeader />
<div class="content-wrapper">
<div class="main-content">
<Router>
<aside class="sidebar">
<nav class="main-nav">
<ul>
<li>
<A href="/">"Home"</A>
</li>
<li>
<A href="/posts">"Posts"</A>
</li>
<li>
<A href="/projects">"Projects"</A>
</li>
<li>
<hr />
</li>
<li>
<a href="https://git.mascully.com/mascully?tab=repositories">
"Git"
</a>
</li>
<li>
<A href="/contact">"Contact"</A>
</li>
<li>
<A href="/hire_me">"Hire me"</A>
</li>
</ul>
</nav>
</aside>
<main>
<Routes fallback=NotFound>
<Route path=path!("/") view=HomePage />
<Route path=path!("/posts") view=Posts />
<Route path=path!("/posts/plot_demo") view=PlotDemo />
<Route path=path!("/projects") view=Projects />
<Route path=path!("/contact") view=Contact />
<Route path=path!("/hire_me") view=HireMe />
</Routes>
</main>
</Router>
</div>
</div>
</Root>
}
}
#[component]
fn Root(children: Children) -> impl IntoView {
view! { <div id="root">{children()}</div> }
}
#[component]
fn NotFound() -> impl IntoView {
view! { <h1>404</h1> }
}
#[component]
fn HomePage() -> impl IntoView {
view! {
<p>"Hi. I'm Markus. I mostly do full stack development with Rust and Nix."</p>
<p>
"You can check out some of my projects "<A href="/projects">"here"</A>" or on my "<A href="https://git.mascully.com/mascully?tab=repositories">"Git forge"</A>", including the "
<A href="https://git.mascully.com/mascully/mascully_website">
"source code for this website"
</A>"."
</p>
}
}
#[component]
fn HireMe() -> impl IntoView {
view! {
<p>"I am available for freelancing work (remote, European time zone)."</p>
<p>"I can work comfortably with most tech stacks in most domains."</p>
<p>"Email me at "<a href="mailto:contact@mascully.com">"contact@mascully.com"</a>.</p>
}
}

43
crates/app/src/plot.rs Normal file
View file

@ -0,0 +1,43 @@
#![allow(unused)]
#![warn(unused_must_use)]
use leptos::prelude::*;
use wasm_bindgen::prelude::*;
use web_sys::HtmlCanvasElement;
#[derive(Clone, Debug)]
pub struct Camera {
// TODO: Add camera fields (position, zoom, etc.)
}
impl Default for Camera {
fn default() -> Self {
Self {
// TODO: Initialize default camera values
}
}
}
#[derive(Clone, Debug)]
pub struct Element {
// TODO: Add element fields (position, shape, color, etc.)
}
#[component]
pub fn Plot(camera: Camera, elements: Vec<Element>) -> impl IntoView {
let canvas_ref = NodeRef::<leptos::html::Canvas>::new();
// TODO: Implement rendering logic
// - Set up canvas context
// - Handle resize events
// - Render elements based on camera view
// - Handle user interactions (pan, zoom, etc.)
view! {
<div class="plot-container">
<canvas node_ref=canvas_ref class="plot-canvas" width="800" height="600">
"Your browser does not support the HTML5 Canvas element."
</canvas>
</div>
}
}

53
crates/app/src/posts.rs Normal file
View file

@ -0,0 +1,53 @@
#![allow(unused)]
#![warn(unused_must_use)]
use leptos::prelude::*;
use leptos_router::components::A;
pub mod plot_demo;
#[derive(Clone)]
pub struct PostMeta {
pub slug: &'static str,
pub title: &'static str,
pub description: &'static str,
}
fn get_all_posts() -> Vec<PostMeta> {
vec![
// PostMeta {
// slug: "plot_demo",
// title: "Interactive Plot Demo",
// description: "A demonstration of the Plot component with interactive graphics rendering.",
// },
]
}
#[component]
pub fn Posts() -> impl IntoView {
let posts = get_all_posts();
view! {
<section>
<header>
<h1>"Posts"</h1>
</header>
"I haven't published anything yet. Try again later."
<section class="posts-list">
{posts
.into_iter()
.map(|post| {
view! {
<article class="post-preview">
<h2>
<A href=format!("/posts/{}", post.slug)>{post.title}</A>
</h2>
<p class="post-description">{post.description}</p>
</article>
}
})
.collect::<Vec<_>>()}
</section>
</section>
}
}

View file

@ -0,0 +1,37 @@
#![allow(unused)]
#![warn(unused_must_use)]
use crate::plot::{Camera, Element, Plot};
use leptos::prelude::*;
#[component]
pub fn PlotDemo() -> impl IntoView {
let camera = Camera::default();
// TODO: Create sample elements for the plot
let elements = vec![
Element {
// TODO: Initialize with sample data
},
Element {
// TODO: Initialize with sample data
},
];
view! {
<article class="plot-demo-post">
<header>
<h1>"Interactive Plot Demo"</h1>
<p class="post-description">
"A demonstration of the Plot component with interactive graphics rendering."
</p>
</header>
<section class="plot-section">
<h2>"Sample Plot"</h2>
<p>"This plot component will render interactive graphics:"</p>
<Plot camera=camera elements=elements />
<p>"TODO: Implement rendering logic, camera controls, and element interactions."</p>
</section>
</article>
}
}

122
crates/app/src/projects.rs Normal file
View file

@ -0,0 +1,122 @@
#![allow(unused)]
#![warn(unused_must_use)]
use indoc::indoc;
use leptos::prelude::*;
use leptos_router::components::A;
#[component]
pub fn Projects() -> impl IntoView {
view! {
<h1>"Personal projects"</h1>
<section class="projects-list">
<ProjectSummary image="/parva_1.png".into()>
<ProjectSummaryTitle slot>"LittleBigPlanet Computer"</ProjectSummaryTitle>
<ProjectSummaryMain slot>
<p>
"A 120Hz 24-bit computer built using logic gates inside the game "
<A href="https://en.wikipedia.org/wiki/LittleBigPlanet_3">
"LittleBigPlanet 3"
</A>" that I call \"Parva\"."
</p>
<p>
"There's been several iterations of it. Currently I'm trying to expand the memory from ~2kiB to 24kiB using "
<A href="https://www.youtube.com/watch?v=jzAx65my6N0">"this"</A>
" technique but I'm running into problems with non-determinism in how the game simulates the logic."
</p>
<p>
"In the process of building it, some other members of the LittleBigPlanet logic community and I did a lot of research into the game's mechanics. Some of the findings are documented "
<A href="https://github.com/Nomkid/littlebiglogic/blob/master/wiki/README.md">
"here"
</A>"."
</p>
<p>
"I will write up a blogpost about Parva and the other computers people have built in the game along with instructions on how to use them at some point."
</p>
</ProjectSummaryMain>
<ProjectSummaryLinks slot>
<a href="https://wiki.littlebigcomputer.com">"Wiki"</a>
<a href="https://littlebigcomputer.com">"Assembler"</a>
<a href="https://git.mascully.com/mascully/littlebigcomputer/src/branch/master/programs/parva_0.1">
"Example assembly programs"
</a>
</ProjectSummaryLinks>
</ProjectSummary>
<ProjectSummary image="/carplexity_1.png".into()>
<ProjectSummaryTitle slot>"Carplexity"</ProjectSummaryTitle>
<ProjectSummaryMain slot>
<p>
"Very early in development. A physics car game built using Bevy meant to be similar to Rocket League."
</p>
<p>
"360 ticks per second instead of 120, so less input lag, which is basically the only thing that matters."
</p>
</ProjectSummaryMain>
<ProjectSummaryLinks slot>
<A href="https://carplexity.com">"Website"</A>
</ProjectSummaryLinks>
</ProjectSummary>
<ProjectSummary image="/endolingual_1.png".into()>
<ProjectSummaryTitle slot>"Endolingual"</ProjectSummaryTitle>
<ProjectSummaryMain slot>
<p>"Compile-time string localization for Rust."</p>
// <pre>
// {
// indoc! {
// r#"
// let current_language = get_current_language();
// let my_string = translate!("Enter your password: ");
// println!("{}", my_string[current_language]);
// "#
// }
// }
// </pre>
// <p>
// "You can try this out by clicking the translate button at the top right of this website. All of the translation"
// </p>
</ProjectSummaryMain>
<ProjectSummaryLinks slot>
<A href="https://github.com/mascully/endolingual">"Repo"</A>
</ProjectSummaryLinks>
</ProjectSummary>
</section>
}
}
#[component]
pub fn ProjectSummary(
project_summary_title: ProjectSummaryTitle,
project_summary_main: ProjectSummaryMain,
project_summary_links: ProjectSummaryLinks,
image: String,
) -> impl IntoView {
view! {
<div class="project-grid">
<h2 class="project-title">{(project_summary_title.children)()}</h2>
<picture class="project-image">
<img
src=image
// srcset={image.concat(" x1")}
loading="lazy"
/>
</picture>
<div class="project-text">{(project_summary_main.children)()}</div>
<div class="project-links">{(project_summary_links.children)()}</div>
</div>
}
}
#[slot]
pub struct ProjectSummaryTitle {
children: ChildrenFn,
}
#[slot]
pub struct ProjectSummaryLinks {
children: ChildrenFn,
}
#[slot]
pub struct ProjectSummaryMain {
children: ChildrenFn,
}

View file

View file

@ -0,0 +1,31 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mJMEaHK66RMJKyQDAwIIAQENBAMEU30JORhgrFANgMzvqeaX/T1ZfA9FjLPVsJQx
MTrQ0m6oCBjqi4vSnMmB+AO9BhEGMnaJmrx1xf96eWNWv6OEBAwED6nxB+zxV1O2
ixh+LxMdJCXJfDDTb/DDNkna0nTfNRdjduqNYa5TfXQ56Jz4/srHwJQJg723nrXO
zeEc4wC0Hm1hc2N1bGx5IDxtYXJrdXNAbWFzY3VsbHkuY29tPojTBBMTCgA7FiEE
dGuDuzFFGDwlt8+KXne6BGBk3ZAFAmhyuukCGwMFCwkIBwICIgIGFQoJCAsCBBYC
AwECHgcCF4AACgkQXne6BGBk3ZBeMwIAjrmfwrh/3A0fFAKL8Ov/VzBdh9ErpLI8
MrD2GiFhGUNDZfSQ6pJsu/k9US0YM7HygNBVSs+VCiZIs91XIVcuewH/XOI2yKIG
y/e5r7G9cYm1i5C7tPfux4cI83gd14b368KlrxsrgyVh3RwC6mogB4FO+S0q9mMe
qzTlBXC/8Xxov7iXBGhyuukSCSskAwMCCAEBDQQDBJa9IbHmgiKRbm8r6JCVOCoi
4ZRI4ms90IaI0wctMlhMntnbJFWgW+/SoHuV9fBfIYbuN3L77d6wYA0xhi59UT85
qfTia06Kd6q6KJvHjCLN2nFYHECFU0Qz4Q8U4q+WD2nStYylI8BZixEih7dYzyjh
g7GmA+d7dNCBqiR6aiViAwEKCYi4BBgTCgAgFiEEdGuDuzFFGDwlt8+KXne6BGBk
3ZAFAmhyuukCGwwACgkQXne6BGBk3ZCk2AH6A/w7IvHl4qRZJm+8rjMAgRUf9YyT
XZO2ndkzOHFXsfWIRi/FKd7qP5MXokBRAfwe07zFy4f6rrhhb6XPCKPARAH+OAon
wgnE5CWj8wvUgZ0vjImAp4PlLvF1hhMN3Eb4ptT5ytfTo8Cssgu+Et6YpKoQh2uz
yTjBNEAgyvGT/ORQaLiTBGiEg5UTCSskAwMCCAEBDQQDBBSg23AZV79tF5upAgT5
pLoAXPkMLU5daER2JUElNX+bzSnEoDp6OyMgVawYsXePaCa7k9PUm6PPQU9L2bpi
NxwoUKhISZ+BID87trh2dRMSjta4JU+4SbR8uRz4OlJShjupb2M1MycFLHJ5Z4QS
0tz7DW5OUEZ/XuvpIl9sPy44iQF1BBgTCgAmFiEEdGuDuzFFGDwlt8+KXne6BGBk
3ZAFAmiEg5UCGwIFCQGYxmoAwQkQXne6BGBk3ZC2IAQZEwoAHRYhBEytVj5gfxiX
tnSKV5PKWBS2mBAcBQJohIOVAAoJEJPKWBS2mBActqQCAJSLMh4z9JgpY++JIQfg
pz7NRXOYNWKmGco6OD/msZps4ZoW9AyI8TMGwtioq0cPV7dztsm07JLQteezhIPd
fjAB/2CUnzTmVXzIJmP4LflwaASkVaDx6B5ZmUo+hrgDJ2ka/fSkUUowUboQOTvV
i8q5QxFYMmrG3GHTuLgUYkEPGceyMQIAmCz5SfRmEmKxHQTPhymIgbKT073vkmtI
uy3F+/YolAjKangwm5EPf+huOnddxUEbqlSoxoMM2Zs1rSMubsvnKQH/c+VBdyGR
AtTRFhprNN9qb6F9SRX5F/lOgCuReDRAKAW+FqFWQs1wgBDYm/Y/SA4h4OKzMt09
lUHKVwcI4MRRHw==
=fxmK
-----END PGP PUBLIC KEY BLOCK-----

5
crates/app/src/sigil.rs Normal file
View file

@ -0,0 +1,5 @@
use leptos::{prelude::*, svg::Svg};
pub fn website_icon() -> impl IntoView {
view! { "test" }
}

View file

@ -0,0 +1,175 @@
#![allow(unused)]
#![warn(unused_must_use)]
use leptos::html::Input;
use leptos::prelude::*;
#[cfg(feature = "hydrate")]
use wasm_bindgen::prelude::*;
#[cfg(feature = "hydrate")]
use web_sys::{Storage, window};
use crate::command_line::CommandLine;
#[derive(Clone, Copy, PartialEq)]
pub enum Theme {
Light,
Dark,
Auto,
}
impl Theme {
fn as_str(&self) -> &'static str {
match self {
Theme::Light => "light",
Theme::Dark => "dark",
Theme::Auto => "auto",
}
}
fn from_str(s: &str) -> Self {
match s {
"light" => Theme::Light,
"dark" => Theme::Dark,
_ => Theme::Auto,
}
}
}
#[cfg(feature = "hydrate")]
fn get_stored_theme() -> Theme {
if let Some(window) = window()
&& let Ok(Some(storage)) = window.local_storage()
&& let Ok(Some(theme_str)) = storage.get_item("theme")
{
return Theme::from_str(&theme_str);
}
Theme::Auto
}
#[cfg(not(feature = "hydrate"))]
fn get_stored_theme() -> Theme {
Theme::Auto
}
#[cfg(feature = "hydrate")]
fn set_stored_theme(theme: Theme) {
if let Some(window) = window()
&& let Ok(Some(storage)) = window.local_storage()
{
let _ = storage.set_item("theme", theme.as_str());
}
}
#[cfg(not(feature = "hydrate"))]
fn set_stored_theme(_theme: Theme) {
// No-op on server side
}
#[cfg(feature = "hydrate")]
fn apply_theme(theme: Theme) {
if let Some(window) = window()
&& let Some(document) = window.document()
&& let Some(html) = document.document_element()
{
let class_list = html.class_list();
let _ = class_list.remove_2("light", "dark");
match theme {
Theme::Light => {
let _ = class_list.add_1("light");
}
Theme::Dark => {
let _ = class_list.add_1("dark");
}
Theme::Auto => {}
}
}
}
#[cfg(not(feature = "hydrate"))]
fn apply_theme(_theme: Theme) {
// No-op on server side
}
#[component]
pub fn ThemeSwitcher() -> impl IntoView {
let (current_theme, set_current_theme) = signal(Theme::Auto);
let (dropdown_open, set_dropdown_open) = signal(false);
// Load theme from localStorage and apply it on client side only
Effect::new(move |_| {
#[cfg(feature = "hydrate")]
{
let stored_theme = get_stored_theme();
set_current_theme.set(stored_theme);
apply_theme(stored_theme);
}
});
// Apply theme when it changes (client side only)
Effect::new(move |_| {
#[cfg(feature = "hydrate")]
{
let theme = current_theme.get();
apply_theme(theme);
}
});
let toggle_dropdown = move |_| {
set_dropdown_open.update(|open| *open = !*open);
};
let select_theme = move |theme: Theme| {
set_current_theme.set(theme);
#[cfg(feature = "hydrate")]
set_stored_theme(theme);
set_dropdown_open.set(false);
};
let theme_icon = move || match current_theme.get() {
Theme::Light => "☀️",
Theme::Dark => "🌙",
Theme::Auto => "🌓",
};
view! {
<div class="theme-switcher">
<button class="theme-button" on:click=toggle_dropdown>
{theme_icon}
</button>
<div class="theme-dropdown" class:hidden=move || !dropdown_open.get()>
<button
class="theme-option"
class:active=move || current_theme.get() == Theme::Light
on:click=move |_| select_theme(Theme::Light)
>
"Light"
</button>
<button
class="theme-option"
class:active=move || current_theme.get() == Theme::Dark
on:click=move |_| select_theme(Theme::Dark)
>
"Dark"
</button>
<button
class="theme-option"
class:active=move || current_theme.get() == Theme::Auto
on:click=move |_| select_theme(Theme::Auto)
>
"Auto"
</button>
</div>
</div>
}
}
#[component]
pub fn SiteHeader() -> impl IntoView {
view! {
<header class="site-header">
<CommandLine />
<ThemeSwitcher />
</header>
}
}

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,183 @@
use leptos::prelude::*;
use log::*;
use std::cell::RefCell;
use std::rc::Rc;
use wasm_bindgen::{prelude::*, JsCast};
use web_sys::{HtmlCanvasElement, WebGl2RenderingContext as GL, WebGlProgram, WebGlShader};
// Vertex shader source
const VERTEX_SHADER_SRC: &str = r#"#version 300 es
precision mediump float;
void main() {
// Define the vertices of a triangle
const vec2 positions[3] = vec2[3](
vec2(0.0, 0.5),
vec2(-0.5, -0.5),
vec2(0.5, -0.5)
);
gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0);
}
"#;
// Fragment shader source
const FRAGMENT_SHADER_SRC: &str = r#"#version 300 es
precision mediump float;
out vec4 fragColor;
void main() {
fragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
"#;
// Implementation for WASM/browser targets
fn set_up_wgpu_triangle(canvas_id: &str) {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
// Check if document is ready
if let Some(element) = document.get_element_by_id(canvas_id) {
let canvas = element.dyn_into::<HtmlCanvasElement>().unwrap();
if let Err(e) = set_up_webgl(&canvas) {
error!("WebGL setup failed: {:?}", e);
} else {
info!("WebGL triangle renderer initialized");
}
}
}
// Sets up WebGL and renders a triangle
fn set_up_webgl(canvas: &HtmlCanvasElement) -> Result<(), JsValue> {
// Get WebGL context
let context = canvas.get_context("webgl2")?.unwrap().dyn_into::<GL>()?;
// Set clear color and viewport
context.clear_color(0.1, 0.2, 0.3, 1.0);
context.clear(GL::COLOR_BUFFER_BIT);
// Create shaders
let vert_shader = compile_shader(&context, GL::VERTEX_SHADER, VERTEX_SHADER_SRC)?;
let frag_shader = compile_shader(&context, GL::FRAGMENT_SHADER, FRAGMENT_SHADER_SRC)?;
// Create program
let program = link_program(&context, &vert_shader, &frag_shader)?;
context.use_program(Some(&program));
// Create empty vertex array object (required for core profile)
let vao = context
.create_vertex_array()
.ok_or("Could not create vertex array object")?;
context.bind_vertex_array(Some(&vao));
// Start rendering
render_loop(context);
Ok(())
}
// Compiles a shader from source
fn compile_shader(context: &GL, shader_type: u32, source: &str) -> Result<WebGlShader, String> {
let shader = context
.create_shader(shader_type)
.ok_or_else(|| String::from("Unable to create shader object"))?;
context.shader_source(&shader, source);
context.compile_shader(&shader);
if context
.get_shader_parameter(&shader, GL::COMPILE_STATUS)
.as_bool()
.unwrap_or(false)
{
Ok(shader)
} else {
Err(context
.get_shader_info_log(&shader)
.unwrap_or_else(|| String::from("Unknown error creating shader")))
}
}
// Links a shader program
fn link_program(
context: &GL,
vert_shader: &WebGlShader,
frag_shader: &WebGlShader,
) -> Result<WebGlProgram, String> {
let program = context
.create_program()
.ok_or_else(|| String::from("Unable to create shader program"))?;
context.attach_shader(&program, vert_shader);
context.attach_shader(&program, frag_shader);
context.link_program(&program);
if context
.get_program_parameter(&program, GL::LINK_STATUS)
.as_bool()
.unwrap_or(false)
{
Ok(program)
} else {
Err(context
.get_program_info_log(&program)
.unwrap_or_else(|| String::from("Unknown error creating program")))
}
}
// Sets up an animation loop
fn render_loop(context: GL) {
// Create a render function
let f = Rc::new(RefCell::new(None));
let g = f.clone();
*g.borrow_mut() = Some(Closure::new(move || {
// Clear the canvas
context.clear_color(0.1, 0.2, 0.3, 1.0);
context.clear(GL::COLOR_BUFFER_BIT);
// Draw the triangle (3 vertices starting at index 0)
context.draw_arrays(GL::TRIANGLES, 0, 3);
// Request next frame
request_animation_frame(f.borrow().as_ref().unwrap());
}));
// Start the render loop
request_animation_frame(g.borrow().as_ref().unwrap());
}
// Helper function to request animation frame
fn request_animation_frame(f: &Closure<dyn FnMut()>) {
web_sys::window()
.unwrap()
.request_animation_frame(f.as_ref().unchecked_ref())
.expect("should register `requestAnimationFrame` OK");
}
#[component]
pub fn WgpuTriangle() -> impl IntoView {
// Use a canvas with a fixed ID
let canvas_id = "triangle-canvas";
// create_effect(move |_| {
// // This will only run client-side code
// if cfg!(target_arch = "wasm32") {
// set_up_wgpu_triangle(canvas_id);
// }
// });
view! {
<div class="triangle-container" style="width: 100%; height: 100%">
<canvas
id=canvas_id
width="800"
height="600"
style="width: 100%; height: 100%; border: 1px solid #ccc"
></canvas>
</div>
}
}

View file

@ -0,0 +1,15 @@
[package]
name = "mascully_website_frontend"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
mascully_website_app = { path = "../app", default-features = false, features = ["hydrate"] }
leptos = { workspace = true, features = [ "hydrate" ] }
log.workspace = true
wasm-bindgen.workspace = true
console_log.workspace = true
console_error_panic_hook.workspace = true

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 KiB

View file

@ -0,0 +1,443 @@
@font-face {
font-family: "Space Grotesk";
font-style: normal;
font-weight: 300 700;
font-display: swap;
src: url("/fonts/space_grotesk_variable.woff2") format("woff2-variations");
}
@font-face {
font-family: "IBM Plex Sans";
font-style: normal;
font-weight: 100 700;
font-display: swap;
src: url("/fonts/ibm_plex_sans_variable.ttf") format("truetype-variations");
}
@font-face {
font-family: "Zed Mono";
font-style: normal;
font-weight: 400 700;
font-display: swap;
src: url("/fonts/zed_mono_variable.woff2") format("woff2-variations");
}
:root {
--body-bg: oklch(15% 0.02 240); /* darker background */
--main-bg: oklch(20% 0 0 / 0.7); /* more transparency */
--text-color: oklch(80% 0 0);
--link-color: oklch(70% 0.15 240); /* lighter blue for links */
--link-hover-color: oklch(80% 0.15 240); /* blue */
--border-color: oklch(40% 0 0);
--post-description-color: oklch(60% 0 0);
--header-bg: oklch(15% 0.02 240); /* same as body background */
--input-bg: oklch(25% 0 0);
--button-bg: oklch(30% 0 0);
--button-hover-bg: oklch(35% 0 0);
--bg-tile: url("/tile_1_dark.png");
--bg-color: oklch(20% 0 0);
}
:root.light {
--navbar-opacity: 0.5;
--navbar-opacity-hover: 1;
--transition-time: 0.3s;
--body-bg: oklch(98% 0 0); /* white */
--main-bg: oklch(100% 0 0 / 0.85); /* white with transparency */
--text-color: oklch(0% 0 0); /* black */
--link-color: oklch(50% 0.15 240); /* blue for links */
--link-hover-color: oklch(60% 0.15 240); /* blue */
--border-color: oklch(90% 0 0);
--post-description-color: oklch(60% 0 0);
--header-bg: oklch(98% 0 0); /* same as body background */
--input-bg: oklch(95% 0 0);
--button-bg: oklch(90% 0 0);
--button-hover-bg: oklch(85% 0 0);
--bg-tile: url("/tile_1_light.png");
--bg-color: oklch(100% 0 0); /* white background */
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--body-bg);
background-image: var(--bg-tile);
background-repeat: repeat;
color: var(--text-color);
display: grid;
grid-template-rows: auto 1fr;
min-height: 100vh;
font-family: "IBM Plex Sans", sans-serif;
}
html {
background-color: var(--bg-color);
color: var(--text-color);
}
p {
padding: 1rem 0rem;
}
h1 {
width: 100%;
text-align: center;
font-size: xx-large;
}
#root {
display: grid;
grid-template-rows: auto 1fr;
min-height: 100%;
}
.content-wrapper {
display: grid;
place-items: center;
padding: 2rem;
}
.main-content {
display: grid;
grid-template-columns: min-content 1fr;
max-width: 1200px;
width: 100%;
}
.sidebar {
opacity: var(--navbar-opacity);
transition: opacity var(--transition-time) ease;
display: grid;
grid-auto-rows: max-content;
grid-auto-flow: row;
padding: 1rem;
align-content: start;
text-align: right;
}
.sidebar:hover {
opacity: var(--navbar-opacity-hover);
}
.main-nav ul {
list-style: none;
display: grid;
gap: 1rem;
}
a {
text-decoration: none;
color: var(--link-color);
transition: color var(--transition-time) ease;
}
a:hover {
color: var(--link-hover-color);
}
.main-nav a {
color: inherit;
}
.main-nav hr {
border: 0;
border-top: 1px solid var(--border-color);
}
main {
position: relative;
z-index: 0;
background-color: var(--main-bg);
backdrop-filter: blur(10px);
border: 1px solid var(--border-color);
padding: 2rem;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: "Space Grotesk", sans-serif;
}
/* Posts page styling */
.posts-page {
max-width: 800px;
}
.posts-page header {
padding-bottom: 2rem;
}
.posts-list {
display: grid;
gap: 2rem;
}
.post-preview {
border-bottom: 1px solid var(--border-color);
padding-bottom: 1.5rem;
}
.post-preview:last-child {
border-bottom: none;
}
.post-preview h2 {
padding-bottom: 0.5rem;
}
.post-preview h2 a {
text-decoration: none;
color: inherit;
transition: color var(--transition-time) ease;
}
.post-preview h2 a:hover {
color: var(--link-hover-color);
}
.post-description {
color: var(--post-description-color);
padding: 0;
line-height: 1.5;
}
/* Plot component styling */
.plot-container {
display: grid;
place-items: center;
padding: 1rem;
background-color: var(--main-bg);
backdrop-filter: blur(10px);
border: 1px solid var(--border-color);
}
.plot-canvas {
border: 1px solid var(--border-color);
background-color: oklch(100% 0 0);
}
.plot-demo-post {
max-width: 900px;
}
.plot-demo-post header {
padding-bottom: 2rem;
}
.plot-section {
display: grid;
gap: 1rem;
}
.plot-section h2 {
padding-bottom: 0.5rem;
}
/* Header styling */
.site-header {
position: relative;
z-index: 1;
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
padding: 1rem 2rem;
background-color: var(--header-bg);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--border-color);
width: 100%;
}
.terminal-input {
font-family: "Zed Mono", monospace;
background: none;
border: none;
color: var(--text-color);
padding: 0.5rem;
font-size: 0.9rem;
width: 100%;
outline: none;
}
.terminal-input::placeholder {
color: var(--post-description-color);
}
.theme-switcher {
position: relative;
display: grid;
}
.theme-button {
background-color: var(--button-bg);
border: 1px solid var(--border-color);
color: var(--text-color);
padding: 0.5rem;
border-radius: 4px;
cursor: pointer;
transition: background-color var(--transition-time) ease;
font-size: 1.2rem;
width: 2.5rem;
height: 2.5rem;
display: grid;
place-items: center;
}
.theme-button:hover {
background-color: var(--button-hover-bg);
}
.theme-dropdown {
position: absolute;
top: 100%;
right: 0;
background-color: var(--header-bg);
backdrop-filter: blur(10px);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 0.5rem;
display: grid;
gap: 0.25rem;
min-width: 80px;
z-index: 9999;
}
.theme-dropdown.hidden {
display: none;
}
.theme-option {
background: none;
border: none;
color: var(--text-color);
padding: 0.5rem;
text-align: left;
cursor: pointer;
border-radius: 2px;
transition: background-color var(--transition-time) ease;
}
.theme-option:hover {
background-color: var(--button-hover-bg);
}
.theme-option.active {
background-color: var(--button-bg);
}
/* Projects page styling */
.projects-list {
display: grid;
gap: 2rem;
}
.project_listing {
border: 1px solid var(--border-color);
padding: 1rem;
background-color: var(--main-bg);
}
.project-grid {
display: grid;
grid-template-columns: 3fr 2fr;
gap: 1rem;
}
.project-title {
grid-column: 1;
grid-row: 1;
}
.project-image {
grid-column: 2;
grid-row: 1 / 4;
}
.project-image img {
width: 100%;
height: auto;
object-fit: cover;
}
.project-text {
grid-column: 1;
grid-row: 2;
}
.project-links {
grid-column: 1;
grid-row: 3;
display: flex;
gap: 1rem;
}
@media (max-width: 768px) {
.main-content {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
}
.sidebar {
grid-auto-flow: column;
justify-items: center;
align-content: center;
text-align: center;
}
.main-nav ul {
grid-auto-flow: column;
}
.plot-canvas {
max-width: 100%;
height: auto;
}
.site-header {
grid-template-columns: 1fr;
gap: 1rem;
text-align: center;
}
.terminal-input {
width: 100%;
}
.theme-switcher {
justify-self: center;
}
.project-grid {
grid-template-columns: 1fr;
}
.project-title {
grid-column: 1;
grid-row: 1;
}
.project-image {
grid-column: 1;
grid-row: 2;
max-width: 100%;
}
.project-text {
grid-column: 1;
grid-row: 3;
}
.project-links {
grid-column: 1;
grid-row: 4;
display: flex;
gap: 1rem;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

View file

@ -0,0 +1,11 @@
use leptos::prelude::*;
use mascully_website_app::App;
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen]
pub fn hydrate() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount::hydrate_body(App);
}

View file

@ -0,0 +1,11 @@
[package]
name = "grid-shader"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
env_logger = "0.11.6"
log = "0.4.25"
wgpu = "26.0.1"
winit = "0.30.11"

View file

@ -0,0 +1,32 @@
// use winit::{
// event::*,
// event_loop::EventLoop,
// keyboard::{KeyCode, PhysicalKey},
// };
// TODO
pub fn run() {
// env_logger::init();
// let event_loop = EventLoop::new().unwrap();
// let window = WindowBuilder::new().build(&event_loop).unwrap();
// let _ = event_loop.run(move |event, control_flow| match event {
// Event::WindowEvent {
// ref event,
// window_id,
// } if window_id == window.id() => match event {
// WindowEvent::CloseRequested
// | WindowEvent::KeyboardInput {
// event:
// KeyEvent {
// state: ElementState::Pressed,
// physical_key: PhysicalKey::Code(KeyCode::Escape),
// ..
// },
// ..
// } => control_flow.exit(),
// _ => {}
// },
// _ => {}
// });
}

27
crates/server/Cargo.toml Normal file
View file

@ -0,0 +1,27 @@
[package]
name = "mascully_website_server"
version.workspace = true
edition.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
mascully_website_app = { path = "../app", default-features = false, features = [
"ssr",
] }
leptos = { workspace = true, features = ["ssr"] }
leptos_meta.workspace = true
leptos_router.workspace = true
leptos_dom.workspace = true
leptos_axum.workspace = true
gloo-net.workspace = true
log.workspace = true
futures.workspace = true
simple_logger.workspace = true
serde_json.workspace = true
reqwest.workspace = true
dotenvy.workspace = true
axum.workspace = true
tokio.workspace = true
tower.workspace = true
tower-http.workspace = true

23
crates/server/src/main.rs Normal file
View file

@ -0,0 +1,23 @@
use leptos::{logging::log, prelude::*};
use leptos_axum::{LeptosRoutes, generate_route_list};
use mascully_website_app::{App, shell};
#[tokio::main]
pub async fn main() {
let conf = get_configuration(None).unwrap();
let addr = conf.leptos_options.site_addr;
let leptos_options = conf.leptos_options;
let routes = generate_route_list(App);
let app = axum::Router::new()
.leptos_routes(&leptos_options, routes, {
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())
})
.fallback(leptos_axum::file_and_error_handler(shell))
.with_state(leptos_options);
log!("listening on http://{}", &addr);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service()).await.unwrap();
}

116
flake.lock generated Normal file
View file

@ -0,0 +1,116 @@
{
"nodes": {
"crane": {
"locked": {
"lastModified": 1752946753,
"narHash": "sha256-g5uP3jIj+STUcfTJDKYopxnSijs2agRg13H0SGL5iE4=",
"owner": "ipetkov",
"repo": "crane",
"rev": "544d09fecc8c2338542c57f3f742f1a0c8c71e13",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"fenix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1752561972,
"narHash": "sha256-pPqES/udciKmKo422mfwRQ3YzjUCVyCTOsgZYA1xh+g=",
"owner": "nix-community",
"repo": "fenix",
"rev": "d17ca03c15660ecb8e5a01ca34e441f594feec62",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "fenix",
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1751413152,
"narHash": "sha256-Tyw1RjYEsp5scoigs1384gIg6e0GoBVjms4aXFfRssQ=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "77826244401ea9de6e3bac47c2db46005e1f30b5",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1752446735,
"narHash": "sha256-Nz2vtUEaRB/UjvPfuhHpez060P/4mvGpXW4JCDIboA4=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "a421ac6595024edcfbb1ef950a3712b89161c359",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1751159883,
"narHash": "sha256-urW/Ylk9FIfvXfliA1ywh75yszAbiTEVgpPeinFyVZo=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "14a40a1d7fb9afa4739275ac642ed7301a9ba1ab",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixpkgs.lib",
"type": "github"
}
},
"root": {
"inputs": {
"crane": "crane",
"fenix": "fenix",
"flake-parts": "flake-parts",
"nixpkgs": "nixpkgs"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1752475356,
"narHash": "sha256-V2nHrCJ0/Pv30j8NWJ4GfDlaNzfkOdYI0jS69GdVpq8=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "e10d64eb402a25a32d9f1ef60cacc89d82a01b85",
"type": "github"
},
"original": {
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

137
flake.nix Normal file
View file

@ -0,0 +1,137 @@
{
description = "Flake for building mascully with Crane";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
flake-parts.url = "github:hercules-ci/flake-parts";
fenix = {
url = "github:nix-community/fenix";
inputs.nixpkgs.follows = "nixpkgs";
};
crane.url = "github:ipetkov/crane";
};
nixConfig = { };
outputs =
inputs@{
flake-parts,
nixpkgs,
fenix,
crane,
...
}:
flake-parts.lib.mkFlake { inherit inputs; } {
debug = true;
systems = nixpkgs.lib.systems.flakeExposed;
perSystem =
{
config,
self',
inputs',
pkgs,
system,
...
}:
let
pkgs = import nixpkgs {
inherit system;
config.allowUnfree = true;
};
rustToolchain = fenix.packages.${system}.combine ([
(fenix.packages.${system}.complete.withComponents [
"cargo"
"clippy"
"rust-src"
"rustc"
"rustfmt"
])
fenix.packages.${system}.complete.rust-analyzer
fenix.packages.${system}.targets.wasm32-unknown-unknown.latest.rust-std
]);
craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain;
# extraPackages = with pkgs; [
# cargo-leptos
# binaryen
# leptosfmt
# ];
commonArgs = {
# src = craneLib.cleanCargoSource ./.;
src = ./.;
strictDeps = true;
buildInputs = with pkgs; [
cargo-leptos
leptosfmt
binaryen
];
nativeBuildInputs = with pkgs; [
cargo-leptos
leptosfmt
binaryen
];
};
leptosOptions = builtins.elemAt (builtins.fromTOML (builtins.readFile ./Cargo.toml)).workspace.metadata.leptos 0;
# --profile=${leptosOptions.lib-profile-release}
cargoArtifacts = craneLib.buildDepsOnly (
commonArgs
// {
# if work is duplicated by the `mascullyWebsiteServer` package, update these
# commands from the logs of `cargo leptos build --release -vvv`
buildPhaseCargoCommand = ''
cargo build --package=${leptosOptions.lib-package} --lib --target-dir=/build/source/target/front --target=wasm32-unknown-unknown --no-default-features
cargo build --package=${leptosOptions.bin-package} --no-default-features --release
'';
}
);
mascullyWebsiteServer = craneLib.buildPackage (
commonArgs
// {
pname = "mascully_website_server";
version = "0.1.0";
nativeBuildInputs = commonArgs.nativeBuildInputs ++ [ pkgs.makeWrapper ];
buildPhaseCargoCommand = ''
RUST_BACKTRACE=full cargo-leptos build --release -vvv
'';
installPhaseCommand = ''
mkdir -p $out/bin
ls target/
pwd
cp target/server/release/mascully_website_server $out/bin/
# cp target/release/hash.txt $out/bin/
cp -r target/site $out/bin/
wrapProgram $out/bin/mascully_website_server --set LEPTOS_SITE_ROOT $out/bin/site
'';
doCheck = false;
doNotPostBuildInstallCargoBinaries = true;
cargoArtifacts = cargoArtifacts;
}
);
in
{
packages = rec {
inherit mascullyWebsiteServer;
default = mascullyWebsiteServer;
};
devShells.default = craneLib.devShell {
inputsFrom = [ mascullyWebsiteServer ];
# packages = extraPackages;
RUST_SRC_PATH = "${rustToolchain}/lib/rustlib/src/rust/library";
};
};
};
}

2
rust-analyzer.toml Normal file
View file

@ -0,0 +1,2 @@
[rustfmt]
overrideCommand = ["leptosfmt", "--stdin", "--rustfmt"]

2
rustfmt.toml Normal file
View file

@ -0,0 +1,2 @@
edition = "2024"
max_width = 120