diff --git a/Cargo.lock b/Cargo.lock index 28a46a7..f388348 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -253,6 +253,24 @@ dependencies = [ "libloading 0.7.4", ] +[[package]] +name = "ashpd" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7370b58af1d7e96df3ca0f454b57e69acf9aa42ed2d7337bd206923bae0d5754" +dependencies = [ + "enumflags2", + "futures-channel", + "futures-util", + "once_cell", + "rand", + "serde", + "serde_repr", + "tokio", + "url", + "zbus", +] + [[package]] name = "async-broadcast" version = "0.5.1" @@ -329,7 +347,7 @@ dependencies = [ "polling 2.8.0", "rustix 0.37.27", "slab", - "socket2", + "socket2 0.4.10", "waker-fn", ] @@ -645,6 +663,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + [[package]] name = "cairo-sys-rs" version = "0.18.2" @@ -963,11 +987,13 @@ dependencies = [ "lazy_static", "libcosmic", "log", + "notify", "rfd", "rust-embed", "serde", "syntect", "systemicons", + "tokio", "two-face", ] @@ -2416,6 +2442,7 @@ dependencies = [ "futures", "iced_core", "log", + "tokio", "wasm-bindgen-futures", "wasm-timer", ] @@ -2829,6 +2856,7 @@ version = "0.1.0" source = "git+https://github.com/pop-os/libcosmic#001fd744c5f80c9ce058eb0e22ae92f19d12c844" dependencies = [ "apply", + "ashpd", "cosmic-config", "cosmic-theme", "css-color", @@ -2850,9 +2878,11 @@ dependencies = [ "slotmap", "taffy", "thiserror", + "tokio", "tracing", "unicode-segmentation", "url", + "zbus", ] [[package]] @@ -4813,6 +4843,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "softbuffer" version = "0.3.3" @@ -5198,6 +5238,24 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tokio" +version = "1.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio 0.8.9", + "num_cpus", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.5.5", + "tracing", + "windows-sys 0.48.0", +] + [[package]] name = "toml" version = "0.5.11" @@ -5472,6 +5530,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -6419,6 +6478,7 @@ dependencies = [ "serde_repr", "sha1", "static_assertions", + "tokio", "tracing", "uds_windows", "winapi", @@ -6499,6 +6559,7 @@ dependencies = [ "libc", "serde", "static_assertions", + "url", "zvariant_derive", ] diff --git a/Cargo.toml b/Cargo.toml index a949562..31bbd4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,9 +9,12 @@ license = "GPL-3.0-only" env_logger = "0.10.0" lazy_static = "1.4.0" log = "0.4.20" +notify = "6.1.1" #TODO: this is using gtk for file dialogues rfd = { version = "0.12.0", optional = true } serde = { version = "1", features = ["serde_derive"] } +tokio = { version = "1", features = ["time"] } +# Extra syntax highlighting syntect = "5.1.0" two-face = "0.3.0" # Internationalization @@ -27,7 +30,7 @@ features = ["syntect", "vi"] [dependencies.libcosmic] git = "https://github.com/pop-os/libcosmic" default-features = false -features = ["winit", "wgpu"] +features = ["tokio", "winit", "wgpu"] #path = "../libcosmic" #TODO: clean up and send changes upstream diff --git a/src/main.rs b/src/main.rs index 63831a0..77d23d0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,9 @@ use cosmic::{ cosmic_config::{self, CosmicConfigEntry}, cosmic_theme, executor, iced::{ - clipboard, event, keyboard, subscription, + clipboard, event, + futures::{self, SinkExt}, + keyboard, subscription, widget::{row, text}, window, Alignment, Length, Point, }, @@ -20,6 +22,7 @@ use std::{ process, sync::Mutex, }; +use tokio::time; use config::{Action, AppTheme, Config, CONFIG_VERSION}; mod config; @@ -104,6 +107,23 @@ pub struct Flags { config: Config, } +#[derive(Debug)] +pub struct WatcherWrapper { + watcher_opt: Option, +} + +impl Clone for WatcherWrapper { + fn clone(&self) -> Self { + Self { watcher_opt: None } + } +} + +impl PartialEq for WatcherWrapper { + fn eq(&self, _other: &Self) -> bool { + false + } +} + #[allow(dead_code)] #[derive(Clone, Debug, PartialEq)] pub enum Message { @@ -118,6 +138,8 @@ pub enum Message { Key(keyboard::Modifiers, keyboard::KeyCode), NewFile, NewWindow, + NotifyEvent(notify::Event), + NotifyWatcher(WatcherWrapper), OpenFileDialog, OpenFile(PathBuf), OpenProjectDialog, @@ -171,6 +193,7 @@ pub struct App { font_sizes: Vec, theme_names: Vec, context_page: ContextPage, + watcher_opt: Option, } impl App { @@ -304,6 +327,7 @@ impl App { let mut tab = Tab::new(&self.config); tab.open(canonical); + tab.watch(&mut self.watcher_opt); tab } None => Tab::new(&self.config), @@ -337,9 +361,7 @@ impl App { log::error!("failed to save config: {}", err); } }, - None => { - //TODO: log that there is no handler? - } + None => {} } self.update_config() } @@ -485,6 +507,7 @@ impl Application for App { font_sizes, theme_names, context_page: ContextPage::Settings, + watcher_opt: None, }; for arg in env::args().skip(1) { @@ -681,6 +704,57 @@ impl Application for App { } } } + Message::NotifyEvent(event) => { + let mut needs_reload = Vec::new(); + for entity in self.tab_model.iter() { + match self.tab_model.data::(entity) { + Some(tab) => { + if let Some(path) = &tab.path_opt { + if event.paths.contains(&path) { + if tab.changed() { + log::warn!( + "file changed externally before being saved: {:?}", + path + ); + } else { + needs_reload.push(entity); + } + } + } + } + None => {} + } + } + + for entity in needs_reload { + match self.tab_model.data_mut::(entity) { + Some(tab) => { + tab.reload(); + } + None => { + log::warn!("failed to find tab {:?} that needs reload", entity); + } + } + } + } + Message::NotifyWatcher(mut watcher_wrapper) => match watcher_wrapper.watcher_opt.take() + { + Some(watcher) => { + self.watcher_opt = Some(watcher); + + for entity in self.tab_model.iter() { + match self.tab_model.data::(entity) { + Some(tab) => { + tab.watch(&mut self.watcher_opt); + } + None => {} + } + } + } + None => { + log::warn!("message did not contain notify watcher"); + } + }, Message::OpenFileDialog => { #[cfg(feature = "rfd")] return Command::perform( @@ -1113,6 +1187,63 @@ impl Application for App { }) => Some(Message::Key(modifiers, key_code)), _ => None, }), + subscription::channel(0, 100, |mut output| async move { + let watcher_res = { + let mut output = output.clone(); + notify::recommended_watcher( + move |event_res: Result| match event_res { + Ok(event) => { + match &event.kind { + notify::EventKind::Access(_) + | notify::EventKind::Modify( + notify::event::ModifyKind::Metadata(_), + ) => { + // Data not mutated + return; + } + _ => {} + } + + match futures::executor::block_on(async { + output.send(Message::NotifyEvent(event)).await + }) { + Ok(()) => {} + Err(err) => { + log::warn!("failed to send notify event: {:?}", err); + } + } + } + Err(err) => { + log::warn!("failed to watch files: {:?}", err); + } + }, + ) + }; + + match watcher_res { + Ok(watcher) => { + match output + .send(Message::NotifyWatcher(WatcherWrapper { + watcher_opt: Some(watcher), + })) + .await + { + Ok(()) => {} + Err(err) => { + log::warn!("failed to send notify watcher: {:?}", err); + } + } + } + Err(err) => { + log::warn!("failed to create file watcher: {:?}", err); + } + } + + //TODO: how to properly kill this task? + loop { + time::sleep(time::Duration::new(1, 0)).await; + } + }), cosmic_config::config_subscription(0, Self::APP_ID.into(), CONFIG_VERSION).map( |(_, res)| match res { Ok(config) => Message::Config(config), diff --git a/src/tab.rs b/src/tab.rs index 23d36aa..a5ecf25 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -5,6 +5,7 @@ use cosmic::{ widget::{icon, Icon}, }; use cosmic_text::{Attrs, Buffer, Edit, Shaping, SyntaxEditor, ViEditor, Wrap}; +use notify::Watcher; use std::{fs, path::PathBuf, sync::Mutex}; use crate::{fl, mime_icon, Config, FALLBACK_MIME_ICON, FONT_SYSTEM, SYNTAX_SYSTEM}; @@ -82,6 +83,31 @@ impl Tab { } } + pub fn reload(&mut self) { + let mut editor = self.editor.lock().unwrap(); + let mut font_system = FONT_SYSTEM.lock().unwrap(); + let mut editor = editor.borrow_with(&mut font_system); + if let Some(path) = &self.path_opt { + // Save scroll + let scroll = editor.buffer().scroll(); + //TODO: save/restore more? + + match editor.load_text(path, self.attrs) { + Ok(()) => { + log::info!("reloaded {:?}", path); + } + Err(err) => { + log::error!("failed to reload {:?}: {}", path, err); + } + } + + // Restore scroll + editor.buffer_mut().set_scroll(scroll); + } else { + log::warn!("tried to reload with no path"); + } + } + pub fn save(&mut self) { if let Some(path) = &self.path_opt { let mut editor = self.editor.lock().unwrap(); @@ -104,6 +130,21 @@ impl Tab { } } + pub fn watch(&self, watcher_opt: &mut Option) { + if let Some(path) = &self.path_opt { + if let Some(watcher) = watcher_opt { + match watcher.watch(&path, notify::RecursiveMode::NonRecursive) { + Ok(()) => { + log::info!("watching {:?} for changes", path); + } + Err(err) => { + log::warn!("failed to watch {:?} for changes: {:?}", path, err); + } + } + } + } + } + pub fn changed(&self) -> bool { let editor = self.editor.lock().unwrap(); editor.changed()