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
|
# 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]
|
[dependencies]
|
||||||
clap = { version = "4.5.4", features = ["derive"] }
|
clap = { version = "4.5.4", features = ["derive"] }
|
||||||
cli-log = "2.0"
|
cli-log = "2.0"
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
matrix-sdk = "0.6"
|
tokio = { version = "1.37", features = ["macros", "rt-multi-thread", "fs", "time"] }
|
||||||
tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] }
|
tokio-util = {version = "0.7.10"}
|
||||||
|
|
||||||
# config
|
# config
|
||||||
trinitry = {version = "0.1.0"}
|
trinitry = {version = "0.1.0"}
|
||||||
keymaps = {version = "0.1.1", features = ["crossterm"] }
|
keymaps = {version = "0.1.1", features = ["crossterm"] }
|
||||||
|
directories = "5.0.1"
|
||||||
|
|
||||||
# c api
|
# c api
|
||||||
libloading = "0.8.3"
|
libloading = "0.8.3"
|
||||||
|
@ -31,13 +28,9 @@ mlua = { version = "0.9.7", features = ["lua54", "async", "send", "serialize"] }
|
||||||
once_cell = "1.19.0"
|
once_cell = "1.19.0"
|
||||||
|
|
||||||
# tui feature specific parts
|
# tui feature specific parts
|
||||||
tui = {version = "0.19", optional = true}
|
tui = {version = "0.19"}
|
||||||
tui-textarea = { version = "0.2", features = ["crossterm"], optional = true }
|
tui-textarea = { version = "0.2", features = ["crossterm"]}
|
||||||
crossterm = { version = "0.25", optional = true }
|
crossterm = { version = "0.25"}
|
||||||
tokio-util = { version = "0.7", optional = true }
|
|
||||||
serde = { version = "1.0", optional = true }
|
|
||||||
indexmap = { version = "2.2.6", optional = true }
|
|
||||||
directories = "5.0.1"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
pretty_assertions = "1.4.0"
|
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
|
/// Closes the application
|
||||||
fn exit();
|
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
|
//// Open the help pages at the first occurrence of
|
||||||
//// the input string if it is Some, otherwise open
|
//// the input string if it is Some, otherwise open
|
||||||
//// the help pages at the start
|
//// the help pages at the start
|
||||||
// TODO(@soispha): To be implemented <2024-03-09>
|
// TODO(@soispha): To be implemented <2024-03-09>
|
||||||
// fn help(Option<String>);
|
// 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
|
/// Function that change the UI, or UI state
|
||||||
mod ui {
|
mod ui {
|
||||||
enum Mode {
|
enum Mode {
|
||||||
|
|
|
@ -87,21 +87,6 @@ pub async fn handle(
|
||||||
warn!("Terminating the application");
|
warn!("Terminating the application");
|
||||||
EventStatus::Terminate
|
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 {
|
Api::Ui(ui) => match ui {
|
||||||
Ui::set_mode { mode } => match mode {
|
Ui::set_mode { mode } => match mode {
|
||||||
Mode::Normal => {
|
Mode::Normal => {
|
||||||
|
@ -213,7 +198,6 @@ pub async fn handle(
|
||||||
EventStatus::Ok
|
EventStatus::Ok
|
||||||
}
|
}
|
||||||
State::Normal
|
State::Normal
|
||||||
| State::Setup
|
|
||||||
| State::KeyInputPending {
|
| State::KeyInputPending {
|
||||||
old_state: _,
|
old_state: _,
|
||||||
pending_keys: _,
|
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
|
// input events
|
||||||
pub mod input;
|
pub mod input;
|
||||||
pub mod setup;
|
|
||||||
|
|
||||||
// matrix
|
|
||||||
pub mod matrix;
|
|
||||||
|
|
||||||
// ci
|
// ci
|
||||||
pub mod command;
|
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;
|
use crate::app::events::Event;
|
||||||
pub mod matrix;
|
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 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 cli_log::{trace, warn};
|
||||||
use crossterm::event::Event as CrosstermEvent;
|
use crossterm::event::Event as CrosstermEvent;
|
||||||
use handlers::{command, input, lua_command, matrix, setup};
|
use handlers::{command, input};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
InputEvent(CrosstermEvent),
|
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>>),
|
CommandEvent(Commands, Option<trixy::oneshot::Sender<String>>),
|
||||||
LuaCommand(String),
|
LuaCommand(String),
|
||||||
}
|
}
|
||||||
|
@ -21,10 +21,6 @@ impl Event {
|
||||||
pub async fn handle(self, app: &mut App<'_>) -> Result<EventStatus> {
|
pub async fn handle(self, app: &mut App<'_>) -> Result<EventStatus> {
|
||||||
trace!("Received event to handle: `{:#?}`", &self);
|
trace!("Received event to handle: `{:#?}`", &self);
|
||||||
match 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)
|
Event::CommandEvent(event, callback_tx) => command::handle(app, &event, callback_tx)
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("Failed to handle command event: `{:#?}`", event)),
|
.with_context(|| format!("Failed to handle command event: `{:#?}`", event)),
|
||||||
|
@ -43,9 +39,6 @@ impl Event {
|
||||||
// .await
|
// .await
|
||||||
// .with_context(|| format!("Failed to handle function: `{}`", function)),
|
// .with_context(|| format!("Failed to handle function: `{}`", function)),
|
||||||
Event::InputEvent(event) => match app.status.state() {
|
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(|| {
|
_ => input::handle(app, &event).await.with_context(|| {
|
||||||
format!("Failed to handle input (non-setup) event: `{:#?}`", event)
|
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 events;
|
||||||
pub mod status;
|
pub mod status;
|
||||||
|
|
||||||
use std::{
|
use std::{collections::HashMap, ffi::c_int, path::PathBuf, sync::OnceLock};
|
||||||
collections::HashMap,
|
|
||||||
ffi::c_int,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
sync::OnceLock,
|
|
||||||
};
|
|
||||||
|
|
||||||
use anyhow::{Context, Error, Result};
|
use anyhow::{Context, Result};
|
||||||
use cli_log::{info, warn};
|
use cli_log::{info, warn};
|
||||||
use crossterm::{
|
|
||||||
event::DisableMouseCapture,
|
|
||||||
execute,
|
|
||||||
terminal::{disable_raw_mode, LeaveAlternateScreen},
|
|
||||||
};
|
|
||||||
use directories::ProjectDirs;
|
use directories::ProjectDirs;
|
||||||
use keymaps::trie::Node;
|
use keymaps::trie::Node;
|
||||||
use libloading::{Library, Symbol};
|
use libloading::{Library, Symbol};
|
||||||
use matrix_sdk::Client;
|
|
||||||
use tokio::sync::mpsc::{self, Sender};
|
use tokio::sync::mpsc::{self, Sender};
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
|
@ -29,24 +18,21 @@ use tokio_util::sync::CancellationToken;
|
||||||
// };
|
// };
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
accounts::{Account, AccountsManager},
|
|
||||||
app::{
|
app::{
|
||||||
events::{Event, EventStatus},
|
events::{Event, EventStatus},
|
||||||
status::{State, Status},
|
status::{State, Status},
|
||||||
},
|
},
|
||||||
ui::{central, setup},
|
ui::central,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct App<'runtime> {
|
pub struct App<'runtime> {
|
||||||
ui: central::UI<'runtime>,
|
ui: central::UI<'runtime>,
|
||||||
accounts_manager: AccountsManager,
|
|
||||||
status: Status,
|
status: Status,
|
||||||
|
|
||||||
tx: mpsc::Sender<Event>,
|
tx: mpsc::Sender<Event>,
|
||||||
rx: mpsc::Receiver<Event>,
|
rx: mpsc::Receiver<Event>,
|
||||||
|
|
||||||
input_listener_killer: CancellationToken,
|
input_listener_killer: CancellationToken,
|
||||||
matrix_listener_killer: CancellationToken,
|
|
||||||
|
|
||||||
// lua: LuaCommandManager,
|
// lua: LuaCommandManager,
|
||||||
project_dirs: ProjectDirs,
|
project_dirs: ProjectDirs,
|
||||||
|
@ -58,14 +44,6 @@ pub static COMMAND_TRANSMITTER: OnceLock<Sender<Event>> = OnceLock::new();
|
||||||
|
|
||||||
impl App<'_> {
|
impl App<'_> {
|
||||||
pub fn new() -> Result<Self> {
|
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);
|
let (tx, rx) = mpsc::channel(256);
|
||||||
|
|
||||||
COMMAND_TRANSMITTER
|
COMMAND_TRANSMITTER
|
||||||
|
@ -74,13 +52,11 @@ impl App<'_> {
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
ui: central::UI::new()?,
|
ui: central::UI::new()?,
|
||||||
accounts_manager: AccountsManager::new(config)?,
|
status: Status::new(),
|
||||||
status: Status::new(None),
|
|
||||||
|
|
||||||
tx: tx.clone(),
|
tx: tx.clone(),
|
||||||
rx,
|
rx,
|
||||||
input_listener_killer: CancellationToken::new(),
|
input_listener_killer: CancellationToken::new(),
|
||||||
matrix_listener_killer: CancellationToken::new(),
|
|
||||||
|
|
||||||
// lua: LuaCommandManager::new(tx),
|
// lua: LuaCommandManager::new(tx),
|
||||||
|
|
||||||
|
@ -99,7 +75,7 @@ impl App<'_> {
|
||||||
plugin_path: Option<PathBuf>,
|
plugin_path: Option<PathBuf>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// Spawn input event listener
|
// Spawn input event listener
|
||||||
tokio::task::spawn(events::listeners::input::poll(
|
tokio::task::spawn(events::listeners::poll(
|
||||||
self.tx.clone(),
|
self.tx.clone(),
|
||||||
self.input_listener_killer.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);
|
self.status.set_state(State::Normal);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
@ -171,89 +139,4 @@ impl App<'_> {
|
||||||
self.input_listener_killer.cancel();
|
self.input_listener_killer.cancel();
|
||||||
Ok(())
|
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 core::fmt;
|
||||||
|
|
||||||
use anyhow::{bail, Error, Result};
|
use anyhow::{bail, Result};
|
||||||
use cli_log::warn;
|
|
||||||
use indexmap::IndexMap;
|
|
||||||
use keymaps::key_repr::Keys;
|
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)]
|
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug)]
|
||||||
pub enum State {
|
pub enum State {
|
||||||
Normal,
|
Normal,
|
||||||
Insert,
|
Insert,
|
||||||
Command,
|
Command,
|
||||||
/// Temporary workaround until command based login is working
|
|
||||||
Setup,
|
|
||||||
/// Only used internally to signal, that we are waiting on further keyinputs, if multiple
|
/// Only used internally to signal, that we are waiting on further keyinputs, if multiple
|
||||||
/// keymappings have the same prefix
|
/// keymappings have the same prefix
|
||||||
KeyInputPending {
|
KeyInputPending {
|
||||||
|
@ -34,7 +22,6 @@ impl State {
|
||||||
'n' => State::Normal,
|
'n' => State::Normal,
|
||||||
'i' => State::Insert,
|
'i' => State::Insert,
|
||||||
'c' => State::Command,
|
'c' => State::Command,
|
||||||
's' => State::Setup,
|
|
||||||
_ => bail!(
|
_ => bail!(
|
||||||
"The letter '{}' is either not connected to a state or not yet implemented",
|
"The letter '{}' is either not connected to a state or not yet implemented",
|
||||||
c
|
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 {
|
pub struct StatusMessage {
|
||||||
content: String,
|
content: String,
|
||||||
is_error: bool,
|
is_error: bool,
|
||||||
|
@ -67,12 +45,7 @@ impl StatusMessage {
|
||||||
|
|
||||||
pub struct Status {
|
pub struct Status {
|
||||||
state: State,
|
state: State,
|
||||||
account_name: String,
|
|
||||||
account_user_id: String,
|
|
||||||
|
|
||||||
client: Option<Client>,
|
|
||||||
rooms: IndexMap<String, Room>,
|
|
||||||
current_room_id: String,
|
|
||||||
status_messages: Vec<StatusMessage>,
|
status_messages: Vec<StatusMessage>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,7 +55,6 @@ impl fmt::Display for State {
|
||||||
Self::Normal => write!(f, "Normal"),
|
Self::Normal => write!(f, "Normal"),
|
||||||
Self::Insert => write!(f, "Insert"),
|
Self::Insert => write!(f, "Insert"),
|
||||||
Self::Command => write!(f, "Command"),
|
Self::Command => write!(f, "Command"),
|
||||||
Self::Setup => write!(f, "Setup (!! workaround !!)"),
|
|
||||||
Self::KeyInputPending {
|
Self::KeyInputPending {
|
||||||
old_state: _,
|
old_state: _,
|
||||||
pending_keys: keys,
|
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 {
|
impl Status {
|
||||||
pub fn new(client: Option<Client>) -> Self {
|
pub fn new() -> 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()));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
state: State::Normal,
|
state: State::Normal,
|
||||||
account_name: "".to_owned(),
|
|
||||||
account_user_id: "".to_owned(),
|
|
||||||
client,
|
|
||||||
rooms,
|
|
||||||
current_room_id: "".to_owned(),
|
|
||||||
status_messages: vec![StatusMessage {
|
status_messages: vec![StatusMessage {
|
||||||
content: "Initialized!".to_owned(),
|
content: "Initialized!".to_owned(),
|
||||||
is_error: false,
|
is_error: false,
|
||||||
|
@ -215,70 +96,6 @@ impl Status {
|
||||||
&self.status_messages
|
&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 {
|
pub fn state(&self) -> &State {
|
||||||
&self.state
|
&self.state
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
mod accounts;
|
|
||||||
mod app;
|
mod app;
|
||||||
mod cli;
|
mod cli;
|
||||||
mod ui;
|
mod ui;
|
||||||
|
@ -17,7 +16,7 @@ async fn main() -> anyhow::Result<()> {
|
||||||
Command::Start {} => {
|
Command::Start {} => {
|
||||||
let mut app = app::App::new()?;
|
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?;
|
app.run(None, args.plugin_path).await?;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,7 +2,7 @@ pub mod update;
|
||||||
|
|
||||||
use std::io::Stdout;
|
use std::io::Stdout;
|
||||||
|
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use cli_log::{info, warn};
|
use cli_log::{info, warn};
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
event::DisableMouseCapture,
|
event::DisableMouseCapture,
|
||||||
|
@ -16,9 +16,7 @@ use tui::{
|
||||||
Terminal,
|
Terminal,
|
||||||
};
|
};
|
||||||
use tui_textarea::TextArea;
|
use tui_textarea::TextArea;
|
||||||
pub use update::*;
|
|
||||||
|
|
||||||
use super::setup;
|
|
||||||
use crate::ui::{terminal_prepare, textarea_activate, textarea_inactivate};
|
use crate::ui::{terminal_prepare, textarea_activate, textarea_inactivate};
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq)]
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
|
@ -148,8 +146,6 @@ pub struct UI<'a> {
|
||||||
pub rooms_state: ListState,
|
pub rooms_state: ListState,
|
||||||
pub message_compose: TextArea<'a>,
|
pub message_compose: TextArea<'a>,
|
||||||
pub cli: Option<TextArea<'a>>,
|
pub cli: Option<TextArea<'a>>,
|
||||||
|
|
||||||
pub setup_ui: Option<setup::UI<'a>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for UI<'_> {
|
impl Drop for UI<'_> {
|
||||||
|
@ -190,7 +186,6 @@ impl UI<'_> {
|
||||||
rooms_state: ListState::default(),
|
rooms_state: ListState::default(),
|
||||||
message_compose,
|
message_compose,
|
||||||
cli: None,
|
cli: None,
|
||||||
setup_ui: None,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -267,15 +262,4 @@ impl UI<'_> {
|
||||||
}
|
}
|
||||||
self.cli = None;
|
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 std::cmp;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::Result;
|
||||||
use tui::{
|
use tui::{
|
||||||
layout::{Constraint, Direction, Layout},
|
layout::{Constraint, Direction, Layout},
|
||||||
style::{Color, Style},
|
style::{Color, Style},
|
||||||
widgets::{Block, Borders, Paragraph},
|
widgets::{Block, Borders, Paragraph},
|
||||||
};
|
};
|
||||||
|
|
||||||
use self::widgets::{command_monitor, messages, room_info, rooms, status};
|
use self::widgets::command_monitor;
|
||||||
use super::UI;
|
use super::UI;
|
||||||
use crate::app::status::Status;
|
use crate::app::status::Status;
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ impl UI<'_> {
|
||||||
)
|
)
|
||||||
.split(chunks[0]);
|
.split(chunks[0]);
|
||||||
|
|
||||||
let left_chunks = Layout::default()
|
let _left_chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([Constraint::Length(5), Constraint::Min(4)].as_ref())
|
.constraints([Constraint::Length(5), Constraint::Min(4)].as_ref())
|
||||||
.split(main_chunks[0]);
|
.split(main_chunks[0]);
|
||||||
|
@ -76,25 +76,16 @@ impl UI<'_> {
|
||||||
.style(Style::default().fg(Color::DarkGray)),
|
.style(Style::default().fg(Color::DarkGray)),
|
||||||
)
|
)
|
||||||
.style(Style::default().fg(Color::LightYellow));
|
.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);
|
let command_monitor = command_monitor::init(status.status_messages(), &colors);
|
||||||
|
|
||||||
// render the widgets
|
// render the widgets
|
||||||
self.terminal.draw(|frame| {
|
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(self.message_compose.widget(), middle_chunks[1]);
|
||||||
frame.render_widget(mode_indicator, bottom_chunks[0]);
|
frame.render_widget(mode_indicator, bottom_chunks[0]);
|
||||||
match &self.cli {
|
match &self.cli {
|
||||||
Some(cli) => frame.render_widget(cli.widget(), bottom_chunks[1]),
|
Some(cli) => frame.render_widget(cli.widget(), bottom_chunks[1]),
|
||||||
None => (),
|
None => (),
|
||||||
};
|
};
|
||||||
frame.render_widget(room_info_panel, right_chunks[0]);
|
|
||||||
frame.render_widget(command_monitor, right_chunks[1]);
|
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 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 central;
|
||||||
pub mod setup;
|
|
||||||
|
|
||||||
use std::{io, io::Stdout};
|
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