summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAndrew Pantuso <ajpantuso@gmail.com>2023-12-17 02:22:29 -0500
committerGitHub <noreply@github.com>2023-12-17 08:22:29 +0100
commite47bfbabb9b7d6af12a29db9413a6ec03fba174b (patch)
treeef6ca7ea19ae1e30fb23d66fb19fe09a8f668fa6
parent6d96df3c6828161bb9dc922fe45ef35a1ce33771 (diff)
feat(direnv): add new direnv module (#5157)
-rw-r--r--.github/config-schema.json83
-rw-r--r--docs/config/README.md42
-rwxr-xr-xsrc/configs/direnv.rs40
-rw-r--r--src/configs/mod.rs3
-rw-r--r--src/configs/starship_root.rs1
-rw-r--r--src/module.rs1
-rw-r--r--src/modules/direnv.rs280
-rw-r--r--src/modules/mod.rs3
8 files changed, 453 insertions, 0 deletions
diff --git a/.github/config-schema.json b/.github/config-schema.json
index a036fddb8..914693c34 100644
--- a/.github/config-schema.json
+++ b/.github/config-schema.json
@@ -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": {
diff --git a/docs/config/README.md b/docs/config/README.md
index 25b52b0d9..046ad01d4 100644
--- a/docs/config/README.md
+++ b/docs/config/README.md
@@ -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
diff --git a/src/configs/direnv.rs b/src/configs/direnv.rs
new file mode 100755
index 000000000..5a58d795c
--- /dev/null
+++ b/src/configs/direnv.rs
@@ -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",
+ }
+ }
+}
diff --git a/src/configs/mod.rs b/src/configs/mod.rs
index 883055b81..c2cf0a8d9 100644
--- a/src/configs/mod.rs
+++ b/src/configs/mod.rs
@@ -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>,
diff --git a/src/configs/starship_root.rs b/src/configs/starship_root.rs
index c93c1543f..86fe7e754 100644
--- a/src/configs/starship_root.rs
+++ b/src/configs/starship_root.rs
@@ -107,6 +107,7 @@ pub const PROMPT_ORDER: &[&str] = &[
"gcloud",
"openstack",
"azure",
+ "direnv",
"env_var",
"crystal",
"custom",
diff --git a/src/module.rs b/src/module.rs
index 33f8287b7..0dcc0aad4 100644
--- a/src/module.rs
+++ b/src/module.rs
@@ -27,6 +27,7 @@ pub const ALL_MODULES: &[&str] = &[
"dart",
"deno",
"directory",
+ "direnv",
"docker_context",
"dotnet",
"elixir",
diff --git a/src/modules/direnv.rs b/src/modules/direnv.rs
new file mode 100644
index 000000000..4f67800ea
--- /dev/null
+++ b/src/modules/direnv.rs
@@ -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
+"#
+ )
+ }
+}
diff --git a/src/modules/mod.rs b/src/modules/mod.rs
index cc1eec4e4..57983cd1e 100644
--- a/src/modules/mod.rs
+++ b/src/modules/mod.rs
@@ -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",