Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- Vertical scrollbar for tables taller than the interactive viewport ([#38](https://github.com/bgreenwell/xleak/issues/38))
- OSC 52 clipboard support: `c`/`C` now copy via OSC 52 (works over SSH) in addition to the system clipboard
- CSV/TSV support: read and interactively view `.csv`/`.tsv` files as a single sheet (behind the default-on `csv` feature)
- `--csv-delimiter` option to override the inferred CSV/TSV field delimiter
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Inspired by [doxx](https://github.com/bgreenwell/doxx), `xleak` brings Excel spr
- **Formula display** - view Excel formulas in cell detail view (Enter key)
- **Jump to row/column** - press `Ctrl+G` to jump to any cell (e.g., `A100`, `500`, `10,5`)
- **Large file optimization** - lazy loading for files with 1000+ rows
- **Vertical scrollbar** - shows the current position when table rows exceed the viewport
- **Progress indicators** - real-time feedback for long operations
- **Visual cell highlighting** - current row, column, and cell clearly marked

Expand Down
66 changes: 65 additions & 1 deletion src/tui/rendering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,27 @@ use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Cell, Clear, Paragraph, Row, Table, Wrap},
widgets::{
Block, Borders, Cell, Clear, Paragraph, Row, Scrollbar, ScrollbarOrientation,
ScrollbarState, Table, Wrap,
},
};
use std::time::Duration;

use super::state::TuiState;

fn table_scrollbar_state(
content_length: usize,
viewport_height: usize,
scroll_offset: usize,
) -> Option<ScrollbarState> {
(viewport_height > 0 && content_length > viewport_height).then(|| {
ScrollbarState::new(content_length)
.position(scroll_offset)
.viewport_content_length(viewport_height)
})
}

impl TuiState {
pub fn render(&mut self, frame: &mut Frame) {
let area = frame.area();
Expand Down Expand Up @@ -342,6 +357,29 @@ impl TuiState {

frame.render_widget(table, chunks[0]);

if let Some(mut scrollbar_state) =
table_scrollbar_state(self.sheet_data.height(), table_height, self.scroll_offset)
{
let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(None)
.end_symbol(None)
.track_symbol(Some("│"))
.thumb_symbol("█")
.track_style(Style::default().fg(colors.border_fg))
.thumb_style(Style::default().fg(colors.current_col_fg));
let scrollbar_area = Rect::new(
chunks[0].right().saturating_sub(1),
chunks[0]
.y
.saturating_add(1)
.saturating_add(header_row_count),
1,
table_height as u16,
);

frame.render_stateful_widget(scrollbar, scrollbar_area, &mut scrollbar_state);
}

// Info bar: left segment shows the cell address (and live search query while
// searching); right segment shows the theme and key hints.
let cell_addr = self.current_cell_address();
Expand Down Expand Up @@ -1075,3 +1113,29 @@ impl TuiState {
frame.render_widget(feedback_paragraph, popup_area);
}
}

#[cfg(test)]
mod tests {
use ratatui::widgets::ScrollbarState;

use super::table_scrollbar_state;

#[test]
fn table_scrollbar_is_hidden_when_all_rows_fit() {
assert_eq!(table_scrollbar_state(8, 8, 0), None);
assert_eq!(table_scrollbar_state(5, 8, 0), None);
assert_eq!(table_scrollbar_state(5, 0, 0), None);
}

#[test]
fn table_scrollbar_tracks_the_visible_rows_and_offset() {
assert_eq!(
table_scrollbar_state(20, 8, 7),
Some(
ScrollbarState::new(20)
.position(7)
.viewport_content_length(8)
)
);
}
}