mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-27 02:27:18 +00:00
progress
This commit is contained in:
1 parent
2e1ca00afa
commit
1ba5436af3
29 files changed
+3348
-190
No files matched your search
+206
-34
@@ -1,48 +1,196 @@
|
||||
//! The state the commands share: the engine, and the settings of the frontend.
|
||||
//! The state the commands share: the engine, and what goes with it.
|
||||
//!
|
||||
//! The engine is not `Send` (it shares its data with `Rc`), so it lives on a thread of its own and
|
||||
//! the commands send it jobs: they are done one after the other, in the order they were sent.
|
||||
//! The engine is not `Send` (it shares its data with `Rc`), so it lives on a thread of its own with
|
||||
//! everything that works on it (its storage, the link between its files and the file system, the
|
||||
//! watcher of the open folder), and the commands send that thread jobs: they are done one after the
|
||||
//! other, in the order they were sent.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::mpsc::{self, Sender};
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use gpx_engine::Engine;
|
||||
use gpx_engine::{Engine, Persister, Storage, api};
|
||||
use notify::RecursiveMode;
|
||||
use notify_debouncer_mini::{DebounceEventResult, Debouncer, new_debouncer};
|
||||
use serde::Serialize;
|
||||
|
||||
type Job = Box<dyn FnOnce(&mut Engine) + Send>;
|
||||
use crate::store::FileStorage;
|
||||
use crate::workspace::{Workspace, WorkspaceState};
|
||||
|
||||
pub struct AppState {
|
||||
jobs: Mutex<Sender<Job>>,
|
||||
/// The settings of the frontend, as JSON strings by key.
|
||||
settings: Mutex<BTreeMap<String, String>>,
|
||||
/// Tells the frontend that something happened that it did not ask for: a name and a JSON value.
|
||||
pub type Emit = Arc<dyn Fn(&str, serde_json::Value) + Send + Sync>;
|
||||
|
||||
type Job = Box<dyn FnOnce(&mut Core) + Send>;
|
||||
|
||||
/// Everything that lives on the thread of the engine.
|
||||
pub struct Core {
|
||||
pub engine: Engine,
|
||||
pub workspace: Workspace,
|
||||
storage: Arc<FileStorage>,
|
||||
persister: Persister,
|
||||
watcher: Option<Debouncer<notify::RecommendedWatcher>>,
|
||||
jobs: Sender<Job>,
|
||||
emit: Emit,
|
||||
}
|
||||
|
||||
impl Default for AppState {
|
||||
fn default() -> Self {
|
||||
let (jobs, receiver) = mpsc::channel::<Job>();
|
||||
thread::Builder::new()
|
||||
.name("engine".into())
|
||||
.spawn(move || {
|
||||
let mut engine = Engine::default();
|
||||
while let Ok(job) = receiver.recv() {
|
||||
job(&mut engine);
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
jobs: Arc<Mutex<Sender<Job>>>,
|
||||
storage: Arc<FileStorage>,
|
||||
}
|
||||
|
||||
impl Core {
|
||||
fn start(storage: Arc<FileStorage>, jobs: Sender<Job>, emit: Emit) -> Self {
|
||||
let mut core = Self {
|
||||
engine: Engine::default(),
|
||||
workspace: Workspace::load(storage.root()),
|
||||
persister: Persister::default(),
|
||||
storage,
|
||||
watcher: None,
|
||||
jobs,
|
||||
emit,
|
||||
};
|
||||
match pollster::block_on(core.persister.restore(&*core.storage)) {
|
||||
Ok(restored) => {
|
||||
core.engine.restore(restored);
|
||||
}
|
||||
// what is stored comes from a newer version, which is left alone
|
||||
Err(gpx_engine::StorageError::Newer) => {}
|
||||
Err(error) => eprintln!("the files could not be read: {error}"),
|
||||
}
|
||||
core.workspace.prune(&core.engine);
|
||||
core.workspace.verify(&core.engine);
|
||||
// what happened on the disk while the app was closed
|
||||
core.workspace.reconcile(&mut core.engine, None);
|
||||
core.workspace.write(core.storage.root());
|
||||
core.watch();
|
||||
core.persist();
|
||||
core
|
||||
}
|
||||
|
||||
/// Writes what changed in the engine.
|
||||
pub fn persist(&mut self) {
|
||||
match pollster::block_on(self.persister.save(&*self.storage, self.engine.snapshot())) {
|
||||
Ok(()) | Err(gpx_engine::CommitError::Failed(gpx_engine::StorageError::Newer)) => {}
|
||||
Err(error) => eprintln!("the files could not be kept: {error}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens files that the system gave to the app (open with..., double click, the command line),
|
||||
/// which get selected. The frontend is told, if it already runs: when it starts it reads what
|
||||
/// the engine has.
|
||||
pub fn open_external(&mut self, paths: &[PathBuf]) {
|
||||
if paths.is_empty() {
|
||||
return;
|
||||
}
|
||||
let outcome = self.workspace.open_files(&mut self.engine, paths, true);
|
||||
self.keep_workspace();
|
||||
self.persist();
|
||||
(self.emit)("files-opened", to_json(&outcome));
|
||||
self.announce(None);
|
||||
}
|
||||
|
||||
pub fn keep_workspace(&self) {
|
||||
self.workspace.write(self.storage.root());
|
||||
}
|
||||
|
||||
pub fn workspace_state(&self) -> WorkspaceState {
|
||||
self.workspace.state(&self.engine)
|
||||
}
|
||||
|
||||
/// Tells the frontend what changed without it asking, and where the files are.
|
||||
pub fn announce(&self, outcome: Option<&api::Outcome>) {
|
||||
if let Some(outcome) = outcome {
|
||||
(self.emit)("engine-changed", to_json(outcome));
|
||||
}
|
||||
(self.emit)("workspace-changed", to_json(&self.workspace_state()));
|
||||
}
|
||||
|
||||
/// Watches the open folder (and only it).
|
||||
pub fn watch(&mut self) {
|
||||
self.watcher = None;
|
||||
let Some(folder) = self.workspace.folder.clone() else {
|
||||
return;
|
||||
};
|
||||
if !Path::new(&folder).is_dir() {
|
||||
return;
|
||||
}
|
||||
let jobs = self.jobs.clone();
|
||||
let debouncer = new_debouncer(
|
||||
Duration::from_millis(400),
|
||||
move |result: DebounceEventResult| {
|
||||
let Ok(events) = result else { return };
|
||||
let paths: Vec<PathBuf> = events.into_iter().map(|event| event.path).collect();
|
||||
let _ = jobs.send(Box::new(move |core: &mut Core| {
|
||||
core.on_disk_changed(&paths)
|
||||
}));
|
||||
},
|
||||
);
|
||||
match debouncer {
|
||||
Ok(mut debouncer) => {
|
||||
match debouncer.watcher().watch(&folder, RecursiveMode::Recursive) {
|
||||
Ok(()) => self.watcher = Some(debouncer),
|
||||
Err(error) => eprintln!("the folder cannot be watched: {error}"),
|
||||
}
|
||||
})
|
||||
.expect("the engine thread could not be started");
|
||||
Self {
|
||||
jobs: Mutex::new(jobs),
|
||||
settings: Mutex::default(),
|
||||
}
|
||||
Err(error) => eprintln!("the folder cannot be watched: {error}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_disk_changed(&mut self, paths: &[PathBuf]) {
|
||||
let outcome = self.workspace.reconcile(&mut self.engine, Some(paths));
|
||||
if let Some(outcome) = outcome {
|
||||
self.workspace.write(self.storage.root());
|
||||
self.persist();
|
||||
self.announce(Some(&outcome));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn to_json<T: Serialize>(value: &T) -> serde_json::Value {
|
||||
serde_json::to_value(value).unwrap_or_default()
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
/// Runs `f` on the engine and waits for its result.
|
||||
pub fn run<T: Send + 'static>(&self, f: impl FnOnce(&mut Engine) -> T + Send + 'static) -> T {
|
||||
/// Starts the engine, which restores what was kept in `directory`.
|
||||
pub fn open(directory: impl Into<PathBuf>, emit: Emit) -> Result<Self, String> {
|
||||
let storage = Arc::new(FileStorage::open(directory).map_err(|error| error.to_string())?);
|
||||
let (jobs, receiver) = mpsc::channel::<Job>();
|
||||
let (thread_storage, thread_jobs) = (storage.clone(), jobs.clone());
|
||||
thread::Builder::new()
|
||||
.name("engine".into())
|
||||
.spawn(move || {
|
||||
let mut core = Core::start(thread_storage, thread_jobs, emit);
|
||||
while let Ok(job) = receiver.recv() {
|
||||
job(&mut core);
|
||||
}
|
||||
})
|
||||
.map_err(|error| error.to_string())?;
|
||||
Ok(Self {
|
||||
jobs: Arc::new(Mutex::new(jobs)),
|
||||
storage,
|
||||
})
|
||||
}
|
||||
|
||||
/// An engine that keeps its data in a directory of its own, and has nobody to tell.
|
||||
#[cfg(test)]
|
||||
pub fn temporary() -> Self {
|
||||
let directory = std::env::temp_dir().join(format!("gpx-studio-{}", uuid::Uuid::new_v4()));
|
||||
Self::open(directory, Arc::new(|_, _| {})).expect("the storage can be opened")
|
||||
}
|
||||
|
||||
/// Opens files that the system gave to the app, see `Core::open_external`.
|
||||
pub fn open_external(&self, paths: Vec<PathBuf>) {
|
||||
self.core(move |core| core.open_external(&paths));
|
||||
}
|
||||
|
||||
/// Runs `f` on the thread of the engine and waits for its result.
|
||||
pub fn core<T: Send + 'static>(&self, f: impl FnOnce(&mut Core) -> T + Send + 'static) -> T {
|
||||
let (reply, result) = mpsc::channel();
|
||||
let job: Job = Box::new(move |engine| {
|
||||
let _ = reply.send(f(engine));
|
||||
let job: Job = Box::new(move |core| {
|
||||
let _ = reply.send(f(core));
|
||||
});
|
||||
self.jobs
|
||||
.lock()
|
||||
@@ -52,12 +200,36 @@ impl AppState {
|
||||
result.recv().expect("the engine thread stopped")
|
||||
}
|
||||
|
||||
/// Reads from the engine.
|
||||
pub fn read<T: Send + 'static>(&self, f: impl FnOnce(&Engine) -> T + Send + 'static) -> T {
|
||||
self.run(move |engine| f(engine))
|
||||
/// Runs `f` on the engine, which can change: what changed is kept.
|
||||
pub fn run<T: Send + 'static>(&self, f: impl FnOnce(&mut Engine) -> T + Send + 'static) -> T {
|
||||
self.core(move |core| {
|
||||
let result = f(&mut core.engine);
|
||||
core.persist();
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
pub fn settings(&self) -> MutexGuard<'_, BTreeMap<String, String>> {
|
||||
self.settings.lock().unwrap()
|
||||
/// Reads from the engine.
|
||||
pub fn read<T: Send + 'static>(&self, f: impl FnOnce(&Engine) -> T + Send + 'static) -> T {
|
||||
self.core(move |core| f(&core.engine))
|
||||
}
|
||||
|
||||
pub fn storage(&self) -> &FileStorage {
|
||||
&self.storage
|
||||
}
|
||||
}
|
||||
|
||||
// the settings only are read and written by the commands, from any thread
|
||||
impl AppState {
|
||||
pub fn put_setting(&self, key: &str, value: &str) {
|
||||
if let Err(error) = pollster::block_on(self.storage.put_setting(key, value)) {
|
||||
eprintln!("the setting could not be kept: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_setting(&self, key: &str) {
|
||||
if let Err(error) = pollster::block_on(self.storage.delete_setting(key)) {
|
||||
eprintln!("the setting could not be kept: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user