mirror of
https://github.com/TaKO8Ki/gobang.git
synced 2021-09-19 22:32:56 +03:00
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
This commit is contained in:
1
.gitignore
vendored
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 |
389
src/app.rs
389
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<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 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 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 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(());
|
||||
}
|
||||
|
||||
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 increment_input_cursor_x(&mut self) {
|
||||
if self.input_cursor_x > 0 {
|
||||
self.input_cursor_x -= 1;
|
||||
}
|
||||
}
|
||||
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());
|
||||
|
||||
pub fn decrement_input_cursor_x(&mut self) {
|
||||
if self.input_cursor_x < self.input.width() as u16 {
|
||||
self.input_cursor_x += 1;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
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()),
|
||||
];
|
||||
}
|
||||
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,
|
||||
|
||||
126
src/components/connections.rs
Normal file
126
src/components/connections.rs
Normal file
@@ -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
|
||||
|
||||
83
src/components/query.rs
Normal file
83
src/components/query.rs
Normal file
@@ -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(())
|
||||
}
|
||||
}
|
||||
73
src/components/tab.rs
Normal file
73
src/components/tab.rs
Normal file
@@ -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(())
|
||||
}
|
||||
}
|
||||
196
src/components/table.rs
Normal file
196
src/components/table.rs
Normal file
@@ -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(())
|
||||
}
|
||||
}
|
||||
91
src/components/table_status.rs
Normal file
91
src/components/table_status.rs
Normal file
@@ -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);
|
||||
}
|
||||
None => {
|
||||
sqlx::query(app.query.input.as_str())
|
||||
.execute(app.pool.as_ref().unwrap())
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
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(())
|
||||
}
|
||||
|
||||
12
src/handlers/structure_table.rs
Normal file
12
src/handlers/structure_table.rs
Normal file
@@ -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(())
|
||||
}
|
||||
14
src/log.rs
Normal file
14
src/log.rs
Normal file
@@ -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");
|
||||
}}
|
||||
}
|
||||
16
src/main.rs
16
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') {
|
||||
|
||||
206
src/ui/mod.rs
206
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<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,
|
||||
|
||||
Reference in New Issue
Block a user