refactor (architecture): implemented an event based architecture

This commit is contained in:
antifallobst 2023-07-04 18:32:57 +02:00
parent 6ba8d3dbf0
commit dfc87ff937
7 changed files with 534 additions and 229 deletions

1
Cargo.lock generated
View File

@ -2651,6 +2651,7 @@ dependencies = [
"matrix-sdk",
"serde",
"tokio",
"tokio-util",
"tui",
"tui-textarea",
]

View File

@ -13,5 +13,6 @@ crossterm = "*"
matrix-sdk = "0.6"
anyhow = "1.0"
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
tokio-util = "0.7"
serde = "1.0"
cli-log = "2.0"

207
src/app/event.rs Normal file
View File

@ -0,0 +1,207 @@
use crate::app::{App, status::{Status, State}};
use crate::ui;
use tokio::time::Duration;
use tokio::sync::{mpsc, broadcast};
use tokio_util::sync::CancellationToken;
use anyhow::{Result, Error};
use matrix_sdk::{
Client,
room::{Room},
config::SyncSettings,
ruma::events::room::{
member::StrippedRoomMemberEvent,
message::{MessageType, OriginalSyncRoomMessageEvent, RoomMessageEventContent},
},
event_handler::Ctx
};
#[derive(Debug)]
pub enum EventStatus {
Ok,
Finished,
Terminate,
}
#[derive(Debug)]
pub struct Event {
input_event: Option<crossterm::event::Event>,
}
pub struct EventBuilder {
event: Event,
}
impl Default for Event {
fn default() -> Self {
Self {
input_event: None,
}
}
}
impl Default for EventBuilder {
fn default() -> Self {
Self {
event: Event::default(),
}
}
}
impl EventBuilder {
fn input_event(&mut self, input_event: crossterm::event::Event) -> &Self {
self.event.input_event = Some(input_event);
self
}
fn build(&self) -> Event {
Event {
input_event: self.event.input_event.clone(),
}
}
}
impl Event {
pub async fn handle(&self, app: &mut App<'_>) -> Result<EventStatus> {
let status = match app.status.state() {
State::None => EventStatus::Ok,
State::Main => self.handle_main(app).await?,
State::Setup => self.handle_setup(app).await?,
};
Ok(status)
}
async fn handle_main(&self, app: &mut App<'_>) -> Result<EventStatus> {
if self.input_event.is_some() {
match tui_textarea::Input::from(self.input_event.clone().unwrap()) {
tui_textarea::Input { key: tui_textarea::Key::Esc, .. } => return Ok(EventStatus::Terminate),
tui_textarea::Input {
key: tui_textarea::Key::Tab,
..
} => {
app.ui.cycle_main_input_position();
}
input => {
match app.ui.input_position() {
ui::MainInputPosition::MessageCompose => { app.ui.message_compose.input(input); },
_ => (),
}
}
};
}
Ok(EventStatus::Ok)
}
async fn handle_setup(&self, app: &mut App<'_>) -> Result<EventStatus> {
let ui = match &mut app.ui.setup_ui {
Some(ui) => ui,
None => return Err(Error::msg("SetupUI instance not found"))
};
if self.input_event.is_some() {
match tui_textarea::Input::from(self.input_event.clone().unwrap()) {
tui_textarea::Input { key: tui_textarea::Key::Esc, .. } => return Ok(EventStatus::Terminate),
tui_textarea::Input {
key: tui_textarea::Key::Tab,
..
} => {
ui.cycle_input_position();
},
tui_textarea::Input {
key: tui_textarea::Key::Enter,
..
} => {
match ui.input_position() {
ui::SetupInputPosition::Ok => {
let homeserver = ui.homeserver.lines()[0].clone();
let username = ui.username.lines()[0].clone();
let password = ui.password_data.lines()[0].clone();
let login = app.login(&homeserver, &username, &password).await;
if login.is_ok() {
return Ok(EventStatus::Finished);
}
},
_ => ui.cycle_input_position(),
};
},
input => {
match ui.input_position() {
ui::SetupInputPosition::Homeserver => { ui.homeserver.input(input); },
ui::SetupInputPosition::Username => { ui.username.input(input); },
ui::SetupInputPosition::Password => {
ui.password_data.input(input.clone());
match input.key {
tui_textarea::Key::Char(_) => {
ui.password.input(tui_textarea::Input { key: tui_textarea::Key::Char('*'), ctrl: false, alt: false });
},
_ => { ui.password.input(input); },
}
},
_ => (),
}
}
};
}
Ok(EventStatus::Ok)
}
}
async fn poll_input_events_stage_2(channel: mpsc::Sender<Event>) -> Result<()> {
loop {
if crossterm::event::poll(Duration::from_millis(100))? {
// It's guaranteed that `read` won't block, because `poll` returned
// `Ok(true)`.
let event = EventBuilder::default()
.input_event(crossterm::event::read()?)
.build();
channel.send(event).await?;
} else {
tokio::task::yield_now().await;
}
}
}
pub async fn poll_input_events(channel: mpsc::Sender<Event>, kill: CancellationToken) -> Result<()> {
tokio::select! {
output = poll_input_events_stage_2(channel) => output,
_ = kill.cancelled() => Err(Error::msg("received kill signal"))
}
}
pub async fn poll_matrix_events_stage_2(channel: mpsc::Sender<Event>, app: &App<'_>) -> Result<()> {
app.accounts_manager.client();
let client = match app.client(){
Some(c) => c,
None => return Err(Error::msg("Failed to fetch current client")),
};
client.add_event_handler_context(channel.clone());
client.add_event_handler(on_stripped_state_member);
Ok(())
}
pub async fn poll_matrix_events(channel: mpsc::Sender<Event>, kill: CancellationToken, app: &App<'_>) -> Result<()> {
tokio::select! {
output = poll_matrix_events_stage_2(channel, app) => output,
_ = kill.cancelled() => Err(Error::msg("received kill signal"))
}
}
async fn on_stripped_state_member(
room_member: StrippedRoomMemberEvent,
client: Client,
room: Room,
context: Ctx<mpsc::Sender<Event>>
) -> Result<()> {
let event = EventBuilder::default()
.build();
context.send(event).await?;
Ok(())
}

View File

@ -1,28 +1,43 @@
pub mod event;
pub mod status;
use crate::accounts;
use crate::ui;
use std::path::Path;
use matrix_sdk::{Client};
use matrix_sdk::{Client,
room::{Room},
config::SyncSettings,
ruma::events::room::{
member::StrippedRoomMemberEvent,
message::{MessageType, OriginalSyncRoomMessageEvent, RoomMessageEventContent},
},
event_handler::Ctx
};
use accounts::Account;
use accounts::AccountsManager;
use anyhow::{Result, Error};
use cli_log::{error, warn, info};
use tokio::{time::{sleep, Duration}, sync::{mpsc, broadcast}};
use tokio_util::sync::CancellationToken;
use status::{Status, State};
pub struct Message {
pub author: String,
pub message: String,
pub struct App<'a> {
ui: ui::UI<'a>,
accounts_manager: accounts::AccountsManager,
status: Status,
input_listener_killer: CancellationToken,
}
pub struct Room {
pub messages: Vec<Message>,
impl Drop for App<'_> {
fn drop(&mut self) {
}
}
pub struct App {
pub accounts_manager: accounts::AccountsManager,
client: Option<Client>,
current_room_id: u32,
rooms: Vec<Room>,
}
impl App {
impl App<'_> {
pub fn new() -> Self {
let path:&std::path::Path = Path::new("userdata/accounts.json");
let config = if path.exists() {
@ -33,27 +48,89 @@ impl App {
};
Self {
ui: ui::UI::new(),
accounts_manager: AccountsManager::new(config),
client: None,
current_room_id: 0,
rooms: Vec::new(),
status: Status::default(),
input_listener_killer: CancellationToken::new(),
}
}
pub fn fill_test_data(&mut self) {
let mut room = Room {
messages: Vec::new()
};
pub async fn run(&mut self) -> Result<()> {
let (channel_tx, mut channel_rx) = mpsc::channel(256);
room.messages.push(Message {author: "someone".to_string(), message: "test".to_string()});
// Spawn input event listener
tokio::task::spawn(event::poll_input_events(channel_tx.clone(), self.input_listener_killer.clone()));
info!("Filling in test data");
if self.account().is_err() {
info!("No saved sessions found -> jumping into setup");
self.setup(&mut channel_rx).await?;
}
self.rooms.push(room);
loop {
self.status.set_state(State::Main);
self.ui.update(&self.status).await?;
let event: event::Event = match channel_rx.recv().await {
Some(e) => e,
None => return Err(Error::msg("Event channel has no senders"))
};
match event.handle(self).await? {
event::EventStatus::Ok => (),
event::EventStatus::Terminate => break,
_ => (),
};
}
self.input_listener_killer.cancel();
Ok(())
}
pub fn room(&self) -> Option<&Room> {
self.rooms.get(self.current_room_id as usize)
async fn setup(&mut self, receiver: &mut mpsc::Receiver<event::Event>) -> Result<()> {
self.ui.setup_ui = Some(ui::SetupUI::new());
loop {
self.status.set_state(State::Setup);
self.ui.update_setup().await?;
let event: event::Event = match receiver.recv().await {
Some(e) => e,
None => return Err(Error::msg("Event channel has no senders"))
};
match event.handle(self).await? {
event::EventStatus::Ok => (),
event::EventStatus::Finished => return Ok(()),
event::EventStatus::Terminate => return Err(Error::msg("Terminated by user")),
}
}
}
pub async fn init_account(&mut self) -> Result<()> {
let client = match self.client() {
Some(c) => c,
None => return Err(Error::msg("failed to get current client"))
};
info!("Initializing client for the current account");
Ok(())
}
pub async fn switch_account(&mut self, account_id: u32) -> Result<()> {
Ok(())
}
pub async fn login(&mut self, homeserver: &String, username: &String, password: &String) -> Result<()> {
self.accounts_manager.add(homeserver, username, password).await?;
self.init_account().await?;
Ok(())
}
pub fn room(&self) -> Option<()> {
None
}
pub fn account(&self) -> Result<&Account, ()> {
@ -63,4 +140,8 @@ impl App {
Some(a) => Ok(a)
}
}
pub fn client(&self) -> &Option<Client> {
self.accounts_manager.client()
}
}

46
src/app/status.rs Normal file
View File

@ -0,0 +1,46 @@
pub enum State {
None,
Main,
Setup,
}
pub struct Status {
state: State,
account_name: String,
account_user_id: String,
current_room_id: u32,
}
impl Default for Status {
fn default() -> Self {
Self {
state: State::None,
account_name: "".to_string(),
account_user_id: "".to_string(),
current_room_id: 0,
}
}
}
impl Status {
pub fn account_name(&self) -> &String {
&self.account_name
}
pub fn account_user_id(&self) -> &String {
&self.account_user_id
}
pub fn room(&self) -> Option<()> {
None
}
pub fn state(&self) -> &State {
&self.state
}
pub fn set_state(&mut self, state: State) {
self.state = state;
}
}

View File

@ -9,15 +9,7 @@ async fn main() -> anyhow::Result<()> {
cli_log::init_cli_log!();
let mut app = app::App::new();
app.fill_test_data();
let mut ui = ui::UI::new();
if app.accounts_manager.num_accounts() == 0 {
info!("No saved sessions found -> jumping into setup");
ui.setup(&mut app).await?;
}
ui.main(&mut app).await?;
app.run().await?;
Ok(())
}

View File

@ -1,4 +1,4 @@
use crate::app::{App};
use crate::app::status::Status;
use std::cmp;
use crossterm::{
@ -16,11 +16,10 @@ use tui::style::{Color, Modifier, Style};
use tui::text::{Spans, Span, Text};
use tui::widgets::{Paragraph, Wrap};
use tui_textarea::{Input, Key, TextArea};
use tui_textarea::Key::Char;
use cli_log::{error, warn, info};
#[derive(Clone, Copy)]
enum SetupInputPosition {
pub enum SetupInputPosition {
Homeserver,
Username,
Password,
@ -28,7 +27,7 @@ enum SetupInputPosition {
}
#[derive(Clone, Copy)]
enum MainInputPosition {
pub enum MainInputPosition {
Status,
Rooms,
Messages,
@ -36,11 +35,21 @@ enum MainInputPosition {
RoomInfo
}
pub struct SetupUI<'a> {
input_position: SetupInputPosition,
pub homeserver: TextArea<'a>,
pub username: TextArea<'a>,
pub password: TextArea<'a>,
pub password_data: TextArea<'a>,
}
pub struct UI<'a> {
terminal: Terminal<CrosstermBackend<Stdout>>,
input_position: MainInputPosition,
message_compose: TextArea<'a>,
pub message_compose: TextArea<'a>,
pub setup_ui: Option<SetupUI<'a>>,
}
@ -52,7 +61,7 @@ fn terminal_prepare() -> Result<Stdout> {
Ok(stdout)
}
fn textarea_activate(textarea: &mut TextArea) {
pub fn textarea_activate(textarea: &mut TextArea) {
textarea.set_cursor_line_style(Style::default().add_modifier(Modifier::UNDERLINED));
textarea.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
let b = textarea
@ -62,7 +71,7 @@ fn textarea_activate(textarea: &mut TextArea) {
textarea.set_block(b.style(Style::default()));
}
fn textarea_inactivate(textarea: &mut TextArea) {
pub fn textarea_inactivate(textarea: &mut TextArea) {
textarea.set_cursor_line_style(Style::default());
textarea.set_cursor_style(Style::default());
let b = textarea
@ -74,16 +83,6 @@ fn textarea_inactivate(textarea: &mut TextArea) {
);
}
fn cycle_main_input_position(input_position: &MainInputPosition) -> MainInputPosition {
match input_position {
MainInputPosition::Status => MainInputPosition::Rooms,
MainInputPosition::Rooms => MainInputPosition::Messages,
MainInputPosition::Messages => MainInputPosition::MessageCompose,
MainInputPosition::MessageCompose => MainInputPosition::RoomInfo,
MainInputPosition::RoomInfo => MainInputPosition::Status,
}
}
impl Drop for UI<'_> {
fn drop(&mut self) {
info!("Destructing UI");
@ -97,6 +96,122 @@ impl Drop for UI<'_> {
}
}
impl SetupUI<'_> {
pub fn new() -> Self {
let mut homeserver = TextArea::new(vec!["https://matrix.org".to_string()]);
let mut username = TextArea::default();
let mut password = TextArea::default();
let mut password_data = TextArea::default();
homeserver.set_block(
Block::default()
.title("Homeserver")
.borders(Borders::ALL));
username.set_block(
Block::default()
.title("Username")
.borders(Borders::ALL));
password.set_block(
Block::default()
.title("Password")
.borders(Borders::ALL));
textarea_activate(&mut homeserver);
textarea_inactivate(&mut username);
textarea_inactivate(&mut password);
Self {
input_position: SetupInputPosition::Homeserver,
homeserver,
username,
password,
password_data,
}
}
pub fn cycle_input_position(&mut self) {
self.input_position = match self.input_position {
SetupInputPosition::Homeserver => {
textarea_inactivate(&mut self.homeserver);
textarea_activate(&mut self.username);
textarea_inactivate(&mut self.password);
SetupInputPosition::Username
},
SetupInputPosition::Username => {
textarea_inactivate(&mut self.homeserver);
textarea_inactivate(&mut self.username);
textarea_activate(&mut self.password);
SetupInputPosition::Password
},
SetupInputPosition::Password => {
textarea_inactivate(&mut self.homeserver);
textarea_inactivate(&mut self.username);
textarea_inactivate(&mut self.password);
SetupInputPosition::Ok
},
SetupInputPosition::Ok => {
textarea_activate(&mut self.homeserver);
textarea_inactivate(&mut self.username);
textarea_inactivate(&mut self.password);
SetupInputPosition::Homeserver
},
};
}
pub fn input_position(&self) -> &SetupInputPosition { &self.input_position }
pub async fn update(&'_ mut self, terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
let mut strings: Vec<String> = Vec::new();
strings.resize(3, "".to_string());
let content_ok = match self.input_position {
SetupInputPosition:: Ok => Span::styled("OK", Style::default().add_modifier(Modifier::UNDERLINED)),
_ => Span::styled("OK", Style::default().fg(Color::DarkGray)),
};
let block = Block::default()
.title("Login")
.borders(Borders::ALL);
let mut ok = Paragraph::new(content_ok)
.alignment(Alignment::Center);
// define a 32 * 6 chunk in the middle of the screen
let mut chunk = terminal.size()?;
chunk.x = (chunk.width / 2) - 16;
chunk.y = (chunk.height / 2) - 5;
chunk.height = 12;
chunk.width = 32;
let mut split_chunk = chunk.clone();
split_chunk.x += 1;
split_chunk.y += 1;
split_chunk.height -= 1;
split_chunk.width -= 2;
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), // 0. Homserver:
Constraint::Length(3), // 1. Username:
Constraint::Length(3), // 2. Password:
Constraint::Length(1) // 3. OK
].as_ref())
.split(split_chunk);
terminal.draw(|frame| {
frame.render_widget(block.clone(), chunk);
frame.render_widget(self.homeserver.widget(), chunks[0]);
frame.render_widget(self.username.widget(), chunks[1]);
frame.render_widget(self.password.widget(), chunks[2]);
frame.render_widget(ok.clone(), chunks[3]);
})?;
Ok(())
}
}
impl UI<'_> {
pub fn new() -> Self {
let stdout = terminal_prepare().expect("failed to prepare terminal");
@ -117,10 +232,23 @@ impl UI<'_> {
terminal,
input_position: MainInputPosition::Rooms,
message_compose,
setup_ui: None
}
}
pub fn main_update(&'_ mut self, app: &App) -> Result<()> {
pub fn cycle_main_input_position(&mut self) {
self.input_position = match self.input_position {
MainInputPosition::Status => MainInputPosition::Rooms,
MainInputPosition::Rooms => MainInputPosition::Messages,
MainInputPosition::Messages => MainInputPosition::MessageCompose,
MainInputPosition::MessageCompose => MainInputPosition::RoomInfo,
MainInputPosition::RoomInfo => MainInputPosition::Status,
};
}
pub fn input_position(&self) -> &MainInputPosition { &self.input_position }
pub async fn update(&'_ mut self, status: &Status) -> Result<()> {
// TODO: make thread safe
let chunk = self.terminal.size()?;
@ -145,30 +273,27 @@ impl UI<'_> {
.constraints([Constraint::Min(4)].as_ref())
.split(main_chunks[2]);
// collect data to render
let account = app.accounts_manager.current().expect("failed to resolve current account");
let mut status_content = Text::styled(account.name(), Style::default().add_modifier(Modifier::BOLD));
status_content.extend(Text::styled(account.user_id(), Style::default()));
let mut status_content = Text::styled(status.account_name(), Style::default().add_modifier(Modifier::BOLD));
status_content.extend(Text::styled(status.account_user_id(), Style::default()));
status_content.extend(Text::styled("settings", Style::default().fg(Color::LightMagenta).add_modifier(Modifier::ITALIC | Modifier::UNDERLINED)));
let messages_content = match app.room() {
None => {
let messages_content = match status.room() {
_ => {
vec![Spans::from(Span::styled("No room selected!", Style::default().fg(Color::Magenta)))]
},
Some(r) => {
r.messages
.iter()
.rev()
.map(|msg| {
Spans::from(vec![
Span::styled(&msg.author, Style::default().fg(Color::Cyan)),
Span::styled(": ", Style::default().fg(Color::Cyan)),
Span::styled(&msg.message, Style::default().fg(Color::White)),
])
})
.collect::<Vec<_>>()
},
// Some(r) => {
// r.messages
// .iter()
// .rev()
// .map(|msg| {
// Spans::from(vec![
// Span::styled(&msg.author, Style::default().fg(Color::Cyan)),
// Span::styled(": ", Style::default().fg(Color::Cyan)),
// Span::styled(&msg.message, Style::default().fg(Color::White)),
// ])
// })
// .collect::<Vec<_>>()
// },
};
// calculate to widgets colors, based of which widget is currently selected
@ -234,162 +359,14 @@ impl UI<'_> {
Ok(())
}
pub async fn main(&mut self, app: &mut App) -> Result<()> {
info!("Starting main UI");
pub async fn update_setup(&mut self) -> Result<()> {
let ui = match &mut self.setup_ui {
Some(c) => c,
None => return Err(Error::msg("SetupUI instance not found")),
};
textarea_activate(&mut self.message_compose);
ui.update(&mut self.terminal).await?;
self.terminal.clear().expect("failed to clear screen");
loop {
self.main_update(app)?;
match Input::from(read()?.clone()) {
Input { key: Key::Esc, .. } => return Ok(()),
Input {
key: Key::Tab,
..
} => {
self.input_position = cycle_main_input_position(&self.input_position);
}
input => {
match self.input_position {
MainInputPosition::MessageCompose => { self.message_compose.input(input); },
_ => continue,
}
}
};
}
}
pub async fn setup(&mut self, app: &mut App) -> Result<()> {
info!("Starting setup UI");
let mut input_index = SetupInputPosition::Homeserver;
let mut strings: Vec<String> = Vec::new();
strings.resize(3, "".to_string());
let content_ok_active = Span::styled("OK", Style::default().add_modifier(Modifier::UNDERLINED));
let content_ok_inactive = Span::styled("OK", Style::default().fg(Color::DarkGray));
let block = Block::default()
.title("Login")
.borders(Borders::ALL);
let mut homeserver = TextArea::new(vec!["https://matrix.org".to_string()]);
let mut username = TextArea::default();
let mut password = TextArea::default();
let mut password_data = TextArea::default();
homeserver.set_block(
Block::default()
.title("Homeserver")
.borders(Borders::ALL));
username.set_block(
Block::default()
.title("Username")
.borders(Borders::ALL));
password.set_block(
Block::default()
.title("Password")
.borders(Borders::ALL));
textarea_activate(&mut homeserver);
textarea_inactivate(&mut username);
textarea_inactivate(&mut password);
loop {
let mut ok = Paragraph::new(match input_index {
SetupInputPosition::Ok => content_ok_active.clone(),
_ => content_ok_inactive.clone(),
}).alignment(Alignment::Center);
// define a 32 * 6 chunk in the middle of the screen
let mut chunk = self.terminal.size()?;
chunk.x = (chunk.width / 2) - 16;
chunk.y = (chunk.height / 2) - 5;
chunk.height = 12;
chunk.width = 32;
let mut split_chunk = chunk.clone();
split_chunk.x += 1;
split_chunk.y += 1;
split_chunk.height -= 1;
split_chunk.width -= 2;
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), // 0. Homserver:
Constraint::Length(3), // 1. Username:
Constraint::Length(3), // 2. Password:
Constraint::Length(1) // 3. OK
].as_ref())
.split(split_chunk);
self.terminal.draw(|frame| {
frame.render_widget(block.clone(), chunk);
frame.render_widget(homeserver.widget(), chunks[0]);
frame.render_widget(username.widget(), chunks[1]);
frame.render_widget(password.widget(), chunks[2]);
frame.render_widget(ok.clone(), chunks[3]);
})?;
match Input::from(read()?.clone()) {
Input { key: Key::Esc, .. } => return Err(Error::msg("Login cancelled by user")),
Input {
key: Key::Enter,
..
} => {
input_index = match input_index {
SetupInputPosition::Homeserver => {
textarea_inactivate(&mut homeserver);
textarea_activate(&mut username);
textarea_inactivate(&mut password);
SetupInputPosition::Username
},
SetupInputPosition::Username => {
textarea_inactivate(&mut homeserver);
textarea_inactivate(&mut username);
textarea_activate(&mut password);
SetupInputPosition::Password
},
SetupInputPosition::Password => {
textarea_inactivate(&mut homeserver);
textarea_inactivate(&mut username);
textarea_inactivate(&mut password);
SetupInputPosition::Ok
},
SetupInputPosition::Ok => {
if app.accounts_manager.add(&homeserver.lines()[0], &username.lines()[0], &password_data.lines()[0]).await.is_ok() {
return Ok(())
}
textarea_activate(&mut homeserver);
textarea_inactivate(&mut username);
textarea_inactivate(&mut password);
SetupInputPosition::Homeserver
},
}
}
input => {
match input_index {
SetupInputPosition::Homeserver => { homeserver.input(input); },
SetupInputPosition::Username => { username.input(input); },
SetupInputPosition::Password => {
password_data.input(input.clone());
match input.key {
Char(_) => { password.input(Input { key: Char('*'), ctrl: false, alt: false }); },
_ => { password.input(input); },
}
},
_ => continue,
}
}
};
}
Ok(())
}
}