1
0
mirror of https://github.com/Llewellynvdm/starship.git synced 2024-06-01 08:00:51 +00:00
starship/src/module.rs
Daniel Watkins 1a72757f01
fix: combine ANSI color codes before wrapping them (#5762)
* combine ANSI color codes before wrapping them

The existing code wraps each individual module's output for
`context.shell`, concatenates all that output together and passes it to
`AnsiStrings` to merge ANSI color codes.  However, the wrapping obscures
ANSI color codes, meaning that no merging is possible.

This commit changes the shell-specific wrapping to happen right before
output, once all modules' output has been concatenated together.  This
results in ANSI color codes being correctly merged, as well as reducing
the number of calls to `wrap_colorseq_for_shell` to one.

With a minimal `starship.toml`:

```
format = """$directory"""

[directory]
format = '[a]($style)[b]($style)'
```

The current code produces[0]:

```
\n%{\x1b[31m%}a%{\x1b[0m%}%{\x1b[31m%}b%{\x1b[0m%
```

And this commit's code:

```
\n%{\x1b[31m%}ab%{\x1b[0m%}
```

You can see that the current code emits an additional reset and repeated
color code between "a" and "b" compared to the new code.

[0] Produced in a Python shell with:

```
subprocess.check_output(
    "./target/debug/starship prompt", shell=True,
    env={"STARSHIP_CONFIG": "./starship.toml", "STARSHIP_SHELL": "zsh"}
)
```

* utils: return early from wrap_seq_for_shell unless wrapping required

* refactor(utils): simplify wrap_seq_for_shell

This commit modifies wrap_seq_for_shell to (a) return early for shells
with no wrapping required, and (b) determine the wrapping characters
once at the start of the function (rather than inline in the map
function for every character).
2024-04-06 15:28:26 +02:00

298 lines
7.1 KiB
Rust

use crate::segment;
use crate::segment::{FillSegment, Segment};
use nu_ansi_term::{AnsiString, AnsiStrings};
use std::fmt;
use std::time::Duration;
// List of all modules
// Default ordering is handled in configs/starship_root.rs
pub const ALL_MODULES: &[&str] = &[
"aws",
"azure",
#[cfg(feature = "battery")]
"battery",
"buf",
"bun",
"c",
"character",
"cmake",
"cmd_duration",
"cobol",
"conda",
"container",
"crystal",
"daml",
"dart",
"deno",
"directory",
"direnv",
"docker_context",
"dotnet",
"elixir",
"elm",
"erlang",
"fennel",
"fill",
"fossil_branch",
"fossil_metrics",
"gcloud",
"git_branch",
"git_commit",
"git_metrics",
"git_state",
"git_status",
"gleam",
"golang",
"gradle",
"guix_shell",
"haskell",
"haxe",
"helm",
"hg_branch",
"hostname",
"java",
"jobs",
"julia",
"kotlin",
"kubernetes",
"line_break",
"localip",
"lua",
"memory_usage",
"meson",
"nim",
"nix_shell",
"nodejs",
"ocaml",
"odin",
"opa",
"openstack",
"os",
"package",
"perl",
"php",
"pijul_channel",
"pulumi",
"purescript",
"python",
"quarto",
"raku",
"red",
"rlang",
"ruby",
"rust",
"scala",
"shell",
"shlvl",
"singularity",
"solidity",
"spack",
"status",
"sudo",
"swift",
"terraform",
"time",
"typst",
"username",
"vagrant",
"vcsh",
"vlang",
"zig",
];
/// A module is a collection of segments showing data for a single integration
/// (e.g. The git module shows the current git branch and status)
pub struct Module<'a> {
/// The module's configuration map if available
pub config: Option<&'a toml::Value>,
/// The module's name, to be used in configuration and logging.
name: String,
/// The module's description
description: String,
/// The collection of segments that compose this module.
pub segments: Vec<Segment>,
/// the time it took to compute this module
pub duration: Duration,
}
impl<'a> Module<'a> {
/// Creates a module with no segments.
pub fn new(name: &str, desc: &str, config: Option<&'a toml::Value>) -> Module<'a> {
Module {
config,
name: name.to_string(),
description: desc.to_string(),
segments: Vec::new(),
duration: Duration::default(),
}
}
/// Set segments in module
pub fn set_segments(&mut self, segments: Vec<Segment>) {
self.segments = segments;
}
/// Get module's name
pub fn get_name(&self) -> &String {
&self.name
}
/// Get module's description
pub fn get_description(&self) -> &String {
&self.description
}
/// Whether a module has non-empty segments
pub fn is_empty(&self) -> bool {
self.segments
.iter()
// no trim: if we add spaces/linebreaks it's not "empty" as we change the final output
.all(|segment| segment.value().is_empty())
}
/// Get values of the module's segments
pub fn get_segments(&self) -> Vec<&str> {
self.segments.iter().map(segment::Segment::value).collect()
}
/// Returns a vector of colored `AnsiString` elements to be later used with
/// `AnsiStrings()` to optimize ANSI codes
pub fn ansi_strings(&self) -> Vec<AnsiString> {
self.ansi_strings_for_width(None)
}
pub fn ansi_strings_for_width(&self, width: Option<usize>) -> Vec<AnsiString> {
let mut iter = self.segments.iter().peekable();
let mut ansi_strings: Vec<AnsiString> = Vec::new();
while iter.peek().is_some() {
ansi_strings.extend(ansi_line(&mut iter, width));
}
ansi_strings
}
}
impl<'a> fmt::Display for Module<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let ansi_strings = self.ansi_strings();
write!(f, "{}", AnsiStrings(&ansi_strings))
}
}
fn ansi_line<'a, I>(segments: &mut I, term_width: Option<usize>) -> Vec<AnsiString<'a>>
where
I: Iterator<Item = &'a Segment>,
{
let mut used = 0usize;
let mut current: Vec<AnsiString> = Vec::new();
let mut chunks: Vec<(Vec<AnsiString>, &FillSegment)> = Vec::new();
for segment in segments {
match segment {
Segment::Fill(fs) => {
chunks.push((current, fs));
current = Vec::new();
}
_ => {
used += segment.width_graphemes();
current.push(segment.ansi_string());
}
}
if matches!(segment, Segment::LineTerm) {
break;
}
}
if chunks.is_empty() {
current
} else {
let fill_size = term_width
.and_then(|tw| if tw > used { Some(tw - used) } else { None })
.map(|remaining| remaining / chunks.len());
chunks
.into_iter()
.flat_map(|(strs, fill)| {
strs.into_iter()
.chain(std::iter::once(fill.ansi_string(fill_size)))
})
.chain(current)
.collect::<Vec<AnsiString>>()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_all_modules_is_in_alphabetical_order() {
let mut sorted_modules: Vec<&str> = ALL_MODULES.to_vec();
sorted_modules.sort_unstable();
assert_eq!(sorted_modules.as_slice(), ALL_MODULES);
}
#[test]
fn test_module_is_empty_with_no_segments() {
let name = "unit_test";
let desc = "This is a unit test";
let module = Module {
config: None,
name: name.to_string(),
description: desc.to_string(),
segments: Vec::new(),
duration: Duration::default(),
};
assert!(module.is_empty());
}
#[test]
fn test_module_is_empty_with_all_empty_segments() {
let name = "unit_test";
let desc = "This is a unit test";
let module = Module {
config: None,
name: name.to_string(),
description: desc.to_string(),
segments: Segment::from_text(None, ""),
duration: Duration::default(),
};
assert!(module.is_empty());
}
#[test]
fn test_module_is_not_empty_with_linebreak_only() {
let name = "unit_test";
let desc = "This is a unit test";
let module = Module {
config: None,
name: name.to_string(),
description: desc.to_string(),
segments: Segment::from_text(None, "\n"),
duration: Duration::default(),
};
assert!(!module.is_empty());
}
#[test]
fn test_module_is_not_empty_with_space_only() {
let name = "unit_test";
let desc = "This is a unit test";
let module = Module {
config: None,
name: name.to_string(),
description: desc.to_string(),
segments: Segment::from_text(None, " "),
duration: Duration::default(),
};
assert!(!module.is_empty());
}
}