cp-cli 0.9.1

Human-first command-line client for competitive-programming platforms
Documentation
use std::path::PathBuf;

use clap::{Args, Parser, Subcommand, ValueEnum};

use crate::domain::{
    AccountName, ContestSlug, Difficulty, DiscussionId, LanguageSlug, PracticeTopic, ProblemQuery,
    ProblemSelector, ProblemTag,
};

#[derive(Parser)]
#[command(
    version,
    disable_help_subcommand = true,
    about = "Solve problems across coding platforms from your terminal",
    after_help = "Get started:\n  cp-cli problem daily\n  cp-cli problem list --difficulty medium --tag graph\n  cp-cli problem search \"two sum\"\n  cp-cli problem show 1\n  cp-cli --platform hackerrank problem show solve-me-first\n  cp-cli --platform codeforces problem show 4A\n  cp-cli --platform exercism problem pick two-fer --track rust\n  cp-cli --platform project-euler problem show 1\n  cp-cli --platform hackerearth problem list --topic algorithms/searching/linear-search\n  cp-cli --platform codechef stats ksun48\n  cp-cli problem pick 1\n  cp-cli stats\n  cp-cli contest list\n  cp-cli discussion list\n  cp-cli discussion show 8533478\n\nUse the identifiers and paths returned by each platform.\nFor scripts, add --format json."
)]
pub(crate) struct Cli {
    #[arg(long, value_enum, global = true, default_value = "leetcode")]
    pub(crate) platform: Platform,
    /// Readable text by default; JSON for scripts
    #[arg(long, value_enum, global = true, default_value = "text")]
    pub(crate) format: Format,
    /// Colorize terminal output (auto respects NO_COLOR)
    #[arg(long, value_enum, global = true, default_value = "auto")]
    pub(crate) color: Color,
    /// Enlarge titles in supported terminals when space permits
    #[arg(long, value_enum, global = true, default_value = "auto")]
    pub(crate) heading_size: HeadingSize,
    /// Possum, arcade, phosphor, amber, moonlight, or neutral colors
    #[arg(long, value_enum, global = true, default_value = "possum")]
    pub(crate) theme: Theme,
    /// Match the terminal profile, or explicitly select its background tone
    #[arg(long, value_enum, global = true, default_value = "auto")]
    pub(crate) background: Background,
    /// Disable terminal motion
    #[arg(long, global = true)]
    pub(crate) no_animation: bool,
    /// Ring the terminal bell when the problem is ready
    #[arg(long, global = true)]
    pub(crate) sound: bool,
    #[command(subcommand)]
    pub(crate) command: Command,
}

#[derive(Clone, Copy, PartialEq, Eq, ValueEnum)]
pub(crate) enum Platform {
    #[value(name = "codechef")]
    CodeChef,
    #[value(name = "codeforces")]
    Codeforces,
    #[value(name = "exercism")]
    Exercism,
    #[value(name = "hackerearth")]
    HackerEarth,
    #[value(name = "hackerrank")]
    HackerRank,
    #[value(name = "leetcode")]
    LeetCode,
    #[value(name = "project-euler")]
    ProjectEuler,
}

impl Platform {
    pub(crate) fn label(self) -> &'static str {
        match self {
            Self::CodeChef => "CodeChef",
            Self::Codeforces => "Codeforces",
            Self::Exercism => "Exercism",
            Self::HackerEarth => "HackerEarth",
            Self::HackerRank => "HackerRank",
            Self::LeetCode => "LeetCode",
            Self::ProjectEuler => "Project Euler",
        }
    }
}

#[derive(Clone, Copy, ValueEnum)]
pub(crate) enum Format {
    Text,
    Json,
}

#[derive(Clone, Copy, ValueEnum)]
pub(crate) enum Color {
    Auto,
    Always,
    Never,
}

#[derive(Clone, Copy, ValueEnum)]
pub(crate) enum HeadingSize {
    Auto,
    Normal,
    Large,
}

#[derive(Clone, Copy, ValueEnum)]
pub(crate) enum Theme {
    Possum,
    Arcade,
    Moonlight,
    Phosphor,
    Amber,
    Dark,
    Light,
}

#[derive(Clone, Copy, ValueEnum)]
pub(crate) enum Background {
    Auto,
    Dark,
    Light,
}

#[derive(Clone, Copy, ValueEnum)]
pub(crate) enum ListDifficulty {
    Easy,
    Medium,
    Hard,
}

impl From<ListDifficulty> for Difficulty {
    fn from(value: ListDifficulty) -> Self {
        match value {
            ListDifficulty::Easy => Self::Easy,
            ListDifficulty::Medium => Self::Medium,
            ListDifficulty::Hard => Self::Hard,
        }
    }
}

#[derive(Subcommand)]
pub(crate) enum Command {
    /// Configure or inspect supported terminal authentication
    Auth {
        #[command(subcommand)]
        command: AuthCommand,
    },
    /// Browse and participate in platform contests
    Contest {
        #[command(subcommand)]
        command: ContestCommand,
    },
    /// Browse public discussions
    Discussion {
        #[command(subcommand)]
        command: DiscussionCommand,
    },
    /// Browse public problems
    Problem {
        #[command(subcommand)]
        command: ProblemCommand,
    },
    /// Show account progress and recent submissions
    Stats {
        /// Public account name when the platform cannot identify an authenticated session
        #[arg(value_name = "ACCOUNT")]
        user: Option<AccountName>,
    },
}

impl Command {
    pub(crate) fn path(&self) -> &'static str {
        match self {
            Self::Auth { command } => match command {
                AuthCommand::Login => "auth login",
                AuthCommand::Logout => "auth logout",
                AuthCommand::Status => "auth status",
            },
            Self::Contest { command } => match command {
                ContestCommand::Create => "contest create",
                ContestCommand::Delete { .. } => "contest delete",
                ContestCommand::Edit { .. } => "contest edit",
                ContestCommand::Join { .. } => "contest join",
                ContestCommand::Leave { .. } => "contest leave",
                ContestCommand::List => "contest list",
                ContestCommand::Show { .. } => "contest show",
                ContestCommand::Status { .. } => "contest status",
            },
            Self::Discussion { command } => match command {
                DiscussionCommand::Create { .. } => "discussion create",
                DiscussionCommand::Delete { .. } => "discussion delete",
                DiscussionCommand::Edit { .. } => "discussion edit",
                DiscussionCommand::List { .. } => "discussion list",
                DiscussionCommand::Show { .. } => "discussion show",
                DiscussionCommand::Reply { .. } => "discussion reply",
            },
            Self::Problem { command } => match command {
                ProblemCommand::Daily => "problem daily",
                ProblemCommand::List { .. } => "problem list",
                ProblemCommand::Pick { .. } => "problem pick",
                ProblemCommand::Test(_) => "problem test",
                ProblemCommand::Submit(_) => "problem submit",
                ProblemCommand::Show { .. } => "problem show",
                ProblemCommand::Search { .. } => "problem search",
            },
            Self::Stats { .. } => "stats",
        }
    }
}

#[derive(Subcommand)]
#[command(disable_help_subcommand = true)]
pub(crate) enum DiscussionCommand {
    /// Create a discussion through a verified platform API
    Create {
        /// Optional problem number or slug for a solution post
        #[arg(value_name = "NUMBER|SLUG")]
        problem: Option<ProblemSelector>,
    },
    /// Delete a discussion through a verified platform API
    Delete {
        /// Numeric topic ID from the platform discussion URL
        #[arg(value_name = "ID")]
        discussion: DiscussionId,
    },
    /// Edit a discussion through a verified platform API
    Edit {
        /// Numeric topic ID from the platform discussion URL
        #[arg(value_name = "ID")]
        discussion: DiscussionId,
    },
    /// List trending posts, or recent posts for one problem
    List {
        /// Optional problem number or slug
        #[arg(value_name = "NUMBER|SLUG")]
        problem: Option<ProblemSelector>,
    },
    /// Show one discussion by its numeric topic ID
    Show {
        /// Numeric topic ID from a LeetCode discussion URL
        #[arg(value_name = "ID")]
        discussion: DiscussionId,
    },
    /// Reply through a verified platform API
    Reply {
        /// Numeric topic ID from a LeetCode discussion URL
        #[arg(value_name = "ID")]
        discussion: DiscussionId,
    },
}

#[derive(Subcommand)]
#[command(disable_help_subcommand = true)]
pub(crate) enum ContestCommand {
    /// Create a contest through a verified platform API
    Create,
    /// Edit a contest through a verified platform API
    Edit {
        /// Contest ID or slug from its platform URL
        #[arg(value_name = "ID")]
        contest: ContestSlug,
    },
    /// Delete a contest through a verified platform API
    Delete {
        /// Contest ID or slug from its platform URL
        #[arg(value_name = "ID")]
        contest: ContestSlug,
    },
    /// Join a contest through a verified platform API
    Join {
        /// Contest slug from its LeetCode URL
        #[arg(value_name = "SLUG")]
        contest: ContestSlug,
    },
    /// Leave a contest through a verified platform API
    Leave {
        /// Contest slug from its LeetCode URL
        #[arg(value_name = "SLUG")]
        contest: ContestSlug,
    },
    /// List upcoming contests
    List,
    /// Show one contest's schedule
    Show {
        /// Contest slug from its LeetCode URL
        #[arg(value_name = "SLUG")]
        contest: ContestSlug,
    },
    /// Show whether the signed-in account joined a contest
    Status {
        /// Contest slug from its LeetCode URL
        #[arg(value_name = "SLUG")]
        contest: ContestSlug,
    },
}

#[derive(Subcommand)]
#[command(disable_help_subcommand = true)]
pub(crate) enum AuthCommand {
    /// Configure supported account credentials
    Login,
    /// Remove credentials saved by this CLI
    Logout,
    /// Check whether configured credentials authenticate successfully
    Status,
}

#[derive(Args)]
pub(crate) struct SolutionTarget {
    /// Solution file created by problem pick
    #[arg(value_name = "FILE")]
    pub(crate) path: PathBuf,
}

#[derive(Subcommand)]
#[command(disable_help_subcommand = true)]
pub(crate) enum ProblemCommand {
    /// Show the platform's daily challenge when available
    Daily,
    /// Browse public problems
    List {
        /// Easy, medium or hard
        #[arg(long, value_enum)]
        difficulty: Option<ListDifficulty>,
        /// LeetCode tag slug, such as graph or dynamic-programming
        #[arg(long, value_name = "TAG")]
        tag: Option<ProblemTag>,
        /// Exercism or HackerRank track slug
        #[arg(long, value_name = "TRACK")]
        track: Option<ProblemTag>,
        /// HackerEarth practice path, such as algorithms/searching/linear-search
        #[arg(long, value_name = "PATH")]
        topic: Option<PracticeTopic>,
        /// Catalogue page
        #[arg(long, value_name = "PAGE")]
        page: Option<u8>,
        /// Show Project Euler's ten newest problems
        #[arg(long, conflicts_with = "page")]
        recent: bool,
    },
    /// Choose a language starter and save it locally
    Pick {
        /// Positive problem number or slug from the problem URL
        #[arg(value_name = "NUMBER|SLUG")]
        problem: ProblemSelector,
        /// Platform language slug; omitted values are prompted for when needed
        #[arg(long, value_name = "LANG")]
        lang: Option<LanguageSlug>,
        /// Destination directory; omitted values are prompted for
        #[arg(long, value_name = "DIR")]
        dir: Option<PathBuf>,
        /// Exercism track slug, such as rust or python
        #[arg(long, value_name = "TRACK")]
        track: Option<ProblemTag>,
    },
    /// Run the platform's supported local or remote tests
    Test(SolutionTarget),
    /// Submit the marked solution through the supported platform workflow
    Submit(SolutionTarget),
    /// Show a problem's details and available statement
    Show {
        /// Problem ID from the platform URL
        #[arg(value_name = "ID")]
        problem: ProblemSelector,
        /// Exercism track slug, such as rust or python
        #[arg(long, value_name = "TRACK")]
        track: Option<ProblemTag>,
    },
    /// Search public problems by title or number
    Search {
        /// Search text, such as two sum or 1
        #[arg(value_name = "QUERY")]
        query: ProblemQuery,
    },
}