refactor (architecture): implemented an event based architecture
This commit is contained in:
parent
6ba8d3dbf0
commit
dfc87ff937
|
@ -2651,6 +2651,7 @@ dependencies = [
|
||||||
"matrix-sdk",
|
"matrix-sdk",
|
||||||
"serde",
|
"serde",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
"tui",
|
"tui",
|
||||||
"tui-textarea",
|
"tui-textarea",
|
||||||
]
|
]
|
||||||
|
|
|
@ -13,5 +13,6 @@ crossterm = "*"
|
||||||
matrix-sdk = "0.6"
|
matrix-sdk = "0.6"
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
|
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
|
||||||
|
tokio-util = "0.7"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
cli-log = "2.0"
|
cli-log = "2.0"
|
|
@ -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(())
|
||||||
|
}
|
133
src/app/mod.rs
133
src/app/mod.rs
|
@ -1,28 +1,43 @@
|
||||||
|
pub mod event;
|
||||||
|
pub mod status;
|
||||||
|
|
||||||
use crate::accounts;
|
use crate::accounts;
|
||||||
|
use crate::ui;
|
||||||
|
|
||||||
use std::path::Path;
|
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::Account;
|
||||||
use accounts::AccountsManager;
|
use accounts::AccountsManager;
|
||||||
|
use anyhow::{Result, Error};
|
||||||
use cli_log::{error, warn, info};
|
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 struct App<'a> {
|
||||||
pub message: String,
|
ui: ui::UI<'a>,
|
||||||
|
accounts_manager: accounts::AccountsManager,
|
||||||
|
status: Status,
|
||||||
|
|
||||||
|
input_listener_killer: CancellationToken,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Room {
|
impl Drop for App<'_> {
|
||||||
pub messages: Vec<Message>,
|
fn drop(&mut self) {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct App {
|
impl App<'_> {
|
||||||
pub accounts_manager: accounts::AccountsManager,
|
|
||||||
client: Option<Client>,
|
|
||||||
current_room_id: u32,
|
|
||||||
rooms: Vec<Room>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl App {
|
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let path:&std::path::Path = Path::new("userdata/accounts.json");
|
let path:&std::path::Path = Path::new("userdata/accounts.json");
|
||||||
let config = if path.exists() {
|
let config = if path.exists() {
|
||||||
|
@ -33,27 +48,89 @@ impl App {
|
||||||
};
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
ui: ui::UI::new(),
|
||||||
accounts_manager: AccountsManager::new(config),
|
accounts_manager: AccountsManager::new(config),
|
||||||
client: None,
|
status: Status::default(),
|
||||||
current_room_id: 0,
|
input_listener_killer: CancellationToken::new(),
|
||||||
rooms: Vec::new(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fill_test_data(&mut self) {
|
pub async fn run(&mut self) -> Result<()> {
|
||||||
let mut room = Room {
|
let (channel_tx, mut channel_rx) = mpsc::channel(256);
|
||||||
messages: Vec::new()
|
|
||||||
};
|
|
||||||
|
|
||||||
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> {
|
async fn setup(&mut self, receiver: &mut mpsc::Receiver<event::Event>) -> Result<()> {
|
||||||
self.rooms.get(self.current_room_id as usize)
|
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, ()> {
|
pub fn account(&self) -> Result<&Account, ()> {
|
||||||
|
@ -63,4 +140,8 @@ impl App {
|
||||||
Some(a) => Ok(a)
|
Some(a) => Ok(a)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn client(&self) -> &Option<Client> {
|
||||||
|
self.accounts_manager.client()
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
10
src/main.rs
10
src/main.rs
|
@ -9,15 +9,7 @@ async fn main() -> anyhow::Result<()> {
|
||||||
cli_log::init_cli_log!();
|
cli_log::init_cli_log!();
|
||||||
|
|
||||||
let mut app = app::App::new();
|
let mut app = app::App::new();
|
||||||
app.fill_test_data();
|
app.run().await?;
|
||||||
|
|
||||||
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?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
365
src/ui/mod.rs
365
src/ui/mod.rs
|
@ -1,4 +1,4 @@
|
||||||
use crate::app::{App};
|
use crate::app::status::Status;
|
||||||
|
|
||||||
use std::cmp;
|
use std::cmp;
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
|
@ -16,11 +16,10 @@ use tui::style::{Color, Modifier, Style};
|
||||||
use tui::text::{Spans, Span, Text};
|
use tui::text::{Spans, Span, Text};
|
||||||
use tui::widgets::{Paragraph, Wrap};
|
use tui::widgets::{Paragraph, Wrap};
|
||||||
use tui_textarea::{Input, Key, TextArea};
|
use tui_textarea::{Input, Key, TextArea};
|
||||||
use tui_textarea::Key::Char;
|
|
||||||
use cli_log::{error, warn, info};
|
use cli_log::{error, warn, info};
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
enum SetupInputPosition {
|
pub enum SetupInputPosition {
|
||||||
Homeserver,
|
Homeserver,
|
||||||
Username,
|
Username,
|
||||||
Password,
|
Password,
|
||||||
|
@ -28,7 +27,7 @@ enum SetupInputPosition {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
enum MainInputPosition {
|
pub enum MainInputPosition {
|
||||||
Status,
|
Status,
|
||||||
Rooms,
|
Rooms,
|
||||||
Messages,
|
Messages,
|
||||||
|
@ -36,11 +35,21 @@ enum MainInputPosition {
|
||||||
RoomInfo
|
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> {
|
pub struct UI<'a> {
|
||||||
terminal: Terminal<CrosstermBackend<Stdout>>,
|
terminal: Terminal<CrosstermBackend<Stdout>>,
|
||||||
|
|
||||||
input_position: MainInputPosition,
|
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)
|
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_line_style(Style::default().add_modifier(Modifier::UNDERLINED));
|
||||||
textarea.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
|
textarea.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
|
||||||
let b = textarea
|
let b = textarea
|
||||||
|
@ -62,7 +71,7 @@ fn textarea_activate(textarea: &mut TextArea) {
|
||||||
textarea.set_block(b.style(Style::default()));
|
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_line_style(Style::default());
|
||||||
textarea.set_cursor_style(Style::default());
|
textarea.set_cursor_style(Style::default());
|
||||||
let b = textarea
|
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<'_> {
|
impl Drop for UI<'_> {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
info!("Destructing UI");
|
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<'_> {
|
impl UI<'_> {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let stdout = terminal_prepare().expect("failed to prepare terminal");
|
let stdout = terminal_prepare().expect("failed to prepare terminal");
|
||||||
|
@ -117,10 +232,23 @@ impl UI<'_> {
|
||||||
terminal,
|
terminal,
|
||||||
input_position: MainInputPosition::Rooms,
|
input_position: MainInputPosition::Rooms,
|
||||||
message_compose,
|
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
|
// TODO: make thread safe
|
||||||
|
|
||||||
let chunk = self.terminal.size()?;
|
let chunk = self.terminal.size()?;
|
||||||
|
@ -145,30 +273,27 @@ impl UI<'_> {
|
||||||
.constraints([Constraint::Min(4)].as_ref())
|
.constraints([Constraint::Min(4)].as_ref())
|
||||||
.split(main_chunks[2]);
|
.split(main_chunks[2]);
|
||||||
|
|
||||||
// collect data to render
|
let mut status_content = Text::styled(status.account_name(), Style::default().add_modifier(Modifier::BOLD));
|
||||||
let account = app.accounts_manager.current().expect("failed to resolve current account");
|
status_content.extend(Text::styled(status.account_user_id(), Style::default()));
|
||||||
|
|
||||||
let mut status_content = Text::styled(account.name(), Style::default().add_modifier(Modifier::BOLD));
|
|
||||||
status_content.extend(Text::styled(account.user_id(), Style::default()));
|
|
||||||
status_content.extend(Text::styled("settings", Style::default().fg(Color::LightMagenta).add_modifier(Modifier::ITALIC | Modifier::UNDERLINED)));
|
status_content.extend(Text::styled("settings", Style::default().fg(Color::LightMagenta).add_modifier(Modifier::ITALIC | Modifier::UNDERLINED)));
|
||||||
|
|
||||||
let messages_content = match app.room() {
|
let messages_content = match status.room() {
|
||||||
None => {
|
_ => {
|
||||||
vec![Spans::from(Span::styled("No room selected!", Style::default().fg(Color::Magenta)))]
|
vec![Spans::from(Span::styled("No room selected!", Style::default().fg(Color::Magenta)))]
|
||||||
},
|
},
|
||||||
Some(r) => {
|
// Some(r) => {
|
||||||
r.messages
|
// r.messages
|
||||||
.iter()
|
// .iter()
|
||||||
.rev()
|
// .rev()
|
||||||
.map(|msg| {
|
// .map(|msg| {
|
||||||
Spans::from(vec![
|
// Spans::from(vec![
|
||||||
Span::styled(&msg.author, Style::default().fg(Color::Cyan)),
|
// Span::styled(&msg.author, Style::default().fg(Color::Cyan)),
|
||||||
Span::styled(": ", Style::default().fg(Color::Cyan)),
|
// Span::styled(": ", Style::default().fg(Color::Cyan)),
|
||||||
Span::styled(&msg.message, Style::default().fg(Color::White)),
|
// Span::styled(&msg.message, Style::default().fg(Color::White)),
|
||||||
])
|
// ])
|
||||||
})
|
// })
|
||||||
.collect::<Vec<_>>()
|
// .collect::<Vec<_>>()
|
||||||
},
|
// },
|
||||||
};
|
};
|
||||||
|
|
||||||
// calculate to widgets colors, based of which widget is currently selected
|
// calculate to widgets colors, based of which widget is currently selected
|
||||||
|
@ -234,162 +359,14 @@ impl UI<'_> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn main(&mut self, app: &mut App) -> Result<()> {
|
pub async fn update_setup(&mut self) -> Result<()> {
|
||||||
info!("Starting main UI");
|
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");
|
Ok(())
|
||||||
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Reference in New Issue