diff --git a/Cargo.lock b/Cargo.lock index e5f8d6e..11df0f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -249,10 +249,12 @@ dependencies = [ "indicatif", "once_cell", "regex", + "serde", "serde_json", "similar", "thirtyfour", "tokio", + "toml", ] [[package]] @@ -816,18 +818,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.199" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c9f6e76df036c77cd94996771fb40db98187f096dd0b9af39c6c6e452ba966a" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.199" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11bd257a6541e141e42ca6d24ae26f7714887b47e89aa739099104c7e4d3b7fc" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", @@ -857,6 +859,15 @@ dependencies = [ "syn 2.0.60", ] +[[package]] +name = "serde_spanned" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1099,6 +1110,40 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tower" version = "0.4.13" @@ -1454,6 +1499,15 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +[[package]] +name = "winnow" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86c949fede1d13936a99f14fafd3e76fd642b556dd2ce96287fbe2e0151bfac6" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index 095e8cd..f289503 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,9 @@ dialoguer = { version = "0.11.0", features = ["history"], default-features = fal indicatif = "0.17.8" once_cell = "1.19.0" regex = "1.10.4" +serde = "1.0.203" serde_json = "1.0.117" similar = "2.5.0" thirtyfour = "0.32.0" tokio = { version = "1.37.0", features = ["time", "process", "rt"] } +toml = "0.8.13" diff --git a/src/command.rs b/src/command.rs index d3a368d..2b9bafb 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,6 +1,8 @@ mod parser; mod executor; +use crate::data::Credentials; + #[derive(Debug, Clone)] pub(crate) struct InputCommand { raw_command: String, @@ -35,6 +37,7 @@ impl std::str::FromStr for InputCommand { #[derive(Debug, Clone)] pub(crate) enum Command { Set(Setting), + Preset { name: String }, Prob { prob: String }, Build { build: Option }, Run { @@ -55,12 +58,10 @@ pub(crate) enum Command { #[derive(Debug, Clone)] pub(crate) enum Setting { - Credentials { - bojautologin: String, - onlinejudge: String, - }, + Credentials(Credentials), Lang(String), File(String), + Init(String), Build(String), Cmd(String), Input(String), diff --git a/src/command/executor.rs b/src/command/executor.rs index 48c3d5c..83b40e3 100644 --- a/src/command/executor.rs +++ b/src/command/executor.rs @@ -1,6 +1,6 @@ -use super::{Command, Setting, CommandExecuteError}; +use super::{Command, Setting, CommandExecuteError, Credentials}; use crate::global_state::GlobalState; -use crate::data::{ProblemId, ExampleIO}; +use crate::data::{ProblemId, ExampleIO, Preset}; use crate::infra::subprocess::{run_silent, run_with_input_timed, run_interactive, Output}; use regex::{Regex, Captures, Replacer}; use once_cell::sync::Lazy; @@ -43,6 +43,13 @@ impl GlobalState { pub(crate) fn execute(&mut self, command: &Command) -> anyhow::Result<()> { match command { Command::Set(setting) => self.set(setting)?, + Command::Preset { name } => { + let Some(preset) = self.presets.get(name) else { + error!("preset: Unknown preset name")? + }; + let preset = preset.clone(); + self.preset(preset)?; + } Command::Prob { prob } => self.prob(prob)?, Command::Build { build } => { let Some(prob) = self.problem.as_ref().map(|p| &p.id) else { @@ -130,7 +137,7 @@ impl GlobalState { fn set(&mut self, setting: &Setting) -> anyhow::Result<()> { match setting { - Setting::Credentials { bojautologin, onlinejudge } => { + Setting::Credentials(Credentials { bojautologin, onlinejudge }) => { self.credentials.bojautologin.clear(); self.credentials.bojautologin += bojautologin; self.credentials.onlinejudge.clear(); @@ -150,6 +157,11 @@ impl GlobalState { self.file.clear(); self.file += file; } + Setting::Init(init) => { + self.init.clear(); + self.init += init; + self.init()?; + } Setting::Build(build) => { self.build.clear(); self.build += build; @@ -166,14 +178,63 @@ impl GlobalState { Ok(()) } + fn preset(&mut self, preset: Preset) -> anyhow::Result<()> { + let Preset { credentials, lang, file, init, build, cmd, input, .. } = preset; + if let Some(credentials) = credentials { + self.set(&Setting::Credentials(credentials))?; + } + if let Some(lang) = lang { + self.set(&Setting::Lang(lang))?; + } + if let Some(file) = file { + self.set(&Setting::File(file))?; + } + if let Some(init) = init { + self.set(&Setting::Init(init))?; + } + if let Some(build) = build { + self.set(&Setting::Build(build))?; + } + if let Some(cmd) = cmd { + self.set(&Setting::Cmd(cmd))?; + } + if let Some(input) = input { + self.set(&Setting::Input(input))?; + } + Ok(()) + } + fn prob(&mut self, prob: &str) -> anyhow::Result<()> { - // TODO: try fetching from local datastore first let problem_id = prob.parse::()?; - self.problem = Some(self.browser.get_problem(&problem_id)?); + if let Some(problem) = self.problem_cache.get(&problem_id) { + // try copying from the cache first + self.problem = Some(problem.clone()); + } else { + // store the fetched problem to the cache + self.problem = Some(self.browser.get_problem(&problem_id)?); + self.problem_cache.insert(problem_id, self.problem.clone().unwrap()); + } let problem = self.problem.as_ref().unwrap(); println!("Problem {} {}", problem.id, problem.title); println!("Time limit: {:.3}s{} / Memory limit: {}MB{}", problem.time, if !problem.time_bonus { " (No bonus)" } else { "" }, problem.memory, if !problem.memory_bonus { " (No bonus)" } else { "" }); - // TODO: store the fetched problem to the local datastore + self.init()?; + Ok(()) + } + + fn init(&self) -> anyhow::Result<()> { + // if init is empty, do nothing + if self.init.is_empty() { + return Ok(()); + } + // if prob is not set, do not try to run init + let Some(prob) = self.problem.as_ref() else { + return Ok(()); + }; + let init_cmd = substitute_problem(&self.init, &prob.id); + let res = run_silent(&init_cmd)?; + if let Some(err) = res { + error!("Init returned nonzero exit code. STDERR:\n{}", err)? + } Ok(()) } @@ -289,12 +350,14 @@ set credentials Set BOJ login cookies and log in with them. set lang set file +set init set build set cmd set input Set default value for the given variable. prob Load the problem and set it as the current problem. + If is set, run it. build [build] Build your solution. run [i=input] [c=cmd] @@ -303,6 +366,8 @@ test [c=cmd] Test your solution against sample test cases. submit [l=lang] [f=file] Submit your solution to BOJ. +preset + Apply one of the presets defined in boj.toml. help Display this help. exit diff --git a/src/command/parser.rs b/src/command/parser.rs index b22b1eb..ab8e9ca 100644 --- a/src/command/parser.rs +++ b/src/command/parser.rs @@ -1,4 +1,4 @@ -use super::{Command, Setting, CommandParseError}; +use super::{Command, Setting, CommandParseError, Credentials}; use std::collections::HashMap; macro_rules! error { @@ -175,12 +175,12 @@ impl std::str::FromStr for Command { } else if args.len() > 3 { return error!("set credentials: Too many arguments"); } - Setting::Credentials { + Setting::Credentials(Credentials { bojautologin: args[1].clone(), onlinejudge: args[2].clone(), - } + }) } - "lang" | "file" | "build" | "cmd" | "input" => { + "lang" | "file" | "build" | "cmd" | "input" | "init" => { if args.len() == 1 { return error!("set {}: Missing argument <{}>", variable, variable); } else if args.len() > 2 { @@ -190,6 +190,7 @@ impl std::str::FromStr for Command { match variable { "lang" => Setting::Lang(arg), "file" => Setting::File(arg), + "init" => Setting::Init(arg), "build" => Setting::Build(arg), "cmd" => Setting::Cmd(arg), "input" => Setting::Input(arg), @@ -205,6 +206,19 @@ impl std::str::FromStr for Command { } Ok(Command::Set(setting)) } + "preset" => { + if args.len() == 0 { + error!("preset: Missing argument ") + } else if args.len() > 1 { + error!("preset: Too many positional arguments") + } else if kwargs.len() > 0 { + error!("preset: Unexpected keyword argument(s)") + } else { + Ok(Self::Preset { + name: args[0].clone() + }) + } + } "prob" => { if args.len() == 0 { error!("prob: Missing argument ") diff --git a/src/data.rs b/src/data.rs index d0a624b..7075731 100644 --- a/src/data.rs +++ b/src/data.rs @@ -1,4 +1,4 @@ -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] pub(crate) enum ProblemId { Problem(String), ContestProblem(String) @@ -58,7 +58,7 @@ impl ProblemId { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub(crate) struct ExampleIO { pub(crate) input: String, pub(crate) output: String, @@ -146,7 +146,7 @@ impl ProblemKind { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub(crate) struct Problem { pub(crate) id: ProblemId, pub(crate) title: String, @@ -158,7 +158,26 @@ pub(crate) struct Problem { pub(crate) io: Vec, } +#[derive(Debug, Clone, serde::Deserialize)] pub(crate) struct Credentials { pub(crate) bojautologin: String, pub(crate) onlinejudge: String, +} + +#[derive(Clone, serde::Deserialize)] +pub(crate) struct Preset { + pub(crate) name: String, + pub(crate) credentials: Option, + pub(crate) lang: Option, + pub(crate) file: Option, + pub(crate) init: Option, + pub(crate) build: Option, + pub(crate) cmd: Option, + pub(crate) input: Option, +} + +#[derive(serde::Deserialize)] +pub(crate) struct BojConfig { + pub(crate) start: Option, + pub(crate) preset: Vec, } \ No newline at end of file diff --git a/src/global_state.rs b/src/global_state.rs index b3d8b2e..cd8edd4 100644 --- a/src/global_state.rs +++ b/src/global_state.rs @@ -1,35 +1,82 @@ -use crate::data::{Credentials, Problem}; +use crate::data::{Credentials, Problem, ProblemId, BojConfig, Preset}; use crate::infra::browser::Browser; +use std::collections::HashMap; pub(crate) struct GlobalState { pub(crate) credentials: Credentials, pub(crate) problem: Option, + pub(crate) init: String, pub(crate) build: String, pub(crate) cmd: String, pub(crate) input: String, pub(crate) lang: String, pub(crate) file: String, pub(crate) browser: Browser, + pub(crate) problem_cache: HashMap, + pub(crate) presets: HashMap, } impl GlobalState { pub(crate) fn new() -> anyhow::Result { - Ok(Self { + let mut state = Self { credentials: Credentials { bojautologin: String::new(), onlinejudge: String::new(), }, problem: None, + init: String::new(), build: "cargo build --release".to_string(), cmd: "cargo run --release".to_string(), input: "input.txt".to_string(), lang: "Rust 2021".to_string(), file: "src/main.rs".to_string(), - browser: Browser::new()? - }) + browser: Browser::new()?, + problem_cache: HashMap::new(), + presets: HashMap::new(), + }; + // println!("state initialized"); + match BojConfig::from_config() { + Ok(config) => { + for preset in &config.preset { + state.presets.insert(preset.name.clone(), preset.clone()); + } + if let Some(start) = config.start.as_ref() { + for (lineno, line) in start.lines().enumerate() { + if line.is_empty() { continue; } + match line.parse::() { + Ok(cmd) => { + if let Err(err) = state.execute(&cmd) { + println!("boj.toml start script execution error at line {}: {}", lineno + 1, err); + break; + } + } + Err(err) => { + println!("boj.toml start script parse error at line {}: {}", lineno + 1, err); + break; + } + } + } + } + } + Err(_error) => {} + } + Ok(state) } pub(crate) fn quit(self) -> anyhow::Result<()> { self.browser.quit() } +} + +impl BojConfig { + fn from_config() -> anyhow::Result { + let mut boj_toml = std::env::current_dir()?; + boj_toml.push("boj.toml"); + let boj_toml_content = std::fs::read_to_string(boj_toml)?; + let config = toml::from_str(&boj_toml_content); + if let Err(error) = &config { + println!("boj.toml parse error:\n{}", error); + } + Ok(config?) + } } \ No newline at end of file diff --git a/src/infra/browser.rs b/src/infra/browser.rs index e77dc6d..4b03c8c 100644 --- a/src/infra/browser.rs +++ b/src/infra/browser.rs @@ -22,10 +22,13 @@ impl Browser { pub(crate) fn new() -> anyhow::Result { with_async_runtime(async { let geckodriver = Command::new("geckodriver").stdout(Stdio::null()).stderr(Stdio::null()).spawn()?; + tokio::time::sleep(std::time::Duration::from_millis(500)).await; // Use headless firefox to allow running without a graphic device let mut caps = DesiredCapabilities::firefox(); caps.set_headless()?; + // println!("webdriver initializing"); let webdriver = WebDriver::new("http://localhost:4444", caps).await?; + // println!("webdriver initialized"); // Handle AWS WAF challenge webdriver.get("https://www.acmicpc.net").await?; diff --git a/src/main.rs b/src/main.rs index 19bf138..f74b1e6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -53,25 +53,25 @@ fn main() -> anyhow::Result<()> { // Read and execute .bojrc before entering the loop // TODO: Attach indicatif progress bar - let mut bojrc = std::env::current_dir()?; - bojrc.push(".bojrc"); - if let Ok(bojrc_content) = std::fs::read_to_string(bojrc) { - for (lineno, line) in bojrc_content.lines().enumerate() { - if line.is_empty() { continue; } - match line.parse::() { - Ok(cmd) => { - if let Err(err) = state.execute(&cmd) { - println!(".bojrc execution error at line {}: {}", lineno + 1, err); - break; - } - } - Err(err) => { - println!(".bojrc parse error at line {}: {}", lineno + 1, err); - break; - } - } - } - } + // let mut bojrc = std::env::current_dir()?; + // bojrc.push(".bojrc"); + // if let Ok(bojrc_content) = std::fs::read_to_string(bojrc) { + // for (lineno, line) in bojrc_content.lines().enumerate() { + // if line.is_empty() { continue; } + // match line.parse::() { + // Ok(cmd) => { + // if let Err(err) = state.execute(&cmd) { + // println!(".bojrc execution error at line {}: {}", lineno + 1, err); + // break; + // } + // } + // Err(err) => { + // println!(".bojrc parse error at line {}: {}", lineno + 1, err); + // break; + // } + // } + // } + // } loop { if let Ok(cmd) = Input::::with_theme(&ColorfulTheme::default())