use crate::config::CshipConfig;
use crate::context::Context;
const DEFAULT_START_HOUR: u32 = 7;
const DEFAULT_END_HOUR: u32 = 17;
const DEFAULT_SYMBOL: &str = "⏰ ";
const PEAK_LABEL: &str = "Peak";
const SECS_PER_DAY: u64 = 86400;
const SECS_PER_HOUR: u64 = 3600;
pub fn render(_ctx: &Context, cfg: &CshipConfig) -> Option<String> {
let pk_cfg = cfg.peak_usage.as_ref();
if pk_cfg.and_then(|c| c.disabled) == Some(true) {
return None;
}
let now_secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.ok()?
.as_secs();
if !is_peak_time(now_secs, pk_cfg) {
return None;
}
let symbol = pk_cfg
.and_then(|c| c.symbol.as_deref())
.unwrap_or(DEFAULT_SYMBOL);
let style = pk_cfg.and_then(|c| c.style.as_deref());
if let Some(fmt) = pk_cfg.and_then(|c| c.format.as_deref()) {
return crate::format::apply_module_format(fmt, Some(PEAK_LABEL), Some(symbol), style);
}
let content = format!("{symbol}{PEAK_LABEL}");
Some(crate::ansi::apply_style(&content, style))
}
fn is_peak_time(utc_epoch_secs: u64, pk_cfg: Option<&crate::config::PeakUsageConfig>) -> bool {
let start = pk_cfg
.and_then(|c| c.start_hour)
.unwrap_or(DEFAULT_START_HOUR);
let end = pk_cfg.and_then(|c| c.end_hour).unwrap_or(DEFAULT_END_HOUR);
if start >= end {
tracing::warn!(
"cship.peak_usage: start_hour ({start}) >= end_hour ({end}); \
overnight wrap-around is not supported — module will never activate"
);
return false;
}
if start > 23 || end > 24 {
tracing::warn!(
"cship.peak_usage: start_hour ({start}) or end_hour ({end}) out of range \
(start: 0–23, end: 0–24)"
);
return false;
}
let offset_secs = pacific_offset_secs(utc_epoch_secs);
let pacific_secs = (utc_epoch_secs as i64 + offset_secs) as u64;
let day_secs = pacific_secs % SECS_PER_DAY;
let hour = (day_secs / SECS_PER_HOUR) as u32;
let days_since_epoch = pacific_secs / SECS_PER_DAY;
let weekday = ((days_since_epoch + 4) % 7) as u32;
let is_weekday = (1..=5).contains(&weekday); is_weekday && hour >= start && hour < end
}
fn pacific_offset_secs(utc_epoch_secs: u64) -> i64 {
let (year, month, day, hour) = utc_epoch_to_ymd_h(utc_epoch_secs);
let march_second_sunday = nth_sunday_of_month(year, 3, 2);
let november_first_sunday = nth_sunday_of_month(year, 11, 1);
let is_pdt = match month {
4..=10 => true, 1..=2 | 12 => false, 3 => {
day > march_second_sunday || (day == march_second_sunday && hour >= 10)
}
11 => {
day < november_first_sunday || (day == november_first_sunday && hour < 9)
}
_ => false,
};
if is_pdt { -7 * 3600 } else { -8 * 3600 }
}
fn utc_epoch_to_ymd_h(secs: u64) -> (i32, u32, u32, u32) {
let z = (secs / SECS_PER_DAY) as i64 + 719468;
let era = if z >= 0 { z } else { z - 146096 } / 146097;
let doe = (z - era * 146097) as u64; let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
let day_secs = secs % SECS_PER_DAY;
let hour = (day_secs / SECS_PER_HOUR) as u32;
(y as i32, m as u32, d as u32, hour)
}
fn nth_sunday_of_month(year: i32, month: u32, n: u32) -> u32 {
let dow_first = day_of_week(year, month, 1);
let first_sunday = if dow_first == 0 {
1
} else {
1 + (7 - dow_first)
};
first_sunday + 7 * (n - 1)
}
fn day_of_week(mut year: i32, month: u32, day: u32) -> u32 {
static T: [u32; 12] = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4];
if month < 3 {
year -= 1;
}
((year + year / 4 - year / 100 + year / 400 + T[(month - 1) as usize] as i32 + day as i32) % 7)
as u32
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{CshipConfig, PeakUsageConfig};
use crate::context::Context;
fn pacific_to_utc(year: i32, month: u32, day: u32, hour: u32) -> u64 {
let days = civil_days_from_epoch(year, month, day);
let utc_approx = (days as u64) * SECS_PER_DAY + (hour as u64) * SECS_PER_HOUR;
let offset = pacific_offset_secs(utc_approx);
(utc_approx as i64 - offset) as u64
}
fn civil_days_from_epoch(year: i32, month: u32, day: u32) -> i64 {
let y = if month <= 2 {
year as i64 - 1
} else {
year as i64
};
let m = if month <= 2 {
month as i64 + 9
} else {
month as i64 - 3
};
let era = if y >= 0 { y } else { y - 399 } / 400;
let yoe = (y - era * 400) as u64;
let doy = (153 * m as u64 + 2) / 5 + day as u64 - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
era * 146097 + doe as i64 - 719468
}
#[test]
fn test_peak_active_weekday_morning() {
let ts = pacific_to_utc(2026, 4, 8, 10);
assert!(is_peak_time(ts, None));
}
#[test]
fn test_off_peak_weekday_evening() {
let ts = pacific_to_utc(2026, 4, 8, 20);
assert!(!is_peak_time(ts, None));
}
#[test]
fn test_off_peak_weekend() {
let ts = pacific_to_utc(2026, 4, 11, 12);
assert!(!is_peak_time(ts, None));
}
#[test]
fn test_peak_boundary_start() {
let ts = pacific_to_utc(2026, 4, 8, 7);
assert!(is_peak_time(ts, None));
}
#[test]
fn test_peak_boundary_end() {
let ts = pacific_to_utc(2026, 4, 8, 17);
assert!(!is_peak_time(ts, None));
}
#[test]
fn test_peak_boundary_just_before_end() {
let ts = pacific_to_utc(2026, 4, 8, 16);
assert!(is_peak_time(ts, None));
}
#[test]
fn test_custom_hours() {
let pk_cfg = PeakUsageConfig {
start_hour: Some(9),
end_hour: Some(18),
..Default::default()
};
let ts = pacific_to_utc(2026, 4, 8, 8);
assert!(!is_peak_time(ts, Some(&pk_cfg)));
let ts = pacific_to_utc(2026, 4, 8, 17);
assert!(is_peak_time(ts, Some(&pk_cfg)));
}
#[test]
fn test_dst_spring_forward_march() {
let ts = pacific_to_utc(2026, 3, 9, 10);
assert!(is_peak_time(ts, None));
}
#[test]
fn test_dst_fall_back_november() {
let ts = pacific_to_utc(2026, 11, 2, 10);
assert!(is_peak_time(ts, None));
}
#[test]
fn test_pst_january() {
let ts = pacific_to_utc(2026, 1, 7, 10);
assert!(is_peak_time(ts, None));
}
#[test]
fn test_disabled_returns_none() {
let cfg = CshipConfig {
peak_usage: Some(PeakUsageConfig {
disabled: Some(true),
..Default::default()
}),
..Default::default()
};
let ctx = Context::default();
assert_eq!(render(&ctx, &cfg), None);
}
#[test]
fn test_render_with_custom_symbol() {
let pk_cfg = PeakUsageConfig {
symbol: Some("🔥 ".to_string()),
..Default::default()
};
let symbol = pk_cfg.symbol.as_deref().unwrap_or(DEFAULT_SYMBOL);
let content = format!("{symbol}{PEAK_LABEL}");
assert_eq!(content, "🔥 Peak");
}
#[test]
fn test_day_of_week_known_dates() {
assert_eq!(day_of_week(2026, 4, 8), 3);
assert_eq!(day_of_week(2026, 4, 11), 6);
assert_eq!(day_of_week(2026, 4, 12), 0);
assert_eq!(day_of_week(1970, 1, 1), 4);
}
#[test]
fn test_nth_sunday_of_month() {
assert_eq!(nth_sunday_of_month(2026, 3, 2), 8);
assert_eq!(nth_sunday_of_month(2026, 11, 1), 1);
}
#[test]
fn test_utc_epoch_to_ymd_h_known_date() {
let days = civil_days_from_epoch(2026, 4, 8);
let secs = (days as u64) * SECS_PER_DAY + 14 * SECS_PER_HOUR; let (y, m, d, h) = utc_epoch_to_ymd_h(secs);
assert_eq!((y, m, d, h), (2026, 4, 8, 14));
}
#[test]
fn test_monday_is_peak() {
let ts = pacific_to_utc(2026, 4, 6, 10);
assert!(is_peak_time(ts, None));
}
#[test]
fn test_friday_is_peak() {
let ts = pacific_to_utc(2026, 4, 10, 10);
assert!(is_peak_time(ts, None));
}
#[test]
fn test_start_ge_end_returns_false() {
let pk_cfg = PeakUsageConfig {
start_hour: Some(18),
end_hour: Some(6),
..Default::default()
};
let ts = pacific_to_utc(2026, 4, 8, 10);
assert!(!is_peak_time(ts, Some(&pk_cfg)));
}
#[test]
fn test_start_equals_end_returns_false() {
let pk_cfg = PeakUsageConfig {
start_hour: Some(10),
end_hour: Some(10),
..Default::default()
};
let ts = pacific_to_utc(2026, 4, 8, 10);
assert!(!is_peak_time(ts, Some(&pk_cfg)));
}
#[test]
fn test_hour_out_of_range_returns_false() {
let pk_cfg = PeakUsageConfig {
start_hour: Some(25),
end_hour: Some(30),
..Default::default()
};
let ts = pacific_to_utc(2026, 4, 8, 10);
assert!(!is_peak_time(ts, Some(&pk_cfg)));
}
#[test]
fn test_end_hour_24_covers_full_day() {
let pk_cfg = PeakUsageConfig {
start_hour: Some(0),
end_hour: Some(24),
..Default::default()
};
let ts = pacific_to_utc(2026, 4, 8, 23);
assert!(is_peak_time(ts, Some(&pk_cfg)));
}
#[test]
fn test_end_hour_24_is_valid() {
let pk_cfg = PeakUsageConfig {
start_hour: Some(7),
end_hour: Some(24),
..Default::default()
};
let ts = pacific_to_utc(2026, 4, 8, 23);
assert!(is_peak_time(ts, Some(&pk_cfg)));
}
#[test]
fn test_sunday_is_not_peak() {
let ts = pacific_to_utc(2026, 4, 12, 10);
assert!(!is_peak_time(ts, None));
}
}