Menu
cosmic-mobile
publicLatest change bd10038fdf127d3b0f9f4ddc6c5afd463c4d2e34 - Calendar: iOS month grid, today/selection, event dots, day agenda, timed events via chrono by AkurAI Build
use chrono::{NaiveDate, NaiveTime};
use std::{io, path::PathBuf};
/// Calendar "when" field: date with optional 24h time. Lexical order equals chronological order.
pub fn parse_when(text: &str) -> Option<(NaiveDate, Option<NaiveTime>)> {
let (date, time) = match text.split_once(' ') {
Some((d, t)) => (d, Some(t)),
None => (text, None),
};
let date = NaiveDate::parse_from_str(date, "%Y-%m-%d").ok()?;
if date.format("%Y-%m-%d").to_string() != text.split(' ').next()? {
return None;
}
match time {
None => Some((date, None)),
Some(t) => Some((date, Some(NaiveTime::parse_from_str(t, "%H:%M").ok()?))),
}
}
pub struct Store {
pub rows: Vec<(String, String, String, String)>,
path: PathBuf,
}
impl Store {
pub fn for_app(&self, app: &str) -> Vec<&(String, String, String, String)> {
let mut rows: Vec<_> = self.rows.iter().filter(|r| r.0 == app).collect();
if app == "Calendar" {
rows.sort_by(|a, b| a.1.cmp(&b.1));
}
rows
}
pub fn load(path: PathBuf) -> io::Result<Self> {
let text = match std::fs::read_to_string(&path) {
Ok(text) => text,
Err(e) if e.kind() == io::ErrorKind::NotFound => String::new(),
Err(e) => return Err(e),
};
let mut rows = Vec::new();
for line in text.lines() {
let fields: Vec<_> = line.split('\t').collect();
if fields.len() != 4 {
return Err(io::Error::other("Invalid records; file preserved"));
}
rows.push((
fields[0].into(),
fields[1].into(),
fields[2].into(),
fields[3].into(),
));
}
Ok(Self { rows, path })
}
pub fn add(&mut self, app: &str, first: &str, second: &str) -> io::Result<()> {
let first = first.trim();
let second = second.trim();
if !["Phone", "Contacts", "Calendar", "Messages"].contains(&app)
|| [first, second]
.iter()
.any(|v| v.is_empty() || v.len() > 4096 || v.chars().any(char::is_control))
{
return Err(io::Error::other(
"Enter both fields; control characters are not allowed",
));
}
let number = match app {
"Contacts" => Some(second),
"Phone" | "Messages" => Some(first),
_ => None,
};
if let Some(number) = number {
if number.len() > 64
|| !number.bytes().any(|b| b.is_ascii_digit())
|| !number
.bytes()
.all(|b| b.is_ascii_digit() || b"+ -()*#".contains(&b))
{
return Err(io::Error::other("Enter a valid phone number"));
}
}
if app == "Calendar" && parse_when(first).is_none() {
return Err(io::Error::other("Use YYYY-MM-DD or YYYY-MM-DD HH:MM"));
}
let status = match app {
"Phone" => "Simulated — no call placed",
"Messages" => "Simulated — not sent",
_ => "Saved locally",
};
let mut rows = self.rows.clone();
rows.push((app.into(), first.into(), second.into(), status.into()));
let data: String = rows
.iter()
.map(|r| format!("{}\t{}\t{}\t{}\n", r.0, r.1, r.2, r.3))
.collect();
if let Some(parent) = self.path.parent() {
std::fs::create_dir_all(parent)?;
}
let tmp = self.path.with_extension("tmp");
std::fs::write(&tmp, data)?;
std::fs::rename(tmp, &self.path)?;
self.rows = rows;
Ok(())
}
}