linesmith_core/segments/
session_duration.rs1use super::{RenderContext, RenderResult, RenderedSegment, Segment, SegmentDefaults};
7use crate::data_context::DataContext;
8use crate::theme::Role;
9
10pub struct SessionDurationSegment;
11
12const PRIORITY: u8 = 192;
16
17impl Segment for SessionDurationSegment {
18 fn render(&self, ctx: &DataContext, _rc: &RenderContext) -> RenderResult {
19 let Some(cost) = ctx.status.cost.as_ref() else {
20 crate::lsm_debug!("session_duration: status.cost absent; hiding");
21 return Ok(None);
22 };
23 let Some(ms) = cost.total_duration_ms else {
24 crate::lsm_debug!("session_duration: total_duration_ms null; hiding");
25 return Ok(None);
26 };
27 Ok(Some(
28 RenderedSegment::new(format_duration(ms)).with_role(Role::Muted),
29 ))
30 }
31
32 fn defaults(&self) -> SegmentDefaults {
33 SegmentDefaults::with_priority(PRIORITY)
34 }
35}
36
37fn format_duration(ms: u64) -> String {
38 let total_secs = ms / 1000;
39 let h = total_secs / 3600;
40 let m = (total_secs % 3600) / 60;
41 let s = total_secs % 60;
42 if h > 0 {
43 if m > 0 {
44 format!("{h}h {m}m")
45 } else {
46 format!("{h}h")
47 }
48 } else if m > 0 {
49 if s > 0 {
50 format!("{m}m {s}s")
51 } else {
52 format!("{m}m")
53 }
54 } else {
55 format!("{s}s")
56 }
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62 use crate::input::{CostMetrics, ModelInfo, StatusContext, Tool, WorkspaceInfo};
63 use std::path::PathBuf;
64 use std::sync::Arc;
65
66 fn rc() -> RenderContext {
67 RenderContext::new(80)
68 }
69
70 fn ctx(cost: Option<CostMetrics>) -> DataContext {
71 DataContext::new(StatusContext {
72 tool: Tool::ClaudeCode,
73 model: Some(ModelInfo {
74 display_name: "X".into(),
75 }),
76 workspace: Some(WorkspaceInfo {
77 project_dir: PathBuf::from("/repo"),
78 git_worktree: None,
79 }),
80 context_window: None,
81 cost,
82 effort: None,
83 vim: None,
84 output_style: None,
85 agent_name: None,
86 version: None,
87 raw: Arc::new(serde_json::Value::Null),
88 })
89 }
90
91 fn cost_of(duration_ms: u64) -> CostMetrics {
92 CostMetrics {
93 total_cost_usd: Some(0.0),
94 total_duration_ms: Some(duration_ms),
95 total_api_duration_ms: Some(0),
96 total_lines_added: Some(0),
97 total_lines_removed: Some(0),
98 }
99 }
100
101 fn render_for(duration_ms: u64) -> Option<RenderedSegment> {
102 SessionDurationSegment
103 .render(&ctx(Some(cost_of(duration_ms))), &rc())
104 .unwrap()
105 }
106
107 #[test]
108 fn renders_zero_seconds() {
109 assert_eq!(
110 render_for(0),
111 Some(RenderedSegment::new("0s").with_role(Role::Muted))
112 );
113 }
114
115 #[test]
116 fn renders_sub_minute_as_seconds_only() {
117 assert_eq!(render_for(5_000).unwrap().text(), "5s");
118 assert_eq!(render_for(59_999).unwrap().text(), "59s");
119 }
120
121 #[test]
122 fn renders_exact_minute_without_trailing_zero_seconds() {
123 assert_eq!(render_for(60_000).unwrap().text(), "1m");
124 }
125
126 #[test]
127 fn renders_minutes_and_seconds() {
128 assert_eq!(render_for(65_000).unwrap().text(), "1m 5s");
129 assert_eq!(render_for(125_500).unwrap().text(), "2m 5s");
130 }
131
132 #[test]
133 fn renders_just_below_an_hour() {
134 assert_eq!(render_for(3_599_999).unwrap().text(), "59m 59s");
137 }
138
139 #[test]
140 fn renders_exact_hour_without_trailing_zero_minutes() {
141 assert_eq!(render_for(3_600_000).unwrap().text(), "1h");
142 }
143
144 #[test]
145 fn renders_hours_and_minutes_drops_seconds() {
146 assert_eq!(render_for(3_725_000).unwrap().text(), "1h 2m");
149 }
150
151 #[test]
152 fn renders_long_sessions_at_hour_resolution() {
153 assert_eq!(render_for(86_400_000).unwrap().text(), "24h");
154 assert_eq!(render_for(91_800_000).unwrap().text(), "25h 30m");
155 }
156
157 #[test]
158 fn renders_hour_with_seconds_only_drops_them() {
159 assert_eq!(render_for(3_630_000).unwrap().text(), "1h");
164 }
165
166 #[test]
167 fn renders_with_muted_role() {
168 assert_eq!(render_for(5_000).unwrap().style().role, Some(Role::Muted));
169 }
170
171 #[test]
172 fn hidden_when_cost_absent() {
173 assert_eq!(
174 SessionDurationSegment.render(&ctx(None), &rc()).unwrap(),
175 None
176 );
177 }
178
179 #[test]
180 fn defaults_use_expected_priority() {
181 assert_eq!(SessionDurationSegment.defaults().priority, PRIORITY);
182 }
183
184 #[test]
185 fn sub_second_durations_floor_to_zero_seconds() {
186 assert_eq!(render_for(999).unwrap().text(), "0s");
190 }
191}