Skip to content

Commit

Permalink
feat(dotfiles): support syncing shell/env vars
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ellie committed Apr 24, 2024
1 parent bf88b42 commit aa1de6d
Show file tree
Hide file tree
Showing 17 changed files with 742 additions and 32 deletions.
65 changes: 62 additions & 3 deletions crates/atuin-dotfiles/src/shell.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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
Expand Down Expand Up @@ -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();
Expand Down
33 changes: 31 additions & 2 deletions crates/atuin-dotfiles/src/shell/bash.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -16,14 +16,28 @@ 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.
///
/// 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");

Expand All @@ -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
}
33 changes: 31 additions & 2 deletions crates/atuin-dotfiles/src/shell/fish.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -17,14 +17,28 @@ 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.
///
/// 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");

Expand All @@ -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
}
33 changes: 31 additions & 2 deletions crates/atuin-dotfiles/src/shell/xonsh.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -16,14 +16,28 @@ 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.
///
/// 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");

Expand All @@ -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
}
33 changes: 31 additions & 2 deletions crates/atuin-dotfiles/src/shell/zsh.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -16,14 +16,28 @@ 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.
///
/// 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");

Expand All @@ -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
}
3 changes: 3 additions & 0 deletions crates/atuin-dotfiles/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions crates/atuin-dotfiles/src/store/alias.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

Loading

0 comments on commit aa1de6d

Please sign in to comment.