1
0
Fork 0

Compare commits

..

2 Commits

Author SHA1 Message Date
Benedikt Peetz 5e04ab49ee
Build(cargo): Remove wildcard version specification in `Cargo.toml`
Wildcard version is comparable to selecting a version depending on
the result of a dice roll. What I mean with this is, that the version
will be selected based on what the specific user already has in their
cargo registry. This means that the same codebase will compile
wonderfully on one machine but will fail with weird errors like:
`the trait 'From<crossterm::event::Event>' is not implemented for 'Input'`
on an other one.

Additionally crates.io does not accept crates with a bare wildcard
version requirement for aforementioned reasons.

Lastly using direct versions requirement allows to use `cargo upgrade`
to automatically update these requirements to their highest possible
value, as determined by SemVer. This works especially well, when adding
new dependencies with `cargo add`, as this also adds a direct version
requirement.
2023-07-09 08:38:59 +02:00
Benedikt Peetz 2905a0830a
Style(treewide): Format with rustfmt 2023-07-09 07:58:18 +02:00
7 changed files with 547 additions and 344 deletions

View File

@ -8,12 +8,12 @@ license = "MIT"
[dependencies] [dependencies]
tui = "0.19" tui = "0.19"
tui-textarea = { version = "*" } tui-textarea = { version = "0.2", features = ["crossterm"] }
crossterm = "*" crossterm = "0.25"
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 = "*" indexmap = "2.0.0"

View File

@ -1,13 +1,12 @@
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 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 serde::{Deserialize, Serialize};
use cli_log::{error, warn, info}; use std::fs;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Account { pub struct Account {
@ -33,7 +32,7 @@ pub struct AccountsManager {
} }
impl Account { impl Account {
pub fn name (&self) -> &String { pub fn name(&self) -> &String {
&self.name &self.name
} }
@ -43,11 +42,12 @@ 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 = serde_json::from_str(&s).expect("failed to deserialize json"); let accounts_data: AccountsData =
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,7 +74,12 @@ impl AccountsManager {
Ok(()) Ok(())
} }
pub async fn add(&mut self, homeserver: &String, username: &String, password: &String) -> Result<u32> { pub async fn add(
&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;
@ -84,7 +89,8 @@ impl AccountsManager {
.build() .build()
.await?; .await?;
client.login_username(username, password) client
.login_username(username, password)
.initial_device_display_name("Trinitrix") .initial_device_display_name("Trinitrix")
.send() .send()
.await?; .await?;
@ -94,9 +100,13 @@ impl AccountsManager {
let account = Account { let account = Account {
homeserver: homeserver.to_string(), homeserver: homeserver.to_string(),
id, id,
name: client.account().get_display_name().await?.expect("failed to fetch display name"), name: client
.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?;
@ -105,12 +115,16 @@ impl AccountsManager {
self.clients.push(Some(client)); self.clients.push(Some(client));
self.save()?; self.save()?;
info!("Logged in as '{}' device ID: {}", session.user_id.to_string(), session.device_id.to_string()); info!(
"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 {
@ -120,17 +134,28 @@ impl AccountsManager {
self.get(account_id).expect("Account lookup failed") self.get(account_id).expect("Account lookup failed")
}; };
if self.clients.get(account_id as usize).expect("client lookup failed").is_none() { if self
info!("No client cached for account: '{}' -> requesting a new one", &account.session.user_id); .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() 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!("Using cached client for account: '{}'", &account.session.user_id); info!(
"Using cached client for account: '{}'",
&account.session.user_id
);
}; };
info!("Restored account"); info!("Restored account");
@ -156,7 +181,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(),
@ -188,5 +213,7 @@ impl AccountsManager {
} }
} }
pub fn num_accounts(&self) -> u32 { self.num_accounts } pub fn num_accounts(&self) -> u32 {
} self.num_accounts
}
}

View File

@ -1,14 +1,22 @@
use crate::app::{App, status::{Status, State}}; use crate::app::{
status::{State, Status},
App,
};
use crate::ui; use crate::ui;
use anyhow::{Error, Result};
use cli_log::{error, info, warn};
use matrix_sdk::{
config::SyncSettings,
room::Room,
ruma::events::room::{
member::StrippedRoomMemberEvent,
message::{MessageType, OriginalSyncRoomMessageEvent, RoomMessageEventContent},
},
Client, LoopCtrl,
};
use tokio::sync::{broadcast, mpsc};
use tokio::time::Duration; use tokio::time::Duration;
use tokio::sync::{mpsc, broadcast};
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use anyhow::{Result, Error};
use matrix_sdk::{Client, room::{Room}, config::SyncSettings, ruma::events::room::{
member::StrippedRoomMemberEvent,
message::{MessageType, OriginalSyncRoomMessageEvent, RoomMessageEventContent},
}, LoopCtrl};
use cli_log::{error, warn, info};
#[derive(Debug)] #[derive(Debug)]
pub enum EventStatus { pub enum EventStatus {
@ -20,7 +28,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 {
@ -42,7 +50,6 @@ impl Default for EventBuilder {
event: Event::default(), event: Event::default(),
} }
} }
} }
impl EventBuilder { impl EventBuilder {
@ -51,7 +58,10 @@ impl EventBuilder {
self self
} }
fn matrix_event(&mut self, matrix_event: matrix_sdk::deserialized_responses::SyncResponse) -> &Self { fn matrix_event(
&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
} }
@ -66,7 +76,6 @@ 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;
} }
@ -88,7 +97,11 @@ 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.event.deserialize().unwrap().into_full_event(m_room_id.clone()); let event = m_event
.event
.deserialize()
.unwrap()
.into_full_event(m_room_id.clone());
room.timeline_add(event); room.timeline_add(event);
} }
} }
@ -99,99 +112,125 @@ 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 { key: tui_textarea::Key::Esc, .. } => return Ok(EventStatus::Terminate), tui_textarea::Input {
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 => { input => match app.ui.input_position() {
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,
match app.status.room_mut() { alt: true,
Some(room) => { ..
room.send(app.ui.message_compose.lines().join("\n")).await?; } => {
app.ui.message_compose_clear(); match app.status.room_mut() {
}, Some(room) => {
None => () room.send(app.ui.message_compose.lines().join("\n"))
}; .await?;
} app.ui.message_compose_clear();
_ => { app.ui.message_compose.input(input); } }
}; None => (),
}, };
ui::MainInputPosition::Rooms => { }
match input { _ => {
tui_textarea::Input {key: tui_textarea::Key::Up, ..} => { app.ui.message_compose.input(input);
let i = match app.ui.rooms_state.selected() { }
Some(i) => { };
if i > 0 { i - 1 }
else { i }
},
None => 0,
};
app.ui.rooms_state.select(Some(i));
app.status.set_room_by_index(i)?;
},
tui_textarea::Input {key: tui_textarea::Key::Down, ..} => {
let i = match app.ui.rooms_state.selected() {
Some(i) => {
if i < app.status.rooms().len() { i + 1 }
else { i }
},
None => 0,
};
app.ui.rooms_state.select(Some(i));
app.status.set_room_by_index(i)?;
},
_ => ()
};
},
ui::MainInputPosition::Messages => {
match input {
tui_textarea::Input {key: tui_textarea::Key::Up, ..} => {
match app.status.room_mut(){
Some(room) => {
let len = room.timeline().len();
let i = match room.view_scroll() {
Some(i) => i+1,
None => 0,
};
if i < len {
room.set_view_scroll(Some(i))
}
if i <= len - 5 {
room.poll_old_timeline().await?;
}
},
None => (),
};
},
tui_textarea::Input {key: tui_textarea::Key::Down, ..} => {
match app.status.room_mut(){
Some(room) => {
match room.view_scroll() {
Some(i) => {
if i == 0 {
room.set_view_scroll(None);
} else {
room.set_view_scroll(Some(i - 1));
}
}
None => (),
};
},
None => (),
};
},
_ => ()
};
},
_ => (),
} }
} ui::MainInputPosition::Rooms => {
match input {
tui_textarea::Input {
key: tui_textarea::Key::Up,
..
} => {
let i = match app.ui.rooms_state.selected() {
Some(i) => {
if i > 0 {
i - 1
} else {
i
}
}
None => 0,
};
app.ui.rooms_state.select(Some(i));
app.status.set_room_by_index(i)?;
}
tui_textarea::Input {
key: tui_textarea::Key::Down,
..
} => {
let i = match app.ui.rooms_state.selected() {
Some(i) => {
if i < app.status.rooms().len() {
i + 1
} else {
i
}
}
None => 0,
};
app.ui.rooms_state.select(Some(i));
app.status.set_room_by_index(i)?;
}
_ => (),
};
}
ui::MainInputPosition::Messages => {
match input {
tui_textarea::Input {
key: tui_textarea::Key::Up,
..
} => {
match app.status.room_mut() {
Some(room) => {
let len = room.timeline().len();
let i = match room.view_scroll() {
Some(i) => i + 1,
None => 0,
};
if i < len {
room.set_view_scroll(Some(i))
}
if i <= len - 5 {
room.poll_old_timeline().await?;
}
}
None => (),
};
}
tui_textarea::Input {
key: tui_textarea::Key::Down,
..
} => {
match app.status.room_mut() {
Some(room) => {
match room.view_scroll() {
Some(i) => {
if i == 0 {
room.set_view_scroll(None);
} else {
room.set_view_scroll(Some(i - 1));
}
}
None => (),
};
}
None => (),
};
}
_ => (),
};
}
_ => (),
},
}; };
} }
Ok(EventStatus::Ok) Ok(EventStatus::Ok)
@ -200,18 +239,21 @@ 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 { key: tui_textarea::Key::Esc, .. } => return Ok(EventStatus::Terminate), tui_textarea::Input {
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,
.. ..
@ -225,33 +267,40 @@ 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() {
ui::SetupInputPosition::Homeserver => { ui.homeserver.input(input); },
ui::SetupInputPosition::Username => { ui.username.input(input); },
ui::SetupInputPosition::Password => {
ui.password_data.input(input.clone());
match input.key {
tui_textarea::Key::Char(_) => {
ui.password.input(tui_textarea::Input { key: tui_textarea::Key::Char('*'), ctrl: false, alt: false });
},
_ => { ui.password.input(input); },
}
},
_ => (),
}
} }
input => match ui.input_position() {
ui::SetupInputPosition::Homeserver => {
ui.homeserver.input(input);
}
ui::SetupInputPosition::Username => {
ui.username.input(input);
}
ui::SetupInputPosition::Password => {
ui.password_data.input(input.clone());
match input.key {
tui_textarea::Key::Char(_) => {
ui.password.input(tui_textarea::Input {
key: tui_textarea::Key::Char('*'),
ctrl: false,
alt: false,
});
}
_ => {
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))? {
@ -266,7 +315,10 @@ async fn poll_input_events_stage_2(channel: mpsc::Sender<Event>) -> Result<()> {
} }
} }
pub async fn poll_input_events(channel: mpsc::Sender<Event>, kill: CancellationToken) -> Result<()> { pub async fn poll_input_events(
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"))
@ -275,28 +327,32 @@ pub async fn poll_input_events(channel: mpsc::Sender<Event>, kill: CancellationT
async fn poll_matrix_events_stage_2(channel: mpsc::Sender<Event>, client: Client) -> Result<()> { async fn poll_matrix_events_stage_2(channel: mpsc::Sender<Event>, client: Client) -> Result<()> {
let sync_settings = SyncSettings::default(); let sync_settings = SyncSettings::default();
// .token(sync_token) // .token(sync_token)
// .timeout(Duration::from_secs(30)); // .timeout(Duration::from_secs(30));
let tx = &channel; let tx = &channel;
client.sync_with_callback(sync_settings, |response| async move { client
let event = EventBuilder::default() .sync_with_callback(sync_settings, |response| async move {
.matrix_event(response) let event = EventBuilder::default().matrix_event(response).build();
.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(channel: mpsc::Sender<Event>, kill: CancellationToken, client: Client) -> Result<()> { pub async fn poll_matrix_events(
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,24 +4,27 @@ pub mod status;
use crate::accounts; use crate::accounts;
use crate::ui; use crate::ui;
use std::path::Path;
use matrix_sdk::{Client,
room::{Room},
config::SyncSettings,
ruma::events::room::{
member::StrippedRoomMemberEvent,
message::{MessageType, OriginalSyncRoomMessageEvent, RoomMessageEventContent},
},
event_handler::Ctx
};
use accounts::Account; use accounts::Account;
use accounts::AccountsManager; use accounts::AccountsManager;
use anyhow::{Result, Error}; use anyhow::{Error, Result};
use cli_log::{error, warn, info}; use cli_log::{error, info, warn};
use tokio::{time::{sleep, Duration}, sync::{mpsc, broadcast}}; use matrix_sdk::{
config::SyncSettings,
event_handler::Ctx,
room::Room,
ruma::events::room::{
member::StrippedRoomMemberEvent,
message::{MessageType, OriginalSyncRoomMessageEvent, RoomMessageEventContent},
},
Client,
};
use status::{State, Status};
use std::path::Path;
use tokio::{
sync::{broadcast, mpsc},
time::{sleep, Duration},
};
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>,
@ -35,14 +38,12 @@ 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"))
@ -65,9 +66,11 @@ 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(self.channel_tx.clone(), self.input_listener_killer.clone())); tokio::task::spawn(event::poll_input_events(
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");
@ -77,14 +80,13 @@ 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? {
@ -107,7 +109,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? {
@ -121,14 +123,19 @@ 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(self.channel_tx.clone(), self.matrix_listener_killer.clone(), client.clone())); tokio::task::spawn(event::poll_matrix_events(
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));
@ -139,10 +146,11 @@ 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 { room.poll_old_timeline().await?; } for _ in 0..3 {
room.poll_old_timeline().await?;
}
} }
info!("Initializing client for the current account"); info!("Initializing client for the current account");
@ -151,12 +159,18 @@ 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(&mut self, homeserver: &String, username: &String, password: &String) -> Result<()> { pub async fn login(
self.accounts_manager.add(homeserver, username, password).await?; &mut self,
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(())
} }
@ -165,11 +179,11 @@ 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),
} }
} }
pub fn client(&self) -> Option<&Client> { pub fn client(&self) -> Option<&Client> {
self.accounts_manager.client() self.accounts_manager.client()
} }
} }

View File

@ -1,14 +1,15 @@
use std::any::Any; use anyhow::{Error, Result};
use cli_log::{error, info, warn};
use indexmap::IndexMap; use indexmap::IndexMap;
use matrix_sdk::{Client, use matrix_sdk::{
ruma::{events::{AnyTimelineEvent, room::MessagesOptions,
room::message::RoomMessageEventContent, ruma::{
StateEventType}, events::{room::message::RoomMessageEventContent, AnyTimelineEvent, StateEventType},
RoomId, RoomId, TransactionId,
TransactionId}, },
room::MessagesOptions}; Client,
use anyhow::{Result, Error}; };
use cli_log::{error, warn, info}; use std::any::Any;
pub enum State { pub enum State {
None, None,
@ -36,7 +37,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(),
@ -50,26 +51,29 @@ 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(0, match event.event.deserialize() { self.timeline.insert(
Ok(ev) => ev, 0,
Err(err) => { match event.event.deserialize() {
warn!("Failed to deserialize timeline event - {err}"); Ok(ev) => ev,
continue; Err(err) => {
} warn!("Failed to deserialize timeline event - {err}");
}); continue;
}
},
);
} }
Ok(()) Ok(())
} }
@ -87,7 +91,9 @@ impl Room {
self.timeline.push(event); self.timeline.push(event);
} }
pub fn timeline(&self) -> &Vec<AnyTimelineEvent> { &self.timeline } pub fn timeline(&self) -> &Vec<AnyTimelineEvent> {
&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);
@ -114,9 +120,7 @@ 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( rooms.insert(r.room_id().to_string(), Room::new(r.clone()));
r.room_id().to_string(),
Room::new(r.clone()));
} }
}; };
@ -167,7 +171,10 @@ 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!("failed to set room -> invalid room id {}", room_id.to_string()))) Err(Error::msg(format!(
"failed to set room -> invalid room id {}",
room_id.to_string()
)))
} }
} }
@ -176,7 +183,10 @@ impl Status {
self.current_room_id = room_id.clone(); self.current_room_id = room_id.clone();
Ok(()) Ok(())
} else { } else {
Err(Error::msg(format!("failed to set room -> invalid room index {}", room_index))) Err(Error::msg(format!(
"failed to set room -> invalid room index {}",
room_index
)))
} }
} }
@ -187,12 +197,12 @@ impl Status {
pub fn get_room_mut(&mut self, room_id: &RoomId) -> Option<&mut Room> { pub fn get_room_mut(&mut self, room_id: &RoomId) -> Option<&mut Room> {
self.rooms.get_mut(room_id.as_str()) self.rooms.get_mut(room_id.as_str())
} }
pub fn state(&self) -> &State { pub fn state(&self) -> &State {
&self.state &self.state
} }
pub fn set_state(&mut self, state: State) { pub fn set_state(&mut self, state: State) {
self.state = state; self.state = state;
} }
} }

View File

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

View File

@ -1,30 +1,38 @@
use crate::app::status::Status; use crate::app::status::Status;
use std::cmp;
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 anyhow::{Error, Result};
use std::io::Stdout; 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::io; use std::io;
use tui::{backend::CrosstermBackend, layout::{Constraint, Direction, Layout}, widgets::{Block, Borders, Widget}, Terminal, Frame}; use std::io::Stdout;
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::{Spans, Span, Text}; use tui::text::{Span, Spans, 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)]
@ -33,7 +41,7 @@ pub enum MainInputPosition {
Rooms, Rooms,
Messages, Messages,
MessageCompose, MessageCompose,
RoomInfo RoomInfo,
} }
pub struct SetupUI<'a> { pub struct SetupUI<'a> {
@ -54,7 +62,6 @@ 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();
@ -80,9 +87,7 @@ 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( textarea.set_block(b.style(Style::default().fg(Color::DarkGray)));
b.style(Style::default().fg(Color::DarkGray))
);
} }
impl Drop for UI<'_> { impl Drop for UI<'_> {
@ -94,7 +99,9 @@ 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.show_cursor().expect("While destructing UI -> Failed to re-enable cursor"); self.terminal
.show_cursor()
.expect("While destructing UI -> Failed to re-enable cursor");
} }
} }
@ -105,18 +112,9 @@ 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( homeserver.set_block(Block::default().title("Homeserver").borders(Borders::ALL));
Block::default() username.set_block(Block::default().title("Username").borders(Borders::ALL));
.title("Homeserver") password.set_block(Block::default().title("Password").borders(Borders::ALL));
.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);
@ -138,68 +136,74 @@ 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 { &self.input_position } pub fn input_position(&self) -> &SetupInputPosition {
&self.input_position
}
pub async fn update(&'_ mut self, terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> { pub async fn update(
&'_ 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 => Span::styled("OK", Style::default().add_modifier(Modifier::UNDERLINED)), SetupInputPosition::Ok => {
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() let block = Block::default().title("Login").borders(Borders::ALL);
.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()?;
chunk.x = (chunk.width / 2) - 16; chunk.x = (chunk.width / 2) - 16;
chunk.y = (chunk.height / 2) - 5; chunk.y = (chunk.height / 2) - 5;
chunk.height = 12; chunk.height = 12;
chunk.width = 32; chunk.width = 32;
let mut split_chunk = chunk.clone(); let mut split_chunk = chunk.clone();
split_chunk.x += 1; split_chunk.x += 1;
split_chunk.y += 1; split_chunk.y += 1;
split_chunk.height -= 1; split_chunk.height -= 1;
split_chunk.width -= 2; split_chunk.width -= 2;
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([ .constraints(
Constraint::Length(3), // 0. Homserver: [
Constraint::Length(3), // 1. Username: Constraint::Length(3), // 0. Homserver:
Constraint::Length(3), // 2. Password: Constraint::Length(3), // 1. Username:
Constraint::Length(1) // 3. OK Constraint::Length(3), // 2. Password:
].as_ref()) Constraint::Length(1), // 3. OK
]
.as_ref(),
)
.split(split_chunk); .split(split_chunk);
terminal.draw(|frame| { terminal.draw(|frame| {
@ -226,7 +230,8 @@ 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");
@ -235,7 +240,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,
} }
} }
@ -249,14 +254,17 @@ impl UI<'_> {
}; };
} }
pub fn input_position(&self) -> &MainInputPosition { &self.input_position } pub fn input_position(&self) -> &MainInputPosition {
&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<()> {
@ -264,7 +272,14 @@ impl UI<'_> {
let main_chunks = Layout::default() let main_chunks = Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.constraints([Constraint::Length(32), Constraint::Min(16), Constraint::Length(32)].as_ref()) .constraints(
[
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()
@ -274,7 +289,13 @@ impl UI<'_> {
let middle_chunks = Layout::default() let middle_chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([Constraint::Min(4), Constraint::Length(cmp::min(2 + self.message_compose.lines().len() as u16, 8))].as_ref()) .constraints(
[
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()
@ -282,15 +303,22 @@ 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(status.account_name(), Style::default().add_modifier(Modifier::BOLD)); 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(status.account_user_id(), Style::default()));
status_content.extend(Text::styled("settings", Style::default().fg(Color::LightMagenta).add_modifier(Modifier::ITALIC | Modifier::UNDERLINED))); status_content.extend(Text::styled(
"settings",
Style::default()
.fg(Color::LightMagenta)
.add_modifier(Modifier::ITALIC | Modifier::UNDERLINED),
));
let rooms_content = status.rooms() let rooms_content = status
.rooms()
.iter() .iter()
.map(|(_, room)| { .map(|(_, room)| ListItem::new(Span::styled(room.name(), Style::default())))
ListItem::new(Span::styled(room.name(), Style::default()))
})
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let messages_content = match status.room() { let messages_content = match status.room() {
@ -300,7 +328,6 @@ 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 {
@ -312,30 +339,48 @@ 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(message_like_event.sender().to_string(), let mut text = Text::styled(
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)); message_like_event.sender().to_string(),
text.extend(Text::styled(content.to_string(), Style::default()
Style::default().fg(color))); .fg(Color::Cyan)
.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(state.sender().to_string(), Style::default().fg(Color::DarkGray)), Span::styled(
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(state.event_type().to_string(), Style::default().fg(Color::DarkGray)) Span::styled(
state.event_type().to_string(),
Style::default().fg(Color::DarkGray),
),
])]) ])])
} }
} }
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
}, }
None => { None => {
vec![ListItem::new(Text::styled("No room selected!", Style::default().fg(Color::Red)))] vec![ListItem::new(Text::styled(
"No room selected!",
Style::default().fg(Color::Red),
))]
} }
}; };
@ -347,78 +392,129 @@ 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.extend(Text::styled("Encrypted", Style::default().fg(Color::Green))); room_info_content
.extend(Text::styled("Encrypted", Style::default().fg(Color::Green)));
} else { } else {
room_info_content.extend(Text::styled("Not Encrypted!", Style::default().fg(Color::Red))); room_info_content.extend(Text::styled(
"Not Encrypted!",
Style::default().fg(Color::Red),
));
} }
} else { } else {
room_info_content.extend(Text::styled("No room selected!", Style::default().fg(Color::Red))); room_info_content.extend(Text::styled(
"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![Color::White, Color::DarkGray, Color::DarkGray, Color::DarkGray, Color::DarkGray] vec![
}, 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![Color::DarkGray, Color::White, Color::DarkGray, Color::DarkGray, Color::DarkGray] vec![
}, 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![Color::DarkGray, Color::DarkGray, Color::White, Color::DarkGray, Color::DarkGray] vec![
}, 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![Color::DarkGray, Color::DarkGray, Color::DarkGray, Color::DarkGray, Color::DarkGray] vec![
}, 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![Color::DarkGray, Color::DarkGray, Color::DarkGray, Color::DarkGray, Color::White] vec![
}, 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::default() .block(
.title("Status") Block::default()
.borders(Borders::ALL) .title("Status")
.style(Style::default().fg(colors[MainInputPosition::Status as usize]))) .borders(Borders::ALL)
.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::default() .block(
.title("Rooms (navigate: arrow keys)") Block::default()
.borders(Borders::ALL) .title("Rooms (navigate: arrow keys)")
.style(Style::default().fg(colors[MainInputPosition::Rooms as usize]))) .borders(Borders::ALL)
.style(Style::default().fg(colors[MainInputPosition::Rooms as usize])),
)
.style(Style::default().fg(Color::DarkGray)) .style(Style::default().fg(Color::DarkGray))
.highlight_style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)) .highlight_style(
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::default() .block(
.title("Messages") Block::default()
.borders(Borders::ALL) .title("Messages")
.style(Style::default().fg(colors[MainInputPosition::Messages as usize]))) .borders(Borders::ALL)
.style(Style::default().fg(colors[MainInputPosition::Messages as usize])),
)
.start_corner(Corner::BottomLeft) .start_corner(Corner::BottomLeft)
.highlight_symbol(">") .highlight_symbol(">")
.highlight_style(Style::default().fg(Color::LightMagenta).add_modifier(Modifier::BOLD)); .highlight_style(
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::default() .block(
.title("Room Info") Block::default()
.borders(Borders::ALL) .title("Room Info")
.style(Style::default().fg(colors[MainInputPosition::RoomInfo as usize]))) .borders(Borders::ALL)
.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]);
})?; })?;