From 83ff3fb6c40f136c00044b227dc92a7ac288ff0d Mon Sep 17 00:00:00 2001 From: RainBus Date: Mon, 23 Sep 2024 18:17:28 +0800 Subject: [PATCH] init --- .DS_Store | Bin 0 -> 6148 bytes Cargo.lock | 106 +++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 3 +- src/app.rs | 49 +++++++++++++++++++ src/component.rs | 6 --- src/component/input.rs | 16 ------- src/event.rs | 97 +++++++++++++++++++++++++++++++++++++ src/handler.rs | 28 +++++++++++ src/main.rs | 47 ++++++++++++++++-- src/tui.rs | 76 +++++++++++++++++++++++++++++ src/ui.rs | 34 +++++++++++++ 11 files changed, 435 insertions(+), 27 deletions(-) create mode 100644 .DS_Store create mode 100644 src/app.rs delete mode 100644 src/component.rs delete mode 100644 src/component/input.rs create mode 100644 src/event.rs create mode 100644 src/handler.rs create mode 100644 src/tui.rs create mode 100644 src/ui.rs diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..2ca3e0b4072cc0cfa63ca0145d3effd0aea4b24d GIT binary patch literal 6148 zcmeHKy-ve05I(mB6~a(9#N>?)wG(To!ob2nHzsI{0#c(?Q1 z0>pRrDUBPt0z!9^{WrxpJ0#b36}X(&k=QB`5Y?BfHI&A{5Auq*(~kIqt42JGN25U4Dk2CVlc*ng-7@4 zz~ov0U<+y|SaUDIF;*}ZEIh&jF+LRNLyfc;#)m_1#V-~tJo<1l(tH@nY@|amE<5(O z&Yeu`QD7^fI4>{oa! kL$GkA7`|MJw=kSQZ+QWX1q+YRK=hA*qd_NS;71ww0wB|1;Q#;t literal 0 HcmV?d00001 diff --git a/Cargo.lock b/Cargo.lock index 2db1516..045f5d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -111,6 +111,7 @@ checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ "bitflags", "crossterm_winapi", + "futures-core", "mio", "parking_lot", "rustix", @@ -153,6 +154,95 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "gimli" version = "0.31.0" @@ -336,6 +426,12 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "powerfmt" version = "0.2.0" @@ -478,6 +574,15 @@ dependencies = [ "libc", ] +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + [[package]] name = "smallvec" version = "1.13.2" @@ -538,6 +643,7 @@ name = "tethers" version = "0.1.0" dependencies = [ "crossterm", + "futures", "ratatui", "tokio", ] diff --git a/Cargo.toml b/Cargo.toml index 75bc6c2..7b049d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -crossterm = "0.28.1" +crossterm = { version = "0.28.1", features = ["event-stream"] } +futures = "0.3.30" ratatui = { version = "0.28.1", features = ["all-widgets"] } tokio = { version = "1.40.0", features = ["full"] } diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..e99d547 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,49 @@ +use std::error; + +/// Application result type. +pub type AppResult = std::result::Result>; + +/// Application. +#[derive(Debug)] +pub struct App { + /// Is the application running? + pub running: bool, + /// counter + pub counter: u8, +} + +impl Default for App { + fn default() -> Self { + Self { + running: true, + counter: 0, + } + } +} + +impl App { + /// Constructs a new instance of [`App`]. + pub fn new() -> Self { + Self::default() + } + + /// 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; + } + + 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/component.rs b/src/component.rs deleted file mode 100644 index 4950707..0000000 --- a/src/component.rs +++ /dev/null @@ -1,6 +0,0 @@ -use ratatui::widgets::Widget; - - -pub trait Component { - fn render(&self) -> Box; -} \ No newline at end of file diff --git a/src/component/input.rs b/src/component/input.rs deleted file mode 100644 index 8f03cc9..0000000 --- a/src/component/input.rs +++ /dev/null @@ -1,16 +0,0 @@ - - -pub struct Input { - value: String, -} - -impl Input { - pub fn new() -> Self { - Self { value: String::new() } - } - - pub fn value(&self) -> &str { - &self.value - } - -} \ No newline at end of file diff --git a/src/event.rs b/src/event.rs new file mode 100644 index 0000000..45ac4b4 --- /dev/null +++ b/src/event.rs @@ -0,0 +1,97 @@ +use std::time::Duration; + +use crossterm::event::{Event as CrosstermEvent, KeyEvent, MouseEvent}; +use futures::{FutureExt, StreamExt}; +use tokio::sync::mpsc; + +use crate::app::AppResult; + +/// 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::UnboundedSender, + /// Event receiver channel. + receiver: mpsc::UnboundedReceiver, + /// Event handler thread. + handler: tokio::task::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::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, + } + } + + /// 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 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..3cea937 --- /dev/null +++ b/src/handler.rs @@ -0,0 +1,28 @@ +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/main.rs b/src/main.rs index 999b638..4608770 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,46 @@ +use std::io; -pub mod component; +use ratatui::{backend::CrosstermBackend, Terminal}; + +use crate::{ + app::{App, AppResult}, + event::{Event, EventHandler}, + handler::handle_key_events, + tui::Tui, +}; + +pub mod app; +pub mod event; +pub mod handler; +pub mod tui; +pub mod ui; #[tokio::main] -async fn main() { - println!("Hello, world!"); -} \ No newline at end of file +async fn main() -> AppResult<()> { + // Create an application. + let mut app = App::new(); + + // Initialize the terminal user interface. + let backend = CrosstermBackend::new(io::stdout()); + let terminal = Terminal::new(backend)?; + let events = EventHandler::new(250); + let mut tui = Tui::new(terminal, events); + tui.init()?; + + // Start the main loop. + while app.running { + // Render the user interface. + tui.draw(&mut app)?; + // Handle events. + match tui.events.next().await? { + Event::Tick => app.tick(), + Event::Key(key_event) => handle_key_events(key_event, &mut app)?, + Event::Mouse(_) => {} + Event::Resize(_, _) => {} + } + } + + // Exit the user interface. + tui.exit()?; + Ok(()) +} diff --git a/src/tui.rs b/src/tui.rs new file mode 100644 index 0000000..b15541b --- /dev/null +++ b/src/tui.rs @@ -0,0 +1,76 @@ +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; + +/// Representation of a terminal user interface. +/// +/// It is responsible for setting up the terminal, +/// initializing the interface and handling the draw events. +#[derive(Debug)] +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 } + } + + /// Initializes the terminal interface. + /// + /// It enables the raw mode and sets terminal properties. + pub fn init(&mut self) -> AppResult<()> { + terminal::enable_raw_mode()?; + crossterm::execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture)?; + + // Define a custom panic hook to reset the terminal properties. + // This way, you won't have your terminal messed up if an unexpected error happens. + 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(()) + } + + /// [`Draw`] the terminal interface by [`rendering`] the widgets. + /// + /// [`Draw`]: ratatui::Terminal::draw + /// [`rendering`]: crate::ui::render + pub fn draw(&mut self, app: &mut App) -> AppResult<()> { + self.terminal.draw(|frame| ui::render(app, frame))?; + Ok(()) + } + + /// Resets the terminal interface. + /// + /// This function is also used for the panic hook to revert + /// the terminal properties if unexpected errors occur. + fn reset() -> AppResult<()> { + terminal::disable_raw_mode()?; + crossterm::execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture)?; + Ok(()) + } + + /// Exits the terminal interface. + /// + /// It disables the raw mode and reverts back the terminal properties. + 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..defd047 --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,34 @@ +use ratatui::{ + layout::Alignment, + style::{Color, Style}, + widgets::{Block, BorderType, Paragraph}, + Frame, +}; + +use crate::app::App; + +/// Renders the user interface widgets. +pub fn render(app: &mut App, frame: &mut Frame) { + // This is where you add new widgets. + // See the following resources: + // - https://docs.rs/ratatui/latest/ratatui/widgets/index.html + // - https://github.com/ratatui/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::bordered() + .title("Template") + .title_alignment(Alignment::Center) + .border_type(BorderType::Rounded), + ) + .style(Style::default().fg(Color::Cyan).bg(Color::Black)) + .centered(), + frame.area(), + ) +}