practicode 0.1.4 → 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/core.rs +20 -18
- package/src/lib.rs +11 -3
- package/src/tui.rs +127 -38
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/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
|
@@ -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",
|
|
@@ -297,6 +286,9 @@ pub struct PracticodeApp {
|
|
|
297
286
|
model_message: Option<String>,
|
|
298
287
|
update_check: Option<UpdateCheck>,
|
|
299
288
|
update_notice: Option<String>,
|
|
289
|
+
code_area: Rect,
|
|
290
|
+
output_area: Rect,
|
|
291
|
+
command_area: Rect,
|
|
300
292
|
should_quit: bool,
|
|
301
293
|
}
|
|
302
294
|
|
|
@@ -342,6 +334,9 @@ impl PracticodeApp {
|
|
|
342
334
|
model_message: None,
|
|
343
335
|
update_check: None,
|
|
344
336
|
update_notice: None,
|
|
337
|
+
code_area: Rect::default(),
|
|
338
|
+
output_area: Rect::default(),
|
|
339
|
+
command_area: Rect::default(),
|
|
345
340
|
should_quit: false,
|
|
346
341
|
};
|
|
347
342
|
app.load_code_editor()?;
|
|
@@ -357,11 +352,12 @@ impl PracticodeApp {
|
|
|
357
352
|
self.check_update();
|
|
358
353
|
self.start_model_check();
|
|
359
354
|
self.check_models();
|
|
360
|
-
if event::poll(Duration::from_millis(100))?
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
+
}
|
|
365
361
|
}
|
|
366
362
|
if !self.busy_label.is_empty() {
|
|
367
363
|
self.busy_frame = (self.busy_frame + 1) % 16;
|
|
@@ -395,6 +391,16 @@ impl PracticodeApp {
|
|
|
395
391
|
self.handle_key(key)
|
|
396
392
|
}
|
|
397
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
|
+
|
|
398
404
|
pub fn busy_label(&self) -> &str {
|
|
399
405
|
&self.busy_label
|
|
400
406
|
}
|
|
@@ -448,6 +454,9 @@ impl PracticodeApp {
|
|
|
448
454
|
.direction(Direction::Horizontal)
|
|
449
455
|
.constraints([Constraint::Percentage(58), Constraint::Percentage(42)])
|
|
450
456
|
.split(vertical[0]);
|
|
457
|
+
self.code_area = body[1];
|
|
458
|
+
self.output_area = body[1];
|
|
459
|
+
self.command_area = vertical[2];
|
|
451
460
|
|
|
452
461
|
let problem = Paragraph::new(self.problem_text())
|
|
453
462
|
.block(Self::block(
|
|
@@ -459,13 +468,7 @@ impl PracticodeApp {
|
|
|
459
468
|
frame.render_widget(problem, body[0]);
|
|
460
469
|
|
|
461
470
|
if self.show_output {
|
|
462
|
-
let text =
|
|
463
|
-
format!("{}{}", self.busy_body, self.busy_dots())
|
|
464
|
-
} else if self.output_is_markdown {
|
|
465
|
-
render_markdown_plain(&self.output)
|
|
466
|
-
} else {
|
|
467
|
-
self.output.clone()
|
|
468
|
-
};
|
|
471
|
+
let text = self.output_text();
|
|
469
472
|
let output = Paragraph::new(text)
|
|
470
473
|
.block(Self::block(
|
|
471
474
|
ui_text(&self.state.settings.ui_language, "output"),
|
|
@@ -518,6 +521,79 @@ impl PracticodeApp {
|
|
|
518
521
|
self.set_terminal_cursor(frame, body[1], vertical[2]);
|
|
519
522
|
}
|
|
520
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
|
+
|
|
521
597
|
fn problem_text(&self) -> Text<'static> {
|
|
522
598
|
let lang = normalize_ui_language(&self.state.settings.ui_language);
|
|
523
599
|
let light = self.state.settings.theme == "light";
|
|
@@ -732,6 +808,19 @@ impl PracticodeApp {
|
|
|
732
808
|
}
|
|
733
809
|
}
|
|
734
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
|
+
|
|
735
824
|
fn handle_command_key(&mut self, key: KeyEvent) -> Result<()> {
|
|
736
825
|
match key.code {
|
|
737
826
|
KeyCode::Esc => {
|
|
@@ -887,7 +976,7 @@ impl PracticodeApp {
|
|
|
887
976
|
}
|
|
888
977
|
match command {
|
|
889
978
|
"run" | "r" => self.action_run()?,
|
|
890
|
-
"edit" | "e" => self.action_edit()?,
|
|
979
|
+
"code" | "edit" | "e" => self.action_edit()?,
|
|
891
980
|
"next" | "n" => self.action_next(arg)?,
|
|
892
981
|
"prev" | "previous" | "p" => self.action_previous()?,
|
|
893
982
|
"giveup" | "give" | "g" => self.action_give_up()?,
|
|
@@ -900,12 +989,12 @@ impl PracticodeApp {
|
|
|
900
989
|
"theme" if arg.is_empty() => self.action_toggle_theme()?,
|
|
901
990
|
"theme" if THEMES.contains(&arg) => self.set_theme(arg)?,
|
|
902
991
|
"source" | "next-source" if arg.is_empty() => {
|
|
903
|
-
self.write_text_output(&
|
|
992
|
+
self.write_text_output(&self.next_source_help());
|
|
904
993
|
}
|
|
905
994
|
"source" | "next-source" if matches!(arg, "bank" | "local" | "ai") => {
|
|
906
995
|
self.state.settings.next_source = normalize_next_source(arg);
|
|
907
996
|
save_state(&self.root, &self.state)?;
|
|
908
|
-
self.write_text_output(&
|
|
997
|
+
self.write_text_output(&self.next_source_help());
|
|
909
998
|
}
|
|
910
999
|
"ai-next-command" if !arg.is_empty() => {
|
|
911
1000
|
self.state.settings.ai_next_command = arg.to_string();
|
|
@@ -982,9 +1071,9 @@ impl PracticodeApp {
|
|
|
982
1071
|
result.total_cases
|
|
983
1072
|
);
|
|
984
1073
|
let next_step = if result.passed {
|
|
985
|
-
|
|
1074
|
+
ui_text(&self.state.settings.ui_language, "run_pass_next")
|
|
986
1075
|
} else {
|
|
987
|
-
|
|
1076
|
+
ui_text(&self.state.settings.ui_language, "run_fail_next")
|
|
988
1077
|
};
|
|
989
1078
|
self.write_text_output(&format!("{headline}\n{}\n\n{next_step}", result.output));
|
|
990
1079
|
Ok(())
|
|
@@ -1655,11 +1744,11 @@ impl PracticodeApp {
|
|
|
1655
1744
|
)
|
|
1656
1745
|
}
|
|
1657
1746
|
|
|
1658
|
-
fn
|
|
1747
|
+
fn next_source_help(&self) -> String {
|
|
1659
1748
|
if self.state.settings.next_source == "ai" {
|
|
1660
|
-
"
|
|
1749
|
+
"Next behavior: plain /next asks AI every time. Use /source local to use local problems first.".to_string()
|
|
1661
1750
|
} else {
|
|
1662
|
-
"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()
|
|
1663
1752
|
}
|
|
1664
1753
|
}
|
|
1665
1754
|
|