use databasetree

pull/10/head
Takayuki Maeda 3 years ago
parent 6f4b454e01
commit 694e219020

3
Cargo.lock generated

@ -302,7 +302,10 @@ dependencies = [
name = "database-tree"
version = "0.1.2"
dependencies = [
"anyhow",
"chrono",
"pretty_assertions",
"sqlx",
"thiserror",
]

@ -11,6 +11,9 @@ description = "A cross-platform terminal database tool written in Rust"
[dependencies]
thiserror = "1.0"
sqlx = { version = "0.4.1", features = ["mysql", "chrono", "runtime-tokio-rustls"] }
chrono = "0.4"
anyhow = "1.0.38"
[dev-dependencies]
pretty_assertions = "0.7"

@ -1,5 +1,7 @@
use crate::Table;
use crate::{
databasetreeitems::DatabaseTreeItems, error::Result, tree_iter::TreeIterator, TreeItemInfo,
databasetreeitems::DatabaseTreeItems, error::Result, item::DatabaseTreeItemKind,
tree_iter::TreeIterator,
};
use std::{collections::BTreeSet, usize};
@ -59,13 +61,19 @@ impl DatabaseTree {
self.visual_selection.as_ref()
}
pub fn selected_file(&self) -> Option<&TreeItemInfo> {
pub fn selected_item(&self) -> Option<&crate::DatabaseTreeItem> {
self.selection
.and_then(|index| self.items.tree_items.get(index))
}
pub fn selected_table(&self) -> Option<(Table, String)> {
self.selection.and_then(|index| {
let item = &self.items.tree_items[index];
if item.kind().is_database() {
None
} else {
Some(item.info())
match item.kind() {
DatabaseTreeItemKind::Database { .. } => None,
DatabaseTreeItemKind::Table { table, database } => {
Some((table.clone(), database.clone()))
}
}
})
}
@ -191,6 +199,21 @@ impl DatabaseTree {
break;
}
let item = self
.items
.tree_items
.iter()
.filter(|item| item.info().is_visible())
.last()
.unwrap();
if !up
&& self.selected_item().unwrap().kind().is_database()
&& self.selected_item().unwrap() == item
{
break;
}
new_index
};
@ -207,7 +230,7 @@ impl DatabaseTree {
}
fn select_parent(&mut self, current_index: usize) -> Option<usize> {
let indent = self.items.tree_items[current_index].info().indent();
let indent = self.items.tree_items.get(current_index)?.info().indent();
let mut index = current_index;
@ -227,7 +250,7 @@ impl DatabaseTree {
}
fn selection_left(&mut self, current_index: usize) -> Option<usize> {
let item = &mut self.items.tree_items[current_index];
let item = &mut self.items.tree_items.get(current_index)?;
if item.kind().is_database() && !item.kind().is_database_collapsed() {
self.items.collapse(current_index, false);
@ -238,7 +261,7 @@ impl DatabaseTree {
}
fn selection_right(&mut self, current_selection: usize) -> Option<usize> {
let item = &mut self.items.tree_items[current_selection];
let item = &mut self.items.tree_items.get(current_selection)?;
if item.kind().is_database() {
if item.kind().is_database_collapsed() {

@ -6,11 +6,9 @@ use std::{
usize,
};
///
#[derive(Default)]
pub struct DatabaseTreeItems {
pub tree_items: Vec<DatabaseTreeItem>,
files: usize,
}
impl DatabaseTreeItems {
@ -18,7 +16,6 @@ impl DatabaseTreeItems {
pub fn new(list: &[Database], collapsed: &BTreeSet<&String>) -> Result<Self> {
Ok(Self {
tree_items: Self::create_items(list, collapsed)?,
files: list.len(),
})
}
@ -27,13 +24,13 @@ impl DatabaseTreeItems {
collapsed: &BTreeSet<&String>,
) -> Result<Vec<DatabaseTreeItem>> {
let mut items = Vec::with_capacity(list.len());
let mut paths_added: HashMap<String, usize> = HashMap::with_capacity(list.len());
let mut items_added: HashMap<String, usize> = HashMap::with_capacity(list.len());
for e in list {
{
Self::push_databases(e, &mut items, &mut paths_added, collapsed)?;
Self::push_databases(e, &mut items, &mut items_added, collapsed)?;
}
for table in e.tables.clone() {
for table in &e.tables {
items.push(DatabaseTreeItem::new_table(e, table)?);
}
}
@ -46,11 +43,6 @@ impl DatabaseTreeItems {
self.tree_items.len()
}
/// how many files were added to this list
pub const fn file_count(&self) -> usize {
self.files
}
/// iterates visible elements
pub const fn iterate(&self, start: usize, max_amount: usize) -> TreeItemsIterator<'_> {
TreeItemsIterator::new(self, start, max_amount)
@ -61,25 +53,25 @@ impl DatabaseTreeItems {
nodes: &mut Vec<DatabaseTreeItem>,
// helps to only add new nodes for paths that were not added before
// we also count the number of children a node has for later folding
paths_added: &mut HashMap<String, usize>,
items_added: &mut HashMap<String, usize>,
collapsed: &BTreeSet<&String>,
) -> Result<()> {
let c = item_path.name.clone();
if !paths_added.contains_key(&c) {
if !items_added.contains_key(&c) {
// add node and set count to have no children
paths_added.insert(c.clone(), 0);
items_added.insert(c.clone(), 0);
// increase the number of children in the parent node count
*paths_added.entry(item_path.name.clone()).or_insert(0) += 1;
*items_added.entry(item_path.name.clone()).or_insert(0) += 1;
//TODO: make non alloc
let path_string = c.clone();
let path_string = c;
let is_collapsed = collapsed.contains(&path_string);
nodes.push(DatabaseTreeItem::new_database(item_path, is_collapsed)?);
}
// increase child count in parent node (the above ancenstor ignores the leaf component)
*paths_added.entry(item_path.name.clone()).or_insert(0) += 1;
*items_added.entry(item_path.name.clone()).or_insert(0) += 1;
Ok(())
}
@ -97,8 +89,8 @@ impl DatabaseTreeItems {
item.collapse_database();
}
if let Some(db) = item.info().database.as_ref() {
if db.to_string() == path {
if let Some(db) = item.kind().database_name() {
if db == path {
item.hide();
}
} else {
@ -118,7 +110,7 @@ impl DatabaseTreeItems {
for i in index + 1..self.tree_items.len() {
let item = &mut self.tree_items[i];
if let Some(db) = item.info().database.as_ref() {
if let Some(db) = item.kind().database_name() {
if *db != full_path {
break;
}
@ -140,12 +132,11 @@ impl DatabaseTreeItems {
for i in start_idx..self.tree_items.len() {
if let Some(ref collapsed_path) = inner_collapsed {
if let Some(db) = self.tree_items[i].info().database.clone() {
if let Some(db) = self.tree_items[i].kind().database_name().clone() {
if db == *collapsed_path {
if set_defaults {
self.tree_items[i].info_mut().set_visible(false);
}
// we are still in a collapsed inner path
continue;
}
}
@ -153,18 +144,14 @@ impl DatabaseTreeItems {
}
let item_kind = self.tree_items[i].kind().clone();
let item_info = self.tree_items[i].info();
if matches!(item_kind, DatabaseTreeItemKind::Database{ collapsed, .. } if collapsed) {
// we encountered an inner path that is still collapsed
inner_collapsed = item_info.database.clone();
inner_collapsed = item_kind.database_name().clone();
}
if let Some(db) = item_info.database.as_ref() {
if prefix
.as_ref()
.map_or(true, |prefix| *prefix == db.to_string())
{
if let Some(db) = item_kind.database_name() {
if prefix.as_ref().map_or(true, |prefix| *prefix == *db) {
self.tree_items[i].info_mut().set_visible(true);
}
} else {

@ -1,5 +1,5 @@
use crate::error::Result;
use crate::Database;
use crate::{Database, Table};
use std::{convert::TryFrom, path::PathBuf};
/// holds the information shared among all `DatabaseTreeItem` in a `FileTree`
@ -14,17 +14,15 @@ pub struct TreeItemInfo {
folded: Option<PathBuf>,
/// the full path
pub full_path: String,
pub database: Option<String>,
}
impl TreeItemInfo {
pub const fn new(indent: u8, database: Option<String>, full_path: String) -> Self {
pub const fn new(indent: u8, full_path: String) -> Self {
Self {
indent,
visible: true,
folded: None,
full_path,
database,
}
}
@ -60,7 +58,7 @@ impl TreeItemInfo {
#[derive(PartialEq, Debug, Clone)]
pub enum DatabaseTreeItemKind {
Database { name: String, collapsed: bool },
Table,
Table { database: String, table: Table },
}
impl DatabaseTreeItemKind {
@ -69,13 +67,20 @@ impl DatabaseTreeItemKind {
}
pub const fn is_table(&self) -> bool {
matches!(self, Self::Table)
matches!(self, Self::Table { .. })
}
pub const fn is_database_collapsed(&self) -> bool {
match self {
Self::Database { collapsed, .. } => *collapsed,
Self::Table => false,
Self::Table { .. } => false,
}
}
pub fn database_name(&self) -> Option<String> {
match self {
Self::Database { .. } => None,
Self::Table { database, .. } => Some(database.clone()),
}
}
}
@ -88,18 +93,21 @@ pub struct DatabaseTreeItem {
}
impl DatabaseTreeItem {
pub fn new_table(database: &Database, table: String) -> Result<Self> {
pub fn new_table(database: &Database, table: &Table) -> Result<Self> {
let indent = u8::try_from((3_usize).saturating_sub(2))?;
Ok(Self {
info: TreeItemInfo::new(indent, Some(database.name.to_string()), table),
kind: DatabaseTreeItemKind::Table,
info: TreeItemInfo::new(indent, table.name.clone()),
kind: DatabaseTreeItemKind::Table {
database: database.name.clone(),
table: table.clone(),
},
})
}
pub fn new_database(database: &Database, collapsed: bool) -> Result<Self> {
Ok(Self {
info: TreeItemInfo::new(0, None, database.name.to_string()),
info: TreeItemInfo::new(0, database.name.to_string()),
kind: DatabaseTreeItemKind::Database {
name: database.name.to_string(),
collapsed,

@ -14,5 +14,26 @@ pub use crate::{
#[derive(Clone)]
pub struct Database {
pub name: String,
pub tables: Vec<String>,
pub tables: Vec<Table>,
}
impl Database {
pub fn new(database: String, tables: Vec<Table>) -> Self {
Self {
name: database,
tables,
}
}
}
#[derive(sqlx::FromRow, Debug, Clone, PartialEq)]
pub struct Table {
#[sqlx(rename = "Name")]
pub name: String,
#[sqlx(rename = "Create_time")]
pub create_time: chrono::DateTime<chrono::Utc>,
#[sqlx(rename = "Update_time")]
pub update_time: Option<chrono::DateTime<chrono::Utc>>,
#[sqlx(rename = "Engine")]
pub engine: Option<String>,
}

@ -1,11 +1,18 @@
use crate::components::utils::scroll_vertical::VerticalScroll;
use crate::{
components::DatabasesComponent,
user_config::{Connection, UserConfig},
utils::get_tables,
};
use sqlx::mysql::MySqlPool;
use strum::IntoEnumIterator;
use strum_macros::EnumIter;
use tui::widgets::{ListState, TableState};
use tui::{
backend::Backend,
layout::{Constraint, Rect},
style::{Color, Style},
widgets::{Block, Borders, Cell, ListState, Row, Table as WTable, TableState},
Frame,
};
use unicode_width::UnicodeWidthStr;
#[derive(Debug, Clone, Copy, EnumIter)]
@ -29,29 +36,10 @@ impl Tab {
}
pub enum FocusBlock {
DabataseList(bool),
TableList(bool),
RecordTable(bool),
DabataseList,
RecordTable,
ConnectionList,
Query(bool),
}
#[derive(Clone)]
pub struct Database {
pub name: String,
pub tables: Vec<Table>,
}
#[derive(sqlx::FromRow, Debug, Clone)]
pub struct Table {
#[sqlx(rename = "Name")]
pub name: String,
#[sqlx(rename = "Create_time")]
pub create_time: chrono::DateTime<chrono::Utc>,
#[sqlx(rename = "Update_time")]
pub update_time: Option<chrono::DateTime<chrono::Utc>>,
#[sqlx(rename = "Engine")]
pub engine: Option<String>,
Query,
}
#[derive(sqlx::FromRow, Debug, Clone)]
@ -71,6 +59,7 @@ pub struct RecordTable {
pub headers: Vec<String>,
pub rows: Vec<Vec<String>>,
pub column_index: usize,
pub scroll: VerticalScroll,
}
impl Default for RecordTable {
@ -80,6 +69,7 @@ impl Default for RecordTable {
headers: vec![],
rows: vec![],
column_index: 0,
scroll: VerticalScroll::new(),
}
}
}
@ -89,28 +79,28 @@ impl RecordTable {
let i = match self.state.selected() {
Some(i) => {
if i >= self.rows.len() - 1 {
0
Some(i)
} else {
i + 1
Some(i + 1)
}
}
None => 0,
None => None,
};
self.state.select(Some(i));
self.state.select(i);
}
pub fn previous(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i == 0 {
self.rows.len() - 1
Some(i)
} else {
i - 1
Some(i - 1)
}
}
None => 0,
None => None,
};
self.state.select(Some(i));
self.state.select(i);
}
pub fn next_column(&mut self) {
@ -142,14 +132,61 @@ impl RecordTable {
}
rows
}
}
impl Database {
pub async fn new(name: String, pool: &MySqlPool) -> anyhow::Result<Self> {
Ok(Self {
name: name.clone(),
tables: get_tables(name, pool).await?,
})
pub fn draw<B: Backend>(
&mut self,
f: &mut Frame<'_, B>,
layout_chunk: Rect,
focused: bool,
) -> anyhow::Result<()> {
self.state.selected().map_or_else(
|| {
self.scroll.reset();
},
|selection| {
self.scroll.update(
selection,
self.rows.len(),
layout_chunk.height.saturating_sub(2) as usize,
);
},
);
let headers = self.headers();
let header_cells = 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 = self.rows();
let rows = rows.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()).style(Style::default().fg(Color::White)));
Row::new(cells).height(height as u16).bottom_margin(1)
});
let widths = (0..10)
.map(|_| Constraint::Percentage(10))
.collect::<Vec<Constraint>>();
let t = WTable::new(rows)
.header(header)
.block(Block::default().borders(Borders::ALL).title("Records"))
.highlight_style(Style::default().fg(Color::Green))
.style(if focused {
Style::default()
} else {
Style::default().fg(Color::DarkGray)
})
.widths(&widths);
f.render_stateful_widget(t, layout_chunk, &mut self.state);
self.scroll.draw(f, layout_chunk);
Ok(())
}
}
@ -157,7 +194,6 @@ pub struct App {
pub input: String,
pub input_cursor_x: u16,
pub query: String,
pub databases: Vec<Database>,
pub record_table: RecordTable,
pub structure_table: RecordTable,
pub focus_block: FocusBlock,
@ -166,7 +202,7 @@ pub struct App {
pub selected_connection: ListState,
pub selected_database: ListState,
pub selected_table: ListState,
pub revision_files: crate::components::DatabasesComponent,
pub databases: DatabasesComponent,
pub pool: Option<MySqlPool>,
pub error: Option<String>,
}
@ -177,16 +213,15 @@ impl Default for App {
input: String::new(),
input_cursor_x: 0,
query: String::new(),
databases: Vec::new(),
record_table: RecordTable::default(),
structure_table: RecordTable::default(),
focus_block: FocusBlock::DabataseList(false),
focus_block: FocusBlock::DabataseList,
selected_tab: Tab::Records,
user_config: None,
selected_connection: ListState::default(),
selected_database: ListState::default(),
selected_table: ListState::default(),
revision_files: crate::components::DatabasesComponent::new(),
databases: DatabasesComponent::new(),
pool: None,
error: None,
}
@ -194,64 +229,6 @@ impl Default for App {
}
impl App {
pub fn next_table(&mut self) {
let i = match self.selected_table.selected() {
Some(i) => {
if i >= self.selected_database().unwrap().tables.len() - 1 {
0
} else {
i + 1
}
}
None => 0,
};
self.selected_table.select(Some(i));
}
pub fn previous_table(&mut self) {
let i = match self.selected_table.selected() {
Some(i) => {
if i == 0 {
self.selected_database().unwrap().tables.len() - 1
} else {
i - 1
}
}
None => 0,
};
self.selected_table.select(Some(i));
}
pub fn next_database(&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_table.select(Some(0));
self.selected_database.select(Some(i));
}
pub fn previous_database(&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_table.select(Some(0));
self.selected_database.select(Some(i));
}
pub fn next_connection(&mut self) {
if let Some(config) = &self.user_config {
let i = match self.selected_connection.selected() {
@ -296,23 +273,6 @@ impl App {
}
}
pub fn selected_database(&self) -> Option<&Database> {
match self.selected_database.selected() {
Some(i) => self.databases.get(i),
None => None,
}
}
pub fn selected_table(&self) -> Option<&Table> {
match self.selected_table.selected() {
Some(i) => match self.selected_database() {
Some(db) => db.tables.get(i),
None => None,
},
None => None,
}
}
pub fn selected_connection(&self) -> Option<&Connection> {
match &self.user_config {
Some(config) => match self.selected_connection.selected() {
@ -324,7 +284,7 @@ impl App {
}
pub fn table_status(&self) -> Vec<String> {
if let Some(table) = self.selected_table() {
if let Some((table, _)) = self.databases.tree.selected_table() {
return vec![
format!("created: {}", table.create_time.to_string()),
format!(

@ -2,12 +2,10 @@ use super::{
utils::scroll_vertical::VerticalScroll, CommandBlocking, CommandInfo, Component,
DrawableComponent, EventState,
};
use crate::event::{Event as Ev, Key};
use crate::event::Key;
use crate::ui::common_nav;
use crate::ui::scrolllist::draw_list_block;
use anyhow::Result;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
// use DatabaseTreelist::{DatabaseTree, DatabaseTreeItem};
use database_tree::{DatabaseTree, DatabaseTreeItem};
use std::convert::From;
use tui::{
@ -28,6 +26,7 @@ const EMPTY_STR: &str = "";
pub struct DatabasesComponent {
pub tree: DatabaseTree,
pub scroll: VerticalScroll,
pub focused: bool,
}
impl DatabasesComponent {
@ -35,10 +34,11 @@ impl DatabasesComponent {
Self {
tree: DatabaseTree::default(),
scroll: VerticalScroll::new(),
focused: true,
}
}
fn tree_item_to_span<'a>(item: &'a DatabaseTreeItem, selected: bool, width: u16) -> Span<'a> {
fn tree_item_to_span(item: &DatabaseTreeItem, selected: bool, width: u16) -> Span<'_> {
let path = item.info().full_path.to_string();
let indent = item.info().indent();
@ -99,6 +99,11 @@ impl DatabasesComponent {
area,
Block::default()
.title(Span::styled(title, Style::default()))
.style(if self.focused {
Style::default()
} else {
Style::default().fg(Color::DarkGray)
})
.borders(Borders::ALL)
.border_style(Style::default()),
items,
@ -126,35 +131,17 @@ impl Component for DatabasesComponent {
CommandBlocking::PassingOn
}
fn event(&mut self, event: Ev<Key>) -> Result<EventState> {
if let crate::event::Event::Input(key) = event {
if tree_nav(&mut self.tree, key) {
return Ok(EventState::Consumed);
} else if key == Key::Enter {
}
fn event(&mut self, key: Key) -> Result<EventState> {
if tree_nav(&mut self.tree, key) {
return Ok(EventState::Consumed);
}
Ok(EventState::NotConsumed)
}
}
fn tree_nav(tree: &mut DatabaseTree, key: Key) -> bool {
let tree_collapse_recursive = KeyEvent {
code: KeyCode::Left,
modifiers: KeyModifiers::SHIFT,
};
let tree_expand_recursive = KeyEvent {
code: KeyCode::Right,
modifiers: KeyModifiers::SHIFT,
};
if let Some(common_nav) = common_nav(key) {
tree.move_selection(common_nav)
} else if key == Key::from(tree_collapse_recursive) {
tree.collapse_recursive();
true
} else if key == Key::from(tree_expand_recursive) {
tree.expand_recursive();
true
} else {
false
}

@ -25,20 +25,17 @@ pub enum Direction {
Down,
}
///
#[derive(PartialEq)]
pub enum CommandBlocking {
Blocking,
PassingOn,
}
///
pub trait DrawableComponent {
///
fn draw<B: Backend>(&self, f: &mut Frame<B>, rect: Rect) -> Result<()>;
}
///
#[derive(PartialEq)]
pub enum EventState {
Consumed,
@ -57,30 +54,26 @@ impl From<bool> for EventState {
/// base component trait
pub trait Component {
///
fn commands(&self, out: &mut Vec<CommandInfo>, force_all: bool) -> CommandBlocking;
///
fn event(&mut self, ev: crate::event::Event<crate::event::Key>) -> Result<EventState>;
fn event(&mut self, key: crate::event::Key) -> Result<EventState>;
///
fn focused(&self) -> bool {
false
}
/// focus/unfocus this component depending on param
fn focus(&mut self, _focus: bool) {}
///
fn is_visible(&self) -> bool {
true
}
///
fn hide(&mut self) {}
///
fn show(&mut self) -> Result<()> {
Ok(())
}
///
fn toggle_visible(&mut self) -> Result<()> {
if self.is_visible() {
self.hide();

@ -25,7 +25,7 @@ impl VerticalScroll {
self.top.set(0);
}
pub fn move_top(&self, move_type: ScrollType) -> bool {
pub fn _move_top(&self, move_type: ScrollType) -> bool {
let old = self.top.get();
let max = self.max_top.get();
@ -62,7 +62,7 @@ impl VerticalScroll {
new_top
}
pub fn update_no_selection(&self, line_count: usize, visual_height: usize) -> usize {
pub fn _update_no_selection(&self, line_count: usize, visual_height: usize) -> usize {
self.update(self.get_top(), line_count, visual_height)
}

@ -1,7 +1,9 @@
use crate::app::{App, Database, FocusBlock};
use crate::app::{App, FocusBlock};
use crate::event::Key;
use crate::utils::get_databases;
use crate::utils::{get_databases, get_tables};
use database_tree::{Database, DatabaseTree};
use sqlx::mysql::MySqlPool;
use std::collections::BTreeSet;
pub async fn handler(key: Key, app: &mut App) -> anyhow::Result<()> {
match key {
@ -17,16 +19,28 @@ pub async fn handler(key: Key, app: &mut App) -> anyhow::Result<()> {
}
let pool = MySqlPool::connect(conn.database_url().as_str()).await?;
app.pool = Some(pool);
app.focus_block = FocusBlock::DabataseList(false);
app.focus_block = FocusBlock::DabataseList;
}
app.databases = match app.selected_connection() {
Some(conn) => match &conn.database {
if let Some(conn) = app.selected_connection() {
match &conn.database {
Some(database) => {
vec![Database::new(database.clone(), app.pool.as_ref().unwrap()).await?]
app.databases.tree = DatabaseTree::new(
&[Database::new(
database.clone(),
get_tables(database.clone(), app.pool.as_ref().unwrap()).await?,
)],
&BTreeSet::new(),
)
.unwrap()
}
None => {
app.databases.tree = DatabaseTree::new(
get_databases(app.pool.as_ref().unwrap()).await?.as_slice(),
&BTreeSet::new(),
)
.unwrap()
}
None => get_databases(app.pool.as_ref().unwrap()).await?,
},
None => vec![],
}
};
}
_ => (),

@ -1,22 +1,45 @@
use crate::app::{App, FocusBlock};
use crate::components::Component as _;
use crate::event::Key;
use crate::utils::{get_columns, get_records};
use database_tree::Database;
pub async fn handler(key: Key, app: &mut App, focused: bool) -> anyhow::Result<()> {
if focused {
match key {
Key::Char('j') => app.next_database(),
Key::Char('k') => app.previous_database(),
Key::Esc => app.focus_block = FocusBlock::DabataseList(false),
_ => (),
}
} else {
match key {
Key::Char('j') => app.focus_block = FocusBlock::TableList(false),
Key::Char('l') => app.focus_block = FocusBlock::RecordTable(false),
Key::Char('c') => app.focus_block = FocusBlock::ConnectionList,
Key::Enter => app.focus_block = FocusBlock::DabataseList(true),
_ => (),
pub async fn handler(key: Key, app: &mut App) -> anyhow::Result<()> {
app.databases.event(key)?;
match key {
Key::Esc => app.focus_block = FocusBlock::DabataseList,
Key::Right => app.focus_block = FocusBlock::RecordTable,
Key::Char('c') => app.focus_block = FocusBlock::ConnectionList,
Key::Enter => {
if let Some((table, database)) = app.databases.tree.selected_table() {
let (headers, records) = get_records(
&Database {
name: database.clone(),
tables: vec![],
},
&table,
app.pool.as_ref().unwrap(),
)
.await?;
app.record_table.state.select(Some(0));
app.record_table.headers = headers;
app.record_table.rows = records;
let (headers, records) = get_columns(
&Database {
name: database,
tables: vec![],
},
&table,
app.pool.as_ref().unwrap(),
)
.await?;
app.structure_table.state.select(Some(0));
app.structure_table.headers = headers;
app.structure_table.rows = records;
}
}
_ => (),
}
Ok(())
}

@ -2,7 +2,6 @@ pub mod connection_list;
pub mod database_list;
pub mod query;
pub mod record_table;
pub mod table_list;
use crate::app::{App, FocusBlock, Tab};
use crate::event::Key;
@ -10,31 +9,25 @@ use crate::event::Key;
pub async fn handle_app(key: Key, app: &mut App) -> anyhow::Result<()> {
match app.focus_block {
FocusBlock::ConnectionList => connection_list::handler(key, app).await?,
FocusBlock::DabataseList(focused) => database_list::handler(key, app, focused).await?,
FocusBlock::TableList(focused) => table_list::handler(key, app, focused).await?,
FocusBlock::RecordTable(focused) => record_table::handler(key, app, focused).await?,
FocusBlock::Query(focused) => query::handler(key, app, focused).await?,
FocusBlock::DabataseList => database_list::handler(key, app).await?,
FocusBlock::RecordTable => record_table::handler(key, app).await?,
FocusBlock::Query => query::handler(key, app).await?,
}
match key {
Key::Char('d') => match app.focus_block {
FocusBlock::Query(true) => (),
_ => app.focus_block = FocusBlock::DabataseList(true),
},
Key::Char('t') => match app.focus_block {
FocusBlock::Query(true) => (),
_ => app.focus_block = FocusBlock::TableList(true),
FocusBlock::Query => (),
_ => app.focus_block = FocusBlock::DabataseList,
},
Key::Char('r') => match app.focus_block {
FocusBlock::Query(true) => (),
_ => app.focus_block = FocusBlock::RecordTable(true),
FocusBlock::Query => (),
_ => app.focus_block = FocusBlock::RecordTable,
},
Key::Char('e') => app.focus_block = FocusBlock::Query(true),
Key::Char('e') => app.focus_block = FocusBlock::Query,
Key::Char('1') => app.selected_tab = Tab::Records,
Key::Char('2') => app.selected_tab = Tab::Structure,
// Key::Right => app.next_tab(),
// Key::Left => app.previous_tab(),
Key::Esc => app.error = None,
_ => (),
}
app.databases.focused = matches!(app.focus_block, FocusBlock::DabataseList);
Ok(())
}

@ -6,8 +6,8 @@ use regex::Regex;
use sqlx::Row;
use unicode_width::UnicodeWidthStr;
pub async fn handler(key: Key, app: &mut App, focused: bool) -> anyhow::Result<()> {
if focused {
pub async fn handler(key: Key, app: &mut App) -> anyhow::Result<()> {
if true {
match key {
Key::Enter => {
app.query = app.input.drain(..).collect();
@ -58,15 +58,15 @@ pub async fn handler(key: Key, app: &mut App, focused: bool) -> anyhow::Result<(
}
Key::Left => app.decrement_input_cursor_x(),
Key::Right => app.increment_input_cursor_x(),
Key::Esc => app.focus_block = FocusBlock::Query(false),
Key::Esc => app.focus_block = FocusBlock::Query,
_ => {}
}
} else {
match key {
Key::Char('h') => app.focus_block = FocusBlock::DabataseList(false),
Key::Char('j') => app.focus_block = FocusBlock::RecordTable(false),
Key::Char('h') => app.focus_block = FocusBlock::DabataseList,
Key::Char('j') => app.focus_block = FocusBlock::RecordTable,
Key::Char('c') => app.focus_block = FocusBlock::ConnectionList,
Key::Enter => app.focus_block = FocusBlock::Query(true),
Key::Enter => app.focus_block = FocusBlock::Query,
_ => (),
}
}

@ -1,23 +1,15 @@
use crate::app::{App, FocusBlock};
use crate::event::Key;
pub async fn handler(key: Key, app: &mut App, focused: bool) -> anyhow::Result<()> {
if focused {
match key {
Key::Char('h') => app.record_table.previous_column(),
Key::Char('j') => app.record_table.next(),
Key::Char('k') => app.record_table.previous(),
Key::Char('l') => app.record_table.next_column(),
Key::Esc => app.focus_block = FocusBlock::RecordTable(false),
_ => (),
}
} else {
match key {
Key::Char('h') => app.focus_block = FocusBlock::TableList(false),
Key::Char('c') => app.focus_block = FocusBlock::ConnectionList,
Key::Enter => app.focus_block = FocusBlock::RecordTable(true),
_ => (),
}
pub async fn handler(key: Key, app: &mut App) -> anyhow::Result<()> {
match key {
Key::Char('h') => app.record_table.previous_column(),
Key::Char('j') => app.record_table.next(),
Key::Char('k') => app.record_table.previous(),
Key::Char('l') => app.record_table.next_column(),
Key::Left => app.focus_block = FocusBlock::DabataseList,
Key::Char('c') => app.focus_block = FocusBlock::ConnectionList,
_ => (),
}
Ok(())
}

@ -1,73 +0,0 @@
use crate::app::{App, FocusBlock};
use crate::event::Key;
use crate::utils::{get_columns, get_records};
pub async fn handler(key: Key, app: &mut App, focused: bool) -> anyhow::Result<()> {
if focused {
match key {
Key::Char('j') => {
if app.selected_database.selected().is_some() {
app.next_table();
app.record_table.column_index = 0;
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.state.select(Some(0));
app.record_table.headers = headers;
app.record_table.rows = records;
}
}
app.structure_table.column_index = 0;
if let Some(database) = app.selected_database() {
if let Some(table) = app.selected_table() {
let (headers, records) =
get_columns(database, table, app.pool.as_ref().unwrap()).await?;
app.structure_table.state.select(Some(0));
app.structure_table.headers = headers;
app.structure_table.rows = records;
}
}
}
}
Key::Char('k') => {
if app.selected_database.selected().is_some() {
app.previous_table();
app.record_table.column_index = 0;
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.state.select(Some(0));
app.record_table.headers = headers;
app.record_table.rows = records;
}
}
app.structure_table.column_index = 0;
if let Some(database) = app.selected_database() {
if let Some(table) = app.selected_table() {
let (headers, records) =
get_columns(database, table, app.pool.as_ref().unwrap()).await?;
app.structure_table.state.select(Some(0));
app.structure_table.headers = headers;
app.structure_table.rows = records;
}
}
}
}
Key::Esc => app.focus_block = FocusBlock::TableList(false),
_ => (),
}
} else {
match key {
Key::Char('k') => app.focus_block = FocusBlock::DabataseList(false),
Key::Char('l') => app.focus_block = FocusBlock::RecordTable(false),
Key::Char('c') => app.focus_block = FocusBlock::ConnectionList,
Key::Enter => app.focus_block = FocusBlock::TableList(true),
_ => (),
}
}
Ok(())
}

@ -38,39 +38,9 @@ async fn main() -> anyhow::Result<()> {
terminal.clear()?;
// let mut tree = FileTree::new(
// &[
// std::path::Path::new("world/city"),
// std::path::Path::new("world/country"),
// std::path::Path::new("c/bar.rs"),
// ],
// &BTreeSet::new(),
// )
// .unwrap();
use crate::components::Component as _;
use database_tree::{Database, DatabaseTree};
use std::collections::BTreeSet;
let mut tree = DatabaseTree::new(
&[
Database {
name: "world".to_string(),
tables: vec!["country".to_string(), "city".to_string()],
},
Database {
name: "foo".to_string(),
tables: vec!["bar".to_string(), "baz".to_string(), "city".to_string()],
},
],
&BTreeSet::new(),
)
.unwrap();
tree.selection = Some(0);
app.revision_files.tree = tree;
loop {
terminal.draw(|f| ui::draw(f, &mut app).unwrap())?;
let event = events.next()?;
app.revision_files.event(event)?;
match event {
match events.next()? {
Event::Input(key) => {
if key == Key::Char('q') {
break;

@ -1,10 +0,0 @@
use crate::app::App;
use tui::{backend::Backend, layout::Rect, Frame};
fn draw_database_list<B: Backend>(
f: &mut Frame<'_, B>,
app: &mut App,
layout_chunk: Rect,
) -> anyhow::Result<()> {
Ok(())
}

@ -1,4 +1,5 @@
use crate::app::{App, FocusBlock, Tab};
use crate::components::DrawableComponent as _;
use crate::event::Key;
use database_tree::MoveSelection;
use tui::{
@ -11,7 +12,6 @@ use tui::{
};
use unicode_width::UnicodeWidthStr;
pub mod database_list;
pub mod scrollbar;
pub mod scrolllist;
@ -70,44 +70,8 @@ pub fn draw<B: Backend>(f: &mut Frame<'_, B>, app: &mut App) -> anyhow::Result<(
let left_chunks = Layout::default()
.constraints([Constraint::Min(8), Constraint::Length(7)].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_block {
// FocusBlock::DabataseList(false) => Style::default(),
// FocusBlock::DabataseList(true) => Style::default().fg(Color::Green),
// _ => Style::default().fg(Color::DarkGray),
// });
// f.render_stateful_widget(tasks, left_chunks[0], &mut app.selected_database);
use crate::components::DrawableComponent as _;
app.revision_files.draw(f, left_chunks[0]).unwrap();
// let databases = app.databases.clone();
// let tables: Vec<ListItem> = databases[app.selected_database.selected().unwrap_or(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().fg(Color::Green))
// .style(match app.focus_block {
// FocusBlock::TableList(false) => Style::default(),
// FocusBlock::TableList(true) => Style::default().fg(Color::Green),
// _ => Style::default().fg(Color::DarkGray),
// });
// f.render_stateful_widget(tasks, left_chunks[1], &mut app.selected_table);
app.databases.draw(f, left_chunks[0]).unwrap();
let table_status: Vec<ListItem> = app
.table_status()
@ -148,20 +112,23 @@ pub fn draw<B: Backend>(f: &mut Frame<'_, B>, app: &mut App) -> anyhow::Result<(
let query = Paragraph::new(app.input.as_ref())
.style(match app.focus_block {
FocusBlock::Query(true) => Style::default().fg(Color::Green),
FocusBlock::Query(false) => Style::default(),
FocusBlock::Query => Style::default(),
_ => Style::default().fg(Color::DarkGray),
})
.block(Block::default().borders(Borders::ALL).title("Query"));
f.render_widget(query, right_chunks[1]);
if let FocusBlock::Query(true) = app.focus_block {
if let FocusBlock::Query = app.focus_block {
f.set_cursor(
right_chunks[1].x + app.input.width() as u16 + 1 - app.input_cursor_x,
right_chunks[1].y + 1,
)
}
match app.selected_tab {
Tab::Records => draw_records_table(f, app, right_chunks[2])?,
Tab::Records => app.record_table.draw(
f,
right_chunks[2],
matches!(app.focus_block, FocusBlock::RecordTable),
)?,
Tab::Structure => draw_structure_table(f, app, right_chunks[2])?,
}
if let Some(err) = app.error.clone() {
@ -201,8 +168,7 @@ fn draw_structure_table<B: Backend>(
.block(Block::default().borders(Borders::ALL).title("Structure"))
.highlight_style(Style::default().fg(Color::Green))
.style(match app.focus_block {
FocusBlock::RecordTable(false) => Style::default(),
FocusBlock::RecordTable(true) => Style::default().fg(Color::Green),
FocusBlock::RecordTable => Style::default(),
_ => Style::default().fg(Color::DarkGray),
})
.widths(&widths);
@ -210,46 +176,6 @@ fn draw_structure_table<B: Backend>(
Ok(())
}
fn draw_records_table<B: Backend>(
f: &mut Frame<'_, B>,
app: &mut App,
layout_chunk: Rect,
) -> anyhow::Result<()> {
let headers = app.record_table.headers();
let header_cells = 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 = app.record_table.rows();
let rows = rows.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()).style(Style::default().fg(Color::White)));
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)
.header(header)
.block(Block::default().borders(Borders::ALL).title("Records"))
.highlight_style(Style::default().fg(Color::Green))
.style(match app.focus_block {
FocusBlock::RecordTable(false) => Style::default(),
FocusBlock::RecordTable(true) => Style::default().fg(Color::Green),
_ => Style::default().fg(Color::DarkGray),
})
.widths(&widths);
f.render_stateful_widget(t, layout_chunk, &mut app.record_table.state);
Ok(())
}
fn draw_error_popup<B: Backend>(f: &mut Frame<'_, B>, error: String) -> anyhow::Result<()> {
let percent_x = 60;
let percent_y = 20;
@ -285,17 +211,17 @@ fn draw_error_popup<B: Backend>(f: &mut Frame<'_, B>, error: String) -> anyhow::
}
pub fn common_nav(key: Key) -> Option<MoveSelection> {
if key == Key::Down {
if key == Key::Char('j') {
Some(MoveSelection::Down)
} else if key == Key::Up {
} else if key == Key::Char('k') {
Some(MoveSelection::Up)
} else if key == Key::PageUp {
Some(MoveSelection::PageUp)
} else if key == Key::PageDown {
Some(MoveSelection::PageDown)
} else if key == Key::Right {
} else if key == Key::Char('l') {
Some(MoveSelection::Right)
} else if key == Key::Left {
} else if key == Key::Char('h') {
Some(MoveSelection::Left)
} else {
None

@ -3,9 +3,9 @@ use tui::{
backend::Backend,
buffer::Buffer,
layout::Rect,
style::{Color, Style},
style::Style,
text::Span,
widgets::{Block, Borders, List, ListItem, Widget},
widgets::{Block, List, ListItem, Widget},
Frame,
};

@ -1,5 +1,5 @@
use crate::app::{Database, Table};
use chrono::NaiveDate;
use database_tree::{Database, Table};
use futures::TryStreamExt;
use sqlx::mysql::{MySqlColumn, MySqlPool, MySqlRow};
use sqlx::{Column as _, Row, TypeInfo};
@ -13,7 +13,10 @@ pub async fn get_databases(pool: &MySqlPool) -> anyhow::Result<Vec<Database>> {
.collect::<Vec<String>>();
let mut list = vec![];
for db in databases {
list.push(Database::new(db, pool).await?)
list.push(Database::new(
db.clone(),
get_tables(db.clone(), pool).await?,
))
}
Ok(list)
}

Loading…
Cancel
Save