starship/
module.rs

1use crate::segment;
2use crate::segment::{FillSegment, Segment};
3use nu_ansi_term::{AnsiString, AnsiStrings, Style as AnsiStyle};
4use std::fmt;
5use std::time::Duration;
6
7// List of all modules
8// Default ordering is handled in configs/starship_root.rs
9pub const ALL_MODULES: &[&str] = &[
10    "aws",
11    "azure",
12    #[cfg(feature = "battery")]
13    "battery",
14    "buf",
15    "bun",
16    "c",
17    "character",
18    "cmake",
19    "cmd_duration",
20    "cobol",
21    "conda",
22    "container",
23    "cpp",
24    "crystal",
25    "daml",
26    "dart",
27    "deno",
28    "directory",
29    "direnv",
30    "docker_context",
31    "dotnet",
32    "elixir",
33    "elm",
34    "erlang",
35    "fennel",
36    "fill",
37    "fortran",
38    "fossil_branch",
39    "fossil_metrics",
40    "gcloud",
41    "git_branch",
42    "git_commit",
43    "git_metrics",
44    "git_state",
45    "git_status",
46    "gleam",
47    "golang",
48    "gradle",
49    "guix_shell",
50    "haskell",
51    "haxe",
52    "helm",
53    "hg_branch",
54    "hg_state",
55    "hostname",
56    "java",
57    "jobs",
58    "julia",
59    "kotlin",
60    "kubernetes",
61    "line_break",
62    "localip",
63    "lua",
64    "memory_usage",
65    "meson",
66    "mise",
67    "mojo",
68    "nats",
69    "netns",
70    "nim",
71    "nix_shell",
72    "nodejs",
73    "ocaml",
74    "odin",
75    "opa",
76    "openstack",
77    "os",
78    "package",
79    "perl",
80    "php",
81    "pijul_channel",
82    "pixi",
83    "pulumi",
84    "purescript",
85    "python",
86    "quarto",
87    "raku",
88    "red",
89    "rlang",
90    "ruby",
91    "rust",
92    "scala",
93    "shell",
94    "shlvl",
95    "singularity",
96    "solidity",
97    "spack",
98    "status",
99    "sudo",
100    "swift",
101    "terraform",
102    "time",
103    "typst",
104    "username",
105    "vagrant",
106    "vcsh",
107    "vlang",
108    "xmake",
109    "zig",
110];
111
112/// A module is a collection of segments showing data for a single integration
113/// (e.g. The git module shows the current git branch and status)
114pub struct Module<'a> {
115    /// The module's configuration map if available
116    pub config: Option<&'a toml::Value>,
117
118    /// The module's name, to be used in configuration and logging.
119    name: String,
120
121    /// The module's description
122    description: String,
123
124    /// The collection of segments that compose this module.
125    pub segments: Vec<Segment>,
126
127    /// the time it took to compute this module
128    pub duration: Duration,
129}
130
131impl<'a> Module<'a> {
132    /// Creates a module with no segments.
133    pub fn new(
134        name: impl Into<String>,
135        desc: impl Into<String>,
136        config: Option<&'a toml::Value>,
137    ) -> Self {
138        Self {
139            config,
140            name: name.into(),
141            description: desc.into(),
142            segments: Vec::new(),
143            duration: Duration::default(),
144        }
145    }
146
147    /// Set segments in module
148    pub fn set_segments(&mut self, segments: Vec<Segment>) {
149        self.segments = segments;
150    }
151
152    /// Get module's name
153    pub fn get_name(&self) -> &String {
154        &self.name
155    }
156
157    /// Get module's description
158    pub fn get_description(&self) -> &String {
159        &self.description
160    }
161
162    /// Whether a module has non-empty segments
163    pub fn is_empty(&self) -> bool {
164        self.segments
165            .iter()
166            // no trim: if we add spaces/linebreaks it's not "empty" as we change the final output
167            .all(|segment| segment.value().is_empty())
168    }
169
170    /// Get values of the module's segments
171    pub fn get_segments(&self) -> Vec<&str> {
172        self.segments.iter().map(segment::Segment::value).collect()
173    }
174
175    /// Returns a vector of colored `AnsiString` elements to be later used with
176    /// `AnsiStrings()` to optimize ANSI codes
177    pub fn ansi_strings(&self) -> Vec<AnsiString<'_>> {
178        self.ansi_strings_for_width(None)
179    }
180
181    pub fn ansi_strings_for_width(&self, width: Option<usize>) -> Vec<AnsiString<'_>> {
182        let mut iter = self.segments.iter().peekable();
183        let mut ansi_strings: Vec<AnsiString> = Vec::new();
184        while iter.peek().is_some() {
185            ansi_strings.extend(ansi_line(&mut iter, width));
186        }
187        ansi_strings
188    }
189}
190
191impl fmt::Display for Module<'_> {
192    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
193        let ansi_strings = self.ansi_strings();
194        write!(f, "{}", AnsiStrings(&ansi_strings))
195    }
196}
197
198fn ansi_line<'a, I>(segments: &mut I, term_width: Option<usize>) -> Vec<AnsiString<'a>>
199where
200    I: Iterator<Item = &'a Segment>,
201{
202    let mut used = 0usize;
203    let mut current: Vec<AnsiString> = Vec::new();
204    let mut chunks: Vec<(Vec<AnsiString>, &FillSegment)> = Vec::new();
205    let mut prev_style: Option<AnsiStyle> = None;
206
207    for segment in segments {
208        match segment {
209            Segment::Fill(fs) => {
210                chunks.push((current, fs));
211                current = Vec::new();
212                prev_style = None;
213            }
214            _ => {
215                used += segment.width_graphemes();
216                let current_segment_string = segment.ansi_string(prev_style.as_ref());
217
218                prev_style = Some(*current_segment_string.style_ref());
219                current.push(current_segment_string);
220            }
221        }
222
223        if matches!(segment, Segment::LineTerm) {
224            break;
225        }
226    }
227
228    if chunks.is_empty() {
229        current
230    } else {
231        let fill_size = term_width
232            .and_then(|tw| if tw > used { Some(tw - used) } else { None })
233            .map(|remaining| remaining / chunks.len());
234        chunks
235            .into_iter()
236            .flat_map(|(strs, fill)| {
237                let fill_string = fill.ansi_string(
238                    fill_size,
239                    strs.last().map(nu_ansi_term::AnsiGenericString::style_ref),
240                );
241                strs.into_iter().chain(std::iter::once(fill_string))
242            })
243            .chain(current)
244            .collect::<Vec<AnsiString>>()
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    #[test]
253    fn test_all_modules_is_in_alphabetical_order() {
254        let mut sorted_modules: Vec<&str> = ALL_MODULES.to_vec();
255        sorted_modules.sort_unstable();
256        assert_eq!(sorted_modules.as_slice(), ALL_MODULES);
257    }
258
259    #[test]
260    fn test_module_is_empty_with_no_segments() {
261        let name = "unit_test";
262        let desc = "This is a unit test";
263        let module = Module {
264            config: None,
265            name: name.to_string(),
266            description: desc.to_string(),
267            segments: Vec::new(),
268            duration: Duration::default(),
269        };
270
271        assert!(module.is_empty());
272    }
273
274    #[test]
275    fn test_module_is_empty_with_all_empty_segments() {
276        let name = "unit_test";
277        let desc = "This is a unit test";
278        let module = Module {
279            config: None,
280            name: name.to_string(),
281            description: desc.to_string(),
282            segments: Segment::from_text(None, ""),
283            duration: Duration::default(),
284        };
285
286        assert!(module.is_empty());
287    }
288
289    #[test]
290    fn test_module_is_not_empty_with_linebreak_only() {
291        let name = "unit_test";
292        let desc = "This is a unit test";
293        let module = Module {
294            config: None,
295            name: name.to_string(),
296            description: desc.to_string(),
297            segments: Segment::from_text(None, "\n"),
298            duration: Duration::default(),
299        };
300
301        assert!(!module.is_empty());
302    }
303
304    #[test]
305    fn test_module_is_not_empty_with_space_only() {
306        let name = "unit_test";
307        let desc = "This is a unit test";
308        let module = Module {
309            config: None,
310            name: name.to_string(),
311            description: desc.to_string(),
312            segments: Segment::from_text(None, " "),
313            duration: Duration::default(),
314        };
315
316        assert!(!module.is_empty());
317    }
318}