Skip to content

Commit

Permalink
feat: Add support for RPROMPT (right prompt) (starship#3026)
Browse files Browse the repository at this point in the history
Adds support for zsh, fish, and elvish.

Co-authored-by: Matan Kushner <[email protected]>
  • Loading branch information
mjeffryes and matchai authored Sep 8, 2021
1 parent cb8dca2 commit 79585dc
Show file tree
Hide file tree
Showing 11 changed files with 180 additions and 37 deletions.
31 changes: 31 additions & 0 deletions docs/advanced-config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,37 @@ function set_win_title(){
starship_precmd_user_func="set_win_title"
```

## Enable Right Prompt

Some shells support a right prompt which renders on the same line as the input. Starship can
set the content of the right prompt using the `right_format` option. Any module that can be used
in `format` is also supported in `right_format`. The `$all` variable will only contain modules
not explicitly used in either `format` or `right_format`.

Note: The right prompt is a single line following the input location. To right align modules above
the input line in a multi-line prompt, see the [fill module](/config/#fill).

`right_format` is currently supported for the following shells: elvish, fish, zsh.

### Example

```toml
# ~/.config/starship.toml

# A minimal left prompt
format = """$character"""

# move the rest of the prompt to the right
right_format = """$all"""
```

Produces a prompt like the following:

```
▶ starship on  rprompt [!] is 📦 v0.57.0 via 🦀 v1.54.0 took 17s
```


## Style Strings

Style strings are a list of words, separated by whitespace. The words are not case sensitive (i.e. `bold` and `BoLd` are considered the same string). Each word can be one of the following:
Expand Down
22 changes: 16 additions & 6 deletions docs/config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,12 +151,14 @@ This is the list of prompt-wide configuration options.

### Options

| Option | Default | Description |
| ----------------- | ------------------------------ | ------------------------------------------------------------ |
| `format` | [link](#default-prompt-format) | Configure the format of the prompt. |
| `scan_timeout` | `30` | Timeout for starship to scan files (in milliseconds). |
| `command_timeout` | `500` | Timeout for commands executed by starship (in milliseconds). |
| `add_newline` | `true` | Inserts blank line between shell prompts. |
| Option | Default | Description |
| ----------------- | ------------------------------- | ---------------------------------------------------------------- |
| `format` | [link](#default-prompt-format) | Configure the format of the prompt. |
| `right_format` | `""` | See [Enable Right Prompt](/advanced-config/#enable-right-prompt) |
| `scan_timeout` | `30` | Timeout for starship to scan files (in milliseconds). |
| `command_timeout` | `500` | Timeout for commands executed by starship (in milliseconds). |
| `add_newline` | `true` | Inserts blank line between shell prompts. |


### Example

Expand Down Expand Up @@ -247,6 +249,14 @@ $shell\
$character"""
```

If you just want to extend the default format, you can use `$all`;
modules you explicitly add to the format will not be duplicated. Eg.

```toml
# Move the directory to the second line
format="$all$directory$character"
```

## AWS

The `aws` module shows the current AWS region and profile. This is based on
Expand Down
4 changes: 4 additions & 0 deletions src/configs/starship_root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use std::cmp::Ordering;
#[derive(Clone, Serialize)]
pub struct StarshipRootConfig<'a> {
pub format: &'a str,
pub right_format: &'a str,
pub scan_timeout: u64,
pub command_timeout: u64,
pub add_newline: bool,
Expand Down Expand Up @@ -90,6 +91,7 @@ impl<'a> Default for StarshipRootConfig<'a> {
fn default() -> Self {
StarshipRootConfig {
format: "$all",
right_format: "",
scan_timeout: 30,
command_timeout: 500,
add_newline: true,
Expand All @@ -102,6 +104,7 @@ impl<'a> ModuleConfig<'a> for StarshipRootConfig<'a> {
if let toml::Value::Table(config) = config {
config.iter().for_each(|(k, v)| match k.as_str() {
"format" => self.format.load_config(v),
"right_format" => self.right_format.load_config(v),
"scan_timeout" => self.scan_timeout.load_config(v),
"command_timeout" => self.command_timeout.load_config(v),
"add_newline" => self.add_newline.load_config(v),
Expand All @@ -115,6 +118,7 @@ impl<'a> ModuleConfig<'a> for StarshipRootConfig<'a> {
let did_you_mean = &[
// Root options
"format",
"right_format",
"scan_timeout",
"command_timeout",
"add_newline",
Expand Down
6 changes: 6 additions & 0 deletions src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ pub struct Context<'a> {
/// The shell the user is assumed to be running
pub shell: Shell,

/// Construct the right prompt instead of the left prompt
pub right: bool,

/// A HashMap of environment variable mocks
#[cfg(test)]
pub env: HashMap<&'a str, String>,
Expand Down Expand Up @@ -120,6 +123,8 @@ impl<'a> Context<'a> {

let cmd_timeout = Duration::from_millis(config.get_root_config().command_timeout);

let right = arguments.is_present("right");

Context {
config,
properties,
Expand All @@ -129,6 +134,7 @@ impl<'a> Context<'a> {
dir_contents: OnceCell::new(),
repo: OnceCell::new(),
shell,
right,
#[cfg(test)]
env: HashMap::new(),
#[cfg(test)]
Expand Down
9 changes: 9 additions & 0 deletions src/formatter/string_formatter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,15 @@ impl<'a> StringFormatter<'a> {
})
}

/// A StringFormatter that does no formatting, parse just returns the raw text
pub fn raw(text: &'a str) -> Self {
Self {
format: vec![FormatElement::Text(text.into())],
variables: BTreeMap::new(),
style_variables: BTreeMap::new(),
}
}

/// Maps variable name to its value
///
/// You should provide a function or closure that accepts the variable name `name: &str` as a
Expand Down
16 changes: 13 additions & 3 deletions src/init/starship.elv
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ set-env STARSHIP_SESSION_KEY (::STARSHIP:: session)
# Define Hooks
local:cmd-start-time = 0
local:cmd-end-time = 0
local:cmd-duration = 0

fn starship-after-readline-hook [line]{
cmd-start-time = (::STARSHIP:: time)
}

fn starship-before-readline-hook {
cmd-end-time = (::STARSHIP:: time)
cmd-duration = (- $cmd-end-time $cmd-start-time)
}

# Install Hooks
Expand All @@ -25,9 +27,17 @@ edit:prompt = {
if (== $cmd-start-time 0) {
::STARSHIP:: prompt --jobs=$num-bg-jobs
} else {
::STARSHIP:: prompt --jobs=$num-bg-jobs --cmd-duration=(- $cmd-end-time $cmd-start-time)
::STARSHIP:: prompt --jobs=$num-bg-jobs --cmd-duration=$cmd-duration
}
}

# Get rid of default rprompt
edit:rprompt = { }
edit:rprompt = {
# Note:
# Elvish does not appear to support exit status codes (--status)

if (== $cmd-start-time 0) {
::STARSHIP:: prompt --right --jobs=$num-bg-jobs
} else {
::STARSHIP:: prompt --right --jobs=$num-bg-jobs --cmd-duration=$cmd-duration
}
}
7 changes: 6 additions & 1 deletion src/init/starship.fish
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ function fish_prompt
set STARSHIP_CMD_STATUS $status
# Account for changes in variable name between v2.7 and v3.0
set STARSHIP_DURATION "$CMD_DURATION$cmd_duration"
::STARSHIP:: prompt --status=$STARSHIP_CMD_STATUS --pipestatus=$pipestatus --keymap=$STARSHIP_KEYMAP --cmd-duration=$STARSHIP_DURATION --jobs=(count (jobs -p))
set STARSHIP_JOBS (count (jobs -p))
::STARSHIP:: prompt --status=$STARSHIP_CMD_STATUS --pipestatus=$pipestatus --keymap=$STARSHIP_KEYMAP --cmd-duration=$STARSHIP_DURATION --jobs=$STARSHIP_JOBS
end

function fish_right_prompt
::STARSHIP:: prompt --right --status=$STARSHIP_CMD_STATUS --pipestatus=$pipestatus --keymap=$STARSHIP_KEYMAP --cmd-duration=$STARSHIP_DURATION --jobs=$STARSHIP_JOBS
end

# Disable virtualenv prompt, it breaks starship
Expand Down
1 change: 1 addition & 0 deletions src/init/starship.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,4 @@ VIRTUAL_ENV_DISABLE_PROMPT=1

setopt promptsubst
PROMPT='$(::STARSHIP:: prompt --keymap="$KEYMAP" --status="$STARSHIP_CMD_STATUS" --pipestatus ${STARSHIP_PIPE_STATUS[@]} --cmd-duration="$STARSHIP_DURATION" --jobs="$STARSHIP_JOBS_COUNT")'
RPROMPT='$(::STARSHIP:: prompt --right --keymap="$KEYMAP" --status="$STARSHIP_CMD_STATUS" --pipestatus ${STARSHIP_PIPE_STATUS[@]} --cmd-duration="$STARSHIP_DURATION" --jobs="$STARSHIP_JOBS_COUNT")'
5 changes: 5 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ fn main() {
.subcommand(
SubCommand::with_name("prompt")
.about("Prints the full starship prompt")
.arg(
Arg::with_name("right")
.long("right")
.help("Print the right prompt (instead of the standard left prompt)"),
)
.arg(&status_code_arg)
.arg(&pipestatus_arg)
.arg(&path_arg)
Expand Down
97 changes: 77 additions & 20 deletions src/print.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,18 +79,12 @@ pub fn get_prompt(context: Context) -> String {
buf.push_str("\x1b[J"); // An ASCII control code to clear screen
}

let formatter = if let Ok(formatter) = StringFormatter::new(config.format) {
formatter
} else {
log::error!("Error parsing `format`");
buf.push('>');
return buf;
};
let modules = formatter.get_variables();
let (formatter, modules) = load_formatter_and_modules(&context);

let formatter = formatter.map_variables_to_segments(|module| {
// Make $all display all modules
// Make $all display all modules not explicitly referenced
if module == "all" {
Some(Ok(PROMPT_ORDER
Some(Ok(all_modules_uniq(&modules)
.par_iter()
.flat_map(|module| {
handle_module(module, &context, &modules)
Expand Down Expand Up @@ -124,6 +118,11 @@ pub fn get_prompt(context: Context) -> String {
}
write!(buf, "{}", ANSIStrings(&module_strings)).unwrap();

if context.right {
// right prompts generally do not allow newlines
buf = buf.replace('\n', "");
}

// escape \n and ! characters for tcsh
if let Shell::Tcsh = context.shell {
buf = buf.replace('!', "\\!");
Expand Down Expand Up @@ -288,20 +287,13 @@ pub fn explain(args: ArgMatches) {
fn compute_modules<'a>(context: &'a Context) -> Vec<Module<'a>> {
let mut prompt_order: Vec<Module<'a>> = Vec::new();

let config = context.config.get_root_config();
let formatter = if let Ok(formatter) = StringFormatter::new(config.format) {
formatter
} else {
log::error!("Error parsing `format`");
return Vec::new();
};
let modules = formatter.get_variables();
let (_formatter, modules) = load_formatter_and_modules(context);

for module in &modules {
// Manually add all modules if `$all` is encountered
if module == "all" {
for module in PROMPT_ORDER {
let modules = handle_module(module, context, &modules);
for module in all_modules_uniq(&modules) {
let modules = handle_module(&module, context, &modules);
prompt_order.extend(modules);
}
} else {
Expand Down Expand Up @@ -403,3 +395,68 @@ pub fn format_duration(duration: &Duration) -> String {
format!("{:?}ms", &milis)
}
}

/// Return the modules from $all that are not already in the list
fn all_modules_uniq(module_list: &BTreeSet<String>) -> Vec<String> {
let mut prompt_order: Vec<String> = Vec::new();
for module in PROMPT_ORDER.iter() {
if !module_list.contains(*module) {
prompt_order.push(String::from(*module))
}
}

prompt_order
}

/// Load the correct formatter for the context (ie left prompt or right prompt)
/// and the list of all modules used in a format string
fn load_formatter_and_modules<'a>(context: &'a Context) -> (StringFormatter<'a>, BTreeSet<String>) {
let config = context.config.get_root_config();

let lformatter = StringFormatter::new(config.format);
let rformatter = StringFormatter::new(config.right_format);
if lformatter.is_err() {
log::error!("Error parsing `format`")
}
if rformatter.is_err() {
log::error!("Error parsing `right_format`")
}

match (lformatter, rformatter) {
(Ok(lf), Ok(rf)) => {
let mut modules: BTreeSet<String> = BTreeSet::new();
modules.extend(lf.get_variables());
modules.extend(rf.get_variables());
if context.right {
(rf, modules)
} else {
(lf, modules)
}
}
_ => (StringFormatter::raw(">"), BTreeSet::new()),
}
}

#[cfg(test)]
mod test {
use super::*;
use crate::config::StarshipConfig;
use crate::test::default_context;

#[test]
fn right_prompt() {
let mut context = default_context();
context.config = StarshipConfig {
config: Some(toml::toml! {
right_format="$character"
[character]
format=">\n>"
}),
};
context.right = true;

let expected = String::from(">>"); // should strip new lines
let actual = get_prompt(context);
assert_eq!(expected, actual);
}
}
19 changes: 12 additions & 7 deletions src/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@ static LOGGER: Lazy<()> = Lazy::new(|| {
log::set_boxed_logger(Box::new(logger)).unwrap();
});

pub fn default_context() -> Context<'static> {
let mut context = Context::new_with_shell_and_path(
clap::ArgMatches::default(),
Shell::Unknown,
PathBuf::new(),
PathBuf::new(),
);
context.config = StarshipConfig { config: None };
context
}

/// Render a specific starship module by name
pub struct ModuleRenderer<'a> {
name: &'a str,
Expand All @@ -43,13 +54,7 @@ impl<'a> ModuleRenderer<'a> {
// Start logger
Lazy::force(&LOGGER);

let mut context = Context::new_with_shell_and_path(
clap::ArgMatches::default(),
Shell::Unknown,
PathBuf::new(),
PathBuf::new(),
);
context.config = StarshipConfig { config: None };
let context = default_context();

Self { name, context }
}
Expand Down

0 comments on commit 79585dc

Please sign in to comment.