From cf9ce9d4ff16a00afae97d719b234a55a67463f9 Mon Sep 17 00:00:00 2001 From: extrawurst <776816+extrawurst@users.noreply.github.com> Date: Fri, 2 Sep 2022 09:59:13 +0200 Subject: [PATCH] File watching using notify (#1310) closes #1 --- Cargo.lock | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 ++ deny.toml | 4 ++- src/app.rs | 2 +- src/main.rs | 25 ++++++++------ src/watcher.rs | 70 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 182 insertions(+), 12 deletions(-) create mode 100644 src/watcher.rs diff --git a/Cargo.lock b/Cargo.lock index 56f87761..4a2ffc23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -455,6 +455,18 @@ dependencies = [ "instant", ] +[[package]] +name = "filetime" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94a7bbaa59354bc20dd75b67f23e2797b4490e9d6928203fb105c79e448c86c" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "windows-sys", +] + [[package]] name = "filetreelist" version = "0.5.0" @@ -502,6 +514,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "futures" version = "0.3.24" @@ -673,6 +694,8 @@ dependencies = [ "gh-emoji", "itertools", "log", + "notify", + "notify-debouncer-mini", "once_cell", "pprof", "pretty_assertions", @@ -764,6 +787,26 @@ dependencies = [ "str_stack", ] +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "instant" version = "0.1.12" @@ -816,6 +859,26 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kqueue" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d6112e8f37b59803ac47a42d14f1f3a59bbf72fc6857ffc5be455e28a691f8e" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8367585489f01bc55dd27404dcf56b95e6da061a256a666ab23be9ba96a2e587" +dependencies = [ + "bitflags", + "libc", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -955,6 +1018,34 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "notify" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2c66da08abae1c024c01d635253e402341b4060a12e99b31c7594063bf490a" +dependencies = [ + "bitflags", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "mio", + "walkdir", + "winapi", +] + +[[package]] +name = "notify-debouncer-mini" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c538ea1dd436b41e751922510cfbcaea2def87ed6ed94aa1edc15dc31b4c179" +dependencies = [ + "crossbeam-channel", + "notify", +] + [[package]] name = "num-format" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index b1fcc61d..acdfea83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,8 @@ fuzzy-matcher = "0.3" gh-emoji = { version = "1.0", optional = true } itertools = "0.10" log = "0.4" +notify = "5.0" +notify-debouncer-mini = "0.2" once_cell = "1" rayon-core = "1.9" ron = "0.8" diff --git a/deny.toml b/deny.toml index e67f4800..8f51f269 100644 --- a/deny.toml +++ b/deny.toml @@ -4,7 +4,9 @@ allow = [ "MIT", "Apache-2.0", "BSD-2-Clause", - "BSD-3-Clause" + "BSD-3-Clause", + "CC0-1.0", + "ISC" ] copyleft = "warn" allow-osi-fsf-free = "neither" diff --git a/src/app.rs b/src/app.rs index e3cc7dc2..a2d7b07a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -110,7 +110,7 @@ impl App { theme: Theme, key_config: KeyConfig, ) -> Self { - log::trace!("open repo at: {:?}", repo); + log::trace!("open repo at: {:?}", &repo); let queue = Queue::new(); let theme = Rc::new(theme); diff --git a/src/main.rs b/src/main.rs index b9395be4..fd70f233 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,11 +36,15 @@ mod strings; mod tabs; mod ui; mod version; +mod watcher; use crate::{app::App, args::process_cmdline}; use anyhow::{bail, Result}; use app::QuitState; -use asyncgit::{sync::RepoPath, AsyncGitNotification}; +use asyncgit::{ + sync::{utils::repo_work_dir, RepoPath}, + AsyncGitNotification, +}; use backtrace::Backtrace; use crossbeam_channel::{tick, unbounded, Receiver, Select}; use crossterm::{ @@ -67,14 +71,14 @@ use tui::{ Terminal, }; use ui::style::Theme; +use watcher::RepoWatcher; -static TICK_INTERVAL: Duration = Duration::from_secs(5); static SPINNER_INTERVAL: Duration = Duration::from_millis(80); /// #[derive(Clone)] pub enum QueueEvent { - Tick, + Notify, SpinnerUpdate, AsyncEvent(AsyncNotification), InputEvent(InputEvent), @@ -161,7 +165,8 @@ fn run_app( let (tx_app, rx_app) = unbounded(); let rx_input = input.receiver(); - let ticker = tick(TICK_INTERVAL); + let watcher = RepoWatcher::new(repo_work_dir(&repo)?.as_str())?; + let rx_watcher = watcher.receiver(); let spinner_ticker = tick(SPINNER_INTERVAL); let mut app = App::new( @@ -179,13 +184,13 @@ fn run_app( loop { let event = if first_update { first_update = false; - QueueEvent::Tick + QueueEvent::Notify } else { select_event( &rx_input, &rx_git, &rx_app, - &ticker, + &rx_watcher, &spinner_ticker, )? }; @@ -208,7 +213,7 @@ fn run_app( } app.event(ev)?; } - QueueEvent::Tick => app.update()?, + QueueEvent::Notify => app.update()?, QueueEvent::AsyncEvent(ev) => { if !matches!( ev, @@ -282,7 +287,7 @@ fn select_event( rx_input: &Receiver, rx_git: &Receiver, rx_app: &Receiver, - rx_ticker: &Receiver, + rx_notify: &Receiver<()>, rx_spinner: &Receiver, ) -> Result { let mut sel = Select::new(); @@ -290,7 +295,7 @@ fn select_event( sel.recv(rx_input); sel.recv(rx_git); sel.recv(rx_app); - sel.recv(rx_ticker); + sel.recv(rx_notify); sel.recv(rx_spinner); let oper = sel.select(); @@ -304,7 +309,7 @@ fn select_event( 2 => oper.recv(rx_app).map(|e| { QueueEvent::AsyncEvent(AsyncNotification::App(e)) }), - 3 => oper.recv(rx_ticker).map(|_| QueueEvent::Tick), + 3 => oper.recv(rx_notify).map(|_| QueueEvent::Notify), 4 => oper.recv(rx_spinner).map(|_| QueueEvent::SpinnerUpdate), _ => bail!("unknown select source"), }?; diff --git a/src/watcher.rs b/src/watcher.rs new file mode 100644 index 00000000..a50b6534 --- /dev/null +++ b/src/watcher.rs @@ -0,0 +1,70 @@ +use anyhow::Result; +use crossbeam_channel::{unbounded, Sender}; +use notify::{Error, RecommendedWatcher, RecursiveMode}; +use notify_debouncer_mini::{ + new_debouncer, DebouncedEvent, Debouncer, +}; +use std::{ + path::Path, sync::mpsc::RecvError, thread, time::Duration, +}; + +pub struct RepoWatcher { + receiver: crossbeam_channel::Receiver<()>, + #[allow(dead_code)] + debouncer: Debouncer, +} + +impl RepoWatcher { + pub fn new(workdir: &str) -> Result { + let (tx, rx) = std::sync::mpsc::channel(); + + let mut debouncer = + new_debouncer(Duration::from_secs(2), None, tx)?; + + debouncer + .watcher() + .watch(Path::new(workdir), RecursiveMode::Recursive)?; + + let (out_tx, out_rx) = unbounded(); + + thread::spawn(move || { + if let Err(e) = Self::forwarder(&rx, &out_tx) { + //maybe we need to restart the forwarder now? + log::error!("notify receive error: {}", e); + } + }); + + Ok(Self { + debouncer, + receiver: out_rx, + }) + } + + /// + pub fn receiver(&self) -> crossbeam_channel::Receiver<()> { + self.receiver.clone() + } + + fn forwarder( + receiver: &std::sync::mpsc::Receiver< + Result, Vec>, + >, + sender: &Sender<()>, + ) -> Result<(), RecvError> { + loop { + let ev = receiver.recv()?; + + if let Ok(ev) = ev { + log::debug!("notify events: {}", ev.len()); + + for (idx, ev) in ev.iter().enumerate() { + log::debug!("notify [{}]: {:?}", idx, ev); + } + + if !ev.is_empty() { + sender.send(()).expect("send error"); + } + } + } + } +}