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::{eyre, Result};
8use serde::Deserialize;
9use time::OffsetDateTime;
10use uuid::timestamp::{context::NoContext, Timestamp};
11use uuid::Uuid;
12
13use super::{get_histpath, Importer, Loader};
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() && ext == Some("json") {
70 if let Some(data) = load_session(&p)? {
71 sessions.push(data);
72 }
73 }
74 }
75 Ok(sessions)
76}
77
78fn load_session(path: &Path) -> Result<Option<HistoryData>> {
79 let file = File::open(path)?;
80 if file.metadata()?.len() == 0 {
82 return Ok(None);
83 }
84
85 let mut hist_file: HistoryFile = serde_json::from_reader(file)?;
86
87 if let Some(cmd) = hist_file.data.cmds.first() {
90 let seconds = cmd.ts.0.trunc() as u64;
91 let nanos = (cmd.ts.0.fract() * 1_000_000_000_f64) as u32;
92 let ts = Timestamp::from_unix(NoContext, seconds, nanos);
93 hist_file.data.sessionid = Uuid::new_v7(ts).to_string();
94 }
95 Ok(Some(hist_file.data))
96}
97
98#[async_trait]
99impl Importer for Xonsh {
100 const NAME: &'static str = "xonsh";
101
102 async fn new() -> Result<Self> {
103 let xonsh_data_dir = env::var("XONSH_DATA_DIR").ok();
105 let hist_dir = get_histpath(|| xonsh_hist_dir(xonsh_data_dir))?;
106 let sessions = load_sessions(&hist_dir)?;
107 let hostname = get_host_user();
108 Ok(Xonsh { sessions, hostname })
109 }
110
111 async fn entries(&mut self) -> Result<usize> {
112 let total = self.sessions.iter().map(|s| s.cmds.len()).sum();
113 Ok(total)
114 }
115
116 async fn load(self, loader: &mut impl Loader) -> Result<()> {
117 for session in self.sessions {
118 for cmd in session.cmds {
119 let (start, end) = cmd.ts;
120 let ts_nanos = (start * 1_000_000_000_f64) as i128;
121 let timestamp = OffsetDateTime::from_unix_timestamp_nanos(ts_nanos)?;
122
123 let duration = (end - start) * 1_000_000_000_f64;
124
125 match cmd.rtn {
126 Some(exit) => {
127 let entry = History::import()
128 .timestamp(timestamp)
129 .duration(duration.trunc() as i64)
130 .exit(exit)
131 .command(cmd.inp.trim())
132 .cwd(cmd.cwd)
133 .session(session.sessionid.clone())
134 .hostname(self.hostname.clone());
135 loader.push(entry.build().into()).await?;
136 }
137 None => {
138 let entry = History::import()
139 .timestamp(timestamp)
140 .duration(duration.trunc() as i64)
141 .command(cmd.inp.trim())
142 .cwd(cmd.cwd)
143 .session(session.sessionid.clone())
144 .hostname(self.hostname.clone());
145 loader.push(entry.build().into()).await?;
146 }
147 }
148 }
149 }
150 Ok(())
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use time::macros::datetime;
157
158 use super::*;
159
160 use crate::history::History;
161 use crate::import::tests::TestLoader;
162
163 #[test]
164 fn test_hist_dir_xonsh() {
165 let hist_dir = xonsh_hist_dir(Some("/home/user/xonsh_data".to_string())).unwrap();
166 assert_eq!(
167 hist_dir,
168 PathBuf::from("/home/user/xonsh_data/history_json")
169 );
170 }
171
172 #[tokio::test]
173 async fn test_import() {
174 let dir = PathBuf::from("tests/data/xonsh");
175 let sessions = load_sessions(&dir).unwrap();
176 let hostname = "box:user".to_string();
177 let xonsh = Xonsh { sessions, hostname };
178
179 let mut loader = TestLoader::default();
180 xonsh.load(&mut loader).await.unwrap();
181 loader.buf.sort_by_key(|h| h.timestamp);
183 for (actual, expected) in loader.buf.iter().zip(expected_hist_entries().iter()) {
184 assert_eq!(actual.timestamp, expected.timestamp);
185 assert_eq!(actual.command, expected.command);
186 assert_eq!(actual.cwd, expected.cwd);
187 assert_eq!(actual.exit, expected.exit);
188 assert_eq!(actual.duration, expected.duration);
189 assert_eq!(actual.hostname, expected.hostname);
190 }
191 }
192
193 fn expected_hist_entries() -> [History; 4] {
194 [
195 History::import()
196 .timestamp(datetime!(2024-02-6 04:17:59.478272256 +00:00:00))
197 .command("echo hello world!".to_string())
198 .cwd("/home/user/Documents/code/atuin".to_string())
199 .exit(0)
200 .duration(4651069)
201 .hostname("box:user".to_string())
202 .build()
203 .into(),
204 History::import()
205 .timestamp(datetime!(2024-02-06 04:18:01.70632832 +00:00:00))
206 .command("ls -l".to_string())
207 .cwd("/home/user/Documents/code/atuin".to_string())
208 .exit(0)
209 .duration(21288633)
210 .hostname("box:user".to_string())
211 .build()
212 .into(),
213 History::import()
214 .timestamp(datetime!(2024-02-06 17:41:31.142515968 +00:00:00))
215 .command("false".to_string())
216 .cwd("/home/user/Documents/code/atuin/atuin-client".to_string())
217 .exit(1)
218 .duration(10269403)
219 .hostname("box:user".to_string())
220 .build()
221 .into(),
222 History::import()
223 .timestamp(datetime!(2024-02-06 17:41:32.271584 +00:00:00))
224 .command("exit".to_string())
225 .cwd("/home/user/Documents/code/atuin/atuin-client".to_string())
226 .exit(0)
227 .duration(4259347)
228 .hostname("box:user".to_string())
229 .build()
230 .into(),
231 ]
232 }
233}