From 3c42525a13a702e733937054c354f74d274d5d31 Mon Sep 17 00:00:00 2001 From: Nikita Galaiko Date: Wed, 22 Feb 2023 11:49:25 +0100 Subject: [PATCH] programmaticaly control window and channel events --- src-tauri/src/events.rs | 63 ++++++------- src-tauri/src/main.rs | 154 +++++++++++++++++++++++--------- src-tauri/src/watchers/delta.rs | 55 +++++++----- src-tauri/src/watchers/git.rs | 12 ++- src-tauri/src/watchers/mod.rs | 14 +-- src-tauri/tauri.conf.json | 14 --- 6 files changed, 192 insertions(+), 120 deletions(-) diff --git a/src-tauri/src/events.rs b/src-tauri/src/events.rs index 99f40f455..9eda258b6 100644 --- a/src-tauri/src/events.rs +++ b/src-tauri/src/events.rs @@ -1,41 +1,34 @@ use crate::{deltas, projects, sessions}; -use serde; -pub fn session( - window: &tauri::Window, - project: &projects::Project, - session: &sessions::Session, -) { - let event_name = format!("project://{}/sessions", project.id); - match window.emit(&event_name, &session) { - Ok(_) => {} - Err(e) => log::error!("Error: {:?}", e), - }; +#[derive(Debug)] +pub struct Event { + pub name: String, + pub payload: serde_json::Value, } -#[derive(serde::Serialize, Debug)] -#[serde(rename_all = "camelCase")] -struct DeltasEvent { - file_path: String, - deltas: Vec, -} +impl Event { + pub fn session(project: &projects::Project, session: &sessions::Session) -> Self { + let event_name = format!("project://{}/sessions", project.id); + Event { + name: event_name, + payload: serde_json::to_value(session).unwrap(), + } + } -pub fn deltas( - window: &tauri::Window, - project: &projects::Project, - session: &sessions::Session, - deltas: &Vec, - relative_file_path: &std::path::Path, -) { - let event_name = format!("project://{}/deltas/{}", project.id, session.id); - match window.emit( - &event_name, - &DeltasEvent { - deltas: deltas.clone(), - file_path: relative_file_path.to_str().unwrap().to_string(), - }, - ) { - Ok(_) => {} - Err(e) => log::error!("Error: {:?}", e), - }; + pub fn detlas( + project: &projects::Project, + session: &sessions::Session, + deltas: &Vec, + relative_file_path: &std::path::Path, + ) -> Self { + let event_name = format!("project://{}/deltas/{}", project.id, session.id); + let payload = serde_json::json!({ + "deltas": deltas, + "relative_file_path": relative_file_path, + }); + Event { + name: event_name, + payload, + } + } } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 692c0d58b..cb808cf85 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -8,12 +8,13 @@ mod storage; mod users; mod watchers; +use anyhow::Result; use deltas::Delta; use log; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use std::{collections::HashMap, sync::mpsc, thread}; use storage::Storage; -use tauri::{Manager, Window}; +use tauri::Manager; use tauri_plugin_log::{ fern::colors::{Color, ColoredLevelConfig}, LogTarget, @@ -124,11 +125,7 @@ fn update_project( } #[tauri::command] -fn add_project( - handle: tauri::AppHandle, - window: Window, - path: &str, -) -> Result { +fn add_project(handle: tauri::AppHandle, path: &str) -> Result { let path_resolver = handle.path_resolver(); let storage = storage::Storage::from_path_resolver(&path_resolver); let projects_storage = projects::Storage::new(storage.clone()); @@ -178,12 +175,19 @@ fn add_project( message: "Failed to add project".to_string(), } })?; - watchers.watch(window, &project).map_err(|e| { + + let (tx, rx): (mpsc::Sender, mpsc::Receiver) = + mpsc::channel(); + + watchers.watch(tx, &project).map_err(|e| { log::error!("{}", e); Error { message: "Failed to watch project".to_string(), } })?; + + watch_events(handle, rx); + return Ok(project); } else { return Err(Error { @@ -341,51 +345,59 @@ fn main() { tauri::Builder::default() .system_tray(tray) - .on_window_event(|event| match event.event() { - // Hide window instead of closing. - tauri::WindowEvent::CloseRequested { api, .. } => { - api.prevent_close(); - event.window().hide().unwrap(); - event - .window() - .app_handle() - .tray_handle() - .get_item("toggle") - .set_title(format!("Show {}", app_title())) - .unwrap(); - } - _ => {} - }) - .on_system_tray_event(|app, event| match event { + .on_system_tray_event(|app_handle, event| match event { tauri::SystemTrayEvent::MenuItemClick { id, .. } => { - let item_handle = app.tray_handle().get_item(&id); + let item_handle = app_handle.tray_handle().get_item(&id); match id.as_str() { "quit" => { - app.exit(0); + app_handle.exit(0); } - "toggle" => { - let main_window = app.get_window("main").unwrap(); - if main_window.is_visible().unwrap() { - main_window.hide().unwrap(); - item_handle - .set_title(format!("Show {}", app_title())) - .unwrap(); - } else { - main_window.show().unwrap(); - main_window.set_focus().unwrap(); + "toggle" => match get_window(&app_handle) { + Some(window) => { + if window.is_visible().unwrap() { + window.hide().unwrap(); + item_handle + .set_title(format!("Show {}", app_title())) + .unwrap(); + } else { + window.show().unwrap(); + item_handle + .set_title(format!("Hide {}", app_title())) + .unwrap(); + } + } + None => { + create_window(&app_handle).expect("Failed to create window"); item_handle .set_title(format!("Hide {}", app_title())) .unwrap(); } - } + }, _ => {} } } _ => {} }) + .on_window_event(|event| match event.event() { + tauri::WindowEvent::CloseRequested { api, .. } => { + api.prevent_close(); + let window = event.window(); + + window + .app_handle() + .tray_handle() + .get_item("toggle") + .set_title(format!("Show {}", app_title())) + .expect("Failed to set tray item title"); + + window.hide().expect("Failed to hide window"); + } + _ => {} + }) .setup(move |app| { - #[cfg(target_os = "macos")] - app.set_activation_policy(tauri::ActivationPolicy::Accessory); + let window = create_window(&app.handle()).expect("Failed to create window"); + #[cfg(debug_assertions)] + window.open_devtools(); let resolver = app.path_resolver(); log::info!( @@ -403,18 +415,20 @@ fn main() { users_storage.clone(), ); + let (tx, rx): (mpsc::Sender, mpsc::Receiver) = + mpsc::channel(); + if let Ok(projects) = projects_storage.list_projects() { for project in projects { watchers - .watch(app.get_window("main").unwrap(), &project) + .watch(tx.clone(), &project) .map_err(|e| e.to_string())?; } } else { log::error!("Failed to list projects"); } - #[cfg(debug_assertions)] - app.get_window("main").unwrap().open_devtools(); + watch_events(app.handle(), rx); app.manage(watcher_collection); @@ -444,8 +458,62 @@ fn main() { delete_user, get_user ]) - .run(tauri::generate_context!()) + .build(tauri::generate_context!()) .expect("error while running tauri application") + .run(|app_handle, event| match event { + tauri::RunEvent::ExitRequested { api, .. } => { + hide_window(&app_handle).expect("Failed to hide window"); + api.prevent_exit(); + } + _ => {} + }); }, ); } + +fn watch_events(handle: tauri::AppHandle, rx: mpsc::Receiver) { + thread::spawn(move || { + while let Ok(event) = rx.recv() { + if let Some(window) = handle.get_window("main") { + match window.emit(&event.name, event.payload) { + Err(e) => log::error!("Failed to emit event: {}", e), + _ => {} + } + } + } + }); +} + +fn get_window(handle: &tauri::AppHandle) -> Option { + handle.get_window("main") +} + +fn create_window(handle: &tauri::AppHandle) -> tauri::Result { + log::info!("Creating window"); + tauri::WindowBuilder::new(handle, "main", tauri::WindowUrl::App("index.html".into())) + .resizable(true) + .hidden_title(true) + .theme(Some(tauri::Theme::Dark)) + .min_inner_size(600.0, 300.0) + .inner_size(800.0, 600.0) + .title_bar_style(tauri::TitleBarStyle::Overlay) + .build() +} + +fn hide_window(handle: &tauri::AppHandle) -> tauri::Result<()> { + handle + .tray_handle() + .get_item("toggle") + .set_title(format!("Show {}", app_title()))?; + + match get_window(handle) { + Some(window) => { + if window.is_visible()? { + window.hide() + } else { + Ok(()) + } + } + None => Ok(()), + } +} diff --git a/src-tauri/src/watchers/delta.rs b/src-tauri/src/watchers/delta.rs index ce337a624..e374dfd2b 100644 --- a/src-tauri/src/watchers/delta.rs +++ b/src-tauri/src/watchers/delta.rs @@ -6,7 +6,7 @@ use git2; use notify::{Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; use std::fs; use std::path::Path; -use std::sync::mpsc::channel; +use std::sync::mpsc; use std::thread; use std::{collections::HashMap, sync::Mutex}; @@ -22,11 +22,15 @@ impl<'a> DeltaWatchers<'a> { Self { watchers } } - pub fn watch(&self, window: tauri::Window, project: projects::Project) -> Result<()> { + pub fn watch( + &self, + sender: mpsc::Sender, + project: projects::Project, + ) -> Result<()> { log::info!("Watching deltas for {}", project.path); let project_path = Path::new(&project.path); - let (tx, rx) = channel(); + let (tx, rx) = mpsc::channel(); let mut watcher = RecommendedWatcher::new(tx, Config::default())?; watcher.watch(project_path, RecursiveMode::Recursive)?; @@ -38,6 +42,7 @@ impl<'a> DeltaWatchers<'a> { .insert(project.path.clone(), watcher); let repo = git2::Repository::open(project_path); + thread::spawn(move || { if repo.is_err() { log::error!("failed to open git repo: {:?}", repo.err()); @@ -51,20 +56,32 @@ impl<'a> DeltaWatchers<'a> { let relative_file_path = file_path.strip_prefix(repo.workdir().unwrap()).unwrap(); match register_file_change( - &window, &project, &repo, &event.kind, &relative_file_path, ) { Ok(Some((session, deltas))) => { - events::deltas( - &window, - &project, - &session, - &deltas, - &relative_file_path, - ); + match sender.send(events::Event::session(&project, &session)) { + Err(e) => log::error!("filed to send session event: {:?}", e), + Ok(_) => {} + } + match deltas { + Some(deltas) => { + match sender.send(events::Event::detlas( + &project, + &session, + &deltas, + &relative_file_path, + )) { + Err(e) => { + log::error!("filed to send deltas event: {:?}", e) + } + Ok(_) => {} + } + } + None => {} + } } Ok(None) => {} Err(e) => log::error!("Error: {:?}", e), @@ -92,13 +109,12 @@ impl<'a> DeltaWatchers<'a> { // it should figure out delta data (crdt) and update the file at .git/gb/session/deltas/path/to/file // it also writes the metadata stuff which marks the beginning of a session if a session is not yet started // returns updated project deltas -fn register_file_change( - window: &tauri::Window, +fn register_file_change( project: &projects::Project, repo: &git2::Repository, kind: &EventKind, relative_file_path: &Path, -) -> Result)>, Box> { +) -> Result>)>, Box> { if repo.is_path_ignored(&relative_file_path).unwrap_or(true) { // make sure we're not watching ignored files return Ok(None); @@ -121,7 +137,7 @@ fn register_file_change( }; // update meta files every time file change is detected - let session = write_beginning_meta_files(&window, &project, &repo)?; + let session = write_beginning_meta_files(&project, &repo)?; if EventKind::is_modify(&kind) { log::info!("File modified: {:?}", file_path); @@ -157,13 +173,13 @@ fn register_file_change( }; if !text_doc.update(&file_contents) { - return Ok(None); + return Ok(Some((session, None))); } // if the file was modified, save the deltas let deltas = text_doc.get_deltas(); write(repo, project, relative_file_path, &deltas)?; - return Ok(Some((session, deltas))); + return Ok(Some((session, Some(deltas)))); } // returns last commited file contents from refs/gitbutler/current ref @@ -224,8 +240,7 @@ fn get_latest_file_contents( // this function is called when the user modifies a file, it writes starting metadata if not there // and also touches the last activity timestamp, so we can tell when we are idle -fn write_beginning_meta_files( - window: &tauri::Window, +fn write_beginning_meta_files( project: &projects::Project, repo: &git2::Repository, ) -> Result> { @@ -236,12 +251,10 @@ fn write_beginning_meta_files( session .update(project) .with_context(|| "failed to update session")?; - events::session(&window, &project, &session); Ok(session) } None => { let session = sessions::Session::from_head(repo, project)?; - events::session(&window, &project, &session); Ok(session) } } diff --git a/src-tauri/src/watchers/git.rs b/src-tauri/src/watchers/git.rs index 2744bea58..ea40c44aa 100644 --- a/src-tauri/src/watchers/git.rs +++ b/src-tauri/src/watchers/git.rs @@ -2,6 +2,7 @@ use crate::{events, projects, sessions, users}; use anyhow::{Context, Result}; use git2::Repository; use std::{ + sync::mpsc, thread, time::{Duration, SystemTime}, }; @@ -23,7 +24,11 @@ impl GitWatcher { } } - pub fn watch(&self, window: tauri::Window, project: projects::Project) -> Result<()> { + pub fn watch( + &self, + sender: mpsc::Sender, + project: projects::Project, + ) -> Result<()> { log::info!("Watching git for {}", project.path); let shared_self = std::sync::Arc::new(self.clone()); @@ -62,7 +67,10 @@ impl GitWatcher { match local_self.check_for_changes(&project, &user) { Ok(Some(session)) => { - events::session(&window, &project, &session); + match sender.send(events::Event::session(&project, &session)) { + Err(e) => log::error!("filed to send session event: {:?}", e), + Ok(_) => {} + } } Ok(None) => {} Err(error) => { diff --git a/src-tauri/src/watchers/mod.rs b/src-tauri/src/watchers/mod.rs index 934d4458d..14dda9410 100644 --- a/src-tauri/src/watchers/mod.rs +++ b/src-tauri/src/watchers/mod.rs @@ -2,9 +2,9 @@ mod delta; mod git; pub use self::delta::WatcherCollection; -use crate::{projects, users}; +use crate::{events, projects, users}; use anyhow::Result; -use tauri; +use std::sync::mpsc; pub struct Watcher<'a> { git_watcher: git::GitWatcher, @@ -25,9 +25,13 @@ impl<'a> Watcher<'a> { } } - pub fn watch(&self, window: tauri::Window, project: &projects::Project) -> Result<()> { - self.delta_watcher.watch(window.clone(), project.clone())?; - self.git_watcher.watch(window.clone(), project.clone())?; + pub fn watch( + &self, + sender: mpsc::Sender, + project: &projects::Project, + ) -> Result<()> { + self.delta_watcher.watch(sender.clone(), project.clone())?; + self.git_watcher.watch(sender.clone(), project.clone())?; Ok(()) } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 5bc4e8580..0bbc5e4fe 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -45,20 +45,6 @@ "security": { "csp": null }, - "windows": [ - { - "fullscreen": false, - "height": 600, - "resizable": true, - "title": "git-butler-tauri", - "width": 800, - "hiddenTitle": true, - "theme": "Dark", - "titleBarStyle": "Overlay", - "minWidth": 600, - "minHeight": 300 - } - ], "systemTray": { "iconPath": "icons/tray.png", "iconAsTemplate": true