limb 0.1.0

A focused CLI for git worktree management
Documentation
//! Cooperative cancellation via Ctrl-C / SIGTERM / SIGHUP.
//!
//! [`Cancel::install`] registers a single process-wide handler that, on
//! SIGINT (Ctrl-C), SIGTERM, or SIGHUP, flips an [`AtomicBool`] shared
//! with the [`Context`]. Subcommands call [`Cancel::check`] at safe
//! points to short-circuit with [`Canceled`]; rollback logic lives
//! co-located with the destructive step that might need it.
//!
//! The handler body only touches the atomic. Async-signal-safe on
//! Unix, compatible with `SetConsoleCtrlHandler` on Windows via the
//! [`ctrlc`] crate.
//!
//! [`Context`]: crate::context::Context
//! [`Canceled`]: crate::error::Canceled

use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};

use anyhow::Result;

use crate::error::Canceled;

/// Handle to the process-wide cancel flag.
#[derive(Clone)]
pub struct Cancel {
    flag: Arc<AtomicBool>,
}

impl Cancel {
    /// Installs the signal handler and returns a handle.
    ///
    /// Called once at program start. If handler registration fails (for
    /// example because one is already installed), a warning is printed
    /// and a no-op [`Cancel`] is returned. The CLI still runs, just
    /// without graceful-cancel rollback.
    #[must_use]
    pub fn install() -> Self {
        let flag = Arc::new(AtomicBool::new(false));
        let handler_flag = Arc::clone(&flag);
        if let Err(e) = ctrlc::set_handler(move || {
            handler_flag.store(true, Ordering::SeqCst);
        }) {
            anstream::eprintln!("warning: failed to install signal handler: {e}");
        }
        Self { flag }
    }

    /// Returns `true` if a cancellation signal has been delivered.
    #[must_use]
    pub fn is_set(&self) -> bool {
        self.flag.load(Ordering::SeqCst)
    }

    /// Returns [`Canceled`] if cancelled, else `Ok(())`.
    ///
    /// # Errors
    ///
    /// Returns [`Canceled`] when a cancellation signal has been
    /// received since the last check.
    pub fn check(&self) -> Result<()> {
        if self.is_set() {
            Err(Canceled.into())
        } else {
            Ok(())
        }
    }

    /// A non-installing handle for unit tests.
    #[cfg(test)]
    #[must_use]
    pub fn noop() -> Self {
        Self {
            flag: Arc::new(AtomicBool::new(false)),
        }
    }

    /// Flip the flag manually (tests only).
    #[cfg(test)]
    pub fn trigger(&self) {
        self.flag.store(true, Ordering::SeqCst);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn unset_returns_ok() {
        let c = Cancel::noop();
        assert!(!c.is_set());
        assert!(c.check().is_ok());
    }

    #[test]
    fn triggered_returns_canceled() {
        let c = Cancel::noop();
        c.trigger();
        assert!(c.is_set());
        let err = c.check().unwrap_err();
        assert!(err.downcast_ref::<Canceled>().is_some());
    }

    #[test]
    fn clones_share_flag() {
        let a = Cancel::noop();
        let b = a.clone();
        a.trigger();
        assert!(b.is_set());
    }
}