scrollbartable widget

This commit is contained in:
Robin Appelman 2024-07-29 20:17:23 +02:00
commit 5d4a17f289
7 changed files with 157 additions and 178 deletions

View file

@ -1,13 +1,14 @@
use crate::app::{App, LogMatch}; use crate::app::{App, LogMatch};
use crate::ui::histogram::sparkline; use crate::ui::histogram::sparkline;
use crate::ui::style::{TABLE_HEADER_STYLE, TABLE_SELECTED_STYLE}; use crate::ui::style::TABLE_HEADER_STYLE;
use crate::ui::table::ScrollbarTable;
use itertools::Either; use itertools::Either;
use ratatui::prelude::*; use ratatui::prelude::*;
use ratatui::widgets::{Cell, HighlightSpacing, Row, Table}; use ratatui::widgets::{Cell, Row};
use std::fmt::Write; use std::fmt::Write;
use std::iter::{empty, once}; use std::iter::{empty, once};
pub fn match_list(app: &App) -> Table { pub fn match_list(app: &App) -> ScrollbarTable {
let header = [ let header = [
Text::from("Statement"), Text::from("Statement"),
Text::from("File"), Text::from("File"),
@ -36,15 +37,13 @@ pub fn match_list(app: &App) -> Table {
Either::Left(once(log_row(&app.unmatched, app, "Unmatched lines"))) Either::Left(once(log_row(&app.unmatched, app, "Unmatched lines")))
}; };
Table::new( ScrollbarTable::new(
once(all) once(all)
.chain(app.matches.iter().map(|result| log_row(result, app, ""))) .chain(app.matches.iter().map(|result| log_row(result, app, "")))
.chain(unmatched), .chain(unmatched),
widths, widths,
) )
.header(header) .header(header)
.highlight_style(TABLE_SELECTED_STYLE)
.highlight_spacing(HighlightSpacing::Always)
} }
fn log_row<'a>(result: &LogMatch, app: &'a App, name: &'static str) -> Row<'a> { fn log_row<'a>(result: &LogMatch, app: &'a App, name: &'static str) -> Row<'a> {

View file

@ -13,7 +13,6 @@ use ratatui::crossterm::terminal::{
}; };
use ratatui::crossterm::{event, ExecutableCommand}; use ratatui::crossterm::{event, ExecutableCommand};
use ratatui::prelude::*; use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders, Scrollbar, ScrollbarOrientation};
use ratatui::Terminal; use ratatui::Terminal;
use std::io; use std::io;
use std::io::stdout; use std::io::stdout;
@ -26,6 +25,7 @@ mod single_log;
mod single_match; mod single_match;
mod state; mod state;
pub mod style; pub mod style;
mod table;
pub fn run_ui(app: App) -> Result<(), UiError> { pub fn run_ui(app: App) -> Result<(), UiError> {
enable_raw_mode()?; enable_raw_mode()?;
@ -91,11 +91,8 @@ fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
match state { match state {
UiState::Quit => {} UiState::Quit => {}
UiState::MatchList(MatchListState { UiState::MatchList(MatchListState { table_state }) => {
table_state, let selected = table_state.selected();
scroll_state,
}) => {
let selected = table_state.selected().unwrap_or(0);
let histogram = if selected == 0 { let histogram = if selected == 0 {
&app.all.histogram &app.all.histogram
} else if selected < app.matches.len() + 1 { } else if selected < app.matches.len() + 1 {
@ -104,75 +101,30 @@ fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
} else { } else {
&app.unmatched.histogram &app.unmatched.histogram
}; };
let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some(""))
.end_symbol(Some(""));
frame.render_widget(UiHistogram::new(histogram), layout[0]); frame.render_widget(UiHistogram::new(histogram), layout[0]);
frame.render_stateful_widget( frame.render_stateful_widget(match_list(app), layout[1], table_state);
match_list(app).block(Block::new().borders(Borders::RIGHT)),
layout[1],
table_state,
);
frame.render_stateful_widget(
scrollbar,
layout[1].inner(Margin {
vertical: 1,
horizontal: 0,
}),
scroll_state,
);
frame.render_widget(footer(app, page), layout[2]); frame.render_widget(footer(app, page), layout[2]);
} }
UiState::Match(MatchState { UiState::Match(MatchState {
result, result,
table_state, table_state,
scroll_state,
.. ..
}) => { }) => {
let selected_group = &result.grouped[table_state.selected().unwrap_or_default()]; let selected_group = &result.grouped[table_state.selected()];
let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some(""))
.end_symbol(Some(""));
frame.render_widget(UiHistogram::new(&selected_group.histogram), layout[0]); frame.render_widget(UiHistogram::new(&selected_group.histogram), layout[0]);
frame.render_stateful_widget( frame.render_stateful_widget(grouped_lines(app, result), layout[1], table_state);
grouped_lines(app, result).block(Block::new().borders(Borders::RIGHT)),
layout[1],
table_state,
);
frame.render_stateful_widget(
scrollbar,
layout[1].inner(Margin {
vertical: 1,
horizontal: 0,
}),
scroll_state,
);
frame.render_widget(footer(app, page), layout[2]); frame.render_widget(footer(app, page), layout[2]);
} }
UiState::Logs(LogsState { UiState::Logs(LogsState {
lines, lines, table_state, ..
table_state,
scroll_state,
..
}) => { }) => {
let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some(""))
.end_symbol(Some(""));
frame.render_stateful_widget( frame.render_stateful_widget(
raw_logs(app, lines).block(Block::new().borders(Borders::RIGHT)), raw_logs(app, lines),
layout[0].union(layout[1]), layout[0].union(layout[1]),
table_state, table_state,
); );
frame.render_stateful_widget(
scrollbar,
layout[0].union(layout[1]).inner(Margin {
vertical: 1,
horizontal: 0,
}),
scroll_state,
);
frame.render_widget(footer(app, page), layout[2]); frame.render_widget(footer(app, page), layout[2]);
} }
UiState::Log(LogState { UiState::Log(LogState {

View file

@ -1,12 +1,13 @@
use crate::app::App; use crate::app::App;
use crate::logline::LogLine; use crate::logline::LogLine;
use crate::ui::style::{TABLE_HEADER_STYLE, TABLE_SELECTED_STYLE, TIME_FORMAT}; use crate::ui::style::{TABLE_HEADER_STYLE, TIME_FORMAT};
use crate::ui::table::ScrollbarTable;
use ratatui::layout::{Alignment, Constraint}; use ratatui::layout::{Alignment, Constraint};
use ratatui::text::Text; use ratatui::text::Text;
use ratatui::widgets::{Cell, HighlightSpacing, Row, Table}; use ratatui::widgets::{Cell, Row};
use time::format_description::well_known::Iso8601; use time::format_description::well_known::Iso8601;
pub fn raw_logs(app: &App, lines: &[usize]) -> Table<'static> { pub fn raw_logs(app: &App, lines: &[usize]) -> ScrollbarTable<'static> {
let lines = lines.iter().copied().map(|i| &app.lines[i]); let lines = lines.iter().copied().map(|i| &app.lines[i]);
let header = [ let header = [
Text::from("Level"), Text::from("Level"),
@ -26,11 +27,7 @@ pub fn raw_logs(app: &App, lines: &[usize]) -> Table<'static> {
Constraint::Percentage(100), Constraint::Percentage(100),
Constraint::Length(27), Constraint::Length(27),
]; ];
let table = Table::new(lines.map(log_row), widths) ScrollbarTable::new(lines.map(log_row), widths).header(header)
.header(header)
.highlight_style(TABLE_SELECTED_STYLE)
.highlight_spacing(HighlightSpacing::Always);
table
} }
fn log_row(line: &LogLine) -> Row<'static> { fn log_row(line: &LogLine) -> Row<'static> {

View file

@ -1,8 +1,9 @@
use crate::app::App; use crate::app::App;
use crate::logline::{FullException, FullLogLine, LogLine, Trace}; use crate::logline::{FullException, FullLogLine, LogLine, Trace};
use crate::ui::style::{TABLE_HEADER_STYLE, TABLE_SELECTED_STYLE, TIME_FORMAT}; use crate::ui::style::{TABLE_HEADER_STYLE, TIME_FORMAT};
use crate::ui::table::{ScrollbarTable, ScrollbarTableState};
use ratatui::prelude::*; use ratatui::prelude::*;
use ratatui::widgets::{Cell, HighlightSpacing, Paragraph, Row, Table, TableState, Wrap}; use ratatui::widgets::{Cell, Paragraph, Row, Wrap};
use std::iter::once; use std::iter::once;
use time::format_description::well_known::Iso8601; use time::format_description::well_known::Iso8601;
@ -17,7 +18,7 @@ pub struct SingleLog {
} }
impl StatefulWidget for SingleLog { impl StatefulWidget for SingleLog {
type State = TableState; type State = ScrollbarTableState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State)
where where
@ -48,7 +49,7 @@ impl StatefulWidget for SingleLog {
} }
} }
pub fn render_exception(exception: &FullException) -> Table { pub fn render_exception(exception: &FullException) -> ScrollbarTable {
let header = [ let header = [
Text::from("File"), Text::from("File"),
Text::from("Line").alignment(Alignment::Right), Text::from("Line").alignment(Alignment::Right),
@ -66,11 +67,7 @@ pub fn render_exception(exception: &FullException) -> Table {
Constraint::Percentage(60), Constraint::Percentage(60),
]; ];
let rows = exception.stack().flat_map(exception_trace); let rows = exception.stack().flat_map(exception_trace);
let table = Table::new(rows, widths) ScrollbarTable::new(rows, widths).header(header)
.header(header)
.highlight_style(TABLE_SELECTED_STYLE)
.highlight_spacing(HighlightSpacing::Always);
table
} }
fn exception_trace(exception: &FullException) -> impl Iterator<Item = Row> + '_ { fn exception_trace(exception: &FullException) -> impl Iterator<Item = Row> + '_ {

View file

@ -1,11 +1,12 @@
use crate::app::{App, GroupedLines, LogMatch}; use crate::app::{App, GroupedLines, LogMatch};
use crate::ui::histogram::sparkline; use crate::ui::histogram::sparkline;
use crate::ui::style::{TABLE_HEADER_STYLE, TABLE_SELECTED_STYLE}; use crate::ui::style::TABLE_HEADER_STYLE;
use crate::ui::table::ScrollbarTable;
use ratatui::layout::Constraint; use ratatui::layout::Constraint;
use ratatui::text::Text; use ratatui::text::Text;
use ratatui::widgets::{Cell, HighlightSpacing, Row, Table}; use ratatui::widgets::{Cell, Row};
pub fn grouped_lines(app: &App, log_match: &LogMatch) -> Table<'static> { pub fn grouped_lines(app: &App, log_match: &LogMatch) -> ScrollbarTable<'static> {
let grouped = &log_match.grouped; let grouped = &log_match.grouped;
let header = [ let header = [
Text::from("Level"), Text::from("Level"),
@ -27,11 +28,7 @@ pub fn grouped_lines(app: &App, log_match: &LogMatch) -> Table<'static> {
Constraint::Length(10), Constraint::Length(10),
Constraint::Min(10), Constraint::Min(10),
]; ];
let table = Table::new(grouped.iter().map(|group| group_row(app, group)), widths) ScrollbarTable::new(grouped.iter().map(|group| group_row(app, group)), widths).header(header)
.header(header)
.highlight_style(TABLE_SELECTED_STYLE)
.highlight_spacing(HighlightSpacing::Always);
table
} }
fn group_row(app: &App, group: &GroupedLines) -> Row<'static> { fn group_row(app: &App, group: &GroupedLines) -> Row<'static> {

View file

@ -1,9 +1,9 @@
use crate::app::{App, LogMatch}; use crate::app::{App, LogMatch};
use crate::copy_osc; use crate::copy_osc;
use crate::logline::{FullLogLine, LogLine}; use crate::logline::{FullLogLine, LogLine};
use crate::ui::table::ScrollbarTableState;
use derive_more::From; use derive_more::From;
use ratatui::widgets::{ScrollbarState, TableState}; use ratatui::widgets::TableState;
use table_state::TableStateExt;
#[derive(Clone, From)] #[derive(Clone, From)]
pub enum UiState<'a> { pub enum UiState<'a> {
@ -16,41 +16,38 @@ pub enum UiState<'a> {
#[derive(Clone)] #[derive(Clone)]
pub struct MatchListState { pub struct MatchListState {
pub table_state: TableState, pub table_state: ScrollbarTableState,
pub scroll_state: ScrollbarState,
} }
impl MatchListState { impl MatchListState {
fn selected(&self) -> usize { fn selected(&self) -> usize {
self.table_state.selected().unwrap() self.table_state.selected()
} }
} }
#[derive(Clone)] #[derive(Clone)]
pub struct MatchState<'a> { pub struct MatchState<'a> {
pub result: &'a LogMatch, pub result: &'a LogMatch,
pub table_state: TableState, pub table_state: ScrollbarTableState,
pub scroll_state: ScrollbarState,
pub previous: Box<UiState<'a>>, pub previous: Box<UiState<'a>>,
} }
impl<'a> MatchState<'a> { impl<'a> MatchState<'a> {
fn selected(&self) -> usize { fn selected(&self) -> usize {
self.table_state.selected().unwrap() self.table_state.selected()
} }
} }
#[derive(Clone)] #[derive(Clone)]
pub struct LogsState<'a> { pub struct LogsState<'a> {
pub lines: &'a [usize], pub lines: &'a [usize],
pub table_state: TableState, pub table_state: ScrollbarTableState,
pub scroll_state: ScrollbarState,
pub previous: Box<UiState<'a>>, pub previous: Box<UiState<'a>>,
} }
impl<'a> LogsState<'a> { impl<'a> LogsState<'a> {
fn selected(&self) -> usize { fn selected(&self) -> usize {
self.table_state.selected().unwrap() self.table_state.selected()
} }
} }
@ -59,7 +56,7 @@ pub struct LogState<'a> {
pub trace_len: usize, pub trace_len: usize,
pub log: &'a LogLine, pub log: &'a LogLine,
pub full_line: FullLogLine, pub full_line: FullLogLine,
pub table_state: TableState, pub table_state: ScrollbarTableState,
pub previous: Box<UiState<'a>>, pub previous: Box<UiState<'a>>,
} }
@ -68,8 +65,7 @@ impl<'a> UiState<'a> {
let mut table_state = TableState::default(); let mut table_state = TableState::default();
table_state.select(Some(0)); table_state.select(Some(0));
UiState::MatchList(MatchListState { UiState::MatchList(MatchListState {
table_state, table_state: ScrollbarTableState::new(app.match_lines()),
scroll_state: ScrollbarState::new(app.match_lines()),
}) })
} }
@ -82,7 +78,7 @@ impl<'a> UiState<'a> {
} }
} }
fn table_state(&mut self) -> Option<&mut TableState> { fn table_state(&mut self) -> Option<&mut ScrollbarTableState> {
match self { match self {
UiState::MatchList(state) => Some(&mut state.table_state), UiState::MatchList(state) => Some(&mut state.table_state),
UiState::Match(state) => Some(&mut state.table_state), UiState::Match(state) => Some(&mut state.table_state),
@ -92,54 +88,25 @@ impl<'a> UiState<'a> {
} }
} }
fn scroll_state(&mut self) -> Option<&mut ScrollbarState> {
match self {
UiState::MatchList(state) => Some(&mut state.scroll_state),
UiState::Match(state) => Some(&mut state.scroll_state),
UiState::Logs(state) => Some(&mut state.scroll_state),
_ => None,
}
}
fn row_count(&self, app: &App) -> usize {
match self {
UiState::MatchList(_) => app.match_lines(),
UiState::Match(state) => state.result.grouped.len(),
UiState::Logs(state) => state.lines.len(),
UiState::Log(state) => state.trace_len,
_ => 0,
}
}
pub fn process(self, event: UiEvent, app: &'a App) -> (bool, UiState) { pub fn process(self, event: UiEvent, app: &'a App) -> (bool, UiState) {
match (self, event) { match (self, event) {
(UiState::Quit, _) => (true, UiState::Quit), (UiState::Quit, _) => (true, UiState::Quit),
(_, UiEvent::Quit) => (true, UiState::Quit), (_, UiEvent::Quit) => (true, UiState::Quit),
(UiState::MatchList(_), UiEvent::Back) => (true, UiState::Quit), (UiState::MatchList(_), UiEvent::Back) => (true, UiState::Quit),
(mut state, UiEvent::Down(step)) => { (mut state, UiEvent::Down(step)) => {
let count = state.row_count(app);
if let Some(table_state) = state.table_state() { if let Some(table_state) = state.table_state() {
let pos = table_state.down(count, step); table_state.down(step);
if let Some(scroll_state) = state.scroll_state() {
*scroll_state = scroll_state.position(pos);
}
} }
(true, state) (true, state)
} }
(mut state, UiEvent::Up(step)) => { (mut state, UiEvent::Up(step)) => {
let count = state.row_count(app);
if let Some(table_state) = state.table_state() { if let Some(table_state) = state.table_state() {
let pos = table_state.up(count, step); table_state.up(step);
if let Some(scroll_state) = state.scroll_state() {
*scroll_state = scroll_state.position(pos);
}
} }
(true, state) (true, state)
} }
(UiState::MatchList(state), UiEvent::Select) => { (UiState::MatchList(state), UiEvent::Select) => {
let selected = state.selected(); let selected = state.selected();
let mut table_state = TableState::default();
table_state.select(Some(0));
let result = if selected == 0 { let result = if selected == 0 {
&app.all &app.all
@ -148,12 +115,12 @@ impl<'a> UiState<'a> {
} else { } else {
&app.matches[selected - 1] &app.matches[selected - 1]
}; };
let table_state = ScrollbarTableState::new(result.grouped.len());
( (
true, true,
UiState::Match(MatchState { UiState::Match(MatchState {
result, result,
table_state, table_state,
scroll_state: ScrollbarState::new(result.count()),
previous: Box::new(state.into()), previous: Box::new(state.into()),
}), }),
) )
@ -164,20 +131,18 @@ impl<'a> UiState<'a> {
table_state.select(Some(0)); table_state.select(Some(0));
let lines = state.result.grouped[selected].lines.as_slice(); let lines = state.result.grouped[selected].lines.as_slice();
let table_state = ScrollbarTableState::new(lines.len());
( (
true, true,
UiState::Logs(LogsState { UiState::Logs(LogsState {
lines, lines,
table_state, table_state,
scroll_state: ScrollbarState::new(lines.len()),
previous: Box::new(state.into()), previous: Box::new(state.into()),
}), }),
) )
} }
(UiState::Logs(state), UiEvent::Select) => { (UiState::Logs(state), UiEvent::Select) => {
let selected = state.selected(); let selected = state.selected();
let mut table_state = TableState::default();
table_state.select(Some(0));
let line = state.lines[selected]; let line = state.lines[selected];
let log = &app.lines[line]; let log = &app.lines[line];
@ -188,6 +153,8 @@ impl<'a> UiState<'a> {
} else { } else {
0 0
}; };
let table_state = ScrollbarTableState::new(trace_len);
( (
true, true,
UiState::Log(LogState { UiState::Log(LogState {
@ -241,44 +208,3 @@ pub enum UiPage {
Logs, Logs,
Log, Log,
} }
mod table_state {
use ratatui::widgets::TableState;
pub trait TableStateExt {
fn up(&mut self, count: usize, step: usize) -> usize;
fn down(&mut self, count: usize, step: usize) -> usize;
}
impl TableStateExt for TableState {
fn up(&mut self, count: usize, step: usize) -> usize {
let current = self.selected().unwrap_or(0);
let after = if step > current {
if step == 1 {
count - 1
} else {
0
}
} else {
current - step
};
self.select(Some(after));
after
}
fn down(&mut self, count: usize, step: usize) -> usize {
let current = self.selected().unwrap_or(0);
let after = if step >= count - current {
if step == 1 {
0
} else {
count - 1
}
} else {
current + step
};
self.select(Some(after));
after
}
}
}

111
src/ui/table.rs Normal file
View file

@ -0,0 +1,111 @@
use crate::ui::style::TABLE_SELECTED_STYLE;
use ratatui::buffer::Buffer;
use ratatui::layout::{Constraint, Margin, Rect};
use ratatui::widgets::{
Block, Borders, HighlightSpacing, Row, Scrollbar, ScrollbarOrientation, ScrollbarState,
StatefulWidget, Table, TableState,
};
pub struct ScrollbarTable<'a> {
table: Table<'a>,
scrollbar: Scrollbar<'a>,
}
impl<'a> ScrollbarTable<'a> {
pub fn new<R, C>(rows: R, widths: C) -> Self
where
R: IntoIterator,
R::Item: Into<Row<'a>>,
C: IntoIterator,
C::Item: Into<Constraint>,
{
let rows: Vec<_> = rows.into_iter().collect();
ScrollbarTable {
table: Table::new(rows, widths)
.block(Block::new().borders(Borders::RIGHT))
.highlight_style(TABLE_SELECTED_STYLE)
.highlight_spacing(HighlightSpacing::Always),
scrollbar: Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some(""))
.end_symbol(Some("")),
}
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn header(mut self, header: Row<'a>) -> Self {
self.table = self.table.header(header);
self
}
}
#[derive(Clone)]
pub struct ScrollbarTableState {
count: usize,
table: TableState,
scrollbar: ScrollbarState,
}
impl ScrollbarTableState {
pub fn new(count: usize) -> Self {
let mut table = TableState::new();
table.select(Some(0));
ScrollbarTableState {
count,
table,
scrollbar: ScrollbarState::new(count),
}
}
pub fn selected(&self) -> usize {
self.table.selected().unwrap()
}
pub fn up(&mut self, step: usize) -> usize {
let current = self.table.selected().unwrap_or(0);
let after = if step > current {
if step == 1 {
self.count - 1
} else {
0
}
} else {
current - step
};
self.table.select(Some(after));
self.scrollbar = self.scrollbar.position(after);
after
}
pub fn down(&mut self, step: usize) -> usize {
let current = self.table.selected().unwrap_or(0);
let after = if step >= self.count - current {
if step == 1 {
0
} else {
self.count - 1
}
} else {
current + step
};
self.table.select(Some(after));
self.scrollbar = self.scrollbar.position(after);
after
}
}
impl<'a> StatefulWidget for ScrollbarTable<'a> {
type State = ScrollbarTableState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
StatefulWidget::render(self.table, area, buf, &mut state.table);
StatefulWidget::render(
self.scrollbar,
area.inner(Margin {
vertical: 1,
horizontal: 0,
}),
buf,
&mut state.scrollbar,
);
}
}