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::Nushell => read_nushell_history(),
22        HistorySource::Atuin => read_atuin_history(),
23    }
24}
25
26fn read_bash_history() -> Result<String> {
27    read_history_from_home(&[".bash_history"])
28}
29
30fn read_zsh_history() -> Result<String> {
31    read_history_from_home(&[".zsh_history"])
32}
33
34fn read_fish_history() -> Result<String> {
35    read_history_from_home(&[".local", "share", "fish", "fish_history"])
36}
37
38fn read_powershell_history() -> Result<String> {
39    let path = if cfg!(windows) {
40        vec![
41            "AppData",
42            "Roaming",
43            "Microsoft",
44            "Windows",
45            "PowerShell",
46            "PSReadLine",
47            "ConsoleHost_history.txt",
48        ]
49    } else {
50        vec![".local", "share", "powershell", "PSReadLine", "ConsoleHost_history.txt"]
51    };
52    read_history_from_home(&path)
53}
54
55fn read_nushell_history() -> Result<String> {
56    // Execute the `nu` command to get a newline-separated list of history entries
57    let output = Command::new("nu")
58        .arg("-c")
59        .arg("history | get command | str join \"\n\"")
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 nu 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 nu: {stderr}");
70            }
71            Err(UserFacingError::HistoryNushellFailed.into())
72        }
73        Err(err) if err.kind() == ErrorKind::NotFound => Err(UserFacingError::HistoryNushellNotFound.into()),
74        Err(err) => Err(Report::from(err).wrap_err("Couldn't run nu").into()),
75    }
76}
77
78fn read_atuin_history() -> Result<String> {
79    // Execute the `atuin history list` command
80    let output = Command::new("atuin")
81        .arg("history")
82        .arg("list")
83        .arg("--cmd-only")
84        .output();
85
86    match output {
87        Ok(output) if output.status.success() => {
88            Ok(String::from_utf8(output.stdout).wrap_err("Couldn't read atuin output")?)
89        }
90        Ok(output) => {
91            let stderr = String::from_utf8_lossy(&output.stderr);
92            if !stderr.is_empty() {
93                tracing::error!("Couldn't execute atuin: {stderr}");
94            }
95            Err(UserFacingError::HistoryAtuinFailed.into())
96        }
97        Err(err) if err.kind() == ErrorKind::NotFound => Err(UserFacingError::HistoryAtuinNotFound.into()),
98        Err(err) => Err(Report::from(err).wrap_err("Couldn't run atuin").into()),
99    }
100}
101
102/// A helper function to construct a path from the user's home directory and read the file's content
103fn read_history_from_home(path_segments: &[&str]) -> Result<String> {
104    let mut path = BaseDirs::new()
105        .ok_or(UserFacingError::HistoryHomeDirNotFound)?
106        .home_dir()
107        .to_path_buf();
108    for segment in path_segments {
109        path.push(segment);
110    }
111    fs::read_to_string(&path).map_err(|err| {
112        if err.kind() == ErrorKind::NotFound {
113            UserFacingError::HistoryFileNotFound(path.to_string_lossy().into_owned()).into()
114        } else if err.kind() == ErrorKind::PermissionDenied {
115            UserFacingError::FileNotAccessible("read").into()
116        } else {
117            Report::from(err)
118                .wrap_err(format!("Couldn't read history file {}", path.display()))
119                .into()
120        }
121    })
122}