Skip to content

Commit e4a1bb2

Browse files
stephenleoclaude
andcommitted
refactor: apply PR #108 sub-field boilerplate refactor and remove unused HasThresholdStyle impls
- Merged CostSubfieldConfig and ContextWindowSubfieldConfig into a unified SubfieldConfig struct with #[cfg(test)] type aliases for backward compat. - Added HasThresholdStyle trait and impl_has_threshold_style! macro to config.rs; only ContextWindowConfig gets an impl (the three unused CostConfig, ContextBarConfig, UsageLimitsConfig impls are intentionally absent per Story 3.1). - Added render_styled_value() to format.rs with six unit tests covering all paths (format, threshold, invert_threshold, parent fallback, no-sub/no-parent). - Replaced all inline threshold/style boilerplate in cost.rs (5 functions) and context_window.rs (8 functions) with render_styled_value() calls. - cargo build, cargo test (65/65), and cargo clippy -- -D warnings all exit 0. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent d573141 commit e4a1bb2

4 files changed

Lines changed: 296 additions & 575 deletions

File tree

src/config.rs

Lines changed: 91 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -48,34 +48,18 @@ pub struct CostConfig {
4848
pub critical_style: Option<String>,
4949
pub format: Option<String>,
5050
// Sub-field per-display configs (map to [cship.cost.total_cost_usd] etc.)
51-
pub total_cost_usd: Option<CostSubfieldConfig>,
52-
pub total_duration_ms: Option<CostSubfieldConfig>,
53-
pub total_api_duration_ms: Option<CostSubfieldConfig>,
54-
pub total_lines_added: Option<CostSubfieldConfig>,
55-
pub total_lines_removed: Option<CostSubfieldConfig>,
51+
pub total_cost_usd: Option<SubfieldConfig>,
52+
pub total_duration_ms: Option<SubfieldConfig>,
53+
pub total_api_duration_ms: Option<SubfieldConfig>,
54+
pub total_lines_added: Option<SubfieldConfig>,
55+
pub total_lines_removed: Option<SubfieldConfig>,
5656
}
5757

58-
/// Configuration for individual `[cship.cost.*]` sub-field modules.
58+
/// Unified configuration for individual sub-field modules
59+
/// (e.g. `[cship.cost.total_cost_usd]`, `[cship.context_window.used_percentage]`).
5960
#[derive(Debug, Deserialize, Default)]
60-
pub struct CostSubfieldConfig {
61+
pub struct SubfieldConfig {
6162
pub style: Option<String>,
62-
/// Reserved — not yet rendered; included for config schema consistency.
63-
pub symbol: Option<String>,
64-
pub disabled: Option<bool>,
65-
/// Reserved — not yet rendered; included for config schema consistency.
66-
pub label: Option<String>,
67-
pub warn_threshold: Option<f64>,
68-
pub warn_style: Option<String>,
69-
pub critical_threshold: Option<f64>,
70-
pub critical_style: Option<String>,
71-
pub format: Option<String>,
72-
}
73-
74-
/// Configuration for individual `[cship.context_window.*]` sub-field modules.
75-
#[derive(Debug, Deserialize, Default)]
76-
pub struct ContextWindowSubfieldConfig {
77-
pub style: Option<String>,
78-
/// Used only in the format path (via `$symbol`); ignored in the default render path.
7963
pub symbol: Option<String>,
8064
pub disabled: Option<bool>,
8165
pub warn_threshold: Option<f64>,
@@ -97,6 +81,55 @@ pub struct ContextWindowSubfieldConfig {
9781
pub invert_threshold: Option<bool>,
9882
}
9983

84+
/// Backwards-compatible type aliases (used by test code).
85+
#[cfg(test)]
86+
pub type CostSubfieldConfig = SubfieldConfig;
87+
#[cfg(test)]
88+
pub type ContextWindowSubfieldConfig = SubfieldConfig;
89+
90+
/// Trait for uniform access to style/threshold fields shared by config types.
91+
/// Used by `render_styled_value()` to resolve sub-field → parent fallback.
92+
///
93+
/// `format_str()` and `symbol_str()` default to `None`. Only parent configs
94+
/// whose sub-fields should inherit format/symbol (i.e., `ContextWindowConfig`)
95+
/// override these.
96+
pub trait HasThresholdStyle {
97+
fn style(&self) -> Option<&str>;
98+
fn warn_threshold(&self) -> Option<f64>;
99+
fn warn_style(&self) -> Option<&str>;
100+
fn critical_threshold(&self) -> Option<f64>;
101+
fn critical_style(&self) -> Option<&str>;
102+
fn format_str(&self) -> Option<&str> {
103+
None
104+
}
105+
fn symbol_str(&self) -> Option<&str> {
106+
None
107+
}
108+
}
109+
110+
#[allow(unused_macros)]
111+
macro_rules! impl_has_threshold_style {
112+
($t:ty) => {
113+
impl HasThresholdStyle for $t {
114+
fn style(&self) -> Option<&str> {
115+
self.style.as_deref()
116+
}
117+
fn warn_threshold(&self) -> Option<f64> {
118+
self.warn_threshold
119+
}
120+
fn warn_style(&self) -> Option<&str> {
121+
self.warn_style.as_deref()
122+
}
123+
fn critical_threshold(&self) -> Option<f64> {
124+
self.critical_threshold
125+
}
126+
fn critical_style(&self) -> Option<&str> {
127+
self.critical_style.as_deref()
128+
}
129+
}
130+
};
131+
}
132+
100133
/// Configuration for `[cship.context_bar]` — visual progress bar with thresholds.
101134
/// Implemented in Story 2.2. Defined here so all Epic 2 config is available.
102135
#[derive(Debug, Deserialize, Default)]
@@ -127,16 +160,40 @@ pub struct ContextWindowConfig {
127160
pub critical_style: Option<String>,
128161
pub format: Option<String>,
129162
// Per-sub-field configs (map to [cship.context_window.used_percentage] etc.)
130-
pub used_percentage: Option<ContextWindowSubfieldConfig>,
131-
pub remaining_percentage: Option<ContextWindowSubfieldConfig>,
132-
pub size: Option<ContextWindowSubfieldConfig>,
133-
pub total_input_tokens: Option<ContextWindowSubfieldConfig>,
134-
pub total_output_tokens: Option<ContextWindowSubfieldConfig>,
135-
pub current_usage_input_tokens: Option<ContextWindowSubfieldConfig>,
136-
pub current_usage_output_tokens: Option<ContextWindowSubfieldConfig>,
137-
pub current_usage_cache_creation_input_tokens: Option<ContextWindowSubfieldConfig>,
138-
pub current_usage_cache_read_input_tokens: Option<ContextWindowSubfieldConfig>,
139-
pub used_tokens: Option<ContextWindowSubfieldConfig>,
163+
pub used_percentage: Option<SubfieldConfig>,
164+
pub remaining_percentage: Option<SubfieldConfig>,
165+
pub size: Option<SubfieldConfig>,
166+
pub total_input_tokens: Option<SubfieldConfig>,
167+
pub total_output_tokens: Option<SubfieldConfig>,
168+
pub current_usage_input_tokens: Option<SubfieldConfig>,
169+
pub current_usage_output_tokens: Option<SubfieldConfig>,
170+
pub current_usage_cache_creation_input_tokens: Option<SubfieldConfig>,
171+
pub current_usage_cache_read_input_tokens: Option<SubfieldConfig>,
172+
pub used_tokens: Option<SubfieldConfig>,
173+
}
174+
175+
impl HasThresholdStyle for ContextWindowConfig {
176+
fn style(&self) -> Option<&str> {
177+
self.style.as_deref()
178+
}
179+
fn warn_threshold(&self) -> Option<f64> {
180+
self.warn_threshold
181+
}
182+
fn warn_style(&self) -> Option<&str> {
183+
self.warn_style.as_deref()
184+
}
185+
fn critical_threshold(&self) -> Option<f64> {
186+
self.critical_threshold
187+
}
188+
fn critical_style(&self) -> Option<&str> {
189+
self.critical_style.as_deref()
190+
}
191+
fn format_str(&self) -> Option<&str> {
192+
self.format.as_deref()
193+
}
194+
fn symbol_str(&self) -> Option<&str> {
195+
self.symbol.as_deref()
196+
}
140197
}
141198

142199
/// Configuration for `[cship.vim]` — vim mode display.

src/format.rs

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,76 @@
1010
//!
1111
//! Source: architecture.md#Core Architectural Decisions, epics.md#Story 2.5
1212
13+
/// Centralized style/threshold/format rendering for sub-field render functions.
14+
///
15+
/// Resolves style, thresholds, and format strings using sub-field → parent fallback,
16+
/// handling `invert_threshold` for decreasing-health indicators (e.g. remaining_percentage).
17+
///
18+
/// # Arguments
19+
/// - `val_str`: Already-formatted display string (e.g. `"85"`, `"0.0123"`)
20+
/// - `threshold_val`: Numeric value for threshold comparison; `None` for non-threshold fields
21+
/// - `sub_cfg`: The sub-field's own `SubfieldConfig` (may be `None`)
22+
/// - `parent`: Parent config implementing `HasThresholdStyle` for fallback (may be `None`)
23+
///
24+
/// # Returns
25+
/// `None` when the format path renders empty (conditional group with absent `$value`).
26+
/// `Some(styled_string)` otherwise.
27+
pub fn render_styled_value(
28+
val_str: &str,
29+
threshold_val: Option<f64>,
30+
sub_cfg: Option<&crate::config::SubfieldConfig>,
31+
parent: Option<&dyn crate::config::HasThresholdStyle>,
32+
) -> Option<String> {
33+
// Resolve all fields with sub → parent fallback
34+
let style = sub_cfg
35+
.and_then(|c| c.style.as_deref())
36+
.or_else(|| parent.and_then(|p| p.style()));
37+
let mut effective_val = threshold_val;
38+
let mut warn_threshold = sub_cfg
39+
.and_then(|c| c.warn_threshold)
40+
.or_else(|| parent.and_then(|p| p.warn_threshold()));
41+
let mut warn_style = sub_cfg
42+
.and_then(|c| c.warn_style.as_deref())
43+
.or_else(|| parent.and_then(|p| p.warn_style()));
44+
let mut critical_threshold = sub_cfg
45+
.and_then(|c| c.critical_threshold)
46+
.or_else(|| parent.and_then(|p| p.critical_threshold()));
47+
let mut critical_style = sub_cfg
48+
.and_then(|c| c.critical_style.as_deref())
49+
.or_else(|| parent.and_then(|p| p.critical_style()));
50+
51+
// Inverted thresholds: use sub-only values (negated) and negate the comparison value.
52+
// Parent thresholds are in the non-inverted domain and must not be inherited.
53+
if sub_cfg.and_then(|c| c.invert_threshold).unwrap_or(false) {
54+
effective_val = threshold_val.map(|v| -v);
55+
warn_threshold = sub_cfg.and_then(|c| c.warn_threshold).map(|t| -t);
56+
warn_style = sub_cfg.and_then(|c| c.warn_style.as_deref());
57+
critical_threshold = sub_cfg.and_then(|c| c.critical_threshold).map(|t| -t);
58+
critical_style = sub_cfg.and_then(|c| c.critical_style.as_deref());
59+
}
60+
61+
// Format path: resolve symbol, threshold style, then apply format
62+
let fmt = sub_cfg
63+
.and_then(|c| c.format.as_deref())
64+
.or_else(|| parent.and_then(|p| p.format_str()));
65+
if let Some(fmt) = fmt {
66+
let symbol = sub_cfg
67+
.and_then(|c| c.symbol.as_deref())
68+
.or_else(|| parent.and_then(|p| p.symbol_str()));
69+
let effective_style = crate::ansi::resolve_threshold_style(
70+
effective_val, style, warn_threshold, warn_style,
71+
critical_threshold, critical_style,
72+
);
73+
return apply_module_format(fmt, Some(val_str), symbol, effective_style);
74+
}
75+
76+
// Default path: apply_style_with_threshold handles no-threshold gracefully
77+
Some(crate::ansi::apply_style_with_threshold(
78+
val_str, effective_val, style, warn_threshold, warn_style,
79+
critical_threshold, critical_style,
80+
))
81+
}
82+
1383
/// Apply a per-module format string.
1484
///
1585
/// # Arguments
@@ -150,6 +220,91 @@ fn find_matching_close(s: &str, start: usize, open: char, close: char) -> Option
150220
mod tests {
151221
use super::*;
152222

223+
// --- render_styled_value tests ---
224+
225+
#[test]
226+
fn test_render_styled_value_with_format_string() {
227+
let sub = crate::config::SubfieldConfig {
228+
format: Some("[$value]($style)".to_string()),
229+
style: Some("bold green".to_string()),
230+
..Default::default()
231+
};
232+
let result = render_styled_value("85", Some(85.0), Some(&sub), None);
233+
let s = result.unwrap();
234+
assert!(s.contains("85"), "value present: {s:?}");
235+
assert!(s.contains('\x1b'), "ANSI codes present: {s:?}");
236+
}
237+
238+
#[test]
239+
fn test_render_styled_value_no_format_threshold_above_warn() {
240+
let sub = crate::config::SubfieldConfig {
241+
warn_threshold: Some(50.0),
242+
warn_style: Some("yellow".to_string()),
243+
critical_threshold: Some(90.0),
244+
critical_style: Some("bold red".to_string()),
245+
..Default::default()
246+
};
247+
let result = render_styled_value("75", Some(75.0), Some(&sub), None);
248+
let s = result.unwrap();
249+
assert!(s.contains("75"), "value present: {s:?}");
250+
assert!(s.contains('\x1b'), "ANSI codes for warn style: {s:?}");
251+
}
252+
253+
#[test]
254+
fn test_render_styled_value_no_format_no_threshold() {
255+
let sub = crate::config::SubfieldConfig {
256+
style: Some("cyan".to_string()),
257+
..Default::default()
258+
};
259+
let result = render_styled_value("hello", None, Some(&sub), None);
260+
let s = result.unwrap();
261+
assert!(s.contains("hello"), "value present: {s:?}");
262+
assert!(s.contains('\x1b'), "ANSI codes for base style: {s:?}");
263+
}
264+
265+
#[test]
266+
fn test_render_styled_value_invert_threshold() {
267+
// invert_threshold: value 20 with warn_threshold 30 → inverted: -20 >= -30 → warn fires
268+
let sub = crate::config::SubfieldConfig {
269+
invert_threshold: Some(true),
270+
warn_threshold: Some(30.0),
271+
warn_style: Some("yellow".to_string()),
272+
critical_threshold: Some(10.0),
273+
critical_style: Some("bold red".to_string()),
274+
..Default::default()
275+
};
276+
let result = render_styled_value("20", Some(20.0), Some(&sub), None);
277+
let s = result.unwrap();
278+
assert!(s.contains("20"), "value present: {s:?}");
279+
assert!(s.contains('\x1b'), "ANSI codes for inverted warn: {s:?}");
280+
}
281+
282+
#[test]
283+
fn test_render_styled_value_parent_fallback_style() {
284+
let sub = crate::config::SubfieldConfig {
285+
..Default::default()
286+
};
287+
let parent = crate::config::ContextWindowConfig {
288+
style: Some("bold magenta".to_string()),
289+
..Default::default()
290+
};
291+
let result = render_styled_value(
292+
"42",
293+
None,
294+
Some(&sub),
295+
Some(&parent as &dyn crate::config::HasThresholdStyle),
296+
);
297+
let s = result.unwrap();
298+
assert!(s.contains("42"), "value present: {s:?}");
299+
assert!(s.contains('\x1b'), "ANSI codes from parent style: {s:?}");
300+
}
301+
302+
#[test]
303+
fn test_render_styled_value_no_sub_no_parent() {
304+
let result = render_styled_value("plain", None, None, None);
305+
assert_eq!(result, Some("plain".to_string()));
306+
}
307+
153308
#[test]
154309
fn test_dollar_value_substituted() {
155310
let result = apply_module_format("$value", Some("35"), None, None);

0 commit comments

Comments
 (0)