feat: add exit dialogue and auth fields
This commit is contained in:
parent
7cf30ab18b
commit
52d184626d
9 changed files with 367 additions and 18 deletions
124
Cargo.lock
generated
124
Cargo.lock
generated
|
@ -32,6 +32,17 @@ version = "0.2.18"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
|
@ -77,6 +88,12 @@ version = "3.16.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.8.0"
|
||||
|
@ -233,24 +250,28 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "dotenvy"
|
||||
version = "0.15.7"
|
||||
name = "dyn-clone"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
||||
checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
|
||||
|
||||
[[package]]
|
||||
name = "echoed"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"color-eyre",
|
||||
"cookie_store",
|
||||
"crossterm",
|
||||
"dotenvy",
|
||||
"dyn-clone",
|
||||
"paste",
|
||||
"ratatui",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"reqwest_cookie_store",
|
||||
"tokio",
|
||||
"tui-textarea",
|
||||
"tui_confirm_dialog",
|
||||
"url",
|
||||
]
|
||||
|
||||
|
@ -739,6 +760,15 @@ version = "0.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
|
||||
[[package]]
|
||||
name = "num_threads"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.32.2"
|
||||
|
@ -863,6 +893,15 @@ version = "0.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.89"
|
||||
|
@ -897,6 +936,36 @@ dependencies = [
|
|||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ratatui"
|
||||
version = "0.29.0"
|
||||
|
@ -913,6 +982,7 @@ dependencies = [
|
|||
"lru",
|
||||
"paste",
|
||||
"strum",
|
||||
"time",
|
||||
"unicode-segmentation",
|
||||
"unicode-truncate",
|
||||
"unicode-width 0.2.0",
|
||||
|
@ -1363,7 +1433,9 @@ checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
|
|||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"libc",
|
||||
"num-conv",
|
||||
"num_threads",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"time-core",
|
||||
|
@ -1515,6 +1587,29 @@ version = "0.2.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "tui-textarea"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a5318dd619ed73c52a9417ad19046724effc1287fb75cdcc4eca1d6ac1acbae"
|
||||
dependencies = [
|
||||
"crossterm",
|
||||
"ratatui",
|
||||
"unicode-width 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tui_confirm_dialog"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "117929c8e294f8ceea4f56f4d09a6a4e694f057d175f2cf61433309fb01e9a94"
|
||||
dependencies = [
|
||||
"crossterm",
|
||||
"rand",
|
||||
"ratatui",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.8.0"
|
||||
|
@ -1832,6 +1927,27 @@ version = "0.52.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.1"
|
||||
|
|
|
@ -4,17 +4,18 @@ publish = false
|
|||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
dotenvy = ["dep:dotenvy"]
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1.83"
|
||||
color-eyre = "0.6.3"
|
||||
cookie_store = "0.21.0"
|
||||
crossterm = "0.28.1"
|
||||
dotenvy = { version = "0.15.7", optional = true }
|
||||
dyn-clone = "1.0.17"
|
||||
paste = "1.0.15"
|
||||
ratatui = "0.29.0"
|
||||
regex = "1.11.1"
|
||||
reqwest = { version = "0.12.9", features = ["cookies", "multipart"] }
|
||||
reqwest_cookie_store = "0.8.0"
|
||||
tokio = { version = "1.41.0", features = ["macros", "rt-multi-thread"] }
|
||||
tui-textarea = "0.7.0"
|
||||
tui_confirm_dialog = "0.2.4"
|
||||
url = "2.5.2"
|
||||
|
|
1
src/helper.rs
Normal file
1
src/helper.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod tui;
|
20
src/helper/tui.rs
Normal file
20
src/helper/tui.rs
Normal file
|
@ -0,0 +1,20 @@
|
|||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{Block, BorderType, Borders},
|
||||
};
|
||||
|
||||
// Create a block with
|
||||
pub fn border(frame: &mut Frame, title: Option<&str>) -> Rect {
|
||||
let outer = frame.area();
|
||||
let mut block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded);
|
||||
|
||||
if let Some(title) = title {
|
||||
block = block.title(title);
|
||||
}
|
||||
|
||||
frame.render_widget(block.clone(), outer);
|
||||
|
||||
block.inner(outer)
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
pub type Terminal = ratatui::Terminal<ratatui::backend::CrosstermBackend<std::io::Stdout>>;
|
127
src/main.rs
127
src/main.rs
|
@ -1,18 +1,127 @@
|
|||
use color_eyre::Result;
|
||||
use crossterm::event::{self, KeyCode, KeyEventKind};
|
||||
use echoed::Terminal;
|
||||
use ratatui::widgets::Paragraph;
|
||||
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{BorderType, Borders},
|
||||
};
|
||||
use std::{
|
||||
io::Stdout,
|
||||
sync::{mpsc, Arc},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tokio::sync::Mutex;
|
||||
use tui_confirm_dialog::{ConfirmDialog, ConfirmDialogState, Listener, PopupMessage};
|
||||
use views::{ViewCommand, ViewContainer};
|
||||
|
||||
#[macro_use]
|
||||
extern crate async_trait;
|
||||
|
||||
mod helper;
|
||||
pub mod prelude;
|
||||
mod views;
|
||||
|
||||
const EXIT_TIMEOUT: Duration = Duration::from_millis(150);
|
||||
const POLL_TIMEOUT: Duration = Duration::from_millis(16);
|
||||
|
||||
pub struct State {
|
||||
exit: ConfirmDialogState,
|
||||
exit_rx: mpsc::Receiver<Listener>,
|
||||
}
|
||||
|
||||
impl Default for State {
|
||||
fn default() -> Self {
|
||||
let (exit_tx, exit_rx) = mpsc::channel::<Listener>();
|
||||
|
||||
Self {
|
||||
exit: ConfirmDialogState::default()
|
||||
.with_title("Exit")
|
||||
.with_text(Text::raw("Are you sure you would like to exit?"))
|
||||
.with_listener(Some(exit_tx)),
|
||||
exit_rx,
|
||||
// ..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn run(mut terminal: Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
|
||||
let mut view: ViewContainer = Arc::new(Mutex::new(views::Auth::default()));
|
||||
view.lock().await.setup();
|
||||
let mut state = State::default();
|
||||
let (view_tx, view_rx) = mpsc::channel::<ViewCommand>();
|
||||
let mut exit_triggered = Instant::now();
|
||||
exit_triggered -= EXIT_TIMEOUT;
|
||||
|
||||
async fn run(mut terminal: Terminal) -> Result<()> {
|
||||
loop {
|
||||
// check if the app should exit
|
||||
if let Ok((_, exit)) = state.exit_rx.try_recv() {
|
||||
if exit.unwrap_or_default() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// check for new view commands
|
||||
match view_rx.try_recv() {
|
||||
Ok(ViewCommand::Change(new_view)) => {
|
||||
view = new_view;
|
||||
view.lock().await.setup();
|
||||
}
|
||||
|
||||
Err(_) => {}
|
||||
}
|
||||
|
||||
// draw the view
|
||||
let mut view = view.lock().await;
|
||||
|
||||
terminal.draw(|frame| {
|
||||
let greeting = Paragraph::new("Hello Ratatui! (press 'q' to quit)");
|
||||
frame.render_widget(greeting, frame.area());
|
||||
view.draw(frame);
|
||||
|
||||
if state.exit.is_opened() {
|
||||
frame.render_stateful_widget(
|
||||
ConfirmDialog::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.selected_button_style(Style::default().yellow().underlined().bold()),
|
||||
frame.area(),
|
||||
&mut state.exit,
|
||||
);
|
||||
}
|
||||
})?;
|
||||
|
||||
if let event::Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
||||
return Ok(());
|
||||
// handle events
|
||||
if event::poll(POLL_TIMEOUT)? {
|
||||
match event::read()? {
|
||||
Event::Key(key) if key.kind == KeyEventKind::Press => {
|
||||
// handle exit popup keypresses above all else
|
||||
if state.exit.is_opened() {
|
||||
if key.code == KeyCode::Esc && exit_triggered.elapsed() < EXIT_TIMEOUT {
|
||||
continue;
|
||||
} else {
|
||||
state.exit.handle(key);
|
||||
exit_triggered = Instant::now();
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// then handle view keypresses
|
||||
view.keypress(key, &view_tx).await?;
|
||||
|
||||
// then handle global keypresses
|
||||
|
||||
// esc + ctrl+c quit
|
||||
if (key.code == KeyCode::Esc
|
||||
|| (key.code == KeyCode::Char('c')
|
||||
&& key.modifiers.contains(KeyModifiers::CONTROL)))
|
||||
&& !state.exit.is_opened()
|
||||
&& exit_triggered.elapsed() >= EXIT_TIMEOUT
|
||||
{
|
||||
state.exit = state.exit.open();
|
||||
exit_triggered = Instant::now();
|
||||
}
|
||||
|
||||
drop(view);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
7
src/prelude.rs
Normal file
7
src/prelude.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
pub use crate::{helper::tui as helper, views::ViewCommand};
|
||||
pub use color_eyre::Result;
|
||||
pub use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
|
||||
pub use ratatui::prelude::*;
|
||||
pub use std::any::Any;
|
||||
|
||||
pub type ViewSender = std::sync::mpsc::Sender<ViewCommand>;
|
30
src/views.rs
Normal file
30
src/views.rs
Normal file
|
@ -0,0 +1,30 @@
|
|||
use crate::prelude::*;
|
||||
use dyn_clone::DynClone;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
macro_rules! import_view {
|
||||
($($view:ident),+) => {
|
||||
paste::paste! {
|
||||
$(
|
||||
mod [<$view:lower>];
|
||||
pub use [<$view:lower>]::$view;
|
||||
)+
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
import_view!(Auth);
|
||||
|
||||
#[async_trait]
|
||||
pub trait View: DynClone {
|
||||
fn setup(&mut self);
|
||||
fn draw(&self, frame: &mut Frame);
|
||||
async fn keypress(&mut self, key: KeyEvent, command_tx: &ViewSender) -> Result<()>;
|
||||
}
|
||||
|
||||
pub type ViewContainer = Arc<Mutex<dyn View + Send + Sync>>;
|
||||
|
||||
pub enum ViewCommand {
|
||||
Change(ViewContainer),
|
||||
}
|
66
src/views/auth.rs
Normal file
66
src/views/auth.rs
Normal file
|
@ -0,0 +1,66 @@
|
|||
use super::View;
|
||||
use crate::prelude::*;
|
||||
use tui_textarea::TextArea;
|
||||
|
||||
const SHOW_STYLE: Style = Style::new().add_modifier(Modifier::REVERSED);
|
||||
const HIDE_STYLE: Style = Style::new().bg(Color::Reset);
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct Auth<'t> {
|
||||
email: TextArea<'t>,
|
||||
password: TextArea<'t>,
|
||||
pass_select: bool,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<'t> View for Auth<'t> {
|
||||
fn setup(&mut self) {
|
||||
self.password.set_mask_char('\u{2022}');
|
||||
self.password.set_cursor_style(HIDE_STYLE);
|
||||
}
|
||||
|
||||
fn draw(&self, frame: &mut Frame) {
|
||||
let area = helper::border(frame, Some("Auth"));
|
||||
let field_areas =
|
||||
Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]).split(area);
|
||||
|
||||
#[allow(unused_mut)]
|
||||
for ((mut field, label), area) in [(&self.email, "Email"), (&self.password, "Password")]
|
||||
.iter()
|
||||
.zip(field_areas.iter())
|
||||
{
|
||||
let [label_area, field_area] =
|
||||
Layout::horizontal([Constraint::Percentage(20), Constraint::Percentage(80)])
|
||||
.areas(*area);
|
||||
|
||||
frame.render_widget(Text::raw(*label), label_area);
|
||||
frame.render_widget(field, field_area);
|
||||
}
|
||||
}
|
||||
|
||||
async fn keypress(&mut self, key: KeyEvent, _command_tx: &ViewSender) -> Result<()> {
|
||||
match key.code {
|
||||
KeyCode::Up => {
|
||||
self.pass_select = false;
|
||||
self.email.set_cursor_style(SHOW_STYLE);
|
||||
self.password.set_cursor_style(HIDE_STYLE);
|
||||
}
|
||||
KeyCode::Down => {
|
||||
self.pass_select = true;
|
||||
self.email.set_cursor_style(HIDE_STYLE);
|
||||
self.password.set_cursor_style(SHOW_STYLE);
|
||||
}
|
||||
KeyCode::Char(_) | KeyCode::Backspace => {
|
||||
let field = if self.pass_select {
|
||||
&mut self.password
|
||||
} else {
|
||||
&mut self.email
|
||||
};
|
||||
|
||||
field.input(key);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue