diff --git a/src/app/events/event_types/event/handlers/main.rs b/src/app/events/event_types/event/handlers/main.rs index 8e3bdd1..880339a 100644 --- a/src/app/events/event_types/event/handlers/main.rs +++ b/src/app/events/event_types/event/handlers/main.rs @@ -3,7 +3,7 @@ use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEvent, KeyModifiers} use crate::{ app::{command, command::Command, events::event_types::EventStatus, App}, - ui, + ui::central, }; pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result { @@ -32,7 +32,7 @@ pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result match app.ui.input_position() { - ui::MainInputPosition::MessageCompose => { + central::InputPosition::MessageCompose => { match input { CrosstermEvent::Key(KeyEvent { code: KeyCode::Enter, @@ -53,7 +53,7 @@ pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result { + central::InputPosition::Rooms => { match input { CrosstermEvent::Key(KeyEvent { code: KeyCode::Up, .. @@ -91,7 +91,7 @@ pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result (), }; } - ui::MainInputPosition::Messages => { + central::InputPosition::Messages => { match input { CrosstermEvent::Key(KeyEvent { code: KeyCode::Up, .. @@ -136,7 +136,7 @@ pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result (), }; } - ui::MainInputPosition::CLI => { + central::InputPosition::CLI => { if let Some(_) = app.ui.cli { match input { CrosstermEvent::Key(KeyEvent { diff --git a/src/app/events/event_types/event/handlers/setup.rs b/src/app/events/event_types/event/handlers/setup.rs index 38cafb2..8e22128 100644 --- a/src/app/events/event_types/event/handlers/setup.rs +++ b/src/app/events/event_types/event/handlers/setup.rs @@ -1,10 +1,7 @@ use anyhow::{bail, Context, Result}; use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEvent}; -use crate::{ - app::{events::event_types::EventStatus, App}, - ui, -}; +use crate::{app::{events::event_types::EventStatus, App}, ui::setup}; pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result { let ui = match &mut app.ui.setup_ui { @@ -32,7 +29,7 @@ pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result { match ui.input_position() { - ui::SetupInputPosition::Ok => { + 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(); @@ -46,13 +43,13 @@ pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result match ui.input_position() { - ui::SetupInputPosition::Homeserver => { + setup::InputPosition::Homeserver => { ui.homeserver.input(input.to_owned()); } - ui::SetupInputPosition::Username => { + setup::InputPosition::Username => { ui.username.input(input.to_owned()); } - ui::SetupInputPosition::Password => { + setup::InputPosition::Password => { let textarea_input = tui_textarea::Input::from(input.to_owned()); ui.password_data.input(textarea_input.clone()); match textarea_input.key { diff --git a/src/app/mod.rs b/src/app/mod.rs index 10652c9..785fa8a 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -14,12 +14,12 @@ use status::{State, Status}; use tokio::sync::mpsc; 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}; pub struct App<'ui> { - ui: ui::UI<'ui>, + ui: central::UI<'ui>, accounts_manager: accounts::AccountsManager, status: Status, @@ -53,7 +53,7 @@ impl App<'_> { let (channel_tx, channel_rx) = mpsc::channel(256); Ok(Self { - ui: ui::UI::new()?, + ui: central::UI::new()?, accounts_manager: AccountsManager::new(config)?, status: Status::new(None), @@ -117,7 +117,7 @@ impl App<'_> { } async fn setup(&mut self) -> Result<()> { - self.ui.setup_ui = Some(ui::SetupUI::new()); + self.ui.setup_ui = Some(setup::UI::new()); loop { self.status.set_state(State::Setup); diff --git a/src/ui/central/mod.rs b/src/ui/central/mod.rs new file mode 100644 index 0000000..e03e658 --- /dev/null +++ b/src/ui/central/mod.rs @@ -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>, + input_position: InputPosition, + pub rooms_state: ListState, + pub message_compose: TextArea<'a>, + pub cli: Option>, + + pub setup_ui: Option>, +} + +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 { + 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: +)") + .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: +)") + .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(()) + } +} diff --git a/src/ui/central/update/mod.rs b/src/ui/central/update/mod.rs new file mode 100644 index 0000000..412cb8c --- /dev/null +++ b/src/ui/central/update/mod.rs @@ -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(()) + } +} diff --git a/src/ui/central/update/widgets/messages.rs b/src/ui/central/update/widgets/messages.rs new file mode 100644 index 0000000..0c077b8 --- /dev/null +++ b/src/ui/central/update/widgets/messages.rs @@ -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) -> 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> { + let results: Vec> = 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> { + 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), + ), + ])])), + } +} diff --git a/src/ui/central/update/widgets/mod.rs b/src/ui/central/update/widgets/mod.rs new file mode 100644 index 0000000..850c27f --- /dev/null +++ b/src/ui/central/update/widgets/mod.rs @@ -0,0 +1,4 @@ +pub mod messages; +pub mod room_info; +pub mod rooms; +pub mod status; diff --git a/src/ui/central/update/widgets/room_info.rs b/src/ui/central/update/widgets/room_info.rs new file mode 100644 index 0000000..a277e65 --- /dev/null +++ b/src/ui/central/update/widgets/room_info.rs @@ -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) -> 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) +} diff --git a/src/ui/central/update/widgets/rooms.rs b/src/ui/central/update/widgets/rooms.rs new file mode 100644 index 0000000..4e09a31 --- /dev/null +++ b/src/ui/central/update/widgets/rooms.rs @@ -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) -> 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(">") +} diff --git a/src/ui/central/update/widgets/status.rs b/src/ui/central/update/widgets/status.rs new file mode 100644 index 0000000..108668e --- /dev/null +++ b/src/ui/central/update/widgets/status.rs @@ -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) -> 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) +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index f4d627f..28ebea2 100644 --- a/src/ui/mod.rs +++ b/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 crossterm::{ - event::{DisableMouseCapture, EnableMouseCapture}, + event::EnableMouseCapture, execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, + terminal::{enable_raw_mode, EnterAlternateScreen}, }; -use matrix_sdk::ruma::events::{AnyMessageLikeEvent, AnyTimelineEvent}; use tui::{ - backend::CrosstermBackend, - layout::{Alignment, Constraint, Corner, Direction, Layout}, style::{Color, Modifier, Style}, - text::{Span, Spans, Text}, - widgets::{Block, Borders, List, ListItem, ListState, Paragraph}, - Terminal, + widgets::{Block, Borders}, }; 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>, - input_position: MainInputPosition, - pub rooms_state: ListState, - pub message_compose: TextArea<'a>, - pub cli: Option>, - - pub setup_ui: Option>, -} - fn terminal_prepare() -> Result { - enable_raw_mode()?; + enable_raw_mode().context("Failed to enable raw mode")?; let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; info!("Prepared terminal"); @@ -84,553 +43,3 @@ pub fn textarea_inactivate(textarea: &mut TextArea) { .unwrap_or_else(|| Block::default().borders(Borders::ALL)); 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>, - ) -> Result<()> { - let mut strings: Vec = 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 { - 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: +)") - .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: +)") - .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::>(); - - 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::>() - } - 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(()) - } -} diff --git a/src/ui/setup.rs b/src/ui/setup.rs new file mode 100644 index 0000000..da339b2 --- /dev/null +++ b/src/ui/setup.rs @@ -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>, + ) -> Result<()> { + let strings: Vec = 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(()) + } +}