1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
//! # Humantalk
//! Designed to make talking with the user easier
//! [crates.io](https://crates.io/crates/humantalk)
//! [github](https://github.com/werdl/humantalk)
 
/// console crate styling to customise the output of humantalk
/// 
pub use console::{style, Color};
use std::{collections::HashMap, io::Write};

use thetime::{System, Time};

/// version of humantalk, manually updated each release
pub const VERSION: &str = "0.1.1";

use rustc_version::version_meta;

/// severity enum to denote severity of logging
/// 
/// # Examples
/// ```rust
/// use humantalk::{Severity, Config};
/// let config = Config::default();
/// 
/// config.write(Severity::Error, "oh no!"); // non fatal
/// config.write(Severity::Debug, "this will not trigger if compiled with --release");
/// ```
#[derive(PartialEq, Eq, Hash, Debug, Clone)]
pub enum Severity {
    Error,
    Warning,
    Info,
    Debug,
}

impl std::fmt::Display for Severity {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        let s = match self {
            Severity::Error => "error",
            Severity::Warning => "warning",
            Severity::Info => "info",
            Severity::Debug => "debug",
        };
        write!(f, "{}", s)
    }
}

/// Bug report struct, printed at fatal error
#[derive(Debug, Clone)]
pub struct HowToBugReport {
    /// the message to be displayed on crash
    pub message: String,

    /// url where users are pointed
    pub url: String,
}

impl HowToBugReport {
    /// create a new bug report
    pub fn new(message: String, url: String) -> Self {
        HowToBugReport { message, url }
    }
}


/// configuration struct for humantalk
#[derive(Clone, Debug)]
pub struct Config {
    /// colors hashmap for each severity level
    pub colors: HashMap<Severity, Color>,

    /// the bug reporting struct
    pub bug_report: Option<HowToBugReport>,
}

trait ColorToColor256 {
    fn to_color256(&self) -> u8;
}

impl ColorToColor256 for Color {
    fn to_color256(&self) -> u8 {
        match self {
            Color::Black => 0,
            Color::Red => 1,
            Color::Green => 2,
            Color::Yellow => 3,
            Color::Blue => 4,
            Color::Magenta => 5,
            Color::Cyan => 6,
            Color::White => 7,
            Color::Color256(code) => *code,
        }
    }
}

impl Config {
    /// create a new configuration, with default colors and no bug report (auto-filled with default values on use)
    pub fn default() -> Config {
        let mut colors = HashMap::new();
        colors.insert(Severity::Error, Color::Red);
        colors.insert(Severity::Warning, Color::Yellow);
        colors.insert(Severity::Info, Color::Green);
        colors.insert(Severity::Debug, Color::Blue);
        Config {
            colors,
            bug_report: None,
        }
    }

    /// create a custom config, with your own colors and bug report. If you just want custom bug report, just use this code (inverse for colors):
    /// ```
    /// use humantalk::{Config, Severity, HowToBugReport};
    /// 
    /// let my_config = Config::custom(
    ///     Config::default().colors,
    ///     HowToBugReport::new(
    ///         "message".to_string(),
    ///         "url".to_string()
    ///     )
    /// );
    /// ```
    pub fn custom(
        colors: HashMap<Severity, Color>,
        bug_report: HowToBugReport,
    ) -> Config {
        Config {
            colors,
            bug_report: Some(bug_report),
        }
    }

    /// find the specified color for a given severity
    pub fn get_color(&self, severity: &Severity) -> Color {
        match self.colors.get(severity) {
            Some(color) => *color,
            None => Color::White,
        }
    }

    /// set the color for the specified severity level
    pub fn set_color(&mut self, severity: Severity, color: Color) {
        let _ = self.colors.remove(&severity);
        self.colors.insert(severity, color);
    }

    /// write a logging message to stdout. if the binary has been compiled with --release, it will not print debug assertions.
    pub fn write(&self, severity: Severity, message: &str) {
        #[cfg(not(debug_assertions))]
        if severity == Severity::Debug {
            return;
        }

        let color = self.get_color(&severity);
        let styled = style(format!("({}) [{}] {}", System::now().strftime("%H:%m:%S%p"), severity, message)).color256(color.to_color256());
        println!("{}", styled);
    }
    
    /// shorthand for `config.write(Severity::Debug, ...)`
    pub fn debug(&self, message: &str) {
        self.write(Severity::Debug, message);
    }

    /// shorthand for `config.write(Severity::Info, ...)`
    pub fn info(&self, message: &str) {
        self.write(Severity::Info, message);
    }

    /// shorthand for `config.write(Severity::Error, ...)`
    pub fn error(&self, message: &str) {
        self.write(Severity::Error, message);
    } 

    /// shorthand for `config.write(Severity::Warning, ...)`
    pub fn warning(&self, message: &str) {
        self.write(Severity::Warning, message);
    }

    /// get machine info represented as a string. Contains info including OS family, os, arch, rust version, llvm version and humantalk version
    pub fn machine_info(&self) -> String {
        let arch = std::env::consts::ARCH.to_string();
        let os = std::env::consts::OS.to_string();
        let family = std::env::consts::FAMILY.to_string();

        let rustc_info = version_meta().unwrap_or_else(|e: rustc_version::Error| {
            println!("humantalk has failed (with message {e}). Please submit an issue at github.com/werdl/humantalk");

            std::process::exit(-1)
        }
        );

        let llvm_version_string = match rustc_info.llvm_version {
            Some(version) => {
                format!("{}.{}", version.major, version.minor)
            }
            None => "unknown".to_string(),
        };

        format!(
            "{family}-{os}-{arch} - Rust version {}, running on LLVM {}. information stuff generated by humantalk {}",
            rustc_info.short_version_string,
            llvm_version_string,
            VERSION
        )
    }

    /// error fatally, crashing the program. then exits with error code `3`, indincating that erroring out has succeeded
    pub fn fatal_error(&self, message: &str) {
        let bug_report = match self.bug_report.clone() {
            Some(x) => x,
            None => HowToBugReport {
                message: "Oh no! The program has crashed".to_string(),
                url: "the appropriate place".to_string(),
            },
        };
        let styled = style(format!(
            "[FATAL] {}
{}. Please submit a report to {}, along with a copy of this error message, which can also be found in crash_report.log as plaintext.

",
            message, bug_report.message, bug_report.url
        ))
        .red();

        println!("{}", styled);

        println!(
            "{}",
            style(format!("[PLATFORM INFO]\n{}", self.machine_info())).cyan()
        );

        let mut debug_file = std::fs::File::create("crash_report.log").unwrap_or_else(|_| {
            println!("Failed to create debug file - just copy the information displayed above.");

            std::process::exit(-1);
        });

        let _ = debug_file
            .write(format!(
                "[FATAL] {}
{}. Please submit a report to {}, along with a copy of this error message, which can also be found in crash_report.log as plaintext.",
                message, bug_report.message, bug_report.url
            ).as_bytes())
            .unwrap_or_else(|_| {
                println!(
                    "Failed to write to debug file - just copy the information displayed above."
                );

                std::process::exit(-1);
            });

        let _ = debug_file
            .write(format!("\n[PLATFORM INFO]\n{}", self.machine_info()).as_bytes())
            .unwrap_or_else(|_| {
                println!(
                    "Failed to write to debug file - just copy the information displayed above."
                );

                std::process::exit(-1);
            });

        std::process::exit(3)
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_write() {
        let config = Config::custom(
            Config::default().colors,
            HowToBugReport::new(
                "Oh no! Humantalk testing has crashed (don't worry it was manually induced)".to_string(),
                "https://github.com/werdl/humantalk".to_string()
            )
        );
        config.write(Severity::Debug, "hello world!");
        config.write(Severity::Info, "hello information world!")
    }
}