practicode 0.1.9 → 0.1.11

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.
@@ -0,0 +1,109 @@
1
+ use super::*;
2
+
3
+ impl PracticodeApp {
4
+ pub(super) fn status_text(&self) -> String {
5
+ let code_status = self.submission_status(&self.problem).0;
6
+ let activity = if self.busy_label.is_empty() {
7
+ "idle".to_string()
8
+ } else {
9
+ format!("{}{}", self.busy_body, self.busy_dots())
10
+ };
11
+ let tail = if let Some(version) = self.update_notice.as_ref() {
12
+ format!(
13
+ "{}:{version} /update",
14
+ ui_text(&self.state.settings.ui_language, "update")
15
+ )
16
+ } else if self.task_rx.is_some() {
17
+ self.mode_hint().to_string()
18
+ } else if let Some(status) = self.background_generation_status() {
19
+ status
20
+ } else {
21
+ self.mode_hint().to_string()
22
+ };
23
+ format!(
24
+ " PRACTICODE | {} | {} | {} | {} | code:{} | {} | {} ",
25
+ self.problem.id,
26
+ self.problem.difficulty,
27
+ self.problem_status(&self.problem),
28
+ activity,
29
+ code_status,
30
+ self.state.settings.language,
31
+ tail,
32
+ )
33
+ }
34
+
35
+ pub(super) fn next_source_help(&self) -> String {
36
+ "Next behavior: /next opens unsolved local problems first and asks AI only when none remain. Use /generate <request> to create a problem in the background.".to_string()
37
+ }
38
+
39
+ pub(super) fn background_generation_status(&self) -> Option<String> {
40
+ if self.generate_rx.is_some() {
41
+ let elapsed = self
42
+ .generate_started
43
+ .map(|started| started.elapsed().as_secs())
44
+ .unwrap_or_default();
45
+ Some(format!("bg generate {elapsed}s"))
46
+ } else {
47
+ self.generate_notice.clone()
48
+ }
49
+ }
50
+
51
+ pub(super) fn busy_dots(&self) -> String {
52
+ ".".repeat((self.busy_frame / 8) % 4)
53
+ }
54
+
55
+ pub(super) fn busy_game_track(&self) -> String {
56
+ let width = 9;
57
+ let target = width / 2;
58
+ let position = (self.busy_frame / 2) % width;
59
+ let mut cells = vec!['-'; width];
60
+ cells[target] = '|';
61
+ cells[position] = if position == target { 'X' } else { '*' };
62
+ format!("[{}]", cells.into_iter().collect::<String>())
63
+ }
64
+
65
+ pub(super) fn busy_game_on_target(&self) -> bool {
66
+ (self.busy_frame / 2) % 9 == 4
67
+ }
68
+
69
+ pub(super) fn mode_hint(&self) -> &'static str {
70
+ let lang = &self.state.settings.ui_language;
71
+ if self.task_rx.is_some() {
72
+ return if self.busy_label == "next" {
73
+ ui_text(lang, "hint_busy_next")
74
+ } else {
75
+ ui_text(lang, "hint_busy")
76
+ };
77
+ }
78
+ if self.editing_notes {
79
+ return "notes: type to edit, Esc profile";
80
+ }
81
+ match (self.focus, self.list_cursor.is_some(), self.show_output) {
82
+ (Focus::Command, _, _) => ui_text(lang, "hint_command"),
83
+ (_, true, _) => ui_text(lang, "hint_list"),
84
+ (_, _, true) if self.settings_cursor.is_some() => ui_text(lang, "hint_settings"),
85
+ (_, _, true) => ui_text(lang, "hint_output"),
86
+ (Focus::Code, _, _) => ui_text(lang, "hint_code"),
87
+ _ => ui_text(lang, "hint_idle"),
88
+ }
89
+ }
90
+
91
+ pub(super) fn help_text(&self) -> String {
92
+ let lang = &self.state.settings.ui_language;
93
+ let commands = COMMAND_HINTS
94
+ .iter()
95
+ .filter(|hint| hint.help)
96
+ .map(|hint| format!("- `{}` {}", hint.display, ui_text(lang, hint.desc_key)))
97
+ .collect::<Vec<_>>()
98
+ .join("\n");
99
+ format!(
100
+ "# {}\n\n## {}\n\n1. Type code in the right pane.\n2. Press `Esc`, then choose `/run` from the command palette.\n3. Use `/next` when it passes.\n\n## {}\n\n{}\n\n## {}\n\n- `/` opens the command palette outside the editor.\n- `↑/↓` selects a command and `Enter` accepts it.\n- `Esc` cancels the command palette or leaves output.\n\n## {}\n\n- stdout is shown when a case fails.\n- stderr is shown without affecting the expected stdout.",
101
+ ui_text(lang, "help_title"),
102
+ ui_text(lang, "daily_loop"),
103
+ ui_text(lang, "commands"),
104
+ commands,
105
+ ui_text(lang, "keys"),
106
+ ui_text(lang, "debug_prints"),
107
+ )
108
+ }
109
+ }
@@ -0,0 +1,303 @@
1
+ use super::*;
2
+
3
+ impl PracticodeApp {
4
+ pub(super) fn start_ai_prompt(&mut self, prompt: &str) -> Result<()> {
5
+ if self.task_rx.is_some() {
6
+ self.write_text_output(ui_text(&self.state.settings.ui_language, "already_busy"));
7
+ return Ok(());
8
+ }
9
+ self.save_code()?;
10
+ let label = normalize_ai_provider(&self.state.settings.ai_provider);
11
+ self.start_busy("ai", &format!("{label} is thinking"));
12
+ let root = self.root.clone();
13
+ let problem = self.problem.clone();
14
+ let settings = self.state.settings.clone();
15
+ let prompt = prompt.to_string();
16
+ let (tx, rx) = mpsc::channel();
17
+ thread::spawn(move || {
18
+ let output = run_ai_prompt(&root, &problem, &settings, &prompt);
19
+ let _ = tx.send(TaskResult::AiPrompt(output));
20
+ });
21
+ self.task_rx = Some(rx);
22
+ Ok(())
23
+ }
24
+
25
+ pub(super) fn check_task(&mut self) {
26
+ let task = self.task_rx.as_ref().and_then(|rx| rx.try_recv().ok());
27
+ if let Some(task) = task {
28
+ self.task_rx = None;
29
+ self.stop_busy();
30
+ match task {
31
+ TaskResult::AiPrompt(output) => self.write_output(&output),
32
+ TaskResult::Next {
33
+ output,
34
+ old_problem,
35
+ fallback_to_local,
36
+ } => {
37
+ if let Err(error) =
38
+ self.finish_next_problem(output, old_problem, fallback_to_local)
39
+ {
40
+ self.write_text_output(&format!("Next failed\n{error}"));
41
+ }
42
+ }
43
+ }
44
+ }
45
+ }
46
+
47
+ pub(super) fn check_background_generation(&mut self) {
48
+ let output = self.generate_rx.as_ref().and_then(|rx| rx.try_recv().ok());
49
+ let Some(output) = output else {
50
+ return;
51
+ };
52
+ self.generate_rx = None;
53
+ self.generate_started = None;
54
+ let old_len = self.generate_bank_len;
55
+ match load_bank(&self.root) {
56
+ Ok(bank) => {
57
+ let added = bank.len().saturating_sub(old_len);
58
+ self.bank = bank;
59
+ let _ = save_state(&self.root, &self.state);
60
+ self.generate_notice = Some(if added > 0 {
61
+ format!("Generated {added} problem in background. Use /next.")
62
+ } else if output.contains("failed") {
63
+ "Background generation failed. Use /generate to retry.".to_string()
64
+ } else {
65
+ "Background generation finished. Use /problems to review.".to_string()
66
+ });
67
+ }
68
+ Err(error) => {
69
+ self.generate_notice = Some(format!(
70
+ "Background generation finished, but bank reload failed: {error}"
71
+ ));
72
+ }
73
+ }
74
+ }
75
+
76
+ pub(super) fn check_update(&mut self) {
77
+ let result = self.update_rx.as_ref().and_then(|rx| rx.try_recv().ok());
78
+ if let Some(result) = result {
79
+ self.update_rx = None;
80
+ self.update_check = Some(result.clone());
81
+ match &result {
82
+ UpdateCheck::Available(version) => self.update_notice = Some(version.clone()),
83
+ UpdateCheck::Current | UpdateCheck::Disabled => self.update_notice = None,
84
+ UpdateCheck::Failed => {}
85
+ }
86
+ }
87
+ }
88
+
89
+ pub(super) fn start_update_check(&mut self) {
90
+ if self.update_rx.is_some() {
91
+ return;
92
+ }
93
+ self.last_update_check = Some(Instant::now());
94
+ let (tx, rx) = mpsc::channel();
95
+ thread::spawn(move || {
96
+ let _ = tx.send(check_latest_version());
97
+ });
98
+ self.update_rx = Some(rx);
99
+ }
100
+
101
+ pub(super) fn maybe_start_periodic_update_check(&mut self) {
102
+ if self.update_rx.is_some() {
103
+ return;
104
+ }
105
+ if self
106
+ .last_update_check
107
+ .is_none_or(|last| last.elapsed() >= UPDATE_CHECK_INTERVAL)
108
+ {
109
+ self.start_update_check();
110
+ }
111
+ }
112
+
113
+ pub(super) fn start_model_check(&mut self) {
114
+ let provider = self.state.settings.ai_provider.clone();
115
+ if self.model_rx.is_some() || self.available_models_provider == provider {
116
+ return;
117
+ }
118
+ let query_provider = provider.clone();
119
+ let (tx, rx) = mpsc::channel();
120
+ thread::spawn(move || {
121
+ let _ = tx.send(available_models(&query_provider));
122
+ });
123
+ self.available_models_provider = provider;
124
+ self.model_rx = Some(rx);
125
+ }
126
+
127
+ pub(super) fn check_models(&mut self) {
128
+ let models = self.model_rx.as_ref().and_then(|rx| rx.try_recv().ok());
129
+ if let Some(catalog) = models {
130
+ self.model_rx = None;
131
+ self.available_models = catalog.models;
132
+ self.model_message = catalog.message;
133
+ if self.showing_model_status {
134
+ self.output = self.model_status_text();
135
+ self.output_is_markdown = false;
136
+ self.show_output = true;
137
+ } else if self.settings_cursor.is_some() {
138
+ self.output = self.profile_text();
139
+ self.output_is_markdown = false;
140
+ self.show_output = true;
141
+ }
142
+ }
143
+ }
144
+
145
+ pub(super) fn model_status_text(&self) -> String {
146
+ let mut lines = vec![
147
+ format!("AI provider: {}", self.state.settings.ai_provider),
148
+ format!(
149
+ "AI model: {}",
150
+ if self.state.settings.ai_model == "auto" {
151
+ "auto (provider default)"
152
+ } else {
153
+ self.state.settings.ai_model.as_str()
154
+ }
155
+ ),
156
+ format!(
157
+ "AI effort: {}",
158
+ if self.state.settings.ai_effort == "auto" {
159
+ "auto (provider default)"
160
+ } else {
161
+ self.state.settings.ai_effort.as_str()
162
+ }
163
+ ),
164
+ "Use /model auto to let the provider choose its default.".to_string(),
165
+ "Use /effort auto to let the provider choose its default.".to_string(),
166
+ ];
167
+ if self.model_rx.is_some() {
168
+ lines.push("Loading provider model list...".to_string());
169
+ } else if self.available_models.is_empty() {
170
+ lines.push(
171
+ self.model_message
172
+ .clone()
173
+ .unwrap_or_else(|| "Provider model list is unavailable.".to_string()),
174
+ );
175
+ lines.push("Use /model <name> for a known model.".to_string());
176
+ } else {
177
+ if let Some(message) = &self.model_message {
178
+ lines.push(message.clone());
179
+ }
180
+ let efforts = if self.state.settings.ai_provider == "claude" {
181
+ CLAUDE_AI_EFFORTS
182
+ } else {
183
+ CODEX_AI_EFFORTS
184
+ };
185
+ lines.push(format!("Available efforts: {}", efforts.join(", ")));
186
+ lines.push("Available models:".to_string());
187
+ lines.extend(
188
+ self.available_models
189
+ .iter()
190
+ .map(|model| format!("- /model {model}")),
191
+ );
192
+ }
193
+ lines.join("\n")
194
+ }
195
+
196
+ pub(super) fn start_busy(&mut self, label: &str, body: &str) {
197
+ self.settings_cursor = None;
198
+ self.busy_label = label.to_string();
199
+ self.busy_body = body.to_string();
200
+ self.busy_started = Some(Instant::now());
201
+ self.busy_frame = 0;
202
+ self.busy_hits = 0;
203
+ self.busy_misses = 0;
204
+ self.show_output = true;
205
+ self.focus = Focus::Output;
206
+ }
207
+
208
+ pub(super) fn stop_busy(&mut self) {
209
+ self.busy_label.clear();
210
+ self.busy_body.clear();
211
+ self.busy_started = None;
212
+ self.busy_frame = 0;
213
+ }
214
+
215
+ pub(super) fn handle_busy_key(&mut self, key: KeyEvent) -> bool {
216
+ if self.task_rx.is_none() {
217
+ return false;
218
+ }
219
+ if key.code == KeyCode::Char('q') && key.modifiers.is_empty() {
220
+ self.should_quit = true;
221
+ } else if self.busy_label == "next"
222
+ && key.code == KeyCode::Char(' ')
223
+ && key.modifiers.is_empty()
224
+ {
225
+ if self.busy_game_on_target() {
226
+ self.busy_hits += 1;
227
+ } else {
228
+ self.busy_misses += 1;
229
+ }
230
+ }
231
+ self.focus = Focus::Output;
232
+ true
233
+ }
234
+
235
+ pub(super) fn write_output(&mut self, output: &str) {
236
+ self.settings_cursor = None;
237
+ self.editing_notes = false;
238
+ self.showing_model_status = false;
239
+ self.output = output.to_string();
240
+ self.output_is_markdown = true;
241
+ self.show_output = true;
242
+ self.focus = Focus::Output;
243
+ }
244
+
245
+ pub(super) fn write_text_output(&mut self, output: &str) {
246
+ self.settings_cursor = None;
247
+ self.editing_notes = false;
248
+ self.showing_model_status = false;
249
+ self.output = output.trim_end().to_string();
250
+ self.output_is_markdown = false;
251
+ self.show_output = true;
252
+ self.focus = Focus::Output;
253
+ }
254
+
255
+ pub(super) fn write_model_status(&mut self) {
256
+ self.output = self.model_status_text();
257
+ self.output_is_markdown = false;
258
+ self.showing_model_status = true;
259
+ self.show_output = true;
260
+ self.focus = Focus::Output;
261
+ }
262
+
263
+ pub(super) fn refresh_update_notice(&mut self) {
264
+ self.update_check = None;
265
+ self.update_notice = None;
266
+ self.start_update_check();
267
+ self.show_update_notice();
268
+ }
269
+
270
+ pub(super) fn show_update_notice(&mut self) {
271
+ let lang = self.state.settings.ui_language.clone();
272
+ if let Some(version) = &self.update_notice {
273
+ self.write_text_output(&format!(
274
+ "{}: practicode {version} (current {CURRENT_VERSION})\n\nnpm update -g practicode\ncargo install --force practicode",
275
+ ui_text(&lang, "update_available")
276
+ ));
277
+ } else if self.update_rx.is_some() {
278
+ self.write_text_output("Checking for updates...");
279
+ } else if matches!(self.update_check, Some(UpdateCheck::Disabled)) {
280
+ self.write_text_output(ui_text(&lang, "update_check_disabled"));
281
+ } else if matches!(self.update_check, Some(UpdateCheck::Failed)) {
282
+ self.write_text_output(ui_text(&lang, "update_check_failed"));
283
+ } else {
284
+ self.write_text_output(ui_text(&lang, "update_none"));
285
+ }
286
+ }
287
+
288
+ pub(super) fn append_note(&mut self, note: &str) -> Result<()> {
289
+ append_problem_note(&self.root, note)?;
290
+ self.write_text_output(&format!("Problem note saved to {PROBLEM_NOTES_PATH}."));
291
+ Ok(())
292
+ }
293
+
294
+ pub(super) fn show_notes(&mut self) -> Result<()> {
295
+ let notes = read_problem_notes(&self.root)?;
296
+ if notes.is_empty() {
297
+ self.write_text_output("No notes yet. Use /note to edit problem-generation notes.");
298
+ } else {
299
+ self.write_text_output(&format!("Problem notes ({PROBLEM_NOTES_PATH})\n\n{notes}"));
300
+ }
301
+ Ok(())
302
+ }
303
+ }