Add key config (#33)

* implement key config

* use key config in help component

* use key config in databases component

* use key config in error component

* use key config in connections component

* set commands

* use database/table as a table component name

* fix a test for get_text

* use key config in tab component

* fix function name

* add focus_connections key

* use quit exit key
pull/36/head
Takayuki Maeda 3 years ago committed by GitHub
parent 3c00d78326
commit 07b2b5090f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -31,9 +31,9 @@ $ cargo install --version 0.1.0-alpha.0 gobang
| Key | Description | | Key | Description |
| ---- | ---- | | ---- | ---- |
| <kbd>h</kbd> | move left | | <kbd>h</kbd> | scroll left |
| <kbd>j</kbd> | move down | | <kbd>j</kbd> | scroll down |
| <kbd>k</kbd> | move up | | <kbd>k</kbd> | scroll up |
| <kbd>l</kbd> | move right | | <kbd>l</kbd> | scroll right |
| <kbd>Ctrl</kbd> + <kbd>d</kbd> | scroll down multiple lines | | <kbd>Ctrl</kbd> + <kbd>d</kbd> | scroll down multiple lines |
| <kbd>Ctrl</kbd> + <kbd>u</kbd> | scroll up multiple lines | | <kbd>Ctrl</kbd> + <kbd>u</kbd> | scroll up multiple lines |

@ -1,8 +1,8 @@
use crate::Table;
use crate::{ use crate::{
databasetreeitems::DatabaseTreeItems, error::Result, item::DatabaseTreeItemKind, databasetreeitems::DatabaseTreeItems, error::Result, item::DatabaseTreeItemKind,
tree_iter::TreeIterator, tree_iter::TreeIterator,
}; };
use crate::{Database, Table};
use std::{collections::BTreeSet, usize}; use std::{collections::BTreeSet, usize};
/// ///
@ -14,8 +14,6 @@ pub enum MoveSelection {
Right, Right,
Top, Top,
End, End,
PageDown,
PageUp,
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@ -76,13 +74,13 @@ impl DatabaseTree {
.and_then(|index| self.items.tree_items.get(index)) .and_then(|index| self.items.tree_items.get(index))
} }
pub fn selected_table(&self) -> Option<(Table, String)> { pub fn selected_table(&self) -> Option<(Database, Table)> {
self.selection.and_then(|index| { self.selection.and_then(|index| {
let item = &self.items.tree_items[index]; let item = &self.items.tree_items[index];
match item.kind() { match item.kind() {
DatabaseTreeItemKind::Database { .. } => None, DatabaseTreeItemKind::Database { .. } => None,
DatabaseTreeItemKind::Table { table, database } => { DatabaseTreeItemKind::Table { table, database } => {
Some((table.clone(), database.clone())) Some((database.clone(), table.clone()))
} }
} }
}) })
@ -109,7 +107,6 @@ impl DatabaseTree {
MoveSelection::Right => self.selection_right(selection), MoveSelection::Right => self.selection_right(selection),
MoveSelection::Top => Self::selection_start(selection), MoveSelection::Top => Self::selection_start(selection),
MoveSelection::End => self.selection_end(selection), MoveSelection::End => self.selection_end(selection),
MoveSelection::PageDown | MoveSelection::PageUp => None,
}; };
let changed_index = new_index.map(|i| i != selection).unwrap_or_default(); let changed_index = new_index.map(|i| i != selection).unwrap_or_default();

@ -34,7 +34,7 @@ impl TreeItemInfo {
#[derive(PartialEq, Debug, Clone)] #[derive(PartialEq, Debug, Clone)]
pub enum DatabaseTreeItemKind { pub enum DatabaseTreeItemKind {
Database { name: String, collapsed: bool }, Database { name: String, collapsed: bool },
Table { database: String, table: Table }, Table { database: Database, table: Table },
} }
impl DatabaseTreeItemKind { impl DatabaseTreeItemKind {
@ -63,7 +63,7 @@ impl DatabaseTreeItemKind {
pub fn database_name(&self) -> Option<String> { pub fn database_name(&self) -> Option<String> {
match self { match self {
Self::Database { .. } => None, Self::Database { .. } => None,
Self::Table { database, .. } => Some(database.clone()), Self::Table { database, .. } => Some(database.name.clone()),
} }
} }
} }
@ -82,7 +82,7 @@ impl DatabaseTreeItem {
Ok(Self { Ok(Self {
info: TreeItemInfo::new(indent, false), info: TreeItemInfo::new(indent, false),
kind: DatabaseTreeItemKind::Table { kind: DatabaseTreeItemKind::Table {
database: database.name.clone(), database: database.clone(),
table: table.clone(), table: table.clone(),
}, },
}) })

@ -11,7 +11,7 @@ pub use crate::{
item::{DatabaseTreeItem, TreeItemInfo}, item::{DatabaseTreeItem, TreeItemInfo},
}; };
#[derive(Clone)] #[derive(Clone, PartialEq, Debug)]
pub struct Database { pub struct Database {
pub name: String, pub name: String,
pub tables: Vec<Table>, pub tables: Vec<Table>,

@ -5,10 +5,10 @@ use crate::utils::{MySqlPool, Pool};
use crate::{ use crate::{
components::tab::Tab, components::tab::Tab,
components::{ components::{
ConnectionsComponent, DatabasesComponent, ErrorComponent, HelpComponent, command, ConnectionsComponent, DatabasesComponent, ErrorComponent, HelpComponent,
RecordTableComponent, TabComponent, TableComponent, TableStatusComponent, RecordTableComponent, TabComponent, TableComponent, TableStatusComponent,
}, },
user_config::UserConfig, config::Config,
}; };
use database_tree::Database; use database_tree::Database;
use tui::{ use tui::{
@ -33,36 +33,25 @@ pub struct App {
table_status: TableStatusComponent, table_status: TableStatusComponent,
clipboard: Clipboard, clipboard: Clipboard,
pool: Option<Box<dyn Pool>>, pool: Option<Box<dyn Pool>>,
pub user_config: Option<UserConfig>, pub config: Config,
pub error: ErrorComponent, pub error: ErrorComponent,
} }
impl Default for App {
fn default() -> App {
App {
record_table: RecordTableComponent::default(),
structure_table: TableComponent::default(),
focus: Focus::DabataseList,
tab: TabComponent::default(),
help: HelpComponent::new(),
user_config: None,
databases: DatabasesComponent::new(),
connections: ConnectionsComponent::default(),
table_status: TableStatusComponent::default(),
clipboard: Clipboard::new(),
pool: None,
error: ErrorComponent::default(),
}
}
}
impl App { impl App {
pub fn new(user_config: UserConfig) -> App { pub fn new(config: Config) -> App {
Self { Self {
user_config: Some(user_config.clone()), config: config.clone(),
connections: ConnectionsComponent::new(user_config.conn), connections: ConnectionsComponent::new(config.key_config.clone(), config.conn),
record_table: RecordTableComponent::new(config.key_config.clone()),
structure_table: TableComponent::new(config.key_config.clone()),
tab: TabComponent::new(config.key_config.clone()),
help: HelpComponent::new(config.key_config.clone()),
databases: DatabasesComponent::new(config.key_config.clone()),
table_status: TableStatusComponent::default(),
error: ErrorComponent::new(config.key_config),
focus: Focus::ConnectionList, focus: Focus::ConnectionList,
..App::default() clipboard: Clipboard::new(),
pool: None,
} }
} }
@ -119,34 +108,24 @@ impl App {
} }
fn commands(&self) -> Vec<CommandInfo> { fn commands(&self) -> Vec<CommandInfo> {
let res = vec![ let mut res = vec![
CommandInfo::new(crate::components::command::move_left("h"), true, true), CommandInfo::new(command::scroll(&self.config.key_config)),
CommandInfo::new(crate::components::command::move_down("j"), true, true), CommandInfo::new(command::scroll_to_top_bottom(&self.config.key_config)),
CommandInfo::new(crate::components::command::move_up("k"), true, true), CommandInfo::new(command::move_focus(&self.config.key_config)),
CommandInfo::new(crate::components::command::move_right("l"), true, true), CommandInfo::new(command::filter(&self.config.key_config)),
CommandInfo::new(crate::components::command::filter("/"), true, true), CommandInfo::new(command::help(&self.config.key_config)),
CommandInfo::new( CommandInfo::new(command::toggle_tabs(&self.config.key_config)),
crate::components::command::move_focus_to_right_widget(
Key::Right.to_string().as_str(),
),
true,
true,
),
]; ];
self.databases.commands(&mut res);
self.record_table.commands(&mut res);
res res
} }
pub async fn event(&mut self, key: Key) -> anyhow::Result<EventState> { pub async fn event(&mut self, key: Key) -> anyhow::Result<EventState> {
self.update_commands(); self.update_commands();
if let Key::Esc = key {
if self.error.error.is_some() {
self.error.error = None;
return Ok(EventState::Consumed);
}
}
if self.components_event(key).await?.is_consumed() { if self.components_event(key).await?.is_consumed() {
return Ok(EventState::Consumed); return Ok(EventState::Consumed);
}; };
@ -158,7 +137,11 @@ impl App {
} }
pub async fn components_event(&mut self, key: Key) -> anyhow::Result<EventState> { pub async fn components_event(&mut self, key: Key) -> anyhow::Result<EventState> {
if self.help.event(key)?.is_consumed() { if self.error.event(key)?.is_consumed() {
return Ok(EventState::Consumed);
}
if !matches!(self.focus, Focus::ConnectionList) && self.help.event(key)?.is_consumed() {
return Ok(EventState::Consumed); return Ok(EventState::Consumed);
} }
@ -168,8 +151,7 @@ impl App {
return Ok(EventState::Consumed); return Ok(EventState::Consumed);
} }
if let Key::Enter = key { if key == self.config.key_config.enter {
self.record_table.reset();
if let Some(conn) = self.connections.selected_connection() { if let Some(conn) = self.connections.selected_connection() {
if let Some(pool) = self.pool.as_ref() { if let Some(pool) = self.pool.as_ref() {
pool.close().await; pool.close().await;
@ -189,7 +171,8 @@ impl App {
None => self.pool.as_ref().unwrap().get_databases().await?, None => self.pool.as_ref().unwrap().get_databases().await?,
}; };
self.databases.update(databases.as_slice()).unwrap(); self.databases.update(databases.as_slice()).unwrap();
self.focus = Focus::DabataseList self.focus = Focus::DabataseList;
self.record_table.reset();
} }
return Ok(EventState::Consumed); return Ok(EventState::Consumed);
} }
@ -199,25 +182,30 @@ impl App {
return Ok(EventState::Consumed); return Ok(EventState::Consumed);
} }
if matches!(key, Key::Enter) && self.databases.tree_focused() { if key == self.config.key_config.enter && self.databases.tree_focused() {
if let Some((table, database)) = self.databases.tree().selected_table() { if let Some((database, table)) = self.databases.tree().selected_table() {
self.focus = Focus::Table; self.focus = Focus::Table;
let (headers, records) = self let (headers, records) = self
.pool .pool
.as_ref() .as_ref()
.unwrap() .unwrap()
.get_records(&database, &table.name, 0, None) .get_records(&database.name, &table.name, 0, None)
.await?; .await?;
self.record_table = RecordTableComponent::new(records, headers); self.record_table
self.record_table.set_table(table.name.to_string()); .update(records, headers, database.clone(), table.clone());
let (headers, records) = self let (headers, records) = self
.pool .pool
.as_ref() .as_ref()
.unwrap() .unwrap()
.get_columns(&database, &table.name) .get_columns(&database.name, &table.name)
.await?; .await?;
self.structure_table = TableComponent::new(records, headers); self.structure_table.update(
records,
headers,
database.clone(),
table.clone(),
);
self.table_status self.table_status
.update(self.record_table.len() as u64, table); .update(self.record_table.len() as u64, table);
} }
@ -231,22 +219,23 @@ impl App {
return Ok(EventState::Consumed); return Ok(EventState::Consumed);
}; };
if let Key::Char('y') = key { if key == self.config.key_config.copy {
if let Some(text) = self.record_table.table.selected_cells() { if let Some(text) = self.record_table.table.selected_cells() {
self.clipboard.store(text) self.clipboard.store(text)
} }
} }
if matches!(key, Key::Enter) && self.record_table.filter_focused() { if key == self.config.key_config.enter && self.record_table.filter_focused()
{
self.record_table.focus = crate::components::record_table::Focus::Table; self.record_table.focus = crate::components::record_table::Focus::Table;
if let Some((table, database)) = self.databases.tree().selected_table() if let Some((database, table)) = self.databases.tree().selected_table()
{ {
let (headers, records) = self let (headers, records) = self
.pool .pool
.as_ref() .as_ref()
.unwrap() .unwrap()
.get_records( .get_records(
&database.clone(), &database.name.clone(),
&table.name, &table.name,
0, 0,
if self.record_table.filter.input.is_empty() { if self.record_table.filter.input.is_empty() {
@ -256,7 +245,7 @@ impl App {
}, },
) )
.await?; .await?;
self.record_table.update(records, headers); self.record_table.update(records, headers, database, table);
} }
} }
@ -269,7 +258,7 @@ impl App {
% crate::utils::RECORDS_LIMIT_PER_PAGE as usize % crate::utils::RECORDS_LIMIT_PER_PAGE as usize
== 0 == 0
{ {
if let Some((table, database)) = if let Some((database, table)) =
self.databases.tree().selected_table() self.databases.tree().selected_table()
{ {
let (_, records) = self let (_, records) = self
@ -277,7 +266,7 @@ impl App {
.as_ref() .as_ref()
.unwrap() .unwrap()
.get_records( .get_records(
&database.clone(), &database.name.clone(),
&table.name, &table.name,
index as u16, index as u16,
if self.record_table.filter.input.is_empty() { if self.record_table.filter.input.is_empty() {
@ -301,7 +290,7 @@ impl App {
return Ok(EventState::Consumed); return Ok(EventState::Consumed);
}; };
if let Key::Char('y') = key { if key == self.config.key_config.copy {
if let Some(text) = self.structure_table.selected_cells() { if let Some(text) = self.structure_table.selected_cells() {
self.clipboard.store(text) self.clipboard.store(text)
} }
@ -314,7 +303,7 @@ impl App {
} }
pub fn move_focus(&mut self, key: Key) -> anyhow::Result<EventState> { pub fn move_focus(&mut self, key: Key) -> anyhow::Result<EventState> {
if let Key::Char('c') = key { if key == self.config.key_config.focus_connections {
self.focus = Focus::ConnectionList; self.focus = Focus::ConnectionList;
return Ok(EventState::Consumed); return Ok(EventState::Consumed);
} }
@ -323,19 +312,19 @@ impl App {
} }
match self.focus { match self.focus {
Focus::ConnectionList => { Focus::ConnectionList => {
if let Key::Enter = key { if key == self.config.key_config.enter {
self.focus = Focus::DabataseList; self.focus = Focus::DabataseList;
return Ok(EventState::Consumed); return Ok(EventState::Consumed);
} }
} }
Focus::DabataseList => { Focus::DabataseList => {
if matches!(key, Key::Right) && self.databases.tree_focused() { if key == self.config.key_config.focus_right && self.databases.tree_focused() {
self.focus = Focus::Table; self.focus = Focus::Table;
return Ok(EventState::Consumed); return Ok(EventState::Consumed);
} }
} }
Focus::Table => { Focus::Table => {
if let Key::Left = key { if key == self.config.key_config.focus_left {
self.focus = Focus::DabataseList; self.focus = Focus::DabataseList;
return Ok(EventState::Consumed); return Ok(EventState::Consumed);
} }

@ -1,18 +1,20 @@
use crate::config::KeyConfig;
static CMD_GROUP_GENERAL: &str = "-- General --"; static CMD_GROUP_GENERAL: &str = "-- General --";
static CMD_GROUP_TABLE: &str = "-- Table --";
static CMD_GROUP_DATABASES: &str = "-- Databases --";
#[derive(Clone, PartialEq, PartialOrd, Ord, Eq)] #[derive(Clone, PartialEq, PartialOrd, Ord, Eq)]
pub struct CommandText { pub struct CommandText {
pub name: String, pub name: String,
pub desc: &'static str,
pub group: &'static str, pub group: &'static str,
pub hide_help: bool, pub hide_help: bool,
} }
impl CommandText { impl CommandText {
pub const fn new(name: String, desc: &'static str, group: &'static str) -> Self { pub const fn new(name: String, group: &'static str) -> Self {
Self { Self {
name, name,
desc,
group, group,
hide_help: false, hide_help: false,
} }
@ -21,70 +23,103 @@ impl CommandText {
pub struct CommandInfo { pub struct CommandInfo {
pub text: CommandText, pub text: CommandText,
pub enabled: bool,
pub quick_bar: bool,
pub available: bool,
pub order: i8,
} }
impl CommandInfo { impl CommandInfo {
pub const fn new(text: CommandText, enabled: bool, available: bool) -> Self { pub const fn new(text: CommandText) -> Self {
Self { Self { text }
text,
enabled,
quick_bar: true,
available,
order: 0,
}
} }
}
pub const fn order(self, order: i8) -> Self { pub fn scroll(key: &KeyConfig) -> CommandText {
let mut res = self; CommandText::new(
res.order = order; format!(
res "Scroll up/down/left/right [{},{},{},{}]",
} key.scroll_up.to_string(),
key.scroll_down.to_string(),
key.scroll_left.to_string(),
key.scroll_right.to_string()
),
CMD_GROUP_GENERAL,
)
} }
pub fn move_down(key: &str) -> CommandText { pub fn scroll_to_top_bottom(key: &KeyConfig) -> CommandText {
CommandText::new( CommandText::new(
format!("Move down [{}]", key), format!(
"move down", "Scroll to top/bottom [{},{}]",
key.scroll_to_top.to_string(),
key.scroll_to_bottom.to_string(),
),
CMD_GROUP_GENERAL, CMD_GROUP_GENERAL,
) )
} }
pub fn move_up(key: &str) -> CommandText { pub fn expand_collapse(key: &KeyConfig) -> CommandText {
CommandText::new(format!("Move up [{}]", key), "move up", CMD_GROUP_GENERAL) CommandText::new(
format!(
"Expand/Collapse [{},{}]",
key.scroll_right.to_string(),
key.scroll_left.to_string(),
),
CMD_GROUP_DATABASES,
)
} }
pub fn move_right(key: &str) -> CommandText { pub fn filter(key: &KeyConfig) -> CommandText {
CommandText::new( CommandText::new(
format!("Move right [{}]", key), format!("Filter [{}]", key.filter.to_string()),
"move right",
CMD_GROUP_GENERAL, CMD_GROUP_GENERAL,
) )
} }
pub fn move_left(key: &str) -> CommandText { pub fn move_focus(key: &KeyConfig) -> CommandText {
CommandText::new( CommandText::new(
format!("Move left [{}]", key), format!(
"move left", "Move focus to left/right [{},{}]",
key.focus_left, key.focus_right
),
CMD_GROUP_GENERAL, CMD_GROUP_GENERAL,
) )
} }
pub fn filter(key: &str) -> CommandText { pub fn extend_selection_by_one_cell(key: &KeyConfig) -> CommandText {
CommandText::new(
format!(
"Extend selection by one cell up/down/left/right [{},{},{},{}]",
key.extend_selection_by_one_cell_up,
key.extend_selection_by_one_cell_down,
key.extend_selection_by_one_cell_left,
key.extend_selection_by_one_cell_right
),
CMD_GROUP_TABLE,
)
}
pub fn tab_records(key: &KeyConfig) -> CommandText {
CommandText::new(format!("Records [{}]", key.tab_records), CMD_GROUP_TABLE)
}
pub fn tab_structure(key: &KeyConfig) -> CommandText {
CommandText::new(
format!("Structure [{}]", key.tab_structure),
CMD_GROUP_TABLE,
)
}
pub fn toggle_tabs(key_config: &KeyConfig) -> CommandText {
CommandText::new( CommandText::new(
format!("Filter [{}]", key), format!(
"enter input for filter", "Tab [{},{}]",
key_config.tab_records, key_config.tab_structure
),
CMD_GROUP_GENERAL, CMD_GROUP_GENERAL,
) )
} }
pub fn move_focus_to_right_widget(key: &str) -> CommandText { pub fn help(key_config: &KeyConfig) -> CommandText {
CommandText::new( CommandText::new(
format!("Move focus to right [{}]", key), format!("Help [{}]", key_config.open_help),
"move focus to right",
CMD_GROUP_GENERAL, CMD_GROUP_GENERAL,
) )
} }

@ -1,7 +1,7 @@
use super::{Component, DrawableComponent, EventState}; use super::{Component, DrawableComponent, EventState};
use crate::components::command::CommandInfo; use crate::components::command::CommandInfo;
use crate::config::{Connection, KeyConfig};
use crate::event::Key; use crate::event::Key;
use crate::user_config::Connection;
use anyhow::Result; use anyhow::Result;
use tui::{ use tui::{
backend::Backend, backend::Backend,
@ -15,22 +15,15 @@ use tui::{
pub struct ConnectionsComponent { pub struct ConnectionsComponent {
connections: Vec<Connection>, connections: Vec<Connection>,
state: ListState, state: ListState,
} key_config: KeyConfig,
impl Default for ConnectionsComponent {
fn default() -> Self {
Self {
connections: Vec::new(),
state: ListState::default(),
}
}
} }
impl ConnectionsComponent { impl ConnectionsComponent {
pub fn new(connections: Vec<Connection>) -> Self { pub fn new(key_config: KeyConfig, connections: Vec<Connection>) -> Self {
Self { Self {
connections, connections,
..Self::default() key_config,
state: ListState::default(),
} }
} }
@ -100,19 +93,15 @@ impl DrawableComponent for ConnectionsComponent {
} }
impl Component for ConnectionsComponent { impl Component for ConnectionsComponent {
fn commands(&self, out: &mut Vec<CommandInfo>) {} fn commands(&self, _out: &mut Vec<CommandInfo>) {}
fn event(&mut self, key: Key) -> Result<EventState> { fn event(&mut self, key: Key) -> Result<EventState> {
match key { if key == self.key_config.scroll_down {
Key::Char('j') => { self.next_connection();
self.next_connection(); return Ok(EventState::Consumed);
return Ok(EventState::Consumed); } else if key == self.key_config.scroll_up {
} self.previous_connection();
Key::Char('k') => { return Ok(EventState::Consumed);
self.previous_connection();
return Ok(EventState::Consumed);
}
_ => (),
} }
Ok(EventState::NotConsumed) Ok(EventState::NotConsumed)
} }

@ -2,7 +2,8 @@ use super::{
compute_character_width, utils::scroll_vertical::VerticalScroll, Component, DrawableComponent, compute_character_width, utils::scroll_vertical::VerticalScroll, Component, DrawableComponent,
EventState, EventState,
}; };
use crate::components::command::CommandInfo; use crate::components::command::{self, CommandInfo};
use crate::config::KeyConfig;
use crate::event::Key; use crate::event::Key;
use crate::ui::common_nav; use crate::ui::common_nav;
use crate::ui::scrolllist::draw_list_block; use crate::ui::scrolllist::draw_list_block;
@ -26,6 +27,7 @@ const FOLDER_ICON_COLLAPSED: &str = "\u{25b8}";
const FOLDER_ICON_EXPANDED: &str = "\u{25be}"; const FOLDER_ICON_EXPANDED: &str = "\u{25be}";
const EMPTY_STR: &str = ""; const EMPTY_STR: &str = "";
#[derive(PartialEq)]
pub enum FocusBlock { pub enum FocusBlock {
Filter, Filter,
Tree, Tree,
@ -39,10 +41,11 @@ pub struct DatabasesComponent {
input_idx: usize, input_idx: usize,
input_cursor_position: u16, input_cursor_position: u16,
focus_block: FocusBlock, focus_block: FocusBlock,
key_config: KeyConfig,
} }
impl DatabasesComponent { impl DatabasesComponent {
pub fn new() -> Self { pub fn new(key_config: KeyConfig) -> Self {
Self { Self {
tree: DatabaseTree::default(), tree: DatabaseTree::default(),
filterd_tree: None, filterd_tree: None,
@ -51,6 +54,7 @@ impl DatabasesComponent {
input_idx: 0, input_idx: 0,
input_cursor_position: 0, input_cursor_position: 0,
focus_block: FocusBlock::Tree, focus_block: FocusBlock::Tree,
key_config,
} }
} }
@ -192,7 +196,9 @@ impl DrawableComponent for DatabasesComponent {
} }
impl Component for DatabasesComponent { impl Component for DatabasesComponent {
fn commands(&self, out: &mut Vec<CommandInfo>) {} fn commands(&self, out: &mut Vec<CommandInfo>) {
out.push(CommandInfo::new(command::expand_collapse(&self.key_config)))
}
fn event(&mut self, key: Key) -> Result<EventState> { fn event(&mut self, key: Key) -> Result<EventState> {
let input_str: String = self.input.iter().collect(); let input_str: String = self.input.iter().collect();
@ -203,15 +209,16 @@ impl Component for DatabasesComponent {
&mut self.tree &mut self.tree
}, },
key, key,
&self.key_config,
) { ) {
return Ok(EventState::Consumed); return Ok(EventState::Consumed);
} }
if key == self.key_config.filter && self.focus_block == FocusBlock::Tree {
self.focus_block = FocusBlock::Filter;
return Ok(EventState::Consumed);
}
match key { match key {
Key::Char('/') if matches!(self.focus_block, FocusBlock::Tree) => { Key::Char(c) if self.focus_block == FocusBlock::Filter => {
self.focus_block = FocusBlock::Filter;
return Ok(EventState::Consumed);
}
Key::Char(c) if matches!(self.focus_block, FocusBlock::Filter) => {
self.input.insert(self.input_idx, c); self.input.insert(self.input_idx, c);
self.input_idx += 1; self.input_idx += 1;
self.input_cursor_position += compute_character_width(c); self.input_cursor_position += compute_character_width(c);
@ -276,8 +283,8 @@ impl Component for DatabasesComponent {
} }
} }
fn tree_nav(tree: &mut DatabaseTree, key: Key) -> bool { fn tree_nav(tree: &mut DatabaseTree, key: Key, key_config: &KeyConfig) -> bool {
if let Some(common_nav) = common_nav(key) { if let Some(common_nav) = common_nav(key, key_config) {
tree.move_selection(common_nav) tree.move_selection(common_nav)
} else { } else {
false false

@ -1,5 +1,6 @@
use super::{Component, DrawableComponent, EventState}; use super::{Component, DrawableComponent, EventState};
use crate::components::command::CommandInfo; use crate::components::command::CommandInfo;
use crate::config::KeyConfig;
use crate::event::Key; use crate::event::Key;
use anyhow::Result; use anyhow::Result;
use tui::{ use tui::{
@ -11,27 +12,34 @@ use tui::{
}; };
pub struct ErrorComponent { pub struct ErrorComponent {
pub error: Option<String>, pub error: String,
visible: bool,
key_config: KeyConfig,
} }
impl Default for ErrorComponent { impl ErrorComponent {
fn default() -> Self { pub fn new(key_config: KeyConfig) -> Self {
Self { error: None } Self {
error: String::new(),
visible: false,
key_config,
}
} }
} }
impl ErrorComponent { impl ErrorComponent {
pub fn set(&mut self, error: String) { pub fn set(&mut self, error: String) -> anyhow::Result<()> {
self.error = Some(error); self.error = error;
self.show()
} }
} }
impl DrawableComponent for ErrorComponent { impl DrawableComponent for ErrorComponent {
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, _area: Rect, _focused: bool) -> Result<()> { fn draw<B: Backend>(&mut self, f: &mut Frame<B>, _area: Rect, _focused: bool) -> Result<()> {
if let Some(error) = self.error.as_ref() { if self.visible {
let width = 65; let width = 65;
let height = 10; let height = 10;
let error = Paragraph::new(error.to_string()) let error = Paragraph::new(self.error.to_string())
.block(Block::default().title("Error").borders(Borders::ALL)) .block(Block::default().title("Error").borders(Borders::ALL))
.style(Style::default().fg(Color::Red)) .style(Style::default().fg(Color::Red))
.alignment(Alignment::Left) .alignment(Alignment::Left)
@ -50,9 +58,27 @@ impl DrawableComponent for ErrorComponent {
} }
impl Component for ErrorComponent { impl Component for ErrorComponent {
fn commands(&self, out: &mut Vec<CommandInfo>) {} fn commands(&self, _out: &mut Vec<CommandInfo>) {}
fn event(&mut self, _key: Key) -> Result<EventState> { fn event(&mut self, key: Key) -> Result<EventState> {
if self.visible {
if key == self.key_config.exit_popup {
self.error = String::new();
self.hide();
return Ok(EventState::Consumed);
}
return Ok(EventState::NotConsumed);
}
Ok(EventState::NotConsumed) Ok(EventState::NotConsumed)
} }
fn hide(&mut self) {
self.visible = false;
}
fn show(&mut self) -> Result<()> {
self.visible = true;
Ok(())
}
} }

@ -1,5 +1,6 @@
use super::{Component, DrawableComponent, EventState}; use super::{Component, DrawableComponent, EventState};
use crate::components::command::CommandInfo; use crate::components::command::CommandInfo;
use crate::config::KeyConfig;
use crate::event::Key; use crate::event::Key;
use anyhow::Result; use anyhow::Result;
use itertools::Itertools; use itertools::Itertools;
@ -17,6 +18,7 @@ pub struct HelpComponent {
cmds: Vec<CommandInfo>, cmds: Vec<CommandInfo>,
visible: bool, visible: bool,
selection: u16, selection: u16,
key_config: KeyConfig,
} }
impl DrawableComponent for HelpComponent { impl DrawableComponent for HelpComponent {
@ -69,27 +71,22 @@ impl DrawableComponent for HelpComponent {
} }
impl Component for HelpComponent { impl Component for HelpComponent {
fn commands(&self, out: &mut Vec<CommandInfo>) {} fn commands(&self, _out: &mut Vec<CommandInfo>) {}
fn event(&mut self, key: Key) -> Result<EventState> { fn event(&mut self, key: Key) -> Result<EventState> {
if self.visible { if self.visible {
match key { if key == self.key_config.exit_popup {
Key::Esc => { self.hide();
self.hide(); return Ok(EventState::Consumed);
return Ok(EventState::Consumed); } else if key == self.key_config.scroll_down {
} self.scroll_selection(true);
Key::Char('j') => { return Ok(EventState::Consumed);
self.move_selection(true); } else if key == self.key_config.scroll_up {
return Ok(EventState::Consumed); self.scroll_selection(false);
} return Ok(EventState::Consumed);
Key::Char('k') => {
self.move_selection(false);
return Ok(EventState::Consumed);
}
_ => (),
} }
return Ok(EventState::NotConsumed); return Ok(EventState::NotConsumed);
} else if let Key::Char('?') = key { } else if key == self.key_config.open_help {
self.show()?; self.show()?;
return Ok(EventState::Consumed); return Ok(EventState::Consumed);
} }
@ -108,11 +105,12 @@ impl Component for HelpComponent {
} }
impl HelpComponent { impl HelpComponent {
pub const fn new() -> Self { pub const fn new(key_config: KeyConfig) -> Self {
Self { Self {
cmds: vec![], cmds: vec![],
visible: false, visible: false,
selection: 0, selection: 0,
key_config,
} }
} }
@ -123,7 +121,7 @@ impl HelpComponent {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
} }
fn move_selection(&mut self, inc: bool) { fn scroll_selection(&mut self, inc: bool) {
let mut new_selection = self.selection; let mut new_selection = self.selection;
new_selection = if inc { new_selection = if inc {
@ -152,7 +150,7 @@ impl HelpComponent {
processed += 1; processed += 1;
txt.push(Spans::from(Span::styled( txt.push(Spans::from(Span::styled(
format!("{}{:w$}", command_info.text.name, w = width), format!(" {}{:w$}", command_info.text.name, w = width),
if is_selected { if is_selected {
Style::default().bg(Color::Blue) Style::default().bg(Color::Blue)
} else { } else {
@ -168,15 +166,16 @@ impl HelpComponent {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::{Color, CommandInfo, HelpComponent, Modifier, Span, Spans, Style}; use super::{Color, CommandInfo, HelpComponent, KeyConfig, Modifier, Span, Spans, Style};
#[test] #[test]
fn test_get_text() { fn test_get_text() {
let width = 3; let width = 3;
let mut component = HelpComponent::new(); let key_config = KeyConfig::default();
let mut component = HelpComponent::new(key_config.clone());
component.set_cmds(vec![ component.set_cmds(vec![
CommandInfo::new(crate::components::command::move_left("h"), true, true), CommandInfo::new(crate::components::command::scroll(&key_config)),
CommandInfo::new(crate::components::command::move_right("l"), true, true), CommandInfo::new(crate::components::command::filter(&key_config)),
]); ]);
assert_eq!( assert_eq!(
component.get_text(width), component.get_text(width),
@ -186,10 +185,10 @@ mod test {
Style::default().add_modifier(Modifier::REVERSED) Style::default().add_modifier(Modifier::REVERSED)
)), )),
Spans::from(Span::styled( Spans::from(Span::styled(
"Move left [h] 3", " Scroll up/down/left/right [k,j,h,l] 3",
Style::default().bg(Color::Blue) Style::default().bg(Color::Blue)
)), )),
Spans::from(Span::styled("Move right [l] 3", Style::default())) Spans::from(Span::styled(" Filter [/] 3", Style::default()))
] ]
); );
} }

@ -1,8 +1,10 @@
use super::{Component, DrawableComponent, EventState}; use super::{Component, DrawableComponent, EventState};
use crate::components::command::CommandInfo; use crate::components::command::CommandInfo;
use crate::components::{TableComponent, TableFilterComponent}; use crate::components::{TableComponent, TableFilterComponent};
use crate::config::KeyConfig;
use crate::event::Key; use crate::event::Key;
use anyhow::Result; use anyhow::Result;
use database_tree::{Database, Table as DTable};
use tui::{ use tui::{
backend::Backend, backend::Backend,
layout::{Constraint, Direction, Layout, Rect}, layout::{Constraint, Direction, Layout, Rect},
@ -18,52 +20,39 @@ pub struct RecordTableComponent {
pub filter: TableFilterComponent, pub filter: TableFilterComponent,
pub table: TableComponent, pub table: TableComponent,
pub focus: Focus, pub focus: Focus,
key_config: KeyConfig,
} }
impl Default for RecordTableComponent { impl RecordTableComponent {
fn default() -> Self { pub fn new(key_config: KeyConfig) -> Self {
Self { Self {
filter: TableFilterComponent::default(), filter: TableFilterComponent::default(),
table: TableComponent::default(), table: TableComponent::new(key_config.clone()),
focus: Focus::Table, focus: Focus::Table,
key_config,
} }
} }
}
impl RecordTableComponent { pub fn update(
pub fn new(rows: Vec<Vec<String>>, headers: Vec<String>) -> Self { &mut self,
Self { rows: Vec<Vec<String>>,
table: TableComponent::new(rows, headers), headers: Vec<String>,
..Self::default() database: Database,
} table: DTable,
} ) {
self.table.update(rows, headers, database, table.clone());
pub fn update(&mut self, rows: Vec<Vec<String>>, headers: Vec<String>) { self.filter.table = Some(table);
self.table.rows = rows;
self.table.headers = headers;
if !self.table.rows.is_empty() {
self.table.selected_row.select(None);
self.table.selected_row.select(Some(0));
}
} }
pub fn reset(&mut self) { pub fn reset(&mut self) {
self.table = TableComponent::default(); self.table.reset();
if !self.table.rows.is_empty() { self.filter.reset();
self.table.selected_row.select(None);
self.table.selected_row.select(Some(0))
}
self.filter = TableFilterComponent::default();
} }
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.table.rows.len() self.table.rows.len()
} }
pub fn set_table(&mut self, table: String) {
self.filter.table = Some(table)
}
pub fn filter_focused(&self) -> bool { pub fn filter_focused(&self) -> bool {
matches!(self.focus, Focus::Filter) matches!(self.focus, Focus::Filter)
} }
@ -86,14 +75,16 @@ impl DrawableComponent for RecordTableComponent {
} }
impl Component for RecordTableComponent { impl Component for RecordTableComponent {
fn commands(&self, out: &mut Vec<CommandInfo>) {} fn commands(&self, out: &mut Vec<CommandInfo>) {
self.table.commands(out)
}
fn event(&mut self, key: Key) -> Result<EventState> { fn event(&mut self, key: Key) -> Result<EventState> {
if key == self.key_config.filter {
self.focus = Focus::Filter;
return Ok(EventState::Consumed);
}
match key { match key {
Key::Char('/') => {
self.focus = Focus::Filter;
return Ok(EventState::Consumed);
}
key if matches!(self.focus, Focus::Filter) => return self.filter.event(key), key if matches!(self.focus, Focus::Filter) => return self.filter.event(key),
key if matches!(self.focus, Focus::Table) => return self.table.event(key), key if matches!(self.focus, Focus::Table) => return self.table.event(key),
_ => (), _ => (),

@ -1,8 +1,8 @@
use super::{Component, DrawableComponent, EventState}; use super::{Component, DrawableComponent, EventState};
use crate::components::command::CommandInfo; use crate::components::command::{self, CommandInfo};
use crate::config::KeyConfig;
use crate::event::Key; use crate::event::Key;
use anyhow::Result; use anyhow::Result;
use strum::IntoEnumIterator;
use strum_macros::EnumIter; use strum_macros::EnumIter;
use tui::{ use tui::{
backend::Backend, backend::Backend,
@ -25,29 +25,30 @@ impl std::fmt::Display for Tab {
} }
} }
impl Tab {
pub fn names() -> Vec<String> {
Self::iter()
.map(|tab| format!("{} [{}]", tab, tab as u8 + 1))
.collect()
}
}
pub struct TabComponent { pub struct TabComponent {
pub selected_tab: Tab, pub selected_tab: Tab,
key_config: KeyConfig,
} }
impl Default for TabComponent { impl TabComponent {
fn default() -> Self { pub fn new(key_config: KeyConfig) -> Self {
Self { Self {
selected_tab: Tab::Records, selected_tab: Tab::Records,
key_config,
} }
} }
fn names(&self) -> Vec<String> {
vec![
command::tab_records(&self.key_config).name,
command::tab_structure(&self.key_config).name,
]
}
} }
impl DrawableComponent for TabComponent { impl DrawableComponent for TabComponent {
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, area: Rect, _focused: bool) -> Result<()> { fn draw<B: Backend>(&mut self, f: &mut Frame<B>, area: Rect, _focused: bool) -> Result<()> {
let titles = Tab::names().iter().cloned().map(Spans::from).collect(); let titles = self.names().iter().cloned().map(Spans::from).collect();
let tabs = Tabs::new(titles) let tabs = Tabs::new(titles)
.block(Block::default().borders(Borders::ALL)) .block(Block::default().borders(Borders::ALL))
.select(self.selected_tab as usize) .select(self.selected_tab as usize)
@ -63,19 +64,16 @@ impl DrawableComponent for TabComponent {
} }
impl Component for TabComponent { impl Component for TabComponent {
fn commands(&self, out: &mut Vec<CommandInfo>) {} fn commands(&self, _out: &mut Vec<CommandInfo>) {}
fn event(&mut self, key: Key) -> Result<EventState> { fn event(&mut self, key: Key) -> Result<EventState> {
match key { if key == self.key_config.tab_records {
Key::Char('1') => { self.selected_tab = Tab::Records;
self.selected_tab = Tab::Records; return Ok(EventState::Consumed);
Ok(EventState::Consumed) } else if key == self.key_config.tab_structure {
} self.selected_tab = Tab::Structure;
Key::Char('2') => { return Ok(EventState::Consumed);
self.selected_tab = Tab::Structure;
Ok(EventState::Consumed)
}
_ => Ok(EventState::NotConsumed),
} }
Ok(EventState::NotConsumed)
} }
} }

@ -2,9 +2,11 @@ use super::{
utils::scroll_vertical::VerticalScroll, Component, DrawableComponent, EventState, utils::scroll_vertical::VerticalScroll, Component, DrawableComponent, EventState,
TableValueComponent, TableValueComponent,
}; };
use crate::components::command::CommandInfo; use crate::components::command::{self, CommandInfo};
use crate::config::KeyConfig;
use crate::event::Key; use crate::event::Key;
use anyhow::Result; use anyhow::Result;
use database_tree::{Database, Table as DTable};
use std::convert::From; use std::convert::From;
use tui::{ use tui::{
backend::Backend, backend::Backend,
@ -20,43 +22,70 @@ pub struct TableComponent {
pub rows: Vec<Vec<String>>, pub rows: Vec<Vec<String>>,
pub eod: bool, pub eod: bool,
pub selected_row: TableState, pub selected_row: TableState,
table: Option<(Database, DTable)>,
selected_column: usize, selected_column: usize,
selection_area_corner: Option<(usize, usize)>, selection_area_corner: Option<(usize, usize)>,
column_page_start: std::cell::Cell<usize>, column_page_start: std::cell::Cell<usize>,
scroll: VerticalScroll, scroll: VerticalScroll,
key_config: KeyConfig,
} }
impl Default for TableComponent { impl TableComponent {
fn default() -> Self { pub fn new(key_config: KeyConfig) -> Self {
Self { Self {
selected_row: TableState::default(), selected_row: TableState::default(),
headers: vec![], headers: vec![],
rows: vec![], rows: vec![],
table: None,
selected_column: 0, selected_column: 0,
selection_area_corner: None, selection_area_corner: None,
column_page_start: std::cell::Cell::new(0), column_page_start: std::cell::Cell::new(0),
scroll: VerticalScroll::new(), scroll: VerticalScroll::new(),
eod: false, eod: false,
key_config,
} }
} }
}
impl TableComponent { fn title(&self) -> String {
pub fn new(rows: Vec<Vec<String>>, headers: Vec<String>) -> Self { self.table.as_ref().map_or(" - ".to_string(), |table| {
let mut selected_row = TableState::default(); format!("{}/{}", table.0.name, table.1.name)
})
}
pub fn update(
&mut self,
rows: Vec<Vec<String>>,
headers: Vec<String>,
database: Database,
table: DTable,
) {
if !rows.is_empty() { if !rows.is_empty() {
selected_row.select(None); self.selected_row.select(None);
selected_row.select(Some(0)) self.selected_row.select(Some(0))
}
Self {
headers,
rows,
selected_row,
..Self::default()
} }
self.headers = headers;
self.rows = rows;
self.selected_column = 0;
self.selection_area_corner = None;
self.column_page_start = std::cell::Cell::new(0);
self.scroll = VerticalScroll::new();
self.eod = false;
self.table = Some((database, table));
}
pub fn reset(&mut self) {
self.selected_row.select(None);
self.headers = Vec::new();
self.rows = Vec::new();
self.selected_column = 0;
self.selection_area_corner = None;
self.column_page_start = std::cell::Cell::new(0);
self.scroll = VerticalScroll::new();
self.eod = false;
self.table = None;
} }
fn reset(&mut self) { fn reset_selection(&mut self) {
self.selection_area_corner = None; self.selection_area_corner = None;
} }
@ -75,7 +104,7 @@ impl TableComponent {
} }
None => None, None => None,
}; };
self.reset(); self.reset_selection();
self.selected_row.select(i); self.selected_row.select(i);
} }
@ -90,7 +119,7 @@ impl TableComponent {
} }
None => None, None => None,
}; };
self.reset(); self.reset_selection();
self.selected_row.select(i); self.selected_row.select(i);
} }
@ -98,7 +127,7 @@ impl TableComponent {
if self.rows.is_empty() { if self.rows.is_empty() {
return; return;
} }
self.reset(); self.reset_selection();
self.selected_row.select(Some(0)); self.selected_row.select(Some(0));
} }
@ -106,7 +135,7 @@ impl TableComponent {
if self.rows.is_empty() { if self.rows.is_empty() {
return; return;
} }
self.reset(); self.reset_selection();
self.selected_row.select(Some(self.rows.len() - 1)); self.selected_row.select(Some(self.rows.len() - 1));
} }
@ -117,7 +146,7 @@ impl TableComponent {
if self.selected_column >= self.headers.len().saturating_sub(1) { if self.selected_column >= self.headers.len().saturating_sub(1) {
return; return;
} }
self.reset(); self.reset_selection();
self.selected_column += 1; self.selected_column += 1;
} }
@ -128,7 +157,7 @@ impl TableComponent {
if self.selected_column == 0 { if self.selected_column == 0 {
return; return;
} }
self.reset(); self.reset_selection();
self.selected_column -= 1; self.selected_column -= 1;
} }
@ -389,7 +418,7 @@ impl DrawableComponent for TableComponent {
TableValueComponent::new(self.selected_cells().unwrap_or_default()) TableValueComponent::new(self.selected_cells().unwrap_or_default())
.draw(f, layout[0], focused)?; .draw(f, layout[0], focused)?;
let block = Block::default().borders(Borders::ALL).title("Records"); let block = Block::default().borders(Borders::ALL).title(self.title());
let (selected_column_index, headers, rows, constraints) = let (selected_column_index, headers, rows, constraints) =
self.calculate_cell_widths(block.inner(layout[1]).width); self.calculate_cell_widths(block.inner(layout[1]).width);
let header_cells = headers.iter().enumerate().map(|(column_index, h)| { let header_cells = headers.iter().enumerate().map(|(column_index, h)| {
@ -448,59 +477,49 @@ impl DrawableComponent for TableComponent {
} }
impl Component for TableComponent { impl Component for TableComponent {
fn commands(&self, out: &mut Vec<CommandInfo>) {} fn commands(&self, out: &mut Vec<CommandInfo>) {
out.push(CommandInfo::new(command::extend_selection_by_one_cell(
&self.key_config,
)));
}
fn event(&mut self, key: Key) -> Result<EventState> { fn event(&mut self, key: Key) -> Result<EventState> {
match key { if key == self.key_config.scroll_left {
Key::Char('h') => { self.previous_column();
self.previous_column(); return Ok(EventState::Consumed);
return Ok(EventState::Consumed); } else if key == self.key_config.scroll_down {
} self.next_row(1);
Key::Char('j') => { return Ok(EventState::NotConsumed);
self.next_row(1); } else if key == self.key_config.scroll_down_multiple_lines {
return Ok(EventState::NotConsumed); self.next_row(10);
} return Ok(EventState::NotConsumed);
Key::Ctrl('d') => { } else if key == self.key_config.scroll_up {
self.next_row(10); self.previous_row(1);
return Ok(EventState::NotConsumed); return Ok(EventState::Consumed);
} } else if key == self.key_config.scroll_up_multiple_lines {
Key::Char('k') => { self.previous_row(10);
self.previous_row(1); return Ok(EventState::Consumed);
return Ok(EventState::Consumed); } else if key == self.key_config.scroll_to_top {
} self.scroll_top();
Key::Ctrl('u') => { return Ok(EventState::Consumed);
self.previous_row(10); } else if key == self.key_config.scroll_to_bottom {
return Ok(EventState::Consumed); self.scroll_bottom();
} return Ok(EventState::Consumed);
Key::Char('g') => { } else if key == self.key_config.scroll_right {
self.scroll_top(); self.next_column();
return Ok(EventState::Consumed); return Ok(EventState::Consumed);
} } else if key == self.key_config.extend_selection_by_one_cell_left {
Key::Char('G') => { self.expand_selected_area_x(false);
self.scroll_bottom(); return Ok(EventState::Consumed);
return Ok(EventState::Consumed); } else if key == self.key_config.extend_selection_by_one_cell_up {
} self.expand_selected_area_y(false);
Key::Char('l') => { return Ok(EventState::Consumed);
self.next_column(); } else if key == self.key_config.extend_selection_by_one_cell_down {
return Ok(EventState::Consumed); self.expand_selected_area_y(true);
} return Ok(EventState::Consumed);
Key::Char('H') => { } else if key == self.key_config.extend_selection_by_one_cell_right {
self.expand_selected_area_x(false); self.expand_selected_area_x(true);
return Ok(EventState::Consumed); return Ok(EventState::Consumed);
}
Key::Char('K') => {
self.expand_selected_area_y(false);
return Ok(EventState::Consumed);
}
Key::Char('J') => {
self.expand_selected_area_y(true);
return Ok(EventState::Consumed);
}
Key::Char('L') => {
self.expand_selected_area_x(true);
return Ok(EventState::Consumed);
}
_ => (),
} }
Ok(EventState::NotConsumed) Ok(EventState::NotConsumed)
} }
@ -508,19 +527,19 @@ impl Component for TableComponent {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::TableComponent; use super::{KeyConfig, TableComponent};
use tui::layout::Constraint; use tui::layout::Constraint;
#[test] #[test]
fn test_headers() { fn test_headers() {
let mut component = TableComponent::default(); let mut component = TableComponent::new(KeyConfig::default());
component.headers = vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect(); component.headers = vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect();
assert_eq!(component.headers(1, 2), vec!["", "b"]) assert_eq!(component.headers(1, 2), vec!["", "b"])
} }
#[test] #[test]
fn test_rows() { fn test_rows() {
let mut component = TableComponent::default(); let mut component = TableComponent::new(KeyConfig::default());
component.rows = vec![ component.rows = vec![
vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect(), vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect(),
vec!["d", "e", "f"].iter().map(|h| h.to_string()).collect(), vec!["d", "e", "f"].iter().map(|h| h.to_string()).collect(),
@ -540,7 +559,7 @@ mod test {
// 1 a b c // 1 a b c
// 2 |d e| f // 2 |d e| f
let mut component = TableComponent::default(); let mut component = TableComponent::new(KeyConfig::default());
component.headers = vec!["1", "2", "3"].iter().map(|h| h.to_string()).collect(); component.headers = vec!["1", "2", "3"].iter().map(|h| h.to_string()).collect();
component.rows = vec![ component.rows = vec![
vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect(), vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect(),
@ -565,7 +584,7 @@ mod test {
// 1 a b c // 1 a b c
// 2 d |e f| // 2 d |e f|
let mut component = TableComponent::default(); let mut component = TableComponent::new(KeyConfig::default());
component.headers = vec!["1", "2", "3"].iter().map(|h| h.to_string()).collect(); component.headers = vec!["1", "2", "3"].iter().map(|h| h.to_string()).collect();
component.rows = vec![ component.rows = vec![
vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect(), vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect(),
@ -590,7 +609,7 @@ mod test {
// 1 a |b| c // 1 a |b| c
// 2 d |e| f // 2 d |e| f
let mut component = TableComponent::default(); let mut component = TableComponent::new(KeyConfig::default());
component.rows = vec![ component.rows = vec![
vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect(), vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect(),
vec!["d", "e", "f"].iter().map(|h| h.to_string()).collect(), vec!["d", "e", "f"].iter().map(|h| h.to_string()).collect(),
@ -614,7 +633,7 @@ mod test {
// 1 a |b| c // 1 a |b| c
// 2 d |e| f // 2 d |e| f
let mut component = TableComponent::default(); let mut component = TableComponent::new(KeyConfig::default());
component.rows = vec![ component.rows = vec![
vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect(), vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect(),
vec!["d", "e", "f"].iter().map(|h| h.to_string()).collect(), vec!["d", "e", "f"].iter().map(|h| h.to_string()).collect(),
@ -628,7 +647,7 @@ mod test {
#[test] #[test]
fn test_is_number_column() { fn test_is_number_column() {
let mut component = TableComponent::default(); let mut component = TableComponent::new(KeyConfig::default());
component.headers = vec!["1", "2", "3"].iter().map(|h| h.to_string()).collect(); component.headers = vec!["1", "2", "3"].iter().map(|h| h.to_string()).collect();
component.rows = vec![ component.rows = vec![
vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect(), vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect(),
@ -645,7 +664,7 @@ mod test {
// 1 |a| b c // 1 |a| b c
// 2 d e f // 2 d e f
let mut component = TableComponent::default(); let mut component = TableComponent::new(KeyConfig::default());
component.headers = vec!["1", "2", "3"].iter().map(|h| h.to_string()).collect(); component.headers = vec!["1", "2", "3"].iter().map(|h| h.to_string()).collect();
component.rows = vec![ component.rows = vec![
vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect(), vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect(),
@ -661,7 +680,7 @@ mod test {
// 1 |a b| c // 1 |a b| c
// 2 |d e| f // 2 |d e| f
let mut component = TableComponent::default(); let mut component = TableComponent::new(KeyConfig::default());
component.headers = vec!["1", "2", "3"].iter().map(|h| h.to_string()).collect(); component.headers = vec!["1", "2", "3"].iter().map(|h| h.to_string()).collect();
component.rows = vec![ component.rows = vec![
vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect(), vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect(),
@ -678,7 +697,7 @@ mod test {
// 1 |a| b c // 1 |a| b c
// 2 d e f // 2 d e f
let mut component = TableComponent::default(); let mut component = TableComponent::new(KeyConfig::default());
component.headers = vec!["1", "2", "3"].iter().map(|h| h.to_string()).collect(); component.headers = vec!["1", "2", "3"].iter().map(|h| h.to_string()).collect();
component.rows = vec![ component.rows = vec![
vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect(), vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect(),
@ -699,7 +718,7 @@ mod test {
// 1 |a b| c // 1 |a b| c
// 2 |d e| f // 2 |d e| f
let mut component = TableComponent::default(); let mut component = TableComponent::new(KeyConfig::default());
component.headers = vec!["1", "2", "3"].iter().map(|h| h.to_string()).collect(); component.headers = vec!["1", "2", "3"].iter().map(|h| h.to_string()).collect();
component.rows = vec![ component.rows = vec![
vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect(), vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect(),
@ -721,7 +740,7 @@ mod test {
#[test] #[test]
fn test_calculate_cell_widths() { fn test_calculate_cell_widths() {
let mut component = TableComponent::default(); let mut component = TableComponent::new(KeyConfig::default());
component.headers = vec!["1", "2", "3"].iter().map(|h| h.to_string()).collect(); component.headers = vec!["1", "2", "3"].iter().map(|h| h.to_string()).collect();
component.rows = vec![ component.rows = vec![
vec!["aaaaa", "bbbbb", "ccccc"] vec!["aaaaa", "bbbbb", "ccccc"]
@ -765,7 +784,7 @@ mod test {
] ]
); );
let mut component = TableComponent::default(); let mut component = TableComponent::new(KeyConfig::default());
component.headers = vec!["1", "2", "3"].iter().map(|h| h.to_string()).collect(); component.headers = vec!["1", "2", "3"].iter().map(|h| h.to_string()).collect();
component.rows = vec![ component.rows = vec![
vec!["aaaaa", "bbbbb", "ccccc"] vec!["aaaaa", "bbbbb", "ccccc"]

@ -2,6 +2,7 @@ use super::{compute_character_width, Component, DrawableComponent, EventState};
use crate::components::command::CommandInfo; use crate::components::command::CommandInfo;
use crate::event::Key; use crate::event::Key;
use anyhow::Result; use anyhow::Result;
use database_tree::Table;
use tui::{ use tui::{
backend::Backend, backend::Backend,
layout::Rect, layout::Rect,
@ -13,7 +14,7 @@ use tui::{
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
pub struct TableFilterComponent { pub struct TableFilterComponent {
pub table: Option<String>, pub table: Option<Table>,
pub input: Vec<char>, pub input: Vec<char>,
input_idx: usize, input_idx: usize,
input_cursor_position: u16, input_cursor_position: u16,
@ -34,6 +35,13 @@ impl TableFilterComponent {
pub fn input_str(&self) -> String { pub fn input_str(&self) -> String {
self.input.iter().collect() self.input.iter().collect()
} }
pub fn reset(&mut self) {
self.table = None;
self.input = Vec::new();
self.input_idx = 0;
self.input_cursor_position = 0;
}
} }
impl DrawableComponent for TableFilterComponent { impl DrawableComponent for TableFilterComponent {
@ -42,7 +50,7 @@ impl DrawableComponent for TableFilterComponent {
Span::styled( Span::styled(
self.table self.table
.as_ref() .as_ref()
.map_or("-".to_string(), |table| table.to_string()), .map_or("-".to_string(), |table| table.name.to_string()),
Style::default().fg(Color::Blue), Style::default().fg(Color::Blue),
), ),
Span::from(format!( Span::from(format!(
@ -67,7 +75,7 @@ impl DrawableComponent for TableFilterComponent {
+ (1 + self + (1 + self
.table .table
.as_ref() .as_ref()
.map_or(String::new(), |table| table.to_string()) .map_or(String::new(), |table| table.name.to_string())
.width() .width()
+ 1) as u16) + 1) as u16)
.saturating_add(self.input_cursor_position), .saturating_add(self.input_cursor_position),
@ -79,7 +87,7 @@ impl DrawableComponent for TableFilterComponent {
} }
impl Component for TableFilterComponent { impl Component for TableFilterComponent {
fn commands(&self, out: &mut Vec<CommandInfo>) {} fn commands(&self, _out: &mut Vec<CommandInfo>) {}
fn event(&mut self, key: Key) -> Result<EventState> { fn event(&mut self, key: Key) -> Result<EventState> {
let input_str: String = self.input.iter().collect(); let input_str: String = self.input.iter().collect();

@ -86,7 +86,7 @@ impl DrawableComponent for TableStatusComponent {
} }
impl Component for TableStatusComponent { impl Component for TableStatusComponent {
fn commands(&self, out: &mut Vec<CommandInfo>) {} fn commands(&self, _out: &mut Vec<CommandInfo>) {}
fn event(&mut self, _key: Key) -> Result<EventState> { fn event(&mut self, _key: Key) -> Result<EventState> {
Ok(EventState::NotConsumed) Ok(EventState::NotConsumed)

@ -46,7 +46,7 @@ impl DrawableComponent for TableValueComponent {
} }
impl Component for TableValueComponent { impl Component for TableValueComponent {
fn commands(&self, out: &mut Vec<CommandInfo>) {} fn commands(&self, _out: &mut Vec<CommandInfo>) {}
fn event(&mut self, _key: Key) -> Result<EventState> { fn event(&mut self, _key: Key) -> Result<EventState> {
todo!("scroll"); todo!("scroll");

@ -0,0 +1,131 @@
use crate::Key;
use serde::Deserialize;
use std::fs::File;
use std::io::{BufReader, Read};
#[derive(Debug, Deserialize, Clone)]
pub struct Config {
pub conn: Vec<Connection>,
#[serde(default)]
pub key_config: KeyConfig,
}
impl Default for Config {
fn default() -> Self {
Self {
conn: vec![Connection {
name: None,
user: "root".to_string(),
host: "localhost".to_string(),
port: 3306,
database: None,
}],
key_config: KeyConfig::default(),
}
}
}
#[derive(Debug, Deserialize, Clone)]
pub struct Connection {
name: Option<String>,
user: String,
host: String,
port: u64,
pub database: Option<String>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct KeyConfig {
pub scroll_up: Key,
pub scroll_down: Key,
pub scroll_right: Key,
pub scroll_left: Key,
pub copy: Key,
pub enter: Key,
pub exit: Key,
pub quit: Key,
pub exit_popup: Key,
pub focus_right: Key,
pub focus_left: Key,
pub focus_connections: Key,
pub open_help: Key,
pub filter: Key,
pub scroll_down_multiple_lines: Key,
pub scroll_up_multiple_lines: Key,
pub scroll_to_top: Key,
pub scroll_to_bottom: Key,
pub extend_selection_by_one_cell_left: Key,
pub extend_selection_by_one_cell_right: Key,
pub extend_selection_by_one_cell_up: Key,
pub extend_selection_by_one_cell_down: Key,
pub tab_records: Key,
pub tab_structure: Key,
}
impl Default for KeyConfig {
fn default() -> Self {
Self {
scroll_up: Key::Char('k'),
scroll_down: Key::Char('j'),
scroll_right: Key::Char('l'),
scroll_left: Key::Char('h'),
copy: Key::Char('y'),
enter: Key::Enter,
exit: Key::Ctrl('c'),
quit: Key::Char('q'),
exit_popup: Key::Esc,
focus_right: Key::Right,
focus_left: Key::Left,
focus_connections: Key::Char('c'),
open_help: Key::Char('?'),
filter: Key::Char('/'),
scroll_down_multiple_lines: Key::Ctrl('d'),
scroll_up_multiple_lines: Key::Ctrl('u'),
scroll_to_top: Key::Char('g'),
scroll_to_bottom: Key::Char('G'),
extend_selection_by_one_cell_left: Key::Char('H'),
extend_selection_by_one_cell_right: Key::Char('L'),
extend_selection_by_one_cell_down: Key::Char('J'),
extend_selection_by_one_cell_up: Key::Char('K'),
tab_records: Key::Char('1'),
tab_structure: Key::Char('2'),
}
}
}
impl Config {
pub fn new(path: &str) -> anyhow::Result<Self> {
if let Ok(file) = File::open(path) {
let mut buf_reader = BufReader::new(file);
let mut contents = String::new();
buf_reader.read_to_string(&mut contents)?;
let config: Result<Config, toml::de::Error> = toml::from_str(&contents);
match config {
Ok(config) => return Ok(config),
Err(e) => panic!("fail to parse config file: {}", e),
}
}
Ok(Config::default())
}
}
impl Connection {
pub fn database_url(&self) -> String {
match &self.database {
Some(database) => format!(
"mysql://{user}:@{host}:{port}/{database}",
user = self.user,
host = self.host,
port = self.port,
database = database
),
None => format!(
"mysql://{user}:@{host}:{port}",
user = self.user,
host = self.host,
port = self.port,
),
}
}
}

@ -3,11 +3,8 @@ use crossterm::event;
use std::{sync::mpsc, thread, time::Duration}; use std::{sync::mpsc, thread, time::Duration};
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
/// Configuration for event handling.
pub struct EventConfig { pub struct EventConfig {
/// The key that is used to exit the application.
pub exit_key: Key, pub exit_key: Key,
/// The tick rate at which the application will sent an tick event.
pub tick_rate: Duration, pub tick_rate: Duration,
} }
@ -20,25 +17,18 @@ impl Default for EventConfig {
} }
} }
/// An occurred event.
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
pub enum Event<I> { pub enum Event<I> {
/// An input event occurred.
Input(I), Input(I),
/// An tick event occurred.
Tick, Tick,
} }
/// A small event handler that wrap crossterm input and tick event. Each event
/// type is handled in its own thread and returned to a common `Receiver`
pub struct Events { pub struct Events {
rx: mpsc::Receiver<Event<Key>>, rx: mpsc::Receiver<Event<Key>>,
// Need to be kept around to prevent disposing the sender side.
_tx: mpsc::Sender<Event<Key>>, _tx: mpsc::Sender<Event<Key>>,
} }
impl Events { impl Events {
/// Constructs an new instance of `Events` with the default config.
pub fn new(tick_rate: u64) -> Events { pub fn new(tick_rate: u64) -> Events {
Events::with_config(EventConfig { Events::with_config(EventConfig {
tick_rate: Duration::from_millis(tick_rate), tick_rate: Duration::from_millis(tick_rate),
@ -46,31 +36,25 @@ impl Events {
}) })
} }
/// Constructs an new instance of `Events` from given config.
pub fn with_config(config: EventConfig) -> Events { pub fn with_config(config: EventConfig) -> Events {
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
let event_tx = tx.clone(); let event_tx = tx.clone();
thread::spawn(move || { thread::spawn(move || loop {
loop { if event::poll(config.tick_rate).unwrap() {
// poll for tick rate duration, if no event, sent tick event. if let event::Event::Key(key) = event::read().unwrap() {
if event::poll(config.tick_rate).unwrap() { let key = Key::from(key);
if let event::Event::Key(key) = event::read().unwrap() {
let key = Key::from(key);
event_tx.send(Event::Input(key)).unwrap(); event_tx.send(Event::Input(key)).unwrap();
}
} }
event_tx.send(Event::Tick).unwrap();
} }
event_tx.send(Event::Tick).unwrap();
}); });
Events { rx, _tx: tx } Events { rx, _tx: tx }
} }
/// Attempts to read an event.
/// This function will block the current thread.
pub fn next(&self) -> Result<Event<Key>, mpsc::RecvError> { pub fn next(&self) -> Result<Event<Key>, mpsc::RecvError> {
self.rx.recv() self.rx.recv()
} }

@ -1,8 +1,9 @@
use crossterm::event; use crossterm::event;
use serde::Deserialize;
use std::fmt; use std::fmt;
/// Represents a key. /// Represents a key.
#[derive(PartialEq, Eq, Clone, Copy, Hash, Debug)] #[derive(PartialEq, Eq, Clone, Copy, Hash, Debug, Deserialize)]
pub enum Key { pub enum Key {
/// Both Enter (or Return) and numpad Enter /// Both Enter (or Return) and numpad Enter
Enter, Enter,
@ -104,7 +105,10 @@ impl fmt::Display for Key {
Key::Alt(c) => write!(f, "<Alt+{}>", c), Key::Alt(c) => write!(f, "<Alt+{}>", c),
Key::Ctrl(c) => write!(f, "<Ctrl+{}>", c), Key::Ctrl(c) => write!(f, "<Ctrl+{}>", c),
Key::Char(c) => write!(f, "{}", c), Key::Char(c) => write!(f, "{}", c),
Key::Left | Key::Right | Key::Up | Key::Down => write!(f, "<{:?} Arrow Key>", self), Key::Left => write!(f, "\u{2190}"), //←
Key::Right => write!(f, "\u{2192}"), //→
Key::Up => write!(f, "\u{2191}"), //↑
Key::Down => write!(f, "\u{2193}"), //↓
Key::Enter Key::Enter
| Key::Tab | Key::Tab
| Key::Backspace | Key::Backspace

@ -1,9 +1,9 @@
mod app; mod app;
mod clipboard; mod clipboard;
mod components; mod components;
mod config;
mod event; mod event;
mod ui; mod ui;
mod user_config;
mod utils; mod utils;
#[macro_use] #[macro_use]
@ -28,7 +28,7 @@ async fn main() -> anyhow::Result<()> {
outln!("gobang logger"); outln!("gobang logger");
let user_config = user_config::UserConfig::new("sample.toml").ok(); let config = config::Config::new("sample.toml")?;
let stdout = stdout(); let stdout = stdout();
setup_terminal()?; setup_terminal()?;
@ -36,7 +36,7 @@ async fn main() -> anyhow::Result<()> {
let backend = CrosstermBackend::new(stdout); let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?; let mut terminal = Terminal::new(backend)?;
let events = event::Events::new(250); let events = event::Events::new(250);
let mut app = App::new(user_config.unwrap()); let mut app = App::new(config);
terminal.clear()?; terminal.clear()?;
@ -45,11 +45,13 @@ async fn main() -> anyhow::Result<()> {
match events.next()? { match events.next()? {
Event::Input(key) => match app.event(key).await { Event::Input(key) => match app.event(key).await {
Ok(state) => { Ok(state) => {
if !state.is_consumed() && (key == Key::Char('q') || key == Key::Ctrl('c')) { if !state.is_consumed()
&& (key == app.config.key_config.quit || key == app.config.key_config.exit)
{
break; break;
} }
} }
Err(err) => app.error.set(err.to_string()), Err(err) => app.error.set(err.to_string())?,
}, },
Event::Tick => (), Event::Tick => (),
} }

@ -1,22 +1,23 @@
use crate::config::KeyConfig;
use crate::event::Key; use crate::event::Key;
use database_tree::MoveSelection; use database_tree::MoveSelection;
pub mod scrollbar; pub mod scrollbar;
pub mod scrolllist; pub mod scrolllist;
pub fn common_nav(key: Key) -> Option<MoveSelection> { pub fn common_nav(key: Key, key_config: &KeyConfig) -> Option<MoveSelection> {
if key == Key::Char('j') { if key == key_config.scroll_down {
Some(MoveSelection::Down) Some(MoveSelection::Down)
} else if key == Key::Char('k') { } else if key == key_config.scroll_up {
Some(MoveSelection::Up) Some(MoveSelection::Up)
} else if key == Key::PageUp { } else if key == key_config.scroll_right {
Some(MoveSelection::PageUp)
} else if key == Key::PageDown {
Some(MoveSelection::PageDown)
} else if key == Key::Char('l') {
Some(MoveSelection::Right) Some(MoveSelection::Right)
} else if key == Key::Char('h') { } else if key == key_config.scroll_left {
Some(MoveSelection::Left) Some(MoveSelection::Left)
} else if key == key_config.scroll_to_top {
Some(MoveSelection::Top)
} else if key == key_config.scroll_to_bottom {
Some(MoveSelection::End)
} else { } else {
None None
} }

@ -1,52 +0,0 @@
use serde::Deserialize;
use std::fs::File;
use std::io::{BufReader, Read};
#[derive(Debug, Deserialize, Clone)]
pub struct UserConfig {
pub conn: Vec<Connection>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct Connection {
name: Option<String>,
user: String,
host: String,
port: u64,
pub database: Option<String>,
}
impl UserConfig {
pub fn new(path: &str) -> anyhow::Result<Self> {
let file = File::open(path)?;
let mut buf_reader = BufReader::new(file);
let mut contents = String::new();
buf_reader.read_to_string(&mut contents)?;
let config: Result<UserConfig, toml::de::Error> = toml::from_str(&contents);
match config {
Ok(config) => Ok(config),
Err(e) => panic!("fail to parse config file: {}", e),
}
}
}
impl Connection {
pub fn database_url(&self) -> String {
match &self.database {
Some(database) => format!(
"mysql://{user}:@{host}:{port}/{database}",
user = self.user,
host = self.host,
port = self.port,
database = database
),
None => format!(
"mysql://{user}:@{host}:{port}",
user = self.user,
host = self.host,
port = self.port,
),
}
}
}
Loading…
Cancel
Save