refactor: Refactoring config (#383)
This PR refactors config and puts configuration files for all modules in `configs/`.
This commit is contained in:
parent
9e9eb6a8ef
commit
dd0b1a1aa2
|
@ -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
|
||||
|
|
File diff suppressed because it is too large
Load Diff
54
Cargo.toml
54
Cargo.toml
|
@ -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",
|
||||
]
|
||||
|
|
473
src/config.rs
473
src/config.rs
|
@ -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))
|
||||
);
|
||||
}
|
||||
}
|
115
src/print.rs
115
src/print.rs
|
@ -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);
|
||||
}
|
|
@ -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"
|
|
@ -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))
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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",
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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;
|
|
@ -2,6 +2,7 @@
|
|||
extern crate clap;
|
||||
|
||||
mod config;
|
||||
mod configs;
|
||||
mod context;
|
||||
mod init;
|
||||
mod module;
|
|
@ -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)?)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 })
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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" }
|
|
@ -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
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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"]
|
||||
|
|
Loading…
Reference in New Issue