diff --git a/.gitignore b/.gitignore index 4458c1e..8ee035e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target gobang gobang.yml +gobang.log diff --git a/database-tree/src/item.rs b/database-tree/src/item.rs index d771775..27a237c 100644 --- a/database-tree/src/item.rs +++ b/database-tree/src/item.rs @@ -9,11 +9,8 @@ pub struct TreeItemInfo { } impl TreeItemInfo { - pub const fn new(indent: u8) -> Self { - Self { - indent, - visible: true, - } + pub const fn new(indent: u8, visible: bool) -> Self { + Self { indent, visible } } pub const fn is_visible(&self) -> bool { @@ -83,7 +80,7 @@ impl DatabaseTreeItem { let indent = u8::try_from((3_usize).saturating_sub(2))?; Ok(Self { - info: TreeItemInfo::new(indent), + info: TreeItemInfo::new(indent, false), kind: DatabaseTreeItemKind::Table { database: database.name.clone(), table: table.clone(), @@ -91,12 +88,12 @@ impl DatabaseTreeItem { }) } - pub fn new_database(database: &Database, collapsed: bool) -> Result { + pub fn new_database(database: &Database, _collapsed: bool) -> Result { Ok(Self { - info: TreeItemInfo::new(0), + info: TreeItemInfo::new(0, true), kind: DatabaseTreeItemKind::Database { name: database.name.to_string(), - collapsed, + collapsed: true, }, }) } diff --git a/database-tree/src/lib.rs b/database-tree/src/lib.rs index a4c3ebf..0303210 100644 --- a/database-tree/src/lib.rs +++ b/database-tree/src/lib.rs @@ -31,7 +31,7 @@ pub struct Table { #[sqlx(rename = "Name")] pub name: String, #[sqlx(rename = "Create_time")] - pub create_time: chrono::DateTime, + pub create_time: Option>, #[sqlx(rename = "Update_time")] pub update_time: Option>, #[sqlx(rename = "Engine")] diff --git a/resources/gobang.gif b/resources/gobang.gif index b5042c0..4d4e87b 100644 Binary files a/resources/gobang.gif and b/resources/gobang.gif differ diff --git a/src/app.rs b/src/app.rs index 7c3848a..3449d83 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,208 +1,38 @@ -use crate::components::utils::scroll_vertical::VerticalScroll; +use crate::components::DrawableComponent as _; use crate::{ - components::DatabasesComponent, - user_config::{Connection, UserConfig}, + components::tab::Tab, + components::{ + ConnectionsComponent, DatabasesComponent, QueryComponent, TabComponent, TableComponent, + TableStatusComponent, + }, + user_config::UserConfig, }; -use sqlx::mysql::MySqlPool; -use strum::IntoEnumIterator; -use strum_macros::EnumIter; +use sqlx::MySqlPool; use tui::{ backend::Backend, - layout::{Constraint, Rect}, + layout::{Constraint, Direction, Layout}, style::{Color, Style}, - widgets::{Block, Borders, Cell, ListState, Row, Table as WTable, TableState}, + widgets::{Block, Borders, Clear, ListState, Paragraph}, Frame, }; -use unicode_width::UnicodeWidthStr; - -#[derive(Debug, Clone, Copy, EnumIter)] -pub enum Tab { - Records, - Structure, -} - -impl std::fmt::Display for Tab { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{:?}", self) - } -} - -impl Tab { - pub fn names() -> Vec { - Self::iter() - .map(|tab| format!("{} [{}]", tab, tab as u8 + 1)) - .collect() - } -} pub enum FocusBlock { DabataseList, - RecordTable, + Table, ConnectionList, Query, } - -#[derive(sqlx::FromRow, Debug, Clone)] -pub struct Column { - #[sqlx(rename = "Field")] - pub field: String, - #[sqlx(rename = "Type")] - pub r#type: String, - #[sqlx(rename = "Collation")] - pub collation: String, - #[sqlx(rename = "Null")] - pub null: String, -} - -pub struct RecordTable { - pub state: TableState, - pub headers: Vec, - pub rows: Vec>, - pub column_index: usize, - pub scroll: VerticalScroll, -} - -impl Default for RecordTable { - fn default() -> Self { - Self { - state: TableState::default(), - headers: vec![], - rows: vec![], - column_index: 0, - scroll: VerticalScroll::new(), - } - } -} - -impl RecordTable { - pub fn next(&mut self) { - let i = match self.state.selected() { - Some(i) => { - if i >= self.rows.len() - 1 { - Some(i) - } else { - Some(i + 1) - } - } - None => None, - }; - self.state.select(i); - } - - pub fn previous(&mut self) { - let i = match self.state.selected() { - Some(i) => { - if i == 0 { - Some(i) - } else { - Some(i - 1) - } - } - 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 - } - } - - pub fn previous_column(&mut self) { - if self.column_index > 0 { - self.column_index -= 1 - } - } - - pub fn headers(&self) -> Vec { - let mut headers = self.headers[self.column_index..].to_vec(); - headers.insert(0, "".to_string()); - headers - } - - pub fn rows(&self) -> Vec> { - let mut rows = self - .rows - .iter() - .map(|row| row[self.column_index..].to_vec()) - .collect::>>(); - for (index, row) in rows.iter_mut().enumerate() { - row.insert(0, (index + 1).to_string()) - } - rows - } - - pub fn draw( - &mut self, - f: &mut Frame<'_, B>, - layout_chunk: Rect, - focused: bool, - ) -> anyhow::Result<()> { - self.state.selected().map_or_else( - || { - self.scroll.reset(); - }, - |selection| { - self.scroll.update( - selection, - self.rows.len(), - layout_chunk.height.saturating_sub(2) as usize, - ); - }, - ); - - let headers = self.headers(); - let header_cells = headers - .iter() - .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 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())); - Row::new(cells).height(height as u16).bottom_margin(1) - }); - let widths = (0..10) - .map(|_| Constraint::Percentage(10)) - .collect::>(); - let t = WTable::new(rows) - .header(header) - .block(Block::default().borders(Borders::ALL).title("Records")) - .highlight_style(Style::default().fg(Color::Green)) - .style(if focused { - Style::default() - } else { - Style::default().fg(Color::DarkGray) - }) - .widths(&widths); - f.render_stateful_widget(t, layout_chunk, &mut self.state); - - self.scroll.draw(f, layout_chunk); - Ok(()) - } -} - pub struct App { - pub input: String, - pub input_cursor_x: u16, - pub query: String, - pub record_table: RecordTable, - pub structure_table: RecordTable, + pub query: QueryComponent, + pub record_table: TableComponent, + pub structure_table: TableComponent, pub focus_block: FocusBlock, - pub selected_tab: Tab, + pub tab: TabComponent, pub user_config: Option, pub selected_connection: ListState, - pub selected_database: ListState, - pub selected_table: ListState, pub databases: DatabasesComponent, + pub connections: ConnectionsComponent, + pub table_status: TableStatusComponent, pub pool: Option, pub error: Option, } @@ -210,18 +40,16 @@ pub struct App { impl Default for App { fn default() -> App { App { - input: String::new(), - input_cursor_x: 0, - query: String::new(), - record_table: RecordTable::default(), - structure_table: RecordTable::default(), + query: QueryComponent::default(), + record_table: TableComponent::default(), + structure_table: TableComponent::default(), focus_block: FocusBlock::DabataseList, - selected_tab: Tab::Records, + tab: TabComponent::default(), user_config: None, selected_connection: ListState::default(), - selected_database: ListState::default(), - selected_table: ListState::default(), databases: DatabasesComponent::new(), + connections: ConnectionsComponent::default(), + table_status: TableStatusComponent::default(), pool: None, error: None, } @@ -229,82 +57,115 @@ impl Default for App { } impl App { - pub fn next_connection(&mut self) { - if let Some(config) = &self.user_config { - let i = match self.selected_connection.selected() { - Some(i) => { - if i >= config.conn.len() - 1 { - 0 - } else { - i + 1 - } - } - None => 0, - }; - self.selected_connection.select(Some(i)); - } - } - - pub fn previous_connection(&mut self) { - if let Some(config) = &self.user_config { - let i = match self.selected_connection.selected() { - Some(i) => { - if i == 0 { - config.conn.len() - 1 - } else { - i - 1 - } - } - None => 0, - }; - self.selected_connection.select(Some(i)); - } - } - - pub fn increment_input_cursor_x(&mut self) { - if self.input_cursor_x > 0 { - self.input_cursor_x -= 1; + pub fn new(user_config: UserConfig) -> App { + App { + user_config: Some(user_config.clone()), + connections: ConnectionsComponent::new(user_config.conn), + focus_block: FocusBlock::ConnectionList, + ..App::default() } } - pub fn decrement_input_cursor_x(&mut self) { - if self.input_cursor_x < self.input.width() as u16 { - self.input_cursor_x += 1; + pub fn draw(&mut self, f: &mut Frame<'_, B>) -> anyhow::Result<()> { + if let FocusBlock::ConnectionList = self.focus_block { + self.connections.draw( + f, + Layout::default() + .constraints([Constraint::Percentage(100)]) + .split(f.size())[0], + false, + )?; + return Ok(()); } - } - pub fn selected_connection(&self) -> Option<&Connection> { - match &self.user_config { - Some(config) => match self.selected_connection.selected() { - Some(i) => config.conn.get(i), - None => None, - }, - None => None, + let main_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(15), Constraint::Percentage(85)]) + .split(f.size()); + let left_chunks = Layout::default() + .constraints([Constraint::Min(8), Constraint::Length(7)].as_ref()) + .split(main_chunks[0]); + + self.databases + .draw( + f, + left_chunks[0], + matches!(self.focus_block, FocusBlock::DabataseList), + ) + .unwrap(); + self.table_status.draw( + f, + left_chunks[1], + matches!(self.focus_block, FocusBlock::DabataseList), + )?; + + let right_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Length(3), + 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), + )?, } + self.draw_error_popup(f); + Ok(()) } - pub fn table_status(&self) -> Vec { - if let Some((table, _)) = self.databases.tree.selected_table() { - return vec![ - format!("created: {}", table.create_time.to_string()), - format!( - "updated: {}", - table - .update_time - .map(|time| time.to_string()) - .unwrap_or_default() - ), - format!( - "engine: {}", - table - .engine - .as_ref() - .map(|engine| engine.to_string()) - .unwrap_or_default() - ), - format!("rows: {}", self.record_table.rows.len()), - ]; + fn draw_error_popup(&self, f: &mut Frame<'_, B>) { + if let Some(error) = self.error.as_ref() { + let percent_x = 60; + let percent_y = 20; + let error = Paragraph::new(error.to_string()) + .block(Block::default().title("Error").borders(Borders::ALL)) + .style(Style::default().fg(Color::Red)); + 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]; + f.render_widget(Clear, area); + f.render_widget(error, area); } - Vec::new() } } diff --git a/src/components/command.rs b/src/components/command.rs index 055ae19..2fe446d 100644 --- a/src/components/command.rs +++ b/src/components/command.rs @@ -1,4 +1,3 @@ -/// #[derive(Clone, PartialEq, PartialOrd, Ord, Eq)] pub struct CommandText { /// @@ -10,8 +9,6 @@ pub struct CommandText { /// pub hide_help: bool, } - -/// pub struct CommandInfo { /// pub text: CommandText, diff --git a/src/components/connections.rs b/src/components/connections.rs new file mode 100644 index 0000000..e066a38 --- /dev/null +++ b/src/components/connections.rs @@ -0,0 +1,126 @@ +use super::{Component, DrawableComponent}; +use crate::event::Key; +use crate::user_config::Connection; +use anyhow::Result; +use tui::{ + backend::Backend, + layout::{Constraint, Direction, Layout, Rect}, + style::{Color, Style}, + text::{Span, Spans}, + widgets::{Block, Borders, Clear, List, ListItem, ListState}, + Frame, +}; + +pub struct ConnectionsComponent { + pub connections: Vec, + pub state: ListState, +} + +impl Default for ConnectionsComponent { + fn default() -> Self { + Self { + connections: Vec::new(), + state: ListState::default(), + } + } +} + +impl ConnectionsComponent { + pub fn new(connections: Vec) -> Self { + Self { + connections, + ..Self::default() + } + } + + pub fn next_connection(&mut self) { + let i = match self.state.selected() { + Some(i) => { + if i >= self.connections.len() - 1 { + 0 + } else { + i + 1 + } + } + None => 0, + }; + self.state.select(Some(i)); + } + + pub fn previous_connection(&mut self) { + let i = match self.state.selected() { + Some(i) => { + if i == 0 { + self.connections.len() - 1 + } else { + i - 1 + } + } + None => 0, + }; + self.state.select(Some(i)); + } + + pub fn selected_connection(&self) -> Option<&Connection> { + match self.state.selected() { + Some(i) => self.connections.get(i), + None => None, + } + } +} + +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 conns = &self.connections; + let connections: Vec = conns + .iter() + .map(|i| { + ListItem::new(vec![Spans::from(Span::raw(i.database_url()))]) + .style(Style::default()) + }) + .collect(); + let tasks = List::new(connections) + .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]; + f.render_widget(Clear, area); + f.render_stateful_widget(tasks, area, &mut self.state); + Ok(()) + } +} + +impl Component for ConnectionsComponent { + fn event(&mut self, key: Key) -> Result<()> { + match key { + Key::Char('j') => self.next_connection(), + Key::Char('k') => self.previous_connection(), + _ => (), + } + Ok(()) + } +} diff --git a/src/components/databases.rs b/src/components/databases.rs index 4303435..1e38519 100644 --- a/src/components/databases.rs +++ b/src/components/databases.rs @@ -1,7 +1,4 @@ -use super::{ - utils::scroll_vertical::VerticalScroll, CommandBlocking, CommandInfo, Component, - DrawableComponent, EventState, -}; +use super::{utils::scroll_vertical::VerticalScroll, Component, DrawableComponent}; use crate::event::Key; use crate::ui::common_nav; use crate::ui::scrolllist::draw_list_block; @@ -26,7 +23,6 @@ const EMPTY_STR: &str = ""; pub struct DatabasesComponent { pub tree: DatabaseTree, pub scroll: VerticalScroll, - pub focused: bool, } impl DatabasesComponent { @@ -34,7 +30,6 @@ impl DatabasesComponent { Self { tree: DatabaseTree::default(), scroll: VerticalScroll::new(), - focused: true, } } @@ -69,14 +64,14 @@ impl DatabasesComponent { Span::styled( name, if selected { - Style::default().fg(Color::Magenta).bg(Color::Green) + Style::default().bg(Color::Blue) } else { Style::default() }, ) } - fn draw_tree(&self, f: &mut Frame, area: Rect) { + fn draw_tree(&self, f: &mut Frame, area: Rect, focused: bool) { let tree_height = usize::from(area.height.saturating_sub(2)); self.tree.visual_selection().map_or_else( || { @@ -99,7 +94,7 @@ impl DatabasesComponent { area, Block::default() .title(Span::styled(title, Style::default())) - .style(if self.focused { + .style(if focused { Style::default() } else { Style::default().fg(Color::DarkGray) @@ -113,29 +108,25 @@ impl DatabasesComponent { } impl DrawableComponent for DatabasesComponent { - fn draw(&self, f: &mut Frame, area: Rect) -> Result<()> { + fn draw(&mut self, f: &mut Frame, area: Rect, focused: bool) -> Result<()> { if true { let chunks = Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Percentage(100)].as_ref()) .split(area); - self.draw_tree(f, chunks[0]); + self.draw_tree(f, chunks[0], focused); } Ok(()) } } impl Component for DatabasesComponent { - fn commands(&self, _out: &mut Vec, _force_all: bool) -> CommandBlocking { - CommandBlocking::PassingOn - } - - fn event(&mut self, key: Key) -> Result { + fn event(&mut self, key: Key) -> Result<()> { if tree_nav(&mut self.tree, key) { - return Ok(EventState::Consumed); + return Ok(()); } - Ok(EventState::NotConsumed) + Ok(()) } } diff --git a/src/components/mod.rs b/src/components/mod.rs index af21f1c..1a809a5 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,12 +1,21 @@ pub mod command; +pub mod connections; pub mod databases; +pub mod query; +pub mod tab; +pub mod table; +pub mod table_status; pub mod utils; pub use command::{CommandInfo, CommandText}; +pub use connections::ConnectionsComponent; pub use databases::DatabasesComponent; +pub use query::QueryComponent; +pub use tab::TabComponent; +pub use table::TableComponent; +pub use table_status::TableStatusComponent; use anyhow::Result; -use std::convert::From; use tui::{backend::Backend, layout::Rect, Frame}; #[derive(Copy, Clone)] @@ -25,38 +34,13 @@ pub enum Direction { Down, } -#[derive(PartialEq)] -pub enum CommandBlocking { - Blocking, - PassingOn, -} - pub trait DrawableComponent { - /// - fn draw(&self, f: &mut Frame, rect: Rect) -> Result<()>; -} - -#[derive(PartialEq)] -pub enum EventState { - Consumed, - NotConsumed, -} - -impl From for EventState { - fn from(consumed: bool) -> Self { - if consumed { - Self::Consumed - } else { - Self::NotConsumed - } - } + fn draw(&mut self, f: &mut Frame, rect: Rect, focused: bool) -> Result<()>; } /// base component trait pub trait Component { - fn commands(&self, out: &mut Vec, force_all: bool) -> CommandBlocking; - - fn event(&mut self, key: crate::event::Key) -> Result; + fn event(&mut self, key: crate::event::Key) -> Result<()>; fn focused(&self) -> bool { false diff --git a/src/components/query.rs b/src/components/query.rs new file mode 100644 index 0000000..edeb133 --- /dev/null +++ b/src/components/query.rs @@ -0,0 +1,83 @@ +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/tab.rs b/src/components/tab.rs new file mode 100644 index 0000000..bcc5794 --- /dev/null +++ b/src/components/tab.rs @@ -0,0 +1,73 @@ +use super::{Component, DrawableComponent}; +use crate::event::Key; +use anyhow::Result; +use strum::IntoEnumIterator; +use strum_macros::EnumIter; +use tui::{ + backend::Backend, + layout::Rect, + style::{Color, Modifier, Style}, + text::Spans, + widgets::{Block, Borders, Tabs}, + Frame, +}; + +#[derive(Debug, Clone, Copy, EnumIter)] +pub enum Tab { + Records, + Structure, +} + +impl std::fmt::Display for Tab { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl Tab { + pub fn names() -> Vec { + Self::iter() + .map(|tab| format!("{} [{}]", tab, tab as u8 + 1)) + .collect() + } +} + +pub struct TabComponent { + pub selected_tab: Tab, +} + +impl Default for TabComponent { + fn default() -> Self { + Self { + selected_tab: Tab::Records, + } + } +} + +impl DrawableComponent for TabComponent { + fn draw(&mut self, f: &mut Frame, area: Rect, _focused: bool) -> Result<()> { + let titles = Tab::names().iter().cloned().map(Spans::from).collect(); + let tabs = Tabs::new(titles) + .block(Block::default().borders(Borders::ALL)) + .select(self.selected_tab as usize) + .style(Style::default().fg(Color::DarkGray)) + .highlight_style( + Style::default() + .fg(Color::Reset) + .add_modifier(Modifier::UNDERLINED), + ); + f.render_widget(tabs, area); + Ok(()) + } +} + +impl Component for TabComponent { + 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, + _ => (), + } + Ok(()) + } +} diff --git a/src/components/table.rs b/src/components/table.rs new file mode 100644 index 0000000..12f4116 --- /dev/null +++ b/src/components/table.rs @@ -0,0 +1,196 @@ +use super::{utils::scroll_vertical::VerticalScroll, Component, DrawableComponent}; +use crate::event::Key; +use anyhow::Result; +use std::convert::From; +use tui::{ + backend::Backend, + layout::{Constraint, Rect}, + style::{Color, Style}, + widgets::{Block, Borders, Cell, Row, Table as WTable, TableState}, + Frame, +}; + +pub struct TableComponent { + pub state: TableState, + pub headers: Vec, + pub rows: Vec>, + pub column_index: usize, + pub scroll: VerticalScroll, +} + +impl Default for TableComponent { + fn default() -> Self { + Self { + state: TableState::default(), + headers: vec![], + rows: vec![], + column_index: 0, + scroll: VerticalScroll::new(), + } + } +} + +impl TableComponent { + pub fn next(&mut self, lines: usize) { + let i = match self.state.selected() { + Some(i) => { + if i + lines >= self.rows.len() { + Some(self.rows.len() - 1) + } else { + Some(i + lines) + } + } + None => None, + }; + self.state.select(i); + } + + pub fn reset(&mut self, headers: Vec, rows: Vec>) { + 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 scroll_top(&mut self) { + if self.rows.is_empty() { + return; + } + self.state.select(None); + self.state.select(Some(0)); + } + + pub fn scroll_bottom(&mut self) { + if self.rows.is_empty() { + return; + } + 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 + } + } + + pub fn previous_column(&mut self) { + if self.column_index > 0 { + self.column_index -= 1 + } + } + + pub fn headers(&self) -> Vec { + let mut headers = self.headers[self.column_index..].to_vec(); + headers.insert(0, "".to_string()); + headers + } + + pub fn rows(&self) -> Vec> { + let rows = self + .rows + .iter() + .map(|row| row[self.column_index..].to_vec()) + .collect::>>(); + let mut new_rows = match self.state.selected() { + Some(index) => { + if index + 100 <= self.rows.len() { + rows[..index + 100].to_vec() + } else { + rows + } + } + None => rows, + }; + for (index, row) in new_rows.iter_mut().enumerate() { + row.insert(0, (index + 1).to_string()) + } + new_rows + } +} + +impl DrawableComponent for TableComponent { + fn draw(&mut self, f: &mut Frame, area: Rect, focused: bool) -> Result<()> { + self.state.selected().map_or_else( + || { + self.scroll.reset(); + }, + |selection| { + self.scroll.update( + selection, + self.rows.len(), + area.height.saturating_sub(2) as usize, + ); + }, + ); + + let headers = self.headers(); + let header_cells = headers + .iter() + .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 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())); + Row::new(cells).height(height as u16).bottom_margin(1) + }); + let widths = (0..10) + .map(|_| Constraint::Percentage(10)) + .collect::>(); + let t = WTable::new(rows) + .header(header) + .block(Block::default().borders(Borders::ALL).title("Records")) + .highlight_style(Style::default().bg(Color::Blue)) + .style(if focused { + Style::default() + } else { + Style::default().fg(Color::DarkGray) + }) + .widths(&widths); + f.render_stateful_widget(t, area, &mut self.state); + + self.scroll.draw(f, area); + Ok(()) + } +} + +impl Component for TableComponent { + 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::Shift('G') | Key::Shift('g') => self.scroll_bottom(), + Key::Char('l') => self.next_column(), + _ => (), + } + Ok(()) + } +} diff --git a/src/components/table_status.rs b/src/components/table_status.rs new file mode 100644 index 0000000..8cf9dee --- /dev/null +++ b/src/components/table_status.rs @@ -0,0 +1,91 @@ +use super::{Component, DrawableComponent}; +use crate::event::Key; +use anyhow::Result; +use database_tree::Table; +use tui::{ + backend::Backend, + layout::Rect, + style::{Color, Style}, + text::{Span, Spans}, + widgets::{Block, Borders, List, ListItem}, + Frame, +}; + +pub struct TableStatusComponent { + pub rows_count: u64, + pub table: Option, +} + +impl Default for TableStatusComponent { + fn default() -> Self { + Self { + rows_count: 0, + table: None, + } + } +} + +impl TableStatusComponent { + pub fn update(&mut self, count: u64, table: Table) { + self.rows_count = count; + self.table = Some(table); + } + + pub fn status_str(&self) -> Vec { + if let Some(table) = self.table.as_ref() { + return vec![ + format!( + "created: {}", + table + .create_time + .map(|time| time.to_string()) + .unwrap_or_default() + ), + format!( + "updated: {}", + table + .update_time + .map(|time| time.to_string()) + .unwrap_or_default() + ), + format!( + "engine: {}", + table + .engine + .as_ref() + .map(|engine| engine.to_string()) + .unwrap_or_default() + ), + format!("rows: {}", self.rows_count), + ]; + } + Vec::new() + } +} + +impl DrawableComponent for TableStatusComponent { + fn draw(&mut self, f: &mut Frame, area: Rect, focused: bool) -> Result<()> { + let table_status: Vec = self + .status_str() + .iter() + .map(|i| { + ListItem::new(vec![Spans::from(Span::raw(i.to_string()))]).style(Style::default()) + }) + .collect(); + let tasks = List::new(table_status).block(Block::default().borders(Borders::ALL).style( + if focused { + Style::default() + } else { + Style::default().fg(Color::DarkGray) + }, + )); + f.render_widget(tasks, area); + Ok(()) + } +} + +impl Component for TableStatusComponent { + fn event(&mut self, _key: Key) -> Result<()> { + Ok(()) + } +} diff --git a/src/event/key.rs b/src/event/key.rs index 4e29c35..ba4979f 100644 --- a/src/event/key.rs +++ b/src/event/key.rs @@ -63,6 +63,7 @@ pub enum Key { F12, Char(char), Ctrl(char), + Shift(char), Alt(char), Unkown, } @@ -103,6 +104,7 @@ 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 @@ -193,6 +195,10 @@ 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 index e87b7a1..822d5e6 100644 --- a/src/handlers/connection_list.rs +++ b/src/handlers/connection_list.rs @@ -1,4 +1,5 @@ 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, DatabaseTree}; @@ -7,13 +8,10 @@ use std::collections::BTreeSet; pub async fn handler(key: Key, app: &mut App) -> anyhow::Result<()> { match key { - Key::Char('j') => app.next_connection(), - Key::Char('k') => app.previous_connection(), Key::Enter => { - app.selected_database.select(Some(0)); - app.selected_table.select(Some(0)); + app.record_table.reset(vec![], vec![]); app.record_table.state.select(Some(0)); - if let Some(conn) = app.selected_connection() { + if let Some(conn) = app.connections.selected_connection() { if let Some(pool) = app.pool.as_ref() { pool.close().await; } @@ -21,7 +19,7 @@ pub async fn handler(key: Key, app: &mut App) -> anyhow::Result<()> { app.pool = Some(pool); app.focus_block = FocusBlock::DabataseList; } - if let Some(conn) = app.selected_connection() { + if let Some(conn) = app.connections.selected_connection() { match &conn.database { Some(database) => { app.databases.tree = DatabaseTree::new( @@ -43,7 +41,7 @@ pub async fn handler(key: Key, app: &mut App) -> anyhow::Result<()> { } }; } - _ => (), + key => app.connections.event(key)?, } Ok(()) } diff --git a/src/handlers/database_list.rs b/src/handlers/database_list.rs index 2e5c135..c8f4b9f 100644 --- a/src/handlers/database_list.rs +++ b/src/handlers/database_list.rs @@ -5,13 +5,13 @@ use crate::utils::{get_columns, get_records}; use database_tree::Database; pub async fn handler(key: Key, app: &mut App) -> anyhow::Result<()> { - app.databases.event(key)?; match key { Key::Esc => app.focus_block = FocusBlock::DabataseList, - Key::Right => app.focus_block = FocusBlock::RecordTable, + Key::Right => app.focus_block = FocusBlock::Table, Key::Char('c') => app.focus_block = FocusBlock::ConnectionList, Key::Enter => { if let Some((table, database)) = app.databases.tree.selected_table() { + app.focus_block = FocusBlock::Table; let (headers, records) = get_records( &Database { name: database.clone(), @@ -21,9 +21,7 @@ pub async fn handler(key: Key, app: &mut App) -> anyhow::Result<()> { app.pool.as_ref().unwrap(), ) .await?; - app.record_table.state.select(Some(0)); - app.record_table.headers = headers; - app.record_table.rows = records; + app.record_table.reset(headers, records); let (headers, records) = get_columns( &Database { @@ -34,12 +32,13 @@ pub async fn handler(key: Key, app: &mut App) -> anyhow::Result<()> { app.pool.as_ref().unwrap(), ) .await?; - app.structure_table.state.select(Some(0)); - app.structure_table.headers = headers; - app.structure_table.rows = records; + 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 index 264262b..3bf09a6 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -2,15 +2,21 @@ pub mod connection_list; pub mod database_list; pub mod query; pub mod record_table; +pub mod structure_table; -use crate::app::{App, FocusBlock, Tab}; +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 app.focus_block { FocusBlock::ConnectionList => connection_list::handler(key, app).await?, FocusBlock::DabataseList => database_list::handler(key, app).await?, - FocusBlock::RecordTable => record_table::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?, } match key { @@ -20,14 +26,11 @@ pub async fn handle_app(key: Key, app: &mut App) -> anyhow::Result<()> { }, Key::Char('r') => match app.focus_block { FocusBlock::Query => (), - _ => app.focus_block = FocusBlock::RecordTable, + _ => app.focus_block = FocusBlock::Table, }, Key::Char('e') => app.focus_block = FocusBlock::Query, - Key::Char('1') => app.selected_tab = Tab::Records, - Key::Char('2') => app.selected_tab = Tab::Structure, Key::Esc => app.error = None, - _ => (), + key => app.tab.event(key)?, } - app.databases.focused = matches!(app.focus_block, FocusBlock::DabataseList); Ok(()) } diff --git a/src/handlers/query.rs b/src/handlers/query.rs index 6f7e390..22f65f4 100644 --- a/src/handlers/query.rs +++ b/src/handlers/query.rs @@ -1,74 +1,46 @@ 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; -use unicode_width::UnicodeWidthStr; pub async fn handler(key: Key, app: &mut App) -> anyhow::Result<()> { - if true { - match key { - Key::Enter => { - app.query = app.input.drain(..).collect(); - let re = Regex::new(r"select .+ from ([^ ]+).*").unwrap(); - match re.captures(app.query.as_str()) { - Some(caps) => { - let mut rows = - sqlx::query(app.query.as_str()).fetch(app.pool.as_ref().unwrap()); - let headers = sqlx::query( - format!("desc `{}`", caps.get(1).unwrap().as_str()).as_str(), + 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::>(), ) - .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.headers = headers; - app.record_table.rows = records; - } - None => { - sqlx::query(app.query.as_str()) - .execute(app.pool.as_ref().unwrap()) - .await?; } + app.record_table.reset(headers, records); } - } - Key::Char(c) => app.input.push(c), - Key::Delete | Key::Backspace => { - if app.input.width() > 0 { - if app.input_cursor_x == 0 { - app.input.pop(); - return Ok(()); - } - if app.input.width() - app.input_cursor_x as usize > 0 { - app.input - .remove(app.input.width() - app.input_cursor_x as usize); - } + None => { + sqlx::query(app.query.input.as_str()) + .execute(app.pool.as_ref().unwrap()) + .await?; } } - Key::Left => app.decrement_input_cursor_x(), - Key::Right => app.increment_input_cursor_x(), - Key::Esc => app.focus_block = FocusBlock::Query, - _ => {} - } - } else { - match key { - Key::Char('h') => app.focus_block = FocusBlock::DabataseList, - Key::Char('j') => app.focus_block = FocusBlock::RecordTable, - Key::Char('c') => app.focus_block = FocusBlock::ConnectionList, - Key::Enter => app.focus_block = FocusBlock::Query, - _ => (), } + Key::Esc => app.focus_block = FocusBlock::DabataseList, + key => app.query.event(key)?, } Ok(()) } diff --git a/src/handlers/record_table.rs b/src/handlers/record_table.rs index 2ef9249..e470331 100644 --- a/src/handlers/record_table.rs +++ b/src/handlers/record_table.rs @@ -1,15 +1,12 @@ 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::Char('h') => app.record_table.previous_column(), - Key::Char('j') => app.record_table.next(), - Key::Char('k') => app.record_table.previous(), - Key::Char('l') => app.record_table.next_column(), Key::Left => app.focus_block = FocusBlock::DabataseList, Key::Char('c') => app.focus_block = FocusBlock::ConnectionList, - _ => (), + key => app.record_table.event(key)?, } Ok(()) } diff --git a/src/handlers/structure_table.rs b/src/handlers/structure_table.rs new file mode 100644 index 0000000..7ba3b3c --- /dev/null +++ b/src/handlers/structure_table.rs @@ -0,0 +1,12 @@ +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 => app.structure_table.event(key)?, + } + Ok(()) +} diff --git a/src/log.rs b/src/log.rs new file mode 100644 index 0000000..eecf27c --- /dev/null +++ b/src/log.rs @@ -0,0 +1,14 @@ +#[macro_export] +macro_rules! outln { + ($($expr:expr),+) => {{ + use std::io::{Write}; + use std::fs::OpenOptions; + let mut file = OpenOptions::new() + .write(true) + .create(true) + .append(true) + .open("gobang.log") + .unwrap(); + writeln!(file, $($expr),+).expect("Can't write output"); + }} +} diff --git a/src/main.rs b/src/main.rs index df5b53b..c50f29f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,10 @@ mod ui; mod user_config; mod utils; -use crate::app::{App, FocusBlock}; +#[macro_use] +mod log; + +use crate::app::App; use crate::event::{Event, Key}; use crate::handlers::handle_app; use crossterm::{ @@ -21,6 +24,8 @@ use tui::{backend::CrosstermBackend, Terminal}; async fn main() -> anyhow::Result<()> { enable_raw_mode()?; + outln!("gobang logger"); + let user_config = user_config::UserConfig::new("sample.toml").ok(); let mut stdout = stdout(); @@ -29,17 +34,12 @@ async fn main() -> anyhow::Result<()> { let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; let events = event::Events::new(250); - - let mut app = App { - user_config, - focus_block: FocusBlock::ConnectionList, - ..App::default() - }; + let mut app = App::new(user_config.unwrap()); terminal.clear()?; loop { - terminal.draw(|f| ui::draw(f, &mut app).unwrap())?; + terminal.draw(|f| app.draw(f).unwrap())?; match events.next()? { Event::Input(key) => { if key == Key::Char('q') { diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 4811efc..45766f3 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,215 +1,9 @@ -use crate::app::{App, FocusBlock, Tab}; -use crate::components::DrawableComponent as _; use crate::event::Key; use database_tree::MoveSelection; -use tui::{ - backend::Backend, - layout::{Constraint, Direction, Layout, Rect}, - style::{Color, Modifier, Style}, - text::{Span, Spans}, - widgets::{Block, Borders, Cell, Clear, List, ListItem, Paragraph, Row, Table, Tabs}, - Frame, -}; -use unicode_width::UnicodeWidthStr; pub mod scrollbar; pub mod scrolllist; -pub fn draw(f: &mut Frame<'_, B>, app: &mut App) -> anyhow::Result<()> { - if let FocusBlock::ConnectionList = app.focus_block { - let percent_x = 60; - let percent_y = 50; - let conns = &app.user_config.as_ref().unwrap().conn; - let connections: Vec = conns - .iter() - .map(|i| { - ListItem::new(vec![Spans::from(Span::raw(i.database_url()))]) - .style(Style::default().fg(Color::White)) - }) - .collect(); - let tasks = List::new(connections) - .block(Block::default().borders(Borders::ALL).title("Connections")) - .highlight_style(Style::default().fg(Color::Green)) - .style(match app.focus_block { - FocusBlock::ConnectionList => Style::default().fg(Color::Green), - _ => Style::default().fg(Color::DarkGray), - }); - 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]; - f.render_widget(Clear, area); - f.render_stateful_widget(tasks, area, &mut app.selected_connection); - return Ok(()); - } - - let main_chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(15), Constraint::Percentage(85)]) - .split(f.size()); - - let left_chunks = Layout::default() - .constraints([Constraint::Min(8), Constraint::Length(7)].as_ref()) - .split(main_chunks[0]); - - app.databases.draw(f, left_chunks[0]).unwrap(); - - let table_status: Vec = app - .table_status() - .iter() - .map(|i| { - ListItem::new(vec![Spans::from(Span::raw(i.to_string()))]) - .style(Style::default().fg(Color::White)) - }) - .collect(); - let tasks = List::new(table_status) - .block(Block::default().borders(Borders::ALL)) - .highlight_style(Style::default().fg(Color::Green)); - f.render_widget(tasks, left_chunks[1]); - - let right_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Length(3), - Constraint::Length(3), - Constraint::Length(5), - ] - .as_ref(), - ) - .split(main_chunks[1]); - - let titles = Tab::names().iter().cloned().map(Spans::from).collect(); - let tabs = Tabs::new(titles) - .block(Block::default().borders(Borders::ALL)) - .select(app.selected_tab as usize) - .style(Style::default().fg(Color::DarkGray)) - .highlight_style( - Style::default() - .fg(Color::Reset) - .add_modifier(Modifier::UNDERLINED), - ); - f.render_widget(tabs, right_chunks[0]); - - let query = Paragraph::new(app.input.as_ref()) - .style(match app.focus_block { - FocusBlock::Query => Style::default(), - _ => Style::default().fg(Color::DarkGray), - }) - .block(Block::default().borders(Borders::ALL).title("Query")); - f.render_widget(query, right_chunks[1]); - if let FocusBlock::Query = app.focus_block { - f.set_cursor( - right_chunks[1].x + app.input.width() as u16 + 1 - app.input_cursor_x, - right_chunks[1].y + 1, - ) - } - match app.selected_tab { - Tab::Records => app.record_table.draw( - f, - right_chunks[2], - matches!(app.focus_block, FocusBlock::RecordTable), - )?, - Tab::Structure => draw_structure_table(f, app, right_chunks[2])?, - } - if let Some(err) = app.error.clone() { - draw_error_popup(f, err)?; - } - Ok(()) -} - -fn draw_structure_table( - f: &mut Frame<'_, B>, - app: &mut App, - layout_chunk: Rect, -) -> anyhow::Result<()> { - let headers = app.structure_table.headers(); - let header_cells = headers - .iter() - .map(|h| Cell::from(h.to_string()).style(Style::default().fg(Color::White))); - let header = Row::new(header_cells).height(1).bottom_margin(1); - let rows = app.structure_table.rows(); - let rows = rows.iter().map(|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().fg(Color::White))); - Row::new(cells).height(height as u16).bottom_margin(1) - }); - let widths = (0..10) - .map(|_| Constraint::Percentage(10)) - .collect::>(); - let t = Table::new(rows) - .header(header) - .block(Block::default().borders(Borders::ALL).title("Structure")) - .highlight_style(Style::default().fg(Color::Green)) - .style(match app.focus_block { - FocusBlock::RecordTable => Style::default(), - _ => Style::default().fg(Color::DarkGray), - }) - .widths(&widths); - f.render_stateful_widget(t, layout_chunk, &mut app.structure_table.state); - Ok(()) -} - -fn draw_error_popup(f: &mut Frame<'_, B>, error: String) -> anyhow::Result<()> { - let percent_x = 60; - let percent_y = 20; - let error = Paragraph::new(error) - .block(Block::default().title("Error").borders(Borders::ALL)) - .style(Style::default().fg(Color::Red)); - 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]; - f.render_widget(Clear, area); - f.render_widget(error, area); - Ok(()) -} - pub fn common_nav(key: Key) -> Option { if key == Key::Char('j') { Some(MoveSelection::Down) diff --git a/src/user_config.rs b/src/user_config.rs index c868653..80da370 100644 --- a/src/user_config.rs +++ b/src/user_config.rs @@ -2,12 +2,12 @@ use serde::Deserialize; use std::fs::File; use std::io::{BufReader, Read}; -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Clone)] pub struct UserConfig { pub conn: Vec, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Clone)] pub struct Connection { pub name: Option, pub user: String,