summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEllie Huxtable <ellie@elliehuxtable.com>2024-04-03 10:19:24 +0100
committerGitHub <noreply@github.com>2024-04-03 10:19:24 +0100
commit894eaa6faff86e1839510e114427b949b2440d39 (patch)
tree7bbda2eebb75b79292f3095017c44ebc97387654
parent61daae27ab559d6895c055f2bbe9261292d7963e (diff)
perf(dotfiles): cache aliases and read straight from file (#1918)
* cache aliases when set locally * handle rebuild on sync and tidy things a bit * support all shells except nu * make clippy happy * fmt * fix for no features
-rw-r--r--atuin-common/src/utils.rs8
-rw-r--r--atuin-dotfiles/src/shell/bash.rs41
-rw-r--r--atuin-dotfiles/src/shell/fish.rs42
-rw-r--r--atuin-dotfiles/src/shell/xonsh.rs41
-rw-r--r--atuin-dotfiles/src/shell/zsh.rs41
-rw-r--r--atuin-dotfiles/src/store.rs54
-rw-r--r--atuin/src/command/client/history.rs2
-rw-r--r--atuin/src/command/client/init/bash.rs3
-rw-r--r--atuin/src/command/client/init/fish.rs3
-rw-r--r--atuin/src/command/client/init/xonsh.rs3
-rw-r--r--atuin/src/command/client/init/zsh.rs3
-rw-r--r--atuin/src/command/client/store/pull.rs12
-rw-r--r--atuin/src/command/client/store/rebuild.rs16
-rw-r--r--atuin/src/command/client/sync.rs4
-rw-r--r--atuin/src/main.rs3
-rw-r--r--atuin/src/sync.rs37
16 files changed, 264 insertions, 49 deletions
diff --git a/atuin-common/src/utils.rs b/atuin-common/src/utils.rs
index 1e0f5a9a..7c533663 100644
--- a/atuin-common/src/utils.rs
+++ b/atuin-common/src/utils.rs
@@ -75,6 +75,14 @@ pub fn data_dir() -> PathBuf {
data_dir.join("atuin")
}
+pub fn dotfiles_cache_dir() -> PathBuf {
+ // In most cases, this will be ~/.local/share/atuin/dotfiles/cache
+ let data_dir = std::env::var("XDG_DATA_HOME")
+ .map_or_else(|_| home_dir().join(".local").join("share"), PathBuf::from);
+
+ data_dir.join("atuin").join("dotfiles").join("cache")
+}
+
pub fn get_current_dir() -> String {
// Prefer PWD environment variable over cwd if available to better support symbolic links
match env::var("PWD") {
diff --git a/atuin-dotfiles/src/shell/bash.rs b/atuin-dotfiles/src/shell/bash.rs
index c5bd87b2..5bdd7dce 100644
--- a/atuin-dotfiles/src/shell/bash.rs
+++ b/atuin-dotfiles/src/shell/bash.rs
@@ -1,12 +1,39 @@
-use super::Alias;
+use std::path::PathBuf;
-// Configuration for bash
-pub fn build(aliases: &[Alias]) -> String {
- let mut config = String::new();
+use crate::store::AliasStore;
- for alias in aliases {
- config.push_str(&format!("alias {}='{}'\n", alias.name, alias.value));
+async fn cached_aliases(path: PathBuf, store: &AliasStore) -> String {
+ match tokio::fs::read_to_string(path).await {
+ Ok(aliases) => aliases,
+ Err(r) => {
+ // we failed to read the file for some reason, but the file does exist
+ // fallback to generating new aliases on the fly
+
+ store.posix().await.unwrap_or_else(|e| {
+ format!("echo 'Atuin: failed to read and generate aliases: \n{r}\n{e}'",)
+ })
+ }
+ }
+}
+
+/// 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 {
+ // First try to read the cached config
+ let aliases = atuin_common::utils::dotfiles_cache_dir().join("aliases.bash");
+
+ if aliases.exists() {
+ return cached_aliases(aliases, store).await;
+ }
+
+ if let Err(e) = store.build().await {
+ return format!("echo 'Atuin: failed to generate aliases: {}'", e);
}
- config
+ cached_aliases(aliases, store).await
}
diff --git a/atuin-dotfiles/src/shell/fish.rs b/atuin-dotfiles/src/shell/fish.rs
index c6277f34..bf4e1a3b 100644
--- a/atuin-dotfiles/src/shell/fish.rs
+++ b/atuin-dotfiles/src/shell/fish.rs
@@ -1,12 +1,40 @@
-use super::Alias;
-
// Configuration for fish
-pub fn build(aliases: &[Alias]) -> String {
- let mut config = String::new();
+use std::path::PathBuf;
+
+use crate::store::AliasStore;
+
+async fn cached_aliases(path: PathBuf, store: &AliasStore) -> String {
+ match tokio::fs::read_to_string(path).await {
+ Ok(aliases) => aliases,
+ Err(r) => {
+ // we failed to read the file for some reason, but the file does exist
+ // fallback to generating new aliases on the fly
+
+ store.posix().await.unwrap_or_else(|e| {
+ format!("echo 'Atuin: failed to read and generate aliases: \n{r}\n{e}'",)
+ })
+ }
+ }
+}
+
+/// 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 {
+ // First try to read the cached config
+ let aliases = atuin_common::utils::dotfiles_cache_dir().join("aliases.fish");
+
+ if aliases.exists() {
+ return cached_aliases(aliases, store).await;
+ }
- for alias in aliases {
- config.push_str(&format!("alias {}='{}'\n", alias.name, alias.value));
+ if let Err(e) = store.build().await {
+ return format!("echo 'Atuin: failed to generate aliases: {}'", e);
}
- config
+ cached_aliases(aliases, store).await
}
diff --git a/atuin-dotfiles/src/shell/xonsh.rs b/atuin-dotfiles/src/shell/xonsh.rs
index 8b61ff4c..383df4ec 100644
--- a/atuin-dotfiles/src/shell/xonsh.rs
+++ b/atuin-dotfiles/src/shell/xonsh.rs
@@ -1,12 +1,39 @@
-use super::Alias;
+use std::path::PathBuf;
-// Configuration for xonsh
-pub fn build(aliases: &[Alias]) -> String {
- let mut config = String::new();
+use crate::store::AliasStore;
- for alias in aliases {
- config.push_str(&format!("aliases['{}'] ='{}'\n", alias.name, alias.value));
+async fn cached_aliases(path: PathBuf, store: &AliasStore) -> String {
+ match tokio::fs::read_to_string(path).await {
+ Ok(aliases) => aliases,
+ Err(r) => {
+ // we failed to read the file for some reason, but the file does exist
+ // fallback to generating new aliases on the fly
+
+ store.xonsh().await.unwrap_or_else(|e| {
+ format!("echo 'Atuin: failed to read and generate aliases: \n{r}\n{e}'",)
+ })
+ }
+ }
+}
+
+/// 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 {
+ // First try to read the cached config
+ let aliases = atuin_common::utils::dotfiles_cache_dir().join("aliases.xsh");
+
+ if aliases.exists() {
+ return cached_aliases(aliases, store).await;
+ }
+
+ if let Err(e) = store.build().await {
+ return format!("echo 'Atuin: failed to generate aliases: {}'", e);
}
- config
+ cached_aliases(aliases, store).await
}
diff --git a/atuin-dotfiles/src/shell/zsh.rs b/atuin-dotfiles/src/shell/zsh.rs
index 6f81ed55..d863b261 100644
--- a/atuin-dotfiles/src/shell/zsh.rs
+++ b/atuin-dotfiles/src/shell/zsh.rs
@@ -1,12 +1,39 @@
-use super::Alias;
+use std::path::PathBuf;
-// Configuration for zsh
-pub fn build(aliases: &[Alias]) -> String {
- let mut config = String::new();
+use crate::store::AliasStore;
- for alias in aliases {
- config.push_str(&format!("alias {}='{}'\n", alias.name, alias.value));
+async fn cached_aliases(path: PathBuf, store: &AliasStore) -> String {
+ match tokio::fs::read_to_string(path).await {
+ Ok(aliases) => aliases,
+ Err(r) => {
+ // we failed to read the file for some reason, but the file does exist
+ // fallback to generating new aliases on the fly
+
+ store.posix().await.unwrap_or_else(|e| {
+ format!("echo 'Atuin: failed to read and generate aliases: \n{r}\n{e}'",)
+ })
+ }
+ }
+}
+
+/// 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 {
+ // First try to read the cached config
+ let aliases = atuin_common::utils::dotfiles_cache_dir().join("aliases.zsh");
+
+ if aliases.exists() {
+ return cached_aliases(aliases, store).await;
+ }
+
+ if let Err(e) = store.build().await {
+ return format!("echo 'Atuin: failed to generate aliases: {}'", e);
}
- config
+ cached_aliases(aliases, store).await
}
diff --git a/atuin-dotfiles/src/store.rs b/atuin-dotfiles/src/store.rs
index 96e0fb32..425a5e1e 100644
--- a/atuin-dotfiles/src/store.rs
+++ b/atuin-dotfiles/src/store.rs
@@ -136,6 +136,54 @@ impl AliasStore {
}
}
+ pub async fn posix(&self) -> Result<String> {
+ let aliases = self.aliases().await?;
+
+ let mut config = String::new();
+
+ for alias in aliases {
+ config.push_str(&format!("alias {}='{}'\n", alias.name, alias.value));
+ }
+
+ Ok(config)
+ }
+
+ pub async fn xonsh(&self) -> Result<String> {
+ let aliases = self.aliases().await?;
+
+ let mut config = String::new();
+
+ for alias in aliases {
+ config.push_str(&format!("aliases['{}'] ='{}'\n", alias.name, alias.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?;
+
+ // All the same contents, maybe optimize in the future or perhaps there will be quirks
+ // per-shell
+ // I'd prefer separation atm
+ let zsh = dir.join("aliases.zsh");
+ let bash = dir.join("aliases.bash");
+ let fish = dir.join("aliases.fish");
+ let xsh = dir.join("aliases.xsh");
+
+ tokio::fs::write(zsh, &posix).await?;
+ tokio::fs::write(bash, &posix).await?;
+ tokio::fs::write(fish, &posix).await?;
+ tokio::fs::write(xsh, &xonsh).await?;
+
+ Ok(())
+ }
+
pub async fn set(&self, name: &str, value: &str) -> Result<()> {
if name.len() + value.len() > CONFIG_SHELL_ALIAS_FIELD_MAX_LEN {
return Err(eyre!(
@@ -169,6 +217,9 @@ impl AliasStore {
.push(&record.encrypt::<PASETO_V4>(&self.encryption_key))
.await?;
+ // set mutates shell config, so build again
+ self.build().await?;
+
Ok(())
}
@@ -202,6 +253,9 @@ impl AliasStore {
.push(&record.encrypt::<PASETO_V4>(&self.encryption_key))
.await?;
+ // delete mutates shell config, so build again
+ self.build().await?;
+
Ok(())
}
diff --git a/atuin/src/command/client/history.rs b/atuin/src/command/client/history.rs
index e5acc8b1..b9e54b50 100644
--- a/atuin/src/command/client/history.rs
+++ b/atuin/src/command/client/history.rs
@@ -365,7 +365,7 @@ impl Cmd {
let (_, downloaded) = record::sync::sync(settings, &store).await?;
Settings::save_sync_time()?;
- history_store.incremental_build(db, &downloaded).await?;
+ crate::sync::build(settings, &store, db, Some(&downloaded)).await?;
} else {
debug!("running periodic background sync");
sync::sync(settings, false, db).await?;
diff --git a/atuin/src/command/client/init/bash.rs b/atuin/src/command/client/init/bash.rs
index 2fd7c195..6e7f14e7 100644
--- a/atuin/src/command/client/init/bash.rs
+++ b/atuin/src/command/client/init/bash.rs
@@ -18,8 +18,7 @@ pub fn init_static(disable_up_arrow: bool, disable_ctrl_r: bool) {
pub async fn init(store: AliasStore, disable_up_arrow: bool, disable_ctrl_r: bool) -> Result<()> {
init_static(disable_up_arrow, disable_ctrl_r);
- let aliases = store.aliases().await?;
- let aliases = atuin_dotfiles::shell::bash::build(&aliases[..]);
+ let aliases = atuin_dotfiles::shell::bash::config(&store).await;
println!("{aliases}");
diff --git a/atuin/src/command/client/init/fish.rs b/atuin/src/command/client/init/fish.rs
index 83bf0500..4ec74952 100644
--- a/atuin/src/command/client/init/fish.rs
+++ b/atuin/src/command/client/init/fish.rs
@@ -37,8 +37,7 @@ bind -M insert \e\[A _atuin_bind_up";
pub async fn init(store: AliasStore, disable_up_arrow: bool, disable_ctrl_r: bool) -> Result<()> {
init_static(disable_up_arrow, disable_ctrl_r);
- let aliases = store.aliases().await?;
- let aliases = atuin_dotfiles::shell::fish::build(&aliases[..]);
+ let aliases = atuin_dotfiles::shell::fish::config(&store).await;
println!("{aliases}");
diff --git a/atuin/src/command/client/init/xonsh.rs b/atuin/src/command/client/init/xonsh.rs
index 75ab4a1a..cfe64f7e 100644
--- a/atuin/src/command/client/init/xonsh.rs
+++ b/atuin/src/command/client/init/xonsh.rs
@@ -23,8 +23,7 @@ pub fn init_static(disable_up_arrow: bool, disable_ctrl_r: bool) {
pub async fn init(store: AliasStore, disable_up_arrow: bool, disable_ctrl_r: bool) -> Result<()> {
init_static(disable_up_arrow, disable_ctrl_r);
- let aliases = store.aliases().await?;
- let aliases = atuin_dotfiles::shell::xonsh::build(&aliases[..]);
+ let aliases = atuin_dotfiles::shell::xonsh::config(&store).await;
println!("{aliases}");
diff --git a/atuin/src/command/client/init/zsh.rs b/atuin/src/command/client/init/zsh.rs
index 574047a4..2341e203 100644
--- a/atuin/src/command/client/init/zsh.rs
+++ b/atuin/src/command/client/init/zsh.rs
@@ -31,8 +31,7 @@ bindkey -M vicmd 'k' atuin-up-search-vicmd";
pub async fn init(store: AliasStore, disable_up_arrow: bool, disable_ctrl_r: bool) -> Result<()> {
init_static(disable_up_arrow, disable_ctrl_r);
- let aliases = store.aliases().await?;
- let aliases = atuin_dotfiles::shell::zsh::build(&aliases[..]);
+ let aliases = atuin_dotfiles::shell::zsh::config(&store).await;
println!("{aliases}");
diff --git a/atuin/src/command/client/store/pull.rs b/atuin/src/command/client/store/pull.rs
index d920dd21..36450fbf 100644
--- a/atuin/src/command/client/store/pull.rs
+++ b/atuin/src/command/client/store/pull.rs
@@ -1,10 +1,8 @@
use clap::Args;
-use eyre::{Result, WrapErr};
+use eyre::Result;
use atuin_client::{
database::Database,
- encryption,
- history::store::HistoryStore,
record::store::Store,
record::sync::Operation,
record::{sqlite_store::SqliteStore, sync},
@@ -73,13 +71,7 @@ impl Pull {
println!("Downloaded {} records", downloaded.len());
- 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 history_store = HistoryStore::new(store.clone(), host_id, encryption_key);
- history_store.incremental_build(db, &downloaded).await?;
+ crate::sync::build(settings, &store, db, Some(&downloaded)).await?;
Ok(())
}
diff --git a/atuin/src/command/client/store/rebuild.rs b/atuin/src/command/client/store/rebuild.rs
index 880647b4..f99d3247 100644
--- a/atuin/src/command/client/store/rebuild.rs
+++ b/atuin/src/command/client/store/rebuild.rs
@@ -1,3 +1,4 @@
+use atuin_dotfiles::store::AliasStore;
use clap::Args;
use eyre::{bail, Result};
@@ -28,6 +29,10 @@ impl Rebuild {
.await?;
}
+ "dotfiles" => {
+ self.rebuild_dotfiles(settings, store.clone()).await?;
+ }
+
tag => bail!("unknown tag: {tag}"),
}
@@ -49,4 +54,15 @@ impl Rebuild {
Ok(())
}
+
+ async fn rebuild_dotfiles(&self, settings: &Settings, store: SqliteStore) -> Result<()> {
+ 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);
+
+ alias_store.build().await?;
+
+ Ok(())
+ }
}
diff --git a/atuin/src/command/client/sync.rs b/atuin/src/command/client/sync.rs
index 4889cbca..be1bf6d2 100644
--- a/atuin/src/command/client/sync.rs
+++ b/atuin/src/command/client/sync.rs
@@ -90,7 +90,7 @@ async fn run(
let (uploaded, downloaded) = sync::sync(settings, &store).await?;
- history_store.incremental_build(db, &downloaded).await?;
+ crate::sync::build(settings, &store, db, Some(&downloaded)).await?;
println!("{uploaded}/{} up/down to record store", downloaded.len());
@@ -113,7 +113,7 @@ async fn run(
// we'll want to run sync once more, as there will now be stuff to upload
let (uploaded, downloaded) = sync::sync(settings, &store).await?;
- history_store.incremental_build(db, &downloaded).await?;
+ crate::sync::build(settings, &store, db, Some(&downloaded)).await?;
println!("{uploaded}/{} up/down to record store", downloaded.len());
}
diff --git a/atuin/src/main.rs b/atuin/src/main.rs
index e24b0120..16a80b10 100644
--- a/atuin/src/main.rs
+++ b/atuin/src/main.rs
@@ -8,6 +8,9 @@ use command::AtuinCmd;
mod command;
+#[cfg(feature = "sync")]
+mod sync;
+
const VERSION: &str = env!("CARGO_PKG_VERSION");
const SHA: &str = env!("GIT_HASH");
diff --git a/atuin/src/sync.rs b/atuin/src/sync.rs
new file mode 100644
index 00000000..894a4aaa
--- /dev/null
+++ b/atuin/src/sync.rs
@@ -0,0 +1,37 @@
+use atuin_dotfiles::store::AliasStore;
+use eyre::{Context, Result};
+
+use atuin_client::{
+ database::Database, history::store::HistoryStore, record::sqlite_store::SqliteStore,
+ settings::Settings,
+};
+use atuin_common::record::RecordId;
+
+/// This is the only crate that ties together all other crates.
+/// Therefore, it's the only crate where functions tying together all stores can live
+
+/// Rebuild all stores after a sync
+/// Note: for history, this only does an _incremental_ sync. Hence the need to specify downloaded
+/// records.
+pub async fn build(
+ settings: &Settings,
+ store: &SqliteStore,
+ db: &dyn Database,
+ downloaded: Option<&[RecordId]>,
+) -> Result<()> {
+ let encryption_key: [u8; 32] = atuin_client::encryption::load_key(settings)
+ .context("could not load encryption key")?
+ .into();
+
+ let host_id = Settings::host_id().expect("failed to get host_id");
+
+ let downloaded = downloaded.unwrap_or(&[]);
+
+ let history_store = HistoryStore::new(store.clone(), host_id, encryption_key);
+ let alias_store = AliasStore::new(store.clone(), host_id, encryption_key);
+
+ history_store.incremental_build(db, downloaded).await?;
+ alias_store.build().await?;
+
+ Ok(())
+}