mirror of
https://github.com/atuinsh/atuin.git
synced 2024-11-25 17:54:55 +01:00
feat(dotfiles): add alias import (#1938)
* feat(dotfiles): add alias import * things * clippy clappy
This commit is contained in:
parent
0ab9f4d9ff
commit
7ced31c354
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -285,6 +285,8 @@ dependencies = [
|
||||
"semver",
|
||||
"serde",
|
||||
"sqlx",
|
||||
"sysinfo",
|
||||
"thiserror",
|
||||
"time",
|
||||
"typed-builder",
|
||||
"uuid",
|
||||
|
@ -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"
|
||||
|
||||
|
@ -54,4 +54,5 @@ macro_rules! new_uuid {
|
||||
|
||||
pub mod api;
|
||||
pub mod record;
|
||||
pub mod shell;
|
||||
pub mod utils;
|
||||
|
66
atuin-common/src/shell.rs
Normal file
66
atuin-common/src/shell.rs
Normal file
@ -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()
|
||||
}
|
@ -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'");
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user