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

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

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

@ -1,32 +1,74 @@
use crate::app::App;
use crate::app::{App, FocusBlock};
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<()> {
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::<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>>(),
)
pub async fn handler(key: Key, app: &mut App, focused: bool) -> anyhow::Result<()> {
if focused {
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(),
)
.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?;
}
}
}
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(())
}

@ -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('k') => app.record_table.previous(),
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 {
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),
Key::Char('h') => app.focus_block = FocusBlock::TableList(false),
Key::Char('c') => app.focus_block = FocusBlock::ConnectionList,
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 {
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),
Key::Char('k') => app.focus_block = FocusBlock::DabataseList(false),
Key::Char('l') => app.focus_block = FocusBlock::RecordTable(false),
Key::Char('c') => app.focus_block = FocusBlock::ConnectionList,
Key::Enter => app.focus_block = FocusBlock::TableList(true),
_ => (),
}
}

@ -31,7 +31,7 @@ async fn main() -> anyhow::Result<()> {
let mut app = App {
user_config,
focus_type: FocusBlock::ConnectionList,
focus_block: FocusBlock::ConnectionList,
..App::default()
};
@ -44,7 +44,10 @@ async fn main() -> anyhow::Result<()> {
if key == Key::Char('q') {
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 => (),
}

@ -1,4 +1,3 @@
use crate::app::InputMode;
use crate::app::{App, FocusBlock};
use tui::{
backend::Backend,
@ -11,7 +10,7 @@ use tui::{
use unicode_width::UnicodeWidthStr;
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_y = 50;
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)
.block(Block::default().borders(Borders::ALL).title("Connections"))
.highlight_style(Style::default().fg(Color::Green))
.style(match app.focus_type {
.style(match app.focus_block {
FocusBlock::ConnectionList => Style::default().fg(Color::Green),
_ => 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)
.block(Block::default().borders(Borders::ALL).title("Databases"))
.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(true) => Style::default().fg(Color::Green),
_ => 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)
.block(Block::default().borders(Borders::ALL).title("Tables"))
.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(true) => Style::default().fg(Color::Green),
_ => Style::default(),
});
f.render_stateful_widget(tasks, left_chunks[1], &mut app.selected_table);
let info: Vec<ListItem> = 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)
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[2]);
@ -138,18 +128,18 @@ pub fn draw<B: Backend>(f: &mut Frame<'_, B>, app: &mut App) -> anyhow::Result<(
.split(main_chunks[1]);
let query = Paragraph::new(app.input.as_ref())
.style(match app.input_mode {
InputMode::Normal => Style::default(),
InputMode::Editing => Style::default().fg(Color::Yellow),
.style(match app.focus_block {
FocusBlock::Query(true) => Style::default().fg(Color::Green),
FocusBlock::Query(false) => Style::default().fg(Color::Magenta),
_ => Style::default(),
})
.block(Block::default().borders(Borders::ALL).title("Query"));
f.render_widget(query, right_chunks[0]);
match app.input_mode {
InputMode::Normal => (),
InputMode::Editing => f.set_cursor(
right_chunks[0].x + app.input.width() as u16 + 1,
if let FocusBlock::Query(true) = app.focus_block {
f.set_cursor(
right_chunks[0].x + app.input.width() as u16 + 1 - app.input_cursor_x,
right_chunks[0].y + 1,
),
)
}
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)
.block(Block::default().borders(Borders::ALL).title("Records"))
.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(true) => Style::default().fg(Color::Green),
_ => Style::default(),
@ -183,5 +173,42 @@ pub fn draw<B: Backend>(f: &mut Frame<'_, B>, app: &mut App) -> anyhow::Result<(
.widths(&widths);
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(())
}

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

Loading…
Cancel
Save