implement query widget (#8)

pull/9/head
Takayuki Maeda 3 years ago committed by GitHub
parent 4b26b71a38
commit b8c36b5f94
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -4,17 +4,14 @@ use crate::{
}; };
use sqlx::mysql::MySqlPool; use sqlx::mysql::MySqlPool;
use tui::widgets::{ListState, TableState}; use tui::widgets::{ListState, TableState};
use unicode_width::UnicodeWidthStr;
pub enum InputMode {
Normal,
Editing,
}
pub enum FocusBlock { pub enum FocusBlock {
DabataseList(bool), DabataseList(bool),
TableList(bool), TableList(bool),
RecordTable(bool), RecordTable(bool),
ConnectionList, ConnectionList,
Query(bool),
} }
#[derive(Clone)] #[derive(Clone)]
@ -32,7 +29,7 @@ pub struct Table {
#[sqlx(rename = "Update_time")] #[sqlx(rename = "Update_time")]
pub update_time: Option<chrono::DateTime<chrono::Utc>>, pub update_time: Option<chrono::DateTime<chrono::Utc>>,
#[sqlx(rename = "Engine")] #[sqlx(rename = "Engine")]
pub engine: String, pub engine: Option<String>,
} }
pub struct RecordTable { pub struct RecordTable {
@ -106,32 +103,34 @@ impl Database {
pub struct App { pub struct App {
pub input: String, pub input: String,
pub input_mode: InputMode, pub input_cursor_x: u16,
pub query: String, pub query: String,
pub databases: Vec<Database>, pub databases: Vec<Database>,
pub record_table: RecordTable, pub record_table: RecordTable,
pub focus_type: FocusBlock, pub focus_block: FocusBlock,
pub user_config: Option<UserConfig>, pub user_config: Option<UserConfig>,
pub selected_connection: ListState, pub selected_connection: ListState,
pub selected_database: ListState, pub selected_database: ListState,
pub selected_table: ListState, pub selected_table: ListState,
pub pool: Option<MySqlPool>, pub pool: Option<MySqlPool>,
pub error: Option<String>,
} }
impl Default for App { impl Default for App {
fn default() -> App { fn default() -> App {
App { App {
input: String::new(), input: String::new(),
input_mode: InputMode::Normal, input_cursor_x: 0,
query: String::new(), query: String::new(),
databases: Vec::new(), databases: Vec::new(),
record_table: RecordTable::default(), record_table: RecordTable::default(),
focus_type: FocusBlock::DabataseList(false), focus_block: FocusBlock::DabataseList(false),
user_config: None, user_config: None,
selected_connection: ListState::default(), selected_connection: ListState::default(),
selected_database: ListState::default(), selected_database: ListState::default(),
selected_table: ListState::default(), selected_table: ListState::default(),
pool: None, pool: None,
error: None,
} }
} }
} }
@ -227,6 +226,18 @@ impl App {
} }
} }
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;
}
}
pub fn selected_database(&self) -> Option<&Database> { pub fn selected_database(&self) -> Option<&Database> {
match self.selected_database.selected() { match self.selected_database.selected() {
Some(i) => self.databases.get(i), Some(i) => self.databases.get(i),
@ -253,4 +264,29 @@ impl App {
None => None, None => None,
} }
} }
pub fn table_status(&self) -> Vec<String> {
if let Some(table) = self.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()
}
} }

@ -10,13 +10,14 @@ pub async fn handler(key: Key, app: &mut App) -> anyhow::Result<()> {
Key::Enter => { Key::Enter => {
app.selected_database.select(Some(0)); app.selected_database.select(Some(0));
app.selected_table.select(Some(0)); app.selected_table.select(Some(0));
app.record_table.state.select(Some(0));
if let Some(conn) = app.selected_connection() { if let Some(conn) = app.selected_connection() {
if let Some(pool) = app.pool.as_ref() { if let Some(pool) = app.pool.as_ref() {
pool.close().await; pool.close().await;
} }
let pool = MySqlPool::connect(conn.database_url().as_str()).await?; let pool = MySqlPool::connect(conn.database_url().as_str()).await?;
app.pool = Some(pool); app.pool = Some(pool);
app.focus_type = FocusBlock::DabataseList(false); app.focus_block = FocusBlock::DabataseList(false);
} }
app.databases = match app.selected_connection() { app.databases = match app.selected_connection() {
Some(conn) => match &conn.database { Some(conn) => match &conn.database {

@ -6,15 +6,15 @@ pub async fn handler(key: Key, app: &mut App, focused: bool) -> anyhow::Result<(
match key { match key {
Key::Char('j') => app.next_database(), Key::Char('j') => app.next_database(),
Key::Char('k') => app.previous_database(), Key::Char('k') => app.previous_database(),
Key::Esc => app.focus_type = FocusBlock::DabataseList(false), Key::Esc => app.focus_block = FocusBlock::DabataseList(false),
_ => (), _ => (),
} }
} else { } else {
match key { match key {
Key::Char('j') => app.focus_type = FocusBlock::TableList(false), Key::Char('j') => app.focus_block = FocusBlock::TableList(false),
Key::Char('l') => app.focus_type = FocusBlock::RecordTable(false), Key::Char('l') => app.focus_block = FocusBlock::RecordTable(false),
Key::Char('c') => app.focus_type = FocusBlock::ConnectionList, Key::Char('c') => app.focus_block = FocusBlock::ConnectionList,
Key::Enter => app.focus_type = FocusBlock::DabataseList(true), Key::Enter => app.focus_block = FocusBlock::DabataseList(true),
_ => (), _ => (),
} }
} }

@ -4,35 +4,33 @@ pub mod query;
pub mod record_table; pub mod record_table;
pub mod table_list; pub mod table_list;
use crate::app::{App, FocusBlock, InputMode}; use crate::app::{App, FocusBlock};
use crate::event::Key; use crate::event::Key;
pub async fn handle_app(key: Key, app: &mut App) -> anyhow::Result<()> { pub async fn handle_app(key: Key, app: &mut App) -> anyhow::Result<()> {
match app.input_mode { match app.focus_block {
InputMode::Normal => { FocusBlock::ConnectionList => connection_list::handler(key, app).await?,
match app.focus_type { FocusBlock::DabataseList(focused) => database_list::handler(key, app, focused).await?,
FocusBlock::ConnectionList => connection_list::handler(key, app).await?, FocusBlock::TableList(focused) => table_list::handler(key, app, focused).await?,
FocusBlock::DabataseList(focused) => { FocusBlock::RecordTable(focused) => record_table::handler(key, app, focused).await?,
database_list::handler(key, app, focused).await? FocusBlock::Query(focused) => query::handler(key, app, focused).await?,
} }
FocusBlock::TableList(focused) => table_list::handler(key, app, focused).await?, match key {
FocusBlock::RecordTable(focused) => { Key::Char('d') => match app.focus_block {
record_table::handler(key, app, focused).await? FocusBlock::Query(true) => (),
} _ => app.focus_block = FocusBlock::DabataseList(true),
} },
if let Key::Char('e') = key { Key::Char('t') => match app.focus_block {
app.input_mode = InputMode::Editing FocusBlock::Query(true) => (),
} _ => app.focus_block = FocusBlock::TableList(true),
} },
InputMode::Editing => match key { Key::Char('r') => match app.focus_block {
Key::Enter => query::handler(key, app).await?, FocusBlock::Query(true) => (),
Key::Char(c) => app.input.push(c), _ => app.focus_block = FocusBlock::RecordTable(true),
Key::Backspace => {
app.input.pop();
}
Key::Esc => app.input_mode = InputMode::Normal,
_ => {}
}, },
Key::Char('e') => app.focus_block = FocusBlock::Query(true),
Key::Esc => app.error = None,
_ => (),
} }
Ok(()) Ok(())
} }

@ -1,32 +1,74 @@
use crate::app::App; use crate::app::{App, FocusBlock};
use crate::event::Key; use crate::event::Key;
use crate::utils::convert_column_value_to_string; use crate::utils::convert_column_value_to_string;
use futures::TryStreamExt; use futures::TryStreamExt;
use regex::Regex; use regex::Regex;
use sqlx::Row; use sqlx::Row;
use unicode_width::UnicodeWidthStr;
pub async fn handler(_key: Key, app: &mut App) -> anyhow::Result<()> { pub async fn handler(key: Key, app: &mut App, focused: bool) -> anyhow::Result<()> {
app.query = app.input.drain(..).collect(); if focused {
let re = Regex::new(r"select .+ from (.+)").unwrap(); match key {
if let Some(caps) = re.captures(app.query.as_str()) { Key::Enter => {
let mut rows = sqlx::query(app.query.as_str()).fetch(app.pool.as_ref().unwrap()); app.query = app.input.drain(..).collect();
let headers = sqlx::query(format!("desc `{}`", caps.get(1).unwrap().as_str()).as_str()) let re = Regex::new(r"select .+ from ([^ ]+).*").unwrap();
.fetch_all(app.pool.as_ref().unwrap()) match re.captures(app.query.as_str()) {
.await? Some(caps) => {
.iter() let mut rows =
.map(|table| table.get(0)) sqlx::query(app.query.as_str()).fetch(app.pool.as_ref().unwrap());
.collect::<Vec<String>>(); let headers = sqlx::query(
let mut records = vec![]; format!("desc `{}`", caps.get(1).unwrap().as_str()).as_str(),
while let Some(row) = rows.try_next().await? { )
records.push( .fetch_all(app.pool.as_ref().unwrap())
row.columns() .await?
.iter() .iter()
.map(|col| convert_column_value_to_string(&row, col)) .map(|table| table.get(0))
.collect::<Vec<String>>(), .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?;
}
}
}
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(false),
_ => {}
}
} else {
match key {
Key::Char('h') => app.focus_block = FocusBlock::DabataseList(false),
Key::Char('j') => app.focus_block = FocusBlock::RecordTable(false),
Key::Char('c') => app.focus_block = FocusBlock::ConnectionList,
Key::Enter => app.focus_block = FocusBlock::Query(true),
_ => (),
} }
app.record_table.headers = headers;
app.record_table.rows = records;
} }
Ok(()) Ok(())
} }

@ -8,14 +8,14 @@ pub async fn handler(key: Key, app: &mut App, focused: bool) -> anyhow::Result<(
Key::Char('j') => app.record_table.next(), Key::Char('j') => app.record_table.next(),
Key::Char('k') => app.record_table.previous(), Key::Char('k') => app.record_table.previous(),
Key::Char('l') => app.record_table.next_column(), Key::Char('l') => app.record_table.next_column(),
Key::Esc => app.focus_type = FocusBlock::RecordTable(false), Key::Esc => app.focus_block = FocusBlock::RecordTable(false),
_ => (), _ => (),
} }
} else { } else {
match key { match key {
Key::Char('h') => app.focus_type = FocusBlock::TableList(false), Key::Char('h') => app.focus_block = FocusBlock::TableList(false),
Key::Char('c') => app.focus_type = FocusBlock::ConnectionList, Key::Char('c') => app.focus_block = FocusBlock::ConnectionList,
Key::Enter => app.focus_type = FocusBlock::RecordTable(true), Key::Enter => app.focus_block = FocusBlock::RecordTable(true),
_ => (), _ => (),
} }
} }

@ -35,15 +35,15 @@ pub async fn handler(key: Key, app: &mut App, focused: bool) -> anyhow::Result<(
} }
} }
} }
Key::Esc => app.focus_type = FocusBlock::TableList(false), Key::Esc => app.focus_block = FocusBlock::TableList(false),
_ => (), _ => (),
} }
} else { } else {
match key { match key {
Key::Char('k') => app.focus_type = FocusBlock::DabataseList(false), Key::Char('k') => app.focus_block = FocusBlock::DabataseList(false),
Key::Char('l') => app.focus_type = FocusBlock::RecordTable(false), Key::Char('l') => app.focus_block = FocusBlock::RecordTable(false),
Key::Char('c') => app.focus_type = FocusBlock::ConnectionList, Key::Char('c') => app.focus_block = FocusBlock::ConnectionList,
Key::Enter => app.focus_type = FocusBlock::TableList(true), Key::Enter => app.focus_block = FocusBlock::TableList(true),
_ => (), _ => (),
} }
} }

@ -31,7 +31,7 @@ async fn main() -> anyhow::Result<()> {
let mut app = App { let mut app = App {
user_config, user_config,
focus_type: FocusBlock::ConnectionList, focus_block: FocusBlock::ConnectionList,
..App::default() ..App::default()
}; };
@ -44,7 +44,10 @@ async fn main() -> anyhow::Result<()> {
if key == Key::Char('q') { if key == Key::Char('q') {
break; break;
}; };
handle_app(key, &mut app).await? match handle_app(key, &mut app).await {
Ok(_) => (),
Err(err) => app.error = Some(err.to_string()),
}
} }
Event::Tick => (), Event::Tick => (),
} }

@ -1,4 +1,3 @@
use crate::app::InputMode;
use crate::app::{App, FocusBlock}; use crate::app::{App, FocusBlock};
use tui::{ use tui::{
backend::Backend, backend::Backend,
@ -11,7 +10,7 @@ use tui::{
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
pub fn draw<B: Backend>(f: &mut Frame<'_, B>, app: &mut App) -> anyhow::Result<()> { pub fn draw<B: Backend>(f: &mut Frame<'_, B>, app: &mut App) -> anyhow::Result<()> {
if let FocusBlock::ConnectionList = app.focus_type { if let FocusBlock::ConnectionList = app.focus_block {
let percent_x = 60; let percent_x = 60;
let percent_y = 50; let percent_y = 50;
let conns = &app.user_config.as_ref().unwrap().conn; let conns = &app.user_config.as_ref().unwrap().conn;
@ -25,7 +24,7 @@ pub fn draw<B: Backend>(f: &mut Frame<'_, B>, app: &mut App) -> anyhow::Result<(
let tasks = List::new(connections) let tasks = List::new(connections)
.block(Block::default().borders(Borders::ALL).title("Connections")) .block(Block::default().borders(Borders::ALL).title("Connections"))
.highlight_style(Style::default().fg(Color::Green)) .highlight_style(Style::default().fg(Color::Green))
.style(match app.focus_type { .style(match app.focus_block {
FocusBlock::ConnectionList => Style::default().fg(Color::Green), FocusBlock::ConnectionList => Style::default().fg(Color::Green),
_ => Style::default(), _ => Style::default(),
}); });
@ -84,7 +83,7 @@ pub fn draw<B: Backend>(f: &mut Frame<'_, B>, app: &mut App) -> anyhow::Result<(
let tasks = List::new(databases) let tasks = List::new(databases)
.block(Block::default().borders(Borders::ALL).title("Databases")) .block(Block::default().borders(Borders::ALL).title("Databases"))
.highlight_style(Style::default().fg(Color::Green)) .highlight_style(Style::default().fg(Color::Green))
.style(match app.focus_type { .style(match app.focus_block {
FocusBlock::DabataseList(false) => Style::default().fg(Color::Magenta), FocusBlock::DabataseList(false) => Style::default().fg(Color::Magenta),
FocusBlock::DabataseList(true) => Style::default().fg(Color::Green), FocusBlock::DabataseList(true) => Style::default().fg(Color::Green),
_ => Style::default(), _ => Style::default(),
@ -103,31 +102,22 @@ pub fn draw<B: Backend>(f: &mut Frame<'_, B>, app: &mut App) -> anyhow::Result<(
let tasks = List::new(tables) let tasks = List::new(tables)
.block(Block::default().borders(Borders::ALL).title("Tables")) .block(Block::default().borders(Borders::ALL).title("Tables"))
.highlight_style(Style::default().fg(Color::Green)) .highlight_style(Style::default().fg(Color::Green))
.style(match app.focus_type { .style(match app.focus_block {
FocusBlock::TableList(false) => Style::default().fg(Color::Magenta), FocusBlock::TableList(false) => Style::default().fg(Color::Magenta),
FocusBlock::TableList(true) => Style::default().fg(Color::Green), FocusBlock::TableList(true) => Style::default().fg(Color::Green),
_ => Style::default(), _ => Style::default(),
}); });
f.render_stateful_widget(tasks, left_chunks[1], &mut app.selected_table); f.render_stateful_widget(tasks, left_chunks[1], &mut app.selected_table);
let info: Vec<ListItem> = vec![ let table_status: Vec<ListItem> = app
format!( .table_status()
"created: {}", .iter()
app.selected_table().unwrap().create_time.to_string() .map(|i| {
), ListItem::new(vec![Spans::from(Span::raw(i.to_string()))])
// format!( .style(Style::default().fg(Color::White))
// "updated: {}", })
// app.selected_table().unwrap().update_time.to_string() .collect();
// ), let tasks = List::new(table_status)
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)) .block(Block::default().borders(Borders::ALL))
.highlight_style(Style::default().fg(Color::Green)); .highlight_style(Style::default().fg(Color::Green));
f.render_widget(tasks, left_chunks[2]); f.render_widget(tasks, left_chunks[2]);
@ -138,18 +128,18 @@ pub fn draw<B: Backend>(f: &mut Frame<'_, B>, app: &mut App) -> anyhow::Result<(
.split(main_chunks[1]); .split(main_chunks[1]);
let query = Paragraph::new(app.input.as_ref()) let query = Paragraph::new(app.input.as_ref())
.style(match app.input_mode { .style(match app.focus_block {
InputMode::Normal => Style::default(), FocusBlock::Query(true) => Style::default().fg(Color::Green),
InputMode::Editing => Style::default().fg(Color::Yellow), FocusBlock::Query(false) => Style::default().fg(Color::Magenta),
_ => Style::default(),
}) })
.block(Block::default().borders(Borders::ALL).title("Query")); .block(Block::default().borders(Borders::ALL).title("Query"));
f.render_widget(query, right_chunks[0]); f.render_widget(query, right_chunks[0]);
match app.input_mode { if let FocusBlock::Query(true) = app.focus_block {
InputMode::Normal => (), f.set_cursor(
InputMode::Editing => f.set_cursor( right_chunks[0].x + app.input.width() as u16 + 1 - app.input_cursor_x,
right_chunks[0].x + app.input.width() as u16 + 1,
right_chunks[0].y + 1, right_chunks[0].y + 1,
), )
} }
let header_cells = app.record_table.headers[app.record_table.column_index..] let header_cells = app.record_table.headers[app.record_table.column_index..]
@ -175,7 +165,7 @@ pub fn draw<B: Backend>(f: &mut Frame<'_, B>, app: &mut App) -> anyhow::Result<(
.header(header) .header(header)
.block(Block::default().borders(Borders::ALL).title("Records")) .block(Block::default().borders(Borders::ALL).title("Records"))
.highlight_style(Style::default().fg(Color::Green)) .highlight_style(Style::default().fg(Color::Green))
.style(match app.focus_type { .style(match app.focus_block {
FocusBlock::RecordTable(false) => Style::default().fg(Color::Magenta), FocusBlock::RecordTable(false) => Style::default().fg(Color::Magenta),
FocusBlock::RecordTable(true) => Style::default().fg(Color::Green), FocusBlock::RecordTable(true) => Style::default().fg(Color::Green),
_ => Style::default(), _ => Style::default(),
@ -183,5 +173,42 @@ pub fn draw<B: Backend>(f: &mut Frame<'_, B>, app: &mut App) -> anyhow::Result<(
.widths(&widths); .widths(&widths);
f.render_stateful_widget(t, right_chunks[1], &mut app.record_table.state); f.render_stateful_widget(t, right_chunks[1], &mut app.record_table.state);
if let Some(err) = app.error.clone() {
draw_error_popup(f, err)?;
}
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(()) Ok(())
} }

@ -33,12 +33,13 @@ pub async fn get_records(
) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)> { ) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)> {
let query = format!("SELECT * FROM `{}`.`{}`", database.name, table.name); let query = format!("SELECT * FROM `{}`.`{}`", database.name, table.name);
let mut rows = sqlx::query(query.as_str()).fetch(pool); let mut rows = sqlx::query(query.as_str()).fetch(pool);
let headers = sqlx::query(format!("desc `{}`", table.name).as_str()) let headers =
.fetch_all(pool) sqlx::query(format!("SHOW COLUMNS FROM `{}`.`{}`", database.name, table.name).as_str())
.await? .fetch_all(pool)
.iter() .await?
.map(|table| table.get(0)) .iter()
.collect::<Vec<String>>(); .map(|table| table.get(0))
.collect::<Vec<String>>();
let mut records = vec![]; let mut records = vec![];
while let Some(row) = rows.try_next().await? { while let Some(row) = rows.try_next().await? {
records.push( records.push(

Loading…
Cancel
Save