aboutsummaryrefslogtreecommitdiffstats
path: root/crates/atuin-dotfiles
diff options
context:
space:
mode:
Diffstat (limited to 'crates/atuin-dotfiles')
-rw-r--r--crates/atuin-dotfiles/Cargo.toml25
-rw-r--r--crates/atuin-dotfiles/src/lib.rs2
-rw-r--r--crates/atuin-dotfiles/src/shell.rs241
-rw-r--r--crates/atuin-dotfiles/src/shell/bash.rs68
-rw-r--r--crates/atuin-dotfiles/src/shell/fish.rs69
-rw-r--r--crates/atuin-dotfiles/src/shell/powershell.rs169
-rw-r--r--crates/atuin-dotfiles/src/shell/xonsh.rs68
-rw-r--r--crates/atuin-dotfiles/src/shell/zsh.rs68
-rw-r--r--crates/atuin-dotfiles/src/store.rs421
-rw-r--r--crates/atuin-dotfiles/src/store/alias.rs1
-rw-r--r--crates/atuin-dotfiles/src/store/var.rs542
11 files changed, 0 insertions, 1674 deletions
diff --git a/crates/atuin-dotfiles/Cargo.toml b/crates/atuin-dotfiles/Cargo.toml
deleted file mode 100644
index 183091b3..00000000
--- a/crates/atuin-dotfiles/Cargo.toml
+++ /dev/null
@@ -1,25 +0,0 @@
-[package]
-name = "atuin-dotfiles"
-description = "The dotfiles crate for Atuin"
-edition = "2024"
-version = { workspace = true }
-
-authors.workspace = true
-rust-version.workspace = true
-license.workspace = true
-homepage.workspace = true
-repository.workspace = true
-readme.workspace = true
-
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
-
-[dependencies]
-atuin-common = { path = "../atuin-common", version = "18.16.1" }
-atuin-client = { path = "../atuin-client", version = "18.16.1" }
-
-eyre = { workspace = true }
-tokio = { workspace = true }
-rmp = { version = "0.8.14" }
-rand = { workspace = true }
-serde = { workspace = true }
-crypto_secretbox = "0.1.1"
diff --git a/crates/atuin-dotfiles/src/lib.rs b/crates/atuin-dotfiles/src/lib.rs
deleted file mode 100644
index 74daf8ef..00000000
--- a/crates/atuin-dotfiles/src/lib.rs
+++ /dev/null
@@ -1,2 +0,0 @@
-pub mod shell;
-pub mod store;
diff --git a/crates/atuin-dotfiles/src/shell.rs b/crates/atuin-dotfiles/src/shell.rs
deleted file mode 100644
index 73a9ce8c..00000000
--- a/crates/atuin-dotfiles/src/shell.rs
+++ /dev/null
@@ -1,241 +0,0 @@
-use eyre::{Result, ensure, eyre};
-use rmp::{decode, encode};
-use serde::Serialize;
-
-use atuin_common::shell::{Shell, ShellError};
-
-use crate::store::AliasStore;
-
-pub mod bash;
-pub mod fish;
-pub mod powershell;
-pub mod xonsh;
-pub mod zsh;
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
-pub struct Alias {
- pub name: String,
- pub value: String,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
-pub struct Var {
- pub name: String,
- pub value: String,
-
- // False? This is a _shell var_
- // True? This is an _env var_
- pub export: bool,
-}
-
-impl Var {
- /// Serialize into the given vec
- /// This is intended to be called by the store
- pub fn serialize(&self, output: &mut Vec<u8>) -> Result<()> {
- encode::write_array_len(output, 3)?; // 3 fields
-
- encode::write_str(output, self.name.as_str())?;
- encode::write_str(output, self.value.as_str())?;
- encode::write_bool(output, self.export)?;
-
- Ok(())
- }
-
- pub fn deserialize(bytes: &mut decode::Bytes) -> Result<Self> {
- fn error_report<E: std::fmt::Debug>(err: E) -> eyre::Report {
- eyre!("{err:?}")
- }
-
- let nfields = decode::read_array_len(bytes).map_err(error_report)?;
-
- ensure!(
- nfields == 3,
- "too many entries in v0 dotfiles env create record, got {}, expected {}",
- nfields,
- 3
- );
-
- let bytes = bytes.remaining_slice();
-
- let (key, bytes) = decode::read_str_from_slice(bytes).map_err(error_report)?;
- let (value, bytes) = decode::read_str_from_slice(bytes).map_err(error_report)?;
-
- let mut bytes = decode::Bytes::new(bytes);
- let export = decode::read_bool(&mut bytes).map_err(error_report)?;
-
- ensure!(
- bytes.remaining_slice().is_empty(),
- "trailing bytes in encoded dotfiles env record, malformed"
- );
-
- Ok(Var {
- name: key.to_owned(),
- value: value.to_owned(),
- export,
- })
- }
-}
-
-pub fn parse_alias(line: &str) -> Option<Alias> {
- // consider the fact we might be importing a fish alias
- // 'alias' output
- // fish: alias foo bar
- // posix: foo=bar
-
- let is_fish = line.split(' ').next().unwrap_or("") == "alias";
-
- let parts: Vec<&str> = if is_fish {
- line.split(' ')
- .enumerate()
- .filter_map(|(n, i)| if n == 0 { None } else { Some(i) })
- .collect()
- } else {
- line.split('=').collect()
- };
-
- if parts.len() <= 1 {
- return None;
- }
-
- let mut parts = parts.iter().map(|s| s.to_string());
-
- let name = parts.next().unwrap();
-
- let remaining = if is_fish {
- parts.collect::<Vec<String>>().join(" ")
- } else {
- parts.collect::<Vec<String>>().join("=")
- };
-
- Some(Alias {
- name,
- value: remaining.trim().to_string(),
- })
-}
-
-pub fn existing_aliases(shell: Option<Shell>) -> Result<Vec<Alias>, ShellError> {
- let shell = if let Some(shell) = shell {
- shell
- } else {
- Shell::current()
- };
-
- // this only supports posix-y shells atm
- if !shell.is_posixish() {
- return Err(ShellError::NotSupported);
- }
-
- // This will return a list of aliases, each on its own line
- // They will be in the form foo=bar
- let aliases = shell.run_interactive(["alias"])?;
-
- let aliases: Vec<Alias> = aliases.lines().filter_map(parse_alias).collect();
-
- Ok(aliases)
-}
-
-/// Import aliases from the current shell
-/// This will not import aliases already in the store
-/// Returns aliases that were set
-pub async fn import_aliases(store: &AliasStore) -> Result<Vec<Alias>> {
- let shell_aliases = existing_aliases(None)?;
- let store_aliases = store.aliases().await?;
-
- let mut res = Vec::new();
-
- for alias in shell_aliases {
- // O(n), but n is small, and imports infrequent
- // can always make a map
- if store_aliases.contains(&alias) {
- continue;
- }
-
- res.push(alias.clone());
- store.set(&alias.name, &alias.value).await?;
- }
-
- Ok(res)
-}
-
-#[cfg(test)]
-mod tests {
- use crate::shell::{Alias, parse_alias};
-
- #[test]
- fn test_parse_simple_alias() {
- let alias = super::parse_alias("foo=bar").expect("failed to parse alias");
- assert_eq!(alias.name, "foo");
- assert_eq!(alias.value, "bar");
- }
-
- #[test]
- fn test_parse_quoted_alias() {
- let alias = super::parse_alias("emacs='TERM=xterm-24bits emacs -nw'")
- .expect("failed to parse alias");
-
- assert_eq!(alias.name, "emacs");
- assert_eq!(alias.value, "'TERM=xterm-24bits emacs -nw'");
-
- let git_alias = super::parse_alias("gwip='git add -A; git rm $(git ls-files --deleted) 2> /dev/null; git commit --no-verify --no-gpg-sign --message \"--wip-- [skip ci]\"'").expect("failed to parse alias");
- assert_eq!(git_alias.name, "gwip");
- assert_eq!(
- git_alias.value,
- "'git add -A; git rm $(git ls-files --deleted) 2> /dev/null; git commit --no-verify --no-gpg-sign --message \"--wip-- [skip ci]\"'"
- );
- }
-
- #[test]
- fn test_parse_quoted_alias_equals() {
- let alias = super::parse_alias("emacs='TERM=xterm-24bits emacs -nw --foo=bar'")
- .expect("failed to parse alias");
- assert_eq!(alias.name, "emacs");
- assert_eq!(alias.value, "'TERM=xterm-24bits emacs -nw --foo=bar'");
- }
-
- #[test]
- fn test_parse_fish() {
- let alias = super::parse_alias("alias foo bar").expect("failed to parse alias");
- assert_eq!(alias.name, "foo");
- assert_eq!(alias.value, "bar");
-
- let alias =
- super::parse_alias("alias x 'exa --icons --git --classify --group-directories-first'")
- .expect("failed to parse alias");
-
- assert_eq!(alias.name, "x");
- assert_eq!(
- alias.value,
- "'exa --icons --git --classify --group-directories-first'"
- );
- }
-
- #[test]
- fn test_parse_with_fortune() {
- // Because we run the alias command in an interactive subshell
- // there may be other output.
- // Ensure that the parser can handle it
- // Annoyingly not all aliases are picked up all the time if we use
- // a non-interactive subshell. Boo.
- let shell = "
-/ In a consumer society there are \\
-| inevitably two kinds of slaves: the |
-| prisoners of addiction and the |
-\\ prisoners of envy. /
- -------------------------------------
- \\ ^__^
- \\ (oo)\\_______
- (__)\\ )\\/\\
- ||----w |
- || ||
-emacs='TERM=xterm-24bits emacs -nw --foo=bar'
-k=kubectl
-";
-
- let aliases: Vec<Alias> = shell.lines().filter_map(parse_alias).collect();
- assert_eq!(aliases[0].name, "emacs");
- assert_eq!(aliases[0].value, "'TERM=xterm-24bits emacs -nw --foo=bar'");
-
- assert_eq!(aliases[1].name, "k");
- assert_eq!(aliases[1].value, "kubectl");
- }
-}
diff --git a/crates/atuin-dotfiles/src/shell/bash.rs b/crates/atuin-dotfiles/src/shell/bash.rs
deleted file mode 100644
index 2b9b4c88..00000000
--- a/crates/atuin-dotfiles/src/shell/bash.rs
+++ /dev/null
@@ -1,68 +0,0 @@
-use std::path::PathBuf;
-
-use crate::store::{AliasStore, var::VarStore};
-
-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.posix().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.posix().await.unwrap_or_else(|e| {
- format!("echo 'Atuin: failed to read and generate vars: \n{r}\n{e}'",)
- })
- }
- }
-}
-
-/// Return bash 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.bash");
-
- 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.bash");
-
- 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
-}
diff --git a/crates/atuin-dotfiles/src/shell/fish.rs b/crates/atuin-dotfiles/src/shell/fish.rs
deleted file mode 100644
index 6d472f67..00000000
--- a/crates/atuin-dotfiles/src/shell/fish.rs
+++ /dev/null
@@ -1,69 +0,0 @@
-// Configuration for fish
-use std::path::PathBuf;
-
-use crate::store::{AliasStore, var::VarStore};
-
-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.posix().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.posix().await.unwrap_or_else(|e| {
- format!("echo 'Atuin: failed to read and generate vars: \n{r}\n{e}'",)
- })
- }
- }
-}
-
-/// Return fish 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.fish");
-
- 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.fish");
-
- 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
-}
diff --git a/crates/atuin-dotfiles/src/shell/powershell.rs b/crates/atuin-dotfiles/src/shell/powershell.rs
deleted file mode 100644
index 1daee28b..00000000
--- a/crates/atuin-dotfiles/src/shell/powershell.rs
+++ /dev/null
@@ -1,169 +0,0 @@
-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/shell/xonsh.rs b/crates/atuin-dotfiles/src/shell/xonsh.rs
deleted file mode 100644
index 1e56fc1d..00000000
--- a/crates/atuin-dotfiles/src/shell/xonsh.rs
+++ /dev/null
@@ -1,68 +0,0 @@
-use std::path::PathBuf;
-
-use crate::store::{AliasStore, var::VarStore};
-
-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.xonsh().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.xonsh().await.unwrap_or_else(|e| {
- format!("echo 'Atuin: failed to read and generate vars: \n{r}\n{e}'",)
- })
- }
- }
-}
-
-/// Return xonsh 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.xsh");
-
- 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.xsh");
-
- 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
-}
diff --git a/crates/atuin-dotfiles/src/shell/zsh.rs b/crates/atuin-dotfiles/src/shell/zsh.rs
deleted file mode 100644
index 117e9403..00000000
--- a/crates/atuin-dotfiles/src/shell/zsh.rs
+++ /dev/null
@@ -1,68 +0,0 @@
-use std::path::PathBuf;
-
-use crate::store::{AliasStore, var::VarStore};
-
-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.posix().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(aliases) => aliases,
- 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.posix().await.unwrap_or_else(|e| {
- format!("echo 'Atuin: failed to read and generate aliases: \n{r}\n{e}'",)
- })
- }
- }
-}
-
-/// Return zsh 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.zsh");
-
- 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.zsh");
-
- if vars.exists() {
- return cached_vars(vars, store).await;
- }
-
- if let Err(e) = store.build().await {
- return format!("echo 'Atuin: failed to generate aliases: {e}'");
- }
-
- cached_vars(vars, store).await
-}
diff --git a/crates/atuin-dotfiles/src/store.rs b/crates/atuin-dotfiles/src/store.rs
deleted file mode 100644
index 17597065..00000000
--- a/crates/atuin-dotfiles/src/store.rs
+++ /dev/null
@@ -1,421 +0,0 @@
-use std::collections::BTreeMap;
-
-use atuin_client::record::sqlite_store::SqliteStore;
-// Sync aliases
-// This will be noticeable similar to the kv store, though I expect the two shall diverge
-// While we will support a range of shell config, I'd rather have a larger number of small records
-// + stores, rather than one mega config store.
-use atuin_common::record::{DecryptedData, Host, HostId};
-use atuin_common::utils::unquote;
-use eyre::{Result, bail, ensure, eyre};
-
-use atuin_client::record::encryption::PASETO_V4;
-use atuin_client::record::store::Store;
-
-use crate::shell::Alias;
-
-const CONFIG_SHELL_ALIAS_VERSION: &str = "v0";
-const CONFIG_SHELL_ALIAS_TAG: &str = "config-shell-alias";
-const CONFIG_SHELL_ALIAS_FIELD_MAX_LEN: usize = 20000; // 20kb max total len, way more than should be needed.
-
-mod alias;
-pub mod var;
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum AliasRecord {
- Create(Alias), // create a full record
- Delete(String), // delete by name
-}
-
-impl AliasRecord {
- pub fn serialize(&self) -> Result<DecryptedData> {
- use rmp::encode;
-
- let mut output = vec![];
-
- match self {
- AliasRecord::Create(alias) => {
- encode::write_u8(&mut output, 0)?; // create
- encode::write_array_len(&mut output, 2)?; // 2 fields
-
- encode::write_str(&mut output, alias.name.as_str())?;
- encode::write_str(&mut output, alias.value.as_str())?;
- }
- AliasRecord::Delete(name) => {
- encode::write_u8(&mut output, 1)?; // delete
- encode::write_array_len(&mut output, 1)?; // 1 field
-
- encode::write_str(&mut output, name.as_str())?;
- }
- }
-
- Ok(DecryptedData(output))
- }
-
- pub fn deserialize(data: &DecryptedData, version: &str) -> Result<Self> {
- use rmp::decode;
-
- fn error_report<E: std::fmt::Debug>(err: E) -> eyre::Report {
- eyre!("{err:?}")
- }
-
- match version {
- CONFIG_SHELL_ALIAS_VERSION => {
- let mut bytes = decode::Bytes::new(&data.0);
-
- let record_type = decode::read_u8(&mut bytes).map_err(error_report)?;
-
- match record_type {
- // create
- 0 => {
- let nfields = decode::read_array_len(&mut bytes).map_err(error_report)?;
- ensure!(
- nfields == 2,
- "too many entries in v0 shell alias create record"
- );
-
- let bytes = bytes.remaining_slice();
-
- let (key, bytes) =
- decode::read_str_from_slice(bytes).map_err(error_report)?;
- let (value, bytes) =
- decode::read_str_from_slice(bytes).map_err(error_report)?;
-
- if !bytes.is_empty() {
- bail!("trailing bytes in encoded shell alias record. malformed")
- }
-
- Ok(AliasRecord::Create(Alias {
- name: key.to_owned(),
- value: value.to_owned(),
- }))
- }
-
- // delete
- 1 => {
- let nfields = decode::read_array_len(&mut bytes).map_err(error_report)?;
- ensure!(
- nfields == 1,
- "too many entries in v0 shell alias delete record"
- );
-
- let bytes = bytes.remaining_slice();
-
- let (key, bytes) =
- decode::read_str_from_slice(bytes).map_err(error_report)?;
-
- if !bytes.is_empty() {
- bail!("trailing bytes in encoded shell alias record. malformed")
- }
-
- Ok(AliasRecord::Delete(key.to_owned()))
- }
-
- n => {
- bail!("unknown AliasRecord type {n}")
- }
- }
- }
- _ => {
- bail!("unknown version {version:?}")
- }
- }
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct AliasStore {
- pub store: SqliteStore,
- pub host_id: HostId,
- pub encryption_key: [u8; 32],
-}
-
-impl AliasStore {
- // will want to init the actual kv store when that is done
- pub fn new(store: SqliteStore, host_id: HostId, encryption_key: [u8; 32]) -> AliasStore {
- AliasStore {
- store,
- host_id,
- encryption_key,
- }
- }
-
- 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 {
- // If it's quoted, remove the quotes. If it's not quoted, do nothing.
- let value = unquote(alias.value.as_str()).unwrap_or(alias.value.clone());
-
- // we're about to quote it ourselves anyway!
- config.push_str(&format!("alias {}='{}'\n", alias.name, value));
- }
-
- config
- }
-
- 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));
- }
-
- 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::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
- // I'd prefer separation atm
- let zsh = dir.join("aliases.zsh");
- 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(())
- }
-
- pub async fn set(&self, name: &str, value: &str) -> Result<()> {
- if name.len() + value.len() > CONFIG_SHELL_ALIAS_FIELD_MAX_LEN {
- return Err(eyre!(
- "alias record too large: max len {} bytes",
- CONFIG_SHELL_ALIAS_FIELD_MAX_LEN
- ));
- }
-
- let record = AliasRecord::Create(Alias {
- name: name.to_string(),
- value: value.to_string(),
- });
-
- let bytes = record.serialize()?;
-
- let idx = self
- .store
- .last(self.host_id, CONFIG_SHELL_ALIAS_TAG)
- .await?
- .map_or(0, |entry| entry.idx + 1);
-
- let record = atuin_common::record::Record::builder()
- .host(Host::new(self.host_id))
- .version(CONFIG_SHELL_ALIAS_VERSION.to_string())
- .tag(CONFIG_SHELL_ALIAS_TAG.to_string())
- .idx(idx)
- .data(bytes)
- .build();
-
- self.store
- .push(&record.encrypt::<PASETO_V4>(&self.encryption_key))
- .await?;
-
- // set mutates shell config, so build again
- self.build().await?;
-
- Ok(())
- }
-
- pub async fn delete(&self, name: &str) -> Result<()> {
- if name.len() > CONFIG_SHELL_ALIAS_FIELD_MAX_LEN {
- return Err(eyre!(
- "alias record too large: max len {} bytes",
- CONFIG_SHELL_ALIAS_FIELD_MAX_LEN
- ));
- }
-
- let record = AliasRecord::Delete(name.to_string());
-
- let bytes = record.serialize()?;
-
- let idx = self
- .store
- .last(self.host_id, CONFIG_SHELL_ALIAS_TAG)
- .await?
- .map_or(0, |entry| entry.idx + 1);
-
- let record = atuin_common::record::Record::builder()
- .host(Host::new(self.host_id))
- .version(CONFIG_SHELL_ALIAS_VERSION.to_string())
- .tag(CONFIG_SHELL_ALIAS_TAG.to_string())
- .idx(idx)
- .data(bytes)
- .build();
-
- self.store
- .push(&record.encrypt::<PASETO_V4>(&self.encryption_key))
- .await?;
-
- // delete mutates shell config, so build again
- self.build().await?;
-
- Ok(())
- }
-
- pub async fn aliases(&self) -> Result<Vec<Alias>> {
- let mut build = BTreeMap::new();
-
- // this is sorted, oldest to newest
- let tagged = self.store.all_tagged(CONFIG_SHELL_ALIAS_TAG).await?;
-
- for record in tagged {
- let version = record.version.clone();
-
- let decrypted = match version.as_str() {
- CONFIG_SHELL_ALIAS_VERSION => record.decrypt::<PASETO_V4>(&self.encryption_key)?,
- version => bail!("unknown version {version:?}"),
- };
-
- let ar = AliasRecord::deserialize(&decrypted.data, version.as_str())?;
-
- match ar {
- AliasRecord::Create(a) => {
- build.insert(a.name.clone(), a);
- }
- AliasRecord::Delete(d) => {
- build.remove(&d);
- }
- }
- }
-
- Ok(build.into_values().collect())
- }
-}
-
-#[cfg(test)]
-pub(crate) fn test_local_timeout() -> f64 {
- std::env::var("ATUIN_TEST_LOCAL_TIMEOUT")
- .ok()
- .and_then(|x| x.parse().ok())
- // this hardcoded value should be replaced by a simple way to get the
- // default local_timeout of Settings if possible
- .unwrap_or(2.0)
-}
-
-#[cfg(test)]
-mod tests {
- use rand::rngs::OsRng;
-
- use atuin_client::record::sqlite_store::SqliteStore;
-
- use crate::shell::Alias;
-
- use super::{AliasRecord, AliasStore, CONFIG_SHELL_ALIAS_VERSION, test_local_timeout};
- use crypto_secretbox::{KeyInit, XSalsa20Poly1305};
-
- #[test]
- fn encode_decode() {
- let record = Alias {
- name: "k".to_owned(),
- value: "kubectl".to_owned(),
- };
- let record = AliasRecord::Create(record);
-
- let snapshot = [204, 0, 146, 161, 107, 167, 107, 117, 98, 101, 99, 116, 108];
-
- let encoded = record.serialize().unwrap();
- let decoded = AliasRecord::deserialize(&encoded, CONFIG_SHELL_ALIAS_VERSION).unwrap();
-
- assert_eq!(encoded.0, &snapshot);
- assert_eq!(decoded, record);
- }
-
- #[tokio::test]
- async fn build_aliases() {
- let store = SqliteStore::new(":memory:", test_local_timeout())
- .await
- .unwrap();
- let key: [u8; 32] = XSalsa20Poly1305::generate_key(&mut OsRng).into();
- let host_id = atuin_common::record::HostId(atuin_common::utils::uuid_v7());
-
- let alias = AliasStore::new(store, host_id, key);
-
- alias.set("k", "kubectl").await.unwrap();
- alias.set("gp", "git push").await.unwrap();
- alias
- .set("kgap", "'kubectl get pods --all-namespaces'")
- .await
- .unwrap();
-
- let mut aliases = alias.aliases().await.unwrap();
-
- aliases.sort_by_key(|a| a.name.clone());
-
- assert_eq!(aliases.len(), 3);
-
- assert_eq!(
- aliases[0],
- Alias {
- name: String::from("gp"),
- value: String::from("git push")
- }
- );
-
- assert_eq!(
- aliases[1],
- Alias {
- name: String::from("k"),
- value: String::from("kubectl")
- }
- );
-
- assert_eq!(
- aliases[2],
- Alias {
- name: String::from("kgap"),
- value: String::from("'kubectl get pods --all-namespaces'")
- }
- );
-
- let build = alias.posix().await.expect("failed to build aliases");
-
- assert_eq!(
- build,
- "alias gp='git push'
-alias k='kubectl'
-alias kgap='kubectl get pods --all-namespaces'
-"
- )
- }
-}
diff --git a/crates/atuin-dotfiles/src/store/alias.rs b/crates/atuin-dotfiles/src/store/alias.rs
deleted file mode 100644
index 8b137891..00000000
--- a/crates/atuin-dotfiles/src/store/alias.rs
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/crates/atuin-dotfiles/src/store/var.rs b/crates/atuin-dotfiles/src/store/var.rs
deleted file mode 100644
index 9d25b85d..00000000
--- a/crates/atuin-dotfiles/src/store/var.rs
+++ /dev/null
@@ -1,542 +0,0 @@
-/// Store for shell vars
-/// I should abstract this and reuse code between the alias/env stores
-/// This is easier for now
-/// Once I have two implementations, building a common base is much easier.
-use std::collections::BTreeMap;
-
-use atuin_client::record::sqlite_store::SqliteStore;
-use atuin_common::record::{DecryptedData, Host, HostId};
-use eyre::{Result, bail, ensure, eyre};
-
-use atuin_client::record::encryption::PASETO_V4;
-use atuin_client::record::store::Store;
-
-use crate::shell::Var;
-
-const DOTFILES_VAR_VERSION: &str = "v0";
-const DOTFILES_VAR_TAG: &str = "dotfiles-var";
-const DOTFILES_VAR_LEN: usize = 20000; // 20kb max total len, way more than should be needed.
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum VarRecord {
- Create(Var), // create a full record
- Delete(String), // delete by name
-}
-
-impl VarRecord {
- pub fn serialize(&self) -> Result<DecryptedData> {
- use rmp::encode;
-
- let mut output = vec![];
-
- match self {
- VarRecord::Create(env) => {
- encode::write_u8(&mut output, 0)?; // create
-
- env.serialize(&mut output)?;
- }
- VarRecord::Delete(env) => {
- encode::write_u8(&mut output, 1)?; // delete
- encode::write_array_len(&mut output, 1)?; // 1 field
-
- encode::write_str(&mut output, env.as_str())?;
- }
- }
-
- Ok(DecryptedData(output))
- }
-
- pub fn deserialize(data: &DecryptedData, version: &str) -> Result<Self> {
- use rmp::decode;
-
- fn error_report<E: std::fmt::Debug>(err: E) -> eyre::Report {
- eyre!("{err:?}")
- }
-
- match version {
- DOTFILES_VAR_VERSION => {
- let mut bytes = decode::Bytes::new(&data.0);
-
- let record_type = decode::read_u8(&mut bytes).map_err(error_report)?;
-
- match record_type {
- // create
- 0 => {
- let env = Var::deserialize(&mut bytes)?;
- Ok(VarRecord::Create(env))
- }
-
- // delete
- 1 => {
- let nfields = decode::read_array_len(&mut bytes).map_err(error_report)?;
- ensure!(
- nfields == 1,
- "too many entries in v0 dotfiles var delete record"
- );
-
- let bytes = bytes.remaining_slice();
-
- let (key, bytes) =
- decode::read_str_from_slice(bytes).map_err(error_report)?;
-
- if !bytes.is_empty() {
- bail!("trailing bytes in encoded dotfiles var record. malformed")
- }
-
- Ok(VarRecord::Delete(key.to_owned()))
- }
-
- n => {
- bail!("unknown Dotfiles var record type {n}")
- }
- }
- }
- _ => {
- bail!("unknown version {version:?}")
- }
- }
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct VarStore {
- pub store: SqliteStore,
- pub host_id: HostId,
- pub encryption_key: [u8; 32],
-}
-
-impl VarStore {
- // will want to init the actual kv store when that is done
- pub fn new(store: SqliteStore, host_id: HostId, encryption_key: [u8; 32]) -> VarStore {
- VarStore {
- store,
- host_id,
- encryption_key,
- }
- }
-
- /// Escape a value for use in POSIX shells (bash, zsh)
- /// This adds double quotes around the value and escapes any embedded double quotes
- fn escape_posix_value(value: &str) -> String {
- // If the value contains no special characters, we can use it unquoted
- if value
- .chars()
- .all(|c| c.is_alphanumeric() || c == '_' || c == '-' || c == '/' || c == '.')
- {
- value.to_string()
- } else {
- // Otherwise, wrap in double quotes and escape any special characters
- format!(
- "\"{}\"",
- value
- .replace('\\', "\\\\")
- .replace('"', "\\\"")
- .replace('$', "\\$")
- .replace('`', "\\`")
- )
- }
- }
-
- /// Escape a value for use in fish shell
- /// Fish uses single quotes for literal strings, but we need to handle embedded single quotes
- fn escape_fish_value(value: &str) -> String {
- // If the value contains no special characters, we can use it unquoted
- if value
- .chars()
- .all(|c| c.is_alphanumeric() || c == '_' || c == '-' || c == '/' || c == '.')
- {
- value.to_string()
- } else {
- // Use single quotes and escape any embedded single quotes
- format!("'{}'", value.replace('\'', "\\'"))
- }
- }
-
- /// Escape a value for use in xonsh
- /// Xonsh uses Python-style string literals
- fn escape_xonsh_value(value: &str) -> String {
- // If the value contains no special characters, we can use it unquoted
- if value
- .chars()
- .all(|c| c.is_alphanumeric() || c == '_' || c == '-' || c == '/' || c == '.')
- {
- value.to_string()
- } else {
- // Use double quotes and escape appropriately for Python strings
- format!("\"{}\"", value.replace('\\', "\\\\").replace('"', "\\\""))
- }
- }
-
- 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 {
- let escaped_value = Self::escape_xonsh_value(&env.value);
- config.push_str(&format!("${}={}\n", env.name, escaped_value));
- }
-
- config
- }
-
- fn format_fish(env: &[Var]) -> String {
- let mut config = String::new();
-
- for env in env {
- let escaped_value = Self::escape_fish_value(&env.value);
- config.push_str(&format!("set -gx {} {}\n", env.name, escaped_value));
- }
-
- config
- }
-
- fn format_posix(env: &[Var]) -> String {
- let mut config = String::new();
-
- for env in env {
- let escaped_value = Self::escape_posix_value(&env.value);
- if env.export {
- config.push_str(&format!("export {}={}\n", env.name, escaped_value));
- } else {
- config.push_str(&format!("{}={}\n", env.name, escaped_value));
- }
- }
-
- 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::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
- // I'd prefer separation atm
- let zsh = dir.join("vars.zsh");
- 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(())
- }
-
- pub async fn set(&self, name: &str, value: &str, export: bool) -> Result<()> {
- if name.len() + value.len() > DOTFILES_VAR_LEN {
- return Err(eyre!(
- "var record too large: max len {} bytes",
- DOTFILES_VAR_LEN
- ));
- }
-
- let record = VarRecord::Create(Var {
- name: name.to_string(),
- value: value.to_string(),
- export,
- });
-
- let bytes = record.serialize()?;
-
- let idx = self
- .store
- .last(self.host_id, DOTFILES_VAR_TAG)
- .await?
- .map_or(0, |entry| entry.idx + 1);
-
- let record = atuin_common::record::Record::builder()
- .host(Host::new(self.host_id))
- .version(DOTFILES_VAR_VERSION.to_string())
- .tag(DOTFILES_VAR_TAG.to_string())
- .idx(idx)
- .data(bytes)
- .build();
-
- self.store
- .push(&record.encrypt::<PASETO_V4>(&self.encryption_key))
- .await?;
-
- // set mutates shell config, so build again
- self.build().await?;
-
- Ok(())
- }
-
- pub async fn delete(&self, name: &str) -> Result<()> {
- if name.len() > DOTFILES_VAR_LEN {
- return Err(eyre!(
- "var record too large: max len {} bytes",
- DOTFILES_VAR_LEN,
- ));
- }
-
- let record = VarRecord::Delete(name.to_string());
-
- let bytes = record.serialize()?;
-
- let idx = self
- .store
- .last(self.host_id, DOTFILES_VAR_TAG)
- .await?
- .map_or(0, |entry| entry.idx + 1);
-
- let record = atuin_common::record::Record::builder()
- .host(Host::new(self.host_id))
- .version(DOTFILES_VAR_VERSION.to_string())
- .tag(DOTFILES_VAR_TAG.to_string())
- .idx(idx)
- .data(bytes)
- .build();
-
- self.store
- .push(&record.encrypt::<PASETO_V4>(&self.encryption_key))
- .await?;
-
- // delete mutates shell config, so build again
- self.build().await?;
-
- Ok(())
- }
-
- pub async fn vars(&self) -> Result<Vec<Var>> {
- let mut build = BTreeMap::new();
-
- // this is sorted, oldest to newest
- let tagged = self.store.all_tagged(DOTFILES_VAR_TAG).await?;
-
- for record in tagged {
- let version = record.version.clone();
-
- let decrypted = match version.as_str() {
- DOTFILES_VAR_VERSION => record.decrypt::<PASETO_V4>(&self.encryption_key)?,
- version => bail!("unknown version {version:?}"),
- };
-
- let ar = VarRecord::deserialize(&decrypted.data, version.as_str())?;
-
- match ar {
- VarRecord::Create(a) => {
- build.insert(a.name.clone(), a);
- }
- VarRecord::Delete(d) => {
- build.remove(&d);
- }
- }
- }
-
- Ok(build.into_values().collect())
- }
-}
-
-#[cfg(test)]
-mod tests {
- use rand::rngs::OsRng;
-
- use atuin_client::record::sqlite_store::SqliteStore;
-
- use crate::{shell::Var, store::test_local_timeout};
-
- use super::{DOTFILES_VAR_VERSION, VarRecord, VarStore};
- use crypto_secretbox::{KeyInit, XSalsa20Poly1305};
-
- #[test]
- fn encode_decode() {
- let record = Var {
- name: "BEEP".to_owned(),
- value: "boop".to_owned(),
- export: false,
- };
- let record = VarRecord::Create(record);
-
- let snapshot = [
- 204, 0, 147, 164, 66, 69, 69, 80, 164, 98, 111, 111, 112, 194,
- ];
-
- let encoded = record.serialize().unwrap();
- let decoded = VarRecord::deserialize(&encoded, DOTFILES_VAR_VERSION).unwrap();
-
- assert_eq!(encoded.0, &snapshot);
- assert_eq!(decoded, record);
- }
-
- #[test]
- fn test_escape_posix_value() {
- // Simple values should not be quoted
- assert_eq!(VarStore::escape_posix_value("simple"), "simple");
- assert_eq!(VarStore::escape_posix_value("path/to/file"), "path/to/file");
- assert_eq!(
- VarStore::escape_posix_value("value_with_underscores"),
- "value_with_underscores"
- );
-
- // Values with spaces should be quoted
- assert_eq!(
- VarStore::escape_posix_value("hello world"),
- "\"hello world\""
- );
- assert_eq!(VarStore::escape_posix_value("bar baz"), "\"bar baz\"");
-
- // Values with special characters should be quoted and escaped
- assert_eq!(
- VarStore::escape_posix_value("say \"hello\""),
- "\"say \\\"hello\\\"\""
- );
- assert_eq!(
- VarStore::escape_posix_value("path\\with\\backslashes"),
- "\"path\\\\with\\\\backslashes\""
- );
- assert_eq!(
- VarStore::escape_posix_value("say $hello"),
- "\"say \\$hello\""
- );
- assert_eq!(
- VarStore::escape_posix_value("see `example.md`"),
- "\"see \\`example.md\\`\""
- );
- }
-
- #[test]
- fn test_escape_fish_value() {
- // Simple values should not be quoted
- assert_eq!(VarStore::escape_fish_value("simple"), "simple");
- assert_eq!(VarStore::escape_fish_value("path/to/file"), "path/to/file");
-
- // Values with spaces should be single-quoted
- assert_eq!(VarStore::escape_fish_value("hello world"), "'hello world'");
- assert_eq!(VarStore::escape_fish_value("bar baz"), "'bar baz'");
-
- // Values with single quotes should be escaped
- assert_eq!(VarStore::escape_fish_value("don't"), "'don\\'t'");
- }
-
- #[test]
- fn test_escape_xonsh_value() {
- // Simple values should not be quoted
- assert_eq!(VarStore::escape_xonsh_value("simple"), "simple");
- assert_eq!(VarStore::escape_xonsh_value("path/to/file"), "path/to/file");
-
- // Values with spaces should be quoted
- assert_eq!(
- VarStore::escape_xonsh_value("hello world"),
- "\"hello world\""
- );
- assert_eq!(VarStore::escape_xonsh_value("bar baz"), "\"bar baz\"");
-
- // Values with special characters should be quoted and escaped
- assert_eq!(
- VarStore::escape_xonsh_value("say \"hello\""),
- "\"say \\\"hello\\\"\""
- );
- assert_eq!(
- VarStore::escape_xonsh_value("path\\with\\backslashes"),
- "\"path\\\\with\\\\backslashes\""
- );
- }
-
- #[tokio::test]
- async fn build_vars() {
- let store = SqliteStore::new(":memory:", test_local_timeout())
- .await
- .unwrap();
- let key: [u8; 32] = XSalsa20Poly1305::generate_key(&mut OsRng).into();
- let host_id = atuin_common::record::HostId(atuin_common::utils::uuid_v7());
-
- let env = VarStore::new(store, host_id, key);
-
- env.set("BEEP", "boop", false).await.unwrap();
- env.set("HOMEBREW_NO_AUTO_UPDATE", "1", true).await.unwrap();
-
- let mut env_vars = env.vars().await.unwrap();
-
- env_vars.sort_by_key(|a| a.name.clone());
-
- assert_eq!(env_vars.len(), 2);
-
- assert_eq!(
- env_vars[0],
- Var {
- name: String::from("BEEP"),
- value: String::from("boop"),
- export: false,
- }
- );
-
- assert_eq!(
- env_vars[1],
- Var {
- name: String::from("HOMEBREW_NO_AUTO_UPDATE"),
- value: String::from("1"),
- export: true,
- }
- );
- }
-
- #[tokio::test]
- async fn test_var_generation_with_spaces() {
- let store = SqliteStore::new(":memory:", test_local_timeout())
- .await
- .unwrap();
- let key: [u8; 32] = XSalsa20Poly1305::generate_key(&mut OsRng).into();
- let host_id = atuin_common::record::HostId(atuin_common::utils::uuid_v7());
-
- let env = VarStore::new(store, host_id, key);
-
- // Test the exact scenario from the bug report
- env.set("FOO", "bar baz", true).await.unwrap();
-
- let posix_output = env.posix().await.unwrap();
- let fish_output = env.fish().await.unwrap();
- let xonsh_output = env.xonsh().await.unwrap();
-
- // POSIX should quote the value
- assert_eq!(posix_output, "export FOO=\"bar baz\"\n");
-
- // Fish should quote the value
- assert_eq!(fish_output, "set -gx FOO 'bar baz'\n");
-
- // Xonsh should quote the value
- assert_eq!(xonsh_output, "$FOO=\"bar baz\"\n");
- }
-}