ratatui_toolkit/services/repo_watcher/mod.rs
1//! Repository watcher for detecting git and working tree changes.
2//!
3//! Combines the git watcher (for `.git` state changes) with a file watcher
4//! (for working tree edits) and provides a cached list of modified files
5//! via `git status --porcelain`.
6//!
7//! # Example
8//!
9//! ```no_run
10//! use ratatui_toolkit::services::repo_watcher::RepoWatcher;
11//! use std::path::Path;
12//!
13//! let mut watcher = RepoWatcher::new().unwrap();
14//! watcher.watch(Path::new("/path/to/repo")).unwrap();
15//!
16//! // In your event loop:
17//! if watcher.check_for_changes() {
18//! let changes = watcher.get_change_set();
19//! for path in changes.all_paths() {
20//! println!("Changed: {}", path.display());
21//! }
22//! }
23//! ```
24
25mod constructors;
26mod helpers;
27mod methods;
28mod traits;
29
30use std::path::PathBuf;
31
32use crate::services::file_watcher::{FileWatcher, WatchConfig, WatchMode};
33use crate::services::git_watcher::{GitWatchConfig, GitWatcher};
34
35pub use constructors::*;
36
37/// Configuration for the repository watcher.
38#[derive(Debug, Clone)]
39pub struct RepoWatchConfig {
40 /// Configuration for the git watcher.
41 pub git: GitWatchConfig,
42 /// Configuration for the file watcher.
43 pub files: WatchConfig,
44 /// Whether to include untracked files in the change set.
45 pub include_untracked: bool,
46}
47
48impl Default for RepoWatchConfig {
49 fn default() -> Self {
50 Self {
51 git: GitWatchConfig::default(),
52 files: WatchConfig {
53 mode: WatchMode::Recursive,
54 debounce_ms: 200,
55 },
56 include_untracked: true,
57 }
58 }
59}
60
61impl RepoWatchConfig {
62 /// Create a new config with default settings.
63 pub fn new() -> Self {
64 Self::default()
65 }
66
67 /// Set the git watcher configuration.
68 pub fn git_config(mut self, config: GitWatchConfig) -> Self {
69 self.git = config;
70 self
71 }
72
73 /// Set the file watcher configuration.
74 pub fn file_config(mut self, config: WatchConfig) -> Self {
75 self.files = config;
76 self
77 }
78
79 /// Control whether untracked files are included in the change set.
80 pub fn include_untracked(mut self, include: bool) -> Self {
81 self.include_untracked = include;
82 self
83 }
84}
85
86/// Status for a file reported by git.
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum GitFileStatus {
89 /// File was added or copied.
90 Added,
91 /// File was modified.
92 Modified,
93 /// File was deleted.
94 Deleted,
95 /// File was renamed.
96 Renamed,
97 /// File is untracked.
98 Untracked,
99}
100
101/// Collection of git changes discovered for a repository.
102///
103/// All paths are repository-relative.
104#[derive(Debug, Clone, Default, PartialEq, Eq)]
105pub struct GitChangeSet {
106 /// Added files.
107 pub added: Vec<PathBuf>,
108 /// Modified files.
109 pub modified: Vec<PathBuf>,
110 /// Deleted files.
111 pub deleted: Vec<PathBuf>,
112 /// Renamed files (new paths).
113 pub renamed: Vec<PathBuf>,
114 /// Untracked files.
115 pub untracked: Vec<PathBuf>,
116}
117
118impl GitChangeSet {
119 /// Returns true if no changes are present.
120 pub fn is_empty(&self) -> bool {
121 self.added.is_empty()
122 && self.modified.is_empty()
123 && self.deleted.is_empty()
124 && self.renamed.is_empty()
125 && self.untracked.is_empty()
126 }
127
128 /// Returns all changed paths in a single list.
129 pub fn all_paths(&self) -> Vec<PathBuf> {
130 let mut paths = Vec::new();
131 paths.extend(self.added.iter().cloned());
132 paths.extend(self.modified.iter().cloned());
133 paths.extend(self.deleted.iter().cloned());
134 paths.extend(self.renamed.iter().cloned());
135 paths.extend(self.untracked.iter().cloned());
136 paths
137 }
138}
139
140/// A watcher that combines git and working tree signals.
141pub struct RepoWatcher {
142 /// Underlying git watcher for `.git` events.
143 pub(crate) git_watcher: GitWatcher,
144 /// Underlying file watcher for working tree events.
145 pub(crate) file_watcher: FileWatcher,
146 /// Configuration for this watcher.
147 pub(crate) config: RepoWatchConfig,
148 /// Path to the repository root being watched.
149 pub(crate) repo_path: Option<PathBuf>,
150 /// Cached change set from the last update.
151 pub(crate) change_set: GitChangeSet,
152 /// Whether a refresh is pending.
153 pub(crate) has_pending_changes: bool,
154}