use serde_json::{Map, Value};
mod ceiling;
mod fields;
mod json;
mod knobs;
mod panels;
mod prices;
mod prune;
pub use fields::SeenKind;
use json::{default_root, descend, parse_or_default, string_array};
pub use panels::Panel;
use std::fs;
use std::hash::{Hash, Hasher};
use std::io;
use std::path::PathBuf;
use std::time::Instant;
const EXPAND_RESPONSES: &str = "transcript_expand_responses";
const EXPAND_OTHERS: &str = "transcript_expand_others";
pub trait Clock: Send + Sync {
fn now(&self) -> Instant;
fn stamp(&self) -> String;
}
#[derive(Clone, Copy)]
pub struct SystemClock;
impl Clock for SystemClock {
fn now(&self) -> Instant {
Instant::now()
}
fn stamp(&self) -> String {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_secs())
.to_string()
}
}
pub(crate) fn format_iso8601(
year: i64,
month: i64,
day: i64,
hour: i64,
minute: i64,
second: i64,
) -> String {
format!("{year:04}-{month:02}-{day:02} {hour:02}:{minute:02}:{second:02}Z")
}
pub fn iso8601_extended(epoch_secs: i64) -> String {
let days = epoch_secs.div_euclid(86_400);
let secs_of_day = epoch_secs.rem_euclid(86_400);
let (year, month, day) = civil_from_days(days);
format_iso8601(
year,
month,
day,
secs_of_day / 3600,
(secs_of_day % 3600) / 60,
secs_of_day % 60,
)
}
fn civil_from_days(z: i64) -> (i64, i64, i64) {
let z = z + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = z - era * 146_097; let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let day = doy - (153 * mp + 2) / 5 + 1; let month = if mp < 10 { mp + 3 } else { mp - 9 }; let year = if month <= 2 { y + 1 } else { y };
(year, month, day)
}
pub fn content_hash(bytes: &[u8]) -> u64 {
let mut h = std::collections::hash_map::DefaultHasher::new();
bytes.hash(&mut h);
h.finish()
}
pub fn derive_startup_focus(roster: &[&str], attention: &[&str]) -> Option<String> {
for &w in roster {
if attention.contains(&w) {
return Some(w.to_string());
}
}
roster.first().map(|w| (*w).to_string())
}
pub struct UiState {
path: PathBuf,
root: Map<String, Value>,
last_hash: Option<u64>,
}
impl UiState {
pub fn open(path: PathBuf) -> Self {
let (root, last_hash) = match fs::read(&path) {
Ok(bytes) => (parse_or_default(&bytes), Some(content_hash(&bytes))),
Err(_) => (default_root(), None),
};
Self {
path,
root,
last_hash,
}
}
pub fn is_echo(&self, bytes: &[u8]) -> bool {
self.last_hash == Some(content_hash(bytes))
}
pub fn adopt(&mut self, bytes: &[u8]) {
self.root = parse_or_default(bytes);
self.last_hash = Some(content_hash(bytes));
}
fn save(&mut self) {
let bytes = self.serialize();
let hash = content_hash(&bytes);
if self.last_hash == Some(hash) {
return;
}
if self.write_atomic(&bytes).is_ok() {
self.last_hash = Some(hash);
}
}
fn serialize(&self) -> Vec<u8> {
serde_json::to_vec_pretty(&self.root).unwrap_or_default()
}
fn write_atomic(&self, bytes: &[u8]) -> io::Result<()> {
let dir = self.path.parent().ok_or(io::Error::other("no parent"))?;
fs::create_dir_all(dir)?;
let name = self
.path
.file_name()
.ok_or(io::Error::other("no file name"))?;
let tmp_name = format!(".{}.yog-tmp-{}", name.to_string_lossy(), std::process::id());
let tmp = dir.join(tmp_name);
fs::write(&tmp, bytes)?;
fs::rename(&tmp, &self.path)
}
}
#[cfg(test)]
mod tests;