atuin_client/import/
xonsh.rs1use std::env;
2use std::fs::{self, File};
3use std::path::{Path, PathBuf};
4
5use async_trait::async_trait;
6use directories::BaseDirs;
7use eyre::{Result, eyre};
8use serde::Deserialize;
9use time::OffsetDateTime;
10use uuid::Uuid;
11use uuid::timestamp::{Timestamp, context::NoContext};
12
13use super::{Importer, Loader, get_histdir_path};
14use crate::history::History;
15use crate::utils::get_host_user;
16
17#[derive(Debug, Deserialize)]
20struct HistoryFile {
21 data: HistoryData,
22}
23
24#[derive(Debug, Deserialize)]
25struct HistoryData {
26 sessionid: String,
27 cmds: Vec<HistoryCmd>,
28}
29
30#[derive(Debug, Deserialize)]
31struct HistoryCmd {
32 cwd: String,
33 inp: String,
34 rtn: Option<i64>,
35 ts: (f64, f64),
36}
37
38#[derive(Debug)]
39pub struct Xonsh {
40 sessions: Vec<HistoryData>,
42 hostname: String,
43}
44
45fn xonsh_hist_dir(xonsh_data_dir: Option<String>) -> Result<PathBuf> {
46 if let Some(d) = xonsh_data_dir {
48 let mut path = PathBuf::from(d);
49 path.push("history_json");
50 return Ok(path);
51 }
52
53 let base = BaseDirs::new().ok_or_else(|| eyre!("Could not determine home directory"))?;
55
56 let hist_dir = base.data_dir().join("xonsh/history_json");
57 if hist_dir.exists() || cfg!(test) {
58 Ok(hist_dir)
59 } else {
60 Err(eyre!("Could not find xonsh history files"))
61 }
62}
63
64fn load_sessions(hist_dir: &Path) -> Result<Vec<HistoryData>> {
65 let mut sessions = vec![];
66 for entry in fs::read_dir(hist_dir)? {
67 let p = entry?.path();
68 let ext = p.extension().and_then(|e| e.to_str());
69 if p.is_file()
70 && ext == Some("json")
71 && let Some(data) = load_session(&p)?
72 {
73 sessions.push(data);
74 }
75 }
76 Ok(sessions)
77}
78
79fn load_session(path: &Path) -> Result<Option<HistoryData>> {
80 let file = File::open(path)?;
81 if file.metadata()?.len() == 0 {
83 return Ok(None);
84 }
85
86 let mut hist_file: HistoryFile = serde_json::from_reader(file)?;
87
88 if let Some(cmd) = hist_file.data.cmds.first() {
91 let seconds = cmd.ts.0.trunc() as u64;
92 let nanos = (cmd.ts.0.fract() * 1_000_000_000_f64) as u32;
93 let ts = Timestamp::from_unix(NoContext, seconds, nanos);
94 hist_file.data.sessionid = Uuid::new_v7(ts).to_string();
95 }
96 Ok(Some(hist_file.data))
97}
98
99#[async_trait]
100impl Importer for Xonsh {
101 const NAME: &'static str = "xonsh";
102
103 async fn new() -> Result<Self> {
104 let xonsh_data_dir = env::var("XONSH_DATA_DIR").ok();
106 let hist_dir = get_histdir_path(|| xonsh_hist_dir(xonsh_data_dir))?;
107 let sessions = load_sessions(&hist_dir)?;
108 let hostname = get_host_user();
109 Ok(Xonsh { sessions, hostname })
110 }
111
112 async fn entries(&mut self) -> Result<usize> {
113 let total = self.sessions.iter().map(|s| s.cmds.len()).sum();
114 Ok(total)
115 }
116
117 async fn load(self, loader: &mut impl Loader) -> Result<()> {
118 for session in self.sessions {
119 for cmd in session.cmds {
120 let (start, end) = cmd.ts;
121 let ts_nanos = (start * 1_000_000_000_f64) as i128;
122 let timestamp = OffsetDateTime::from_unix_timestamp_nanos(ts_nanos)?;
123
124 let duration = (end - start) * 1_000_000_000_f64;
125
126 match cmd.rtn {
127 Some(exit) => {
128 let entry = History::import()
129 .timestamp(timestamp)
130 .duration(duration.trunc() as i64)
131 .exit(exit)
132 .command(cmd.inp.trim())
133 .cwd(cmd.cwd)
134 .session(session.sessionid.clone())
135 .hostname(self.hostname.clone());
136 loader.push(entry.build().into()).await?;
137 }
138 None => {
139 let entry = History::import()
140 .timestamp(timestamp)
141 .duration(duration.trunc() as i64)
142 .command(cmd.inp.trim())
143 .cwd(cmd.cwd)
144 .session(session.sessionid.clone())
145 .hostname(self.hostname.clone());
146 loader.push(entry.build().into()).await?;
147 }
148 }
149 }
150 }
151 Ok(())
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use time::macros::datetime;
158
159 use super::*;
160
161 use crate::history::History;
162 use crate::import::tests::TestLoader;
163
164 #[test]
165 fn test_hist_dir_xonsh() {
166 let hist_dir = xonsh_hist_dir(Some("/home/user/xonsh_data".to_string())).unwrap();
167 assert_eq!(
168 hist_dir,
169 PathBuf::from("/home/user/xonsh_data/history_json")
170 );
171 }
172
173 #[tokio::test]
174 async fn test_import() {
175 let dir = PathBuf::from("tests/data/xonsh");
176 let sessions = load_sessions(&dir).unwrap();
177 let hostname = "box:user".to_string();
178 let xonsh = Xonsh { sessions, hostname };
179
180 let mut loader = TestLoader::default();
181 xonsh.load(&mut loader).await.unwrap();
182 loader.buf.sort_by_key(|h| h.timestamp);
184 for (actual, expected) in loader.buf.iter().zip(expected_hist_entries().iter()) {
185 assert_eq!(actual.timestamp, expected.timestamp);
186 assert_eq!(actual.command, expected.command);
187 assert_eq!(actual.cwd, expected.cwd);
188 assert_eq!(actual.exit, expected.exit);
189 assert_eq!(actual.duration, expected.duration);
190 assert_eq!(actual.hostname, expected.hostname);
191 }
192 }
193
194 fn expected_hist_entries() -> [History; 4] {
195 [
196 History::import()
197 .timestamp(datetime!(2024-02-6 04:17:59.478272256 +00:00:00))
198 .command("echo hello world!".to_string())
199 .cwd("/home/user/Documents/code/atuin".to_string())
200 .exit(0)
201 .duration(4651069)
202 .hostname("box:user".to_string())
203 .build()
204 .into(),
205 History::import()
206 .timestamp(datetime!(2024-02-06 04:18:01.70632832 +00:00:00))
207 .command("ls -l".to_string())
208 .cwd("/home/user/Documents/code/atuin".to_string())
209 .exit(0)
210 .duration(21288633)
211 .hostname("box:user".to_string())
212 .build()
213 .into(),
214 History::import()
215 .timestamp(datetime!(2024-02-06 17:41:31.142515968 +00:00:00))
216 .command("false".to_string())
217 .cwd("/home/user/Documents/code/atuin/atuin-client".to_string())
218 .exit(1)
219 .duration(10269403)
220 .hostname("box:user".to_string())
221 .build()
222 .into(),
223 History::import()
224 .timestamp(datetime!(2024-02-06 17:41:32.271584 +00:00:00))
225 .command("exit".to_string())
226 .cwd("/home/user/Documents/code/atuin/atuin-client".to_string())
227 .exit(0)
228 .duration(4259347)
229 .hostname("box:user".to_string())
230 .build()
231 .into(),
232 ]
233 }
234}