summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEllie Huxtable <ellie@elliehuxtable.com>2024-02-15 19:07:08 +0000
committerGitHub <noreply@github.com>2024-02-15 19:07:08 +0000
commit20f329646894e11e47e7e516b076fa23976c0d5a (patch)
treefc0add6e23c4a4a8fd4ae85520aafc5576848590
parentf8d01eef998c86c52514896539c13dbfe1837e55 (diff)
feat: support syncing aliases (#1721)
* feat: support syncing aliases This is definitely not yet finished, but works for zsh right now. TODO: 1. Support other shells 2. Cache the alias generation, so we don't have to do a bunch of work at shell init time * correct imports * fix clippy errors * fix tests * add the other shells * support xonsh * add delete * update rust, then make clippy happy once more * omfg fmt too
-rw-r--r--Cargo.lock14
-rw-r--r--Cargo.toml3
-rw-r--r--atuin-config/Cargo.toml23
-rw-r--r--atuin-config/src/lib.rs2
-rw-r--r--atuin-config/src/shell.rs10
-rw-r--r--atuin-config/src/shell/bash.rs12
-rw-r--r--atuin-config/src/shell/fish.rs12
-rw-r--r--atuin-config/src/shell/xonsh.rs12
-rw-r--r--atuin-config/src/shell/zsh.rs12
-rw-r--r--atuin-config/src/store.rs310
-rw-r--r--atuin/Cargo.toml1
-rw-r--r--atuin/src/command/client.rs14
-rw-r--r--atuin/src/command/client/config.rs22
-rw-r--r--atuin/src/command/client/config/alias.rs42
-rw-r--r--atuin/src/command/client/default_config.rs5
-rw-r--r--atuin/src/command/client/init.rs112
-rw-r--r--atuin/src/command/client/init/bash.rs23
-rw-r--r--atuin/src/command/client/init/fish.rs42
-rw-r--r--atuin/src/command/client/init/xonsh.rs28
-rw-r--r--atuin/src/command/client/init/zsh.rs36
-rw-r--r--atuin/src/command/init.rs172
-rw-r--r--atuin/src/command/mod.rs9
22 files changed, 730 insertions, 186 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 280b16b3..0131333a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -185,6 +185,7 @@ dependencies = [
"async-trait",
"atuin-client",
"atuin-common",
+ "atuin-config",
"atuin-server",
"atuin-server-postgres",
"base64 0.21.7",
@@ -287,6 +288,19 @@ dependencies = [
]
[[package]]
+name = "atuin-config"
+version = "0.1.0"
+dependencies = [
+ "atuin-client",
+ "atuin-common",
+ "crypto_secretbox",
+ "eyre",
+ "rand",
+ "rmp",
+ "tokio",
+]
+
+[[package]]
name = "atuin-server"
version = "18.0.1"
dependencies = [
diff --git a/Cargo.toml b/Cargo.toml
index e0cd62e8..580c730a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,7 +5,8 @@ members = [
"atuin-server",
"atuin-server-postgres",
"atuin-server-database",
- "atuin-common",
+ "atuin-common",
+ "atuin-config",
]
resolver = "2"
diff --git a/atuin-config/Cargo.toml b/atuin-config/Cargo.toml
new file mode 100644
index 00000000..b6fffbf4
--- /dev/null
+++ b/atuin-config/Cargo.toml
@@ -0,0 +1,23 @@
+[package]
+name = "atuin-config"
+edition = "2021"
+version = "0.1.0" # intentionally not the same as the rest
+
+authors.workspace = true
+rust-version.workspace = true
+license.workspace = true
+homepage.workspace = true
+repository.workspace = true
+readme.workspace = true
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+atuin-common = { path = "../atuin-common", version = "18.0.1" }
+atuin-client = { path = "../atuin-client", version = "18.0.1" }
+
+eyre = { workspace = true }
+tokio = { workspace = true }
+rmp = { version = "0.8.11" }
+rand = { workspace = true }
+crypto_secretbox = "0.1.1"
diff --git a/atuin-config/src/lib.rs b/atuin-config/src/lib.rs
new file mode 100644
index 00000000..74daf8ef
--- /dev/null
+++ b/atuin-config/src/lib.rs
@@ -0,0 +1,2 @@
+pub mod shell;
+pub mod store;
diff --git a/atuin-config/src/shell.rs b/atuin-config/src/shell.rs
new file mode 100644
index 00000000..a69a2d6b
--- /dev/null
+++ b/atuin-config/src/shell.rs
@@ -0,0 +1,10 @@
+pub mod bash;
+pub mod fish;
+pub mod xonsh;
+pub mod zsh;
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct Alias {
+ pub name: String,
+ pub value: String,
+}
diff --git a/atuin-config/src/shell/bash.rs b/atuin-config/src/shell/bash.rs
new file mode 100644
index 00000000..c5bd87b2
--- /dev/null
+++ b/atuin-config/src/shell/bash.rs
@@ -0,0 +1,12 @@
+use super::Alias;
+
+// Configuration for bash
+pub fn build(aliases: &[Alias]) -> String {
+ let mut config = String::new();
+
+ for alias in aliases {
+ config.push_str(&format!("alias {}='{}'\n", alias.name, alias.value));
+ }
+
+ config
+}
diff --git a/atuin-config/src/shell/fish.rs b/atuin-config/src/shell/fish.rs
new file mode 100644
index 00000000..c6277f34
--- /dev/null
+++ b/atuin-config/src/shell/fish.rs
@@ -0,0 +1,12 @@
+use super::Alias;
+
+// Configuration for fish
+pub fn build(aliases: &[Alias]) -> String {
+ let mut config = String::new();
+
+ for alias in aliases {
+ config.push_str(&format!("alias {}='{}'\n", alias.name, alias.value));
+ }
+
+ config
+}
diff --git a/atuin-config/src/shell/xonsh.rs b/atuin-config/src/shell/xonsh.rs
new file mode 100644
index 00000000..8b61ff4c
--- /dev/null
+++ b/atuin-config/src/shell/xonsh.rs
@@ -0,0 +1,12 @@
+use super::Alias;
+
+// Configuration for xonsh
+pub fn build(aliases: &[Alias]) -> String {
+ let mut config = String::new();
+
+ for alias in aliases {
+ config.push_str(&format!("aliases['{}'] ='{}'\n", alias.name, alias.value));
+ }
+
+ config
+}
diff --git a/atuin-config/src/shell/zsh.rs b/atuin-config/src/shell/zsh.rs
new file mode 100644
index 00000000..6f81ed55
--- /dev/null
+++ b/atuin-config/src/shell/zsh.rs
@@ -0,0 +1,12 @@
+use super::Alias;
+
+// Configuration for zsh
+pub fn build(aliases: &[Alias]) -> String {
+ let mut config = String::new();
+
+ for alias in aliases {
+ config.push_str(&format!("alias {}='{}'\n", alias.name, alias.value));
+ }
+
+ config
+}
diff --git a/atuin-config/src/store.rs b/atuin-config/src/store.rs
new file mode 100644
index 00000000..96e0fb32
--- /dev/null
+++ b/atuin-config/src/store.rs
@@ -0,0 +1,310 @@
+use std::collections::BTreeMap;
+
+use atuin_client::record::sqlite_store::SqliteStore;
+// Sync aliases
+// This will be noticeable similar to the kv store, though I expect the two shall diverge
+// While we will support a range of shell config, I'd rather have a larger number of small records
+// + stores, rather than one mega config store.
+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::Alias;
+
+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.
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum AliasRecord {
+ Create(Alias), // create a full record
+ Delete(String), // delete by name
+}
+
+impl AliasRecord {
+ pub fn serialize(&self) -> Result<DecryptedData> {
+ use rmp::encode;
+
+ let mut output = vec![];
+
+ match self {
+ AliasRecord::Create(alias) => {
+ encode::write_u8(&mut output, 0)?; // create
+ encode::write_array_len(&mut output, 2)?; // 2 fields
+
+ encode::write_str(&mut output, alias.name.as_str())?;
+ encode::write_str(&mut output, alias.value.as_str())?;
+ }
+ AliasRecord::Delete(name) => {
+ encode::write_u8(&mut output, 1)?; // delete
+ encode::write_array_len(&mut output, 1)?; // 1 field
+
+ encode::write_str(&mut output, name.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 {
+ CONFIG_SHELL_ALIAS_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 nfields = decode::read_array_len(&mut bytes).map_err(error_report)?;
+ ensure!(
+ nfields == 2,
+ "too many entries in v0 shell alias create record"
+ );
+
+ 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)?;
+
+ if !bytes.is_empty() {
+ bail!("trailing bytes in encoded shell alias record. malformed")
+ }
+
+ Ok(AliasRecord::Create(Alias {
+ name: key.to_owned(),
+ value: value.to_owned(),
+ }))
+ }
+
+ // delete
+ 1 => {
+ let nfields = decode::read_array_len(&mut bytes).map_err(error_report)?;
+ ensure!(
+ nfields == 1,
+ "too many entries in v0 shell alias 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 shell alias record. malformed")
+ }
+
+ Ok(AliasRecord::Delete(key.to_owned()))
+ }
+
+ n => {
+ bail!("unknown AliasRecord type {n}")
+ }
+ }
+ }
+ _ => {
+ bail!("unknown version {version:?}")
+ }
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct AliasStore {
+ pub store: SqliteStore,
+ pub host_id: HostId,
+ pub encryption_key: [u8; 32],
+}
+
+impl AliasStore {
+ // will want to init the actual kv store when that is done
+ pub fn new(store: SqliteStore, host_id: HostId, encryption_key: [u8; 32]) -> AliasStore {
+ AliasStore {
+ store,
+ host_id,
+ encryption_key,
+ }
+ }
+
+ pub async fn set(&self, name: &str, value: &str) -> Result<()> {
+ if name.len() + value.len() > CONFIG_SHELL_ALIAS_FIELD_MAX_LEN {
+ return Err(eyre!(
+ "alias record too large: max len {} bytes",
+ CONFIG_SHELL_ALIAS_FIELD_MAX_LEN
+ ));
+ }
+
+ let record = AliasRecord::Create(Alias {
+ name: name.to_string(),
+ value: value.to_string(),
+ });
+
+ let bytes = record.serialize()?;
+
+ let idx = self
+ .store
+ .last(self.host_id, CONFIG_SHELL_ALIAS_TAG)
+ .await?
+ .map_or(0, |entry| entry.idx + 1);
+
+ let record = atuin_common::record::Record::builder()
+ .host(Host::new(self.host_id))
+ .version(CONFIG_SHELL_ALIAS_VERSION.to_string())
+ .tag(CONFIG_SHELL_ALIAS_TAG.to_string())
+ .idx(idx)
+ .data(bytes)
+ .build();
+
+ self.store
+ .push(&record.encrypt::<PASETO_V4>(&self.encryption_key))
+ .await?;
+
+ Ok(())
+ }
+
+ pub async fn delete(&self, name: &str) -> Result<()> {
+ if name.len() > CONFIG_SHELL_ALIAS_FIELD_MAX_LEN {
+ return Err(eyre!(
+ "alias record too large: max len {} bytes",
+ CONFIG_SHELL_ALIAS_FIELD_MAX_LEN
+ ));
+ }
+
+ let record = AliasRecord::Delete(name.to_string());
+
+ let bytes = record.serialize()?;
+
+ let idx = self
+ .store
+ .last(self.host_id, CONFIG_SHELL_ALIAS_TAG)
+ .await?
+ .map_or(0, |entry| entry.idx + 1);
+
+ let record = atuin_common::record::Record::builder()
+ .host(Host::new(self.host_id))
+ .version(CONFIG_SHELL_ALIAS_VERSION.to_string())
+ .tag(CONFIG_SHELL_ALIAS_TAG.to_string())
+ .idx(idx)
+ .data(bytes)
+ .build();
+
+ self.store
+ .push(&record.encrypt::<PASETO_V4>(&self.encryption_key))
+ .await?;
+
+ Ok(())
+ }
+
+ pub async fn aliases(&self) -> Result<Vec<Alias>> {
+ let mut build = BTreeMap::new();
+
+ // this is sorted, oldest to newest
+ let tagged = self.store.all_tagged(CONFIG_SHELL_ALIAS_TAG).await?;
+
+ for record in tagged {
+ let version = record.version.clone();
+
+ let decrypted = match version.as_str() {
+ CONFIG_SHELL_ALIAS_VERSION => record.decrypt::<PASETO_V4>(&self.encryption_key)?,
+ version => bail!("unknown version {version:?}"),
+ };
+
+ let ar = AliasRecord::deserialize(&decrypted.data, version.as_str())?;
+
+ match ar {
+ AliasRecord::Create(a) => {
+ build.insert(a.name.clone(), a);
+ }
+ AliasRecord::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::Alias;
+
+ use super::{test_sqlite_store_timeout, AliasRecord, AliasStore, CONFIG_SHELL_ALIAS_VERSION};
+ use crypto_secretbox::{KeyInit, XSalsa20Poly1305};
+
+ #[test]
+ fn encode_decode() {
+ let record = Alias {
+ name: "k".to_owned(),
+ value: "kubectl".to_owned(),
+ };
+ let record = AliasRecord::Create(record);
+
+ let snapshot = [204, 0, 146, 161, 107, 167, 107, 117, 98, 101, 99, 116, 108];
+
+ let encoded = record.serialize().unwrap();
+ let decoded = AliasRecord::deserialize(&encoded, CONFIG_SHELL_ALIAS_VERSION).unwrap();
+
+ assert_eq!(encoded.0, &snapshot);
+ assert_eq!(decoded, record);
+ }
+
+ #[tokio::test]
+ async fn build_aliases() {
+ 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 alias = AliasStore::new(store, host_id, key);
+
+ alias.set("k", "kubectl").await.unwrap();
+
+ alias.set("gp", "git push").await.unwrap();
+
+ let mut aliases = alias.aliases().await.unwrap();
+
+ aliases.sort_by_key(|a| a.name.clone());
+
+ assert_eq!(aliases.len(), 2);
+
+ assert_eq!(
+ aliases[0],
+ Alias {
+ name: String::from("gp"),
+ value: String::from("git push")
+ }
+ );
+
+ assert_eq!(
+ aliases[1],
+ Alias {
+ name: String::from("k"),
+ value: String::from("kubectl")
+ }
+ );
+ }
+}
diff --git a/atuin/Cargo.toml b/atuin/Cargo.toml
index 4941026e..694daa80 100644
--- a/atuin/Cargo.toml
+++ b/atuin/Cargo.toml
@@ -45,6 +45,7 @@ atuin-server-postgres = { path = "../atuin-server-postgres", version = "18.0.1",
atuin-server = { path = "../atuin-server", version = "18.0.1", optional = true }
atuin-client = { path = "../atuin-client", version = "18.0.1", optional = true, default-features = false }
atuin-common = { path = "../atuin-common", version = "18.0.1" }
+atuin-config = { path = "../atuin-config", version = "0.1.0" }
log = { workspace = true }
env_logger = "0.10.0"
diff --git a/atuin/src/command/client.rs b/atuin/src/command/client.rs
index 5b87d3ba..b74c9bc7 100644
--- a/atuin/src/command/client.rs
+++ b/atuin/src/command/client.rs
@@ -13,8 +13,10 @@ mod sync;
mod account;
mod config;
+mod default_config;
mod history;
mod import;
+mod init;
mod kv;
mod search;
mod stats;
@@ -50,6 +52,12 @@ pub enum Cmd {
#[command(subcommand)]
Store(store::Cmd),
+ #[command(subcommand)]
+ Config(config::Cmd),
+
+ #[command()]
+ Init(init::Cmd),
+
/// Print example configuration
#[command()]
DefaultConfig,
@@ -101,8 +109,12 @@ impl Cmd {
Self::Store(store) => store.run(&settings, &db, sqlite_store).await,
+ Self::Config(config) => config.run(&settings, sqlite_store).await,
+
+ Self::Init(init) => init.run(&settings).await,
+
Self::DefaultConfig => {
- config::run();
+ default_config::run();
Ok(())
}
}
diff --git a/atuin/src/command/client/config.rs b/atuin/src/command/client/config.rs
index f51e45c2..a3967719 100644
--- a/atuin/src/command/client/config.rs
+++ b/atuin/src/command/client/config.rs
@@ -1,5 +1,21 @@
-use atuin_client::settings::Settings;
+use clap::Subcommand;
+use eyre::Result;
-pub fn run() {
- println!("{}", Settings::example_config());
+use atuin_client::{record::sqlite_store::SqliteStore, settings::Settings};
+
+mod alias;
+
+#[derive(Subcommand, Debug)]
+#[command(infer_subcommands = true)]
+pub enum Cmd {
+ #[command(subcommand)]
+ Alias(alias::Cmd),
+}
+
+impl Cmd {
+ pub async fn run(self, settings: &Settings, store: SqliteStore) -> Result<()> {
+ match self {
+ Self::Alias(cmd) => cmd.run(settings, store).await,
+ }
+ }
}
diff --git a/atuin/src/command/client/config/alias.rs b/atuin/src/command/client/config/alias.rs
new file mode 100644
index 00000000..9d34d84a
--- /dev/null
+++ b/atuin/src/command/client/config/alias.rs
@@ -0,0 +1,42 @@
+use clap::Subcommand;
+use eyre::{Context, Result};
+
+use atuin_client::{encryption, record::sqlite_store::SqliteStore, settings::Settings};
+
+use atuin_config::store::AliasStore;
+
+#[derive(Subcommand, Debug)]
+#[command(infer_subcommands = true)]
+pub enum Cmd {
+ Set { name: String, value: String },
+ Delete { name: String },
+}
+
+impl Cmd {
+ async fn set(&self, store: AliasStore, name: String, value: String) -> Result<()> {
+ store.set(&name, &value).await?;
+
+ Ok(())
+ }
+
+ async fn delete(&self, store: AliasStore, name: String) -> Result<()> {
+ store.delete(&name).await?;
+
+ Ok(())
+ }
+
+ pub async fn run(&self, settings: &Settings, store: SqliteStore) -> Result<()> {
+ 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 alias_store = AliasStore::new(store, host_id, encryption_key);
+
+ match self {
+ Self::Set { name, value } => self.set(alias_store, name.clone(), value.clone()).await,
+
+ Self::Delete { name } => self.delete(alias_store, name.clone()).await,
+ }
+ }
+}
diff --git a/atuin/src/command/client/default_config.rs b/atuin/src/command/client/default_config.rs
new file mode 100644
index 00000000..f51e45c2
--- /dev/null
+++ b/atuin/src/command/client/default_config.rs
@@ -0,0 +1,5 @@
+use atuin_client::settings::Settings;
+
+pub fn run() {
+ println!("{}", Settings::example_config());
+}
diff --git a/atuin/src/command/client/init.rs b/atuin/src/command/client/init.rs
new file mode 100644
index 00000000..9bff3fcc
--- /dev/null
+++ b/atuin/src/command/client/init.rs
@@ -0,0 +1,112 @@
+use std::path::PathBuf;
+
+use atuin_client::{encryption, record::sqlite_store::SqliteStore, settings::Settings};
+use atuin_config::store::AliasStore;
+use clap::{Parser, ValueEnum};
+use eyre::{Result, WrapErr};
+
+mod bash;
+mod fish;
+mod xonsh;
+mod zsh;
+
+#[derive(Parser, Debug)]
+pub struct Cmd {
+ shell: Shell,
+
+ /// Disable the binding of CTRL-R to atuin
+ #[clap(long)]
+ disable_ctrl_r: bool,
+
+ /// Disable the binding of the Up Arrow key to atuin
+ #[clap(long)]
+ disable_up_arrow: bool,
+}
+
+#[derive(Clone, Copy, ValueEnum, Debug)]
+pub enum Shell {
+ /// Zsh setup
+ Zsh,
+ /// Bash setup
+ Bash,
+ /// Fish setup
+ Fish,
+ /// Nu setup
+ Nu,
+ /// Xonsh setup
+ Xonsh,
+}
+
+impl Cmd {
+ fn init_nu(&self) {
+ let full = include_str!("../../shell/atuin.nu");
+ println!("{full}");
+
+ if std::env::var("ATUIN_NOBIND").is_err() {
+ const BIND_CTRL_R: &str = r"$env.config = (
+ $env.config | upsert keybindings (
+ $env.config.keybindings
+ | append {
+ name: atuin
+ modifier: control
+ keycode: char_r
+ mode: [emacs, vi_normal, vi_insert]
+ event: { send: executehostcommand cmd: (_atuin_search_cmd) }
+ }
+ )
+)";
+ const BIND_UP_ARROW: &str = r"
+# The up arrow keybinding has surprising behavior in Nu, and is disabled by default.
+# See https://github.com/atuinsh/atuin/issues/1025 for details
+# $env.config = (
+# $env.config | upsert keybindings (
+# $env.config.keybindings
+# | append {
+# name: atuin
+# modifier: none
+# keycode: up
+# mode: [emacs, vi_normal, vi_insert]
+# event: { send: executehostcommand cmd: (_atuin_search_cmd '--shell-up-key-binding') }
+# }
+# )
+# )
+";
+ if !self.disable_ctrl_r {
+ println!("{BIND_CTRL_R}");
+ }
+ if !self.disable_up_arrow {
+ println!("{BIND_UP_ARROW}");
+ }
+ }
+ }
+
+ pub async fn run(self, settings: &Settings) -> Result<()> {
+ let record_store_path = PathBuf::from(settings.record_store_path.as_str());
+ let sqlite_store = SqliteStore::new(record_store_path, settings.local_timeout).await?;
+
+ 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 alias_store = AliasStore::new(sqlite_store, host_id, encryption_key);
+
+ match self.shell {
+ Shell::Zsh => {
+ zsh::init(alias_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?;
+ }
+ Shell::Fish => {
+ fish::init(alias_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?;
+ }
+ }
+
+ Ok(())
+ }
+}
diff --git a/atuin/src/command/client/init/bash.rs b/atuin/src/command/client/init/bash.rs
new file mode 100644
index 00000000..ac2bfb00
--- /dev/null
+++ b/atuin/src/command/client/init/bash.rs
@@ -0,0 +1,23 @@
+use atuin_config::store::AliasStore;
+use eyre::Result;
+
+pub async fn init(store: AliasStore, disable_up_arrow: bool, disable_ctrl_r: bool) -> Result<()> {
+ let base = include_str!("../../../shell/atuin.bash");
+
+ let aliases = store.aliases().await?;
+
+ let aliases = atuin_config::shell::bash::build(&aliases[..]);
+
+ let (bind_ctrl_r, bind_up_arrow) = if std::env::var("ATUIN_NOBIND").is_ok() {
+ (false, false)
+ } else {
+ (!disable_ctrl_r, !disable_up_arrow)
+ };
+
+ println!("__atuin_bind_ctrl_r={bind_ctrl_r}");
+ println!("__atuin_bind_up_arrow={bind_up_arrow}");
+ println!("{base}");
+ println!("{aliases}");
+
+ Ok(())
+}
diff --git a/atuin/src/command/client/init/fish.rs b/atuin/src/command/client/init/fish.rs
new file mode 100644
index 00000000..00913a90
--- /dev/null
+++ b/atuin/src/command/client/init/fish.rs
@@ -0,0 +1,42 @@
+use atuin_config::store::AliasStore;
+use eyre::Result;
+
+pub async fn init(store: AliasStore, disable_up_arrow: bool, disable_ctrl_r: bool) -> Result<()> {
+ let base = include_str!("../../../shell/atuin.zsh");
+
+ println!("{base}");
+
+ if std::env::var("ATUIN_NOBIND").is_err() {
+ const BIND_CTRL_R: &str = r"bind \cr _atuin_search";
+ const BIND_UP_ARROW: &str = r"bind -k up _atuin_bind_up
+bind \eOA _atuin_bind_up
+bind \e\[A _atuin_bind_up";
+ const BIND_CTRL_R_INS: &str = r"bind -M insert \cr _atuin_search";
+ const BIND_UP_ARROW_INS: &str = r"bind -M insert -k up _atuin_bind_up
+bind -M insert \eOA _atuin_bind_up
+bind -M insert \e\[A _atuin_bind_up";
+
+ if !disable_ctrl_r {
+ println!("{BIND_CTRL_R}");
+ }
+ if !disable_up_arrow {
+ println!("{BIND_UP_ARROW}");
+ }
+
+ println!("if bind -M insert > /dev/null 2>&1");
+ if !disable_ctrl_r {
+ println!("{BIND_CTRL_R_INS}");
+ }
+ if !disable_up_arrow {
+ println!("{BIND_UP_ARROW_INS}");
+ }
+ println!("end");
+ }
+
+ let aliases = store.aliases().await?;
+ let aliases = atuin_config::shell::fish::build(&aliases[..]);
+
+ println!("{aliases}");
+
+ Ok(())
+}
diff --git a/atuin/src/command/client/init/xonsh.rs b/atuin/src/command/client/init/xonsh.rs
new file mode 100644
index 00000000..4e5929ed
--- /dev/null
+++ b/atuin/src/command/client/init/xonsh.rs
@@ -0,0 +1,28 @@
+use atuin_config::store::AliasStore;
+use eyre::Result;
+
+pub async fn init(store: AliasStore, disable_up_arrow: bool, disable_ctrl_r: bool) -> Result<()> {
+ let base = include_str!("../../../shell/atuin.xsh");
+
+ let (bind_ctrl_r, bind_up_arrow) = if std::env::var("ATUIN_NOBIND").is_ok() {
+ (false, false)
+ } else {
+ (!disable_ctrl_r, !disable_up_arrow)
+ };
+ println!(
+ "_ATUIN_BIND_CTRL_R={}",
+ if bind_ctrl_r { "True" } else { "False" }
+ );
+ println!(
+ "_ATUIN_BIND_UP_ARROW={}",
+ if bind_up_arrow { "True" } else { "False" }
+ );
+ println!("{base}");
+
+ let aliases = store.aliases().await?;
+ let aliases = atuin_config::shell::xonsh::build(&aliases[..]);
+
+ println!("{aliases}");
+
+ Ok(())
+}
diff --git a/atuin/src/command/client/init/zsh.r