diff --git a/Cargo.lock b/Cargo.lock index 0aec9c7..1b39bfd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -469,10 +469,14 @@ name = "gobang" version = "0.1.0" dependencies = [ "anyhow", + "chrono", "crossterm 0.19.0", "futures", + "serde", + "serde_json", "sqlx", "tokio", + "toml", "tui", "unicode-width", ] @@ -1255,6 +1259,7 @@ dependencies = [ "bitflags", "byteorder", "bytes", + "chrono", "crc", "crossbeam-channel", "crossbeam-queue", @@ -1466,6 +1471,15 @@ dependencies = [ "webpki", ] +[[package]] +name = "toml" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758664fc71a3a69038656bee8b6be6477d2a6c315a6b81f7081f591bffa4111f" +dependencies = [ + "serde", +] + [[package]] name = "tui" version = "0.14.0" diff --git a/Cargo.toml b/Cargo.toml index 0be25da..48883ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,10 @@ tui = { version = "0.14.0", features = ["crossterm"], default-features = false } crossterm = "0.19" anyhow = "1.0.38" unicode-width = "0.1" -sqlx = { version = "0.4.1", features = ["mysql", "runtime-tokio-rustls"] } +sqlx = { version = "0.4.1", features = ["mysql", "chrono", "runtime-tokio-rustls"] } +chrono = "0.4" tokio = { version = "0.2", features = ["full"] } futures = "0.3.5" +serde_json = "1.0" +serde = "1.0" +toml = "0.4" diff --git a/sample.toml b/sample.toml new file mode 100644 index 0000000..9beec70 --- /dev/null +++ b/sample.toml @@ -0,0 +1,7 @@ +[conn.sample] +name = "sample" +user = "root" + +[conn.hoge] +name = "hoge" +user = "root" diff --git a/src/app.rs b/src/app.rs index d940bbe..504491f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -29,7 +29,7 @@ pub struct RecordTable { pub state: TableState, pub headers: Vec, pub rows: Vec>, - pub column_index: u64, + pub column_index: usize, } impl Default for RecordTable { @@ -73,13 +73,15 @@ impl RecordTable { } pub fn next_column(&mut self) { - if (self.column_index as usize) < self.headers.len() - 9 { - self.column_index += 1 + if self.headers.len() > 9 { + if self.column_index < self.headers.len() - 9 { + self.column_index += 1 + } } } pub fn previous_column(&mut self) { - if self.column_index != 0 { + if self.column_index > 0 { self.column_index -= 1 } } diff --git a/src/event/events.rs b/src/event/events.rs new file mode 100644 index 0000000..b004da6 --- /dev/null +++ b/src/event/events.rs @@ -0,0 +1,76 @@ +use crate::event::Key; +use crossterm::event; +use std::{sync::mpsc, thread, time::Duration}; + +#[derive(Debug, Clone, Copy)] +/// Configuration for event handling. +pub struct EventConfig { + /// The key that is used to exit the application. + pub exit_key: Key, + /// The tick rate at which the application will sent an tick event. + pub tick_rate: Duration, +} + +impl Default for EventConfig { + fn default() -> EventConfig { + EventConfig { + exit_key: Key::Ctrl('c'), + tick_rate: Duration::from_millis(250), + } + } +} + +/// An occurred event. +pub enum Event { + /// An input event occurred. + Input(I), + /// An tick event occurred. + Tick, +} + +/// A small event handler that wrap crossterm input and tick event. Each event +/// type is handled in its own thread and returned to a common `Receiver` +pub struct Events { + rx: mpsc::Receiver>, + // Need to be kept around to prevent disposing the sender side. + _tx: mpsc::Sender>, +} + +impl Events { + /// Constructs an new instance of `Events` with the default config. + pub fn new(tick_rate: u64) -> Events { + Events::with_config(EventConfig { + tick_rate: Duration::from_millis(tick_rate), + ..Default::default() + }) + } + + /// Constructs an new instance of `Events` from given config. + pub fn with_config(config: EventConfig) -> Events { + let (tx, rx) = mpsc::channel(); + + let event_tx = tx.clone(); + thread::spawn(move || { + loop { + // poll for tick rate duration, if no event, sent tick event. + if event::poll(config.tick_rate).unwrap() { + if let event::Event::Key(key) = event::read().unwrap() { + let key = Key::from(key); + + event_tx.send(Event::Input(key)).unwrap(); + } + } + + event_tx.send(Event::Tick).unwrap(); + } + }); + + Events { rx, _tx: tx } + } + + /// Attempts to read an event. + /// This function will block the current thread. + pub fn next(&self) -> Result, mpsc::RecvError> { + self.rx.recv() + } +} diff --git a/src/event/key.rs b/src/event/key.rs new file mode 100644 index 0000000..4e29c35 --- /dev/null +++ b/src/event/key.rs @@ -0,0 +1,205 @@ +use crossterm::event; +use std::fmt; + +/// Represents a key. +#[derive(PartialEq, Eq, Clone, Copy, Hash, Debug)] +pub enum Key { + /// Both Enter (or Return) and numpad Enter + Enter, + /// Tabulation key + Tab, + /// Backspace key + Backspace, + /// Escape key + Esc, + + /// Left arrow + Left, + /// Right arrow + Right, + /// Up arrow + Up, + /// Down arrow + Down, + + /// Insert key + Ins, + /// Delete key + Delete, + /// Home key + Home, + /// End key + End, + /// Page Up key + PageUp, + /// Page Down key + PageDown, + + /// F0 key + F0, + /// F1 key + F1, + /// F2 key + F2, + /// F3 key + F3, + /// F4 key + F4, + /// F5 key + F5, + /// F6 key + F6, + /// F7 key + F7, + /// F8 key + F8, + /// F9 key + F9, + /// F10 key + F10, + /// F11 key + F11, + /// F12 key + F12, + Char(char), + Ctrl(char), + Alt(char), + Unkown, +} + +impl Key { + /// Returns the function key corresponding to the given number + /// + /// 1 -> F1, etc... + /// + /// # Panics + /// + /// If `n == 0 || n > 12` + pub fn from_f(n: u8) -> Key { + match n { + 0 => Key::F0, + 1 => Key::F1, + 2 => Key::F2, + 3 => Key::F3, + 4 => Key::F4, + 5 => Key::F5, + 6 => Key::F6, + 7 => Key::F7, + 8 => Key::F8, + 9 => Key::F9, + 10 => Key::F10, + 11 => Key::F11, + 12 => Key::F12, + _ => panic!("unknown function key: F{}", n), + } + } +} + +impl fmt::Display for Key { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Key::Alt(' ') => write!(f, ""), + Key::Ctrl(' ') => write!(f, ""), + Key::Char(' ') => write!(f, ""), + Key::Alt(c) => write!(f, "", c), + Key::Ctrl(c) => write!(f, "", c), + Key::Char(c) => write!(f, "{}", c), + Key::Left | Key::Right | Key::Up | Key::Down => write!(f, "<{:?} Arrow Key>", self), + Key::Enter + | Key::Tab + | Key::Backspace + | Key::Esc + | Key::Ins + | Key::Delete + | Key::Home + | Key::End + | Key::PageUp + | Key::PageDown => write!(f, "<{:?}>", self), + _ => write!(f, "{:?}", self), + } + } +} + +impl From for Key { + fn from(key_event: event::KeyEvent) -> Self { + match key_event { + event::KeyEvent { + code: event::KeyCode::Esc, + .. + } => Key::Esc, + event::KeyEvent { + code: event::KeyCode::Backspace, + .. + } => Key::Backspace, + event::KeyEvent { + code: event::KeyCode::Left, + .. + } => Key::Left, + event::KeyEvent { + code: event::KeyCode::Right, + .. + } => Key::Right, + event::KeyEvent { + code: event::KeyCode::Up, + .. + } => Key::Up, + event::KeyEvent { + code: event::KeyCode::Down, + .. + } => Key::Down, + event::KeyEvent { + code: event::KeyCode::Home, + .. + } => Key::Home, + event::KeyEvent { + code: event::KeyCode::End, + .. + } => Key::End, + event::KeyEvent { + code: event::KeyCode::PageUp, + .. + } => Key::PageUp, + event::KeyEvent { + code: event::KeyCode::PageDown, + .. + } => Key::PageDown, + event::KeyEvent { + code: event::KeyCode::Delete, + .. + } => Key::Delete, + event::KeyEvent { + code: event::KeyCode::Insert, + .. + } => Key::Ins, + event::KeyEvent { + code: event::KeyCode::F(n), + .. + } => Key::from_f(n), + event::KeyEvent { + code: event::KeyCode::Enter, + .. + } => Key::Enter, + event::KeyEvent { + code: event::KeyCode::Tab, + .. + } => Key::Tab, + + // First check for char + modifier + event::KeyEvent { + code: event::KeyCode::Char(c), + modifiers: event::KeyModifiers::ALT, + } => Key::Alt(c), + event::KeyEvent { + code: event::KeyCode::Char(c), + modifiers: event::KeyModifiers::CONTROL, + } => Key::Ctrl(c), + + event::KeyEvent { + code: event::KeyCode::Char(c), + .. + } => Key::Char(c), + + _ => Key::Unkown, + } + } +} diff --git a/src/event/mod.rs b/src/event/mod.rs new file mode 100644 index 0000000..e4b704a --- /dev/null +++ b/src/event/mod.rs @@ -0,0 +1,7 @@ +mod events; +mod key; + +pub use self::{ + events::{Event, Events}, + key::Key, +}; diff --git a/src/handlers/database_list.rs b/src/handlers/database_list.rs index e69de29..c7d40a7 100644 --- a/src/handlers/database_list.rs +++ b/src/handlers/database_list.rs @@ -0,0 +1,17 @@ +use crate::app::{App, Database}; +use crate::event::Key; +use sqlx::mysql::MySqlPool; +use sqlx::Row; + +pub async fn handler(key: Key, app: &mut App, pool: &MySqlPool) -> anyhow::Result<()> { + let databases = sqlx::query("show databases") + .fetch_all(pool) + .await? + .iter() + .map(|table| table.get(0)) + .collect::>(); + for db in databases { + app.databases.push(Database::new(db, pool).await?) + } + Ok(()) +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 63a9d85..120be81 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -2,12 +2,103 @@ pub mod database_list; pub mod record_table; pub mod table_list; -use crate::app::App; -use crossterm::event::KeyCode; +use crate::app::{App, FocusType, InputMode}; +use crate::event::Key; +use futures::TryStreamExt; +use sqlx::mysql::MySqlPool; +use sqlx::{Column, Executor, Row, TypeInfo}; -pub fn handle_app(key: KeyCode, app: &mut App) { - match key { - KeyCode::Char('e') => (), - _ => (), +pub async fn handle_app(key: Key, app: &mut App, pool: &MySqlPool) -> anyhow::Result<()> { + match app.input_mode { + InputMode::Normal => match key { + Key::Char('e') => { + app.input_mode = InputMode::Editing; + } + 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) + } + } + 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::Records(true) => app.record_table.previous(), + FocusType::Dabatases(true) => app.previous(), + FocusType::Tables(true) => match app.selected_database.selected() { + Some(index) => { + app.record_table.column_index = 0; + app.databases[index].previous(); + let db = &app.databases[app.selected_database.selected().unwrap_or(0)]; + let (headers, records) = crate::utils::get_records( + db, + &db.tables[db.selected_table.selected().unwrap()], + &pool, + ) + .await?; + app.record_table.rows = records; + app.record_table.headers = headers; + } + None => (), + }, + _ => (), + }, + Key::Down => match app.focus_type { + FocusType::Records(true) => app.record_table.next(), + FocusType::Dabatases(true) => app.next(), + FocusType::Tables(true) => match app.selected_database.selected() { + Some(index) => { + app.record_table.column_index = 0; + &app.databases[index].next(); + let db = &app.databases[app.selected_database.selected().unwrap_or(0)]; + let (headers, records) = crate::utils::get_records( + db, + &db.tables[db.selected_table.selected().unwrap()], + &pool, + ) + .await?; + app.record_table.rows = records; + app.record_table.headers = headers; + } + None => (), + }, + _ => (), + }, + Key::Enter => match &app.focus_type { + 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); + } + Key::Backspace => { + app.input.pop(); + } + Key::Esc => { + app.input_mode = InputMode::Normal; + } + _ => {} + }, } + Ok(()) } diff --git a/src/handlers/record_table.rs b/src/handlers/record_table.rs index b6af952..db7c51b 100644 --- a/src/handlers/record_table.rs +++ b/src/handlers/record_table.rs @@ -1,45 +1,7 @@ -use tui::widgets::TableState; +use crate::app::App; +use crate::event::Key; +use sqlx::mysql::MySqlPool; -pub struct RecordTable { - state: TableState, - column_names: Vec, - records: Vec>, -} - -impl RecordTable { - fn new() -> Self { - Self { - state: TableState::default(), - column_names: vec![], - records: vec![], - } - } - - pub fn next(&mut self) { - let i = match self.state.selected() { - Some(i) => { - if i >= self.records.len() - 1 { - 0 - } else { - i + 1 - } - } - None => 0, - }; - self.state.select(Some(i)); - } - - pub fn previous(&mut self) { - let i = match self.state.selected() { - Some(i) => { - if i == 0 { - self.records.len() - 1 - } else { - i - 1 - } - } - None => 0, - }; - self.state.select(Some(i)); - } +pub async fn handler(key: Key, app: &mut App, pool: &MySqlPool) -> anyhow::Result<()> { + Ok(()) } diff --git a/src/handlers/table_list.rs b/src/handlers/table_list.rs index 2641a38..ed238c7 100644 --- a/src/handlers/table_list.rs +++ b/src/handlers/table_list.rs @@ -1,43 +1,57 @@ -use tui::widgets::TableState; +use crate::app::App; +use crate::event::Key; +use futures::TryStreamExt; +use sqlx::mysql::MySqlPool; +use sqlx::{Column, Executor, Row, TypeInfo}; -pub struct TableList { - state: TableState, - tables: Vec>, -} +pub async fn handler(key: Key, app: &mut App, pool: &MySqlPool) -> anyhow::Result<()> { + match app.selected_database.selected() { + Some(index) => { + &app.databases[index].next(); + let db = &app.databases[app.selected_database.selected().unwrap_or(0)]; + &pool.execute(format!("use {}", db.name).as_str()).await?; + let table_name = format!( + "SELECT * FROM {}", + &db.tables[db.selected_table.selected().unwrap_or(0)].name + ); + let mut rows = sqlx::query(table_name.as_str()).fetch(pool); + let headers = sqlx::query( + format!( + "desc {}", + &db.tables[db.selected_table.selected().unwrap_or(0)].name + ) + .as_str(), + ) + .fetch_all(pool) + .await? + .iter() + .map(|table| table.get(0)) + .collect::>(); + let mut records = vec![]; -impl TableList { - fn new() -> Self { - Self { - state: TableState::default(), - tables: 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" => { + let value: i32 = row.try_get(col_name).unwrap_or(0); + row_vec.push(value.to_string()); + } + "VARCHAR" => { + let value: String = row.try_get(col_name).unwrap_or("".to_string()); + row_vec.push(value); + } + _ => (), + } + } + records.push(row_vec) + } + + app.record_table.rows = records; + app.record_table.headers = headers; } + None => (), } - - pub fn next(&mut self) { - let i = match self.state.selected() { - Some(i) => { - if i >= self.tables.len() - 1 { - 0 - } else { - i + 1 - } - } - None => 0, - }; - self.state.select(Some(i)); - } - - pub fn previous(&mut self) { - let i = match self.state.selected() { - Some(i) => { - if i == 0 { - self.tables.len() - 1 - } else { - i - 1 - } - } - None => 0, - }; - self.state.select(Some(i)); - } + Ok(()) } diff --git a/src/main.rs b/src/main.rs index 7e799e1..071d7f8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,15 @@ mod app; +mod event; mod handlers; mod ui; +mod user_config; +mod utils; use crate::app::{Database, FocusType, InputMode, Table}; +use crate::event::{Event, Key}; +use crate::handlers::handle_app; use crossterm::{ - event::{self, DisableMouseCapture, EnableMouseCapture, Event as CEvent, KeyCode}, + event::{DisableMouseCapture, EnableMouseCapture, Event as CEvent, KeyCode}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; @@ -23,11 +28,7 @@ use std::{ time::{Duration, Instant}, }; use tui::{backend::CrosstermBackend, widgets::TableState, Terminal}; - -enum Event { - Input(I), - Tick, -} +use user_config::UserConfig; pub struct StatefulTable { state: TableState, @@ -39,62 +40,30 @@ pub struct StatefulTable { async fn main() -> anyhow::Result<()> { enable_raw_mode()?; + let config = user_config::UserConfig::new("sample.toml").unwrap(); + let mut stdout = stdout(); execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; - - // Setup input handling - let (tx, rx) = mpsc::channel(); - - let tick_rate = Duration::from_millis(250); - thread::spawn(move || { - let mut last_tick = Instant::now(); - loop { - // poll for tick rate duration, if no events, sent tick event. - let timeout = tick_rate - .checked_sub(last_tick.elapsed()) - .unwrap_or_else(|| Duration::from_secs(0)); - if event::poll(timeout).unwrap() { - if let CEvent::Key(key) = event::read().unwrap() { - tx.send(Event::Input(key)).unwrap(); - } - } - if last_tick.elapsed() >= tick_rate { - tx.send(Event::Tick).unwrap(); - last_tick = Instant::now(); - } - } - }); + let events = event::Events::new(250); let mut app = &mut app::App::default(); let pool = MySqlPool::connect("mysql://root:@localhost:3306").await?; - let databases = sqlx::query("show databases") + + app.databases = utils::get_databases(&pool).await?; + &pool.execute("use dev_payer").await?; + let mut rows = sqlx::query("SELECT * FROM incoming_invoices").fetch(&pool); + let headers = sqlx::query("desc incoming_invoices") .fetch_all(&pool) .await? .iter() .map(|table| table.get(0)) .collect::>(); - for db in databases { - app.databases.push(Database::new(db, &pool).await?) - } - - &pool.execute("use dev_payer").await?; - let mut rows = sqlx::query("SELECT * FROM incoming_invoices").fetch(&pool); - let mut headers: Vec = vec![]; let mut records = vec![]; while let Some(row) = rows.try_next().await? { - if headers.is_empty() { - headers.extend( - row.columns() - .iter() - .map(|col| col.name().to_string()) - .collect::>(), - ); - } let mut row_vec = vec![]; for col in row.columns() { let col_name = col.name(); @@ -119,130 +88,24 @@ async fn main() -> anyhow::Result<()> { loop { terminal.draw(|f| ui::draw(f, &mut app).unwrap())?; - match rx.recv()? { - Event::Input(event) => match app.input_mode { - InputMode::Normal => match event.code { - KeyCode::Char('e') => { - app.input_mode = InputMode::Editing; - } - KeyCode::Char('q') => { - disable_raw_mode()?; - execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - )?; - terminal.show_cursor()?; - break; - } - KeyCode::Char('l') => app.focus_type = FocusType::Records(false), - KeyCode::Char('h') => app.focus_type = FocusType::Dabatases(false), - KeyCode::Char('j') => { - if let FocusType::Dabatases(_) = app.focus_type { - app.focus_type = FocusType::Tables(false) - } - } - KeyCode::Char('k') => { - if let FocusType::Tables(_) = app.focus_type { - app.focus_type = FocusType::Dabatases(false) - } - } - KeyCode::Right => match app.focus_type { - FocusType::Records(true) => app.record_table.next_column(), - _ => (), - }, - KeyCode::Left => match app.focus_type { - FocusType::Records(true) => app.record_table.previous_column(), - _ => (), - }, - KeyCode::Up => match app.focus_type { - FocusType::Records(true) => app.record_table.previous(), - FocusType::Dabatases(true) => app.previous(), - FocusType::Tables(true) => match app.selected_database.selected() { - Some(index) => app.databases[index].previous(), - None => (), - }, - _ => (), - }, - KeyCode::Down => match app.focus_type { - FocusType::Records(true) => app.record_table.next(), - FocusType::Dabatases(true) => app.next(), - FocusType::Tables(true) => match app.selected_database.selected() { - Some(index) => { - &app.databases[index].next(); - let db = - &app.databases[app.selected_database.selected().unwrap_or(0)]; - &pool.execute(format!("use {}", db.name).as_str()).await?; - let table_name = format!( - "SELECT * FROM {}", - &db.tables[db.selected_table.selected().unwrap_or(0)].name - ); - let mut rows = sqlx::query(table_name.as_str()).fetch(&pool); - let mut headers: Vec = vec![]; - let mut records = vec![]; - - while let Some(row) = rows.try_next().await? { - if headers.is_empty() { - headers.extend( - row.columns() - .iter() - .map(|col| col.name().to_string()) - .collect::>(), - ); - } - let mut row_vec = vec![]; - for col in row.columns() { - let col_name = col.name(); - match col.type_info().clone().name() { - "INT" => { - let value: i32 = row.try_get(col_name).unwrap_or(0); - row_vec.push(value.to_string()); - } - "VARCHAR" => { - let value: String = - row.try_get(col_name).unwrap_or("".to_string()); - row_vec.push(value); - } - _ => (), - } - } - records.push(row_vec) - } - - app.record_table.rows = records; - app.record_table.headers = headers; - } - None => (), - }, - _ => (), - }, - KeyCode::Enter => match &app.focus_type { - 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 event.code { - KeyCode::Enter => { - app.messages.push(vec![app.input.drain(..).collect()]); - } - KeyCode::Char(c) => { - app.input.push(c); - } - KeyCode::Backspace => { - app.input.pop(); - } - KeyCode::Esc => { - app.input_mode = InputMode::Normal; - } - _ => {} - }, - }, + match events.next()? { + Event::Input(key) => { + if key == Key::Char('q') { + break; + }; + handle_app(key, app, &pool).await? + } Event::Tick => (), } } + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + Ok(()) } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 5d21218..43aecd4 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -92,34 +92,24 @@ pub fn draw(f: &mut Frame<'_, B>, app: &mut App) -> anyhow::Result<( ), } - let header_cells = app - .record_table - .headers + let header_cells = app.record_table.headers[app.record_table.column_index..] .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.record_table.rows.iter().map(|item| { - let height = item + let height = item[app.record_table.column_index..] .iter() .map(|content| content.chars().filter(|c| *c == '\n').count()) .max() .unwrap_or(0) + 1; - let cells = item + let cells = item[app.record_table.column_index..] .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..app.record_table.headers.len() + 1) - .map(|idx| { - if idx >= app.record_table.column_index as usize - && idx <= app.record_table.column_index as usize + 9 - { - Constraint::Percentage(10) - } else { - Constraint::Percentage(0) - } - }) + let widths = (0..10) + .map(|_| Constraint::Percentage(10)) .collect::>(); let t = Table::new(rows) .header(header) diff --git a/src/user_config.rs b/src/user_config.rs new file mode 100644 index 0000000..5c49570 --- /dev/null +++ b/src/user_config.rs @@ -0,0 +1,30 @@ +use serde::Deserialize; +use std::collections::HashMap; +use std::fs::File; +use std::io::{BufReader, Read}; + +#[derive(Debug, Deserialize)] +pub struct UserConfig { + pub conn: Option>, +} + +#[derive(Debug, Deserialize)] +pub struct Connection { + pub name: Option, + pub user: String, +} + +impl UserConfig { + pub fn new(path: &str) -> anyhow::Result { + let file = File::open(path)?; + let mut buf_reader = BufReader::new(file); + let mut contents = String::new(); + buf_reader.read_to_string(&mut contents)?; + + let config: Result = toml::from_str(&contents); + match config { + Ok(config) => Ok(config), + Err(e) => panic!("fail to parse config file: {}", e), + } + } +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..5629dcc --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,110 @@ +use crate::app::{Database, Table}; +use chrono::{DateTime, NaiveDate, NaiveDateTime}; +use futures::TryStreamExt; +use sqlx::mysql::MySqlPool; +use sqlx::{Column, Executor, Row, TypeInfo}; + +pub async fn get_databases(pool: &MySqlPool) -> anyhow::Result> { + let databases = sqlx::query("show databases") + .fetch_all(pool) + .await? + .iter() + .map(|table| table.get(0)) + .collect::>(); + let mut list = vec![]; + for db in databases { + list.push(Database::new(db, pool).await?) + } + Ok(list) +} + +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::>(); + Ok(tables) +} + +pub async fn get_records( + database: &Database, + 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 headers = sqlx::query(format!("desc {}", 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? { + let mut row_vec = vec![]; + for col in row.columns() { + let col_name = col.name(); + match col.type_info().clone().name() { + "INT" => 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" => { + 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); + } + // DATE + // VARCHAR + // INT UNSIGNED + // VARCHAR + // ENUM + // ENUM + // VARCHAR + // BOOLEAN + _ => (), + } + } + records.push(row_vec) + } + Ok((headers, records)) +}