1use crate::content::types::{Exercise, ModuleFile};
2use crate::executor::{ExecutionResult, Executor};
3use crate::matcher::Matcher;
4use crate::progress::{ModuleProgress, Progress};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum ContentView {
8 Intro,
9 Examples,
10 Exercise,
11}
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum SubmitState {
15 Idle,
16 Correct,
17 Wrong,
18 Error,
19}
20
21pub struct App {
22 pub modules: Vec<ModuleFile>,
23 pub selected_module: usize,
24 pub current_view: ContentView,
25 pub current_exercise: usize,
26
27 pub input: String,
28 pub cursor_pos: usize,
29
30 pub last_output: Option<ExecutionResult>,
31 pub submit_state: SubmitState,
32
33 pub hints_revealed: usize,
34 pub show_solution: bool,
35 pub show_files: bool,
36 pub show_help: bool,
37
38 pub intro_scroll: u16,
39 pub examples_scroll: u16,
40 pub output_scroll: u16,
41
42 pub progress: Progress,
43 pub should_quit: bool,
44}
45
46impl App {
47 pub fn new(modules: Vec<ModuleFile>) -> Self {
48 let progress = Progress::load();
49 App {
50 modules,
51 selected_module: 0,
52 current_view: ContentView::Intro,
53 current_exercise: 0,
54 input: String::new(),
55 cursor_pos: 0,
56 last_output: None,
57 submit_state: SubmitState::Idle,
58 hints_revealed: 0,
59 show_solution: false,
60 show_files: false,
61 show_help: false,
62 intro_scroll: 0,
63 examples_scroll: 0,
64 output_scroll: 0,
65 progress,
66 should_quit: false,
67 }
68 }
69
70 pub fn current_module(&self) -> &ModuleFile {
71 &self.modules[self.selected_module]
72 }
73
74 pub fn current_exercise_opt(&self) -> Option<&Exercise> {
75 self.current_module().exercises.get(self.current_exercise)
76 }
77
78 pub fn exercise_count(&self) -> usize {
79 self.current_module().exercises.len()
80 }
81
82 pub fn module_progress(&self) -> Option<&ModuleProgress> {
83 let name = &self.current_module().module.name;
84 self.progress.modules.get(name)
85 }
86
87 pub fn exercise_is_completed(&self) -> bool {
88 if let Some(ex) = self.current_exercise_opt() {
89 let module_name = &self.current_module().module.name;
90 self.progress.is_completed(module_name, &ex.id)
91 } else {
92 false
93 }
94 }
95
96 pub fn input_push(&mut self, c: char) {
99 self.input.insert(self.cursor_pos, c);
100 self.cursor_pos += c.len_utf8();
101 }
102
103 pub fn input_backspace(&mut self) {
104 if self.cursor_pos > 0 {
105 let prev = self.input[..self.cursor_pos]
106 .char_indices()
107 .last()
108 .map(|(i, _)| i)
109 .unwrap_or(0);
110 self.input.remove(prev);
111 self.cursor_pos = prev;
112 }
113 }
114
115 pub fn input_delete(&mut self) {
116 if self.cursor_pos < self.input.len() {
117 self.input.remove(self.cursor_pos);
118 }
119 }
120
121 pub fn cursor_left(&mut self) {
122 if self.cursor_pos > 0 {
123 self.cursor_pos = self.input[..self.cursor_pos]
124 .char_indices()
125 .last()
126 .map(|(i, _)| i)
127 .unwrap_or(0);
128 }
129 }
130
131 pub fn cursor_right(&mut self) {
132 if self.cursor_pos < self.input.len() {
133 let mut chars = self.input[self.cursor_pos..].char_indices();
134 if let Some((_, c)) = chars.next() {
135 self.cursor_pos += c.len_utf8();
136 }
137 }
138 }
139
140 pub fn cursor_home(&mut self) {
141 self.cursor_pos = 0;
142 }
143
144 pub fn cursor_end(&mut self) {
145 self.cursor_pos = self.input.len();
146 }
147
148 pub fn clear_input(&mut self) {
149 self.input.clear();
150 self.cursor_pos = 0;
151 }
152
153 pub fn select_prev_module(&mut self) {
156 if self.selected_module > 0 {
157 self.selected_module -= 1;
158 self.reset_content_state();
159 }
160 }
161
162 pub fn select_next_module(&mut self) {
163 if self.selected_module + 1 < self.modules.len() {
164 self.selected_module += 1;
165 self.reset_content_state();
166 }
167 }
168
169 pub fn cycle_view(&mut self) {
170 self.current_view = match self.current_view {
171 ContentView::Intro => ContentView::Examples,
172 ContentView::Examples => ContentView::Exercise,
173 ContentView::Exercise => ContentView::Intro,
174 };
175 }
176
177 pub fn next_exercise(&mut self) {
178 let count = self.exercise_count();
179 if count > 0 && self.current_exercise + 1 < count {
180 self.current_exercise += 1;
181 self.reset_exercise_state();
182 }
183 }
184
185 pub fn prev_exercise(&mut self) {
186 if self.current_exercise > 0 {
187 self.current_exercise -= 1;
188 self.reset_exercise_state();
189 }
190 }
191
192 pub fn submit_command(&mut self) {
195 let command = self.input.clone();
196 if command.trim().is_empty() {
197 return;
198 }
199
200 let exercise = match self.current_exercise_opt() {
201 Some(ex) => ex.clone(),
202 None => return,
203 };
204
205 let result = Executor::run(&command, &exercise.fixtures);
206 let (exec_result, exec_error) = match result {
207 Ok(o) => (Some(o), false),
208 Err(e) => (
209 Some(ExecutionResult {
210 stdout: String::new(),
211 stderr: e.to_string(),
212 timed_out: false,
213 }),
214 true,
215 ),
216 };
217
218 let correct = exec_result
219 .as_ref()
220 .map(|o| {
221 !exec_error
222 && Matcher::check(&o.stdout, &exercise.expected_output, &exercise.match_mode)
223 })
224 .unwrap_or(false);
225
226 self.last_output = exec_result;
227 self.output_scroll = 0;
228
229 let module_name = self.current_module().module.name.clone();
230 if exec_error {
231 self.submit_state = SubmitState::Error;
232 } else if correct {
233 self.submit_state = SubmitState::Correct;
234 self.progress.mark_completed(&module_name, &exercise.id);
235 let _ = self.progress.save();
236 } else {
237 self.submit_state = SubmitState::Wrong;
238 self.progress.mark_attempted(&module_name, &exercise.id);
239 }
240 }
241
242 pub fn reveal_next_hint(&mut self) {
243 if let Some(ex) = self.current_exercise_opt() {
244 if self.hints_revealed < ex.hints.len() {
245 self.hints_revealed += 1;
246 }
247 }
248 }
249
250 pub fn toggle_solution(&mut self) {
251 self.show_solution = !self.show_solution;
252 }
253
254 pub fn toggle_files(&mut self) {
255 self.show_files = !self.show_files;
256 }
257
258 pub fn toggle_help(&mut self) {
259 self.show_help = !self.show_help;
260 }
261
262 pub fn reset_exercise(&mut self) {
263 self.reset_exercise_state();
264 }
265
266 pub fn clear_output(&mut self) {
267 self.last_output = None;
268 self.submit_state = SubmitState::Idle;
269 self.output_scroll = 0;
270 }
271
272 pub fn scroll_up(&mut self) {
275 match self.current_view {
276 ContentView::Intro => self.intro_scroll = self.intro_scroll.saturating_sub(1),
277 ContentView::Examples => self.examples_scroll = self.examples_scroll.saturating_sub(1),
278 ContentView::Exercise => self.output_scroll = self.output_scroll.saturating_sub(1),
279 }
280 }
281
282 pub fn scroll_down(&mut self) {
283 match self.current_view {
284 ContentView::Intro => self.intro_scroll += 1,
285 ContentView::Examples => self.examples_scroll += 1,
286 ContentView::Exercise => self.output_scroll += 1,
287 }
288 }
289
290 fn reset_content_state(&mut self) {
293 self.current_view = ContentView::Intro;
294 self.current_exercise = 0;
295 self.reset_exercise_state();
296 self.intro_scroll = 0;
297 self.examples_scroll = 0;
298 }
299
300 fn reset_exercise_state(&mut self) {
301 self.clear_input();
302 self.last_output = None;
303 self.submit_state = SubmitState::Idle;
304 self.hints_revealed = 0;
305 self.show_solution = false;
306 self.show_files = false;
307 self.output_scroll = 0;
308 }
309}