mirror of https://github.com/TaKO8Ki/gobang
Refactor components (#12)
* add logger * add shift key * implement record table reset function * fix style * ignore gobang.log * use table component * remove doc * pass focused as an argument * remove unused enums * create table component * use databases component * use query component * remove unused fields * use query component * use connections component * remove unused struct * use tab component * use table status component * remove unneeded return type * update gobang.gifpull/14/head
parent
acac235c6e
commit
f221e817a3
@ -1,3 +1,4 @@
|
||||
/target
|
||||
gobang
|
||||
gobang.yml
|
||||
gobang.log
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 6.8 MiB After Width: | Height: | Size: 3.9 MiB |
@ -0,0 +1,126 @@
|
||||
use super::{Component, DrawableComponent};
|
||||
use crate::event::Key;
|
||||
use crate::user_config::Connection;
|
||||
use anyhow::Result;
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Style},
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, Borders, Clear, List, ListItem, ListState},
|
||||
Frame,
|
||||
};
|
||||
|
||||
pub struct ConnectionsComponent {
|
||||
pub connections: Vec<Connection>,
|
||||
pub state: ListState,
|
||||
}
|
||||
|
||||
impl Default for ConnectionsComponent {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
connections: Vec::new(),
|
||||
state: ListState::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectionsComponent {
|
||||
pub fn new(connections: Vec<Connection>) -> Self {
|
||||
Self {
|
||||
connections,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_connection(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.connections.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
pub fn previous_connection(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
self.connections.len() - 1
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
pub fn selected_connection(&self) -> Option<&Connection> {
|
||||
match self.state.selected() {
|
||||
Some(i) => self.connections.get(i),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DrawableComponent for ConnectionsComponent {
|
||||
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, _area: Rect, _focused: bool) -> Result<()> {
|
||||
let percent_x = 60;
|
||||
let percent_y = 50;
|
||||
let conns = &self.connections;
|
||||
let connections: Vec<ListItem> = conns
|
||||
.iter()
|
||||
.map(|i| {
|
||||
ListItem::new(vec![Spans::from(Span::raw(i.database_url()))])
|
||||
.style(Style::default())
|
||||
})
|
||||
.collect();
|
||||
let tasks = List::new(connections)
|
||||
.block(Block::default().borders(Borders::ALL).title("Connections"))
|
||||
.highlight_style(Style::default().bg(Color::Blue))
|
||||
.style(Style::default());
|
||||
let popup_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage((100 - percent_y) / 2),
|
||||
Constraint::Percentage(percent_y),
|
||||
Constraint::Percentage((100 - percent_y) / 2),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
|
||||
let area = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage((100 - percent_x) / 2),
|
||||
Constraint::Percentage(percent_x),
|
||||
Constraint::Percentage((100 - percent_x) / 2),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(popup_layout[1])[1];
|
||||
f.render_widget(Clear, area);
|
||||
f.render_stateful_widget(tasks, area, &mut self.state);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for ConnectionsComponent {
|
||||
fn event(&mut self, key: Key) -> Result<()> {
|
||||
match key {
|
||||
Key::Char('j') => self.next_connection(),
|
||||
Key::Char('k') => self.previous_connection(),
|
||||
_ => (),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
use super::{Component, DrawableComponent};
|
||||
use crate::event::Key;
|
||||
use anyhow::Result;
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
pub struct QueryComponent {
|
||||
pub input: String,
|
||||
pub input_cursor_x: u16,
|
||||
}
|
||||
|
||||
impl Default for QueryComponent {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
input: String::new(),
|
||||
input_cursor_x: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl QueryComponent {
|
||||
pub fn increment_input_cursor_x(&mut self) {
|
||||
if self.input_cursor_x > 0 {
|
||||
self.input_cursor_x -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decrement_input_cursor_x(&mut self) {
|
||||
if self.input_cursor_x < self.input.width() as u16 {
|
||||
self.input_cursor_x += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DrawableComponent for QueryComponent {
|
||||
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, area: Rect, focused: bool) -> Result<()> {
|
||||
let query = Paragraph::new(self.input.as_ref())
|
||||
.style(if focused {
|
||||
Style::default()
|
||||
} else {
|
||||
Style::default().fg(Color::DarkGray)
|
||||
})
|
||||
.block(Block::default().borders(Borders::ALL).title("Query"));
|
||||
f.render_widget(query, area);
|
||||
if focused {
|
||||
f.set_cursor(
|
||||
area.x + self.input.width() as u16 + 1 - self.input_cursor_x,
|
||||
area.y + 1,
|
||||
)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for QueryComponent {
|
||||
fn event(&mut self, key: Key) -> Result<()> {
|
||||
match key {
|
||||
Key::Char(c) => self.input.push(c),
|
||||
Key::Delete | Key::Backspace => {
|
||||
if self.input.width() > 0 {
|
||||
if self.input_cursor_x == 0 {
|
||||
self.input.pop();
|
||||
return Ok(());
|
||||
}
|
||||
if self.input.width() - self.input_cursor_x as usize > 0 {
|
||||
self.input
|
||||
.remove(self.input.width() - self.input_cursor_x as usize);
|
||||
}
|
||||
}
|
||||
}
|
||||
Key::Left => self.decrement_input_cursor_x(),
|
||||
Key::Right => self.increment_input_cursor_x(),
|
||||
_ => (),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
use super::{Component, DrawableComponent};
|
||||
use crate::event::Key;
|
||||
use anyhow::Result;
|
||||
use strum::IntoEnumIterator;
|
||||
use strum_macros::EnumIter;
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
layout::Rect,
|
||||
style::{Color, Modifier, Style},
|
||||
text::Spans,
|
||||
widgets::{Block, Borders, Tabs},
|
||||
Frame,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, EnumIter)]
|
||||
pub enum Tab {
|
||||
Records,
|
||||
Structure,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Tab {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
impl Default for TabComponent {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
selected_tab: Tab::Records,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 tabs = Tabs::new(titles)
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.select(self.selected_tab as usize)
|
||||
.style(Style::default().fg(Color::DarkGray))
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.fg(Color::Reset)
|
||||
.add_modifier(Modifier::UNDERLINED),
|
||||
);
|
||||
f.render_widget(tabs, area);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for TabComponent {
|
||||
fn event(&mut self, key: Key) -> Result<()> {
|
||||
match key {
|
||||
Key::Char('1') => self.selected_tab = Tab::Records,
|
||||
Key::Char('2') => self.selected_tab = Tab::Structure,
|
||||
_ => (),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -0,0 +1,196 @@
|
||||
use super::{utils::scroll_vertical::VerticalScroll, Component, DrawableComponent};
|
||||
use crate::event::Key;
|
||||
use anyhow::Result;
|
||||
use std::convert::From;
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
layout::{Constraint, Rect},
|
||||
style::{Color, Style},
|
||||
widgets::{Block, Borders, Cell, Row, Table as WTable, TableState},
|
||||
Frame,
|
||||
};
|
||||
|
||||
pub struct TableComponent {
|
||||
pub state: TableState,
|
||||
pub headers: Vec<String>,
|
||||
pub rows: Vec<Vec<String>>,
|
||||
pub column_index: usize,
|
||||
pub scroll: VerticalScroll,
|
||||
}
|
||||
|
||||
impl Default for TableComponent {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
state: TableState::default(),
|
||||
headers: vec![],
|
||||
rows: vec![],
|
||||
column_index: 0,
|
||||
scroll: VerticalScroll::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TableComponent {
|
||||
pub fn next(&mut self, lines: usize) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i + lines >= self.rows.len() {
|
||||
Some(self.rows.len() - 1)
|
||||
} else {
|
||||
Some(i + lines)
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
self.state.select(i);
|
||||
}
|
||||
|
||||
pub fn reset(&mut self, headers: Vec<String>, rows: Vec<Vec<String>>) {
|
||||
self.headers = headers;
|
||||
self.rows = rows;
|
||||
self.column_index = 0;
|
||||
self.state.select(None);
|
||||
if !self.rows.is_empty() {
|
||||
self.state.select(Some(0));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scroll_top(&mut self) {
|
||||
if self.rows.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.state.select(None);
|
||||
self.state.select(Some(0));
|
||||
}
|
||||
|
||||
pub fn scroll_bottom(&mut self) {
|
||||
if self.rows.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.state.select(Some(self.rows.len() - 1));
|
||||
}
|
||||
|
||||
pub fn previous(&mut self, lines: usize) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i <= lines {
|
||||
Some(0)
|
||||
} else {
|
||||
Some(i - lines)
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
self.state.select(i);
|
||||
}
|
||||
|
||||
pub fn next_column(&mut self) {
|
||||
if self.headers.len() > 9 && self.column_index < self.headers.len() - 9 {
|
||||
self.column_index += 1
|
||||
}
|
||||
}
|
||||
|
||||
pub fn previous_column(&mut self) {
|
||||
if self.column_index > 0 {
|
||||
self.column_index -= 1
|
||||
}
|
||||
}
|
||||
|
||||
pub fn headers(&self) -> Vec<String> {
|
||||
let mut headers = self.headers[self.column_index..].to_vec();
|
||||
headers.insert(0, "".to_string());
|
||||
headers
|
||||
}
|
||||
|
||||
pub fn rows(&self) -> Vec<Vec<String>> {
|
||||
let rows = self
|
||||
.rows
|
||||
.iter()
|
||||
.map(|row| row[self.column_index..].to_vec())
|
||||
.collect::<Vec<Vec<String>>>();
|
||||
let mut new_rows = match self.state.selected() {
|
||||
Some(index) => {
|
||||
if index + 100 <= self.rows.len() {
|
||||
rows[..index + 100].to_vec()
|
||||
} else {
|
||||
rows
|
||||
}
|
||||
}
|
||||
None => rows,
|
||||
};
|
||||
for (index, row) in new_rows.iter_mut().enumerate() {
|
||||
row.insert(0, (index + 1).to_string())
|
||||
}
|
||||
new_rows
|
||||
}
|
||||
}
|
||||
|
||||
impl DrawableComponent for TableComponent {
|
||||
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, area: Rect, focused: bool) -> Result<()> {
|
||||
self.state.selected().map_or_else(
|
||||
|| {
|
||||
self.scroll.reset();
|
||||
},
|
||||
|selection| {
|
||||
self.scroll.update(
|
||||
selection,
|
||||
self.rows.len(),
|
||||
area.height.saturating_sub(2) as usize,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
let headers = self.headers();
|
||||
let header_cells = headers
|
||||
.iter()
|
||||
.map(|h| Cell::from(h.to_string()).style(Style::default()));
|
||||
let header = Row::new(header_cells).height(1).bottom_margin(1);
|
||||
let rows = self.rows();
|
||||
let rows = rows.iter().map(|item| {
|
||||
let height = item
|
||||
.iter()
|
||||
.map(|content| content.chars().filter(|c| *c == '\n').count())
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
+ 1;
|
||||
let cells = item
|
||||
.iter()
|
||||
.map(|c| Cell::from(c.to_string()).style(Style::default()));
|
||||
Row::new(cells).height(height as u16).bottom_margin(1)
|
||||
});
|
||||
let widths = (0..10)
|
||||
.map(|_| Constraint::Percentage(10))
|
||||
.collect::<Vec<Constraint>>();
|
||||
let t = WTable::new(rows)
|
||||
.header(header)
|
||||
.block(Block::default().borders(Borders::ALL).title("Records"))
|
||||
.highlight_style(Style::default().bg(Color::Blue))
|
||||
.style(if focused {
|
||||
Style::default()
|
||||
} else {
|
||||
Style::default().fg(Color::DarkGray)
|
||||
})
|
||||
.widths(&widths);
|
||||
f.render_stateful_widget(t, area, &mut self.state);
|
||||
|
||||
self.scroll.draw(f, area);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for TableComponent {
|
||||
fn event(&mut self, key: Key) -> Result<()> {
|
||||
match key {
|
||||
Key::Char('h') => self.previous_column(),
|
||||
Key::Char('j') => self.next(1),
|
||||
Key::Ctrl('d') => self.next(10),
|
||||
Key::Char('k') => self.previous(1),
|
||||
Key::Ctrl('u') => self.previous(10),
|
||||
Key::Char('g') => self.scroll_top(),
|
||||
Key::Shift('G') | Key::Shift('g') => self.scroll_bottom(),
|
||||
Key::Char('l') => self.next_column(),
|
||||
_ => (),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
use super::{Component, DrawableComponent};
|
||||
use crate::event::Key;
|
||||
use anyhow::Result;
|
||||
use database_tree::Table;
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, Borders, List, ListItem},
|
||||
Frame,
|
||||
};
|
||||
|
||||
pub struct TableStatusComponent {
|
||||
pub rows_count: u64,
|
||||
pub table: Option<Table>,
|
||||
}
|
||||
|
||||
impl Default for TableStatusComponent {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
rows_count: 0,
|
||||
table: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TableStatusComponent {
|
||||
pub fn update(&mut self, count: u64, table: Table) {
|
||||
self.rows_count = count;
|
||||
self.table = Some(table);
|
||||
}
|
||||
|
||||
pub fn status_str(&self) -> Vec<String> {
|
||||
if let Some(table) = self.table.as_ref() {
|
||||
return vec![
|
||||
format!(
|
||||
"created: {}",
|
||||
table
|
||||
.create_time
|
||||
.map(|time| time.to_string())
|
||||
.unwrap_or_default()
|
||||
),
|
||||
format!(
|
||||
"updated: {}",
|
||||
table
|
||||
.update_time
|
||||
.map(|time| time.to_string())
|
||||
.unwrap_or_default()
|
||||
),
|
||||
format!(
|
||||
"engine: {}",
|
||||
table
|
||||
.engine
|
||||
.as_ref()
|
||||
.map(|engine| engine.to_string())
|
||||
.unwrap_or_default()
|
||||
),
|
||||
format!("rows: {}", self.rows_count),
|
||||
];
|
||||
}
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl DrawableComponent for TableStatusComponent {
|
||||
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, area: Rect, focused: bool) -> Result<()> {
|
||||
let table_status: Vec<ListItem> = self
|
||||
.status_str()
|
||||
.iter()
|
||||
.map(|i| {
|
||||
ListItem::new(vec![Spans::from(Span::raw(i.to_string()))]).style(Style::default())
|
||||
})
|
||||
.collect();
|
||||
let tasks = List::new(table_status).block(Block::default().borders(Borders::ALL).style(
|
||||
if focused {
|
||||
Style::default()
|
||||
} else {
|
||||
Style::default().fg(Color::DarkGray)
|
||||
},
|
||||
));
|
||||
f.render_widget(tasks, area);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for TableStatusComponent {
|
||||
fn event(&mut self, _key: Key) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,15 +1,12 @@
|
||||
use crate::app::{App, FocusBlock};
|
||||
use crate::components::Component as _;
|
||||
use crate::event::Key;
|
||||
|
||||
pub async fn handler(key: Key, app: &mut App) -> anyhow::Result<()> {
|
||||
match key {
|
||||
Key::Char('h') => app.record_table.previous_column(),
|
||||
Key::Char('j') => app.record_table.next(),
|
||||
Key::Char('k') => app.record_table.previous(),
|
||||
Key::Char('l') => app.record_table.next_column(),
|
||||
Key::Left => app.focus_block = FocusBlock::DabataseList,
|
||||
Key::Char('c') => app.focus_block = FocusBlock::ConnectionList,
|
||||
_ => (),
|
||||
key => app.record_table.event(key)?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -0,0 +1,12 @@
|
||||
use crate::app::{App, FocusBlock};
|
||||
use crate::components::Component as _;
|
||||
use crate::event::Key;
|
||||
|
||||
pub async fn handler(key: Key, app: &mut App) -> anyhow::Result<()> {
|
||||
match key {
|
||||
Key::Left => app.focus_block = FocusBlock::DabataseList,
|
||||
Key::Char('c') => app.focus_block = FocusBlock::ConnectionList,
|
||||
key => app.structure_table.event(key)?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
#[macro_export]
|
||||
macro_rules! outln {
|
||||
($($expr:expr),+) => {{
|
||||
use std::io::{Write};
|
||||
use std::fs::OpenOptions;
|
||||
let mut file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open("gobang.log")
|
||||
.unwrap();
|
||||
writeln!(file, $($expr),+).expect("Can't write output");
|
||||
}}
|
||||
}
|
Loading…
Reference in New Issue