summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEllie Huxtable <ellie@elliehuxtable.com>2024-04-10 13:01:48 +0100
committerGitHub <noreply@github.com>2024-04-10 13:01:48 +0100
commit7ced31c354bdfb2e256de3ecc49bcc4f379f78af (patch)
tree7ea1b639c40ef470a43cf82e8dae834b48285685
parent0ab9f4d9ff545d83dc664b494ecf450750c0f184 (diff)
feat(dotfiles): add alias import (#1938)
* feat(dotfiles): add alias import * things * clippy clappy
-rw-r--r--Cargo.lock2
-rw-r--r--atuin-common/Cargo.toml2
-rw-r--r--atuin-common/src/lib.rs1
-rw-r--r--atuin-common/src/shell.rs66
-rw-r--r--atuin-dotfiles/src/shell.rs100
-rw-r--r--atuin/src/command/client/doctor.rs5
-rw-r--r--atuin/src/command/client/dotfiles/alias.rs19
7 files changed, 192 insertions, 3 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 820152ce..33f17a34 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -285,6 +285,8 @@ dependencies = [
"semver",
"serde",
"sqlx",
+ "sysinfo",
+ "thiserror",
"time",
"typed-builder",
"uuid",
diff --git a/atuin-common/Cargo.toml b/atuin-common/Cargo.toml
index 847cea96..85e41ef6 100644
--- a/atuin-common/Cargo.toml
+++ b/atuin-common/Cargo.toml
@@ -21,6 +21,8 @@ typed-builder = { workspace = true }
eyre = { workspace = true }
sqlx = { workspace = true }
semver = { workspace = true }
+thiserror = { workspace = true }
+sysinfo = "0.30.7"
lazy_static = "1.4.0"
diff --git a/atuin-common/src/lib.rs b/atuin-common/src/lib.rs
index d4513ee0..2d848f6f 100644
--- a/atuin-common/src/lib.rs
+++ b/atuin-common/src/lib.rs
@@ -54,4 +54,5 @@ macro_rules! new_uuid {
pub mod api;
pub mod record;
+pub mod shell;
pub mod utils;
diff --git a/atuin-common/src/shell.rs b/atuin-common/src/shell.rs
new file mode 100644
index 00000000..2c0f3534
--- /dev/null
+++ b/atuin-common/src/shell.rs
@@ -0,0 +1,66 @@
+use sysinfo::{get_current_pid, Process, System};
+use thiserror::Error;
+
+pub enum Shell {
+ Sh,
+ Bash,
+ Fish,
+ Zsh,
+ Xonsh,
+ Nu,
+
+ Unknown,
+}
+
+#[derive(Debug, Error)]
+pub enum ShellError {
+ #[error("shell not supported")]
+ NotSupported,
+
+ #[error("failed to execute shell command: {0}")]
+ ExecError(String),
+}
+
+pub fn shell() -> Shell {
+ let name = shell_name(None);
+
+ match name.as_str() {
+ "bash" => Shell::Bash,
+ "fish" => Shell::Fish,
+ "zsh" => Shell::Zsh,
+ "xonsh" => Shell::Xonsh,
+ "nu" => Shell::Nu,
+ "sh" => Shell::Sh,
+
+ _ => Shell::Unknown,
+ }
+}
+
+impl Shell {
+ /// Returns true if the shell is posix-like
+ /// Note that while fish is not posix compliant, it behaves well enough for our current
+ /// featureset that this does not matter.
+ pub fn is_posixish(&self) -> bool {
+ matches!(self, Shell::Bash | Shell::Fish | Shell::Zsh)
+ }
+}
+
+pub fn shell_name(parent: Option<&Process>) -> String {
+ let sys = System::new_all();
+
+ let parent = if let Some(parent) = parent {
+ parent
+ } else {
+ let process = sys
+ .process(get_current_pid().expect("Failed to get current PID"))
+ .expect("Process with current pid does not exist");
+
+ sys.process(process.parent().expect("Atuin running with no parent!"))
+ .expect("Process with parent pid does not exist")
+ };
+
+ let shell = parent.name().trim().to_lowercase();
+ let shell = shell.strip_prefix('-').unwrap_or(&shell);
+
+ shell.to_string()
+}
diff --git a/atuin-dotfiles/src/shell.rs b/atuin-dotfiles/src/shell.rs
index a69a2d6b..c779cadb 100644
--- a/atuin-dotfiles/src/shell.rs
+++ b/atuin-dotfiles/src/shell.rs
@@ -1,3 +1,10 @@
+use std::{ffi::OsStr, process::Command};
+
+use atuin_common::shell::{shell, shell_name, ShellError};
+use eyre::Result;
+
+use crate::store::AliasStore;
+
pub mod bash;
pub mod fish;
pub mod xonsh;
@@ -8,3 +15,96 @@ pub struct Alias {
pub name: String,
pub value: String,
}
+
+pub fn run_interactive<I, S>(args: I) -> Result<String, ShellError>
+where
+ I: IntoIterator<Item = S>,
+ S: AsRef<OsStr>,
+{
+ let shell = shell_name(None);
+
+ let output = Command::new(shell)
+ .arg("-ic")
+ .args(args)
+ .output()
+ .map_err(|e| ShellError::ExecError(e.to_string()))?;
+
+ Ok(String::from_utf8(output.stdout).unwrap())
+}
+
+pub fn parse_alias(line: &str) -> Alias {
+ let mut parts = line.split('=');
+
+ let name = parts.next().unwrap().to_string();
+ let remaining = parts.collect::<Vec<&str>>().join("=").to_string();
+
+ Alias {
+ name,
+ value: remaining,
+ }
+}
+
+pub fn existing_aliases() -> Result<Vec<Alias>, ShellError> {
+ // this only supports posix-y shells atm
+ if !shell().is_posixish() {
+ return Err(ShellError::NotSupported);
+ }
+
+ // This will return a list of aliases, each on its own line
+ // They will be in the form foo=bar
+ let aliases = run_interactive(["alias"])?;
+ let aliases: Vec<Alias> = aliases.lines().map(parse_alias).collect();
+
+ Ok(aliases)
+}
+
+/// Import aliases from the current shell
+/// This will not import aliases already in the store
+/// Returns aliases that were set
+pub async fn import_aliases(store: AliasStore) -> Result<Vec<Alias>> {
+ let shell_aliases = existing_aliases()?;
+ let store_aliases = store.aliases().await?;
+
+ let mut res = Vec::new();
+
+ for alias in shell_aliases {
+ // O(n), but n is small, and imports infrequent
+ // can always make a map
+ if store_aliases.contains(&alias) {
+ continue;
+ }
+
+ res.push(alias.clone());
+ store.set(&alias.name, &alias.value).await?;
+ }
+
+ Ok(res)
+}
+
+#[cfg(test)]
+mod tests {
+ #[test]
+ fn test_parse_simple_alias() {
+ let alias = super::parse_alias("foo=bar");
+ assert_eq!(alias.name, "foo");
+ assert_eq!(alias.value, "bar");
+ }
+
+ #[test]
+ fn test_parse_quoted_alias() {
+ let alias = super::parse_alias("emacs='TERM=xterm-24bits emacs -nw'");
+ assert_eq!(alias.name, "emacs");
+ assert_eq!(alias.value, "'TERM=xterm-24bits emacs -nw'");
+
+ let git_alias = super::parse_alias("gwip='git add -A; git rm $(git ls-files --deleted) 2> /dev/null; git commit --no-verify --no-gpg-sign --message \"--wip-- [skip ci]\"'");
+ assert_eq!(git_alias.name, "gwip");
+ assert_eq!(git_alias.value, "'git add -A; git rm $(git ls-files --deleted) 2> /dev/null; git commit --no-verify --no-gpg-sign --message \"--wip-- [skip ci]\"'");
+ }
+
+ #[test]
+ fn test_parse_quoted_alias_equals() {
+ let alias = super::parse_alias("emacs='TERM=xterm-24bits emacs -nw --foo=bar'");
+ assert_eq!(alias.name, "emacs");
+ assert_eq!(alias.value, "'TERM=xterm-24bits emacs -nw --foo=bar'");
+ }
+}
diff --git a/atuin/src/command/client/doctor.rs b/atuin/src/command/client/doctor.rs
index 5c144fed..21981b4b 100644
--- a/atuin/src/command/client/doctor.rs
+++ b/atuin/src/command/client/doctor.rs
@@ -2,6 +2,7 @@ use std::process::Command;
use std::{env, path::PathBuf, str::FromStr};
use atuin_client::settings::Settings;
+use atuin_common::shell::shell_name;
use colored::Colorize;
use eyre::Result;
use serde::{Deserialize, Serialize};
@@ -144,9 +145,7 @@ impl ShellInfo {
.process(process.parent().expect("Atuin running with no parent!"))
.expect("Process with parent pid does not exist");
- let shell = parent.name().trim().to_lowercase();
- let shell = shell.strip_prefix('-').unwrap_or(&shell);
- let name = shell.to_string();
+ let name = shell_name(Some(parent));
let plugins = ShellInfo::plugins(name.as_str(), parent);
diff --git a/atuin/src/command/client/dotfiles/alias.rs b/atuin/src/command/client/dotfiles/alias.rs
index 60de1f84..6456a8b0 100644
--- a/atuin/src/command/client/dotfiles/alias.rs
+++ b/atuin/src/command/client/dotfiles/alias.rs
@@ -8,9 +8,17 @@ use atuin_dotfiles::{shell::Alias, store::AliasStore};
#[derive(Subcommand, Debug)]
#[command(infer_subcommands = true)]
pub enum Cmd {
+ /// Set an alias
Set { name: String, value: String },
+
+ /// Delete an alias
Delete { name: String },
+
+ /// List all aliases
List,
+
+ /// Import aliases set in the current shell
+ Import,
}
impl Cmd {
@@ -53,6 +61,16 @@ impl Cmd {
Ok(())
}
+ async fn import(&self, store: AliasStore) -> Result<()> {
+ let aliases = atuin_dotfiles::shell::import_aliases(store).await?;
+
+ for i in aliases {
+ println!("Importing {}={}", i.name, i.value);
+ }
+
+ Ok(())
+ }
+
pub async fn run(&self, settings: &Settings, store: SqliteStore) -> Result<()> {
if !settings.dotfiles.enabled {
eprintln!("Dotfiles are not enabled. Add\n\n[dotfiles]\nenabled = true\n\nto your configuration file to enable them.\n");
@@ -71,6 +89,7 @@ impl Cmd {
Self::Set { name, value } => self.set(alias_store, name.clone(), value.clone()).await,
Self::Delete { name } => self.delete(alias_store, name.clone()).await,
Self::List => self.list(alias_store).await,
+ Self::Import => self.import(alias_store).await,
}
}
}