mirror of https://github.com/TaKO8Ki/gobang
Merge branch 'main' of github.com:TaKO8Ki/gobang
commit
1daec68bb2
@ -1 +1,3 @@
|
|||||||
/target
|
/target
|
||||||
|
gobang
|
||||||
|
gobang.yml
|
||||||
|
Binary file not shown.
After Width: | Height: | Size: 8.5 MiB |
@ -0,0 +1,11 @@
|
|||||||
|
[[conn]]
|
||||||
|
name = "sample"
|
||||||
|
user = "root"
|
||||||
|
host = "localhost"
|
||||||
|
port = 3306
|
||||||
|
|
||||||
|
[[conn]]
|
||||||
|
user = "root"
|
||||||
|
host = "localhost"
|
||||||
|
port = 3306
|
||||||
|
database = "world"
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
mod events;
|
||||||
|
mod key;
|
||||||
|
|
||||||
|
pub use self::{
|
||||||
|
events::{Event, Events},
|
||||||
|
key::Key,
|
||||||
|
};
|
@ -0,0 +1,13 @@
|
|||||||
|
use crate::app::{App, FocusType};
|
||||||
|
use crate::event::Key;
|
||||||
|
use sqlx::mysql::MySqlPool;
|
||||||
|
|
||||||
|
pub async fn handler(_key: Key, app: &mut App) -> anyhow::Result<()> {
|
||||||
|
if let Some(conn) = app.selected_connection() {
|
||||||
|
app.pool.as_ref().unwrap().close().await;
|
||||||
|
let pool = MySqlPool::connect(conn.database_url().as_str()).await?;
|
||||||
|
app.pool = Some(pool);
|
||||||
|
app.focus_type = FocusType::Dabatases(true);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
use crate::app::{App, Database};
|
||||||
|
use crate::event::Key;
|
||||||
|
use crate::utils::get_databases;
|
||||||
|
|
||||||
|
pub async fn handler(_key: Key, app: &mut App) -> anyhow::Result<()> {
|
||||||
|
app.databases = match app.selected_connection() {
|
||||||
|
Some(conn) => match &conn.database {
|
||||||
|
Some(database) => {
|
||||||
|
vec![Database::new(database.clone(), app.pool.as_ref().unwrap()).await?]
|
||||||
|
}
|
||||||
|
None => get_databases(app.pool.as_ref().unwrap()).await?,
|
||||||
|
},
|
||||||
|
None => vec![],
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -0,0 +1,94 @@
|
|||||||
|
pub mod create_connection;
|
||||||
|
pub mod database_list;
|
||||||
|
pub mod record_table;
|
||||||
|
|
||||||
|
use crate::app::{App, FocusType, InputMode};
|
||||||
|
use crate::event::Key;
|
||||||
|
|
||||||
|
pub async fn handle_app(key: Key, app: &mut App) -> anyhow::Result<()> {
|
||||||
|
match app.input_mode {
|
||||||
|
InputMode::Normal => match key {
|
||||||
|
Key::Char('e') => {
|
||||||
|
app.input_mode = InputMode::Editing;
|
||||||
|
}
|
||||||
|
Key::Char('c') => {
|
||||||
|
app.focus_type = FocusType::Connections;
|
||||||
|
}
|
||||||
|
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::Connections => app.previous_connection(),
|
||||||
|
FocusType::Records(true) => app.record_table.previous(),
|
||||||
|
FocusType::Dabatases(true) => app.previous_database(),
|
||||||
|
FocusType::Tables(true) => match app.selected_database.selected() {
|
||||||
|
Some(index) => {
|
||||||
|
app.record_table.column_index = 0;
|
||||||
|
app.databases[index].previous();
|
||||||
|
record_table::handler(key, app).await?;
|
||||||
|
}
|
||||||
|
None => (),
|
||||||
|
},
|
||||||
|
_ => (),
|
||||||
|
},
|
||||||
|
Key::Down => match app.focus_type {
|
||||||
|
FocusType::Connections => app.next_connection(),
|
||||||
|
FocusType::Records(true) => app.record_table.next(),
|
||||||
|
FocusType::Dabatases(true) => app.next_database(),
|
||||||
|
FocusType::Tables(true) => match app.selected_database.selected() {
|
||||||
|
Some(index) => {
|
||||||
|
app.record_table.column_index = 0;
|
||||||
|
app.databases[index].next();
|
||||||
|
record_table::handler(key, app).await?
|
||||||
|
}
|
||||||
|
None => (),
|
||||||
|
},
|
||||||
|
_ => (),
|
||||||
|
},
|
||||||
|
Key::Enter => match app.focus_type {
|
||||||
|
FocusType::Connections => {
|
||||||
|
create_connection::handler(key, app).await?;
|
||||||
|
database_list::handler(key, app).await?;
|
||||||
|
}
|
||||||
|
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(())
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
use crate::app::App;
|
||||||
|
use crate::event::Key;
|
||||||
|
use crate::utils::get_records;
|
||||||
|
|
||||||
|
pub async fn handler(_key: Key, app: &mut App) -> anyhow::Result<()> {
|
||||||
|
if let Some(database) = app.selected_database() {
|
||||||
|
if let Some(table) = app.selected_table() {
|
||||||
|
let (headers, records) =
|
||||||
|
get_records(database, table, app.pool.as_ref().unwrap()).await?;
|
||||||
|
app.record_table.headers = headers;
|
||||||
|
app.record_table.rows = records;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -1,141 +1,164 @@
|
|||||||
use crate::app::InputMode;
|
use crate::app::InputMode;
|
||||||
use crate::App;
|
use crate::app::{App, FocusType};
|
||||||
use crate::StatefulTable;
|
|
||||||
use tui::{
|
use tui::{
|
||||||
backend::Backend,
|
backend::Backend,
|
||||||
layout::{Constraint, Direction, Layout, Rect},
|
layout::{Constraint, Direction, Layout},
|
||||||
style::{Color, Modifier, Style},
|
style::{Color, Style},
|
||||||
symbols,
|
text::{Span, Spans},
|
||||||
text::{Span, Spans, Text},
|
widgets::{Block, Borders, Cell, Clear, List, ListItem, Paragraph, Row, Table},
|
||||||
widgets::canvas::{Canvas, Line, Map, MapResolution, Rectangle},
|
|
||||||
widgets::{
|
|
||||||
Axis, BarChart, Block, Borders, Cell, Chart, Dataset, Gauge, LineGauge, List, ListItem,
|
|
||||||
Paragraph, Row, Sparkline, Table, Tabs, Wrap,
|
|
||||||
},
|
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
pub fn draw<B: Backend>(
|
pub fn draw<B: Backend>(f: &mut Frame<'_, B>, app: &mut App) -> anyhow::Result<()> {
|
||||||
f: &mut Frame<'_, B>,
|
if let FocusType::Connections = app.focus_type {
|
||||||
app: &mut App,
|
let percent_x = 60;
|
||||||
table: &mut StatefulTable<'_>,
|
let percent_y = 50;
|
||||||
) -> anyhow::Result<()> {
|
let conns = &app.user_config.as_ref().unwrap().conn;
|
||||||
let chunks = Layout::default()
|
let connections: Vec<ListItem> = conns
|
||||||
|
.iter()
|
||||||
|
.map(|i| {
|
||||||
|
ListItem::new(vec![Spans::from(Span::raw(i.database_url()))])
|
||||||
|
.style(Style::default().fg(Color::White))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
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 {
|
||||||
|
FocusType::Connections => Style::default().fg(Color::Green),
|
||||||
|
_ => Style::default(),
|
||||||
|
});
|
||||||
|
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_stateful_widget(tasks, area, &mut app.selected_connection);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let main_chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.margin(2)
|
.margin(2)
|
||||||
.constraints([Constraint::Percentage(15), Constraint::Percentage(85)])
|
.constraints([Constraint::Percentage(15), Constraint::Percentage(85)])
|
||||||
.direction(Direction::Horizontal)
|
.direction(Direction::Horizontal)
|
||||||
.split(f.size());
|
.split(f.size());
|
||||||
|
|
||||||
let tables: Vec<ListItem> = app
|
let left_chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)].as_ref())
|
||||||
|
.split(main_chunks[0]);
|
||||||
|
let databases: Vec<ListItem> = app
|
||||||
|
.databases
|
||||||
|
.iter()
|
||||||
|
.map(|i| {
|
||||||
|
ListItem::new(vec![Spans::from(Span::raw(&i.name))])
|
||||||
|
.style(Style::default().fg(Color::White))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
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 {
|
||||||
|
FocusType::Dabatases(false) => Style::default().fg(Color::Magenta),
|
||||||
|
FocusType::Dabatases(true) => Style::default().fg(Color::Green),
|
||||||
|
_ => Style::default(),
|
||||||
|
});
|
||||||
|
f.render_stateful_widget(tasks, left_chunks[0], &mut app.selected_database);
|
||||||
|
|
||||||
|
let databases = app.databases.clone();
|
||||||
|
let tables: Vec<ListItem> = databases[app.selected_database.selected().unwrap_or(0)]
|
||||||
.tables
|
.tables
|
||||||
.iter()
|
.iter()
|
||||||
.map(|i| ListItem::new(vec![Spans::from(Span::raw(i))]))
|
.map(|i| {
|
||||||
|
ListItem::new(vec![Spans::from(Span::raw(&i.name))])
|
||||||
|
.style(Style::default().fg(Color::White))
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let tasks = List::new(tables)
|
let tasks = List::new(tables)
|
||||||
.block(Block::default().borders(Borders::ALL).title("Tables"))
|
.block(Block::default().borders(Borders::ALL).title("Tables"))
|
||||||
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
|
.highlight_style(Style::default().fg(Color::Green))
|
||||||
.highlight_symbol("> ");
|
.style(match app.focus_type {
|
||||||
f.render_widget(tasks, chunks[0]);
|
FocusType::Tables(false) => Style::default().fg(Color::Magenta),
|
||||||
|
FocusType::Tables(true) => Style::default().fg(Color::Green),
|
||||||
|
_ => Style::default(),
|
||||||
|
});
|
||||||
|
f.render_stateful_widget(
|
||||||
|
tasks,
|
||||||
|
left_chunks[1],
|
||||||
|
&mut app.databases[app.selected_database.selected().unwrap_or(0)].selected_table,
|
||||||
|
);
|
||||||
|
|
||||||
let chunks = Layout::default()
|
let right_chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints(
|
.constraints([Constraint::Length(3), Constraint::Min(1)].as_ref())
|
||||||
[
|
.split(main_chunks[1]);
|
||||||
Constraint::Length(1),
|
|
||||||
Constraint::Length(3),
|
|
||||||
Constraint::Min(1),
|
|
||||||
]
|
|
||||||
.as_ref(),
|
|
||||||
)
|
|
||||||
.split(chunks[1]);
|
|
||||||
|
|
||||||
let (msg, style) = match app.input_mode {
|
|
||||||
InputMode::Normal => (
|
|
||||||
vec![
|
|
||||||
Span::raw("Press "),
|
|
||||||
Span::styled("q", Style::default().add_modifier(Modifier::BOLD)),
|
|
||||||
Span::raw(" to exit, "),
|
|
||||||
Span::styled("e", Style::default().add_modifier(Modifier::BOLD)),
|
|
||||||
Span::raw(" to start editing."),
|
|
||||||
],
|
|
||||||
Style::default().add_modifier(Modifier::RAPID_BLINK),
|
|
||||||
),
|
|
||||||
InputMode::Editing => (
|
|
||||||
vec![
|
|
||||||
Span::raw("Press "),
|
|
||||||
Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)),
|
|
||||||
Span::raw(" to stop editing, "),
|
|
||||||
Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)),
|
|
||||||
Span::raw(" to record the message"),
|
|
||||||
],
|
|
||||||
Style::default(),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
let mut text = Text::from(Spans::from(msg));
|
|
||||||
text.patch_style(style);
|
|
||||||
let help_message = Paragraph::new(text);
|
|
||||||
f.render_widget(help_message, chunks[0]);
|
|
||||||
|
|
||||||
let input = Paragraph::new(app.input.as_ref())
|
let input = Paragraph::new(app.input.as_ref())
|
||||||
.style(match app.input_mode {
|
.style(match app.input_mode {
|
||||||
InputMode::Normal => Style::default(),
|
InputMode::Normal => Style::default(),
|
||||||
InputMode::Editing => Style::default().fg(Color::Yellow),
|
InputMode::Editing => Style::default().fg(Color::Yellow),
|
||||||
})
|
})
|
||||||
.block(Block::default().borders(Borders::ALL).title("Input"));
|
.block(Block::default().borders(Borders::ALL).title("Query"));
|
||||||
f.render_widget(input, chunks[1]);
|
f.render_widget(input, right_chunks[0]);
|
||||||
match app.input_mode {
|
match app.input_mode {
|
||||||
InputMode::Normal =>
|
InputMode::Normal => (),
|
||||||
// Hide the cursor. `Frame` does this by default, so we don't need to do anything here
|
InputMode::Editing => f.set_cursor(
|
||||||
{}
|
right_chunks[0].x + app.input.width() as u16 + 1,
|
||||||
|
right_chunks[0].y + 1,
|
||||||
InputMode::Editing => {
|
),
|
||||||
// Make the cursor visible and ask tui-rs to put it at the specified coordinates after rendering
|
|
||||||
f.set_cursor(
|
|
||||||
// Put cursor past the end of the input text
|
|
||||||
chunks[1].x + app.input.width() as u16 + 1,
|
|
||||||
// Move one line down, from the border to the input line
|
|
||||||
chunks[1].y + 1,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let selected_style = Style::default().add_modifier(Modifier::REVERSED);
|
let header_cells = app.record_table.headers[app.record_table.column_index..]
|
||||||
let normal_style = Style::default().bg(Color::Blue);
|
|
||||||
let header_cells = [
|
|
||||||
"Header1", "Header2", "Header3", "Header4", "Header5", "Header6",
|
|
||||||
]
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(|h| Cell::from(*h).style(Style::default().fg(Color::Red)));
|
.map(|h| Cell::from(h.to_string()).style(Style::default().fg(Color::White)));
|
||||||
let header = Row::new(header_cells)
|
let header = Row::new(header_cells).height(1).bottom_margin(1);
|
||||||
.style(normal_style)
|
let rows = app.record_table.rows.iter().map(|item| {
|
||||||
.height(1)
|
let height = item[app.record_table.column_index..]
|
||||||
.bottom_margin(1);
|
|
||||||
let rows = app.messages.iter().map(|item| {
|
|
||||||
let height = item
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(|content| content.chars().filter(|c| *c == '\n').count())
|
.map(|content| content.chars().filter(|c| *c == '\n').count())
|
||||||
.max()
|
.max()
|
||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
+ 1;
|
+ 1;
|
||||||
let cells = item.iter().map(|c| Cell::from(c.to_string()));
|
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)
|
Row::new(cells).height(height as u16).bottom_margin(1)
|
||||||
});
|
});
|
||||||
|
let widths = (0..10)
|
||||||
|
.map(|_| Constraint::Percentage(10))
|
||||||
|
.collect::<Vec<Constraint>>();
|
||||||
let t = Table::new(rows)
|
let t = Table::new(rows)
|
||||||
.header(header)
|
.header(header)
|
||||||
.block(Block::default().borders(Borders::ALL).title("Table"))
|
.block(Block::default().borders(Borders::ALL).title("Records"))
|
||||||
.highlight_style(selected_style)
|
.highlight_style(Style::default().fg(Color::Green))
|
||||||
.highlight_symbol(">> ")
|
.style(match app.focus_type {
|
||||||
.widths(&[
|
FocusType::Records(false) => Style::default().fg(Color::Magenta),
|
||||||
Constraint::Percentage(10),
|
FocusType::Records(true) => Style::default().fg(Color::Green),
|
||||||
Constraint::Percentage(10),
|
_ => Style::default(),
|
||||||
Constraint::Percentage(10),
|
})
|
||||||
Constraint::Percentage(10),
|
.widths(&widths);
|
||||||
Constraint::Percentage(10),
|
f.render_stateful_widget(t, right_chunks[1], &mut app.record_table.state);
|
||||||
Constraint::Percentage(10),
|
|
||||||
]);
|
|
||||||
f.render_stateful_widget(t, chunks[2], &mut table.state);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::{BufReader, Read};
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct UserConfig {
|
||||||
|
pub conn: Vec<Connection>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Connection {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub user: String,
|
||||||
|
pub host: String,
|
||||||
|
pub port: u64,
|
||||||
|
pub database: Option<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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Connection {
|
||||||
|
pub fn database_url(&self) -> String {
|
||||||
|
match &self.database {
|
||||||
|
Some(database) => format!(
|
||||||
|
"mysql://{user}:@{host}:{port}/{database}",
|
||||||
|
user = self.user,
|
||||||
|
host = self.host,
|
||||||
|
port = self.port,
|
||||||
|
database = database
|
||||||
|
),
|
||||||
|
None => format!(
|
||||||
|
"mysql://{user}:@{host}:{port}",
|
||||||
|
user = self.user,
|
||||||
|
host = self.host,
|
||||||
|
port = self.port,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,101 @@
|
|||||||
|
use crate::app::{Database, Table};
|
||||||
|
use chrono::NaiveDate;
|
||||||
|
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" | "DECIMAL" | "SMALLINT" => 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" | "CHAR" => {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
records.push(row_vec)
|
||||||
|
}
|
||||||
|
Ok((headers, records))
|
||||||
|
}
|
Loading…
Reference in New Issue