From 4deaa02d6fb3e72f286d822ac4c987b763c415dc Mon Sep 17 00:00:00 2001 From: Ryan Cohen Date: Sat, 1 Jan 2022 08:12:11 -0500 Subject: feat: set a continuation prompt for supporting shells (#3322) * feat: set a continuation prompt for supporting shells (#3134) * docs: fixed wording of documentation * fix: continuation prompt is now only set once * fix(docs): fixed typo in advanced-config/README.md Co-authored-by: Segev Finer * fix: update --continuation argument Co-authored-by: David Knaack * fix: updated continuation prompt - PROMPT2 was fixed to be set only once in zsh. - `continuation_symbol` and `continuation_format` were removed in place of a single variable; `continuation_prompt`. - The continuation prompt was moved out of the character module. * fix: ran rustfmt * docs: updated continuation prompt docs Co-authored-by: Segev Finer Co-authored-by: David Knaack --- docs/advanced-config/README.md | 22 +++ src/configs/mod.rs | 2 + src/configs/starship_root.rs | 4 + src/context.rs | 5 + src/init/starship.bash | 4 + src/init/starship.ps1 | 120 +++++++++-------- src/init/starship.zsh | 1 + src/main.rs | 299 +++++++++++++++++++++-------------------- src/print.rs | 37 ++++- 9 files changed, 289 insertions(+), 205 deletions(-) mode change 100644 => 100755 src/init/starship.ps1 diff --git a/docs/advanced-config/README.md b/docs/advanced-config/README.md index 15698e330..4fc029830 100644 --- a/docs/advanced-config/README.md +++ b/docs/advanced-config/README.md @@ -141,6 +141,28 @@ Produces a prompt like the following: β–Ά starship on ξ‚  rprompt [!] is πŸ“¦ v0.57.0 via πŸ¦€ v1.54.0 took 17s ``` +## Continuation Prompt + +Some shells support a continuation prompt along with the normal prompt. This prompt is rendered instead of the normal prompt when the user has entered an incomplete statement (such as a single left parenthesis or quote). + +Starship can set the continuation prompt using the `continuation_prompt` option. The default prompt is `"[❯](bold yellow)"`. + +Note: `continuation_prompt` should be set to a literal string without any variables. + +Note: Continuation prompts are only available in the following shells: + + - `bash` + - `zsh` + - `PowerShell` + +### Example + +```toml +# ~/.config/starship.toml + +# A continuation prompt that displays two filled in arrows +continuation_prompt = "β–Άβ–Ά" +``` ## Style Strings diff --git a/src/configs/mod.rs b/src/configs/mod.rs index 4073bf561..2ac9fe612 100644 --- a/src/configs/mod.rs +++ b/src/configs/mod.rs @@ -80,6 +80,7 @@ pub struct FullConfig<'a> { // Root config pub format: String, pub right_format: String, + pub continuation_prompt: String, pub scan_timeout: u64, pub command_timeout: u64, pub add_newline: bool, @@ -158,6 +159,7 @@ impl<'a> Default for FullConfig<'a> { Self { format: "$all".to_string(), right_format: "".to_string(), + continuation_prompt: "[❯](bold yellow)".to_string(), scan_timeout: 30, command_timeout: 500, add_newline: true, diff --git a/src/configs/starship_root.rs b/src/configs/starship_root.rs index 8a70a2f53..a8461191e 100644 --- a/src/configs/starship_root.rs +++ b/src/configs/starship_root.rs @@ -8,6 +8,7 @@ use std::cmp::Ordering; pub struct StarshipRootConfig { pub format: String, pub right_format: String, + pub continuation_prompt: String, pub scan_timeout: u64, pub command_timeout: u64, pub add_newline: bool, @@ -95,6 +96,7 @@ impl<'a> Default for StarshipRootConfig { StarshipRootConfig { format: "$all".to_string(), right_format: "".to_string(), + continuation_prompt: "[❯](bold yellow)".to_string(), scan_timeout: 30, command_timeout: 500, add_newline: true, @@ -108,6 +110,7 @@ impl<'a> ModuleConfig<'a> for StarshipRootConfig { config.iter().for_each(|(k, v)| match k.as_str() { "format" => self.format.load_config(v), "right_format" => self.right_format.load_config(v), + "continuation_prompt" => self.continuation_prompt.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), @@ -122,6 +125,7 @@ impl<'a> ModuleConfig<'a> for StarshipRootConfig { // Root options "format", "right_format", + "continuation_prompt", "scan_timeout", "command_timeout", "add_newline", diff --git a/src/context.rs b/src/context.rs index a67fdc065..6c24c06d6 100644 --- a/src/context.rs +++ b/src/context.rs @@ -51,6 +51,9 @@ pub struct Context<'a> { /// Construct the right prompt instead of the left prompt pub right: bool, + /// Construct the continuation prompt instead of the normal prompt + pub continuation: bool, + /// Width of terminal, or zero if width cannot be detected. pub width: usize, @@ -134,6 +137,7 @@ impl<'a> Context<'a> { .map_or_else(StarshipRootConfig::default, StarshipRootConfig::load); let right = arguments.is_present("right"); + let continuation = arguments.is_present("continuation"); let width = arguments .value_of("terminal_width") @@ -151,6 +155,7 @@ impl<'a> Context<'a> { repo: OnceCell::new(), shell, right, + continuation, width, #[cfg(test)] env: HashMap::new(), diff --git a/src/init/starship.bash b/src/init/starship.bash index abff609c6..6d1d2ae31 100644 --- a/src/init/starship.bash +++ b/src/init/starship.bash @@ -100,3 +100,7 @@ export STARSHIP_SHELL="bash" STARSHIP_SESSION_KEY="$RANDOM$RANDOM$RANDOM$RANDOM$RANDOM"; # Random generates a number b/w 0 - 32767 STARSHIP_SESSION_KEY="${STARSHIP_SESSION_KEY}0000000000000000" # Pad it to 16+ chars. export STARSHIP_SESSION_KEY=${STARSHIP_SESSION_KEY:0:16}; # Trim to 16-digits if excess. + +# Set the continuation prompt +PS2="$(::STARSHIP:: prompt --continuation)" + diff --git a/src/init/starship.ps1 b/src/init/starship.ps1 old mode 100644 new mode 100755 index 2fc6b0460..266ad2c65 --- a/src/init/starship.ps1 +++ b/src/init/starship.ps1 @@ -1,7 +1,6 @@ #!/usr/bin/env pwsh -function global:prompt { - +function Get-Arguments { function Get-Cwd { $cwd = Get-Location $provider_prefix = "$($cwd.Provider.ModuleName)\$($cwd.Provider.Name)::" @@ -22,57 +21,6 @@ function global:prompt { } } - function Invoke-Native { - param($Executable, $Arguments) - $startInfo = New-Object System.Diagnostics.ProcessStartInfo -ArgumentList $Executable -Property @{ - StandardOutputEncoding = [System.Text.Encoding]::UTF8; - RedirectStandardOutput = $true; - RedirectStandardError = $true; - CreateNoWindow = $true; - UseShellExecute = $false; - }; - if ($startInfo.ArgumentList.Add) { - # PowerShell 6+ uses .NET 5+ and supports the ArgumentList property - # which bypasses the need for manually escaping the argument list into - # a command string. - foreach ($arg in $Arguments) { - $startInfo.ArgumentList.Add($arg); - } - } - else { - # Build an arguments string which follows the C++ command-line argument quoting rules - # See: https://docs.microsoft.com/en-us/previous-versions//17w5ykft(v=vs.85)?redirectedfrom=MSDN - $escaped = $Arguments | ForEach-Object { - $s = $_ -Replace '(\\+)"','$1$1"'; # Escape backslash chains immediately preceding quote marks. - $s = $s -Replace '(\\+)$','$1$1'; # Escape backslash chains immediately preceding the end of the string. - $s = $s -Replace '"','\"'; # Escape quote marks. - "`"$s`"" # Quote the argument. - } - $startInfo.Arguments = $escaped -Join ' '; - } - $process = [System.Diagnostics.Process]::Start($startInfo) - - # stderr isn't displayed with this style of invocation - # Manually write it to console - $stderr = $process.StandardError.ReadToEnd().Trim() - if ($stderr -ne '') { - # Write-Error doesn't work here - $host.ui.WriteErrorLine($stderr) - } - - $process.StandardOutput.ReadToEnd(); - } - - $origDollarQuestion = $global:? - $origLastExitCode = $global:LASTEXITCODE - - # Invoke precmd, if specified - try { - if (Test-Path function:Invoke-Starship-PreCommand) { - Invoke-Starship-PreCommand - } - } catch {} - # @ makes sure the result is an array even if single or no values are returned $jobs = @(Get-Job | Where-Object { $_.State -eq 'Running' }).Count @@ -103,6 +51,64 @@ function global:prompt { $arguments += "--status=$($lastExitCodeForPrompt)" + return $arguments +} + +function Invoke-Native { + param($Executable, $Arguments) + $startInfo = New-Object System.Diagnostics.ProcessStartInfo -ArgumentList $Executable -Property @{ + StandardOutputEncoding = [System.Text.Encoding]::UTF8; + RedirectStandardOutput = $true; + RedirectStandardError = $true; + CreateNoWindow = $true; + UseShellExecute = $false; + }; + if ($startInfo.ArgumentList.Add) { + # PowerShell 6+ uses .NET 5+ and supports the ArgumentList property + # which bypasses the need for manually escaping the argument list into + # a command string. + foreach ($arg in $Arguments) { + $startInfo.ArgumentList.Add($arg); + } + } + else { + # Build an arguments string which follows the C++ command-line argument quoting rules + # See: https://docs.microsoft.com/en-us/previous-versions//17w5ykft(v=vs.85)?redirectedfrom=MSDN + $escaped = $Arguments | ForEach-Object { + $s = $_ -Replace '(\\+)"','$1$1"'; # Escape backslash chains immediately preceding quote marks. + $s = $s -Replace '(\\+)$','$1$1'; # Escape backslash chains immediately preceding the end of the string. + $s = $s -Replace '"','\"'; # Escape quote marks. + "`"$s`"" # Quote the argument. + } + $startInfo.Arguments = $escaped -Join ' '; + } + $process = [System.Diagnostics.Process]::Start($startInfo) + + # stderr isn't displayed with this style of invocation + # Manually write it to console + $stderr = $process.StandardError.ReadToEnd().Trim() + if ($stderr -ne '') { + # Write-Error doesn't work here + $host.ui.WriteErrorLine($stderr) + } + + $process.StandardOutput.ReadToEnd(); +} + +function global:prompt { + $origDollarQuestion = $global:? + $origLastExitCode = $global:LASTEXITCODE + + # Invoke precmd, if specified + try { + if (Test-Path function:Invoke-Starship-PreCommand) { + Invoke-Starship-PreCommand + } + } catch {} + + # Get arguments for starship prompt + $arguments = Get-Arguments + # Invoke Starship Invoke-Native -Executable ::STARSHIP:: -Arguments $arguments @@ -130,6 +136,14 @@ function global:prompt { } +# Get arguments for starship continuation prompt +$arguments = Get-Arguments +$arguments += "--continuation" + +# Invoke Starship and set continuation prompt +$continuation = Invoke-Native -Executable ::STARSHIP:: -Arguments $arguments +Set-PSReadLineOption -ContinuationPrompt $continuation + # Disable virtualenv prompt, it breaks starship $ENV:VIRTUAL_ENV_DISABLE_PROMPT=1 diff --git a/src/init/starship.zsh b/src/init/starship.zsh index c4a2f9f9c..da2fbf5ac 100644 --- a/src/init/starship.zsh +++ b/src/init/starship.zsh @@ -94,4 +94,5 @@ setopt promptsubst PROMPT='$(::STARSHIP:: prompt --terminal-width="$COLUMNS" --keymap="${KEYMAP:-}" --status="$STARSHIP_CMD_STATUS" --pipestatus="${STARSHIP_PIPE_STATUS[*]}" --cmd-duration="${STARSHIP_DURATION:-}" --jobs="$STARSHIP_JOBS_COUNT")' RPROMPT='$(::STARSHIP:: prompt --right --terminal-width="$COLUMNS" --keymap="${KEYMAP:-}" --status="$STARSHIP_CMD_STATUS" --pipestatus="${STARSHIP_PIPE_STATUS[*]}" --cmd-duration="${STARSHIP_DURATION:-}" --jobs="$STARSHIP_JOBS_COUNT")' +PROMPT2="$(::STARSHIP:: prompt --continuation)" diff --git a/src/main.rs b/src/main.rs index 88c9be2dd..028e98389 100644 --- a/src/main.rs +++ b/src/main.rs @@ -88,153 +88,160 @@ fn main() { .help("Print the main initialization script (as opposed to the init stub)"); let long_version = crate::shadow::clap_version(); - let mut app = - App::new("starship") - .about("The cross-shell prompt for astronauts. β˜„πŸŒŒοΈ") - // pull the version number from Cargo.toml - .version(shadow::PKG_VERSION) - .long_version(long_version.as_str()) - // pull the authors from Cargo.toml - .author(crate_authors!()) - .after_help("https://github.com/starship/starship") - .setting(AppSettings::SubcommandRequiredElseHelp) - .subcommand( - SubCommand::with_name("init") - .about("Prints the shell function used to execute starship") - .arg(&shell_arg) - .arg(&init_scripts_arg), - ) - .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(&terminal_width_arg) - .arg(&path_arg) - .arg(&logical_path_arg) - .arg(&cmd_duration_arg) - .arg(&keymap_arg) - .arg(&jobs_arg), - ) - .subcommand( - SubCommand::with_name("module") - .about("Prints a specific prompt module") - .arg( - Arg::with_name("name") - .help("The name of the module to be printed") - .required(true) - .required_unless("list"), - ) - .arg( - Arg::with_name("list") - .short("l") - .long("list") - .help("List out all supported modules"), - ) - .arg(&status_code_arg) - .arg(&pipestatus_arg) - .arg(&terminal_width_arg) - .arg(&path_arg) - .arg(&logical_path_arg) - .arg(&cmd_duration_arg) - .arg(&keymap_arg) - .arg(&jobs_arg), - ) - .subcommand( - SubCommand::with_name("config") - .alias("configure") - .about("Edit the starship configuration") - .arg( - Arg::with_name("name") - .help("Configuration key to edit") - .required(false) - .requires("value"), - ) - .arg(Arg::with_name("value").help("Value to place into that key")), - ) - .subcommand( - SubCommand::with_name("print-config") - .about("Prints the computed starship configuration") - .arg( - Arg::with_name("default") - .short("d") - .long("default") - .help("Print the default instead of the computed config") - .takes_value(false), - ) - .arg( - Arg::with_name("name") - .help("Configuration keys to print") - .multiple(true) - .required(false), - ), - ) - .subcommand( - SubCommand::with_name("toggle") - .about("Toggle a given starship module") - .arg( - Arg::with_name("name") - .help("The name of the module to be toggled") - .required(true), - ) - .arg( - Arg::with_name("key") - .help("The key of the config to be toggled") - .required(false) - .required_unless("name"), - ), - ) - .subcommand(SubCommand::with_name("bug-report").about( + let mut app = App::new("starship") + .about("The cross-shell prompt for astronauts. β˜„πŸŒŒοΈ") + // pull the version number from Cargo.toml + .version(shadow::PKG_VERSION) + .long_version(long_version.as_str()) + // pull the authors from Cargo.toml + .author(crate_authors!()) + .after_help("https://github.com/starship/starship") + .setting(AppSettings::SubcommandRequiredElseHelp) + .subcommand( + SubCommand::with_name("init") + .about("Prints the shell function used to execute starship") + .arg(&shell_arg) + .arg(&init_scripts_arg), + ) + .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( + Arg::with_name("continuation") + .long("continuation") + .help("Print the continuation prompt (instead of the standard left prompt)") + .conflicts_with("right"), + ) + .arg(&status_code_arg) + .arg(&pipestatus_arg) + .arg(&terminal_width_arg) + .arg(&path_arg) + .arg(&logical_path_arg) + .arg(&cmd_duration_arg) + .arg(&keymap_arg) + .arg(&jobs_arg), + ) + .subcommand( + SubCommand::with_name("module") + .about("Prints a specific prompt module") + .arg( + Arg::with_name("name") + .help("The name of the module to be printed") + .required(true) + .required_unless("list"), + ) + .arg( + Arg::with_name("list") + .short("l") + .long("list") + .help("List out all supported modules"), + ) + .arg(&status_code_arg) + .arg(&pipestatus_arg) + .arg(&terminal_width_arg) + .arg(&path_arg) + .arg(&logical_path_arg) + .arg(&cmd_duration_arg) + .arg(&keymap_arg) + .arg(&jobs_arg), + ) + .subcommand( + SubCommand::with_name("config") + .alias("configure") + .about("Edit the starship configuration") + .arg( + Arg::with_name("name") + .help("Configuration key to edit") + .required(false) + .requires("value"), + ) + .arg(Arg::with_name("value").help("Value to place into that key")), + ) + .subcommand( + SubCommand::with_name("print-config") + .about("Prints the computed starship configuration") + .arg( + Arg::with_name("default") + .short("d") + .long("default") + .help("Print the default instead of the computed config") + .takes_value(false), + ) + .arg( + Arg::with_name("name") + .help("Configuration keys to print") + .multiple(true) + .required(false), + ), + ) + .subcommand( + SubCommand::with_name("toggle") + .about("Toggle a given starship module") + .arg( + Arg::with_name("name") + .help("The name of the module to be toggled") + .required(true), + ) + .arg( + Arg::with_name("key") + .help("The key of the config to be toggled") + .required(false) + .required_unless("name"), + ), + ) + .subcommand( + SubCommand::with_name("bug-report").about( "Create a pre-populated GitHub issue with information about your configuration", - )) - .subcommand( - SubCommand::with_name("time") - .about("Prints time in milliseconds") - .settings(&[AppSettings::Hidden]), - ) - .subcommand( - SubCommand::with_name("explain") - .about("Explains the currently showing modules") - .arg(&status_code_arg) - .arg(&pipestatus_arg) - .arg(&terminal_width_arg) - .arg(&path_arg) - .arg(&logical_path_arg) - .arg(&cmd_duration_arg) - .arg(&keymap_arg) - .arg(&jobs_arg), - ) - .subcommand( - SubCommand::with_name("timings") - .about("Prints timings of all active modules") - .arg(&status_code_arg) - .arg(&pipestatus_arg) - .arg(&terminal_width_arg) - .arg(&path_arg) - .arg(&logical_path_arg) - .arg(&cmd_duration_arg) - .arg(&keymap_arg) - .arg(&jobs_arg), - ) - .subcommand( - SubCommand::with_name("completions") - .about("Generate starship shell completions for your shell to stdout") - .arg( - Arg::with_name("shell") - .takes_value(true) - .possible_values(&Shell::variants()) - .help("the shell to generate completions for") - .value_name("SHELL") - .required(true) - .env("STARSHIP_SHELL"), - ), - ) - .subcommand(SubCommand::with_name("session").about("Generate random session key")); + ), + ) + .subcommand( + SubCommand::with_name("time") + .about("Prints time in milliseconds") + .settings(&[AppSettings::Hidden]), + ) + .subcommand( + SubCommand::with_name("explain") + .about("Explains the currently showing modules") + .arg(&status_code_arg) + .arg(&pipestatus_arg) + .arg(&terminal_width_arg) + .arg(&path_arg) + .arg(&logical_path_arg) + .arg(&cmd_duration_arg) + .arg(&keymap_arg) + .arg(&jobs_arg), + ) + .subcommand( + SubCommand::with_name("timings") + .about("Prints timings of all active modules") + .arg(&status_code_arg) + .arg(&pipestatus_arg) + .arg(&terminal_width_arg) + .arg(&path_arg) + .arg(&logical_path_arg) + .arg(&cmd_duration_arg) + .arg(&keymap_arg) + .arg(&jobs_arg), + ) + .subcommand( + SubCommand::with_name("completions") + .about("Generate starship shell completions for your shell to stdout") + .arg( + Arg::with_name("shell") + .takes_value(true) + .possible_values(&Shell::variants()) + .help("the shell to generate completions for") + .value_name("SHELL") + .required(true) + .env("STARSHIP_SHELL"), + ), + ) + .subcommand(SubCommand::with_name("session").about("Generate random session key")); let matches = app.clone().get_matches(); diff --git a/src/print.rs b/src/print.rs index 11d834128..b7db0a10b 100644 --- a/src/print.rs +++ b/src/print.rs @@ -114,7 +114,8 @@ pub fn get_prompt(context: Context) -> String { ); let module_strings = root_module.ansi_strings_for_shell(context.shell, Some(context.width)); - if config.add_newline { + if config.add_newline && !context.continuation { + // continuation prompts normally do not include newlines, but they can writeln!(buf).unwrap(); } write!(buf, "{}", ANSIStrings(&module_strings)).unwrap(); @@ -416,19 +417,27 @@ fn load_formatter_and_modules<'a>(context: &'a Context) -> (StringFormatter<'a>, let lformatter = StringFormatter::new(&config.format); let rformatter = StringFormatter::new(&config.right_format); + let cformatter = StringFormatter::new(&config.continuation_prompt); if lformatter.is_err() { log::error!("Error parsing `format`") } if rformatter.is_err() { log::error!("Error parsing `right_format`") } + if cformatter.is_err() { + log::error!("Error parsing `continuation_prompt`") + } - match (lformatter, rformatter) { - (Ok(lf), Ok(rf)) => { + match (lformatter, rformatter, cformatter) { + (Ok(lf), Ok(rf), Ok(cf)) => { let mut modules: BTreeSet = BTreeSet::new(); - modules.extend(lf.get_variables()); - modules.extend(rf.get_variables()); - if context.right { + if !context.continuation { + modules.extend(lf.get_variables()); + modules.extend(rf.get_variables()); + } + if context.continuation { + (cf, modules) + } else if context.right { (rf, modules) } else { (lf, modules) @@ -461,4 +470,20 @@ mod test { let actual = get_prompt(context); assert_eq!(expected, actual); } + + #[test] + fn continuation_prompt() { + let mut context = default_context(); + context.config = StarshipConfig { + config: Some(toml::toml! { + continuation_prompt="><>" + }), + }; + context.root_config.continuation_prompt = "><>".to_string(); + context.continuation = true; + + let expected = String::from("><>"); + let actual = get_prompt(context); + assert_eq!(expected, actual); + } } -- cgit v1.2.3