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.
This commit is contained in:
817 files changed
+143101
No files matched your search
@@ -0,0 +1,280 @@
|
||||
//! Opening user files and copies over one worker (FILE-03).
|
||||
|
||||
use std::{
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use ely_files::{Open, Session};
|
||||
use gpui::{AppContext as _, Context, Entity, TaskExt as _, Window};
|
||||
|
||||
use crate::engine::{Engine, Load};
|
||||
use crate::files::{self, file_name};
|
||||
use crate::{Host, Tab, Tabs, View, in_window, text};
|
||||
|
||||
/// How host `H`'s engine reads text files.
|
||||
type ImportOf<H> = <<H as Host>::Engine as Engine>::Import;
|
||||
|
||||
/// What a load starts from.
|
||||
pub enum Source<I> {
|
||||
/// A user file under its registry id, with its password.
|
||||
File(PathBuf, usize, String),
|
||||
/// A template copy; the result is untitled.
|
||||
Copy(PathBuf),
|
||||
/// A copy that needed a password, with the one given.
|
||||
Locked(PathBuf, String),
|
||||
/// Checked text bytes, where they go, how to read.
|
||||
Import(PathBuf, Arc<Vec<u8>>, I),
|
||||
}
|
||||
|
||||
pub trait Opening: Host {
|
||||
/// An untitled copy of a local template.
|
||||
fn new_from_file(&mut self, template: &Path, cx: &mut Context<Self>) {
|
||||
let name = file_name(template);
|
||||
let copy = files::work_dir(&self.core().work, self.core().next).and_then(|dir| {
|
||||
let copy = dir.join(name.as_ref());
|
||||
std::fs::copy(template, ©)?;
|
||||
Ok(copy)
|
||||
});
|
||||
match copy {
|
||||
Ok(copy) => self.load(Source::Copy(copy), cx),
|
||||
Err(error) => self.failed_open(&name, &error, cx),
|
||||
}
|
||||
}
|
||||
|
||||
/// A new document from checked text bytes named `name`.
|
||||
fn import_text(
|
||||
&mut self,
|
||||
name: &str,
|
||||
bytes: Arc<Vec<u8>>,
|
||||
how: ImportOf<Self>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match files::work_dir(&self.core().work, self.core().next) {
|
||||
Ok(dir) => self.load(Source::Import(dir.join(name), bytes, how), cx),
|
||||
Err(error) => self.failed_open(name, &error, cx),
|
||||
}
|
||||
}
|
||||
|
||||
/// An untitled document from `contents` named `name`.
|
||||
fn new_from_text(&mut self, name: &str, contents: &str, cx: &mut Context<Self>) {
|
||||
let copy = files::work_dir(&self.core().work, self.core().next).and_then(|dir| {
|
||||
let copy = dir.join(name);
|
||||
std::fs::write(©, contents)?;
|
||||
Ok(copy)
|
||||
});
|
||||
match copy {
|
||||
Ok(copy) => self.load(Source::Copy(copy), cx),
|
||||
Err(error) => self.failed_open(name, &error, cx),
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens a user file; an open one gets focus (FILE-03).
|
||||
fn open_file(&mut self, path: PathBuf, cx: &mut Context<Self>) {
|
||||
self.open_with(path, String::new(), cx);
|
||||
}
|
||||
|
||||
/// Opens a user file with its password.
|
||||
fn open_with(&mut self, path: PathBuf, password: String, cx: &mut Context<Self>) {
|
||||
match self.core_mut().sessions.open(&path, false) {
|
||||
Ok(Open::Focus(registry)) => {
|
||||
let core = self.core_mut();
|
||||
core.active = core
|
||||
.tabs
|
||||
.iter()
|
||||
.find(|tab| tab.registry == Some(registry))
|
||||
.map(|tab| tab.id);
|
||||
cx.notify();
|
||||
}
|
||||
Ok(Open::Fresh(registry) | Open::View(registry)) => {
|
||||
self.load(Source::File(path, registry, password), cx)
|
||||
}
|
||||
Err(error) => self.failed_open(&path.display().to_string(), &error, cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn failed_open(&self, name: &str, error: &io::Error, cx: &mut gpui::App) {
|
||||
log::error!("{}: {name} did not open: {error}", Self::KIND.app.slug());
|
||||
let error = error.to_string();
|
||||
let text = text(
|
||||
cx,
|
||||
"office.error.open",
|
||||
&[("name", name), ("error", &error)],
|
||||
);
|
||||
self.toast(text, ely_gpui_component::primitives::Severity::Danger, cx);
|
||||
}
|
||||
|
||||
/// Loads off the main thread; the tab takes focus.
|
||||
fn load(&mut self, source: Source<ImportOf<Self>>, cx: &mut Context<Self>) {
|
||||
let id = self.core().next;
|
||||
self.core_mut().next += 1;
|
||||
self.load_into(id, source, cx);
|
||||
}
|
||||
|
||||
/// Loads into tab id `id`, taken earlier from `next`.
|
||||
fn load_into(&mut self, id: u64, source: Source<ImportOf<Self>>, cx: &mut Context<Self>) {
|
||||
let (path, password, text) = match &source {
|
||||
Source::File(path, _, password) => (path.clone(), password.clone(), None),
|
||||
Source::Copy(path) => (path.clone(), String::new(), None),
|
||||
Source::Locked(path, password) => (path.clone(), password.clone(), None),
|
||||
Source::Import(path, bytes, how) => (
|
||||
path.clone(),
|
||||
String::new(),
|
||||
Some((Arc::clone(bytes), how.clone())),
|
||||
),
|
||||
};
|
||||
let written = path.clone();
|
||||
self.core_mut().loading.push((id, file_name(&path)));
|
||||
cx.notify();
|
||||
let worker = Arc::clone(&self.core().worker);
|
||||
cx.spawn(async move |this, cx| {
|
||||
let loaded = cx
|
||||
.background_spawn(async move {
|
||||
if let Some((bytes, _)) = &text {
|
||||
std::fs::write(&written, bytes.as_slice())?;
|
||||
}
|
||||
let mut worker = worker.lock().expect("the worker lock");
|
||||
let from = match &text {
|
||||
None => Load::File(&written, &password),
|
||||
Some((_, how)) => Load::Import(&written, how),
|
||||
};
|
||||
Self::Engine::load(&mut worker, from)
|
||||
})
|
||||
.await;
|
||||
in_window(&this, cx, |host, window, cx| {
|
||||
host.loaded(id, source, loaded, window, cx)
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn loaded(
|
||||
&mut self,
|
||||
id: u64,
|
||||
source: Source<ImportOf<Self>>,
|
||||
loaded: io::Result<<Self::Engine as Engine>::Loaded>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.core_mut()
|
||||
.loading
|
||||
.retain(|(loading, _)| *loading != id);
|
||||
let imported = matches!(source, Source::Import(..));
|
||||
let (path, registry, tried) = match source {
|
||||
Source::File(path, registry, password) => (path, Some(registry), !password.is_empty()),
|
||||
Source::Copy(path) | Source::Import(path, ..) => (path, None, false),
|
||||
Source::Locked(path, _) => (path, None, true),
|
||||
};
|
||||
let opened = loaded.and_then(|loaded| {
|
||||
let risk = match files::macro_risk(&Self::KIND, &path)? {
|
||||
true => Some(crate::Risk::Macros),
|
||||
false => Self::Engine::risk(&loaded),
|
||||
};
|
||||
let (work, session) = match registry {
|
||||
Some(_) => (
|
||||
files::work_dir(&self.core().work, id)?,
|
||||
Session::load(path.clone())?,
|
||||
),
|
||||
None => {
|
||||
let dir = path.parent().expect("a copy sits in its work dir");
|
||||
(dir.to_path_buf(), Session::new_file())
|
||||
}
|
||||
};
|
||||
let (doc, view, mark) = Self::Engine::open(&self.core().worker, loaded, window, cx)?;
|
||||
Ok((doc, work, session, risk, view, mark))
|
||||
});
|
||||
let (doc, work, session, risk, view, mark) = match opened {
|
||||
Ok(opened) => opened,
|
||||
Err(error) => {
|
||||
if let Some(registry) = registry {
|
||||
self.core_mut().sessions.close(registry);
|
||||
}
|
||||
cx.notify();
|
||||
// A locked copy asks too, and stays a copy.
|
||||
if error.kind() == io::ErrorKind::PermissionDenied && !imported {
|
||||
return crate::Dialogs::ask_password(self, path, tried, registry.is_none(), window, cx);
|
||||
}
|
||||
return self.failed_open(&path.display().to_string(), &error, cx);
|
||||
}
|
||||
};
|
||||
let name = match registry {
|
||||
Some(_) => file_name(&path),
|
||||
None if imported => file_name(&path),
|
||||
None => {
|
||||
self.core_mut().untitled += 1;
|
||||
let n = self.core().untitled.to_string();
|
||||
text(cx, "office.untitled", &[("n", &n)])
|
||||
}
|
||||
};
|
||||
log::info!(
|
||||
"{}: opened {name} as tab {id}, risk {risk:?}",
|
||||
Self::KIND.app.slug()
|
||||
);
|
||||
self.watch(id, &view, cx);
|
||||
window.focus(&Self::Engine::focus(view.read(cx)), cx);
|
||||
self.core_mut().tabs.push(Tab {
|
||||
id,
|
||||
view,
|
||||
doc,
|
||||
session,
|
||||
registry,
|
||||
name,
|
||||
modified: false,
|
||||
risk,
|
||||
mark,
|
||||
work,
|
||||
exporting: false,
|
||||
reopen: false,
|
||||
extra: Default::default(),
|
||||
});
|
||||
self.core_mut().active = Some(id);
|
||||
// Points from now on; a crash copy comes back unsaved.
|
||||
let named = self.tab(id).expect("just opened").name.clone();
|
||||
crate::Recovering::keep_tab(self, id, &named, tried, cx);
|
||||
crate::Recovering::reopened(self, id, &path, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Engine errors become toasts; kernel edits mark the session.
|
||||
fn watch(&mut self, id: u64, view: &Entity<View<Self>>, cx: &mut Context<Self>) {
|
||||
let reported = move |host: &mut Self,
|
||||
view: &Entity<View<Self>>,
|
||||
modified: bool,
|
||||
cx: &mut Context<Self>| {
|
||||
if let Some(tab) = host.tab_mut(id) {
|
||||
match (modified, tab.modified) {
|
||||
(true, false) => tab.session.edit(),
|
||||
(false, true) => tab.session.reverted(),
|
||||
_ => {}
|
||||
}
|
||||
tab.modified = modified;
|
||||
}
|
||||
host.observed(id, view, cx);
|
||||
if Self::Engine::settled(view.read(cx)) {
|
||||
let (ready, rest): (Vec<_>, Vec<_>) = host
|
||||
.core_mut()
|
||||
.waiting
|
||||
.drain(..)
|
||||
.partition(|(tab, _)| *tab == id);
|
||||
host.core_mut().waiting = rest;
|
||||
for (_, then) in ready {
|
||||
cx.spawn(async move |this, cx| in_window(&this, cx, then))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
};
|
||||
Self::Engine::watch(view, cx, reported, move |host, error, cx| {
|
||||
// A released tab's view may finish its queue late.
|
||||
if host.tab(id).is_none() {
|
||||
log::info!("office: tab {id} failed after it closed");
|
||||
return;
|
||||
}
|
||||
host.engine_failed(error, cx);
|
||||
crate::Stopping::check_engine(host, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: Host> Opening for H {}
|
||||
Reference in new issue
Block a user