practicode 0.1.1 → 0.1.2
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 +6 -3
- package/assets/i18n/en.json +50 -0
- package/assets/i18n/es.json +50 -0
- package/assets/i18n/ja.json +50 -0
- package/assets/i18n/ko.json +50 -0
- package/assets/i18n/zh.json +50 -0
- package/docs/CONTRIBUTING.md +7 -2
- package/package.json +1 -1
- package/src/ai.rs +38 -6
- package/src/core.rs +61 -216
- package/src/i18n.rs +45 -0
- package/src/lib.rs +2 -0
- package/src/tui.rs +111 -35
- package/src/update.rs +45 -0
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
package/README.md
CHANGED
|
@@ -70,7 +70,7 @@ Submissions are saved as you type under `submissions/<problem-id>/solution.<ext>
|
|
|
70
70
|
| `/open 2` | Open by number, id, or slug |
|
|
71
71
|
| `/giveup` | Show the reference answer |
|
|
72
72
|
| `/ai hint` | Ask the selected AI about the current problem and submission |
|
|
73
|
-
| `/provider codex` | Set AI provider
|
|
73
|
+
| `/provider codex` | Set AI provider and show local CLI/daemon status |
|
|
74
74
|
| `/model auto` | Set the model for `/ai` and AI-backed `/next` |
|
|
75
75
|
| `/note prefer hashmap practice` | Append a standing note for future problem generation |
|
|
76
76
|
| `/notes` | Show your local next-problem notes |
|
|
@@ -78,6 +78,7 @@ Submissions are saved as you type under `submissions/<problem-id>/solution.<ext>
|
|
|
78
78
|
| `/ui en` | Set UI language: `en`, `ko`, `ja`, `zh`, `es` |
|
|
79
79
|
| `/theme dark` | Set theme: `dark` or `light` |
|
|
80
80
|
| `/source ai` | Prefer AI for next-problem generation |
|
|
81
|
+
| `/update` | Show update instructions when a newer version is available |
|
|
81
82
|
| `/exit` | Quit |
|
|
82
83
|
|
|
83
84
|
The default UI language is English. Switch it any time with `/ui ko`, `/ui ja`, `/ui zh`, or `/ui es`.
|
|
@@ -107,11 +108,11 @@ Claude Code is also supported:
|
|
|
107
108
|
/source ai
|
|
108
109
|
```
|
|
109
110
|
|
|
110
|
-
Generated
|
|
111
|
+
Generated problems and submissions stay local:
|
|
111
112
|
|
|
112
113
|
| Path | Purpose |
|
|
113
114
|
| --- | --- |
|
|
114
|
-
| `.practicode/problem_bank.json` | Local/custom/generated
|
|
115
|
+
| `.practicode/problem_bank.json` | Local/custom/generated problems |
|
|
115
116
|
| `.practicode/problem_notes.md` | Optional personal problem-generation notes |
|
|
116
117
|
| `.practicode/problem-state.json` | Current problem, history, settings |
|
|
117
118
|
| `problems/` | Generated problem markdown/index files |
|
|
@@ -121,6 +122,8 @@ Those paths are ignored by git, so your practice history stays yours.
|
|
|
121
122
|
|
|
122
123
|
## Update
|
|
123
124
|
|
|
125
|
+
The app checks for newer npm releases in the background and shows `/update` in the status line when one is available. Disable that check with `PRACTICODE_NO_UPDATE_CHECK=1`.
|
|
126
|
+
|
|
124
127
|
```bash
|
|
125
128
|
npm update -g practicode
|
|
126
129
|
cargo install --force practicode
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"problem": "Problem",
|
|
3
|
+
"output": "Output",
|
|
4
|
+
"command": "Command",
|
|
5
|
+
"commands": "Commands",
|
|
6
|
+
"difficulty": "Difficulty",
|
|
7
|
+
"topics": "Topics",
|
|
8
|
+
"input": "Input",
|
|
9
|
+
"examples": "Examples",
|
|
10
|
+
"example": "Example",
|
|
11
|
+
"answer": "Answer",
|
|
12
|
+
"source": "source",
|
|
13
|
+
"update": "update",
|
|
14
|
+
"command_placeholder": "Type /, move with up/down, Enter runs",
|
|
15
|
+
"palette_hint": "up/down select | Enter run | Esc cancel",
|
|
16
|
+
"hint_command": "Enter submit | Esc cancel",
|
|
17
|
+
"hint_list": "up/down move | Enter open | Esc close",
|
|
18
|
+
"hint_output": "Esc code | / command | ? help",
|
|
19
|
+
"hint_code": "Esc then / command",
|
|
20
|
+
"hint_idle": "/ command | ? help",
|
|
21
|
+
"help_title": "Help",
|
|
22
|
+
"daily_loop": "Daily loop",
|
|
23
|
+
"keys": "Keys",
|
|
24
|
+
"debug_prints": "Debug prints",
|
|
25
|
+
"cmd_run": "Judge the current submission",
|
|
26
|
+
"cmd_edit": "Return to the code editor",
|
|
27
|
+
"cmd_next": "Open the next problem",
|
|
28
|
+
"cmd_prev": "Open the previous problem",
|
|
29
|
+
"cmd_list": "Browse problems",
|
|
30
|
+
"cmd_open": "Open by number, id, or slug",
|
|
31
|
+
"cmd_giveup": "Show the reference answer",
|
|
32
|
+
"cmd_ai": "Ask AI about the current problem and code",
|
|
33
|
+
"cmd_provider": "Set AI provider",
|
|
34
|
+
"cmd_model": "Set AI model",
|
|
35
|
+
"cmd_note": "Add a next-problem note",
|
|
36
|
+
"cmd_notes": "Show saved notes",
|
|
37
|
+
"cmd_lang": "Set code language",
|
|
38
|
+
"cmd_ui": "Set UI language",
|
|
39
|
+
"cmd_theme": "Set theme",
|
|
40
|
+
"cmd_source": "Set next-problem source",
|
|
41
|
+
"cmd_update": "Show update instructions",
|
|
42
|
+
"cmd_help": "Open help",
|
|
43
|
+
"cmd_exit": "Quit",
|
|
44
|
+
"update_available": "Update available",
|
|
45
|
+
"update_none": "practicode is up to date.",
|
|
46
|
+
"update_check_disabled": "Update check is disabled.",
|
|
47
|
+
"update_check_failed": "Could not check for updates.",
|
|
48
|
+
"generating_next": "Generating next problem",
|
|
49
|
+
"already_busy": "Already busy."
|
|
50
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"problem": "Problema",
|
|
3
|
+
"output": "Salida",
|
|
4
|
+
"command": "Comando",
|
|
5
|
+
"commands": "Comandos",
|
|
6
|
+
"difficulty": "Dificultad",
|
|
7
|
+
"topics": "Temas",
|
|
8
|
+
"input": "Entrada",
|
|
9
|
+
"examples": "Ejemplos",
|
|
10
|
+
"example": "Ejemplo",
|
|
11
|
+
"answer": "Respuesta",
|
|
12
|
+
"source": "fuente",
|
|
13
|
+
"update": "actualizacion",
|
|
14
|
+
"command_placeholder": "Escribe /, elige con arriba/abajo, Enter ejecuta",
|
|
15
|
+
"palette_hint": "arriba/abajo elegir | Enter ejecutar | Esc cancelar",
|
|
16
|
+
"hint_command": "Enter ejecutar | Esc cancelar",
|
|
17
|
+
"hint_list": "arriba/abajo mover | Enter abrir | Esc cerrar",
|
|
18
|
+
"hint_output": "Esc codigo | / comando | ? ayuda",
|
|
19
|
+
"hint_code": "Esc y luego / comando",
|
|
20
|
+
"hint_idle": "/ comando | ? ayuda",
|
|
21
|
+
"help_title": "Ayuda",
|
|
22
|
+
"daily_loop": "Flujo diario",
|
|
23
|
+
"keys": "Teclas",
|
|
24
|
+
"debug_prints": "Salida de depuracion",
|
|
25
|
+
"cmd_run": "Evalua la solucion actual",
|
|
26
|
+
"cmd_edit": "Volver al editor de codigo",
|
|
27
|
+
"cmd_next": "Abrir el siguiente problema",
|
|
28
|
+
"cmd_prev": "Abrir el problema anterior",
|
|
29
|
+
"cmd_list": "Abrir la lista de problemas",
|
|
30
|
+
"cmd_open": "Abrir por numero, id o slug",
|
|
31
|
+
"cmd_giveup": "Mostrar la respuesta de referencia",
|
|
32
|
+
"cmd_ai": "Preguntar a AI sobre el problema y codigo actuales",
|
|
33
|
+
"cmd_provider": "Configurar AI provider",
|
|
34
|
+
"cmd_model": "Configurar AI model",
|
|
35
|
+
"cmd_note": "Agregar nota para generar problemas",
|
|
36
|
+
"cmd_notes": "Ver notas guardadas",
|
|
37
|
+
"cmd_lang": "Configurar lenguaje de codigo",
|
|
38
|
+
"cmd_ui": "Configurar idioma de UI",
|
|
39
|
+
"cmd_theme": "Configurar tema",
|
|
40
|
+
"cmd_source": "Configurar fuente del siguiente problema",
|
|
41
|
+
"cmd_update": "Mostrar instrucciones de actualizacion",
|
|
42
|
+
"cmd_help": "Abrir ayuda",
|
|
43
|
+
"cmd_exit": "Salir",
|
|
44
|
+
"update_available": "Hay una nueva version",
|
|
45
|
+
"update_none": "practicode esta actualizado.",
|
|
46
|
+
"update_check_disabled": "La comprobacion de actualizaciones esta desactivada.",
|
|
47
|
+
"update_check_failed": "No se pudo comprobar actualizaciones.",
|
|
48
|
+
"generating_next": "Generando el siguiente problema",
|
|
49
|
+
"already_busy": "Ya hay una tarea en curso."
|
|
50
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"problem": "問題",
|
|
3
|
+
"output": "出力",
|
|
4
|
+
"command": "コマンド",
|
|
5
|
+
"commands": "コマンド",
|
|
6
|
+
"difficulty": "難易度",
|
|
7
|
+
"topics": "トピック",
|
|
8
|
+
"input": "入力",
|
|
9
|
+
"examples": "例",
|
|
10
|
+
"example": "例",
|
|
11
|
+
"answer": "解答",
|
|
12
|
+
"source": "source",
|
|
13
|
+
"update": "update",
|
|
14
|
+
"command_placeholder": "/ を入力し上下で選択、Enter で実行",
|
|
15
|
+
"palette_hint": "上下 選択 | Enter 実行 | Esc キャンセル",
|
|
16
|
+
"hint_command": "Enter 実行 | Esc キャンセル",
|
|
17
|
+
"hint_list": "上下 移動 | Enter 開く | Esc 閉じる",
|
|
18
|
+
"hint_output": "Esc コード | / コマンド | ? ヘルプ",
|
|
19
|
+
"hint_code": "Esc の後 / コマンド",
|
|
20
|
+
"hint_idle": "/ コマンド | ? ヘルプ",
|
|
21
|
+
"help_title": "ヘルプ",
|
|
22
|
+
"daily_loop": "基本フロー",
|
|
23
|
+
"keys": "キー",
|
|
24
|
+
"debug_prints": "デバッグ出力",
|
|
25
|
+
"cmd_run": "現在の提出を判定",
|
|
26
|
+
"cmd_edit": "コードエディタに戻る",
|
|
27
|
+
"cmd_next": "次の問題を開く",
|
|
28
|
+
"cmd_prev": "前の問題を開く",
|
|
29
|
+
"cmd_list": "問題一覧を開く",
|
|
30
|
+
"cmd_open": "番号、id、slug で問題を開く",
|
|
31
|
+
"cmd_giveup": "解答を見る",
|
|
32
|
+
"cmd_ai": "現在の問題とコードについて AI に質問",
|
|
33
|
+
"cmd_provider": "AI provider を設定",
|
|
34
|
+
"cmd_model": "AI model を設定",
|
|
35
|
+
"cmd_note": "次の問題生成メモを追加",
|
|
36
|
+
"cmd_notes": "保存済みメモを見る",
|
|
37
|
+
"cmd_lang": "コード言語を設定",
|
|
38
|
+
"cmd_ui": "UI 言語を設定",
|
|
39
|
+
"cmd_theme": "テーマを設定",
|
|
40
|
+
"cmd_source": "次の問題の取得元を設定",
|
|
41
|
+
"cmd_update": "更新手順を表示",
|
|
42
|
+
"cmd_help": "ヘルプを開く",
|
|
43
|
+
"cmd_exit": "終了",
|
|
44
|
+
"update_available": "新しいバージョンがあります",
|
|
45
|
+
"update_none": "practicode は最新です。",
|
|
46
|
+
"update_check_disabled": "更新確認は無効です。",
|
|
47
|
+
"update_check_failed": "更新を確認できませんでした。",
|
|
48
|
+
"generating_next": "次の問題を生成中",
|
|
49
|
+
"already_busy": "すでに処理中です。"
|
|
50
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"problem": "문제",
|
|
3
|
+
"output": "출력",
|
|
4
|
+
"command": "명령",
|
|
5
|
+
"commands": "명령",
|
|
6
|
+
"difficulty": "난이도",
|
|
7
|
+
"topics": "주제",
|
|
8
|
+
"input": "입력",
|
|
9
|
+
"examples": "예시",
|
|
10
|
+
"example": "예시",
|
|
11
|
+
"answer": "정답",
|
|
12
|
+
"source": "출처",
|
|
13
|
+
"update": "업데이트",
|
|
14
|
+
"command_placeholder": "/ 입력 후 위/아래로 선택, Enter 실행",
|
|
15
|
+
"palette_hint": "위/아래 선택 | Enter 실행 | Esc 취소",
|
|
16
|
+
"hint_command": "Enter 실행 | Esc 취소",
|
|
17
|
+
"hint_list": "위/아래 이동 | Enter 열기 | Esc 닫기",
|
|
18
|
+
"hint_output": "Esc 코드 | / 명령 | ? 도움말",
|
|
19
|
+
"hint_code": "Esc 후 / 명령",
|
|
20
|
+
"hint_idle": "/ 명령 | ? 도움말",
|
|
21
|
+
"help_title": "도움말",
|
|
22
|
+
"daily_loop": "기본 흐름",
|
|
23
|
+
"keys": "키",
|
|
24
|
+
"debug_prints": "디버그 출력",
|
|
25
|
+
"cmd_run": "현재 제출을 채점",
|
|
26
|
+
"cmd_edit": "코드 편집기로 돌아가기",
|
|
27
|
+
"cmd_next": "다음 문제 열기",
|
|
28
|
+
"cmd_prev": "이전 문제 열기",
|
|
29
|
+
"cmd_list": "문제 목록 열기",
|
|
30
|
+
"cmd_open": "번호, id, slug로 문제 열기",
|
|
31
|
+
"cmd_giveup": "정답 보기",
|
|
32
|
+
"cmd_ai": "현재 문제와 코드에 대해 AI에게 질문",
|
|
33
|
+
"cmd_provider": "AI provider 설정",
|
|
34
|
+
"cmd_model": "AI model 설정",
|
|
35
|
+
"cmd_note": "다음 문제 생성 메모 추가",
|
|
36
|
+
"cmd_notes": "저장된 메모 보기",
|
|
37
|
+
"cmd_lang": "코드 언어 설정",
|
|
38
|
+
"cmd_ui": "UI 언어 설정",
|
|
39
|
+
"cmd_theme": "테마 설정",
|
|
40
|
+
"cmd_source": "다음 문제 출처 설정",
|
|
41
|
+
"cmd_update": "업데이트 안내 보기",
|
|
42
|
+
"cmd_help": "도움말 열기",
|
|
43
|
+
"cmd_exit": "종료",
|
|
44
|
+
"update_available": "새 버전 사용 가능",
|
|
45
|
+
"update_none": "practicode가 최신 버전입니다.",
|
|
46
|
+
"update_check_disabled": "업데이트 확인이 꺼져 있습니다.",
|
|
47
|
+
"update_check_failed": "업데이트를 확인할 수 없습니다.",
|
|
48
|
+
"generating_next": "다음 문제 생성 중",
|
|
49
|
+
"already_busy": "이미 작업 중입니다."
|
|
50
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"problem": "题目",
|
|
3
|
+
"output": "输出",
|
|
4
|
+
"command": "命令",
|
|
5
|
+
"commands": "命令",
|
|
6
|
+
"difficulty": "难度",
|
|
7
|
+
"topics": "主题",
|
|
8
|
+
"input": "输入",
|
|
9
|
+
"examples": "示例",
|
|
10
|
+
"example": "示例",
|
|
11
|
+
"answer": "答案",
|
|
12
|
+
"source": "来源",
|
|
13
|
+
"update": "更新",
|
|
14
|
+
"command_placeholder": "输入 / 后用上下键选择,Enter 执行",
|
|
15
|
+
"palette_hint": "上下 选择 | Enter 执行 | Esc 取消",
|
|
16
|
+
"hint_command": "Enter 执行 | Esc 取消",
|
|
17
|
+
"hint_list": "上下 移动 | Enter 打开 | Esc 关闭",
|
|
18
|
+
"hint_output": "Esc 代码 | / 命令 | ? 帮助",
|
|
19
|
+
"hint_code": "Esc 后输入 / 命令",
|
|
20
|
+
"hint_idle": "/ 命令 | ? 帮助",
|
|
21
|
+
"help_title": "帮助",
|
|
22
|
+
"daily_loop": "日常流程",
|
|
23
|
+
"keys": "按键",
|
|
24
|
+
"debug_prints": "调试输出",
|
|
25
|
+
"cmd_run": "评测当前提交",
|
|
26
|
+
"cmd_edit": "回到代码编辑器",
|
|
27
|
+
"cmd_next": "打开下一题",
|
|
28
|
+
"cmd_prev": "打开上一题",
|
|
29
|
+
"cmd_list": "打开题目列表",
|
|
30
|
+
"cmd_open": "按编号、id 或 slug 打开题目",
|
|
31
|
+
"cmd_giveup": "显示参考答案",
|
|
32
|
+
"cmd_ai": "向 AI 询问当前题目和代码",
|
|
33
|
+
"cmd_provider": "设置 AI provider",
|
|
34
|
+
"cmd_model": "设置 AI model",
|
|
35
|
+
"cmd_note": "添加下次出题备注",
|
|
36
|
+
"cmd_notes": "查看保存的备注",
|
|
37
|
+
"cmd_lang": "设置代码语言",
|
|
38
|
+
"cmd_ui": "设置 UI 语言",
|
|
39
|
+
"cmd_theme": "设置主题",
|
|
40
|
+
"cmd_source": "设置下一题来源",
|
|
41
|
+
"cmd_update": "显示更新说明",
|
|
42
|
+
"cmd_help": "打开帮助",
|
|
43
|
+
"cmd_exit": "退出",
|
|
44
|
+
"update_available": "有新版本可用",
|
|
45
|
+
"update_none": "practicode 已是最新版本。",
|
|
46
|
+
"update_check_disabled": "更新检查已禁用。",
|
|
47
|
+
"update_check_failed": "无法检查更新。",
|
|
48
|
+
"generating_next": "正在生成下一题",
|
|
49
|
+
"already_busy": "已有任务正在运行。"
|
|
50
|
+
}
|
package/docs/CONTRIBUTING.md
CHANGED
|
@@ -27,7 +27,8 @@ The source is split by boring responsibility:
|
|
|
27
27
|
|
|
28
28
|
| Path | Role |
|
|
29
29
|
| --- | --- |
|
|
30
|
-
| `src/core.rs` | Problem
|
|
30
|
+
| `src/core.rs` | Problem storage, state, rendering, judging |
|
|
31
|
+
| `src/i18n.rs` | Loads UI strings from `assets/i18n/*.json` |
|
|
31
32
|
| `src/tui.rs` | Ratatui app, editor, command parser |
|
|
32
33
|
| `src/ai.rs` | Codex/Claude command integration and notes |
|
|
33
34
|
| `src/text.rs` | UTF-8 cursor math and Hangul composition |
|
|
@@ -42,7 +43,7 @@ Local generated data stays ignored by git:
|
|
|
42
43
|
|
|
43
44
|
| Path | Purpose |
|
|
44
45
|
| --- | --- |
|
|
45
|
-
| `.practicode/problem_bank.json` | Local/custom/generated
|
|
46
|
+
| `.practicode/problem_bank.json` | Local/custom/generated problems |
|
|
46
47
|
| `.practicode/problem_notes.md` | Personal problem-generation notes |
|
|
47
48
|
| `.practicode/problem-state.json` | Current problem, history, settings |
|
|
48
49
|
| `problems/` | Generated problem markdown/index files |
|
|
@@ -64,6 +65,10 @@ Keep the root [README](../README.md) focused on users. Put contributor workflow,
|
|
|
64
65
|
|
|
65
66
|
Use relative links for repo-local docs and assets. The terminal screenshot is stored at [assets/practicode-terminal.svg](../assets/practicode-terminal.svg).
|
|
66
67
|
|
|
68
|
+
## Localization
|
|
69
|
+
|
|
70
|
+
UI strings live in [assets/i18n](../assets/i18n). Keep English complete first; other locales can be partial because the runtime falls back per key to English.
|
|
71
|
+
|
|
67
72
|
## UX And Documentation References
|
|
68
73
|
|
|
69
74
|
- WAI-ARIA combobox keyboard interaction: https://www.w3.org/WAI/ARIA/apg/patterns/combobox/
|
package/package.json
CHANGED
package/src/ai.rs
CHANGED
|
@@ -3,13 +3,14 @@ use crate::{
|
|
|
3
3
|
AppState, PROBLEM_NOTES_PATH, Problem, Settings, ensure_submission, normalize_ai_provider,
|
|
4
4
|
render_problem,
|
|
5
5
|
},
|
|
6
|
-
process::{run_capture, sh_quote, shell_process, unique_temp_path},
|
|
6
|
+
process::{run_capture, sh_quote, shell_process, unique_temp_path, which},
|
|
7
7
|
};
|
|
8
8
|
use anyhow::Result;
|
|
9
9
|
use std::{
|
|
10
|
+
env,
|
|
10
11
|
fs::{self, OpenOptions},
|
|
11
12
|
io::Write,
|
|
12
|
-
path::Path,
|
|
13
|
+
path::{Path, PathBuf},
|
|
13
14
|
process::Command,
|
|
14
15
|
time::Duration,
|
|
15
16
|
};
|
|
@@ -39,7 +40,7 @@ pub fn run_ai_prompt(root: &Path, problem: &Problem, settings: &Settings, prompt
|
|
|
39
40
|
|
|
40
41
|
pub fn run_ai_next(root: &Path, state: &AppState, force: bool, request: &str) -> String {
|
|
41
42
|
if state.settings.next_source != "ai" && !force {
|
|
42
|
-
return "AI next is disabled; using local
|
|
43
|
+
return "AI next is disabled; using local problems.".to_string();
|
|
43
44
|
}
|
|
44
45
|
let provider = normalize_ai_provider(&state.settings.ai_provider);
|
|
45
46
|
let command = if state.settings.next_ai_command().trim().is_empty() {
|
|
@@ -78,9 +79,32 @@ pub fn default_ai_next_command(root: &Path, settings: &Settings, request: &str)
|
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
pub fn provider_status(provider: &str) -> String {
|
|
83
|
+
match normalize_ai_provider(provider).as_str() {
|
|
84
|
+
"claude" => {
|
|
85
|
+
if which("claude").is_some() {
|
|
86
|
+
"Claude CLI found.".to_string()
|
|
87
|
+
} else {
|
|
88
|
+
"Claude CLI not found. Install Claude Code or choose /provider codex.".to_string()
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
_ => {
|
|
92
|
+
if which("codex").is_none() {
|
|
93
|
+
return "Codex CLI not found. Install Codex CLI or choose /provider claude."
|
|
94
|
+
.to_string();
|
|
95
|
+
}
|
|
96
|
+
if codex_daemon_path().is_some_and(|path| path.exists()) {
|
|
97
|
+
"Codex CLI found. App-server daemon is available.".to_string()
|
|
98
|
+
} else {
|
|
99
|
+
"Codex CLI found. App-server daemon is not available; practicode will use codex exec directly.".to_string()
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
81
105
|
pub fn default_ai_next_prompt(request: &str) -> String {
|
|
82
106
|
format!(
|
|
83
|
-
"Read AGENTS.md, docs/problem-authoring-notes.md if present, .practicode/problem_notes.md if present, problems/INDEX.md if present, .practicode/problem_bank.json if present, and .practicode/problem-state.json.
|
|
107
|
+
"Read AGENTS.md, docs/problem-authoring-notes.md if present, .practicode/problem_notes.md if present, problems/INDEX.md if present, .practicode/problem_bank.json if present, and .practicode/problem-state.json. Create exactly one new non-duplicate coding practice problem. The built-in 001-hello-world already exists, so do not duplicate it. User request: {}. Make the smallest valid edits: update .practicode/problem_bank.json, one problem directory, problems/INDEX.md, and .practicode/problem-state.json. Do not include the answer in the problem statement.",
|
|
84
108
|
if request.is_empty() {
|
|
85
109
|
"(none)"
|
|
86
110
|
} else {
|
|
@@ -177,9 +201,9 @@ fn run_claude_prompt(root: &Path, settings: &Settings, prompt: &str) -> String {
|
|
|
177
201
|
}
|
|
178
202
|
|
|
179
203
|
fn default_codex_next_command(root: &Path, settings: &Settings, request: &str) -> String {
|
|
180
|
-
let start = "codex app-server daemon start >/dev/null 2>&1 || true";
|
|
204
|
+
let start = "if [ -x \"$HOME/.codex/packages/standalone/current/codex\" ]; then codex app-server daemon start >/dev/null 2>&1 || true; fi";
|
|
181
205
|
let mut exec = format!(
|
|
182
|
-
"codex exec --cd {} --sandbox workspace-write",
|
|
206
|
+
"codex exec --ephemeral --cd {} --sandbox workspace-write",
|
|
183
207
|
sh_quote(&root.display().to_string())
|
|
184
208
|
);
|
|
185
209
|
if let Some(model) = settings.model_arg() {
|
|
@@ -190,6 +214,14 @@ fn default_codex_next_command(root: &Path, settings: &Settings, request: &str) -
|
|
|
190
214
|
format!("{start}; {exec}")
|
|
191
215
|
}
|
|
192
216
|
|
|
217
|
+
fn codex_daemon_path() -> Option<PathBuf> {
|
|
218
|
+
env::var_os("HOME").map(|home| {
|
|
219
|
+
PathBuf::from(home)
|
|
220
|
+
.join(".codex/packages/standalone/current")
|
|
221
|
+
.join(if cfg!(windows) { "codex.exe" } else { "codex" })
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
|
|
193
225
|
fn default_claude_next_command(root: &Path, settings: &Settings, request: &str) -> String {
|
|
194
226
|
let mut claude = "claude --permission-mode acceptEdits".to_string();
|
|
195
227
|
if let Some(model) = settings.model_arg() {
|
package/src/core.rs
CHANGED
|
@@ -10,7 +10,7 @@ use std::{
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
pub const LANGUAGES: &[&str] = &["python", "ts", "java", "rust"];
|
|
13
|
-
pub
|
|
13
|
+
pub use crate::i18n::{UI_LANGUAGES, normalize_ui_language, ui_text};
|
|
14
14
|
pub const THEMES: &[&str] = &["dark", "light"];
|
|
15
15
|
pub const AI_PROVIDERS: &[&str] = &["codex", "claude"];
|
|
16
16
|
pub const BANK_PATH: &str = ".practicode/problem_bank.json";
|
|
@@ -383,20 +383,6 @@ pub fn normalize_language(language: &str) -> String {
|
|
|
383
383
|
}
|
|
384
384
|
}
|
|
385
385
|
|
|
386
|
-
pub fn normalize_ui_language(language: &str) -> String {
|
|
387
|
-
let lower = language.trim().to_lowercase();
|
|
388
|
-
let short = lower
|
|
389
|
-
.split(['-', '_'])
|
|
390
|
-
.next()
|
|
391
|
-
.filter(|value| !value.is_empty())
|
|
392
|
-
.unwrap_or("en");
|
|
393
|
-
if UI_LANGUAGES.contains(&short) {
|
|
394
|
-
short.to_string()
|
|
395
|
-
} else {
|
|
396
|
-
default_ui_language()
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
386
|
pub fn normalize_next_source(source: &str) -> String {
|
|
401
387
|
if source == "ai" {
|
|
402
388
|
"ai".to_string()
|
|
@@ -423,207 +409,6 @@ pub fn localized(map: &HashMap<String, String>, lang: &str) -> String {
|
|
|
423
409
|
.unwrap_or_default()
|
|
424
410
|
}
|
|
425
411
|
|
|
426
|
-
pub fn ui_text(lang: &str, key: &str) -> &'static str {
|
|
427
|
-
let lang = normalize_ui_language(lang);
|
|
428
|
-
match (lang.as_str(), key) {
|
|
429
|
-
("ko", "problem") => "문제",
|
|
430
|
-
("ko", "output") => "출력",
|
|
431
|
-
("ko", "command") => "명령",
|
|
432
|
-
("ko", "commands") => "명령",
|
|
433
|
-
("ko", "difficulty") => "난이도",
|
|
434
|
-
("ko", "topics") => "주제",
|
|
435
|
-
("ko", "input") => "입력",
|
|
436
|
-
("ko", "examples") => "예시",
|
|
437
|
-
("ko", "example") => "예시",
|
|
438
|
-
("ko", "command_placeholder") => "/ 입력 후 ↑/↓로 선택, Enter 실행",
|
|
439
|
-
("ko", "palette_hint") => "↑/↓ 선택 | Enter 실행 | Esc 취소",
|
|
440
|
-
("ko", "hint_command") => "Enter 실행 | Esc 취소",
|
|
441
|
-
("ko", "hint_list") => "↑/↓ 이동 | Enter 열기 | Esc 닫기",
|
|
442
|
-
("ko", "hint_output") => "Esc 코드 | / 명령 | ? 도움말",
|
|
443
|
-
("ko", "hint_code") => "Esc 후 / 명령",
|
|
444
|
-
("ko", "hint_idle") => "/ 명령 | ? 도움말",
|
|
445
|
-
("ko", "help_title") => "도움말",
|
|
446
|
-
("ko", "daily_loop") => "기본 흐름",
|
|
447
|
-
("ko", "keys") => "키",
|
|
448
|
-
("ko", "debug_prints") => "디버그 출력",
|
|
449
|
-
("ko", "cmd_run") => "현재 제출을 채점",
|
|
450
|
-
("ko", "cmd_edit") => "코드 편집기로 돌아가기",
|
|
451
|
-
("ko", "cmd_next") => "다음 문제 열기",
|
|
452
|
-
("ko", "cmd_prev") => "이전 문제 열기",
|
|
453
|
-
("ko", "cmd_list") => "문제 목록 열기",
|
|
454
|
-
("ko", "cmd_open") => "번호, id, slug로 문제 열기",
|
|
455
|
-
("ko", "cmd_giveup") => "정답 보기",
|
|
456
|
-
("ko", "cmd_ai") => "현재 문제와 코드에 대해 AI에게 질문",
|
|
457
|
-
("ko", "cmd_provider") => "AI provider 설정",
|
|
458
|
-
("ko", "cmd_model") => "AI model 설정",
|
|
459
|
-
("ko", "cmd_note") => "다음 문제 생성 메모 추가",
|
|
460
|
-
("ko", "cmd_notes") => "저장된 메모 보기",
|
|
461
|
-
("ko", "cmd_lang") => "코드 언어 설정",
|
|
462
|
-
("ko", "cmd_ui") => "UI 언어 설정",
|
|
463
|
-
("ko", "cmd_theme") => "테마 설정",
|
|
464
|
-
("ko", "cmd_source") => "다음 문제 출처 설정",
|
|
465
|
-
("ko", "cmd_exit") => "종료",
|
|
466
|
-
("ko", "cmd_help") => "도움말 열기",
|
|
467
|
-
|
|
468
|
-
("ja", "problem") => "問題",
|
|
469
|
-
("ja", "output") => "出力",
|
|
470
|
-
("ja", "command") => "コマンド",
|
|
471
|
-
("ja", "commands") => "コマンド",
|
|
472
|
-
("ja", "difficulty") => "難易度",
|
|
473
|
-
("ja", "topics") => "トピック",
|
|
474
|
-
("ja", "input") => "入力",
|
|
475
|
-
("ja", "examples") => "例",
|
|
476
|
-
("ja", "example") => "例",
|
|
477
|
-
("ja", "command_placeholder") => "/ を入力し ↑/↓ で選択、Enter で実行",
|
|
478
|
-
("ja", "palette_hint") => "↑/↓ 選択 | Enter 実行 | Esc キャンセル",
|
|
479
|
-
("ja", "hint_command") => "Enter 実行 | Esc キャンセル",
|
|
480
|
-
("ja", "hint_list") => "↑/↓ 移動 | Enter 開く | Esc 閉じる",
|
|
481
|
-
("ja", "hint_output") => "Esc コード | / コマンド | ? ヘルプ",
|
|
482
|
-
("ja", "hint_code") => "Esc の後 / コマンド",
|
|
483
|
-
("ja", "hint_idle") => "/ コマンド | ? ヘルプ",
|
|
484
|
-
("ja", "help_title") => "ヘルプ",
|
|
485
|
-
("ja", "daily_loop") => "基本フロー",
|
|
486
|
-
("ja", "keys") => "キー",
|
|
487
|
-
("ja", "debug_prints") => "デバッグ出力",
|
|
488
|
-
("ja", "cmd_run") => "現在の提出を判定",
|
|
489
|
-
("ja", "cmd_edit") => "コードエディタに戻る",
|
|
490
|
-
("ja", "cmd_next") => "次の問題を開く",
|
|
491
|
-
("ja", "cmd_prev") => "前の問題を開く",
|
|
492
|
-
("ja", "cmd_list") => "問題一覧を開く",
|
|
493
|
-
("ja", "cmd_open") => "番号、id、slug で問題を開く",
|
|
494
|
-
("ja", "cmd_giveup") => "解答を見る",
|
|
495
|
-
("ja", "cmd_ai") => "現在の問題とコードについて AI に質問",
|
|
496
|
-
("ja", "cmd_provider") => "AI provider を設定",
|
|
497
|
-
("ja", "cmd_model") => "AI model を設定",
|
|
498
|
-
("ja", "cmd_note") => "次の問題生成メモを追加",
|
|
499
|
-
("ja", "cmd_notes") => "保存済みメモを見る",
|
|
500
|
-
("ja", "cmd_lang") => "コード言語を設定",
|
|
501
|
-
("ja", "cmd_ui") => "UI 言語を設定",
|
|
502
|
-
("ja", "cmd_theme") => "テーマを設定",
|
|
503
|
-
("ja", "cmd_source") => "次の問題の取得元を設定",
|
|
504
|
-
("ja", "cmd_exit") => "終了",
|
|
505
|
-
("ja", "cmd_help") => "ヘルプを開く",
|
|
506
|
-
|
|
507
|
-
("zh", "problem") => "题目",
|
|
508
|
-
("zh", "output") => "输出",
|
|
509
|
-
("zh", "command") => "命令",
|
|
510
|
-
("zh", "commands") => "命令",
|
|
511
|
-
("zh", "difficulty") => "难度",
|
|
512
|
-
("zh", "topics") => "主题",
|
|
513
|
-
("zh", "input") => "输入",
|
|
514
|
-
("zh", "examples") => "示例",
|
|
515
|
-
("zh", "example") => "示例",
|
|
516
|
-
("zh", "command_placeholder") => "输入 / 后用 ↑/↓ 选择,Enter 执行",
|
|
517
|
-
("zh", "palette_hint") => "↑/↓ 选择 | Enter 执行 | Esc 取消",
|
|
518
|
-
("zh", "hint_command") => "Enter 执行 | Esc 取消",
|
|
519
|
-
("zh", "hint_list") => "↑/↓ 移动 | Enter 打开 | Esc 关闭",
|
|
520
|
-
("zh", "hint_output") => "Esc 代码 | / 命令 | ? 帮助",
|
|
521
|
-
("zh", "hint_code") => "Esc 后输入 / 命令",
|
|
522
|
-
("zh", "hint_idle") => "/ 命令 | ? 帮助",
|
|
523
|
-
("zh", "help_title") => "帮助",
|
|
524
|
-
("zh", "daily_loop") => "日常流程",
|
|
525
|
-
("zh", "keys") => "按键",
|
|
526
|
-
("zh", "debug_prints") => "调试输出",
|
|
527
|
-
("zh", "cmd_run") => "评测当前提交",
|
|
528
|
-
("zh", "cmd_edit") => "回到代码编辑器",
|
|
529
|
-
("zh", "cmd_next") => "打开下一题",
|
|
530
|
-
("zh", "cmd_prev") => "打开上一题",
|
|
531
|
-
("zh", "cmd_list") => "打开题目列表",
|
|
532
|
-
("zh", "cmd_open") => "按编号、id 或 slug 打开题目",
|
|
533
|
-
("zh", "cmd_giveup") => "显示参考答案",
|
|
534
|
-
("zh", "cmd_ai") => "向 AI 询问当前题目和代码",
|
|
535
|
-
("zh", "cmd_provider") => "设置 AI provider",
|
|
536
|
-
("zh", "cmd_model") => "设置 AI model",
|
|
537
|
-
("zh", "cmd_note") => "添加下次出题备注",
|
|
538
|
-
("zh", "cmd_notes") => "查看保存的备注",
|
|
539
|
-
("zh", "cmd_lang") => "设置代码语言",
|
|
540
|
-
("zh", "cmd_ui") => "设置 UI 语言",
|
|
541
|
-
("zh", "cmd_theme") => "设置主题",
|
|
542
|
-
("zh", "cmd_source") => "设置下一题来源",
|
|
543
|
-
("zh", "cmd_exit") => "退出",
|
|
544
|
-
("zh", "cmd_help") => "打开帮助",
|
|
545
|
-
|
|
546
|
-
("es", "problem") => "Problema",
|
|
547
|
-
("es", "output") => "Salida",
|
|
548
|
-
("es", "command") => "Comando",
|
|
549
|
-
("es", "commands") => "Comandos",
|
|
550
|
-
("es", "difficulty") => "Dificultad",
|
|
551
|
-
("es", "topics") => "Temas",
|
|
552
|
-
("es", "input") => "Entrada",
|
|
553
|
-
("es", "examples") => "Ejemplos",
|
|
554
|
-
("es", "example") => "Ejemplo",
|
|
555
|
-
("es", "command_placeholder") => "Escribe /, elige con ↑/↓, Enter ejecuta",
|
|
556
|
-
("es", "palette_hint") => "↑/↓ elegir | Enter ejecutar | Esc cancelar",
|
|
557
|
-
("es", "hint_command") => "Enter ejecutar | Esc cancelar",
|
|
558
|
-
("es", "hint_list") => "↑/↓ mover | Enter abrir | Esc cerrar",
|
|
559
|
-
("es", "hint_output") => "Esc codigo | / comando | ? ayuda",
|
|
560
|
-
("es", "hint_code") => "Esc y luego / comando",
|
|
561
|
-
("es", "hint_idle") => "/ comando | ? ayuda",
|
|
562
|
-
("es", "help_title") => "Ayuda",
|
|
563
|
-
("es", "daily_loop") => "Flujo diario",
|
|
564
|
-
("es", "keys") => "Teclas",
|
|
565
|
-
("es", "debug_prints") => "Salida de depuracion",
|
|
566
|
-
("es", "cmd_run") => "Evalua la solucion actual",
|
|
567
|
-
("es", "cmd_edit") => "Volver al editor de codigo",
|
|
568
|
-
("es", "cmd_next") => "Abrir el siguiente problema",
|
|
569
|
-
("es", "cmd_prev") => "Abrir el problema anterior",
|
|
570
|
-
("es", "cmd_list") => "Abrir la lista de problemas",
|
|
571
|
-
("es", "cmd_open") => "Abrir por numero, id o slug",
|
|
572
|
-
("es", "cmd_giveup") => "Mostrar la respuesta de referencia",
|
|
573
|
-
("es", "cmd_ai") => "Preguntar a AI sobre el problema y codigo actuales",
|
|
574
|
-
("es", "cmd_provider") => "Configurar AI provider",
|
|
575
|
-
("es", "cmd_model") => "Configurar AI model",
|
|
576
|
-
("es", "cmd_note") => "Agregar nota para generar problemas",
|
|
577
|
-
("es", "cmd_notes") => "Ver notas guardadas",
|
|
578
|
-
("es", "cmd_lang") => "Configurar lenguaje de codigo",
|
|
579
|
-
("es", "cmd_ui") => "Configurar idioma de UI",
|
|
580
|
-
("es", "cmd_theme") => "Configurar tema",
|
|
581
|
-
("es", "cmd_source") => "Configurar fuente del siguiente problema",
|
|
582
|
-
("es", "cmd_exit") => "Salir",
|
|
583
|
-
("es", "cmd_help") => "Abrir ayuda",
|
|
584
|
-
|
|
585
|
-
(_, "problem") => "Problem",
|
|
586
|
-
(_, "output") => "Output",
|
|
587
|
-
(_, "command") => "Command",
|
|
588
|
-
(_, "commands") => "Commands",
|
|
589
|
-
(_, "difficulty") => "Difficulty",
|
|
590
|
-
(_, "topics") => "Topics",
|
|
591
|
-
(_, "input") => "Input",
|
|
592
|
-
(_, "examples") => "Examples",
|
|
593
|
-
(_, "example") => "Example",
|
|
594
|
-
(_, "command_placeholder") => "Type /, move with ↑/↓, Enter runs",
|
|
595
|
-
(_, "palette_hint") => "↑/↓ select | Enter run | Esc cancel",
|
|
596
|
-
(_, "hint_command") => "Enter submit | Esc cancel",
|
|
597
|
-
(_, "hint_list") => "up/down move | Enter open | Esc close",
|
|
598
|
-
(_, "hint_output") => "Esc code | / command | ? help",
|
|
599
|
-
(_, "hint_code") => "Esc then / command",
|
|
600
|
-
(_, "hint_idle") => "/ command | ? help",
|
|
601
|
-
(_, "help_title") => "Help",
|
|
602
|
-
(_, "daily_loop") => "Daily loop",
|
|
603
|
-
(_, "keys") => "Keys",
|
|
604
|
-
(_, "debug_prints") => "Debug prints",
|
|
605
|
-
(_, "cmd_run") => "Judge the current submission",
|
|
606
|
-
(_, "cmd_edit") => "Return to the code editor",
|
|
607
|
-
(_, "cmd_next") => "Open the next problem",
|
|
608
|
-
(_, "cmd_prev") => "Open the previous problem",
|
|
609
|
-
(_, "cmd_list") => "Browse problems",
|
|
610
|
-
(_, "cmd_open") => "Open by number, id, or slug",
|
|
611
|
-
(_, "cmd_giveup") => "Show the reference answer",
|
|
612
|
-
(_, "cmd_ai") => "Ask AI about the current problem and code",
|
|
613
|
-
(_, "cmd_provider") => "Set AI provider",
|
|
614
|
-
(_, "cmd_model") => "Set AI model",
|
|
615
|
-
(_, "cmd_note") => "Add a next-problem note",
|
|
616
|
-
(_, "cmd_notes") => "Show saved notes",
|
|
617
|
-
(_, "cmd_lang") => "Set code language",
|
|
618
|
-
(_, "cmd_ui") => "Set UI language",
|
|
619
|
-
(_, "cmd_theme") => "Set theme",
|
|
620
|
-
(_, "cmd_source") => "Set next-problem source",
|
|
621
|
-
(_, "cmd_exit") => "Quit",
|
|
622
|
-
(_, "cmd_help") => "Open help",
|
|
623
|
-
_ => "",
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
|
|
627
412
|
pub fn template_for(language: &str) -> String {
|
|
628
413
|
match normalize_language(language).as_str() {
|
|
629
414
|
"python" => "# Read from stdin and print to stdout.\nimport sys\n\n\n".to_string(),
|
|
@@ -690,6 +475,66 @@ pub fn render_problem(problem: &Problem, ui_language: &str) -> String {
|
|
|
690
475
|
)
|
|
691
476
|
}
|
|
692
477
|
|
|
478
|
+
pub fn render_problem_tui(problem: &Problem, ui_language: &str) -> String {
|
|
479
|
+
let lang = normalize_ui_language(ui_language);
|
|
480
|
+
let number = problem
|
|
481
|
+
.id
|
|
482
|
+
.split_once('-')
|
|
483
|
+
.map(|(number, _)| number)
|
|
484
|
+
.unwrap_or(&problem.id);
|
|
485
|
+
let mut lines = vec![
|
|
486
|
+
format!("{number}. {}", localized(&problem.title, &lang)),
|
|
487
|
+
format!(
|
|
488
|
+
"{}: {} {}: {}",
|
|
489
|
+
ui_text(&lang, "difficulty"),
|
|
490
|
+
problem.difficulty,
|
|
491
|
+
ui_text(&lang, "topics"),
|
|
492
|
+
problem.topics.join(", ")
|
|
493
|
+
),
|
|
494
|
+
String::new(),
|
|
495
|
+
localized(&problem.statement, &lang),
|
|
496
|
+
];
|
|
497
|
+
push_tui_section(
|
|
498
|
+
&mut lines,
|
|
499
|
+
ui_text(&lang, "input"),
|
|
500
|
+
&localized(&problem.input, &lang),
|
|
501
|
+
);
|
|
502
|
+
push_tui_section(
|
|
503
|
+
&mut lines,
|
|
504
|
+
ui_text(&lang, "output"),
|
|
505
|
+
&localized(&problem.output, &lang),
|
|
506
|
+
);
|
|
507
|
+
lines.push(String::new());
|
|
508
|
+
lines.push(ui_text(&lang, "examples").to_string());
|
|
509
|
+
for (index, case) in problem.examples.iter().enumerate() {
|
|
510
|
+
lines.push(format!(" {} {}", ui_text(&lang, "example"), index + 1));
|
|
511
|
+
lines.push(format!(" {}:", ui_text(&lang, "input")));
|
|
512
|
+
push_case_text(&mut lines, &case.input);
|
|
513
|
+
lines.push(format!(" {}:", ui_text(&lang, "output")));
|
|
514
|
+
push_case_text(&mut lines, &case.output);
|
|
515
|
+
}
|
|
516
|
+
lines.join("\n").trim_end().to_string()
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
fn push_tui_section(lines: &mut Vec<String>, title: &str, body: &str) {
|
|
520
|
+
lines.push(String::new());
|
|
521
|
+
lines.push(title.to_string());
|
|
522
|
+
for line in body.trim_end().lines() {
|
|
523
|
+
lines.push(format!(" {line}"));
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
fn push_case_text(lines: &mut Vec<String>, body: &str) {
|
|
528
|
+
let body = body.trim_end();
|
|
529
|
+
if body.is_empty() {
|
|
530
|
+
lines.push(" <empty>".to_string());
|
|
531
|
+
} else {
|
|
532
|
+
for line in body.lines() {
|
|
533
|
+
lines.push(format!(" {line}"));
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
693
538
|
pub fn fenced_text(value: &str) -> String {
|
|
694
539
|
let mut body = value.to_string();
|
|
695
540
|
if !body.ends_with('\n') {
|
package/src/i18n.rs
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
use std::{collections::HashMap, sync::OnceLock};
|
|
2
|
+
|
|
3
|
+
pub const UI_LANGUAGES: &[&str] = &["en", "ko", "ja", "zh", "es"];
|
|
4
|
+
|
|
5
|
+
static EN: OnceLock<HashMap<String, String>> = OnceLock::new();
|
|
6
|
+
static KO: OnceLock<HashMap<String, String>> = OnceLock::new();
|
|
7
|
+
static JA: OnceLock<HashMap<String, String>> = OnceLock::new();
|
|
8
|
+
static ZH: OnceLock<HashMap<String, String>> = OnceLock::new();
|
|
9
|
+
static ES: OnceLock<HashMap<String, String>> = OnceLock::new();
|
|
10
|
+
|
|
11
|
+
pub fn normalize_ui_language(language: &str) -> String {
|
|
12
|
+
let lower = language.trim().to_lowercase();
|
|
13
|
+
let short = lower
|
|
14
|
+
.split(['-', '_'])
|
|
15
|
+
.next()
|
|
16
|
+
.filter(|value| !value.is_empty())
|
|
17
|
+
.unwrap_or("en");
|
|
18
|
+
if UI_LANGUAGES.contains(&short) {
|
|
19
|
+
short.to_string()
|
|
20
|
+
} else {
|
|
21
|
+
"en".to_string()
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
pub fn ui_text(lang: &str, key: &str) -> &'static str {
|
|
26
|
+
catalog(&normalize_ui_language(lang))
|
|
27
|
+
.get(key)
|
|
28
|
+
.or_else(|| catalog("en").get(key))
|
|
29
|
+
.map(String::as_str)
|
|
30
|
+
.unwrap_or("")
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
fn catalog(lang: &str) -> &'static HashMap<String, String> {
|
|
34
|
+
match lang {
|
|
35
|
+
"ko" => KO.get_or_init(|| load(include_str!("../assets/i18n/ko.json"))),
|
|
36
|
+
"ja" => JA.get_or_init(|| load(include_str!("../assets/i18n/ja.json"))),
|
|
37
|
+
"zh" => ZH.get_or_init(|| load(include_str!("../assets/i18n/zh.json"))),
|
|
38
|
+
"es" => ES.get_or_init(|| load(include_str!("../assets/i18n/es.json"))),
|
|
39
|
+
_ => EN.get_or_init(|| load(include_str!("../assets/i18n/en.json"))),
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fn load(text: &str) -> HashMap<String, String> {
|
|
44
|
+
serde_json::from_str(text).expect("valid i18n catalog")
|
|
45
|
+
}
|
package/src/lib.rs
CHANGED
package/src/tui.rs
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
use crate::{
|
|
2
|
-
ai::{append_problem_note, read_problem_notes, run_ai_next, run_ai_prompt},
|
|
2
|
+
ai::{append_problem_note, provider_status, read_problem_notes, run_ai_next, run_ai_prompt},
|
|
3
3
|
core::{
|
|
4
4
|
AI_PROVIDERS, AppState, HistoryItem, LANGUAGES, PROBLEM_NOTES_PATH, Problem, THEMES,
|
|
5
5
|
UI_LANGUAGES, ensure_problem_files, ensure_submission, ext_for, give_up, judge, load_bank,
|
|
6
6
|
load_state, localized, next_problem, normalize_ai_provider, normalize_language,
|
|
7
7
|
normalize_next_source, normalize_ui_language, previous_problem, problem_by_id, record_pass,
|
|
8
|
-
|
|
8
|
+
render_problem_tui, save_state, template_for, ui_text,
|
|
9
9
|
},
|
|
10
10
|
text::{
|
|
11
11
|
byte_index, char_len, compose_hangul_jamo, display_width, prefix, render_markdown_plain,
|
|
12
12
|
},
|
|
13
|
+
update::{CURRENT_VERSION, UpdateCheck, check_latest_version},
|
|
13
14
|
};
|
|
14
15
|
use anyhow::Result;
|
|
15
16
|
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
|
@@ -214,11 +215,11 @@ const COMMAND_HINTS: &[CommandHint] = &[
|
|
|
214
215
|
help: false,
|
|
215
216
|
},
|
|
216
217
|
CommandHint {
|
|
217
|
-
insert: "source
|
|
218
|
-
display: "/source
|
|
218
|
+
insert: "source local",
|
|
219
|
+
display: "/source local",
|
|
219
220
|
desc_key: "cmd_source",
|
|
220
221
|
keep_open: false,
|
|
221
|
-
help:
|
|
222
|
+
help: false,
|
|
222
223
|
},
|
|
223
224
|
CommandHint {
|
|
224
225
|
insert: "source ai",
|
|
@@ -227,6 +228,13 @@ const COMMAND_HINTS: &[CommandHint] = &[
|
|
|
227
228
|
keep_open: false,
|
|
228
229
|
help: false,
|
|
229
230
|
},
|
|
231
|
+
CommandHint {
|
|
232
|
+
insert: "update",
|
|
233
|
+
display: "/update",
|
|
234
|
+
desc_key: "cmd_update",
|
|
235
|
+
keep_open: false,
|
|
236
|
+
help: true,
|
|
237
|
+
},
|
|
230
238
|
CommandHint {
|
|
231
239
|
insert: "help",
|
|
232
240
|
display: "/help",
|
|
@@ -269,6 +277,9 @@ pub struct PracticodeApp {
|
|
|
269
277
|
busy_body: String,
|
|
270
278
|
busy_frame: usize,
|
|
271
279
|
task_rx: Option<Receiver<TaskResult>>,
|
|
280
|
+
update_rx: Option<Receiver<UpdateCheck>>,
|
|
281
|
+
update_check: Option<UpdateCheck>,
|
|
282
|
+
update_notice: Option<String>,
|
|
272
283
|
should_quit: bool,
|
|
273
284
|
}
|
|
274
285
|
|
|
@@ -306,6 +317,9 @@ impl PracticodeApp {
|
|
|
306
317
|
busy_body: String::new(),
|
|
307
318
|
busy_frame: 0,
|
|
308
319
|
task_rx: None,
|
|
320
|
+
update_rx: None,
|
|
321
|
+
update_check: None,
|
|
322
|
+
update_notice: None,
|
|
309
323
|
should_quit: false,
|
|
310
324
|
};
|
|
311
325
|
app.load_code_editor()?;
|
|
@@ -313,9 +327,11 @@ impl PracticodeApp {
|
|
|
313
327
|
}
|
|
314
328
|
|
|
315
329
|
pub fn run(&mut self, terminal: &mut DefaultTerminal) -> Result<()> {
|
|
330
|
+
self.start_update_check();
|
|
316
331
|
while !self.should_quit {
|
|
317
332
|
terminal.draw(|frame| self.draw(frame))?;
|
|
318
333
|
self.check_task();
|
|
334
|
+
self.check_update();
|
|
319
335
|
if event::poll(Duration::from_millis(100))?
|
|
320
336
|
&& let Event::Key(key) = event::read()?
|
|
321
337
|
&& key.kind != KeyEventKind::Release
|
|
@@ -323,7 +339,7 @@ impl PracticodeApp {
|
|
|
323
339
|
self.handle_key(key)?;
|
|
324
340
|
}
|
|
325
341
|
if !self.busy_label.is_empty() {
|
|
326
|
-
self.busy_frame = (self.busy_frame + 1) %
|
|
342
|
+
self.busy_frame = (self.busy_frame + 1) % 16;
|
|
327
343
|
}
|
|
328
344
|
}
|
|
329
345
|
self.save_code().ok();
|
|
@@ -362,6 +378,10 @@ impl PracticodeApp {
|
|
|
362
378
|
self.task_rx.is_some()
|
|
363
379
|
}
|
|
364
380
|
|
|
381
|
+
pub fn status_text_for_test(&self) -> String {
|
|
382
|
+
self.status_text()
|
|
383
|
+
}
|
|
384
|
+
|
|
365
385
|
fn draw(&mut self, frame: &mut Frame) {
|
|
366
386
|
let size = frame.area();
|
|
367
387
|
let vertical = Layout::default()
|
|
@@ -377,10 +397,10 @@ impl PracticodeApp {
|
|
|
377
397
|
.constraints([Constraint::Percentage(58), Constraint::Percentage(42)])
|
|
378
398
|
.split(vertical[0]);
|
|
379
399
|
|
|
380
|
-
let problem = Paragraph::new(
|
|
400
|
+
let problem = Paragraph::new(render_problem_tui(
|
|
381
401
|
&self.problem,
|
|
382
402
|
&self.state.settings.ui_language,
|
|
383
|
-
))
|
|
403
|
+
))
|
|
384
404
|
.block(Self::block(
|
|
385
405
|
ui_text(&self.state.settings.ui_language, "problem"),
|
|
386
406
|
self.state.settings.theme == "light",
|
|
@@ -390,7 +410,7 @@ impl PracticodeApp {
|
|
|
390
410
|
|
|
391
411
|
if self.show_output {
|
|
392
412
|
let text = if !self.busy_label.is_empty() {
|
|
393
|
-
format!("{}{}", self.busy_body,
|
|
413
|
+
format!("{}{}", self.busy_body, self.busy_dots())
|
|
394
414
|
} else if self.output_is_markdown {
|
|
395
415
|
render_markdown_plain(&self.output)
|
|
396
416
|
} else {
|
|
@@ -667,18 +687,12 @@ impl PracticodeApp {
|
|
|
667
687
|
"theme" if arg.is_empty() => self.action_toggle_theme()?,
|
|
668
688
|
"theme" if THEMES.contains(&arg) => self.set_theme(arg)?,
|
|
669
689
|
"source" | "next-source" if arg.is_empty() => {
|
|
670
|
-
self.write_text_output(&format!(
|
|
671
|
-
"Next source: {}",
|
|
672
|
-
self.state.settings.next_source
|
|
673
|
-
));
|
|
690
|
+
self.write_text_output(&format!("Next source: {}", self.next_source_label()));
|
|
674
691
|
}
|
|
675
|
-
"source" | "next-source" if matches!(arg, "bank" | "ai") => {
|
|
692
|
+
"source" | "next-source" if matches!(arg, "bank" | "local" | "ai") => {
|
|
676
693
|
self.state.settings.next_source = normalize_next_source(arg);
|
|
677
694
|
save_state(&self.root, &self.state)?;
|
|
678
|
-
self.write_text_output(&format!(
|
|
679
|
-
"Next source: {}",
|
|
680
|
-
self.state.settings.next_source
|
|
681
|
-
));
|
|
695
|
+
self.write_text_output(&format!("Next source: {}", self.next_source_label()));
|
|
682
696
|
}
|
|
683
697
|
"ai-next-command" if !arg.is_empty() => {
|
|
684
698
|
self.state.settings.ai_next_command = arg.to_string();
|
|
@@ -688,16 +702,18 @@ impl PracticodeApp {
|
|
|
688
702
|
}
|
|
689
703
|
"provider" | "ai-provider" if arg.is_empty() => {
|
|
690
704
|
self.write_text_output(&format!(
|
|
691
|
-
"AI provider: {}",
|
|
692
|
-
self.state.settings.ai_provider
|
|
705
|
+
"AI provider: {}\n{}",
|
|
706
|
+
self.state.settings.ai_provider,
|
|
707
|
+
provider_status(&self.state.settings.ai_provider)
|
|
693
708
|
));
|
|
694
709
|
}
|
|
695
710
|
"provider" | "ai-provider" if AI_PROVIDERS.contains(&arg) => {
|
|
696
711
|
self.state.settings.ai_provider = normalize_ai_provider(arg);
|
|
697
712
|
save_state(&self.root, &self.state)?;
|
|
698
713
|
self.write_text_output(&format!(
|
|
699
|
-
"AI provider: {}",
|
|
700
|
-
self.state.settings.ai_provider
|
|
714
|
+
"AI provider: {}\n{}",
|
|
715
|
+
self.state.settings.ai_provider,
|
|
716
|
+
provider_status(&self.state.settings.ai_provider)
|
|
701
717
|
));
|
|
702
718
|
}
|
|
703
719
|
"model" if arg.is_empty() => {
|
|
@@ -711,6 +727,7 @@ impl PracticodeApp {
|
|
|
711
727
|
"ai" if !arg.is_empty() => self.start_ai_prompt(arg)?,
|
|
712
728
|
"note" if !arg.is_empty() => self.append_note(arg)?,
|
|
713
729
|
"note" | "notes" => self.show_notes()?,
|
|
730
|
+
"update" => self.show_update_notice(),
|
|
714
731
|
"exit" | "quit" | "q" => self.should_quit = true,
|
|
715
732
|
_ => self.write_text_output(&format!("Unknown command: {value}\nTry /help.")),
|
|
716
733
|
}
|
|
@@ -769,10 +786,13 @@ impl PracticodeApp {
|
|
|
769
786
|
|
|
770
787
|
fn start_next_problem(&mut self, old_problem: String, force: bool, request: String) {
|
|
771
788
|
if self.task_rx.is_some() {
|
|
772
|
-
self.write_text_output(
|
|
789
|
+
self.write_text_output(ui_text(&self.state.settings.ui_language, "already_busy"));
|
|
773
790
|
return;
|
|
774
791
|
}
|
|
775
|
-
self.start_busy(
|
|
792
|
+
self.start_busy(
|
|
793
|
+
"next",
|
|
794
|
+
ui_text(&self.state.settings.ui_language, "generating_next"),
|
|
795
|
+
);
|
|
776
796
|
let root = self.root.clone();
|
|
777
797
|
let state = self.state.clone();
|
|
778
798
|
let (tx, rx) = mpsc::channel();
|
|
@@ -890,7 +910,7 @@ impl PracticodeApp {
|
|
|
890
910
|
|
|
891
911
|
fn start_ai_prompt(&mut self, prompt: &str) -> Result<()> {
|
|
892
912
|
if self.task_rx.is_some() {
|
|
893
|
-
self.write_text_output(
|
|
913
|
+
self.write_text_output(ui_text(&self.state.settings.ui_language, "already_busy"));
|
|
894
914
|
return Ok(());
|
|
895
915
|
}
|
|
896
916
|
self.save_code()?;
|
|
@@ -929,6 +949,28 @@ impl PracticodeApp {
|
|
|
929
949
|
}
|
|
930
950
|
}
|
|
931
951
|
|
|
952
|
+
fn check_update(&mut self) {
|
|
953
|
+
let result = self.update_rx.as_ref().and_then(|rx| rx.try_recv().ok());
|
|
954
|
+
if let Some(result) = result {
|
|
955
|
+
self.update_rx = None;
|
|
956
|
+
self.update_check = Some(result.clone());
|
|
957
|
+
if let UpdateCheck::Available(version) = &result {
|
|
958
|
+
self.update_notice = Some(version.clone());
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
fn start_update_check(&mut self) {
|
|
964
|
+
if self.update_rx.is_some() {
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
let (tx, rx) = mpsc::channel();
|
|
968
|
+
thread::spawn(move || {
|
|
969
|
+
let _ = tx.send(check_latest_version());
|
|
970
|
+
});
|
|
971
|
+
self.update_rx = Some(rx);
|
|
972
|
+
}
|
|
973
|
+
|
|
932
974
|
fn start_busy(&mut self, label: &str, body: &str) {
|
|
933
975
|
self.busy_label = label.to_string();
|
|
934
976
|
self.busy_body = body.to_string();
|
|
@@ -957,6 +999,24 @@ impl PracticodeApp {
|
|
|
957
999
|
self.focus = Focus::Output;
|
|
958
1000
|
}
|
|
959
1001
|
|
|
1002
|
+
fn show_update_notice(&mut self) {
|
|
1003
|
+
let lang = self.state.settings.ui_language.clone();
|
|
1004
|
+
if let Some(version) = &self.update_notice {
|
|
1005
|
+
self.write_text_output(&format!(
|
|
1006
|
+
"{}: practicode {version} (current {CURRENT_VERSION})\n\nnpm update -g practicode\ncargo install --force practicode",
|
|
1007
|
+
ui_text(&lang, "update_available")
|
|
1008
|
+
));
|
|
1009
|
+
} else if self.update_rx.is_some() {
|
|
1010
|
+
self.write_text_output("Checking for updates...");
|
|
1011
|
+
} else if matches!(self.update_check, Some(UpdateCheck::Disabled)) {
|
|
1012
|
+
self.write_text_output(ui_text(&lang, "update_check_disabled"));
|
|
1013
|
+
} else if matches!(self.update_check, Some(UpdateCheck::Failed)) {
|
|
1014
|
+
self.write_text_output(ui_text(&lang, "update_check_failed"));
|
|
1015
|
+
} else {
|
|
1016
|
+
self.write_text_output(ui_text(&lang, "update_none"));
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
960
1020
|
fn append_note(&mut self, note: &str) -> Result<()> {
|
|
961
1021
|
append_problem_note(&self.root, note)?;
|
|
962
1022
|
self.write_text_output(&format!("Problem note saved to {PROBLEM_NOTES_PATH}."));
|
|
@@ -1243,29 +1303,45 @@ impl PracticodeApp {
|
|
|
1243
1303
|
|
|
1244
1304
|
fn status_text(&self) -> String {
|
|
1245
1305
|
let code_status = self.submission_status(&self.problem).0;
|
|
1306
|
+
let activity = if self.busy_label.is_empty() {
|
|
1307
|
+
"idle".to_string()
|
|
1308
|
+
} else {
|
|
1309
|
+
format!("{}{}", self.busy_body, self.busy_dots())
|
|
1310
|
+
};
|
|
1311
|
+
let tail = self
|
|
1312
|
+
.update_notice
|
|
1313
|
+
.as_ref()
|
|
1314
|
+
.map(|version| {
|
|
1315
|
+
format!(
|
|
1316
|
+
"{}:{version} /update",
|
|
1317
|
+
ui_text(&self.state.settings.ui_language, "update")
|
|
1318
|
+
)
|
|
1319
|
+
})
|
|
1320
|
+
.unwrap_or_else(|| self.mode_hint().to_string());
|
|
1246
1321
|
format!(
|
|
1247
|
-
" PRACTICODE | {} | {} | {} | {} | code:{} | {} |
|
|
1322
|
+
" PRACTICODE | {} | {} | {} | {} | code:{} | {} | {} ",
|
|
1248
1323
|
self.problem.id,
|
|
1249
1324
|
self.problem.difficulty,
|
|
1250
|
-
self.busy_status(),
|
|
1251
1325
|
self.problem_status(&self.problem),
|
|
1326
|
+
activity,
|
|
1252
1327
|
code_status,
|
|
1253
1328
|
self.state.settings.language,
|
|
1254
|
-
|
|
1255
|
-
self.state.settings.ai_provider,
|
|
1256
|
-
self.state.settings.ai_model,
|
|
1257
|
-
self.mode_hint(),
|
|
1329
|
+
tail,
|
|
1258
1330
|
)
|
|
1259
1331
|
}
|
|
1260
1332
|
|
|
1261
|
-
fn
|
|
1262
|
-
if self.
|
|
1263
|
-
"
|
|
1333
|
+
fn next_source_label(&self) -> &'static str {
|
|
1334
|
+
if self.state.settings.next_source == "ai" {
|
|
1335
|
+
"ai"
|
|
1264
1336
|
} else {
|
|
1265
|
-
|
|
1337
|
+
"local"
|
|
1266
1338
|
}
|
|
1267
1339
|
}
|
|
1268
1340
|
|
|
1341
|
+
fn busy_dots(&self) -> String {
|
|
1342
|
+
".".repeat(self.busy_frame / 4)
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1269
1345
|
fn mode_hint(&self) -> &'static str {
|
|
1270
1346
|
let lang = &self.state.settings.ui_language;
|
|
1271
1347
|
match (self.focus, self.list_cursor.is_some(), self.show_output) {
|
package/src/update.rs
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
use crate::process::{run_capture, which};
|
|
2
|
+
use std::{env, process::Command, time::Duration};
|
|
3
|
+
|
|
4
|
+
pub const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
|
|
5
|
+
|
|
6
|
+
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
7
|
+
pub enum UpdateCheck {
|
|
8
|
+
Disabled,
|
|
9
|
+
Current,
|
|
10
|
+
Available(String),
|
|
11
|
+
Failed,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
pub fn check_latest_version() -> UpdateCheck {
|
|
15
|
+
if env::var("PRACTICODE_NO_UPDATE_CHECK").ok().as_deref() == Some("1") {
|
|
16
|
+
return UpdateCheck::Disabled;
|
|
17
|
+
}
|
|
18
|
+
let Some(npm) = which("npm") else {
|
|
19
|
+
return UpdateCheck::Failed;
|
|
20
|
+
};
|
|
21
|
+
let mut command = Command::new(npm);
|
|
22
|
+
command.args(["view", "practicode", "version", "--silent"]);
|
|
23
|
+
match run_capture(&mut command, "", Duration::from_secs(5)) {
|
|
24
|
+
Ok(output) if output.code == Some(0) => {
|
|
25
|
+
let latest = output.stdout.trim();
|
|
26
|
+
if is_newer(latest, CURRENT_VERSION) {
|
|
27
|
+
UpdateCheck::Available(latest.to_string())
|
|
28
|
+
} else {
|
|
29
|
+
UpdateCheck::Current
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
_ => UpdateCheck::Failed,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
pub fn is_newer(latest: &str, current: &str) -> bool {
|
|
37
|
+
version_parts(latest) > version_parts(current)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fn version_parts(version: &str) -> Vec<u64> {
|
|
41
|
+
version
|
|
42
|
+
.split('.')
|
|
43
|
+
.map(|part| part.parse::<u64>().unwrap_or(0))
|
|
44
|
+
.collect()
|
|
45
|
+
}
|