diff --git a/.gitattributes b/.gitattributes
deleted file mode 100644
index 8524696a..00000000
--- a/.gitattributes
+++ /dev/null
@@ -1 +0,0 @@
-**/coi-serviceworker.js linguist-vendored
diff --git a/Cargo.lock b/Cargo.lock
index d1bcd43a..99ea426f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3032,7 +3032,6 @@ dependencies = [
"color-eyre",
"egui",
"egui_extras",
- "flume",
"futures-lite 2.1.0",
"git-version",
"image 0.24.7",
@@ -3048,11 +3047,13 @@ dependencies = [
"luminol-ui",
"luminol-web",
"once_cell",
+ "oneshot",
"parking_lot",
"poll-promise",
"rfd",
"steamworks",
"strum",
+ "tempfile",
"tokio",
"tracing",
"tracing-log 0.1.4",
@@ -3134,13 +3135,13 @@ dependencies = [
"egui-modal",
"egui-notify",
"egui_dock",
- "git-version",
"itertools",
"luminol-audio",
"luminol-config",
"luminol-data",
"luminol-filesystem",
"luminol-graphics",
+ "once_cell",
"poll-promise",
"rand",
"serde",
@@ -3318,7 +3319,6 @@ dependencies = [
"color-eyre",
"egui",
"futures-util",
- "git-version",
"itertools",
"luminol-audio",
"luminol-components",
@@ -3333,7 +3333,10 @@ dependencies = [
"poll-promise",
"qp-trie",
"reqwest",
+ "serde",
+ "strip-ansi-escapes",
"strum",
+ "target-triple",
"zip",
]
@@ -4759,9 +4762,9 @@ checksum = "216080ab382b992234dda86873c18d4c48358f5cfcb70fd693d7f6f2131b628b"
[[package]]
name = "reqwest"
-version = "0.11.22"
+version = "0.11.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b"
+checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41"
dependencies = [
"base64",
"bytes",
@@ -5402,6 +5405,15 @@ dependencies = [
"float-cmp",
]
+[[package]]
+name = "strip-ansi-escapes"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55ff8ef943b384c414f54aefa961dd2bd853add74ec75e7ac74cf91dba62bcfa"
+dependencies = [
+ "vte",
+]
+
[[package]]
name = "strsim"
version = "0.10.0"
@@ -5573,6 +5585,12 @@ version = "0.12.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a"
+[[package]]
+name = "target-triple"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea67965c3f2666e59325eec14b6f0e296d27044051e65fc2a7d77ed3a3eff82d"
+
[[package]]
name = "tempfile"
version = "3.8.1"
@@ -6235,6 +6253,26 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
+[[package]]
+name = "vte"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197"
+dependencies = [
+ "utf8parse",
+ "vte_generate_state_changes",
+]
+
+[[package]]
+name = "vte_generate_state_changes"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff"
+dependencies = [
+ "proc-macro2",
+ "quote",
+]
+
[[package]]
name = "vtparse"
version = "0.6.2"
diff --git a/Cargo.toml b/Cargo.toml
index 84991939..9730131a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -42,7 +42,7 @@ doc_markdown = "allow"
missing_panics_doc = "allow"
too_many_lines = "allow"
# you must provide a safety doc.
-missing_safety_doc = "forbid"
+missing_safety_doc = "warn"
[workspace.package]
version = "0.4.0"
@@ -125,11 +125,10 @@ qp-trie = "0.8.2"
itertools = "0.11.0"
rfd = "0.12.0"
+tempfile = "3.8.1"
rand = "0.8.5"
-git-version = "0.3.9"
-
luminol-audio = { version = "0.4.0", path = "crates/audio/" }
luminol-components = { version = "0.4.0", path = "crates/components/" }
luminol-config = { version = "0.4.0", path = "crates/config/" }
@@ -180,7 +179,7 @@ zstd = "0.13.0"
async-std.workspace = true
futures-lite.workspace = true
-git-version.workspace = true
+git-version = "0.3.9"
# Native
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
@@ -192,6 +191,7 @@ tokio = { version = "1.33", features = [
"rt-multi-thread",
"parking_lot",
] } # *sigh*
+tempfile.workspace = true
luminol-term.workspace = true
# Set poll promise features here based on the target
@@ -213,7 +213,7 @@ wasm-bindgen = "0.2.87"
wasm-bindgen-futures = "0.4"
js-sys = "0.3"
-flume.workspace = true
+oneshot.workspace = true
luminol-web = { version = "0.4.0", path = "crates/web/" }
diff --git a/assets/coi-serviceworker.js b/assets/coi-serviceworker.js
deleted file mode 100644
index af2caf1b..00000000
--- a/assets/coi-serviceworker.js
+++ /dev/null
@@ -1,118 +0,0 @@
-/*! coi-serviceworker v0.1.7 - Guido Zuidhof and contributors, licensed under MIT */
-let coepCredentialless = false;
-if (typeof window === 'undefined') {
- self.addEventListener("install", () => self.skipWaiting());
- self.addEventListener("activate", (event) => event.waitUntil(self.clients.claim()));
-
- self.addEventListener("message", (ev) => {
- if (!ev.data) {
- return;
- } else if (ev.data.type === "deregister") {
- self.registration
- .unregister()
- .then(() => {
- return self.clients.matchAll();
- })
- .then(clients => {
- clients.forEach((client) => client.navigate(client.url));
- });
- } else if (ev.data.type === "coepCredentialless") {
- coepCredentialless = ev.data.value;
- }
- });
-
- self.addEventListener("fetch", function (event) {
- const r = event.request;
- if (r.cache === "only-if-cached" && r.mode !== "same-origin") {
- return;
- }
-
- const request = (coepCredentialless && r.mode === "no-cors")
- ? new Request(r, {
- credentials: "omit",
- })
- : r;
- event.respondWith(
- fetch(request)
- .then((response) => {
- if (response.status === 0) {
- return response;
- }
-
- const newHeaders = new Headers(response.headers);
- newHeaders.set("Cross-Origin-Embedder-Policy",
- coepCredentialless ? "credentialless" : "require-corp"
- );
- if (!coepCredentialless) {
- newHeaders.set("Cross-Origin-Resource-Policy", "cross-origin");
- }
- newHeaders.set("Cross-Origin-Opener-Policy", "same-origin");
-
- return new Response(response.body, {
- status: response.status,
- statusText: response.statusText,
- headers: newHeaders,
- });
- })
- .catch((e) => console.error(e))
- );
- });
-
-} else {
- (() => {
- // You can customize the behavior of this script through a global `coi` variable.
- const coi = {
- shouldRegister: () => true,
- shouldDeregister: () => false,
- coepCredentialless: () => (window.chrome !== undefined || window.netscape !== undefined),
- doReload: () => window.location.reload(),
- quiet: false,
- ...window.coi
- };
-
- const n = navigator;
-
- if (n.serviceWorker && n.serviceWorker.controller) {
- n.serviceWorker.controller.postMessage({
- type: "coepCredentialless",
- value: coi.coepCredentialless(),
- });
-
- if (coi.shouldDeregister()) {
- n.serviceWorker.controller.postMessage({ type: "deregister" });
- }
- }
-
- // If we're already coi: do nothing. Perhaps it's due to this script doing its job, or COOP/COEP are
- // already set from the origin server. Also if the browser has no notion of crossOriginIsolated, just give up here.
- if (window.crossOriginIsolated !== false || !coi.shouldRegister()) return;
-
- if (!window.isSecureContext) {
- !coi.quiet && console.log("COOP/COEP Service Worker not registered, a secure context is required.");
- return;
- }
-
- // In some environments (e.g. Chrome incognito mode) this won't be available
- if (n.serviceWorker) {
- n.serviceWorker.register(window.document.currentScript.src).then(
- (registration) => {
- !coi.quiet && console.log("COOP/COEP Service Worker registered", registration.scope);
-
- registration.addEventListener("updatefound", () => {
- !coi.quiet && console.log("Reloading page to make use of updated COOP/COEP Service Worker.");
- coi.doReload();
- });
-
- // If the registration is active, but it's not controlling the page
- if (registration.active && !n.serviceWorker.controller) {
- !coi.quiet && console.log("Reloading page to make use of COOP/COEP Service Worker.");
- coi.doReload();
- }
- },
- (err) => {
- !coi.quiet && console.error("COOP/COEP Service Worker failed to register:", err);
- }
- );
- }
- })();
-}
diff --git a/assets/main.js b/assets/main.js
index 6663047b..c1c42e08 100644
--- a/assets/main.js
+++ b/assets/main.js
@@ -15,7 +15,16 @@
// You should have received a copy of the GNU General Public License
// along with Luminol. If not, see .
-import wasm_bindgen, { luminol_main_start } from './luminol.js';
+window.restartLuminol = async function() {
+ // We need to reload luminol.js every time by invalidating the cache,
+ // otherwise it'll just reload the same WebAssembly module every time
+ // instead of reinstantiating it
+ const invalidator = crypto.randomUUID();
-await wasm_bindgen();
-luminol_main_start();
+ const { default: wasm_bindgen, luminol_main_start } = await import(`./luminol.js?luminol-invalidator=${invalidator}`);
+
+ await wasm_bindgen();
+ luminol_main_start();
+};
+
+await window.restartLuminol();
diff --git a/assets/sw.js b/assets/sw.js
new file mode 100644
index 00000000..c9626394
--- /dev/null
+++ b/assets/sw.js
@@ -0,0 +1,201 @@
+//! This is a slightly modified version of coi-serviceworker, commit 7b1d2a092d0d2dd2b7270b6f12f13605de26f214
+//! https://github.com/gzuidhof/coi-serviceworker
+/*!
+ * MIT License
+ *
+ * Copyright (c) 2021 Guido Zuidhof
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+*/
+
+const CACHE_NAME = "luminol";
+
+let coepCredentialless = false;
+if (typeof window === 'undefined') {
+ self.addEventListener("install", () => self.skipWaiting());
+ self.addEventListener("activate", (event) => event.waitUntil(self.clients.claim()));
+
+ self.addEventListener("message", (ev) => {
+ if (!ev.data) {
+ return;
+ } else if (ev.data.type === "deregister") {
+ self.registration
+ .unregister()
+ .then(() => {
+ return self.clients.matchAll();
+ })
+ .then(clients => {
+ clients.forEach((client) => client.navigate(client.url));
+ });
+ } else if (ev.data.type === "coepCredentialless") {
+ coepCredentialless = ev.data.value;
+ }
+ });
+
+ self.addEventListener("fetch", function (event) {
+ const r = event.request;
+ if (r.cache === "only-if-cached" && r.mode !== "same-origin") {
+ return;
+ }
+
+ const request = (coepCredentialless && r.mode === "no-cors")
+ ? new Request(r, {
+ credentials: "omit",
+ })
+ : new URL(r.url).searchParams.has("luminol-invalidator")
+ ? (() => {
+ // Remove 'luminol-invalidator' from the request's query string if it exists in the query string
+ const url = new URL(r.url);
+ url.searchParams.delete("luminol-invalidator");
+ return new Request(url, r);
+ })()
+ : r;
+ event.respondWith(
+ self.caches
+ .match(request)
+ .then((cached) => cached || fetch(request)) // Respond with cached response if one exists for this request
+ .then((response) => {
+ if (response.status === 0) {
+ return new Response();
+ }
+
+ const newHeaders = new Headers(response.headers);
+ newHeaders.set("Cross-Origin-Embedder-Policy",
+ coepCredentialless ? "credentialless" : "require-corp"
+ );
+ if (!coepCredentialless) {
+ newHeaders.set("Cross-Origin-Resource-Policy", "cross-origin");
+ }
+ newHeaders.set("Cross-Origin-Opener-Policy", "same-origin");
+
+ const newResponse = new Response(response.body, {
+ status: response.status,
+ statusText: response.statusText,
+ headers: newHeaders,
+ });
+
+ // Auto-cache non-error, non-opaque responses for all same-origin requests
+ if (response.type === "error" || new URL(request.url).origin !== self.origin) {
+ return newResponse;
+ } else {
+ return self.caches
+ .open(CACHE_NAME)
+ .then((cache) => cache.put(request, newResponse.clone()))
+ .then(() => newResponse);
+ }
+ })
+ .catch((e) => console.error(e))
+ );
+ });
+
+} else {
+ (() => {
+ const reloadedBySelf = window.sessionStorage.getItem("coiReloadedBySelf");
+ window.sessionStorage.removeItem("coiReloadedBySelf");
+ const coepDegrading = (reloadedBySelf == "coepdegrade");
+
+ // You can customize the behavior of this script through a global `coi` variable.
+ const coi = {
+ shouldRegister: () => !reloadedBySelf,
+ shouldDeregister: () => false,
+ coepCredentialless: () => true,
+ coepDegrade: () => true,
+ doReload: () => window.location.reload(),
+ quiet: false,
+ ...window.coi
+ };
+
+ const n = navigator;
+ const controlling = n.serviceWorker && n.serviceWorker.controller;
+
+ // Record the failure if the page is served by serviceWorker.
+ if (controlling && !window.crossOriginIsolated) {
+ window.sessionStorage.setItem("coiCoepHasFailed", "true");
+ }
+ const coepHasFailed = window.sessionStorage.getItem("coiCoepHasFailed");
+
+ if (controlling) {
+ // Reload only on the first failure.
+ const reloadToDegrade = coi.coepDegrade() && !(
+ coepDegrading || window.crossOriginIsolated
+ );
+ n.serviceWorker.controller.postMessage({
+ type: "coepCredentialless",
+ value: (reloadToDegrade || coepHasFailed && coi.coepDegrade())
+ ? false
+ : coi.coepCredentialless(),
+ });
+ if (reloadToDegrade) {
+ !coi.quiet && console.log("Reloading page to degrade COEP.");
+ window.sessionStorage.setItem("coiReloadedBySelf", "coepdegrade");
+ coi.doReload("coepdegrade");
+ }
+
+ if (coi.shouldDeregister()) {
+ n.serviceWorker.controller.postMessage({ type: "deregister" });
+ }
+ }
+
+ // If we're already coi: do nothing. Perhaps it's due to this script doing its job, or COOP/COEP are
+ // already set from the origin server. Also if the browser has no notion of crossOriginIsolated, just give up here.
+ if (window.crossOriginIsolated !== false || !coi.shouldRegister()) {
+ // Reload once to set the COEP for this service worker as well
+ if (!window.sessionStorage.getItem("coiReloadedAfterSuccess")) {
+ !coi.quiet && console.log("Reloading page to set COEP for this service worker.");
+ window.sessionStorage.setItem("coiReloadedAfterSuccess", "true");
+ coi.doReload("coepaftersuccess");
+ }
+ return;
+ }
+ window.sessionStorage.removeItem("coiReloadedAfterSuccess");
+
+ if (!window.isSecureContext) {
+ !coi.quiet && console.log("COOP/COEP Service Worker not registered, a secure context is required.");
+ return;
+ }
+
+ // In some environments (e.g. Firefox private mode) this won't be available
+ if (!n.serviceWorker) {
+ !coi.quiet && console.error("COOP/COEP Service Worker not registered, perhaps due to private mode.");
+ return;
+ }
+
+ n.serviceWorker.register(window.document.currentScript.src).then(
+ (registration) => {
+ !coi.quiet && console.log("COOP/COEP Service Worker registered", registration.scope);
+
+ registration.addEventListener("updatefound", () => {
+ !coi.quiet && console.log("Reloading page to make use of updated COOP/COEP Service Worker.");
+ window.sessionStorage.setItem("coiReloadedBySelf", "updatefound");
+ coi.doReload();
+ });
+
+ // If the registration is active, but it's not controlling the page
+ if (registration.active && !n.serviceWorker.controller) {
+ !coi.quiet && console.log("Reloading page to make use of COOP/COEP Service Worker.");
+ window.sessionStorage.setItem("coiReloadedBySelf", "notcontrolling");
+ coi.doReload();
+ }
+ },
+ (err) => {
+ !coi.quiet && console.error("COOP/COEP Service Worker failed to register:", err);
+ }
+ );
+ })();
+}
diff --git a/crates/components/src/lib.rs b/crates/components/src/lib.rs
index 42903467..64fb53e6 100644
--- a/crates/components/src/lib.rs
+++ b/crates/components/src/lib.rs
@@ -21,6 +21,7 @@
// it with Steamworks API by Valve Corporation, containing parts covered by
// terms of the Steamworks API by Valve Corporation, the licensors of this
// Program grant you additional permission to convey the resulting work.
+#![cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))]
#![feature(is_sorted)]
/// Syntax highlighter
diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml
index cda0e472..8b5c8fa1 100644
--- a/crates/core/Cargo.toml
+++ b/crates/core/Cargo.toml
@@ -24,6 +24,7 @@ egui-notify = "0.12.0"
egui-modal = "0.3.2"
poll-promise.workspace = true
+once_cell.workspace = true
tracing.workspace = true
color-eyre.workspace = true
@@ -38,8 +39,6 @@ alox-48.workspace = true
rand.workspace = true
-git-version.workspace = true
-
luminol-audio.workspace = true
luminol-config.workspace = true
luminol-data.workspace = true
diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs
index ae6ff149..fa0553f6 100644
--- a/crates/core/src/lib.rs
+++ b/crates/core/src/lib.rs
@@ -45,6 +45,12 @@ pub mod project_manager;
pub use project_manager::spawn_future;
pub use project_manager::ProjectManager;
+static GIT_REVISION: once_cell::sync::OnceCell<&'static str> = once_cell::sync::OnceCell::new();
+
+pub fn set_git_revision(revision: &'static str) {
+ let _ = GIT_REVISION.set(revision);
+}
+
pub struct UpdateState<'res> {
pub ctx: &'res egui::Context,
@@ -72,6 +78,8 @@ pub struct UpdateState<'res> {
pub modified: ModifiedState,
pub project_manager: &'res mut ProjectManager,
+
+ pub git_revision: &'static str,
}
/// This stores whether or not there are unsaved changes in any file in the current project and is
@@ -148,6 +156,7 @@ impl<'res> UpdateState<'res> {
toolbar: self.toolbar,
modified: self.modified.clone(),
project_manager: self.project_manager,
+ git_revision: self.git_revision,
}
}
@@ -170,6 +179,7 @@ impl<'res> UpdateState<'res> {
toolbar: self.toolbar,
modified: self.modified.clone(),
project_manager: self.project_manager,
+ git_revision: self.git_revision,
}
}
diff --git a/crates/core/src/toasts.rs b/crates/core/src/toasts.rs
index 54387f0c..d5dc3903 100644
--- a/crates/core/src/toasts.rs
+++ b/crates/core/src/toasts.rs
@@ -67,7 +67,11 @@ impl Toasts {
#[doc(hidden)]
pub fn _e_add_version_section(error: color_eyre::Report) -> color_eyre::Report {
- error.section(format!("Luminol version: {}", git_version::git_version!()))
+ if let Some(git_revision) = crate::GIT_REVISION.get() {
+ error.section(format!("Luminol version: {git_revision}"))
+ } else {
+ error
+ }
}
#[doc(hidden)]
diff --git a/crates/core/src/window.rs b/crates/core/src/window.rs
index 584a18cd..324d0960 100644
--- a/crates/core/src/window.rs
+++ b/crates/core/src/window.rs
@@ -41,6 +41,16 @@ pub struct EditWindows {
type CleanFn = Box) -> bool>;
impl Windows {
+ pub fn new() -> Self {
+ Default::default()
+ }
+
+ pub fn new_with_windows(windows: Vec) -> Self {
+ Self {
+ windows: windows.into_iter().map(|w| Box::new(w) as Box<_>).collect(),
+ }
+ }
+
/// A function to add a window.
pub fn add_window(&mut self, window: impl Window + 'static) {
self.add_boxed_window(Box::new(window))
@@ -177,3 +187,9 @@ impl Window for Box {
}
}
*/
+
+impl From>> for Windows {
+ fn from(windows: Vec>) -> Self {
+ Self { windows }
+ }
+}
diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs
index 7e5fade9..93024519 100644
--- a/crates/eframe/src/web/events.rs
+++ b/crates/eframe/src/web/events.rs
@@ -750,6 +750,8 @@ pub(crate) fn install_canvas_events(state: &MainState) -> Result<(), JsValue> {
let mut options = web_sys::MutationObserverInit::new();
options.attributes(true);
observer.observe_with_options(&state.canvas, &options)?;
+ // We don't need to unregister this mutation observer on panic because it auto-deregisters
+ // when the target (the canvas) is removed from the DOM and garbage-collected
callback.forget();
}
diff --git a/crates/eframe/src/web/mod.rs b/crates/eframe/src/web/mod.rs
index 61e701a6..e64f4057 100644
--- a/crates/eframe/src/web/mod.rs
+++ b/crates/eframe/src/web/mod.rs
@@ -374,7 +374,17 @@ impl MainState {
// Add the event listener to the target
target.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?;
- closure.forget();
+ // Add a hook to unregister this event listener after panicking
+ EVENTS_TO_UNSUBSCRIBE.with_borrow_mut(|events| {
+ events.push(web_runner::EventToUnsubscribe::TargetEvent(
+ web_runner::TargetEvent {
+ target: target.clone(),
+ event_name: event_name.to_string(),
+ closure,
+ },
+ ));
+ });
+
Ok(())
}
}
@@ -433,3 +443,7 @@ enum WebRunnerOutput {
}
static PANIC_LOCK: once_cell::sync::OnceCell<()> = once_cell::sync::OnceCell::new();
+
+thread_local! {
+ static EVENTS_TO_UNSUBSCRIBE: std::cell::RefCell> = std::cell::RefCell::new(Vec::new());
+}
diff --git a/crates/eframe/src/web/panic_handler.rs b/crates/eframe/src/web/panic_handler.rs
index 9656ba28..cadeed2a 100644
--- a/crates/eframe/src/web/panic_handler.rs
+++ b/crates/eframe/src/web/panic_handler.rs
@@ -13,13 +13,18 @@ pub struct PanicHandler(Arc>);
impl PanicHandler {
/// Install a panic hook.
- pub fn install() -> Self {
+ pub fn install(panic_tx: Arc>>>) -> Self {
let handler = Self(Arc::new(Mutex::new(Default::default())));
let handler_clone = handler.clone();
let previous_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic_info| {
let _ = super::PANIC_LOCK.set(());
+ if let Some(mut panic_tx) = panic_tx.try_lock() {
+ if let Some(panic_tx) = panic_tx.take() {
+ let _ = panic_tx.send(());
+ }
+ }
let summary = PanicSummary::new(panic_info);
diff --git a/crates/eframe/src/web/storage.rs b/crates/eframe/src/web/storage.rs
index 09af42a2..e92bb673 100644
--- a/crates/eframe/src/web/storage.rs
+++ b/crates/eframe/src/web/storage.rs
@@ -34,7 +34,7 @@ pub(crate) async fn load_memory(_: &egui::Context, _: &super::WorkerChannels) {}
#[cfg(feature = "persistence")]
pub(crate) fn save_memory(ctx: &egui::Context, channels: &super::WorkerChannels) {
- match ctx.memory(|mem| ron::to_string(mem)) {
+ match ctx.memory(ron::to_string) {
Ok(ron) => {
let (oneshot_tx, oneshot_rx) = oneshot::channel();
channels.send(super::WebRunnerOutput::StorageSet(
diff --git a/crates/eframe/src/web/web_runner.rs b/crates/eframe/src/web/web_runner.rs
index df8c963f..187ab6a2 100644
--- a/crates/eframe/src/web/web_runner.rs
+++ b/crates/eframe/src/web/web_runner.rs
@@ -29,13 +29,13 @@ pub struct WebRunner {
impl WebRunner {
/// Will install a panic handler that will catch and log any panics
#[allow(clippy::new_without_default)]
- pub fn new() -> Self {
+ pub fn new(panic_tx: std::sync::Arc>>>) -> Self {
#[cfg(not(web_sys_unstable_apis))]
log::warn!(
"eframe compiled without RUSTFLAGS='--cfg=web_sys_unstable_apis'. Copying text won't work."
);
- let panic_handler = PanicHandler::install();
+ let panic_handler = PanicHandler::install(panic_tx);
Self {
panic_handler,
@@ -46,7 +46,25 @@ impl WebRunner {
/// Set up the event listeners on the main thread in order to do things like respond to
/// mouse events and resize the canvas to fill the screen.
- pub fn setup_main_thread_hooks(state: super::MainState) -> Result<(), JsValue> {
+ pub fn setup_main_thread_hooks(
+ state: super::MainState,
+ ) -> Result>>>, JsValue> {
+ let (panic_tx, panic_rx) = oneshot::channel();
+
+ wasm_bindgen_futures::spawn_local(async move {
+ let _ = panic_rx.await;
+ super::EVENTS_TO_UNSUBSCRIBE.with_borrow_mut(|events| {
+ for event in events.drain(..) {
+ if let Err(e) = event.unsubscribe() {
+ log::warn!(
+ "Failed to unsubscribe from event: {}",
+ super::string_from_js_value(&e),
+ );
+ }
+ }
+ });
+ });
+
{
events::install_canvas_events(&state)?;
events::install_document_events(&state)?;
@@ -93,7 +111,7 @@ impl WebRunner {
}
});
- Ok(())
+ Ok(std::sync::Arc::new(parking_lot::Mutex::new(Some(panic_tx))))
}
/// Create the application, install callbacks, and start running the app.
@@ -225,19 +243,19 @@ impl WebRunner {
// ----------------------------------------------------------------------------
-struct TargetEvent {
- target: web_sys::EventTarget,
- event_name: String,
- closure: Closure,
+pub(super) struct TargetEvent {
+ pub(super) target: web_sys::EventTarget,
+ pub(super) event_name: String,
+ pub(super) closure: Closure,
}
#[allow(unused)]
-struct IntervalHandle {
- handle: i32,
- closure: Closure,
+pub(super) struct IntervalHandle {
+ pub(super) handle: i32,
+ pub(super) closure: Closure,
}
-enum EventToUnsubscribe {
+pub(super) enum EventToUnsubscribe {
TargetEvent(TargetEvent),
#[allow(unused)]
diff --git a/crates/egui-wgpu/src/lib.rs b/crates/egui-wgpu/src/lib.rs
index da3e4763..30b5f5a9 100644
--- a/crates/egui-wgpu/src/lib.rs
+++ b/crates/egui-wgpu/src/lib.rs
@@ -19,7 +19,7 @@
#![allow(unsafe_code)]
// Luminol's wgpu resources are not Send or Sync on web.
// We are doing this here to reduce merge conflicts, since it's likely wgpu will fix this.
-#![allow(clippy::arc_with_non_send_sync)]
+#![cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))]
pub use wgpu;
diff --git a/crates/filesystem/Cargo.toml b/crates/filesystem/Cargo.toml
index 4188442e..ded3e385 100644
--- a/crates/filesystem/Cargo.toml
+++ b/crates/filesystem/Cargo.toml
@@ -54,7 +54,7 @@ slab.workspace = true
winreg = "0.51.0"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
-tempfile = "3.8.1"
+tempfile.workspace = true
async-fs = "2.1.0"
[target.'cfg(target_arch = "wasm32")'.dependencies]
diff --git a/crates/filesystem/src/project.rs b/crates/filesystem/src/project.rs
index e5872243..0bf03edd 100644
--- a/crates/filesystem/src/project.rs
+++ b/crates/filesystem/src/project.rs
@@ -458,21 +458,17 @@ impl FileSystem {
global_config: &mut luminol_config::global::Config,
) -> Result {
let entries = host.read_dir("")?;
- if entries
- .iter()
- .find(|e| {
- if let Some(extension) = e.path.extension() {
- e.metadata.is_file
- && (extension == "rxproj"
- || extension == "rvproj"
- || extension == "rvproj2"
- || extension == "lumproj")
- } else {
- false
- }
- })
- .is_none()
- {
+ if !entries.iter().any(|e| {
+ if let Some(extension) = e.path.extension() {
+ e.metadata.is_file
+ && (extension == "rxproj"
+ || extension == "rvproj"
+ || extension == "rvproj2"
+ || extension == "lumproj")
+ } else {
+ false
+ }
+ }) {
return Err(Error::InvalidProjectFolder.into());
};
diff --git a/crates/filesystem/src/web/mod.rs b/crates/filesystem/src/web/mod.rs
index 4ff3a4c8..c62c549f 100644
--- a/crates/filesystem/src/web/mod.rs
+++ b/crates/filesystem/src/web/mod.rs
@@ -146,7 +146,7 @@ impl FileSystem {
/// Returns whether or not the user's browser supports the JavaScript File System API.
pub fn filesystem_supported() -> bool {
- send_and_recv(|tx| FileSystemCommand::Supported(tx))
+ send_and_recv(FileSystemCommand::Supported)
}
/// Attempts to prompt the user to choose a directory from their local machine using the
@@ -159,7 +159,7 @@ impl FileSystem {
if !Self::filesystem_supported() {
return Err(Error::Wasm32FilesystemNotSupported).wrap_err(c);
}
- send_and_await(|tx| FileSystemCommand::DirPicker(tx))
+ send_and_await(FileSystemCommand::DirPicker)
.await
.map(|(key, name)| Self {
key,
@@ -314,7 +314,7 @@ impl File {
/// Creates a new empty temporary file with read-write permissions.
pub fn new() -> std::io::Result {
let c = "While creating a temporary file on a host filesystem";
- send_and_recv(|tx| FileSystemCommand::FileCreateTemp(tx))
+ send_and_recv(FileSystemCommand::FileCreateTemp)
.map(|(key, temp_file_name)| Self {
key,
path: None,
diff --git a/crates/filesystem/src/web/util.rs b/crates/filesystem/src/web/util.rs
index bd2af6c8..a1cd91fa 100644
--- a/crates/filesystem/src/web/util.rs
+++ b/crates/filesystem/src/web/util.rs
@@ -115,10 +115,10 @@ pub(super) async fn idb(
// Create store for our directory handles if it doesn't exist
db_req.set_on_upgrade_needed(Some(|e: &IdbVersionChangeEvent| {
- if e.db()
+ if !e
+ .db()
.object_store_names()
- .find(|n| n == "filesystem.dir_handles")
- .is_none()
+ .any(|n| n == "filesystem.dir_handles")
{
e.db().create_object_store("filesystem.dir_handles")?;
}
diff --git a/crates/graphics/src/lib.rs b/crates/graphics/src/lib.rs
index 1627759b..1d6d02cb 100644
--- a/crates/graphics/src/lib.rs
+++ b/crates/graphics/src/lib.rs
@@ -14,6 +14,7 @@
//
// You should have received a copy of the GNU General Public License
// along with Luminol. If not, see .
+#![cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))]
pub mod binding_helpers;
pub use binding_helpers::{BindGroupBuilder, BindGroupLayoutBuilder};
diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml
index 86975edb..b4da44a0 100644
--- a/crates/ui/Cargo.toml
+++ b/crates/ui/Cargo.toml
@@ -34,12 +34,16 @@ camino.workspace = true
strum.workspace = true
-git-version.workspace = true
+serde.workspace = true
+
+target-triple = "0.1.2"
+
+strip-ansi-escapes = "0.2.0"
poll-promise.workspace = true
async-std.workspace = true
futures-util = "0.3.30"
-reqwest = "0.11.22"
+reqwest = { version = "0.11.23", features = ["json"] }
zip = { version = "0.6.6", default-features = false, features = ["deflate"] }
diff --git a/crates/ui/src/windows/about.rs b/crates/ui/src/windows/about.rs
index d4ce6948..d4e8c888 100644
--- a/crates/ui/src/windows/about.rs
+++ b/crates/ui/src/windows/about.rs
@@ -54,7 +54,7 @@ impl luminol_core::Window for Window {
&mut self,
ctx: &egui::Context,
open: &mut bool,
- _update_state: &mut luminol_core::UpdateState<'_>,
+ update_state: &mut luminol_core::UpdateState<'_>,
) {
// Show the window. Name it "About Luminol"
egui::Window::new("About Luminol")
@@ -69,7 +69,7 @@ impl luminol_core::Window for Window {
ui.separator();
ui.label(format!("Luminol version {}", env!("CARGO_PKG_VERSION")));
- ui.label(format!("git-rev {}", git_version::git_version!()));
+ ui.label(format!("git-rev {}", update_state.git_revision));
ui.separator();
ui.label("Luminol is a FOSS version of the RPG Maker XP editor.");
diff --git a/crates/ui/src/windows/items.rs b/crates/ui/src/windows/items.rs
index 4ca63ab4..ff88c433 100644
--- a/crates/ui/src/windows/items.rs
+++ b/crates/ui/src/windows/items.rs
@@ -56,7 +56,7 @@ impl luminol_core::Window for Window {
}
fn id(&self) -> egui::Id {
- egui::Id::new("Item Editor")
+ egui::Id::new("item_editor")
}
fn requires_filesystem(&self) -> bool {
diff --git a/crates/ui/src/windows/mod.rs b/crates/ui/src/windows/mod.rs
index 5f103472..b619ddc7 100644
--- a/crates/ui/src/windows/mod.rs
+++ b/crates/ui/src/windows/mod.rs
@@ -45,6 +45,8 @@ pub mod map_picker;
pub mod misc;
/// New project window
pub mod new_project;
+/// The crash reporter.
+pub mod reporter;
/// The script editor
pub mod script_edit;
/// The sound test.
diff --git a/crates/ui/src/windows/reporter.rs b/crates/ui/src/windows/reporter.rs
new file mode 100644
index 00000000..bd6194d6
--- /dev/null
+++ b/crates/ui/src/windows/reporter.rs
@@ -0,0 +1,222 @@
+// Copyright (C) 2023 Lily Lyons
+//
+// This file is part of Luminol.
+//
+// Luminol is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Luminol is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Luminol. If not, see .
+//
+// Additional permission under GNU GPL version 3 section 7
+//
+// If you modify this Program, or any covered work, by linking or combining
+// it with Steamworks API by Valve Corporation, containing parts covered by
+// terms of the Steamworks API by Valve Corporation, the licensors of this
+// Program grant you additional permission to convey the resulting work.
+
+use luminol_components::UiExt;
+
+/// Crash reporter window.
+pub struct Window {
+ normalized_report: String,
+ json: ReportJson,
+ send_promise: Option>>,
+ first_render: bool,
+}
+
+#[derive(Debug, Clone, serde::Serialize)]
+struct ReportJson {
+ reporter_version: u32,
+ luminol_revision: String,
+ target: String,
+ wgpu_backend: String,
+ debug: bool,
+ report: String,
+}
+
+impl Window {
+ pub fn new(report: impl Into, git_revision: impl Into) -> Self {
+ let report: String = report.into();
+
+ Self {
+ normalized_report: strip_ansi_escapes::strip_str(&report),
+ json: ReportJson {
+ reporter_version: 1,
+ luminol_revision: git_revision.into(),
+ target: target_triple::target!().to_string(),
+ wgpu_backend: "empty".into(),
+ debug: cfg!(debug_assertions),
+ report,
+ },
+ send_promise: None,
+ first_render: true,
+ }
+ }
+}
+
+impl luminol_core::Window for Window {
+ fn name(&self) -> String {
+ "Crash Reporter".into()
+ }
+
+ fn id(&self) -> egui::Id {
+ egui::Id::new("reporter")
+ }
+
+ fn requires_filesystem(&self) -> bool {
+ false
+ }
+
+ fn show(
+ &mut self,
+ ctx: &egui::Context,
+ open: &mut bool,
+ update_state: &mut luminol_core::UpdateState<'_>,
+ ) {
+ if self.first_render {
+ self.json.wgpu_backend = update_state
+ .graphics
+ .render_state
+ .adapter
+ .get_info()
+ .backend
+ .to_str()
+ .to_string();
+ }
+
+ let mut should_close = false;
+
+ egui::Window::new(self.name())
+ .id(egui::Id::new("reporter"))
+ .default_width(500.)
+ .open(open)
+ .show(ctx, |ui| {
+ ui.label("Luminol has crashed!");
+ ui.label(
+ "Would you like to send the following crash report to the Luminol developers?",
+ );
+
+ ui.add_space(ui.spacing().indent);
+
+ ui.label(format!("Luminol version: {}", self.json.luminol_revision));
+ ui.label(format!("Target platform: {}", self.json.target));
+ ui.label(format!("Graphics backend: {}", self.json.wgpu_backend));
+ ui.label(format!(
+ "Build profile: {}",
+ if self.json.debug { "debug" } else { "release" }
+ ));
+
+ ui.group(|ui| {
+ ui.with_cross_justify(|ui| {
+ // Forget the scroll position from the last time the reporter opened
+ let scroll_area_id_source = "scroll_area";
+ if self.first_render {
+ self.first_render = false;
+ let scroll_area_id =
+ ui.make_persistent_id(egui::Id::new(scroll_area_id_source));
+ egui::scroll_area::State::default().store(ui.ctx(), scroll_area_id);
+ }
+
+ egui::ScrollArea::both()
+ .id_source(scroll_area_id_source)
+ .max_height(
+ ui.available_height()
+ - ui.spacing().interact_size.y.max(
+ ui.text_style_height(&egui::TextStyle::Button)
+ + 2. * ui.spacing().button_padding.y,
+ )
+ - ui.spacing().item_spacing.y,
+ )
+ .show(ui, |ui| {
+ ui.add(
+ egui::TextEdit::multiline(&mut self.normalized_report.as_str())
+ .layouter(&mut |ui, text, wrap_width| {
+ // Make the text monospace and non-wrapping
+ egui::WidgetText::from(text)
+ .color(
+ ui.visuals()
+ .override_text_color
+ .unwrap_or_else(|| {
+ ui.visuals()
+ .widgets
+ .noninteractive
+ .fg_stroke
+ .color
+ }),
+ )
+ .into_galley(
+ ui,
+ Some(false),
+ wrap_width,
+ egui::TextStyle::Monospace,
+ )
+ }),
+ );
+ });
+ });
+ });
+
+ ui.with_cross_justify_center(|ui| {
+ if self.send_promise.is_none() {
+ ui.columns(2, |columns| {
+ if columns[0].button("Don't send").clicked() {
+ should_close = true;
+ }
+
+ if columns[1].button("Send").clicked() {
+ let json = self.json.clone();
+ self.send_promise = Some(luminol_core::spawn_future(async move {
+ let client = reqwest::Client::new();
+ let response = client
+ .post("http://localhost:3246")
+ .json(&json)
+ .fetch_mode_no_cors()
+ .send()
+ .await
+ .map_err(|e| color_eyre::eyre::eyre!(e))?;
+ if response.status().is_success() {
+ Ok(())
+ } else {
+ Err(color_eyre::eyre::eyre!(format!(
+ "Request returned {}",
+ response.status()
+ )))
+ }
+ }));
+ }
+ });
+ } else {
+ ui.spinner();
+ }
+ });
+ });
+
+ if let Some(p) = self.send_promise.take() {
+ match p.try_take() {
+ Ok(Ok(())) => {
+ luminol_core::info!(update_state.toasts, "Crash report sent!");
+ should_close = true;
+ }
+ Ok(Err(e)) => {
+ luminol_core::error!(
+ update_state.toasts,
+ e.wrap_err("Error sending crash report")
+ );
+ }
+ Err(p) => self.send_promise = Some(p),
+ }
+ }
+
+ if should_close {
+ *open = false;
+ }
+ }
+}
diff --git a/hooks/trunk_enable_build_std.sh.cmd b/hooks/trunk_enable_build_std.sh.cmd
index 6c28a0a9..05d263db 100644
--- a/hooks/trunk_enable_build_std.sh.cmd
+++ b/hooks/trunk_enable_build_std.sh.cmd
@@ -1,3 +1,4 @@
@echo off
+setlocal
start /b %TRUNK_SOURCE_DIR%\hooks\trunk_enable_build_std_background.sh
diff --git a/hooks/trunk_enable_build_std_background.sh.cmd b/hooks/trunk_enable_build_std_background.sh.cmd
index d0f587cd..d5d27c80 100644
--- a/hooks/trunk_enable_build_std_background.sh.cmd
+++ b/hooks/trunk_enable_build_std_background.sh.cmd
@@ -1,4 +1,5 @@
@echo off
+setlocal
:: Wait until Trunk errors out or builds successfully, then restore the old Cargo config
:loop
diff --git a/hooks/trunk_enable_build_std_pre.sh b/hooks/trunk_enable_build_std_pre.sh
index 983b99bf..47df86dd 100755
--- a/hooks/trunk_enable_build_std_pre.sh
+++ b/hooks/trunk_enable_build_std_pre.sh
@@ -1,8 +1,14 @@
#!/bin/sh
set -e
-# Enable std support for multithreading
+git_version=$(git describe --always --dirty=-modified)
+
+# Enable std support for multithreading and set the LUMINOL_VERSION environment variable
[ ! -f $TRUNK_SOURCE_DIR/.cargo/config.toml.bak ] || mv $TRUNK_SOURCE_DIR/.cargo/config.toml.bak $TRUNK_SOURCE_DIR/.cargo/config.toml
cp $TRUNK_SOURCE_DIR/.cargo/config.toml $TRUNK_SOURCE_DIR/.cargo/config.toml.bak
+
+echo '[env]' >> $TRUNK_SOURCE_DIR/.cargo/config.toml
+echo "LUMINOL_VERSION = { value = \"$git_version\", force = true }" >> $TRUNK_SOURCE_DIR/.cargo/config.toml
+
echo '[unstable]' >> $TRUNK_SOURCE_DIR/.cargo/config.toml
echo 'build-std = ["std", "panic_abort"]' >> $TRUNK_SOURCE_DIR/.cargo/config.toml
diff --git a/hooks/trunk_enable_build_std_pre.sh.cmd b/hooks/trunk_enable_build_std_pre.sh.cmd
index 21354b1d..f609e79c 100644
--- a/hooks/trunk_enable_build_std_pre.sh.cmd
+++ b/hooks/trunk_enable_build_std_pre.sh.cmd
@@ -1,7 +1,14 @@
@echo off
+setlocal
-:: Enable std support for multithreading
+for /f "tokens=*" %%i in ('git describe --always --dirty=-modified') do set git_version=%%i
+
+:: Enable std support for multithreading and set the LUMINOL_VERSION environment variable
if exist %TRUNK_SOURCE_DIR%\.cargo\config.toml.bak move %TRUNK_SOURCE_DIR%\.cargo\config.toml.bak %TRUNK_SOURCE_DIR%\.cargo\config.toml
copy %TRUNK_SOURCE_DIR%\.cargo\config.toml %TRUNK_SOURCE_DIR%\.cargo\config.toml.bak
+
+echo [env] >> %TRUNK_SOURCE_DIR%\.cargo\config.toml
+echo LUMINOL_VERSION = { value = "%git_version%", force = true } >> %TRUNK_SOURCE_DIR%\.cargo\config.toml
+
echo [unstable] >> %TRUNK_SOURCE_DIR%\.cargo\config.toml
echo build-std = ["std", "panic_abort"] >> %TRUNK_SOURCE_DIR%\.cargo\config.toml
diff --git a/index.html b/index.html
index 9ad4d580..66f29cc5 100644
--- a/index.html
+++ b/index.html
@@ -20,7 +20,7 @@
-
+
@@ -122,13 +122,13 @@
-
+
-
+
diff --git a/src/app/mod.rs b/src/app/mod.rs
index a937f44b..a0f2256a 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -83,6 +83,7 @@ impl App {
#[must_use]
pub fn new(
cc: &luminol_eframe::CreationContext<'_>,
+ report: Option,
modified: luminol_core::ModifiedState,
#[cfg(not(target_arch = "wasm32"))] log_term_rx: luminol_term::TermReceiver,
#[cfg(not(target_arch = "wasm32"))] log_byte_rx: luminol_term::ByteReceiver,
@@ -90,6 +91,8 @@ impl App {
#[cfg(target_arch = "wasm32")] audio: luminol_audio::AudioWrapper,
#[cfg(feature = "steamworks")] steamworks: Steamworks,
) -> Self {
+ luminol_core::set_git_revision(crate::git_revision());
+
let render_state = cc
.wgpu_render_state
.clone()
@@ -238,7 +241,11 @@ impl App {
bytes_loader,
toasts,
- windows: luminol_core::Windows::default(),
+ windows: report.map_or_else(luminol_core::Windows::new, |report| {
+ luminol_core::Windows::new_with_windows(vec![
+ luminol_ui::windows::reporter::Window::new(report, crate::git_revision()),
+ ])
+ }),
tabs: luminol_core::Tabs::new_with_tabs(
"luminol_main_tabs",
vec![luminol_ui::tabs::started::Tab::default()],
@@ -266,6 +273,8 @@ impl luminol_eframe::App for App {
#[cfg(not(target_arch = "wasm32"))]
ctx.input(|i| {
if let Some(f) = i.raw.dropped_files.first() {
+ super::RESTART_AFTER_PANIC.store(true, std::sync::atomic::Ordering::Relaxed);
+
let path = f.path.clone().expect("dropped file has no path");
let path = camino::Utf8PathBuf::from_path_buf(path).expect("path was not utf8");
@@ -305,6 +314,7 @@ impl luminol_eframe::App for App {
toolbar: &mut self.toolbar,
modified: self.modified.clone(),
project_manager: &mut self.project_manager,
+ git_revision: crate::git_revision(),
};
// If a file/folder picker is open, prevent the user from interacting with the application
@@ -378,6 +388,8 @@ impl luminol_eframe::App for App {
self.lumi.ui(ctx);
+ super::RESTART_AFTER_PANIC.store(true, std::sync::atomic::Ordering::Relaxed);
+
self.bytes_loader.load_unloaded_files(ctx, &self.filesystem);
#[cfg(feature = "steamworks")]
diff --git a/src/main.rs b/src/main.rs
index 85926f19..6bd93eb5 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -21,9 +21,13 @@
// it with Steamworks API by Valve Corporation, containing parts covered by
// terms of the Steamworks API by Valve Corporation, the licensors of this
// Program grant you additional permission to convey the resulting work.
+#![cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))]
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
#![cfg_attr(target_arch = "wasm32", no_main)] // there is no main function in web builds
+#[cfg(not(target_arch = "wasm32"))]
+use std::io::{Read, Write};
+
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
@@ -31,6 +35,9 @@ use wasm_bindgen::prelude::*;
/// Embedded icon 256x256 in size.
const ICON: &[u8] = include_bytes!("../assets/icon-256.png");
+static RESTART_AFTER_PANIC: std::sync::atomic::AtomicBool =
+ std::sync::atomic::AtomicBool::new(false);
+
mod app;
mod lumi;
@@ -40,15 +47,24 @@ compile_error!("Steamworks is not supported on webassembly");
#[cfg(feature = "steamworks")]
mod steam;
+pub fn git_revision() -> &'static str {
+ #[cfg(not(target_arch = "wasm32"))]
+ {
+ git_version::git_version!()
+ }
+ #[cfg(target_arch = "wasm32")]
+ option_env!("LUMINOL_VERSION").unwrap_or(git_version::git_version!())
+}
+
#[cfg(not(target_arch = "wasm32"))]
/// A writer that copies whatever is written to it to two other writers.
struct CopyWriter(A, B);
#[cfg(not(target_arch = "wasm32"))]
-impl std::io::Write for CopyWriter
+impl Write for CopyWriter
where
- A: std::io::Write,
- B: std::io::Write,
+ A: Write,
+ B: Write,
{
fn write(&mut self, buf: &[u8]) -> std::io::Result {
Ok(self.0.write(buf)?.min(self.1.write(buf)?))
@@ -77,7 +93,7 @@ static CONTEXT: once_cell::sync::OnceCell = once_cell::sync::Once
struct LogWriter(luminol_term::termwiz::escape::parser::Parser);
#[cfg(not(target_arch = "wasm32"))]
-impl std::io::Write for LogWriter {
+impl Write for LogWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result {
LOG_BYTE_SENDER
.get()
@@ -122,6 +138,18 @@ impl std::io::Write for LogWriter {
#[cfg(not(target_arch = "wasm32"))]
fn main() {
+ // Load the panic report from the previous run if it exists
+ let mut report = None;
+ if let Some(path) = std::env::var_os("LUMINOL_PANIC_REPORT_FILE") {
+ if let Ok(mut file) = std::fs::File::open(&path) {
+ let mut buffer = String::new();
+ if file.read_to_string(&mut buffer).is_ok() {
+ report = Some(buffer);
+ }
+ }
+ let _ = std::fs::remove_file(path);
+ }
+
#[cfg(feature = "steamworks")]
let steamworks = match steam::Steamworks::new() {
Ok(s) => s,
@@ -172,14 +200,12 @@ fn main() {
std::process::abort();
});
- // Enable full backtraces unless the user manually set the RUST_BACKTRACE environment variable
- if std::env::var("RUST_BACKTRACE").is_err() {
- std::env::set_var("RUST_BACKTRACE", "full");
- }
+ // Enable full backtraces
+ std::env::set_var("RUST_BACKTRACE", "full");
// Set up hooks for formatting errors and panics
- color_eyre::config::HookBuilder::default()
- .panic_section(format!("Luminol version: {}", git_version::git_version!()))
+ let (panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default()
+ .panic_section(format!("Luminol version: {}", git_revision()))
.add_frame_filter(Box::new(|frames| {
let filters = &[
"_",
@@ -209,8 +235,49 @@ fn main() {
})
})
}))
+ .into_hooks();
+ eyre_hook
.install()
.expect("failed to install color-eyre hooks");
+ std::panic::set_hook(Box::new(move |info| {
+ let report = panic_hook.panic_report(info).to_string();
+ eprintln!("{report}");
+
+ if !RESTART_AFTER_PANIC.load(std::sync::atomic::Ordering::Relaxed) {
+ return;
+ }
+
+ let mut args = std::env::args_os();
+ let arg0 = args.next();
+ let exe_path = std::env::current_exe().map_or_else(
+ |_| arg0.expect("could not get path to current executable"),
+ |exe_path| exe_path.into_os_string(),
+ );
+
+ let mut file = tempfile::NamedTempFile::new().expect("failed to create temporary file");
+ file.write_all(report.as_bytes())
+ .expect("failed to write to temporary file");
+ file.flush().expect("failed to flush temporary file");
+ let (_, path) = file.keep().expect("failed to persist temporary file");
+ std::env::set_var("LUMINOL_PANIC_REPORT_FILE", &path);
+
+ #[cfg(unix)]
+ {
+ use std::os::unix::process::CommandExt;
+
+ let error = std::process::Command::new(exe_path).args(args).exec();
+ eprintln!("Failed to restart Luminol: {error:?}");
+ let _ = std::fs::remove_file(&path);
+ }
+
+ #[cfg(not(unix))]
+ {
+ if let Err(error) = std::process::Command::new(exe_path).args(args).spawn() {
+ eprintln!("Failed to restart Luminol: {error:?}");
+ let _ = std::fs::remove_file(&path);
+ }
+ }
+ }));
// Log to stderr as well as Luminol's log.
let (log_term_tx, log_term_rx) = luminol_term::unbounded();
@@ -262,6 +329,7 @@ fn main() {
CONTEXT.set(cc.egui_ctx.clone()).unwrap();
Box::new(app::App::new(
cc,
+ report,
Default::default(),
log_term_rx,
log_byte_rx,
@@ -274,16 +342,27 @@ fn main() {
.expect("failed to start luminol");
}
+#[cfg(target_arch = "wasm32")]
+#[wasm_bindgen(
+ inline_js = "let report = null; export function get_panic_report() { return report; }; export function set_panic_report(r) { report = r; window.restartLuminol(); };"
+)]
+extern "C" {
+ fn get_panic_report() -> Option;
+ fn set_panic_report(r: String);
+}
+
#[cfg(target_arch = "wasm32")]
const CANVAS_ID: &str = "luminol-canvas";
#[cfg(target_arch = "wasm32")]
struct WorkerData {
+ report: Option,
audio: luminol_audio::AudioWrapper,
modified: luminol_core::ModifiedState,
prefers_color_scheme_dark: Option,
fs_worker_channels: luminol_filesystem::web::WorkerChannels,
runner_worker_channels: luminol_eframe::web::WorkerChannels,
+ runner_panic_tx: std::sync::Arc>>>,
}
#[cfg(target_arch = "wasm32")]
@@ -292,40 +371,82 @@ static WORKER_DATA: parking_lot::Mutex