From 857f0771f029f87465ed5e8b297bb9995dc990d1 Mon Sep 17 00:00:00 2001 From: Nickiel12 Date: Wed, 31 May 2023 18:00:17 -0700 Subject: [PATCH] basic example with tabs and a header --- Cargo.toml | 3 +- src/app.rs | 67 ++++++++----------------------------- src/event.rs | 77 ------------------------------------------ src/handler.rs | 28 ---------------- src/lib.rs | 12 ++----- src/main.rs | 38 +++++++++++++-------- src/tui.rs | 13 +++----- src/ui.rs | 90 ++++++++++++++++++++++++++++++++++---------------- 8 files changed, 108 insertions(+), 220 deletions(-) delete mode 100644 src/event.rs delete mode 100644 src/handler.rs diff --git a/Cargo.toml b/Cargo.toml index a07e62f..bdf8070 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,4 +7,5 @@ edition = "2021" [dependencies] crossterm = "0.26.1" -ratatui = "0.20.1" +ratatui = "0.21.0" +rusqlite = "0.29.0" diff --git a/src/app.rs b/src/app.rs index 2ec8371..bdd8058 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,69 +1,30 @@ -use std::error; -/// Application result type. -pub type AppResult = std::result::Result>; +pub type AppResult = std::result::Result>; -/// Application. -#[derive(Debug)] pub struct App<'a> { - /// Is the application running? pub running: bool, - /// counter - pub counter: u8, - - pub tab_titles: Vec<&'a str>, - pub current_tab: usize, -} - -impl<'a> Default for App<'a> { - fn default() -> Self { - Self { - running: true, - counter: 0, - tab_titles: vec!["History", "New Entry", "Accounts"], - current_tab: 0, - } - } + pub tabs: Vec<&'a str>, + pub tab_index: usize, } impl<'a> App<'a> { - /// Constructs a new instance of [`App`]. - pub fn new() -> Self { - Self::default() - } + pub fn new() -> App<'a> { + App { + running: true, - /// Handles the tick event of the terminal. - pub fn tick(&self) {} - - /// Set running to false to quit the application. - pub fn quit(&mut self) { - self.running = false; + tabs: vec!["Tab1", "Tab2", "Tab3"], + tab_index: 0, + } } pub fn next_tab(&mut self) { - self.current_tab += 1; - if self.current_tab >= self.tab_titles.len() { - self.current_tab = 0; - } + self.tab_index = (self.tab_index + 1) % self.tabs.len(); } pub fn prev_tab(&mut self) { - if self.current_tab == 0 { - self.current_tab = self.tab_titles.len() - 1; + if self.tab_index > 0 { + self.tab_index -= 1; } else { - self.current_tab -= 1; + self.tab_index = self.tabs.len() - 1; } - } - - pub fn increment_counter(&mut self) { - if let Some(res) = self.counter.checked_add(1) { - self.counter = res; - } - } - - pub fn decrement_counter(&mut self) { - if let Some(res) = self.counter.checked_sub(1) { - self.counter = res; - } - } -} + }} diff --git a/src/event.rs b/src/event.rs deleted file mode 100644 index 99f5787..0000000 --- a/src/event.rs +++ /dev/null @@ -1,77 +0,0 @@ -use crate::app::AppResult; -use crossterm::event::{self, Event as CrosstermEvent, KeyEvent, MouseEvent}; -use std::sync::mpsc; -use std::thread; -use std::time::{Duration, Instant}; - -/// Terminal events. -#[derive(Clone, Copy, Debug)] -pub enum Event { - /// Terminal tick. - Tick, - /// Key press. - Key(KeyEvent), - /// Mouse click/scroll. - Mouse(MouseEvent), - /// Terminal resize. - Resize(u16, u16), -} - -/// Terminal event handler. -#[allow(dead_code)] -#[derive(Debug)] -pub struct EventHandler { - /// Event sender channel. - sender: mpsc::Sender, - /// Event receiver channel. - receiver: mpsc::Receiver, - /// Event handler thread. - handler: thread::JoinHandle<()>, -} - -impl EventHandler { - /// Constructs a new instance of [`EventHandler`]. - pub fn new(tick_rate: u64) -> Self { - let tick_rate = Duration::from_millis(tick_rate); - let (sender, receiver) = mpsc::channel(); - let handler = { - let sender = sender.clone(); - thread::spawn(move || { - let mut last_tick = Instant::now(); - loop { - let timeout = tick_rate - .checked_sub(last_tick.elapsed()) - .unwrap_or(tick_rate); - - if event::poll(timeout).expect("no events available") { - match event::read().expect("unable to read event") { - CrosstermEvent::Key(e) => sender.send(Event::Key(e)), - CrosstermEvent::Mouse(e) => sender.send(Event::Mouse(e)), - CrosstermEvent::Resize(w, h) => sender.send(Event::Resize(w, h)), - _ => unimplemented!(), - } - .expect("failed to send terminal event") - } - - if last_tick.elapsed() >= tick_rate { - sender.send(Event::Tick).expect("failed to send tick event"); - last_tick = Instant::now(); - } - } - }) - }; - Self { - sender, - receiver, - handler, - } - } - - /// Receive the next event from the handler thread. - /// - /// This function will always block the current thread if - /// there is no data available and it's possible for more data to be sent. - pub fn next(&self) -> AppResult { - Ok(self.receiver.recv()?) - } -} diff --git a/src/handler.rs b/src/handler.rs deleted file mode 100644 index 3cea937..0000000 --- a/src/handler.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::app::{App, AppResult}; -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; - -/// Handles the key events and updates the state of [`App`]. -pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { - match key_event.code { - // Exit application on `ESC` or `q` - KeyCode::Esc | KeyCode::Char('q') => { - app.quit(); - } - // Exit application on `Ctrl-C` - KeyCode::Char('c') | KeyCode::Char('C') => { - if key_event.modifiers == KeyModifiers::CONTROL { - app.quit(); - } - } - // Counter handlers - KeyCode::Right => { - app.increment_counter(); - } - KeyCode::Left => { - app.decrement_counter(); - } - // Other handlers you could add here. - _ => {} - } - Ok(()) -} diff --git a/src/lib.rs b/src/lib.rs index 85ad8bf..717054c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,6 @@ -/// Application. + pub mod app; -/// Terminal events handler. -pub mod event; - -/// Widget renderer. -pub mod ui; - -/// Terminal user interface. pub mod tui; -/// Event handler. -pub mod handler; +pub mod ui; diff --git a/src/main.rs b/src/main.rs index 5934ce0..0f624ef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,13 @@ use std::io; -use ratatui::backend::CrosstermBackend; -use ratatui::Terminal; +use std::time::Duration; +use crossterm::event::{self, Event, KeyCode, KeyEventKind}; + +use ratatui::{ + backend::CrosstermBackend, + Terminal, +}; + use recount::app::{App, AppResult}; -use recount::event::{Event, EventHandler}; -use recount::handler::handle_key_events; use recount::tui::Tui; fn main() -> AppResult<()> { @@ -13,24 +17,30 @@ fn main() -> AppResult<()> { // Initialize the terminal user interface. let backend = CrosstermBackend::new(io::stderr()); let terminal = Terminal::new(backend)?; - let events = EventHandler::new(250); - let mut tui = Tui::new(terminal, events); + let mut tui = Tui::new(terminal); + tui.init()?; // Start the main loop. while app.running { // Render the user interface. tui.draw(&mut app)?; + // Handle events. - match tui.events.next()? { - Event::Tick => app.tick(), - Event::Key(key_event) => handle_key_events(key_event, &mut app)?, - Event::Mouse(_) => {} - Event::Resize(_, _) => {} + if let Event::Key(key) = event::read()? { + if key.kind == KeyEventKind::Press { + match key.code { + KeyCode::Char('q') => break, + KeyCode::Tab => app.next_tab(), + // KeyCode::Right => app.next(), + // KeyCode::Left => app.previous(), + _ => {} + } + } } + std::thread::sleep(Duration::from_millis(20)); } - // Exit the user interface. - tui.exit()?; - Ok(()) + // restore terminal + tui.exit() } diff --git a/src/tui.rs b/src/tui.rs index 18f28f9..e34a81c 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -1,6 +1,5 @@ -use crate::app::{App, AppResult}; -use crate::event::EventHandler; use crate::ui; +use crate::app::{App, AppResult}; use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}; use std::io; @@ -15,20 +14,18 @@ use ratatui::Terminal; pub struct Tui { /// Interface to the Terminal. terminal: Terminal, - /// Terminal event handler. - pub events: EventHandler, } impl Tui { /// Constructs a new instance of [`Tui`]. - pub fn new(terminal: Terminal, events: EventHandler) -> Self { - Self { terminal, events } + pub fn new(terminal: Terminal) -> Self { + Self { terminal } } /// Initializes the terminal interface. /// /// It enables the raw mode and sets terminal properties. - pub fn init(&mut self) -> AppResult<()> { + pub fn init(&mut self) -> Result<(), Box> { terminal::enable_raw_mode()?; crossterm::execute!(io::stderr(), EnterAlternateScreen, EnableMouseCapture)?; self.terminal.hide_cursor()?; @@ -41,7 +38,7 @@ impl Tui { /// [`Draw`]: tui::Terminal::draw /// [`rendering`]: crate::ui:render pub fn draw(&mut self, app: &mut App) -> AppResult<()> { - self.terminal.draw(|frame| ui::render(app, frame))?; + self.terminal.draw(|frame| ui::render(frame, app))?; Ok(()) } diff --git a/src/ui.rs b/src/ui.rs index e717470..7d2830f 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,36 +1,68 @@ +use std::rc::Rc; + +use crate::app::{AppResult, App}; use ratatui::{ backend::Backend, - layout::Alignment, - style::{Color, Style}, - widgets::{Block, BorderType, Borders, Paragraph}, + layout::{Constraint, Direction, Layout, Rect}, + style::{Color, Modifier, Style}, + text::{Line, Text}, + widgets::{Block, Borders, Tabs, Paragraph}, Frame, }; -use crate::app::App; -/// Renders the user interface widgets. -pub fn render(app: &mut App, frame: &mut Frame<'_, B>) { - // This is where you add new widgets. - // See the following resources: - // - https://docs.rs/ratatui/latest/ratatui/widgets/index.html - // - https://github.com/tui-rs-revival/ratatui/tree/master/examples - frame.render_widget( - Paragraph::new(format!( - "This is a tui template.\n\ - Press `Esc`, `Ctrl-C` or `q` to stop running.\n\ - Press left and right to increment and decrement the counter respectively.\n\ - Counter: {}", - app.counter - )) - .block( - Block::default() - .title("Template") - .title_alignment(Alignment::Center) - .borders(Borders::ALL) - .border_type(BorderType::Rounded), - ) - .style(Style::default().fg(Color::Cyan).bg(Color::Black)) - .alignment(Alignment::Center), - frame.size(), - ) +pub fn render (f: &mut Frame, app: &App) { + + let size = f.size(); + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(3), Constraint::Min(0)].as_ref()) + .split(size); + + let bottom_block = Block::default(); + f.render_widget(bottom_block, chunks[1]); + + render_statusbar(f, chunks[0], app); +} + +pub fn render_statusbar (f: &mut Frame, status_rect: Rect, app: &App) { + + let status_bar_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Min(10)]) + .margin(1) + .split(status_rect); + + let top_block = Block::default() + .borders(Borders::ALL); + + f.render_widget(top_block, status_rect); + + let titles = app + .tabs + .iter() + .cloned() + .map(Line::from) + .collect(); + + let tabs = Tabs::new(titles) + .block(Block::default().borders(Borders::NONE)) + .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) + ) + ); + + f.render_widget(connection_paragraph, status_bar_chunks[1]); }