Refactor(ui): Split into multiple files
This commit is contained in:
parent
dfeac4662d
commit
8f9a2a3f22
|
@ -3,7 +3,7 @@ use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEvent, KeyModifiers}
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{command, command::Command, events::event_types::EventStatus, App},
|
app::{command, command::Command, events::event_types::EventStatus, App},
|
||||||
ui,
|
ui::central,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<EventStatus> {
|
pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<EventStatus> {
|
||||||
|
@ -32,7 +32,7 @@ pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<E
|
||||||
command::execute(app.channel_tx(), Command::CommandLineShow).await?;
|
command::execute(app.channel_tx(), Command::CommandLineShow).await?;
|
||||||
}
|
}
|
||||||
input => match app.ui.input_position() {
|
input => match app.ui.input_position() {
|
||||||
ui::MainInputPosition::MessageCompose => {
|
central::InputPosition::MessageCompose => {
|
||||||
match input {
|
match input {
|
||||||
CrosstermEvent::Key(KeyEvent {
|
CrosstermEvent::Key(KeyEvent {
|
||||||
code: KeyCode::Enter,
|
code: KeyCode::Enter,
|
||||||
|
@ -53,7 +53,7 @@ pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<E
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
ui::MainInputPosition::Rooms => {
|
central::InputPosition::Rooms => {
|
||||||
match input {
|
match input {
|
||||||
CrosstermEvent::Key(KeyEvent {
|
CrosstermEvent::Key(KeyEvent {
|
||||||
code: KeyCode::Up, ..
|
code: KeyCode::Up, ..
|
||||||
|
@ -91,7 +91,7 @@ pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<E
|
||||||
_ => (),
|
_ => (),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
ui::MainInputPosition::Messages => {
|
central::InputPosition::Messages => {
|
||||||
match input {
|
match input {
|
||||||
CrosstermEvent::Key(KeyEvent {
|
CrosstermEvent::Key(KeyEvent {
|
||||||
code: KeyCode::Up, ..
|
code: KeyCode::Up, ..
|
||||||
|
@ -136,7 +136,7 @@ pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<E
|
||||||
_ => (),
|
_ => (),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
ui::MainInputPosition::CLI => {
|
central::InputPosition::CLI => {
|
||||||
if let Some(_) = app.ui.cli {
|
if let Some(_) = app.ui.cli {
|
||||||
match input {
|
match input {
|
||||||
CrosstermEvent::Key(KeyEvent {
|
CrosstermEvent::Key(KeyEvent {
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEvent};
|
use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEvent};
|
||||||
|
|
||||||
use crate::{
|
use crate::{app::{events::event_types::EventStatus, App}, ui::setup};
|
||||||
app::{events::event_types::EventStatus, App},
|
|
||||||
ui,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<EventStatus> {
|
pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<EventStatus> {
|
||||||
let ui = match &mut app.ui.setup_ui {
|
let ui = match &mut app.ui.setup_ui {
|
||||||
|
@ -32,7 +29,7 @@ pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<E
|
||||||
..
|
..
|
||||||
}) => {
|
}) => {
|
||||||
match ui.input_position() {
|
match ui.input_position() {
|
||||||
ui::SetupInputPosition::Ok => {
|
setup::InputPosition::Ok => {
|
||||||
let homeserver = ui.homeserver.lines()[0].clone();
|
let homeserver = ui.homeserver.lines()[0].clone();
|
||||||
let username = ui.username.lines()[0].clone();
|
let username = ui.username.lines()[0].clone();
|
||||||
let password = ui.password_data.lines()[0].clone();
|
let password = ui.password_data.lines()[0].clone();
|
||||||
|
@ -46,13 +43,13 @@ pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<E
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
input => match ui.input_position() {
|
input => match ui.input_position() {
|
||||||
ui::SetupInputPosition::Homeserver => {
|
setup::InputPosition::Homeserver => {
|
||||||
ui.homeserver.input(input.to_owned());
|
ui.homeserver.input(input.to_owned());
|
||||||
}
|
}
|
||||||
ui::SetupInputPosition::Username => {
|
setup::InputPosition::Username => {
|
||||||
ui.username.input(input.to_owned());
|
ui.username.input(input.to_owned());
|
||||||
}
|
}
|
||||||
ui::SetupInputPosition::Password => {
|
setup::InputPosition::Password => {
|
||||||
let textarea_input = tui_textarea::Input::from(input.to_owned());
|
let textarea_input = tui_textarea::Input::from(input.to_owned());
|
||||||
ui.password_data.input(textarea_input.clone());
|
ui.password_data.input(textarea_input.clone());
|
||||||
match textarea_input.key {
|
match textarea_input.key {
|
||||||
|
|
|
@ -14,12 +14,12 @@ use status::{State, Status};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
use crate::{accounts, app::command_interface::generate_ci_functions, ui};
|
use crate::{accounts, app::command_interface::generate_ci_functions, ui::{central, setup}};
|
||||||
|
|
||||||
use self::events::event_types::{self, Event};
|
use self::events::event_types::{self, Event};
|
||||||
|
|
||||||
pub struct App<'ui> {
|
pub struct App<'ui> {
|
||||||
ui: ui::UI<'ui>,
|
ui: central::UI<'ui>,
|
||||||
accounts_manager: accounts::AccountsManager,
|
accounts_manager: accounts::AccountsManager,
|
||||||
status: Status,
|
status: Status,
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ impl App<'_> {
|
||||||
let (channel_tx, channel_rx) = mpsc::channel(256);
|
let (channel_tx, channel_rx) = mpsc::channel(256);
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
ui: ui::UI::new()?,
|
ui: central::UI::new()?,
|
||||||
accounts_manager: AccountsManager::new(config)?,
|
accounts_manager: AccountsManager::new(config)?,
|
||||||
status: Status::new(None),
|
status: Status::new(None),
|
||||||
|
|
||||||
|
@ -117,7 +117,7 @@ impl App<'_> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn setup(&mut self) -> Result<()> {
|
async fn setup(&mut self) -> Result<()> {
|
||||||
self.ui.setup_ui = Some(ui::SetupUI::new());
|
self.ui.setup_ui = Some(setup::UI::new());
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
self.status.set_state(State::Setup);
|
self.status.set_state(State::Setup);
|
||||||
|
|
|
@ -0,0 +1,155 @@
|
||||||
|
pub mod update;
|
||||||
|
|
||||||
|
use std::io::Stdout;
|
||||||
|
|
||||||
|
use anyhow::{bail, Result, Context};
|
||||||
|
use cli_log::info;
|
||||||
|
use crossterm::{
|
||||||
|
event::DisableMouseCapture,
|
||||||
|
execute,
|
||||||
|
terminal::{disable_raw_mode, LeaveAlternateScreen},
|
||||||
|
};
|
||||||
|
use tui::{
|
||||||
|
backend::CrosstermBackend,
|
||||||
|
widgets::{Block, Borders, ListState},
|
||||||
|
Terminal,
|
||||||
|
};
|
||||||
|
use tui_textarea::TextArea;
|
||||||
|
|
||||||
|
use crate::ui::terminal_prepare;
|
||||||
|
|
||||||
|
use super::setup;
|
||||||
|
|
||||||
|
pub use update::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
|
pub enum InputPosition {
|
||||||
|
Status,
|
||||||
|
Rooms,
|
||||||
|
Messages,
|
||||||
|
MessageCompose,
|
||||||
|
RoomInfo,
|
||||||
|
CLI,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct UI<'a> {
|
||||||
|
terminal: Terminal<CrosstermBackend<Stdout>>,
|
||||||
|
input_position: InputPosition,
|
||||||
|
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<'_> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
info!("Destructing UI");
|
||||||
|
disable_raw_mode().expect("While destructing UI -> Failed to disable raw mode");
|
||||||
|
execute!(
|
||||||
|
self.terminal.backend_mut(),
|
||||||
|
LeaveAlternateScreen,
|
||||||
|
DisableMouseCapture
|
||||||
|
).expect("While destructing UI -> Failed execute backend commands (LeaveAlternateScreen and DisableMouseCapture)");
|
||||||
|
self.terminal
|
||||||
|
.show_cursor()
|
||||||
|
.expect("While destructing UI -> Failed to re-enable cursor");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UI<'_> {
|
||||||
|
pub fn new() -> Result<Self> {
|
||||||
|
let stdout = terminal_prepare().context("Falied to prepare terminal")?;
|
||||||
|
let backend = CrosstermBackend::new(stdout);
|
||||||
|
let mut terminal = Terminal::new(backend)?;
|
||||||
|
|
||||||
|
terminal.clear()?;
|
||||||
|
|
||||||
|
let mut message_compose = TextArea::default();
|
||||||
|
message_compose.set_block(
|
||||||
|
Block::default()
|
||||||
|
.title("Message Compose (send: <Alt>+<Enter>)")
|
||||||
|
.borders(Borders::ALL),
|
||||||
|
);
|
||||||
|
|
||||||
|
info!("Initialized UI");
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
terminal,
|
||||||
|
input_position: InputPosition::Rooms,
|
||||||
|
rooms_state: ListState::default(),
|
||||||
|
message_compose,
|
||||||
|
cli: None,
|
||||||
|
setup_ui: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cycle_main_input_position(&mut self) {
|
||||||
|
self.input_position = match self.input_position {
|
||||||
|
InputPosition::Status => InputPosition::Rooms,
|
||||||
|
InputPosition::Rooms => InputPosition::Messages,
|
||||||
|
InputPosition::Messages => InputPosition::MessageCompose,
|
||||||
|
InputPosition::MessageCompose => InputPosition::RoomInfo,
|
||||||
|
InputPosition::RoomInfo => match self.cli {
|
||||||
|
Some(_) => InputPosition::CLI,
|
||||||
|
None => InputPosition::Status,
|
||||||
|
},
|
||||||
|
InputPosition::CLI => InputPosition::Status,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cycle_main_input_position_rev(&mut self) {
|
||||||
|
self.input_position = match self.input_position {
|
||||||
|
InputPosition::Status => match self.cli {
|
||||||
|
Some(_) => InputPosition::CLI,
|
||||||
|
None => InputPosition::RoomInfo,
|
||||||
|
},
|
||||||
|
InputPosition::Rooms => InputPosition::Status,
|
||||||
|
InputPosition::Messages => InputPosition::Rooms,
|
||||||
|
InputPosition::MessageCompose => InputPosition::Messages,
|
||||||
|
InputPosition::RoomInfo => InputPosition::MessageCompose,
|
||||||
|
InputPosition::CLI => InputPosition::RoomInfo,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn input_position(&self) -> &InputPosition {
|
||||||
|
&self.input_position
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn message_compose_clear(&mut self) {
|
||||||
|
self.message_compose = TextArea::default();
|
||||||
|
self.message_compose.set_block(
|
||||||
|
Block::default()
|
||||||
|
.title("Message Compose (send: <Alt>+<Enter>)")
|
||||||
|
.borders(Borders::ALL),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cli_enable(&mut self) {
|
||||||
|
self.input_position = InputPosition::CLI;
|
||||||
|
if self.cli.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut cli = TextArea::default();
|
||||||
|
cli.set_block(Block::default().borders(Borders::ALL));
|
||||||
|
self.cli = Some(cli);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cli_disable(&mut self) {
|
||||||
|
if self.input_position == InputPosition::CLI {
|
||||||
|
self.cycle_main_input_position();
|
||||||
|
}
|
||||||
|
self.cli = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_setup(&mut self) -> Result<()> {
|
||||||
|
let ui = match &mut self.setup_ui {
|
||||||
|
Some(c) => c,
|
||||||
|
None => bail!("SetupUI instance not found"),
|
||||||
|
};
|
||||||
|
|
||||||
|
ui.update(&mut self.terminal).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,167 @@
|
||||||
|
use std::cmp;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use tui::{
|
||||||
|
layout::{Constraint, Direction, Layout},
|
||||||
|
style::Color,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::status::Status,
|
||||||
|
ui::{textarea_activate, textarea_inactivate},
|
||||||
|
};
|
||||||
|
|
||||||
|
use self::widgets::{messages, room_info, rooms, status};
|
||||||
|
|
||||||
|
use super::{InputPosition, UI};
|
||||||
|
|
||||||
|
pub mod widgets;
|
||||||
|
|
||||||
|
impl UI<'_> {
|
||||||
|
pub async fn update(&mut self, status: &Status) -> Result<()> {
|
||||||
|
let chunks = match self.cli {
|
||||||
|
Some(_) => Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Min(10), Constraint::Length(3)].as_ref())
|
||||||
|
.split(self.terminal.size()?),
|
||||||
|
None => vec![self.terminal.size()?],
|
||||||
|
};
|
||||||
|
|
||||||
|
let main_chunks = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints(
|
||||||
|
[
|
||||||
|
Constraint::Length(32),
|
||||||
|
Constraint::Min(16),
|
||||||
|
Constraint::Length(32),
|
||||||
|
]
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
|
.split(chunks[0]);
|
||||||
|
|
||||||
|
let left_chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Length(5), Constraint::Min(4)].as_ref())
|
||||||
|
.split(main_chunks[0]);
|
||||||
|
|
||||||
|
let middle_chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints(
|
||||||
|
[
|
||||||
|
Constraint::Min(4),
|
||||||
|
Constraint::Length(cmp::min(2 + self.message_compose.lines().len() as u16, 8)),
|
||||||
|
]
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
|
.split(main_chunks[1]);
|
||||||
|
|
||||||
|
let right_chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Min(4)].as_ref())
|
||||||
|
.split(main_chunks[2]);
|
||||||
|
|
||||||
|
// calculate to widgets colors, based of which widget is currently selected
|
||||||
|
let colors = match self.input_position {
|
||||||
|
InputPosition::Status => {
|
||||||
|
textarea_inactivate(&mut self.message_compose);
|
||||||
|
if let Some(cli) = &mut self.cli {
|
||||||
|
textarea_inactivate(cli);
|
||||||
|
}
|
||||||
|
vec![
|
||||||
|
Color::White,
|
||||||
|
Color::DarkGray,
|
||||||
|
Color::DarkGray,
|
||||||
|
Color::DarkGray,
|
||||||
|
Color::DarkGray,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
InputPosition::Rooms => {
|
||||||
|
textarea_inactivate(&mut self.message_compose);
|
||||||
|
if let Some(cli) = &mut self.cli {
|
||||||
|
textarea_inactivate(cli);
|
||||||
|
}
|
||||||
|
vec![
|
||||||
|
Color::DarkGray,
|
||||||
|
Color::White,
|
||||||
|
Color::DarkGray,
|
||||||
|
Color::DarkGray,
|
||||||
|
Color::DarkGray,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
InputPosition::Messages => {
|
||||||
|
textarea_inactivate(&mut self.message_compose);
|
||||||
|
if let Some(cli) = &mut self.cli {
|
||||||
|
textarea_inactivate(cli);
|
||||||
|
}
|
||||||
|
vec![
|
||||||
|
Color::DarkGray,
|
||||||
|
Color::DarkGray,
|
||||||
|
Color::White,
|
||||||
|
Color::DarkGray,
|
||||||
|
Color::DarkGray,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
InputPosition::MessageCompose => {
|
||||||
|
textarea_activate(&mut self.message_compose);
|
||||||
|
if let Some(cli) = &mut self.cli {
|
||||||
|
textarea_inactivate(cli);
|
||||||
|
}
|
||||||
|
vec![
|
||||||
|
Color::DarkGray,
|
||||||
|
Color::DarkGray,
|
||||||
|
Color::DarkGray,
|
||||||
|
Color::DarkGray,
|
||||||
|
Color::DarkGray,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
InputPosition::RoomInfo => {
|
||||||
|
textarea_inactivate(&mut self.message_compose);
|
||||||
|
if let Some(cli) = &mut self.cli {
|
||||||
|
textarea_inactivate(cli);
|
||||||
|
}
|
||||||
|
vec![
|
||||||
|
Color::DarkGray,
|
||||||
|
Color::DarkGray,
|
||||||
|
Color::DarkGray,
|
||||||
|
Color::DarkGray,
|
||||||
|
Color::White,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
InputPosition::CLI => {
|
||||||
|
textarea_inactivate(&mut self.message_compose);
|
||||||
|
if let Some(cli) = &mut self.cli {
|
||||||
|
textarea_activate(cli);
|
||||||
|
}
|
||||||
|
vec![
|
||||||
|
Color::DarkGray,
|
||||||
|
Color::DarkGray,
|
||||||
|
Color::DarkGray,
|
||||||
|
Color::DarkGray,
|
||||||
|
Color::DarkGray,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// initiate the widgets
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 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]);
|
||||||
|
match &self.cli {
|
||||||
|
Some(cli) => frame.render_widget(cli.widget(), chunks[1]),
|
||||||
|
None => (),
|
||||||
|
};
|
||||||
|
frame.render_widget(room_info_panel, right_chunks[0]);
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
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<&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),
|
||||||
|
),
|
||||||
|
])])),
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
pub mod messages;
|
||||||
|
pub mod room_info;
|
||||||
|
pub mod rooms;
|
||||||
|
pub mod status;
|
|
@ -0,0 +1,36 @@
|
||||||
|
use tui::{
|
||||||
|
style::{Color, Style},
|
||||||
|
text::Text,
|
||||||
|
widgets::{Block, Borders, Paragraph}, layout::Alignment,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{app::status::Room, ui::central::InputPosition};
|
||||||
|
|
||||||
|
pub fn init<'a>(room: Option<&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)
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
use tui::{
|
||||||
|
style::{Color, Modifier, Style},
|
||||||
|
text::Span,
|
||||||
|
widgets::{Borders, List, ListItem, Block},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{app::status::Status, ui::central::InputPosition};
|
||||||
|
|
||||||
|
pub fn init<'a>(status: &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(">")
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
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: &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)
|
||||||
|
}
|
609
src/ui/mod.rs
609
src/ui/mod.rs
|
@ -1,64 +1,23 @@
|
||||||
use std::{cmp, io, io::Stdout};
|
pub mod central;
|
||||||
|
pub mod setup;
|
||||||
|
|
||||||
use anyhow::{Error, Result};
|
use std::{io, io::Stdout};
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
use cli_log::info;
|
use cli_log::info;
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
event::{DisableMouseCapture, EnableMouseCapture},
|
event::EnableMouseCapture,
|
||||||
execute,
|
execute,
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
terminal::{enable_raw_mode, EnterAlternateScreen},
|
||||||
};
|
};
|
||||||
use matrix_sdk::ruma::events::{AnyMessageLikeEvent, AnyTimelineEvent};
|
|
||||||
use tui::{
|
use tui::{
|
||||||
backend::CrosstermBackend,
|
|
||||||
layout::{Alignment, Constraint, Corner, Direction, Layout},
|
|
||||||
style::{Color, Modifier, Style},
|
style::{Color, Modifier, Style},
|
||||||
text::{Span, Spans, Text},
|
widgets::{Block, Borders},
|
||||||
widgets::{Block, Borders, List, ListItem, ListState, Paragraph},
|
|
||||||
Terminal,
|
|
||||||
};
|
};
|
||||||
use tui_textarea::TextArea;
|
use tui_textarea::TextArea;
|
||||||
|
|
||||||
use crate::app::status::Status;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
pub enum SetupInputPosition {
|
|
||||||
Homeserver,
|
|
||||||
Username,
|
|
||||||
Password,
|
|
||||||
Ok,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq)]
|
|
||||||
pub enum MainInputPosition {
|
|
||||||
Status,
|
|
||||||
Rooms,
|
|
||||||
Messages,
|
|
||||||
MessageCompose,
|
|
||||||
RoomInfo,
|
|
||||||
CLI,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct SetupUI<'a> {
|
|
||||||
input_position: SetupInputPosition,
|
|
||||||
|
|
||||||
pub homeserver: TextArea<'a>,
|
|
||||||
pub username: TextArea<'a>,
|
|
||||||
pub password: TextArea<'a>,
|
|
||||||
pub password_data: TextArea<'a>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct UI<'a> {
|
|
||||||
terminal: Terminal<CrosstermBackend<Stdout>>,
|
|
||||||
input_position: MainInputPosition,
|
|
||||||
pub rooms_state: ListState,
|
|
||||||
pub message_compose: TextArea<'a>,
|
|
||||||
pub cli: Option<TextArea<'a>>,
|
|
||||||
|
|
||||||
pub setup_ui: Option<SetupUI<'a>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn terminal_prepare() -> Result<Stdout> {
|
fn terminal_prepare() -> Result<Stdout> {
|
||||||
enable_raw_mode()?;
|
enable_raw_mode().context("Failed to enable raw mode")?;
|
||||||
let mut stdout = io::stdout();
|
let mut stdout = io::stdout();
|
||||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||||
info!("Prepared terminal");
|
info!("Prepared terminal");
|
||||||
|
@ -84,553 +43,3 @@ pub fn textarea_inactivate(textarea: &mut TextArea) {
|
||||||
.unwrap_or_else(|| Block::default().borders(Borders::ALL));
|
.unwrap_or_else(|| Block::default().borders(Borders::ALL));
|
||||||
textarea.set_block(b.style(Style::default().fg(Color::DarkGray)));
|
textarea.set_block(b.style(Style::default().fg(Color::DarkGray)));
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for UI<'_> {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
info!("Destructing UI");
|
|
||||||
disable_raw_mode().expect("While destructing UI -> Failed to disable raw mode");
|
|
||||||
execute!(
|
|
||||||
self.terminal.backend_mut(),
|
|
||||||
LeaveAlternateScreen,
|
|
||||||
DisableMouseCapture
|
|
||||||
).expect("While destructing UI -> Failed execute backend commands (LeaveAlternateScreen and DisableMouseCapture)");
|
|
||||||
self.terminal
|
|
||||||
.show_cursor()
|
|
||||||
.expect("While destructing UI -> Failed to re-enable cursor");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SetupUI<'_> {
|
|
||||||
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: SetupInputPosition::Homeserver,
|
|
||||||
homeserver,
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
password_data,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cycle_input_position(&mut self) {
|
|
||||||
self.input_position = match self.input_position {
|
|
||||||
SetupInputPosition::Homeserver => {
|
|
||||||
textarea_inactivate(&mut self.homeserver);
|
|
||||||
textarea_activate(&mut self.username);
|
|
||||||
textarea_inactivate(&mut self.password);
|
|
||||||
SetupInputPosition::Username
|
|
||||||
}
|
|
||||||
SetupInputPosition::Username => {
|
|
||||||
textarea_inactivate(&mut self.homeserver);
|
|
||||||
textarea_inactivate(&mut self.username);
|
|
||||||
textarea_activate(&mut self.password);
|
|
||||||
SetupInputPosition::Password
|
|
||||||
}
|
|
||||||
SetupInputPosition::Password => {
|
|
||||||
textarea_inactivate(&mut self.homeserver);
|
|
||||||
textarea_inactivate(&mut self.username);
|
|
||||||
textarea_inactivate(&mut self.password);
|
|
||||||
SetupInputPosition::Ok
|
|
||||||
}
|
|
||||||
SetupInputPosition::Ok => {
|
|
||||||
textarea_activate(&mut self.homeserver);
|
|
||||||
textarea_inactivate(&mut self.username);
|
|
||||||
textarea_inactivate(&mut self.password);
|
|
||||||
SetupInputPosition::Homeserver
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cycle_input_position_rev(&mut self) {
|
|
||||||
self.input_position = match self.input_position {
|
|
||||||
SetupInputPosition::Homeserver => {
|
|
||||||
textarea_inactivate(&mut self.homeserver);
|
|
||||||
textarea_inactivate(&mut self.username);
|
|
||||||
textarea_inactivate(&mut self.password);
|
|
||||||
SetupInputPosition::Ok
|
|
||||||
}
|
|
||||||
SetupInputPosition::Username => {
|
|
||||||
textarea_activate(&mut self.homeserver);
|
|
||||||
textarea_inactivate(&mut self.username);
|
|
||||||
textarea_inactivate(&mut self.password);
|
|
||||||
SetupInputPosition::Homeserver
|
|
||||||
}
|
|
||||||
SetupInputPosition::Password => {
|
|
||||||
textarea_inactivate(&mut self.homeserver);
|
|
||||||
textarea_activate(&mut self.username);
|
|
||||||
textarea_inactivate(&mut self.password);
|
|
||||||
SetupInputPosition::Username
|
|
||||||
}
|
|
||||||
SetupInputPosition::Ok => {
|
|
||||||
textarea_inactivate(&mut self.homeserver);
|
|
||||||
textarea_inactivate(&mut self.username);
|
|
||||||
textarea_activate(&mut self.password);
|
|
||||||
SetupInputPosition::Password
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn input_position(&self) -> &SetupInputPosition {
|
|
||||||
&self.input_position
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update(
|
|
||||||
&'_ mut self,
|
|
||||||
terminal: &mut Terminal<CrosstermBackend<Stdout>>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let mut strings: Vec<String> = Vec::new();
|
|
||||||
strings.resize(3, "".to_string());
|
|
||||||
|
|
||||||
let content_ok = match self.input_position {
|
|
||||||
SetupInputPosition::Ok => {
|
|
||||||
Span::styled("OK", Style::default().add_modifier(Modifier::UNDERLINED))
|
|
||||||
}
|
|
||||||
_ => 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(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UI<'_> {
|
|
||||||
pub fn new() -> Result<Self> {
|
|
||||||
let stdout = terminal_prepare()?;
|
|
||||||
let backend = CrosstermBackend::new(stdout);
|
|
||||||
let mut terminal = Terminal::new(backend)?;
|
|
||||||
|
|
||||||
terminal.clear()?;
|
|
||||||
|
|
||||||
let mut message_compose = TextArea::default();
|
|
||||||
message_compose.set_block(
|
|
||||||
Block::default()
|
|
||||||
.title("Message Compose (send: <Alt>+<Enter>)")
|
|
||||||
.borders(Borders::ALL),
|
|
||||||
);
|
|
||||||
|
|
||||||
info!("Initialized UI");
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
terminal,
|
|
||||||
input_position: MainInputPosition::Rooms,
|
|
||||||
rooms_state: ListState::default(),
|
|
||||||
message_compose,
|
|
||||||
cli: None,
|
|
||||||
setup_ui: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cycle_main_input_position(&mut self) {
|
|
||||||
self.input_position = match self.input_position {
|
|
||||||
MainInputPosition::Status => MainInputPosition::Rooms,
|
|
||||||
MainInputPosition::Rooms => MainInputPosition::Messages,
|
|
||||||
MainInputPosition::Messages => MainInputPosition::MessageCompose,
|
|
||||||
MainInputPosition::MessageCompose => MainInputPosition::RoomInfo,
|
|
||||||
MainInputPosition::RoomInfo => match self.cli {
|
|
||||||
Some(_) => MainInputPosition::CLI,
|
|
||||||
None => MainInputPosition::Status,
|
|
||||||
},
|
|
||||||
MainInputPosition::CLI => MainInputPosition::Status,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cycle_main_input_position_rev(&mut self) {
|
|
||||||
self.input_position = match self.input_position {
|
|
||||||
MainInputPosition::Status => match self.cli {
|
|
||||||
Some(_) => MainInputPosition::CLI,
|
|
||||||
None => MainInputPosition::RoomInfo,
|
|
||||||
},
|
|
||||||
MainInputPosition::Rooms => MainInputPosition::Status,
|
|
||||||
MainInputPosition::Messages => MainInputPosition::Rooms,
|
|
||||||
MainInputPosition::MessageCompose => MainInputPosition::Messages,
|
|
||||||
MainInputPosition::RoomInfo => MainInputPosition::MessageCompose,
|
|
||||||
MainInputPosition::CLI => MainInputPosition::RoomInfo,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn input_position(&self) -> &MainInputPosition {
|
|
||||||
&self.input_position
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn message_compose_clear(&mut self) {
|
|
||||||
self.message_compose = TextArea::default();
|
|
||||||
self.message_compose.set_block(
|
|
||||||
Block::default()
|
|
||||||
.title("Message Compose (send: <Alt>+<Enter>)")
|
|
||||||
.borders(Borders::ALL),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cli_enable(&mut self) {
|
|
||||||
self.input_position = MainInputPosition::CLI;
|
|
||||||
if self.cli.is_some() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let mut cli = TextArea::default();
|
|
||||||
cli.set_block(Block::default().borders(Borders::ALL));
|
|
||||||
self.cli = Some(cli);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cli_disable(&mut self) {
|
|
||||||
if self.input_position == MainInputPosition::CLI {
|
|
||||||
self.cycle_main_input_position();
|
|
||||||
}
|
|
||||||
self.cli = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update(&mut self, status: &Status) -> Result<()> {
|
|
||||||
let chunks = match self.cli {
|
|
||||||
Some(_) => Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints([Constraint::Min(10), Constraint::Length(3)].as_ref())
|
|
||||||
.split(self.terminal.size()?),
|
|
||||||
None => vec![self.terminal.size()?],
|
|
||||||
};
|
|
||||||
|
|
||||||
let main_chunks = Layout::default()
|
|
||||||
.direction(Direction::Horizontal)
|
|
||||||
.constraints(
|
|
||||||
[
|
|
||||||
Constraint::Length(32),
|
|
||||||
Constraint::Min(16),
|
|
||||||
Constraint::Length(32),
|
|
||||||
]
|
|
||||||
.as_ref(),
|
|
||||||
)
|
|
||||||
.split(chunks[0]);
|
|
||||||
|
|
||||||
let left_chunks = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints([Constraint::Length(5), Constraint::Min(4)].as_ref())
|
|
||||||
.split(main_chunks[0]);
|
|
||||||
|
|
||||||
let middle_chunks = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints(
|
|
||||||
[
|
|
||||||
Constraint::Min(4),
|
|
||||||
Constraint::Length(cmp::min(2 + self.message_compose.lines().len() as u16, 8)),
|
|
||||||
]
|
|
||||||
.as_ref(),
|
|
||||||
)
|
|
||||||
.split(main_chunks[1]);
|
|
||||||
|
|
||||||
let right_chunks = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints([Constraint::Min(4)].as_ref())
|
|
||||||
.split(main_chunks[2]);
|
|
||||||
|
|
||||||
let mut status_content = Text::styled(
|
|
||||||
status.account_name(),
|
|
||||||
Style::default().add_modifier(Modifier::BOLD),
|
|
||||||
);
|
|
||||||
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),
|
|
||||||
));
|
|
||||||
|
|
||||||
let rooms_content = status
|
|
||||||
.rooms()
|
|
||||||
.iter()
|
|
||||||
.map(|(_, room)| ListItem::new(Span::styled(room.name(), Style::default())))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let messages_content = match status.room() {
|
|
||||||
Some(r) => {
|
|
||||||
r.timeline()
|
|
||||||
.iter()
|
|
||||||
.rev()
|
|
||||||
.map(|event| {
|
|
||||||
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()
|
|
||||||
.unwrap()
|
|
||||||
.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),
|
|
||||||
));
|
|
||||||
ListItem::new(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
// State Events
|
|
||||||
AnyTimelineEvent::State(state) => {
|
|
||||||
ListItem::new(vec![Spans::from(vec![
|
|
||||||
Span::styled(
|
|
||||||
state.sender().to_string(),
|
|
||||||
Style::default().fg(Color::DarkGray),
|
|
||||||
),
|
|
||||||
Span::styled(": ", Style::default().fg(Color::DarkGray)),
|
|
||||||
Span::styled(
|
|
||||||
state.event_type().to_string(),
|
|
||||||
Style::default().fg(Color::DarkGray),
|
|
||||||
),
|
|
||||||
])])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
vec![ListItem::new(Text::styled(
|
|
||||||
"No room selected!",
|
|
||||||
Style::default().fg(Color::Red),
|
|
||||||
))]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut messages_state = ListState::default();
|
|
||||||
let mut room_info_content = Text::default();
|
|
||||||
|
|
||||||
if let Some(room) = status.room() {
|
|
||||||
messages_state.select(room.view_scroll());
|
|
||||||
|
|
||||||
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),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// calculate to widgets colors, based of which widget is currently selected
|
|
||||||
let colors = match self.input_position {
|
|
||||||
MainInputPosition::Status => {
|
|
||||||
textarea_inactivate(&mut self.message_compose);
|
|
||||||
if let Some(cli) = &mut self.cli {
|
|
||||||
textarea_inactivate(cli);
|
|
||||||
}
|
|
||||||
vec![
|
|
||||||
Color::White,
|
|
||||||
Color::DarkGray,
|
|
||||||
Color::DarkGray,
|
|
||||||
Color::DarkGray,
|
|
||||||
Color::DarkGray,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
MainInputPosition::Rooms => {
|
|
||||||
textarea_inactivate(&mut self.message_compose);
|
|
||||||
if let Some(cli) = &mut self.cli {
|
|
||||||
textarea_inactivate(cli);
|
|
||||||
}
|
|
||||||
vec![
|
|
||||||
Color::DarkGray,
|
|
||||||
Color::White,
|
|
||||||
Color::DarkGray,
|
|
||||||
Color::DarkGray,
|
|
||||||
Color::DarkGray,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
MainInputPosition::Messages => {
|
|
||||||
textarea_inactivate(&mut self.message_compose);
|
|
||||||
if let Some(cli) = &mut self.cli {
|
|
||||||
textarea_inactivate(cli);
|
|
||||||
}
|
|
||||||
vec![
|
|
||||||
Color::DarkGray,
|
|
||||||
Color::DarkGray,
|
|
||||||
Color::White,
|
|
||||||
Color::DarkGray,
|
|
||||||
Color::DarkGray,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
MainInputPosition::MessageCompose => {
|
|
||||||
textarea_activate(&mut self.message_compose);
|
|
||||||
if let Some(cli) = &mut self.cli {
|
|
||||||
textarea_inactivate(cli);
|
|
||||||
}
|
|
||||||
vec![
|
|
||||||
Color::DarkGray,
|
|
||||||
Color::DarkGray,
|
|
||||||
Color::DarkGray,
|
|
||||||
Color::DarkGray,
|
|
||||||
Color::DarkGray,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
MainInputPosition::RoomInfo => {
|
|
||||||
textarea_inactivate(&mut self.message_compose);
|
|
||||||
if let Some(cli) = &mut self.cli {
|
|
||||||
textarea_inactivate(cli);
|
|
||||||
}
|
|
||||||
vec![
|
|
||||||
Color::DarkGray,
|
|
||||||
Color::DarkGray,
|
|
||||||
Color::DarkGray,
|
|
||||||
Color::DarkGray,
|
|
||||||
Color::White,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
MainInputPosition::CLI => {
|
|
||||||
textarea_inactivate(&mut self.message_compose);
|
|
||||||
if let Some(cli) = &mut self.cli {
|
|
||||||
textarea_activate(cli);
|
|
||||||
}
|
|
||||||
vec![
|
|
||||||
Color::DarkGray,
|
|
||||||
Color::DarkGray,
|
|
||||||
Color::DarkGray,
|
|
||||||
Color::DarkGray,
|
|
||||||
Color::DarkGray,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// initiate the widgets
|
|
||||||
let status_panel = Paragraph::new(status_content)
|
|
||||||
.block(
|
|
||||||
Block::default()
|
|
||||||
.title("Status")
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.style(Style::default().fg(colors[MainInputPosition::Status as usize])),
|
|
||||||
)
|
|
||||||
.alignment(Alignment::Left);
|
|
||||||
|
|
||||||
let rooms_panel = List::new(rooms_content)
|
|
||||||
.block(
|
|
||||||
Block::default()
|
|
||||||
.title("Rooms (navigate: arrow keys)")
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.style(Style::default().fg(colors[MainInputPosition::Rooms as usize])),
|
|
||||||
)
|
|
||||||
.style(Style::default().fg(Color::DarkGray))
|
|
||||||
.highlight_style(
|
|
||||||
Style::default()
|
|
||||||
.fg(Color::Cyan)
|
|
||||||
.add_modifier(Modifier::BOLD),
|
|
||||||
)
|
|
||||||
.highlight_symbol(">");
|
|
||||||
|
|
||||||
let messages_panel = List::new(messages_content)
|
|
||||||
.block(
|
|
||||||
Block::default()
|
|
||||||
.title("Messages")
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.style(Style::default().fg(colors[MainInputPosition::Messages as usize])),
|
|
||||||
)
|
|
||||||
.start_corner(Corner::BottomLeft)
|
|
||||||
.highlight_symbol(">")
|
|
||||||
.highlight_style(
|
|
||||||
Style::default()
|
|
||||||
.fg(Color::LightMagenta)
|
|
||||||
.add_modifier(Modifier::BOLD),
|
|
||||||
);
|
|
||||||
|
|
||||||
let room_info_panel = Paragraph::new(room_info_content)
|
|
||||||
.block(
|
|
||||||
Block::default()
|
|
||||||
.title("Room Info")
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.style(Style::default().fg(colors[MainInputPosition::RoomInfo as usize])),
|
|
||||||
)
|
|
||||||
.alignment(Alignment::Center);
|
|
||||||
|
|
||||||
// render the widgets
|
|
||||||
self.terminal.draw(|frame| {
|
|
||||||
frame.render_widget(status_panel, left_chunks[0]);
|
|
||||||
frame.render_stateful_widget(rooms_panel, left_chunks[1], &mut self.rooms_state);
|
|
||||||
frame.render_stateful_widget(messages_panel, middle_chunks[0], &mut messages_state);
|
|
||||||
frame.render_widget(self.message_compose.widget(), middle_chunks[1]);
|
|
||||||
match &self.cli {
|
|
||||||
Some(cli) => frame.render_widget(cli.widget(), chunks[1]),
|
|
||||||
None => (),
|
|
||||||
};
|
|
||||||
frame.render_widget(room_info_panel, right_chunks[0]);
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_setup(&mut self) -> Result<()> {
|
|
||||||
let ui = match &mut self.setup_ui {
|
|
||||||
Some(c) => c,
|
|
||||||
None => return Err(Error::msg("SetupUI instance not found")),
|
|
||||||
};
|
|
||||||
|
|
||||||
ui.update(&mut self.terminal).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,172 @@
|
||||||
|
use std::io::Stdout;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use tui::{
|
||||||
|
backend::CrosstermBackend,
|
||||||
|
layout::{Constraint, Direction, Layout, Alignment},
|
||||||
|
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