feat(direnv): add new direnv module (#5157)

This commit is contained in:
Andrew Pantuso 2023-12-17 02:22:29 -05:00 committed by GitHub
parent 6d96df3c68
commit e47bfbabb9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 453 additions and 0 deletions

View File

@ -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": {

View File

@ -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

40
src/configs/direnv.rs Executable file
View File

@ -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",
}
}
}

View File

@ -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>,

View File

@ -107,6 +107,7 @@ pub const PROMPT_ORDER: &[&str] = &[
"gcloud",
"openstack",
"azure",
"direnv",
"env_var",
"crystal",
"custom",

View File

@ -27,6 +27,7 @@ pub const ALL_MODULES: &[&str] = &[
"dart",
"deno",
"directory",
"direnv",
"docker_context",
"dotnet",
"elixir",

280
src/modules/direnv.rs Normal file
View File

@ -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
"#
)
}
}

View File

@ -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",