use chrono::{DateTime, Duration, Utc};
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::Widget;
use zero_engine_client::{BudgetSnapshot, EngineState, HlRate, Risk};
use zero_operator_state::Snapshot as OperatorSnapshot;
use crate::app::mode::Mode;
use crate::theme::Theme;
const OPERATOR_STATE_STALE_AFTER: Duration = Duration::seconds(30);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Tier {
Full,
Compact,
Minimal,
}
#[derive(Debug)]
pub struct StatusBar<'a> {
pub mode: Mode,
pub engine: &'a EngineState,
pub theme: Theme,
pub now: DateTime<Utc>,
pub rate_budget: Option<BudgetSnapshot>,
}
impl Widget for StatusBar<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
for y in area.top()..area.bottom() {
for x in area.left()..area.right() {
buf[(x, y)].set_char(' ');
}
}
let line = self.build_line_for_width(area.width);
line.render(area, buf);
}
}
impl StatusBar<'_> {
#[must_use]
pub fn pick_tier(&self, available_width: u16) -> Tier {
for tier in [Tier::Full, Tier::Compact] {
let line = self.build_line(tier);
if line.width() <= usize::from(available_width) {
return tier;
}
}
Tier::Minimal
}
fn build_line_for_width(&self, available_width: u16) -> Line<'static> {
let tier = self.pick_tier(available_width);
self.build_line(tier)
}
fn build_line(&self, tier: Tier) -> Line<'static> {
let mode_span = self.mode_span();
let (ops_prefix, ops_label, ops_marker) = self.ops_spans();
let dd_span = self.drawdown_span();
let sep_wide = Span::styled(" ", Style::default().fg(self.theme.metadata));
let sep_narrow = Span::styled(" ", Style::default().fg(self.theme.metadata));
match tier {
Tier::Full => {
let sep = || sep_wide.clone();
let mut spans: Vec<Span<'static>> = vec![mode_span, self.engine_span()];
if let Some(retry) = self.retry_span() {
spans.push(retry);
}
spans.extend([sep(), self.feed_span(), sep(), self.rate_span(), sep()]);
spans.push(self.hl_span());
spans.extend([sep(), dd_span, sep(), ops_prefix, ops_label, ops_marker]);
Line::from(spans)
}
Tier::Compact => {
let sep = || sep_narrow.clone();
Line::from(vec![
mode_span,
self.engine_span(),
sep(),
self.feed_span(),
sep(),
dd_span,
sep(),
ops_prefix,
ops_label,
ops_marker,
])
}
Tier::Minimal => Line::from(vec![
mode_span, ops_prefix, ops_label, ops_marker, sep_narrow, dd_span,
]),
}
}
fn mode_span(&self) -> Span<'static> {
Span::styled(
format!(" [{}] ", self.mode.short()),
Style::default()
.fg(self.theme.primary)
.add_modifier(Modifier::BOLD),
)
}
fn engine_span(&self) -> Span<'static> {
let (label, color) = if self.engine.connection.ws_connected {
("OK", self.theme.primary)
} else if self.engine.connection.total_attempts > 0 {
("RECONNECTING", self.theme.caution)
} else {
("DOWN", self.theme.alert)
};
Span::styled(format!("engine:{label}"), Style::default().fg(color))
}
fn retry_span(&self) -> Option<Span<'static>> {
if !self.engine.connection.ws_connected && self.engine.connection.reconnect_count > 0 {
Some(Span::styled(
format!(" retry:{}", self.engine.connection.reconnect_count),
Style::default().fg(self.theme.caution),
))
} else {
None
}
}
fn feed_span(&self) -> Span<'static> {
match self.engine.feed_age_seconds(self.now) {
None => Span::styled("feed:--", Style::default().fg(self.theme.metadata)),
Some(age) => {
let color = if age < 0 {
self.theme.metadata
} else if age <= 3 {
self.theme.primary
} else if age <= 10 {
self.theme.caution
} else {
self.theme.alert
};
Span::styled(format!("feed:{age}s"), Style::default().fg(color))
}
}
}
fn drawdown_span(&self) -> Span<'static> {
match self.engine.risk.as_ref() {
None => Span::styled("dd:--", Style::default().fg(self.theme.metadata)),
Some(stat) => render_drawdown(&stat.value, &self.theme),
}
}
fn rate_span(&self) -> Span<'static> {
let prefix = "rate:";
let Some(snap) = self.rate_budget else {
return Span::styled(
format!("{prefix}?"),
Style::default().fg(self.theme.metadata),
);
};
if snap.capacity == 0 {
return Span::styled(
format!("{prefix}?"),
Style::default().fg(self.theme.metadata),
);
}
if snap.tokens == 0 {
return Span::styled(
format!("{prefix}EXH"),
Style::default()
.fg(self.theme.alert)
.add_modifier(Modifier::BOLD),
);
}
Span::styled(
format!("{prefix}{}/{}", snap.tokens, snap.capacity),
Style::default().fg(self.pressure_color(snap.headroom())),
)
}
fn hl_span(&self) -> Span<'static> {
let prefix = "hl:";
let Some(HlRate { used, cap }) = self.engine.hl_rate_snapshot() else {
return Span::styled(
format!("{prefix}?"),
Style::default().fg(self.theme.metadata),
);
};
if cap == 0 {
return Span::styled(
format!("{prefix}?"),
Style::default().fg(self.theme.metadata),
);
}
if used >= cap {
return Span::styled(
format!("{prefix}EXH"),
Style::default()
.fg(self.theme.alert)
.add_modifier(Modifier::BOLD),
);
}
let headroom = f64::from(cap.saturating_sub(used)) / f64::from(cap);
Span::styled(
format!("{prefix}{used}/{cap}"),
Style::default().fg(self.pressure_color(headroom)),
)
}
fn pressure_color(&self, headroom: f64) -> ratatui::style::Color {
if headroom >= 0.25 {
self.theme.primary
} else if headroom >= 0.10 {
self.theme.caution
} else {
self.theme.alert
}
}
fn ops_spans(&self) -> (Span<'static>, Span<'static>, Span<'static>) {
let metadata = self.theme.metadata;
let prefix = Span::styled("ops:", Style::default().fg(metadata));
match &self.engine.operator_state {
None => (
prefix,
Span::styled("?", Style::default().fg(metadata)),
Span::raw(""),
),
Some(stat) => {
let snap: &OperatorSnapshot = &stat.value;
let color = self.theme.resolve_hint(snap.label.color_hint());
let stale = stat.is_stale(self.now, OPERATOR_STATE_STALE_AFTER);
let label_color = if stale { metadata } else { color };
let label_span = Span::styled(
snap.label.short().to_string(),
Style::default()
.fg(label_color)
.add_modifier(Modifier::BOLD),
);
let marker_span = if stale {
Span::styled("*", Style::default().fg(metadata))
} else {
Span::raw("")
};
(prefix, label_span, marker_span)
}
}
}
}
fn render_drawdown(risk: &Risk, theme: &Theme) -> Span<'static> {
if risk.is_halted() {
return Span::styled(
"dd:HALT",
Style::default()
.fg(theme.alert)
.add_modifier(Modifier::BOLD),
);
}
match risk.drawdown_pct {
None => Span::styled("dd:--", Style::default().fg(theme.metadata)),
Some(pct) => {
let magnitude = pct.max(0.0);
let color = if magnitude <= 2.0 {
theme.primary
} else if magnitude <= 5.0 {
theme.caution
} else {
theme.alert
};
Span::styled(format!("dd:{pct:.1}%"), Style::default().fg(color))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone;
use ratatui::Terminal;
use ratatui::backend::TestBackend;
use zero_engine_client::{Source, Stat, V2Status};
use zero_operator_state::{Label, StateVector};
fn snapshot_at(label: Label, as_of: DateTime<Utc>) -> Stat<OperatorSnapshot> {
let snap = OperatorSnapshot::new(label, StateVector::default(), as_of, 1);
Stat::new(snap, Source::Http).with_as_of(as_of)
}
fn risk_stat(risk: Risk, as_of: DateTime<Utc>) -> Stat<Risk> {
Stat::new(risk, Source::Ws).with_as_of(as_of)
}
fn render_bar_at(engine: &EngineState, now: DateTime<Utc>, width: u16) -> Vec<String> {
let backend = TestBackend::new(width, 1);
let mut term = Terminal::new(backend).expect("terminal");
term.draw(|f| {
let bar = StatusBar {
mode: Mode::Conversation,
engine,
theme: Theme::default(),
now,
rate_budget: None,
};
f.render_widget(bar, f.area());
})
.expect("draw");
let buf = term.backend().buffer().clone();
(0..buf.area.height)
.map(|y| {
(0..buf.area.width)
.map(|x| buf[(x, y)].symbol().to_string())
.collect::<String>()
})
.collect()
}
fn render_bar(engine: &EngineState, now: DateTime<Utc>) -> Vec<String> {
render_bar_at(engine, now, 80)
}
#[test]
fn unseen_snapshot_renders_question_mark() {
let now = Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap();
let engine = EngineState::new();
let lines = render_bar(&engine, now);
assert!(
lines[0].contains("ops:?"),
"expected ops:? placeholder, got {lines:?}"
);
}
#[test]
fn fresh_snapshot_renders_label() {
let now = Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap();
let mut engine = EngineState::new();
engine.operator_state = Some(snapshot_at(Label::Steady, now));
let lines = render_bar(&engine, now);
assert!(
lines[0].contains("ops:STEADY"),
"expected ops:STEADY, got {lines:?}"
);
assert!(
!lines[0].contains("STEADY*"),
"fresh snapshot should not carry staleness marker: {lines:?}"
);
}
#[test]
fn stale_snapshot_gets_asterisk() {
let as_of = Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap();
let now = as_of + Duration::seconds(60);
let mut engine = EngineState::new();
engine.operator_state = Some(snapshot_at(Label::Tilt, as_of));
let lines = render_bar(&engine, now);
assert!(
lines[0].contains("ops:TILT*"),
"stale TILT should render with asterisk: {lines:?}"
);
}
#[test]
fn every_label_has_a_rendered_form() {
let now = Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap();
for (label, expected) in [
(Label::Fresh, "ops:FRESH"),
(Label::Steady, "ops:STEADY"),
(Label::Elevated, "ops:ELEVATED"),
(Label::Tilt, "ops:TILT"),
(Label::Fatigued, "ops:FATIGUED"),
(Label::Recovery, "ops:RECOVERY"),
] {
let mut engine = EngineState::new();
engine.operator_state = Some(snapshot_at(label, now));
let lines = render_bar(&engine, now);
assert!(
lines[0].contains(expected),
"label {label:?} should render as {expected}, got {lines:?}"
);
}
}
#[test]
fn drawdown_missing_shows_placeholder() {
let now = Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap();
let engine = EngineState::new();
let lines = render_bar(&engine, now);
assert!(lines[0].contains("dd:--"), "got {lines:?}");
}
#[test]
fn drawdown_renders_one_decimal_percent() {
let now = Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap();
let mut engine = EngineState::new();
let risk = Risk {
drawdown_pct: Some(1.23),
..Default::default()
};
engine.risk = Some(risk_stat(risk, now));
let lines = render_bar(&engine, now);
assert!(lines[0].contains("dd:1.2%"), "got {lines:?}");
}
#[test]
fn drawdown_halted_reads_halt() {
let now = Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap();
let mut engine = EngineState::new();
let risk = Risk {
drawdown_pct: Some(0.5),
halted: true,
..Default::default()
};
engine.risk = Some(risk_stat(risk, now));
let lines = render_bar(&engine, now);
assert!(
lines[0].contains("dd:HALT"),
"halt must override the number: {lines:?}"
);
assert!(
!lines[0].contains("dd:0.5%"),
"number must not leak when halted: {lines:?}"
);
}
#[test]
fn drawdown_circuit_breaker_reads_halt() {
let now = Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap();
let mut engine = EngineState::new();
let risk = Risk {
drawdown_pct: Some(3.0),
global_halt: true,
..Default::default()
};
engine.risk = Some(risk_stat(risk, now));
let lines = render_bar(&engine, now);
assert!(lines[0].contains("dd:HALT"), "got {lines:?}");
}
#[test]
fn minimal_tier_drops_engine_and_feed() {
let now = Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap();
let mut engine = EngineState::new();
engine.operator_state = Some(snapshot_at(Label::Steady, now));
engine.risk = Some(risk_stat(
Risk {
drawdown_pct: Some(1.0),
..Default::default()
},
now,
));
let lines = render_bar_at(&engine, now, 40);
assert!(lines[0].contains("ops:STEADY"), "got {lines:?}");
assert!(lines[0].contains("dd:1.0%"), "got {lines:?}");
assert!(
!lines[0].contains("engine:"),
"minimal drops engine: {lines:?}"
);
assert!(!lines[0].contains("feed:"), "minimal drops feed: {lines:?}");
}
#[test]
fn full_tier_includes_all_segments_at_120_cols() {
let now = Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap();
let mut engine = EngineState::new();
engine.operator_state = Some(snapshot_at(Label::Elevated, now));
engine.risk = Some(risk_stat(
Risk {
drawdown_pct: Some(2.5),
..Default::default()
},
now,
));
engine.apply_status(V2Status::default(), now, Source::Ws);
engine.on_ws_connected();
let lines = render_bar_at(&engine, now, 120);
for needle in [" [CONV]", "engine:OK", "feed:0s", "dd:2.5%", "ops:ELEVATED"] {
assert!(
lines[0].contains(needle),
"full tier missing {needle}: {lines:?}"
);
}
}
#[test]
fn pick_tier_prefers_widest_fit() {
let now = Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap();
let mut engine = EngineState::new();
engine.operator_state = Some(snapshot_at(Label::Elevated, now));
engine.risk = Some(risk_stat(
Risk {
drawdown_pct: Some(3.3),
..Default::default()
},
now,
));
engine.on_ws_connected();
let make_bar = |w: u16| {
let bar = StatusBar {
mode: Mode::Conversation,
engine: &engine,
theme: Theme::default(),
now,
rate_budget: None,
};
bar.pick_tier(w)
};
assert_eq!(make_bar(200), Tier::Full);
assert_eq!(make_bar(80), Tier::Full);
assert_eq!(make_bar(30), Tier::Minimal);
}
fn bar_with_budget(snap: BudgetSnapshot) -> StatusBar<'static> {
let engine: &'static EngineState = Box::leak(Box::new(EngineState::new()));
StatusBar {
mode: Mode::Conversation,
engine,
theme: Theme::default(),
now: Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap(),
rate_budget: Some(snap),
}
}
#[test]
fn rate_segment_is_question_mark_without_bucket() {
let engine = EngineState::new();
let bar = StatusBar {
mode: Mode::Conversation,
engine: &engine,
theme: Theme::default(),
now: Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap(),
rate_budget: None,
};
let span = bar.rate_span();
assert_eq!(span.content, "rate:?");
}
#[test]
fn rate_segment_renders_n_over_m() {
let bar = bar_with_budget(BudgetSnapshot {
capacity: 60,
refill_per_second: 1.0,
tokens: 42,
});
assert_eq!(bar.rate_span().content, "rate:42/60");
}
#[test]
fn rate_segment_renders_exh_at_zero_tokens() {
let bar = bar_with_budget(BudgetSnapshot {
capacity: 60,
refill_per_second: 1.0,
tokens: 0,
});
let span = bar.rate_span();
assert_eq!(span.content, "rate:EXH");
assert!(
span.style.add_modifier.contains(Modifier::BOLD),
"rate:EXH must render bold for at-a-glance operator visibility",
);
}
#[test]
fn rate_segment_zero_capacity_renders_question_mark() {
let bar = bar_with_budget(BudgetSnapshot {
capacity: 0,
refill_per_second: 0.0,
tokens: 0,
});
assert_eq!(bar.rate_span().content, "rate:?");
}
#[test]
fn rate_segment_color_bands_cover_all_headroom_regions() {
let theme = Theme::default();
let mk = |tokens: u32| {
bar_with_budget(BudgetSnapshot {
capacity: 60,
refill_per_second: 1.0,
tokens,
})
.rate_span()
.style
.fg
.unwrap()
};
assert_eq!(mk(60), theme.primary);
assert_eq!(mk(15), theme.primary);
assert_eq!(mk(14), theme.caution);
assert_eq!(mk(7), theme.caution);
assert_eq!(mk(5), theme.alert);
assert_eq!(mk(1), theme.alert);
}
#[test]
fn hl_segment_is_question_mark_when_engine_silent() {
let engine = EngineState::new();
let bar = StatusBar {
mode: Mode::Conversation,
engine: &engine,
theme: Theme::default(),
now: Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap(),
rate_budget: None,
};
assert_eq!(bar.hl_span().content, "hl:?");
}
#[test]
fn hl_segment_renders_used_over_cap_from_v2status() {
let now = Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap();
let mut engine = EngineState::new();
engine.apply_status(
V2Status {
hl_rate: Some(zero_engine_client::HlRate {
used: 120,
cap: 240,
}),
..V2Status::default()
},
now,
Source::Ws,
);
let bar = StatusBar {
mode: Mode::Conversation,
engine: &engine,
theme: Theme::default(),
now,
rate_budget: None,
};
assert_eq!(bar.hl_span().content, "hl:120/240");
}
#[test]
fn hl_segment_overshoot_renders_exh() {
let now = Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap();
let mut engine = EngineState::new();
engine.apply_status(
V2Status {
hl_rate: Some(zero_engine_client::HlRate {
used: 245,
cap: 240,
}),
..V2Status::default()
},
now,
Source::Ws,
);
let bar = StatusBar {
mode: Mode::Conversation,
engine: &engine,
theme: Theme::default(),
now,
rate_budget: None,
};
let span = bar.hl_span();
assert_eq!(span.content, "hl:EXH");
assert!(span.style.add_modifier.contains(Modifier::BOLD));
}
#[test]
fn narrow_render_never_wraps_or_panics() {
let now = Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap();
let mut engine = EngineState::new();
engine.operator_state = Some(snapshot_at(Label::Tilt, now));
let lines = render_bar_at(&engine, now, 20);
assert_eq!(lines.len(), 1, "status bar is single-row: {lines:?}");
assert_eq!(lines[0].chars().count(), 20);
}
}