Refactor(ui): Split into multiple files

This commit is contained in:
Benedikt Peetz 2023-07-15 12:29:35 +02:00
parent dfeac4662d
commit 8f9a2a3f22
Signed by: bpeetz
GPG Key ID: A5E94010C3A642AD
12 changed files with 725 additions and 617 deletions

View File

@ -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<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?;
}
input => 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<E
}
};
}
ui::MainInputPosition::Rooms => {
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<E
_ => (),
};
}
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<E
_ => (),
};
}
ui::MainInputPosition::CLI => {
central::InputPosition::CLI => {
if let Some(_) = app.ui.cli {
match input {
CrosstermEvent::Key(KeyEvent {

View File

@ -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<EventStatus> {
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() {
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<E
};
}
input => 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 {

View File

@ -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);

155
src/ui/central/mod.rs Normal file
View File

@ -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(())
}
}

View File

@ -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(())
}
}

View File

@ -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),
),
])])),
}
}

View File

@ -0,0 +1,4 @@
pub mod messages;
pub mod room_info;
pub mod rooms;
pub mod status;

View File

@ -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)
}

View File

@ -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(">")
}

View File

@ -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)
}

View File

@ -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<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> {
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<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(())
}
}

172
src/ui/setup.rs Normal file
View File

@ -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(())
}
}