From 07b2b5090f34f3d8e682f9eff194aa5412fdd161 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda <41065217+TaKO8Ki@users.noreply.github.com> Date: Sun, 1 Aug 2021 01:03:39 +0900 Subject: [PATCH] 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 --- README.md | 8 +- database-tree/src/databasetree.rs | 9 +- database-tree/src/item.rs | 6 +- database-tree/src/lib.rs | 2 +- src/app.rs | 127 +++++++++---------- src/components/command.rs | 109 +++++++++++------ src/components/connections.rs | 35 ++---- src/components/databases.rs | 27 +++-- src/components/error.rs | 46 +++++-- src/components/help.rs | 49 ++++---- src/components/record_table.rs | 59 ++++----- src/components/tab.rs | 46 ++++--- src/components/table.rs | 195 ++++++++++++++++-------------- src/components/table_filter.rs | 16 ++- src/components/table_status.rs | 2 +- src/components/table_value.rs | 2 +- src/config.rs | 131 ++++++++++++++++++++ src/event/events.rs | 30 ++--- src/event/key.rs | 8 +- src/main.rs | 12 +- src/ui/mod.rs | 19 +-- src/user_config.rs | 52 -------- 22 files changed, 559 insertions(+), 431 deletions(-) create mode 100644 src/config.rs delete mode 100644 src/user_config.rs diff --git a/README.md b/README.md index 9d92fc5..c38f5c4 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,9 @@ $ cargo install --version 0.1.0-alpha.0 gobang | Key | Description | | ---- | ---- | -| h | move left | -| j | move down | -| k | move up | -| l | move right | +| h | scroll left | +| j | scroll down | +| k | scroll up | +| l | scroll right | | Ctrl + d | scroll down multiple lines | | Ctrl + u | scroll up multiple lines | diff --git a/database-tree/src/databasetree.rs b/database-tree/src/databasetree.rs index 844691a..b8842d3 100644 --- a/database-tree/src/databasetree.rs +++ b/database-tree/src/databasetree.rs @@ -1,8 +1,8 @@ -use crate::Table; use crate::{ databasetreeitems::DatabaseTreeItems, error::Result, item::DatabaseTreeItemKind, tree_iter::TreeIterator, }; +use crate::{Database, Table}; use std::{collections::BTreeSet, usize}; /// @@ -14,8 +14,6 @@ pub enum MoveSelection { Right, Top, End, - PageDown, - PageUp, } #[derive(Debug, Clone, Copy)] @@ -76,13 +74,13 @@ impl DatabaseTree { .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| { let item = &self.items.tree_items[index]; match item.kind() { DatabaseTreeItemKind::Database { .. } => None, 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::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(); diff --git a/database-tree/src/item.rs b/database-tree/src/item.rs index a3cc268..edab342 100644 --- a/database-tree/src/item.rs +++ b/database-tree/src/item.rs @@ -34,7 +34,7 @@ impl TreeItemInfo { #[derive(PartialEq, Debug, Clone)] pub enum DatabaseTreeItemKind { Database { name: String, collapsed: bool }, - Table { database: String, table: Table }, + Table { database: Database, table: Table }, } impl DatabaseTreeItemKind { @@ -63,7 +63,7 @@ impl DatabaseTreeItemKind { pub fn database_name(&self) -> Option { match self { Self::Database { .. } => None, - Self::Table { database, .. } => Some(database.clone()), + Self::Table { database, .. } => Some(database.name.clone()), } } } @@ -82,7 +82,7 @@ impl DatabaseTreeItem { Ok(Self { info: TreeItemInfo::new(indent, false), kind: DatabaseTreeItemKind::Table { - database: database.name.clone(), + database: database.clone(), table: table.clone(), }, }) diff --git a/database-tree/src/lib.rs b/database-tree/src/lib.rs index 0303210..34f107d 100644 --- a/database-tree/src/lib.rs +++ b/database-tree/src/lib.rs @@ -11,7 +11,7 @@ pub use crate::{ item::{DatabaseTreeItem, TreeItemInfo}, }; -#[derive(Clone)] +#[derive(Clone, PartialEq, Debug)] pub struct Database { pub name: String, pub tables: Vec, diff --git a/src/app.rs b/src/app.rs index d44aee5..683dbeb 100644 --- a/src/app.rs +++ b/src/app.rs @@ -5,10 +5,10 @@ use crate::utils::{MySqlPool, Pool}; use crate::{ components::tab::Tab, components::{ - ConnectionsComponent, DatabasesComponent, ErrorComponent, HelpComponent, + command, ConnectionsComponent, DatabasesComponent, ErrorComponent, HelpComponent, RecordTableComponent, TabComponent, TableComponent, TableStatusComponent, }, - user_config::UserConfig, + config::Config, }; use database_tree::Database; use tui::{ @@ -33,36 +33,25 @@ pub struct App { table_status: TableStatusComponent, clipboard: Clipboard, pool: Option>, - pub user_config: Option, + pub config: Config, 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 { - pub fn new(user_config: UserConfig) -> App { + pub fn new(config: Config) -> App { Self { - user_config: Some(user_config.clone()), - connections: ConnectionsComponent::new(user_config.conn), + config: config.clone(), + 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, - ..App::default() + clipboard: Clipboard::new(), + pool: None, } } @@ -119,34 +108,24 @@ impl App { } fn commands(&self) -> Vec { - let res = vec![ - CommandInfo::new(crate::components::command::move_left("h"), true, true), - CommandInfo::new(crate::components::command::move_down("j"), true, true), - CommandInfo::new(crate::components::command::move_up("k"), true, true), - CommandInfo::new(crate::components::command::move_right("l"), true, true), - CommandInfo::new(crate::components::command::filter("/"), true, true), - CommandInfo::new( - crate::components::command::move_focus_to_right_widget( - Key::Right.to_string().as_str(), - ), - true, - true, - ), + let mut res = vec![ + CommandInfo::new(command::scroll(&self.config.key_config)), + CommandInfo::new(command::scroll_to_top_bottom(&self.config.key_config)), + CommandInfo::new(command::move_focus(&self.config.key_config)), + CommandInfo::new(command::filter(&self.config.key_config)), + CommandInfo::new(command::help(&self.config.key_config)), + CommandInfo::new(command::toggle_tabs(&self.config.key_config)), ]; + self.databases.commands(&mut res); + self.record_table.commands(&mut res); + res } pub async fn event(&mut self, key: Key) -> anyhow::Result { 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() { return Ok(EventState::Consumed); }; @@ -158,7 +137,11 @@ impl App { } pub async fn components_event(&mut self, key: Key) -> anyhow::Result { - 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); } @@ -168,8 +151,7 @@ impl App { return Ok(EventState::Consumed); } - if let Key::Enter = key { - self.record_table.reset(); + if key == self.config.key_config.enter { if let Some(conn) = self.connections.selected_connection() { if let Some(pool) = self.pool.as_ref() { pool.close().await; @@ -189,7 +171,8 @@ impl App { None => self.pool.as_ref().unwrap().get_databases().await?, }; self.databases.update(databases.as_slice()).unwrap(); - self.focus = Focus::DabataseList + self.focus = Focus::DabataseList; + self.record_table.reset(); } return Ok(EventState::Consumed); } @@ -199,25 +182,30 @@ impl App { return Ok(EventState::Consumed); } - if matches!(key, Key::Enter) && self.databases.tree_focused() { - if let Some((table, database)) = self.databases.tree().selected_table() { + if key == self.config.key_config.enter && self.databases.tree_focused() { + if let Some((database, table)) = self.databases.tree().selected_table() { self.focus = Focus::Table; let (headers, records) = self .pool .as_ref() .unwrap() - .get_records(&database, &table.name, 0, None) + .get_records(&database.name, &table.name, 0, None) .await?; - self.record_table = RecordTableComponent::new(records, headers); - self.record_table.set_table(table.name.to_string()); + self.record_table + .update(records, headers, database.clone(), table.clone()); let (headers, records) = self .pool .as_ref() .unwrap() - .get_columns(&database, &table.name) + .get_columns(&database.name, &table.name) .await?; - self.structure_table = TableComponent::new(records, headers); + self.structure_table.update( + records, + headers, + database.clone(), + table.clone(), + ); self.table_status .update(self.record_table.len() as u64, table); } @@ -231,22 +219,23 @@ impl App { 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() { 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; - if let Some((table, database)) = self.databases.tree().selected_table() + if let Some((database, table)) = self.databases.tree().selected_table() { let (headers, records) = self .pool .as_ref() .unwrap() .get_records( - &database.clone(), + &database.name.clone(), &table.name, 0, if self.record_table.filter.input.is_empty() { @@ -256,7 +245,7 @@ impl App { }, ) .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 == 0 { - if let Some((table, database)) = + if let Some((database, table)) = self.databases.tree().selected_table() { let (_, records) = self @@ -277,7 +266,7 @@ impl App { .as_ref() .unwrap() .get_records( - &database.clone(), + &database.name.clone(), &table.name, index as u16, if self.record_table.filter.input.is_empty() { @@ -301,7 +290,7 @@ impl App { 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() { self.clipboard.store(text) } @@ -314,7 +303,7 @@ impl App { } pub fn move_focus(&mut self, key: Key) -> anyhow::Result { - if let Key::Char('c') = key { + if key == self.config.key_config.focus_connections { self.focus = Focus::ConnectionList; return Ok(EventState::Consumed); } @@ -323,19 +312,19 @@ impl App { } match self.focus { Focus::ConnectionList => { - if let Key::Enter = key { + if key == self.config.key_config.enter { self.focus = Focus::DabataseList; return Ok(EventState::Consumed); } } 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; return Ok(EventState::Consumed); } } Focus::Table => { - if let Key::Left = key { + if key == self.config.key_config.focus_left { self.focus = Focus::DabataseList; return Ok(EventState::Consumed); } diff --git a/src/components/command.rs b/src/components/command.rs index 7c94ba3..c1a816d 100644 --- a/src/components/command.rs +++ b/src/components/command.rs @@ -1,18 +1,20 @@ +use crate::config::KeyConfig; + static CMD_GROUP_GENERAL: &str = "-- General --"; +static CMD_GROUP_TABLE: &str = "-- Table --"; +static CMD_GROUP_DATABASES: &str = "-- Databases --"; #[derive(Clone, PartialEq, PartialOrd, Ord, Eq)] pub struct CommandText { pub name: String, - pub desc: &'static str, pub group: &'static str, pub hide_help: bool, } 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 { name, - desc, group, hide_help: false, } @@ -21,70 +23,103 @@ impl CommandText { pub struct CommandInfo { pub text: CommandText, - pub enabled: bool, - pub quick_bar: bool, - pub available: bool, - pub order: i8, } impl CommandInfo { - pub const fn new(text: CommandText, enabled: bool, available: bool) -> Self { - Self { - text, - enabled, - quick_bar: true, - available, - order: 0, - } + pub const fn new(text: CommandText) -> Self { + Self { text } } +} - pub const fn order(self, order: i8) -> Self { - let mut res = self; - res.order = order; - res - } +pub fn scroll(key: &KeyConfig) -> CommandText { + CommandText::new( + format!( + "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( - format!("Move down [{}]", key), - "move down", + format!( + "Scroll to top/bottom [{},{}]", + key.scroll_to_top.to_string(), + key.scroll_to_bottom.to_string(), + ), CMD_GROUP_GENERAL, ) } -pub fn move_up(key: &str) -> CommandText { - CommandText::new(format!("Move up [{}]", key), "move up", CMD_GROUP_GENERAL) +pub fn expand_collapse(key: &KeyConfig) -> CommandText { + 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( - format!("Move right [{}]", key), - "move right", + format!("Filter [{}]", key.filter.to_string()), CMD_GROUP_GENERAL, ) } -pub fn move_left(key: &str) -> CommandText { +pub fn move_focus(key: &KeyConfig) -> CommandText { CommandText::new( - format!("Move left [{}]", key), - "move left", + format!( + "Move focus to left/right [{},{}]", + key.focus_left, key.focus_right + ), 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( - format!("Filter [{}]", key), - "enter input for filter", + format!( + "Tab [{},{}]", + key_config.tab_records, key_config.tab_structure + ), CMD_GROUP_GENERAL, ) } -pub fn move_focus_to_right_widget(key: &str) -> CommandText { +pub fn help(key_config: &KeyConfig) -> CommandText { CommandText::new( - format!("Move focus to right [{}]", key), - "move focus to right", + format!("Help [{}]", key_config.open_help), CMD_GROUP_GENERAL, ) } diff --git a/src/components/connections.rs b/src/components/connections.rs index 1d9c33c..4d87bb5 100644 --- a/src/components/connections.rs +++ b/src/components/connections.rs @@ -1,7 +1,7 @@ use super::{Component, DrawableComponent, EventState}; use crate::components::command::CommandInfo; +use crate::config::{Connection, KeyConfig}; use crate::event::Key; -use crate::user_config::Connection; use anyhow::Result; use tui::{ backend::Backend, @@ -15,22 +15,15 @@ use tui::{ pub struct ConnectionsComponent { connections: Vec, state: ListState, -} - -impl Default for ConnectionsComponent { - fn default() -> Self { - Self { - connections: Vec::new(), - state: ListState::default(), - } - } + key_config: KeyConfig, } impl ConnectionsComponent { - pub fn new(connections: Vec) -> Self { + pub fn new(key_config: KeyConfig, connections: Vec) -> Self { Self { connections, - ..Self::default() + key_config, + state: ListState::default(), } } @@ -100,19 +93,15 @@ impl DrawableComponent for ConnectionsComponent { } impl Component for ConnectionsComponent { - fn commands(&self, out: &mut Vec) {} + fn commands(&self, _out: &mut Vec) {} fn event(&mut self, key: Key) -> Result { - match key { - Key::Char('j') => { - self.next_connection(); - return Ok(EventState::Consumed); - } - Key::Char('k') => { - self.previous_connection(); - return Ok(EventState::Consumed); - } - _ => (), + if key == self.key_config.scroll_down { + self.next_connection(); + return Ok(EventState::Consumed); + } else if key == self.key_config.scroll_up { + self.previous_connection(); + return Ok(EventState::Consumed); } Ok(EventState::NotConsumed) } diff --git a/src/components/databases.rs b/src/components/databases.rs index f12c570..b764420 100644 --- a/src/components/databases.rs +++ b/src/components/databases.rs @@ -2,7 +2,8 @@ use super::{ compute_character_width, utils::scroll_vertical::VerticalScroll, Component, DrawableComponent, EventState, }; -use crate::components::command::CommandInfo; +use crate::components::command::{self, CommandInfo}; +use crate::config::KeyConfig; use crate::event::Key; use crate::ui::common_nav; 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 EMPTY_STR: &str = ""; +#[derive(PartialEq)] pub enum FocusBlock { Filter, Tree, @@ -39,10 +41,11 @@ pub struct DatabasesComponent { input_idx: usize, input_cursor_position: u16, focus_block: FocusBlock, + key_config: KeyConfig, } impl DatabasesComponent { - pub fn new() -> Self { + pub fn new(key_config: KeyConfig) -> Self { Self { tree: DatabaseTree::default(), filterd_tree: None, @@ -51,6 +54,7 @@ impl DatabasesComponent { input_idx: 0, input_cursor_position: 0, focus_block: FocusBlock::Tree, + key_config, } } @@ -192,7 +196,9 @@ impl DrawableComponent for DatabasesComponent { } impl Component for DatabasesComponent { - fn commands(&self, out: &mut Vec) {} + fn commands(&self, out: &mut Vec) { + out.push(CommandInfo::new(command::expand_collapse(&self.key_config))) + } fn event(&mut self, key: Key) -> Result { let input_str: String = self.input.iter().collect(); @@ -203,15 +209,16 @@ impl Component for DatabasesComponent { &mut self.tree }, key, + &self.key_config, ) { 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 { - Key::Char('/') if matches!(self.focus_block, FocusBlock::Tree) => { - self.focus_block = FocusBlock::Filter; - return Ok(EventState::Consumed); - } - Key::Char(c) if matches!(self.focus_block, FocusBlock::Filter) => { + Key::Char(c) if self.focus_block == FocusBlock::Filter => { self.input.insert(self.input_idx, c); self.input_idx += 1; 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 { - if let Some(common_nav) = common_nav(key) { +fn tree_nav(tree: &mut DatabaseTree, key: Key, key_config: &KeyConfig) -> bool { + if let Some(common_nav) = common_nav(key, key_config) { tree.move_selection(common_nav) } else { false diff --git a/src/components/error.rs b/src/components/error.rs index acac088..9836d00 100644 --- a/src/components/error.rs +++ b/src/components/error.rs @@ -1,5 +1,6 @@ use super::{Component, DrawableComponent, EventState}; use crate::components::command::CommandInfo; +use crate::config::KeyConfig; use crate::event::Key; use anyhow::Result; use tui::{ @@ -11,27 +12,34 @@ use tui::{ }; pub struct ErrorComponent { - pub error: Option, + pub error: String, + visible: bool, + key_config: KeyConfig, } -impl Default for ErrorComponent { - fn default() -> Self { - Self { error: None } +impl ErrorComponent { + pub fn new(key_config: KeyConfig) -> Self { + Self { + error: String::new(), + visible: false, + key_config, + } } } impl ErrorComponent { - pub fn set(&mut self, error: String) { - self.error = Some(error); + pub fn set(&mut self, error: String) -> anyhow::Result<()> { + self.error = error; + self.show() } } impl DrawableComponent for ErrorComponent { fn draw(&mut self, f: &mut Frame, _area: Rect, _focused: bool) -> Result<()> { - if let Some(error) = self.error.as_ref() { + if self.visible { let width = 65; 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)) .style(Style::default().fg(Color::Red)) .alignment(Alignment::Left) @@ -50,9 +58,27 @@ impl DrawableComponent for ErrorComponent { } impl Component for ErrorComponent { - fn commands(&self, out: &mut Vec) {} + fn commands(&self, _out: &mut Vec) {} - fn event(&mut self, _key: Key) -> Result { + fn event(&mut self, key: Key) -> Result { + 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) } + + fn hide(&mut self) { + self.visible = false; + } + + fn show(&mut self) -> Result<()> { + self.visible = true; + + Ok(()) + } } diff --git a/src/components/help.rs b/src/components/help.rs index f1bc03f..2b68838 100644 --- a/src/components/help.rs +++ b/src/components/help.rs @@ -1,5 +1,6 @@ use super::{Component, DrawableComponent, EventState}; use crate::components::command::CommandInfo; +use crate::config::KeyConfig; use crate::event::Key; use anyhow::Result; use itertools::Itertools; @@ -17,6 +18,7 @@ pub struct HelpComponent { cmds: Vec, visible: bool, selection: u16, + key_config: KeyConfig, } impl DrawableComponent for HelpComponent { @@ -69,27 +71,22 @@ impl DrawableComponent for HelpComponent { } impl Component for HelpComponent { - fn commands(&self, out: &mut Vec) {} + fn commands(&self, _out: &mut Vec) {} fn event(&mut self, key: Key) -> Result { if self.visible { - match key { - Key::Esc => { - self.hide(); - return Ok(EventState::Consumed); - } - Key::Char('j') => { - self.move_selection(true); - return Ok(EventState::Consumed); - } - Key::Char('k') => { - self.move_selection(false); - return Ok(EventState::Consumed); - } - _ => (), + if key == self.key_config.exit_popup { + self.hide(); + return Ok(EventState::Consumed); + } else if key == self.key_config.scroll_down { + self.scroll_selection(true); + return Ok(EventState::Consumed); + } else if key == self.key_config.scroll_up { + self.scroll_selection(false); + return Ok(EventState::Consumed); } return Ok(EventState::NotConsumed); - } else if let Key::Char('?') = key { + } else if key == self.key_config.open_help { self.show()?; return Ok(EventState::Consumed); } @@ -108,11 +105,12 @@ impl Component for HelpComponent { } impl HelpComponent { - pub const fn new() -> Self { + pub const fn new(key_config: KeyConfig) -> Self { Self { cmds: vec![], visible: false, selection: 0, + key_config, } } @@ -123,7 +121,7 @@ impl HelpComponent { .collect::>(); } - fn move_selection(&mut self, inc: bool) { + fn scroll_selection(&mut self, inc: bool) { let mut new_selection = self.selection; new_selection = if inc { @@ -152,7 +150,7 @@ impl HelpComponent { processed += 1; 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 { Style::default().bg(Color::Blue) } else { @@ -168,15 +166,16 @@ impl HelpComponent { #[cfg(test)] mod test { - use super::{Color, CommandInfo, HelpComponent, Modifier, Span, Spans, Style}; + use super::{Color, CommandInfo, HelpComponent, KeyConfig, Modifier, Span, Spans, Style}; #[test] fn test_get_text() { 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![ - CommandInfo::new(crate::components::command::move_left("h"), true, true), - CommandInfo::new(crate::components::command::move_right("l"), true, true), + CommandInfo::new(crate::components::command::scroll(&key_config)), + CommandInfo::new(crate::components::command::filter(&key_config)), ]); assert_eq!( component.get_text(width), @@ -186,10 +185,10 @@ mod test { Style::default().add_modifier(Modifier::REVERSED) )), Spans::from(Span::styled( - "Move left [h] 3", + " Scroll up/down/left/right [k,j,h,l] 3", Style::default().bg(Color::Blue) )), - Spans::from(Span::styled("Move right [l] 3", Style::default())) + Spans::from(Span::styled(" Filter [/] 3", Style::default())) ] ); } diff --git a/src/components/record_table.rs b/src/components/record_table.rs index 8b2a087..7c9898c 100644 --- a/src/components/record_table.rs +++ b/src/components/record_table.rs @@ -1,8 +1,10 @@ use super::{Component, DrawableComponent, EventState}; use crate::components::command::CommandInfo; use crate::components::{TableComponent, TableFilterComponent}; +use crate::config::KeyConfig; use crate::event::Key; use anyhow::Result; +use database_tree::{Database, Table as DTable}; use tui::{ backend::Backend, layout::{Constraint, Direction, Layout, Rect}, @@ -18,52 +20,39 @@ pub struct RecordTableComponent { pub filter: TableFilterComponent, pub table: TableComponent, pub focus: Focus, + key_config: KeyConfig, } -impl Default for RecordTableComponent { - fn default() -> Self { +impl RecordTableComponent { + pub fn new(key_config: KeyConfig) -> Self { Self { filter: TableFilterComponent::default(), - table: TableComponent::default(), + table: TableComponent::new(key_config.clone()), focus: Focus::Table, + key_config, } } -} -impl RecordTableComponent { - pub fn new(rows: Vec>, headers: Vec) -> Self { - Self { - table: TableComponent::new(rows, headers), - ..Self::default() - } - } - - pub fn update(&mut self, rows: Vec>, headers: Vec) { - 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 update( + &mut self, + rows: Vec>, + headers: Vec, + database: Database, + table: DTable, + ) { + self.table.update(rows, headers, database, table.clone()); + self.filter.table = Some(table); } pub fn reset(&mut self) { - self.table = TableComponent::default(); - if !self.table.rows.is_empty() { - self.table.selected_row.select(None); - self.table.selected_row.select(Some(0)) - } - self.filter = TableFilterComponent::default(); + self.table.reset(); + self.filter.reset(); } pub fn len(&self) -> usize { self.table.rows.len() } - pub fn set_table(&mut self, table: String) { - self.filter.table = Some(table) - } - pub fn filter_focused(&self) -> bool { matches!(self.focus, Focus::Filter) } @@ -86,14 +75,16 @@ impl DrawableComponent for RecordTableComponent { } impl Component for RecordTableComponent { - fn commands(&self, out: &mut Vec) {} + fn commands(&self, out: &mut Vec) { + self.table.commands(out) + } fn event(&mut self, key: Key) -> Result { + if key == self.key_config.filter { + self.focus = Focus::Filter; + return Ok(EventState::Consumed); + } 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::Table) => return self.table.event(key), _ => (), diff --git a/src/components/tab.rs b/src/components/tab.rs index 206f5be..4e7d030 100644 --- a/src/components/tab.rs +++ b/src/components/tab.rs @@ -1,8 +1,8 @@ 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 anyhow::Result; -use strum::IntoEnumIterator; use strum_macros::EnumIter; use tui::{ backend::Backend, @@ -25,29 +25,30 @@ impl std::fmt::Display for Tab { } } -impl Tab { - pub fn names() -> Vec { - Self::iter() - .map(|tab| format!("{} [{}]", tab, tab as u8 + 1)) - .collect() - } -} - pub struct TabComponent { pub selected_tab: Tab, + key_config: KeyConfig, } -impl Default for TabComponent { - fn default() -> Self { +impl TabComponent { + pub fn new(key_config: KeyConfig) -> Self { Self { selected_tab: Tab::Records, + key_config, } } + + fn names(&self) -> Vec { + vec![ + command::tab_records(&self.key_config).name, + command::tab_structure(&self.key_config).name, + ] + } } impl DrawableComponent for TabComponent { fn draw(&mut self, f: &mut Frame, 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) .block(Block::default().borders(Borders::ALL)) .select(self.selected_tab as usize) @@ -63,19 +64,16 @@ impl DrawableComponent for TabComponent { } impl Component for TabComponent { - fn commands(&self, out: &mut Vec) {} + fn commands(&self, _out: &mut Vec) {} fn event(&mut self, key: Key) -> Result { - match key { - Key::Char('1') => { - self.selected_tab = Tab::Records; - Ok(EventState::Consumed) - } - Key::Char('2') => { - self.selected_tab = Tab::Structure; - Ok(EventState::Consumed) - } - _ => Ok(EventState::NotConsumed), + if key == self.key_config.tab_records { + self.selected_tab = Tab::Records; + return Ok(EventState::Consumed); + } else if key == self.key_config.tab_structure { + self.selected_tab = Tab::Structure; + return Ok(EventState::Consumed); } + Ok(EventState::NotConsumed) } } diff --git a/src/components/table.rs b/src/components/table.rs index 6bc4406..c4ba7e7 100644 --- a/src/components/table.rs +++ b/src/components/table.rs @@ -2,9 +2,11 @@ use super::{ utils::scroll_vertical::VerticalScroll, Component, DrawableComponent, EventState, TableValueComponent, }; -use crate::components::command::CommandInfo; +use crate::components::command::{self, CommandInfo}; +use crate::config::KeyConfig; use crate::event::Key; use anyhow::Result; +use database_tree::{Database, Table as DTable}; use std::convert::From; use tui::{ backend::Backend, @@ -20,43 +22,70 @@ pub struct TableComponent { pub rows: Vec>, pub eod: bool, pub selected_row: TableState, + table: Option<(Database, DTable)>, selected_column: usize, selection_area_corner: Option<(usize, usize)>, column_page_start: std::cell::Cell, scroll: VerticalScroll, + key_config: KeyConfig, } -impl Default for TableComponent { - fn default() -> Self { +impl TableComponent { + pub fn new(key_config: KeyConfig) -> Self { Self { selected_row: TableState::default(), headers: vec![], rows: vec![], + table: None, selected_column: 0, selection_area_corner: None, column_page_start: std::cell::Cell::new(0), scroll: VerticalScroll::new(), eod: false, + key_config, } } -} -impl TableComponent { - pub fn new(rows: Vec>, headers: Vec) -> Self { - let mut selected_row = TableState::default(); + fn title(&self) -> String { + self.table.as_ref().map_or(" - ".to_string(), |table| { + format!("{}/{}", table.0.name, table.1.name) + }) + } + + pub fn update( + &mut self, + rows: Vec>, + headers: Vec, + database: Database, + table: DTable, + ) { if !rows.is_empty() { - selected_row.select(None); - selected_row.select(Some(0)) - } - Self { - headers, - rows, - selected_row, - ..Self::default() + self.selected_row.select(None); + self.selected_row.select(Some(0)) } + 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; } @@ -75,7 +104,7 @@ impl TableComponent { } None => None, }; - self.reset(); + self.reset_selection(); self.selected_row.select(i); } @@ -90,7 +119,7 @@ impl TableComponent { } None => None, }; - self.reset(); + self.reset_selection(); self.selected_row.select(i); } @@ -98,7 +127,7 @@ impl TableComponent { if self.rows.is_empty() { return; } - self.reset(); + self.reset_selection(); self.selected_row.select(Some(0)); } @@ -106,7 +135,7 @@ impl TableComponent { if self.rows.is_empty() { return; } - self.reset(); + self.reset_selection(); 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) { return; } - self.reset(); + self.reset_selection(); self.selected_column += 1; } @@ -128,7 +157,7 @@ impl TableComponent { if self.selected_column == 0 { return; } - self.reset(); + self.reset_selection(); self.selected_column -= 1; } @@ -389,7 +418,7 @@ impl DrawableComponent for TableComponent { TableValueComponent::new(self.selected_cells().unwrap_or_default()) .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) = self.calculate_cell_widths(block.inner(layout[1]).width); let header_cells = headers.iter().enumerate().map(|(column_index, h)| { @@ -448,59 +477,49 @@ impl DrawableComponent for TableComponent { } impl Component for TableComponent { - fn commands(&self, out: &mut Vec) {} + fn commands(&self, out: &mut Vec) { + out.push(CommandInfo::new(command::extend_selection_by_one_cell( + &self.key_config, + ))); + } fn event(&mut self, key: Key) -> Result { - match key { - Key::Char('h') => { - self.previous_column(); - return Ok(EventState::Consumed); - } - Key::Char('j') => { - self.next_row(1); - return Ok(EventState::NotConsumed); - } - Key::Ctrl('d') => { - self.next_row(10); - return Ok(EventState::NotConsumed); - } - Key::Char('k') => { - self.previous_row(1); - return Ok(EventState::Consumed); - } - Key::Ctrl('u') => { - self.previous_row(10); - return Ok(EventState::Consumed); - } - Key::Char('g') => { - self.scroll_top(); - return Ok(EventState::Consumed); - } - Key::Char('G') => { - self.scroll_bottom(); - return Ok(EventState::Consumed); - } - Key::Char('l') => { - self.next_column(); - return Ok(EventState::Consumed); - } - Key::Char('H') => { - self.expand_selected_area_x(false); - 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); - } - _ => (), + if key == self.key_config.scroll_left { + self.previous_column(); + return Ok(EventState::Consumed); + } else if key == self.key_config.scroll_down { + self.next_row(1); + return Ok(EventState::NotConsumed); + } else if key == self.key_config.scroll_down_multiple_lines { + self.next_row(10); + return Ok(EventState::NotConsumed); + } else if key == self.key_config.scroll_up { + self.previous_row(1); + return Ok(EventState::Consumed); + } else if key == self.key_config.scroll_up_multiple_lines { + self.previous_row(10); + return Ok(EventState::Consumed); + } else if key == self.key_config.scroll_to_top { + self.scroll_top(); + return Ok(EventState::Consumed); + } else if key == self.key_config.scroll_to_bottom { + self.scroll_bottom(); + return Ok(EventState::Consumed); + } else if key == self.key_config.scroll_right { + self.next_column(); + return Ok(EventState::Consumed); + } else if key == self.key_config.extend_selection_by_one_cell_left { + self.expand_selected_area_x(false); + return Ok(EventState::Consumed); + } else if key == self.key_config.extend_selection_by_one_cell_up { + self.expand_selected_area_y(false); + return Ok(EventState::Consumed); + } else if key == self.key_config.extend_selection_by_one_cell_down { + self.expand_selected_area_y(true); + return Ok(EventState::Consumed); + } else if key == self.key_config.extend_selection_by_one_cell_right { + self.expand_selected_area_x(true); + return Ok(EventState::Consumed); } Ok(EventState::NotConsumed) } @@ -508,19 +527,19 @@ impl Component for TableComponent { #[cfg(test)] mod test { - use super::TableComponent; + use super::{KeyConfig, TableComponent}; use tui::layout::Constraint; #[test] 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(); assert_eq!(component.headers(1, 2), vec!["", "b"]) } #[test] fn test_rows() { - let mut component = TableComponent::default(); + let mut component = TableComponent::new(KeyConfig::default()); component.rows = vec![ vec!["a", "b", "c"].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 // 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.rows = vec![ vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect(), @@ -565,7 +584,7 @@ mod test { // 1 a b c // 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.rows = vec![ vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect(), @@ -590,7 +609,7 @@ mod test { // 1 a |b| c // 2 d |e| f - let mut component = TableComponent::default(); + let mut component = TableComponent::new(KeyConfig::default()); component.rows = vec![ vec!["a", "b", "c"].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 // 2 d |e| f - let mut component = TableComponent::default(); + let mut component = TableComponent::new(KeyConfig::default()); component.rows = vec![ vec!["a", "b", "c"].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] 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.rows = vec![ vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect(), @@ -645,7 +664,7 @@ mod test { // 1 |a| b c // 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.rows = vec![ vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect(), @@ -661,7 +680,7 @@ mod test { // 1 |a b| c // 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.rows = vec![ vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect(), @@ -678,7 +697,7 @@ mod test { // 1 |a| b c // 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.rows = vec![ vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect(), @@ -699,7 +718,7 @@ mod test { // 1 |a b| c // 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.rows = vec![ vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect(), @@ -721,7 +740,7 @@ mod test { #[test] 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.rows = vec![ 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.rows = vec![ vec!["aaaaa", "bbbbb", "ccccc"] diff --git a/src/components/table_filter.rs b/src/components/table_filter.rs index bcf93dd..e91932a 100644 --- a/src/components/table_filter.rs +++ b/src/components/table_filter.rs @@ -2,6 +2,7 @@ use super::{compute_character_width, Component, DrawableComponent, EventState}; use crate::components::command::CommandInfo; use crate::event::Key; use anyhow::Result; +use database_tree::Table; use tui::{ backend::Backend, layout::Rect, @@ -13,7 +14,7 @@ use tui::{ use unicode_width::UnicodeWidthStr; pub struct TableFilterComponent { - pub table: Option, + pub table: Option
, pub input: Vec, input_idx: usize, input_cursor_position: u16, @@ -34,6 +35,13 @@ impl TableFilterComponent { pub fn input_str(&self) -> String { 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 { @@ -42,7 +50,7 @@ impl DrawableComponent for TableFilterComponent { Span::styled( self.table .as_ref() - .map_or("-".to_string(), |table| table.to_string()), + .map_or("-".to_string(), |table| table.name.to_string()), Style::default().fg(Color::Blue), ), Span::from(format!( @@ -67,7 +75,7 @@ impl DrawableComponent for TableFilterComponent { + (1 + self .table .as_ref() - .map_or(String::new(), |table| table.to_string()) + .map_or(String::new(), |table| table.name.to_string()) .width() + 1) as u16) .saturating_add(self.input_cursor_position), @@ -79,7 +87,7 @@ impl DrawableComponent for TableFilterComponent { } impl Component for TableFilterComponent { - fn commands(&self, out: &mut Vec) {} + fn commands(&self, _out: &mut Vec) {} fn event(&mut self, key: Key) -> Result { let input_str: String = self.input.iter().collect(); diff --git a/src/components/table_status.rs b/src/components/table_status.rs index 582e368..89998d5 100644 --- a/src/components/table_status.rs +++ b/src/components/table_status.rs @@ -86,7 +86,7 @@ impl DrawableComponent for TableStatusComponent { } impl Component for TableStatusComponent { - fn commands(&self, out: &mut Vec) {} + fn commands(&self, _out: &mut Vec) {} fn event(&mut self, _key: Key) -> Result { Ok(EventState::NotConsumed) diff --git a/src/components/table_value.rs b/src/components/table_value.rs index eed0726..243803d 100644 --- a/src/components/table_value.rs +++ b/src/components/table_value.rs @@ -46,7 +46,7 @@ impl DrawableComponent for TableValueComponent { } impl Component for TableValueComponent { - fn commands(&self, out: &mut Vec) {} + fn commands(&self, _out: &mut Vec) {} fn event(&mut self, _key: Key) -> Result { todo!("scroll"); diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..cb85e04 --- /dev/null +++ b/src/config.rs @@ -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, + #[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, + user: String, + host: String, + port: u64, + pub database: Option, +} + +#[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 { + 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 = 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, + ), + } + } +} diff --git a/src/event/events.rs b/src/event/events.rs index afa3aba..a01e93d 100644 --- a/src/event/events.rs +++ b/src/event/events.rs @@ -3,11 +3,8 @@ use crossterm::event; use std::{sync::mpsc, thread, time::Duration}; #[derive(Debug, Clone, Copy)] -/// Configuration for event handling. pub struct EventConfig { - /// The key that is used to exit the application. pub exit_key: Key, - /// The tick rate at which the application will sent an tick event. pub tick_rate: Duration, } @@ -20,25 +17,18 @@ impl Default for EventConfig { } } -/// An occurred event. #[derive(Copy, Clone)] pub enum Event { - /// An input event occurred. Input(I), - /// An tick event occurred. Tick, } -/// A small event handler that wrap crossterm input and tick event. Each event -/// type is handled in its own thread and returned to a common `Receiver` pub struct Events { rx: mpsc::Receiver>, - // Need to be kept around to prevent disposing the sender side. _tx: mpsc::Sender>, } impl Events { - /// Constructs an new instance of `Events` with the default config. pub fn new(tick_rate: u64) -> Events { Events::with_config(EventConfig { tick_rate: Duration::from_millis(tick_rate), @@ -46,31 +36,25 @@ impl Events { }) } - /// Constructs an new instance of `Events` from given config. pub fn with_config(config: EventConfig) -> Events { let (tx, rx) = mpsc::channel(); let event_tx = tx.clone(); - thread::spawn(move || { - loop { - // poll for tick rate duration, if no event, sent tick event. - if event::poll(config.tick_rate).unwrap() { - if let event::Event::Key(key) = event::read().unwrap() { - let key = Key::from(key); + thread::spawn(move || loop { + if event::poll(config.tick_rate).unwrap() { + if let event::Event::Key(key) = event::read().unwrap() { + let key = Key::from(key); - event_tx.send(Event::Input(key)).unwrap(); - } + event_tx.send(Event::Input(key)).unwrap(); } - - event_tx.send(Event::Tick).unwrap(); } + + event_tx.send(Event::Tick).unwrap(); }); Events { rx, _tx: tx } } - /// Attempts to read an event. - /// This function will block the current thread. pub fn next(&self) -> Result, mpsc::RecvError> { self.rx.recv() } diff --git a/src/event/key.rs b/src/event/key.rs index 4e29c35..cfbe1eb 100644 --- a/src/event/key.rs +++ b/src/event/key.rs @@ -1,8 +1,9 @@ use crossterm::event; +use serde::Deserialize; use std::fmt; /// Represents a key. -#[derive(PartialEq, Eq, Clone, Copy, Hash, Debug)] +#[derive(PartialEq, Eq, Clone, Copy, Hash, Debug, Deserialize)] pub enum Key { /// Both Enter (or Return) and numpad Enter Enter, @@ -104,7 +105,10 @@ impl fmt::Display for Key { Key::Alt(c) => write!(f, "", c), Key::Ctrl(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::Tab | Key::Backspace diff --git a/src/main.rs b/src/main.rs index 3da4693..8fe10a2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,9 @@ mod app; mod clipboard; mod components; +mod config; mod event; mod ui; -mod user_config; mod utils; #[macro_use] @@ -28,7 +28,7 @@ async fn main() -> anyhow::Result<()> { outln!("gobang logger"); - let user_config = user_config::UserConfig::new("sample.toml").ok(); + let config = config::Config::new("sample.toml")?; let stdout = stdout(); setup_terminal()?; @@ -36,7 +36,7 @@ async fn main() -> anyhow::Result<()> { let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; let events = event::Events::new(250); - let mut app = App::new(user_config.unwrap()); + let mut app = App::new(config); terminal.clear()?; @@ -45,11 +45,13 @@ async fn main() -> anyhow::Result<()> { match events.next()? { Event::Input(key) => match app.event(key).await { 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; } } - Err(err) => app.error.set(err.to_string()), + Err(err) => app.error.set(err.to_string())?, }, Event::Tick => (), } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 45766f3..6f81822 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,22 +1,23 @@ +use crate::config::KeyConfig; use crate::event::Key; use database_tree::MoveSelection; pub mod scrollbar; pub mod scrolllist; -pub fn common_nav(key: Key) -> Option { - if key == Key::Char('j') { +pub fn common_nav(key: Key, key_config: &KeyConfig) -> Option { + if key == key_config.scroll_down { Some(MoveSelection::Down) - } else if key == Key::Char('k') { + } else if key == key_config.scroll_up { 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') { + } else if key == key_config.scroll_right { Some(MoveSelection::Right) - } else if key == Key::Char('h') { + } else if key == key_config.scroll_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 { None } diff --git a/src/user_config.rs b/src/user_config.rs deleted file mode 100644 index dc7d13a..0000000 --- a/src/user_config.rs +++ /dev/null @@ -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, -} - -#[derive(Debug, Deserialize, Clone)] -pub struct Connection { - name: Option, - user: String, - host: String, - port: u64, - pub database: Option, -} - -impl UserConfig { - pub fn new(path: &str) -> anyhow::Result { - 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 = 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, - ), - } - } -}