diff --git a/src/app.rs b/src/app.rs index 189c3da..adc87b2 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,60 +1,42 @@ -use std::collections::HashMap; +use std::error; -use serde_json::Result; +pub type AppResult = std::result::Result>; -pub enum CurrentScreen { - Main, - Editing, - Exiting, -} - -pub enum CurrentlyEditing { - Key, - Value, +#[derive(Debug)] +pub struct App { + pub running: bool, + pub counter: u8, } -pub struct App { - pub key_input: String, - pub value_input: String, - pub pairs: HashMap, - pub current_screen: CurrentScreen, - pub currently_editing: Option, +impl Default for App { + fn default() -> Self { + Self { + running: true, + counter: 0, + } + } } impl App { - pub fn new() -> App { - App { - key_input: String::new(), - value_input: String::new(), - pairs: HashMap::new(), - current_screen: CurrentScreen::Main, - currently_editing: None, - } + pub fn new() -> Self { + Self::default() } - pub fn save_key_value(&mut self) { - self.pairs - .insert(self.key_input.clone(), self.value_input.clone()); + pub fn tick(&self) {} - self.key_input = String::new(); - self.value_input = String::new(); - self.currently_editing = None; + pub fn quit(&mut self) { + self.running = false; } - pub fn toggle_edititng(&mut self) { - if let Some(edit_mode) = &self.currently_editing { - match edit_mode { - CurrentlyEditing::Key => self.currently_editing = Some(CurrentlyEditing::Value), - CurrentlyEditing::Value => self.currently_editing = Some(CurrentlyEditing::Key), - }; - } else { - self.currently_editing = Some(CurrentlyEditing::Key); + pub fn increment_counter(&mut self) { + if let Some(res) = self.counter.checked_add(1) { + self.counter = res; } } - pub fn print_json(&self) -> Result<()> { - let output = serde_json::to_string(&self.pairs)?; - println!("{}", output); - Ok(()) + pub fn decrement_counter(&mut self) { + if let Some(res) = self.counter.checked_sub(1) { + self.counter = res; + } } } diff --git a/src/errors.rs b/src/errors.rs deleted file mode 100644 index d840e43..0000000 --- a/src/errors.rs +++ /dev/null @@ -1,28 +0,0 @@ -use std::panic; - -use color_eyre::{ - config::HookBuilder, - eyre::{self, Ok}, -}; - -use crate::tui; - -pub fn install_hooks() -> color_eyre::Result<()> { - let (panic_hook, eyre_hook) = HookBuilder::default().into_hooks(); - - let panic_hook = panic_hook.into_panic_hook(); - panic::set_hook(Box::new(move |panic_info| { - tui::restore().unwrap(); - panic_hook(panic_info); - })); - - let eyre_hook = eyre_hook.into_eyre_hook(); - eyre::set_hook(Box::new( - move |error: &(dyn std::error::Error + 'static)| { - tui::restore().unwrap(); - eyre_hook(error) - }, - ))?; - - Ok(()) -} diff --git a/src/event.rs b/src/event.rs new file mode 100644 index 0000000..819595b --- /dev/null +++ b/src/event.rs @@ -0,0 +1,83 @@ +use std::time::Duration; + +use crossterm::event::{Event as CrosstermEvent, KeyEvent, MouseEvent}; +use futures::{FutureExt, StreamExt}; +use tokio::sync::mpsc; + +use crate::app::AppResult; + +#[derive(Clone, Copy, Debug)] +pub enum Event { + Tick, + Key(KeyEvent), + Mouse(MouseEvent), + Resize(u16, u16), +} + +#[allow(dead_code)] +#[derive(Debug)] +pub struct EventHandler { + sender: mpsc::UnboundedSender, + receiver: mpsc::UnboundedReceiver, + handler: tokio::task::JoinHandle<()>, +} + +impl EventHandler { + pub fn new(tick_rate: u64) -> Self { + let tick_rate = Duration::from_millis(tick_rate); + let (sender, receiver) = mpsc::unbounded_channel(); + let _sender = sender.clone(); + let handler = tokio::spawn(async move { + let mut reader = crossterm::event::EventStream::new(); + let mut tick = tokio::time::interval(tick_rate); + loop { + let tick_delay = tick.tick(); + let crossterm_event = reader.next().fuse(); + tokio::select! { + _ = _sender.closed() => { + break; + } + _ = tick_delay => { + _sender.send(Event::Tick).unwrap(); + } + Some(Ok(evt)) = crossterm_event => { + match evt { + CrosstermEvent::Key(key) => { + if key.kind == crossterm::event::KeyEventKind::Press { + _sender.send(Event::Key(key)).unwrap(); + } + }, + CrosstermEvent::Mouse(mouse) => { + _sender.send(Event::Mouse(mouse)).unwrap(); + }, + CrosstermEvent::Resize(x, y) => { + _sender.send(Event::Resize(x, y)).unwrap(); + }, + CrosstermEvent::FocusLost => { + }, + CrosstermEvent::FocusGained => { + }, + CrosstermEvent::Paste(_) => { + }, + } + } + }; + } + }); + Self { + sender, + receiver, + handler, + } + } + + pub async fn next(&mut self) -> AppResult { + self.receiver + .recv() + .await + .ok_or(Box::new(std::io::Error::new( + std::io::ErrorKind::Other, + "This is an IO error", + ))) + } +} diff --git a/src/handler.rs b/src/handler.rs new file mode 100644 index 0000000..d998f39 --- /dev/null +++ b/src/handler.rs @@ -0,0 +1,23 @@ +use crate::app::{App, AppResult}; +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; + +pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { + match key_event.code { + KeyCode::Esc | KeyCode::Char('q') => { + app.quit(); + } + KeyCode::Char('c') | KeyCode::Char('C') => { + if key_event.modifiers == KeyModifiers::CONTROL { + app.quit(); + } + } + KeyCode::Right => { + app.increment_counter(); + } + KeyCode::Left => { + app.decrement_counter(); + } + _ => {} + } + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..da86307 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,9 @@ +pub mod app; + +pub mod event; + +pub mod ui; + +pub mod tui; + +pub mod handler; diff --git a/src/main.rs b/src/main.rs index 802d506..eb07921 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,60 +1,30 @@ use ratatui::{backend::CrosstermBackend, Terminal}; -mod tui; +use rust_edit::app::{App, AppResult}; +use rust_edit::event::{Event, EventHandler}; +use rust_edit::handler::handle_key_events; +use rust_edit::tui::Tui; -pub enum Event { - Quit, - Error, - Tick, - Render, - Key(KeyEvent), -} - -fn update(app: &mut App, event: Event) -> Result<()> { - if let Event::Key(key) = event { - match key.code { - Char('j') => app.counter += 1, - Char('k') => app.counter -= 1, - Char('q') => app.should_quit = true, - _ => {} - } - } - Ok(()) -} - -async fn run() => Result<()> { - let mut events = tui::EventHandler::new(); - - let mut t = Terminal::new(CrosstermBackend::new(std::io::stderr()))?; - - let mut app = App { counter: 0, should_quit: false}; - - loop { - let event = events.next().await?; - - update(&mut app, event)?; - - t.draw(|f| { - ui(f, &app); - })?; - - if app.should_quit { - break; +#[tokio::main] +async fn main() -> AppResult<()> { + let mut app = App::new(); + + let backend = CrosstermBackend::new(std::io::stderr()); + let terminal = Terminal::new(backend)?; + let events = EventHandler::new(250); + let mut tui = Tui::new(terminal, events); + tui.init()?; + + while app.running { + tui.draw(&mut app)?; + match tui.events.next().await? { + Event::Tick => app.tick(), + Event::Key(key_event) => handle_key_events(key_event, &mut app)?, + Event::Mouse(_) => {} + Event::Resize(_, _) => {} } } - Ok(()) -} - -#[tokio::main] -async fn main() -> Result<()> { - startup()?; - - let result = run(); - - shutdown()?; - - result?; - + tui.exit()?; Ok(()) } diff --git a/src/tui.rs b/src/tui.rs new file mode 100644 index 0000000..1c6df72 --- /dev/null +++ b/src/tui.rs @@ -0,0 +1,53 @@ +use crate::app::{App, AppResult}; +use crate::event::EventHandler; +use crate::ui; +use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; +use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}; +use ratatui::backend::Backend; +use ratatui::Terminal; +use std::io; +use std::panic; + +#[derive(Debug)] +pub struct Tui { + terminal: Terminal, + pub events: EventHandler, +} + +impl Tui { + pub fn new(terminal: Terminal, events: EventHandler) -> Self { + Self { terminal, events } + } + + pub fn init(&mut self) -> AppResult<()> { + terminal::enable_raw_mode()?; + crossterm::execute!(io::stderr(), EnterAlternateScreen, EnableMouseCapture)?; + + let panic_hook = panic::take_hook(); + panic::set_hook(Box::new(move |panic| { + Self::reset().expect("failed to reset the terminal"); + panic_hook(panic); + })); + + self.terminal.hide_cursor()?; + self.terminal.clear()?; + Ok(()) + } + + pub fn draw(&mut self, app: &mut App) -> AppResult<()> { + self.terminal.draw(|frame| ui::render(app, frame))?; + Ok(()) + } + + fn reset() -> AppResult<()> { + terminal::disable_raw_mode()?; + crossterm::execute!(io::stderr(), LeaveAlternateScreen, DisableMouseCapture)?; + Ok(()) + } + + pub fn exit(&mut self) -> AppResult<()> { + Self::reset()?; + self.terminal.show_cursor()?; + Ok(()) + } +} diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 0000000..7cd25fd --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,29 @@ +use ratatui::{ + layout::Alignment, + style::{Color, Style}, + widgets::{Block, BorderType, Paragraph}, + Frame, +}; + +use crate::app::App; + +pub fn render(app: &mut App, frame: &mut Frame) { + 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::bordered() + .title("Template") + .title_alignment(Alignment::Center) + .border_type(BorderType::Rounded), + ) + .style(Style::default().fg(Color::Cyan).bg(Color::Black)) + .centered(), + frame.size(), + ) +}