feat: Add a disabled configuration option for modules (#86)

• Add support for the disabled configuration option
This will allow you to selectively disable modules that you don't want or need. 😄
• Overwrite starship configuration file path with STARSHIP_CONFIG environment variable
• Write tests for the two configuration options that are available
This commit is contained in:
Matan Kushner 2019-07-02 16:12:53 -04:00 committed by GitHub
parent 2440ed60d0
commit 463ec26024
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 240 additions and 104 deletions

1
Cargo.lock generated
View File

@ -818,6 +818,7 @@ dependencies = [
"criterion 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"dirs 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"git2 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"pretty_env_logger 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rayon 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",

View File

@ -15,6 +15,7 @@ rayon = "1.1.0"
pretty_env_logger = "0.3.0"
log = "0.4.6"
battery = "0.7.4"
lazy_static = "1.3.0"
[dev-dependencies]
tempfile = "3.1.0"

View File

@ -1,4 +1,5 @@
use crate::utils;
use std::env;
use dirs::home_dir;
@ -20,9 +21,33 @@ impl Config {
/// Create a config from a starship configuration file
fn config_from_file() -> Option<toml::value::Table> {
let file_path = home_dir()?.join(".config/starship.toml");
let toml_content = utils::read_file(&file_path.to_str()?).ok()?;
log::trace!("Config file content: \n{}", &toml_content);
let file_path = match env::var("STARSHIP_CONFIG") {
Ok(path) => {
// Use $STARSHIP_CONFIG as the config path if available
log::debug!("STARSHIP_CONFIG is set: {}", &path);
path
}
Err(_) => {
// Default to using ~/.config/starhip.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 found: \n{:?}", &config);
@ -40,3 +65,35 @@ impl Config {
module_config
}
}
/// Extends `toml::value::Table` with useful methods
pub trait TableExt {
fn get_as_bool(&self, key: &str) -> Option<bool>;
}
impl TableExt for toml::value::Table {
/// Get a key from a module's configuration as a boolean
fn get_as_bool(&self, key: &str) -> Option<bool> {
self.get(key).map(toml::Value::as_bool).unwrap_or(None)
}
}
mod tests {
use super::*;
#[test]
fn table_get_as_bool() {
let mut table = toml::value::Table::new();
// Use with boolean value
table.insert("boolean".to_string(), toml::value::Value::Boolean(true));
assert_eq!(table.get_as_bool("boolean"), Some(true));
// Use with string value
table.insert(
"string".to_string(),
toml::value::Value::String("true".to_string()),
);
assert_eq!(table.get_as_bool("string"), None);
}
}

View File

@ -1,4 +1,4 @@
use crate::config::Config;
use crate::config::{Config, TableExt};
use crate::module::Module;
use clap::ArgMatches;
@ -75,8 +75,23 @@ impl<'a> Context<'a> {
dir
}
pub fn new_module(&self, name: &str) -> Module {
Module::new(name, self.config.get_module_config(name))
/// Create a new module
///
/// Will return `None` if the module is disabled by configuration, by setting
/// the `disabled` key to `true` in the configuration for that module.
pub fn new_module(&self, name: &str) -> Option<Module> {
let config = self.config.get_module_config(name);
// If the segment has "disabled" set to "true", don't show it
let disabled = config
.map(|table| table.get_as_bool("disabled"))
.unwrap_or(None);
if disabled == Some(true) {
return None;
}
Some(Module::new(name, config))
}
// returns a new ScanDir struct with reference to current dir_files of context

View File

@ -14,6 +14,20 @@ use clap::{App, 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 matches = App::new("Starship")
.about("The cross-shell prompt for astronauts. ✨🚀")
// pull the version number from Cargo.toml
@ -24,22 +38,8 @@ fn main() {
.subcommand(
SubCommand::with_name("prompt")
.about("Prints the full starship prompt")
.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),
)
.arg(
Arg::with_name("path")
.short("p")
.long("path")
.value_name("PATH")
.help("The path that the prompt should render for ($PWD by default)")
.takes_value(true),
),
.arg(&status_code_arg)
.arg(&path_arg),
)
.subcommand(
SubCommand::with_name("module")
@ -49,22 +49,8 @@ fn main() {
.help("The name of the module to be printed")
.required(true),
)
.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),
)
.arg(
Arg::with_name("path")
.short("p")
.long("path")
.value_name("PATH")
.help("The path the prompt should render for ($PWD by default)")
.takes_value(true),
),
.arg(&status_code_arg)
.arg(&path_arg),
)
.get_matches();

View File

@ -3,7 +3,7 @@ use ansi_term::Color;
use super::{Context, Module};
/// Creates a segment for the battery percentage and charging state
pub fn segment<'a>(context: &'a Context) -> Option<Module<'a>> {
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
const BATTERY_FULL: &str = "";
const BATTERY_CHARGING: &str = "";
const BATTERY_DISCHARGING: &str = "";
@ -22,7 +22,7 @@ pub fn segment<'a>(context: &'a Context) -> Option<Module<'a>> {
}
// TODO: Set style based on percentage when threshold is modifiable
let mut module = context.new_module("battery");
let mut module = context.new_module("battery")?;
module.set_style(Color::Red.bold());
module.get_prefix().set_value("");
@ -61,7 +61,7 @@ fn get_battery_status() -> Option<BatteryStatus> {
Some(battery_status)
}
Some(Err(e)) => {
log::debug!("Unable to access battery information:\n{}", e);
log::debug!("Unable to access battery information:\n{}", &e);
None
}
None => {

View File

@ -9,12 +9,12 @@ use ansi_term::Color;
/// (green by default)
/// - If the exit-code was anything else, the arrow will be formatted with
/// `COLOR_FAILURE` (red by default)
pub fn segment<'a>(context: &'a Context) -> Option<Module<'a>> {
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
const PROMPT_CHAR: &str = "";
let color_success = Color::Green.bold();
let color_failure = Color::Red.bold();
let mut module = context.new_module("char");
let mut module = context.new_module("char")?;
module.get_prefix().set_value("");
let symbol = module.new_segment("symbol", PROMPT_CHAR);

View File

@ -12,12 +12,12 @@ use super::{Context, Module};
///
/// **Truncation**
/// Paths will be limited in length to `3` path components by default.
pub fn segment<'a>(context: &'a Context) -> Option<Module<'a>> {
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
const HOME_SYMBOL: &str = "~";
const DIR_TRUNCATION_LENGTH: usize = 3;
let module_color = Color::Cyan.bold();
let mut module = context.new_module("directory");
let mut module = context.new_module("directory")?;
module.set_style(module_color);
let current_dir = &context.current_dir;

View File

@ -5,13 +5,13 @@ use super::{Context, Module};
/// Creates a segment with the Git branch in the current directory
///
/// Will display the branch name if the current directory is a git repo
pub fn segment<'a>(context: &'a Context) -> Option<Module<'a>> {
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
let branch_name = context.branch_name.as_ref()?;
const GIT_BRANCH_CHAR: &str = "";
let segment_color = Color::Purple.bold();
let mut module = context.new_module("git_branch");
let mut module = context.new_module("git_branch")?;
module.set_style(segment_color);
module.get_prefix().set_value("on ");

View File

@ -17,7 +17,7 @@ use super::{Context, Module};
/// - `+` — 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 segment<'a>(context: &'a Context) -> Option<Module<'a>> {
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 = "";
@ -35,7 +35,7 @@ pub fn segment<'a>(context: &'a Context) -> Option<Module<'a>> {
let repository = Repository::open(repo_root).ok()?;
let module_style = Color::Red.bold();
let mut module = context.new_module("git_status");
let mut module = context.new_module("git_status")?;
module.get_prefix().set_value("[").set_style(module_style);
module.get_suffix().set_value("] ").set_style(module_style);
module.set_style(module_style);

View File

@ -13,7 +13,7 @@ use super::{Context, Module};
/// - Current directory contains a `Gopkg.lock` file
/// - Current directory contains a `.go` file
/// - Current directory contains a `Godeps` directory
pub fn segment<'a>(context: &'a Context) -> Option<Module<'a>> {
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
let is_go_project = context
.new_scan_dir()
.set_files(&["go.mod", "go.sum", "glide.yaml", "Gopkg.yml", "Gopkg.lock"])
@ -30,7 +30,7 @@ pub fn segment<'a>(context: &'a Context) -> Option<Module<'a>> {
const GO_CHAR: &str = "🐹 ";
let module_color = Color::Cyan.bold();
let mut module = context.new_module("go");
let mut module = context.new_module("go")?;
module.set_style(module_color);
let formatted_version = format_go_version(go_version)?;

View File

@ -1,10 +1,10 @@
use super::{Context, Module};
/// Creates a segment for the line break
pub fn segment<'a>(context: &'a Context) -> Option<Module<'a>> {
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
const LINE_ENDING: &str = "\n";
let mut module = context.new_module("line_break");
let mut module = context.new_module("line_break")?;
module.get_prefix().set_value("");
module.get_suffix().set_value("");

View File

@ -16,18 +16,18 @@ use crate::module::Module;
pub fn handle<'a>(module: &str, context: &'a Context) -> Option<Module<'a>> {
match module {
"dir" | "directory" => directory::segment(context),
"char" | "character" => character::segment(context),
"node" | "nodejs" => nodejs::segment(context),
"rust" | "rustlang" => rust::segment(context),
"python" => python::segment(context),
"go" | "golang" => go::segment(context),
"line_break" => line_break::segment(context),
"package" => package::segment(context),
"git_branch" => git_branch::segment(context),
"git_status" => git_status::segment(context),
"username" => username::segment(context),
"battery" => battery::segment(context),
"dir" | "directory" => directory::module(context),
"char" | "character" => character::module(context),
"node" | "nodejs" => nodejs::module(context),
"rust" | "rustlang" => rust::module(context),
"python" => python::module(context),
"go" | "golang" => go::module(context),
"line_break" => line_break::module(context),
"package" => package::module(context),
"git_branch" => git_branch::module(context),
"git_status" => git_status::module(context),
"username" => username::module(context),
"battery" => battery::module(context),
_ => panic!("Unknown module: {}", module),
}

View File

@ -9,7 +9,7 @@ use super::{Context, Module};
/// - Current directory contains a `.js` file
/// - Current directory contains a `package.json` file
/// - Current directory contains a `node_modules` directory
pub fn segment<'a>(context: &'a Context) -> Option<Module<'a>> {
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
let is_js_project = context
.new_scan_dir()
.set_files(&["package.json"])
@ -26,7 +26,7 @@ pub fn segment<'a>(context: &'a Context) -> Option<Module<'a>> {
const NODE_CHAR: &str = "";
let module_color = Color::Green.bold();
let mut module = context.new_module("node");
let mut module = context.new_module("node")?;
module.set_style(module_color);
let formatted_version = node_version.trim();

View File

@ -8,13 +8,13 @@ use toml;
/// Creates a segment with the current package version
///
/// Will display if a version is defined for your Node.js or Rust project (if one exists)
pub fn segment<'a>(context: &'a Context) -> Option<Module<'a>> {
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
match get_package_version() {
Some(package_version) => {
const PACKAGE_CHAR: &str = "📦 ";
let module_color = Color::Red.bold();
let mut module = context.new_module("package");
let mut module = context.new_module("package")?;
module.set_style(module_color);
module.get_prefix().set_value("is ");
@ -75,19 +75,21 @@ mod tests {
#[test]
fn test_extract_cargo_version() {
let cargo_with_version = r#"
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 = r#"
let cargo_without_version = toml::toml! {
[package]
name = "starship"
"#;
}
.to_string();
let expected_version = None;
assert_eq!(

View File

@ -10,7 +10,7 @@ use super::{Context, Module};
/// - Current directory contains a `.python-version` file
/// - Current directory contains a `requirements.txt` file
/// - Current directory contains a `pyproject.toml` file
pub fn segment<'a>(context: &'a Context) -> Option<Module<'a>> {
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
let is_py_project = context
.new_scan_dir()
.set_files(&["requirements.txt", ".python-version", "pyproject.toml"])
@ -26,7 +26,7 @@ pub fn segment<'a>(context: &'a Context) -> Option<Module<'a>> {
const PYTHON_CHAR: &str = "🐍 ";
let module_color = Color::Yellow.bold();
let mut module = context.new_module("python");
let mut module = context.new_module("python")?;
module.set_style(module_color);
let formatted_version = format_python_version(python_version);

View File

@ -8,7 +8,7 @@ use super::{Context, Module};
/// 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 segment<'a>(context: &'a Context) -> Option<Module<'a>> {
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
let is_rs_project = context
.new_scan_dir()
.set_files(&["Cargo.toml"])
@ -24,7 +24,7 @@ pub fn segment<'a>(context: &'a Context) -> Option<Module<'a>> {
const RUST_CHAR: &str = "🦀 ";
let module_color = Color::Red.bold();
let mut module = context.new_module("rust");
let mut module = context.new_module("rust")?;
module.set_style(module_color);
let formatted_version = format_rustc_version(rust_version);
@ -38,14 +38,14 @@ pub fn segment<'a>(context: &'a Context) -> Option<Module<'a>> {
}
fn get_rust_version() -> Option<String> {
match Command::new("rustc").arg("-V").output() {
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();
let offset = &rustc_stdout.find('(').unwrap_or(rustc_stdout.len());
let formatted_version: String = rustc_stdout.drain(..offset).collect();
format!("v{}", formatted_version.replace("rustc", "").trim())
@ -65,5 +65,8 @@ mod tests {
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");
}
}

View File

@ -10,7 +10,7 @@ use super::{Context, Module};
/// - 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 segment<'a>(context: &'a Context) -> Option<Module<'a>> {
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();
@ -18,7 +18,7 @@ pub fn segment<'a>(context: &'a Context) -> Option<Module<'a>> {
let mut module_color = Color::Yellow.bold();
if user != logname || ssh_connection.is_some() || is_root(&mut module_color) {
let mut module = context.new_module("username");
let mut module = context.new_module("username")?;
module.set_style(module_color);
module.new_segment("username", user?);

View File

@ -6,21 +6,22 @@ use crate::context::Context;
use crate::module::Module;
use crate::modules;
const PROMPT_ORDER: &[&str] = &[
"battery",
"username",
"directory",
"git_branch",
"git_status",
"package",
"nodejs",
"rust",
"python",
"go",
"line_break",
"character",
];
pub fn prompt(args: ArgMatches) {
let prompt_order = vec![
"battery",
"username",
"directory",
"git_branch",
"git_status",
"package",
"nodejs",
"rust",
"python",
"go",
"line_break",
"character",
];
let context = Context::new(args);
let stdout = io::stdout();
@ -29,7 +30,7 @@ pub fn prompt(args: ArgMatches) {
// Write a new line before the prompt
writeln!(handle).unwrap();
let modules = prompt_order
let modules = PROMPT_ORDER
.par_iter()
.map(|module| modules::handle(module, &context)) // Compute modules
.flatten()

0
tests/fixtures/empty_config.toml vendored Normal file
View File

View File

@ -1,15 +1,36 @@
use lazy_static::lazy_static;
use std::io::prelude::*;
use std::path::{Path, PathBuf};
use std::{io, process};
pub fn render_prompt() -> process::Command {
lazy_static! {
static ref MANIFEST_DIR: &'static Path = Path::new(env!("CARGO_MANIFEST_DIR"));
pub static ref FIXTURES_DIR: PathBuf = MANIFEST_DIR.join("tests/fixtures");
static ref EMPTY_CONFIG: PathBuf = MANIFEST_DIR.join("empty_config.toml");
}
/// Run an instance of starship
fn run_starship() -> process::Command {
let mut command = process::Command::new("./target/debug/starship");
command.arg("prompt");
command
.arg("prompt")
.env_clear()
.env("PATH", env!("PATH")) // Provide the $PATH variable so that external programs are runnable
.env("STARSHIP_CONFIG", EMPTY_CONFIG.as_os_str());
command
}
pub fn render_module(module_name: &str) -> process::Command {
let mut command = process::Command::new("./target/debug/starship");
command.arg("module").arg(module_name);
command
.arg("module")
.arg(module_name)
.env_clear()
.env("PATH", env!("PATH")) // Provide the $PATH variable so that external programs are runnable
.env("STARSHIP_CONFIG", EMPTY_CONFIG.as_os_str());
command
}
@ -20,3 +41,21 @@ pub fn new_tempdir() -> io::Result<tempfile::TempDir> {
// "/var/folders", which provides us with restricted permissions (rwxr-xr-x)
tempfile::tempdir_in("/tmp")
}
/// Extends `std::process::Command` with methods for testing
pub trait TestCommand {
fn use_config(&mut self, toml: toml::value::Value) -> &mut process::Command;
}
impl TestCommand for process::Command {
/// Create a configuration file with the provided TOML and use it
fn use_config(&mut self, toml: toml::value::Value) -> &mut process::Command {
// Create a persistent config file in a tempdir
let (mut config_file, config_path) =
tempfile::NamedTempFile::new().unwrap().keep().unwrap();
write!(config_file, "{}", toml.to_string()).unwrap();
// Set that newly-created file as the config for the prompt instance
self.env("STARSHIP_CONFIG", config_path)
}
}

View File

@ -0,0 +1,34 @@
use ansi_term::Color;
use std::io;
use crate::common::{self, TestCommand};
#[test]
fn char_symbol_configuration() -> io::Result<()> {
let expected = format!("{} ", Color::Green.bold().paint(""));
let output = common::render_module("char")
.use_config(toml::toml! {
[char]
symbol = ""
})
.output()?;
let actual = String::from_utf8(output.stdout).unwrap();
assert_eq!(expected, actual);
Ok(())
}
#[test]
fn disabled_module() -> io::Result<()> {
let output = common::render_module("package")
.use_config(toml::toml! {
[package]
disabled = true
})
.output()?;
let actual = String::from_utf8(output.stdout).unwrap();
assert_eq!("", actual);
Ok(())
}

View File

@ -1,5 +1,6 @@
mod character;
mod common;
mod configuration;
mod directory;
mod golang;
mod line_break;

View File

@ -8,7 +8,7 @@ use crate::common;
#[test]
fn no_env_variables() -> io::Result<()> {
let output = common::render_module("username").env_clear().output()?;
let output = common::render_module("username").output()?;
let actual = String::from_utf8(output.stdout).unwrap();
assert_eq!("", actual);
Ok(())
@ -17,7 +17,6 @@ fn no_env_variables() -> io::Result<()> {
#[test]
fn logname_equals_user() -> io::Result<()> {
let output = common::render_module("username")
.env_clear()
.env("LOGNAME", "astronaut")
.env("USER", "astronaut")
.output()?;
@ -30,7 +29,6 @@ fn logname_equals_user() -> io::Result<()> {
fn ssh_wo_username() -> io::Result<()> {
// SSH connection w/o username
let output = common::render_module("username")
.env_clear()
.env("SSH_CONNECTION", "192.168.223.17 36673 192.168.223.229 22")
.output()?;
let actual = String::from_utf8(output.stdout).unwrap();
@ -41,7 +39,6 @@ fn ssh_wo_username() -> io::Result<()> {
#[test]
fn current_user_not_logname() -> io::Result<()> {
let output = common::render_module("username")
.env_clear()
.env("LOGNAME", "astronaut")
.env("USER", "cosmonaut")
.output()?;
@ -55,7 +52,6 @@ fn current_user_not_logname() -> io::Result<()> {
#[test]
fn ssh_connection() -> io::Result<()> {
let output = common::render_module("username")
.env_clear()
.env("USER", "astronaut")
.env("SSH_CONNECTION", "192.168.223.17 36673 192.168.223.229 22")
.output()?;