#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] use notify::{Event, RecursiveMode, Watcher}; use std::path::Path; use std::time::{Duration, Instant}; use tray_icon::{ menu::{Menu, MenuItem}, Icon, TrayIconBuilder, }; use winit::event::StartCause; use winit::{ application::ApplicationHandler, event::WindowEvent, event_loop::{ActiveEventLoop, ControlFlow, EventLoop}, }; mod filesync; mod git; const PROTECT_PREFIX: &str = ".git"; // only at first directory level const SOURCE_VAULT: &str = "D:\\Cloud\\Syncthing\\obsidian-vault"; // syncthing vault folder const REPO_VAULT: &str = "C:\\Users\\robin\\AppData\\Roaming\\obsidian-git-backup\\obsidian-vault-clone"; // git repo folder const GIT_COMMIT_DELAY_AFTER_FILE_CHANGE: Duration = Duration::from_secs(30 * 60); // 30 min #[derive(Debug)] enum UserEvent { MenuEvent(tray_icon::menu::MenuEvent), FileEvent(Event), } #[derive(Default)] struct App { tray_icon: Option, menu_changed_status: Option, menu_backup_now: Option, menu_exit: Option, last_change_time: Option, current_change_count: usize, } impl App { fn create_tray_icon_menu(&mut self) { // Create and show the tray icon let menu_item_changed_status = MenuItem::new("Changed: 0", true, None); let menu_item_backup_now = MenuItem::new("Backup Now", true, None); let menu_item_exit = MenuItem::new("Exit", true, None); let tray_menu = Menu::new(); tray_menu.append(&menu_item_changed_status).unwrap(); tray_menu.append(&menu_item_backup_now).unwrap(); tray_menu.append(&menu_item_exit).unwrap(); let icon_path = Path::new(".\\sync.ico"); let icon = Icon::from_path(icon_path, None).unwrap(); let tray_icon = TrayIconBuilder::new() .with_menu(Box::new(tray_menu)) .with_tooltip("Obsidian Git Backup") .with_icon(icon) .build() .unwrap(); self.tray_icon = Some(tray_icon); self.menu_changed_status = Some(menu_item_changed_status); self.menu_backup_now = Some(menu_item_backup_now); self.menu_exit = Some(menu_item_exit); } fn start_backup(&mut self) { self.last_change_time = None; let repo_dir = Path::new(REPO_VAULT); git::backup_changes(repo_dir).expect("Changes could not be pushed"); self.current_change_count = 0; self.menu_update_change_count(); } fn process_file_event(&mut self) { self.last_change_time = Some(Instant::now()); let source = Path::new(SOURCE_VAULT); let target = Path::new(REPO_VAULT); filesync::sync_directory(source, target, Some(PROTECT_PREFIX)) .expect("Directories could not be synced"); self.current_change_count = git::current_change_count(target).expect("Changes could not get counted using git"); self.menu_update_change_count(); println!("Currently changed files: {:?}", self.current_change_count); } fn menu_update_change_count(&mut self) { if let Some(menu_item) = &mut self.menu_changed_status { menu_item.set_text(format!("Changed: {:?}", self.current_change_count)); } } } impl ApplicationHandler for App { fn resumed(&mut self, _event_loop: &ActiveEventLoop) { self.create_tray_icon_menu(); self.last_change_time = None; } fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) { match event { UserEvent::MenuEvent(menu_event) => { match menu_event.id() { id if id == self.menu_backup_now.as_ref().unwrap().id() => { println!("---> Backup triggered by menu"); // Handle push now menu item self.start_backup(); } id if id == self.menu_exit.as_ref().unwrap().id() => { println!("---> Exit triggert by menu"); // Handle exit menu item event_loop.exit(); } _ => {} } } UserEvent::FileEvent(_event) => { println!("---> File change detected"); self.process_file_event(); } } } fn new_events(&mut self, event_loop: &ActiveEventLoop, _cause: StartCause) { let now = Instant::now(); // Backup changes if last file change time exceeded threshold if let Some(last_change_time) = self.last_change_time { if now.duration_since(last_change_time) > GIT_COMMIT_DELAY_AFTER_FILE_CHANGE { println!("---> Backup triggered by file change"); self.start_backup(); } } // Reset timer event_loop.set_control_flow(ControlFlow::WaitUntil(now + Duration::from_secs(5))); } fn window_event( &mut self, _event_loop: &ActiveEventLoop, _window_id: winit::window::WindowId, _event: WindowEvent, ) { } } fn main() { // Sync folder and check for changes to back up let source_dir = Path::new(SOURCE_VAULT); let repo_dir = Path::new(REPO_VAULT); filesync::sync_directory(source_dir, repo_dir, Some(PROTECT_PREFIX)) .expect("Directories could not be synced"); git::backup_changes(repo_dir).expect("Changes could not be pushed"); // Create event loop let event_loop = EventLoop::::with_user_event().build().unwrap(); event_loop.set_control_flow(ControlFlow::Wait); // Set up menu event handler let proxy = event_loop.create_proxy(); tray_icon::menu::MenuEvent::set_event_handler(Some(move |event| { proxy.send_event(UserEvent::MenuEvent(event)).unwrap(); })); // Set up file change event handler let proxy = event_loop.create_proxy(); let mut watcher = notify::recommended_watcher(move |event| { if let Ok(event) = event { proxy.send_event(UserEvent::FileEvent(event)).unwrap(); } }) .unwrap(); watcher.watch(source_dir, RecursiveMode::Recursive).unwrap(); // Start event loop println!("Starting event loop..."); let mut app = App::default(); event_loop.run_app(&mut app).unwrap(); }