diff --git a/database-tree/src/databasetreeitems.rs b/database-tree/src/databasetreeitems.rs index d0fedb9..feeed28 100644 --- a/database-tree/src/databasetreeitems.rs +++ b/database-tree/src/databasetreeitems.rs @@ -52,7 +52,7 @@ impl DatabaseTreeItems { Self::push_databases(e, &mut items, &mut items_added, collapsed)?; } for table in &e.tables { - items.push(DatabaseTreeItem::new_table(e, table)?); + items.push(DatabaseTreeItem::new_table(e, table)); } } @@ -84,7 +84,7 @@ impl DatabaseTreeItems { *items_added.entry(database.name.clone()).or_insert(0) += 1; let is_collapsed = collapsed.contains(&c); - nodes.push(DatabaseTreeItem::new_database(database, is_collapsed)?); + nodes.push(DatabaseTreeItem::new_database(database, is_collapsed)); } // increase child count in parent node (the above ancenstor ignores the leaf component) diff --git a/database-tree/src/item.rs b/database-tree/src/item.rs index edab342..aa8aa37 100644 --- a/database-tree/src/item.rs +++ b/database-tree/src/item.rs @@ -1,6 +1,4 @@ -use crate::error::Result; use crate::{Database, Table}; -use std::convert::TryFrom; #[derive(Debug, Clone)] pub struct TreeItemInfo { @@ -76,26 +74,24 @@ pub struct DatabaseTreeItem { } impl DatabaseTreeItem { - pub fn new_table(database: &Database, table: &Table) -> Result { - let indent = u8::try_from((3_usize).saturating_sub(2))?; - - Ok(Self { - info: TreeItemInfo::new(indent, false), + pub fn new_table(database: &Database, table: &Table) -> Self { + Self { + info: TreeItemInfo::new(1, false), kind: DatabaseTreeItemKind::Table { database: database.clone(), table: table.clone(), }, - }) + } } - pub fn new_database(database: &Database, _collapsed: bool) -> Result { - Ok(Self { + pub fn new_database(database: &Database, _collapsed: bool) -> Self { + Self { info: TreeItemInfo::new(0, true), kind: DatabaseTreeItemKind::Database { name: database.name.to_string(), collapsed: true, }, - }) + } } pub fn set_collapsed(&mut self, collapsed: bool) { diff --git a/resources/gobang.gif b/resources/gobang.gif index 66760a7..c7255a8 100644 Binary files a/resources/gobang.gif and b/resources/gobang.gif differ diff --git a/src/app.rs b/src/app.rs index 683dbeb..78d4bad 100644 --- a/src/app.rs +++ b/src/app.rs @@ -111,6 +111,9 @@ impl App { let mut res = vec![ CommandInfo::new(command::scroll(&self.config.key_config)), CommandInfo::new(command::scroll_to_top_bottom(&self.config.key_config)), + CommandInfo::new(command::scroll_up_down_multiple_lines( + &self.config.key_config, + )), CommandInfo::new(command::move_focus(&self.config.key_config)), CommandInfo::new(command::filter(&self.config.key_config)), CommandInfo::new(command::help(&self.config.key_config)), @@ -123,6 +126,81 @@ impl App { res } + async fn update_databases(&mut self) -> anyhow::Result<()> { + if let Some(conn) = self.connections.selected_connection() { + if let Some(pool) = self.pool.as_ref() { + pool.close().await; + } + self.pool = Some(Box::new( + MySqlPool::new(conn.database_url().as_str()).await?, + )); + let databases = match &conn.database { + Some(database) => vec![Database::new( + database.clone(), + self.pool + .as_ref() + .unwrap() + .get_tables(database.clone()) + .await?, + )], + None => self.pool.as_ref().unwrap().get_databases().await?, + }; + self.databases.update(databases.as_slice()).unwrap(); + self.focus = Focus::DabataseList; + self.record_table.reset(); + } + Ok(()) + } + + async fn update_table(&mut self) -> anyhow::Result<()> { + if let Some((database, table)) = self.databases.tree().selected_table() { + self.focus = Focus::Table; + let (headers, records) = self + .pool + .as_ref() + .unwrap() + .get_records(&database.name, &table.name, 0, None) + .await?; + self.record_table + .update(records, headers, database.clone(), table.clone()); + + let (headers, records) = self + .pool + .as_ref() + .unwrap() + .get_columns(&database.name, &table.name) + .await?; + self.structure_table + .update(records, headers, database.clone(), table.clone()); + self.table_status + .update(self.record_table.len() as u64, table); + } + Ok(()) + } + + async fn update_record_table(&mut self) -> anyhow::Result<()> { + if let Some((database, table)) = self.databases.tree().selected_table() { + let (headers, records) = self + .pool + .as_ref() + .unwrap() + .get_records( + &database.name, + &table.name, + 0, + if self.record_table.filter.input.is_empty() { + None + } else { + Some(self.record_table.filter.input_str()) + }, + ) + .await?; + self.record_table + .update(records, headers, database.clone(), table.clone()); + } + Ok(()) + } + pub async fn event(&mut self, key: Key) -> anyhow::Result { self.update_commands(); @@ -152,28 +230,7 @@ impl App { } if key == self.config.key_config.enter { - if let Some(conn) = self.connections.selected_connection() { - if let Some(pool) = self.pool.as_ref() { - pool.close().await; - } - self.pool = Some(Box::new( - MySqlPool::new(conn.database_url().as_str()).await?, - )); - let databases = match &conn.database { - Some(database) => vec![Database::new( - database.clone(), - self.pool - .as_ref() - .unwrap() - .get_tables(database.clone()) - .await?, - )], - None => self.pool.as_ref().unwrap().get_databases().await?, - }; - self.databases.update(databases.as_slice()).unwrap(); - self.focus = Focus::DabataseList; - self.record_table.reset(); - } + self.update_databases().await?; return Ok(EventState::Consumed); } } @@ -183,32 +240,7 @@ impl App { } if key == self.config.key_config.enter && self.databases.tree_focused() { - if let Some((database, table)) = self.databases.tree().selected_table() { - self.focus = Focus::Table; - let (headers, records) = self - .pool - .as_ref() - .unwrap() - .get_records(&database.name, &table.name, 0, None) - .await?; - self.record_table - .update(records, headers, database.clone(), table.clone()); - - let (headers, records) = self - .pool - .as_ref() - .unwrap() - .get_columns(&database.name, &table.name) - .await?; - self.structure_table.update( - records, - headers, - database.clone(), - table.clone(), - ); - self.table_status - .update(self.record_table.len() as u64, table); - } + self.update_table().await?; return Ok(EventState::Consumed); } } @@ -228,25 +260,7 @@ impl App { if key == self.config.key_config.enter && self.record_table.filter_focused() { self.record_table.focus = crate::components::record_table::Focus::Table; - if let Some((database, table)) = self.databases.tree().selected_table() - { - let (headers, records) = self - .pool - .as_ref() - .unwrap() - .get_records( - &database.name.clone(), - &table.name, - 0, - if self.record_table.filter.input.is_empty() { - None - } else { - Some(self.record_table.filter.input_str()) - }, - ) - .await?; - self.record_table.update(records, headers, database, table); - } + self.update_record_table().await?; } if self.record_table.table.eod { diff --git a/src/components/command.rs b/src/components/command.rs index c1a816d..c5adb28 100644 --- a/src/components/command.rs +++ b/src/components/command.rs @@ -35,10 +35,17 @@ pub fn scroll(key: &KeyConfig) -> CommandText { CommandText::new( format!( "Scroll up/down/left/right [{},{},{},{}]", - key.scroll_up.to_string(), - key.scroll_down.to_string(), - key.scroll_left.to_string(), - key.scroll_right.to_string() + key.scroll_up, key.scroll_down, key.scroll_left, key.scroll_right + ), + CMD_GROUP_GENERAL, + ) +} + +pub fn scroll_up_down_multiple_lines(key: &KeyConfig) -> CommandText { + CommandText::new( + format!( + "Scroll up/down multiple lines [{},{}]", + key.scroll_up_multiple_lines, key.scroll_down_multiple_lines, ), CMD_GROUP_GENERAL, ) @@ -48,8 +55,7 @@ pub fn scroll_to_top_bottom(key: &KeyConfig) -> CommandText { CommandText::new( format!( "Scroll to top/bottom [{},{}]", - key.scroll_to_top.to_string(), - key.scroll_to_bottom.to_string(), + key.scroll_to_top, key.scroll_to_bottom, ), CMD_GROUP_GENERAL, ) @@ -57,20 +63,13 @@ pub fn scroll_to_top_bottom(key: &KeyConfig) -> CommandText { pub fn expand_collapse(key: &KeyConfig) -> CommandText { CommandText::new( - format!( - "Expand/Collapse [{},{}]", - key.scroll_right.to_string(), - key.scroll_left.to_string(), - ), + format!("Expand/Collapse [{},{}]", key.scroll_right, key.scroll_left,), CMD_GROUP_DATABASES, ) } pub fn filter(key: &KeyConfig) -> CommandText { - CommandText::new( - format!("Filter [{}]", key.filter.to_string()), - CMD_GROUP_GENERAL, - ) + CommandText::new(format!("Filter [{}]", key.filter), CMD_GROUP_GENERAL) } pub fn move_focus(key: &KeyConfig) -> CommandText { diff --git a/src/components/databases.rs b/src/components/databases.rs index b764420..9e46e03 100644 --- a/src/components/databases.rs +++ b/src/components/databases.rs @@ -15,7 +15,7 @@ use tui::{ backend::Backend, layout::{Constraint, Direction, Layout, Rect}, style::{Color, Style}, - text::Span, + text::{Span, Spans}, widgets::{Block, Borders, Paragraph}, Frame, }; @@ -28,7 +28,7 @@ const FOLDER_ICON_EXPANDED: &str = "\u{25be}"; const EMPTY_STR: &str = ""; #[derive(PartialEq)] -pub enum FocusBlock { +pub enum Focus { Filter, Tree, } @@ -40,7 +40,7 @@ pub struct DatabasesComponent { input: Vec, input_idx: usize, input_cursor_position: u16, - focus_block: FocusBlock, + focus: Focus, key_config: KeyConfig, } @@ -53,7 +53,7 @@ impl DatabasesComponent { input: Vec::new(), input_idx: 0, input_cursor_position: 0, - focus_block: FocusBlock::Tree, + focus: Focus::Tree, key_config, } } @@ -67,18 +67,24 @@ impl DatabasesComponent { self.filterd_tree = None; self.input = Vec::new(); self.input_idx = 0; + self.input_cursor_position = 0; Ok(()) } pub fn tree_focused(&self) -> bool { - matches!(self.focus_block, FocusBlock::Tree) + matches!(self.focus, Focus::Tree) } pub fn tree(&self) -> &DatabaseTree { self.filterd_tree.as_ref().unwrap_or(&self.tree) } - fn tree_item_to_span(item: DatabaseTreeItem, selected: bool, width: u16) -> Span<'static> { + fn tree_item_to_span( + item: DatabaseTreeItem, + selected: bool, + width: u16, + filter: Option, + ) -> Spans<'static> { let name = item.kind().name(); let indent = item.info().indent(); @@ -88,8 +94,7 @@ impl DatabasesComponent { format!("{:w$}", " ", w = (indent as usize) * 2) }; - let is_database = item.kind().is_database(); - let path_arrow = if is_database { + let arrow = if item.kind().is_database() { if item.kind().is_database_collapsed() { FOLDER_ICON_COLLAPSED } else { @@ -99,21 +104,47 @@ impl DatabasesComponent { EMPTY_STR }; - let name = format!( - "{}{}{:w$}", - indent_str, - path_arrow, - name, - w = width as usize - ); - Span::styled( - name, + if let Some(filter) = filter { + if item.kind().is_table() && name.contains(&filter) { + let (first, rest) = &name.split_at(name.find(filter.as_str()).unwrap_or(0)); + let (middle, last) = &rest.split_at(filter.len().clamp(0, rest.len())); + return Spans::from(vec![ + Span::styled( + format!("{}{}{}", indent_str, arrow, first), + if selected { + Style::default().bg(Color::Blue) + } else { + Style::default() + }, + ), + Span::styled( + middle.to_string(), + if selected { + Style::default().bg(Color::Blue).fg(Color::Blue) + } else { + Style::default().fg(Color::Blue) + }, + ), + Span::styled( + format!("{:w$}", last.to_string(), w = width as usize), + if selected { + Style::default().bg(Color::Blue) + } else { + Style::default() + }, + ), + ]); + } + } + + Spans::from(Span::styled( + format!("{}{}{:w$}", indent_str, arrow, name, w = width as usize), if selected { Style::default().bg(Color::Blue) } else { Style::default() }, - ) + )) } fn draw_tree(&self, f: &mut Frame, area: Rect, focused: bool) { @@ -139,14 +170,14 @@ impl DatabasesComponent { let filter = Paragraph::new(Span::styled( format!( "{}{:w$}", - if self.input.is_empty() && matches!(self.focus_block, FocusBlock::Tree) { + if self.input.is_empty() && matches!(self.focus, Focus::Tree) { "Filter tables".to_string() } else { self.input_str() }, w = area.width as usize ), - if let FocusBlock::Filter = self.focus_block { + if let Focus::Filter = self.focus { Style::default() } else { Style::default().fg(Color::DarkGray) @@ -173,11 +204,22 @@ impl DatabasesComponent { let items = tree .iterate(self.scroll.get_top(), tree_height) - .map(|(item, selected)| Self::tree_item_to_span(item.clone(), selected, area.width)); + .map(|(item, selected)| { + Self::tree_item_to_span( + item.clone(), + selected, + area.width, + if self.input.is_empty() { + None + } else { + Some(self.input_str()) + }, + ) + }); draw_list_block(f, chunks[1], Block::default().borders(Borders::NONE), items); self.scroll.draw(f, area); - if let FocusBlock::Filter = self.focus_block { + if let Focus::Filter = self.focus { f.set_cursor(area.x + self.input_cursor_position + 1, area.y + 1) } } @@ -202,31 +244,19 @@ impl Component for DatabasesComponent { fn event(&mut self, key: Key) -> Result { let input_str: String = self.input.iter().collect(); - if tree_nav( - if let Some(tree) = self.filterd_tree.as_mut() { - tree - } else { - &mut self.tree - }, - key, - &self.key_config, - ) { - return Ok(EventState::Consumed); - } - if key == self.key_config.filter && self.focus_block == FocusBlock::Tree { - self.focus_block = FocusBlock::Filter; + if key == self.key_config.filter && self.focus == Focus::Tree { + self.focus = Focus::Filter; return Ok(EventState::Consumed); } match key { - Key::Char(c) if self.focus_block == FocusBlock::Filter => { + Key::Char(c) if self.focus == Focus::Filter => { self.input.insert(self.input_idx, c); self.input_idx += 1; self.input_cursor_position += compute_character_width(c); - self.filterd_tree = Some(self.tree.filter(self.input_str())); return Ok(EventState::Consumed); } - Key::Delete | Key::Backspace if matches!(self.focus_block, FocusBlock::Filter) => { + Key::Delete | Key::Backspace if matches!(self.focus, Focus::Filter) => { if input_str.width() > 0 { if !self.input.is_empty() && self.input_idx > 0 { let last_c = self.input.remove(self.input_idx - 1); @@ -242,7 +272,7 @@ impl Component for DatabasesComponent { return Ok(EventState::Consumed); } } - Key::Left if matches!(self.focus_block, FocusBlock::Filter) => { + Key::Left if matches!(self.focus, Focus::Filter) => { if !self.input.is_empty() && self.input_idx > 0 { self.input_idx -= 1; self.input_cursor_position = self @@ -258,7 +288,7 @@ impl Component for DatabasesComponent { } return Ok(EventState::Consumed); } - Key::Right if matches!(self.focus_block, FocusBlock::Filter) => { + Key::Right if matches!(self.focus, Focus::Filter) => { if self.input_idx < self.input.len() { let next_c = self.input[self.input_idx]; self.input_idx += 1; @@ -273,11 +303,23 @@ impl Component for DatabasesComponent { } return Ok(EventState::Consumed); } - Key::Enter if matches!(self.focus_block, FocusBlock::Filter) => { - self.focus_block = FocusBlock::Tree; + Key::Enter if matches!(self.focus, Focus::Filter) => { + self.focus = Focus::Tree; return Ok(EventState::Consumed); } - _ => (), + key => { + if tree_nav( + if let Some(tree) = self.filterd_tree.as_mut() { + tree + } else { + &mut self.tree + }, + key, + &self.key_config, + ) { + return Ok(EventState::Consumed); + } + } } Ok(EventState::NotConsumed) } @@ -290,3 +332,162 @@ fn tree_nav(tree: &mut DatabaseTree, key: Key, key_config: &KeyConfig) -> bool { false } } + +#[cfg(test)] +mod test { + use super::{Color, Database, DatabaseTreeItem, DatabasesComponent, Span, Spans, Style}; + use database_tree::Table; + + #[test] + fn test_tree_database_tree_item_to_span() { + const WIDTH: u16 = 10; + assert_eq!( + DatabasesComponent::tree_item_to_span( + DatabaseTreeItem::new_database( + &Database { + name: "foo".to_string(), + tables: Vec::new(), + }, + false, + ), + false, + WIDTH, + None, + ), + Spans::from(vec![Span::raw(format!( + "\u{25b8}{:w$}", + "foo", + w = WIDTH as usize + ))]) + ); + + assert_eq!( + DatabasesComponent::tree_item_to_span( + DatabaseTreeItem::new_database( + &Database { + name: "foo".to_string(), + tables: Vec::new(), + }, + false, + ), + true, + WIDTH, + None, + ), + Spans::from(vec![Span::styled( + format!("\u{25b8}{:w$}", "foo", w = WIDTH as usize), + Style::default().bg(Color::Blue) + )]) + ); + } + + #[test] + fn test_tree_table_tree_item_to_span() { + const WIDTH: u16 = 10; + assert_eq!( + DatabasesComponent::tree_item_to_span( + DatabaseTreeItem::new_table( + &Database { + name: "foo".to_string(), + tables: Vec::new(), + }, + &Table { + name: "bar".to_string(), + create_time: None, + update_time: None, + engine: None, + }, + ), + false, + WIDTH, + None, + ), + Spans::from(vec![Span::raw(format!( + " {:w$}", + "bar", + w = WIDTH as usize + ))]) + ); + + assert_eq!( + DatabasesComponent::tree_item_to_span( + DatabaseTreeItem::new_table( + &Database { + name: "foo".to_string(), + tables: Vec::new(), + }, + &Table { + name: "bar".to_string(), + create_time: None, + update_time: None, + engine: None, + }, + ), + true, + WIDTH, + None, + ), + Spans::from(Span::styled( + format!(" {:w$}", "bar", w = WIDTH as usize), + Style::default().bg(Color::Blue), + )) + ); + } + + #[test] + fn test_filterd_tree_item_to_span() { + const WIDTH: u16 = 10; + assert_eq!( + DatabasesComponent::tree_item_to_span( + DatabaseTreeItem::new_table( + &Database { + name: "foo".to_string(), + tables: Vec::new(), + }, + &Table { + name: "barbaz".to_string(), + create_time: None, + update_time: None, + engine: None, + }, + ), + false, + WIDTH, + Some("rb".to_string()), + ), + Spans::from(vec![ + Span::raw(format!(" {}", "ba")), + Span::styled("rb", Style::default().fg(Color::Blue)), + Span::raw(format!("{:w$}", "az", w = WIDTH as usize)) + ]) + ); + + assert_eq!( + DatabasesComponent::tree_item_to_span( + DatabaseTreeItem::new_table( + &Database { + name: "foo".to_string(), + tables: Vec::new(), + }, + &Table { + name: "barbaz".to_string(), + create_time: None, + update_time: None, + engine: None, + }, + ), + true, + WIDTH, + Some("rb".to_string()), + ), + Spans::from(vec![ + Span::styled(format!(" {}", "ba"), Style::default().bg(Color::Blue)), + Span::styled("rb", Style::default().bg(Color::Blue).fg(Color::Blue)), + Span::styled( + format!("{:w$}", "az", w = WIDTH as usize), + Style::default().bg(Color::Blue) + ) + ]) + ); + } +} diff --git a/src/ui/scrolllist.rs b/src/ui/scrolllist.rs index 2f990b5..45916af 100644 --- a/src/ui/scrolllist.rs +++ b/src/ui/scrolllist.rs @@ -4,26 +4,23 @@ use tui::{ buffer::Buffer, layout::Rect, style::Style, - text::Span, + text::Spans, widgets::{Block, List, ListItem, Widget}, Frame, }; -/// struct ScrollableList<'b, L> where - L: Iterator>, + L: Iterator>, { block: Option>, - /// Items to be displayed items: L, - /// Base style of the widget style: Style, } impl<'b, L> ScrollableList<'b, L> where - L: Iterator>, + L: Iterator>, { fn new(items: L) -> Self { Self { @@ -41,10 +38,9 @@ where impl<'b, L> Widget for ScrollableList<'b, L> where - L: Iterator>, + L: Iterator>, { fn render(self, area: Rect, buf: &mut Buffer) { - // Render items List::new(self.items.map(ListItem::new).collect::>()) .block(self.block.unwrap_or_default()) .style(self.style) @@ -54,7 +50,7 @@ where pub fn draw_list_block<'b, B: Backend, L>(f: &mut Frame, r: Rect, block: Block<'b>, items: L) where - L: Iterator>, + L: Iterator>, { let list = ScrollableList::new(items).block(block); f.render_widget(list, r);