feat(profiles): add right prompt, set via env & add docs

This commit is contained in:
David Knaack 2024-09-26 16:38:36 +02:00
parent 36134d896b
commit 141474d688
9 changed files with 296 additions and 62 deletions

View File

@ -1969,10 +1969,15 @@
}
},
"profiles": {
"default": {},
"default": {
"transient": {
"format": "$character",
"right_format": ""
}
},
"type": "object",
"additionalProperties": {
"type": "string"
"$ref": "#/definitions/Either_for_String_and_Profile"
}
}
},
@ -6550,6 +6555,31 @@
"type": "string"
}
]
},
"Either_for_String_and_Profile": {
"anyOf": [
{
"type": "string"
},
{
"$ref": "#/definitions/Profile"
}
]
},
"Profile": {
"description": "Profiles are either a string containing the format, or a struct containing the format and right_format",
"type": "object",
"properties": {
"format": {
"default": "",
"type": "string"
},
"right_format": {
"default": "",
"type": "string"
}
},
"additionalProperties": false
}
}
}
}

View File

@ -43,7 +43,7 @@ gix-faster = ["gix-features/zlib-stock", "gix/fast-sha1"]
[dependencies]
chrono = { version = "0.4.38", default-features = false, features = ["clock", "std", "wasmbind"] }
clap = { version = "4.5.18", features = ["derive", "cargo", "unicode"] }
clap = { version = "4.5.18", features = ["derive", "env", "cargo", "unicode"] }
clap_complete = "4.5.29"
dirs = "5.0.1"
dunce = "1.0.5"

View File

@ -20,11 +20,11 @@ this statement in your `$PROFILE`. Transience can be disabled on-the-fly with
By default, the left side of input gets replaced with `>`. To customize this,
define a new function called `Invoke-Starship-TransientFunction`. For example, to
display Starship's `character` module here, you would do
display Starship's `transient` profile with the `character` module:
```powershell
function Invoke-Starship-TransientFunction {
&starship module character
&starship prompt --profile=transient
}
Invoke-Expression (&starship init powershell)
@ -48,11 +48,11 @@ to customize what gets displayed on the left and on the right:
- By default, the left side of input gets replaced with `>`. To customize this,
define a new function called `starship_transient_prompt_func`. This function
receives the current prompt as a string that you can utilize. For example, to
display Starship's `character` module here, you would do
display Starship's default `transient` profile with the `character` module, you would do
```lua
function starship_transient_prompt_func(prompt)
return io.popen("starship module character"
return io.popen("starship prompt --profile=transient"
.." --keymap="..rl.getvariable('keymap')
):read("*a")
end
@ -62,11 +62,22 @@ load(io.popen('starship init cmd'):read("*a"))()
- By default, the right side of input is empty. To customize this, define a new
function called `starship_transient_rprompt_func`. This function receives the
current prompt as a string that you can utilize. For example, to display
the time at which the last command was started here, you would do
the time at which the last command was started here, you could define the following:
```toml
# ~/.config/starship.toml
[profiles.transient]
format = "$character"
right_format = "$time"
[time]
disabled = false
```
```lua
function starship_transient_rprompt_func(prompt)
return io.popen("starship module time"):read("*a")
return io.popen("starship prompt --profile=transient --right"):read("*a")
end
load(io.popen('starship init cmd'):read("*a"))()
```
@ -84,11 +95,11 @@ and syntactically correct.
- By default, the left side of input gets replaced with a bold-green ``. To customize this,
define a new function called `starship_transient_prompt_func`. For example, to
display Starship's `character` module here, you would do
display Starship's default `transient` profile with the `character` module, you would do
```fish
function starship_transient_prompt_func
starship module character
starship prompt --profile=transient
end
starship init fish | source
enable_transience
@ -98,9 +109,20 @@ enable_transience
function called `starship_transient_rprompt_func`. For example, to display
the time at which the last command was started here, you would do
```toml
# ~/.config/starship.toml
[profiles.transient]
format = "$character"
right_format = "$time"
[time]
disabled = false
```
```fish
function starship_transient_rprompt_func
starship module time
starship prompt --right --profile=transient
end
starship init fish | source
enable_transience

View File

@ -210,16 +210,17 @@ 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. |
| `palette` | `''` | Sets which color palette from `palettes` to use. |
| `palettes` | `{}` | Collection of color palettes that assign [colors](../advanced-config/#style-strings) to user-defined names. Note that color palettes cannot reference their own color definitions. |
| `follow_symlinks` | `true` | Follows symlinks to check if they're directories; used in modules such as git. |
| 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. |
| `palette` | `''` | Sets which color palette from `palettes` to use. |
| `palettes` | `{}` | Collection of color palettes that assign [colors](../advanced-config/#style-strings) to user-defined names. Note that color palettes cannot reference their own color definitions. |
| `follow_symlinks` | `true` | Follows symlinks to check if they're directories; used in modules such as git. |
| `profiles` | `{ transient: { format: "$character" } }` | Collection of prompt strings that can be used instead of the main `format` and `right_format`. For more information please consult the profile section below. |
::: tip
@ -365,6 +366,29 @@ modules you explicitly add to the format will not be duplicated. Eg.
format = '$all$directory$character'
```
### Profiles
Profiles can be used to set a key-value-map with alternate versions of the top-level `format` (the main prompt format string) and `right_format` (the same for the `right_prompt`). To quickly switch to a different profile of the current prompt, you can set the `STARSHIP_PROFILE` environment variable to the name of the profile, for instance by running `export STARSHIP_PROFILE=my_profile` or `$ENV:STARSHIP_PROFILE="my_profile"` in PowerShell. Alternatively, it can also be set via an `--profile=my_profile` flag for commands like `starship prompt` and `starship explain`.
| Option | Default | Description |
| -------------- | ------- | --------------------------------------------------------------------------------------- |
| `format` | `''` | Configure the format of the prompt profile. |
| `right_format` | `''` | [Enable the right prompt of the prompt profile.](/advanced-config/#enable-right-prompt) |
#### Examples
```toml
# Define custom profiles
[profiles]
# Instead of a map you can also set a profile to a simple string instead
# Starship includes the definition below for use with transient prompts
transient = "$character"
[profiles.as_map_with_right_format]
format = "$character"
right_format = "$git_branch"
```
## AWS
The `aws` module shows the current AWS region and profile and an expiration timer when using temporary credentials.

View File

@ -12,6 +12,7 @@ use std::borrow::Cow;
use std::clone::Clone;
use std::collections::HashMap;
use std::ffi::OsString;
use std::fmt::Debug;
use std::io::ErrorKind;
use toml::Value;
@ -76,6 +77,15 @@ pub enum Either<A, B> {
Second(B),
}
impl<A: Debug, B: Debug> Debug for Either<A, B> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Either::First(a) => write!(f, "First({:?})", a),
Either::Second(b) => write!(f, "Second({:?})", b),
}
}
}
/// A wrapper around `Vec<T>` that implements `ModuleConfig`, and either
/// accepts a value of type `T` or a list of values of type `T`.
#[derive(Clone, Default, Serialize)]

View File

@ -1,3 +1,4 @@
use crate::config::Either;
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
@ -22,7 +23,20 @@ pub struct StarshipRootConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub palette: Option<String>,
pub palettes: HashMap<String, Palette>,
pub profiles: IndexMap<String, String>,
pub profiles: IndexMap<String, Either<String, Profile>>,
}
/// Profiles are either a string containing the format, or a struct containing the format and right_format
#[derive(Clone, Serialize, Deserialize, Default, Debug)]
#[cfg_attr(
feature = "config-schema",
derive(schemars::JsonSchema),
schemars(deny_unknown_fields)
)]
#[serde(default)]
pub struct Profile {
pub format: String,
pub right_format: String,
}
pub type Palette = HashMap<String, String>;
@ -130,15 +144,22 @@ pub const PROMPT_ORDER: &[&str] = &[
"character",
];
// On changes please also update `Default` for the `FullConfig` struct in `mod.rs`
impl Default for StarshipRootConfig {
fn default() -> Self {
let mut profiles = IndexMap::new();
profiles.insert(
"transient".to_string(),
Either::Second(Profile {
format: "$character".to_string(),
right_format: String::new(),
}),
);
Self {
schema: "https://starship.rs/config-schema.json".to_string(),
format: "$all".to_string(),
right_format: String::new(),
continuation_prompt: "[∙](bright-black) ".to_string(),
profiles: Default::default(),
profiles,
scan_timeout: 30,
command_timeout: 500,
add_newline: true,

View File

@ -58,6 +58,10 @@ pub struct Context<'a> {
/// Which prompt to print (main, right, ...)
pub target: Target,
/// The profile to use for the prompt
/// If None, the default profile will be used
pub profile: Option<String>,
/// Width of terminal, or zero if width cannot be detected.
pub width: usize,
@ -148,6 +152,8 @@ impl<'a> Context<'a> {
properties.status_code = None;
}
let profile = properties.profile.take();
// Canonicalize the current path to resolve symlinks, etc.
// NOTE: On Windows this may convert the path to extended-path syntax.
let current_dir = Context::expand_tilde(path);
@ -160,7 +166,6 @@ impl<'a> Context<'a> {
.map_or_else(StarshipRootConfig::default, StarshipRootConfig::load);
let width = properties.terminal_width;
Context {
config,
properties,
@ -170,6 +175,7 @@ impl<'a> Context<'a> {
repo: OnceCell::new(),
shell,
target,
profile,
width,
env,
#[cfg(test)]
@ -830,12 +836,11 @@ pub enum Shell {
}
/// Which kind of prompt target to print (main prompt, rprompt, ...)
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Target {
Main,
Right,
Continuation,
Profile(String),
}
/// Properties as passed on from the shell as arguments
@ -866,6 +871,9 @@ pub struct Properties {
/// The number of currently running jobs
#[clap(short, long, default_value_t, value_parser=parse_jobs)]
pub jobs: i64,
/// Print the prompt with the specified profile name (instead of the standard left prompt)
#[clap(long, env = "STARSHIP_PROFILE")]
pub profile: Option<String>,
}
impl Default for Properties {
@ -879,6 +887,7 @@ impl Default for Properties {
cmd_duration: None,
keymap: "viins".to_string(),
jobs: 0,
profile: None,
}
}
}

View File

@ -89,11 +89,8 @@ enum Commands {
/// Print the right prompt (instead of the standard left prompt)
#[clap(long)]
right: bool,
/// Print the prompt with the specified profile name (instead of the standard left prompt)
#[clap(long, conflicts_with = "right")]
profile: Option<String>,
/// Print the continuation prompt (instead of the standard left prompt)
#[clap(long, conflicts_with = "right", conflicts_with = "profile")]
#[clap(long, conflicts_with = "right")]
continuation: bool,
#[clap(flatten)]
properties: Properties,
@ -179,14 +176,12 @@ fn main() {
Commands::Prompt {
properties,
right,
profile,
continuation,
} => {
let target = match (right, profile, continuation) {
(true, _, _) => Target::Right,
(_, Some(profile_name), _) => Target::Profile(profile_name),
(_, _, true) => Target::Continuation,
(_, _, _) => Target::Main,
let target = match (right, continuation) {
(true, _) => Target::Right,
(_, true) => Target::Continuation,
(_, _) => Target::Main,
};
print::prompt(properties, target);
}

View File

@ -10,6 +10,7 @@ use terminal_size::terminal_size;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthChar;
use crate::config::Either;
use crate::configs::PROMPT_ORDER;
use crate::context::{Context, Properties, Shell, Target};
use crate::formatter::{StringFormatter, VariableHolder};
@ -409,7 +410,6 @@ fn all_modules_uniq(module_list: &BTreeSet<String>) -> Vec<String> {
/// 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.root_config;
if context.target == Target::Continuation {
let cf = &config.continuation_prompt;
let formatter = StringFormatter::new(cf);
@ -425,33 +425,49 @@ fn load_formatter_and_modules<'a>(context: &'a Context) -> (StringFormatter<'a>,
};
}
let (left_format_str, right_format_str): (&str, &str) = match context.target {
Target::Main | Target::Right => (&config.format, &config.right_format),
Target::Profile(ref name) => {
if let Some(lf) = config.profiles.get(name) {
(lf, "")
} else {
log::error!("Profile {name:?} not found");
return (StringFormatter::raw(">"), BTreeSet::new());
}
}
Target::Continuation => unreachable!("Continuation prompt should have been handled above"),
let profile_name = context.profile.as_deref();
let profile = profile_name.and_then(|name| config.profiles.get(name));
// Reset `profile_name` if the profile is not found
let profile_name = if let (Some(profile_name), None) = (profile_name, profile) {
log::error!("Unknown profile {profile_name:?}, using default prompt");
None
} else {
profile_name
};
let (left_format_str, right_format_str) = profile
.as_ref()
.map(|p| match p {
Either::First(p) => (p.as_str(), ""),
Either::Second(p) => (p.format.as_str(), p.right_format.as_str()),
})
.unwrap_or((config.format.as_str(), config.right_format.as_str()));
let lf = StringFormatter::new(left_format_str);
let rf = StringFormatter::new(right_format_str);
if let Err(ref e) = lf {
let name = if let Target::Profile(ref profile_name) = context.target {
format!("profile.{profile_name}")
} else {
"format".to_string()
let name = match (profile, profile_name) {
// String-style profile
(Some(Either::First(_)), Some(name)) => name.to_string(),
// Table-style profile
(Some(Either::Second(_)), Some(name)) => {
format!("{name}.format")
}
// No profile
(_, _) => "format".to_string(),
};
log::error!("Error parsing {name:?}: {e}");
};
}
if let Err(ref e) = rf {
log::error!("Error parsing right_format: {e}");
let name = if let Some(profile_name) = profile_name {
format!("{profile_name}.right_format")
} else {
"right_format".to_string()
};
log::error!("Error parsing {name:?}: {e}");
}
let modules = [&lf, &rf]
@ -459,9 +475,8 @@ fn load_formatter_and_modules<'a>(context: &'a Context) -> (StringFormatter<'a>,
.flatten()
.flat_map(VariableHolder::get_variables)
.collect();
let main_formatter = match context.target {
Target::Main | Target::Profile(_) => lf,
Target::Main => lf,
Target::Right => rf,
Target::Continuation => unreachable!("Continuation prompt should have been handled above"),
};
@ -520,6 +535,7 @@ fn preset_list() -> String {
#[cfg(test)]
mod test {
use super::*;
use crate::test::default_context;
use crate::utils;
@ -599,7 +615,7 @@ mod test {
[character]
format=">>"
});
context.target = Target::Profile("test".to_string());
context.profile = Some("test".to_string());
let expected = String::from("0_0>>");
let actual = get_prompt(context);
@ -610,18 +626,125 @@ mod test {
fn custom_prompt_fallback() {
let mut context = default_context().set_config(toml::toml! {
add_newline=false
format = ">fallback>"
[profiles]
test="0_0$character"
[character]
format=">>"
});
context.target = Target::Profile("wrong_prompt".to_string());
context.profile = Some("wrong_prompt".to_string());
let expected = String::from(">");
let expected = String::from(">fallback>");
let actual = get_prompt(context);
assert_eq!(expected, actual);
}
#[test]
fn custom_prompt_verbose() {
let mut context = default_context().set_config(toml::toml! {
add_newline=false
[profiles.test]
format = "0_0$character"
[character]
format=">>"
});
context.profile = Some("test".to_string());
context.root_config.add_newline = false;
let expected = String::from("0_0>>");
let actual = get_prompt(context);
assert_eq!(expected, actual, "profile format should be used");
}
#[test]
fn custom_prompt_right() {
let mut context = default_context().set_config(toml::toml! {
add_newline=false
right_format = "rf"
[profiles.test]
right_format = "0_0$character"
[character]
format=">>"
});
context.profile = Some("test".to_string());
context.target = Target::Right;
context.root_config.add_newline = false;
let expected = String::from("0_0>>");
let actual = get_prompt(context);
assert_eq!(expected, actual, "profile right_format should be used");
}
#[test]
fn custom_prompt_right_not_set() {
let cfg = toml::toml! {
add_newline=false
right_format = "rf"
[profiles.test]
format = "0_0$character"
[character]
format=">>"
};
let mut context = default_context().set_config(cfg.clone());
context.profile = Some("test".to_string());
context.target = Target::Right;
let expected = String::from("");
let actual = get_prompt(context);
assert_eq!(
expected, actual,
"right prompt should be empty if not defined in verbose cfg"
);
let mut context = default_context().set_config(cfg);
context.profile = Some("test".to_string());
context.target = Target::Right;
context.root_config.profiles.insert(
"test".to_string(),
Either::First("0_0$character".to_string()),
);
let expected = String::from("");
let actual = get_prompt(context);
assert_eq!(
expected, actual,
"right prompt should be empty if not defined in short cfg"
);
}
#[test]
fn custom_prompt_verbose_not_defined() {
let cfg = toml::toml! {
format = ">fallback>"
right_format = ">right_fallback>"
add_newline=false
[profiles.test]
format = "0_0$character"
[character]
format=">>"
};
let mut context = default_context().set_config(cfg.clone());
context.profile = Some("wrong_prompt".to_string());
let expected = String::from(">fallback>");
let actual = get_prompt(context);
assert_eq!(
expected, actual,
"verbose prompt should use main format as fallback if not defined in verbose cfg"
);
let mut context = default_context().set_config(cfg);
context.profile = Some("wrong_prompt".to_string());
context.target = Target::Right;
let expected = String::from(">right_fallback>");
let actual = get_prompt(context);
assert_eq!(
expected, actual,
"right prompt should use main right_format as fallback if not defined in verbose cfg"
);
}
#[test]
fn continuation_prompt() {
let mut context = default_context().set_config(toml::toml! {