1
0
Fork 0

Compare commits

..

No commits in common. "5e04ab49eebc1070d9704ba7de8a93f8b33fe67a" and "74be1c2506875d69ec2071cbe43fa144cdd2a9fc" have entirely different histories.

7 changed files with 344 additions and 547 deletions

View File

@ -8,12 +8,12 @@ license = "MIT"
[dependencies] [dependencies]
tui = "0.19" tui = "0.19"
tui-textarea = { version = "0.2", features = ["crossterm"] } tui-textarea = { version = "*" }
crossterm = "0.25" crossterm = "*"
matrix-sdk = "0.6" matrix-sdk = "0.6"
anyhow = "1.0" anyhow = "1.0"
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
tokio-util = "0.7" tokio-util = "0.7"
serde = "1.0" serde = "1.0"
cli-log = "2.0" cli-log = "2.0"
indexmap = "2.0.0" indexmap = "*"

View File

@ -1,12 +1,13 @@
use anyhow::{Error, Result};
use cli_log::{error, info, warn};
use matrix_sdk::{
config::SyncSettings,
ruma::{events::room::message::SyncRoomMessageEvent, exports::serde_json, user_id},
Client, Session,
};
use serde::{Deserialize, Serialize};
use std::fs; use std::fs;
use matrix_sdk::{Client,
config::SyncSettings,
ruma::{user_id,
events::room::message::SyncRoomMessageEvent,
exports::serde_json},
Session};
use anyhow::{Error, Result};
use serde::{Deserialize, Serialize};
use cli_log::{error, warn, info};
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Account { pub struct Account {
@ -32,7 +33,7 @@ pub struct AccountsManager {
} }
impl Account { impl Account {
pub fn name(&self) -> &String { pub fn name (&self) -> &String {
&self.name &self.name
} }
@ -42,12 +43,11 @@ impl Account {
} }
impl AccountsManager { impl AccountsManager {
pub fn new(config: Option<String>) -> Self { pub fn new(config:Option<String>) -> Self {
return match config { return match config {
Some(s) => { Some(s) => {
info!("Loading serialized AccountsManager"); info!("Loading serialized AccountsManager");
let accounts_data: AccountsData = let accounts_data:AccountsData = serde_json::from_str(&s).expect("failed to deserialize json");
serde_json::from_str(&s).expect("failed to deserialize json");
let mut clients = Vec::new(); let mut clients = Vec::new();
clients.resize(accounts_data.accounts.len(), None); clients.resize(accounts_data.accounts.len(), None);
Self { Self {
@ -56,7 +56,7 @@ impl AccountsManager {
accounts: accounts_data.accounts, accounts: accounts_data.accounts,
clients, clients,
} }
} },
None => { None => {
info!("Creating empty AccountsManager"); info!("Creating empty AccountsManager");
Self { Self {
@ -66,7 +66,7 @@ impl AccountsManager {
clients: Vec::new(), clients: Vec::new(),
} }
} }
}; }
} }
pub async fn restore(&mut self) -> Result<()> { pub async fn restore(&mut self) -> Result<()> {
@ -74,12 +74,7 @@ impl AccountsManager {
Ok(()) Ok(())
} }
pub async fn add( pub async fn add(&mut self, homeserver: &String, username: &String, password: &String) -> Result<u32> {
&mut self,
homeserver: &String,
username: &String,
password: &String,
) -> Result<u32> {
let id = self.num_accounts; let id = self.num_accounts;
self.num_accounts += 1; self.num_accounts += 1;
@ -89,8 +84,7 @@ impl AccountsManager {
.build() .build()
.await?; .await?;
client client.login_username(username, password)
.login_username(username, password)
.initial_device_display_name("Trinitrix") .initial_device_display_name("Trinitrix")
.send() .send()
.await?; .await?;
@ -100,13 +94,9 @@ impl AccountsManager {
let account = Account { let account = Account {
homeserver: homeserver.to_string(), homeserver: homeserver.to_string(),
id, id,
name: client name: client.account().get_display_name().await?.expect("failed to fetch display name"),
.account()
.get_display_name()
.await?
.expect("failed to fetch display name"),
session: session.clone(), session: session.clone(),
sync_token: None, sync_token: None
}; };
self.logout().await?; self.logout().await?;
@ -115,16 +105,12 @@ impl AccountsManager {
self.clients.push(Some(client)); self.clients.push(Some(client));
self.save()?; self.save()?;
info!( info!("Logged in as '{}' device ID: {}", session.user_id.to_string(), session.device_id.to_string());
"Logged in as '{}' device ID: {}",
session.user_id.to_string(),
session.device_id.to_string()
);
Ok(id) Ok(id)
} }
pub async fn login(&mut self, account_id: u32) -> Result<()> { pub async fn login(&mut self, account_id:u32) -> Result<()> {
self.logout().await?; // log out the current account self.logout().await?; // log out the current account
let account = if account_id >= self.num_accounts { let account = if account_id >= self.num_accounts {
@ -134,28 +120,17 @@ impl AccountsManager {
self.get(account_id).expect("Account lookup failed") self.get(account_id).expect("Account lookup failed")
}; };
if self if self.clients.get(account_id as usize).expect("client lookup failed").is_none() {
.clients info!("No client cached for account: '{}' -> requesting a new one", &account.session.user_id);
.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() let client = Client::builder()
.homeserver_url(&account.homeserver) .homeserver_url(&account.homeserver)
.sled_store(format!("userdata/{account_id}"), Some("supersecure"))? .sled_store(format!("userdata/{account_id}"), Some("supersecure")) ?
.build() .build()
.await?; .await?;
client.restore_login(account.session.clone()).await?; client.restore_login(account.session.clone()).await?;
self.clients.insert(account_id as usize, Some(client)); self.clients.insert(account_id as usize, Some(client));
} else { } else {
info!( info!("Using cached client for account: '{}'", &account.session.user_id);
"Using cached client for account: '{}'",
&account.session.user_id
);
}; };
info!("Restored account"); info!("Restored account");
@ -181,7 +156,7 @@ impl AccountsManager {
Ok(()) Ok(())
} }
pub fn save(&self) -> Result<()> { pub fn save(&self) -> Result<()>{
let accounts_data = AccountsData { let accounts_data = AccountsData {
current_account: self.current_account, current_account: self.current_account,
accounts: self.accounts.clone(), accounts: self.accounts.clone(),
@ -213,7 +188,5 @@ impl AccountsManager {
} }
} }
pub fn num_accounts(&self) -> u32 { pub fn num_accounts(&self) -> u32 { self.num_accounts }
self.num_accounts
}
} }

View File

@ -1,22 +1,14 @@
use crate::app::{ use crate::app::{App, status::{Status, State}};
status::{State, Status},
App,
};
use crate::ui; use crate::ui;
use anyhow::{Error, Result}; use tokio::time::Duration;
use cli_log::{error, info, warn}; use tokio::sync::{mpsc, broadcast};
use matrix_sdk::{ use tokio_util::sync::CancellationToken;
config::SyncSettings, use anyhow::{Result, Error};
room::Room, use matrix_sdk::{Client, room::{Room}, config::SyncSettings, ruma::events::room::{
ruma::events::room::{
member::StrippedRoomMemberEvent, member::StrippedRoomMemberEvent,
message::{MessageType, OriginalSyncRoomMessageEvent, RoomMessageEventContent}, message::{MessageType, OriginalSyncRoomMessageEvent, RoomMessageEventContent},
}, }, LoopCtrl};
Client, LoopCtrl, use cli_log::{error, warn, info};
};
use tokio::sync::{broadcast, mpsc};
use tokio::time::Duration;
use tokio_util::sync::CancellationToken;
#[derive(Debug)] #[derive(Debug)]
pub enum EventStatus { pub enum EventStatus {
@ -28,7 +20,7 @@ pub enum EventStatus {
#[derive(Debug)] #[derive(Debug)]
pub struct Event { pub struct Event {
input_event: Option<crossterm::event::Event>, input_event: Option<crossterm::event::Event>,
matrix_event: Option<matrix_sdk::deserialized_responses::SyncResponse>, matrix_event: Option<matrix_sdk::deserialized_responses::SyncResponse>
} }
pub struct EventBuilder { pub struct EventBuilder {
@ -50,6 +42,7 @@ impl Default for EventBuilder {
event: Event::default(), event: Event::default(),
} }
} }
} }
impl EventBuilder { impl EventBuilder {
@ -58,10 +51,7 @@ impl EventBuilder {
self self
} }
fn matrix_event( fn matrix_event(&mut self, matrix_event: matrix_sdk::deserialized_responses::SyncResponse) -> &Self {
&mut self,
matrix_event: matrix_sdk::deserialized_responses::SyncResponse,
) -> &Self {
self.event.matrix_event = Some(matrix_event); self.event.matrix_event = Some(matrix_event);
self self
} }
@ -76,6 +66,7 @@ impl EventBuilder {
impl Event { impl Event {
pub async fn handle(&self, app: &mut App<'_>) -> Result<EventStatus> { pub async fn handle(&self, app: &mut App<'_>) -> Result<EventStatus> {
if self.matrix_event.is_some() { if self.matrix_event.is_some() {
return self.handle_matrix(app).await; return self.handle_matrix(app).await;
} }
@ -97,11 +88,7 @@ impl Event {
None => continue, None => continue,
}; };
for m_event in m_room.timeline.events.clone() { for m_event in m_room.timeline.events.clone() {
let event = m_event let event = m_event.event.deserialize().unwrap().into_full_event(m_room_id.clone());
.event
.deserialize()
.unwrap()
.into_full_event(m_room_id.clone());
room.timeline_add(event); room.timeline_add(event);
} }
} }
@ -112,88 +99,64 @@ impl Event {
async fn handle_main(&self, app: &mut App<'_>) -> Result<EventStatus> { async fn handle_main(&self, app: &mut App<'_>) -> Result<EventStatus> {
if self.input_event.is_some() { if self.input_event.is_some() {
match tui_textarea::Input::from(self.input_event.clone().unwrap()) { match tui_textarea::Input::from(self.input_event.clone().unwrap()) {
tui_textarea::Input { tui_textarea::Input { key: tui_textarea::Key::Esc, .. } => return Ok(EventStatus::Terminate),
key: tui_textarea::Key::Esc,
..
} => return Ok(EventStatus::Terminate),
tui_textarea::Input { tui_textarea::Input {
key: tui_textarea::Key::Tab, key: tui_textarea::Key::Tab,
.. ..
} => { } => {
app.ui.cycle_main_input_position(); app.ui.cycle_main_input_position();
} }
input => match app.ui.input_position() { input => {
match app.ui.input_position() {
ui::MainInputPosition::MessageCompose => { ui::MainInputPosition::MessageCompose => {
match input { match input {
tui_textarea::Input { tui_textarea::Input {key: tui_textarea::Key::Enter, alt: true, ..} => {
key: tui_textarea::Key::Enter,
alt: true,
..
} => {
match app.status.room_mut() { match app.status.room_mut() {
Some(room) => { Some(room) => {
room.send(app.ui.message_compose.lines().join("\n")) room.send(app.ui.message_compose.lines().join("\n")).await?;
.await?;
app.ui.message_compose_clear(); app.ui.message_compose_clear();
} },
None => (), None => ()
}; };
} }
_ => { _ => { app.ui.message_compose.input(input); }
app.ui.message_compose.input(input);
}
}; };
} },
ui::MainInputPosition::Rooms => { ui::MainInputPosition::Rooms => {
match input { match input {
tui_textarea::Input { tui_textarea::Input {key: tui_textarea::Key::Up, ..} => {
key: tui_textarea::Key::Up,
..
} => {
let i = match app.ui.rooms_state.selected() { let i = match app.ui.rooms_state.selected() {
Some(i) => { Some(i) => {
if i > 0 { if i > 0 { i - 1 }
i - 1 else { i }
} else { },
i
}
}
None => 0, None => 0,
}; };
app.ui.rooms_state.select(Some(i)); app.ui.rooms_state.select(Some(i));
app.status.set_room_by_index(i)?; app.status.set_room_by_index(i)?;
} },
tui_textarea::Input { tui_textarea::Input {key: tui_textarea::Key::Down, ..} => {
key: tui_textarea::Key::Down,
..
} => {
let i = match app.ui.rooms_state.selected() { let i = match app.ui.rooms_state.selected() {
Some(i) => { Some(i) => {
if i < app.status.rooms().len() { if i < app.status.rooms().len() { i + 1 }
i + 1 else { i }
} else { },
i
}
}
None => 0, None => 0,
}; };
app.ui.rooms_state.select(Some(i)); app.ui.rooms_state.select(Some(i));
app.status.set_room_by_index(i)?; app.status.set_room_by_index(i)?;
} },
_ => (), _ => ()
}; };
} },
ui::MainInputPosition::Messages => { ui::MainInputPosition::Messages => {
match input { match input {
tui_textarea::Input { tui_textarea::Input {key: tui_textarea::Key::Up, ..} => {
key: tui_textarea::Key::Up, match app.status.room_mut(){
..
} => {
match app.status.room_mut() {
Some(room) => { Some(room) => {
let len = room.timeline().len(); let len = room.timeline().len();
let i = match room.view_scroll() { let i = match room.view_scroll() {
Some(i) => i + 1, Some(i) => i+1,
None => 0, None => 0,
}; };
if i < len { if i < len {
@ -202,15 +165,12 @@ impl Event {
if i <= len - 5 { if i <= len - 5 {
room.poll_old_timeline().await?; room.poll_old_timeline().await?;
} }
} },
None => (), None => (),
}; };
} },
tui_textarea::Input { tui_textarea::Input {key: tui_textarea::Key::Down, ..} => {
key: tui_textarea::Key::Down, match app.status.room_mut(){
..
} => {
match app.status.room_mut() {
Some(room) => { Some(room) => {
match room.view_scroll() { match room.view_scroll() {
Some(i) => { Some(i) => {
@ -222,15 +182,16 @@ impl Event {
} }
None => (), None => (),
}; };
} },
None => (), None => (),
}; };
}
_ => (),
};
}
_ => (),
}, },
_ => ()
};
},
_ => (),
}
}
}; };
} }
Ok(EventStatus::Ok) Ok(EventStatus::Ok)
@ -239,21 +200,18 @@ impl Event {
async fn handle_setup(&self, app: &mut App<'_>) -> Result<EventStatus> { async fn handle_setup(&self, app: &mut App<'_>) -> Result<EventStatus> {
let ui = match &mut app.ui.setup_ui { let ui = match &mut app.ui.setup_ui {
Some(ui) => ui, Some(ui) => ui,
None => return Err(Error::msg("SetupUI instance not found")), None => return Err(Error::msg("SetupUI instance not found"))
}; };
if self.input_event.is_some() { if self.input_event.is_some() {
match tui_textarea::Input::from(self.input_event.clone().unwrap()) { match tui_textarea::Input::from(self.input_event.clone().unwrap()) {
tui_textarea::Input { tui_textarea::Input { key: tui_textarea::Key::Esc, .. } => return Ok(EventStatus::Terminate),
key: tui_textarea::Key::Esc,
..
} => return Ok(EventStatus::Terminate),
tui_textarea::Input { tui_textarea::Input {
key: tui_textarea::Key::Tab, key: tui_textarea::Key::Tab,
.. ..
} => { } => {
ui.cycle_input_position(); ui.cycle_input_position();
} },
tui_textarea::Input { tui_textarea::Input {
key: tui_textarea::Key::Enter, key: tui_textarea::Key::Enter,
.. ..
@ -267,40 +225,33 @@ impl Event {
if login.is_ok() { if login.is_ok() {
return Ok(EventStatus::Finished); return Ok(EventStatus::Finished);
} }
} },
_ => ui.cycle_input_position(), _ => ui.cycle_input_position(),
}; };
} },
input => match ui.input_position() { input => {
ui::SetupInputPosition::Homeserver => { match ui.input_position() {
ui.homeserver.input(input); ui::SetupInputPosition::Homeserver => { ui.homeserver.input(input); },
} ui::SetupInputPosition::Username => { ui.username.input(input); },
ui::SetupInputPosition::Username => {
ui.username.input(input);
}
ui::SetupInputPosition::Password => { ui::SetupInputPosition::Password => {
ui.password_data.input(input.clone()); ui.password_data.input(input.clone());
match input.key { match input.key {
tui_textarea::Key::Char(_) => { tui_textarea::Key::Char(_) => {
ui.password.input(tui_textarea::Input { ui.password.input(tui_textarea::Input { key: tui_textarea::Key::Char('*'), ctrl: false, alt: false });
key: tui_textarea::Key::Char('*'),
ctrl: false,
alt: false,
});
}
_ => {
ui.password.input(input);
}
}
}
_ => (),
}, },
_ => { ui.password.input(input); },
}
},
_ => (),
}
}
}; };
} }
Ok(EventStatus::Ok) Ok(EventStatus::Ok)
} }
} }
async fn poll_input_events_stage_2(channel: mpsc::Sender<Event>) -> Result<()> { async fn poll_input_events_stage_2(channel: mpsc::Sender<Event>) -> Result<()> {
loop { loop {
if crossterm::event::poll(Duration::from_millis(100))? { if crossterm::event::poll(Duration::from_millis(100))? {
@ -315,10 +266,7 @@ async fn poll_input_events_stage_2(channel: mpsc::Sender<Event>) -> Result<()> {
} }
} }
pub async fn poll_input_events( pub async fn poll_input_events(channel: mpsc::Sender<Event>, kill: CancellationToken) -> Result<()> {
channel: mpsc::Sender<Event>,
kill: CancellationToken,
) -> Result<()> {
tokio::select! { tokio::select! {
output = poll_input_events_stage_2(channel) => output, output = poll_input_events_stage_2(channel) => output,
_ = kill.cancelled() => Err(Error::msg("received kill signal")) _ = kill.cancelled() => Err(Error::msg("received kill signal"))
@ -332,25 +280,21 @@ async fn poll_matrix_events_stage_2(channel: mpsc::Sender<Event>, client: Client
let tx = &channel; let tx = &channel;
client client.sync_with_callback(sync_settings, |response| async move {
.sync_with_callback(sync_settings, |response| async move { let event = EventBuilder::default()
let event = EventBuilder::default().matrix_event(response).build(); .matrix_event(response)
.build();
match tx.send(event).await { match tx.send(event).await {
Ok(_) => LoopCtrl::Continue, Ok(_) => LoopCtrl::Continue,
Err(_) => LoopCtrl::Break, Err(_) => LoopCtrl::Break,
} }
}) }).await?;
.await?;
Ok(()) Ok(())
} }
pub async fn poll_matrix_events( pub async fn poll_matrix_events(channel: mpsc::Sender<Event>, kill: CancellationToken, client: Client) -> Result<()> {
channel: mpsc::Sender<Event>,
kill: CancellationToken,
client: Client,
) -> Result<()> {
tokio::select! { tokio::select! {
output = poll_matrix_events_stage_2(channel, client) => output, output = poll_matrix_events_stage_2(channel, client) => output,
_ = kill.cancelled() => Err(Error::msg("received kill signal")), _ = kill.cancelled() => Err(Error::msg("received kill signal")),

View File

@ -4,27 +4,24 @@ pub mod status;
use crate::accounts; use crate::accounts;
use crate::ui; use crate::ui;
use accounts::Account; use std::path::Path;
use accounts::AccountsManager; use matrix_sdk::{Client,
use anyhow::{Error, Result}; room::{Room},
use cli_log::{error, info, warn};
use matrix_sdk::{
config::SyncSettings, config::SyncSettings,
event_handler::Ctx,
room::Room,
ruma::events::room::{ ruma::events::room::{
member::StrippedRoomMemberEvent, member::StrippedRoomMemberEvent,
message::{MessageType, OriginalSyncRoomMessageEvent, RoomMessageEventContent}, message::{MessageType, OriginalSyncRoomMessageEvent, RoomMessageEventContent},
}, },
Client, event_handler::Ctx
};
use status::{State, Status};
use std::path::Path;
use tokio::{
sync::{broadcast, mpsc},
time::{sleep, Duration},
}; };
use accounts::Account;
use accounts::AccountsManager;
use anyhow::{Result, Error};
use cli_log::{error, warn, info};
use tokio::{time::{sleep, Duration}, sync::{mpsc, broadcast}};
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use status::{Status, State};
pub struct App<'a> { pub struct App<'a> {
ui: ui::UI<'a>, ui: ui::UI<'a>,
@ -38,12 +35,14 @@ pub struct App<'a> {
} }
impl Drop for App<'_> { impl Drop for App<'_> {
fn drop(&mut self) {} fn drop(&mut self) {
}
} }
impl App<'_> { impl App<'_> {
pub fn new() -> Self { pub fn new() -> Self {
let path: &std::path::Path = Path::new("userdata/accounts.json"); let path:&std::path::Path = Path::new("userdata/accounts.json");
let config = if path.exists() { let config = if path.exists() {
info!("Reading account config (userdata/accounts.json)"); info!("Reading account config (userdata/accounts.json)");
Some(std::fs::read_to_string(path).expect("failed to read accounts config")) Some(std::fs::read_to_string(path).expect("failed to read accounts config"))
@ -66,11 +65,9 @@ impl App<'_> {
} }
pub async fn run(&mut self) -> Result<()> { pub async fn run(&mut self) -> Result<()> {
// Spawn input event listener // Spawn input event listener
tokio::task::spawn(event::poll_input_events( tokio::task::spawn(event::poll_input_events(self.channel_tx.clone(), self.input_listener_killer.clone()));
self.channel_tx.clone(),
self.input_listener_killer.clone(),
));
if self.account().is_err() { if self.account().is_err() {
info!("No saved sessions found -> jumping into setup"); info!("No saved sessions found -> jumping into setup");
@ -80,13 +77,14 @@ impl App<'_> {
self.init_account().await?; self.init_account().await?;
} }
loop { loop {
self.status.set_state(State::Main); self.status.set_state(State::Main);
self.ui.update(&self.status).await?; self.ui.update(&self.status).await?;
let event: event::Event = match self.channel_rx.recv().await { let event: event::Event = match self.channel_rx.recv().await {
Some(e) => e, Some(e) => e,
None => return Err(Error::msg("Event channel has no senders")), None => return Err(Error::msg("Event channel has no senders"))
}; };
match event.handle(self).await? { match event.handle(self).await? {
@ -109,7 +107,7 @@ impl App<'_> {
let event: event::Event = match self.channel_rx.recv().await { let event: event::Event = match self.channel_rx.recv().await {
Some(e) => e, Some(e) => e,
None => return Err(Error::msg("Event channel has no senders")), None => return Err(Error::msg("Event channel has no senders"))
}; };
match event.handle(self).await? { match event.handle(self).await? {
@ -123,19 +121,14 @@ impl App<'_> {
pub async fn init_account(&mut self) -> Result<()> { pub async fn init_account(&mut self) -> Result<()> {
let client = match self.client() { let client = match self.client() {
Some(c) => c, Some(c) => c,
None => return Err(Error::msg("failed to get current client")), None => return Err(Error::msg("failed to get current client"))
} }.clone();
.clone();
self.matrix_listener_killer.cancel(); self.matrix_listener_killer.cancel();
self.matrix_listener_killer = CancellationToken::new(); self.matrix_listener_killer = CancellationToken::new();
// Spawn Matrix Event Listener // Spawn Matrix Event Listener
tokio::task::spawn(event::poll_matrix_events( tokio::task::spawn(event::poll_matrix_events(self.channel_tx.clone(), self.matrix_listener_killer.clone(), client.clone()));
self.channel_tx.clone(),
self.matrix_listener_killer.clone(),
client.clone(),
));
// Reset Status // Reset Status
self.status = Status::new(Some(client)); self.status = Status::new(Some(client));
@ -146,11 +139,10 @@ impl App<'_> {
self.status.set_account_name(name); self.status.set_account_name(name);
self.status.set_account_user_id(user_id); self.status.set_account_user_id(user_id);
for (_, room) in self.status.rooms_mut() { for (_, room) in self.status.rooms_mut() {
room.update_name().await?; room.update_name().await?;
for _ in 0..3 { for _ in 0..3 { room.poll_old_timeline().await?; }
room.poll_old_timeline().await?;
}
} }
info!("Initializing client for the current account"); info!("Initializing client for the current account");
@ -159,18 +151,12 @@ impl App<'_> {
} }
pub async fn switch_account(&mut self, account_id: u32) -> Result<()> { pub async fn switch_account(&mut self, account_id: u32) -> Result<()> {
Ok(()) Ok(())
} }
pub async fn login( pub async fn login(&mut self, homeserver: &String, username: &String, password: &String) -> Result<()> {
&mut self, self.accounts_manager.add(homeserver, username, password).await?;
homeserver: &String,
username: &String,
password: &String,
) -> Result<()> {
self.accounts_manager
.add(homeserver, username, password)
.await?;
self.init_account().await?; self.init_account().await?;
Ok(()) Ok(())
} }
@ -179,7 +165,7 @@ impl App<'_> {
let account = self.accounts_manager.current(); let account = self.accounts_manager.current();
match account { match account {
None => Err(Error::msg("failed to resolve current account")), None => Err(Error::msg("failed to resolve current account")),
Some(a) => Ok(a), Some(a) => Ok(a)
} }
} }

View File

@ -1,15 +1,14 @@
use anyhow::{Error, Result};
use cli_log::{error, info, warn};
use indexmap::IndexMap;
use matrix_sdk::{
room::MessagesOptions,
ruma::{
events::{room::message::RoomMessageEventContent, AnyTimelineEvent, StateEventType},
RoomId, TransactionId,
},
Client,
};
use std::any::Any; use std::any::Any;
use indexmap::IndexMap;
use matrix_sdk::{Client,
ruma::{events::{AnyTimelineEvent,
room::message::RoomMessageEventContent,
StateEventType},
RoomId,
TransactionId},
room::MessagesOptions};
use anyhow::{Result, Error};
use cli_log::{error, warn, info};
pub enum State { pub enum State {
None, None,
@ -37,7 +36,7 @@ pub struct Status {
} }
impl Room { impl Room {
pub fn new(matrix_room: matrix_sdk::room::Joined) -> Self { pub fn new (matrix_room: matrix_sdk::room::Joined) -> Self {
Self { Self {
matrix_room, matrix_room,
name: "".to_string(), name: "".to_string(),
@ -51,29 +50,26 @@ impl Room {
pub async fn poll_old_timeline(&mut self) -> Result<()> { pub async fn poll_old_timeline(&mut self) -> Result<()> {
if let Some(AnyTimelineEvent::State(event)) = &self.timeline.get(0) { if let Some(AnyTimelineEvent::State(event)) = &self.timeline.get(0) {
if event.event_type() == StateEventType::RoomCreate { if event.event_type() == StateEventType::RoomCreate {
return Ok(()); return Ok(())
} }
} }
let mut messages_options = MessagesOptions::backward(); let mut messages_options = MessagesOptions::backward();
messages_options = match &self.timeline_end { messages_options = match &self.timeline_end {
Some(end) => messages_options.from(end.as_str()), Some(end) => messages_options.from(end.as_str()),
None => messages_options, None => messages_options
}; };
let events = self.matrix_room.messages(messages_options).await?; let events = self.matrix_room.messages(messages_options).await?;
self.timeline_end = events.end; self.timeline_end = events.end;
for event in events.chunk.iter() { for event in events.chunk.iter() {
self.timeline.insert( self.timeline.insert(0, match event.event.deserialize() {
0,
match event.event.deserialize() {
Ok(ev) => ev, Ok(ev) => ev,
Err(err) => { Err(err) => {
warn!("Failed to deserialize timeline event - {err}"); warn!("Failed to deserialize timeline event - {err}");
continue; continue;
} }
}, });
);
} }
Ok(()) Ok(())
} }
@ -91,9 +87,7 @@ impl Room {
self.timeline.push(event); self.timeline.push(event);
} }
pub fn timeline(&self) -> &Vec<AnyTimelineEvent> { pub fn timeline(&self) -> &Vec<AnyTimelineEvent> { &self.timeline }
&self.timeline
}
pub async fn send(&mut self, message: String) -> Result<()> { pub async fn send(&mut self, message: String) -> Result<()> {
let content = RoomMessageEventContent::text_plain(message); let content = RoomMessageEventContent::text_plain(message);
@ -120,7 +114,9 @@ impl Status {
let mut rooms = IndexMap::new(); let mut rooms = IndexMap::new();
if let Some(c) = &client { if let Some(c) = &client {
for r in c.joined_rooms() { for r in c.joined_rooms() {
rooms.insert(r.room_id().to_string(), Room::new(r.clone())); rooms.insert(
r.room_id().to_string(),
Room::new(r.clone()));
} }
}; };
@ -171,10 +167,7 @@ impl Status {
self.current_room_id = room_id.to_string(); self.current_room_id = room_id.to_string();
Ok(()) Ok(())
} else { } else {
Err(Error::msg(format!( Err(Error::msg(format!("failed to set room -> invalid room id {}", room_id.to_string())))
"failed to set room -> invalid room id {}",
room_id.to_string()
)))
} }
} }
@ -183,10 +176,7 @@ impl Status {
self.current_room_id = room_id.clone(); self.current_room_id = room_id.clone();
Ok(()) Ok(())
} else { } else {
Err(Error::msg(format!( Err(Error::msg(format!("failed to set room -> invalid room index {}", room_index)))
"failed to set room -> invalid room index {}",
room_index
)))
} }
} }

View File

@ -1,8 +1,8 @@
mod ui;
mod accounts; mod accounts;
mod app; mod app;
mod ui;
use cli_log::{error, info, warn}; use cli_log::{error, warn, info};
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {

View File

@ -1,38 +1,30 @@
use crate::app::status::Status; use crate::app::status::Status;
use anyhow::{Error, Result};
use cli_log::{error, info, warn};
use crossterm::{
event::{self, read, DisableMouseCapture, EnableMouseCapture, Event},
execute,
style::style,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use matrix_sdk::{
room::MessagesOptions,
ruma::events::{AnyMessageLikeEvent, AnyTimelineEvent},
};
use std::cmp; use std::cmp;
use std::io; use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, read},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
style::{style},
};
use anyhow::{Error, Result};
use std::io::Stdout; use std::io::Stdout;
use std::io;
use tui::{backend::CrosstermBackend, layout::{Constraint, Direction, Layout}, widgets::{Block, Borders, Widget}, Terminal, Frame};
use tui::layout::{Alignment, Corner}; use tui::layout::{Alignment, Corner};
use tui::style::{Color, Modifier, Style}; use tui::style::{Color, Modifier, Style};
use tui::text::{Span, Spans, Text}; use tui::text::{Spans, Span, Text};
use tui::widgets::{List, ListItem, ListState, Paragraph, Wrap}; use tui::widgets::{List, ListItem, ListState, Paragraph, Wrap};
use tui::{
backend::CrosstermBackend,
layout::{Constraint, Direction, Layout},
widgets::{Block, Borders, Widget},
Frame, Terminal,
};
use tui_textarea::{Input, Key, TextArea}; use tui_textarea::{Input, Key, TextArea};
use cli_log::{error, warn, info};
use matrix_sdk::{room::MessagesOptions, ruma::events::{AnyTimelineEvent, AnyMessageLikeEvent}};
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub enum SetupInputPosition { pub enum SetupInputPosition {
Homeserver, Homeserver,
Username, Username,
Password, Password,
Ok, Ok
} }
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
@ -41,7 +33,7 @@ pub enum MainInputPosition {
Rooms, Rooms,
Messages, Messages,
MessageCompose, MessageCompose,
RoomInfo, RoomInfo
} }
pub struct SetupUI<'a> { pub struct SetupUI<'a> {
@ -62,6 +54,7 @@ pub struct UI<'a> {
pub setup_ui: Option<SetupUI<'a>>, pub setup_ui: Option<SetupUI<'a>>,
} }
fn terminal_prepare() -> Result<Stdout> { fn terminal_prepare() -> Result<Stdout> {
enable_raw_mode()?; enable_raw_mode()?;
let mut stdout = io::stdout(); let mut stdout = io::stdout();
@ -87,7 +80,9 @@ pub fn textarea_inactivate(textarea: &mut TextArea) {
.block() .block()
.cloned() .cloned()
.unwrap_or_else(|| Block::default().borders(Borders::ALL)); .unwrap_or_else(|| Block::default().borders(Borders::ALL));
textarea.set_block(b.style(Style::default().fg(Color::DarkGray))); textarea.set_block(
b.style(Style::default().fg(Color::DarkGray))
);
} }
impl Drop for UI<'_> { impl Drop for UI<'_> {
@ -99,9 +94,7 @@ impl Drop for UI<'_> {
LeaveAlternateScreen, LeaveAlternateScreen,
DisableMouseCapture DisableMouseCapture
).expect("While destructing UI -> Failed execute backend commands (LeaveAlternateScreen and DisableMouseCapture)"); ).expect("While destructing UI -> Failed execute backend commands (LeaveAlternateScreen and DisableMouseCapture)");
self.terminal self.terminal.show_cursor().expect("While destructing UI -> Failed to re-enable cursor");
.show_cursor()
.expect("While destructing UI -> Failed to re-enable cursor");
} }
} }
@ -112,9 +105,18 @@ impl SetupUI<'_> {
let mut password = TextArea::default(); let mut password = TextArea::default();
let mut password_data = TextArea::default(); let mut password_data = TextArea::default();
homeserver.set_block(Block::default().title("Homeserver").borders(Borders::ALL)); homeserver.set_block(
username.set_block(Block::default().title("Username").borders(Borders::ALL)); Block::default()
password.set_block(Block::default().title("Password").borders(Borders::ALL)); .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_activate(&mut homeserver);
textarea_inactivate(&mut username); textarea_inactivate(&mut username);
@ -136,49 +138,46 @@ impl SetupUI<'_> {
textarea_activate(&mut self.username); textarea_activate(&mut self.username);
textarea_inactivate(&mut self.password); textarea_inactivate(&mut self.password);
SetupInputPosition::Username SetupInputPosition::Username
} },
SetupInputPosition::Username => { SetupInputPosition::Username => {
textarea_inactivate(&mut self.homeserver); textarea_inactivate(&mut self.homeserver);
textarea_inactivate(&mut self.username); textarea_inactivate(&mut self.username);
textarea_activate(&mut self.password); textarea_activate(&mut self.password);
SetupInputPosition::Password SetupInputPosition::Password
} },
SetupInputPosition::Password => { SetupInputPosition::Password => {
textarea_inactivate(&mut self.homeserver); textarea_inactivate(&mut self.homeserver);
textarea_inactivate(&mut self.username); textarea_inactivate(&mut self.username);
textarea_inactivate(&mut self.password); textarea_inactivate(&mut self.password);
SetupInputPosition::Ok SetupInputPosition::Ok
} },
SetupInputPosition::Ok => { SetupInputPosition::Ok => {
textarea_activate(&mut self.homeserver); textarea_activate(&mut self.homeserver);
textarea_inactivate(&mut self.username); textarea_inactivate(&mut self.username);
textarea_inactivate(&mut self.password); textarea_inactivate(&mut self.password);
SetupInputPosition::Homeserver SetupInputPosition::Homeserver
} },
}; };
} }
pub fn input_position(&self) -> &SetupInputPosition { pub fn input_position(&self) -> &SetupInputPosition { &self.input_position }
&self.input_position
}
pub async fn update( pub async fn update(&'_ mut self, terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
&'_ mut self,
terminal: &mut Terminal<CrosstermBackend<Stdout>>,
) -> Result<()> {
let mut strings: Vec<String> = Vec::new(); let mut strings: Vec<String> = Vec::new();
strings.resize(3, "".to_string()); strings.resize(3, "".to_string());
let content_ok = match self.input_position { let content_ok = match self.input_position {
SetupInputPosition::Ok => { SetupInputPosition:: Ok => Span::styled("OK", Style::default().add_modifier(Modifier::UNDERLINED)),
Span::styled("OK", Style::default().add_modifier(Modifier::UNDERLINED))
}
_ => Span::styled("OK", Style::default().fg(Color::DarkGray)), _ => Span::styled("OK", Style::default().fg(Color::DarkGray)),
}; };
let block = Block::default().title("Login").borders(Borders::ALL); let block = Block::default()
.title("Login")
.borders(Borders::ALL);
let mut ok = Paragraph::new(content_ok)
.alignment(Alignment::Center);
let mut ok = Paragraph::new(content_ok).alignment(Alignment::Center);
// define a 32 * 6 chunk in the middle of the screen // define a 32 * 6 chunk in the middle of the screen
let mut chunk = terminal.size()?; let mut chunk = terminal.size()?;
@ -195,15 +194,12 @@ impl SetupUI<'_> {
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints( .constraints([
[
Constraint::Length(3), // 0. Homserver: Constraint::Length(3), // 0. Homserver:
Constraint::Length(3), // 1. Username: Constraint::Length(3), // 1. Username:
Constraint::Length(3), // 2. Password: Constraint::Length(3), // 2. Password:
Constraint::Length(1), // 3. OK Constraint::Length(1) // 3. OK
] ].as_ref())
.as_ref(),
)
.split(split_chunk); .split(split_chunk);
terminal.draw(|frame| { terminal.draw(|frame| {
@ -230,8 +226,7 @@ impl UI<'_> {
message_compose.set_block( message_compose.set_block(
Block::default() Block::default()
.title("Message Compose (send: <Alt>+<Enter>)") .title("Message Compose (send: <Alt>+<Enter>)")
.borders(Borders::ALL), .borders(Borders::ALL));
);
info!("Initialized UI"); info!("Initialized UI");
@ -240,7 +235,7 @@ impl UI<'_> {
input_position: MainInputPosition::Rooms, input_position: MainInputPosition::Rooms,
rooms_state: ListState::default(), rooms_state: ListState::default(),
message_compose, message_compose,
setup_ui: None, setup_ui: None
} }
} }
@ -254,17 +249,14 @@ impl UI<'_> {
}; };
} }
pub fn input_position(&self) -> &MainInputPosition { pub fn input_position(&self) -> &MainInputPosition { &self.input_position }
&self.input_position
}
pub fn message_compose_clear(&mut self) { pub fn message_compose_clear(&mut self) {
self.message_compose = TextArea::default(); self.message_compose = TextArea::default();
self.message_compose.set_block( self.message_compose.set_block(
Block::default() Block::default()
.title("Message Compose (send: <Alt>+<Enter>)") .title("Message Compose (send: <Alt>+<Enter>)")
.borders(Borders::ALL), .borders(Borders::ALL));
);
} }
pub async fn update(&mut self, status: &Status) -> Result<()> { pub async fn update(&mut self, status: &Status) -> Result<()> {
@ -272,14 +264,7 @@ impl UI<'_> {
let main_chunks = Layout::default() let main_chunks = Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.constraints( .constraints([Constraint::Length(32), Constraint::Min(16), Constraint::Length(32)].as_ref())
[
Constraint::Length(32),
Constraint::Min(16),
Constraint::Length(32),
]
.as_ref(),
)
.split(chunk); .split(chunk);
let left_chunks = Layout::default() let left_chunks = Layout::default()
@ -289,13 +274,7 @@ impl UI<'_> {
let middle_chunks = Layout::default() let middle_chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints( .constraints([Constraint::Min(4), Constraint::Length(cmp::min(2 + self.message_compose.lines().len() as u16, 8))].as_ref())
[
Constraint::Min(4),
Constraint::Length(cmp::min(2 + self.message_compose.lines().len() as u16, 8)),
]
.as_ref(),
)
.split(main_chunks[1]); .split(main_chunks[1]);
let right_chunks = Layout::default() let right_chunks = Layout::default()
@ -303,22 +282,15 @@ impl UI<'_> {
.constraints([Constraint::Min(4)].as_ref()) .constraints([Constraint::Min(4)].as_ref())
.split(main_chunks[2]); .split(main_chunks[2]);
let mut status_content = Text::styled( let mut status_content = Text::styled(status.account_name(), Style::default().add_modifier(Modifier::BOLD));
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(status.account_user_id(), Style::default()));
status_content.extend(Text::styled( status_content.extend(Text::styled("settings", Style::default().fg(Color::LightMagenta).add_modifier(Modifier::ITALIC | Modifier::UNDERLINED)));
"settings",
Style::default()
.fg(Color::LightMagenta)
.add_modifier(Modifier::ITALIC | Modifier::UNDERLINED),
));
let rooms_content = status let rooms_content = status.rooms()
.rooms()
.iter() .iter()
.map(|(_, room)| ListItem::new(Span::styled(room.name(), Style::default()))) .map(|(_, room)| {
ListItem::new(Span::styled(room.name(), Style::default()))
})
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let messages_content = match status.room() { let messages_content = match status.room() {
@ -328,6 +300,7 @@ impl UI<'_> {
.rev() .rev()
.map(|event| { .map(|event| {
match event { match event {
// Message Like Events // Message Like Events
AnyTimelineEvent::MessageLike(message_like_event) => { AnyTimelineEvent::MessageLike(message_like_event) => {
let (content, color) = match &message_like_event { let (content, color) = match &message_like_event {
@ -339,48 +312,30 @@ impl UI<'_> {
.body(); .body();
(message_content.to_string(), Color::White) (message_content.to_string(), Color::White)
} },
_ => ( _ => ("~~~ not supported message like event ~~~".to_string(), Color::Red)
"~~~ not supported message like event ~~~".to_string(),
Color::Red,
),
}; };
let mut text = Text::styled( let mut text = Text::styled(message_like_event.sender().to_string(),
message_like_event.sender().to_string(), Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD));
Style::default() text.extend(Text::styled(content.to_string(),
.fg(Color::Cyan) Style::default().fg(color)));
.add_modifier(Modifier::BOLD),
);
text.extend(Text::styled(
content.to_string(),
Style::default().fg(color),
));
ListItem::new(text) ListItem::new(text)
} },
// State Events // State Events
AnyTimelineEvent::State(state) => { AnyTimelineEvent::State(state) => {
ListItem::new(vec![Spans::from(vec![ ListItem::new(vec![Spans::from(vec![
Span::styled( Span::styled(state.sender().to_string(), Style::default().fg(Color::DarkGray)),
state.sender().to_string(),
Style::default().fg(Color::DarkGray),
),
Span::styled(": ", Style::default().fg(Color::DarkGray)), Span::styled(": ", Style::default().fg(Color::DarkGray)),
Span::styled( Span::styled(state.event_type().to_string(), Style::default().fg(Color::DarkGray))
state.event_type().to_string(),
Style::default().fg(Color::DarkGray),
),
])]) ])])
} }
} }
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
} },
None => { None => {
vec![ListItem::new(Text::styled( vec![ListItem::new(Text::styled("No room selected!", Style::default().fg(Color::Red)))]
"No room selected!",
Style::default().fg(Color::Red),
))]
} }
}; };
@ -392,129 +347,78 @@ impl UI<'_> {
room_info_content.extend(Text::styled(room.name(), Style::default().fg(Color::Cyan))); room_info_content.extend(Text::styled(room.name(), Style::default().fg(Color::Cyan)));
if room.encrypted() { if room.encrypted() {
room_info_content room_info_content.extend(Text::styled("Encrypted", Style::default().fg(Color::Green)));
.extend(Text::styled("Encrypted", Style::default().fg(Color::Green)));
} else { } else {
room_info_content.extend(Text::styled( room_info_content.extend(Text::styled("Not Encrypted!", Style::default().fg(Color::Red)));
"Not Encrypted!",
Style::default().fg(Color::Red),
));
} }
} else { } else {
room_info_content.extend(Text::styled( room_info_content.extend(Text::styled("No room selected!", Style::default().fg(Color::Red)));
"No room selected!",
Style::default().fg(Color::Red),
));
} }
// calculate to widgets colors, based of which widget is currently selected // calculate to widgets colors, based of which widget is currently selected
let colors = match self.input_position { let colors = match self.input_position {
MainInputPosition::Status => { MainInputPosition::Status => {
textarea_inactivate(&mut self.message_compose); textarea_inactivate(&mut self.message_compose);
vec![ vec![Color::White, Color::DarkGray, Color::DarkGray, Color::DarkGray, Color::DarkGray]
Color::White, },
Color::DarkGray,
Color::DarkGray,
Color::DarkGray,
Color::DarkGray,
]
}
MainInputPosition::Rooms => { MainInputPosition::Rooms => {
textarea_inactivate(&mut self.message_compose); textarea_inactivate(&mut self.message_compose);
vec![ vec![Color::DarkGray, Color::White, Color::DarkGray, Color::DarkGray, Color::DarkGray]
Color::DarkGray, },
Color::White,
Color::DarkGray,
Color::DarkGray,
Color::DarkGray,
]
}
MainInputPosition::Messages => { MainInputPosition::Messages => {
textarea_inactivate(&mut self.message_compose); textarea_inactivate(&mut self.message_compose);
vec![ vec![Color::DarkGray, Color::DarkGray, Color::White, Color::DarkGray, Color::DarkGray]
Color::DarkGray, },
Color::DarkGray,
Color::White,
Color::DarkGray,
Color::DarkGray,
]
}
MainInputPosition::MessageCompose => { MainInputPosition::MessageCompose => {
textarea_activate(&mut self.message_compose); textarea_activate(&mut self.message_compose);
vec![ vec![Color::DarkGray, Color::DarkGray, Color::DarkGray, Color::DarkGray, Color::DarkGray]
Color::DarkGray, },
Color::DarkGray,
Color::DarkGray,
Color::DarkGray,
Color::DarkGray,
]
}
MainInputPosition::RoomInfo => { MainInputPosition::RoomInfo => {
textarea_inactivate(&mut self.message_compose); textarea_inactivate(&mut self.message_compose);
vec![ vec![Color::DarkGray, Color::DarkGray, Color::DarkGray, Color::DarkGray, Color::White]
Color::DarkGray, },
Color::DarkGray,
Color::DarkGray,
Color::DarkGray,
Color::White,
]
}
}; };
// initiate the widgets // initiate the widgets
let status_panel = Paragraph::new(status_content) let status_panel = Paragraph::new(status_content)
.block( .block(Block::default()
Block::default()
.title("Status") .title("Status")
.borders(Borders::ALL) .borders(Borders::ALL)
.style(Style::default().fg(colors[MainInputPosition::Status as usize])), .style(Style::default().fg(colors[MainInputPosition::Status as usize])))
)
.alignment(Alignment::Left); .alignment(Alignment::Left);
let rooms_panel = List::new(rooms_content) let rooms_panel = List::new(rooms_content)
.block( .block(Block::default()
Block::default()
.title("Rooms (navigate: arrow keys)") .title("Rooms (navigate: arrow keys)")
.borders(Borders::ALL) .borders(Borders::ALL)
.style(Style::default().fg(colors[MainInputPosition::Rooms as usize])), .style(Style::default().fg(colors[MainInputPosition::Rooms as usize])))
)
.style(Style::default().fg(Color::DarkGray)) .style(Style::default().fg(Color::DarkGray))
.highlight_style( .highlight_style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD))
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol(">"); .highlight_symbol(">");
let messages_panel = List::new(messages_content) let messages_panel = List::new(messages_content)
.block( .block(Block::default()
Block::default()
.title("Messages") .title("Messages")
.borders(Borders::ALL) .borders(Borders::ALL)
.style(Style::default().fg(colors[MainInputPosition::Messages as usize])), .style(Style::default().fg(colors[MainInputPosition::Messages as usize])))
)
.start_corner(Corner::BottomLeft) .start_corner(Corner::BottomLeft)
.highlight_symbol(">") .highlight_symbol(">")
.highlight_style( .highlight_style(Style::default().fg(Color::LightMagenta).add_modifier(Modifier::BOLD));
Style::default()
.fg(Color::LightMagenta)
.add_modifier(Modifier::BOLD),
);
let room_info_panel = Paragraph::new(room_info_content) let room_info_panel = Paragraph::new(room_info_content)
.block( .block(Block::default()
Block::default()
.title("Room Info") .title("Room Info")
.borders(Borders::ALL) .borders(Borders::ALL)
.style(Style::default().fg(colors[MainInputPosition::RoomInfo as usize])), .style(Style::default().fg(colors[MainInputPosition::RoomInfo as usize])))
)
.alignment(Alignment::Center); .alignment(Alignment::Center);
// render the widgets // render the widgets
self.terminal.draw(|frame| { self.terminal.draw(|frame| {
frame.render_widget(status_panel, left_chunks[0]); 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(rooms_panel, left_chunks[1], &mut self.rooms_state);
frame.render_stateful_widget(messages_panel, middle_chunks[0], &mut messages_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(room_info_panel, right_chunks[0]); frame.render_widget(room_info_panel, right_chunks[0]);
})?; })?;