diff --git a/crates/atuin-dotfiles/src/shell.rs b/crates/atuin-dotfiles/src/shell.rs index a5cb0b7a4f7..f96dc6f15d7 100644 --- a/crates/atuin-dotfiles/src/shell.rs +++ b/crates/atuin-dotfiles/src/shell.rs @@ -1,7 +1,11 @@ -use eyre::Result; +use eyre::{ensure, eyre, Result}; +use rmp::{decode, encode}; use serde::Serialize; -use atuin_common::shell::{Shell, ShellError}; +use atuin_common::{ + record::DecryptedData, + shell::{Shell, ShellError}, +}; use crate::store::AliasStore; @@ -16,6 +20,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) -> 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 { + fn error_report(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 { // consider the fact we might be importing a fish alias // 'alias' output @@ -158,14 +220,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 = 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 5bdd7dce02a..b4c873361f2 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 bf4e1a3b468..fc1aeee5e4d 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 383df4ec18e..a416ccb2be1 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 d863b261ba1..efb8389726f 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 b7984c1cd1c..f1789e2b8b9 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 00000000000..139597f9cb0 --- /dev/null +++ b/crates/atuin-dotfiles/src/store/alias.rs @@ -0,0 +1,2 @@ + + diff --git a/crates/atuin-dotfiles/src/store/var.rs b/crates/atuin-dotfiles/src/store/var.rs new file mode 100644 index 00000000000..1828a87bffc --- /dev/null +++ b/crates/atuin-dotfiles/src/store/var.rs @@ -0,0 +1,367 @@ +/// 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 atuin_common::utils::unquote; +use eyre::{bail, ensure, eyre, Result}; + +use atuin_client::record::encryption::PASETO_V4; +use atuin_client::record::store::Store; +use serde::Serialize; + +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 { + 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 { + use rmp::decode; + + fn error_report(err: E) -> eyre::Report { + eyre!("{err:?}") + } + + match version { + DOTFILES_ENV_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 { + 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 { + 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 { + 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::(&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::(&self.encryption_key)) + .await?; + + // delete mutates shell config, so build again + self.build().await?; + + Ok(()) + } + + pub async fn vars(&self) -> Result> { + 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::(&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 291c794dd25..f42b18f2f85 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 00000000000..4329179be4c --- /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 = 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 bfda75be0df..8238a69bcf7 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 6e7f14e7a42..27871bee58f 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 4ec74952208..fe58dbed4a4 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 cfe64f7eda9..8febcc92eb2 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 2341e20310b..1b5b3b7721e 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 f99d3247403..ad83c041ef4 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 894a4aaa1db..feca3026c97 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(()) }