diff --git a/Cargo.lock b/Cargo.lock index b77e92d..b09dccf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -139,15 +139,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" -[[package]] -name = "castaway" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" -dependencies = [ - "rustversion", -] - [[package]] name = "cc" version = "1.2.19" @@ -205,7 +196,7 @@ version = "4.5.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn", @@ -232,20 +223,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" -[[package]] -name = "compact_str" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" -dependencies = [ - "castaway", - "cfg-if", - "itoa", - "rustversion", - "ryu", - "static_assertions", -] - [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -276,29 +253,13 @@ dependencies = [ "bitflags", "crossterm_winapi", "libc", - "mio 0.8.11", + "mio", "parking_lot", "signal-hook", "signal-hook-mio", "winapi", ] -[[package]] -name = "crossterm" -version = "0.28.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" -dependencies = [ - "bitflags", - "crossterm_winapi", - "mio 1.0.3", - "parking_lot", - "rustix", - "signal-hook", - "signal-hook-mio", - "winapi", -] - [[package]] name = "crossterm_winapi" version = "0.9.1" @@ -308,41 +269,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core", - "quote", - "syn", -] - [[package]] name = "derive_arbitrary" version = "1.4.1" @@ -375,16 +301,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" -[[package]] -name = "errno" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - [[package]] name = "excel-cli" version = "0.2.0" @@ -393,7 +309,7 @@ dependencies = [ "calamine", "chrono", "clap", - "crossterm 0.27.0", + "crossterm", "indexmap", "ratatui", "rust_xlsxwriter", @@ -412,12 +328,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - [[package]] name = "foldhash" version = "0.1.5" @@ -435,6 +345,12 @@ dependencies = [ "foldhash", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" @@ -465,12 +381,6 @@ dependencies = [ "cc", ] -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - [[package]] name = "indexmap" version = "2.9.0" @@ -488,19 +398,6 @@ version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" -[[package]] -name = "instability" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf9fed6d91cfb734e7476a06bde8300a1b94e217e1b523b6f0cd1a01998c71d" -dependencies = [ - "darling", - "indoc", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -509,9 +406,9 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itertools" -version = "0.13.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" dependencies = [ "either", ] @@ -538,12 +435,6 @@ version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - [[package]] name = "lock_api" version = "0.4.12" @@ -596,18 +487,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "mio" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.52.0", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -682,23 +561,20 @@ dependencies = [ [[package]] name = "ratatui" -version = "0.29.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +checksum = "0ebc917cfb527a566c37ecb94c7e3fd098353516fb4eb6bea17015ade0182425" dependencies = [ "bitflags", "cassowary", - "compact_str", - "crossterm 0.28.1", + "crossterm", "indoc", - "instability", "itertools", "lru", "paste", "strum", "unicode-segmentation", - "unicode-truncate", - "unicode-width 0.2.0", + "unicode-width", ] [[package]] @@ -719,19 +595,6 @@ dependencies = [ "zip 2.6.1", ] -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.59.0", -] - [[package]] name = "rustversion" version = "1.0.20" @@ -805,8 +668,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", - "mio 0.8.11", - "mio 1.0.3", + "mio", "signal-hook", ] @@ -831,12 +693,6 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "strsim" version = "0.11.1" @@ -845,20 +701,20 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.26.3" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.26.4" +version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "rustversion", @@ -878,13 +734,13 @@ dependencies = [ [[package]] name = "tui-textarea" -version = "0.7.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a5318dd619ed73c52a9417ad19046724effc1287fb75cdcc4eca1d6ac1acbae" +checksum = "a3e38ced1f941a9cfc923fbf2fe6858443c42cc5220bfd35bdd3648371e7bd8e" dependencies = [ - "crossterm 0.28.1", + "crossterm", "ratatui", - "unicode-width 0.2.0", + "unicode-width", ] [[package]] @@ -899,29 +755,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" -[[package]] -name = "unicode-truncate" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" -dependencies = [ - "itertools", - "unicode-segmentation", - "unicode-width 0.1.14", -] - [[package]] name = "unicode-width" version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" -[[package]] -name = "unicode-width" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" - [[package]] name = "utf8parse" version = "0.2.2" @@ -1082,15 +921,6 @@ dependencies = [ "windows-targets 0.48.5", ] -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.59.0" diff --git a/Cargo.toml b/Cargo.toml index aa5d68d..8acc0a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ categories = ["command-line-utilities", "development-tools", "data-structures", exclude = ["/.github", "CHANGELOG.md", ".gitignore"] [dependencies] -ratatui = "0.29.0" +ratatui = "0.24.0" crossterm = "0.27.0" calamine = "0.22.1" anyhow = "1.0.79" @@ -20,7 +20,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" chrono = "0.4" indexmap = { version = "2.0", features = ["serde"] } -tui-textarea = "0.7.0" +tui-textarea = "0.4.0" [profile.release] opt-level = 3 diff --git a/src/app/edit.rs b/src/app/edit.rs index 239957e..9410da2 100644 --- a/src/app/edit.rs +++ b/src/app/edit.rs @@ -3,6 +3,7 @@ use crate::app::AppState; use crate::app::InputMode; use crate::app::{Transition, VimMode, VimState}; use anyhow::Result; +use ratatui::style::{Modifier, Style}; use tui_textarea::Input; impl AppState<'_> { @@ -11,10 +12,14 @@ impl AppState<'_> { let content = self.get_cell_content(self.selected_cell.0, self.selected_cell.1); self.input_buffer = content.clone(); - self.text_area = tui_textarea::TextArea::default(); - self.text_area.insert_str(&content); - self.text_area.set_tab_length(4); + // Initialize TextArea with content and settings + let mut text_area = tui_textarea::TextArea::default(); + text_area.insert_str(&content); + text_area.set_tab_length(4); + text_area.set_cursor_line_style(Style::default()); + text_area.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED)); + self.text_area = text_area; self.vim_state = Some(VimState::new(VimMode::Normal)); } diff --git a/src/app/mod.rs b/src/app/mod.rs index e18025c..3f67c11 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -6,6 +6,7 @@ mod state; mod ui; mod undo_manager; mod vim; +mod word; pub use state::*; pub use vim::*; diff --git a/src/app/search.rs b/src/app/search.rs index f587cdd..8ad3ceb 100644 --- a/src/app/search.rs +++ b/src/app/search.rs @@ -1,11 +1,18 @@ use crate::app::AppState; use crate::app::InputMode; +use ratatui::style::{Modifier, Style}; impl AppState<'_> { pub fn start_search_forward(&mut self) { self.input_mode = InputMode::SearchForward; self.input_buffer = String::new(); - self.text_area = tui_textarea::TextArea::default(); + + // Initialize TextArea + let mut text_area = tui_textarea::TextArea::default(); + text_area.set_cursor_line_style(Style::default()); + text_area.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED)); + self.text_area = text_area; + self.add_notification("Search forward mode".to_string()); self.highlight_enabled = true; } @@ -13,7 +20,13 @@ impl AppState<'_> { pub fn start_search_backward(&mut self) { self.input_mode = InputMode::SearchBackward; self.input_buffer = String::new(); - self.text_area = tui_textarea::TextArea::default(); + + // Initialize TextArea + let mut text_area = tui_textarea::TextArea::default(); + text_area.set_cursor_line_style(Style::default()); + text_area.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED)); + self.text_area = text_area; + self.add_notification("Search backward mode".to_string()); self.highlight_enabled = true; } diff --git a/src/app/sheet.rs b/src/app/sheet.rs index 1c12b43..f7bf710 100644 --- a/src/app/sheet.rs +++ b/src/app/sheet.rs @@ -611,8 +611,7 @@ impl AppState<'_> { max_width = max_width.max(display_width); } - let padding = (max_width / 5).max(2); - max_width + padding + max_width } pub fn get_column_width(&self, col: usize) -> usize { diff --git a/src/app/vim.rs b/src/app/vim.rs index 4fc72ab..feb2652 100644 --- a/src/app/vim.rs +++ b/src/app/vim.rs @@ -3,6 +3,8 @@ use ratatui::widgets::Block; use std::fmt; use tui_textarea::{CursorMove, Input, Key, TextArea}; +use crate::app::word::move_cursor_to_word_end; + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum VimMode { Normal, @@ -109,9 +111,39 @@ impl VimState { ctrl: false, .. } => { - textarea.move_cursor(CursorMove::WordEnd); + // Use custom WordEnd implementation + let lines = textarea.lines(); + let (row, col) = textarea.cursor(); + let (new_row, new_col) = move_cursor_to_word_end(lines, row, col); + + // Set the cursor to the new position + if row != new_row { + // If need to move to a different row + while textarea.cursor().0 < new_row { + textarea.move_cursor(CursorMove::Down); + } + textarea.move_cursor(CursorMove::Head); + while textarea.cursor().1 < new_col { + textarea.move_cursor(CursorMove::Forward); + } + } else { + // If staying on the same row + if col < new_col { + // Move forward + while textarea.cursor().1 < new_col { + textarea.move_cursor(CursorMove::Forward); + } + } else { + // Move backward + while textarea.cursor().1 > new_col { + textarea.move_cursor(CursorMove::Back); + } + } + } + + // For operator mode, include the character under the cursor if matches!(self.mode, VimMode::Operator(_)) { - textarea.move_cursor(CursorMove::Forward); // Include the text under the cursor + textarea.move_cursor(CursorMove::Forward); } } Input { diff --git a/src/app/word.rs b/src/app/word.rs new file mode 100644 index 0000000..4a6c481 --- /dev/null +++ b/src/app/word.rs @@ -0,0 +1,69 @@ +// Custom implementation of word navigation functions from tui-textarea v0.5.2+ + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum CharKind { + Space, + Punctuation, + Other, +} + +impl CharKind { + fn new(c: char) -> Self { + if c.is_whitespace() { + Self::Space + } else if c.is_ascii_punctuation() { + Self::Punctuation + } else { + Self::Other + } + } +} + +/// Find the end of the next word +/// This is a custom implementation of the `find_word_end_next` function from tui-textarea v0.5.2+ +pub fn find_word_end_next(line: &str, start_col: usize) -> Option { + let mut it = line.chars().enumerate().skip(start_col); + let (mut cur_col, cur_char) = it.next()?; + let mut cur = CharKind::new(cur_char); + + for (next_col, c) in it { + let next = CharKind::new(c); + // if cursor started at the end of a word, don't stop + if next_col.saturating_sub(start_col) > 1 && cur != CharKind::Space && next != cur { + return Some(next_col.saturating_sub(1)); + } + cur = next; + cur_col = next_col; + } + + // if end of line is whitespace, don't stop the cursor + if cur != CharKind::Space && cur_col.saturating_sub(start_col) >= 1 { + return Some(cur_col); + } + + None +} + +/// Move cursor to the end of the next word +pub fn move_cursor_to_word_end(text: &[String], row: usize, col: usize) -> (usize, usize) { + if row >= text.len() { + return (row, col); + } + + let line = &text[row]; + + if let Some(new_col) = find_word_end_next(line, col) { + return (row, new_col); + } else if row + 1 < text.len() { + // Try to find word end in the next line + if let Some(new_col) = find_word_end_next(&text[row + 1], 0) { + return (row + 1, new_col); + } else if !text[row + 1].is_empty() { + // If no word end found but line is not empty, go to the end of the line + return (row + 1, text[row + 1].chars().count().saturating_sub(1)); + } + } + + // Can't find a word end, stay at the current position + (row, col) +} diff --git a/src/ui/render.rs b/src/ui/render.rs index ae147bb..f0fea38 100644 --- a/src/ui/render.rs +++ b/src/ui/render.rs @@ -22,13 +22,9 @@ use crate::utils::index_to_col_name; pub fn run_app(mut app_state: AppState) -> Result<()> { // Setup terminal - enable_raw_mode()?; - let mut stdout = io::stdout(); - stdout.execute(EnterAlternateScreen)?; - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; + let mut terminal = setup_terminal()?; - // Main loop + // Main event loop while !app_state.should_quit { terminal.draw(|f| ui(f, &mut app_state))?; @@ -42,6 +38,25 @@ pub fn run_app(mut app_state: AppState) -> Result<()> { } // Restore terminal + restore_terminal(&mut terminal)?; + + Ok(()) +} + +/// Setup the terminal for the application +fn setup_terminal() -> Result>> { + enable_raw_mode()?; + let mut stdout = io::stdout(); + stdout.execute(EnterAlternateScreen)?; + + let backend = CrosstermBackend::new(stdout); + let terminal = Terminal::new(backend)?; + + Ok(terminal) +} + +/// Restore the terminal to its original state +fn restore_terminal(terminal: &mut Terminal>) -> Result<()> { disable_raw_mode()?; terminal.backend_mut().execute(LeaveAlternateScreen)?; terminal.show_cursor()?; @@ -49,8 +64,9 @@ pub fn run_app(mut app_state: AppState) -> Result<()> { Ok(()) } +/// Update the visible area of the spreadsheet based on the available space fn update_visible_area(app_state: &mut AppState, area: Rect) { - // Calculate visible rows based on available height + // Calculate visible rows based on available height (subtract header and borders) app_state.visible_rows = (area.height as usize).saturating_sub(3); // Ensure the selected column is visible @@ -63,31 +79,28 @@ fn update_visible_area(app_state: &mut AppState, area: Rect) { let mut visible_cols = 0; let mut width_used = 0; - // Start from the leftmost visible column and add columns until run out of space + // Iterate through columns starting from the leftmost visible column for col_idx in app_state.start_col.. { let col_width = app_state.get_column_width(col_idx); - // Always include the first column even if it's wider than available space if col_idx == app_state.start_col { + // Always include the first column even if it's wider than available space width_used += col_width; visible_cols += 1; if width_used >= available_width { break; } - } - // For subsequent columns, add them if they fit completely - else if width_used + col_width <= available_width { + } else if width_used + col_width <= available_width { + // Add columns that fit completely width_used += col_width; visible_cols += 1; - } - // Excel-like behavior: include one partially visible column if there's any space left - else if width_used < available_width { + } else if width_used < available_width { + // Excel-like behavior: include one partially visible column visible_cols += 1; break; - } - // No more space available - else { + } else { + // No more space available break; } } @@ -106,7 +119,7 @@ fn ui(f: &mut Frame, app_state: &mut AppState) { Constraint::Length(app_state.info_panel_height as u16), // Info panel Constraint::Length(1), // Status bar ]) - .split(f.area()); + .split(f.size()); draw_title_with_tabs(f, app_state, chunks[0]); @@ -118,7 +131,7 @@ fn ui(f: &mut Frame, app_state: &mut AppState) { // If in help mode, draw the help popup over everything else if let InputMode::Help = app_state.input_mode { - draw_help_popup(f, app_state, f.area()); + draw_help_popup(f, app_state, f.size()); } } @@ -129,11 +142,11 @@ fn draw_spreadsheet(f: &mut Frame, app_state: &AppState, area: Rect) { let start_col = app_state.start_col; let end_col = start_col + app_state.visible_cols - 1; - let mut col_constraints = Vec::with_capacity(app_state.visible_cols + 1); - col_constraints.push(Constraint::Length(5)); // Row header width + let mut constraints = Vec::with_capacity(app_state.visible_cols + 1); + constraints.push(Constraint::Length(5)); // Row header width for col in start_col..=end_col { - col_constraints.push(Constraint::Length(app_state.get_column_width(col) as u16)); + constraints.push(Constraint::Length(app_state.get_column_width(col) as u16)); } // Set table style based on current mode @@ -156,6 +169,7 @@ fn draw_spreadsheet(f: &mut Frame, app_state: &AppState, area: Rect) { ) }; + // Create header row let mut header_cells = Vec::with_capacity(app_state.visible_cols + 1); header_cells.push(Cell::from("").style(header_style)); @@ -165,16 +179,14 @@ fn draw_spreadsheet(f: &mut Frame, app_state: &AppState, area: Rect) { header_cells.push(Cell::from(col_name).style(header_style)); } - let header_row = Row::new(header_cells).height(1); - - let mut rows = Vec::with_capacity(app_state.visible_rows); + let header = Row::new(header_cells).height(1); // Create data rows - for row in start_row..=end_row { - let row_header = Cell::from(row.to_string()).style(header_style); - + let rows = (start_row..=end_row).map(|row| { let mut cells = Vec::with_capacity(app_state.visible_cols + 1); - cells.push(row_header); + + // Add row header + cells.push(Cell::from(row.to_string()).style(header_style)); // Add cells for this row for col in start_col..=end_col { @@ -259,14 +271,17 @@ fn draw_spreadsheet(f: &mut Frame, app_state: &AppState, area: Rect) { cells.push(Cell::from(content).style(style)); } - rows.push(Row::new(cells)); - } + Row::new(cells) + }); - let table = Table::new(std::iter::once(header_row).chain(rows), &col_constraints) - .block(table_block) - .style(cell_style) - .row_highlight_style(Style::default().add_modifier(Modifier::BOLD)) - .highlight_symbol(">> "); + // Create table with header and rows + let table = Table::new( + // Combine header and data rows + std::iter::once(header).chain(rows), + ) + .block(table_block) + .style(cell_style) + .widths(&constraints); f.render_widget(table, area); } @@ -396,8 +411,6 @@ fn draw_info_panel(f: &mut Frame, app_state: &mut AppState, area: Rect) { .border_style(Style::default().fg(Color::LightCyan)) .title(title); - f.render_widget(edit_block.clone(), chunks[0]); - // Calculate inner area with padding let inner_area = edit_block.inner(chunks[0]); let padded_area = Rect { @@ -407,7 +420,8 @@ fn draw_info_panel(f: &mut Frame, app_state: &mut AppState, area: Rect) { height: inner_area.height, }; - f.render_widget(&app_state.text_area, padded_area); + f.render_widget(edit_block, chunks[0]); + f.render_widget(app_state.text_area.widget(), padded_area); } _ => { // Get cell content @@ -416,22 +430,12 @@ fn draw_info_panel(f: &mut Frame, app_state: &mut AppState, area: Rect) { let title = format!(" Cell {} Content ", cell_ref); let cell_block = Block::default().borders(Borders::ALL).title(title); - f.render_widget(cell_block.clone(), chunks[0]); - - // Calculate inner area with padding - let inner_area = cell_block.inner(chunks[0]); - let padded_area = Rect { - x: inner_area.x + 1, // Add 1 character padding on the left - y: inner_area.y, - width: inner_area.width.saturating_sub(2), // Subtract 2 for left and right padding - height: inner_area.height, - }; - + // Create paragraph with cell content let cell_paragraph = Paragraph::new(content) - .wrap(ratatui::widgets::Wrap { trim: false }) - .scroll((0, 0)); + .block(cell_block) + .wrap(ratatui::widgets::Wrap { trim: false }); - f.render_widget(cell_paragraph, padded_area); + f.render_widget(cell_paragraph, chunks[0]); } } @@ -450,77 +454,49 @@ fn draw_info_panel(f: &mut Frame, app_state: &mut AppState, area: Rect) { .title(" Notifications ") }; - f.render_widget(notification_block.clone(), chunks[1]); - - // Calculate inner area with padding - let inner_area = notification_block.inner(chunks[1]); - let padded_area = Rect { - x: inner_area.x + 1, // Add 1 character padding on the left - y: inner_area.y, - width: inner_area.width.saturating_sub(2), // Subtract 2 for left and right padding - height: inner_area.height, - }; - // Calculate how many notifications can be shown - let notification_height = inner_area.height as usize; + let notification_height = notification_block.inner(chunks[1]).height as usize; + // Prepare notifications text let notifications_text = if app_state.notification_messages.is_empty() { String::new() } else if app_state.notification_messages.len() <= notification_height { app_state.notification_messages.join("\n") } else { + // Show only the most recent notifications that fit let start_idx = app_state.notification_messages.len() - notification_height; - - let mut result = String::with_capacity( - app_state.notification_messages[start_idx..] - .iter() - .map(|s| s.len()) - .sum::() - + notification_height, // Account for newlines - ); - - for (i, msg) in app_state.notification_messages[start_idx..] - .iter() - .enumerate() - { - if i > 0 { - result.push('\n'); - } - result.push_str(msg); - } - - result + app_state.notification_messages[start_idx..].join("\n") }; - let notification_paragraph = if matches!(app_state.input_mode, InputMode::Editing) { - Paragraph::new(notifications_text) - .style(Style::default().fg(Color::DarkGray)) - .wrap(ratatui::widgets::Wrap { trim: false }) - .scroll((0, 0)) - } else { - Paragraph::new(notifications_text) - .wrap(ratatui::widgets::Wrap { trim: false }) - .scroll((0, 0)) - }; + let notification_paragraph = Paragraph::new(notifications_text) + .block(notification_block) + .wrap(ratatui::widgets::Wrap { trim: false }) + .style(if matches!(app_state.input_mode, InputMode::Editing) { + Style::default().fg(Color::DarkGray) + } else { + Style::default() + }); - f.render_widget(notification_paragraph, padded_area); + f.render_widget(notification_paragraph, chunks[1]); } fn draw_status_bar(f: &mut Frame, app_state: &AppState, area: Rect) { match app_state.input_mode { InputMode::Normal => { - let status = - "Input :help for operating instructions | hjkl=move [ ]=prev/next-sheet Enter=edit y=copy d=cut p=paste /=search N/n=prev/next-search-result :=command ".to_string(); + let status = "Input :help for operating instructions | hjkl=move [ ]=prev/next-sheet Enter=edit y=copy d=cut p=paste /=search N/n=prev/next-search-result :=command "; + + let status_widget = Paragraph::new(status) + .style(Style::default()) + .alignment(ratatui::layout::Alignment::Left); - let status_style = Style::default(); - let status_widget = Paragraph::new(status).style(status_style); f.render_widget(status_widget, area); } InputMode::Editing => { - let status_style = Style::default().fg(Color::DarkGray); - let status_widget = - Paragraph::new("Press Esc to exit editing mode".to_string()).style(status_style); + let status_widget = Paragraph::new("Press Esc to exit editing mode") + .style(Style::default().fg(Color::DarkGray)) + .alignment(ratatui::layout::Alignment::Left); + f.render_widget(status_widget, area); } @@ -531,52 +507,57 @@ fn draw_status_bar(f: &mut Frame, app_state: &AppState, area: Rect) { spans.extend(command_spans); let text = Line::from(spans); - let status_style = Style::default(); - let status_widget = Paragraph::new(text).style(status_style); + let status_widget = Paragraph::new(text) + .style(Style::default()) + .alignment(ratatui::layout::Alignment::Left); + f.render_widget(status_widget, area); } - InputMode::SearchForward => { - let text_area = app_state.text_area.clone(); + InputMode::SearchForward | InputMode::SearchBackward => { + // Get search prefix based on mode + let prefix = if matches!(app_state.input_mode, InputMode::SearchForward) { + "/" + } else { + "?" + }; + // Split the area for search prefix and search input let chunks = Layout::default() .direction(Direction::Horizontal) - .constraints([Constraint::Length(1), Constraint::Min(1)]) + .constraints([ + Constraint::Length(1), // Search prefix + Constraint::Min(1), // Search input + ]) .split(area); - let prefix_widget = Paragraph::new("/").style(Style::default()); - f.render_widget(prefix_widget, chunks[0]); + // Render search prefix + let prefix_widget = Paragraph::new(prefix) + .style(Style::default()) + .alignment(ratatui::layout::Alignment::Left); - f.render_widget(&text_area, chunks[1]); - } - - InputMode::SearchBackward => { - let text_area = app_state.text_area.clone(); - - let chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Length(1), Constraint::Min(1)]) - .split(area); - - let prefix_widget = Paragraph::new("?").style(Style::default()); f.render_widget(prefix_widget, chunks[0]); - f.render_widget(&text_area, chunks[1]); + // Render search input with cursor visible + let mut text_area = app_state.text_area.clone(); + text_area.set_cursor_line_style(Style::default()); + text_area.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED)); + + f.render_widget(text_area.widget(), chunks[1]); } - InputMode::Help => {} + InputMode::Help => { + // No status bar in help mode + } } } fn draw_help_popup(f: &mut Frame, app_state: &mut AppState, area: Rect) { - let overlay = Block::default() - .style(Style::default()) - .borders(Borders::NONE); + // Clear the background f.render_widget(Clear, area); - f.render_widget(overlay, area); + // Calculate popup dimensions let line_count = app_state.help_text.lines().count() as u16; - let content_height = line_count + 2; // +2 for borders let max_line_width = app_state @@ -588,6 +569,7 @@ fn draw_help_popup(f: &mut Frame, app_state: &mut AppState, area: Rect) { let content_width = max_line_width + 4; // +4 for borders and padding + // Ensure popup fits within screen let popup_width = content_width.min(area.width.saturating_sub(4)); let popup_height = content_height.min(area.height.saturating_sub(4)); @@ -597,6 +579,7 @@ fn draw_help_popup(f: &mut Frame, app_state: &mut AppState, area: Rect) { let popup_area = Rect::new(popup_x, popup_y, popup_width, popup_height); + // Calculate scrolling parameters let visible_lines = popup_height.saturating_sub(2) as usize; // Subtract 2 for top and bottom borders app_state.help_visible_lines = visible_lines; @@ -607,7 +590,6 @@ fn draw_help_popup(f: &mut Frame, app_state: &mut AppState, area: Rect) { let mut title = " [ESC/Enter to close] ".to_string(); - // Add scroll indicators if content is scrollable if max_scroll > 0 { let scroll_indicator = if app_state.help_scroll == 0 { " [↓ or j to scroll] " @@ -630,21 +612,13 @@ fn draw_help_popup(f: &mut Frame, app_state: &mut AppState, area: Rect) { .border_style(Style::default().fg(Color::LightCyan)) .style(Style::default().bg(Color::Blue).fg(Color::White)); - f.render_widget(help_block.clone(), popup_area); - - let inner_area = help_block.inner(popup_area); - let padded_area = Rect { - x: inner_area.x + 1, // Add 1 character padding on the left - y: inner_area.y, - width: inner_area.width.saturating_sub(2), // Subtract 2 for left and right padding - height: inner_area.height, - }; - + // Create paragraph with help text let help_paragraph = Paragraph::new(app_state.help_text.clone()) + .block(help_block) .wrap(ratatui::widgets::Wrap { trim: false }) .scroll((app_state.help_scroll as u16, 0)); - f.render_widget(help_paragraph, padded_area); + f.render_widget(help_paragraph, popup_area); } fn draw_title_with_tabs(f: &mut Frame, app_state: &AppState, area: Rect) { @@ -669,21 +643,25 @@ fn draw_title_with_tabs(f: &mut Frame, app_state: &AppState, area: Rect) { let mut tab_widths = Vec::new(); let mut total_width = 0; let mut visible_tabs = Vec::new(); + for (i, name) in sheet_names.iter().enumerate() { - let tab_width = name.len() + 2; + let tab_width = name.len(); if total_width + tab_width <= available_width { tab_widths.push(tab_width as u16); total_width += tab_width; visible_tabs.push(i); } else { + // If current tab isn't visible, make room for it if !visible_tabs.contains(¤t_index) { + // Remove tabs from the beginning until there's enough space while !visible_tabs.is_empty() && total_width + tab_width > available_width { let removed_width = tab_widths.remove(0) as usize; visible_tabs.remove(0); total_width -= removed_width; } + // Add current tab if there's now enough space if total_width + tab_width <= available_width { tab_widths.push(tab_width as u16); visible_tabs.push(current_index); @@ -693,6 +671,7 @@ fn draw_title_with_tabs(f: &mut Frame, app_state: &AppState, area: Rect) { } } + // Limit title width to at most 2/3 of the area let max_title_width = (area.width * 2 / 3).min(title_width); // Create a two-column layout: title column and tab column @@ -701,27 +680,29 @@ fn draw_title_with_tabs(f: &mut Frame, app_state: &AppState, area: Rect) { .constraints([Constraint::Length(max_title_width), Constraint::Min(0)]) .split(area); - let title_widget = if is_editing { - Paragraph::new(title_content.clone()) - .style(Style::default().bg(Color::DarkGray).fg(Color::Gray)) + let title_style = if is_editing { + Style::default().bg(Color::DarkGray).fg(Color::Gray) } else { - Paragraph::new(title_content.clone()) - .style(Style::default().bg(Color::DarkGray).fg(Color::White)) + Style::default().bg(Color::DarkGray).fg(Color::White) }; + + let title_widget = Paragraph::new(title_content).style(title_style); + f.render_widget(title_widget, horizontal_layout[0]); + // Create constraints for tab layout let mut tab_constraints = Vec::new(); for &width in &tab_widths { tab_constraints.push(Constraint::Length(width)); } - - tab_constraints.push(Constraint::Min(0)); + tab_constraints.push(Constraint::Min(0)); // Filler space let tab_layout = Layout::default() .direction(Direction::Horizontal) .constraints(tab_constraints) .split(horizontal_layout[1]); + // Render each visible tab for (layout_idx, &sheet_idx) in visible_tabs.iter().enumerate() { if layout_idx >= tab_layout.len() - 1 { break; @@ -745,14 +726,17 @@ fn draw_title_with_tabs(f: &mut Frame, app_state: &AppState, area: Rect) { let tab_widget = Paragraph::new(name.to_string()) .style(style) .alignment(ratatui::layout::Alignment::Center); + f.render_widget(tab_widget, tab_layout[layout_idx]); } + // Show indicator if not all tabs are visible if visible_tabs.len() < sheet_names.len() { let more_indicator = "..."; let indicator_style = Style::default().bg(Color::DarkGray).fg(Color::White); let indicator_width = more_indicator.len() as u16; + // Position indicator at the right edge let indicator_rect = Rect { x: area.x + area.width - indicator_width, y: area.y,