use crate::client;
use crate::config::Paths;
use serde_json::Value;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
#[derive(Debug, Default, PartialEq)]
pub struct Seen {
pub session: Option<String>,
pub cwd: Option<String>,
pub model: String,
pub pct: Option<u8>,
pub size: Option<u64>,
pub input: Option<u64>,
pub used: Option<u64>,
pub five_hour: Option<Limit>,
pub seven_day: Option<Limit>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Limit {
pub used: f64,
pub resets_at: i64,
}
pub fn read(v: &Value) -> Seen {
let s = |p: &str| v.pointer(p).and_then(Value::as_str).map(str::to_string);
let n = |p: &str| v.pointer(p).and_then(Value::as_f64);
let tokens = |p: &str| {
n(p).filter(|x| x.is_finite() && *x >= 0.0)
.map(|x| x as u64)
};
let now = [
"input_tokens",
"cache_creation_input_tokens",
"cache_read_input_tokens",
]
.map(|k| tokens(&format!("/context_window/current_usage/{k}")));
let input = tokens("/context_window/total_input_tokens");
let agent = s("/agent/name").filter(|a| !a.trim().is_empty());
let model = s("/model/display_name").unwrap_or_default();
let model = match agent {
Some(a) if !model.is_empty() => format!("{a} ยท {model}"),
Some(a) => a,
None => model,
};
Seen {
session: s("/session_id").filter(|x| crate::desk::valid_session(x)),
cwd: s("/cwd").or_else(|| s("/workspace/current_dir")),
model: clean(&model),
pct: n("/context_window/used_percentage")
.filter(|p| p.is_finite())
.map(|p| p.clamp(0.0, 100.0).round() as u8),
size: n("/context_window/context_window_size")
.filter(|x| x.is_finite() && *x > 0.0)
.map(|x| x as u64),
input,
used: match now.iter().any(Option::is_some) {
true => Some(now.iter().flatten().sum()),
false => input,
},
five_hour: limit(v, "five_hour"),
seven_day: limit(v, "seven_day"),
}
}
fn limit(v: &Value, window: &str) -> Option<Limit> {
let used = v
.pointer(&format!("/rate_limits/{window}/used_percentage"))
.and_then(Value::as_f64)
.filter(|p| p.is_finite())?;
let resets_at = v
.pointer(&format!("/rate_limits/{window}/resets_at"))
.and_then(Value::as_f64)
.filter(|t| t.is_finite() && *t > 0.0)? as i64;
Some(Limit {
used: used.clamp(0.0, 100.0),
resets_at,
})
}
pub fn clean(s: &str) -> String {
s.chars()
.filter(|c| !c.is_control())
.take(48)
.collect::<String>()
.trim()
.to_string()
}
pub fn run(paths: &Paths) -> anyhow::Result<()> {
let mut input = Vec::new();
if std::io::stdin().read_to_end(&mut input).is_err() {
return Ok(());
}
if let Some(cmd) = saved_command(&before_path(paths)) {
chain(&cmd, &input);
}
let Ok(v) = serde_json::from_slice::<Value>(&input) else {
return Ok(());
};
let seen = read(&v);
let pane = std::env::var("SNYVI_SESSION")
.ok()
.filter(|p| crate::pane::valid_id(p));
if let (Some(cwd), Some(sid)) = (&seen.cwd, &seen.session) {
crate::session::record(paths, cwd, sid, pane.as_deref());
}
if let Some(pane) = pane {
client::agent_context(paths, &pane, &seen);
}
Ok(())
}
pub fn before_path(paths: &Paths) -> PathBuf {
paths.config_dir.join("statusline-before.json")
}
pub fn saved_command(path: &Path) -> Option<String> {
let v: Value = serde_json::from_str(&std::fs::read_to_string(path).ok()?).ok()?;
let c = v.get("command")?.as_str()?.trim();
(!c.is_empty()).then(|| c.to_string())
}
fn chain(cmd: &str, input: &[u8]) {
#[cfg(windows)]
let mut c = std::process::Command::new("cmd");
#[cfg(windows)]
c.args(["/C", cmd]);
#[cfg(not(windows))]
let mut c = std::process::Command::new("sh");
#[cfg(not(windows))]
c.args(["-c", cmd]);
let Ok(mut child) = c
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.spawn()
else {
return;
};
if let Some(mut stdin) = child.stdin.take() {
let _ = stdin.write_all(input);
}
if let Ok(out) = child.wait_with_output() {
let _ = std::io::stdout().write_all(&out.stdout);
}
}
pub fn ours(entry: &Value) -> bool {
entry
.get("command")
.and_then(Value::as_str)
.is_some_and(|c| c.ends_with(" statusline") && c.contains("snyvi"))
}
pub fn install_into(settings: &mut Value, command: &str, before: &Path) -> anyhow::Result<bool> {
let obj = settings
.as_object_mut()
.ok_or_else(|| anyhow::anyhow!("settings.json is not an object"))?;
let mut ours_now = serde_json::json!({ "type": "command", "command": command });
match obj.get("statusLine") {
Some(e) if ours(e) => {
if e.get("command").and_then(Value::as_str) == Some(command) {
return Ok(false);
}
ours_now = e.clone();
ours_now["command"] = Value::from(command);
}
Some(e) => {
if let Some(dir) = before.parent() {
std::fs::create_dir_all(dir)?;
}
std::fs::write(before, serde_json::to_string_pretty(e)?)?;
if let Some(p) = e.get("padding") {
ours_now["padding"] = p.clone();
}
}
None => {}
}
obj.insert("statusLine".into(), ours_now);
Ok(true)
}
pub fn remove_from(settings: &mut Value, before: &Path) -> bool {
let Some(obj) = settings.as_object_mut() else {
return false;
};
if !obj.get("statusLine").is_some_and(ours) {
return false;
}
let kept = std::fs::read_to_string(before)
.ok()
.and_then(|s| serde_json::from_str::<Value>(&s).ok());
match kept {
Some(k) => obj.insert("statusLine".into(), k),
None => obj.remove("statusLine"),
};
let _ = std::fs::remove_file(before);
true
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
const PAYLOAD: &str = include_str!("../tests/fixtures/statusline.json");
#[test]
fn a_real_payload_gives_the_model_and_how_full_the_window_is() {
let v: Value = serde_json::from_str(PAYLOAD).unwrap();
let s = read(&v);
assert_eq!(s.model, "Fable 5.1");
assert_eq!(s.pct, Some(43));
assert_eq!(s.size, Some(200_000));
assert_eq!(s.input, Some(86_120));
assert_eq!(
s.used,
Some(86_120),
"no current_usage: the total stands in"
);
assert_eq!(
s.session.as_deref(),
Some("f94bace6-1a2b-4c3d-8e9f-0123456789ab")
);
assert_eq!(s.cwd.as_deref(), Some("/home/reader/Projects/bos_dog"));
}
#[test]
fn a_thin_or_odd_payload_says_what_it_can() {
assert_eq!(read(&json!({})), Seen::default());
let s = read(&json!({
"session_id": "not-a-uuid",
"model": { "display_name": "Fable\n5.1\u{7}" },
"agent": { "name": "reviewer" },
"context_window": { "used_percentage": 131.6, "context_window_size": -5 }
}));
assert_eq!(s.session, None, "only a UUID is kept");
assert_eq!(s.model, "reviewer ยท Fable5.1");
assert_eq!(s.pct, Some(100));
assert_eq!(s.size, None);
assert_eq!(s.five_hour, None);
}
#[test]
fn the_rate_limit_windows_are_read_when_they_are_there() {
let s = read(&json!({ "rate_limits": {
"five_hour": { "used_percentage": 23.5, "resets_at": 1738425600 },
"seven_day": { "used_percentage": "lots", "resets_at": 1738857600 },
"spend_limit": { "used_percentage": 62.8, "resets_at": 1740787200 }
}}));
assert_eq!(
s.five_hour,
Some(Limit {
used: 23.5,
resets_at: 1738425600
})
);
assert_eq!(s.seven_day, None);
let over = read(
&json!({ "rate_limits": { "seven_day": { "used_percentage": 140, "resets_at": 5 } } }),
);
assert_eq!(
over.seven_day,
Some(Limit {
used: 100.0,
resets_at: 5
})
);
}
#[test]
fn the_window_now_is_input_and_cache_not_output() {
let window = |current: Value| {
json!({ "context_window": {
"total_input_tokens": 15500,
"total_output_tokens": 1200,
"context_window_size": 200000,
"used_percentage": 8,
"remaining_percentage": 92,
"current_usage": current,
}})
};
let s = read(&window(json!({
"input_tokens": 8500,
"output_tokens": 1200,
"cache_creation_input_tokens": 5000,
"cache_read_input_tokens": 2000
})));
assert_eq!(s.used, Some(15_500));
assert_eq!(read(&window(Value::Null)).used, Some(15_500));
let s = read(&json!({ "context_window": {
"total_input_tokens": 40_000,
"current_usage": null
}}));
assert_eq!(s.used, Some(40_000), "the total stands in");
let s = read(&json!({ "model": { "display_name": "Fable 5.1" } }));
assert_eq!(s.used, None);
}
#[test]
fn a_readers_own_line_is_kept_and_given_back() {
let dir = crate::store::tempdir::Dir::new("snyvi-statusline");
let before = dir.path.join("statusline-before.json");
let mine =
json!({ "type": "command", "command": "~/bin/line.sh --short 'x y'", "padding": 1 });
let mut settings = json!({ "statusLine": mine.clone(), "model": "opus" });
assert!(install_into(&mut settings, "snyvi statusline", &before).unwrap());
assert_eq!(settings["statusLine"]["command"], "snyvi statusline");
assert_eq!(settings["statusLine"]["padding"], 1);
assert_eq!(
saved_command(&before).as_deref(),
Some("~/bin/line.sh --short 'x y'")
);
assert!(!install_into(&mut settings, "snyvi statusline", &before).unwrap());
assert!(install_into(&mut settings, "/opt/snyvi statusline", &before).unwrap());
assert_eq!(
saved_command(&before).as_deref(),
Some("~/bin/line.sh --short 'x y'")
);
assert!(remove_from(&mut settings, &before));
assert_eq!(settings["statusLine"], mine);
assert!(!before.exists());
assert!(!remove_from(&mut settings, &before), "theirs is left alone");
}
#[test]
fn with_no_line_before_ours_comes_and_goes_alone() {
let dir = crate::store::tempdir::Dir::new("snyvi-statusline");
let before = dir.path.join("statusline-before.json");
let mut settings = json!({});
assert!(install_into(&mut settings, "snyvi statusline", &before).unwrap());
assert!(!before.exists());
assert!(remove_from(&mut settings, &before));
assert_eq!(settings, json!({}));
}
}