practicode 0.1.4 → 0.1.6
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 +15 -8
- package/assets/i18n/en.json +9 -2
- package/assets/i18n/es.json +9 -2
- package/assets/i18n/ja.json +9 -2
- package/assets/i18n/ko.json +9 -2
- package/assets/i18n/zh.json +9 -2
- package/assets/practicode-terminal.svg +33 -21
- package/docs/ARCHITECTURE.md +25 -0
- package/docs/CONTRIBUTING.md +1 -0
- package/package.json +1 -1
- package/src/ai.rs +25 -4
- package/src/core/profile.rs +29 -0
- package/src/core.rs +45 -21
- package/src/lib.rs +11 -3
- package/src/tui/commands.rs +242 -0
- package/src/tui.rs +251 -261
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
package/README.md
CHANGED
|
@@ -63,27 +63,33 @@ 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
|
-
| `/
|
|
69
|
+
| `/back` | Go back through problem history |
|
|
70
|
+
| `/problems` | Browse problems with `up/down` or `j/k`, open with `Enter` |
|
|
70
71
|
| `/open 2` | Open by number, id, or slug |
|
|
71
|
-
| `/
|
|
72
|
+
| `/answer` | Show the reference answer |
|
|
72
73
|
| `/hint` | Ask the selected AI for a concise hint |
|
|
73
74
|
| `/hint explain my bug` | Ask the selected AI about the current problem and submission |
|
|
75
|
+
| `/profile` | Show your current practice profile |
|
|
76
|
+
| `/difficulty auto` | Set difficulty preference: `auto`, `easy`, `medium`, `hard` |
|
|
77
|
+
| `/topics arrays, strings` | Set preferred topics for future problems |
|
|
78
|
+
| `/avoid dp, graph` | Set topics to avoid in future problems |
|
|
74
79
|
| `/provider codex` | Set AI provider and show local CLI/daemon status |
|
|
75
80
|
| `/model auto` | Use the provider default model for `/hint` and AI-backed `/next` |
|
|
76
|
-
| `/
|
|
77
|
-
| `/notes` | Show your local next-problem notes |
|
|
78
|
-
| `/lang python` | Set code language: `python`, `ts`, `java`, `rust` |
|
|
81
|
+
| `/language python` | Set code language: `python`, `ts`, `java`, `rust` |
|
|
79
82
|
| `/ui en` | Set UI language: `en`, `ko`, `ja`, `zh`, `es` |
|
|
80
83
|
| `/theme dark` | Set theme: `dark` or `light` |
|
|
81
|
-
| `/source ai` | Prefer AI for next-problem generation |
|
|
82
84
|
| `/update` | Show update instructions when a newer version is available |
|
|
83
85
|
| `/exit` | Quit |
|
|
84
86
|
|
|
87
|
+
Older command names such as `/prev`, `/list`, `/giveup`, and `/lang` still work as aliases.
|
|
88
|
+
|
|
85
89
|
The default UI language is English. Switch it any time with `/ui ko`, `/ui ja`, `/ui zh`, or `/ui es`.
|
|
86
90
|
|
|
91
|
+
Your practice profile is saved in `.practicode/problem-state.json`. It keeps UI language, code language, theme, preferred difficulty, preferred topics, and topics to avoid. `auto` difficulty follows gradual progression; a fixed difficulty asks local selection and AI generation to prefer that level.
|
|
92
|
+
|
|
87
93
|
## AI Problems
|
|
88
94
|
|
|
89
95
|
`/next <request>` passes your request into the selected AI problem generator.
|
|
@@ -106,7 +112,6 @@ Claude Code is also supported:
|
|
|
106
112
|
```text
|
|
107
113
|
/provider claude
|
|
108
114
|
/model sonnet
|
|
109
|
-
/source ai
|
|
110
115
|
```
|
|
111
116
|
|
|
112
117
|
Generated problems and submissions stay local:
|
|
@@ -140,6 +145,8 @@ External contributions use the fork and pull request flow in [docs/CONTRIBUTING.
|
|
|
140
145
|
|
|
141
146
|
Maintainer-only review and release notes live in [docs/MAINTAINING.md](docs/MAINTAINING.md).
|
|
142
147
|
|
|
148
|
+
Code layout and extension boundaries live in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
|
|
149
|
+
|
|
143
150
|
## License
|
|
144
151
|
|
|
145
152
|
practicode is MIT licensed. Third-party dependency license notes are in [THIRD_PARTY_LICENSES.md](THIRD_PARTY_LICENSES.md).
|
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",
|
|
@@ -31,6 +32,10 @@
|
|
|
31
32
|
"cmd_giveup": "Show the reference answer",
|
|
32
33
|
"cmd_hint": "Ask for a hint about the current problem",
|
|
33
34
|
"cmd_ai": "Ask AI about the current problem and code",
|
|
35
|
+
"cmd_profile": "Show practice profile",
|
|
36
|
+
"cmd_difficulty": "Set preferred difficulty",
|
|
37
|
+
"cmd_topics": "Set preferred topics",
|
|
38
|
+
"cmd_avoid": "Set topics to avoid",
|
|
34
39
|
"cmd_provider": "Set AI provider",
|
|
35
40
|
"cmd_model": "Set AI model",
|
|
36
41
|
"cmd_model_auto": "Use provider default model",
|
|
@@ -50,5 +55,7 @@
|
|
|
50
55
|
"update_check_disabled": "Update check is disabled.",
|
|
51
56
|
"update_check_failed": "Could not check for updates.",
|
|
52
57
|
"generating_next": "Generating next problem",
|
|
53
|
-
"already_busy": "Already busy."
|
|
58
|
+
"already_busy": "Already busy.",
|
|
59
|
+
"run_pass_next": "Next: /next",
|
|
60
|
+
"run_fail_next": "Fix: press Esc or click this pane to edit, then /run"
|
|
54
61
|
}
|
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",
|
|
@@ -31,6 +32,10 @@
|
|
|
31
32
|
"cmd_giveup": "Mostrar la respuesta de referencia",
|
|
32
33
|
"cmd_hint": "Pedir una pista para el problema actual",
|
|
33
34
|
"cmd_ai": "Preguntar a AI sobre el problema y codigo actuales",
|
|
35
|
+
"cmd_profile": "Mostrar perfil de practica",
|
|
36
|
+
"cmd_difficulty": "Configurar dificultad preferida",
|
|
37
|
+
"cmd_topics": "Configurar temas preferidos",
|
|
38
|
+
"cmd_avoid": "Configurar temas a evitar",
|
|
34
39
|
"cmd_provider": "Configurar AI provider",
|
|
35
40
|
"cmd_model": "Configurar AI model",
|
|
36
41
|
"cmd_model_auto": "Usar el modelo predeterminado del provider",
|
|
@@ -50,5 +55,7 @@
|
|
|
50
55
|
"update_check_disabled": "La comprobacion de actualizaciones esta desactivada.",
|
|
51
56
|
"update_check_failed": "No se pudo comprobar actualizaciones.",
|
|
52
57
|
"generating_next": "Generando el siguiente problema",
|
|
53
|
-
"already_busy": "Ya hay una tarea en curso."
|
|
58
|
+
"already_busy": "Ya hay una tarea en curso.",
|
|
59
|
+
"run_pass_next": "Siguiente: /next",
|
|
60
|
+
"run_fail_next": "Corrige: pulsa Esc o haz clic en este panel para editar, luego /run"
|
|
54
61
|
}
|
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": "前の問題を開く",
|
|
@@ -31,6 +32,10 @@
|
|
|
31
32
|
"cmd_giveup": "解答を見る",
|
|
32
33
|
"cmd_hint": "現在の問題のヒントを依頼",
|
|
33
34
|
"cmd_ai": "現在の問題とコードについて AI に質問",
|
|
35
|
+
"cmd_profile": "練習プロファイルを表示",
|
|
36
|
+
"cmd_difficulty": "希望難易度を設定",
|
|
37
|
+
"cmd_topics": "希望トピックを設定",
|
|
38
|
+
"cmd_avoid": "避けるトピックを設定",
|
|
34
39
|
"cmd_provider": "AI provider を設定",
|
|
35
40
|
"cmd_model": "AI model を設定",
|
|
36
41
|
"cmd_model_auto": "provider の既定モデルを使用",
|
|
@@ -50,5 +55,7 @@
|
|
|
50
55
|
"update_check_disabled": "更新確認は無効です。",
|
|
51
56
|
"update_check_failed": "更新を確認できませんでした。",
|
|
52
57
|
"generating_next": "次の問題を生成中",
|
|
53
|
-
"already_busy": "すでに処理中です。"
|
|
58
|
+
"already_busy": "すでに処理中です。",
|
|
59
|
+
"run_pass_next": "次: /next",
|
|
60
|
+
"run_fail_next": "修正: Esc またはこのペインをクリックして編集し、/run"
|
|
54
61
|
}
|
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": "이전 문제 열기",
|
|
@@ -31,6 +32,10 @@
|
|
|
31
32
|
"cmd_giveup": "정답 보기",
|
|
32
33
|
"cmd_hint": "현재 문제 힌트 요청",
|
|
33
34
|
"cmd_ai": "현재 문제와 코드에 대해 AI에게 질문",
|
|
35
|
+
"cmd_profile": "연습 프로파일 보기",
|
|
36
|
+
"cmd_difficulty": "선호 난이도 설정",
|
|
37
|
+
"cmd_topics": "선호 주제 설정",
|
|
38
|
+
"cmd_avoid": "피할 주제 설정",
|
|
34
39
|
"cmd_provider": "AI provider 설정",
|
|
35
40
|
"cmd_model": "AI model 설정",
|
|
36
41
|
"cmd_model_auto": "provider 기본 모델 사용",
|
|
@@ -50,5 +55,7 @@
|
|
|
50
55
|
"update_check_disabled": "업데이트 확인이 꺼져 있습니다.",
|
|
51
56
|
"update_check_failed": "업데이트를 확인할 수 없습니다.",
|
|
52
57
|
"generating_next": "다음 문제 생성 중",
|
|
53
|
-
"already_busy": "이미 작업 중입니다."
|
|
58
|
+
"already_busy": "이미 작업 중입니다.",
|
|
59
|
+
"run_pass_next": "다음: /next",
|
|
60
|
+
"run_fail_next": "수정: Esc를 누르거나 이 창을 클릭해 편집한 뒤 /run"
|
|
54
61
|
}
|
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": "打开上一题",
|
|
@@ -31,6 +32,10 @@
|
|
|
31
32
|
"cmd_giveup": "显示参考答案",
|
|
32
33
|
"cmd_hint": "请求当前题目的提示",
|
|
33
34
|
"cmd_ai": "向 AI 询问当前题目和代码",
|
|
35
|
+
"cmd_profile": "显示练习配置",
|
|
36
|
+
"cmd_difficulty": "设置偏好难度",
|
|
37
|
+
"cmd_topics": "设置偏好主题",
|
|
38
|
+
"cmd_avoid": "设置要避开的主题",
|
|
34
39
|
"cmd_provider": "设置 AI provider",
|
|
35
40
|
"cmd_model": "设置 AI model",
|
|
36
41
|
"cmd_model_auto": "使用 provider 默认模型",
|
|
@@ -50,5 +55,7 @@
|
|
|
50
55
|
"update_check_disabled": "更新检查已禁用。",
|
|
51
56
|
"update_check_failed": "无法检查更新。",
|
|
52
57
|
"generating_next": "正在生成下一题",
|
|
53
|
-
"already_busy": "已有任务正在运行。"
|
|
58
|
+
"already_busy": "已有任务正在运行。",
|
|
59
|
+
"run_pass_next": "下一步: /next",
|
|
60
|
+
"run_fail_next": "修复: 按 Esc 或点击此面板编辑,然后 /run"
|
|
54
61
|
}
|
|
@@ -1,24 +1,36 @@
|
|
|
1
1
|
<svg width="1200" height="720" viewBox="0 0 1200 720" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
2
|
<rect width="1200" height="720" fill="#070b12"/>
|
|
3
|
-
<rect x="
|
|
4
|
-
|
|
5
|
-
<rect x="
|
|
6
|
-
<
|
|
7
|
-
|
|
8
|
-
<text x="
|
|
9
|
-
<text x="
|
|
10
|
-
|
|
11
|
-
<text x="
|
|
12
|
-
<text x="
|
|
13
|
-
<text x="
|
|
14
|
-
|
|
15
|
-
<text x="
|
|
16
|
-
<
|
|
17
|
-
<text x="
|
|
18
|
-
|
|
19
|
-
<
|
|
20
|
-
<
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
<text x="
|
|
3
|
+
<rect x="54" y="42" width="1092" height="636" rx="10" fill="#0b111a" stroke="#214c5c" stroke-width="2"/>
|
|
4
|
+
|
|
5
|
+
<rect x="76" y="68" width="560" height="430" rx="4" fill="#0f1720" stroke="#00c2d1" stroke-width="2"/>
|
|
6
|
+
<rect x="664" y="68" width="458" height="430" rx="4" fill="#0d141c" stroke="#f8e71c" stroke-width="2"/>
|
|
7
|
+
|
|
8
|
+
<text x="92" y="91" fill="#16d6e8" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="15" font-weight="700">Problem</text>
|
|
9
|
+
<text x="680" y="91" fill="#f8e71c" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="15" font-weight="700">> solution.py</text>
|
|
10
|
+
|
|
11
|
+
<text x="96" y="128" fill="#f8fafc" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="22" font-weight="700">001. Hello World</text>
|
|
12
|
+
<text x="96" y="164" fill="#c8d3f5" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="16">Difficulty: easy Topics: io</text>
|
|
13
|
+
<text x="96" y="206" fill="#f8fafc" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="17">Print exactly Hello, World! to stdout.</text>
|
|
14
|
+
|
|
15
|
+
<text x="96" y="286" fill="#00d2e8" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="17" font-weight="700">Input</text>
|
|
16
|
+
<rect x="96" y="304" width="486" height="44" rx="3" fill="#101d28" stroke="#29495a"/>
|
|
17
|
+
<text x="112" y="332" fill="#f8fafc" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="16">No input.</text>
|
|
18
|
+
|
|
19
|
+
<text x="96" y="388" fill="#00d2e8" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="17" font-weight="700">Output</text>
|
|
20
|
+
<rect x="96" y="406" width="486" height="66" rx="3" fill="#101d28" stroke="#29495a"/>
|
|
21
|
+
<text x="112" y="446" fill="#f6c177" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="16">Hello, World!</text>
|
|
22
|
+
|
|
23
|
+
<text x="690" y="128" fill="#64748b" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="16"> 1</text>
|
|
24
|
+
<text x="738" y="128" fill="#7dd3fc" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="16">print</text>
|
|
25
|
+
<text x="790" y="128" fill="#f8fafc" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="16">('Hello, World!')</text>
|
|
26
|
+
|
|
27
|
+
<rect x="76" y="512" width="1046" height="30" fill="#152033"/>
|
|
28
|
+
<text x="94" y="533" fill="#c8d3f5" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="15" font-weight="700">PRACTICODE | 001-hello-world | easy | idle | assigned | code:written | python | Esc then / command</text>
|
|
29
|
+
|
|
30
|
+
<rect x="76" y="558" width="1046" height="48" rx="2" fill="#0b1017" stroke="#00c2d1" stroke-width="2"/>
|
|
31
|
+
<text x="94" y="579" fill="#16d6e8" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="15" font-weight="700">Command</text>
|
|
32
|
+
<text x="94" y="596" fill="#f8fafc" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="18">/</text>
|
|
33
|
+
|
|
34
|
+
<rect x="76" y="614" width="1046" height="46" rx="2" fill="#101923" stroke="#214c5c"/>
|
|
35
|
+
<text x="94" y="641" fill="#f8fafc" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="15">/run /next /hint <request> /profile /difficulty auto /topics arrays, strings</text>
|
|
24
36
|
</svg>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
Practicode is local-first: user data stays under `.practicode/`, `problems/`, and `submissions/`.
|
|
4
|
+
|
|
5
|
+
## Source Layout
|
|
6
|
+
|
|
7
|
+
- `src/core.rs` owns problem data, state loading/saving, judging, and file generation.
|
|
8
|
+
- `src/core/profile.rs` owns practice-profile defaults and normalization.
|
|
9
|
+
- `src/tui.rs` owns Ratatui rendering and interaction flow.
|
|
10
|
+
- `src/tui/commands.rs` owns the command palette catalog.
|
|
11
|
+
- `src/ai.rs` owns provider commands, daemon/model checks, and AI prompts.
|
|
12
|
+
- `src/update.rs` owns update checks.
|
|
13
|
+
- `src/text.rs` owns terminal text editing and markdown/plain rendering helpers.
|
|
14
|
+
|
|
15
|
+
## Extension Rules
|
|
16
|
+
|
|
17
|
+
- Add domain logic under the owning module first; keep `tui.rs` as orchestration and rendering.
|
|
18
|
+
- Add user-visible commands in `src/tui/commands.rs`, then route behavior in `PracticodeApp::handle_command`.
|
|
19
|
+
- Add persisted profile settings to `Settings`, normalize them in `normalize_settings`, and cover old-state compatibility with tests.
|
|
20
|
+
- Keep provider-specific behavior in `src/ai.rs`; TUI should ask for status or start tasks, not know provider internals.
|
|
21
|
+
- Keep local user data backwards-compatible. Missing fields should default cleanly.
|
|
22
|
+
|
|
23
|
+
## Release
|
|
24
|
+
|
|
25
|
+
See [MAINTAINING.md](MAINTAINING.md) for tag-based releases.
|
package/docs/CONTRIBUTING.md
CHANGED
|
@@ -9,6 +9,7 @@ Maintainer-only review and release steps live in [MAINTAINING.md](MAINTAINING.md
|
|
|
9
9
|
- Search existing issues and pull requests first.
|
|
10
10
|
- Small bug fixes, docs fixes, tests, and localization updates can go straight to a pull request.
|
|
11
11
|
- For larger UI, AI-generation, storage, or packaging changes, open an issue first so the scope is clear.
|
|
12
|
+
- Check [ARCHITECTURE.md](ARCHITECTURE.md) before adding commands, settings, provider behavior, or persisted state.
|
|
12
13
|
- Do not commit local practice data from `.practicode/`, `problems/`, or `submissions/`.
|
|
13
14
|
- Do not include secrets, tokens, private prompts, or generated answer keys in docs or examples.
|
|
14
15
|
|
package/package.json
CHANGED
package/src/ai.rs
CHANGED
|
@@ -123,13 +123,22 @@ pub fn available_models(provider: &str) -> ModelCatalog {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
pub fn default_ai_next_prompt(request: &str) -> String {
|
|
126
|
+
default_ai_next_prompt_with_settings(&Settings::default(), request)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
pub fn default_ai_next_prompt_with_settings(settings: &Settings, request: &str) -> String {
|
|
126
130
|
format!(
|
|
127
|
-
"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.",
|
|
131
|
+
"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: {}. Practice profile: difficulty preference: {}; preferred topics: {}; avoid topics: {}; code language: {}; UI language: {}. Treat difficulty auto as gradual progression from state; otherwise prefer the requested difficulty unless the direct user request conflicts. 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.",
|
|
128
132
|
if request.is_empty() {
|
|
129
133
|
"(none)"
|
|
130
134
|
} else {
|
|
131
135
|
request
|
|
132
|
-
}
|
|
136
|
+
},
|
|
137
|
+
settings.difficulty,
|
|
138
|
+
list_or_none(&settings.topics),
|
|
139
|
+
list_or_none(&settings.avoid_topics),
|
|
140
|
+
settings.language,
|
|
141
|
+
settings.ui_language
|
|
133
142
|
)
|
|
134
143
|
}
|
|
135
144
|
|
|
@@ -304,7 +313,9 @@ fn default_codex_next_command(root: &Path, settings: &Settings, request: &str) -
|
|
|
304
313
|
exec.push_str(&format!(" --model {}", sh_quote(model)));
|
|
305
314
|
}
|
|
306
315
|
exec.push(' ');
|
|
307
|
-
exec.push_str(&sh_quote(&
|
|
316
|
+
exec.push_str(&sh_quote(&default_ai_next_prompt_with_settings(
|
|
317
|
+
settings, request,
|
|
318
|
+
)));
|
|
308
319
|
format!("{start}; {exec}")
|
|
309
320
|
}
|
|
310
321
|
|
|
@@ -322,7 +333,9 @@ fn default_claude_next_command(root: &Path, settings: &Settings, request: &str)
|
|
|
322
333
|
claude.push_str(&format!(" --model {}", sh_quote(model)));
|
|
323
334
|
}
|
|
324
335
|
claude.push_str(" -p ");
|
|
325
|
-
claude.push_str(&sh_quote(&
|
|
336
|
+
claude.push_str(&sh_quote(&default_ai_next_prompt_with_settings(
|
|
337
|
+
settings, request,
|
|
338
|
+
)));
|
|
326
339
|
format!(
|
|
327
340
|
"claude daemon status >/dev/null 2>&1 || true; cd {}; {}",
|
|
328
341
|
sh_quote(&root.display().to_string()),
|
|
@@ -338,6 +351,14 @@ fn output_text(stdout: &str, stderr: &str) -> String {
|
|
|
338
351
|
.join("\n")
|
|
339
352
|
}
|
|
340
353
|
|
|
354
|
+
fn list_or_none(values: &[String]) -> String {
|
|
355
|
+
if values.is_empty() {
|
|
356
|
+
"(none)".to_string()
|
|
357
|
+
} else {
|
|
358
|
+
values.join(", ")
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
341
362
|
#[cfg(test)]
|
|
342
363
|
mod tests {
|
|
343
364
|
use super::parse_model_list;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
pub const DIFFICULTIES: &[&str] = &["auto", "easy", "medium", "hard"];
|
|
2
|
+
|
|
3
|
+
pub fn default_difficulty() -> String {
|
|
4
|
+
"auto".to_string()
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
pub fn normalize_difficulty(difficulty: &str) -> String {
|
|
8
|
+
let difficulty = difficulty.trim().to_lowercase();
|
|
9
|
+
if DIFFICULTIES.contains(&difficulty.as_str()) {
|
|
10
|
+
difficulty
|
|
11
|
+
} else {
|
|
12
|
+
default_difficulty()
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
pub fn parse_topic_list(value: &str) -> Vec<String> {
|
|
17
|
+
let mut topics = Vec::new();
|
|
18
|
+
for topic in value.split(',') {
|
|
19
|
+
let topic = topic.trim().trim_start_matches('#').trim().to_lowercase();
|
|
20
|
+
if !topic.is_empty() && !topics.contains(&topic) {
|
|
21
|
+
topics.push(topic);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
topics
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
pub fn normalize_topic_list(topics: &[String]) -> Vec<String> {
|
|
28
|
+
parse_topic_list(&topics.join(","))
|
|
29
|
+
}
|
package/src/core.rs
CHANGED
|
@@ -9,6 +9,11 @@ use std::{
|
|
|
9
9
|
time::Duration,
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
+
mod profile;
|
|
13
|
+
pub use profile::{
|
|
14
|
+
DIFFICULTIES, default_difficulty, normalize_difficulty, normalize_topic_list, parse_topic_list,
|
|
15
|
+
};
|
|
16
|
+
|
|
12
17
|
pub const LANGUAGES: &[&str] = &["python", "ts", "java", "rust"];
|
|
13
18
|
pub use crate::i18n::{UI_LANGUAGES, normalize_ui_language, ui_text};
|
|
14
19
|
pub const THEMES: &[&str] = &["dark", "light"];
|
|
@@ -25,6 +30,12 @@ pub struct Settings {
|
|
|
25
30
|
pub ui_language: String,
|
|
26
31
|
#[serde(default = "default_theme")]
|
|
27
32
|
pub theme: String,
|
|
33
|
+
#[serde(default = "default_difficulty")]
|
|
34
|
+
pub difficulty: String,
|
|
35
|
+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
36
|
+
pub topics: Vec<String>,
|
|
37
|
+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
38
|
+
pub avoid_topics: Vec<String>,
|
|
28
39
|
#[serde(default = "default_editor")]
|
|
29
40
|
pub editor: String,
|
|
30
41
|
#[serde(default = "default_next_source")]
|
|
@@ -43,6 +54,9 @@ impl Default for Settings {
|
|
|
43
54
|
language: default_language(),
|
|
44
55
|
ui_language: default_ui_language(),
|
|
45
56
|
theme: default_theme(),
|
|
57
|
+
difficulty: default_difficulty(),
|
|
58
|
+
topics: Vec::new(),
|
|
59
|
+
avoid_topics: Vec::new(),
|
|
46
60
|
editor: default_editor(),
|
|
47
61
|
next_source: default_next_source(),
|
|
48
62
|
ai_provider: default_ai_provider(),
|
|
@@ -364,6 +378,9 @@ pub fn normalize_settings(settings: &mut Settings) {
|
|
|
364
378
|
if !THEMES.contains(&settings.theme.as_str()) {
|
|
365
379
|
settings.theme = "dark".to_string();
|
|
366
380
|
}
|
|
381
|
+
settings.difficulty = normalize_difficulty(&settings.difficulty);
|
|
382
|
+
settings.topics = normalize_topic_list(&settings.topics);
|
|
383
|
+
settings.avoid_topics = normalize_topic_list(&settings.avoid_topics);
|
|
367
384
|
settings.next_source = normalize_next_source(&settings.next_source);
|
|
368
385
|
settings.ai_provider = normalize_ai_provider(&settings.ai_provider);
|
|
369
386
|
if settings.ai_model.trim().is_empty() {
|
|
@@ -376,8 +393,9 @@ pub fn problem_by_id<'a>(bank: &'a [Problem], problem_id: &str) -> Option<&'a Pr
|
|
|
376
393
|
}
|
|
377
394
|
|
|
378
395
|
pub fn normalize_language(language: &str) -> String {
|
|
379
|
-
|
|
380
|
-
|
|
396
|
+
let language = language.trim().to_lowercase();
|
|
397
|
+
if LANGUAGES.contains(&language.as_str()) {
|
|
398
|
+
language
|
|
381
399
|
} else {
|
|
382
400
|
"python".to_string()
|
|
383
401
|
}
|
|
@@ -601,8 +619,8 @@ pub fn judge(root: &Path, problem: &Problem, settings: &Settings) -> JudgeResult
|
|
|
601
619
|
let run = match run_capture(&mut process, &case.input, Duration::from_secs(5)) {
|
|
602
620
|
Ok(run) => run,
|
|
603
621
|
Err(error) => {
|
|
604
|
-
lines.push(format!("
|
|
605
|
-
lines
|
|
622
|
+
lines.push(format!("Case {}: FAIL", index + 1));
|
|
623
|
+
push_labeled_block(&mut lines, "Error", &error.to_string());
|
|
606
624
|
break;
|
|
607
625
|
}
|
|
608
626
|
};
|
|
@@ -610,28 +628,20 @@ pub fn judge(root: &Path, problem: &Problem, settings: &Settings) -> JudgeResult
|
|
|
610
628
|
let expected = case.output.trim();
|
|
611
629
|
if !run.timed_out && run.code == Some(0) && got == expected {
|
|
612
630
|
passed += 1;
|
|
613
|
-
lines.push(format!("
|
|
631
|
+
lines.push(format!("Case {}: PASS", index + 1));
|
|
614
632
|
if !run.stderr.trim().is_empty() {
|
|
615
|
-
lines
|
|
616
|
-
lines.push(run.stderr.trim_end().to_string());
|
|
633
|
+
push_labeled_block(&mut lines, "Stderr", run.stderr.trim_end());
|
|
617
634
|
}
|
|
618
635
|
} else {
|
|
619
|
-
lines.push(format!("
|
|
636
|
+
lines.push(format!("Case {}: FAIL", index + 1));
|
|
620
637
|
if run.timed_out {
|
|
621
|
-
lines
|
|
638
|
+
push_labeled_block(&mut lines, "Error", "timeout: 5s");
|
|
622
639
|
}
|
|
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
|
-
});
|
|
640
|
+
push_labeled_block(&mut lines, "Input", case.input.trim_end());
|
|
641
|
+
push_labeled_block(&mut lines, "Expected", expected);
|
|
642
|
+
push_labeled_block(&mut lines, "Got", run.stdout.trim_end());
|
|
632
643
|
if !run.stderr.trim().is_empty() {
|
|
633
|
-
lines
|
|
634
|
-
lines.push(run.stderr.trim_end().to_string());
|
|
644
|
+
push_labeled_block(&mut lines, "Stderr", run.stderr.trim_end());
|
|
635
645
|
}
|
|
636
646
|
break;
|
|
637
647
|
}
|
|
@@ -645,6 +655,16 @@ pub fn judge(root: &Path, problem: &Problem, settings: &Settings) -> JudgeResult
|
|
|
645
655
|
}
|
|
646
656
|
}
|
|
647
657
|
|
|
658
|
+
fn push_labeled_block(lines: &mut Vec<String>, label: &str, body: &str) {
|
|
659
|
+
lines.push(String::new());
|
|
660
|
+
lines.push(label.to_string());
|
|
661
|
+
if body.is_empty() {
|
|
662
|
+
lines.push(" <empty>".to_string());
|
|
663
|
+
} else {
|
|
664
|
+
lines.extend(body.lines().map(|line| format!(" {line}")));
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
648
668
|
pub fn command_for(root: &Path, path: &Path, language: &str) -> Result<Option<CommandSpec>> {
|
|
649
669
|
match language {
|
|
650
670
|
"python" => Ok(which("python3")
|
|
@@ -754,7 +774,11 @@ pub fn next_problem(
|
|
|
754
774
|
.iter()
|
|
755
775
|
.map(|item| item.id.as_str())
|
|
756
776
|
.collect::<Vec<_>>();
|
|
757
|
-
let preferred =
|
|
777
|
+
let preferred = if state.settings.difficulty == "auto" {
|
|
778
|
+
&state.suggested_next_difficulty
|
|
779
|
+
} else {
|
|
780
|
+
&state.settings.difficulty
|
|
781
|
+
};
|
|
758
782
|
let problem = bank
|
|
759
783
|
.iter()
|
|
760
784
|
.find(|item| !seen.contains(&item.id.as_str()) && &item.difficulty == preferred)
|
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
|
}
|