gravityfile_plugin/hooks.rs
1//! Hook definitions for plugin event system.
2//!
3//! Hooks allow plugins to respond to events in the gravityfile application.
4//! Each hook has a specific context and expected result type.
5
6use std::collections::HashMap;
7use std::path::PathBuf;
8
9use serde::{Deserialize, Serialize};
10
11use crate::types::Value;
12
13/// Events that plugins can hook into.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15#[serde(tag = "type", rename_all = "snake_case")]
16pub enum Hook {
17 // ==================== Navigation Events ====================
18 /// Fired when navigating to a new directory.
19 OnNavigate {
20 /// Previous directory path.
21 from: PathBuf,
22 /// New directory path.
23 to: PathBuf,
24 },
25
26 /// Fired when drilling down into a directory.
27 OnDrillDown {
28 /// Directory being entered.
29 path: PathBuf,
30 },
31
32 /// Fired when navigating back.
33 OnBack {
34 /// Directory being left.
35 from: PathBuf,
36 /// Directory being returned to.
37 to: PathBuf,
38 },
39
40 // ==================== Scan Events ====================
41 /// Fired when a scan operation starts.
42 OnScanStart {
43 /// Root path being scanned.
44 path: PathBuf,
45 },
46
47 /// Fired periodically during scanning with progress info.
48 OnScanProgress {
49 /// Files scanned so far.
50 files_scanned: u64,
51 /// Directories scanned so far.
52 dirs_scanned: u64,
53 /// Bytes scanned so far.
54 bytes_scanned: u64,
55 },
56
57 /// Fired when scan completes successfully.
58 OnScanComplete {
59 /// Root path that was scanned.
60 path: PathBuf,
61 /// Total files found.
62 total_files: u64,
63 /// Total directories found.
64 total_dirs: u64,
65 /// Total size in bytes.
66 total_size: u64,
67 },
68
69 /// Fired when scan fails.
70 OnScanError {
71 /// Root path that failed.
72 path: PathBuf,
73 /// Error message.
74 error: String,
75 },
76
77 // ==================== File Operation Events ====================
78 /// Fired before deletion starts.
79 OnDeleteStart {
80 /// Items to be deleted.
81 items: Vec<PathBuf>,
82 /// Whether using trash (recoverable).
83 use_trash: bool,
84 },
85
86 /// Fired after deletion completes.
87 OnDeleteComplete {
88 /// Number of items successfully deleted.
89 deleted: usize,
90 /// Number of items that failed to delete.
91 failed: usize,
92 /// Total bytes freed.
93 bytes_freed: u64,
94 },
95
96 /// Fired before copy operation starts.
97 OnCopyStart {
98 /// Source paths.
99 sources: Vec<PathBuf>,
100 /// Destination directory.
101 destination: PathBuf,
102 },
103
104 /// Fired after copy completes.
105 OnCopyComplete {
106 /// Number of items successfully copied.
107 succeeded: usize,
108 /// Number of items that failed.
109 failed: usize,
110 /// Total bytes copied.
111 bytes_copied: u64,
112 },
113
114 /// Fired before move operation starts.
115 OnMoveStart {
116 /// Source paths.
117 sources: Vec<PathBuf>,
118 /// Destination directory.
119 destination: PathBuf,
120 },
121
122 /// Fired after move completes.
123 OnMoveComplete {
124 /// Number of items successfully moved.
125 succeeded: usize,
126 /// Number of items that failed.
127 failed: usize,
128 },
129
130 /// Fired before rename operation.
131 OnRenameStart {
132 /// Original path.
133 source: PathBuf,
134 /// New name.
135 new_name: String,
136 },
137
138 /// Fired after rename completes.
139 OnRenameComplete {
140 /// Original path.
141 source: PathBuf,
142 /// New path after rename.
143 new_path: PathBuf,
144 },
145
146 // ==================== Analysis Events ====================
147 /// Fired when duplicate analysis completes.
148 OnDuplicatesFound {
149 /// Number of duplicate groups found.
150 group_count: usize,
151 /// Total wasted space in bytes.
152 wasted_bytes: u64,
153 },
154
155 /// Fired when age analysis completes.
156 OnAgeAnalysisComplete {
157 /// Number of stale directories found.
158 stale_dirs: usize,
159 /// Oldest file age in seconds.
160 oldest_age_secs: u64,
161 },
162
163 // ==================== UI Events ====================
164 /// Fired before rendering a view.
165 OnRender {
166 /// Current view name.
167 view: String,
168 /// Render area dimensions.
169 width: u16,
170 height: u16,
171 },
172
173 /// Fired when user performs an action.
174 OnAction {
175 /// Action name (e.g., "delete", "copy", "move").
176 action: String,
177 },
178
179 /// Fired when application mode changes.
180 OnModeChange {
181 /// Previous mode.
182 from: String,
183 /// New mode.
184 to: String,
185 },
186
187 /// Fired when selection changes.
188 OnSelectionChange {
189 /// Currently selected paths.
190 selected: Vec<PathBuf>,
191 /// Number of items selected.
192 count: usize,
193 },
194
195 // ==================== Lifecycle Events ====================
196 /// Fired when application starts.
197 OnStartup,
198
199 /// Fired when application is about to quit.
200 OnShutdown,
201
202 /// Fired when a plugin is loaded.
203 OnPluginLoad {
204 /// Name of the plugin being loaded.
205 name: String,
206 },
207
208 /// Fired when a plugin is unloaded.
209 OnPluginUnload {
210 /// Name of the plugin being unloaded.
211 name: String,
212 },
213}
214
215impl Hook {
216 /// Get the hook name as a string (for matching in plugins).
217 pub fn name(&self) -> &'static str {
218 match self {
219 Self::OnNavigate { .. } => "on_navigate",
220 Self::OnDrillDown { .. } => "on_drill_down",
221 Self::OnBack { .. } => "on_back",
222 Self::OnScanStart { .. } => "on_scan_start",
223 Self::OnScanProgress { .. } => "on_scan_progress",
224 Self::OnScanComplete { .. } => "on_scan_complete",
225 Self::OnScanError { .. } => "on_scan_error",
226 Self::OnDeleteStart { .. } => "on_delete_start",
227 Self::OnDeleteComplete { .. } => "on_delete_complete",
228 Self::OnCopyStart { .. } => "on_copy_start",
229 Self::OnCopyComplete { .. } => "on_copy_complete",
230 Self::OnMoveStart { .. } => "on_move_start",
231 Self::OnMoveComplete { .. } => "on_move_complete",
232 Self::OnRenameStart { .. } => "on_rename_start",
233 Self::OnRenameComplete { .. } => "on_rename_complete",
234 Self::OnDuplicatesFound { .. } => "on_duplicates_found",
235 Self::OnAgeAnalysisComplete { .. } => "on_age_analysis_complete",
236 Self::OnRender { .. } => "on_render",
237 Self::OnAction { .. } => "on_action",
238 Self::OnModeChange { .. } => "on_mode_change",
239 Self::OnSelectionChange { .. } => "on_selection_change",
240 Self::OnStartup => "on_startup",
241 Self::OnShutdown => "on_shutdown",
242 Self::OnPluginLoad { .. } => "on_plugin_load",
243 Self::OnPluginUnload { .. } => "on_plugin_unload",
244 }
245 }
246
247 /// Check if this is a lifecycle event (startup/shutdown).
248 pub fn is_lifecycle(&self) -> bool {
249 matches!(
250 self,
251 Self::OnStartup
252 | Self::OnShutdown
253 | Self::OnPluginLoad { .. }
254 | Self::OnPluginUnload { .. }
255 )
256 }
257
258 /// Check if this hook should run synchronously (blocking).
259 pub fn is_sync(&self) -> bool {
260 // Render and action hooks should be sync to avoid UI lag
261 matches!(
262 self,
263 Self::OnRender { .. }
264 | Self::OnAction { .. }
265 | Self::OnModeChange { .. }
266 | Self::OnSelectionChange { .. }
267 )
268 }
269}
270
271/// Context provided to plugins when a hook is invoked.
272#[derive(Debug, Clone, Default)]
273pub struct HookContext {
274 /// Additional data passed to the hook.
275 pub data: HashMap<String, Value>,
276
277 /// Current working directory.
278 pub cwd: Option<PathBuf>,
279
280 /// Current view root (drill-down location).
281 pub view_root: Option<PathBuf>,
282
283 /// Theme variant (dark/light).
284 pub theme: Option<String>,
285}
286
287impl HookContext {
288 /// Create a new empty context.
289 pub fn new() -> Self {
290 Self::default()
291 }
292
293 /// Set a value in the context.
294 pub fn set(&mut self, key: impl Into<String>, value: impl Into<Value>) -> &mut Self {
295 self.data.insert(key.into(), value.into());
296 self
297 }
298
299 /// Get a value from the context.
300 pub fn get(&self, key: &str) -> Option<&Value> {
301 self.data.get(key)
302 }
303
304 /// Set the current working directory.
305 pub fn with_cwd(mut self, cwd: PathBuf) -> Self {
306 self.cwd = Some(cwd);
307 self
308 }
309
310 /// Set the view root.
311 pub fn with_view_root(mut self, view_root: PathBuf) -> Self {
312 self.view_root = Some(view_root);
313 self
314 }
315
316 /// Set the theme.
317 pub fn with_theme(mut self, theme: impl Into<String>) -> Self {
318 self.theme = Some(theme.into());
319 self
320 }
321}
322
323/// Result returned by a plugin hook handler.
324#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
325pub struct HookResult {
326 /// Whether the hook was handled.
327 pub handled: bool,
328
329 /// Whether to prevent default behavior.
330 pub prevent_default: bool,
331
332 /// Whether to stop propagation to other plugins.
333 pub stop_propagation: bool,
334
335 /// Return value from the hook (if any).
336 pub value: Option<Value>,
337
338 /// Error message (if hook failed).
339 pub error: Option<String>,
340}
341
342impl HookResult {
343 /// Create a successful result.
344 pub fn ok() -> Self {
345 Self {
346 handled: true,
347 ..Default::default()
348 }
349 }
350
351 /// Create a result with a return value.
352 pub fn with_value(value: impl Into<Value>) -> Self {
353 Self {
354 handled: true,
355 value: Some(value.into()),
356 ..Default::default()
357 }
358 }
359
360 /// Create an error result.
361 pub fn error(message: impl Into<String>) -> Self {
362 Self {
363 handled: true,
364 error: Some(message.into()),
365 ..Default::default()
366 }
367 }
368
369 /// Mark this result as preventing default behavior.
370 pub fn prevent_default(mut self) -> Self {
371 self.prevent_default = true;
372 self
373 }
374
375 /// Mark this result as stopping propagation.
376 pub fn stop_propagation(mut self) -> Self {
377 self.stop_propagation = true;
378 self
379 }
380
381 /// Check if the hook execution was successful.
382 pub fn is_ok(&self) -> bool {
383 self.error.is_none()
384 }
385
386 /// Check if the hook had an error.
387 pub fn is_err(&self) -> bool {
388 self.error.is_some()
389 }
390}