mirror of
https://github.com/atuinsh/atuin.git
synced 2025-01-11 16:59:09 +01:00
feat(dotfiles): support syncing shell/env vars (#1977)
There's a bunch of duplication here! I'd also like to support syncing shell "snippets", aka just bits of shell config that don't fit into the structure here. Potentially special handling for PATH too. Rather than come up with some abstraction in the beginning, which inevitably will not fit future uses, I'm duplicating code _for now_. Once all the functionality is there, I can tidy things up and sort a proper abstraction out. Something in atuin-client for map/list style synced structures would probably work best.
This commit is contained in:
parent
38ea7706a0
commit
d020c815c1
@ -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();
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
1
crates/atuin-dotfiles/src/store/alias.rs
Normal file
1
crates/atuin-dotfiles/src/store/alias.rs
Normal file
@ -0,0 +1 @@
|
||||
|
365
crates/atuin-dotfiles/src/store/var.rs
Normal file
365
crates/atuin-dotfiles/src/store/var.rs
Normal file
@ -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,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
101
crates/atuin/src/command/client/dotfiles/var.rs
Normal file
101
crates/atuin/src/command/client/dotfiles/var.rs
Normal file
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
@ -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?;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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(())
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user