diff --git a/src/app/event.rs b/src/app/event.rs deleted file mode 100644 index b7d14b6..0000000 --- a/src/app/event.rs +++ /dev/null @@ -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, - matrix_event: Option, -} - -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 { - 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 { - 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 { - 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 { - 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) -> 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, - 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, 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, - kill: CancellationToken, - client: Client, -) -> Result<()> { - tokio::select! { - output = poll_matrix_events_stage_2(channel, client) => output, - _ = kill.cancelled() => Err(Error::msg("received kill signal")), - } -} diff --git a/src/app/events/event_types/event/handlers/main.rs b/src/app/events/event_types/event/handlers/main.rs new file mode 100644 index 0000000..410e2ed --- /dev/null +++ b/src/app/events/event_types/event/handlers/main.rs @@ -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 { + 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) +} diff --git a/src/app/events/event_types/event/handlers/matrix.rs b/src/app/events/event_types/event/handlers/matrix.rs new file mode 100644 index 0000000..5215a28 --- /dev/null +++ b/src/app/events/event_types/event/handlers/matrix.rs @@ -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 { + 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) +} diff --git a/src/app/events/event_types/event/handlers/mod.rs b/src/app/events/event_types/event/handlers/mod.rs new file mode 100644 index 0000000..0d96b56 --- /dev/null +++ b/src/app/events/event_types/event/handlers/mod.rs @@ -0,0 +1,3 @@ +pub mod matrix; +pub mod setup; +pub mod main; diff --git a/src/app/events/event_types/event/handlers/setup.rs b/src/app/events/event_types/event/handlers/setup.rs new file mode 100644 index 0000000..38cafb2 --- /dev/null +++ b/src/app/events/event_types/event/handlers/setup.rs @@ -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 { + 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) +} diff --git a/src/app/events/event_types/event/mod.rs b/src/app/events/event_types/event/mod.rs new file mode 100644 index 0000000..686d99c --- /dev/null +++ b/src/app/events/event_types/event/mod.rs @@ -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, + pub(super) matrix_event: Option, +} + +impl Event { + pub async fn handle(&self, app: &mut App<'_>) -> Result { + 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) + } +} diff --git a/src/app/events/event_types/event_builder.rs b/src/app/events/event_types/event_builder.rs new file mode 100644 index 0000000..36177e6 --- /dev/null +++ b/src/app/events/event_types/event_builder.rs @@ -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(), + } + } +} diff --git a/src/app/events/event_types/event_status.rs b/src/app/events/event_types/event_status.rs new file mode 100644 index 0000000..d7642cb --- /dev/null +++ b/src/app/events/event_types/event_status.rs @@ -0,0 +1,6 @@ +#[derive(Debug)] +pub enum EventStatus { + Ok, + Finished, + Terminate, +} diff --git a/src/app/events/event_types/mod.rs b/src/app/events/event_types/mod.rs new file mode 100644 index 0000000..48a0186 --- /dev/null +++ b/src/app/events/event_types/mod.rs @@ -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::*; + diff --git a/src/app/events/mod.rs b/src/app/events/mod.rs new file mode 100644 index 0000000..830cd61 --- /dev/null +++ b/src/app/events/mod.rs @@ -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, + kill: CancellationToken, +) -> Result<()> { + async fn poll_input_events_stage_2(channel: mpsc::Sender) -> 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, + kill: CancellationToken, + client: Client, +) -> Result<()> { + async fn poll_matrix_events_stage_2( + channel: mpsc::Sender, + 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"), + } +} diff --git a/src/app/mod.rs b/src/app/mod.rs index 4cb6ed6..126b757 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -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, - channel_rx: mpsc::Receiver, + channel_tx: mpsc::Sender, + channel_rx: mpsc::Receiver, input_listener_killer: CancellationToken, matrix_listener_killer: CancellationToken, } -impl Drop for App<'_> { - fn drop(&mut self) {} -} - impl App<'_> { pub fn new() -> Result { 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(), diff --git a/src/app/status.rs b/src/app/status.rs index 063c20c..430f8a8 100644 --- a/src/app/status.rs +++ b/src/app/status.rs @@ -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); - } }