init
This commit is contained in:
106
Cargo.lock
generated
106
Cargo.lock
generated
@@ -111,6 +111,7 @@ checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"crossterm_winapi",
|
"crossterm_winapi",
|
||||||
|
"futures-core",
|
||||||
"mio",
|
"mio",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"rustix",
|
"rustix",
|
||||||
@@ -153,6 +154,95 @@ dependencies = [
|
|||||||
"windows-sys",
|
"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]]
|
[[package]]
|
||||||
name = "gimli"
|
name = "gimli"
|
||||||
version = "0.31.0"
|
version = "0.31.0"
|
||||||
@@ -336,6 +426,12 @@ version = "0.2.14"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
|
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-utils"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "powerfmt"
|
name = "powerfmt"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -478,6 +574,15 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "slab"
|
||||||
|
version = "0.4.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.13.2"
|
version = "1.13.2"
|
||||||
@@ -538,6 +643,7 @@ name = "tethers"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crossterm",
|
"crossterm",
|
||||||
|
"futures",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ edition = "2021"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[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"] }
|
ratatui = { version = "0.28.1", features = ["all-widgets"] }
|
||||||
tokio = { version = "1.40.0", features = ["full"] }
|
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(())
|
||||||
|
}
|
||||||
47
src/main.rs
47
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]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() -> AppResult<()> {
|
||||||
println!("Hello, world!");
|
// 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