mirror of
https://github.com/TaKO8Ki/gobang
synced 2024-11-15 18:13:50 +00:00
implement util functions
This commit is contained in:
parent
a4d9dc1562
commit
3ba9e04cf1
14
Cargo.lock
generated
14
Cargo.lock
generated
@ -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"
|
||||
|
@ -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"
|
||||
|
7
sample.toml
Normal file
7
sample.toml
Normal file
@ -0,0 +1,7 @@
|
||||
[conn.sample]
|
||||
name = "sample"
|
||||
user = "root"
|
||||
|
||||
[conn.hoge]
|
||||
name = "hoge"
|
||||
user = "root"
|
10
src/app.rs
10
src/app.rs
@ -29,7 +29,7 @@ pub struct RecordTable {
|
||||
pub state: TableState,
|
||||
pub headers: Vec<String>,
|
||||
pub rows: Vec<Vec<String>>,
|
||||
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
|
||||
}
|
||||
}
|
||||
|
76
src/event/events.rs
Normal file
76
src/event/events.rs
Normal file
@ -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<I> {
|
||||
/// 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<Event<Key>>,
|
||||
// Need to be kept around to prevent disposing the sender side.
|
||||
_tx: mpsc::Sender<Event<Key>>,
|
||||
}
|
||||
|
||||
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<Event<Key>, mpsc::RecvError> {
|
||||
self.rx.recv()
|
||||
}
|
||||
}
|
205
src/event/key.rs
Normal file
205
src/event/key.rs
Normal file
@ -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, "<Alt+Space>"),
|
||||
Key::Ctrl(' ') => write!(f, "<Ctrl+Space>"),
|
||||
Key::Char(' ') => write!(f, "<Space>"),
|
||||
Key::Alt(c) => write!(f, "<Alt+{}>", c),
|
||||
Key::Ctrl(c) => write!(f, "<Ctrl+{}>", 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<event::KeyEvent> 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,
|
||||
}
|
||||
}
|
||||
}
|
7
src/event/mod.rs
Normal file
7
src/event/mod.rs
Normal file
@ -0,0 +1,7 @@
|
||||
mod events;
|
||||
mod key;
|
||||
|
||||
pub use self::{
|
||||
events::{Event, Events},
|
||||
key::Key,
|
||||
};
|
@ -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::<Vec<String>>();
|
||||
for db in databases {
|
||||
app.databases.push(Database::new(db, pool).await?)
|
||||
}
|
||||
Ok(())
|
||||
}
|
@ -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(())
|
||||
}
|
||||
|
@ -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<String>,
|
||||
records: Vec<Vec<String>>,
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
|
@ -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<Vec<String>>,
|
||||
}
|
||||
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::<Vec<String>>();
|
||||
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(())
|
||||
}
|
||||
|
197
src/main.rs
197
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<I> {
|
||||
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::<Vec<String>>();
|
||||
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<String> = 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::<Vec<String>>(),
|
||||
);
|
||||
}
|
||||
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<String> = 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::<Vec<String>>(),
|
||||
);
|
||||
}
|
||||
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(())
|
||||
}
|
||||
|
@ -92,34 +92,24 @@ pub fn draw<B: Backend>(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::<Vec<Constraint>>();
|
||||
let t = Table::new(rows)
|
||||
.header(header)
|
||||
|
30
src/user_config.rs
Normal file
30
src/user_config.rs
Normal file
@ -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<HashMap<String, Connection>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Connection {
|
||||
pub name: Option<String>,
|
||||
pub user: String,
|
||||
}
|
||||
|
||||
impl UserConfig {
|
||||
pub fn new(path: &str) -> anyhow::Result<Self> {
|
||||
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<UserConfig, toml::de::Error> = toml::from_str(&contents);
|
||||
match config {
|
||||
Ok(config) => Ok(config),
|
||||
Err(e) => panic!("fail to parse config file: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
110
src/utils.rs
Normal file
110
src/utils.rs
Normal file
@ -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<Vec<Database>> {
|
||||
let databases = sqlx::query("show databases")
|
||||
.fetch_all(pool)
|
||||
.await?
|
||||
.iter()
|
||||
.map(|table| table.get(0))
|
||||
.collect::<Vec<String>>();
|
||||
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<Vec<Table>> {
|
||||
let tables = sqlx::query(format!("show tables from {}", database).as_str())
|
||||
.fetch_all(pool)
|
||||
.await?
|
||||
.iter()
|
||||
.map(|table| Table { name: table.get(0) })
|
||||
.collect::<Vec<Table>>();
|
||||
Ok(tables)
|
||||
}
|
||||
|
||||
pub async fn get_records(
|
||||
database: &Database,
|
||||
table: &Table,
|
||||
pool: &MySqlPool,
|
||||
) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)> {
|
||||
&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::<Vec<String>>();
|
||||
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<chrono::Utc> = 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))
|
||||
}
|
Loading…
Reference in New Issue
Block a user