Merge branch 'master' into conditional-style

This commit is contained in:
Filip Bachul 2023-09-18 09:51:01 +02:00
commit de14c8101e
27 changed files with 1770 additions and 553 deletions

View File

@ -41,9 +41,9 @@
"target/"
],
"plugins": [
"https://github.com/dprint/dprint-plugin-typescript/releases/download/0.86.2/plugin.wasm",
"https://github.com/dprint/dprint-plugin-typescript/releases/download/0.87.1/plugin.wasm",
"https://github.com/dprint/dprint-plugin-json/releases/download/0.17.4/plugin.wasm",
"https://github.com/dprint/dprint-plugin-markdown/releases/download/0.16.0/plugin.wasm",
"https://github.com/dprint/dprint-plugin-markdown/releases/download/0.16.1/plugin.wasm",
"https://github.com/dprint/dprint-plugin-toml/releases/download/0.5.4/plugin.wasm"
]
}

View File

@ -527,6 +527,20 @@
}
]
},
"fossil_metrics": {
"default": {
"added_style": "bold green",
"deleted_style": "bold red",
"disabled": true,
"format": "([+$added]($added_style) )([-$deleted]($deleted_style) )",
"only_nonzero_diffs": true
},
"allOf": [
{
"$ref": "#/definitions/FossilMetricsConfig"
}
]
},
"gcloud": {
"default": {
"detect_env_vars": [],
@ -791,6 +805,7 @@
},
"hostname": {
"default": {
"detect_env_vars": [],
"disabled": false,
"format": "[$ssh_symbol$hostname]($style) in ",
"ssh_only": true,
@ -899,6 +914,7 @@
"kubernetes": {
"default": {
"context_aliases": {},
"contexts": [],
"detect_extensions": [],
"detect_files": [],
"detect_folders": [],
@ -3150,6 +3166,32 @@
},
"additionalProperties": false
},
"FossilMetricsConfig": {
"type": "object",
"properties": {
"format": {
"default": "([+$added]($added_style) )([-$deleted]($deleted_style) )",
"type": "string"
},
"added_style": {
"default": "bold green",
"type": "string"
},
"deleted_style": {
"default": "bold red",
"type": "string"
},
"only_nonzero_diffs": {
"default": true,
"type": "boolean"
},
"disabled": {
"default": true,
"type": "boolean"
}
},
"additionalProperties": false
},
"GcloudConfig": {
"type": "object",
"properties": {
@ -3781,6 +3823,13 @@
"default": ".",
"type": "string"
},
"detect_env_vars": {
"default": [],
"type": "array",
"items": {
"type": "string"
}
},
"format": {
"default": "[$ssh_symbol$hostname]($style) in ",
"type": "string"
@ -4055,6 +4104,58 @@
"items": {
"type": "string"
}
},
"contexts": {
"default": [],
"type": "array",
"items": {
"$ref": "#/definitions/KubernetesContextConfig"
}
}
},
"additionalProperties": false
},
"KubernetesContextConfig": {
"type": "object",
"properties": {
"context_pattern": {
"default": "",
"type": "string"
},
"user_pattern": {
"default": null,
"type": [
"string",
"null"
]
},
"symbol": {
"default": null,
"type": [
"string",
"null"
]
},
"style": {
"default": null,
"type": [
"string",
"null"
]
},
"context_alias": {
"default": null,
"type": [
"string",
"null"
]
},
"user_alias": {
"default": null,
"type": [
"string",
"null"
]
}
},
"additionalProperties": false

View File

@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Setup | Checkout
uses: actions/checkout@v3
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
- name: Docs | Format
uses: dprint/check@v2.2
@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Setup | Checkout
uses: actions/checkout@v3
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
- name: Install | Taplo
run: cargo install --debug --locked --version 0.8.1 taplo-cli
- name: Presets | Validate with schema

View File

@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Setup | Checkout
uses: actions/checkout@v3
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
- name: Setup | Node
uses: actions/setup-node@v3

View File

@ -89,7 +89,7 @@ jobs:
RUSTFLAGS: ${{ matrix.rustflags || '' }}
steps:
- name: Setup | Checkout
uses: actions/checkout@v3
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
- name: Setup | Rust
uses: dtolnay/rust-toolchain@master
@ -178,7 +178,7 @@ jobs:
KEYCHAIN_ENTRY: AC_PASSWORD
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
with:
# Required to include the recently merged Crowdin PR
ref: master
@ -284,7 +284,7 @@ jobs:
if: ${{ needs.release_please.outputs.release_created == 'true' }}
steps:
- name: Setup | Checkout
uses: actions/checkout@v3
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
- name: Setup | Rust
uses: dtolnay/rust-toolchain@stable
@ -298,7 +298,7 @@ jobs:
needs: [release_please, upload_artifacts]
if: ${{ needs.release_please.outputs.release_created == 'true' }}
steps:
- uses: mislav/bump-homebrew-formula-action@v2.2
- uses: mislav/bump-homebrew-formula-action@v2.3
with:
formula-name: starship
tag-name: ${{ needs.release_please.outputs.tag_name }}
@ -329,7 +329,7 @@ jobs:
if: ${{ needs.release_please.outputs.release_created == 'true' }}
steps:
- name: Setup | Checkout
uses: actions/checkout@v3
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
- name: Setup | Artifacts
uses: actions/download-artifact@v3
- run: pwsh ./install/windows/choco/update.ps1
@ -345,7 +345,7 @@ jobs:
continue-on-error: true
steps:
- name: Setup | Checkout
uses: actions/checkout@v3
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
- name: Merge | Merge Crowdin PR
run: gh pr merge i18n_master --squash --repo=starship/starship
env:
@ -357,7 +357,7 @@ jobs:
needs: merge_crowdin_pr
steps:
- name: Setup | Checkout
uses: actions/checkout@v3
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
- name: Trigger workflow dispatch
run: gh workflow run publish-docs.yml
env:

View File

@ -22,7 +22,7 @@ jobs:
steps:
- name: Setup | Checkout
uses: actions/checkout@v3
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
- name: Test | Security Audit
uses: EmbarkStudios/cargo-deny-action@v1.5.4
with:

View File

@ -6,5 +6,5 @@ jobs:
name: Spell Check with Typos
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: crate-ci/typos@v1.16.8
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
- uses: crate-ci/typos@v1.16.11

View File

@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Setup | Checkout
uses: actions/checkout@v3
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
- name: Setup | Rust
uses: dtolnay/rust-toolchain@stable
@ -43,7 +43,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Setup | Checkout
uses: actions/checkout@v3
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
- name: Setup | Rust
uses: dtolnay/rust-toolchain@stable
@ -62,7 +62,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Setup | Checkout
uses: actions/checkout@v3
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
- name: Setup | Rust
uses: dtolnay/rust-toolchain@stable
@ -80,7 +80,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Setup | Checkout
uses: actions/checkout@v3
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
- name: Setup | Rust
uses: dtolnay/rust-toolchain@stable
@ -98,7 +98,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Setup | Checkout
uses: actions/checkout@v3
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
- name: Setup | Rust
uses: dtolnay/rust-toolchain@stable
@ -120,7 +120,7 @@ jobs:
pull-requests: write
steps:
- name: Setup | Checkout
uses: actions/checkout@v3
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
- name: Setup | Rust
uses: dtolnay/rust-toolchain@stable
@ -132,7 +132,7 @@ jobs:
run: cargo run --locked --features config-schema -- config-schema > .github/config-schema.json
- name: Check | Detect Changes
uses: reviewdog/action-suggester@v1.7.4
uses: reviewdog/action-suggester@v1.8.0
with:
tool_name: starship config-schema
filter_mode: nofilter
@ -156,7 +156,7 @@ jobs:
RUSTFLAGS: ${{ matrix.rustflags || '' }}
steps:
- name: Setup | Checkout
uses: actions/checkout@v3
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
# Install all the required dependencies for testing
- name: Setup | Rust

503
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -42,16 +42,16 @@ gix-max-perf = ["gix-features/zlib-ng", "gix/fast-sha1"]
gix-faster = ["gix-features/zlib-stock", "gix/fast-sha1"]
[dependencies]
chrono = { version = "0.4.26", default-features = false, features = ["clock", "std", "wasmbind"] }
clap = { version = "4.4.1", features = ["derive", "cargo", "unicode"] }
clap_complete = "4.4.0"
chrono = { version = "0.4.30", default-features = false, features = ["clock", "std", "wasmbind"] }
clap = { version = "4.4.3", features = ["derive", "cargo", "unicode"] }
clap_complete = "4.4.1"
dirs-next = "2.0.0"
dunce = "1.0.4"
gethostname = "0.4.3"
# default feature restriction addresses https://github.com/starship/starship/issues/4251
gix = { version = "0.52.0", default-features = false, features = ["max-performance-safe"] }
gix-features = { version = "0.33.0", optional = true }
indexmap = { version = "1.9.3", features = ["serde"] }
gix = { version = "0.53.1", default-features = false, features = ["max-performance-safe", "revision"] }
gix-features = { version = "0.34.0", optional = true }
indexmap = { version = "2.0.0", features = ["serde"] }
log = { version = "0.4.20", features = ["std"] }
# notify-rust is optional (on by default) because the crate doesn't currently build for darwin with nix
# see: https://github.com/NixOS/nixpkgs/issues/160876
@ -62,16 +62,16 @@ open = "5.0.0"
# update os module config and tests when upgrading os_info
os_info = "3.7.0"
path-slash = "0.2.1"
pest = "2.7.2"
pest_derive = "2.7.2"
pest = "2.7.3"
pest_derive = "2.7.3"
quick-xml = "0.30.0"
rand = "0.8.5"
rayon = "1.7.0"
regex = { version = "1.9.4", default-features = false, features = ["perf", "std", "unicode-perl"] }
regex = { version = "1.9.5", default-features = false, features = ["perf", "std", "unicode-perl"] }
rust-ini = "0.19.0"
semver = "1.0.18"
serde = { version = "1.0.188", features = ["derive"] }
serde_json = "1.0.105"
serde_json = "1.0.107"
sha1 = "0.10.5"
shadow-rs = { version = "0.23.0", default-features = false }
# battery is optional (on by default) because the crate doesn't currently build for Termux
@ -80,13 +80,13 @@ starship-battery = { version = "0.8.2", optional = true }
strsim = "0.10.0"
systemstat = "=0.2.3"
terminal_size = "0.2.6"
toml = { version = "0.7.6", features = ["preserve_order"] }
toml_edit = "0.19.14"
toml = { version = "0.8.0", features = ["preserve_order"] }
toml_edit = "0.20.0"
unicode-segmentation = "1.10.1"
unicode-width = "0.1.10"
urlencoding = "2.1.3"
versions = "5.0.1"
which = "4.4.0"
which = "4.4.2"
yaml-rust = "0.4.5"
process_control = { version = "4.0.3", features = ["crossbeam-channel"] }
@ -98,7 +98,7 @@ shell-words = "1.1.0"
[dependencies.schemars]
version = "0.8.13"
optional = true
features = ["preserve_order", "indexmap"]
features = ["preserve_order", "indexmap2"]
[target.'cfg(windows)'.dependencies]
deelevate = "0.2.0"

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

View File

@ -0,0 +1,284 @@
format = """
$cmd_duration\
$hostname\
$localip\
$shlvl\
$shell\
$env_var\
$username\
$sudo\
$character\
"""
right_format = """
$singularity\
$kubernetes\
$directory\
$vcsh\
$fossil_branch\
$git_branch\
$git_commit\
$git_state\
$git_metrics\
$git_status\
$hg_branch\
$pijul_channel\
$docker_context\
$package\
$c\
$cmake\
$cobol\
$daml\
$dart\
$deno\
$dotnet\
$elixir\
$elm\
$erlang\
$fennel\
$golang\
$guix_shell\
$haskell\
$haxe\
$helm\
$java\
$julia\
$kotlin\
$gradle\
$lua\
$nim\
$nodejs\
$ocaml\
$opa\
$perl\
$php\
$pulumi\
$purescript\
$python\
$raku\
$rlang\
$red\
$ruby\
$rust\
$scala\
$solidity\
$swift\
$terraform\
$vlang\
$vagrant\
$zig\
$buf\
$nix_shell\
$conda\
$meson\
$spack\
$memory_usage\
$aws\
$gcloud\
$openstack\
$azure\
$crystal\
$custom\
$jobs\
$status\
$os\
$container\
$battery\
$time\
"""
add_newline = true
[character]
format = "$symbol "
success_symbol = "[◉](bold italic bright-yellow)"
error_symbol = "[⊘](italic purple)"
[env_var.VIMSHELL] # vim subshell
format = "[$env_value]($style)"
style = 'green italic'
[sudo]
format = "[$symbol]($style)"
style = "italic bright-purple"
symbol = "◇┈"
disabled = false
[username]
style_user = "yellow bold"
style_root = "purple bold italic"
format = "[$user]($style) ▻ "
disabled = false
[directory]
home_symbol = "⌂"
truncation_length = 2
truncation_symbol = "▦ "
read_only = " ■"
style = "italic blue"
format = ' [$path]($style)[$read_only]($read_only_style)'
[cmd_duration]
min_time = 500
format = "[$duration ](italic bright-yellow)"
[jobs]
format = "[ $symbol$number]($style)"
style = "white"
symbol = "[▶ ](blue italic)"
[localip]
ssh_only = true
format = " ◯[$localipv4](bold magenta)"
disabled = false
[time]
disabled = false
format = "[ $time]($style)"
time_format = "%R"
utc_time_offset = "local"
style = "dimmed white"
[battery]
format = "[ $percentage $symbol]($style)"
full_symbol = "[█](italic green)"
charging_symbol = "[↑](italic green)"
discharging_symbol = "[↓](italic)"
unknown_symbol = "[░](italic)"
empty_symbol = "[▃](italic red)"
[[battery.display]]
threshold = 40
style = "dimmed yellow"
[[battery.display]]
threshold = 70
style = "dimmed white"
[git_branch]
format = "[ $symbol $branch(:$remote_branch)]($style)"
symbol = "[◬](bold bright-blue)"
style = "bold italic bright-blue"
[git_status]
style = "italic bright-blue"
format = """([⎪$ahead_behind$staged$modified$untracked$renamed$deleted$conflicted$stashed⎥]($style))"""
conflicted = "[◪◦](italic bright-magenta)"
ahead = "[▲│[${count}](bold white)│](italic green)"
behind = "[▽│[${count}](bold white)│](italic red)"
diverged = "[◇ ▲┤[${ahead_count}](regular white)│▽┤[${behind_count}](regular white)│](italic bright-magenta)"
untracked = "[◌◦](italic bright-yellow)"
stashed = "[◦◫◦](italic white)"
modified = "[●◦](italic yellow)"
staged = "[■┤[$count](bold white)│](italic bright-cyan)"
renamed = "[◎◦](italic bright-blue)"
deleted = "[✕](italic red)"
[deno]
format = " deno [∫ $version](blue italic)"
version_format = "${major}.${minor}"
[lua]
format = " lua [${symbol}${version}]($style)"
symbol = "⨀ "
style = "italic bright-yellow"
[nodejs]
format = " node [◫ ($version)](italic bright-green)"
detect_files = ["package-lock.json", "yarn.lock"]
version_format = "${major}.${minor}"
[python]
format = " py [${symbol}${version}]($style)"
symbol = "[⌉](italic bright-blue)⌊ "
version_format = "${major}.${minor}"
style = "italic bright-yellow"
[ruby]
format = " rb [${symbol}${version}]($style)"
symbol = "◆ "
version_format = "${major}.${minor}"
style = "italic red"
[rust]
format = " rs [$symbol$version]($style)"
symbol = "⊃ "
version_format = "${major}.${minor}"
style = "italic red"
[package]
format = " pkg [$symbol$version]($style)"
version_format = "${major}.${minor}"
symbol = "◫ "
style = "bright-yellow italic"
[swift]
format = " sw [${symbol}${version}]($style)"
symbol = "◁ "
style = "italic bright-red"
version_format = "${major}.${minor}"
[aws]
format = " aws [$symbol $profile $region]($style)"
style = "italic blue"
symbol = "▲ "
[buf]
symbol = "■ "
format = " buf [$symbol $version $buf_version]($style)"
[c]
symbol = "∁ "
format = " c [$symbol($version(-$name))]($style)"
[conda]
symbol = "◯ "
format = " conda [$symbol$environment]($style)"
[dart]
symbol = "◁◅ "
format = " dart [$symbol($version )]($style)"
[docker_context]
symbol = "◧ "
format = " docker [$symbol$context]($style)"
[elixir]
symbol = "△ "
format = " exs [$symbol $version OTP $otp_version ]($style)"
[elm]
symbol = "◩ "
format = " elm [$symbol($version )]($style)"
[golang]
symbol = "∩ "
format = " go [$symbol($version )]($style)"
[haskell]
symbol = "❯λ "
format = " hs [$symbol($version )]($style)"
[java]
symbol = " "
format = " java [${symbol}(${version} )]($style)"
[julia]
symbol = "◎ "
format = " jl [$symbol($version )]($style)"
[memory_usage]
symbol = "▪▫▪ "
format = " mem [${ram}( ${swap})]($style)"
[nim]
symbol = "▴▲▴ "
format = " nim [$symbol($version )]($style)"
[nix_shell]
symbol = "⊛ "
format = " nix [$symbol$state $name]($style)"
[spack]
symbol = "◇ "
format = " spack [$symbol$environment]($style)"

View File

@ -266,6 +266,7 @@ $kubernetes\
$directory\
$vcsh\
$fossil_branch\
$fossil_metrics\
$git_branch\
$git_commit\
$git_state\
@ -1604,6 +1605,41 @@ truncation_length = 4
truncation_symbol = ''
```
## Fossil Metrics
The `fossil_metrics` module will show the number of added and deleted lines in the check-out in your current directory. At least v2.14 (2021-01-20) of Fossil is required.
### Options
| Option | Default | Description |
| -------------------- | ------------------------------------------------------------ | ------------------------------------- |
| `format` | `'([+$added]($added_style) )([-$deleted]($deleted_style) )'` | The format for the module. |
| `added_style` | `'bold green'` | The style for the added count. |
| `deleted_style` | `'bold red'` | The style for the deleted count. |
| `only_nonzero_diffs` | `true` | Render status only for changed items. |
| `disabled` | `true` | Disables the `fossil_metrics` module. |
### Variables
| Variable | Example | Description |
| --------------- | ------- | ------------------------------------------- |
| added | `1` | The current number of added lines |
| deleted | `2` | The current number of deleted lines |
| added_style\* | | Mirrors the value of option `added_style` |
| deleted_style\* | | Mirrors the value of option `deleted_style` |
*: This variable can only be used as a part of a style string
### Example
```toml
# ~/.config/starship.toml
[fossil_metrics]
added_style = 'bold blue'
format = '[+$added]($added_style)/[-$deleted]($deleted_style) '
```
## Google Cloud (`gcloud`)
The `gcloud` module shows the current configuration for [`gcloud`](https://cloud.google.com/sdk/gcloud) CLI.
@ -2202,14 +2238,15 @@ The `hostname` module shows the system hostname.
### Options
| Option | Default | Description |
| ------------ | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `ssh_only` | `true` | Only show hostname when connected to an SSH session. |
| `ssh_symbol` | `'🌐 '` | A format string representing the symbol when connected to SSH session. |
| `trim_at` | `'.'` | String that the hostname is cut off at, after the first match. `'.'` will stop after the first dot. `''` will disable any truncation |
| `format` | `'[$ssh_symbol$hostname]($style) in '` | The format for the module. |
| `style` | `'bold dimmed green'` | The style for the module. |
| `disabled` | `false` | Disables the `hostname` module. |
| Option | Default | Description |
| ----------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `ssh_only` | `true` | Only show hostname when connected to an SSH session. |
| `ssh_symbol` | `'🌐 '` | A format string representing the symbol when connected to SSH session. |
| `trim_at` | `'.'` | String that the hostname is cut off at, after the first match. `'.'` will stop after the first dot. `''` will disable any truncation. |
| `detect_env_vars` | `[]` | Which environment variable(s) should trigger this module. |
| `format` | `'[$ssh_symbol$hostname]($style) in '` | The format for the module. |
| `style` | `'bold dimmed green'` | The style for the module. |
| `disabled` | `false` | Disables the `hostname` module. |
### Variables
@ -2221,7 +2258,9 @@ The `hostname` module shows the system hostname.
*: This variable can only be used as a part of a style string
### Example
### Examples
#### Always show the hostname
```toml
# ~/.config/starship.toml
@ -2233,6 +2272,17 @@ trim_at = '.companyname.com'
disabled = false
```
#### Hide the hostname in remote tmux sessions
```toml
# ~/.config/starship.toml
[hostname]
ssh_only = false
detect_env_vars = ['!TMUX', 'SSH_CONNECTION']
disabled = false
```
## Java
The `java` module shows the currently installed version of [Java](https://www.oracle.com/java/).
@ -2434,7 +2484,8 @@ kotlin_binary = 'kotlinc'
Displays the current [Kubernetes context](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#context) name and, if set, the namespace, user and cluster from the kubeconfig file.
The namespace needs to be set in the kubeconfig file, this can be done via
`kubectl config set-context starship-context --namespace astronaut`.
Similarly the user and cluster can be set with `kubectl config set-context starship-context --user starship-user` and `kubectl config set-context starship-context --cluster starship-cluster`.
Similarly, the user and cluster can be set with `kubectl config set-context starship-context --user starship-user`
and `kubectl config set-context starship-context --cluster starship-cluster`.
If the `$KUBECONFIG` env var is set the module will use that if not it will use the `~/.kube/config`.
::: tip
@ -2450,18 +2501,45 @@ case the module will only be active in directories that match those conditions.
### Options
::: warning
The `context_aliases` and `user_aliases` options are deprecated. Use `contexts` and the corresponding `context_alias`
and `user_alias` options instead.
:::
| Option | Default | Description |
| ------------------- | -------------------------------------------------- | --------------------------------------------------------------------- |
| `symbol` | `'☸ '` | A format string representing the symbol displayed before the Cluster. |
| `format` | `'[$symbol$context( \($namespace\))]($style) in '` | The format for the module. |
| `style` | `'cyan bold'` | The style for the module. |
| `context_aliases` | `{}` | Table of context aliases to display. |
| `user_aliases` | `{}` | Table of user aliases to display. |
| `context_aliases`* | `{}` | Table of context aliases to display. |
| `user_aliases`* | `{}` | Table of user aliases to display. |
| `detect_extensions` | `[]` | Which extensions should trigger this module. |
| `detect_files` | `[]` | Which filenames should trigger this module. |
| `detect_folders` | `[]` | Which folders should trigger this modules. |
| `contexts` | `[]` | Customized styles and symbols for specific contexts. |
| `disabled` | `true` | Disables the `kubernetes` module. |
*: This option is deprecated, please add `contexts` with the corresponding `context_alias` and `user_alias` options instead.
To customize the style of the module for specific environments, use the following configuration as
part of the `contexts` list:
| Variable | Description |
| ----------------- | ---------------------------------------------------------------------------------------- |
| `context_pattern` | **Required** Regular expression to match current Kubernetes context name. |
| `user_pattern` | Regular expression to match current Kubernetes user name. |
| `context_alias` | Context alias to display instead of the full context name. |
| `user_alias` | User alias to display instead of the full user name. |
| `style` | The style for the module when using this context. If not set, will use module's style. |
| `symbol` | The symbol for the module when using this context. If not set, will use module's symbol. |
Note that all regular expression are anchored with `^<pattern>$` and so must match the whole string. The `*_pattern`
regular expressions may contain capture groups, which can be referenced in the corresponding alias via `$name` and `$N`
(see example below and the
[rust Regex::replace() documentation](https://docs.rs/regex/latest/regex/struct.Regex.html#method.replace)).
### Variables
| Variable | Example | Description |
@ -2483,13 +2561,9 @@ case the module will only be active in directories that match those conditions.
[kubernetes]
format = 'on [⛵ ($user on )($cluster in )$context \($namespace\)](dimmed green) '
disabled = false
[kubernetes.context_aliases]
'dev.local.cluster.k8s' = 'dev'
'.*/openshift-cluster/.*' = 'openshift'
'gke_.*_(?P<var_cluster>[\w-]+)' = 'gke-$var_cluster'
[kubernetes.user_aliases]
'dev.local.cluster.k8s' = 'dev'
'root/.*' = 'root'
contexts = [
{ context_pattern = "dev.local.cluster.k8s", style = "green", symbol = "💔 " },
]
```
Only show the module in directories that contain a `k8s` file.
@ -2502,29 +2576,37 @@ disabled = false
detect_files = ['k8s']
```
#### Regex Matching
#### Kubernetes Context specific config
Additional to simple aliasing, `context_aliases` and `user_aliases` also supports
extended matching and renaming using regular expressions.
The regular expression must match on the entire kube context,
capture groups can be referenced using `$name` and `$N` in the replacement.
This is more explained in the [regex crate](https://docs.rs/regex/1.5.4/regex/struct.Regex.html#method.replace) documentation.
Long and automatically generated cluster names can be identified
and shortened using regular expressions:
The `contexts` configuration option is used to customise what the current Kubernetes context name looks
like (style and symbol) if the name matches the defined regular expression.
```toml
[kubernetes.context_aliases]
# OpenShift contexts carry the namespace and user in the kube context: `namespace/name/user`:
'.*/openshift-cluster/.*' = 'openshift'
# Or better, to rename every OpenShift cluster at once:
'.*/(?P<var_cluster>[\w-]+)/.*' = '$var_cluster'
# ~/.config/starship.toml
[[kubernetes.contexts]]
# "bold red" style + default symbol when Kubernetes current context name equals "production" *and* the current user
# equals "admin_user"
context_pattern = "production"
user_pattern = "admin_user"
style = "bold red"
context_alias = "prod"
user_alias = "admin"
[[kubernetes.contexts]]
# "green" style + a different symbol when Kubernetes current context name contains openshift
context_pattern = ".*openshift.*"
style = "green"
symbol = "💔 "
context_alias = "openshift"
[[kubernetes.contexts]]
# Using capture groups
# Contexts from GKE, AWS and other cloud providers usually carry additional information, like the region/zone.
# The following entry matches on the GKE format (`gke_projectname_zone_cluster-name`)
# and renames every matching kube context into a more readable format (`gke-cluster-name`):
'gke_.*_(?P<var_cluster>[\w-]+)' = 'gke-$var_cluster'
context_pattern = "gke_.*_(?P<cluster>[\\w-]+)"
context_alias = "gke-$cluster"
```
## Line Break

24
docs/presets/jetpack.md Normal file
View File

@ -0,0 +1,24 @@
[Return to Presets](./README.md#jetpack)
# Jetpack Preset
This is a pseudo minimalist preset inspired by the [geometry](https://github.com/geometry-zsh/geometry) and [spaceship](https://github.com/spaceship-prompt/spaceship-prompt) prompts.
> Jetpack uses the terminal's color theme.
![Screenshot of Jetpack preset](/presets/img/jetpack.png)
### Prerequisite
- Requires a shell with [`right-prompt`](https://starship.rs/advanced-config/#enable-right-prompt) support.
- [Jetbrains Mono](https://www.jetbrains.com/lp/mono/) is recommended.
### Configuration
```sh
starship preset jetpack -o ~/.config/starship.toml
```
[Click to download TOML](/presets/toml/jetpack.toml)
<<< @/.vuepress/public/presets/toml/jetpack.toml

View File

@ -0,0 +1,28 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Deserialize, Serialize)]
#[cfg_attr(
feature = "config-schema",
derive(schemars::JsonSchema),
schemars(deny_unknown_fields)
)]
#[serde(default)]
pub struct FossilMetricsConfig<'a> {
pub format: &'a str,
pub added_style: &'a str,
pub deleted_style: &'a str,
pub only_nonzero_diffs: bool,
pub disabled: bool,
}
impl<'a> Default for FossilMetricsConfig<'a> {
fn default() -> Self {
FossilMetricsConfig {
format: "([+$added]($added_style) )([-$deleted]($deleted_style) )",
added_style: "bold green",
deleted_style: "bold red",
only_nonzero_diffs: true,
disabled: true,
}
}
}

View File

@ -11,6 +11,7 @@ pub struct HostnameConfig<'a> {
pub ssh_only: bool,
pub ssh_symbol: &'a str,
pub trim_at: &'a str,
pub detect_env_vars: Vec<&'a str>,
pub format: &'a str,
pub style: &'a str,
pub disabled: bool,
@ -22,6 +23,7 @@ impl<'a> Default for HostnameConfig<'a> {
ssh_only: true,
ssh_symbol: "🌐 ",
trim_at: ".",
detect_env_vars: vec![],
format: "[$ssh_symbol$hostname]($style) in ",
style: "green dimmed bold",
disabled: false,

View File

@ -18,6 +18,7 @@ pub struct KubernetesConfig<'a> {
pub detect_extensions: Vec<&'a str>,
pub detect_files: Vec<&'a str>,
pub detect_folders: Vec<&'a str>,
pub contexts: Vec<KubernetesContextConfig<'a>>,
}
impl<'a> Default for KubernetesConfig<'a> {
@ -32,6 +33,23 @@ impl<'a> Default for KubernetesConfig<'a> {
detect_extensions: vec![],
detect_files: vec![],
detect_folders: vec![],
contexts: vec![],
}
}
}
#[derive(Clone, Deserialize, Serialize, Default)]
#[cfg_attr(
feature = "config-schema",
derive(schemars::JsonSchema),
schemars(deny_unknown_fields)
)]
#[serde(default)]
pub struct KubernetesContextConfig<'a> {
pub context_pattern: &'a str,
pub user_pattern: Option<&'a str>,
pub symbol: Option<&'a str>,
pub style: Option<&'a str>,
pub context_alias: Option<&'a str>,
pub user_alias: Option<&'a str>,
}

View File

@ -28,6 +28,7 @@ pub mod erlang;
pub mod fennel;
pub mod fill;
pub mod fossil_branch;
pub mod fossil_metrics;
pub mod gcloud;
pub mod git_branch;
pub mod git_commit;
@ -159,6 +160,8 @@ pub struct FullConfig<'a> {
#[serde(borrow)]
fossil_branch: fossil_branch::FossilBranchConfig<'a>,
#[serde(borrow)]
fossil_metrics: fossil_metrics::FossilMetricsConfig<'a>,
#[serde(borrow)]
gcloud: gcloud::GcloudConfig<'a>,
#[serde(borrow)]
git_branch: git_branch::GitBranchConfig<'a>,

View File

@ -39,6 +39,7 @@ pub const PROMPT_ORDER: &[&str] = &[
"directory",
"vcsh",
"fossil_branch",
"fossil_metrics",
"git_branch",
"git_commit",
"git_state",

View File

@ -235,8 +235,32 @@ impl<'a> Context<'a> {
disabled == Some(true)
}
/// Returns true when a negated environment variable is defined in `env_vars` and is present
fn has_negated_env_var(&self, env_vars: &'a [&'a str]) -> bool {
env_vars
.iter()
.filter_map(|env_var| env_var.strip_prefix('!'))
.any(|env_var| self.get_env(env_var).is_some())
}
/// Returns true if 'detect_env_vars' is empty,
/// or if at least one environment variable is set and no negated environment variable is set
pub fn detect_env_vars(&'a self, env_vars: &'a [&'a str]) -> bool {
env_vars.is_empty() || (env_vars.iter().any(|e| self.get_env(e).is_some()))
if env_vars.is_empty() {
return true;
}
if self.has_negated_env_var(env_vars) {
return false;
}
// Returns true if at least one environment variable is set
let mut iter = env_vars
.iter()
.filter(|env_var| !env_var.starts_with('!'))
.peekable();
iter.peek().is_none() || iter.any(|env_var| self.get_env(env_var).is_some())
}
// returns a new ScanDir struct with reference to current dir_files of context

View File

@ -153,10 +153,7 @@ pub fn init_stub(shell_name: &str) -> io::Result<()> {
"#,
starship.sprint_posix()?
),
"zsh" => print!(
r#"source <({} init zsh --print-full-init)"#,
starship.sprint_posix()?
),
"zsh" => print_script(ZSH_INIT, &starship.sprint_posix()?),
"fish" => print!(
// Fish does process substitution with pipes and psub instead of bash syntax
r#"source ({} init fish --print-full-init | psub)"#,

View File

@ -35,6 +35,7 @@ pub const ALL_MODULES: &[&str] = &[
"fennel",
"fill",
"fossil_branch",
"fossil_metrics",
"gcloud",
"git_branch",
"git_commit",

View File

@ -0,0 +1,297 @@
use regex::Regex;
use super::{Context, Module, ModuleConfig};
use crate::configs::fossil_metrics::FossilMetricsConfig;
use crate::formatter::StringFormatter;
/// Creates a module with currently added/deleted lines in the Fossil check-out in the current
/// directory.
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
let mut module = context.new_module("fossil_metrics");
let config = FossilMetricsConfig::try_load(module.config);
// As we default to disabled=true, we have to check here after loading our config module,
// before it was only checking against whatever is in the config starship.toml
if config.disabled {
return None;
};
let checkout_db = if cfg!(windows) {
"_FOSSIL_"
} else {
".fslckout"
};
// See if we're in a check-out by scanning upwards for a directory containing the checkout_db file
context
.begin_ancestor_scan()
.set_files(&[checkout_db])
.scan()?;
// Read the total number of added and deleted lines from "fossil diff --numstat"
let output = context.exec_cmd("fossil", &["diff", "--numstat"])?.stdout;
let stats = FossilDiff::parse(&output, config.only_nonzero_diffs);
let parsed = StringFormatter::new(config.format).and_then(|formatter| {
formatter
.map_style(|variable| match variable {
"added_style" => Some(Ok(config.added_style)),
"deleted_style" => Some(Ok(config.deleted_style)),
_ => None,
})
.map(|variable| match variable {
"added" => Some(Ok(stats.added)),
"deleted" => Some(Ok(stats.deleted)),
_ => None,
})
.parse(None, Some(context))
});
module.set_segments(match parsed {
Ok(segments) => segments,
Err(error) => {
log::warn!("Error in module `fossil_metrics`:\n{}", error);
return None;
}
});
Some(module)
}
/// Represents the parsed output from a Fossil diff with the --numstat option enabled.
#[derive(Debug, PartialEq)]
struct FossilDiff<'a> {
added: &'a str,
deleted: &'a str,
}
impl<'a> FossilDiff<'a> {
/// Parses the output of `fossil diff --numstat` as a `FossilDiff` struct.
pub fn parse(diff_numstat: &'a str, only_nonzero_diffs: bool) -> Self {
// Fossil formats the last line of the output as "%10d %10d TOTAL over %d changed files\n"
// where the 1st and 2nd placeholders are the number of added and deleted lines respectively
let re = Regex::new(r"^\s*(\d+)\s+(\d+) TOTAL over \d+ changed files$").unwrap();
let (added, deleted) = diff_numstat
.lines()
.last()
.and_then(|s| re.captures(s))
.and_then(|caps| {
let added = match caps.get(1)?.as_str() {
"0" if only_nonzero_diffs => "",
s => s,
};
let deleted = match caps.get(2)?.as_str() {
"0" if only_nonzero_diffs => "",
s => s,
};
Some((added, deleted))
})
.unwrap_or_default();
Self { added, deleted }
}
}
#[cfg(test)]
mod tests {
use std::io;
use std::path::Path;
use nu_ansi_term::{Color, Style};
use crate::test::{fixture_repo, FixtureProvider, ModuleRenderer};
use super::FossilDiff;
enum Expect<'a> {
Empty,
Added(Option<&'a str>),
AddedStyle(Style),
Deleted(Option<&'a str>),
DeletedStyle(Style),
}
#[test]
fn show_nothing_on_empty_dir() -> io::Result<()> {
let checkout_dir = tempfile::tempdir()?;
let actual = ModuleRenderer::new("fossil_metrics")
.path(checkout_dir.path())
.collect();
let expected = None;
assert_eq!(expected, actual);
checkout_dir.close()
}
#[test]
fn test_fossil_metrics_disabled_per_default() -> io::Result<()> {
let tempdir = fixture_repo(FixtureProvider::Fossil)?;
let checkout_dir = tempdir.path();
expect_fossil_metrics_with_config(
checkout_dir,
Some(toml::toml! {
// no "disabled=false" in config!
[fossil_metrics]
only_nonzero_diffs = false
}),
&[Expect::Empty],
);
tempdir.close()
}
#[test]
fn test_fossil_metrics_autodisabled() -> io::Result<()> {
let tempdir = tempfile::tempdir()?;
expect_fossil_metrics_with_config(tempdir.path(), None, &[Expect::Empty]);
tempdir.close()
}
#[test]
fn test_fossil_metrics() -> io::Result<()> {
let tempdir = fixture_repo(FixtureProvider::Fossil)?;
let checkout_dir = tempdir.path();
expect_fossil_metrics_with_config(
checkout_dir,
None,
&[Expect::Added(Some("3")), Expect::Deleted(Some("2"))],
);
tempdir.close()
}
#[test]
fn test_fossil_metrics_subdir() -> io::Result<()> {
let tempdir = fixture_repo(FixtureProvider::Fossil)?;
let checkout_dir = tempdir.path();
expect_fossil_metrics_with_config(
&checkout_dir.join("subdir"),
None,
&[Expect::Added(Some("3")), Expect::Deleted(Some("2"))],
);
tempdir.close()
}
#[test]
fn test_fossil_metrics_configured() -> io::Result<()> {
let tempdir = fixture_repo(FixtureProvider::Fossil)?;
let checkout_dir = tempdir.path();
expect_fossil_metrics_with_config(
checkout_dir,
Some(toml::toml! {
[fossil_metrics]
added_style = "underline blue"
deleted_style = "underline purple"
disabled = false
}),
&[
Expect::Added(Some("3")),
Expect::AddedStyle(Color::Blue.underline()),
Expect::Deleted(Some("2")),
Expect::DeletedStyle(Color::Purple.underline()),
],
);
tempdir.close()
}
#[test]
fn parse_no_changes_discard_zeros() {
let actual = FossilDiff::parse(" 0 0 TOTAL over 0 changed files\n", true);
let expected = FossilDiff {
added: "",
deleted: "",
};
assert_eq!(expected, actual);
}
#[test]
fn parse_no_changes_keep_zeros() {
let actual = FossilDiff::parse(" 0 0 TOTAL over 0 changed files\n", false);
let expected = FossilDiff {
added: "0",
deleted: "0",
};
assert_eq!(expected, actual);
}
#[test]
fn parse_with_changes() {
let actual = FossilDiff::parse(
" 3 2 README.md\n 3 2 TOTAL over 1 changed files\n",
true,
);
let expected = FossilDiff {
added: "3",
deleted: "2",
};
assert_eq!(expected, actual);
}
#[test]
fn parse_ignore_empty() {
let actual = FossilDiff::parse("", true);
let expected = FossilDiff {
added: "",
deleted: "",
};
assert_eq!(expected, actual);
}
/// Tests output as produced by Fossil v2.3 to v2.14, i.e. without the summary line.
#[test]
fn parse_ignore_when_missing_total_line() {
let actual = FossilDiff::parse(" 3 2 README.md\n", true);
let expected = FossilDiff {
added: "",
deleted: "",
};
assert_eq!(expected, actual);
}
fn expect_fossil_metrics_with_config(
checkout_dir: &Path,
config: Option<toml::Table>,
expectations: &[Expect],
) {
let actual = ModuleRenderer::new("fossil_metrics")
.path(checkout_dir.to_str().unwrap())
.config(config.unwrap_or_else(|| {
toml::toml! {
[fossil_metrics]
disabled = false
}
}))
.collect();
let mut expect_added = Some("3");
let mut expect_added_style = Color::Green.bold();
let mut expect_deleted = Some("2");
let mut expect_deleted_style = Color::Red.bold();
for expect in expectations {
match expect {
Expect::Empty => {
assert_eq!(None, actual);
return;
}
Expect::Added(added) => expect_added = *added,
Expect::AddedStyle(style) => expect_added_style = *style,
Expect::Deleted(deleted) => expect_deleted = *deleted,
Expect::DeletedStyle(style) => expect_deleted_style = *style,
}
}
let expected = Some(format!(
"{}{}",
expect_added
.map(|added| format!("{} ", expect_added_style.paint(format!("+{added}"))))
.unwrap_or(String::from("")),
expect_deleted
.map(|deleted| format!("{} ", expect_deleted_style.paint(format!("-{deleted}"))))
.unwrap_or(String::from("")),
));
assert_eq!(expected, actual);
}
}

View File

@ -8,14 +8,18 @@ use crate::formatter::StringFormatter;
/// Creates a module with the system hostname
///
/// Will display the hostname if all of the following criteria are met:
/// - hostname.disabled is absent or false
/// - `hostname.disabled` is absent or false
/// - `hostname.ssh_only` is false OR the user is currently connected as an SSH session (`$SSH_CONNECTION`)
/// - `hostname.ssh_only` is false AND `hostname.detect_env_vars` is either empty or contains a defined environment variable
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
let mut module = context.new_module("hostname");
let config: HostnameConfig = HostnameConfig::try_load(module.config);
let ssh_connection = context.get_env("SSH_CONNECTION");
if config.ssh_only && ssh_connection.is_none() {
if (config.ssh_only && ssh_connection.is_none())
|| !context.detect_env_vars(&config.detect_env_vars)
{
return None;
}
@ -96,17 +100,82 @@ mod tests {
}
#[test]
fn ssh_only_false_no_ssh() {
fn ssh_only_false_with_empty_detect_env_vars() {
let hostname = get_hostname!();
let actual = ModuleRenderer::new("hostname")
.config(toml::toml! {
[hostname]
ssh_only = false
trim_at = ""
detect_env_vars = []
})
.collect();
let expected = Some(format!("{} in ", style().paint(hostname)));
println!("{}", expected.as_ref().unwrap());
assert_eq!(expected, actual);
}
#[test]
fn ssh_only_false_with_matching_negated_env_var() {
let actual = ModuleRenderer::new("hostname")
.config(toml::toml! {
[hostname]
ssh_only = false
trim_at = ""
detect_env_vars = ["!NEGATED"]
})
.env("NEGATED", "true")
.collect();
let expected = None;
assert_eq!(expected, actual);
}
#[test]
fn ssh_only_false_with_only_negated_env_vars() {
let hostname = get_hostname!();
let actual = ModuleRenderer::new("hostname")
.config(toml::toml! {
[hostname]
ssh_only = false
trim_at = ""
detect_env_vars = ["!NEGATED_ONE", "!NEGATED_TWO", "!NEGATED_THREE"]
})
.collect();
let expected = Some(format!("{} in ", style().paint(hostname)));
assert_eq!(expected, actual);
}
#[test]
fn ssh_only_false_with_matching_env_var() {
let hostname = get_hostname!();
let actual = ModuleRenderer::new("hostname")
.config(toml::toml! {
[hostname]
ssh_only = false
trim_at = ""
detect_env_vars = ["FORCE_HOSTNAME"]
})
.env("FORCE_HOSTNAME", "true")
.collect();
let expected = Some(format!("{} in ", style().paint(hostname)));
assert_eq!(expected, actual);
}
#[test]
fn ssh_only_false_without_matching_env_vars() {
let actual = ModuleRenderer::new("hostname")
.config(toml::toml! {
[hostname]
ssh_only = false
trim_at = ""
detect_env_vars = ["FORCE_HOSTNAME", "!NEGATED"]
})
.collect();
let expected = None;
assert_eq!(expected, actual);
}
@ -121,6 +190,7 @@ mod tests {
})
.collect();
let expected = Some(format!("{} in ", style().paint(hostname)));
assert_eq!(expected, actual);
}

View File

@ -1,7 +1,6 @@
use yaml_rust::YamlLoader;
use std::borrow::Cow;
use std::collections::HashMap;
use std::env;
use std::path;
@ -11,99 +10,91 @@ use crate::configs::kubernetes::KubernetesConfig;
use crate::formatter::StringFormatter;
use crate::utils;
#[derive(Default)]
struct KubeCtxComponents {
user: Option<String>,
namespace: Option<String>,
cluster: Option<String>,
}
fn get_kube_context(filename: path::PathBuf) -> Option<String> {
fn get_current_kube_context_name(filename: path::PathBuf) -> Option<String> {
let contents = utils::read_file(filename).ok()?;
let yaml_docs = YamlLoader::load_from_str(&contents).ok()?;
if yaml_docs.is_empty() {
return None;
}
let conf = &yaml_docs[0];
let current_ctx = conf["current-context"].as_str()?;
if current_ctx.is_empty() {
return None;
}
Some(current_ctx.to_string())
let conf = yaml_docs.get(0)?;
conf["current-context"]
.as_str()
.filter(|s| !s.is_empty())
.map(String::from)
}
fn get_kube_ctx_component(filename: path::PathBuf, current_ctx: &str) -> Option<KubeCtxComponents> {
fn get_kube_ctx_components(
filename: path::PathBuf,
current_ctx_name: &str,
) -> Option<KubeCtxComponents> {
let contents = utils::read_file(filename).ok()?;
let yaml_docs = YamlLoader::load_from_str(&contents).ok()?;
if yaml_docs.is_empty() {
return None;
}
let conf = &yaml_docs[0];
let conf = yaml_docs.get(0)?;
let contexts = conf["contexts"].as_vec()?;
let ctx_yaml = conf["contexts"].as_vec().and_then(|contexts| {
contexts
.iter()
.filter_map(|ctx| Some((ctx, ctx["name"].as_str()?)))
.find(|(_, name)| *name == current_ctx)
});
// Find the context with the name we're looking for
// or return None if we can't find it
let (ctx_yaml, _) = contexts
.iter()
.filter_map(|ctx| Some((ctx, ctx["name"].as_str()?)))
.find(|(_, name)| name == &current_ctx_name)?;
let ctx_components = KubeCtxComponents {
user: ctx_yaml
.and_then(|(ctx, _)| ctx["context"]["user"].as_str())
.and_then(|s| {
if s.is_empty() {
return None;
}
Some(s.to_owned())
}),
namespace: ctx_yaml
.and_then(|(ctx, _)| ctx["context"]["namespace"].as_str())
.and_then(|s| {
if s.is_empty() {
return None;
}
Some(s.to_owned())
}),
cluster: ctx_yaml
.and_then(|(ctx, _)| ctx["context"]["cluster"].as_str())
.and_then(|s| {
if s.is_empty() {
return None;
}
Some(s.to_owned())
}),
user: ctx_yaml["context"]["user"]
.as_str()
.filter(|s| !s.is_empty())
.map(String::from),
namespace: ctx_yaml["context"]["namespace"]
.as_str()
.filter(|s| !s.is_empty())
.map(String::from),
cluster: ctx_yaml["context"]["cluster"]
.as_str()
.filter(|s| !s.is_empty())
.map(String::from),
};
Some(ctx_components)
}
fn get_kube_user<'a>(config: &'a KubernetesConfig, kube_user: &'a str) -> Cow<'a, str> {
return get_alias(&config.user_aliases, kube_user).unwrap_or(Cow::Borrowed(kube_user));
}
fn get_kube_context_name<'a>(config: &'a KubernetesConfig, kube_ctx: &'a str) -> Cow<'a, str> {
return get_alias(&config.context_aliases, kube_ctx).unwrap_or(Cow::Borrowed(kube_ctx));
}
fn get_alias<'a>(
aliases: &'a HashMap<String, &'a str>,
alias_candidate: &'a str,
) -> Option<Cow<'a, str>> {
if let Some(val) = aliases.get(alias_candidate) {
return Some(Cow::Borrowed(val));
fn get_aliased_name<'a>(
pattern: Option<&'a str>,
current_value: Option<&str>,
alias: Option<&'a str>,
) -> Option<String> {
let replacement = alias.or(current_value)?.to_string();
let Some(pattern) = pattern else {
// If user pattern not set, treat it as a match-all pattern
return Some(replacement);
};
// If a pattern is set, but we have no value, there is no match
let value = current_value?;
if value == pattern {
return Some(replacement);
}
return aliases.iter().find_map(|(k, v)| {
let re = regex::Regex::new(&format!("^{k}$")).ok()?;
let replaced = re.replace(alias_candidate, *v);
match replaced {
Cow::Owned(replaced) => Some(Cow::Owned(replaced)),
_ => None,
let re = match regex::Regex::new(&format!("^{pattern}$")) {
Ok(re) => re,
Err(error) => {
log::warn!(
"Could not compile regular expression `{}`:\n{}",
&format!("^{pattern}$"),
error
);
return None;
}
});
};
let replaced = re.replace(value, replacement.as_str());
match replaced {
Cow::Owned(replaced) => Some(replaced),
// It didn't match...
_ => None,
}
}
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
@ -118,18 +109,28 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
// If we have some config for doing the directory scan then we use it but if we don't then we
// assume we should treat it like the module is enabled to preserve backward compatibility.
let have_scan_config = !(config.detect_files.is_empty()
&& config.detect_folders.is_empty()
&& config.detect_extensions.is_empty());
let have_scan_config = [
&config.detect_files,
&config.detect_folders,
&config.detect_extensions,
]
.into_iter()
.any(|v| !v.is_empty());
let is_kube_project = context
.try_begin_scan()?
.set_files(&config.detect_files)
.set_folders(&config.detect_folders)
.set_extensions(&config.detect_extensions)
.is_match();
let is_kube_project = have_scan_config.then(|| {
context
.try_begin_scan()
.map(|scanner| {
scanner
.set_files(&config.detect_files)
.set_folders(&config.detect_folders)
.set_extensions(&config.detect_extensions)
.is_match()
})
.unwrap_or(false)
});
if have_scan_config && !is_kube_project {
if !is_kube_project.unwrap_or(true) {
return None;
}
@ -139,39 +140,89 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
.get_env("KUBECONFIG")
.unwrap_or(default_config_file.to_str()?.to_string());
let kube_ctx = env::split_paths(&kube_cfg).find_map(get_kube_context)?;
let current_kube_ctx_name =
env::split_paths(&kube_cfg).find_map(get_current_kube_context_name)?;
let ctx_components: Vec<KubeCtxComponents> = env::split_paths(&kube_cfg)
.filter_map(|filename| get_kube_ctx_component(filename, &kube_ctx))
.collect();
// Even if we have multiple config files, the first key wins
// https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/
// > Never change the value or map key. ... Example: If two files specify a red-user,
// > use only values from the first file's red-user. Even if the second file has
// > non-conflicting entries under red-user, discard them.
// for that reason, we can pick the first context with that name
let ctx_components: KubeCtxComponents = env::split_paths(&kube_cfg)
.find_map(|filename| get_kube_ctx_components(filename, &current_kube_ctx_name))
.unwrap_or_else(|| {
// TODO: figure out if returning is more sensible. But currently we have tests depending on this
log::warn!(
"Invalid KUBECONFIG: identified current-context `{}`, but couldn't find the context in any config file(s): `{}`.\n",
&current_kube_ctx_name,
&kube_cfg
);
KubeCtxComponents::default()
});
// Select the first style that matches the context_pattern and,
// if it is defined, the user_pattern
let (matched_context_config, display_context, display_user) = config
.contexts
.iter()
.find_map(|context_config| {
let context_alias = get_aliased_name(
Some(context_config.context_pattern),
Some(&current_kube_ctx_name),
context_config.context_alias,
)?;
let user_alias = get_aliased_name(
context_config.user_pattern,
ctx_components.user.as_deref(),
context_config.user_alias,
);
if matches!((context_config.user_pattern, &user_alias), (Some(_), None)) {
// defined pattern, but it didn't match
return None;
}
Some((Some(context_config), context_alias, user_alias))
})
.unwrap_or_else(|| (None, current_kube_ctx_name.clone(), ctx_components.user));
// TODO: remove deprecated aliases after starship 2.0
let display_context =
deprecated::get_alias(display_context, &config.context_aliases, "context").unwrap();
let display_user =
display_user.and_then(|user| deprecated::get_alias(user, &config.user_aliases, "user"));
let display_style = matched_context_config
.and_then(|ctx_cfg| ctx_cfg.style)
.unwrap_or(config.style);
let display_symbol = matched_context_config
.and_then(|ctx_cfg| ctx_cfg.symbol)
.unwrap_or(config.symbol);
let parsed = StringFormatter::new(config.format).and_then(|formatter| {
formatter
.map_meta(|variable, _| match variable {
"symbol" => Some(config.symbol),
"symbol" => Some(display_symbol),
_ => None,
})
.map_style(|variable| match variable {
"style" => Some(Ok(config.style)),
"style" => Some(Ok(display_style)),
_ => None,
})
.map(|variable| match variable {
"context" => Some(Ok(get_kube_context_name(&config, &kube_ctx))),
"context" => Some(Ok(Cow::Borrowed(display_context.as_str()))),
"namespace" => ctx_components
.iter()
.find_map(|kube| kube.namespace.as_deref())
.map(|namespace| Ok(Cow::Borrowed(namespace))),
"user" => ctx_components
.iter()
.find_map(|kube| kube.user.as_deref())
.map(|user| Ok(get_kube_user(&config, user))),
.namespace
.as_ref()
.map(|kube_ns| Ok(Cow::Borrowed(kube_ns.as_str()))),
"cluster" => ctx_components
.iter()
.find_map(|kube| kube.cluster.as_deref())
.map(|cluster| Ok(Cow::Borrowed(cluster))),
.cluster
.as_ref()
.map(|kube_cluster| Ok(Cow::Borrowed(kube_cluster.as_str()))),
"user" => display_user
.as_ref()
.map(|kube_user| Ok(Cow::Borrowed(kube_user.as_str()))),
_ => None,
})
.parse(None, Some(context))
@ -188,6 +239,47 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
Some(module)
}
mod deprecated {
use std::borrow::Cow;
use std::collections::HashMap;
pub fn get_alias<'a>(
current_value: String,
aliases: &'a HashMap<String, &'a str>,
name: &'a str,
) -> Option<String> {
let alias = if let Some(val) = aliases.get(current_value.as_str()) {
// simple match without regex
Some((*val).to_string())
} else {
// regex match
aliases.iter().find_map(|(k, v)| {
let re = regex::Regex::new(&format!("^{k}$")).ok()?;
let replaced = re.replace(current_value.as_str(), *v);
match replaced {
// We have a match if the replaced string is different from the original
Cow::Owned(replaced) => Some(replaced),
_ => None,
}
})
};
match alias {
Some(alias) => {
log::warn!(
"Usage of '{}_aliases' is deprecated and will be removed in 2.0; Use 'contexts' with '{}_alias' instead. (`{}` -> `{}`)",
&name,
&name,
&current_value,
&alias
);
Some(alias)
}
None => Some(current_value),
}
}
}
#[cfg(test)]
mod tests {
use crate::test::ModuleRenderer;
@ -429,6 +521,21 @@ users: []
)
}
#[test]
fn test_config_context_ctx_alias_regex_replace() -> io::Result<()> {
base_test_ctx_alias(
"gke_infra-cluster-28cccff6_europe-west4_cluster-1",
toml::toml! {
[kubernetes]
disabled = false
[[kubernetes.contexts]]
context_pattern = "gke_.*_(?P<cluster>[\\w-]+)"
context_alias = "example: $cluster"
},
"☸ example: cluster-1",
)
}
#[test]
fn test_ctx_alias_broken_regex() -> io::Result<()> {
base_test_ctx_alias(
@ -577,7 +684,9 @@ users: []
}
#[test]
fn test_multiple_config_files_with_ns() -> io::Result<()> {
fn test_multiple_config_files_with_context_defined_once() -> io::Result<()> {
// test that we get the current context from the first config file in the KUBECONFIG,
// no matter if it is only defined in the latter
let dir = tempfile::tempdir()?;
let filename_cc = dir.path().join("config_cc");
@ -630,7 +739,7 @@ users: []
})
.collect();
// And tes with context and namespace first
// And test with context and namespace first
let actual_ctx_first = ModuleRenderer::new("kubernetes")
.path(dir.path())
.env(
@ -655,6 +764,87 @@ users: []
dir.close()
}
#[test]
fn test_multiple_config_files_with_context_defined_twice() -> io::Result<()> {
// tests that, if two files contain the same context,
// only the context config from the first is used.
let dir = tempfile::tempdir()?;
let config1 = dir.path().join("config1");
let mut file1 = File::create(&config1)?;
file1.write_all(
b"
apiVersion: v1
clusters: []
contexts:
- context:
cluster: test_cluster1
namespace: test_namespace1
name: test_context
current-context: test_context
kind: Config
preferences: {}
users: []
",
)?;
file1.sync_all()?;
let config2 = dir.path().join("config2");
let mut file2 = File::create(&config2)?;
file2.write_all(
b"
apiVersion: v1
clusters: []
contexts:
- context:
cluster: test_cluster2
user: test_user2
name: test_context
current-context: test_context
kind: Config
preferences: {}
users: []
",
)?;
file2.sync_all()?;
let paths1 = [config1.clone(), config2.clone()];
let kubeconfig_content1 = env::join_paths(paths1.iter()).unwrap();
let actual1 = ModuleRenderer::new("kubernetes")
.path(dir.path())
.env("KUBECONFIG", kubeconfig_content1.to_string_lossy())
.config(toml::toml! {
[kubernetes]
format = "($user )($cluster )($namespace )"
disabled = false
})
.collect();
let expected1 = Some("test_cluster1 test_namespace1 ".to_string());
assert_eq!(expected1, actual1);
let paths2 = [config2, config1];
let kubeconfig_content2 = env::join_paths(paths2.iter()).unwrap();
let actual2 = ModuleRenderer::new("kubernetes")
.path(dir.path())
.env("KUBECONFIG", kubeconfig_content2.to_string_lossy())
.config(toml::toml! {
[kubernetes]
format = "($user )($cluster )($namespace )"
disabled = false
})
.collect();
let expected2 = Some("test_user2 test_cluster2 ".to_string());
assert_eq!(expected2, actual2);
dir.close()
}
fn base_test_user_alias(
user_name: &str,
config: toml::Table,
@ -744,6 +934,23 @@ users: []
)
}
#[test]
fn test_config_context_user_alias_regex_replace() -> io::Result<()> {
base_test_user_alias(
"gke_infra-user-28cccff6_europe-west4_cluster-1",
toml::toml! {
[kubernetes]
disabled = false
format = "[$symbol$context( \\($user\\))]($style) in "
[[kubernetes.contexts]]
context_pattern = ".*"
user_pattern = "gke_.*_(?P<cluster>[\\w-]+)"
user_alias = "example: $cluster"
},
"☸ test_context (example: cluster-1)",
)
}
#[test]
fn test_user_alias_broken_regex() -> io::Result<()> {
base_test_user_alias(
@ -932,4 +1139,310 @@ users: []
assert_eq!(expected, actual);
dir.close()
}
#[test]
fn test_config_context_overwrites_defaults() -> std::io::Result<()> {
let dir = tempfile::tempdir()?;
let filename = dir.path().join("config");
let mut file = File::create(&filename)?;
file.write_all(
b"
apiVersion: v1
clusters: []
contexts:
- context:
user: test_user
namespace: test_namespace
name: test_context
current-context: test_context
kind: Config
preferences: {}
users: []
",
)?;
file.sync_all()?;
let actual = ModuleRenderer::new("kubernetes")
.path(dir.path())
.env("KUBECONFIG", filename.to_string_lossy().as_ref())
.config(toml::toml! {
[kubernetes]
disabled = false
style = "bold red"
[[kubernetes.contexts]]
context_pattern = "test.*"
style = "bold green"
symbol = "§ "
})
.collect();
let expected = Some(format!(
"{} in ",
Color::Green.bold().paint("§ test_context (test_namespace)")
));
assert_eq!(expected, actual);
dir.close()
}
#[test]
fn test_config_context_both_pattern_must_match() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let filename = dir.path().join("config");
let mut file = File::create(&filename)?;
file.write_all(
b"
apiVersion: v1
clusters: []
contexts:
- context:
user: test_user
name: test_context
current-context: test_context
kind: Config
preferences: {}
users: []
",
)?;
file.sync_all()?;
let actual = ModuleRenderer::new("kubernetes")
.path(dir.path())
.env("KUBECONFIG", filename.to_string_lossy().as_ref())
.config(toml::toml! {
[kubernetes]
disabled = false
format = "$symbol$context ($user )"
[[kubernetes.contexts]]
context_pattern = "test.*"
user_pattern = "test.*"
context_alias = "yy"
user_alias = "xx"
symbol = "§ "
})
.collect();
let expected = Some("§ yy xx ".to_string());
assert_eq!(expected, actual);
dir.close()
}
#[test]
fn test_config_context_only_one_pattern_matches() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let filename = dir.path().join("config");
let mut file = File::create(&filename)?;
file.write_all(
b"
apiVersion: v1
clusters: []
contexts:
- context:
user: test_user
name: test_context
current-context: test_context
kind: Config
preferences: {}
users: []
",
)?;
file.sync_all()?;
let actual = ModuleRenderer::new("kubernetes")
.path(dir.path())
.env("KUBECONFIG", filename.to_string_lossy().as_ref())
.config(toml::toml! {
[kubernetes]
disabled = false
format = "$symbol$context ($user )"
[[kubernetes.contexts]]
context_pattern = "test.*"
user_pattern = "test_BAD.*"
context_alias = "yy"
user_alias = "xx"
symbol = "§ "
})
.collect();
let expected = Some("☸ test_context test_user ".to_string());
assert_eq!(expected, actual);
dir.close()
}
#[test]
fn test_config_context_uses_aliases() -> std::io::Result<()> {
let dir = tempfile::tempdir()?;
let filename = dir.path().join("config");
let mut file = File::create(&filename)?;
file.write_all(
b"
apiVersion: v1
clusters: []
contexts:
- context:
user: test_user
namespace: test_namespace
name: test_context
current-context: test_context
kind: Config
preferences: {}
users: []
",
)?;
file.sync_all()?;
let actual = ModuleRenderer::new("kubernetes")
.path(dir.path())
.env("KUBECONFIG", filename.to_string_lossy().as_ref())
.config(toml::toml! {
[kubernetes]
disabled = false
style = "bold red"
format = "$symbol($user )($context )($cluster )($namespace)"
[[kubernetes.contexts]]
context_pattern = "test.*"
context_alias = "xyz"
user_alias = "abc"
symbol = "§ "
})
.collect();
let expected = Some("§ abc xyz test_namespace".to_string());
assert_eq!(expected, actual);
dir.close()
}
#[test]
fn test_config_context_user_pattern_does_not_match() -> std::io::Result<()> {
let dir = tempfile::tempdir()?;
let filename = dir.path().join("config");
let mut file = File::create(&filename)?;
file.write_all(
b"
apiVersion: v1
clusters: []
contexts:
- context:
user: test_user
namespace: test_namespace
name: test_context
current-context: test_context
kind: Config
preferences: {}
users: []
",
)?;
file.sync_all()?;
let actual = ModuleRenderer::new("kubernetes")
.path(dir.path())
.env("KUBECONFIG", filename.to_string_lossy().as_ref())
.config(toml::toml! {
[kubernetes]
disabled = false
style = "bold red"
format = "$symbol($user )($context )($cluster )($namespace)"
[[kubernetes.contexts]]
context_pattern = "test"
user_pattern = "not_matching"
context_alias = "xyz"
user_alias = "abc"
symbol = "§ "
})
.collect();
let expected = Some("☸ test_user test_context test_namespace".to_string());
assert_eq!(expected, actual);
dir.close()
}
#[test]
fn test_config_contexts_does_not_match() -> std::io::Result<()> {
let dir = tempfile::tempdir()?;
let filename = dir.path().join("config");
let mut file = File::create(&filename)?;
file.write_all(
b"
apiVersion: v1
clusters: []
contexts:
- context:
user: test_user
namespace: test_namespace
name: test_context
current-context: test_context
kind: Config
preferences: {}
users: []
",
)?;
file.sync_all()?;
let actual = ModuleRenderer::new("kubernetes")
.path(dir.path())
.env("KUBECONFIG", filename.to_string_lossy().as_ref())
.config(toml::toml! {
[kubernetes]
disabled = false
style = "bold red"
contexts = [
{context_pattern = "tests_.*", style = "bold green", symbol = "§ "},
]
})
.collect();
let expected = Some(format!(
"{} in ",
Color::Red.bold().paint("☸ test_context (test_namespace)")
));
assert_eq!(expected, actual);
dir.close()
}
#[test]
fn test_config_context_bad_regex_should_not_panic() -> std::io::Result<()> {
let dir = tempfile::tempdir()?;
let filename = dir.path().join("config");
let mut file = File::create(&filename)?;
file.write_all(
b"
apiVersion: v1
clusters: []
contexts:
- context:
user: test_user
namespace: test_namespace
name: test_context
current-context: test_context
kind: Config
preferences: {}
users: []
",
)?;
file.sync_all()?;
let actual = ModuleRenderer::new("kubernetes")
.path(dir.path())
.env("KUBECONFIG", filename.to_string_lossy().as_ref())
.config(toml::toml! {
[kubernetes]
disabled = false
style = "bold red"
contexts = [
{context_pattern = "tests_(.*", style = "bold green", symbol = "§ "},
]
})
.collect();
let expected = Some(format!(
"{} in ",
Color::Red.bold().paint("☸ test_context (test_namespace)")
));
assert_eq!(expected, actual);
dir.close()
}
}

View File

@ -25,6 +25,7 @@ mod erlang;
mod fennel;
mod fill;
mod fossil_branch;
mod fossil_metrics;
mod gcloud;
mod git_branch;
mod git_commit;
@ -129,6 +130,7 @@ pub fn handle<'a>(module: &str, context: &'a Context) -> Option<Module<'a>> {
"fennel" => fennel::module(context),
"fill" => fill::module(context),
"fossil_branch" => fossil_branch::module(context),
"fossil_metrics" => fossil_metrics::module(context),
"gcloud" => gcloud::module(context),
"git_branch" => git_branch::module(context),
"git_commit" => git_commit::module(context),
@ -244,6 +246,7 @@ pub fn description(module: &str) -> &'static str {
"fennel" => "The currently installed version of Fennel",
"fill" => "Fills the remaining space on the line with a pad string",
"fossil_branch" => "The active branch of the check-out in your current directory",
"fossil_metrics" => "The currently added/deleted lines in your check-out",
"gcloud" => "The current GCP client configuration",
"git_branch" => "The active branch of the repo in your current directory",
"git_commit" => "The active commit (and tag if any) of the repo in your current directory",

View File

@ -257,6 +257,12 @@ Elixir 1.10 (compiled with Erlang/OTP 22)\n",
stdout: String::default(),
stderr: String::default(),
}),
"fossil diff --numstat" => Some(CommandOutput{
stdout: String::from("\
3 2 README.md
3 2 TOTAL over 1 changed files"),
stderr: String::default(),
}),
"fossil update topic-branch" => Some(CommandOutput{
stdout: String::default(),
stderr: String::default(),