practicode 0.1.0 → 0.1.1
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 +39 -100
- package/THIRD_PARTY_LICENSES.md +58 -0
- package/docs/CONTRIBUTING.md +76 -0
- package/package.json +4 -2
- package/scripts/release.sh +69 -0
- package/src/core.rs +271 -32
- package/src/tui.rs +363 -60
package/src/tui.rs
CHANGED
|
@@ -4,8 +4,8 @@ use crate::{
|
|
|
4
4
|
AI_PROVIDERS, AppState, HistoryItem, LANGUAGES, PROBLEM_NOTES_PATH, Problem, THEMES,
|
|
5
5
|
UI_LANGUAGES, ensure_problem_files, ensure_submission, ext_for, give_up, judge, load_bank,
|
|
6
6
|
load_state, localized, next_problem, normalize_ai_provider, normalize_language,
|
|
7
|
-
normalize_next_source, previous_problem, problem_by_id, record_pass,
|
|
8
|
-
save_state, template_for,
|
|
7
|
+
normalize_next_source, normalize_ui_language, previous_problem, problem_by_id, record_pass,
|
|
8
|
+
render_problem, save_state, template_for, ui_text,
|
|
9
9
|
},
|
|
10
10
|
text::{
|
|
11
11
|
byte_index, char_len, compose_hangul_jamo, display_width, prefix, render_markdown_plain,
|
|
@@ -17,7 +17,7 @@ use ratatui::{
|
|
|
17
17
|
DefaultTerminal, Frame,
|
|
18
18
|
layout::{Constraint, Direction, Layout, Position, Rect},
|
|
19
19
|
style::{Color, Modifier, Style},
|
|
20
|
-
widgets::{Block, Borders, Paragraph, Wrap},
|
|
20
|
+
widgets::{Block, Borders, Clear, Paragraph, Wrap},
|
|
21
21
|
};
|
|
22
22
|
use std::{
|
|
23
23
|
collections::HashMap,
|
|
@@ -28,46 +28,220 @@ use std::{
|
|
|
28
28
|
time::Duration,
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
## Commands
|
|
40
|
-
|
|
41
|
-
- `/run` judge current submission
|
|
42
|
-
- `/edit` focus the code editor
|
|
43
|
-
- `/next [request]` next problem, optionally with a request
|
|
44
|
-
- `/prev` previous problem
|
|
45
|
-
- `/list` choose from problem list
|
|
46
|
-
- `/open 2` open by number, id, or slug
|
|
47
|
-
- `/giveup` show answer
|
|
48
|
-
- `/ai hint` ask the selected AI about current problem + code
|
|
49
|
-
- `/provider codex|claude`
|
|
50
|
-
- `/model auto|sonnet|opus|...`
|
|
51
|
-
- `/note prefer strings this week`
|
|
52
|
-
- `/notes` show next-problem notes
|
|
53
|
-
- `/lang python|ts|java|rust`
|
|
54
|
-
- `/ui ko|en`
|
|
55
|
-
- `/theme dark|light`
|
|
56
|
-
- `/source bank|ai`
|
|
57
|
-
- `/exit` quit
|
|
58
|
-
|
|
59
|
-
## Keys
|
|
60
|
-
|
|
61
|
-
- `Esc` leaves the editor or output pane
|
|
62
|
-
- `/` opens the command bar when the editor is not focused
|
|
63
|
-
- `?` opens this help when the editor is not focused
|
|
64
|
-
- `up/down` or `j/k` move in `/list`
|
|
65
|
-
|
|
66
|
-
## Debug prints
|
|
31
|
+
#[derive(Clone, Copy)]
|
|
32
|
+
struct CommandHint {
|
|
33
|
+
insert: &'static str,
|
|
34
|
+
display: &'static str,
|
|
35
|
+
desc_key: &'static str,
|
|
36
|
+
keep_open: bool,
|
|
37
|
+
help: bool,
|
|
38
|
+
}
|
|
67
39
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
"
|
|
40
|
+
const COMMAND_HINTS: &[CommandHint] = &[
|
|
41
|
+
CommandHint {
|
|
42
|
+
insert: "run",
|
|
43
|
+
display: "/run",
|
|
44
|
+
desc_key: "cmd_run",
|
|
45
|
+
keep_open: false,
|
|
46
|
+
help: true,
|
|
47
|
+
},
|
|
48
|
+
CommandHint {
|
|
49
|
+
insert: "edit",
|
|
50
|
+
display: "/edit",
|
|
51
|
+
desc_key: "cmd_edit",
|
|
52
|
+
keep_open: false,
|
|
53
|
+
help: true,
|
|
54
|
+
},
|
|
55
|
+
CommandHint {
|
|
56
|
+
insert: "next",
|
|
57
|
+
display: "/next",
|
|
58
|
+
desc_key: "cmd_next",
|
|
59
|
+
keep_open: false,
|
|
60
|
+
help: true,
|
|
61
|
+
},
|
|
62
|
+
CommandHint {
|
|
63
|
+
insert: "prev",
|
|
64
|
+
display: "/prev",
|
|
65
|
+
desc_key: "cmd_prev",
|
|
66
|
+
keep_open: false,
|
|
67
|
+
help: true,
|
|
68
|
+
},
|
|
69
|
+
CommandHint {
|
|
70
|
+
insert: "list",
|
|
71
|
+
display: "/list",
|
|
72
|
+
desc_key: "cmd_list",
|
|
73
|
+
keep_open: false,
|
|
74
|
+
help: true,
|
|
75
|
+
},
|
|
76
|
+
CommandHint {
|
|
77
|
+
insert: "open ",
|
|
78
|
+
display: "/open <id>",
|
|
79
|
+
desc_key: "cmd_open",
|
|
80
|
+
keep_open: true,
|
|
81
|
+
help: true,
|
|
82
|
+
},
|
|
83
|
+
CommandHint {
|
|
84
|
+
insert: "giveup",
|
|
85
|
+
display: "/giveup",
|
|
86
|
+
desc_key: "cmd_giveup",
|
|
87
|
+
keep_open: false,
|
|
88
|
+
help: true,
|
|
89
|
+
},
|
|
90
|
+
CommandHint {
|
|
91
|
+
insert: "ai ",
|
|
92
|
+
display: "/ai <prompt>",
|
|
93
|
+
desc_key: "cmd_ai",
|
|
94
|
+
keep_open: true,
|
|
95
|
+
help: true,
|
|
96
|
+
},
|
|
97
|
+
CommandHint {
|
|
98
|
+
insert: "provider codex",
|
|
99
|
+
display: "/provider codex",
|
|
100
|
+
desc_key: "cmd_provider",
|
|
101
|
+
keep_open: false,
|
|
102
|
+
help: true,
|
|
103
|
+
},
|
|
104
|
+
CommandHint {
|
|
105
|
+
insert: "provider claude",
|
|
106
|
+
display: "/provider claude",
|
|
107
|
+
desc_key: "cmd_provider",
|
|
108
|
+
keep_open: false,
|
|
109
|
+
help: false,
|
|
110
|
+
},
|
|
111
|
+
CommandHint {
|
|
112
|
+
insert: "model auto",
|
|
113
|
+
display: "/model auto",
|
|
114
|
+
desc_key: "cmd_model",
|
|
115
|
+
keep_open: false,
|
|
116
|
+
help: true,
|
|
117
|
+
},
|
|
118
|
+
CommandHint {
|
|
119
|
+
insert: "model ",
|
|
120
|
+
display: "/model <name>",
|
|
121
|
+
desc_key: "cmd_model",
|
|
122
|
+
keep_open: true,
|
|
123
|
+
help: false,
|
|
124
|
+
},
|
|
125
|
+
CommandHint {
|
|
126
|
+
insert: "note ",
|
|
127
|
+
display: "/note <text>",
|
|
128
|
+
desc_key: "cmd_note",
|
|
129
|
+
keep_open: true,
|
|
130
|
+
help: true,
|
|
131
|
+
},
|
|
132
|
+
CommandHint {
|
|
133
|
+
insert: "notes",
|
|
134
|
+
display: "/notes",
|
|
135
|
+
desc_key: "cmd_notes",
|
|
136
|
+
keep_open: false,
|
|
137
|
+
help: true,
|
|
138
|
+
},
|
|
139
|
+
CommandHint {
|
|
140
|
+
insert: "lang python",
|
|
141
|
+
display: "/lang python",
|
|
142
|
+
desc_key: "cmd_lang",
|
|
143
|
+
keep_open: false,
|
|
144
|
+
help: true,
|
|
145
|
+
},
|
|
146
|
+
CommandHint {
|
|
147
|
+
insert: "lang ts",
|
|
148
|
+
display: "/lang ts",
|
|
149
|
+
desc_key: "cmd_lang",
|
|
150
|
+
keep_open: false,
|
|
151
|
+
help: false,
|
|
152
|
+
},
|
|
153
|
+
CommandHint {
|
|
154
|
+
insert: "lang java",
|
|
155
|
+
display: "/lang java",
|
|
156
|
+
desc_key: "cmd_lang",
|
|
157
|
+
keep_open: false,
|
|
158
|
+
help: false,
|
|
159
|
+
},
|
|
160
|
+
CommandHint {
|
|
161
|
+
insert: "lang rust",
|
|
162
|
+
display: "/lang rust",
|
|
163
|
+
desc_key: "cmd_lang",
|
|
164
|
+
keep_open: false,
|
|
165
|
+
help: false,
|
|
166
|
+
},
|
|
167
|
+
CommandHint {
|
|
168
|
+
insert: "ui en",
|
|
169
|
+
display: "/ui en",
|
|
170
|
+
desc_key: "cmd_ui",
|
|
171
|
+
keep_open: false,
|
|
172
|
+
help: true,
|
|
173
|
+
},
|
|
174
|
+
CommandHint {
|
|
175
|
+
insert: "ui ko",
|
|
176
|
+
display: "/ui ko",
|
|
177
|
+
desc_key: "cmd_ui",
|
|
178
|
+
keep_open: false,
|
|
179
|
+
help: false,
|
|
180
|
+
},
|
|
181
|
+
CommandHint {
|
|
182
|
+
insert: "ui ja",
|
|
183
|
+
display: "/ui ja",
|
|
184
|
+
desc_key: "cmd_ui",
|
|
185
|
+
keep_open: false,
|
|
186
|
+
help: false,
|
|
187
|
+
},
|
|
188
|
+
CommandHint {
|
|
189
|
+
insert: "ui zh",
|
|
190
|
+
display: "/ui zh",
|
|
191
|
+
desc_key: "cmd_ui",
|
|
192
|
+
keep_open: false,
|
|
193
|
+
help: false,
|
|
194
|
+
},
|
|
195
|
+
CommandHint {
|
|
196
|
+
insert: "ui es",
|
|
197
|
+
display: "/ui es",
|
|
198
|
+
desc_key: "cmd_ui",
|
|
199
|
+
keep_open: false,
|
|
200
|
+
help: false,
|
|
201
|
+
},
|
|
202
|
+
CommandHint {
|
|
203
|
+
insert: "theme dark",
|
|
204
|
+
display: "/theme dark",
|
|
205
|
+
desc_key: "cmd_theme",
|
|
206
|
+
keep_open: false,
|
|
207
|
+
help: true,
|
|
208
|
+
},
|
|
209
|
+
CommandHint {
|
|
210
|
+
insert: "theme light",
|
|
211
|
+
display: "/theme light",
|
|
212
|
+
desc_key: "cmd_theme",
|
|
213
|
+
keep_open: false,
|
|
214
|
+
help: false,
|
|
215
|
+
},
|
|
216
|
+
CommandHint {
|
|
217
|
+
insert: "source bank",
|
|
218
|
+
display: "/source bank",
|
|
219
|
+
desc_key: "cmd_source",
|
|
220
|
+
keep_open: false,
|
|
221
|
+
help: true,
|
|
222
|
+
},
|
|
223
|
+
CommandHint {
|
|
224
|
+
insert: "source ai",
|
|
225
|
+
display: "/source ai",
|
|
226
|
+
desc_key: "cmd_source",
|
|
227
|
+
keep_open: false,
|
|
228
|
+
help: false,
|
|
229
|
+
},
|
|
230
|
+
CommandHint {
|
|
231
|
+
insert: "help",
|
|
232
|
+
display: "/help",
|
|
233
|
+
desc_key: "cmd_help",
|
|
234
|
+
keep_open: false,
|
|
235
|
+
help: true,
|
|
236
|
+
},
|
|
237
|
+
CommandHint {
|
|
238
|
+
insert: "exit",
|
|
239
|
+
display: "/exit",
|
|
240
|
+
desc_key: "cmd_exit",
|
|
241
|
+
keep_open: false,
|
|
242
|
+
help: true,
|
|
243
|
+
},
|
|
244
|
+
];
|
|
71
245
|
|
|
72
246
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
73
247
|
enum Focus {
|
|
@@ -85,6 +259,7 @@ pub struct PracticodeApp {
|
|
|
85
259
|
editor: TextEditor,
|
|
86
260
|
command: String,
|
|
87
261
|
command_cursor: usize,
|
|
262
|
+
command_palette_cursor: usize,
|
|
88
263
|
output: String,
|
|
89
264
|
output_is_markdown: bool,
|
|
90
265
|
show_output: bool,
|
|
@@ -121,6 +296,7 @@ impl PracticodeApp {
|
|
|
121
296
|
editor: TextEditor::default(),
|
|
122
297
|
command: String::new(),
|
|
123
298
|
command_cursor: 0,
|
|
299
|
+
command_palette_cursor: 0,
|
|
124
300
|
output: String::new(),
|
|
125
301
|
output_is_markdown: false,
|
|
126
302
|
show_output: false,
|
|
@@ -174,6 +350,10 @@ impl PracticodeApp {
|
|
|
174
350
|
self.command_cursor
|
|
175
351
|
}
|
|
176
352
|
|
|
353
|
+
pub fn handle_key_for_test(&mut self, key: KeyEvent) -> Result<()> {
|
|
354
|
+
self.handle_key(key)
|
|
355
|
+
}
|
|
356
|
+
|
|
177
357
|
pub fn busy_label(&self) -> &str {
|
|
178
358
|
&self.busy_label
|
|
179
359
|
}
|
|
@@ -201,7 +381,10 @@ impl PracticodeApp {
|
|
|
201
381
|
&self.problem,
|
|
202
382
|
&self.state.settings.ui_language,
|
|
203
383
|
)))
|
|
204
|
-
.block(Self::block(
|
|
384
|
+
.block(Self::block(
|
|
385
|
+
ui_text(&self.state.settings.ui_language, "problem"),
|
|
386
|
+
self.state.settings.theme == "light",
|
|
387
|
+
))
|
|
205
388
|
.wrap(Wrap { trim: false });
|
|
206
389
|
frame.render_widget(problem, body[0]);
|
|
207
390
|
|
|
@@ -214,7 +397,10 @@ impl PracticodeApp {
|
|
|
214
397
|
self.output.clone()
|
|
215
398
|
};
|
|
216
399
|
let output = Paragraph::new(text)
|
|
217
|
-
.block(Self::block(
|
|
400
|
+
.block(Self::block(
|
|
401
|
+
ui_text(&self.state.settings.ui_language, "output"),
|
|
402
|
+
self.state.settings.theme == "light",
|
|
403
|
+
))
|
|
218
404
|
.wrap(Wrap { trim: false });
|
|
219
405
|
frame.render_widget(output, body[1]);
|
|
220
406
|
} else {
|
|
@@ -244,15 +430,56 @@ impl PracticodeApp {
|
|
|
244
430
|
let command_text = if self.focus == Focus::Command || !self.command.is_empty() {
|
|
245
431
|
self.command.clone()
|
|
246
432
|
} else {
|
|
247
|
-
|
|
433
|
+
ui_text(&self.state.settings.ui_language, "command_placeholder").to_string()
|
|
248
434
|
};
|
|
249
435
|
let command = Paragraph::new(command_text)
|
|
250
|
-
.block(Self::block(
|
|
436
|
+
.block(Self::block(
|
|
437
|
+
ui_text(&self.state.settings.ui_language, "command"),
|
|
438
|
+
self.state.settings.theme == "light",
|
|
439
|
+
))
|
|
251
440
|
.wrap(Wrap { trim: false });
|
|
252
441
|
frame.render_widget(command, vertical[2]);
|
|
442
|
+
self.draw_command_palette(frame, vertical[2]);
|
|
253
443
|
self.set_terminal_cursor(frame, body[1], vertical[2]);
|
|
254
444
|
}
|
|
255
445
|
|
|
446
|
+
fn draw_command_palette(&self, frame: &mut Frame, command_area: Rect) {
|
|
447
|
+
let suggestions = self.command_suggestions();
|
|
448
|
+
if suggestions.is_empty() || command_area.y < 3 {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
let height = ((suggestions.len() + 3) as u16).min(10).min(command_area.y);
|
|
452
|
+
let area = Rect::new(
|
|
453
|
+
command_area.x,
|
|
454
|
+
command_area.y - height,
|
|
455
|
+
command_area.width,
|
|
456
|
+
height,
|
|
457
|
+
);
|
|
458
|
+
let selected = self.command_palette_cursor.min(suggestions.len() - 1);
|
|
459
|
+
let mut lines = suggestions
|
|
460
|
+
.iter()
|
|
461
|
+
.enumerate()
|
|
462
|
+
.take(height.saturating_sub(2) as usize)
|
|
463
|
+
.map(|(index, hint)| {
|
|
464
|
+
let marker = if index == selected { ">" } else { " " };
|
|
465
|
+
format!(
|
|
466
|
+
"{marker} {:<16} {}",
|
|
467
|
+
hint.display,
|
|
468
|
+
ui_text(&self.state.settings.ui_language, hint.desc_key)
|
|
469
|
+
)
|
|
470
|
+
})
|
|
471
|
+
.collect::<Vec<_>>();
|
|
472
|
+
lines.push(ui_text(&self.state.settings.ui_language, "palette_hint").to_string());
|
|
473
|
+
frame.render_widget(Clear, area);
|
|
474
|
+
frame.render_widget(
|
|
475
|
+
Paragraph::new(lines.join("\n")).block(Self::block(
|
|
476
|
+
ui_text(&self.state.settings.ui_language, "commands"),
|
|
477
|
+
self.state.settings.theme == "light",
|
|
478
|
+
)),
|
|
479
|
+
area,
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
256
483
|
fn block(title: &str, light: bool) -> Block<'_> {
|
|
257
484
|
Block::default()
|
|
258
485
|
.borders(Borders::ALL)
|
|
@@ -277,14 +504,18 @@ impl PracticodeApp {
|
|
|
277
504
|
KeyCode::Esc => {
|
|
278
505
|
self.command.clear();
|
|
279
506
|
self.command_cursor = 0;
|
|
507
|
+
self.command_palette_cursor = 0;
|
|
280
508
|
self.focus = Focus::None;
|
|
281
509
|
}
|
|
282
510
|
KeyCode::Enter => {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
511
|
+
if !self.accept_command_palette()? {
|
|
512
|
+
let value = self.command.trim().to_string();
|
|
513
|
+
self.command.clear();
|
|
514
|
+
self.command_cursor = 0;
|
|
515
|
+
self.command_palette_cursor = 0;
|
|
516
|
+
self.focus = Focus::None;
|
|
517
|
+
self.submit_command(&value)?;
|
|
518
|
+
}
|
|
288
519
|
}
|
|
289
520
|
KeyCode::Backspace => self.delete_command_before_cursor(),
|
|
290
521
|
KeyCode::Delete => self.delete_command_at_cursor(),
|
|
@@ -292,11 +523,14 @@ impl PracticodeApp {
|
|
|
292
523
|
KeyCode::Right => {
|
|
293
524
|
self.command_cursor = (self.command_cursor + 1).min(char_len(&self.command));
|
|
294
525
|
}
|
|
526
|
+
KeyCode::Up => self.move_command_palette(-1),
|
|
527
|
+
KeyCode::Down => self.move_command_palette(1),
|
|
295
528
|
KeyCode::Home => self.command_cursor = 0,
|
|
296
529
|
KeyCode::End => self.command_cursor = char_len(&self.command),
|
|
297
530
|
KeyCode::Char('?') if self.command.trim().is_empty() || self.command.trim() == "/" => {
|
|
298
531
|
self.command.clear();
|
|
299
532
|
self.command_cursor = 0;
|
|
533
|
+
self.command_palette_cursor = 0;
|
|
300
534
|
self.focus = Focus::None;
|
|
301
535
|
self.handle_command("help")?;
|
|
302
536
|
}
|
|
@@ -389,6 +623,7 @@ impl PracticodeApp {
|
|
|
389
623
|
self.command.push('/');
|
|
390
624
|
self.command_cursor = 1;
|
|
391
625
|
}
|
|
626
|
+
self.command_palette_cursor = 0;
|
|
392
627
|
self.focus = Focus::Command;
|
|
393
628
|
}
|
|
394
629
|
|
|
@@ -404,7 +639,7 @@ impl PracticodeApp {
|
|
|
404
639
|
fn handle_command(&mut self, value: &str) -> Result<()> {
|
|
405
640
|
if value.is_empty() || matches!(value, "help" | "h" | "?") {
|
|
406
641
|
self.list_cursor = None;
|
|
407
|
-
self.write_output(
|
|
642
|
+
self.write_output(&self.help_text());
|
|
408
643
|
return Ok(());
|
|
409
644
|
}
|
|
410
645
|
if value.starts_with("vim") {
|
|
@@ -428,7 +663,7 @@ impl PracticodeApp {
|
|
|
428
663
|
"lang" if arg.is_empty() => self.action_cycle_language()?,
|
|
429
664
|
"lang" if LANGUAGES.contains(&arg) => self.set_language(arg)?,
|
|
430
665
|
"ui" if arg.is_empty() => self.action_toggle_ui_language()?,
|
|
431
|
-
"ui"
|
|
666
|
+
"ui" => self.set_ui_language(&normalize_ui_language(arg))?,
|
|
432
667
|
"theme" if arg.is_empty() => self.action_toggle_theme()?,
|
|
433
668
|
"theme" if THEMES.contains(&arg) => self.set_theme(arg)?,
|
|
434
669
|
"source" | "next-source" if arg.is_empty() => {
|
|
@@ -640,9 +875,9 @@ impl PracticodeApp {
|
|
|
640
875
|
}
|
|
641
876
|
|
|
642
877
|
fn set_ui_language(&mut self, language: &str) -> Result<()> {
|
|
643
|
-
self.state.settings.ui_language = language
|
|
878
|
+
self.state.settings.ui_language = normalize_ui_language(language);
|
|
644
879
|
save_state(&self.root, &self.state)?;
|
|
645
|
-
self.write_text_output(&format!("UI language: {
|
|
880
|
+
self.write_text_output(&format!("UI language: {}", self.state.settings.ui_language));
|
|
646
881
|
Ok(())
|
|
647
882
|
}
|
|
648
883
|
|
|
@@ -742,6 +977,7 @@ impl PracticodeApp {
|
|
|
742
977
|
let byte = byte_index(&self.command, self.command_cursor);
|
|
743
978
|
self.command.insert(byte, char);
|
|
744
979
|
self.command_cursor += 1;
|
|
980
|
+
self.command_palette_cursor = 0;
|
|
745
981
|
self.normalize_command_input();
|
|
746
982
|
}
|
|
747
983
|
|
|
@@ -753,6 +989,7 @@ impl PracticodeApp {
|
|
|
753
989
|
let end = byte_index(&self.command, self.command_cursor);
|
|
754
990
|
self.command.replace_range(start..end, "");
|
|
755
991
|
self.command_cursor -= 1;
|
|
992
|
+
self.command_palette_cursor = 0;
|
|
756
993
|
self.normalize_command_input();
|
|
757
994
|
}
|
|
758
995
|
|
|
@@ -763,9 +1000,55 @@ impl PracticodeApp {
|
|
|
763
1000
|
let start = byte_index(&self.command, self.command_cursor);
|
|
764
1001
|
let end = byte_index(&self.command, self.command_cursor + 1);
|
|
765
1002
|
self.command.replace_range(start..end, "");
|
|
1003
|
+
self.command_palette_cursor = 0;
|
|
766
1004
|
self.normalize_command_input();
|
|
767
1005
|
}
|
|
768
1006
|
|
|
1007
|
+
fn command_suggestions(&self) -> Vec<&'static CommandHint> {
|
|
1008
|
+
if self.focus != Focus::Command {
|
|
1009
|
+
return Vec::new();
|
|
1010
|
+
}
|
|
1011
|
+
let Some(query) = self.command.trim_start().strip_prefix('/') else {
|
|
1012
|
+
return Vec::new();
|
|
1013
|
+
};
|
|
1014
|
+
let query = query.to_lowercase();
|
|
1015
|
+
COMMAND_HINTS
|
|
1016
|
+
.iter()
|
|
1017
|
+
.filter(|hint| hint.insert.starts_with(query.trim_start()))
|
|
1018
|
+
.take(7)
|
|
1019
|
+
.collect()
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
fn move_command_palette(&mut self, delta: isize) {
|
|
1023
|
+
let len = self.command_suggestions().len();
|
|
1024
|
+
if len == 0 {
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
let cursor = self.command_palette_cursor as isize;
|
|
1028
|
+
self.command_palette_cursor = ((cursor + delta).rem_euclid(len as isize)) as usize;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
fn accept_command_palette(&mut self) -> Result<bool> {
|
|
1032
|
+
let suggestions = self.command_suggestions();
|
|
1033
|
+
if suggestions.is_empty() {
|
|
1034
|
+
return Ok(false);
|
|
1035
|
+
}
|
|
1036
|
+
let hint = suggestions[self.command_palette_cursor.min(suggestions.len() - 1)];
|
|
1037
|
+
if hint.keep_open {
|
|
1038
|
+
self.command = format!("/{}", hint.insert);
|
|
1039
|
+
self.command_cursor = char_len(&self.command);
|
|
1040
|
+
self.command_palette_cursor = 0;
|
|
1041
|
+
return Ok(true);
|
|
1042
|
+
}
|
|
1043
|
+
let value = hint.insert;
|
|
1044
|
+
self.command.clear();
|
|
1045
|
+
self.command_cursor = 0;
|
|
1046
|
+
self.command_palette_cursor = 0;
|
|
1047
|
+
self.focus = Focus::None;
|
|
1048
|
+
self.submit_command(value)?;
|
|
1049
|
+
Ok(true)
|
|
1050
|
+
}
|
|
1051
|
+
|
|
769
1052
|
fn normalize_command_input(&mut self) {
|
|
770
1053
|
let normalized = compose_hangul_jamo(&self.command);
|
|
771
1054
|
if normalized == self.command {
|
|
@@ -984,14 +1267,34 @@ impl PracticodeApp {
|
|
|
984
1267
|
}
|
|
985
1268
|
|
|
986
1269
|
fn mode_hint(&self) -> &'static str {
|
|
1270
|
+
let lang = &self.state.settings.ui_language;
|
|
987
1271
|
match (self.focus, self.list_cursor.is_some(), self.show_output) {
|
|
988
|
-
(Focus::Command, _, _) =>
|
|
989
|
-
(_, true, _) =>
|
|
990
|
-
(_, _, true) =>
|
|
991
|
-
(Focus::Code, _, _) =>
|
|
992
|
-
_ =>
|
|
1272
|
+
(Focus::Command, _, _) => ui_text(lang, "hint_command"),
|
|
1273
|
+
(_, true, _) => ui_text(lang, "hint_list"),
|
|
1274
|
+
(_, _, true) => ui_text(lang, "hint_output"),
|
|
1275
|
+
(Focus::Code, _, _) => ui_text(lang, "hint_code"),
|
|
1276
|
+
_ => ui_text(lang, "hint_idle"),
|
|
993
1277
|
}
|
|
994
1278
|
}
|
|
1279
|
+
|
|
1280
|
+
fn help_text(&self) -> String {
|
|
1281
|
+
let lang = &self.state.settings.ui_language;
|
|
1282
|
+
let commands = COMMAND_HINTS
|
|
1283
|
+
.iter()
|
|
1284
|
+
.filter(|hint| hint.help)
|
|
1285
|
+
.map(|hint| format!("- `{}` {}", hint.display, ui_text(lang, hint.desc_key)))
|
|
1286
|
+
.collect::<Vec<_>>()
|
|
1287
|
+
.join("\n");
|
|
1288
|
+
format!(
|
|
1289
|
+
"# {}\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.",
|
|
1290
|
+
ui_text(lang, "help_title"),
|
|
1291
|
+
ui_text(lang, "daily_loop"),
|
|
1292
|
+
ui_text(lang, "commands"),
|
|
1293
|
+
commands,
|
|
1294
|
+
ui_text(lang, "keys"),
|
|
1295
|
+
ui_text(lang, "debug_prints"),
|
|
1296
|
+
)
|
|
1297
|
+
}
|
|
995
1298
|
}
|
|
996
1299
|
|
|
997
1300
|
#[derive(Clone, Debug)]
|