1use crate::models::WindowInfo;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum OutputFormat {
6 Json,
8 JsonPretty,
10 Csv,
12 Yaml,
14 Simple,
16 Detail,
18 Table,
20 Custom,
22}
23
24#[derive(Debug, Clone)]
26pub enum TemplateFormat {
27 Fields(Vec<String>),
29 KeyValue(Vec<String>),
31 Custom(String),
33}
34
35#[derive(Debug, Clone)]
37pub struct FormatConfig {
38 pub format: OutputFormat,
40 pub template: Option<TemplateFormat>,
42 pub show_headers: bool,
44 pub max_title_length: Option<usize>,
46}
47
48impl Default for FormatConfig {
49 fn default() -> Self {
50 Self {
51 format: OutputFormat::Table,
52 template: None,
53 show_headers: true,
54 max_title_length: Some(50),
55 }
56 }
57}
58
59pub struct WindowFormatter;
61
62impl WindowFormatter {
63 pub fn format_window(window: &WindowInfo, config: &FormatConfig) -> String {
65 match config.format {
66 OutputFormat::Json => {
67 serde_json::to_string(window).unwrap_or_else(|_| "{}".to_string())
68 }
69 OutputFormat::JsonPretty => {
70 serde_json::to_string_pretty(window).unwrap_or_else(|_| "{}".to_string())
71 }
72 OutputFormat::Yaml => {
73 serde_yaml::to_string(window).unwrap_or_else(|_| "---".to_string())
74 }
75 OutputFormat::Simple => Self::format_simple(window, config),
76 OutputFormat::Detail => Self::format_detail(window, config),
77 OutputFormat::Table => Self::format_table_single(window),
78 OutputFormat::Custom => Self::format_custom(window, config),
79 OutputFormat::Csv => Self::format_csv_single(window),
80 }
81 }
82
83 pub fn format_windows(windows: &[WindowInfo], config: &FormatConfig) -> String {
85 if windows.is_empty() {
86 return "No windows found".to_string();
87 }
88
89 match config.format {
90 OutputFormat::Json => {
91 serde_json::to_string(windows).unwrap_or_else(|_| "[]".to_string())
92 }
93 OutputFormat::JsonPretty => {
94 serde_json::to_string_pretty(windows).unwrap_or_else(|_| "[]".to_string())
95 }
96 OutputFormat::Yaml => {
97 serde_yaml::to_string(windows).unwrap_or_else(|_| "---".to_string())
98 }
99 OutputFormat::Simple => Self::format_simple_list(windows, config),
100 OutputFormat::Detail => Self::format_detail_list(windows, config),
101 OutputFormat::Table => Self::format_table(windows, config),
102 OutputFormat::Custom => Self::format_custom_list(windows, config),
103 OutputFormat::Csv => Self::format_csv(windows, config),
104 }
105 }
106
107 fn format_simple(window: &WindowInfo, config: &FormatConfig) -> String {
109 if let Some(template) = &config.template {
110 return Self::apply_template(window, template);
111 }
112
113 let title = Self::truncate_title(&window.title, config.max_title_length);
114 format!(
115 "[{}] {} (PID: {}) @ ({},{})",
116 window.index, title, window.pid, window.position.x, window.position.y
117 )
118 }
119
120 fn format_detail(window: &WindowInfo, _config: &FormatConfig) -> String {
122 format!(
123 "Index: {}\n\
124 Handle: 0x{:x}\n\
125 PID: {}\n\
126 Title: {}\n\
127 Class: {}\n\
128 Process: {}\n\
129 File: {}\n\
130 Position: ({}, {}) Size: {}x{}\n\
131 {}",
132 window.index,
133 window.hwnd,
134 window.pid,
135 window.title,
136 window.class_name,
137 window.process_name,
138 window.process_file.display(),
139 window.position.x,
140 window.position.y,
141 window.position.width,
142 window.position.height,
143 "-".repeat(40)
144 )
145 }
146
147 fn format_table(windows: &[WindowInfo], config: &FormatConfig) -> String {
149 let mut output = String::new();
150
151 if config.show_headers {
153 output.push_str(&format!(
154 "{:<6} {:<12} {:<8} {:<12} {}\n",
155 "Index", "Handle", "PID", "Position", "Title"
156 ));
157 output.push_str(&format!(
158 "{:-<6} {:-<12} {:-<8} {:-<12} {:-<30}\n",
159 "", "", "", "", ""
160 ));
161 }
162
163 for window in windows {
165 let title = Self::truncate_title(&window.title, config.max_title_length);
166 output.push_str(&format!(
167 "{:<6} 0x{:<10x} {:<8} {:4},{:<7} {}\n",
168 window.index, window.hwnd, window.pid, window.position.x, window.position.y, title
169 ));
170 }
171
172 output
173 }
174
175 fn format_table_single(window: &WindowInfo) -> String {
177 Self::format_table(std::slice::from_ref(window), &FormatConfig::default())
178 }
179
180 fn format_csv(windows: &[WindowInfo], config: &FormatConfig) -> String {
182 let mut output = String::new();
183
184 if config.show_headers {
185 output.push_str("Index,Handle,PID,Title,Class,Process,File,X,Y,Width,Height\n");
186 }
187
188 for window in windows {
189 let title = Self::escape_csv_field(&window.title);
190 let class_name = Self::escape_csv_field(&window.class_name);
191 let process_name = Self::escape_csv_field(&window.process_name);
192 let file_path = Self::escape_csv_field(&window.process_file.to_string_lossy());
193
194 output.push_str(&format!(
195 "{},{},{},{},{},{},{},{},{},{},{}\n",
196 window.index,
197 window.hwnd,
198 window.pid,
199 title,
200 class_name,
201 process_name,
202 file_path,
203 window.position.x,
204 window.position.y,
205 window.position.width,
206 window.position.height
207 ));
208 }
209
210 output
211 }
212
213 fn format_csv_single(window: &WindowInfo) -> String {
215 Self::format_csv(std::slice::from_ref(window), &FormatConfig::default())
216 }
217
218 fn format_simple_list(windows: &[WindowInfo], config: &FormatConfig) -> String {
220 windows
221 .iter()
222 .map(|w| Self::format_simple(w, config))
223 .collect::<Vec<_>>()
224 .join("\n")
225 }
226
227 fn format_detail_list(windows: &[WindowInfo], config: &FormatConfig) -> String {
229 windows
230 .iter()
231 .map(|w| Self::format_detail(w, config))
232 .collect::<Vec<_>>()
233 .join("\n")
234 }
235
236 fn format_custom(window: &WindowInfo, config: &FormatConfig) -> String {
238 if let Some(template) = &config.template {
239 Self::apply_template(window, template)
240 } else {
241 Self::format_simple(window, config)
242 }
243 }
244
245 fn format_custom_list(windows: &[WindowInfo], config: &FormatConfig) -> String {
247 windows
248 .iter()
249 .map(|w| Self::format_custom(w, config))
250 .collect::<Vec<_>>()
251 .join("\n")
252 }
253
254 fn apply_template(window: &WindowInfo, template: &TemplateFormat) -> String {
256 match template {
257 TemplateFormat::Fields(fields) => Self::format_fields(window, fields),
258 TemplateFormat::KeyValue(fields) => Self::format_key_value(window, fields),
259 TemplateFormat::Custom(template_str) => {
260 Self::format_custom_template(window, template_str)
261 }
262 }
263 }
264
265 fn format_fields(window: &WindowInfo, fields: &[String]) -> String {
267 let values: Vec<String> = fields
268 .iter()
269 .map(|field| Self::get_field_value(window, field))
270 .collect();
271
272 values.join("\t")
273 }
274
275 fn format_key_value(window: &WindowInfo, fields: &[String]) -> String {
277 fields
278 .iter()
279 .map(|field| {
280 let value = Self::get_field_value(window, field);
281 format!("{}: {}", field, value)
282 })
283 .collect::<Vec<_>>()
284 .join(" | ")
285 }
286
287 fn format_custom_template(window: &WindowInfo, template: &str) -> String {
289 let mut result = template.to_string();
290
291 let replacements = [
293 ("{index}", &window.index.to_string()),
294 ("{hwnd}", &format!("0x{:x}", window.hwnd)),
295 ("{pid}", &window.pid.to_string()),
296 ("{title}", &window.title),
297 ("{class}", &window.class_name),
298 ("{process}", &window.process_name),
299 (
300 "{file}",
301 &window.process_file.to_string_lossy().into_owned(),
302 ),
303 ("{x}", &window.position.x.to_string()),
304 ("{y}", &window.position.y.to_string()),
305 ("{width}", &window.position.width.to_string()),
306 ("{height}", &window.position.height.to_string()),
307 ];
308
309 for (pattern, replacement) in replacements {
310 result = result.replace(pattern, replacement);
311 }
312
313 result
314 }
315
316 fn get_field_value(window: &WindowInfo, field: &str) -> String {
318 match field.to_lowercase().as_str() {
319 "index" => window.index.to_string(),
320 "hwnd" => format!("0x{:x}", window.hwnd),
321 "pid" => window.pid.to_string(),
322 "title" => window.title.clone(),
323 "class" => window.class_name.clone(),
324 "process" => window.process_name.clone(),
325 "file" => window.process_file.to_string_lossy().to_string(),
326 "x" => window.position.x.to_string(),
327 "y" => window.position.y.to_string(),
328 "width" => window.position.width.to_string(),
329 "height" => window.position.height.to_string(),
330 _ => format!("[unknown field: {}]", field),
331 }
332 }
333
334 fn truncate_title(title: &str, max_length: Option<usize>) -> String {
336 if let Some(max) = max_length {
337 if title.len() > max {
338 format!("{}...", &title[..max - 3])
339 } else {
340 title.to_string()
341 }
342 } else {
343 title.to_string()
344 }
345 }
346
347 fn escape_csv_field(field: &str) -> String {
348 if field.contains(',') || field.contains('"') || field.contains('\n') {
349 format!("\"{}\"", field.replace('"', "\"\""))
350 } else {
351 field.to_string()
352 }
353 }
354}
355
356pub trait WindowListFormat {
358 fn format_output(&self, config: &FormatConfig) -> String;
360
361 fn format_with(&self, format: OutputFormat) -> String;
363}
364
365impl WindowListFormat for [WindowInfo] {
366 fn format_output(&self, config: &FormatConfig) -> String {
367 WindowFormatter::format_windows(self, config)
368 }
369
370 fn format_with(&self, format: OutputFormat) -> String {
371 let config = FormatConfig {
372 format,
373 ..Default::default()
374 };
375 self.format_output(&config)
376 }
377}
378
379impl WindowListFormat for Vec<WindowInfo> {
380 fn format_output(&self, config: &FormatConfig) -> String {
381 WindowFormatter::format_windows(self, config)
382 }
383
384 fn format_with(&self, format: OutputFormat) -> String {
385 let config = FormatConfig {
386 format,
387 ..Default::default()
388 };
389 self.format_output(&config)
390 }
391}