diff options
Diffstat (limited to 'crates/atuin-dotfiles')
| -rw-r--r-- | crates/atuin-dotfiles/src/shell.rs | 1 | ||||
| -rw-r--r-- | crates/atuin-dotfiles/src/shell/powershell.rs | 169 | ||||
| -rw-r--r-- | crates/atuin-dotfiles/src/store.rs | 40 | ||||
| -rw-r--r-- | crates/atuin-dotfiles/src/store/var.rs | 53 |
4 files changed, 244 insertions, 19 deletions
diff --git a/crates/atuin-dotfiles/src/shell.rs b/crates/atuin-dotfiles/src/shell.rs index bd61aafa..73a9ce8c 100644 --- a/crates/atuin-dotfiles/src/shell.rs +++ b/crates/atuin-dotfiles/src/shell.rs @@ -8,6 +8,7 @@ use crate::store::AliasStore; pub mod bash; pub mod fish; +pub mod powershell; pub mod xonsh; pub mod zsh; diff --git a/crates/atuin-dotfiles/src/shell/powershell.rs b/crates/atuin-dotfiles/src/shell/powershell.rs new file mode 100644 index 00000000..1daee28b --- /dev/null +++ b/crates/atuin-dotfiles/src/shell/powershell.rs @@ -0,0 +1,169 @@ +use crate::shell::{Alias, Var}; +use crate::store::{AliasStore, var::VarStore}; +use std::path::PathBuf; + +async fn cached_aliases(path: PathBuf, store: &AliasStore) -> String { + match tokio::fs::read_to_string(path).await { + Ok(aliases) => aliases, + Err(r) => { + // we failed to read the file for some reason, but the file does exist + // fallback to generating new aliases on the fly + + store.powershell().await.unwrap_or_else(|e| { + format!("echo 'Atuin: failed to read and generate aliases: \n{r}\n{e}'",) + }) + } + } +} + +async fn cached_vars(path: PathBuf, store: &VarStore) -> String { + match tokio::fs::read_to_string(path).await { + Ok(vars) => vars, + Err(r) => { + // we failed to read the file for some reason, but the file does exist + // fallback to generating new vars on the fly + + store.powershell().await.unwrap_or_else(|e| { + format!("echo 'Atuin: failed to read and generate vars: \n{r}\n{e}'",) + }) + } + } +} + +/// Return powershell dotfile config +/// +/// Do not return an error. We should not prevent the shell from starting. +/// +/// In the worst case, Atuin should not function but the shell should start correctly. +/// +/// While currently this only returns aliases, it will be extended to also return other synced dotfiles +pub async fn alias_config(store: &AliasStore) -> String { + // First try to read the cached config + let aliases = atuin_common::utils::dotfiles_cache_dir().join("aliases.ps1"); + + if aliases.exists() { + return cached_aliases(aliases, store).await; + } + + if let Err(e) = store.build().await { + return format!("echo 'Atuin: failed to generate aliases: {e}'"); + } + + cached_aliases(aliases, store).await +} + +pub async fn var_config(store: &VarStore) -> String { + // First try to read the cached config + let vars = atuin_common::utils::dotfiles_cache_dir().join("vars.ps1"); + + if vars.exists() { + return cached_vars(vars, store).await; + } + + if let Err(e) = store.build().await { + return format!("echo 'Atuin: failed to generate vars: {e}'"); + } + + cached_vars(vars, store).await +} + +pub fn format_alias(alias: &Alias) -> String { + // Set-Alias doesn't support adding implicit arguments, so use a function. + // See https://github.com/PowerShell/PowerShell/issues/12962 + + let mut result = secure_command(&format!( + "function {} {{\n {}{} @args\n}}", + alias.name, + if alias.value.starts_with(['"', '\'']) { + "& " + } else { + "" + }, + alias.value + )); + + // This makes the file layout prettier + result.insert(0, '\n'); + result +} + +pub fn format_var(var: &Var) -> String { + secure_command(&format!( + "${}{} = '{}'", + if var.export { "env:" } else { "" }, + var.name, + var.value.replace("'", "''") + )) +} + +/// Wraps the given command in an Invoke-Expression to ensure the outer script is not halted +/// if the inner command contains a syntax error. +fn secure_command(command: &str) -> String { + format!( + "Invoke-Expression -ErrorAction Continue -Command '{}'\n", + command.replace("'", "''") + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn aliases() { + assert_eq!( + format_alias(&Alias { + name: "gp".to_string(), + value: "git push".to_string(), + }), + "\n".to_string() + + &secure_command( + "function gp { + git push @args +}" + ) + ); + + assert_eq!( + format_alias(&Alias { + name: "spc".to_string(), + value: "\"path with spaces\" arg".to_string(), + }), + "\n".to_string() + + &secure_command( + "function spc { + & \"path with spaces\" arg @args +}" + ) + ); + } + + #[test] + fn vars() { + assert_eq!( + format_var(&Var { + name: "FOO".to_owned(), + value: "bar 'baz'".to_owned(), + export: true, + }), + secure_command("$env:FOO = 'bar ''baz'''") + ); + + assert_eq!( + format_var(&Var { + name: "TEST".to_owned(), + value: "1".to_owned(), + export: false, + }), + secure_command("$TEST = '1'") + ); + } + + #[test] + fn invoke_expression() { + assert_eq!( + secure_command("echo 'foo'"), + "Invoke-Expression -ErrorAction Continue -Command 'echo ''foo'''\n" + ) + } +} diff --git a/crates/atuin-dotfiles/src/store.rs b/crates/atuin-dotfiles/src/store.rs index 01316b4e..17597065 100644 --- a/crates/atuin-dotfiles/src/store.rs +++ b/crates/atuin-dotfiles/src/store.rs @@ -142,7 +142,20 @@ impl AliasStore { pub async fn posix(&self) -> Result<String> { let aliases = self.aliases().await?; + Ok(Self::format_posix(&aliases)) + } + + pub async fn xonsh(&self) -> Result<String> { + let aliases = self.aliases().await?; + Ok(Self::format_xonsh(&aliases)) + } + pub async fn powershell(&self) -> Result<String> { + let aliases = self.aliases().await?; + Ok(Self::format_powershell(&aliases)) + } + + fn format_posix(aliases: &[Alias]) -> String { let mut config = String::new(); for alias in aliases { @@ -153,28 +166,39 @@ impl AliasStore { config.push_str(&format!("alias {}='{}'\n", alias.name, value)); } - Ok(config) + config } - pub async fn xonsh(&self) -> Result<String> { - let aliases = self.aliases().await?; - + fn format_xonsh(aliases: &[Alias]) -> String { let mut config = String::new(); for alias in aliases { config.push_str(&format!("aliases['{}'] ='{}'\n", alias.name, alias.value)); } - Ok(config) + config + } + + fn format_powershell(aliases: &[Alias]) -> String { + let mut config = String::new(); + + for alias in aliases { + config.push_str(&crate::shell::powershell::format_alias(alias)); + } + + config } pub async fn build(&self) -> Result<()> { let dir = atuin_common::utils::dotfiles_cache_dir(); tokio::fs::create_dir_all(dir.clone()).await?; + let aliases = self.aliases().await?; + // Build for all supported shells - let posix = self.posix().await?; - let xonsh = self.xonsh().await?; + let posix = Self::format_posix(&aliases); + let xonsh = Self::format_xonsh(&aliases); + let powershell = Self::format_powershell(&aliases); // All the same contents, maybe optimize in the future or perhaps there will be quirks // per-shell @@ -183,11 +207,13 @@ impl AliasStore { let bash = dir.join("aliases.bash"); let fish = dir.join("aliases.fish"); let xsh = dir.join("aliases.xsh"); + let ps1 = dir.join("aliases.ps1"); tokio::fs::write(zsh, &posix).await?; tokio::fs::write(bash, &posix).await?; tokio::fs::write(fish, &posix).await?; tokio::fs::write(xsh, &xonsh).await?; + tokio::fs::write(ps1, &powershell).await?; Ok(()) } diff --git a/crates/atuin-dotfiles/src/store/var.rs b/crates/atuin-dotfiles/src/store/var.rs index 402ba684..9d25b85d 100644 --- a/crates/atuin-dotfiles/src/store/var.rs +++ b/crates/atuin-dotfiles/src/store/var.rs @@ -169,7 +169,25 @@ impl VarStore { pub async fn xonsh(&self) -> Result<String> { let env = self.vars().await?; + Ok(Self::format_xonsh(&env)) + } + pub async fn fish(&self) -> Result<String> { + let env = self.vars().await?; + Ok(Self::format_fish(&env)) + } + + pub async fn posix(&self) -> Result<String> { + let env = self.vars().await?; + Ok(Self::format_posix(&env)) + } + + pub async fn powershell(&self) -> Result<String> { + let env = self.vars().await?; + Ok(Self::format_powershell(&env)) + } + + fn format_xonsh(env: &[Var]) -> String { let mut config = String::new(); for env in env { @@ -177,12 +195,10 @@ impl VarStore { config.push_str(&format!("${}={}\n", env.name, escaped_value)); } - Ok(config) + config } - pub async fn fish(&self) -> Result<String> { - let env = self.vars().await?; - + fn format_fish(env: &[Var]) -> String { let mut config = String::new(); for env in env { @@ -190,12 +206,10 @@ impl VarStore { config.push_str(&format!("set -gx {} {}\n", env.name, escaped_value)); } - Ok(config) + config } - pub async fn posix(&self) -> Result<String> { - let env = self.vars().await?; - + fn format_posix(env: &[Var]) -> String { let mut config = String::new(); for env in env { @@ -207,17 +221,30 @@ impl VarStore { } } - Ok(config) + config + } + + fn format_powershell(env: &[Var]) -> String { + let mut config = String::new(); + + for var in env { + config.push_str(&crate::shell::powershell::format_var(var)); + } + + config } pub async fn build(&self) -> Result<()> { let dir = atuin_common::utils::dotfiles_cache_dir(); tokio::fs::create_dir_all(dir.clone()).await?; + let env = self.vars().await?; + // Build for all supported shells - let posix = self.posix().await?; - let xonsh = self.xonsh().await?; - let fsh = self.fish().await?; + let posix = Self::format_posix(&env); + let xonsh = Self::format_xonsh(&env); + let fsh = Self::format_fish(&env); + let powershell = Self::format_powershell(&env); // All the same contents, maybe optimize in the future or perhaps there will be quirks // per-shell @@ -226,11 +253,13 @@ impl VarStore { let bash = dir.join("vars.bash"); let fish = dir.join("vars.fish"); let xsh = dir.join("vars.xsh"); + let ps1 = dir.join("vars.ps1"); tokio::fs::write(zsh, &posix).await?; tokio::fs::write(bash, &posix).await?; tokio::fs::write(fish, &fsh).await?; tokio::fs::write(xsh, &xonsh).await?; + tokio::fs::write(ps1, &powershell).await?; Ok(()) } |
