practicode 0.1.8 → 0.1.10

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,312 @@
1
+ use super::*;
2
+
3
+ impl PracticodeApp {
4
+ pub(super) fn action_edit(&mut self) -> Result<()> {
5
+ self.load_code_editor()?;
6
+ self.settings_cursor = None;
7
+ self.show_output = false;
8
+ self.focus = Focus::Code;
9
+ Ok(())
10
+ }
11
+
12
+ pub(super) fn action_run(&mut self) -> Result<()> {
13
+ self.save_code()?;
14
+ let result = judge(&self.root, &self.problem, &self.state.settings);
15
+ if result.passed {
16
+ record_pass(&self.root, &self.problem, &mut self.state)?;
17
+ }
18
+ let headline = format!(
19
+ "{} {}/{}",
20
+ if result.passed { "PASS" } else { "FAIL" },
21
+ result.passed_cases,
22
+ result.total_cases
23
+ );
24
+ let next_step = if result.passed {
25
+ ui_text(&self.state.settings.ui_language, "run_pass_next")
26
+ } else {
27
+ ui_text(&self.state.settings.ui_language, "run_fail_next")
28
+ };
29
+ self.write_text_output(&format!("{headline}\n{}\n\n{next_step}", result.output));
30
+ Ok(())
31
+ }
32
+
33
+ pub(super) fn action_next(&mut self, request: &str) -> Result<()> {
34
+ self.check_background_generation();
35
+ let request = request.trim();
36
+ let old_problem = self.state.current_problem.clone();
37
+ if let Some(problem) = next_problem(&self.root, &self.bank, &mut self.state)? {
38
+ self.generate_notice = None;
39
+ self.problem = problem;
40
+ self.load_code_editor()?;
41
+ self.settings_cursor = None;
42
+ self.show_output = false;
43
+ self.focus = Focus::Code;
44
+ return Ok(());
45
+ }
46
+ if self.generate_rx.is_some() {
47
+ self.write_text_output(
48
+ "A background generation is already running. Keep solving; /next will pick up the new problem when it finishes.",
49
+ );
50
+ return Ok(());
51
+ }
52
+ self.start_next_problem(old_problem, true, request.to_string());
53
+ Ok(())
54
+ }
55
+
56
+ pub(super) fn action_generate(&mut self, request: &str) {
57
+ self.check_background_generation();
58
+ if self.task_rx.is_some() || self.generate_rx.is_some() {
59
+ let message = "Generation is already running; skipped duplicate /generate.";
60
+ self.generate_notice = Some(message.to_string());
61
+ self.write_text_output(message);
62
+ return;
63
+ }
64
+ self.start_background_generation(request.trim().to_string());
65
+ }
66
+
67
+ pub(super) fn start_background_generation(&mut self, request: String) {
68
+ let root = self.root.clone();
69
+ let state = self.state.clone();
70
+ let (tx, rx) = mpsc::channel();
71
+ thread::spawn(move || {
72
+ let _ = tx.send(run_ai_generate(&root, &state, &request));
73
+ });
74
+ self.generate_bank_len = self.bank.len();
75
+ self.generate_started = Some(Instant::now());
76
+ self.generate_notice = Some("Generating in background.".to_string());
77
+ self.generate_rx = Some(rx);
78
+ self.settings_cursor = None;
79
+ self.show_output = false;
80
+ self.focus = Focus::Code;
81
+ }
82
+
83
+ pub(super) fn start_next_problem(
84
+ &mut self,
85
+ old_problem: String,
86
+ fallback_to_local: bool,
87
+ request: String,
88
+ ) {
89
+ if self.task_rx.is_some() {
90
+ self.write_text_output(ui_text(&self.state.settings.ui_language, "already_busy"));
91
+ return;
92
+ }
93
+ self.start_busy(
94
+ "next",
95
+ ui_text(&self.state.settings.ui_language, "generating_next"),
96
+ );
97
+ let root = self.root.clone();
98
+ let state = self.state.clone();
99
+ let (tx, rx) = mpsc::channel();
100
+ thread::spawn(move || {
101
+ let output = run_ai_next(&root, &state, true, &request);
102
+ let _ = tx.send(TaskResult::Next {
103
+ output,
104
+ old_problem,
105
+ fallback_to_local,
106
+ });
107
+ });
108
+ self.task_rx = Some(rx);
109
+ }
110
+
111
+ pub(super) fn finish_next_problem(
112
+ &mut self,
113
+ output: String,
114
+ old_problem: String,
115
+ fallback_to_local: bool,
116
+ ) -> Result<()> {
117
+ self.bank = load_bank(&self.root)?;
118
+ self.state = load_state(&self.root, &self.bank)?;
119
+ self.problem = problem_by_id(&self.bank, &self.state.current_problem)
120
+ .cloned()
121
+ .unwrap_or_else(|| self.bank[0].clone());
122
+ if self.state.current_problem == old_problem {
123
+ if fallback_to_local
124
+ && let Some(problem) = next_problem(&self.root, &self.bank, &mut self.state)?
125
+ {
126
+ self.problem = problem;
127
+ } else {
128
+ self.write_text_output(&format!(
129
+ "{}{}No next problem is available yet.",
130
+ if output.is_empty() { "" } else { &output },
131
+ if output.is_empty() { "" } else { "\n\n" }
132
+ ));
133
+ return Ok(());
134
+ }
135
+ }
136
+ self.load_code_editor()?;
137
+ self.settings_cursor = None;
138
+ self.show_output = false;
139
+ self.focus = Focus::Code;
140
+ Ok(())
141
+ }
142
+
143
+ pub(super) fn action_previous(&mut self) -> Result<()> {
144
+ let old_problem = self.state.current_problem.clone();
145
+ self.problem = previous_problem(&self.root, &self.bank, &mut self.state)?;
146
+ if self.state.current_problem == old_problem {
147
+ self.write_text_output("Already at the first known problem.");
148
+ } else {
149
+ self.load_code_editor()?;
150
+ self.settings_cursor = None;
151
+ self.show_output = false;
152
+ self.focus = Focus::Code;
153
+ }
154
+ Ok(())
155
+ }
156
+
157
+ pub(super) fn action_give_up(&mut self) -> Result<()> {
158
+ let answer = give_up(&self.root, &self.problem, &mut self.state)?;
159
+ let language = normalize_language(&self.state.settings.language);
160
+ self.write_output(&format!(
161
+ "Answer for {language}:\n\n```{language}\n{}\n```",
162
+ answer.trim_end()
163
+ ));
164
+ Ok(())
165
+ }
166
+
167
+ pub(super) fn action_cycle_language(&mut self) -> Result<()> {
168
+ let current = LANGUAGES
169
+ .iter()
170
+ .position(|language| language == &self.state.settings.language)
171
+ .unwrap_or(0);
172
+ self.set_language(LANGUAGES[(current + 1) % LANGUAGES.len()])
173
+ }
174
+
175
+ pub(super) fn action_toggle_ui_language(&mut self) -> Result<()> {
176
+ let current = UI_LANGUAGES
177
+ .iter()
178
+ .position(|language| language == &self.state.settings.ui_language)
179
+ .unwrap_or(0);
180
+ self.set_ui_language(UI_LANGUAGES[(current + 1) % UI_LANGUAGES.len()])
181
+ }
182
+
183
+ pub(super) fn action_toggle_theme(&mut self) -> Result<()> {
184
+ let current = THEMES
185
+ .iter()
186
+ .position(|theme| theme == &self.state.settings.theme)
187
+ .unwrap_or(0);
188
+ self.set_theme(THEMES[(current + 1) % THEMES.len()])
189
+ }
190
+
191
+ pub(super) fn set_language(&mut self, language: &str) -> Result<()> {
192
+ self.state.settings.language = language.to_string();
193
+ save_state(&self.root, &self.state)?;
194
+ self.load_code_editor()?;
195
+ self.settings_cursor = None;
196
+ self.show_output = false;
197
+ self.focus = Focus::Code;
198
+ Ok(())
199
+ }
200
+
201
+ pub(super) fn set_ui_language(&mut self, language: &str) -> Result<()> {
202
+ self.state.settings.ui_language = normalize_ui_language(language);
203
+ save_state(&self.root, &self.state)?;
204
+ self.write_text_output(&format!("UI language: {}", self.state.settings.ui_language));
205
+ Ok(())
206
+ }
207
+
208
+ pub(super) fn set_theme(&mut self, theme: &str) -> Result<()> {
209
+ self.state.settings.theme = theme.to_string();
210
+ save_state(&self.root, &self.state)?;
211
+ self.write_text_output(&format!("Theme: {theme}"));
212
+ Ok(())
213
+ }
214
+
215
+ pub(super) fn set_difficulty(&mut self, difficulty: &str) -> Result<()> {
216
+ let difficulty = difficulty.trim().to_lowercase();
217
+ if !DIFFICULTIES.contains(&difficulty.as_str()) {
218
+ self.write_text_output("Difficulty: auto, easy, medium, or hard.");
219
+ return Ok(());
220
+ }
221
+ let normalized = normalize_difficulty(&difficulty);
222
+ self.state.settings.difficulty = normalized.clone();
223
+ if normalized != "auto" {
224
+ self.state.suggested_next_difficulty = normalized;
225
+ }
226
+ save_state(&self.root, &self.state)?;
227
+ self.show_profile();
228
+ Ok(())
229
+ }
230
+
231
+ pub(super) fn set_topics(&mut self, topics: &str, avoid: bool) -> Result<()> {
232
+ let topics = parse_topic_list(topics);
233
+ if avoid {
234
+ self.state.settings.avoid_topics = topics;
235
+ } else {
236
+ self.state.settings.topics = topics;
237
+ }
238
+ save_state(&self.root, &self.state)?;
239
+ self.show_profile();
240
+ Ok(())
241
+ }
242
+
243
+ pub(super) fn set_generate_languages(&mut self, value: &str, ui: bool) -> Result<()> {
244
+ if ui {
245
+ self.state.settings.generate_ui_languages = parse_ui_language_list(value);
246
+ } else {
247
+ self.state.settings.generate_languages = parse_language_list(value);
248
+ }
249
+ save_state(&self.root, &self.state)?;
250
+ self.show_profile();
251
+ Ok(())
252
+ }
253
+
254
+ pub(super) fn reset_profile(&mut self) -> Result<()> {
255
+ self.state.settings.difficulty = "auto".to_string();
256
+ self.state.settings.topics.clear();
257
+ self.state.settings.avoid_topics.clear();
258
+ self.state.settings.generate_languages.clear();
259
+ self.state.settings.generate_ui_languages.clear();
260
+ save_state(&self.root, &self.state)?;
261
+ self.show_profile();
262
+ Ok(())
263
+ }
264
+
265
+ pub(super) fn show_profile(&mut self) {
266
+ self.show_profile_with_intro("");
267
+ }
268
+
269
+ pub(super) fn show_profile_with_intro(&mut self, intro: &str) {
270
+ self.showing_model_status = false;
271
+ if self.settings_cursor.is_none() {
272
+ self.settings_cursor = Some(0);
273
+ }
274
+ let profile = self.profile_text();
275
+ self.output = if intro.trim().is_empty() {
276
+ profile
277
+ } else {
278
+ format!("{}\n\n{profile}", intro.trim_end())
279
+ };
280
+ self.output_is_markdown = false;
281
+ self.show_output = true;
282
+ self.focus = Focus::Output;
283
+ }
284
+
285
+ pub(super) fn profile_text(&self) -> String {
286
+ settings_panel::render(&self.state, self.settings_cursor)
287
+ }
288
+
289
+ pub(super) fn settings_row_count(&self) -> usize {
290
+ settings_panel::row_count()
291
+ }
292
+
293
+ pub(super) fn move_settings_cursor(&mut self, delta: isize) {
294
+ let len = self.settings_row_count() as isize;
295
+ let cursor = self.settings_cursor.unwrap_or(0) as isize;
296
+ self.settings_cursor = Some(((cursor + delta).rem_euclid(len)) as usize);
297
+ self.show_profile();
298
+ }
299
+
300
+ pub(super) fn change_selected_setting(&mut self) -> Result<()> {
301
+ let Some(row) = self.settings_cursor else {
302
+ return Ok(());
303
+ };
304
+ let change = settings_panel::apply_selected(&mut self.state, row);
305
+ if change.reload_editor {
306
+ self.load_code_editor()?;
307
+ }
308
+ save_state(&self.root, &self.state)?;
309
+ self.show_profile();
310
+ Ok(())
311
+ }
312
+ }
@@ -0,0 +1,128 @@
1
+ use super::*;
2
+
3
+ impl PracticodeApp {
4
+ pub(super) fn handle_command(&mut self, value: &str) -> Result<()> {
5
+ if self.task_rx.is_some() {
6
+ let command = value
7
+ .trim()
8
+ .strip_prefix('/')
9
+ .unwrap_or(value.trim())
10
+ .split_whitespace()
11
+ .next()
12
+ .unwrap_or("");
13
+ if matches!(command, "exit" | "quit" | "q") {
14
+ self.should_quit = true;
15
+ } else {
16
+ self.focus = Focus::Output;
17
+ }
18
+ return Ok(());
19
+ }
20
+ if value.is_empty() || matches!(value, "help" | "h" | "?") {
21
+ self.list_cursor = None;
22
+ self.write_output(&self.help_text());
23
+ return Ok(());
24
+ }
25
+ if value.starts_with("vim") {
26
+ self.list_cursor = None;
27
+ self.write_text_output("The code editor is already open on the right.");
28
+ return Ok(());
29
+ }
30
+ let (command, arg) = value.split_once(char::is_whitespace).unwrap_or((value, ""));
31
+ let arg = arg.trim();
32
+ if !matches!(command, "list" | "problems") {
33
+ self.list_cursor = None;
34
+ }
35
+ match command {
36
+ "run" | "r" => self.action_run()?,
37
+ "code" | "edit" | "e" => self.action_edit()?,
38
+ "next" | "n" => self.action_next(arg)?,
39
+ "generate" | "gen" | "new" => self.action_generate(arg),
40
+ "back" | "prev" | "previous" | "p" => self.action_previous()?,
41
+ "answer" | "giveup" | "give" | "g" => self.action_give_up()?,
42
+ "problems" | "list" => self.start_problem_list(),
43
+ "open" | "o" if !arg.is_empty() => self.open_problem(arg)?,
44
+ "language" | "lang" if arg.is_empty() => self.action_cycle_language()?,
45
+ "language" | "lang" if LANGUAGES.contains(&arg) => self.set_language(arg)?,
46
+ "ui" if arg.is_empty() => self.action_toggle_ui_language()?,
47
+ "ui" => self.set_ui_language(&normalize_ui_language(arg))?,
48
+ "theme" if arg.is_empty() => self.action_toggle_theme()?,
49
+ "theme" if THEMES.contains(&arg) => self.set_theme(arg)?,
50
+ "profile" | "settings" if arg.is_empty() => self.show_profile(),
51
+ "profile" | "settings" if arg == "reset" => self.reset_profile()?,
52
+ "difficulty" | "level" if arg.is_empty() => self.show_profile(),
53
+ "difficulty" | "level" => self.set_difficulty(arg)?,
54
+ "topics" | "topic" if arg.is_empty() => self.show_profile(),
55
+ "topics" | "topic" => self.set_topics(arg, false)?,
56
+ "avoid" | "skip" if arg.is_empty() => self.show_profile(),
57
+ "avoid" | "skip" => self.set_topics(arg, true)?,
58
+ "generate-languages" | "gen-languages" | "gen-lang" if arg.is_empty() => {
59
+ self.show_profile()
60
+ }
61
+ "generate-languages" | "gen-languages" | "gen-lang" => {
62
+ self.set_generate_languages(arg, false)?
63
+ }
64
+ "generate-ui" | "gen-ui" if arg.is_empty() => self.show_profile(),
65
+ "generate-ui" | "gen-ui" => self.set_generate_languages(arg, true)?,
66
+ "source" | "next-source" if arg.is_empty() => {
67
+ self.write_text_output(&self.next_source_help());
68
+ }
69
+ "source" | "next-source" if matches!(arg, "bank" | "local" | "ai") => {
70
+ self.state.settings.next_source = normalize_next_source(arg);
71
+ save_state(&self.root, &self.state)?;
72
+ self.write_text_output(&self.next_source_help());
73
+ }
74
+ "ai-next-command" if !arg.is_empty() => {
75
+ self.state.settings.ai_next_command = arg.to_string();
76
+ self.state.settings.next_source = "ai".to_string();
77
+ save_state(&self.root, &self.state)?;
78
+ self.write_text_output("AI next command saved.");
79
+ }
80
+ "provider" | "ai-provider" if arg.is_empty() => {
81
+ self.write_text_output(&format!(
82
+ "AI provider: {}\n{}",
83
+ self.state.settings.ai_provider,
84
+ provider_status(&self.state.settings.ai_provider)
85
+ ));
86
+ }
87
+ "provider" | "ai-provider" if AI_PROVIDERS.contains(&arg) => {
88
+ self.state.settings.ai_provider = normalize_ai_provider(arg);
89
+ self.model_rx = None;
90
+ self.available_models.clear();
91
+ self.available_models_provider.clear();
92
+ self.model_message = None;
93
+ save_state(&self.root, &self.state)?;
94
+ self.write_text_output(&format!(
95
+ "AI provider: {}\n{}",
96
+ self.state.settings.ai_provider,
97
+ provider_status(&self.state.settings.ai_provider)
98
+ ));
99
+ }
100
+ "model" if arg.is_empty() => {
101
+ self.start_model_check();
102
+ self.check_models();
103
+ self.write_model_status();
104
+ }
105
+ "model" => {
106
+ self.state.settings.ai_model = if arg == "auto" {
107
+ "auto".to_string()
108
+ } else {
109
+ arg.to_string()
110
+ };
111
+ save_state(&self.root, &self.state)?;
112
+ self.start_model_check();
113
+ self.check_models();
114
+ self.write_model_status();
115
+ }
116
+ "hint" if arg.is_empty() => {
117
+ self.start_ai_prompt("Give one concise hint for the current problem.")?
118
+ }
119
+ "hint" | "ask" | "ai" if !arg.is_empty() => self.start_ai_prompt(arg)?,
120
+ "note" if !arg.is_empty() => self.append_note(arg)?,
121
+ "note" | "notes" => self.show_notes()?,
122
+ "update" => self.refresh_update_notice(),
123
+ "exit" | "quit" | "q" => self.should_quit = true,
124
+ _ => self.write_text_output(&format!("Unknown command: {value}\nTry /help.")),
125
+ }
126
+ Ok(())
127
+ }
128
+ }
@@ -0,0 +1,117 @@
1
+ use super::*;
2
+
3
+ impl PracticodeApp {
4
+ pub(super) fn insert_command_char(&mut self, char: char) {
5
+ let byte = byte_index(&self.command, self.command_cursor);
6
+ self.command.insert(byte, char);
7
+ self.command_cursor += 1;
8
+ self.command_palette_cursor = 0;
9
+ self.normalize_command_input();
10
+ }
11
+
12
+ pub(super) fn delete_command_before_cursor(&mut self) {
13
+ if self.command_cursor == 0 {
14
+ return;
15
+ }
16
+ let start = byte_index(&self.command, self.command_cursor - 1);
17
+ let end = byte_index(&self.command, self.command_cursor);
18
+ self.command.replace_range(start..end, "");
19
+ self.command_cursor -= 1;
20
+ self.command_palette_cursor = 0;
21
+ self.normalize_command_input();
22
+ }
23
+
24
+ pub(super) fn delete_command_at_cursor(&mut self) {
25
+ if self.command_cursor >= char_len(&self.command) {
26
+ return;
27
+ }
28
+ let start = byte_index(&self.command, self.command_cursor);
29
+ let end = byte_index(&self.command, self.command_cursor + 1);
30
+ self.command.replace_range(start..end, "");
31
+ self.command_palette_cursor = 0;
32
+ self.normalize_command_input();
33
+ }
34
+
35
+ pub(super) fn command_suggestions(&self) -> Vec<CommandChoice> {
36
+ if self.focus != Focus::Command {
37
+ return Vec::new();
38
+ }
39
+ let Some(query) = self.command.trim_start().strip_prefix('/') else {
40
+ return Vec::new();
41
+ };
42
+ let query = query.to_lowercase();
43
+ self.command_choices()
44
+ .into_iter()
45
+ .filter(|hint| hint.insert.starts_with(query.trim_start()))
46
+ .collect()
47
+ }
48
+
49
+ pub(super) fn command_choices(&self) -> Vec<CommandChoice> {
50
+ let mut choices = Vec::new();
51
+ for hint in COMMAND_HINTS {
52
+ if hint.insert == "model " {
53
+ for model in self
54
+ .available_models
55
+ .iter()
56
+ .filter(|model| *model != "auto")
57
+ {
58
+ choices.push(CommandChoice {
59
+ insert: format!("model {model}"),
60
+ display: format!("/model {model}"),
61
+ desc_key: "cmd_model_available",
62
+ keep_open: false,
63
+ });
64
+ }
65
+ }
66
+ choices.push(CommandChoice {
67
+ insert: hint.insert.to_string(),
68
+ display: hint.display.to_string(),
69
+ desc_key: hint.desc_key,
70
+ keep_open: hint.keep_open,
71
+ });
72
+ }
73
+ choices
74
+ }
75
+
76
+ pub(super) fn move_command_palette(&mut self, delta: isize) {
77
+ let len = self.command_suggestions().len();
78
+ if len == 0 {
79
+ return;
80
+ }
81
+ let cursor = self.command_palette_cursor as isize;
82
+ self.command_palette_cursor = ((cursor + delta).rem_euclid(len as isize)) as usize;
83
+ }
84
+
85
+ pub(super) fn accept_command_palette(&mut self) -> Result<bool> {
86
+ let suggestions = self.command_suggestions();
87
+ if suggestions.is_empty() {
88
+ return Ok(false);
89
+ }
90
+ let hint = &suggestions[self.command_palette_cursor.min(suggestions.len() - 1)];
91
+ if hint.keep_open {
92
+ self.command = format!("/{}", hint.insert);
93
+ self.command_cursor = char_len(&self.command);
94
+ self.command_palette_cursor = 0;
95
+ return Ok(true);
96
+ }
97
+ let value = hint.insert.clone();
98
+ self.command.clear();
99
+ self.command_cursor = 0;
100
+ self.command_palette_cursor = 0;
101
+ self.focus = Focus::None;
102
+ self.submit_command(&value)?;
103
+ Ok(true)
104
+ }
105
+
106
+ pub(super) fn normalize_command_input(&mut self) {
107
+ let normalized = compose_hangul_jamo(&self.command);
108
+ if normalized == self.command {
109
+ self.command_cursor = self.command_cursor.min(char_len(&self.command));
110
+ return;
111
+ }
112
+ let old_prefix = prefix(&self.command, self.command_cursor);
113
+ self.command = normalized;
114
+ self.command_cursor =
115
+ char_len(&compose_hangul_jamo(&old_prefix)).min(char_len(&self.command));
116
+ }
117
+ }