programmaticaly control window and channel events

This commit is contained in:
Nikita Galaiko 2023-02-22 11:49:25 +01:00
parent 76b0989ece
commit 3c42525a13
No known key found for this signature in database
GPG Key ID: EBAB54E845BA519D
6 changed files with 192 additions and 120 deletions

View File

@ -1,41 +1,34 @@
use crate::{deltas, projects, sessions}; use crate::{deltas, projects, sessions};
use serde;
pub fn session<R: tauri::Runtime>( #[derive(Debug)]
window: &tauri::Window<R>, pub struct Event {
project: &projects::Project, pub name: String,
session: &sessions::Session, pub payload: serde_json::Value,
) {
let event_name = format!("project://{}/sessions", project.id);
match window.emit(&event_name, &session) {
Ok(_) => {}
Err(e) => log::error!("Error: {:?}", e),
};
} }
#[derive(serde::Serialize, Debug)] impl Event {
#[serde(rename_all = "camelCase")] pub fn session(project: &projects::Project, session: &sessions::Session) -> Self {
struct DeltasEvent { let event_name = format!("project://{}/sessions", project.id);
file_path: String, Event {
deltas: Vec<deltas::Delta>, name: event_name,
} payload: serde_json::to_value(session).unwrap(),
}
}
pub fn deltas<R: tauri::Runtime>( pub fn detlas(
window: &tauri::Window<R>, project: &projects::Project,
project: &projects::Project, session: &sessions::Session,
session: &sessions::Session, deltas: &Vec<deltas::Delta>,
deltas: &Vec<deltas::Delta>, relative_file_path: &std::path::Path,
relative_file_path: &std::path::Path, ) -> Self {
) { let event_name = format!("project://{}/deltas/{}", project.id, session.id);
let event_name = format!("project://{}/deltas/{}", project.id, session.id); let payload = serde_json::json!({
match window.emit( "deltas": deltas,
&event_name, "relative_file_path": relative_file_path,
&DeltasEvent { });
deltas: deltas.clone(), Event {
file_path: relative_file_path.to_str().unwrap().to_string(), name: event_name,
}, payload,
) { }
Ok(_) => {} }
Err(e) => log::error!("Error: {:?}", e),
};
} }

View File

@ -8,12 +8,13 @@ mod storage;
mod users; mod users;
mod watchers; mod watchers;
use anyhow::Result;
use deltas::Delta; use deltas::Delta;
use log; use log;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::{collections::HashMap, sync::mpsc, thread};
use storage::Storage; use storage::Storage;
use tauri::{Manager, Window}; use tauri::Manager;
use tauri_plugin_log::{ use tauri_plugin_log::{
fern::colors::{Color, ColoredLevelConfig}, fern::colors::{Color, ColoredLevelConfig},
LogTarget, LogTarget,
@ -124,11 +125,7 @@ fn update_project(
} }
#[tauri::command] #[tauri::command]
fn add_project( fn add_project(handle: tauri::AppHandle, path: &str) -> Result<projects::Project, Error> {
handle: tauri::AppHandle,
window: Window,
path: &str,
) -> Result<projects::Project, Error> {
let path_resolver = handle.path_resolver(); let path_resolver = handle.path_resolver();
let storage = storage::Storage::from_path_resolver(&path_resolver); let storage = storage::Storage::from_path_resolver(&path_resolver);
let projects_storage = projects::Storage::new(storage.clone()); let projects_storage = projects::Storage::new(storage.clone());
@ -178,12 +175,19 @@ fn add_project(
message: "Failed to add project".to_string(), message: "Failed to add project".to_string(),
} }
})?; })?;
watchers.watch(window, &project).map_err(|e| {
let (tx, rx): (mpsc::Sender<events::Event>, mpsc::Receiver<events::Event>) =
mpsc::channel();
watchers.watch(tx, &project).map_err(|e| {
log::error!("{}", e); log::error!("{}", e);
Error { Error {
message: "Failed to watch project".to_string(), message: "Failed to watch project".to_string(),
} }
})?; })?;
watch_events(handle, rx);
return Ok(project); return Ok(project);
} else { } else {
return Err(Error { return Err(Error {
@ -341,51 +345,59 @@ fn main() {
tauri::Builder::default() tauri::Builder::default()
.system_tray(tray) .system_tray(tray)
.on_window_event(|event| match event.event() { .on_system_tray_event(|app_handle, event| match 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 {
tauri::SystemTrayEvent::MenuItemClick { id, .. } => { 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() { match id.as_str() {
"quit" => { "quit" => {
app.exit(0); app_handle.exit(0);
} }
"toggle" => { "toggle" => match get_window(&app_handle) {
let main_window = app.get_window("main").unwrap(); Some(window) => {
if main_window.is_visible().unwrap() { if window.is_visible().unwrap() {
main_window.hide().unwrap(); window.hide().unwrap();
item_handle item_handle
.set_title(format!("Show {}", app_title())) .set_title(format!("Show {}", app_title()))
.unwrap(); .unwrap();
} else { } else {
main_window.show().unwrap(); window.show().unwrap();
main_window.set_focus().unwrap(); item_handle
.set_title(format!("Hide {}", app_title()))
.unwrap();
}
}
None => {
create_window(&app_handle).expect("Failed to create window");
item_handle item_handle
.set_title(format!("Hide {}", app_title())) .set_title(format!("Hide {}", app_title()))
.unwrap(); .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| { .setup(move |app| {
#[cfg(target_os = "macos")] let window = create_window(&app.handle()).expect("Failed to create window");
app.set_activation_policy(tauri::ActivationPolicy::Accessory); #[cfg(debug_assertions)]
window.open_devtools();
let resolver = app.path_resolver(); let resolver = app.path_resolver();
log::info!( log::info!(
@ -403,18 +415,20 @@ fn main() {
users_storage.clone(), users_storage.clone(),
); );
let (tx, rx): (mpsc::Sender<events::Event>, mpsc::Receiver<events::Event>) =
mpsc::channel();
if let Ok(projects) = projects_storage.list_projects() { if let Ok(projects) = projects_storage.list_projects() {
for project in projects { for project in projects {
watchers watchers
.watch(app.get_window("main").unwrap(), &project) .watch(tx.clone(), &project)
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
} }
} else { } else {
log::error!("Failed to list projects"); log::error!("Failed to list projects");
} }
#[cfg(debug_assertions)] watch_events(app.handle(), rx);
app.get_window("main").unwrap().open_devtools();
app.manage(watcher_collection); app.manage(watcher_collection);
@ -444,8 +458,62 @@ fn main() {
delete_user, delete_user,
get_user get_user
]) ])
.run(tauri::generate_context!()) .build(tauri::generate_context!())
.expect("error while running tauri application") .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<events::Event>) {
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<tauri::Window> {
handle.get_window("main")
}
fn create_window(handle: &tauri::AppHandle) -> tauri::Result<tauri::Window> {
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(()),
}
}

View File

@ -6,7 +6,7 @@ use git2;
use notify::{Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; use notify::{Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use std::fs; use std::fs;
use std::path::Path; use std::path::Path;
use std::sync::mpsc::channel; use std::sync::mpsc;
use std::thread; use std::thread;
use std::{collections::HashMap, sync::Mutex}; use std::{collections::HashMap, sync::Mutex};
@ -22,11 +22,15 @@ impl<'a> DeltaWatchers<'a> {
Self { watchers } Self { watchers }
} }
pub fn watch(&self, window: tauri::Window, project: projects::Project) -> Result<()> { pub fn watch(
&self,
sender: mpsc::Sender<events::Event>,
project: projects::Project,
) -> Result<()> {
log::info!("Watching deltas for {}", project.path); log::info!("Watching deltas for {}", project.path);
let project_path = Path::new(&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())?; let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
watcher.watch(project_path, RecursiveMode::Recursive)?; watcher.watch(project_path, RecursiveMode::Recursive)?;
@ -38,6 +42,7 @@ impl<'a> DeltaWatchers<'a> {
.insert(project.path.clone(), watcher); .insert(project.path.clone(), watcher);
let repo = git2::Repository::open(project_path); let repo = git2::Repository::open(project_path);
thread::spawn(move || { thread::spawn(move || {
if repo.is_err() { if repo.is_err() {
log::error!("failed to open git repo: {:?}", repo.err()); log::error!("failed to open git repo: {:?}", repo.err());
@ -51,20 +56,32 @@ impl<'a> DeltaWatchers<'a> {
let relative_file_path = let relative_file_path =
file_path.strip_prefix(repo.workdir().unwrap()).unwrap(); file_path.strip_prefix(repo.workdir().unwrap()).unwrap();
match register_file_change( match register_file_change(
&window,
&project, &project,
&repo, &repo,
&event.kind, &event.kind,
&relative_file_path, &relative_file_path,
) { ) {
Ok(Some((session, deltas))) => { Ok(Some((session, deltas))) => {
events::deltas( match sender.send(events::Event::session(&project, &session)) {
&window, Err(e) => log::error!("filed to send session event: {:?}", e),
&project, Ok(_) => {}
&session, }
&deltas, match deltas {
&relative_file_path, 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) => {} Ok(None) => {}
Err(e) => log::error!("Error: {:?}", e), 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 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 // it also writes the metadata stuff which marks the beginning of a session if a session is not yet started
// returns updated project deltas // returns updated project deltas
fn register_file_change<R: tauri::Runtime>( fn register_file_change(
window: &tauri::Window<R>,
project: &projects::Project, project: &projects::Project,
repo: &git2::Repository, repo: &git2::Repository,
kind: &EventKind, kind: &EventKind,
relative_file_path: &Path, relative_file_path: &Path,
) -> Result<Option<(sessions::Session, Vec<Delta>)>, Box<dyn std::error::Error>> { ) -> Result<Option<(sessions::Session, Option<Vec<Delta>>)>, Box<dyn std::error::Error>> {
if repo.is_path_ignored(&relative_file_path).unwrap_or(true) { if repo.is_path_ignored(&relative_file_path).unwrap_or(true) {
// make sure we're not watching ignored files // make sure we're not watching ignored files
return Ok(None); return Ok(None);
@ -121,7 +137,7 @@ fn register_file_change<R: tauri::Runtime>(
}; };
// update meta files every time file change is detected // 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) { if EventKind::is_modify(&kind) {
log::info!("File modified: {:?}", file_path); log::info!("File modified: {:?}", file_path);
@ -157,13 +173,13 @@ fn register_file_change<R: tauri::Runtime>(
}; };
if !text_doc.update(&file_contents) { if !text_doc.update(&file_contents) {
return Ok(None); return Ok(Some((session, None)));
} }
// if the file was modified, save the deltas // if the file was modified, save the deltas
let deltas = text_doc.get_deltas(); let deltas = text_doc.get_deltas();
write(repo, project, relative_file_path, &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 // 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 // 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 // and also touches the last activity timestamp, so we can tell when we are idle
fn write_beginning_meta_files<R: tauri::Runtime>( fn write_beginning_meta_files(
window: &tauri::Window<R>,
project: &projects::Project, project: &projects::Project,
repo: &git2::Repository, repo: &git2::Repository,
) -> Result<sessions::Session, Box<dyn std::error::Error>> { ) -> Result<sessions::Session, Box<dyn std::error::Error>> {
@ -236,12 +251,10 @@ fn write_beginning_meta_files<R: tauri::Runtime>(
session session
.update(project) .update(project)
.with_context(|| "failed to update session")?; .with_context(|| "failed to update session")?;
events::session(&window, &project, &session);
Ok(session) Ok(session)
} }
None => { None => {
let session = sessions::Session::from_head(repo, project)?; let session = sessions::Session::from_head(repo, project)?;
events::session(&window, &project, &session);
Ok(session) Ok(session)
} }
} }

View File

@ -2,6 +2,7 @@ use crate::{events, projects, sessions, users};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use git2::Repository; use git2::Repository;
use std::{ use std::{
sync::mpsc,
thread, thread,
time::{Duration, SystemTime}, 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<events::Event>,
project: projects::Project,
) -> Result<()> {
log::info!("Watching git for {}", project.path); log::info!("Watching git for {}", project.path);
let shared_self = std::sync::Arc::new(self.clone()); let shared_self = std::sync::Arc::new(self.clone());
@ -62,7 +67,10 @@ impl GitWatcher {
match local_self.check_for_changes(&project, &user) { match local_self.check_for_changes(&project, &user) {
Ok(Some(session)) => { 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) => {} Ok(None) => {}
Err(error) => { Err(error) => {

View File

@ -2,9 +2,9 @@ mod delta;
mod git; mod git;
pub use self::delta::WatcherCollection; pub use self::delta::WatcherCollection;
use crate::{projects, users}; use crate::{events, projects, users};
use anyhow::Result; use anyhow::Result;
use tauri; use std::sync::mpsc;
pub struct Watcher<'a> { pub struct Watcher<'a> {
git_watcher: git::GitWatcher, git_watcher: git::GitWatcher,
@ -25,9 +25,13 @@ impl<'a> Watcher<'a> {
} }
} }
pub fn watch(&self, window: tauri::Window, project: &projects::Project) -> Result<()> { pub fn watch(
self.delta_watcher.watch(window.clone(), project.clone())?; &self,
self.git_watcher.watch(window.clone(), project.clone())?; sender: mpsc::Sender<events::Event>,
project: &projects::Project,
) -> Result<()> {
self.delta_watcher.watch(sender.clone(), project.clone())?;
self.git_watcher.watch(sender.clone(), project.clone())?;
Ok(()) Ok(())
} }

View File

@ -45,20 +45,6 @@
"security": { "security": {
"csp": null "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": { "systemTray": {
"iconPath": "icons/tray.png", "iconPath": "icons/tray.png",
"iconAsTemplate": true "iconAsTemplate": true