refactor(src): Remove all matrix related code
This obviously is a big regression, but having this matrix code in the core trinitrix tree is no longer planned. And if we start writing the matrix cbs, referring to this commit should be possible.
This commit is contained in:
parent
c233b30a52
commit
08c4724a94
19
Cargo.toml
19
Cargo.toml
|
@ -7,20 +7,17 @@ default-run = "trinitrix"
|
|||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
default = ["tui"]
|
||||
tui = ["dep:tui", "dep:tui-textarea", "dep:crossterm", "dep:tokio-util", "dep:serde", "dep:indexmap"]
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5.4", features = ["derive"] }
|
||||
cli-log = "2.0"
|
||||
anyhow = "1.0"
|
||||
matrix-sdk = "0.6"
|
||||
tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] }
|
||||
tokio = { version = "1.37", features = ["macros", "rt-multi-thread", "fs", "time"] }
|
||||
tokio-util = {version = "0.7.10"}
|
||||
|
||||
# config
|
||||
trinitry = {version = "0.1.0"}
|
||||
keymaps = {version = "0.1.1", features = ["crossterm"] }
|
||||
directories = "5.0.1"
|
||||
|
||||
# c api
|
||||
libloading = "0.8.3"
|
||||
|
@ -31,13 +28,9 @@ mlua = { version = "0.9.7", features = ["lua54", "async", "send", "serialize"] }
|
|||
once_cell = "1.19.0"
|
||||
|
||||
# tui feature specific parts
|
||||
tui = {version = "0.19", optional = true}
|
||||
tui-textarea = { version = "0.2", features = ["crossterm"], optional = true }
|
||||
crossterm = { version = "0.25", optional = true }
|
||||
tokio-util = { version = "0.7", optional = true }
|
||||
serde = { version = "1.0", optional = true }
|
||||
indexmap = { version = "2.2.6", optional = true }
|
||||
directories = "5.0.1"
|
||||
tui = {version = "0.19"}
|
||||
tui-textarea = { version = "0.2", features = ["crossterm"]}
|
||||
crossterm = { version = "0.25"}
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1.4.0"
|
||||
|
|
|
@ -1,223 +0,0 @@
|
|||
use std::fs;
|
||||
|
||||
use anyhow::{Error, Result};
|
||||
use cli_log::{error, info};
|
||||
use matrix_sdk::{ruma::exports::serde_json, Client, Session};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Account {
|
||||
homeserver: String,
|
||||
id: u32,
|
||||
name: String,
|
||||
|
||||
session: Session,
|
||||
sync_token: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct AccountsData {
|
||||
current_account: u32,
|
||||
accounts: Vec<Account>,
|
||||
}
|
||||
|
||||
pub struct AccountsManager {
|
||||
current_account: u32,
|
||||
num_accounts: u32,
|
||||
accounts: Vec<Account>,
|
||||
clients: Vec<Option<Client>>,
|
||||
}
|
||||
|
||||
impl Account {
|
||||
pub fn name(&self) -> &String {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn user_id(&self) -> String {
|
||||
self.session.user_id.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl AccountsManager {
|
||||
pub fn new(config: Option<String>) -> Result<Self> {
|
||||
return match config {
|
||||
Some(s) => {
|
||||
info!("Loading serialized AccountsManager");
|
||||
let accounts_data: AccountsData = serde_json::from_str(&s)?;
|
||||
let mut clients = Vec::new();
|
||||
clients.resize(accounts_data.accounts.len(), None);
|
||||
Ok(Self {
|
||||
current_account: accounts_data.current_account,
|
||||
num_accounts: accounts_data.accounts.len() as u32,
|
||||
accounts: accounts_data.accounts,
|
||||
clients,
|
||||
})
|
||||
}
|
||||
None => {
|
||||
info!("Creating empty AccountsManager");
|
||||
Ok(Self {
|
||||
current_account: 0,
|
||||
num_accounts: 0,
|
||||
accounts: Vec::new(),
|
||||
clients: Vec::new(),
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub async fn restore(&mut self) -> Result<()> {
|
||||
self.login(self.current_account).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn add(
|
||||
&mut self,
|
||||
homeserver: &String,
|
||||
username: &String,
|
||||
password: &String,
|
||||
) -> Result<u32> {
|
||||
let id = self.num_accounts;
|
||||
self.num_accounts += 1;
|
||||
|
||||
let client = Client::builder()
|
||||
.homeserver_url(homeserver)
|
||||
.sled_store(format!("userdata/{id}"), Some("supersecure"))?
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
client
|
||||
.login_username(username, password)
|
||||
.initial_device_display_name("Trinitrix")
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let session = match client.session() {
|
||||
Some(s) => s,
|
||||
None => return Err(Error::msg("Failed to get session")),
|
||||
};
|
||||
|
||||
let name = match client.account().get_display_name().await? {
|
||||
Some(n) => n,
|
||||
None => return Err(Error::msg("Failed to get display name")),
|
||||
};
|
||||
|
||||
let account = Account {
|
||||
homeserver: homeserver.to_string(),
|
||||
id,
|
||||
name,
|
||||
session: session.clone(),
|
||||
sync_token: None,
|
||||
};
|
||||
|
||||
self.logout().await?;
|
||||
self.current_account = id;
|
||||
self.accounts.push(account);
|
||||
self.clients.push(Some(client));
|
||||
self.save()?;
|
||||
|
||||
info!(
|
||||
"Logged in as '{}' device ID: {}",
|
||||
session.user_id.to_string(),
|
||||
session.device_id.to_string()
|
||||
);
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
pub async fn login(&mut self, account_id: u32) -> Result<()> {
|
||||
self.logout().await?; // log out the current account
|
||||
|
||||
let account = if account_id >= self.num_accounts {
|
||||
error!("Tried to log in with an invalid account ID {}", account_id);
|
||||
return Err(Error::msg("Invalid account ID"));
|
||||
} else {
|
||||
if let Some(a) = self.get(account_id) {
|
||||
a
|
||||
} else {
|
||||
return Err(Error::msg("Failed to get account"));
|
||||
}
|
||||
};
|
||||
|
||||
if self
|
||||
.clients
|
||||
.get(account_id as usize)
|
||||
.expect("client lookup failed")
|
||||
.is_none()
|
||||
{
|
||||
info!(
|
||||
"No client cached for account: '{}' -> requesting a new one",
|
||||
&account.session.user_id
|
||||
);
|
||||
let client = Client::builder()
|
||||
.homeserver_url(&account.homeserver)
|
||||
.sled_store(format!("userdata/{account_id}"), Some("supersecure"))?
|
||||
.build()
|
||||
.await?;
|
||||
client.restore_login(account.session.clone()).await?;
|
||||
self.clients.insert(account_id as usize, Some(client));
|
||||
} else {
|
||||
info!(
|
||||
"Using cached client for account: '{}'",
|
||||
&account.session.user_id
|
||||
);
|
||||
};
|
||||
|
||||
info!("Restored account");
|
||||
|
||||
self.current_account = account_id;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn logout(&mut self) -> Result<()> {
|
||||
// idk, do some matrix related stuff in here or something like that
|
||||
if self.clients.get(self.current_account as usize).is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
let client = match self.clients.get(self.current_account as usize).unwrap() {
|
||||
None => return Ok(()),
|
||||
Some(c) => c,
|
||||
};
|
||||
|
||||
info!("Logged out '{}' locally", client.session().unwrap().user_id);
|
||||
client.logout().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn save(&self) -> Result<()> {
|
||||
let accounts_data = AccountsData {
|
||||
current_account: self.current_account,
|
||||
accounts: self.accounts.clone(),
|
||||
};
|
||||
|
||||
let serialized = serde_json::to_string(&accounts_data)?;
|
||||
fs::write("userdata/accounts.json", serialized)?;
|
||||
|
||||
info!("Saved serialized accounts config (userdata/accounts.json)");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get(&self, id: u32) -> Option<&Account> {
|
||||
self.accounts.get(id as usize)
|
||||
}
|
||||
|
||||
pub fn current(&self) -> Option<&Account> {
|
||||
self.get(self.current_account)
|
||||
}
|
||||
|
||||
pub fn client(&self) -> Option<&Client> {
|
||||
match self.clients.get(self.current_account as usize) {
|
||||
None => None,
|
||||
Some(oc) => match oc {
|
||||
None => None,
|
||||
Some(c) => Some(c),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn num_accounts(&self) -> u32 {
|
||||
self.num_accounts
|
||||
}
|
||||
}
|
|
@ -40,20 +40,12 @@ mod trinitrix {
|
|||
/// Closes the application
|
||||
fn exit();
|
||||
|
||||
/// Send a message to the current room
|
||||
/// The send message is interpreted literally.
|
||||
fn room_message_send(message: String);
|
||||
|
||||
//// Open the help pages at the first occurrence of
|
||||
//// the input string if it is Some, otherwise open
|
||||
//// the help pages at the start
|
||||
// TODO(@soispha): To be implemented <2024-03-09>
|
||||
// fn help(Option<String>);
|
||||
|
||||
//// Register a function to be used with the Trinitrix api
|
||||
// (This function is not actually implemented here)
|
||||
/* declare register_function: false, */
|
||||
|
||||
/// Function that change the UI, or UI state
|
||||
mod ui {
|
||||
enum Mode {
|
||||
|
|
|
@ -87,21 +87,6 @@ pub async fn handle(
|
|||
warn!("Terminating the application");
|
||||
EventStatus::Terminate
|
||||
}
|
||||
Api::room_message_send { message } => {
|
||||
if let Some(room) = app.status.room_mut() {
|
||||
room.send(message.to_string()).await?;
|
||||
send_status_output!("Sent message: `{}`", message);
|
||||
} else {
|
||||
// FIXME(@soispha): This should raise an error within lua, as it would
|
||||
// otherwise be very confusing <2023-09-20>
|
||||
warn!(
|
||||
"Can't send message: `{}`, as there is no open room!",
|
||||
&message
|
||||
);
|
||||
}
|
||||
|
||||
EventStatus::Ok
|
||||
}
|
||||
Api::Ui(ui) => match ui {
|
||||
Ui::set_mode { mode } => match mode {
|
||||
Mode::Normal => {
|
||||
|
@ -213,7 +198,6 @@ pub async fn handle(
|
|||
EventStatus::Ok
|
||||
}
|
||||
State::Normal
|
||||
| State::Setup
|
||||
| State::KeyInputPending {
|
||||
old_state: _,
|
||||
pending_keys: _,
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use matrix_sdk::deserialized_responses::SyncResponse;
|
||||
|
||||
use crate::app::{events::EventStatus, App};
|
||||
|
||||
pub async fn handle(app: &mut App<'_>, sync: &SyncResponse) -> Result<EventStatus> {
|
||||
for (m_room_id, m_room) in sync.rooms.join.iter() {
|
||||
let room = match app.status.get_room_mut(m_room_id) {
|
||||
Some(r) => r,
|
||||
None => continue,
|
||||
};
|
||||
for m_event in m_room.timeline.events.clone() {
|
||||
let event = m_event
|
||||
.event
|
||||
.deserialize()
|
||||
.unwrap()
|
||||
.into_full_event(m_room_id.clone());
|
||||
room.timeline_add(event);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(EventStatus::Ok)
|
||||
}
|
|
@ -1,9 +1,5 @@
|
|||
// input events
|
||||
pub mod input;
|
||||
pub mod setup;
|
||||
|
||||
// matrix
|
||||
pub mod matrix;
|
||||
|
||||
// ci
|
||||
pub mod command;
|
||||
|
|
|
@ -1,75 +0,0 @@
|
|||
use anyhow::{bail, Context, Result};
|
||||
use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEvent};
|
||||
|
||||
use crate::{
|
||||
app::{events::EventStatus, App},
|
||||
ui::setup,
|
||||
};
|
||||
|
||||
pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<EventStatus> {
|
||||
let ui = match &mut app.ui.setup_ui {
|
||||
Some(ui) => ui,
|
||||
None => bail!("SetupUI instance not found"),
|
||||
};
|
||||
|
||||
match input_event {
|
||||
CrosstermEvent::Key(KeyEvent {
|
||||
code: KeyCode::Esc, ..
|
||||
}) => return Ok(EventStatus::Terminate),
|
||||
CrosstermEvent::Key(KeyEvent {
|
||||
code: KeyCode::Tab, ..
|
||||
}) => {
|
||||
ui.cycle_input_position();
|
||||
}
|
||||
CrosstermEvent::Key(KeyEvent {
|
||||
code: KeyCode::BackTab,
|
||||
..
|
||||
}) => {
|
||||
ui.cycle_input_position_rev();
|
||||
}
|
||||
CrosstermEvent::Key(KeyEvent {
|
||||
code: KeyCode::Enter,
|
||||
..
|
||||
}) => {
|
||||
match ui.input_position() {
|
||||
setup::InputPosition::Ok => {
|
||||
let homeserver = ui.homeserver.lines()[0].clone();
|
||||
let username = ui.username.lines()[0].clone();
|
||||
let password = ui.password_data.lines()[0].clone();
|
||||
app.login(&homeserver, &username, &password)
|
||||
.await
|
||||
.context("Failed to login")?;
|
||||
// We bailed in the line above, thus login must have succeeded
|
||||
return Ok(EventStatus::Finished);
|
||||
}
|
||||
_ => ui.cycle_input_position(),
|
||||
};
|
||||
}
|
||||
input => match ui.input_position() {
|
||||
setup::InputPosition::Homeserver => {
|
||||
ui.homeserver.input(input.to_owned());
|
||||
}
|
||||
setup::InputPosition::Username => {
|
||||
ui.username.input(input.to_owned());
|
||||
}
|
||||
setup::InputPosition::Password => {
|
||||
let textarea_input = tui_textarea::Input::from(input.to_owned());
|
||||
ui.password_data.input(textarea_input.clone());
|
||||
match textarea_input.key {
|
||||
tui_textarea::Key::Char(_) => {
|
||||
ui.password.input(tui_textarea::Input {
|
||||
key: tui_textarea::Key::Char('*'),
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
});
|
||||
}
|
||||
_ => {
|
||||
ui.password.input(textarea_input);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
};
|
||||
Ok(EventStatus::Ok)
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
use crate::app::events::Event;
|
||||
use anyhow::{bail, Result};
|
||||
use tokio::{sync::mpsc, time::Duration};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
pub async fn poll(channel: mpsc::Sender<Event>, kill: CancellationToken) -> Result<()> {
|
||||
async fn stage_2(channel: mpsc::Sender<Event>) -> Result<()> {
|
||||
loop {
|
||||
if crossterm::event::poll(Duration::from_millis(100))? {
|
||||
let event = Event::InputEvent(crossterm::event::read()?);
|
||||
channel.send(event).await?;
|
||||
} else {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
tokio::select! {
|
||||
output = stage_2(channel) => output,
|
||||
_ = kill.cancelled() => bail!("received kill signal")
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
/* WARNING(@antifallobst):
|
||||
* This file is going to be removed while implementing Chat Backend Servers!
|
||||
* <19-10-2023>
|
||||
*/
|
||||
|
||||
use crate::app::events::Event;
|
||||
use anyhow::{bail, Result};
|
||||
use matrix_sdk::{config::SyncSettings, Client, LoopCtrl};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
pub async fn poll(
|
||||
channel: mpsc::Sender<Event>,
|
||||
kill: CancellationToken,
|
||||
client: Client,
|
||||
) -> Result<()> {
|
||||
async fn stage_2(channel: mpsc::Sender<Event>, client: Client) -> Result<()> {
|
||||
let sync_settings = SyncSettings::default();
|
||||
// .token(sync_token)
|
||||
// .timeout(Duration::from_secs(30));
|
||||
|
||||
let tx = &channel;
|
||||
|
||||
client
|
||||
.sync_with_callback(sync_settings, |response| async move {
|
||||
let event = Event::MatrixEvent(response);
|
||||
|
||||
match tx.send(event).await {
|
||||
Ok(_) => LoopCtrl::Continue,
|
||||
Err(_) => LoopCtrl::Break,
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
tokio::select! {
|
||||
output = stage_2(channel, client) => output,
|
||||
_ = kill.cancelled() => bail!("received kill signal"),
|
||||
}
|
||||
}
|
|
@ -1,2 +1,21 @@
|
|||
pub mod input;
|
||||
pub mod matrix;
|
||||
use crate::app::events::Event;
|
||||
use anyhow::{bail, Result};
|
||||
use tokio::{sync::mpsc, time::Duration};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
pub async fn poll(channel: mpsc::Sender<Event>, kill: CancellationToken) -> Result<()> {
|
||||
async fn stage_2(channel: mpsc::Sender<Event>) -> Result<()> {
|
||||
loop {
|
||||
if crossterm::event::poll(Duration::from_millis(100))? {
|
||||
let event = Event::InputEvent(crossterm::event::read()?);
|
||||
channel.send(event).await?;
|
||||
} else {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
tokio::select! {
|
||||
output = stage_2(channel) => output,
|
||||
_ = kill.cancelled() => bail!("received kill signal")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,16 +3,16 @@ pub mod listeners;
|
|||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use crate::app::{command_interface::Commands, status::State, App};
|
||||
use crate::app::{command_interface::Commands, App};
|
||||
use cli_log::{trace, warn};
|
||||
use crossterm::event::Event as CrosstermEvent;
|
||||
use handlers::{command, input, lua_command, matrix, setup};
|
||||
use handlers::{command, input};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Event {
|
||||
InputEvent(CrosstermEvent),
|
||||
MatrixEvent(matrix_sdk::deserialized_responses::SyncResponse),
|
||||
// FIXME(@soispha): The `String` is also wrong <2024-05-03>
|
||||
|
||||
// FIXME(@soispha): The `String` here is just wrong <2024-05-03>
|
||||
CommandEvent(Commands, Option<trixy::oneshot::Sender<String>>),
|
||||
LuaCommand(String),
|
||||
}
|
||||
|
@ -21,10 +21,6 @@ impl Event {
|
|||
pub async fn handle(self, app: &mut App<'_>) -> Result<EventStatus> {
|
||||
trace!("Received event to handle: `{:#?}`", &self);
|
||||
match self {
|
||||
Event::MatrixEvent(event) => matrix::handle(app, &event)
|
||||
.await
|
||||
.with_context(|| format!("Failed to handle matrix event: `{:#?}`", event)),
|
||||
|
||||
Event::CommandEvent(event, callback_tx) => command::handle(app, &event, callback_tx)
|
||||
.await
|
||||
.with_context(|| format!("Failed to handle command event: `{:#?}`", event)),
|
||||
|
@ -43,9 +39,6 @@ impl Event {
|
|||
// .await
|
||||
// .with_context(|| format!("Failed to handle function: `{}`", function)),
|
||||
Event::InputEvent(event) => match app.status.state() {
|
||||
State::Setup => setup::handle(app, &event).await.with_context(|| {
|
||||
format!("Failed to handle input (setup) event: `{:#?}`", event)
|
||||
}),
|
||||
_ => input::handle(app, &event).await.with_context(|| {
|
||||
format!("Failed to handle input (non-setup) event: `{:#?}`", event)
|
||||
}),
|
||||
|
|
127
src/app/mod.rs
127
src/app/mod.rs
|
@ -3,24 +3,13 @@ pub mod config;
|
|||
pub mod events;
|
||||
pub mod status;
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ffi::c_int,
|
||||
path::{Path, PathBuf},
|
||||
sync::OnceLock,
|
||||
};
|
||||
use std::{collections::HashMap, ffi::c_int, path::PathBuf, sync::OnceLock};
|
||||
|
||||
use anyhow::{Context, Error, Result};
|
||||
use anyhow::{Context, Result};
|
||||
use cli_log::{info, warn};
|
||||
use crossterm::{
|
||||
event::DisableMouseCapture,
|
||||
execute,
|
||||
terminal::{disable_raw_mode, LeaveAlternateScreen},
|
||||
};
|
||||
use directories::ProjectDirs;
|
||||
use keymaps::trie::Node;
|
||||
use libloading::{Library, Symbol};
|
||||
use matrix_sdk::Client;
|
||||
use tokio::sync::mpsc::{self, Sender};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
|
@ -29,24 +18,21 @@ use tokio_util::sync::CancellationToken;
|
|||
// };
|
||||
|
||||
use crate::{
|
||||
accounts::{Account, AccountsManager},
|
||||
app::{
|
||||
events::{Event, EventStatus},
|
||||
status::{State, Status},
|
||||
},
|
||||
ui::{central, setup},
|
||||
ui::central,
|
||||
};
|
||||
|
||||
pub struct App<'runtime> {
|
||||
ui: central::UI<'runtime>,
|
||||
accounts_manager: AccountsManager,
|
||||
status: Status,
|
||||
|
||||
tx: mpsc::Sender<Event>,
|
||||
rx: mpsc::Receiver<Event>,
|
||||
|
||||
input_listener_killer: CancellationToken,
|
||||
matrix_listener_killer: CancellationToken,
|
||||
|
||||
// lua: LuaCommandManager,
|
||||
project_dirs: ProjectDirs,
|
||||
|
@ -58,14 +44,6 @@ pub static COMMAND_TRANSMITTER: OnceLock<Sender<Event>> = OnceLock::new();
|
|||
|
||||
impl App<'_> {
|
||||
pub fn new() -> Result<Self> {
|
||||
let path: &std::path::Path = Path::new("userdata/accounts.json");
|
||||
let config = if path.exists() {
|
||||
info!("Reading account config (userdata/accounts.json)");
|
||||
Some(std::fs::read_to_string(path)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (tx, rx) = mpsc::channel(256);
|
||||
|
||||
COMMAND_TRANSMITTER
|
||||
|
@ -74,13 +52,11 @@ impl App<'_> {
|
|||
|
||||
Ok(Self {
|
||||
ui: central::UI::new()?,
|
||||
accounts_manager: AccountsManager::new(config)?,
|
||||
status: Status::new(None),
|
||||
status: Status::new(),
|
||||
|
||||
tx: tx.clone(),
|
||||
rx,
|
||||
input_listener_killer: CancellationToken::new(),
|
||||
matrix_listener_killer: CancellationToken::new(),
|
||||
|
||||
// lua: LuaCommandManager::new(tx),
|
||||
|
||||
|
@ -99,7 +75,7 @@ impl App<'_> {
|
|||
plugin_path: Option<PathBuf>,
|
||||
) -> Result<()> {
|
||||
// Spawn input event listener
|
||||
tokio::task::spawn(events::listeners::input::poll(
|
||||
tokio::task::spawn(events::listeners::poll(
|
||||
self.tx.clone(),
|
||||
self.input_listener_killer.clone(),
|
||||
));
|
||||
|
@ -147,14 +123,6 @@ impl App<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
if self.account().is_err() {
|
||||
info!("No saved sessions found -> jumping into setup");
|
||||
self.setup().await?;
|
||||
} else {
|
||||
self.accounts_manager.restore().await?;
|
||||
self.init_account().await?;
|
||||
}
|
||||
|
||||
self.status.set_state(State::Normal);
|
||||
|
||||
loop {
|
||||
|
@ -171,89 +139,4 @@ impl App<'_> {
|
|||
self.input_listener_killer.cancel();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn setup(&mut self) -> Result<()> {
|
||||
self.ui.setup_ui = Some(setup::UI::new());
|
||||
|
||||
self.status.set_state(State::Setup);
|
||||
|
||||
loop {
|
||||
self.ui.update_setup().await?;
|
||||
|
||||
let event = self.rx.recv().await.context("Failed to get next event")?;
|
||||
|
||||
match event.handle(self).await? {
|
||||
EventStatus::Ok => (),
|
||||
EventStatus::Finished => return Ok(()),
|
||||
EventStatus::Terminate => return Err(Error::msg("Terminated by user")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn init_account(&mut self) -> Result<()> {
|
||||
let client = match self.client() {
|
||||
Some(c) => c,
|
||||
None => return Err(Error::msg("failed to get current client")),
|
||||
}
|
||||
.clone();
|
||||
|
||||
self.matrix_listener_killer.cancel();
|
||||
self.matrix_listener_killer = CancellationToken::new();
|
||||
|
||||
// Spawn Matrix Event Listener
|
||||
tokio::task::spawn(events::listeners::matrix::poll(
|
||||
self.tx.clone(),
|
||||
self.matrix_listener_killer.clone(),
|
||||
client.clone(),
|
||||
));
|
||||
|
||||
// Reset Status
|
||||
self.status = Status::new(Some(client));
|
||||
|
||||
let account = self.account()?;
|
||||
let name = account.name().clone();
|
||||
let user_id = account.user_id().clone();
|
||||
self.status.set_account_name(name);
|
||||
self.status.set_account_user_id(user_id);
|
||||
|
||||
for (_, room) in self.status.rooms_mut() {
|
||||
room.update_name().await?;
|
||||
for _ in 0..3 {
|
||||
room.poll_old_timeline().await?;
|
||||
}
|
||||
}
|
||||
|
||||
info!("Initializing client for the current account");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn switch_account(&mut self, account_id: u32) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn login(
|
||||
&mut self,
|
||||
homeserver: &String,
|
||||
username: &String,
|
||||
password: &String,
|
||||
) -> Result<()> {
|
||||
self.accounts_manager
|
||||
.add(homeserver, username, password)
|
||||
.await?;
|
||||
self.init_account().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn account(&self) -> Result<&Account> {
|
||||
let account = self.accounts_manager.current();
|
||||
match account {
|
||||
None => Err(Error::msg("failed to resolve current account")),
|
||||
Some(a) => Ok(a),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn client(&self) -> Option<&Client> {
|
||||
self.accounts_manager.client()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,25 +1,13 @@
|
|||
use core::fmt;
|
||||
|
||||
use anyhow::{bail, Error, Result};
|
||||
use cli_log::warn;
|
||||
use indexmap::IndexMap;
|
||||
use anyhow::{bail, Result};
|
||||
use keymaps::key_repr::Keys;
|
||||
use matrix_sdk::{
|
||||
room::MessagesOptions,
|
||||
ruma::{
|
||||
events::{room::message::RoomMessageEventContent, AnyTimelineEvent, StateEventType},
|
||||
RoomId, TransactionId,
|
||||
},
|
||||
Client,
|
||||
};
|
||||
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug)]
|
||||
pub enum State {
|
||||
Normal,
|
||||
Insert,
|
||||
Command,
|
||||
/// Temporary workaround until command based login is working
|
||||
Setup,
|
||||
/// Only used internally to signal, that we are waiting on further keyinputs, if multiple
|
||||
/// keymappings have the same prefix
|
||||
KeyInputPending {
|
||||
|
@ -34,7 +22,6 @@ impl State {
|
|||
'n' => State::Normal,
|
||||
'i' => State::Insert,
|
||||
'c' => State::Command,
|
||||
's' => State::Setup,
|
||||
_ => bail!(
|
||||
"The letter '{}' is either not connected to a state or not yet implemented",
|
||||
c
|
||||
|
@ -43,15 +30,6 @@ impl State {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct Room {
|
||||
matrix_room: matrix_sdk::room::Joined,
|
||||
name: String,
|
||||
encrypted: bool,
|
||||
timeline: Vec<AnyTimelineEvent>,
|
||||
timeline_end: Option<String>,
|
||||
view_scroll: Option<usize>,
|
||||
}
|
||||
|
||||
pub struct StatusMessage {
|
||||
content: String,
|
||||
is_error: bool,
|
||||
|
@ -67,12 +45,7 @@ impl StatusMessage {
|
|||
|
||||
pub struct Status {
|
||||
state: State,
|
||||
account_name: String,
|
||||
account_user_id: String,
|
||||
|
||||
client: Option<Client>,
|
||||
rooms: IndexMap<String, Room>,
|
||||
current_room_id: String,
|
||||
status_messages: Vec<StatusMessage>,
|
||||
}
|
||||
|
||||
|
@ -82,7 +55,6 @@ impl fmt::Display for State {
|
|||
Self::Normal => write!(f, "Normal"),
|
||||
Self::Insert => write!(f, "Insert"),
|
||||
Self::Command => write!(f, "Command"),
|
||||
Self::Setup => write!(f, "Setup (!! workaround !!)"),
|
||||
Self::KeyInputPending {
|
||||
old_state: _,
|
||||
pending_keys: keys,
|
||||
|
@ -91,101 +63,10 @@ impl fmt::Display for State {
|
|||
}
|
||||
}
|
||||
|
||||
impl Room {
|
||||
pub fn new(matrix_room: matrix_sdk::room::Joined) -> Self {
|
||||
Self {
|
||||
matrix_room,
|
||||
name: "".to_string(),
|
||||
encrypted: false,
|
||||
timeline: Vec::new(),
|
||||
timeline_end: None,
|
||||
view_scroll: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn poll_old_timeline(&mut self) -> Result<()> {
|
||||
if let Some(AnyTimelineEvent::State(event)) = &self.timeline.get(0) {
|
||||
if event.event_type() == StateEventType::RoomCreate {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let mut messages_options = MessagesOptions::backward();
|
||||
messages_options = match &self.timeline_end {
|
||||
Some(end) => messages_options.from(end.as_str()),
|
||||
None => messages_options,
|
||||
};
|
||||
let events = self.matrix_room.messages(messages_options).await?;
|
||||
self.timeline_end = events.end;
|
||||
|
||||
for event in events.chunk.iter() {
|
||||
self.timeline.insert(
|
||||
0,
|
||||
match event.event.deserialize() {
|
||||
Ok(ev) => ev,
|
||||
Err(err) => {
|
||||
warn!("Failed to deserialize timeline event - {err}");
|
||||
continue;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &String {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub async fn update_name(&mut self) -> Result<()> {
|
||||
self.name = self.matrix_room.display_name().await?.to_string();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn timeline_add(&mut self, event: AnyTimelineEvent) {
|
||||
self.timeline.push(event);
|
||||
}
|
||||
|
||||
pub fn timeline(&self) -> &Vec<AnyTimelineEvent> {
|
||||
&self.timeline
|
||||
}
|
||||
|
||||
pub async fn send(&mut self, message: String) -> Result<()> {
|
||||
let content = RoomMessageEventContent::text_plain(message);
|
||||
let id = TransactionId::new();
|
||||
self.matrix_room.send(content, Some(&id)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn view_scroll(&self) -> Option<usize> {
|
||||
self.view_scroll
|
||||
}
|
||||
|
||||
pub fn set_view_scroll(&mut self, scroll: Option<usize>) {
|
||||
self.view_scroll = scroll;
|
||||
}
|
||||
|
||||
pub fn encrypted(&self) -> bool {
|
||||
self.matrix_room.is_encrypted()
|
||||
}
|
||||
}
|
||||
|
||||
impl Status {
|
||||
pub fn new(client: Option<Client>) -> Self {
|
||||
let mut rooms = IndexMap::new();
|
||||
if let Some(c) = &client {
|
||||
for r in c.joined_rooms() {
|
||||
rooms.insert(r.room_id().to_string(), Room::new(r.clone()));
|
||||
}
|
||||
};
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
state: State::Normal,
|
||||
account_name: "".to_owned(),
|
||||
account_user_id: "".to_owned(),
|
||||
client,
|
||||
rooms,
|
||||
current_room_id: "".to_owned(),
|
||||
status_messages: vec![StatusMessage {
|
||||
content: "Initialized!".to_owned(),
|
||||
is_error: false,
|
||||
|
@ -215,70 +96,6 @@ impl Status {
|
|||
&self.status_messages
|
||||
}
|
||||
|
||||
pub fn account_name(&self) -> &String {
|
||||
&self.account_name
|
||||
}
|
||||
|
||||
pub fn set_account_name(&mut self, name: String) {
|
||||
self.account_name = name;
|
||||
}
|
||||
|
||||
pub fn account_user_id(&self) -> &String {
|
||||
&self.account_user_id
|
||||
}
|
||||
|
||||
pub fn set_account_user_id(&mut self, user_id: String) {
|
||||
self.account_user_id = user_id;
|
||||
}
|
||||
|
||||
pub fn room(&self) -> Option<&Room> {
|
||||
self.rooms.get(self.current_room_id.as_str())
|
||||
}
|
||||
|
||||
pub fn room_mut(&mut self) -> Option<&mut Room> {
|
||||
self.rooms.get_mut(self.current_room_id.as_str())
|
||||
}
|
||||
|
||||
pub fn rooms(&self) -> &IndexMap<String, Room> {
|
||||
&self.rooms
|
||||
}
|
||||
|
||||
pub fn rooms_mut(&mut self) -> &mut IndexMap<String, Room> {
|
||||
&mut self.rooms
|
||||
}
|
||||
|
||||
pub fn set_room(&mut self, room_id: &RoomId) -> Result<()> {
|
||||
if self.rooms.contains_key(room_id.as_str()) {
|
||||
self.current_room_id = room_id.to_string();
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::msg(format!(
|
||||
"failed to set room -> invalid room id {}",
|
||||
room_id.to_string()
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_room_by_index(&mut self, room_index: usize) -> Result<()> {
|
||||
if let Some((room_id, _)) = self.rooms.get_index(room_index) {
|
||||
self.current_room_id = room_id.clone();
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::msg(format!(
|
||||
"failed to set room -> invalid room index {}",
|
||||
room_index
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_room(&self, room_id: &RoomId) -> Option<&Room> {
|
||||
self.rooms.get(room_id.as_str())
|
||||
}
|
||||
|
||||
pub fn get_room_mut(&mut self, room_id: &RoomId) -> Option<&mut Room> {
|
||||
self.rooms.get_mut(room_id.as_str())
|
||||
}
|
||||
|
||||
pub fn state(&self) -> &State {
|
||||
&self.state
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
mod accounts;
|
||||
mod app;
|
||||
mod cli;
|
||||
mod ui;
|
||||
|
@ -17,7 +16,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
Command::Start {} => {
|
||||
let mut app = app::App::new()?;
|
||||
|
||||
// NOTE(@soispha): The `None` is temporary <2024-05-03>
|
||||
// NOTE(@soispha): The 'None' here is temporary <2024-05-03>
|
||||
app.run(None, args.plugin_path).await?;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -2,7 +2,7 @@ pub mod update;
|
|||
|
||||
use std::io::Stdout;
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use anyhow::{Context, Result};
|
||||
use cli_log::{info, warn};
|
||||
use crossterm::{
|
||||
event::DisableMouseCapture,
|
||||
|
@ -16,9 +16,7 @@ use tui::{
|
|||
Terminal,
|
||||
};
|
||||
use tui_textarea::TextArea;
|
||||
pub use update::*;
|
||||
|
||||
use super::setup;
|
||||
use crate::ui::{terminal_prepare, textarea_activate, textarea_inactivate};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
|
@ -148,8 +146,6 @@ pub struct UI<'a> {
|
|||
pub rooms_state: ListState,
|
||||
pub message_compose: TextArea<'a>,
|
||||
pub cli: Option<TextArea<'a>>,
|
||||
|
||||
pub setup_ui: Option<setup::UI<'a>>,
|
||||
}
|
||||
|
||||
impl Drop for UI<'_> {
|
||||
|
@ -190,7 +186,6 @@ impl UI<'_> {
|
|||
rooms_state: ListState::default(),
|
||||
message_compose,
|
||||
cli: None,
|
||||
setup_ui: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -267,15 +262,4 @@ impl UI<'_> {
|
|||
}
|
||||
self.cli = None;
|
||||
}
|
||||
|
||||
pub async fn update_setup(&mut self) -> Result<()> {
|
||||
let setup_ui = match &mut self.setup_ui {
|
||||
Some(c) => c,
|
||||
None => bail!("SetupUI instance not found"),
|
||||
};
|
||||
|
||||
setup_ui.update(&mut self.terminal).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
use std::cmp;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use anyhow::Result;
|
||||
use tui::{
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Style},
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
};
|
||||
|
||||
use self::widgets::{command_monitor, messages, room_info, rooms, status};
|
||||
use self::widgets::command_monitor;
|
||||
use super::UI;
|
||||
use crate::app::status::Status;
|
||||
|
||||
|
@ -43,7 +43,7 @@ impl UI<'_> {
|
|||
)
|
||||
.split(chunks[0]);
|
||||
|
||||
let left_chunks = Layout::default()
|
||||
let _left_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Length(5), Constraint::Min(4)].as_ref())
|
||||
.split(main_chunks[0]);
|
||||
|
@ -76,25 +76,16 @@ impl UI<'_> {
|
|||
.style(Style::default().fg(Color::DarkGray)),
|
||||
)
|
||||
.style(Style::default().fg(Color::LightYellow));
|
||||
let status_panel = status::init(status, &colors);
|
||||
let rooms_panel = rooms::init(status, &colors);
|
||||
let (messages_panel, mut messages_state) = messages::init(status.room(), &colors)
|
||||
.context("Failed to initiate the messages widget")?;
|
||||
let room_info_panel = room_info::init(status.room(), &colors);
|
||||
let command_monitor = command_monitor::init(status.status_messages(), &colors);
|
||||
|
||||
// render the widgets
|
||||
self.terminal.draw(|frame| {
|
||||
frame.render_widget(status_panel, left_chunks[0]);
|
||||
frame.render_stateful_widget(rooms_panel, left_chunks[1], &mut self.rooms_state);
|
||||
frame.render_stateful_widget(messages_panel, middle_chunks[0], &mut messages_state);
|
||||
frame.render_widget(self.message_compose.widget(), middle_chunks[1]);
|
||||
frame.render_widget(mode_indicator, bottom_chunks[0]);
|
||||
match &self.cli {
|
||||
Some(cli) => frame.render_widget(cli.widget(), bottom_chunks[1]),
|
||||
None => (),
|
||||
};
|
||||
frame.render_widget(room_info_panel, right_chunks[0]);
|
||||
frame.render_widget(command_monitor, right_chunks[1]);
|
||||
})?;
|
||||
|
||||
|
|
|
@ -1,109 +0,0 @@
|
|||
use anyhow::{Context, Result};
|
||||
use matrix_sdk::ruma::events::{AnyMessageLikeEvent, AnyTimelineEvent};
|
||||
use tui::{
|
||||
layout::Corner,
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans, Text},
|
||||
widgets::{Block, Borders, List, ListItem, ListState},
|
||||
};
|
||||
|
||||
use crate::{app::status::Room, ui::central::InputPosition};
|
||||
|
||||
pub fn init<'a>(room: Option<&'a Room>, colors: &Vec<Color>) -> Result<(List<'a>, ListState)> {
|
||||
let content = match room {
|
||||
Some(room) => get_content_from_room(room).context("Failed to get content from room")?,
|
||||
None => vec![ListItem::new(Text::styled(
|
||||
"No room selected!",
|
||||
Style::default().fg(Color::Red),
|
||||
))],
|
||||
};
|
||||
|
||||
let mut messages_state = ListState::default();
|
||||
|
||||
if let Some(room) = room {
|
||||
messages_state.select(room.view_scroll());
|
||||
}
|
||||
|
||||
Ok((
|
||||
List::new(content)
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Messages")
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().fg(colors[InputPosition::Messages as usize])),
|
||||
)
|
||||
.start_corner(Corner::BottomLeft)
|
||||
.highlight_symbol(">")
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.fg(Color::LightMagenta)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
messages_state,
|
||||
))
|
||||
}
|
||||
|
||||
fn get_content_from_room(room: &Room) -> Result<Vec<ListItem>> {
|
||||
let results: Vec<Result<ListItem>> = room
|
||||
.timeline()
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|event| filter_event(event).context("Failed to filter event"))
|
||||
.collect();
|
||||
|
||||
let mut output = Vec::with_capacity(results.len());
|
||||
for result in results {
|
||||
output.push(result?);
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn filter_event<'a>(event: &AnyTimelineEvent) -> Result<ListItem<'a>> {
|
||||
match event {
|
||||
// Message Like Events
|
||||
AnyTimelineEvent::MessageLike(message_like_event) => {
|
||||
let (content, color) = match &message_like_event {
|
||||
AnyMessageLikeEvent::RoomMessage(room_message_event) => {
|
||||
let message_content = &room_message_event
|
||||
.as_original()
|
||||
.context("Failed to get inner original message_event")?
|
||||
.content
|
||||
.body();
|
||||
|
||||
(message_content.to_string(), Color::White)
|
||||
}
|
||||
_ => (
|
||||
format!(
|
||||
"~~~ not supported message like event: {} ~~~",
|
||||
message_like_event.event_type().to_string()
|
||||
),
|
||||
Color::Red,
|
||||
),
|
||||
};
|
||||
let mut text = Text::styled(
|
||||
message_like_event.sender().to_string(),
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
text.extend(Text::styled(
|
||||
content.to_string(),
|
||||
Style::default().fg(color),
|
||||
));
|
||||
Ok(ListItem::new(text))
|
||||
}
|
||||
|
||||
// State Events
|
||||
AnyTimelineEvent::State(state) => Ok(ListItem::new(vec![Spans::from(vec![
|
||||
Span::styled(
|
||||
state.sender().to_string(),
|
||||
Style::default().fg(Color::DarkGray),
|
||||
),
|
||||
Span::styled(": ", Style::default().fg(Color::DarkGray)),
|
||||
Span::styled(
|
||||
state.event_type().to_string(),
|
||||
Style::default().fg(Color::DarkGray),
|
||||
),
|
||||
])])),
|
||||
}
|
||||
}
|
|
@ -1,5 +1 @@
|
|||
pub mod command_monitor;
|
||||
pub mod messages;
|
||||
pub mod room_info;
|
||||
pub mod rooms;
|
||||
pub mod status;
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
use tui::{
|
||||
layout::Alignment,
|
||||
style::{Color, Style},
|
||||
text::Text,
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
};
|
||||
|
||||
use crate::{app::status::Room, ui::central::InputPosition};
|
||||
|
||||
pub fn init<'a>(room: Option<&'a Room>, colors: &Vec<Color>) -> Paragraph<'a> {
|
||||
let mut room_info_content = Text::default();
|
||||
if let Some(room) = room {
|
||||
room_info_content.extend(Text::styled(room.name(), Style::default().fg(Color::Cyan)));
|
||||
if room.encrypted() {
|
||||
room_info_content.extend(Text::styled("Encrypted", Style::default().fg(Color::Green)));
|
||||
} else {
|
||||
room_info_content.extend(Text::styled(
|
||||
"Not Encrypted!",
|
||||
Style::default().fg(Color::Red),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
room_info_content.extend(Text::styled(
|
||||
"No room selected!",
|
||||
Style::default().fg(Color::Red),
|
||||
));
|
||||
}
|
||||
|
||||
Paragraph::new(room_info_content)
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Room Info")
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().fg(colors[InputPosition::RoomInfo as usize])),
|
||||
)
|
||||
.alignment(Alignment::Center)
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
use tui::{
|
||||
style::{Color, Modifier, Style},
|
||||
text::Span,
|
||||
widgets::{Block, Borders, List, ListItem},
|
||||
};
|
||||
|
||||
use crate::{app::status::Status, ui::central::InputPosition};
|
||||
|
||||
pub fn init<'a>(status: &'a Status, colors: &Vec<Color>) -> List<'a> {
|
||||
let rooms_content: Vec<_> = status
|
||||
.rooms()
|
||||
.iter()
|
||||
.map(|(_, room)| ListItem::new(Span::styled(room.name(), Style::default())))
|
||||
.collect();
|
||||
List::new(rooms_content)
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Rooms (navigate: arrow keys)")
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().fg(colors[InputPosition::Rooms as usize])),
|
||||
)
|
||||
.style(Style::default().fg(Color::DarkGray))
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)
|
||||
.highlight_symbol(">")
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
use tui::{
|
||||
layout::Alignment,
|
||||
style::{Color, Modifier, Style},
|
||||
text::Text,
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
};
|
||||
|
||||
use crate::{app::status::Status, ui::central::InputPosition};
|
||||
|
||||
pub fn init<'a>(status: &'a Status, colors: &Vec<Color>) -> Paragraph<'a> {
|
||||
let mut status_content = Text::styled(
|
||||
status.account_name(),
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
);
|
||||
status_content.extend(Text::styled(status.account_user_id(), Style::default()));
|
||||
status_content.extend(Text::styled(
|
||||
"settings",
|
||||
Style::default()
|
||||
.fg(Color::LightMagenta)
|
||||
.add_modifier(Modifier::ITALIC | Modifier::UNDERLINED),
|
||||
));
|
||||
Paragraph::new(status_content)
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Status")
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().fg(colors[InputPosition::Status as usize])),
|
||||
)
|
||||
.alignment(Alignment::Left)
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
pub mod central;
|
||||
pub mod setup;
|
||||
|
||||
use std::{io, io::Stdout};
|
||||
|
||||
|
|
172
src/ui/setup.rs
172
src/ui/setup.rs
|
@ -1,172 +0,0 @@
|
|||
use std::io::Stdout;
|
||||
|
||||
use anyhow::Result;
|
||||
use tui::{
|
||||
backend::CrosstermBackend,
|
||||
layout::{Alignment, Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::Span,
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
Terminal,
|
||||
};
|
||||
use tui_textarea::TextArea;
|
||||
|
||||
use crate::ui::{textarea_activate, textarea_inactivate};
|
||||
|
||||
pub struct UI<'a> {
|
||||
input_position: InputPosition,
|
||||
|
||||
pub homeserver: TextArea<'a>,
|
||||
pub username: TextArea<'a>,
|
||||
pub password: TextArea<'a>,
|
||||
pub password_data: TextArea<'a>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum InputPosition {
|
||||
Homeserver,
|
||||
Username,
|
||||
Password,
|
||||
Ok,
|
||||
}
|
||||
|
||||
impl UI<'_> {
|
||||
pub fn new() -> Self {
|
||||
let mut homeserver = TextArea::new(vec!["https://matrix.org".to_string()]);
|
||||
let mut username = TextArea::default();
|
||||
let mut password = TextArea::default();
|
||||
let password_data = TextArea::default();
|
||||
|
||||
homeserver.set_block(Block::default().title("Homeserver").borders(Borders::ALL));
|
||||
username.set_block(Block::default().title("Username").borders(Borders::ALL));
|
||||
password.set_block(Block::default().title("Password").borders(Borders::ALL));
|
||||
|
||||
textarea_activate(&mut homeserver);
|
||||
textarea_inactivate(&mut username);
|
||||
textarea_inactivate(&mut password);
|
||||
|
||||
Self {
|
||||
input_position: InputPosition::Homeserver,
|
||||
homeserver,
|
||||
username,
|
||||
password,
|
||||
password_data,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cycle_input_position(&mut self) {
|
||||
self.input_position = match self.input_position {
|
||||
InputPosition::Homeserver => {
|
||||
textarea_inactivate(&mut self.homeserver);
|
||||
textarea_activate(&mut self.username);
|
||||
textarea_inactivate(&mut self.password);
|
||||
InputPosition::Username
|
||||
}
|
||||
InputPosition::Username => {
|
||||
textarea_inactivate(&mut self.homeserver);
|
||||
textarea_inactivate(&mut self.username);
|
||||
textarea_activate(&mut self.password);
|
||||
InputPosition::Password
|
||||
}
|
||||
InputPosition::Password => {
|
||||
textarea_inactivate(&mut self.homeserver);
|
||||
textarea_inactivate(&mut self.username);
|
||||
textarea_inactivate(&mut self.password);
|
||||
InputPosition::Ok
|
||||
}
|
||||
InputPosition::Ok => {
|
||||
textarea_activate(&mut self.homeserver);
|
||||
textarea_inactivate(&mut self.username);
|
||||
textarea_inactivate(&mut self.password);
|
||||
InputPosition::Homeserver
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn cycle_input_position_rev(&mut self) {
|
||||
self.input_position = match self.input_position {
|
||||
InputPosition::Homeserver => {
|
||||
textarea_inactivate(&mut self.homeserver);
|
||||
textarea_inactivate(&mut self.username);
|
||||
textarea_inactivate(&mut self.password);
|
||||
InputPosition::Ok
|
||||
}
|
||||
InputPosition::Username => {
|
||||
textarea_activate(&mut self.homeserver);
|
||||
textarea_inactivate(&mut self.username);
|
||||
textarea_inactivate(&mut self.password);
|
||||
InputPosition::Homeserver
|
||||
}
|
||||
InputPosition::Password => {
|
||||
textarea_inactivate(&mut self.homeserver);
|
||||
textarea_activate(&mut self.username);
|
||||
textarea_inactivate(&mut self.password);
|
||||
InputPosition::Username
|
||||
}
|
||||
InputPosition::Ok => {
|
||||
textarea_inactivate(&mut self.homeserver);
|
||||
textarea_inactivate(&mut self.username);
|
||||
textarea_activate(&mut self.password);
|
||||
InputPosition::Password
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn input_position(&self) -> &InputPosition {
|
||||
&self.input_position
|
||||
}
|
||||
|
||||
pub async fn update(
|
||||
&mut self,
|
||||
terminal: &mut Terminal<CrosstermBackend<Stdout>>,
|
||||
) -> Result<()> {
|
||||
let strings: Vec<String> = vec!["".to_owned(); 3];
|
||||
|
||||
let content_ok = match self.input_position {
|
||||
InputPosition::Ok => {
|
||||
Span::styled("OK", Style::default().add_modifier(Modifier::UNDERLINED))
|
||||
}
|
||||
_ => Span::styled("OK", Style::default().fg(Color::DarkGray)),
|
||||
};
|
||||
|
||||
let block = Block::default().title("Login").borders(Borders::ALL);
|
||||
|
||||
let ok = Paragraph::new(content_ok).alignment(Alignment::Center);
|
||||
|
||||
// define a 32 * 6 chunk in the middle of the screen
|
||||
let mut chunk = terminal.size()?;
|
||||
chunk.x = (chunk.width / 2) - 16;
|
||||
chunk.y = (chunk.height / 2) - 5;
|
||||
chunk.height = 12;
|
||||
chunk.width = 32;
|
||||
|
||||
let mut split_chunk = chunk.clone();
|
||||
split_chunk.x += 1;
|
||||
split_chunk.y += 1;
|
||||
split_chunk.height -= 1;
|
||||
split_chunk.width -= 2;
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3), // 0. Homserver:
|
||||
Constraint::Length(3), // 1. Username:
|
||||
Constraint::Length(3), // 2. Password:
|
||||
Constraint::Length(1), // 3. OK
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(split_chunk);
|
||||
|
||||
terminal.draw(|frame| {
|
||||
frame.render_widget(block.clone(), chunk);
|
||||
frame.render_widget(self.homeserver.widget(), chunks[0]);
|
||||
frame.render_widget(self.username.widget(), chunks[1]);
|
||||
frame.render_widget(self.password.widget(), chunks[2]);
|
||||
frame.render_widget(ok.clone(), chunks[3]);
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
Reference in New Issue