atuin_client/import/
zsh.rs1use std::borrow::Cow;
5use std::path::PathBuf;
6
7use async_trait::async_trait;
8use directories::UserDirs;
9use eyre::{Result, eyre};
10use time::OffsetDateTime;
11
12use super::{Importer, Loader, get_histfile_path, unix_byte_lines};
13use crate::history::History;
14use crate::import::read_to_end;
15
16#[derive(Debug)]
17pub struct Zsh {
18 bytes: Vec<u8>,
19}
20
21fn default_histpath() -> Result<PathBuf> {
22 let user_dirs = UserDirs::new().ok_or_else(|| eyre!("could not find user directories"))?;
27 let home_dir = user_dirs.home_dir();
28
29 let mut candidates = [".zhistory", ".zsh_history", ".histfile"].iter();
30 loop {
31 match candidates.next() {
32 Some(candidate) => {
33 let histpath = home_dir.join(candidate);
34 if histpath.exists() {
35 break Ok(histpath);
36 }
37 }
38 None => {
39 break Err(eyre!(
40 "Could not find history file. Try setting and exporting $HISTFILE"
41 ));
42 }
43 }
44 }
45}
46
47#[async_trait]
48impl Importer for Zsh {
49 const NAME: &'static str = "zsh";
50
51 async fn new() -> Result<Self> {
52 let bytes = read_to_end(get_histfile_path(default_histpath)?)?;
53 Ok(Self { bytes })
54 }
55
56 async fn entries(&mut self) -> Result<usize> {
57 Ok(super::count_lines(&self.bytes))
58 }
59
60 async fn load(self, h: &mut impl Loader) -> Result<()> {
61 let now = OffsetDateTime::now_utc();
62 let mut line = String::new();
63
64 let mut counter = 0;
65 for b in unix_byte_lines(&self.bytes) {
66 let s = match unmetafy(b) {
67 Some(s) => s,
68 _ => continue, };
70
71 if let Some(s) = s.strip_suffix('\\') {
72 line.push_str(s);
73 line.push_str("\\\n");
74 } else {
75 line.push_str(&s);
76 let command = std::mem::take(&mut line);
77
78 if let Some(command) = command.strip_prefix(": ") {
79 counter += 1;
80 h.push(parse_extended(command, counter)).await?;
81 } else {
82 let offset = time::Duration::seconds(counter);
83 counter += 1;
84
85 let imported = History::import()
86 .timestamp(now - offset)
88 .command(command.trim_end().to_string());
89
90 h.push(imported.build().into()).await?;
91 }
92 }
93 }
94
95 Ok(())
96 }
97}
98
99fn parse_extended(line: &str, counter: i64) -> History {
100 let (time, duration) = line.split_once(':').unwrap();
101 let (duration, command) = duration.split_once(';').unwrap();
102
103 let time = time
104 .parse::<i64>()
105 .ok()
106 .and_then(|t| OffsetDateTime::from_unix_timestamp(t).ok())
107 .unwrap_or_else(OffsetDateTime::now_utc)
108 + time::Duration::milliseconds(counter);
109
110 let duration = duration.parse::<i64>().map_or(-1, |t| t * 1_000_000_000);
112
113 let imported = History::import()
114 .timestamp(time)
115 .command(command.trim_end().to_string())
116 .duration(duration);
117
118 imported.build().into()
119}
120
121fn unmetafy(line: &[u8]) -> Option<Cow<str>> {
122 if line.contains(&0x83) {
123 let mut s = Vec::with_capacity(line.len());
124 let mut is_meta = false;
125 for ch in line {
126 if *ch == 0x83 {
127 is_meta = true;
128 } else if is_meta {
129 is_meta = false;
130 s.push(*ch ^ 32);
131 } else {
132 s.push(*ch)
133 }
134 }
135 String::from_utf8(s).ok().map(Cow::Owned)
136 } else {
137 std::str::from_utf8(line).ok().map(Cow::Borrowed)
138 }
139}
140
141#[cfg(test)]
142mod test {
143 use itertools::assert_equal;
144
145 use crate::import::tests::TestLoader;
146
147 use super::*;
148
149 #[test]
150 fn test_parse_extended_simple() {
151 let parsed = parse_extended("1613322469:0;cargo install atuin", 0);
152
153 assert_eq!(parsed.command, "cargo install atuin");
154 assert_eq!(parsed.duration, 0);
155 assert_eq!(
156 parsed.timestamp,
157 OffsetDateTime::from_unix_timestamp(1_613_322_469).unwrap()
158 );
159
160 let parsed = parse_extended("1613322469:10;cargo install atuin;cargo update", 0);
161
162 assert_eq!(parsed.command, "cargo install atuin;cargo update");
163 assert_eq!(parsed.duration, 10_000_000_000);
164 assert_eq!(
165 parsed.timestamp,
166 OffsetDateTime::from_unix_timestamp(1_613_322_469).unwrap()
167 );
168
169 let parsed = parse_extended("1613322469:10;cargo :b̷i̶t̴r̵o̴t̴ ̵i̷s̴ ̷r̶e̵a̸l̷", 0);
170
171 assert_eq!(parsed.command, "cargo :b̷i̶t̴r̵o̴t̴ ̵i̷s̴ ̷r̶e̵a̸l̷");
172 assert_eq!(parsed.duration, 10_000_000_000);
173 assert_eq!(
174 parsed.timestamp,
175 OffsetDateTime::from_unix_timestamp(1_613_322_469).unwrap()
176 );
177
178 let parsed = parse_extended("1613322469:10;cargo install \\n atuin\n", 0);
179
180 assert_eq!(parsed.command, "cargo install \\n atuin");
181 assert_eq!(parsed.duration, 10_000_000_000);
182 assert_eq!(
183 parsed.timestamp,
184 OffsetDateTime::from_unix_timestamp(1_613_322_469).unwrap()
185 );
186 }
187
188 #[tokio::test]
189 async fn test_parse_file() {
190 let bytes = r": 1613322469:0;cargo install atuin
191: 1613322469:10;cargo install atuin; \
192cargo update
193: 1613322469:10;cargo :b̷i̶t̴r̵o̴t̴ ̵i̷s̴ ̷r̶e̵a̸l̷
194"
195 .as_bytes()
196 .to_owned();
197
198 let mut zsh = Zsh { bytes };
199 assert_eq!(zsh.entries().await.unwrap(), 4);
200
201 let mut loader = TestLoader::default();
202 zsh.load(&mut loader).await.unwrap();
203
204 assert_equal(
205 loader.buf.iter().map(|h| h.command.as_str()),
206 [
207 "cargo install atuin",
208 "cargo install atuin; \\\ncargo update",
209 "cargo :b̷i̶t̴r̵o̴t̴ ̵i̷s̴ ̷r̶e̵a̸l̷",
210 ],
211 );
212 }
213
214 #[tokio::test]
215 async fn test_parse_metafied() {
216 let bytes =
217 b"echo \xe4\xbd\x83\x80\xe5\xa5\xbd\nls ~/\xe9\x83\xbf\xb3\xe4\xb9\x83\xb0\n".to_vec();
218
219 let mut zsh = Zsh { bytes };
220 assert_eq!(zsh.entries().await.unwrap(), 2);
221
222 let mut loader = TestLoader::default();
223 zsh.load(&mut loader).await.unwrap();
224
225 assert_equal(
226 loader.buf.iter().map(|h| h.command.as_str()),
227 ["echo 你好", "ls ~/音乐"],
228 );
229 }
230}