Skip to content

Commit 18a5375

Browse files
authored
Merge pull request #209 from ryanoneill/feature/hook-once-methods
Add on_setup_once and on_teardown_once convenience methods
2 parents f731228 + e545825 commit 18a5375

File tree

1 file changed

+183
-0
lines changed

1 file changed

+183
-0
lines changed

src/app/runtime/config.rs

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,70 @@ impl RuntimeConfig {
157157
self.on_teardown = Some(hook);
158158
self
159159
}
160+
161+
/// Sets a hook to be called after terminal setup, accepting a `FnOnce` closure.
162+
///
163+
/// This is a convenience wrapper around [`on_setup`](Self::on_setup) for closures
164+
/// that consume captured state. The closure runs at most once; subsequent calls
165+
/// (e.g., on a cloned config) are no-ops.
166+
///
167+
/// # Example
168+
///
169+
/// ```rust,ignore
170+
/// use envision::RuntimeConfig;
171+
///
172+
/// let config = RuntimeConfig::new()
173+
/// .on_setup_once(|| {
174+
/// // Move captured values into this closure
175+
/// eprintln!("Terminal is set up");
176+
/// Ok(())
177+
/// });
178+
/// ```
179+
pub fn on_setup_once<F>(self, hook: F) -> Self
180+
where
181+
F: FnOnce() -> std::io::Result<()> + Send + Sync + 'static,
182+
{
183+
let hook = std::sync::Mutex::new(Some(hook));
184+
self.on_setup(Arc::new(move || {
185+
if let Some(f) = hook.lock().unwrap().take() {
186+
f()
187+
} else {
188+
Ok(())
189+
}
190+
}))
191+
}
192+
193+
/// Sets a hook to be called before terminal teardown, accepting a `FnOnce` closure.
194+
///
195+
/// This is a convenience wrapper around [`on_teardown`](Self::on_teardown) for closures
196+
/// that consume captured state. The closure runs at most once; subsequent calls
197+
/// (e.g., on a cloned config) are no-ops.
198+
///
199+
/// # Example
200+
///
201+
/// ```rust,ignore
202+
/// use envision::RuntimeConfig;
203+
///
204+
/// let config = RuntimeConfig::new()
205+
/// .on_teardown_once(|| {
206+
/// // Move captured values into this closure
207+
/// eprintln!("Terminal is being torn down");
208+
/// Ok(())
209+
/// });
210+
/// ```
211+
pub fn on_teardown_once<F>(self, hook: F) -> Self
212+
where
213+
F: FnOnce() -> std::io::Result<()> + Send + Sync + 'static,
214+
{
215+
let hook = std::sync::Mutex::new(Some(hook));
216+
self.on_teardown(Arc::new(move || {
217+
if let Some(f) = hook.lock().unwrap().take() {
218+
f()
219+
} else {
220+
Ok(())
221+
}
222+
}))
223+
}
160224
}
161225

162226
#[cfg(test)]
@@ -260,4 +324,123 @@ mod tests {
260324
assert!(debug.contains("on_setup: None"));
261325
assert!(debug.contains("on_teardown: None"));
262326
}
327+
328+
#[test]
329+
fn test_on_setup_once_stored() {
330+
let config = RuntimeConfig::new().on_setup_once(|| Ok(()));
331+
assert!(config.on_setup.is_some());
332+
assert!(config.on_teardown.is_none());
333+
}
334+
335+
#[test]
336+
fn test_on_teardown_once_stored() {
337+
let config = RuntimeConfig::new().on_teardown_once(|| Ok(()));
338+
assert!(config.on_setup.is_none());
339+
assert!(config.on_teardown.is_some());
340+
}
341+
342+
#[test]
343+
fn test_on_setup_once_callable() {
344+
use std::sync::atomic::{AtomicBool, Ordering};
345+
346+
let called = Arc::new(AtomicBool::new(false));
347+
let flag = called.clone();
348+
349+
let config = RuntimeConfig::new().on_setup_once(move || {
350+
flag.store(true, Ordering::SeqCst);
351+
Ok(())
352+
});
353+
354+
config.on_setup.as_ref().unwrap()().unwrap();
355+
assert!(called.load(Ordering::SeqCst));
356+
}
357+
358+
#[test]
359+
fn test_on_teardown_once_callable() {
360+
use std::sync::atomic::{AtomicBool, Ordering};
361+
362+
let called = Arc::new(AtomicBool::new(false));
363+
let flag = called.clone();
364+
365+
let config = RuntimeConfig::new().on_teardown_once(move || {
366+
flag.store(true, Ordering::SeqCst);
367+
Ok(())
368+
});
369+
370+
config.on_teardown.as_ref().unwrap()().unwrap();
371+
assert!(called.load(Ordering::SeqCst));
372+
}
373+
374+
#[test]
375+
fn test_on_setup_once_runs_only_once() {
376+
use std::sync::atomic::{AtomicUsize, Ordering};
377+
378+
let call_count = Arc::new(AtomicUsize::new(0));
379+
let counter = call_count.clone();
380+
381+
let config = RuntimeConfig::new().on_setup_once(move || {
382+
counter.fetch_add(1, Ordering::SeqCst);
383+
Ok(())
384+
});
385+
386+
let hook = config.on_setup.as_ref().unwrap();
387+
hook().unwrap();
388+
hook().unwrap();
389+
hook().unwrap();
390+
391+
assert_eq!(call_count.load(Ordering::SeqCst), 1);
392+
}
393+
394+
#[test]
395+
fn test_on_setup_once_with_consuming_capture() {
396+
use std::sync::atomic::{AtomicBool, Ordering};
397+
398+
let dropped = Arc::new(AtomicBool::new(false));
399+
400+
struct Guard {
401+
flag: Arc<AtomicBool>,
402+
}
403+
404+
impl Drop for Guard {
405+
fn drop(&mut self) {
406+
self.flag.store(true, Ordering::SeqCst);
407+
}
408+
}
409+
410+
let guard = Guard {
411+
flag: dropped.clone(),
412+
};
413+
414+
let config = RuntimeConfig::new().on_setup_once(move || {
415+
drop(guard);
416+
Ok(())
417+
});
418+
419+
assert!(!dropped.load(Ordering::SeqCst));
420+
config.on_setup.as_ref().unwrap()().unwrap();
421+
assert!(dropped.load(Ordering::SeqCst));
422+
}
423+
424+
#[test]
425+
fn test_cloned_config_once_hook_runs_on_first_only() {
426+
use std::sync::atomic::{AtomicUsize, Ordering};
427+
428+
let call_count = Arc::new(AtomicUsize::new(0));
429+
let counter = call_count.clone();
430+
431+
let config = RuntimeConfig::new().on_setup_once(move || {
432+
counter.fetch_add(1, Ordering::SeqCst);
433+
Ok(())
434+
});
435+
436+
let cloned = config.clone();
437+
438+
// Call on original - should run the FnOnce
439+
config.on_setup.as_ref().unwrap()().unwrap();
440+
assert_eq!(call_count.load(Ordering::SeqCst), 1);
441+
442+
// Call on clone - shares the same Arc, so FnOnce is already consumed
443+
cloned.on_setup.as_ref().unwrap()().unwrap();
444+
assert_eq!(call_count.load(Ordering::SeqCst), 1);
445+
}
263446
}

0 commit comments

Comments
 (0)