Refactor components (#12)

* add logger

* add shift key

* implement record table reset function

* fix style

* ignore gobang.log

* use table component

* remove doc

* pass focused as an argument

* remove unused enums

* create table component

* use databases component

* use query component

* remove unused fields

* use query component

* use connections component

* remove unused struct

* use tab component

* use table status component

* remove unneeded return type

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

1
.gitignore vendored

@ -1,3 +1,4 @@
/target
gobang
gobang.yml
gobang.log

@ -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<Self> {
pub fn new_database(database: &Database, _collapsed: bool) -> Result<Self> {
Ok(Self {
info: TreeItemInfo::new(0),
info: TreeItemInfo::new(0, true),
kind: DatabaseTreeItemKind::Database {
name: database.name.to_string(),
collapsed,
collapsed: true,
},
})
}

@ -31,7 +31,7 @@ pub struct Table {
#[sqlx(rename = "Name")]
pub name: String,
#[sqlx(rename = "Create_time")]
pub create_time: chrono::DateTime<chrono::Utc>,
pub create_time: Option<chrono::DateTime<chrono::Utc>>,
#[sqlx(rename = "Update_time")]
pub update_time: Option<chrono::DateTime<chrono::Utc>>,
#[sqlx(rename = "Engine")]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 MiB

After

Width:  |  Height:  |  Size: 3.9 MiB

@ -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<String> {
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<String>,
pub rows: Vec<Vec<String>>,
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<String> {
let mut headers = self.headers[self.column_index..].to_vec();
headers.insert(0, "".to_string());
headers
}
pub fn rows(&self) -> Vec<Vec<String>> {
let mut rows = self
.rows
.iter()
.map(|row| row[self.column_index..].to_vec())
.collect::<Vec<Vec<String>>>();
for (index, row) in rows.iter_mut().enumerate() {
row.insert(0, (index + 1).to_string())
}
rows
}
pub fn draw<B: Backend>(
&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::<Vec<Constraint>>();
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<UserConfig>,
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<MySqlPool>,
pub error: Option<String>,
}
@ -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<B: Backend>(&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<String> {
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<B: Backend>(&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()
}
}

@ -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,

@ -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<Connection>,
pub state: ListState,
}
impl Default for ConnectionsComponent {
fn default() -> Self {
Self {
connections: Vec::new(),
state: ListState::default(),
}
}
}
impl ConnectionsComponent {
pub fn new(connections: Vec<Connection>) -> 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<B: Backend>(&mut self, f: &mut Frame<B>, _area: Rect, _focused: bool) -> Result<()> {
let percent_x = 60;
let percent_y = 50;
let conns = &self.connections;
let connections: Vec<ListItem> = 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(())
}
}

@ -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<B: Backend>(&self, f: &mut Frame<B>, area: Rect) {
fn draw_tree<B: Backend>(&self, f: &mut Frame<B>, 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<B: Backend>(&self, f: &mut Frame<B>, area: Rect) -> Result<()> {
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, 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<CommandInfo>, _force_all: bool) -> CommandBlocking {
CommandBlocking::PassingOn
}
fn event(&mut self, key: Key) -> Result<EventState> {
fn event(&mut self, key: Key) -> Result<()> {
if tree_nav(&mut self.tree, key) {
return Ok(EventState::Consumed);
return Ok(());
}
Ok(EventState::NotConsumed)
Ok(())
}
}

@ -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<B: Backend>(&self, f: &mut Frame<B>, rect: Rect) -> Result<()>;
}
#[derive(PartialEq)]
pub enum EventState {
Consumed,
NotConsumed,
}
impl From<bool> for EventState {
fn from(consumed: bool) -> Self {
if consumed {
Self::Consumed
} else {
Self::NotConsumed
}
}
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, rect: Rect, focused: bool) -> Result<()>;
}
/// base component trait
pub trait Component {
fn commands(&self, out: &mut Vec<CommandInfo>, force_all: bool) -> CommandBlocking;
fn event(&mut self, key: crate::event::Key) -> Result<EventState>;
fn event(&mut self, key: crate::event::Key) -> Result<()>;
fn focused(&self) -> bool {
false

@ -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<B: Backend>(&mut self, f: &mut Frame<B>, 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(())
}
}

@ -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<String> {
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<B: Backend>(&mut self, f: &mut Frame<B>, 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(())
}
}

@ -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<String>,
pub rows: Vec<Vec<String>>,
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<String>, rows: Vec<Vec<String>>) {
self.headers = headers;
self.rows = rows;
self.column_index = 0;
self.state.select(None);
if !self.rows.is_empty() {
self.state.select(Some(0));
}
}
pub fn 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<String> {
let mut headers = self.headers[self.column_index..].to_vec();
headers.insert(0, "".to_string());
headers
}
pub fn rows(&self) -> Vec<Vec<String>> {
let rows = self
.rows
.iter()
.map(|row| row[self.column_index..].to_vec())
.collect::<Vec<Vec<String>>>();
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<B: Backend>(&mut self, f: &mut Frame<B>, 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::<Vec<Constraint>>();
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(())
}
}

@ -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<Table>,
}
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<String> {
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<B: Backend>(&mut self, f: &mut Frame<B>, area: Rect, focused: bool) -> Result<()> {
let table_status: Vec<ListItem> = 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(())
}
}

@ -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, "<Space>"),
Key::Alt(c) => write!(f, "<Alt+{}>", c),
Key::Ctrl(c) => write!(f, "<Ctrl+{}>", c),
Key::Shift(c) => write!(f, "<Shift+{}>", 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<event::KeyEvent> 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),

@ -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(())
}

@ -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(())
}

@ -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(())
}

@ -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::<Vec<String>>();
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::<Vec<String>>(),
)
.fetch_all(app.pool.as_ref().unwrap())
.await?
.iter()
.map(|table| table.get(0))
.collect::<Vec<String>>();
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::<Vec<String>>(),
)
}
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(())
}

@ -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(())
}

@ -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(())
}

@ -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");
}}
}

@ -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') {

@ -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<B: Backend>(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<ListItem> = 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<ListItem> = 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<B: Backend>(
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::<Vec<Constraint>>();
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<B: Backend>(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<MoveSelection> {
if key == Key::Char('j') {
Some(MoveSelection::Down)

@ -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<Connection>,
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, Clone)]
pub struct Connection {
pub name: Option<String>,
pub user: String,

Loading…
Cancel
Save