architecture

pull/1/head
Takayuki Maeda 3 years ago
parent 8b4dee7b5a
commit 8a0f5b9d3b

@ -1,16 +1,82 @@
use sqlx::mysql::MySqlPool;
use sqlx::Row;
use tui::widgets::List;
use tui::widgets::{ListState, TableState};
pub enum InputMode {
Normal,
Editing,
}
pub enum FocusType {
Dabatases(bool),
Tables(bool),
Records(bool),
}
#[derive(Clone)]
pub struct Database {
pub selected_table: ListState,
pub name: String,
pub tables: Vec<Table>,
}
#[derive(Clone)]
pub struct Table {
pub name: String,
}
impl Database {
pub async fn new(name: String, pool: &MySqlPool) -> anyhow::Result<Self> {
let tables = sqlx::query(format!("show tables from {}", name).as_str())
.fetch_all(pool)
.await?
.iter()
.map(|table| Table { name: table.get(0) })
.collect::<Vec<Table>>();
Ok(Self {
selected_table: ListState::default(),
name,
tables,
})
}
pub fn next(&mut self) {
let i = match self.selected_table.selected() {
Some(i) => {
if i >= self.tables.len() - 1 {
0
} else {
i + 1
}
}
None => 0,
};
self.selected_table.select(Some(i));
}
pub fn previous(&mut self) {
let i = match self.selected_table.selected() {
Some(i) => {
if i == 0 {
self.tables.len() - 1
} else {
i - 1
}
}
None => 0,
};
self.selected_table.select(Some(i));
}
}
pub struct App {
/// Current value of the input box
pub input: String,
/// Current input mode
pub input_mode: InputMode,
/// History of recorded messages
pub messages: Vec<Vec<String>>,
pub tables: Vec<String>,
pub selected_database: ListState,
pub databases: Vec<Database>,
pub focus_type: FocusType,
}
impl Default for App {
@ -19,13 +85,39 @@ impl Default for App {
input: String::new(),
input_mode: InputMode::Normal,
messages: Vec::new(),
tables: Vec::new(),
selected_database: ListState::default(),
databases: Vec::new(),
focus_type: FocusType::Dabatases(false),
}
}
}
impl App {
pub fn new(title: &str, enhanced_graphics: bool) -> App {
Self::default()
pub fn next(&mut self) {
let i = match self.selected_database.selected() {
Some(i) => {
if i >= self.databases.len() - 1 {
0
} else {
i + 1
}
}
None => 0,
};
self.selected_database.select(Some(i));
}
pub fn previous(&mut self) {
let i = match self.selected_database.selected() {
Some(i) => {
if i == 0 {
self.databases.len() - 1
} else {
i - 1
}
}
None => 0,
};
self.selected_database.select(Some(i));
}
}

@ -0,0 +1,3 @@
pub mod database_list;
pub mod record_table;
pub mod table_list;

@ -0,0 +1,45 @@
use tui::widgets::TableState;
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));
}
}

@ -0,0 +1,43 @@
use tui::widgets::TableState;
pub struct TableList {
state: TableState,
tables: Vec<Vec<String>>,
}
impl TableList {
fn new() -> Self {
Self {
state: TableState::default(),
tables: vec![],
}
}
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));
}
}

@ -1,13 +1,16 @@
mod app;
mod handlers;
mod ui;
use crate::app::App;
use crate::app::InputMode;
use crate::app::{Database, FocusType, InputMode, Table};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event as CEvent, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use futures::TryStreamExt;
use sqlx::mysql::MySqlPool;
use sqlx::{Column, Executor, Row, TypeInfo};
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
@ -26,19 +29,18 @@ enum Event<I> {
Tick,
}
pub struct StatefulTable<'a> {
pub struct StatefulTable {
state: TableState,
items: Vec<Vec<&'a str>>,
headers: Vec<String>,
items: Vec<Vec<String>>,
}
impl<'a> StatefulTable<'a> {
fn new() -> StatefulTable<'a> {
impl StatefulTable {
fn new() -> StatefulTable {
StatefulTable {
state: TableState::default(),
items: vec![
vec!["Row11", "Row12", "Row13", "Row14", "Row15", "Row16"],
vec!["Row11", "Row12", "Row13", "Row13", "Row13", "Row13"],
],
headers: vec![],
items: vec![],
}
}
pub fn next(&mut self) {
@ -70,22 +72,8 @@ impl<'a> StatefulTable<'a> {
}
}
/// Crossterm demo
#[derive(Debug)]
struct Cli {
/// time in ms between two ticks.
tick_rate: u64,
/// whether unicode symbols are used to improve the overall look of the app
enhanced_graphics: bool,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli: Cli = Cli {
tick_rate: 250,
enhanced_graphics: true,
};
enable_raw_mode()?;
let mut stdout = stdout();
@ -98,7 +86,7 @@ async fn main() -> anyhow::Result<()> {
// Setup input handling
let (tx, rx) = mpsc::channel();
let tick_rate = Duration::from_millis(cli.tick_rate);
let tick_rate = Duration::from_millis(250);
thread::spawn(move || {
let mut last_tick = Instant::now();
loop {
@ -118,23 +106,54 @@ async fn main() -> anyhow::Result<()> {
}
});
use sqlx::mysql::{MySqlPool, MySqlRow};
use sqlx::Row as _;
let mut app = App::new("Crossterm Demo", cli.enhanced_graphics);
let pool = MySqlPool::connect("mysql://root:@localhost:3306/hoge").await?;
let mut rows = sqlx::query("SELECT * FROM user").fetch(&pool);
let mut tables = sqlx::query("show tables")
let mut app = &mut app::App::default();
let pool = MySqlPool::connect("mysql://root:@localhost:3306").await?;
let databases = sqlx::query("show databases")
.fetch_all(&pool)
.await?
.iter()
.map(|table| table.get(0))
.collect::<Vec<String>>();
app.tables = tables;
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();
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)
}
terminal.clear()?;
let mut table = StatefulTable::new();
table.items = records;
table.headers = headers;
loop {
terminal.draw(|f| ui::draw(f, &mut app, &mut table).unwrap())?;
@ -154,8 +173,86 @@ async fn main() -> anyhow::Result<()> {
terminal.show_cursor()?;
break;
}
KeyCode::Up => table.previous(),
KeyCode::Down => table.next(),
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::Up => match app.focus_type {
FocusType::Records(true) => 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) => 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();
// println!("{}", col.type_info().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)
}
table.items = records;
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 {

@ -1,5 +1,5 @@
use crate::app::InputMode;
use crate::App;
use crate::app::{App, FocusType};
use crate::StatefulTable;
use tui::{
backend::Backend,
@ -19,27 +19,64 @@ use unicode_width::UnicodeWidthStr;
pub fn draw<B: Backend>(
f: &mut Frame<'_, B>,
app: &mut App,
table: &mut StatefulTable<'_>,
table: &mut StatefulTable,
) -> anyhow::Result<()> {
let chunks = Layout::default()
let main_chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints([Constraint::Percentage(15), Constraint::Percentage(85)])
.direction(Direction::Horizontal)
.split(f.size());
let tables: Vec<ListItem> = app
.tables
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))]))
.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[match app.selected_database.selected() {
Some(index) => index,
None => 0,
}]
.tables
.iter()
.map(|i| {
ListItem::new(vec![Spans::from(Span::raw(&i.name))])
.style(Style::default().fg(Color::White))
})
.collect();
let tasks = List::new(tables)
.block(Block::default().borders(Borders::ALL).title("Tables"))
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
.highlight_symbol("> ");
f.render_widget(tasks, chunks[0]);
.highlight_style(Style::default().fg(Color::Green))
.style(match app.focus_type {
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)
.constraints(
[
@ -49,7 +86,7 @@ pub fn draw<B: Backend>(
]
.as_ref(),
)
.split(chunks[1]);
.split(main_chunks[1]);
let (msg, style) = match app.input_mode {
InputMode::Normal => (
@ -76,7 +113,7 @@ pub fn draw<B: Backend>(
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]);
f.render_widget(help_message, right_chunks[0]);
let input = Paragraph::new(app.input.as_ref())
.style(match app.input_mode {
@ -84,7 +121,7 @@ pub fn draw<B: Backend>(
InputMode::Editing => Style::default().fg(Color::Yellow),
})
.block(Block::default().borders(Borders::ALL).title("Input"));
f.render_widget(input, chunks[1]);
f.render_widget(input, right_chunks[1]);
match app.input_mode {
InputMode::Normal =>
// Hide the cursor. `Frame` does this by default, so we don't need to do anything here
@ -94,39 +131,39 @@ pub fn draw<B: Backend>(
// 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,
right_chunks[1].x + app.input.width() as u16 + 1,
// Move one line down, from the border to the input line
chunks[1].y + 1,
right_chunks[1].y + 1,
)
}
}
let selected_style = Style::default().add_modifier(Modifier::REVERSED);
let normal_style = Style::default().bg(Color::Blue);
let header_cells = [
"Header1", "Header2", "Header3", "Header4", "Header5", "Header6",
]
.iter()
.map(|h| Cell::from(*h).style(Style::default().fg(Color::Red)));
let header = Row::new(header_cells)
.style(normal_style)
.height(1)
.bottom_margin(1);
let rows = app.messages.iter().map(|item| {
let header_cells = table
.headers
.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 = table.items.iter().map(|item| {
let height = item
.iter()
.map(|content| content.chars().filter(|c| *c == '\n').count())
.max()
.unwrap_or(0)
+ 1;
let cells = item.iter().map(|c| Cell::from(c.to_string()));
let cells = item
.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 t = Table::new(rows)
.header(header)
.block(Block::default().borders(Borders::ALL).title("Table"))
.highlight_style(selected_style)
.highlight_symbol(">> ")
.block(Block::default().borders(Borders::ALL).title("Records"))
.highlight_style(Style::default().fg(Color::Green))
.style(match app.focus_type {
FocusType::Records(false) => Style::default().fg(Color::Magenta),
FocusType::Records(true) => Style::default().fg(Color::Green),
_ => Style::default(),
})
.widths(&[
Constraint::Percentage(10),
Constraint::Percentage(10),
@ -134,8 +171,12 @@ pub fn draw<B: Backend>(
Constraint::Percentage(10),
Constraint::Percentage(10),
Constraint::Percentage(10),
Constraint::Percentage(10),
Constraint::Percentage(10),
Constraint::Percentage(10),
Constraint::Percentage(10),
]);
f.render_stateful_widget(t, chunks[2], &mut table.state);
f.render_stateful_widget(t, right_chunks[2], &mut table.state);
Ok(())
}

Loading…
Cancel
Save