Files
ElyOffice/crates/ely-office/src/stopped.rs
T
ZacharyZhang-NY b7fca0146d Ely Office: six offline-first desktop apps in Rust and GPUI
ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git),
ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
2026-10-08 21:20:08 -04:00

128 lines
5.0 KiB
Rust

//! The engine ended: say so, then reopen (§6.3).
use std::{path::PathBuf, sync::Arc};
use ely_gpui_component::{feedback::Banner, primitives::Severity};
use gpui::{AppContext as _, Context};
use crate::{Closing as _, Dialog, Engine, Host, Opening as _, Recovering as _, Tabs as _, recover::clock, text};
/// Why the engine ended, and whether it is coming back.
pub struct Stopped {
why: String,
restarting: bool,
}
pub trait Stopping: Host {
/// After an engine error: asks if it ended.
fn check_engine(&mut self, cx: &mut Context<Self>) {
if self.core().stopped.is_some() {
return;
}
let worker = Arc::clone(&self.core().worker);
cx.spawn(async move |this, cx| {
let ended = cx
.background_spawn(async move { Self::Engine::ended(&mut worker.lock().expect("the worker lock")) })
.await;
let Some(why) = ended else { return };
let noted = this.update(cx, |host, cx| {
if host.core().stopped.is_none() {
log::error!("{}: the engine ended ({why})", Self::KIND.app.slug());
host.core_mut().stopped = Some(Stopped { why, restarting: false });
cx.notify();
}
});
if noted.is_err() {
log::info!("office: the engine ended after its window");
}
})
.detach();
}
/// What ended, the last point, and the way back.
fn stopped_banner(&self, cx: &mut Context<Self>) -> Option<Banner> {
let stopped = self.core().stopped.as_ref()?;
let at = match self.core().keeping.as_ref().and_then(|keeping| keeping.last) {
Some(at) => clock(at),
None => text(cx, "office.stopped.never", &[]).to_string(),
};
let said = text(cx, "office.stopped", &[("why", &stopped.why), ("at", &at)]);
let banner = Banner::new("office-stopped", Severity::Danger, said);
if stopped.restarting {
return Some(banner);
}
let this = cx.entity();
Some(banner.action(text(cx, "office.stopped.reopen", &[]), move |_, cx| {
this.update(cx, |host, cx| host.restart_engine(cx))
}))
}
/// A new worker, then each tab from point or file.
fn restart_engine(&mut self, cx: &mut Context<Self>) {
let Some(stopped) = self.core_mut().stopped.as_mut() else { return };
stopped.restarting = true;
cx.notify();
let worker = Arc::clone(&self.core().worker);
cx.spawn(async move |this, cx| {
let restarted = cx
.background_spawn(async move {
Self::Engine::restart(Self::KIND.app, &mut worker.lock().expect("the worker lock"))
})
.await;
let reopened = this.update(cx, |host, cx| match restarted {
Ok(()) => host.reopen_all(cx),
Err(error) => {
log::error!("{}: the engine does not restart: {error}", Self::KIND.app.slug());
host.core_mut().stopped = Some(Stopped { why: error.to_string(), restarting: false });
cx.notify();
}
});
if reopened.is_err() {
log::info!("office: the engine restarted after its window");
}
})
.detach();
}
/// Unsaved tabs from their newest point, clean ones from file.
fn reopen_all(&mut self, cx: &mut Context<Self>) {
self.core_mut().stopped = None;
let tabs: Vec<(u64, String, Option<PathBuf>, bool)> = self
.core()
.tabs
.iter()
.map(|tab| (tab.id, tab.name.to_string(), tab.session.path().map(PathBuf::from), tab.dirty(cx)))
.collect();
let mut lost = Vec::new();
for (id, name, path, dirty) in tabs {
if !dirty {
self.release_tab(id, cx);
if let Some(path) = path {
self.open_file(path, cx);
}
continue;
}
match self.reopen_point(id, cx) {
Ok(true) => {}
Ok(false) => lost.push(name),
Err(error) => {
log::error!("office: tab {id} point does not open ({:?})", error.kind());
lost.push(format!("{name} ({error})"));
}
}
self.release_tab(id, cx);
}
// Questions about the released tabs have no answer now.
if matches!(self.core().dialog, Some(Dialog::Unsaved { .. } | Dialog::Conflict { .. })) {
self.core_mut().dialog = None;
}
log::info!("{}: the engine is back, {} tab(s) lost", Self::KIND.app.slug(), lost.len());
if !lost.is_empty() {
let shown = text(cx, "office.stopped.lost", &[("names", &lost.join(", "))]);
self.toast_unlogged(shown, Severity::Danger, cx);
}
}
}
impl<H: Host> Stopping for H {}