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 <segev208@gmail.com>

* fix: update --continuation argument

Co-authored-by: David Knaack <davidkna@users.noreply.github.com>

* 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 <segev208@gmail.com>
Co-authored-by: David Knaack <davidkna@users.noreply.github.com>
This commit is contained in:
Ryan Cohen 2022-01-01 08:12:11 -05:00 committed by GitHub
parent 3f97068538
commit 4deaa02d6f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 289 additions and 205 deletions

View File

@ -141,6 +141,28 @@ Produces a prompt like the following:
▶ starship on  rprompt [!] is 📦 v0.57.0 via 🦀 v1.54.0 took 17s ▶ 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 ## Style Strings

View File

@ -80,6 +80,7 @@ pub struct FullConfig<'a> {
// Root config // Root config
pub format: String, pub format: String,
pub right_format: String, pub right_format: String,
pub continuation_prompt: String,
pub scan_timeout: u64, pub scan_timeout: u64,
pub command_timeout: u64, pub command_timeout: u64,
pub add_newline: bool, pub add_newline: bool,
@ -158,6 +159,7 @@ impl<'a> Default for FullConfig<'a> {
Self { Self {
format: "$all".to_string(), format: "$all".to_string(),
right_format: "".to_string(), right_format: "".to_string(),
continuation_prompt: "[](bold yellow)".to_string(),
scan_timeout: 30, scan_timeout: 30,
command_timeout: 500, command_timeout: 500,
add_newline: true, add_newline: true,

View File

@ -8,6 +8,7 @@ use std::cmp::Ordering;
pub struct StarshipRootConfig { pub struct StarshipRootConfig {
pub format: String, pub format: String,
pub right_format: String, pub right_format: String,
pub continuation_prompt: String,
pub scan_timeout: u64, pub scan_timeout: u64,
pub command_timeout: u64, pub command_timeout: u64,
pub add_newline: bool, pub add_newline: bool,
@ -95,6 +96,7 @@ impl<'a> Default for StarshipRootConfig {
StarshipRootConfig { StarshipRootConfig {
format: "$all".to_string(), format: "$all".to_string(),
right_format: "".to_string(), right_format: "".to_string(),
continuation_prompt: "[](bold yellow)".to_string(),
scan_timeout: 30, scan_timeout: 30,
command_timeout: 500, command_timeout: 500,
add_newline: true, add_newline: true,
@ -108,6 +110,7 @@ impl<'a> ModuleConfig<'a> for StarshipRootConfig {
config.iter().for_each(|(k, v)| match k.as_str() { config.iter().for_each(|(k, v)| match k.as_str() {
"format" => self.format.load_config(v), "format" => self.format.load_config(v),
"right_format" => self.right_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), "scan_timeout" => self.scan_timeout.load_config(v),
"command_timeout" => self.command_timeout.load_config(v), "command_timeout" => self.command_timeout.load_config(v),
"add_newline" => self.add_newline.load_config(v), "add_newline" => self.add_newline.load_config(v),
@ -122,6 +125,7 @@ impl<'a> ModuleConfig<'a> for StarshipRootConfig {
// Root options // Root options
"format", "format",
"right_format", "right_format",
"continuation_prompt",
"scan_timeout", "scan_timeout",
"command_timeout", "command_timeout",
"add_newline", "add_newline",

View File

@ -51,6 +51,9 @@ pub struct Context<'a> {
/// Construct the right prompt instead of the left prompt /// Construct the right prompt instead of the left prompt
pub right: bool, 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. /// Width of terminal, or zero if width cannot be detected.
pub width: usize, pub width: usize,
@ -134,6 +137,7 @@ impl<'a> Context<'a> {
.map_or_else(StarshipRootConfig::default, StarshipRootConfig::load); .map_or_else(StarshipRootConfig::default, StarshipRootConfig::load);
let right = arguments.is_present("right"); let right = arguments.is_present("right");
let continuation = arguments.is_present("continuation");
let width = arguments let width = arguments
.value_of("terminal_width") .value_of("terminal_width")
@ -151,6 +155,7 @@ impl<'a> Context<'a> {
repo: OnceCell::new(), repo: OnceCell::new(),
shell, shell,
right, right,
continuation,
width, width,
#[cfg(test)] #[cfg(test)]
env: HashMap::new(), env: HashMap::new(),

View File

@ -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="$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. 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. export STARSHIP_SESSION_KEY=${STARSHIP_SESSION_KEY:0:16}; # Trim to 16-digits if excess.
# Set the continuation prompt
PS2="$(::STARSHIP:: prompt --continuation)"

120
src/init/starship.ps1 Normal file → Executable file
View File

@ -1,7 +1,6 @@
#!/usr/bin/env pwsh #!/usr/bin/env pwsh
function global:prompt { function Get-Arguments {
function Get-Cwd { function Get-Cwd {
$cwd = Get-Location $cwd = Get-Location
$provider_prefix = "$($cwd.Provider.ModuleName)\$($cwd.Provider.Name)::" $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 # @ makes sure the result is an array even if single or no values are returned
$jobs = @(Get-Job | Where-Object { $_.State -eq 'Running' }).Count $jobs = @(Get-Job | Where-Object { $_.State -eq 'Running' }).Count
@ -103,6 +51,64 @@ function global:prompt {
$arguments += "--status=$($lastExitCodeForPrompt)" $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 Starship
Invoke-Native -Executable ::STARSHIP:: -Arguments $arguments 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 # Disable virtualenv prompt, it breaks starship
$ENV:VIRTUAL_ENV_DISABLE_PROMPT=1 $ENV:VIRTUAL_ENV_DISABLE_PROMPT=1

View File

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

View File

@ -88,153 +88,160 @@ fn main() {
.help("Print the main initialization script (as opposed to the init stub)"); .help("Print the main initialization script (as opposed to the init stub)");
let long_version = crate::shadow::clap_version(); let long_version = crate::shadow::clap_version();
let mut app = let mut app = App::new("starship")
App::new("starship") .about("The cross-shell prompt for astronauts. ☄🌌️")
.about("The cross-shell prompt for astronauts. ☄🌌️") // pull the version number from Cargo.toml
// pull the version number from Cargo.toml .version(shadow::PKG_VERSION)
.version(shadow::PKG_VERSION) .long_version(long_version.as_str())
.long_version(long_version.as_str()) // pull the authors from Cargo.toml
// pull the authors from Cargo.toml .author(crate_authors!())
.author(crate_authors!()) .after_help("https://github.com/starship/starship")
.after_help("https://github.com/starship/starship") .setting(AppSettings::SubcommandRequiredElseHelp)
.setting(AppSettings::SubcommandRequiredElseHelp) .subcommand(
.subcommand( SubCommand::with_name("init")
SubCommand::with_name("init") .about("Prints the shell function used to execute starship")
.about("Prints the shell function used to execute starship") .arg(&shell_arg)
.arg(&shell_arg) .arg(&init_scripts_arg),
.arg(&init_scripts_arg), )
) .subcommand(
.subcommand( SubCommand::with_name("prompt")
SubCommand::with_name("prompt") .about("Prints the full starship prompt")
.about("Prints the full starship prompt") .arg(
.arg( Arg::with_name("right")
Arg::with_name("right") .long("right")
.long("right") .help("Print the right prompt (instead of the standard left prompt)"),
.help("Print the right prompt (instead of the standard left prompt)"), )
) .arg(
.arg(&status_code_arg) Arg::with_name("continuation")
.arg(&pipestatus_arg) .long("continuation")
.arg(&terminal_width_arg) .help("Print the continuation prompt (instead of the standard left prompt)")
.arg(&path_arg) .conflicts_with("right"),
.arg(&logical_path_arg) )
.arg(&cmd_duration_arg) .arg(&status_code_arg)
.arg(&keymap_arg) .arg(&pipestatus_arg)
.arg(&jobs_arg), .arg(&terminal_width_arg)
) .arg(&path_arg)
.subcommand( .arg(&logical_path_arg)
SubCommand::with_name("module") .arg(&cmd_duration_arg)
.about("Prints a specific prompt module") .arg(&keymap_arg)
.arg( .arg(&jobs_arg),
Arg::with_name("name") )
.help("The name of the module to be printed") .subcommand(
.required(true) SubCommand::with_name("module")
.required_unless("list"), .about("Prints a specific prompt module")
) .arg(
.arg( Arg::with_name("name")
Arg::with_name("list") .help("The name of the module to be printed")
.short("l") .required(true)
.long("list") .required_unless("list"),
.help("List out all supported modules"), )
) .arg(
.arg(&status_code_arg) Arg::with_name("list")
.arg(&pipestatus_arg) .short("l")
.arg(&terminal_width_arg) .long("list")
.arg(&path_arg) .help("List out all supported modules"),
.arg(&logical_path_arg) )
.arg(&cmd_duration_arg) .arg(&status_code_arg)
.arg(&keymap_arg) .arg(&pipestatus_arg)
.arg(&jobs_arg), .arg(&terminal_width_arg)
) .arg(&path_arg)
.subcommand( .arg(&logical_path_arg)
SubCommand::with_name("config") .arg(&cmd_duration_arg)
.alias("configure") .arg(&keymap_arg)
.about("Edit the starship configuration") .arg(&jobs_arg),
.arg( )
Arg::with_name("name") .subcommand(
.help("Configuration key to edit") SubCommand::with_name("config")
.required(false) .alias("configure")
.requires("value"), .about("Edit the starship configuration")
) .arg(
.arg(Arg::with_name("value").help("Value to place into that key")), Arg::with_name("name")
) .help("Configuration key to edit")
.subcommand( .required(false)
SubCommand::with_name("print-config") .requires("value"),
.about("Prints the computed starship configuration") )
.arg( .arg(Arg::with_name("value").help("Value to place into that key")),
Arg::with_name("default") )
.short("d") .subcommand(
.long("default") SubCommand::with_name("print-config")
.help("Print the default instead of the computed config") .about("Prints the computed starship configuration")
.takes_value(false), .arg(
) Arg::with_name("default")
.arg( .short("d")
Arg::with_name("name") .long("default")
.help("Configuration keys to print") .help("Print the default instead of the computed config")
.multiple(true) .takes_value(false),
.required(false), )
), .arg(
) Arg::with_name("name")
.subcommand( .help("Configuration keys to print")
SubCommand::with_name("toggle") .multiple(true)
.about("Toggle a given starship module") .required(false),
.arg( ),
Arg::with_name("name") )
.help("The name of the module to be toggled") .subcommand(
.required(true), SubCommand::with_name("toggle")
) .about("Toggle a given starship module")
.arg( .arg(
Arg::with_name("key") Arg::with_name("name")
.help("The key of the config to be toggled") .help("The name of the module to be toggled")
.required(false) .required(true),
.required_unless("name"), )
), .arg(
) Arg::with_name("key")
.subcommand(SubCommand::with_name("bug-report").about( .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", "Create a pre-populated GitHub issue with information about your configuration",
)) ),
.subcommand( )
SubCommand::with_name("time") .subcommand(
.about("Prints time in milliseconds") SubCommand::with_name("time")
.settings(&[AppSettings::Hidden]), .about("Prints time in milliseconds")
) .settings(&[AppSettings::Hidden]),
.subcommand( )
SubCommand::with_name("explain") .subcommand(
.about("Explains the currently showing modules") SubCommand::with_name("explain")
.arg(&status_code_arg) .about("Explains the currently showing modules")
.arg(&pipestatus_arg) .arg(&status_code_arg)
.arg(&terminal_width_arg) .arg(&pipestatus_arg)
.arg(&path_arg) .arg(&terminal_width_arg)
.arg(&logical_path_arg) .arg(&path_arg)
.arg(&cmd_duration_arg) .arg(&logical_path_arg)
.arg(&keymap_arg) .arg(&cmd_duration_arg)
.arg(&jobs_arg), .arg(&keymap_arg)
) .arg(&jobs_arg),
.subcommand( )
SubCommand::with_name("timings") .subcommand(
.about("Prints timings of all active modules") SubCommand::with_name("timings")
.arg(&status_code_arg) .about("Prints timings of all active modules")
.arg(&pipestatus_arg) .arg(&status_code_arg)
.arg(&terminal_width_arg) .arg(&pipestatus_arg)
.arg(&path_arg) .arg(&terminal_width_arg)
.arg(&logical_path_arg) .arg(&path_arg)
.arg(&cmd_duration_arg) .arg(&logical_path_arg)
.arg(&keymap_arg) .arg(&cmd_duration_arg)
.arg(&jobs_arg), .arg(&keymap_arg)
) .arg(&jobs_arg),
.subcommand( )
SubCommand::with_name("completions") .subcommand(
.about("Generate starship shell completions for your shell to stdout") SubCommand::with_name("completions")
.arg( .about("Generate starship shell completions for your shell to stdout")
Arg::with_name("shell") .arg(
.takes_value(true) Arg::with_name("shell")
.possible_values(&Shell::variants()) .takes_value(true)
.help("the shell to generate completions for") .possible_values(&Shell::variants())
.value_name("SHELL") .help("the shell to generate completions for")
.required(true) .value_name("SHELL")
.env("STARSHIP_SHELL"), .required(true)
), .env("STARSHIP_SHELL"),
) ),
.subcommand(SubCommand::with_name("session").about("Generate random session key")); )
.subcommand(SubCommand::with_name("session").about("Generate random session key"));
let matches = app.clone().get_matches(); let matches = app.clone().get_matches();

View File

@ -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)); 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(); writeln!(buf).unwrap();
} }
write!(buf, "{}", ANSIStrings(&module_strings)).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 lformatter = StringFormatter::new(&config.format);
let rformatter = StringFormatter::new(&config.right_format); let rformatter = StringFormatter::new(&config.right_format);
let cformatter = StringFormatter::new(&config.continuation_prompt);
if lformatter.is_err() { if lformatter.is_err() {
log::error!("Error parsing `format`") log::error!("Error parsing `format`")
} }
if rformatter.is_err() { if rformatter.is_err() {
log::error!("Error parsing `right_format`") log::error!("Error parsing `right_format`")
} }
if cformatter.is_err() {
log::error!("Error parsing `continuation_prompt`")
}
match (lformatter, rformatter) { match (lformatter, rformatter, cformatter) {
(Ok(lf), Ok(rf)) => { (Ok(lf), Ok(rf), Ok(cf)) => {
let mut modules: BTreeSet<String> = BTreeSet::new(); let mut modules: BTreeSet<String> = BTreeSet::new();
modules.extend(lf.get_variables()); if !context.continuation {
modules.extend(rf.get_variables()); modules.extend(lf.get_variables());
if context.right { modules.extend(rf.get_variables());
}
if context.continuation {
(cf, modules)
} else if context.right {
(rf, modules) (rf, modules)
} else { } else {
(lf, modules) (lf, modules)
@ -461,4 +470,20 @@ mod test {
let actual = get_prompt(context); let actual = get_prompt(context);
assert_eq!(expected, actual); 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);
}
} }