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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/README.md +5 -0
- package/SECURITY.md +11 -0
- package/docs/ARCHITECTURE.md +20 -4
- package/docs/MAINTAINING.md +5 -0
- package/package.json +7 -2
- package/src/core/bank.rs +148 -0
- package/src/core/judge.rs +205 -0
- package/src/core/language.rs +92 -0
- package/src/core/model.rs +153 -0
- package/src/core/problem_files.rs +92 -0
- package/src/core/progress.rs +97 -0
- package/src/core/render.rs +119 -0
- package/src/core/state.rs +80 -0
- package/src/core.rs +18 -983
- package/src/tui/actions.rs +312 -0
- package/src/tui/command_handlers.rs +128 -0
- package/src/tui/command_input.rs +117 -0
- package/src/tui/events.rs +188 -0
- package/src/tui/problem_list.rs +163 -0
- package/src/tui/status.rs +106 -0
- package/src/tui/tasks.rs +279 -0
- package/src/tui/view.rs +342 -0
- package/src/tui.rs +8 -1607
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
|
|
3
|
+
impl PracticodeApp {
|
|
4
|
+
pub(super) fn handle_key(&mut self, key: KeyEvent) -> Result<()> {
|
|
5
|
+
if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) {
|
|
6
|
+
self.should_quit = true;
|
|
7
|
+
return Ok(());
|
|
8
|
+
}
|
|
9
|
+
if self.handle_busy_key(key) {
|
|
10
|
+
return Ok(());
|
|
11
|
+
}
|
|
12
|
+
match self.focus {
|
|
13
|
+
Focus::Command => self.handle_command_key(key),
|
|
14
|
+
Focus::Code => self.handle_code_key(key),
|
|
15
|
+
_ => self.handle_global_key(key),
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
pub(super) fn handle_mouse(&mut self, mouse: MouseEvent) -> Result<()> {
|
|
20
|
+
if self.task_rx.is_some() {
|
|
21
|
+
self.focus = Focus::Output;
|
|
22
|
+
return Ok(());
|
|
23
|
+
}
|
|
24
|
+
if !matches!(mouse.kind, MouseEventKind::Down(MouseButton::Left)) {
|
|
25
|
+
return Ok(());
|
|
26
|
+
}
|
|
27
|
+
let position = Position::new(mouse.column, mouse.row);
|
|
28
|
+
if self.command_area.contains(position) {
|
|
29
|
+
self.focus_command();
|
|
30
|
+
} else if self.show_output && self.output_area.contains(position) {
|
|
31
|
+
self.focus = Focus::Output;
|
|
32
|
+
} else if self.code_area.contains(position) {
|
|
33
|
+
self.action_edit()?;
|
|
34
|
+
}
|
|
35
|
+
Ok(())
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
pub(super) fn handle_command_key(&mut self, key: KeyEvent) -> Result<()> {
|
|
39
|
+
match key.code {
|
|
40
|
+
KeyCode::Esc => {
|
|
41
|
+
self.command.clear();
|
|
42
|
+
self.command_cursor = 0;
|
|
43
|
+
self.command_palette_cursor = 0;
|
|
44
|
+
self.focus = Focus::None;
|
|
45
|
+
}
|
|
46
|
+
KeyCode::Enter => {
|
|
47
|
+
if !self.accept_command_palette()? {
|
|
48
|
+
let value = self.command.trim().to_string();
|
|
49
|
+
self.command.clear();
|
|
50
|
+
self.command_cursor = 0;
|
|
51
|
+
self.command_palette_cursor = 0;
|
|
52
|
+
self.focus = Focus::None;
|
|
53
|
+
self.submit_command(&value)?;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
KeyCode::Backspace => self.delete_command_before_cursor(),
|
|
57
|
+
KeyCode::Delete => self.delete_command_at_cursor(),
|
|
58
|
+
KeyCode::Left => self.command_cursor = self.command_cursor.saturating_sub(1),
|
|
59
|
+
KeyCode::Right => {
|
|
60
|
+
self.command_cursor = (self.command_cursor + 1).min(char_len(&self.command));
|
|
61
|
+
}
|
|
62
|
+
KeyCode::Up => self.move_command_palette(-1),
|
|
63
|
+
KeyCode::Down => self.move_command_palette(1),
|
|
64
|
+
KeyCode::Home => self.command_cursor = 0,
|
|
65
|
+
KeyCode::End => self.command_cursor = char_len(&self.command),
|
|
66
|
+
KeyCode::Char('?') if self.command.trim().is_empty() || self.command.trim() == "/" => {
|
|
67
|
+
self.command.clear();
|
|
68
|
+
self.command_cursor = 0;
|
|
69
|
+
self.command_palette_cursor = 0;
|
|
70
|
+
self.focus = Focus::None;
|
|
71
|
+
self.handle_command("help")?;
|
|
72
|
+
}
|
|
73
|
+
KeyCode::Char(char) if !key.modifiers.contains(KeyModifiers::CONTROL) => {
|
|
74
|
+
self.insert_command_char(char);
|
|
75
|
+
}
|
|
76
|
+
_ => {}
|
|
77
|
+
}
|
|
78
|
+
Ok(())
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
pub(super) fn handle_code_key(&mut self, key: KeyEvent) -> Result<()> {
|
|
82
|
+
match key.code {
|
|
83
|
+
KeyCode::Esc => self.focus = Focus::None,
|
|
84
|
+
KeyCode::Char(char) if !key.modifiers.contains(KeyModifiers::CONTROL) => {
|
|
85
|
+
self.editor.insert_char(char);
|
|
86
|
+
self.save_code()?;
|
|
87
|
+
}
|
|
88
|
+
KeyCode::Enter => {
|
|
89
|
+
self.editor.insert_newline();
|
|
90
|
+
self.save_code()?;
|
|
91
|
+
}
|
|
92
|
+
KeyCode::Backspace => {
|
|
93
|
+
self.editor.backspace();
|
|
94
|
+
self.save_code()?;
|
|
95
|
+
}
|
|
96
|
+
KeyCode::Delete => {
|
|
97
|
+
self.editor.delete();
|
|
98
|
+
self.save_code()?;
|
|
99
|
+
}
|
|
100
|
+
KeyCode::Tab => {
|
|
101
|
+
for _ in 0..4 {
|
|
102
|
+
self.editor.insert_char(' ');
|
|
103
|
+
}
|
|
104
|
+
self.save_code()?;
|
|
105
|
+
}
|
|
106
|
+
KeyCode::Left => self.editor.move_left(),
|
|
107
|
+
KeyCode::Right => self.editor.move_right(),
|
|
108
|
+
KeyCode::Up => self.editor.move_up(),
|
|
109
|
+
KeyCode::Down => self.editor.move_down(),
|
|
110
|
+
_ => {}
|
|
111
|
+
}
|
|
112
|
+
Ok(())
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
pub(super) fn handle_global_key(&mut self, key: KeyEvent) -> Result<()> {
|
|
116
|
+
if self.settings_cursor.is_some() {
|
|
117
|
+
match key.code {
|
|
118
|
+
KeyCode::Up | KeyCode::Char('k') => self.move_settings_cursor(-1),
|
|
119
|
+
KeyCode::Down | KeyCode::Char('j') => self.move_settings_cursor(1),
|
|
120
|
+
KeyCode::Char(' ') | KeyCode::Enter => self.change_selected_setting()?,
|
|
121
|
+
KeyCode::Esc => {
|
|
122
|
+
self.settings_cursor = None;
|
|
123
|
+
self.show_output = false;
|
|
124
|
+
self.focus = Focus::Code;
|
|
125
|
+
}
|
|
126
|
+
_ => self.handle_global_shortcut(key)?,
|
|
127
|
+
}
|
|
128
|
+
return Ok(());
|
|
129
|
+
}
|
|
130
|
+
if let Some(cursor) = self.list_cursor {
|
|
131
|
+
match key.code {
|
|
132
|
+
KeyCode::Up | KeyCode::Char('k') => self.move_list_cursor(-1),
|
|
133
|
+
KeyCode::Down | KeyCode::Char('j') => self.move_list_cursor(1),
|
|
134
|
+
KeyCode::Enter => self.open_selected_problem()?,
|
|
135
|
+
KeyCode::Esc => {
|
|
136
|
+
self.list_cursor = None;
|
|
137
|
+
self.write_text_output("Closed list.");
|
|
138
|
+
}
|
|
139
|
+
_ => {
|
|
140
|
+
self.list_cursor = Some(cursor);
|
|
141
|
+
self.handle_global_shortcut(key)?;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return Ok(());
|
|
145
|
+
}
|
|
146
|
+
if key.code == KeyCode::Esc && self.show_output {
|
|
147
|
+
self.show_output = false;
|
|
148
|
+
self.focus = Focus::Code;
|
|
149
|
+
return Ok(());
|
|
150
|
+
}
|
|
151
|
+
self.handle_global_shortcut(key)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
pub(super) fn handle_global_shortcut(&mut self, key: KeyEvent) -> Result<()> {
|
|
155
|
+
match key.code {
|
|
156
|
+
KeyCode::Char('/') => self.focus_command(),
|
|
157
|
+
KeyCode::Char('?') => self.handle_command("help")?,
|
|
158
|
+
KeyCode::Char('r') => self.action_run()?,
|
|
159
|
+
KeyCode::Char('n') => self.action_next("")?,
|
|
160
|
+
KeyCode::Char('p') => self.action_previous()?,
|
|
161
|
+
KeyCode::Char('g') => self.action_give_up()?,
|
|
162
|
+
KeyCode::Char('e') => self.action_edit()?,
|
|
163
|
+
KeyCode::Char('l') => self.action_cycle_language()?,
|
|
164
|
+
KeyCode::Char('u') => self.action_toggle_ui_language()?,
|
|
165
|
+
KeyCode::Char('q') => self.should_quit = true,
|
|
166
|
+
_ => {}
|
|
167
|
+
}
|
|
168
|
+
Ok(())
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
pub(super) fn focus_command(&mut self) {
|
|
172
|
+
if self.command.is_empty() {
|
|
173
|
+
self.command.push('/');
|
|
174
|
+
self.command_cursor = 1;
|
|
175
|
+
}
|
|
176
|
+
self.command_palette_cursor = 0;
|
|
177
|
+
self.focus = Focus::Command;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
pub(super) fn submit_command(&mut self, value: &str) -> Result<()> {
|
|
181
|
+
let value = value
|
|
182
|
+
.trim()
|
|
183
|
+
.strip_prefix('/')
|
|
184
|
+
.unwrap_or(value.trim())
|
|
185
|
+
.trim();
|
|
186
|
+
self.handle_command(value)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
|
|
3
|
+
impl PracticodeApp {
|
|
4
|
+
pub(super) fn load_code_editor(&mut self) -> Result<()> {
|
|
5
|
+
let path = ensure_submission(&self.root, &self.problem, &self.state.settings)?;
|
|
6
|
+
let text = fs::read_to_string(path).unwrap_or_default();
|
|
7
|
+
self.editor.set_text(&text);
|
|
8
|
+
Ok(())
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
pub(super) fn save_code(&self) -> Result<()> {
|
|
12
|
+
let path = ensure_submission(&self.root, &self.problem, &self.state.settings)?;
|
|
13
|
+
fs::write(path, self.editor.text())?;
|
|
14
|
+
Ok(())
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
pub(super) fn start_problem_list(&mut self) {
|
|
18
|
+
self.list_cursor = Some(self.current_problem_index());
|
|
19
|
+
self.write_text_output(&self.render_problem_list());
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
pub(super) fn render_problem_list(&self) -> String {
|
|
23
|
+
let status_by_id = self
|
|
24
|
+
.state
|
|
25
|
+
.history
|
|
26
|
+
.iter()
|
|
27
|
+
.map(|item| (item.id.as_str(), item.status.as_str()))
|
|
28
|
+
.collect::<HashMap<_, _>>();
|
|
29
|
+
let cursor = self
|
|
30
|
+
.list_cursor
|
|
31
|
+
.unwrap_or_else(|| self.current_problem_index());
|
|
32
|
+
let mut lines = vec![
|
|
33
|
+
"Problems".to_string(),
|
|
34
|
+
String::new(),
|
|
35
|
+
" # ID Difficulty Status Code Title".to_string(),
|
|
36
|
+
];
|
|
37
|
+
for (index, problem) in self.bank.iter().enumerate() {
|
|
38
|
+
let marker = if index == cursor { ">" } else { " " };
|
|
39
|
+
let current = if problem.id == self.problem.id {
|
|
40
|
+
"*"
|
|
41
|
+
} else {
|
|
42
|
+
" "
|
|
43
|
+
};
|
|
44
|
+
let title = localized(&problem.title, &self.state.settings.ui_language);
|
|
45
|
+
let code_status = self.submission_status(problem).0;
|
|
46
|
+
lines.push(format!(
|
|
47
|
+
"{marker} {current} {:>2} {:<18} {:<10} {:<10} {:<9} {title}",
|
|
48
|
+
index + 1,
|
|
49
|
+
problem.id,
|
|
50
|
+
problem.difficulty,
|
|
51
|
+
status_by_id
|
|
52
|
+
.get(problem.id.as_str())
|
|
53
|
+
.copied()
|
|
54
|
+
.unwrap_or("-"),
|
|
55
|
+
code_status,
|
|
56
|
+
));
|
|
57
|
+
}
|
|
58
|
+
lines.push("\nup/down or j/k select | enter open | esc close".to_string());
|
|
59
|
+
lines.join("\n")
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
pub(super) fn current_problem_index(&self) -> usize {
|
|
63
|
+
self.bank
|
|
64
|
+
.iter()
|
|
65
|
+
.position(|problem| problem.id == self.problem.id)
|
|
66
|
+
.unwrap_or(0)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
pub(super) fn move_list_cursor(&mut self, delta: isize) {
|
|
70
|
+
if self.bank.is_empty() {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
let cursor = self
|
|
74
|
+
.list_cursor
|
|
75
|
+
.unwrap_or_else(|| self.current_problem_index()) as isize;
|
|
76
|
+
let len = self.bank.len() as isize;
|
|
77
|
+
self.list_cursor = Some(((cursor + delta).rem_euclid(len)) as usize);
|
|
78
|
+
self.write_text_output(&self.render_problem_list());
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
pub(super) fn open_selected_problem(&mut self) -> Result<()> {
|
|
82
|
+
if let Some(cursor) = self.list_cursor {
|
|
83
|
+
let problem_id = self.bank[cursor].id.clone();
|
|
84
|
+
self.list_cursor = None;
|
|
85
|
+
self.open_problem(&problem_id)?;
|
|
86
|
+
}
|
|
87
|
+
Ok(())
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
pub(super) fn open_problem(&mut self, query: &str) -> Result<()> {
|
|
91
|
+
self.list_cursor = None;
|
|
92
|
+
let Some(problem) = self.find_problem(query).cloned() else {
|
|
93
|
+
self.write_text_output(&format!("Problem not found: {query}\nTry /problems."));
|
|
94
|
+
return Ok(());
|
|
95
|
+
};
|
|
96
|
+
self.problem = problem;
|
|
97
|
+
self.state.current_problem = self.problem.id.clone();
|
|
98
|
+
if !self
|
|
99
|
+
.state
|
|
100
|
+
.history
|
|
101
|
+
.iter()
|
|
102
|
+
.any(|item| item.id == self.problem.id)
|
|
103
|
+
{
|
|
104
|
+
self.state.history.push(HistoryItem {
|
|
105
|
+
id: self.problem.id.clone(),
|
|
106
|
+
status: "assigned".to_string(),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
save_state(&self.root, &self.state)?;
|
|
110
|
+
ensure_problem_files(&self.root, &self.problem)?;
|
|
111
|
+
self.load_code_editor()?;
|
|
112
|
+
self.show_output = false;
|
|
113
|
+
self.focus = Focus::Code;
|
|
114
|
+
Ok(())
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
pub(super) fn find_problem(&self, query: &str) -> Option<&Problem> {
|
|
118
|
+
let needle = if query.trim().chars().all(|c| c.is_ascii_digit()) {
|
|
119
|
+
format!("{:03}", query.trim().parse::<usize>().ok()?)
|
|
120
|
+
} else {
|
|
121
|
+
query.trim().to_lowercase()
|
|
122
|
+
};
|
|
123
|
+
self.bank.iter().find(|problem| {
|
|
124
|
+
needle == problem.id.to_lowercase()
|
|
125
|
+
|| needle == problem.slug.to_lowercase()
|
|
126
|
+
|| problem.id.starts_with(&needle)
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
pub(super) fn problem_status(&self, problem: &Problem) -> String {
|
|
131
|
+
if self.state.solved.contains(&problem.id) {
|
|
132
|
+
return "solved".to_string();
|
|
133
|
+
}
|
|
134
|
+
self.state
|
|
135
|
+
.history
|
|
136
|
+
.iter()
|
|
137
|
+
.rev()
|
|
138
|
+
.find(|item| item.id == problem.id)
|
|
139
|
+
.map(|item| item.status.clone())
|
|
140
|
+
.unwrap_or_else(|| "not_started".to_string())
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
pub(super) fn submission_status(&self, problem: &Problem) -> (String, String) {
|
|
144
|
+
let language = normalize_language(&self.state.settings.language);
|
|
145
|
+
let path = self
|
|
146
|
+
.root
|
|
147
|
+
.join("submissions")
|
|
148
|
+
.join(&problem.id)
|
|
149
|
+
.join(format!("solution.{}", ext_for(&language)));
|
|
150
|
+
if !path.exists() {
|
|
151
|
+
return ("missing".to_string(), format!("({language})"));
|
|
152
|
+
}
|
|
153
|
+
let content = fs::read_to_string(&path).unwrap_or_default();
|
|
154
|
+
let relative = path.strip_prefix(&self.root).unwrap_or(&path).display();
|
|
155
|
+
if content == template_for(&language) {
|
|
156
|
+
("template".to_string(), format!("({relative})"))
|
|
157
|
+
} else if content.trim().is_empty() {
|
|
158
|
+
("empty".to_string(), format!("({relative})"))
|
|
159
|
+
} else {
|
|
160
|
+
("written".to_string(), format!("({relative})"))
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
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
|
+
match (self.focus, self.list_cursor.is_some(), self.show_output) {
|
|
79
|
+
(Focus::Command, _, _) => ui_text(lang, "hint_command"),
|
|
80
|
+
(_, true, _) => ui_text(lang, "hint_list"),
|
|
81
|
+
(_, _, true) if self.settings_cursor.is_some() => ui_text(lang, "hint_settings"),
|
|
82
|
+
(_, _, true) => ui_text(lang, "hint_output"),
|
|
83
|
+
(Focus::Code, _, _) => ui_text(lang, "hint_code"),
|
|
84
|
+
_ => ui_text(lang, "hint_idle"),
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
pub(super) fn help_text(&self) -> String {
|
|
89
|
+
let lang = &self.state.settings.ui_language;
|
|
90
|
+
let commands = COMMAND_HINTS
|
|
91
|
+
.iter()
|
|
92
|
+
.filter(|hint| hint.help)
|
|
93
|
+
.map(|hint| format!("- `{}` {}", hint.display, ui_text(lang, hint.desc_key)))
|
|
94
|
+
.collect::<Vec<_>>()
|
|
95
|
+
.join("\n");
|
|
96
|
+
format!(
|
|
97
|
+
"# {}\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.",
|
|
98
|
+
ui_text(lang, "help_title"),
|
|
99
|
+
ui_text(lang, "daily_loop"),
|
|
100
|
+
ui_text(lang, "commands"),
|
|
101
|
+
commands,
|
|
102
|
+
ui_text(lang, "keys"),
|
|
103
|
+
ui_text(lang, "debug_prints"),
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
}
|