diff --git a/src/app.rs b/src/app.rs index e7f2933..d561512 100644 --- a/src/app.rs +++ b/src/app.rs @@ -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>, #[sqlx(rename = "Engine")] - pub engine: String, + pub engine: Option, } 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, pub record_table: RecordTable, - pub focus_type: FocusBlock, + pub focus_block: FocusBlock, pub user_config: Option, pub selected_connection: ListState, pub selected_database: ListState, pub selected_table: ListState, pub pool: Option, + pub error: Option, } 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 { + 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() + } } diff --git a/src/handlers/connection_list.rs b/src/handlers/connection_list.rs index fc07108..377c573 100644 --- a/src/handlers/connection_list.rs +++ b/src/handlers/connection_list.rs @@ -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 { diff --git a/src/handlers/database_list.rs b/src/handlers/database_list.rs index e5016e9..ec1bd7c 100644 --- a/src/handlers/database_list.rs +++ b/src/handlers/database_list.rs @@ -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), _ => (), } } diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 101e5a1..e69d34d 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -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(()) } diff --git a/src/handlers/query.rs b/src/handlers/query.rs index 69e479d..8003e13 100644 --- a/src/handlers/query.rs +++ b/src/handlers/query.rs @@ -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::>(); - 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::>(), - ) +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::>(); + 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; + } + 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(()) } diff --git a/src/handlers/record_table.rs b/src/handlers/record_table.rs index fae8a7c..e39817b 100644 --- a/src/handlers/record_table.rs +++ b/src/handlers/record_table.rs @@ -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), _ => (), } } diff --git a/src/handlers/table_list.rs b/src/handlers/table_list.rs index 284b645..d75d453 100644 --- a/src/handlers/table_list.rs +++ b/src/handlers/table_list.rs @@ -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), _ => (), } } diff --git a/src/main.rs b/src/main.rs index 7a32790..dd75779 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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 => (), } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index be806d3..70fdecb 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -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(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(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(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(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 = 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 = 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(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(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(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(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(()) } diff --git a/src/utils.rs b/src/utils.rs index 49018dc..05916fa 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -33,12 +33,13 @@ pub async fn get_records( ) -> anyhow::Result<(Vec, Vec>)> { 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::>(); + 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::>(); let mut records = vec![]; while let Some(row) = rows.try_next().await? { records.push(