refactor: Refactoring config (#383)

This PR refactors config and puts configuration files for all modules in `configs/`.
This commit is contained in:
Zhenhui Xie 2019-09-30 20:10:35 +08:00 committed by Matan Kushner
parent 9e9eb6a8ef
commit dd0b1a1aa2
48 changed files with 1290 additions and 946 deletions

7
.gitignore vendored
View File

@ -1,5 +1,5 @@
# will have compiled files and executables
/target/
target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
@ -15,6 +15,5 @@ Cargo.lock
.idea/
/*.iml
# Compiled files for documentation
docs/node_modules
docs/.vuepress/dist/
# Vim swap files
*.swp

442
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,49 +1,5 @@
[package]
name = "starship"
version = "0.19.0"
edition = "2018"
authors = ["Matan Kushner <hello@matchai.me>"]
homepage = "https://starship.rs"
documentation = "https://starship.rs/guide/"
repository = "https://github.com/starship/starship"
readme = "README.md"
license = "ISC"
keywords = ["prompt", "shell", "bash", "fish", "zsh"]
categories = ["command-line-utilities"]
description = """
The cross-shell prompt for astronauts. 🌌
"""
exclude = ["docs/**/*"]
[badges]
azure-devops = { project = "starship-control/starship", pipeline = "Starship Test Suite" }
is-it-maintained-issue-resolution = { repository = "starship/starship" }
is-it-maintained-open-issues = { repository = "starship/starship" }
maintenance = { status = "actively-developed" }
[features]
default = ["battery"]
[dependencies]
clap = "2.33.0"
ansi_term = "0.12.1"
dirs = "2.0.2"
git2 = { version = "0.10.1", default-features = false, features = [] }
toml = "0.5.3"
serde_json = "1.0.40"
rayon = "1.2.0"
pretty_env_logger = "0.3.1"
log = "0.4.8"
# battery is optional (on by default) because the crate doesn't currently build for Termux
# see: https://github.com/svartalf/rust-battery/issues/33
battery = { version = "0.7.4", optional = true }
path-slash = "0.1.1"
unicode-segmentation = "1.3.0"
gethostname = "0.2.0"
once_cell = "1.2.0"
chrono = "0.4"
sysinfo = "0.9.5"
byte-unit = "3.0.3"
[dev-dependencies]
tempfile = "3.1.0"
[workspace]
members = [
"starship",
"starship_module_config_derive",
]

View File

@ -1,473 +0,0 @@
use crate::utils;
use std::env;
use dirs::home_dir;
use toml::value::Table;
use toml::value::Value;
use ansi_term::Color;
pub trait Config {
fn initialize() -> Table;
fn config_from_file() -> Option<Table>;
fn get_module_config(&self, module_name: &str) -> Option<&Table>;
// Config accessor methods
fn get_as_bool(&self, key: &str) -> Option<bool>;
fn get_as_str(&self, key: &str) -> Option<&str>;
fn get_as_i64(&self, key: &str) -> Option<i64>;
fn get_as_array(&self, key: &str) -> Option<&Vec<Value>>;
fn get_as_ansi_style(&self, key: &str) -> Option<ansi_term::Style>;
fn get_as_segment_config(&self, key: &str) -> Option<SegmentConfig>;
// Internal implementation for accessors
fn get_config(&self, key: &str) -> Option<&Value>;
}
impl Config for Table {
/// Initialize the Config struct
fn initialize() -> Table {
if let Some(file_data) = Self::config_from_file() {
return file_data;
}
Self::new()
}
/// Create a config from a starship configuration file
fn config_from_file() -> Option<Table> {
let file_path = if let Ok(path) = env::var("STARSHIP_CONFIG") {
// Use $STARSHIP_CONFIG as the config path if available
log::debug!("STARSHIP_CONFIG is set: \n{}", &path);
path
} else {
// Default to using ~/.config/starship.toml
log::debug!("STARSHIP_CONFIG is not set");
let config_path = home_dir()?.join(".config/starship.toml");
let config_path_str = config_path.to_str()?.to_owned();
log::debug!("Using default config path: {}", config_path_str);
config_path_str
};
let toml_content = match utils::read_file(&file_path) {
Ok(content) => {
log::trace!("Config file content: \n{}", &content);
Some(content)
}
Err(e) => {
log::debug!("Unable to read config file content: \n{}", &e);
None
}
}?;
let config = toml::from_str(&toml_content).ok()?;
log::debug!("Config parsed: \n{:?}", &config);
Some(config)
}
/// Get the config value for a given key
fn get_config(&self, key: &str) -> Option<&Value> {
log::trace!("Looking for config key \"{}\"", key);
let value = self.get(key);
log_if_key_found(key, value);
value
}
/// Get the subset of the table for a module by its name
fn get_module_config(&self, key: &str) -> Option<&Table> {
log::trace!("Looking for module key \"{}\"", key);
let value = self.get(key);
log_if_key_found(key, value);
value.and_then(|value| {
let casted = Value::as_table(value);
log_if_type_correct(key, value, casted);
casted
})
}
/// Get a key from a module's configuration as a boolean
fn get_as_bool(&self, key: &str) -> Option<bool> {
log::trace!("Looking for boolean key \"{}\"", key);
let value = self.get(key);
log_if_key_found(key, value);
value.and_then(|value| {
let casted = Value::as_bool(value);
log_if_type_correct(key, value, casted);
casted
})
}
/// Get a key from a module's configuration as a string
fn get_as_str(&self, key: &str) -> Option<&str> {
log::trace!("Looking for string key \"{}\"", key);
let value = self.get(key);
log_if_key_found(key, value);
value.and_then(|value| {
let casted = Value::as_str(value);
log_if_type_correct(key, value, casted);
casted
})
}
/// Get a key from a module's configuration as an integer
fn get_as_i64(&self, key: &str) -> Option<i64> {
log::trace!("Looking for integer key \"{}\"", key);
let value = self.get(key);
log_if_key_found(key, value);
value.and_then(|value| {
let casted = Value::as_integer(value);
log_if_type_correct(key, value, casted);
casted
})
}
/// Get a key from a module's configuration as a vector
fn get_as_array(&self, key: &str) -> Option<&Vec<Value>> {
log::trace!("Looking for array key \"{}\"", key);
let value = self.get(key);
log_if_key_found(key, value);
value.and_then(|value| {
let casted = Value::as_array(value);
log_if_type_correct(key, value, casted);
casted
})
}
/// Get a text key and attempt to interpret it into an ANSI style.
fn get_as_ansi_style(&self, key: &str) -> Option<ansi_term::Style> {
// TODO: This should probably not unwrap to an empty new Style but inform the user about the problem
self.get_as_str(key)
.map(|x| parse_style_string(x).unwrap_or_default())
}
/// Get a key from a module's configuration as a segment config.
///
/// The config can be
///
/// - a string, will be interpreted as value.
/// - a table with optional { value, style } keys.
/// If omitted, default value will be used.
///
/// Returns `Some(SegmentConfig)` if key exists in the configuration, else `None`.
fn get_as_segment_config(&self, key: &str) -> Option<SegmentConfig> {
self.get_config(key).and_then(|segment_config: &Value| {
match segment_config {
toml::Value::String(value) => Some(SegmentConfig {
value: Some(value.as_str()),
style: None,
}),
toml::Value::Table(config_table) => Some(SegmentConfig {
value: config_table.get_as_str("value"),
style: config_table.get_as_ansi_style("style"),
}),
_ => {
log::debug!(
"Expected \"{}\" to be a string or config table. Instead received {} of type {}.",
key,
segment_config,
segment_config.type_str()
);
None
}
}
})
}
}
fn log_if_key_found(key: &str, something: Option<&Value>) {
if something.is_some() {
log::trace!("Value found for \"{}\": {:?}", key, &something);
} else {
log::trace!("No value found for \"{}\"", key);
}
}
fn log_if_type_correct<T: std::fmt::Debug>(
key: &str,
something: &Value,
casted_something: Option<T>,
) {
if let Some(casted) = casted_something {
log::trace!(
"Value under key \"{}\" has the expected type. Proceeding with {:?} which was build from {:?}.",
key,
casted,
something
);
} else {
log::debug!(
"Value under key \"{}\" did not have the expected type. Instead received {} of type {}.",
key,
something,
something.type_str()
);
}
}
/** Parse a style string which represents an ansi style. Valid tokens in the style
string include the following:
- 'fg:<color>' (specifies that the color read should be a foreground color)
- 'bg:<color>' (specifies that the color read should be a background color)
- 'underline'
- 'bold'
- 'italic'
- '<color>' (see the parse_color_string doc for valid color strings)
*/
fn parse_style_string(style_string: &str) -> Option<ansi_term::Style> {
style_string
.split_whitespace()
.fold(Some(ansi_term::Style::new()), |maybe_style, token| {
maybe_style.and_then(|style| {
let token = token.to_lowercase();
// Check for FG/BG identifiers and strip them off if appropriate
// If col_fg is true, color the foreground. If it's false, color the background.
let (token, col_fg) = if token.as_str().starts_with("fg:") {
(token.trim_start_matches("fg:").to_owned(), true)
} else if token.as_str().starts_with("bg:") {
(token.trim_start_matches("bg:").to_owned(), false)
} else {
(token, true) // Bare colors are assumed to color the foreground
};
match token.as_str() {
"underline" => Some(style.underline()),
"bold" => Some(style.bold()),
"italic" => Some(style.italic()),
"dimmed" => Some(style.dimmed()),
"none" => None,
// Try to see if this token parses as a valid color string
color_string => parse_color_string(color_string).map(|ansi_color| {
if col_fg {
style.fg(ansi_color)
} else {
style.on(ansi_color)
}
}),
}
})
})
}
/** Parse a string that represents a color setting, returning None if this fails
There are three valid color formats:
- #RRGGBB (a hash followed by an RGB hex)
- u8 (a number from 0-255, representing an ANSI color)
- colstring (one of the 16 predefined color strings)
*/
fn parse_color_string(color_string: &str) -> Option<ansi_term::Color> {
// Parse RGB hex values
log::trace!("Parsing color_string: {}", color_string);
if color_string.starts_with('#') {
log::trace!(
"Attempting to read hexadecimal color string: {}",
color_string
);
let r: u8 = u8::from_str_radix(&color_string[1..3], 16).ok()?;
let g: u8 = u8::from_str_radix(&color_string[3..5], 16).ok()?;
let b: u8 = u8::from_str_radix(&color_string[5..7], 16).ok()?;
log::trace!("Read RGB color string: {},{},{}", r, g, b);
return Some(Color::RGB(r, g, b));
}
// Parse a u8 (ansi color)
if let Result::Ok(ansi_color_num) = color_string.parse::<u8>() {
log::trace!("Read ANSI color string: {}", ansi_color_num);
return Some(Color::Fixed(ansi_color_num));
}
// Check for any predefined color strings
// There are no predefined enums for bright colors, so we use Color::Fixed
let predefined_color = match color_string.to_lowercase().as_str() {
"black" => Some(Color::Black),
"red" => Some(Color::Red),
"green" => Some(Color::Green),
"yellow" => Some(Color::Yellow),
"blue" => Some(Color::Blue),
"purple" => Some(Color::Purple),
"cyan" => Some(Color::Cyan),
"white" => Some(Color::White),
"bright-black" => Some(Color::Fixed(8)), // "bright-black" is dark grey
"bright-red" => Some(Color::Fixed(9)),
"bright-green" => Some(Color::Fixed(10)),
"bright-yellow" => Some(Color::Fixed(11)),
"bright-blue" => Some(Color::Fixed(12)),
"bright-purple" => Some(Color::Fixed(13)),
"bright-cyan" => Some(Color::Fixed(14)),
"bright-white" => Some(Color::Fixed(15)),
_ => None,
};
if predefined_color.is_some() {
log::trace!("Read predefined color: {}", color_string);
} else {
log::debug!("Could not parse color in string: {}", color_string);
}
predefined_color
}
pub struct SegmentConfig<'a> {
pub value: Option<&'a str>,
pub style: Option<ansi_term::Style>,
}
#[cfg(test)]
mod tests {
use super::*;
use ansi_term::Style;
#[test]
fn table_get_nonexisting() {
let table = toml::value::Table::new();
assert_eq!(table.get_as_bool("boolean"), None);
}
#[test]
fn table_get_config() {
let mut table = toml::value::Table::new();
table.insert(String::from("config"), Value::Boolean(true));
assert_eq!(table.get_config("config"), Some(&Value::Boolean(true)));
}
#[test]
fn table_get_as_bool() {
let mut table = toml::value::Table::new();
table.insert(String::from("boolean"), Value::Boolean(true));
assert_eq!(table.get_as_bool("boolean"), Some(true));
table.insert(String::from("string"), Value::String(String::from("true")));
assert_eq!(table.get_as_bool("string"), None);
}
#[test]
fn table_get_as_str() {
let mut table = toml::value::Table::new();
table.insert(String::from("string"), Value::String(String::from("hello")));
assert_eq!(table.get_as_str("string"), Some("hello"));
table.insert(String::from("boolean"), Value::Boolean(true));
assert_eq!(table.get_as_str("boolean"), None);
}
#[test]
fn table_get_as_i64() {
let mut table = toml::value::Table::new();
table.insert(String::from("integer"), Value::Integer(82));
assert_eq!(table.get_as_i64("integer"), Some(82));
table.insert(String::from("string"), Value::String(String::from("82")));
assert_eq!(table.get_as_bool("string"), None);
}
#[test]
fn table_get_as_array() {
let mut table = toml::value::Table::new();
table.insert(
String::from("array"),
Value::Array(vec![Value::Integer(1), Value::Integer(2)]),
);
assert_eq!(
table.get_as_array("array"),
Some(&vec![Value::Integer(1), Value::Integer(2)])
);
table.insert(String::from("string"), Value::String(String::from("82")));
assert_eq!(table.get_as_array("string"), None);
}
#[test]
fn table_get_styles_bold_italic_underline_green_dimmy_silly_caps() {
let mut table = toml::value::Table::new();
table.insert(
String::from("mystyle"),
Value::String(String::from("bOlD ItAlIc uNdErLiNe GrEeN dimmed")),
);
assert!(table.get_as_ansi_style("mystyle").unwrap().is_bold);
assert!(table.get_as_ansi_style("mystyle").unwrap().is_italic);
assert!(table.get_as_ansi_style("mystyle").unwrap().is_underline);
assert!(table.get_as_ansi_style("mystyle").unwrap().is_dimmed);
assert_eq!(
table.get_as_ansi_style("mystyle").unwrap(),
ansi_term::Style::new()
.bold()
.italic()
.underline()
.dimmed()
.fg(Color::Green)
);
}
#[test]
fn table_get_styles_plain_and_broken_styles() {
let mut table = toml::value::Table::new();
// Test a "plain" style with no formatting
table.insert(String::from("plainstyle"), Value::String(String::from("")));
assert_eq!(
table.get_as_ansi_style("plainstyle").unwrap(),
ansi_term::Style::new()
);
// Test a string that's clearly broken
table.insert(
String::from("broken"),
Value::String(String::from("djklgfhjkldhlhk;j")),
);
assert_eq!(
table.get_as_ansi_style("broken").unwrap(),
ansi_term::Style::new()
);
// Test a string that's nullified by `none`
table.insert(
String::from("nullified"),
Value::String(String::from("fg:red bg:green bold none")),
);
assert_eq!(
table.get_as_ansi_style("nullified").unwrap(),
ansi_term::Style::new()
);
// Test a string that's nullified by `none` at the start
table.insert(
String::from("nullified-start"),
Value::String(String::from("none fg:red bg:green bold")),
);
assert_eq!(
table.get_as_ansi_style("nullified-start").unwrap(),
ansi_term::Style::new()
);
}
#[test]
fn table_get_styles_ordered() {
let mut table = toml::value::Table::new();
// Test a background style with inverted order (also test hex + ANSI)
table.insert(
String::from("flipstyle"),
Value::String(String::from("bg:#050505 underline fg:120")),
);
assert_eq!(
table.get_as_ansi_style("flipstyle").unwrap(),
Style::new()
.underline()
.fg(Color::Fixed(120))
.on(Color::RGB(5, 5, 5))
);
// Test that the last color style is always the one used
table.insert(
String::from("multistyle"),
Value::String(String::from("bg:120 bg:125 bg:127 fg:127 122 125")),
);
assert_eq!(
table.get_as_ansi_style("multistyle").unwrap(),
Style::new().fg(Color::Fixed(125)).on(Color::Fixed(127))
);
}
}

View File

@ -1,115 +0,0 @@
use clap::ArgMatches;
use rayon::prelude::*;
use std::io::{self, Write};
use crate::config::Config;
use crate::context::Context;
use crate::module::Module;
use crate::module::ALL_MODULES;
use crate::modules;
// List of default prompt order
// NOTE: If this const value is changed then Default prompt order subheading inside
// prompt heading of config docs needs to be updated according to changes made here.
const DEFAULT_PROMPT_ORDER: &[&str] = &[
"username",
"hostname",
"directory",
"git_branch",
"git_state",
"git_status",
"package",
"nodejs",
"ruby",
"rust",
"python",
"golang",
"java",
"nix_shell",
"memory_usage",
"aws",
"env_var",
"cmd_duration",
"line_break",
"jobs",
#[cfg(feature = "battery")]
"battery",
"time",
"character",
];
pub fn prompt(args: ArgMatches) {
let context = Context::new(args);
let config = &context.config;
let stdout = io::stdout();
let mut handle = stdout.lock();
// Write a new line before the prompt
if config.get_as_bool("add_newline") != Some(false) {
writeln!(handle).unwrap();
}
let mut prompt_order: Vec<&str> = Vec::new();
// Write out a custom prompt order
if let Some(modules) = config.get_as_array("prompt_order") {
// if prompt_order = [] use default_prompt_order
if !modules.is_empty() {
for module in modules {
let str_value = module.as_str();
if let Some(value) = str_value {
if ALL_MODULES.contains(&value) {
prompt_order.push(value);
} else {
log::debug!(
"Expected prompt_order to contain value from {:?}. Instead received {}",
ALL_MODULES,
value,
);
}
} else {
log::debug!(
"Expected prompt_order to be an array of strings. Instead received {} of type {}",
module,
module.type_str()
);
}
}
} else {
prompt_order = DEFAULT_PROMPT_ORDER.to_vec();
}
} else {
prompt_order = DEFAULT_PROMPT_ORDER.to_vec();
}
let modules = &prompt_order
.par_iter()
.filter(|module| context.is_module_enabled(module))
.map(|module| modules::handle(module, &context)) // Compute modules
.flatten()
.collect::<Vec<Module>>(); // Remove segments set to `None`
let mut printable = modules.iter();
// Print the first module without its prefix
if let Some(first_module) = printable.next() {
let module_without_prefix = first_module.to_string_without_prefix();
write!(handle, "{}", module_without_prefix).unwrap()
}
// Print all remaining modules
printable.for_each(|module| write!(handle, "{}", module).unwrap());
}
pub fn module(module_name: &str, args: ArgMatches) {
let context = Context::new(args);
// If the module returns `None`, print an empty string
let module = modules::handle(module_name, &context)
.map(|m| m.to_string())
.unwrap_or_default();
print!("{}", module);
}

54
starship/Cargo.toml Normal file
View File

@ -0,0 +1,54 @@
[package]
name = "starship"
version = "0.20.0"
edition = "2018"
authors = ["Matan Kushner <hello@matchai.me>"]
homepage = "https://starship.rs"
documentation = "https://starship.rs/guide/"
repository = "https://github.com/starship/starship"
readme = "README.md"
license = "ISC"
keywords = ["prompt", "shell", "bash", "fish", "zsh"]
categories = ["command-line-utilities"]
description = """
The cross-shell prompt for astronauts. 🌌
"""
exclude = ["docs/**/*"]
[badges]
azure-devops = { project = "starship-control/starship", pipeline = "Starship Test Suite" }
is-it-maintained-issue-resolution = { repository = "starship/starship" }
is-it-maintained-open-issues = { repository = "starship/starship" }
maintenance = { status = "actively-developed" }
[features]
default = ["battery"]
[dependencies]
clap = "2.33.0"
ansi_term = "0.12.1"
dirs = "2.0.2"
git2 = { version = "0.10.1", default-features = false, features = [] }
toml = "0.5.3"
serde_json = "1.0.40"
rayon = "1.2.0"
pretty_env_logger = "0.3.1"
log = "0.4.8"
# battery is optional (on by default) because the crate doesn't currently build for Termux
# see: https://github.com/svartalf/rust-battery/issues/33
battery = { version = "0.7.4", optional = true }
path-slash = "0.1.1"
unicode-segmentation = "1.3.0"
gethostname = "0.2.0"
once_cell = "1.2.0"
chrono = "0.4"
sysinfo = "0.9.5"
byte-unit = "3.0.3"
starship_module_config_derive = { version = "0.20", path = "../starship_module_config_derive" }
[dev-dependencies]
tempfile = "3.1.0"
[[bin]]
name = "starship"
path = "src/main.rs"

590
starship/src/config.rs Normal file
View File

@ -0,0 +1,590 @@
#![allow(dead_code)]
use crate::configs::StarshipRootConfig;
use crate::utils;
use ansi_term::{Color, Style};
use std::clone::Clone;
use std::marker::Sized;
use dirs::home_dir;
use std::env;
use toml::Value;
/// Root config of a module.
pub trait RootModuleConfig<'a>
where
Self: ModuleConfig<'a>,
{
/// Create a new root module config with default values.
fn new() -> Self;
/// Load root module config from given Value and fill unset variables with default
/// values.
fn load(config: &'a Value) -> Self {
Self::new().load_config(config)
}
/// Helper function that will call RootModuleConfig::load(config) if config is Some,
/// or RootModuleConfig::new() if config is None.
fn try_load(config: Option<&'a Value>) -> Self {
if let Some(config) = config {
Self::load(config)
} else {
Self::new()
}
}
}
/// Parsable config.
pub trait ModuleConfig<'a>
where
Self: Sized + Clone,
{
/// Construct a `ModuleConfig` from a toml value.
fn from_config(_config: &'a Value) -> Option<Self> {
None
}
/// Merge `self` with config from a toml table.
fn load_config(&self, config: &'a Value) -> Self {
Self::from_config(config).unwrap_or_else(|| self.clone())
}
}
// TODO: Add logging to default implementations
impl<'a> ModuleConfig<'a> for &'a str {
fn from_config(config: &'a Value) -> Option<Self> {
config.as_str()
}
}
impl<'a> ModuleConfig<'a> for Style {
fn from_config(config: &Value) -> Option<Self> {
parse_style_string(config.as_str()?)
}
}
impl<'a> ModuleConfig<'a> for bool {
fn from_config(config: &Value) -> Option<Self> {
config.as_bool()
}
}
impl<'a> ModuleConfig<'a> for i64 {
fn from_config(config: &Value) -> Option<Self> {
config.as_integer()
}
}
impl<'a> ModuleConfig<'a> for f64 {
fn from_config(config: &Value) -> Option<Self> {
config.as_float()
}
}
impl<'a, T> ModuleConfig<'a> for Vec<T>
where
T: ModuleConfig<'a>,
{
fn from_config(config: &'a Value) -> Option<Self> {
config
.as_array()?
.iter()
.map(|value| T::from_config(value))
.collect()
}
}
impl<'a, T> ModuleConfig<'a> for Option<T>
where
T: ModuleConfig<'a> + Sized,
{
fn from_config(config: &'a Value) -> Option<Self> {
Some(T::from_config(config))
}
}
/// Root config of starship.
pub struct StarshipConfig {
pub config: Option<Value>,
}
impl StarshipConfig {
/// Initialize the Config struct
pub fn initialize() -> Self {
if let Some(file_data) = Self::config_from_file() {
StarshipConfig {
config: Some(file_data),
}
} else {
StarshipConfig {
config: Some(Value::Table(toml::value::Table::new())),
}
}
}
/// Create a config from a starship configuration file
fn config_from_file() -> Option<Value> {
let file_path = if let Ok(path) = env::var("STARSHIP_CONFIG") {
// Use $STARSHIP_CONFIG as the config path if available
log::debug!("STARSHIP_CONFIG is set: \n{}", &path);
path
} else {
// Default to using ~/.config/starship.toml
log::debug!("STARSHIP_CONFIG is not set");
let config_path = home_dir()?.join(".config/starship.toml");
let config_path_str = config_path.to_str()?.to_owned();
log::debug!("Using default config path: {}", config_path_str);
config_path_str
};
let toml_content = match utils::read_file(&file_path) {
Ok(content) => {
log::trace!("Config file content: \n{}", &content);
Some(content)
}
Err(e) => {
log::debug!("Unable to read config file content: \n{}", &e);
None
}
}?;
let config = toml::from_str(&toml_content).ok()?;
log::debug!("Config parsed: \n{:?}", &config);
Some(config)
}
/// Get the subset of the table for a module by its name
pub fn get_module_config(&self, module_name: &str) -> Option<&Value> {
let module_config = self.config.as_ref()?.as_table()?.get(module_name);
if module_config.is_some() {
log::debug!(
"Config found for \"{}\": \n{:?}",
&module_name,
&module_config
);
} else {
log::trace!("No config found for \"{}\"", &module_name);
}
module_config
}
pub fn get_root_config(&self) -> StarshipRootConfig {
if let Some(root_config) = &self.config {
StarshipRootConfig::load(root_config)
} else {
StarshipRootConfig::new()
}
}
}
#[derive(Clone)]
pub struct SegmentConfig<'a> {
pub value: &'a str,
pub style: Option<Style>,
}
impl<'a> ModuleConfig<'a> for SegmentConfig<'a> {
fn from_config(config: &'a Value) -> Option<Self> {
match config {
Value::String(ref config_str) => Some(Self {
value: config_str,
style: None,
}),
Value::Table(ref config_table) => Some(Self {
value: config_table.get("value")?.as_str()?,
style: config_table.get("style").and_then(<Style>::from_config),
}),
_ => None,
}
}
fn load_config(&self, config: &'a Value) -> Self {
let mut new_config = self.clone();
match config {
Value::String(ref config_str) => {
new_config.value = config_str;
}
Value::Table(ref config_table) => {
if let Some(Value::String(value)) = config_table.get("value") {
new_config.value = value;
};
if let Some(style) = config_table.get("style") {
new_config.style = <Style>::from_config(style);
};
}
_ => {}
};
new_config
}
}
impl<'a> SegmentConfig<'a> {
/// Mutably set value
pub fn set_value(&mut self, value: &'a str) {
self.value = value;
}
/// Mutably set style
pub fn set_style(&mut self, style: Style) {
self.style = Some(style);
}
/// Immutably set value
pub fn with_value(&self, value: &'a str) -> Self {
Self {
value,
style: self.style,
}
}
/// Immutably set style
pub fn with_style(&self, style: Style) -> Self {
Self {
value: self.value,
style: Some(style),
}
}
}
/** Parse a style string which represents an ansi style. Valid tokens in the style
string include the following:
- 'fg:<color>' (specifies that the color read should be a foreground color)
- 'bg:<color>' (specifies that the color read should be a background color)
- 'underline'
- 'bold'
- 'italic'
- '<color>' (see the parse_color_string doc for valid color strings)
*/
fn parse_style_string(style_string: &str) -> Option<ansi_term::Style> {
style_string
.split_whitespace()
.fold(Some(ansi_term::Style::new()), |maybe_style, token| {
maybe_style.and_then(|style| {
let token = token.to_lowercase();
// Check for FG/BG identifiers and strip them off if appropriate
// If col_fg is true, color the foreground. If it's false, color the background.
let (token, col_fg) = if token.as_str().starts_with("fg:") {
(token.trim_start_matches("fg:").to_owned(), true)
} else if token.as_str().starts_with("bg:") {
(token.trim_start_matches("bg:").to_owned(), false)
} else {
(token, true) // Bare colors are assumed to color the foreground
};
match token.as_str() {
"underline" => Some(style.underline()),
"bold" => Some(style.bold()),
"italic" => Some(style.italic()),
"dimmed" => Some(style.dimmed()),
"none" => None,
// Try to see if this token parses as a valid color string
color_string => parse_color_string(color_string).map(|ansi_color| {
if col_fg {
style.fg(ansi_color)
} else {
style.on(ansi_color)
}
}),
}
})
})
}
/** Parse a string that represents a color setting, returning None if this fails
There are three valid color formats:
- #RRGGBB (a hash followed by an RGB hex)
- u8 (a number from 0-255, representing an ANSI color)
- colstring (one of the 16 predefined color strings)
*/
fn parse_color_string(color_string: &str) -> Option<ansi_term::Color> {
// Parse RGB hex values
log::trace!("Parsing color_string: {}", color_string);
if color_string.starts_with('#') {
log::trace!(
"Attempting to read hexadecimal color string: {}",
color_string
);
let r: u8 = u8::from_str_radix(&color_string[1..3], 16).ok()?;
let g: u8 = u8::from_str_radix(&color_string[3..5], 16).ok()?;
let b: u8 = u8::from_str_radix(&color_string[5..7], 16).ok()?;
log::trace!("Read RGB color string: {},{},{}", r, g, b);
return Some(Color::RGB(r, g, b));
}
// Parse a u8 (ansi color)
if let Result::Ok(ansi_color_num) = color_string.parse::<u8>() {
log::trace!("Read ANSI color string: {}", ansi_color_num);
return Some(Color::Fixed(ansi_color_num));
}
// Check for any predefined color strings
// There are no predefined enums for bright colors, so we use Color::Fixed
let predefined_color = match color_string.to_lowercase().as_str() {
"black" => Some(Color::Black),
"red" => Some(Color::Red),
"green" => Some(Color::Green),
"yellow" => Some(Color::Yellow),
"blue" => Some(Color::Blue),
"purple" => Some(Color::Purple),
"cyan" => Some(Color::Cyan),
"white" => Some(Color::White),
"bright-black" => Some(Color::Fixed(8)), // "bright-black" is dark grey
"bright-red" => Some(Color::Fixed(9)),
"bright-green" => Some(Color::Fixed(10)),
"bright-yellow" => Some(Color::Fixed(11)),
"bright-blue" => Some(Color::Fixed(12)),
"bright-purple" => Some(Color::Fixed(13)),
"bright-cyan" => Some(Color::Fixed(14)),
"bright-white" => Some(Color::Fixed(15)),
_ => None,
};
if predefined_color.is_some() {
log::trace!("Read predefined color: {}", color_string);
} else {
log::debug!("Could not parse color in string: {}", color_string);
}
predefined_color
}
#[cfg(test)]
mod tests {
use super::*;
use starship_module_config_derive::ModuleConfig;
use toml;
#[test]
fn test_load_config() {
#[derive(Clone, ModuleConfig)]
struct TestConfig<'a> {
pub symbol: &'a str,
pub disabled: bool,
pub some_array: Vec<&'a str>,
}
let config = toml::toml! {
symbol = "T "
disabled = true
some_array = ["A"]
};
let default_config = TestConfig {
symbol: "S ",
disabled: false,
some_array: vec!["A", "B", "C"],
};
let rust_config = default_config.load_config(&config);
assert_eq!(rust_config.symbol, "T ");
assert_eq!(rust_config.disabled, true);
assert_eq!(rust_config.some_array, vec!["A"]);
}
#[test]
fn test_load_nested_config() {
#[derive(Clone, ModuleConfig)]
struct TestConfig<'a> {
pub untracked: SegmentDisplayConfig<'a>,
pub modified: SegmentDisplayConfig<'a>,
}
#[derive(PartialEq, Debug, Clone, ModuleConfig)]
struct SegmentDisplayConfig<'a> {
pub value: &'a str,
pub style: Style,
}
let config = toml::toml! {
untracked.value = "x"
modified = { value = "", style = "red" }
};
let default_config = TestConfig {
untracked: SegmentDisplayConfig {
value: "?",
style: Color::Red.bold(),
},
modified: SegmentDisplayConfig {
value: "!",
style: Color::Red.bold(),
},
};
let git_status_config = default_config.load_config(&config);
assert_eq!(
git_status_config.untracked,
SegmentDisplayConfig {
value: "x",
style: Color::Red.bold(),
}
);
assert_eq!(
git_status_config.modified,
SegmentDisplayConfig {
value: "",
style: Color::Red.normal(),
}
);
}
#[test]
fn test_load_optional_config() {
#[derive(Clone, ModuleConfig)]
struct TestConfig<'a> {
pub optional: Option<&'a str>,
pub hidden: Option<&'a str>,
}
let config = toml::toml! {
optional = "test"
};
let default_config = TestConfig {
optional: None,
hidden: None,
};
let rust_config = default_config.load_config(&config);
assert_eq!(rust_config.optional, Some("test"));
assert_eq!(rust_config.hidden, None);
}
#[test]
fn test_load_enum_config() {
#[derive(Clone, ModuleConfig)]
struct TestConfig {
pub switch_a: Switch,
pub switch_b: Switch,
pub switch_c: Switch,
}
#[derive(Debug, PartialEq, Clone)]
enum Switch {
ON,
OFF,
}
impl<'a> ModuleConfig<'a> for Switch {
fn from_config(config: &'a Value) -> Option<Self> {
match config.as_str()? {
"on" => Some(Self::ON),
"off" => Some(Self::OFF),
_ => None,
}
}
}
let config = toml::toml! {
switch_a = "on"
switch_b = "any"
};
let default_config = TestConfig {
switch_a: Switch::OFF,
switch_b: Switch::OFF,
switch_c: Switch::OFF,
};
let rust_config = default_config.load_config(&config);
assert_eq!(rust_config.switch_a, Switch::ON);
assert_eq!(rust_config.switch_b, Switch::OFF);
assert_eq!(rust_config.switch_c, Switch::OFF);
}
#[test]
fn test_from_string() {
let config = Value::String(String::from("S"));
assert_eq!(<&str>::from_config(&config).unwrap(), "S");
}
#[test]
fn test_from_bool() {
let config = Value::Boolean(true);
assert_eq!(<bool>::from_config(&config).unwrap(), true);
}
#[test]
fn test_from_i64() {
let config = Value::Integer(42);
assert_eq!(<i64>::from_config(&config).unwrap(), 42);
}
#[test]
fn test_from_style() {
let config = Value::from("red bold");
assert_eq!(<Style>::from_config(&config).unwrap(), Color::Red.bold());
}
#[test]
fn test_from_vec() {
let config: Value = Value::Array(vec![Value::from("S")]);
assert_eq!(<Vec<&str>>::from_config(&config).unwrap(), vec!["S"]);
}
#[test]
fn test_from_option() {
let config: Value = Value::String(String::from("S"));
assert_eq!(<Option<&str>>::from_config(&config).unwrap(), Some("S"));
}
#[test]
fn table_get_styles_bold_italic_underline_green_dimmy_silly_caps() {
let config = Value::from("bOlD ItAlIc uNdErLiNe GrEeN diMMeD");
let mystyle = <Style>::from_config(&config).unwrap();
assert!(mystyle.is_bold);
assert!(mystyle.is_italic);
assert!(mystyle.is_underline);
assert!(mystyle.is_dimmed);
assert_eq!(
mystyle,
ansi_term::Style::new()
.bold()
.italic()
.underline()
.dimmed()
.fg(Color::Green)
);
}
#[test]
fn table_get_styles_plain_and_broken_styles() {
// Test a "plain" style with no formatting
let config = Value::from("");
let plain_style = <Style>::from_config(&config).unwrap();
assert_eq!(plain_style, ansi_term::Style::new());
// Test a string that's clearly broken
let config = Value::from("djklgfhjkldhlhk;j");
assert!(<Style>::from_config(&config).is_none());
// Test a string that's nullified by `none`
let config = Value::from("fg:red bg:green bold none");
assert!(<Style>::from_config(&config).is_none());
// Test a string that's nullified by `none` at the start
let config = Value::from("none fg:red bg:green bold");
assert!(<Style>::from_config(&config).is_none());
}
#[test]
fn table_get_styles_ordered() {
// Test a background style with inverted order (also test hex + ANSI)
let config = Value::from("bg:#050505 underline fg:120");
let flipped_style = <Style>::from_config(&config).unwrap();
assert_eq!(
flipped_style,
Style::new()
.underline()
.fg(Color::Fixed(120))
.on(Color::RGB(5, 5, 5))
);
// Test that the last color style is always the one used
let config = Value::from("bg:120 bg:125 bg:127 fg:127 122 125");
let multi_style = <Style>::from_config(&config).unwrap();
assert_eq!(
multi_style,
Style::new().fg(Color::Fixed(125)).on(Color::Fixed(127))
);
}
}

View File

@ -0,0 +1,38 @@
use crate::config::{ModuleConfig, RootModuleConfig};
use ansi_term::{Color, Style};
use starship_module_config_derive::ModuleConfig;
#[derive(Clone, ModuleConfig)]
pub struct BatteryConfig<'a> {
pub full_symbol: &'a str,
pub charging_symbol: &'a str,
pub discharging_symbol: &'a str,
pub unknown_symbol: Option<&'a str>,
pub empty_symbol: Option<&'a str>,
pub display: Vec<BatteryDisplayConfig>,
pub disabled: bool,
}
impl<'a> RootModuleConfig<'a> for BatteryConfig<'a> {
fn new() -> Self {
BatteryConfig {
full_symbol: "",
charging_symbol: "",
discharging_symbol: "",
unknown_symbol: None,
empty_symbol: None,
display: vec![BatteryDisplayConfig {
threshold: 10,
style: Color::Red.bold(),
}],
disabled: false,
}
}
}
#[derive(Clone, ModuleConfig)]
pub struct BatteryDisplayConfig {
pub threshold: i64,
pub style: Style,
}

View File

@ -0,0 +1,49 @@
pub mod battery;
pub mod rust;
use crate::config::{ModuleConfig, RootModuleConfig};
use starship_module_config_derive::ModuleConfig;
#[derive(Clone, ModuleConfig)]
pub struct StarshipRootConfig<'a> {
pub add_newline: bool,
pub prompt_order: Vec<&'a str>,
}
impl<'a> RootModuleConfig<'a> for StarshipRootConfig<'a> {
fn new() -> Self {
StarshipRootConfig {
add_newline: true,
// List of default prompt order
// NOTE: If this const value is changed then Default prompt order subheading inside
// prompt heading of config docs needs to be updated according to changes made here.
prompt_order: vec![
"username",
"hostname",
"directory",
"git_branch",
"git_state",
"git_status",
"package",
"nodejs",
"ruby",
"rust",
"python",
"golang",
"java",
"nix_shell",
"memory_usage",
"aws",
"env_var",
"cmd_duration",
"line_break",
"jobs",
#[cfg(feature = "battery")]
"battery",
"time",
"character",
],
}
}
}

View File

@ -0,0 +1,57 @@
use crate::config::{ModuleConfig, RootModuleConfig, SegmentConfig};
use ansi_term::{Color, Style};
use starship_module_config_derive::ModuleConfig;
#[derive(Clone, ModuleConfig)]
pub struct RustConfig<'a> {
pub symbol: SegmentConfig<'a>,
pub version: SegmentConfig<'a>,
pub style: Style,
pub disabled: bool,
}
/* This is what the macro adds.
impl<'a> ModuleConfig<'a> for RustConfig<'a> {
fn load_config(&self, config: &'a toml::Value) -> Self {
let mut new_module_config = self.clone();
if let toml::Value::Table(config) = config {
if let Some(config_str) = config.get("symbol") {
new_module_config.symbol = new_module_config.symbol.load_config(config_str);
}
if let Some(config_str) = config.get("disabled") {
new_module_config.disabled = new_module_config.disabled.load_config(config_str);
}
if let Some(config_str) = config.get("style") {
new_module_config.style = new_module_config.style.load_config(config_str);
}
}
new_module_config
}
fn from_config(config: &'a toml::Value) -> Option<Self> {
let config = config.as_table()?;
Some(RustConfig {
symbol: <&'a str>::from_config(config.get("symbol")?)?,
style: <Style>::from_config(config.get("style")?)?,
disabled: <bool>::from_config(config.get("disabled")?)?,
})
}
}
*/
impl<'a> RootModuleConfig<'a> for RustConfig<'a> {
fn new() -> Self {
RustConfig {
symbol: SegmentConfig {
value: "🦀 ",
style: None,
},
version: SegmentConfig {
value: "",
style: None,
},
style: Color::Red.bold(),
disabled: false,
}
}
}

View File

@ -1,4 +1,4 @@
use crate::config::Config;
use crate::config::StarshipConfig;
use crate::module::Module;
use clap::ArgMatches;
@ -14,7 +14,7 @@ use std::path::{Path, PathBuf};
/// of the prompt.
pub struct Context<'a> {
/// The deserialized configuration map from the user's `starship.toml` file.
pub config: toml::value::Table,
pub config: StarshipConfig,
/// The current working directory that starship is being called in.
pub current_dir: PathBuf,
@ -47,7 +47,7 @@ impl<'a> Context<'a> {
where
T: Into<PathBuf>,
{
let config = toml::value::Table::initialize();
let config = StarshipConfig::initialize();
// TODO: Currently gets the physical directory. Get the logical directory.
let current_dir = Context::expand_tilde(dir.into());
@ -82,7 +82,7 @@ impl<'a> Context<'a> {
let config = self.config.get_module_config(name);
// If the segment has "disabled" set to "true", don't show it
let disabled = config.and_then(|table| table.get_as_bool("disabled"));
let disabled = config.and_then(|table| table.as_table()?.get("disabled")?.as_bool());
disabled != Some(true)
}

View File

@ -1,5 +1,6 @@
// Lib is present to allow for benchmarking
mod config;
pub mod config;
pub mod configs;
pub mod context;
pub mod module;
pub mod modules;

View File

@ -2,6 +2,7 @@
extern crate clap;
mod config;
mod configs;
mod context;
mod init;
mod module;

View File

@ -1,5 +1,4 @@
use crate::config::Config;
use crate::config::SegmentConfig;
use crate::config::{ModuleConfig, SegmentConfig};
use crate::segment::Segment;
use ansi_term::Style;
use ansi_term::{ANSIString, ANSIStrings};
@ -37,7 +36,7 @@ pub const ALL_MODULES: &[&str] = &[
/// (e.g. The git module shows the current git branch and status)
pub struct Module<'a> {
/// The module's configuration map if available
config: Option<&'a toml::value::Table>,
pub config: Option<&'a toml::Value>,
/// The module's name, to be used in configuration and logging.
_name: String,
@ -57,7 +56,7 @@ pub struct Module<'a> {
impl<'a> Module<'a> {
/// Creates a module with no segments.
pub fn new(name: &str, config: Option<&'a toml::value::Table>) -> Module<'a> {
pub fn new(name: &str, config: Option<&'a toml::Value>) -> Module<'a> {
Module {
config,
_name: name.to_string(),
@ -69,22 +68,42 @@ impl<'a> Module<'a> {
}
/// Get a reference to a newly created segment in the module
#[deprecated(
since = "0.20.0",
note = "please use `module.create_segment()` instead"
)]
pub fn new_segment(&mut self, name: &str, value: &str) -> &mut Segment {
let mut segment = Segment::new(name);
if let Some(segment_config) = self.config_value_segment_config(name) {
let segment_config_mock = SegmentConfig { value, style: None };
if let Some(module_config) = self.config {
let segment_config = segment_config_mock.load_config(&module_config);
segment.set_style(segment_config.style.unwrap_or(self.style));
segment.set_value(segment_config.value.unwrap_or(value));
segment.set_value(segment_config.value);
} else {
segment.set_style(self.style);
// Use the provided value unless overwritten by config
segment.set_value(self.config_value_str(name).unwrap_or(value));
segment.set_style(segment_config_mock.style.unwrap_or(self.style));
segment.set_value(segment_config_mock.value);
}
self.segments.push(segment);
self.segments.last_mut().unwrap()
}
/// Get a reference to a newly created segment in the module
pub fn create_segment(&mut self, name: &str, segment_config: &SegmentConfig) -> &mut Segment {
let mut segment = Segment::new(name);
segment.set_style(segment_config.style.unwrap_or(self.style));
segment.set_value(segment_config.value);
self.segments.push(segment);
self.segments.last_mut().unwrap()
}
/// Should config exists, get a reference to a newly created segment in the module
#[deprecated(
since = "0.20.0",
note = "please use `module.create_segment()` instead"
)]
pub fn new_segment_if_config_exists(&mut self, name: &str) -> Option<&mut Segment> {
// Use the provided value unless overwritten by config
if let Some(value) = self.config_value_str(name) {
@ -151,34 +170,48 @@ impl<'a> Module<'a> {
}
/// Get a module's config value as a string
#[deprecated(
since = "0.20.0",
note = "please use <RootModuleConfig>::try_load(module.config) instead"
)]
pub fn config_value_str(&self, key: &str) -> Option<&str> {
self.config.and_then(|config| config.get_as_str(key))
<&str>::from_config(self.config?.as_table()?.get(key)?)
}
/// Get a module's config value as an int
#[deprecated(
since = "0.20.0",
note = "please use <RootModuleConfig>::try_load(module.config) instead"
)]
pub fn config_value_i64(&self, key: &str) -> Option<i64> {
self.config.and_then(|config| config.get_as_i64(key))
<i64>::from_config(self.config?.as_table()?.get(key)?)
}
/// Get a module's config value as a bool
#[deprecated(
since = "0.20.0",
note = "please use <RootModuleConfig>::try_load(module.config) instead"
)]
pub fn config_value_bool(&self, key: &str) -> Option<bool> {
self.config.and_then(|config| config.get_as_bool(key))
<bool>::from_config(self.config?.as_table()?.get(key)?)
}
/// Get a module's config value as a style
#[deprecated(
since = "0.20.0",
note = "please use <RootModuleConfig>::try_load(module.config) instead"
)]
pub fn config_value_style(&self, key: &str) -> Option<Style> {
self.config.and_then(|config| config.get_as_ansi_style(key))
}
/// Get a module's config value as an array
pub fn config_value_array(&self, key: &str) -> Option<&Vec<toml::Value>> {
self.config.and_then(|config| config.get_as_array(key))
<Style>::from_config(self.config?.as_table()?.get(key)?)
}
/// Get a module's config value as a table of segment config
#[deprecated(
since = "0.20.0",
note = "please use <RootModuleConfig>::try_load(module.config) instead"
)]
pub fn config_value_segment_config(&self, key: &str) -> Option<SegmentConfig> {
self.config
.and_then(|config| config.get_as_segment_config(key))
<SegmentConfig>::from_config(self.config?.as_table()?.get(key)?)
}
}

View File

@ -1,13 +1,9 @@
use ansi_term::{Color, Style};
use super::{Context, Module};
use crate::config::Config;
use crate::config::RootModuleConfig;
use crate::configs::battery::BatteryConfig;
/// Creates a module for the battery percentage and charging state
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
const BATTERY_FULL: &str = "";
const BATTERY_CHARGING: &str = "";
const BATTERY_DISCHARGING: &str = "";
// TODO: Update when v1.0 printing refactor is implemented to only
// print escapes in a prompt context.
let shell = std::env::var("STARSHIP_SHELL").unwrap_or_default();
@ -20,37 +16,39 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
let BatteryStatus { state, percentage } = battery_status;
let mut module = context.new_module("battery");
let battery_config = BatteryConfig::try_load(module.config);
// Parse config under `display`
let display_styles = get_display_styles(&module);
let display_style = display_styles.iter().find(|display_style| {
let BatteryDisplayStyle { threshold, .. } = display_style;
percentage <= *threshold as f32
});
let display_styles = &battery_config.display;
let display_style = display_styles
.iter()
.find(|display_style| percentage <= display_style.threshold as f32);
if let Some(display_style) = display_style {
let BatteryDisplayStyle { style, .. } = display_style;
// Set style based on percentage
module.set_style(*style);
module.set_style(display_style.style);
module.get_prefix().set_value("");
match state {
battery::State::Full => {
module.new_segment("full_symbol", BATTERY_FULL);
module.new_segment("full_symbol", battery_config.full_symbol);
}
battery::State::Charging => {
module.new_segment("charging_symbol", BATTERY_CHARGING);
module.new_segment("charging_symbol", battery_config.charging_symbol);
}
battery::State::Discharging => {
module.new_segment("discharging_symbol", BATTERY_DISCHARGING);
module.new_segment("discharging_symbol", battery_config.discharging_symbol);
}
battery::State::Unknown => {
log::debug!("Unknown detected");
module.new_segment_if_config_exists("unknown_symbol")?;
if let Some(unknown_symbol) = battery_config.unknown_symbol {
module.new_segment("unknown_symbol", unknown_symbol);
}
}
battery::State::Empty => {
module.new_segment_if_config_exists("empty_symbol")?;
if let Some(empty_symbol) = battery_config.empty_symbol {
module.new_segment("empty_symbol", empty_symbol);
}
}
_ => {
log::debug!("Unhandled battery state `{}`", state);
@ -70,28 +68,6 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
}
}
fn get_display_styles(module: &Module) -> Vec<BatteryDisplayStyle> {
if let Some(display_configs) = module.config_value_array("display") {
let mut display_styles: Vec<BatteryDisplayStyle> = vec![];
for display_config in display_configs.iter() {
if let toml::Value::Table(config) = display_config {
if let Some(display_style) = BatteryDisplayStyle::from_config(config) {
display_styles.push(display_style);
}
}
}
// Return display styles as long as display array exists, even if it is empty.
display_styles
} else {
// Default display styles: [{ threshold = 10, style = "red bold" }]
vec![BatteryDisplayStyle {
threshold: 10,
style: Color::Red.bold(),
}]
}
}
fn get_battery_status() -> Option<BatteryStatus> {
let battery_manager = battery::Manager::new().ok()?;
match battery_manager.batteries().ok()?.next() {
@ -119,19 +95,3 @@ struct BatteryStatus {
percentage: f32,
state: battery::State,
}
#[derive(Clone, Debug)]
struct BatteryDisplayStyle {
threshold: i64,
style: Style,
}
impl BatteryDisplayStyle {
/// construct battery display style from toml table
pub fn from_config(config: &toml::value::Table) -> Option<BatteryDisplayStyle> {
let threshold = config.get_as_i64("threshold")?;
let style = config.get_as_ansi_style("style")?;
Some(BatteryDisplayStyle { threshold, style })
}
}

View File

@ -1,4 +1,3 @@
use ansi_term::Color;
use std::ffi::OsStr;
use std::path::Path;
use std::process::{Command, Output};
@ -6,14 +5,15 @@ use std::{env, fs};
use super::{Context, Module};
use crate::config::RootModuleConfig;
use crate::configs::rust::RustConfig;
/// Creates a module with the current Rust version
///
/// Will display the Rust version if any of the following criteria are met:
/// - Current directory contains a file with a `.rs` extension
/// - Current directory contains a `Cargo.toml` file
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
const RUST_CHAR: &str = "🦀 ";
let is_rs_project = context
.try_begin_scan()?
.set_files(&["Cargo.toml"])
@ -59,13 +59,11 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
};
let mut module = context.new_module("rust");
let config = RustConfig::try_load(module.config);
module.set_style(config.style);
let module_style = module
.config_value_style("style")
.unwrap_or_else(|| Color::Red.bold());
module.set_style(module_style);
module.new_segment("symbol", RUST_CHAR);
module.new_segment("version", &module_version);
module.create_segment("symbol", &config.symbol);
module.create_segment("version", &config.version.with_value(&module_version));
Some(module)
}

65
starship/src/print.rs Normal file
View File

@ -0,0 +1,65 @@
use clap::ArgMatches;
use rayon::prelude::*;
use std::io::{self, Write};
use crate::context::Context;
use crate::module::Module;
use crate::module::ALL_MODULES;
use crate::modules;
pub fn prompt(args: ArgMatches) {
let context = Context::new(args);
let config = context.config.get_root_config();
let stdout = io::stdout();
let mut handle = stdout.lock();
// Write a new line before the prompt
if config.add_newline {
writeln!(handle).unwrap();
}
let mut prompt_order: Vec<&str> = Vec::new();
// Write out a custom prompt order
for module in config.prompt_order {
if ALL_MODULES.contains(&module) {
prompt_order.push(module);
} else {
log::debug!(
"Expected prompt_order to contain value from {:?}. Instead received {}",
ALL_MODULES,
module,
);
}
}
let modules = &prompt_order
.par_iter()
.filter(|module| context.is_module_enabled(module))
.map(|module| modules::handle(module, &context)) // Compute modules
.flatten()
.collect::<Vec<Module>>(); // Remove segments set to `None`
let mut printable = modules.iter();
// Print the first module without its prefix
if let Some(first_module) = printable.next() {
let module_without_prefix = first_module.to_string_without_prefix();
write!(handle, "{}", module_without_prefix).unwrap()
}
// Print all remaining modules
printable.for_each(|module| write!(handle, "{}", module).unwrap());
}
pub fn module(module_name: &str, args: ArgMatches) {
let context = Context::new(args);
// If the module returns `None`, print an empty string
let module = modules::handle(module_name, &context)
.map(|m| m.to_string())
.unwrap_or_default();
print!("{}", module);
}

View File

@ -0,0 +1,28 @@
[package]
name = "starship_module_config_derive"
version = "0.20.0"
edition = "2018"
authors = ["Matan Kushner <hello@matchai.me>"]
homepage = "https://starship.rs"
documentation = "https://starship.rs/guide/"
repository = "https://github.com/starship/starship"
readme = "README.md"
license = "ISC"
keywords = ["prompt", "shell", "bash", "fish", "zsh"]
categories = ["command-line-utilities"]
description = """
The cross-shell prompt for astronauts. 🌌
"""
exclude = ["docs/**/*"]
[lib]
name = "starship_module_config_derive"
proc-macro = true
[dependencies]
proc-macro2 = "~1"
quote = "~1"
syn = "~1"
[dev-dependencies]
starship = { version = "0.20.0", path = "../starship" }

View File

@ -0,0 +1,76 @@
extern crate proc_macro;
extern crate proc_macro2;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(ModuleConfig)]
pub fn derive_module_config(input: TokenStream) -> TokenStream {
let dinput = parse_macro_input!(input as DeriveInput);
impl_module_config(dinput)
}
fn impl_module_config(dinput: DeriveInput) -> proc_macro::TokenStream {
let struct_ident = &dinput.ident;
let (_impl_generics, ty_generics, where_clause) = dinput.generics.split_for_impl();
let mut from_config = quote! {};
let mut load_config = quote! {};
if let syn::Data::Struct(data) = dinput.data {
if let syn::Fields::Named(fields_named) = data.fields {
let mut load_tokens = quote! {};
let mut from_tokens = quote! {};
for field in fields_named.named.iter() {
let ident = field.ident.as_ref().unwrap();
let ty = &field.ty;
let new_load_tokens = quote! {
if let Some(config_str) = config.get(stringify!(#ident)) {
new_module_config.#ident = new_module_config.#ident.load_config(config_str);
}
};
let new_from_tokens = quote! {
#ident: <#ty>::from_config(config.get(stringify!(#ident))?)?,
};
load_tokens = quote! {
#load_tokens
#new_load_tokens
};
from_tokens = quote! {
#from_tokens
#new_from_tokens
}
}
load_config = quote! {
fn load_config(&self, config: &'a toml::Value) -> Self {
let mut new_module_config = self.clone();
if let toml::Value::Table(config) = config {
#load_tokens
}
new_module_config
}
};
from_config = quote! {
fn from_config(config: &'a toml::Value) -> Option<Self> {
let config = config.as_table()?;
Some(#struct_ident {
#from_tokens
})
}
};
}
}
TokenStream::from(quote! {
impl<'a> ModuleConfig<'a> for #struct_ident #ty_generics #where_clause {
#from_config
#load_config
}
})
}

View File

@ -52,17 +52,8 @@ RUN python --version
RUN USER=nonroot cargo new --bin /src/starship
WORKDIR /src/starship
# We want dependencies cached, so copy those first
COPY ./Cargo.lock ./Cargo.lock
COPY ./Cargo.toml ./Cargo.toml
# Cargo.toml will fail to parse without my_benchmark
RUN mkdir benches
RUN touch benches/my_benchmark.rs
# This is a dummy build to get dependencies cached
RUN cargo build --release \
&& rm -rf src target/debug/starship*
# Copy the whole project
COPY . .
# "-Z unstable-options" is required for "--include-ignored"
CMD ["cargo", "test", "--", "-Z", "unstable-options", "--include-ignored"]