diff --git a/Cargo.toml b/Cargo.toml index 58f3d4c..cf7fd9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,20 +7,17 @@ default-run = "trinitrix" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[features] -default = ["tui"] -tui = ["dep:tui", "dep:tui-textarea", "dep:crossterm", "dep:tokio-util", "dep:serde", "dep:indexmap"] - [dependencies] clap = { version = "4.5.4", features = ["derive"] } cli-log = "2.0" anyhow = "1.0" -matrix-sdk = "0.6" -tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } +tokio = { version = "1.37", features = ["macros", "rt-multi-thread", "fs", "time"] } +tokio-util = {version = "0.7.10"} # config trinitry = {version = "0.1.0"} keymaps = {version = "0.1.1", features = ["crossterm"] } +directories = "5.0.1" # c api libloading = "0.8.3" @@ -31,13 +28,9 @@ mlua = { version = "0.9.7", features = ["lua54", "async", "send", "serialize"] } once_cell = "1.19.0" # tui feature specific parts -tui = {version = "0.19", optional = true} -tui-textarea = { version = "0.2", features = ["crossterm"], optional = true } -crossterm = { version = "0.25", optional = true } -tokio-util = { version = "0.7", optional = true } -serde = { version = "1.0", optional = true } -indexmap = { version = "2.2.6", optional = true } -directories = "5.0.1" +tui = {version = "0.19"} +tui-textarea = { version = "0.2", features = ["crossterm"]} +crossterm = { version = "0.25"} [dev-dependencies] pretty_assertions = "1.4.0" diff --git a/src/accounts/mod.rs b/src/accounts/mod.rs deleted file mode 100644 index b9311eb..0000000 --- a/src/accounts/mod.rs +++ /dev/null @@ -1,223 +0,0 @@ -use std::fs; - -use anyhow::{Error, Result}; -use cli_log::{error, info}; -use matrix_sdk::{ruma::exports::serde_json, Client, Session}; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Account { - homeserver: String, - id: u32, - name: String, - - session: Session, - sync_token: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -struct AccountsData { - current_account: u32, - accounts: Vec, -} - -pub struct AccountsManager { - current_account: u32, - num_accounts: u32, - accounts: Vec, - clients: Vec>, -} - -impl Account { - pub fn name(&self) -> &String { - &self.name - } - - pub fn user_id(&self) -> String { - self.session.user_id.to_string() - } -} - -impl AccountsManager { - pub fn new(config: Option) -> Result { - return match config { - Some(s) => { - info!("Loading serialized AccountsManager"); - let accounts_data: AccountsData = serde_json::from_str(&s)?; - let mut clients = Vec::new(); - clients.resize(accounts_data.accounts.len(), None); - Ok(Self { - current_account: accounts_data.current_account, - num_accounts: accounts_data.accounts.len() as u32, - accounts: accounts_data.accounts, - clients, - }) - } - None => { - info!("Creating empty AccountsManager"); - Ok(Self { - current_account: 0, - num_accounts: 0, - accounts: Vec::new(), - clients: Vec::new(), - }) - } - }; - } - - pub async fn restore(&mut self) -> Result<()> { - self.login(self.current_account).await?; - Ok(()) - } - - pub async fn add( - &mut self, - homeserver: &String, - username: &String, - password: &String, - ) -> Result { - let id = self.num_accounts; - self.num_accounts += 1; - - let client = Client::builder() - .homeserver_url(homeserver) - .sled_store(format!("userdata/{id}"), Some("supersecure"))? - .build() - .await?; - - client - .login_username(username, password) - .initial_device_display_name("Trinitrix") - .send() - .await?; - - let session = match client.session() { - Some(s) => s, - None => return Err(Error::msg("Failed to get session")), - }; - - let name = match client.account().get_display_name().await? { - Some(n) => n, - None => return Err(Error::msg("Failed to get display name")), - }; - - let account = Account { - homeserver: homeserver.to_string(), - id, - name, - session: session.clone(), - sync_token: None, - }; - - self.logout().await?; - self.current_account = id; - self.accounts.push(account); - self.clients.push(Some(client)); - self.save()?; - - info!( - "Logged in as '{}' device ID: {}", - session.user_id.to_string(), - session.device_id.to_string() - ); - - Ok(id) - } - - pub async fn login(&mut self, account_id: u32) -> Result<()> { - self.logout().await?; // log out the current account - - let account = if account_id >= self.num_accounts { - error!("Tried to log in with an invalid account ID {}", account_id); - return Err(Error::msg("Invalid account ID")); - } else { - if let Some(a) = self.get(account_id) { - a - } else { - return Err(Error::msg("Failed to get account")); - } - }; - - if self - .clients - .get(account_id as usize) - .expect("client lookup failed") - .is_none() - { - info!( - "No client cached for account: '{}' -> requesting a new one", - &account.session.user_id - ); - let client = Client::builder() - .homeserver_url(&account.homeserver) - .sled_store(format!("userdata/{account_id}"), Some("supersecure"))? - .build() - .await?; - client.restore_login(account.session.clone()).await?; - self.clients.insert(account_id as usize, Some(client)); - } else { - info!( - "Using cached client for account: '{}'", - &account.session.user_id - ); - }; - - info!("Restored account"); - - self.current_account = account_id; - - Ok(()) - } - - pub async fn logout(&mut self) -> Result<()> { - // idk, do some matrix related stuff in here or something like that - if self.clients.get(self.current_account as usize).is_none() { - return Ok(()); - } - let client = match self.clients.get(self.current_account as usize).unwrap() { - None => return Ok(()), - Some(c) => c, - }; - - info!("Logged out '{}' locally", client.session().unwrap().user_id); - client.logout().await?; - - Ok(()) - } - - pub fn save(&self) -> Result<()> { - let accounts_data = AccountsData { - current_account: self.current_account, - accounts: self.accounts.clone(), - }; - - let serialized = serde_json::to_string(&accounts_data)?; - fs::write("userdata/accounts.json", serialized)?; - - info!("Saved serialized accounts config (userdata/accounts.json)"); - - Ok(()) - } - - pub fn get(&self, id: u32) -> Option<&Account> { - self.accounts.get(id as usize) - } - - pub fn current(&self) -> Option<&Account> { - self.get(self.current_account) - } - - pub fn client(&self) -> Option<&Client> { - match self.clients.get(self.current_account as usize) { - None => None, - Some(oc) => match oc { - None => None, - Some(c) => Some(c), - }, - } - } - - pub fn num_accounts(&self) -> u32 { - self.num_accounts - } -} diff --git a/src/app/command_interface/command_list/api.tri b/src/app/command_interface/command_list/api.tri index 8d7f6de..47b4834 100644 --- a/src/app/command_interface/command_list/api.tri +++ b/src/app/command_interface/command_list/api.tri @@ -40,20 +40,12 @@ mod trinitrix { /// Closes the application fn exit(); - /// Send a message to the current room - /// The send message is interpreted literally. - fn room_message_send(message: String); - //// Open the help pages at the first occurrence of //// the input string if it is Some, otherwise open //// the help pages at the start // TODO(@soispha): To be implemented <2024-03-09> // fn help(Option); - //// Register a function to be used with the Trinitrix api - // (This function is not actually implemented here) - /* declare register_function: false, */ - /// Function that change the UI, or UI state mod ui { enum Mode { diff --git a/src/app/events/handlers/command.rs b/src/app/events/handlers/command.rs index c678fbb..0476b14 100644 --- a/src/app/events/handlers/command.rs +++ b/src/app/events/handlers/command.rs @@ -87,21 +87,6 @@ pub async fn handle( warn!("Terminating the application"); EventStatus::Terminate } - Api::room_message_send { message } => { - if let Some(room) = app.status.room_mut() { - room.send(message.to_string()).await?; - send_status_output!("Sent message: `{}`", message); - } else { - // FIXME(@soispha): This should raise an error within lua, as it would - // otherwise be very confusing <2023-09-20> - warn!( - "Can't send message: `{}`, as there is no open room!", - &message - ); - } - - EventStatus::Ok - } Api::Ui(ui) => match ui { Ui::set_mode { mode } => match mode { Mode::Normal => { @@ -213,7 +198,6 @@ pub async fn handle( EventStatus::Ok } State::Normal - | State::Setup | State::KeyInputPending { old_state: _, pending_keys: _, diff --git a/src/app/events/handlers/matrix.rs b/src/app/events/handlers/matrix.rs deleted file mode 100644 index 8c2b5d4..0000000 --- a/src/app/events/handlers/matrix.rs +++ /dev/null @@ -1,23 +0,0 @@ -use anyhow::Result; -use matrix_sdk::deserialized_responses::SyncResponse; - -use crate::app::{events::EventStatus, App}; - -pub async fn handle(app: &mut App<'_>, 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/handlers/mod.rs b/src/app/events/handlers/mod.rs index 4a9b144..3cf95ee 100644 --- a/src/app/events/handlers/mod.rs +++ b/src/app/events/handlers/mod.rs @@ -1,9 +1,5 @@ // input events pub mod input; -pub mod setup; - -// matrix -pub mod matrix; // ci pub mod command; diff --git a/src/app/events/handlers/setup.rs b/src/app/events/handlers/setup.rs deleted file mode 100644 index fe20751..0000000 --- a/src/app/events/handlers/setup.rs +++ /dev/null @@ -1,75 +0,0 @@ -use anyhow::{bail, Context, Result}; -use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEvent}; - -use crate::{ - app::{events::EventStatus, App}, - ui::setup, -}; - -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() { - setup::InputPosition::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() { - setup::InputPosition::Homeserver => { - ui.homeserver.input(input.to_owned()); - } - setup::InputPosition::Username => { - ui.username.input(input.to_owned()); - } - setup::InputPosition::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/listeners/input.rs b/src/app/events/listeners/input.rs deleted file mode 100644 index 351eb18..0000000 --- a/src/app/events/listeners/input.rs +++ /dev/null @@ -1,21 +0,0 @@ -use crate::app::events::Event; -use anyhow::{bail, Result}; -use tokio::{sync::mpsc, time::Duration}; -use tokio_util::sync::CancellationToken; - -pub async fn poll(channel: mpsc::Sender, kill: CancellationToken) -> Result<()> { - async fn stage_2(channel: mpsc::Sender) -> Result<()> { - loop { - if crossterm::event::poll(Duration::from_millis(100))? { - let event = Event::InputEvent(crossterm::event::read()?); - channel.send(event).await?; - } else { - tokio::task::yield_now().await; - } - } - } - tokio::select! { - output = stage_2(channel) => output, - _ = kill.cancelled() => bail!("received kill signal") - } -} diff --git a/src/app/events/listeners/matrix.rs b/src/app/events/listeners/matrix.rs deleted file mode 100644 index 5db0c10..0000000 --- a/src/app/events/listeners/matrix.rs +++ /dev/null @@ -1,41 +0,0 @@ -/* WARNING(@antifallobst): - * This file is going to be removed while implementing Chat Backend Servers! - * <19-10-2023> - */ - -use crate::app::events::Event; -use anyhow::{bail, Result}; -use matrix_sdk::{config::SyncSettings, Client, LoopCtrl}; -use tokio::sync::mpsc; -use tokio_util::sync::CancellationToken; - -pub async fn poll( - channel: mpsc::Sender, - kill: CancellationToken, - client: Client, -) -> Result<()> { - async fn 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 = Event::MatrixEvent(response); - - match tx.send(event).await { - Ok(_) => LoopCtrl::Continue, - Err(_) => LoopCtrl::Break, - } - }) - .await?; - - Ok(()) - } - tokio::select! { - output = stage_2(channel, client) => output, - _ = kill.cancelled() => bail!("received kill signal"), - } -} diff --git a/src/app/events/listeners/mod.rs b/src/app/events/listeners/mod.rs index c8f1fee..351eb18 100644 --- a/src/app/events/listeners/mod.rs +++ b/src/app/events/listeners/mod.rs @@ -1,2 +1,21 @@ -pub mod input; -pub mod matrix; +use crate::app::events::Event; +use anyhow::{bail, Result}; +use tokio::{sync::mpsc, time::Duration}; +use tokio_util::sync::CancellationToken; + +pub async fn poll(channel: mpsc::Sender, kill: CancellationToken) -> Result<()> { + async fn stage_2(channel: mpsc::Sender) -> Result<()> { + loop { + if crossterm::event::poll(Duration::from_millis(100))? { + let event = Event::InputEvent(crossterm::event::read()?); + channel.send(event).await?; + } else { + tokio::task::yield_now().await; + } + } + } + tokio::select! { + output = stage_2(channel) => output, + _ = kill.cancelled() => bail!("received kill signal") + } +} diff --git a/src/app/events/mod.rs b/src/app/events/mod.rs index 2cf81eb..3260e13 100644 --- a/src/app/events/mod.rs +++ b/src/app/events/mod.rs @@ -3,16 +3,16 @@ pub mod listeners; use anyhow::{Context, Result}; -use crate::app::{command_interface::Commands, status::State, App}; +use crate::app::{command_interface::Commands, App}; use cli_log::{trace, warn}; use crossterm::event::Event as CrosstermEvent; -use handlers::{command, input, lua_command, matrix, setup}; +use handlers::{command, input}; #[derive(Debug)] pub enum Event { InputEvent(CrosstermEvent), - MatrixEvent(matrix_sdk::deserialized_responses::SyncResponse), - // FIXME(@soispha): The `String` is also wrong <2024-05-03> + + // FIXME(@soispha): The `String` here is just wrong <2024-05-03> CommandEvent(Commands, Option>), LuaCommand(String), } @@ -21,10 +21,6 @@ impl Event { pub async fn handle(self, app: &mut App<'_>) -> Result { trace!("Received event to handle: `{:#?}`", &self); match self { - Event::MatrixEvent(event) => matrix::handle(app, &event) - .await - .with_context(|| format!("Failed to handle matrix event: `{:#?}`", event)), - Event::CommandEvent(event, callback_tx) => command::handle(app, &event, callback_tx) .await .with_context(|| format!("Failed to handle command event: `{:#?}`", event)), @@ -43,9 +39,6 @@ impl Event { // .await // .with_context(|| format!("Failed to handle function: `{}`", function)), Event::InputEvent(event) => match app.status.state() { - State::Setup => setup::handle(app, &event).await.with_context(|| { - format!("Failed to handle input (setup) event: `{:#?}`", event) - }), _ => input::handle(app, &event).await.with_context(|| { format!("Failed to handle input (non-setup) event: `{:#?}`", event) }), diff --git a/src/app/mod.rs b/src/app/mod.rs index 98f8569..c926382 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -3,24 +3,13 @@ pub mod config; pub mod events; pub mod status; -use std::{ - collections::HashMap, - ffi::c_int, - path::{Path, PathBuf}, - sync::OnceLock, -}; +use std::{collections::HashMap, ffi::c_int, path::PathBuf, sync::OnceLock}; -use anyhow::{Context, Error, Result}; +use anyhow::{Context, Result}; use cli_log::{info, warn}; -use crossterm::{ - event::DisableMouseCapture, - execute, - terminal::{disable_raw_mode, LeaveAlternateScreen}, -}; use directories::ProjectDirs; use keymaps::trie::Node; use libloading::{Library, Symbol}; -use matrix_sdk::Client; use tokio::sync::mpsc::{self, Sender}; use tokio_util::sync::CancellationToken; @@ -29,24 +18,21 @@ use tokio_util::sync::CancellationToken; // }; use crate::{ - accounts::{Account, AccountsManager}, app::{ events::{Event, EventStatus}, status::{State, Status}, }, - ui::{central, setup}, + ui::central, }; pub struct App<'runtime> { ui: central::UI<'runtime>, - accounts_manager: AccountsManager, status: Status, tx: mpsc::Sender, rx: mpsc::Receiver, input_listener_killer: CancellationToken, - matrix_listener_killer: CancellationToken, // lua: LuaCommandManager, project_dirs: ProjectDirs, @@ -58,14 +44,6 @@ pub static COMMAND_TRANSMITTER: OnceLock> = OnceLock::new(); impl App<'_> { pub fn new() -> Result { - let path: &std::path::Path = Path::new("userdata/accounts.json"); - let config = if path.exists() { - info!("Reading account config (userdata/accounts.json)"); - Some(std::fs::read_to_string(path)?) - } else { - None - }; - let (tx, rx) = mpsc::channel(256); COMMAND_TRANSMITTER @@ -74,13 +52,11 @@ impl App<'_> { Ok(Self { ui: central::UI::new()?, - accounts_manager: AccountsManager::new(config)?, - status: Status::new(None), + status: Status::new(), tx: tx.clone(), rx, input_listener_killer: CancellationToken::new(), - matrix_listener_killer: CancellationToken::new(), // lua: LuaCommandManager::new(tx), @@ -99,7 +75,7 @@ impl App<'_> { plugin_path: Option, ) -> Result<()> { // Spawn input event listener - tokio::task::spawn(events::listeners::input::poll( + tokio::task::spawn(events::listeners::poll( self.tx.clone(), self.input_listener_killer.clone(), )); @@ -147,14 +123,6 @@ impl App<'_> { } } - if self.account().is_err() { - info!("No saved sessions found -> jumping into setup"); - self.setup().await?; - } else { - self.accounts_manager.restore().await?; - self.init_account().await?; - } - self.status.set_state(State::Normal); loop { @@ -171,89 +139,4 @@ impl App<'_> { self.input_listener_killer.cancel(); Ok(()) } - - async fn setup(&mut self) -> Result<()> { - self.ui.setup_ui = Some(setup::UI::new()); - - self.status.set_state(State::Setup); - - loop { - self.ui.update_setup().await?; - - let event = self.rx.recv().await.context("Failed to get next event")?; - - match event.handle(self).await? { - EventStatus::Ok => (), - EventStatus::Finished => return Ok(()), - 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")), - } - .clone(); - - self.matrix_listener_killer.cancel(); - self.matrix_listener_killer = CancellationToken::new(); - - // Spawn Matrix Event Listener - tokio::task::spawn(events::listeners::matrix::poll( - self.tx.clone(), - self.matrix_listener_killer.clone(), - client.clone(), - )); - - // Reset Status - self.status = Status::new(Some(client)); - - let account = self.account()?; - let name = account.name().clone(); - let user_id = account.user_id().clone(); - self.status.set_account_name(name); - self.status.set_account_user_id(user_id); - - for (_, room) in self.status.rooms_mut() { - room.update_name().await?; - for _ in 0..3 { - room.poll_old_timeline().await?; - } - } - - 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 account(&self) -> Result<&Account> { - let account = self.accounts_manager.current(); - match account { - None => Err(Error::msg("failed to resolve current account")), - Some(a) => Ok(a), - } - } - - pub fn client(&self) -> Option<&Client> { - self.accounts_manager.client() - } } diff --git a/src/app/status.rs b/src/app/status.rs index 814fc80..55d3c1e 100644 --- a/src/app/status.rs +++ b/src/app/status.rs @@ -1,25 +1,13 @@ use core::fmt; -use anyhow::{bail, Error, Result}; -use cli_log::warn; -use indexmap::IndexMap; +use anyhow::{bail, Result}; use keymaps::key_repr::Keys; -use matrix_sdk::{ - room::MessagesOptions, - ruma::{ - events::{room::message::RoomMessageEventContent, AnyTimelineEvent, StateEventType}, - RoomId, TransactionId, - }, - Client, -}; #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug)] pub enum State { Normal, Insert, Command, - /// Temporary workaround until command based login is working - Setup, /// Only used internally to signal, that we are waiting on further keyinputs, if multiple /// keymappings have the same prefix KeyInputPending { @@ -34,7 +22,6 @@ impl State { 'n' => State::Normal, 'i' => State::Insert, 'c' => State::Command, - 's' => State::Setup, _ => bail!( "The letter '{}' is either not connected to a state or not yet implemented", c @@ -43,15 +30,6 @@ impl State { } } -pub struct Room { - matrix_room: matrix_sdk::room::Joined, - name: String, - encrypted: bool, - timeline: Vec, - timeline_end: Option, - view_scroll: Option, -} - pub struct StatusMessage { content: String, is_error: bool, @@ -67,12 +45,7 @@ impl StatusMessage { pub struct Status { state: State, - account_name: String, - account_user_id: String, - client: Option, - rooms: IndexMap, - current_room_id: String, status_messages: Vec, } @@ -82,7 +55,6 @@ impl fmt::Display for State { Self::Normal => write!(f, "Normal"), Self::Insert => write!(f, "Insert"), Self::Command => write!(f, "Command"), - Self::Setup => write!(f, "Setup (!! workaround !!)"), Self::KeyInputPending { old_state: _, pending_keys: keys, @@ -91,101 +63,10 @@ impl fmt::Display for State { } } -impl Room { - pub fn new(matrix_room: matrix_sdk::room::Joined) -> Self { - Self { - matrix_room, - name: "".to_string(), - encrypted: false, - timeline: Vec::new(), - timeline_end: None, - view_scroll: None, - } - } - - pub async fn poll_old_timeline(&mut self) -> Result<()> { - if let Some(AnyTimelineEvent::State(event)) = &self.timeline.get(0) { - if event.event_type() == StateEventType::RoomCreate { - return Ok(()); - } - } - - let mut messages_options = MessagesOptions::backward(); - messages_options = match &self.timeline_end { - Some(end) => messages_options.from(end.as_str()), - None => messages_options, - }; - let events = self.matrix_room.messages(messages_options).await?; - self.timeline_end = events.end; - - for event in events.chunk.iter() { - self.timeline.insert( - 0, - match event.event.deserialize() { - Ok(ev) => ev, - Err(err) => { - warn!("Failed to deserialize timeline event - {err}"); - continue; - } - }, - ); - } - Ok(()) - } - - pub fn name(&self) -> &String { - &self.name - } - - pub async fn update_name(&mut self) -> Result<()> { - self.name = self.matrix_room.display_name().await?.to_string(); - Ok(()) - } - - pub fn timeline_add(&mut self, event: AnyTimelineEvent) { - self.timeline.push(event); - } - - pub fn timeline(&self) -> &Vec { - &self.timeline - } - - pub async fn send(&mut self, message: String) -> Result<()> { - let content = RoomMessageEventContent::text_plain(message); - let id = TransactionId::new(); - self.matrix_room.send(content, Some(&id)).await?; - Ok(()) - } - - pub fn view_scroll(&self) -> Option { - self.view_scroll - } - - pub fn set_view_scroll(&mut self, scroll: Option) { - self.view_scroll = scroll; - } - - pub fn encrypted(&self) -> bool { - self.matrix_room.is_encrypted() - } -} - impl Status { - pub fn new(client: Option) -> Self { - let mut rooms = IndexMap::new(); - if let Some(c) = &client { - for r in c.joined_rooms() { - rooms.insert(r.room_id().to_string(), Room::new(r.clone())); - } - }; - + pub fn new() -> Self { Self { state: State::Normal, - account_name: "".to_owned(), - account_user_id: "".to_owned(), - client, - rooms, - current_room_id: "".to_owned(), status_messages: vec![StatusMessage { content: "Initialized!".to_owned(), is_error: false, @@ -215,70 +96,6 @@ impl Status { &self.status_messages } - pub fn account_name(&self) -> &String { - &self.account_name - } - - pub fn set_account_name(&mut self, name: String) { - self.account_name = name; - } - - pub fn account_user_id(&self) -> &String { - &self.account_user_id - } - - pub fn set_account_user_id(&mut self, user_id: String) { - self.account_user_id = user_id; - } - - pub fn room(&self) -> Option<&Room> { - self.rooms.get(self.current_room_id.as_str()) - } - - pub fn room_mut(&mut self) -> Option<&mut Room> { - self.rooms.get_mut(self.current_room_id.as_str()) - } - - pub fn rooms(&self) -> &IndexMap { - &self.rooms - } - - pub fn rooms_mut(&mut self) -> &mut IndexMap { - &mut self.rooms - } - - pub fn set_room(&mut self, room_id: &RoomId) -> Result<()> { - if self.rooms.contains_key(room_id.as_str()) { - self.current_room_id = room_id.to_string(); - Ok(()) - } else { - Err(Error::msg(format!( - "failed to set room -> invalid room id {}", - room_id.to_string() - ))) - } - } - - pub fn set_room_by_index(&mut self, room_index: usize) -> Result<()> { - if let Some((room_id, _)) = self.rooms.get_index(room_index) { - self.current_room_id = room_id.clone(); - Ok(()) - } else { - Err(Error::msg(format!( - "failed to set room -> invalid room index {}", - room_index - ))) - } - } - - pub fn get_room(&self, room_id: &RoomId) -> Option<&Room> { - self.rooms.get(room_id.as_str()) - } - - pub fn get_room_mut(&mut self, room_id: &RoomId) -> Option<&mut Room> { - self.rooms.get_mut(room_id.as_str()) - } - pub fn state(&self) -> &State { &self.state } diff --git a/src/main.rs b/src/main.rs index 3d3fe01..ba0ec74 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,3 @@ -mod accounts; mod app; mod cli; mod ui; @@ -17,7 +16,7 @@ async fn main() -> anyhow::Result<()> { Command::Start {} => { let mut app = app::App::new()?; - // NOTE(@soispha): The `None` is temporary <2024-05-03> + // NOTE(@soispha): The 'None' here is temporary <2024-05-03> app.run(None, args.plugin_path).await?; } }; diff --git a/src/ui/central/mod.rs b/src/ui/central/mod.rs index a37d43c..066e775 100644 --- a/src/ui/central/mod.rs +++ b/src/ui/central/mod.rs @@ -2,7 +2,7 @@ pub mod update; use std::io::Stdout; -use anyhow::{bail, Context, Result}; +use anyhow::{Context, Result}; use cli_log::{info, warn}; use crossterm::{ event::DisableMouseCapture, @@ -16,9 +16,7 @@ use tui::{ Terminal, }; use tui_textarea::TextArea; -pub use update::*; -use super::setup; use crate::ui::{terminal_prepare, textarea_activate, textarea_inactivate}; #[derive(Clone, Copy, PartialEq)] @@ -148,8 +146,6 @@ pub struct UI<'a> { pub rooms_state: ListState, pub message_compose: TextArea<'a>, pub cli: Option>, - - pub setup_ui: Option>, } impl Drop for UI<'_> { @@ -190,7 +186,6 @@ impl UI<'_> { rooms_state: ListState::default(), message_compose, cli: None, - setup_ui: None, }) } @@ -267,15 +262,4 @@ impl UI<'_> { } self.cli = None; } - - pub async fn update_setup(&mut self) -> Result<()> { - let setup_ui = match &mut self.setup_ui { - Some(c) => c, - None => bail!("SetupUI instance not found"), - }; - - setup_ui.update(&mut self.terminal).await?; - - Ok(()) - } } diff --git a/src/ui/central/update/mod.rs b/src/ui/central/update/mod.rs index 76d56ea..6b62076 100644 --- a/src/ui/central/update/mod.rs +++ b/src/ui/central/update/mod.rs @@ -1,13 +1,13 @@ use std::cmp; -use anyhow::{Context, Result}; +use anyhow::Result; use tui::{ layout::{Constraint, Direction, Layout}, style::{Color, Style}, widgets::{Block, Borders, Paragraph}, }; -use self::widgets::{command_monitor, messages, room_info, rooms, status}; +use self::widgets::command_monitor; use super::UI; use crate::app::status::Status; @@ -43,7 +43,7 @@ impl UI<'_> { ) .split(chunks[0]); - let left_chunks = Layout::default() + let _left_chunks = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Length(5), Constraint::Min(4)].as_ref()) .split(main_chunks[0]); @@ -76,25 +76,16 @@ impl UI<'_> { .style(Style::default().fg(Color::DarkGray)), ) .style(Style::default().fg(Color::LightYellow)); - let status_panel = status::init(status, &colors); - let rooms_panel = rooms::init(status, &colors); - let (messages_panel, mut messages_state) = messages::init(status.room(), &colors) - .context("Failed to initiate the messages widget")?; - let room_info_panel = room_info::init(status.room(), &colors); let command_monitor = command_monitor::init(status.status_messages(), &colors); // render the widgets self.terminal.draw(|frame| { - frame.render_widget(status_panel, left_chunks[0]); - frame.render_stateful_widget(rooms_panel, left_chunks[1], &mut self.rooms_state); - frame.render_stateful_widget(messages_panel, middle_chunks[0], &mut messages_state); frame.render_widget(self.message_compose.widget(), middle_chunks[1]); frame.render_widget(mode_indicator, bottom_chunks[0]); match &self.cli { Some(cli) => frame.render_widget(cli.widget(), bottom_chunks[1]), None => (), }; - frame.render_widget(room_info_panel, right_chunks[0]); frame.render_widget(command_monitor, right_chunks[1]); })?; diff --git a/src/ui/central/update/widgets/messages.rs b/src/ui/central/update/widgets/messages.rs deleted file mode 100644 index 9a4f64d..0000000 --- a/src/ui/central/update/widgets/messages.rs +++ /dev/null @@ -1,109 +0,0 @@ -use anyhow::{Context, Result}; -use matrix_sdk::ruma::events::{AnyMessageLikeEvent, AnyTimelineEvent}; -use tui::{ - layout::Corner, - style::{Color, Modifier, Style}, - text::{Span, Spans, Text}, - widgets::{Block, Borders, List, ListItem, ListState}, -}; - -use crate::{app::status::Room, ui::central::InputPosition}; - -pub fn init<'a>(room: Option<&'a Room>, colors: &Vec) -> Result<(List<'a>, ListState)> { - let content = match room { - Some(room) => get_content_from_room(room).context("Failed to get content from room")?, - None => vec![ListItem::new(Text::styled( - "No room selected!", - Style::default().fg(Color::Red), - ))], - }; - - let mut messages_state = ListState::default(); - - if let Some(room) = room { - messages_state.select(room.view_scroll()); - } - - Ok(( - List::new(content) - .block( - Block::default() - .title("Messages") - .borders(Borders::ALL) - .style(Style::default().fg(colors[InputPosition::Messages as usize])), - ) - .start_corner(Corner::BottomLeft) - .highlight_symbol(">") - .highlight_style( - Style::default() - .fg(Color::LightMagenta) - .add_modifier(Modifier::BOLD), - ), - messages_state, - )) -} - -fn get_content_from_room(room: &Room) -> Result> { - let results: Vec> = room - .timeline() - .iter() - .rev() - .map(|event| filter_event(event).context("Failed to filter event")) - .collect(); - - let mut output = Vec::with_capacity(results.len()); - for result in results { - output.push(result?); - } - Ok(output) -} - -fn filter_event<'a>(event: &AnyTimelineEvent) -> Result> { - match event { - // Message Like Events - AnyTimelineEvent::MessageLike(message_like_event) => { - let (content, color) = match &message_like_event { - AnyMessageLikeEvent::RoomMessage(room_message_event) => { - let message_content = &room_message_event - .as_original() - .context("Failed to get inner original message_event")? - .content - .body(); - - (message_content.to_string(), Color::White) - } - _ => ( - format!( - "~~~ not supported message like event: {} ~~~", - message_like_event.event_type().to_string() - ), - Color::Red, - ), - }; - let mut text = Text::styled( - message_like_event.sender().to_string(), - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ); - text.extend(Text::styled( - content.to_string(), - Style::default().fg(color), - )); - Ok(ListItem::new(text)) - } - - // State Events - AnyTimelineEvent::State(state) => Ok(ListItem::new(vec![Spans::from(vec![ - Span::styled( - state.sender().to_string(), - Style::default().fg(Color::DarkGray), - ), - Span::styled(": ", Style::default().fg(Color::DarkGray)), - Span::styled( - state.event_type().to_string(), - Style::default().fg(Color::DarkGray), - ), - ])])), - } -} diff --git a/src/ui/central/update/widgets/mod.rs b/src/ui/central/update/widgets/mod.rs index f35b656..e72d096 100644 --- a/src/ui/central/update/widgets/mod.rs +++ b/src/ui/central/update/widgets/mod.rs @@ -1,5 +1 @@ pub mod command_monitor; -pub mod messages; -pub mod room_info; -pub mod rooms; -pub mod status; diff --git a/src/ui/central/update/widgets/room_info.rs b/src/ui/central/update/widgets/room_info.rs deleted file mode 100644 index 599280c..0000000 --- a/src/ui/central/update/widgets/room_info.rs +++ /dev/null @@ -1,37 +0,0 @@ -use tui::{ - layout::Alignment, - style::{Color, Style}, - text::Text, - widgets::{Block, Borders, Paragraph}, -}; - -use crate::{app::status::Room, ui::central::InputPosition}; - -pub fn init<'a>(room: Option<&'a Room>, colors: &Vec) -> Paragraph<'a> { - let mut room_info_content = Text::default(); - if let Some(room) = room { - room_info_content.extend(Text::styled(room.name(), Style::default().fg(Color::Cyan))); - if room.encrypted() { - room_info_content.extend(Text::styled("Encrypted", Style::default().fg(Color::Green))); - } else { - room_info_content.extend(Text::styled( - "Not Encrypted!", - Style::default().fg(Color::Red), - )); - } - } else { - room_info_content.extend(Text::styled( - "No room selected!", - Style::default().fg(Color::Red), - )); - } - - Paragraph::new(room_info_content) - .block( - Block::default() - .title("Room Info") - .borders(Borders::ALL) - .style(Style::default().fg(colors[InputPosition::RoomInfo as usize])), - ) - .alignment(Alignment::Center) -} diff --git a/src/ui/central/update/widgets/rooms.rs b/src/ui/central/update/widgets/rooms.rs deleted file mode 100644 index feab3c9..0000000 --- a/src/ui/central/update/widgets/rooms.rs +++ /dev/null @@ -1,29 +0,0 @@ -use tui::{ - style::{Color, Modifier, Style}, - text::Span, - widgets::{Block, Borders, List, ListItem}, -}; - -use crate::{app::status::Status, ui::central::InputPosition}; - -pub fn init<'a>(status: &'a Status, colors: &Vec) -> List<'a> { - let rooms_content: Vec<_> = status - .rooms() - .iter() - .map(|(_, room)| ListItem::new(Span::styled(room.name(), Style::default()))) - .collect(); - List::new(rooms_content) - .block( - Block::default() - .title("Rooms (navigate: arrow keys)") - .borders(Borders::ALL) - .style(Style::default().fg(colors[InputPosition::Rooms as usize])), - ) - .style(Style::default().fg(Color::DarkGray)) - .highlight_style( - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ) - .highlight_symbol(">") -} diff --git a/src/ui/central/update/widgets/status.rs b/src/ui/central/update/widgets/status.rs deleted file mode 100644 index 0d87034..0000000 --- a/src/ui/central/update/widgets/status.rs +++ /dev/null @@ -1,30 +0,0 @@ -use tui::{ - layout::Alignment, - style::{Color, Modifier, Style}, - text::Text, - widgets::{Block, Borders, Paragraph}, -}; - -use crate::{app::status::Status, ui::central::InputPosition}; - -pub fn init<'a>(status: &'a Status, colors: &Vec) -> Paragraph<'a> { - 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), - )); - Paragraph::new(status_content) - .block( - Block::default() - .title("Status") - .borders(Borders::ALL) - .style(Style::default().fg(colors[InputPosition::Status as usize])), - ) - .alignment(Alignment::Left) -} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 28ebea2..c948474 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,5 +1,4 @@ pub mod central; -pub mod setup; use std::{io, io::Stdout}; diff --git a/src/ui/setup.rs b/src/ui/setup.rs deleted file mode 100644 index b336452..0000000 --- a/src/ui/setup.rs +++ /dev/null @@ -1,172 +0,0 @@ -use std::io::Stdout; - -use anyhow::Result; -use tui::{ - backend::CrosstermBackend, - layout::{Alignment, Constraint, Direction, Layout}, - style::{Color, Modifier, Style}, - text::Span, - widgets::{Block, Borders, Paragraph}, - Terminal, -}; -use tui_textarea::TextArea; - -use crate::ui::{textarea_activate, textarea_inactivate}; - -pub struct UI<'a> { - input_position: InputPosition, - - pub homeserver: TextArea<'a>, - pub username: TextArea<'a>, - pub password: TextArea<'a>, - pub password_data: TextArea<'a>, -} - -#[derive(Clone, Copy)] -pub enum InputPosition { - Homeserver, - Username, - Password, - Ok, -} - -impl UI<'_> { - 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 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: InputPosition::Homeserver, - homeserver, - username, - password, - password_data, - } - } - - pub fn cycle_input_position(&mut self) { - self.input_position = match self.input_position { - InputPosition::Homeserver => { - textarea_inactivate(&mut self.homeserver); - textarea_activate(&mut self.username); - textarea_inactivate(&mut self.password); - InputPosition::Username - } - InputPosition::Username => { - textarea_inactivate(&mut self.homeserver); - textarea_inactivate(&mut self.username); - textarea_activate(&mut self.password); - InputPosition::Password - } - InputPosition::Password => { - textarea_inactivate(&mut self.homeserver); - textarea_inactivate(&mut self.username); - textarea_inactivate(&mut self.password); - InputPosition::Ok - } - InputPosition::Ok => { - textarea_activate(&mut self.homeserver); - textarea_inactivate(&mut self.username); - textarea_inactivate(&mut self.password); - InputPosition::Homeserver - } - }; - } - - pub fn cycle_input_position_rev(&mut self) { - self.input_position = match self.input_position { - InputPosition::Homeserver => { - textarea_inactivate(&mut self.homeserver); - textarea_inactivate(&mut self.username); - textarea_inactivate(&mut self.password); - InputPosition::Ok - } - InputPosition::Username => { - textarea_activate(&mut self.homeserver); - textarea_inactivate(&mut self.username); - textarea_inactivate(&mut self.password); - InputPosition::Homeserver - } - InputPosition::Password => { - textarea_inactivate(&mut self.homeserver); - textarea_activate(&mut self.username); - textarea_inactivate(&mut self.password); - InputPosition::Username - } - InputPosition::Ok => { - textarea_inactivate(&mut self.homeserver); - textarea_inactivate(&mut self.username); - textarea_activate(&mut self.password); - InputPosition::Password - } - }; - } - - pub fn input_position(&self) -> &InputPosition { - &self.input_position - } - - pub async fn update( - &mut self, - terminal: &mut Terminal>, - ) -> Result<()> { - let strings: Vec = vec!["".to_owned(); 3]; - - let content_ok = match self.input_position { - InputPosition::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 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(()) - } -}