window_enumerator_formatter/
formatter.rs

1use crate::models::WindowInfo;
2
3/// Supported output formats.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum OutputFormat {
6    /// Compact JSON format.
7    Json,
8    /// Pretty-printed JSON format.
9    JsonPretty,
10    /// CSV format.
11    Csv,
12    /// YAML format.
13    Yaml,
14    /// Simple one-line format.
15    Simple,
16    /// Detailed multi-line format.
17    Detail,
18    /// Formatted table.
19    Table,
20    /// Custom template format.
21    Custom,
22}
23
24/// Template formats for custom output.
25#[derive(Debug, Clone)]
26pub enum TemplateFormat {
27    /// Output only the values of specified fields (tab-separated).
28    Fields(Vec<String>),
29    /// Output field names and values.
30    KeyValue(Vec<String>),
31    /// Custom template string with placeholders.
32    Custom(String),
33}
34
35/// Configuration for formatting output.
36#[derive(Debug, Clone)]
37pub struct FormatConfig {
38    /// The output format to use.
39    pub format: OutputFormat,
40    /// Template configuration for custom formats.
41    pub template: Option<TemplateFormat>,
42    /// Whether to show headers in CSV/Table formats.
43    pub show_headers: bool,
44    /// Maximum title length before truncation.
45    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
59/// Main formatter for window information.
60pub struct WindowFormatter;
61
62impl WindowFormatter {
63    /// Format a single window according to the configuration.
64    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    /// Format a list of windows according to the configuration.
84    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    // Simple format - single window
108    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    // Detailed format - single window
121    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    // Table format - list
148    fn format_table(windows: &[WindowInfo], config: &FormatConfig) -> String {
149        let mut output = String::new();
150
151        // Header
152        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        // Rows
164        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    // Table format - single window
176    fn format_table_single(window: &WindowInfo) -> String {
177        Self::format_table(std::slice::from_ref(window), &FormatConfig::default())
178    }
179
180    // CSV format - list
181    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    // CSV format - single window
214    fn format_csv_single(window: &WindowInfo) -> String {
215        Self::format_csv(std::slice::from_ref(window), &FormatConfig::default())
216    }
217
218    // Simple format list
219    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    // Detailed format list
228    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    // Custom template formatting
237    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    // Custom template list
246    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    // Apply template
255    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    // Output only field values
266    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    // Output field names and values
276    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    // Custom template string
288    fn format_custom_template(window: &WindowInfo, template: &str) -> String {
289        let mut result = template.to_string();
290
291        // Replace template variables
292        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    // Get field value
317    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    // Utility functions
335    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
356/// Extension trait for formatting window information.
357pub trait WindowListFormat {
358    /// Format windows according to the configuration.
359    fn format_output(&self, config: &FormatConfig) -> String;
360
361    /// Format windows with a specific output format.
362    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}