//! Event system for progress and status reporting. //! //! The lib fires events during operations like rip(). //! The app registers a callback to receive them. //! No display logic, no English text — just data. //! //! ```rust,ignore //! disc.rip(&mut session, 0, output, |event| { //! match event.kind { //! EventKind::BytesRead { bytes, total } => update_progress(bytes, total), //! EventKind::SectorSkipped { sector } => log_skip(sector), //! EventKind::BatchSizeChanged { new_size, .. } => note_recovery(new_size), //! _ => {} //! } //! }); //! ``` //! //! Note: the library currently emits only `BytesRead`, `SectorSkipped`, //! and `BatchSizeChanged`. The other [`EventKind`] variants are part of //! the stable event vocabulary for consumers (and future emit sites) but //! are not produced by the library today. use crate::error::Error; /// An event fired by the lib during operations. #[derive(Debug)] pub struct Event { pub kind: EventKind, } /// Types of events the lib can fire. #[derive(Debug)] pub enum EventKind { // ── Init sequence events ──────────────────────────────────────── /// Drive opened successfully. DriveOpened { device: String }, /// Drive is ready (disc spun up). DriveReady, /// Firmware init completed. InitComplete { success: bool }, /// Disc probe completed. ProbeComplete { success: bool }, /// Disc scan completed. ScanComplete { titles: usize }, // ── Read events ───────────────────────────────────────────────── /// Bytes successfully read and written to output. BytesRead { /// Bytes written so far. bytes: u64, /// Total bytes expected (0 if unknown). total: u64, }, /// A read error occurred. The lib will retry automatically. ReadError { /// Sector that failed. sector: u64, /// Error code. error: Error, }, /// Retrying a failed read. Retry { /// Current attempt number (1-based). attempt: u32, }, /// Drive speed changed (error recovery or restoration). SpeedChange { /// New speed in KB/s (0xFFFF = max). speed_kbs: u16, }, /// Starting a new disc extent. ExtentStart { /// Extent index (0-based). index: usize, /// First sector of extent. start_sector: u64, /// Number of sectors in extent. sector_count: u64, }, /// Sector recovered after a retry (Drive::read multi-phase recovery). SectorRecovered { sector: u64 }, /// Sector unreadable, zero-filled (skip mode). SectorSkipped { sector: u64 }, /// Adaptive batch sizer changed the read size. /// /// Fires on shrink (read failed at larger size) and on probe-up /// (enough clean reads to try larger again). Consumers use this to /// display a "recovering" state distinct from "ripping normally". BatchSizeChanged { new_size: u16, reason: BatchSizeReason, }, /// Operation complete. Complete { /// Total bytes written. bytes: u64, /// Total read errors encountered. errors: u32, }, } /// Why the adaptive batch sizer changed size. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum BatchSizeReason { /// Read failed; sizer halved the batch. Shrunk, /// Clean-read streak threshold hit; sizer doubled toward preferred. Probed, } /// A no-op event handler. Ignores all events. pub fn ignore(_event: Event) {} #[cfg(test)] mod tests { use super::*; /// BatchSizeReason::Shrunk != BatchSizeReason::Probed. /// These two variants carry distinct meanings (error vs. recovery); they /// must not compare as equal. /// Mutation: deriving PartialEq without proper variant discrimination /// could make two distinct variants equal. #[test] fn batch_size_reason_variants_are_not_equal() { assert_ne!(BatchSizeReason::Shrunk, BatchSizeReason::Probed); } /// BatchSizeReason is Clone + Copy: cloning does not move the original. /// This is required because EventKind::BatchSizeChanged embeds it by value. /// Mutation: removing Copy would require the caller to clone explicitly; /// code that passes reason by value would fail to compile. #[test] fn batch_size_reason_is_copy() { let r = BatchSizeReason::Shrunk; let _r2 = r; // copy, not move let _r3 = r; // r still usable after copy } }