intelli_shell/utils/
history.rs1use 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
11pub 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 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 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
102fn 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}