mirror of
https://github.com/TaKO8Ki/gobang.git
synced 2021-09-19 22:32:56 +03:00
Implement DatabaseTree (#10)
* remove unused functions * remove unused functions * use databasetree * update gobang.gif * refactor * remove unused dependencies * fix header's and row's default style
This commit is contained in:
18
Cargo.lock
generated
18
Cargo.lock
generated
@@ -279,6 +279,16 @@ dependencies = [
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "database-tree"
|
||||
version = "0.1.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"sqlx",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.9.0"
|
||||
@@ -294,6 +304,12 @@ version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
|
||||
|
||||
[[package]]
|
||||
name = "easy-cast"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4bd102ee8c418348759919b83b81cdbdc933ffe29740b903df448b4bafaa348e"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.6.1"
|
||||
@@ -471,6 +487,8 @@ dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"crossterm 0.19.0",
|
||||
"database-tree",
|
||||
"easy-cast",
|
||||
"futures",
|
||||
"regex",
|
||||
"serde",
|
||||
|
||||
@@ -24,3 +24,10 @@ toml = "0.4"
|
||||
regex = "1"
|
||||
strum = "0.21"
|
||||
strum_macros = "0.21"
|
||||
database-tree = { path = "./database-tree", version = "0.1" }
|
||||
easy-cast = "0.4"
|
||||
|
||||
[workspace]
|
||||
members=[
|
||||
"database-tree"
|
||||
]
|
||||
|
||||
16
database-tree/Cargo.toml
Normal file
16
database-tree/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "database-tree"
|
||||
version = "0.1.2"
|
||||
authors = ["Stephan Dilly <dilly.stephan@gmail.com>"]
|
||||
edition = "2018"
|
||||
license = "MIT"
|
||||
homepage = "https://github.com/TaKO8Ki/gobang"
|
||||
repository = "https://github.com/TaKO8Ki/gobang"
|
||||
readme = "README.md"
|
||||
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"
|
||||
284
database-tree/src/databasetree.rs
Normal file
284
database-tree/src/databasetree.rs
Normal file
@@ -0,0 +1,284 @@
|
||||
use crate::Table;
|
||||
use crate::{
|
||||
databasetreeitems::DatabaseTreeItems, error::Result, item::DatabaseTreeItemKind,
|
||||
tree_iter::TreeIterator,
|
||||
};
|
||||
use std::{collections::BTreeSet, usize};
|
||||
|
||||
///
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum MoveSelection {
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
Top,
|
||||
End,
|
||||
PageDown,
|
||||
PageUp,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct VisualSelection {
|
||||
pub count: usize,
|
||||
pub index: usize,
|
||||
}
|
||||
|
||||
/// wraps `DatabaseTreeItems` as a datastore and adds selection functionality
|
||||
#[derive(Default)]
|
||||
pub struct DatabaseTree {
|
||||
items: DatabaseTreeItems,
|
||||
pub selection: Option<usize>,
|
||||
visual_selection: Option<VisualSelection>,
|
||||
}
|
||||
|
||||
impl DatabaseTree {
|
||||
pub fn new(list: &[crate::Database], collapsed: &BTreeSet<&String>) -> Result<Self> {
|
||||
let mut new_self = Self {
|
||||
items: DatabaseTreeItems::new(list, collapsed)?,
|
||||
selection: if list.is_empty() { None } else { Some(0) },
|
||||
visual_selection: None,
|
||||
};
|
||||
new_self.visual_selection = new_self.calc_visual_selection();
|
||||
|
||||
Ok(new_self)
|
||||
}
|
||||
|
||||
pub fn collapse_but_root(&mut self) {
|
||||
self.items.collapse(0, true);
|
||||
self.items.expand(0, false);
|
||||
}
|
||||
|
||||
/// iterates visible elements starting from `start_index_visual`
|
||||
pub fn iterate(&self, start_index_visual: usize, max_amount: usize) -> TreeIterator<'_> {
|
||||
let start = self
|
||||
.visual_index_to_absolute(start_index_visual)
|
||||
.unwrap_or_default();
|
||||
TreeIterator::new(self.items.iterate(start, max_amount), self.selection)
|
||||
}
|
||||
|
||||
pub const fn visual_selection(&self) -> Option<&VisualSelection> {
|
||||
self.visual_selection.as_ref()
|
||||
}
|
||||
|
||||
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];
|
||||
match item.kind() {
|
||||
DatabaseTreeItemKind::Database { .. } => None,
|
||||
DatabaseTreeItemKind::Table { table, database } => {
|
||||
Some((table.clone(), database.clone()))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn collapse_recursive(&mut self) {
|
||||
if let Some(selection) = self.selection {
|
||||
self.items.collapse(selection, true);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expand_recursive(&mut self) {
|
||||
if let Some(selection) = self.selection {
|
||||
self.items.expand(selection, true);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_selection(&mut self, dir: MoveSelection) -> bool {
|
||||
self.selection.map_or(false, |selection| {
|
||||
let new_index = match dir {
|
||||
MoveSelection::Up => self.selection_updown(selection, true),
|
||||
MoveSelection::Down => self.selection_updown(selection, false),
|
||||
MoveSelection::Left => self.selection_left(selection),
|
||||
MoveSelection::Right => self.selection_right(selection),
|
||||
MoveSelection::Top => Self::selection_start(selection),
|
||||
MoveSelection::End => self.selection_end(selection),
|
||||
MoveSelection::PageDown | MoveSelection::PageUp => None,
|
||||
};
|
||||
|
||||
let changed_index = new_index.map(|i| i != selection).unwrap_or_default();
|
||||
|
||||
if changed_index {
|
||||
self.selection = new_index;
|
||||
self.visual_selection = self.calc_visual_selection();
|
||||
}
|
||||
|
||||
changed_index || new_index.is_some()
|
||||
})
|
||||
}
|
||||
|
||||
fn visual_index_to_absolute(&self, visual_index: usize) -> Option<usize> {
|
||||
self.items
|
||||
.iterate(0, self.items.len())
|
||||
.enumerate()
|
||||
.find_map(
|
||||
|(i, (abs, _))| {
|
||||
if i == visual_index {
|
||||
Some(abs)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn calc_visual_selection(&self) -> Option<VisualSelection> {
|
||||
self.selection.map(|selection_absolute| {
|
||||
let mut count = 0;
|
||||
let mut visual_index = 0;
|
||||
for (index, _item) in self.items.iterate(0, self.items.len()) {
|
||||
if selection_absolute == index {
|
||||
visual_index = count;
|
||||
}
|
||||
|
||||
count += 1;
|
||||
}
|
||||
|
||||
VisualSelection {
|
||||
index: visual_index,
|
||||
count,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const fn selection_start(current_index: usize) -> Option<usize> {
|
||||
if current_index == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(0)
|
||||
}
|
||||
}
|
||||
|
||||
fn selection_end(&self, current_index: usize) -> Option<usize> {
|
||||
let items_max = self.items.len().saturating_sub(1);
|
||||
|
||||
let mut new_index = items_max;
|
||||
|
||||
loop {
|
||||
if self.is_visible_index(new_index) {
|
||||
break;
|
||||
}
|
||||
|
||||
if new_index == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
new_index = new_index.saturating_sub(1);
|
||||
new_index = std::cmp::min(new_index, items_max);
|
||||
}
|
||||
|
||||
if new_index == current_index {
|
||||
None
|
||||
} else {
|
||||
Some(new_index)
|
||||
}
|
||||
}
|
||||
|
||||
fn selection_updown(&self, current_index: usize, up: bool) -> Option<usize> {
|
||||
let mut index = current_index;
|
||||
|
||||
loop {
|
||||
index = {
|
||||
let new_index = if up {
|
||||
index.saturating_sub(1)
|
||||
} else {
|
||||
index.saturating_add(1)
|
||||
};
|
||||
|
||||
if new_index == index {
|
||||
break;
|
||||
}
|
||||
|
||||
if new_index >= self.items.len() {
|
||||
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
|
||||
};
|
||||
|
||||
if self.is_visible_index(index) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if index == current_index {
|
||||
None
|
||||
} else {
|
||||
Some(index)
|
||||
}
|
||||
}
|
||||
|
||||
fn select_parent(&mut self, current_index: usize) -> Option<usize> {
|
||||
let indent = self.items.tree_items.get(current_index)?.info().indent();
|
||||
|
||||
let mut index = current_index;
|
||||
|
||||
while let Some(selection) = self.selection_updown(index, true) {
|
||||
index = selection;
|
||||
|
||||
if self.items.tree_items[index].info().indent() < indent {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if index == current_index {
|
||||
None
|
||||
} else {
|
||||
Some(index)
|
||||
}
|
||||
}
|
||||
|
||||
fn selection_left(&mut self, current_index: usize) -> Option<usize> {
|
||||
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);
|
||||
return Some(current_index);
|
||||
}
|
||||
|
||||
self.select_parent(current_index)
|
||||
}
|
||||
|
||||
fn selection_right(&mut self, current_selection: usize) -> Option<usize> {
|
||||
let item = &mut self.items.tree_items.get(current_selection)?;
|
||||
|
||||
if item.kind().is_database() {
|
||||
if item.kind().is_database_collapsed() {
|
||||
self.items.expand(current_selection, false);
|
||||
return Some(current_selection);
|
||||
}
|
||||
return self.selection_updown(current_selection, false);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn is_visible_index(&self, index: usize) -> bool {
|
||||
self.items
|
||||
.tree_items
|
||||
.get(index)
|
||||
.map(|item| item.info().is_visible())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
161
database-tree/src/databasetreeitems.rs
Normal file
161
database-tree/src/databasetreeitems.rs
Normal file
@@ -0,0 +1,161 @@
|
||||
use crate::Database;
|
||||
use crate::{error::Result, treeitems_iter::TreeItemsIterator};
|
||||
use crate::{item::DatabaseTreeItemKind, DatabaseTreeItem};
|
||||
use std::{
|
||||
collections::{BTreeSet, HashMap},
|
||||
usize,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct DatabaseTreeItems {
|
||||
pub tree_items: Vec<DatabaseTreeItem>,
|
||||
}
|
||||
|
||||
impl DatabaseTreeItems {
|
||||
///
|
||||
pub fn new(list: &[Database], collapsed: &BTreeSet<&String>) -> Result<Self> {
|
||||
Ok(Self {
|
||||
tree_items: Self::create_items(list, collapsed)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn create_items(
|
||||
list: &[Database],
|
||||
collapsed: &BTreeSet<&String>,
|
||||
) -> Result<Vec<DatabaseTreeItem>> {
|
||||
let mut items = Vec::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 items_added, collapsed)?;
|
||||
}
|
||||
for table in &e.tables {
|
||||
items.push(DatabaseTreeItem::new_table(e, table)?);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
/// how many individual items are in the list
|
||||
pub fn len(&self) -> usize {
|
||||
self.tree_items.len()
|
||||
}
|
||||
|
||||
/// iterates visible elements
|
||||
pub const fn iterate(&self, start: usize, max_amount: usize) -> TreeItemsIterator<'_> {
|
||||
TreeItemsIterator::new(self, start, max_amount)
|
||||
}
|
||||
|
||||
fn push_databases<'a>(
|
||||
database: &'a Database,
|
||||
nodes: &mut Vec<DatabaseTreeItem>,
|
||||
items_added: &mut HashMap<String, usize>,
|
||||
collapsed: &BTreeSet<&String>,
|
||||
) -> Result<()> {
|
||||
let c = database.name.clone();
|
||||
if !items_added.contains_key(&c) {
|
||||
// add node and set count to have no children
|
||||
items_added.insert(c.clone(), 0);
|
||||
|
||||
// increase the number of children in the parent node count
|
||||
*items_added.entry(database.name.clone()).or_insert(0) += 1;
|
||||
|
||||
let is_collapsed = collapsed.contains(&c);
|
||||
nodes.push(DatabaseTreeItem::new_database(database, is_collapsed)?);
|
||||
}
|
||||
|
||||
// increase child count in parent node (the above ancenstor ignores the leaf component)
|
||||
*items_added.entry(database.name.clone()).or_insert(0) += 1;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn collapse(&mut self, index: usize, recursive: bool) {
|
||||
if self.tree_items[index].kind().is_database() {
|
||||
self.tree_items[index].collapse_database();
|
||||
|
||||
let name = self.tree_items[index].kind().name();
|
||||
|
||||
for i in index + 1..self.tree_items.len() {
|
||||
let item = &mut self.tree_items[i];
|
||||
|
||||
if recursive && item.kind().is_database() {
|
||||
item.collapse_database();
|
||||
}
|
||||
|
||||
if let Some(db) = item.kind().database_name() {
|
||||
if db == name {
|
||||
item.hide();
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expand(&mut self, index: usize, recursive: bool) {
|
||||
if self.tree_items[index].kind().is_database() {
|
||||
self.tree_items[index].expand_database();
|
||||
|
||||
let name = self.tree_items[index].kind().name();
|
||||
|
||||
if recursive {
|
||||
for i in index + 1..self.tree_items.len() {
|
||||
let item = &mut self.tree_items[i];
|
||||
|
||||
if let Some(db) = item.kind().database_name() {
|
||||
if *db != name {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if item.kind().is_database() && item.kind().is_database_collapsed() {
|
||||
item.expand_database();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.update_visibility(&Some(name), index + 1, false);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_visibility(&mut self, prefix: &Option<String>, start_idx: usize, set_defaults: bool) {
|
||||
let mut inner_collapsed: Option<String> = None;
|
||||
|
||||
for i in start_idx..self.tree_items.len() {
|
||||
if let Some(ref collapsed_item) = inner_collapsed {
|
||||
if let Some(db) = self.tree_items[i].kind().database_name().clone() {
|
||||
if db == *collapsed_item {
|
||||
if set_defaults {
|
||||
self.tree_items[i].info_mut().set_visible(false);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
inner_collapsed = None;
|
||||
}
|
||||
|
||||
let item_kind = self.tree_items[i].kind().clone();
|
||||
|
||||
if matches!(item_kind, DatabaseTreeItemKind::Database{ collapsed, .. } if collapsed) {
|
||||
inner_collapsed = item_kind.database_name().clone();
|
||||
}
|
||||
|
||||
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 {
|
||||
// if we do not set defaults we can early out
|
||||
if set_defaults {
|
||||
self.tree_items[i].info_mut().set_visible(false);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
database-tree/src/error.rs
Normal file
10
database-tree/src/error.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
use std::num::TryFromIntError;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("TryFromInt error:{0}")]
|
||||
IntConversion(#[from] TryFromIntError),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
163
database-tree/src/item.rs
Normal file
163
database-tree/src/item.rs
Normal file
@@ -0,0 +1,163 @@
|
||||
use crate::error::Result;
|
||||
use crate::{Database, Table};
|
||||
use std::convert::TryFrom;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TreeItemInfo {
|
||||
indent: u8,
|
||||
visible: bool,
|
||||
}
|
||||
|
||||
impl TreeItemInfo {
|
||||
pub const fn new(indent: u8) -> Self {
|
||||
Self {
|
||||
indent,
|
||||
visible: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn is_visible(&self) -> bool {
|
||||
self.visible
|
||||
}
|
||||
|
||||
pub const fn indent(&self) -> u8 {
|
||||
self.indent
|
||||
}
|
||||
|
||||
pub fn unindent(&mut self) {
|
||||
self.indent = self.indent.saturating_sub(1);
|
||||
}
|
||||
|
||||
pub fn set_visible(&mut self, visible: bool) {
|
||||
self.visible = visible;
|
||||
}
|
||||
}
|
||||
|
||||
/// `DatabaseTreeItem` can be of two kinds
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
pub enum DatabaseTreeItemKind {
|
||||
Database { name: String, collapsed: bool },
|
||||
Table { database: String, table: Table },
|
||||
}
|
||||
|
||||
impl DatabaseTreeItemKind {
|
||||
pub const fn is_database(&self) -> bool {
|
||||
matches!(self, Self::Database { .. })
|
||||
}
|
||||
|
||||
pub const fn is_table(&self) -> bool {
|
||||
matches!(self, Self::Table { .. })
|
||||
}
|
||||
|
||||
pub const fn is_database_collapsed(&self) -> bool {
|
||||
match self {
|
||||
Self::Database { collapsed, .. } => *collapsed,
|
||||
Self::Table { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> String {
|
||||
match self {
|
||||
Self::Database { name, .. } => name.to_string(),
|
||||
Self::Table { table, .. } => table.name.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn database_name(&self) -> Option<String> {
|
||||
match self {
|
||||
Self::Database { .. } => None,
|
||||
Self::Table { database, .. } => Some(database.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `DatabaseTreeItem` can be of two kinds: see `DatabaseTreeItem` but shares an info
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DatabaseTreeItem {
|
||||
info: TreeItemInfo,
|
||||
kind: DatabaseTreeItemKind,
|
||||
}
|
||||
|
||||
impl DatabaseTreeItem {
|
||||
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),
|
||||
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),
|
||||
kind: DatabaseTreeItemKind::Database {
|
||||
name: database.name.to_string(),
|
||||
collapsed,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub const fn info(&self) -> &TreeItemInfo {
|
||||
&self.info
|
||||
}
|
||||
|
||||
pub fn info_mut(&mut self) -> &mut TreeItemInfo {
|
||||
&mut self.info
|
||||
}
|
||||
|
||||
pub const fn kind(&self) -> &DatabaseTreeItemKind {
|
||||
&self.kind
|
||||
}
|
||||
|
||||
pub fn collapse_database(&mut self) {
|
||||
if let DatabaseTreeItemKind::Database { name, .. } = &self.kind {
|
||||
self.kind = DatabaseTreeItemKind::Database {
|
||||
name: name.to_string(),
|
||||
collapsed: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expand_database(&mut self) {
|
||||
if let DatabaseTreeItemKind::Database { name, .. } = &self.kind {
|
||||
self.kind = DatabaseTreeItemKind::Database {
|
||||
name: name.to_string(),
|
||||
collapsed: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hide(&mut self) {
|
||||
self.info.visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for DatabaseTreeItem {}
|
||||
|
||||
impl PartialEq for DatabaseTreeItem {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
if self.kind.is_database() && other.kind().is_database() {
|
||||
return self.kind.name().eq(&other.kind.name());
|
||||
}
|
||||
if !self.kind.is_database() && !other.kind.is_database() {
|
||||
return self.kind.name().eq(&other.kind.name());
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for DatabaseTreeItem {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
self.kind.name().partial_cmp(&other.kind.name())
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for DatabaseTreeItem {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.kind.name().cmp(&other.kind.name())
|
||||
}
|
||||
}
|
||||
39
database-tree/src/lib.rs
Normal file
39
database-tree/src/lib.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
mod databasetree;
|
||||
mod databasetreeitems;
|
||||
mod error;
|
||||
mod item;
|
||||
mod tree_iter;
|
||||
mod treeitems_iter;
|
||||
|
||||
pub use crate::{
|
||||
databasetree::DatabaseTree,
|
||||
databasetree::MoveSelection,
|
||||
item::{DatabaseTreeItem, TreeItemInfo},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Database {
|
||||
pub name: 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>,
|
||||
}
|
||||
25
database-tree/src/tree_iter.rs
Normal file
25
database-tree/src/tree_iter.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use crate::{item::DatabaseTreeItem, treeitems_iter::TreeItemsIterator};
|
||||
|
||||
pub struct TreeIterator<'a> {
|
||||
item_iter: TreeItemsIterator<'a>,
|
||||
selection: Option<usize>,
|
||||
}
|
||||
|
||||
impl<'a> TreeIterator<'a> {
|
||||
pub const fn new(item_iter: TreeItemsIterator<'a>, selection: Option<usize>) -> Self {
|
||||
Self {
|
||||
item_iter,
|
||||
selection,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for TreeIterator<'a> {
|
||||
type Item = (&'a DatabaseTreeItem, bool);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.item_iter
|
||||
.next()
|
||||
.map(|(index, item)| (item, self.selection.map(|i| i == index).unwrap_or_default()))
|
||||
}
|
||||
}
|
||||
56
database-tree/src/treeitems_iter.rs
Normal file
56
database-tree/src/treeitems_iter.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use crate::{databasetreeitems::DatabaseTreeItems, item::DatabaseTreeItem};
|
||||
|
||||
pub struct TreeItemsIterator<'a> {
|
||||
tree: &'a DatabaseTreeItems,
|
||||
index: usize,
|
||||
increments: Option<usize>,
|
||||
max_amount: usize,
|
||||
}
|
||||
|
||||
impl<'a> TreeItemsIterator<'a> {
|
||||
pub const fn new(tree: &'a DatabaseTreeItems, start: usize, max_amount: usize) -> Self {
|
||||
TreeItemsIterator {
|
||||
max_amount,
|
||||
increments: None,
|
||||
index: start,
|
||||
tree,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for TreeItemsIterator<'a> {
|
||||
type Item = (usize, &'a DatabaseTreeItem);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.increments.unwrap_or_default() < self.max_amount {
|
||||
let items = &self.tree.tree_items;
|
||||
|
||||
let mut init = self.increments.is_none();
|
||||
|
||||
if let Some(i) = self.increments.as_mut() {
|
||||
*i += 1;
|
||||
} else {
|
||||
self.increments = Some(0);
|
||||
};
|
||||
|
||||
loop {
|
||||
if !init {
|
||||
self.index += 1;
|
||||
}
|
||||
init = false;
|
||||
|
||||
if self.index >= self.tree.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
let elem = &items[self.index];
|
||||
|
||||
if elem.info().is_visible() {
|
||||
return Some((self.index, &items[self.index]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.4 MiB After Width: | Height: | Size: 6.8 MiB |
216
src/app.rs
216
src/app.rs
@@ -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)]
|
||||
@@ -22,34 +29,17 @@ impl std::fmt::Display for Tab {
|
||||
|
||||
impl Tab {
|
||||
pub fn names() -> Vec<String> {
|
||||
Self::iter().map(|tab| tab.to_string()).collect()
|
||||
Self::iter()
|
||||
.map(|tab| format!("{} [{}]", tab, tab as u8 + 1))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
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)]
|
||||
@@ -69,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 {
|
||||
@@ -78,6 +69,7 @@ impl Default for RecordTable {
|
||||
headers: vec![],
|
||||
rows: vec![],
|
||||
column_index: 0,
|
||||
scroll: VerticalScroll::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,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) {
|
||||
@@ -140,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()));
|
||||
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()));
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,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,
|
||||
@@ -164,6 +202,7 @@ pub struct App {
|
||||
pub selected_connection: ListState,
|
||||
pub selected_database: ListState,
|
||||
pub selected_table: ListState,
|
||||
pub databases: DatabasesComponent,
|
||||
pub pool: Option<MySqlPool>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
@@ -174,15 +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(),
|
||||
databases: DatabasesComponent::new(),
|
||||
pool: None,
|
||||
error: None,
|
||||
}
|
||||
@@ -190,78 +229,6 @@ impl Default for App {
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn next_tab(&mut self) {
|
||||
self.selected_tab = match self.selected_tab {
|
||||
Tab::Records => Tab::Structure,
|
||||
Tab::Structure => Tab::Records,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn previous_tab(&mut self) {
|
||||
self.selected_tab = match self.selected_tab {
|
||||
Tab::Records => Tab::Structure,
|
||||
Tab::Structure => Tab::Records,
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -306,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() {
|
||||
@@ -334,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!(
|
||||
|
||||
27
src/components/command.rs
Normal file
27
src/components/command.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
///
|
||||
#[derive(Clone, PartialEq, PartialOrd, Ord, Eq)]
|
||||
pub struct CommandText {
|
||||
///
|
||||
pub name: String,
|
||||
///
|
||||
pub desc: &'static str,
|
||||
///
|
||||
pub group: &'static str,
|
||||
///
|
||||
pub hide_help: bool,
|
||||
}
|
||||
|
||||
///
|
||||
pub struct CommandInfo {
|
||||
///
|
||||
pub text: CommandText,
|
||||
/// available but not active in the context
|
||||
pub enabled: bool,
|
||||
/// will show up in the quick bar
|
||||
pub quick_bar: bool,
|
||||
|
||||
/// available in current app state
|
||||
pub available: bool,
|
||||
/// used to order commands in quickbar
|
||||
pub order: i8,
|
||||
}
|
||||
148
src/components/databases.rs
Normal file
148
src/components/databases.rs
Normal file
@@ -0,0 +1,148 @@
|
||||
use super::{
|
||||
utils::scroll_vertical::VerticalScroll, CommandBlocking, CommandInfo, Component,
|
||||
DrawableComponent, EventState,
|
||||
};
|
||||
use crate::event::Key;
|
||||
use crate::ui::common_nav;
|
||||
use crate::ui::scrolllist::draw_list_block;
|
||||
use anyhow::Result;
|
||||
use database_tree::{DatabaseTree, DatabaseTreeItem};
|
||||
use std::convert::From;
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Style},
|
||||
text::Span,
|
||||
widgets::{Block, Borders},
|
||||
Frame,
|
||||
};
|
||||
|
||||
// ▸
|
||||
const FOLDER_ICON_COLLAPSED: &str = "\u{25b8}";
|
||||
// ▾
|
||||
const FOLDER_ICON_EXPANDED: &str = "\u{25be}";
|
||||
const EMPTY_STR: &str = "";
|
||||
|
||||
pub struct DatabasesComponent {
|
||||
pub tree: DatabaseTree,
|
||||
pub scroll: VerticalScroll,
|
||||
pub focused: bool,
|
||||
}
|
||||
|
||||
impl DatabasesComponent {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
tree: DatabaseTree::default(),
|
||||
scroll: VerticalScroll::new(),
|
||||
focused: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn tree_item_to_span(item: &DatabaseTreeItem, selected: bool, width: u16) -> Span<'_> {
|
||||
let name = item.kind().name();
|
||||
let indent = item.info().indent();
|
||||
|
||||
let indent_str = if indent == 0 {
|
||||
String::from("")
|
||||
} else {
|
||||
format!("{:w$}", " ", w = (indent as usize) * 2)
|
||||
};
|
||||
|
||||
let is_database = item.kind().is_database();
|
||||
let path_arrow = if is_database {
|
||||
if item.kind().is_database_collapsed() {
|
||||
FOLDER_ICON_COLLAPSED
|
||||
} else {
|
||||
FOLDER_ICON_EXPANDED
|
||||
}
|
||||
} else {
|
||||
EMPTY_STR
|
||||
};
|
||||
|
||||
let name = format!(
|
||||
"{}{}{:w$}",
|
||||
indent_str,
|
||||
path_arrow,
|
||||
name,
|
||||
w = width as usize
|
||||
);
|
||||
Span::styled(
|
||||
name,
|
||||
if selected {
|
||||
Style::default().fg(Color::Magenta).bg(Color::Green)
|
||||
} else {
|
||||
Style::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn draw_tree<B: Backend>(&self, f: &mut Frame<B>, area: Rect) {
|
||||
let tree_height = usize::from(area.height.saturating_sub(2));
|
||||
self.tree.visual_selection().map_or_else(
|
||||
|| {
|
||||
self.scroll.reset();
|
||||
},
|
||||
|selection| {
|
||||
self.scroll
|
||||
.update(selection.index, selection.count, tree_height);
|
||||
},
|
||||
);
|
||||
|
||||
let items = self
|
||||
.tree
|
||||
.iterate(self.scroll.get_top(), tree_height)
|
||||
.map(|(item, selected)| Self::tree_item_to_span(item, selected, area.width));
|
||||
|
||||
let title = "Databases";
|
||||
draw_list_block(
|
||||
f,
|
||||
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,
|
||||
);
|
||||
self.scroll.draw(f, area);
|
||||
}
|
||||
}
|
||||
|
||||
impl DrawableComponent for DatabasesComponent {
|
||||
fn draw<B: Backend>(&self, f: &mut Frame<B>, area: Rect) -> Result<()> {
|
||||
if true {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(100)].as_ref())
|
||||
.split(area);
|
||||
|
||||
self.draw_tree(f, chunks[0]);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for DatabasesComponent {
|
||||
fn commands(&self, _out: &mut Vec<CommandInfo>, _force_all: bool) -> CommandBlocking {
|
||||
CommandBlocking::PassingOn
|
||||
}
|
||||
|
||||
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 {
|
||||
if let Some(common_nav) = common_nav(key) {
|
||||
tree.move_selection(common_nav)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
85
src/components/mod.rs
Normal file
85
src/components/mod.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
pub mod command;
|
||||
pub mod databases;
|
||||
pub mod utils;
|
||||
|
||||
pub use command::{CommandInfo, CommandText};
|
||||
pub use databases::DatabasesComponent;
|
||||
|
||||
use anyhow::Result;
|
||||
use std::convert::From;
|
||||
use tui::{backend::Backend, layout::Rect, Frame};
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum ScrollType {
|
||||
Up,
|
||||
Down,
|
||||
Home,
|
||||
End,
|
||||
PageUp,
|
||||
PageDown,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum Direction {
|
||||
Up,
|
||||
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,
|
||||
NotConsumed,
|
||||
}
|
||||
|
||||
impl From<bool> for EventState {
|
||||
fn from(consumed: bool) -> Self {
|
||||
if consumed {
|
||||
Self::Consumed
|
||||
} else {
|
||||
Self::NotConsumed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// base component trait
|
||||
pub trait Component {
|
||||
fn commands(&self, out: &mut Vec<CommandInfo>, force_all: bool) -> CommandBlocking;
|
||||
|
||||
fn event(&mut self, key: crate::event::Key) -> Result<EventState>;
|
||||
|
||||
fn focused(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
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();
|
||||
Ok(())
|
||||
} else {
|
||||
self.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
1
src/components/utils/mod.rs
Normal file
1
src/components/utils/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod scroll_vertical;
|
||||
94
src/components/utils/scroll_vertical.rs
Normal file
94
src/components/utils/scroll_vertical.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
use std::cell::Cell;
|
||||
|
||||
use tui::{backend::Backend, layout::Rect, Frame};
|
||||
|
||||
use crate::{components::ScrollType, ui::scrollbar::draw_scrollbar};
|
||||
|
||||
pub struct VerticalScroll {
|
||||
top: Cell<usize>,
|
||||
max_top: Cell<usize>,
|
||||
}
|
||||
|
||||
impl VerticalScroll {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
top: Cell::new(0),
|
||||
max_top: Cell::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_top(&self) -> usize {
|
||||
self.top.get()
|
||||
}
|
||||
|
||||
pub fn reset(&self) {
|
||||
self.top.set(0);
|
||||
}
|
||||
|
||||
pub fn _move_top(&self, move_type: ScrollType) -> bool {
|
||||
let old = self.top.get();
|
||||
let max = self.max_top.get();
|
||||
|
||||
let new_scroll_top = match move_type {
|
||||
ScrollType::Down => old.saturating_add(1),
|
||||
ScrollType::Up => old.saturating_sub(1),
|
||||
ScrollType::Home => 0,
|
||||
ScrollType::End => max,
|
||||
_ => old,
|
||||
};
|
||||
|
||||
let new_scroll_top = new_scroll_top.clamp(0, max);
|
||||
|
||||
if new_scroll_top == old {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.top.set(new_scroll_top);
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn update(&self, selection: usize, selection_max: usize, visual_height: usize) -> usize {
|
||||
let new_top = calc_scroll_top(self.get_top(), visual_height, selection, selection_max);
|
||||
self.top.set(new_top);
|
||||
|
||||
if visual_height == 0 {
|
||||
self.max_top.set(0);
|
||||
} else {
|
||||
let new_max = selection_max.saturating_sub(visual_height);
|
||||
self.max_top.set(new_max);
|
||||
}
|
||||
|
||||
new_top
|
||||
}
|
||||
|
||||
pub fn _update_no_selection(&self, line_count: usize, visual_height: usize) -> usize {
|
||||
self.update(self.get_top(), line_count, visual_height)
|
||||
}
|
||||
|
||||
pub fn draw<B: Backend>(&self, f: &mut Frame<B>, r: Rect) {
|
||||
draw_scrollbar(f, r, self.max_top.get(), self.top.get());
|
||||
}
|
||||
}
|
||||
|
||||
const fn calc_scroll_top(
|
||||
current_top: usize,
|
||||
height_in_lines: usize,
|
||||
selection: usize,
|
||||
selection_max: usize,
|
||||
) -> usize {
|
||||
if height_in_lines == 0 {
|
||||
return 0;
|
||||
}
|
||||
if selection_max <= height_in_lines {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if current_top + height_in_lines <= selection {
|
||||
selection.saturating_sub(height_in_lines) + 1
|
||||
} else if current_top > selection {
|
||||
selection
|
||||
} else {
|
||||
current_top
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ impl Default for EventConfig {
|
||||
}
|
||||
|
||||
/// An occurred event.
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum Event<I> {
|
||||
/// An input event occurred.
|
||||
Input(I),
|
||||
|
||||
@@ -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 => get_databases(app.pool.as_ref().unwrap()).await?,
|
||||
},
|
||||
None => vec![],
|
||||
None => {
|
||||
app.databases.tree = DatabaseTree::new(
|
||||
get_databases(app.pool.as_ref().unwrap()).await?.as_slice(),
|
||||
&BTreeSet::new(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
_ => (),
|
||||
|
||||
@@ -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,37 +2,32 @@ pub mod connection_list;
|
||||
pub mod database_list;
|
||||
pub mod query;
|
||||
pub mod record_table;
|
||||
pub mod table_list;
|
||||
|
||||
use crate::app::{App, FocusBlock};
|
||||
use crate::app::{App, FocusBlock, Tab};
|
||||
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::Right => app.next_tab(),
|
||||
Key::Left => app.previous_tab(),
|
||||
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::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(())
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
mod app;
|
||||
mod components;
|
||||
mod event;
|
||||
mod handlers;
|
||||
mod ui;
|
||||
|
||||
125
src/ui/mod.rs
125
src/ui/mod.rs
@@ -1,4 +1,7 @@
|
||||
use crate::app::{App, FocusBlock, Tab};
|
||||
use crate::components::DrawableComponent as _;
|
||||
use crate::event::Key;
|
||||
use database_tree::MoveSelection;
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
@@ -9,6 +12,9 @@ use tui::{
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
pub mod scrollbar;
|
||||
pub mod scrolllist;
|
||||
|
||||
pub fn draw<B: Backend>(f: &mut Frame<'_, B>, app: &mut App) -> anyhow::Result<()> {
|
||||
if let FocusBlock::ConnectionList = app.focus_block {
|
||||
let percent_x = 60;
|
||||
@@ -62,51 +68,10 @@ pub fn draw<B: Backend>(f: &mut Frame<'_, B>, app: &mut App) -> anyhow::Result<(
|
||||
.split(f.size());
|
||||
|
||||
let left_chunks = Layout::default()
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(9),
|
||||
Constraint::Min(8),
|
||||
Constraint::Length(7),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.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);
|
||||
|
||||
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()
|
||||
@@ -119,7 +84,7 @@ pub fn draw<B: Backend>(f: &mut Frame<'_, B>, app: &mut App) -> anyhow::Result<(
|
||||
let tasks = List::new(table_status)
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.highlight_style(Style::default().fg(Color::Green));
|
||||
f.render_widget(tasks, left_chunks[2]);
|
||||
f.render_widget(tasks, left_chunks[1]);
|
||||
|
||||
let right_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
@@ -147,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() {
|
||||
@@ -200,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);
|
||||
@@ -209,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;
|
||||
@@ -282,3 +209,21 @@ fn draw_error_popup<B: Backend>(f: &mut Frame<'_, B>, error: String) -> anyhow::
|
||||
f.render_widget(error, area);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn common_nav(key: Key) -> Option<MoveSelection> {
|
||||
if key == Key::Char('j') {
|
||||
Some(MoveSelection::Down)
|
||||
} 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::Char('l') {
|
||||
Some(MoveSelection::Right)
|
||||
} else if key == Key::Char('h') {
|
||||
Some(MoveSelection::Left)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
75
src/ui/scrollbar.rs
Normal file
75
src/ui/scrollbar.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use easy_cast::CastFloat;
|
||||
use std::convert::TryFrom;
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
buffer::Buffer,
|
||||
layout::{Margin, Rect},
|
||||
style::Style,
|
||||
symbols::{block::FULL, line::DOUBLE_VERTICAL},
|
||||
widgets::Widget,
|
||||
Frame,
|
||||
};
|
||||
|
||||
///
|
||||
struct Scrollbar {
|
||||
max: u16,
|
||||
pos: u16,
|
||||
style_bar: Style,
|
||||
style_pos: Style,
|
||||
}
|
||||
|
||||
impl Scrollbar {
|
||||
fn new(max: usize, pos: usize) -> Self {
|
||||
Self {
|
||||
max: u16::try_from(max).unwrap_or_default(),
|
||||
pos: u16::try_from(pos).unwrap_or_default(),
|
||||
style_pos: Style::default(),
|
||||
style_bar: Style::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Scrollbar {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
if area.height <= 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.max == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let right = area.right().saturating_sub(1);
|
||||
if right <= area.left() {
|
||||
return;
|
||||
};
|
||||
|
||||
let (bar_top, bar_height) = {
|
||||
let scrollbar_area = area.inner(&Margin {
|
||||
horizontal: 0,
|
||||
vertical: 1,
|
||||
});
|
||||
|
||||
(scrollbar_area.top(), scrollbar_area.height)
|
||||
};
|
||||
|
||||
for y in bar_top..(bar_top + bar_height) {
|
||||
buf.set_string(right, y, DOUBLE_VERTICAL, self.style_bar);
|
||||
}
|
||||
|
||||
let progress = f32::from(self.pos) / f32::from(self.max);
|
||||
let progress = if progress > 1.0 { 1.0 } else { progress };
|
||||
let pos = f32::from(bar_height) * progress;
|
||||
|
||||
let pos: u16 = pos.cast_nearest();
|
||||
let pos = pos.saturating_sub(1);
|
||||
|
||||
buf.set_string(right, bar_top + pos, FULL, self.style_pos);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_scrollbar<B: Backend>(f: &mut Frame<B>, r: Rect, max: usize, pos: usize) {
|
||||
let mut widget = Scrollbar::new(max, pos);
|
||||
widget.style_pos = Style::default();
|
||||
f.render_widget(widget, r);
|
||||
}
|
||||
61
src/ui/scrolllist.rs
Normal file
61
src/ui/scrolllist.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
use std::iter::Iterator;
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::Style,
|
||||
text::Span,
|
||||
widgets::{Block, List, ListItem, Widget},
|
||||
Frame,
|
||||
};
|
||||
|
||||
///
|
||||
struct ScrollableList<'b, L>
|
||||
where
|
||||
L: Iterator<Item = Span<'b>>,
|
||||
{
|
||||
block: Option<Block<'b>>,
|
||||
/// Items to be displayed
|
||||
items: L,
|
||||
/// Base style of the widget
|
||||
style: Style,
|
||||
}
|
||||
|
||||
impl<'b, L> ScrollableList<'b, L>
|
||||
where
|
||||
L: Iterator<Item = Span<'b>>,
|
||||
{
|
||||
fn new(items: L) -> Self {
|
||||
Self {
|
||||
block: None,
|
||||
items,
|
||||
style: Style::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn block(mut self, block: Block<'b>) -> Self {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'b, L> Widget for ScrollableList<'b, L>
|
||||
where
|
||||
L: Iterator<Item = Span<'b>>,
|
||||
{
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
// Render items
|
||||
List::new(self.items.map(ListItem::new).collect::<Vec<ListItem>>())
|
||||
.block(self.block.unwrap_or_default())
|
||||
.style(self.style)
|
||||
.render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_list_block<'b, B: Backend, L>(f: &mut Frame<B>, r: Rect, block: Block<'b>, items: L)
|
||||
where
|
||||
L: Iterator<Item = Span<'b>>,
|
||||
{
|
||||
let list = ScrollableList::new(items).block(block);
|
||||
f.render_widget(list, r);
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user