feat: Add support for RPROMPT (right prompt) (#3026)

Adds support for zsh, fish, and elvish.

Co-authored-by: Matan Kushner <hello@matchai.dev>
This commit is contained in:
Matthew (Matt) Jeffryes 2021-09-08 12:45:27 -07:00 committed by GitHub
parent cb8dca2101
commit 79585dcb17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 180 additions and 37 deletions

View File

@ -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:

View File

@ -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
@ -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

View File

@ -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,
@ -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,
@ -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),
@ -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",

View File

@ -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>,
@ -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,
@ -129,6 +134,7 @@ impl<'a> Context<'a> {
dir_contents: OnceCell::new(),
repo: OnceCell::new(),
shell,
right,
#[cfg(test)]
env: HashMap::new(),
#[cfg(test)]

View File

@ -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

View File

@ -4,6 +4,7 @@ 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)
@ -11,6 +12,7 @@ fn starship-after-readline-hook [line]{
fn starship-before-readline-hook {
cmd-end-time = (::STARSHIP:: time)
cmd-duration = (- $cmd-end-time $cmd-start-time)
}
# Install Hooks
@ -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
}
}

View File

@ -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

View File

@ -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")'

View File

@ -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)

View File

@ -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)
@ -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('!', "\\!");
@ -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 {
@ -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);
}
}

View File

@ -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,
@ -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 }
}