use ratatui::layout::{Constraint, Direction, Layout, Rect};
pub const INTERVAL_PRESETS: &[u64] = &[
30, 60, 120, 300, 600, 900, 1800, 3600, 7200, 10800, 21600, 43200, 86400, 172800, 259200, 345600, 432000, 518400, 604800, ];
pub fn next_interval_preset(current: u64) -> u64 {
for &preset in INTERVAL_PRESETS {
if preset > current {
return preset;
}
}
*INTERVAL_PRESETS.last().unwrap_or(¤t)
}
pub fn prev_interval_preset(current: u64) -> u64 {
let mut prev = INTERVAL_PRESETS[0];
for &preset in INTERVAL_PRESETS {
if preset >= current {
return prev;
}
prev = preset;
}
prev
}
pub fn format_interval(secs: u64) -> String {
if secs < 60 {
format!("{}s", secs)
} else if secs < 3600 {
format!("{}m", secs / 60)
} else if secs < 86400 {
format!("{}h", secs / 3600)
} else if secs >= 604800 && secs % 604800 == 0 {
format!("{}w", secs / 604800)
} else {
format!("{}d", secs / 86400)
}
}
pub fn parse_time_to_datetime(time_str: &str) -> Option<chrono::DateTime<chrono::Utc>> {
use chrono::{Local, NaiveTime, Utc};
let parts: Vec<&str> = time_str.split(':').collect();
if parts.len() != 2 {
return None;
}
let hour: u32 = parts[0].parse().ok()?;
let minute: u32 = parts[1].parse().ok()?;
let target_time = NaiveTime::from_hms_opt(hour, minute, 0)?;
let now = Local::now();
let today = now.date_naive();
let target_datetime = if let Some(dt) = today.and_time(target_time).and_local_timezone(Local).single() {
if dt > now {
dt
} else {
let tomorrow = today.succ_opt()?;
tomorrow.and_time(target_time).and_local_timezone(Local).single()?
}
} else {
return None;
};
Some(target_datetime.with_timezone(&Utc))
}
pub fn parse_duration_str(s: &str) -> Option<u64> {
let s = s.trim().to_lowercase();
if s.is_empty() {
return None;
}
if let Ok(secs) = s.parse::<u64>() {
return Some(secs);
}
let (num_str, unit) = if s.ends_with('s') {
(&s[..s.len()-1], 1u64)
} else if s.ends_with('m') {
(&s[..s.len()-1], 60u64)
} else if s.ends_with('h') {
(&s[..s.len()-1], 3600u64)
} else if s.ends_with('d') {
(&s[..s.len()-1], 86400u64)
} else if s.ends_with('w') {
(&s[..s.len()-1], 604800u64)
} else {
return None;
};
num_str.parse::<u64>().ok().map(|n| n * unit)
}
pub fn parse_extraction_string(s: &str) -> crate::watch::Extraction {
use crate::watch::Extraction;
let s = s.trim().to_lowercase();
if s == "auto" || s.is_empty() {
Extraction::Auto
} else if s == "full" {
Extraction::Full
} else if s == "rss" {
Extraction::Rss
} else if s == "jsonld" || s == "json_ld" || s == "json-ld" {
Extraction::JsonLd { types: None }
} else if let Some(selector) = s.strip_prefix("css:") {
Extraction::Selector { selector: selector.to_string() }
} else if let Some(tags) = s.strip_prefix("meta:") {
Extraction::Meta { tags: tags.split(',').map(|t| t.trim().to_string()).collect() }
} else if let Some(types) = s.strip_prefix("jsonld:") {
Extraction::JsonLd { types: Some(types.split(',').map(|t| t.trim().to_string()).collect()) }
} else if !s.is_empty() {
Extraction::Selector { selector: s.to_string() }
} else {
Extraction::Auto
}
}
pub fn build_notify_target(notify_type: &super::types::NotifyType, value: &str) -> Option<crate::config::NotifyTarget> {
use crate::config::NotifyTarget;
use super::types::NotifyType;
let value = value.trim();
if value.is_empty() {
return None;
}
match notify_type {
NotifyType::Ntfy => Some(NotifyTarget::Ntfy {
topic: value.to_string(),
server: None,
}),
NotifyType::Gotify => {
let parts: Vec<&str> = value.splitn(2, '|').collect();
if parts.len() == 2 && !parts[0].is_empty() && !parts[1].is_empty() {
Some(NotifyTarget::Gotify {
server: parts[0].to_string(),
token: parts[1].to_string(),
})
} else {
None
}
}
NotifyType::Slack => Some(NotifyTarget::Slack {
webhook_url: value.to_string(),
}),
NotifyType::Discord => Some(NotifyTarget::Discord {
webhook_url: value.to_string(),
}),
NotifyType::Telegram => {
let parts: Vec<&str> = value.splitn(2, '|').collect();
if parts.len() == 2 && !parts[0].is_empty() && !parts[1].is_empty() {
Some(NotifyTarget::Telegram {
chat_id: parts[0].to_string(),
bot_token: parts[1].to_string(),
})
} else {
None
}
}
NotifyType::Pushover => {
let parts: Vec<&str> = value.splitn(2, '|').collect();
if parts.len() == 2 && !parts[0].is_empty() && !parts[1].is_empty() {
Some(NotifyTarget::Pushover {
user_key: parts[0].to_string(),
api_token: parts[1].to_string(),
})
} else {
None
}
}
NotifyType::Command => Some(NotifyTarget::Command {
command: value.to_string(),
}),
}
}
pub fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
let popup_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(r);
Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(popup_layout[1])[1]
}