tmux_backup/actions/
save.rs1use std::path::{Path, PathBuf};
4
5use async_fs as fs;
6use futures::future::join_all;
7use smol;
8use tempfile::TempDir;
9
10use crate::{Result, management::archive::v1, tmux};
11use tmux_lib::utils;
12
13const DETECTED_SHELLS: &[&str] = &["zsh", "bash", "fish"];
18
19pub async fn save<P: AsRef<Path>>(
30 backup_dirpath: P,
31 num_lines_to_drop: usize,
32) -> Result<(PathBuf, v1::Overview)> {
33 let temp_dir = TempDir::new()?;
35
36 let metadata_task: smol::Task<Result<(PathBuf, PathBuf, u16, u16)>> = {
38 let temp_dirpath = temp_dir.path().to_path_buf();
39
40 smol::spawn(async move {
41 let temp_version_filepath = temp_dirpath.join(v1::VERSION_FILENAME);
42 fs::write(&temp_version_filepath, v1::FORMAT_VERSION).await?;
43
44 let metadata = v1::Metadata::new().await?;
45
46 let json = serde_json::to_string(&metadata)?;
47
48 let temp_metadata_filepath = temp_dirpath.join(v1::METADATA_FILENAME);
49 fs::write(temp_metadata_filepath.as_path(), json).await?;
50
51 Ok((
52 temp_version_filepath,
53 temp_metadata_filepath,
54 metadata.sessions.len() as u16,
55 metadata.windows.len() as u16,
56 ))
57 })
58 };
59
60 let (temp_panes_content_dir, num_panes) = {
62 let temp_panes_content_dir = temp_dir.path().join(v1::PANES_DIR_NAME);
63 fs::create_dir_all(&temp_panes_content_dir).await?;
64
65 let panes = tmux::pane::available_panes().await?;
66 let num_panes = panes.len() as u16;
67 save_panes_content(panes, &temp_panes_content_dir, num_lines_to_drop).await?;
68
69 (temp_panes_content_dir, num_panes)
70 };
71 let (temp_version_filepath, temp_metadata_filepath, num_sessions, num_windows) =
72 metadata_task.await?;
73
74 let new_backup_filepath = v1::new_backup_filepath(backup_dirpath.as_ref());
76
77 v1::create_from_paths(
78 &new_backup_filepath,
79 &temp_version_filepath,
80 &temp_metadata_filepath,
81 &temp_panes_content_dir,
82 )?;
83
84 temp_dir.close()?;
86
87 let overview = v1::Overview {
88 version: v1::FORMAT_VERSION.to_string(),
89 num_sessions,
90 num_windows,
91 num_panes,
92 };
93
94 Ok((new_backup_filepath, overview))
95}
96
97fn is_shell_command(command: &str) -> bool {
101 DETECTED_SHELLS.contains(&command)
102}
103
104fn lines_to_drop_for_pane(pane_command: &str, num_lines_to_drop: usize) -> usize {
109 if is_shell_command(pane_command) {
110 num_lines_to_drop
111 } else {
112 0
113 }
114}
115
116async fn save_panes_content<P: AsRef<Path>>(
118 panes: Vec<tmux::pane::Pane>,
119 destination_dir: P,
120 num_lines_to_drop: usize,
121) -> Result<()> {
122 let mut handles = Vec::new();
123
124 for pane in panes {
125 let dest_dir = destination_dir.as_ref().to_path_buf();
126 let drop_n_last_lines = lines_to_drop_for_pane(&pane.command, num_lines_to_drop);
127
128 let handle = smol::spawn(async move {
129 let stdout = pane.capture().await.unwrap();
130 let cleaned_buffer = utils::cleanup_captured_buffer(&stdout, drop_n_last_lines);
131
132 let filename = format!("pane-{}.txt", pane.id);
133 let filepath = dest_dir.join(filename);
134 fs::write(filepath, cleaned_buffer).await
135 });
136 handles.push(handle);
137 }
138
139 join_all(handles).await;
140 Ok(())
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 mod shell_detection {
148 use super::*;
149
150 #[test]
151 fn recognizes_zsh() {
152 assert!(is_shell_command("zsh"));
153 }
154
155 #[test]
156 fn recognizes_bash() {
157 assert!(is_shell_command("bash"));
158 }
159
160 #[test]
161 fn recognizes_fish() {
162 assert!(is_shell_command("fish"));
163 }
164
165 #[test]
166 fn rejects_vim() {
167 assert!(!is_shell_command("vim"));
168 }
169
170 #[test]
171 fn rejects_nvim() {
172 assert!(!is_shell_command("nvim"));
173 }
174
175 #[test]
176 fn rejects_python() {
177 assert!(!is_shell_command("python"));
178 }
179
180 #[test]
181 fn rejects_empty_command() {
182 assert!(!is_shell_command(""));
183 }
184
185 #[test]
186 fn rejects_similar_but_different() {
187 assert!(!is_shell_command("zsh-5.9"));
189 assert!(!is_shell_command("/bin/zsh"));
190 assert!(!is_shell_command("bash-5.2"));
191 }
192
193 #[test]
194 fn case_sensitive() {
195 assert!(!is_shell_command("ZSH"));
196 assert!(!is_shell_command("BASH"));
197 assert!(!is_shell_command("Fish"));
198 }
199 }
200
201 mod lines_to_drop {
202 use super::*;
203
204 #[test]
205 fn drops_lines_for_shells() {
206 assert_eq!(lines_to_drop_for_pane("zsh", 2), 2);
207 assert_eq!(lines_to_drop_for_pane("bash", 3), 3);
208 assert_eq!(lines_to_drop_for_pane("fish", 1), 1);
209 }
210
211 #[test]
212 fn zero_drop_for_non_shells() {
213 assert_eq!(lines_to_drop_for_pane("vim", 5), 0);
214 assert_eq!(lines_to_drop_for_pane("python", 10), 0);
215 assert_eq!(lines_to_drop_for_pane("htop", 3), 0);
216 }
217
218 #[test]
219 fn zero_requested_means_zero_dropped() {
220 assert_eq!(lines_to_drop_for_pane("zsh", 0), 0);
221 assert_eq!(lines_to_drop_for_pane("bash", 0), 0);
222 }
223 }
224
225 mod constants {
226 use super::*;
227
228 #[test]
229 fn detected_shells_includes_common_shells() {
230 assert!(DETECTED_SHELLS.contains(&"zsh"));
231 assert!(DETECTED_SHELLS.contains(&"bash"));
232 assert!(DETECTED_SHELLS.contains(&"fish"));
233 }
234 }
235}