allow clicking rows to select them

This commit is contained in:
Robin Appelman 2024-09-16 10:46:04 +02:00
commit dfcecb9510
4 changed files with 184 additions and 72 deletions

View file

@ -61,6 +61,10 @@ impl LogMatch {
grouped,
}
}
pub fn row_count(&self) -> usize {
self.result.as_ref().map(|res| res.len()).unwrap_or(1)
}
}
impl LogMatch {

View file

@ -10,7 +10,10 @@ use crate::ui::single_match::grouped_lines;
use crate::ui::state::{
ErrorState, LogState, LogsState, MatchListState, MatchState, UiEvent, UiPage, UiState,
};
use ratatui::crossterm::event::{DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers, MouseEventKind};
use ratatui::crossterm::event::{
DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers, MouseButton,
MouseEventKind,
};
use ratatui::crossterm::terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
};
@ -47,7 +50,7 @@ pub fn run_ui(app: App) -> Result<(), UiError> {
terminal.draw(|frame| ui(frame, &app, &mut ui_state))?;
}
update = false;
if let Some(event) = handle_events(ui_state.page())? {
if let Some(event) = handle_events(ui_state.page(), &ui_state)? {
(update, ui_state) = ui_state.process(event, &app);
}
}
@ -58,7 +61,7 @@ pub fn run_ui(app: App) -> Result<(), UiError> {
Ok(())
}
fn handle_events(page: UiPage) -> io::Result<Option<UiEvent>> {
fn handle_events(page: UiPage, ui_state: &UiState) -> io::Result<Option<UiEvent>> {
if event::poll(Duration::from_millis(50))? {
match event::read()? {
Event::Key(key) if key.kind == event::KeyEventKind::Press => {
@ -85,15 +88,31 @@ fn handle_events(page: UiPage) -> io::Result<Option<UiEvent>> {
return Ok(match mouse.kind {
MouseEventKind::ScrollUp => Some(UiEvent::Up(1, false)),
MouseEventKind::ScrollDown => Some(UiEvent::Down(1, false)),
MouseEventKind::Down(MouseButton::Left) => {
find_hit_row(mouse.row, ui_state).map(UiEvent::Enter)
}
_ => None,
})
},
}
_ => {}
}
}
Ok(None)
}
fn find_hit_row(row: u16, ui_state: &UiState) -> Option<usize> {
if let Some(table_row) = row.checked_sub(ui_state.content_offset()) {
let selected = ui_state.index_for_row(table_row as usize);
if selected < ui_state.row_count() {
Some(selected)
} else {
None
}
} else {
None
}
}
const UI_HEADER_SIZE: u16 = 5;
fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
@ -109,7 +128,7 @@ fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
match state {
UiState::Quit => {}
UiState::MatchList(MatchListState { table_state }) => {
UiState::MatchList(MatchListState { table_state, .. }) => {
let selected = table_state.selected();
let histogram = if selected == 0 {
&app.all.histogram

View file

@ -1,13 +1,15 @@
use std::iter::once;
use crate::app::{App, LogMatch};
use crate::logline::{FullLogLine, LogLine};
use crate::ui::table::ScrollbarTableState;
use crate::ui::UI_HEADER_SIZE;
use crate::{copy_osc, parse_line_full};
use derive_more::From;
use ratatui::widgets::TableState;
#[derive(Clone, From, PartialEq)]
pub enum UiState<'a> {
MatchList(MatchListState),
MatchList(MatchListState<'a>),
Match(MatchState<'a>),
Logs(LogsState<'a>),
Log(LogState<'a>),
@ -16,17 +18,34 @@ pub enum UiState<'a> {
}
#[derive(Clone)]
pub struct MatchListState {
pub struct MatchListState<'a> {
app: &'a App<'a>,
pub table_state: ScrollbarTableState,
}
impl MatchListState {
impl<'a> MatchListState<'a> {
fn selected(&self) -> usize {
self.table_state.selected()
}
fn enter(self, selected: usize, app: &'a App) -> UiState<'a> {
let result = if selected == 0 {
&app.all
} else if selected == app.match_lines() - 1 {
&app.unmatched
} else {
&app.matches[selected - 1]
};
let table_state = ScrollbarTableState::new(result.grouped.len());
UiState::Match(MatchState {
result,
table_state,
previous: Box::new(self.into()),
})
}
}
impl PartialEq for MatchListState {
impl PartialEq for MatchListState<'_> {
fn eq(&self, _other: &Self) -> bool {
true
}
@ -43,6 +62,19 @@ impl<'a> MatchState<'a> {
fn selected(&self) -> usize {
self.table_state.selected()
}
fn enter(self, selected: usize) -> UiState<'a> {
let mut table_state = TableState::default();
table_state.select(Some(0));
let lines = self.result.grouped[selected].lines.as_slice();
let table_state = ScrollbarTableState::new(lines.len());
UiState::Logs(LogsState {
lines,
table_state,
previous: Box::new(self.into()),
})
}
}
impl PartialEq for MatchState<'_> {
@ -58,6 +90,33 @@ pub struct LogsState<'a> {
pub previous: Box<UiState<'a>>,
}
impl<'a> LogsState<'a> {
fn selected(&self) -> usize {
self.table_state.selected()
}
fn enter(self, selected: usize, app: &'a App<'a>) -> UiState<'a> {
let line = self.lines[selected];
let log = &app.lines[line];
let raw_line = app.get_line(log.index).unwrap();
let full_line = parse_line_full(raw_line).unwrap();
let trace_len = if let Some(exception) = &full_line.exception {
exception.stack().map(|e| 1 + e.trace.len()).sum()
} else {
0
};
let table_state = ScrollbarTableState::new(trace_len);
UiState::Log(LogState {
log,
full_line,
trace_len,
table_state,
previous: Box::new(self.into()),
})
}
}
impl PartialEq for LogsState<'_> {
fn eq(&self, other: &Self) -> bool {
self.lines == other.lines
@ -85,12 +144,6 @@ pub struct LogState<'a> {
pub previous: Box<UiState<'a>>,
}
impl<'a> LogsState<'a> {
fn selected(&self) -> usize {
self.table_state.selected()
}
}
impl PartialEq for LogState<'_> {
fn eq(&self, other: &Self) -> bool {
self.log.index == other.log.index
@ -98,10 +151,11 @@ impl PartialEq for LogState<'_> {
}
impl<'a> UiState<'a> {
pub fn new(app: &App) -> Self {
pub fn new(app: &'a App<'a>) -> Self {
let mut table_state = TableState::default();
table_state.select(Some(0));
UiState::MatchList(MatchListState {
app,
table_state: ScrollbarTableState::new(app.match_lines()),
})
}
@ -116,7 +170,18 @@ impl<'a> UiState<'a> {
}
}
fn table_state(&mut self) -> Option<&mut ScrollbarTableState> {
fn table_state(&self) -> Option<&ScrollbarTableState> {
match self {
UiState::MatchList(state) => Some(&state.table_state),
UiState::Match(state) => Some(&state.table_state),
UiState::Logs(state) => Some(&state.table_state),
UiState::Log(state) => Some(&state.table_state),
UiState::Errors(state) => Some(&state.table_state),
_ => None,
}
}
fn table_state_mut(&mut self) -> Option<&mut ScrollbarTableState> {
match self {
UiState::MatchList(state) => Some(&mut state.table_state),
UiState::Match(state) => Some(&mut state.table_state),
@ -127,42 +192,85 @@ impl<'a> UiState<'a> {
}
}
pub fn scroll_offset(&self) -> usize {
if let Some(table_state) = self.table_state() {
table_state.offset()
} else {
0
}
}
pub fn row_count(&self) -> usize {
if let Some(table_state) = self.table_state() {
table_state.row_count()
} else {
0
}
}
pub fn index_for_row(&self, row: usize) -> usize {
match self {
UiState::MatchList(MatchListState { app, .. }) => {
let mut total_height = 0;
let match_row_counts = app.matches.iter().map(|m| m.row_count());
for (index, row_count) in once(1).chain(match_row_counts).chain(once(1)).enumerate().skip(self.scroll_offset())
{
if total_height > row {
return index - 1;
}
total_height += row_count;
}
if total_height > row {
app.matches.len() + 1
} else {
app.matches.len() + 2
}
}
_ => row + self.scroll_offset(),
}
}
/// get the offset of the "main content" from the top of the screen
pub fn content_offset(&self) -> u16 {
match self {
UiState::MatchList(_) => UI_HEADER_SIZE + 1,
UiState::Match(_) => UI_HEADER_SIZE + 1,
UiState::Logs(_) => 0,
UiState::Log(_) => 0,
UiState::Errors(_) => 0,
UiState::Quit => 0,
}
}
pub fn process(self, event: UiEvent, app: &'a App<'a>) -> (bool, UiState) {
match (self, event) {
(UiState::Quit, _) => (true, UiState::Quit),
(_, UiEvent::Quit) => (true, UiState::Quit),
(UiState::MatchList(_), UiEvent::Back) => (true, UiState::Quit),
(mut state, UiEvent::Down(step, rollover)) => {
if let Some(table_state) = state.table_state() {
if let Some(table_state) = state.table_state_mut() {
table_state.down(step, rollover);
}
(true, state)
}
(mut state, UiEvent::Up(step, rollover)) => {
if let Some(table_state) = state.table_state() {
if let Some(table_state) = state.table_state_mut() {
table_state.up(step, rollover);
}
(true, state)
}
(mut state, UiEvent::SelectAt(selected)) => {
if let Some(table_state) = state.table_state_mut() {
table_state.select(selected);
}
(true, state)
}
(UiState::MatchList(state), UiEvent::Select) => {
let selected = state.selected();
let result = if selected == 0 {
&app.all
} else if selected == app.match_lines() - 1 {
&app.unmatched
} else {
&app.matches[selected - 1]
};
let table_state = ScrollbarTableState::new(result.grouped.len());
(
true,
UiState::Match(MatchState {
result,
table_state,
previous: Box::new(state.into()),
}),
)
(true, state.enter(selected, app))
}
(UiState::MatchList(state), UiEvent::Enter(selected)) => {
(true, state.enter(selected, app))
}
(UiState::MatchList(state), UiEvent::Errors) => {
let table_state = ScrollbarTableState::new(app.error_lines.len());
@ -176,45 +284,14 @@ impl<'a> UiState<'a> {
}
(UiState::Match(state), UiEvent::Select) => {
let selected = state.selected();
let mut table_state = TableState::default();
table_state.select(Some(0));
let lines = state.result.grouped[selected].lines.as_slice();
let table_state = ScrollbarTableState::new(lines.len());
(
true,
UiState::Logs(LogsState {
lines,
table_state,
previous: Box::new(state.into()),
}),
)
(true, state.enter(selected))
}
(UiState::Match(state), UiEvent::Enter(selected)) => (true, state.enter(selected)),
(UiState::Logs(state), UiEvent::Select) => {
let selected = state.selected();
let line = state.lines[selected];
let log = &app.lines[line];
let raw_line = app.get_line(log.index).unwrap();
let full_line = parse_line_full(raw_line).unwrap();
let trace_len = if let Some(exception) = &full_line.exception {
exception.stack().map(|e| 1 + e.trace.len()).sum()
} else {
0
};
let table_state = ScrollbarTableState::new(trace_len);
(
true,
UiState::Log(LogState {
log,
full_line,
trace_len,
table_state,
previous: Box::new(state.into()),
}),
)
(true, state.enter(selected, app))
}
(UiState::Logs(state), UiEvent::Enter(selected)) => (true, state.enter(selected, app)),
(UiState::Logs(state), UiEvent::Copy) => {
let selected = state.selected();
let mut table_state = TableState::default();
@ -258,6 +335,9 @@ pub enum UiEvent {
Down(usize, bool),
Errors,
Select,
#[allow(dead_code)]
SelectAt(usize),
Enter(usize),
Copy,
}

View file

@ -64,6 +64,10 @@ impl ScrollbarTableState {
self.table.offset()
}
pub fn row_count(&self) -> usize {
self.count
}
pub fn up(&mut self, step: usize, rollover: bool) -> usize {
let current = self.table.selected().unwrap_or(0);
let after = if step > current {
@ -95,6 +99,11 @@ impl ScrollbarTableState {
self.scrollbar = self.scrollbar.position(after);
after
}
pub fn select(&mut self, selected: usize) {
self.table.select(Some(selected));
self.scrollbar = self.scrollbar.position(selected);
}
}
impl<'a> StatefulWidget for ScrollbarTable<'a> {