diff --git a/apps/lightcraft-web/src/lib.rs b/apps/lightcraft-web/src/lib.rs index 3b19987f..3cbb27b3 100644 --- a/apps/lightcraft-web/src/lib.rs +++ b/apps/lightcraft-web/src/lib.rs @@ -14,6 +14,7 @@ //! console (see [`bench`]); `?store=idb` forces the IndexedDB backend, `?store=memory` disables //! persistence and `?workers=0` renders on the main thread. #![forbid(unsafe_code)] +#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] pub mod bench; pub mod files; diff --git a/apps/lightcraft-web/src/web.rs b/apps/lightcraft-web/src/web.rs index 63308367..0d50ebe8 100644 --- a/apps/lightcraft-web/src/web.rs +++ b/apps/lightcraft-web/src/web.rs @@ -29,17 +29,18 @@ const LIBRARY_DIR: &str = "library"; /// How often view state, UI prefs and the thumbnail index are saved (ms). const SAVE_EVERY_MS: f64 = 1000.0; -fn window() -> web_sys::Window { - web_sys::window().expect("no window") +/// The page's window (`None` only outside a browser page, e.g. in a worker). +fn window() -> Option { + web_sys::window() } /// Milliseconds since navigation start. fn perf_now() -> f64 { - window().performance().map(|p| p.now()).unwrap_or(0.0) + window().and_then(|w| w.performance()).map(|p| p.now()).unwrap_or(0.0) } fn set_status(text: &str) { - if let Some(el) = window().document().and_then(|d| d.get_element_by_id("lightcraft_status")) { + if let Some(el) = window().and_then(|w| w.document()).and_then(|d| d.get_element_by_id("lightcraft_status")) { el.set_text_content(Some(text)); } } @@ -59,7 +60,7 @@ struct Options { impl Options { fn from_url() -> Options { - let q = window().location().search().unwrap_or_default(); + let q = window().and_then(|w| w.location().search().ok()).unwrap_or_default(); let params: Vec<(String, String)> = q .trim_start_matches('?') .split('&') @@ -102,7 +103,7 @@ async fn read_file(originals: Originals, backend: Option, file: web_sys /// Show the browser's open dialog; picked files are imported asynchronously. fn open_picker(originals: Originals, backend: Option, ctx: egui::Context) { - let Some(doc) = window().document() else { return }; + let Some(doc) = window().and_then(|w| w.document()) else { return }; let Ok(input) = doc.create_element("input").map(|e| e.unchecked_into::()) else { return }; input.set_type("file"); input.set_multiple(true); @@ -131,7 +132,7 @@ fn download(path: &str, bytes: &[u8]) -> Result<(), String> { opts.set_type(crate::store::mime_for(name)); let blob = web_sys::Blob::new_with_u8_array_sequence_and_options(&arr, &opts).map_err(e)?; let url = web_sys::Url::create_object_url_with_blob(&blob).map_err(e)?; - let doc = window().document().ok_or("no document")?; + let doc = window().and_then(|w| w.document()).ok_or("no document")?; let a: web_sys::HtmlAnchorElement = doc.create_element("a").map_err(e)?.unchecked_into(); a.set_href(&url); a.set_download(name); @@ -140,7 +141,9 @@ fn download(path: &str, bytes: &[u8]) -> Result<(), String> { let revoke = Closure::once_into_js(move || { let _ = web_sys::Url::revoke_object_url(&url); }); - let _ = window().set_timeout_with_callback_and_timeout_and_arguments_0(revoke.unchecked_ref(), 5000); + if let Some(w) = window() { + let _ = w.set_timeout_with_callback_and_timeout_and_arguments_0(revoke.unchecked_ref(), 5000); + } log::info!("exported {name} ({} bytes)", bytes.len()); Ok(()) } @@ -315,7 +318,7 @@ impl WebApp { } app.ui = app.ui.sanitized(); let n = opts.workers.unwrap_or_else(|| { - let cores = window().navigator().hardware_concurrency() as usize; + let cores = window().map_or(1, |w| w.navigator().hardware_concurrency() as usize); cores.saturating_sub(1).clamp(1, 4) }); let workers = @@ -516,8 +519,10 @@ pub fn start() { log::info!("lightcraft: wasm instantiated at {:.0} ms", perf_now()); let opts = Options::from_url(); wasm_bindgen_futures::spawn_local(async move { - let Some(canvas) = - window().document().and_then(|d| d.get_element_by_id("lightcraft_canvas")).and_then(|e| e.dyn_into::().ok()) + let Some(canvas) = window() + .and_then(|w| w.document()) + .and_then(|d| d.get_element_by_id("lightcraft_canvas")) + .and_then(|e| e.dyn_into::().ok()) else { log::error!("missing "); return; @@ -527,7 +532,7 @@ pub fn start() { let r = runner.start(canvas, eframe::WebOptions::default(), Box::new(move |cc| Ok(Box::new(WebApp::new(cc, boot))))).await; match r { Ok(()) => { - if let Some(el) = window().document().and_then(|d| d.get_element_by_id("lightcraft_loading")) { + if let Some(el) = window().and_then(|w| w.document()).and_then(|d| d.get_element_by_id("lightcraft_loading")) { el.remove(); } } diff --git a/crates/ui-egui/src/import.rs b/crates/ui-egui/src/import.rs index e58854a6..717cb727 100644 --- a/crates/ui-egui/src/import.rs +++ b/crates/ui-egui/src/import.rs @@ -214,7 +214,7 @@ pub fn poll_scan(app: &mut LightcraftApp, ctx: &egui::Context) { return; } }; - let task = app.scan.take().expect("scan task"); + let Some(task) = app.scan.take() else { return }; if task.progress.cancel.load(Ordering::Relaxed) { return; } @@ -333,7 +333,7 @@ pub fn tick(app: &mut LightcraftApp, ctx: &egui::Context) { p["renameStart"] = json!(1 + task.imported); } let r = app.session.execute("library.import", &p); - let task = app.import.as_mut().expect("import task"); + let Some(task) = app.import.as_mut() else { return }; task.done += n; match r { Ok(v) => { @@ -354,7 +354,7 @@ pub fn tick(app: &mut LightcraftApp, ctx: &egui::Context) { if !task.queue.is_empty() { return; } - let task = app.import.take().expect("import task"); + let Some(task) = app.import.take() else { return }; let steps = app.session.undo.len().saturating_sub(task.undo0); let label = format!("Add {} Photo{}", task.imported, if task.imported == 1 { "" } else { "s" }); app.session.merge_undo(steps, &label); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index c785ac07..aa60a80e 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -4,6 +4,7 @@ //! (views, panels, zoom — see [`menus::UI_COMMANDS`]) and forwards everything else to the engine. //! The same entry point serves menus, shortcuts, buttons and the control channel ([`control`]). #![forbid(unsafe_code)] +#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] pub mod control; pub mod export_task; diff --git a/crates/ui-egui/src/merge.rs b/crates/ui-egui/src/merge.rs index f68292b2..5e153723 100644 --- a/crates/ui-egui/src/merge.rs +++ b/crates/ui-egui/src/merge.rs @@ -124,12 +124,16 @@ fn spawn(app: &LightcraftApp, options: &MergeDialog, ids: &[PhotoId], preview: b let (tx, rx) = channel(); let (j, p, c) = (job.clone(), progress.clone(), cancel.clone()); let work = move || { - let r = j.run(&|f, stage| { - if let Ok(mut g) = p.lock() { - *g = (f, stage.to_string()); - } - !c.load(Ordering::Relaxed) - }); + let run = || { + j.run(&|f, stage| { + if let Ok(mut g) = p.lock() { + *g = (f, stage.to_string()); + } + !c.load(Ordering::Relaxed) + }) + }; + // a panicking merge reports an error instead of leaving "Merging…" up forever + let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(run)).unwrap_or_else(|_| Err("the merge failed unexpectedly".into())); let _ = tx.send(r); }; #[cfg(not(target_arch = "wasm32"))] @@ -220,7 +224,7 @@ pub fn poll(app: &mut LightcraftApp, ctx: &egui::Context) { if let Some(t) = &app.merge.preview_task { match t.rx.try_recv() { Ok(Ok(out)) => { - let t = app.merge.preview_task.take().expect("task"); + let Some(t) = app.merge.preview_task.take() else { return }; if let Some(img) = out.preview { let color = Arc::new(egui::ColorImage::from_rgba_unmultiplied([img.width, img.height], &img.as_bytes())); let tex = ctx.load_texture("merge-preview", color.clone(), egui::TextureOptions::LINEAR); @@ -229,7 +233,7 @@ pub fn poll(app: &mut LightcraftApp, ctx: &egui::Context) { } } Ok(Err(e)) => { - let t = app.merge.preview_task.take().expect("task"); + let Some(t) = app.merge.preview_task.take() else { return }; if e != "cancelled" { app.merge.error = Some(e); app.merge.failed_options = Some(t.options); @@ -242,7 +246,7 @@ pub fn poll(app: &mut LightcraftApp, ctx: &egui::Context) { if let Some(t) = &app.merge.final_task { match t.rx.try_recv() { Ok(r) => { - let t = app.merge.final_task.take().expect("task"); + let Some(t) = app.merge.final_task.take() else { return }; let what = if t.options.command == "merge.panorama" { "Panorama" } else if t.options.command == "merge.hdrPanorama" {