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,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
+ }