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,377 @@
1
+ use super::*;
2
+
3
+ impl PracticodeApp {
4
+ pub(super) fn action_edit(&mut self) -> Result<()> {
5
+ self.editing_notes = false;
6
+ self.load_code_editor()?;
7
+ self.settings_cursor = None;
8
+ self.show_output = false;
9
+ self.focus = Focus::Code;
10
+ Ok(())
11
+ }
12
+
13
+ pub(super) fn action_run(&mut self) -> Result<()> {
14
+ self.save_code()?;
15
+ let result = judge(&self.root, &self.problem, &self.state.settings);
16
+ if result.passed {
17
+ record_pass(&self.root, &self.problem, &mut self.state)?;
18
+ }
19
+ let headline = format!(
20
+ "{} {}/{}",
21
+ if result.passed { "PASS" } else { "FAIL" },
22
+ result.passed_cases,
23
+ result.total_cases
24
+ );
25
+ let next_step = if result.passed {
26
+ ui_text(&self.state.settings.ui_language, "run_pass_next")
27
+ } else {
28
+ ui_text(&self.state.settings.ui_language, "run_fail_next")
29
+ };
30
+ self.write_text_output(&format!("{headline}\n{}\n\n{next_step}", result.output));
31
+ Ok(())
32
+ }
33
+
34
+ pub(super) fn action_next(&mut self, request: &str) -> Result<()> {
35
+ self.check_background_generation();
36
+ let request = request.trim();
37
+ let old_problem = self.state.current_problem.clone();
38
+ if let Some(problem) = next_problem(&self.root, &self.bank, &mut self.state)? {
39
+ self.generate_notice = None;
40
+ self.problem = problem;
41
+ self.load_code_editor()?;
42
+ self.settings_cursor = None;
43
+ self.show_output = false;
44
+ self.focus = Focus::Code;
45
+ return Ok(());
46
+ }
47
+ if self.generate_rx.is_some() {
48
+ self.write_text_output(
49
+ "A background generation is already running. Keep solving; /next will pick up the new problem when it finishes.",
50
+ );
51
+ return Ok(());
52
+ }
53
+ self.start_next_problem(old_problem, true, request.to_string());
54
+ Ok(())
55
+ }
56
+
57
+ pub(super) fn action_generate(&mut self, request: &str) {
58
+ self.check_background_generation();
59
+ if self.task_rx.is_some() || self.generate_rx.is_some() {
60
+ let message = "Generation is already running; skipped duplicate /generate.";
61
+ self.generate_notice = Some(message.to_string());
62
+ self.write_text_output(message);
63
+ return;
64
+ }
65
+ self.start_background_generation(request.trim().to_string());
66
+ }
67
+
68
+ pub(super) fn start_background_generation(&mut self, request: String) {
69
+ let root = self.root.clone();
70
+ let state = self.state.clone();
71
+ let (tx, rx) = mpsc::channel();
72
+ thread::spawn(move || {
73
+ let _ = tx.send(run_ai_generate(&root, &state, &request));
74
+ });
75
+ self.generate_bank_len = self.bank.len();
76
+ self.generate_started = Some(Instant::now());
77
+ self.generate_notice = Some("Generating in background.".to_string());
78
+ self.generate_rx = Some(rx);
79
+ self.settings_cursor = None;
80
+ self.show_output = false;
81
+ self.focus = Focus::Code;
82
+ }
83
+
84
+ pub(super) fn start_next_problem(
85
+ &mut self,
86
+ old_problem: String,
87
+ fallback_to_local: bool,
88
+ request: String,
89
+ ) {
90
+ if self.task_rx.is_some() {
91
+ self.write_text_output(ui_text(&self.state.settings.ui_language, "already_busy"));
92
+ return;
93
+ }
94
+ self.start_busy(
95
+ "next",
96
+ ui_text(&self.state.settings.ui_language, "generating_next"),
97
+ );
98
+ let root = self.root.clone();
99
+ let state = self.state.clone();
100
+ let (tx, rx) = mpsc::channel();
101
+ thread::spawn(move || {
102
+ let output = run_ai_next(&root, &state, true, &request);
103
+ let _ = tx.send(TaskResult::Next {
104
+ output,
105
+ old_problem,
106
+ fallback_to_local,
107
+ });
108
+ });
109
+ self.task_rx = Some(rx);
110
+ }
111
+
112
+ pub(super) fn finish_next_problem(
113
+ &mut self,
114
+ output: String,
115
+ old_problem: String,
116
+ fallback_to_local: bool,
117
+ ) -> Result<()> {
118
+ self.bank = load_bank(&self.root)?;
119
+ self.state = load_state(&self.root, &self.bank)?;
120
+ self.problem = problem_by_id(&self.bank, &self.state.current_problem)
121
+ .cloned()
122
+ .unwrap_or_else(|| self.bank[0].clone());
123
+ if self.state.current_problem == old_problem {
124
+ if fallback_to_local
125
+ && let Some(problem) = next_problem(&self.root, &self.bank, &mut self.state)?
126
+ {
127
+ self.problem = problem;
128
+ } else {
129
+ self.write_text_output(&format!(
130
+ "{}{}No next problem is available yet.",
131
+ if output.is_empty() { "" } else { &output },
132
+ if output.is_empty() { "" } else { "\n\n" }
133
+ ));
134
+ return Ok(());
135
+ }
136
+ }
137
+ self.load_code_editor()?;
138
+ self.settings_cursor = None;
139
+ self.show_output = false;
140
+ self.focus = Focus::Code;
141
+ Ok(())
142
+ }
143
+
144
+ pub(super) fn action_previous(&mut self) -> Result<()> {
145
+ let old_problem = self.state.current_problem.clone();
146
+ self.problem = previous_problem(&self.root, &self.bank, &mut self.state)?;
147
+ if self.state.current_problem == old_problem {
148
+ self.write_text_output("Already at the first known problem.");
149
+ } else {
150
+ self.load_code_editor()?;
151
+ self.settings_cursor = None;
152
+ self.show_output = false;
153
+ self.focus = Focus::Code;
154
+ }
155
+ Ok(())
156
+ }
157
+
158
+ pub(super) fn action_give_up(&mut self) -> Result<()> {
159
+ let answer = give_up(&self.root, &self.problem, &mut self.state)?;
160
+ let language = normalize_language(&self.state.settings.language);
161
+ self.write_output(&format!(
162
+ "Answer for {language}:\n\n```{language}\n{}\n```",
163
+ answer.trim_end()
164
+ ));
165
+ Ok(())
166
+ }
167
+
168
+ pub(super) fn action_cycle_language(&mut self) -> Result<()> {
169
+ let current = LANGUAGES
170
+ .iter()
171
+ .position(|language| language == &self.state.settings.language)
172
+ .unwrap_or(0);
173
+ self.set_language(LANGUAGES[(current + 1) % LANGUAGES.len()])
174
+ }
175
+
176
+ pub(super) fn action_toggle_ui_language(&mut self) -> Result<()> {
177
+ let current = UI_LANGUAGES
178
+ .iter()
179
+ .position(|language| language == &self.state.settings.ui_language)
180
+ .unwrap_or(0);
181
+ self.set_ui_language(UI_LANGUAGES[(current + 1) % UI_LANGUAGES.len()])
182
+ }
183
+
184
+ pub(super) fn action_toggle_theme(&mut self) -> Result<()> {
185
+ let current = THEMES
186
+ .iter()
187
+ .position(|theme| theme == &self.state.settings.theme)
188
+ .unwrap_or(0);
189
+ self.set_theme(THEMES[(current + 1) % THEMES.len()])
190
+ }
191
+
192
+ pub(super) fn set_language(&mut self, language: &str) -> Result<()> {
193
+ self.state.settings.language = language.to_string();
194
+ save_state(&self.root, &self.state)?;
195
+ self.load_code_editor()?;
196
+ self.settings_cursor = None;
197
+ self.show_output = false;
198
+ self.focus = Focus::Code;
199
+ Ok(())
200
+ }
201
+
202
+ pub(super) fn set_ui_language(&mut self, language: &str) -> Result<()> {
203
+ self.state.settings.ui_language = normalize_ui_language(language);
204
+ save_state(&self.root, &self.state)?;
205
+ self.write_text_output(&format!("UI language: {}", self.state.settings.ui_language));
206
+ Ok(())
207
+ }
208
+
209
+ pub(super) fn set_theme(&mut self, theme: &str) -> Result<()> {
210
+ self.state.settings.theme = theme.to_string();
211
+ save_state(&self.root, &self.state)?;
212
+ self.write_text_output(&format!("Theme: {theme}"));
213
+ Ok(())
214
+ }
215
+
216
+ pub(super) fn set_difficulty(&mut self, difficulty: &str) -> Result<()> {
217
+ let difficulty = difficulty.trim().to_lowercase();
218
+ if !DIFFICULTIES.contains(&difficulty.as_str()) {
219
+ self.write_text_output("Difficulty: auto, easy, medium, or hard.");
220
+ return Ok(());
221
+ }
222
+ let normalized = normalize_difficulty(&difficulty);
223
+ self.state.settings.difficulty = normalized.clone();
224
+ if normalized != "auto" {
225
+ self.state.suggested_next_difficulty = normalized;
226
+ }
227
+ save_state(&self.root, &self.state)?;
228
+ self.show_profile();
229
+ Ok(())
230
+ }
231
+
232
+ pub(super) fn set_topics(&mut self, topics: &str, avoid: bool) -> Result<()> {
233
+ let topics = parse_topic_list(topics);
234
+ if avoid {
235
+ self.state.settings.avoid_topics = topics;
236
+ } else {
237
+ self.state.settings.topics = topics;
238
+ }
239
+ save_state(&self.root, &self.state)?;
240
+ self.show_profile();
241
+ Ok(())
242
+ }
243
+
244
+ pub(super) fn set_generate_languages(&mut self, value: &str, ui: bool) -> Result<()> {
245
+ if ui {
246
+ self.state.settings.generate_ui_languages = parse_ui_language_list(value);
247
+ } else {
248
+ self.state.settings.generate_languages = parse_language_list(value);
249
+ }
250
+ save_state(&self.root, &self.state)?;
251
+ self.show_profile();
252
+ Ok(())
253
+ }
254
+
255
+ pub(super) fn set_ai_effort(&mut self, effort: &str) -> Result<()> {
256
+ self.state.settings.ai_effort =
257
+ normalize_ai_effort(&self.state.settings.ai_provider, effort);
258
+ save_state(&self.root, &self.state)?;
259
+ self.write_model_status();
260
+ Ok(())
261
+ }
262
+
263
+ pub(super) fn reset_profile(&mut self) -> Result<()> {
264
+ self.state.settings.difficulty = "auto".to_string();
265
+ self.state.settings.topics.clear();
266
+ self.state.settings.avoid_topics.clear();
267
+ self.state.settings.generate_languages.clear();
268
+ self.state.settings.generate_ui_languages.clear();
269
+ save_state(&self.root, &self.state)?;
270
+ self.show_profile();
271
+ Ok(())
272
+ }
273
+
274
+ pub(super) fn show_profile(&mut self) {
275
+ self.show_profile_with_intro("");
276
+ }
277
+
278
+ pub(super) fn show_profile_with_intro(&mut self, intro: &str) {
279
+ self.editing_notes = false;
280
+ self.showing_model_status = false;
281
+ if self.settings_cursor.is_none() {
282
+ self.settings_cursor = Some(0);
283
+ }
284
+ let profile = self.profile_text();
285
+ self.output = if intro.trim().is_empty() {
286
+ profile
287
+ } else {
288
+ format!("{}\n\n{profile}", intro.trim_end())
289
+ };
290
+ self.output_is_markdown = false;
291
+ self.show_output = true;
292
+ self.focus = Focus::Output;
293
+ }
294
+
295
+ pub(super) fn profile_text(&self) -> String {
296
+ settings_panel::render(
297
+ &self.state,
298
+ self.settings_cursor,
299
+ &self.available_models,
300
+ self.model_rx.is_some(),
301
+ )
302
+ }
303
+
304
+ pub(super) fn settings_row_count(&self) -> usize {
305
+ settings_panel::row_count()
306
+ }
307
+
308
+ pub(super) fn move_settings_cursor(&mut self, delta: isize) {
309
+ let len = self.settings_row_count() as isize;
310
+ let cursor = self.settings_cursor.unwrap_or(0) as isize;
311
+ self.settings_cursor = Some(((cursor + delta).rem_euclid(len)) as usize);
312
+ self.show_profile();
313
+ }
314
+
315
+ pub(super) fn change_selected_setting(&mut self) -> Result<()> {
316
+ let Some(row) = self.settings_cursor else {
317
+ return Ok(());
318
+ };
319
+ if row == settings_panel::AI_MODEL_ROW
320
+ && self.available_models_provider != self.state.settings.ai_provider
321
+ {
322
+ self.start_model_check();
323
+ self.check_models();
324
+ if self.model_rx.is_some() {
325
+ self.show_profile();
326
+ return Ok(());
327
+ }
328
+ }
329
+ let change = settings_panel::apply_selected(&mut self.state, row, &self.available_models);
330
+ if change.edit_notes {
331
+ self.start_note_editor()?;
332
+ return Ok(());
333
+ }
334
+ if change.provider_changed {
335
+ self.model_rx = None;
336
+ self.available_models.clear();
337
+ self.available_models_provider.clear();
338
+ self.model_message = None;
339
+ }
340
+ if change.reload_editor {
341
+ self.load_code_editor()?;
342
+ }
343
+ save_state(&self.root, &self.state)?;
344
+ self.show_profile();
345
+ Ok(())
346
+ }
347
+
348
+ pub(super) fn start_note_editor(&mut self) -> Result<()> {
349
+ self.save_code()?;
350
+ self.note_editor
351
+ .set_text(&read_problem_notes(&self.root).unwrap_or_default());
352
+ self.settings_cursor = None;
353
+ self.showing_model_status = false;
354
+ self.editing_notes = true;
355
+ self.show_output = true;
356
+ self.focus = Focus::Output;
357
+ Ok(())
358
+ }
359
+
360
+ pub(super) fn save_notes(&self) -> Result<()> {
361
+ let path = self.root.join(PROBLEM_NOTES_PATH);
362
+ if let Some(parent) = path.parent() {
363
+ fs::create_dir_all(parent)?;
364
+ }
365
+ let text = self.note_editor.text();
366
+ let text = text.trim_end();
367
+ fs::write(path, if text.is_empty() { "" } else { text })?;
368
+ Ok(())
369
+ }
370
+
371
+ pub(super) fn close_note_editor(&mut self) -> Result<()> {
372
+ self.save_notes()?;
373
+ self.editing_notes = false;
374
+ self.show_profile();
375
+ Ok(())
376
+ }
377
+ }
@@ -0,0 +1,138 @@
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.state.settings.ai_model = "auto".to_string();
90
+ self.state.settings.ai_effort = normalize_ai_effort(
91
+ &self.state.settings.ai_provider,
92
+ &self.state.settings.ai_effort,
93
+ );
94
+ self.model_rx = None;
95
+ self.available_models.clear();
96
+ self.available_models_provider.clear();
97
+ self.model_message = None;
98
+ save_state(&self.root, &self.state)?;
99
+ self.write_text_output(&format!(
100
+ "AI provider: {}\n{}",
101
+ self.state.settings.ai_provider,
102
+ provider_status(&self.state.settings.ai_provider)
103
+ ));
104
+ }
105
+ "model" if arg.is_empty() => {
106
+ self.start_model_check();
107
+ self.check_models();
108
+ self.write_model_status();
109
+ }
110
+ "model" => {
111
+ self.state.settings.ai_model = if arg == "auto" {
112
+ "auto".to_string()
113
+ } else {
114
+ arg.to_string()
115
+ };
116
+ save_state(&self.root, &self.state)?;
117
+ self.start_model_check();
118
+ self.check_models();
119
+ self.write_model_status();
120
+ }
121
+ "effort" | "reasoning" | "ai-effort" if arg.is_empty() => {
122
+ self.write_model_status();
123
+ }
124
+ "effort" | "reasoning" | "ai-effort" => self.set_ai_effort(arg)?,
125
+ "hint" if arg.is_empty() => {
126
+ self.start_ai_prompt("Give one concise hint for the current problem.")?
127
+ }
128
+ "hint" | "ask" | "ai" if !arg.is_empty() => self.start_ai_prompt(arg)?,
129
+ "note" if !arg.is_empty() => self.append_note(arg)?,
130
+ "note" => self.start_note_editor()?,
131
+ "notes" => self.show_notes()?,
132
+ "update" => self.refresh_update_notice(),
133
+ "exit" | "quit" | "q" => self.should_quit = true,
134
+ _ => self.write_text_output(&format!("Unknown command: {value}\nTry /help.")),
135
+ }
136
+ Ok(())
137
+ }
138
+ }
@@ -0,0 +1,120 @@
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 == "effort max" && self.state.settings.ai_provider != "claude" {
53
+ continue;
54
+ }
55
+ if hint.insert == "model " {
56
+ for model in self
57
+ .available_models
58
+ .iter()
59
+ .filter(|model| *model != "auto")
60
+ {
61
+ choices.push(CommandChoice {
62
+ insert: format!("model {model}"),
63
+ display: format!("/model {model}"),
64
+ desc_key: "cmd_model_available",
65
+ keep_open: false,
66
+ });
67
+ }
68
+ }
69
+ choices.push(CommandChoice {
70
+ insert: hint.insert.to_string(),
71
+ display: hint.display.to_string(),
72
+ desc_key: hint.desc_key,
73
+ keep_open: hint.keep_open,
74
+ });
75
+ }
76
+ choices
77
+ }
78
+
79
+ pub(super) fn move_command_palette(&mut self, delta: isize) {
80
+ let len = self.command_suggestions().len();
81
+ if len == 0 {
82
+ return;
83
+ }
84
+ let cursor = self.command_palette_cursor as isize;
85
+ self.command_palette_cursor = ((cursor + delta).rem_euclid(len as isize)) as usize;
86
+ }
87
+
88
+ pub(super) fn accept_command_palette(&mut self) -> Result<bool> {
89
+ let suggestions = self.command_suggestions();
90
+ if suggestions.is_empty() {
91
+ return Ok(false);
92
+ }
93
+ let hint = &suggestions[self.command_palette_cursor.min(suggestions.len() - 1)];
94
+ if hint.keep_open {
95
+ self.command = format!("/{}", hint.insert);
96
+ self.command_cursor = char_len(&self.command);
97
+ self.command_palette_cursor = 0;
98
+ return Ok(true);
99
+ }
100
+ let value = hint.insert.clone();
101
+ self.command.clear();
102
+ self.command_cursor = 0;
103
+ self.command_palette_cursor = 0;
104
+ self.focus = Focus::None;
105
+ self.submit_command(&value)?;
106
+ Ok(true)
107
+ }
108
+
109
+ pub(super) fn normalize_command_input(&mut self) {
110
+ let normalized = compose_hangul_jamo(&self.command);
111
+ if normalized == self.command {
112
+ self.command_cursor = self.command_cursor.min(char_len(&self.command));
113
+ return;
114
+ }
115
+ let old_prefix = prefix(&self.command, self.command_cursor);
116
+ self.command = normalized;
117
+ self.command_cursor =
118
+ char_len(&compose_hangul_jamo(&old_prefix)).min(char_len(&self.command));
119
+ }
120
+ }
@@ -162,6 +162,62 @@ pub(super) const COMMAND_HINTS: &[CommandHint] = &[
162
162
  keep_open: true,
163
163
  help: false,
164
164
  },
165
+ CommandHint {
166
+ insert: "note",
167
+ display: "/note",
168
+ desc_key: "cmd_note",
169
+ keep_open: false,
170
+ help: true,
171
+ },
172
+ CommandHint {
173
+ insert: "notes",
174
+ display: "/notes",
175
+ desc_key: "cmd_notes",
176
+ keep_open: false,
177
+ help: true,
178
+ },
179
+ CommandHint {
180
+ insert: "effort auto",
181
+ display: "/effort auto",
182
+ desc_key: "cmd_effort_auto",
183
+ keep_open: false,
184
+ help: true,
185
+ },
186
+ CommandHint {
187
+ insert: "effort low",
188
+ display: "/effort low",
189
+ desc_key: "cmd_effort",
190
+ keep_open: false,
191
+ help: false,
192
+ },
193
+ CommandHint {
194
+ insert: "effort medium",
195
+ display: "/effort medium",
196
+ desc_key: "cmd_effort",
197
+ keep_open: false,
198
+ help: false,
199
+ },
200
+ CommandHint {
201
+ insert: "effort high",
202
+ display: "/effort high",
203
+ desc_key: "cmd_effort",
204
+ keep_open: false,
205
+ help: false,
206
+ },
207
+ CommandHint {
208
+ insert: "effort xhigh",
209
+ display: "/effort xhigh",
210
+ desc_key: "cmd_effort",
211
+ keep_open: false,
212
+ help: false,
213
+ },
214
+ CommandHint {
215
+ insert: "effort max",
216
+ display: "/effort max",
217
+ desc_key: "cmd_effort_max",
218
+ keep_open: false,
219
+ help: false,
220
+ },
165
221
  CommandHint {
166
222
  insert: "language python",
167
223
  display: "/language python",