mirror of
https://github.com/starship/starship.git
synced 2026-06-23 02:05:51 +07:00
refactor: Refactoring config (#383)
This PR refactors config and puts configuration files for all modules in `configs/`.
This commit is contained in:
committed by
Matan Kushner
parent
9e9eb6a8ef
commit
dd0b1a1aa2
-473
@@ -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))
|
||||
);
|
||||
}
|
||||
}
|
||||
-294
@@ -1,294 +0,0 @@
|
||||
use crate::config::Config;
|
||||
use crate::module::Module;
|
||||
|
||||
use clap::ArgMatches;
|
||||
use git2::{Repository, RepositoryState};
|
||||
use once_cell::sync::OnceCell;
|
||||
use std::env;
|
||||
use std::ffi::OsStr;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Context contains data or common methods that may be used by multiple modules.
|
||||
/// The data contained within Context will be relevant to this particular rendering
|
||||
/// of the prompt.
|
||||
pub struct Context<'a> {
|
||||
/// The deserialized configuration map from the user's `starship.toml` file.
|
||||
pub config: toml::value::Table,
|
||||
|
||||
/// The current working directory that starship is being called in.
|
||||
pub current_dir: PathBuf,
|
||||
|
||||
/// A vector containing the full paths of all the files in `current_dir`.
|
||||
dir_files: OnceCell<Vec<PathBuf>>,
|
||||
|
||||
/// The map of arguments that were passed when starship was called.
|
||||
pub arguments: ArgMatches<'a>,
|
||||
|
||||
/// Private field to store Git information for modules who need it
|
||||
repo: OnceCell<Repo>,
|
||||
}
|
||||
|
||||
impl<'a> Context<'a> {
|
||||
/// Identify the current working directory and create an instance of Context
|
||||
/// for it.
|
||||
pub fn new(arguments: ArgMatches) -> Context {
|
||||
// Retrieve the "path" flag. If unavailable, use the current directory instead.
|
||||
let path = arguments
|
||||
.value_of("path")
|
||||
.map(From::from)
|
||||
.unwrap_or_else(|| env::current_dir().expect("Unable to identify current directory."));
|
||||
|
||||
Context::new_with_dir(arguments, path)
|
||||
}
|
||||
|
||||
/// Create a new instance of Context for the provided directory
|
||||
pub fn new_with_dir<T>(arguments: ArgMatches, dir: T) -> Context
|
||||
where
|
||||
T: Into<PathBuf>,
|
||||
{
|
||||
let config = toml::value::Table::initialize();
|
||||
|
||||
// TODO: Currently gets the physical directory. Get the logical directory.
|
||||
let current_dir = Context::expand_tilde(dir.into());
|
||||
|
||||
Context {
|
||||
config,
|
||||
arguments,
|
||||
current_dir,
|
||||
dir_files: OnceCell::new(),
|
||||
repo: OnceCell::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a `~` in a path to the home directory
|
||||
fn expand_tilde(dir: PathBuf) -> PathBuf {
|
||||
if dir.starts_with("~") {
|
||||
let without_home = dir.strip_prefix("~").unwrap();
|
||||
return dirs::home_dir().unwrap().join(without_home);
|
||||
}
|
||||
dir
|
||||
}
|
||||
|
||||
/// Create a new module
|
||||
pub fn new_module(&self, name: &str) -> Module {
|
||||
let config = self.config.get_module_config(name);
|
||||
|
||||
Module::new(name, config)
|
||||
}
|
||||
|
||||
/// Check the `disabled` configuration of the module
|
||||
pub fn is_module_enabled(&self, name: &str) -> bool {
|
||||
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"));
|
||||
|
||||
disabled != Some(true)
|
||||
}
|
||||
|
||||
// returns a new ScanDir struct with reference to current dir_files of context
|
||||
// see ScanDir for methods
|
||||
pub fn try_begin_scan(&'a self) -> Option<ScanDir<'a>> {
|
||||
Some(ScanDir {
|
||||
dir_files: self.get_dir_files().ok()?,
|
||||
files: &[],
|
||||
folders: &[],
|
||||
extensions: &[],
|
||||
})
|
||||
}
|
||||
|
||||
/// Will lazily get repo root and branch when a module requests it.
|
||||
pub fn get_repo(&self) -> Result<&Repo, std::io::Error> {
|
||||
self.repo
|
||||
.get_or_try_init(|| -> Result<Repo, std::io::Error> {
|
||||
let repository = Repository::discover(&self.current_dir).ok();
|
||||
let branch = repository
|
||||
.as_ref()
|
||||
.and_then(|repo| get_current_branch(repo));
|
||||
let root = repository
|
||||
.as_ref()
|
||||
.and_then(|repo| repo.workdir().map(Path::to_path_buf));
|
||||
let state = repository.as_ref().map(|repo| repo.state());
|
||||
|
||||
Ok(Repo {
|
||||
branch,
|
||||
root,
|
||||
state,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_dir_files(&self) -> Result<&Vec<PathBuf>, std::io::Error> {
|
||||
self.dir_files
|
||||
.get_or_try_init(|| -> Result<Vec<PathBuf>, std::io::Error> {
|
||||
let dir_files = fs::read_dir(&self.current_dir)?
|
||||
.filter_map(Result::ok)
|
||||
.map(|entry| entry.path())
|
||||
.collect::<Vec<PathBuf>>();
|
||||
|
||||
Ok(dir_files)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Repo {
|
||||
/// If `current_dir` is a git repository or is contained within one,
|
||||
/// this is the current branch name of that repo.
|
||||
pub branch: Option<String>,
|
||||
|
||||
/// If `current_dir` is a git repository or is contained within one,
|
||||
/// this is the path to the root of that repo.
|
||||
pub root: Option<PathBuf>,
|
||||
|
||||
/// State
|
||||
pub state: Option<RepositoryState>,
|
||||
}
|
||||
|
||||
// A struct of Criteria which will be used to verify current PathBuf is
|
||||
// of X language, criteria can be set via the builder pattern
|
||||
pub struct ScanDir<'a> {
|
||||
dir_files: &'a Vec<PathBuf>,
|
||||
files: &'a [&'a str],
|
||||
folders: &'a [&'a str],
|
||||
extensions: &'a [&'a str],
|
||||
}
|
||||
|
||||
impl<'a> ScanDir<'a> {
|
||||
pub const fn set_files(mut self, files: &'a [&'a str]) -> Self {
|
||||
self.files = files;
|
||||
self
|
||||
}
|
||||
|
||||
pub const fn set_extensions(mut self, extensions: &'a [&'a str]) -> Self {
|
||||
self.extensions = extensions;
|
||||
self
|
||||
}
|
||||
|
||||
pub const fn set_folders(mut self, folders: &'a [&'a str]) -> Self {
|
||||
self.folders = folders;
|
||||
self
|
||||
}
|
||||
|
||||
/// based on the current Pathbuf check to see
|
||||
/// if any of this criteria match or exist and returning a boolean
|
||||
pub fn is_match(&self) -> bool {
|
||||
self.dir_files.iter().any(|path| {
|
||||
if path.is_dir() {
|
||||
path_has_name(path, self.folders)
|
||||
} else {
|
||||
path_has_name(path, self.files) || has_extension(path, self.extensions)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// checks to see if the pathbuf matches a file or folder name
|
||||
pub fn path_has_name<'a>(dir_entry: &PathBuf, names: &'a [&'a str]) -> bool {
|
||||
let found_file_or_folder_name = names.iter().find(|file_or_folder_name| {
|
||||
dir_entry
|
||||
.file_name()
|
||||
.and_then(OsStr::to_str)
|
||||
.unwrap_or_default()
|
||||
== **file_or_folder_name
|
||||
});
|
||||
|
||||
match found_file_or_folder_name {
|
||||
Some(name) => !name.is_empty(),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// checks if pathbuf doesn't start with a dot and matches any provided extension
|
||||
pub fn has_extension<'a>(dir_entry: &PathBuf, extensions: &'a [&'a str]) -> bool {
|
||||
if let Some(file_name) = dir_entry.file_name() {
|
||||
if file_name.to_string_lossy().starts_with('.') {
|
||||
return false;
|
||||
}
|
||||
return extensions.iter().any(|ext| {
|
||||
dir_entry
|
||||
.extension()
|
||||
.and_then(OsStr::to_str)
|
||||
.map_or(false, |e| e == *ext)
|
||||
});
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn get_current_branch(repository: &Repository) -> Option<String> {
|
||||
let head = repository.head().ok()?;
|
||||
let shorthand = head.shorthand();
|
||||
|
||||
shorthand.map(std::string::ToString::to_string)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_path_has_name() {
|
||||
let mut buf = PathBuf::from("/");
|
||||
let files = vec!["package.json"];
|
||||
|
||||
assert_eq!(path_has_name(&buf, &files), false);
|
||||
|
||||
buf.set_file_name("some-file.js");
|
||||
assert_eq!(path_has_name(&buf, &files), false);
|
||||
|
||||
buf.set_file_name("package.json");
|
||||
assert_eq!(path_has_name(&buf, &files), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_has_extension() {
|
||||
let mut buf = PathBuf::from("/");
|
||||
let extensions = vec!["js"];
|
||||
|
||||
assert_eq!(has_extension(&buf, &extensions), false);
|
||||
|
||||
buf.set_file_name("some-file.rs");
|
||||
assert_eq!(has_extension(&buf, &extensions), false);
|
||||
|
||||
buf.set_file_name(".some-file.js");
|
||||
assert_eq!(has_extension(&buf, &extensions), false);
|
||||
|
||||
buf.set_file_name("some-file.js");
|
||||
assert_eq!(has_extension(&buf, &extensions), true)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_criteria_scan_fails() {
|
||||
let failing_criteria = ScanDir {
|
||||
dir_files: &vec![PathBuf::new()],
|
||||
files: &["package.json"],
|
||||
extensions: &["js"],
|
||||
folders: &["node_modules"],
|
||||
};
|
||||
|
||||
// fails if buffer does not match any criteria
|
||||
assert_eq!(failing_criteria.is_match(), false);
|
||||
|
||||
let failing_dir_criteria = ScanDir {
|
||||
dir_files: &vec![PathBuf::from("/package.js/dog.go")],
|
||||
files: &["package.json"],
|
||||
extensions: &["js"],
|
||||
folders: &["node_modules"],
|
||||
};
|
||||
|
||||
// fails when passed a pathbuf dir matches extension path
|
||||
assert_eq!(failing_dir_criteria.is_match(), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_criteria_scan_passes() {
|
||||
let passing_criteria = ScanDir {
|
||||
dir_files: &vec![PathBuf::from("package.json")],
|
||||
files: &["package.json"],
|
||||
extensions: &["js"],
|
||||
folders: &["node_modules"],
|
||||
};
|
||||
|
||||
assert_eq!(passing_criteria.is_match(), true);
|
||||
}
|
||||
}
|
||||
-170
@@ -1,170 +0,0 @@
|
||||
use std::ffi::OsStr;
|
||||
use std::path::Path;
|
||||
use std::{env, io};
|
||||
|
||||
/* We use a two-phase init here: the first phase gives a simple command to the
|
||||
shell. This command evaluates a more complicated script using `source` and
|
||||
process substitution.
|
||||
|
||||
Directly using `eval` on a shell script causes it to be evaluated in
|
||||
a single line, which sucks because things like comments will comment out the
|
||||
rest of the script, and you have to spam semicolons everywhere. By using
|
||||
source and process substitutions, we make it possible to comment and debug
|
||||
the init scripts.
|
||||
|
||||
In the future, this may be changed to just directly evaluating the initscript
|
||||
using whatever mechanism is available in the host shell--this two-phase solution
|
||||
has been developed as a compatibility measure with `eval $(starship init X)`
|
||||
*/
|
||||
|
||||
fn path_to_starship() -> io::Result<String> {
|
||||
let current_exe = env::current_exe()?
|
||||
.to_str()
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "can't convert to str"))?
|
||||
.to_string();
|
||||
Ok(current_exe)
|
||||
}
|
||||
|
||||
/* This prints the setup stub, the short piece of code which sets up the main
|
||||
init code. The stub produces the main init script, then evaluates it with
|
||||
`source` and process substitution */
|
||||
pub fn init_stub(shell_name: &str) -> io::Result<()> {
|
||||
log::debug!("Shell name: {}", shell_name);
|
||||
|
||||
let shell_basename = Path::new(shell_name).file_stem().and_then(OsStr::to_str);
|
||||
|
||||
let starship = path_to_starship()?.replace("\"", "\"'\"'\"");
|
||||
|
||||
let setup_stub = match shell_basename {
|
||||
Some("bash") => {
|
||||
/*
|
||||
* The standard bash bootstrap is:
|
||||
* `source <(starship init bash --print-full-init)`
|
||||
*
|
||||
* Unfortunately there is an issue with bash 3.2 (the MacOS
|
||||
* default) which prevents this from working. It does not support
|
||||
* `source` with process substitution.
|
||||
*
|
||||
* There are more details here: https://stackoverflow.com/a/32596626
|
||||
*
|
||||
* The workaround for MacOS is to use the `/dev/stdin` trick you
|
||||
* see below. However, there are some systems with emulated POSIX
|
||||
* environments which do not support `/dev/stdin`. For example,
|
||||
* `Git Bash` within `Git for Windows and `Termux` on Android.
|
||||
*
|
||||
* Fortunately, these apps ship with recent-ish versions of bash.
|
||||
* Git Bash is currently shipping bash 4.4 and Termux is shipping
|
||||
* bash 5.0.
|
||||
*
|
||||
* Some testing has suggested that bash 4.0 is also incompatible
|
||||
* with the standard bootstrap, whereas bash 4.1 appears to be
|
||||
* consistently compatible.
|
||||
*
|
||||
* The upshot of all of this, is that we will use the standard
|
||||
* bootstrap whenever the bash version is 4.1 or higher. Otherwise,
|
||||
* we fall back to the `/dev/stdin` solution.
|
||||
*
|
||||
* More background can be found in these pull requests:
|
||||
* https://github.com/starship/starship/pull/241
|
||||
* https://github.com/starship/starship/pull/278
|
||||
*/
|
||||
let script = {
|
||||
format!(
|
||||
r#"if [ "${{BASH_VERSINFO[0]}}" -gt 4 ] || ([ "${{BASH_VERSINFO[0]}}" -eq 4 ] && [ "${{BASH_VERSINFO[1]}}" -ge 1 ])
|
||||
then
|
||||
source <("{}" init bash --print-full-init)
|
||||
else
|
||||
source /dev/stdin <<<"$("{}" init bash --print-full-init)"
|
||||
fi"#,
|
||||
starship, starship
|
||||
)
|
||||
};
|
||||
|
||||
Some(script)
|
||||
}
|
||||
Some("zsh") => {
|
||||
let script = format!("source <(\"{}\" init zsh --print-full-init)", starship);
|
||||
Some(script)
|
||||
}
|
||||
Some("fish") => {
|
||||
// Fish does process substitution with pipes and psub instead of bash syntax
|
||||
let script = format!(
|
||||
"source (\"{}\" init fish --print-full-init | psub)",
|
||||
starship
|
||||
);
|
||||
Some(script)
|
||||
}
|
||||
None => {
|
||||
println!(
|
||||
"Invalid shell name provided: {}\\n\
|
||||
If this issue persists, please open an \
|
||||
issue in the starship repo: \\n\
|
||||
https://github.com/starship/starship/issues/new\\n\"",
|
||||
shell_name
|
||||
);
|
||||
None
|
||||
}
|
||||
Some(shell_basename) => {
|
||||
println!(
|
||||
"printf \"\\n{0} is not yet supported by starship.\\n\
|
||||
For the time being, we support bash, zsh, and fish.\\n\
|
||||
Please open an issue in the starship repo if you would like to \
|
||||
see support for {0}:\\nhttps://github.com/starship/starship/issues/new\"\\n\\n",
|
||||
shell_basename
|
||||
);
|
||||
None
|
||||
}
|
||||
};
|
||||
if let Some(script) = setup_stub {
|
||||
print!("{}", script);
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/* This function (called when `--print-full-init` is passed to `starship init`)
|
||||
prints out the main initialization script */
|
||||
pub fn init_main(shell_name: &str) -> io::Result<()> {
|
||||
let starship_path = path_to_starship()?.replace("\"", "\"'\"'\"");
|
||||
|
||||
let setup_script = match shell_name {
|
||||
"bash" => Some(BASH_INIT),
|
||||
"zsh" => Some(ZSH_INIT),
|
||||
"fish" => Some(FISH_INIT),
|
||||
_ => {
|
||||
println!(
|
||||
"printf \"Shell name detection failed on phase two init.\\n\
|
||||
This probably indicates a bug within starship: please open\\n\
|
||||
an issue at https://github.com/starship/starship/issues/new\\n\""
|
||||
);
|
||||
None
|
||||
}
|
||||
};
|
||||
if let Some(script) = setup_script {
|
||||
// Set up quoting for starship path in case it has spaces.
|
||||
let starship_path_string = format!("\"{}\"", starship_path);
|
||||
let script = script.replace("::STARSHIP::", &starship_path_string);
|
||||
print!("{}", script);
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/* GENERAL INIT SCRIPT NOTES
|
||||
|
||||
Each init script will be passed as-is. Global notes for init scripts are in this
|
||||
comment, with additional per-script comments in the strings themselves.
|
||||
|
||||
JOBS: The argument to `--jobs` is quoted because MacOS's `wc` leaves whitespace
|
||||
in the output. We pass it to starship and do the whitespace removal in Rust,
|
||||
to avoid the cost of an additional shell fork every shell draw.
|
||||
|
||||
Note that the init scripts are not in their final form--they are processed by
|
||||
`starship init` prior to emitting the final form. In this processing, some tokens
|
||||
are replaced, e.g. `::STARSHIP::` is replaced by the full path to the
|
||||
starship binary.
|
||||
*/
|
||||
|
||||
const BASH_INIT: &str = include_str!("starship.bash");
|
||||
|
||||
const ZSH_INIT: &str = include_str!("starship.zsh");
|
||||
|
||||
const FISH_INIT: &str = include_str!("starship.fish");
|
||||
@@ -1,69 +0,0 @@
|
||||
# We use PROMPT_COMMAND and the DEBUG trap to generate timing information. We try
|
||||
# to avoid clobbering what we can, and try to give the user ways around our
|
||||
# clobbers, if it's unavoidable. For example, PROMPT_COMMAND is appended to,
|
||||
# and the DEBUG trap is layered with other traps, if it exists.
|
||||
|
||||
# A bash quirk is that the DEBUG trap is fired every time a command runs, even
|
||||
# if it's later on in the pipeline. If uncorrected, this could cause bad timing
|
||||
# data for commands like `slow | slow | fast`, since the timer starts at the start
|
||||
# of the "fast" command.
|
||||
|
||||
# To solve this, we set a flag `PREEXEC_READY` when the prompt is drawn, and only
|
||||
# start the timer if this flag is present. That way, timing is for the entire command,
|
||||
# and not just a portion of it.
|
||||
|
||||
# Will be run before *every* command (even ones in pipes!)
|
||||
starship_preexec() {
|
||||
# Avoid restarting the timer for commands in the same pipeline
|
||||
if [ "$PREEXEC_READY" = "true" ]; then
|
||||
PREEXEC_READY=false
|
||||
STARSHIP_START_TIME=$(date +%s)
|
||||
fi
|
||||
}
|
||||
|
||||
# Will be run before the prompt is drawn
|
||||
starship_precmd() {
|
||||
# Save the status, because commands in this pipeline will change $?
|
||||
STATUS=$?
|
||||
|
||||
# Run the bash precmd function, if it's set. If not set, evaluates to no-op
|
||||
"${starship_precmd_user_func-:}"
|
||||
|
||||
# Prepare the timer data, if needed.
|
||||
if [[ $STARSHIP_START_TIME ]]; then
|
||||
STARSHIP_END_TIME=$(date +%s)
|
||||
STARSHIP_DURATION=$((STARSHIP_END_TIME - STARSHIP_START_TIME))
|
||||
PS1="$(::STARSHIP:: prompt --status=$STATUS --jobs="$(jobs -p | wc -l)" --cmd-duration=$STARSHIP_DURATION)"
|
||||
unset STARSHIP_START_TIME
|
||||
else
|
||||
PS1="$(::STARSHIP:: prompt --status=$STATUS --jobs="$(jobs -p | wc -l)")"
|
||||
fi
|
||||
PREEXEC_READY=true; # Signal that we can safely restart the timer
|
||||
}
|
||||
|
||||
# If the user appears to be using https://github.com/rcaloras/bash-preexec,
|
||||
# then hook our functions into their framework.
|
||||
if [[ $preexec_functions ]]; then
|
||||
preexec_functions+=(starship_preexec)
|
||||
precmd_functions+=(starship_precmd)
|
||||
else
|
||||
# We want to avoid destroying an existing DEBUG hook. If we detect one, create
|
||||
# a new function that runs both the existing function AND our function, then
|
||||
# re-trap DEBUG to use this new function. This prevents a trap clobber.
|
||||
dbg_trap="$(trap -p DEBUG | cut -d' ' -f3 | tr -d \')"
|
||||
if [[ -z "$dbg_trap" ]]; then
|
||||
trap starship_preexec DEBUG
|
||||
elif [[ "$dbg_trap" != "starship_preexec" && "$dbg_trap" != "starship_preexec_all" ]]; then
|
||||
function starship_preexec_all(){
|
||||
$dbg_trap; starship_preexec
|
||||
}
|
||||
trap starship_preexec_all DEBUG
|
||||
fi
|
||||
|
||||
# Finally, prepare the precmd function and set up the start time.
|
||||
PROMPT_COMMAND="starship_precmd;$PROMPT_COMMAND"
|
||||
fi
|
||||
|
||||
# Set up the start time and STARSHIP_SHELL, which controls shell-specific sequences
|
||||
STARSHIP_START_TIME=$(date +%s)
|
||||
export STARSHIP_SHELL="bash"
|
||||
@@ -1,15 +0,0 @@
|
||||
function fish_prompt
|
||||
switch "$fish_key_bindings"
|
||||
case fish_hybrid_key_bindings fish_vi_key_bindings
|
||||
set keymap "$fish_bind_mode"
|
||||
case '*'
|
||||
set keymap insert
|
||||
end
|
||||
set -l exit_code $status
|
||||
# Account for changes in variable name between v2.7 and v3.0
|
||||
set -l CMD_DURATION "$CMD_DURATION$cmd_duration"
|
||||
set -l starship_duration (math --scale=0 "$CMD_DURATION / 1000")
|
||||
::STARSHIP:: prompt --status=$exit_code --keymap=$keymap --cmd-duration=$starship_duration --jobs=(count (jobs -p))
|
||||
end
|
||||
function fish_mode_prompt; end
|
||||
export STARSHIP_SHELL="fish"
|
||||
@@ -1,58 +0,0 @@
|
||||
# ZSH has a quirk where `preexec` is only run if a command is actually run (i.e
|
||||
# pressing ENTER at an empty command line will not cause preexec to fire). This
|
||||
# can cause timing issues, as a user who presses "ENTER" without running a command
|
||||
# will see the time to the start of the last command, which may be very large.
|
||||
|
||||
# To fix this, we create STARSHIP_START_TIME upon preexec() firing, and destroy it
|
||||
# after drawing the prompt. This ensures that the timing for one command is only
|
||||
# ever drawn once (for the prompt immediately after it is run).
|
||||
|
||||
zmodload zsh/parameter # Needed to access jobstates variable for NUM_JOBS
|
||||
|
||||
# Will be run before every prompt draw
|
||||
starship_precmd() {
|
||||
# Save the status, because commands in this pipeline will change $?
|
||||
STATUS=$?
|
||||
|
||||
# Use length of jobstates array as number of jobs. Expansion fails inside
|
||||
# quotes so we set it here and then use the value later on.
|
||||
NUM_JOBS=$#jobstates
|
||||
# Compute cmd_duration, if we have a time to consume
|
||||
if [[ ! -z "${STARSHIP_START_TIME+1}" ]]; then
|
||||
STARSHIP_END_TIME="$(date +%s)"
|
||||
STARSHIP_DURATION=$((STARSHIP_END_TIME - STARSHIP_START_TIME))
|
||||
PROMPT="$(::STARSHIP:: prompt --status=$STATUS --cmd-duration=$STARSHIP_DURATION --jobs="$NUM_JOBS")"
|
||||
unset STARSHIP_START_TIME
|
||||
else
|
||||
PROMPT="$(::STARSHIP:: prompt --status=$STATUS --jobs="$NUM_JOBS")"
|
||||
fi
|
||||
}
|
||||
starship_preexec(){
|
||||
STARSHIP_START_TIME="$(date +%s)"
|
||||
}
|
||||
|
||||
# If precmd/preexec arrays are not already set, set them. If we don't do this,
|
||||
# the code to detect whether starship_precmd is already in precmd_functions will
|
||||
# fail because the array doesn't exist (and same for starship_preexec)
|
||||
[[ -z "${precmd_functions+1}" ]] && precmd_functions=()
|
||||
[[ -z "${preexec_functions+1}" ]] && preexec_functions=()
|
||||
|
||||
# If starship precmd/preexec functions are already hooked, don't double-hook them
|
||||
# to avoid unnecessary performance degradation in nested shells
|
||||
if [[ ${precmd_functions[(ie)starship_precmd]} -gt ${#precmd_functions} ]]; then
|
||||
precmd_functions+=(starship_precmd)
|
||||
fi
|
||||
if [[ ${preexec_functions[(ie)starship_preexec]} -gt ${#preexec_functions} ]]; then
|
||||
preexec_functions+=(starship_preexec)
|
||||
fi
|
||||
|
||||
# Set up a function to redraw the prompt if the user switches vi modes
|
||||
function zle-keymap-select
|
||||
{
|
||||
PROMPT=$(::STARSHIP:: prompt --keymap=$KEYMAP --jobs="$(jobs | wc -l)")
|
||||
zle reset-prompt
|
||||
}
|
||||
|
||||
STARSHIP_START_TIME="$(date +%s)"
|
||||
zle -N zle-keymap-select
|
||||
export STARSHIP_SHELL="zsh"
|
||||
@@ -1,8 +0,0 @@
|
||||
// Lib is present to allow for benchmarking
|
||||
mod config;
|
||||
pub mod context;
|
||||
pub mod module;
|
||||
pub mod modules;
|
||||
pub mod print;
|
||||
pub mod segment;
|
||||
mod utils;
|
||||
-136
@@ -1,136 +0,0 @@
|
||||
#[macro_use]
|
||||
extern crate clap;
|
||||
|
||||
mod config;
|
||||
mod context;
|
||||
mod init;
|
||||
mod module;
|
||||
mod modules;
|
||||
mod print;
|
||||
mod segment;
|
||||
mod utils;
|
||||
|
||||
use crate::module::ALL_MODULES;
|
||||
use clap::{App, AppSettings, Arg, SubCommand};
|
||||
|
||||
fn main() {
|
||||
pretty_env_logger::init();
|
||||
|
||||
let status_code_arg = Arg::with_name("status_code")
|
||||
.short("s")
|
||||
.long("status")
|
||||
.value_name("STATUS_CODE")
|
||||
.help("The status code of the previously run command")
|
||||
.takes_value(true);
|
||||
|
||||
let path_arg = Arg::with_name("path")
|
||||
.short("p")
|
||||
.long("path")
|
||||
.value_name("PATH")
|
||||
.help("The path that the prompt should render for")
|
||||
.takes_value(true);
|
||||
|
||||
let shell_arg = Arg::with_name("shell")
|
||||
.value_name("SHELL")
|
||||
.help(
|
||||
"The name of the currently running shell\nCurrently supported options: bash, zsh, fish",
|
||||
)
|
||||
.required(true);
|
||||
|
||||
let cmd_duration_arg = Arg::with_name("cmd_duration")
|
||||
.short("d")
|
||||
.long("cmd-duration")
|
||||
.value_name("CMD_DURATION")
|
||||
.help("The execution duration of the last command, in seconds")
|
||||
.takes_value(true);
|
||||
|
||||
let keymap_arg = Arg::with_name("keymap")
|
||||
.short("k")
|
||||
.long("keymap")
|
||||
.value_name("KEYMAP")
|
||||
// fish/zsh only
|
||||
.help("The keymap of fish/zsh")
|
||||
.takes_value(true);
|
||||
|
||||
let jobs_arg = Arg::with_name("jobs")
|
||||
.short("j")
|
||||
.long("jobs")
|
||||
.value_name("JOBS")
|
||||
.help("The number of currently running jobs")
|
||||
.takes_value(true);
|
||||
|
||||
let init_scripts_arg = Arg::with_name("print_full_init")
|
||||
.long("print-full-init")
|
||||
.help("Print the main initialization script (as opposed to the init stub)");
|
||||
|
||||
let matches = App::new("starship")
|
||||
.about("The cross-shell prompt for astronauts. ☄🌌️")
|
||||
// pull the version number from Cargo.toml
|
||||
.version(crate_version!())
|
||||
// pull the authors from Cargo.toml
|
||||
.author(crate_authors!())
|
||||
.after_help("https://github.com/starship/starship")
|
||||
.setting(AppSettings::SubcommandRequiredElseHelp)
|
||||
.subcommand(
|
||||
SubCommand::with_name("init")
|
||||
.about("Prints the shell function used to execute starship")
|
||||
.arg(&shell_arg)
|
||||
.arg(&init_scripts_arg),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("prompt")
|
||||
.about("Prints the full starship prompt")
|
||||
.arg(&status_code_arg)
|
||||
.arg(&path_arg)
|
||||
.arg(&cmd_duration_arg)
|
||||
.arg(&keymap_arg)
|
||||
.arg(&jobs_arg),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("module")
|
||||
.about("Prints a specific prompt module")
|
||||
.arg(
|
||||
Arg::with_name("name")
|
||||
.help("The name of the module to be printed")
|
||||
.required(true)
|
||||
.required_unless("list"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("list")
|
||||
.short("l")
|
||||
.long("list")
|
||||
.help("List out all supported modules"),
|
||||
)
|
||||
.arg(&status_code_arg)
|
||||
.arg(&path_arg)
|
||||
.arg(&cmd_duration_arg)
|
||||
.arg(&keymap_arg)
|
||||
.arg(&jobs_arg),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
match matches.subcommand() {
|
||||
("init", Some(sub_m)) => {
|
||||
let shell_name = sub_m.value_of("shell").expect("Shell name missing.");
|
||||
if sub_m.is_present("print_full_init") {
|
||||
init::init_main(shell_name).expect("can't init_main");
|
||||
} else {
|
||||
init::init_stub(shell_name).expect("can't init_stub");
|
||||
}
|
||||
}
|
||||
("prompt", Some(sub_m)) => print::prompt(sub_m.clone()),
|
||||
("module", Some(sub_m)) => {
|
||||
if sub_m.is_present("list") {
|
||||
println!("Supported modules list");
|
||||
println!("----------------------");
|
||||
for modules in ALL_MODULES {
|
||||
println!("{}", modules);
|
||||
}
|
||||
}
|
||||
if let Some(module_name) = sub_m.value_of("name") {
|
||||
print::module(module_name, sub_m.clone());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
-328
@@ -1,328 +0,0 @@
|
||||
use crate::config::Config;
|
||||
use crate::config::SegmentConfig;
|
||||
use crate::segment::Segment;
|
||||
use ansi_term::Style;
|
||||
use ansi_term::{ANSIString, ANSIStrings};
|
||||
use std::fmt;
|
||||
|
||||
// List of all modules
|
||||
pub const ALL_MODULES: &[&str] = &[
|
||||
"aws",
|
||||
#[cfg(feature = "battery")]
|
||||
"battery",
|
||||
"character",
|
||||
"cmd_duration",
|
||||
"directory",
|
||||
"env_var",
|
||||
"git_branch",
|
||||
"git_state",
|
||||
"git_status",
|
||||
"golang",
|
||||
"hostname",
|
||||
"java",
|
||||
"jobs",
|
||||
"line_break",
|
||||
"memory_usage",
|
||||
"nix_shell",
|
||||
"nodejs",
|
||||
"package",
|
||||
"python",
|
||||
"ruby",
|
||||
"rust",
|
||||
"time",
|
||||
"username",
|
||||
];
|
||||
|
||||
/// A module is a collection of segments showing data for a single integration
|
||||
/// (e.g. The git module shows the current git branch and status)
|
||||
pub struct Module<'a> {
|
||||
/// The module's configuration map if available
|
||||
config: Option<&'a toml::value::Table>,
|
||||
|
||||
/// The module's name, to be used in configuration and logging.
|
||||
_name: String,
|
||||
|
||||
/// The styling to be inherited by all segments contained within this module.
|
||||
style: Style,
|
||||
|
||||
/// The prefix used to separate the current module from the previous one.
|
||||
prefix: Affix,
|
||||
|
||||
/// The collection of segments that compose this module.
|
||||
segments: Vec<Segment>,
|
||||
|
||||
/// The suffix used to separate the current module from the next one.
|
||||
suffix: Affix,
|
||||
}
|
||||
|
||||
impl<'a> Module<'a> {
|
||||
/// Creates a module with no segments.
|
||||
pub fn new(name: &str, config: Option<&'a toml::value::Table>) -> Module<'a> {
|
||||
Module {
|
||||
config,
|
||||
_name: name.to_string(),
|
||||
style: Style::default(),
|
||||
prefix: Affix::default_prefix(name),
|
||||
segments: Vec::new(),
|
||||
suffix: Affix::default_suffix(name),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to a newly created segment in the module
|
||||
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) {
|
||||
segment.set_style(segment_config.style.unwrap_or(self.style));
|
||||
segment.set_value(segment_config.value.unwrap_or(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));
|
||||
}
|
||||
self.segments.push(segment);
|
||||
|
||||
self.segments.last_mut().unwrap()
|
||||
}
|
||||
|
||||
/// Should config exists, get a reference to a newly created segment in the module
|
||||
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) {
|
||||
let mut segment = Segment::new(name);
|
||||
segment.set_style(self.style);
|
||||
segment.set_value(value);
|
||||
self.segments.push(segment);
|
||||
Some(self.segments.last_mut().unwrap())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a module has non-empty segments
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.segments.iter().all(|segment| segment.is_empty())
|
||||
}
|
||||
|
||||
/// Get the module's prefix
|
||||
pub fn get_prefix(&mut self) -> &mut Affix {
|
||||
&mut self.prefix
|
||||
}
|
||||
|
||||
/// Get the module's suffix
|
||||
pub fn get_suffix(&mut self) -> &mut Affix {
|
||||
&mut self.suffix
|
||||
}
|
||||
|
||||
/// Sets the style of the segment.
|
||||
///
|
||||
/// Accepts either `Color` or `Style`.
|
||||
pub fn set_style<T>(&mut self, style: T) -> &mut Module<'a>
|
||||
where
|
||||
T: Into<Style>,
|
||||
{
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns a vector of colored ANSIString elements to be later used with
|
||||
/// `ANSIStrings()` to optimize ANSI codes
|
||||
pub fn ansi_strings(&self) -> Vec<ANSIString> {
|
||||
let shell = std::env::var("STARSHIP_SHELL").unwrap_or_default();
|
||||
let ansi_strings = self
|
||||
.segments
|
||||
.iter()
|
||||
.map(Segment::ansi_string)
|
||||
.collect::<Vec<ANSIString>>();
|
||||
|
||||
let mut ansi_strings = match shell.as_str() {
|
||||
"bash" => ansi_strings_modified(ansi_strings, shell),
|
||||
"zsh" => ansi_strings_modified(ansi_strings, shell),
|
||||
_ => ansi_strings,
|
||||
};
|
||||
|
||||
ansi_strings.insert(0, self.prefix.ansi_string());
|
||||
ansi_strings.push(self.suffix.ansi_string());
|
||||
|
||||
ansi_strings
|
||||
}
|
||||
|
||||
pub fn to_string_without_prefix(&self) -> String {
|
||||
ANSIStrings(&self.ansi_strings()[1..]).to_string()
|
||||
}
|
||||
|
||||
/// Get a module's config value as a string
|
||||
pub fn config_value_str(&self, key: &str) -> Option<&str> {
|
||||
self.config.and_then(|config| config.get_as_str(key))
|
||||
}
|
||||
|
||||
/// Get a module's config value as an int
|
||||
pub fn config_value_i64(&self, key: &str) -> Option<i64> {
|
||||
self.config.and_then(|config| config.get_as_i64(key))
|
||||
}
|
||||
|
||||
/// Get a module's config value as a bool
|
||||
pub fn config_value_bool(&self, key: &str) -> Option<bool> {
|
||||
self.config.and_then(|config| config.get_as_bool(key))
|
||||
}
|
||||
|
||||
/// Get a module's config value as a style
|
||||
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))
|
||||
}
|
||||
|
||||
/// Get a module's config value as a table of segment config
|
||||
pub fn config_value_segment_config(&self, key: &str) -> Option<SegmentConfig> {
|
||||
self.config
|
||||
.and_then(|config| config.get_as_segment_config(key))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for Module<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let ansi_strings = self.ansi_strings();
|
||||
write!(f, "{}", ANSIStrings(&ansi_strings))
|
||||
}
|
||||
}
|
||||
|
||||
/// Many shells cannot deal with raw unprintable characters (like ANSI escape sequences) and
|
||||
/// miscompute the cursor position as a result, leading to strange visual bugs. Here, we wrap these
|
||||
/// characters in shell-specific escape codes to indicate to the shell that they are zero-length.
|
||||
fn ansi_strings_modified(ansi_strings: Vec<ANSIString>, shell: String) -> Vec<ANSIString> {
|
||||
const ESCAPE_BEGIN: char = '\u{1b}';
|
||||
const MAYBE_ESCAPE_END: char = 'm';
|
||||
ansi_strings
|
||||
.iter()
|
||||
.map(|ansi| {
|
||||
let mut escaped = false;
|
||||
let final_string: String = ansi
|
||||
.to_string()
|
||||
.chars()
|
||||
.map(|x| match x {
|
||||
ESCAPE_BEGIN => {
|
||||
escaped = true;
|
||||
match shell.as_str() {
|
||||
"bash" => String::from("\u{5c}\u{5b}\u{1b}"), // => \[ESC
|
||||
"zsh" => String::from("\u{25}\u{7b}\u{1b}"), // => %{ESC
|
||||
_ => x.to_string(),
|
||||
}
|
||||
}
|
||||
MAYBE_ESCAPE_END => {
|
||||
if escaped {
|
||||
escaped = false;
|
||||
match shell.as_str() {
|
||||
"bash" => String::from("m\u{5c}\u{5d}"), // => m\]
|
||||
"zsh" => String::from("m\u{25}\u{7d}"), // => m%}
|
||||
_ => x.to_string(),
|
||||
}
|
||||
} else {
|
||||
x.to_string()
|
||||
}
|
||||
}
|
||||
_ => x.to_string(),
|
||||
})
|
||||
.collect();
|
||||
ANSIString::from(final_string)
|
||||
})
|
||||
.collect::<Vec<ANSIString>>()
|
||||
}
|
||||
|
||||
/// Module affixes are to be used for the prefix or suffix of a module.
|
||||
pub struct Affix {
|
||||
/// The affix's name, to be used in configuration and logging.
|
||||
_name: String,
|
||||
|
||||
/// The affix's style.
|
||||
style: Style,
|
||||
|
||||
/// The string value of the affix.
|
||||
value: String,
|
||||
}
|
||||
|
||||
impl Affix {
|
||||
pub fn default_prefix(name: &str) -> Self {
|
||||
Self {
|
||||
_name: format!("{}_prefix", name),
|
||||
style: Style::default(),
|
||||
value: "via ".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_suffix(name: &str) -> Self {
|
||||
Self {
|
||||
_name: format!("{}_suffix", name),
|
||||
style: Style::default(),
|
||||
value: " ".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the style of the module.
|
||||
///
|
||||
/// Accepts either `Color` or `Style`.
|
||||
pub fn set_style<T>(&mut self, style: T) -> &mut Self
|
||||
where
|
||||
T: Into<Style>,
|
||||
{
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the value of the module.
|
||||
pub fn set_value<T>(&mut self, value: T) -> &mut Self
|
||||
where
|
||||
T: Into<String>,
|
||||
{
|
||||
self.value = value.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Generates the colored ANSIString output.
|
||||
pub fn ansi_string(&self) -> ANSIString {
|
||||
self.style.paint(&self.value)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Affix {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.ansi_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_module_is_empty_with_no_segments() {
|
||||
let name = "unit_test";
|
||||
let module = Module {
|
||||
config: None,
|
||||
_name: name.to_string(),
|
||||
style: Style::default(),
|
||||
prefix: Affix::default_prefix(name),
|
||||
segments: Vec::new(),
|
||||
suffix: Affix::default_suffix(name),
|
||||
};
|
||||
|
||||
assert!(module.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_module_is_empty_with_all_empty_segments() {
|
||||
let name = "unit_test";
|
||||
let module = Module {
|
||||
config: None,
|
||||
_name: name.to_string(),
|
||||
style: Style::default(),
|
||||
prefix: Affix::default_prefix(name),
|
||||
segments: vec![Segment::new("test_segment")],
|
||||
suffix: Affix::default_suffix(name),
|
||||
};
|
||||
|
||||
assert!(module.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
use std::env;
|
||||
|
||||
use ansi_term::Color;
|
||||
|
||||
use super::{Context, Module};
|
||||
|
||||
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
||||
const AWS_CHAR: &str = "☁️ ";
|
||||
const AWS_PREFIX: &str = "on ";
|
||||
|
||||
let aws_profile = env::var("AWS_PROFILE").ok()?;
|
||||
if aws_profile.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut module = context.new_module("aws");
|
||||
|
||||
let module_style = module
|
||||
.config_value_style("style")
|
||||
.unwrap_or_else(|| Color::Yellow.bold());
|
||||
module.set_style(module_style);
|
||||
|
||||
module.get_prefix().set_value(AWS_PREFIX);
|
||||
|
||||
module.new_segment("symbol", AWS_CHAR);
|
||||
module.new_segment("profile", &aws_profile);
|
||||
|
||||
Some(module)
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
use ansi_term::{Color, Style};
|
||||
|
||||
use super::{Context, Module};
|
||||
use crate::config::Config;
|
||||
|
||||
/// 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();
|
||||
let percentage_char = match shell.as_str() {
|
||||
"zsh" => "%%", // % is an escape in zsh, see PROMPT in `man zshmisc`
|
||||
_ => "%",
|
||||
};
|
||||
|
||||
let battery_status = get_battery_status()?;
|
||||
let BatteryStatus { state, percentage } = battery_status;
|
||||
|
||||
let mut module = context.new_module("battery");
|
||||
|
||||
// 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
|
||||
});
|
||||
|
||||
if let Some(display_style) = display_style {
|
||||
let BatteryDisplayStyle { style, .. } = display_style;
|
||||
|
||||
// Set style based on percentage
|
||||
module.set_style(*style);
|
||||
module.get_prefix().set_value("");
|
||||
|
||||
match state {
|
||||
battery::State::Full => {
|
||||
module.new_segment("full_symbol", BATTERY_FULL);
|
||||
}
|
||||
battery::State::Charging => {
|
||||
module.new_segment("charging_symbol", BATTERY_CHARGING);
|
||||
}
|
||||
battery::State::Discharging => {
|
||||
module.new_segment("discharging_symbol", BATTERY_DISCHARGING);
|
||||
}
|
||||
battery::State::Unknown => {
|
||||
log::debug!("Unknown detected");
|
||||
module.new_segment_if_config_exists("unknown_symbol")?;
|
||||
}
|
||||
battery::State::Empty => {
|
||||
module.new_segment_if_config_exists("empty_symbol")?;
|
||||
}
|
||||
_ => {
|
||||
log::debug!("Unhandled battery state `{}`", state);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let mut percent_string = Vec::<String>::with_capacity(2);
|
||||
// Round the percentage to a whole number
|
||||
percent_string.push(percentage.round().to_string());
|
||||
percent_string.push(percentage_char.to_string());
|
||||
module.new_segment("percentage", percent_string.join("").as_ref());
|
||||
|
||||
Some(module)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
Some(Ok(battery)) => {
|
||||
log::debug!("Battery found: {:?}", battery);
|
||||
let battery_status = BatteryStatus {
|
||||
percentage: battery.state_of_charge().value * 100.0,
|
||||
state: battery.state(),
|
||||
};
|
||||
|
||||
Some(battery_status)
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
log::debug!("Unable to access battery information:\n{}", &e);
|
||||
None
|
||||
}
|
||||
None => {
|
||||
log::debug!("No batteries found");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,69 +0,0 @@
|
||||
use super::{Context, Module};
|
||||
use ansi_term::Color;
|
||||
|
||||
/// Creates a module for the prompt character
|
||||
///
|
||||
/// The character segment prints an arrow character in a color dependant on the exit-
|
||||
/// code of the last executed command:
|
||||
/// - If the exit-code was "0", the arrow will be formatted with `style_success`
|
||||
/// (green by default)
|
||||
/// - If the exit-code was anything else, the arrow will be formatted with
|
||||
/// `style_failure` (red by default)
|
||||
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
||||
const SUCCESS_CHAR: &str = "❯";
|
||||
const FAILURE_CHAR: &str = "✖";
|
||||
const VICMD_CHAR: &str = "❮";
|
||||
enum ShellEditMode {
|
||||
Normal,
|
||||
Insert,
|
||||
};
|
||||
const ASSUMED_MODE: ShellEditMode = ShellEditMode::Insert;
|
||||
// TODO: extend config to more modes
|
||||
|
||||
let mut module = context.new_module("character");
|
||||
module.get_prefix().set_value("");
|
||||
|
||||
let style_success = module
|
||||
.config_value_style("style_success")
|
||||
.unwrap_or_else(|| Color::Green.bold());
|
||||
let style_failure = module
|
||||
.config_value_style("style_failure")
|
||||
.unwrap_or_else(|| Color::Red.bold());
|
||||
|
||||
let arguments = &context.arguments;
|
||||
let use_symbol = module
|
||||
.config_value_bool("use_symbol_for_status")
|
||||
.unwrap_or(false);
|
||||
let exit_success = arguments.value_of("status_code").unwrap_or("0") == "0";
|
||||
let shell = std::env::var("STARSHIP_SHELL").unwrap_or_default();
|
||||
let keymap = arguments.value_of("keymap").unwrap_or("viins");
|
||||
|
||||
// Match shell "keymap" names to normalized vi modes
|
||||
// NOTE: in vi mode, fish reports normal mode as "default".
|
||||
// Unfortunately, this is also the name of the non-vi default mode.
|
||||
// We do some environment detection in src/init.rs to translate.
|
||||
// The result: in non-vi fish, keymap is always reported as "insert"
|
||||
let mode = match (shell.as_str(), keymap) {
|
||||
("fish", "default") | ("zsh", "vicmd") => ShellEditMode::Normal,
|
||||
_ => ASSUMED_MODE,
|
||||
};
|
||||
|
||||
/* If an error symbol is set in the config, use symbols to indicate
|
||||
success/failure, in addition to color */
|
||||
let symbol = if use_symbol && !exit_success {
|
||||
module.new_segment("error_symbol", FAILURE_CHAR)
|
||||
} else {
|
||||
match mode {
|
||||
ShellEditMode::Normal => module.new_segment("vicmd_symbol", VICMD_CHAR),
|
||||
ShellEditMode::Insert => module.new_segment("symbol", SUCCESS_CHAR),
|
||||
}
|
||||
};
|
||||
|
||||
if exit_success {
|
||||
symbol.set_style(style_success);
|
||||
} else {
|
||||
symbol.set_style(style_failure);
|
||||
};
|
||||
|
||||
Some(module)
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
use ansi_term::Color;
|
||||
|
||||
use super::{Context, Module};
|
||||
|
||||
/// Outputs the time it took the last command to execute
|
||||
///
|
||||
/// Will only print if last command took more than a certain amount of time to
|
||||
/// execute. Default is two seconds, but can be set by config option `min_time`.
|
||||
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
||||
let mut module = context.new_module("cmd_duration");
|
||||
|
||||
let arguments = &context.arguments;
|
||||
let elapsed = arguments
|
||||
.value_of("cmd_duration")
|
||||
.unwrap_or("invalid_time")
|
||||
.parse::<u64>()
|
||||
.ok()?;
|
||||
|
||||
let prefix = module
|
||||
.config_value_str("prefix")
|
||||
.unwrap_or("took ")
|
||||
.to_owned();
|
||||
|
||||
let signed_config_min = module.config_value_i64("min_time").unwrap_or(2);
|
||||
|
||||
/* TODO: Once error handling is implemented, warn the user if their config
|
||||
min time is nonsensical */
|
||||
if signed_config_min < 0 {
|
||||
log::debug!(
|
||||
"[WARN]: min_time in [cmd_duration] ({}) was less than zero",
|
||||
signed_config_min
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
let config_min = signed_config_min as u64;
|
||||
|
||||
let module_color = match elapsed {
|
||||
time if time < config_min => return None,
|
||||
_ => module
|
||||
.config_value_style("style")
|
||||
.unwrap_or_else(|| Color::Yellow.bold()),
|
||||
};
|
||||
|
||||
module.set_style(module_color);
|
||||
module.new_segment(
|
||||
"cmd_duration",
|
||||
&format!("{}{}", prefix, render_time(elapsed)),
|
||||
);
|
||||
module.get_prefix().set_value("");
|
||||
|
||||
Some(module)
|
||||
}
|
||||
|
||||
// Render the time into a nice human-readable string
|
||||
fn render_time(raw_seconds: u64) -> String {
|
||||
// Calculate a simple breakdown into days/hours/minutes/seconds
|
||||
let (seconds, raw_minutes) = (raw_seconds % 60, raw_seconds / 60);
|
||||
let (minutes, raw_hours) = (raw_minutes % 60, raw_minutes / 60);
|
||||
let (hours, days) = (raw_hours % 24, raw_hours / 24);
|
||||
|
||||
let components = [days, hours, minutes, seconds];
|
||||
let suffixes = ["d", "h", "m", "s"];
|
||||
|
||||
let rendered_components: Vec<String> = components
|
||||
.iter()
|
||||
.zip(&suffixes)
|
||||
.map(render_time_component)
|
||||
.collect();
|
||||
rendered_components.join("")
|
||||
}
|
||||
|
||||
/// Render a single component of the time string, giving an empty string if component is zero
|
||||
fn render_time_component((component, suffix): (&u64, &&str)) -> String {
|
||||
match component {
|
||||
0 => String::new(),
|
||||
n => format!("{}{}", n, suffix),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_10s() {
|
||||
assert_eq!(render_time(10 as u64), "10s")
|
||||
}
|
||||
#[test]
|
||||
fn test_90s() {
|
||||
assert_eq!(render_time(90 as u64), "1m30s")
|
||||
}
|
||||
#[test]
|
||||
fn test_10110s() {
|
||||
assert_eq!(render_time(10110 as u64), "2h48m30s")
|
||||
}
|
||||
#[test]
|
||||
fn test_1d() {
|
||||
assert_eq!(render_time(86400 as u64), "1d")
|
||||
}
|
||||
}
|
||||
@@ -1,312 +0,0 @@
|
||||
use ansi_term::Color;
|
||||
use path_slash::PathExt;
|
||||
use std::path::Path;
|
||||
|
||||
use super::{Context, Module};
|
||||
|
||||
/// Creates a module with the current directory
|
||||
///
|
||||
/// Will perform path contraction and truncation.
|
||||
/// **Contraction**
|
||||
/// - Paths beginning with the home directory or with a git repo right
|
||||
/// inside the home directory will be contracted to `~`
|
||||
/// - Paths containing a git repo will contract to begin at the repo root
|
||||
///
|
||||
/// **Truncation**
|
||||
/// Paths will be limited in length to `3` path components by default.
|
||||
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
||||
const HOME_SYMBOL: &str = "~";
|
||||
const DIR_TRUNCATION_LENGTH: i64 = 3;
|
||||
const FISH_STYLE_PWD_DIR_LENGTH: i64 = 0;
|
||||
|
||||
let mut module = context.new_module("directory");
|
||||
let module_color = module
|
||||
.config_value_style("style")
|
||||
.unwrap_or_else(|| Color::Cyan.bold());
|
||||
module.set_style(module_color);
|
||||
|
||||
let truncation_length = module
|
||||
.config_value_i64("truncation_length")
|
||||
.unwrap_or(DIR_TRUNCATION_LENGTH);
|
||||
let truncate_to_repo = module.config_value_bool("truncate_to_repo").unwrap_or(true);
|
||||
let fish_style_pwd_dir_length = module
|
||||
.config_value_i64("fish_style_pwd_dir_length")
|
||||
.unwrap_or(FISH_STYLE_PWD_DIR_LENGTH);
|
||||
|
||||
// Using environment PWD is the standard approach for determining logical path
|
||||
let use_logical_path = module.config_value_bool("use_logical_path").unwrap_or(true);
|
||||
// If this is None for any reason, we fall back to reading the os-provided path
|
||||
let logical_current_dir = if use_logical_path {
|
||||
match std::env::var("PWD") {
|
||||
Ok(x) => Some(x),
|
||||
Err(_) => {
|
||||
log::debug!("Asked for logical path, but PWD was invalid.");
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let current_dir = logical_current_dir
|
||||
.as_ref()
|
||||
.map(|d| Path::new(d))
|
||||
.unwrap_or_else(|| context.current_dir.as_ref());
|
||||
|
||||
let home_dir = dirs::home_dir().unwrap();
|
||||
log::debug!("Current directory: {:?}", current_dir);
|
||||
|
||||
let repo = &context.get_repo().ok()?;
|
||||
|
||||
let dir_string = match &repo.root {
|
||||
Some(repo_root) if truncate_to_repo && (repo_root != &home_dir) => {
|
||||
let repo_folder_name = repo_root.file_name().unwrap().to_str().unwrap();
|
||||
|
||||
// Contract the path to the git repo root
|
||||
contract_path(current_dir, repo_root, repo_folder_name)
|
||||
}
|
||||
// Contract the path to the home directory
|
||||
_ => contract_path(current_dir, &home_dir, HOME_SYMBOL),
|
||||
};
|
||||
|
||||
// Truncate the dir string to the maximum number of path components
|
||||
let truncated_dir_string = truncate(dir_string, truncation_length as usize);
|
||||
|
||||
if fish_style_pwd_dir_length > 0 {
|
||||
// If user is using fish style path, we need to add the segment first
|
||||
let contracted_home_dir = contract_path(¤t_dir, &home_dir, HOME_SYMBOL);
|
||||
let fish_style_dir = to_fish_style(
|
||||
fish_style_pwd_dir_length as usize,
|
||||
contracted_home_dir,
|
||||
&truncated_dir_string,
|
||||
);
|
||||
|
||||
module.new_segment("path", &fish_style_dir);
|
||||
}
|
||||
|
||||
module.new_segment("path", &truncated_dir_string);
|
||||
|
||||
module.get_prefix().set_value("in ");
|
||||
|
||||
Some(module)
|
||||
}
|
||||
|
||||
/// Contract the root component of a path
|
||||
///
|
||||
/// Replaces the `top_level_path` in a given `full_path` with the provided
|
||||
/// `top_level_replacement`.
|
||||
fn contract_path(full_path: &Path, top_level_path: &Path, top_level_replacement: &str) -> String {
|
||||
if !full_path.starts_with(top_level_path) {
|
||||
return replace_c_dir(full_path.to_slash().unwrap());
|
||||
}
|
||||
|
||||
if full_path == top_level_path {
|
||||
return replace_c_dir(top_level_replacement.to_string());
|
||||
}
|
||||
|
||||
format!(
|
||||
"{replacement}{separator}{path}",
|
||||
replacement = top_level_replacement,
|
||||
separator = "/",
|
||||
path = replace_c_dir(
|
||||
full_path
|
||||
.strip_prefix(top_level_path)
|
||||
.unwrap()
|
||||
.to_slash()
|
||||
.unwrap()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// Replaces "C://" with "/c/" within a Windows path
|
||||
///
|
||||
/// On non-Windows OS, does nothing
|
||||
#[cfg(target_os = "windows")]
|
||||
fn replace_c_dir(path: String) -> String {
|
||||
return path.replace("C:/", "/c");
|
||||
}
|
||||
|
||||
/// Replaces "C://" with "/c/" within a Windows path
|
||||
///
|
||||
/// On non-Windows OS, does nothing
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
const fn replace_c_dir(path: String) -> String {
|
||||
path
|
||||
}
|
||||
|
||||
/// Truncate a path to only have a set number of path components
|
||||
///
|
||||
/// Will truncate a path to only show the last `length` components in a path.
|
||||
/// If a length of `0` is provided, the path will not be truncated.
|
||||
fn truncate(dir_string: String, length: usize) -> String {
|
||||
if length == 0 {
|
||||
return dir_string;
|
||||
}
|
||||
|
||||
let components = dir_string.split('/').collect::<Vec<&str>>();
|
||||
if components.len() <= length {
|
||||
return dir_string;
|
||||
}
|
||||
|
||||
let truncated_components = &components[components.len() - length..];
|
||||
truncated_components.join("/")
|
||||
}
|
||||
|
||||
/// Takes part before contracted path and replaces it with fish style path
|
||||
///
|
||||
/// Will take the first letter of each directory before the contracted path and
|
||||
/// use that in the path instead. See the following example.
|
||||
///
|
||||
/// Absolute Path: `/Users/Bob/Projects/work/a_repo`
|
||||
/// Contracted Path: `a_repo`
|
||||
/// With Fish Style: `~/P/w/a_repo`
|
||||
///
|
||||
/// Absolute Path: `/some/Path/not/in_a/repo/but_nested`
|
||||
/// Contracted Path: `in_a/repo/but_nested`
|
||||
/// With Fish Style: `/s/P/n/in_a/repo/but_nested`
|
||||
fn to_fish_style(pwd_dir_length: usize, dir_string: String, truncated_dir_string: &str) -> String {
|
||||
let replaced_dir_string = dir_string.trim_end_matches(truncated_dir_string).to_owned();
|
||||
let components = replaced_dir_string.split('/').collect::<Vec<&str>>();
|
||||
|
||||
if components.is_empty() {
|
||||
return replaced_dir_string;
|
||||
}
|
||||
|
||||
components
|
||||
.into_iter()
|
||||
.map(|word| match word {
|
||||
"" => "",
|
||||
_ if word.len() <= pwd_dir_length => word,
|
||||
_ if word.starts_with('.') => &word[..=pwd_dir_length],
|
||||
_ => &word[..pwd_dir_length],
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("/")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn contract_home_directory() {
|
||||
let full_path = Path::new("/Users/astronaut/schematics/rocket");
|
||||
let home = Path::new("/Users/astronaut");
|
||||
|
||||
let output = contract_path(full_path, home, "~");
|
||||
assert_eq!(output, "~/schematics/rocket");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contract_repo_directory() {
|
||||
let full_path = Path::new("/Users/astronaut/dev/rocket-controls/src");
|
||||
let repo_root = Path::new("/Users/astronaut/dev/rocket-controls");
|
||||
|
||||
let output = contract_path(full_path, repo_root, "rocket-controls");
|
||||
assert_eq!(output, "rocket-controls/src");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "windows")]
|
||||
fn contract_windows_style_home_directory() {
|
||||
let full_path = Path::new("C:\\Users\\astronaut\\schematics\\rocket");
|
||||
let home = Path::new("C:\\Users\\astronaut");
|
||||
|
||||
let output = contract_path(full_path, home, "~");
|
||||
assert_eq!(output, "~/schematics/rocket");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "windows")]
|
||||
fn contract_windows_style_repo_directory() {
|
||||
let full_path = Path::new("C:\\Users\\astronaut\\dev\\rocket-controls\\src");
|
||||
let repo_root = Path::new("C:\\Users\\astronaut\\dev\\rocket-controls");
|
||||
|
||||
let output = contract_path(full_path, repo_root, "rocket-controls");
|
||||
assert_eq!(output, "rocket-controls/src");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "windows")]
|
||||
fn contract_windows_style_no_top_level_directory() {
|
||||
let full_path = Path::new("C:\\Some\\Other\\Path");
|
||||
let top_level_path = Path::new("C:\\Users\\astronaut");
|
||||
|
||||
let output = contract_path(full_path, top_level_path, "~");
|
||||
assert_eq!(output, "/c/Some/Other/Path");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "windows")]
|
||||
fn contract_windows_style_root_directory() {
|
||||
let full_path = Path::new("C:\\");
|
||||
let top_level_path = Path::new("C:\\Users\\astronaut");
|
||||
|
||||
let output = contract_path(full_path, top_level_path, "~");
|
||||
assert_eq!(output, "/c");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncate_smaller_path_than_provided_length() {
|
||||
let path = "~/starship";
|
||||
let output = truncate(path.to_string(), 3);
|
||||
assert_eq!(output, "~/starship")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncate_same_path_as_provided_length() {
|
||||
let path = "~/starship/engines";
|
||||
let output = truncate(path.to_string(), 3);
|
||||
assert_eq!(output, "~/starship/engines")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncate_slightly_larger_path_than_provided_length() {
|
||||
let path = "~/starship/engines/booster";
|
||||
let output = truncate(path.to_string(), 3);
|
||||
assert_eq!(output, "starship/engines/booster")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncate_larger_path_than_provided_length() {
|
||||
let path = "~/starship/engines/booster/rocket";
|
||||
let output = truncate(path.to_string(), 3);
|
||||
assert_eq!(output, "engines/booster/rocket")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fish_style_with_user_home_contracted_path() {
|
||||
let path = "~/starship/engines/booster/rocket";
|
||||
let output = to_fish_style(1, path.to_string(), "engines/booster/rocket");
|
||||
assert_eq!(output, "~/s/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fish_style_with_user_home_contracted_path_and_dot_dir() {
|
||||
let path = "~/.starship/engines/booster/rocket";
|
||||
let output = to_fish_style(1, path.to_string(), "engines/booster/rocket");
|
||||
assert_eq!(output, "~/.s/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fish_style_with_no_contracted_path() {
|
||||
// `truncatation_length = 2`
|
||||
let path = "/absolute/Path/not/in_a/repo/but_nested";
|
||||
let output = to_fish_style(1, path.to_string(), "repo/but_nested");
|
||||
assert_eq!(output, "/a/P/n/i/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fish_style_with_pwd_dir_len_no_contracted_path() {
|
||||
// `truncatation_length = 2`
|
||||
let path = "/absolute/Path/not/in_a/repo/but_nested";
|
||||
let output = to_fish_style(2, path.to_string(), "repo/but_nested");
|
||||
assert_eq!(output, "/ab/Pa/no/in/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fish_style_with_duplicate_directories() {
|
||||
let path = "~/starship/tmp/C++/C++/C++";
|
||||
let output = to_fish_style(1, path.to_string(), "C++");
|
||||
assert_eq!(output, "~/s/t/C/C/");
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
use ansi_term::Color;
|
||||
use std::env;
|
||||
|
||||
use super::{Context, Module};
|
||||
|
||||
/// Creates a module with the value of the chosen environment variable
|
||||
///
|
||||
/// Will display the environment variable's value if all of the following criteria are met:
|
||||
/// - env_var.disabled is absent or false
|
||||
/// - env_var.variable is defined
|
||||
/// - a variable named as the value of env_var.variable is defined
|
||||
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
||||
let mut module = context.new_module("env_var");
|
||||
let module_style = module
|
||||
.config_value_style("style")
|
||||
.unwrap_or_else(|| Color::Black.bold().dimmed());
|
||||
|
||||
let env_name = module.config_value_str("variable")?;
|
||||
|
||||
let default_value = module.config_value_str("default");
|
||||
|
||||
let env_value = get_env_value(env_name, default_value)?;
|
||||
|
||||
let prefix = module.config_value_str("prefix").unwrap_or("").to_owned();
|
||||
let suffix = module.config_value_str("suffix").unwrap_or("").to_owned();
|
||||
|
||||
module.set_style(module_style);
|
||||
module.get_prefix().set_value("with ");
|
||||
module.new_segment_if_config_exists("symbol");
|
||||
module.new_segment("env_var", &format!("{}{}{}", prefix, env_value, suffix));
|
||||
|
||||
Some(module)
|
||||
}
|
||||
|
||||
fn get_env_value(name: &str, default: Option<&str>) -> Option<String> {
|
||||
match env::var_os(name) {
|
||||
Some(os_value) => match os_value.into_string() {
|
||||
Ok(value) => Some(value),
|
||||
Err(_error) => None,
|
||||
},
|
||||
None => default.map(|value| value.to_owned()),
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
use ansi_term::Color;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
use super::{Context, Module};
|
||||
|
||||
/// Creates a module with the Git branch in the current directory
|
||||
///
|
||||
/// Will display the branch name if the current directory is a git repo
|
||||
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
||||
const GIT_BRANCH_CHAR: &str = " ";
|
||||
|
||||
let mut module = context.new_module("git_branch");
|
||||
|
||||
let segment_color = module
|
||||
.config_value_style("style")
|
||||
.unwrap_or_else(|| Color::Purple.bold());
|
||||
module.set_style(segment_color);
|
||||
module.get_prefix().set_value("on ");
|
||||
|
||||
let unsafe_truncation_length = module
|
||||
.config_value_i64("truncation_length")
|
||||
.unwrap_or(std::i64::MAX);
|
||||
let truncation_symbol = get_graphemes(
|
||||
module.config_value_str("truncation_symbol").unwrap_or("…"),
|
||||
1,
|
||||
);
|
||||
|
||||
module.new_segment("symbol", GIT_BRANCH_CHAR);
|
||||
|
||||
// TODO: Once error handling is implemented, warn the user if their config
|
||||
// truncation length is nonsensical
|
||||
let len = if unsafe_truncation_length <= 0 {
|
||||
log::debug!(
|
||||
"[WARN]: \"truncation_length\" should be a positive value, found {}",
|
||||
unsafe_truncation_length
|
||||
);
|
||||
std::usize::MAX
|
||||
} else {
|
||||
unsafe_truncation_length as usize
|
||||
};
|
||||
let repo = context.get_repo().ok()?;
|
||||
let branch_name = repo.branch.as_ref()?;
|
||||
let truncated_graphemes = get_graphemes(&branch_name, len);
|
||||
// The truncation symbol should only be added if we truncated
|
||||
let truncated_and_symbol = if len < graphemes_len(&branch_name) {
|
||||
truncated_graphemes + &truncation_symbol
|
||||
} else {
|
||||
truncated_graphemes
|
||||
};
|
||||
|
||||
module.new_segment("name", &truncated_and_symbol);
|
||||
|
||||
Some(module)
|
||||
}
|
||||
|
||||
fn get_graphemes(text: &str, length: usize) -> String {
|
||||
UnicodeSegmentation::graphemes(text, true)
|
||||
.take(length)
|
||||
.collect::<Vec<&str>>()
|
||||
.concat()
|
||||
}
|
||||
|
||||
fn graphemes_len(text: &str) -> usize {
|
||||
UnicodeSegmentation::graphemes(&text[..], true).count()
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
use ansi_term::Color;
|
||||
use git2::RepositoryState;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use super::{Context, Module};
|
||||
|
||||
/// Creates a module with the state of the git repository at the current directory
|
||||
///
|
||||
/// During a git operation it will show: REBASING, BISECTING, MERGING, etc.
|
||||
/// If the progress information is available (e.g. rebasing 3/10), it will show that too.
|
||||
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
||||
let mut module = context.new_module("git_state");
|
||||
|
||||
let repo = context.get_repo().ok()?;
|
||||
let repo_root = repo.root.as_ref()?;
|
||||
let repo_state = repo.state?;
|
||||
let state_description = get_state_description(repo_state, repo_root);
|
||||
|
||||
if let StateDescription::Clean = state_description {
|
||||
return None;
|
||||
}
|
||||
|
||||
let module_style = module
|
||||
.config_value_style("style")
|
||||
.unwrap_or_else(|| Color::Yellow.bold());
|
||||
module.set_style(module_style);
|
||||
module.get_prefix().set_value("(");
|
||||
module.get_suffix().set_value(") ");
|
||||
|
||||
let label = match state_description {
|
||||
StateDescription::Label(label) => label,
|
||||
StateDescription::LabelAndProgress(label, _) => label,
|
||||
// Should only be possible if you've added a new variant to StateDescription
|
||||
_ => panic!("Expected to have a label at this point in the control flow."),
|
||||
};
|
||||
|
||||
module.new_segment(label.segment_name, label.message_default);
|
||||
|
||||
if let StateDescription::LabelAndProgress(_, progress) = state_description {
|
||||
module.new_segment("progress_current", &format!(" {}", progress.current));
|
||||
module.new_segment("progress_divider", "/");
|
||||
module.new_segment("progress_total", &format!("{}", progress.total));
|
||||
}
|
||||
|
||||
Some(module)
|
||||
}
|
||||
|
||||
static MERGE_LABEL: StateLabel = StateLabel {
|
||||
segment_name: "merge",
|
||||
message_default: "MERGING",
|
||||
};
|
||||
|
||||
static REVERT_LABEL: StateLabel = StateLabel {
|
||||
segment_name: "revert",
|
||||
message_default: "REVERTING",
|
||||
};
|
||||
|
||||
static CHERRY_LABEL: StateLabel = StateLabel {
|
||||
segment_name: "cherry_pick",
|
||||
message_default: "CHERRY-PICKING",
|
||||
};
|
||||
|
||||
static BISECT_LABEL: StateLabel = StateLabel {
|
||||
segment_name: "bisect",
|
||||
message_default: "BISECTING",
|
||||
};
|
||||
|
||||
static AM_LABEL: StateLabel = StateLabel {
|
||||
segment_name: "am",
|
||||
message_default: "AM",
|
||||
};
|
||||
|
||||
static REBASE_LABEL: StateLabel = StateLabel {
|
||||
segment_name: "rebase",
|
||||
message_default: "REBASING",
|
||||
};
|
||||
|
||||
static AM_OR_REBASE_LABEL: StateLabel = StateLabel {
|
||||
segment_name: "am_or_rebase",
|
||||
message_default: "AM/REBASE",
|
||||
};
|
||||
|
||||
/// Returns the state of the current repository
|
||||
///
|
||||
/// During a git operation it will show: REBASING, BISECTING, MERGING, etc.
|
||||
fn get_state_description(state: RepositoryState, root: &PathBuf) -> StateDescription {
|
||||
match state {
|
||||
RepositoryState::Clean => StateDescription::Clean,
|
||||
RepositoryState::Merge => StateDescription::Label(&MERGE_LABEL),
|
||||
RepositoryState::Revert => StateDescription::Label(&REVERT_LABEL),
|
||||
RepositoryState::RevertSequence => StateDescription::Label(&REVERT_LABEL),
|
||||
RepositoryState::CherryPick => StateDescription::Label(&CHERRY_LABEL),
|
||||
RepositoryState::CherryPickSequence => StateDescription::Label(&CHERRY_LABEL),
|
||||
RepositoryState::Bisect => StateDescription::Label(&BISECT_LABEL),
|
||||
RepositoryState::ApplyMailbox => StateDescription::Label(&AM_LABEL),
|
||||
RepositoryState::ApplyMailboxOrRebase => StateDescription::Label(&AM_OR_REBASE_LABEL),
|
||||
RepositoryState::Rebase => describe_rebase(root),
|
||||
RepositoryState::RebaseInteractive => describe_rebase(root),
|
||||
RepositoryState::RebaseMerge => describe_rebase(root),
|
||||
}
|
||||
}
|
||||
|
||||
fn describe_rebase(root: &PathBuf) -> StateDescription {
|
||||
/*
|
||||
* Sadly, libgit2 seems to have some issues with reading the state of
|
||||
* interactive rebases. So, instead, we'll poke a few of the .git files
|
||||
* ourselves. This might be worth re-visiting this in the future...
|
||||
*
|
||||
* The following is based heavily on: https://github.com/magicmonty/bash-git-prompt
|
||||
*/
|
||||
|
||||
let just_label = StateDescription::Label(&REBASE_LABEL);
|
||||
|
||||
let dot_git = root.join(".git");
|
||||
|
||||
let has_path = |relative_path: &str| {
|
||||
let path = dot_git.join(Path::new(relative_path));
|
||||
path.exists()
|
||||
};
|
||||
|
||||
let file_to_usize = |relative_path: &str| {
|
||||
let path = dot_git.join(Path::new(relative_path));
|
||||
let contents = crate::utils::read_file(path).ok()?;
|
||||
let quantity = contents.trim().parse::<usize>().ok()?;
|
||||
Some(quantity)
|
||||
};
|
||||
|
||||
let paths_to_progress = |current_path: &str, total_path: &str| {
|
||||
let current = file_to_usize(current_path)?;
|
||||
let total = file_to_usize(total_path)?;
|
||||
Some(StateProgress { current, total })
|
||||
};
|
||||
|
||||
let progress = if has_path("rebase-merge") {
|
||||
paths_to_progress("rebase-merge/msgnum", "rebase-merge/end")
|
||||
} else if has_path("rebase-apply") {
|
||||
paths_to_progress("rebase-apply/next", "rebase-apply/last")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
match progress {
|
||||
None => just_label,
|
||||
Some(progress) => StateDescription::LabelAndProgress(&REBASE_LABEL, progress),
|
||||
}
|
||||
}
|
||||
|
||||
enum StateDescription {
|
||||
Clean,
|
||||
Label(&'static StateLabel),
|
||||
LabelAndProgress(&'static StateLabel, StateProgress),
|
||||
}
|
||||
|
||||
struct StateLabel {
|
||||
segment_name: &'static str,
|
||||
message_default: &'static str,
|
||||
}
|
||||
|
||||
struct StateProgress {
|
||||
current: usize,
|
||||
total: usize,
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
use ansi_term::Color;
|
||||
use git2::{Repository, Status};
|
||||
|
||||
use super::{Context, Module};
|
||||
|
||||
/// Creates a module with the Git branch in the current directory
|
||||
///
|
||||
/// Will display the branch name if the current directory is a git repo
|
||||
/// By default, the following symbols will be used to represent the repo's status:
|
||||
/// - `=` – This branch has merge conflicts
|
||||
/// - `⇡` – This branch is ahead of the branch being tracked
|
||||
/// - `⇣` – This branch is behind of the branch being tracked
|
||||
/// - `⇕` – This branch has diverged from the branch being tracked
|
||||
/// - `?` — There are untracked files in the working directory
|
||||
/// - `$` — A stash exists for the local repository
|
||||
/// - `!` — There are file modifications in the working directory
|
||||
/// - `+` — A new file has been added to the staging area
|
||||
/// - `»` — A renamed file has been added to the staging area
|
||||
/// - `✘` — A file's deletion has been added to the staging area
|
||||
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
||||
// This is the order that the sections will appear in
|
||||
const GIT_STATUS_CONFLICTED: &str = "=";
|
||||
const GIT_STATUS_AHEAD: &str = "⇡";
|
||||
const GIT_STATUS_BEHIND: &str = "⇣";
|
||||
const GIT_STATUS_DIVERGED: &str = "⇕";
|
||||
const GIT_STATUS_UNTRACKED: &str = "?";
|
||||
const GIT_STATUS_STASHED: &str = "$";
|
||||
const GIT_STATUS_MODIFIED: &str = "!";
|
||||
const GIT_STATUS_ADDED: &str = "+";
|
||||
const GIT_STATUS_RENAMED: &str = "»";
|
||||
const GIT_STATUS_DELETED: &str = "✘";
|
||||
const PREFIX: &str = "[";
|
||||
const SUFFIX: &str = "] ";
|
||||
|
||||
let repo = context.get_repo().ok()?;
|
||||
let branch_name = repo.branch.as_ref()?;
|
||||
let repo_root = repo.root.as_ref()?;
|
||||
let repository = Repository::open(repo_root).ok()?;
|
||||
|
||||
let mut module = context.new_module("git_status");
|
||||
let show_sync_count = module.config_value_bool("show_sync_count").unwrap_or(false);
|
||||
let module_style = module
|
||||
.config_value_style("style")
|
||||
.unwrap_or_else(|| Color::Red.bold());
|
||||
let start_symbol = module
|
||||
.config_value_str("prefix")
|
||||
.unwrap_or(PREFIX)
|
||||
.to_owned();
|
||||
let end_symbol = module
|
||||
.config_value_str("suffix")
|
||||
.unwrap_or(SUFFIX)
|
||||
.to_owned();
|
||||
|
||||
module
|
||||
.get_prefix()
|
||||
.set_value(start_symbol)
|
||||
.set_style(module_style);
|
||||
module
|
||||
.get_suffix()
|
||||
.set_value(end_symbol)
|
||||
.set_style(module_style);
|
||||
module.set_style(module_style);
|
||||
|
||||
let ahead_behind = get_ahead_behind(&repository, branch_name);
|
||||
if ahead_behind == Ok((0, 0)) {
|
||||
log::trace!("No ahead/behind found");
|
||||
} else {
|
||||
log::debug!("Repo ahead/behind: {:?}", ahead_behind);
|
||||
}
|
||||
|
||||
let stash_object = repository.revparse_single("refs/stash");
|
||||
if stash_object.is_ok() {
|
||||
log::debug!("Stash object: {:?}", stash_object);
|
||||
} else {
|
||||
log::trace!("No stash object found");
|
||||
}
|
||||
|
||||
let repo_status = get_repo_status(&repository);
|
||||
log::debug!("Repo status: {:?}", repo_status);
|
||||
|
||||
// Add the conflicted segment
|
||||
if let Ok(repo_status) = repo_status {
|
||||
if repo_status.is_conflicted() {
|
||||
module.new_segment("conflicted", GIT_STATUS_CONFLICTED);
|
||||
}
|
||||
}
|
||||
|
||||
// Add the ahead/behind segment
|
||||
if let Ok((ahead, behind)) = ahead_behind {
|
||||
let add_ahead = |m: &mut Module<'a>| {
|
||||
m.new_segment("ahead", GIT_STATUS_AHEAD);
|
||||
|
||||
if show_sync_count {
|
||||
m.new_segment("ahead_count", &ahead.to_string());
|
||||
}
|
||||
};
|
||||
|
||||
let add_behind = |m: &mut Module<'a>| {
|
||||
m.new_segment("behind", GIT_STATUS_BEHIND);
|
||||
|
||||
if show_sync_count {
|
||||
m.new_segment("behind_count", &behind.to_string());
|
||||
}
|
||||
};
|
||||
|
||||
if ahead > 0 && behind > 0 {
|
||||
module.new_segment("diverged", GIT_STATUS_DIVERGED);
|
||||
|
||||
if show_sync_count {
|
||||
add_ahead(&mut module);
|
||||
add_behind(&mut module);
|
||||
}
|
||||
}
|
||||
|
||||
if ahead > 0 && behind == 0 {
|
||||
add_ahead(&mut module);
|
||||
}
|
||||
|
||||
if behind > 0 && ahead == 0 {
|
||||
add_behind(&mut module);
|
||||
}
|
||||
}
|
||||
|
||||
// Add the stashed segment
|
||||
if stash_object.is_ok() {
|
||||
module.new_segment("stashed", GIT_STATUS_STASHED);
|
||||
}
|
||||
|
||||
// Add all remaining status segments
|
||||
if let Ok(repo_status) = repo_status {
|
||||
if repo_status.is_wt_deleted() || repo_status.is_index_deleted() {
|
||||
module.new_segment("deleted", GIT_STATUS_DELETED);
|
||||
}
|
||||
|
||||
if repo_status.is_wt_renamed() || repo_status.is_index_renamed() {
|
||||
module.new_segment("renamed", GIT_STATUS_RENAMED);
|
||||
}
|
||||
|
||||
if repo_status.is_wt_modified() {
|
||||
module.new_segment("modified", GIT_STATUS_MODIFIED);
|
||||
}
|
||||
|
||||
if repo_status.is_index_modified() || repo_status.is_index_new() {
|
||||
module.new_segment("staged", GIT_STATUS_ADDED);
|
||||
}
|
||||
|
||||
if repo_status.is_wt_new() {
|
||||
module.new_segment("untracked", GIT_STATUS_UNTRACKED);
|
||||
}
|
||||
}
|
||||
|
||||
if module.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(module)
|
||||
}
|
||||
|
||||
/// Gets the bitflags associated with the repo's git status
|
||||
fn get_repo_status(repository: &Repository) -> Result<Status, git2::Error> {
|
||||
let mut status_options = git2::StatusOptions::new();
|
||||
|
||||
match repository.config()?.get_entry("status.showUntrackedFiles") {
|
||||
Ok(entry) => status_options.include_untracked(entry.value() != Some("no")),
|
||||
_ => status_options.include_untracked(true),
|
||||
};
|
||||
status_options.renames_from_rewrites(true);
|
||||
status_options.renames_head_to_index(true);
|
||||
status_options.renames_index_to_workdir(true);
|
||||
|
||||
let repo_file_statuses = repository.statuses(Some(&mut status_options))?;
|
||||
|
||||
// Statuses are stored as bitflags, so use BitOr to join them all into a single value
|
||||
let repo_status: Status = repo_file_statuses.iter().map(|e| e.status()).collect();
|
||||
if repo_status.is_empty() {
|
||||
return Err(git2::Error::from_str("Repo has no status"));
|
||||
}
|
||||
|
||||
Ok(repo_status)
|
||||
}
|
||||
|
||||
/// Compares the current branch with the branch it is tracking to determine how
|
||||
/// far ahead or behind it is in relation
|
||||
fn get_ahead_behind(
|
||||
repository: &Repository,
|
||||
branch_name: &str,
|
||||
) -> Result<(usize, usize), git2::Error> {
|
||||
let branch_object = repository.revparse_single(branch_name)?;
|
||||
let tracking_branch_name = format!("{}@{{upstream}}", branch_name);
|
||||
let tracking_object = repository.revparse_single(&tracking_branch_name)?;
|
||||
|
||||
let branch_oid = branch_object.id();
|
||||
let tracking_oid = tracking_object.id();
|
||||
|
||||
repository.graph_ahead_behind(branch_oid, tracking_oid)
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
use ansi_term::Color;
|
||||
use std::process::Command;
|
||||
|
||||
use super::{Context, Module};
|
||||
|
||||
/// Creates a module with the current Go version
|
||||
///
|
||||
/// Will display the Go version if any of the following criteria are met:
|
||||
/// - Current directory contains a `go.mod` file
|
||||
/// - Current directory contains a `go.sum` file
|
||||
/// - Current directory contains a `glide.yaml` file
|
||||
/// - Current directory contains a `Gopkg.yml` file
|
||||
/// - Current directory contains a `Gopkg.lock` file
|
||||
/// - Current directory contains a `Godeps` directory
|
||||
/// - Current directory contains a file with the `.go` extension
|
||||
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
||||
let is_go_project = context
|
||||
.try_begin_scan()?
|
||||
.set_files(&["go.mod", "go.sum", "glide.yaml", "Gopkg.yml", "Gopkg.lock"])
|
||||
.set_extensions(&["go"])
|
||||
.set_folders(&["Godeps"])
|
||||
.is_match();
|
||||
|
||||
if !is_go_project {
|
||||
return None;
|
||||
}
|
||||
|
||||
match get_go_version() {
|
||||
Some(go_version) => {
|
||||
const GO_CHAR: &str = "🐹 ";
|
||||
|
||||
let mut module = context.new_module("golang");
|
||||
let module_style = module
|
||||
.config_value_style("style")
|
||||
.unwrap_or_else(|| Color::Cyan.bold());
|
||||
module.set_style(module_style);
|
||||
|
||||
let formatted_version = format_go_version(&go_version)?;
|
||||
module.new_segment("symbol", GO_CHAR);
|
||||
module.new_segment("version", &formatted_version);
|
||||
|
||||
Some(module)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_go_version() -> Option<String> {
|
||||
Command::new("go")
|
||||
.arg("version")
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|output| String::from_utf8(output.stdout).ok())
|
||||
}
|
||||
|
||||
fn format_go_version(go_stdout: &str) -> Option<String> {
|
||||
let version = go_stdout
|
||||
// split into ["", "1.12.4 linux/amd64"]
|
||||
.splitn(2, "go version go")
|
||||
// return "1.12.4 linux/amd64"
|
||||
.nth(1)?
|
||||
// split into ["1.12.4", "linux/amd64"]
|
||||
.split_whitespace()
|
||||
// return "1.12.4"
|
||||
.next()?;
|
||||
|
||||
let mut formatted_version = String::with_capacity(version.len() + 1);
|
||||
formatted_version.push('v');
|
||||
formatted_version.push_str(version);
|
||||
Some(formatted_version)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_format_go_version() {
|
||||
let input = "go version go1.12 darwin/amd64";
|
||||
assert_eq!(format_go_version(input), Some("v1.12".to_string()));
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
use ansi_term::Color;
|
||||
use std::env;
|
||||
|
||||
use super::{Context, Module};
|
||||
use std::ffi::OsString;
|
||||
|
||||
/// Creates a module with the system hostname
|
||||
///
|
||||
/// Will display the hostname if all of the following criteria are met:
|
||||
/// - hostname.disabled is absent or false
|
||||
/// - hostname.ssh_only is false OR the user is currently connected as an SSH session (`$SSH_CONNECTION`)
|
||||
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
||||
let mut module = context.new_module("hostname");
|
||||
let module_style = module
|
||||
.config_value_style("style")
|
||||
.unwrap_or_else(|| Color::Green.bold().dimmed());
|
||||
|
||||
let ssh_connection = env::var("SSH_CONNECTION").ok();
|
||||
if module.config_value_bool("ssh_only").unwrap_or(true) && ssh_connection.is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let os_hostname: OsString = gethostname::gethostname();
|
||||
|
||||
let host = match os_hostname.into_string() {
|
||||
Ok(host) => host,
|
||||
Err(bad) => {
|
||||
log::debug!("hostname is not valid UTF!\n{:?}", bad);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let prefix = module.config_value_str("prefix").unwrap_or("").to_owned();
|
||||
let suffix = module.config_value_str("suffix").unwrap_or("").to_owned();
|
||||
|
||||
module.set_style(module_style);
|
||||
module.new_segment("hostname", &format!("{}{}{}", prefix, host, suffix));
|
||||
module.get_prefix().set_value("on ");
|
||||
|
||||
Some(module)
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
use std::process::Command;
|
||||
|
||||
use ansi_term::Color;
|
||||
|
||||
use super::{Context, Module};
|
||||
|
||||
/// Creates a module with the current Java version
|
||||
///
|
||||
/// Will display the Java version if any of the following criteria are met:
|
||||
/// - Current directory contains a file with a `.java`, `.class` or `.jar` extension
|
||||
/// - Current directory contains a `pom.xml` or `build.gradle` file
|
||||
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
||||
let is_java_project = context
|
||||
.try_begin_scan()?
|
||||
.set_files(&["pom.xml", "build.gradle"])
|
||||
.set_extensions(&["java", "class", "jar"])
|
||||
.is_match();
|
||||
|
||||
if !is_java_project {
|
||||
return None;
|
||||
}
|
||||
|
||||
match get_java_version() {
|
||||
Some(java_version) => {
|
||||
const JAVA_CHAR: &str = "☕ ";
|
||||
|
||||
let mut module = context.new_module("java");
|
||||
let module_style = module
|
||||
.config_value_style("style")
|
||||
.unwrap_or_else(|| Color::Red.dimmed());
|
||||
module.set_style(module_style);
|
||||
|
||||
let formatted_version = format_java_version(java_version)?;
|
||||
module.new_segment("symbol", JAVA_CHAR);
|
||||
module.new_segment("version", &formatted_version);
|
||||
|
||||
Some(module)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_java_version() -> Option<String> {
|
||||
let java_command = match std::env::var("JAVA_HOME") {
|
||||
Ok(java_home) => format!("{}/bin/java", java_home),
|
||||
Err(_) => String::from("java"),
|
||||
};
|
||||
|
||||
match Command::new(java_command).arg("-Xinternalversion").output() {
|
||||
Ok(output) => Some(String::from_utf8(output.stdout).unwrap()),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the java version from `java_stdout`.
|
||||
/// The expected format is similar to: "JRE (1.8.0_222-b10)".
|
||||
/// Some Java vendors don't follow this format: "JRE (Zulu 8.40.0.25-CA-linux64)").
|
||||
fn format_java_version(java_stdout: String) -> Option<String> {
|
||||
let start = java_stdout.find("JRE (")? + "JRE (".len();
|
||||
let end = start
|
||||
+ (java_stdout[start..].find(|c| match c {
|
||||
'0'..='9' | '.' => false,
|
||||
_ => true,
|
||||
})?);
|
||||
|
||||
if start == end {
|
||||
None
|
||||
} else {
|
||||
Some(format!("v{}", &java_stdout[start..end]))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_format_java_version_openjdk() {
|
||||
let java_8 = String::from("OpenJDK 64-Bit Server VM (25.222-b10) for linux-amd64 JRE (1.8.0_222-b10), built on Jul 11 2019 10:18:43 by \"openjdk\" with gcc 4.4.7 20120313 (Red Hat 4.4.7-23)");
|
||||
let java_11 = String::from("OpenJDK 64-Bit Server VM (11.0.4+11-post-Ubuntu-1ubuntu219.04) for linux-amd64 JRE (11.0.4+11-post-Ubuntu-1ubuntu219.04), built on Jul 18 2019 18:21:46 by \"build\" with gcc 8.3.0");
|
||||
assert_eq!(format_java_version(java_11), Some(String::from("v11.0.4")));
|
||||
assert_eq!(format_java_version(java_8), Some(String::from("v1.8.0")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_java_version_oracle() {
|
||||
let java_8 = String::from("Java HotSpot(TM) Client VM (25.65-b01) for linux-arm-vfp-hflt JRE (1.8.0_65-b17), built on Oct 6 2015 16:19:04 by \"java_re\" with gcc 4.7.2 20120910 (prerelease)");
|
||||
assert_eq!(format_java_version(java_8), Some(String::from("v1.8.0")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_java_version_redhat() {
|
||||
let java_8 = String::from("OpenJDK 64-Bit Server VM (25.222-b10) for linux-amd64 JRE (1.8.0_222-b10), built on Jul 11 2019 20:48:53 by \"root\" with gcc 7.3.1 20180303 (Red Hat 7.3.1-5)");
|
||||
let java_12 = String::from("OpenJDK 64-Bit Server VM (12.0.2+10) for linux-amd64 JRE (12.0.2+10), built on Jul 18 2019 14:41:47 by \"jenkins\" with gcc 7.3.1 20180303 (Red Hat 7.3.1-5)");
|
||||
assert_eq!(format_java_version(java_8), Some(String::from("v1.8.0")));
|
||||
assert_eq!(format_java_version(java_12), Some(String::from("v12.0.2")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_java_version_zulu() {
|
||||
// Not currently supported
|
||||
let java_8 = String::from("OpenJDK 64-Bit Server VM (25.222-b10) for linux-amd64 JRE (Zulu 8.40.0.25-CA-linux64) (1.8.0_222-b10), built on Jul 11 2019 11:36:39 by \"zulu_re\" with gcc 4.4.7 20120313 (Red Hat 4.4.7-3)");
|
||||
assert_eq!(format_java_version(java_8), None);
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
use ansi_term::Color;
|
||||
|
||||
use super::{Context, Module};
|
||||
|
||||
/// Creates a segment to show if there are any active jobs running
|
||||
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
||||
let mut module = context.new_module("jobs");
|
||||
|
||||
let threshold = module.config_value_i64("threshold").unwrap_or(1);
|
||||
|
||||
const JOB_CHAR: &str = "✦";
|
||||
let module_style = module
|
||||
.config_value_style("style")
|
||||
.unwrap_or_else(|| Color::Blue.bold());
|
||||
module.set_style(module_style);
|
||||
|
||||
let arguments = &context.arguments;
|
||||
let num_of_jobs = arguments
|
||||
.value_of("jobs")
|
||||
.unwrap_or("0")
|
||||
.trim()
|
||||
.parse::<i64>()
|
||||
.ok()?;
|
||||
if num_of_jobs == 0 {
|
||||
return None;
|
||||
}
|
||||
module.new_segment("symbol", JOB_CHAR);
|
||||
if num_of_jobs > threshold {
|
||||
module.new_segment("number", &num_of_jobs.to_string());
|
||||
}
|
||||
module.get_prefix().set_value("");
|
||||
|
||||
Some(module)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
use super::{Context, Module};
|
||||
|
||||
/// Creates a module for the line break
|
||||
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
||||
const LINE_ENDING: &str = "\n";
|
||||
|
||||
let mut module = context.new_module("line_break");
|
||||
|
||||
module.get_prefix().set_value("");
|
||||
module.get_suffix().set_value("");
|
||||
|
||||
module.new_segment("character", LINE_ENDING);
|
||||
|
||||
Some(module)
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
use ansi_term::Color;
|
||||
|
||||
use super::{Context, Module};
|
||||
use byte_unit::{Byte, ByteUnit};
|
||||
use sysinfo::{RefreshKind, SystemExt};
|
||||
|
||||
/// Creates a module with system memory usage information
|
||||
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
||||
const DEFAULT_THRESHOLD: i64 = 75;
|
||||
const DEFAULT_SHOW_PERCENTAGE: bool = false;
|
||||
const RAM_CHAR: &str = "🐏 ";
|
||||
|
||||
let mut module = context.new_module("memory_usage");
|
||||
|
||||
if module.config_value_bool("disabled").unwrap_or(true) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let module_style = module
|
||||
.config_value_style("style")
|
||||
.unwrap_or_else(|| Color::White.bold().dimmed());
|
||||
|
||||
let system = sysinfo::System::new_with_specifics(RefreshKind::new().with_system());
|
||||
|
||||
let used_memory_kib = system.get_used_memory();
|
||||
let total_memory_kib = system.get_total_memory();
|
||||
let used_swap_kib = system.get_used_swap();
|
||||
let total_swap_kib = system.get_total_swap();
|
||||
|
||||
let percent_mem_used = (used_memory_kib as f64 / total_memory_kib as f64) * 100.;
|
||||
let percent_swap_used = (used_swap_kib as f64 / total_swap_kib as f64) * 100.;
|
||||
|
||||
let threshold = module
|
||||
.config_value_i64("threshold")
|
||||
.unwrap_or(DEFAULT_THRESHOLD);
|
||||
|
||||
if percent_mem_used.round() < threshold as f64 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let show_percentage = module
|
||||
.config_value_bool("show_percentage")
|
||||
.unwrap_or(DEFAULT_SHOW_PERCENTAGE);
|
||||
|
||||
let (display_mem, display_swap) = if show_percentage {
|
||||
(
|
||||
format!("{:.0}%", percent_mem_used),
|
||||
format!("{:.0}%", percent_swap_used),
|
||||
)
|
||||
} else {
|
||||
fn format_kib(n_kib: u64) -> String {
|
||||
let byte = Byte::from_unit(n_kib as f64, ByteUnit::KiB)
|
||||
.unwrap_or_else(|_| Byte::from_bytes(0));
|
||||
let mut display_bytes = byte.get_appropriate_unit(true).format(0);
|
||||
display_bytes.retain(|c| c != ' ');
|
||||
display_bytes
|
||||
}
|
||||
(
|
||||
format!(
|
||||
"{}/{}",
|
||||
format_kib(used_memory_kib),
|
||||
format_kib(total_memory_kib)
|
||||
),
|
||||
format!(
|
||||
"{}/{}",
|
||||
format_kib(used_swap_kib),
|
||||
format_kib(total_swap_kib)
|
||||
),
|
||||
)
|
||||
};
|
||||
|
||||
let show_swap = module
|
||||
.config_value_bool("show_swap")
|
||||
.unwrap_or(total_swap_kib != 0);
|
||||
|
||||
module.new_segment("symbol", RAM_CHAR);
|
||||
|
||||
module.set_style(module_style);
|
||||
if show_swap {
|
||||
module.new_segment(
|
||||
"memory_usage",
|
||||
&format!("{} | {}", display_mem, display_swap),
|
||||
);
|
||||
} else {
|
||||
module.new_segment("memory_usage", &display_mem);
|
||||
}
|
||||
|
||||
module.get_prefix().set_value("");
|
||||
|
||||
Some(module)
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
// While adding out new module add out module to src/module.rs ALL_MODULES const array also.
|
||||
mod aws;
|
||||
mod character;
|
||||
mod cmd_duration;
|
||||
mod directory;
|
||||
mod env_var;
|
||||
mod git_branch;
|
||||
mod git_state;
|
||||
mod git_status;
|
||||
mod golang;
|
||||
mod hostname;
|
||||
mod java;
|
||||
mod jobs;
|
||||
mod line_break;
|
||||
mod memory_usage;
|
||||
mod nix_shell;
|
||||
mod nodejs;
|
||||
mod package;
|
||||
mod python;
|
||||
mod ruby;
|
||||
mod rust;
|
||||
mod time;
|
||||
mod username;
|
||||
|
||||
#[cfg(feature = "battery")]
|
||||
mod battery;
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::module::Module;
|
||||
|
||||
pub fn handle<'a>(module: &str, context: &'a Context) -> Option<Module<'a>> {
|
||||
match module {
|
||||
"aws" => aws::module(context),
|
||||
"directory" => directory::module(context),
|
||||
"env_var" => env_var::module(context),
|
||||
"character" => character::module(context),
|
||||
"nodejs" => nodejs::module(context),
|
||||
"rust" => rust::module(context),
|
||||
"python" => python::module(context),
|
||||
"ruby" => ruby::module(context),
|
||||
"golang" => golang::module(context),
|
||||
"line_break" => line_break::module(context),
|
||||
"package" => package::module(context),
|
||||
"git_branch" => git_branch::module(context),
|
||||
"git_state" => git_state::module(context),
|
||||
"git_status" => git_status::module(context),
|
||||
"username" => username::module(context),
|
||||
#[cfg(feature = "battery")]
|
||||
"battery" => battery::module(context),
|
||||
"cmd_duration" => cmd_duration::module(context),
|
||||
"java" => java::module(context),
|
||||
"jobs" => jobs::module(context),
|
||||
"nix_shell" => nix_shell::module(context),
|
||||
"hostname" => hostname::module(context),
|
||||
"time" => time::module(context),
|
||||
"memory_usage" => memory_usage::module(context),
|
||||
|
||||
_ => {
|
||||
eprintln!("Error: Unknown module {}. Use starship module --list to list out all supported modules.", module);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
use ansi_term::Color;
|
||||
use std::env;
|
||||
|
||||
use super::{Context, Module};
|
||||
|
||||
// IN_NIX_SHELL should be "pure" or "impure" but lorri uses "1" for "impure"
|
||||
// https://github.com/target/lorri/issues/140
|
||||
|
||||
/// Creates a module showing if inside a nix-shell
|
||||
///
|
||||
/// The module will use the `$IN_NIX_SHELL` and `$name` environment variable to
|
||||
/// determine if it's inside a nix-shell and the name of it.
|
||||
///
|
||||
/// The following options are availables:
|
||||
/// - use_name (bool) // print the name of the nix-shell
|
||||
/// - impure_msg (string) // change the impure msg
|
||||
/// - pure_msg (string) // change the pure msg
|
||||
///
|
||||
/// Will display the following:
|
||||
/// - name (pure) // use_name == true in a pure nix-shell
|
||||
/// - name (impure) // use_name == true in an impure nix-shell
|
||||
/// - pure // use_name == false in a pure nix-shell
|
||||
/// - impure // use_name == false in an impure nix-shell
|
||||
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
||||
let mut module = context.new_module("nix_shell");
|
||||
|
||||
env::var("IN_NIX_SHELL")
|
||||
.ok()
|
||||
.and_then(|shell_type| {
|
||||
if shell_type == "1" || shell_type == "impure" {
|
||||
Some(module.config_value_str("impure_msg").unwrap_or("impure"))
|
||||
} else if shell_type == "pure" {
|
||||
Some(module.config_value_str("pure_msg").unwrap_or("pure"))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.map(|shell_type| {
|
||||
if module.config_value_bool("use_name").unwrap_or(false) {
|
||||
match env::var("name").ok() {
|
||||
Some(name) => format!("{} ({})", name, shell_type),
|
||||
None => shell_type.to_string(),
|
||||
}
|
||||
} else {
|
||||
shell_type.to_string()
|
||||
}
|
||||
})
|
||||
.map(|segment| {
|
||||
let module_style = module
|
||||
.config_value_style("style")
|
||||
.unwrap_or_else(|| Color::Red.bold());
|
||||
module.set_style(module_style);
|
||||
module.new_segment("nix_shell", &segment);
|
||||
module
|
||||
})
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
use ansi_term::Color;
|
||||
use std::process::Command;
|
||||
|
||||
use super::{Context, Module};
|
||||
|
||||
/// Creates a module with the current Node.js version
|
||||
///
|
||||
/// Will display the Node.js version if any of the following criteria are met:
|
||||
/// - Current directory contains a `.js` file
|
||||
/// - Current directory contains a `package.json` file
|
||||
/// - Current directory contains a `node_modules` directory
|
||||
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
||||
let is_js_project = context
|
||||
.try_begin_scan()?
|
||||
.set_files(&["package.json"])
|
||||
.set_extensions(&["js"])
|
||||
.set_folders(&["node_modules"])
|
||||
.is_match();
|
||||
|
||||
if !is_js_project {
|
||||
return None;
|
||||
}
|
||||
|
||||
match get_node_version() {
|
||||
Some(node_version) => {
|
||||
const NODE_CHAR: &str = "⬢ ";
|
||||
|
||||
let mut module = context.new_module("nodejs");
|
||||
let module_style = module
|
||||
.config_value_style("style")
|
||||
.unwrap_or_else(|| Color::Green.bold());
|
||||
module.set_style(module_style);
|
||||
|
||||
let formatted_version = node_version.trim();
|
||||
module.new_segment("symbol", NODE_CHAR);
|
||||
module.new_segment("version", formatted_version);
|
||||
|
||||
Some(module)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_node_version() -> Option<String> {
|
||||
match Command::new("node").arg("--version").output() {
|
||||
Ok(output) => Some(String::from_utf8(output.stdout).unwrap()),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
use super::{Context, Module};
|
||||
use crate::utils;
|
||||
|
||||
use ansi_term::Color;
|
||||
use serde_json as json;
|
||||
use toml;
|
||||
|
||||
/// Creates a module with the current package version
|
||||
///
|
||||
/// Will display if a version is defined for your Node.js or Rust project (if one exists)
|
||||
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
||||
match get_package_version() {
|
||||
Some(package_version) => {
|
||||
const PACKAGE_CHAR: &str = "📦 ";
|
||||
|
||||
let mut module = context.new_module("package");
|
||||
let module_style = module
|
||||
.config_value_style("style")
|
||||
.unwrap_or_else(|| Color::Red.bold());
|
||||
module.set_style(module_style);
|
||||
module.get_prefix().set_value("is ");
|
||||
|
||||
module.new_segment("symbol", PACKAGE_CHAR);
|
||||
module.new_segment("version", &package_version);
|
||||
|
||||
Some(module)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_cargo_version(file_contents: &str) -> Option<String> {
|
||||
let cargo_toml: toml::Value = toml::from_str(file_contents).ok()?;
|
||||
let raw_version = cargo_toml.get("package")?.get("version")?.as_str()?;
|
||||
|
||||
let formatted_version = format_version(raw_version);
|
||||
Some(formatted_version)
|
||||
}
|
||||
|
||||
fn extract_package_version(file_contents: &str) -> Option<String> {
|
||||
let package_json: json::Value = json::from_str(file_contents).ok()?;
|
||||
let raw_version = package_json.get("version")?.as_str()?;
|
||||
if raw_version == "null" {
|
||||
return None;
|
||||
};
|
||||
|
||||
let formatted_version = format_version(raw_version);
|
||||
Some(formatted_version)
|
||||
}
|
||||
|
||||
fn extract_poetry_version(file_contents: &str) -> Option<String> {
|
||||
let poetry_toml: toml::Value = toml::from_str(file_contents).ok()?;
|
||||
let raw_version = poetry_toml
|
||||
.get("tool")?
|
||||
.get("poetry")?
|
||||
.get("version")?
|
||||
.as_str()?;
|
||||
|
||||
let formatted_version = format_version(raw_version);
|
||||
Some(formatted_version)
|
||||
}
|
||||
|
||||
fn get_package_version() -> Option<String> {
|
||||
if let Ok(cargo_toml) = utils::read_file("Cargo.toml") {
|
||||
extract_cargo_version(&cargo_toml)
|
||||
} else if let Ok(package_json) = utils::read_file("package.json") {
|
||||
extract_package_version(&package_json)
|
||||
} else if let Ok(poetry_toml) = utils::read_file("pyproject.toml") {
|
||||
extract_poetry_version(&poetry_toml)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn format_version(version: &str) -> String {
|
||||
format!("v{}", version.replace('"', "").trim())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_format_version() {
|
||||
assert_eq!(format_version("0.1.0"), "v0.1.0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_cargo_version() {
|
||||
let cargo_with_version = toml::toml! {
|
||||
[package]
|
||||
name = "starship"
|
||||
version = "0.1.0"
|
||||
}
|
||||
.to_string();
|
||||
|
||||
let expected_version = Some("v0.1.0".to_string());
|
||||
assert_eq!(extract_cargo_version(&cargo_with_version), expected_version);
|
||||
|
||||
let cargo_without_version = toml::toml! {
|
||||
[package]
|
||||
name = "starship"
|
||||
}
|
||||
.to_string();
|
||||
|
||||
let expected_version = None;
|
||||
assert_eq!(
|
||||
extract_cargo_version(&cargo_without_version),
|
||||
expected_version
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_package_version() {
|
||||
let package_with_version = json::json!({
|
||||
"name": "spacefish",
|
||||
"version": "0.1.0"
|
||||
})
|
||||
.to_string();
|
||||
|
||||
let expected_version = Some("v0.1.0".to_string());
|
||||
assert_eq!(
|
||||
extract_package_version(&package_with_version),
|
||||
expected_version
|
||||
);
|
||||
|
||||
let package_without_version = json::json!({
|
||||
"name": "spacefish"
|
||||
})
|
||||
.to_string();
|
||||
|
||||
let expected_version = None;
|
||||
assert_eq!(
|
||||
extract_package_version(&package_without_version),
|
||||
expected_version
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_poetry_version() {
|
||||
let poetry_with_version = toml::toml! {
|
||||
[tool.poetry]
|
||||
name = "starship"
|
||||
version = "0.1.0"
|
||||
}
|
||||
.to_string();
|
||||
|
||||
let expected_version = Some("v0.1.0".to_string());
|
||||
assert_eq!(
|
||||
extract_poetry_version(&poetry_with_version),
|
||||
expected_version
|
||||
);
|
||||
|
||||
let poetry_without_version = toml::toml! {
|
||||
[tool.poetry]
|
||||
name = "starship"
|
||||
}
|
||||
.to_string();
|
||||
|
||||
let expected_version = None;
|
||||
assert_eq!(
|
||||
extract_poetry_version(&poetry_without_version),
|
||||
expected_version
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
use std::env;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
use ansi_term::Color;
|
||||
|
||||
use super::{Context, Module};
|
||||
|
||||
/// Creates a module with the current Python version
|
||||
///
|
||||
/// Will display the Python version if any of the following criteria are met:
|
||||
/// - Current directory contains a `.python-version` file
|
||||
/// - Current directory contains a `requirements.txt` file
|
||||
/// - Current directory contains a `pyproject.toml` file
|
||||
/// - Current directory contains a file with the `.py` extension
|
||||
/// - Current directory contains a `Pipfile` file
|
||||
/// - Current directory contains a `tox.ini` file
|
||||
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
||||
let is_py_project = context
|
||||
.try_begin_scan()?
|
||||
.set_files(&[
|
||||
"requirements.txt",
|
||||
".python-version",
|
||||
"pyproject.toml",
|
||||
"Pipfile",
|
||||
"tox.ini",
|
||||
])
|
||||
.set_extensions(&["py"])
|
||||
.is_match();
|
||||
|
||||
if !is_py_project {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut module = context.new_module("python");
|
||||
let pyenv_version_name = module
|
||||
.config_value_bool("pyenv_version_name")
|
||||
.unwrap_or(false);
|
||||
|
||||
const PYTHON_CHAR: &str = "🐍 ";
|
||||
let module_color = module
|
||||
.config_value_style("style")
|
||||
.unwrap_or_else(|| Color::Yellow.bold());
|
||||
module.set_style(module_color);
|
||||
module.new_segment("symbol", PYTHON_CHAR);
|
||||
|
||||
select_python_version(pyenv_version_name)
|
||||
.map(|python_version| python_module(module, pyenv_version_name, python_version))
|
||||
}
|
||||
|
||||
fn python_module(mut module: Module, pyenv_version_name: bool, python_version: String) -> Module {
|
||||
const PYENV_PREFIX: &str = "pyenv ";
|
||||
|
||||
if pyenv_version_name {
|
||||
module.new_segment("pyenv_prefix", PYENV_PREFIX);
|
||||
module.new_segment("version", &python_version.trim());
|
||||
} else {
|
||||
let formatted_version = format_python_version(&python_version);
|
||||
module.new_segment("version", &formatted_version);
|
||||
get_python_virtual_env()
|
||||
.map(|virtual_env| module.new_segment("virtualenv", &format!("({})", virtual_env)));
|
||||
};
|
||||
|
||||
module
|
||||
}
|
||||
|
||||
fn select_python_version(pyenv_version_name: bool) -> Option<String> {
|
||||
if pyenv_version_name {
|
||||
get_pyenv_version()
|
||||
} else {
|
||||
get_python_version()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_pyenv_version() -> Option<String> {
|
||||
Command::new("pyenv")
|
||||
.arg("version-name")
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|output| String::from_utf8(output.stdout).ok())
|
||||
}
|
||||
|
||||
fn get_python_version() -> Option<String> {
|
||||
match Command::new("python").arg("--version").output() {
|
||||
Ok(output) => {
|
||||
// We have to check both stdout and stderr since for Python versions
|
||||
// < 3.4, Python reports to stderr and for Python version >= 3.5,
|
||||
// Python reports to stdout
|
||||
if output.stdout.is_empty() {
|
||||
let stderr_string = String::from_utf8(output.stderr).unwrap();
|
||||
Some(stderr_string)
|
||||
} else {
|
||||
let stdout_string = String::from_utf8(output.stdout).unwrap();
|
||||
Some(stdout_string)
|
||||
}
|
||||
}
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn format_python_version(python_stdout: &str) -> String {
|
||||
format!("v{}", python_stdout.trim_start_matches("Python ").trim())
|
||||
}
|
||||
|
||||
fn get_python_virtual_env() -> Option<String> {
|
||||
env::var("VIRTUAL_ENV").ok().and_then(|venv| {
|
||||
Path::new(&venv)
|
||||
.file_name()
|
||||
.map(|filename| String::from(filename.to_str().unwrap_or("")))
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_format_python_version() {
|
||||
let input = "Python 3.7.2";
|
||||
assert_eq!(format_python_version(input), "v3.7.2");
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
use ansi_term::Color;
|
||||
use std::process::Command;
|
||||
|
||||
use super::{Context, Module};
|
||||
|
||||
/// Creates a module with the current Ruby version
|
||||
///
|
||||
/// Will display the Ruby version if any of the following criteria are met:
|
||||
/// - Current directory contains a `.rb` file
|
||||
/// - Current directory contains a `Gemfile` file
|
||||
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
||||
let is_rb_project = context
|
||||
.try_begin_scan()?
|
||||
.set_files(&["Gemfile"])
|
||||
.set_extensions(&["rb"])
|
||||
.is_match();
|
||||
|
||||
if !is_rb_project {
|
||||
return None;
|
||||
}
|
||||
|
||||
match get_ruby_version() {
|
||||
Some(ruby_version) => {
|
||||
const RUBY_CHAR: &str = "💎 ";
|
||||
|
||||
let mut module = context.new_module("ruby");
|
||||
let module_style = module
|
||||
.config_value_style("style")
|
||||
.unwrap_or_else(|| Color::Red.bold());
|
||||
module.set_style(module_style);
|
||||
|
||||
let formatted_version = format_ruby_version(&ruby_version)?;
|
||||
module.new_segment("symbol", RUBY_CHAR);
|
||||
module.new_segment("version", &formatted_version);
|
||||
|
||||
Some(module)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_ruby_version() -> Option<String> {
|
||||
match Command::new("ruby").arg("-v").output() {
|
||||
Ok(output) => Some(String::from_utf8(output.stdout).unwrap()),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn format_ruby_version(ruby_version: &str) -> Option<String> {
|
||||
let version = ruby_version
|
||||
// split into ["ruby", "2.6.0p0", "linux/amd64"]
|
||||
.split_whitespace()
|
||||
// return "2.6.0p0"
|
||||
.nth(1)?
|
||||
.get(0..5)?;
|
||||
|
||||
let mut formatted_version = String::with_capacity(version.len() + 1);
|
||||
formatted_version.push('v');
|
||||
formatted_version.push_str(version);
|
||||
Some(formatted_version)
|
||||
}
|
||||
@@ -1,291 +0,0 @@
|
||||
use ansi_term::Color;
|
||||
use std::ffi::OsStr;
|
||||
use std::path::Path;
|
||||
use std::process::{Command, Output};
|
||||
use std::{env, fs};
|
||||
|
||||
use super::{Context, Module};
|
||||
|
||||
/// 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"])
|
||||
.set_extensions(&["rs"])
|
||||
.is_match();
|
||||
|
||||
if !is_rs_project {
|
||||
return None;
|
||||
}
|
||||
|
||||
// `$CARGO_HOME/bin/rustc(.exe) --version` may attempt installing a rustup toolchain.
|
||||
// https://github.com/starship/starship/issues/417
|
||||
//
|
||||
// To display appropriate versions preventing `rustc` from downloading toolchains, we have to
|
||||
// check
|
||||
// 1. `$RUSTUP_TOOLCHAIN`
|
||||
// 2. `rustup override list`
|
||||
// 3. `rust-toolchain` in `.` or parent directories
|
||||
// as `rustup` does.
|
||||
// https://github.com/rust-lang/rustup.rs/tree/eb694fcada7becc5d9d160bf7c623abe84f8971d#override-precedence
|
||||
//
|
||||
// Probably we have no other way to know whether any toolchain override is specified for the
|
||||
// current directory. The following commands also cause toolchain installations.
|
||||
// - `rustup show`
|
||||
// - `rustup show active-toolchain`
|
||||
// - `rustup which`
|
||||
let module_version = if let Some(toolchain) = env_rustup_toolchain()
|
||||
.or_else(|| execute_rustup_override_list(&context.current_dir))
|
||||
.or_else(|| find_rust_toolchain_file(&context))
|
||||
{
|
||||
match execute_rustup_run_rustc_version(&toolchain) {
|
||||
RustupRunRustcVersionOutcome::RustcVersion(stdout) => format_rustc_version(stdout),
|
||||
RustupRunRustcVersionOutcome::ToolchainName(toolchain) => toolchain,
|
||||
RustupRunRustcVersionOutcome::RustupNotWorking => {
|
||||
// If `rustup` is not in `$PATH` or cannot be executed for other reasons, we can
|
||||
// safely execute `rustc --version`.
|
||||
format_rustc_version(execute_rustc_version()?)
|
||||
}
|
||||
RustupRunRustcVersionOutcome::Err => return None,
|
||||
}
|
||||
} else {
|
||||
format_rustc_version(execute_rustc_version()?)
|
||||
};
|
||||
|
||||
let mut module = context.new_module("rust");
|
||||
|
||||
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);
|
||||
|
||||
Some(module)
|
||||
}
|
||||
|
||||
fn env_rustup_toolchain() -> Option<String> {
|
||||
let val = env::var("RUSTUP_TOOLCHAIN").ok()?;
|
||||
Some(val.trim().to_owned())
|
||||
}
|
||||
|
||||
fn execute_rustup_override_list(cwd: &Path) -> Option<String> {
|
||||
let Output { stdout, .. } = Command::new("rustup")
|
||||
.args(&["override", "list"])
|
||||
.output()
|
||||
.ok()?;
|
||||
let stdout = String::from_utf8(stdout).ok()?;
|
||||
extract_toolchain_from_rustup_override_list(&stdout, cwd)
|
||||
}
|
||||
|
||||
fn extract_toolchain_from_rustup_override_list(stdout: &str, cwd: &Path) -> Option<String> {
|
||||
if stdout == "no overrides\n" {
|
||||
return None;
|
||||
}
|
||||
stdout
|
||||
.lines()
|
||||
.flat_map(|line| {
|
||||
let mut words = line.split_whitespace();
|
||||
let dir = words.next()?;
|
||||
let toolchain = words.next()?;
|
||||
Some((dir, toolchain))
|
||||
})
|
||||
.find(|(dir, _)| cwd.starts_with(dir))
|
||||
.map(|(_, toolchain)| toolchain.to_owned())
|
||||
}
|
||||
|
||||
fn find_rust_toolchain_file(context: &Context) -> Option<String> {
|
||||
// Look for 'rust-toolchain' as rustup does.
|
||||
// https://github.com/rust-lang/rustup.rs/blob/d84e6e50126bccd84649e42482fc35a11d019401/src/config.rs#L320-L358
|
||||
|
||||
fn read_first_line(path: &Path) -> Option<String> {
|
||||
let content = fs::read_to_string(path).ok()?;
|
||||
let line = content.lines().next()?;
|
||||
Some(line.trim().to_owned())
|
||||
}
|
||||
|
||||
if let Some(path) = context
|
||||
.get_dir_files()
|
||||
.ok()?
|
||||
.iter()
|
||||
.find(|p| p.file_name() == Some(OsStr::new("rust-toolchain")))
|
||||
{
|
||||
if let Some(toolchain) = read_first_line(path) {
|
||||
return Some(toolchain);
|
||||
}
|
||||
}
|
||||
|
||||
let mut dir = &*context.current_dir;
|
||||
loop {
|
||||
if let Some(toolchain) = read_first_line(&dir.join("rust-toolchain")) {
|
||||
return Some(toolchain);
|
||||
}
|
||||
dir = dir.parent()?;
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_rustup_run_rustc_version(toolchain: &str) -> RustupRunRustcVersionOutcome {
|
||||
Command::new("rustup")
|
||||
.args(&["run", toolchain, "rustc", "--version"])
|
||||
.output()
|
||||
.map(extract_toolchain_from_rustup_run_rustc_version)
|
||||
.unwrap_or(RustupRunRustcVersionOutcome::RustupNotWorking)
|
||||
}
|
||||
|
||||
fn extract_toolchain_from_rustup_run_rustc_version(output: Output) -> RustupRunRustcVersionOutcome {
|
||||
if output.status.success() {
|
||||
if let Ok(output) = String::from_utf8(output.stdout) {
|
||||
return RustupRunRustcVersionOutcome::RustcVersion(output);
|
||||
}
|
||||
} else if let Ok(stderr) = String::from_utf8(output.stderr) {
|
||||
if stderr.starts_with("error: toolchain '") && stderr.ends_with("' is not installed\n") {
|
||||
let stderr = stderr
|
||||
["error: toolchain '".len()..stderr.len() - "' is not installed\n".len()]
|
||||
.to_owned();
|
||||
return RustupRunRustcVersionOutcome::ToolchainName(stderr);
|
||||
}
|
||||
}
|
||||
RustupRunRustcVersionOutcome::Err
|
||||
}
|
||||
|
||||
fn execute_rustc_version() -> Option<String> {
|
||||
match Command::new("rustc").arg("--version").output() {
|
||||
Ok(output) => Some(String::from_utf8(output.stdout).unwrap()),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn format_rustc_version(mut rustc_stdout: String) -> String {
|
||||
let offset = &rustc_stdout.find('(').unwrap_or_else(|| rustc_stdout.len());
|
||||
let formatted_version: String = rustc_stdout.drain(..offset).collect();
|
||||
|
||||
format!("v{}", formatted_version.replace("rustc", "").trim())
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum RustupRunRustcVersionOutcome {
|
||||
RustcVersion(String),
|
||||
ToolchainName(String),
|
||||
RustupNotWorking,
|
||||
Err,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use once_cell::sync::Lazy;
|
||||
use std::process::{ExitStatus, Output};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_extract_toolchain_from_rustup_override_list() {
|
||||
static NO_OVERRIDES_INPUT: &str = "no overrides\n";
|
||||
static NO_OVERRIDES_CWD: &str = "";
|
||||
assert_eq!(
|
||||
extract_toolchain_from_rustup_override_list(
|
||||
NO_OVERRIDES_INPUT,
|
||||
NO_OVERRIDES_CWD.as_ref(),
|
||||
),
|
||||
None,
|
||||
);
|
||||
|
||||
static OVERRIDES_INPUT: &str =
|
||||
"/home/user/src/a beta-x86_64-unknown-linux-gnu\n\
|
||||
/home/user/src/b nightly-x86_64-unknown-linux-gnu\n";
|
||||
static OVERRIDES_CWD_A: &str = "/home/user/src/a/src";
|
||||
static OVERRIDES_CWD_B: &str = "/home/user/src/b/tests";
|
||||
static OVERRIDES_CWD_C: &str = "/home/user/src/c/examples";
|
||||
assert_eq!(
|
||||
extract_toolchain_from_rustup_override_list(OVERRIDES_INPUT, OVERRIDES_CWD_A.as_ref()),
|
||||
Some("beta-x86_64-unknown-linux-gnu".to_owned()),
|
||||
);
|
||||
assert_eq!(
|
||||
extract_toolchain_from_rustup_override_list(OVERRIDES_INPUT, OVERRIDES_CWD_B.as_ref()),
|
||||
Some("nightly-x86_64-unknown-linux-gnu".to_owned()),
|
||||
);
|
||||
assert_eq!(
|
||||
extract_toolchain_from_rustup_override_list(OVERRIDES_INPUT, OVERRIDES_CWD_C.as_ref()),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(any(unix, windows))]
|
||||
#[test]
|
||||
fn test_extract_toolchain_from_rustup_run_rustc_version() {
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::process::ExitStatusExt as _;
|
||||
#[cfg(windows)]
|
||||
use std::os::windows::process::ExitStatusExt as _;
|
||||
|
||||
static RUSTC_VERSION: Lazy<Output> = Lazy::new(|| Output {
|
||||
status: ExitStatus::from_raw(0),
|
||||
stdout: b"rustc 1.34.0\n"[..].to_owned(),
|
||||
stderr: vec![],
|
||||
});
|
||||
assert_eq!(
|
||||
extract_toolchain_from_rustup_run_rustc_version(RUSTC_VERSION.clone()),
|
||||
RustupRunRustcVersionOutcome::RustcVersion("rustc 1.34.0\n".to_owned()),
|
||||
);
|
||||
|
||||
static TOOLCHAIN_NAME: Lazy<Output> = Lazy::new(|| Output {
|
||||
status: ExitStatus::from_raw(1),
|
||||
stdout: vec![],
|
||||
stderr: b"error: toolchain 'channel-triple' is not installed\n"[..].to_owned(),
|
||||
});
|
||||
assert_eq!(
|
||||
extract_toolchain_from_rustup_run_rustc_version(TOOLCHAIN_NAME.clone()),
|
||||
RustupRunRustcVersionOutcome::ToolchainName("channel-triple".to_owned()),
|
||||
);
|
||||
|
||||
static INVALID_STDOUT: Lazy<Output> = Lazy::new(|| Output {
|
||||
status: ExitStatus::from_raw(0),
|
||||
stdout: b"\xc3\x28"[..].to_owned(),
|
||||
stderr: vec![],
|
||||
});
|
||||
assert_eq!(
|
||||
extract_toolchain_from_rustup_run_rustc_version(INVALID_STDOUT.clone()),
|
||||
RustupRunRustcVersionOutcome::Err,
|
||||
);
|
||||
|
||||
static INVALID_STDERR: Lazy<Output> = Lazy::new(|| Output {
|
||||
status: ExitStatus::from_raw(1),
|
||||
stdout: vec![],
|
||||
stderr: b"\xc3\x28"[..].to_owned(),
|
||||
});
|
||||
assert_eq!(
|
||||
extract_toolchain_from_rustup_run_rustc_version(INVALID_STDERR.clone()),
|
||||
RustupRunRustcVersionOutcome::Err,
|
||||
);
|
||||
|
||||
static UNEXPECTED_FORMAT_OF_ERROR: Lazy<Output> = Lazy::new(|| Output {
|
||||
status: ExitStatus::from_raw(1),
|
||||
stdout: vec![],
|
||||
stderr: b"error:"[..].to_owned(),
|
||||
});
|
||||
assert_eq!(
|
||||
extract_toolchain_from_rustup_run_rustc_version(UNEXPECTED_FORMAT_OF_ERROR.clone()),
|
||||
RustupRunRustcVersionOutcome::Err,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_rustc_version() {
|
||||
let nightly_input = String::from("rustc 1.34.0-nightly (b139669f3 2019-04-10)");
|
||||
assert_eq!(format_rustc_version(nightly_input), "v1.34.0-nightly");
|
||||
|
||||
let beta_input = String::from("rustc 1.34.0-beta.1 (2bc1d406d 2019-04-10)");
|
||||
assert_eq!(format_rustc_version(beta_input), "v1.34.0-beta.1");
|
||||
|
||||
let stable_input = String::from("rustc 1.34.0 (91856ed52 2019-04-10)");
|
||||
assert_eq!(format_rustc_version(stable_input), "v1.34.0");
|
||||
|
||||
let version_without_hash = String::from("rustc 1.34.0");
|
||||
assert_eq!(format_rustc_version(version_without_hash), "v1.34.0");
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
use ansi_term::Color;
|
||||
use chrono::{DateTime, Local};
|
||||
|
||||
use super::{Context, Module};
|
||||
|
||||
/// Outputs the current time
|
||||
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
||||
let mut module = context.new_module("time");
|
||||
|
||||
if module.config_value_bool("disabled").unwrap_or(true) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let module_style = module
|
||||
.config_value_style("style")
|
||||
.unwrap_or_else(|| Color::Yellow.bold());
|
||||
module.set_style(module_style);
|
||||
|
||||
// Load module settings
|
||||
let is_12hr = module.config_value_bool("12hr").unwrap_or(false);
|
||||
|
||||
let default_format = if is_12hr { "%r" } else { "%T" };
|
||||
let time_format = module
|
||||
.config_value_str("format")
|
||||
.unwrap_or(default_format)
|
||||
.to_owned();
|
||||
|
||||
log::trace!(
|
||||
"Timer module is enabled with format string: {}",
|
||||
time_format
|
||||
);
|
||||
|
||||
let local: DateTime<Local> = Local::now();
|
||||
let formatted_time_string = format_time(&time_format, local);
|
||||
module.new_segment("time", &formatted_time_string);
|
||||
module.get_prefix().set_value("at ");
|
||||
|
||||
Some(module)
|
||||
}
|
||||
|
||||
/// Format a given time into the given string. This function should be referentially
|
||||
/// transparent, which makes it easy to test (unlike anything involving the actual time)
|
||||
fn format_time(time_format: &str, local_time: DateTime<Local>) -> String {
|
||||
local_time.format(time_format).to_string()
|
||||
}
|
||||
|
||||
/* Because we cannot make acceptance tests for the time module, these unit
|
||||
tests become extra important */
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::offset::TimeZone;
|
||||
|
||||
const FMT_12: &str = "%r";
|
||||
const FMT_24: &str = "%T";
|
||||
|
||||
#[test]
|
||||
fn test_midnight_12hr() {
|
||||
let time = Local.ymd(2014, 7, 8).and_hms(0, 0, 0);
|
||||
let formatted = format_time(FMT_12, time);
|
||||
assert_eq!(formatted, "12:00:00 AM");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_midnight_24hr() {
|
||||
let time = Local.ymd(2014, 7, 8).and_hms(0, 0, 0);
|
||||
let formatted = format_time(FMT_24, time);
|
||||
assert_eq!(formatted, "00:00:00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_noon_12hr() {
|
||||
let time = Local.ymd(2014, 7, 8).and_hms(12, 0, 0);
|
||||
let formatted = format_time(FMT_12, time);
|
||||
assert_eq!(formatted, "12:00:00 PM");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_noon_24hr() {
|
||||
let time = Local.ymd(2014, 7, 8).and_hms(12, 0, 0);
|
||||
let formatted = format_time(FMT_24, time);
|
||||
assert_eq!(formatted, "12:00:00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_arbtime_12hr() {
|
||||
let time = Local.ymd(2014, 7, 8).and_hms(15, 36, 47);
|
||||
let formatted = format_time(FMT_12, time);
|
||||
assert_eq!(formatted, "03:36:47 PM");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_arbtime_24hr() {
|
||||
let time = Local.ymd(2014, 7, 8).and_hms(15, 36, 47);
|
||||
let formatted = format_time(FMT_24, time);
|
||||
assert_eq!(formatted, "15:36:47");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_with_paren() {
|
||||
let time = Local.ymd(2014, 7, 8).and_hms(15, 36, 47);
|
||||
let formatted = format_time("[%T]", time);
|
||||
assert_eq!(formatted, "[15:36:47]");
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
use ansi_term::{Color, Style};
|
||||
use std::env;
|
||||
use std::process::Command;
|
||||
|
||||
use super::{Context, Module};
|
||||
|
||||
/// Creates a module with the current user's username
|
||||
///
|
||||
/// Will display the username if any of the following criteria are met:
|
||||
/// - The current user isn't the same as the one that is logged in (`$LOGNAME` != `$USER`)
|
||||
/// - The current user is root (UID = 0)
|
||||
/// - The user is currently connected as an SSH session (`$SSH_CONNECTION`)
|
||||
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
||||
let user = env::var("USER").ok();
|
||||
let logname = env::var("LOGNAME").ok();
|
||||
let ssh_connection = env::var("SSH_CONNECTION").ok();
|
||||
|
||||
const ROOT_UID: Option<u32> = Some(0);
|
||||
let user_uid = get_uid();
|
||||
|
||||
let mut module = context.new_module("username");
|
||||
let show_always = module.config_value_bool("show_always").unwrap_or(false);
|
||||
|
||||
if user != logname || ssh_connection.is_some() || user_uid == ROOT_UID || show_always {
|
||||
let module_style = get_mod_style(user_uid, &module);
|
||||
module.set_style(module_style);
|
||||
module.new_segment("username", &user?);
|
||||
|
||||
return Some(module);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn get_uid() -> Option<u32> {
|
||||
match Command::new("id").arg("-u").output() {
|
||||
Ok(output) => String::from_utf8(output.stdout)
|
||||
.map(|uid| uid.trim().parse::<u32>().ok())
|
||||
.ok()?,
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_mod_style(user_uid: Option<u32>, module: &Module) -> Style {
|
||||
match user_uid {
|
||||
Some(0) => module
|
||||
.config_value_style("style_root")
|
||||
.unwrap_or_else(|| Color::Red.bold()),
|
||||
_ => module
|
||||
.config_value_style("style_user")
|
||||
.unwrap_or_else(|| Color::Yellow.bold()),
|
||||
}
|
||||
}
|
||||
-115
@@ -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);
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
use ansi_term::{ANSIString, Style};
|
||||
use std::fmt;
|
||||
|
||||
/// A segment is a single configurable element in a module. This will usually
|
||||
/// contain a data point to provide context for the prompt's user
|
||||
/// (e.g. The version that software is running).
|
||||
pub struct Segment {
|
||||
/// The segment's name, to be used in configuration and logging.
|
||||
_name: String,
|
||||
|
||||
/// The segment's style. If None, will inherit the style of the module containing it.
|
||||
style: Option<Style>,
|
||||
|
||||
/// The string value of the current segment.
|
||||
value: String,
|
||||
}
|
||||
|
||||
impl Segment {
|
||||
/// Creates a new segment with default fields.
|
||||
pub fn new(name: &str) -> Self {
|
||||
Self {
|
||||
_name: name.to_string(),
|
||||
style: None,
|
||||
value: "".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the style of the segment.
|
||||
///
|
||||
/// Accepts either `Color` or `Style`.
|
||||
pub fn set_style<T>(&mut self, style: T) -> &mut Self
|
||||
where
|
||||
T: Into<Style>,
|
||||
{
|
||||
self.style = Some(style.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the value of the segment.
|
||||
pub fn set_value<T>(&mut self, value: T) -> &mut Self
|
||||
where
|
||||
T: Into<String>,
|
||||
{
|
||||
self.value = value.into();
|
||||
self
|
||||
}
|
||||
|
||||
// Returns the ANSIString of the segment value, not including its prefix and suffix
|
||||
pub fn ansi_string(&self) -> ANSIString {
|
||||
match self.style {
|
||||
Some(style) => style.paint(&self.value),
|
||||
None => ANSIString::from(&self.value),
|
||||
}
|
||||
}
|
||||
|
||||
/// Determines if the segment contains a value.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.value.trim().is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Segment {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.ansi_string())
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Result};
|
||||
use std::path::Path;
|
||||
|
||||
/// Return the string contents of a file
|
||||
pub fn read_file<P: AsRef<Path>>(file_name: P) -> Result<String> {
|
||||
let mut file = File::open(file_name)?;
|
||||
let mut data = String::new();
|
||||
|
||||
file.read_to_string(&mut data)?;
|
||||
Ok(data)
|
||||
}
|
||||
Reference in New Issue
Block a user