//! The Export panel's export in the background (W10-13): the conversion //! runs on a thread while the UI keeps drawing, with a progress window, //! Cancel and "Show in folder". Native only: on the web the export stays //! a synchronous download path. Commands and MCP stay synchronous. use crate::App; use serde_json::Value; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::{channel, Receiver, TryRecvError}; use std::sync::Arc; /// An export on its thread. pub struct Running { rx: Receiver>, cancel: Arc, path: String, started: std::time::Instant, /// What to do once the file is written: the `file.export` parameters /// to keep up to date on save, or switching that off. after: After, } /// The export-on-save change that follows a finished export. pub enum After { Nothing, Keep(Value), Off, } /// Start `p` (`file.export`'s parameters) in the background. An error /// from the checks (bad options, input file) comes back at once. pub fn start(app: &mut App, p: &Value, after: After) -> Result<(), String> { if app.export_bg.is_some() { return Err("An export is already running".into()); } let job = rustfig_engine::export_job(&app.engine, p).map_err(|e| e.message)?; let path = job.path().to_string(); let cancel = Arc::new(AtomicBool::new(false)); let flag = cancel.clone(); let (tx, rx) = channel(); std::thread::Builder::new() .name("export".into()) .spawn(move || { // A panic in a driver becomes an error message, not a crash. let r = match rustfig_engine::guard::catch(|| job.run(&flag).map_err(|e| e.message)) { Ok(r) => r, Err(m) => Err(format!("internal error (please report): {m}")), }; let _ = tx.send(r); // the UI may be gone (cancelled) }) .map_err(|e| format!("cannot start the export: {e}"))?; app.export_bg = Some(Running { rx, cancel, path, started: std::time::Instant::now(), after, }); Ok(()) } /// Cancel the running export: its result is discarded and no file is /// written once the conversion in progress ends (fig2dev's drivers have /// no cancel hook inside one conversion, so that step runs to its end). pub fn cancel(app: &mut App) { if let Some(r) = app.export_bg.take() { r.cancel.store(true, Ordering::Relaxed); app.info("Export cancelled"); } } /// Collect a finished export, and show the progress window. pub fn tick(app: &mut App, ctx: &egui::Context) { let Some(r) = &app.export_bg else { show_done(app, ctx); return; }; match r.rx.try_recv() { Ok(result) => { let Some(r) = app.export_bg.take() else { return }; match result { Ok(v) => finished(app, r.after, &v), Err(m) => app.error(m), } } Err(TryRecvError::Disconnected) => { app.export_bg = None; app.error("The export stopped unexpectedly"); } Err(TryRecvError::Empty) => { ctx.request_repaint_after(std::time::Duration::from_millis(50)); let secs = r.started.elapsed().as_secs(); let name = r.path.clone(); let mut cancel_clicked = false; egui::Window::new("Exporting") .collapsible(false) .resizable(false) .anchor(egui::Align2::CENTER_CENTER, egui::vec2(0.0, 0.0)) .show(ctx, |ui| { crate::dialog_body(ui, |ui| { ui.horizontal(|ui| { ui.spinner(); ui.label(format!("Writing {name} ({secs} s)")); }); if ui.button("Cancel").clicked() { cancel_clicked = true; } }) }); if cancel_clicked { cancel(app); } } } } fn finished(app: &mut App, after: After, v: &Value) { let paths: Vec = v["paths"] .as_array() .into_iter() .flatten() .filter_map(|p| p.as_str().map(str::to_string)) .collect(); app.info(format!("Exported {}", paths.join(" and "))); match after { After::Nothing => {} After::Keep(p) => { app.run("file.export_on_save", p); } After::Off => { if app.engine.doc().export_on_save.is_some() { app.run("file.export_on_save", serde_json::json!({ "off": true })); } } } app.dialogs.export.open = false; app.last_export = paths.first().cloned(); } /// A small notice after a background export, with "Show in folder". fn show_done(app: &mut App, ctx: &egui::Context) { let Some(path) = app.last_export.clone() else { return }; let mut close = false; let mut show = false; egui::Window::new("Export finished") .collapsible(false) .resizable(false) .anchor(egui::Align2::RIGHT_BOTTOM, egui::vec2(-12.0, -12.0)) .show(ctx, |ui| { crate::dialog_body(ui, |ui| { ui.label(format!("Exported {path}")); ui.horizontal(|ui| { if ui.button("Show in folder").clicked() { show = true; } if ui.button("Close").clicked() { close = true; } }); }) }); if show { app.run("app.show_export_folder", serde_json::json!({})); } if close { app.last_export = None; } } /// Open the folder holding `path` with the file selected where the /// system can (macOS `open -R`, Windows `explorer /select,`, elsewhere /// xdg-open on the folder). pub fn show_in_folder(path: &str) -> Result<(), String> { use std::process::Command; let p = std::path::Path::new(path); let dir = p .parent() .filter(|d| !d.as_os_str().is_empty()) .unwrap_or(std::path::Path::new(".")); let mut cmd; if cfg!(target_os = "macos") { cmd = Command::new("open"); cmd.arg("-R").arg(p); } else if cfg!(target_os = "windows") { cmd = Command::new("explorer"); cmd.arg(format!("/select,{path}")); } else { cmd = Command::new("xdg-open"); cmd.arg(dir); } // The child is the file manager; it is not waited for. cmd.spawn() .map(|_| ()) .map_err(|e| format!("cannot show the folder: {e}")) } .