practicode 0.1.5 → 0.1.7

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/README.md CHANGED
@@ -12,7 +12,14 @@ Personal coding practice, right in your terminal.
12
12
 
13
13
  `practicode` is a small Rust TUI for stdin/stdout practice: problem on the left, code on the right, judge loop in the same terminal.
14
14
 
15
- ## Start
15
+ ## What You Get
16
+
17
+ - Local stdin/stdout judging for Python, TypeScript, Java, and Rust.
18
+ - A two-pane terminal UI with problem text, editor, output, and command palette.
19
+ - Local-first problem history under ignored `.practicode/`, `problems/`, and `submissions/` paths.
20
+ - Optional Codex or Claude Code help for hints and generated next problems.
21
+
22
+ ## Install
16
23
 
17
24
  ### Prerequisites
18
25
 
@@ -43,6 +50,14 @@ npm install
43
50
  npm start
44
51
  ```
45
52
 
53
+ ### Check Install
54
+
55
+ ```bash
56
+ practicode --version
57
+ practicode --smoke
58
+ practicode --help
59
+ ```
60
+
46
61
  ## Daily Loop
47
62
 
48
63
  The code editor starts focused.
@@ -54,44 +69,60 @@ choose /run
54
69
  choose /next when it passes
55
70
  ```
56
71
 
57
- Typing `/` outside the editor opens the command palette. Use `up/down` to move, `Enter` to run or complete the selected command, and `Esc` to cancel.
72
+ Typing `/` outside the editor opens the command palette. Use `up/down` to move, `Enter` to run or complete the selected command, and `Esc` to cancel. Press `?` for in-app help or `Ctrl+C` to quit.
58
73
 
59
74
  Submissions are saved as you type under `submissions/<problem-id>/solution.<ext>`.
60
75
 
76
+ ## CLI Flags
77
+
78
+ | Flag | Action |
79
+ | --- | --- |
80
+ | `--help`, `-h` | Show non-interactive help |
81
+ | `--version`, `-V` | Print the installed version |
82
+ | `--smoke` | Print the current problem title and exit |
83
+
61
84
  ## Commands
62
85
 
63
86
  | Command | Action |
64
87
  | --- | --- |
65
88
  | `/run` | Judge the current submission |
66
89
  | `/code` | Return to the code editor |
67
- | `/next` | Open the next local problem, or ask AI to create one |
68
- | `/next easy string problem` | Ask AI for a custom next problem |
69
- | `/prev` | Go back through problem history |
70
- | `/list` | Browse problems with `up/down` or `j/k`, open with `Enter` |
90
+ | `/next` | Open the next unsolved problem, or ask AI only when none remain |
91
+ | `/generate easy string problem` | Ask AI to create a new problem now |
92
+ | `/back` | Go back through problem history |
93
+ | `/problems` | Browse problems with `up/down` or `j/k`, open with `Enter` |
71
94
  | `/open 2` | Open by number, id, or slug |
72
- | `/giveup` | Show the reference answer |
95
+ | `/answer` | Show the reference answer |
73
96
  | `/hint` | Ask the selected AI for a concise hint |
74
97
  | `/hint explain my bug` | Ask the selected AI about the current problem and submission |
98
+ | `/profile` | Show your current practice profile |
99
+ | `/difficulty auto` | Set difficulty preference: `auto`, `easy`, `medium`, `hard` |
100
+ | `/topics arrays, strings` | Set preferred topics for future problems |
101
+ | `/avoid dp, graph` | Set topics to avoid in future problems |
75
102
  | `/provider codex` | Set AI provider and show local CLI/daemon status |
76
103
  | `/model auto` | Use the provider default model for `/hint` and AI-backed `/next` |
77
- | `/note prefer hashmap practice` | Append a standing note for future problem generation |
78
- | `/notes` | Show your local next-problem notes |
79
- | `/lang python` | Set code language: `python`, `ts`, `java`, `rust` |
104
+ | `/language python` | Set code language: `python`, `ts`, `java`, `rust` |
80
105
  | `/ui en` | Set UI language: `en`, `ko`, `ja`, `zh`, `es` |
81
106
  | `/theme dark` | Set theme: `dark` or `light` |
82
107
  | `/update` | Show update instructions when a newer version is available |
83
108
  | `/exit` | Quit |
84
109
 
110
+ Older command names such as `/prev`, `/list`, `/giveup`, and `/lang` still work as aliases.
111
+
85
112
  The default UI language is English. Switch it any time with `/ui ko`, `/ui ja`, `/ui zh`, or `/ui es`.
86
113
 
87
- ## AI Problems
114
+ 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.
115
+
116
+ ## Problem Flow
117
+
118
+ `/next` is local-first: it opens the next unsolved local problem before generating anything. When no unsolved problem remains, it asks the selected AI provider to create one.
88
119
 
89
- `/next <request>` passes your request into the selected AI problem generator.
120
+ Use `/generate <request>` when you explicitly want to create a new problem now.
90
121
 
91
122
  ```text
92
- /next a slightly harder string problem
93
- /next hashmap practice, easy
94
- /next sorting problem, no graph yet
123
+ /generate a slightly harder string problem
124
+ /generate hashmap practice, easy
125
+ /generate sorting problem, no graph yet
95
126
  ```
96
127
 
97
128
  Codex is the default provider:
@@ -129,9 +160,22 @@ npm update -g practicode
129
160
  cargo install --force practicode
130
161
  ```
131
162
 
132
- ## Safety
163
+ ## Safety And Security
133
164
 
134
- `/run` executes your local submission as a normal process. practicode runs it from `.practicode/build/<problem-id>/run`, but this is not an OS sandbox. Only run code you trust.
165
+ - `/run` executes your local submission as a normal process. practicode runs it from `.practicode/build/<problem-id>/run`, but this is not an OS sandbox. Only run code you trust.
166
+ - `/hint` sends the current problem and submission to the selected AI provider CLI.
167
+ - AI-backed `/next` can run a custom shell command from `settings.ai_next_command`; save only commands you trust.
168
+ - Local `.env`, `.npmrc`, `.practicode/`, `problems/`, and `submissions/` are ignored by git. Do not commit tokens, private prompts, or answer keys.
169
+
170
+ ## Development Checks
171
+
172
+ ```bash
173
+ cargo test
174
+ cargo run -- --smoke
175
+ cargo audit
176
+ ```
177
+
178
+ This repo has no npm dependencies or lockfile today, so `npm audit` and `pnpm audit` are not applicable until a matching lockfile is added.
135
179
 
136
180
  ## Contributing
137
181
 
@@ -139,6 +183,8 @@ External contributions use the fork and pull request flow in [docs/CONTRIBUTING.
139
183
 
140
184
  Maintainer-only review and release notes live in [docs/MAINTAINING.md](docs/MAINTAINING.md).
141
185
 
186
+ Code layout and extension boundaries live in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
187
+
142
188
  ## License
143
189
 
144
190
  practicode is MIT licensed. Third-party dependency license notes are in [THIRD_PARTY_LICENSES.md](THIRD_PARTY_LICENSES.md).
@@ -26,12 +26,17 @@
26
26
  "cmd_code": "Return to the code editor",
27
27
  "cmd_edit": "Return to the code editor",
28
28
  "cmd_next": "Open the next problem",
29
+ "cmd_generate": "Generate a new problem now",
29
30
  "cmd_prev": "Open the previous problem",
30
31
  "cmd_list": "Browse problems",
31
32
  "cmd_open": "Open by number, id, or slug",
32
33
  "cmd_giveup": "Show the reference answer",
33
34
  "cmd_hint": "Ask for a hint about the current problem",
34
35
  "cmd_ai": "Ask AI about the current problem and code",
36
+ "cmd_profile": "Show practice profile",
37
+ "cmd_difficulty": "Set preferred difficulty",
38
+ "cmd_topics": "Set preferred topics",
39
+ "cmd_avoid": "Set topics to avoid",
35
40
  "cmd_provider": "Set AI provider",
36
41
  "cmd_model": "Set AI model",
37
42
  "cmd_model_auto": "Use provider default model",
@@ -26,12 +26,17 @@
26
26
  "cmd_code": "Volver al editor de codigo",
27
27
  "cmd_edit": "Volver al editor de codigo",
28
28
  "cmd_next": "Abrir el siguiente problema",
29
+ "cmd_generate": "Generar un problema nuevo ahora",
29
30
  "cmd_prev": "Abrir el problema anterior",
30
31
  "cmd_list": "Abrir la lista de problemas",
31
32
  "cmd_open": "Abrir por numero, id o slug",
32
33
  "cmd_giveup": "Mostrar la respuesta de referencia",
33
34
  "cmd_hint": "Pedir una pista para el problema actual",
34
35
  "cmd_ai": "Preguntar a AI sobre el problema y codigo actuales",
36
+ "cmd_profile": "Mostrar perfil de practica",
37
+ "cmd_difficulty": "Configurar dificultad preferida",
38
+ "cmd_topics": "Configurar temas preferidos",
39
+ "cmd_avoid": "Configurar temas a evitar",
35
40
  "cmd_provider": "Configurar AI provider",
36
41
  "cmd_model": "Configurar AI model",
37
42
  "cmd_model_auto": "Usar el modelo predeterminado del provider",
@@ -26,12 +26,17 @@
26
26
  "cmd_code": "コードエディタに戻る",
27
27
  "cmd_edit": "コードエディタに戻る",
28
28
  "cmd_next": "次の問題を開く",
29
+ "cmd_generate": "新しい問題を今すぐ生成",
29
30
  "cmd_prev": "前の問題を開く",
30
31
  "cmd_list": "問題一覧を開く",
31
32
  "cmd_open": "番号、id、slug で問題を開く",
32
33
  "cmd_giveup": "解答を見る",
33
34
  "cmd_hint": "現在の問題のヒントを依頼",
34
35
  "cmd_ai": "現在の問題とコードについて AI に質問",
36
+ "cmd_profile": "練習プロファイルを表示",
37
+ "cmd_difficulty": "希望難易度を設定",
38
+ "cmd_topics": "希望トピックを設定",
39
+ "cmd_avoid": "避けるトピックを設定",
35
40
  "cmd_provider": "AI provider を設定",
36
41
  "cmd_model": "AI model を設定",
37
42
  "cmd_model_auto": "provider の既定モデルを使用",
@@ -26,12 +26,17 @@
26
26
  "cmd_code": "코드 편집기로 돌아가기",
27
27
  "cmd_edit": "코드 편집기로 돌아가기",
28
28
  "cmd_next": "다음 문제 열기",
29
+ "cmd_generate": "새 문제를 바로 생성",
29
30
  "cmd_prev": "이전 문제 열기",
30
31
  "cmd_list": "문제 목록 열기",
31
32
  "cmd_open": "번호, id, slug로 문제 열기",
32
33
  "cmd_giveup": "정답 보기",
33
34
  "cmd_hint": "현재 문제 힌트 요청",
34
35
  "cmd_ai": "현재 문제와 코드에 대해 AI에게 질문",
36
+ "cmd_profile": "연습 프로파일 보기",
37
+ "cmd_difficulty": "선호 난이도 설정",
38
+ "cmd_topics": "선호 주제 설정",
39
+ "cmd_avoid": "피할 주제 설정",
35
40
  "cmd_provider": "AI provider 설정",
36
41
  "cmd_model": "AI model 설정",
37
42
  "cmd_model_auto": "provider 기본 모델 사용",
@@ -26,12 +26,17 @@
26
26
  "cmd_code": "回到代码编辑器",
27
27
  "cmd_edit": "回到代码编辑器",
28
28
  "cmd_next": "打开下一题",
29
+ "cmd_generate": "立即生成新题",
29
30
  "cmd_prev": "打开上一题",
30
31
  "cmd_list": "打开题目列表",
31
32
  "cmd_open": "按编号、id 或 slug 打开题目",
32
33
  "cmd_giveup": "显示参考答案",
33
34
  "cmd_hint": "请求当前题目的提示",
34
35
  "cmd_ai": "向 AI 询问当前题目和代码",
36
+ "cmd_profile": "显示练习配置",
37
+ "cmd_difficulty": "设置偏好难度",
38
+ "cmd_topics": "设置偏好主题",
39
+ "cmd_avoid": "设置要避开的主题",
35
40
  "cmd_provider": "设置 AI provider",
36
41
  "cmd_model": "设置 AI model",
37
42
  "cmd_model_auto": "使用 provider 默认模型",
@@ -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="64" y="52" width="1072" height="616" rx="10" fill="#0b111a" stroke="#1f6f7a" stroke-width="2"/>
4
- <rect x="88" y="78" width="594" height="440" rx="4" fill="#0f1720" stroke="#00c2d1" stroke-width="2"/>
5
- <rect x="704" y="78" width="408" height="440" rx="4" fill="#0d141c" stroke="#31536b" stroke-width="2"/>
6
- <text x="112" y="112" fill="#d7e1ec" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="18" font-weight="700">Problem</text>
7
- <text x="728" y="112" fill="#d7e1ec" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="18" font-weight="700">solution.py</text>
8
- <text x="112" y="158" fill="#f8fafc" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="22" font-weight="700">001. Hello World</text>
9
- <text x="112" y="204" fill="#c8d3f5" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="16">Difficulty: easy</text>
10
- <text x="112" y="232" fill="#c8d3f5" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="16">Topics: io</text>
11
- <text x="112" y="286" fill="#f8fafc" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="17">Print exactly Hello, World! to stdout.</text>
12
- <text x="112" y="352" fill="#00a8ff" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="17" text-decoration="underline">Input</text>
13
- <text x="112" y="386" fill="#f8fafc" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="17">No input.</text>
14
- <text x="112" y="442" fill="#00a8ff" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="17" text-decoration="underline">Output</text>
15
- <text x="112" y="476" fill="#f6c177" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="17">Hello, World!</text>
16
- <text x="728" y="158" fill="#6b7280" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="17"> 1</text>
17
- <text x="776" y="158" fill="#7dd3fc" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="17">print</text>
18
- <text x="828" y="158" fill="#f8fafc" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="17">('Hello, World!')</text>
19
- <rect x="64" y="536" width="1072" height="28" fill="#152033"/>
20
- <text x="84" y="556" fill="#c8d3f5" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="15" font-weight="700">PRACTICODE | 001-hello-world | easy | idle | solved | code:written | python | next:ai | ai:codex/auto</text>
21
- <rect x="64" y="586" width="1072" height="58" rx="2" fill="#0b1017" stroke="#008cff" stroke-width="2"/>
22
- <text x="84" y="606" fill="#c8d3f5" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="15" font-weight="700">Command</text>
23
- <text x="84" y="630" fill="#f8fafc" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="18">/ai hint about edge cases</text>
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">&gt; 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 &lt;request&gt; /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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "practicode",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Local-first coding-test practice in a Rust terminal UI with optional AI help.",
5
5
  "license": "MIT",
6
6
  "repository": {
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(&default_ai_next_prompt(request)));
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(&default_ai_next_prompt(request)));
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
- if LANGUAGES.contains(&language) {
380
- language.to_string()
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
  }
@@ -756,7 +774,11 @@ pub fn next_problem(
756
774
  .iter()
757
775
  .map(|item| item.id.as_str())
758
776
  .collect::<Vec<_>>();
759
- let preferred = &state.suggested_next_difficulty;
777
+ let preferred = if state.settings.difficulty == "auto" {
778
+ &state.suggested_next_difficulty
779
+ } else {
780
+ &state.settings.difficulty
781
+ };
760
782
  let problem = bank
761
783
  .iter()
762
784
  .find(|item| !seen.contains(&item.id.as_str()) && &item.difficulty == preferred)
package/src/lib.rs CHANGED
@@ -14,9 +14,39 @@ pub mod text;
14
14
  pub mod tui;
15
15
  pub mod update;
16
16
 
17
+ pub fn cli_help_text() -> &'static str {
18
+ "practicode - local-first coding-test practice in your terminal
19
+
20
+ Usage:
21
+ practicode Open the TUI
22
+ practicode --smoke Print the current problem title and exit
23
+ practicode --version Print the installed version
24
+ practicode --help Show this help
25
+
26
+ Inside the TUI:
27
+ Esc then / Open the command palette
28
+ ? Open help
29
+ Ctrl+C Quit"
30
+ }
31
+
17
32
  pub fn run_cli() -> Result<()> {
18
33
  let root = env::current_dir().context("read current directory")?;
19
- if env::args().any(|arg| arg == "--smoke") {
34
+ let args = env::args().skip(1).collect::<Vec<_>>();
35
+ if args
36
+ .iter()
37
+ .any(|arg| matches!(arg.as_str(), "-h" | "--help"))
38
+ {
39
+ println!("{}", cli_help_text());
40
+ return Ok(());
41
+ }
42
+ if args
43
+ .iter()
44
+ .any(|arg| matches!(arg.as_str(), "-V" | "--version"))
45
+ {
46
+ println!("practicode {}", update::CURRENT_VERSION);
47
+ return Ok(());
48
+ }
49
+ if args.iter().any(|arg| arg == "--smoke") {
20
50
  let bank = core::load_bank(&root)?;
21
51
  let state = core::load_state(&root, &bank)?;
22
52
  let problem = core::problem_by_id(&bank, &state.current_problem).unwrap_or(&bank[0]);
@@ -39,3 +69,16 @@ pub fn run_cli() -> Result<()> {
39
69
  );
40
70
  result
41
71
  }
72
+
73
+ #[cfg(test)]
74
+ mod tests {
75
+ use super::cli_help_text;
76
+
77
+ #[test]
78
+ fn cli_help_lists_non_interactive_flags_and_tui_exit() {
79
+ let help = cli_help_text();
80
+ assert!(help.contains("--smoke"));
81
+ assert!(help.contains("--version"));
82
+ assert!(help.contains("Ctrl+C"));
83
+ }
84
+ }