Skip to main content

iced_swdir_tree/directory_tree/
executor.rs

1//! Pluggable executor for blocking directory scans.
2//!
3//! [`ScanExecutor`] is the seam between the widget and whatever
4//! runtime the host application is built on. By default the widget
5//! uses [`ThreadExecutor`] (one `std::thread::spawn` per expansion),
6//! which is correct but slightly wasteful for apps that already have
7//! a blocking-task pool — tokio, smol, rayon, etc. Those apps can
8//! implement this trait to route scans through their own pool.
9//!
10//! # Why a trait, not a feature flag
11//!
12//! Runtimes vary in their "how do I run blocking work" API, and
13//! hard-coding any one of them would shut out the others. A trait
14//! lets each application plug in exactly what fits — with zero
15//! default dependencies on tokio, smol, or similar.
16//!
17//! # Example — tokio
18//!
19//! ```ignore
20//! use std::sync::Arc;
21//! use std::future::Future;
22//! use std::pin::Pin;
23//! use std::path::Path;
24//! use iced_swdir_tree::{ScanExecutor, DirectoryTree};
25//!
26//! struct TokioExecutor;
27//!
28//! impl ScanExecutor for TokioExecutor {
29//!     fn spawn_blocking(
30//!         &self,
31//!         job: Box<dyn FnOnce() -> Result<Vec<swdir::DirEntry>, swdir::ScanError> + Send>,
32//!     ) -> Pin<Box<dyn Future<Output = Result<Vec<swdir::DirEntry>, swdir::ScanError>> + Send>> {
33//!         Box::pin(async move {
34//!             tokio::task::spawn_blocking(job)
35//!                 .await
36//!                 .expect("scan task panicked")
37//!         })
38//!     }
39//! }
40//!
41//! let tree = DirectoryTree::new("/".into())
42//!     .with_executor(Arc::new(TokioExecutor));
43//! ```
44
45use std::future::Future;
46use std::path::{Path, PathBuf};
47use std::pin::Pin;
48use std::sync::Arc;
49
50/// The job passed to a [`ScanExecutor`] — always just "call
51/// `swdir::scan_dir` on this path".
52///
53/// We pre-bake the path into the closure rather than exposing a
54/// "scan this path" method on the trait, because trait methods
55/// returning boxed futures must take a `'static` closure argument,
56/// and smuggling the path via capture is the cleanest way to do that.
57///
58/// Kept as a type alias so the trait signature reads at a glance.
59pub type ScanJob =
60    Box<dyn FnOnce() -> Result<Vec<swdir::DirEntry>, swdir::ScanError> + Send + 'static>;
61
62/// The future that runs a [`ScanJob`] to completion.
63///
64/// `Send + 'static` because the widget hands this off to
65/// `iced::Task::perform`, which requires both.
66pub type ScanFuture =
67    Pin<Box<dyn Future<Output = Result<Vec<swdir::DirEntry>, swdir::ScanError>> + Send + 'static>>;
68
69/// A pluggable executor for blocking `scan_dir` calls.
70///
71/// Applications that already manage a blocking-task pool can
72/// implement this to route tree expansions through it instead of
73/// spinning up a new `std::thread` per scan.
74///
75/// Implementors should ensure the returned future resolves once the
76/// job has actually run — cancelling or losing the job will leave
77/// the widget stuck in "loading" forever (`is_loaded` never flips
78/// to `true` for the affected directory).
79///
80/// The widget holds the executor behind an `Arc`, so an impl that
81/// owns any shared state should wrap it in an `Arc` internally or
82/// store only `Send + Sync` references.
83pub trait ScanExecutor: Send + Sync + 'static {
84    /// Run `job` on a blocking-capable worker and return a future
85    /// that resolves to its result.
86    ///
87    /// The future is driven from the iced runtime. It must be
88    /// `Send + 'static` — iced spawns tasks across threads.
89    fn spawn_blocking(&self, job: ScanJob) -> ScanFuture;
90}
91
92/// Default executor — one `std::thread::spawn` per scan.
93///
94/// This is what the widget uses if you never call
95/// [`DirectoryTree::with_executor`](crate::DirectoryTree::with_executor).
96/// It is completely runtime-agnostic: no tokio, no smol, no async.
97///
98/// Thread-spawn overhead is on the order of tens of microseconds —
99/// usually negligible next to the `readdir` syscall the thread is
100/// about to do — so this is a reasonable default even for apps
101/// that could plug in something fancier.
102#[derive(Debug, Clone, Copy, Default)]
103pub struct ThreadExecutor;
104
105impl ScanExecutor for ThreadExecutor {
106    fn spawn_blocking(&self, job: ScanJob) -> ScanFuture {
107        Box::pin(async move {
108            // `thread::spawn + join` inside an `async move` block
109            // is the standard trick for adapting a synchronous
110            // blocking primitive to a runtime-agnostic future.
111            std::thread::spawn(job).join().unwrap_or_else(|_| {
112                // Worker panic (exceedingly rare — effectively
113                // OOM-only in swdir's case). Fabricate an I/O
114                // error so the widget sees a failed scan rather
115                // than a runtime panic propagating. We construct
116                // the enum variant directly because `ScanError::io`
117                // is pub(crate) in swdir.
118                Err(swdir::ScanError::Io {
119                    path: PathBuf::new(),
120                    source: std::io::Error::other("scan worker thread panicked"),
121                })
122            })
123        })
124    }
125}
126
127/// Convenience helper: run `path` through `executor` and get a future
128/// of the scan result.
129///
130/// Crate-internal; used by the walker to keep the async plumbing in
131/// one place regardless of executor choice.
132pub(crate) fn run_scan(executor: &Arc<dyn ScanExecutor>, path: PathBuf) -> ScanFuture {
133    let job: ScanJob = Box::new(move || swdir::scan_dir(&path as &Path));
134    executor.spawn_blocking(job)
135}