Files
ElyOffice/crates/ely-office/src/lok.rs
T

242 lines
8.8 KiB
Rust

//! LibreOfficeKit as the shell's engine (§7).
use std::{
io,
path::{Path, PathBuf},
sync::{Arc, Mutex},
};
use ely_lok::{Doc, LokError, LokView, Reported, Worker, sheet::csv::Import};
use ely_platform::AppId;
use gpui::{App, Context, Entity, FocusHandle, Window};
use crate::engine::{Engine, Load};
use crate::{Host, Tabs, text};
/// The LOK engine.
pub struct Lok;
/// A load: the document, its layout, its history.
pub struct Loaded {
doc: Doc,
laid: ely_lok::Layout,
mark: String,
}
impl Engine for Lok {
type Worker = Worker;
type Doc = Doc;
type View = LokView;
type Loaded = Loaded;
type Import = Import;
fn load(worker: &mut Worker, from: Load<Import>) -> io::Result<Loaded> {
let doc = match from {
Load::File(path, password) => worker.unlock(&ely_lok::file_url(path), password)?,
Load::Import(path, how) => worker.import(&ely_lok::file_url(path), how)?,
};
// Layout and history too: the window never waits.
let laid = ely_lok::layout(worker, &doc)?;
let mark = worker.values(&doc, ".uno:Undo")?;
Ok(Loaded { doc, laid, mark })
}
fn open(
worker: &Arc<Mutex<Worker>>,
loaded: Loaded,
_: &mut Window,
cx: &mut App,
) -> io::Result<(Doc, Entity<LokView>, String)> {
let Loaded { doc, laid, mark } = loaded;
let view = LokView::open(Arc::clone(worker), doc.clone(), laid, cx);
Ok((doc, view, mark))
}
fn history(worker: &mut Worker, doc: &Doc) -> io::Result<String> {
worker.values(doc, ".uno:Undo")
}
fn write(worker: &mut Worker, doc: &Doc, path: &Path, format: &str) -> io::Result<()> {
doc.save_owned(worker, &ely_lok::file_url(path), format)
}
fn snapshot(worker: &mut Worker, doc: &Doc, path: &Path, format: &str) -> io::Result<()> {
doc.save_as(worker, &ely_lok::file_url(path), format)
}
fn close(worker: &mut Worker, doc: Doc) -> io::Result<()> {
worker.close_doc(doc)
}
fn retire(view: &Entity<LokView>, cx: &mut App) -> ely_gpui_component::primitives::Retired {
view.update(cx, |view, _| view.retire_all())
}
fn ended(worker: &mut Worker) -> Option<String> {
worker.ended()
}
fn restart(app: AppId, worker: &mut Worker) -> io::Result<()> {
let author = worker.author().map(str::to_string);
let author = author.ok_or_else(|| io::Error::other("the ended worker had no author"))?;
// The ended worker stays until the new one is ready.
let mut fresh = spawn(app)?;
fresh.ready()?;
fresh.set_author(&author)?;
*worker = fresh;
Ok(())
}
fn settled(view: &LokView) -> bool {
view.settled()
}
fn edited(view: &LokView) -> u64 {
view.edited()
}
fn edit_pending(view: &LokView) -> bool {
view.edit_pending()
}
fn focus(view: &LokView) -> FocusHandle {
view.focus_handle().clone()
}
/// The kernel reports its own modified status.
fn saved(_: &Entity<LokView>, _: &str, _: &mut App) {}
fn watch<H: Host<Engine = Self>>(
view: &Entity<LokView>,
cx: &mut Context<H>,
reported: impl Fn(&mut H, &Entity<LokView>, bool, &mut Context<H>) + 'static,
failed: impl Fn(&mut H, &str, &mut Context<H>) + 'static,
) {
cx.subscribe(view, move |host, _, error: &LokError, cx| match error.0.starts_with(ely_lok::STALE) {
true => failed(host, &text(cx, "office.error.changed", &[]), cx),
false => failed(host, &error.0, cx),
})
.detach();
// The status shows the zoom.
cx.subscribe(view, |_, _, _: &ely_lok::Zoomed, cx| cx.notify()).detach();
cx.subscribe(view, move |host, view, _: &Reported, cx| {
let modified = view.read(cx).state(".uno:ModifiedStatus") == Some("true");
reported(host, &view, modified, cx)
})
.detach();
}
}
/// The worker and the scratch root of `app`.
pub fn start(app: AppId, cx: &mut App) -> io::Result<(Arc<Mutex<Worker>>, PathBuf)> {
let data = ely_platform::data_dir(app).map_err(io::Error::other)?;
let work = crate::fresh_work(&data)?;
let worker = Arc::new(Mutex::new(spawn(app)?));
// The window opens now; the kernel finishes starting behind it.
let starting = Arc::clone(&worker);
// Settings as read at launch; defaults if unreadable.
let wanted = Arc::new(Mutex::new(ely_ui::settings::current(cx).author.clone()));
let first = Arc::clone(&wanted);
let started = cx.background_executor().spawn(async move {
let mut worker = starting.lock().expect("the worker lock");
worker.ready()?;
sign(&mut worker, &first)
});
cx.spawn(async move |_| {
if let Err(error) = started.await {
ely_platform::startup::fail(app, &format!("the document engine did not start: {error}"));
}
})
.detach();
follow_author(&worker, wanted, cx);
Ok((worker, work))
}
/// Sends the newest wanted name, read under the worker lock.
fn sign(worker: &mut Worker, wanted: &Mutex<String>) -> io::Result<()> {
let author = wanted.lock().expect("the author lock").clone();
worker.set_author(&ely_platform::user::named(&author)?)
}
/// A worker process on the app's profile, not yet checked.
fn spawn(app: AppId) -> io::Result<Worker> {
let data = ely_platform::data_dir(app).map_err(io::Error::other)?;
let install = ely_lok::install_dir()?;
let worker = Worker::launch(&install, &data.join("lok-profile"))?;
log::info!("{}: worker from {}", app.slug(), install.display());
Ok(worker)
}
/// A new author name signs from the next comment on.
fn follow_author(worker: &Arc<Mutex<Worker>>, wanted: Arc<Mutex<String>>, cx: &mut App) {
let worker = Arc::clone(worker);
cx.observe_global::<ely_ui::settings::Current>(move |cx| {
let author = ely_ui::settings::current(cx).author.clone();
if *wanted.lock().expect("the author lock") == author {
return;
}
*wanted.lock().expect("the author lock") = author;
let (worker, wanted) = (Arc::clone(&worker), Arc::clone(&wanted));
cx.background_executor()
.spawn(async move {
// Any order of these tasks ends on the newest name.
let set = sign(&mut worker.lock().expect("the worker lock"), &wanted);
if let Err(error) = set {
log::error!("lok: the new author name is not set: {error}");
}
})
.detach();
})
.detach();
}
/// Mapped UNO commands on LOK tabs.
pub trait Commands: Host<Engine = Lok> {
/// Runs a mapped command on the active tab.
fn run_active(&mut self, id: &str, values: &[&str], cx: &mut Context<Self>) {
self.run_in(self.core().active, id, values, cx);
}
/// Runs a mapped command on tab `tab`.
fn run_in(&mut self, tab: Option<u64>, id: &str, values: &[&str], cx: &mut Context<Self>) {
let Some(tab) = tab.and_then(|tab| self.tab(tab)) else {
let text = text(cx, "command.blocked.no_document", &[]);
return self.toast(text, ely_gpui_component::primitives::Severity::Warning, cx);
};
log::info!("{}: runs {id}", Self::KIND.app.slug());
// Numbers hold only in the state they were read.
let addressed = ely_lok::commands::find(id).is_ok_and(|command| command.addressed(values));
let sent = match (addressed, self.read_at(tab.id, cx)) {
(true, Some(at)) => tab.view.update(cx, |view, _| view.run_at(&at, id, values)),
_ => tab.view.update(cx, |view, _| view.run(id, values)),
};
if let Err(error) = sent {
self.engine_failed(&error.to_string(), cx);
}
}
/// Runs `commands` made from tab `tab`'s last read, as one.
fn run_all_in(&mut self, tab: u64, commands: &[(&str, &[&str])], cx: &mut Context<Self>) {
let (Some(open), Some(at)) = (self.tab(tab), self.read_at(tab, cx)) else {
return log::warn!("{}: tab {tab} has no read to run from", Self::KIND.app.slug());
};
log::info!("{}: runs {} commands from one read", Self::KIND.app.slug(), commands.len());
if let Err(error) = open.view.update(cx, |view, _| view.run_all_at(&at, commands)) {
self.engine_failed(&error.to_string(), cx);
}
}
/// Runs a numbered command against the read state `at`.
fn run_on(&mut self, tab: u64, at: &Doc, id: &str, values: &[&str], cx: &mut Context<Self>) {
let Some(tab) = self.tab(tab) else {
return;
};
log::info!("{}: runs {id} at a read state", Self::KIND.app.slug());
if let Err(error) = tab.view.update(cx, |view, _| view.run_at(at, id, values)) {
self.engine_failed(&error.to_string(), cx);
}
}
}
impl<H: Host<Engine = Lok>> Commands for H {}