mod branch;
pub(crate) mod compaction;
mod usage;
use std::io::Read as _;
use crate::formatters::info_box::DaemonInfo;
use branch::project_segment;
use compaction::{ContextWindow, colorize_ctx_segment, compaction_segment};
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) 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,
}
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 {
let daemon = DaemonInfo::from_lock_file();
let version_port = version_port_segment(&daemon);
let project = project_segment(&input.cwd);
let gh = gh_account_segment_probe();
let model = model_segment(&input.model);
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());
assemble_statusline(version_port, project, gh, model, ctx, cost, usage)
}
#[allow(clippy::too_many_arguments)]
fn assemble_statusline(
version_port: String,
project: Option<String>,
gh: Option<String>,
model: Option<String>,
ctx: Option<String>,
cost: Option<String>,
usage: Option<String>,
) -> String {
[Some(version_port), project, gh, model, ctx, cost, usage]
.into_iter()
.flatten()
.collect::<Vec<_>>()
.join(" | ")
}
fn version_port_segment(daemon: &DaemonInfo) -> String {
let ver = env!("CARGO_PKG_VERSION");
match daemon_port(daemon) {
Some(port) if daemon.online => format!("TM {ver} {port}"),
_ => format!("TM {ver}"),
}
}
fn daemon_port(daemon: &DaemonInfo) -> Option<&str> {
daemon.addr.rfind(':').map(|i| &daemon.addr[i + 1..])
}
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;
use std::time::Duration;
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(),
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!(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!(
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 7880".to_string(),
Some("bobmatnyc/trusty-tools \u{2387} main".to_string()),
Some("@bobmatnyc".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()),
);
assert_eq!(
out,
"TM 1.2.3 7880 | bobmatnyc/trusty-tools \u{2387} main | @bobmatnyc | Claude Sonnet 4.6 | ctx 41% | $1.23 | \u{23f3}24% \u{1f4c5}41%"
);
}
#[test]
fn assemble_statusline_pins_segment_order_with_markers() {
let out = assemble_statusline(
"VERSION_PORT".to_string(),
Some("PROJECT".to_string()),
Some("GH".to_string()),
Some("MODEL".to_string()),
Some("CTX".to_string()),
Some("COST".to_string()),
Some("USAGE".to_string()),
);
assert_eq!(
out,
"VERSION_PORT | PROJECT | GH | MODEL | CTX | COST | USAGE"
);
assert_eq!(
out.split(" | ").collect::<Vec<_>>(),
vec![
"VERSION_PORT",
"PROJECT",
"GH",
"MODEL",
"CTX",
"COST",
"USAGE"
],
"segment order must exactly match [version_port, project, gh, model, ctx, cost, usage]"
);
}
#[test]
fn assemble_statusline_omits_none_segments() {
let out = assemble_statusline("TM 1.2.3".to_string(), 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,
Some("model".to_string()),
None,
Some("$0.50".to_string()),
None,
);
assert_eq!(out, "TM 1.2.3 | proj | model | $0.50");
}
#[test]
fn version_port_segment_online_includes_port() {
let daemon = DaemonInfo {
addr: "127.0.0.1:7880".to_string(),
online: true,
session_count: None,
};
let seg = version_port_segment(&daemon);
assert_eq!(seg, format!("TM {} 7880", env!("CARGO_PKG_VERSION")));
}
#[test]
fn version_port_segment_offline_omits_port() {
let daemon = DaemonInfo::default();
let seg = version_port_segment(&daemon);
assert_eq!(seg, format!("TM {}", env!("CARGO_PKG_VERSION")));
}
#[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"), "model segment must be omitted");
assert!(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!(
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_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}");
assert!(
out.ends_with("41%") || out.contains("41%\u{1b}[0m"),
"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}"
);
}
}