Better code block CSS, building Leptos on NixOS, a no-WASM fallback message in Leptos, among other things I did this week.
Website
I fixed a couple of earlier log entries that had too-long code blocks on mobile. It had forced a horizontal scroll bar to appear. I added wrapping for now. I also tweaked the print CSS styles.
Excerpt from the CSS Breakdown, where I explain the changes:
Code blocks
To avoid horizontal scroll for the whole page, especially on mobile, I need to either wrap the code blocks or give them a horizontal overflow. As for what I prefer personally, I think it actually depends on the language. I feel disoriented reading languages with meaningful whitespace and line breaks that have been wrapped like Python or shell commands. Wrapping HTML, for example, feels much less bad.
I am worried about making people scroll, though. It can be difficult to track lines. On some devices, it's more of a pain than others. I think that until I learn more, it's less risky to just wrap.
pre { white-space: pre-wrap; }
I chose to put this property on the pre tag instead of the enclosed code tag, because I may someday want to preformat something that isn't code (like a TUI output, or something). That may or not play nicely with wrapping. It might make sense to have a no-wrap class for exceptions later. But at least this way I won't accidentally have four-way scrolling on mobile in the mean time!
There is a danger that the word wrap from white-space may still fail to wrap. It doesn't address very long single words. As a failsafe, I added horizontal scrolling for overflow (for screens only, because my testing on Firefox showed that it can cut off text when printing.)
@media screen { pre { overflow-x: auto; } }
There is still more I want to do with code blocks. I want a subtle way to more clearly delineate them from the surrounding text. I want to explore my options for full-bleed code blocks to avoid unneeded wrapping on wider screens. (If I wanted to do it without hacks, I think I might have to restructure my current CSS?) But this is a good start.
Navigation when printing
I have hidden navigation in the bottom footer when printing because it isn't useful information when reading on a page. Examples of this right now are "back" and "first previous inxed next latest".
@media print { footer nav { display: none; } }
I've kept header navigation visible because "Quinten's Blog Home" is useful: it tells you which website you're on. Eventaully, the header navigation might become a breadcrumb navigation. This could also be useful to understand the context of the page.
Accordion Task
Removing panics
Over the week I cleaned up the worst of the panicable error handling. My most recent commit explains:
Reduce panic locations in code
Still some remaining. Tried to move them up to app.rs so that the modules behave well, at least. Some remaining ones are unclear how to handle and suggest to me that I need better models of those areas. It also still panics on DST issues, which I will not be prioritizing.
I think I'm okay with there being some remaining panics IF I can mitigate data loss in the case of a panic that I had thought would never happen. I'm not sure how to do that yet. It might be more of a priority to work on usability than robustness right now, considering the nature of the tool.
Building Leptos on NixOS
To build Leptos on my home server so that I can build it from my phone, I had to figure out how to add the wasm32-unknown-unknown
build target to my rust tools. This isn't as straightforward as installing the normal Rust compiler on NixOS, and I had to figure out how to do it.
The blog post Nix shell with rustup was the resource that ended up helping me. I still can't wrap my head around Nix Flakes (very popular but not yet standard; I don't want to give up a simple config file approach; its code examples look more complicated), so I really appreciated that this post gave an example for using rust-overlay for a newbie like me who couldn't understand the README's example. It uses nix-shell
, which is a way that Nix can control the packages available for a project so that you can reproduce things more easily across machines.
I had to add a single line to that post's "classic" example in order to run the server-side-rendering server Leptos recommends for development: Trunk.
let
rust-overlay = builtins.fetchTarball "https://github.com/oxalica/rust-overlay/archive/master.tar.gz";
pkgs = import <nixpkgs> {
overlays = [(import rust-overlay)];
};
toolchain = pkgs.rust-bin.fromRustupToolchainFile ./toolchain.toml;
in
pkgs.mkShell {
packages = [
toolchain
+ pkgs.trunk
];
}
# shell.nix
This, along with a toolchain.toml
file containing:
[toolchain]
channel = "nightly"
targets = ["wasm32-unknown-unknown"]
was all I needed to get building! (I think I'll try using stable, though. The nix-shell
command checks for updates every time I start a new session, which means that I have to do a fresh rebuild of both my project and Trunk. It's possible that nix develop
, the Nix Flakes replacement for nix-shell
, might help with that, too.)
Since I am developing headless, I need to be able to access the development server from my phone's web browser. I can't just use localhost
like I'll typically do when developing from my laptop. Luckily, I already know how to do this from setting up port forwarding with Caddy in my NixOS config. This way, I can access the page from the url dev.[server name].lan
from any computer on my LAN! I need a short config file to set the port and adress in Trunk.toml
:
[serve]
addresses = ["[my server's IP according to the LAN]"]
port = [an unused port, that I give to Caddy]
Leptos WASM fallback message
Leptos can be used either for server-side rendering (SSR) or client-side rendering (CSR). I have decided to start with CSR because it means that it would be very cheap and easy to share the web version of Accordion Task, especially for users who are not familiar with server technology (which is most of the people in the world).
A major down side of using Leptos for CSR is that Leptos uses WASM, so it will simply not work when WASM is unavailable. In the CSR section of the Leptos book, it says that when using CSR with Leptos:
...what’s served is an HTML page with
- the URL of your Leptos app, which has been compiled to WebAssembly (WASM)
- the URL of the JavaScript used to initialize this WASM blob
- an empty
<body>
elementWhen the JS and WASM have loaded, Leptos will render your app into the
<body>
. This means that nothing appears on the screen until JS/WASM have loaded and run.
I can accept that, when writing a program that uses a specific technology at its core, the program will not be availabe without that technology. That is to be expected. What I can not accept is to not have a helpful little message explaining why the program is unavailable, and maybe some suggestions on where to look when troubleshooting. This bare-minimum of graceful degradation is important to me because I don't want to feel embarassed when a user encounters such clumsiness, or have someone assume the program is broken and give up if a simple toggle could fix it.
Fortunately, I was able to come up with a way to add a fallback message. This centers around the fact that Leptos doesn't actually render an empty <body>
element, which it replaces with the Leptos app using WASM (and JS to bootload the WASM). It renders whatever HTML is in an index.html
file in the project root, and then appends the contents of the webapp to the end of the body.
First, I put a message to display when JS is disabled. We can use the handy <noscript>
tag for this:
<body>
<noscript>
You don't have JavaScript enabled! You need JavaScript and WebAssembly enabled to run this webapp.
</noscript>
</body>
There's no tag like this for WASM, so we need to display the message and then use WASM to hide it once it's loaded:
<body>
...
<p id=nowasm>
This webapp needs WebAssembly to run. It hasn't loaded yet. Either the WebAssembly is still loading, it failed to load, or WebAssembly is not enabled in your browser.
</p>
</body>
To hide it, we need to enable some DOM manipulation features using the WASM library web-sys and its features Document and Element, and call this function at the top of our main function:
fn remove_nowasm_message() {
let window = web_sys::window()
.expect("global window does not exist");
let document = window.document()
.expect("expecting a document on window");
if let Some(message) = document.get_element_by_id("nowasm") {
message.set_attribute("hidden", "true");
}
}
This will make the message display until the WASM runs, and then disappear. However there is an unpleasant flash of the fallback message. This is why it would be nice to have a <nowasm>
tag!
We can get around this by running a timer before displaying the fallback. Add hidden=true
as a default for the fallback:
<body>
...
<p id=nowasm hidden=true>
</body>
Then add a script to unhide it after one second:
<head><script>
setTimeout(() => {
let nowasm = document.getElementById("nowasm");
nowasm.removeAttribube("hidden");
}, "1000");
</head></script>
in the likely event that the WASM function to hide the fallback runs before the one-second timer runs out, the fallback will still appear (above your webapp). To prevent this, we can just change the ID of the element to yeswasm
after hiding it!
fn remove_nowasm_message() {
//...
if let Some(message) = document.get_element_by_id("nowasm") {
message.set_attribute("hidden", "true");
message.set_attribute("id", "yeswasm");
}
}
The JS will still run, but won't be able to find any element with an ID of nowasm
.
This is almost complete! The only thing I see left is to ensure it doesn't break for screen readers. I don't have experience with the details of morphing pages because I haven't done much DOM manipulation yet. I suspect the answer will be related to ARIA live regions. Maybe it should have an alert role? I need to refresh my memory on how screen readers interact with the hidden
attribute, too.
I'll share the source code in full as a litte git repo once I'm confident the accessibility is sound.
Next:
I'll be working on getting more comfortable with Leptos by remaking a little calculator I made for work in vanilla JavaScript. I'm curious how the experience will compare.
Icecube
Icecube is a spinoff project of Pyx Rusterizer so that we can have a low-res, low-color-depth UI to play with our 3D rendering parameters in. John and I managed to add text wrapping to our render pipeline. It works! The video How Clay's UI Layout Algorithm Works by Nic Barker has been extremely helpful. It's a delightful, well-animated explainer and I'd recommend it to anyone who's into that sort of thing. I'm really enjoying the design challenge of turning a C library into (hopefully) ideomatic Rust.
Self Hosting
I set up a separate WiFi for IOT devices this video on configuring it with OpenWrt's GUI was the gentle reassurance I needed, and a helpful exposure to more networking concepts I'm gradually absorbing. With every device now on my router's WiFi, I was able to put my ISP's gateway thingie into plain-old-modem mode. It didn't magically make Wireguard work, but it's one less variable for when I get around to more troubleshooting.
Discoveries
- I finished watching the first episode of a PCB lecture series. It was really exciting to watch because I was in charge of ordering (already-designed) PCB assemblies at a prior job, and it's cool to fill in the gaps in my learning from that experience. I found out about this series from a podcast episode: Small Scale Electronics Manufacturing With Colin O'Flynn.
- I enjoyed the most recent episode of the Rust in Production podcast: 1Password with Andrew Burkhart. I thought it was fun to hear them discuss managing many different build targets and platform-native apps with a core library shared between all of them.
- As I was looking up the current state of the Ladybird browser project, I encountered an online book they recommend to contributors: Web Browser Engineering by Pavel Panchekha & Chris Harrelson. It walks you throuhg building a browser from scratch in Python with a usable (if incomplete) browser at each step of the way! I sure have enough on my plate right now, but it seems like a really fun little project.