diff --git a/crates/atuin-client/src/settings.rs b/crates/atuin-client/src/settings.rs index 4df404c46d8..e819b42980f 100644 --- a/crates/atuin-client/src/settings.rs +++ b/crates/atuin-client/src/settings.rs @@ -306,6 +306,8 @@ pub struct Stats { pub common_subcommands: Vec, // kubectl, commands we should consider subcommands for #[serde(default = "Stats::ignored_commands_default")] pub ignored_commands: Vec, // cd, ls, etc. commands we want to completely hide from stats + #[serde(default)] + pub command_aliases: HashMap, // e.g., "g" -> "git", "k" -> "kubectl" } impl Stats { @@ -353,6 +355,7 @@ impl Default for Stats { common_prefix: Self::common_prefix_default(), common_subcommands: Self::common_subcommands_default(), ignored_commands: Self::ignored_commands_default(), + command_aliases: HashMap::new(), } } } diff --git a/crates/atuin-history/src/stats.rs b/crates/atuin-history/src/stats.rs index fedb1487b26..c9545c6bfd0 100644 --- a/crates/atuin-history/src/stats.rs +++ b/crates/atuin-history/src/stats.rs @@ -239,40 +239,69 @@ pub fn pretty_print(stats: Stats, ngram_size: usize, theme: &Theme) { println!("Unique commands: {}", stats.unique_commands); } +fn resolve_alias(alias_keys: &[(&String, &String)], command: &str) -> String { + // try longest alias first + for (key, value) in alias_keys { + if command == key.as_str() { + return value.to_string(); + } + if let Some(rest) = command.strip_prefix(key.as_str()) + && rest.starts_with(char::is_whitespace) + { + return format!("{value}{rest}"); + } + } + command.to_string() +} + pub fn compute( settings: &Settings, history: &[History], count: usize, ngram_size: usize, ) -> Option { - let mut commands = HashSet::<&str>::with_capacity(history.len()); + let mut commands = HashSet::::with_capacity(history.len()); let mut total_unignored = 0; - let mut prefixes = HashMap::, usize>::with_capacity(history.len()); + let mut prefixes = HashMap::, usize>::with_capacity(history.len()); + + let mut alias_keys: Vec<(&String, &String)> = settings.stats.command_aliases.iter().collect(); + alias_keys.sort_by_key(|(k, _)| std::cmp::Reverse(k.len())); for i in history { // just in case it somehow has a leading tab or space or something (legacy atuin didn't ignore space prefixes) let command = strip_leading_env_vars(i.command.trim()); - let prefix = interesting_command(settings, command); + + // resolve aliases first + let command = resolve_alias(&alias_keys, command); + + let prefix = interesting_command(settings, &command); if settings.stats.ignored_commands.iter().any(|c| c == prefix) { continue; } total_unignored += 1; - commands.insert(command); + commands.insert(command.clone()); - split_at_pipe(command) + split_at_pipe(&command) .iter() .map(|l| { - let command = l.trim(); - commands.insert(command); + let command = resolve_alias(&alias_keys, l.trim()); + commands.insert(command.clone()); command }) .collect::>() + .iter() + .map(|s| s.as_str()) + .collect::>() .windows(ngram_size) .for_each(|w| { *prefixes - .entry(w.iter().map(|c| interesting_command(settings, c)).collect()) + .entry( + w.iter() + .map(|c| interesting_command(settings, c).to_string()) + .collect(), + ) .or_default() += 1; }); } @@ -292,7 +321,7 @@ pub fn compute( total_commands: total_unignored, top: top .into_iter() - .map(|t| (t.0.into_iter().map(|s| s.to_string()).collect(), t.1)) + .map(|t| (t.0.into_iter().collect(), t.1)) .collect(), }) }