practicode 0.1.3 → 0.1.5
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 +1 -2
- package/assets/i18n/en.json +5 -2
- package/assets/i18n/es.json +5 -2
- package/assets/i18n/ja.json +5 -2
- package/assets/i18n/ko.json +5 -2
- package/assets/i18n/zh.json +5 -2
- package/package.json +1 -1
- package/src/ai.rs +56 -7
- package/src/core.rs +20 -18
- package/src/lib.rs +11 -3
- package/src/tui.rs +178 -48
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
package/README.md
CHANGED
|
@@ -63,6 +63,7 @@ Submissions are saved as you type under `submissions/<problem-id>/solution.<ext>
|
|
|
63
63
|
| Command | Action |
|
|
64
64
|
| --- | --- |
|
|
65
65
|
| `/run` | Judge the current submission |
|
|
66
|
+
| `/code` | Return to the code editor |
|
|
66
67
|
| `/next` | Open the next local problem, or ask AI to create one |
|
|
67
68
|
| `/next easy string problem` | Ask AI for a custom next problem |
|
|
68
69
|
| `/prev` | Go back through problem history |
|
|
@@ -78,7 +79,6 @@ Submissions are saved as you type under `submissions/<problem-id>/solution.<ext>
|
|
|
78
79
|
| `/lang python` | Set code language: `python`, `ts`, `java`, `rust` |
|
|
79
80
|
| `/ui en` | Set UI language: `en`, `ko`, `ja`, `zh`, `es` |
|
|
80
81
|
| `/theme dark` | Set theme: `dark` or `light` |
|
|
81
|
-
| `/source ai` | Prefer AI for next-problem generation |
|
|
82
82
|
| `/update` | Show update instructions when a newer version is available |
|
|
83
83
|
| `/exit` | Quit |
|
|
84
84
|
|
|
@@ -106,7 +106,6 @@ Claude Code is also supported:
|
|
|
106
106
|
```text
|
|
107
107
|
/provider claude
|
|
108
108
|
/model sonnet
|
|
109
|
-
/source ai
|
|
110
109
|
```
|
|
111
110
|
|
|
112
111
|
Generated problems and submissions stay local:
|
package/assets/i18n/en.json
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"palette_hint": "up/down select | Enter run | Esc cancel",
|
|
16
16
|
"hint_command": "Enter submit | Esc cancel",
|
|
17
17
|
"hint_list": "up/down move | Enter open | Esc close",
|
|
18
|
-
"hint_output": "Esc
|
|
18
|
+
"hint_output": "Esc/click edit | / command | ? help",
|
|
19
19
|
"hint_code": "Esc then / command",
|
|
20
20
|
"hint_idle": "/ command | ? help",
|
|
21
21
|
"help_title": "Help",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"keys": "Keys",
|
|
24
24
|
"debug_prints": "Debug prints",
|
|
25
25
|
"cmd_run": "Judge the current submission",
|
|
26
|
+
"cmd_code": "Return to the code editor",
|
|
26
27
|
"cmd_edit": "Return to the code editor",
|
|
27
28
|
"cmd_next": "Open the next problem",
|
|
28
29
|
"cmd_prev": "Open the previous problem",
|
|
@@ -50,5 +51,7 @@
|
|
|
50
51
|
"update_check_disabled": "Update check is disabled.",
|
|
51
52
|
"update_check_failed": "Could not check for updates.",
|
|
52
53
|
"generating_next": "Generating next problem",
|
|
53
|
-
"already_busy": "Already busy."
|
|
54
|
+
"already_busy": "Already busy.",
|
|
55
|
+
"run_pass_next": "Next: /next",
|
|
56
|
+
"run_fail_next": "Fix: press Esc or click this pane to edit, then /run"
|
|
54
57
|
}
|
package/assets/i18n/es.json
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"palette_hint": "arriba/abajo elegir | Enter ejecutar | Esc cancelar",
|
|
16
16
|
"hint_command": "Enter ejecutar | Esc cancelar",
|
|
17
17
|
"hint_list": "arriba/abajo mover | Enter abrir | Esc cerrar",
|
|
18
|
-
"hint_output": "Esc
|
|
18
|
+
"hint_output": "Esc/clic editar | / comando | ? ayuda",
|
|
19
19
|
"hint_code": "Esc y luego / comando",
|
|
20
20
|
"hint_idle": "/ comando | ? ayuda",
|
|
21
21
|
"help_title": "Ayuda",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"keys": "Teclas",
|
|
24
24
|
"debug_prints": "Salida de depuracion",
|
|
25
25
|
"cmd_run": "Evalua la solucion actual",
|
|
26
|
+
"cmd_code": "Volver al editor de codigo",
|
|
26
27
|
"cmd_edit": "Volver al editor de codigo",
|
|
27
28
|
"cmd_next": "Abrir el siguiente problema",
|
|
28
29
|
"cmd_prev": "Abrir el problema anterior",
|
|
@@ -50,5 +51,7 @@
|
|
|
50
51
|
"update_check_disabled": "La comprobacion de actualizaciones esta desactivada.",
|
|
51
52
|
"update_check_failed": "No se pudo comprobar actualizaciones.",
|
|
52
53
|
"generating_next": "Generando el siguiente problema",
|
|
53
|
-
"already_busy": "Ya hay una tarea en curso."
|
|
54
|
+
"already_busy": "Ya hay una tarea en curso.",
|
|
55
|
+
"run_pass_next": "Siguiente: /next",
|
|
56
|
+
"run_fail_next": "Corrige: pulsa Esc o haz clic en este panel para editar, luego /run"
|
|
54
57
|
}
|
package/assets/i18n/ja.json
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"palette_hint": "上下 選択 | Enter 実行 | Esc キャンセル",
|
|
16
16
|
"hint_command": "Enter 実行 | Esc キャンセル",
|
|
17
17
|
"hint_list": "上下 移動 | Enter 開く | Esc 閉じる",
|
|
18
|
-
"hint_output": "Esc
|
|
18
|
+
"hint_output": "Esc/クリック 編集 | / コマンド | ? ヘルプ",
|
|
19
19
|
"hint_code": "Esc の後 / コマンド",
|
|
20
20
|
"hint_idle": "/ コマンド | ? ヘルプ",
|
|
21
21
|
"help_title": "ヘルプ",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"keys": "キー",
|
|
24
24
|
"debug_prints": "デバッグ出力",
|
|
25
25
|
"cmd_run": "現在の提出を判定",
|
|
26
|
+
"cmd_code": "コードエディタに戻る",
|
|
26
27
|
"cmd_edit": "コードエディタに戻る",
|
|
27
28
|
"cmd_next": "次の問題を開く",
|
|
28
29
|
"cmd_prev": "前の問題を開く",
|
|
@@ -50,5 +51,7 @@
|
|
|
50
51
|
"update_check_disabled": "更新確認は無効です。",
|
|
51
52
|
"update_check_failed": "更新を確認できませんでした。",
|
|
52
53
|
"generating_next": "次の問題を生成中",
|
|
53
|
-
"already_busy": "すでに処理中です。"
|
|
54
|
+
"already_busy": "すでに処理中です。",
|
|
55
|
+
"run_pass_next": "次: /next",
|
|
56
|
+
"run_fail_next": "修正: Esc またはこのペインをクリックして編集し、/run"
|
|
54
57
|
}
|
package/assets/i18n/ko.json
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"palette_hint": "위/아래 선택 | Enter 실행 | Esc 취소",
|
|
16
16
|
"hint_command": "Enter 실행 | Esc 취소",
|
|
17
17
|
"hint_list": "위/아래 이동 | Enter 열기 | Esc 닫기",
|
|
18
|
-
"hint_output": "Esc
|
|
18
|
+
"hint_output": "Esc/클릭 편집 | / 명령 | ? 도움말",
|
|
19
19
|
"hint_code": "Esc 후 / 명령",
|
|
20
20
|
"hint_idle": "/ 명령 | ? 도움말",
|
|
21
21
|
"help_title": "도움말",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"keys": "키",
|
|
24
24
|
"debug_prints": "디버그 출력",
|
|
25
25
|
"cmd_run": "현재 제출을 채점",
|
|
26
|
+
"cmd_code": "코드 편집기로 돌아가기",
|
|
26
27
|
"cmd_edit": "코드 편집기로 돌아가기",
|
|
27
28
|
"cmd_next": "다음 문제 열기",
|
|
28
29
|
"cmd_prev": "이전 문제 열기",
|
|
@@ -50,5 +51,7 @@
|
|
|
50
51
|
"update_check_disabled": "업데이트 확인이 꺼져 있습니다.",
|
|
51
52
|
"update_check_failed": "업데이트를 확인할 수 없습니다.",
|
|
52
53
|
"generating_next": "다음 문제 생성 중",
|
|
53
|
-
"already_busy": "이미 작업 중입니다."
|
|
54
|
+
"already_busy": "이미 작업 중입니다.",
|
|
55
|
+
"run_pass_next": "다음: /next",
|
|
56
|
+
"run_fail_next": "수정: Esc를 누르거나 이 창을 클릭해 편집한 뒤 /run"
|
|
54
57
|
}
|
package/assets/i18n/zh.json
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"palette_hint": "上下 选择 | Enter 执行 | Esc 取消",
|
|
16
16
|
"hint_command": "Enter 执行 | Esc 取消",
|
|
17
17
|
"hint_list": "上下 移动 | Enter 打开 | Esc 关闭",
|
|
18
|
-
"hint_output": "Esc
|
|
18
|
+
"hint_output": "Esc/点击 编辑 | / 命令 | ? 帮助",
|
|
19
19
|
"hint_code": "Esc 后输入 / 命令",
|
|
20
20
|
"hint_idle": "/ 命令 | ? 帮助",
|
|
21
21
|
"help_title": "帮助",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"keys": "按键",
|
|
24
24
|
"debug_prints": "调试输出",
|
|
25
25
|
"cmd_run": "评测当前提交",
|
|
26
|
+
"cmd_code": "回到代码编辑器",
|
|
26
27
|
"cmd_edit": "回到代码编辑器",
|
|
27
28
|
"cmd_next": "打开下一题",
|
|
28
29
|
"cmd_prev": "打开上一题",
|
|
@@ -50,5 +51,7 @@
|
|
|
50
51
|
"update_check_disabled": "更新检查已禁用。",
|
|
51
52
|
"update_check_failed": "无法检查更新。",
|
|
52
53
|
"generating_next": "正在生成下一题",
|
|
53
|
-
"already_busy": "已有任务正在运行。"
|
|
54
|
+
"already_busy": "已有任务正在运行。",
|
|
55
|
+
"run_pass_next": "下一步: /next",
|
|
56
|
+
"run_fail_next": "修复: 按 Esc 或点击此面板编辑,然后 /run"
|
|
54
57
|
}
|
package/package.json
CHANGED
package/src/ai.rs
CHANGED
|
@@ -15,6 +15,12 @@ use std::{
|
|
|
15
15
|
time::Duration,
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
#[derive(Clone, Debug, Default)]
|
|
19
|
+
pub struct ModelCatalog {
|
|
20
|
+
pub models: Vec<String>,
|
|
21
|
+
pub message: Option<String>,
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
pub fn run_ai_prompt(root: &Path, problem: &Problem, settings: &Settings, prompt: &str) -> String {
|
|
19
25
|
let solution = match ensure_submission(root, problem, settings) {
|
|
20
26
|
Ok(path) => path,
|
|
@@ -102,10 +108,17 @@ pub fn provider_status(provider: &str) -> String {
|
|
|
102
108
|
}
|
|
103
109
|
}
|
|
104
110
|
|
|
105
|
-
pub fn available_models(provider: &str) ->
|
|
111
|
+
pub fn available_models(provider: &str) -> ModelCatalog {
|
|
106
112
|
match normalize_ai_provider(provider).as_str() {
|
|
107
113
|
"codex" => codex_models(),
|
|
108
|
-
|
|
114
|
+
"claude" => ModelCatalog {
|
|
115
|
+
models: Vec::new(),
|
|
116
|
+
message: Some(
|
|
117
|
+
"Claude CLI does not expose a model list; use /model <name> for a known model."
|
|
118
|
+
.to_string(),
|
|
119
|
+
),
|
|
120
|
+
},
|
|
121
|
+
_ => ModelCatalog::default(),
|
|
109
122
|
}
|
|
110
123
|
}
|
|
111
124
|
|
|
@@ -139,20 +152,56 @@ pub fn read_problem_notes(root: &Path) -> Result<String> {
|
|
|
139
152
|
}
|
|
140
153
|
}
|
|
141
154
|
|
|
142
|
-
fn codex_models() ->
|
|
155
|
+
fn codex_models() -> ModelCatalog {
|
|
143
156
|
if which("codex").is_none() {
|
|
144
|
-
return
|
|
157
|
+
return ModelCatalog {
|
|
158
|
+
models: Vec::new(),
|
|
159
|
+
message: Some(
|
|
160
|
+
"Codex CLI not found; choose /provider claude or install Codex CLI.".to_string(),
|
|
161
|
+
),
|
|
162
|
+
};
|
|
145
163
|
}
|
|
164
|
+
if codex_daemon_path().is_none_or(|path| !path.exists()) {
|
|
165
|
+
return ModelCatalog {
|
|
166
|
+
models: Vec::new(),
|
|
167
|
+
message: Some("Codex app-server daemon is unavailable; install the standalone Codex app to list models, or use /model <name>.".to_string()),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
let mut start = Command::new("codex");
|
|
171
|
+
start.args(["app-server", "daemon", "start"]);
|
|
172
|
+
let _ = run_capture(&mut start, "", Duration::from_secs(5));
|
|
146
173
|
let mut command = Command::new("codex");
|
|
147
174
|
command.args(["app-server", "proxy"]);
|
|
148
175
|
let input = r#"{"id":1,"method":"model/list","params":{"limit":25}}"#;
|
|
149
176
|
let Ok(run) = run_capture(&mut command, &format!("{input}\n"), Duration::from_secs(2)) else {
|
|
150
|
-
return
|
|
177
|
+
return ModelCatalog {
|
|
178
|
+
models: Vec::new(),
|
|
179
|
+
message: Some("Could not query Codex model list.".to_string()),
|
|
180
|
+
};
|
|
151
181
|
};
|
|
152
182
|
if run.code != Some(0) {
|
|
153
|
-
|
|
183
|
+
let detail = output_text(&run.stdout, &run.stderr);
|
|
184
|
+
return ModelCatalog {
|
|
185
|
+
models: Vec::new(),
|
|
186
|
+
message: Some(if detail.is_empty() {
|
|
187
|
+
"Could not query Codex model list.".to_string()
|
|
188
|
+
} else {
|
|
189
|
+
format!("Could not query Codex model list: {detail}")
|
|
190
|
+
}),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
let models = parse_model_list(&run.stdout);
|
|
194
|
+
if models.is_empty() {
|
|
195
|
+
ModelCatalog {
|
|
196
|
+
models,
|
|
197
|
+
message: Some("Codex app-server returned no models.".to_string()),
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
ModelCatalog {
|
|
201
|
+
models,
|
|
202
|
+
message: None,
|
|
203
|
+
}
|
|
154
204
|
}
|
|
155
|
-
parse_model_list(&run.stdout)
|
|
156
205
|
}
|
|
157
206
|
|
|
158
207
|
fn parse_model_list(output: &str) -> Vec<String> {
|
package/src/core.rs
CHANGED
|
@@ -601,8 +601,8 @@ pub fn judge(root: &Path, problem: &Problem, settings: &Settings) -> JudgeResult
|
|
|
601
601
|
let run = match run_capture(&mut process, &case.input, Duration::from_secs(5)) {
|
|
602
602
|
Ok(run) => run,
|
|
603
603
|
Err(error) => {
|
|
604
|
-
lines.push(format!("
|
|
605
|
-
lines
|
|
604
|
+
lines.push(format!("Case {}: FAIL", index + 1));
|
|
605
|
+
push_labeled_block(&mut lines, "Error", &error.to_string());
|
|
606
606
|
break;
|
|
607
607
|
}
|
|
608
608
|
};
|
|
@@ -610,28 +610,20 @@ pub fn judge(root: &Path, problem: &Problem, settings: &Settings) -> JudgeResult
|
|
|
610
610
|
let expected = case.output.trim();
|
|
611
611
|
if !run.timed_out && run.code == Some(0) && got == expected {
|
|
612
612
|
passed += 1;
|
|
613
|
-
lines.push(format!("
|
|
613
|
+
lines.push(format!("Case {}: PASS", index + 1));
|
|
614
614
|
if !run.stderr.trim().is_empty() {
|
|
615
|
-
lines
|
|
616
|
-
lines.push(run.stderr.trim_end().to_string());
|
|
615
|
+
push_labeled_block(&mut lines, "Stderr", run.stderr.trim_end());
|
|
617
616
|
}
|
|
618
617
|
} else {
|
|
619
|
-
lines.push(format!("
|
|
618
|
+
lines.push(format!("Case {}: FAIL", index + 1));
|
|
620
619
|
if run.timed_out {
|
|
621
|
-
lines
|
|
620
|
+
push_labeled_block(&mut lines, "Error", "timeout: 5s");
|
|
622
621
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
lines.push("stdout:".to_string());
|
|
627
|
-
lines.push(if run.stdout.trim_end().is_empty() {
|
|
628
|
-
"<empty>".to_string()
|
|
629
|
-
} else {
|
|
630
|
-
run.stdout.trim_end().to_string()
|
|
631
|
-
});
|
|
622
|
+
push_labeled_block(&mut lines, "Input", case.input.trim_end());
|
|
623
|
+
push_labeled_block(&mut lines, "Expected", expected);
|
|
624
|
+
push_labeled_block(&mut lines, "Got", run.stdout.trim_end());
|
|
632
625
|
if !run.stderr.trim().is_empty() {
|
|
633
|
-
lines
|
|
634
|
-
lines.push(run.stderr.trim_end().to_string());
|
|
626
|
+
push_labeled_block(&mut lines, "Stderr", run.stderr.trim_end());
|
|
635
627
|
}
|
|
636
628
|
break;
|
|
637
629
|
}
|
|
@@ -645,6 +637,16 @@ pub fn judge(root: &Path, problem: &Problem, settings: &Settings) -> JudgeResult
|
|
|
645
637
|
}
|
|
646
638
|
}
|
|
647
639
|
|
|
640
|
+
fn push_labeled_block(lines: &mut Vec<String>, label: &str, body: &str) {
|
|
641
|
+
lines.push(String::new());
|
|
642
|
+
lines.push(label.to_string());
|
|
643
|
+
if body.is_empty() {
|
|
644
|
+
lines.push(" <empty>".to_string());
|
|
645
|
+
} else {
|
|
646
|
+
lines.extend(body.lines().map(|line| format!(" {line}")));
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
648
650
|
pub fn command_for(root: &Path, path: &Path, language: &str) -> Result<Option<CommandSpec>> {
|
|
649
651
|
match language {
|
|
650
652
|
"python" => Ok(which("python3")
|
package/src/lib.rs
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
use anyhow::{Context, Result};
|
|
2
|
-
use crossterm::{
|
|
2
|
+
use crossterm::{
|
|
3
|
+
cursor::SetCursorStyle,
|
|
4
|
+
event::{DisableMouseCapture, EnableMouseCapture},
|
|
5
|
+
execute,
|
|
6
|
+
};
|
|
3
7
|
use std::{env, io::stdout};
|
|
4
8
|
|
|
5
9
|
pub mod ai;
|
|
@@ -25,9 +29,13 @@ pub fn run_cli() -> Result<()> {
|
|
|
25
29
|
|
|
26
30
|
let mut app = tui::PracticodeApp::new(root)?;
|
|
27
31
|
let mut terminal = ratatui::init();
|
|
28
|
-
let _ = execute!(stdout(), SetCursorStyle::SteadyBar);
|
|
32
|
+
let _ = execute!(stdout(), SetCursorStyle::SteadyBar, EnableMouseCapture);
|
|
29
33
|
let result = app.run(&mut terminal);
|
|
30
34
|
ratatui::restore();
|
|
31
|
-
let _ = execute!(
|
|
35
|
+
let _ = execute!(
|
|
36
|
+
stdout(),
|
|
37
|
+
SetCursorStyle::DefaultUserShape,
|
|
38
|
+
DisableMouseCapture
|
|
39
|
+
);
|
|
32
40
|
result
|
|
33
41
|
}
|
package/src/tui.rs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
use crate::{
|
|
2
2
|
ai::{
|
|
3
|
-
append_problem_note, available_models, provider_status, read_problem_notes,
|
|
4
|
-
run_ai_prompt,
|
|
3
|
+
ModelCatalog, append_problem_note, available_models, provider_status, read_problem_notes,
|
|
4
|
+
run_ai_next, run_ai_prompt,
|
|
5
5
|
},
|
|
6
6
|
core::{
|
|
7
7
|
AI_PROVIDERS, AppState, HistoryItem, LANGUAGES, PROBLEM_NOTES_PATH, Problem, THEMES,
|
|
@@ -16,7 +16,10 @@ use crate::{
|
|
|
16
16
|
update::{CURRENT_VERSION, UpdateCheck, check_latest_version},
|
|
17
17
|
};
|
|
18
18
|
use anyhow::Result;
|
|
19
|
-
use crossterm::event::{
|
|
19
|
+
use crossterm::event::{
|
|
20
|
+
self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEvent,
|
|
21
|
+
MouseEventKind,
|
|
22
|
+
};
|
|
20
23
|
use ratatui::{
|
|
21
24
|
DefaultTerminal, Frame,
|
|
22
25
|
layout::{Constraint, Direction, Layout, Position, Rect},
|
|
@@ -59,9 +62,9 @@ const COMMAND_HINTS: &[CommandHint] = &[
|
|
|
59
62
|
help: true,
|
|
60
63
|
},
|
|
61
64
|
CommandHint {
|
|
62
|
-
insert: "
|
|
63
|
-
display: "/
|
|
64
|
-
desc_key: "
|
|
65
|
+
insert: "code",
|
|
66
|
+
display: "/code",
|
|
67
|
+
desc_key: "cmd_code",
|
|
65
68
|
keep_open: false,
|
|
66
69
|
help: true,
|
|
67
70
|
},
|
|
@@ -226,20 +229,6 @@ const COMMAND_HINTS: &[CommandHint] = &[
|
|
|
226
229
|
keep_open: false,
|
|
227
230
|
help: false,
|
|
228
231
|
},
|
|
229
|
-
CommandHint {
|
|
230
|
-
insert: "source local",
|
|
231
|
-
display: "/source local",
|
|
232
|
-
desc_key: "cmd_source",
|
|
233
|
-
keep_open: false,
|
|
234
|
-
help: false,
|
|
235
|
-
},
|
|
236
|
-
CommandHint {
|
|
237
|
-
insert: "source ai",
|
|
238
|
-
display: "/source ai",
|
|
239
|
-
desc_key: "cmd_source",
|
|
240
|
-
keep_open: false,
|
|
241
|
-
help: false,
|
|
242
|
-
},
|
|
243
232
|
CommandHint {
|
|
244
233
|
insert: "update",
|
|
245
234
|
display: "/update",
|
|
@@ -282,6 +271,7 @@ pub struct PracticodeApp {
|
|
|
282
271
|
command_palette_cursor: usize,
|
|
283
272
|
output: String,
|
|
284
273
|
output_is_markdown: bool,
|
|
274
|
+
showing_model_status: bool,
|
|
285
275
|
show_output: bool,
|
|
286
276
|
focus: Focus,
|
|
287
277
|
list_cursor: Option<usize>,
|
|
@@ -290,11 +280,15 @@ pub struct PracticodeApp {
|
|
|
290
280
|
busy_frame: usize,
|
|
291
281
|
task_rx: Option<Receiver<TaskResult>>,
|
|
292
282
|
update_rx: Option<Receiver<UpdateCheck>>,
|
|
293
|
-
model_rx: Option<Receiver<
|
|
283
|
+
model_rx: Option<Receiver<ModelCatalog>>,
|
|
294
284
|
available_models: Vec<String>,
|
|
295
285
|
available_models_provider: String,
|
|
286
|
+
model_message: Option<String>,
|
|
296
287
|
update_check: Option<UpdateCheck>,
|
|
297
288
|
update_notice: Option<String>,
|
|
289
|
+
code_area: Rect,
|
|
290
|
+
output_area: Rect,
|
|
291
|
+
command_area: Rect,
|
|
298
292
|
should_quit: bool,
|
|
299
293
|
}
|
|
300
294
|
|
|
@@ -325,6 +319,7 @@ impl PracticodeApp {
|
|
|
325
319
|
command_palette_cursor: 0,
|
|
326
320
|
output: String::new(),
|
|
327
321
|
output_is_markdown: false,
|
|
322
|
+
showing_model_status: false,
|
|
328
323
|
show_output: false,
|
|
329
324
|
focus: Focus::Code,
|
|
330
325
|
list_cursor: None,
|
|
@@ -336,8 +331,12 @@ impl PracticodeApp {
|
|
|
336
331
|
model_rx: None,
|
|
337
332
|
available_models: Vec::new(),
|
|
338
333
|
available_models_provider: String::new(),
|
|
334
|
+
model_message: None,
|
|
339
335
|
update_check: None,
|
|
340
336
|
update_notice: None,
|
|
337
|
+
code_area: Rect::default(),
|
|
338
|
+
output_area: Rect::default(),
|
|
339
|
+
command_area: Rect::default(),
|
|
341
340
|
should_quit: false,
|
|
342
341
|
};
|
|
343
342
|
app.load_code_editor()?;
|
|
@@ -353,11 +352,12 @@ impl PracticodeApp {
|
|
|
353
352
|
self.check_update();
|
|
354
353
|
self.start_model_check();
|
|
355
354
|
self.check_models();
|
|
356
|
-
if event::poll(Duration::from_millis(100))?
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
355
|
+
if event::poll(Duration::from_millis(100))? {
|
|
356
|
+
match event::read()? {
|
|
357
|
+
Event::Key(key) if key.kind != KeyEventKind::Release => self.handle_key(key)?,
|
|
358
|
+
Event::Mouse(mouse) => self.handle_mouse(mouse)?,
|
|
359
|
+
_ => {}
|
|
360
|
+
}
|
|
361
361
|
}
|
|
362
362
|
if !self.busy_label.is_empty() {
|
|
363
363
|
self.busy_frame = (self.busy_frame + 1) % 16;
|
|
@@ -391,6 +391,16 @@ impl PracticodeApp {
|
|
|
391
391
|
self.handle_key(key)
|
|
392
392
|
}
|
|
393
393
|
|
|
394
|
+
pub fn handle_mouse_for_test(&mut self, mouse: MouseEvent) -> Result<()> {
|
|
395
|
+
self.handle_mouse(mouse)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
pub fn set_pane_areas_for_test(&mut self, code: Rect, output: Rect, command: Rect) {
|
|
399
|
+
self.code_area = code;
|
|
400
|
+
self.output_area = output;
|
|
401
|
+
self.command_area = command;
|
|
402
|
+
}
|
|
403
|
+
|
|
394
404
|
pub fn busy_label(&self) -> &str {
|
|
395
405
|
&self.busy_label
|
|
396
406
|
}
|
|
@@ -403,6 +413,10 @@ impl PracticodeApp {
|
|
|
403
413
|
self.status_text()
|
|
404
414
|
}
|
|
405
415
|
|
|
416
|
+
pub fn output_for_test(&self) -> &str {
|
|
417
|
+
&self.output
|
|
418
|
+
}
|
|
419
|
+
|
|
406
420
|
pub fn command_suggestions_for_test(&self) -> Vec<String> {
|
|
407
421
|
self.command_suggestions()
|
|
408
422
|
.into_iter()
|
|
@@ -413,6 +427,13 @@ impl PracticodeApp {
|
|
|
413
427
|
pub fn set_available_models_for_test(&mut self, models: Vec<&str>) {
|
|
414
428
|
self.available_models = models.into_iter().map(str::to_string).collect();
|
|
415
429
|
self.available_models_provider = self.state.settings.ai_provider.clone();
|
|
430
|
+
self.model_message = None;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
pub fn set_model_message_for_test(&mut self, message: &str) {
|
|
434
|
+
self.available_models.clear();
|
|
435
|
+
self.available_models_provider = self.state.settings.ai_provider.clone();
|
|
436
|
+
self.model_message = Some(message.to_string());
|
|
416
437
|
}
|
|
417
438
|
|
|
418
439
|
pub fn pane_title_for_test(title: &str, active: bool) -> String {
|
|
@@ -433,6 +454,9 @@ impl PracticodeApp {
|
|
|
433
454
|
.direction(Direction::Horizontal)
|
|
434
455
|
.constraints([Constraint::Percentage(58), Constraint::Percentage(42)])
|
|
435
456
|
.split(vertical[0]);
|
|
457
|
+
self.code_area = body[1];
|
|
458
|
+
self.output_area = body[1];
|
|
459
|
+
self.command_area = vertical[2];
|
|
436
460
|
|
|
437
461
|
let problem = Paragraph::new(self.problem_text())
|
|
438
462
|
.block(Self::block(
|
|
@@ -444,13 +468,7 @@ impl PracticodeApp {
|
|
|
444
468
|
frame.render_widget(problem, body[0]);
|
|
445
469
|
|
|
446
470
|
if self.show_output {
|
|
447
|
-
let text =
|
|
448
|
-
format!("{}{}", self.busy_body, self.busy_dots())
|
|
449
|
-
} else if self.output_is_markdown {
|
|
450
|
-
render_markdown_plain(&self.output)
|
|
451
|
-
} else {
|
|
452
|
-
self.output.clone()
|
|
453
|
-
};
|
|
471
|
+
let text = self.output_text();
|
|
454
472
|
let output = Paragraph::new(text)
|
|
455
473
|
.block(Self::block(
|
|
456
474
|
ui_text(&self.state.settings.ui_language, "output"),
|
|
@@ -503,6 +521,79 @@ impl PracticodeApp {
|
|
|
503
521
|
self.set_terminal_cursor(frame, body[1], vertical[2]);
|
|
504
522
|
}
|
|
505
523
|
|
|
524
|
+
fn output_text(&self) -> Text<'static> {
|
|
525
|
+
let light = self.state.settings.theme == "light";
|
|
526
|
+
let title_style = if light {
|
|
527
|
+
Style::default()
|
|
528
|
+
.fg(Color::Blue)
|
|
529
|
+
.add_modifier(Modifier::BOLD)
|
|
530
|
+
} else {
|
|
531
|
+
Style::default()
|
|
532
|
+
.fg(Color::Yellow)
|
|
533
|
+
.add_modifier(Modifier::BOLD)
|
|
534
|
+
};
|
|
535
|
+
let label_style = if light {
|
|
536
|
+
Style::default()
|
|
537
|
+
.fg(Color::Magenta)
|
|
538
|
+
.add_modifier(Modifier::BOLD)
|
|
539
|
+
} else {
|
|
540
|
+
Style::default()
|
|
541
|
+
.fg(Color::Cyan)
|
|
542
|
+
.add_modifier(Modifier::BOLD)
|
|
543
|
+
};
|
|
544
|
+
let body_style = if light {
|
|
545
|
+
Style::default().fg(Color::Black)
|
|
546
|
+
} else {
|
|
547
|
+
Style::default().fg(Color::Rgb(229, 231, 235))
|
|
548
|
+
};
|
|
549
|
+
let code_style = if light {
|
|
550
|
+
Style::default()
|
|
551
|
+
.fg(Color::Black)
|
|
552
|
+
.bg(Color::Rgb(229, 231, 235))
|
|
553
|
+
} else {
|
|
554
|
+
Style::default()
|
|
555
|
+
.fg(Color::Rgb(243, 244, 246))
|
|
556
|
+
.bg(Color::Rgb(31, 41, 55))
|
|
557
|
+
};
|
|
558
|
+
if !self.busy_label.is_empty() {
|
|
559
|
+
return Text::from(Line::from(Span::styled(
|
|
560
|
+
format!("{}{}", self.busy_body, self.busy_dots()),
|
|
561
|
+
title_style,
|
|
562
|
+
)));
|
|
563
|
+
}
|
|
564
|
+
let output = if self.output_is_markdown {
|
|
565
|
+
render_markdown_plain(&self.output)
|
|
566
|
+
} else {
|
|
567
|
+
self.output.clone()
|
|
568
|
+
};
|
|
569
|
+
let mut lines = Vec::new();
|
|
570
|
+
for line in output.lines() {
|
|
571
|
+
if line.is_empty() {
|
|
572
|
+
lines.push(Line::default());
|
|
573
|
+
} else if line.starts_with("PASS ")
|
|
574
|
+
|| line.starts_with("FAIL ")
|
|
575
|
+
|| line.starts_with("Case ")
|
|
576
|
+
|| line.starts_with("Next:")
|
|
577
|
+
|| line.starts_with("Fix:")
|
|
578
|
+
{
|
|
579
|
+
lines.push(Line::from(Span::styled(line.to_string(), title_style)));
|
|
580
|
+
} else if matches!(
|
|
581
|
+
line,
|
|
582
|
+
"Input" | "Expected" | "Got" | "Stdout" | "Stderr" | "Compile" | "Error"
|
|
583
|
+
) {
|
|
584
|
+
lines.push(Line::from(Span::styled(line.to_string(), label_style)));
|
|
585
|
+
} else if line.starts_with(" ") {
|
|
586
|
+
lines.push(Line::from(vec![
|
|
587
|
+
Span::raw(" "),
|
|
588
|
+
Span::styled(line.trim_start().to_string(), code_style),
|
|
589
|
+
]));
|
|
590
|
+
} else {
|
|
591
|
+
lines.push(Line::from(Span::styled(line.to_string(), body_style)));
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
Text::from(lines)
|
|
595
|
+
}
|
|
596
|
+
|
|
506
597
|
fn problem_text(&self) -> Text<'static> {
|
|
507
598
|
let lang = normalize_ui_language(&self.state.settings.ui_language);
|
|
508
599
|
let light = self.state.settings.theme == "light";
|
|
@@ -717,6 +808,19 @@ impl PracticodeApp {
|
|
|
717
808
|
}
|
|
718
809
|
}
|
|
719
810
|
|
|
811
|
+
fn handle_mouse(&mut self, mouse: MouseEvent) -> Result<()> {
|
|
812
|
+
if !matches!(mouse.kind, MouseEventKind::Down(MouseButton::Left)) {
|
|
813
|
+
return Ok(());
|
|
814
|
+
}
|
|
815
|
+
let position = Position::new(mouse.column, mouse.row);
|
|
816
|
+
if self.command_area.contains(position) {
|
|
817
|
+
self.focus_command();
|
|
818
|
+
} else if self.code_area.contains(position) || self.output_area.contains(position) {
|
|
819
|
+
self.action_edit()?;
|
|
820
|
+
}
|
|
821
|
+
Ok(())
|
|
822
|
+
}
|
|
823
|
+
|
|
720
824
|
fn handle_command_key(&mut self, key: KeyEvent) -> Result<()> {
|
|
721
825
|
match key.code {
|
|
722
826
|
KeyCode::Esc => {
|
|
@@ -872,7 +976,7 @@ impl PracticodeApp {
|
|
|
872
976
|
}
|
|
873
977
|
match command {
|
|
874
978
|
"run" | "r" => self.action_run()?,
|
|
875
|
-
"edit" | "e" => self.action_edit()?,
|
|
979
|
+
"code" | "edit" | "e" => self.action_edit()?,
|
|
876
980
|
"next" | "n" => self.action_next(arg)?,
|
|
877
981
|
"prev" | "previous" | "p" => self.action_previous()?,
|
|
878
982
|
"giveup" | "give" | "g" => self.action_give_up()?,
|
|
@@ -885,12 +989,12 @@ impl PracticodeApp {
|
|
|
885
989
|
"theme" if arg.is_empty() => self.action_toggle_theme()?,
|
|
886
990
|
"theme" if THEMES.contains(&arg) => self.set_theme(arg)?,
|
|
887
991
|
"source" | "next-source" if arg.is_empty() => {
|
|
888
|
-
self.write_text_output(&
|
|
992
|
+
self.write_text_output(&self.next_source_help());
|
|
889
993
|
}
|
|
890
994
|
"source" | "next-source" if matches!(arg, "bank" | "local" | "ai") => {
|
|
891
995
|
self.state.settings.next_source = normalize_next_source(arg);
|
|
892
996
|
save_state(&self.root, &self.state)?;
|
|
893
|
-
self.write_text_output(&
|
|
997
|
+
self.write_text_output(&self.next_source_help());
|
|
894
998
|
}
|
|
895
999
|
"ai-next-command" if !arg.is_empty() => {
|
|
896
1000
|
self.state.settings.ai_next_command = arg.to_string();
|
|
@@ -910,6 +1014,7 @@ impl PracticodeApp {
|
|
|
910
1014
|
self.model_rx = None;
|
|
911
1015
|
self.available_models.clear();
|
|
912
1016
|
self.available_models_provider.clear();
|
|
1017
|
+
self.model_message = None;
|
|
913
1018
|
save_state(&self.root, &self.state)?;
|
|
914
1019
|
self.write_text_output(&format!(
|
|
915
1020
|
"AI provider: {}\n{}",
|
|
@@ -918,7 +1023,9 @@ impl PracticodeApp {
|
|
|
918
1023
|
));
|
|
919
1024
|
}
|
|
920
1025
|
"model" if arg.is_empty() => {
|
|
921
|
-
self.
|
|
1026
|
+
self.start_model_check();
|
|
1027
|
+
self.check_models();
|
|
1028
|
+
self.write_model_status();
|
|
922
1029
|
}
|
|
923
1030
|
"model" => {
|
|
924
1031
|
self.state.settings.ai_model = if arg == "auto" {
|
|
@@ -927,7 +1034,9 @@ impl PracticodeApp {
|
|
|
927
1034
|
arg.to_string()
|
|
928
1035
|
};
|
|
929
1036
|
save_state(&self.root, &self.state)?;
|
|
930
|
-
self.
|
|
1037
|
+
self.start_model_check();
|
|
1038
|
+
self.check_models();
|
|
1039
|
+
self.write_model_status();
|
|
931
1040
|
}
|
|
932
1041
|
"hint" if arg.is_empty() => {
|
|
933
1042
|
self.start_ai_prompt("Give one concise hint for the current problem.")?
|
|
@@ -962,9 +1071,9 @@ impl PracticodeApp {
|
|
|
962
1071
|
result.total_cases
|
|
963
1072
|
);
|
|
964
1073
|
let next_step = if result.passed {
|
|
965
|
-
|
|
1074
|
+
ui_text(&self.state.settings.ui_language, "run_pass_next")
|
|
966
1075
|
} else {
|
|
967
|
-
|
|
1076
|
+
ui_text(&self.state.settings.ui_language, "run_fail_next")
|
|
968
1077
|
};
|
|
969
1078
|
self.write_text_output(&format!("{headline}\n{}\n\n{next_step}", result.output));
|
|
970
1079
|
Ok(())
|
|
@@ -1195,14 +1304,21 @@ impl PracticodeApp {
|
|
|
1195
1304
|
|
|
1196
1305
|
fn check_models(&mut self) {
|
|
1197
1306
|
let models = self.model_rx.as_ref().and_then(|rx| rx.try_recv().ok());
|
|
1198
|
-
if let Some(
|
|
1307
|
+
if let Some(catalog) = models {
|
|
1199
1308
|
self.model_rx = None;
|
|
1200
|
-
self.available_models = models;
|
|
1309
|
+
self.available_models = catalog.models;
|
|
1310
|
+
self.model_message = catalog.message;
|
|
1311
|
+
if self.showing_model_status {
|
|
1312
|
+
self.output = self.model_status_text();
|
|
1313
|
+
self.output_is_markdown = false;
|
|
1314
|
+
self.show_output = true;
|
|
1315
|
+
}
|
|
1201
1316
|
}
|
|
1202
1317
|
}
|
|
1203
1318
|
|
|
1204
1319
|
fn model_status_text(&self) -> String {
|
|
1205
1320
|
let mut lines = vec![
|
|
1321
|
+
format!("AI provider: {}", self.state.settings.ai_provider),
|
|
1206
1322
|
format!(
|
|
1207
1323
|
"AI model: {}",
|
|
1208
1324
|
if self.state.settings.ai_model == "auto" {
|
|
@@ -1213,11 +1329,15 @@ impl PracticodeApp {
|
|
|
1213
1329
|
),
|
|
1214
1330
|
"Use /model auto to let the provider choose its default.".to_string(),
|
|
1215
1331
|
];
|
|
1216
|
-
if self.
|
|
1332
|
+
if self.model_rx.is_some() {
|
|
1333
|
+
lines.push("Loading provider model list...".to_string());
|
|
1334
|
+
} else if self.available_models.is_empty() {
|
|
1217
1335
|
lines.push(
|
|
1218
|
-
|
|
1219
|
-
.
|
|
1336
|
+
self.model_message
|
|
1337
|
+
.clone()
|
|
1338
|
+
.unwrap_or_else(|| "Provider model list is unavailable.".to_string()),
|
|
1220
1339
|
);
|
|
1340
|
+
lines.push("Use /model <name> for a known model.".to_string());
|
|
1221
1341
|
} else {
|
|
1222
1342
|
lines.push("Available models:".to_string());
|
|
1223
1343
|
lines.extend(
|
|
@@ -1244,6 +1364,7 @@ impl PracticodeApp {
|
|
|
1244
1364
|
}
|
|
1245
1365
|
|
|
1246
1366
|
fn write_output(&mut self, output: &str) {
|
|
1367
|
+
self.showing_model_status = false;
|
|
1247
1368
|
self.output = output.to_string();
|
|
1248
1369
|
self.output_is_markdown = true;
|
|
1249
1370
|
self.show_output = true;
|
|
@@ -1251,12 +1372,21 @@ impl PracticodeApp {
|
|
|
1251
1372
|
}
|
|
1252
1373
|
|
|
1253
1374
|
fn write_text_output(&mut self, output: &str) {
|
|
1375
|
+
self.showing_model_status = false;
|
|
1254
1376
|
self.output = output.trim_end().to_string();
|
|
1255
1377
|
self.output_is_markdown = false;
|
|
1256
1378
|
self.show_output = true;
|
|
1257
1379
|
self.focus = Focus::Output;
|
|
1258
1380
|
}
|
|
1259
1381
|
|
|
1382
|
+
fn write_model_status(&mut self) {
|
|
1383
|
+
self.output = self.model_status_text();
|
|
1384
|
+
self.output_is_markdown = false;
|
|
1385
|
+
self.showing_model_status = true;
|
|
1386
|
+
self.show_output = true;
|
|
1387
|
+
self.focus = Focus::Output;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1260
1390
|
fn show_update_notice(&mut self) {
|
|
1261
1391
|
let lang = self.state.settings.ui_language.clone();
|
|
1262
1392
|
if let Some(version) = &self.update_notice {
|
|
@@ -1614,11 +1744,11 @@ impl PracticodeApp {
|
|
|
1614
1744
|
)
|
|
1615
1745
|
}
|
|
1616
1746
|
|
|
1617
|
-
fn
|
|
1747
|
+
fn next_source_help(&self) -> String {
|
|
1618
1748
|
if self.state.settings.next_source == "ai" {
|
|
1619
|
-
"
|
|
1749
|
+
"Next behavior: plain /next asks AI every time. Use /source local to use local problems first.".to_string()
|
|
1620
1750
|
} else {
|
|
1621
|
-
"local"
|
|
1751
|
+
"Next behavior: /next uses local problems first and asks AI when it needs a new one. Use /next <request> to ask AI directly.".to_string()
|
|
1622
1752
|
}
|
|
1623
1753
|
}
|
|
1624
1754
|
|