practicode 0.1.8 → 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 +1 -1
- package/Cargo.toml +1 -1
- package/README.md +5 -0
- package/SECURITY.md +11 -0
- package/docs/ARCHITECTURE.md +20 -4
- package/docs/MAINTAINING.md +5 -0
- package/package.json +7 -2
- package/src/core/bank.rs +148 -0
- package/src/core/judge.rs +205 -0
- package/src/core/language.rs +92 -0
- package/src/core/model.rs +153 -0
- package/src/core/problem_files.rs +92 -0
- package/src/core/progress.rs +97 -0
- package/src/core/render.rs +119 -0
- package/src/core/state.rs +80 -0
- package/src/core.rs +18 -983
- package/src/tui/actions.rs +312 -0
- package/src/tui/command_handlers.rs +128 -0
- package/src/tui/command_input.rs +117 -0
- package/src/tui/events.rs +188 -0
- package/src/tui/problem_list.rs +163 -0
- package/src/tui/status.rs +106 -0
- package/src/tui/tasks.rs +279 -0
- package/src/tui/view.rs +342 -0
- package/src/tui.rs +8 -1607
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|

|
|
8
8
|

|
|
9
9
|

|
|
10
|
+
[Socket.dev package health](https://socket.dev/npm/package/practicode)
|
|
10
11
|
|
|
11
12
|

|
|
12
13
|
|
|
@@ -36,6 +37,8 @@ npm install -g practicode
|
|
|
36
37
|
practicode
|
|
37
38
|
```
|
|
38
39
|
|
|
40
|
+
The npm package has a `postinstall` step that runs `cargo build --release --locked` from the package root so the Rust TUI binary is ready. Set `PRACTICODE_SKIP_BUILD=1` to skip that install-time build; the `practicode` launcher will try the same locked Cargo build on first run if the binary is missing.
|
|
41
|
+
|
|
39
42
|
### Cargo
|
|
40
43
|
|
|
41
44
|
```bash
|
|
@@ -176,6 +179,8 @@ cargo install --force practicode
|
|
|
176
179
|
- `/run` executes your local submission as a normal process. practicode runs it from `.practicode/build/<problem-id>/run`, but this is not an OS sandbox. Only run code you trust.
|
|
177
180
|
- `/hint` sends the current problem and submission to the selected AI provider CLI.
|
|
178
181
|
- AI-backed `/next` and `/generate` can run a custom shell command from `settings.ai_next_command`; save only commands you trust.
|
|
182
|
+
- npm installs run the package `postinstall` script described above. It only invokes Cargo with the checked-in lockfile from this package root; it does not read local `.env`/`.npmrc` files or contact the configured AI provider.
|
|
183
|
+
- npm releases are published from GitHub Actions with registry signatures and provenance enabled in `package.json`. The release workflow is also prepared for npm Trusted Publishing/OIDC; maintainers should prefer that over long-lived publish tokens when the package setting is enabled on npm.
|
|
179
184
|
- Local `.env`, `.npmrc`, `.practicode/`, `problems/`, and `submissions/` are ignored by git. Do not commit tokens, private prompts, or answer keys.
|
|
180
185
|
|
|
181
186
|
## Development Checks
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Reporting
|
|
4
|
+
|
|
5
|
+
Report vulnerabilities through GitHub Security Advisories for this repository when available. If that is not available, open a minimal public issue asking for a private security contact and avoid posting exploit details.
|
|
6
|
+
|
|
7
|
+
Do not include tokens, private prompts, `.env`, `.npmrc`, `.practicode/`, `problems/`, or `submissions/` contents in public reports.
|
|
8
|
+
|
|
9
|
+
## Scope
|
|
10
|
+
|
|
11
|
+
Security-sensitive areas include npm install scripts, release publishing, command execution, local judging, AI provider prompts, update checks, and local user data handling.
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -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`
|
|
8
|
-
- `src/core/
|
|
9
|
-
- `src/
|
|
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
|
|
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/docs/MAINTAINING.md
CHANGED
|
@@ -45,11 +45,16 @@ Verify publication:
|
|
|
45
45
|
```bash
|
|
46
46
|
gh run list --limit 5
|
|
47
47
|
npm view practicode version
|
|
48
|
+
npm view practicode dist.signatures dist.attestations --json
|
|
48
49
|
cargo search practicode --limit 1
|
|
49
50
|
```
|
|
50
51
|
|
|
51
52
|
Do not print or commit tokens. Local `.env` and `.npmrc` are ignored; GitHub Actions uses `NPM_TOKEN` and `CRATES_TOKEN` repository secrets.
|
|
52
53
|
|
|
54
|
+
For npm supply-chain posture, keep `publishConfig.provenance` enabled and keep the release job's `id-token: write` permission. When the npm package's Trusted Publisher setting is configured for this repository and `.github/workflows/release.yml`, remove the long-lived `NPM_TOKEN` dependency from the npm publish steps and disallow token publishing in the npm package settings.
|
|
55
|
+
|
|
56
|
+
Socket.dev indexes the npm package page at <https://socket.dev/npm/package/practicode>. It may lag behind npm immediately after a release; verify npm first with `npm view practicode version`, then re-check Socket after indexing catches up. If Socket flags the npm `postinstall` script, confirm it still only runs the locked Cargo build documented in the README.
|
|
57
|
+
|
|
53
58
|
## Documentation Ownership
|
|
54
59
|
|
|
55
60
|
| File | Audience |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "practicode",
|
|
3
|
-
"version": "0.1.
|
|
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": {
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
"url": "git+https://github.com/baba9811/practicode.git"
|
|
9
9
|
},
|
|
10
10
|
"homepage": "https://github.com/baba9811/practicode#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/baba9811/practicode/issues"
|
|
13
|
+
},
|
|
11
14
|
"bin": {
|
|
12
15
|
"practicode": "bin/practicode.js"
|
|
13
16
|
},
|
|
@@ -27,6 +30,7 @@
|
|
|
27
30
|
"Cargo.toml",
|
|
28
31
|
"LICENSE",
|
|
29
32
|
"README.md",
|
|
33
|
+
"SECURITY.md",
|
|
30
34
|
"THIRD_PARTY_LICENSES.md"
|
|
31
35
|
],
|
|
32
36
|
"keywords": [
|
|
@@ -41,6 +45,7 @@
|
|
|
41
45
|
"node": ">=18"
|
|
42
46
|
},
|
|
43
47
|
"publishConfig": {
|
|
44
|
-
"access": "public"
|
|
48
|
+
"access": "public",
|
|
49
|
+
"provenance": true
|
|
45
50
|
}
|
|
46
51
|
}
|
package/src/core/bank.rs
ADDED
|
@@ -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
|
+
}
|