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:
parent
cb8dca2101
commit
79585dcb17
|
@ -85,6 +85,37 @@ function set_win_title(){
|
||||||
starship_precmd_user_func="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
|
||||||
|
|
||||||
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:
|
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:
|
||||||
|
|
|
@ -152,12 +152,14 @@ This is the list of prompt-wide configuration options.
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
| ----------------- | ------------------------------ | ------------------------------------------------------------ |
|
| ----------------- | ------------------------------- | ---------------------------------------------------------------- |
|
||||||
| `format` | [link](#default-prompt-format) | Configure the format of the prompt. |
|
| `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). |
|
| `scan_timeout` | `30` | Timeout for starship to scan files (in milliseconds). |
|
||||||
| `command_timeout` | `500` | Timeout for commands executed by starship (in milliseconds). |
|
| `command_timeout` | `500` | Timeout for commands executed by starship (in milliseconds). |
|
||||||
| `add_newline` | `true` | Inserts blank line between shell prompts. |
|
| `add_newline` | `true` | Inserts blank line between shell prompts. |
|
||||||
|
|
||||||
|
|
||||||
### Example
|
### Example
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
|
@ -247,6 +249,14 @@ $shell\
|
||||||
$character"""
|
$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
|
## AWS
|
||||||
|
|
||||||
The `aws` module shows the current AWS region and profile. This is based on
|
The `aws` module shows the current AWS region and profile. This is based on
|
||||||
|
|
|
@ -7,6 +7,7 @@ use std::cmp::Ordering;
|
||||||
#[derive(Clone, Serialize)]
|
#[derive(Clone, Serialize)]
|
||||||
pub struct StarshipRootConfig<'a> {
|
pub struct StarshipRootConfig<'a> {
|
||||||
pub format: &'a str,
|
pub format: &'a str,
|
||||||
|
pub right_format: &'a str,
|
||||||
pub scan_timeout: u64,
|
pub scan_timeout: u64,
|
||||||
pub command_timeout: u64,
|
pub command_timeout: u64,
|
||||||
pub add_newline: bool,
|
pub add_newline: bool,
|
||||||
|
@ -90,6 +91,7 @@ impl<'a> Default for StarshipRootConfig<'a> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
StarshipRootConfig {
|
StarshipRootConfig {
|
||||||
format: "$all",
|
format: "$all",
|
||||||
|
right_format: "",
|
||||||
scan_timeout: 30,
|
scan_timeout: 30,
|
||||||
command_timeout: 500,
|
command_timeout: 500,
|
||||||
add_newline: true,
|
add_newline: true,
|
||||||
|
@ -102,6 +104,7 @@ impl<'a> ModuleConfig<'a> for StarshipRootConfig<'a> {
|
||||||
if let toml::Value::Table(config) = config {
|
if let toml::Value::Table(config) = config {
|
||||||
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),
|
||||||
"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),
|
||||||
|
@ -115,6 +118,7 @@ impl<'a> ModuleConfig<'a> for StarshipRootConfig<'a> {
|
||||||
let did_you_mean = &[
|
let did_you_mean = &[
|
||||||
// Root options
|
// Root options
|
||||||
"format",
|
"format",
|
||||||
|
"right_format",
|
||||||
"scan_timeout",
|
"scan_timeout",
|
||||||
"command_timeout",
|
"command_timeout",
|
||||||
"add_newline",
|
"add_newline",
|
||||||
|
|
|
@ -46,6 +46,9 @@ pub struct Context<'a> {
|
||||||
/// The shell the user is assumed to be running
|
/// The shell the user is assumed to be running
|
||||||
pub shell: Shell,
|
pub shell: Shell,
|
||||||
|
|
||||||
|
/// Construct the right prompt instead of the left prompt
|
||||||
|
pub right: bool,
|
||||||
|
|
||||||
/// A HashMap of environment variable mocks
|
/// A HashMap of environment variable mocks
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub env: HashMap<&'a str, String>,
|
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 cmd_timeout = Duration::from_millis(config.get_root_config().command_timeout);
|
||||||
|
|
||||||
|
let right = arguments.is_present("right");
|
||||||
|
|
||||||
Context {
|
Context {
|
||||||
config,
|
config,
|
||||||
properties,
|
properties,
|
||||||
|
@ -129,6 +134,7 @@ impl<'a> Context<'a> {
|
||||||
dir_contents: OnceCell::new(),
|
dir_contents: OnceCell::new(),
|
||||||
repo: OnceCell::new(),
|
repo: OnceCell::new(),
|
||||||
shell,
|
shell,
|
||||||
|
right,
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
env: HashMap::new(),
|
env: HashMap::new(),
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -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
|
/// Maps variable name to its value
|
||||||
///
|
///
|
||||||
/// You should provide a function or closure that accepts the variable name `name: &str` as a
|
/// You should provide a function or closure that accepts the variable name `name: &str` as a
|
||||||
|
|
|
@ -4,6 +4,7 @@ set-env STARSHIP_SESSION_KEY (::STARSHIP:: session)
|
||||||
# Define Hooks
|
# Define Hooks
|
||||||
local:cmd-start-time = 0
|
local:cmd-start-time = 0
|
||||||
local:cmd-end-time = 0
|
local:cmd-end-time = 0
|
||||||
|
local:cmd-duration = 0
|
||||||
|
|
||||||
fn starship-after-readline-hook [line]{
|
fn starship-after-readline-hook [line]{
|
||||||
cmd-start-time = (::STARSHIP:: time)
|
cmd-start-time = (::STARSHIP:: time)
|
||||||
|
@ -11,6 +12,7 @@ fn starship-after-readline-hook [line]{
|
||||||
|
|
||||||
fn starship-before-readline-hook {
|
fn starship-before-readline-hook {
|
||||||
cmd-end-time = (::STARSHIP:: time)
|
cmd-end-time = (::STARSHIP:: time)
|
||||||
|
cmd-duration = (- $cmd-end-time $cmd-start-time)
|
||||||
}
|
}
|
||||||
|
|
||||||
# Install Hooks
|
# Install Hooks
|
||||||
|
@ -25,9 +27,17 @@ edit:prompt = {
|
||||||
if (== $cmd-start-time 0) {
|
if (== $cmd-start-time 0) {
|
||||||
::STARSHIP:: prompt --jobs=$num-bg-jobs
|
::STARSHIP:: prompt --jobs=$num-bg-jobs
|
||||||
} else {
|
} 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -8,7 +8,12 @@ function fish_prompt
|
||||||
set STARSHIP_CMD_STATUS $status
|
set STARSHIP_CMD_STATUS $status
|
||||||
# Account for changes in variable name between v2.7 and v3.0
|
# Account for changes in variable name between v2.7 and v3.0
|
||||||
set STARSHIP_DURATION "$CMD_DURATION$cmd_duration"
|
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
|
end
|
||||||
|
|
||||||
# Disable virtualenv prompt, it breaks starship
|
# Disable virtualenv prompt, it breaks starship
|
||||||
|
|
|
@ -92,3 +92,4 @@ VIRTUAL_ENV_DISABLE_PROMPT=1
|
||||||
|
|
||||||
setopt promptsubst
|
setopt promptsubst
|
||||||
PROMPT='$(::STARSHIP:: prompt --keymap="$KEYMAP" --status="$STARSHIP_CMD_STATUS" --pipestatus ${STARSHIP_PIPE_STATUS[@]} --cmd-duration="$STARSHIP_DURATION" --jobs="$STARSHIP_JOBS_COUNT")'
|
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")'
|
||||||
|
|
|
@ -99,6 +99,11 @@ fn main() {
|
||||||
.subcommand(
|
.subcommand(
|
||||||
SubCommand::with_name("prompt")
|
SubCommand::with_name("prompt")
|
||||||
.about("Prints the full starship 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(&status_code_arg)
|
||||||
.arg(&pipestatus_arg)
|
.arg(&pipestatus_arg)
|
||||||
.arg(&path_arg)
|
.arg(&path_arg)
|
||||||
|
|
97
src/print.rs
97
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
|
buf.push_str("\x1b[J"); // An ASCII control code to clear screen
|
||||||
}
|
}
|
||||||
|
|
||||||
let formatter = if let Ok(formatter) = StringFormatter::new(config.format) {
|
let (formatter, modules) = load_formatter_and_modules(&context);
|
||||||
formatter
|
|
||||||
} else {
|
|
||||||
log::error!("Error parsing `format`");
|
|
||||||
buf.push('>');
|
|
||||||
return buf;
|
|
||||||
};
|
|
||||||
let modules = formatter.get_variables();
|
|
||||||
let formatter = formatter.map_variables_to_segments(|module| {
|
let formatter = formatter.map_variables_to_segments(|module| {
|
||||||
// Make $all display all modules
|
// Make $all display all modules not explicitly referenced
|
||||||
if module == "all" {
|
if module == "all" {
|
||||||
Some(Ok(PROMPT_ORDER
|
Some(Ok(all_modules_uniq(&modules)
|
||||||
.par_iter()
|
.par_iter()
|
||||||
.flat_map(|module| {
|
.flat_map(|module| {
|
||||||
handle_module(module, &context, &modules)
|
handle_module(module, &context, &modules)
|
||||||
|
@ -124,6 +118,11 @@ pub fn get_prompt(context: Context) -> String {
|
||||||
}
|
}
|
||||||
write!(buf, "{}", ANSIStrings(&module_strings)).unwrap();
|
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
|
// escape \n and ! characters for tcsh
|
||||||
if let Shell::Tcsh = context.shell {
|
if let Shell::Tcsh = context.shell {
|
||||||
buf = buf.replace('!', "\\!");
|
buf = buf.replace('!', "\\!");
|
||||||
|
@ -288,20 +287,13 @@ pub fn explain(args: ArgMatches) {
|
||||||
fn compute_modules<'a>(context: &'a Context) -> Vec<Module<'a>> {
|
fn compute_modules<'a>(context: &'a Context) -> Vec<Module<'a>> {
|
||||||
let mut prompt_order: Vec<Module<'a>> = Vec::new();
|
let mut prompt_order: Vec<Module<'a>> = Vec::new();
|
||||||
|
|
||||||
let config = context.config.get_root_config();
|
let (_formatter, modules) = load_formatter_and_modules(context);
|
||||||
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();
|
|
||||||
|
|
||||||
for module in &modules {
|
for module in &modules {
|
||||||
// Manually add all modules if `$all` is encountered
|
// Manually add all modules if `$all` is encountered
|
||||||
if module == "all" {
|
if module == "all" {
|
||||||
for module in PROMPT_ORDER {
|
for module in all_modules_uniq(&modules) {
|
||||||
let modules = handle_module(module, context, &modules);
|
let modules = handle_module(&module, context, &modules);
|
||||||
prompt_order.extend(modules);
|
prompt_order.extend(modules);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -403,3 +395,68 @@ pub fn format_duration(duration: &Duration) -> String {
|
||||||
format!("{:?}ms", &milis)
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -31,6 +31,17 @@ static LOGGER: Lazy<()> = Lazy::new(|| {
|
||||||
log::set_boxed_logger(Box::new(logger)).unwrap();
|
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
|
/// Render a specific starship module by name
|
||||||
pub struct ModuleRenderer<'a> {
|
pub struct ModuleRenderer<'a> {
|
||||||
name: &'a str,
|
name: &'a str,
|
||||||
|
@ -43,13 +54,7 @@ impl<'a> ModuleRenderer<'a> {
|
||||||
// Start logger
|
// Start logger
|
||||||
Lazy::force(&LOGGER);
|
Lazy::force(&LOGGER);
|
||||||
|
|
||||||
let mut context = Context::new_with_shell_and_path(
|
let context = default_context();
|
||||||
clap::ArgMatches::default(),
|
|
||||||
Shell::Unknown,
|
|
||||||
PathBuf::new(),
|
|
||||||
PathBuf::new(),
|
|
||||||
);
|
|
||||||
context.config = StarshipConfig { config: None };
|
|
||||||
|
|
||||||
Self { name, context }
|
Self { name, context }
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue