mirror of
https://github.com/veeso/termscp.git
synced 2025-12-07 09:36:00 -08:00
Removed tui; added tuirealm
This commit is contained in:
@@ -32,11 +32,11 @@ extern crate dirs;
|
||||
use super::{AuthActivity, FileTransferProtocol};
|
||||
use crate::system::bookmarks_client::BookmarksClient;
|
||||
use crate::system::environment;
|
||||
use crate::ui::layout::props::PropValue;
|
||||
use crate::ui::layout::Payload;
|
||||
|
||||
// Ext
|
||||
use std::path::PathBuf;
|
||||
use tuirealm::components::{input::InputPropsBuilder, radio::RadioPropsBuilder};
|
||||
use tuirealm::{Payload, PropsBuilder};
|
||||
|
||||
impl AuthActivity {
|
||||
/// ### del_bookmark
|
||||
@@ -83,7 +83,7 @@ impl AuthActivity {
|
||||
let password: Option<String> = match save_password {
|
||||
true => match self
|
||||
.view
|
||||
.get_value(super::COMPONENT_RADIO_BOOKMARK_SAVE_PWD)
|
||||
.get_state(super::COMPONENT_RADIO_BOOKMARK_SAVE_PWD)
|
||||
{
|
||||
Some(Payload::Unsigned(0)) => Some(password), // Yes
|
||||
_ => None, // No such component / No
|
||||
@@ -246,32 +246,34 @@ impl AuthActivity {
|
||||
password: Option<String>,
|
||||
) {
|
||||
// Load parameters into components
|
||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_ADDR) {
|
||||
let props = props.with_value(PropValue::Str(addr)).build();
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_ADDR) {
|
||||
let props = InputPropsBuilder::from(props).with_value(addr).build();
|
||||
self.view.update(super::COMPONENT_INPUT_ADDR, props);
|
||||
}
|
||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_PORT) {
|
||||
let props = props.with_value(PropValue::Str(port.to_string())).build();
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_PORT) {
|
||||
let props = InputPropsBuilder::from(props)
|
||||
.with_value(port.to_string())
|
||||
.build();
|
||||
self.view.update(super::COMPONENT_INPUT_PORT, props);
|
||||
}
|
||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_RADIO_PROTOCOL) {
|
||||
let props = props
|
||||
.with_value(PropValue::Unsigned(match protocol {
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_PROTOCOL) {
|
||||
let props = RadioPropsBuilder::from(props)
|
||||
.with_value(match protocol {
|
||||
FileTransferProtocol::Sftp => 0,
|
||||
FileTransferProtocol::Scp => 1,
|
||||
FileTransferProtocol::Ftp(false) => 2,
|
||||
FileTransferProtocol::Ftp(true) => 3,
|
||||
}))
|
||||
})
|
||||
.build();
|
||||
self.view.update(super::COMPONENT_RADIO_PROTOCOL, props);
|
||||
}
|
||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_USERNAME) {
|
||||
let props = props.with_value(PropValue::Str(username)).build();
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_USERNAME) {
|
||||
let props = InputPropsBuilder::from(props).with_value(username).build();
|
||||
self.view.update(super::COMPONENT_INPUT_USERNAME, props);
|
||||
}
|
||||
if let Some(password) = password {
|
||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_PASSWORD) {
|
||||
let props = props.with_value(PropValue::Str(password)).build();
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_PASSWORD) {
|
||||
let props = InputPropsBuilder::from(props).with_value(password).build();
|
||||
self.view.update(super::COMPONENT_INPUT_PASSWORD, props);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,18 +32,18 @@ mod view;
|
||||
|
||||
// Dependencies
|
||||
extern crate crossterm;
|
||||
extern crate tui;
|
||||
extern crate tuirealm;
|
||||
|
||||
// locals
|
||||
use super::{Activity, Context, ExitReason};
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
use crate::system::bookmarks_client::BookmarksClient;
|
||||
use crate::ui::context::FileTransferParams;
|
||||
use crate::ui::layout::view::View;
|
||||
use crate::utils::git;
|
||||
|
||||
// Includes
|
||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||
use tuirealm::View;
|
||||
|
||||
// -- components
|
||||
const COMPONENT_TEXT_HEADER: &str = "TEXT_HEADER";
|
||||
|
||||
@@ -35,7 +35,7 @@ use super::{
|
||||
COMPONENT_TEXT_HELP,
|
||||
};
|
||||
use crate::ui::activities::keymap::*;
|
||||
use crate::ui::layout::{Msg, Payload};
|
||||
use tuirealm::{Msg, Payload};
|
||||
|
||||
// -- update
|
||||
|
||||
@@ -164,7 +164,7 @@ impl AuthActivity {
|
||||
match *index {
|
||||
0 => {
|
||||
// Get selected bookmark
|
||||
match self.view.get_value(COMPONENT_BOOKMARKS_LIST) {
|
||||
match self.view.get_state(COMPONENT_BOOKMARKS_LIST) {
|
||||
Some(Payload::Unsigned(index)) => {
|
||||
// Delete bookmark
|
||||
self.del_bookmark(index);
|
||||
@@ -184,7 +184,7 @@ impl AuthActivity {
|
||||
match *index {
|
||||
0 => {
|
||||
// Get selected bookmark
|
||||
match self.view.get_value(COMPONENT_RECENTS_LIST) {
|
||||
match self.view.get_state(COMPONENT_RECENTS_LIST) {
|
||||
Some(Payload::Unsigned(index)) => {
|
||||
// Delete recent
|
||||
self.del_recent(index);
|
||||
@@ -199,36 +199,12 @@ impl AuthActivity {
|
||||
}
|
||||
// <ESC> hide tab
|
||||
(COMPONENT_RADIO_BOOKMARK_DEL_RECENT, &MSG_KEY_ESC) => {
|
||||
match self
|
||||
.view
|
||||
.get_props(COMPONENT_RADIO_BOOKMARK_DEL_RECENT)
|
||||
.as_mut()
|
||||
{
|
||||
Some(props) => {
|
||||
let msg = self.view.update(
|
||||
COMPONENT_RADIO_BOOKMARK_DEL_RECENT,
|
||||
props.hidden().build(),
|
||||
);
|
||||
self.update(msg)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
self.umount_recent_del_dialog();
|
||||
None
|
||||
}
|
||||
(COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK, &MSG_KEY_ESC) => {
|
||||
match self
|
||||
.view
|
||||
.get_props(COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK)
|
||||
.as_mut()
|
||||
{
|
||||
Some(props) => {
|
||||
let msg = self.view.update(
|
||||
COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK,
|
||||
props.hidden().build(),
|
||||
);
|
||||
self.update(msg)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
self.umount_bookmark_del_dialog();
|
||||
None
|
||||
}
|
||||
// Help
|
||||
(_, &MSG_KEY_CTRL_H) => {
|
||||
@@ -269,12 +245,12 @@ impl AuthActivity {
|
||||
| (COMPONENT_RADIO_BOOKMARK_SAVE_PWD, Msg::OnSubmit(_)) => {
|
||||
// Get values
|
||||
let bookmark_name: String =
|
||||
match self.view.get_value(COMPONENT_INPUT_BOOKMARK_NAME) {
|
||||
match self.view.get_state(COMPONENT_INPUT_BOOKMARK_NAME) {
|
||||
Some(Payload::Text(s)) => s,
|
||||
_ => String::new(),
|
||||
};
|
||||
let save_pwd: bool = matches!(
|
||||
self.view.get_value(COMPONENT_RADIO_BOOKMARK_SAVE_PWD),
|
||||
self.view.get_state(COMPONENT_RADIO_BOOKMARK_SAVE_PWD),
|
||||
Some(Payload::Unsigned(0))
|
||||
);
|
||||
// Save bookmark
|
||||
|
||||
@@ -27,20 +27,27 @@
|
||||
*/
|
||||
// Locals
|
||||
use super::{AuthActivity, Context, FileTransferProtocol};
|
||||
use crate::ui::layout::components::{
|
||||
bookmark_list::BookmarkList, input::Input, msgbox::MsgBox, radio_group::RadioGroup,
|
||||
table::Table, text::Text, title::Title,
|
||||
use crate::ui::components::{
|
||||
bookmark_list::{BookmarkList, BookmarkListPropsBuilder},
|
||||
msgbox::{MsgBox, MsgBoxPropsBuilder},
|
||||
};
|
||||
use crate::ui::layout::props::{
|
||||
InputType, PropValue, PropsBuilder, TableBuilder, TextParts, TextSpan, TextSpanBuilder,
|
||||
};
|
||||
use crate::ui::layout::utils::draw_area_in;
|
||||
use crate::ui::layout::{Msg, Payload};
|
||||
use crate::utils::ui::draw_area_in;
|
||||
// Ext
|
||||
use tui::{
|
||||
use tuirealm::components::{
|
||||
input::{Input, InputPropsBuilder},
|
||||
label::{Label, LabelPropsBuilder},
|
||||
radio::{Radio, RadioPropsBuilder},
|
||||
span::{Span, SpanPropsBuilder},
|
||||
table::{Table, TablePropsBuilder},
|
||||
};
|
||||
use tuirealm::tui::{
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::Color,
|
||||
widgets::Clear,
|
||||
widgets::{BorderType, Borders, Clear},
|
||||
};
|
||||
use tuirealm::{
|
||||
props::{InputType, PropsBuilder, TableBuilder, TextSpan, TextSpanBuilder},
|
||||
Msg, Payload,
|
||||
};
|
||||
|
||||
impl AuthActivity {
|
||||
@@ -50,35 +57,32 @@ impl AuthActivity {
|
||||
pub(super) fn init(&mut self) {
|
||||
// Header
|
||||
self.view.mount(super::COMPONENT_TEXT_HEADER, Box::new(
|
||||
Title::new(
|
||||
PropsBuilder::default().with_foreground(Color::White).with_texts(
|
||||
TextParts::new(Some(String::from(" _____ ____ ____ ____ \n|_ _|__ _ __ _ __ ___ / ___| / ___| _ \\ \n | |/ _ \\ '__| '_ ` _ \\\\___ \\| | | |_) |\n | | __/ | | | | | | |___) | |___| __/ \n |_|\\___|_| |_| |_| |_|____/ \\____|_| \n")), None)
|
||||
Label::new(
|
||||
LabelPropsBuilder::default().with_foreground(Color::White).with_text(
|
||||
String::from(" _____ ____ ____ ____ \n|_ _|__ _ __ _ __ ___ / ___| / ___| _ \\ \n | |/ _ \\ '__| '_ ` _ \\\\___ \\| | | |_) |\n | | __/ | | | | | | |___) | |___| __/ \n |_|\\___|_| |_| |_| |_|____/ \\____|_| \n")
|
||||
).bold().build()
|
||||
)
|
||||
));
|
||||
// Footer
|
||||
self.view.mount(
|
||||
super::COMPONENT_TEXT_FOOTER,
|
||||
Box::new(Text::new(
|
||||
PropsBuilder::default()
|
||||
.with_texts(TextParts::new(
|
||||
None,
|
||||
Some(vec![
|
||||
TextSpanBuilder::new("Press ").bold().build(),
|
||||
TextSpanBuilder::new("<CTRL+H>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.build(),
|
||||
TextSpanBuilder::new(" to show keybindings; ")
|
||||
.bold()
|
||||
.build(),
|
||||
TextSpanBuilder::new("<CTRL+C>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.build(),
|
||||
TextSpanBuilder::new(" to enter setup").bold().build(),
|
||||
]),
|
||||
))
|
||||
Box::new(Span::new(
|
||||
SpanPropsBuilder::default()
|
||||
.with_spans(vec![
|
||||
TextSpanBuilder::new("Press ").bold().build(),
|
||||
TextSpanBuilder::new("<CTRL+H>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.build(),
|
||||
TextSpanBuilder::new(" to show keybindings; ")
|
||||
.bold()
|
||||
.build(),
|
||||
TextSpanBuilder::new("<CTRL+C>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.build(),
|
||||
TextSpanBuilder::new(" to enter setup").bold().build(),
|
||||
])
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -86,9 +90,10 @@ impl AuthActivity {
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_ADDR,
|
||||
Box::new(Input::new(
|
||||
PropsBuilder::default()
|
||||
InputPropsBuilder::default()
|
||||
.with_foreground(Color::Yellow)
|
||||
.with_texts(TextParts::new(Some(String::from("Remote address")), None))
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightYellow)
|
||||
.with_label(String::from("Remote address"))
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -96,31 +101,33 @@ impl AuthActivity {
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_PORT,
|
||||
Box::new(Input::new(
|
||||
PropsBuilder::default()
|
||||
InputPropsBuilder::default()
|
||||
.with_foreground(Color::LightCyan)
|
||||
.with_texts(TextParts::new(Some(String::from("Port number")), None))
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightCyan)
|
||||
.with_label(String::from("Port number"))
|
||||
.with_input(InputType::Number)
|
||||
.with_input_len(5)
|
||||
.with_value(PropValue::Str(String::from("22")))
|
||||
.with_value(String::from("22"))
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
// Protocol
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_PROTOCOL,
|
||||
Box::new(RadioGroup::new(
|
||||
PropsBuilder::default()
|
||||
.with_foreground(Color::LightGreen)
|
||||
.with_background(Color::Black)
|
||||
.with_texts(TextParts::new(
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.with_color(Color::LightGreen)
|
||||
.with_inverted_color(Color::Black)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightGreen)
|
||||
.with_options(
|
||||
Some(String::from("Protocol")),
|
||||
Some(vec![
|
||||
TextSpan::from("SFTP"),
|
||||
TextSpan::from("SCP"),
|
||||
TextSpan::from("FTP"),
|
||||
TextSpan::from("FTPS"),
|
||||
]),
|
||||
))
|
||||
vec![
|
||||
String::from("SFTP"),
|
||||
String::from("SCP"),
|
||||
String::from("FTP"),
|
||||
String::from("FTPS"),
|
||||
],
|
||||
)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -128,9 +135,10 @@ impl AuthActivity {
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_USERNAME,
|
||||
Box::new(Input::new(
|
||||
PropsBuilder::default()
|
||||
InputPropsBuilder::default()
|
||||
.with_foreground(Color::LightMagenta)
|
||||
.with_texts(TextParts::new(Some(String::from("Username")), None))
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightMagenta)
|
||||
.with_label(String::from("Username"))
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -138,9 +146,10 @@ impl AuthActivity {
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_PASSWORD,
|
||||
Box::new(Input::new(
|
||||
PropsBuilder::default()
|
||||
InputPropsBuilder::default()
|
||||
.with_foreground(Color::LightBlue)
|
||||
.with_texts(TextParts::new(Some(String::from("Password")), None))
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightBlue)
|
||||
.with_label(String::from("Password"))
|
||||
.with_input(InputType::Password)
|
||||
.build(),
|
||||
)),
|
||||
@@ -155,10 +164,16 @@ impl AuthActivity {
|
||||
{
|
||||
self.view.mount(
|
||||
super::COMPONENT_TEXT_NEW_VERSION,
|
||||
Box::new(Text::new(
|
||||
PropsBuilder::default()
|
||||
Box::new(Span::new(
|
||||
SpanPropsBuilder::default()
|
||||
.with_foreground(Color::Yellow)
|
||||
.with_texts(TextParts::new(None, Some(vec![TextSpan::from(format!("TermSCP {} is now available! Download it from <https://github.com/veeso/termscp/releases/latest>", version))])))
|
||||
.with_spans(
|
||||
vec![
|
||||
TextSpan::from("TermSCP "),
|
||||
TextSpanBuilder::new(version).underlined().bold().build(),
|
||||
TextSpan::from(" is now available! Download it from <https://github.com/veeso/termscp/releases/latest>")
|
||||
]
|
||||
)
|
||||
.build()
|
||||
))
|
||||
);
|
||||
@@ -167,10 +182,11 @@ impl AuthActivity {
|
||||
self.view.mount(
|
||||
super::COMPONENT_BOOKMARKS_LIST,
|
||||
Box::new(BookmarkList::new(
|
||||
PropsBuilder::default()
|
||||
BookmarkListPropsBuilder::default()
|
||||
.with_background(Color::LightGreen)
|
||||
.with_foreground(Color::Black)
|
||||
.with_texts(TextParts::new(Some(String::from("Bookmarks")), None))
|
||||
.with_borders(Borders::ALL, BorderType::Plain, Color::LightGreen)
|
||||
.with_bookmarks(Some(String::from("Bookmarks")), vec![])
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -179,13 +195,11 @@ impl AuthActivity {
|
||||
self.view.mount(
|
||||
super::COMPONENT_RECENTS_LIST,
|
||||
Box::new(BookmarkList::new(
|
||||
PropsBuilder::default()
|
||||
BookmarkListPropsBuilder::default()
|
||||
.with_background(Color::LightBlue)
|
||||
.with_foreground(Color::Black)
|
||||
.with_texts(TextParts::new(
|
||||
Some(String::from("Recent connections")),
|
||||
None,
|
||||
))
|
||||
.with_borders(Borders::ALL, BorderType::Plain, Color::LightBlue)
|
||||
.with_bookmarks(Some(String::from("Recent connections")), vec![])
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -258,27 +272,27 @@ impl AuthActivity {
|
||||
self.view
|
||||
.render(super::COMPONENT_RECENTS_LIST, f, bookmark_chunks[1]);
|
||||
// Popups
|
||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) {
|
||||
if props.build().visible {
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) {
|
||||
if props.visible {
|
||||
let popup = draw_area_in(f.size(), 50, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
// make popup
|
||||
self.view.render(super::COMPONENT_TEXT_ERROR, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) {
|
||||
if props.build().visible {
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) {
|
||||
if props.visible {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 30, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
self.view.render(super::COMPONENT_RADIO_QUIT, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(mut props) = self
|
||||
if let Some(props) = self
|
||||
.view
|
||||
.get_props(super::COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK)
|
||||
{
|
||||
if props.build().visible {
|
||||
if props.visible {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 30, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
@@ -286,11 +300,11 @@ impl AuthActivity {
|
||||
.render(super::COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(mut props) = self
|
||||
if let Some(props) = self
|
||||
.view
|
||||
.get_props(super::COMPONENT_RADIO_BOOKMARK_DEL_RECENT)
|
||||
{
|
||||
if props.build().visible {
|
||||
if props.visible {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 30, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
@@ -298,19 +312,19 @@ impl AuthActivity {
|
||||
.render(super::COMPONENT_RADIO_BOOKMARK_DEL_RECENT, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_TEXT_HELP) {
|
||||
if props.build().visible {
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_HELP) {
|
||||
if props.visible {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 50, 70);
|
||||
f.render_widget(Clear, popup);
|
||||
self.view.render(super::COMPONENT_TEXT_HELP, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(mut props) = self
|
||||
if let Some(props) = self
|
||||
.view
|
||||
.get_props(super::COMPONENT_RADIO_BOOKMARK_SAVE_PWD)
|
||||
{
|
||||
if props.build().visible {
|
||||
if props.visible {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 20, 20);
|
||||
f.render_widget(Clear, popup);
|
||||
@@ -340,7 +354,7 @@ impl AuthActivity {
|
||||
///
|
||||
/// Make text span from bookmarks
|
||||
pub(super) fn view_bookmarks(&mut self) -> Option<(String, Msg)> {
|
||||
let bookmarks: Vec<TextSpan> = self
|
||||
let bookmarks: Vec<String> = self
|
||||
.bookmarks_list
|
||||
.iter()
|
||||
.map(|x| {
|
||||
@@ -350,33 +364,23 @@ impl AuthActivity {
|
||||
.unwrap()
|
||||
.get_bookmark(x)
|
||||
.unwrap();
|
||||
TextSpan::from(
|
||||
format!(
|
||||
"{} ({}://{}@{}:{})",
|
||||
x,
|
||||
entry.2.to_string().to_lowercase(),
|
||||
entry.3,
|
||||
entry.0,
|
||||
entry.1
|
||||
)
|
||||
.as_str(),
|
||||
format!(
|
||||
"{} ({}://{}@{}:{})",
|
||||
x,
|
||||
entry.2.to_string().to_lowercase(),
|
||||
entry.3,
|
||||
entry.0,
|
||||
entry.1
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
match self
|
||||
.view
|
||||
.get_props(super::COMPONENT_BOOKMARKS_LIST)
|
||||
.as_mut()
|
||||
{
|
||||
match self.view.get_props(super::COMPONENT_BOOKMARKS_LIST) {
|
||||
None => None,
|
||||
Some(props) => {
|
||||
let msg = self.view.update(
|
||||
super::COMPONENT_BOOKMARKS_LIST,
|
||||
props
|
||||
.with_texts(TextParts::new(
|
||||
Some(String::from("Bookmarks")),
|
||||
Some(bookmarks),
|
||||
))
|
||||
BookmarkListPropsBuilder::from(props)
|
||||
.with_bookmarks(Some(String::from("Bookmarks")), bookmarks)
|
||||
.build(),
|
||||
);
|
||||
msg
|
||||
@@ -388,7 +392,7 @@ impl AuthActivity {
|
||||
///
|
||||
/// View recent connections
|
||||
pub(super) fn view_recent_connections(&mut self) -> Option<(String, Msg)> {
|
||||
let bookmarks: Vec<TextSpan> = self
|
||||
let bookmarks: Vec<String> = self
|
||||
.recents_list
|
||||
.iter()
|
||||
.map(|x| {
|
||||
@@ -398,28 +402,23 @@ impl AuthActivity {
|
||||
.unwrap()
|
||||
.get_recent(x)
|
||||
.unwrap();
|
||||
TextSpan::from(
|
||||
format!(
|
||||
"{}://{}@{}:{}",
|
||||
entry.2.to_string().to_lowercase(),
|
||||
entry.3,
|
||||
entry.0,
|
||||
entry.1
|
||||
)
|
||||
.as_str(),
|
||||
|
||||
format!(
|
||||
"{}://{}@{}:{}",
|
||||
entry.2.to_string().to_lowercase(),
|
||||
entry.3,
|
||||
entry.0,
|
||||
entry.1
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
match self.view.get_props(super::COMPONENT_RECENTS_LIST).as_mut() {
|
||||
match self.view.get_props(super::COMPONENT_RECENTS_LIST) {
|
||||
None => None,
|
||||
Some(props) => {
|
||||
let msg = self.view.update(
|
||||
super::COMPONENT_RECENTS_LIST,
|
||||
props
|
||||
.with_texts(TextParts::new(
|
||||
Some(String::from("Recent connections")),
|
||||
Some(bookmarks),
|
||||
))
|
||||
BookmarkListPropsBuilder::from(props)
|
||||
.with_bookmarks(Some(String::from("Recent connections")), bookmarks)
|
||||
.build(),
|
||||
);
|
||||
msg
|
||||
@@ -437,10 +436,11 @@ impl AuthActivity {
|
||||
self.view.mount(
|
||||
super::COMPONENT_TEXT_ERROR,
|
||||
Box::new(MsgBox::new(
|
||||
PropsBuilder::default()
|
||||
MsgBoxPropsBuilder::default()
|
||||
.with_foreground(Color::Red)
|
||||
.with_borders(Borders::ALL, BorderType::Thick, Color::Red)
|
||||
.bold()
|
||||
.with_texts(TextParts::new(None, Some(vec![TextSpan::from(text)])))
|
||||
.with_texts(None, vec![TextSpan::from(text)])
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -462,14 +462,15 @@ impl AuthActivity {
|
||||
// Protocol
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_QUIT,
|
||||
Box::new(RadioGroup::new(
|
||||
PropsBuilder::default()
|
||||
.with_foreground(Color::Yellow)
|
||||
.with_background(Color::Black)
|
||||
.with_texts(TextParts::new(
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.with_color(Color::Yellow)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::Yellow)
|
||||
.with_inverted_color(Color::Black)
|
||||
.with_options(
|
||||
Some(String::from("Quit TermSCP?")),
|
||||
Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]),
|
||||
))
|
||||
vec![String::from("Yes"), String::from("No")],
|
||||
)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -489,15 +490,16 @@ impl AuthActivity {
|
||||
pub(super) fn mount_bookmark_del_dialog(&mut self) {
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK,
|
||||
Box::new(RadioGroup::new(
|
||||
PropsBuilder::default()
|
||||
.with_foreground(Color::Yellow)
|
||||
.with_background(Color::Black)
|
||||
.with_texts(TextParts::new(
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.with_color(Color::Yellow)
|
||||
.with_inverted_color(Color::Black)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::Yellow)
|
||||
.with_options(
|
||||
Some(String::from("Delete bookmark?")),
|
||||
Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]),
|
||||
))
|
||||
.with_value(PropValue::Unsigned(1))
|
||||
vec![String::from("Yes"), String::from("No")],
|
||||
)
|
||||
.with_value(1)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -520,15 +522,16 @@ impl AuthActivity {
|
||||
pub(super) fn mount_recent_del_dialog(&mut self) {
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_BOOKMARK_DEL_RECENT,
|
||||
Box::new(RadioGroup::new(
|
||||
PropsBuilder::default()
|
||||
.with_foreground(Color::Yellow)
|
||||
.with_background(Color::Black)
|
||||
.with_texts(TextParts::new(
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.with_color(Color::Yellow)
|
||||
.with_inverted_color(Color::Black)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::Yellow)
|
||||
.with_options(
|
||||
Some(String::from("Delete bookmark?")),
|
||||
Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]),
|
||||
))
|
||||
.with_value(PropValue::Unsigned(1))
|
||||
vec![String::from("Yes"), String::from("No")],
|
||||
)
|
||||
.with_value(1)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -550,27 +553,31 @@ impl AuthActivity {
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_BOOKMARK_NAME,
|
||||
Box::new(Input::new(
|
||||
PropsBuilder::default()
|
||||
InputPropsBuilder::default()
|
||||
.with_foreground(Color::LightCyan)
|
||||
.with_texts(TextParts::new(
|
||||
Some(String::from("Save bookmark as...")),
|
||||
None,
|
||||
))
|
||||
//.with_borders(Borders::TOP | Borders::RIGHT | Borders::LEFT)
|
||||
.with_label(String::from("Save bookmark as..."))
|
||||
.with_borders(
|
||||
Borders::TOP | Borders::RIGHT | Borders::LEFT,
|
||||
BorderType::Rounded,
|
||||
Color::Reset,
|
||||
)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_BOOKMARK_SAVE_PWD,
|
||||
Box::new(RadioGroup::new(
|
||||
PropsBuilder::default()
|
||||
.with_foreground(Color::Red)
|
||||
//.with_borders(Borders::BOTTOM | Borders::RIGHT | Borders::LEFT)
|
||||
.with_texts(TextParts::new(
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.with_color(Color::Red)
|
||||
.with_borders(
|
||||
Borders::BOTTOM | Borders::RIGHT | Borders::LEFT,
|
||||
BorderType::Rounded,
|
||||
Color::Reset,
|
||||
)
|
||||
.with_options(
|
||||
Some(String::from("Save password?")),
|
||||
Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]),
|
||||
))
|
||||
//.with_value(PropValue::Unsigned(1))
|
||||
vec![String::from("Yes"), String::from("No")],
|
||||
)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -593,8 +600,9 @@ impl AuthActivity {
|
||||
self.view.mount(
|
||||
super::COMPONENT_TEXT_HELP,
|
||||
Box::new(Table::new(
|
||||
PropsBuilder::default()
|
||||
.with_texts(TextParts::table(
|
||||
TablePropsBuilder::default()
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
|
||||
.with_table(
|
||||
Some(String::from("Help")),
|
||||
TableBuilder::default()
|
||||
.add_col(
|
||||
@@ -661,7 +669,7 @@ impl AuthActivity {
|
||||
)
|
||||
.add_col(TextSpan::from(" Save bookmark"))
|
||||
.build(),
|
||||
))
|
||||
)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -680,16 +688,16 @@ impl AuthActivity {
|
||||
///
|
||||
/// Collect input values from view
|
||||
pub(super) fn get_input(&self) -> (String, u16, FileTransferProtocol, String, String) {
|
||||
let addr: String = match self.view.get_value(super::COMPONENT_INPUT_ADDR) {
|
||||
let addr: String = match self.view.get_state(super::COMPONENT_INPUT_ADDR) {
|
||||
Some(Payload::Text(a)) => a,
|
||||
_ => String::new(),
|
||||
};
|
||||
let port: u16 = match self.view.get_value(super::COMPONENT_INPUT_PORT) {
|
||||
let port: u16 = match self.view.get_state(super::COMPONENT_INPUT_PORT) {
|
||||
Some(Payload::Unsigned(p)) => p as u16,
|
||||
_ => 0,
|
||||
};
|
||||
let protocol: FileTransferProtocol =
|
||||
match self.view.get_value(super::COMPONENT_RADIO_PROTOCOL) {
|
||||
match self.view.get_state(super::COMPONENT_RADIO_PROTOCOL) {
|
||||
Some(Payload::Unsigned(p)) => match p {
|
||||
1 => FileTransferProtocol::Scp,
|
||||
2 => FileTransferProtocol::Ftp(false),
|
||||
@@ -698,11 +706,11 @@ impl AuthActivity {
|
||||
},
|
||||
_ => FileTransferProtocol::Sftp,
|
||||
};
|
||||
let username: String = match self.view.get_value(super::COMPONENT_INPUT_USERNAME) {
|
||||
let username: String = match self.view.get_state(super::COMPONENT_INPUT_USERNAME) {
|
||||
Some(Payload::Text(a)) => a,
|
||||
_ => String::new(),
|
||||
};
|
||||
let password: String = match self.view.get_value(super::COMPONENT_INPUT_PASSWORD) {
|
||||
let password: String = match self.view.get_state(super::COMPONENT_INPUT_PASSWORD) {
|
||||
Some(Payload::Text(a)) => a,
|
||||
_ => String::new(),
|
||||
};
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
*/
|
||||
// locals
|
||||
use super::{FileExplorerTab, FileTransferActivity, FsEntry, LogLevel};
|
||||
use crate::ui::layout::Payload;
|
||||
use tuirealm::Payload;
|
||||
// externals
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -622,7 +622,7 @@ impl FileTransferActivity {
|
||||
///
|
||||
/// Get index of selected file in the local tab
|
||||
fn get_local_file_idx(&self) -> Option<usize> {
|
||||
match self.view.get_value(super::COMPONENT_EXPLORER_LOCAL) {
|
||||
match self.view.get_state(super::COMPONENT_EXPLORER_LOCAL) {
|
||||
Some(Payload::Unsigned(idx)) => Some(idx),
|
||||
_ => None,
|
||||
}
|
||||
@@ -632,7 +632,7 @@ impl FileTransferActivity {
|
||||
///
|
||||
/// Get index of selected file in the remote file
|
||||
fn get_remote_file_idx(&self) -> Option<usize> {
|
||||
match self.view.get_value(super::COMPONENT_EXPLORER_REMOTE) {
|
||||
match self.view.get_state(super::COMPONENT_EXPLORER_REMOTE) {
|
||||
Some(Payload::Unsigned(idx)) => Some(idx),
|
||||
_ => None,
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ mod view;
|
||||
extern crate chrono;
|
||||
extern crate crossterm;
|
||||
extern crate textwrap;
|
||||
extern crate tui;
|
||||
extern crate tuirealm;
|
||||
|
||||
// locals
|
||||
use super::{Activity, Context, ExitReason};
|
||||
@@ -47,7 +47,6 @@ use crate::filetransfer::{FileTransfer, FileTransferProtocol};
|
||||
use crate::fs::explorer::FileExplorer;
|
||||
use crate::fs::FsEntry;
|
||||
use crate::system::config_client::ConfigClient;
|
||||
use crate::ui::layout::view::View;
|
||||
|
||||
// Includes
|
||||
use chrono::{DateTime, Local};
|
||||
@@ -55,6 +54,7 @@ use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||
use std::collections::VecDeque;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Instant;
|
||||
use tuirealm::View;
|
||||
|
||||
// -- Storage keys
|
||||
|
||||
|
||||
@@ -40,14 +40,16 @@ use super::{
|
||||
use crate::fs::explorer::FileSorting;
|
||||
use crate::fs::FsEntry;
|
||||
use crate::ui::activities::keymap::*;
|
||||
use crate::ui::layout::props::{
|
||||
PropValue, PropsBuilder, TableBuilder, TextParts, TextSpan, TextSpanBuilder,
|
||||
};
|
||||
use crate::ui::layout::{Msg, Payload};
|
||||
use crate::ui::components::{file_list::FileListPropsBuilder, logbox::LogboxPropsBuilder};
|
||||
// externals
|
||||
use bytesize::ByteSize;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tui::style::Color;
|
||||
use tuirealm::{
|
||||
components::progress_bar::ProgressBarPropsBuilder,
|
||||
props::{PropsBuilder, TableBuilder, TextSpan, TextSpanBuilder},
|
||||
tui::style::Color,
|
||||
Msg, Payload,
|
||||
};
|
||||
|
||||
impl FileTransferActivity {
|
||||
// -- update
|
||||
@@ -394,7 +396,7 @@ impl FileTransferActivity {
|
||||
}
|
||||
(COMPONENT_EXPLORER_FIND, &MSG_KEY_SPACE) => {
|
||||
// Get entry
|
||||
match self.view.get_value(COMPONENT_EXPLORER_FIND) {
|
||||
match self.view.get_state(COMPONENT_EXPLORER_FIND) {
|
||||
Some(Payload::Unsigned(idx)) => {
|
||||
self.action_find_transfer(idx, None);
|
||||
// Reload files
|
||||
@@ -585,7 +587,7 @@ impl FileTransferActivity {
|
||||
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => {
|
||||
// Get entry
|
||||
if let Some(Payload::Unsigned(idx)) =
|
||||
self.view.get_value(COMPONENT_EXPLORER_FIND)
|
||||
self.view.get_state(COMPONENT_EXPLORER_FIND)
|
||||
{
|
||||
self.action_find_transfer(idx, Some(input.to_string()));
|
||||
}
|
||||
@@ -621,7 +623,7 @@ impl FileTransferActivity {
|
||||
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => {
|
||||
// Get entry
|
||||
if let Some(Payload::Unsigned(idx)) =
|
||||
self.view.get_value(COMPONENT_EXPLORER_FIND)
|
||||
self.view.get_state(COMPONENT_EXPLORER_FIND)
|
||||
{
|
||||
self.action_find_delete(idx);
|
||||
// Reload entries
|
||||
@@ -662,11 +664,12 @@ impl FileTransferActivity {
|
||||
None
|
||||
}
|
||||
// -- sorting
|
||||
(COMPONENT_RADIO_SORTING, &MSG_KEY_ESC) => {
|
||||
(COMPONENT_RADIO_SORTING, &MSG_KEY_ESC)
|
||||
| (COMPONENT_RADIO_SORTING, Msg::OnSubmit(_)) => {
|
||||
self.umount_file_sorting();
|
||||
None
|
||||
}
|
||||
(COMPONENT_RADIO_SORTING, Msg::OnSubmit(Payload::Unsigned(mode))) => {
|
||||
(COMPONENT_RADIO_SORTING, Msg::OnChange(Payload::Unsigned(mode))) => {
|
||||
// Get sorting mode
|
||||
let sorting: FileSorting = match mode {
|
||||
1 => FileSorting::ByModifyTime,
|
||||
@@ -679,7 +682,6 @@ impl FileTransferActivity {
|
||||
FileExplorerTab::Remote => self.remote.sort_by(sorting),
|
||||
_ => panic!("Found result doesn't support SORTING"),
|
||||
}
|
||||
self.umount_file_sorting();
|
||||
// Reload files
|
||||
match self.tab {
|
||||
FileExplorerTab::Local => self.update_local_filelist(),
|
||||
@@ -718,11 +720,7 @@ impl FileTransferActivity {
|
||||
///
|
||||
/// Update local file list
|
||||
pub(super) fn update_local_filelist(&mut self) -> Option<(String, Msg)> {
|
||||
match self
|
||||
.view
|
||||
.get_props(super::COMPONENT_EXPLORER_LOCAL)
|
||||
.as_mut()
|
||||
{
|
||||
match self.view.get_props(super::COMPONENT_EXPLORER_LOCAL) {
|
||||
Some(props) => {
|
||||
// Get width
|
||||
let width: usize = self
|
||||
@@ -750,14 +748,14 @@ impl FileTransferActivity {
|
||||
)
|
||||
.display()
|
||||
);
|
||||
let files: Vec<TextSpan> = self
|
||||
let files: Vec<String> = self
|
||||
.local
|
||||
.iter_files()
|
||||
.map(|x: &FsEntry| TextSpan::from(self.local.fmt_file(x)))
|
||||
.map(|x: &FsEntry| self.local.fmt_file(x))
|
||||
.collect();
|
||||
// Update
|
||||
let props = props
|
||||
.with_texts(TextParts::new(Some(hostname), Some(files)))
|
||||
let props = FileListPropsBuilder::from(props)
|
||||
.with_files(Some(hostname), files)
|
||||
.build();
|
||||
// Update
|
||||
self.view.update(super::COMPONENT_EXPLORER_LOCAL, props)
|
||||
@@ -770,11 +768,7 @@ impl FileTransferActivity {
|
||||
///
|
||||
/// Update remote file list
|
||||
pub(super) fn update_remote_filelist(&mut self) -> Option<(String, Msg)> {
|
||||
match self
|
||||
.view
|
||||
.get_props(super::COMPONENT_EXPLORER_REMOTE)
|
||||
.as_mut()
|
||||
{
|
||||
match self.view.get_props(super::COMPONENT_EXPLORER_REMOTE) {
|
||||
Some(props) => {
|
||||
// Get width
|
||||
let width: usize = self
|
||||
@@ -795,14 +789,14 @@ impl FileTransferActivity {
|
||||
)
|
||||
.display()
|
||||
);
|
||||
let files: Vec<TextSpan> = self
|
||||
let files: Vec<String> = self
|
||||
.remote
|
||||
.iter_files()
|
||||
.map(|x: &FsEntry| TextSpan::from(self.remote.fmt_file(x)))
|
||||
.map(|x: &FsEntry| self.remote.fmt_file(x))
|
||||
.collect();
|
||||
// Update
|
||||
let props = props
|
||||
.with_texts(TextParts::new(Some(hostname), Some(files)))
|
||||
let props = FileListPropsBuilder::from(props)
|
||||
.with_files(Some(hostname), files)
|
||||
.build();
|
||||
self.view.update(super::COMPONENT_EXPLORER_REMOTE, props)
|
||||
}
|
||||
@@ -814,7 +808,7 @@ impl FileTransferActivity {
|
||||
///
|
||||
/// Update log box
|
||||
pub(super) fn update_logbox(&mut self) -> Option<(String, Msg)> {
|
||||
match self.view.get_props(super::COMPONENT_LOG_BOX).as_mut() {
|
||||
match self.view.get_props(super::COMPONENT_LOG_BOX) {
|
||||
Some(props) => {
|
||||
// Get width
|
||||
let width: usize = self
|
||||
@@ -876,8 +870,8 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
let table = table.build();
|
||||
let props = props
|
||||
.with_texts(TextParts::table(Some(String::from("Log")), table))
|
||||
let props = LogboxPropsBuilder::from(props)
|
||||
.with_log(Some(String::from("Log")), table)
|
||||
.build();
|
||||
self.view.update(super::COMPONENT_LOG_BOX, props)
|
||||
}
|
||||
@@ -886,7 +880,7 @@ impl FileTransferActivity {
|
||||
}
|
||||
|
||||
pub(super) fn update_progress_bar(&mut self, text: String) -> Option<(String, Msg)> {
|
||||
match self.view.get_props(COMPONENT_PROGRESS_BAR).as_mut() {
|
||||
match self.view.get_props(COMPONENT_PROGRESS_BAR) {
|
||||
Some(props) => {
|
||||
// Calculate ETA
|
||||
let elapsed_secs: u64 = self.transfer.started.elapsed().as_secs();
|
||||
@@ -905,12 +899,9 @@ impl FileTransferActivity {
|
||||
eta,
|
||||
ByteSize(self.transfer.bytes_per_second())
|
||||
);
|
||||
let props = props
|
||||
.with_texts(TextParts::new(
|
||||
Some(text),
|
||||
Some(vec![TextSpan::from(label)]),
|
||||
))
|
||||
.with_value(PropValue::Float(self.transfer.progress / 100.0))
|
||||
let props = ProgressBarPropsBuilder::from(props)
|
||||
.with_texts(Some(text), label)
|
||||
.with_progress(self.transfer.progress / 100.0)
|
||||
.build();
|
||||
self.view.update(COMPONENT_PROGRESS_BAR, props)
|
||||
}
|
||||
@@ -933,22 +924,20 @@ impl FileTransferActivity {
|
||||
}
|
||||
|
||||
fn update_find_list(&mut self) -> Option<(String, Msg)> {
|
||||
match self.view.get_props(COMPONENT_EXPLORER_FIND).as_mut() {
|
||||
match self.view.get_props(COMPONENT_EXPLORER_FIND) {
|
||||
None => None,
|
||||
Some(props) => {
|
||||
let props = props.build();
|
||||
let title: String = props.texts.title.clone().unwrap_or_default();
|
||||
let mut props = PropsBuilder::from(props);
|
||||
// Prepare files
|
||||
let file_texts: Vec<TextSpan> = self
|
||||
let files: Vec<String> = self
|
||||
.found
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.iter_files()
|
||||
.map(|x: &FsEntry| TextSpan::from(self.found.as_ref().unwrap().fmt_file(x)))
|
||||
.map(|x: &FsEntry| self.found.as_ref().unwrap().fmt_file(x))
|
||||
.collect();
|
||||
let props = props
|
||||
.with_texts(TextParts::new(Some(title), Some(file_texts)))
|
||||
let props = FileListPropsBuilder::from(props)
|
||||
.with_files(Some(title), files)
|
||||
.build();
|
||||
self.view.update(COMPONENT_EXPLORER_FIND, props)
|
||||
}
|
||||
|
||||
@@ -34,23 +34,28 @@ extern crate users;
|
||||
use super::{Context, FileExplorerTab, FileTransferActivity};
|
||||
use crate::fs::explorer::FileSorting;
|
||||
use crate::fs::FsEntry;
|
||||
use crate::ui::layout::components::{
|
||||
file_list::FileList, input::Input, logbox::LogBox, msgbox::MsgBox, progress_bar::ProgressBar,
|
||||
radio_group::RadioGroup, table::Table,
|
||||
use crate::ui::components::{
|
||||
file_list::{FileList, FileListPropsBuilder},
|
||||
logbox::{LogBox, LogboxPropsBuilder},
|
||||
msgbox::{MsgBox, MsgBoxPropsBuilder},
|
||||
};
|
||||
use crate::ui::layout::props::{
|
||||
PropValue, PropsBuilder, TableBuilder, TextParts, TextSpan, TextSpanBuilder,
|
||||
};
|
||||
use crate::ui::layout::utils::draw_area_in;
|
||||
use crate::ui::store::Store;
|
||||
use crate::utils::fmt::fmt_time;
|
||||
use crate::utils::ui::draw_area_in;
|
||||
// Ext
|
||||
use bytesize::ByteSize;
|
||||
use std::path::PathBuf;
|
||||
use tui::{
|
||||
use tuirealm::components::{
|
||||
input::{Input, InputPropsBuilder},
|
||||
progress_bar::{ProgressBar, ProgressBarPropsBuilder},
|
||||
radio::{Radio, RadioPropsBuilder},
|
||||
table::{Table, TablePropsBuilder},
|
||||
};
|
||||
use tuirealm::props::{PropsBuilder, TableBuilder, TextSpan, TextSpanBuilder};
|
||||
use tuirealm::tui::{
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::Color,
|
||||
widgets::Clear,
|
||||
widgets::{BorderType, Borders, Clear},
|
||||
};
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
use users::{get_group_by_gid, get_user_by_uid};
|
||||
@@ -66,9 +71,10 @@ impl FileTransferActivity {
|
||||
self.view.mount(
|
||||
super::COMPONENT_EXPLORER_LOCAL,
|
||||
Box::new(FileList::new(
|
||||
PropsBuilder::default()
|
||||
FileListPropsBuilder::default()
|
||||
.with_background(Color::Yellow)
|
||||
.with_foreground(Color::Yellow)
|
||||
.with_borders(Borders::ALL, BorderType::Plain, Color::Yellow)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -76,9 +82,10 @@ impl FileTransferActivity {
|
||||
self.view.mount(
|
||||
super::COMPONENT_EXPLORER_REMOTE,
|
||||
Box::new(FileList::new(
|
||||
PropsBuilder::default()
|
||||
FileListPropsBuilder::default()
|
||||
.with_background(Color::LightBlue)
|
||||
.with_foreground(Color::LightBlue)
|
||||
.with_borders(Borders::ALL, BorderType::Plain, Color::LightBlue)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -86,9 +93,8 @@ impl FileTransferActivity {
|
||||
self.view.mount(
|
||||
super::COMPONENT_LOG_BOX,
|
||||
Box::new(LogBox::new(
|
||||
PropsBuilder::default()
|
||||
.with_foreground(Color::LightGreen)
|
||||
.bold()
|
||||
LogboxPropsBuilder::default()
|
||||
.with_borders(Borders::ALL, BorderType::Plain, Color::LightGreen)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -156,96 +162,96 @@ impl FileTransferActivity {
|
||||
// Draw log box
|
||||
self.view.render(super::COMPONENT_LOG_BOX, f, chunks[1]);
|
||||
// @! Draw popups
|
||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_COPY) {
|
||||
if props.build().visible {
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_COPY) {
|
||||
if props.visible {
|
||||
let popup = draw_area_in(f.size(), 40, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
// make popup
|
||||
self.view.render(super::COMPONENT_INPUT_COPY, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_FIND) {
|
||||
if props.build().visible {
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_FIND) {
|
||||
if props.visible {
|
||||
let popup = draw_area_in(f.size(), 40, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
// make popup
|
||||
self.view.render(super::COMPONENT_INPUT_FIND, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_GOTO) {
|
||||
if props.build().visible {
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_GOTO) {
|
||||
if props.visible {
|
||||
let popup = draw_area_in(f.size(), 40, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
// make popup
|
||||
self.view.render(super::COMPONENT_INPUT_GOTO, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_MKDIR) {
|
||||
if props.build().visible {
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_MKDIR) {
|
||||
if props.visible {
|
||||
let popup = draw_area_in(f.size(), 40, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
// make popup
|
||||
self.view.render(super::COMPONENT_INPUT_MKDIR, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_NEWFILE) {
|
||||
if props.build().visible {
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_NEWFILE) {
|
||||
if props.visible {
|
||||
let popup = draw_area_in(f.size(), 40, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
// make popup
|
||||
self.view.render(super::COMPONENT_INPUT_NEWFILE, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_RENAME) {
|
||||
if props.build().visible {
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_RENAME) {
|
||||
if props.visible {
|
||||
let popup = draw_area_in(f.size(), 40, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
// make popup
|
||||
self.view.render(super::COMPONENT_INPUT_RENAME, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_SAVEAS) {
|
||||
if props.build().visible {
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_SAVEAS) {
|
||||
if props.visible {
|
||||
let popup = draw_area_in(f.size(), 40, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
// make popup
|
||||
self.view.render(super::COMPONENT_INPUT_SAVEAS, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_EXEC) {
|
||||
if props.build().visible {
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_EXEC) {
|
||||
if props.visible {
|
||||
let popup = draw_area_in(f.size(), 40, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
// make popup
|
||||
self.view.render(super::COMPONENT_INPUT_EXEC, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_LIST_FILEINFO) {
|
||||
if props.build().visible {
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_LIST_FILEINFO) {
|
||||
if props.visible {
|
||||
let popup = draw_area_in(f.size(), 50, 50);
|
||||
f.render_widget(Clear, popup);
|
||||
// make popup
|
||||
self.view.render(super::COMPONENT_LIST_FILEINFO, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_PROGRESS_BAR) {
|
||||
if props.build().visible {
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_PROGRESS_BAR) {
|
||||
if props.visible {
|
||||
let popup = draw_area_in(f.size(), 40, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
// make popup
|
||||
self.view.render(super::COMPONENT_PROGRESS_BAR, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_RADIO_DELETE) {
|
||||
if props.build().visible {
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_DELETE) {
|
||||
if props.visible {
|
||||
let popup = draw_area_in(f.size(), 30, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
// make popup
|
||||
self.view.render(super::COMPONENT_RADIO_DELETE, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_RADIO_DISCONNECT) {
|
||||
if props.build().visible {
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_DISCONNECT) {
|
||||
if props.visible {
|
||||
let popup = draw_area_in(f.size(), 30, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
// make popup
|
||||
@@ -253,48 +259,48 @@ impl FileTransferActivity {
|
||||
.render(super::COMPONENT_RADIO_DISCONNECT, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) {
|
||||
if props.build().visible {
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) {
|
||||
if props.visible {
|
||||
let popup = draw_area_in(f.size(), 30, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
// make popup
|
||||
self.view.render(super::COMPONENT_RADIO_QUIT, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_RADIO_SORTING) {
|
||||
if props.build().visible {
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_SORTING) {
|
||||
if props.visible {
|
||||
let popup = draw_area_in(f.size(), 50, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
// make popup
|
||||
self.view.render(super::COMPONENT_RADIO_SORTING, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) {
|
||||
if props.build().visible {
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) {
|
||||
if props.visible {
|
||||
let popup = draw_area_in(f.size(), 50, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
// make popup
|
||||
self.view.render(super::COMPONENT_TEXT_ERROR, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_TEXT_FATAL) {
|
||||
if props.build().visible {
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_FATAL) {
|
||||
if props.visible {
|
||||
let popup = draw_area_in(f.size(), 50, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
// make popup
|
||||
self.view.render(super::COMPONENT_TEXT_FATAL, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_TEXT_WAIT) {
|
||||
if props.build().visible {
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_WAIT) {
|
||||
if props.visible {
|
||||
let popup = draw_area_in(f.size(), 50, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
// make popup
|
||||
self.view.render(super::COMPONENT_TEXT_WAIT, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_TEXT_HELP) {
|
||||
if props.build().visible {
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_HELP) {
|
||||
if props.visible {
|
||||
let popup = draw_area_in(f.size(), 50, 80);
|
||||
f.render_widget(Clear, popup);
|
||||
// make popup
|
||||
@@ -316,10 +322,11 @@ impl FileTransferActivity {
|
||||
self.view.mount(
|
||||
super::COMPONENT_TEXT_ERROR,
|
||||
Box::new(MsgBox::new(
|
||||
PropsBuilder::default()
|
||||
MsgBoxPropsBuilder::default()
|
||||
.with_foreground(Color::Red)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::Red)
|
||||
.bold()
|
||||
.with_texts(TextParts::new(None, Some(vec![TextSpan::from(text)])))
|
||||
.with_texts(None, vec![TextSpan::from(text)])
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -339,10 +346,11 @@ impl FileTransferActivity {
|
||||
self.view.mount(
|
||||
super::COMPONENT_TEXT_FATAL,
|
||||
Box::new(MsgBox::new(
|
||||
PropsBuilder::default()
|
||||
MsgBoxPropsBuilder::default()
|
||||
.with_foreground(Color::Red)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::Red)
|
||||
.bold()
|
||||
.with_texts(TextParts::new(None, Some(vec![TextSpan::from(text)])))
|
||||
.with_texts(None, vec![TextSpan::from(text)])
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -355,10 +363,11 @@ impl FileTransferActivity {
|
||||
self.view.mount(
|
||||
super::COMPONENT_TEXT_WAIT,
|
||||
Box::new(MsgBox::new(
|
||||
PropsBuilder::default()
|
||||
MsgBoxPropsBuilder::default()
|
||||
.with_foreground(Color::White)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
|
||||
.bold()
|
||||
.with_texts(TextParts::new(None, Some(vec![TextSpan::from(text)])))
|
||||
.with_texts(None, vec![TextSpan::from(text)])
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -377,14 +386,15 @@ impl FileTransferActivity {
|
||||
// Protocol
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_QUIT,
|
||||
Box::new(RadioGroup::new(
|
||||
PropsBuilder::default()
|
||||
.with_foreground(Color::Yellow)
|
||||
.with_background(Color::Black)
|
||||
.with_texts(TextParts::new(
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.with_color(Color::Yellow)
|
||||
.with_inverted_color(Color::Black)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::Yellow)
|
||||
.with_options(
|
||||
Some(String::from("Are you sure you want to quit?")),
|
||||
Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]),
|
||||
))
|
||||
vec![String::from("Yes"), String::from("No")],
|
||||
)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -405,14 +415,15 @@ impl FileTransferActivity {
|
||||
// Protocol
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_DISCONNECT,
|
||||
Box::new(RadioGroup::new(
|
||||
PropsBuilder::default()
|
||||
.with_foreground(Color::Yellow)
|
||||
.with_background(Color::Black)
|
||||
.with_texts(TextParts::new(
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.with_color(Color::Yellow)
|
||||
.with_inverted_color(Color::Black)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::Yellow)
|
||||
.with_options(
|
||||
Some(String::from("Are you sure you want to disconnect?")),
|
||||
Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]),
|
||||
))
|
||||
vec![String::from("Yes"), String::from("No")],
|
||||
)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -430,11 +441,9 @@ impl FileTransferActivity {
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_COPY,
|
||||
Box::new(Input::new(
|
||||
PropsBuilder::default()
|
||||
.with_texts(TextParts::new(
|
||||
Some(String::from("Insert destination name")),
|
||||
None,
|
||||
))
|
||||
InputPropsBuilder::default()
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
|
||||
.with_label(String::from("Insert destination name"))
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -449,8 +458,9 @@ impl FileTransferActivity {
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_EXEC,
|
||||
Box::new(Input::new(
|
||||
PropsBuilder::default()
|
||||
.with_texts(TextParts::new(Some(String::from("Execute command")), None))
|
||||
InputPropsBuilder::default()
|
||||
.with_borders(Borders::ALL, BorderType::Plain, Color::White)
|
||||
.with_label(String::from("Execute command"))
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -471,11 +481,9 @@ impl FileTransferActivity {
|
||||
self.view.mount(
|
||||
super::COMPONENT_EXPLORER_FIND,
|
||||
Box::new(FileList::new(
|
||||
PropsBuilder::default()
|
||||
.with_texts(TextParts::new(
|
||||
Some(format!("Search results for \"{}\"", search)),
|
||||
Some(vec![]),
|
||||
))
|
||||
FileListPropsBuilder::default()
|
||||
.with_files(Some(format!("Search results for \"{}\"", search)), vec![])
|
||||
.with_borders(Borders::ALL, BorderType::Plain, color)
|
||||
.with_background(color)
|
||||
.with_foreground(color)
|
||||
.build(),
|
||||
@@ -493,11 +501,9 @@ impl FileTransferActivity {
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_FIND,
|
||||
Box::new(Input::new(
|
||||
PropsBuilder::default()
|
||||
.with_texts(TextParts::new(
|
||||
Some(String::from("Search files by name")),
|
||||
None,
|
||||
))
|
||||
InputPropsBuilder::default()
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
|
||||
.with_label(String::from("Search files by name"))
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -514,11 +520,9 @@ impl FileTransferActivity {
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_GOTO,
|
||||
Box::new(Input::new(
|
||||
PropsBuilder::default()
|
||||
.with_texts(TextParts::new(
|
||||
Some(String::from("Change working directory")),
|
||||
None,
|
||||
))
|
||||
InputPropsBuilder::default()
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
|
||||
.with_label(String::from("Change working directory"))
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -533,11 +537,9 @@ impl FileTransferActivity {
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_MKDIR,
|
||||
Box::new(Input::new(
|
||||
PropsBuilder::default()
|
||||
.with_texts(TextParts::new(
|
||||
Some(String::from("Insert directory name")),
|
||||
None,
|
||||
))
|
||||
InputPropsBuilder::default()
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
|
||||
.with_label(String::from("Insert directory name"))
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -552,8 +554,9 @@ impl FileTransferActivity {
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_NEWFILE,
|
||||
Box::new(Input::new(
|
||||
PropsBuilder::default()
|
||||
.with_texts(TextParts::new(Some(String::from("New file name")), None))
|
||||
InputPropsBuilder::default()
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
|
||||
.with_label(String::from("New file name"))
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -568,8 +571,9 @@ impl FileTransferActivity {
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_RENAME,
|
||||
Box::new(Input::new(
|
||||
PropsBuilder::default()
|
||||
.with_texts(TextParts::new(Some(String::from("Insert new name")), None))
|
||||
InputPropsBuilder::default()
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
|
||||
.with_label(String::from("Insert new name"))
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -584,8 +588,9 @@ impl FileTransferActivity {
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_SAVEAS,
|
||||
Box::new(Input::new(
|
||||
PropsBuilder::default()
|
||||
.with_texts(TextParts::new(Some(String::from("Save as...")), None))
|
||||
InputPropsBuilder::default()
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
|
||||
.with_label(String::from("Save as..."))
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -600,10 +605,11 @@ impl FileTransferActivity {
|
||||
self.view.mount(
|
||||
super::COMPONENT_PROGRESS_BAR,
|
||||
Box::new(ProgressBar::new(
|
||||
PropsBuilder::default()
|
||||
.with_foreground(Color::LightGreen)
|
||||
ProgressBarPropsBuilder::default()
|
||||
.with_progbar_color(Color::LightGreen)
|
||||
.with_background(Color::Black)
|
||||
.with_texts(TextParts::new(Some(String::from("Please wait")), None))
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightGreen)
|
||||
.with_texts(Some(String::from("Please wait")), String::new())
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -628,20 +634,21 @@ impl FileTransferActivity {
|
||||
};
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_SORTING,
|
||||
Box::new(RadioGroup::new(
|
||||
PropsBuilder::default()
|
||||
.with_foreground(Color::LightMagenta)
|
||||
.with_background(Color::Black)
|
||||
.with_texts(TextParts::new(
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.with_color(Color::LightMagenta)
|
||||
.with_inverted_color(Color::Black)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightMagenta)
|
||||
.with_options(
|
||||
Some(String::from("Sort files by")),
|
||||
Some(vec![
|
||||
TextSpan::from("Name"),
|
||||
TextSpan::from("Modify time"),
|
||||
TextSpan::from("Creation time"),
|
||||
TextSpan::from("Size"),
|
||||
]),
|
||||
))
|
||||
.with_value(PropValue::Unsigned(index))
|
||||
vec![
|
||||
String::from("Name"),
|
||||
String::from("Modify time"),
|
||||
String::from("Creation time"),
|
||||
String::from("Size"),
|
||||
],
|
||||
)
|
||||
.with_value(index)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -655,15 +662,16 @@ impl FileTransferActivity {
|
||||
pub(super) fn mount_radio_delete(&mut self) {
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_DELETE,
|
||||
Box::new(RadioGroup::new(
|
||||
PropsBuilder::default()
|
||||
.with_foreground(Color::Red)
|
||||
.with_background(Color::Black)
|
||||
.with_texts(TextParts::new(
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.with_color(Color::Red)
|
||||
.with_inverted_color(Color::Black)
|
||||
.with_borders(Borders::ALL, BorderType::Plain, Color::Red)
|
||||
.with_options(
|
||||
Some(String::from("Delete file")),
|
||||
Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]),
|
||||
))
|
||||
.with_value(PropValue::Unsigned(1))
|
||||
vec![String::from("Yes"), String::from("No")],
|
||||
)
|
||||
.with_value(1)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -772,11 +780,9 @@ impl FileTransferActivity {
|
||||
self.view.mount(
|
||||
super::COMPONENT_LIST_FILEINFO,
|
||||
Box::new(Table::new(
|
||||
PropsBuilder::default()
|
||||
.with_texts(TextParts::table(
|
||||
Some(file.get_name().to_string()),
|
||||
texts.build(),
|
||||
))
|
||||
TablePropsBuilder::default()
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
|
||||
.with_table(Some(file.get_name().to_string()), texts.build())
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -794,8 +800,9 @@ impl FileTransferActivity {
|
||||
self.view.mount(
|
||||
super::COMPONENT_TEXT_HELP,
|
||||
Box::new(Table::new(
|
||||
PropsBuilder::default()
|
||||
.with_texts(TextParts::table(
|
||||
TablePropsBuilder::default()
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
|
||||
.with_table(
|
||||
Some(String::from("Help")),
|
||||
TableBuilder::default()
|
||||
.add_col(
|
||||
@@ -984,7 +991,7 @@ impl FileTransferActivity {
|
||||
)
|
||||
.add_col(TextSpan::from(" Interrupt file transfer"))
|
||||
.build(),
|
||||
))
|
||||
)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
use crate::ui::layout::Msg;
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use tuirealm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use tuirealm::Msg;
|
||||
|
||||
// -- Special keys
|
||||
|
||||
|
||||
@@ -28,10 +28,10 @@
|
||||
*/
|
||||
// Locals
|
||||
use super::SetupActivity;
|
||||
use crate::ui::layout::Payload;
|
||||
// Ext
|
||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||
use std::env;
|
||||
use tuirealm::Payload;
|
||||
|
||||
impl SetupActivity {
|
||||
/// ### action_save_config
|
||||
@@ -63,7 +63,7 @@ impl SetupActivity {
|
||||
// Get key
|
||||
if let Some(config_cli) = self.context.as_mut().unwrap().config_client.as_mut() {
|
||||
// get index
|
||||
let idx: Option<usize> = match self.view.get_value(super::COMPONENT_LIST_SSH_KEYS) {
|
||||
let idx: Option<usize> = match self.view.get_state(super::COMPONENT_LIST_SSH_KEYS) {
|
||||
Some(Payload::Unsigned(idx)) => Some(idx),
|
||||
_ => None,
|
||||
};
|
||||
@@ -99,11 +99,11 @@ impl SetupActivity {
|
||||
pub(super) fn action_new_ssh_key(&mut self) {
|
||||
if let Some(cli) = self.context.as_mut().unwrap().config_client.as_mut() {
|
||||
// get parameters
|
||||
let host: String = match self.view.get_value(super::COMPONENT_INPUT_SSH_HOST) {
|
||||
let host: String = match self.view.get_state(super::COMPONENT_INPUT_SSH_HOST) {
|
||||
Some(Payload::Text(host)) => host,
|
||||
_ => String::new(),
|
||||
};
|
||||
let username: String = match self.view.get_value(super::COMPONENT_INPUT_SSH_USERNAME) {
|
||||
let username: String = match self.view.get_state(super::COMPONENT_INPUT_SSH_USERNAME) {
|
||||
Some(Payload::Text(user)) => user,
|
||||
_ => String::new(),
|
||||
};
|
||||
|
||||
@@ -34,13 +34,13 @@ mod view;
|
||||
|
||||
// Deps
|
||||
extern crate crossterm;
|
||||
extern crate tui;
|
||||
extern crate tuirealm;
|
||||
|
||||
// Locals
|
||||
use super::{Activity, Context, ExitReason};
|
||||
use crate::ui::layout::view::View;
|
||||
// Ext
|
||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||
use tuirealm::View;
|
||||
|
||||
// -- components
|
||||
const COMPONENT_TEXT_HELP: &str = "TEXT_HELP";
|
||||
|
||||
@@ -35,7 +35,9 @@ use super::{
|
||||
COMPONENT_RADIO_UPDATES, COMPONENT_TEXT_ERROR, COMPONENT_TEXT_HELP,
|
||||
};
|
||||
use crate::ui::activities::keymap::*;
|
||||
use crate::ui::layout::{Msg, Payload};
|
||||
|
||||
// ext
|
||||
use tuirealm::{Msg, Payload};
|
||||
|
||||
impl SetupActivity {
|
||||
/// ### update
|
||||
|
||||
@@ -30,22 +30,27 @@
|
||||
use super::{Context, SetupActivity, ViewLayout};
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
use crate::fs::explorer::GroupDirs;
|
||||
use crate::ui::layout::components::{
|
||||
bookmark_list::BookmarkList, input::Input, msgbox::MsgBox, radio_group::RadioGroup,
|
||||
table::Table, text::Text,
|
||||
use crate::ui::components::{
|
||||
bookmark_list::{BookmarkList, BookmarkListPropsBuilder},
|
||||
msgbox::{MsgBox, MsgBoxPropsBuilder},
|
||||
};
|
||||
use crate::ui::layout::props::{
|
||||
PropValue, PropsBuilder, TableBuilder, TextParts, TextSpan, TextSpanBuilder,
|
||||
};
|
||||
use crate::ui::layout::utils::draw_area_in;
|
||||
use crate::ui::layout::view::View;
|
||||
use crate::ui::layout::Payload;
|
||||
use crate::utils::ui::draw_area_in;
|
||||
// Ext
|
||||
use std::path::PathBuf;
|
||||
use tui::{
|
||||
use tuirealm::components::{
|
||||
input::{Input, InputPropsBuilder},
|
||||
radio::{Radio, RadioPropsBuilder},
|
||||
span::{Span, SpanPropsBuilder},
|
||||
table::{Table, TablePropsBuilder},
|
||||
};
|
||||
use tuirealm::tui::{
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::Color,
|
||||
widgets::{Borders, Clear},
|
||||
widgets::{BorderType, Borders, Clear},
|
||||
};
|
||||
use tuirealm::{
|
||||
props::{PropsBuilder, TableBuilder, TextSpan, TextSpanBuilder},
|
||||
Payload, View,
|
||||
};
|
||||
|
||||
impl SetupActivity {
|
||||
@@ -61,38 +66,32 @@ impl SetupActivity {
|
||||
// Radio tab
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_TAB,
|
||||
Box::new(RadioGroup::new(
|
||||
PropsBuilder::default()
|
||||
.with_foreground(Color::LightYellow)
|
||||
.with_background(Color::Black)
|
||||
.with_borders(Borders::BOTTOM)
|
||||
.with_texts(TextParts::new(
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.with_color(Color::LightYellow)
|
||||
.with_inverted_color(Color::Black)
|
||||
.with_borders(Borders::BOTTOM, BorderType::Thick, Color::White)
|
||||
.with_options(
|
||||
None,
|
||||
Some(vec![
|
||||
TextSpan::from("User Interface"),
|
||||
TextSpan::from("SSH Keys"),
|
||||
]),
|
||||
))
|
||||
.with_value(PropValue::Unsigned(0))
|
||||
vec![String::from("User Interface"), String::from("SSH Keys")],
|
||||
)
|
||||
.with_value(0)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
// Footer
|
||||
self.view.mount(
|
||||
super::COMPONENT_TEXT_FOOTER,
|
||||
Box::new(Text::new(
|
||||
PropsBuilder::default()
|
||||
.with_texts(TextParts::new(
|
||||
None,
|
||||
Some(vec![
|
||||
TextSpanBuilder::new("Press ").bold().build(),
|
||||
TextSpanBuilder::new("<CTRL+H>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.build(),
|
||||
TextSpanBuilder::new(" to show keybindings").bold().build(),
|
||||
]),
|
||||
))
|
||||
Box::new(Span::new(
|
||||
SpanPropsBuilder::default()
|
||||
.with_spans(vec![
|
||||
TextSpanBuilder::new("Press ").bold().build(),
|
||||
TextSpanBuilder::new("<CTRL+H>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.build(),
|
||||
TextSpanBuilder::new(" to show keybindings").bold().build(),
|
||||
])
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -100,83 +99,86 @@ impl SetupActivity {
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_TEXT_EDITOR,
|
||||
Box::new(Input::new(
|
||||
PropsBuilder::default()
|
||||
InputPropsBuilder::default()
|
||||
.with_foreground(Color::LightGreen)
|
||||
.with_texts(TextParts::new(Some(String::from("Text editor")), None))
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightGreen)
|
||||
.with_label(String::from("Text editor"))
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
self.view.active(super::COMPONENT_INPUT_TEXT_EDITOR); // <-- Focus
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_DEFAULT_PROTOCOL,
|
||||
Box::new(RadioGroup::new(
|
||||
PropsBuilder::default()
|
||||
.with_foreground(Color::LightCyan)
|
||||
.with_background(Color::Black)
|
||||
.with_texts(TextParts::new(
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.with_color(Color::LightCyan)
|
||||
.with_inverted_color(Color::Black)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightCyan)
|
||||
.with_options(
|
||||
Some(String::from("Default file transfer protocol")),
|
||||
Some(vec![
|
||||
TextSpan::from("SFTP"),
|
||||
TextSpan::from("SCP"),
|
||||
TextSpan::from("FTP"),
|
||||
TextSpan::from("FTPS"),
|
||||
]),
|
||||
))
|
||||
vec![
|
||||
String::from("SFTP"),
|
||||
String::from("SCP"),
|
||||
String::from("FTP"),
|
||||
String::from("FTPS"),
|
||||
],
|
||||
)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_HIDDEN_FILES,
|
||||
Box::new(RadioGroup::new(
|
||||
PropsBuilder::default()
|
||||
.with_foreground(Color::LightRed)
|
||||
.with_background(Color::Black)
|
||||
.with_texts(TextParts::new(
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.with_color(Color::LightRed)
|
||||
.with_inverted_color(Color::Black)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightRed)
|
||||
.with_options(
|
||||
Some(String::from("Show hidden files (by default)")),
|
||||
Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]),
|
||||
))
|
||||
vec![String::from("Yes"), String::from("No")],
|
||||
)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_UPDATES,
|
||||
Box::new(RadioGroup::new(
|
||||
PropsBuilder::default()
|
||||
.with_foreground(Color::LightYellow)
|
||||
.with_background(Color::Black)
|
||||
.with_texts(TextParts::new(
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.with_color(Color::LightYellow)
|
||||
.with_inverted_color(Color::Black)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightYellow)
|
||||
.with_options(
|
||||
Some(String::from("Check for updates?")),
|
||||
Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]),
|
||||
))
|
||||
vec![String::from("Yes"), String::from("No")],
|
||||
)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_GROUP_DIRS,
|
||||
Box::new(RadioGroup::new(
|
||||
PropsBuilder::default()
|
||||
.with_foreground(Color::LightMagenta)
|
||||
.with_background(Color::Black)
|
||||
.with_texts(TextParts::new(
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.with_color(Color::LightMagenta)
|
||||
.with_inverted_color(Color::Black)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightMagenta)
|
||||
.with_options(
|
||||
Some(String::from("Group directories")),
|
||||
Some(vec![
|
||||
TextSpan::from("Display first"),
|
||||
TextSpan::from("Display Last"),
|
||||
TextSpan::from("No"),
|
||||
]),
|
||||
))
|
||||
vec![
|
||||
String::from("Display first"),
|
||||
String::from("Display Last"),
|
||||
String::from("No"),
|
||||
],
|
||||
)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_FILE_FMT,
|
||||
Box::new(Input::new(
|
||||
PropsBuilder::default()
|
||||
InputPropsBuilder::default()
|
||||
.with_foreground(Color::LightBlue)
|
||||
.with_texts(TextParts::new(
|
||||
Some(String::from("File formatter syntax")),
|
||||
None,
|
||||
))
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightBlue)
|
||||
.with_label(String::from("File formatter syntax"))
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -196,46 +198,41 @@ impl SetupActivity {
|
||||
// Radio tab
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_TAB,
|
||||
Box::new(RadioGroup::new(
|
||||
PropsBuilder::default()
|
||||
.with_foreground(Color::LightYellow)
|
||||
.with_background(Color::Black)
|
||||
.with_borders(Borders::BOTTOM)
|
||||
.with_texts(TextParts::new(
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.with_color(Color::LightYellow)
|
||||
.with_inverted_color(Color::Black)
|
||||
.with_borders(Borders::BOTTOM, BorderType::Thick, Color::LightYellow)
|
||||
.with_options(
|
||||
None,
|
||||
Some(vec![
|
||||
TextSpan::from("User Interface"),
|
||||
TextSpan::from("SSH Keys"),
|
||||
]),
|
||||
))
|
||||
.with_value(PropValue::Unsigned(1))
|
||||
vec![String::from("User Interface"), String::from("SSH Keys")],
|
||||
)
|
||||
.with_value(1)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
// Footer
|
||||
self.view.mount(
|
||||
super::COMPONENT_TEXT_FOOTER,
|
||||
Box::new(Text::new(
|
||||
PropsBuilder::default()
|
||||
.with_texts(TextParts::new(
|
||||
None,
|
||||
Some(vec![
|
||||
TextSpanBuilder::new("Press ").bold().build(),
|
||||
TextSpanBuilder::new("<CTRL+H>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.build(),
|
||||
TextSpanBuilder::new(" to show keybindings").bold().build(),
|
||||
]),
|
||||
))
|
||||
Box::new(Span::new(
|
||||
SpanPropsBuilder::default()
|
||||
.with_spans(vec![
|
||||
TextSpanBuilder::new("Press ").bold().build(),
|
||||
TextSpanBuilder::new("<CTRL+H>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.build(),
|
||||
TextSpanBuilder::new(" to show keybindings").bold().build(),
|
||||
])
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
self.view.mount(
|
||||
super::COMPONENT_LIST_SSH_KEYS,
|
||||
Box::new(BookmarkList::new(
|
||||
PropsBuilder::default()
|
||||
.with_texts(TextParts::new(Some(String::from("SSH Keys")), Some(vec![])))
|
||||
BookmarkListPropsBuilder::default()
|
||||
.with_bookmarks(Some(String::from("SSH Keys")), vec![])
|
||||
.with_borders(Borders::ALL, BorderType::Plain, Color::LightGreen)
|
||||
.with_background(Color::LightGreen)
|
||||
.with_foreground(Color::Black)
|
||||
.build(),
|
||||
@@ -312,40 +309,40 @@ impl SetupActivity {
|
||||
}
|
||||
}
|
||||
// Popups
|
||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) {
|
||||
if props.build().visible {
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) {
|
||||
if props.visible {
|
||||
let popup = draw_area_in(f.size(), 50, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
// make popup
|
||||
self.view.render(super::COMPONENT_TEXT_ERROR, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) {
|
||||
if props.build().visible {
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) {
|
||||
if props.visible {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 40, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
self.view.render(super::COMPONENT_RADIO_QUIT, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_TEXT_HELP) {
|
||||
if props.build().visible {
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_HELP) {
|
||||
if props.visible {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 50, 70);
|
||||
f.render_widget(Clear, popup);
|
||||
self.view.render(super::COMPONENT_TEXT_HELP, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_RADIO_SAVE) {
|
||||
if props.build().visible {
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_SAVE) {
|
||||
if props.visible {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 30, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
self.view.render(super::COMPONENT_RADIO_SAVE, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_RADIO_DEL_SSH_KEY) {
|
||||
if props.build().visible {
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_DEL_SSH_KEY) {
|
||||
if props.visible {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 30, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
@@ -353,8 +350,8 @@ impl SetupActivity {
|
||||
.render(super::COMPONENT_RADIO_DEL_SSH_KEY, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_SSH_HOST) {
|
||||
if props.build().visible {
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_SSH_HOST) {
|
||||
if props.visible {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 50, 20);
|
||||
f.render_widget(Clear, popup);
|
||||
@@ -389,10 +386,11 @@ impl SetupActivity {
|
||||
self.view.mount(
|
||||
super::COMPONENT_TEXT_ERROR,
|
||||
Box::new(MsgBox::new(
|
||||
PropsBuilder::default()
|
||||
MsgBoxPropsBuilder::default()
|
||||
.with_foreground(Color::Red)
|
||||
.bold()
|
||||
.with_texts(TextParts::new(None, Some(vec![TextSpan::from(text)])))
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::Red)
|
||||
.with_texts(None, vec![TextSpan::from(text)])
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -413,15 +411,16 @@ impl SetupActivity {
|
||||
pub(super) fn mount_del_ssh_key(&mut self) {
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_DEL_SSH_KEY,
|
||||
Box::new(RadioGroup::new(
|
||||
PropsBuilder::default()
|
||||
.with_foreground(Color::LightRed)
|
||||
.bold()
|
||||
.with_texts(TextParts::new(
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.with_color(Color::LightRed)
|
||||
.with_inverted_color(Color::Black)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightRed)
|
||||
.with_options(
|
||||
Some(String::from("Delete key?")),
|
||||
Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]),
|
||||
))
|
||||
.with_value(PropValue::Unsigned(1)) // Default: No
|
||||
vec![String::from("Yes"), String::from("No")],
|
||||
)
|
||||
.with_value(1) // Default: No
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -443,21 +442,26 @@ impl SetupActivity {
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_SSH_HOST,
|
||||
Box::new(Input::new(
|
||||
PropsBuilder::default()
|
||||
.with_texts(TextParts::new(
|
||||
Some(String::from("Hostname or address")),
|
||||
None,
|
||||
))
|
||||
.with_borders(Borders::TOP | Borders::RIGHT | Borders::LEFT)
|
||||
InputPropsBuilder::default()
|
||||
.with_label(String::from("Hostname or address"))
|
||||
.with_borders(
|
||||
Borders::TOP | Borders::RIGHT | Borders::LEFT,
|
||||
BorderType::Plain,
|
||||
Color::Reset,
|
||||
)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_SSH_USERNAME,
|
||||
Box::new(Input::new(
|
||||
PropsBuilder::default()
|
||||
.with_texts(TextParts::new(Some(String::from("Username")), None))
|
||||
.with_borders(Borders::BOTTOM | Borders::RIGHT | Borders::LEFT)
|
||||
InputPropsBuilder::default()
|
||||
.with_label(String::from("Username"))
|
||||
.with_borders(
|
||||
Borders::ALL | Borders::RIGHT | Borders::LEFT,
|
||||
BorderType::Plain,
|
||||
Color::Reset,
|
||||
)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -478,18 +482,19 @@ impl SetupActivity {
|
||||
pub(super) fn mount_quit(&mut self) {
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_QUIT,
|
||||
Box::new(RadioGroup::new(
|
||||
PropsBuilder::default()
|
||||
.with_foreground(Color::LightRed)
|
||||
.bold()
|
||||
.with_texts(TextParts::new(
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.with_color(Color::LightRed)
|
||||
.with_inverted_color(Color::Black)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightRed)
|
||||
.with_options(
|
||||
Some(String::from("Exit setup?")),
|
||||
Some(vec![
|
||||
TextSpan::from("Save"),
|
||||
TextSpan::from("Don't save"),
|
||||
TextSpan::from("Cancel"),
|
||||
]),
|
||||
))
|
||||
vec![
|
||||
String::from("Save"),
|
||||
String::from("Don't save"),
|
||||
String::from("Cancel"),
|
||||
],
|
||||
)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -510,14 +515,15 @@ impl SetupActivity {
|
||||
pub(super) fn mount_save_popup(&mut self) {
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_SAVE,
|
||||
Box::new(RadioGroup::new(
|
||||
PropsBuilder::default()
|
||||
.with_foreground(Color::LightYellow)
|
||||
.bold()
|
||||
.with_texts(TextParts::new(
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.with_color(Color::LightYellow)
|
||||
.with_inverted_color(Color::Black)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightYellow)
|
||||
.with_options(
|
||||
Some(String::from("Save changes?")),
|
||||
Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]),
|
||||
))
|
||||
vec![String::from("Yes"), String::from("No")],
|
||||
)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -539,8 +545,9 @@ impl SetupActivity {
|
||||
self.view.mount(
|
||||
super::COMPONENT_TEXT_HELP,
|
||||
Box::new(Table::new(
|
||||
PropsBuilder::default()
|
||||
.with_texts(TextParts::table(
|
||||
TablePropsBuilder::default()
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
|
||||
.with_table(
|
||||
Some(String::from("Help")),
|
||||
TableBuilder::default()
|
||||
.add_col(
|
||||
@@ -615,7 +622,7 @@ impl SetupActivity {
|
||||
)
|
||||
.add_col(TextSpan::from(" Save configuration"))
|
||||
.build(),
|
||||
))
|
||||
)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -636,77 +643,59 @@ impl SetupActivity {
|
||||
pub(super) fn load_input_values(&mut self) {
|
||||
if let Some(cli) = self.context.as_mut().unwrap().config_client.as_mut() {
|
||||
// Text editor
|
||||
if let Some(props) = self
|
||||
.view
|
||||
.get_props(super::COMPONENT_INPUT_TEXT_EDITOR)
|
||||
.as_mut()
|
||||
{
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_TEXT_EDITOR) {
|
||||
let text_editor: String =
|
||||
String::from(cli.get_text_editor().as_path().to_string_lossy());
|
||||
let props = props.with_value(PropValue::Str(text_editor)).build();
|
||||
let props = InputPropsBuilder::from(props)
|
||||
.with_value(text_editor)
|
||||
.build();
|
||||
let _ = self.view.update(super::COMPONENT_INPUT_TEXT_EDITOR, props);
|
||||
}
|
||||
// Protocol
|
||||
if let Some(props) = self
|
||||
.view
|
||||
.get_props(super::COMPONENT_RADIO_DEFAULT_PROTOCOL)
|
||||
.as_mut()
|
||||
{
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_DEFAULT_PROTOCOL) {
|
||||
let protocol: usize = match cli.get_default_protocol() {
|
||||
FileTransferProtocol::Sftp => 0,
|
||||
FileTransferProtocol::Scp => 1,
|
||||
FileTransferProtocol::Ftp(false) => 2,
|
||||
FileTransferProtocol::Ftp(true) => 3,
|
||||
};
|
||||
let props = props.with_value(PropValue::Unsigned(protocol)).build();
|
||||
let props = RadioPropsBuilder::from(props).with_value(protocol).build();
|
||||
let _ = self
|
||||
.view
|
||||
.update(super::COMPONENT_RADIO_DEFAULT_PROTOCOL, props);
|
||||
}
|
||||
// Hidden files
|
||||
if let Some(props) = self
|
||||
.view
|
||||
.get_props(super::COMPONENT_RADIO_HIDDEN_FILES)
|
||||
.as_mut()
|
||||
{
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_HIDDEN_FILES) {
|
||||
let hidden: usize = match cli.get_show_hidden_files() {
|
||||
true => 0,
|
||||
false => 1,
|
||||
};
|
||||
let props = props.with_value(PropValue::Unsigned(hidden)).build();
|
||||
let props = RadioPropsBuilder::from(props).with_value(hidden).build();
|
||||
let _ = self.view.update(super::COMPONENT_RADIO_HIDDEN_FILES, props);
|
||||
}
|
||||
// Updates
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_UPDATES).as_mut() {
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_UPDATES) {
|
||||
let updates: usize = match cli.get_check_for_updates() {
|
||||
true => 0,
|
||||
false => 1,
|
||||
};
|
||||
let props = props.with_value(PropValue::Unsigned(updates)).build();
|
||||
let props = RadioPropsBuilder::from(props).with_value(updates).build();
|
||||
let _ = self.view.update(super::COMPONENT_RADIO_UPDATES, props);
|
||||
}
|
||||
// Group dirs
|
||||
if let Some(props) = self
|
||||
.view
|
||||
.get_props(super::COMPONENT_RADIO_GROUP_DIRS)
|
||||
.as_mut()
|
||||
{
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_GROUP_DIRS) {
|
||||
let dirs: usize = match cli.get_group_dirs() {
|
||||
Some(GroupDirs::First) => 0,
|
||||
Some(GroupDirs::Last) => 1,
|
||||
None => 2,
|
||||
};
|
||||
let props = props.with_value(PropValue::Unsigned(dirs)).build();
|
||||
let props = RadioPropsBuilder::from(props).with_value(dirs).build();
|
||||
let _ = self.view.update(super::COMPONENT_RADIO_GROUP_DIRS, props);
|
||||
}
|
||||
// File Fmt
|
||||
if let Some(props) = self
|
||||
.view
|
||||
.get_props(super::COMPONENT_INPUT_FILE_FMT)
|
||||
.as_mut()
|
||||
{
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_FILE_FMT) {
|
||||
let file_fmt: String = cli.get_file_fmt().unwrap_or_default();
|
||||
let props = props.with_value(PropValue::Str(file_fmt)).build();
|
||||
let props = InputPropsBuilder::from(props).with_value(file_fmt).build();
|
||||
let _ = self.view.update(super::COMPONENT_INPUT_FILE_FMT, props);
|
||||
}
|
||||
}
|
||||
@@ -718,12 +707,12 @@ impl SetupActivity {
|
||||
pub(super) fn collect_input_values(&mut self) {
|
||||
if let Some(cli) = self.context.as_mut().unwrap().config_client.as_mut() {
|
||||
if let Some(Payload::Text(editor)) =
|
||||
self.view.get_value(super::COMPONENT_INPUT_TEXT_EDITOR)
|
||||
self.view.get_state(super::COMPONENT_INPUT_TEXT_EDITOR)
|
||||
{
|
||||
cli.set_text_editor(PathBuf::from(editor.as_str()));
|
||||
}
|
||||
if let Some(Payload::Unsigned(protocol)) =
|
||||
self.view.get_value(super::COMPONENT_RADIO_DEFAULT_PROTOCOL)
|
||||
self.view.get_state(super::COMPONENT_RADIO_DEFAULT_PROTOCOL)
|
||||
{
|
||||
let protocol: FileTransferProtocol = match protocol {
|
||||
1 => FileTransferProtocol::Scp,
|
||||
@@ -734,22 +723,22 @@ impl SetupActivity {
|
||||
cli.set_default_protocol(protocol);
|
||||
}
|
||||
if let Some(Payload::Unsigned(opt)) =
|
||||
self.view.get_value(super::COMPONENT_RADIO_HIDDEN_FILES)
|
||||
self.view.get_state(super::COMPONENT_RADIO_HIDDEN_FILES)
|
||||
{
|
||||
let show: bool = matches!(opt, 0);
|
||||
cli.set_show_hidden_files(show);
|
||||
}
|
||||
if let Some(Payload::Unsigned(opt)) =
|
||||
self.view.get_value(super::COMPONENT_RADIO_UPDATES)
|
||||
self.view.get_state(super::COMPONENT_RADIO_UPDATES)
|
||||
{
|
||||
let check: bool = matches!(opt, 0);
|
||||
cli.set_check_for_updates(check);
|
||||
}
|
||||
if let Some(Payload::Text(fmt)) = self.view.get_value(super::COMPONENT_INPUT_FILE_FMT) {
|
||||
if let Some(Payload::Text(fmt)) = self.view.get_state(super::COMPONENT_INPUT_FILE_FMT) {
|
||||
cli.set_file_fmt(fmt);
|
||||
}
|
||||
if let Some(Payload::Unsigned(opt)) =
|
||||
self.view.get_value(super::COMPONENT_RADIO_GROUP_DIRS)
|
||||
self.view.get_state(super::COMPONENT_RADIO_GROUP_DIRS)
|
||||
{
|
||||
let dirs: Option<GroupDirs> = match opt {
|
||||
0 => Some(GroupDirs::First),
|
||||
@@ -767,17 +756,17 @@ impl SetupActivity {
|
||||
pub(super) fn reload_ssh_keys(&mut self) {
|
||||
if let Some(cli) = self.context.as_ref().unwrap().config_client.as_ref() {
|
||||
// get props
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_LIST_SSH_KEYS).as_mut() {
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_LIST_SSH_KEYS) {
|
||||
// Create texts
|
||||
let keys: Vec<TextSpan> = cli
|
||||
let keys: Vec<String> = cli
|
||||
.iter_ssh_keys()
|
||||
.map(|x| {
|
||||
let (addr, username, _) = cli.get_ssh_key(x).ok().unwrap().unwrap();
|
||||
TextSpan::from(format!("{} at {}", addr, username).as_str())
|
||||
format!("{} at {}", addr, username)
|
||||
})
|
||||
.collect();
|
||||
let props = props
|
||||
.with_texts(TextParts::new(Some(String::from("SSH Keys")), Some(keys)))
|
||||
let props = BookmarkListPropsBuilder::from(props)
|
||||
.with_bookmarks(Some(String::from("SSH Keys")), keys)
|
||||
.build();
|
||||
self.view.update(super::COMPONENT_LIST_SSH_KEYS, props);
|
||||
}
|
||||
|
||||
@@ -25,16 +25,106 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// locals
|
||||
use super::{Canvas, Component, InputEvent, Msg, Payload, Props, PropsBuilder};
|
||||
// ext
|
||||
use crossterm::event::KeyCode;
|
||||
use tui::{
|
||||
use tuirealm::components::utils::get_block;
|
||||
use tuirealm::event::{Event, KeyCode};
|
||||
use tuirealm::props::{BordersProps, Props, PropsBuilder, TextParts, TextSpan};
|
||||
use tuirealm::tui::{
|
||||
layout::{Corner, Rect},
|
||||
style::{Color, Style},
|
||||
text::Span,
|
||||
widgets::{Block, List, ListItem, ListState},
|
||||
widgets::{BorderType, Borders, List, ListItem, ListState},
|
||||
};
|
||||
use tuirealm::{Canvas, Component, Msg, Payload};
|
||||
|
||||
// -- props
|
||||
|
||||
pub struct BookmarkListPropsBuilder {
|
||||
props: Option<Props>,
|
||||
}
|
||||
|
||||
impl Default for BookmarkListPropsBuilder {
|
||||
fn default() -> Self {
|
||||
BookmarkListPropsBuilder {
|
||||
props: Some(Props::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PropsBuilder for BookmarkListPropsBuilder {
|
||||
fn build(&mut self) -> Props {
|
||||
self.props.take().unwrap()
|
||||
}
|
||||
|
||||
fn hidden(&mut self) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.visible = false;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn visible(&mut self) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.visible = true;
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Props> for BookmarkListPropsBuilder {
|
||||
fn from(props: Props) -> Self {
|
||||
BookmarkListPropsBuilder { props: Some(props) }
|
||||
}
|
||||
}
|
||||
|
||||
impl BookmarkListPropsBuilder {
|
||||
/// ### with_foreground
|
||||
///
|
||||
/// Set foreground color for area
|
||||
pub fn with_foreground(&mut self, color: Color) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.foreground = color;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// ### with_background
|
||||
///
|
||||
/// Set background color for area
|
||||
pub fn with_background(&mut self, color: Color) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.background = color;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// ### with_borders
|
||||
///
|
||||
/// Set component borders style
|
||||
pub fn with_borders(
|
||||
&mut self,
|
||||
borders: Borders,
|
||||
variant: BorderType,
|
||||
color: Color,
|
||||
) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.borders = BordersProps {
|
||||
borders,
|
||||
variant,
|
||||
color,
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_bookmarks(&mut self, title: Option<String>, bookmarks: Vec<String>) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
let bookmarks: Vec<TextSpan> = bookmarks.into_iter().map(TextSpan::from).collect();
|
||||
props.texts = TextParts::new(title, Some(bookmarks));
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// -- states
|
||||
|
||||
@@ -120,7 +210,7 @@ impl BookmarkList {
|
||||
// Initialize states
|
||||
let mut states: OwnStates = OwnStates::default();
|
||||
// Set list length
|
||||
states.set_list_len(match &props.texts.rows {
|
||||
states.set_list_len(match &props.texts.spans {
|
||||
Some(tokens) => tokens.len(),
|
||||
None => 0,
|
||||
});
|
||||
@@ -129,15 +219,11 @@ impl BookmarkList {
|
||||
}
|
||||
|
||||
impl Component for BookmarkList {
|
||||
/// ### render
|
||||
///
|
||||
/// Based on the current properties and states, renders a widget using the provided render engine in the provided Area
|
||||
/// If focused, cursor is also set (if supported by widget)
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
fn render(&self, render: &mut Canvas, area: Rect) {
|
||||
if self.props.visible {
|
||||
// Make list
|
||||
let list_item: Vec<ListItem> = match self.props.texts.rows.as_ref() {
|
||||
let list_item: Vec<ListItem> = match self.props.texts.spans.as_ref() {
|
||||
None => vec![],
|
||||
Some(lines) => lines
|
||||
.iter()
|
||||
@@ -148,30 +234,22 @@ impl Component for BookmarkList {
|
||||
true => (self.props.foreground, self.props.background),
|
||||
false => (Color::Reset, Color::Reset),
|
||||
};
|
||||
let title: String = match self.props.texts.title.as_ref() {
|
||||
Some(t) => t.clone(),
|
||||
None => String::new(),
|
||||
};
|
||||
// Render
|
||||
let mut state: ListState = ListState::default();
|
||||
state.select(Some(self.states.list_index));
|
||||
render.render_stateful_widget(
|
||||
List::new(list_item)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(self.props.borders)
|
||||
.border_style(match self.states.focus {
|
||||
true => Style::default().fg(self.props.background),
|
||||
false => Style::default(),
|
||||
})
|
||||
.title(title),
|
||||
)
|
||||
.block(get_block(
|
||||
&self.props.borders,
|
||||
&self.props.texts.title,
|
||||
self.states.focus,
|
||||
))
|
||||
.start_corner(Corner::TopLeft)
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.bg(bg)
|
||||
.fg(fg)
|
||||
.add_modifier(self.props.get_modifiers()),
|
||||
.add_modifier(self.props.modifiers),
|
||||
),
|
||||
area,
|
||||
&mut state,
|
||||
@@ -179,15 +257,10 @@ impl Component for BookmarkList {
|
||||
}
|
||||
}
|
||||
|
||||
/// ### update
|
||||
///
|
||||
/// Update component properties
|
||||
/// Properties should first be retrieved through `get_props` which creates a builder from
|
||||
/// existing properties and then edited before calling update
|
||||
fn update(&mut self, props: Props) -> Msg {
|
||||
self.props = props;
|
||||
// re-Set list length
|
||||
self.states.set_list_len(match &self.props.texts.rows {
|
||||
self.states.set_list_len(match &self.props.texts.spans {
|
||||
Some(tokens) => tokens.len(),
|
||||
None => 0,
|
||||
});
|
||||
@@ -196,21 +269,13 @@ impl Component for BookmarkList {
|
||||
Msg::None
|
||||
}
|
||||
|
||||
/// ### get_props
|
||||
///
|
||||
/// Returns a props builder starting from component properties.
|
||||
/// This returns a prop builder in order to make easier to create
|
||||
/// new properties for the element.
|
||||
fn get_props(&self) -> PropsBuilder {
|
||||
PropsBuilder::from(self.props.clone())
|
||||
fn get_props(&self) -> Props {
|
||||
self.props.clone()
|
||||
}
|
||||
|
||||
/// ### on
|
||||
///
|
||||
/// Handle input event and update internal states
|
||||
fn on(&mut self, ev: InputEvent) -> Msg {
|
||||
fn on(&mut self, ev: Event) -> Msg {
|
||||
// Match event
|
||||
if let InputEvent::Key(key) = ev {
|
||||
if let Event::Key(key) = ev {
|
||||
match key.code {
|
||||
KeyCode::Down => {
|
||||
// Update states
|
||||
@@ -238,7 +303,7 @@ impl Component for BookmarkList {
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
// Report event
|
||||
Msg::OnSubmit(self.get_value())
|
||||
Msg::OnSubmit(self.get_state())
|
||||
}
|
||||
_ => {
|
||||
// Return key event to activity
|
||||
@@ -251,25 +316,14 @@ impl Component for BookmarkList {
|
||||
}
|
||||
}
|
||||
|
||||
/// ### get_value
|
||||
///
|
||||
/// Return component value. File list return index
|
||||
fn get_value(&self) -> Payload {
|
||||
fn get_state(&self) -> Payload {
|
||||
Payload::Unsigned(self.states.get_list_index())
|
||||
}
|
||||
|
||||
// -- events
|
||||
|
||||
/// ### blur
|
||||
///
|
||||
/// Blur component; basically remove focus
|
||||
fn blur(&mut self) {
|
||||
self.states.focus = false;
|
||||
}
|
||||
|
||||
/// ### active
|
||||
///
|
||||
/// Active component; basically give focus
|
||||
fn active(&mut self) {
|
||||
self.states.focus = true;
|
||||
}
|
||||
@@ -279,21 +333,32 @@ impl Component for BookmarkList {
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::ui::layout::props::{TextParts, TextSpan};
|
||||
|
||||
use crossterm::event::KeyEvent;
|
||||
use tuirealm::event::KeyEvent;
|
||||
|
||||
#[test]
|
||||
fn test_ui_layout_components_bookmarks_list() {
|
||||
fn test_ui_components_bookmarks_list() {
|
||||
// Make component
|
||||
let mut component: BookmarkList = BookmarkList::new(
|
||||
PropsBuilder::default()
|
||||
.with_texts(TextParts::new(
|
||||
BookmarkListPropsBuilder::default()
|
||||
.hidden()
|
||||
.visible()
|
||||
.with_foreground(Color::Red)
|
||||
.with_background(Color::Blue)
|
||||
.with_borders(Borders::ALL, BorderType::Double, Color::Red)
|
||||
.with_bookmarks(
|
||||
Some(String::from("filelist")),
|
||||
Some(vec![TextSpan::from("file1"), TextSpan::from("file2")]),
|
||||
))
|
||||
vec![String::from("file1"), String::from("file2")],
|
||||
)
|
||||
.build(),
|
||||
);
|
||||
assert_eq!(component.props.foreground, Color::Red);
|
||||
assert_eq!(component.props.background, Color::Blue);
|
||||
assert_eq!(component.props.visible, true);
|
||||
assert_eq!(
|
||||
component.props.texts.title.as_ref().unwrap().as_str(),
|
||||
"filelist"
|
||||
);
|
||||
assert_eq!(component.props.texts.spans.as_ref().unwrap().len(), 2);
|
||||
// Verify states
|
||||
assert_eq!(component.states.list_index, 0);
|
||||
assert_eq!(component.states.list_len, 2);
|
||||
@@ -304,69 +369,72 @@ mod tests {
|
||||
component.blur();
|
||||
assert_eq!(component.states.focus, false);
|
||||
// Update
|
||||
let props = component.get_props().with_foreground(Color::Red).build();
|
||||
let props = BookmarkListPropsBuilder::from(component.get_props())
|
||||
.with_foreground(Color::Yellow)
|
||||
.hidden()
|
||||
.build();
|
||||
assert_eq!(component.update(props), Msg::None);
|
||||
assert_eq!(component.props.foreground, Color::Red);
|
||||
assert_eq!(component.props.foreground, Color::Yellow);
|
||||
assert_eq!(component.props.visible, false);
|
||||
// Increment list index
|
||||
component.states.list_index += 1;
|
||||
assert_eq!(component.states.list_index, 1);
|
||||
// Update
|
||||
component.update(
|
||||
component
|
||||
.get_props()
|
||||
.with_texts(TextParts::new(
|
||||
BookmarkListPropsBuilder::from(component.get_props())
|
||||
.with_bookmarks(
|
||||
Some(String::from("filelist")),
|
||||
Some(vec![
|
||||
TextSpan::from("file1"),
|
||||
TextSpan::from("file2"),
|
||||
TextSpan::from("file3"),
|
||||
]),
|
||||
))
|
||||
vec![
|
||||
String::from("file1"),
|
||||
String::from("file2"),
|
||||
String::from("file3"),
|
||||
],
|
||||
)
|
||||
.build(),
|
||||
);
|
||||
// Verify states
|
||||
assert_eq!(component.states.list_index, 0);
|
||||
assert_eq!(component.states.list_len, 3);
|
||||
// get value
|
||||
assert_eq!(component.get_value(), Payload::Unsigned(0));
|
||||
assert_eq!(component.get_state(), Payload::Unsigned(0));
|
||||
// Render
|
||||
assert_eq!(component.states.list_index, 0);
|
||||
// Handle inputs
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Down))),
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::Down))),
|
||||
Msg::None
|
||||
);
|
||||
// Index should be incremented
|
||||
assert_eq!(component.states.list_index, 1);
|
||||
// Index should be decremented
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Up))),
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::Up))),
|
||||
Msg::None
|
||||
);
|
||||
// Index should be incremented
|
||||
assert_eq!(component.states.list_index, 0);
|
||||
// Index should be 2
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::PageDown))),
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::PageDown))),
|
||||
Msg::None
|
||||
);
|
||||
// Index should be incremented
|
||||
assert_eq!(component.states.list_index, 2);
|
||||
// Index should be 0
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::PageUp))),
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::PageUp))),
|
||||
Msg::None
|
||||
);
|
||||
// Index should be incremented
|
||||
assert_eq!(component.states.list_index, 0);
|
||||
// Enter
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Enter))),
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::Enter))),
|
||||
Msg::OnSubmit(Payload::Unsigned(0))
|
||||
);
|
||||
// On key
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Backspace))),
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::Backspace))),
|
||||
Msg::OnKey(KeyEvent::from(KeyCode::Backspace))
|
||||
);
|
||||
}
|
||||
@@ -25,16 +25,106 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// locals
|
||||
use super::{Canvas, Component, InputEvent, Msg, Payload, Props, PropsBuilder};
|
||||
// ext
|
||||
use crossterm::event::KeyCode;
|
||||
use tui::{
|
||||
use tuirealm::components::utils::get_block;
|
||||
use tuirealm::event::{Event, KeyCode};
|
||||
use tuirealm::props::{BordersProps, Props, PropsBuilder, TextParts, TextSpan};
|
||||
use tuirealm::tui::{
|
||||
layout::{Corner, Rect},
|
||||
style::{Color, Style},
|
||||
text::Span,
|
||||
widgets::{Block, List, ListItem, ListState},
|
||||
widgets::{BorderType, Borders, List, ListItem, ListState},
|
||||
};
|
||||
use tuirealm::{Canvas, Component, Msg, Payload};
|
||||
|
||||
// -- props
|
||||
|
||||
pub struct FileListPropsBuilder {
|
||||
props: Option<Props>,
|
||||
}
|
||||
|
||||
impl Default for FileListPropsBuilder {
|
||||
fn default() -> Self {
|
||||
FileListPropsBuilder {
|
||||
props: Some(Props::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PropsBuilder for FileListPropsBuilder {
|
||||
fn build(&mut self) -> Props {
|
||||
self.props.take().unwrap()
|
||||
}
|
||||
|
||||
fn hidden(&mut self) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.visible = false;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn visible(&mut self) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.visible = true;
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Props> for FileListPropsBuilder {
|
||||
fn from(props: Props) -> Self {
|
||||
FileListPropsBuilder { props: Some(props) }
|
||||
}
|
||||
}
|
||||
|
||||
impl FileListPropsBuilder {
|
||||
/// ### with_foreground
|
||||
///
|
||||
/// Set foreground color for area
|
||||
pub fn with_foreground(&mut self, color: Color) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.foreground = color;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// ### with_background
|
||||
///
|
||||
/// Set background color for area
|
||||
pub fn with_background(&mut self, color: Color) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.background = color;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// ### with_borders
|
||||
///
|
||||
/// Set component borders style
|
||||
pub fn with_borders(
|
||||
&mut self,
|
||||
borders: Borders,
|
||||
variant: BorderType,
|
||||
color: Color,
|
||||
) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.borders = BordersProps {
|
||||
borders,
|
||||
variant,
|
||||
color,
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_files(&mut self, title: Option<String>, files: Vec<String>) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
let files: Vec<TextSpan> = files.into_iter().map(TextSpan::from).collect();
|
||||
props.texts = TextParts::new(title, Some(files));
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// -- states
|
||||
|
||||
@@ -124,7 +214,7 @@ impl FileList {
|
||||
// Initialize states
|
||||
let mut states: OwnStates = OwnStates::default();
|
||||
// Set list length
|
||||
states.set_list_len(match &props.texts.rows {
|
||||
states.set_list_len(match &props.texts.spans {
|
||||
Some(tokens) => tokens.len(),
|
||||
None => 0,
|
||||
});
|
||||
@@ -133,15 +223,11 @@ impl FileList {
|
||||
}
|
||||
|
||||
impl Component for FileList {
|
||||
/// ### render
|
||||
///
|
||||
/// Based on the current properties and states, renders a widget using the provided render engine in the provided Area
|
||||
/// If focused, cursor is also set (if supported by widget)
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
fn render(&self, render: &mut Canvas, area: Rect) {
|
||||
if self.props.visible {
|
||||
// Make list
|
||||
let list_item: Vec<ListItem> = match self.props.texts.rows.as_ref() {
|
||||
let list_item: Vec<ListItem> = match self.props.texts.spans.as_ref() {
|
||||
None => vec![],
|
||||
Some(lines) => lines
|
||||
.iter()
|
||||
@@ -152,30 +238,22 @@ impl Component for FileList {
|
||||
true => (Color::Black, self.props.background),
|
||||
false => (self.props.foreground, Color::Reset),
|
||||
};
|
||||
let title: String = match self.props.texts.title.as_ref() {
|
||||
Some(t) => t.clone(),
|
||||
None => String::new(),
|
||||
};
|
||||
// Render
|
||||
let mut state: ListState = ListState::default();
|
||||
state.select(Some(self.states.list_index));
|
||||
render.render_stateful_widget(
|
||||
List::new(list_item)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(self.props.borders)
|
||||
.border_style(match self.states.focus {
|
||||
true => Style::default().fg(self.props.foreground),
|
||||
false => Style::default(),
|
||||
})
|
||||
.title(title),
|
||||
)
|
||||
.block(get_block(
|
||||
&self.props.borders,
|
||||
&self.props.texts.title,
|
||||
self.states.focus,
|
||||
))
|
||||
.start_corner(Corner::TopLeft)
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.bg(bg)
|
||||
.fg(fg)
|
||||
.add_modifier(self.props.get_modifiers()),
|
||||
.add_modifier(self.props.modifiers),
|
||||
),
|
||||
area,
|
||||
&mut state,
|
||||
@@ -183,15 +261,10 @@ impl Component for FileList {
|
||||
}
|
||||
}
|
||||
|
||||
/// ### update
|
||||
///
|
||||
/// Update component properties
|
||||
/// Properties should first be retrieved through `get_props` which creates a builder from
|
||||
/// existing properties and then edited before calling update
|
||||
fn update(&mut self, props: Props) -> Msg {
|
||||
self.props = props;
|
||||
// re-Set list length
|
||||
self.states.set_list_len(match &self.props.texts.rows {
|
||||
self.states.set_list_len(match &self.props.texts.spans {
|
||||
Some(tokens) => tokens.len(),
|
||||
None => 0,
|
||||
});
|
||||
@@ -200,21 +273,13 @@ impl Component for FileList {
|
||||
Msg::None
|
||||
}
|
||||
|
||||
/// ### get_props
|
||||
///
|
||||
/// Returns a props builder starting from component properties.
|
||||
/// This returns a prop builder in order to make easier to create
|
||||
/// new properties for the element.
|
||||
fn get_props(&self) -> PropsBuilder {
|
||||
PropsBuilder::from(self.props.clone())
|
||||
fn get_props(&self) -> Props {
|
||||
self.props.clone()
|
||||
}
|
||||
|
||||
/// ### on
|
||||
///
|
||||
/// Handle input event and update internal states
|
||||
fn on(&mut self, ev: InputEvent) -> Msg {
|
||||
fn on(&mut self, ev: Event) -> Msg {
|
||||
// Match event
|
||||
if let InputEvent::Key(key) = ev {
|
||||
if let Event::Key(key) = ev {
|
||||
match key.code {
|
||||
KeyCode::Down => {
|
||||
// Update states
|
||||
@@ -242,7 +307,7 @@ impl Component for FileList {
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
// Report event
|
||||
Msg::OnSubmit(self.get_value())
|
||||
Msg::OnSubmit(self.get_state())
|
||||
}
|
||||
_ => {
|
||||
// Return key event to activity
|
||||
@@ -255,10 +320,7 @@ impl Component for FileList {
|
||||
}
|
||||
}
|
||||
|
||||
/// ### get_value
|
||||
///
|
||||
/// Return component value. File list return index
|
||||
fn get_value(&self) -> Payload {
|
||||
fn get_state(&self) -> Payload {
|
||||
Payload::Unsigned(self.states.get_list_index())
|
||||
}
|
||||
|
||||
@@ -283,21 +345,32 @@ impl Component for FileList {
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::ui::layout::props::{TextParts, TextSpan};
|
||||
|
||||
use crossterm::event::KeyEvent;
|
||||
use tuirealm::event::KeyEvent;
|
||||
|
||||
#[test]
|
||||
fn test_ui_layout_components_file_list() {
|
||||
fn test_ui_components_file_list() {
|
||||
// Make component
|
||||
let mut component: FileList = FileList::new(
|
||||
PropsBuilder::default()
|
||||
.with_texts(TextParts::new(
|
||||
Some(String::from("filelist")),
|
||||
Some(vec![TextSpan::from("file1"), TextSpan::from("file2")]),
|
||||
))
|
||||
FileListPropsBuilder::default()
|
||||
.hidden()
|
||||
.visible()
|
||||
.with_foreground(Color::Red)
|
||||
.with_background(Color::Blue)
|
||||
.with_borders(Borders::ALL, BorderType::Double, Color::Red)
|
||||
.with_files(
|
||||
Some(String::from("files")),
|
||||
vec![String::from("file1"), String::from("file2")],
|
||||
)
|
||||
.build(),
|
||||
);
|
||||
assert_eq!(component.props.foreground, Color::Red);
|
||||
assert_eq!(component.props.background, Color::Blue);
|
||||
assert_eq!(component.props.visible, true);
|
||||
assert_eq!(
|
||||
component.props.texts.title.as_ref().unwrap().as_str(),
|
||||
"files"
|
||||
);
|
||||
assert_eq!(component.props.texts.spans.as_ref().unwrap().len(), 2);
|
||||
// Verify states
|
||||
assert_eq!(component.states.list_index, 0);
|
||||
assert_eq!(component.states.list_len, 2);
|
||||
@@ -308,69 +381,72 @@ mod tests {
|
||||
component.blur();
|
||||
assert_eq!(component.states.focus, false);
|
||||
// Update
|
||||
let props = component.get_props().with_foreground(Color::Red).build();
|
||||
let props = FileListPropsBuilder::from(component.get_props())
|
||||
.with_foreground(Color::Yellow)
|
||||
.hidden()
|
||||
.build();
|
||||
assert_eq!(component.update(props), Msg::None);
|
||||
assert_eq!(component.props.foreground, Color::Red);
|
||||
assert_eq!(component.props.visible, false);
|
||||
assert_eq!(component.props.foreground, Color::Yellow);
|
||||
// Increment list index
|
||||
component.states.list_index += 1;
|
||||
assert_eq!(component.states.list_index, 1);
|
||||
// Update
|
||||
component.update(
|
||||
component
|
||||
.get_props()
|
||||
.with_texts(TextParts::new(
|
||||
FileListPropsBuilder::from(component.get_props())
|
||||
.with_files(
|
||||
Some(String::from("filelist")),
|
||||
Some(vec![
|
||||
TextSpan::from("file1"),
|
||||
TextSpan::from("file2"),
|
||||
TextSpan::from("file3"),
|
||||
]),
|
||||
))
|
||||
vec![
|
||||
String::from("file1"),
|
||||
String::from("file2"),
|
||||
String::from("file3"),
|
||||
],
|
||||
)
|
||||
.build(),
|
||||
);
|
||||
// Verify states
|
||||
assert_eq!(component.states.list_index, 1); // Kept
|
||||
assert_eq!(component.states.list_len, 3);
|
||||
// get value
|
||||
assert_eq!(component.get_value(), Payload::Unsigned(1));
|
||||
assert_eq!(component.get_state(), Payload::Unsigned(1));
|
||||
// Render
|
||||
assert_eq!(component.states.list_index, 1);
|
||||
// Handle inputs
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Down))),
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::Down))),
|
||||
Msg::None
|
||||
);
|
||||
// Index should be incremented
|
||||
assert_eq!(component.states.list_index, 2);
|
||||
// Index should be decremented
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Up))),
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::Up))),
|
||||
Msg::None
|
||||
);
|
||||
// Index should be incremented
|
||||
assert_eq!(component.states.list_index, 1);
|
||||
// Index should be 2
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::PageDown))),
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::PageDown))),
|
||||
Msg::None
|
||||
);
|
||||
// Index should be incremented
|
||||
assert_eq!(component.states.list_index, 2);
|
||||
// Index should be 0
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::PageUp))),
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::PageUp))),
|
||||
Msg::None
|
||||
);
|
||||
// Index should be incremented
|
||||
assert_eq!(component.states.list_index, 0);
|
||||
// Enter
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Enter))),
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::Enter))),
|
||||
Msg::OnSubmit(Payload::Unsigned(0))
|
||||
);
|
||||
// On key
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Backspace))),
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::Backspace))),
|
||||
Msg::OnKey(KeyEvent::from(KeyCode::Backspace))
|
||||
);
|
||||
}
|
||||
@@ -25,17 +25,84 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// locals
|
||||
use super::{Canvas, Component, InputEvent, Msg, Payload, Props, PropsBuilder};
|
||||
// ext
|
||||
use crossterm::event::KeyCode;
|
||||
use std::collections::VecDeque;
|
||||
use tui::{
|
||||
use tuirealm::components::utils::{get_block, wrap_spans};
|
||||
use tuirealm::event::{Event, KeyCode};
|
||||
use tuirealm::props::{BordersProps, Props, PropsBuilder, Table as TextTable, TextParts};
|
||||
use tuirealm::tui::{
|
||||
layout::{Corner, Rect},
|
||||
style::Style,
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, List, ListItem, ListState},
|
||||
style::{Color, Style},
|
||||
widgets::{BorderType, Borders, List, ListItem, ListState},
|
||||
};
|
||||
use tuirealm::{Canvas, Component, Msg, Payload};
|
||||
|
||||
// -- props
|
||||
|
||||
pub struct LogboxPropsBuilder {
|
||||
props: Option<Props>,
|
||||
}
|
||||
|
||||
impl Default for LogboxPropsBuilder {
|
||||
fn default() -> Self {
|
||||
LogboxPropsBuilder {
|
||||
props: Some(Props::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PropsBuilder for LogboxPropsBuilder {
|
||||
fn build(&mut self) -> Props {
|
||||
self.props.take().unwrap()
|
||||
}
|
||||
|
||||
fn hidden(&mut self) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.visible = false;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn visible(&mut self) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.visible = true;
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Props> for LogboxPropsBuilder {
|
||||
fn from(props: Props) -> Self {
|
||||
LogboxPropsBuilder { props: Some(props) }
|
||||
}
|
||||
}
|
||||
|
||||
impl LogboxPropsBuilder {
|
||||
/// ### with_borders
|
||||
///
|
||||
/// Set component borders style
|
||||
pub fn with_borders(
|
||||
&mut self,
|
||||
borders: Borders,
|
||||
variant: BorderType,
|
||||
color: Color,
|
||||
) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.borders = BordersProps {
|
||||
borders,
|
||||
variant,
|
||||
color,
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_log(&mut self, title: Option<String>, table: TextTable) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.texts = TextParts::table(title, table);
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// -- states
|
||||
|
||||
@@ -132,10 +199,6 @@ impl LogBox {
|
||||
}
|
||||
|
||||
impl Component for LogBox {
|
||||
/// ### render
|
||||
///
|
||||
/// Based on the current properties and states, renders a widget using the provided render engine in the provided Area
|
||||
/// If focused, cursor is also set (if supported by widget)
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
fn render(&self, render: &mut Canvas, area: Rect) {
|
||||
if self.props.visible {
|
||||
@@ -144,70 +207,24 @@ impl Component for LogBox {
|
||||
None => Vec::new(),
|
||||
Some(table) => table
|
||||
.iter()
|
||||
.map(|row| {
|
||||
let mut columns: VecDeque<Span> = row
|
||||
.iter()
|
||||
.map(|col| {
|
||||
Span::styled(
|
||||
col.content.clone(),
|
||||
Style::default()
|
||||
.add_modifier(col.get_modifiers())
|
||||
.fg(col.fg)
|
||||
.bg(col.bg),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
// Let's convert column spans into Spans rows NOTE: -4 because first line is always made by 5 columns; but there's always 1
|
||||
let mut rows: Vec<Spans> = Vec::with_capacity(columns.len() - 4);
|
||||
// Get first row
|
||||
let mut first_row: Vec<Span> = Vec::with_capacity(5);
|
||||
for _ in 0..5 {
|
||||
if let Some(col) = columns.pop_front() {
|
||||
first_row.push(col);
|
||||
}
|
||||
}
|
||||
rows.push(Spans::from(first_row));
|
||||
// Fill remaining rows
|
||||
let cycles: usize = columns.len();
|
||||
for _ in 0..cycles {
|
||||
if let Some(col) = columns.pop_front() {
|
||||
rows.push(Spans::from(vec![col]));
|
||||
}
|
||||
}
|
||||
ListItem::new(rows)
|
||||
})
|
||||
.map(|row| ListItem::new(wrap_spans(row, area.width.into(), &self.props)))
|
||||
.collect(), // Make List item from TextSpan
|
||||
};
|
||||
let title: String = match self.props.texts.title.as_ref() {
|
||||
Some(t) => t.clone(),
|
||||
None => String::new(),
|
||||
};
|
||||
// Render
|
||||
|
||||
let w = List::new(list_items)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(self.props.borders)
|
||||
.border_style(match self.states.focus {
|
||||
true => Style::default().fg(self.props.foreground),
|
||||
false => Style::default(),
|
||||
})
|
||||
.title(title),
|
||||
)
|
||||
.block(get_block(
|
||||
&self.props.borders,
|
||||
&self.props.texts.title,
|
||||
self.states.focus,
|
||||
))
|
||||
.start_corner(Corner::BottomLeft)
|
||||
.highlight_symbol(">> ")
|
||||
.highlight_style(Style::default().add_modifier(self.props.get_modifiers()));
|
||||
.highlight_style(Style::default().add_modifier(self.props.modifiers));
|
||||
let mut state: ListState = ListState::default();
|
||||
state.select(Some(self.states.list_index));
|
||||
render.render_stateful_widget(w, area, &mut state);
|
||||
}
|
||||
}
|
||||
|
||||
/// ### update
|
||||
///
|
||||
/// Update component properties
|
||||
/// Properties should first be retrieved through `get_props` which creates a builder from
|
||||
/// existing properties and then edited before calling update
|
||||
fn update(&mut self, props: Props) -> Msg {
|
||||
self.props = props;
|
||||
// re-Set list length
|
||||
@@ -220,21 +237,13 @@ impl Component for LogBox {
|
||||
Msg::None
|
||||
}
|
||||
|
||||
/// ### get_props
|
||||
///
|
||||
/// Returns a props builder starting from component properties.
|
||||
/// This returns a prop builder in order to make easier to create
|
||||
/// new properties for the element.
|
||||
fn get_props(&self) -> PropsBuilder {
|
||||
PropsBuilder::from(self.props.clone())
|
||||
fn get_props(&self) -> Props {
|
||||
self.props.clone()
|
||||
}
|
||||
|
||||
/// ### on
|
||||
///
|
||||
/// Handle input event and update internal states
|
||||
fn on(&mut self, ev: InputEvent) -> Msg {
|
||||
fn on(&mut self, ev: Event) -> Msg {
|
||||
// Match event
|
||||
if let InputEvent::Key(key) = ev {
|
||||
if let Event::Key(key) = ev {
|
||||
match key.code {
|
||||
KeyCode::Up => {
|
||||
// Update states
|
||||
@@ -271,25 +280,14 @@ impl Component for LogBox {
|
||||
}
|
||||
}
|
||||
|
||||
/// ### get_value
|
||||
///
|
||||
/// Return component value. File list return index
|
||||
fn get_value(&self) -> Payload {
|
||||
fn get_state(&self) -> Payload {
|
||||
Payload::Unsigned(self.states.get_list_index())
|
||||
}
|
||||
|
||||
// -- events
|
||||
|
||||
/// ### blur
|
||||
///
|
||||
/// Blur component; basically remove focus
|
||||
fn blur(&mut self) {
|
||||
self.states.focus = false;
|
||||
}
|
||||
|
||||
/// ### active
|
||||
///
|
||||
/// Active component; basically give focus
|
||||
fn active(&mut self) {
|
||||
self.states.focus = true;
|
||||
}
|
||||
@@ -299,16 +297,18 @@ impl Component for LogBox {
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::ui::layout::props::{TableBuilder, TextParts, TextSpan};
|
||||
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use tui::style::Color;
|
||||
use tuirealm::event::{KeyCode, KeyEvent};
|
||||
use tuirealm::props::{TableBuilder, TextSpan};
|
||||
use tuirealm::tui::style::Color;
|
||||
|
||||
#[test]
|
||||
fn test_ui_layout_components_logbox() {
|
||||
fn test_ui_components_logbox() {
|
||||
let mut component: LogBox = LogBox::new(
|
||||
PropsBuilder::default()
|
||||
.with_texts(TextParts::table(
|
||||
LogboxPropsBuilder::default()
|
||||
.hidden()
|
||||
.visible()
|
||||
.with_borders(Borders::ALL, BorderType::Double, Color::Red)
|
||||
.with_log(
|
||||
Some(String::from("Log")),
|
||||
TableBuilder::default()
|
||||
.add_col(TextSpan::from("12:29"))
|
||||
@@ -317,9 +317,15 @@ mod tests {
|
||||
.add_col(TextSpan::from("12:38"))
|
||||
.add_col(TextSpan::from("system alive"))
|
||||
.build(),
|
||||
))
|
||||
)
|
||||
.build(),
|
||||
);
|
||||
assert_eq!(component.props.visible, true);
|
||||
assert_eq!(
|
||||
component.props.texts.title.as_ref().unwrap().as_str(),
|
||||
"Log"
|
||||
);
|
||||
assert_eq!(component.props.texts.table.as_ref().unwrap().len(), 2);
|
||||
// Verify states
|
||||
assert_eq!(component.states.list_index, 0);
|
||||
assert_eq!(component.states.list_len, 2);
|
||||
@@ -330,17 +336,18 @@ mod tests {
|
||||
component.blur();
|
||||
assert_eq!(component.states.focus, false);
|
||||
// Update
|
||||
let props = component.get_props().with_foreground(Color::Red).build();
|
||||
let props = LogboxPropsBuilder::from(component.get_props())
|
||||
.hidden()
|
||||
.build();
|
||||
assert_eq!(component.update(props), Msg::None);
|
||||
assert_eq!(component.props.foreground, Color::Red);
|
||||
assert_eq!(component.props.visible, false);
|
||||
// Increment list index
|
||||
component.states.list_index += 1;
|
||||
assert_eq!(component.states.list_index, 1);
|
||||
// Update
|
||||
component.update(
|
||||
component
|
||||
.get_props()
|
||||
.with_texts(TextParts::table(
|
||||
LogboxPropsBuilder::from(component.get_props())
|
||||
.with_log(
|
||||
Some(String::from("Log")),
|
||||
TableBuilder::default()
|
||||
.add_col(TextSpan::from("12:29"))
|
||||
@@ -352,49 +359,49 @@ mod tests {
|
||||
.add_col(TextSpan::from("12:41"))
|
||||
.add_col(TextSpan::from("system is going down for REBOOT"))
|
||||
.build(),
|
||||
))
|
||||
)
|
||||
.build(),
|
||||
);
|
||||
// Verify states
|
||||
assert_eq!(component.states.list_index, 0); // Last item
|
||||
assert_eq!(component.states.list_len, 3);
|
||||
// get value
|
||||
assert_eq!(component.get_value(), Payload::Unsigned(0));
|
||||
assert_eq!(component.get_state(), Payload::Unsigned(0));
|
||||
// RenderData
|
||||
assert_eq!(component.states.list_index, 0);
|
||||
// Set cursor to 0
|
||||
component.states.list_index = 0;
|
||||
// Handle inputs
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Up))),
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::Up))),
|
||||
Msg::None
|
||||
);
|
||||
// Index should be incremented
|
||||
assert_eq!(component.states.list_index, 1);
|
||||
// Index should be decremented
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Down))),
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::Down))),
|
||||
Msg::None
|
||||
);
|
||||
// Index should be incremented
|
||||
assert_eq!(component.states.list_index, 0);
|
||||
// Index should be 2
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::PageUp))),
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::PageUp))),
|
||||
Msg::None
|
||||
);
|
||||
// Index should be incremented
|
||||
assert_eq!(component.states.list_index, 2);
|
||||
// Index should be 0
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::PageDown))),
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::PageDown))),
|
||||
Msg::None
|
||||
);
|
||||
// Index should be incremented
|
||||
assert_eq!(component.states.list_index, 0);
|
||||
// On key
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Backspace))),
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::Backspace))),
|
||||
Msg::OnKey(KeyEvent::from(KeyCode::Backspace))
|
||||
);
|
||||
}
|
||||
@@ -25,17 +25,8 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// imports
|
||||
use super::{Canvas, Component, InputEvent, Msg, Payload, PropValue, Props, PropsBuilder};
|
||||
|
||||
// exports
|
||||
pub mod bookmark_list;
|
||||
pub mod file_list;
|
||||
pub mod input;
|
||||
pub mod logbox;
|
||||
pub mod msgbox;
|
||||
pub mod progress_bar;
|
||||
pub mod radio_group;
|
||||
pub mod table;
|
||||
pub mod text;
|
||||
pub mod title;
|
||||
@@ -27,16 +27,99 @@
|
||||
*/
|
||||
// deps
|
||||
extern crate textwrap;
|
||||
extern crate tuirealm;
|
||||
// locals
|
||||
use super::{Canvas, Component, InputEvent, Msg, Payload, Props, PropsBuilder};
|
||||
use crate::utils::fmt::align_text_center;
|
||||
// ext
|
||||
use tui::{
|
||||
use tuirealm::components::utils::{get_block, use_or_default_styles};
|
||||
use tuirealm::event::Event;
|
||||
use tuirealm::props::{BordersProps, Props, PropsBuilder, TextParts, TextSpan};
|
||||
use tuirealm::tui::{
|
||||
layout::{Corner, Rect},
|
||||
style::{Color, Style},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, BorderType, List, ListItem},
|
||||
widgets::{BorderType, Borders, List, ListItem},
|
||||
};
|
||||
use tuirealm::{Canvas, Component, Msg, Payload};
|
||||
|
||||
// -- Props
|
||||
|
||||
pub struct MsgBoxPropsBuilder {
|
||||
props: Option<Props>,
|
||||
}
|
||||
|
||||
impl Default for MsgBoxPropsBuilder {
|
||||
fn default() -> Self {
|
||||
MsgBoxPropsBuilder {
|
||||
props: Some(Props::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PropsBuilder for MsgBoxPropsBuilder {
|
||||
fn build(&mut self) -> Props {
|
||||
self.props.take().unwrap()
|
||||
}
|
||||
|
||||
fn hidden(&mut self) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.visible = false;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn visible(&mut self) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.visible = true;
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Props> for MsgBoxPropsBuilder {
|
||||
fn from(props: Props) -> Self {
|
||||
MsgBoxPropsBuilder { props: Some(props) }
|
||||
}
|
||||
}
|
||||
|
||||
impl MsgBoxPropsBuilder {
|
||||
pub fn with_foreground(&mut self, color: Color) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.foreground = color;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn bold(&mut self) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.modifiers |= Modifier::BOLD;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_borders(
|
||||
&mut self,
|
||||
borders: Borders,
|
||||
variant: BorderType,
|
||||
color: Color,
|
||||
) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.borders = BordersProps {
|
||||
borders,
|
||||
variant,
|
||||
color,
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_texts(&mut self, title: Option<String>, texts: Vec<TextSpan>) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.texts = TextParts::new(title, Some(texts));
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// -- component
|
||||
|
||||
@@ -54,56 +137,36 @@ impl MsgBox {
|
||||
}
|
||||
|
||||
impl Component for MsgBox {
|
||||
/// ### render
|
||||
///
|
||||
/// Based on the current properties and states, renders a widget using the provided render engine in the provided Area
|
||||
/// If focused, cursor is also set (if supported by widget)
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
fn render(&self, render: &mut Canvas, area: Rect) {
|
||||
// Make a Span
|
||||
if self.props.visible {
|
||||
let lines: Vec<ListItem> = match self.props.texts.rows.as_ref() {
|
||||
let lines: Vec<ListItem> = match self.props.texts.spans.as_ref() {
|
||||
None => Vec::new(),
|
||||
Some(rows) => {
|
||||
let mut lines: Vec<ListItem> = Vec::new();
|
||||
for line in rows.iter() {
|
||||
// Keep line color, or use default
|
||||
let line_fg: Color = match line.fg {
|
||||
Color::Reset => self.props.foreground,
|
||||
_ => line.fg,
|
||||
};
|
||||
let line_bg: Color = match line.bg {
|
||||
Color::Reset => self.props.background,
|
||||
_ => line.bg,
|
||||
};
|
||||
let (fg, bg, modifiers) = use_or_default_styles(&self.props, line);
|
||||
let message_row =
|
||||
textwrap::wrap(line.content.as_str(), area.width as usize);
|
||||
for msg in message_row.iter() {
|
||||
lines.push(ListItem::new(Spans::from(vec![Span::styled(
|
||||
align_text_center(msg, area.width),
|
||||
Style::default()
|
||||
.add_modifier(line.get_modifiers())
|
||||
.fg(line_fg)
|
||||
.bg(line_bg),
|
||||
Style::default().add_modifier(modifiers).fg(fg).bg(bg),
|
||||
)])));
|
||||
}
|
||||
}
|
||||
lines
|
||||
}
|
||||
};
|
||||
let title: String = match self.props.texts.title.as_ref() {
|
||||
Some(t) => t.clone(),
|
||||
None => String::new(),
|
||||
};
|
||||
render.render_widget(
|
||||
List::new(lines)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(self.props.borders)
|
||||
.border_style(Style::default().fg(self.props.foreground))
|
||||
.border_type(BorderType::Rounded)
|
||||
.title(title),
|
||||
)
|
||||
.block(get_block(
|
||||
&self.props.borders,
|
||||
&self.props.texts.title,
|
||||
true,
|
||||
))
|
||||
.start_corner(Corner::TopLeft)
|
||||
.style(
|
||||
Style::default()
|
||||
@@ -115,59 +178,31 @@ impl Component for MsgBox {
|
||||
}
|
||||
}
|
||||
|
||||
/// ### update
|
||||
///
|
||||
/// Update component properties
|
||||
/// Properties should first be retrieved through `get_props` which creates a builder from
|
||||
/// existing properties and then edited before calling update.
|
||||
/// Returns a Msg to the view
|
||||
fn update(&mut self, props: Props) -> Msg {
|
||||
self.props = props;
|
||||
// Return None
|
||||
Msg::None
|
||||
}
|
||||
|
||||
/// ### get_props
|
||||
///
|
||||
/// Returns a props builder starting from component properties.
|
||||
/// This returns a prop builder in order to make easier to create
|
||||
/// new properties for the element.
|
||||
fn get_props(&self) -> PropsBuilder {
|
||||
PropsBuilder::from(self.props.clone())
|
||||
fn get_props(&self) -> Props {
|
||||
self.props.clone()
|
||||
}
|
||||
|
||||
/// ### on
|
||||
///
|
||||
/// Handle input event and update internal states.
|
||||
/// Returns a Msg to the view.
|
||||
/// Returns always None, since cannot have any focus
|
||||
fn on(&mut self, ev: InputEvent) -> Msg {
|
||||
fn on(&mut self, ev: Event) -> Msg {
|
||||
// Return key
|
||||
if let InputEvent::Key(key) = ev {
|
||||
if let Event::Key(key) = ev {
|
||||
Msg::OnKey(key)
|
||||
} else {
|
||||
Msg::None
|
||||
}
|
||||
}
|
||||
|
||||
/// ### get_value
|
||||
///
|
||||
/// Get current value from component
|
||||
/// For this component returns always None
|
||||
fn get_value(&self) -> Payload {
|
||||
fn get_state(&self) -> Payload {
|
||||
Payload::None
|
||||
}
|
||||
|
||||
// -- events
|
||||
|
||||
/// ### blur
|
||||
///
|
||||
/// Blur component
|
||||
fn blur(&mut self) {}
|
||||
|
||||
/// ### active
|
||||
///
|
||||
/// Active component
|
||||
fn active(&mut self) {}
|
||||
}
|
||||
|
||||
@@ -175,39 +210,51 @@ impl Component for MsgBox {
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::ui::layout::props::{TextParts, TextSpan, TextSpanBuilder};
|
||||
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use tui::style::Color;
|
||||
use tuirealm::event::{KeyCode, KeyEvent};
|
||||
use tuirealm::props::{TextSpan, TextSpanBuilder};
|
||||
use tuirealm::tui::style::Color;
|
||||
|
||||
#[test]
|
||||
fn test_ui_layout_components_msgbox() {
|
||||
fn test_ui_components_msgbox() {
|
||||
let mut component: MsgBox = MsgBox::new(
|
||||
PropsBuilder::default()
|
||||
.with_texts(TextParts::new(
|
||||
MsgBoxPropsBuilder::default()
|
||||
.hidden()
|
||||
.visible()
|
||||
.with_foreground(Color::Red)
|
||||
.bold()
|
||||
.with_borders(Borders::ALL, BorderType::Double, Color::Red)
|
||||
.with_texts(
|
||||
None,
|
||||
Some(vec![
|
||||
vec![
|
||||
TextSpan::from("Press "),
|
||||
TextSpanBuilder::new("<ESC>")
|
||||
.with_foreground(Color::Cyan)
|
||||
.bold()
|
||||
.build(),
|
||||
TextSpan::from(" to quit"),
|
||||
]),
|
||||
))
|
||||
],
|
||||
)
|
||||
.build(),
|
||||
);
|
||||
assert_eq!(component.props.foreground, Color::Red);
|
||||
assert!(component.props.modifiers.intersects(Modifier::BOLD));
|
||||
assert_eq!(component.props.visible, true);
|
||||
assert_eq!(component.props.texts.spans.as_ref().unwrap().len(), 3);
|
||||
component.active();
|
||||
component.blur();
|
||||
// Update
|
||||
let props = component.get_props().with_foreground(Color::Red).build();
|
||||
let props = MsgBoxPropsBuilder::from(component.get_props())
|
||||
.hidden()
|
||||
.with_foreground(Color::Yellow)
|
||||
.build();
|
||||
assert_eq!(component.update(props), Msg::None);
|
||||
assert_eq!(component.props.foreground, Color::Red);
|
||||
assert_eq!(component.props.visible, false);
|
||||
assert_eq!(component.props.foreground, Color::Yellow);
|
||||
// Get value
|
||||
assert_eq!(component.get_value(), Payload::None);
|
||||
assert_eq!(component.get_state(), Payload::None);
|
||||
// Event
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Delete))),
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::Delete))),
|
||||
Msg::OnKey(KeyEvent::from(KeyCode::Delete))
|
||||
);
|
||||
}
|
||||
@@ -27,7 +27,7 @@
|
||||
*/
|
||||
// Dependencies
|
||||
extern crate crossterm;
|
||||
extern crate tui;
|
||||
extern crate tuirealm;
|
||||
|
||||
// Locals
|
||||
use super::input::InputHandler;
|
||||
@@ -42,8 +42,8 @@ use crossterm::execute;
|
||||
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
|
||||
use std::io::{stdout, Stdout};
|
||||
use std::path::PathBuf;
|
||||
use tui::backend::CrosstermBackend;
|
||||
use tui::Terminal;
|
||||
use tuirealm::tui::backend::CrosstermBackend;
|
||||
use tuirealm::tui::Terminal;
|
||||
|
||||
/// ## Context
|
||||
///
|
||||
|
||||
@@ -1,564 +0,0 @@
|
||||
//! ## Input
|
||||
//!
|
||||
//! `Input` component renders an input box
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// locals
|
||||
use super::super::props::InputType;
|
||||
use super::{Canvas, Component, InputEvent, Msg, Payload, PropValue, Props, PropsBuilder};
|
||||
// ext
|
||||
use crossterm::event::{KeyCode, KeyModifiers};
|
||||
use tui::{
|
||||
layout::Rect,
|
||||
style::Style,
|
||||
widgets::{Block, BorderType, Paragraph},
|
||||
};
|
||||
|
||||
// -- states
|
||||
|
||||
/// ## OwnStates
|
||||
///
|
||||
/// OwnStates contains states for this component
|
||||
#[derive(Clone)]
|
||||
struct OwnStates {
|
||||
input: Vec<char>, // Current input
|
||||
cursor: usize, // Input position
|
||||
focus: bool, // Focus
|
||||
}
|
||||
|
||||
impl Default for OwnStates {
|
||||
fn default() -> Self {
|
||||
OwnStates {
|
||||
input: Vec::new(),
|
||||
cursor: 0,
|
||||
focus: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OwnStates {
|
||||
/// ### append
|
||||
///
|
||||
/// Append, if possible according to input type, the character to the input vec
|
||||
pub fn append(&mut self, ch: char, itype: InputType, max_len: Option<usize>) {
|
||||
// Check if max length has been reached
|
||||
if self.input.len() < max_len.unwrap_or(usize::MAX) {
|
||||
match itype {
|
||||
InputType::Number => {
|
||||
if ch.is_digit(10) {
|
||||
// Must be digit
|
||||
self.input.insert(self.cursor, ch);
|
||||
// Increment cursor
|
||||
self.cursor += 1;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// No rule
|
||||
self.input.insert(self.cursor, ch);
|
||||
// Increment cursor
|
||||
self.cursor += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### backspace
|
||||
///
|
||||
/// Delete element at cursor -1; then decrement cursor by 1
|
||||
pub fn backspace(&mut self) {
|
||||
if self.cursor > 0 && !self.input.is_empty() {
|
||||
self.input.remove(self.cursor - 1);
|
||||
// Decrement cursor
|
||||
self.cursor -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// ### delete
|
||||
///
|
||||
/// Delete element at cursor
|
||||
pub fn delete(&mut self) {
|
||||
if self.cursor < self.input.len() {
|
||||
self.input.remove(self.cursor);
|
||||
}
|
||||
}
|
||||
|
||||
/// ### incr_cursor
|
||||
///
|
||||
/// Increment cursor value by one if possible
|
||||
pub fn incr_cursor(&mut self) {
|
||||
if self.cursor < self.input.len() {
|
||||
self.cursor += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// ### cursoro_at_begin
|
||||
///
|
||||
/// Place cursor at the begin of the input
|
||||
pub fn cursor_at_begin(&mut self) {
|
||||
self.cursor = 0;
|
||||
}
|
||||
|
||||
/// ### cursor_at_end
|
||||
///
|
||||
/// Place cursor at the end of the input
|
||||
pub fn cursor_at_end(&mut self) {
|
||||
self.cursor = self.input.len();
|
||||
}
|
||||
|
||||
/// ### decr_cursor
|
||||
///
|
||||
/// Decrement cursor value by one if possible
|
||||
pub fn decr_cursor(&mut self) {
|
||||
if self.cursor > 0 {
|
||||
self.cursor -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// ### render_value
|
||||
///
|
||||
/// Get value as string to render
|
||||
pub fn render_value(&self, itype: InputType) -> String {
|
||||
match itype {
|
||||
InputType::Password => (0..self.input.len()).map(|_| '*').collect(),
|
||||
_ => self.get_value(),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### get_value
|
||||
///
|
||||
/// Get value as string
|
||||
pub fn get_value(&self) -> String {
|
||||
self.input.iter().collect()
|
||||
}
|
||||
}
|
||||
|
||||
// -- Component
|
||||
|
||||
/// ## FileList
|
||||
///
|
||||
/// File list component
|
||||
pub struct Input {
|
||||
props: Props,
|
||||
states: OwnStates,
|
||||
}
|
||||
|
||||
impl Input {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiates a new Input starting from Props
|
||||
/// The method also initializes the component states.
|
||||
pub fn new(props: Props) -> Self {
|
||||
// Initialize states
|
||||
let mut states: OwnStates = OwnStates::default();
|
||||
// Set state value from props
|
||||
if let PropValue::Str(val) = props.value.clone() {
|
||||
for ch in val.chars() {
|
||||
states.append(ch, props.input_type, props.input_len);
|
||||
}
|
||||
}
|
||||
Input { props, states }
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Input {
|
||||
/// ### render
|
||||
///
|
||||
/// Based on the current properties and states, renders a widget using the provided render engine in the provided Area
|
||||
/// If focused, cursor is also set (if supported by widget)
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
fn render(&self, render: &mut Canvas, area: Rect) {
|
||||
if self.props.visible {
|
||||
let title: String = match self.props.texts.title.as_ref() {
|
||||
Some(t) => t.clone(),
|
||||
None => String::new(),
|
||||
};
|
||||
let p: Paragraph = Paragraph::new(self.states.render_value(self.props.input_type))
|
||||
.style(match self.states.focus {
|
||||
true => Style::default().fg(self.props.foreground),
|
||||
false => Style::default(),
|
||||
})
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(self.props.borders)
|
||||
.border_type(BorderType::Rounded)
|
||||
.title(title),
|
||||
);
|
||||
render.render_widget(p, area);
|
||||
// Set cursor, if focus
|
||||
if self.states.focus {
|
||||
let x: u16 = area.x + (self.states.cursor as u16) + 1;
|
||||
render.set_cursor(x, area.y + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### update
|
||||
///
|
||||
/// Update component properties
|
||||
/// Properties should first be retrieved through `get_props` which creates a builder from
|
||||
/// existing properties and then edited before calling update.
|
||||
/// Returns a Msg to the view
|
||||
fn update(&mut self, props: Props) -> Msg {
|
||||
self.props = props;
|
||||
// Set value from props
|
||||
if let PropValue::Str(val) = self.props.value.clone() {
|
||||
self.states.input = Vec::new();
|
||||
self.states.cursor = 0;
|
||||
for ch in val.chars() {
|
||||
self.states
|
||||
.append(ch, self.props.input_type, self.props.input_len);
|
||||
}
|
||||
}
|
||||
Msg::None
|
||||
}
|
||||
|
||||
/// ### get_props
|
||||
///
|
||||
/// Returns a props builder starting from component properties.
|
||||
/// This returns a prop builder in order to make easier to create
|
||||
/// new properties for the element.
|
||||
fn get_props(&self) -> PropsBuilder {
|
||||
// Make properties with value from states
|
||||
let mut props: Props = self.props.clone();
|
||||
props.value = PropValue::Str(self.states.get_value());
|
||||
PropsBuilder::from(props)
|
||||
}
|
||||
|
||||
/// ### on
|
||||
///
|
||||
/// Handle input event and update internal states.
|
||||
/// Returns a Msg to the view
|
||||
fn on(&mut self, ev: InputEvent) -> Msg {
|
||||
if let InputEvent::Key(key) = ev {
|
||||
match key.code {
|
||||
KeyCode::Backspace => {
|
||||
// Backspace and None
|
||||
self.states.backspace();
|
||||
Msg::None
|
||||
}
|
||||
KeyCode::Delete => {
|
||||
// Delete and None
|
||||
self.states.delete();
|
||||
Msg::None
|
||||
}
|
||||
KeyCode::Enter => Msg::OnSubmit(self.get_value()),
|
||||
KeyCode::Left => {
|
||||
// Move cursor left; msg None
|
||||
self.states.decr_cursor();
|
||||
Msg::None
|
||||
}
|
||||
KeyCode::Right => {
|
||||
// Move cursor right; Msg None
|
||||
self.states.incr_cursor();
|
||||
Msg::None
|
||||
}
|
||||
KeyCode::End => {
|
||||
// Cursor at last position
|
||||
self.states.cursor_at_end();
|
||||
Msg::None
|
||||
}
|
||||
KeyCode::Home => {
|
||||
// Cursor at first positon
|
||||
self.states.cursor_at_begin();
|
||||
Msg::None
|
||||
}
|
||||
KeyCode::Char(ch) => {
|
||||
// Check if modifiers is NOT CTRL OR ALT
|
||||
if !key.modifiers.intersects(KeyModifiers::CONTROL)
|
||||
&& !key.modifiers.intersects(KeyModifiers::ALT)
|
||||
{
|
||||
// Push char to input
|
||||
self.states
|
||||
.append(ch, self.props.input_type, self.props.input_len);
|
||||
// Message none
|
||||
Msg::None
|
||||
} else {
|
||||
// Return key
|
||||
Msg::OnKey(key)
|
||||
}
|
||||
}
|
||||
_ => Msg::OnKey(key),
|
||||
}
|
||||
} else {
|
||||
Msg::None
|
||||
}
|
||||
}
|
||||
|
||||
/// ### get_value
|
||||
///
|
||||
/// Get current value from component
|
||||
/// Returns the value as string or as a number based on the input value
|
||||
fn get_value(&self) -> Payload {
|
||||
match self.props.input_type {
|
||||
InputType::Number => {
|
||||
Payload::Unsigned(self.states.get_value().parse::<usize>().ok().unwrap_or(0))
|
||||
}
|
||||
_ => Payload::Text(self.states.get_value()),
|
||||
}
|
||||
}
|
||||
|
||||
// -- events
|
||||
|
||||
/// ### blur
|
||||
///
|
||||
/// Blur component; basically remove focus
|
||||
fn blur(&mut self) {
|
||||
self.states.focus = false;
|
||||
}
|
||||
|
||||
/// ### active
|
||||
///
|
||||
/// Active component; basically give focus
|
||||
fn active(&mut self) {
|
||||
self.states.focus = true;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
use crossterm::event::KeyEvent;
|
||||
use tui::style::Color;
|
||||
|
||||
#[test]
|
||||
fn test_ui_layout_components_input_text() {
|
||||
// Instantiate Input with value
|
||||
let mut component: Input = Input::new(
|
||||
PropsBuilder::default()
|
||||
.with_input(InputType::Text)
|
||||
.with_input_len(5)
|
||||
.with_value(PropValue::Str(String::from("home")))
|
||||
.build(),
|
||||
);
|
||||
// Verify initial state
|
||||
assert_eq!(component.states.cursor, 4);
|
||||
assert_eq!(component.states.input.len(), 4);
|
||||
// Focus
|
||||
assert_eq!(component.states.focus, false);
|
||||
component.active();
|
||||
assert_eq!(component.states.focus, true);
|
||||
component.blur();
|
||||
assert_eq!(component.states.focus, false);
|
||||
// Update
|
||||
let props = component.get_props().with_foreground(Color::Red).build();
|
||||
assert_eq!(component.update(props), Msg::None);
|
||||
assert_eq!(component.props.foreground, Color::Red);
|
||||
// Get value
|
||||
assert_eq!(component.get_value(), Payload::Text(String::from("home")));
|
||||
// RenderData
|
||||
//assert_eq!(component.render().unwrap().cursor, 4);
|
||||
assert_eq!(component.states.cursor, 4);
|
||||
// Handle events
|
||||
// Try key with ctrl
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::new(
|
||||
KeyCode::Char('a'),
|
||||
KeyModifiers::CONTROL
|
||||
))),
|
||||
Msg::OnKey(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL)),
|
||||
);
|
||||
// String shouldn't have changed
|
||||
assert_eq!(component.get_value(), Payload::Text(String::from("home")));
|
||||
//assert_eq!(component.render().unwrap().cursor, 4);
|
||||
assert_eq!(component.states.cursor, 4);
|
||||
// Character
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Char('/')))),
|
||||
Msg::None
|
||||
);
|
||||
assert_eq!(component.get_value(), Payload::Text(String::from("home/")));
|
||||
//assert_eq!(component.render().unwrap().cursor, 5);
|
||||
assert_eq!(component.states.cursor, 5);
|
||||
// Verify max length (shouldn't push any character)
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Char('a')))),
|
||||
Msg::None
|
||||
);
|
||||
assert_eq!(component.get_value(), Payload::Text(String::from("home/")));
|
||||
//assert_eq!(component.render().unwrap().cursor, 5);
|
||||
assert_eq!(component.states.cursor, 5);
|
||||
// Enter
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Enter))),
|
||||
Msg::OnSubmit(Payload::Text(String::from("home/")))
|
||||
);
|
||||
// Backspace
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Backspace))),
|
||||
Msg::None
|
||||
);
|
||||
assert_eq!(component.get_value(), Payload::Text(String::from("home")));
|
||||
//assert_eq!(component.render().unwrap().cursor, 4);
|
||||
assert_eq!(component.states.cursor, 4);
|
||||
// Check backspace at 0
|
||||
component.states.input = vec!['h'];
|
||||
component.states.cursor = 1;
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Backspace))),
|
||||
Msg::None
|
||||
);
|
||||
assert_eq!(component.get_value(), Payload::Text(String::from("")));
|
||||
//assert_eq!(component.render().unwrap().cursor, 0);
|
||||
assert_eq!(component.states.cursor, 0);
|
||||
// Another one...
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Backspace))),
|
||||
Msg::None
|
||||
);
|
||||
assert_eq!(component.get_value(), Payload::Text(String::from("")));
|
||||
//assert_eq!(component.render().unwrap().cursor, 0);
|
||||
assert_eq!(component.states.cursor, 0);
|
||||
// See del behaviour here
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Delete))),
|
||||
Msg::None
|
||||
);
|
||||
assert_eq!(component.get_value(), Payload::Text(String::from("")));
|
||||
//assert_eq!(component.render().unwrap().cursor, 0);
|
||||
assert_eq!(component.states.cursor, 0);
|
||||
// Check del behaviour
|
||||
component.states.input = vec!['h', 'e'];
|
||||
component.states.cursor = 1;
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Delete))),
|
||||
Msg::None
|
||||
);
|
||||
assert_eq!(component.get_value(), Payload::Text(String::from("h")));
|
||||
//assert_eq!(component.render().unwrap().cursor, 1); // Shouldn't move
|
||||
assert_eq!(component.states.cursor, 1);
|
||||
// Another one (should do nothing)
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Delete))),
|
||||
Msg::None
|
||||
);
|
||||
assert_eq!(component.get_value(), Payload::Text(String::from("h")));
|
||||
//assert_eq!(component.render().unwrap().cursor, 1); // Shouldn't move
|
||||
assert_eq!(component.states.cursor, 1);
|
||||
// Move cursor right
|
||||
component.states.input = vec!['h', 'e', 'l', 'l', 'o'];
|
||||
component.states.cursor = 1;
|
||||
component.props.input_len = Some(16); // Let's change length
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Right))), // between 'e' and 'l'
|
||||
Msg::None
|
||||
);
|
||||
//assert_eq!(component.render().unwrap().cursor, 2); // Should increment
|
||||
assert_eq!(component.states.cursor, 2);
|
||||
// Put a character here
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Char('a')))),
|
||||
Msg::None
|
||||
);
|
||||
assert_eq!(component.get_value(), Payload::Text(String::from("heallo")));
|
||||
//assert_eq!(component.render().unwrap().cursor, 3);
|
||||
assert_eq!(component.states.cursor, 3);
|
||||
// Move left
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Left))),
|
||||
Msg::None
|
||||
);
|
||||
//assert_eq!(component.render().unwrap().cursor, 2); // Should decrement
|
||||
assert_eq!(component.states.cursor, 2);
|
||||
// Go at the end
|
||||
component.states.cursor = 6;
|
||||
// Move right
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Right))),
|
||||
Msg::None
|
||||
);
|
||||
//assert_eq!(component.render().unwrap().cursor, 6); // Should stay
|
||||
assert_eq!(component.states.cursor, 6);
|
||||
// Move left
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Left))),
|
||||
Msg::None
|
||||
);
|
||||
//assert_eq!(component.render().unwrap().cursor, 5); // Should decrement
|
||||
assert_eq!(component.states.cursor, 5);
|
||||
// Go at the beginning
|
||||
component.states.cursor = 0;
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Left))),
|
||||
Msg::None
|
||||
);
|
||||
//assert_eq!(component.render().unwrap().cursor, 0); // Should stay
|
||||
assert_eq!(component.states.cursor, 0);
|
||||
// End - begin
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::End))),
|
||||
Msg::None
|
||||
);
|
||||
assert_eq!(component.states.cursor, 6);
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Home))),
|
||||
Msg::None
|
||||
);
|
||||
assert_eq!(component.states.cursor, 0);
|
||||
// Update value
|
||||
component.update(
|
||||
component
|
||||
.get_props()
|
||||
.with_value(PropValue::Str("new-value".to_string()))
|
||||
.build(),
|
||||
);
|
||||
assert_eq!(
|
||||
component.get_value(),
|
||||
Payload::Text(String::from("new-value"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ui_layout_components_input_number() {
|
||||
// Instantiate Input with value
|
||||
let mut component: Input = Input::new(
|
||||
PropsBuilder::default()
|
||||
.with_input(InputType::Number)
|
||||
.with_input_len(5)
|
||||
.with_value(PropValue::Str(String::from("3000")))
|
||||
.build(),
|
||||
);
|
||||
// Verify initial state
|
||||
assert_eq!(component.states.cursor, 4);
|
||||
assert_eq!(component.states.input.len(), 4);
|
||||
// Push a non numeric value
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Char('a')))),
|
||||
Msg::None
|
||||
);
|
||||
assert_eq!(component.get_value(), Payload::Unsigned(3000));
|
||||
//assert_eq!(component.render().unwrap().cursor, 4);
|
||||
assert_eq!(component.states.cursor, 4);
|
||||
// Push a number
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Char('1')))),
|
||||
Msg::None
|
||||
);
|
||||
assert_eq!(component.get_value(), Payload::Unsigned(30001));
|
||||
//assert_eq!(component.render().unwrap().cursor, 5);
|
||||
assert_eq!(component.states.cursor, 5);
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
//! ## ProgressBar
|
||||
//!
|
||||
//! `ProgressBar` component renders a progress bar
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// locals
|
||||
use super::{Canvas, Component, InputEvent, Msg, Payload, PropValue, Props, PropsBuilder};
|
||||
// ext
|
||||
use tui::{
|
||||
layout::Rect,
|
||||
style::Style,
|
||||
widgets::{Block, Gauge},
|
||||
};
|
||||
|
||||
// -- component
|
||||
|
||||
pub struct ProgressBar {
|
||||
props: Props,
|
||||
}
|
||||
|
||||
impl ProgressBar {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiate a new Progress Bar
|
||||
pub fn new(props: Props) -> Self {
|
||||
ProgressBar { props }
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for ProgressBar {
|
||||
/// ### render
|
||||
///
|
||||
/// Based on the current properties and states, renders a widget using the provided render engine in the provided Area
|
||||
/// If focused, cursor is also set (if supported by widget)
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
fn render(&self, render: &mut Canvas, area: Rect) {
|
||||
// Make a Span
|
||||
if self.props.visible {
|
||||
let title: String = match self.props.texts.title.as_ref() {
|
||||
Some(t) => t.clone(),
|
||||
None => String::new(),
|
||||
};
|
||||
// Text
|
||||
let label: String = match self.props.texts.rows.as_ref() {
|
||||
Some(rows) => match rows.get(0) {
|
||||
Some(label) => label.content.clone(),
|
||||
None => String::new(),
|
||||
},
|
||||
None => String::new(),
|
||||
};
|
||||
// Get percentage
|
||||
let percentage: f64 = match self.props.value {
|
||||
PropValue::Float(ratio) => ratio,
|
||||
_ => 0.0,
|
||||
};
|
||||
// Make progress bar
|
||||
render.render_widget(
|
||||
Gauge::default()
|
||||
.block(Block::default().borders(self.props.borders).title(title))
|
||||
.gauge_style(
|
||||
Style::default()
|
||||
.fg(self.props.foreground)
|
||||
.bg(self.props.background)
|
||||
.add_modifier(self.props.get_modifiers()),
|
||||
)
|
||||
.label(label)
|
||||
.ratio(percentage),
|
||||
area,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// ### update
|
||||
///
|
||||
/// Update component properties
|
||||
/// Properties should first be retrieved through `get_props` which creates a builder from
|
||||
/// existing properties and then edited before calling update.
|
||||
/// Returns a Msg to the view
|
||||
fn update(&mut self, props: Props) -> Msg {
|
||||
self.props = props;
|
||||
// Return None
|
||||
Msg::None
|
||||
}
|
||||
|
||||
/// ### get_props
|
||||
///
|
||||
/// Returns a props builder starting from component properties.
|
||||
/// This returns a prop builder in order to make easier to create
|
||||
/// new properties for the element.
|
||||
fn get_props(&self) -> PropsBuilder {
|
||||
PropsBuilder::from(self.props.clone())
|
||||
}
|
||||
|
||||
/// ### on
|
||||
///
|
||||
/// Handle input event and update internal states.
|
||||
/// Returns a Msg to the view.
|
||||
/// Returns always None, since cannot have any focus
|
||||
fn on(&mut self, ev: InputEvent) -> Msg {
|
||||
// Return key
|
||||
if let InputEvent::Key(key) = ev {
|
||||
Msg::OnKey(key)
|
||||
} else {
|
||||
Msg::None
|
||||
}
|
||||
}
|
||||
|
||||
/// ### get_value
|
||||
///
|
||||
/// Get current value from component
|
||||
/// For this component returns always None
|
||||
fn get_value(&self) -> Payload {
|
||||
Payload::None
|
||||
}
|
||||
|
||||
// -- events
|
||||
|
||||
/// ### blur
|
||||
///
|
||||
/// Blur component
|
||||
fn blur(&mut self) {}
|
||||
|
||||
/// ### active
|
||||
///
|
||||
/// Active component
|
||||
fn active(&mut self) {}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::ui::layout::props::{TextParts, TextSpan};
|
||||
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use tui::style::Color;
|
||||
|
||||
#[test]
|
||||
fn test_ui_layout_components_progress_bar() {
|
||||
let mut component: ProgressBar = ProgressBar::new(
|
||||
PropsBuilder::default()
|
||||
.with_texts(TextParts::new(
|
||||
Some(String::from("Uploading file...")),
|
||||
Some(vec![TextSpan::from("96.5% - ETA 00:02 (4.2MB/s)")]),
|
||||
))
|
||||
.build(),
|
||||
);
|
||||
// Get value
|
||||
assert_eq!(component.get_value(), Payload::None);
|
||||
component.active();
|
||||
component.blur();
|
||||
// Update
|
||||
let props = component.get_props().with_foreground(Color::Red).build();
|
||||
assert_eq!(component.update(props), Msg::None);
|
||||
assert_eq!(component.props.foreground, Color::Red);
|
||||
// Event
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Delete))),
|
||||
Msg::OnKey(KeyEvent::from(KeyCode::Delete))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,334 +0,0 @@
|
||||
//! ## RadioGroup
|
||||
//!
|
||||
//! `RadioGroup` component renders a radio group
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// locals
|
||||
use super::super::props::TextSpan;
|
||||
use super::{Canvas, Component, InputEvent, Msg, Payload, PropValue, Props, PropsBuilder};
|
||||
// ext
|
||||
use crossterm::event::KeyCode;
|
||||
use tui::{
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
text::Spans,
|
||||
widgets::{Block, BorderType, Tabs},
|
||||
};
|
||||
|
||||
// -- states
|
||||
|
||||
/// ## OwnStates
|
||||
///
|
||||
/// OwnStates contains states for this component
|
||||
#[derive(Clone)]
|
||||
struct OwnStates {
|
||||
choice: usize, // Selected option
|
||||
choices: Vec<String>, // Available choices
|
||||
focus: bool, // has focus?
|
||||
}
|
||||
|
||||
impl Default for OwnStates {
|
||||
fn default() -> Self {
|
||||
OwnStates {
|
||||
choice: 0,
|
||||
choices: Vec::new(),
|
||||
focus: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OwnStates {
|
||||
/// ### next_choice
|
||||
///
|
||||
/// Move choice index to next choice
|
||||
pub fn next_choice(&mut self) {
|
||||
if self.choice + 1 < self.choices.len() {
|
||||
self.choice += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// ### prev_choice
|
||||
///
|
||||
/// Move choice index to previous choice
|
||||
pub fn prev_choice(&mut self) {
|
||||
if self.choice > 0 {
|
||||
self.choice -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// ### make_choices
|
||||
///
|
||||
/// Set OwnStates choices from a vector of text spans
|
||||
pub fn make_choices(&mut self, spans: &[TextSpan]) {
|
||||
self.choices = spans.iter().map(|x| x.content.clone()).collect();
|
||||
}
|
||||
}
|
||||
|
||||
// -- component
|
||||
|
||||
/// ## RadioGroup
|
||||
///
|
||||
/// RadioGroup component represents a group of tabs to select from
|
||||
pub struct RadioGroup {
|
||||
props: Props,
|
||||
states: OwnStates,
|
||||
}
|
||||
|
||||
impl RadioGroup {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiate a new Radio Group component
|
||||
pub fn new(props: Props) -> Self {
|
||||
// Make states
|
||||
let mut states: OwnStates = OwnStates::default();
|
||||
// Update choices (vec of TextSpan to String)
|
||||
states.make_choices(props.texts.rows.as_ref().unwrap_or(&Vec::new()));
|
||||
// Get value
|
||||
if let PropValue::Unsigned(choice) = props.value {
|
||||
states.choice = choice;
|
||||
}
|
||||
RadioGroup { props, states }
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for RadioGroup {
|
||||
/// ### render
|
||||
///
|
||||
/// Based on the current properties and states, renders a widget using the provided render engine in the provided Area
|
||||
/// If focused, cursor is also set (if supported by widget)
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
fn render(&self, render: &mut Canvas, area: Rect) {
|
||||
if self.props.visible {
|
||||
// Make choices
|
||||
let choices: Vec<Spans> = self
|
||||
.states
|
||||
.choices
|
||||
.iter()
|
||||
.map(|x| Spans::from(x.clone()))
|
||||
.collect();
|
||||
// Make colors
|
||||
let (bg, fg, block_color): (Color, Color, Color) = match &self.states.focus {
|
||||
true => (
|
||||
self.props.foreground,
|
||||
self.props.background,
|
||||
self.props.foreground,
|
||||
),
|
||||
false => (Color::Reset, self.props.foreground, Color::Reset),
|
||||
};
|
||||
let title: String = match &self.props.texts.title {
|
||||
Some(t) => t.clone(),
|
||||
None => String::new(),
|
||||
};
|
||||
render.render_widget(
|
||||
Tabs::new(choices)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(self.props.borders)
|
||||
.border_type(BorderType::Rounded)
|
||||
.style(Style::default())
|
||||
.title(title),
|
||||
)
|
||||
.select(self.states.choice)
|
||||
.style(Style::default().fg(block_color))
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.add_modifier(self.props.get_modifiers())
|
||||
.fg(fg)
|
||||
.bg(bg),
|
||||
),
|
||||
area,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// ### update
|
||||
///
|
||||
/// Update component properties
|
||||
/// Properties should first be retrieved through `get_props` which creates a builder from
|
||||
/// existing properties and then edited before calling update.
|
||||
/// Returns a Msg to the view
|
||||
fn update(&mut self, props: Props) -> Msg {
|
||||
// Reset choices
|
||||
self.states
|
||||
.make_choices(props.texts.rows.as_ref().unwrap_or(&Vec::new()));
|
||||
// Get value
|
||||
if let PropValue::Unsigned(choice) = props.value {
|
||||
self.states.choice = choice;
|
||||
}
|
||||
self.props = props;
|
||||
// Msg none
|
||||
Msg::None
|
||||
}
|
||||
|
||||
/// ### get_props
|
||||
///
|
||||
/// Returns a props builder starting from component properties.
|
||||
/// This returns a prop builder in order to make easier to create
|
||||
/// new properties for the element.
|
||||
fn get_props(&self) -> PropsBuilder {
|
||||
PropsBuilder::from(self.props.clone())
|
||||
}
|
||||
|
||||
/// ### on
|
||||
///
|
||||
/// Handle input event and update internal states.
|
||||
/// Returns a Msg to the view
|
||||
fn on(&mut self, ev: InputEvent) -> Msg {
|
||||
// Match event
|
||||
if let InputEvent::Key(key) = ev {
|
||||
match key.code {
|
||||
KeyCode::Right => {
|
||||
// Increment choice
|
||||
self.states.next_choice();
|
||||
// Return Msg On Change
|
||||
Msg::OnChange(self.get_value())
|
||||
}
|
||||
KeyCode::Left => {
|
||||
// Decrement choice
|
||||
self.states.prev_choice();
|
||||
// Return Msg On Change
|
||||
Msg::OnChange(self.get_value())
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
// Return Submit
|
||||
Msg::OnSubmit(self.get_value())
|
||||
}
|
||||
_ => {
|
||||
// Return key event to activity
|
||||
Msg::OnKey(key)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Ignore event
|
||||
Msg::None
|
||||
}
|
||||
}
|
||||
|
||||
/// ### get_value
|
||||
///
|
||||
/// Get current value from component
|
||||
/// Returns the selected option
|
||||
fn get_value(&self) -> Payload {
|
||||
Payload::Unsigned(self.states.choice)
|
||||
}
|
||||
|
||||
// -- events
|
||||
|
||||
/// ### blur
|
||||
///
|
||||
/// Blur component; basically remove focus
|
||||
fn blur(&mut self) {
|
||||
self.states.focus = false;
|
||||
}
|
||||
|
||||
/// ### active
|
||||
///
|
||||
/// Active component; basically give focus
|
||||
fn active(&mut self) {
|
||||
self.states.focus = true;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::ui::layout::props::{TextParts, TextSpan};
|
||||
|
||||
use crossterm::event::KeyEvent;
|
||||
|
||||
#[test]
|
||||
fn test_ui_layout_components_radio() {
|
||||
// Make component
|
||||
let mut component: RadioGroup = RadioGroup::new(
|
||||
PropsBuilder::default()
|
||||
.with_texts(TextParts::new(
|
||||
Some(String::from("yes or no?")),
|
||||
Some(vec![
|
||||
TextSpan::from("Yes!"),
|
||||
TextSpan::from("No"),
|
||||
TextSpan::from("Maybe"),
|
||||
]),
|
||||
))
|
||||
.with_value(PropValue::Unsigned(1))
|
||||
.build(),
|
||||
);
|
||||
// Verify states
|
||||
assert_eq!(component.states.choice, 1);
|
||||
assert_eq!(component.states.choices.len(), 3);
|
||||
// Focus
|
||||
assert_eq!(component.states.focus, false);
|
||||
component.active();
|
||||
assert_eq!(component.states.focus, true);
|
||||
component.blur();
|
||||
assert_eq!(component.states.focus, false);
|
||||
// Update
|
||||
let props = component.get_props().with_foreground(Color::Red).build();
|
||||
assert_eq!(component.update(props), Msg::None);
|
||||
assert_eq!(component.props.foreground, Color::Red);
|
||||
// Get value
|
||||
assert_eq!(component.get_value(), Payload::Unsigned(1));
|
||||
// Handle events
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Left))),
|
||||
Msg::OnChange(Payload::Unsigned(0)),
|
||||
);
|
||||
assert_eq!(component.get_value(), Payload::Unsigned(0));
|
||||
// Left again
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Left))),
|
||||
Msg::OnChange(Payload::Unsigned(0)),
|
||||
);
|
||||
assert_eq!(component.get_value(), Payload::Unsigned(0));
|
||||
// Right
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Right))),
|
||||
Msg::OnChange(Payload::Unsigned(1)),
|
||||
);
|
||||
assert_eq!(component.get_value(), Payload::Unsigned(1));
|
||||
// Right again
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Right))),
|
||||
Msg::OnChange(Payload::Unsigned(2)),
|
||||
);
|
||||
assert_eq!(component.get_value(), Payload::Unsigned(2));
|
||||
// Right again
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Right))),
|
||||
Msg::OnChange(Payload::Unsigned(2)),
|
||||
);
|
||||
assert_eq!(component.get_value(), Payload::Unsigned(2));
|
||||
// Submit
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Enter))),
|
||||
Msg::OnSubmit(Payload::Unsigned(2)),
|
||||
);
|
||||
// Any key
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Char('a')))),
|
||||
Msg::OnKey(KeyEvent::from(KeyCode::Char('a'))),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
//! ## TextList
|
||||
//!
|
||||
//! `TextList` component renders a radio group
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// locals
|
||||
use super::{Canvas, Component, InputEvent, Msg, Payload, Props, PropsBuilder};
|
||||
// ext
|
||||
use tui::{
|
||||
layout::{Corner, Rect},
|
||||
style::Style,
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, BorderType, List, ListItem},
|
||||
};
|
||||
|
||||
// -- component
|
||||
|
||||
/// ## Table
|
||||
///
|
||||
/// Table is a table component. List n rows with n text span columns
|
||||
pub struct Table {
|
||||
props: Props,
|
||||
}
|
||||
|
||||
impl Table {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiate a new Table component
|
||||
pub fn new(props: Props) -> Self {
|
||||
Table { props }
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Table {
|
||||
/// ### render
|
||||
///
|
||||
/// Based on the current properties and states, renders a widget using the provided render engine in the provided Area
|
||||
/// If focused, cursor is also set (if supported by widget)
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
fn render(&self, render: &mut Canvas, area: Rect) {
|
||||
// Make a Span
|
||||
if self.props.visible {
|
||||
let title: String = match self.props.texts.title.as_ref() {
|
||||
Some(t) => t.clone(),
|
||||
None => String::new(),
|
||||
};
|
||||
// Make list entries
|
||||
let list_items: Vec<ListItem> = match self.props.texts.table.as_ref() {
|
||||
None => Vec::new(),
|
||||
Some(table) => table
|
||||
.iter()
|
||||
.map(|row| {
|
||||
let columns: Vec<Span> = row
|
||||
.iter()
|
||||
.map(|col| {
|
||||
Span::styled(
|
||||
col.content.clone(),
|
||||
Style::default()
|
||||
.add_modifier(col.get_modifiers())
|
||||
.fg(col.fg)
|
||||
.bg(col.bg),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
ListItem::new(Spans::from(columns))
|
||||
})
|
||||
.collect(), // Make List item from TextSpan
|
||||
};
|
||||
// Make list
|
||||
render.render_widget(
|
||||
List::new(list_items)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(self.props.borders)
|
||||
.border_style(Style::default())
|
||||
.border_type(BorderType::Rounded)
|
||||
.title(title),
|
||||
)
|
||||
.start_corner(Corner::TopLeft),
|
||||
area,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// ### update
|
||||
///
|
||||
/// Update component properties
|
||||
/// Properties should first be retrieved through `get_props` which creates a builder from
|
||||
/// existing properties and then edited before calling update.
|
||||
/// Returns a Msg to the view
|
||||
fn update(&mut self, props: Props) -> Msg {
|
||||
self.props = props;
|
||||
// Return None
|
||||
Msg::None
|
||||
}
|
||||
|
||||
/// ### get_props
|
||||
///
|
||||
/// Returns a props builder starting from component properties.
|
||||
/// This returns a prop builder in order to make easier to create
|
||||
/// new properties for the element.
|
||||
fn get_props(&self) -> PropsBuilder {
|
||||
PropsBuilder::from(self.props.clone())
|
||||
}
|
||||
|
||||
/// ### on
|
||||
///
|
||||
/// Handle input event and update internal states.
|
||||
/// Returns a Msg to the view.
|
||||
/// Returns always None, since cannot have any focus
|
||||
fn on(&mut self, ev: InputEvent) -> Msg {
|
||||
// Return key
|
||||
if let InputEvent::Key(key) = ev {
|
||||
Msg::OnKey(key)
|
||||
} else {
|
||||
Msg::None
|
||||
}
|
||||
}
|
||||
|
||||
/// ### get_value
|
||||
///
|
||||
/// Get current value from component
|
||||
/// For this component returns always None
|
||||
fn get_value(&self) -> Payload {
|
||||
Payload::None
|
||||
}
|
||||
|
||||
// -- events
|
||||
|
||||
/// ### blur
|
||||
///
|
||||
/// Blur component
|
||||
fn blur(&mut self) {}
|
||||
|
||||
/// ### active
|
||||
///
|
||||
/// Active component
|
||||
fn active(&mut self) {}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::ui::layout::props::{TableBuilder, TextParts, TextSpan};
|
||||
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use tui::style::Color;
|
||||
|
||||
#[test]
|
||||
fn test_ui_layout_components_table() {
|
||||
let mut component: Table = Table::new(
|
||||
PropsBuilder::default()
|
||||
.with_texts(TextParts::table(
|
||||
Some(String::from("My data")),
|
||||
TableBuilder::default()
|
||||
.add_col(TextSpan::from("name"))
|
||||
.add_col(TextSpan::from("age"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::from("omar"))
|
||||
.add_col(TextSpan::from("24"))
|
||||
.build(),
|
||||
))
|
||||
.build(),
|
||||
);
|
||||
component.active();
|
||||
component.blur();
|
||||
// Update
|
||||
let props = component.get_props().with_foreground(Color::Red).build();
|
||||
assert_eq!(component.update(props), Msg::None);
|
||||
assert_eq!(component.props.foreground, Color::Red);
|
||||
// Get value
|
||||
assert_eq!(component.get_value(), Payload::None);
|
||||
// Event
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Delete))),
|
||||
Msg::OnKey(KeyEvent::from(KeyCode::Delete))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
//! ## Text
|
||||
//!
|
||||
//! `Text` component renders a simple readonly no event associated text
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// locals
|
||||
use super::{Canvas, Component, InputEvent, Msg, Payload, Props, PropsBuilder};
|
||||
// ext
|
||||
use tui::{
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
text::{Span, Spans, Text as TuiText},
|
||||
widgets::Paragraph,
|
||||
};
|
||||
|
||||
// -- component
|
||||
|
||||
pub struct Text {
|
||||
props: Props,
|
||||
}
|
||||
|
||||
impl Text {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiate a new Text component
|
||||
pub fn new(props: Props) -> Self {
|
||||
Text { props }
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Text {
|
||||
/// ### render
|
||||
///
|
||||
/// Based on the current properties and states, renders a widget using the provided render engine in the provided Area
|
||||
/// If focused, cursor is also set (if supported by widget)
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
fn render(&self, render: &mut Canvas, area: Rect) {
|
||||
// Make a Span
|
||||
if self.props.visible {
|
||||
let spans: Vec<Span> = match self.props.texts.rows.as_ref() {
|
||||
None => Vec::new(),
|
||||
Some(rows) => rows
|
||||
.iter()
|
||||
.map(|x| {
|
||||
// Keep line color, or use default
|
||||
let fg: Color = match x.fg {
|
||||
Color::Reset => self.props.foreground,
|
||||
_ => x.fg,
|
||||
};
|
||||
let bg: Color = match x.bg {
|
||||
Color::Reset => self.props.background,
|
||||
_ => x.bg,
|
||||
};
|
||||
Span::styled(
|
||||
x.content.clone(),
|
||||
Style::default()
|
||||
.add_modifier(x.get_modifiers())
|
||||
.fg(fg)
|
||||
.bg(bg),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
// Make text
|
||||
let mut text: TuiText = TuiText::from(Spans::from(spans));
|
||||
// Apply style
|
||||
text.patch_style(
|
||||
Style::default()
|
||||
//.add_modifier(self.props.get_modifiers())
|
||||
//.fg(self.props.foreground) NOTE: don't style twice !!!
|
||||
//.bg(self.props.background),
|
||||
);
|
||||
render.render_widget(Paragraph::new(text), area);
|
||||
}
|
||||
}
|
||||
|
||||
/// ### update
|
||||
///
|
||||
/// Update component properties
|
||||
/// Properties should first be retrieved through `get_props` which creates a builder from
|
||||
/// existing properties and then edited before calling update.
|
||||
/// Returns a Msg to the view
|
||||
fn update(&mut self, props: Props) -> Msg {
|
||||
self.props = props;
|
||||
// Return None
|
||||
Msg::None
|
||||
}
|
||||
|
||||
/// ### get_props
|
||||
///
|
||||
/// Returns a props builder starting from component properties.
|
||||
/// This returns a prop builder in order to make easier to create
|
||||
/// new properties for the element.
|
||||
fn get_props(&self) -> PropsBuilder {
|
||||
PropsBuilder::from(self.props.clone())
|
||||
}
|
||||
|
||||
/// ### on
|
||||
///
|
||||
/// Handle input event and update internal states.
|
||||
/// Returns a Msg to the view.
|
||||
/// Returns always None, since cannot have any focus
|
||||
fn on(&mut self, ev: InputEvent) -> Msg {
|
||||
// Return key
|
||||
if let InputEvent::Key(key) = ev {
|
||||
Msg::OnKey(key)
|
||||
} else {
|
||||
Msg::None
|
||||
}
|
||||
}
|
||||
|
||||
/// ### get_value
|
||||
///
|
||||
/// Get current value from component
|
||||
/// For this component returns always None
|
||||
fn get_value(&self) -> Payload {
|
||||
Payload::None
|
||||
}
|
||||
|
||||
// -- events
|
||||
|
||||
/// ### blur
|
||||
///
|
||||
/// Blur component
|
||||
fn blur(&mut self) {}
|
||||
|
||||
/// ### active
|
||||
///
|
||||
/// Active component
|
||||
fn active(&mut self) {}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::ui::layout::props::{TextParts, TextSpan, TextSpanBuilder};
|
||||
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use tui::style::Color;
|
||||
|
||||
#[test]
|
||||
fn test_ui_layout_components_text() {
|
||||
let mut component: Text = Text::new(
|
||||
PropsBuilder::default()
|
||||
.with_texts(TextParts::new(
|
||||
None,
|
||||
Some(vec![
|
||||
TextSpan::from("Press "),
|
||||
TextSpanBuilder::new("<ESC>")
|
||||
.with_foreground(Color::Cyan)
|
||||
.bold()
|
||||
.build(),
|
||||
TextSpan::from(" to quit"),
|
||||
]),
|
||||
))
|
||||
.build(),
|
||||
);
|
||||
component.active();
|
||||
component.blur();
|
||||
// Update
|
||||
let props = component.get_props().with_foreground(Color::Red).build();
|
||||
assert_eq!(component.update(props), Msg::None);
|
||||
assert_eq!(component.props.foreground, Color::Red);
|
||||
// Get value
|
||||
assert_eq!(component.get_value(), Payload::None);
|
||||
// Event
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Delete))),
|
||||
Msg::OnKey(KeyEvent::from(KeyCode::Delete))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
//! ## Title
|
||||
//!
|
||||
//! `Title` component renders a simple readonly no event associated title
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// locals
|
||||
use super::{Canvas, Component, InputEvent, Msg, Payload, Props, PropsBuilder};
|
||||
// ext
|
||||
use tui::{layout::Rect, style::Style, widgets::Paragraph};
|
||||
|
||||
// -- component
|
||||
|
||||
pub struct Title {
|
||||
props: Props,
|
||||
}
|
||||
|
||||
impl Title {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiate a new Title component
|
||||
pub fn new(props: Props) -> Self {
|
||||
Title { props }
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Title {
|
||||
/// ### render
|
||||
///
|
||||
/// Based on the current properties and states, renders a widget using the provided render engine in the provided Area
|
||||
/// If focused, cursor is also set (if supported by widget)
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
fn render(&self, render: &mut Canvas, area: Rect) {
|
||||
// Make a Span
|
||||
if self.props.visible {
|
||||
let title: String = match self.props.texts.title.as_ref() {
|
||||
None => String::new(),
|
||||
Some(t) => t.clone(),
|
||||
};
|
||||
render.render_widget(
|
||||
Paragraph::new(title).style(
|
||||
Style::default()
|
||||
.fg(self.props.foreground)
|
||||
.bg(self.props.background)
|
||||
.add_modifier(self.props.get_modifiers()),
|
||||
),
|
||||
area,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// ### update
|
||||
///
|
||||
/// Update component properties
|
||||
/// Properties should first be retrieved through `get_props` which creates a builder from
|
||||
/// existing properties and then edited before calling update.
|
||||
/// Returns a Msg to the view
|
||||
fn update(&mut self, props: Props) -> Msg {
|
||||
self.props = props;
|
||||
// Return None
|
||||
Msg::None
|
||||
}
|
||||
|
||||
/// ### get_props
|
||||
///
|
||||
/// Returns a props builder starting from component properties.
|
||||
/// This returns a prop builder in order to make easier to create
|
||||
/// new properties for the element.
|
||||
fn get_props(&self) -> PropsBuilder {
|
||||
PropsBuilder::from(self.props.clone())
|
||||
}
|
||||
|
||||
/// ### on
|
||||
///
|
||||
/// Handle input event and update internal states.
|
||||
/// Returns a Msg to the view.
|
||||
/// Returns always None, since cannot have any focus
|
||||
fn on(&mut self, ev: InputEvent) -> Msg {
|
||||
// Return key
|
||||
if let InputEvent::Key(key) = ev {
|
||||
Msg::OnKey(key)
|
||||
} else {
|
||||
Msg::None
|
||||
}
|
||||
}
|
||||
|
||||
/// ### get_value
|
||||
///
|
||||
/// Get current value from component
|
||||
/// For this component returns always None
|
||||
fn get_value(&self) -> Payload {
|
||||
Payload::None
|
||||
}
|
||||
|
||||
// -- events
|
||||
|
||||
/// ### blur
|
||||
///
|
||||
/// Blur component
|
||||
fn blur(&mut self) {}
|
||||
|
||||
/// ### active
|
||||
///
|
||||
/// Active component
|
||||
fn active(&mut self) {}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::ui::layout::props::TextParts;
|
||||
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use tui::style::Color;
|
||||
|
||||
#[test]
|
||||
fn test_ui_layout_components_title() {
|
||||
let mut component: Title = Title::new(
|
||||
PropsBuilder::default()
|
||||
.with_texts(TextParts::new(Some(String::from("Title")), None))
|
||||
.build(),
|
||||
);
|
||||
component.active();
|
||||
component.blur();
|
||||
// Update
|
||||
let props = component.get_props().with_foreground(Color::Red).build();
|
||||
assert_eq!(component.update(props), Msg::None);
|
||||
assert_eq!(component.props.foreground, Color::Red);
|
||||
// Get value
|
||||
assert_eq!(component.get_value(), Payload::None);
|
||||
// Event
|
||||
assert_eq!(
|
||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Delete))),
|
||||
Msg::OnKey(KeyEvent::from(KeyCode::Delete))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
//! ## Layout
|
||||
//!
|
||||
//! `Layout` is the module which provides components, view, state and properties to create layouts
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// Modules
|
||||
pub mod components;
|
||||
pub mod props;
|
||||
pub mod utils;
|
||||
pub mod view;
|
||||
|
||||
// locals
|
||||
use props::{PropValue, Props, PropsBuilder};
|
||||
// ext
|
||||
use crossterm::event::Event as InputEvent;
|
||||
use crossterm::event::KeyEvent;
|
||||
use std::io::Stdout;
|
||||
use tui::backend::CrosstermBackend;
|
||||
use tui::layout::Rect;
|
||||
use tui::Frame;
|
||||
|
||||
type Backend = CrosstermBackend<Stdout>;
|
||||
pub(crate) type Canvas<'a> = Frame<'a, Backend>;
|
||||
|
||||
// -- Msg
|
||||
|
||||
/// ## Msg
|
||||
///
|
||||
/// Msg is an enum returned after an event is raised for a certain component
|
||||
/// Yep, I took inspiration from Elm.
|
||||
#[derive(std::fmt::Debug, PartialEq, Eq)]
|
||||
pub enum Msg {
|
||||
OnSubmit(Payload),
|
||||
OnChange(Payload),
|
||||
OnKey(KeyEvent),
|
||||
None,
|
||||
}
|
||||
|
||||
/// ## Payload
|
||||
///
|
||||
/// Payload describes a component value
|
||||
#[derive(std::fmt::Debug, PartialEq, Eq)]
|
||||
pub enum Payload {
|
||||
Text(String),
|
||||
//Signed(isize),
|
||||
Unsigned(usize),
|
||||
None,
|
||||
}
|
||||
|
||||
// -- Component
|
||||
|
||||
/// ## Component
|
||||
///
|
||||
/// Component is a trait which defines the behaviours for a Layout component.
|
||||
/// All layout components must implement a method to render and one to update
|
||||
pub trait Component {
|
||||
/// ### render
|
||||
///
|
||||
/// Based on the current properties and states, renders the component in the provided area frame
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
fn render(&self, frame: &mut Canvas, area: Rect);
|
||||
|
||||
/// ### update
|
||||
///
|
||||
/// Update component properties
|
||||
/// Properties should first be retrieved through `get_props` which creates a builder from
|
||||
/// existing properties and then edited before calling update.
|
||||
/// Returns a Msg to the view
|
||||
fn update(&mut self, props: Props) -> Msg;
|
||||
|
||||
/// ### get_props
|
||||
///
|
||||
/// Returns a props builder starting from component properties.
|
||||
/// This returns a prop builder in order to make easier to create
|
||||
/// new properties for the element.
|
||||
fn get_props(&self) -> PropsBuilder;
|
||||
|
||||
/// ### on
|
||||
///
|
||||
/// Handle input event and update internal states.
|
||||
/// Returns a Msg to the view
|
||||
fn on(&mut self, ev: InputEvent) -> Msg;
|
||||
|
||||
/// ### get_value
|
||||
///
|
||||
/// Get current value from component
|
||||
fn get_value(&self) -> Payload;
|
||||
|
||||
// -- events
|
||||
|
||||
/// ### blur
|
||||
///
|
||||
/// Blur component; basically remove focus
|
||||
fn blur(&mut self);
|
||||
|
||||
/// ### active
|
||||
///
|
||||
/// Active component; basically give focus
|
||||
fn active(&mut self);
|
||||
}
|
||||
@@ -1,779 +0,0 @@
|
||||
//! ## Props
|
||||
//!
|
||||
//! `Props` is the module which defines properties for layout components
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// ext
|
||||
use tui::style::{Color, Modifier};
|
||||
use tui::widgets::Borders;
|
||||
|
||||
// -- Props
|
||||
|
||||
/// ## Props
|
||||
///
|
||||
/// Props holds all the possible properties for a layout component
|
||||
#[derive(Clone)]
|
||||
pub struct Props {
|
||||
// Values
|
||||
pub visible: bool, // Is the element visible ON CREATE?
|
||||
pub foreground: Color, // Foreground color
|
||||
pub background: Color, // Background color
|
||||
pub borders: Borders, // Borders
|
||||
pub bold: bool, // Text bold
|
||||
pub italic: bool, // Italic
|
||||
pub underlined: bool, // Underlined
|
||||
pub input_type: InputType, // Input type
|
||||
pub input_len: Option<usize>, // max input len
|
||||
pub texts: TextParts, // text parts
|
||||
pub value: PropValue, // Initial value
|
||||
}
|
||||
|
||||
impl Default for Props {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
// Values
|
||||
visible: true,
|
||||
foreground: Color::Reset,
|
||||
background: Color::Reset,
|
||||
borders: Borders::ALL,
|
||||
bold: false,
|
||||
italic: false,
|
||||
underlined: false,
|
||||
input_type: InputType::Text,
|
||||
input_len: None,
|
||||
texts: TextParts::default(),
|
||||
value: PropValue::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Props {
|
||||
/// ### get_modifiers
|
||||
///
|
||||
/// Get text modifiers from properties
|
||||
pub fn get_modifiers(&self) -> Modifier {
|
||||
Modifier::empty()
|
||||
| (match self.bold {
|
||||
true => Modifier::BOLD,
|
||||
false => Modifier::empty(),
|
||||
})
|
||||
| (match self.italic {
|
||||
true => Modifier::ITALIC,
|
||||
false => Modifier::empty(),
|
||||
})
|
||||
| (match self.underlined {
|
||||
true => Modifier::UNDERLINED,
|
||||
false => Modifier::empty(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// -- Props builder
|
||||
|
||||
/// ## PropsBuilder
|
||||
///
|
||||
/// Chain constructor for `Props`
|
||||
pub struct PropsBuilder {
|
||||
props: Option<Props>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl PropsBuilder {
|
||||
/// ### build
|
||||
///
|
||||
/// Build Props from builder
|
||||
/// Don't call this method twice for any reasons!
|
||||
pub fn build(&mut self) -> Props {
|
||||
self.props.take().unwrap()
|
||||
}
|
||||
|
||||
/// ### hidden
|
||||
///
|
||||
/// Initialize props with visible set to False
|
||||
pub fn hidden(&mut self) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.visible = false;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// ### visible
|
||||
///
|
||||
/// Initialize props with visible set to True
|
||||
pub fn visible(&mut self) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.visible = true;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// ### with_foreground
|
||||
///
|
||||
/// Set foreground color for component
|
||||
pub fn with_foreground(&mut self, color: Color) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.foreground = color;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// ### with_background
|
||||
///
|
||||
/// Set background color for component
|
||||
pub fn with_background(&mut self, color: Color) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.background = color;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// ### with_borders
|
||||
///
|
||||
/// Set component borders style
|
||||
pub fn with_borders(&mut self, borders: Borders) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.borders = borders;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// ### bold
|
||||
///
|
||||
/// Set bold property for component
|
||||
pub fn bold(&mut self) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.bold = true;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// ### italic
|
||||
///
|
||||
/// Set italic property for component
|
||||
pub fn italic(&mut self) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.italic = true;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// ### underlined
|
||||
///
|
||||
/// Set underlined property for component
|
||||
pub fn underlined(&mut self) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.underlined = true;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// ### with_texts
|
||||
///
|
||||
/// Set texts for component
|
||||
pub fn with_texts(&mut self, texts: TextParts) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.texts = texts;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// ### with_input
|
||||
///
|
||||
/// Set input type for component
|
||||
pub fn with_input(&mut self, input_type: InputType) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.input_type = input_type;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// ### with_input_len
|
||||
///
|
||||
/// Set max input len
|
||||
pub fn with_input_len(&mut self, len: usize) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.input_len = Some(len);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// ### with_value
|
||||
///
|
||||
/// Set initial value for component
|
||||
pub fn with_value(&mut self, value: PropValue) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.value = value;
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Props> for PropsBuilder {
|
||||
fn from(props: Props) -> Self {
|
||||
PropsBuilder { props: Some(props) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PropsBuilder {
|
||||
fn default() -> Self {
|
||||
PropsBuilder {
|
||||
props: Some(Props::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- Text parts
|
||||
|
||||
/// ## Table
|
||||
///
|
||||
/// Table represents a list of rows with a list of columns of text spans
|
||||
pub type Table = Vec<Vec<TextSpan>>;
|
||||
|
||||
/// ## TextParts
|
||||
///
|
||||
/// TextParts holds optional component for the text displayed by a component
|
||||
#[derive(Clone)]
|
||||
pub struct TextParts {
|
||||
pub title: Option<String>,
|
||||
pub rows: Option<Vec<TextSpan>>,
|
||||
pub table: Option<Table>, // First vector is rows, inner vec is column
|
||||
}
|
||||
|
||||
impl TextParts {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiates a new TextParts entity
|
||||
pub fn new(title: Option<String>, rows: Option<Vec<TextSpan>>) -> Self {
|
||||
TextParts {
|
||||
title,
|
||||
rows,
|
||||
table: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// ### table
|
||||
///
|
||||
/// Instantiates a new TextParts as a Table
|
||||
pub fn table(title: Option<String>, table: Table) -> Self {
|
||||
TextParts {
|
||||
title,
|
||||
rows: None,
|
||||
table: Some(table),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TextParts {
|
||||
fn default() -> Self {
|
||||
TextParts {
|
||||
title: None,
|
||||
rows: None,
|
||||
table: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ## TableBuilder
|
||||
///
|
||||
/// Table builder is a helper to make it easier to build text tables
|
||||
pub struct TableBuilder {
|
||||
table: Option<Table>,
|
||||
}
|
||||
|
||||
impl TableBuilder {
|
||||
/// ### add_col
|
||||
///
|
||||
/// Add a column to the last row
|
||||
pub fn add_col(&mut self, span: TextSpan) -> &mut Self {
|
||||
if let Some(table) = self.table.as_mut() {
|
||||
if let Some(row) = table.last_mut() {
|
||||
row.push(span);
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// ### add_row
|
||||
///
|
||||
/// Add a new row to the table
|
||||
pub fn add_row(&mut self) -> &mut Self {
|
||||
if let Some(table) = self.table.as_mut() {
|
||||
table.push(vec![]);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// ### build
|
||||
///
|
||||
/// Take table out of builder
|
||||
/// Don't call this method twice for any reasons!
|
||||
pub fn build(&mut self) -> Table {
|
||||
self.table.take().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TableBuilder {
|
||||
fn default() -> Self {
|
||||
TableBuilder {
|
||||
table: Some(vec![vec![]]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### TextSpan
|
||||
///
|
||||
/// TextSpan is a "cell" of text with its attributes
|
||||
#[derive(Clone, std::fmt::Debug)]
|
||||
pub struct TextSpan {
|
||||
pub content: String,
|
||||
pub fg: Color,
|
||||
pub bg: Color,
|
||||
pub bold: bool,
|
||||
pub italic: bool,
|
||||
pub underlined: bool,
|
||||
}
|
||||
|
||||
impl From<&str> for TextSpan {
|
||||
fn from(txt: &str) -> Self {
|
||||
TextSpan {
|
||||
content: txt.to_string(),
|
||||
fg: Color::Reset,
|
||||
bg: Color::Reset,
|
||||
bold: false,
|
||||
italic: false,
|
||||
underlined: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for TextSpan {
|
||||
fn from(content: String) -> Self {
|
||||
TextSpan {
|
||||
content,
|
||||
fg: Color::Reset,
|
||||
bg: Color::Reset,
|
||||
bold: false,
|
||||
italic: false,
|
||||
underlined: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TextSpan {
|
||||
/// ### get_modifiers
|
||||
///
|
||||
/// Get text modifiers from properties
|
||||
pub fn get_modifiers(&self) -> Modifier {
|
||||
Modifier::empty()
|
||||
| (match self.bold {
|
||||
true => Modifier::BOLD,
|
||||
false => Modifier::empty(),
|
||||
})
|
||||
| (match self.italic {
|
||||
true => Modifier::ITALIC,
|
||||
false => Modifier::empty(),
|
||||
})
|
||||
| (match self.underlined {
|
||||
true => Modifier::UNDERLINED,
|
||||
false => Modifier::empty(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// -- TextSpan builder
|
||||
|
||||
/// ## TextSpanBuilder
|
||||
///
|
||||
/// TextSpanBuilder is a struct which helps building quickly a TextSpan
|
||||
pub struct TextSpanBuilder {
|
||||
text: Option<TextSpan>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl TextSpanBuilder {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiate a new TextSpanBuilder
|
||||
pub fn new(text: &str) -> Self {
|
||||
TextSpanBuilder {
|
||||
text: Some(TextSpan::from(text)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### with_foreground
|
||||
///
|
||||
/// Set foreground for text span
|
||||
pub fn with_foreground(&mut self, color: Color) -> &mut Self {
|
||||
if let Some(text) = self.text.as_mut() {
|
||||
text.fg = color;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// ### with_background
|
||||
///
|
||||
/// Set background for text span
|
||||
pub fn with_background(&mut self, color: Color) -> &mut Self {
|
||||
if let Some(text) = self.text.as_mut() {
|
||||
text.bg = color;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// ### italic
|
||||
///
|
||||
/// Set italic for text span
|
||||
pub fn italic(&mut self) -> &mut Self {
|
||||
if let Some(text) = self.text.as_mut() {
|
||||
text.italic = true;
|
||||
}
|
||||
self
|
||||
}
|
||||
/// ### bold
|
||||
///
|
||||
/// Set bold for text span
|
||||
pub fn bold(&mut self) -> &mut Self {
|
||||
if let Some(text) = self.text.as_mut() {
|
||||
text.bold = true;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// ### underlined
|
||||
///
|
||||
/// Set underlined for text span
|
||||
pub fn underlined(&mut self) -> &mut Self {
|
||||
if let Some(text) = self.text.as_mut() {
|
||||
text.underlined = true;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// ### build
|
||||
///
|
||||
/// Make TextSpan out of builder
|
||||
/// Don't call this method twice for any reasons!
|
||||
pub fn build(&mut self) -> TextSpan {
|
||||
self.text.take().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
// -- Prop value
|
||||
|
||||
/// ### PropValue
|
||||
///
|
||||
/// PropValue describes a property initial value
|
||||
#[derive(Clone, PartialEq, std::fmt::Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub enum PropValue {
|
||||
Str(String),
|
||||
Unsigned(usize),
|
||||
Signed(isize),
|
||||
Float(f64),
|
||||
Boolean(bool),
|
||||
None,
|
||||
}
|
||||
|
||||
// -- Input Type
|
||||
|
||||
/// ## InputType
|
||||
///
|
||||
/// Input type for text inputs
|
||||
#[derive(Clone, Copy, PartialEq, std::fmt::Debug)]
|
||||
pub enum InputType {
|
||||
Text,
|
||||
Number,
|
||||
Password,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_ui_layout_props_default() {
|
||||
let props: Props = Props::default();
|
||||
assert_eq!(props.visible, true);
|
||||
assert_eq!(props.background, Color::Reset);
|
||||
assert_eq!(props.foreground, Color::Reset);
|
||||
assert_eq!(props.borders, Borders::ALL);
|
||||
assert_eq!(props.bold, false);
|
||||
assert_eq!(props.italic, false);
|
||||
assert_eq!(props.underlined, false);
|
||||
assert!(props.texts.title.is_none());
|
||||
assert_eq!(props.input_type, InputType::Text);
|
||||
assert!(props.input_len.is_none());
|
||||
assert_eq!(props.value, PropValue::None);
|
||||
assert!(props.texts.rows.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ui_layout_props_modifiers() {
|
||||
// Make properties
|
||||
let props: Props = PropsBuilder::default().bold().italic().underlined().build();
|
||||
// Get modifiers
|
||||
let modifiers: Modifier = props.get_modifiers();
|
||||
assert!(modifiers.intersects(Modifier::BOLD));
|
||||
assert!(modifiers.intersects(Modifier::ITALIC));
|
||||
assert!(modifiers.intersects(Modifier::UNDERLINED));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ui_layout_props_builder() {
|
||||
let props: Props = PropsBuilder::default()
|
||||
.hidden()
|
||||
.with_background(Color::Blue)
|
||||
.with_foreground(Color::Green)
|
||||
.with_borders(Borders::BOTTOM)
|
||||
.bold()
|
||||
.italic()
|
||||
.underlined()
|
||||
.with_texts(TextParts::new(
|
||||
Some(String::from("hello")),
|
||||
Some(vec![TextSpan::from("hey")]),
|
||||
))
|
||||
.with_input(InputType::Password)
|
||||
.with_input_len(16)
|
||||
.with_value(PropValue::Str(String::from("Hello")))
|
||||
.build();
|
||||
assert_eq!(props.background, Color::Blue);
|
||||
assert_eq!(props.borders, Borders::BOTTOM);
|
||||
assert_eq!(props.bold, true);
|
||||
assert_eq!(props.foreground, Color::Green);
|
||||
assert_eq!(props.italic, true);
|
||||
assert_eq!(props.texts.title.as_ref().unwrap().as_str(), "hello");
|
||||
assert_eq!(props.input_type, InputType::Password);
|
||||
assert_eq!(*props.input_len.as_ref().unwrap(), 16);
|
||||
if let PropValue::Str(s) = props.value {
|
||||
assert_eq!(s.as_str(), "Hello");
|
||||
} else {
|
||||
panic!("Expected value to be a string");
|
||||
}
|
||||
assert_eq!(
|
||||
props
|
||||
.texts
|
||||
.rows
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get(0)
|
||||
.unwrap()
|
||||
.content
|
||||
.as_str(),
|
||||
"hey"
|
||||
);
|
||||
assert_eq!(props.underlined, true);
|
||||
assert_eq!(props.visible, false);
|
||||
let props: Props = PropsBuilder::default()
|
||||
.visible()
|
||||
.with_background(Color::Blue)
|
||||
.with_foreground(Color::Green)
|
||||
.bold()
|
||||
.italic()
|
||||
.underlined()
|
||||
.with_texts(TextParts::new(
|
||||
Some(String::from("hello")),
|
||||
Some(vec![TextSpan::from("hey")]),
|
||||
))
|
||||
.build();
|
||||
assert_eq!(props.background, Color::Blue);
|
||||
assert_eq!(props.bold, true);
|
||||
assert_eq!(props.foreground, Color::Green);
|
||||
assert_eq!(props.italic, true);
|
||||
assert_eq!(props.texts.title.as_ref().unwrap().as_str(), "hello");
|
||||
assert_eq!(
|
||||
props
|
||||
.texts
|
||||
.rows
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get(0)
|
||||
.unwrap()
|
||||
.content
|
||||
.as_str(),
|
||||
"hey"
|
||||
);
|
||||
assert_eq!(props.underlined, true);
|
||||
assert_eq!(props.visible, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_ui_layout_props_build_twice() {
|
||||
let mut builder: PropsBuilder = PropsBuilder::default();
|
||||
let _ = builder.build();
|
||||
builder
|
||||
.hidden()
|
||||
.with_background(Color::Blue)
|
||||
.with_foreground(Color::Green)
|
||||
.bold()
|
||||
.italic()
|
||||
.underlined()
|
||||
.with_texts(TextParts::new(
|
||||
Some(String::from("hello")),
|
||||
Some(vec![TextSpan::from("hey")]),
|
||||
));
|
||||
// Rebuild
|
||||
let _ = builder.build();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ui_layout_props_builder_from_props() {
|
||||
let props: Props = PropsBuilder::default()
|
||||
.hidden()
|
||||
.with_background(Color::Blue)
|
||||
.with_foreground(Color::Green)
|
||||
.bold()
|
||||
.italic()
|
||||
.underlined()
|
||||
.with_texts(TextParts::new(
|
||||
Some(String::from("hello")),
|
||||
Some(vec![TextSpan::from("hey")]),
|
||||
))
|
||||
.build();
|
||||
// Ok, now make a builder from properties
|
||||
let builder: PropsBuilder = PropsBuilder::from(props);
|
||||
assert!(builder.props.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ui_layout_props_text_parts_with_values() {
|
||||
let parts: TextParts = TextParts::new(
|
||||
Some(String::from("Hello world!")),
|
||||
Some(vec![TextSpan::from("row1"), TextSpan::from("row2")]),
|
||||
);
|
||||
assert_eq!(parts.title.as_ref().unwrap().as_str(), "Hello world!");
|
||||
assert_eq!(
|
||||
parts
|
||||
.rows
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get(0)
|
||||
.unwrap()
|
||||
.content
|
||||
.as_str(),
|
||||
"row1"
|
||||
);
|
||||
assert_eq!(
|
||||
parts
|
||||
.rows
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get(1)
|
||||
.unwrap()
|
||||
.content
|
||||
.as_str(),
|
||||
"row2"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ui_layout_props_text_parts_default() {
|
||||
let parts: TextParts = TextParts::default();
|
||||
assert!(parts.title.is_none());
|
||||
assert!(parts.rows.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ui_layout_props_text_parts_table() {
|
||||
let table: TextParts = TextParts::table(
|
||||
Some(String::from("my data")),
|
||||
TableBuilder::default()
|
||||
.add_col(TextSpan::from("name"))
|
||||
.add_col(TextSpan::from("age"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::from("christian"))
|
||||
.add_col(TextSpan::from("23"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::from("omar"))
|
||||
.add_col(TextSpan::from("25"))
|
||||
.add_row()
|
||||
.add_row()
|
||||
.add_col(TextSpan::from("pippo"))
|
||||
.build(),
|
||||
);
|
||||
// Verify table
|
||||
assert_eq!(table.title.as_ref().unwrap().as_str(), "my data");
|
||||
assert!(table.rows.is_none());
|
||||
assert_eq!(table.table.as_ref().unwrap().len(), 5); // 5 rows
|
||||
assert_eq!(table.table.as_ref().unwrap().get(0).unwrap().len(), 2); // 2 cols
|
||||
assert_eq!(table.table.as_ref().unwrap().get(1).unwrap().len(), 2); // 2 cols
|
||||
assert_eq!(
|
||||
table
|
||||
.table
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get(1)
|
||||
.unwrap()
|
||||
.get(0)
|
||||
.unwrap()
|
||||
.content
|
||||
.as_str(),
|
||||
"christian"
|
||||
); // check content
|
||||
assert_eq!(table.table.as_ref().unwrap().get(2).unwrap().len(), 2); // 2 cols
|
||||
assert_eq!(table.table.as_ref().unwrap().get(3).unwrap().len(), 0); // 0 cols
|
||||
assert_eq!(table.table.as_ref().unwrap().get(4).unwrap().len(), 1); // 1 cols
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ui_layout_props_text_span() {
|
||||
// from str
|
||||
let span: TextSpan = TextSpan::from("Hello!");
|
||||
assert_eq!(span.content.as_str(), "Hello!");
|
||||
assert_eq!(span.bold, false);
|
||||
assert_eq!(span.fg, Color::Reset);
|
||||
assert_eq!(span.bg, Color::Reset);
|
||||
assert_eq!(span.italic, false);
|
||||
assert_eq!(span.underlined, false);
|
||||
// From String
|
||||
let span: TextSpan = TextSpan::from(String::from("omar"));
|
||||
assert_eq!(span.content.as_str(), "omar");
|
||||
assert_eq!(span.bold, false);
|
||||
assert_eq!(span.fg, Color::Reset);
|
||||
assert_eq!(span.bg, Color::Reset);
|
||||
assert_eq!(span.italic, false);
|
||||
assert_eq!(span.underlined, false);
|
||||
// With attributes
|
||||
let span: TextSpan = TextSpanBuilder::new("Error")
|
||||
.with_background(Color::Red)
|
||||
.with_foreground(Color::Black)
|
||||
.bold()
|
||||
.italic()
|
||||
.underlined()
|
||||
.build();
|
||||
assert_eq!(span.content.as_str(), "Error");
|
||||
assert_eq!(span.bold, true);
|
||||
assert_eq!(span.fg, Color::Black);
|
||||
assert_eq!(span.bg, Color::Red);
|
||||
assert_eq!(span.italic, true);
|
||||
assert_eq!(span.underlined, true);
|
||||
// Check modifiers
|
||||
let modifiers: Modifier = span.get_modifiers();
|
||||
assert!(modifiers.intersects(Modifier::BOLD));
|
||||
assert!(modifiers.intersects(Modifier::ITALIC));
|
||||
assert!(modifiers.intersects(Modifier::UNDERLINED));
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
//! ## Utils
|
||||
//!
|
||||
//! `Utils` implements utilities functions to work with layouts
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
use tui::layout::{Constraint, Direction, Layout, Rect};
|
||||
|
||||
/// ### draw_area_in
|
||||
///
|
||||
/// Draw an area (WxH / 3) in the middle of the parent area
|
||||
pub fn draw_area_in(parent: Rect, width: u16, height: u16) -> Rect {
|
||||
let new_area = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage((100 - height) / 2),
|
||||
Constraint::Percentage(height),
|
||||
Constraint::Percentage((100 - height) / 2),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(parent);
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage((100 - width) / 2),
|
||||
Constraint::Percentage(width),
|
||||
Constraint::Percentage((100 - width) / 2),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(new_area[1])[1]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_ui_layout_utils_draw_area_in() {
|
||||
let area: Rect = Rect::new(0, 0, 1024, 512);
|
||||
let child: Rect = draw_area_in(area, 75, 30);
|
||||
assert_eq!(child.x, 43);
|
||||
assert_eq!(child.y, 63);
|
||||
assert_eq!(child.width, 271);
|
||||
assert_eq!(child.height, 54);
|
||||
}
|
||||
}
|
||||
@@ -1,461 +0,0 @@
|
||||
//! ## View
|
||||
//!
|
||||
//! `View` is the module which handles layout components
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// imports
|
||||
use super::{Canvas, Component, InputEvent, Msg, Payload, Props, PropsBuilder, Rect};
|
||||
// ext
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// ## View
|
||||
///
|
||||
/// View is the wrapper and manager for all the components.
|
||||
/// A View is a container for all the components in a certain layout.
|
||||
/// Each View can have only one focused component.
|
||||
pub struct View {
|
||||
components: HashMap<String, Box<dyn Component>>, // all the components in the view
|
||||
focus: Option<String>, // Current active component
|
||||
focus_stack: Vec<String>, // Focus stack; used to give focus in case the current element loses focus
|
||||
}
|
||||
|
||||
// -- view
|
||||
|
||||
impl View {
|
||||
/// ### init
|
||||
///
|
||||
/// Initialize a new `View`
|
||||
pub fn init() -> Self {
|
||||
View {
|
||||
components: HashMap::new(),
|
||||
focus: None,
|
||||
focus_stack: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
// -- mount / umount
|
||||
|
||||
/// ### mount
|
||||
///
|
||||
/// Mount a new component in the view
|
||||
pub fn mount(&mut self, id: &str, component: Box<dyn Component>) {
|
||||
self.components.insert(id.to_string(), component);
|
||||
}
|
||||
|
||||
/// ### umount
|
||||
///
|
||||
/// Umount a component from the view.
|
||||
/// If component has focus, blur component and remove it from the stack
|
||||
pub fn umount(&mut self, id: &str) {
|
||||
// Check if component has focus
|
||||
if let Some(focus) = self.focus.as_ref() {
|
||||
// If has focus, blur component
|
||||
if focus == id {
|
||||
self.blur();
|
||||
}
|
||||
}
|
||||
// Remove component from focus stack
|
||||
self.pop_from_stack(id);
|
||||
self.components.remove(id);
|
||||
}
|
||||
|
||||
// -- render
|
||||
|
||||
/// ### render
|
||||
///
|
||||
/// RenderData component with the provided id
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
pub fn render(&self, id: &str, frame: &mut Canvas, area: Rect) {
|
||||
if let Some(component) = self.components.get(id) {
|
||||
component.render(frame, area);
|
||||
}
|
||||
}
|
||||
|
||||
// -- props
|
||||
|
||||
/// ### get_props
|
||||
///
|
||||
/// Get component properties
|
||||
pub fn get_props(&self, id: &str) -> Option<PropsBuilder> {
|
||||
self.components.get(id).map(|cmp| cmp.get_props())
|
||||
}
|
||||
|
||||
/// update
|
||||
///
|
||||
/// Update component properties
|
||||
/// Returns `None` if component doesn't exist
|
||||
pub fn update(&mut self, id: &str, props: Props) -> Option<(String, Msg)> {
|
||||
self.components
|
||||
.get_mut(id)
|
||||
.map(|cmp| (id.to_string(), cmp.update(props)))
|
||||
}
|
||||
|
||||
// -- state
|
||||
|
||||
/// ### get_value
|
||||
///
|
||||
/// Get component value
|
||||
pub fn get_value(&self, id: &str) -> Option<Payload> {
|
||||
self.components.get(id).map(|cmp| cmp.get_value())
|
||||
}
|
||||
|
||||
// -- events
|
||||
|
||||
/// ### on
|
||||
///
|
||||
/// Handle event for the focused component (if any)
|
||||
/// Returns `None` if no component is focused
|
||||
pub fn on(&mut self, ev: InputEvent) -> Option<(String, Msg)> {
|
||||
match self.focus.as_ref() {
|
||||
None => None,
|
||||
Some(id) => self
|
||||
.components
|
||||
.get_mut(id)
|
||||
.map(|cmp| (id.to_string(), cmp.on(ev))),
|
||||
}
|
||||
}
|
||||
|
||||
// -- focus
|
||||
|
||||
/// ### blur
|
||||
///
|
||||
/// Blur selected element AND DON'T PUSH CURRENT ACTIVE ELEMENT INTO THE STACK
|
||||
/// Last element in stack becomes active and is removed from the stack
|
||||
pub fn blur(&mut self) {
|
||||
if let Some(component) = self.focus.take() {
|
||||
// Blur component
|
||||
if let Some(cmp) = self.components.get_mut(component.as_str()) {
|
||||
cmp.blur();
|
||||
}
|
||||
// Set last element in the stack as active
|
||||
let mut new: Option<String> = None;
|
||||
if let Some(last) = self.focus_stack.last() {
|
||||
// Set focus to last element
|
||||
new = Some(last.clone());
|
||||
self.focus = Some(last.clone());
|
||||
// Active
|
||||
if let Some(new) = self.components.get_mut(last) {
|
||||
new.active();
|
||||
}
|
||||
}
|
||||
// Pop element from stack
|
||||
if let Some(new) = new {
|
||||
self.pop_from_stack(new.as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### active
|
||||
///
|
||||
/// Active provided element
|
||||
/// Current active component, if any, GETS PUSHED to the STACK
|
||||
pub fn active(&mut self, component: &str) {
|
||||
// Active component if exists
|
||||
if let Some(cmp) = self.components.get_mut(component) {
|
||||
// Active component
|
||||
cmp.active();
|
||||
// Put current focus if any, into the stack
|
||||
if let Some(active_component) = self.focus.take() {
|
||||
if active_component != component {
|
||||
// Blur active component if are different
|
||||
if let Some(active_component) =
|
||||
self.components.get_mut(active_component.as_str())
|
||||
{
|
||||
active_component.blur();
|
||||
}
|
||||
}
|
||||
self.push_to_stack(active_component.as_str());
|
||||
}
|
||||
// Give focus to component
|
||||
self.focus = Some(component.to_string());
|
||||
// Remove new component from the stack
|
||||
self.pop_from_stack(component);
|
||||
}
|
||||
}
|
||||
|
||||
// -- private
|
||||
|
||||
/// ### push_to_stack
|
||||
///
|
||||
/// Push component to stack; first remove it from the stack if any
|
||||
fn push_to_stack(&mut self, name: &str) {
|
||||
self.pop_from_stack(name);
|
||||
self.focus_stack.push(name.to_string());
|
||||
}
|
||||
|
||||
/// ### pop_from_stack
|
||||
///
|
||||
/// Pop element from focus stack
|
||||
fn pop_from_stack(&mut self, name: &str) {
|
||||
self.focus_stack.retain(|c| c.as_str() != name);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
use super::super::components::input::Input;
|
||||
use super::super::components::text::Text;
|
||||
use super::super::props::{PropValue, TextParts, TextSpan};
|
||||
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
|
||||
#[test]
|
||||
fn test_ui_layout_view_init() {
|
||||
let view: View = View::init();
|
||||
// Verify view
|
||||
assert_eq!(view.components.len(), 0);
|
||||
assert!(view.focus.is_none());
|
||||
assert_eq!(view.focus_stack.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ui_layout_view_mount_umount() {
|
||||
let mut view: View = View::init();
|
||||
// Mount component
|
||||
let input: &str = "INPUT";
|
||||
view.mount(input, make_component_input());
|
||||
// Verify is mounted
|
||||
assert!(view.components.get(input).is_some());
|
||||
// Mount another
|
||||
let text: &str = "TEXT";
|
||||
view.mount(text, make_component_text());
|
||||
assert!(view.components.get(text).is_some());
|
||||
assert_eq!(view.components.len(), 2);
|
||||
// Verify you cannot have duplicates
|
||||
view.mount(input, make_component_input());
|
||||
assert_eq!(view.components.len(), 2); // length should still be 2
|
||||
// Umount
|
||||
view.umount(text);
|
||||
assert_eq!(view.components.len(), 1);
|
||||
assert!(view.components.get(text).is_none());
|
||||
}
|
||||
|
||||
/*
|
||||
#[test]
|
||||
fn test_ui_layout_view_mount_render() {
|
||||
let mut view: View = View::init();
|
||||
// Mount component
|
||||
let input: &str = "INPUT";
|
||||
view.mount(input, make_component_input());
|
||||
assert!(view.render(input).is_some());
|
||||
assert!(view.render("unexisting").is_none());
|
||||
}
|
||||
*/
|
||||
|
||||
#[test]
|
||||
fn test_ui_layout_view_focus() {
|
||||
let mut view: View = View::init();
|
||||
// Prepare ids
|
||||
let input1: &str = "INPUT_1";
|
||||
let input2: &str = "INPUT_2";
|
||||
let input3: &str = "INPUT_3";
|
||||
let text1: &str = "TEXT_1";
|
||||
let text2: &str = "TEXT_2";
|
||||
// Mount components
|
||||
view.mount(input1, make_component_input());
|
||||
view.mount(input2, make_component_input());
|
||||
view.mount(input3, make_component_input());
|
||||
view.mount(text1, make_component_text());
|
||||
view.mount(text2, make_component_text());
|
||||
// Verify focus
|
||||
assert!(view.focus.is_none());
|
||||
assert_eq!(view.focus_stack.len(), 0);
|
||||
// Blur when nothing is selected
|
||||
view.blur();
|
||||
assert!(view.focus.is_none());
|
||||
assert_eq!(view.focus_stack.len(), 0);
|
||||
// Active unexisting component
|
||||
view.active("UNEXISTING-COMPONENT");
|
||||
assert!(view.focus.is_none());
|
||||
assert_eq!(view.focus_stack.len(), 0);
|
||||
// Give focus to a component
|
||||
view.active(input1);
|
||||
// Check focus
|
||||
assert_eq!(view.focus.as_ref().unwrap().as_str(), input1);
|
||||
assert_eq!(view.focus_stack.len(), 0); // NOTE: stack is empty until a focus gets blurred
|
||||
// Active a new component
|
||||
view.active(input2);
|
||||
// Now focus should be on input2, but input 1 should be in the focus stack
|
||||
assert_eq!(view.focus.as_ref().unwrap().as_str(), input2);
|
||||
assert_eq!(view.focus_stack.len(), 1);
|
||||
assert_eq!(view.focus_stack[0].as_str(), input1);
|
||||
// Active input 3
|
||||
view.active(input3);
|
||||
// now focus should be hold by input3, and stack should have len 2
|
||||
assert_eq!(view.focus.as_ref().unwrap().as_str(), input3);
|
||||
assert_eq!(view.focus_stack.len(), 2);
|
||||
assert_eq!(view.focus_stack[0].as_str(), input1);
|
||||
assert_eq!(view.focus_stack[1].as_str(), input2);
|
||||
// blur
|
||||
view.blur();
|
||||
// Focus should now be hold by input2; input 3 should NOT be in the stack
|
||||
assert_eq!(view.focus.as_ref().unwrap().as_str(), input2);
|
||||
assert_eq!(view.focus_stack.len(), 1);
|
||||
assert_eq!(view.focus_stack[0].as_str(), input1);
|
||||
// Active twice
|
||||
view.active(input2);
|
||||
// Nothing should have changed
|
||||
assert_eq!(view.focus.as_ref().unwrap().as_str(), input2);
|
||||
assert_eq!(view.focus_stack.len(), 1);
|
||||
assert_eq!(view.focus_stack[0].as_str(), input1);
|
||||
// Blur again; stack should become empty, whether focus should then be hold by input 1
|
||||
view.blur();
|
||||
assert_eq!(view.focus.as_ref().unwrap().as_str(), input1);
|
||||
assert_eq!(view.focus_stack.len(), 0);
|
||||
// Blur again; now everything should be none
|
||||
view.blur();
|
||||
assert!(view.focus.is_none());
|
||||
assert_eq!(view.focus_stack.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ui_layout_view_focus_umount() {
|
||||
let mut view: View = View::init();
|
||||
// Mount component
|
||||
let input: &str = "INPUT";
|
||||
let text: &str = "TEXT";
|
||||
let text2: &str = "TEXT2";
|
||||
view.mount(input, make_component_input());
|
||||
view.mount(text, make_component_text());
|
||||
view.mount(text2, make_component_text());
|
||||
// Give focus to input
|
||||
view.active(input);
|
||||
// Give focus to text
|
||||
view.active(text);
|
||||
view.active(text2);
|
||||
// Stack should have 1 element
|
||||
assert_eq!(view.focus_stack.len(), 2);
|
||||
// Focus should be some
|
||||
assert!(view.focus.is_some());
|
||||
// Umount text
|
||||
view.umount(text2);
|
||||
// Focus should now be hold by 'text'; stack should now have size 1
|
||||
assert_eq!(view.focus.as_ref().unwrap(), text);
|
||||
assert_eq!(view.focus_stack.len(), 1);
|
||||
// Umount input
|
||||
view.umount(input);
|
||||
assert_eq!(view.focus.as_ref().unwrap(), text);
|
||||
assert_eq!(view.focus_stack.len(), 0);
|
||||
// Umount text
|
||||
view.umount(text);
|
||||
assert!(view.focus.is_none());
|
||||
assert_eq!(view.focus_stack.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ui_layout_view_update() {
|
||||
let mut view: View = View::init();
|
||||
// Prepare ids
|
||||
let text: &str = "TEXT";
|
||||
// Mount text
|
||||
view.mount(text, make_component_text());
|
||||
// Get properties and update
|
||||
let props: Props = view.get_props(text).unwrap().build();
|
||||
// Verify bold is false
|
||||
assert_eq!(props.bold, false);
|
||||
// Update properties and set bold to true
|
||||
let mut builder = view.get_props(text).unwrap();
|
||||
let (id, msg) = view.update(text, builder.bold().build()).unwrap();
|
||||
// Verify return values
|
||||
assert_eq!(id, text);
|
||||
assert_eq!(msg, Msg::None);
|
||||
// Verify bold is now true
|
||||
let props: Props = view.get_props(text).unwrap().build();
|
||||
// Verify bold is false
|
||||
assert_eq!(props.bold, true);
|
||||
// Get properties for unexisting component
|
||||
assert!(view.update("foobar", props).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ui_layout_view_on() {
|
||||
let mut view: View = View::init();
|
||||
// Prepare ids
|
||||
let text: &str = "TEXT";
|
||||
let input: &str = "INPUT";
|
||||
// Mount
|
||||
view.mount(text, make_component_text());
|
||||
view.mount(input, make_component_input());
|
||||
// Verify current value
|
||||
assert_eq!(view.get_value(text).unwrap(), Payload::None); // Text value is Nothing
|
||||
assert_eq!(
|
||||
view.get_value(input).unwrap(),
|
||||
Payload::Text(String::from("text"))
|
||||
); // Defined in `make_component_input`
|
||||
// Handle events WITHOUT ANY ACTIVE ELEMENT
|
||||
assert!(view
|
||||
.on(InputEvent::Key(KeyEvent::from(KeyCode::Enter)))
|
||||
.is_none());
|
||||
// Active input
|
||||
view.active(input);
|
||||
// Now handle events on input
|
||||
// Try char
|
||||
assert_eq!(
|
||||
view.on(InputEvent::Key(KeyEvent::from(KeyCode::Char('1'))))
|
||||
.unwrap(),
|
||||
(input.to_string(), Msg::None)
|
||||
);
|
||||
// Verify new value
|
||||
assert_eq!(
|
||||
view.get_value(input).unwrap(),
|
||||
Payload::Text(String::from("text1"))
|
||||
);
|
||||
// Verify enter
|
||||
assert_eq!(
|
||||
view.on(InputEvent::Key(KeyEvent::from(KeyCode::Enter)))
|
||||
.unwrap(),
|
||||
(
|
||||
input.to_string(),
|
||||
Msg::OnSubmit(Payload::Text(String::from("text1")))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// ### make_component
|
||||
///
|
||||
/// Make a new component; we'll use Input, which uses a rich set of events
|
||||
fn make_component_input() -> Box<dyn Component> {
|
||||
Box::new(Input::new(
|
||||
PropsBuilder::default()
|
||||
.with_texts(TextParts::new(Some(String::from("mytext")), None))
|
||||
.with_value(PropValue::Str(String::from("text")))
|
||||
.build(),
|
||||
))
|
||||
}
|
||||
|
||||
fn make_component_text() -> Box<dyn Component> {
|
||||
Box::new(Text::new(
|
||||
PropsBuilder::default()
|
||||
.with_texts(TextParts::new(
|
||||
None,
|
||||
Some(vec![TextSpan::from("Sample text")]),
|
||||
))
|
||||
.build(),
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@
|
||||
*/
|
||||
// Modules
|
||||
pub mod activities;
|
||||
pub(crate) mod components;
|
||||
pub mod context;
|
||||
pub(crate) mod input;
|
||||
pub(crate) mod layout;
|
||||
pub(crate) mod store;
|
||||
|
||||
Reference in New Issue
Block a user