refactor (architecture): implemented an event based architecture
This commit is contained in:
parent
6ba8d3dbf0
commit
dfc87ff937
|
@ -2651,6 +2651,7 @@ dependencies = [
|
|||
"matrix-sdk",
|
||||
"serde",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tui",
|
||||
"tui-textarea",
|
||||
]
|
||||
|
|
|
@ -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"
|
|
@ -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::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()
|
||||
}
|
||||
}
|
|
@ -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!();
|
||||
|
||||
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(())
|
||||
}
|
||||
|
|
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 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(())
|
||||
}
|
||||
}
|
||||
|
|
Reference in New Issue