diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1ef5889..d836008 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -61,7 +61,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- os: [ubuntu-latest, macos-latest]
+ os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
needs: check
steps:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 16bcf02..0c81e5d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -8,7 +8,10 @@ on:
jobs:
check:
name: Check
- runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ os: [ubuntu-latest, macos-latest]
+ runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- name: Cargo check
diff --git a/Cargo.lock b/Cargo.lock
index da0b9db..ece13d7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -472,6 +472,7 @@ dependencies = [
"chrono",
"crossterm 0.19.0",
"futures",
+ "regex",
"serde",
"serde_json",
"sqlx",
diff --git a/Cargo.toml b/Cargo.toml
index c31a01d..74f4a4d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -21,3 +21,4 @@ futures = "0.3.5"
serde_json = "1.0"
serde = "1.0"
toml = "0.4"
+regex = "1"
diff --git a/README.md b/README.md
index 8f64334..48106eb 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
A cross-platform terminal database tool written in Rust
-[![github workflow status](https://img.shields.io/github/workflow/status/TaKO8Ki/gobang/CI/main)](https://github.com/TaKO8Ki/gobang/actions)
+[![github workflow status](https://img.shields.io/github/workflow/status/TaKO8Ki/gobang/CI/main)](https://github.com/TaKO8Ki/gobang/actions) [![crates](https://img.shields.io/crates/v/gobang.svg?logo=rust)](https://crates.io/crates/gobang)
![gobang](./resources/gobang.gif)
diff --git a/resources/gobang.gif b/resources/gobang.gif
index ac85192..d602581 100644
Binary files a/resources/gobang.gif and b/resources/gobang.gif differ
diff --git a/src/app.rs b/src/app.rs
index 7017522..e7f2933 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -10,23 +10,29 @@ pub enum InputMode {
Editing,
}
-pub enum FocusType {
- Dabatases(bool),
- Tables(bool),
- Records(bool),
- Connections,
+pub enum FocusBlock {
+ DabataseList(bool),
+ TableList(bool),
+ RecordTable(bool),
+ ConnectionList,
}
#[derive(Clone)]
pub struct Database {
- pub selected_table: ListState,
pub name: String,
pub tables: Vec
,
}
-#[derive(Clone)]
+#[derive(sqlx::FromRow, Debug, Clone)]
pub struct Table {
+ #[sqlx(rename = "Name")]
pub name: String,
+ #[sqlx(rename = "Create_time")]
+ pub create_time: chrono::DateTime,
+ #[sqlx(rename = "Update_time")]
+ pub update_time: Option>,
+ #[sqlx(rename = "Engine")]
+ pub engine: String,
}
pub struct RecordTable {
@@ -92,51 +98,23 @@ impl RecordTable {
impl Database {
pub async fn new(name: String, pool: &MySqlPool) -> anyhow::Result {
Ok(Self {
- selected_table: ListState::default(),
name: name.clone(),
tables: get_tables(name, pool).await?,
})
}
-
- pub fn next(&mut self) {
- let i = match self.selected_table.selected() {
- Some(i) => {
- if i >= self.tables.len() - 1 {
- 0
- } else {
- i + 1
- }
- }
- None => 0,
- };
- self.selected_table.select(Some(i));
- }
-
- pub fn previous(&mut self) {
- let i = match self.selected_table.selected() {
- Some(i) => {
- if i == 0 {
- self.tables.len() - 1
- } else {
- i - 1
- }
- }
- None => 0,
- };
- self.selected_table.select(Some(i));
- }
}
pub struct App {
pub input: String,
pub input_mode: InputMode,
- pub messages: Vec>,
- pub selected_database: ListState,
+ pub query: String,
pub databases: Vec,
pub record_table: RecordTable,
- pub focus_type: FocusType,
+ pub focus_type: FocusBlock,
pub user_config: Option,
pub selected_connection: ListState,
+ pub selected_database: ListState,
+ pub selected_table: ListState,
pub pool: Option,
}
@@ -145,21 +123,46 @@ impl Default for App {
App {
input: String::new(),
input_mode: InputMode::Normal,
- messages: Vec::new(),
- selected_database: ListState::default(),
+ query: String::new(),
databases: Vec::new(),
record_table: RecordTable::default(),
- focus_type: FocusType::Dabatases(false),
+ focus_type: FocusBlock::DabataseList(false),
user_config: None,
selected_connection: ListState::default(),
+ selected_database: ListState::default(),
+ selected_table: ListState::default(),
pool: None,
}
}
}
impl App {
- pub fn new(title: &str, enhanced_graphics: bool) -> App {
- Self::default()
+ pub fn next_table(&mut self) {
+ let i = match self.selected_table.selected() {
+ Some(i) => {
+ if i >= self.selected_database().unwrap().tables.len() - 1 {
+ 0
+ } else {
+ i + 1
+ }
+ }
+ None => 0,
+ };
+ self.selected_table.select(Some(i));
+ }
+
+ pub fn previous_table(&mut self) {
+ let i = match self.selected_table.selected() {
+ Some(i) => {
+ if i == 0 {
+ self.selected_database().unwrap().tables.len() - 1
+ } else {
+ i - 1
+ }
+ }
+ None => 0,
+ };
+ self.selected_table.select(Some(i));
}
pub fn next_database(&mut self) {
@@ -173,6 +176,7 @@ impl App {
}
None => 0,
};
+ self.selected_table.select(Some(0));
self.selected_database.select(Some(i));
}
@@ -187,6 +191,7 @@ impl App {
}
None => 0,
};
+ self.selected_table.select(Some(0));
self.selected_database.select(Some(i));
}
@@ -224,18 +229,15 @@ impl App {
pub fn selected_database(&self) -> Option<&Database> {
match self.selected_database.selected() {
- Some(i) => match self.databases.get(i) {
- Some(db) => Some(db),
- None => None,
- },
+ Some(i) => self.databases.get(i),
None => None,
}
}
pub fn selected_table(&self) -> Option<&Table> {
- match self.selected_database() {
- Some(db) => match db.selected_table.selected() {
- Some(i) => db.tables.get(i),
+ match self.selected_table.selected() {
+ Some(i) => match self.selected_database() {
+ Some(db) => db.tables.get(i),
None => None,
},
None => None,
diff --git a/src/handlers/connection_list.rs b/src/handlers/connection_list.rs
new file mode 100644
index 0000000..fc07108
--- /dev/null
+++ b/src/handlers/connection_list.rs
@@ -0,0 +1,34 @@
+use crate::app::{App, Database, FocusBlock};
+use crate::event::Key;
+use crate::utils::get_databases;
+use sqlx::mysql::MySqlPool;
+
+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));
+ if let Some(conn) = app.selected_connection() {
+ if let Some(pool) = app.pool.as_ref() {
+ pool.close().await;
+ }
+ let pool = MySqlPool::connect(conn.database_url().as_str()).await?;
+ app.pool = Some(pool);
+ app.focus_type = FocusBlock::DabataseList(false);
+ }
+ app.databases = match app.selected_connection() {
+ Some(conn) => match &conn.database {
+ Some(database) => {
+ vec![Database::new(database.clone(), app.pool.as_ref().unwrap()).await?]
+ }
+ None => get_databases(app.pool.as_ref().unwrap()).await?,
+ },
+ None => vec![],
+ };
+ }
+ _ => (),
+ }
+ Ok(())
+}
diff --git a/src/handlers/create_connection.rs b/src/handlers/create_connection.rs
deleted file mode 100644
index 1bb72ff..0000000
--- a/src/handlers/create_connection.rs
+++ /dev/null
@@ -1,13 +0,0 @@
-use crate::app::{App, FocusType};
-use crate::event::Key;
-use sqlx::mysql::MySqlPool;
-
-pub async fn handler(_key: Key, app: &mut App) -> anyhow::Result<()> {
- if let Some(conn) = app.selected_connection() {
- app.pool.as_ref().unwrap().close().await;
- let pool = MySqlPool::connect(conn.database_url().as_str()).await?;
- app.pool = Some(pool);
- app.focus_type = FocusType::Dabatases(true);
- }
- Ok(())
-}
diff --git a/src/handlers/database_list.rs b/src/handlers/database_list.rs
index f86d1f0..e5016e9 100644
--- a/src/handlers/database_list.rs
+++ b/src/handlers/database_list.rs
@@ -1,16 +1,22 @@
-use crate::app::{App, Database};
+use crate::app::{App, FocusBlock};
use crate::event::Key;
-use crate::utils::get_databases;
-pub async fn handler(_key: Key, app: &mut App) -> anyhow::Result<()> {
- app.databases = match app.selected_connection() {
- Some(conn) => match &conn.database {
- Some(database) => {
- vec![Database::new(database.clone(), app.pool.as_ref().unwrap()).await?]
- }
- None => get_databases(app.pool.as_ref().unwrap()).await?,
- },
- None => vec![],
- };
+pub async fn handler(key: Key, app: &mut App, focused: bool) -> anyhow::Result<()> {
+ if focused {
+ match key {
+ Key::Char('j') => app.next_database(),
+ Key::Char('k') => app.previous_database(),
+ Key::Esc => app.focus_type = FocusBlock::DabataseList(false),
+ _ => (),
+ }
+ } else {
+ match key {
+ Key::Char('j') => app.focus_type = FocusBlock::TableList(false),
+ Key::Char('l') => app.focus_type = FocusBlock::RecordTable(false),
+ Key::Char('c') => app.focus_type = FocusBlock::ConnectionList,
+ Key::Enter => app.focus_type = FocusBlock::DabataseList(true),
+ _ => (),
+ }
+ }
Ok(())
}
diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs
index c70ad9b..101e5a1 100644
--- a/src/handlers/mod.rs
+++ b/src/handlers/mod.rs
@@ -1,93 +1,36 @@
-pub mod create_connection;
+pub mod connection_list;
pub mod database_list;
+pub mod query;
pub mod record_table;
+pub mod table_list;
-use crate::app::{App, FocusType, InputMode};
+use crate::app::{App, FocusBlock, InputMode};
use crate::event::Key;
pub async fn handle_app(key: Key, app: &mut App) -> anyhow::Result<()> {
match app.input_mode {
- InputMode::Normal => match key {
- Key::Char('e') => {
- app.input_mode = InputMode::Editing;
- }
- Key::Char('c') => {
- app.focus_type = FocusType::Connections;
- }
- Key::Char('l') => app.focus_type = FocusType::Records(false),
- Key::Char('h') => app.focus_type = FocusType::Tables(false),
- Key::Char('j') => {
- if let FocusType::Dabatases(_) = app.focus_type {
- app.focus_type = FocusType::Tables(false)
+ InputMode::Normal => {
+ match app.focus_type {
+ FocusBlock::ConnectionList => connection_list::handler(key, app).await?,
+ FocusBlock::DabataseList(focused) => {
+ database_list::handler(key, app, focused).await?
}
- }
- Key::Char('k') => {
- if let FocusType::Tables(_) = app.focus_type {
- app.focus_type = FocusType::Dabatases(false)
- }
- }
- Key::Right => match app.focus_type {
- FocusType::Records(true) => app.record_table.next_column(),
- _ => (),
- },
- Key::Left => match app.focus_type {
- FocusType::Records(true) => app.record_table.previous_column(),
- _ => (),
- },
- Key::Up => match app.focus_type {
- FocusType::Connections => app.previous_connection(),
- FocusType::Records(true) => app.record_table.previous(),
- FocusType::Dabatases(true) => app.previous_database(),
- FocusType::Tables(true) => match app.selected_database.selected() {
- Some(index) => {
- app.record_table.column_index = 0;
- app.databases[index].previous();
- record_table::handler(key, app).await?;
- }
- None => (),
- },
- _ => (),
- },
- Key::Down => match app.focus_type {
- FocusType::Connections => app.next_connection(),
- FocusType::Records(true) => app.record_table.next(),
- FocusType::Dabatases(true) => app.next_database(),
- FocusType::Tables(true) => match app.selected_database.selected() {
- Some(index) => {
- app.record_table.column_index = 0;
- app.databases[index].next();
- record_table::handler(key, app).await?
- }
- None => (),
- },
- _ => (),
- },
- Key::Enter => match app.focus_type {
- FocusType::Connections => {
- app.selected_database.select(Some(0));
- create_connection::handler(key, app).await?;
- database_list::handler(key, app).await?;
+ FocusBlock::TableList(focused) => table_list::handler(key, app, focused).await?,
+ FocusBlock::RecordTable(focused) => {
+ record_table::handler(key, app, focused).await?
}
- FocusType::Records(false) => app.focus_type = FocusType::Records(true),
- FocusType::Dabatases(false) => app.focus_type = FocusType::Dabatases(true),
- FocusType::Tables(false) => app.focus_type = FocusType::Tables(true),
- _ => (),
- },
- _ => {}
- },
- InputMode::Editing => match key {
- Key::Enter => {
- app.messages.push(vec![app.input.drain(..).collect()]);
}
- Key::Char(c) => {
- app.input.push(c);
+ if let Key::Char('e') = key {
+ app.input_mode = InputMode::Editing
}
+ }
+ InputMode::Editing => match key {
+ Key::Enter => query::handler(key, app).await?,
+ Key::Char(c) => app.input.push(c),
Key::Backspace => {
app.input.pop();
}
- Key::Esc => {
- app.input_mode = InputMode::Normal;
- }
+ Key::Esc => app.input_mode = InputMode::Normal,
_ => {}
},
}
diff --git a/src/handlers/query.rs b/src/handlers/query.rs
new file mode 100644
index 0000000..69e479d
--- /dev/null
+++ b/src/handlers/query.rs
@@ -0,0 +1,32 @@
+use crate::app::App;
+use crate::event::Key;
+use crate::utils::convert_column_value_to_string;
+use futures::TryStreamExt;
+use regex::Regex;
+use sqlx::Row;
+
+pub async fn handler(_key: Key, app: &mut App) -> anyhow::Result<()> {
+ app.query = app.input.drain(..).collect();
+ let re = Regex::new(r"select .+ from (.+)").unwrap();
+ if let Some(caps) = re.captures(app.query.as_str()) {
+ 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())
+ .fetch_all(app.pool.as_ref().unwrap())
+ .await?
+ .iter()
+ .map(|table| table.get(0))
+ .collect::>();
+ let mut records = vec![];
+ while let Some(row) = rows.try_next().await? {
+ records.push(
+ row.columns()
+ .iter()
+ .map(|col| convert_column_value_to_string(&row, col))
+ .collect::>(),
+ )
+ }
+ app.record_table.headers = headers;
+ app.record_table.rows = records;
+ }
+ Ok(())
+}
diff --git a/src/handlers/record_table.rs b/src/handlers/record_table.rs
index a2b3532..fae8a7c 100644
--- a/src/handlers/record_table.rs
+++ b/src/handlers/record_table.rs
@@ -1,14 +1,22 @@
-use crate::app::App;
+use crate::app::{App, FocusBlock};
use crate::event::Key;
-use crate::utils::get_records;
-pub async fn handler(_key: Key, app: &mut App) -> anyhow::Result<()> {
- if let Some(database) = app.selected_database() {
- if let Some(table) = app.selected_table() {
- let (headers, records) =
- get_records(database, table, app.pool.as_ref().unwrap()).await?;
- app.record_table.headers = headers;
- app.record_table.rows = records;
+pub async fn handler(key: Key, app: &mut App, focused: bool) -> anyhow::Result<()> {
+ if focused {
+ 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::Esc => app.focus_type = FocusBlock::RecordTable(false),
+ _ => (),
+ }
+ } else {
+ match key {
+ Key::Char('h') => app.focus_type = FocusBlock::TableList(false),
+ Key::Char('c') => app.focus_type = FocusBlock::ConnectionList,
+ Key::Enter => app.focus_type = FocusBlock::RecordTable(true),
+ _ => (),
}
}
Ok(())
diff --git a/src/handlers/table_list.rs b/src/handlers/table_list.rs
new file mode 100644
index 0000000..284b645
--- /dev/null
+++ b/src/handlers/table_list.rs
@@ -0,0 +1,51 @@
+use crate::app::{App, FocusBlock};
+use crate::event::Key;
+use crate::utils::get_records;
+
+pub async fn handler(key: Key, app: &mut App, focused: bool) -> anyhow::Result<()> {
+ if focused {
+ match key {
+ Key::Char('j') => {
+ if app.selected_database.selected().is_some() {
+ app.record_table.column_index = 0;
+ app.next_table();
+ if let Some(database) = app.selected_database() {
+ if let Some(table) = app.selected_table() {
+ let (headers, records) =
+ get_records(database, table, app.pool.as_ref().unwrap()).await?;
+ app.record_table.state.select(Some(0));
+ app.record_table.headers = headers;
+ app.record_table.rows = records;
+ }
+ }
+ }
+ }
+ Key::Char('k') => {
+ if app.selected_database.selected().is_some() {
+ app.record_table.column_index = 0;
+ app.previous_table();
+ if let Some(database) = app.selected_database() {
+ if let Some(table) = app.selected_table() {
+ let (headers, records) =
+ get_records(database, table, app.pool.as_ref().unwrap()).await?;
+ app.record_table.state.select(Some(0));
+ app.record_table.headers = headers;
+ app.record_table.rows = records;
+ }
+ }
+ }
+ }
+ Key::Esc => app.focus_type = FocusBlock::TableList(false),
+ _ => (),
+ }
+ } else {
+ match key {
+ Key::Char('k') => app.focus_type = FocusBlock::DabataseList(false),
+ Key::Char('l') => app.focus_type = FocusBlock::RecordTable(false),
+ Key::Char('c') => app.focus_type = FocusBlock::ConnectionList,
+ Key::Enter => app.focus_type = FocusBlock::TableList(true),
+ _ => (),
+ }
+ }
+ Ok(())
+}
diff --git a/src/main.rs b/src/main.rs
index 066079f..7a32790 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -5,7 +5,7 @@ mod ui;
mod user_config;
mod utils;
-use crate::app::FocusType;
+use crate::app::{App, FocusBlock};
use crate::event::{Event, Key};
use crate::handlers::handle_app;
use crossterm::{
@@ -13,7 +13,6 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
-use sqlx::mysql::MySqlPool;
use std::io::stdout;
use tui::{backend::CrosstermBackend, Terminal};
@@ -21,7 +20,7 @@ use tui::{backend::CrosstermBackend, Terminal};
async fn main() -> anyhow::Result<()> {
enable_raw_mode()?;
- let config = user_config::UserConfig::new("sample.toml").unwrap();
+ let user_config = user_config::UserConfig::new("sample.toml").ok();
let mut stdout = stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
@@ -30,32 +29,11 @@ async fn main() -> anyhow::Result<()> {
let mut terminal = Terminal::new(backend)?;
let events = event::Events::new(250);
- let mut app = &mut app::App::default();
- app.user_config = Some(config);
- let conn = &app.user_config.as_ref().unwrap().conn.get(0).unwrap();
- let pool = MySqlPool::connect(
- format!(
- "mysql://{user}:@{host}:{port}",
- user = conn.user,
- host = conn.host,
- port = conn.port
- )
- .as_str(),
- )
- .await?;
- app.pool = Some(pool);
-
- app.databases = utils::get_databases(app.pool.as_ref().unwrap()).await?;
- let (headers, records) = utils::get_records(
- app.databases.first().unwrap(),
- app.databases.first().unwrap().tables.first().unwrap(),
- app.pool.as_ref().unwrap(),
- )
- .await?;
- app.record_table.rows = records;
- app.record_table.headers = headers;
- app.selected_database.select(Some(0));
- app.focus_type = FocusType::Connections;
+ let mut app = App {
+ user_config,
+ focus_type: FocusBlock::ConnectionList,
+ ..App::default()
+ };
terminal.clear()?;
@@ -66,7 +44,7 @@ async fn main() -> anyhow::Result<()> {
if key == Key::Char('q') {
break;
};
- handle_app(key, app).await?
+ handle_app(key, &mut app).await?
}
Event::Tick => (),
}
diff --git a/src/ui/mod.rs b/src/ui/mod.rs
index d24a9ca..be806d3 100644
--- a/src/ui/mod.rs
+++ b/src/ui/mod.rs
@@ -1,5 +1,5 @@
use crate::app::InputMode;
-use crate::app::{App, FocusType};
+use crate::app::{App, FocusBlock};
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout},
@@ -11,7 +11,7 @@ use tui::{
use unicode_width::UnicodeWidthStr;
pub fn draw(f: &mut Frame<'_, B>, app: &mut App) -> anyhow::Result<()> {
- if let FocusType::Connections = app.focus_type {
+ if let FocusBlock::ConnectionList = app.focus_type {
let percent_x = 60;
let percent_y = 50;
let conns = &app.user_config.as_ref().unwrap().conn;
@@ -26,7 +26,7 @@ pub fn draw(f: &mut Frame<'_, B>, app: &mut App) -> anyhow::Result<(
.block(Block::default().borders(Borders::ALL).title("Connections"))
.highlight_style(Style::default().fg(Color::Green))
.style(match app.focus_type {
- FocusType::Connections => Style::default().fg(Color::Green),
+ FocusBlock::ConnectionList => Style::default().fg(Color::Green),
_ => Style::default(),
});
let popup_layout = Layout::default()
@@ -58,15 +58,20 @@ pub fn draw(f: &mut Frame<'_, B>, app: &mut App) -> anyhow::Result<(
}
let main_chunks = Layout::default()
- .direction(Direction::Vertical)
.margin(2)
- .constraints([Constraint::Percentage(15), Constraint::Percentage(85)])
.direction(Direction::Horizontal)
+ .constraints([Constraint::Percentage(15), Constraint::Percentage(85)])
.split(f.size());
let left_chunks = Layout::default()
- .direction(Direction::Vertical)
- .constraints([Constraint::Percentage(30), Constraint::Percentage(70)].as_ref())
+ .constraints(
+ [
+ Constraint::Length(9),
+ Constraint::Min(8),
+ Constraint::Length(7),
+ ]
+ .as_ref(),
+ )
.split(main_chunks[0]);
let databases: Vec = app
.databases
@@ -80,8 +85,8 @@ pub fn draw(f: &mut Frame<'_, B>, app: &mut App) -> anyhow::Result<(
.block(Block::default().borders(Borders::ALL).title("Databases"))
.highlight_style(Style::default().fg(Color::Green))
.style(match app.focus_type {
- FocusType::Dabatases(false) => Style::default().fg(Color::Magenta),
- FocusType::Dabatases(true) => Style::default().fg(Color::Green),
+ FocusBlock::DabataseList(false) => Style::default().fg(Color::Magenta),
+ FocusBlock::DabataseList(true) => Style::default().fg(Color::Green),
_ => Style::default(),
});
f.render_stateful_widget(tasks, left_chunks[0], &mut app.selected_database);
@@ -99,28 +104,46 @@ pub fn draw(f: &mut Frame<'_, B>, app: &mut App) -> anyhow::Result<(
.block(Block::default().borders(Borders::ALL).title("Tables"))
.highlight_style(Style::default().fg(Color::Green))
.style(match app.focus_type {
- FocusType::Tables(false) => Style::default().fg(Color::Magenta),
- FocusType::Tables(true) => Style::default().fg(Color::Green),
+ FocusBlock::TableList(false) => Style::default().fg(Color::Magenta),
+ FocusBlock::TableList(true) => Style::default().fg(Color::Green),
_ => Style::default(),
});
- f.render_stateful_widget(
- tasks,
- left_chunks[1],
- &mut app.databases[app.selected_database.selected().unwrap_or(0)].selected_table,
- );
+ f.render_stateful_widget(tasks, left_chunks[1], &mut app.selected_table);
+
+ let info: Vec = vec![
+ format!(
+ "created: {}",
+ app.selected_table().unwrap().create_time.to_string()
+ ),
+ // format!(
+ // "updated: {}",
+ // app.selected_table().unwrap().update_time.to_string()
+ // ),
+ format!("rows: {}", app.record_table.rows.len()),
+ ]
+ .iter()
+ .map(|i| {
+ ListItem::new(vec![Spans::from(Span::raw(i.to_string()))])
+ .style(Style::default().fg(Color::White))
+ })
+ .collect();
+ let tasks = List::new(info)
+ .block(Block::default().borders(Borders::ALL))
+ .highlight_style(Style::default().fg(Color::Green));
+ f.render_widget(tasks, left_chunks[2]);
let right_chunks = Layout::default()
.direction(Direction::Vertical)
- .constraints([Constraint::Length(3), Constraint::Min(1)].as_ref())
+ .constraints([Constraint::Length(3), Constraint::Length(5)].as_ref())
.split(main_chunks[1]);
- let input = Paragraph::new(app.input.as_ref())
+ let query = Paragraph::new(app.input.as_ref())
.style(match app.input_mode {
InputMode::Normal => Style::default(),
InputMode::Editing => Style::default().fg(Color::Yellow),
})
.block(Block::default().borders(Borders::ALL).title("Query"));
- f.render_widget(input, right_chunks[0]);
+ f.render_widget(query, right_chunks[0]);
match app.input_mode {
InputMode::Normal => (),
InputMode::Editing => f.set_cursor(
@@ -153,8 +176,8 @@ pub fn draw(f: &mut Frame<'_, B>, app: &mut App) -> anyhow::Result<(
.block(Block::default().borders(Borders::ALL).title("Records"))
.highlight_style(Style::default().fg(Color::Green))
.style(match app.focus_type {
- FocusType::Records(false) => Style::default().fg(Color::Magenta),
- FocusType::Records(true) => Style::default().fg(Color::Green),
+ FocusBlock::RecordTable(false) => Style::default().fg(Color::Magenta),
+ FocusBlock::RecordTable(true) => Style::default().fg(Color::Green),
_ => Style::default(),
})
.widths(&widths);
diff --git a/src/utils.rs b/src/utils.rs
index 6045bf6..49018dc 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -1,11 +1,11 @@
use crate::app::{Database, Table};
use chrono::NaiveDate;
use futures::TryStreamExt;
-use sqlx::mysql::MySqlPool;
-use sqlx::{Column, Executor, Row, TypeInfo};
+use sqlx::mysql::{MySqlColumn, MySqlPool, MySqlRow};
+use sqlx::{Column, Row, TypeInfo};
pub async fn get_databases(pool: &MySqlPool) -> anyhow::Result> {
- let databases = sqlx::query("show databases")
+ let databases = sqlx::query("SHOW DATABASES")
.fetch_all(pool)
.await?
.iter()
@@ -19,12 +19,10 @@ pub async fn get_databases(pool: &MySqlPool) -> anyhow::Result> {
}
pub async fn get_tables(database: String, pool: &MySqlPool) -> anyhow::Result> {
- let tables = sqlx::query(format!("show tables from `{}`", database).as_str())
- .fetch_all(pool)
- .await?
- .iter()
- .map(|table| Table { name: table.get(0) })
- .collect::>();
+ let tables =
+ sqlx::query_as::<_, Table>(format!("SHOW TABLE STATUS FROM `{}`", database).as_str())
+ .fetch_all(pool)
+ .await?;
Ok(tables)
}
@@ -33,10 +31,8 @@ pub async fn get_records(
table: &Table,
pool: &MySqlPool,
) -> anyhow::Result<(Vec, Vec>)> {
- pool.execute(format!("use `{}`", database.name).as_str())
- .await?;
- let table_name = format!("SELECT * FROM `{}`", table.name);
- let mut rows = sqlx::query(table_name.as_str()).fetch(pool);
+ let query = format!("SELECT * FROM `{}`.`{}`", database.name, table.name);
+ let mut rows = sqlx::query(query.as_str()).fetch(pool);
let headers = sqlx::query(format!("desc `{}`", table.name).as_str())
.fetch_all(pool)
.await?
@@ -45,57 +41,55 @@ pub async fn get_records(
.collect::>();
let mut records = vec![];
while let Some(row) = rows.try_next().await? {
- let mut row_vec = vec![];
- for col in row.columns() {
- let col_name = col.name();
- match col.type_info().clone().name() {
- "INT" | "DECIMAL" | "SMALLINT" => match row.try_get(col_name) {
- Ok(value) => {
- let value: i64 = value;
- row_vec.push(value.to_string())
- }
- Err(_) => row_vec.push("".to_string()),
- },
- "INT UNSIGNED" => match row.try_get(col_name) {
- Ok(value) => {
- let value: u64 = value;
- row_vec.push(value.to_string())
- }
- Err(_) => row_vec.push("".to_string()),
- },
- "VARCHAR" | "CHAR" => {
- let value: String = row.try_get(col_name).unwrap_or("".to_string());
- row_vec.push(value);
- }
- "DATE" => match row.try_get(col_name) {
- Ok(value) => {
- let value: NaiveDate = value;
- row_vec.push(value.to_string())
- }
- Err(_) => row_vec.push("".to_string()),
- },
- "TIMESTAMP" => match row.try_get(col_name) {
- Ok(value) => {
- let value: chrono::DateTime = value;
- row_vec.push(value.to_string())
- }
- Err(_) => row_vec.push("".to_string()),
- },
- "BOOLEAN" => match row.try_get(col_name) {
- Ok(value) => {
- let value: bool = value;
- row_vec.push(value.to_string())
- }
- Err(_) => row_vec.push("".to_string()),
- },
- "ENUM" => {
- let value: String = row.try_get(col_name).unwrap_or("".to_string());
- row_vec.push(value);
- }
- _ => (),
- }
- }
- records.push(row_vec)
+ records.push(
+ row.columns()
+ .iter()
+ .map(|col| convert_column_value_to_string(&row, col))
+ .collect::>(),
+ )
}
Ok((headers, records))
}
+
+pub fn convert_column_value_to_string(row: &MySqlRow, column: &MySqlColumn) -> String {
+ let column_name = column.name();
+ match column.type_info().clone().name() {
+ "INT" | "DECIMAL" | "SMALLINT" => match row.try_get(column_name) {
+ Ok(value) => {
+ let value: i64 = value;
+ value.to_string()
+ }
+ Err(_) => "".to_string(),
+ },
+ "INT UNSIGNED" => match row.try_get(column_name) {
+ Ok(value) => {
+ let value: u64 = value;
+ value.to_string()
+ }
+ Err(_) => "".to_string(),
+ },
+ "VARCHAR" | "CHAR" | "ENUM" => row.try_get(column_name).unwrap_or_else(|_| "".to_string()),
+ "DATE" => match row.try_get(column_name) {
+ Ok(value) => {
+ let value: NaiveDate = value;
+ value.to_string()
+ }
+ Err(_) => "".to_string(),
+ },
+ "TIMESTAMP" => match row.try_get(column_name) {
+ Ok(value) => {
+ let value: chrono::DateTime = value;
+ value.to_string()
+ }
+ Err(_) => "".to_string(),
+ },
+ "BOOLEAN" => match row.try_get(column_name) {
+ Ok(value) => {
+ let value: bool = value;
+ value.to_string()
+ }
+ Err(_) => "".to_string(),
+ },
+ _ => "".to_string(),
+ }
+}