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
[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"

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
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<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
mod ui {
enum Mode {

View File

@ -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: _,

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
pub mod input;
pub mod setup;
// matrix
pub mod matrix;
// ci
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;
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<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 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<trixy::oneshot::Sender<String>>),
LuaCommand(String),
}
@ -21,10 +21,6 @@ impl Event {
pub async fn handle(self, app: &mut App<'_>) -> Result<EventStatus> {
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)
}),

View File

@ -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<Event>,
rx: mpsc::Receiver<Event>,
input_listener_killer: CancellationToken,
matrix_listener_killer: CancellationToken,
// lua: LuaCommandManager,
project_dirs: ProjectDirs,
@ -58,14 +44,6 @@ pub static COMMAND_TRANSMITTER: OnceLock<Sender<Event>> = OnceLock::new();
impl App<'_> {
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);
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<PathBuf>,
) -> 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()
}
}

View File

@ -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<AnyTimelineEvent>,
timeline_end: Option<String>,
view_scroll: Option<usize>,
}
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<Client>,
rooms: IndexMap<String, Room>,
current_room_id: String,
status_messages: Vec<StatusMessage>,
}
@ -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<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 {
pub fn new(client: Option<Client>) -> 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<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 {
&self.state
}

View File

@ -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?;
}
};

View File

@ -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<TextArea<'a>>,
pub setup_ui: Option<setup::UI<'a>>,
}
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(())
}
}

View File

@ -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]);
})?;

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 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 setup;
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(())
}
}