Skip to content

Commit

Permalink
Add support for prompt configuration (#62)
Browse files Browse the repository at this point in the history
- Create `Config` struct that is added to `Context` when initialized
- Read `~/.confg/starship.toml` during initialization (can be updated later to also look at `$XDG_CONFIG_HOME`)
- `Context` now has a method for creating modules. This allows us to provide modules with a reference to the configuration specific to that module
  • Loading branch information
Matan Kushner committed Jun 10, 2019
1 parent 8239fbd commit 097f1b0
Show file tree
Hide file tree
Showing 27 changed files with 196 additions and 182 deletions.
6 changes: 3 additions & 3 deletions README.md
Expand Up @@ -57,7 +57,7 @@ I'm very new to Rust, so any help is appreciated when it comes to improving deve

### Other features

- [ ] `.starshiprc` configuration (JSON or TOML)
- [x] `starship.toml` configuration
- [ ] Custom sections given commands or binaries
- [ ] Self-updating

Expand All @@ -69,9 +69,9 @@ I'm very new to Rust, so any help is appreciated when it comes to improving deve

### Test strategy

- [x] Per-segment benchmarking
- [ ] Per-segment benchmarking
- [x] Per-segment unit + integration tests
- [ ] Shell + OS matrix acceptance tests
- [x] Shell + OS matrix acceptance tests

## Setup

Expand Down
6 changes: 5 additions & 1 deletion ci/azure-test-docker.yml
Expand Up @@ -8,7 +8,11 @@ jobs:
- script: docker pull starshipcommand/starship-test
displayName: Pull docker image

- script: ./integration_test
- script: |
# In order to run tests as a non-root user in docker,
# the files need to be accessible to non-root users
chmod -R a+w .
./integration_test
displayName: Run integration test suite
- script: |
Expand Down
9 changes: 6 additions & 3 deletions ci/azure-test-stable.yml
Expand Up @@ -7,8 +7,10 @@ jobs:
vmImage: ubuntu-16.04
MacOS:
vmImage: macOS-10.13
Windows:
vmImage: vs2017-win2016
# # Temporarily disabling Windows tests while I'm away
# # Will reenable Windows tests once I'm able to troubleshoot Windows bugs
# Windows:
# vmImage: vs2017-win2016
pool:
vmImage: $(vmImage)

Expand All @@ -19,5 +21,6 @@ jobs:

- template: azure-setup-test-env.yml

- script: cargo test -- --ignored
# "-Z unstable-options" is required for "--include-ignored"
- script: cargo test -- -Z unstable-options --include-ignored
displayName: cargo test
2 changes: 1 addition & 1 deletion integration_test
Expand Up @@ -13,4 +13,4 @@ docker build -f tests/Dockerfile \
.

printf 'Running test suite:\n'
docker run --rm -v $(pwd):/starship starshipcommand/starship-test
docker run --rm -v $(pwd):/src/starship starshipcommand/starship-test
42 changes: 42 additions & 0 deletions src/config.rs
@@ -0,0 +1,42 @@
use crate::utils;

use dirs::home_dir;

pub struct Config {
data: toml::value::Table,
}

impl Config {
/// Initialize the Config struct
pub fn initialize() -> Config {
if let Some(file_data) = Config::config_from_file() {
return Config { data: file_data };
}

Config {
data: toml::value::Table::new(),
}
}

/// 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 config = toml::from_str(&toml_content).ok()?;
log::debug!("Config found: \n{:?}", &config);
Some(config)
}

/// Get the subset of the table for a module by its name
pub fn get_module_config(&self, module_name: &str) -> Option<&toml::value::Table> {
let module_config = self
.data
.get(module_name)
.map(toml::Value::as_table)
.unwrap_or(None);
log::debug!("Config found for {}: {:?}", &module_name, &module_config);
module_config
}
}
11 changes: 11 additions & 0 deletions src/context.rs
@@ -1,3 +1,6 @@
use crate::config::Config;
use crate::module::Module;

use clap::ArgMatches;
use git2::Repository;
use std::env;
Expand All @@ -6,6 +9,7 @@ use std::fs;
use std::path::PathBuf;

pub struct Context<'a> {
pub config: Config,
pub current_dir: PathBuf,
pub dir_files: Vec<PathBuf>,
pub arguments: ArgMatches<'a>,
Expand All @@ -28,6 +32,8 @@ impl<'a> Context<'a> {
where
T: Into<PathBuf>,
{
let config = Config::initialize();

// TODO: Currently gets the physical directory. Get the logical directory.
let current_dir = Context::expand_tilde(dir.into());

Expand All @@ -51,6 +57,7 @@ impl<'a> Context<'a> {
.and_then(|repo| get_current_branch(&repo));

Context {
config,
arguments,
current_dir,
dir_files,
Expand All @@ -68,6 +75,10 @@ impl<'a> Context<'a> {
dir
}

pub fn new_module(&self, name: &str) -> Module {
Module::new(name, self.config.get_module_config(name))
}

// returns a new ScanDir struct with reference to current dir_files of context
// see ScanDir for methods
pub fn new_scan_dir(&'a self) -> ScanDir<'a> {
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
@@ -1,6 +1,8 @@
// 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;
2 changes: 2 additions & 0 deletions src/main.rs
@@ -1,11 +1,13 @@
#[macro_use]
extern crate clap;

mod config;
mod context;
mod module;
mod modules;
mod print;
mod segment;
mod utils;

use clap::{App, Arg, SubCommand};

Expand Down
33 changes: 25 additions & 8 deletions src/module.rs
Expand Up @@ -6,7 +6,10 @@ use std::string::ToString;

/// 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 {
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,

Expand All @@ -23,10 +26,11 @@ pub struct Module {
suffix: ModuleAffix,
}

impl Module {
impl<'a> Module<'a> {
/// Creates a module with no segments.
pub fn new(name: &str) -> Module {
pub fn new(name: &str, config: Option<&'a toml::value::Table>) -> Module<'a> {
Module {
config,
name: name.to_string(),
style: Style::default(),
prefix: ModuleAffix::default_prefix(name.to_string()),
Expand All @@ -42,7 +46,8 @@ impl Module {
{
let mut segment = Segment::new(name);
segment.set_style(self.style);
segment.set_value(value.into());
// Use the provided value unless overwritten by config
segment.set_value(self.config_value(name).unwrap_or_else(|| value.into()));
self.segments.push(segment);

self.segments.last_mut().unwrap()
Expand All @@ -66,7 +71,7 @@ impl Module {
/// Sets the style of the segment.
///
/// Accepts either `Color` or `Style`.
pub fn set_style<T>(&mut self, style: T) -> &mut Module
pub fn set_style<T>(&mut self, style: T) -> &mut Module<'a>
where
T: Into<Style>,
{
Expand All @@ -80,8 +85,7 @@ impl Module {
let mut ansi_strings = self
.segments
.iter()
.map(|s| s.ansi_strings())
.flat_map(|s| s.into_iter())
.map(|s| s.ansi_string())
.collect::<Vec<ANSIString>>();

ansi_strings.insert(0, self.prefix.ansi_string());
Expand All @@ -93,9 +97,22 @@ impl Module {
pub fn to_string_without_prefix(&self) -> String {
ANSIStrings(&self.ansi_strings()[1..]).to_string()
}

/// Get a module's config value as a string
fn config_value(&self, key: &str) -> Option<String> {
self.config
// Find the config value by its key
.map(|config| config.get(key))
.unwrap_or(None)
// Get the config value as a `&str`
.map(toml::Value::as_str)
.unwrap_or(None)
// Convert it to a String
.map(str::to_string)
}
}

impl fmt::Display for Module {
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))
Expand Down
4 changes: 2 additions & 2 deletions src/modules/battery.rs
Expand Up @@ -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(_context: &Context) -> Option<Module> {
pub fn segment<'a>(context: &'a Context) -> Option<Module<'a>> {
const BATTERY_FULL: &str = "•";
const BATTERY_CHARGING: &str = "⇡";
const BATTERY_DISCHARGING: &str = "⇣";
Expand All @@ -22,7 +22,7 @@ pub fn segment(_context: &Context) -> Option<Module> {
}

// TODO: Set style based on percentage when threshold is modifiable
let mut module = Module::new("battery");
let mut module = context.new_module("battery");
module.set_style(Color::Red.bold());
module.get_prefix().set_value("");

Expand Down
4 changes: 2 additions & 2 deletions src/modules/character.rs
Expand Up @@ -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(context: &Context) -> Option<Module> {
pub fn segment<'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 = Module::new("char");
let mut module = context.new_module("char");
module.get_prefix().set_value("");

let symbol = module.new_segment("symbol", PROMPT_CHAR);
Expand Down
4 changes: 2 additions & 2 deletions src/modules/directory.rs
Expand Up @@ -12,12 +12,12 @@ use super::{Context, Module};
///
/// **Truncation**
/// Paths will be limited in length to `3` path components by default.
pub fn segment(context: &Context) -> Option<Module> {
pub fn segment<'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 = Module::new("directory");
let mut module = context.new_module("directory");
module.set_style(module_color);

let current_dir = &context.current_dir;
Expand Down
4 changes: 2 additions & 2 deletions src/modules/git_branch.rs
Expand Up @@ -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(context: &Context) -> Option<Module> {
pub fn segment<'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 = Module::new("git_branch");
let mut module = context.new_module("git_branch");
module.set_style(segment_color);
module.get_prefix().set_value("on ");

Expand Down
4 changes: 2 additions & 2 deletions src/modules/git_status.rs
Expand Up @@ -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(context: &Context) -> Option<Module> {
pub fn segment<'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 = "⇡";
Expand All @@ -35,7 +35,7 @@ pub fn segment(context: &Context) -> Option<Module> {
let repository = Repository::open(repo_root).ok()?;

let module_style = Color::Red.bold();
let mut module = Module::new("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);
Expand Down
4 changes: 2 additions & 2 deletions src/modules/go.rs
Expand Up @@ -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(context: &Context) -> Option<Module> {
pub fn segment<'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"])
Expand All @@ -30,7 +30,7 @@ pub fn segment(context: &Context) -> Option<Module> {
const GO_CHAR: &str = "🐹 ";
let module_color = Color::Cyan.bold();

let mut module = Module::new("go");
let mut module = context.new_module("go");
module.set_style(module_color);

let formatted_version = format_go_version(go_version)?;
Expand Down
4 changes: 2 additions & 2 deletions src/modules/line_break.rs
@@ -1,10 +1,10 @@
use super::{Context, Module};

/// Creates a segment for the line break
pub fn segment(_context: &Context) -> Option<Module> {
pub fn segment<'a>(context: &'a Context) -> Option<Module<'a>> {
const LINE_ENDING: &str = "\n";

let mut module = Module::new("line_break");
let mut module = context.new_module("line_break");

module.get_prefix().set_value("");
module.get_suffix().set_value("");
Expand Down
2 changes: 1 addition & 1 deletion src/modules/mod.rs
Expand Up @@ -14,7 +14,7 @@ mod username;
use crate::context::Context;
use crate::module::Module;

pub fn handle(module: &str, context: &Context) -> Option<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),
Expand Down
4 changes: 2 additions & 2 deletions src/modules/nodejs.rs
Expand Up @@ -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(context: &Context) -> Option<Module> {
pub fn segment<'a>(context: &'a Context) -> Option<Module<'a>> {
let is_js_project = context
.new_scan_dir()
.set_files(&["package.json"])
Expand All @@ -26,7 +26,7 @@ pub fn segment(context: &Context) -> Option<Module> {
const NODE_CHAR: &str = "⬢ ";
let module_color = Color::Green.bold();

let mut module = Module::new("node");
let mut module = context.new_module("node");
module.set_style(module_color);

let formatted_version = node_version.trim();
Expand Down

0 comments on commit 097f1b0

Please sign in to comment.