Skip to content
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ max_expand_height = 6

## Screenshots

<img src="./img/table-list-list.png" width=400> <img src="./img/table-list-detail-json.png" width=400> <img src="./img/table-list-filtering.png" width=400> <img src="./img/table.png" width=400> <img src="./img/table-expand-attr.png" width=400> <img src="./img/item-kv.png" width=400> <img src="./img/item-plain-json.png" width=400> <img src="./img/item-raw-json.png" width=400> <img src="./img/table-insight.png" width=400>
<img src="./img/table-list-list.png" width=400> <img src="./img/table-list-detail-json.png" width=400> <img src="./img/table-list-filtering.png" width=400> <img src="./img/table.png" width=400> <img src="./img/table-filtering.png" width=400> <img src="./img/table-expand-attr.png" width=400> <img src="./img/item-kv.png" width=400> <img src="./img/item-plain-json.png" width=400> <img src="./img/item-raw-json.png" width=400> <img src="./img/table-insight.png" width=400>

## Related projects

Expand Down
Binary file modified img/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions img/demo.tape
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ Type@500ms "jjj"
Sleep 500ms
Type "h"
Sleep 1s

Type "/"
Sleep 500ms
Type@200ms "critical"
Screenshot "./img/table-filtering.png"
Sleep 500ms
Enter
Sleep 1s

Enter

Sleep 2s
Expand Down
Binary file modified img/item-kv.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified img/item-plain-json.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified img/item-raw-json.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/table-filtering.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified img/table-list-detail-json.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified img/table-list-detail-kv.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
180 changes: 150 additions & 30 deletions src/view/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ use ratatui::{
layout::{Margin, Rect},
style::Stylize,
symbols::border,
text::Line,
text::{Line, Span},
widgets::{Block, Cell, Clear},
Frame,
};
use tui_input::{backend::crossterm::EventHandler, Input};

use crate::{
color::ColorTheme,
Expand All @@ -16,13 +17,13 @@ use crate::{
TableDescription, TableInsight,
},
event::{AppEvent, Sender, UserEvent, UserEventMapper},
handle_user_events,
handle_user_events, handle_user_events_with_default,
help::{
build_help_spans, build_short_help_spans, BuildHelpsItem, BuildShortHelpsItem, Spans,
SpansWithPriority,
},
view::common::{attribute_to_spans, cut_spans_by_width, to_highlighted_lines},
widget::{ScrollLines, ScrollLinesOptions, ScrollLinesState, Table, TableState},
widget::{CellItem, ScrollLines, ScrollLinesOptions, ScrollLinesState, Table, TableState},
};

const ELLIPSIS: &str = "...";
Expand All @@ -36,11 +37,21 @@ pub struct TableView {
tx: Sender,

helps: TableViewHelps,
row_cells: Vec<Vec<Cell<'static>>>,
row_cell_items: Vec<Vec<CellItem<'static>>>,
header_row_cells: Vec<Cell<'static>>,
table_state: TableState,
attr_expanded: bool,
attr_scroll_lines_state: ScrollLinesState,

filter_state: FilterState,
filter_input: Input,
view_indices: Vec<usize>,
}

enum FilterState {
None,
Filtering,
Filtered,
}

struct TableViewHelps {
Expand All @@ -59,11 +70,12 @@ impl TableView {
theme: ColorTheme,
tx: Sender,
) -> Self {
let (table_state, row_cells, header_row_cells) =
let (table_state, row_cell_items, header_row_cells) =
new_table_state(&table_description, &items, &config, theme);
let helps = TableViewHelps::new(mapper, theme);
let attr_scroll_lines_state =
ScrollLinesState::new(vec![], ScrollLinesOptions::new(false, false));
let view_indices = (0..items.len()).collect();

TableView {
table_description,
Expand All @@ -74,17 +86,35 @@ impl TableView {
tx,

helps,
row_cells,
row_cell_items,
header_row_cells,
table_state,
attr_expanded: false,
attr_scroll_lines_state,
filter_state: FilterState::None,
filter_input: Input::default(),
view_indices,
}
}
}

impl TableView {
pub fn handle_user_key_event(&mut self, user_events: Vec<UserEvent>, _key_event: KeyEvent) {
pub fn handle_user_key_event(&mut self, user_events: Vec<UserEvent>, key_event: KeyEvent) {
if let FilterState::Filtering = self.filter_state {
handle_user_events_with_default! { user_events =>
UserEvent::Confirm => {
self.apply_filter();
}
UserEvent::Reset => {
self.reset_filter();
}
=> {
self.update_filter(key_event);
}
}
return;
}

if self.attr_expanded {
handle_user_events! { user_events =>
UserEvent::Close | UserEvent::Expand => {
Expand Down Expand Up @@ -175,6 +205,12 @@ impl TableView {
self.table_state.select_prev_col();
self.table_state.update_table_state();
}
UserEvent::QuickFilter => {
self.start_filtering();
}
UserEvent::Reset => {
self.reset_filter();
}
UserEvent::Confirm => {
self.open_item();
}
Expand Down Expand Up @@ -216,7 +252,14 @@ impl TableView {
f.render_widget(block, area);

let table_area = area.inner(Margin::new(2, 1));
let table = Table::new(&self.row_cells, &self.header_row_cells).theme(&self.theme);
let filtered_row_cell_items: Vec<&Vec<CellItem<'static>>> = self
.view_indices
.iter()
.map(|&i| &self.row_cell_items[i])
.collect();
let query = self.filter_input.value();
let table =
Table::new(&filtered_row_cell_items, &self.header_row_cells, query).theme(&self.theme);
f.render_stateful_widget(table, table_area, &mut self.table_state);

if self.attr_expanded {
Expand Down Expand Up @@ -401,15 +444,15 @@ impl TableView {
let attribute_keys =
list_attribute_keys(&self.items, &self.table_description.key_schema_type);
let max_attribute_width = self.table_state.selected_col_width().unwrap();
for (i, cells) in self.row_cells.iter_mut().enumerate() {
for (i, cell_items) in self.row_cell_items.iter_mut().enumerate() {
let item = &self.items[i];
let key = &attribute_keys[col];
let (cell, _) = item
let (cell_item, _) = item
.attributes
.get(key)
.map(|attr| attribute_to_cell(attr, max_attribute_width, &self.theme))
.unwrap_or(undefined_cell(&self.theme));
cells[col] = cell;
.map(|attr| attribute_to_cell_item(attr, max_attribute_width, &self.theme))
.unwrap_or(undefined_cell_item(&self.theme));
cell_items[col] = cell_item;
}
}
}
Expand All @@ -423,6 +466,77 @@ impl TableView {
self.tx.send(AppEvent::LoadTableItems(desc));
}

fn start_filtering(&mut self) {
match self.filter_state {
FilterState::None | FilterState::Filtered => {
self.filter_input.reset();
self.filter_state = FilterState::Filtering;
self.update_status_input();
}
FilterState::Filtering => {}
}
}

fn update_filter(&mut self, key_event: KeyEvent) {
let event = &ratatui::crossterm::event::Event::Key(key_event);
self.filter_input.handle_event(event);
self.filter_view_indices();
self.update_status_input();
}

fn update_status_input(&mut self) {
let query = format!("/{}", self.filter_input.value());
let cursor_pos = self.filter_input.cursor() as u16 + 1; // "/"
self.tx
.send(AppEvent::UpdateStatusInput(query, Some(cursor_pos)));
}

fn apply_filter(&mut self) {
if self.filter_input.value().is_empty() {
self.filter_state = FilterState::None;
} else {
self.filter_state = FilterState::Filtered;
}
if self.view_indices.is_empty() {
self.reset_filter();
return;
}
self.filter_view_indices();
self.tx.send(AppEvent::ClearStatus);
}

fn reset_filter(&mut self) {
match self.filter_state {
FilterState::Filtering | FilterState::Filtered => {
self.filter_input.reset();
self.filter_state = FilterState::None;
// let orig_idx = self.view_indices[self.list_state.selected];
self.filter_view_indices();
// self.list_state.select_index(orig_idx);
self.tx.send(AppEvent::ClearStatus);
}
FilterState::None => {}
}
}

fn filter_view_indices(&mut self) {
let query = self.filter_input.value();
self.view_indices = self
.row_cell_items
.iter()
.enumerate()
.filter(|(_, cell_items)| {
cell_items
.iter()
.any(|cell_item| cell_item.matched_index(query).is_some())
})
.map(|(i, _)| i)
.collect();
self.table_state = self
.table_state
.with_new_total_rows(self.view_indices.len());
}

fn copy_to_clipboard(&self) {
let selected_item = &self.items[self.table_state.selected_row];
let schema = &self.table_description.key_schema_type;
Expand Down Expand Up @@ -461,29 +575,29 @@ fn new_table_state(
items: &[Item],
config: &UiTableConfig,
theme: ColorTheme,
) -> (TableState, Vec<Vec<Cell<'static>>>, Vec<Cell<'static>>) {
) -> (TableState, Vec<Vec<CellItem<'static>>>, Vec<Cell<'static>>) {
let attribute_keys = list_attribute_keys(items, &table_description.key_schema_type);
let total_rows = items.len();
let total_cols = attribute_keys.len();

let mut max_width_vec: Vec<usize> = vec![0; total_cols];

let mut row_cells: Vec<Vec<Cell>> = Vec::with_capacity(total_rows);
let mut row_cell_items: Vec<Vec<CellItem>> = Vec::with_capacity(total_rows);
for item in items {
let mut cells: Vec<Cell> = Vec::new();
let mut cell_items: Vec<CellItem> = Vec::new();
for (i, key) in attribute_keys.iter().enumerate() {
let (cell, width) = item
let (cell_item, width) = item
.attributes
.get(key)
.map(|attr| attribute_to_cell(attr, config.max_attribute_width, &theme))
.unwrap_or(undefined_cell(&theme));
cells.push(cell);
.map(|attr| attribute_to_cell_item(attr, config.max_attribute_width, &theme))
.unwrap_or(undefined_cell_item(&theme));
cell_items.push(cell_item);

if width > max_width_vec[i] {
max_width_vec[i] = width;
}
}
row_cells.push(cells);
row_cell_items.push(cell_items);
}

let mut header_row_cells: Vec<Cell> = Vec::with_capacity(total_cols);
Expand All @@ -497,19 +611,23 @@ fn new_table_state(

let table_state = TableState::new(total_rows, total_cols, max_width_vec);

(table_state, row_cells, header_row_cells)
(table_state, row_cell_items, header_row_cells)
}

fn attribute_to_cell(
fn attribute_to_cell_item(
attr: &Attribute,
max_attribute_width: usize,
theme: &ColorTheme,
) -> (Cell<'static>, usize) {
) -> (CellItem<'static>, usize) {
let spans = attribute_to_spans(attr, theme);
let spans = cut_spans_by_width(spans, max_attribute_width, ELLIPSIS, theme);
let line = Line::from(spans);
let width = line.width();
(Cell::new(line), width)
let plain = spans
.iter()
.map(|span| span.content.as_ref())
.collect::<String>();
let cut_spans = cut_spans_by_width(spans, max_attribute_width, ELLIPSIS, theme);
let width = cut_spans.iter().map(Span::width).sum();
let plain_width = console::measure_text_width(&plain);
(CellItem::new(cut_spans, plain, plain_width), width)
}

fn key_to_cell(key: &str, config: &UiTableConfig, theme: &ColorTheme) -> (Cell<'static>, usize) {
Expand All @@ -520,8 +638,10 @@ fn key_to_cell(key: &str, config: &UiTableConfig, theme: &ColorTheme) -> (Cell<'
(Cell::new(line), width)
}

fn undefined_cell(theme: &ColorTheme) -> (Cell<'static>, usize) {
(Cell::new("-").fg(theme.cell_undefined_fg), 1)
fn undefined_cell_item(theme: &ColorTheme) -> (CellItem<'static>, usize) {
let s = "-";
let content = vec![s.fg(theme.cell_undefined_fg)];
(CellItem::new(content, s, 1), 1)
}

fn get_raw_json_string(item: &Item, schema: &KeySchemaType) -> String {
Expand Down
Loading