// // RuleStatus.swift // Severed // import Foundation /// The live state of a rule at a moment in time. Derived, never stored. enum RuleStatus: Equatable, Sendable { case disabled /// Enabled but no days selected, so it never fires. case dormant /// Currently blocking; ends at the associated date. case active(until: Date) /// The user unblocked the current window; blocking resumes at the next window. case paused(until: Date) case upcoming(startsAt: Date) var isActive: Bool { if case .active = self { return true } return false } /// Short status label shown on rule cards and detail sheets: /// "6h left", "Starts in 22h", "Paused", "Disabled". func label(relativeTo now: Date) -> String { switch self { case .disabled: "Disabled" case .dormant: "No days selected" case .paused: "Paused" case .active(let until): "\(Self.countdown(from: now, to: until)) left" case .upcoming(let start): "Starts in \(Self.countdown(from: now, to: start))" } } /// Compact countdown matching the reference app: minutes under an hour, /// hours (rounded up) under two days, then days. static func countdown(from now: Date, to target: Date) -> String { let minutes = max(1, Int(ceil(target.timeIntervalSince(now) / 60))) guard minutes >= 60 else { return "\(minutes)m" } let hours = (minutes + 59) / 60 guard hours >= 48 else { return "\(hours)h" } return "\(hours / 24)d" } } extension BlockingRule { /// Live status of this rule. Schedule rules derive it from their time window; /// time/open-limit rules report upcoming based on their enabled days only /// (their blocking is triggered by usage, not the clock). func status(at now: Date = .now, calendar: Calendar = .current) -> RuleStatus { guard isEnabled else { return .disabled } guard !days.isEmpty else { return .dormant } guard kind == .schedule else { guard let next = schedule.nextStart(after: now, calendar: calendar) else { return .dormant } return .upcoming(startsAt: next) } if let window = schedule.activeWindow(containing: now, calendar: calendar) { if let pausedUntil, pausedUntil > now { return .paused(until: min(pausedUntil, window.end)) } return .active(until: window.end) } if let next = schedule.nextStart(after: now, calendar: calendar) { return .upcoming(startsAt: next) } return .dormant } }