pub(crate) mod account;
pub(crate) mod branch;
pub(crate) mod compaction;
mod savings;
mod usage;
use std::io::Read as _;
use std::path::Path;
use std::time::Duration;
use crate::formatters::info_box::DaemonInfo;
use account::{ACCOUNT_PROBE_BUDGET, claude_account_segment_probe, claude_json_path};
use branch::project_segment;
use compaction::{ContextWindow, colorize_ctx_segment, compaction_segment};
use savings::{record_session_facts, savings_segment_probe};
use usage::{RateLimits, usage_segment};
#[derive(Debug, Default, serde::Deserialize)]
pub(crate) struct StatusInput {
#[serde(default)]
pub(crate) session_id: String,
#[serde(default)]
pub(crate) cwd: String,
#[serde(default)]
pub(crate) transcript_path: String,
#[serde(default)]
pub(crate) model: ModelInfo,
#[serde(default)]
pub(crate) cost: CostInfo,
#[serde(default)]
pub(crate) exceeds_200k_tokens: bool,
#[serde(default)]
pub(crate) context_window: Option<ContextWindow>,
#[serde(default)]
pub(crate) rate_limits: Option<RateLimits>,
}
#[derive(Debug, Default, serde::Deserialize)]
pub(crate) struct ModelInfo {
#[serde(default)]
pub(crate) id: String,
#[serde(default)]
pub(crate) display_name: String,
}
#[derive(Debug, Default, serde::Deserialize)]
pub(crate) struct CostInfo {
#[serde(default)]
pub(crate) total_cost_usd: f64,
}
#[cfg(test)]
pub(crate) const SAVINGS_SEGMENT_STDIN_FIELDS: &[(&str, &str)] = &[
(
"session_id",
"required -- the ledger fold key `savings_segment_probe` reads",
),
(
"context_window.total_input_tokens",
"optional -- feeds the session-actual-tokens counter via \
compaction_segment, which prices ctx% and the commit footer; it \
moves no 💸 figure since #8063",
),
];
pub(crate) fn run_statusline() -> anyhow::Result<()> {
let mut raw = String::new();
let _ = std::io::stdin().read_to_string(&mut raw);
let input: StatusInput = serde_json::from_str(&raw).unwrap_or_default();
println!("{}", render_statusline(&input));
Ok(())
}
pub(crate) fn render_statusline(input: &StatusInput) -> String {
render_statusline_from(input, claude_json_path().as_deref(), ACCOUNT_PROBE_BUDGET)
}
fn render_statusline_from(
input: &StatusInput,
account_config: Option<&Path>,
account_budget: Duration,
) -> String {
let daemon = DaemonInfo::from_lock_file();
let version = version_segment(&daemon);
let project = project_segment(&input.cwd);
let gh = gh_account_segment_probe();
let account = claude_account_segment_probe(account_config, account_budget);
let model = model_segment(&input.model);
record_session_facts(
&input.session_id,
&input.model.id,
&input.transcript_path,
&input.cwd,
);
let ctx = compaction_segment(&input.session_id, input.context_window.as_ref()).or_else(|| {
input
.exceeds_200k_tokens
.then(|| colorize_ctx_segment("ctx>200k", 1.0))
});
let cost = (input.cost.total_cost_usd > 0.0).then(|| cost_segment(input.cost.total_cost_usd));
let usage = usage_segment(input.rate_limits.as_ref());
let savings = savings_segment_probe(&input.session_id);
assemble_statusline(
version, project, gh, account, model, ctx, cost, usage, savings,
)
}
#[allow(clippy::too_many_arguments)]
fn assemble_statusline(
version: String,
project: Option<String>,
gh: Option<String>,
account: Option<String>,
model: Option<String>,
ctx: Option<String>,
cost: Option<String>,
usage: Option<String>,
savings: Option<String>,
) -> String {
[
Some(version),
project,
gh,
account,
model,
ctx,
cost,
usage,
savings,
]
.into_iter()
.flatten()
.collect::<Vec<_>>()
.join(" | ")
}
fn version_segment(daemon: &DaemonInfo) -> String {
version_segment_linked(daemon, &trusty_mpm::core::discovery::console_base_url())
}
fn version_segment_linked(daemon: &DaemonInfo, console_url: &str) -> String {
hyperlink(&version_text(daemon), console_url)
}
fn version_text(daemon: &DaemonInfo) -> String {
let ver = env!("CARGO_PKG_VERSION");
if daemon.online {
format!("TM {ver} \u{25cf}")
} else {
format!("TM {ver}")
}
}
fn hyperlink(text: &str, url: &str) -> String {
format!("\u{1b}]8;;{url}\u{7}{text}\u{1b}]8;;\u{7}")
}
fn model_segment(model: &ModelInfo) -> Option<String> {
if !model.display_name.is_empty() {
Some(model.display_name.clone())
} else if !model.id.is_empty() {
Some(model.id.clone())
} else {
None
}
}
fn cost_segment(usd: f64) -> String {
format!("${usd:.2}")
}
fn gh_account_segment_probe() -> Option<String> {
use std::sync::mpsc;
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
let _ = tx.send(trusty_mpm::core::gh_account::gh_account_status_local());
});
let status = rx.recv_timeout(Duration::from_millis(100)).ok().flatten()?;
render_gh_account_segment(status.active.as_deref(), status.logged_in.len())
}
fn render_gh_account_segment(active: Option<&str>, logged_in_count: usize) -> Option<String> {
let active = active?.trim();
if active.is_empty() {
return None;
}
Some(if logged_in_count > 1 {
format!("@{active}\u{26a0}")
} else {
format!("@{active}")
})
}
#[cfg(test)]
mod tests {
use super::*;
use compaction::ContextWindow;
fn full_input() -> StatusInput {
StatusInput {
session_id: String::new(),
cwd: "/home/user/my-project".to_string(),
transcript_path: String::new(),
model: ModelInfo {
id: "claude-sonnet-4-6".to_string(),
display_name: "Claude Sonnet 4.6".to_string(),
},
cost: CostInfo {
total_cost_usd: 1.23,
},
exceeds_200k_tokens: false,
context_window: None,
rate_limits: None,
}
}
#[test]
fn render_statusline_minimal_input() {
let out = render_statusline(&StatusInput::default());
assert!(
visible_text(&out).starts_with("TM "),
"TM <ver> segment must always lead"
);
assert!(!out.is_empty(), "output must not be empty");
}
#[test]
fn render_statusline_full_payload() {
let input = full_input();
let out = render_statusline(&input);
assert!(
out.contains("Claude Sonnet 4.6"),
"model display name must appear"
);
assert!(out.contains("$1.23"), "cost must appear");
assert!(
out.contains("my-project"),
"project name from cwd must appear"
);
}
#[test]
fn render_statusline_full_payload_matches_pipe_format() {
let input = full_input();
let out = render_statusline(&input);
let ver = env!("CARGO_PKG_VERSION");
assert!(
visible_text(&out).starts_with(&format!("TM {ver}")),
"must lead with TM <ver>: {out}"
);
assert!(
out.contains(" | "),
"segments must be pipe-separated: {out}"
);
assert!(
!out.contains('\u{2502}'),
"old │ separator must not appear: {out}"
);
assert!(
!out.contains('\u{26c1}'),
"session-count segment must be dropped: {out}"
);
}
#[test]
fn assemble_statusline_full_payload_pins_order_and_format() {
let out = assemble_statusline(
"TM 1.2.3 \u{25cf}".to_string(),
Some("bobmatnyc/trusty-tools \u{2387} main".to_string()),
Some("@bobmatnyc".to_string()),
Some("\u{273b}someone@example.com".to_string()),
Some("Claude Sonnet 4.6".to_string()),
Some("ctx 41%".to_string()),
Some("$1.23".to_string()),
Some("\u{23f3}24% \u{1f4c5}41%".to_string()),
Some("\u{1f4b8}~$0.42".to_string()),
);
assert_eq!(
out,
"TM 1.2.3 \u{25cf} | bobmatnyc/trusty-tools \u{2387} main | @bobmatnyc | \u{273b}someone@example.com | Claude Sonnet 4.6 | ctx 41% | $1.23 | \u{23f3}24% \u{1f4c5}41% | \u{1f4b8}~$0.42"
);
}
#[test]
fn assemble_statusline_pins_segment_order_with_markers() {
let out = assemble_statusline(
"VERSION".to_string(),
Some("PROJECT".to_string()),
Some("GH".to_string()),
Some("ACCOUNT".to_string()),
Some("MODEL".to_string()),
Some("CTX".to_string()),
Some("COST".to_string()),
Some("USAGE".to_string()),
Some("SAVINGS".to_string()),
);
assert_eq!(
out,
"VERSION | PROJECT | GH | ACCOUNT | MODEL | CTX | COST | USAGE | SAVINGS"
);
assert_eq!(
out.split(" | ").collect::<Vec<_>>(),
vec![
"VERSION", "PROJECT", "GH", "ACCOUNT", "MODEL", "CTX", "COST", "USAGE", "SAVINGS"
],
"segment order must exactly match [version, project, gh, account, model, ctx, cost, usage, savings]"
);
}
#[test]
fn assemble_statusline_omits_none_segments() {
let out = assemble_statusline(
"TM 1.2.3".to_string(),
None,
None,
None,
None,
None,
None,
None,
None,
);
assert_eq!(out, "TM 1.2.3");
let out = assemble_statusline(
"TM 1.2.3".to_string(),
Some("proj".to_string()),
None,
None,
Some("model".to_string()),
None,
Some("$0.50".to_string()),
None,
None,
);
assert_eq!(out, "TM 1.2.3 | proj | model | $0.50");
}
#[test]
fn version_segment_online_shows_dot_not_port() {
let daemon = DaemonInfo {
addr: "127.0.0.1:7880".to_string(),
online: true,
session_count: None,
};
let seg = version_text(&daemon);
assert_eq!(seg, format!("TM {} \u{25cf}", env!("CARGO_PKG_VERSION")));
assert!(!seg.contains("7880"), "port must not appear: {seg}");
}
#[test]
fn version_segment_offline_omits_dot() {
let daemon = DaemonInfo::default();
let seg = version_text(&daemon);
assert_eq!(seg, format!("TM {}", env!("CARGO_PKG_VERSION")));
assert!(!seg.contains('\u{25cf}'), "no dot when offline: {seg}");
}
fn visible_text(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut rest = s;
while let Some(start) = rest.find("\u{1b}]8;;") {
out.push_str(&rest[..start]);
let after = &rest[start + "\u{1b}]8;;".len()..];
match after.find('\u{7}') {
Some(end) => rest = &after[end + 1..],
None => return out,
}
}
out.push_str(rest);
out
}
#[test]
fn render_statusline_never_contains_the_daemon_port() {
for online in [true, false] {
let daemon = DaemonInfo {
addr: "http://127.0.0.1:7880".to_string(),
online,
session_count: None,
};
let seg = version_text(&daemon);
assert!(
!seg.contains("7880") && !seg.contains(':'),
"no port or host:port remnant (online={online}): {seg}"
);
let linked = version_segment_linked(&daemon, "http://127.0.0.1:9911");
assert_eq!(visible_text(&linked), seg);
}
let out = render_statusline(&full_input());
let seen = visible_text(&out);
assert!(
!seen.contains("7880"),
"no port in the rendered line: {seen}"
);
assert!(
!out.contains("7880"),
"the daemon port is not the link target either: {out}"
);
}
#[test]
fn version_segment_links_to_the_console_on_a_non_default_port() {
let daemon = DaemonInfo {
addr: "127.0.0.1:7880".to_string(),
online: true,
session_count: None,
};
let seg = version_segment_linked(&daemon, "http://127.0.0.1:9911");
assert!(
seg.contains("http://127.0.0.1:9911"),
"the configured port must be the link target: {seg:?}"
);
assert!(
!seg.contains("7788"),
"the default port must not be substituted: {seg:?}"
);
}
#[test]
fn version_segment_links_to_the_default_console_port() {
let url = format!(
"http://{}",
trusty_mpm::core::discovery::DEFAULT_CONSOLE_ADDR
);
assert_eq!(url, "http://127.0.0.1:7788");
let seg = version_segment_linked(&DaemonInfo::default(), &url);
assert!(
seg.contains("http://127.0.0.1:7788"),
"default console port must be the link target: {seg:?}"
);
}
#[test]
fn version_segment_keeps_the_version_text_verbatim() {
let daemon = DaemonInfo {
addr: String::new(),
online: true,
session_count: None,
};
let text = version_text(&daemon);
let seg = version_segment_linked(&daemon, "http://127.0.0.1:9911");
assert!(
seg.contains(&text),
"version text must survive verbatim: {seg:?}"
);
assert_eq!(visible_text(&seg), text);
assert_eq!(
visible_text(&seg),
format!("TM {} \u{25cf}", env!("CARGO_PKG_VERSION"))
);
}
#[test]
fn hyperlink_wraps_text_in_osc8() {
assert_eq!(
hyperlink("TM 1.2.3", "http://127.0.0.1:9911"),
"\u{1b}]8;;http://127.0.0.1:9911\u{7}TM 1.2.3\u{1b}]8;;\u{7}"
);
}
#[test]
fn render_statusline_leads_with_the_linked_version_segment() {
let out = render_statusline(&full_input());
assert!(
out.starts_with("\u{1b}]8;;http://"),
"the line must open with the console link: {out:?}"
);
assert!(
visible_text(&out).starts_with(&format!("TM {}", env!("CARGO_PKG_VERSION"))),
"visible text must still lead with TM <ver>: {out:?}"
);
}
#[test]
fn render_gh_account_segment_single() {
assert_eq!(
render_gh_account_segment(Some("bobmatnyc"), 1).as_deref(),
Some("@bobmatnyc")
);
assert_eq!(
render_gh_account_segment(Some("bobmatnyc"), 0).as_deref(),
Some("@bobmatnyc")
);
}
#[test]
fn render_gh_account_segment_multi() {
let seg = render_gh_account_segment(Some("bob-duetto"), 2).expect("segment");
assert_eq!(seg, "@bob-duetto\u{26a0}");
assert!(seg.contains('\u{26a0}'), "multi-account marker must appear");
}
#[test]
fn render_gh_account_segment_absent() {
assert_eq!(render_gh_account_segment(None, 0), None);
assert_eq!(render_gh_account_segment(Some(" "), 3), None);
assert_eq!(render_gh_account_segment(Some(""), 1), None);
}
#[test]
fn render_statusline_missing_model_omits_segment() {
let mut input = full_input();
input.model = ModelInfo::default();
let out = render_statusline(&input);
assert!(
!out.contains("claude-sonnet-4-6") && !out.contains("Claude Sonnet 4.6"),
"model segment must be omitted: {out}"
);
assert!(
visible_text(&out).starts_with("TM "),
"TM <ver> segment must still appear"
);
assert!(out.contains("$1.23"), "cost must still appear");
}
#[test]
fn render_statusline_exceeds_200k_shows_ctx_segment() {
let mut input = full_input();
input.exceeds_200k_tokens = true;
let out = render_statusline(&input);
assert!(out.contains("ctx>200k"), "ctx>200k segment must appear");
}
#[test]
fn render_statusline_zero_cost_omits_cost_segment() {
let mut input = full_input();
input.cost.total_cost_usd = 0.0;
let out = render_statusline(&input);
assert!(!out.contains('$'), "cost segment must be omitted when zero");
}
#[test]
fn render_statusline_invalid_json_falls_back_gracefully() {
let out = render_statusline(&StatusInput::default());
assert!(
visible_text(&out).starts_with("TM "),
"graceful fallback must still emit the TM <ver> segment"
);
assert!(
!out.contains("| |"),
"must not produce empty separator pairs"
);
}
#[test]
fn render_statusline_no_empty_separator_pairs() {
let out = render_statusline(&StatusInput::default());
assert!(!out.contains("| | "), "no empty | | pairs");
assert!(!out.contains("| |"), "no adjacent separators");
}
#[test]
fn render_statusline_with_context_window_shows_live_fill() {
use compaction::{CompactionState, render_compaction_segment};
let state = CompactionState::default();
let cw = ContextWindow {
total_input_tokens: 82_000,
context_window_size: 200_000,
used_percentage: 41.0,
};
let seg = render_compaction_segment(&state, &cw);
assert_eq!(seg.as_deref(), Some("ctx 41%"));
}
#[test]
fn render_statusline_with_context_window_no_session_id_omits_segment() {
let mut input = full_input();
input.context_window = Some(ContextWindow {
total_input_tokens: 82_000,
context_window_size: 200_000,
used_percentage: 41.0,
});
let out = render_statusline(&input);
assert!(!out.contains("ctx "), "no ctx segment without session_id");
}
#[test]
fn render_statusline_shows_claude_account_when_config_is_present() {
const WAIT_FOR_READER: Duration = Duration::MAX;
let dir = tempfile::tempdir().expect("temp dir");
let cfg = dir.path().join(".claude.json");
let absent = render_statusline_from(&full_input(), Some(&cfg), WAIT_FOR_READER);
assert!(
!absent.contains('\u{273b}'),
"account segment must be omitted with no config: {absent}"
);
let none = render_statusline_from(&full_input(), None, WAIT_FOR_READER);
assert!(
!none.contains('\u{273b}'),
"account segment must be omitted with no config path: {none}"
);
std::fs::write(
&cfg,
r#"{"oauthAccount": {"emailAddress": "someone@example.com"}}"#,
)
.expect("write config");
let present = render_statusline_from(&full_input(), Some(&cfg), WAIT_FOR_READER);
assert!(
present.contains("\u{273b}someone@example.com"),
"account segment must appear once the config names one: {present}"
);
}
#[test]
fn render_statusline_with_rate_limits_shows_usage_segment() {
use usage::{RateLimits, RateWindow};
let mut input = full_input();
input.rate_limits = Some(RateLimits {
five_hour: Some(RateWindow {
used_percentage: Some(24.0),
}),
seven_day: Some(RateWindow {
used_percentage: Some(41.0),
}),
});
let out = render_statusline(&input);
assert!(out.contains("24%"), "5h usage % must appear: {out}");
assert!(out.contains("41%"), "7d usage % must appear: {out}");
let cost_at = out.find("$1.23").expect("cost segment: {out}");
let usage_at = out.find("24%").expect("usage segment: {out}");
assert!(
cost_at < usage_at,
"usage segment must be appended after cost: {out}"
);
}
#[test]
fn render_statusline_without_rate_limits_omits_usage_segment() {
let input = full_input();
let out = render_statusline(&input);
assert!(
!out.contains('\u{23f3}') && !out.contains('\u{1f4c5}'),
"usage segment must be omitted when rate_limits is absent: {out}"
);
}
}