use std::{cell::Cell, cell::RefCell, io, rc::Rc};
use ratatui::Frame;
use super::{modal::ModalSignal, terminal::Tui};
type Guard = dyn Fn(
&mut Tui,
QuitKind,
&dyn Fn(&mut Frame),
) -> io::Result<ModalSignal<bool>>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QuitKind {
Soft,
Hard,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum QuitConfirm {
#[default]
Never,
Soft,
Hard,
Both,
}
impl QuitConfirm {
fn asks(self, kind: QuitKind) -> bool {
match self {
QuitConfirm::Never => false,
QuitConfirm::Soft => kind == QuitKind::Soft,
QuitConfirm::Hard => kind == QuitKind::Hard,
QuitConfirm::Both => true,
}
}
}
thread_local! {
static CONFIRM: Cell<QuitConfirm> = const { Cell::new(QuitConfirm::Never) };
static GUARD: RefCell<Option<Rc<Guard>>> = const { RefCell::new(None) };
static ASKING: Cell<bool> = const { Cell::new(false) };
}
pub fn confirm_mode() -> QuitConfirm {
CONFIRM.with(Cell::get)
}
pub fn set_confirm(mode: QuitConfirm) {
CONFIRM.with(|policy| policy.set(mode));
}
pub fn set_guard(
guard: impl Fn(
&mut Tui,
QuitKind,
&dyn Fn(&mut Frame),
) -> io::Result<ModalSignal<bool>>
+ 'static,
) {
GUARD.with(|slot| *slot.borrow_mut() = Some(Rc::new(guard)));
}
pub fn request(
tui: &mut Tui,
kind: QuitKind,
render_bg: &dyn Fn(&mut Frame),
) -> bool {
if !should_ask(kind) {
return true;
}
let Some(guard) = GUARD.with(|slot| slot.borrow().clone()) else {
return true;
};
let _asking = Asking::enter();
match guard(tui, kind, render_bg) {
Ok(ModalSignal::Value(answer)) => answer,
Ok(ModalSignal::Quit) => true,
Ok(ModalSignal::Cancelled) => false,
Err(error) => {
log::warn!("quit confirmation failed: {error}; quitting");
true
}
}
}
fn should_ask(kind: QuitKind) -> bool {
if !confirm_mode().asks(kind) {
return false;
}
if ASKING.with(Cell::get) {
return false;
}
if GUARD.with(|slot| slot.borrow().is_none()) {
log::warn!("quit confirmation requested without a guard; quitting");
return false;
}
true
}
struct Asking;
impl Asking {
fn enter() -> Self {
ASKING.with(|flag| flag.set(true));
Self
}
}
impl Drop for Asking {
fn drop(&mut self) {
ASKING.with(|flag| flag.set(false));
}
}
#[cfg(test)]
mod tests {
use super::*;
fn with_policy(mode: QuitConfirm, body: impl FnOnce()) {
let before = confirm_mode();
set_confirm(mode);
body();
set_confirm(before);
}
fn register_guard() {
set_guard(|_tui, _kind, _bg| Ok(ModalSignal::Value(true)));
}
fn clear_guard() {
GUARD.with(|slot| *slot.borrow_mut() = None);
}
#[test]
fn nothing_is_asked_by_default() {
assert_eq!(confirm_mode(), QuitConfirm::Never);
assert!(!should_ask(QuitKind::Soft));
assert!(!should_ask(QuitKind::Hard));
}
#[test]
fn without_a_guard_nothing_is_asked_even_when_the_policy_wants_to() {
clear_guard();
with_policy(QuitConfirm::Both, || {
assert!(!should_ask(QuitKind::Soft));
assert!(!should_ask(QuitKind::Hard));
});
}
#[test]
fn a_policy_asks_only_for_its_own_kind() {
register_guard();
with_policy(QuitConfirm::Soft, || {
assert!(should_ask(QuitKind::Soft));
assert!(!should_ask(QuitKind::Hard));
});
with_policy(QuitConfirm::Hard, || {
assert!(!should_ask(QuitKind::Soft));
assert!(should_ask(QuitKind::Hard));
});
with_policy(QuitConfirm::Both, || {
assert!(should_ask(QuitKind::Soft));
assert!(should_ask(QuitKind::Hard));
});
clear_guard();
}
#[test]
fn a_guard_already_on_screen_is_not_asked_again() {
register_guard();
with_policy(QuitConfirm::Both, || {
let _asking = Asking::enter();
assert!(!should_ask(QuitKind::Hard));
});
with_policy(QuitConfirm::Both, || {
assert!(should_ask(QuitKind::Hard));
});
clear_guard();
}
}