mirror of
https://github.com/Llewellynvdm/starship.git
synced 2025-01-13 10:25:33 +00:00
feat: add a container indicator (#3304)
* test: add mock method for absolute files Signed-off-by: Harald Hoyer <harald@hoyer.xyz> * feat(module): add a container indicator module Adds a container type indicator, if inside a container, detected via the presence of some marker files. E.g. inside a podman container entered with `toolbox enter` the prompt changes to the container name and version. ``` starship on container_rebased [$!] is 📦 v1.0.0 via 🦀 v1.56.1 ❯ toolbox enter starship on container_rebased [$!] is 📦 v1.0.0 via 🦀 v1.56.1 ⬢ [fedora-toolbox:35] ❯ ``` Signed-off-by: Harald Hoyer <harald@hoyer.xyz>
This commit is contained in:
parent
0d573ac5ea
commit
4f46411403
@ -70,6 +70,47 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
|||||||
|
|
||||||
If using `context.exec_cmd` isn't possible, please use `crate::utils::create_command` instead of `std::process::Command::new`.
|
If using `context.exec_cmd` isn't possible, please use `crate::utils::create_command` instead of `std::process::Command::new`.
|
||||||
|
|
||||||
|
## Absolute Filenames
|
||||||
|
|
||||||
|
To use absolute filenames in your module, use `crate::utils::context_path()` to create a `PathBuf` from an absolute pathname.
|
||||||
|
In the test environment the root directory will be replaced with a `Tempdir`, which you can get via `ModuleRenderer::root_path()`.
|
||||||
|
So, you can populate that mocked root directory with any files you want.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use crate::utils::context_path;
|
||||||
|
|
||||||
|
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
||||||
|
if !context_path(context, "/run/test/testfile").exists() {
|
||||||
|
return None
|
||||||
|
}
|
||||||
|
// ..
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[test]
|
||||||
|
fn test_testfile() {
|
||||||
|
let renderer = ModuleRenderer::new("mymodule");
|
||||||
|
|
||||||
|
let root_path = renderer.root_path();
|
||||||
|
|
||||||
|
// This creates `$TEMPDIR/run/test/testfile`
|
||||||
|
|
||||||
|
let mut absolute_test_file = PathBuf::from(root_path);
|
||||||
|
|
||||||
|
absolute_test_file.push("run");
|
||||||
|
absolute_test_file.push("test");
|
||||||
|
std::fs::DirBuilder::new()
|
||||||
|
.recursive(true)
|
||||||
|
.create(&absolute_test_file)?;
|
||||||
|
|
||||||
|
absolute_test_file.push("testfile");
|
||||||
|
std::fs::File::create(&absolute_test_file)?;
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Logging
|
## Logging
|
||||||
|
|
||||||
Debug logging in starship is done with our custom logger implementation.
|
Debug logging in starship is done with our custom logger implementation.
|
||||||
|
@ -207,6 +207,7 @@ $docker_context\
|
|||||||
$package\
|
$package\
|
||||||
$cmake\
|
$cmake\
|
||||||
$cobol\
|
$cobol\
|
||||||
|
$container\
|
||||||
$dart\
|
$dart\
|
||||||
$deno\
|
$deno\
|
||||||
$dotnet\
|
$dotnet\
|
||||||
@ -672,6 +673,40 @@ This does not suppress conda's own prompt modifier, you may want to run `conda c
|
|||||||
format = "[$symbol$environment](dimmed green) "
|
format = "[$symbol$environment](dimmed green) "
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Container
|
||||||
|
|
||||||
|
The `container` module displays a symbol and container name, if inside a container.
|
||||||
|
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
| Option | Default | Description |
|
||||||
|
|------------|-----------------------------------|-------------------------------------------|
|
||||||
|
| `symbol` | `"⬢"` | The symbol shown, when inside a container |
|
||||||
|
| `style` | `"bold red dimmed"` | The style for the module. |
|
||||||
|
| `format` | "[$symbol \\[$name\\]]($style) " | The format for the module. |
|
||||||
|
| `disabled` | `false` | Disables the `container` module. |
|
||||||
|
|
||||||
|
|
||||||
|
### Variables
|
||||||
|
|
||||||
|
| Variable | Example | Description |
|
||||||
|
|----------|---------------------|--------------------------------------|
|
||||||
|
| name | `fedora-toolbox:35` | The name of the container |
|
||||||
|
| symbol | | Mirrors the value of option `symbol` |
|
||||||
|
| style\* | | Mirrors the value of option `style` |
|
||||||
|
|
||||||
|
\*: This variable can only be used as a part of a style string
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# ~/.config/starship.toml
|
||||||
|
|
||||||
|
[container]
|
||||||
|
format = "[$symbol \\[$name\\]]($style) "
|
||||||
|
```
|
||||||
|
|
||||||
## Crystal
|
## Crystal
|
||||||
|
|
||||||
The `crystal` module shows the currently installed version of [Crystal](https://crystal-lang.org/).
|
The `crystal` module shows the currently installed version of [Crystal](https://crystal-lang.org/).
|
||||||
|
23
src/configs/container.rs
Normal file
23
src/configs/container.rs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
use crate::config::ModuleConfig;
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
use starship_module_config_derive::ModuleConfig;
|
||||||
|
|
||||||
|
#[derive(Clone, ModuleConfig, Serialize)]
|
||||||
|
pub struct ContainerConfig<'a> {
|
||||||
|
pub format: &'a str,
|
||||||
|
pub symbol: &'a str,
|
||||||
|
pub style: &'a str,
|
||||||
|
pub disabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Default for ContainerConfig<'a> {
|
||||||
|
fn default() -> Self {
|
||||||
|
ContainerConfig {
|
||||||
|
format: "[$symbol \\[$name\\]]($style) ",
|
||||||
|
symbol: "⬢",
|
||||||
|
style: "red bold dimmed",
|
||||||
|
disabled: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,7 @@ pub mod cmake;
|
|||||||
pub mod cmd_duration;
|
pub mod cmd_duration;
|
||||||
pub mod cobol;
|
pub mod cobol;
|
||||||
pub mod conda;
|
pub mod conda;
|
||||||
|
pub mod container;
|
||||||
pub mod crystal;
|
pub mod crystal;
|
||||||
pub mod custom;
|
pub mod custom;
|
||||||
pub mod dart;
|
pub mod dart;
|
||||||
@ -93,6 +94,7 @@ pub struct FullConfig<'a> {
|
|||||||
cmd_duration: cmd_duration::CmdDurationConfig<'a>,
|
cmd_duration: cmd_duration::CmdDurationConfig<'a>,
|
||||||
cobol: cobol::CobolConfig<'a>,
|
cobol: cobol::CobolConfig<'a>,
|
||||||
conda: conda::CondaConfig<'a>,
|
conda: conda::CondaConfig<'a>,
|
||||||
|
container: container::ContainerConfig<'a>,
|
||||||
crystal: crystal::CrystalConfig<'a>,
|
crystal: crystal::CrystalConfig<'a>,
|
||||||
dart: dart::DartConfig<'a>,
|
dart: dart::DartConfig<'a>,
|
||||||
deno: deno::DenoConfig<'a>,
|
deno: deno::DenoConfig<'a>,
|
||||||
@ -172,6 +174,7 @@ impl<'a> Default for FullConfig<'a> {
|
|||||||
cmd_duration: Default::default(),
|
cmd_duration: Default::default(),
|
||||||
cobol: Default::default(),
|
cobol: Default::default(),
|
||||||
conda: Default::default(),
|
conda: Default::default(),
|
||||||
|
container: Default::default(),
|
||||||
crystal: Default::default(),
|
crystal: Default::default(),
|
||||||
dart: Default::default(),
|
dart: Default::default(),
|
||||||
deno: Default::default(),
|
deno: Default::default(),
|
||||||
|
@ -86,6 +86,7 @@ pub const PROMPT_ORDER: &[&str] = &[
|
|||||||
"battery",
|
"battery",
|
||||||
"time",
|
"time",
|
||||||
"status",
|
"status",
|
||||||
|
"container",
|
||||||
"shell",
|
"shell",
|
||||||
"character",
|
"character",
|
||||||
];
|
];
|
||||||
|
@ -63,6 +63,10 @@ pub struct Context<'a> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub cmd: HashMap<&'a str, Option<CommandOutput>>,
|
pub cmd: HashMap<&'a str, Option<CommandOutput>>,
|
||||||
|
|
||||||
|
/// a mock of the root directory
|
||||||
|
#[cfg(test)]
|
||||||
|
pub root_dir: tempfile::TempDir,
|
||||||
|
|
||||||
#[cfg(feature = "battery")]
|
#[cfg(feature = "battery")]
|
||||||
pub battery_info_provider: &'a (dyn crate::modules::BatteryInfoProvider + Send + Sync),
|
pub battery_info_provider: &'a (dyn crate::modules::BatteryInfoProvider + Send + Sync),
|
||||||
|
|
||||||
@ -155,6 +159,8 @@ impl<'a> Context<'a> {
|
|||||||
target,
|
target,
|
||||||
width,
|
width,
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
root_dir: tempfile::TempDir::new().unwrap(),
|
||||||
|
#[cfg(test)]
|
||||||
env: HashMap::new(),
|
env: HashMap::new(),
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
cmd: HashMap::new(),
|
cmd: HashMap::new(),
|
||||||
|
@ -17,6 +17,7 @@ pub const ALL_MODULES: &[&str] = &[
|
|||||||
"cmd_duration",
|
"cmd_duration",
|
||||||
"cobol",
|
"cobol",
|
||||||
"conda",
|
"conda",
|
||||||
|
"container",
|
||||||
"crystal",
|
"crystal",
|
||||||
"dart",
|
"dart",
|
||||||
"deno",
|
"deno",
|
||||||
|
196
src/modules/container.rs
Normal file
196
src/modules/container.rs
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
use super::{Context, Module};
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
pub fn module<'a>(_context: &'a Context) -> Option<Module<'a>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
||||||
|
use super::RootModuleConfig;
|
||||||
|
use crate::configs::container::ContainerConfig;
|
||||||
|
use crate::formatter::StringFormatter;
|
||||||
|
use crate::utils::read_file;
|
||||||
|
|
||||||
|
pub fn container_name(context: &Context) -> Option<String> {
|
||||||
|
use crate::utils::context_path;
|
||||||
|
|
||||||
|
if context_path(context, "/proc/vz").exists() && !context_path(context, "/proc/bc").exists()
|
||||||
|
{
|
||||||
|
// OpenVZ
|
||||||
|
return Some("OpenVZ".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if context_path(context, "/run/host/container-manager").exists() {
|
||||||
|
// OCI
|
||||||
|
return Some("OCI".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if context_path(context, "/run/systemd/container").exists() {
|
||||||
|
// systemd
|
||||||
|
return Some("Systemd".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let container_env_path = context_path(context, "/run/.containerenv");
|
||||||
|
|
||||||
|
if container_env_path.exists() {
|
||||||
|
// podman and others
|
||||||
|
|
||||||
|
let image_res = read_file(container_env_path)
|
||||||
|
.map(|s| {
|
||||||
|
s.lines()
|
||||||
|
.find_map(|l| {
|
||||||
|
l.starts_with("image=\"").then(|| {
|
||||||
|
let r = l.split_at(7).1;
|
||||||
|
let name = r.rfind('/').map(|n| r.split_at(n + 1).1);
|
||||||
|
String::from(name.unwrap_or(r).trim_end_matches('"'))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| "podman".into())
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|_| "podman".into());
|
||||||
|
|
||||||
|
return Some(image_res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if context_path(context, "/.dockerenv").exists() {
|
||||||
|
// docker
|
||||||
|
return Some("Docker".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut module = context.new_module("container");
|
||||||
|
let config: ContainerConfig = ContainerConfig::try_load(module.config);
|
||||||
|
|
||||||
|
if config.disabled {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let container_name = container_name(context)?;
|
||||||
|
|
||||||
|
let parsed = StringFormatter::new(config.format).and_then(|formatter| {
|
||||||
|
formatter
|
||||||
|
.map_meta(|variable, _| match variable {
|
||||||
|
"symbol" => Some(config.symbol),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.map_style(|variable| match variable {
|
||||||
|
"style" => Some(Ok(config.style)),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.map(|variable| match variable {
|
||||||
|
"name" => Some(Ok(&container_name)),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.parse(None, Some(context))
|
||||||
|
});
|
||||||
|
|
||||||
|
module.set_segments(match parsed {
|
||||||
|
Ok(segments) => segments,
|
||||||
|
Err(error) => {
|
||||||
|
log::warn!("Error in module `container`: \n{}", error);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(module)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::test::ModuleRenderer;
|
||||||
|
use ansi_term::Color;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_none_if_disabled() {
|
||||||
|
let expected = None;
|
||||||
|
let actual = ModuleRenderer::new("container")
|
||||||
|
// For a custom config
|
||||||
|
.config(toml::toml! {
|
||||||
|
[container]
|
||||||
|
disabled = true
|
||||||
|
})
|
||||||
|
// Run the module and collect the output
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
assert_eq!(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn containerenv(name: Option<&str>) -> std::io::Result<(Option<String>, Option<String>)> {
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
let renderer = ModuleRenderer::new("container")
|
||||||
|
// For a custom config
|
||||||
|
.config(toml::toml! {
|
||||||
|
[container]
|
||||||
|
disabled = false
|
||||||
|
});
|
||||||
|
|
||||||
|
let root_path = renderer.root_path();
|
||||||
|
|
||||||
|
let mut containerenv = PathBuf::from(root_path);
|
||||||
|
|
||||||
|
containerenv.push("run");
|
||||||
|
std::fs::DirBuilder::new()
|
||||||
|
.recursive(true)
|
||||||
|
.create(&containerenv)?;
|
||||||
|
|
||||||
|
containerenv.push(".containerenv");
|
||||||
|
let mut file = std::fs::File::create(&containerenv)?;
|
||||||
|
if let Some(name) = name {
|
||||||
|
file.write_all(format!("image=\"{}\"\n", name).as_bytes())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The output of the module
|
||||||
|
let actual = renderer
|
||||||
|
// Run the module and collect the output
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// The value that should be rendered by the module.
|
||||||
|
let expected = Some(format!(
|
||||||
|
"{} ",
|
||||||
|
Color::Red
|
||||||
|
.bold()
|
||||||
|
.dimmed()
|
||||||
|
.paint(format!("⬢ [{}]", name.unwrap_or("podman")))
|
||||||
|
));
|
||||||
|
|
||||||
|
Ok((actual, expected))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn test_containerenv() -> std::io::Result<()> {
|
||||||
|
let (actual, expected) = containerenv(None)?;
|
||||||
|
|
||||||
|
// Assert that the actual and expected values are the same
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn test_containerenv_fedora() -> std::io::Result<()> {
|
||||||
|
let (actual, expected) = containerenv(Some("fedora-toolbox:35"))?;
|
||||||
|
|
||||||
|
// Assert that the actual and expected values are the same
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
fn test_containerenv() -> std::io::Result<()> {
|
||||||
|
let (actual, expected) = containerenv(None)?;
|
||||||
|
|
||||||
|
// Assert that the actual and expected values are not the same
|
||||||
|
assert_ne!(actual, expected);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
@ -6,6 +6,7 @@ mod cmake;
|
|||||||
mod cmd_duration;
|
mod cmd_duration;
|
||||||
mod cobol;
|
mod cobol;
|
||||||
mod conda;
|
mod conda;
|
||||||
|
mod container;
|
||||||
mod crystal;
|
mod crystal;
|
||||||
pub(crate) mod custom;
|
pub(crate) mod custom;
|
||||||
mod dart;
|
mod dart;
|
||||||
@ -93,6 +94,7 @@ pub fn handle<'a>(module: &str, context: &'a Context) -> Option<Module<'a>> {
|
|||||||
"cmd_duration" => cmd_duration::module(context),
|
"cmd_duration" => cmd_duration::module(context),
|
||||||
"cobol" => cobol::module(context),
|
"cobol" => cobol::module(context),
|
||||||
"conda" => conda::module(context),
|
"conda" => conda::module(context),
|
||||||
|
"container" => container::module(context),
|
||||||
"dart" => dart::module(context),
|
"dart" => dart::module(context),
|
||||||
"deno" => deno::module(context),
|
"deno" => deno::module(context),
|
||||||
"directory" => directory::module(context),
|
"directory" => directory::module(context),
|
||||||
@ -182,6 +184,7 @@ pub fn description(module: &str) -> &'static str {
|
|||||||
"cmd_duration" => "How long the last command took to execute",
|
"cmd_duration" => "How long the last command took to execute",
|
||||||
"cobol" => "The currently installed version of COBOL/GNUCOBOL",
|
"cobol" => "The currently installed version of COBOL/GNUCOBOL",
|
||||||
"conda" => "The current conda environment, if $CONDA_DEFAULT_ENV is set",
|
"conda" => "The current conda environment, if $CONDA_DEFAULT_ENV is set",
|
||||||
|
"container" => "The container indicator, if inside a container.",
|
||||||
"crystal" => "The currently installed version of Crystal",
|
"crystal" => "The currently installed version of Crystal",
|
||||||
"dart" => "The currently installed version of Dart",
|
"dart" => "The currently installed version of Dart",
|
||||||
"deno" => "The currently installed version of Deno",
|
"deno" => "The currently installed version of Deno",
|
||||||
|
@ -8,7 +8,7 @@ use crate::{
|
|||||||
use log::{Level, LevelFilter};
|
use log::{Level, LevelFilter};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
static FIXTURE_DIR: Lazy<PathBuf> =
|
static FIXTURE_DIR: Lazy<PathBuf> =
|
||||||
@ -70,6 +70,10 @@ impl<'a> ModuleRenderer<'a> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn root_path(&self) -> &Path {
|
||||||
|
self.context.root_dir.path()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn logical_path<T>(mut self, path: T) -> Self
|
pub fn logical_path<T>(mut self, path: T) -> Self
|
||||||
where
|
where
|
||||||
T: Into<PathBuf>,
|
T: Into<PathBuf>,
|
||||||
|
24
src/utils.rs
24
src/utils.rs
@ -7,8 +7,32 @@ use std::path::{Path, PathBuf};
|
|||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use crate::context::Context;
|
||||||
use crate::context::Shell;
|
use crate::context::Shell;
|
||||||
|
|
||||||
|
/// Create a `PathBuf` from an absolute path, where the root directory will be mocked in test
|
||||||
|
#[cfg(not(test))]
|
||||||
|
#[inline]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn context_path<S: AsRef<OsStr> + ?Sized>(_context: &Context, s: &S) -> PathBuf {
|
||||||
|
PathBuf::from(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a `PathBuf` from an absolute path, where the root directory will be mocked in test
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn context_path<S: AsRef<OsStr> + ?Sized>(context: &Context, s: &S) -> PathBuf {
|
||||||
|
let requested_path = PathBuf::from(s);
|
||||||
|
|
||||||
|
if requested_path.is_absolute() {
|
||||||
|
let mut path = PathBuf::from(context.root_dir.path());
|
||||||
|
path.extend(requested_path.components().skip(1));
|
||||||
|
path
|
||||||
|
} else {
|
||||||
|
requested_path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Return the string contents of a file
|
/// Return the string contents of a file
|
||||||
pub fn read_file<P: AsRef<Path> + Debug>(file_name: P) -> Result<String> {
|
pub fn read_file<P: AsRef<Path> + Debug>(file_name: P) -> Result<String> {
|
||||||
log::trace!("Trying to read from {:?}", file_name);
|
log::trace!("Trying to read from {:?}", file_name);
|
||||||
|
Loading…
Reference in New Issue
Block a user