feat: get courses

This commit is contained in:
newt 2024-11-11 15:45:33 +00:00
parent 3cbc26e11c
commit 9d247c76d5
21 changed files with 2604 additions and 255 deletions

6
.gitmodules vendored Normal file
View file

@ -0,0 +1,6 @@
[submodule "crates/tui-confirm-dialog"]
path = crates/tui-confirm-dialog
url = https://git.newty.dev/newt/tui-confirm-dialog-tokio
[submodule "crates/reqwest_cookie_store_tokio"]
path = crates/reqwest_cookie_store_tokio
url = https://git.newty.dev/newt/reqwest_cookie_store_tokio

View file

@ -1,3 +1,5 @@
{
"rust-analyzer.cargo.features": "all"
"files.exclude": {
"target": true
}
}

134
Cargo.lock generated
View file

@ -28,9 +28,31 @@ dependencies = [
[[package]]
name = "allocator-api2"
version = "0.2.18"
version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
checksum = "45862d1c77f2228b9e10bc609d5bc203d86ebc9b87ad8d5d5167a6c9abf739d9"
[[package]]
name = "async-stream"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
dependencies = [
"async-stream-impl",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-stream-impl"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "async-trait"
@ -117,9 +139,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.1.31"
version = "1.1.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f"
checksum = "40545c26d092346d8a8dab71ee48e7685a7a9cba76e634790c215b41a4a7b4cf"
dependencies = [
"shlex",
]
@ -241,6 +263,16 @@ dependencies = [
"winapi",
]
[[package]]
name = "dashmap"
version = "4.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e77a43b28d0668df09411cb0bc9a8c2adc40f9a048afe863e05fd43251e8e39c"
dependencies = [
"cfg-if",
"num_cpus",
]
[[package]]
name = "deranged"
version = "0.3.11"
@ -279,6 +311,7 @@ dependencies = [
"cookie_store",
"crossterm",
"email_address",
"ijson",
"lazy_static",
"paste",
"ratatui",
@ -345,9 +378,9 @@ dependencies = [
[[package]]
name = "fastrand"
version = "2.1.1"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4"
[[package]]
name = "fnv"
@ -506,9 +539,9 @@ checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403"
[[package]]
name = "hashbrown"
version = "0.15.0"
version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb"
checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3"
dependencies = [
"allocator-api2",
"equivalent",
@ -788,6 +821,18 @@ dependencies = [
"icu_properties",
]
[[package]]
name = "ijson"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b96214564d1f12875bd9661b183d8494dd10e373cb693629536fe2f3125e254b"
dependencies = [
"dashmap",
"lazy_static",
"serde",
"serde_json",
]
[[package]]
name = "indenter"
version = "0.3.3"
@ -858,9 +903,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.161"
version = "0.2.162"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398"
[[package]]
name = "linux-raw-sys"
@ -972,6 +1017,16 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num_cpus"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
"hermit-abi",
"libc",
]
[[package]]
name = "num_threads"
version = "0.1.7"
@ -1223,9 +1278,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.8"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
@ -1287,13 +1342,15 @@ dependencies = [
[[package]]
name = "reqwest_cookie_store"
version = "0.8.0"
source = "git+https://git.newty.dev/newt/reqwest_cookie_store_tokio.git#aad0c697e209c6e79b15ff49b3bf0d05663492c3"
dependencies = [
"bytes",
"cookie_store",
"futures",
"reqwest",
"serde",
"serde_derive",
"tokio",
"tokio-test",
"url",
]
@ -1320,9 +1377,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustix"
version = "0.38.38"
version = "0.38.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a"
checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0"
dependencies = [
"bitflags",
"errno",
@ -1412,9 +1469,9 @@ dependencies = [
[[package]]
name = "security-framework-sys"
version = "2.12.0"
version = "2.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6"
checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2"
dependencies = [
"core-foundation-sys",
"libc",
@ -1592,9 +1649,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.86"
version = "2.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89275301d38033efb81a6e60e3497e734dfcc62571f2854bf4b16690398824c"
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
dependencies = [
"proc-macro2",
"quote",
@ -1644,9 +1701,9 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.13.0"
version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b"
checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c"
dependencies = [
"cfg-if",
"fastrand",
@ -1657,18 +1714,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.66"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d171f59dbaa811dbbb1aee1e73db92ec2b122911a48e1390dfe327a821ddede"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.66"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b08be0f17bd307950653ce45db00cd31200d82b624b36e181337d9c7d92765b5"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
@ -1791,6 +1848,30 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-stream"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1"
dependencies = [
"futures-core",
"pin-project-lite",
"tokio",
]
[[package]]
name = "tokio-test"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7"
dependencies = [
"async-stream",
"bytes",
"futures-core",
"tokio",
"tokio-stream",
]
[[package]]
name = "tokio-util"
version = "0.7.12"
@ -1871,13 +1952,12 @@ dependencies = [
[[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",
"tokio",
]
[[package]]

View file

@ -1,24 +1,4 @@
[package]
name = "echoed"
publish = false
version = "0.1.0"
edition = "2021"
[dependencies]
async-trait = "0.1.83"
color-eyre = "0.6.3"
cookie_store = "0.21.1"
crossterm = "0.28.1"
email_address = "0.2.9"
lazy_static = "1.5.0"
paste = "1.0.15"
ratatui = "0.29.0"
regex = "1.11.1"
reqwest = { version = "0.12.9", features = ["cookies", "multipart"] }
reqwest_cookie_store = { git = "https://git.newty.dev/newt/reqwest_cookie_store_tokio.git" }
serde_cbor = "0.11.2"
thiserror = "1.0.66"
tokio = { version = "1.41.1", features = ["macros", "rt-multi-thread"] }
tui-textarea = "0.7.0"
tui_confirm_dialog = "0.2.4"
url = "2.5.3"
[workspace]
resolver = "2"
default-members = ["crates/echoed"]
members = ["crates/*"]

2349
crates/echoed/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

25
crates/echoed/Cargo.toml Normal file
View file

@ -0,0 +1,25 @@
[package]
name = "echoed"
publish = false
version = "0.1.0"
edition = "2021"
[dependencies]
async-trait = "0.1.83"
color-eyre = "0.6.3"
cookie_store = "0.21.1"
crossterm = "0.28.1"
email_address = "0.2.9"
ijson = "0.1.3"
lazy_static = "1.5.0"
paste = "1.0.15"
ratatui = "0.29.0"
regex = "1.11.1"
reqwest = { version = "0.12.9", features = ["cookies", "json", "multipart"] }
reqwest_cookie_store = { path = "../reqwest_cookie_store_tokio" }
serde_cbor = "0.11.2"
thiserror = "1.0.66"
tokio = { version = "1.41.1", default-features = false, features = ["macros", "rt-multi-thread"] }
tui-textarea = "0.7.0"
tui_confirm_dialog = { path = "../tui-confirm-dialog" }
url = "2.5.3"

View file

@ -2,7 +2,6 @@ use color_eyre::Result;
use echoed::{UserData, DEFAULT_ECHO360};
use regex::Regex;
use reqwest::multipart::Form;
use reqwest_cookie_store::CookieStoreMutex;
use thiserror::Error;
use crate::State;

View file

@ -2,6 +2,7 @@ use color_eyre::Result;
use cookie_store::{Cookie, CookieDomain, CookieStore};
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
use echoed::{UserData, DEFAULT_ECHO360};
use helper::echo;
use ratatui::{
prelude::*,
widgets::{BorderType, Borders},
@ -9,11 +10,12 @@ use ratatui::{
use reqwest::Client;
use reqwest_cookie_store::CookieStoreMutex;
use std::{
env::var,
io::{Stdout, Write},
sync::{mpsc as std_mpsc, Arc},
sync::Arc,
time::{Duration, Instant},
};
use tokio::sync::Mutex;
use tokio::sync::{mpsc, Mutex};
use tui_confirm_dialog::{ConfirmDialog, ConfirmDialogState, Listener};
use url::Url;
use views::ViewContainer;
@ -34,7 +36,7 @@ pub enum Command {
pub struct State {
exit: ConfirmDialogState,
exit_rx: std_mpsc::Receiver<Listener>,
exit_rx: mpsc::Receiver<Listener>,
client: Client,
cookies: Arc<CookieStoreMutex>,
echo_user: Option<UserData>,
@ -42,7 +44,7 @@ pub struct State {
impl State {
fn new(jar: CookieStoreMutex) -> Result<Self> {
let (exit_tx, exit_rx) = std_mpsc::channel::<Listener>();
let (exit_tx, exit_rx) = mpsc::channel::<Listener>(1);
let jar = Arc::new(jar);
Ok(Self {
@ -83,16 +85,27 @@ async fn run(mut terminal: Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
// check if the cookies provide authentication
let mut state = State::new(jar)?;
let authenticated = helper::echo::check_auth(&mut state).await?;
let mut authenticated = echo::check_auth(&mut state).await?;
// authenticate if env variables are provided
if !authenticated {
if let Ok((email, password)) = var("ECHO360_EMAIL")
.and_then(|email| var("ECHO360_PASSWORD").map(|password| (email, password)))
{
authenticated = echo::authenticate(&email, &password, &mut state)
.await
.is_ok();
}
}
let mut view: ViewContainer = if authenticated {
Arc::new(Mutex::new(views::Home::default())) as ViewContainer
Arc::new(Mutex::new(views::Courses::default())) as ViewContainer
} else {
Arc::new(Mutex::new(views::Auth::default())) as ViewContainer
};
view.lock().await.setup();
let (view_tx, view_rx) = std_mpsc::channel::<Command>();
view.lock().await.setup(&state).await?;
let (view_tx, mut view_rx) = mpsc::channel::<Command>(1);
let mut exit_triggered = Instant::now();
exit_triggered -= EXIT_TIMEOUT;
@ -100,6 +113,7 @@ async fn run(mut terminal: Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
loop {
// check if the app should exit
if let Ok((_, exit)) = state.exit_rx.try_recv() {
println!("{:?}", exit);
if exit.unwrap_or_default() {
// save cookies
// todo: decide on a place to store this
@ -116,7 +130,7 @@ async fn run(mut terminal: Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
match view_rx.try_recv() {
Ok(Command::ChangeView(new_view)) => {
view = new_view;
view.lock().await.setup();
view.lock().await.setup(&state).await?;
}
_ => {}
@ -149,7 +163,7 @@ async fn run(mut terminal: Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
if key.code == KeyCode::Esc && exit_triggered.elapsed() < EXIT_TIMEOUT {
continue;
} else {
state.exit.handle(key);
state.exit.handle(key).await;
exit_triggered = Instant::now();
}

View file

@ -6,4 +6,4 @@ pub use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
pub use ratatui::prelude::*;
pub use std::any::Any;
pub type ViewSender = std::sync::mpsc::Sender<Command>;
pub type ViewSender = tokio::sync::mpsc::Sender<Command>;

View file

@ -13,17 +13,19 @@ macro_rules! import_view {
};
}
import_view!(Auth, Home);
import_view!(Auth, Courses);
#[async_trait]
pub trait View {
fn setup(&mut self) {}
async fn setup(&mut self, _state: &State) -> Result<()> {
Ok(())
}
fn draw(&self, frame: &mut Frame, state: &State);
async fn keypress(
&mut self,
key: KeyEvent,
state: &mut State,
command_tx: &ViewSender,
_key: KeyEvent,
_state: &mut State,
_command_tx: &ViewSender,
) -> Result<()> {
Ok(())
}
@ -35,7 +37,8 @@ pub type ViewContainer = Arc<Mutex<dyn View + Send + Sync>>;
macro_rules! change_view {
($tx:expr, $view:ident) => {
$tx.send(Command::ChangeView(std::sync::Arc::new(
tokio::sync::Mutex::new(super::$view),
)))?
tokio::sync::Mutex::new(super::$view::default()),
)))
.await?
};
}

View file

@ -18,9 +18,10 @@ pub struct Auth<'t> {
#[async_trait]
impl<'t> View for Auth<'t> {
fn setup(&mut self) {
async fn setup(&mut self, _: &State) -> Result<()> {
self.password.set_mask_char('\u{2022}');
self.password.set_cursor_style(HIDE_STYLE);
Ok(())
}
fn draw(&self, frame: &mut Frame, _: &State) {
@ -120,7 +121,7 @@ impl<'t> View for Auth<'t> {
.await
{
Ok(_) => {
change_view!(tx, Home);
change_view!(tx, Courses);
}
Err(e) => {
self.error = Some(e.to_string().into());

View file

@ -0,0 +1,70 @@
use color_eyre::eyre::eyre;
use echoed::DEFAULT_ECHO360;
use crate::prelude::*;
use std::borrow::Cow;
#[derive(Debug)]
pub struct Course {
name: Cow<'static, str>,
code: Cow<'static, str>,
section_id: Cow<'static, str>,
}
#[derive(Default)]
pub struct Courses {
courses: Vec<Course>,
}
#[async_trait]
impl View for Courses {
async fn setup(&mut self, state: &State) -> Result<()> {
let res = state
.client
.get(format!("{}user/enrollments", DEFAULT_ECHO360.clone()))
.send()
.await?
.json::<ijson::IValue>()
.await?;
let enrollments = res["data"]
.as_array()
.map(|data| data[0]["userSections"].as_array())
.flatten()
.ok_or(eyre!("No courses found"))?
.iter()
.map(|course| {
course["sectionId"].as_string().and_then(|section_id| {
course["sectionName"].as_string().and_then(|code| {
course["courseName"].as_string().map(|name| Course {
name: name.to_string().into(),
code: code.to_string().into(),
section_id: section_id.to_string().into(),
})
})
})
})
.collect::<Option<Vec<_>>>()
.ok_or(eyre!("No courses found"))?;
self.courses = enrollments;
Ok(())
}
fn draw(&self, frame: &mut Frame, state: &State) {
// let user = state.echo_user.as_ref().unwrap();
if let Some(x) = self.courses.first() {
frame.render_widget(Line::raw(x.name.to_string()), frame.area());
}
}
async fn keypress(
&mut self,
key: KeyEvent,
state: &mut State,
_command_tx: &ViewSender,
) -> Result<()> {
Ok(())
}
}

@ -0,0 +1 @@
Subproject commit aad0c697e209c6e79b15ff49b3bf0d05663492c3

@ -0,0 +1 @@
Subproject commit 5c1156b1cd6ed765d4d06be82f8c027043aa0258

99
old.rs
View file

@ -1,99 +0,0 @@
use color_eyre::eyre::eyre;
use color_eyre::Result;
use regex::Regex;
use reqwest::multipart::Form;
use reqwest::{Client, Url};
use reqwest_cookie_store::CookieStoreMutex;
use std::env;
use std::sync::Arc;
const DEFAULT_BASE_URL: &str = "https://echo360.org.uk";
#[tokio::main]
async fn main() -> Result<()> {
color_eyre::install()?;
#[cfg(feature = "dotenvy")]
dotenvy::dotenv()?;
let jar = Arc::new(CookieStoreMutex::default());
let client = Client::builder().cookie_provider(jar.clone()).build()?;
let base_url = Url::parse(DEFAULT_BASE_URL)?;
let domain = base_url.domain().unwrap();
// get the csrf token
client.get(base_url.clone()).send().await?;
let csrf_token = jar
.lock()
.unwrap()
.get(domain, "/", "PLAY_SESSION")
.map(|cookie| {
let cookie_value = cookie.value().to_string();
cookie_value
.split("&")
.nth(1)
.map(|part| part.split("=").last())
.flatten()
.map(|x| x.to_string())
})
.flatten()
.ok_or(eyre!("No csrf token found"))?;
// authenticate
client
.post(format!("{base_url}login"))
.query(&[("csrfToken", csrf_token)])
.multipart(
Form::new()
.text("email", env::var("ECHO360_EMAIL")?)
.text("password", env::var("ECHO360_PASSWORD")?),
)
.send()
.await?;
// get institution id
let insitution_id = jar
.lock()
.unwrap()
.get(domain, "/", "PLAY_SESSION")
.map(|cookie| {
let cookie_value = cookie.value().to_string();
cookie_value
.split("&")
.nth(1)
.map(|part| part.split("=").last())
.flatten()
.map(|x| x.to_string())
})
.flatten()
.ok_or(eyre!("No institution id found"))?;
println!("Institution ID: {}", insitution_id);
// get authenticated user's name
let name = {
let html = client
.get(format!("{base_url}courses"))
.send()
.await?
.text()
.await?;
let first_name = Regex::new(r#"\\"firstName\\":\\"(\w+)\\""#)?
.captures(&html)
.unwrap()
.get(1)
.unwrap()
.as_str();
let last_name = Regex::new(r#"\\"lastName\\":\\"(\w+)\\""#)?
.captures(&html)
.unwrap()
.get(1)
.unwrap()
.as_str();
format!("{} {}", first_name, last_name)
};
println!("Name: {}", name);
Ok(())
}

View file

@ -1,22 +0,0 @@
use crate::prelude::*;
#[derive(Default)]
pub struct Home;
#[async_trait]
impl View for Home {
fn draw(&self, frame: &mut Frame, state: &State) {
let user = state.echo_user.as_ref().unwrap();
frame.render_widget(Line::raw(user.name.to_string()), frame.area());
}
async fn keypress(
&mut self,
key: KeyEvent,
state: &mut State,
_command_tx: &ViewSender,
) -> Result<()> {
Ok(())
}
}

View file

@ -1,61 +0,0 @@
use super::{View, ViewContainer};
use crate::prelude::*;
use ratatui::widgets::{Block, Borders};
#[derive(Clone)]
pub struct Quit {
previous: ViewContainer,
yes: bool,
}
impl Quit {
pub fn new(previous: ViewContainer) -> Self {
Self {
previous,
yes: true,
}
}
}
#[async_trait]
impl View for Quit {
fn draw(&self, frame: &mut Frame) {
let area = helper::border(frame, None);
// split vertically
let [title_area, buttons_area] =
Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]).areas(area);
// render title
frame.render_widget(
Text::raw("Are you sure you would like to quit?").centered(),
title_area,
);
// render buttons
let [yes_btn_area, no_btn_area] =
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
.areas(buttons_area);
let yes_btn = Text::raw("Yes");
let no_btn = Text::raw("No");
frame.render_widget(yes_btn, yes_btn_area);
frame.render_widget(no_btn, no_btn_area);
}
async fn keypress(&mut self, key: KeyEvent, tx: &ViewSender) -> Result<()> {
match key.code {
KeyCode::Esc => {
tx.send(ViewCommand::Change(self.previous.clone())).await?;
}
_ => {}
}
Ok(())
}
fn as_any(&self) -> &dyn Any {
self
}
}