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