@@ -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