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,162 @@
|
||||
//! Folders, files, the recent list, going to a line.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use ely_gpui_component::primitives::Severity;
|
||||
use ely_office::prelude::*;
|
||||
use ely_ui::i18n::text;
|
||||
use gpui::{Context, Window};
|
||||
|
||||
use crate::desk::Desk;
|
||||
use crate::folder::{Folder, markdown_under};
|
||||
|
||||
impl Desk {
|
||||
/// A folder lists beside; a file opens in a tab.
|
||||
pub fn open_path(&mut self, path: PathBuf, cx: &mut Context<Self>) {
|
||||
match path.is_dir() {
|
||||
true => self.open_folder(path, cx),
|
||||
false => self.open_file(path, cx),
|
||||
}
|
||||
}
|
||||
|
||||
/// Lists `root`'s Markdown files off the window (MARK-01).
|
||||
pub fn open_folder(&mut self, root: PathBuf, cx: &mut Context<Self>) {
|
||||
log::info!("mark: opens folder {}", root.display());
|
||||
cx.spawn(async move |desk, cx| {
|
||||
let listed = root.clone();
|
||||
// The real root: listed files compare with tabs' paths.
|
||||
let files = cx
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
let real = std::fs::canonicalize(&listed)?;
|
||||
Ok::<_, std::io::Error>((markdown_under(&real)?, real))
|
||||
})
|
||||
.await;
|
||||
desk.update(cx, |desk, cx| match files {
|
||||
Ok((files, real)) => {
|
||||
desk.remember(&real, cx);
|
||||
desk.folder = Some(Folder { root: real, files });
|
||||
desk.listings += 1;
|
||||
cx.notify();
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!("mark: {} did not list: {error}", root.display());
|
||||
let (name, error) = (root.display().to_string(), error.to_string());
|
||||
let said = text(
|
||||
cx,
|
||||
"office.error.open",
|
||||
&[("name", &name), ("error", &error)],
|
||||
);
|
||||
desk.toast(said, Severity::Danger, cx);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
/// The open folder again, as the disk has it now.
|
||||
pub fn relist(&mut self, cx: &mut Context<Self>) {
|
||||
if let Some(root) = self.folder.as_ref().map(|folder| folder.root.clone()) {
|
||||
self.open_folder(root, cx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Puts `path` first in the recent list.
|
||||
pub fn remember(&mut self, path: &std::path::Path, cx: &mut Context<Self>) {
|
||||
// One spelling per file: its real path.
|
||||
let remembered = std::fs::canonicalize(path)
|
||||
.and_then(|path| ely_files::recent::remember(&self.recent_file, &path));
|
||||
match remembered {
|
||||
Ok(list) => self.recent = list,
|
||||
Err(error) => {
|
||||
log::error!("mark: the recent list did not save: {error}");
|
||||
self.toast(
|
||||
text(cx, "office.error.engine", &[("error", &error.to_string())]),
|
||||
Severity::Danger,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tab `id` shows `path`; the recent list follows.
|
||||
pub fn track_recent(&mut self, id: u64, path: &std::path::Path, cx: &mut Context<Self>) {
|
||||
let Ok(real) = std::fs::canonicalize(path) else {
|
||||
return log::warn!("mark: {} is gone; the recent list keeps it", path.display());
|
||||
};
|
||||
if self.remembered.get(&id) == Some(&real) {
|
||||
return;
|
||||
}
|
||||
if let Some(old) = self.remembered.insert(id, real)
|
||||
&& !old.exists()
|
||||
{
|
||||
self.forget(&old, cx);
|
||||
}
|
||||
self.remember(path, cx);
|
||||
}
|
||||
|
||||
/// Takes a path that is gone out of the list.
|
||||
pub fn forget(&mut self, path: &std::path::Path, cx: &mut Context<Self>) {
|
||||
match ely_files::recent::forget(&self.recent_file, path) {
|
||||
Ok(()) => self.recent.retain(|known| known != path),
|
||||
Err(error) => {
|
||||
log::error!("mark: the recent list did not save: {error}");
|
||||
let said = text(cx, "office.error.engine", &[("error", &error.to_string())]);
|
||||
self.toast(said, Severity::Danger, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `place` in `tab`, else in `file` once open.
|
||||
pub fn open_at(
|
||||
&mut self,
|
||||
tab: Option<u64>,
|
||||
file: PathBuf,
|
||||
place: serde_json::Value,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let held = tab.or_else(|| self.tab_of(&file));
|
||||
if let Some(id) = held.filter(|id| self.tab(*id).is_some()) {
|
||||
self.core.active = Some(id);
|
||||
let view = self.tab(id).map(|open| open.view.clone());
|
||||
if let Some(view) = view {
|
||||
view.update(cx, |view, cx| view.reveal(place, cx));
|
||||
// After the list's own key handling, which keeps focus.
|
||||
let focus = view.read(cx).focus.clone();
|
||||
window.defer(cx, move |window, cx| window.focus(&focus, cx));
|
||||
}
|
||||
return cx.notify();
|
||||
}
|
||||
// A file gone is reported by the open itself.
|
||||
if let Ok(real) = std::fs::canonicalize(&file) {
|
||||
self.going = Some((real, place));
|
||||
}
|
||||
self.open_file(file, cx);
|
||||
}
|
||||
|
||||
/// The tab showing `path`, by real path.
|
||||
pub fn tab_of(&self, path: &std::path::Path) -> Option<u64> {
|
||||
let real = std::fs::canonicalize(path).ok()?;
|
||||
let held = |tab: &&ely_office::Tab<crate::outside::Extra, crate::engine::Mark>| {
|
||||
tab.session
|
||||
.path()
|
||||
.and_then(|open| std::fs::canonicalize(open).ok())
|
||||
== Some(real.clone())
|
||||
};
|
||||
self.core.tabs.iter().find(held).map(|tab| tab.id)
|
||||
}
|
||||
|
||||
/// Tab `id`'s text as the page last sent it.
|
||||
pub fn text_of(&self, id: u64) -> Option<String> {
|
||||
Some(
|
||||
self.tab(id)?
|
||||
.doc
|
||||
.lock()
|
||||
.expect("the text lock")
|
||||
.current
|
||||
.clone(),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user