feat: Added history tab, and event passing
This commit is contained in:
parent
6ef492ee10
commit
7d1613b0c8
7 changed files with 327 additions and 138 deletions
135
src/app.rs
135
src/app.rs
|
@ -1,53 +1,134 @@
|
|||
|
||||
use crossterm::event::{Event, self, KeyCode};
|
||||
use ratatui::widgets::ListState;
|
||||
|
||||
use crate::uis::history::HistoryState;
|
||||
use crate::uis::new_transaction::NewTransactionTabState;
|
||||
use crate::uis::navigation_frame::NavigationState;
|
||||
|
||||
pub type AppResult<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
pub enum FocusedBlock {
|
||||
Navigation,
|
||||
Body,
|
||||
pub struct StatefulList<T> {
|
||||
pub state: ListState,
|
||||
pub items: Vec<T>,
|
||||
}
|
||||
|
||||
impl<T> StatefulList<T> {
|
||||
pub fn with_items(items: Vec<T>) -> StatefulList<T> {
|
||||
StatefulList {
|
||||
state: ListState::default(),
|
||||
items,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.items.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
pub fn previous(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
self.items.len() - 1
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
pub fn unselect(&mut self) {
|
||||
self.state.select(None);
|
||||
}
|
||||
}
|
||||
|
||||
pub enum InputMode {
|
||||
Insert,
|
||||
Normal,
|
||||
}
|
||||
|
||||
pub struct App<'a> {
|
||||
pub running: bool,
|
||||
pub tabs: Vec<&'a str>,
|
||||
pub tab_index: usize,
|
||||
pub focus: FocusedBlock,
|
||||
|
||||
pub new_transaction_tab_state: NewTransactionTabState<'a>,
|
||||
pub states: States<'a>,
|
||||
|
||||
pub input_mode: InputMode,
|
||||
}
|
||||
|
||||
impl<'a> App<'a> {
|
||||
pub fn new() -> App<'a> {
|
||||
App {
|
||||
running: true,
|
||||
|
||||
tabs: vec!["History", "Tab2", "Tab3"],
|
||||
tab_index: 0,
|
||||
focus: FocusedBlock::Navigation,
|
||||
|
||||
new_transaction_tab_state: NewTransactionTabState::new(),
|
||||
states: States::new(),
|
||||
input_mode: InputMode::Normal,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_tab(&mut self) {
|
||||
self.tab_index = (self.tab_index + 1) % self.tabs.len();
|
||||
}
|
||||
|
||||
pub fn prev_tab(&mut self) {
|
||||
if self.tab_index > 0 {
|
||||
self.tab_index -= 1;
|
||||
pub fn poll_events(&mut self) -> AppResult<()> {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
// 'q' needs to be handled at the top level so it can't be
|
||||
// accidentally handed to a dead end
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
if let Some(_) = self.states.nav_state.message.clone() {
|
||||
self.states.nav_state.message = None;
|
||||
} else {
|
||||
self.tab_index = self.tabs.len() - 1;
|
||||
if let ActiveFrame::Navigation = self.states.active_frame {
|
||||
self.running = false;
|
||||
} else {
|
||||
self.states.active_frame = ActiveFrame::Navigation;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn focus_navigation(&mut self) {
|
||||
self.focus = FocusedBlock::Navigation;
|
||||
}
|
||||
|
||||
pub fn focus_body(&mut self) {
|
||||
self.focus = FocusedBlock::Body;
|
||||
match self.states.active_frame {
|
||||
ActiveFrame::Navigation => {
|
||||
NavigationState::handle_event(key, self);
|
||||
}
|
||||
ActiveFrame::History => {
|
||||
HistoryState::handle_event(key, self);
|
||||
}
|
||||
ActiveFrame::NewTransaction => {
|
||||
NewTransactionTabState::handle_event(key, self);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ActiveFrame {
|
||||
Navigation,
|
||||
NewTransaction,
|
||||
History,
|
||||
}
|
||||
|
||||
pub struct States<'a> {
|
||||
pub nav_state: NavigationState<'a>,
|
||||
pub transactions: NewTransactionTabState<'a>,
|
||||
pub history: HistoryState,
|
||||
|
||||
pub active_frame: ActiveFrame,
|
||||
}
|
||||
|
||||
impl<'a> States<'a> {
|
||||
pub fn new() -> States<'a> {
|
||||
States {
|
||||
nav_state: NavigationState::new(),
|
||||
transactions: NewTransactionTabState::new(),
|
||||
history: HistoryState::new(),
|
||||
|
||||
active_frame: ActiveFrame::Navigation,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
32
src/main.rs
32
src/main.rs
|
@ -7,9 +7,8 @@ use ratatui::{
|
|||
Terminal,
|
||||
};
|
||||
|
||||
use recount::app::{App, AppResult, FocusedBlock};
|
||||
use recount::app::{App, AppResult};
|
||||
use recount::tui::Tui;
|
||||
use recount::uis::new_transaction::NewTransactionTabState;
|
||||
|
||||
fn main() -> AppResult<()> {
|
||||
// Create an application.
|
||||
|
@ -28,34 +27,7 @@ fn main() -> AppResult<()> {
|
|||
tui.draw(&mut app)?;
|
||||
|
||||
// Handle events.
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match app.focus {
|
||||
FocusedBlock::Body => {
|
||||
match app.tab_index {
|
||||
2 => {
|
||||
NewTransactionTabState::handle_event(key, &mut app);
|
||||
},
|
||||
_ => {
|
||||
// Work around "You entered an empty body, and now, I am dead" loops
|
||||
app.focus_navigation();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
FocusedBlock::Navigation => {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => break,
|
||||
KeyCode::Tab => app.next_tab(),
|
||||
KeyCode::Enter => app.focus_body(),
|
||||
// KeyCode::Right => app.next(),
|
||||
// KeyCode::Left => app.previous(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
app.poll_events()?;
|
||||
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
|
|
85
src/ui.rs
85
src/ui.rs
|
@ -1,79 +1,52 @@
|
|||
use crate::app::App;
|
||||
use crate::{app::{App, ActiveFrame}, uis::render_history_tab};
|
||||
use ratatui::{
|
||||
backend::Backend,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Text},
|
||||
widgets::{Block, Borders, Tabs, Paragraph},
|
||||
Frame,
|
||||
layout::{Constraint, Direction, Layout},
|
||||
widgets::{Block, Borders},
|
||||
Frame, style::Style,
|
||||
};
|
||||
|
||||
use crate::uis::{render_new_transaction_tab, render_history_tab};
|
||||
use crate::uis::{render_navigation_frame, render_new_transaction_tab};
|
||||
|
||||
|
||||
pub fn render<B: Backend> (f: &mut Frame<B>, app: &App) {
|
||||
pub fn render<B: Backend> (f: &mut Frame<B>, app: &mut App) {
|
||||
|
||||
let size = f.size();
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
|
||||
.constraints([Constraint::Length(3), Constraint::Min(0), Constraint::Length(3)].as_ref())
|
||||
.split(size);
|
||||
|
||||
let bottom_block = Block::default()
|
||||
.borders(Borders::ALL);
|
||||
.borders(Borders::ALL)
|
||||
.border_style({
|
||||
if let ActiveFrame::Navigation = app.states.active_frame {
|
||||
Style::default()
|
||||
} else {
|
||||
Style::default().fg(ratatui::style::Color::Green)
|
||||
}
|
||||
});
|
||||
f.render_widget(bottom_block, chunks[1]);
|
||||
|
||||
render_statusbar(f, chunks[0], app);
|
||||
render_navigation_frame(f, chunks[0], chunks[2], app);
|
||||
|
||||
let bottom_chunk = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(1)
|
||||
.constraints([Constraint::Percentage(100)])
|
||||
.split(chunks[1]);
|
||||
// if app.tabs selected == history,
|
||||
//render_history_tab(f, bottom_chunk[0], app);
|
||||
|
||||
if let Some(active_frame) = app.states.nav_state.get_active_tab_frametype() {
|
||||
match active_frame {
|
||||
ActiveFrame::History => {
|
||||
render_history_tab(f, bottom_chunk[0], app);
|
||||
}
|
||||
ActiveFrame::NewTransaction => {
|
||||
render_new_transaction_tab(f, bottom_chunk[0], app);
|
||||
}
|
||||
|
||||
pub fn render_statusbar<B: Backend> (f: &mut Frame<B>, status_rect: Rect, app: &App) {
|
||||
|
||||
let status_bar_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(75), Constraint::Min(10)])
|
||||
.split(status_rect);
|
||||
|
||||
let left_block = Block::default()
|
||||
.borders(Borders::ALL);
|
||||
let right_block = Block::default()
|
||||
.borders(Borders::ALL);
|
||||
|
||||
|
||||
let titles = app
|
||||
.tabs
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(Line::from)
|
||||
.collect();
|
||||
|
||||
let tabs = Tabs::new(titles)
|
||||
//.block(Block::default().borders(Borders::NONE))
|
||||
.block(left_block)
|
||||
.select(app.tab_index)
|
||||
//.style(Style::default().fg(Color::Cyan))
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.fg(Color::White)
|
||||
.add_modifier(Modifier::BOLD | Modifier::SLOW_BLINK)
|
||||
.bg(Color::Blue),
|
||||
);
|
||||
|
||||
f.render_widget(tabs, status_bar_chunks[0]);
|
||||
|
||||
let connection_paragraph = Paragraph::new(
|
||||
Text::styled("Aurora",
|
||||
Style::default().fg(Color::Green)
|
||||
)
|
||||
).block(right_block);
|
||||
|
||||
f.render_widget(connection_paragraph, status_bar_chunks[1]);
|
||||
}
|
||||
_ => {
|
||||
panic!("ui.rs Render encountered a thought impossible state")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,56 @@
|
|||
use ratatui::{backend::Backend, Frame, layout::Rect, widgets::Paragraph, text::Text, style::{Style, Color}};
|
||||
use crate::app::App;
|
||||
use crossterm::event::{KeyEvent, KeyEventKind, KeyCode};
|
||||
use ratatui::{backend::Backend, Frame, layout::Rect, widgets::{Block, Borders, List, ListItem}, text::Text, style::{Style, Color, Modifier}};
|
||||
use crate::app::{App, StatefulList};
|
||||
|
||||
|
||||
pub fn render_history_tab<B: Backend> (f: &mut Frame<B>, status_rect: Rect, app: &App) {
|
||||
let connection_paragraph = Paragraph::new(
|
||||
Text::styled("I'm a ghost!",
|
||||
Style::default().fg(Color::Green)
|
||||
)
|
||||
);
|
||||
f.render_widget(connection_paragraph, status_rect);
|
||||
pub struct HistoryState {
|
||||
pub transacts_list: StatefulList<String>
|
||||
}
|
||||
|
||||
impl HistoryState {
|
||||
pub fn new() -> HistoryState {
|
||||
HistoryState {
|
||||
transacts_list: StatefulList::with_items(vec![
|
||||
"Item0".to_string(),
|
||||
"Item1".to_string(),
|
||||
"Item2".to_string(),
|
||||
"Item3".to_string(),
|
||||
"Item4".to_string(),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_event(event: KeyEvent, app: &mut App) {
|
||||
|
||||
if event.kind == KeyEventKind::Press {
|
||||
match event.code {
|
||||
KeyCode::Tab => app.states.transactions.next_tab(),
|
||||
KeyCode::Up => app.states.history.transacts_list.previous(),
|
||||
KeyCode::Down => app.states.history.transacts_list.next(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_history_tab<B: Backend> (f: &mut Frame<B>, body_rect: Rect, app: &mut App) {
|
||||
// Iterate through all elements in the `items` app and append some debug text to it.
|
||||
let items: Vec<ListItem> = app
|
||||
.states.history.transacts_list
|
||||
.items
|
||||
.iter()
|
||||
.map(|i| {
|
||||
ListItem::new(Text::from(i.as_str())).style(Style::default().fg(Color::Black).bg(Color::White))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let history_items = List::new(items)
|
||||
.block(Block::default().borders(Borders::NONE))
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.bg(Color::Gray)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
);
|
||||
|
||||
f.render_stateful_widget(history_items, body_rect, &mut app.states.history.transacts_list.state)
|
||||
}
|
||||
|
|
|
@ -5,3 +5,6 @@ pub use self::history::render_history_tab;
|
|||
|
||||
pub mod new_transaction;
|
||||
pub use self::new_transaction::render_new_transaction_tab;
|
||||
|
||||
pub mod navigation_frame;
|
||||
pub use self::navigation_frame::render_navigation_frame;
|
||||
|
|
120
src/uis/navigation_frame.rs
Normal file
120
src/uis/navigation_frame.rs
Normal file
|
@ -0,0 +1,120 @@
|
|||
use crossterm::event::{KeyEvent, KeyEventKind, KeyCode};
|
||||
use ratatui::{widgets::{Borders, Paragraph, Block, Tabs}, backend::Backend, Frame, style::{Style, Color, Modifier}, text::{Text, Line}, layout::{Rect, Layout, Direction, Constraint}};
|
||||
|
||||
use crate::app::{App, ActiveFrame};
|
||||
|
||||
pub struct NavigationState<'a> {
|
||||
pub tabs: Vec<&'a str>,
|
||||
pub tab_index: usize,
|
||||
pub cur_tab_index: usize,
|
||||
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
impl<'a> NavigationState<'a> {
|
||||
pub fn new() -> NavigationState<'a> {
|
||||
NavigationState {
|
||||
tabs: vec!["History", "New Transaction"],
|
||||
tab_index: 0,
|
||||
cur_tab_index: 0,
|
||||
message: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_tab(&mut self) {
|
||||
self.tab_index = (self.tab_index + 1) % self.tabs.len();
|
||||
}
|
||||
|
||||
pub fn prev_tab(&mut self) {
|
||||
if self.tab_index > 0 {
|
||||
self.tab_index -= 1;
|
||||
} else {
|
||||
self.tab_index = self.tabs.len() - 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_active_tab_frametype(&self) -> Option<ActiveFrame> {
|
||||
match self.tab_index {
|
||||
0 => Some(ActiveFrame::History),
|
||||
1 => Some(ActiveFrame::NewTransaction),
|
||||
_ => todo!()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_event(event: KeyEvent, app: &mut App) {
|
||||
if event.kind == KeyEventKind::Press {
|
||||
match event.code {
|
||||
KeyCode::Tab => app.states.nav_state.next_tab(),
|
||||
KeyCode::Enter => app.states.active_frame = app.states.nav_state.get_active_tab_frametype().unwrap(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_navigation_frame<B: Backend> (f: &mut Frame<B>, status_rect: Rect, navbar_rect: Rect, app: &App) {
|
||||
|
||||
let status_bar_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(75), Constraint::Min(10)])
|
||||
.split(status_rect);
|
||||
|
||||
let left_block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style({
|
||||
if let ActiveFrame::Navigation = app.states.active_frame {
|
||||
Style::default().fg(ratatui::style::Color::Green)
|
||||
} else {
|
||||
Style::default()
|
||||
}
|
||||
});
|
||||
let right_block = Block::default()
|
||||
.borders(Borders::ALL);
|
||||
|
||||
|
||||
let titles = app.states.nav_state
|
||||
.tabs
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(Line::from)
|
||||
.collect();
|
||||
|
||||
let tabs = Tabs::new(titles)
|
||||
//.block(Block::default().borders(Borders::NONE))
|
||||
.block(left_block)
|
||||
.select(app.states.nav_state.tab_index)
|
||||
//.style(Style::default().fg(Color::Cyan))
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.fg(Color::White)
|
||||
.add_modifier(Modifier::BOLD | Modifier::SLOW_BLINK)
|
||||
.bg(Color::Blue),
|
||||
);
|
||||
|
||||
f.render_widget(tabs, status_bar_chunks[0]);
|
||||
|
||||
let connection_paragraph = Paragraph::new(
|
||||
Text::styled("Aurora",
|
||||
Style::default().fg(Color::Green)
|
||||
)
|
||||
).block(right_block);
|
||||
|
||||
f.render_widget(connection_paragraph, status_bar_chunks[1]);
|
||||
|
||||
// Navbar section
|
||||
let navbar = {
|
||||
if let None = app.states.nav_state.message {
|
||||
match app.states.active_frame {
|
||||
ActiveFrame::Navigation => "Navigating: `q` to exit".to_string(),
|
||||
_ => "Editing Body: 'q' to go back to navigating".to_string(),
|
||||
}
|
||||
} else {
|
||||
app.states.nav_state.message.clone().unwrap()
|
||||
}
|
||||
};
|
||||
let bottom_navbar = Paragraph::new(
|
||||
Text::styled(navbar, Style::default().fg(Color::White))
|
||||
)
|
||||
.block(Block::default().borders(Borders::ALL));
|
||||
f.render_widget(bottom_navbar, navbar_rect);
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
use crate::app::App;
|
||||
use crossterm::event::{Event, KeyEvent, KeyEventKind, KeyCode};
|
||||
use crossterm::event::{KeyEvent, KeyEventKind, KeyCode};
|
||||
use ratatui::{backend::Backend, Frame, layout::{Rect, Layout, Direction, Constraint}, widgets::{Paragraph, Borders, Block}, text::Text, style::{Style, Color}};
|
||||
|
||||
pub struct NewTransactionTabState<'a> {
|
||||
|
@ -10,7 +10,7 @@ pub struct NewTransactionTabState<'a> {
|
|||
impl<'a> NewTransactionTabState<'a> {
|
||||
pub fn new() -> NewTransactionTabState<'a> {
|
||||
NewTransactionTabState {
|
||||
cur_tab_index: 1,
|
||||
cur_tab_index: 0,
|
||||
tabs: vec!["Quick Entry", "Manual Entry"]
|
||||
}
|
||||
}
|
||||
|
@ -22,14 +22,10 @@ impl<'a> NewTransactionTabState<'a> {
|
|||
pub fn handle_event(event: KeyEvent, app: &mut App) {
|
||||
if event.kind == KeyEventKind::Press {
|
||||
match event.code {
|
||||
KeyCode::Char('q') => app.focus_navigation(),
|
||||
KeyCode::Tab => app.new_transaction_tab_state.next_tab(),
|
||||
// KeyCode::Right => app.next(),
|
||||
// KeyCode::Left => app.previous(),
|
||||
KeyCode::Tab => app.states.transactions.next_tab(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -42,8 +38,8 @@ pub fn render_new_transaction_tab<B: Backend> (f: &mut Frame<B>, body_rect: Rect
|
|||
|
||||
// Render the custom tab bar
|
||||
let mut constraints: Vec<Constraint> = vec![];
|
||||
let tab_percent: u16 = (100 / app.new_transaction_tab_state.tabs.len()) as u16;
|
||||
for _ in 0..app.new_transaction_tab_state.tabs.len() {
|
||||
let tab_percent: u16 = (100 / app.states.transactions.tabs.len()) as u16;
|
||||
for _ in 0..app.states.transactions.tabs.len() {
|
||||
constraints.push(Constraint::Percentage(tab_percent));
|
||||
}
|
||||
|
||||
|
@ -52,10 +48,10 @@ pub fn render_new_transaction_tab<B: Backend> (f: &mut Frame<B>, body_rect: Rect
|
|||
.constraints(constraints)
|
||||
.split(chunks[0]);
|
||||
|
||||
for i in 0..app.new_transaction_tab_state.tabs.len() {
|
||||
for i in 0..app.states.transactions.tabs.len() {
|
||||
|
||||
let tab = Paragraph::new(
|
||||
Text::styled(app.new_transaction_tab_state.tabs[i],
|
||||
Text::styled(app.states.transactions.tabs[i],
|
||||
Style::default().fg(Color::White)
|
||||
)
|
||||
)
|
||||
|
@ -64,7 +60,7 @@ pub fn render_new_transaction_tab<B: Backend> (f: &mut Frame<B>, body_rect: Rect
|
|||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.style({
|
||||
if app.new_transaction_tab_state.cur_tab_index == i {
|
||||
if app.states.transactions.cur_tab_index == i {
|
||||
Style::default().bg(Color::Blue)
|
||||
} else {
|
||||
Style::default()
|
||||
|
|
Loading…
Reference in a new issue