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
This commit is contained in:
Ellie Huxtable 2024-02-15 19:07:08 +00:00 committed by GitHub
parent f8d01eef99
commit 20f3296468
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 730 additions and 186 deletions

14
Cargo.lock generated
View File

@ -185,6 +185,7 @@ dependencies = [
"async-trait", "async-trait",
"atuin-client", "atuin-client",
"atuin-common", "atuin-common",
"atuin-config",
"atuin-server", "atuin-server",
"atuin-server-postgres", "atuin-server-postgres",
"base64 0.21.7", "base64 0.21.7",
@ -286,6 +287,19 @@ dependencies = [
"uuid", "uuid",
] ]
[[package]]
name = "atuin-config"
version = "0.1.0"
dependencies = [
"atuin-client",
"atuin-common",
"crypto_secretbox",
"eyre",
"rand",
"rmp",
"tokio",
]
[[package]] [[package]]
name = "atuin-server" name = "atuin-server"
version = "18.0.1" version = "18.0.1"

View File

@ -6,6 +6,7 @@ members = [
"atuin-server-postgres", "atuin-server-postgres",
"atuin-server-database", "atuin-server-database",
"atuin-common", "atuin-common",
"atuin-config",
] ]
resolver = "2" resolver = "2"

23
atuin-config/Cargo.toml Normal file
View File

@ -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"

2
atuin-config/src/lib.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod shell;
pub mod store;

10
atuin-config/src/shell.rs Normal file
View File

@ -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,
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

310
atuin-config/src/store.rs Normal file
View File

@ -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")
}
);
}
}

View File

@ -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-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-client = { path = "../atuin-client", version = "18.0.1", optional = true, default-features = false }
atuin-common = { path = "../atuin-common", version = "18.0.1" } atuin-common = { path = "../atuin-common", version = "18.0.1" }
atuin-config = { path = "../atuin-config", version = "0.1.0" }
log = { workspace = true } log = { workspace = true }
env_logger = "0.10.0" env_logger = "0.10.0"

View File

@ -13,8 +13,10 @@ mod sync;
mod account; mod account;
mod config; mod config;
mod default_config;
mod history; mod history;
mod import; mod import;
mod init;
mod kv; mod kv;
mod search; mod search;
mod stats; mod stats;
@ -50,6 +52,12 @@ pub enum Cmd {
#[command(subcommand)] #[command(subcommand)]
Store(store::Cmd), Store(store::Cmd),
#[command(subcommand)]
Config(config::Cmd),
#[command()]
Init(init::Cmd),
/// Print example configuration /// Print example configuration
#[command()] #[command()]
DefaultConfig, DefaultConfig,
@ -101,8 +109,12 @@ impl Cmd {
Self::Store(store) => store.run(&settings, &db, sqlite_store).await, 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 => { Self::DefaultConfig => {
config::run(); default_config::run();
Ok(()) Ok(())
} }
} }

View File

@ -1,5 +1,21 @@
use atuin_client::settings::Settings; use clap::Subcommand;
use eyre::Result;
pub fn run() { use atuin_client::{record::sqlite_store::SqliteStore, settings::Settings};
println!("{}", Settings::example_config());
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,
}
}
} }

View File

@ -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,
}
}
}

View File

@ -0,0 +1,5 @@
use atuin_client::settings::Settings;
pub fn run() {
println!("{}", Settings::example_config());
}

View File

@ -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(())
}
}

View File

@ -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(())
}

View File

@ -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(())
}

View File

@ -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(())
}

View File

@ -0,0 +1,36 @@
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"bindkey -M emacs '^r' atuin-search
bindkey -M viins '^r' atuin-search-viins
bindkey -M vicmd '/' atuin-search";
const BIND_UP_ARROW: &str = r"bindkey -M emacs '^[[A' atuin-up-search
bindkey -M vicmd '^[[A' atuin-up-search-vicmd
bindkey -M viins '^[[A' atuin-up-search-viins
bindkey -M emacs '^[OA' atuin-up-search
bindkey -M vicmd '^[OA' atuin-up-search-vicmd
bindkey -M viins '^[OA' atuin-up-search-viins
bindkey -M vicmd 'k' atuin-up-search-vicmd";
if !disable_ctrl_r {
println!("{BIND_CTRL_R}");
}
if !disable_up_arrow {
println!("{BIND_UP_ARROW}");
}
}
let aliases = store.aliases().await?;
let aliases = atuin_config::shell::zsh::build(&aliases[..]);
println!("{aliases}");
Ok(())
}

View File

@ -1,172 +0,0 @@
use clap::{Parser, ValueEnum};
#[derive(Parser)]
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)]
pub enum Shell {
/// Zsh setup
Zsh,
/// Bash setup
Bash,
/// Fish setup
Fish,
/// Nu setup
Nu,
/// Xonsh setup
Xonsh,
}
impl Cmd {
fn init_zsh(&self) {
let base = include_str!("../shell/atuin.zsh");
println!("{base}");
if std::env::var("ATUIN_NOBIND").is_err() {
const BIND_CTRL_R: &str = r"bindkey -M emacs '^r' atuin-search
bindkey -M viins '^r' atuin-search-viins
bindkey -M vicmd '/' atuin-search";
const BIND_UP_ARROW: &str = r"bindkey -M emacs '^[[A' atuin-up-search
bindkey -M vicmd '^[[A' atuin-up-search-vicmd
bindkey -M viins '^[[A' atuin-up-search-viins
bindkey -M emacs '^[OA' atuin-up-search
bindkey -M vicmd '^[OA' atuin-up-search-vicmd
bindkey -M viins '^[OA' atuin-up-search-viins
bindkey -M vicmd 'k' atuin-up-search-vicmd";
if !self.disable_ctrl_r {
println!("{BIND_CTRL_R}");
}
if !self.disable_up_arrow {
println!("{BIND_UP_ARROW}");
}
}
}
fn init_bash(&self) {
let base = include_str!("../shell/atuin.bash");
let (bind_ctrl_r, bind_up_arrow) = if std::env::var("ATUIN_NOBIND").is_ok() {
(false, false)
} else {
(!self.disable_ctrl_r, !self.disable_up_arrow)
};
println!("__atuin_bind_ctrl_r={bind_ctrl_r}");
println!("__atuin_bind_up_arrow={bind_up_arrow}");
println!("{base}");
}
fn init_fish(&self) {
let full = include_str!("../shell/atuin.fish");
println!("{full}");
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 !self.disable_ctrl_r {
println!("{BIND_CTRL_R}");
}
if !self.disable_up_arrow {
println!("{BIND_UP_ARROW}");
}
println!("if bind -M insert > /dev/null 2>&1");
if !self.disable_ctrl_r {
println!("{BIND_CTRL_R_INS}");
}
if !self.disable_up_arrow {
println!("{BIND_UP_ARROW_INS}");
}
println!("end");
}
}
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}");
}
}
}
fn init_xonsh(&self) {
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 {
(!self.disable_ctrl_r, !self.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}");
}
pub fn run(self) {
match self.shell {
Shell::Zsh => self.init_zsh(),
Shell::Bash => self.init_bash(),
Shell::Fish => self.init_fish(),
Shell::Nu => self.init_nu(),
Shell::Xonsh => self.init_xonsh(),
}
}
}

View File

@ -11,8 +11,6 @@ mod client;
#[cfg(feature = "server")] #[cfg(feature = "server")]
mod server; mod server;
mod init;
mod contributors; mod contributors;
#[derive(Subcommand)] #[derive(Subcommand)]
@ -27,9 +25,6 @@ pub enum AtuinCmd {
#[command(subcommand)] #[command(subcommand)]
Server(server::Cmd), Server(server::Cmd),
/// Output shell setup
Init(init::Cmd),
/// Generate a UUID /// Generate a UUID
Uuid, Uuid,
@ -67,10 +62,6 @@ impl AtuinCmd {
contributors::run(); contributors::run();
Ok(()) Ok(())
} }
Self::Init(init) => {
init.run();
Ok(())
}
Self::Uuid => { Self::Uuid => {
println!("{}", atuin_common::utils::uuid_v7().as_simple()); println!("{}", atuin_common::utils::uuid_v7().as_simple());
Ok(()) Ok(())