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