Skip to main content

vtcode_config/
terminal_title.rs

1use serde::{Deserialize, Serialize};
2
3pub const DEFAULT_TERMINAL_TITLE_ITEMS: &[&str] = &["spinner", "project"];
4
5#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
6#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, Eq)]
7pub struct TerminalTitleConfig {
8    /// Ordered title items; `None` resolves to `["spinner", "project"]`
9    /// (spinner enabled by default), and an empty array disables VT
10    /// Code-managed titles.
11    #[serde(default)]
12    pub items: Option<Vec<String>>,
13
14    /// Show the rotating loading indicator in the terminal title. Disable
15    /// to suppress the tab spinner (footer shimmer is unaffected).
16    #[serde(default = "default_global_indicator")]
17    pub global_indicator: bool,
18}
19
20fn default_global_indicator() -> bool {
21    true
22}
23
24/// Effective title items with the global loading indicator applied: a
25/// disabled indicator drops the spinner entry, keeping every other item.
26pub fn effective_title_items(items: Option<&[String]>, global_indicator: bool) -> Vec<String> {
27    let resolved = items
28        .map(<[String]>::to_vec)
29        .unwrap_or_else(|| DEFAULT_TERMINAL_TITLE_ITEMS.iter().map(|item| (*item).to_string()).collect());
30    if global_indicator {
31        resolved
32    } else {
33        resolved.into_iter().filter(|item| item != "spinner").collect()
34    }
35}
36
37impl TerminalTitleConfig {
38    #[must_use]
39    fn effective_items(&self) -> Vec<String> {
40        self.items
41            .clone()
42            .unwrap_or_else(|| DEFAULT_TERMINAL_TITLE_ITEMS.iter().map(|item| (*item).to_string()).collect())
43    }
44
45    #[must_use]
46    fn manages_title(&self) -> bool {
47        self.items.as_ref().is_none_or(|items| !items.is_empty())
48    }
49}
50
51#[cfg(test)]
52mod tests {
53    use super::{DEFAULT_TERMINAL_TITLE_ITEMS, TerminalTitleConfig, effective_title_items};
54
55    #[test]
56    fn shipped_default_items_lead_with_the_spinner() {
57        // The shipped binary must always manage the title with the activity
58        // spinner first; single source in `data/default_config.toml` mirrors it.
59        assert_eq!(DEFAULT_TERMINAL_TITLE_ITEMS, ["spinner", "project"]);
60    }
61
62    #[test]
63    fn serde_defaults_when_terminal_title_table_is_missing() {
64        let config: TerminalTitleConfig = toml::from_str("").expect("config should parse");
65
66        assert_eq!(config.items, None);
67    }
68
69    #[test]
70    fn serde_preserves_explicit_empty_items() {
71        let config: TerminalTitleConfig = toml::from_str("items = []").expect("config should parse");
72
73        assert_eq!(config.items, Some(Vec::new()));
74    }
75
76    #[test]
77    fn serde_preserves_valid_item_list() {
78        let config: TerminalTitleConfig =
79            toml::from_str("items = [\"spinner\", \"project\", \"model\"]").expect("config should parse");
80
81        assert_eq!(config.items, Some(vec!["spinner".to_string(), "project".to_string(), "model".to_string()]));
82    }
83
84    #[test]
85    fn disabled_global_indicator_drops_only_the_spinner() {
86        assert_eq!(
87            effective_title_items(None, true),
88            DEFAULT_TERMINAL_TITLE_ITEMS
89                .iter()
90                .map(|item| (*item).to_string())
91                .collect::<Vec<_>>()
92        );
93        // Defaults resolve first, so the spinner drops even when unset.
94        assert_eq!(effective_title_items(None, false), vec!["project".to_string()]);
95        let configured = Some(vec!["status".to_string(), "spinner".to_string(), "thread".to_string()]);
96        assert_eq!(
97            effective_title_items(configured.as_deref(), false),
98            vec!["status".to_string(), "thread".to_string()]
99        );
100        // The setting defaults on through serde.
101        let config: TerminalTitleConfig = toml::from_str("").expect("config should parse");
102        assert!(config.global_indicator);
103    }
104
105    #[test]
106    fn missing_items_uses_default_terminal_title_items() {
107        let config = TerminalTitleConfig::default();
108
109        assert_eq!(
110            config.effective_items(),
111            DEFAULT_TERMINAL_TITLE_ITEMS
112                .iter()
113                .map(|item| (*item).to_string())
114                .collect::<Vec<_>>()
115        );
116        assert!(config.manages_title());
117    }
118
119    #[test]
120    fn explicit_empty_items_disables_terminal_title_management() {
121        let config = TerminalTitleConfig { items: Some(Vec::new()), global_indicator: true };
122
123        assert!(config.effective_items().is_empty());
124        assert!(!config.manages_title());
125    }
126
127    #[test]
128    fn explicit_items_are_preserved() {
129        let config = TerminalTitleConfig {
130            items: Some(vec!["project".to_string(), "model".to_string()]),
131            global_indicator: true,
132        };
133
134        assert_eq!(config.effective_items(), vec!["project".to_string(), "model".to_string()]);
135        assert!(config.manages_title());
136    }
137}