1use std::fmt::Write as _;
6use std::path::Path;
7
8use super::{PaneConfig, ShellCommand, WindowConfig, Workspace};
9
10impl Workspace {
11 #[must_use]
36 pub fn to_yaml(&self) -> String {
37 let mut out = String::new();
38 let _ = writeln!(out, "session_name: {}", quoted(&self.session_name));
39 if let Some(directory) = &self.start_directory {
40 let _ = writeln!(out, "start_directory: {}", path(directory));
41 }
42 if !self.suppress_history {
43 out.push_str("suppress_history: false\n");
44 }
45 write_pairs(&mut out, Some("environment"), &self.environment, 2);
46 write_pairs(&mut out, Some("options"), &self.options, 2);
47 write_pairs(&mut out, Some("global_options"), &self.global_options, 2);
48 write_commands(
49 &mut out,
50 Some("shell_command_before"),
51 &self.shell_command_before,
52 2,
53 );
54
55 if self.windows.is_empty() {
56 out.push_str("windows: []\n");
59 } else {
60 out.push_str("windows:\n");
61 for window in &self.windows {
62 window.write_yaml(&mut out);
63 }
64 }
65
66 out
67 }
68}
69
70struct Entry {
75 marker: &'static str,
76 indent: &'static str,
77 first: bool,
78}
79
80impl Entry {
81 const fn new(marker: &'static str, indent: &'static str) -> Self {
82 Self {
83 marker,
84 indent,
85 first: true,
86 }
87 }
88
89 fn key(&mut self, out: &mut String, line: &str) {
90 out.push_str(if self.first { self.marker } else { self.indent });
91 self.first = false;
92 out.push_str(line);
93 out.push('\n');
94 }
95}
96
97impl WindowConfig {
98 fn write_yaml(&self, out: &mut String) {
100 let mut entry = Entry::new(" - ", " ");
101
102 if let Some(name) = &self.window_name {
103 entry.key(out, &format!("window_name: {}", quoted(name)));
104 }
105 if let Some(index) = self.window_index {
106 entry.key(out, &format!("window_index: {index}"));
107 }
108 if let Some(shell) = &self.window_shell {
109 entry.key(out, &format!("window_shell: {}", quoted(shell)));
110 }
111 if let Some(layout) = &self.layout {
112 entry.key(out, &format!("layout: {}", quoted(layout)));
113 }
114 if let Some(directory) = &self.start_directory {
115 entry.key(out, &format!("start_directory: {}", path(directory)));
116 }
117 if self.focus {
118 entry.key(out, "focus: true");
119 }
120 if let Some(suppress) = self.suppress_history {
121 entry.key(out, &format!("suppress_history: {suppress}"));
122 }
123 if !self.environment.is_empty() {
124 entry.key(out, "environment:");
125 write_pairs(out, None, &self.environment, 6);
126 }
127 if !self.options.is_empty() {
128 entry.key(out, "options:");
129 write_pairs(out, None, &self.options, 6);
130 }
131 if !self.shell_command_before.is_empty() {
132 entry.key(out, "shell_command_before:");
133 write_commands(out, None, &self.shell_command_before, 6);
134 }
135
136 entry.key(out, "panes:");
139 for pane in &self.panes {
140 pane.write_yaml(out);
141 }
142 }
143}
144
145impl PaneConfig {
146 fn write_yaml(&self, out: &mut String) {
148 let mut entry = Entry::new(" - ", " ");
149
150 if !self.shell_command_before.is_empty() {
151 entry.key(out, "shell_command_before:");
152 write_commands(out, None, &self.shell_command_before, 10);
153 }
154 if !self.shell_commands.is_empty() {
155 entry.key(out, "shell_command:");
156 write_commands(out, None, &self.shell_commands, 10);
157 }
158 if let Some(directory) = &self.start_directory {
159 entry.key(out, &format!("start_directory: {}", path(directory)));
160 }
161 if self.focus {
162 entry.key(out, "focus: true");
163 }
164 if let Some(shell) = &self.shell {
165 entry.key(out, &format!("shell: {}", quoted(shell)));
166 }
167 if !self.enter {
168 entry.key(out, "enter: false");
169 }
170 if let Some(sleep) = self.sleep_before {
171 entry.key(out, &format!("sleep_before: {}", sleep.as_secs_f64()));
172 }
173 if let Some(sleep) = self.sleep_after {
174 entry.key(out, &format!("sleep_after: {}", sleep.as_secs_f64()));
175 }
176 if let Some(suppress) = self.suppress_history {
177 entry.key(out, &format!("suppress_history: {suppress}"));
178 }
179 if let Some(environment) = &self.environment {
180 entry.key(out, "environment:");
181 write_pairs(out, None, environment, 10);
182 }
183 if entry.first {
184 out.push_str(" - {}\n");
187 }
188 }
189}
190
191fn write_pairs(out: &mut String, name: Option<&str>, values: &[(String, String)], indent: usize) {
193 if values.is_empty() {
194 return;
195 }
196 if let Some(name) = name {
197 let _ = writeln!(out, "{name}:");
198 }
199 for (key, value) in values {
200 let _ = writeln!(out, "{:indent$}{}: {}", "", quoted(key), quoted(value));
201 }
202}
203
204fn write_commands(out: &mut String, name: Option<&str>, commands: &[ShellCommand], indent: usize) {
206 if commands.is_empty() {
207 return;
208 }
209 if let Some(name) = name {
210 let _ = writeln!(out, "{name}:");
211 }
212 for command in commands {
213 let _ = writeln!(out, "{:indent$}- {}", "", command_yaml(command));
214 }
215}
216
217fn command_yaml(command: &ShellCommand) -> String {
219 if command.is_plain() {
220 return quoted(&command.cmd);
221 }
222 let mut fields = vec![format!("cmd: {}", quoted(&command.cmd))];
223 if let Some(enter) = command.enter {
224 fields.push(format!("enter: {enter}"));
225 }
226 if let Some(sleep) = command.sleep_before {
227 fields.push(format!("sleep_before: {}", sleep.as_secs_f64()));
228 }
229 if let Some(sleep) = command.sleep_after {
230 fields.push(format!("sleep_after: {}", sleep.as_secs_f64()));
231 }
232 format!("{{{}}}", fields.join(", "))
233}
234
235fn path(value: &Path) -> String {
237 quoted(&value.display().to_string())
238}
239
240fn quoted(value: &str) -> String {
246 let mut escaped = String::with_capacity(value.len() + 2);
247 escaped.push('"');
248 for character in value.chars() {
249 match character {
250 '"' => escaped.push_str(r#"\""#),
251 '\\' => escaped.push_str(r"\\"),
252 character if character.is_control() || matches!(character, '\u{2028}' | '\u{2029}') => {
253 let code = u32::from(character);
254 let _ = write!(escaped, r"\u{code:04x}");
255 }
256 _ => escaped.push(character),
257 }
258 }
259 escaped.push('"');
260 escaped
261}
262
263#[cfg(test)]
264mod tests {
265 use super::quoted;
266
267 #[test]
268 fn quoted_escapes_the_characters_yaml_or_a_shell_would_read_specially() {
269 assert_eq!(quoted("plain"), "\"plain\"");
270 assert_eq!(quoted(r#"a "quote""#), r#""a \"quote\"""#);
271 assert_eq!(quoted(r"back\slash"), r#""back\\slash""#);
272 assert_eq!(quoted("tab\there"), "\"tab\\u0009here\"");
273 }
274}