Merge pull request #22 from storytold/pr/never-crash-ui

Never crash: UI and app shell have no expect/unreachable
This commit is contained in:
Brandon Thomas
2026-10-04 14:48:48 -04:00
committed by GitHub
20 changed files with 80 additions and 54 deletions
+2
View File
@@ -29,6 +29,8 @@
//! *separate child process* with a wall-clock timeout, so hangs, panics and aborts in any
//! dependency are observed and reported instead of taking the harness down.
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode, Stdio};
+2
View File
@@ -1,6 +1,8 @@
//! PrintCraft in the browser. Build: `cd apps/printcraft-web && trunk build --release`
//! (trunk generates the JS loader; no handwritten JS — plan/execution-plan.md §1).
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
#[cfg(target_arch = "wasm32")]
fn main() {
use eframe::wasm_bindgen::JsCast;
+2 -1
View File
@@ -21,7 +21,8 @@ fn main() {
.set("InternalName", "printcraft");
if let Err(e) = res.compile() {
if std::env::var_os("PRINTCRAFT_REQUIRE_WINRES").is_some() {
panic!("embedding Windows resources failed: {e}");
println!("cargo::error=embedding Windows resources failed: {e}");
return;
}
println!("cargo:warning=printcraft.exe built without icon/version resources: {e}");
}
+2
View File
@@ -11,6 +11,8 @@
//! loopback port and writes `{"port", "token", "pid"}` to `<file>` (owner-only permissions).
//! Agents then drive it with `printcraft-cli ui --control <file> <method> …`.
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
use printcraft_ui_egui::PrintCraftApp;
/// Freedesktop app id: the `.desktop` file name and the hicolor icon name.
+15 -10
View File
@@ -103,8 +103,9 @@ impl PrintCraftApp {
self.dialog = Some(Dialog::NumberPages);
}
link if printcraft_engine::links::for_command(link).is_some() => {
let url = printcraft_engine::links::for_command(link).expect("checked").url;
self.open_url(url);
if let Some(l) = printcraft_engine::links::for_command(link) {
self.open_url(l.url);
}
}
"bookmark.add" => self.bookmark_action(crate::panels::BmAction::New),
"edit.undo" => self.undo(),
@@ -132,7 +133,7 @@ impl PrintCraftApp {
}
"comment.list" => self.right = Some(RightPanel::Comments),
tool if crate::comments::CommentTool::from_command(tool).is_some() => {
let tool = crate::comments::CommentTool::from_command(tool).expect("checked");
let Some(tool) = crate::comments::CommentTool::from_command(tool) else { return false };
self.comment_prefs.group_tool[tool.group()] = tool;
self.quick_tool = crate::QuickTool::Comment(tool);
// Acrobat opens the Comments panel with the commenting tools.
@@ -140,8 +141,10 @@ impl PrintCraftApp {
self.right = Some(RightPanel::Comments);
}
// A text selection made before picking a markup tool is marked right away.
if let (Some(kind), Some(i)) = (tool.markup(), active) {
let info = &self.session.get(self.views[i].id).expect("active").info;
if let (Some(kind), Some(i)) = (tool.markup(), active)
&& let Some(doc) = self.session.get(self.views[i].id)
{
let info = &doc.info;
if let Some((page, quads)) = self.views[i].selection_quads(info) {
self.views[i].clear_selection();
let style = self.comment_prefs.style(tool);
@@ -281,8 +284,10 @@ impl PrintCraftApp {
self.left = crate::LeftPanel::Tool("redact");
self.left_open = true;
// A text selection made first is marked right away.
if let Some(i) = active {
let info = &self.session.get(self.views[i].id).expect("active").info;
if let Some(i) = active
&& let Some(doc) = self.session.get(self.views[i].id)
{
let info = &doc.info;
if let Some((page, quads)) = self.views[i].selection_quads(info) {
self.views[i].clear_selection();
let author = self.comment_prefs.author.clone();
@@ -409,7 +414,7 @@ impl PrintCraftApp {
}
}
field if crate::prepare::FieldTool::from_command(field).is_some() => {
let tool = crate::prepare::FieldTool::from_command(field).expect("checked");
let Some(tool) = crate::prepare::FieldTool::from_command(field) else { return false };
self.quick_tool = crate::QuickTool::Field(tool);
self.left = crate::LeftPanel::Tool("form");
self.left_open = true;
@@ -419,7 +424,7 @@ impl PrintCraftApp {
self.notify(format!("Click on the page to add a {}, or drag to set its size", tool.label().to_lowercase()));
}
fill if crate::fill_sign::FillTool::from_command(fill).is_some() => {
let tool = crate::fill_sign::FillTool::from_command(fill).expect("checked");
let Some(tool) = crate::fill_sign::FillTool::from_command(fill) else { return false };
self.quick_tool = crate::QuickTool::Fill(tool);
let initials = tool == crate::fill_sign::FillTool::Initials;
if (tool == crate::fill_sign::FillTool::Signature && self.signature.is_none()) || (initials && self.initials.is_none()) {
@@ -467,7 +472,7 @@ impl PrintCraftApp {
let mut specs: Vec<&CommandSpec> = COMMANDS.iter().filter(|c| c.shortcut.is_some()).collect();
specs.sort_by_key(|c| std::cmp::Reverse(c.shortcut.map(|s| s.modifier_count()).unwrap_or(0)));
for spec in specs {
let s = spec.shortcut.expect("filtered");
let Some(s) = spec.shortcut else { continue };
if typing && !spec.in_text {
continue;
}
+4 -3
View File
@@ -832,9 +832,10 @@ pub(crate) fn page_after_text(resp: &egui::Response, cx: &PageCx<'_>, view: &mut
if tool.markup().is_none() || !cx.allowed || !(resp.drag_stopped() || resp.double_clicked()) {
return;
}
if let Some((page, quads)) = view.selection_quads(cx.info).filter(|(p, _)| *p == cx.page) {
if let Some(kind) = tool.markup()
&& let Some((page, quads)) = view.selection_quads(cx.info).filter(|(p, _)| *p == cx.page)
{
view.clear_selection();
let kind = tool.markup().expect("checked");
view.pending_edit = Some(new_comment(cx, tool, Shape::TextMarkup { kind, quads }, String::new()));
let _ = page;
}
@@ -994,7 +995,7 @@ pub(crate) fn composer(ctx: &egui::Context, view: &mut DocView, info: &DocInfo,
let t = Tokens::get(ctx);
let mut post = false;
let mut cancel = false;
let c = view.comments.composer.as_mut().expect("checked");
let c = view.comments.composer.as_mut()?;
let title = match c.kind {
ComposerKind::Note => "Sticky note",
ComposerKind::TextBox => "Text box",
+4 -4
View File
@@ -280,15 +280,15 @@ fn card(
});
egui::Frame::NONE.inner_margin(egui::Margin { left: 34, right: 0, top: 4, bottom: 0 }).show(ui, |ui| {
ui.set_width(ui.available_width());
if editing {
let (_, _, text) = view.comments.editing.as_mut().expect("checked");
if editing && let Some((_, _, text)) = view.comments.editing.as_mut() {
let r = ui.add(egui::TextEdit::multiline(text).desired_rows(2).desired_width(f32::INFINITY).id_salt(("comment-edit", key)));
if !r.has_focus() && !ui.memory(|m| m.focused().is_some()) {
r.request_focus();
}
ui.horizontal(|ui| {
if ui.add(egui::Button::new(egui::RichText::new("Save").color(Color32::WHITE)).fill(t.accent).corner_radius(12)).clicked() {
let (page, index, text) = view.comments.editing.take().expect("checked");
if ui.add(egui::Button::new(egui::RichText::new("Save").color(Color32::WHITE)).fill(t.accent).corner_radius(12)).clicked()
&& let Some((page, index, text)) = view.comments.editing.take()
{
edit = Some(Edit::SetAnnotationContents { page, index, text });
}
if ui.button("Cancel").clicked() || ui.input(|i| i.key_pressed(egui::Key::Escape)) {
+1 -1
View File
@@ -273,7 +273,7 @@ pub(crate) fn editor(ctx: &egui::Context, view: &mut DocView, info: &DocInfo, ad
let zoom = xf.rect.width() / xf.pw.max(1.0);
let (mut commit, mut cancel) = (false, false);
egui::Area::new(egui::Id::new(("added-text", view.id.0))).order(egui::Order::Foreground).fixed_pos(r.min).show(ctx, |ui| {
let t = view.content.draft.as_mut().expect("checked");
let Some(t) = view.content.draft.as_mut() else { return };
let [cr, cg, cb] = t.style.color.map(|v| (v.clamp(0.0, 1.0) * 255.0) as u8);
let resp = ui.add(
egui::TextEdit::multiline(&mut t.text)
+5 -1
View File
@@ -419,7 +419,8 @@ fn widget(id: NodeId, n: &accesskit::Node, depth: usize, focused: bool) -> Value
"enabled": !n.is_disabled(),
"clickable": n.supports_action(Action::Click),
});
let obj = w.as_object_mut().expect("object");
let mut more = serde_json::Map::new();
let obj = &mut more;
if let Some(l) = n.label() {
obj.insert("label".into(), json!(l));
}
@@ -441,6 +442,9 @@ fn widget(id: NodeId, n: &accesskit::Node, depth: usize, focused: bool) -> Value
if focused {
obj.insert("focused".into(), json!(true));
}
if let Value::Object(m) = &mut w {
m.extend(more);
}
w
}
+1 -1
View File
@@ -84,7 +84,7 @@ impl PrintCraftApp {
fn open_created_bytes(&mut self, name: &str, created: Result<Arc<Vec<u8>>, String>) -> Result<(), String> {
let bytes = created?;
let id = self.session.open_new(name, bytes).map_err(|e| e.to_string())?;
let info = &self.session.get(id).expect("just opened").info;
let info = &self.session.get(id).ok_or("the new document could not be opened")?.info;
self.views.push(crate::DocView::new(id, info));
self.active = Some(self.views.len() - 1);
Ok(())
+11 -7
View File
@@ -32,7 +32,8 @@ impl PrintCraftApp {
let label = edit.label();
match self.session.apply(id, edit.clone()) {
Ok(()) => {
let info = &self.session.get(id).expect("exists").info;
let Some(doc) = self.session.get(id) else { return true };
let info = &doc.info;
let view = &mut self.views[i];
match comment_page(&edit) {
// Comment edits change one page: keep every other raster.
@@ -84,8 +85,9 @@ impl PrintCraftApp {
let result = if undo { self.session.undo(id) } else { self.session.redo(id) };
match result {
Ok(label) => {
let info = &self.session.get(id).expect("exists").info;
self.views[i].document_changed(info);
if let Some(doc) = self.session.get(id) {
self.views[i].document_changed(&doc.info);
}
self.views[i].comments.selected = None;
self.notify(format!("{} {label}", if undo { "Undid" } else { "Redid" }));
}
@@ -177,8 +179,9 @@ impl PrintCraftApp {
match self.session.mark_saved(id, bytes, Some(dest.clone())) {
Ok(()) => {
self.forget_recovery(id);
let info = &self.session.get(id).expect("exists").info;
self.views[index].document_changed(info);
if let Some(doc) = self.session.get(id) {
self.views[index].document_changed(&doc.info);
}
self.notify(format!("Saved {}", short_name(&dest)));
true
}
@@ -194,8 +197,9 @@ impl PrintCraftApp {
match download(&name, &bytes) {
Ok(()) => {
let _ = self.session.mark_saved(id, bytes, None);
let info = &self.session.get(id).expect("exists").info;
self.views[index].document_changed(info);
if let Some(doc) = self.session.get(id) {
self.views[index].document_changed(&doc.info);
}
self.notify(format!("Downloaded {name}"));
true
}
+2 -1
View File
@@ -155,7 +155,8 @@ fn run(
}
let mut ex = Exporter::from_source(src);
match kind {
ExportKind::AllImages => unreachable!("handled above"),
// Handled above.
ExportKind::AllImages => String::new(),
ExportKind::Image => {
for (k, p) in pages.iter().enumerate() {
set(k, None);
+2 -2
View File
@@ -350,8 +350,8 @@ impl PrintCraftApp {
pub(crate) fn open_created(&mut self, name: &str, bytes: Arc<Vec<u8>>, message: &str) {
match self.session.open_new(name, bytes) {
Ok(id) => {
let info = &self.session.get(id).expect("just opened").info;
self.views.push(crate::DocView::new(id, info));
let Some(doc) = self.session.get(id) else { return };
self.views.push(crate::DocView::new(id, &doc.info));
self.active = Some(self.views.len() - 1);
self.notify(message);
}
+2 -2
View File
@@ -252,7 +252,7 @@ pub(crate) fn page_input(
None => Some(FillAction::CreateInitials),
},
mark => {
let mark = mark.mark().expect("marks");
let mark = mark.mark()?;
let (w, h) = if mark == FillMark::Line { (36.0, 4.0) } else { (12.0, 12.0) };
let rect = [at[0] - w / 2.0, at[1] - h / 2.0, at[0] + w / 2.0, at[1] + h / 2.0];
Some(FillAction::Edit(Box::new(new(page, Shape::Mark { rect, mark }, String::new(), author))))
@@ -280,7 +280,7 @@ pub(crate) fn type_box(ctx: &egui::Context, view: &mut DocView, info: &DocInfo,
let mut commit = false;
let mut cancel = false;
egui::Area::new(egui::Id::new(("fill-text", view.id.0))).order(egui::Order::Foreground).fixed_pos(pos).show(ctx, |ui| {
let t = view.fill_text.as_mut().expect("checked");
let Some(t) = view.fill_text.as_mut() else { return };
let width = ((t.text.len().max(8) as f32) * TEXT_SIZE as f32 * 0.6 * zoom).clamp(60.0, 600.0);
let r = ui.add(
egui::TextEdit::singleline(&mut t.text)
+2 -2
View File
@@ -247,7 +247,7 @@ pub(crate) fn overlay(ctx: &egui::Context, view: &mut DocView, info: &DocInfo, f
let font = (size.unwrap_or(((w.rect[3] - w.rect[1]) as f32 * 0.6).clamp(6.0, 12.0)) * zoom).clamp(6.0, 64.0);
egui::Area::new(egui::Id::new(("form-editor", view.id.0))).order(egui::Order::Foreground).fixed_pos(rect.min).show(ctx, |ui| {
ui.set_min_size(rect.size());
let fx = view.forms.focus.as_mut().expect("checked");
let Some(fx) = view.forms.focus.as_mut() else { return };
let mut te = if multiline { egui::TextEdit::multiline(&mut fx.text) } else { egui::TextEdit::singleline(&mut fx.text) };
te = te
.desired_width(rect.width() - 6.0)
@@ -310,7 +310,7 @@ pub(crate) fn overlay(ctx: &egui::Context, view: &mut DocView, info: &DocInfo, f
ui.set_min_width(rect.width().max(160.0));
ui.label(egui::RichText::new(&f.name).small().color(t.text_faint));
egui::ScrollArea::vertical().max_height(260.0).show(ui, |ui| {
let fx = view.forms.focus.as_mut().expect("checked");
let Some(fx) = view.forms.focus.as_mut() else { return };
for (export, display) in &f.options {
let on = if multi { fx.picked.contains(export) } else { f.value.first() == Some(export) };
if multi {
+3 -1
View File
@@ -5,6 +5,8 @@
//! Everything here is presentation: documents, rendering and the tool catalogue live in
//! `printcraft-engine`.
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
mod a11y_ui;
mod actions_ui;
pub mod canvas;
@@ -600,7 +602,7 @@ impl PrintCraftApp {
Err(e) => return Err(e.to_string()),
};
self.password_prompt = None;
let doc = self.session.get(id).expect("just opened");
let doc = self.session.get(id).ok_or("the document could not be opened")?;
let pages = doc.info.pages.len();
// Acrobat opens straight to the Comments panel when a document has comments.
if self.right.is_none() {
+3 -2
View File
@@ -707,8 +707,9 @@ fn outline_item(ui: &mut egui::Ui, t: &Tokens, info: &DocInfo, item: &OutlineIte
if resp.double_clicked() && cx.editable {
*cx.action = Some(BmAction::StartRename(path.to_vec()));
}
if cx.editable {
let i = *path.last().expect("non-empty path");
if cx.editable
&& let Some(&i) = path.last()
{
let current = cx.current;
resp.context_menu(|ui| {
let mut pick = |ui: &mut egui::Ui, label: &str, enabled: bool, a: BmAction| {
+2 -1
View File
@@ -85,7 +85,8 @@ impl FieldTool {
FieldTool::Radio => match join.filter(|f| f.kind == FormFieldKind::Radio) {
Some(g) => {
let taken: Vec<&str> = g.widgets.iter().filter_map(|w| w.on_state.as_deref()).collect();
let export = (1..).map(|i| format!("Choice{i}")).find(|c| !taken.contains(&c.as_str())).expect("unbounded");
let export =
(1..=u64::MAX).map(|i| format!("Choice{i}")).find(|c| !taken.contains(&c.as_str())).unwrap_or_else(|| "Choice".into());
NewField::Radio { group: Some(g.name.clone()), export }
}
None => NewField::Radio { group: None, export: "Choice1".into() },
+5 -5
View File
@@ -188,12 +188,12 @@ impl PrintCraftApp {
self.redact_prefs.mark(p, vec![rect_quad([c[0] as f64, c[1] as f64, c[2] as f64, c[3] as f64])], &author)
})
.collect();
match edits.len() {
0 => {}
1 => {
self.apply_edit(edits.into_iter().next().expect("one"));
match <[Edit; 1]>::try_from(edits) {
Ok([one]) => {
self.apply_edit(one);
}
_ => {
Err(edits) if edits.is_empty() => {}
Err(edits) => {
self.apply_edit(Edit::Batch { label: "Mark pages for redaction".into(), edits });
}
}
+10 -10
View File
@@ -247,10 +247,10 @@ impl PrintCraftApp {
.ok_or("There is no folder to save the digital ID in.")?;
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
let stem: String = d.name.trim().chars().map(|c| if c.is_alphanumeric() || c == ' ' || c == '-' { c } else { '_' }).collect();
let path = (1..)
let path = (1..=u64::MAX)
.map(|i| dir.join(if i == 1 { format!("{stem}.p12") } else { format!("{stem} {i}.p12") }))
.find(|p| !p.exists())
.expect("a free name");
.ok_or("no free file name for the digital ID")?;
std::fs::write(&path, &p12).map_err(|e| e.to_string())?;
Ok(self.add_digital_id(&path.to_string_lossy(), &cert))
}
@@ -327,8 +327,8 @@ impl PrintCraftApp {
let Some((_, id)) = self.active_ids() else { return };
match self.session.open_revision(id, n) {
Ok(new) => {
let info = &self.session.get(new).expect("just opened").info;
self.views.push(DocView::new(new, info));
let Some(doc) = self.session.get(new) else { return };
self.views.push(DocView::new(new, &doc.info));
self.active = Some(self.views.len() - 1);
}
Err(e) => self.notify(format!("Couldn't open revision {n}: {e}")),
@@ -343,8 +343,8 @@ impl PrintCraftApp {
let name = format!("{} (signed version)", doc.name.trim_end_matches(".pdf"));
match self.session.open(format!("{name}.pdf"), None, std::sync::Arc::new(bytes), doc.password.clone().as_deref()) {
Ok(new) => {
let info = &self.session.get(new).expect("just opened").info;
self.views.push(DocView::new(new, info));
let Some(doc) = self.session.get(new) else { return };
self.views.push(DocView::new(new, &doc.info));
self.active = Some(self.views.len() - 1);
}
Err(e) => self.notify(format!("Couldn't open the signed version: {e}")),
@@ -381,7 +381,7 @@ fn choose(ui: &mut egui::Ui, app: &mut PrintCraftApp, t: &Tokens) -> bool {
ui.label("Choose the digital ID that you want to use for signing:");
ui.add_space(6.0);
let ids = app.digital_ids.clone();
let d = app.sign_draft.as_mut().expect("checked");
let Some(d) = app.sign_draft.as_mut() else { return true };
let mut close = false;
egui::ScrollArea::vertical().max_height(220.0).show(ui, |ui| {
for (i, e) in ids.iter().enumerate() {
@@ -438,7 +438,7 @@ fn choose(ui: &mut egui::Ui, app: &mut PrintCraftApp, t: &Tokens) -> bool {
fn configure(ui: &mut egui::Ui, app: &mut PrintCraftApp, t: &Tokens) -> bool {
title(ui, "Configure a Digital ID for Signing");
let d = app.sign_draft.as_mut().expect("checked");
let Some(d) = app.sign_draft.as_mut() else { return true };
let mut close = false;
let mut go = false;
ui.radio_value(&mut d.new_id.create, false, "Use a Digital ID from a file");
@@ -516,7 +516,7 @@ fn configure(ui: &mut egui::Ui, app: &mut PrintCraftApp, t: &Tokens) -> bool {
if go {
let create = app.sign_draft.as_ref().is_some_and(|d| d.new_id.create);
let result = if create { app.create_digital_id() } else { app.import_digital_id() };
let d = app.sign_draft.as_mut().expect("checked");
let Some(d) = app.sign_draft.as_mut() else { return true };
match result {
Ok(i) => {
d.selected = Some(i);
@@ -562,7 +562,7 @@ fn preview(ui: &mut egui::Ui, t: &Tokens, name: &str, d: &SignDraft) {
fn sign_as(ui: &mut egui::Ui, app: &mut PrintCraftApp, t: &Tokens) -> bool {
let ids = app.digital_ids.clone();
let d = app.sign_draft.as_mut().expect("checked");
let Some(d) = app.sign_draft.as_mut() else { return true };
let Some(entry) = d.selected.and_then(|i| ids.get(i)).cloned() else {
d.step = SignStep::Choose;
return false;