init
This commit is contained in:
106
Cargo.lock
generated
106
Cargo.lock
generated
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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
49
src/app.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
use ratatui::widgets::Widget;
|
||||
|
||||
|
||||
pub trait Component {
|
||||
fn render(&self) -> Box<dyn Widget>;
|
||||
}
|
||||
@@ -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
97
src/event.rs
Normal 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
28
src/handler.rs
Normal 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(())
|
||||
}
|
||||
45
src/main.rs
45
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!");
|
||||
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
76
src/tui.rs
Normal 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
34
src/ui.rs
Normal 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(),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user