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 |
| ---- | ---- |
| <kbd>h</kbd> | move left |
| <kbd>j</kbd> | move down |
| <kbd>k</kbd> | move up |
| <kbd>l</kbd> | move right |
| <kbd>h</kbd> | scroll left |
| <kbd>j</kbd> | scroll down |
| <kbd>k</kbd> | scroll up |
| <kbd>l</kbd> | scroll right |
| <kbd>Ctrl</kbd> + <kbd>d</kbd> | scroll down multiple lines |
| <kbd>Ctrl</kbd> + <kbd>u</kbd> | scroll up multiple lines |

@ -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();

@ -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<String> {
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(),
},
})

@ -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<Table>,

@ -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<Box<dyn Pool>>,
pub user_config: Option<UserConfig>,
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<CommandInfo> {
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<EventState> {
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<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);
}
@ -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<EventState> {
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);
}

@ -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,
)
}

@ -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<Connection>,
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<Connection>) -> Self {
pub fn new(key_config: KeyConfig, connections: Vec<Connection>) -> 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<CommandInfo>) {}
fn commands(&self, _out: &mut Vec<CommandInfo>) {}
fn event(&mut self, key: Key) -> Result<EventState> {
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)
}

@ -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<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> {
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

@ -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<String>,
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<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 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<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)
}
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 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<CommandInfo>,
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<CommandInfo>) {}
fn commands(&self, _out: &mut Vec<CommandInfo>) {}
fn event(&mut self, key: Key) -> Result<EventState> {
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::<Vec<_>>();
}
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()))
]
);
}

@ -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<Vec<String>>, headers: Vec<String>) -> Self {
Self {
table: TableComponent::new(rows, headers),
..Self::default()
}
}
pub fn update(&mut self, rows: Vec<Vec<String>>, headers: Vec<String>) {
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<Vec<String>>,
headers: Vec<String>,
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<CommandInfo>) {}
fn commands(&self, out: &mut Vec<CommandInfo>) {
self.table.commands(out)
}
fn event(&mut self, key: Key) -> Result<EventState> {
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),
_ => (),

@ -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<String> {
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<String> {
vec![
command::tab_records(&self.key_config).name,
command::tab_structure(&self.key_config).name,
]
}
}
impl DrawableComponent for TabComponent {
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)
.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<CommandInfo>) {}
fn commands(&self, _out: &mut Vec<CommandInfo>) {}
fn event(&mut self, key: Key) -> Result<EventState> {
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)
}
}

@ -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<Vec<String>>,
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<usize>,
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<Vec<String>>, headers: Vec<String>) -> 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<Vec<String>>,
headers: Vec<String>,
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<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> {
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"]

@ -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<String>,
pub table: Option<Table>,
pub input: Vec<char>,
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<CommandInfo>) {}
fn commands(&self, _out: &mut Vec<CommandInfo>) {}
fn event(&mut self, key: Key) -> Result<EventState> {
let input_str: String = self.input.iter().collect();

@ -86,7 +86,7 @@ impl DrawableComponent 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> {
Ok(EventState::NotConsumed)

@ -46,7 +46,7 @@ impl DrawableComponent 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> {
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};
#[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<I> {
/// 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<Event<Key>>,
// Need to be kept around to prevent disposing the sender side.
_tx: mpsc::Sender<Event<Key>>,
}
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<Event<Key>, mpsc::RecvError> {
self.rx.recv()
}

@ -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, "<Alt+{}>", c),
Key::Ctrl(c) => write!(f, "<Ctrl+{}>", 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

@ -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 => (),
}

@ -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<MoveSelection> {
if key == Key::Char('j') {
pub fn common_nav(key: Key, key_config: &KeyConfig) -> Option<MoveSelection> {
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
}

@ -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