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]
tui = "0.19"
tui-textarea = { version = "*" }
crossterm = "*"
tui-textarea = { version = "0.2", features = ["crossterm"] }
crossterm = "0.25"
matrix-sdk = "0.6"
anyhow = "1.0"
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
tokio-util = "0.7"
serde = "1.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 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 cli_log::{error, warn, info};
use std::fs;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Account {
@ -33,7 +32,7 @@ pub struct AccountsManager {
}
impl Account {
pub fn name (&self) -> &String {
pub fn name(&self) -> &String {
&self.name
}
@ -43,11 +42,12 @@ impl Account {
}
impl AccountsManager {
pub fn new(config:Option<String>) -> Self {
pub fn new(config: Option<String>) -> Self {
return match config {
Some(s) => {
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();
clients.resize(accounts_data.accounts.len(), None);
Self {
@ -56,7 +56,7 @@ impl AccountsManager {
accounts: accounts_data.accounts,
clients,
}
},
}
None => {
info!("Creating empty AccountsManager");
Self {
@ -66,7 +66,7 @@ impl AccountsManager {
clients: Vec::new(),
}
}
}
};
}
pub async fn restore(&mut self) -> Result<()> {
@ -74,7 +74,12 @@ impl AccountsManager {
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;
self.num_accounts += 1;
@ -84,7 +89,8 @@ impl AccountsManager {
.build()
.await?;
client.login_username(username, password)
client
.login_username(username, password)
.initial_device_display_name("Trinitrix")
.send()
.await?;
@ -94,9 +100,13 @@ impl AccountsManager {
let account = Account {
homeserver: homeserver.to_string(),
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(),
sync_token: None
sync_token: None,
};
self.logout().await?;
@ -105,12 +115,16 @@ impl AccountsManager {
self.clients.push(Some(client));
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)
}
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
let account = if account_id >= self.num_accounts {
@ -120,17 +134,28 @@ impl AccountsManager {
self.get(account_id).expect("Account lookup failed")
};
if self.clients.get(account_id as usize).expect("client lookup failed").is_none() {
info!("No client cached for account: '{}' -> requesting a new one", &account.session.user_id);
if self
.clients
.get(account_id as usize)
.expect("client lookup failed")
.is_none()
{
info!(
"No client cached for account: '{}' -> requesting a new one",
&account.session.user_id
);
let client = Client::builder()
.homeserver_url(&account.homeserver)
.sled_store(format!("userdata/{account_id}"), Some("supersecure")) ?
.sled_store(format!("userdata/{account_id}"), Some("supersecure"))?
.build()
.await?;
client.restore_login(account.session.clone()).await?;
self.clients.insert(account_id as usize, Some(client));
} else {
info!("Using cached client for account: '{}'", &account.session.user_id);
info!(
"Using cached client for account: '{}'",
&account.session.user_id
);
};
info!("Restored account");
@ -156,7 +181,7 @@ impl AccountsManager {
Ok(())
}
pub fn save(&self) -> Result<()>{
pub fn save(&self) -> Result<()> {
let accounts_data = AccountsData {
current_account: self.current_account,
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 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::sync::{mpsc, broadcast};
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)]
pub enum EventStatus {
@ -20,7 +28,7 @@ pub enum EventStatus {
#[derive(Debug)]
pub struct 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 {
@ -42,7 +50,6 @@ impl Default for EventBuilder {
event: Event::default(),
}
}
}
impl EventBuilder {
@ -51,7 +58,10 @@ impl EventBuilder {
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
}
@ -66,7 +76,6 @@ impl EventBuilder {
impl Event {
pub async fn handle(&self, app: &mut App<'_>) -> Result<EventStatus> {
if self.matrix_event.is_some() {
return self.handle_matrix(app).await;
}
@ -88,7 +97,11 @@ impl Event {
None => continue,
};
for m_event in m_room.timeline.events.clone() {
let event = m_event.event.deserialize().unwrap().into_full_event(m_room_id.clone());
let event = m_event
.event
.deserialize()
.unwrap()
.into_full_event(m_room_id.clone());
room.timeline_add(event);
}
}
@ -99,99 +112,125 @@ impl Event {
async fn handle_main(&self, app: &mut App<'_>) -> Result<EventStatus> {
if self.input_event.is_some() {
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 {
key: tui_textarea::Key::Tab,
..
} => {
app.ui.cycle_main_input_position();
}
input => {
match app.ui.input_position() {
ui::MainInputPosition::MessageCompose => {
match input {
tui_textarea::Input {key: tui_textarea::Key::Enter, alt: true, ..} => {
match app.status.room_mut() {
Some(room) => {
room.send(app.ui.message_compose.lines().join("\n")).await?;
app.ui.message_compose_clear();
},
None => ()
};
}
_ => { app.ui.message_compose.input(input); }
};
},
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 => (),
};
},
_ => ()
};
},
_ => (),
input => match app.ui.input_position() {
ui::MainInputPosition::MessageCompose => {
match input {
tui_textarea::Input {
key: tui_textarea::Key::Enter,
alt: true,
..
} => {
match app.status.room_mut() {
Some(room) => {
room.send(app.ui.message_compose.lines().join("\n"))
.await?;
app.ui.message_compose_clear();
}
None => (),
};
}
_ => {
app.ui.message_compose.input(input);
}
};
}
}
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)
@ -200,18 +239,21 @@ impl Event {
async fn handle_setup(&self, app: &mut App<'_>) -> Result<EventStatus> {
let ui = match &mut app.ui.setup_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() {
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 {
key: tui_textarea::Key::Tab,
..
} => {
ui.cycle_input_position();
},
}
tui_textarea::Input {
key: tui_textarea::Key::Enter,
..
@ -225,33 +267,40 @@ impl Event {
if login.is_ok() {
return Ok(EventStatus::Finished);
}
},
}
_ => 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)
}
}
async fn poll_input_events_stage_2(channel: mpsc::Sender<Event>) -> Result<()> {
loop {
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! {
output = poll_input_events_stage_2(channel) => output,
_ = 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<()> {
let sync_settings = SyncSettings::default();
// .token(sync_token)
// .timeout(Duration::from_secs(30));
// .token(sync_token)
// .timeout(Duration::from_secs(30));
let tx = &channel;
client.sync_with_callback(sync_settings, |response| async move {
let event = EventBuilder::default()
.matrix_event(response)
.build();
client
.sync_with_callback(sync_settings, |response| async move {
let event = EventBuilder::default().matrix_event(response).build();
match tx.send(event).await {
Ok(_) => LoopCtrl::Continue,
Err(_) => LoopCtrl::Break,
}
}).await?;
match tx.send(event).await {
Ok(_) => LoopCtrl::Continue,
Err(_) => LoopCtrl::Break,
}
})
.await?;
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! {
output = poll_matrix_events_stage_2(channel, client) => output,
_ = kill.cancelled() => Err(Error::msg("received kill signal")),
}
}
}

View File

@ -4,24 +4,27 @@ pub mod status;
use crate::accounts;
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::AccountsManager;
use anyhow::{Result, Error};
use cli_log::{error, warn, info};
use tokio::{time::{sleep, Duration}, sync::{mpsc, broadcast}};
use anyhow::{Error, Result};
use cli_log::{error, info, warn};
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 status::{Status, State};
pub struct App<'a> {
ui: ui::UI<'a>,
@ -35,14 +38,12 @@ pub struct App<'a> {
}
impl Drop for App<'_> {
fn drop(&mut self) {
}
fn drop(&mut self) {}
}
impl App<'_> {
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() {
info!("Reading account config (userdata/accounts.json)");
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<()> {
// 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() {
info!("No saved sessions found -> jumping into setup");
@ -77,14 +80,13 @@ impl App<'_> {
self.init_account().await?;
}
loop {
self.status.set_state(State::Main);
self.ui.update(&self.status).await?;
let event: event::Event = match self.channel_rx.recv().await {
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? {
@ -107,7 +109,7 @@ impl App<'_> {
let event: event::Event = match self.channel_rx.recv().await {
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? {
@ -121,14 +123,19 @@ impl App<'_> {
pub async fn init_account(&mut self) -> Result<()> {
let client = match self.client() {
Some(c) => c,
None => return Err(Error::msg("failed to get current client"))
}.clone();
None => return Err(Error::msg("failed to get current client")),
}
.clone();
self.matrix_listener_killer.cancel();
self.matrix_listener_killer = CancellationToken::new();
// Spawn Matrix Event Listener
tokio::task::spawn(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
self.status = Status::new(Some(client));
@ -139,10 +146,11 @@ impl App<'_> {
self.status.set_account_name(name);
self.status.set_account_user_id(user_id);
for (_, room) in self.status.rooms_mut() {
room.update_name().await?;
for _ in 0..3 { room.poll_old_timeline().await?; }
for _ in 0..3 {
room.poll_old_timeline().await?;
}
}
info!("Initializing client for the current account");
@ -151,12 +159,18 @@ impl App<'_> {
}
pub async fn switch_account(&mut self, account_id: u32) -> Result<()> {
Ok(())
}
pub async fn login(&mut self, homeserver: &String, username: &String, password: &String) -> Result<()> {
self.accounts_manager.add(homeserver, username, password).await?;
pub async fn login(
&mut self,
homeserver: &String,
username: &String,
password: &String,
) -> Result<()> {
self.accounts_manager
.add(homeserver, username, password)
.await?;
self.init_account().await?;
Ok(())
}
@ -165,11 +179,11 @@ impl App<'_> {
let account = self.accounts_manager.current();
match account {
None => Err(Error::msg("failed to resolve current account")),
Some(a) => Ok(a)
Some(a) => Ok(a),
}
}
pub fn client(&self) -> Option<&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 matrix_sdk::{Client,
ruma::{events::{AnyTimelineEvent,
room::message::RoomMessageEventContent,
StateEventType},
RoomId,
TransactionId},
room::MessagesOptions};
use anyhow::{Result, Error};
use cli_log::{error, warn, info};
use matrix_sdk::{
room::MessagesOptions,
ruma::{
events::{room::message::RoomMessageEventContent, AnyTimelineEvent, StateEventType},
RoomId, TransactionId,
},
Client,
};
use std::any::Any;
pub enum State {
None,
@ -36,7 +37,7 @@ pub struct Status {
}
impl Room {
pub fn new (matrix_room: matrix_sdk::room::Joined) -> Self {
pub fn new(matrix_room: matrix_sdk::room::Joined) -> Self {
Self {
matrix_room,
name: "".to_string(),
@ -50,26 +51,29 @@ impl Room {
pub async fn poll_old_timeline(&mut self) -> Result<()> {
if let Some(AnyTimelineEvent::State(event)) = &self.timeline.get(0) {
if event.event_type() == StateEventType::RoomCreate {
return Ok(())
return Ok(());
}
}
let mut messages_options = MessagesOptions::backward();
messages_options = match &self.timeline_end {
Some(end) => messages_options.from(end.as_str()),
None => messages_options
None => messages_options,
};
let events = self.matrix_room.messages(messages_options).await?;
self.timeline_end = events.end;
for event in events.chunk.iter() {
self.timeline.insert(0, match event.event.deserialize() {
Ok(ev) => ev,
Err(err) => {
warn!("Failed to deserialize timeline event - {err}");
continue;
}
});
self.timeline.insert(
0,
match event.event.deserialize() {
Ok(ev) => ev,
Err(err) => {
warn!("Failed to deserialize timeline event - {err}");
continue;
}
},
);
}
Ok(())
}
@ -87,7 +91,9 @@ impl Room {
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<()> {
let content = RoomMessageEventContent::text_plain(message);
@ -114,9 +120,7 @@ impl Status {
let mut rooms = IndexMap::new();
if let Some(c) = &client {
for r in c.joined_rooms() {
rooms.insert(
r.room_id().to_string(),
Room::new(r.clone()));
rooms.insert(r.room_id().to_string(), Room::new(r.clone()));
}
};
@ -167,7 +171,10 @@ impl Status {
self.current_room_id = room_id.to_string();
Ok(())
} else {
Err(Error::msg(format!("failed to set room -> invalid room id {}", room_id.to_string())))
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();
Ok(())
} 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> {
self.rooms.get_mut(room_id.as_str())
}
pub fn state(&self) -> &State {
&self.state
}
pub fn set_state(&mut self, state: State) {
self.state = state;
}
}
}

View File

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

View File

@ -1,30 +1,38 @@
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 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 tui::{backend::CrosstermBackend, layout::{Constraint, Direction, Layout}, widgets::{Block, Borders, Widget}, Terminal, Frame};
use std::io::Stdout;
use tui::layout::{Alignment, Corner};
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::{
backend::CrosstermBackend,
layout::{Constraint, Direction, Layout},
widgets::{Block, Borders, Widget},
Frame, Terminal,
};
use tui_textarea::{Input, Key, TextArea};
use cli_log::{error, warn, info};
use matrix_sdk::{room::MessagesOptions, ruma::events::{AnyTimelineEvent, AnyMessageLikeEvent}};
#[derive(Clone, Copy)]
pub enum SetupInputPosition {
Homeserver,
Username,
Password,
Ok
Ok,
}
#[derive(Clone, Copy)]
@ -33,7 +41,7 @@ pub enum MainInputPosition {
Rooms,
Messages,
MessageCompose,
RoomInfo
RoomInfo,
}
pub struct SetupUI<'a> {
@ -54,7 +62,6 @@ pub struct UI<'a> {
pub setup_ui: Option<SetupUI<'a>>,
}
fn terminal_prepare() -> Result<Stdout> {
enable_raw_mode()?;
let mut stdout = io::stdout();
@ -80,9 +87,7 @@ pub fn textarea_inactivate(textarea: &mut TextArea) {
.block()
.cloned()
.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<'_> {
@ -94,7 +99,9 @@ impl Drop for UI<'_> {
LeaveAlternateScreen,
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_data = TextArea::default();
homeserver.set_block(
Block::default()
.title("Homeserver")
.borders(Borders::ALL));
username.set_block(
Block::default()
.title("Username")
.borders(Borders::ALL));
password.set_block(
Block::default()
.title("Password")
.borders(Borders::ALL));
homeserver.set_block(Block::default().title("Homeserver").borders(Borders::ALL));
username.set_block(Block::default().title("Username").borders(Borders::ALL));
password.set_block(Block::default().title("Password").borders(Borders::ALL));
textarea_activate(&mut homeserver);
textarea_inactivate(&mut username);
@ -138,68 +136,74 @@ impl SetupUI<'_> {
textarea_activate(&mut self.username);
textarea_inactivate(&mut self.password);
SetupInputPosition::Username
},
}
SetupInputPosition::Username => {
textarea_inactivate(&mut self.homeserver);
textarea_inactivate(&mut self.username);
textarea_activate(&mut self.password);
SetupInputPosition::Password
},
}
SetupInputPosition::Password => {
textarea_inactivate(&mut self.homeserver);
textarea_inactivate(&mut self.username);
textarea_inactivate(&mut self.password);
SetupInputPosition::Ok
},
}
SetupInputPosition::Ok => {
textarea_activate(&mut self.homeserver);
textarea_inactivate(&mut self.username);
textarea_inactivate(&mut self.password);
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();
strings.resize(3, "".to_string());
let content_ok = match self.input_position {
SetupInputPosition:: Ok => Span::styled("OK", Style::default().add_modifier(Modifier::UNDERLINED)),
let content_ok = match self.input_position {
SetupInputPosition::Ok => {
Span::styled("OK", Style::default().add_modifier(Modifier::UNDERLINED))
}
_ => Span::styled("OK", Style::default().fg(Color::DarkGray)),
};
let block = Block::default()
.title("Login")
.borders(Borders::ALL);
let mut ok = Paragraph::new(content_ok)
.alignment(Alignment::Center);
let block = Block::default().title("Login").borders(Borders::ALL);
let mut ok = Paragraph::new(content_ok).alignment(Alignment::Center);
// define a 32 * 6 chunk in the middle of the screen
let mut chunk = terminal.size()?;
chunk.x = (chunk.width / 2) - 16;
chunk.y = (chunk.height / 2) - 5;
chunk.x = (chunk.width / 2) - 16;
chunk.y = (chunk.height / 2) - 5;
chunk.height = 12;
chunk.width = 32;
chunk.width = 32;
let mut split_chunk = chunk.clone();
split_chunk.x += 1;
split_chunk.y += 1;
split_chunk.x += 1;
split_chunk.y += 1;
split_chunk.height -= 1;
split_chunk.width -= 2;
split_chunk.width -= 2;
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), // 0. Homserver:
Constraint::Length(3), // 1. Username:
Constraint::Length(3), // 2. Password:
Constraint::Length(1) // 3. OK
].as_ref())
.constraints(
[
Constraint::Length(3), // 0. Homserver:
Constraint::Length(3), // 1. Username:
Constraint::Length(3), // 2. Password:
Constraint::Length(1), // 3. OK
]
.as_ref(),
)
.split(split_chunk);
terminal.draw(|frame| {
@ -226,7 +230,8 @@ impl UI<'_> {
message_compose.set_block(
Block::default()
.title("Message Compose (send: <Alt>+<Enter>)")
.borders(Borders::ALL));
.borders(Borders::ALL),
);
info!("Initialized UI");
@ -235,7 +240,7 @@ impl UI<'_> {
input_position: MainInputPosition::Rooms,
rooms_state: ListState::default(),
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) {
self.message_compose = TextArea::default();
self.message_compose.set_block(
Block::default()
.title("Message Compose (send: <Alt>+<Enter>)")
.borders(Borders::ALL));
.borders(Borders::ALL),
);
}
pub async fn update(&mut self, status: &Status) -> Result<()> {
@ -264,7 +272,14 @@ impl UI<'_> {
let main_chunks = Layout::default()
.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);
let left_chunks = Layout::default()
@ -274,7 +289,13 @@ impl UI<'_> {
let middle_chunks = Layout::default()
.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]);
let right_chunks = Layout::default()
@ -282,15 +303,22 @@ impl UI<'_> {
.constraints([Constraint::Min(4)].as_ref())
.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("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()
.map(|(_, room)| {
ListItem::new(Span::styled(room.name(), Style::default()))
})
.map(|(_, room)| ListItem::new(Span::styled(room.name(), Style::default())))
.collect::<Vec<_>>();
let messages_content = match status.room() {
@ -300,7 +328,6 @@ impl UI<'_> {
.rev()
.map(|event| {
match event {
// Message Like Events
AnyTimelineEvent::MessageLike(message_like_event) => {
let (content, color) = match &message_like_event {
@ -312,30 +339,48 @@ impl UI<'_> {
.body();
(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(),
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD));
text.extend(Text::styled(content.to_string(),
Style::default().fg(color)));
let mut text = Text::styled(
message_like_event.sender().to_string(),
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
);
text.extend(Text::styled(
content.to_string(),
Style::default().fg(color),
));
ListItem::new(text)
},
}
// State Events
AnyTimelineEvent::State(state) => {
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(state.event_type().to_string(), Style::default().fg(Color::DarkGray))
Span::styled(
state.event_type().to_string(),
Style::default().fg(Color::DarkGray),
),
])])
}
}
})
.collect::<Vec<_>>()
},
}
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)));
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 {
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 {
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
let colors = match self.input_position {
MainInputPosition::Status => {
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 => {
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 => {
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 => {
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 => {
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
let status_panel = Paragraph::new(status_content)
.block(Block::default()
.title("Status")
.borders(Borders::ALL)
.style(Style::default().fg(colors[MainInputPosition::Status as usize])))
.block(
Block::default()
.title("Status")
.borders(Borders::ALL)
.style(Style::default().fg(colors[MainInputPosition::Status as usize])),
)
.alignment(Alignment::Left);
let rooms_panel = List::new(rooms_content)
.block(Block::default()
.title("Rooms (navigate: arrow keys)")
.borders(Borders::ALL)
.style(Style::default().fg(colors[MainInputPosition::Rooms as usize])))
.block(
Block::default()
.title("Rooms (navigate: arrow keys)")
.borders(Borders::ALL)
.style(Style::default().fg(colors[MainInputPosition::Rooms as usize])),
)
.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(">");
let messages_panel = List::new(messages_content)
.block(Block::default()
.title("Messages")
.borders(Borders::ALL)
.style(Style::default().fg(colors[MainInputPosition::Messages as usize])))
.block(
Block::default()
.title("Messages")
.borders(Borders::ALL)
.style(Style::default().fg(colors[MainInputPosition::Messages as usize])),
)
.start_corner(Corner::BottomLeft)
.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)
.block(Block::default()
.title("Room Info")
.borders(Borders::ALL)
.style(Style::default().fg(colors[MainInputPosition::RoomInfo as usize])))
.block(
Block::default()
.title("Room Info")
.borders(Borders::ALL)
.style(Style::default().fg(colors[MainInputPosition::RoomInfo as usize])),
)
.alignment(Alignment::Center);
// render the widgets
self.terminal.draw(|frame| {
frame.render_widget(status_panel, left_chunks[0]);
frame.render_stateful_widget(rooms_panel, left_chunks[1], &mut self.rooms_state);
frame.render_stateful_widget(messages_panel, middle_chunks[0], & mut messages_state);
frame.render_stateful_widget(messages_panel, middle_chunks[0], &mut messages_state);
frame.render_widget(self.message_compose.widget(), middle_chunks[1]);
frame.render_widget(room_info_panel, right_chunks[0]);
})?;