From be9c4c718298b912ad99a892ad0bcbf335ed7d0c Mon Sep 17 00:00:00 2001 From: jooaf Date: Sun, 13 Apr 2025 19:06:14 -0500 Subject: [PATCH 1/3] Feature: Adding code block copy selection --- Cargo.lock | 2 +- src/code_block_popup.rs | 141 ++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 + src/ui.rs | 86 ++++++++++++++++++++++++ src/ui_handler.rs | 123 ++++++++++++++++++++++++++++++++++- src/utils.rs | 98 ++++++++++++++++++++++++++++ 6 files changed, 448 insertions(+), 4 deletions(-) create mode 100644 src/code_block_popup.rs diff --git a/Cargo.lock b/Cargo.lock index ef77289..ee0c9fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1436,7 +1436,7 @@ dependencies = [ [[package]] name = "thoth-cli" -version = "0.1.80" +version = "0.1.81" dependencies = [ "anyhow", "arboard", diff --git a/src/code_block_popup.rs b/src/code_block_popup.rs new file mode 100644 index 0000000..aca838b --- /dev/null +++ b/src/code_block_popup.rs @@ -0,0 +1,141 @@ +#[derive(Clone)] +pub struct CodeBlockPopup { + pub code_blocks: Vec, + pub filtered_blocks: Vec, + pub selected_index: usize, + pub visible: bool, + pub scroll_offset: usize, +} + +#[derive(Clone)] +pub struct CodeBlock { + pub content: String, + pub language: String, + pub start_line: usize, + pub end_line: usize, +} + +impl CodeBlock { + pub fn new(content: String, language: String, start_line: usize, end_line: usize) -> Self { + Self { + content, + language, + start_line, + end_line, + } + } +} + +impl Default for CodeBlockPopup { + fn default() -> Self { + Self::new() + } +} + +impl CodeBlockPopup { + pub fn new() -> Self { + CodeBlockPopup { + code_blocks: Vec::new(), + filtered_blocks: Vec::new(), + selected_index: 0, + visible: false, + scroll_offset: 0, + } + } + + pub fn set_code_blocks(&mut self, code_blocks: Vec) { + self.code_blocks = code_blocks; + self.filtered_blocks = self.code_blocks.clone(); + self.selected_index = 0; + self.scroll_offset = 0; + } + + pub fn move_selection_up(&mut self, visible_items: usize) { + if self.filtered_blocks.is_empty() { + return; + } + + if self.selected_index > 0 { + self.selected_index -= 1; + } else { + self.selected_index = self.filtered_blocks.len() - 1; + } + + if self.selected_index <= self.scroll_offset { + self.scroll_offset = self.selected_index; + } + if self.selected_index == self.filtered_blocks.len() - 1 { + self.scroll_offset = self.filtered_blocks.len().saturating_sub(visible_items); + } + } + + pub fn move_selection_down(&mut self, visible_items: usize) { + if self.filtered_blocks.is_empty() { + return; + } + + if self.selected_index < self.filtered_blocks.len() - 1 { + self.selected_index += 1; + } else { + self.selected_index = 0; + self.scroll_offset = 0; + } + + let max_scroll = self.filtered_blocks.len().saturating_sub(visible_items); + if self.selected_index >= self.scroll_offset + visible_items { + self.scroll_offset = (self.selected_index + 1).saturating_sub(visible_items); + if self.scroll_offset > max_scroll { + self.scroll_offset = max_scroll; + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_new_code_block_popup() { + let popup = CodeBlockPopup::new(); + assert!(popup.code_blocks.is_empty()); + assert_eq!(popup.selected_index, 0); + assert!(!popup.visible); + } + + #[test] + fn test_set_code_blocks() { + let mut popup = CodeBlockPopup::new(); + let blocks = vec![ + CodeBlock::new("fn main() {}".to_string(), "rust".to_string(), 0, 2), + CodeBlock::new("def hello(): pass".to_string(), "python".to_string(), 3, 5), + ]; + popup.set_code_blocks(blocks); + assert_eq!(popup.code_blocks.len(), 2); + assert_eq!(popup.code_blocks[0].language, "rust"); + assert_eq!(popup.code_blocks[1].language, "python"); + } + + #[test] + fn test_move_selection() { + let mut popup = CodeBlockPopup::new(); + let blocks = vec![ + CodeBlock::new("Block 1".to_string(), "rust".to_string(), 0, 2), + CodeBlock::new("Block 2".to_string(), "python".to_string(), 3, 5), + CodeBlock::new("Block 3".to_string(), "js".to_string(), 6, 8), + ]; + popup.set_code_blocks(blocks); + + popup.move_selection_down(2); + assert_eq!(popup.selected_index, 1); + + popup.move_selection_up(2); + assert_eq!(popup.selected_index, 0); + + popup.move_selection_up(2); + assert_eq!(popup.selected_index, 2); + + popup.move_selection_down(2); + assert_eq!(popup.selected_index, 0); + } +} diff --git a/src/lib.rs b/src/lib.rs index 2f269bf..8e75d83 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ pub mod cli; pub mod clipboard; +pub mod code_block_popup; pub mod config; pub mod formatter; pub mod markdown_renderer; @@ -13,6 +14,7 @@ pub mod utils; pub use clipboard::ClipboardTrait; pub use clipboard::EditorClipboard; +pub use code_block_popup::CodeBlockPopup; pub use config::{ThemeMode, ThothConfig}; use dirs::home_dir; pub use formatter::{format_json, format_markdown}; diff --git a/src/ui.rs b/src/ui.rs index 8ed8928..426625f 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,4 +1,7 @@ +use crate::code_block_popup::CodeBlockPopup; use crate::{ThemeColors, TitlePopup, TitleSelectPopup, BORDER_PADDING_SIZE, DARK_MODE_COLORS}; +use ratatui::style::Color; +use ratatui::widgets::Wrap; use ratatui::{ layout::{Constraint, Direction, Layout, Rect}, style::{Modifier, Style}, @@ -133,6 +136,7 @@ pub fn render_header(f: &mut Frame, area: Rect, is_edit_mode: bool, theme: &Them "^n:Add", "^d:Del", "^y:Copy", + "^c:Copy Code", "^v:Paste", "Enter:Edit", "^f:Focus", @@ -239,6 +243,88 @@ pub fn render_title_popup(f: &mut Frame, popup: &TitlePopup, theme: &ThemeColors f.render_widget(text, area); } +pub fn render_code_block_popup(f: &mut Frame, popup: &CodeBlockPopup, theme: &ThemeColors) { + if !popup.visible || popup.filtered_blocks.is_empty() { + return; + } + + let area = centered_rect(80, 80, f.size()); + f.render_widget(ratatui::widgets::Clear, area); + + // Split the area into two parts: selection list and code preview + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), // Title + borders + Constraint::Min(1), // Code content area + ]) + .split(area); + + let title_area = chunks[0]; + let code_area = chunks[1]; + + // Create the title block + let title_block = Block::default() + .borders(Borders::TOP | Borders::LEFT | Borders::RIGHT) + .border_style(Style::default().fg(theme.primary)) + .title(format!( + "Code Block {}/{} [{}]", + popup.selected_index + 1, + popup.filtered_blocks.len(), + if !popup.filtered_blocks.is_empty() { + popup.filtered_blocks[popup.selected_index].language.clone() + } else { + String::new() + } + )); + + // Render title with navigation instructions + let title_text = vec![Line::from(vec![ + Span::raw(" "), + Span::styled("↑/↓", Style::default().fg(theme.accent)), + Span::raw(": Navigate "), + Span::styled("Enter", Style::default().fg(theme.accent)), + Span::raw(": Copy "), + Span::styled("Esc", Style::default().fg(theme.accent)), + Span::raw(": Cancel"), + ])]; + + let title_paragraph = Paragraph::new(title_text).block(title_block); + + f.render_widget(title_paragraph, title_area); + + if !popup.filtered_blocks.is_empty() { + let selected_block = &popup.filtered_blocks[popup.selected_index]; + + let code_content = selected_block.content.clone(); + let _language = &selected_block.language; + + let lines: Vec = code_content + .lines() + .enumerate() + .map(|(i, line)| { + Line::from(vec![ + Span::styled( + format!("{:3} │ ", i + 1), + Style::default().fg(Color::DarkGray), + ), + Span::raw(line), + ]) + }) + .collect(); + + let code_block = Block::default() + .borders(Borders::LEFT | Borders::RIGHT | Borders::BOTTOM) + .border_style(Style::default().fg(theme.primary)); + + let code_paragraph = Paragraph::new(lines) + .block(code_block) + .wrap(Wrap { trim: false }); + + f.render_widget(code_paragraph, code_area); + } +} + pub fn render_title_select_popup(f: &mut Frame, popup: &TitleSelectPopup, theme: &ThemeColors) { let area = centered_rect(80, 80, f.size()); f.render_widget(ratatui::widgets::Clear, area); diff --git a/src/ui_handler.rs b/src/ui_handler.rs index 75283d1..92fa99c 100644 --- a/src/ui_handler.rs +++ b/src/ui_handler.rs @@ -1,4 +1,7 @@ -use crate::{get_save_backup_file_path, EditorClipboard, ThemeMode, ThothConfig}; +use crate::{ + get_save_backup_file_path, utils::extract_code_blocks, CodeBlockPopup, EditorClipboard, + ThemeMode, ThothConfig, +}; use anyhow::{bail, Result}; use crossterm::{ event::{self, DisableMouseCapture, EnableMouseCapture, KeyCode, KeyEventKind, KeyModifiers}, @@ -15,8 +18,8 @@ use tui_textarea::TextArea; use crate::{ format_json, format_markdown, get_save_file_path, load_textareas, save_textareas, ui::{ - render_edit_commands_popup, render_header, render_title_popup, render_title_select_popup, - render_ui_popup, EditCommandsPopup, UiPopup, + render_code_block_popup, render_edit_commands_popup, render_header, render_title_popup, + render_title_select_popup, render_ui_popup, EditCommandsPopup, UiPopup, }, ScrollableTextArea, TitlePopup, TitleSelectPopup, }; @@ -34,6 +37,7 @@ pub struct UIState { pub help_popup: UiPopup, pub copy_popup: UiPopup, pub edit_commands_popup: EditCommandsPopup, + pub code_block_popup: CodeBlockPopup, pub clipboard: Option, pub last_draw: Instant, pub config: ThothConfig, @@ -63,6 +67,7 @@ impl UIState { copy_popup: UiPopup::new("Block Copied".to_string(), 60, 20), help_popup: UiPopup::new("Keyboard Shortcuts".to_string(), 60, 80), edit_commands_popup: EditCommandsPopup::new(), + code_block_popup: CodeBlockPopup::new(), clipboard: EditorClipboard::try_new(), last_draw: Instant::now(), config, @@ -105,6 +110,8 @@ pub fn draw_ui( render_title_popup(f, &state.title_popup, theme); } else if state.title_select_popup.visible { render_title_select_popup(f, &state.title_select_popup, theme); + } else if state.code_block_popup.visible { + render_code_block_popup(f, &state.code_block_popup, theme); } if state.edit_commands_popup.visible { @@ -129,6 +136,101 @@ pub fn draw_ui( Ok(()) } +fn handle_code_block_popup_input(state: &mut UIState, key: event::KeyEvent) -> Result { + let visible_items = + (state.scrollable_textarea.viewport_height as f32 * 0.8).floor() as usize - 4; + + match key.code { + KeyCode::Enter => { + if !state.code_block_popup.filtered_blocks.is_empty() { + let selected_index = state.code_block_popup.selected_index; + let content = state.code_block_popup.filtered_blocks[selected_index] + .content + .clone(); + let language = state.code_block_popup.filtered_blocks[selected_index] + .language + .clone(); + + if let Err(e) = copy_code_block_content_to_clipboard(state, &content, &language) { + state.error_popup.show(format!("{}", e)); + } + + state.code_block_popup.visible = false; + } + } + KeyCode::Esc => { + state.code_block_popup.visible = false; + } + KeyCode::Up => { + state.code_block_popup.move_selection_up(visible_items); + } + KeyCode::Down => { + state.code_block_popup.move_selection_down(visible_items); + } + _ => {} + } + Ok(false) +} + +fn extract_and_show_code_blocks(state: &mut UIState) -> Result<()> { + let content = state.scrollable_textarea.textareas[state.scrollable_textarea.focused_index] + .lines() + .join("\n"); + + let code_blocks = extract_code_blocks(&content); + + if code_blocks.is_empty() { + state + .error_popup + .show("No code blocks found in the current note.".to_string()); + return Ok(()); + } + + state.code_block_popup.set_code_blocks(code_blocks); + state.code_block_popup.visible = true; + Ok(()) +} + +fn copy_code_block_content_to_clipboard( + state: &mut UIState, + content: &str, + language: &str, +) -> Result<()> { + match &mut state.clipboard { + Some(clip) => { + if let Err(e) = clip.set_contents(content.to_string()) { + let backup_path = crate::get_clipboard_backup_file_path(); + std::fs::write(&backup_path, content)?; + + return Err(anyhow::anyhow!( + "Clipboard error: {}.\nContent saved to: {}\nPlease use 'thoth read_clipboard' to read the contents from STDOUT.", + e.to_string().split('\n').next().unwrap_or("Unknown error"), + backup_path.display() + )); + } + + state.copy_popup.show(format!( + "Copied code block [{}] to clipboard", + if language.is_empty() { + "no language" + } else { + language + } + )); + } + None => { + let backup_path = crate::get_clipboard_backup_file_path(); + std::fs::write(&backup_path, content)?; + + return Err(anyhow::anyhow!( + "Clipboard unavailable.\nContent saved to: {}\nPlease use 'thoth read_clipboard' to read the contents from STDOUT.", + backup_path.display() + )); + } + } + Ok(()) +} + pub fn handle_input( terminal: &mut Terminal>, state: &mut UIState, @@ -144,6 +246,8 @@ pub fn handle_input( handle_title_popup_input(state, key) } else if state.title_select_popup.visible { handle_title_select_popup_input(state, key) + } else if state.code_block_popup.visible { + handle_code_block_popup_input(state, key) } else { handle_normal_input(terminal, state, key) } @@ -349,6 +453,18 @@ fn handle_normal_input( }; state.config.set_theme(new_theme.clone())?; } + KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => { + if !state.scrollable_textarea.edit_mode { + if let Err(e) = extract_and_show_code_blocks(state) { + state + .error_popup + .show(format!("Error extracting code blocks: {}", e)); + } + } else { + state.scrollable_textarea.textareas[state.scrollable_textarea.focused_index] + .input(key); + } + } KeyCode::Char('y') if key.modifiers.contains(KeyModifiers::CONTROL) => { match state.scrollable_textarea.copy_focused_textarea_contents() { Ok(_) => { @@ -402,6 +518,7 @@ CLIPBOARD: • ^y: Copy current block • ^v: Paste from clipboard • ^b: Copy selection (in edit mode) + • ^c: Copy code block from current note FORMATTING: • ^j: Format as JSON diff --git a/src/utils.rs b/src/utils.rs index 5982a4d..eb7dacc 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,9 +1,56 @@ +use crate::code_block_popup::CodeBlock; use anyhow::Result; use std::io::{BufRead, Write}; use std::path::PathBuf; use std::{fs::File, io::BufReader}; use tui_textarea::TextArea; +pub fn extract_code_blocks(content: &str) -> Vec { + let mut code_blocks = Vec::new(); + let mut in_code_block = false; + let mut current_block = String::new(); + let mut current_language = String::new(); + let mut start_line = 0; + + for (i, line) in content.lines().enumerate() { + if line.trim().starts_with("```") { + if in_code_block { + // End of code block + code_blocks.push(CodeBlock::new( + current_block.trim_end().to_string(), + current_language.clone(), + start_line, + i, + )); + current_block.clear(); + current_language.clear(); + in_code_block = false; + } else { + // Start of code block + in_code_block = true; + start_line = i; + + let lang_part = line.trim_start_matches('`').trim(); + current_language = lang_part.to_string(); + } + } else if in_code_block { + current_block.push_str(line); + current_block.push('\n'); + } + } + + if in_code_block && !current_block.is_empty() { + code_blocks.push(CodeBlock::new( + current_block.trim_end().to_string(), + current_language, + start_line, + content.lines().count() - 1, + )); + } + + code_blocks +} + pub fn save_textareas(textareas: &[TextArea], titles: &[String], file_path: PathBuf) -> Result<()> { let mut file = File::create(file_path)?; for (textarea, title) in textareas.iter().zip(titles.iter()) { @@ -71,3 +118,54 @@ pub fn load_textareas(file_path: PathBuf) -> Result<(Vec>, Vec Ok((textareas, titles)) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_extract_code_blocks() { + let content = r#"# Test Document + +```rust +fn main() { + println!("Hello, world!"); +} +``` + +Some text here + +```python +def hello(): + print("Hello") +```"#; + + let blocks = extract_code_blocks(content); + assert_eq!(blocks.len(), 2); + assert_eq!(blocks[0].language, "rust"); + assert_eq!( + blocks[0].content, + "fn main() {\n println!(\"Hello, world!\");\n}" + ); + assert_eq!(blocks[1].language, "python"); + assert_eq!(blocks[1].content, "def hello():\n print(\"Hello\")"); + } + + #[test] + fn test_extract_empty_code_blocks() { + let content = "```rust\n```"; + let blocks = extract_code_blocks(content); + assert_eq!(blocks.len(), 1); + assert_eq!(blocks[0].language, "rust"); + assert_eq!(blocks[0].content, ""); + } + + #[test] + fn test_unclosed_code_block() { + let content = "```js\nlet x = 1;"; + let blocks = extract_code_blocks(content); + assert_eq!(blocks.len(), 1); + assert_eq!(blocks[0].language, "js"); + assert_eq!(blocks[0].content, "let x = 1;"); + } +} From 99fd574a9046fc22a2f67c98e3f08359cf83bdda Mon Sep 17 00:00:00 2001 From: jooaf Date: Sun, 13 Apr 2025 19:39:28 -0500 Subject: [PATCH 2/3] handing popup transitions --- src/ui_handler.rs | 86 +++++++++++++++++++++++++++++++---------------- 1 file changed, 57 insertions(+), 29 deletions(-) diff --git a/src/ui_handler.rs b/src/ui_handler.rs index 92fa99c..1c7a0bd 100644 --- a/src/ui_handler.rs +++ b/src/ui_handler.rs @@ -106,6 +106,9 @@ pub fn draw_ui( .unwrap(); } + if state.copy_popup.visible { + render_ui_popup(f, &state.copy_popup, theme); + } if state.title_popup.visible { render_title_popup(f, &state.title_popup, theme); } else if state.title_select_popup.visible { @@ -125,10 +128,6 @@ pub fn draw_ui( render_ui_popup(f, &state.help_popup, theme); } - if state.copy_popup.visible { - render_ui_popup(f, &state.copy_popup, theme); - } - if state.help_popup.visible { render_ui_popup(f, &state.help_popup, theme); } @@ -240,31 +239,64 @@ pub fn handle_input( return Ok(false); } - if state.scrollable_textarea.full_screen_mode { - handle_full_screen_input(state, key) + if state.code_block_popup.visible { + handle_code_block_popup_input(state, key) + } else if state.scrollable_textarea.full_screen_mode { + handle_full_screen_input(terminal, state, key) } else if state.title_popup.visible { handle_title_popup_input(state, key) } else if state.title_select_popup.visible { handle_title_select_popup_input(state, key) - } else if state.code_block_popup.visible { - handle_code_block_popup_input(state, key) } else { handle_normal_input(terminal, state, key) } } -fn handle_full_screen_input(state: &mut UIState, key: event::KeyEvent) -> Result { +fn handle_full_screen_input( + terminal: &mut Terminal>, + state: &mut UIState, + key: event::KeyEvent, +) -> Result { match key.code { KeyCode::Esc => { - if state.scrollable_textarea.edit_mode { + if state.copy_popup.visible { + state.copy_popup.hide(); + } else if state.error_popup.visible { + state.error_popup.hide(); + } else if state.help_popup.visible { + state.help_popup.hide(); + } else if state.edit_commands_popup.visible { + state.edit_commands_popup.visible = false; + } else if state.scrollable_textarea.edit_mode { state.scrollable_textarea.edit_mode = false; } else { state.scrollable_textarea.toggle_full_screen(); + state + .scrollable_textarea + .jump_to_textarea(state.scrollable_textarea.focused_index); } + } + KeyCode::Char('e') if key.modifiers.contains(KeyModifiers::CONTROL) => { + if state.scrollable_textarea.edit_mode { + match edit_with_external_editor(state) { + Ok(edited_content) => { + let mut new_textarea = TextArea::default(); + for line in edited_content.lines() { + new_textarea.insert_str(line); + new_textarea.insert_newline(); + } + state.scrollable_textarea.textareas + [state.scrollable_textarea.focused_index] = new_textarea; - state - .scrollable_textarea - .jump_to_textarea(state.scrollable_textarea.focused_index); + terminal.clear()?; + } + Err(e) => { + state + .error_popup + .show(format!("Failed to edit with external editor: {}", e)); + } + } + } } KeyCode::Enter => { if !state.scrollable_textarea.edit_mode { @@ -288,6 +320,18 @@ fn handle_full_screen_input(state: &mut UIState, key: event::KeyEvent) -> Result state.scrollable_textarea.handle_scroll(1); } } + KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => { + if !state.scrollable_textarea.edit_mode { + if let Err(e) = extract_and_show_code_blocks(state) { + state + .error_popup + .show(format!("Error extracting code blocks: {}", e)); + } + } else { + state.scrollable_textarea.textareas[state.scrollable_textarea.focused_index] + .input(key); + } + } KeyCode::Char('y') if key.modifiers.contains(KeyModifiers::CONTROL) => { match state.scrollable_textarea.copy_focused_textarea_contents() { Ok(_) => { @@ -313,22 +357,6 @@ fn handle_full_screen_input(state: &mut UIState, key: event::KeyEvent) -> Result } } } - KeyCode::Char('s') - if key.modifiers.contains(KeyModifiers::ALT) - && key.modifiers.contains(KeyModifiers::SHIFT) => - { - if state.scrollable_textarea.edit_mode { - state.scrollable_textarea.textareas[state.scrollable_textarea.focused_index] - .start_selection(); - } - } - KeyCode::Char('b') if key.modifiers.contains(KeyModifiers::CONTROL) => { - if let Err(e) = state.scrollable_textarea.copy_selection_contents() { - state - .error_popup - .show(format!("Failed to copy to clipboard: {}", e)); - } - } _ => { if state.scrollable_textarea.edit_mode { state.scrollable_textarea.textareas[state.scrollable_textarea.focused_index] From 6199f8b9010835b2aa05d89eb4ac7e362ea14569 Mon Sep 17 00:00:00 2001 From: jooaf Date: Sun, 13 Apr 2025 19:52:38 -0500 Subject: [PATCH 3/3] cleaning --- src/ui.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/ui.rs b/src/ui.rs index 426625f..e01efaa 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -251,19 +251,14 @@ pub fn render_code_block_popup(f: &mut Frame, popup: &CodeBlockPopup, theme: &Th let area = centered_rect(80, 80, f.size()); f.render_widget(ratatui::widgets::Clear, area); - // Split the area into two parts: selection list and code preview let chunks = Layout::default() .direction(Direction::Vertical) - .constraints([ - Constraint::Length(3), // Title + borders - Constraint::Min(1), // Code content area - ]) + .constraints([Constraint::Length(3), Constraint::Min(1)]) .split(area); let title_area = chunks[0]; let code_area = chunks[1]; - // Create the title block let title_block = Block::default() .borders(Borders::TOP | Borders::LEFT | Borders::RIGHT) .border_style(Style::default().fg(theme.primary)) @@ -278,7 +273,6 @@ pub fn render_code_block_popup(f: &mut Frame, popup: &CodeBlockPopup, theme: &Th } )); - // Render title with navigation instructions let title_text = vec![Line::from(vec![ Span::raw(" "), Span::styled("↑/↓", Style::default().fg(theme.accent)), @@ -398,7 +392,7 @@ pub fn render_ui_popup(f: &mut Frame, popup: &UiPopup, theme: &ThemeColors) { .border_style(Style::default().fg(theme.error)) .title(format!("{} - Esc to exit", popup.popup_title)), ) - .wrap(ratatui::widgets::Wrap { trim: true }); // Enable text wrapping + .wrap(ratatui::widgets::Wrap { trim: true }); f.render_widget(text, area); }