ripdiff 0.8.1

Terminal UI for watching and reviewing agent progress
Documentation
use crate::diff::{DiffContent, DiffRequest};
use crossterm::event::{self, Event as CrosstermEvent, KeyEvent};
use std::time::Duration;
use tokio::sync::mpsc;
use tokio::sync::watch;
use tokio::task::JoinHandle;

#[derive(Debug)]
pub enum Event {
    Key(KeyEvent),
    Resize,
    FsChange,
    Tick,
    DiffLoaded {
        request: DiffRequest,
        result: Result<DiffContent, String>,
    },
}

pub fn spawn_event_producer(
    tx: mpsc::UnboundedSender<Event>,
    shutdown: watch::Receiver<bool>,
) -> JoinHandle<()> {
    tokio::task::spawn_blocking(move || loop {
        if *shutdown.borrow() {
            break;
        }

        if event::poll(Duration::from_millis(100)).unwrap_or(false) {
            match event::read() {
                Ok(CrosstermEvent::Key(key)) => {
                    if tx.send(Event::Key(key)).is_err() {
                        break;
                    }
                }
                Ok(CrosstermEvent::Resize(_, _)) => {
                    if tx.send(Event::Resize).is_err() {
                        break;
                    }
                }
                _ => {}
            }
        }
    })
}

pub fn spawn_tick_producer(
    tx: mpsc::UnboundedSender<Event>,
    shutdown: watch::Receiver<bool>,
) -> JoinHandle<()> {
    tokio::spawn(async move {
        let mut shutdown = shutdown;
        loop {
            tokio::select! {
                _ = tokio::time::sleep(Duration::from_millis(500)) => {
                    if tx.send(Event::Tick).is_err() {
                        break;
                    }
                }
                changed = shutdown.changed() => {
                    if changed.is_err() || *shutdown.borrow() {
                        break;
                    }
                }
            }
        }
    })
}

pub fn spawn_watcher(
    tx: mpsc::UnboundedSender<Event>,
    watch_paths: Vec<std::path::PathBuf>,
    shutdown: watch::Receiver<bool>,
) -> JoinHandle<()> {
    use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
    use std::sync::mpsc as std_mpsc;

    tokio::task::spawn_blocking(move || {
        let (notify_tx, notify_rx) = std_mpsc::channel();
        let Ok(mut watcher) = RecommendedWatcher::new(notify_tx, Config::default()) else {
            return;
        };

        for path in &watch_paths {
            if path.exists() {
                let _ = watcher.watch(path, RecursiveMode::NonRecursive);
            }
        }

        loop {
            if *shutdown.borrow() {
                break;
            }

            match notify_rx.recv_timeout(Duration::from_millis(200)) {
                Ok(_) => {
                    if tx.send(Event::FsChange).is_err() {
                        break;
                    }
                }
                Err(std_mpsc::RecvTimeoutError::Timeout) => {}
                Err(_) => break,
            }
        }
    })
}