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 => {
167 if GENERAL_CONFIG.help_menu {
168 app.help_box.toggle();
169 }
170 }
171
172 Action::Escape => {
173 file_tree.unselect();
174 file_tree.search(None);
175 }
176
177 Action::Sort => {
178 file_tree.sort_name();
179 }
180 _ => {}
181 },
182 Boxes::LinkPreview => {
183 if key == KeyCode::Esc {
184 app.boxes = Boxes::None;
185 }
186 }
187 }
188
189 KeyBoardAction::Continue
190}
191
192fn keyboard_mode_view(
193 key: KeyCode,
194 app: &mut App,
195 markdown: &mut ComponentRoot,
196 height: u16,
197 watcher: &mut PollWatcher,
198) -> KeyBoardAction {
199 match app.boxes {
200 Boxes::Error => match key {
201 KeyCode::Enter | KeyCode::Esc => {
202 app.boxes = Boxes::None;
203 }
204 _ => {}
205 },
206 Boxes::Search => match key {
207 KeyCode::Esc => {
208 app.search_box.clear();
209 app.boxes = Boxes::None;
210 }
211 KeyCode::Enter => {
212 let query = app.search_box.content_str();
213
214 markdown.deselect();
215
216 markdown.find_and_mark(query);
217
218 let heights = markdown.search_results_heights();
219
220 if heights.is_empty() {
221 app.message_box
222 .set_message(format!("No results found for\n {query}"));
223 app.boxes = Boxes::Error;
224 return KeyBoardAction::Continue;
225 }
226
227 let next = heights
228 .iter()
229 .find(|row| **row >= (app.vertical_scroll as usize + height as usize / 2));
230
231 if let Some(index) = next {
232 app.vertical_scroll = cmp::min(
233 (*index as u16).saturating_sub(height / 2),
234 markdown.height().saturating_sub(height / 2),
235 );
236 }
237
238 app.boxes = Boxes::None;
239 }
240 KeyCode::Char(c) => {
241 app.search_box.insert(c);
242 }
243 KeyCode::Backspace => {
244 app.search_box.delete();
245 }
246 _ => {}
247 },
248 Boxes::None => match key_to_action(key) {
249 Action::Down => {
250 if app.selected {
251 app.select_index = cmp::min(app.select_index + 1, markdown.num_links() - 1);
252 app.vertical_scroll = if let Ok(scroll) = markdown.select(app.select_index) {
253 app.selected = true;
254 scroll.saturating_sub(height / 3)
255 } else {
256 app.vertical_scroll
257 };
258 } else {
259 app.vertical_scroll = cmp::min(
260 app.vertical_scroll + 1,
261 markdown.height().saturating_sub(height / 2),
262 );
263 }
264 }
265 Action::Up => {
266 if app.selected {
267 app.select_index = app.select_index.saturating_sub(1);
268 app.vertical_scroll = if let Ok(scroll) = markdown.select(app.select_index) {
269 app.selected = true;
270 scroll.saturating_sub(height / 3)
271 } else {
272 app.vertical_scroll
273 };
274 } else {
275 app.vertical_scroll = app.vertical_scroll.saturating_sub(1);
276 }
277 }
278 Action::ToTop => {
279 app.vertical_scroll = 0;
280 }
281 Action::ToBottom => {
282 app.vertical_scroll = markdown.height().saturating_sub(height / 2);
283 }
284
285 Action::HalfPageDown => {
286 app.vertical_scroll += height / 2;
287 app.vertical_scroll = cmp::min(
288 app.vertical_scroll,
289 markdown.height().saturating_sub(height / 2),
290 );
291 }
292 Action::HalfPageUp => {
293 app.vertical_scroll = app.vertical_scroll.saturating_sub(height / 2);
294 }
295
296 Action::PageDown => {
297 app.vertical_scroll = cmp::min(
298 app.vertical_scroll + height,
299 markdown.height().saturating_sub(height / 2),
300 );
301 }
302
303 Action::PageUp => {
304 app.vertical_scroll = app.vertical_scroll.saturating_sub(height);
305 }
306
307 Action::Hover => {
308 if app.selected {
309 let link = markdown.selected();
310
311 let prev_type = markdown.selected_underlying_type();
312
313 if prev_type == WordType::FootnoteInline {
314 app.link_box
315 .set_message(format!("Footnote: {}", markdown.find_footnote(link)));
316 app.boxes = Boxes::LinkPreview;
317 return KeyBoardAction::Continue;
318 }
319
320 let message = match LinkType::from(link) {
321 LinkType::Internal(e) => format!("Internal link: {e}"),
322 LinkType::External(e) => format!("External link: {e}"),
323 LinkType::MarkdownFile(e) => format!("Markdown file: {e}"),
324 };
325
326 app.link_box.set_message(message);
327 app.boxes = Boxes::LinkPreview;
328 } else {
329 app.message_box.set_message("No link selected".to_string());
330 app.boxes = Boxes::Error;
331 }
332 }
333
334 Action::SelectLinkAlt => {
336 let links = markdown.link_index_and_height();
337 if links.is_empty() {
338 app.message_box.set_message("No links found".to_string());
339 app.boxes = Boxes::Error;
340 return KeyBoardAction::Continue;
341 }
342
343 let next = links
344 .iter()
345 .min_by_key(|(_, row)| (*row).abs_diff(app.vertical_scroll + height / 3));
346
347 if let Some((index, _)) = next {
348 app.vertical_scroll = if let Ok(scroll) = markdown.select(*index) {
349 app.select_index = *index;
350 scroll.saturating_sub(height / 3)
351 } else {
352 app.vertical_scroll
353 };
354 app.selected = true;
355 } else {
356 markdown.deselect();
358 }
359 }
360
361 Action::SelectLink => {
363 let mut links = markdown.link_index_and_height();
364 if links.is_empty() {
365 app.message_box.set_message("No links found".to_string());
366 app.boxes = Boxes::Error;
367 return KeyBoardAction::Continue;
368 }
369
370 let mut index = usize::MAX;
371 while let Some(top) = links.pop() {
372 if top.1 >= app.vertical_scroll || index == usize::MAX {
373 index = top.0;
374 } else {
375 break;
376 }
377 }
378
379 app.select_index = index;
380 app.selected = true;
381 app.vertical_scroll = if let Ok(scroll) = markdown.select(app.select_index) {
382 scroll.saturating_sub(height / 3)
383 } else {
384 app.vertical_scroll
385 };
386 }
387
388 Action::Search => {
389 app.search_box.clear();
390 app.search_box.set_position(2, height - 3);
391 app.search_box.set_width(GENERAL_CONFIG.width - 3);
392 app.boxes = Boxes::Search;
393 app.help_box.close();
394 }
395
396 Action::ToFileTree => {
397 app.mode = Mode::FileTree;
398 app.help_box.set_mode(Mode::FileTree);
399 if let Some(file) = markdown.file_name() {
400 app.history.push(Jump::File(file.to_string()));
401 }
402 app.reset();
403 }
404
405 Action::SearchNext => {
406 let heights = markdown.search_results_heights();
407
408 let next = heights
409 .iter()
410 .find(|row| **row > (app.vertical_scroll as usize + height as usize / 2));
411
412 if let Some(index) = next {
413 app.vertical_scroll = cmp::min(
414 (*index as u16).saturating_sub(height / 2),
415 markdown.height().saturating_sub(height / 2),
416 );
417 }
418 }
419
420 Action::SearchPrevious => {
421 let heights = markdown.search_results_heights();
422
423 let next = heights
424 .iter()
425 .rev()
426 .find(|row| **row < (app.vertical_scroll as usize + height as usize / 2));
427
428 if let Some(index) = next {
429 app.vertical_scroll = cmp::min(
430 (*index as u16).saturating_sub(height / 2),
431 markdown.height().saturating_sub(height / 2),
432 );
433 }
434 }
435
436 Action::Edit => return KeyBoardAction::Edit,
437
438 Action::Escape => {
439 app.selected = false;
440 markdown.deselect();
441 }
442
443 Action::Enter => {
444 if !app.selected {
445 return KeyBoardAction::Continue;
446 }
447 let link = markdown.selected();
448 let prev_type = markdown.selected_underlying_type();
449
450 if prev_type == WordType::FootnoteInline {
451 app.message_box.set_message(markdown.find_footnote(link));
452 app.boxes = Boxes::Error;
453 markdown.deselect();
454 app.selected = false;
455 return KeyBoardAction::Continue;
456 }
457
458 match LinkType::from(link) {
459 LinkType::Internal(heading) => {
460 app.vertical_scroll = if let Ok(index) = markdown.heading_offset(heading) {
461 cmp::min(index, markdown.height().saturating_sub(height / 2))
462 } else {
463 app.message_box
464 .set_message(format!("Could not find heading {heading}"));
465 app.boxes = Boxes::Error;
466 markdown.deselect();
467 return KeyBoardAction::Continue;
468 };
469 }
470 LinkType::External(url) => {
471 let _ = open::that(url);
472 }
473 LinkType::MarkdownFile(url) => {
474 let url = if let Some(url) = url.strip_prefix('/') {
476 url
477 } else {
478 url
479 };
480
481 let (url, heading) = if let Some((url, heading)) = url.split_once('#') {
482 (url.to_string(), Some(heading.to_string().to_lowercase()))
483 } else {
484 (url.to_string(), None)
485 };
486
487 let url = if url.ends_with(".md") {
488 url
489 } else {
490 format!("{url}.md")
491 };
492
493 let text = if let Ok(file) = read_to_string(&url) {
494 app.vertical_scroll = 0;
495 file
496 } else {
497 app.message_box
498 .set_message(format!("Could not open file {url}"));
499 app.boxes = Boxes::Error;
500 return KeyBoardAction::Continue;
501 };
502
503 if let Some(file_name) = markdown.file_name() {
504 app.history.push(Jump::File(file_name.to_string()));
505 }
506
507 let path = std::path::Path::new(&url);
508 let _ = watcher.watch(path, notify::RecursiveMode::NonRecursive);
509 *markdown = parse_markdown(Some(&url), &text, app.width() - 2);
510 let index = if let Some(heading) = heading {
511 if let Ok(index) = markdown.heading_offset(&format!("#{heading}")) {
512 cmp::min(index, markdown.height().saturating_sub(height / 2))
513 } else {
514 app.message_box
515 .set_message(format!("Could not find heading {heading}"));
516 app.boxes = Boxes::Error;
517 0
518 }
519 } else {
520 0
521 };
522
523 app.reset();
524 app.vertical_scroll = index;
525 }
526 }
527 markdown.deselect();
528 app.selected = false;
529 }
530
531 Action::Back => match app.history.pop() {
532 Jump::File(e) => {
533 let text = if let Ok(file) = read_to_string(&e) {
534 app.vertical_scroll = 0;
535 file
536 } else {
537 app.message_box
538 .set_message(format!("Could not open file {e}"));
539 app.boxes = Boxes::Error;
540 return KeyBoardAction::Continue;
541 };
542 *markdown = parse_markdown(Some(&e), &text, app.width() - 2);
543 let path = std::path::Path::new(&e);
544 let _ = watcher.watch(path, notify::RecursiveMode::NonRecursive);
545 app.reset();
546 app.mode = Mode::View;
547 app.help_box.set_mode(Mode::View);
548 }
549 Jump::FileTree => {
550 markdown.clear();
551 app.mode = Mode::FileTree;
552 app.help_box.set_mode(Mode::FileTree);
553 }
554 },
555
556 Action::Help => {
557 if GENERAL_CONFIG.help_menu {
558 app.help_box.toggle();
559 }
560 }
561 _ => {}
562 },
563 Boxes::LinkPreview => {
564 if key == KeyCode::Esc {
565 app.boxes = Boxes::None;
566 }
567 }
568 }
569 KeyBoardAction::Continue
570}