Refactor(events): Split up event handling into multiple files

This commit is contained in:
Benedikt Peetz 2023-07-12 21:35:46 +02:00
parent 05d4b4d097
commit ef5afcda02
Signed by: bpeetz
GPG Key ID: A5E94010C3A642AD
12 changed files with 432 additions and 408 deletions

View File

@ -1,384 +0,0 @@
use anyhow::{Error, Result};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use matrix_sdk::{config::SyncSettings, Client, LoopCtrl};
use tokio::{sync::mpsc, time::Duration};
use tokio_util::sync::CancellationToken;
use crate::{
app::{status::State, App},
ui,
};
#[derive(Debug)]
pub enum EventStatus {
Ok,
Finished,
Terminate,
}
#[derive(Debug)]
pub struct Event {
input_event: Option<crossterm::event::Event>,
matrix_event: Option<matrix_sdk::deserialized_responses::SyncResponse>,
}
pub struct EventBuilder {
event: Event,
}
impl Default for Event {
fn default() -> Self {
Self {
input_event: None,
matrix_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 matrix_event(
&mut self,
matrix_event: matrix_sdk::deserialized_responses::SyncResponse,
) -> &Self {
self.event.matrix_event = Some(matrix_event);
self
}
fn build(&self) -> Event {
Event {
input_event: self.event.input_event.clone(),
matrix_event: self.event.matrix_event.clone(),
}
}
}
impl Event {
pub async fn handle(&self, app: &mut App<'_>) -> Result<EventStatus> {
if self.matrix_event.is_some() {
return self.handle_matrix(app).await;
}
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_matrix(&self, app: &mut App<'_>) -> Result<EventStatus> {
let sync = self.matrix_event.clone().unwrap();
for (m_room_id, m_room) in sync.rooms.join.iter() {
let room = match app.status.get_room_mut(m_room_id) {
Some(r) => r,
None => continue,
};
for m_event in m_room.timeline.events.clone() {
let event = m_event
.event
.deserialize()
.unwrap()
.into_full_event(m_room_id.clone());
room.timeline_add(event);
}
}
Ok(EventStatus::Ok)
}
async fn handle_main(&self, app: &mut App<'_>) -> Result<EventStatus> {
if self.input_event.is_some() {
match self.input_event.clone().unwrap() {
crossterm::event::Event::Key(KeyEvent {
code: KeyCode::Esc, ..
}) => return Ok(EventStatus::Terminate),
crossterm::event::Event::Key(KeyEvent {
code: KeyCode::Tab, ..
}) => {
app.ui.cycle_main_input_position();
}
crossterm::event::Event::Key(KeyEvent {
code: KeyCode::BackTab,
..
}) => {
app.ui.cycle_main_input_position_rev();
}
crossterm::event::Event::Key(KeyEvent {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
..
}) => {
app.ui.cli_enable();
}
input => match app.ui.input_position() {
ui::MainInputPosition::MessageCompose => {
match input {
crossterm::event::Event::Key(KeyEvent {
code: KeyCode::Enter,
modifiers: KeyModifiers::ALT,
..
}) => {
match app.status.room_mut() {
Some(room) => {
room.send(app.ui.message_compose.lines().join("\n"))
.await?;
app.ui.message_compose_clear();
}
None => (),
};
}
_ => {
app.ui
.message_compose
.input(tui_textarea::Input::from(input));
}
};
}
ui::MainInputPosition::Rooms => {
match input {
crossterm::event::Event::Key(KeyEvent {
code: KeyCode::Up, ..
}) => {
let i = match app.ui.rooms_state.selected() {
Some(cur) => {
if cur > 0 {
cur - 1
} else {
cur
}
}
None => 0,
};
app.ui.rooms_state.select(Some(i));
app.status.set_room_by_index(i)?;
}
crossterm::event::Event::Key(KeyEvent {
code: KeyCode::Down,
..
}) => {
let i = match app.ui.rooms_state.selected() {
Some(cur) => {
if cur < app.status.rooms().len() - 1 {
cur + 1
} else {
cur
}
}
None => 0,
};
app.ui.rooms_state.select(Some(i));
app.status.set_room_by_index(i)?;
}
_ => (),
};
}
ui::MainInputPosition::Messages => {
match input {
crossterm::event::Event::Key(KeyEvent {
code: KeyCode::Up, ..
}) => {
match app.status.room_mut() {
Some(room) => {
let len = room.timeline().len();
let i = match room.view_scroll() {
Some(i) => i + 1,
None => 0,
};
if i < len {
room.set_view_scroll(Some(i))
}
if i <= len - 5 {
room.poll_old_timeline().await?;
}
}
None => (),
};
}
crossterm::event::Event::Key(KeyEvent {
code: KeyCode::Down,
..
}) => {
match app.status.room_mut() {
Some(room) => {
match room.view_scroll() {
Some(i) => {
if i == 0 {
room.set_view_scroll(None);
} else {
room.set_view_scroll(Some(i - 1));
}
}
None => (),
};
}
None => (),
};
}
_ => (),
};
}
ui::MainInputPosition::CLI => {
if let Some(cli) = &mut app.ui.cli {
match input {
crossterm::event::Event::Key(KeyEvent {
code: KeyCode::Enter,
..
}) => {
let cli_event = cli.lines()[0].clone();
app.status.cli_event(cli_event);
app.ui.cli_disable();
}
_ => {
cli.input(tui_textarea::Input::from(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 self.input_event.clone().unwrap() {
crossterm::event::Event::Key(KeyEvent {
code: KeyCode::Esc, ..
}) => return Ok(EventStatus::Terminate),
crossterm::event::Event::Key(KeyEvent {
code: KeyCode::Tab, ..
}) => {
ui.cycle_input_position();
}
crossterm::event::Event::Key(KeyEvent {
code: KeyCode::BackTab,
..
}) => {
ui.cycle_input_position_rev();
}
crossterm::event::Event::Key(KeyEvent {
code: KeyCode::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 => {
let textarea_input = tui_textarea::Input::from(input);
ui.password_data.input(textarea_input.clone());
match textarea_input.key {
tui_textarea::Key::Char(_) => {
ui.password.input(tui_textarea::Input {
key: tui_textarea::Key::Char('*'),
ctrl: false,
alt: false,
});
}
_ => {
ui.password.input(textarea_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))? {
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"))
}
}
async fn poll_matrix_events_stage_2(channel: mpsc::Sender<Event>, client: Client) -> Result<()> {
let sync_settings = SyncSettings::default();
// .token(sync_token)
// .timeout(Duration::from_secs(30));
let tx = &channel;
client
.sync_with_callback(sync_settings, |response| async move {
let event = EventBuilder::default().matrix_event(response).build();
match tx.send(event).await {
Ok(_) => LoopCtrl::Continue,
Err(_) => LoopCtrl::Break,
}
})
.await?;
Ok(())
}
pub async fn poll_matrix_events(
channel: mpsc::Sender<Event>,
kill: CancellationToken,
client: Client,
) -> Result<()> {
tokio::select! {
output = poll_matrix_events_stage_2(channel, client) => output,
_ = kill.cancelled() => Err(Error::msg("received kill signal")),
}
}

View File

@ -0,0 +1,145 @@
use anyhow::Result;
use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEvent, KeyModifiers};
use crate::{
app::{events::event_types::EventStatus, App},
ui,
};
pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<EventStatus> {
match input_event {
CrosstermEvent::Key(KeyEvent {
code: KeyCode::Esc, ..
}) => return Ok(EventStatus::Terminate),
CrosstermEvent::Key(KeyEvent {
code: KeyCode::Tab, ..
}) => {
app.ui.cycle_main_input_position();
}
CrosstermEvent::Key(KeyEvent {
code: KeyCode::BackTab,
..
}) => {
app.ui.cycle_main_input_position_rev();
}
CrosstermEvent::Key(KeyEvent {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
..
}) => {
app.ui.cli_enable();
}
input => match app.ui.input_position() {
ui::MainInputPosition::MessageCompose => {
match input {
CrosstermEvent::Key(KeyEvent {
code: KeyCode::Enter,
modifiers: KeyModifiers::ALT,
..
}) => {
match app.status.room_mut() {
Some(room) => {
room.send(app.ui.message_compose.lines().join("\n")).await?;
app.ui.message_compose_clear();
}
None => (),
};
}
_ => {
app.ui
.message_compose
.input(tui_textarea::Input::from(input.to_owned()));
}
};
}
ui::MainInputPosition::Rooms => {
match input {
CrosstermEvent::Key(KeyEvent {
code: KeyCode::Up, ..
}) => {
let i = match app.ui.rooms_state.selected() {
Some(cur) => {
if cur > 0 {
cur - 1
} else {
cur
}
}
None => 0,
};
app.ui.rooms_state.select(Some(i));
app.status.set_room_by_index(i)?;
}
CrosstermEvent::Key(KeyEvent {
code: KeyCode::Down,
..
}) => {
let i = match app.ui.rooms_state.selected() {
Some(cur) => {
if cur < app.status.rooms().len() - 1 {
cur + 1
} else {
cur
}
}
None => 0,
};
app.ui.rooms_state.select(Some(i));
app.status.set_room_by_index(i)?;
}
_ => (),
};
}
ui::MainInputPosition::Messages => {
match input {
CrosstermEvent::Key(KeyEvent {
code: KeyCode::Up, ..
}) => {
match app.status.room_mut() {
Some(room) => {
let len = room.timeline().len();
let i = match room.view_scroll() {
Some(i) => i + 1,
None => 0,
};
if i < len {
room.set_view_scroll(Some(i))
}
if i <= len - 5 {
room.poll_old_timeline().await?;
}
}
None => (),
};
}
CrosstermEvent::Key(KeyEvent {
code: KeyCode::Down,
..
}) => {
match app.status.room_mut() {
Some(room) => {
match room.view_scroll() {
Some(i) => {
if i == 0 {
room.set_view_scroll(None);
} else {
room.set_view_scroll(Some(i - 1));
}
}
None => (),
};
}
None => (),
};
}
_ => (),
};
}
ui::MainInputPosition::CLI => {
todo!();
}
_ => (),
},
};
Ok(EventStatus::Ok)
}

View File

@ -0,0 +1,23 @@
use matrix_sdk::deserialized_responses::SyncResponse;
use anyhow::Result;
use crate::app::{events::event_types::EventStatus, App};
pub async fn handle<'a>(app: &mut App<'a>, sync: &SyncResponse) -> Result<EventStatus> {
for (m_room_id, m_room) in sync.rooms.join.iter() {
let room = match app.status.get_room_mut(m_room_id) {
Some(r) => r,
None => continue,
};
for m_event in m_room.timeline.events.clone() {
let event = m_event
.event
.deserialize()
.unwrap()
.into_full_event(m_room_id.clone());
room.timeline_add(event);
}
}
Ok(EventStatus::Ok)
}

View File

@ -0,0 +1,3 @@
pub mod matrix;
pub mod setup;
pub mod main;

View File

@ -0,0 +1,75 @@
use anyhow::{bail, Context, Result};
use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEvent};
use crate::{
app::{events::event_types::EventStatus, App},
ui,
};
pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<EventStatus> {
let ui = match &mut app.ui.setup_ui {
Some(ui) => ui,
None => bail!("SetupUI instance not found"),
};
match input_event {
CrosstermEvent::Key(KeyEvent {
code: KeyCode::Esc, ..
}) => return Ok(EventStatus::Terminate),
CrosstermEvent::Key(KeyEvent {
code: KeyCode::Tab, ..
}) => {
ui.cycle_input_position();
}
CrosstermEvent::Key(KeyEvent {
code: KeyCode::BackTab,
..
}) => {
ui.cycle_input_position_rev();
}
CrosstermEvent::Key(KeyEvent {
code: KeyCode::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();
app.login(&homeserver, &username, &password)
.await
.context("Failed to login")?;
// We bailed in the line above, thus login must have succeeded
return Ok(EventStatus::Finished);
}
_ => ui.cycle_input_position(),
};
}
input => match ui.input_position() {
ui::SetupInputPosition::Homeserver => {
ui.homeserver.input(input.to_owned());
}
ui::SetupInputPosition::Username => {
ui.username.input(input.to_owned());
}
ui::SetupInputPosition::Password => {
let textarea_input = tui_textarea::Input::from(input.to_owned());
ui.password_data.input(textarea_input.clone());
match textarea_input.key {
tui_textarea::Key::Char(_) => {
ui.password.input(tui_textarea::Input {
key: tui_textarea::Key::Char('*'),
ctrl: false,
alt: false,
});
}
_ => {
ui.password.input(textarea_input);
}
}
}
_ => (),
},
};
Ok(EventStatus::Ok)
}

View File

@ -0,0 +1,41 @@
mod handlers;
use anyhow::{Context, Result};
use crossterm::event::Event as CrosstermEvent;
use crate::app::{status::State, App};
use self::handlers::{main, matrix, setup};
use super::EventStatus;
#[derive(Debug)]
pub struct Event {
pub(super) input_event: Option<CrosstermEvent>,
pub(super) matrix_event: Option<matrix_sdk::deserialized_responses::SyncResponse>,
}
impl Event {
pub async fn handle(&self, app: &mut App<'_>) -> Result<EventStatus> {
if let Some(matrix_event) = &self.matrix_event {
return matrix::handle(app, matrix_event)
.await
.with_context(|| format!("Failed to handle matrix event: `{:#?}`", matrix_event));
}
if let Some(input_event) = &self.input_event {
let status = match app.status.state() {
State::None => EventStatus::Ok,
State::Main => main::handle(app, input_event).await.with_context(|| {
format!("Failed to handle input event: `{:#?}`", input_event)
})?,
State::Setup => setup::handle(app, input_event).await.with_context(|| {
format!("Failed to handle input event: `{:#?}`", input_event)
})?,
};
return Ok(status);
}
Ok(EventStatus::Ok)
}
}

View File

@ -0,0 +1,44 @@
use super::Event;
pub struct EventBuilder {
event: Event,
}
impl Default for Event {
fn default() -> Self {
Self {
input_event: None,
matrix_event: None,
}
}
}
impl Default for EventBuilder {
fn default() -> Self {
Self {
event: Event::default(),
}
}
}
impl EventBuilder {
pub fn input_event(&mut self, input_event: crossterm::event::Event) -> &Self {
self.event.input_event = Some(input_event);
self
}
pub fn matrix_event(
&mut self,
matrix_event: matrix_sdk::deserialized_responses::SyncResponse,
) -> &Self {
self.event.matrix_event = Some(matrix_event);
self
}
pub fn build(&self) -> Event {
Event {
input_event: self.event.input_event.to_owned(),
matrix_event: self.event.matrix_event.to_owned(),
}
}
}

View File

@ -0,0 +1,6 @@
#[derive(Debug)]
pub enum EventStatus {
Ok,
Finished,
Terminate,
}

View File

@ -0,0 +1,8 @@
pub mod event_builder;
pub mod event;
pub mod event_status;
pub use self::event_builder::*;
pub use self::event::*;
pub use self::event_status::*;

67
src/app/events/mod.rs Normal file
View File

@ -0,0 +1,67 @@
pub mod event_types;
use anyhow::{bail, Result};
use matrix_sdk::{config::SyncSettings, Client, LoopCtrl};
use tokio::{sync::mpsc, time::Duration};
use tokio_util::sync::CancellationToken;
use crate::app::events::event_types::EventBuilder;
use self::event_types::Event;
pub async fn poll_input_events(
channel: mpsc::Sender<Event>,
kill: CancellationToken,
) -> Result<()> {
async fn poll_input_events_stage_2(channel: mpsc::Sender<Event>) -> Result<()> {
loop {
if crossterm::event::poll(Duration::from_millis(100))? {
let event = EventBuilder::default()
.input_event(crossterm::event::read()?)
.build();
channel.send(event).await?;
} else {
tokio::task::yield_now().await;
}
}
}
tokio::select! {
output = poll_input_events_stage_2(channel) => output,
_ = kill.cancelled() => bail!("received kill signal")
}
}
pub async fn poll_matrix_events(
channel: mpsc::Sender<Event>,
kill: CancellationToken,
client: Client,
) -> Result<()> {
async fn poll_matrix_events_stage_2(
channel: mpsc::Sender<Event>,
client: Client,
) -> Result<()> {
let sync_settings = SyncSettings::default();
// .token(sync_token)
// .timeout(Duration::from_secs(30));
let tx = &channel;
client
.sync_with_callback(sync_settings, |response| async move {
let event = EventBuilder::default().matrix_event(response).build();
match tx.send(event).await {
Ok(_) => LoopCtrl::Continue,
Err(_) => LoopCtrl::Break,
}
})
.await?;
Ok(())
}
tokio::select! {
output = poll_matrix_events_stage_2(channel, client) => output,
_ = kill.cancelled() => bail!("received kill signal"),
}
}

View File

@ -1,33 +1,33 @@
pub mod event;
pub mod command_interface;
pub mod events;
pub mod status;
use std::path::Path;
use accounts::{Account, AccountsManager};
use anyhow::{Error, Result};
use anyhow::{Context, Error, Result};
use cli_log::info;
use matrix_sdk::Client;
use rlua::Lua;
use status::{State, Status};
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
use crate::{accounts, ui};
use crate::{accounts, app::command_interface::generate_ci_functions, ui};
pub struct App<'a> {
ui: ui::UI<'a>,
use self::events::event_types;
pub struct App<'ui> {
ui: ui::UI<'ui>,
accounts_manager: accounts::AccountsManager,
status: Status,
channel_tx: mpsc::Sender<event::Event>,
channel_rx: mpsc::Receiver<event::Event>,
channel_tx: mpsc::Sender<event_types::Event>,
channel_rx: mpsc::Receiver<event_types::Event>,
input_listener_killer: CancellationToken,
matrix_listener_killer: CancellationToken,
}
impl Drop for App<'_> {
fn drop(&mut self) {}
}
impl App<'_> {
pub fn new() -> Result<Self> {
let path: &std::path::Path = Path::new("userdata/accounts.json");
@ -54,7 +54,7 @@ impl App<'_> {
pub async fn run(&mut self) -> Result<()> {
// Spawn input event listener
tokio::task::spawn(event::poll_input_events(
tokio::task::spawn(events::poll_input_events(
self.channel_tx.clone(),
self.input_listener_killer.clone(),
));
@ -71,14 +71,14 @@ impl App<'_> {
self.status.set_state(State::Main);
self.ui.update(&self.status).await?;
let event: event::Event = match self.channel_rx.recv().await {
let event: event_types::Event = match self.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,
event_types::EventStatus::Ok => (),
event_types::EventStatus::Terminate => break,
_ => (),
};
}
@ -94,15 +94,15 @@ impl App<'_> {
self.status.set_state(State::Setup);
self.ui.update_setup().await?;
let event: event::Event = match self.channel_rx.recv().await {
let event: event_types::Event = match self.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::Finished => return Ok(()),
event::EventStatus::Terminate => return Err(Error::msg("Terminated by user")),
event_types::EventStatus::Ok => (),
event_types::EventStatus::Finished => return Ok(()),
event_types::EventStatus::Terminate => return Err(Error::msg("Terminated by user")),
}
}
}
@ -118,7 +118,7 @@ impl App<'_> {
self.matrix_listener_killer = CancellationToken::new();
// Spawn Matrix Event Listener
tokio::task::spawn(event::poll_matrix_events(
tokio::task::spawn(events::poll_matrix_events(
self.channel_tx.clone(),
self.matrix_listener_killer.clone(),
client.clone(),

View File

@ -204,8 +204,4 @@ impl Status {
pub fn set_state(&mut self, state: State) {
self.state = state;
}
pub fn cli_event(&mut self, event: String) {
info!("CLI Event: {}", event);
}
}