Skip to main content

linesmith_core/segments/
version.rs

1//! Version segment: renders Claude Code's CLI version from the
2//! top-level stdin `version` field. Hidden when the payload doesn't
3//! carry it. Opt-in: not in the default segment list.
4
5use std::collections::BTreeMap;
6
7use super::{RenderContext, RenderResult, RenderedSegment, Segment, SegmentDefaults};
8use crate::data_context::DataContext;
9use crate::theme::Role;
10
11pub const ID: &str = "version";
12
13/// Informational; drops before cost (192), kept past rate-limit (96).
14const PRIORITY: u8 = 160;
15
16const DEFAULT_PREFIX: &str = "v";
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub(crate) struct Config {
20    pub(crate) prefix: String,
21}
22
23impl Default for Config {
24    fn default() -> Self {
25        Self {
26            prefix: DEFAULT_PREFIX.to_string(),
27        }
28    }
29}
30
31#[derive(Default)]
32pub struct VersionSegment {
33    cfg: Config,
34}
35
36impl VersionSegment {
37    /// Build from `[segments.version]` extras. Reads `prefix` (string,
38    /// default `"v"`) — text to prepend to the version. Set to `""` for
39    /// raw output.
40    pub fn from_extras(
41        extras: &BTreeMap<String, toml::Value>,
42        warn: &mut impl FnMut(&str),
43    ) -> Self {
44        let mut cfg = Config::default();
45        if let Some(v) = extras.get("prefix") {
46            match v.as_str() {
47                Some(s) => cfg.prefix = s.to_string(),
48                None => warn(&format!(
49                    "segments.{ID}.prefix: expected a string; ignoring"
50                )),
51            }
52        }
53        Self { cfg }
54    }
55}
56
57impl Segment for VersionSegment {
58    fn render(&self, ctx: &DataContext, _rc: &RenderContext) -> RenderResult {
59        let Some(v) = ctx.status.version.as_deref() else {
60            crate::lsm_debug!("version: status.version absent; hiding");
61            return Ok(None);
62        };
63        let text = if self.cfg.prefix.is_empty() {
64            v.to_string()
65        } else {
66            format!("{}{v}", self.cfg.prefix)
67        };
68        Ok(Some(RenderedSegment::new(text).with_role(Role::Muted)))
69    }
70
71    fn defaults(&self) -> SegmentDefaults {
72        SegmentDefaults::with_priority(PRIORITY)
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use crate::input::{ModelInfo, StatusContext, Tool, WorkspaceInfo};
80    use std::path::PathBuf;
81    use std::sync::Arc;
82
83    fn rc() -> RenderContext {
84        RenderContext::new(80)
85    }
86
87    fn ctx(version: Option<String>) -> DataContext {
88        DataContext::new(StatusContext {
89            tool: Tool::ClaudeCode,
90            model: Some(ModelInfo {
91                display_name: "X".into(),
92            }),
93            workspace: Some(WorkspaceInfo {
94                project_dir: PathBuf::from("/repo"),
95                git_worktree: None,
96            }),
97            context_window: None,
98            cost: None,
99            effort: None,
100            vim: None,
101            output_style: None,
102            agent_name: None,
103            version,
104            raw: Arc::new(serde_json::Value::Null),
105        })
106    }
107
108    #[test]
109    fn renders_with_default_v_prefix() {
110        assert_eq!(
111            VersionSegment::default()
112                .render(&ctx(Some("2.1.90".into())), &rc())
113                .unwrap(),
114            Some(RenderedSegment::new("v2.1.90").with_role(Role::Muted))
115        );
116    }
117
118    #[test]
119    fn empty_prefix_emits_raw_version() {
120        // `prefix = ""` is the documented opt-out for the default `v`.
121        // Pin it so a future "non-empty prefix" guard doesn't silently
122        // turn empty back into the default.
123        let mut extras = BTreeMap::new();
124        extras.insert("prefix".to_string(), toml::Value::String(String::new()));
125        let seg = VersionSegment::from_extras(&extras, &mut |_| {});
126        assert_eq!(
127            seg.render(&ctx(Some("2.1.90".into())), &rc()).unwrap(),
128            Some(RenderedSegment::new("2.1.90").with_role(Role::Muted))
129        );
130    }
131
132    #[test]
133    fn custom_prefix_passes_through() {
134        let mut extras = BTreeMap::new();
135        extras.insert("prefix".to_string(), toml::Value::String("CC ".to_string()));
136        let seg = VersionSegment::from_extras(&extras, &mut |_| {});
137        assert_eq!(
138            seg.render(&ctx(Some("2.1.90".into())), &rc()).unwrap(),
139            Some(RenderedSegment::new("CC 2.1.90").with_role(Role::Muted))
140        );
141    }
142
143    #[test]
144    fn hidden_when_absent() {
145        assert_eq!(
146            VersionSegment::default().render(&ctx(None), &rc()).unwrap(),
147            None
148        );
149    }
150
151    #[test]
152    fn from_extras_warns_on_non_string_prefix_and_keeps_default() {
153        let mut extras = BTreeMap::new();
154        extras.insert("prefix".to_string(), toml::Value::Integer(1));
155        let mut warnings = Vec::new();
156        let seg = VersionSegment::from_extras(&extras, &mut |m| warnings.push(m.to_string()));
157        assert_eq!(seg.cfg.prefix, DEFAULT_PREFIX);
158        assert_eq!(warnings.len(), 1);
159        assert!(warnings[0].contains("segments.version.prefix"));
160    }
161
162    #[test]
163    fn defaults_use_expected_priority() {
164        assert_eq!(VersionSegment::default().defaults().priority, PRIORITY);
165    }
166}