mirror of
https://github.com/atuinsh/atuin.git
synced 2024-11-22 08:13:57 +01:00
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:
parent
f8d01eef99
commit
20f3296468
14
Cargo.lock
generated
14
Cargo.lock
generated
@ -185,6 +185,7 @@ dependencies = [
|
||||
"async-trait",
|
||||
"atuin-client",
|
||||
"atuin-common",
|
||||
"atuin-config",
|
||||
"atuin-server",
|
||||
"atuin-server-postgres",
|
||||
"base64 0.21.7",
|
||||
@ -286,6 +287,19 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[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"
|
||||
|
@ -5,7 +5,8 @@ members = [
|
||||
"atuin-server",
|
||||
"atuin-server-postgres",
|
||||
"atuin-server-database",
|
||||
"atuin-common",
|
||||
"atuin-common",
|
||||
"atuin-config",
|
||||
]
|
||||
|
||||
resolver = "2"
|
||||
|
23
atuin-config/Cargo.toml
Normal file
23
atuin-config/Cargo.toml
Normal 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
2
atuin-config/src/lib.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod shell;
|
||||
pub mod store;
|
10
atuin-config/src/shell.rs
Normal file
10
atuin-config/src/shell.rs
Normal 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,
|
||||
}
|
12
atuin-config/src/shell/bash.rs
Normal file
12
atuin-config/src/shell/bash.rs
Normal 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
|
||||
}
|
12
atuin-config/src/shell/fish.rs
Normal file
12
atuin-config/src/shell/fish.rs
Normal 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
|
||||
}
|
12
atuin-config/src/shell/xonsh.rs
Normal file
12
atuin-config/src/shell/xonsh.rs
Normal 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
|
||||
}
|
12
atuin-config/src/shell/zsh.rs
Normal file
12
atuin-config/src/shell/zsh.rs
Normal 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
310
atuin-config/src/store.rs
Normal 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")
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@ -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"
|
||||
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
42
atuin/src/command/client/config/alias.rs
Normal file
42
atuin/src/command/client/config/alias.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
5
atuin/src/command/client/default_config.rs
Normal file
5
atuin/src/command/client/default_config.rs
Normal file
@ -0,0 +1,5 @@
|
||||
use atuin_client::settings::Settings;
|
||||
|
||||
pub fn run() {
|
||||
println!("{}", Settings::example_config());
|
||||
}
|
112
atuin/src/command/client/init.rs
Normal file
112
atuin/src/command/client/init.rs
Normal 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(())
|
||||
}
|
||||
}
|
23
atuin/src/command/client/init/bash.rs
Normal file
23
atuin/src/command/client/init/bash.rs
Normal 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(())
|
||||
}
|
42
atuin/src/command/client/init/fish.rs
Normal file
42
atuin/src/command/client/init/fish.rs
Normal 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(())
|
||||
}
|
28
atuin/src/command/client/init/xonsh.rs
Normal file
28
atuin/src/command/client/init/xonsh.rs
Normal 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(())
|
||||
}
|
36
atuin/src/command/client/init/zsh.rs
Normal file
36
atuin/src/command/client/init/zsh.rs
Normal 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(())
|
||||
}
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
@ -11,8 +11,6 @@ mod client;
|
||||
#[cfg(feature = "server")]
|
||||
mod server;
|
||||
|
||||
mod init;
|
||||
|
||||
mod contributors;
|
||||
|
||||
#[derive(Subcommand)]
|
||||
@ -27,9 +25,6 @@ pub enum AtuinCmd {
|
||||
#[command(subcommand)]
|
||||
Server(server::Cmd),
|
||||
|
||||
/// Output shell setup
|
||||
Init(init::Cmd),
|
||||
|
||||
/// Generate a UUID
|
||||
Uuid,
|
||||
|
||||
@ -67,10 +62,6 @@ impl AtuinCmd {
|
||||
contributors::run();
|
||||
Ok(())
|
||||
}
|
||||
Self::Init(init) => {
|
||||
init.run();
|
||||
Ok(())
|
||||
}
|
||||
Self::Uuid => {
|
||||
println!("{}", atuin_common::utils::uuid_v7().as_simple());
|
||||
Ok(())
|
||||
|
Loading…
Reference in New Issue
Block a user