atuin_client/import/
powershell.rs1use async_trait::async_trait;
2use directories::BaseDirs;
3use eyre::{Result, eyre};
4use std::path::PathBuf;
5use time::{Duration, OffsetDateTime};
6
7use super::{Importer, Loader, count_lines, unix_byte_lines};
8use crate::history::History;
9use crate::import::read_to_end;
10
11#[derive(Debug)]
12pub struct PowerShell {
13 bytes: Vec<u8>,
14 line_count: Option<usize>,
15}
16
17fn get_history_path() -> Result<PathBuf> {
18 let base = BaseDirs::new().ok_or_else(|| eyre!("could not determine data directory"))?;
19
20 let dir = if cfg!(windows) {
30 base.data_dir()
31 .join("Microsoft")
32 .join("Windows")
33 .join("PowerShell")
34 .join("PSReadLine")
35 } else {
36 std::env::var("XDG_DATA_HOME")
37 .map_or_else(
38 |_| base.home_dir().join(".local").join("share"),
39 PathBuf::from,
40 )
41 .join("powershell")
42 .join("PSReadLine")
43 };
44
45 let file = dir.join("ConsoleHost_history.txt");
50
51 if file.is_file() {
52 Ok(file)
53 } else {
54 Err(eyre!("Could not find history file: {}", file.display()))
55 }
56}
57
58#[async_trait]
59impl Importer for PowerShell {
60 const NAME: &'static str = "PowerShell";
61
62 async fn new() -> Result<Self> {
63 let bytes = read_to_end(get_history_path()?)?;
64 Ok(Self {
65 bytes,
66 line_count: None,
67 })
68 }
69
70 async fn entries(&mut self) -> Result<usize> {
71 if self.line_count.is_none() {
75 self.line_count = Some(count_lines(&self.bytes));
76 }
77 Ok(self.line_count.unwrap())
78 }
79
80 async fn load(mut self, h: &mut impl Loader) -> Result<()> {
81 let line_count = self.entries().await?;
82 let start = OffsetDateTime::now_utc() - Duration::milliseconds(line_count as i64);
83
84 let mut counter = 0;
85 let mut iter = unix_byte_lines(&self.bytes);
86
87 while let Some(s) = iter.next() {
88 let Ok(s) = read_line(s) else {
89 continue; };
91
92 let mut cmd = s.to_string();
93
94 while cmd.ends_with('`') {
96 cmd.pop();
97
98 let Some(next) = iter.next() else {
99 break;
100 };
101 let Ok(next) = read_line(next) else {
102 break;
103 };
104
105 cmd.push('\n');
106 cmd.push_str(next);
107 }
108
109 if cmd.is_empty() {
110 continue;
111 }
112
113 let offset = Duration::milliseconds(counter);
114 counter += 1;
115
116 let entry = History::import().timestamp(start + offset).command(cmd);
117 h.push(entry.build().into()).await?;
118 }
119
120 Ok(())
121 }
122}
123
124fn read_line(s: &[u8]) -> Result<&str> {
125 let s = str::from_utf8(s)?;
126
127 let s = s.strip_suffix('\r').unwrap_or(s);
129
130 Ok(s)
131}
132
133#[cfg(test)]
134mod test {
135 use super::*;
136 use crate::import::tests::TestLoader;
137 use itertools::assert_equal;
138
139 const INPUT: &str = r#"cargo install atuin
140cargo update
141echo "first line`
142second line`
143`
144last line"
145echo foo
146
147echo bar
148echo baz
149"#;
150
151 const EXPECTED: &[&str] = &[
152 "cargo install atuin",
153 "cargo update",
154 "echo \"first line\nsecond line\n\nlast line\"",
155 "echo foo",
156 "echo bar",
157 "echo baz",
158 ];
159
160 #[tokio::test]
161 async fn test_import() {
162 let loader = import(INPUT).await;
163
164 let actual = loader.buf.iter().map(|h| h.command.clone());
165 let expected = EXPECTED.iter().map(|s| s.to_string());
166
167 assert_equal(actual, expected);
168 }
169
170 #[tokio::test]
171 async fn test_crlf() {
172 let input = INPUT.replace("\n", "\r\n");
173 let loader = import(input.as_str()).await;
174
175 let actual = loader.buf.iter().map(|h| h.command.clone());
176 let expected = EXPECTED.iter().map(|s| s.to_string());
177
178 assert_equal(actual, expected);
179 }
180
181 #[tokio::test]
182 async fn test_timestamps() {
183 let loader = import(INPUT).await;
184
185 let mut prev = loader.buf.first().unwrap().timestamp;
186 for current in loader.buf.iter().skip(1).map(|h| h.timestamp) {
187 assert!(current > prev);
188 prev = current;
189 }
190 }
191
192 async fn import(input: &str) -> TestLoader {
193 let powershell = PowerShell {
194 bytes: input.as_bytes().to_vec(),
195 line_count: None,
196 };
197
198 let mut loader = TestLoader::default();
199 powershell.load(&mut loader).await.unwrap();
200 loader
201 }
202}