use super::{ShellPreferences, CommandFrequency, CommandHistoryStats, WorkflowPatterns};
use anyhow::Result;
use chrono::{DateTime, Utc, Duration};
use std::collections::HashMap;
use tracing::{info, debug};
pub struct ShellHistoryAnalyzer;
impl ShellHistoryAnalyzer {
pub fn new() -> Self {
Self
}
pub async fn analyze(&self) -> Result<ShellPreferences> {
let shell = self.detect_shell()?;
let history_path = self.get_history_path(&shell);
let mut prefs = ShellPreferences {
shell_type: shell,
shell_config_path: self.get_config_path(),
prompt_style: None,
favorite_commands: Vec::new(),
command_history_stats: CommandHistoryStats::default(),
};
if let Some(path) = &history_path {
if let Ok(stats) = self.analyze_history_file(path).await {
prefs.command_history_stats = stats;
}
}
prefs.prompt_style = self.detect_prompt_style().await.ok();
Ok(prefs)
}
fn detect_shell(&self) -> Result<String> {
if let Ok(shell) = std::env::var("SHELL") {
let shell_name = std::path::Path::new(&shell)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown")
.to_string();
return Ok(shell_name);
}
if std::env::var("ZSH_VERSION").is_ok() {
Ok("zsh".to_string())
} else if std::env::var("BASH_VERSION").is_ok() {
Ok("bash".to_string())
} else {
Ok("sh".to_string())
}
}
fn get_history_path(&self, shell: &str) -> Option<String> {
let home = dirs::home_dir()?;
match shell {
"zsh" => Some(home.join(".zsh_history").to_string_lossy().to_string()),
"bash" => Some(home.join(".bash_history").to_string_lossy().to_string()),
"fish" => Some(home.join(".local/share/fish/fish_history").to_string_lossy().to_string()),
_ => None,
}
}
fn get_config_path(&self) -> Option<String> {
let home = dirs::home_dir()?;
let shell = self.detect_shell().ok()?;
match shell.as_str() {
"zsh" => Some(home.join(".zshrc").to_string_lossy().to_string()),
"bash" => Some(home.join(".bashrc").to_string_lossy().to_string()),
"fish" => Some(home.join(".config/fish/config.fish").to_string_lossy().to_string()),
_ => None,
}
}
async fn analyze_history_file(&self, path: &str) -> Result<CommandHistoryStats> {
use tokio::fs;
let content = fs::read_to_string(path).await?;
let lines: Vec<&str> = content.lines().collect();
let mut command_counts: HashMap<String, i64> = HashMap::new();
let mut hourly_distribution: HashMap<u8, i64> = HashMap::new();
for line in &lines {
let command = line.split_whitespace().next().unwrap_or("");
if !command.is_empty() && !command.starts_with('#') {
*command_counts.entry(command.to_string()).or_insert(0) += 1;
}
}
let total_commands = lines.len() as i64;
let unique_commands = command_counts.len() as i64;
let mut commands: Vec<_> = command_counts.into_iter().collect();
commands.sort_by(|a, b| b.1.cmp(&a.1));
let most_used: Vec<String> = commands.into_iter()
.take(20)
.map(|(cmd, _)| cmd)
.collect();
let typical_hours: Vec<u8> = (9..=18).collect();
Ok(CommandHistoryStats {
total_commands,
unique_commands,
most_used_commands: most_used,
typical_working_hours: typical_hours,
})
}
async fn detect_prompt_style(&self) -> Result<String> {
if let Some(config_path) = self.get_config_path() {
if let Ok(content) = tokio::fs::read_to_string(&config_path).await {
if content.contains("starship") {
return Ok("starship".to_string());
} else if content.contains("powerlevel10k") || content.contains("p10k") {
return Ok("powerlevel10k".to_string());
} else if content.contains("PS1=") && content.contains("\\u@\\h") {
return Ok("classic".to_string());
} else if content.contains("oh-my-zsh") {
return Ok("oh-my-zsh".to_string());
}
}
}
Ok("default".to_string())
}
pub async fn extract_workflow_patterns(&self) -> Result<WorkflowPatterns> {
let mut patterns = WorkflowPatterns::default();
if let Ok(output) = std::process::Command::new("git")
.args(&["config", "--global", "--list"])
.output()
{
let config = String::from_utf8_lossy(&output.stdout);
if config.contains("init.defaultbranch") {
patterns.typical_branch_names.push("main".to_string());
patterns.typical_branch_names.push("master".to_string());
}
}
patterns.commit_message_patterns = vec![
"feat: ".to_string(),
"fix: ".to_string(),
"docs: ".to_string(),
"refactor: ".to_string(),
"test: ".to_string(),
];
patterns.code_review_checklist = vec![
"Tests pass".to_string(),
"Code follows style guide".to_string(),
"Documentation updated".to_string(),
];
patterns.testing_commands = vec![
"cargo test".to_string(),
"npm test".to_string(),
"pytest".to_string(),
];
Ok(patterns)
}
pub async fn get_frequent_directories(&self) -> Result<Vec<String>> {
let mut dir_counts: HashMap<String, i64> = HashMap::new();
if let Some(history_path) = self.get_history_path(&self.detect_shell()?) {
if let Ok(content) = tokio::fs::read_to_string(&history_path).await {
for line in content.lines() {
if line.contains("cd ") {
let parts: Vec<_> = line.split_whitespace().collect();
if parts.len() >= 2 {
let dir = parts[1..].join(" ");
*dir_counts.entry(dir).or_insert(0) += 1;
}
}
}
}
}
let mut dirs: Vec<_> = dir_counts.into_iter().collect();
dirs.sort_by(|a, b| b.1.cmp(&a.1));
Ok(dirs.into_iter().take(10).map(|(d, _)| d).collect())
}
}