feat: Add a fill module to pad out the line (#3029)
This commit is contained in:
parent
5ac7ad741f
commit
5d0a38aca3
|
@ -1119,6 +1119,37 @@ By default the module will be shown if any of the following conditions are met:
|
||||||
format = "via [e $version](bold red) "
|
format = "via [e $version](bold red) "
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Fill
|
||||||
|
|
||||||
|
The `fill` module fills any extra space on the line with a symbol. If multiple `fill` modules are
|
||||||
|
present in a line they will split the space evenly between them. This is useful for aligning
|
||||||
|
other modules.
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
| Option | Default | Description |
|
||||||
|
| ---------- | -------------- | -------------------------------------- |
|
||||||
|
| `symbol` | `"."` | The symbol used to fill the line. |
|
||||||
|
| `style` | `"bold black"` | The style for the module. |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# ~/.config/starship.toml
|
||||||
|
format="AA $fill BB $fill CC"
|
||||||
|
|
||||||
|
[fill]
|
||||||
|
symbol = "-"
|
||||||
|
style = "bold green"
|
||||||
|
```
|
||||||
|
|
||||||
|
Produces a prompt that looks like:
|
||||||
|
|
||||||
|
```
|
||||||
|
AA -------------------------------------------- BB -------------------------------------------- CC
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
## Google Cloud (`gcloud`)
|
## Google Cloud (`gcloud`)
|
||||||
|
|
||||||
The `gcloud` module shows the current configuration for [`gcloud`](https://cloud.google.com/sdk/gcloud) CLI.
|
The `gcloud` module shows the current configuration for [`gcloud`](https://cloud.google.com/sdk/gcloud) CLI.
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
use crate::config::ModuleConfig;
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
use starship_module_config_derive::ModuleConfig;
|
||||||
|
|
||||||
|
#[derive(Clone, ModuleConfig, Serialize)]
|
||||||
|
pub struct FillConfig<'a> {
|
||||||
|
pub style: &'a str,
|
||||||
|
pub symbol: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Default for FillConfig<'a> {
|
||||||
|
fn default() -> Self {
|
||||||
|
FillConfig {
|
||||||
|
style: "bold black",
|
||||||
|
symbol: ".",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,7 @@ pub mod elixir;
|
||||||
pub mod elm;
|
pub mod elm;
|
||||||
pub mod env_var;
|
pub mod env_var;
|
||||||
pub mod erlang;
|
pub mod erlang;
|
||||||
|
pub mod fill;
|
||||||
pub mod gcloud;
|
pub mod gcloud;
|
||||||
pub mod git_branch;
|
pub mod git_branch;
|
||||||
pub mod git_commit;
|
pub mod git_commit;
|
||||||
|
@ -96,6 +97,7 @@ pub struct FullConfig<'a> {
|
||||||
elm: elm::ElmConfig<'a>,
|
elm: elm::ElmConfig<'a>,
|
||||||
env_var: IndexMap<String, env_var::EnvVarConfig<'a>>,
|
env_var: IndexMap<String, env_var::EnvVarConfig<'a>>,
|
||||||
erlang: erlang::ErlangConfig<'a>,
|
erlang: erlang::ErlangConfig<'a>,
|
||||||
|
fill: fill::FillConfig<'a>,
|
||||||
gcloud: gcloud::GcloudConfig<'a>,
|
gcloud: gcloud::GcloudConfig<'a>,
|
||||||
git_branch: git_branch::GitBranchConfig<'a>,
|
git_branch: git_branch::GitBranchConfig<'a>,
|
||||||
git_commit: git_commit::GitCommitConfig<'a>,
|
git_commit: git_commit::GitCommitConfig<'a>,
|
||||||
|
@ -169,6 +171,7 @@ impl<'a> Default for FullConfig<'a> {
|
||||||
elm: Default::default(),
|
elm: Default::default(),
|
||||||
env_var: Default::default(),
|
env_var: Default::default(),
|
||||||
erlang: Default::default(),
|
erlang: Default::default(),
|
||||||
|
fill: Default::default(),
|
||||||
gcloud: Default::default(),
|
gcloud: Default::default(),
|
||||||
git_branch: Default::default(),
|
git_branch: Default::default(),
|
||||||
git_commit: Default::default(),
|
git_commit: Default::default(),
|
||||||
|
|
|
@ -49,6 +49,9 @@ pub struct Context<'a> {
|
||||||
/// Construct the right prompt instead of the left prompt
|
/// Construct the right prompt instead of the left prompt
|
||||||
pub right: bool,
|
pub right: bool,
|
||||||
|
|
||||||
|
/// Width of terminal, or zero if width cannot be detected.
|
||||||
|
pub width: usize,
|
||||||
|
|
||||||
/// A HashMap of environment variable mocks
|
/// A HashMap of environment variable mocks
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub env: HashMap<&'a str, String>,
|
pub env: HashMap<&'a str, String>,
|
||||||
|
@ -135,6 +138,9 @@ impl<'a> Context<'a> {
|
||||||
repo: OnceCell::new(),
|
repo: OnceCell::new(),
|
||||||
shell,
|
shell,
|
||||||
right,
|
right,
|
||||||
|
width: term_size::dimensions()
|
||||||
|
.map(|(width, _)| width)
|
||||||
|
.unwrap_or_default(),
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
env: HashMap::new(),
|
env: HashMap::new(),
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -257,7 +257,7 @@ impl<'a> StringFormatter<'a> {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|el| {
|
.map(|el| {
|
||||||
match el {
|
match el {
|
||||||
FormatElement::Text(text) => Ok(vec![Segment::new(style, text)]),
|
FormatElement::Text(text) => Ok(Segment::from_text(style, text)),
|
||||||
FormatElement::TextGroup(textgroup) => {
|
FormatElement::TextGroup(textgroup) => {
|
||||||
let textgroup = TextGroup {
|
let textgroup = TextGroup {
|
||||||
format: textgroup.format,
|
format: textgroup.format,
|
||||||
|
@ -274,13 +274,11 @@ impl<'a> StringFormatter<'a> {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|mut segment| {
|
.map(|mut segment| {
|
||||||
// Derive upper style if the style of segments are none.
|
// Derive upper style if the style of segments are none.
|
||||||
if segment.style.is_none() {
|
segment.set_style_if_empty(style);
|
||||||
segment.style = style;
|
|
||||||
};
|
|
||||||
segment
|
segment
|
||||||
})
|
})
|
||||||
.collect()),
|
.collect()),
|
||||||
VariableValue::Plain(text) => Ok(vec![Segment::new(style, text)]),
|
VariableValue::Plain(text) => Ok(Segment::from_text(style, text)),
|
||||||
VariableValue::Meta(format) => {
|
VariableValue::Meta(format) => {
|
||||||
let formatter = StringFormatter {
|
let formatter = StringFormatter {
|
||||||
format,
|
format,
|
||||||
|
@ -322,9 +320,9 @@ impl<'a> StringFormatter<'a> {
|
||||||
VariableValue::Plain(plain_value) => {
|
VariableValue::Plain(plain_value) => {
|
||||||
!plain_value.is_empty()
|
!plain_value.is_empty()
|
||||||
}
|
}
|
||||||
VariableValue::Styled(segments) => {
|
VariableValue::Styled(segments) => segments
|
||||||
segments.iter().any(|x| !x.value.is_empty())
|
.iter()
|
||||||
}
|
.any(|x| !x.value().is_empty()),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -391,8 +389,8 @@ mod tests {
|
||||||
macro_rules! match_next {
|
macro_rules! match_next {
|
||||||
($iter:ident, $value:literal, $($style:tt)+) => {
|
($iter:ident, $value:literal, $($style:tt)+) => {
|
||||||
let _next = $iter.next().unwrap();
|
let _next = $iter.next().unwrap();
|
||||||
assert_eq!(_next.value, $value);
|
assert_eq!(_next.value(), $value);
|
||||||
assert_eq!(_next.style, $($style)+);
|
assert_eq!(_next.style(), $($style)+);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -511,14 +509,18 @@ mod tests {
|
||||||
let styled_style = Some(Color::Green.italic());
|
let styled_style = Some(Color::Green.italic());
|
||||||
let styled_no_modifier_style = Some(Color::Green.normal());
|
let styled_no_modifier_style = Some(Color::Green.normal());
|
||||||
|
|
||||||
|
let mut segments: Vec<Segment> = Vec::new();
|
||||||
|
segments.extend(Segment::from_text(None, "styless"));
|
||||||
|
segments.extend(Segment::from_text(styled_style, "styled"));
|
||||||
|
segments.extend(Segment::from_text(
|
||||||
|
styled_no_modifier_style,
|
||||||
|
"styled_no_modifier",
|
||||||
|
));
|
||||||
|
|
||||||
let formatter = StringFormatter::new(FORMAT_STR)
|
let formatter = StringFormatter::new(FORMAT_STR)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.map_variables_to_segments(|variable| match variable {
|
.map_variables_to_segments(|variable| match variable {
|
||||||
"var" => Some(Ok(vec![
|
"var" => Some(Ok(segments.clone())),
|
||||||
Segment::new(None, "styless"),
|
|
||||||
Segment::new(styled_style, "styled"),
|
|
||||||
Segment::new(styled_no_modifier_style, "styled_no_modifier"),
|
|
||||||
])),
|
|
||||||
_ => None,
|
_ => None,
|
||||||
});
|
});
|
||||||
let result = formatter.parse(None).unwrap();
|
let result = formatter.parse(None).unwrap();
|
||||||
|
|
|
@ -56,7 +56,7 @@ impl<'a> VersionFormatter<'a> {
|
||||||
formatted.map(|segments| {
|
formatted.map(|segments| {
|
||||||
segments
|
segments
|
||||||
.iter()
|
.iter()
|
||||||
.map(|segment| segment.value.as_str())
|
.map(|segment| segment.value())
|
||||||
.collect::<String>()
|
.collect::<String>()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::context::Shell;
|
use crate::context::Shell;
|
||||||
use crate::segment::Segment;
|
use crate::segment::{FillSegment, Segment};
|
||||||
use crate::utils::wrap_colorseq_for_shell;
|
use crate::utils::wrap_colorseq_for_shell;
|
||||||
use ansi_term::{ANSIString, ANSIStrings};
|
use ansi_term::{ANSIString, ANSIStrings};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
@ -26,6 +26,7 @@ pub const ALL_MODULES: &[&str] = &[
|
||||||
"elm",
|
"elm",
|
||||||
"env_var",
|
"env_var",
|
||||||
"erlang",
|
"erlang",
|
||||||
|
"fill",
|
||||||
"gcloud",
|
"gcloud",
|
||||||
"git_branch",
|
"git_branch",
|
||||||
"git_commit",
|
"git_commit",
|
||||||
|
@ -124,29 +125,29 @@ impl<'a> Module<'a> {
|
||||||
self.segments
|
self.segments
|
||||||
.iter()
|
.iter()
|
||||||
// no trim: if we add spaces/linebreaks it's not "empty" as we change the final output
|
// no trim: if we add spaces/linebreaks it's not "empty" as we change the final output
|
||||||
.all(|segment| segment.value.is_empty())
|
.all(|segment| segment.value().is_empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get values of the module's segments
|
/// Get values of the module's segments
|
||||||
pub fn get_segments(&self) -> Vec<&str> {
|
pub fn get_segments(&self) -> Vec<&str> {
|
||||||
self.segments
|
self.segments
|
||||||
.iter()
|
.iter()
|
||||||
.map(|segment| segment.value.as_str())
|
.map(|segment| segment.value())
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a vector of colored ANSIString elements to be later used with
|
/// Returns a vector of colored ANSIString elements to be later used with
|
||||||
/// `ANSIStrings()` to optimize ANSI codes
|
/// `ANSIStrings()` to optimize ANSI codes
|
||||||
pub fn ansi_strings(&self) -> Vec<ANSIString> {
|
pub fn ansi_strings(&self) -> Vec<ANSIString> {
|
||||||
self.ansi_strings_for_shell(Shell::Unknown)
|
self.ansi_strings_for_shell(Shell::Unknown, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ansi_strings_for_shell(&self, shell: Shell) -> Vec<ANSIString> {
|
pub fn ansi_strings_for_shell(&self, shell: Shell, width: Option<usize>) -> Vec<ANSIString> {
|
||||||
let ansi_strings = self
|
let mut iter = self.segments.iter().peekable();
|
||||||
.segments
|
let mut ansi_strings: Vec<ANSIString> = Vec::new();
|
||||||
.iter()
|
while iter.peek().is_some() {
|
||||||
.map(Segment::ansi_string)
|
ansi_strings.extend(ansi_line(&mut iter, width));
|
||||||
.collect::<Vec<ANSIString>>();
|
}
|
||||||
|
|
||||||
match shell {
|
match shell {
|
||||||
Shell::Bash => ansi_strings_modified(ansi_strings, shell),
|
Shell::Bash => ansi_strings_modified(ansi_strings, shell),
|
||||||
|
@ -174,6 +175,49 @@ fn ansi_strings_modified(ansi_strings: Vec<ANSIString>, shell: Shell) -> Vec<ANS
|
||||||
.collect::<Vec<ANSIString>>()
|
.collect::<Vec<ANSIString>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 let Segment::LineTerm = segment {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if chunks.is_empty() {
|
||||||
|
current
|
||||||
|
} else {
|
||||||
|
let fill_size = term_width
|
||||||
|
.map(|tw| if tw > used { Some(tw - used) } else { None })
|
||||||
|
.flatten()
|
||||||
|
.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.into_iter())
|
||||||
|
.collect::<Vec<ANSIString>>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -208,7 +252,7 @@ mod tests {
|
||||||
config: None,
|
config: None,
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
description: desc.to_string(),
|
description: desc.to_string(),
|
||||||
segments: vec![Segment::new(None, "")],
|
segments: Segment::from_text(None, ""),
|
||||||
duration: Duration::default(),
|
duration: Duration::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -223,7 +267,7 @@ mod tests {
|
||||||
config: None,
|
config: None,
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
description: desc.to_string(),
|
description: desc.to_string(),
|
||||||
segments: vec![Segment::new(None, "\n")],
|
segments: Segment::from_text(None, "\n"),
|
||||||
duration: Duration::default(),
|
duration: Duration::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -238,7 +282,7 @@ mod tests {
|
||||||
config: None,
|
config: None,
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
description: desc.to_string(),
|
description: desc.to_string(),
|
||||||
segments: vec![Segment::new(None, " ")],
|
segments: Segment::from_text(None, " "),
|
||||||
duration: Duration::default(),
|
duration: Duration::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
use super::{Context, Module};
|
||||||
|
|
||||||
|
use crate::config::{parse_style_string, RootModuleConfig};
|
||||||
|
use crate::configs::fill::FillConfig;
|
||||||
|
use crate::segment::Segment;
|
||||||
|
|
||||||
|
/// Creates a module that fills the any extra space on the line.
|
||||||
|
///
|
||||||
|
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
||||||
|
let mut module = context.new_module("fill");
|
||||||
|
let config: FillConfig = FillConfig::try_load(module.config);
|
||||||
|
|
||||||
|
let style = parse_style_string(config.style);
|
||||||
|
|
||||||
|
module.set_segments(vec![Segment::fill(style, config.symbol)]);
|
||||||
|
|
||||||
|
Some(module)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::test::ModuleRenderer;
|
||||||
|
use ansi_term::Color;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn basic() {
|
||||||
|
let actual = ModuleRenderer::new("fill")
|
||||||
|
.config(toml::toml! {
|
||||||
|
[fill]
|
||||||
|
style = "bold green"
|
||||||
|
symbol = "*-"
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let expected = Some(format!("{}", Color::Green.bold().paint("*-")));
|
||||||
|
|
||||||
|
assert_eq!(expected, actual);
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,11 +3,9 @@ use crate::segment::Segment;
|
||||||
|
|
||||||
/// Creates a module for the line break
|
/// Creates a module for the line break
|
||||||
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
||||||
const LINE_ENDING: &str = "\n";
|
|
||||||
|
|
||||||
let mut module = context.new_module("line_break");
|
let mut module = context.new_module("line_break");
|
||||||
|
|
||||||
module.set_segments(vec![Segment::new(None, LINE_ENDING)]);
|
module.set_segments(vec![Segment::LineTerm]);
|
||||||
|
|
||||||
Some(module)
|
Some(module)
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ mod elixir;
|
||||||
mod elm;
|
mod elm;
|
||||||
mod env_var;
|
mod env_var;
|
||||||
mod erlang;
|
mod erlang;
|
||||||
|
mod fill;
|
||||||
mod gcloud;
|
mod gcloud;
|
||||||
mod git_branch;
|
mod git_branch;
|
||||||
mod git_commit;
|
mod git_commit;
|
||||||
|
@ -97,6 +98,7 @@ pub fn handle<'a>(module: &str, context: &'a Context) -> Option<Module<'a>> {
|
||||||
"elm" => elm::module(context),
|
"elm" => elm::module(context),
|
||||||
"erlang" => erlang::module(context),
|
"erlang" => erlang::module(context),
|
||||||
"env_var" => env_var::module(context),
|
"env_var" => env_var::module(context),
|
||||||
|
"fill" => fill::module(context),
|
||||||
"gcloud" => gcloud::module(context),
|
"gcloud" => gcloud::module(context),
|
||||||
"git_branch" => git_branch::module(context),
|
"git_branch" => git_branch::module(context),
|
||||||
"git_commit" => git_commit::module(context),
|
"git_commit" => git_commit::module(context),
|
||||||
|
@ -181,6 +183,7 @@ pub fn description(module: &str) -> &'static str {
|
||||||
"dotnet" => "The relevant version of the .NET Core SDK for the current directory",
|
"dotnet" => "The relevant version of the .NET Core SDK for the current directory",
|
||||||
"env_var" => "Displays the current value of a selected environment variable",
|
"env_var" => "Displays the current value of a selected environment variable",
|
||||||
"erlang" => "Current OTP version",
|
"erlang" => "Current OTP version",
|
||||||
|
"fill" => "Fills the remaining space on the line with a pad string",
|
||||||
"gcloud" => "The current GCP client configuration",
|
"gcloud" => "The current GCP client configuration",
|
||||||
"git_branch" => "The active branch of the repo in your current directory",
|
"git_branch" => "The active branch of the repo in your current directory",
|
||||||
"git_commit" => "The active commit (and tag if any) of the repo in your current directory",
|
"git_commit" => "The active commit (and tag if any) of the repo in your current directory",
|
||||||
|
|
|
@ -16,7 +16,7 @@ use crate::module::ALL_MODULES;
|
||||||
use crate::modules;
|
use crate::modules;
|
||||||
use crate::segment::Segment;
|
use crate::segment::Segment;
|
||||||
|
|
||||||
pub struct Grapheme<'a>(&'a str);
|
pub struct Grapheme<'a>(pub &'a str);
|
||||||
|
|
||||||
impl<'a> Grapheme<'a> {
|
impl<'a> Grapheme<'a> {
|
||||||
pub fn width(&self) -> usize {
|
pub fn width(&self) -> usize {
|
||||||
|
@ -112,7 +112,7 @@ pub fn get_prompt(context: Context) -> String {
|
||||||
.expect("Unexpected error returned in root format variables"),
|
.expect("Unexpected error returned in root format variables"),
|
||||||
);
|
);
|
||||||
|
|
||||||
let module_strings = root_module.ansi_strings_for_shell(context.shell);
|
let module_strings = root_module.ansi_strings_for_shell(context.shell, Some(context.width));
|
||||||
if config.add_newline {
|
if config.add_newline {
|
||||||
writeln!(buf).unwrap();
|
writeln!(buf).unwrap();
|
||||||
}
|
}
|
||||||
|
|
185
src/segment.rs
185
src/segment.rs
|
@ -1,32 +1,21 @@
|
||||||
|
use crate::print::{Grapheme, UnicodeWidthGraphemes};
|
||||||
use ansi_term::{ANSIString, Style};
|
use ansi_term::{ANSIString, Style};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
/// A segment is a single configurable element in a module. This will usually
|
/// Type that holds text with an associated style
|
||||||
/// contain a data point to provide context for the prompt's user
|
|
||||||
/// (e.g. The version that software is running).
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Segment {
|
pub struct TextSegment {
|
||||||
/// The segment's style. If None, will inherit the style of the module containing it.
|
/// The segment's style. If None, will inherit the style of the module containing it.
|
||||||
pub style: Option<Style>,
|
style: Option<Style>,
|
||||||
|
|
||||||
/// The string value of the current segment.
|
/// The string value of the current segment.
|
||||||
pub value: String,
|
value: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Segment {
|
impl TextSegment {
|
||||||
/// Creates a new segment.
|
// Returns the ANSIString of the segment value
|
||||||
pub fn new<T>(style: Option<Style>, value: T) -> Self
|
fn ansi_string(&self) -> ANSIString {
|
||||||
where
|
|
||||||
T: Into<String>,
|
|
||||||
{
|
|
||||||
Self {
|
|
||||||
style,
|
|
||||||
value: value.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the ANSIString of the segment value, not including its prefix and suffix
|
|
||||||
pub fn ansi_string(&self) -> ANSIString {
|
|
||||||
match self.style {
|
match self.style {
|
||||||
Some(style) => style.paint(&self.value),
|
Some(style) => style.paint(&self.value),
|
||||||
None => ANSIString::from(&self.value),
|
None => ANSIString::from(&self.value),
|
||||||
|
@ -34,6 +23,162 @@ impl Segment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Type that holds fill text with an associated style
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct FillSegment {
|
||||||
|
/// The segment's style. If None, will inherit the style of the module containing it.
|
||||||
|
style: Option<Style>,
|
||||||
|
|
||||||
|
/// The string value of the current segment.
|
||||||
|
value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FillSegment {
|
||||||
|
// Returns the ANSIString of the segment value, not including its prefix and suffix
|
||||||
|
pub fn ansi_string(&self, width: Option<usize>) -> ANSIString {
|
||||||
|
let s = match width {
|
||||||
|
Some(w) => self
|
||||||
|
.value
|
||||||
|
.graphemes(true)
|
||||||
|
.cycle()
|
||||||
|
.scan(0usize, |len, g| {
|
||||||
|
*len += Grapheme(g).width();
|
||||||
|
if *len <= w {
|
||||||
|
Some(g)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<String>(),
|
||||||
|
None => String::from(&self.value),
|
||||||
|
};
|
||||||
|
match self.style {
|
||||||
|
Some(style) => style.paint(s),
|
||||||
|
None => ANSIString::from(s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod fill_seg_tests {
|
||||||
|
use super::FillSegment;
|
||||||
|
use ansi_term::Color;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ansi_string_width() {
|
||||||
|
let width: usize = 10;
|
||||||
|
let style = Color::Blue.bold();
|
||||||
|
|
||||||
|
let inputs = vec![
|
||||||
|
(".", ".........."),
|
||||||
|
(".:", ".:.:.:.:.:"),
|
||||||
|
("-:-", "-:--:--:--"),
|
||||||
|
("🟦", "🟦🟦🟦🟦🟦"),
|
||||||
|
("🟢🔵🟡", "🟢🔵🟡🟢🔵"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (text, expected) in inputs.iter() {
|
||||||
|
let f = FillSegment {
|
||||||
|
value: String::from(*text),
|
||||||
|
style: Some(style),
|
||||||
|
};
|
||||||
|
let actual = f.ansi_string(Some(width));
|
||||||
|
assert_eq!(style.paint(*expected), actual);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A segment is a styled text chunk ready for printing.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum Segment {
|
||||||
|
Text(TextSegment),
|
||||||
|
Fill(FillSegment),
|
||||||
|
LineTerm,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Segment {
|
||||||
|
/// Creates new segments from a text with a style; breaking out LineTerminators.
|
||||||
|
pub fn from_text<T>(style: Option<Style>, value: T) -> Vec<Segment>
|
||||||
|
where
|
||||||
|
T: Into<String>,
|
||||||
|
{
|
||||||
|
let mut segs: Vec<Segment> = Vec::new();
|
||||||
|
value.into().split(LINE_TERMINATOR).for_each(|s| {
|
||||||
|
if !segs.is_empty() {
|
||||||
|
segs.push(Segment::LineTerm)
|
||||||
|
}
|
||||||
|
segs.push(Segment::Text(TextSegment {
|
||||||
|
value: String::from(s),
|
||||||
|
style,
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
segs
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new fill segment
|
||||||
|
pub fn fill<T>(style: Option<Style>, value: T) -> Self
|
||||||
|
where
|
||||||
|
T: Into<String>,
|
||||||
|
{
|
||||||
|
Segment::Fill(FillSegment {
|
||||||
|
style,
|
||||||
|
value: value.into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn style(&self) -> Option<Style> {
|
||||||
|
match self {
|
||||||
|
Segment::Fill(fs) => fs.style,
|
||||||
|
Segment::Text(ts) => ts.style,
|
||||||
|
Segment::LineTerm => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_style_if_empty(&mut self, style: Option<Style>) {
|
||||||
|
match self {
|
||||||
|
Segment::Fill(fs) => {
|
||||||
|
if fs.style.is_none() {
|
||||||
|
fs.style = style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Segment::Text(ts) => {
|
||||||
|
if ts.style.is_none() {
|
||||||
|
ts.style = style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Segment::LineTerm => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn value(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Segment::Fill(fs) => &fs.value,
|
||||||
|
Segment::Text(ts) => &ts.value,
|
||||||
|
Segment::LineTerm => LINE_TERMINATOR_STRING,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the ANSIString of the segment value, not including its prefix and suffix
|
||||||
|
pub fn ansi_string(&self) -> ANSIString {
|
||||||
|
match self {
|
||||||
|
Segment::Fill(fs) => fs.ansi_string(None),
|
||||||
|
Segment::Text(ts) => ts.ansi_string(),
|
||||||
|
Segment::LineTerm => ANSIString::from(LINE_TERMINATOR_STRING),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn width_graphemes(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
Segment::Fill(fs) => fs.value.width_graphemes(),
|
||||||
|
Segment::Text(ts) => ts.value.width_graphemes(),
|
||||||
|
Segment::LineTerm => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const LINE_TERMINATOR: char = '\n';
|
||||||
|
const LINE_TERMINATOR_STRING: &str = "\n";
|
||||||
|
|
||||||
impl fmt::Display for Segment {
|
impl fmt::Display for Segment {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
write!(f, "{}", self.ansi_string())
|
write!(f, "{}", self.ansi_string())
|
||||||
|
|
Loading…
Reference in New Issue