refactor(src): Remove all matrix related code

This obviously is a big regression, but having this matrix code in the
core trinitrix tree is no longer planned. And if we start writing the
matrix cbs, referring to this commit should be possible.
This commit is contained in:
Benedikt Peetz 2024-05-04 15:43:31 +02:00
parent c233b30a52
commit 08c4724a94
23 changed files with 43 additions and 1157 deletions

View File

@ -7,20 +7,17 @@ default-run = "trinitrix"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # 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] [dependencies]
clap = { version = "4.5.4", features = ["derive"] } clap = { version = "4.5.4", features = ["derive"] }
cli-log = "2.0" cli-log = "2.0"
anyhow = "1.0" anyhow = "1.0"
matrix-sdk = "0.6" tokio = { version = "1.37", features = ["macros", "rt-multi-thread", "fs", "time"] }
tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } tokio-util = {version = "0.7.10"}
# config # config
trinitry = {version = "0.1.0"} trinitry = {version = "0.1.0"}
keymaps = {version = "0.1.1", features = ["crossterm"] } keymaps = {version = "0.1.1", features = ["crossterm"] }
directories = "5.0.1"
# c api # c api
libloading = "0.8.3" libloading = "0.8.3"
@ -31,13 +28,9 @@ mlua = { version = "0.9.7", features = ["lua54", "async", "send", "serialize"] }
once_cell = "1.19.0" once_cell = "1.19.0"
# tui feature specific parts # tui feature specific parts
tui = {version = "0.19", optional = true} tui = {version = "0.19"}
tui-textarea = { version = "0.2", features = ["crossterm"], optional = true } tui-textarea = { version = "0.2", features = ["crossterm"]}
crossterm = { version = "0.25", optional = true } crossterm = { version = "0.25"}
tokio-util = { version = "0.7", optional = true }
serde = { version = "1.0", optional = true }
indexmap = { version = "2.2.6", optional = true }
directories = "5.0.1"
[dev-dependencies] [dev-dependencies]
pretty_assertions = "1.4.0" pretty_assertions = "1.4.0"

View File

@ -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<String>,
}
#[derive(Debug, Serialize, Deserialize)]
struct AccountsData {
current_account: u32,
accounts: Vec<Account>,
}
pub struct AccountsManager {
current_account: u32,
num_accounts: u32,
accounts: Vec<Account>,
clients: Vec<Option<Client>>,
}
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<String>) -> Result<Self> {
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<u32> {
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
}
}

View File

@ -40,20 +40,12 @@ mod trinitrix {
/// Closes the application /// Closes the application
fn exit(); 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 //// Open the help pages at the first occurrence of
//// the input string if it is Some, otherwise open //// the input string if it is Some, otherwise open
//// the help pages at the start //// the help pages at the start
// TODO(@soispha): To be implemented <2024-03-09> // TODO(@soispha): To be implemented <2024-03-09>
// fn help(Option<String>); // fn help(Option<String>);
//// 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 /// Function that change the UI, or UI state
mod ui { mod ui {
enum Mode { enum Mode {

View File

@ -87,21 +87,6 @@ pub async fn handle(
warn!("Terminating the application"); warn!("Terminating the application");
EventStatus::Terminate 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 { Api::Ui(ui) => match ui {
Ui::set_mode { mode } => match mode { Ui::set_mode { mode } => match mode {
Mode::Normal => { Mode::Normal => {
@ -213,7 +198,6 @@ pub async fn handle(
EventStatus::Ok EventStatus::Ok
} }
State::Normal State::Normal
| State::Setup
| State::KeyInputPending { | State::KeyInputPending {
old_state: _, old_state: _,
pending_keys: _, pending_keys: _,

View File

@ -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<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

@ -1,9 +1,5 @@
// input events // input events
pub mod input; pub mod input;
pub mod setup;
// matrix
pub mod matrix;
// ci // ci
pub mod command; pub mod command;

View File

@ -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<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() {
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)
}

View File

@ -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<Event>, kill: CancellationToken) -> Result<()> {
async fn stage_2(channel: mpsc::Sender<Event>) -> 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")
}
}

View File

@ -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<Event>,
kill: CancellationToken,
client: Client,
) -> Result<()> {
async fn 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 = 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"),
}
}

View File

@ -1,2 +1,21 @@
pub mod input; use crate::app::events::Event;
pub mod matrix; use anyhow::{bail, Result};
use tokio::{sync::mpsc, time::Duration};
use tokio_util::sync::CancellationToken;
pub async fn poll(channel: mpsc::Sender<Event>, kill: CancellationToken) -> Result<()> {
async fn stage_2(channel: mpsc::Sender<Event>) -> 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")
}
}

View File

@ -3,16 +3,16 @@ pub mod listeners;
use anyhow::{Context, Result}; 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 cli_log::{trace, warn};
use crossterm::event::Event as CrosstermEvent; use crossterm::event::Event as CrosstermEvent;
use handlers::{command, input, lua_command, matrix, setup}; use handlers::{command, input};
#[derive(Debug)] #[derive(Debug)]
pub enum Event { pub enum Event {
InputEvent(CrosstermEvent), 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<trixy::oneshot::Sender<String>>), CommandEvent(Commands, Option<trixy::oneshot::Sender<String>>),
LuaCommand(String), LuaCommand(String),
} }
@ -21,10 +21,6 @@ impl Event {
pub async fn handle(self, app: &mut App<'_>) -> Result<EventStatus> { pub async fn handle(self, app: &mut App<'_>) -> Result<EventStatus> {
trace!("Received event to handle: `{:#?}`", &self); trace!("Received event to handle: `{:#?}`", &self);
match 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) Event::CommandEvent(event, callback_tx) => command::handle(app, &event, callback_tx)
.await .await
.with_context(|| format!("Failed to handle command event: `{:#?}`", event)), .with_context(|| format!("Failed to handle command event: `{:#?}`", event)),
@ -43,9 +39,6 @@ impl Event {
// .await // .await
// .with_context(|| format!("Failed to handle function: `{}`", function)), // .with_context(|| format!("Failed to handle function: `{}`", function)),
Event::InputEvent(event) => match app.status.state() { 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(|| { _ => input::handle(app, &event).await.with_context(|| {
format!("Failed to handle input (non-setup) event: `{:#?}`", event) format!("Failed to handle input (non-setup) event: `{:#?}`", event)
}), }),

View File

@ -3,24 +3,13 @@ pub mod config;
pub mod events; pub mod events;
pub mod status; pub mod status;
use std::{ use std::{collections::HashMap, ffi::c_int, path::PathBuf, sync::OnceLock};
collections::HashMap,
ffi::c_int,
path::{Path, PathBuf},
sync::OnceLock,
};
use anyhow::{Context, Error, Result}; use anyhow::{Context, Result};
use cli_log::{info, warn}; use cli_log::{info, warn};
use crossterm::{
event::DisableMouseCapture,
execute,
terminal::{disable_raw_mode, LeaveAlternateScreen},
};
use directories::ProjectDirs; use directories::ProjectDirs;
use keymaps::trie::Node; use keymaps::trie::Node;
use libloading::{Library, Symbol}; use libloading::{Library, Symbol};
use matrix_sdk::Client;
use tokio::sync::mpsc::{self, Sender}; use tokio::sync::mpsc::{self, Sender};
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
@ -29,24 +18,21 @@ use tokio_util::sync::CancellationToken;
// }; // };
use crate::{ use crate::{
accounts::{Account, AccountsManager},
app::{ app::{
events::{Event, EventStatus}, events::{Event, EventStatus},
status::{State, Status}, status::{State, Status},
}, },
ui::{central, setup}, ui::central,
}; };
pub struct App<'runtime> { pub struct App<'runtime> {
ui: central::UI<'runtime>, ui: central::UI<'runtime>,
accounts_manager: AccountsManager,
status: Status, status: Status,
tx: mpsc::Sender<Event>, tx: mpsc::Sender<Event>,
rx: mpsc::Receiver<Event>, rx: mpsc::Receiver<Event>,
input_listener_killer: CancellationToken, input_listener_killer: CancellationToken,
matrix_listener_killer: CancellationToken,
// lua: LuaCommandManager, // lua: LuaCommandManager,
project_dirs: ProjectDirs, project_dirs: ProjectDirs,
@ -58,14 +44,6 @@ pub static COMMAND_TRANSMITTER: OnceLock<Sender<Event>> = OnceLock::new();
impl App<'_> { impl App<'_> {
pub fn new() -> Result<Self> { pub fn new() -> Result<Self> {
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); let (tx, rx) = mpsc::channel(256);
COMMAND_TRANSMITTER COMMAND_TRANSMITTER
@ -74,13 +52,11 @@ impl App<'_> {
Ok(Self { Ok(Self {
ui: central::UI::new()?, ui: central::UI::new()?,
accounts_manager: AccountsManager::new(config)?, status: Status::new(),
status: Status::new(None),
tx: tx.clone(), tx: tx.clone(),
rx, rx,
input_listener_killer: CancellationToken::new(), input_listener_killer: CancellationToken::new(),
matrix_listener_killer: CancellationToken::new(),
// lua: LuaCommandManager::new(tx), // lua: LuaCommandManager::new(tx),
@ -99,7 +75,7 @@ impl App<'_> {
plugin_path: Option<PathBuf>, plugin_path: Option<PathBuf>,
) -> Result<()> { ) -> Result<()> {
// Spawn input event listener // Spawn input event listener
tokio::task::spawn(events::listeners::input::poll( tokio::task::spawn(events::listeners::poll(
self.tx.clone(), self.tx.clone(),
self.input_listener_killer.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); self.status.set_state(State::Normal);
loop { loop {
@ -171,89 +139,4 @@ impl App<'_> {
self.input_listener_killer.cancel(); self.input_listener_killer.cancel();
Ok(()) 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()
}
} }

View File

@ -1,25 +1,13 @@
use core::fmt; use core::fmt;
use anyhow::{bail, Error, Result}; use anyhow::{bail, Result};
use cli_log::warn;
use indexmap::IndexMap;
use keymaps::key_repr::Keys; 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)] #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug)]
pub enum State { pub enum State {
Normal, Normal,
Insert, Insert,
Command, Command,
/// Temporary workaround until command based login is working
Setup,
/// Only used internally to signal, that we are waiting on further keyinputs, if multiple /// Only used internally to signal, that we are waiting on further keyinputs, if multiple
/// keymappings have the same prefix /// keymappings have the same prefix
KeyInputPending { KeyInputPending {
@ -34,7 +22,6 @@ impl State {
'n' => State::Normal, 'n' => State::Normal,
'i' => State::Insert, 'i' => State::Insert,
'c' => State::Command, 'c' => State::Command,
's' => State::Setup,
_ => bail!( _ => bail!(
"The letter '{}' is either not connected to a state or not yet implemented", "The letter '{}' is either not connected to a state or not yet implemented",
c c
@ -43,15 +30,6 @@ impl State {
} }
} }
pub struct Room {
matrix_room: matrix_sdk::room::Joined,
name: String,
encrypted: bool,
timeline: Vec<AnyTimelineEvent>,
timeline_end: Option<String>,
view_scroll: Option<usize>,
}
pub struct StatusMessage { pub struct StatusMessage {
content: String, content: String,
is_error: bool, is_error: bool,
@ -67,12 +45,7 @@ impl StatusMessage {
pub struct Status { pub struct Status {
state: State, state: State,
account_name: String,
account_user_id: String,
client: Option<Client>,
rooms: IndexMap<String, Room>,
current_room_id: String,
status_messages: Vec<StatusMessage>, status_messages: Vec<StatusMessage>,
} }
@ -82,7 +55,6 @@ impl fmt::Display for State {
Self::Normal => write!(f, "Normal"), Self::Normal => write!(f, "Normal"),
Self::Insert => write!(f, "Insert"), Self::Insert => write!(f, "Insert"),
Self::Command => write!(f, "Command"), Self::Command => write!(f, "Command"),
Self::Setup => write!(f, "Setup (!! workaround !!)"),
Self::KeyInputPending { Self::KeyInputPending {
old_state: _, old_state: _,
pending_keys: keys, 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<AnyTimelineEvent> {
&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<usize> {
self.view_scroll
}
pub fn set_view_scroll(&mut self, scroll: Option<usize>) {
self.view_scroll = scroll;
}
pub fn encrypted(&self) -> bool {
self.matrix_room.is_encrypted()
}
}
impl Status { impl Status {
pub fn new(client: Option<Client>) -> Self { pub fn new() -> 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()));
}
};
Self { Self {
state: State::Normal, state: State::Normal,
account_name: "".to_owned(),
account_user_id: "".to_owned(),
client,
rooms,
current_room_id: "".to_owned(),
status_messages: vec![StatusMessage { status_messages: vec![StatusMessage {
content: "Initialized!".to_owned(), content: "Initialized!".to_owned(),
is_error: false, is_error: false,
@ -215,70 +96,6 @@ impl Status {
&self.status_messages &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<String, Room> {
&self.rooms
}
pub fn rooms_mut(&mut self) -> &mut IndexMap<String, Room> {
&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 { pub fn state(&self) -> &State {
&self.state &self.state
} }

View File

@ -1,4 +1,3 @@
mod accounts;
mod app; mod app;
mod cli; mod cli;
mod ui; mod ui;
@ -17,7 +16,7 @@ async fn main() -> anyhow::Result<()> {
Command::Start {} => { Command::Start {} => {
let mut app = app::App::new()?; 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?; app.run(None, args.plugin_path).await?;
} }
}; };

View File

@ -2,7 +2,7 @@ pub mod update;
use std::io::Stdout; use std::io::Stdout;
use anyhow::{bail, Context, Result}; use anyhow::{Context, Result};
use cli_log::{info, warn}; use cli_log::{info, warn};
use crossterm::{ use crossterm::{
event::DisableMouseCapture, event::DisableMouseCapture,
@ -16,9 +16,7 @@ use tui::{
Terminal, Terminal,
}; };
use tui_textarea::TextArea; use tui_textarea::TextArea;
pub use update::*;
use super::setup;
use crate::ui::{terminal_prepare, textarea_activate, textarea_inactivate}; use crate::ui::{terminal_prepare, textarea_activate, textarea_inactivate};
#[derive(Clone, Copy, PartialEq)] #[derive(Clone, Copy, PartialEq)]
@ -148,8 +146,6 @@ pub struct UI<'a> {
pub rooms_state: ListState, pub rooms_state: ListState,
pub message_compose: TextArea<'a>, pub message_compose: TextArea<'a>,
pub cli: Option<TextArea<'a>>, pub cli: Option<TextArea<'a>>,
pub setup_ui: Option<setup::UI<'a>>,
} }
impl Drop for UI<'_> { impl Drop for UI<'_> {
@ -190,7 +186,6 @@ impl UI<'_> {
rooms_state: ListState::default(), rooms_state: ListState::default(),
message_compose, message_compose,
cli: None, cli: None,
setup_ui: None,
}) })
} }
@ -267,15 +262,4 @@ impl UI<'_> {
} }
self.cli = None; 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(())
}
} }

View File

@ -1,13 +1,13 @@
use std::cmp; use std::cmp;
use anyhow::{Context, Result}; use anyhow::Result;
use tui::{ use tui::{
layout::{Constraint, Direction, Layout}, layout::{Constraint, Direction, Layout},
style::{Color, Style}, style::{Color, Style},
widgets::{Block, Borders, Paragraph}, widgets::{Block, Borders, Paragraph},
}; };
use self::widgets::{command_monitor, messages, room_info, rooms, status}; use self::widgets::command_monitor;
use super::UI; use super::UI;
use crate::app::status::Status; use crate::app::status::Status;
@ -43,7 +43,7 @@ impl UI<'_> {
) )
.split(chunks[0]); .split(chunks[0]);
let left_chunks = Layout::default() let _left_chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([Constraint::Length(5), Constraint::Min(4)].as_ref()) .constraints([Constraint::Length(5), Constraint::Min(4)].as_ref())
.split(main_chunks[0]); .split(main_chunks[0]);
@ -76,25 +76,16 @@ impl UI<'_> {
.style(Style::default().fg(Color::DarkGray)), .style(Style::default().fg(Color::DarkGray)),
) )
.style(Style::default().fg(Color::LightYellow)); .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); let command_monitor = command_monitor::init(status.status_messages(), &colors);
// render the widgets // render the widgets
self.terminal.draw(|frame| { 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(self.message_compose.widget(), middle_chunks[1]);
frame.render_widget(mode_indicator, bottom_chunks[0]); frame.render_widget(mode_indicator, bottom_chunks[0]);
match &self.cli { match &self.cli {
Some(cli) => frame.render_widget(cli.widget(), bottom_chunks[1]), Some(cli) => frame.render_widget(cli.widget(), bottom_chunks[1]),
None => (), None => (),
}; };
frame.render_widget(room_info_panel, right_chunks[0]);
frame.render_widget(command_monitor, right_chunks[1]); frame.render_widget(command_monitor, right_chunks[1]);
})?; })?;

View File

@ -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<Color>) -> 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<Vec<ListItem>> {
let results: Vec<Result<ListItem>> = 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<ListItem<'a>> {
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),
),
])])),
}
}

View File

@ -1,5 +1 @@
pub mod command_monitor; pub mod command_monitor;
pub mod messages;
pub mod room_info;
pub mod rooms;
pub mod status;

View File

@ -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<Color>) -> 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)
}

View File

@ -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<Color>) -> 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(">")
}

View File

@ -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<Color>) -> 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)
}

View File

@ -1,5 +1,4 @@
pub mod central; pub mod central;
pub mod setup;
use std::{io, io::Stdout}; use std::{io, io::Stdout};

View File

@ -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<CrosstermBackend<Stdout>>,
) -> Result<()> {
let strings: Vec<String> = 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(())
}
}