diff --git a/Cargo.lock b/Cargo.lock index 7739dc3..3f1da96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,6 +38,17 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +[[package]] +name = "async-trait" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b98e84bbb4cbcdd97da190ba0c58a1bb0de2c1fdf67d159e192ed766aeca722" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atoi" version = "0.4.0" @@ -514,6 +525,7 @@ name = "gobang" version = "0.1.0-alpha.0" dependencies = [ "anyhow", + "async-trait", "chrono", "copypasta", "crossterm 0.19.0", diff --git a/Cargo.toml b/Cargo.toml index 14b6401..a938073 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ strum_macros = "0.21" database-tree = { path = "./database-tree", version = "0.1" } easy-cast = "0.4" copypasta = { version = "0.7.0", default-features = false } +async-trait = "0.1.50" [target.'cfg(any(target_os = "macos", windows))'.dependencies] copypasta = { version = "0.7.0", default-features = false } diff --git a/src/app.rs b/src/app.rs index 8a54a83..ff69fbd 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,14 +1,18 @@ use crate::clipboard::Clipboard; +use crate::components::Component as _; use crate::components::DrawableComponent as _; +use crate::components::EventState; +use crate::event::Key; +use crate::utils::{MySqlPool, Pool}; use crate::{ components::tab::Tab, components::{ - ConnectionsComponent, DatabasesComponent, ErrorComponent, QueryComponent, TabComponent, - TableComponent, TableStatusComponent, + ConnectionsComponent, DatabasesComponent, ErrorComponent, RecordTableComponent, + TabComponent, TableComponent, TableStatusComponent, }, user_config::UserConfig, }; -use sqlx::MySqlPool; +use database_tree::Database; use tui::{ backend::Backend, layout::{Constraint, Direction, Layout, Rect}, @@ -16,17 +20,15 @@ use tui::{ Frame, }; -pub enum FocusBlock { +pub enum Focus { DabataseList, Table, ConnectionList, - Query, } pub struct App { - pub query: QueryComponent, - pub record_table: TableComponent, + pub record_table: RecordTableComponent, pub structure_table: TableComponent, - pub focus_block: FocusBlock, + pub focus: Focus, pub tab: TabComponent, pub user_config: Option, pub selected_connection: ListState, @@ -34,17 +36,16 @@ pub struct App { pub connections: ConnectionsComponent, pub table_status: TableStatusComponent, pub clipboard: Clipboard, - pub pool: Option, + pub pool: Option>, pub error: ErrorComponent, } impl Default for App { fn default() -> App { App { - query: QueryComponent::default(), - record_table: TableComponent::default(), + record_table: RecordTableComponent::default(), structure_table: TableComponent::default(), - focus_block: FocusBlock::DabataseList, + focus: Focus::DabataseList, tab: TabComponent::default(), user_config: None, selected_connection: ListState::default(), @@ -63,13 +64,13 @@ impl App { App { user_config: Some(user_config.clone()), connections: ConnectionsComponent::new(user_config.conn), - focus_block: FocusBlock::ConnectionList, + focus: Focus::ConnectionList, ..App::default() } } pub fn draw(&mut self, f: &mut Frame<'_, B>) -> anyhow::Result<()> { - if let FocusBlock::ConnectionList = self.focus_block { + if let Focus::ConnectionList = self.focus { self.connections.draw( f, Layout::default() @@ -89,50 +90,233 @@ impl App { .split(main_chunks[0]); self.databases - .draw( - f, - left_chunks[0], - matches!(self.focus_block, FocusBlock::DabataseList), - ) + .draw(f, left_chunks[0], matches!(self.focus, Focus::DabataseList)) .unwrap(); - self.table_status.draw( - f, - left_chunks[1], - matches!(self.focus_block, FocusBlock::DabataseList), - )?; + self.table_status + .draw(f, left_chunks[1], matches!(self.focus, Focus::DabataseList))?; let right_chunks = Layout::default() .direction(Direction::Vertical) - .constraints( - [ - Constraint::Length(3), - Constraint::Length(3), - Constraint::Length(5), - ] - .as_ref(), - ) + .constraints([Constraint::Length(3), Constraint::Length(5)].as_ref()) .split(main_chunks[1]); self.tab.draw(f, right_chunks[0], false)?; - self.query.draw( - f, - right_chunks[1], - matches!(self.focus_block, FocusBlock::Query), - )?; match self.tab.selected_tab { - Tab::Records => self.record_table.draw( - f, - right_chunks[2], - matches!(self.focus_block, FocusBlock::Table), - )?, - Tab::Structure => self.structure_table.draw( - f, - right_chunks[2], - matches!(self.focus_block, FocusBlock::Table), - )?, + Tab::Records => { + self.record_table + .draw(f, right_chunks[1], matches!(self.focus, Focus::Table))? + } + Tab::Structure => { + self.structure_table + .draw(f, right_chunks[1], matches!(self.focus, Focus::Table))? + } } self.error.draw(f, Rect::default(), false)?; Ok(()) } + + pub async fn event(&mut self, key: Key) -> anyhow::Result { + if let Key::Esc = key { + if self.error.error.is_some() { + self.error.error = None; + return Ok(EventState::Consumed); + } + } + + if self.components_event(key).await?.is_consumed() { + return Ok(EventState::Consumed); + }; + + if self.move_focus(key)?.is_consumed() { + return Ok(EventState::Consumed); + }; + Ok(EventState::NotConsumed) + } + + pub async fn components_event(&mut self, key: Key) -> anyhow::Result { + match self.focus { + Focus::ConnectionList => { + if self.connections.event(key)?.is_consumed() { + return Ok(EventState::Consumed); + } + + if let Key::Enter = key { + self.record_table.reset(); + 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 + } + return Ok(EventState::Consumed); + } + } + Focus::DabataseList => { + if self.databases.event(key)?.is_consumed() { + return Ok(EventState::Consumed); + } + + if matches!(key, Key::Enter) && self.databases.tree_focused() { + if let Some((table, database)) = self.databases.tree().selected_table() { + self.focus = Focus::Table; + let (headers, records) = self + .pool + .as_ref() + .unwrap() + .get_records(&database, &table.name, 0, None) + .await?; + self.record_table = RecordTableComponent::new(records, headers); + self.record_table.set_table(table.name.to_string()); + + let (headers, records) = self + .pool + .as_ref() + .unwrap() + .get_columns(&database, &table.name) + .await?; + self.structure_table = TableComponent::new(records, headers); + + self.table_status + .update(self.record_table.len() as u64, table); + } + return Ok(EventState::Consumed); + } + } + Focus::Table => { + match self.tab.selected_tab { + Tab::Records => { + if self.record_table.event(key)?.is_consumed() { + return Ok(EventState::Consumed); + }; + + if let Key::Char('y') = key { + if let Some(text) = self.record_table.table.selected_cell() { + self.clipboard.store(text) + } + } + + if matches!(key, Key::Enter) && self.record_table.filter_focused() { + self.record_table.focus = crate::components::record_table::Focus::Table; + if let Some((table, database)) = self.databases.tree().selected_table() + { + let (headers, records) = self + .pool + .as_ref() + .unwrap() + .get_records( + &database.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); + } + } + + if self.record_table.table.eod { + return Ok(EventState::Consumed); + } + + if let Some(index) = self.record_table.table.state.selected() { + if index.saturating_add(1) + % crate::utils::RECORDS_LIMIT_PER_PAGE as usize + == 0 + { + if let Some((table, database)) = + self.databases.tree().selected_table() + { + let (_, records) = self + .pool + .as_ref() + .unwrap() + .get_records( + &database.clone(), + &table.name, + index as u16, + if self.record_table.filter.input.is_empty() { + None + } else { + Some(self.record_table.filter.input_str()) + }, + ) + .await?; + if !records.is_empty() { + self.record_table.table.rows.extend(records); + } else { + self.record_table.table.end() + } + } + } + }; + } + Tab::Structure => { + if self.structure_table.event(key)?.is_consumed() { + return Ok(EventState::Consumed); + }; + + if let Key::Char('y') = key { + if let Some(text) = self.structure_table.selected_cell() { + self.clipboard.store(text) + } + }; + } + }; + } + } + Ok(EventState::NotConsumed) + } + + pub fn move_focus(&mut self, key: Key) -> anyhow::Result { + if let Key::Char('c') = key { + self.focus = Focus::ConnectionList; + return Ok(EventState::Consumed); + } + if self.tab.event(key)?.is_consumed() { + return Ok(EventState::Consumed); + } + match self.focus { + Focus::ConnectionList => { + if let Key::Enter = key { + self.focus = Focus::DabataseList; + return Ok(EventState::Consumed); + } + } + Focus::DabataseList => match key { + Key::Right if self.databases.tree_focused() => { + self.focus = Focus::Table; + return Ok(EventState::Consumed); + } + _ => (), + }, + Focus::Table => match key { + Key::Left => { + self.focus = Focus::DabataseList; + return Ok(EventState::Consumed); + } + _ => (), + }, + } + Ok(EventState::NotConsumed) + } } diff --git a/src/components/connections.rs b/src/components/connections.rs index e066a38..e68fa71 100644 --- a/src/components/connections.rs +++ b/src/components/connections.rs @@ -1,10 +1,10 @@ -use super::{Component, DrawableComponent}; +use super::{Component, DrawableComponent, EventState}; use crate::event::Key; use crate::user_config::Connection; use anyhow::Result; use tui::{ backend::Backend, - layout::{Constraint, Direction, Layout, Rect}, + layout::Rect, style::{Color, Style}, text::{Span, Spans}, widgets::{Block, Borders, Clear, List, ListItem, ListState}, @@ -71,8 +71,8 @@ impl ConnectionsComponent { impl DrawableComponent for ConnectionsComponent { fn draw(&mut self, f: &mut Frame, _area: Rect, _focused: bool) -> Result<()> { - let percent_x = 60; - let percent_y = 50; + let width = 80; + let height = 20; let conns = &self.connections; let connections: Vec = conns .iter() @@ -85,29 +85,13 @@ impl DrawableComponent for ConnectionsComponent { .block(Block::default().borders(Borders::ALL).title("Connections")) .highlight_style(Style::default().bg(Color::Blue)) .style(Style::default()); - let popup_layout = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Percentage((100 - percent_y) / 2), - Constraint::Percentage(percent_y), - Constraint::Percentage((100 - percent_y) / 2), - ] - .as_ref(), - ) - .split(f.size()); - let area = Layout::default() - .direction(Direction::Horizontal) - .constraints( - [ - Constraint::Percentage((100 - percent_x) / 2), - Constraint::Percentage(percent_x), - Constraint::Percentage((100 - percent_x) / 2), - ] - .as_ref(), - ) - .split(popup_layout[1])[1]; + let area = Rect::new( + (f.size().width.saturating_sub(width)) / 2, + (f.size().height.saturating_sub(height)) / 2, + width.min(f.size().width), + height.min(f.size().height), + ); f.render_widget(Clear, area); f.render_stateful_widget(tasks, area, &mut self.state); Ok(()) @@ -115,12 +99,18 @@ impl DrawableComponent for ConnectionsComponent { } impl Component for ConnectionsComponent { - fn event(&mut self, key: Key) -> Result<()> { + fn event(&mut self, key: Key) -> Result { match key { - Key::Char('j') => self.next_connection(), - Key::Char('k') => self.previous_connection(), + Key::Char('j') => { + self.next_connection(); + return Ok(EventState::Consumed); + } + Key::Char('k') => { + self.previous_connection(); + return Ok(EventState::Consumed); + } _ => (), } - Ok(()) + Ok(EventState::NotConsumed) } } diff --git a/src/components/databases.rs b/src/components/databases.rs index 6065a4d..6c88cff 100644 --- a/src/components/databases.rs +++ b/src/components/databases.rs @@ -1,4 +1,8 @@ -use super::{utils::scroll_vertical::VerticalScroll, Component, DrawableComponent}; +use super::{ + compute_character_width, utils::scroll_vertical::VerticalScroll, Component, DrawableComponent, + EventState, +}; +use crate::components::RecordTableComponent; use crate::event::Key; use crate::ui::common_nav; use crate::ui::scrolllist::draw_list_block; @@ -32,8 +36,10 @@ pub struct DatabasesComponent { pub tree: DatabaseTree, pub filterd_tree: Option, pub scroll: VerticalScroll, - pub input: String, - pub input_cursor_x: u16, + pub input: Vec, + pub input_idx: usize, + pub input_cursor_position: u16, + pub record_table: RecordTableComponent, pub focus_block: FocusBlock, } @@ -43,17 +49,23 @@ impl DatabasesComponent { tree: DatabaseTree::default(), filterd_tree: None, scroll: VerticalScroll::new(), - input: String::new(), - input_cursor_x: 0, + input: Vec::new(), + input_idx: 0, + input_cursor_position: 0, + record_table: RecordTableComponent::default(), focus_block: FocusBlock::Tree, } } - pub fn update(&mut self, list: &[Database], collapsed: &BTreeSet<&String>) -> Result<()> { - self.tree = DatabaseTree::new(list, collapsed)?; + pub fn input_str(&self) -> String { + self.input.iter().collect() + } + + pub fn update(&mut self, list: &[Database]) -> Result<()> { + self.tree = DatabaseTree::new(list, &BTreeSet::new())?; self.filterd_tree = None; - self.input = String::new(); - self.input_cursor_x = 0; + self.input = Vec::new(); + self.input_idx = 0; Ok(()) } @@ -65,18 +77,6 @@ impl DatabasesComponent { self.filterd_tree.as_ref().unwrap_or(&self.tree) } - pub fn increment_input_cursor_x(&mut self) { - if self.input_cursor_x > 0 { - self.input_cursor_x -= 1; - } - } - - pub fn decrement_input_cursor_x(&mut self) { - if self.input_cursor_x < self.input.width() as u16 { - self.input_cursor_x += 1; - } - } - fn tree_item_to_span(item: DatabaseTreeItem, selected: bool, width: u16) -> Span<'static> { let name = item.kind().name(); let indent = item.info().indent(); @@ -156,9 +156,9 @@ impl DatabasesComponent { format!( "{}{:w$}", if self.input.is_empty() && matches!(self.focus_block, FocusBlock::Tree) { - " / to filter tables".to_string() + "Filter tables".to_string() } else { - self.input.clone() + self.input_str() }, w = area.width as usize ), @@ -187,10 +187,7 @@ impl DatabasesComponent { ); self.scroll.draw(f, area); if let FocusBlock::Filter = self.focus_block { - f.set_cursor( - area.x + self.input.width() as u16 + 1 - self.input_cursor_x, - area.y + 1, - ) + f.set_cursor(area.x + self.input_cursor_position + 1, area.y + 1) } } } @@ -208,54 +205,92 @@ impl DrawableComponent for DatabasesComponent { } impl Component for DatabasesComponent { - fn event(&mut self, key: Key) -> Result<()> { + 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, + ) { + return Ok(EventState::Consumed); + } match key { Key::Char('/') if matches!(self.focus_block, FocusBlock::Tree) => { - self.focus_block = FocusBlock::Filter + self.focus_block = FocusBlock::Filter; + return Ok(EventState::Consumed); } Key::Char(c) if matches!(self.focus_block, FocusBlock::Filter) => { - self.input.push(c); - self.filterd_tree = Some(self.tree.filter(self.input.clone())) + 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 !self.input.is_empty() { - if self.input_cursor_x == 0 { - self.input.pop(); - } else if self.input.width() - self.input_cursor_x as usize > 0 { - self.input.remove( - self.input - .width() - .saturating_sub(self.input_cursor_x as usize) - .saturating_sub(1), - ); + Key::Delete | Key::Backspace if matches!(self.focus_block, FocusBlock::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); + self.input_idx -= 1; + self.input_cursor_position -= compute_character_width(last_c); } + self.filterd_tree = if self.input.is_empty() { None } else { - Some(self.tree.filter(self.input.clone())) - } + Some(self.tree.filter(self.input_str())) + }; + return Ok(EventState::Consumed); + } + } + Key::Left if matches!(self.focus_block, FocusBlock::Filter) => { + if !self.input.is_empty() && self.input_idx > 0 { + self.input_idx -= 1; + self.input_cursor_position = self + .input_cursor_position + .saturating_sub(compute_character_width(self.input[self.input_idx])); + } + return Ok(EventState::Consumed); + } + Key::Ctrl('a') => { + if !self.input.is_empty() && self.input_idx > 0 { + self.input_idx = 0; + self.input_cursor_position = 0 + } + return Ok(EventState::Consumed); + } + Key::Right if matches!(self.focus_block, FocusBlock::Filter) => { + if self.input_idx < self.input.len() { + let next_c = self.input[self.input_idx]; + self.input_idx += 1; + self.input_cursor_position += compute_character_width(next_c); } + return Ok(EventState::Consumed); + } + Key::Ctrl('e') => { + if self.input_idx < self.input.len() { + self.input_idx = self.input.len(); + self.input_cursor_position = self.input_str().width() as u16; + } + return Ok(EventState::Consumed); } - Key::Left => self.decrement_input_cursor_x(), - Key::Right => self.increment_input_cursor_x(), Key::Enter if matches!(self.focus_block, FocusBlock::Filter) => { - self.focus_block = FocusBlock::Tree + self.focus_block = FocusBlock::Tree; + return Ok(EventState::Consumed); } - key => tree_nav( - if let Some(tree) = self.filterd_tree.as_mut() { - tree - } else { - &mut self.tree - }, - key, - ), + _ => (), } - Ok(()) + Ok(EventState::NotConsumed) } } -fn tree_nav(tree: &mut DatabaseTree, key: Key) { +fn tree_nav(tree: &mut DatabaseTree, key: Key) -> bool { if let Some(common_nav) = common_nav(key) { - tree.move_selection(common_nav); + tree.move_selection(common_nav) + } else { + false } } diff --git a/src/components/error.rs b/src/components/error.rs index 0230849..0eb0cb0 100644 --- a/src/components/error.rs +++ b/src/components/error.rs @@ -1,4 +1,4 @@ -use super::{Component, DrawableComponent}; +use super::{Component, DrawableComponent, EventState}; use crate::event::Key; use anyhow::Result; use tui::{ @@ -49,7 +49,7 @@ impl DrawableComponent for ErrorComponent { } impl Component for ErrorComponent { - fn event(&mut self, _key: Key) -> Result<()> { - Ok(()) + fn event(&mut self, _key: Key) -> Result { + Ok(EventState::NotConsumed) } } diff --git a/src/components/mod.rs b/src/components/mod.rs index 3d67b41..80f0012 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -2,9 +2,10 @@ pub mod command; pub mod connections; pub mod databases; pub mod error; -pub mod query; +pub mod record_table; pub mod tab; pub mod table; +pub mod table_filter; pub mod table_status; pub mod table_value; pub mod utils; @@ -13,14 +14,18 @@ pub use command::{CommandInfo, CommandText}; pub use connections::ConnectionsComponent; pub use databases::DatabasesComponent; pub use error::ErrorComponent; -pub use query::QueryComponent; +pub use record_table::RecordTableComponent; pub use tab::TabComponent; pub use table::TableComponent; +pub use table_filter::TableFilterComponent; pub use table_status::TableStatusComponent; pub use table_value::TableValueComponent; use anyhow::Result; +use async_trait::async_trait; +use std::convert::TryInto; use tui::{backend::Backend, layout::Rect, Frame}; +use unicode_width::UnicodeWidthChar; #[derive(Copy, Clone)] pub enum ScrollType { @@ -38,13 +43,36 @@ pub enum Direction { Down, } +#[derive(PartialEq)] +pub enum EventState { + Consumed, + NotConsumed, +} + +impl EventState { + pub fn is_consumed(&self) -> bool { + *self == Self::Consumed + } +} + +impl From for EventState { + fn from(consumed: bool) -> Self { + if consumed { + Self::Consumed + } else { + Self::NotConsumed + } + } +} + pub trait DrawableComponent { fn draw(&mut self, f: &mut Frame, rect: Rect, focused: bool) -> Result<()>; } /// base component trait +#[async_trait] pub trait Component { - fn event(&mut self, key: crate::event::Key) -> Result<()>; + fn event(&mut self, key: crate::event::Key) -> Result; fn focused(&self) -> bool { false @@ -71,3 +99,7 @@ pub trait Component { } } } + +fn compute_character_width(c: char) -> u16 { + UnicodeWidthChar::width(c).unwrap().try_into().unwrap() +} diff --git a/src/components/query.rs b/src/components/query.rs deleted file mode 100644 index edeb133..0000000 --- a/src/components/query.rs +++ /dev/null @@ -1,83 +0,0 @@ -use super::{Component, DrawableComponent}; -use crate::event::Key; -use anyhow::Result; -use tui::{ - backend::Backend, - layout::Rect, - style::{Color, Style}, - widgets::{Block, Borders, Paragraph}, - Frame, -}; -use unicode_width::UnicodeWidthStr; - -pub struct QueryComponent { - pub input: String, - pub input_cursor_x: u16, -} - -impl Default for QueryComponent { - fn default() -> Self { - Self { - input: String::new(), - input_cursor_x: 0, - } - } -} - -impl QueryComponent { - pub fn increment_input_cursor_x(&mut self) { - if self.input_cursor_x > 0 { - self.input_cursor_x -= 1; - } - } - - pub fn decrement_input_cursor_x(&mut self) { - if self.input_cursor_x < self.input.width() as u16 { - self.input_cursor_x += 1; - } - } -} - -impl DrawableComponent for QueryComponent { - fn draw(&mut self, f: &mut Frame, area: Rect, focused: bool) -> Result<()> { - let query = Paragraph::new(self.input.as_ref()) - .style(if focused { - Style::default() - } else { - Style::default().fg(Color::DarkGray) - }) - .block(Block::default().borders(Borders::ALL).title("Query")); - f.render_widget(query, area); - if focused { - f.set_cursor( - area.x + self.input.width() as u16 + 1 - self.input_cursor_x, - area.y + 1, - ) - } - Ok(()) - } -} - -impl Component for QueryComponent { - fn event(&mut self, key: Key) -> Result<()> { - match key { - Key::Char(c) => self.input.push(c), - Key::Delete | Key::Backspace => { - if self.input.width() > 0 { - if self.input_cursor_x == 0 { - self.input.pop(); - return Ok(()); - } - if self.input.width() - self.input_cursor_x as usize > 0 { - self.input - .remove(self.input.width() - self.input_cursor_x as usize); - } - } - } - Key::Left => self.decrement_input_cursor_x(), - Key::Right => self.increment_input_cursor_x(), - _ => (), - } - Ok(()) - } -} diff --git a/src/components/record_table.rs b/src/components/record_table.rs new file mode 100644 index 0000000..3e30304 --- /dev/null +++ b/src/components/record_table.rs @@ -0,0 +1,100 @@ +use super::{Component, DrawableComponent, EventState}; +use crate::components::{TableComponent, TableFilterComponent}; +use crate::event::Key; +use anyhow::Result; +use tui::{ + backend::Backend, + layout::{Constraint, Direction, Layout, Rect}, + Frame, +}; + +pub enum Focus { + Table, + Filter, +} + +pub struct RecordTableComponent { + pub filter: TableFilterComponent, + pub table: TableComponent, + pub focus: Focus, +} + +impl Default for RecordTableComponent { + fn default() -> Self { + Self { + filter: TableFilterComponent::default(), + table: TableComponent::default(), + focus: Focus::Table, + } + } +} + +impl RecordTableComponent { + pub fn new(rows: Vec>, headers: Vec) -> Self { + Self { + table: TableComponent::new(rows, headers), + ..Self::default() + } + } + + pub fn update(&mut self, rows: Vec>, headers: Vec) { + self.table.rows = rows; + self.table.headers = headers; + if !self.table.rows.is_empty() { + self.table.state.select(None); + self.table.state.select(Some(0)); + } + } + + pub fn reset(&mut self) { + self.table = TableComponent::default(); + if !self.table.rows.is_empty() { + self.table.state.select(None); + self.table.state.select(Some(0)) + } + self.filter = TableFilterComponent::default(); + } + + pub fn len(&self) -> usize { + self.table.rows.len() + } + + pub fn set_table(&mut self, table: String) { + self.filter.table = Some(table) + } + + pub fn filter_focused(&self) -> bool { + matches!(self.focus, Focus::Filter) + } +} + +impl DrawableComponent for RecordTableComponent { + fn draw(&mut self, f: &mut Frame, area: Rect, focused: bool) -> Result<()> { + let layout = Layout::default() + .direction(Direction::Vertical) + .constraints(vec![Constraint::Length(3), Constraint::Length(5)]) + .split(area); + + self.filter + .draw(f, layout[0], focused && matches!(self.focus, Focus::Filter))?; + + self.table + .draw(f, layout[1], focused && matches!(self.focus, Focus::Table))?; + Ok(()) + } +} + +impl Component for RecordTableComponent { + fn event(&mut self, key: Key) -> Result { + match key { + Key::Char('/') => { + self.focus = Focus::Filter; + return Ok(EventState::Consumed); + } + key if matches!(self.focus, Focus::Filter) => return Ok(self.filter.event(key)?), + key if matches!(self.focus, Focus::Table) => return Ok(self.table.event(key)?), + _ => (), + } + Ok(EventState::NotConsumed) + } +} diff --git a/src/components/tab.rs b/src/components/tab.rs index bcc5794..8f43b22 100644 --- a/src/components/tab.rs +++ b/src/components/tab.rs @@ -1,4 +1,4 @@ -use super::{Component, DrawableComponent}; +use super::{Component, DrawableComponent, EventState}; use crate::event::Key; use anyhow::Result; use strum::IntoEnumIterator; @@ -62,12 +62,17 @@ impl DrawableComponent for TabComponent { } impl Component for TabComponent { - fn event(&mut self, key: Key) -> Result<()> { + fn event(&mut self, key: Key) -> Result { match key { - Key::Char('1') => self.selected_tab = Tab::Records, - Key::Char('2') => self.selected_tab = Tab::Structure, - _ => (), + Key::Char('1') => { + self.selected_tab = Tab::Records; + Ok(EventState::Consumed) + } + Key::Char('2') => { + self.selected_tab = Tab::Structure; + Ok(EventState::Consumed) + } + _ => Ok(EventState::NotConsumed), } - Ok(()) } } diff --git a/src/components/table.rs b/src/components/table.rs index 4d03b38..28801f3 100644 --- a/src/components/table.rs +++ b/src/components/table.rs @@ -1,5 +1,6 @@ use super::{ - utils::scroll_vertical::VerticalScroll, Component, DrawableComponent, TableValueComponent, + utils::scroll_vertical::VerticalScroll, Component, DrawableComponent, EventState, + TableValueComponent, }; use crate::event::Key; use anyhow::Result; @@ -11,9 +12,6 @@ use tui::{ widgets::{Block, Borders, Cell, Row, Table, TableState}, Frame, }; -use unicode_width::UnicodeWidthStr; - -pub const RECORDS_LIMIT_PER_PAGE: u8 = 200; pub struct TableComponent { pub state: TableState, @@ -23,6 +21,7 @@ pub struct TableComponent { pub column_page: usize, pub scroll: VerticalScroll, pub select_entire_row: bool, + pub eod: bool, } impl Default for TableComponent { @@ -32,25 +31,33 @@ impl Default for TableComponent { headers: vec![], rows: vec![], column_page: 0, - column_index: 0, + column_index: 1, scroll: VerticalScroll::new(), select_entire_row: false, + eod: false, } } } impl TableComponent { - pub fn reset(&mut self, headers: Vec, rows: Vec>) { - 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 new(rows: Vec>, headers: Vec) -> Self { + let mut state = TableState::default(); + if !rows.is_empty() { + state.select(None); + state.select(Some(0)) + } + Self { + rows, + headers, + state, + ..Self::default() } } + pub fn end(&mut self) { + self.eod = true; + } + pub fn next(&mut self, lines: usize) { let i = match self.state.selected() { Some(i) => { @@ -239,20 +246,47 @@ impl DrawableComponent for TableComponent { } impl Component for TableComponent { - fn event(&mut self, key: Key) -> Result<()> { + fn event(&mut self, key: Key) -> Result { match key { - Key::Char('h') => self.previous_column(), - Key::Char('j') => self.next(1), - Key::Ctrl('d') => self.next(10), - 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(), + Key::Char('h') => { + self.previous_column(); + return Ok(EventState::Consumed); + } + Key::Char('j') => { + self.next(1); + return Ok(EventState::NotConsumed); + } + Key::Ctrl('d') => { + self.next(10); + return Ok(EventState::NotConsumed); + } + Key::Char('k') => { + self.previous(1); + return Ok(EventState::Consumed); + } + Key::Ctrl('u') => { + self.previous(10); + return Ok(EventState::Consumed); + } + Key::Char('g') => { + self.scroll_top(); + return Ok(EventState::Consumed); + } + Key::Char('r') => { + self.select_entire_row = true; + return Ok(EventState::Consumed); + } + Key::Char('G') => { + self.scroll_bottom(); + return Ok(EventState::Consumed); + } + Key::Char('l') => { + self.next_column(); + return Ok(EventState::Consumed); + } _ => (), } - Ok(()) + Ok(EventState::NotConsumed) } } diff --git a/src/components/table_filter.rs b/src/components/table_filter.rs new file mode 100644 index 0000000..8c878cb --- /dev/null +++ b/src/components/table_filter.rs @@ -0,0 +1,136 @@ +use super::{compute_character_width, Component, DrawableComponent, EventState}; +use crate::event::Key; +use anyhow::Result; +use tui::{ + backend::Backend, + layout::Rect, + style::{Color, Style}, + text::{Span, Spans}, + widgets::{Block, Borders, Paragraph}, + Frame, +}; +use unicode_width::UnicodeWidthStr; + +pub struct TableFilterComponent { + pub table: Option, + pub input: Vec, + pub input_idx: usize, + pub input_cursor_position: u16, +} + +impl Default for TableFilterComponent { + fn default() -> Self { + Self { + table: None, + input: Vec::new(), + input_idx: 0, + input_cursor_position: 0, + } + } +} + +impl TableFilterComponent { + pub fn input_str(&self) -> String { + self.input.iter().collect() + } +} + +impl DrawableComponent for TableFilterComponent { + fn draw(&mut self, f: &mut Frame, area: Rect, focused: bool) -> Result<()> { + let query = Paragraph::new(Spans::from(vec![ + Span::styled( + self.table + .as_ref() + .map_or("-".to_string(), |table| table.to_string()), + Style::default().fg(Color::Blue), + ), + Span::from(format!( + " {}", + if focused || !self.input.is_empty() { + self.input.iter().collect::() + } else { + "Enter a SQL expression in WHERE clause".to_string() + } + )), + ])) + .style(if focused { + Style::default() + } else { + Style::default().fg(Color::DarkGray) + }) + .block(Block::default().borders(Borders::ALL)); + f.render_widget(query, area); + if focused { + f.set_cursor( + (area.x + + (1 + self + .table + .as_ref() + .map_or(String::new(), |table| table.to_string()) + .width() + + 1) as u16) + .saturating_add(self.input_cursor_position), + area.y + 1, + ) + } + Ok(()) + } +} + +impl Component for TableFilterComponent { + fn event(&mut self, key: Key) -> Result { + let input_str: String = self.input.iter().collect(); + match key { + Key::Char(c) => { + self.input.insert(self.input_idx, c); + self.input_idx += 1; + self.input_cursor_position += compute_character_width(c); + + return Ok(EventState::Consumed); + } + Key::Delete | Key::Backspace => { + if input_str.width() > 0 { + if !self.input.is_empty() && self.input_idx > 0 { + let last_c = self.input.remove(self.input_idx - 1); + self.input_idx -= 1; + self.input_cursor_position -= compute_character_width(last_c); + } + } + return Ok(EventState::Consumed); + } + Key::Left => { + if !self.input.is_empty() && self.input_idx > 0 { + self.input_idx -= 1; + self.input_cursor_position = self + .input_cursor_position + .saturating_sub(compute_character_width(self.input[self.input_idx])); + } + return Ok(EventState::Consumed); + } + Key::Ctrl('a') => { + if !self.input.is_empty() && self.input_idx > 0 { + self.input_idx = 0; + self.input_cursor_position = 0 + } + return Ok(EventState::Consumed); + } + Key::Right => { + if self.input_idx < self.input.len() { + let next_c = self.input[self.input_idx]; + self.input_idx += 1; + self.input_cursor_position += compute_character_width(next_c); + } + return Ok(EventState::Consumed); + } + Key::Ctrl('e') => { + if self.input_idx < self.input.len() { + self.input_idx = self.input.len(); + self.input_cursor_position = self.input_str().width() as u16; + } + return Ok(EventState::Consumed); + } + _ => (), + } + Ok(EventState::NotConsumed) + } +} diff --git a/src/components/table_status.rs b/src/components/table_status.rs index 8cf9dee..2fce197 100644 --- a/src/components/table_status.rs +++ b/src/components/table_status.rs @@ -1,4 +1,4 @@ -use super::{Component, DrawableComponent}; +use super::{Component, DrawableComponent, EventState}; use crate::event::Key; use anyhow::Result; use database_tree::Table; @@ -85,7 +85,7 @@ impl DrawableComponent for TableStatusComponent { } impl Component for TableStatusComponent { - fn event(&mut self, _key: Key) -> Result<()> { - Ok(()) + fn event(&mut self, _key: Key) -> Result { + Ok(EventState::NotConsumed) } } diff --git a/src/components/table_value.rs b/src/components/table_value.rs index d4b7b85..c1ff069 100644 --- a/src/components/table_value.rs +++ b/src/components/table_value.rs @@ -1,4 +1,4 @@ -use super::{Component, DrawableComponent}; +use super::{Component, DrawableComponent, EventState}; use crate::event::Key; use anyhow::Result; use tui::{ @@ -45,7 +45,7 @@ impl DrawableComponent for TableValueComponent { } impl Component for TableValueComponent { - fn event(&mut self, _key: Key) -> Result<()> { + fn event(&mut self, _key: Key) -> Result { todo!("scroll"); } } diff --git a/src/components/utils/scroll_vertical.rs b/src/components/utils/scroll_vertical.rs index ceb5fc6..9f8b03f 100644 --- a/src/components/utils/scroll_vertical.rs +++ b/src/components/utils/scroll_vertical.rs @@ -1,9 +1,7 @@ +use crate::{components::ScrollType, ui::scrollbar::draw_scrollbar}; use std::cell::Cell; - use tui::{backend::Backend, layout::Rect, Frame}; -use crate::{components::ScrollType, ui::scrollbar::draw_scrollbar}; - pub struct VerticalScroll { top: Cell, max_top: Cell, diff --git a/src/event/key.rs b/src/event/key.rs index ba4979f..4e29c35 100644 --- a/src/event/key.rs +++ b/src/event/key.rs @@ -63,7 +63,6 @@ pub enum Key { F12, Char(char), Ctrl(char), - Shift(char), Alt(char), Unkown, } @@ -104,7 +103,6 @@ impl fmt::Display for Key { Key::Char(' ') => write!(f, ""), Key::Alt(c) => write!(f, "", c), Key::Ctrl(c) => write!(f, "", c), - Key::Shift(c) => write!(f, "", c), Key::Char(c) => write!(f, "{}", c), Key::Left | Key::Right | Key::Up | Key::Down => write!(f, "<{:?} Arrow Key>", self), Key::Enter @@ -195,10 +193,6 @@ impl From for Key { code: event::KeyCode::Char(c), modifiers: event::KeyModifiers::CONTROL, } => Key::Ctrl(c), - event::KeyEvent { - code: event::KeyCode::Char(c), - modifiers: event::KeyModifiers::SHIFT, - } => Key::Shift(c), event::KeyEvent { code: event::KeyCode::Char(c), diff --git a/src/handlers/connection_list.rs b/src/handlers/connection_list.rs deleted file mode 100644 index ba4dd1a..0000000 --- a/src/handlers/connection_list.rs +++ /dev/null @@ -1,47 +0,0 @@ -use crate::app::{App, FocusBlock}; -use crate::components::Component as _; -use crate::event::Key; -use crate::utils::{get_databases, get_tables}; -use database_tree::Database; -use sqlx::mysql::MySqlPool; -use std::collections::BTreeSet; - -pub async fn handler(key: Key, app: &mut App) -> anyhow::Result<()> { - match key { - Key::Enter => { - app.record_table.reset(vec![], vec![]); - app.record_table.state.select(Some(0)); - if let Some(conn) = app.connections.selected_connection() { - if let Some(pool) = app.pool.as_ref() { - pool.close().await; - } - let pool = MySqlPool::connect(conn.database_url().as_str()).await?; - app.pool = Some(pool); - app.focus_block = FocusBlock::DabataseList; - } - if let Some(conn) = app.connections.selected_connection() { - match &conn.database { - Some(database) => app - .databases - .update( - &[Database::new( - database.clone(), - get_tables(database.clone(), app.pool.as_ref().unwrap()).await?, - )], - &BTreeSet::new(), - ) - .unwrap(), - None => app - .databases - .update( - get_databases(app.pool.as_ref().unwrap()).await?.as_slice(), - &BTreeSet::new(), - ) - .unwrap(), - } - }; - } - key => app.connections.event(key)?, - } - Ok(()) -} diff --git a/src/handlers/database_list.rs b/src/handlers/database_list.rs deleted file mode 100644 index b88130d..0000000 --- a/src/handlers/database_list.rs +++ /dev/null @@ -1,48 +0,0 @@ -use crate::app::{App, FocusBlock}; -use crate::components::table::RECORDS_LIMIT_PER_PAGE; -use crate::components::Component as _; -use crate::event::Key; -use crate::utils::{get_columns, get_records}; -use database_tree::Database; - -pub async fn handler(key: Key, app: &mut App) -> anyhow::Result<()> { - match key { - Key::Char('c') if app.databases.tree_focused() => { - app.focus_block = FocusBlock::ConnectionList - } - Key::Right if app.databases.tree_focused() => app.focus_block = FocusBlock::Table, - Key::Enter if app.databases.tree_focused() => { - if let Some((table, database)) = app.databases.tree().selected_table() { - app.focus_block = FocusBlock::Table; - let (headers, records) = get_records( - &Database { - name: database.clone(), - tables: vec![], - }, - &table, - 0, - RECORDS_LIMIT_PER_PAGE, - app.pool.as_ref().unwrap(), - ) - .await?; - app.record_table.reset(headers, records); - - let (headers, records) = get_columns( - &Database { - name: database, - tables: vec![], - }, - &table, - app.pool.as_ref().unwrap(), - ) - .await?; - app.structure_table.reset(headers, records); - - app.table_status - .update(app.record_table.rows.len() as u64, table); - } - } - key => app.databases.event(key)?, - } - Ok(()) -} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs deleted file mode 100644 index bc7cbc1..0000000 --- a/src/handlers/mod.rs +++ /dev/null @@ -1,32 +0,0 @@ -pub mod connection_list; -pub mod database_list; -pub mod query; -pub mod record_table; -pub mod structure_table; - -use crate::app::{App, FocusBlock}; -use crate::components::tab::Tab; -use crate::components::Component as _; -use crate::event::Key; - -pub async fn handle_app(key: Key, app: &mut App) -> anyhow::Result<()> { - match key { - Key::Ctrl('e') => app.focus_block = FocusBlock::Query, - Key::Esc if app.error.error.is_some() => { - app.error.error = None; - return Ok(()); - } - key => app.tab.event(key)?, - } - - match app.focus_block { - FocusBlock::ConnectionList => connection_list::handler(key, app).await?, - FocusBlock::DabataseList => database_list::handler(key, app).await?, - FocusBlock::Table => match app.tab.selected_tab { - Tab::Records => record_table::handler(key, app).await?, - Tab::Structure => structure_table::handler(key, app).await?, - }, - FocusBlock::Query => query::handler(key, app).await?, - } - Ok(()) -} diff --git a/src/handlers/query.rs b/src/handlers/query.rs deleted file mode 100644 index a1ce829..0000000 --- a/src/handlers/query.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::app::{App, FocusBlock}; -use crate::components::Component as _; -use crate::event::Key; -use crate::utils::convert_column_value_to_string; -use futures::TryStreamExt; -use regex::Regex; -use sqlx::Row; - -pub async fn handler(key: Key, app: &mut App) -> anyhow::Result<()> { - match key { - Key::Enter => { - let re = Regex::new(r"select .+ from ([^ ]+).*").unwrap(); - match re.captures(app.query.input.as_str()) { - Some(caps) => { - let mut rows = - sqlx::query(app.query.input.as_str()).fetch(app.pool.as_ref().unwrap()); - let headers = - sqlx::query(format!("desc `{}`", caps.get(1).unwrap().as_str()).as_str()) - .fetch_all(app.pool.as_ref().unwrap()) - .await? - .iter() - .map(|table| table.get(0)) - .collect::>(); - let mut records = vec![]; - while let Some(row) = rows.try_next().await? { - records.push( - row.columns() - .iter() - .map(|col| convert_column_value_to_string(&row, col)) - .collect::>(), - ) - } - app.record_table.reset(headers, records); - } - None => { - sqlx::query(app.query.input.as_str()) - .execute(app.pool.as_ref().unwrap()) - .await?; - } - } - } - Key::Esc => app.focus_block = FocusBlock::Table, - key => app.query.event(key)?, - } - Ok(()) -} diff --git a/src/handlers/record_table.rs b/src/handlers/record_table.rs deleted file mode 100644 index 7f710f3..0000000 --- a/src/handlers/record_table.rs +++ /dev/null @@ -1,42 +0,0 @@ -use crate::app::{App, FocusBlock}; -use crate::components::table::RECORDS_LIMIT_PER_PAGE; -use crate::components::Component as _; -use crate::event::Key; -use crate::utils::get_records; -use database_tree::Database; - -pub async fn handler(key: Key, app: &mut App) -> anyhow::Result<()> { - match key { - Key::Left => app.focus_block = FocusBlock::DabataseList, - Key::Char('c') => app.focus_block = FocusBlock::ConnectionList, - Key::Char('y') => { - if let Some(text) = app.record_table.selected_cell() { - app.clipboard.store(text) - } - } - key => { - app.record_table.event(key)?; - if let Some(index) = app.record_table.state.selected() { - if index == app.record_table.rows.len().saturating_sub(1) { - if let Some((table, database)) = app.databases.tree().selected_table() { - let (_, records) = get_records( - &Database { - name: database.clone(), - tables: vec![], - }, - &table, - index as u16, - RECORDS_LIMIT_PER_PAGE, - app.pool.as_ref().unwrap(), - ) - .await?; - if !records.is_empty() { - app.record_table.rows.extend(records); - } - } - } - } - } - } - Ok(()) -} diff --git a/src/handlers/structure_table.rs b/src/handlers/structure_table.rs deleted file mode 100644 index ed94a35..0000000 --- a/src/handlers/structure_table.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::app::{App, FocusBlock}; -use crate::components::Component as _; -use crate::event::Key; - -pub async fn handler(key: Key, app: &mut App) -> anyhow::Result<()> { - match key { - Key::Left => app.focus_block = FocusBlock::DabataseList, - Key::Char('c') => app.focus_block = FocusBlock::ConnectionList, - Key::Char('y') => { - if let Some(text) = app.structure_table.selected_cell() { - app.clipboard.store(text) - } - } - key => app.structure_table.event(key)?, - } - Ok(()) -} diff --git a/src/main.rs b/src/main.rs index bd8d88b..3da4693 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,6 @@ mod app; mod clipboard; mod components; mod event; -mod handlers; mod ui; mod user_config; mod utils; @@ -12,7 +11,6 @@ mod log; use crate::app::App; use crate::event::{Event, Key}; -use crate::handlers::handle_app; use anyhow::Result; use crossterm::{ terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, @@ -45,15 +43,14 @@ async fn main() -> anyhow::Result<()> { loop { terminal.draw(|f| app.draw(f).unwrap())?; match events.next()? { - Event::Input(key) => { - if key == Key::Char('q') { - break; - }; - match handle_app(key, &mut app).await { - Ok(_) => (), - Err(err) => app.error.set(err.to_string()), + Event::Input(key) => match app.event(key).await { + Ok(state) => { + if !state.is_consumed() && (key == Key::Char('q') || key == Key::Ctrl('c')) { + break; + } } - } + Err(err) => app.error.set(err.to_string()), + }, Event::Tick => (), } } diff --git a/src/ui/scrollbar.rs b/src/ui/scrollbar.rs index 44d7f5e..0b2344a 100644 --- a/src/ui/scrollbar.rs +++ b/src/ui/scrollbar.rs @@ -4,13 +4,12 @@ use tui::{ backend::Backend, buffer::Buffer, layout::{Margin, Rect}, - style::Style, + style::{Color, Style}, symbols::{block::FULL, line::DOUBLE_VERTICAL}, widgets::Widget, Frame, }; -/// struct Scrollbar { max: u16, pos: u16, @@ -70,6 +69,6 @@ impl Widget for Scrollbar { pub fn draw_scrollbar(f: &mut Frame, r: Rect, max: usize, pos: usize) { let mut widget = Scrollbar::new(max, pos); - widget.style_pos = Style::default(); + widget.style_pos = Style::default().fg(Color::Blue); f.render_widget(widget, r); } diff --git a/src/utils.rs b/src/utils.rs index d53adca..6471278 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,94 +1,150 @@ +use async_trait::async_trait; use chrono::NaiveDate; use database_tree::{Database, Table}; use futures::TryStreamExt; -use sqlx::mysql::{MySqlColumn, MySqlPool, MySqlRow}; +use sqlx::mysql::{MySqlColumn, MySqlPool as MPool, MySqlRow}; use sqlx::{Column as _, Row, TypeInfo}; -pub async fn get_databases(pool: &MySqlPool) -> anyhow::Result> { - let databases = sqlx::query("SHOW DATABASES") - .fetch_all(pool) - .await? - .iter() - .map(|table| table.get(0)) - .collect::>(); - let mut list = vec![]; - for db in databases { - list.push(Database::new( - db.clone(), - get_tables(db.clone(), pool).await?, - )) - } - Ok(list) +pub const RECORDS_LIMIT_PER_PAGE: u8 = 200; + +#[async_trait] +pub trait Pool { + async fn get_databases(&self) -> anyhow::Result>; + async fn get_tables(&self, database: String) -> anyhow::Result>; + async fn get_records( + &self, + database: &String, + table: &String, + page: u16, + filter: Option, + ) -> anyhow::Result<(Vec, Vec>)>; + async fn get_columns( + &self, + database: &String, + table: &String, + ) -> anyhow::Result<(Vec, Vec>)>; + async fn close(&self); } -pub async fn get_tables(database: String, pool: &MySqlPool) -> anyhow::Result> { - let tables = - sqlx::query_as::<_, Table>(format!("SHOW TABLE STATUS FROM `{}`", database).as_str()) - .fetch_all(pool) - .await?; - Ok(tables) +pub struct MySqlPool { + pool: MPool, } -pub async fn get_records( - database: &Database, - table: &Table, - page: u16, - limit: u8, - pool: &MySqlPool, -) -> anyhow::Result<(Vec, Vec>)> { - let query = format!( - "SELECT * FROM `{}`.`{}` limit {page}, {limit}", - database.name, - table.name, - page = page, - limit = limit - ); - let mut rows = sqlx::query(query.as_str()).fetch(pool); - let headers = - sqlx::query(format!("SHOW COLUMNS FROM `{}`.`{}`", database.name, table.name).as_str()) - .fetch_all(pool) +impl MySqlPool { + pub async fn new(database_url: &str) -> anyhow::Result { + Ok(Self { + pool: MPool::connect(database_url).await?, + }) + } +} + +#[async_trait] +impl Pool for MySqlPool { + async fn get_databases(&self) -> anyhow::Result> { + let databases = sqlx::query("SHOW DATABASES") + .fetch_all(&self.pool) .await? .iter() .map(|table| table.get(0)) .collect::>(); - let mut records = vec![]; - while let Some(row) = rows.try_next().await? { - records.push( - row.columns() + let mut list = vec![]; + for db in databases { + list.push(Database::new( + db.clone(), + get_tables(db.clone(), &self.pool).await?, + )) + } + Ok(list) + } + + async fn get_tables(&self, database: String) -> anyhow::Result> { + let tables = + sqlx::query_as::<_, Table>(format!("SHOW TABLE STATUS FROM `{}`", database).as_str()) + .fetch_all(&self.pool) + .await?; + Ok(tables) + } + + async fn get_records( + &self, + database: &String, + table: &String, + page: u16, + filter: Option, + ) -> anyhow::Result<(Vec, Vec>)> { + let query = if let Some(filter) = filter { + format!( + "SELECT * FROM `{database}`.`{table}` WHERE {filter} LIMIT {page}, {limit}", + database = database, + table = table, + filter = filter, + page = page, + limit = RECORDS_LIMIT_PER_PAGE + ) + } else { + format!( + "SELECT * FROM `{}`.`{}` limit {page}, {limit}", + database, + table, + page = page, + limit = RECORDS_LIMIT_PER_PAGE + ) + }; + let mut rows = sqlx::query(query.as_str()).fetch(&self.pool); + let mut headers = vec![]; + let mut records = vec![]; + while let Some(row) = rows.try_next().await? { + headers = row + .columns() .iter() - .map(|col| convert_column_value_to_string(&row, col)) - .collect::>(), - ) + .map(|column| column.name().to_string()) + .collect(); + records.push( + row.columns() + .iter() + .map(|col| convert_column_value_to_string(&row, col)) + .collect::>(), + ) + } + Ok((headers, records)) } - Ok((headers, records)) -} -pub async fn get_columns( - database: &Database, - table: &Table, - pool: &MySqlPool, -) -> anyhow::Result<(Vec, Vec>)> { - let query = format!( - "SHOW FULL COLUMNS FROM `{}`.`{}`", - database.name, table.name - ); - let mut rows = sqlx::query(query.as_str()).fetch(pool); - let mut headers = vec![]; - let mut records = vec![]; - while let Some(row) = rows.try_next().await? { - headers = row - .columns() - .iter() - .map(|column| column.name().to_string()) - .collect(); - records.push( - row.columns() + async fn get_columns( + &self, + database: &String, + table: &String, + ) -> anyhow::Result<(Vec, Vec>)> { + let query = format!("SHOW FULL COLUMNS FROM `{}`.`{}`", database, table); + let mut rows = sqlx::query(query.as_str()).fetch(&self.pool); + let mut headers = vec![]; + let mut records = vec![]; + while let Some(row) = rows.try_next().await? { + headers = row + .columns() .iter() - .map(|col| convert_column_value_to_string(&row, col)) - .collect::>(), - ) + .map(|column| column.name().to_string()) + .collect(); + records.push( + row.columns() + .iter() + .map(|col| convert_column_value_to_string(&row, col)) + .collect::>(), + ) + } + Ok((headers, records)) } - Ok((headers, records)) + + async fn close(&self) { + self.pool.close().await; + } +} + +pub async fn get_tables(database: String, pool: &MPool) -> anyhow::Result> { + let tables = + sqlx::query_as::<_, Table>(format!("SHOW TABLE STATUS FROM `{}`", database).as_str()) + .fetch_all(pool) + .await?; + Ok(tables) } pub fn convert_column_value_to_string(row: &MySqlRow, column: &MySqlColumn) -> String {