Compare commits

..

3 Commits

Author SHA1 Message Date
aba4556144 Create error type
Some checks failed
Push Workflows / rustfmt (push) Failing after 5s
Push Workflows / tailwind-build (push) Successful in 5s
Push Workflows / clippy (push) Failing after 15s
Push Workflows / test (push) Successful in 29s
Push Workflows / docs (push) Successful in 27s
Push Workflows / build (push) Failing after 36s
Push Workflows / nix-build (push) Successful in 4m59s
2026-06-21 12:22:33 -04:00
749b5e7864 Add rand
Enable wasm_js on getrandom
2026-06-20 23:16:15 -04:00
2b5f9d011f Use notifications category of Lucide icons 2026-06-20 23:03:36 -04:00
4 changed files with 329 additions and 11 deletions

77
Cargo.lock generated
View File

@@ -280,6 +280,17 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chacha20"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
"rand_core 0.10.1",
]
[[package]]
name = "charset"
version = "0.1.5"
@@ -490,6 +501,15 @@ dependencies = [
"libc",
]
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
@@ -1521,11 +1541,25 @@ dependencies = [
"cfg-if",
"js-sys",
"libc",
"r-efi",
"r-efi 5.3.0",
"wasip2",
"wasm-bindgen",
]
[[package]]
name = "getrandom"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"r-efi 6.0.0",
"rand_core 0.10.1",
"wasm-bindgen",
]
[[package]]
name = "gloo-net"
version = "0.6.0"
@@ -2059,7 +2093,9 @@ dependencies = [
"diesel_migrations",
"dioxus",
"dotenvy",
"getrandom 0.4.3",
"lucide-dioxus",
"rand 0.10.1",
"serde",
"thiserror 2.0.18",
"tracing",
@@ -2546,7 +2582,7 @@ dependencies = [
"bytes",
"getrandom 0.3.4",
"lru-slab",
"rand",
"rand 0.9.4",
"ring",
"rustc-hash 2.1.2",
"rustls",
@@ -2587,6 +2623,12 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
version = "0.9.4"
@@ -2594,7 +2636,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
dependencies = [
"rand_chacha",
"rand_core",
"rand_core 0.9.5",
]
[[package]]
name = "rand"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
dependencies = [
"chacha20",
"getrandom 0.4.3",
"rand_core 0.10.1",
]
[[package]]
@@ -2604,7 +2657,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core",
"rand_core 0.9.5",
]
[[package]]
@@ -2616,6 +2669,12 @@ dependencies = [
"getrandom 0.3.4",
]
[[package]]
name = "rand_core"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
[[package]]
name = "raw-window-handle"
version = "0.6.2"
@@ -2932,7 +2991,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"digest",
]
@@ -2943,7 +3002,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"digest",
]
@@ -3511,7 +3570,7 @@ dependencies = [
"http",
"httparse",
"log",
"rand",
"rand 0.9.4",
"sha1",
"thiserror 2.0.18",
"utf-8",
@@ -3528,7 +3587,7 @@ dependencies = [
"http",
"httparse",
"log",
"rand",
"rand 0.9.4",
"sha1",
"thiserror 2.0.18",
"utf-8",
@@ -3545,7 +3604,7 @@ dependencies = [
"http",
"httparse",
"log",
"rand",
"rand 0.9.4",
"sha1",
"thiserror 2.0.18",
]

View File

@@ -13,7 +13,8 @@ diesel = { version = "2.3.10", optional = true, features = [ "postgres" ] }
diesel_migrations = { version = "2.3.2", optional = true, features = [ "postgres" ] }
dioxus = { version = "0.7.9", features = ["router", "fullstack"] }
dotenvy = { version = "0.15.7", optional = true }
lucide-dioxus = "3.11.0"
lucide-dioxus = { version = "3.11.0", features = ["notifications"] }
rand = "0.10.1"
serde = { version = "1.0.228", features = ["derive"] }
thiserror = "2.0.18"
tracing = "0.1.44"
@@ -31,3 +32,9 @@ server = [
# Disabled until supported
# desktop = ["dioxus/desktop"]
# mobile = ["dioxus/mobile"]
# Enable wasm_js in getrandom when building for wasm32
# This is a workaround for rand not exposing a wasm_js target
# https://github.com/rust-random/rand/issues/1694#issuecomment-3846362044
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
getrandom = { version = "0.4.3", features = ["wasm_js"] }

252
src/util/error.rs Normal file
View File

@@ -0,0 +1,252 @@
use std::fmt;
use std::panic::Location;
use dioxus::prelude::*;
use serde::{Deserialize, Serialize};
/// A location in the source code
/// A thin wrapper over `std::panic::Location`, which isn't `Serialize` or `Deserialize`
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorLocation {
file: String,
line: u32,
}
impl ErrorLocation {
/// Creates a new `ErrorLocation` with the file and line number of the caller.
#[track_caller]
pub fn here() -> Self {
let location = Location::caller();
ErrorLocation {
file: location.file().to_string(),
line: location.line(),
}
}
/// Get a link to the source code based on the repository URL from Cargo and the Git commit
/// from the build script. Uses a format supported by GitHub, Gitea, and GitLab.
pub fn source_link(&self) -> String {
format!(
"{}/blob/{}/{}#L{}",
env!("CARGO_PKG_REPOSITORY"),
env!("GIT_REV"),
self.file,
self.line
)
}
}
impl fmt::Display for ErrorLocation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.file, self.line)
}
}
#[derive(Debug, Clone, Deserialize, Serialize, thiserror::Error)]
pub struct Error {
#[source]
/// The error type and data
source: ErrorType,
/// The location where the error was created
from: ErrorLocation,
/// Context added to the error, and location where it was added
context: Vec<(ErrorLocation, String)>,
}
impl Error {
/// Creates a new `Error` at this location with the given error type
#[track_caller]
pub fn new_here(source: ErrorType) -> Self {
Error::new(source, ErrorLocation::here())
}
/// Creates a new `Error` with the given location and error type
pub fn new(source: ErrorType, from: ErrorLocation) -> Self {
Error {
source,
from,
context: Vec::new(),
}
}
/// Adds a context message to the error
#[track_caller]
pub fn with_context(mut self, context: impl Into<String>) -> Self {
self.context.push((ErrorLocation::here(), context.into()));
self
}
/// Retrieve the "top" message of the error. Uses either the most recent context entry, or the
/// source message if no context has been added.
pub fn top_message(&self) -> String {
if let Some((_location, message)) = self.context.last() {
message.clone()
} else {
self.source.to_string()
}
}
/// Display this error as a modal dialog activated by a checkbox with the given id
pub fn as_modal(&self, id: String) -> Element {
rsx! {
input {
r#type: "checkbox",
class: "modal-toggle",
id: &id,
}
div {
class: "modal",
role: "dialog",
div {
class: "modal-box border border-error bg-soft-error max-w-200",
h2 {
class: "flex items-center gap-3 text-lg",
lucide_dioxus::CircleAlert {
class: "shrink-0",
}
{self.top_message()}
}
p {
class: "text-base-content/70 py-3",
"Details"
}
div {
class: "md:grid md:grid-cols-[fit-content(calc(var(--spacing)*30))_auto] md:gap-1 md:gap-x-2 mb-6",
for (location, message) in self.context.iter().rev().chain(std::iter::once(&(
self.from.clone(),
self.source.to_string(),
))) {
a {
class: "text-base-content/80 interact underline",
target: "_blank",
href: location.source_link(),
{location.to_string()}
}
p {
class: "ml-3 md:ml-0",
{message.to_string()}
}
}
}
a {
class: "text-base-content/50 interact underline",
target: "_blank",
href: format!("{}/issues/new", env!("CARGO_PKG_REPOSITORY")),
"Report an issue"
}
label {
class: "absolute right-1 top-1 interact hover:bg-base-100/70 p-1 rounded-full",
r#for: &id,
lucide_dioxus::X {
class: "size-7 md:size-5",
}
}
}
label {
class: "modal-backdrop cursor-pointer",
r#for: id,
"Close",
}
}
}
}
/// Convert this error to a toast message, which opens a modal when clicked
pub fn as_toast(&self) -> Element {
// Generate a random string to use as an id for the modal
// This allows multiple toast/modal to be present
let modal_id = {
use rand::RngExt;
let mut rng = rand::rng();
let random_str = (0..5)
.map(|_| rng.sample(rand::distr::Alphanumeric) as char)
.collect::<String>();
format!("err-modal-{random_str}")
};
rsx! {
{self.as_modal(modal_id.clone())}
div {
class: "toast",
label {
class: "alert alert-error alert-soft cursor-pointer",
role: "alert",
r#for: modal_id,
lucide_dioxus::CircleAlert {}
p {
class: "max-w-120 text-ellipsis line-clamp-3",
{self.top_message()}
}
}
}
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Write the error type and its context
writeln!(f, "Error: {}", self.top_message())?;
write!(f, "Context:")?;
for (location, message) in self.context.iter().rev().chain(std::iter::once(&(
self.from.clone(),
self.source.to_string(),
))) {
write!(f, "\n - {location}: {message}")?;
}
Ok(())
}
}
pub trait Contextualize<R> {
/// Add context to the `Result` if it is an `Err`.
#[track_caller]
fn err_context(self, context: impl Into<String>) -> R;
}
impl<T, E: Into<Error>> Contextualize<Result<T, Error>> for Result<T, E> {
#[track_caller]
fn err_context(self, context: impl Into<String>) -> Result<T, Error> {
self.map_err(|e| e.into().with_context(context))
}
}
impl<E: Into<Error>> Contextualize<Error> for E {
#[track_caller]
fn err_context(self, context: impl Into<String>) -> Error {
self.into().with_context(context)
}
}
#[derive(Debug, Clone, thiserror::Error, Deserialize, Serialize)]
pub enum ErrorType {
}
impl From<ErrorType> for Error {
#[track_caller]
fn from(err: ErrorType) -> Self {
Error::new_here(err)
}
}

View File

@@ -1 +1 @@
pub mod error;