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"
|
||||
```
|
||||
|
||||
## 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:
|
||||
|
|
|
@ -152,12 +152,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. |
|
||||
| `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
|
||||
|
||||
```toml
|
||||
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")'
|
||||
|
|
|
@ -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)
|
||||
|
|
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
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue