1use std::{cmp, fs::read_to_string};
2
3use crossterm::event::KeyCode;
4use notify::{PollWatcher, Watcher};
5
6use crate::{
7 nodes::{root::ComponentRoot, word::WordType},
8 pages::file_explorer::FileTree,
9 parser::parse_markdown,
10 util::{
11 App, Boxes, Jump, LinkType, Mode,
12 general::GENERAL_CONFIG,
13 keys::{Action, key_to_action},
14 },
15};
16
17pub enum KeyBoardAction {
18 Continue,
19 Edit,
20 Exit,
21}
22
23pub fn handle_keyboard_input(
24 key: KeyCode,
25 app: &mut App,
26 markdown: &mut ComponentRoot,
27 file_tree: &mut FileTree,
28 height: u16,
29 watcher: &mut PollWatcher,
30) -> KeyBoardAction {
31 if key == KeyCode::Char('q') && app.boxes != Boxes::Search {
32 return KeyBoardAction::Exit;
33 }
34 match app.mode {
35 Mode::FileTree => keyboard_mode_file_tree(key, app, markdown, file_tree, height, watcher),
36 Mode::View => keyboard_mode_view(key, app, markdown, height, watcher),
37 }
38}
39
40pub fn keyboard_mode_file_tree(
41 key: KeyCode,
42 app: &mut App,
43 markdown: &mut ComponentRoot,
44 file_tree: &mut FileTree,
45 height: u16,
46 watcher: &mut PollWatcher,
47) -> KeyBoardAction {
48 match app.boxes {
49 Boxes::Error => match key {
50 KeyCode::Enter | KeyCode::Esc => {
51 app.boxes = Boxes::None;
52 }
53 _ => {}
54 },
55 Boxes::Search => match key {
56 KeyCode::Esc => {
57 app.search_box.clear();
58 file_tree.search(None);
59 app.boxes = Boxes::None;
60 }
61 KeyCode::Enter => {
62 let query = app.search_box.consume();
63 file_tree.search(Some(&query));
64 app.boxes = Boxes::None;
65 }
66
67 KeyCode::Char(c) => {
68 app.search_box.insert(c);
69 file_tree.search(app.search_box.content());
70 let file_height = file_tree.height(height);
71 app.search_box.set_position(10, file_height as u16 + 2);
72 }
73
74 KeyCode::Backspace => {
75 if app.search_box.content().is_none() {
76 app.boxes = Boxes::None;
77 }
78 app.search_box.delete();
79 file_tree.search(app.search_box.content());
80 let file_height = file_tree.height(height);
81 app.search_box.set_position(10, file_height as u16 + 2);
82 }
83 _ => {}
84 },
85 Boxes::None => match key_to_action(key) {
86 Action::Down => {
87 file_tree.next(height);
88 }
89
90 Action::Up => {
91 file_tree.previous(height);
92 }
93
94 Action::PageDown => {
95 file_tree.next_page(height);
96 }
97
98 Action::PageUp => {
99 file_tree.previous_page(height);
100 }
101
102 Action::ToTop => {
103 file_tree.first();
104 }
105
106 Action::ToBottom => {
107 file_tree.last(height);
108 }
109
110 Action::Enter => {
111 let file = if let Some(file) = file_tree.selected() {
112 file
113 } else {
114 app.message_box.set_message("No file selected".to_string());
115 app.boxes = Boxes::Error;
116 return KeyBoardAction::Continue;
117 };
118 let text = if let Ok(file) = read_to_string(file.path_str()) {
119 app.reset();
120 file
121 } else {
122 app.message_box
123 .set_message(format!("Could not open file {}", file.path_str()));
124 app.boxes = Boxes::Error;
125 return KeyBoardAction::Continue;
126 };
127
128 *markdown = parse_markdown(Some(file.path_str()), &text, app.width() - 2);
129 let _ = watcher.watch(file.path(), notify::RecursiveMode::NonRecursive);
130 app.mode = Mode::View;
131 app.help_box.set_mode(Mode::View);
132 app.select_index = 0;
133 }
134 Action::Search => {
135 let file_height = file_tree.height(height);
136 app.search_box.set_position(10, file_height as u16 + 2);
137 app.search_box.set_width(20);
138 app.boxes = Boxes::Search;
139 app.help_box.close();
140 }
141
142 Action::Back => match app.history.pop() {
143 Jump::File(e) => {
144 let text = if let Ok(file) = read_to_string(&e) {
145 app.vertical_scroll = 0;
146 file
147 } else {
148 app.message_box
149 .set_message(format!("Could not open file {e}"));
150 app.boxes = Boxes::Error;
151 return KeyBoardAction::Continue;
152 };
153 *markdown = parse_markdown(Some(&e), &text, app.width() - 2);
154 let path = std::path::Path::new(&e);
155 let _ = watcher.watch(path, notify::RecursiveMode::NonRecursive);
156 app.reset();
157 app.mode = Mode::View;
158 app.help_box.set_mode(Mode::View);
159 }
160 Jump::FileTree => {
161 markdown.clear();
162 app.mode = Mode::FileTree;
163 app.help_box.set_mode(Mode::FileTree);
164 }
165 },
166 Action::Help if GENERAL_CONFIG.help_menu => {
167 app.help_box.toggle();
168 }
169 Action::Help => {}
170
171 Action::Escape => {
172 file_tree.unselect();
173 file_tree.search(None);
174 }
175
176 Action::Sort => {
177 file_tree.sort_name();
178 }
179 _ => {}
180 },
181 Boxes::LinkPreview => {
182 if key == KeyCode::Esc {
183 app.boxes = Boxes::None;
184 }
185 }
186 }
187
188 KeyBoardAction::Continue
189}
190
191fn keyboard_mode_view(
192 key: KeyCode,
193 app: &mut App,
194 markdown: &mut ComponentRoot,
195 height: u16,
196 watcher: &mut PollWatcher,
197) -> KeyBoardAction {
198 match app.boxes {
199 Boxes::Error => match key {
200 KeyCode::Enter | KeyCode::Esc => {
201 app.boxes = Boxes::None;
202 }
203 _ => {}
204 },
205 Boxes::Search => match key {
206 KeyCode::Esc => {
207 app.search_box.clear();
208 app.boxes = Boxes::None;
209 }
210 KeyCode::Enter => {
211 let query = app.search_box.content_str();
212
213 markdown.deselect();
214
215 markdown.find_and_mark(query);
216
217 let heights = markdown.search_results_heights();
218
219 if heights.is_empty() {
220 app.message_box
221 .set_message(format!("No results found for\n {query}"));
222 app.boxes = Boxes::Error;
223 return KeyBoardAction::Continue;
224 }
225
226 let next = heights
227 .iter()
228 .find(|row| **row >= (app.vertical_scroll as usize + height as usize / 2));
229
230 if let Some(index) = next {
231 app.vertical_scroll = cmp::min(
232 (*index as u16).saturating_sub(height / 2),
233 markdown.height().saturating_sub(height / 2),
234 );
235 }
236
237 app.boxes = Boxes::None;
238 }
239 KeyCode::Char(c) => {
240 app.search_box.insert(c);
241 }
242 KeyCode::Backspace => {
243 app.search_box.delete();
244 }
245 _ => {}
246 },
247 Boxes::None => match key_to_action(key) {
248 Action::Down => {
249 if app.selected {
250 app.select_index = cmp::min(app.select_index + 1, markdown.num_links() - 1);
251 app.vertical_scroll = if let Ok(scroll) = markdown.select(app.select_index) {
252 app.selected = true;
253 scroll.saturating_sub(height / 3)
254 } else {
255 app.vertical_scroll
256 };
257 } else if app.details_selected {
258 let max_idx = markdown.num_details().saturating_sub(1);
259 app.details_select_index = cmp::min(app.details_select_index + 1, max_idx);
260 app.vertical_scroll =
261 if let Ok(scroll) = markdown.select_details(app.details_select_index) {
262 app.details_selected = true;
263 scroll.saturating_sub(height / 3)
264 } else {
265 app.vertical_scroll
266 };
267 } else {
268 app.vertical_scroll = cmp::min(
269 app.vertical_scroll + 1,
270 markdown.height().saturating_sub(height / 2),
271 );
272 }
273 }
274 Action::Up => {
275 if app.selected {
276 app.select_index = app.select_index.saturating_sub(1);
277 app.vertical_scroll = if let Ok(scroll) = markdown.select(app.select_index) {
278 app.selected = true;
279 scroll.saturating_sub(height / 3)
280 } else {
281 app.vertical_scroll
282 };
283 } else if app.details_selected {
284 app.details_select_index = app.details_select_index.saturating_sub(1);
285 app.vertical_scroll =
286 if let Ok(scroll) = markdown.select_details(app.details_select_index) {
287 app.details_selected = true;
288 scroll.saturating_sub(height / 3)
289 } else {
290 app.vertical_scroll
291 };
292 } else {
293 app.vertical_scroll = app.vertical_scroll.saturating_sub(1);
294 }
295 }
296 Action::ToTop => {
297 app.vertical_scroll = 0;
298 }
299 Action::ToBottom => {
300 app.vertical_scroll = markdown.height().saturating_sub(height / 2);
301 }
302
303 Action::HalfPageDown => {
304 app.vertical_scroll += height / 2;
305 app.vertical_scroll = cmp::min(
306 app.vertical_scroll,
307 markdown.height().saturating_sub(height / 2),
308 );
309 }
310 Action::HalfPageUp => {
311 app.vertical_scroll = app.vertical_scroll.saturating_sub(height / 2);
312 }
313
314 Action::PageDown => {
315 app.vertical_scroll = cmp::min(
316 app.vertical_scroll + height,
317 markdown.height().saturating_sub(height / 2),
318 );
319 }
320
321 Action::PageUp => {
322 app.vertical_scroll = app.vertical_scroll.saturating_sub(height);
323 }
324
325 Action::Hover => {
326 if app.selected {
327 let link = markdown.selected();
328
329 let prev_type = markdown.selected_underlying_type();
330
331 if prev_type == WordType::FootnoteInline {
332 app.link_box
333 .set_message(format!("Footnote: {}", markdown.find_footnote(link)));
334 app.boxes = Boxes::LinkPreview;
335 return KeyBoardAction::Continue;
336 }
337
338 let message = match LinkType::from(link) {
339 LinkType::Internal(e) => format!("Internal link: {e}"),
340 LinkType::External(e) => format!("External link: {e}"),
341 LinkType::MarkdownFile(e) => format!("Markdown file: {e}"),
342 };
343
344 app.link_box.set_message(message);
345 app.boxes = Boxes::LinkPreview;
346 } else {
347 app.message_box.set_message("No link selected".to_string());
348 app.boxes = Boxes::Error;
349 }
350 }
351
352 Action::SelectLinkAlt => {
354 let links = markdown.link_index_and_height();
355 if links.is_empty() {
356 app.message_box.set_message("No links found".to_string());
357 app.boxes = Boxes::Error;
358 return KeyBoardAction::Continue;
359 }
360
361 let next = links
362 .iter()
363 .min_by_key(|(_, row)| (*row).abs_diff(app.vertical_scroll + height / 3));
364
365 if let Some((index, _)) = next {
366 app.vertical_scroll = if let Ok(scroll) = markdown.select(*index) {
367 app.select_index = *index;
368 scroll.saturating_sub(height / 3)
369 } else {
370 app.vertical_scroll
371 };
372 app.selected = true;
373 app.details_selected = false;
374 markdown.deselect_details();
375 } else {
376 markdown.deselect();
378 }
379 }
380
381 Action::SelectLink => {
383 let mut links = markdown.link_index_and_height();
384 if links.is_empty() {
385 app.message_box.set_message("No links found".to_string());
386 app.boxes = Boxes::Error;
387 return KeyBoardAction::Continue;
388 }
389
390 let mut index = usize::MAX;
391 while let Some(top) = links.pop() {
392 if top.1 >= app.vertical_scroll || index == usize::MAX {
393 index = top.0;
394 } else {
395 break;
396 }
397 }
398
399 app.select_index = index;
400 app.selected = true;
401 app.details_selected = false;
402 markdown.deselect_details();
403 app.vertical_scroll = if let Ok(scroll) = markdown.select(app.select_index) {
404 scroll.saturating_sub(height / 3)
405 } else {
406 app.vertical_scroll
407 };
408 }
409
410 Action::SelectDetails => {
414 let details = markdown.details_index_and_height();
415 if details.is_empty() {
416 app.message_box
417 .set_message("No details blocks found".to_string());
418 app.boxes = Boxes::Error;
419 return KeyBoardAction::Continue;
420 }
421
422 app.selected = false;
425 markdown.deselect();
426
427 let next_idx = if app.details_selected {
428 cmp::min(app.details_select_index + 1, details.len() - 1)
430 } else {
431 details
434 .iter()
435 .find(|(_, y)| *y >= app.vertical_scroll)
436 .map(|(i, _)| *i)
437 .unwrap_or(details.last().map(|(i, _)| *i).unwrap_or(0))
438 };
439
440 app.details_select_index = next_idx;
441 app.details_selected = true;
442 app.vertical_scroll = if let Ok(scroll) = markdown.select_details(next_idx) {
443 scroll.saturating_sub(height / 3)
444 } else {
445 app.vertical_scroll
446 };
447 }
448
449 Action::Search => {
450 app.search_box.clear();
451 app.search_box.set_position(2, height - 3);
452 app.search_box.set_width(GENERAL_CONFIG.width - 3);
453 app.boxes = Boxes::Search;
454 app.help_box.close();
455 }
456
457 Action::ToFileTree => {
458 app.mode = Mode::FileTree;
459 app.help_box.set_mode(Mode::FileTree);
460 if let Some(file) = markdown.file_name() {
461 app.history.push(Jump::File(file.to_string()));
462 }
463 app.reset();
464 }
465
466 Action::SearchNext => {
467 let heights = markdown.search_results_heights();
468
469 let next = heights
470 .iter()
471 .find(|row| **row > (app.vertical_scroll as usize + height as usize / 2));
472
473 if let Some(index) = next {
474 app.vertical_scroll = cmp::min(
475 (*index as u16).saturating_sub(height / 2),
476 markdown.height().saturating_sub(height / 2),
477 );
478 }
479 }
480
481 Action::SearchPrevious => {
482 let heights = markdown.search_results_heights();
483
484 let next = heights
485 .iter()
486 .rev()
487 .find(|row| **row < (app.vertical_scroll as usize + height as usize / 2));
488
489 if let Some(index) = next {
490 app.vertical_scroll = cmp::min(
491 (*index as u16).saturating_sub(height / 2),
492 markdown.height().saturating_sub(height / 2),
493 );
494 }
495 }
496
497 Action::Edit => return KeyBoardAction::Edit,
498
499 Action::Escape => {
500 app.selected = false;
501 markdown.deselect();
502 app.details_selected = false;
503 markdown.deselect_details();
504 }
505
506 Action::Enter => {
507 if app.details_selected {
511 if markdown.toggle_selected_details().is_ok() {
512 markdown.set_scroll(app.vertical_scroll);
513 }
514 return KeyBoardAction::Continue;
515 }
516
517 if !app.selected {
518 return KeyBoardAction::Continue;
519 }
520 let link = markdown.selected();
521 let prev_type = markdown.selected_underlying_type();
522
523 if prev_type == WordType::FootnoteInline {
524 app.message_box.set_message(markdown.find_footnote(link));
525 app.boxes = Boxes::Error;
526 markdown.deselect();
527 app.selected = false;
528 return KeyBoardAction::Continue;
529 }
530
531 match LinkType::from(link) {
532 LinkType::Internal(heading) => {
533 app.vertical_scroll = if let Ok(index) = markdown.heading_offset(heading) {
534 cmp::min(index, markdown.height().saturating_sub(height / 2))
535 } else {
536 app.message_box
537 .set_message(format!("Could not find heading {heading}"));
538 app.boxes = Boxes::Error;
539 markdown.deselect();
540 return KeyBoardAction::Continue;
541 };
542 }
543 LinkType::External(url) => {
544 let _ = open::that(url);
545 }
546 LinkType::MarkdownFile(url) => {
547 let url = if let Some(url) = url.strip_prefix('/') {
549 url
550 } else {
551 url
552 };
553
554 let (url, heading) = if let Some((url, heading)) = url.split_once('#') {
555 (url.to_string(), Some(heading.to_string().to_lowercase()))
556 } else {
557 (url.to_string(), None)
558 };
559
560 let url = if url.ends_with(".md") {
561 url
562 } else {
563 format!("{url}.md")
564 };
565
566 let text = if let Ok(file) = read_to_string(&url) {
567 app.vertical_scroll = 0;
568 file
569 } else {
570 app.message_box
571 .set_message(format!("Could not open file {url}"));
572 app.boxes = Boxes::Error;
573 return KeyBoardAction::Continue;
574 };
575
576 if let Some(file_name) = markdown.file_name() {
577 app.history.push(Jump::File(file_name.to_string()));
578 }
579
580 let path = std::path::Path::new(&url);
581 let _ = watcher.watch(path, notify::RecursiveMode::NonRecursive);
582 *markdown = parse_markdown(Some(&url), &text, app.width() - 2);
583 let index = if let Some(heading) = heading {
584 if let Ok(index) = markdown.heading_offset(&format!("#{heading}")) {
585 cmp::min(index, markdown.height().saturating_sub(height / 2))
586 } else {
587 app.message_box
588 .set_message(format!("Could not find heading {heading}"));
589 app.boxes = Boxes::Error;
590 0
591 }
592 } else {
593 0
594 };
595
596 app.reset();
597 app.vertical_scroll = index;
598 }
599 }
600 markdown.deselect();
601 app.selected = false;
602 }
603
604 Action::Back => match app.history.pop() {
605 Jump::File(e) => {
606 let text = if let Ok(file) = read_to_string(&e) {
607 app.vertical_scroll = 0;
608 file
609 } else {
610 app.message_box
611 .set_message(format!("Could not open file {e}"));
612 app.boxes = Boxes::Error;
613 return KeyBoardAction::Continue;
614 };
615 *markdown = parse_markdown(Some(&e), &text, app.width() - 2);
616 let path = std::path::Path::new(&e);
617 let _ = watcher.watch(path, notify::RecursiveMode::NonRecursive);
618 app.reset();
619 app.mode = Mode::View;
620 app.help_box.set_mode(Mode::View);
621 }
622 Jump::FileTree => {
623 markdown.clear();
624 app.mode = Mode::FileTree;
625 app.help_box.set_mode(Mode::FileTree);
626 }
627 },
628
629 Action::Help if GENERAL_CONFIG.help_menu => {
630 app.help_box.toggle();
631 }
632 _ => {}
633 },
634 Boxes::LinkPreview => {
635 if key == KeyCode::Esc {
636 app.boxes = Boxes::None;
637 }
638 }
639 }
640 KeyBoardAction::Continue
641}