typeman 1.0.1

Typing speed test with practice mode in GUI, TUI and CLI
Documentation
#![allow(clippy::all)]

#[cfg(not(any(feature = "cli", feature = "tui", feature = "gui")))]
compile_error!("At least one of 'cli', 'tui', or 'gui' must be enabled");

use clap::{Parser, ValueHint};
use serde::Deserialize;
use std::path::PathBuf;

pub mod ui {
    #[cfg(feature = "cli")]
    pub mod cli {
        pub mod main;
        pub mod modes;
    }

    #[cfg(feature = "gui")]
    pub mod gui {
        pub mod config;
        pub mod main;
        pub mod popup;
        pub mod practice;
        pub mod results;
    }

    #[cfg(feature = "tui")]
    pub mod tui {
        pub mod app;
        pub mod r#mod;
        pub mod ui;
        pub mod popup;
    }
}

pub mod button_states;
pub mod color_scheme;
pub mod config;
pub mod custom_colors;
pub mod language;
pub mod leaderboard;
pub mod practice;
pub mod utils;
pub mod time_selection;
pub mod word_number_selection;
pub mod settings;
pub mod batch_size_selection;
pub mod top_words_selection;

#[cfg(feature = "cli")]
use crate::ui::cli::modes;

#[cfg(feature = "tui")]
use crate::ui::tui::r#mod as tui_mod;

#[cfg(feature = "gui")]
use crate::ui::gui::main as gui;

#[derive(Parser)]
#[command(
    name = "typeman",
    about = "Welcome to the typeman!",
    version = "1.0",
    after_long_help = "Run examples:
typeman --cli -c ./text.txt
typeman --cli -q
typeman --cli -t=30 -n=500
typeman --cli -w=50 -n=500
typeman --cli -w=50 -n=500 -p -d
typeman --cli --wiki
typeman --gui
typeman",
    long_about = "\n
Run 'typeman --cli -c <path/to/your/file>' to test your typing on a specified text
Run 'typeman --cli -q' to test your typing on a random quote
Run 'typeman --cli -w=50 (-n=500 -p -d)' to test your typing on n most common English words, specify the -w for number of words (default is 50)
Run 'typeman --cli (-t=30 -n=500 -p -d)' to test your typing on random words for t seconds; -t sets the time limit (default is 30 seconds)
Run 'typeman (--tui)' to start the terminal-based interface

Optional:
  - Use -p to include punctuation, -d to include digits
  - Use -n to specify the number of words to type (default is 50, max is 500)
  - Use -t to set a time limit for the test (default is 30 seconds, use 0 for no limit)
  - Use -n to specify the number of top words to use (default is 500, max is 1000)
  - Use --gui for terminal-based interface
  - Use --cli for terminal-based interface

Default behavior for cli is to test typing on random words for 30 seconds with 500 most common English words.
Default mode is tui.
    "
)]
pub struct Cli {
    #[arg(short = 'c', long = "custom", value_name = "FILE", value_hint = ValueHint::FilePath, conflicts_with_all = &["random_quote", "time_limit", "top_words", "word_number", "gui", "tui"])]
    custom_file: Option<PathBuf>,

    #[arg(short = 'q', long = "quote", conflicts_with_all = &["custom_file", "time_limit", "top_words", "gui", "tui"])]
    random_quote: bool,

    #[arg(short = 'p', long = "punctuation", conflicts_with_all = &["custom_file", "random_quote", "gui", "tui"])]
    punctuation: bool,

    #[arg(short = 'd', long = "digits", conflicts_with_all = &["custom_file", "random_quote", "gui", "tui"])]
    digits: bool,

    #[arg(short = 't', long = "time", value_name = "SECONDS", conflicts_with_all = &["gui", "tui"])]
    time_limit: Option<Option<u64>>,

    #[arg(short = 'n', long = "top_words", value_name = "WORDS", conflicts_with_all = &["gui", "tui"])]
    top_words: Option<usize>,

    #[arg(short = 'w', long = "word_number", value_name = "WORDS", num_args = 0..=1, conflicts_with_all = &["gui", "tui"])]
    word_number: Option<Option<usize>>,

    #[arg(short = 'l', long = "level", conflicts_with_all = &["custom_file", "random_quote", "time_limit", "top_words"], conflicts_with_all = &["gui", "tui"])]
    level: Option<Option<usize>>,

    #[arg(long = "gui", conflicts_with_all = &["custom_file", "random_quote", "time_limit", "top_words", "word_number", "level", "tui", "cli"])]
    gui: bool,

    #[arg(long = "tui", conflicts_with_all = &["custom_file", "random_quote", "time_limit", "top_words", "word_number", "level", "gui", "cli"])]
    tui: bool,

    #[arg(long = "cli", conflicts_with_all = &["tui", "gui"])]
    cli: bool,

    #[arg(long = "wiki", conflicts_with_all = &["custom_file", "random_quote", "time_limit", "top_words", "word_number", "level", "tui"])]
    wiki: bool,

    #[arg(
        long = "lang",
        value_name = "LANGUAGE",
        help = "Language for word lists (english, indonesian)"
    )]
    language: Option<String>,
}

#[derive(Debug, Deserialize)]
pub struct Quote {
    author: String,
    text: String,
}

#[cfg(feature = "gui")]
pub fn gui_main() {
    macroquad::Window::new("TypeMan", async { gui::gui_main_async().await });
}

#[cfg(feature = "tui")]
pub fn tui_main() {
    if let Err(e) = tui_mod::main() {
        eprintln!("TUI error: {}", e);
        std::process::exit(1);
    }
}

fn main() {
    let args = Cli::parse();

    if args.tui && !cfg!(feature = "tui") {
        eprintln!("TUI mode is not available in this build.");
        std::process::exit(1);
    }

    if args.gui && !cfg!(feature = "gui") {
        eprintln!("GUI mode is not available in this build.");
        std::process::exit(1);
    }

    if args.cli && !cfg!(feature = "cli") {
        eprintln!("CLI mode is not available in this build.");
        std::process::exit(1);
    }

    #[cfg(feature = "gui")]
    if args.gui {
        gui_main();
        return;
    }

    #[cfg(feature = "cli")]
    if args.cli {
        run_cli(&args);
        return;
    }

    #[cfg(feature = "tui")]
    {
        ui::tui::r#mod::main().unwrap();
        return;
    }
}

#[cfg(feature = "cli")]
fn run_cli(args: &Cli) {
    if let Some(path) = args.custom_file.as_ref() {
        modes::custom_text(path)
    } else if args.random_quote {
        modes::quotes();
    } else if args.level.is_some() {
        modes::practice(args);
    } else if args.word_number.is_some() && args.time_limit.is_none() {
        modes::word_mode(args);
    } else if args.wiki {
        modes::wiki_mode();
    } else {
        modes::time_mode(args);
    }
}