aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--crates/atuin-dotfiles/src/shell.rs65
-rw-r--r--crates/atuin-dotfiles/src/shell/bash.rs33
-rw-r--r--crates/atuin-dotfiles/src/shell/fish.rs33
-rw-r--r--crates/atuin-dotfiles/src/shell/xonsh.rs33
-rw-r--r--crates/atuin-dotfiles/src/shell/zsh.rs33
-rw-r--r--crates/atuin-dotfiles/src/store.rs3
-rw-r--r--crates/atuin-dotfiles/src/store/alias.rs1
-rw-r--r--crates/atuin-dotfiles/src/store/var.rs365
-rw-r--r--crates/atuin/src/command/client/dotfiles.rs6
-rw-r--r--crates/atuin/src/command/client/dotfiles/var.rs101
-rw-r--r--crates/atuin/src/command/client/init.rs37
-rw-r--r--crates/atuin/src/command/client/init/bash.rs13
-rw-r--r--crates/atuin/src/command/client/init/fish.rs13
-rw-r--r--crates/atuin/src/command/client/init/xonsh.rs13
-rw-r--r--crates/atuin/src/command/client/init/zsh.rs13
-rw-r--r--crates/atuin/src/command/client/store/rebuild.rs7
-rw-r--r--crates/atuin/src/sync.rs5
17 files changed, 742 insertions, 32 deletions
diff --git a/crates/atuin-dotfiles/src/shell.rs b/crates/atuin-dotfiles/src/shell.rs
index a5cb0b7a..d4cacf8f 100644
--- a/crates/atuin-dotfiles/src/shell.rs
+++ b/crates/atuin-dotfiles/src/shell.rs
@@ -1,4 +1,5 @@
-use eyre::Result;
+use eyre::{ensure, eyre, Result};
+use rmp::{decode, encode};
use serde::Serialize;
use atuin_common::shell::{Shell, ShellError};
@@ -16,6 +17,64 @@ pub struct Alias {
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
@@ -158,14 +217,14 @@ mod tests {
| 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
+k=kubectl
";
let aliases: Vec<Alias> = shell.lines().filter_map(parse_alias).collect();
diff --git a/crates/atuin-dotfiles/src/shell/bash.rs b/crates/atuin-dotfiles/src/shell/bash.rs
index 5bdd7dce..b4c87336 100644
--- a/crates/atuin-dotfiles/src/shell/bash.rs
+++ b/crates/atuin-dotfiles/src/shell/bash.rs
@@ -1,6 +1,6 @@
use std::path::PathBuf;
-use crate::store::AliasStore;
+use crate::store::{var::VarStore, AliasStore};
async fn cached_aliases(path: PathBuf, store: &AliasStore) -> String {
match tokio::fs::read_to_string(path).await {
@@ -16,6 +16,20 @@ async fn cached_aliases(path: PathBuf, store: &AliasStore) -> String {
}
}
+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.
@@ -23,7 +37,7 @@ async fn cached_aliases(path: PathBuf, store: &AliasStore) -> String {
/// 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 config(store: &AliasStore) -> String {
+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");
@@ -37,3 +51,18 @@ pub async fn config(store: &AliasStore) -> String {
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
index bf4e1a3b..fc1aeee5 100644
--- a/crates/atuin-dotfiles/src/shell/fish.rs
+++ b/crates/atuin-dotfiles/src/shell/fish.rs
@@ -1,7 +1,7 @@
// Configuration for fish
use std::path::PathBuf;
-use crate::store::AliasStore;
+use crate::store::{var::VarStore, AliasStore};
async fn cached_aliases(path: PathBuf, store: &AliasStore) -> String {
match tokio::fs::read_to_string(path).await {
@@ -17,6 +17,20 @@ async fn cached_aliases(path: PathBuf, store: &AliasStore) -> String {
}
}
+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.
@@ -24,7 +38,7 @@ async fn cached_aliases(path: PathBuf, store: &AliasStore) -> String {
/// 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 config(store: &AliasStore) -> String {
+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");
@@ -38,3 +52,18 @@ pub async fn config(store: &AliasStore) -> String {
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/xonsh.rs b/crates/atuin-dotfiles/src/shell/xonsh.rs
index 383df4ec..a416ccb2 100644
--- a/crates/atuin-dotfiles/src/shell/xonsh.rs
+++ b/crates/atuin-dotfiles/src/shell/xonsh.rs
@@ -1,6 +1,6 @@
use std::path::PathBuf;
-use crate::store::AliasStore;
+use crate::store::{var::VarStore, AliasStore};
async fn cached_aliases(path: PathBuf, store: &AliasStore) -> String {
match tokio::fs::read_to_string(path).await {
@@ -16,6 +16,20 @@ async fn cached_aliases(path: PathBuf, store: &AliasStore) -> String {
}
}
+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.
@@ -23,7 +37,7 @@ async fn cached_aliases(path: PathBuf, store: &AliasStore) -> String {
/// 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 config(store: &AliasStore) -> String {
+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");
@@ -37,3 +51,18 @@ pub async fn config(store: &AliasStore) -> String {
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
index d863b261..efb83897 100644
--- a/crates/atuin-dotfiles/src/shell/zsh.rs
+++ b/crates/atuin-dotfiles/src/shell/zsh.rs
@@ -1,6 +1,6 @@
use std::path::PathBuf;
-use crate::store::AliasStore;
+use crate::store::{var::VarStore, AliasStore};
async fn cached_aliases(path: PathBuf, store: &AliasStore) -> String {
match tokio::fs::read_to_string(path).await {
@@ -16,6 +16,20 @@ async fn cached_aliases(path: PathBuf, store: &AliasStore) -> String {
}
}
+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.
@@ -23,7 +37,7 @@ async fn cached_aliases(path: PathBuf, store: &AliasStore) -> String {
/// 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 config(store: &AliasStore) -> String {
+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");
@@ -37,3 +51,18 @@ pub async fn config(store: &AliasStore) -> String {
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
index b7984c1c..f1789e2b 100644
--- a/crates/atuin-dotfiles/src/store.rs
+++ b/crates/atuin-dotfiles/src/store.rs
@@ -18,6 +18,9 @@ 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
diff --git a/crates/atuin-dotfiles/src/store/alias.rs b/crates/atuin-dotfiles/src/store/alias.rs
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/crates/atuin-dotfiles/src/store/alias.rs
@@ -0,0 +1 @@
+
diff --git a/crates/atuin-dotfiles/src/store/var.rs b/crates/atuin-dotfiles/src/store/var.rs
new file mode 100644
index 00000000..2d366f7e
--- /dev/null
+++ b/crates/atuin-dotfiles/src/store/var.rs
@@ -0,0 +1,365 @@
+/// 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::{bail, ensure, eyre, Result};
+
+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,
+ }
+ }
+
+ pub async fn xonsh(&self) -> Result<String> {
+ let env = self.vars().await?;
+
+ let mut config = String::new();
+
+ for env in env {
+ config.push_str(&format!("${}={}\n", env.name, env.value));
+ }
+
+ Ok(config)
+ }
+
+ pub async fn fish(&self) -> Result<String> {
+ let env = self.vars().await?;
+
+ let mut config = String::new();
+
+ for env in env {
+ config.push_str(&format!("set -gx {} {}\n", env.name, env.value));
+ }
+
+ Ok(config)
+ }
+
+ pub async fn posix(&self) -> Result<String> {
+ let env = self.vars().await?;
+
+ let mut config = String::new();
+
+ for env in env {
+ if env.export {
+ config.push_str(&format!("export {}={}\n", env.name, env.value));
+ } else {
+ config.push_str(&format!("{}={}\n", env.name, env.value));
+ }
+ }
+
+ Ok(config)
+ }
+
+ pub async fn build(&self) -> Result<()> {
+ let dir = atuin_common::utils::dotfiles_cache_dir();
+ tokio::fs::create_dir_all(dir.clone()).await?;
+
+ // Build for all supported shells
+ let posix = self.posix().await?;
+ let xonsh = self.xonsh().await?;
+ let fsh = self.fish().await?;
+
+ // 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");
+
+ tokio::fs::write(zsh, &posix).await?;
+ tokio::fs::write(bash, &posix).await?;
+ tokio::fs::write(fish, &fsh).await?;
+ tokio::fs::write(xsh, &xonsh).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)]
+pub(crate) fn test_sqlite_store_timeout() -> f64 {
+ std::env::var("ATUIN_TEST_SQLITE_STORE_TIMEOUT")
+ .ok()
+ .and_then(|x| x.parse().ok())
+ .unwrap_or(0.1)
+}
+
+#[cfg(test)]
+mod tests {
+ use rand::rngs::OsRng;
+
+ use atuin_client::record::sqlite_store::SqliteStore;
+
+ use crate::shell::Var;
+
+ use super::{test_sqlite_store_timeout, VarRecord, VarStore, DOTFILES_VAR_VERSION};
+ 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);
+ }
+
+ #[tokio::test]
+ async fn build_vars() {
+ let store = SqliteStore::new(":memory:", test_sqlite_store_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,
+ }
+ );
+ }
+}
diff --git a/crates/atuin/src/command/client/dotfiles.rs b/crates/atuin/src/command/client/dotfiles.rs
index 291c794d..f42b18f2 100644
--- a/crates/atuin/src/command/client/dotfiles.rs
+++ b/crates/atuin/src/command/client/dotfiles.rs
@@ -4,6 +4,7 @@ use eyre::Result;
use atuin_client::{record::sqlite_store::SqliteStore, settings::Settings};
mod alias;
+mod var;
#[derive(Subcommand, Debug)]
#[command(infer_subcommands = true)]
@@ -11,12 +12,17 @@ pub enum Cmd {
/// Manage shell aliases with Atuin
#[command(subcommand)]
Alias(alias::Cmd),
+
+ /// Manage shell and environment variables with Atuin
+ #[command(subcommand)]
+ Var(var::Cmd),
}
impl Cmd {
pub async fn run(self, settings: &Settings, store: SqliteStore) -> Result<()> {
match self {
Self::Alias(cmd) => cmd.run(settings, store).await,
+ Self::Var(cmd) => cmd.run(settings, store).await,
}
}
}
diff --git a/crates/atuin/src/command/client/dotfiles/var.rs b/crates/atuin/src/command/client/dotfiles/var.rs
new file mode 100644
index 00000000..4329179b
--- /dev/null
+++ b/crates/atuin/src/command/client/dotfiles/var.rs
@@ -0,0 +1,101 @@
+use clap::Subcommand;
+use eyre::{Context, Result};
+
+use atuin_client::{encryption, record::sqlite_store::SqliteStore, settings::Settings};
+
+use atuin_dotfiles::{shell::Var, store::var::VarStore};
+
+#[derive(Subcommand, Debug)]
+#[command(infer_subcommands = true)]
+pub enum Cmd {
+ /// Set a variable
+ Set {
+ name: String,
+ value: String,
+
+ #[clap(long, short, action)]
+ no_export: bool,
+ },
+
+ /// Delete a variable
+ Delete { name: String },
+
+ /// List all variables
+ List,
+}
+
+impl Cmd {
+ async fn set(&self, store: VarStore, name: String, value: String, export: bool) -> Result<()> {
+ let vars = store.vars().await?;
+ let found: Vec<Var> = vars.into_iter().filter(|a| a.name == name).collect();
+ let show_export = if export { "export " } else { "" };
+
+ if found.is_empty() {
+ println!("Setting '{show_export}{name}={value}'.");
+ } else {
+ println!(
+ "Overwriting alias '{show_export}{name}={}' with '{name}={value}'.",
+ found[0].value
+ );
+ }
+
+ store.set(&name, &value, export).await?;
+
+ Ok(())
+ }
+
+ async fn list(&self, store: VarStore) -> Result<()> {
+ let vars = store.vars().await?;
+
+ for i in vars.iter().filter(|v| !v.export) {
+ println!("{}={}", i.name, i.value);
+ }
+
+ for i in vars.iter().filter(|v| v.export) {
+ println!("export {}={}", i.name, i.value);
+ }
+
+ Ok(())
+ }
+
+ async fn delete(&self, store: VarStore, name: String) -> Result<()> {
+ let mut vars = store.vars().await?.into_iter();
+
+ if let Some(var) = vars.find(|var| var.name == name) {
+ println!("Deleting '{name}={}'.", var.value);
+ store.delete(&name).await?;
+ } else {
+ eprintln!("Cannot delete '{name}': Var not set.");
+ };
+
+ Ok(())
+ }
+
+ pub async fn run(&self, settings: &Settings, store: SqliteStore) -> Result<()> {
+ if !settings.dotfiles.enabled {
+ eprintln!("Dotfiles are not enabled. Add\n\n[dotfiles]\nenabled = true\n\nto your configuration file to enable them.\n");
+ eprintln!("The default configuration file is located at ~/.config/atuin/config.toml.");
+ return Ok(());
+ }
+
+ let encryption_key: [u8; 32] = encryption::load_key(settings)
+ .context("could not load encryption key")?
+ .into();
+ let host_id = Settings::host_id().expect("failed to get host_id");
+
+ let var_store = VarStore::new(store, host_id, encryption_key);
+
+ match self {
+ Self::Set {
+ name,
+ value,
+ no_export,
+ } => {
+ self.set(var_store, name.clone(), value.clone(), !no_export)
+ .await
+ }
+ Self::Delete { name } => self.delete(var_store, name.clone()).await,
+ Self::List => self.list(var_store).await,
+ }
+ }
+}
diff --git a/crates/atuin/src/command/client/init.rs b/crates/atuin/src/command/client/init.rs
index bfda75be..8238a69b 100644
--- a/crates/atuin/src/command/client/init.rs
+++ b/crates/atuin/src/command/client/init.rs
@@ -1,7 +1,7 @@
use std::path::PathBuf;
use atuin_client::{encryption, record::sqlite_store::SqliteStore, settings::Settings};
-use atuin_dotfiles::store::AliasStore;
+use atuin_dotfiles::store::{var::VarStore, AliasStore};
use clap::{Parser, ValueEnum};
use eyre::{Result, WrapErr};
@@ -112,21 +112,46 @@ $env.config = (
.into();
let host_id = Settings::host_id().expect("failed to get host_id");
- let alias_store = AliasStore::new(sqlite_store, host_id, encryption_key);
+ let alias_store = AliasStore::new(sqlite_store.clone(), host_id, encryption_key);
+ let var_store = VarStore::new(sqlite_store.clone(), host_id, encryption_key);
match self.shell {
Shell::Zsh => {
- zsh::init(alias_store, self.disable_up_arrow, self.disable_ctrl_r).await?;
+ zsh::init(
+ alias_store,
+ var_store,
+ self.disable_up_arrow,
+ self.disable_ctrl_r,
+ )
+ .await?;
}
Shell::Bash => {
- bash::init(alias_store, self.disable_up_arrow, self.disable_ctrl_r).await?;
+ bash::init(
+ alias_store,
+ var_store,
+ self.disable_up_arrow,
+ self.disable_ctrl_r,
+ )
+ .await?;
}
Shell::Fish => {
- fish::init(alias_store, self.disable_up_arrow, self.disable_ctrl_r).await?;
+ fish::init(
+ alias_store,
+ var_store,
+ self.disable_up_arrow,
+ self.disable_ctrl_r,
+ )
+ .await?;
}
Shell::Nu => self.init_nu(),
Shell::Xonsh => {
- xonsh::init(alias_store, self.disable_up_arrow, self.disable_ctrl_r).await?;
+ xonsh::init(
+ alias_store,
+ var_store,
+ self.disable_up_arrow,
+ self.disable_ctrl_r,
+ )
+ .await?;
}
}
diff --git a/crates/atuin/src/command/client/init/bash.rs b/crates/atuin/src/command/client/init/bash.rs
index 6e7f14e7..27871bee 100644
--- a/crates/atuin/src/command/client/init/bash.rs
+++ b/crates/atuin/src/command/client/init/bash.rs
@@ -1,4 +1,4 @@
-use atuin_dotfiles::store::AliasStore;
+use atuin_dotfiles::store::{var::VarStore, AliasStore};
use eyre::Result;
pub fn init_static(disable_up_arrow: bool, disable_ctrl_r: bool) {
@@ -15,12 +15,19 @@ pub fn init_static(disable_up_arrow: bool, disable_ctrl_r: bool) {
println!("{base}");
}
-pub async fn init(store: AliasStore, disable_up_arrow: bool, disable_ctrl_r: bool) -> Result<()> {
+pub async fn init(
+ aliases: AliasStore,
+ vars: VarStore,
+ disable_up_arrow: bool,
+ disable_ctrl_r: bool,
+) -> Result<()> {
init_static(disable_up_arrow, disable_ctrl_r);
- let aliases = atuin_dotfiles::shell::bash::config(&store).await;
+ let aliases = atuin_dotfiles::shell::bash::alias_config(&aliases).await;
+ let vars = atuin_dotfiles::shell::bash::var_config(&vars).await;
println!("{aliases}");
+ println!("{vars}");
Ok(())
}
diff --git a/crates/atuin/src/command/client/init/fish.rs b/crates/atuin/src/command/client/init/fish.rs
index 4ec74952..fe58dbed 100644
--- a/crates/atuin/src/command/client/init/fish.rs
+++ b/crates/atuin/src/command/client/init/fish.rs
@@ -1,4 +1,4 @@
-use atuin_dotfiles::store::AliasStore;
+use atuin_dotfiles::store::{var::VarStore, AliasStore};
use eyre::Result;
pub fn init_static(disable_up_arrow: bool, disable_ctrl_r: bool) {
@@ -34,12 +34,19 @@ bind -M insert \e\[A _atuin_bind_up";
}
}
-pub async fn init(store: AliasStore, disable_up_arrow: bool, disable_ctrl_r: bool) -> Result<()> {
+pub async fn init(
+ aliases: AliasStore,
+ vars: VarStore,
+ disable_up_arrow: bool,
+ disable_ctrl_r: bool,
+) -> Result<()> {
init_static(disable_up_arrow, disable_ctrl_r);
- let aliases = atuin_dotfiles::shell::fish::config(&store).await;
+ let aliases = atuin_dotfiles::shell::fish::alias_config(&aliases).await;
+ let vars = atuin_dotfiles::shell::fish::var_config(&vars).await;
println!("{aliases}");
+ println!("{vars}");
Ok(())
}
diff --git a/crates/atuin/src/command/client/init/xonsh.rs b/crates/atuin/src/command/client/init/xonsh.rs
index cfe64f7e..8febcc92 100644
--- a/crates/atuin/src/command/client/init/xonsh.rs
+++ b/crates/atuin/src/command/client/init/xonsh.rs
@@ -1,4 +1,4 @@
-use atuin_dotfiles::store::AliasStore;
+use atuin_dotfiles::store::{var::VarStore, AliasStore};
use eyre::Result;
pub fn init_static(disable_up_arrow: bool, disable_ctrl_r: bool) {
@@ -20,12 +20,19 @@ pub fn init_static(disable_up_arrow: bool, disable_ctrl_r: bool) {
println!("{base}");
}
-pub async fn init(store: AliasStore, disable_up_arrow: bool, disable_ctrl_r: bool) -> Result<()> {
+pub async fn init(
+ aliases: AliasStore,
+ vars: VarStore,
+ disable_up_arrow: bool,
+ disable_ctrl_r: bool,
+) -> Result<()> {
init_static(disable_up_arrow, disable_ctrl_r);
- let aliases = atuin_dotfiles::shell::xonsh::config(&store).await;
+ let aliases = atuin_dotfiles::shell::xonsh::alias_config(&aliases).await;
+ let vars = atuin_dotfiles::shell::xonsh::var_config(&vars).await;
println!("{aliases}");
+ println!("{vars}");
Ok(())
}
diff --git a/crates/atuin/src/command/client/init/zsh.rs b/crates/atuin/src/command/client/init/zsh.rs
index 2341e203..1b5b3b77 100644
--- a/crates/atuin/src/command/client/init/zsh.rs
+++ b/crates/atuin/src/command/client/init/zsh.rs
@@ -1,4 +1,4 @@
-use atuin_dotfiles::store::AliasStore;
+use atuin_dotfiles::store::{var::VarStore, AliasStore};
use eyre::Result;
pub fn init_static(disable_up_arrow: bool, disable_ctrl_r: bool) {
@@ -28,12 +28,19 @@ bindkey -M vicmd 'k' atuin-up-search-vicmd";
}
}
-pub async fn init(store: AliasStore, disable_up_arrow: bool, disable_ctrl_r: bool) -> Result<()> {
+pub async fn init(
+ aliases: AliasStore,
+ vars: VarStore,
+ disable_up_arrow: bool,
+ disable_ctrl_r: bool,
+) -> Result<()> {
init_static(disable_up_arrow, disable_ctrl_r);
- let aliases = atuin_dotfiles::shell::zsh::config(&store).await;
+ let aliases = atuin_dotfiles::shell::zsh::alias_config(&aliases).await;
+ let vars = atuin_dotfiles::shell::zsh::var_config(&vars).await;
println!("{aliases}");
+ println!("{vars}");
Ok(())
}
diff --git a/crates/atuin/src/command/client/store/rebuild.rs b/crates/atuin/src/command/client/store/rebuild.rs
index f99d3247..ad83c041 100644
--- a/crates/atuin/src/command/client/store/rebuild.rs
+++ b/crates/atuin/src/command/client/store/rebuild.rs
@@ -1,4 +1,4 @@
-use atuin_dotfiles::store::AliasStore;
+use atuin_dotfiles::store::{var::VarStore, AliasStore};
use clap::Args;
use eyre::{bail, Result};
@@ -59,9 +59,12 @@ impl Rebuild {
let encryption_key: [u8; 32] = encryption::load_key(settings)?.into();
let host_id = Settings::host_id().expect("failed to get host_id");
- let alias_store = AliasStore::new(store, host_id, encryption_key);
+
+ let alias_store = AliasStore::new(store.clone(), host_id, encryption_key);
+ let var_store = VarStore::new(store.clone(), host_id, encryption_key);
alias_store.build().await?;
+ var_store.build().await?;
Ok(())
}
diff --git a/crates/atuin/src/sync.rs b/crates/atuin/src/sync.rs
index 894a4aaa..feca3026 100644
--- a/crates/atuin/src/sync.rs
+++ b/crates/atuin/src/sync.rs
@@ -1,4 +1,4 @@
-use atuin_dotfiles::store::AliasStore;
+use atuin_dotfiles::store::{var::VarStore, AliasStore};
use eyre::{Context, Result};
use atuin_client::{
@@ -29,9 +29,12 @@ pub async fn build(
let history_store = HistoryStore::new(store.clone(), host_id, encryption_key);
let alias_store = AliasStore::new(store.clone(), host_id, encryption_key);
+ let var_store = VarStore::new(store.clone(), host_id, encryption_key);
history_store.incremental_build(db, downloaded).await?;
+
alias_store.build().await?;
+ var_store.build().await?;
Ok(())
}