refactor: Refactoring config (#383)

This PR refactors config and puts configuration files for all modules in `configs/`.
This commit is contained in:
Zhenhui Xie
2019-09-30 20:10:35 +08:00
committed by Matan Kushner
parent 9e9eb6a8ef
commit dd0b1a1aa2
48 changed files with 1290 additions and 946 deletions
-473
View File
@@ -1,473 +0,0 @@
use crate::utils;
use std::env;
use dirs::home_dir;
use toml::value::Table;
use toml::value::Value;
use ansi_term::Color;
pub trait Config {
fn initialize() -> Table;
fn config_from_file() -> Option<Table>;
fn get_module_config(&self, module_name: &str) -> Option<&Table>;
// Config accessor methods
fn get_as_bool(&self, key: &str) -> Option<bool>;
fn get_as_str(&self, key: &str) -> Option<&str>;
fn get_as_i64(&self, key: &str) -> Option<i64>;
fn get_as_array(&self, key: &str) -> Option<&Vec<Value>>;
fn get_as_ansi_style(&self, key: &str) -> Option<ansi_term::Style>;
fn get_as_segment_config(&self, key: &str) -> Option<SegmentConfig>;
// Internal implementation for accessors
fn get_config(&self, key: &str) -> Option<&Value>;
}
impl Config for Table {
/// Initialize the Config struct
fn initialize() -> Table {
if let Some(file_data) = Self::config_from_file() {
return file_data;
}
Self::new()
}
/// Create a config from a starship configuration file
fn config_from_file() -> Option<Table> {
let file_path = if let Ok(path) = env::var("STARSHIP_CONFIG") {
// Use $STARSHIP_CONFIG as the config path if available
log::debug!("STARSHIP_CONFIG is set: \n{}", &path);
path
} else {
// Default to using ~/.config/starship.toml
log::debug!("STARSHIP_CONFIG is not set");
let config_path = home_dir()?.join(".config/starship.toml");
let config_path_str = config_path.to_str()?.to_owned();
log::debug!("Using default config path: {}", config_path_str);
config_path_str
};
let toml_content = match utils::read_file(&file_path) {
Ok(content) => {
log::trace!("Config file content: \n{}", &content);
Some(content)
}
Err(e) => {
log::debug!("Unable to read config file content: \n{}", &e);
None
}
}?;
let config = toml::from_str(&toml_content).ok()?;
log::debug!("Config parsed: \n{:?}", &config);
Some(config)
}
/// Get the config value for a given key
fn get_config(&self, key: &str) -> Option<&Value> {
log::trace!("Looking for config key \"{}\"", key);
let value = self.get(key);
log_if_key_found(key, value);
value
}
/// Get the subset of the table for a module by its name
fn get_module_config(&self, key: &str) -> Option<&Table> {
log::trace!("Looking for module key \"{}\"", key);
let value = self.get(key);
log_if_key_found(key, value);
value.and_then(|value| {
let casted = Value::as_table(value);
log_if_type_correct(key, value, casted);
casted
})
}
/// Get a key from a module's configuration as a boolean
fn get_as_bool(&self, key: &str) -> Option<bool> {
log::trace!("Looking for boolean key \"{}\"", key);
let value = self.get(key);
log_if_key_found(key, value);
value.and_then(|value| {
let casted = Value::as_bool(value);
log_if_type_correct(key, value, casted);
casted
})
}
/// Get a key from a module's configuration as a string
fn get_as_str(&self, key: &str) -> Option<&str> {
log::trace!("Looking for string key \"{}\"", key);
let value = self.get(key);
log_if_key_found(key, value);
value.and_then(|value| {
let casted = Value::as_str(value);
log_if_type_correct(key, value, casted);
casted
})
}
/// Get a key from a module's configuration as an integer
fn get_as_i64(&self, key: &str) -> Option<i64> {
log::trace!("Looking for integer key \"{}\"", key);
let value = self.get(key);
log_if_key_found(key, value);
value.and_then(|value| {
let casted = Value::as_integer(value);
log_if_type_correct(key, value, casted);
casted
})
}
/// Get a key from a module's configuration as a vector
fn get_as_array(&self, key: &str) -> Option<&Vec<Value>> {
log::trace!("Looking for array key \"{}\"", key);
let value = self.get(key);
log_if_key_found(key, value);
value.and_then(|value| {
let casted = Value::as_array(value);
log_if_type_correct(key, value, casted);
casted
})
}
/// Get a text key and attempt to interpret it into an ANSI style.
fn get_as_ansi_style(&self, key: &str) -> Option<ansi_term::Style> {
// TODO: This should probably not unwrap to an empty new Style but inform the user about the problem
self.get_as_str(key)
.map(|x| parse_style_string(x).unwrap_or_default())
}
/// Get a key from a module's configuration as a segment config.
///
/// The config can be
///
/// - a string, will be interpreted as value.
/// - a table with optional { value, style } keys.
/// If omitted, default value will be used.
///
/// Returns `Some(SegmentConfig)` if key exists in the configuration, else `None`.
fn get_as_segment_config(&self, key: &str) -> Option<SegmentConfig> {
self.get_config(key).and_then(|segment_config: &Value| {
match segment_config {
toml::Value::String(value) => Some(SegmentConfig {
value: Some(value.as_str()),
style: None,
}),
toml::Value::Table(config_table) => Some(SegmentConfig {
value: config_table.get_as_str("value"),
style: config_table.get_as_ansi_style("style"),
}),
_ => {
log::debug!(
"Expected \"{}\" to be a string or config table. Instead received {} of type {}.",
key,
segment_config,
segment_config.type_str()
);
None
}
}
})
}
}
fn log_if_key_found(key: &str, something: Option<&Value>) {
if something.is_some() {
log::trace!("Value found for \"{}\": {:?}", key, &something);
} else {
log::trace!("No value found for \"{}\"", key);
}
}
fn log_if_type_correct<T: std::fmt::Debug>(
key: &str,
something: &Value,
casted_something: Option<T>,
) {
if let Some(casted) = casted_something {
log::trace!(
"Value under key \"{}\" has the expected type. Proceeding with {:?} which was build from {:?}.",
key,
casted,
something
);
} else {
log::debug!(
"Value under key \"{}\" did not have the expected type. Instead received {} of type {}.",
key,
something,
something.type_str()
);
}
}
/** Parse a style string which represents an ansi style. Valid tokens in the style
string include the following:
- 'fg:<color>' (specifies that the color read should be a foreground color)
- 'bg:<color>' (specifies that the color read should be a background color)
- 'underline'
- 'bold'
- 'italic'
- '<color>' (see the parse_color_string doc for valid color strings)
*/
fn parse_style_string(style_string: &str) -> Option<ansi_term::Style> {
style_string
.split_whitespace()
.fold(Some(ansi_term::Style::new()), |maybe_style, token| {
maybe_style.and_then(|style| {
let token = token.to_lowercase();
// Check for FG/BG identifiers and strip them off if appropriate
// If col_fg is true, color the foreground. If it's false, color the background.
let (token, col_fg) = if token.as_str().starts_with("fg:") {
(token.trim_start_matches("fg:").to_owned(), true)
} else if token.as_str().starts_with("bg:") {
(token.trim_start_matches("bg:").to_owned(), false)
} else {
(token, true) // Bare colors are assumed to color the foreground
};
match token.as_str() {
"underline" => Some(style.underline()),
"bold" => Some(style.bold()),
"italic" => Some(style.italic()),
"dimmed" => Some(style.dimmed()),
"none" => None,
// Try to see if this token parses as a valid color string
color_string => parse_color_string(color_string).map(|ansi_color| {
if col_fg {
style.fg(ansi_color)
} else {
style.on(ansi_color)
}
}),
}
})
})
}
/** Parse a string that represents a color setting, returning None if this fails
There are three valid color formats:
- #RRGGBB (a hash followed by an RGB hex)
- u8 (a number from 0-255, representing an ANSI color)
- colstring (one of the 16 predefined color strings)
*/
fn parse_color_string(color_string: &str) -> Option<ansi_term::Color> {
// Parse RGB hex values
log::trace!("Parsing color_string: {}", color_string);
if color_string.starts_with('#') {
log::trace!(
"Attempting to read hexadecimal color string: {}",
color_string
);
let r: u8 = u8::from_str_radix(&color_string[1..3], 16).ok()?;
let g: u8 = u8::from_str_radix(&color_string[3..5], 16).ok()?;
let b: u8 = u8::from_str_radix(&color_string[5..7], 16).ok()?;
log::trace!("Read RGB color string: {},{},{}", r, g, b);
return Some(Color::RGB(r, g, b));
}
// Parse a u8 (ansi color)
if let Result::Ok(ansi_color_num) = color_string.parse::<u8>() {
log::trace!("Read ANSI color string: {}", ansi_color_num);
return Some(Color::Fixed(ansi_color_num));
}
// Check for any predefined color strings
// There are no predefined enums for bright colors, so we use Color::Fixed
let predefined_color = match color_string.to_lowercase().as_str() {
"black" => Some(Color::Black),
"red" => Some(Color::Red),
"green" => Some(Color::Green),
"yellow" => Some(Color::Yellow),
"blue" => Some(Color::Blue),
"purple" => Some(Color::Purple),
"cyan" => Some(Color::Cyan),
"white" => Some(Color::White),
"bright-black" => Some(Color::Fixed(8)), // "bright-black" is dark grey
"bright-red" => Some(Color::Fixed(9)),
"bright-green" => Some(Color::Fixed(10)),
"bright-yellow" => Some(Color::Fixed(11)),
"bright-blue" => Some(Color::Fixed(12)),
"bright-purple" => Some(Color::Fixed(13)),
"bright-cyan" => Some(Color::Fixed(14)),
"bright-white" => Some(Color::Fixed(15)),
_ => None,
};
if predefined_color.is_some() {
log::trace!("Read predefined color: {}", color_string);
} else {
log::debug!("Could not parse color in string: {}", color_string);
}
predefined_color
}
pub struct SegmentConfig<'a> {
pub value: Option<&'a str>,
pub style: Option<ansi_term::Style>,
}
#[cfg(test)]
mod tests {
use super::*;
use ansi_term::Style;
#[test]
fn table_get_nonexisting() {
let table = toml::value::Table::new();
assert_eq!(table.get_as_bool("boolean"), None);
}
#[test]
fn table_get_config() {
let mut table = toml::value::Table::new();
table.insert(String::from("config"), Value::Boolean(true));
assert_eq!(table.get_config("config"), Some(&Value::Boolean(true)));
}
#[test]
fn table_get_as_bool() {
let mut table = toml::value::Table::new();
table.insert(String::from("boolean"), Value::Boolean(true));
assert_eq!(table.get_as_bool("boolean"), Some(true));
table.insert(String::from("string"), Value::String(String::from("true")));
assert_eq!(table.get_as_bool("string"), None);
}
#[test]
fn table_get_as_str() {
let mut table = toml::value::Table::new();
table.insert(String::from("string"), Value::String(String::from("hello")));
assert_eq!(table.get_as_str("string"), Some("hello"));
table.insert(String::from("boolean"), Value::Boolean(true));
assert_eq!(table.get_as_str("boolean"), None);
}
#[test]
fn table_get_as_i64() {
let mut table = toml::value::Table::new();
table.insert(String::from("integer"), Value::Integer(82));
assert_eq!(table.get_as_i64("integer"), Some(82));
table.insert(String::from("string"), Value::String(String::from("82")));
assert_eq!(table.get_as_bool("string"), None);
}
#[test]
fn table_get_as_array() {
let mut table = toml::value::Table::new();
table.insert(
String::from("array"),
Value::Array(vec![Value::Integer(1), Value::Integer(2)]),
);
assert_eq!(
table.get_as_array("array"),
Some(&vec![Value::Integer(1), Value::Integer(2)])
);
table.insert(String::from("string"), Value::String(String::from("82")));
assert_eq!(table.get_as_array("string"), None);
}
#[test]
fn table_get_styles_bold_italic_underline_green_dimmy_silly_caps() {
let mut table = toml::value::Table::new();
table.insert(
String::from("mystyle"),
Value::String(String::from("bOlD ItAlIc uNdErLiNe GrEeN dimmed")),
);
assert!(table.get_as_ansi_style("mystyle").unwrap().is_bold);
assert!(table.get_as_ansi_style("mystyle").unwrap().is_italic);
assert!(table.get_as_ansi_style("mystyle").unwrap().is_underline);
assert!(table.get_as_ansi_style("mystyle").unwrap().is_dimmed);
assert_eq!(
table.get_as_ansi_style("mystyle").unwrap(),
ansi_term::Style::new()
.bold()
.italic()
.underline()
.dimmed()
.fg(Color::Green)
);
}
#[test]
fn table_get_styles_plain_and_broken_styles() {
let mut table = toml::value::Table::new();
// Test a "plain" style with no formatting
table.insert(String::from("plainstyle"), Value::String(String::from("")));
assert_eq!(
table.get_as_ansi_style("plainstyle").unwrap(),
ansi_term::Style::new()
);
// Test a string that's clearly broken
table.insert(
String::from("broken"),
Value::String(String::from("djklgfhjkldhlhk;j")),
);
assert_eq!(
table.get_as_ansi_style("broken").unwrap(),
ansi_term::Style::new()
);
// Test a string that's nullified by `none`
table.insert(
String::from("nullified"),
Value::String(String::from("fg:red bg:green bold none")),
);
assert_eq!(
table.get_as_ansi_style("nullified").unwrap(),
ansi_term::Style::new()
);
// Test a string that's nullified by `none` at the start
table.insert(
String::from("nullified-start"),
Value::String(String::from("none fg:red bg:green bold")),
);
assert_eq!(
table.get_as_ansi_style("nullified-start").unwrap(),
ansi_term::Style::new()
);
}
#[test]
fn table_get_styles_ordered() {
let mut table = toml::value::Table::new();
// Test a background style with inverted order (also test hex + ANSI)
table.insert(
String::from("flipstyle"),
Value::String(String::from("bg:#050505 underline fg:120")),
);
assert_eq!(
table.get_as_ansi_style("flipstyle").unwrap(),
Style::new()
.underline()
.fg(Color::Fixed(120))
.on(Color::RGB(5, 5, 5))
);
// Test that the last color style is always the one used
table.insert(
String::from("multistyle"),
Value::String(String::from("bg:120 bg:125 bg:127 fg:127 122 125")),
);
assert_eq!(
table.get_as_ansi_style("multistyle").unwrap(),
Style::new().fg(Color::Fixed(125)).on(Color::Fixed(127))
);
}
}
-294
View File
@@ -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
View File
@@ -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");
-69
View File
@@ -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"
-15
View File
@@ -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"
-58
View File
@@ -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"
-8
View File
@@ -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
View File
@@ -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
View File
@@ -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());
}
}
-29
View File
@@ -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)
}
-137
View File
@@ -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 })
}
}
-69
View File
@@ -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)
}
-101
View File
@@ -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")
}
}
-312
View File
@@ -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(&current_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/");
}
}
-43
View File
@@ -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()),
}
}
-65
View File
@@ -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()
}
-162
View File
@@ -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,
}
-196
View File
@@ -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)
}
-82
View File
@@ -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()));
}
}
-41
View File
@@ -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)
}
-105
View File
@@ -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);
}
}
-34
View File
@@ -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)
}
-15
View File
@@ -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)
}
-91
View File
@@ -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)
}
-63
View File
@@ -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
}
}
}
-56
View File
@@ -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
})
}
-49
View File
@@ -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,
}
}
-166
View File
@@ -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
);
}
}
-122
View File
@@ -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");
}
}
-61
View File
@@ -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)
}
-291
View File
@@ -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");
}
}
-105
View File
@@ -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]");
}
}
-53
View File
@@ -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
View File
@@ -1,115 +0,0 @@
use clap::ArgMatches;
use rayon::prelude::*;
use std::io::{self, Write};
use crate::config::Config;
use crate::context::Context;
use crate::module::Module;
use crate::module::ALL_MODULES;
use crate::modules;
// List of default prompt order
// NOTE: If this const value is changed then Default prompt order subheading inside
// prompt heading of config docs needs to be updated according to changes made here.
const DEFAULT_PROMPT_ORDER: &[&str] = &[
"username",
"hostname",
"directory",
"git_branch",
"git_state",
"git_status",
"package",
"nodejs",
"ruby",
"rust",
"python",
"golang",
"java",
"nix_shell",
"memory_usage",
"aws",
"env_var",
"cmd_duration",
"line_break",
"jobs",
#[cfg(feature = "battery")]
"battery",
"time",
"character",
];
pub fn prompt(args: ArgMatches) {
let context = Context::new(args);
let config = &context.config;
let stdout = io::stdout();
let mut handle = stdout.lock();
// Write a new line before the prompt
if config.get_as_bool("add_newline") != Some(false) {
writeln!(handle).unwrap();
}
let mut prompt_order: Vec<&str> = Vec::new();
// Write out a custom prompt order
if let Some(modules) = config.get_as_array("prompt_order") {
// if prompt_order = [] use default_prompt_order
if !modules.is_empty() {
for module in modules {
let str_value = module.as_str();
if let Some(value) = str_value {
if ALL_MODULES.contains(&value) {
prompt_order.push(value);
} else {
log::debug!(
"Expected prompt_order to contain value from {:?}. Instead received {}",
ALL_MODULES,
value,
);
}
} else {
log::debug!(
"Expected prompt_order to be an array of strings. Instead received {} of type {}",
module,
module.type_str()
);
}
}
} else {
prompt_order = DEFAULT_PROMPT_ORDER.to_vec();
}
} else {
prompt_order = DEFAULT_PROMPT_ORDER.to_vec();
}
let modules = &prompt_order
.par_iter()
.filter(|module| context.is_module_enabled(module))
.map(|module| modules::handle(module, &context)) // Compute modules
.flatten()
.collect::<Vec<Module>>(); // Remove segments set to `None`
let mut printable = modules.iter();
// Print the first module without its prefix
if let Some(first_module) = printable.next() {
let module_without_prefix = first_module.to_string_without_prefix();
write!(handle, "{}", module_without_prefix).unwrap()
}
// Print all remaining modules
printable.for_each(|module| write!(handle, "{}", module).unwrap());
}
pub fn module(module_name: &str, args: ArgMatches) {
let context = Context::new(args);
// If the module returns `None`, print an empty string
let module = modules::handle(module_name, &context)
.map(|m| m.to_string())
.unwrap_or_default();
print!("{}", module);
}
-66
View File
@@ -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())
}
}
-12
View File
@@ -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)
}