1use crate::{
2 get_save_backup_file_path, utils::extract_code_blocks, CodeBlockPopup, EditorClipboard,
3 ThemeMode, ThothConfig,
4};
5use anyhow::{bail, Result};
6use crossterm::{
7 event::{self, DisableMouseCapture, EnableMouseCapture, KeyCode, KeyEventKind, KeyModifiers},
8 execute,
9 terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
10};
11use ratatui::{backend::CrosstermBackend, Terminal};
12use std::{
13 io::{self, Write},
14 time::Instant,
15};
16use tui_textarea::TextArea;
17
18use crate::{
19 format_json, format_markdown, get_save_file_path, load_textareas, save_textareas,
20 ui::{
21 render_code_block_popup, render_edit_commands_popup, render_header, render_title_popup,
22 render_title_select_popup, render_ui_popup, EditCommandsPopup, UiPopup,
23 },
24 ScrollableTextArea, TitlePopup, TitleSelectPopup,
25};
26
27use std::env;
28use std::fs;
29use std::process::Command;
30use tempfile::NamedTempFile;
31
32pub struct UIState {
33 pub scrollable_textarea: ScrollableTextArea,
34 pub title_popup: TitlePopup,
35 pub title_select_popup: TitleSelectPopup,
36 pub error_popup: UiPopup,
37 pub help_popup: UiPopup,
38 pub copy_popup: UiPopup,
39 pub edit_commands_popup: EditCommandsPopup,
40 pub code_block_popup: CodeBlockPopup,
41 pub clipboard: Option<EditorClipboard>,
42 pub last_draw: Instant,
43 pub config: ThothConfig,
44}
45
46impl UIState {
47 pub fn new() -> Result<Self> {
48 let mut scrollable_textarea = ScrollableTextArea::new();
49 let main_save_path = get_save_file_path();
50 if main_save_path.exists() {
51 let (loaded_textareas, loaded_titles) = load_textareas(main_save_path)?;
52 for (textarea, title) in loaded_textareas.into_iter().zip(loaded_titles) {
53 scrollable_textarea.add_textarea(textarea, title);
54 }
55 } else {
56 scrollable_textarea.add_textarea(TextArea::default(), String::from("New Textarea"));
57 }
58 scrollable_textarea.initialize_scroll();
59
60 let config = ThothConfig::load()?;
61
62 Ok(UIState {
63 scrollable_textarea,
64 title_popup: TitlePopup::new(),
65 title_select_popup: TitleSelectPopup::new(),
66 error_popup: UiPopup::new("Error".to_string(), 60, 20),
67 copy_popup: UiPopup::new("Block Copied".to_string(), 60, 20),
68 help_popup: UiPopup::new("Keyboard Shortcuts".to_string(), 60, 80),
69 edit_commands_popup: EditCommandsPopup::new(),
70 code_block_popup: CodeBlockPopup::new(),
71 clipboard: EditorClipboard::try_new(),
72 last_draw: Instant::now(),
73 config,
74 })
75 }
76}
77
78pub fn draw_ui(
79 terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
80 state: &mut UIState,
81) -> Result<()> {
82 terminal.draw(|f| {
83 let theme = state.config.get_theme_colors();
84
85 let chunks = ratatui::layout::Layout::default()
86 .direction(ratatui::layout::Direction::Vertical)
87 .constraints(
88 [
89 ratatui::layout::Constraint::Length(1),
90 ratatui::layout::Constraint::Min(1),
91 ]
92 .as_ref(),
93 )
94 .split(f.size());
95
96 render_header(f, chunks[0], state.scrollable_textarea.edit_mode, theme);
97 if state.scrollable_textarea.full_screen_mode {
98 state
99 .scrollable_textarea
100 .render(f, f.size(), theme, &state.config.theme)
101 .unwrap();
102 } else {
103 state
104 .scrollable_textarea
105 .render(f, chunks[1], theme, &state.config.theme)
106 .unwrap();
107 }
108
109 if state.copy_popup.visible {
110 render_ui_popup(f, &state.copy_popup, theme);
111 }
112 if state.title_popup.visible {
113 render_title_popup(f, &state.title_popup, theme);
114 } else if state.title_select_popup.visible {
115 render_title_select_popup(f, &state.title_select_popup, theme);
116 } else if state.code_block_popup.visible {
117 render_code_block_popup(f, &state.code_block_popup, theme);
118 }
119
120 if state.edit_commands_popup.visible {
121 render_edit_commands_popup(f, theme);
122 }
123
124 if state.error_popup.visible {
125 render_ui_popup(f, &state.error_popup, theme);
126 }
127 if state.help_popup.visible {
128 render_ui_popup(f, &state.help_popup, theme);
129 }
130
131 if state.help_popup.visible {
132 render_ui_popup(f, &state.help_popup, theme);
133 }
134 })?;
135 Ok(())
136}
137
138fn handle_code_block_popup_input(state: &mut UIState, key: event::KeyEvent) -> Result<bool> {
139 let visible_items =
140 (state.scrollable_textarea.viewport_height as f32 * 0.8).floor() as usize - 4;
141
142 match key.code {
143 KeyCode::Enter => {
144 if !state.code_block_popup.filtered_blocks.is_empty() {
145 let selected_index = state.code_block_popup.selected_index;
146 let content = state.code_block_popup.filtered_blocks[selected_index]
147 .content
148 .clone();
149 let language = state.code_block_popup.filtered_blocks[selected_index]
150 .language
151 .clone();
152
153 if let Err(e) = copy_code_block_content_to_clipboard(state, &content, &language) {
154 state.error_popup.show(format!("{}", e));
155 }
156
157 state.code_block_popup.visible = false;
158 }
159 }
160 KeyCode::Esc => {
161 state.code_block_popup.visible = false;
162 }
163 KeyCode::Up => {
164 state.code_block_popup.move_selection_up(visible_items);
165 }
166 KeyCode::Down => {
167 state.code_block_popup.move_selection_down(visible_items);
168 }
169 _ => {}
170 }
171 Ok(false)
172}
173
174fn extract_and_show_code_blocks(state: &mut UIState) -> Result<()> {
175 let content = state.scrollable_textarea.textareas[state.scrollable_textarea.focused_index]
176 .lines()
177 .join("\n");
178
179 let code_blocks = extract_code_blocks(&content);
180
181 if code_blocks.is_empty() {
182 state
183 .error_popup
184 .show("No code blocks found in the current note.".to_string());
185 return Ok(());
186 }
187
188 state.code_block_popup.set_code_blocks(code_blocks);
189 state.code_block_popup.visible = true;
190 Ok(())
191}
192
193fn copy_code_block_content_to_clipboard(
194 state: &mut UIState,
195 content: &str,
196 language: &str,
197) -> Result<()> {
198 match &mut state.clipboard {
199 Some(clip) => {
200 if let Err(e) = clip.set_contents(content.to_string()) {
201 let backup_path = crate::get_clipboard_backup_file_path();
202 std::fs::write(&backup_path, content)?;
203
204 return Err(anyhow::anyhow!(
205 "Clipboard error: {}.\nContent saved to: {}\nPlease use 'thoth read_clipboard' to read the contents from STDOUT.",
206 e.to_string().split('\n').next().unwrap_or("Unknown error"),
207 backup_path.display()
208 ));
209 }
210
211 state.copy_popup.show(format!(
212 "Copied code block [{}] to clipboard",
213 if language.is_empty() {
214 "no language"
215 } else {
216 language
217 }
218 ));
219 }
220 None => {
221 let backup_path = crate::get_clipboard_backup_file_path();
222 std::fs::write(&backup_path, content)?;
223
224 return Err(anyhow::anyhow!(
225 "Clipboard unavailable.\nContent saved to: {}\nPlease use 'thoth read_clipboard' to read the contents from STDOUT.",
226 backup_path.display()
227 ));
228 }
229 }
230 Ok(())
231}
232
233pub fn handle_input(
234 terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
235 state: &mut UIState,
236 key: event::KeyEvent,
237) -> Result<bool> {
238 if key.kind != KeyEventKind::Press {
239 return Ok(false);
240 }
241
242 if state.code_block_popup.visible {
243 handle_code_block_popup_input(state, key)
244 } else if state.scrollable_textarea.full_screen_mode {
245 handle_full_screen_input(terminal, state, key)
246 } else if state.title_popup.visible {
247 handle_title_popup_input(state, key)
248 } else if state.title_select_popup.visible {
249 handle_title_select_popup_input(state, key)
250 } else {
251 handle_normal_input(terminal, state, key)
252 }
253}
254
255fn handle_full_screen_input(
256 terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
257 state: &mut UIState,
258 key: event::KeyEvent,
259) -> Result<bool> {
260 match key.code {
261 KeyCode::Esc => {
262 if state.copy_popup.visible {
263 state.copy_popup.hide();
264 } else if state.error_popup.visible {
265 state.error_popup.hide();
266 } else if state.help_popup.visible {
267 state.help_popup.hide();
268 } else if state.edit_commands_popup.visible {
269 state.edit_commands_popup.visible = false;
270 } else if state.scrollable_textarea.edit_mode {
271 state.scrollable_textarea.edit_mode = false;
272 } else {
273 state.scrollable_textarea.toggle_full_screen();
274 state
275 .scrollable_textarea
276 .jump_to_textarea(state.scrollable_textarea.focused_index);
277 }
278 }
279 KeyCode::Char('e') if key.modifiers.contains(KeyModifiers::CONTROL) => {
280 if state.scrollable_textarea.edit_mode {
281 match edit_with_external_editor(state) {
282 Ok(edited_content) => {
283 let mut new_textarea = TextArea::default();
284 for line in edited_content.lines() {
285 new_textarea.insert_str(line);
286 new_textarea.insert_newline();
287 }
288 state.scrollable_textarea.textareas
289 [state.scrollable_textarea.focused_index] = new_textarea;
290
291 terminal.clear()?;
292 }
293 Err(e) => {
294 state
295 .error_popup
296 .show(format!("Failed to edit with external editor: {}", e));
297 }
298 }
299 }
300 }
301 KeyCode::Enter => {
302 if !state.scrollable_textarea.edit_mode {
303 state.scrollable_textarea.edit_mode = true;
304 } else {
305 state.scrollable_textarea.textareas[state.scrollable_textarea.focused_index]
306 .insert_newline();
307 }
308 }
309 KeyCode::Up => {
310 if state.scrollable_textarea.edit_mode {
311 handle_up_key(state, key);
312 } else {
313 state.scrollable_textarea.handle_scroll(-1);
314 }
315 }
316 KeyCode::Down => {
317 if state.scrollable_textarea.edit_mode {
318 handle_down_key(state, key);
319 } else {
320 state.scrollable_textarea.handle_scroll(1);
321 }
322 }
323 KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
324 if !state.scrollable_textarea.edit_mode {
325 if let Err(e) = extract_and_show_code_blocks(state) {
326 state
327 .error_popup
328 .show(format!("Error extracting code blocks: {}", e));
329 }
330 } else {
331 state.scrollable_textarea.textareas[state.scrollable_textarea.focused_index]
332 .input(key);
333 }
334 }
335 KeyCode::Char('y') if key.modifiers.contains(KeyModifiers::CONTROL) => {
336 match state.scrollable_textarea.copy_focused_textarea_contents() {
337 Ok(_) => {
338 let curr_focused_index = state.scrollable_textarea.focused_index;
339 let curr_title_option =
340 state.scrollable_textarea.titles.get(curr_focused_index);
341
342 match curr_title_option {
343 Some(curr_title) => {
344 state
345 .copy_popup
346 .show(format!("Copied block {}", curr_title));
347 }
348 None => {
349 state
350 .error_popup
351 .show("Failed to copy selection with title".to_string());
352 }
353 }
354 }
355 Err(e) => {
356 state.error_popup.show(format!("{}", e));
357 }
358 }
359 }
360 _ => {
361 if state.scrollable_textarea.edit_mode {
362 state.scrollable_textarea.textareas[state.scrollable_textarea.focused_index]
363 .input(key);
364 }
365 }
366 }
367 Ok(false)
368}
369
370fn handle_title_popup_input(state: &mut UIState, key: event::KeyEvent) -> Result<bool> {
371 match key.code {
372 KeyCode::Enter => {
373 #[allow(clippy::assigning_clones)]
374 state
375 .scrollable_textarea
376 .change_title(state.title_popup.title.clone());
377 state.title_popup.visible = false;
378 state.title_popup.title.clear();
379 }
380 KeyCode::Esc => {
381 state.title_popup.visible = false;
382 state.title_popup.title.clear();
383 }
384 KeyCode::Char(c) => {
385 state.title_popup.title.push(c);
386 }
387 KeyCode::Backspace => {
388 state.title_popup.title.pop();
389 }
390 _ => {}
391 }
392 Ok(false)
393}
394
395fn handle_title_select_popup_input(state: &mut UIState, key: event::KeyEvent) -> Result<bool> {
396 let visible_items =
397 (state.scrollable_textarea.viewport_height as f32 * 0.8).floor() as usize - 10;
398
399 match key.code {
400 KeyCode::Enter => {
401 if !state.title_select_popup.filtered_titles.is_empty() {
402 let selected_title_match = &state.title_select_popup.filtered_titles
403 [state.title_select_popup.selected_index];
404 state
405 .scrollable_textarea
406 .jump_to_textarea(selected_title_match.index);
407 state.title_select_popup.visible = false;
408 if !state.title_select_popup.search_query.is_empty() {
409 state.title_select_popup.search_query.clear();
410 state.title_select_popup.reset_filtered_titles();
411 }
412 }
413 }
414 KeyCode::Esc => {
415 state.title_select_popup.visible = false;
416 state.edit_commands_popup.visible = false;
417 if !state.title_select_popup.search_query.is_empty() {
418 state.title_select_popup.search_query.clear();
419 state.title_select_popup.reset_filtered_titles();
420 }
421 }
422 KeyCode::Up => {
423 state.title_select_popup.move_selection_up(visible_items);
424 }
425 KeyCode::Down => {
426 state.title_select_popup.move_selection_down(visible_items);
427 }
428 KeyCode::Char(c) => {
429 state.title_select_popup.search_query.push(c);
430 state.title_select_popup.update_search();
431 }
432 KeyCode::Backspace => {
433 state.title_select_popup.search_query.pop();
434 state.title_select_popup.update_search();
435 }
436
437 _ => {}
438 }
439 Ok(false)
440}
441
442fn handle_normal_input(
443 terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
444 state: &mut UIState,
445 key: event::KeyEvent,
446) -> Result<bool> {
447 match key.code {
448 KeyCode::Char('k') if key.modifiers.contains(KeyModifiers::CONTROL) => {
449 format_current_textarea(state, format_markdown)?;
450 }
451 KeyCode::Char('j') if key.modifiers.contains(KeyModifiers::CONTROL) => {
452 format_current_textarea(state, format_json)?;
453 }
454 KeyCode::Char('e') if key.modifiers.contains(KeyModifiers::CONTROL) => {
455 if state.scrollable_textarea.edit_mode {
456 match edit_with_external_editor(state) {
457 Ok(edited_content) => {
458 let mut new_textarea = TextArea::default();
459 for line in edited_content.lines() {
460 new_textarea.insert_str(line);
461 new_textarea.insert_newline();
462 }
463 state.scrollable_textarea.textareas
464 [state.scrollable_textarea.focused_index] = new_textarea;
465
466 terminal.clear()?;
468 }
469 Err(e) => {
470 state
471 .error_popup
472 .show(format!("Failed to edit with external editor: {}", e));
473 }
474 }
475 }
476 }
477 KeyCode::Char('l') if key.modifiers.contains(KeyModifiers::CONTROL) => {
478 let new_theme = match state.config.theme {
479 ThemeMode::Light => ThemeMode::Dark,
480 ThemeMode::Dark => ThemeMode::Light,
481 };
482 state.config.set_theme(new_theme.clone())?;
483 }
484 KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
485 if !state.scrollable_textarea.edit_mode {
486 if let Err(e) = extract_and_show_code_blocks(state) {
487 state
488 .error_popup
489 .show(format!("Error extracting code blocks: {}", e));
490 }
491 } else {
492 state.scrollable_textarea.textareas[state.scrollable_textarea.focused_index]
493 .input(key);
494 }
495 }
496 KeyCode::Char('y') if key.modifiers.contains(KeyModifiers::CONTROL) => {
497 match state.scrollable_textarea.copy_focused_textarea_contents() {
498 Ok(_) => {
499 let curr_focused_index = state.scrollable_textarea.focused_index;
500 let curr_title_option =
501 state.scrollable_textarea.titles.get(curr_focused_index);
502
503 match curr_title_option {
504 Some(curr_title) => {
505 state
506 .copy_popup
507 .show(format!("Copied block {}", curr_title));
508 }
509 None => {
510 state
511 .error_popup
512 .show("Failed to copy selection with title".to_string());
513 }
514 }
515 }
516 Err(e) => {
517 state.error_popup.show(format!("{}", e));
518 }
519 }
520 }
521 KeyCode::Char('b') if key.modifiers.contains(KeyModifiers::CONTROL) => {
522 if let Err(e) = state.scrollable_textarea.copy_selection_contents() {
523 state
524 .error_popup
525 .show(format!("Failed to copy to clipboard: {}", e));
526 }
527 }
528 KeyCode::Char('v') if key.modifiers.contains(KeyModifiers::CONTROL) => {
529 handle_paste(state)?;
530 }
531 KeyCode::Char('h') if key.modifiers.contains(KeyModifiers::CONTROL) => {
532 let help_message = "\
533NAVIGATION:
534 • ↑/↓ or j/k: Navigate between blocks
535 • Enter: Enter edit mode
536 • Esc: Exit current mode
537
538BLOCKS:
539 • ^n: Add a new block
540 • ^d: Delete current block
541 • ^t: Change block title
542 • ^s: Select block by title
543 • ^f: Toggle fullscreen mode
544
545CLIPBOARD:
546 • ^y: Copy current block
547 • ^v: Paste from clipboard
548 • ^b: Copy selection (in edit mode)
549 • ^c: Copy code block from current note
550
551FORMATTING:
552 • ^j: Format as JSON
553 • ^k: Format as Markdown
554
555OTHER:
556 • ^l: Toggle light/dark theme
557 • ^e: Edit with external editor (in edit mode)
558 • q: Quit application
559 • ^h: Show this help";
560
561 state.help_popup.show(help_message.to_string());
562 }
563 KeyCode::Char('f') if key.modifiers.contains(KeyModifiers::CONTROL) => {
564 if !state.scrollable_textarea.edit_mode {
565 state.scrollable_textarea.toggle_full_screen();
566 }
567 }
568 KeyCode::Char('h') if key.modifiers.contains(KeyModifiers::CONTROL) => {
569 if state.scrollable_textarea.edit_mode {
570 state.edit_commands_popup.visible = !state.edit_commands_popup.visible;
571 }
572 }
573 #[allow(clippy::assigning_clones)]
574 KeyCode::Char('s')
575 if key.modifiers.contains(KeyModifiers::CONTROL)
576 && !key.modifiers.contains(KeyModifiers::SHIFT) =>
577 {
578 state
580 .title_select_popup
581 .set_titles(state.scrollable_textarea.titles.clone());
582 state.title_select_popup.selected_index = 0;
583 state.title_select_popup.visible = true;
584 }
585 KeyCode::Char('q') => {
586 if !state.scrollable_textarea.edit_mode {
587 save_textareas(
588 &state.scrollable_textarea.textareas,
589 &state.scrollable_textarea.titles,
590 get_save_file_path(),
591 )?;
592 save_textareas(
593 &state.scrollable_textarea.textareas,
594 &state.scrollable_textarea.titles,
595 get_save_backup_file_path(),
596 )?;
597 return Ok(true);
598 }
599 state.scrollable_textarea.textareas[state.scrollable_textarea.focused_index].input(key);
600 }
601 KeyCode::Char('n') if key.modifiers.contains(KeyModifiers::CONTROL) => {
602 if !state.scrollable_textarea.edit_mode {
603 state
604 .scrollable_textarea
605 .add_textarea(TextArea::default(), String::from("New Textarea"));
606 state.scrollable_textarea.adjust_scroll_to_focused();
607 }
608 }
609 KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => {
610 if state.scrollable_textarea.textareas.len() > 1 && !state.scrollable_textarea.edit_mode
611 {
612 state
613 .scrollable_textarea
614 .remove_textarea(state.scrollable_textarea.focused_index);
615 }
616 }
617 KeyCode::Char('g') if key.modifiers.contains(KeyModifiers::CONTROL) => {
618 if state.scrollable_textarea.edit_mode {
619 state.scrollable_textarea.textareas[state.scrollable_textarea.focused_index]
620 .move_cursor(tui_textarea::CursorMove::Top);
621 }
622 }
623 #[allow(clippy::assigning_clones)]
624 KeyCode::Char('t') if key.modifiers.contains(KeyModifiers::CONTROL) => {
625 state.title_popup.visible = true;
626 state.title_popup.title =
627 state.scrollable_textarea.titles[state.scrollable_textarea.focused_index].clone();
628 }
629 KeyCode::Enter => {
630 if state.scrollable_textarea.edit_mode {
631 state.scrollable_textarea.textareas[state.scrollable_textarea.focused_index]
632 .insert_newline();
633 } else {
634 state.scrollable_textarea.edit_mode = true;
635 }
636 }
637 KeyCode::Esc => {
638 if state.edit_commands_popup.visible {
639 state.edit_commands_popup.visible = false;
640 } else {
641 state.scrollable_textarea.edit_mode = false;
642 state.edit_commands_popup.visible = false;
643 }
644
645 if state.error_popup.visible {
646 state.error_popup.hide();
647 }
648 if state.help_popup.visible {
649 state.help_popup.hide();
650 }
651 if state.copy_popup.visible {
652 state.copy_popup.hide();
653 }
654 }
655 KeyCode::Up => handle_up_key(state, key),
656 KeyCode::Down => handle_down_key(state, key),
657 KeyCode::Char('k') if !state.scrollable_textarea.edit_mode => handle_up_key(state, key),
658 KeyCode::Char('j') if !state.scrollable_textarea.edit_mode => handle_down_key(state, key),
659 _ => {
660 if state.scrollable_textarea.edit_mode {
661 state.scrollable_textarea.textareas[state.scrollable_textarea.focused_index]
662 .input(key);
663 state.scrollable_textarea.start_sel = usize::MAX;
664 state.scrollable_textarea.textareas[state.scrollable_textarea.focused_index]
665 .cancel_selection();
666 }
667 }
668 }
669 Ok(false)
670}
671
672fn handle_up_key(state: &mut UIState, key: event::KeyEvent) {
673 if state.scrollable_textarea.edit_mode {
674 let textarea =
675 &mut state.scrollable_textarea.textareas[state.scrollable_textarea.focused_index];
676 if key.modifiers.contains(KeyModifiers::SHIFT) {
677 if state.scrollable_textarea.start_sel == usize::MAX {
678 let (curr_row, _) = textarea.cursor();
679 state.scrollable_textarea.start_sel = curr_row;
680 textarea.start_selection();
681 }
682 if textarea.cursor().0 > 0 {
683 textarea.move_cursor(tui_textarea::CursorMove::Up);
684 }
685 } else {
686 textarea.move_cursor(tui_textarea::CursorMove::Up);
687 state.scrollable_textarea.start_sel = usize::MAX;
688 textarea.cancel_selection();
689 }
690 } else {
691 state.scrollable_textarea.move_focus(-1);
692 }
693}
694
695fn handle_down_key(state: &mut UIState, key: event::KeyEvent) {
696 if state.scrollable_textarea.edit_mode {
697 let textarea =
698 &mut state.scrollable_textarea.textareas[state.scrollable_textarea.focused_index];
699 if key.modifiers.contains(KeyModifiers::SHIFT) {
700 if state.scrollable_textarea.start_sel == usize::MAX {
701 let (curr_row, _) = textarea.cursor();
702 state.scrollable_textarea.start_sel = curr_row;
703 textarea.start_selection();
704 }
705 if textarea.cursor().0 < textarea.lines().len() - 1 {
706 textarea.move_cursor(tui_textarea::CursorMove::Down);
707 }
708 } else {
709 textarea.move_cursor(tui_textarea::CursorMove::Down);
710 state.scrollable_textarea.start_sel = usize::MAX;
711 textarea.cancel_selection();
712 }
713 } else {
714 state.scrollable_textarea.move_focus(1);
715 }
716}
717
718fn format_current_textarea<F>(state: &mut UIState, formatter: F) -> Result<()>
719where
720 F: Fn(&str) -> Result<String>,
721{
722 let current_content = state.scrollable_textarea.textareas
723 [state.scrollable_textarea.focused_index]
724 .lines()
725 .join("\n");
726 match formatter(¤t_content) {
727 Ok(formatted) => {
728 let mut new_textarea = TextArea::default();
729 for line in formatted.lines() {
730 new_textarea.insert_str(line);
731 new_textarea.insert_newline();
732 }
733 state.scrollable_textarea.textareas[state.scrollable_textarea.focused_index] =
734 new_textarea;
735 Ok(())
736 }
737 Err(e) => {
738 state
739 .error_popup
740 .show(format!("Failed to format block: {}", e));
741 Ok(())
742 }
743 }
744}
745
746fn edit_with_external_editor(state: &mut UIState) -> Result<String> {
747 let content = state.scrollable_textarea.textareas[state.scrollable_textarea.focused_index]
748 .lines()
749 .join("\n");
750 let mut temp_file = NamedTempFile::new()?;
751
752 temp_file.write_all(content.as_bytes())?;
753 temp_file.flush()?;
754
755 let editor = env::var("VISUAL")
756 .or_else(|_| env::var("EDITOR"))
757 .unwrap_or_else(|_| "vi".to_string());
758
759 disable_raw_mode()?;
761 execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture)?;
762
763 let status = Command::new(&editor).arg(temp_file.path()).status()?;
764
765 enable_raw_mode()?;
767 execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture)?;
768
769 if !status.success() {
770 bail!(format!("Editor '{}' returned non-zero status", editor));
771 }
772
773 let edited_content = fs::read_to_string(temp_file.path())?;
774
775 Ok(edited_content)
776}
777
778fn handle_paste(state: &mut UIState) -> Result<()> {
779 if state.scrollable_textarea.edit_mode {
780 match &mut state.clipboard {
781 Some(clip) => {
782 if let Ok(content) = clip.get_content() {
783 let textarea = &mut state.scrollable_textarea.textareas
784 [state.scrollable_textarea.focused_index];
785 for line in content.lines() {
786 textarea.insert_str(line);
787 textarea.insert_newline();
788 }
789 if content.ends_with('\n') {
791 textarea.delete_char();
792 }
793 }
794 }
795 None => {
796 state
797 .error_popup
798 .show("Failed to create clipboard".to_string());
799 }
800 }
801 }
802 Ok(())
803}