log_table/
logger.rs

1use crate::color::Color;
2use indexmap::IndexMap;
3use std::fmt::Display;
4use std::fs::{self, OpenOptions};
5use std::io::Write;
6use std::path::PathBuf;
7
8pub struct Logger {
9    frame_color: Option<Color>,
10    content_color: Option<Color>,
11    log_file: Option<PathBuf>,
12}
13
14impl Logger {
15    pub fn new() -> Self {
16        Logger {
17            frame_color: None,
18            content_color: None,
19            log_file: None,
20        }
21    }
22
23    pub fn with_frame_color(mut self, color: Color) -> Self {
24        self.frame_color = Some(color);
25        self
26    }
27
28    pub fn with_content_color(mut self, color: Color) -> Self {
29        self.content_color = Some(color);
30        self
31    }
32
33    pub fn with_save_log(mut self, path: impl Into<PathBuf>) -> Self {
34        self.log_file = Some(path.into());
35        // Create directory if it doesn't exist
36        if let Some(log_file) = &self.log_file {
37            if let Some(parent) = log_file.parent() {
38                fs::create_dir_all(parent).ok();
39            }
40        }
41        self
42    }
43
44    fn write_to_log(&self, content: &str) {
45        if let Some(log_file) = &self.log_file {
46            if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(log_file) {
47                writeln!(file, "{}", content).ok();
48            }
49        }
50    }
51
52    fn print_and_log(&self, content: &str) {
53        println!("{}", content);
54        self.write_to_log(content);
55    }
56
57    fn colorize_frame(&self, s: &str) -> String {
58        if let Some(color) = self.frame_color {
59            color.to_colored_string(s)
60        } else {
61            s.to_string()
62        }
63    }
64
65    fn colorize_content(&self, s: &str) -> String {
66        if let Some(color) = self.content_color {
67            color.to_colored_string(s)
68        } else {
69            s.to_string()
70        }
71    }
72
73    pub fn table<'a, T: Display>(&self, title: &str, data: impl Into<IndexMap<&'a str, T>>) {
74        let data_map: IndexMap<&str, T> = data.into();
75        let data: IndexMap<&str, String> = data_map
76            .into_iter()
77            .map(|(k, v)| (k, format!("{}", v)))
78            .collect();
79
80        let max_key_len = data.keys().map(|k| k.len()).max().unwrap_or(0);
81        let max_val_len = data.values().map(|v| v.len()).max().unwrap_or(0);
82
83        let key_col_width = max_key_len + 2;
84        let val_col_width = max_val_len + 2;
85        let total_width = key_col_width + val_col_width + 3;
86
87        // Frame parts
88        let top_border = self.colorize_frame(&format!("┌{}┐", "─".repeat(total_width - 2)));
89        let title_border_left = self.colorize_frame("│");
90        let title_border_right = self.colorize_frame("│");
91        let title_content =
92            self.colorize_content(&format!("{:^width$}", title, width = total_width - 2));
93
94        self.print_and_log(&top_border);
95        self.print_and_log(&format!(
96            "{}{}{}",
97            title_border_left, title_content, title_border_right
98        ));
99        self.print_and_log(&self.colorize_frame(&format!(
100            "├{}┬{}┤",
101            "─".repeat(key_col_width),
102            "─".repeat(val_col_width)
103        )));
104
105        // Headers
106        let header_left = self.colorize_frame("│");
107        let header_middle = self.colorize_frame("│");
108        let header_right = self.colorize_frame("│");
109        let key_header =
110            self.colorize_content(&format!(" {:<width$} ", "Key", width = max_key_len));
111        let value_header =
112            self.colorize_content(&format!(" {:<width$} ", "Value", width = max_val_len));
113
114        self.print_and_log(&format!(
115            "{}{}{}{}{}",
116            header_left, key_header, header_middle, value_header, header_right
117        ));
118        self.print_and_log(&self.colorize_frame(&format!(
119            "├{}┼{}┤",
120            "─".repeat(key_col_width),
121            "─".repeat(val_col_width)
122        )));
123
124        let entries: Vec<_> = data.iter().collect();
125        for (i, (key, value)) in entries.iter().enumerate() {
126            let row_left = self.colorize_frame("│");
127            let row_middle = self.colorize_frame("│");
128            let row_right = self.colorize_frame("│");
129            let key_content =
130                self.colorize_content(&format!(" {:<width$} ", key, width = max_key_len));
131            let value_content =
132                self.colorize_content(&format!(" {:<width$} ", value, width = max_val_len));
133
134            self.print_and_log(&format!(
135                "{}{}{}{}{}",
136                row_left, key_content, row_middle, value_content, row_right
137            ));
138
139            if i < entries.len() - 1 {
140                self.print_and_log(&self.colorize_frame(&format!(
141                    "├{}┼{}┤",
142                    "─".repeat(key_col_width),
143                    "─".repeat(val_col_width)
144                )));
145            }
146        }
147
148        self.print_and_log(&self.colorize_frame(&format!(
149            "└{}┴{}┘",
150            "─".repeat(key_col_width),
151            "─".repeat(val_col_width)
152        )));
153        self.print_and_log("");
154    }
155}