ares/cli/
first_run.rs

1//! First-run configuration module for Ares
2//!
3//! This module handles the initial setup of Ares, including color scheme configuration
4//! and user preferences. It provides functionality for creating and managing color schemes,
5//! handling user input, and converting between different color formats.
6
7use colored::Colorize;
8use gibberish_or_not::download_model_with_progress_bar;
9use rpassword;
10use std::collections::HashMap;
11use std::fmt::Display;
12use std::io::{self, Write};
13use std::path::Path;
14
15/// Represents a color scheme with RGB values for different message types and roles.
16/// Each color is stored as a comma-separated RGB string in the format "r,g,b"
17/// where r, g, and b are values between 0 and 255.
18#[derive(Debug)]
19pub struct ColorScheme {
20    /// RGB color value for informational messages in format "r,g,b"
21    /// Used for general information and status updates
22    pub informational: String,
23    /// RGB color value for warning messages in format "r,g,b"
24    /// Used for non-critical warnings and cautions
25    pub warning: String,
26    /// RGB color value for success messages in format "r,g,b"
27    /// Used for successful operations and confirmations
28    pub success: String,
29    /// RGB color value for question prompts in format "r,g,b"
30    /// Used for interactive prompts and user queries
31    pub question: String,
32    /// RGB color value for general statements in format "r,g,b"
33    /// Used for standard output and neutral messages
34    pub statement: String,
35}
36
37/// Prints a statement in white color.
38///
39/// # Arguments
40/// * `text` - Any type that implements Display trait to be printed in white
41///
42/// # Returns
43/// * `String` - The input text formatted in white color
44fn print_statement<T: Display>(text: T) -> String {
45    text.to_string().white().to_string()
46}
47
48/// Prints a warning message in red color.
49///
50/// # Arguments
51/// * `text` - Any type that implements Display trait to be printed in red
52///
53/// # Returns
54/// * `String` - The input text formatted in red color
55fn print_warning<T: Display>(text: T) -> String {
56    text.to_string().red().to_string()
57}
58
59/// Prints a question prompt in yellow color.
60///
61/// # Arguments
62/// * `text` - Any type that implements Display trait to be printed in yellow
63///
64/// # Returns
65/// * `String` - The input text formatted in yellow color
66fn print_question<T: Display>(text: T) -> String {
67    text.to_string().yellow().to_string()
68}
69
70/// Prints a success message in green color.
71///
72/// # Arguments
73/// * `text` - Any type that implements Display trait to be printed in green
74///
75/// # Returns
76/// * `String` - The input text formatted in green color
77fn print_success<T: Display>(text: T) -> String {
78    text.to_string().green().to_string()
79}
80
81/// Prints text in a specified RGB color.
82///
83/// # Arguments
84/// * `text` - The text to be colored
85/// * `rgb` - RGB color string in format "r,g,b" where r,g,b are 0-255
86///
87/// # Returns
88/// * `String` - The text colored with the specified RGB values, or uncolored if RGB format is invalid
89fn print_rgb(text: &str, rgb: &str) -> String {
90    let parts: Vec<&str> = rgb.split(',').collect();
91    if parts.len() != 3 {
92        return text.to_string();
93    }
94
95    if let (Ok(r), Ok(g), Ok(b)) = (
96        parts[0].trim().parse::<u8>(),
97        parts[1].trim().parse::<u8>(),
98        parts[2].trim().parse::<u8>(),
99    ) {
100        text.truecolor(r, g, b).to_string()
101    } else {
102        text.to_string()
103    }
104}
105
106/// Returns the Capptucin color scheme with warm, muted colors.
107///
108/// # Returns
109/// * `ColorScheme` - A color scheme with Capptucin's signature warm colors
110fn get_capptucin_scheme() -> ColorScheme {
111    ColorScheme {
112        informational: "238,212,159".to_string(), // rgb(238, 212, 159)
113        warning: "237,135,150".to_string(),       // rgb(237, 135, 150)
114        success: "166,218,149".to_string(),       // rgb(166, 218, 149)
115        question: "202,211,245".to_string(),      // rgb(202, 211, 245)
116        statement: "244,219,214".to_string(),     // rgb(244, 219, 214)
117    }
118}
119
120/// Returns the Darcula color scheme matching JetBrains Darcula theme.
121///
122/// # Returns
123/// * `ColorScheme` - A color scheme with Darcula's signature colors
124fn get_darcula_scheme() -> ColorScheme {
125    ColorScheme {
126        informational: "241,250,140".to_string(), // rgb(241, 250, 140)
127        warning: "255,85,85".to_string(),         // rgb(255, 85, 85)
128        success: "80,250,123".to_string(),        // rgb(80, 250, 123)
129        question: "139,233,253".to_string(),      // rgb(139, 233, 253)
130        statement: "248,248,242".to_string(),     // rgb(248, 248, 242)
131    }
132}
133
134/// Returns Autumn's personal Girly Pop theme with pink and pastel colors.
135///
136/// # Returns
137/// * `ColorScheme` - A color scheme with Girly Pop's signature pastel colors
138fn get_girly_pop_scheme() -> ColorScheme {
139    ColorScheme {
140        informational: "237,69,146".to_string(), // rgb(237,69,146)
141        warning: "241,218,165".to_string(),      // rgb(241, 218, 165)
142        success: "243,214,243".to_string(),      // rgb(243, 214, 243)
143        question: "255,128,177".to_string(),     // rgb(255, 128, 177)
144        statement: "255,148,219".to_string(),    // rgb(255, 148, 219)
145    }
146}
147
148/// Returns the default color scheme with standard terminal colors.
149///
150/// # Returns
151/// * `ColorScheme` - A color scheme with standard, high-contrast colors
152fn get_default_scheme() -> ColorScheme {
153    ColorScheme {
154        informational: "255,215,0".to_string(), // Gold yellow
155        warning: "255,0,0".to_string(),         // Red
156        success: "0,255,0".to_string(),         // Green
157        question: "255,215,0".to_string(),      // Gold yellow (same as informational)
158        statement: "255,255,255".to_string(),   // White
159    }
160}
161
162/// Runs the first-time setup wizard for Ares, allowing users to configure their color scheme.
163///
164/// This function presents users with color scheme options and handles their selection,
165/// including support for custom color schemes. It guides users through the setup process
166/// with clear prompts and visual examples of each color scheme.
167///
168/// # Returns
169/// * `HashMap<String, String>` - A mapping of role names to their RGB color values
170pub fn run_first_time_setup() -> HashMap<String, String> {
171    println!(
172        "\n{}",
173        print_statement("🤠 Howdy! This is your first time running Ares.")
174    );
175    println!("{}", print_statement("Let me help you configure Ares."));
176
177    // ask if user wants a tutorial
178    if ask_yes_no_question("Do you want a tutorial?", true) {
179        println!("ares -t 'encoded text here' to decode.");
180        println!("Have a crib you know is in the plaintext? use --regex 'crib here'");
181        println!("yah that's it. Will write more when we add more :-D\n");
182    }
183
184    // Ask if the user wants a custom color scheme
185    let want_custom = ask_yes_no_question(
186        "Do you want a custom colour scheme? Will be applied after we're done configuring",
187        false,
188    );
189
190    let mut config = if !want_custom {
191        // User doesn't want a custom color scheme, use default
192        color_scheme_to_hashmap(get_default_scheme())
193    } else {
194        // Show color scheme options
195        println!(
196            "\n{}",
197            print_statement("What colour scheme looks best to you?")
198        );
199
200        println!("1. Capptucin");
201        let capptucin = get_capptucin_scheme();
202        print!("   ");
203        print!(
204            "{} | ",
205            print_rgb("Informational", &capptucin.informational)
206        );
207        print!("{} | ", print_rgb("Warning", &capptucin.warning));
208        print!("{} | ", print_rgb("Success", &capptucin.success));
209        print!("{} | ", print_rgb("Questions", &capptucin.question));
210        println!("{}\n", print_rgb("Statements", &capptucin.statement));
211
212        println!("2. Darcula");
213        let darcula = get_darcula_scheme();
214        print!("   ");
215        print!("{} | ", print_rgb("Informational", &darcula.informational));
216        print!("{} | ", print_rgb("Warning", &darcula.warning));
217        print!("{} | ", print_rgb("Success", &darcula.success));
218        print!("{} | ", print_rgb("Questions", &darcula.question));
219        println!("{}\n", print_rgb("Statements", &darcula.statement));
220
221        println!("3. 💖✨💐 GirlyPop");
222        let girly = get_girly_pop_scheme();
223        print!("   ");
224        print!("{} | ", print_rgb("Informational", &girly.informational));
225        print!("{} | ", print_rgb("Warning", &girly.warning));
226        print!("{} | ", print_rgb("Success", &girly.success));
227        print!("{} | ", print_rgb("Questions", &girly.question));
228        println!("{}\n", print_rgb("Statements", &girly.statement));
229
230        println!("4. Default");
231        let default = get_default_scheme();
232        print!("   ");
233        print!("{} | ", print_rgb("Informational", &default.informational));
234        print!("{} | ", print_rgb("Warning", &default.warning));
235        print!("{} | ", print_rgb("Success", &default.success));
236        print!("{} | ", print_rgb("Questions", &default.question));
237        println!("{}\n", print_rgb("Statements", &default.statement));
238
239        // For the Custom option, show format instructions
240        println!("5. Custom");
241        println!("   Format: r,g,b (e.g., 255,0,0 for red)");
242        println!("   Values must be between 0 and 255");
243        println!("   You'll be prompted to enter RGB values for each color.\n");
244
245        // Get user's choice
246        let choice = get_user_input_range("Enter your choice (1-5): ", 1, 5);
247
248        match choice {
249            1 => color_scheme_to_hashmap(get_capptucin_scheme()),
250            2 => color_scheme_to_hashmap(get_darcula_scheme()),
251            3 => color_scheme_to_hashmap(get_girly_pop_scheme()),
252            4 => color_scheme_to_hashmap(get_default_scheme()),
253            5 => {
254                // Custom color scheme
255                println!(
256                    "\n{}",
257                    print_statement("Enter RGB values for each color (format: r,g,b)")
258                );
259
260                let informational = get_user_input_rgb("Informational: ");
261                let warning = get_user_input_rgb("Warning: ");
262                let success = get_user_input_rgb("Success: ");
263                let question = get_user_input_rgb("Questions: ");
264                let statement = get_user_input_rgb("Statements: ");
265
266                let custom_scheme = ColorScheme {
267                    informational,
268                    warning,
269                    success,
270                    question,
271                    statement,
272                };
273
274                color_scheme_to_hashmap(custom_scheme)
275            }
276            _ => unreachable!(),
277        }
278    };
279
280    // ask about top_results
281    println!("\n{}", print_question("What sounds better to you?"));
282    println!(
283        "\n{}",
284        print_statement("1. Ares will ask you everytime it detects plaintext if it is plaintext.\n2. Ares stores all possible plaintext in a list, and at the end of the program presents it to you.")
285    );
286    let wait_athena_choice = get_user_input_range("Enter your choice", 1, 2);
287
288    // Store the top_results choice in the config
289    let top_results = wait_athena_choice == 2;
290    config.insert("top_results".to_string(), top_results.to_string());
291
292    // Set the default timeout
293    let mut timeout = 5; // Default timeout
294
295    if top_results {
296        // user has chosen to use top_results mode
297        println!(
298            "\n{}",
299            print_statement("Ares by default runs for 5 seconds. For this mode we suggest 3 seconds. Please do not complain if you choose too high of a number and your PC freezes up.\n")
300        );
301        timeout = get_user_input_range(
302            "How many seconds do you want Ares to run? (3 suggested) seconds",
303            1,
304            500,
305        );
306    }
307
308    // Store the timeout in the config
309    config.insert("timeout".to_string(), timeout.to_string());
310
311    // Wordlist configuration
312    println!(
313        "{}",
314        print_question("\nWould you like Ares to use custom wordlists to detect plaintext?")
315    );
316    println!(
317        "{}",
318        print_statement(
319            "Ares can use custom wordlists to detect plaintext by checking for exact matches."
320        )
321    );
322    println!(
323        "{}",
324        print_warning("Note: If your wordlist is very large, this can generate excessive matches.")
325    );
326
327    if ask_yes_no_question("", false) {
328        if let Some(wordlist_path) = get_wordlist_path() {
329            config.insert("wordlist_path".to_string(), wordlist_path);
330        }
331    }
332
333    // Enhanced detection section
334    println!(
335        "{}",
336        print_question("\nWould you like to enable Enhanced Plaintext Detection?")
337    );
338    println!("{}", print_statement("This will increase accuracy by around 40%, and you will be asked less frequently if something is plaintext or not."));
339    println!(
340        "{}",
341        print_statement("This will download a 500mb AI model.")
342    );
343    println!(
344        "{}",
345        print_statement("You will need to follow these steps to download it:")
346    );
347    println!(
348        "{}",
349        print_statement("1. Make a HuggingFace account https://huggingface.co/")
350    );
351    println!(
352        "{}",
353        print_statement("2. Make a READ Token https://huggingface.co/settings/tokens")
354    );
355    println!(
356        "{}",
357        print_warning(
358            "Note: You will be able to do this later by running `ares --enable-enhanced-detection`"
359        )
360    );
361    println!("{}", print_statement("We will prompt you for the token if you click Yes. We will not store this token, just use it to download a model."));
362
363    if ask_yes_no_question("", false) {
364        // Enable enhanced detection
365        config.insert("enhanced_detection".to_string(), "true".to_string());
366
367        // Set a default model path
368        let mut config_dir_path = crate::config::get_config_file_path();
369        config_dir_path.pop();
370        config_dir_path.push("models");
371
372        // Create the models directory if it doesn't exist
373        std::fs::create_dir_all(&config_dir_path).unwrap_or_else(|_| {
374            println!(
375                "{}",
376                print_warning(
377                    "Could not create models directory. Enhanced detection may not work."
378                )
379            );
380        });
381
382        config_dir_path.push("model.bin");
383
384        config.insert(
385            "model_path".to_string(),
386            config_dir_path.display().to_string(),
387        );
388
389        // Prompt for HuggingFace token
390        println!(
391            "{}",
392            print_statement("Please enter your HuggingFace token:")
393        );
394        print!(
395            "{}",
396            print_question("Token [invisible for privacy reasons]: ")
397        );
398        io::stdout().flush().unwrap();
399
400        // Use rpassword to hide the token input
401        let token = rpassword::read_password().unwrap_or_else(|_| String::new());
402
403        // Download the model using the token
404        if let Err(e) = download_model_with_progress_bar(&config_dir_path, Some(&token)) {
405            println!(
406                "{}",
407                print_warning(format!("Failed to download model: {}", e))
408            );
409            println!(
410                "{}",
411                print_warning("Enhanced detection may not work properly.")
412            );
413        } else {
414            println!("{}", print_success("Model downloaded successfully!"));
415        }
416    }
417
418    // show cute cat
419    if ask_yes_no_question("Do you want to see a cute cat?", false) {
420        println!(
421            r#"
422        /\_/\
423        ( o.o )
424        o( ( ))
425        "#
426        );
427    }
428
429    config
430}
431
432/// Prompts the user with a yes/no question and returns their response.
433///
434/// # Arguments
435/// * `question` - The question to display to the user
436/// * `default_yes` - Whether the default answer (when user presses enter) should be yes
437///
438/// # Returns
439/// * `bool` - true for yes, false for no
440fn ask_yes_no_question(question: &str, default_yes: bool) -> bool {
441    // Only print the question if it's not empty (for formatted sequences)
442    if !question.is_empty() {
443        println!("\n{}", print_question(question));
444    }
445
446    // Create the prompt
447    let prompt = if default_yes { "(Y/n): " } else { "(y/N): " };
448
449    print!("{}", print_question(prompt));
450    io::stdout().flush().unwrap();
451
452    let mut input = String::new();
453    io::stdin().read_line(&mut input).unwrap();
454
455    let input = input.trim().to_lowercase();
456
457    if input.is_empty() {
458        return default_yes;
459    }
460
461    match input.as_str() {
462        "y" | "yes" => true,
463        "n" | "no" => false,
464        _ => {
465            println!(
466                "{}",
467                print_warning("Invalid input. Please enter 'y' or 'n'.")
468            );
469            ask_yes_no_question(question, default_yes)
470        }
471    }
472}
473
474/// Gets user input within a specified numeric range.
475///
476/// # Arguments
477/// * `prompt` - The prompt to display to the user
478/// * `min` - The minimum acceptable value (inclusive)
479/// * `max` - The maximum acceptable value (inclusive)
480///
481/// # Returns
482/// * `u32` - The user's input within the specified range
483fn get_user_input_range(prompt: &str, min: u32, max: u32) -> u32 {
484    // Create the input prompt with the provided prompt text
485    let input_prompt = format!("{} ({}-{}): ", prompt, min, max);
486    print!("{}", print_question(input_prompt));
487    io::stdout().flush().unwrap();
488
489    let mut input = String::new();
490    io::stdin().read_line(&mut input).unwrap();
491
492    let input = input.trim();
493
494    match input.parse::<u32>() {
495        Ok(num) if num >= min && num <= max => num,
496        _ => {
497            println!(
498                "{}",
499                print_warning(format!(
500                    "Invalid input. Please enter a number between {} and {}.",
501                    min, max
502                ))
503            );
504            get_user_input_range(prompt, min, max)
505        }
506    }
507}
508
509/// Gets user input for RGB color values.
510///
511/// # Arguments
512/// * `prompt` - The prompt to display to the user
513///
514/// # Returns
515/// * `String` - A validated RGB color string in format "r,g,b"
516fn get_user_input_rgb(prompt: &str) -> String {
517    print!("{}", print_question(prompt));
518    io::stdout().flush().unwrap();
519
520    let mut input = String::new();
521    io::stdin().read_line(&mut input).unwrap();
522
523    let input = input.trim();
524
525    // Validate RGB format (r,g,b)
526    if let Some(rgb) = parse_rgb_input(input) {
527        rgb
528    } else {
529        println!(
530            "{}",
531            print_warning("Invalid RGB format. Please use the format 'r,g,b' (e.g., '255,0,0').")
532        );
533        get_user_input_rgb(prompt)
534    }
535}
536
537/// Parses and validates an RGB input string.
538///
539/// # Arguments
540/// * `input` - The RGB string to parse in format "r,g,b"
541///
542/// # Returns
543/// * `Option<String>` - Some(rgb) if valid, None if invalid
544fn parse_rgb_input(input: &str) -> Option<String> {
545    let parts: Vec<&str> = input.split(',').collect();
546
547    if parts.len() != 3 {
548        return None;
549    }
550
551    let r = parts[0].trim().parse::<u8>().ok()?;
552    let g = parts[1].trim().parse::<u8>().ok()?;
553    let b = parts[2].trim().parse::<u8>().ok()?;
554
555    Some(format!("{},{},{}", r, g, b))
556}
557
558/// Converts a ColorScheme struct to a HashMap for configuration storage.
559///
560/// # Arguments
561/// * `scheme` - The ColorScheme to convert
562///
563/// # Returns
564/// * `HashMap<String, String>` - A mapping of role names to their RGB values
565fn color_scheme_to_hashmap(scheme: ColorScheme) -> HashMap<String, String> {
566    let mut map = HashMap::new();
567    map.insert("informational".to_string(), scheme.informational);
568    map.insert("warning".to_string(), scheme.warning);
569    map.insert("success".to_string(), scheme.success);
570    map.insert("question".to_string(), scheme.question);
571    map.insert("statement".to_string(), scheme.statement);
572    map
573}
574
575/// Prompts the user for a wordlist file path and validates that the file exists
576/// Returns the path if valid, or None if the user cancels
577fn get_wordlist_path() -> Option<String> {
578    println!(
579        "\n{}",
580        print_statement("Enter the path to your wordlist file:")
581    );
582    println!("{}", print_statement("(Leave empty to cancel)"));
583
584    let mut input = String::new();
585    std::io::stdin()
586        .read_line(&mut input)
587        .expect("Failed to read input");
588    let input = input.trim();
589
590    if input.is_empty() {
591        println!("{}", print_statement("No wordlist will be used."));
592        return None;
593    }
594
595    // Check if the file exists
596    if !Path::new(input).exists() {
597        println!("{}", print_warning("File does not exist!"));
598        return get_wordlist_path(); // Recursively prompt until valid or cancelled
599    }
600
601    // Check if the file is readable
602    match std::fs::File::open(input) {
603        Ok(_) => Some(input.to_string()),
604        Err(e) => {
605            println!("{}", print_warning(format!("Cannot read file: {}", e)));
606            get_wordlist_path() // Recursively prompt until valid or cancelled
607        }
608    }
609}