practicode 0.1.9 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Cargo.lock CHANGED
@@ -453,7 +453,7 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
453
453
 
454
454
  [[package]]
455
455
  name = "practicode"
456
- version = "0.1.9"
456
+ version = "0.1.10"
457
457
  dependencies = [
458
458
  "anyhow",
459
459
  "crossterm",
package/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "practicode"
3
- version = "0.1.9"
3
+ version = "0.1.10"
4
4
  edition = "2024"
5
5
  description = "Local-first coding-test practice in a Rust terminal UI with optional AI help."
6
6
  readme = "README.md"
@@ -4,9 +4,25 @@ Practicode is local-first: user data stays under `.practicode/`, `problems/`, an
4
4
 
5
5
  ## Source Layout
6
6
 
7
- - `src/core.rs` owns problem data, state loading/saving, judging, and file generation.
8
- - `src/core/profile.rs` owns user-profile defaults and normalization.
9
- - `src/tui.rs` owns the Ratatui app shell, event routing, and workflow orchestration.
7
+ - `src/core.rs` is the public core facade. Keep new domain logic in nested `src/core/` modules.
8
+ - `src/core/model.rs` owns persisted/user-facing data shapes and core constants.
9
+ - `src/core/bank.rs` owns local problem-bank loading, saving, starter data, and bank validation.
10
+ - `src/core/state.rs` owns state loading, saving, and settings normalization.
11
+ - `src/core/language.rs` owns language/provider normalization, templates, and extension mapping.
12
+ - `src/core/render.rs` owns plain/markdown problem rendering.
13
+ - `src/core/judge.rs` owns submission file creation, runtime commands, compilation, and judging.
14
+ - `src/core/progress.rs` owns give-up/next/previous/pass history transitions.
15
+ - `src/core/problem_files.rs` owns generated problem README/index file writes.
16
+ - `src/core/profile.rs` owns user-profile defaults and normalization helpers.
17
+ - `src/tui.rs` owns the `PracticodeApp` state shell, construction, run loop, and test accessors. Keep new TUI behavior in nested `src/tui/` modules.
18
+ - `src/tui/actions.rs` owns user actions such as run, next, generate, language/theme/profile changes.
19
+ - `src/tui/command_handlers.rs` owns slash-command routing.
20
+ - `src/tui/command_input.rs` owns command palette input, completion, and Hangul composition.
21
+ - `src/tui/events.rs` owns keyboard/mouse event routing.
22
+ - `src/tui/tasks.rs` owns background AI/update/model tasks and output writing helpers.
23
+ - `src/tui/view.rs` owns Ratatui drawing, pane styling, mouse-capture toggles, and cursor placement.
24
+ - `src/tui/problem_list.rs` owns problem-list rendering and navigation.
25
+ - `src/tui/status.rs` owns status-line text, busy-game text, mode hints, and help text.
10
26
  - `src/tui/commands.rs` owns the command palette catalog.
11
27
  - `src/tui/editor.rs` owns the in-terminal code editor state.
12
28
  - `src/tui/problem_view.rs` owns problem-statement rendering.
@@ -17,7 +33,7 @@ Practicode is local-first: user data stays under `.practicode/`, `problems/`, an
17
33
 
18
34
  ## Extension Rules
19
35
 
20
- - Add domain logic under the owning module first; keep `tui.rs` as orchestration, not a catch-all.
36
+ - Add domain logic under the owning nested module first; keep `core.rs` and `tui.rs` as facades/shells, not catch-alls.
21
37
  - Add user-visible commands in `src/tui/commands.rs`, then route behavior in `PracticodeApp::handle_command`.
22
38
  - Add persisted user profile settings to `Settings`, normalize them in `normalize_settings`, and cover old-state compatibility with tests.
23
39
  - Keep provider-specific behavior in `src/ai.rs`; TUI should ask for status or start tasks, not know provider internals.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "practicode",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "Local-first coding-test practice in a Rust terminal UI with optional AI help.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -0,0 +1,148 @@
1
+ use super::*;
2
+
3
+ pub fn starter_problem() -> Problem {
4
+ Problem {
5
+ id: "001-hello-world".to_string(),
6
+ slug: "hello-world".to_string(),
7
+ difficulty: "easy".to_string(),
8
+ topics: vec!["io".to_string()],
9
+ title: localized_map(&[
10
+ ("en", "Hello World"),
11
+ ("ko", "Hello World"),
12
+ ("ja", "Hello World"),
13
+ ("zh", "Hello World"),
14
+ ("es", "Hello World"),
15
+ ]),
16
+ statement: localized_map(&[
17
+ ("en", "Print exactly `Hello, World!` to stdout."),
18
+ ("ko", "표준 출력으로 정확히 `Hello, World!`를 출력하세요."),
19
+ ("ja", "標準出力に正確に `Hello, World!` を出力してください。"),
20
+ ("zh", "向标准输出准确打印 `Hello, World!`。"),
21
+ ("es", "Imprime exactamente `Hello, World!` en stdout."),
22
+ ]),
23
+ input: localized_map(&[
24
+ ("en", "No input."),
25
+ ("ko", "입력은 없습니다."),
26
+ ("ja", "入力はありません。"),
27
+ ("zh", "没有输入。"),
28
+ ("es", "No hay entrada."),
29
+ ]),
30
+ output: localized_map(&[
31
+ ("en", "One line: `Hello, World!`"),
32
+ ("ko", "`Hello, World!` 한 줄"),
33
+ ("ja", "1行: `Hello, World!`"),
34
+ ("zh", "一行: `Hello, World!`"),
35
+ ("es", "Una linea: `Hello, World!`"),
36
+ ]),
37
+ examples: vec![IoCase {
38
+ input: String::new(),
39
+ output: "Hello, World!\n".to_string(),
40
+ }],
41
+ cases: vec![IoCase {
42
+ input: String::new(),
43
+ output: "Hello, World!\n".to_string(),
44
+ }],
45
+ answers: HashMap::from([
46
+ ("python".to_string(), "print('Hello, World!')\n".to_string()),
47
+ (
48
+ "ts".to_string(),
49
+ "console.log('Hello, World!');\n".to_string(),
50
+ ),
51
+ (
52
+ "java".to_string(),
53
+ "class Solution {\n public static void main(String[] args) {\n System.out.println(\"Hello, World!\");\n }\n}\n".to_string(),
54
+ ),
55
+ (
56
+ "rust".to_string(),
57
+ "fn main() {\n println!(\"Hello, World!\");\n}\n".to_string(),
58
+ ),
59
+ ]),
60
+ }
61
+ }
62
+
63
+ pub fn map2(k1: &str, v1: &str, k2: &str, v2: &str) -> HashMap<String, String> {
64
+ HashMap::from([
65
+ (k1.to_string(), v1.to_string()),
66
+ (k2.to_string(), v2.to_string()),
67
+ ])
68
+ }
69
+
70
+ pub fn localized_map(entries: &[(&str, &str)]) -> HashMap<String, String> {
71
+ entries
72
+ .iter()
73
+ .map(|(key, value)| ((*key).to_string(), (*value).to_string()))
74
+ .collect()
75
+ }
76
+
77
+ pub fn load_bank(root: &Path) -> Result<Vec<Problem>> {
78
+ let path = root.join(BANK_PATH);
79
+ if path.exists() {
80
+ let text = fs::read_to_string(&path).with_context(|| format!("read {}", path.display()))?;
81
+ let bank: Vec<Problem> =
82
+ serde_json::from_str(&text).with_context(|| format!("parse {}", path.display()))?;
83
+ validate_bank(&bank, &path)?;
84
+ Ok(bank)
85
+ } else {
86
+ Ok(vec![starter_problem()])
87
+ }
88
+ }
89
+
90
+ pub fn save_bank(root: &Path, bank: &[Problem]) -> Result<()> {
91
+ let path = root.join(BANK_PATH);
92
+ validate_bank(bank, &path)?;
93
+ if let Some(parent) = path.parent() {
94
+ fs::create_dir_all(parent)?;
95
+ }
96
+ fs::write(&path, serde_json::to_string_pretty(bank)? + "\n")?;
97
+ Ok(())
98
+ }
99
+
100
+ fn validate_bank(bank: &[Problem], path: &Path) -> Result<()> {
101
+ if bank.is_empty() {
102
+ bail!("{} must contain at least one problem", path.display());
103
+ }
104
+ for problem in bank {
105
+ if !is_safe_name(&problem.id) {
106
+ bail!("{} has invalid problem id {:?}", path.display(), problem.id);
107
+ }
108
+ if !is_safe_name(&problem.slug) {
109
+ bail!(
110
+ "{} has invalid slug {:?} for {}",
111
+ path.display(),
112
+ problem.slug,
113
+ problem.id
114
+ );
115
+ }
116
+ if problem.cases.is_empty() {
117
+ bail!(
118
+ "{} problem {} has no judge cases",
119
+ path.display(),
120
+ problem.id
121
+ );
122
+ }
123
+ if problem.answers.is_empty() {
124
+ bail!(
125
+ "{} problem {} must contain at least one answer",
126
+ path.display(),
127
+ problem.id
128
+ );
129
+ }
130
+ for language in problem.answers.keys() {
131
+ if !LANGUAGES.contains(&language.as_str()) {
132
+ bail!(
133
+ "{} problem {} has unsupported answer language {language}",
134
+ path.display(),
135
+ problem.id,
136
+ );
137
+ }
138
+ }
139
+ }
140
+ Ok(())
141
+ }
142
+
143
+ fn is_safe_name(value: &str) -> bool {
144
+ !value.is_empty()
145
+ && value
146
+ .chars()
147
+ .all(|char| char.is_ascii_alphanumeric() || matches!(char, '-' | '_'))
148
+ }
@@ -0,0 +1,205 @@
1
+ use super::*;
2
+
3
+ pub fn ensure_submission(root: &Path, problem: &Problem, settings: &Settings) -> Result<PathBuf> {
4
+ let language = normalize_language(&settings.language);
5
+ let path = root
6
+ .join("submissions")
7
+ .join(&problem.id)
8
+ .join(format!("solution.{}", ext_for(&language)));
9
+ if !path.exists() {
10
+ if let Some(parent) = path.parent() {
11
+ fs::create_dir_all(parent)?;
12
+ }
13
+ fs::write(&path, template_for(&language))?;
14
+ }
15
+ Ok(path)
16
+ }
17
+ pub fn judge(root: &Path, problem: &Problem, settings: &Settings) -> JudgeResult {
18
+ if problem.cases.is_empty() {
19
+ return JudgeResult {
20
+ passed: false,
21
+ passed_cases: 0,
22
+ total_cases: 0,
23
+ output: "problem has no judge cases".to_string(),
24
+ };
25
+ }
26
+ let path = match ensure_submission(root, problem, settings) {
27
+ Ok(path) => path,
28
+ Err(error) => {
29
+ return JudgeResult {
30
+ passed: false,
31
+ passed_cases: 0,
32
+ total_cases: problem.cases.len(),
33
+ output: error.to_string(),
34
+ };
35
+ }
36
+ };
37
+ let language = normalize_language(&settings.language);
38
+ let command = match command_for(root, &path, &language) {
39
+ Ok(Some(command)) => command,
40
+ Ok(None) => {
41
+ return JudgeResult {
42
+ passed: false,
43
+ passed_cases: 0,
44
+ total_cases: problem.cases.len(),
45
+ output: format!("Missing runtime for {}", settings.language),
46
+ };
47
+ }
48
+ Err(error) => {
49
+ return JudgeResult {
50
+ passed: false,
51
+ passed_cases: 0,
52
+ total_cases: problem.cases.len(),
53
+ output: format!("compile failed\n{error}"),
54
+ };
55
+ }
56
+ };
57
+ let run_dir = root.join(".practicode/build").join(&problem.id).join("run");
58
+ if let Err(error) = fs::create_dir_all(&run_dir) {
59
+ return JudgeResult {
60
+ passed: false,
61
+ passed_cases: 0,
62
+ total_cases: problem.cases.len(),
63
+ output: error.to_string(),
64
+ };
65
+ }
66
+
67
+ let mut passed = 0;
68
+ let mut lines = Vec::new();
69
+ for (index, case) in problem.cases.iter().enumerate() {
70
+ let mut process = Command::new(&command.program);
71
+ process.args(&command.args).current_dir(&run_dir);
72
+ let run = match run_capture(&mut process, &case.input, Duration::from_secs(5)) {
73
+ Ok(run) => run,
74
+ Err(error) => {
75
+ lines.push(format!("Case {}: FAIL", index + 1));
76
+ push_labeled_block(&mut lines, "Error", &error.to_string());
77
+ break;
78
+ }
79
+ };
80
+ let got = run.stdout.trim();
81
+ let expected = case.output.trim();
82
+ if !run.timed_out && run.code == Some(0) && got == expected {
83
+ passed += 1;
84
+ lines.push(format!("Case {}: PASS", index + 1));
85
+ if !run.stderr.trim().is_empty() {
86
+ push_labeled_block(&mut lines, "Stderr", run.stderr.trim_end());
87
+ }
88
+ } else {
89
+ lines.push(format!("Case {}: FAIL", index + 1));
90
+ if run.timed_out {
91
+ push_labeled_block(&mut lines, "Error", "timeout: 5s");
92
+ }
93
+ push_labeled_block(&mut lines, "Input", case.input.trim_end());
94
+ push_labeled_block(&mut lines, "Expected", expected);
95
+ push_labeled_block(&mut lines, "Got", run.stdout.trim_end());
96
+ if !run.stderr.trim().is_empty() {
97
+ push_labeled_block(&mut lines, "Stderr", run.stderr.trim_end());
98
+ }
99
+ break;
100
+ }
101
+ }
102
+
103
+ JudgeResult {
104
+ passed: passed == problem.cases.len(),
105
+ passed_cases: passed,
106
+ total_cases: problem.cases.len(),
107
+ output: lines.join("\n"),
108
+ }
109
+ }
110
+
111
+ fn push_labeled_block(lines: &mut Vec<String>, label: &str, body: &str) {
112
+ lines.push(String::new());
113
+ lines.push(label.to_string());
114
+ if body.is_empty() {
115
+ lines.push(" <empty>".to_string());
116
+ } else {
117
+ lines.extend(body.lines().map(|line| format!(" {line}")));
118
+ }
119
+ }
120
+
121
+ pub fn command_for(root: &Path, path: &Path, language: &str) -> Result<Option<CommandSpec>> {
122
+ match language {
123
+ "python" => Ok(which("python3")
124
+ .or_else(|| which("python"))
125
+ .map(|program| CommandSpec {
126
+ program,
127
+ args: vec![path.display().to_string()],
128
+ })),
129
+ "ts" => Ok(which("node").map(|program| CommandSpec {
130
+ program,
131
+ args: vec![
132
+ "--experimental-strip-types".to_string(),
133
+ path.display().to_string(),
134
+ ],
135
+ })),
136
+ "java" => compile_java(root, path),
137
+ "rust" => compile_rust(root, path),
138
+ _ => Ok(None),
139
+ }
140
+ }
141
+
142
+ fn compile_java(root: &Path, path: &Path) -> Result<Option<CommandSpec>> {
143
+ let Some(javac) = which("javac") else {
144
+ return Ok(None);
145
+ };
146
+ let Some(java) = which("java") else {
147
+ return Ok(None);
148
+ };
149
+ let build = root
150
+ .join(".practicode/build")
151
+ .join(path.parent().and_then(Path::file_name).unwrap_or_default())
152
+ .join("java");
153
+ fs::create_dir_all(&build)?;
154
+ let mut compile = Command::new(javac);
155
+ compile
156
+ .args([
157
+ "-d",
158
+ &build.display().to_string(),
159
+ &path.display().to_string(),
160
+ ])
161
+ .current_dir(root);
162
+ let output = run_capture(&mut compile, "", Duration::from_secs(30))?;
163
+ if output.code != Some(0) {
164
+ return Err(anyhow!(output.stderr.trim().to_string()));
165
+ }
166
+ Ok(Some(CommandSpec {
167
+ program: java,
168
+ args: vec![
169
+ "-cp".to_string(),
170
+ build.display().to_string(),
171
+ "Solution".to_string(),
172
+ ],
173
+ }))
174
+ }
175
+
176
+ fn compile_rust(root: &Path, path: &Path) -> Result<Option<CommandSpec>> {
177
+ let Some(rustc) = which("rustc") else {
178
+ return Ok(None);
179
+ };
180
+ let build = root
181
+ .join(".practicode/build")
182
+ .join(path.parent().and_then(Path::file_name).unwrap_or_default());
183
+ fs::create_dir_all(&build)?;
184
+ let exe = build.join(if cfg!(windows) {
185
+ "solution.exe"
186
+ } else {
187
+ "solution"
188
+ });
189
+ let mut compile = Command::new(rustc);
190
+ compile
191
+ .args([
192
+ path.display().to_string(),
193
+ "-o".to_string(),
194
+ exe.display().to_string(),
195
+ ])
196
+ .current_dir(root);
197
+ let output = run_capture(&mut compile, "", Duration::from_secs(30))?;
198
+ if output.code != Some(0) {
199
+ return Err(anyhow!(output.stderr.trim().to_string()));
200
+ }
201
+ Ok(Some(CommandSpec {
202
+ program: exe,
203
+ args: Vec::new(),
204
+ }))
205
+ }
@@ -0,0 +1,92 @@
1
+ use super::*;
2
+
3
+ pub fn ext_for(language: &str) -> &'static str {
4
+ match normalize_language(language).as_str() {
5
+ "python" => "py",
6
+ "ts" => "ts",
7
+ "java" => "java",
8
+ "rust" => "rs",
9
+ _ => "py",
10
+ }
11
+ }
12
+ pub fn problem_by_id<'a>(bank: &'a [Problem], problem_id: &str) -> Option<&'a Problem> {
13
+ bank.iter().find(|problem| problem.id == problem_id)
14
+ }
15
+
16
+ pub fn normalize_language(language: &str) -> String {
17
+ let language = language.trim().to_lowercase();
18
+ if LANGUAGES.contains(&language.as_str()) {
19
+ language
20
+ } else {
21
+ "python".to_string()
22
+ }
23
+ }
24
+
25
+ pub fn parse_language_list(value: &str) -> Vec<String> {
26
+ let mut languages = Vec::new();
27
+ for language in value.split(',') {
28
+ let language = language.trim().to_lowercase();
29
+ if language == "all" {
30
+ return Vec::new();
31
+ }
32
+ if LANGUAGES.contains(&language.as_str()) && !languages.contains(&language) {
33
+ languages.push(language);
34
+ }
35
+ }
36
+ languages
37
+ }
38
+
39
+ pub fn normalize_language_list(languages: &[String]) -> Vec<String> {
40
+ parse_language_list(&languages.join(","))
41
+ }
42
+
43
+ pub fn parse_ui_language_list(value: &str) -> Vec<String> {
44
+ let mut languages = Vec::new();
45
+ for language in value.split(',') {
46
+ let lower = language.trim().to_lowercase();
47
+ if lower == "all" {
48
+ return Vec::new();
49
+ }
50
+ let language = lower
51
+ .split(['-', '_'])
52
+ .next()
53
+ .filter(|value| UI_LANGUAGES.contains(value))
54
+ .unwrap_or("");
55
+ if language == "all" {
56
+ return Vec::new();
57
+ }
58
+ if !language.is_empty() && !languages.iter().any(|value| value == language) {
59
+ languages.push(language.to_string());
60
+ }
61
+ }
62
+ languages
63
+ }
64
+
65
+ pub fn normalize_ui_language_list(languages: &[String]) -> Vec<String> {
66
+ parse_ui_language_list(&languages.join(","))
67
+ }
68
+
69
+ pub fn normalize_next_source(source: &str) -> String {
70
+ if source == "ai" {
71
+ "ai".to_string()
72
+ } else {
73
+ "bank".to_string()
74
+ }
75
+ }
76
+
77
+ pub fn normalize_ai_provider(provider: &str) -> String {
78
+ if provider == "claude" {
79
+ "claude".to_string()
80
+ } else {
81
+ "codex".to_string()
82
+ }
83
+ }
84
+ pub fn template_for(language: &str) -> String {
85
+ match normalize_language(language).as_str() {
86
+ "python" => "# Read from stdin and print to stdout.\nimport sys\n\n\n".to_string(),
87
+ "ts" => "const fs = require('fs');\nconst input = fs.readFileSync(0, 'utf8');\n\n".to_string(),
88
+ "java" => "import java.io.*;\n\nclass Solution {\n public static void main(String[] args) throws Exception {\n }\n}\n".to_string(),
89
+ "rust" => "fn main() {\n}\n".to_string(),
90
+ _ => String::new(),
91
+ }
92
+ }
@@ -0,0 +1,153 @@
1
+ use super::*;
2
+
3
+ pub const LANGUAGES: &[&str] = &["python", "ts", "java", "rust"];
4
+ pub const THEMES: &[&str] = &["dark", "light"];
5
+ pub const AI_PROVIDERS: &[&str] = &["codex", "claude"];
6
+ pub const BANK_PATH: &str = ".practicode/problem_bank.json";
7
+ pub const STATE_PATH: &str = ".practicode/problem-state.json";
8
+ pub const PROBLEM_NOTES_PATH: &str = ".practicode/problem_notes.md";
9
+
10
+ #[derive(Clone, Debug, Serialize, Deserialize)]
11
+ pub struct Settings {
12
+ #[serde(default = "default_language")]
13
+ pub language: String,
14
+ #[serde(default = "default_ui_language")]
15
+ pub ui_language: String,
16
+ #[serde(default = "default_theme")]
17
+ pub theme: String,
18
+ #[serde(default = "default_difficulty")]
19
+ pub difficulty: String,
20
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
21
+ pub topics: Vec<String>,
22
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
23
+ pub avoid_topics: Vec<String>,
24
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
25
+ pub generate_languages: Vec<String>,
26
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
27
+ pub generate_ui_languages: Vec<String>,
28
+ #[serde(default = "default_editor")]
29
+ pub editor: String,
30
+ #[serde(default = "default_next_source")]
31
+ pub next_source: String,
32
+ #[serde(default = "default_ai_provider")]
33
+ pub ai_provider: String,
34
+ #[serde(default = "default_ai_model")]
35
+ pub ai_model: String,
36
+ #[serde(default, skip_serializing_if = "String::is_empty")]
37
+ pub ai_next_command: String,
38
+ }
39
+
40
+ impl Default for Settings {
41
+ fn default() -> Self {
42
+ Self {
43
+ language: default_language(),
44
+ ui_language: default_ui_language(),
45
+ theme: default_theme(),
46
+ difficulty: default_difficulty(),
47
+ topics: Vec::new(),
48
+ avoid_topics: Vec::new(),
49
+ generate_languages: Vec::new(),
50
+ generate_ui_languages: Vec::new(),
51
+ editor: default_editor(),
52
+ next_source: default_next_source(),
53
+ ai_provider: default_ai_provider(),
54
+ ai_model: default_ai_model(),
55
+ ai_next_command: String::new(),
56
+ }
57
+ }
58
+ }
59
+
60
+ impl Settings {
61
+ pub fn next_ai_command(&self) -> &str {
62
+ &self.ai_next_command
63
+ }
64
+
65
+ pub fn model_arg(&self) -> Option<&str> {
66
+ let model = self.ai_model.trim();
67
+ if model.is_empty() || model == "auto" {
68
+ None
69
+ } else {
70
+ Some(model)
71
+ }
72
+ }
73
+ }
74
+
75
+ #[derive(Clone, Debug, Serialize, Deserialize)]
76
+ pub struct HistoryItem {
77
+ pub id: String,
78
+ pub status: String,
79
+ }
80
+
81
+ #[derive(Clone, Debug, Serialize, Deserialize)]
82
+ pub struct AppState {
83
+ pub current_problem: String,
84
+ #[serde(default)]
85
+ pub settings: Settings,
86
+ #[serde(default)]
87
+ pub solved: Vec<String>,
88
+ #[serde(default)]
89
+ pub history: Vec<HistoryItem>,
90
+ #[serde(default = "default_suggested_difficulty")]
91
+ pub suggested_next_difficulty: String,
92
+ }
93
+
94
+ #[derive(Clone, Debug, Serialize, Deserialize)]
95
+ pub struct Problem {
96
+ pub id: String,
97
+ pub slug: String,
98
+ pub difficulty: String,
99
+ pub topics: Vec<String>,
100
+ pub title: HashMap<String, String>,
101
+ pub statement: HashMap<String, String>,
102
+ pub input: HashMap<String, String>,
103
+ pub output: HashMap<String, String>,
104
+ pub examples: Vec<IoCase>,
105
+ pub cases: Vec<IoCase>,
106
+ pub answers: HashMap<String, String>,
107
+ }
108
+
109
+ #[derive(Clone, Debug, Serialize, Deserialize)]
110
+ pub struct IoCase {
111
+ pub input: String,
112
+ pub output: String,
113
+ }
114
+
115
+ #[derive(Clone, Debug)]
116
+ pub struct JudgeResult {
117
+ pub passed: bool,
118
+ pub passed_cases: usize,
119
+ pub total_cases: usize,
120
+ pub output: String,
121
+ }
122
+
123
+ pub fn default_language() -> String {
124
+ "python".to_string()
125
+ }
126
+
127
+ pub fn default_ui_language() -> String {
128
+ "en".to_string()
129
+ }
130
+
131
+ pub fn default_theme() -> String {
132
+ "dark".to_string()
133
+ }
134
+
135
+ pub fn default_editor() -> String {
136
+ "vim".to_string()
137
+ }
138
+
139
+ pub fn default_next_source() -> String {
140
+ "bank".to_string()
141
+ }
142
+
143
+ pub fn default_ai_provider() -> String {
144
+ "codex".to_string()
145
+ }
146
+
147
+ pub fn default_ai_model() -> String {
148
+ "auto".to_string()
149
+ }
150
+
151
+ pub fn default_suggested_difficulty() -> String {
152
+ "easy".to_string()
153
+ }