diff options
author | Ellie Huxtable <ellie@elliehuxtable.com> | 2024-04-10 13:01:48 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-04-10 13:01:48 +0100 |
commit | 7ced31c354bdfb2e256de3ecc49bcc4f379f78af (patch) | |
tree | 7ea1b639c40ef470a43cf82e8dae834b48285685 | |
parent | 0ab9f4d9ff545d83dc664b494ecf450750c0f184 (diff) |
feat(dotfiles): add alias import (#1938)
* feat(dotfiles): add alias import
* things
* clippy clappy
-rw-r--r-- | Cargo.lock | 2 | ||||
-rw-r--r-- | atuin-common/Cargo.toml | 2 | ||||
-rw-r--r-- | atuin-common/src/lib.rs | 1 | ||||
-rw-r--r-- | atuin-common/src/shell.rs | 66 | ||||
-rw-r--r-- | atuin-dotfiles/src/shell.rs | 100 | ||||
-rw-r--r-- | atuin/src/command/client/doctor.rs | 5 | ||||
-rw-r--r-- | atuin/src/command/client/dotfiles/alias.rs | 19 |
7 files changed, 192 insertions, 3 deletions
@@ -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, } } } |