Skip to main content

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}