use crate::app::{App, MessageClass, Screen, StatusCenter, StatusMessage};
type Effect = Box<dyn FnOnce(&mut App)>;
#[derive(Default)]
pub(super) struct Effects {
queue: Vec<Effect>,
}
impl Effects {
pub(super) fn defer(&mut self, effect: impl FnOnce(&mut App) + 'static) {
self.queue.push(Box::new(effect));
}
pub(super) fn apply(mut self, app: &mut App) {
for effect in std::mem::take(&mut self.queue) {
effect(app);
}
}
}
impl Drop for Effects {
fn drop(&mut self) {
if self.queue.is_empty() {
return;
}
log::error!(
"[purple] {} deferred effect(s) dropped unapplied: a handler returned after defer() without effects.apply()",
self.queue.len()
);
#[cfg(debug_assertions)]
if !std::thread::panicking() {
panic!(
"{} deferred effect(s) dropped unapplied without effects.apply()",
self.queue.len()
);
}
}
}
pub(super) trait Nav {
fn screen_mut(&mut self) -> &mut Screen;
fn set_screen(&mut self, new: Screen) {
let current = self.screen_mut();
if *current != new {
log::debug!(
"screen: {} → {}",
current.variant_name(),
new.variant_name()
);
}
*current = new;
}
}
pub(super) trait Notify {
fn status_mut(&mut self) -> &mut StatusCenter;
fn notify(&mut self, text: impl Into<String>) {
self.status_mut().notify(text);
}
fn notify_error(&mut self, text: impl Into<String>) {
self.status_mut().notify_error(text);
}
fn notify_warning(&mut self, text: impl Into<String>) {
let msg = StatusMessage {
text: text.into(),
class: MessageClass::Warning,
tick_count: 0,
sticky: false,
created_at: std::time::Instant::now(),
};
log::debug!("toast <- Warning: {}", msg.text);
self.status_mut().push_toast(msg);
}
}
pub(super) trait Effectful {
fn effects_mut(&mut self) -> &mut Effects;
fn defer(&mut self, effect: impl FnOnce(&mut App) + 'static) {
self.effects_mut().defer(effect);
}
fn reload_hosts(&mut self) {
self.effects_mut().defer(App::reload_hosts);
}
fn apply_sort(&mut self) {
self.effects_mut().defer(App::apply_sort);
}
fn update_last_modified(&mut self) {
self.effects_mut().defer(App::update_last_modified);
}
fn record_key_use(&mut self, alias: impl Into<String>) {
let alias = alias.into();
let now = crate::key_activity::now_secs();
self.effects_mut()
.defer(move |app| app.record_key_use(&alias, now));
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
use std::rc::Rc;
fn test_app() -> App {
let config = crate::ssh_config::model::SshConfigFile {
elements: crate::ssh_config::model::SshConfigFile::parse_content(""),
path: std::path::PathBuf::from("ctx_test_config"),
crlf: false,
bom: false,
};
App::new(config)
}
#[test]
fn defer_then_apply_consumes_the_queue() {
let mut effects = Effects::default();
effects.defer(|_app| {});
effects.defer(|_app| {});
assert_eq!(effects.queue.len(), 2, "defer pushes onto the queue");
effects.apply(&mut test_app());
}
#[test]
fn apply_runs_every_effect_in_push_order() {
let order = Rc::new(RefCell::new(Vec::new()));
let mut effects = Effects::default();
for i in 0..5 {
let order = Rc::clone(&order);
effects.defer(move |_app| order.borrow_mut().push(i));
}
effects.apply(&mut test_app());
assert_eq!(*order.borrow(), vec![0, 1, 2, 3, 4]);
}
#[test]
fn apply_threads_the_real_app_into_each_effect() {
let mut effects = Effects::default();
effects.defer(|app| app.demo_mode = true);
let mut app = test_app();
assert!(!app.demo_mode);
effects.apply(&mut app);
assert!(
app.demo_mode,
"deferred effect must mutate the App passed to apply"
);
}
#[test]
fn apply_on_empty_queue_is_a_noop() {
let effects = Effects::default();
effects.apply(&mut test_app());
}
#[cfg(debug_assertions)]
#[test]
#[should_panic(expected = "dropped unapplied")]
fn dropping_unapplied_effects_panics_in_debug() {
let mut effects = Effects::default();
effects.defer(|_app| {});
}
}