feat(direnv): add new direnv module (#5157)
This commit is contained in:
parent
6d96df3c68
commit
e47bfbabb9
|
@ -354,6 +354,28 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"direnv": {
|
||||
"default": {
|
||||
"allowed_msg": "allowed",
|
||||
"denied_msg": "denied",
|
||||
"detect_extensions": [],
|
||||
"detect_files": [
|
||||
".envrc"
|
||||
],
|
||||
"detect_folders": [],
|
||||
"disabled": true,
|
||||
"format": "[$symbol$loaded/$allowed]($style) ",
|
||||
"loaded_msg": "loaded",
|
||||
"style": "bold orange",
|
||||
"symbol": "direnv ",
|
||||
"unloaded_msg": "not loaded"
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/DirenvConfig"
|
||||
}
|
||||
]
|
||||
},
|
||||
"docker_context": {
|
||||
"default": {
|
||||
"detect_extensions": [],
|
||||
|
@ -2707,6 +2729,67 @@
|
|||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"DirenvConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"format": {
|
||||
"default": "[$symbol$loaded/$allowed]($style) ",
|
||||
"type": "string"
|
||||
},
|
||||
"symbol": {
|
||||
"default": "direnv ",
|
||||
"type": "string"
|
||||
},
|
||||
"style": {
|
||||
"default": "bold orange",
|
||||
"type": "string"
|
||||
},
|
||||
"disabled": {
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"detect_extensions": {
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"detect_files": {
|
||||
"default": [
|
||||
".envrc"
|
||||
],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"detect_folders": {
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"allowed_msg": {
|
||||
"default": "allowed",
|
||||
"type": "string"
|
||||
},
|
||||
"denied_msg": {
|
||||
"default": "denied",
|
||||
"type": "string"
|
||||
},
|
||||
"loaded_msg": {
|
||||
"default": "loaded",
|
||||
"type": "string"
|
||||
},
|
||||
"unloaded_msg": {
|
||||
"default": "not loaded",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"DockerContextConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
@ -337,6 +337,7 @@ $aws\
|
|||
$gcloud\
|
||||
$openstack\
|
||||
$azure\
|
||||
$direnv\
|
||||
$env_var\
|
||||
$crystal\
|
||||
$custom\
|
||||
|
@ -1208,6 +1209,47 @@ truncation_length = 8
|
|||
truncation_symbol = '…/'
|
||||
```
|
||||
|
||||
## Direnv
|
||||
|
||||
The `direnv` module shows the status of the current rc file if one is present. The status includes the path to the rc file, whether it is loaded, and whether it has been allowed by `direnv`.
|
||||
|
||||
### Options
|
||||
|
||||
| Option | Default | Description |
|
||||
| ------------------- | -------------------------------------- | ----------------------------------------------------- |
|
||||
| `format` | `'[$symbol$loaded/$allowed]($style) '` | The format for the module. |
|
||||
| `symbol` | `'direnv '` | The symbol used before displaying the direnv context. |
|
||||
| `style` | `'bold orange'` | The style for the module. |
|
||||
| `disabled` | `true` | Disables the `direnv` module. |
|
||||
| `detect_extensions` | `[]` | Which extensions should trigger this module. |
|
||||
| `detect_files` | `['.envrc']` | Which filenames should trigger this module. |
|
||||
| `detect_folders` | `[]` | Which folders should trigger this module. |
|
||||
| `allowed_msg` | `'allowed'` | The message displayed when an rc file is allowed. |
|
||||
| `denied_msg` | `'denied'` | The message displayed when an rc file is denied. |
|
||||
| `loaded_msg` | `'loaded'` | The message displayed when an rc file is loaded. |
|
||||
| `unloaded_msg` | `'not loaded'` | The message displayed when an rc file is not loaded. |
|
||||
|
||||
### Variables
|
||||
|
||||
| Variable | Example | Description |
|
||||
| -------- | ------------------- | --------------------------------------- |
|
||||
| loaded | `loaded` | Whether the current rc file is loaded. |
|
||||
| allowed | `denied` | Whether the current rc file is allowed. |
|
||||
| rc_path | `/home/test/.envrc` | The current rc file path. |
|
||||
| symbol | | Mirrors the value of option `symbol`. |
|
||||
| style\* | `red bold` | Mirrors the value of option `style`. |
|
||||
|
||||
*: This variable can only be used as a part of a style string
|
||||
|
||||
### Example
|
||||
|
||||
```toml
|
||||
# ~/.config/starship.toml
|
||||
|
||||
[direnv]
|
||||
disabled = false
|
||||
```
|
||||
|
||||
## Docker Context
|
||||
|
||||
The `docker_context` module shows the currently active
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
#[cfg_attr(
|
||||
feature = "config-schema",
|
||||
derive(schemars::JsonSchema),
|
||||
schemars(deny_unknown_fields)
|
||||
)]
|
||||
#[serde(default)]
|
||||
pub struct DirenvConfig<'a> {
|
||||
pub format: &'a str,
|
||||
pub symbol: &'a str,
|
||||
pub style: &'a str,
|
||||
pub disabled: bool,
|
||||
pub detect_extensions: Vec<&'a str>,
|
||||
pub detect_files: Vec<&'a str>,
|
||||
pub detect_folders: Vec<&'a str>,
|
||||
pub allowed_msg: &'a str,
|
||||
pub denied_msg: &'a str,
|
||||
pub loaded_msg: &'a str,
|
||||
pub unloaded_msg: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> Default for DirenvConfig<'a> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
format: "[$symbol$loaded/$allowed]($style) ",
|
||||
symbol: "direnv ",
|
||||
style: "bold orange",
|
||||
disabled: true,
|
||||
detect_extensions: vec![],
|
||||
detect_files: vec![".envrc"],
|
||||
detect_folders: vec![],
|
||||
allowed_msg: "allowed",
|
||||
denied_msg: "denied",
|
||||
loaded_msg: "loaded",
|
||||
unloaded_msg: "not loaded",
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@ pub mod daml;
|
|||
pub mod dart;
|
||||
pub mod deno;
|
||||
pub mod directory;
|
||||
pub mod direnv;
|
||||
pub mod docker_context;
|
||||
pub mod dotnet;
|
||||
pub mod elixir;
|
||||
|
@ -143,6 +144,8 @@ pub struct FullConfig<'a> {
|
|||
#[serde(borrow)]
|
||||
directory: directory::DirectoryConfig<'a>,
|
||||
#[serde(borrow)]
|
||||
direnv: direnv::DirenvConfig<'a>,
|
||||
#[serde(borrow)]
|
||||
docker_context: docker_context::DockerContextConfig<'a>,
|
||||
#[serde(borrow)]
|
||||
dotnet: dotnet::DotnetConfig<'a>,
|
||||
|
|
|
@ -107,6 +107,7 @@ pub const PROMPT_ORDER: &[&str] = &[
|
|||
"gcloud",
|
||||
"openstack",
|
||||
"azure",
|
||||
"direnv",
|
||||
"env_var",
|
||||
"crystal",
|
||||
"custom",
|
||||
|
|
|
@ -27,6 +27,7 @@ pub const ALL_MODULES: &[&str] = &[
|
|||
"dart",
|
||||
"deno",
|
||||
"directory",
|
||||
"direnv",
|
||||
"docker_context",
|
||||
"dotnet",
|
||||
"elixir",
|
||||
|
|
|
@ -0,0 +1,280 @@
|
|||
use std::borrow::Cow;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use super::{Context, Module, ModuleConfig};
|
||||
|
||||
use crate::configs::direnv::DirenvConfig;
|
||||
use crate::formatter::StringFormatter;
|
||||
|
||||
/// Creates a module with the current direnv rc
|
||||
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
||||
let mut module = context.new_module("direnv");
|
||||
let config = DirenvConfig::try_load(module.config);
|
||||
|
||||
let direnv_applies = !config.disabled
|
||||
&& context
|
||||
.try_begin_scan()?
|
||||
.set_extensions(&config.detect_extensions)
|
||||
.set_files(&config.detect_files)
|
||||
.set_folders(&config.detect_folders)
|
||||
.is_match();
|
||||
|
||||
if !direnv_applies {
|
||||
return None;
|
||||
}
|
||||
|
||||
let direnv_status = &context.exec_cmd("direnv", &["status"])?.stdout;
|
||||
let state = match DirenvState::from_str(direnv_status) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
log::warn!("{e}");
|
||||
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let parsed = StringFormatter::new(config.format).and_then(|formatter| {
|
||||
formatter
|
||||
.map_style(|variable| match variable {
|
||||
"style" => Some(Ok(config.style)),
|
||||
_ => None,
|
||||
})
|
||||
.map(|variable| match variable {
|
||||
"symbol" => Some(Ok(Cow::from(config.symbol))),
|
||||
"rc_path" => Some(Ok(state.rc_path.to_string_lossy())),
|
||||
"allowed" => Some(Ok(match state.allowed {
|
||||
AllowStatus::Allowed => Cow::from(config.allowed_msg),
|
||||
AllowStatus::Denied => Cow::from(config.denied_msg),
|
||||
})),
|
||||
"loaded" => state
|
||||
.loaded
|
||||
.then_some(config.loaded_msg)
|
||||
.or(Some(config.unloaded_msg))
|
||||
.map(Cow::from)
|
||||
.map(Ok),
|
||||
_ => None,
|
||||
})
|
||||
.parse(None, Some(context))
|
||||
});
|
||||
|
||||
module.set_segments(match parsed {
|
||||
Ok(segments) => segments,
|
||||
Err(e) => {
|
||||
log::warn!("{e}");
|
||||
|
||||
return None;
|
||||
}
|
||||
});
|
||||
|
||||
Some(module)
|
||||
}
|
||||
|
||||
struct DirenvState {
|
||||
pub rc_path: PathBuf,
|
||||
pub allowed: AllowStatus,
|
||||
pub loaded: bool,
|
||||
}
|
||||
|
||||
impl FromStr for DirenvState {
|
||||
type Err = Cow<'static, str>;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut rc_path = PathBuf::new();
|
||||
let mut allowed = None;
|
||||
let mut loaded = true;
|
||||
|
||||
for line in s.lines() {
|
||||
if let Some(path) = line.strip_prefix("Found RC path") {
|
||||
rc_path = PathBuf::from_str(path.trim()).map_err(|e| Cow::from(e.to_string()))?
|
||||
} else if let Some(value) = line.strip_prefix("Found RC allowed") {
|
||||
allowed = Some(AllowStatus::from_str(value.trim())?);
|
||||
} else if line.contains("No .envrc or .env loaded") {
|
||||
loaded = false;
|
||||
};
|
||||
}
|
||||
|
||||
if rc_path.as_os_str().is_empty() || allowed.is_none() {
|
||||
return Err(Cow::from("unknown direnv state"));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
rc_path,
|
||||
allowed: allowed.unwrap(),
|
||||
loaded,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum AllowStatus {
|
||||
Allowed,
|
||||
Denied,
|
||||
}
|
||||
|
||||
impl FromStr for AllowStatus {
|
||||
type Err = Cow<'static, str>;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"true" => Ok(AllowStatus::Allowed),
|
||||
"false" => Ok(AllowStatus::Denied),
|
||||
_ => Err(Cow::from("invalid allow status")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::test::ModuleRenderer;
|
||||
use crate::utils::CommandOutput;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
#[test]
|
||||
fn folder_without_rc_files() {
|
||||
let renderer = ModuleRenderer::new("direnv")
|
||||
.config(toml::toml! {
|
||||
[direnv]
|
||||
disabled = false
|
||||
})
|
||||
.cmd(
|
||||
"direnv status",
|
||||
Some(CommandOutput {
|
||||
stdout: status_cmd_output_without_rc(),
|
||||
stderr: String::default(),
|
||||
}),
|
||||
);
|
||||
|
||||
assert_eq!(None, renderer.collect());
|
||||
}
|
||||
#[test]
|
||||
fn folder_with_unloaded_rc_file() -> io::Result<()> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let rc_path = dir.path().join(".envrc");
|
||||
|
||||
std::fs::File::create(&rc_path)?.sync_all()?;
|
||||
|
||||
let renderer = ModuleRenderer::new("direnv")
|
||||
.config(toml::toml! {
|
||||
[direnv]
|
||||
disabled = false
|
||||
})
|
||||
.path(dir.path())
|
||||
.cmd(
|
||||
"direnv status",
|
||||
Some(CommandOutput {
|
||||
stdout: status_cmd_output_with_rc(dir.path(), false, true),
|
||||
stderr: String::default(),
|
||||
}),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Some(format!("direnv not loaded/allowed ")),
|
||||
renderer.collect()
|
||||
);
|
||||
|
||||
dir.close()
|
||||
}
|
||||
#[test]
|
||||
fn folder_with_loaded_rc_file() -> io::Result<()> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let rc_path = dir.path().join(".envrc");
|
||||
|
||||
std::fs::File::create(&rc_path)?.sync_all()?;
|
||||
|
||||
let renderer = ModuleRenderer::new("direnv")
|
||||
.config(toml::toml! {
|
||||
[direnv]
|
||||
disabled = false
|
||||
})
|
||||
.path(dir.path())
|
||||
.cmd(
|
||||
"direnv status",
|
||||
Some(CommandOutput {
|
||||
stdout: status_cmd_output_with_rc(dir.path(), true, true),
|
||||
stderr: String::default(),
|
||||
}),
|
||||
);
|
||||
|
||||
assert_eq!(Some(format!("direnv loaded/allowed ")), renderer.collect());
|
||||
|
||||
dir.close()
|
||||
}
|
||||
#[test]
|
||||
fn folder_with_loaded_and_denied_rc_file() -> io::Result<()> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let rc_path = dir.path().join(".envrc");
|
||||
|
||||
std::fs::File::create(&rc_path)?.sync_all()?;
|
||||
|
||||
let renderer = ModuleRenderer::new("direnv")
|
||||
.config(toml::toml! {
|
||||
[direnv]
|
||||
disabled = false
|
||||
})
|
||||
.path(dir.path())
|
||||
.cmd(
|
||||
"direnv status",
|
||||
Some(CommandOutput {
|
||||
stdout: status_cmd_output_with_rc(dir.path(), true, false),
|
||||
stderr: String::default(),
|
||||
}),
|
||||
);
|
||||
|
||||
assert_eq!(Some(format!("direnv loaded/denied ")), renderer.collect());
|
||||
|
||||
dir.close()
|
||||
}
|
||||
fn status_cmd_output_without_rc() -> String {
|
||||
String::from(
|
||||
r#"\
|
||||
direnv exec path /usr/bin/direnv
|
||||
DIRENV_CONFIG /home/test/.config/direnv
|
||||
bash_path /usr/bin/bash
|
||||
disable_stdin false
|
||||
warn_timeout 5s
|
||||
whitelist.prefix []
|
||||
whitelist.exact map[]
|
||||
No .envrc or .env loaded
|
||||
No .envrc or .env found"#,
|
||||
)
|
||||
}
|
||||
fn status_cmd_output_with_rc(dir: impl AsRef<Path>, loaded: bool, allowed: bool) -> String {
|
||||
let rc_path = dir.as_ref().join(".envrc");
|
||||
let rc_path = rc_path.to_string_lossy();
|
||||
|
||||
let loaded = if loaded {
|
||||
format!(
|
||||
r#"\
|
||||
Loaded RC path {rc_path}
|
||||
Loaded watch: ".envrc" - 2023-04-30T09:51:04-04:00
|
||||
Loaded watch: "../.local/share/direnv/allow/abcd" - 2023-04-30T09:52:58-04:00
|
||||
Loaded RC allowed false
|
||||
Loaded RC allowPath
|
||||
"#
|
||||
)
|
||||
} else {
|
||||
String::from("No .envrc or .env loaded")
|
||||
};
|
||||
|
||||
let state = allowed.to_string();
|
||||
|
||||
format!(
|
||||
r#"\
|
||||
direnv exec path /usr/bin/direnv
|
||||
DIRENV_CONFIG /home/test/.config/direnv
|
||||
bash_path /usr/bin/bash
|
||||
disable_stdin false
|
||||
warn_timeout 5s
|
||||
whitelist.prefix []
|
||||
whitelist.exact map[]
|
||||
{loaded}
|
||||
Found RC path {rc_path}
|
||||
Found watch: ".envrc" - 2023-04-25T18:45:54-04:00
|
||||
Found watch: "../.local/share/direnv/allow/abcd" - 1969-12-31T19:00:00-05:00
|
||||
Found RC allowed {state}
|
||||
Found RC allowPath /home/test/.local/share/direnv/allow/abcd
|
||||
"#
|
||||
)
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@ mod daml;
|
|||
mod dart;
|
||||
mod deno;
|
||||
mod directory;
|
||||
mod direnv;
|
||||
mod docker_context;
|
||||
mod dotnet;
|
||||
mod elixir;
|
||||
|
@ -122,6 +123,7 @@ pub fn handle<'a>(module: &str, context: &'a Context) -> Option<Module<'a>> {
|
|||
"dart" => dart::module(context),
|
||||
"deno" => deno::module(context),
|
||||
"directory" => directory::module(context),
|
||||
"direnv" => direnv::module(context),
|
||||
"docker_context" => docker_context::module(context),
|
||||
"dotnet" => dotnet::module(context),
|
||||
"elixir" => elixir::module(context),
|
||||
|
@ -240,6 +242,7 @@ pub fn description(module: &str) -> &'static str {
|
|||
"dart" => "The currently installed version of Dart",
|
||||
"deno" => "The currently installed version of Deno",
|
||||
"directory" => "The current working directory",
|
||||
"direnv" => "The currently applied direnv file",
|
||||
"docker_context" => "The current docker context",
|
||||
"dotnet" => "The relevant version of the .NET Core SDK for the current directory",
|
||||
"elixir" => "The currently installed versions of Elixir and OTP",
|
||||
|
|
Loading…
Reference in New Issue