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,122 @@
|
||||
//! The fields every app reads from the shared file.
|
||||
|
||||
use std::io;
|
||||
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
use super::Store;
|
||||
|
||||
/// Light, dark, or as the system is.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum Appearance {
|
||||
#[default]
|
||||
System,
|
||||
Light,
|
||||
Dark,
|
||||
}
|
||||
|
||||
/// The interface language; System follows the OS.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum Language {
|
||||
#[default]
|
||||
System,
|
||||
English,
|
||||
Chinese,
|
||||
}
|
||||
|
||||
/// Interface motion; System follows the OS setting.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum Motion {
|
||||
#[default]
|
||||
System,
|
||||
Reduced,
|
||||
Full,
|
||||
}
|
||||
|
||||
pub const APPEARANCE: &str = "appearance";
|
||||
pub const LANGUAGE: &str = "language";
|
||||
pub const MOTION: &str = "motion";
|
||||
pub const OMARCHY_SYNC: &str = "omarchy_sync";
|
||||
pub const AUTHOR: &str = "author";
|
||||
|
||||
/// The shared settings; absent fields take their defaults.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Shared {
|
||||
pub appearance: Appearance,
|
||||
pub language: Language,
|
||||
pub motion: Motion,
|
||||
pub omarchy_sync: bool,
|
||||
/// Signs comments and changes; empty means the account name.
|
||||
pub author: String,
|
||||
}
|
||||
|
||||
impl Default for Shared {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
appearance: Appearance::System,
|
||||
language: Language::System,
|
||||
motion: Motion::System,
|
||||
omarchy_sync: true,
|
||||
author: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Appearance {
|
||||
pub const ALL: [(Self, &'static str); 3] = [(Self::System, "system"), (Self::Light, "light"), (Self::Dark, "dark")];
|
||||
}
|
||||
|
||||
impl Language {
|
||||
pub const ALL: [(Self, &'static str); 3] = [(Self::System, "system"), (Self::English, "en-US"), (Self::Chinese, "zh-CN")];
|
||||
}
|
||||
|
||||
impl Motion {
|
||||
pub const ALL: [(Self, &'static str); 3] = [(Self::System, "system"), (Self::Reduced, "reduce"), (Self::Full, "full")];
|
||||
}
|
||||
|
||||
/// The stored word for `choice` among `all`.
|
||||
pub fn word<T: PartialEq>(all: &[(T, &'static str)], choice: &T) -> &'static str {
|
||||
all.iter().find(|(each, _)| each == choice).map(|(_, word)| *word).expect("every choice has a word")
|
||||
}
|
||||
|
||||
impl Shared {
|
||||
/// The shared file as read now.
|
||||
pub fn load() -> io::Result<Self> {
|
||||
Self::from_fields(&Store::shared()?.read()?)
|
||||
}
|
||||
|
||||
/// Known fields, typed; a wrong value is an error.
|
||||
pub fn from_fields(fields: &Map<String, Value>) -> io::Result<Self> {
|
||||
let defaults = Self::default();
|
||||
Ok(Self {
|
||||
appearance: pick(fields, APPEARANCE, &Appearance::ALL)?.unwrap_or(defaults.appearance),
|
||||
language: pick(fields, LANGUAGE, &Language::ALL)?.unwrap_or(defaults.language),
|
||||
motion: pick(fields, MOTION, &Motion::ALL)?.unwrap_or(defaults.motion),
|
||||
omarchy_sync: match fields.get(OMARCHY_SYNC) {
|
||||
None => defaults.omarchy_sync,
|
||||
Some(Value::Bool(on)) => *on,
|
||||
Some(other) => return Err(wrong(OMARCHY_SYNC, other)),
|
||||
},
|
||||
author: match fields.get(AUTHOR) {
|
||||
None => defaults.author,
|
||||
Some(Value::String(name)) => name.trim().to_string(),
|
||||
Some(other) => return Err(wrong(AUTHOR, other)),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn pick<T: Copy>(fields: &Map<String, Value>, key: &str, all: &[(T, &'static str)]) -> io::Result<Option<T>> {
|
||||
match fields.get(key) {
|
||||
None => Ok(None),
|
||||
Some(Value::String(word)) => match all.iter().find(|(_, each)| each == word) {
|
||||
Some((choice, _)) => Ok(Some(*choice)),
|
||||
None => Err(wrong(key, &Value::String(word.clone()))),
|
||||
},
|
||||
Some(other) => Err(wrong(key, other)),
|
||||
}
|
||||
}
|
||||
|
||||
fn wrong(key: &str, value: &Value) -> io::Error {
|
||||
io::Error::new(io::ErrorKind::InvalidData, format!("settings: {key} cannot be {value}"))
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
use serde_json::json;
|
||||
|
||||
use super::{Store, VERSION};
|
||||
|
||||
fn store() -> (tempfile::TempDir, Store) {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store = Store::at(dir.path().join("ElyOffice").join("settings.json"));
|
||||
(dir, store)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_absent_file_is_empty_and_a_write_makes_it_versioned() {
|
||||
let (_dir, store) = store();
|
||||
assert!(store.read().unwrap().is_empty());
|
||||
store.set("appearance", json!("dark")).unwrap();
|
||||
let read = store.read().unwrap();
|
||||
assert_eq!(read["appearance"], "dark");
|
||||
assert_eq!(read["version"], VERSION);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_write_keeps_fields_another_app_wrote() {
|
||||
let (_dir, store) = store();
|
||||
std::fs::create_dir_all(store.path().parent().unwrap()).unwrap();
|
||||
std::fs::write(store.path(), r#"{"version":1,"later":{"kept":true},"language":"en"}"#).unwrap();
|
||||
store.set("language", json!("zh-CN")).unwrap();
|
||||
let read = store.read().unwrap();
|
||||
assert_eq!(read["language"], "zh-CN");
|
||||
assert_eq!(read["later"], json!({"kept": true}), "unknown fields stay");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_writers_at_once_lose_nothing() {
|
||||
let (_dir, store) = store();
|
||||
let writers: Vec<_> = (0..8)
|
||||
.map(|n| {
|
||||
let store = store.clone();
|
||||
std::thread::spawn(move || store.set(&format!("field{n}"), json!(n)).unwrap())
|
||||
})
|
||||
.collect();
|
||||
for writer in writers {
|
||||
writer.join().unwrap();
|
||||
}
|
||||
let read = store.read().unwrap();
|
||||
assert!((0..8).all(|n| read[&format!("field{n}")] == n), "{read:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_newer_or_broken_file_is_refused_and_left_alone() {
|
||||
let (_dir, store) = store();
|
||||
std::fs::create_dir_all(store.path().parent().unwrap()).unwrap();
|
||||
for text in [r#"{"version":2}"#, r#"{"version":"1"}"#, "[1]", "{"] {
|
||||
std::fs::write(store.path(), text).unwrap();
|
||||
assert!(store.read().is_err(), "{text} is refused");
|
||||
assert!(store.set("language", json!("en")).is_err(), "{text} is not overwritten");
|
||||
assert_eq!(std::fs::read_to_string(store.path()).unwrap(), text);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_fields_type_strictly_and_default_when_absent() {
|
||||
use super::{Appearance, Language, Motion, Shared};
|
||||
let fields = |text: &str| serde_json::from_str::<serde_json::Map<_, _>>(text).unwrap();
|
||||
assert_eq!(Shared::from_fields(&fields("{}")).unwrap(), Shared::default());
|
||||
let read = Shared::from_fields(&fields(
|
||||
r#"{"appearance":"dark","language":"zh-CN","motion":"reduce","omarchy_sync":false,"author":" Ana "}"#,
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
(read.appearance, read.language, read.motion, read.omarchy_sync, read.author.as_str()),
|
||||
(Appearance::Dark, Language::Chinese, Motion::Reduced, false, "Ana")
|
||||
);
|
||||
for bad in [r#"{"appearance":"blue"}"#, r#"{"omarchy_sync":"yes"}"#, r#"{"author":3}"#] {
|
||||
assert!(Shared::from_fields(&fields(bad)).is_err(), "{bad}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_write_overtaken_while_waiting_leaves_the_file() {
|
||||
let (_dir, store) = store();
|
||||
store.set("author", json!("Ana")).unwrap();
|
||||
let held = crate::lock::FileLock::take(&store.path().parent().unwrap().join(".settings.lock")).unwrap();
|
||||
let waiting = {
|
||||
let store = store.clone();
|
||||
std::thread::spawn(move || store.set_if("author", json!("Bob"), || false).unwrap())
|
||||
};
|
||||
std::thread::sleep(std::time::Duration::from_millis(200));
|
||||
drop(held);
|
||||
assert!(!waiting.join().unwrap(), "the overtaken write is skipped");
|
||||
assert_eq!(store.read().unwrap()["author"], "Ana");
|
||||
}
|
||||
Reference in new issue
Block a user