Select one specific Cell (#14)

* select a cell

* add tests for headers and rows

* fix redundant codes

* update README

* update gobang.gif
pull/19/head
Takayuki Maeda 3 years ago committed by GitHub
parent f221e817a3
commit 2f1edc8384
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -9,3 +9,9 @@ A cross-platform terminal database tool written in Rust
![gobang](./resources/gobang.gif)
</div>
## Features
- Cross-platform support (macOS, Windows, Linux)
- Multiple Database support (MySQL PostgreSQL, SQLite)
- Intuitive keyboard only control

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 MiB

After

Width:  |  Height:  |  Size: 4.5 MiB

@ -6,7 +6,7 @@ use tui::{
backend::Backend,
layout::{Constraint, Rect},
style::{Color, Style},
widgets::{Block, Borders, Cell, Row, Table as WTable, TableState},
widgets::{Block, Borders, Cell, Row, Table, TableState},
Frame,
};
@ -15,7 +15,9 @@ pub struct TableComponent {
pub headers: Vec<String>,
pub rows: Vec<Vec<String>>,
pub column_index: usize,
pub column_page: usize,
pub scroll: VerticalScroll,
pub select_entire_row: bool,
}
impl Default for TableComponent {
@ -24,13 +26,26 @@ impl Default for TableComponent {
state: TableState::default(),
headers: vec![],
rows: vec![],
column_page: 0,
column_index: 0,
scroll: VerticalScroll::new(),
select_entire_row: false,
}
}
}
impl TableComponent {
pub fn reset(&mut self, headers: Vec<String>, rows: Vec<Vec<String>>) {
self.headers = headers;
self.rows = rows;
self.column_page = 0;
self.column_index = 1;
self.state.select(None);
if !self.rows.is_empty() {
self.state.select(Some(0));
}
}
pub fn next(&mut self, lines: usize) {
let i = match self.state.selected() {
Some(i) => {
@ -42,17 +57,23 @@ impl TableComponent {
}
None => None,
};
self.select_entire_row = false;
self.state.select(i);
}
pub fn reset(&mut self, headers: Vec<String>, rows: Vec<Vec<String>>) {
self.headers = headers;
self.rows = rows;
self.column_index = 0;
self.state.select(None);
if !self.rows.is_empty() {
self.state.select(Some(0));
}
pub fn previous(&mut self, lines: usize) {
let i = match self.state.selected() {
Some(i) => {
if i <= lines {
Some(0)
} else {
Some(i - lines)
}
}
None => None,
};
self.select_entire_row = false;
self.state.select(i);
}
pub fn scroll_top(&mut self) {
@ -70,34 +91,47 @@ impl TableComponent {
self.state.select(Some(self.rows.len() - 1));
}
pub fn previous(&mut self, lines: usize) {
let i = match self.state.selected() {
Some(i) => {
if i <= lines {
Some(0)
} else {
Some(i - lines)
}
}
None => None,
};
self.state.select(i);
}
pub fn next_column(&mut self) {
if self.headers.len() > 9 && self.column_index < self.headers.len() - 9 {
self.column_index += 1
if self.rows.is_empty() {
return;
}
if self.column_index == self.headers.len() - 1 {
return;
}
if self.column_index == 9 {
self.next_column_page();
return;
}
self.select_entire_row = false;
self.column_index += 1;
}
pub fn previous_column(&mut self) {
if self.column_index > 0 {
self.column_index -= 1
if self.rows.is_empty() {
return;
}
if self.column_index == 1 {
self.previous_column_page();
return;
}
self.select_entire_row = false;
self.column_index -= 1;
}
pub fn next_column_page(&mut self) {
if self.headers.len() > 9 && self.column_page < self.headers.len() - 9 {
self.column_page += 1
}
}
pub fn previous_column_page(&mut self) {
if self.column_page > 0 {
self.column_page -= 1
}
}
pub fn headers(&self) -> Vec<String> {
let mut headers = self.headers[self.column_index..].to_vec();
let mut headers = self.headers[self.column_page..].to_vec();
headers.insert(0, "".to_string());
headers
}
@ -106,7 +140,7 @@ impl TableComponent {
let rows = self
.rows
.iter()
.map(|row| row[self.column_index..].to_vec())
.map(|row| row[self.column_page..].to_vec())
.collect::<Vec<Vec<String>>>();
let mut new_rows = match self.state.selected() {
Some(index) => {
@ -146,25 +180,42 @@ impl DrawableComponent for TableComponent {
.map(|h| Cell::from(h.to_string()).style(Style::default()));
let header = Row::new(header_cells).height(1).bottom_margin(1);
let rows = self.rows();
let rows = rows.iter().map(|item| {
let rows = rows.iter().enumerate().map(|(row_index, item)| {
let height = item
.iter()
.map(|content| content.chars().filter(|c| *c == '\n').count())
.max()
.unwrap_or(0)
+ 1;
let cells = item
.iter()
.map(|c| Cell::from(c.to_string()).style(Style::default()));
let cells = item.iter().enumerate().map(|(column_page, c)| {
Cell::from(c.to_string()).style(if column_page == self.column_index {
match self.state.selected() {
Some(selected_row) => {
if row_index == selected_row {
Style::default().bg(Color::Blue)
} else {
Style::default()
}
}
None => Style::default(),
}
} else {
Style::default()
})
});
Row::new(cells).height(height as u16).bottom_margin(1)
});
let widths = (0..10)
.map(|_| Constraint::Percentage(10))
.collect::<Vec<Constraint>>();
let t = WTable::new(rows)
let t = Table::new(rows)
.header(header)
.block(Block::default().borders(Borders::ALL).title("Records"))
.highlight_style(Style::default().bg(Color::Blue))
.highlight_style(if self.select_entire_row {
Style::default().bg(Color::Blue)
} else {
Style::default()
})
.style(if focused {
Style::default()
} else {
@ -187,6 +238,7 @@ impl Component for TableComponent {
Key::Char('k') => self.previous(1),
Key::Ctrl('u') => self.previous(10),
Key::Char('g') => self.scroll_top(),
Key::Char('r') => self.select_entire_row = true,
Key::Shift('G') | Key::Shift('g') => self.scroll_bottom(),
Key::Char('l') => self.next_column(),
_ => (),
@ -194,3 +246,28 @@ impl Component for TableComponent {
Ok(())
}
}
#[cfg(test)]
mod test {
use super::TableComponent;
#[test]
fn test_headers() {
let mut component = TableComponent::default();
component.headers = vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect();
assert_eq!(component.headers(), vec!["", "a", "b", "c"])
}
#[test]
fn test_rows() {
let mut component = TableComponent::default();
component.rows = vec![
vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect(),
vec!["d", "e", "f"].iter().map(|h| h.to_string()).collect(),
];
assert_eq!(
component.rows(),
vec![vec!["1", "a", "b", "c"], vec!["2", "d", "e", "f"]],
)
}
}

@ -12,12 +12,15 @@ mod log;
use crate::app::App;
use crate::event::{Event, Key};
use crate::handlers::handle_app;
use anyhow::Result;
use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use std::{
io::{self, stdout},
panic,
};
use std::io::stdout;
use tui::{backend::CrosstermBackend, Terminal};
#[tokio::main]
@ -28,8 +31,9 @@ async fn main() -> anyhow::Result<()> {
let user_config = user_config::UserConfig::new("sample.toml").ok();
let mut stdout = stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let stdout = stdout();
setup_terminal()?;
set_panic_handlers()?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
@ -54,13 +58,36 @@ async fn main() -> anyhow::Result<()> {
}
}
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
shutdown_terminal();
terminal.show_cursor()?;
Ok(())
}
fn setup_terminal() -> Result<()> {
enable_raw_mode()?;
io::stdout().execute(EnterAlternateScreen)?;
Ok(())
}
fn set_panic_handlers() -> Result<()> {
panic::set_hook(Box::new(|e| {
eprintln!("panic: {:?}", e);
shutdown_terminal();
}));
Ok(())
}
fn shutdown_terminal() {
let leave_screen = io::stdout().execute(LeaveAlternateScreen).map(|_f| ());
if let Err(e) = leave_screen {
eprintln!("leave_screen failed:\n{}", e);
}
let leave_raw_mode = disable_raw_mode();
if let Err(e) = leave_raw_mode {
eprintln!("leave_raw_mode failed:\n{}", e);
}
}

Loading…
Cancel
Save