diff --git a/leetcode-tui-core/src/content/question.rs b/leetcode-tui-core/src/content/question.rs index cc36a4c..5f7e87b 100644 --- a/leetcode-tui-core/src/content/question.rs +++ b/leetcode-tui-core/src/content/question.rs @@ -6,17 +6,20 @@ use crate::{emit, utils::Paginate}; use fuzzy_matcher::skim::SkimMatcherV2; use fuzzy_matcher::FuzzyMatcher; use html2md::parse_html; +use leetcode_core::errors::AppResult; use leetcode_core::graphql::query::{daily_coding_challenge, RunOrSubmitCodeCheckResult}; +use leetcode_core::types::language::Language; use leetcode_core::types::run_submit_response::display::CustomDisplay; use leetcode_core::types::run_submit_response::ParsedResponse; use leetcode_core::{ GQLLeetcodeRequest, QuestionContentRequest, RunCodeRequest, SubmitCodeRequest, }; use leetcode_tui_config::log; +use leetcode_tui_db::enums::Difficulty; use leetcode_tui_db::{DbQuestion, DbTopic}; use leetcode_tui_shared::layout::Window; pub(crate) use sol_dir::init; -use sol_dir::SOLUTION_FILE_MANAGER; +use sol_dir::{SolutionFile, SolutionFileManager, SOLUTION_FILE_MANAGER}; use stats::Stats; use std::rc::Rc; @@ -25,6 +28,7 @@ pub struct Questions { ques_haystack: Vec>, needle: Option, matcher: SkimMatcherV2, + difficulty: Option, show_stats: bool, } @@ -36,6 +40,7 @@ impl Default for Questions { ques_haystack: vec![], matcher: Default::default(), show_stats: Default::default(), + difficulty: Default::default(), } } } @@ -69,7 +74,7 @@ impl Questions { return true; } else { emit!(Popup( - "not", + "Adhoc question not found", vec![format!( "Question not found with id={}, title={}", question.id, question.title @@ -124,93 +129,67 @@ impl Questions { self._run_solution(true) } + pub fn get_solution_language_list<'a>( + question_id: String, + ) -> Result, crate::errors::CoreError> { + SolutionFileManager::get_instance() + .get_available_languages(question_id.as_str()) + .map(|x| x.into_iter().map(|x| x.clone()).collect()) + } + fn _run_solution(&self, is_submit: bool) -> bool { if let Some(_hovered) = self.hovered() { let mut cloned_quest = _hovered.as_ref().clone(); let id = _hovered.id.to_string(); - if let Ok(lang_refs) = SOLUTION_FILE_MANAGER - .get() - .unwrap() - .read() - .unwrap() - .get_available_languages(id.as_str()) - .emit_if_error() - { + + // get solution language list + if let Ok(lang_refs) = Self::get_solution_language_list(id.clone()).emit_if_error() { let cloned_langs = lang_refs.iter().map(|v| v.to_string()).collect(); tokio::spawn(async move { + // popup that gets the input for language to run solution in if let Some(selected_lang) = emit!(SelectPopup("Available solutions in", cloned_langs)).await { - let selected_sol_file = SOLUTION_FILE_MANAGER - .get() - .unwrap() - .read() - .unwrap() + // fetch solution content + let selected_sol_file = SolutionFileManager::get_instance() .get_solution_file(id.as_str(), selected_lang) .cloned(); + if let Ok(f) = selected_sol_file.emit_if_error() { - if let Ok(contents) = f.read_contents().await.emit_if_error() { - let lang = f.language; - let request = if is_submit { - SubmitCodeRequest::new( - lang, - f.question_id, - contents, - f.title_slug, - ) - .poll_check_response() - .await - } else { - let mut run_code_req = RunCodeRequest::new( - lang, - None, - f.question_id, - contents, - f.title_slug, - ); - if let Err(e) = run_code_req - .set_sample_test_cases_if_none() - .await - .emit_if_error() - { - log::info!( - "error while setting the sample testcase list {}", - e - ); - return; - } else { - run_code_req.poll_check_response().await - } - }; - - if let Ok(response) = request.emit_if_error() { - if let Ok(update_result) = - cloned_quest.mark_attempted().emit_if_error() - { - // when solution is just run against sample cases - if update_result.is_some() { - // fetches latest result from db - emit!(QuestionUpdate); - } + let request = if is_submit { + Self::send_http_request_to_submit_solved_question(&f).await + } else { + Self::send_http_request_to_run_solved_question(&f).await + }; + + if let Ok(response) = request.emit_if_error() { + if let Ok(update_result) = + cloned_quest.mark_attempted().emit_if_error() + { + // when solution is just run against sample cases + if update_result.is_some() { + // fetches latest result from db + emit!(QuestionUpdate); } + } - if is_submit { - let is_submission_accepted = - matches!(response, ParsedResponse::SubmitAccepted(..)); - if is_submission_accepted { - if let Ok(update_result) = - cloned_quest.mark_accepted().emit_if_error() - { - // when solution is accepted - if update_result.is_some() { - // fetches latest result from db - emit!(QuestionUpdate); - } - }; - } + if is_submit { + let is_submission_accepted = + matches!(response, ParsedResponse::SubmitAccepted(..)); + if is_submission_accepted { + if let Ok(update_result) = + cloned_quest.mark_accepted().emit_if_error() + { + // when solution is accepted + if update_result.is_some() { + // fetches latest result from db + emit!(QuestionUpdate); + } + }; } - emit!(Popup(response.get_display_lines())); } + + emit!(Popup(response.get_display_lines())); } } } @@ -220,6 +199,38 @@ impl Questions { false } + async fn send_http_request_to_submit_solved_question( + f: &SolutionFile, + ) -> AppResult { + let solution_text = f.read_contents().await.expect("Cannot read file contents"); + SubmitCodeRequest::new( + f.language.clone(), + f.question_id.clone(), + solution_text, + f.title_slug.clone(), + ) + .poll_check_response() + .await + } + + async fn send_http_request_to_run_solved_question( + f: &SolutionFile, + ) -> AppResult { + let solution_text = f.read_contents().await.expect("Cannot read file contents"); + let mut run_code_req = RunCodeRequest::new( + f.language.clone(), + None, + f.question_id.clone(), + solution_text, + f.title_slug.clone(), + ); + run_code_req + .set_sample_test_cases_if_none() + .await + .expect("error while setting the sample testcase list"); + run_code_req.poll_check_response().await + } + pub fn solve_for_language(&self) -> bool { if let Some(_hovered) = self.hovered() { let slug = _hovered.title_slug.clone(); @@ -280,6 +291,7 @@ impl Questions { pub fn set_questions(&mut self, questions: Vec) { self.ques_haystack = questions.into_iter().map(Rc::new).collect(); + self.ques_haystack.sort(); self.filter_questions(); } @@ -335,7 +347,6 @@ impl Questions { } fn filter_questions(&mut self) { - self.ques_haystack.sort(); let fil_quests = if let Some(needle) = self.needle.as_ref() { let quests: Vec> = self .ques_haystack @@ -364,6 +375,29 @@ impl Questions { }; self.paginate.update_list(fil_quests); } + + pub fn toggle_difficulty(&mut self) -> bool { + match self.difficulty { + Some(Difficulty::Easy) => self.difficulty = Some(Difficulty::Medium), + Some(Difficulty::Medium) => self.difficulty = Some(Difficulty::Hard), + Some(Difficulty::Hard) => self.difficulty = None, + None => self.difficulty = Some(Difficulty::Easy), + } + + let fil_quests: Vec> = self + .ques_haystack + .iter() + .filter(|q| { + self.difficulty + .as_ref() + .map_or(true, |d| d == &q.get_difficulty()) + }) + .cloned() + .collect(); + + self.paginate.update_list(fil_quests); + return true; + } } impl Questions { diff --git a/leetcode-tui-core/src/content/question/sol_dir.rs b/leetcode-tui-core/src/content/question/sol_dir.rs index 3a9f036..1287476 100644 --- a/leetcode-tui-core/src/content/question/sol_dir.rs +++ b/leetcode-tui-core/src/content/question/sol_dir.rs @@ -1,9 +1,14 @@ use indexmap::IndexSet; -use std::{collections::HashMap, hash::Hash, path::PathBuf, sync::RwLock}; +use std::{ + collections::HashMap, + hash::Hash, + path::PathBuf, + sync::{RwLock, RwLockReadGuard}, +}; use tokio::fs::read_to_string; -use leetcode_tui_config::CONFIG; use leetcode_core::types::language::Language; +use leetcode_tui_config::CONFIG; use regex::Regex; use std::sync::OnceLock; @@ -43,6 +48,10 @@ pub struct SolutionFileManager { } impl SolutionFileManager { + pub fn get_instance() -> RwLockReadGuard<'static, Self> { + return SOLUTION_FILE_MANAGER.get().unwrap().read().unwrap(); + } + fn add_solution_file(&mut self, file: SolutionFile) { self.id_language_map .entry(file.question_id.clone()) diff --git a/leetcode-tui-core/src/content/question/stats.rs b/leetcode-tui-core/src/content/question/stats.rs index 36a3735..025f2fb 100644 --- a/leetcode-tui-core/src/content/question/stats.rs +++ b/leetcode-tui-core/src/content/question/stats.rs @@ -1,5 +1,5 @@ use leetcode_tui_config::CONFIG; -use leetcode_tui_db::DbQuestion; +use leetcode_tui_db::{enums::Difficulty, DbQuestion}; use ratatui::style::Style; use std::{fmt::Display, rc::Rc}; @@ -97,35 +97,35 @@ impl<'a> Stats<'a> { } pub fn get_easy_count(&self) -> usize { - self.get_diff_count("Easy") + self.get_diff_count(Difficulty::Easy) } pub fn get_medium_count(&self) -> usize { - self.get_diff_count("Medium") + self.get_diff_count(Difficulty::Medium) } pub fn get_hard_count(&self) -> usize { - self.get_diff_count("Hard") + self.get_diff_count(Difficulty::Hard) } pub fn get_easy_accepted(&self) -> usize { - self.get_diff_accepted("ac", "Easy") + self.get_diff_accepted("ac", Difficulty::Easy) } pub fn get_medium_accepted(&self) -> usize { - self.get_diff_accepted("ac", "Medium") + self.get_diff_accepted("ac", Difficulty::Medium) } pub fn get_hard_accepted(&self) -> usize { - self.get_diff_accepted("ac", "Hard") + self.get_diff_accepted("ac", Difficulty::Hard) } - pub fn get_diff_accepted(&self, status: &str, difficulty: &str) -> usize { + pub fn get_diff_accepted(&self, status: &str, difficulty: Difficulty) -> usize { self.qm .iter() .filter(|q| { if let Some(st) = &q.status { - st.as_str() == status && difficulty == q.difficulty.as_str() + st.as_str() == status && difficulty == q.get_difficulty() } else { false } @@ -146,10 +146,10 @@ impl<'a> Stats<'a> { .count() } - fn get_diff_count(&self, difficulty: &str) -> usize { + fn get_diff_count(&self, difficulty: Difficulty) -> usize { self.qm .iter() - .filter(|q| q.difficulty.as_str() == difficulty) + .filter(|q| q.get_difficulty() == difficulty) .count() } } diff --git a/leetcode-tui-core/src/help.rs b/leetcode-tui-core/src/help.rs index 56c0100..9a2bb77 100644 --- a/leetcode-tui-core/src/help.rs +++ b/leetcode-tui-core/src/help.rs @@ -11,9 +11,6 @@ impl Default for Help { let mut help = Self { state: TableState::default(), items: vec![ - vec!["t", "Move to Next Topic"], - vec!["T", "Move to Previous Topic"], - vec!["Ctrl+s", "Show/Hide topic stats"], vec!["j/Down", "Move to Next Question"], vec!["k/Up", "Move to Previous Question"], vec!["r", "Move to Random Question"], @@ -21,9 +18,14 @@ impl Default for Help { vec!["e", "Open Editor"], vec!["R", "Run Solution"], vec!["s", "Submit Solution"], + vec!["t", "Move to Next Topic"], + vec!["T", "Move to Previous Topic"], + vec!["Ctrl+s", "Show/Hide topic stats"], + vec!["Tab", "Toggle difficulty"], vec!["/", "Search"], vec!["c", "Open config file"], vec!["*", "Sync database!"], + vec!["q", "quit!"], ], visible: Default::default(), }; diff --git a/leetcode-tui-db/src/enums.rs b/leetcode-tui-db/src/enums.rs new file mode 100644 index 0000000..df44435 --- /dev/null +++ b/leetcode-tui-db/src/enums.rs @@ -0,0 +1,7 @@ +#[derive(Default, PartialEq, Eq)] +pub enum Difficulty { + #[default] + Easy, + Medium, + Hard, +} diff --git a/leetcode-tui-db/src/lib.rs b/leetcode-tui-db/src/lib.rs index 07cdd01..462f344 100644 --- a/leetcode-tui-db/src/lib.rs +++ b/leetcode-tui-db/src/lib.rs @@ -1,3 +1,4 @@ +pub mod enums; pub mod errors; pub mod models; use errors::DBResult; diff --git a/leetcode-tui-db/src/models/question.rs b/leetcode-tui-db/src/models/question.rs index 5bef312..cd9ae7a 100644 --- a/leetcode-tui-db/src/models/question.rs +++ b/leetcode-tui-db/src/models/question.rs @@ -1,6 +1,7 @@ use super::{topic::DbTopic, *}; use crate::{ api::types::problemset_question_list::Question, + enums::Difficulty, errors::{DBResult, DbErr}, get_db_client, save, save_multiple, }; @@ -14,7 +15,7 @@ pub struct DbQuestion { pub id: u32, pub title: String, pub title_slug: String, - pub difficulty: String, + difficulty: String, pub paid_only: bool, pub status: Option, pub topics: Vec, @@ -51,6 +52,15 @@ impl Display for DbQuestion { } impl DbQuestion { + pub fn get_difficulty(&self) -> Difficulty { + match self.difficulty.as_str() { + "Hard" => Difficulty::Hard, + "Medium" => Difficulty::Medium, + "Easy" => Difficulty::Easy, + _ => panic!("Cannot parse difficulty"), + } + } + pub fn is_hard(&self) -> bool { self.difficulty == "Hard" } diff --git a/leetcode-tui-rs/src/executor.rs b/leetcode-tui-rs/src/executor.rs index 246fbaf..0d5199d 100644 --- a/leetcode-tui-rs/src/executor.rs +++ b/leetcode-tui-rs/src/executor.rs @@ -89,6 +89,7 @@ impl Executor { Key::Char('s') => cx.content.get_questions_mut().submit_solution(), Key::Ctrl('s') => cx.content.get_questions_mut().toggle_stats(), Key::Char('/') => cx.content.get_questions_mut().toggle_search(), + Key::Tab => cx.content.get_questions_mut().toggle_difficulty(), Key::Char('q') => { emit!(Quit); false