use serde_json::Value;
use std::path::PathBuf;
pub struct SwitchEvent {
pub ts: i64,
pub tool: String,
pub account: String,
}
fn timeline_path() -> Option<PathBuf> {
if let Some(p) = std::env::var_os("SESSIONWIKI_SWAPDEX_TIMELINE") {
return Some(p.into());
}
Some(dirs::data_dir()?.join("swapdex").join("timeline.jsonl"))
}
pub fn load_events() -> Vec<SwitchEvent> {
let Some(path) = timeline_path() else {
return Vec::new();
};
let Ok(text) = std::fs::read_to_string(path) else {
return Vec::new();
};
let mut out = Vec::new();
let lines: Vec<&str> = text.lines().collect();
let tail = lines.len().saturating_sub(4000);
for line in &lines[tail..] {
let Ok(v) = serde_json::from_str::<Value>(line) else {
continue;
};
if !matches!(v["action"].as_str(), Some("use") | Some("restore")) {
continue;
}
if let (Some(ts), Some(tool), Some(account)) =
(v["ts"].as_i64(), v["tool"].as_str(), v["account"].as_str())
{
out.push(SwitchEvent {
ts,
tool: tool.to_string(),
account: account.chars().filter(|c| !c.is_control()).collect(),
});
}
}
out
}
pub fn account_for(
events: &[SwitchEvent],
tool: &str,
started_rfc3339: Option<&str>,
) -> Option<String> {
let started = chrono::DateTime::parse_from_rfc3339(started_rfc3339?)
.ok()?
.timestamp();
events
.iter()
.filter(|e| e.tool == tool && e.ts <= started)
.max_by_key(|e| e.ts)
.map(|e| e.account.clone())
}
pub fn annotate<'a, I: IntoIterator<Item = &'a mut crate::index::SessionRow>>(rows: I) {
let events = load_events();
if events.is_empty() {
return;
}
for r in rows {
r.account = account_for(&events, &r.tool, r.started.as_deref());
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ev(ts: i64, tool: &str, account: &str) -> SwitchEvent {
SwitchEvent {
ts,
tool: tool.into(),
account: account.into(),
}
}
#[test]
fn attributes_to_the_last_switch_before_start() {
let events = vec![
ev(100, "codex", "work"),
ev(200, "codex", "personal"),
ev(150, "claude-code", "work"),
];
let started = chrono::DateTime::from_timestamp(250, 0)
.unwrap()
.to_rfc3339();
assert_eq!(
account_for(&events, "codex", Some(&started)).as_deref(),
Some("personal")
);
assert_eq!(
account_for(&events, "claude-code", Some(&started)).as_deref(),
Some("work")
);
let early = chrono::DateTime::from_timestamp(50, 0)
.unwrap()
.to_rfc3339();
assert_eq!(account_for(&events, "codex", Some(&early)), None);
assert_eq!(account_for(&events, "codex", None), None);
}
}