This commit is contained in:
RainBus
2024-09-23 18:17:28 +08:00
parent 15e02e99e6
commit 83ff3fb6c4
11 changed files with 435 additions and 27 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

106
Cargo.lock generated
View File

@@ -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",
]

View File

@@ -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"] }

49
src/app.rs Normal file
View File

@@ -0,0 +1,49 @@
use std::error;
/// Application result type.
pub type AppResult<T> = std::result::Result<T, Box<dyn error::Error>>;
/// 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;
}
}
}

View File

@@ -1,6 +0,0 @@
use ratatui::widgets::Widget;
pub trait Component {
fn render(&self) -> Box<dyn Widget>;
}

View File

@@ -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
}
}

97
src/event.rs Normal file
View File

@@ -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>,
/// Event receiver channel.
receiver: mpsc::UnboundedReceiver<Event>,
/// 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<Event> {
self.receiver
.recv()
.await
.ok_or(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
"This is an IO error",
)))
}
}

28
src/handler.rs Normal file
View File

@@ -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(())
}

View File

@@ -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!");
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(())
}

76
src/tui.rs Normal file
View File

@@ -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<B: Backend> {
/// Interface to the Terminal.
terminal: Terminal<B>,
/// Terminal event handler.
pub events: EventHandler,
}
impl<B: Backend> Tui<B> {
/// Constructs a new instance of [`Tui`].
pub fn new(terminal: Terminal<B>, 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(())
}
}

34
src/ui.rs Normal file
View File

@@ -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(),
)
}