intelli_shell/utils/
history.rs

1use std::{fs, io::ErrorKind, process::Command};
2
3use color_eyre::eyre::{Context, Report};
4use directories::BaseDirs;
5
6use crate::{
7    cli::HistorySource,
8    errors::{Result, UserFacingError},
9};
10
11/// Reads command history from a specified shell or history manager.
12///
13/// This function dispatches to a specific reader based on the `source` enum variant.
14/// Each reader attempts to find the history in its default location.
15pub fn read_history(source: HistorySource) -> Result<String> {
16    match source {
17        HistorySource::Bash => read_bash_history(),
18        HistorySource::Zsh => read_zsh_history(),
19        HistorySource::Fish => read_fish_history(),
20        HistorySource::Powershell => read_powershell_history(),
21        HistorySource::Atuin => read_atuin_history(),
22    }
23}
24
25fn read_bash_history() -> Result<String> {
26    read_history_from_home(&[".bash_history"])
27}
28
29fn read_zsh_history() -> Result<String> {
30    read_history_from_home(&[".zsh_history"])
31}
32
33fn read_fish_history() -> Result<String> {
34    read_history_from_home(&[".local", "share", "fish", "fish_history"])
35}
36
37fn read_powershell_history() -> Result<String> {
38    let path = if cfg!(windows) {
39        vec![
40            "AppData",
41            "Roaming",
42            "Microsoft",
43            "Windows",
44            "PowerShell",
45            "PSReadLine",
46            "ConsoleHost_history.txt",
47        ]
48    } else {
49        vec![".local", "share", "powershell", "PSReadLine", "ConsoleHost_history.txt"]
50    };
51    read_history_from_home(&path)
52}
53
54fn read_atuin_history() -> Result<String> {
55    // Execute the `atuin history list` command
56    let output = Command::new("atuin")
57        .arg("history")
58        .arg("list")
59        .arg("--cmd-only")
60        .output();
61
62    match output {
63        Ok(output) if output.status.success() => {
64            Ok(String::from_utf8(output.stdout).wrap_err("Couldn't read atuin output")?)
65        }
66        Ok(output) => {
67            let stderr = String::from_utf8_lossy(&output.stderr);
68            if !stderr.is_empty() {
69                tracing::error!("Couldn't execute atuin: {stderr}");
70            }
71            Err(UserFacingError::HistoryAtuinFailed.into())
72        }
73        Err(err) if err.kind() == ErrorKind::NotFound => Err(UserFacingError::HistoryAtuinNotFound.into()),
74        Err(err) => Err(Report::from(err).wrap_err("Couldn't run atuin").into()),
75    }
76}
77
78/// A helper function to construct a path from the user's home directory and read the file's content
79fn read_history_from_home(path_segments: &[&str]) -> Result<String> {
80    let mut path = BaseDirs::new()
81        .ok_or(UserFacingError::HistoryHomeDirNotFound)?
82        .home_dir()
83        .to_path_buf();
84    for segment in path_segments {
85        path.push(segment);
86    }
87    fs::read_to_string(&path).map_err(|err| {
88        if err.kind() == ErrorKind::NotFound {
89            UserFacingError::HistoryFileNotFound(path.to_string_lossy().into_owned()).into()
90        } else if err.kind() == ErrorKind::PermissionDenied {
91            UserFacingError::FileNotAccessible("read").into()
92        } else {
93            Report::from(err)
94                .wrap_err(format!("Couldn't read history file {}", path.display()))
95                .into()
96        }
97    })
98}