diff options
author | Andrew Pantuso <ajpantuso@gmail.com> | 2023-12-17 02:22:29 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-12-17 08:22:29 +0100 |
commit | e47bfbabb9b7d6af12a29db9413a6ec03fba174b (patch) | |
tree | ef6ca7ea19ae1e30fb23d66fb19fe09a8f668fa6 /src/modules | |
parent | 6d96df3c6828161bb9dc922fe45ef35a1ce33771 (diff) |
feat(direnv): add new direnv module (#5157)
Diffstat (limited to 'src/modules')
-rw-r--r-- | src/modules/direnv.rs | 280 | ||||
-rw-r--r-- | src/modules/mod.rs | 3 |
2 files changed, 283 insertions, 0 deletions
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", |