From 79585dcb17f904125e2881fa0182660fcd9002a8 Mon Sep 17 00:00:00 2001 From: "Matthew (Matt) Jeffryes" Date: Wed, 8 Sep 2021 12:45:27 -0700 Subject: [PATCH] feat: Add support for RPROMPT (right prompt) (#3026) Adds support for zsh, fish, and elvish. Co-authored-by: Matan Kushner --- docs/advanced-config/README.md | 31 ++++++++++ docs/config/README.md | 22 +++++-- src/configs/starship_root.rs | 4 ++ src/context.rs | 6 ++ src/formatter/string_formatter.rs | 9 +++ src/init/starship.elv | 16 ++++- src/init/starship.fish | 7 ++- src/init/starship.zsh | 1 + src/main.rs | 5 ++ src/print.rs | 97 ++++++++++++++++++++++++------- src/test/mod.rs | 19 +++--- 11 files changed, 180 insertions(+), 37 deletions(-) diff --git a/docs/advanced-config/README.md b/docs/advanced-config/README.md index 0efecc21..412b74de 100644 --- a/docs/advanced-config/README.md +++ b/docs/advanced-config/README.md @@ -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: diff --git a/docs/config/README.md b/docs/config/README.md index 8b7a6106..c7e94266 100644 --- a/docs/config/README.md +++ b/docs/config/README.md @@ -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 diff --git a/src/configs/starship_root.rs b/src/configs/starship_root.rs index 07500fbf..dfbf8722 100644 --- a/src/configs/starship_root.rs +++ b/src/configs/starship_root.rs @@ -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", diff --git a/src/context.rs b/src/context.rs index 6365e3c6..680d18ca 100644 --- a/src/context.rs +++ b/src/context.rs @@ -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)] diff --git a/src/formatter/string_formatter.rs b/src/formatter/string_formatter.rs index 99218cc4..1a570ee9 100644 --- a/src/formatter/string_formatter.rs +++ b/src/formatter/string_formatter.rs @@ -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 diff --git a/src/init/starship.elv b/src/init/starship.elv index 1b84632a..1f68f1c8 100644 --- a/src/init/starship.elv +++ b/src/init/starship.elv @@ -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 + } +} diff --git a/src/init/starship.fish b/src/init/starship.fish index 18969d2d..bf8944ca 100644 --- a/src/init/starship.fish +++ b/src/init/starship.fish @@ -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 diff --git a/src/init/starship.zsh b/src/init/starship.zsh index 9411423a..77ced513 100644 --- a/src/init/starship.zsh +++ b/src/init/starship.zsh @@ -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")' diff --git a/src/main.rs b/src/main.rs index b24ef228..0a7f0b4c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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) diff --git a/src/print.rs b/src/print.rs index 6ddddb41..a202cb86 100644 --- a/src/print.rs +++ b/src/print.rs @@ -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> { let mut prompt_order: Vec> = 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) -> Vec { + let mut prompt_order: Vec = 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) { + 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 = 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); + } +} diff --git a/src/test/mod.rs b/src/test/mod.rs index 31759a2f..8a3a7cdd 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -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 } }