Migrated termscp to tui-realm 1.x

This commit is contained in:
veeso
2021-11-21 10:02:03 +01:00
committed by Christian Visintin
parent 30851a78e8
commit 54b5583d1a
54 changed files with 10994 additions and 7691 deletions

View File

@@ -35,6 +35,16 @@ Released on FIXME:
- It is now possible to keep navigating on the other explorer while "found tab" is open
- ❗ It is not possible though to have the "found tab" on both explorers (otherwise you wouldn't be able to tell whether you're transferring files)
- Files found from search are now displayed with their relative path from working directory
- **Ui**:
- Transfer abortion is now more responsive
- Selected files will now be rendered with **Reversed, underlined and italic** text modifiers instead of being prepended with `*`.
- **Tui-realm migration**:
- migrated application to tui-realm 1.x
- Improved application performance
- Dependencies:
- Updated `tui-realm` to `1.3.0`
- Updated `tui-realm-stdlib` to `1.1.4`
- Removed `crossterm` (since bridged by tui-realm)
## 0.7.0

27
Cargo.lock generated
View File

@@ -1977,7 +1977,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0bccbcf40c8938196944a3da0e133e031a33f4d6b72db3bda3cc556e361905d"
dependencies = [
"lazy_static",
"parking_lot 0.11.2",
"parking_lot 0.10.2",
"serial_test_derive",
]
@@ -2205,7 +2205,6 @@ dependencies = [
"bytesize",
"chrono",
"content_inspector",
"crossterm",
"dirs 4.0.0",
"edit",
"hostname",
@@ -2413,9 +2412,9 @@ dependencies = [
[[package]]
name = "tui-realm-stdlib"
version = "0.6.3"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f159d383b40dec75e0541530bc3416318f5e0a8b6999db9df9b5efa6b122380e"
checksum = "b6444ac3cf88c6cbee4267b6999775aa65ef4ddf556587d2154631d74b5d65fc"
dependencies = [
"textwrap",
"tuirealm",
@@ -2424,12 +2423,28 @@ dependencies = [
[[package]]
name = "tuirealm"
version = "0.6.0"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "634ad8e6a4b80ef032d31356b55964a995da5d05a9cf3a1bd134bae1ba7c197a"
checksum = "69e5c7137a0bd92feadea98033a1849fe51c83d23f7761b866e8700a3d6f1de7"
dependencies = [
"bitflags 1.3.2",
"crossterm",
"lazy_static",
"regex",
"thiserror",
"tui",
"tuirealm_derive",
]
[[package]]
name = "tuirealm_derive"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0adcdaf59881626555558eae08f8a53003c8a1961723b4d7a10c51599abbc81"
dependencies = [
"proc-macro2",
"quote 1.0.9",
"syn 1.0.76",
]
[[package]]

View File

@@ -37,7 +37,6 @@ bitflags = "1.3.2"
bytesize = "1.1.0"
chrono = "0.4.19"
content_inspector = "0.2.4"
crossterm = "0.20"
dirs = "4.0.0"
edit = "0.1.3"
hostname = "0.3.1"
@@ -60,8 +59,8 @@ tempfile = "3.1.0"
textwrap = "0.14.2"
thiserror = "^1.0.0"
toml = "0.5.8"
tui-realm-stdlib = "0.6.3"
tuirealm = "0.6.0"
tui-realm-stdlib = "^1.1.0"
tuirealm = "^1.2.0"
whoami = "1.1.1"
wildmatch = "2.0.0"

View File

@@ -39,7 +39,6 @@ use crate::ui::context::Context;
// Namespaces
use std::path::{Path, PathBuf};
use std::thread::sleep;
use std::time::Duration;
/// ### NextActivity
@@ -56,7 +55,7 @@ pub enum NextActivity {
/// The activity manager takes care of running activities and handling them until the application has ended
pub struct ActivityManager {
context: Option<Context>,
interval: Duration,
ticks: Duration,
local_dir: PathBuf,
}
@@ -64,7 +63,7 @@ impl ActivityManager {
/// ### new
///
/// Initializes a new Activity Manager
pub fn new(local_dir: &Path, interval: Duration) -> Result<ActivityManager, HostError> {
pub fn new(local_dir: &Path, ticks: Duration) -> Result<ActivityManager, HostError> {
// Prepare Context
// Initialize configuration client
let (config_client, error): (ConfigClient, Option<String>) =
@@ -80,7 +79,7 @@ impl ActivityManager {
Ok(ActivityManager {
context: Some(ctx),
local_dir: local_dir.to_path_buf(),
interval,
ticks,
})
}
@@ -123,7 +122,7 @@ impl ActivityManager {
fn run_authentication(&mut self) -> Option<NextActivity> {
info!("Starting AuthActivity...");
// Prepare activity
let mut activity: AuthActivity = AuthActivity::default();
let mut activity: AuthActivity = AuthActivity::new(self.ticks);
// Prepare result
let result: Option<NextActivity>;
// Get context
@@ -162,8 +161,6 @@ impl ActivityManager {
_ => { /* Nothing to do */ }
}
}
// Sleep for ticks
sleep(self.interval);
}
// Destroy activity
self.context = activity.on_destroy();
@@ -205,7 +202,8 @@ impl ActivityManager {
return None;
}
};
let mut activity: FileTransferActivity = FileTransferActivity::new(host, protocol);
let mut activity: FileTransferActivity =
FileTransferActivity::new(host, protocol, self.ticks);
// Prepare result
let result: Option<NextActivity>;
// Create activity
@@ -230,8 +228,6 @@ impl ActivityManager {
_ => { /* Nothing to do */ }
}
}
// Sleep for ticks
sleep(self.interval);
}
// Destroy activity
self.context = activity.on_destroy();
@@ -245,7 +241,7 @@ impl ActivityManager {
/// Returns the next activity to run
fn run_setup(&mut self) -> Option<NextActivity> {
// Prepare activity
let mut activity: SetupActivity = SetupActivity::default();
let mut activity: SetupActivity = SetupActivity::new(self.ticks);
// Get context
let ctx: Context = match self.context.take() {
Some(ctx) => ctx,
@@ -264,8 +260,6 @@ impl ActivityManager {
info!("SetupActivity terminated due to 'Quit'");
break;
}
// Sleep for ticks
sleep(self.interval);
}
// Destroy activity
self.context = activity.on_destroy();

View File

@@ -920,6 +920,7 @@ mod tests {
}
#[test]
#[cfg(target_family = "unix")]
fn should_fmt_path() {
let t: SystemTime = SystemTime::now();
let entry: FsEntry = FsEntry::File(FsFile {

View File

@@ -187,7 +187,10 @@ mod test {
}
#[test]
#[cfg(not(all(target_os = "macos", feature = "github-actions")))]
#[cfg(not(all(
any(target_os = "macos", target_os = "freebsd"),
feature = "github-actions"
)))]
fn auto_update() {
// Wno version
assert_eq!(
@@ -201,7 +204,10 @@ mod test {
}
#[test]
#[cfg(not(all(target_os = "macos", feature = "github-actions")))]
#[cfg(not(all(
any(target_os = "macos", target_os = "freebsd"),
feature = "github-actions"
)))]
fn check_for_updates() {
println!("{:?}", Update::is_new_version_available());
assert!(Update::is_new_version_available().is_ok());

View File

@@ -33,8 +33,6 @@ use crate::system::environment;
// Ext
use std::path::PathBuf;
use tui_realm_stdlib::{InputPropsBuilder, RadioPropsBuilder};
use tuirealm::PropsBuilder;
impl AuthActivity {
/// ### del_bookmark
@@ -234,12 +232,8 @@ impl AuthActivity {
/// Load bookmark data into the gui components
fn load_bookmark_into_gui(&mut self, bookmark: FileTransferParams) {
// Load parameters into components
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_PROTOCOL) {
let props = RadioPropsBuilder::from(props)
.with_value(Self::protocol_enum_to_opt(bookmark.protocol))
.build();
self.view.update(super::COMPONENT_RADIO_PROTOCOL, props);
}
self.protocol = bookmark.protocol;
self.mount_protocol(bookmark.protocol);
match bookmark.params {
ProtocolParams::AwsS3(params) => self.load_bookmark_s3_into_gui(params),
ProtocolParams::Generic(params) => self.load_bookmark_generic_into_gui(params),
@@ -247,51 +241,15 @@ impl AuthActivity {
}
fn load_bookmark_generic_into_gui(&mut self, params: GenericProtocolParams) {
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_ADDR) {
let props = InputPropsBuilder::from(props)
.with_value(params.address.clone())
.build();
self.view.update(super::COMPONENT_INPUT_ADDR, props);
}
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_PORT) {
let props = InputPropsBuilder::from(props)
.with_value(params.port.to_string())
.build();
self.view.update(super::COMPONENT_INPUT_PORT, props);
}
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_USERNAME) {
let props = InputPropsBuilder::from(props)
.with_value(params.username.as_deref().unwrap_or_default().to_string())
.build();
self.view.update(super::COMPONENT_INPUT_USERNAME, props);
}
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_PASSWORD) {
let props = InputPropsBuilder::from(props)
.with_value(params.password.as_deref().unwrap_or_default().to_string())
.build();
self.view.update(super::COMPONENT_INPUT_PASSWORD, props);
}
self.mount_address(params.address.as_str());
self.mount_port(params.port);
self.mount_username(params.username.as_deref().unwrap_or(""));
self.mount_password(params.password.as_deref().unwrap_or(""));
}
fn load_bookmark_s3_into_gui(&mut self, params: AwsS3Params) {
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_S3_BUCKET) {
let props = InputPropsBuilder::from(props)
.with_value(params.bucket_name.clone())
.build();
self.view.update(super::COMPONENT_INPUT_S3_BUCKET, props);
}
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_S3_REGION) {
let props = InputPropsBuilder::from(props)
.with_value(params.region.clone())
.build();
self.view.update(super::COMPONENT_INPUT_S3_REGION, props);
}
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_S3_PROFILE) {
let props = InputPropsBuilder::from(props)
.with_value(params.profile.as_deref().unwrap_or_default().to_string())
.build();
self.view.update(super::COMPONENT_INPUT_S3_PROFILE, props);
}
self.mount_s3_bucket(params.bucket_name.as_str());
self.mount_s3_region(params.region.as_str());
self.mount_s3_profile(params.profile.as_deref().unwrap_or(""));
}
}

View File

@@ -0,0 +1,445 @@
//! ## Bookmarks
//!
//! auth activity bookmarks 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.
*/
use super::Msg;
use tui_realm_stdlib::{Input, List, Radio};
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
use tuirealm::props::{Alignment, BorderSides, BorderType, Borders, Color, InputType, TextSpan};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
// -- bookmark list
#[derive(MockComponent)]
pub struct BookmarksList {
component: List,
}
impl BookmarksList {
pub fn new(bookmarks: &[String], color: Color) -> Self {
Self {
component: List::default()
.borders(Borders::default().color(color).modifiers(BorderType::Plain))
.highlighted_color(color)
.rewind(true)
.scroll(true)
.step(4)
.title("Bookmarks", Alignment::Left)
.rows(
bookmarks
.iter()
.map(|x| vec![TextSpan::from(x.as_str())])
.collect(),
),
}
}
}
impl Component<Msg, NoUserEvent> for BookmarksList {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => {
self.perform(Cmd::Move(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
self.perform(Cmd::Move(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::PageDown,
..
}) => {
self.perform(Cmd::Scroll(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::PageUp, ..
}) => {
self.perform(Cmd::Scroll(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => match self.state() {
State::One(StateValue::Usize(choice)) => Some(Msg::LoadBookmark(choice)),
_ => Some(Msg::None),
},
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => Some(Msg::BookmarksListBlur),
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::BookmarksTabBlur),
Event::Keyboard(KeyEvent {
code: Key::Delete, ..
}) => Some(Msg::ShowDeleteBookmarkPopup),
_ => None,
}
}
}
// -- recents list
#[derive(MockComponent)]
pub struct RecentsList {
component: List,
}
impl RecentsList {
pub fn new(bookmarks: &[String], color: Color) -> Self {
Self {
component: List::default()
.borders(Borders::default().color(color).modifiers(BorderType::Plain))
.highlighted_color(color)
.rewind(true)
.scroll(true)
.step(4)
.title("Recent connections", Alignment::Left)
.rows(
bookmarks
.iter()
.map(|x| vec![TextSpan::from(x.as_str())])
.collect(),
),
}
}
}
impl Component<Msg, NoUserEvent> for RecentsList {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => {
self.perform(Cmd::Move(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
self.perform(Cmd::Move(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::PageDown,
..
}) => {
self.perform(Cmd::Scroll(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::PageUp, ..
}) => {
self.perform(Cmd::Scroll(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => match self.state() {
State::One(StateValue::Usize(choice)) => Some(Msg::LoadRecent(choice)),
_ => Some(Msg::None),
},
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => Some(Msg::RececentsListBlur),
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::BookmarksTabBlur),
Event::Keyboard(KeyEvent {
code: Key::Delete, ..
}) => Some(Msg::ShowDeleteRecentPopup),
_ => None,
}
}
}
// -- delete bookmark
#[derive(MockComponent)]
pub struct DeleteBookmarkPopup {
component: Radio,
}
impl DeleteBookmarkPopup {
pub fn new(color: Color) -> Self {
Self {
component: Radio::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.choices(&["Yes", "No"])
.value(1)
.rewind(true)
.foreground(color)
.title("Delete selected bookmark?", Alignment::Center),
}
}
}
impl Component<Msg, NoUserEvent> for DeleteBookmarkPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Msg::CloseDeleteBookmark),
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => {
if matches!(
self.perform(Cmd::Submit),
CmdResult::Submit(State::One(StateValue::Usize(0)))
) {
Some(Msg::DeleteBookmark)
} else {
Some(Msg::CloseDeleteBookmark)
}
}
_ => None,
}
}
}
// -- delete recent
#[derive(MockComponent)]
pub struct DeleteRecentPopup {
component: Radio,
}
impl DeleteRecentPopup {
pub fn new(color: Color) -> Self {
Self {
component: Radio::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.choices(&["Yes", "No"])
.value(1)
.rewind(true)
.foreground(color)
.title("Delete selected recent host?", Alignment::Center),
}
}
}
impl Component<Msg, NoUserEvent> for DeleteRecentPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Msg::CloseDeleteRecent),
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => {
if matches!(
self.perform(Cmd::Submit),
CmdResult::Submit(State::One(StateValue::Usize(0)))
) {
Some(Msg::DeleteRecent)
} else {
Some(Msg::CloseDeleteRecent)
}
}
_ => None,
}
}
}
// -- bookmark name
// -- save password
#[derive(MockComponent)]
pub struct BookmarkSavePassword {
component: Radio,
}
impl BookmarkSavePassword {
pub fn new(color: Color) -> Self {
Self {
component: Radio::default()
.borders(
Borders::default()
.color(Color::Reset)
.sides(BorderSides::BOTTOM | BorderSides::LEFT | BorderSides::RIGHT)
.modifiers(BorderType::Rounded),
)
.choices(&["Yes", "No"])
.value(0)
.rewind(true)
.foreground(color)
.title("Save password?", Alignment::Center),
}
}
}
impl Component<Msg, NoUserEvent> for BookmarkSavePassword {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Msg::CloseSaveBookmark),
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => Some(Msg::SaveBookmark),
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::SaveBookmarkPasswordBlur),
_ => None,
}
}
}
// -- new bookmark name
#[derive(MockComponent)]
pub struct BookmarkName {
component: Input,
}
impl BookmarkName {
pub fn new(color: Color) -> Self {
Self {
component: Input::default()
.borders(
Borders::default()
.color(Color::Reset)
.sides(BorderSides::TOP | BorderSides::LEFT | BorderSides::RIGHT)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.title("Bookmark name", Alignment::Left)
.input_type(InputType::Text),
}
}
}
impl Component<Msg, NoUserEvent> for BookmarkName {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Delete, ..
}) => {
self.perform(Cmd::Cancel);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Backspace,
..
}) => {
self.perform(Cmd::Delete);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Char(ch),
modifiers: KeyModifiers::NONE,
}) => {
self.perform(Cmd::Type(ch));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => Some(Msg::SaveBookmark),
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => Some(Msg::BookmarkNameBlur),
_ => None,
}
}
}

View File

@@ -0,0 +1,694 @@
//! ## Form
//!
//! auth activity components for file transfer params form
/**
* 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 super::{FileTransferProtocol, Msg};
use tui_realm_stdlib::{Input, Radio};
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
// -- protocol
#[derive(MockComponent)]
pub struct ProtocolRadio {
component: Radio,
}
impl ProtocolRadio {
pub fn new(default_protocol: FileTransferProtocol, color: Color) -> Self {
Self {
component: Radio::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.choices(&["SFTP", "SCP", "FTP", "FTPS", "AWS S3"])
.foreground(color)
.rewind(true)
.title("Protocol", Alignment::Left)
.value(Self::protocol_enum_to_opt(default_protocol)),
}
}
/// ### protocol_opt_to_enum
///
/// Convert radio index for protocol into a `FileTransferProtocol`
fn protocol_opt_to_enum(protocol: usize) -> FileTransferProtocol {
match protocol {
1 => FileTransferProtocol::Scp,
2 => FileTransferProtocol::Ftp(false),
3 => FileTransferProtocol::Ftp(true),
4 => FileTransferProtocol::AwsS3,
_ => FileTransferProtocol::Sftp,
}
}
/// ### protocol_enum_to_opt
///
/// Convert `FileTransferProtocol` enum into radio group index
fn protocol_enum_to_opt(protocol: FileTransferProtocol) -> usize {
match protocol {
FileTransferProtocol::Sftp => 0,
FileTransferProtocol::Scp => 1,
FileTransferProtocol::Ftp(false) => 2,
FileTransferProtocol::Ftp(true) => 3,
FileTransferProtocol::AwsS3 => 4,
}
}
}
impl Component<Msg, NoUserEvent> for ProtocolRadio {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
let result = match ev {
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => self.perform(Cmd::Move(Direction::Left)),
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => self.perform(Cmd::Move(Direction::Right)),
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => return Some(Msg::Connect),
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => return Some(Msg::ProtocolBlurDown),
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => return Some(Msg::ProtocolBlurUp),
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => return Some(Msg::ParamsFormBlur),
_ => return None,
};
match result {
CmdResult::Changed(State::One(StateValue::Usize(choice))) => {
Some(Msg::ProtocolChanged(Self::protocol_opt_to_enum(choice)))
}
_ => Some(Msg::None),
}
}
}
// -- address
#[derive(MockComponent)]
pub struct InputAddress {
component: Input,
}
impl InputAddress {
pub fn new(host: &str, color: Color) -> Self {
Self {
component: Input::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.placeholder("127.0.0.1", Style::default().fg(Color::Rgb(128, 128, 128)))
.title("Remote host", Alignment::Left)
.input_type(InputType::Text)
.value(host),
}
}
}
impl Component<Msg, NoUserEvent> for InputAddress {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Delete, ..
}) => {
self.perform(Cmd::Cancel);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Backspace,
..
}) => {
self.perform(Cmd::Delete);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Char(ch),
modifiers: KeyModifiers::NONE,
}) => {
self.perform(Cmd::Type(ch));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => Some(Msg::Connect),
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => Some(Msg::AddressBlurDown),
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::AddressBlurUp),
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::ParamsFormBlur),
_ => None,
}
}
}
// -- port number
#[derive(MockComponent)]
pub struct InputPort {
component: Input,
}
impl InputPort {
pub fn new(port: u16, color: Color) -> Self {
Self {
component: Input::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.placeholder("22", Style::default().fg(Color::Rgb(128, 128, 128)))
.input_type(InputType::UnsignedInteger)
.input_len(5)
.title("Port number", Alignment::Left)
.value(port.to_string()),
}
}
}
impl Component<Msg, NoUserEvent> for InputPort {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Delete, ..
}) => {
self.perform(Cmd::Cancel);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Backspace,
..
}) => {
self.perform(Cmd::Delete);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Char(ch),
modifiers: KeyModifiers::NONE,
}) => {
self.perform(Cmd::Type(ch));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => Some(Msg::Connect),
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => Some(Msg::PortBlurDown),
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::PortBlurUp),
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::ParamsFormBlur),
_ => None,
}
}
}
// -- username
#[derive(MockComponent)]
pub struct InputUsername {
component: Input,
}
impl InputUsername {
pub fn new(username: &str, color: Color) -> Self {
Self {
component: Input::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.placeholder("root", Style::default().fg(Color::Rgb(128, 128, 128)))
.title("Username", Alignment::Left)
.input_type(InputType::Text)
.value(username),
}
}
}
impl Component<Msg, NoUserEvent> for InputUsername {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Delete, ..
}) => {
self.perform(Cmd::Cancel);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Backspace,
..
}) => {
self.perform(Cmd::Delete);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Char(ch),
modifiers: KeyModifiers::NONE,
}) => {
self.perform(Cmd::Type(ch));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => Some(Msg::Connect),
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => Some(Msg::UsernameBlurDown),
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::UsernameBlurUp),
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::ParamsFormBlur),
_ => None,
}
}
}
// -- password
#[derive(MockComponent)]
pub struct InputPassword {
component: Input,
}
impl InputPassword {
pub fn new(password: &str, color: Color) -> Self {
Self {
component: Input::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.title("Password", Alignment::Left)
.input_type(InputType::Password('*'))
.value(password),
}
}
}
impl Component<Msg, NoUserEvent> for InputPassword {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Delete, ..
}) => {
self.perform(Cmd::Cancel);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Backspace,
..
}) => {
self.perform(Cmd::Delete);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Char(ch),
modifiers: KeyModifiers::NONE,
}) => {
self.perform(Cmd::Type(ch));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => Some(Msg::Connect),
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => Some(Msg::PasswordBlurDown),
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::PasswordBlurUp),
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::ParamsFormBlur),
_ => None,
}
}
}
// -- s3 bucket
#[derive(MockComponent)]
pub struct InputS3Bucket {
component: Input,
}
impl InputS3Bucket {
pub fn new(bucket: &str, color: Color) -> Self {
Self {
component: Input::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.placeholder("my-bucket", Style::default().fg(Color::Rgb(128, 128, 128)))
.title("Bucket name", Alignment::Left)
.input_type(InputType::Text)
.value(bucket),
}
}
}
impl Component<Msg, NoUserEvent> for InputS3Bucket {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Delete, ..
}) => {
self.perform(Cmd::Cancel);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Backspace,
..
}) => {
self.perform(Cmd::Delete);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Char(ch),
modifiers: KeyModifiers::NONE,
}) => {
self.perform(Cmd::Type(ch));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => Some(Msg::Connect),
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => Some(Msg::S3BucketBlurDown),
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::S3BucketBlurUp),
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::ParamsFormBlur),
_ => None,
}
}
}
// -- s3 bucket
#[derive(MockComponent)]
pub struct InputS3Region {
component: Input,
}
impl InputS3Region {
pub fn new(region: &str, color: Color) -> Self {
Self {
component: Input::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.placeholder("eu-west-1", Style::default().fg(Color::Rgb(128, 128, 128)))
.title("Region", Alignment::Left)
.input_type(InputType::Text)
.value(region),
}
}
}
impl Component<Msg, NoUserEvent> for InputS3Region {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Delete, ..
}) => {
self.perform(Cmd::Cancel);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Backspace,
..
}) => {
self.perform(Cmd::Delete);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Char(ch),
modifiers: KeyModifiers::NONE,
}) => {
self.perform(Cmd::Type(ch));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => Some(Msg::Connect),
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => Some(Msg::S3RegionBlurDown),
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::S3RegionBlurUp),
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::ParamsFormBlur),
_ => None,
}
}
}
// -- s3 bucket
#[derive(MockComponent)]
pub struct InputS3Profile {
component: Input,
}
impl InputS3Profile {
pub fn new(profile: &str, color: Color) -> Self {
Self {
component: Input::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.placeholder("default", Style::default().fg(Color::Rgb(128, 128, 128)))
.title("Profile", Alignment::Left)
.input_type(InputType::Text)
.value(profile),
}
}
}
impl Component<Msg, NoUserEvent> for InputS3Profile {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Delete, ..
}) => {
self.perform(Cmd::Cancel);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Backspace,
..
}) => {
self.perform(Cmd::Delete);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Char(ch),
modifiers: KeyModifiers::NONE,
}) => {
self.perform(Cmd::Type(ch));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => Some(Msg::Connect),
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => Some(Msg::S3ProfileBlurDown),
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::S3ProfileBlurUp),
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::ParamsFormBlur),
_ => None,
}
}
}

View File

@@ -0,0 +1,91 @@
//! ## Components
//!
//! auth activity 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.
*/
use super::{FileTransferProtocol, Msg};
mod bookmarks;
mod form;
mod popup;
mod text;
pub use bookmarks::{
BookmarkName, BookmarkSavePassword, BookmarksList, DeleteBookmarkPopup, DeleteRecentPopup,
RecentsList,
};
pub use form::{
InputAddress, InputPassword, InputPort, InputS3Bucket, InputS3Profile, InputS3Region,
InputUsername, ProtocolRadio,
};
pub use popup::{
ErrorPopup, InfoPopup, InstallUpdatePopup, Keybindings, QuitPopup, ReleaseNotes, WaitPopup,
WindowSizeError,
};
pub use text::{HelpText, NewVersionDisclaimer, Subtitle, Title};
use tui_realm_stdlib::Phantom;
use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent};
use tuirealm::{Component, MockComponent};
// -- global listener
#[derive(MockComponent)]
pub struct GlobalListener {
component: Phantom,
}
impl Default for GlobalListener {
fn default() -> Self {
Self {
component: Phantom::default(),
}
}
}
impl Component<Msg, NoUserEvent> for GlobalListener {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Msg::ShowQuitPopup),
Event::Keyboard(KeyEvent {
code: Key::Char('c'),
modifiers: KeyModifiers::CONTROL,
}) => Some(Msg::EnterSetup),
Event::Keyboard(KeyEvent {
code: Key::Char('h'),
modifiers: KeyModifiers::CONTROL,
}) => Some(Msg::ShowKeybindingsPopup),
Event::Keyboard(KeyEvent {
code: Key::Char('r'),
modifiers: KeyModifiers::CONTROL,
}) => Some(Msg::ShowReleaseNotes),
Event::Keyboard(KeyEvent {
code: Key::Char('s'),
modifiers: KeyModifiers::CONTROL,
}) => Some(Msg::ShowSaveBookmarkPopup),
_ => None,
}
}
}

View File

@@ -0,0 +1,452 @@
//! ## Popup
//!
//! auth activity popups
/**
* 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 super::Msg;
use tui_realm_stdlib::{List, Paragraph, Radio, Textarea};
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::event::{Key, KeyEvent};
use tuirealm::props::{Alignment, BorderType, Borders, Color, TableBuilder, TextSpan};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
// -- error popup
#[derive(MockComponent)]
pub struct ErrorPopup {
component: Paragraph,
}
impl ErrorPopup {
pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self {
Self {
component: Paragraph::default()
.alignment(Alignment::Center)
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.text(&[TextSpan::from(text.as_ref())])
.wrap(true),
}
}
}
impl Component<Msg, NoUserEvent> for ErrorPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Esc | Key::Enter,
..
}) => Some(Msg::CloseErrorPopup),
_ => None,
}
}
}
// -- info popup
#[derive(MockComponent)]
pub struct InfoPopup {
component: Paragraph,
}
impl InfoPopup {
pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self {
Self {
component: Paragraph::default()
.alignment(Alignment::Center)
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.text(&[TextSpan::from(text.as_ref())])
.wrap(true),
}
}
}
impl Component<Msg, NoUserEvent> for InfoPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Esc | Key::Enter,
..
}) => Some(Msg::CloseInfoPopup),
_ => None,
}
}
}
// -- wait popup
#[derive(MockComponent)]
pub struct WaitPopup {
component: Paragraph,
}
impl WaitPopup {
pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self {
Self {
component: Paragraph::default()
.alignment(Alignment::Center)
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.text(&[TextSpan::from(text.as_ref())])
.wrap(true),
}
}
}
impl Component<Msg, NoUserEvent> for WaitPopup {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
None
}
}
// -- window size error
#[derive(MockComponent)]
pub struct WindowSizeError {
component: Paragraph,
}
impl WindowSizeError {
pub fn new(color: Color) -> Self {
Self {
component: Paragraph::default()
.alignment(Alignment::Center)
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.text(&[TextSpan::from(
"termscp requires at least 24 lines of height to run",
)])
.wrap(true),
}
}
}
impl Component<Msg, NoUserEvent> for WindowSizeError {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
None
}
}
// -- quit popup
#[derive(MockComponent)]
pub struct QuitPopup {
component: Radio,
}
impl QuitPopup {
pub fn new(color: Color) -> Self {
Self {
component: Radio::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.title("Quit termscp?", Alignment::Center)
.rewind(true)
.choices(&["Yes", "No"]),
}
}
}
impl Component<Msg, NoUserEvent> for QuitPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Msg::CloseQuitPopup),
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => {
if matches!(
self.perform(Cmd::Submit),
CmdResult::Submit(State::One(StateValue::Usize(0)))
) {
Some(Msg::Quit)
} else {
Some(Msg::CloseQuitPopup)
}
}
_ => None,
}
}
}
// -- install update popup
#[derive(MockComponent)]
pub struct InstallUpdatePopup {
component: Radio,
}
impl InstallUpdatePopup {
pub fn new(color: Color) -> Self {
Self {
component: Radio::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.title("Install update?", Alignment::Center)
.rewind(true)
.choices(&["Yes", "No"]),
}
}
}
impl Component<Msg, NoUserEvent> for InstallUpdatePopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Msg::CloseInstallUpdatePopup),
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => {
if matches!(
self.perform(Cmd::Submit),
CmdResult::Submit(State::One(StateValue::Usize(0)))
) {
Some(Msg::InstallUpdate)
} else {
Some(Msg::CloseInstallUpdatePopup)
}
}
_ => None,
}
}
}
// -- release notes popup
#[derive(MockComponent)]
pub struct ReleaseNotes {
component: Textarea,
}
impl ReleaseNotes {
pub fn new(notes: &str, color: Color) -> Self {
Self {
component: Textarea::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.title("Release notes", Alignment::Center)
.text_rows(
notes
.lines()
.map(TextSpan::from)
.collect::<Vec<TextSpan>>()
.as_slice(),
),
}
}
}
impl Component<Msg, NoUserEvent> for ReleaseNotes {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Esc | Key::Enter,
..
}) => Some(Msg::CloseInstallUpdatePopup),
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => {
self.perform(Cmd::Move(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
self.perform(Cmd::Move(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::PageDown,
..
}) => {
self.perform(Cmd::Scroll(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::PageUp, ..
}) => {
self.perform(Cmd::Scroll(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
_ => None,
}
}
}
// -- keybindings popup
#[derive(MockComponent)]
pub struct Keybindings {
component: List,
}
impl Keybindings {
pub fn new(color: Color) -> Self {
Self {
component: List::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.highlighted_str("? ")
.title("Keybindings", Alignment::Center)
.scroll(true)
.step(4)
.rows(
TableBuilder::default()
.add_col(TextSpan::new("<ESC>").bold().fg(color))
.add_col(TextSpan::from(" Quit termscp"))
.add_row()
.add_col(TextSpan::new("<TAB>").bold().fg(color))
.add_col(TextSpan::from(" Switch from form and bookmarks"))
.add_row()
.add_col(TextSpan::new("<RIGHT/LEFT>").bold().fg(color))
.add_col(TextSpan::from(" Switch bookmark tab"))
.add_row()
.add_col(TextSpan::new("<UP/DOWN>").bold().fg(color))
.add_col(TextSpan::from(" Move up/down in current tab"))
.add_row()
.add_col(TextSpan::new("<ENTER>").bold().fg(color))
.add_col(TextSpan::from(" Connect/Load bookmark"))
.add_row()
.add_col(TextSpan::new("<DEL|E>").bold().fg(color))
.add_col(TextSpan::from(" Delete selected bookmark"))
.add_row()
.add_col(TextSpan::new("<CTRL+C>").bold().fg(color))
.add_col(TextSpan::from(" Enter setup"))
.add_row()
.add_col(TextSpan::new("<CTRL+S>").bold().fg(color))
.add_col(TextSpan::from(" Save bookmark"))
.build(),
),
}
}
}
impl Component<Msg, NoUserEvent> for Keybindings {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Esc | Key::Enter,
..
}) => Some(Msg::CloseKeybindingsPopup),
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => {
self.perform(Cmd::Move(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
self.perform(Cmd::Move(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::PageDown,
..
}) => {
self.perform(Cmd::Scroll(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::PageUp, ..
}) => {
self.perform(Cmd::Scroll(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
_ => None,
}
}
}

View File

@@ -0,0 +1,132 @@
//! ## Text
//!
//! auth activity texts
/**
* 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 super::Msg;
use tui_realm_stdlib::{Label, Span};
use tuirealm::props::{Color, TextModifiers, TextSpan};
use tuirealm::{Component, Event, MockComponent, NoUserEvent};
// -- Title
#[derive(MockComponent)]
pub struct Title {
component: Label,
}
impl Default for Title {
fn default() -> Self {
Self {
component: Label::default()
.modifiers(TextModifiers::BOLD | TextModifiers::ITALIC)
.text("$ termscp"),
}
}
}
impl Component<Msg, NoUserEvent> for Title {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
None
}
}
// -- subtitle
#[derive(MockComponent)]
pub struct Subtitle {
component: Label,
}
impl Default for Subtitle {
fn default() -> Self {
Self {
component: Label::default()
.modifiers(TextModifiers::BOLD | TextModifiers::ITALIC)
.text(format!("$ version {}", env!("CARGO_PKG_VERSION"))),
}
}
}
impl Component<Msg, NoUserEvent> for Subtitle {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
None
}
}
// -- new version disclaimer
#[derive(MockComponent)]
pub struct NewVersionDisclaimer {
component: Span,
}
impl NewVersionDisclaimer {
pub fn new(new_version: &str, color: Color) -> Self {
Self {
component: Span::default().foreground(color).spans(&[
TextSpan::from("termscp "),
TextSpan::new(new_version).underlined().bold(),
TextSpan::from(
" is NOW available! Install update and view release notes with <CTRL+R>",
),
]),
}
}
}
impl Component<Msg, NoUserEvent> for NewVersionDisclaimer {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
None
}
}
// -- HelpText
#[derive(MockComponent)]
pub struct HelpText {
component: Span,
}
impl HelpText {
pub fn new(key_color: Color) -> Self {
Self {
component: Span::default().spans(&[
TextSpan::new("Press ").bold(),
TextSpan::new("<CTRL+H>").bold().fg(key_color),
TextSpan::new(" to show keybindings; ").bold(),
TextSpan::new("<CTRL+C>").bold().fg(key_color),
TextSpan::new(" to enter setup").bold(),
]),
}
}
}
impl Component<Msg, NoUserEvent> for HelpText {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
None
}
}

View File

@@ -31,32 +31,6 @@ use crate::system::auto_update::{Release, Update, UpdateStatus};
use crate::system::notifications::Notification;
impl AuthActivity {
/// ### protocol_opt_to_enum
///
/// Convert radio index for protocol into a `FileTransferProtocol`
pub(super) fn protocol_opt_to_enum(protocol: usize) -> FileTransferProtocol {
match protocol {
1 => FileTransferProtocol::Scp,
2 => FileTransferProtocol::Ftp(false),
3 => FileTransferProtocol::Ftp(true),
4 => FileTransferProtocol::AwsS3,
_ => FileTransferProtocol::Sftp,
}
}
/// ### protocol_enum_to_opt
///
/// Convert `FileTransferProtocol` enum into radio group index
pub(super) fn protocol_enum_to_opt(protocol: FileTransferProtocol) -> usize {
match protocol {
FileTransferProtocol::Sftp => 0,
FileTransferProtocol::Scp => 1,
FileTransferProtocol::Ftp(false) => 2,
FileTransferProtocol::Ftp(true) => 3,
FileTransferProtocol::AwsS3 => 4,
}
}
/// ### get_default_port_for_protocol
///
/// Get the default port for protocol
@@ -91,9 +65,8 @@ impl AuthActivity {
///
/// Collect host params as `FileTransferParams`
pub(super) fn collect_host_params(&self) -> Result<FileTransferParams, &'static str> {
let protocol: FileTransferProtocol = self.get_protocol();
match protocol {
FileTransferProtocol::AwsS3 => self.collect_s3_host_params(protocol),
match self.protocol {
FileTransferProtocol::AwsS3 => self.collect_s3_host_params(),
protocol => self.collect_generic_host_params(protocol),
}
}
@@ -135,10 +108,7 @@ impl AuthActivity {
/// ### collect_s3_host_params
///
/// Get input values from fields or return an error if fields are invalid to work as aws s3
pub(super) fn collect_s3_host_params(
&self,
protocol: FileTransferProtocol,
) -> Result<FileTransferParams, &'static str> {
pub(super) fn collect_s3_host_params(&self) -> Result<FileTransferParams, &'static str> {
let (bucket, region, profile): (String, String, Option<String>) =
self.get_s3_params_input();
if bucket.is_empty() {
@@ -148,7 +118,7 @@ impl AuthActivity {
return Err("Invalid region");
}
Ok(FileTransferParams {
protocol,
protocol: FileTransferProtocol::AwsS3,
params: ProtocolParams::AwsS3(AwsS3Params::new(bucket, region, profile)),
entry_directory: None,
})

View File

@@ -27,6 +27,7 @@
*/
// Sub modules
mod bookmarks;
mod components;
mod misc;
mod update;
mod view;
@@ -39,37 +40,101 @@ use crate::system::bookmarks_client::BookmarksClient;
use crate::system::config_client::ConfigClient;
// Includes
use crossterm::event::Event;
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use tuirealm::{Update, View};
use std::time::Duration;
use tuirealm::listener::EventListenerCfg;
use tuirealm::{application::PollStrategy, Application, NoUserEvent, Update};
// -- components
const COMPONENT_TEXT_H1: &str = "TEXT_H1";
const COMPONENT_TEXT_H2: &str = "TEXT_H2";
const COMPONENT_TEXT_NEW_VERSION: &str = "TEXT_NEW_VERSION";
const COMPONENT_TEXT_NEW_VERSION_NOTES: &str = "TEXTAREA_NEW_VERSION";
const COMPONENT_TEXT_FOOTER: &str = "TEXT_FOOTER";
const COMPONENT_TEXT_HELP: &str = "TEXT_HELP";
const COMPONENT_TEXT_ERROR: &str = "TEXT_ERROR";
const COMPONENT_TEXT_INFO: &str = "TEXT_INFO";
const COMPONENT_TEXT_WAIT: &str = "TEXT_WAIT";
const COMPONENT_TEXT_SIZE_ERR: &str = "TEXT_SIZE_ERR";
const COMPONENT_INPUT_ADDR: &str = "INPUT_ADDRESS";
const COMPONENT_INPUT_PORT: &str = "INPUT_PORT";
const COMPONENT_INPUT_USERNAME: &str = "INPUT_USERNAME";
const COMPONENT_INPUT_PASSWORD: &str = "INPUT_PASSWORD";
const COMPONENT_INPUT_BOOKMARK_NAME: &str = "INPUT_BOOKMARK_NAME";
const COMPONENT_INPUT_S3_BUCKET: &str = "INPUT_S3_BUCKET";
const COMPONENT_INPUT_S3_REGION: &str = "INPUT_S3_REGION";
const COMPONENT_INPUT_S3_PROFILE: &str = "INPUT_S3_PROFILE";
const COMPONENT_RADIO_PROTOCOL: &str = "RADIO_PROTOCOL";
const COMPONENT_RADIO_QUIT: &str = "RADIO_QUIT";
const COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK: &str = "RADIO_DELETE_BOOKMARK";
const COMPONENT_RADIO_BOOKMARK_DEL_RECENT: &str = "RADIO_DELETE_RECENT";
const COMPONENT_RADIO_BOOKMARK_SAVE_PWD: &str = "RADIO_SAVE_PASSWORD";
const COMPONENT_RADIO_INSTALL_UPDATE: &str = "RADIO_INSTALL_UPDATE";
const COMPONENT_BOOKMARKS_LIST: &str = "BOOKMARKS_LIST";
const COMPONENT_RECENTS_LIST: &str = "RECENTS_LIST";
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
pub enum Id {
Address,
BookmarkName,
BookmarkSavePassword,
BookmarksList,
DeleteBookmarkPopup,
DeleteRecentPopup,
ErrorPopup,
GlobalListener,
HelpText,
InfoPopup,
InstallUpdatePopup,
Keybindings,
NewVersionChangelog,
NewVersionDisclaimer,
Password,
Port,
Protocol,
QuitPopup,
RecentsList,
S3Bucket,
S3Profile,
S3Region,
Subtitle,
Title,
Username,
WaitPopup,
WindowSizeError,
}
#[derive(Debug, PartialEq)]
pub enum Msg {
AddressBlurDown,
AddressBlurUp,
BookmarksListBlur,
BookmarksTabBlur,
CloseDeleteBookmark,
CloseDeleteRecent,
CloseErrorPopup,
CloseInfoPopup,
CloseInstallUpdatePopup,
CloseKeybindingsPopup,
CloseQuitPopup,
CloseSaveBookmark,
Connect,
DeleteBookmark,
DeleteRecent,
EnterSetup,
InstallUpdate,
LoadBookmark(usize),
LoadRecent(usize),
ParamsFormBlur,
PasswordBlurDown,
PasswordBlurUp,
PortBlurDown,
PortBlurUp,
ProtocolBlurDown,
ProtocolBlurUp,
ProtocolChanged(FileTransferProtocol),
Quit,
RececentsListBlur,
S3BucketBlurDown,
S3BucketBlurUp,
S3ProfileBlurDown,
S3ProfileBlurUp,
S3RegionBlurDown,
S3RegionBlurUp,
SaveBookmark,
BookmarkNameBlur,
SaveBookmarkPasswordBlur,
ShowDeleteBookmarkPopup,
ShowDeleteRecentPopup,
ShowKeybindingsPopup,
ShowQuitPopup,
ShowReleaseNotes,
ShowSaveBookmarkPopup,
UsernameBlurDown,
UsernameBlurUp,
None,
}
/// ## InputMask
///
/// Auth form input mask
#[derive(Eq, PartialEq)]
enum InputMask {
Generic,
AwsS3,
}
// Store keys
const STORE_KEY_LATEST_VERSION: &str = "AUTH_LATEST_VERSION";
@@ -79,34 +144,39 @@ const STORE_KEY_RELEASE_NOTES: &str = "AUTH_RELEASE_NOTES";
///
/// AuthActivity is the data holder for the authentication activity
pub struct AuthActivity {
exit_reason: Option<ExitReason>,
context: Option<Context>,
view: View,
app: Application<Id, Msg, NoUserEvent>,
bookmarks_client: Option<BookmarksClient>,
redraw: bool, // Should ui actually be redrawned?
bookmarks_list: Vec<String>, // List of bookmarks
recents_list: Vec<String>, // list of recents
}
impl Default for AuthActivity {
fn default() -> Self {
Self::new()
}
/// List of bookmarks
bookmarks_list: Vec<String>,
/// List of recent hosts
recents_list: Vec<String>,
/// Exit reason
exit_reason: Option<ExitReason>,
/// Should redraw ui
redraw: bool,
/// Protocol
protocol: FileTransferProtocol,
context: Option<Context>,
}
impl AuthActivity {
/// ### new
///
/// Instantiates a new AuthActivity
pub fn new() -> AuthActivity {
pub fn new(ticks: Duration) -> AuthActivity {
AuthActivity {
exit_reason: None,
app: Application::init(
EventListenerCfg::default()
.default_input_listener(ticks)
.poll_timeout(ticks),
),
context: None,
view: View::init(),
bookmarks_client: None,
redraw: true, // True at startup
bookmarks_list: Vec::new(),
exit_reason: None,
recents_list: Vec::new(),
redraw: true,
protocol: FileTransferProtocol::Sftp,
}
}
@@ -142,9 +212,11 @@ impl AuthActivity {
///
/// Get current input mask to show
fn input_mask(&self) -> InputMask {
match self.get_protocol() {
match self.protocol {
FileTransferProtocol::AwsS3 => InputMask::AwsS3,
_ => InputMask::Generic,
FileTransferProtocol::Ftp(_)
| FileTransferProtocol::Scp
| FileTransferProtocol::Sftp => InputMask::Generic,
}
}
}
@@ -162,9 +234,11 @@ impl Activity for AuthActivity {
// Set context
self.context = Some(context);
// Clear terminal
self.context_mut().clear_screen();
if let Err(err) = self.context_mut().terminal().clear_screen() {
error!("Failed to clear screen: {}", err);
}
// Put raw mode on enabled
if let Err(err) = enable_raw_mode() {
if let Err(err) = self.context_mut().terminal().enable_raw_mode() {
error!("Failed to enter raw mode: {}", err);
}
// If check for updates is enabled, check for updates
@@ -194,24 +268,23 @@ impl Activity for AuthActivity {
if self.context.is_none() {
return;
}
// Read one event
if let Ok(Some(event)) = self.context().input_hnd().read_event() {
// Set redraw to true
self.redraw = true;
// Handle on resize
if let Event::Resize(_, h) = event {
self.check_minimum_window_size(h);
// Tick
match self.app.tick(PollStrategy::UpTo(3)) {
Ok(messages) => {
for msg in messages.into_iter() {
let mut msg = Some(msg);
while msg.is_some() {
msg = self.update(msg);
}
}
}
Err(err) => {
self.mount_error(format!("Application error: {}", err));
}
// Handle event on view and update
let msg = self.view.on(event);
self.update(msg);
}
// Redraw if necessary
// View
if self.redraw {
// View
self.view();
// Set redraw to false
self.redraw = false;
}
}
@@ -231,26 +304,12 @@ impl Activity for AuthActivity {
/// This function finally releases the context
fn on_destroy(&mut self) -> Option<Context> {
// Disable raw mode
if let Err(err) = disable_raw_mode() {
if let Err(err) = self.context_mut().terminal().disable_raw_mode() {
error!("Failed to disable raw mode: {}", err);
}
self.context.as_ref()?;
// Clear terminal and return
match self.context.take() {
Some(mut ctx) => {
ctx.clear_screen();
Some(ctx)
}
None => None,
if let Err(err) = self.context_mut().terminal().clear_screen() {
error!("Failed to clear screen: {}", err);
}
self.context.take()
}
}
/// ## InputMask
///
/// Auth form input mask
#[derive(Eq, PartialEq)]
enum InputMask {
Generic,
AwsS3,
}

View File

@@ -1,6 +1,6 @@
//! ## AuthActivity
//! ## Update
//!
//! `auth_activity` is the module which implements the authentication activity
//! Update impl
/**
* MIT License
@@ -25,415 +25,222 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// locals
use super::{
AuthActivity, FileTransferProtocol, InputMask, COMPONENT_BOOKMARKS_LIST, COMPONENT_INPUT_ADDR,
COMPONENT_INPUT_BOOKMARK_NAME, COMPONENT_INPUT_PASSWORD, COMPONENT_INPUT_PORT,
COMPONENT_INPUT_S3_BUCKET, COMPONENT_INPUT_S3_PROFILE, COMPONENT_INPUT_S3_REGION,
COMPONENT_INPUT_USERNAME, COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK,
COMPONENT_RADIO_BOOKMARK_DEL_RECENT, COMPONENT_RADIO_BOOKMARK_SAVE_PWD,
COMPONENT_RADIO_INSTALL_UPDATE, COMPONENT_RADIO_PROTOCOL, COMPONENT_RADIO_QUIT,
COMPONENT_RECENTS_LIST, COMPONENT_TEXT_ERROR, COMPONENT_TEXT_HELP, COMPONENT_TEXT_INFO,
COMPONENT_TEXT_NEW_VERSION_NOTES, COMPONENT_TEXT_SIZE_ERR, COMPONENT_TEXT_WAIT,
};
use crate::ui::keymap::*;
use tui_realm_stdlib::InputPropsBuilder;
use tuirealm::{Msg, Payload, PropsBuilder, Update, Value};
use super::{AuthActivity, ExitReason, Id, InputMask, Msg, Update};
// -- update
use tuirealm::{State, StateValue};
impl Update for AuthActivity {
/// ### update
///
/// Update auth activity model based on msg
/// The function exits when returns None
fn update(&mut self, msg: Option<(String, Msg)>) -> Option<(String, Msg)> {
let ref_msg: Option<(&str, &Msg)> = msg.as_ref().map(|(s, msg)| (s.as_str(), msg));
// Match msg
match ref_msg {
None => None, // Exit after None
Some(msg) => match msg {
// Focus ( DOWN )
(COMPONENT_RADIO_PROTOCOL, key) if key == &MSG_KEY_DOWN => {
// Give focus based on current mask
match self.input_mask() {
InputMask::Generic => self.view.active(COMPONENT_INPUT_ADDR),
InputMask::AwsS3 => self.view.active(COMPONENT_INPUT_S3_BUCKET),
};
None
}
// -- generic mask (DOWN)
(COMPONENT_INPUT_ADDR, key) if key == &MSG_KEY_DOWN => {
self.view.active(COMPONENT_INPUT_PORT);
None
}
(COMPONENT_INPUT_PORT, key) if key == &MSG_KEY_DOWN => {
self.view.active(COMPONENT_INPUT_USERNAME);
None
}
(COMPONENT_INPUT_USERNAME, key) if key == &MSG_KEY_DOWN => {
self.view.active(COMPONENT_INPUT_PASSWORD);
None
}
(COMPONENT_INPUT_PASSWORD, key) if key == &MSG_KEY_DOWN => {
self.view.active(COMPONENT_RADIO_PROTOCOL);
None
}
// -- s3 mask (DOWN)
(COMPONENT_INPUT_S3_BUCKET, key) if key == &MSG_KEY_DOWN => {
self.view.active(COMPONENT_INPUT_S3_REGION);
None
}
(COMPONENT_INPUT_S3_REGION, key) if key == &MSG_KEY_DOWN => {
self.view.active(COMPONENT_INPUT_S3_PROFILE);
None
}
(COMPONENT_INPUT_S3_PROFILE, key) if key == &MSG_KEY_DOWN => {
self.view.active(COMPONENT_RADIO_PROTOCOL);
None
}
// Focus ( UP )
// -- generic (UP)
(COMPONENT_INPUT_PASSWORD, key) if key == &MSG_KEY_UP => {
self.view.active(COMPONENT_INPUT_USERNAME);
None
}
(COMPONENT_INPUT_USERNAME, key) if key == &MSG_KEY_UP => {
self.view.active(COMPONENT_INPUT_PORT);
None
}
(COMPONENT_INPUT_PORT, key) if key == &MSG_KEY_UP => {
self.view.active(COMPONENT_INPUT_ADDR);
None
}
(COMPONENT_INPUT_ADDR, key) if key == &MSG_KEY_UP => {
self.view.active(COMPONENT_RADIO_PROTOCOL);
None
}
// -- s3 (UP)
(COMPONENT_INPUT_S3_BUCKET, key) if key == &MSG_KEY_UP => {
self.view.active(COMPONENT_RADIO_PROTOCOL);
None
}
(COMPONENT_INPUT_S3_REGION, key) if key == &MSG_KEY_UP => {
self.view.active(COMPONENT_INPUT_S3_BUCKET);
None
}
(COMPONENT_INPUT_S3_PROFILE, key) if key == &MSG_KEY_UP => {
self.view.active(COMPONENT_INPUT_S3_REGION);
None
}
(COMPONENT_RADIO_PROTOCOL, key) if key == &MSG_KEY_UP => {
// Give focus based on current mask
match self.input_mask() {
InputMask::Generic => self.view.active(COMPONENT_INPUT_PASSWORD),
InputMask::AwsS3 => self.view.active(COMPONENT_INPUT_S3_PROFILE),
};
None
}
// Protocol - On Change
(COMPONENT_RADIO_PROTOCOL, Msg::OnChange(Payload::One(Value::Usize(protocol)))) => {
// If port is standard, update the current port with default for selected protocol
let protocol: FileTransferProtocol = Self::protocol_opt_to_enum(*protocol);
// Get port
let port: u16 = self.get_input_port();
match Self::is_port_standard(port) {
false => None, // Return None
true => {
self.update_input_port(Self::get_default_port_for_protocol(protocol))
}
impl Update<Msg> for AuthActivity {
fn update(&mut self, msg: Option<Msg>) -> Option<Msg> {
self.redraw = true;
match msg.unwrap_or(Msg::None) {
Msg::AddressBlurDown => {
assert!(self.app.active(&Id::Port).is_ok());
}
Msg::AddressBlurUp => {
assert!(self.app.active(&Id::Protocol).is_ok());
}
Msg::BookmarksListBlur => {
assert!(self.app.active(&Id::RecentsList).is_ok());
}
Msg::BookmarkNameBlur => {
assert!(self.app.active(&Id::BookmarkSavePassword).is_ok());
}
Msg::BookmarksTabBlur => {
assert!(self.app.active(&Id::Protocol).is_ok());
}
Msg::CloseDeleteBookmark => {
assert!(self.app.umount(&Id::DeleteBookmarkPopup).is_ok());
}
Msg::CloseDeleteRecent => {
assert!(self.app.umount(&Id::DeleteRecentPopup).is_ok());
}
Msg::CloseErrorPopup => {
self.umount_error();
}
Msg::CloseInfoPopup => {
self.umount_info();
}
Msg::CloseInstallUpdatePopup => {
assert!(self.app.umount(&Id::NewVersionChangelog).is_ok());
assert!(self.app.umount(&Id::InstallUpdatePopup).is_ok());
}
Msg::CloseKeybindingsPopup => {
self.umount_help();
}
Msg::CloseQuitPopup => self.umount_quit(),
Msg::CloseSaveBookmark => {
assert!(self.app.umount(&Id::BookmarkName).is_ok());
assert!(self.app.umount(&Id::BookmarkSavePassword).is_ok());
}
Msg::Connect => {
match self.collect_host_params() {
Err(err) => {
// mount error
self.mount_error(err);
}
Ok(params) => {
self.save_recent();
// Set file transfer params to context
self.context_mut().set_ftparams(params);
// Set exit reason
self.exit_reason = Some(super::ExitReason::Connect);
}
}
// Bookmarks commands
// <RIGHT> / <LEFT>
(COMPONENT_BOOKMARKS_LIST, key) if key == &MSG_KEY_RIGHT => {
// Give focus to recents
self.view.active(COMPONENT_RECENTS_LIST);
None
}
(COMPONENT_RECENTS_LIST, key) if key == &MSG_KEY_LEFT => {
// Give focus to bookmarks
self.view.active(COMPONENT_BOOKMARKS_LIST);
None
}
// <DEL | 'E'>
(COMPONENT_BOOKMARKS_LIST, key)
if key == &MSG_KEY_DEL || key == &MSG_KEY_CHAR_E =>
{
// Show delete popup
self.mount_bookmark_del_dialog();
None
}
(COMPONENT_RECENTS_LIST, key) if key == &MSG_KEY_DEL || key == &MSG_KEY_CHAR_E => {
// Show delete popup
self.mount_recent_del_dialog();
None
}
// Enter
(COMPONENT_BOOKMARKS_LIST, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => {
self.load_bookmark(*idx);
// Give focus to input password (or to protocol if not generic)
self.view.active(match self.input_mask() {
InputMask::Generic => COMPONENT_INPUT_PASSWORD,
InputMask::AwsS3 => COMPONENT_INPUT_S3_BUCKET,
});
None
}
(COMPONENT_RECENTS_LIST, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => {
self.load_recent(*idx);
// Give focus to input password
self.view.active(match self.input_mask() {
InputMask::Generic => COMPONENT_INPUT_PASSWORD,
InputMask::AwsS3 => COMPONENT_INPUT_S3_BUCKET,
});
None
}
// Bookmark radio
// Del bookmarks
(
COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK,
Msg::OnSubmit(Payload::One(Value::Usize(index))),
) => {
// hide bookmark delete
}
Msg::DeleteBookmark => {
if let Ok(State::One(StateValue::Usize(idx))) = self.app.state(&Id::BookmarksList) {
// Umount dialog
self.umount_bookmark_del_dialog();
// Index must be 0 => YES
match *index {
0 => {
// Get selected bookmark
match self.view.get_state(COMPONENT_BOOKMARKS_LIST) {
Some(Payload::One(Value::Usize(index))) => {
// Delete bookmark
self.del_bookmark(index);
// Update bookmarks
self.view_bookmarks()
}
_ => None,
}
}
_ => None,
}
}
(
COMPONENT_RADIO_BOOKMARK_DEL_RECENT,
Msg::OnSubmit(Payload::One(Value::Usize(index))),
) => {
// hide bookmark delete
self.umount_recent_del_dialog();
// Index must be 0 => YES
match *index {
0 => {
// Get selected bookmark
match self.view.get_state(COMPONENT_RECENTS_LIST) {
Some(Payload::One(Value::Usize(index))) => {
// Delete recent
self.del_recent(index);
// Update bookmarks
self.view_recent_connections()
}
_ => None,
}
}
_ => None,
}
}
// <ESC> hide tab
(COMPONENT_RADIO_BOOKMARK_DEL_RECENT, key) if key == &MSG_KEY_ESC => {
self.umount_recent_del_dialog();
None
}
(COMPONENT_RADIO_BOOKMARK_DEL_RECENT, _) => None,
(COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK, key) if key == &MSG_KEY_ESC => {
self.umount_bookmark_del_dialog();
None
}
(COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK, _) => None,
// Error message
(COMPONENT_TEXT_ERROR, key) if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => {
// Umount text error
self.umount_error();
None
}
// -- Text info
(COMPONENT_TEXT_INFO, key) if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => {
// Umount text info
self.umount_info();
None
}
(COMPONENT_TEXT_ERROR, _) | (COMPONENT_TEXT_INFO, _) => None,
// -- Text wait
(COMPONENT_TEXT_WAIT, _) => None,
// -- Release notes
(COMPONENT_TEXT_NEW_VERSION_NOTES, key) if key == &MSG_KEY_ESC => {
// Umount release notes
self.umount_release_notes();
None
}
(COMPONENT_TEXT_NEW_VERSION_NOTES, key) if key == &MSG_KEY_TAB => {
// Focus to radio update
self.view.active(COMPONENT_RADIO_INSTALL_UPDATE);
None
}
(COMPONENT_TEXT_NEW_VERSION_NOTES, _) => None,
// -- Install update radio
(COMPONENT_RADIO_INSTALL_UPDATE, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => {
// Install update
self.install_update();
None
}
(COMPONENT_RADIO_INSTALL_UPDATE, Msg::OnSubmit(Payload::One(Value::Usize(1)))) => {
// Umount
self.umount_release_notes();
None
}
(COMPONENT_RADIO_INSTALL_UPDATE, key) if key == &MSG_KEY_TAB => {
// Focus to changelog
self.view.active(COMPONENT_TEXT_NEW_VERSION_NOTES);
None
}
(COMPONENT_RADIO_INSTALL_UPDATE, _) => None,
// Help
(_, key) if key == &MSG_KEY_CTRL_H => {
// Show help
self.mount_help();
None
}
// Release notes
(_, key) if key == &MSG_KEY_CTRL_R => {
// Show release notes
self.mount_release_notes();
None
}
(COMPONENT_TEXT_HELP, key) if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => {
// Hide text help
self.umount_help();
None
}
(COMPONENT_TEXT_HELP, _) => None,
// Enter setup
(_, key) if key == &MSG_KEY_CTRL_C => {
self.exit_reason = Some(super::ExitReason::EnterSetup);
None
}
// Save bookmark; show popup
(_, key) if key == &MSG_KEY_CTRL_S => {
// Show popup
self.mount_bookmark_save_dialog();
// Give focus to bookmark name
self.view.active(COMPONENT_INPUT_BOOKMARK_NAME);
None
}
(COMPONENT_INPUT_BOOKMARK_NAME, key) if key == &MSG_KEY_DOWN => {
// Give focus to pwd
self.view.active(COMPONENT_RADIO_BOOKMARK_SAVE_PWD);
None
}
(COMPONENT_RADIO_BOOKMARK_SAVE_PWD, key) if key == &MSG_KEY_UP => {
// Give focus to pwd
self.view.active(COMPONENT_INPUT_BOOKMARK_NAME);
None
}
// Save bookmark
(COMPONENT_INPUT_BOOKMARK_NAME, Msg::OnSubmit(_))
| (COMPONENT_RADIO_BOOKMARK_SAVE_PWD, Msg::OnSubmit(_)) => {
// Get values
let bookmark_name: String =
match self.view.get_state(COMPONENT_INPUT_BOOKMARK_NAME) {
Some(Payload::One(Value::Str(s))) => s,
_ => String::new(),
};
let save_pwd: bool = matches!(
self.view.get_state(COMPONENT_RADIO_BOOKMARK_SAVE_PWD),
Some(Payload::One(Value::Usize(0)))
);
// Save bookmark
if !bookmark_name.is_empty() {
self.save_bookmark(bookmark_name, save_pwd);
}
// Umount popup
self.umount_bookmark_save_dialog();
// Reload bookmarks
// Delete bookmark
self.del_bookmark(idx);
// Update bookmarks
self.view_bookmarks()
}
// Hide save bookmark
(COMPONENT_INPUT_BOOKMARK_NAME, key) | (COMPONENT_RADIO_BOOKMARK_SAVE_PWD, key)
if key == &MSG_KEY_ESC =>
{
// Umount popup
self.umount_bookmark_save_dialog();
None
}
(COMPONENT_INPUT_BOOKMARK_NAME, _) | (COMPONENT_RADIO_BOOKMARK_SAVE_PWD, _) => None,
// Quit dialog
(COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(choice)))) => {
// If choice is 0, quit termscp
if *choice == 0 {
self.exit_reason = Some(super::ExitReason::Quit);
}
self.umount_quit();
None
}
(COMPONENT_RADIO_QUIT, key) if key == &MSG_KEY_ESC => {
self.umount_quit();
None
}
// -- text size error; block everything
(COMPONENT_TEXT_SIZE_ERR, _) => None,
// <TAB> bookmarks
(COMPONENT_BOOKMARKS_LIST, key) | (COMPONENT_RECENTS_LIST, key)
if key == &MSG_KEY_TAB =>
{
// Give focus to address
self.view.active(COMPONENT_RADIO_PROTOCOL);
None
}
// Any <TAB>, go to bookmarks
(_, key) if key == &MSG_KEY_TAB => {
self.view.active(COMPONENT_BOOKMARKS_LIST);
None
}
// On submit on any unhandled (connect)
(_, Msg::OnSubmit(_)) => self.on_unhandled_submit(),
(_, key) if key == &MSG_KEY_ENTER => self.on_unhandled_submit(),
// <ESC> => Quit
(_, key) if key == &MSG_KEY_ESC => {
self.mount_quit();
None
}
(_, _) => None, // Ignore other events
},
}
}
}
impl AuthActivity {
fn update_input_port(&mut self, port: u16) -> Option<(String, Msg)> {
match self.view.get_props(COMPONENT_INPUT_PORT) {
None => None,
Some(props) => {
let props = InputPropsBuilder::from(props)
.with_value(port.to_string())
.build();
self.view.update(COMPONENT_INPUT_PORT, props)
}
}
}
fn on_unhandled_submit(&mut self) -> Option<(String, Msg)> {
// Validate fields
match self.collect_host_params() {
Err(err) => {
// mount error
self.mount_error(err);
Msg::DeleteRecent => {
if let Ok(State::One(StateValue::Usize(idx))) = self.app.state(&Id::RecentsList) {
// Umount dialog
self.umount_recent_del_dialog();
// Delete recent
self.del_recent(idx);
// Update recents
self.view_recent_connections();
}
}
Ok(params) => {
self.save_recent();
// Set file transfer params to context
self.context_mut().set_ftparams(params);
// Set exit reason
self.exit_reason = Some(super::ExitReason::Connect);
Msg::EnterSetup => {
self.exit_reason = Some(ExitReason::EnterSetup);
}
Msg::InstallUpdate => {
self.install_update();
}
Msg::LoadBookmark(i) => {
self.load_bookmark(i);
// Give focus to input password (or to protocol if not generic)
assert!(self
.app
.active(match self.input_mask() {
InputMask::Generic => &Id::Password,
InputMask::AwsS3 => &Id::S3Bucket,
})
.is_ok());
}
Msg::LoadRecent(i) => {
self.load_recent(i);
// Give focus to input password (or to protocol if not generic)
assert!(self
.app
.active(match self.input_mask() {
InputMask::Generic => &Id::Password,
InputMask::AwsS3 => &Id::S3Bucket,
})
.is_ok());
}
Msg::ParamsFormBlur => {
assert!(self.app.active(&Id::BookmarksList).is_ok());
}
Msg::PasswordBlurDown => {
assert!(self.app.active(&Id::Protocol).is_ok());
}
Msg::PasswordBlurUp => {
assert!(self.app.active(&Id::Username).is_ok());
}
Msg::PortBlurDown => {
assert!(self.app.active(&Id::Username).is_ok());
}
Msg::PortBlurUp => {
assert!(self.app.active(&Id::Address).is_ok());
}
Msg::ProtocolBlurDown => {
assert!(self
.app
.active(match self.input_mask() {
InputMask::Generic => &Id::Address,
InputMask::AwsS3 => &Id::S3Bucket,
})
.is_ok());
}
Msg::ProtocolBlurUp => {
assert!(self
.app
.active(match self.input_mask() {
InputMask::Generic => &Id::Password,
InputMask::AwsS3 => &Id::S3Profile,
})
.is_ok());
}
Msg::ProtocolChanged(protocol) => {
self.protocol = protocol;
// Update port
let port: u16 = self.get_input_port();
if Self::is_port_standard(port) {
self.mount_port(Self::get_default_port_for_protocol(protocol));
}
}
Msg::Quit => {
self.exit_reason = Some(ExitReason::Quit);
}
Msg::RececentsListBlur => {
assert!(self.app.active(&Id::BookmarksList).is_ok());
}
Msg::S3BucketBlurDown => {
assert!(self.app.active(&Id::S3Region).is_ok());
}
Msg::S3BucketBlurUp => {
assert!(self.app.active(&Id::Protocol).is_ok());
}
Msg::S3RegionBlurDown => {
assert!(self.app.active(&Id::S3Profile).is_ok());
}
Msg::S3RegionBlurUp => {
assert!(self.app.active(&Id::S3Bucket).is_ok());
}
Msg::S3ProfileBlurDown => {
assert!(self.app.active(&Id::Protocol).is_ok());
}
Msg::S3ProfileBlurUp => {
assert!(self.app.active(&Id::S3Region).is_ok());
}
Msg::SaveBookmark => {
// get bookmark name
let (name, save_password) = self.get_new_bookmark();
// Save bookmark
if !name.is_empty() {
self.save_bookmark(name, save_password);
}
// Umount popup
self.umount_bookmark_save_dialog();
// Reload bookmarks
self.view_bookmarks()
}
Msg::SaveBookmarkPasswordBlur => {
assert!(self.app.active(&Id::BookmarkName).is_ok());
}
Msg::ShowDeleteBookmarkPopup => {
self.mount_bookmark_del_dialog();
}
Msg::ShowDeleteRecentPopup => {
self.mount_recent_del_dialog();
}
Msg::ShowKeybindingsPopup => {
self.mount_keybindings();
}
Msg::ShowQuitPopup => {
self.mount_quit();
}
Msg::ShowReleaseNotes => {
self.mount_release_notes();
}
Msg::ShowSaveBookmarkPopup => {
self.mount_bookmark_save_dialog();
}
Msg::UsernameBlurDown => {
assert!(self.app.active(&Id::Password).is_ok());
}
Msg::UsernameBlurUp => {
assert!(self.app.active(&Id::Port).is_ok());
}
Msg::None => {}
}
// Return None
None
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -29,7 +29,6 @@
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry, TransferPayload};
use crate::fs::FsFile;
// ext
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use std::fs::OpenOptions;
use std::io::Read;
use std::path::{Path, PathBuf};
@@ -109,13 +108,15 @@ impl FileTransferActivity {
}
}
// Put input mode back to normal
if let Err(err) = disable_raw_mode() {
if let Err(err) = self.context_mut().terminal().disable_raw_mode() {
error!("Failed to disable raw mode: {}", err);
}
// Leave alternate mode
if let Some(ctx) = self.context.as_mut() {
ctx.leave_alternate_screen();
if let Err(err) = self.context_mut().terminal().leave_alternate_screen() {
error!("Could not leave alternate screen: {}", err);
}
// Lock ports
assert!(self.app.lock_ports().is_ok());
// Open editor
match edit::edit_file(path) {
Ok(_) => self.log(
@@ -128,13 +129,20 @@ impl FileTransferActivity {
Err(err) => return Err(format!("Could not open editor: {}", err)),
}
if let Some(ctx) = self.context.as_mut() {
// Clear screen
ctx.clear_screen();
if let Err(err) = ctx.terminal().clear_screen() {
error!("Could not clear screen screen: {}", err);
}
// Enter alternate mode
ctx.enter_alternate_screen();
if let Err(err) = ctx.terminal().enter_alternate_screen() {
error!("Could not enter alternate screen: {}", err);
}
// Re-enable raw mode
if let Err(err) = ctx.terminal().enable_raw_mode() {
error!("Failed to enter raw mode: {}", err);
}
// Unlock ports
assert!(self.app.unlock_ports().is_ok());
}
// Re-enable raw mode
let _ = enable_raw_mode();
Ok(())
}

View File

@@ -26,10 +26,10 @@
* SOFTWARE.
*/
pub(self) use super::{
browser::FileExplorerTab, FileTransferActivity, FsEntry, LogLevel, TransferOpts,
browser::FileExplorerTab, FileTransferActivity, FsEntry, Id, LogLevel, TransferOpts,
TransferPayload,
};
use tuirealm::{Payload, Value};
use tuirealm::{State, StateValue};
// actions
pub(crate) mod change_dir;
@@ -79,7 +79,7 @@ impl FileTransferActivity {
///
/// Get local file entry
pub(crate) fn get_local_selected_entries(&self) -> SelectedEntry {
match self.get_selected_index(super::COMPONENT_EXPLORER_LOCAL) {
match self.get_selected_index(&Id::ExplorerLocal) {
SelectedEntryIndex::One(idx) => SelectedEntry::from(self.local().get(idx)),
SelectedEntryIndex::Many(files) => {
let files: Vec<&FsEntry> = files
@@ -97,7 +97,7 @@ impl FileTransferActivity {
///
/// Get remote file entry
pub(crate) fn get_remote_selected_entries(&self) -> SelectedEntry {
match self.get_selected_index(super::COMPONENT_EXPLORER_REMOTE) {
match self.get_selected_index(&Id::ExplorerRemote) {
SelectedEntryIndex::One(idx) => SelectedEntry::from(self.remote().get(idx)),
SelectedEntryIndex::Many(files) => {
let files: Vec<&FsEntry> = files
@@ -115,7 +115,7 @@ impl FileTransferActivity {
///
/// Get remote file entry
pub(crate) fn get_found_selected_entries(&self) -> SelectedEntry {
match self.get_selected_index(super::COMPONENT_EXPLORER_FIND) {
match self.get_selected_index(&Id::ExplorerFind) {
SelectedEntryIndex::One(idx) => {
SelectedEntry::from(self.found().as_ref().unwrap().get(idx))
}
@@ -133,14 +133,14 @@ impl FileTransferActivity {
// -- private
fn get_selected_index(&self, component: &str) -> SelectedEntryIndex {
match self.view.get_state(component) {
Some(Payload::One(Value::Usize(idx))) => SelectedEntryIndex::One(idx),
Some(Payload::Vec(files)) => {
fn get_selected_index(&self, id: &Id) -> SelectedEntryIndex {
match self.app.state(id) {
Ok(State::One(StateValue::Usize(idx))) => SelectedEntryIndex::One(idx),
Ok(State::Vec(files)) => {
let list: Vec<usize> = files
.iter()
.map(|x| match x {
Value::Usize(v) => *v,
StateValue::Usize(v) => *v,
_ => 0,
})
.collect();

View File

@@ -160,7 +160,9 @@ impl FileTransferActivity {
// NOTE: clear screen in order to prevent crap on stderr
if let Some(ctx) = self.context.as_mut() {
// Clear screen
ctx.clear_screen();
if let Err(err) = ctx.terminal().clear_screen() {
error!("Could not clear screen screen: {}", err);
}
}
}
}

View File

@@ -0,0 +1,296 @@
//! ## Log
//!
//! log tab component
/**
* 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 super::{Msg, UiMsg};
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::event::{Key, KeyEvent};
use tuirealm::props::{Alignment, AttrValue, Attribute, Borders, Color, Style, Table};
use tuirealm::tui::layout::Corner;
use tuirealm::tui::widgets::{List as TuiList, ListItem, ListState};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, Props, State, StateValue};
pub struct Log {
props: Props,
states: OwnStates,
}
impl Log {
pub fn new(lines: Table, fg: Color, bg: Color) -> Self {
let mut props = Props::default();
props.set(
Attribute::Borders,
AttrValue::Borders(Borders::default().color(fg)),
);
props.set(Attribute::Background, AttrValue::Color(bg));
props.set(Attribute::Content, AttrValue::Table(lines));
Self {
props,
states: OwnStates::default(),
}
}
}
impl MockComponent for Log {
fn view(&mut self, frame: &mut tuirealm::Frame, area: tuirealm::tui::layout::Rect) {
let width: usize = area.width as usize - 4;
let focus = self
.props
.get_or(Attribute::Focus, AttrValue::Flag(false))
.unwrap_flag();
let fg = self
.props
.get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
.unwrap_color();
let bg = self
.props
.get_or(Attribute::Background, AttrValue::Color(Color::Reset))
.unwrap_color();
// Make list
let list_items: Vec<ListItem> = self
.props
.get(Attribute::Content)
.unwrap()
.unwrap_table()
.iter()
.map(|row| ListItem::new(tui_realm_stdlib::utils::wrap_spans(row, width, &self.props)))
.collect();
let w = TuiList::new(list_items)
.block(tui_realm_stdlib::utils::get_block(
Borders::default().color(fg),
Some(("Log".to_string(), Alignment::Left)),
focus,
None,
))
.start_corner(Corner::BottomLeft)
.highlight_symbol(">> ")
.style(Style::default().bg(bg))
.highlight_style(Style::default());
let mut state: ListState = ListState::default();
state.select(Some(self.states.get_list_index()));
frame.render_stateful_widget(w, area, &mut state);
}
fn query(&self, attr: Attribute) -> Option<AttrValue> {
self.props.get(attr)
}
fn attr(&mut self, attr: Attribute, value: AttrValue) {
self.props.set(attr, value);
if matches!(attr, Attribute::Content) {
self.states.set_list_len(
match self.props.get(Attribute::Content).map(|x| x.unwrap_table()) {
Some(spans) => spans.len(),
_ => 0,
},
);
self.states.reset_list_index();
}
}
fn state(&self) -> State {
State::One(StateValue::Usize(self.states.get_list_index()))
}
fn perform(&mut self, cmd: Cmd) -> CmdResult {
match cmd {
Cmd::Move(Direction::Down) => {
let prev = self.states.get_list_index();
self.states.incr_list_index();
if prev != self.states.get_list_index() {
CmdResult::Changed(self.state())
} else {
CmdResult::None
}
}
Cmd::Move(Direction::Up) => {
let prev = self.states.get_list_index();
self.states.decr_list_index();
if prev != self.states.get_list_index() {
CmdResult::Changed(self.state())
} else {
CmdResult::None
}
}
Cmd::Scroll(Direction::Down) => {
let prev = self.states.get_list_index();
(0..8).for_each(|_| self.states.incr_list_index());
if prev != self.states.get_list_index() {
CmdResult::Changed(self.state())
} else {
CmdResult::None
}
}
Cmd::Scroll(Direction::Up) => {
let prev = self.states.get_list_index();
(0..8).for_each(|_| self.states.decr_list_index());
if prev != self.states.get_list_index() {
CmdResult::Changed(self.state())
} else {
CmdResult::None
}
}
Cmd::GoTo(Position::Begin) => {
let prev = self.states.get_list_index();
self.states.reset_list_index();
if prev != self.states.get_list_index() {
CmdResult::Changed(self.state())
} else {
CmdResult::None
}
}
Cmd::GoTo(Position::End) => {
let prev = self.states.get_list_index();
self.states.list_index_at_last();
if prev != self.states.get_list_index() {
CmdResult::Changed(self.state())
} else {
CmdResult::None
}
}
_ => CmdResult::None,
}
}
}
impl Component<Msg, NoUserEvent> for Log {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
self.perform(Cmd::Move(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => {
self.perform(Cmd::Move(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::PageUp, ..
}) => {
self.perform(Cmd::Scroll(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::PageDown,
..
}) => {
self.perform(Cmd::Scroll(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
// -- comp msg
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::Ui(UiMsg::LogTabbed)),
_ => None,
}
}
}
// -- states
/// ## OwnStates
///
/// OwnStates contains states for this component
#[derive(Clone)]
struct OwnStates {
list_index: usize, // Index of selected element in list
list_len: usize, // Length of file list
focus: bool, // Has focus?
}
impl Default for OwnStates {
fn default() -> Self {
OwnStates {
list_index: 0,
list_len: 0,
focus: false,
}
}
}
impl OwnStates {
/// ### set_list_len
///
/// Set list length
pub fn set_list_len(&mut self, len: usize) {
self.list_len = len;
}
/// ### get_list_index
///
/// Return current value for list index
pub fn get_list_index(&self) -> usize {
self.list_index
}
/// ### incr_list_index
///
/// Incremenet list index
pub fn incr_list_index(&mut self) {
// Check if index is at last element
if self.list_index + 1 < self.list_len {
self.list_index += 1;
}
}
/// ### decr_list_index
///
/// Decrement list index
pub fn decr_list_index(&mut self) {
// Check if index is bigger than 0
if self.list_index > 0 {
self.list_index -= 1;
}
}
/// ### list_index_at_last
///
/// Set list index at last item
pub fn list_index_at_last(&mut self) {
self.list_index = match self.list_len {
0 => 0,
len => len - 1,
};
}
/// ### reset_list_index
///
/// Reset list index to last element
pub fn reset_list_index(&mut self) {
self.list_index = 0; // Last element is always 0
}
}

View File

@@ -0,0 +1,72 @@
//! ## Components
//!
//! file transfer activity 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.
*/
use super::{Msg, TransferMsg, UiMsg};
use tui_realm_stdlib::Phantom;
use tuirealm::{
event::{Event, Key, KeyEvent, KeyModifiers},
Component, MockComponent, NoUserEvent,
};
// -- export
mod log;
mod popups;
mod transfer;
pub use self::log::Log;
pub use popups::{
CopyPopup, DeletePopup, DisconnectPopup, ErrorPopup, ExecPopup, FatalPopup, FileInfoPopup,
FindPopup, GoToPopup, KeybindingsPopup, MkdirPopup, NewfilePopup, OpenWithPopup,
ProgressBarFull, ProgressBarPartial, QuitPopup, RenamePopup, ReplacePopup,
ReplacingFilesListPopup, SaveAsPopup, SortingPopup, StatusBarLocal, StatusBarRemote, WaitPopup,
};
pub use transfer::{ExplorerFind, ExplorerLocal, ExplorerRemote};
#[derive(Default, MockComponent)]
pub struct GlobalListener {
component: Phantom,
}
impl Component<Msg, NoUserEvent> for GlobalListener {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Ui(UiMsg::ShowDisconnectPopup))
}
Event::Keyboard(KeyEvent {
code: Key::Char('q'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowQuitPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('h'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowKeybindingsPopup)),
_ => None,
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,400 @@
//! ## FileList
//!
//! `FileList` component renders a file list tab
/**
* 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 tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::props::{
Alignment, AttrValue, Attribute, Borders, Color, Style, Table, TextModifiers,
};
use tuirealm::tui::layout::Corner;
use tuirealm::tui::text::{Span, Spans};
use tuirealm::tui::widgets::{List as TuiList, ListItem, ListState};
use tuirealm::{MockComponent, Props, State, StateValue};
pub const FILE_LIST_CMD_SELECT_ALL: &str = "A";
/// ## OwnStates
///
/// OwnStates contains states for this component
#[derive(Clone)]
struct OwnStates {
list_index: usize, // Index of selected element in list
selected: Vec<usize>, // Selected files
}
impl Default for OwnStates {
fn default() -> Self {
OwnStates {
list_index: 0,
selected: Vec::new(),
}
}
}
impl OwnStates {
/// ### init_list_states
///
/// Initialize list states
pub fn init_list_states(&mut self, len: usize) {
self.selected = Vec::with_capacity(len);
self.fix_list_index();
}
/// ### list_index
///
/// Return current value for list index
pub fn list_index(&self) -> usize {
self.list_index
}
/// ### incr_list_index
///
/// Incremenet list index.
/// If `can_rewind` is `true` the index rewinds when boundary is reached
pub fn incr_list_index(&mut self, can_rewind: bool) {
// Check if index is at last element
if self.list_index + 1 < self.list_len() {
self.list_index += 1;
} else if can_rewind {
self.list_index = 0;
}
}
/// ### decr_list_index
///
/// Decrement list index
/// If `can_rewind` is `true` the index rewinds when boundary is reached
pub fn decr_list_index(&mut self, can_rewind: bool) {
// Check if index is bigger than 0
if self.list_index > 0 {
self.list_index -= 1;
} else if self.list_len() > 0 && can_rewind {
self.list_index = self.list_len() - 1;
}
}
pub fn list_index_at_first(&mut self) {
self.list_index = 0;
}
pub fn list_index_at_last(&mut self) {
self.list_index = match self.list_len() {
0 => 0,
len => len - 1,
};
}
/// ### list_len
///
/// Returns the length of the file list, which is actually the capacity of the selection vector
pub fn list_len(&self) -> usize {
self.selected.capacity()
}
/// ### is_selected
///
/// Returns whether the file with index `entry` is selected
pub fn is_selected(&self, entry: usize) -> bool {
self.selected.contains(&entry)
}
/// ### is_selection_empty
///
/// Returns whether the selection is currently empty
pub fn is_selection_empty(&self) -> bool {
self.selected.is_empty()
}
/// ### get_selection
///
/// Returns current file selection
pub fn get_selection(&self) -> Vec<usize> {
self.selected.clone()
}
/// ### fix_list_index
///
/// Keep index if possible, otherwise set to lenght - 1
fn fix_list_index(&mut self) {
if self.list_index >= self.list_len() && self.list_len() > 0 {
self.list_index = self.list_len() - 1;
} else if self.list_len() == 0 {
self.list_index = 0;
}
}
// -- select manipulation
/// ### toggle_file
///
/// Select or deselect file with provided entry index
pub fn toggle_file(&mut self, entry: usize) {
match self.is_selected(entry) {
true => self.deselect(entry),
false => self.select(entry),
}
}
/// ### select_all
///
/// Select all files
pub fn select_all(&mut self) {
for i in 0..self.list_len() {
self.select(i);
}
}
/// ### select
///
/// Select provided index if not selected yet
fn select(&mut self, entry: usize) {
if !self.is_selected(entry) {
self.selected.push(entry);
}
}
/// ### deselect
///
/// Remove element file with associated index
fn deselect(&mut self, entry: usize) {
if self.is_selected(entry) {
self.selected.retain(|&x| x != entry);
}
}
}
#[derive(Default)]
pub struct FileList {
props: Props,
states: OwnStates,
}
impl FileList {
pub fn foreground(mut self, fg: Color) -> Self {
self.attr(Attribute::Foreground, AttrValue::Color(fg));
self
}
pub fn background(mut self, bg: Color) -> Self {
self.attr(Attribute::Background, AttrValue::Color(bg));
self
}
pub fn borders(mut self, b: Borders) -> Self {
self.attr(Attribute::Borders, AttrValue::Borders(b));
self
}
pub fn title<S: AsRef<str>>(mut self, t: S, a: Alignment) -> Self {
self.attr(
Attribute::Title,
AttrValue::Title((t.as_ref().to_string(), a)),
);
self
}
pub fn highlighted_color(mut self, c: Color) -> Self {
self.attr(Attribute::HighlightedColor, AttrValue::Color(c));
self
}
pub fn rows(mut self, rows: Table) -> Self {
self.attr(Attribute::Content, AttrValue::Table(rows));
self
}
}
impl MockComponent for FileList {
fn view(&mut self, frame: &mut tuirealm::Frame, area: tuirealm::tui::layout::Rect) {
let title = self
.props
.get_or(
Attribute::Title,
AttrValue::Title((String::default(), Alignment::Left)),
)
.unwrap_title();
let borders = self
.props
.get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
.unwrap_borders();
let focus = self
.props
.get_or(Attribute::Focus, AttrValue::Flag(false))
.unwrap_flag();
let div = tui_realm_stdlib::utils::get_block(borders, Some(title), focus, None);
// Make list entries
let list_items: Vec<ListItem> = match self
.props
.get(Attribute::Content)
.map(|x| x.unwrap_table())
{
Some(table) => table
.iter()
.enumerate()
.map(|(num, row)| {
let columns: Vec<Span> = row
.iter()
.map(|col| {
let (fg, bg, mut modifiers) =
tui_realm_stdlib::utils::use_or_default_styles(&self.props, col);
if self.states.is_selected(num) {
modifiers |= TextModifiers::REVERSED
| TextModifiers::UNDERLINED
| TextModifiers::ITALIC;
}
Span::styled(
col.content.clone(),
Style::default().add_modifier(modifiers).fg(fg).bg(bg),
)
})
.collect();
ListItem::new(Spans::from(columns))
})
.collect(), // Make List item from TextSpan
_ => Vec::new(),
};
let highlighted_color = self
.props
.get(Attribute::HighlightedColor)
.map(|x| x.unwrap_color());
let modifiers = match focus {
true => TextModifiers::REVERSED,
false => TextModifiers::empty(),
};
// Make list
let mut list = TuiList::new(list_items)
.block(div)
.start_corner(Corner::TopLeft);
if let Some(highlighted_color) = highlighted_color {
list = list.highlight_style(
Style::default()
.fg(highlighted_color)
.add_modifier(modifiers),
);
}
let mut state: ListState = ListState::default();
state.select(Some(self.states.list_index));
frame.render_stateful_widget(list, area, &mut state);
}
fn attr(&mut self, attr: Attribute, value: AttrValue) {
self.props.set(attr, value);
if matches!(attr, Attribute::Content) {
self.states.init_list_states(
match self.props.get(Attribute::Content).map(|x| x.unwrap_table()) {
Some(spans) => spans.len(),
_ => 0,
},
);
self.states.fix_list_index();
}
}
fn query(&self, attr: Attribute) -> Option<AttrValue> {
self.props.get(attr)
}
fn state(&self) -> State {
match self.states.is_selection_empty() {
true => State::One(StateValue::Usize(self.states.list_index())),
false => State::Vec(
self.states
.get_selection()
.into_iter()
.map(StateValue::Usize)
.collect(),
),
}
}
fn perform(&mut self, cmd: Cmd) -> CmdResult {
match cmd {
Cmd::Move(Direction::Down) => {
let prev = self.states.list_index;
self.states.incr_list_index(true);
if prev != self.states.list_index {
CmdResult::Changed(self.state())
} else {
CmdResult::None
}
}
Cmd::Move(Direction::Up) => {
let prev = self.states.list_index;
self.states.decr_list_index(true);
if prev != self.states.list_index {
CmdResult::Changed(self.state())
} else {
CmdResult::None
}
}
Cmd::Scroll(Direction::Down) => {
let prev = self.states.list_index;
(0..8).for_each(|_| self.states.incr_list_index(false));
if prev != self.states.list_index {
CmdResult::Changed(self.state())
} else {
CmdResult::None
}
}
Cmd::Scroll(Direction::Up) => {
let prev = self.states.list_index;
(0..8).for_each(|_| self.states.decr_list_index(false));
if prev != self.states.list_index {
CmdResult::Changed(self.state())
} else {
CmdResult::None
}
}
Cmd::GoTo(Position::Begin) => {
let prev = self.states.list_index;
self.states.list_index_at_first();
if prev != self.states.list_index {
CmdResult::Changed(self.state())
} else {
CmdResult::None
}
}
Cmd::GoTo(Position::End) => {
let prev = self.states.list_index;
self.states.list_index_at_last();
if prev != self.states.list_index {
CmdResult::Changed(self.state())
} else {
CmdResult::None
}
}
Cmd::Custom(FILE_LIST_CMD_SELECT_ALL) => {
self.states.select_all();
CmdResult::None
}
Cmd::Toggle => {
self.states.toggle_file(self.states.list_index());
CmdResult::None
}
_ => CmdResult::None,
}
}
}

View File

@@ -0,0 +1,494 @@
//! ## Transfer
//!
//! file transfer 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.
*/
use super::{Msg, TransferMsg, UiMsg};
mod file_list;
use file_list::FileList;
use tuirealm::command::{Cmd, Direction, Position};
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
use tuirealm::props::{Alignment, Borders, Color, TextSpan};
use tuirealm::{Component, Event, MockComponent, NoUserEvent};
#[derive(MockComponent)]
pub struct ExplorerFind {
component: FileList,
}
impl ExplorerFind {
pub fn new<S: AsRef<str>>(title: S, files: &[&str], bg: Color, fg: Color, hg: Color) -> Self {
Self {
component: FileList::default()
.background(bg)
.borders(Borders::default().color(hg))
.foreground(fg)
.highlighted_color(hg)
.title(title, Alignment::Left)
.rows(files.iter().map(|x| vec![TextSpan::from(x)]).collect()),
}
}
}
impl Component<Msg, NoUserEvent> for ExplorerFind {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => {
self.perform(Cmd::Move(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
self.perform(Cmd::Move(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::PageDown,
..
}) => {
self.perform(Cmd::Scroll(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::PageUp, ..
}) => {
self.perform(Cmd::Scroll(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Char('a'),
modifiers: KeyModifiers::CONTROL,
}) => {
let _ = self.perform(Cmd::Custom(file_list::FILE_LIST_CMD_SELECT_ALL));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Char('m'),
modifiers: KeyModifiers::NONE,
}) => {
let _ = self.perform(Cmd::Toggle);
Some(Msg::None)
}
// -- comp msg
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
Some(Msg::Ui(UiMsg::ExplorerTabbed))
}
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Ui(UiMsg::CloseFindExplorer))
}
Event::Keyboard(KeyEvent {
code: Key::Left | Key::Right,
..
}) => Some(Msg::Ui(UiMsg::ChangeTransferWindow)),
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => Some(Msg::Transfer(TransferMsg::EnterDirectory)),
Event::Keyboard(KeyEvent {
code: Key::Char(' '),
..
}) => Some(Msg::Transfer(TransferMsg::TransferFile)),
Event::Keyboard(KeyEvent {
code: Key::Backspace,
..
}) => Some(Msg::Transfer(TransferMsg::GoToPreviousDirectory)),
Event::Keyboard(KeyEvent {
code: Key::Char('a'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ToggleHiddenFiles)),
Event::Keyboard(KeyEvent {
code: Key::Char('b'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowFileSortingPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('e') | Key::Delete,
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowDeletePopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('i'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowFileInfoPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('s'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowSaveAsPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('v'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Transfer(TransferMsg::OpenFile)),
Event::Keyboard(KeyEvent {
code: Key::Char('w'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowOpenWithPopup)),
_ => None,
}
}
}
#[derive(MockComponent)]
pub struct ExplorerLocal {
component: FileList,
}
impl ExplorerLocal {
pub fn new<S: AsRef<str>>(title: S, files: &[&str], bg: Color, fg: Color, hg: Color) -> Self {
Self {
component: FileList::default()
.background(bg)
.borders(Borders::default().color(hg))
.foreground(fg)
.highlighted_color(hg)
.title(title, Alignment::Left)
.rows(files.iter().map(|x| vec![TextSpan::from(x)]).collect()),
}
}
}
impl Component<Msg, NoUserEvent> for ExplorerLocal {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => {
self.perform(Cmd::Move(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
self.perform(Cmd::Move(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::PageDown,
..
}) => {
self.perform(Cmd::Scroll(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::PageUp, ..
}) => {
self.perform(Cmd::Scroll(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Char('a'),
modifiers: KeyModifiers::CONTROL,
}) => {
let _ = self.perform(Cmd::Custom(file_list::FILE_LIST_CMD_SELECT_ALL));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Char('m'),
modifiers: KeyModifiers::NONE,
}) => {
let _ = self.perform(Cmd::Toggle);
Some(Msg::None)
}
// -- comp msg
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
Some(Msg::Ui(UiMsg::ExplorerTabbed))
}
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Ui(UiMsg::ShowDisconnectPopup))
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => Some(Msg::Ui(UiMsg::ChangeTransferWindow)),
Event::Keyboard(KeyEvent {
code: Key::Backspace,
..
}) => Some(Msg::Transfer(TransferMsg::GoToPreviousDirectory)),
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => Some(Msg::Transfer(TransferMsg::EnterDirectory)),
Event::Keyboard(KeyEvent {
code: Key::Char(' '),
..
}) => Some(Msg::Transfer(TransferMsg::TransferFile)),
Event::Keyboard(KeyEvent {
code: Key::Char('a'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ToggleHiddenFiles)),
Event::Keyboard(KeyEvent {
code: Key::Char('b'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowFileSortingPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('c'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowCopyPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('d'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowMkdirPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('e') | Key::Delete,
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowDeletePopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('f'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowFindPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('g'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowGotoPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('i'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowFileInfoPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('l'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Transfer(TransferMsg::ReloadDir)),
Event::Keyboard(KeyEvent {
code: Key::Char('n'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowNewFilePopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('o'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Transfer(TransferMsg::OpenTextFile)),
Event::Keyboard(KeyEvent {
code: Key::Char('r'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowRenamePopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('s'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowSaveAsPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('u'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Transfer(TransferMsg::GoToParentDirectory)),
Event::Keyboard(KeyEvent {
code: Key::Char('x'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowExecPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('y'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ToggleSyncBrowsing)),
Event::Keyboard(KeyEvent {
code: Key::Char('v'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Transfer(TransferMsg::OpenFile)),
Event::Keyboard(KeyEvent {
code: Key::Char('w'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowOpenWithPopup)),
_ => None,
}
}
}
#[derive(MockComponent)]
pub struct ExplorerRemote {
component: FileList,
}
impl ExplorerRemote {
pub fn new<S: AsRef<str>>(title: S, files: &[&str], bg: Color, fg: Color, hg: Color) -> Self {
Self {
component: FileList::default()
.background(bg)
.borders(Borders::default().color(hg))
.foreground(fg)
.highlighted_color(hg)
.title(title, Alignment::Left)
.rows(files.iter().map(|x| vec![TextSpan::from(x)]).collect()),
}
}
}
impl Component<Msg, NoUserEvent> for ExplorerRemote {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => {
self.perform(Cmd::Move(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
self.perform(Cmd::Move(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::PageDown,
..
}) => {
self.perform(Cmd::Scroll(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::PageUp, ..
}) => {
self.perform(Cmd::Scroll(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Char('a'),
modifiers: KeyModifiers::CONTROL,
}) => {
let _ = self.perform(Cmd::Custom(file_list::FILE_LIST_CMD_SELECT_ALL));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Char('m'),
modifiers: KeyModifiers::NONE,
}) => {
let _ = self.perform(Cmd::Toggle);
Some(Msg::None)
}
// -- comp msg
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
Some(Msg::Ui(UiMsg::ExplorerTabbed))
}
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Ui(UiMsg::ShowDisconnectPopup))
}
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => Some(Msg::Ui(UiMsg::ChangeTransferWindow)),
Event::Keyboard(KeyEvent {
code: Key::Backspace,
..
}) => Some(Msg::Transfer(TransferMsg::GoToPreviousDirectory)),
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => Some(Msg::Transfer(TransferMsg::EnterDirectory)),
Event::Keyboard(KeyEvent {
code: Key::Char(' '),
..
}) => Some(Msg::Transfer(TransferMsg::TransferFile)),
Event::Keyboard(KeyEvent {
code: Key::Char('a'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ToggleHiddenFiles)),
Event::Keyboard(KeyEvent {
code: Key::Char('b'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowFileSortingPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('c'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowCopyPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('d'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowMkdirPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('e') | Key::Delete,
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowDeletePopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('f'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowFindPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('g'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowGotoPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('i'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowFileInfoPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('l'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Transfer(TransferMsg::ReloadDir)),
Event::Keyboard(KeyEvent {
code: Key::Char('n'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowNewFilePopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('o'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Transfer(TransferMsg::OpenTextFile)),
Event::Keyboard(KeyEvent {
code: Key::Char('r'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowRenamePopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('s'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowSaveAsPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('u'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Transfer(TransferMsg::GoToParentDirectory)),
Event::Keyboard(KeyEvent {
code: Key::Char('x'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowExecPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('y'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ToggleSyncBrowsing)),
Event::Keyboard(KeyEvent {
code: Key::Char('v'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Transfer(TransferMsg::OpenFile)),
Event::Keyboard(KeyEvent {
code: Key::Char('w'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowOpenWithPopup)),
_ => None,
}
}
}

View File

@@ -34,7 +34,7 @@ use std::path::Path;
/// ## FileExplorerTab
///
/// File explorer tab
#[derive(Clone, Copy)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum FileExplorerTab {
Local,
Remote,

View File

@@ -22,22 +22,50 @@
* SOFTWARE.
*/
// Locals
use super::{ConfigClient, FileTransferActivity, LogLevel, LogRecord, TransferPayload};
use super::{
browser::FileExplorerTab, ConfigClient, FileTransferActivity, Id, LogLevel, LogRecord,
TransferPayload,
};
use crate::filetransfer::ProtocolParams;
use crate::system::environment;
use crate::system::notifications::Notification;
use crate::system::sshkey_storage::SshKeyStorage;
use crate::utils::fmt::fmt_millis;
use crate::utils::fmt::{fmt_millis, fmt_path_elide_ex};
use crate::utils::path;
// Ext
use bytesize::ByteSize;
use std::env;
use std::path::{Path, PathBuf};
use tuirealm::Update;
use tuirealm::props::{
Alignment, AttrValue, Attribute, Color, PropPayload, PropValue, TableBuilder, TextSpan,
};
use tuirealm::{PollStrategy, Update};
const LOG_CAPACITY: usize = 256;
impl FileTransferActivity {
/// ### tick
///
/// Call `Application::tick()` and process messages in `Update`
pub(super) fn tick(&mut self) {
match self.app.tick(PollStrategy::UpTo(3)) {
Ok(messages) => {
if !messages.is_empty() {
self.redraw = true;
}
for msg in messages.into_iter() {
let mut msg = Some(msg);
while msg.is_some() {
msg = self.update(msg);
}
}
}
Err(err) => {
self.mount_error(format!("Application error: {}", err));
}
}
}
/// ### log
///
/// Add message to log events
@@ -57,8 +85,7 @@ impl FileTransferActivity {
// Eventually push front the new record
self.log_records.push_front(record);
// Update log
let msg = self.update_logbox();
self.update(msg);
self.update_logbox();
}
/// ### log_and_alert
@@ -68,8 +95,7 @@ impl FileTransferActivity {
self.mount_error(msg.as_str());
self.log(level, msg);
// Update log
let msg = self.update_logbox();
self.update(msg);
self.update_logbox();
}
/// ### init_config_client
@@ -108,23 +134,6 @@ impl FileTransferActivity {
env::set_var("EDITOR", self.config().get_text_editor());
}
/// ### read_input_event
///
/// Read one event.
/// Returns whether at least one event has been handled
pub(super) fn read_input_event(&mut self) -> bool {
if let Ok(Some(event)) = self.context().input_hnd().read_event() {
// Handle event
let msg = self.view.on(event);
self.update(msg);
// Return true
true
} else {
// Error
false
}
}
/// ### local_to_abs_path
///
/// Convert a path to absolute according to local explorer
@@ -231,4 +240,245 @@ impl FileTransferActivity {
}
}
}
/// ### update_local_filelist
///
/// Update local file list
pub(super) fn update_local_filelist(&mut self) {
// Get width
let width: usize = self
.context()
.store()
.get_unsigned(super::STORAGE_EXPLORER_WIDTH)
.unwrap_or(256);
let hostname: String = match hostname::get() {
Ok(h) => {
let hostname: String = h.as_os_str().to_string_lossy().to_string();
let tokens: Vec<&str> = hostname.split('.').collect();
String::from(*tokens.get(0).unwrap_or(&"localhost"))
}
Err(_) => String::from("localhost"),
};
let hostname: String = format!(
"{}:{} ",
hostname,
fmt_path_elide_ex(self.local().wrkdir.as_path(), width, hostname.len() + 3) // 3 because of '/…/'
);
let files: Vec<Vec<TextSpan>> = self
.local()
.iter_files()
.map(|x| vec![TextSpan::from(self.local().fmt_file(x))])
.collect();
// Update content and title
assert!(self
.app
.attr(
&Id::ExplorerLocal,
Attribute::Content,
AttrValue::Table(files)
)
.is_ok());
assert!(self
.app
.attr(
&Id::ExplorerLocal,
Attribute::Title,
AttrValue::Title((hostname, Alignment::Left))
)
.is_ok());
}
/// ### update_remote_filelist
///
/// Update remote file list
pub(super) fn update_remote_filelist(&mut self) {
let width: usize = self
.context()
.store()
.get_unsigned(super::STORAGE_EXPLORER_WIDTH)
.unwrap_or(256);
let hostname = self.get_remote_hostname();
let hostname: String = format!(
"{}:{} ",
hostname,
fmt_path_elide_ex(
self.remote().wrkdir.as_path(),
width,
hostname.len() + 3 // 3 because of '/…/'
)
);
let files: Vec<Vec<TextSpan>> = self
.remote()
.iter_files()
.map(|x| vec![TextSpan::from(self.remote().fmt_file(x))])
.collect();
// Update content and title
assert!(self
.app
.attr(
&Id::ExplorerRemote,
Attribute::Content,
AttrValue::Table(files)
)
.is_ok());
assert!(self
.app
.attr(
&Id::ExplorerRemote,
Attribute::Title,
AttrValue::Title((hostname, Alignment::Left))
)
.is_ok());
}
/// ### update_logbox
///
/// Update log box
pub(super) fn update_logbox(&mut self) {
let mut table: TableBuilder = TableBuilder::default();
for (idx, record) in self.log_records.iter().enumerate() {
// Add row if not first row
if idx > 0 {
table.add_row();
}
let fg = match record.level {
LogLevel::Error => Color::Red,
LogLevel::Warn => Color::Yellow,
LogLevel::Info => Color::Green,
};
table
.add_col(TextSpan::from(format!(
"{}",
record.time.format("%Y-%m-%dT%H:%M:%S%Z")
)))
.add_col(TextSpan::from(" ["))
.add_col(
TextSpan::new(
format!(
"{:5}",
match record.level {
LogLevel::Error => "ERROR",
LogLevel::Warn => "WARN",
LogLevel::Info => "INFO",
}
)
.as_str(),
)
.fg(fg),
)
.add_col(TextSpan::from("]: "))
.add_col(TextSpan::from(record.msg.as_str()));
}
assert!(self
.app
.attr(
&Id::Log,
Attribute::Content,
AttrValue::Table(table.build())
)
.is_ok());
}
pub(super) fn update_progress_bar(&mut self, filename: String) {
assert!(self
.app
.attr(
&Id::ProgressBarFull,
Attribute::Text,
AttrValue::String(self.transfer.full.to_string())
)
.is_ok());
assert!(self
.app
.attr(
&Id::ProgressBarFull,
Attribute::Value,
AttrValue::Payload(PropPayload::One(PropValue::F64(
self.transfer.full.calc_progress()
)))
)
.is_ok());
assert!(self
.app
.attr(
&Id::ProgressBarPartial,
Attribute::Text,
AttrValue::String(self.transfer.partial.to_string())
)
.is_ok());
assert!(self
.app
.attr(
&Id::ProgressBarPartial,
Attribute::Value,
AttrValue::Payload(PropPayload::One(PropValue::F64(
self.transfer.partial.calc_progress()
)))
)
.is_ok());
assert!(self
.app
.attr(
&Id::ProgressBarPartial,
Attribute::Title,
AttrValue::Title((filename, Alignment::Left))
)
.is_ok());
}
/// ### finalize_find
///
/// Finalize find process
pub(super) fn finalize_find(&mut self) {
// Set found to none
self.browser.del_found();
// Restore tab
let new_tab = match self.browser.tab() {
FileExplorerTab::FindLocal => FileExplorerTab::Local,
FileExplorerTab::FindRemote => FileExplorerTab::Remote,
_ => FileExplorerTab::Local,
};
// Give focus to new tab
match new_tab {
FileExplorerTab::Local => assert!(self.app.active(&Id::ExplorerLocal).is_ok()),
FileExplorerTab::Remote => {
assert!(self.app.active(&Id::ExplorerRemote).is_ok())
}
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => {
assert!(self.app.active(&Id::ExplorerFind).is_ok())
}
}
self.browser.change_tab(new_tab);
}
pub(super) fn update_find_list(&mut self) {
let files: Vec<Vec<TextSpan>> = self
.found()
.unwrap()
.iter_files()
.map(|x| vec![TextSpan::from(self.found().unwrap().fmt_file(x))])
.collect();
assert!(self
.app
.attr(
&Id::ExplorerFind,
Attribute::Content,
AttrValue::Table(files)
)
.is_ok());
}
pub(super) fn update_browser_file_list(&mut self) {
match self.browser.tab() {
FileExplorerTab::Local | FileExplorerTab::FindLocal => self.update_local_filelist(),
FileExplorerTab::Remote | FileExplorerTab::FindRemote => self.update_remote_filelist(),
}
}
pub(super) fn update_browser_file_list_swapped(&mut self) {
match self.browser.tab() {
FileExplorerTab::Local | FileExplorerTab::FindLocal => self.update_remote_filelist(),
FileExplorerTab::Remote | FileExplorerTab::FindRemote => self.update_local_filelist(),
}
}
}

View File

@@ -26,19 +26,20 @@
* SOFTWARE.
*/
// This module is split into files, cause it's just too big
pub(self) mod actions;
pub(self) mod lib;
pub(self) mod misc;
pub(self) mod session;
pub(self) mod update;
pub(self) mod view;
mod actions;
mod components;
mod lib;
mod misc;
mod session;
mod update;
mod view;
// locals
use super::{Activity, Context, ExitReason};
use crate::config::themes::Theme;
use crate::filetransfer::{FileTransfer, FileTransferProtocol};
use crate::filetransfer::{FtpFileTransfer, S3FileTransfer, ScpFileTransfer, SftpFileTransfer};
use crate::fs::explorer::FileExplorer;
use crate::fs::explorer::{FileExplorer, FileSorting};
use crate::fs::FsEntry;
use crate::host::Localhost;
use crate::system::config_client::ConfigClient;
@@ -49,10 +50,10 @@ pub(self) use session::TransferPayload;
// Includes
use chrono::{DateTime, Local};
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use std::collections::VecDeque;
use std::time::Duration;
use tempfile::TempDir;
use tuirealm::View;
use tuirealm::{Application, EventListenerCfg, NoUserEvent};
// -- Storage keys
@@ -61,34 +62,115 @@ const STORAGE_PENDING_TRANSFER: &str = "FILETRANSFER_PENDING_TRANSFER";
// -- components
const COMPONENT_EXPLORER_LOCAL: &str = "EXPLORER_LOCAL";
const COMPONENT_EXPLORER_REMOTE: &str = "EXPLORER_REMOTE";
const COMPONENT_EXPLORER_FIND: &str = "EXPLORER_FIND";
const COMPONENT_LOG_BOX: &str = "LOG_BOX";
const COMPONENT_PROGRESS_BAR_FULL: &str = "PROGRESS_BAR_FULL";
const COMPONENT_PROGRESS_BAR_PARTIAL: &str = "PROGRESS_BAR_PARTIAL";
const COMPONENT_TEXT_ERROR: &str = "TEXT_ERROR";
const COMPONENT_TEXT_FATAL: &str = "TEXT_FATAL";
const COMPONENT_TEXT_HELP: &str = "TEXT_HELP";
const COMPONENT_TEXT_WAIT: &str = "TEXT_WAIT";
const COMPONENT_INPUT_COPY: &str = "INPUT_COPY";
const COMPONENT_INPUT_EXEC: &str = "INPUT_EXEC";
const COMPONENT_INPUT_FIND: &str = "INPUT_FIND";
const COMPONENT_INPUT_GOTO: &str = "INPUT_GOTO";
const COMPONENT_INPUT_MKDIR: &str = "INPUT_MKDIR";
const COMPONENT_INPUT_NEWFILE: &str = "INPUT_NEWFILE";
const COMPONENT_INPUT_OPEN_WITH: &str = "INPUT_OPEN_WITH";
const COMPONENT_INPUT_RENAME: &str = "INPUT_RENAME";
const COMPONENT_INPUT_SAVEAS: &str = "INPUT_SAVEAS";
const COMPONENT_RADIO_DELETE: &str = "RADIO_DELETE";
const COMPONENT_RADIO_REPLACE: &str = "RADIO_REPLACE"; // NOTE: used for file transfers, to choose whether to replace files
const COMPONENT_RADIO_DISCONNECT: &str = "RADIO_DISCONNECT";
const COMPONENT_RADIO_QUIT: &str = "RADIO_QUIT";
const COMPONENT_RADIO_SORTING: &str = "RADIO_SORTING";
const COMPONENT_SPAN_STATUS_BAR_LOCAL: &str = "STATUS_BAR_LOCAL";
const COMPONENT_SPAN_STATUS_BAR_REMOTE: &str = "STATUS_BAR_REMOTE";
const COMPONENT_LIST_FILEINFO: &str = "LIST_FILEINFO";
const COMPONENT_LIST_REPLACING_FILES: &str = "LIST_REPLACING_FILES"; // NOTE: used for file transfers, to list files which are going to be replaced
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
enum Id {
CopyPopup,
DeletePopup,
DisconnectPopup,
ErrorPopup,
ExecPopup,
ExplorerFind,
ExplorerLocal,
ExplorerRemote,
FatalPopup,
FileInfoPopup,
FindPopup,
GlobalListener,
GotoPopup,
KeybindingsPopup,
Log,
MkdirPopup,
NewfilePopup,
OpenWithPopup,
ProgressBarFull,
ProgressBarPartial,
QuitPopup,
RenamePopup,
ReplacePopup,
ReplacingFilesListPopup,
SaveAsPopup,
SortingPopup,
StatusBarLocal,
StatusBarRemote,
WaitPopup,
}
#[derive(Debug, PartialEq)]
enum Msg {
Transfer(TransferMsg),
Ui(UiMsg),
None,
}
#[derive(Debug, PartialEq)]
enum TransferMsg {
AbortTransfer,
CopyFileTo(String),
DeleteFile,
EnterDirectory,
ExecuteCmd(String),
GoTo(String),
GoToParentDirectory,
GoToPreviousDirectory,
Mkdir(String),
NewFile(String),
OpenFile,
OpenFileWith(String),
OpenTextFile,
ReloadDir,
RenameFile(String),
SaveFileAs(String),
SearchFile(String),
TransferFile,
TransferPendingFile,
}
#[derive(Debug, PartialEq)]
enum UiMsg {
ChangeFileSorting(FileSorting),
ChangeTransferWindow,
CloseCopyPopup,
CloseDeletePopup,
CloseDisconnectPopup,
CloseErrorPopup,
CloseExecPopup,
CloseFatalPopup,
CloseFileInfoPopup,
CloseFileSortingPopup,
CloseFindExplorer,
CloseFindPopup,
CloseGotoPopup,
CloseKeybindingsPopup,
CloseMkdirPopup,
CloseNewFilePopup,
CloseOpenWithPopup,
CloseQuitPopup,
CloseReplacePopups,
CloseRenamePopup,
CloseSaveAsPopup,
Disconnect,
ExplorerTabbed,
LogTabbed,
Quit,
ReplacePopupTabbed,
ShowCopyPopup,
ShowDeletePopup,
ShowDisconnectPopup,
ShowExecPopup,
ShowFileInfoPopup,
ShowFileSortingPopup,
ShowFindPopup,
ShowGotoPopup,
ShowKeybindingsPopup,
ShowMkdirPopup,
ShowNewFilePopup,
ShowOpenWithPopup,
ShowQuitPopup,
ShowRenamePopup,
ShowSaveAsPopup,
ToggleHiddenFiles,
ToggleSyncBrowsing,
}
/// ## LogLevel
///
@@ -125,28 +207,43 @@ impl LogRecord {
///
/// FileTransferActivity is the data holder for the file transfer activity
pub struct FileTransferActivity {
exit_reason: Option<ExitReason>, // Exit reason
context: Option<Context>, // Context holder
view: View, // View
host: Localhost, // Localhost
client: Box<dyn FileTransfer>, // File transfer client
browser: Browser, // Browser
log_records: VecDeque<LogRecord>, // Log records
transfer: TransferStates, // Transfer states
cache: Option<TempDir>, // Temporary directory where to store stuff
/// Exit reason
exit_reason: Option<ExitReason>,
/// Context holder
context: Option<Context>,
/// Tui-realm application
app: Application<Id, Msg, NoUserEvent>,
/// Whether should redraw UI
redraw: bool,
/// Localhost bridge
host: Localhost,
/// Remote host
client: Box<dyn FileTransfer>,
/// Browser
browser: Browser,
/// Current log lines
log_records: VecDeque<LogRecord>,
transfer: TransferStates,
/// Temporary directory where to store temporary stuff
cache: Option<TempDir>,
}
impl FileTransferActivity {
/// ### new
///
/// Instantiates a new FileTransferActivity
pub fn new(host: Localhost, protocol: FileTransferProtocol) -> FileTransferActivity {
pub fn new(host: Localhost, protocol: FileTransferProtocol, ticks: Duration) -> Self {
// Get config client
let config_client: ConfigClient = Self::init_config_client();
FileTransferActivity {
Self {
exit_reason: None,
context: None,
view: View::init(),
app: Application::init(
EventListenerCfg::default()
.poll_timeout(ticks)
.default_input_listener(ticks),
),
redraw: true,
host,
client: match protocol {
FileTransferProtocol::Sftp => Box::new(SftpFileTransfer::new(
@@ -257,9 +354,11 @@ impl Activity for FileTransferActivity {
// Set context
self.context = Some(context);
// Clear terminal
self.context_mut().clear_screen();
if let Err(err) = self.context.as_mut().unwrap().terminal().clear_screen() {
error!("Failed to clear screen: {}", err);
}
// Put raw mode on enabled
if let Err(err) = enable_raw_mode() {
if let Err(err) = self.context_mut().terminal().enable_raw_mode() {
error!("Failed to enter raw mode: {}", err);
}
// Get files at current pwd
@@ -284,14 +383,12 @@ impl Activity for FileTransferActivity {
/// `on_draw` is the function which draws the graphical interface.
/// This function must be called at each tick to refresh the interface
fn on_draw(&mut self) {
// Should ui actually be redrawned?
let mut redraw: bool = false;
// Context must be something
if self.context.is_none() {
return;
}
// Check if connected (popup must be None, otherwise would try reconnecting in loop in case of error)
if !self.client.is_connected() && self.view.get_props(COMPONENT_TEXT_FATAL).is_none() {
if !self.client.is_connected() && !self.app.mounted(&Id::FatalPopup) {
let ftparams = self.context().ft_params().unwrap();
// print params
let msg: String = Self::get_connection_msg(&ftparams.params);
@@ -302,12 +399,11 @@ impl Activity for FileTransferActivity {
// Connect to remote
self.connect();
// Redraw
redraw = true;
self.redraw = true;
}
// Handle input events (if false, becomes true; otherwise remains true)
redraw |= self.read_input_event();
// @! draw interface
if redraw {
self.tick();
// View
if self.redraw {
self.view();
}
}
@@ -333,20 +429,16 @@ impl Activity for FileTransferActivity {
}
}
// Disable raw mode
if let Err(err) = disable_raw_mode() {
if let Err(err) = self.context_mut().terminal().disable_raw_mode() {
error!("Failed to disable raw mode: {}", err);
}
if let Err(err) = self.context_mut().terminal().clear_screen() {
error!("Failed to clear screen: {}", err);
}
// Disconnect client
if self.client.is_connected() {
let _ = self.client.disconnect();
}
// Clear terminal and return
match self.context.take() {
Some(mut ctx) => {
ctx.clear_screen();
Some(ctx)
}
None => None,
}
self.context.take()
}
}

View File

@@ -505,7 +505,7 @@ impl FileTransferActivity {
>= 500
{
// Read events
self.read_input_event();
self.tick();
// Reset instant
last_input_event_fetch = Some(Instant::now());
}
@@ -937,7 +937,7 @@ impl FileTransferActivity {
>= 500
{
// Read events
self.read_input_event();
self.tick();
// Reset instant
last_input_event_fetch = Some(Instant::now());
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -27,13 +27,12 @@
* SOFTWARE.
*/
// Locals
use super::{SetupActivity, ViewLayout};
use super::{Id, IdSsh, IdTheme, SetupActivity, ViewLayout};
// Ext
use crate::config::themes::Theme;
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use std::env;
use tuirealm::tui::style::Color;
use tuirealm::{Payload, Value};
use tuirealm::{State, StateValue};
impl SetupActivity {
/// ### action_on_esc
@@ -78,7 +77,7 @@ impl SetupActivity {
// Collect input values if in theme form
if self.layout == ViewLayout::Theme {
self.collect_styles()
.map_err(|e| format!("'{}' has an invalid color", e))?;
.map_err(|e| format!("'{:?}' has an invalid color", e))?;
}
// save theme
self.save_theme()
@@ -93,7 +92,7 @@ impl SetupActivity {
ViewLayout::SetupForm => self.collect_input_values(),
ViewLayout::Theme => self
.collect_styles()
.map_err(|e| format!("'{}' has an invalid color", e))?,
.map_err(|e| format!("'{:?}' has an invalid color", e))?,
_ => {}
}
// Update view
@@ -133,8 +132,8 @@ impl SetupActivity {
pub(super) fn action_delete_ssh_key(&mut self) {
// Get key
// get index
let idx: Option<usize> = match self.view.get_state(super::COMPONENT_LIST_SSH_KEYS) {
Some(Payload::One(Value::Usize(idx))) => Some(idx),
let idx: Option<usize> = match self.app.state(&Id::Ssh(IdSsh::SshKeys)) {
Ok(State::One(StateValue::Usize(idx))) => Some(idx),
_ => None,
};
if let Some(idx) = idx {
@@ -166,29 +165,27 @@ impl SetupActivity {
/// Create a new ssh key
pub(super) fn action_new_ssh_key(&mut self) {
// get parameters
let host: String = match self.view.get_state(super::COMPONENT_INPUT_SSH_HOST) {
Some(Payload::One(Value::Str(host))) => host,
let host: String = match self.app.state(&Id::Ssh(IdSsh::SshHost)) {
Ok(State::One(StateValue::String(host))) => host,
_ => String::new(),
};
let username: String = match self.view.get_state(super::COMPONENT_INPUT_SSH_USERNAME) {
Some(Payload::One(Value::Str(user))) => user,
let username: String = match self.app.state(&Id::Ssh(IdSsh::SshUsername)) {
Ok(State::One(StateValue::String(user))) => user,
_ => String::new(),
};
// Prepare text editor
env::set_var("EDITOR", self.config().get_text_editor());
let placeholder: String = format!("# Type private SSH key for {}@{}\n", username, host);
// Put input mode back to normal
if let Err(err) = disable_raw_mode() {
error!("Failed to disable raw mode: {}", err);
if let Err(err) = self.context_mut().terminal().disable_raw_mode() {
error!("Could not disable raw mode: {}", err);
}
// Leave alternate mode
if let Some(ctx) = self.context.as_mut() {
ctx.leave_alternate_screen();
}
// Re-enable raw mode
if let Err(err) = enable_raw_mode() {
error!("Failed to enter raw mode: {}", err);
if let Err(err) = self.context_mut().terminal().leave_alternate_screen() {
error!("Could not leave alternate screen: {}", err);
}
// Lock ports
assert!(self.app.lock_ports().is_ok());
// Write key to file
match edit::edit(placeholder.as_bytes()) {
Ok(rsa_key) => {
@@ -215,101 +212,246 @@ impl SetupActivity {
}
// Restore terminal
if let Some(ctx) = self.context.as_mut() {
// Clear screen
ctx.clear_screen();
if let Err(err) = ctx.terminal().clear_screen() {
error!("Could not clear screen screen: {}", err);
}
// Enter alternate mode
ctx.enter_alternate_screen();
if let Err(err) = ctx.terminal().enter_alternate_screen() {
error!("Could not enter alternate screen: {}", err);
}
// Re-enable raw mode
if let Err(err) = ctx.terminal().enable_raw_mode() {
error!("Failed to enter raw mode: {}", err);
}
// Unlock ports
assert!(self.app.unlock_ports().is_ok());
}
}
/// ### set_color
///
/// Given a component and a color, save the color into the theme
pub(super) fn action_save_color(&mut self, component: &str, color: Color) {
pub(super) fn action_save_color(&mut self, component: IdTheme, color: Color) {
let theme: &mut Theme = self.theme_mut();
match component {
super::COMPONENT_COLOR_AUTH_ADDR => {
IdTheme::AuthAddress => {
theme.auth_address = color;
}
super::COMPONENT_COLOR_AUTH_BOOKMARKS => {
IdTheme::AuthBookmarks => {
theme.auth_bookmarks = color;
}
super::COMPONENT_COLOR_AUTH_PASSWORD => {
IdTheme::AuthPassword => {
theme.auth_password = color;
}
super::COMPONENT_COLOR_AUTH_PORT => {
IdTheme::AuthPort => {
theme.auth_port = color;
}
super::COMPONENT_COLOR_AUTH_PROTOCOL => {
IdTheme::AuthProtocol => {
theme.auth_protocol = color;
}
super::COMPONENT_COLOR_AUTH_RECENTS => {
IdTheme::AuthRecentHosts => {
theme.auth_recents = color;
}
super::COMPONENT_COLOR_AUTH_USERNAME => {
IdTheme::AuthUsername => {
theme.auth_username = color;
}
super::COMPONENT_COLOR_MISC_ERROR => {
IdTheme::MiscError => {
theme.misc_error_dialog = color;
}
super::COMPONENT_COLOR_MISC_INFO => {
IdTheme::MiscInfo => {
theme.misc_info_dialog = color;
}
super::COMPONENT_COLOR_MISC_INPUT => {
IdTheme::MiscInput => {
theme.misc_input_dialog = color;
}
super::COMPONENT_COLOR_MISC_KEYS => {
IdTheme::MiscKeys => {
theme.misc_keys = color;
}
super::COMPONENT_COLOR_MISC_QUIT => {
IdTheme::MiscQuit => {
theme.misc_quit_dialog = color;
}
super::COMPONENT_COLOR_MISC_SAVE => {
IdTheme::MiscSave => {
theme.misc_save_dialog = color;
}
super::COMPONENT_COLOR_MISC_WARN => {
IdTheme::MiscWarn => {
theme.misc_warn_dialog = color;
}
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG => {
IdTheme::ExplorerLocalBg => {
theme.transfer_local_explorer_background = color;
}
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG => {
IdTheme::ExplorerLocalFg => {
theme.transfer_local_explorer_foreground = color;
}
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG => {
IdTheme::ExplorerLocalHg => {
theme.transfer_local_explorer_highlighted = color;
}
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG => {
IdTheme::ExplorerRemoteBg => {
theme.transfer_remote_explorer_background = color;
}
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG => {
IdTheme::ExplorerRemoteFg => {
theme.transfer_remote_explorer_foreground = color;
}
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG => {
IdTheme::ExplorerRemoteHg => {
theme.transfer_remote_explorer_highlighted = color;
}
super::COMPONENT_COLOR_TRANSFER_LOG_BG => {
IdTheme::LogBg => {
theme.transfer_log_background = color;
}
super::COMPONENT_COLOR_TRANSFER_LOG_WIN => {
IdTheme::LogWindow => {
theme.transfer_log_window = color;
}
super::COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL => {
IdTheme::ProgBarFull => {
theme.transfer_progress_bar_full = color;
}
super::COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL => {
IdTheme::ProgBarPartial => {
theme.transfer_progress_bar_partial = color;
}
super::COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN => {
IdTheme::StatusHidden => {
theme.transfer_status_hidden = color;
}
super::COMPONENT_COLOR_TRANSFER_STATUS_SORTING => {
IdTheme::StatusSorting => {
theme.transfer_status_sorting = color;
}
super::COMPONENT_COLOR_TRANSFER_STATUS_SYNC => {
IdTheme::StatusSync => {
theme.transfer_status_sync_browsing = color;
}
_ => {}
}
}
/// ### collect_styles
///
/// Collect values from input and put them into the theme.
/// If a component has an invalid color, returns Err(component_id)
fn collect_styles(&mut self) -> Result<(), Id> {
// auth
let auth_address = self
.get_color(&Id::Theme(IdTheme::AuthAddress))
.map_err(|_| Id::Theme(IdTheme::AuthAddress))?;
let auth_bookmarks = self
.get_color(&Id::Theme(IdTheme::AuthBookmarks))
.map_err(|_| Id::Theme(IdTheme::AuthBookmarks))?;
let auth_password = self
.get_color(&Id::Theme(IdTheme::AuthPassword))
.map_err(|_| Id::Theme(IdTheme::AuthPassword))?;
let auth_port = self
.get_color(&Id::Theme(IdTheme::AuthPort))
.map_err(|_| Id::Theme(IdTheme::AuthPort))?;
let auth_protocol = self
.get_color(&Id::Theme(IdTheme::AuthProtocol))
.map_err(|_| Id::Theme(IdTheme::AuthProtocol))?;
let auth_recents = self
.get_color(&Id::Theme(IdTheme::AuthRecentHosts))
.map_err(|_| Id::Theme(IdTheme::AuthRecentHosts))?;
let auth_username = self
.get_color(&Id::Theme(IdTheme::AuthUsername))
.map_err(|_| Id::Theme(IdTheme::AuthUsername))?;
// misc
let misc_error_dialog = self
.get_color(&Id::Theme(IdTheme::MiscError))
.map_err(|_| Id::Theme(IdTheme::MiscError))?;
let misc_info_dialog = self
.get_color(&Id::Theme(IdTheme::MiscInfo))
.map_err(|_| Id::Theme(IdTheme::MiscInfo))?;
let misc_input_dialog = self
.get_color(&Id::Theme(IdTheme::MiscInput))
.map_err(|_| Id::Theme(IdTheme::MiscInput))?;
let misc_keys = self
.get_color(&Id::Theme(IdTheme::MiscKeys))
.map_err(|_| Id::Theme(IdTheme::MiscKeys))?;
let misc_quit_dialog = self
.get_color(&Id::Theme(IdTheme::MiscQuit))
.map_err(|_| Id::Theme(IdTheme::MiscQuit))?;
let misc_save_dialog = self
.get_color(&Id::Theme(IdTheme::MiscSave))
.map_err(|_| Id::Theme(IdTheme::MiscSave))?;
let misc_warn_dialog = self
.get_color(&Id::Theme(IdTheme::MiscWarn))
.map_err(|_| Id::Theme(IdTheme::MiscWarn))?;
// transfer
let transfer_local_explorer_background = self
.get_color(&Id::Theme(IdTheme::ExplorerLocalBg))
.map_err(|_| Id::Theme(IdTheme::ExplorerLocalBg))?;
let transfer_local_explorer_foreground = self
.get_color(&Id::Theme(IdTheme::ExplorerLocalFg))
.map_err(|_| Id::Theme(IdTheme::ExplorerLocalFg))?;
let transfer_local_explorer_highlighted = self
.get_color(&Id::Theme(IdTheme::ExplorerLocalHg))
.map_err(|_| Id::Theme(IdTheme::ExplorerLocalHg))?;
let transfer_remote_explorer_background = self
.get_color(&Id::Theme(IdTheme::ExplorerRemoteBg))
.map_err(|_| Id::Theme(IdTheme::ExplorerRemoteBg))?;
let transfer_remote_explorer_foreground = self
.get_color(&Id::Theme(IdTheme::ExplorerRemoteFg))
.map_err(|_| Id::Theme(IdTheme::ExplorerRemoteFg))?;
let transfer_remote_explorer_highlighted = self
.get_color(&Id::Theme(IdTheme::ExplorerRemoteHg))
.map_err(|_| Id::Theme(IdTheme::ExplorerRemoteHg))?;
let transfer_log_background = self
.get_color(&Id::Theme(IdTheme::LogBg))
.map_err(|_| Id::Theme(IdTheme::LogBg))?;
let transfer_log_window = self
.get_color(&Id::Theme(IdTheme::LogWindow))
.map_err(|_| Id::Theme(IdTheme::LogWindow))?;
let transfer_progress_bar_full = self
.get_color(&Id::Theme(IdTheme::ProgBarFull))
.map_err(|_| Id::Theme(IdTheme::ProgBarFull))?;
let transfer_progress_bar_partial = self
.get_color(&Id::Theme(IdTheme::ProgBarPartial))
.map_err(|_| Id::Theme(IdTheme::ProgBarPartial))?;
let transfer_status_hidden = self
.get_color(&Id::Theme(IdTheme::StatusHidden))
.map_err(|_| Id::Theme(IdTheme::StatusHidden))?;
let transfer_status_sorting = self
.get_color(&Id::Theme(IdTheme::StatusSorting))
.map_err(|_| Id::Theme(IdTheme::StatusSorting))?;
let transfer_status_sync_browsing = self
.get_color(&Id::Theme(IdTheme::StatusSync))
.map_err(|_| Id::Theme(IdTheme::StatusSync))?;
// Update theme
let mut theme: &mut Theme = self.theme_mut();
theme.auth_address = auth_address;
theme.auth_bookmarks = auth_bookmarks;
theme.auth_password = auth_password;
theme.auth_port = auth_port;
theme.auth_protocol = auth_protocol;
theme.auth_recents = auth_recents;
theme.auth_username = auth_username;
theme.misc_error_dialog = misc_error_dialog;
theme.misc_info_dialog = misc_info_dialog;
theme.misc_input_dialog = misc_input_dialog;
theme.misc_keys = misc_keys;
theme.misc_quit_dialog = misc_quit_dialog;
theme.misc_save_dialog = misc_save_dialog;
theme.misc_warn_dialog = misc_warn_dialog;
theme.transfer_local_explorer_background = transfer_local_explorer_background;
theme.transfer_local_explorer_foreground = transfer_local_explorer_foreground;
theme.transfer_local_explorer_highlighted = transfer_local_explorer_highlighted;
theme.transfer_remote_explorer_background = transfer_remote_explorer_background;
theme.transfer_remote_explorer_foreground = transfer_remote_explorer_foreground;
theme.transfer_remote_explorer_highlighted = transfer_remote_explorer_highlighted;
theme.transfer_log_background = transfer_log_background;
theme.transfer_log_window = transfer_log_window;
theme.transfer_progress_bar_full = transfer_progress_bar_full;
theme.transfer_progress_bar_partial = transfer_progress_bar_partial;
theme.transfer_status_hidden = transfer_status_hidden;
theme.transfer_status_sorting = transfer_status_sorting;
theme.transfer_status_sync_browsing = transfer_status_sync_browsing;
Ok(())
}
/// ### get_color
///
/// Get color from component
fn get_color(&self, component: &Id) -> Result<Color, ()> {
match self.app.state(component) {
Ok(State::One(StateValue::String(color))) => {
match crate::utils::parser::parse_color(color.as_str()) {
Some(c) => Ok(c),
None => Err(()),
}
}
_ => Err(()),
}
}
}

View File

@@ -0,0 +1,334 @@
//! ## Config
//!
//! config tab 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.
*/
use super::{CommonMsg, Msg, ViewLayout};
use tui_realm_stdlib::{List, Paragraph, Radio, Span};
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::event::{Key, KeyEvent};
use tuirealm::props::{Alignment, BorderSides, BorderType, Borders, Color, TableBuilder, TextSpan};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
#[derive(MockComponent)]
pub struct ErrorPopup {
component: Paragraph,
}
impl ErrorPopup {
pub fn new<S: AsRef<str>>(text: S) -> Self {
Self {
component: Paragraph::default()
.alignment(Alignment::Center)
.borders(
Borders::default()
.color(Color::Red)
.modifiers(BorderType::Rounded),
)
.foreground(Color::Red)
.text(&[TextSpan::from(text.as_ref())])
.wrap(true),
}
}
}
impl Component<Msg, NoUserEvent> for ErrorPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Esc | Key::Enter,
..
}) => Some(Msg::Common(CommonMsg::CloseErrorPopup)),
_ => None,
}
}
}
#[derive(MockComponent)]
pub struct Footer {
component: Span,
}
impl Default for Footer {
fn default() -> Self {
Self {
component: Span::default().spans(&[
TextSpan::new("Press ").bold(),
TextSpan::new("<CTRL+H>").bold().fg(Color::Cyan),
TextSpan::new(" to show keybindings; ").bold(),
TextSpan::new("<CTRL+S>").bold().fg(Color::Cyan),
TextSpan::new(" to save parameters; ").bold(),
TextSpan::new("<TAB>").bold().fg(Color::Cyan),
TextSpan::new(" to change panel").bold(),
]),
}
}
}
impl Component<Msg, NoUserEvent> for Footer {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
None
}
}
#[derive(MockComponent)]
pub struct Header {
component: Radio,
}
impl Header {
pub fn new(layout: ViewLayout) -> Self {
Self {
component: Radio::default()
.borders(
Borders::default()
.color(Color::Yellow)
.sides(BorderSides::BOTTOM),
)
.choices(&["User interface", "SSH Keys", "Theme"])
.foreground(Color::Yellow)
.value(match layout {
ViewLayout::SetupForm => 0,
ViewLayout::SshKeys => 1,
ViewLayout::Theme => 2,
}),
}
}
}
impl Component<Msg, NoUserEvent> for Header {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
None
}
}
#[derive(MockComponent)]
pub struct Keybindings {
component: List,
}
impl Default for Keybindings {
fn default() -> Self {
Self {
component: List::default()
.borders(Borders::default().modifiers(BorderType::Rounded))
.title("Keybindings", Alignment::Center)
.scroll(true)
.highlighted_str("? ")
.rows(
TableBuilder::default()
.add_col(TextSpan::new("<ESC>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Exit setup"))
.add_row()
.add_col(TextSpan::new("<TAB>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Change setup page"))
.add_row()
.add_col(TextSpan::new("<RIGHT/LEFT>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Change cursor"))
.add_row()
.add_col(TextSpan::new("<UP/DOWN>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Change input field"))
.add_row()
.add_col(TextSpan::new("<ENTER>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Select / Dismiss popup"))
.add_row()
.add_col(TextSpan::new("<DEL|E>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Delete SSH key"))
.add_row()
.add_col(TextSpan::new("<CTRL+N>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" New SSH key"))
.add_row()
.add_col(TextSpan::new("<CTRL+R>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Revert changes"))
.add_row()
.add_col(TextSpan::new("<CTRL+S>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Save configuration"))
.build(),
),
}
}
}
impl Component<Msg, NoUserEvent> for Keybindings {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Esc | Key::Enter,
..
}) => Some(Msg::Common(CommonMsg::CloseKeybindingsPopup)),
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => {
self.perform(Cmd::Move(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
self.perform(Cmd::Move(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::PageDown,
..
}) => {
self.perform(Cmd::Scroll(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::PageUp, ..
}) => {
self.perform(Cmd::Scroll(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
_ => None,
}
}
}
#[derive(MockComponent)]
pub struct QuitPopup {
component: Radio,
}
impl Default for QuitPopup {
fn default() -> Self {
Self {
component: Radio::default()
.borders(
Borders::default()
.color(Color::Red)
.modifiers(BorderType::Rounded),
)
.foreground(Color::Red)
.title(
"There are unsaved changes! Save changes before leaving?",
Alignment::Center,
)
.rewind(true)
.choices(&["Save", "Don't save", "Cancel"]),
}
}
}
impl Component<Msg, NoUserEvent> for QuitPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Common(CommonMsg::CloseQuitPopup))
}
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => match self.perform(Cmd::Submit) {
CmdResult::Submit(State::One(StateValue::Usize(0))) => {
Some(Msg::Common(CommonMsg::SaveAndQuit))
}
CmdResult::Submit(State::One(StateValue::Usize(1))) => {
Some(Msg::Common(CommonMsg::Quit))
}
_ => Some(Msg::Common(CommonMsg::CloseQuitPopup)),
},
_ => None,
}
}
}
#[derive(MockComponent)]
pub struct SavePopup {
component: Radio,
}
impl Default for SavePopup {
fn default() -> Self {
Self {
component: Radio::default()
.borders(
Borders::default()
.color(Color::Yellow)
.modifiers(BorderType::Rounded),
)
.foreground(Color::Yellow)
.title("Save changes?", Alignment::Center)
.rewind(true)
.choices(&["Yes", "No"]),
}
}
}
impl Component<Msg, NoUserEvent> for SavePopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Common(CommonMsg::CloseSavePopup))
}
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => {
if matches!(
self.perform(Cmd::Submit),
CmdResult::Submit(State::One(StateValue::Usize(0)))
) {
Some(Msg::Common(CommonMsg::SaveConfig))
} else {
Some(Msg::Common(CommonMsg::CloseSavePopup))
}
}
_ => None,
}
}
}

View File

@@ -0,0 +1,489 @@
//! ## Config
//!
//! config tab 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.
*/
use super::{ConfigMsg, Msg};
use crate::filetransfer::FileTransferProtocol;
use crate::fs::explorer::GroupDirs as GroupDirsEnum;
use crate::utils::parser::parse_bytesize;
use tui_realm_stdlib::{Input, Radio};
use tuirealm::command::{Cmd, Direction, Position};
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style};
use tuirealm::{Component, Event, MockComponent, NoUserEvent};
// -- components
#[derive(MockComponent)]
pub struct CheckUpdates {
component: Radio,
}
impl CheckUpdates {
pub fn new(enabled: bool) -> Self {
Self {
component: Radio::default()
.borders(
Borders::default()
.color(Color::LightYellow)
.modifiers(BorderType::Rounded),
)
.choices(&["Yes", "No"])
.foreground(Color::LightYellow)
.rewind(true)
.title("Check for updates?", Alignment::Left)
.value(if enabled { 0 } else { 1 }),
}
}
}
impl Component<Msg, NoUserEvent> for CheckUpdates {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
handle_radio_ev(
self,
ev,
Msg::Config(ConfigMsg::CheckUpdatesBlurDown),
Msg::Config(ConfigMsg::CheckUpdatesBlurUp),
)
}
}
#[derive(MockComponent)]
pub struct DefaultProtocol {
component: Radio,
}
impl DefaultProtocol {
pub fn new(protocol: FileTransferProtocol) -> Self {
Self {
component: Radio::default()
.borders(
Borders::default()
.color(Color::Cyan)
.modifiers(BorderType::Rounded),
)
.choices(&["SFTP", "SCP", "FTP", "FTPS", "AWS S3"])
.foreground(Color::Cyan)
.rewind(true)
.title("Default protocol", Alignment::Left)
.value(match protocol {
FileTransferProtocol::AwsS3 => 4,
FileTransferProtocol::Ftp(true) => 3,
FileTransferProtocol::Ftp(false) => 2,
FileTransferProtocol::Scp => 1,
FileTransferProtocol::Sftp => 0,
}),
}
}
}
impl Component<Msg, NoUserEvent> for DefaultProtocol {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
handle_radio_ev(
self,
ev,
Msg::Config(ConfigMsg::DefaultProtocolBlurDown),
Msg::Config(ConfigMsg::DefaultProtocolBlurUp),
)
}
}
#[derive(MockComponent)]
pub struct GroupDirs {
component: Radio,
}
impl GroupDirs {
pub fn new(opt: Option<GroupDirsEnum>) -> Self {
Self {
component: Radio::default()
.borders(
Borders::default()
.color(Color::LightMagenta)
.modifiers(BorderType::Rounded),
)
.choices(&["Display first", "Display last", "No"])
.foreground(Color::LightMagenta)
.rewind(true)
.title("Group directories", Alignment::Left)
.value(match opt {
Some(GroupDirsEnum::First) => 0,
Some(GroupDirsEnum::Last) => 1,
None => 2,
}),
}
}
}
impl Component<Msg, NoUserEvent> for GroupDirs {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
handle_radio_ev(
self,
ev,
Msg::Config(ConfigMsg::GroupDirsBlurDown),
Msg::Config(ConfigMsg::GroupDirsBlurUp),
)
}
}
#[derive(MockComponent)]
pub struct HiddenFiles {
component: Radio,
}
impl HiddenFiles {
pub fn new(enabled: bool) -> Self {
Self {
component: Radio::default()
.borders(
Borders::default()
.color(Color::LightRed)
.modifiers(BorderType::Rounded),
)
.choices(&["Yes", "No"])
.foreground(Color::LightRed)
.rewind(true)
.title("Show hidden files? (by default)", Alignment::Left)
.value(if enabled { 0 } else { 1 }),
}
}
}
impl Component<Msg, NoUserEvent> for HiddenFiles {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
handle_radio_ev(
self,
ev,
Msg::Config(ConfigMsg::HiddenFilesBlurDown),
Msg::Config(ConfigMsg::HiddenFilesBlurUp),
)
}
}
#[derive(MockComponent)]
pub struct NotificationsEnabled {
component: Radio,
}
impl NotificationsEnabled {
pub fn new(enabled: bool) -> Self {
Self {
component: Radio::default()
.borders(
Borders::default()
.color(Color::LightRed)
.modifiers(BorderType::Rounded),
)
.choices(&["Yes", "No"])
.foreground(Color::LightRed)
.rewind(true)
.title("Enable notifications?", Alignment::Left)
.value(if enabled { 0 } else { 1 }),
}
}
}
impl Component<Msg, NoUserEvent> for NotificationsEnabled {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
handle_radio_ev(
self,
ev,
Msg::Config(ConfigMsg::NotificationsEnabledBlurDown),
Msg::Config(ConfigMsg::NotificationsEnabledBlurUp),
)
}
}
#[derive(MockComponent)]
pub struct PromptOnFileReplace {
component: Radio,
}
impl PromptOnFileReplace {
pub fn new(enabled: bool) -> Self {
Self {
component: Radio::default()
.borders(
Borders::default()
.color(Color::LightBlue)
.modifiers(BorderType::Rounded),
)
.choices(&["Yes", "No"])
.foreground(Color::LightBlue)
.rewind(true)
.title("Prompt when replacing existing files?", Alignment::Left)
.value(if enabled { 0 } else { 1 }),
}
}
}
impl Component<Msg, NoUserEvent> for PromptOnFileReplace {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
handle_radio_ev(
self,
ev,
Msg::Config(ConfigMsg::PromptOnFileReplaceBlurDown),
Msg::Config(ConfigMsg::PromptOnFileReplaceBlurUp),
)
}
}
#[derive(MockComponent)]
pub struct LocalFileFmt {
component: Input,
}
impl LocalFileFmt {
pub fn new(value: &str) -> Self {
Self {
component: Input::default()
.borders(
Borders::default()
.color(Color::LightGreen)
.modifiers(BorderType::Rounded),
)
.foreground(Color::LightGreen)
.input_type(InputType::Text)
.placeholder(
"{NAME:36} {PEX} {SIZE} {MTIME:17:%b %d %Y %H:%M}",
Style::default().fg(Color::Rgb(128, 128, 128)),
)
.title("File formatter syntax (local)", Alignment::Left)
.value(value),
}
}
}
impl Component<Msg, NoUserEvent> for LocalFileFmt {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
handle_input_ev(
self,
ev,
Msg::Config(ConfigMsg::LocalFileFmtBlurDown),
Msg::Config(ConfigMsg::LocalFileFmtBlurUp),
)
}
}
#[derive(MockComponent)]
pub struct NotificationsThreshold {
component: Input,
}
impl NotificationsThreshold {
pub fn new(value: &str) -> Self {
// -- validators
fn validate(bytes: &str) -> bool {
parse_bytesize(bytes).is_some()
}
fn char_valid(_input: &str, incoming: char) -> bool {
incoming.is_digit(10) || ['B', 'K', 'M', 'G', 'T', 'P'].contains(&incoming)
}
Self {
component: Input::default()
.borders(
Borders::default()
.color(Color::LightYellow)
.modifiers(BorderType::Rounded),
)
.foreground(Color::LightYellow)
.invalid_style(Style::default().fg(Color::Red))
.input_type(InputType::Custom(validate, char_valid))
.placeholder("64 MB", Style::default().fg(Color::Rgb(128, 128, 128)))
.title("Notifications: minimum transfer size", Alignment::Left)
.value(value),
}
}
}
impl Component<Msg, NoUserEvent> for NotificationsThreshold {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
handle_input_ev(
self,
ev,
Msg::Config(ConfigMsg::NotificationsThresholdBlurDown),
Msg::Config(ConfigMsg::NotificationsThresholdBlurUp),
)
}
}
#[derive(MockComponent)]
pub struct RemoteFileFmt {
component: Input,
}
impl RemoteFileFmt {
pub fn new(value: &str) -> Self {
Self {
component: Input::default()
.borders(
Borders::default()
.color(Color::Cyan)
.modifiers(BorderType::Rounded),
)
.foreground(Color::Cyan)
.input_type(InputType::Text)
.placeholder(
"{NAME:36} {PEX} {SIZE} {MTIME:17:%b %d %Y %H:%M}",
Style::default().fg(Color::Rgb(128, 128, 128)),
)
.title("File formatter syntax (remote)", Alignment::Left)
.value(value),
}
}
}
impl Component<Msg, NoUserEvent> for RemoteFileFmt {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
handle_input_ev(
self,
ev,
Msg::Config(ConfigMsg::RemoteFileFmtBlurDown),
Msg::Config(ConfigMsg::RemoteFileFmtBlurUp),
)
}
}
#[derive(MockComponent)]
pub struct TextEditor {
component: Input,
}
impl TextEditor {
pub fn new(value: &str) -> Self {
Self {
component: Input::default()
.borders(
Borders::default()
.color(Color::LightGreen)
.modifiers(BorderType::Rounded),
)
.foreground(Color::LightGreen)
.input_type(InputType::Text)
.placeholder("vim", Style::default().fg(Color::Rgb(128, 128, 128)))
.title("Text editor", Alignment::Left)
.value(value),
}
}
}
impl Component<Msg, NoUserEvent> for TextEditor {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
handle_input_ev(
self,
ev,
Msg::Config(ConfigMsg::TextEditorBlurDown),
Msg::Config(ConfigMsg::TextEditorBlurUp),
)
}
}
// -- event handler
fn handle_input_ev(
component: &mut dyn Component<Msg, NoUserEvent>,
ev: Event<NoUserEvent>,
on_key_down: Msg,
on_key_up: Msg,
) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
component.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
component.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
component.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
component.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Delete, ..
}) => {
component.perform(Cmd::Cancel);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Backspace,
..
}) => {
component.perform(Cmd::Delete);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Char(ch),
modifiers: KeyModifiers::NONE,
}) => {
component.perform(Cmd::Type(ch));
Some(Msg::Config(ConfigMsg::ConfigChanged))
}
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => Some(on_key_down),
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(on_key_up),
_ => None,
}
}
fn handle_radio_ev(
component: &mut dyn Component<Msg, NoUserEvent>,
ev: Event<NoUserEvent>,
on_key_down: Msg,
on_key_up: Msg,
) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
component.perform(Cmd::Move(Direction::Left));
Some(Msg::Config(ConfigMsg::ConfigChanged))
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
component.perform(Cmd::Move(Direction::Right));
Some(Msg::Config(ConfigMsg::ConfigChanged))
}
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => Some(on_key_down),
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(on_key_up),
_ => None,
}
}

View File

@@ -0,0 +1,86 @@
//! ## Components
//!
//! setup activity 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.
*/
use super::{CommonMsg, ConfigMsg, Msg, SshMsg, ThemeMsg, ViewLayout};
mod commons;
mod config;
mod ssh;
mod theme;
pub(super) use commons::{ErrorPopup, Footer, Header, Keybindings, QuitPopup, SavePopup};
pub(super) use config::{
CheckUpdates, DefaultProtocol, GroupDirs, HiddenFiles, LocalFileFmt, NotificationsEnabled,
NotificationsThreshold, PromptOnFileReplace, RemoteFileFmt, TextEditor,
};
pub(super) use ssh::{DelSshKeyPopup, SshHost, SshKeys, SshUsername};
pub(super) use theme::*;
use tui_realm_stdlib::Phantom;
use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent};
use tuirealm::{Component, MockComponent};
// -- global listener
#[derive(MockComponent)]
pub struct GlobalListener {
component: Phantom,
}
impl Default for GlobalListener {
fn default() -> Self {
Self {
component: Phantom::default(),
}
}
}
impl Component<Msg, NoUserEvent> for GlobalListener {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Common(CommonMsg::ShowQuitPopup))
}
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
Some(Msg::Common(CommonMsg::ChangeLayout))
}
Event::Keyboard(KeyEvent {
code: Key::Char('h'),
modifiers: KeyModifiers::CONTROL,
}) => Some(Msg::Common(CommonMsg::ShowKeybindings)),
Event::Keyboard(KeyEvent {
code: Key::Char('r'),
modifiers: KeyModifiers::CONTROL,
}) => Some(Msg::Common(CommonMsg::RevertChanges)),
Event::Keyboard(KeyEvent {
code: Key::Char('s'),
modifiers: KeyModifiers::CONTROL,
}) => Some(Msg::Common(CommonMsg::ShowSavePopup)),
_ => None,
}
}
}

View File

@@ -0,0 +1,339 @@
//! ## Ssh
//!
//! ssh 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.
*/
use super::{Msg, SshMsg};
use tui_realm_stdlib::{Input, List, Radio};
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
use tuirealm::props::{
Alignment, BorderSides, BorderType, Borders, Color, InputType, Style, TextSpan,
};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
/* DelSshKeyPopup,
SshHost,
SshKeys,
SshUsername, */
#[derive(MockComponent)]
pub struct DelSshKeyPopup {
component: Radio,
}
impl Default for DelSshKeyPopup {
fn default() -> Self {
Self {
component: Radio::default()
.borders(
Borders::default()
.color(Color::Red)
.modifiers(BorderType::Rounded),
)
.choices(&["Yes", "No"])
.foreground(Color::Red)
.rewind(true)
.title("Delete key?", Alignment::Center)
.value(1),
}
}
}
impl Component<Msg, NoUserEvent> for DelSshKeyPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Ssh(SshMsg::CloseDelSshKeyPopup))
}
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => {
if matches!(
self.perform(Cmd::Submit),
CmdResult::Submit(State::One(StateValue::Usize(0)))
) {
Some(Msg::Ssh(SshMsg::DeleteSshKey))
} else {
Some(Msg::Ssh(SshMsg::CloseDelSshKeyPopup))
}
}
_ => None,
}
}
}
#[derive(MockComponent)]
pub struct SshKeys {
component: List,
}
impl SshKeys {
pub fn new(keys: &[String]) -> Self {
Self {
component: List::default()
.borders(
Borders::default()
.color(Color::LightGreen)
.modifiers(BorderType::Rounded),
)
.foreground(Color::LightGreen)
.highlighted_color(Color::LightGreen)
.rewind(true)
.rows(keys.iter().map(|x| vec![TextSpan::from(x)]).collect())
.step(4)
.scroll(true)
.title("SSH Keys", Alignment::Left),
}
}
}
impl Component<Msg, NoUserEvent> for SshKeys {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => {
self.perform(Cmd::Move(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
self.perform(Cmd::Move(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::PageDown,
..
}) => {
self.perform(Cmd::Scroll(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::PageUp, ..
}) => {
self.perform(Cmd::Scroll(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => match self.state() {
State::One(StateValue::Usize(choice)) => Some(Msg::Ssh(SshMsg::EditSshKey(choice))),
_ => Some(Msg::None),
},
Event::Keyboard(KeyEvent {
code: Key::Delete, ..
}) => Some(Msg::Ssh(SshMsg::ShowDelSshKeyPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('n'),
modifiers: KeyModifiers::CONTROL,
}) => Some(Msg::Ssh(SshMsg::ShowNewSshKeyPopup)),
_ => None,
}
}
}
#[derive(MockComponent)]
pub struct SshHost {
component: Input,
}
impl Default for SshHost {
fn default() -> Self {
Self {
component: Input::default()
.borders(
Borders::default()
.sides(BorderSides::TOP | BorderSides::RIGHT | BorderSides::LEFT),
)
.input_type(InputType::Text)
.placeholder(
"192.168.1.2",
Style::default().fg(Color::Rgb(128, 128, 128)),
)
.title("Hostname or address", Alignment::Center),
}
}
}
impl Component<Msg, NoUserEvent> for SshHost {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Delete, ..
}) => {
self.perform(Cmd::Cancel);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Backspace,
..
}) => {
self.perform(Cmd::Delete);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Char(ch),
modifiers: KeyModifiers::NONE,
}) => {
self.perform(Cmd::Type(ch));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => Some(Msg::Ssh(SshMsg::SaveSshKey)),
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => Some(Msg::Ssh(SshMsg::SshHostBlur)),
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Ssh(SshMsg::CloseNewSshKeyPopup))
}
_ => None,
}
}
}
#[derive(MockComponent)]
pub struct SshUsername {
component: Input,
}
impl Default for SshUsername {
fn default() -> Self {
Self {
component: Input::default()
.borders(
Borders::default()
.sides(BorderSides::BOTTOM | BorderSides::RIGHT | BorderSides::LEFT),
)
.input_type(InputType::Text)
.placeholder("root", Style::default().fg(Color::Rgb(128, 128, 128)))
.title("Username", Alignment::Center),
}
}
}
impl Component<Msg, NoUserEvent> for SshUsername {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Delete, ..
}) => {
self.perform(Cmd::Cancel);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Backspace,
..
}) => {
self.perform(Cmd::Delete);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Char(ch),
modifiers: KeyModifiers::NONE,
}) => {
self.perform(Cmd::Type(ch));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => Some(Msg::Ssh(SshMsg::SaveSshKey)),
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
Some(Msg::Ssh(SshMsg::SshUsernameBlur))
}
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Ssh(SshMsg::CloseNewSshKeyPopup))
}
_ => None,
}
}
}

View File

@@ -0,0 +1,910 @@
//! ## Theme
//!
//! theme tab 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.
*/
use super::{Msg, ThemeMsg};
use crate::ui::activities::setup::IdTheme;
use tui_realm_stdlib::{Input, Label};
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style, TextModifiers};
use tuirealm::{
AttrValue, Attribute, Component, Event, MockComponent, NoUserEvent, State, StateValue,
};
// -- components
#[derive(MockComponent)]
pub struct AuthTitle {
component: Label,
}
impl Default for AuthTitle {
fn default() -> Self {
Self {
component: Label::default()
.modifiers(TextModifiers::BOLD)
.text("Authentication styles"),
}
}
}
impl Component<Msg, NoUserEvent> for AuthTitle {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
None
}
}
#[derive(MockComponent)]
pub struct MiscTitle {
component: Label,
}
impl Default for MiscTitle {
fn default() -> Self {
Self {
component: Label::default()
.modifiers(TextModifiers::BOLD)
.text("Misc styles"),
}
}
}
impl Component<Msg, NoUserEvent> for MiscTitle {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
None
}
}
#[derive(MockComponent)]
pub struct TransferTitle {
component: Label,
}
impl Default for TransferTitle {
fn default() -> Self {
Self {
component: Label::default()
.modifiers(TextModifiers::BOLD)
.text("Transfer styles"),
}
}
}
impl Component<Msg, NoUserEvent> for TransferTitle {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
None
}
}
#[derive(MockComponent)]
pub struct TransferTitle2 {
component: Label,
}
impl Default for TransferTitle2 {
fn default() -> Self {
Self {
component: Label::default()
.modifiers(TextModifiers::BOLD)
.text("Transfer styles (2)"),
}
}
}
impl Component<Msg, NoUserEvent> for TransferTitle2 {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
None
}
}
#[derive(MockComponent)]
pub struct AuthAddress {
component: InputColor,
}
impl AuthAddress {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Ip Address",
IdTheme::AuthAddress,
value,
Msg::Theme(ThemeMsg::AuthAddressBlurDown),
Msg::Theme(ThemeMsg::AuthAddressBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for AuthAddress {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct AuthBookmarks {
component: InputColor,
}
impl AuthBookmarks {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Bookmarks",
IdTheme::AuthBookmarks,
value,
Msg::Theme(ThemeMsg::AuthBookmarksBlurDown),
Msg::Theme(ThemeMsg::AuthBookmarksBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for AuthBookmarks {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct AuthPassword {
component: InputColor,
}
impl AuthPassword {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Password",
IdTheme::AuthPassword,
value,
Msg::Theme(ThemeMsg::AuthPasswordBlurDown),
Msg::Theme(ThemeMsg::AuthPasswordBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for AuthPassword {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct AuthPort {
component: InputColor,
}
impl AuthPort {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Port",
IdTheme::AuthPort,
value,
Msg::Theme(ThemeMsg::AuthPortBlurDown),
Msg::Theme(ThemeMsg::AuthPortBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for AuthPort {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct AuthProtocol {
component: InputColor,
}
impl AuthProtocol {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Protocol",
IdTheme::AuthProtocol,
value,
Msg::Theme(ThemeMsg::AuthProtocolBlurDown),
Msg::Theme(ThemeMsg::AuthProtocolBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for AuthProtocol {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct AuthRecentHosts {
component: InputColor,
}
impl AuthRecentHosts {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Recent connections",
IdTheme::AuthRecentHosts,
value,
Msg::Theme(ThemeMsg::AuthRecentHostsBlurDown),
Msg::Theme(ThemeMsg::AuthRecentHostsBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for AuthRecentHosts {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct AuthUsername {
component: InputColor,
}
impl AuthUsername {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Username",
IdTheme::AuthUsername,
value,
Msg::Theme(ThemeMsg::AuthUsernameBlurDown),
Msg::Theme(ThemeMsg::AuthUsernameBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for AuthUsername {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct ExplorerLocalBg {
component: InputColor,
}
impl ExplorerLocalBg {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Local explorer background",
IdTheme::ExplorerLocalBg,
value,
Msg::Theme(ThemeMsg::ExplorerLocalBgBlurDown),
Msg::Theme(ThemeMsg::ExplorerLocalBgBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for ExplorerLocalBg {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct ExplorerLocalFg {
component: InputColor,
}
impl ExplorerLocalFg {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Local explorer foreground",
IdTheme::ExplorerLocalFg,
value,
Msg::Theme(ThemeMsg::ExplorerLocalFgBlurDown),
Msg::Theme(ThemeMsg::ExplorerLocalFgBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for ExplorerLocalFg {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct ExplorerLocalHg {
component: InputColor,
}
impl ExplorerLocalHg {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Local explorer highlighted",
IdTheme::ExplorerLocalHg,
value,
Msg::Theme(ThemeMsg::ExplorerLocalHgBlurDown),
Msg::Theme(ThemeMsg::ExplorerLocalHgBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for ExplorerLocalHg {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct ExplorerRemoteBg {
component: InputColor,
}
impl ExplorerRemoteBg {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Remote explorer background",
IdTheme::ExplorerRemoteBg,
value,
Msg::Theme(ThemeMsg::ExplorerRemoteBgBlurDown),
Msg::Theme(ThemeMsg::ExplorerRemoteBgBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for ExplorerRemoteBg {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct ExplorerRemoteFg {
component: InputColor,
}
impl ExplorerRemoteFg {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Remote explorer foreground",
IdTheme::ExplorerRemoteFg,
value,
Msg::Theme(ThemeMsg::ExplorerRemoteFgBlurDown),
Msg::Theme(ThemeMsg::ExplorerRemoteFgBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for ExplorerRemoteFg {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct ExplorerRemoteHg {
component: InputColor,
}
impl ExplorerRemoteHg {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Remote explorer highlighted",
IdTheme::ExplorerRemoteHg,
value,
Msg::Theme(ThemeMsg::ExplorerRemoteHgBlurDown),
Msg::Theme(ThemeMsg::ExplorerRemoteHgBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for ExplorerRemoteHg {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct LogBg {
component: InputColor,
}
impl LogBg {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Log window background",
IdTheme::LogBg,
value,
Msg::Theme(ThemeMsg::LogBgBlurDown),
Msg::Theme(ThemeMsg::LogBgBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for LogBg {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct LogWindow {
component: InputColor,
}
impl LogWindow {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Log window",
IdTheme::LogWindow,
value,
Msg::Theme(ThemeMsg::LogWindowBlurDown),
Msg::Theme(ThemeMsg::LogWindowBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for LogWindow {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct MiscError {
component: InputColor,
}
impl MiscError {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Error",
IdTheme::MiscError,
value,
Msg::Theme(ThemeMsg::MiscErrorBlurDown),
Msg::Theme(ThemeMsg::MiscErrorBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for MiscError {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct MiscInfo {
component: InputColor,
}
impl MiscInfo {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Info",
IdTheme::MiscInfo,
value,
Msg::Theme(ThemeMsg::MiscInfoBlurDown),
Msg::Theme(ThemeMsg::MiscInfoBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for MiscInfo {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct MiscInput {
component: InputColor,
}
impl MiscInput {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Input",
IdTheme::MiscInput,
value,
Msg::Theme(ThemeMsg::MiscInputBlurDown),
Msg::Theme(ThemeMsg::MiscInputBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for MiscInput {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct MiscKeys {
component: InputColor,
}
impl MiscKeys {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Key strokes",
IdTheme::MiscKeys,
value,
Msg::Theme(ThemeMsg::MiscKeysBlurDown),
Msg::Theme(ThemeMsg::MiscKeysBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for MiscKeys {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct MiscQuit {
component: InputColor,
}
impl MiscQuit {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Quit dialogs",
IdTheme::MiscQuit,
value,
Msg::Theme(ThemeMsg::MiscQuitBlurDown),
Msg::Theme(ThemeMsg::MiscQuitBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for MiscQuit {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct MiscSave {
component: InputColor,
}
impl MiscSave {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Save confirmations",
IdTheme::MiscSave,
value,
Msg::Theme(ThemeMsg::MiscSaveBlurDown),
Msg::Theme(ThemeMsg::MiscSaveBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for MiscSave {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct MiscWarn {
component: InputColor,
}
impl MiscWarn {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Warnings",
IdTheme::MiscWarn,
value,
Msg::Theme(ThemeMsg::MiscWarnBlurDown),
Msg::Theme(ThemeMsg::MiscWarnBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for MiscWarn {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct ProgBarFull {
component: InputColor,
}
impl ProgBarFull {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"'Full transfer' Progress bar",
IdTheme::ProgBarFull,
value,
Msg::Theme(ThemeMsg::ProgBarFullBlurDown),
Msg::Theme(ThemeMsg::ProgBarFullBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for ProgBarFull {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct ProgBarPartial {
component: InputColor,
}
impl ProgBarPartial {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"'Partial transfer' Progress bar",
IdTheme::ProgBarPartial,
value,
Msg::Theme(ThemeMsg::ProgBarPartialBlurDown),
Msg::Theme(ThemeMsg::ProgBarPartialBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for ProgBarPartial {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct StatusHidden {
component: InputColor,
}
impl StatusHidden {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Hidden files",
IdTheme::StatusHidden,
value,
Msg::Theme(ThemeMsg::StatusHiddenBlurDown),
Msg::Theme(ThemeMsg::StatusHiddenBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for StatusHidden {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct StatusSorting {
component: InputColor,
}
impl StatusSorting {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"File sorting",
IdTheme::StatusSorting,
value,
Msg::Theme(ThemeMsg::StatusSortingBlurDown),
Msg::Theme(ThemeMsg::StatusSortingBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for StatusSorting {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
#[derive(MockComponent)]
pub struct StatusSync {
component: InputColor,
}
impl StatusSync {
pub fn new(value: Color) -> Self {
Self {
component: InputColor::new(
"Synchronized browsing",
IdTheme::StatusSync,
value,
Msg::Theme(ThemeMsg::StatusSyncBlurDown),
Msg::Theme(ThemeMsg::StatusSyncBlurUp),
),
}
}
}
impl Component<Msg, NoUserEvent> for StatusSync {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev)
}
}
// -- input color
#[derive(MockComponent)]
struct InputColor {
component: Input,
id: IdTheme,
on_key_down: Msg,
on_key_up: Msg,
}
impl InputColor {
pub fn new(name: &str, id: IdTheme, color: Color, on_key_down: Msg, on_key_up: Msg) -> Self {
let value = crate::utils::fmt::fmt_color(&color);
Self {
component: Input::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.input_type(InputType::Color)
.placeholder("#aa33ee", Style::default().fg(Color::Rgb(128, 128, 128)))
.title(name, Alignment::Left)
.value(value),
id,
on_key_down,
on_key_up,
}
}
fn update_color(&mut self, result: CmdResult) -> Option<Msg> {
if let CmdResult::Changed(State::One(StateValue::String(color))) = result {
let color = tuirealm::utils::parser::parse_color(&color).unwrap();
self.attr(Attribute::Foreground, AttrValue::Color(color));
self.attr(
Attribute::Borders,
AttrValue::Borders(
Borders::default()
.modifiers(BorderType::Rounded)
.color(color),
),
);
Some(Msg::Theme(ThemeMsg::ColorChanged(self.id.clone(), color)))
} else {
self.attr(Attribute::Foreground, AttrValue::Color(Color::Red));
self.attr(
Attribute::Borders,
AttrValue::Borders(
Borders::default()
.modifiers(BorderType::Rounded)
.color(Color::Red),
),
);
Some(Msg::None)
}
}
}
impl Component<Msg, NoUserEvent> for InputColor {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Delete, ..
}) => {
let result = self.perform(Cmd::Cancel);
self.update_color(result)
}
Event::Keyboard(KeyEvent {
code: Key::Backspace,
..
}) => {
let result = self.perform(Cmd::Delete);
self.update_color(result)
}
Event::Keyboard(KeyEvent {
code: Key::Char(ch),
modifiers: KeyModifiers::NONE,
}) => {
let result = self.perform(Cmd::Type(ch));
self.update_color(result)
}
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => Some(self.on_key_down.clone()),
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(self.on_key_up.clone()),
_ => None,
}
}
}

View File

@@ -29,7 +29,6 @@
// Locals
use super::SetupActivity;
// Ext
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use std::env;
impl SetupActivity {
@@ -94,11 +93,15 @@ impl SetupActivity {
// Set editor if config client exists
env::set_var("EDITOR", ctx.config().get_text_editor());
// Prepare terminal
if let Err(err) = disable_raw_mode() {
if let Err(err) = ctx.terminal().disable_raw_mode() {
error!("Failed to disable raw mode: {}", err);
}
// Leave alternate mode
ctx.leave_alternate_screen();
if let Err(err) = ctx.terminal().leave_alternate_screen() {
error!("Could not leave alternate screen: {}", err);
}
// Lock ports
assert!(self.app.lock_ports().is_ok());
// Get result
let result: Result<(), String> = match ctx.config().iter_ssh_keys().nth(idx) {
Some(key) => {
@@ -120,13 +123,19 @@ impl SetupActivity {
};
// Restore terminal
// Clear screen
ctx.clear_screen();
if let Err(err) = ctx.terminal().clear_screen() {
error!("Could not clear screen screen: {}", err);
}
// Enter alternate mode
ctx.enter_alternate_screen();
if let Err(err) = ctx.terminal().enter_alternate_screen() {
error!("Could not enter alternate screen: {}", err);
}
// Re-enable raw mode
if let Err(err) = enable_raw_mode() {
if let Err(err) = ctx.terminal().enable_raw_mode() {
error!("Failed to enter raw mode: {}", err);
}
// Unlock ports
assert!(self.app.unlock_ports().is_ok());
// Return result
result
}

View File

@@ -28,6 +28,7 @@
*/
// Submodules
mod actions;
mod components;
mod config;
mod update;
mod view;
@@ -38,71 +39,209 @@ use crate::config::themes::Theme;
use crate::system::config_client::ConfigClient;
use crate::system::theme_provider::ThemeProvider;
// Ext
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use tuirealm::{Update, View};
use std::time::Duration;
use tuirealm::listener::EventListenerCfg;
use tuirealm::props::Color;
use tuirealm::{application::PollStrategy, Application, NoUserEvent, Update};
// -- components
// -- common
const COMPONENT_TEXT_HELP: &str = "TEXT_HELP";
const COMPONENT_TEXT_FOOTER: &str = "TEXT_FOOTER";
const COMPONENT_TEXT_ERROR: &str = "TEXT_ERROR";
const COMPONENT_RADIO_QUIT: &str = "RADIO_QUIT";
const COMPONENT_RADIO_SAVE: &str = "RADIO_SAVE";
const COMPONENT_RADIO_TAB: &str = "RADIO_TAB";
// -- config
const COMPONENT_INPUT_TEXT_EDITOR: &str = "INPUT_TEXT_EDITOR";
const COMPONENT_RADIO_DEFAULT_PROTOCOL: &str = "RADIO_DEFAULT_PROTOCOL";
const COMPONENT_RADIO_HIDDEN_FILES: &str = "RADIO_HIDDEN_FILES";
const COMPONENT_RADIO_UPDATES: &str = "RADIO_CHECK_UPDATES";
const COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE: &str = "RADIO_PROMPT_ON_FILE_REPLACE";
const COMPONENT_RADIO_GROUP_DIRS: &str = "RADIO_GROUP_DIRS";
const COMPONENT_INPUT_LOCAL_FILE_FMT: &str = "INPUT_LOCAL_FILE_FMT";
const COMPONENT_INPUT_REMOTE_FILE_FMT: &str = "INPUT_REMOTE_FILE_FMT";
const COMPONENT_RADIO_NOTIFICATIONS_ENABLED: &str = "RADIO_NOTIFICATIONS_ENABLED";
const COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD: &str = "INPUT_NOTIFICATIONS_THRESHOLD";
// -- ssh keys
const COMPONENT_LIST_SSH_KEYS: &str = "LIST_SSH_KEYS";
const COMPONENT_INPUT_SSH_HOST: &str = "INPUT_SSH_HOST";
const COMPONENT_INPUT_SSH_USERNAME: &str = "INPUT_SSH_USERNAME";
const COMPONENT_RADIO_DEL_SSH_KEY: &str = "RADIO_DEL_SSH_KEY";
// -- theme
const COMPONENT_COLOR_AUTH_TITLE: &str = "COMPONENT_COLOR_AUTH_TITLE";
const COMPONENT_COLOR_MISC_TITLE: &str = "COMPONENT_COLOR_MISC_TITLE";
const COMPONENT_COLOR_TRANSFER_TITLE: &str = "COMPONENT_COLOR_TRANSFER_TITLE";
const COMPONENT_COLOR_TRANSFER_TITLE_2: &str = "COMPONENT_COLOR_TRANSFER_TITLE_2";
const COMPONENT_COLOR_AUTH_ADDR: &str = "COMPONENT_COLOR_AUTH_ADDR";
const COMPONENT_COLOR_AUTH_BOOKMARKS: &str = "COMPONENT_COLOR_AUTH_BOOKMARKS";
const COMPONENT_COLOR_AUTH_PASSWORD: &str = "COMPONENT_COLOR_AUTH_PASSWORD";
const COMPONENT_COLOR_AUTH_PORT: &str = "COMPONENT_COLOR_AUTH_PORT";
const COMPONENT_COLOR_AUTH_PROTOCOL: &str = "COMPONENT_COLOR_AUTH_PROTOCOL";
const COMPONENT_COLOR_AUTH_RECENTS: &str = "COMPONENT_COLOR_AUTH_RECENTS";
const COMPONENT_COLOR_AUTH_USERNAME: &str = "COMPONENT_COLOR_AUTH_USERNAME";
const COMPONENT_COLOR_MISC_ERROR: &str = "COMPONENT_COLOR_MISC_ERROR";
const COMPONENT_COLOR_MISC_INFO: &str = "COMPONENT_COLOR_MISC_INFO";
const COMPONENT_COLOR_MISC_INPUT: &str = "COMPONENT_COLOR_MISC_INPUT";
const COMPONENT_COLOR_MISC_KEYS: &str = "COMPONENT_COLOR_MISC_KEYS";
const COMPONENT_COLOR_MISC_QUIT: &str = "COMPONENT_COLOR_MISC_QUIT";
const COMPONENT_COLOR_MISC_SAVE: &str = "COMPONENT_COLOR_MISC_SAVE";
const COMPONENT_COLOR_MISC_WARN: &str = "COMPONENT_COLOR_MISC_WARN";
const COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG: &str =
"COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG";
const COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG: &str =
"COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG";
const COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG: &str =
"COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG";
const COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG: &str =
"COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG";
const COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG: &str =
"COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG";
const COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG: &str =
"COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG";
const COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL: &str = "COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL";
const COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL: &str = "COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL";
const COMPONENT_COLOR_TRANSFER_LOG_BG: &str = "COMPONENT_COLOR_TRANSFER_LOG_BG";
const COMPONENT_COLOR_TRANSFER_LOG_WIN: &str = "COMPONENT_COLOR_TRANSFER_LOG_WIN";
const COMPONENT_COLOR_TRANSFER_STATUS_SORTING: &str = "COMPONENT_COLOR_TRANSFER_STATUS_SORTING";
const COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN: &str = "COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN";
const COMPONENT_COLOR_TRANSFER_STATUS_SYNC: &str = "COMPONENT_COLOR_TRANSFER_STATUS_SYNC";
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
enum Id {
Common(IdCommon),
Config(IdConfig),
Ssh(IdSsh),
Theme(IdTheme),
}
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
enum IdCommon {
ErrorPopup,
Footer,
GlobalListener,
Header,
Keybindings,
QuitPopup,
SavePopup,
}
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
enum IdConfig {
CheckUpdates,
DefaultProtocol,
GroupDirs,
HiddenFiles,
LocalFileFmt,
NotificationsEnabled,
NotificationsThreshold,
PromptOnFileReplace,
RemoteFileFmt,
TextEditor,
}
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
enum IdSsh {
DelSshKeyPopup,
SshHost,
SshKeys,
SshUsername,
}
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
pub enum IdTheme {
AuthAddress,
AuthBookmarks,
AuthPassword,
AuthPort,
AuthProtocol,
AuthRecentHosts,
AuthTitle,
AuthUsername,
ExplorerLocalBg,
ExplorerLocalFg,
ExplorerLocalHg,
ExplorerRemoteBg,
ExplorerRemoteFg,
ExplorerRemoteHg,
LogBg,
LogWindow,
MiscError,
MiscInfo,
MiscInput,
MiscKeys,
MiscQuit,
MiscSave,
MiscTitle,
MiscWarn,
ProgBarFull,
ProgBarPartial,
StatusHidden,
StatusSorting,
StatusSync,
TransferTitle,
TransferTitle2,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Msg {
Common(CommonMsg),
Config(ConfigMsg),
Ssh(SshMsg),
Theme(ThemeMsg),
None,
}
#[derive(Debug, Clone, PartialEq)]
pub enum CommonMsg {
ChangeLayout,
CloseErrorPopup,
CloseKeybindingsPopup,
CloseQuitPopup,
CloseSavePopup,
Quit,
RevertChanges,
SaveAndQuit,
SaveConfig,
ShowKeybindings,
ShowQuitPopup,
ShowSavePopup,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ConfigMsg {
CheckUpdatesBlurDown,
CheckUpdatesBlurUp,
ConfigChanged,
DefaultProtocolBlurDown,
DefaultProtocolBlurUp,
GroupDirsBlurDown,
GroupDirsBlurUp,
HiddenFilesBlurDown,
HiddenFilesBlurUp,
LocalFileFmtBlurDown,
LocalFileFmtBlurUp,
NotificationsEnabledBlurDown,
NotificationsEnabledBlurUp,
NotificationsThresholdBlurDown,
NotificationsThresholdBlurUp,
PromptOnFileReplaceBlurDown,
PromptOnFileReplaceBlurUp,
RemoteFileFmtBlurDown,
RemoteFileFmtBlurUp,
TextEditorBlurDown,
TextEditorBlurUp,
}
#[derive(Debug, Clone, PartialEq)]
pub enum SshMsg {
CloseDelSshKeyPopup,
CloseNewSshKeyPopup,
DeleteSshKey,
EditSshKey(usize),
SaveSshKey,
ShowDelSshKeyPopup,
ShowNewSshKeyPopup,
SshHostBlur,
SshUsernameBlur,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ThemeMsg {
AuthAddressBlurDown,
AuthAddressBlurUp,
AuthBookmarksBlurDown,
AuthBookmarksBlurUp,
AuthPasswordBlurDown,
AuthPasswordBlurUp,
AuthPortBlurDown,
AuthPortBlurUp,
AuthProtocolBlurDown,
AuthProtocolBlurUp,
AuthRecentHostsBlurDown,
AuthRecentHostsBlurUp,
AuthUsernameBlurDown,
AuthUsernameBlurUp,
ColorChanged(IdTheme, Color),
ExplorerLocalBgBlurDown,
ExplorerLocalBgBlurUp,
ExplorerLocalFgBlurDown,
ExplorerLocalFgBlurUp,
ExplorerLocalHgBlurDown,
ExplorerLocalHgBlurUp,
ExplorerRemoteBgBlurDown,
ExplorerRemoteBgBlurUp,
ExplorerRemoteFgBlurDown,
ExplorerRemoteFgBlurUp,
ExplorerRemoteHgBlurDown,
ExplorerRemoteHgBlurUp,
LogBgBlurDown,
LogBgBlurUp,
LogWindowBlurDown,
LogWindowBlurUp,
MiscErrorBlurDown,
MiscErrorBlurUp,
MiscInfoBlurDown,
MiscInfoBlurUp,
MiscInputBlurDown,
MiscInputBlurUp,
MiscKeysBlurDown,
MiscKeysBlurUp,
MiscQuitBlurDown,
MiscQuitBlurUp,
MiscSaveBlurDown,
MiscSaveBlurUp,
MiscWarnBlurDown,
MiscWarnBlurUp,
ProgBarFullBlurDown,
ProgBarFullBlurUp,
ProgBarPartialBlurDown,
ProgBarPartialBlurUp,
StatusHiddenBlurDown,
StatusHiddenBlurUp,
StatusSortingBlurDown,
StatusSortingBlurUp,
StatusSyncBlurDown,
StatusSyncBlurUp,
}
// -- store
const STORE_CONFIG_CHANGED: &str = "SETUP_CONFIG_CHANGED";
@@ -110,8 +249,8 @@ const STORE_CONFIG_CHANGED: &str = "SETUP_CONFIG_CHANGED";
/// ### ViewLayout
///
/// Current view layout
#[derive(std::cmp::PartialEq)]
enum ViewLayout {
#[derive(PartialEq)]
pub enum ViewLayout {
SetupForm,
SshKeys,
Theme,
@@ -121,26 +260,28 @@ enum ViewLayout {
///
/// Setup activity states holder
pub struct SetupActivity {
app: Application<Id, Msg, NoUserEvent>,
exit_reason: Option<ExitReason>,
context: Option<Context>, // Context holder
view: View, // View
layout: ViewLayout, // View layout
redraw: bool,
}
impl Default for SetupActivity {
fn default() -> Self {
SetupActivity {
impl SetupActivity {
pub fn new(ticks: Duration) -> Self {
Self {
app: Application::init(
EventListenerCfg::default()
.default_input_listener(ticks)
.poll_timeout(ticks),
),
exit_reason: None,
context: None,
view: View::init(),
layout: ViewLayout::SetupForm,
redraw: true, // Draw at first `on_draw`
}
}
}
impl SetupActivity {
/// ### context
///
/// Returns a reference to context
@@ -205,11 +346,13 @@ impl Activity for SetupActivity {
// Set context
self.context = Some(context);
// Clear terminal
self.context.as_mut().unwrap().clear_screen();
if let Err(err) = self.context.as_mut().unwrap().terminal().clear_screen() {
error!("Failed to clear screen: {}", err);
}
// Set config changed to false
self.set_config_changed(false);
// Put raw mode on enabled
if let Err(err) = enable_raw_mode() {
if let Err(err) = self.context_mut().terminal().enable_raw_mode() {
error!("Failed to enter raw mode: {}", err);
}
// Init view
@@ -229,20 +372,25 @@ impl Activity for SetupActivity {
if self.context.is_none() {
return;
}
// Read one event
if let Ok(Some(event)) = self.context().input_hnd().read_event() {
// Set redraw to true
self.redraw = true;
// Handle event
let msg = self.view.on(event);
self.update(msg);
match self.app.tick(PollStrategy::UpTo(3)) {
Ok(messages) => {
if !messages.is_empty() {
self.redraw = true;
}
for msg in messages.into_iter() {
let mut msg = Some(msg);
while msg.is_some() {
msg = self.update(msg);
}
}
}
Err(err) => {
self.mount_error(format!("Application error: {}", err));
}
}
// Redraw if necessary
// View
if self.redraw {
// View
self.view();
// Redraw back to false
self.redraw = false;
}
}
@@ -262,17 +410,12 @@ impl Activity for SetupActivity {
/// This function finally releases the context
fn on_destroy(&mut self) -> Option<Context> {
// Disable raw mode
if let Err(err) = disable_raw_mode() {
if let Err(err) = self.context_mut().terminal().disable_raw_mode() {
error!("Failed to disable raw mode: {}", err);
}
self.context.as_ref()?;
// Clear terminal and return
match self.context.take() {
Some(mut ctx) => {
ctx.clear_screen();
Some(ctx)
}
None => None,
if let Err(err) = self.context_mut().terminal().clear_screen() {
error!("Failed to clear screen: {}", err);
}
self.context.take()
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -31,18 +31,15 @@ pub mod ssh_keys;
pub mod theme;
use super::*;
use crate::utils::ui::draw_area_in;
pub use setup::*;
pub use ssh_keys::*;
pub use theme::*;
// Ext
use tui_realm_stdlib::{
Input, InputPropsBuilder, List, ListPropsBuilder, Paragraph, ParagraphPropsBuilder, Radio,
RadioPropsBuilder, Span, SpanPropsBuilder,
};
use tuirealm::props::{Alignment, InputType, PropsBuilder, TableBuilder, TextSpan};
use tuirealm::tui::{
style::Color,
widgets::{BorderType, Borders},
use tuirealm::tui::widgets::Clear;
use tuirealm::{
event::{Key, KeyEvent, KeyModifiers},
Frame, Sub, SubClause, SubEventClause,
};
impl SetupActivity {
@@ -61,6 +58,7 @@ impl SetupActivity {
///
/// View gui
pub(super) fn view(&mut self) {
self.redraw = false;
match self.layout {
ViewLayout::SetupForm => self.view_setup(),
ViewLayout::SshKeys => self.view_ssh_keys(),
@@ -73,238 +71,229 @@ impl SetupActivity {
/// ### mount_error
///
/// Mount error box
pub(super) fn mount_error(&mut self, text: &str) {
self.mount_text_dialog(super::COMPONENT_TEXT_ERROR, text, Color::Red);
pub(super) fn mount_error<S: AsRef<str>>(&mut self, text: S) {
assert!(self
.app
.remount(
Id::Common(IdCommon::ErrorPopup),
Box::new(components::ErrorPopup::new(text)),
vec![],
)
.is_ok());
assert!(self.app.active(&Id::Common(IdCommon::ErrorPopup)).is_ok());
}
/// ### umount_error
///
/// Umount error message
pub(super) fn umount_error(&mut self) {
self.view.umount(super::COMPONENT_TEXT_ERROR);
let _ = self.app.umount(&Id::Common(IdCommon::ErrorPopup));
}
/// ### mount_quit
///
/// Mount quit popup
pub(super) fn mount_quit(&mut self) {
self.mount_radio_dialog(
super::COMPONENT_RADIO_QUIT,
"There are unsaved changes! Save changes before leaving?",
&["Save", "Don't save", "Cancel"],
0,
Color::LightRed,
);
assert!(self
.app
.remount(
Id::Common(IdCommon::QuitPopup),
Box::new(components::QuitPopup::default()),
vec![],
)
.is_ok());
assert!(self.app.active(&Id::Common(IdCommon::QuitPopup)).is_ok());
}
/// ### umount_quit
///
/// Umount quit
pub(super) fn umount_quit(&mut self) {
self.view.umount(super::COMPONENT_RADIO_QUIT);
let _ = self.app.umount(&Id::Common(IdCommon::QuitPopup));
}
/// ### mount_save_popup
///
/// Mount save popup
pub(super) fn mount_save_popup(&mut self) {
self.mount_radio_dialog(
super::COMPONENT_RADIO_SAVE,
"Save changes?",
&["Yes", "No"],
0,
Color::LightYellow,
);
assert!(self
.app
.remount(
Id::Common(IdCommon::SavePopup),
Box::new(components::SavePopup::default()),
vec![],
)
.is_ok());
assert!(self.app.active(&Id::Common(IdCommon::SavePopup)).is_ok());
}
/// ### umount_quit
///
/// Umount quit
pub(super) fn umount_save_popup(&mut self) {
self.view.umount(super::COMPONENT_RADIO_SAVE);
}
pub(self) fn mount_header_tab(&mut self, idx: usize) {
self.view.mount(
super::COMPONENT_RADIO_TAB,
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(&[
String::from("User Interface"),
String::from("SSH Keys"),
String::from("Theme"),
])
.with_value(idx)
.rewind(true)
.build(),
)),
);
}
pub(self) fn mount_footer(&mut self) {
self.view.mount(
super::COMPONENT_TEXT_FOOTER,
Box::new(Span::new(
SpanPropsBuilder::default()
.with_spans(vec![
TextSpan::new("Press ").bold(),
TextSpan::new("<CTRL+H>").bold().fg(Color::Cyan),
TextSpan::new(" to show keybindings").bold(),
])
.build(),
)),
);
let _ = self.app.umount(&Id::Common(IdCommon::SavePopup));
}
/// ### mount_help
///
/// Mount help
pub(super) fn mount_help(&mut self) {
self.view.mount(
super::COMPONENT_TEXT_HELP,
Box::new(List::new(
ListPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
.with_highlighted_str(Some("?"))
.with_max_scroll_step(8)
.bold()
.with_title("Help", Alignment::Center)
.scrollable(true)
.with_rows(
TableBuilder::default()
.add_col(TextSpan::new("<ESC>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Exit setup"))
.add_row()
.add_col(TextSpan::new("<TAB>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Change setup page"))
.add_row()
.add_col(TextSpan::new("<RIGHT/LEFT>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Change cursor"))
.add_row()
.add_col(TextSpan::new("<UP/DOWN>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Change input field"))
.add_row()
.add_col(TextSpan::new("<ENTER>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Select / Dismiss popup"))
.add_row()
.add_col(TextSpan::new("<DEL|E>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Delete SSH key"))
.add_row()
.add_col(TextSpan::new("<CTRL+N>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" New SSH key"))
.add_row()
.add_col(TextSpan::new("<CTRL+R>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Revert changes"))
.add_row()
.add_col(TextSpan::new("<CTRL+S>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Save configuration"))
.build(),
)
.build(),
)),
);
// Active help
self.view.active(super::COMPONENT_TEXT_HELP);
assert!(self
.app
.remount(
Id::Common(IdCommon::Keybindings),
Box::new(components::Keybindings::default()),
vec![],
)
.is_ok());
assert!(self.app.active(&Id::Common(IdCommon::Keybindings)).is_ok());
}
/// ### umount_help
///
/// Umount help
pub(super) fn umount_help(&mut self) {
self.view.umount(super::COMPONENT_TEXT_HELP);
let _ = self.app.umount(&Id::Common(IdCommon::Keybindings));
}
// -- mount helpers
fn mount_text_dialog(&mut self, id: &str, text: &str, color: Color) {
// Mount
self.view.mount(
id,
Box::new(Paragraph::new(
ParagraphPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Thick, color)
.with_foreground(color)
.bold()
.with_text_alignment(Alignment::Center)
.with_texts(vec![TextSpan::from(text)])
.build(),
)),
);
// Give focus to error
self.view.active(id);
}
fn mount_radio_dialog(
&mut self,
id: &str,
text: &str,
opts: &[&str],
default: usize,
color: Color,
) {
self.view.mount(
id,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(color)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, color)
.with_title(text, Alignment::Center)
.with_options(opts)
.with_value(default)
.rewind(true)
.build(),
)),
);
// Active
self.view.active(id);
}
fn mount_radio(&mut self, id: &str, text: &str, opts: &[&str], default: usize, color: Color) {
self.view.mount(
id,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(color)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, color)
.with_title(text, Alignment::Left)
.with_options(opts)
.with_value(default)
.rewind(true)
.build(),
)),
);
}
fn mount_input(&mut self, id: &str, label: &str, fg: Color, typ: InputType) {
self.mount_input_ex(id, label, fg, typ, None, None);
}
fn mount_input_ex(
&mut self,
id: &str,
label: &str,
fg: Color,
typ: InputType,
len: Option<usize>,
value: Option<String>,
) {
let mut props = InputPropsBuilder::default();
props
.with_foreground(fg)
.with_borders(Borders::ALL, BorderType::Rounded, fg)
.with_label(label, Alignment::Left)
.with_input(typ);
if let Some(len) = len {
props.with_input_len(len);
pub(super) fn view_popups(&mut self, f: &mut Frame) {
if self.app.mounted(&Id::Common(IdCommon::ErrorPopup)) {
let popup = draw_area_in(f.size(), 50, 10);
f.render_widget(Clear, popup);
// make popup
self.app.view(&Id::Common(IdCommon::ErrorPopup), f, popup);
} else if self.app.mounted(&Id::Common(IdCommon::QuitPopup)) {
// make popup
let popup = draw_area_in(f.size(), 40, 10);
f.render_widget(Clear, popup);
self.app.view(&Id::Common(IdCommon::QuitPopup), f, popup);
} else if self.app.mounted(&Id::Common(IdCommon::Keybindings)) {
// make popup
let popup = draw_area_in(f.size(), 50, 70);
f.render_widget(Clear, popup);
self.app.view(&Id::Common(IdCommon::Keybindings), f, popup);
} else if self.app.mounted(&Id::Common(IdCommon::SavePopup)) {
// make popup
let popup = draw_area_in(f.size(), 30, 10);
f.render_widget(Clear, popup);
self.app.view(&Id::Common(IdCommon::SavePopup), f, popup);
}
if let Some(value) = value {
props.with_value(value);
}
self.view.mount(id, Box::new(Input::new(props.build())));
}
/// ### new_app
///
/// Clean app up and remount common components and global listener
fn new_app(&mut self, layout: ViewLayout) {
self.app.umount_all();
self.mount_global_listener();
self.mount_commons(layout);
}
/// ### mount_commons
///
/// Mount common components
fn mount_commons(&mut self, layout: ViewLayout) {
// Radio tab
assert!(self
.app
.remount(
Id::Common(IdCommon::Header),
Box::new(components::Header::new(layout)),
vec![],
)
.is_ok());
// Footer
assert!(self
.app
.remount(
Id::Common(IdCommon::Footer),
Box::new(components::Footer::default()),
vec![],
)
.is_ok());
}
/// ### mount_global_listener
///
/// Mount global listener
fn mount_global_listener(&mut self) {
assert!(self
.app
.mount(
Id::Common(IdCommon::GlobalListener),
Box::new(components::GlobalListener::default()),
vec![
Sub::new(
SubEventClause::Keyboard(KeyEvent {
code: Key::Esc,
modifiers: KeyModifiers::NONE,
}),
Self::no_popup_mounted_clause(),
),
Sub::new(
SubEventClause::Keyboard(KeyEvent {
code: Key::Tab,
modifiers: KeyModifiers::NONE,
}),
Self::no_popup_mounted_clause(),
),
Sub::new(
SubEventClause::Keyboard(KeyEvent {
code: Key::Char('h'),
modifiers: KeyModifiers::CONTROL,
}),
Self::no_popup_mounted_clause(),
),
Sub::new(
SubEventClause::Keyboard(KeyEvent {
code: Key::Char('r'),
modifiers: KeyModifiers::CONTROL,
}),
Self::no_popup_mounted_clause(),
),
Sub::new(
SubEventClause::Keyboard(KeyEvent {
code: Key::Char('s'),
modifiers: KeyModifiers::CONTROL,
}),
Self::no_popup_mounted_clause(),
),
]
)
.is_ok());
}
/// ### no_popup_mounted_clause
///
/// Returns a sub clause which requires that no popup is mounted in order to be satisfied
fn no_popup_mounted_clause() -> SubClause<Id> {
SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(Id::Common(
IdCommon::ErrorPopup,
))))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(Id::Common(
IdCommon::Keybindings,
))))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(Id::Common(
IdCommon::QuitPopup,
))))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(Id::Common(
IdCommon::SavePopup,
))))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(Id::Ssh(
IdSsh::DelSshKeyPopup,
))))),
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(Id::Ssh(
IdSsh::SshHost,
))))),
)),
)),
)),
)),
)
}
}

View File

@@ -27,23 +27,15 @@
* SOFTWARE.
*/
// Locals
use super::{Context, InputType, SetupActivity};
use super::{components, Context, Id, IdCommon, IdConfig, SetupActivity, ViewLayout};
use crate::filetransfer::FileTransferProtocol;
use crate::fs::explorer::GroupDirs;
use crate::ui::components::bytes::{Bytes, BytesPropsBuilder};
use crate::utils::ui::draw_area_in;
use crate::utils::fmt::fmt_bytes;
// Ext
use std::path::PathBuf;
use tui_realm_stdlib::{InputPropsBuilder, RadioPropsBuilder};
use tuirealm::tui::{
layout::{Constraint, Direction, Layout},
style::Color,
widgets::{BorderType, Borders, Clear},
};
use tuirealm::{
props::{Alignment, PropsBuilder},
Payload, Value, View,
};
use tuirealm::tui::layout::{Constraint, Direction, Layout};
use tuirealm::{State, StateValue};
impl SetupActivity {
// -- view
@@ -52,92 +44,17 @@ impl SetupActivity {
///
/// Initialize setup view
pub(super) fn init_setup(&mut self) {
// Init view
self.view = View::init();
// Common stuff
// Radio tab
self.mount_header_tab(0);
// Footer
self.mount_footer();
// Input fields
self.mount_input(
super::COMPONENT_INPUT_TEXT_EDITOR,
"Text editor",
Color::LightGreen,
InputType::Text,
);
self.view.active(super::COMPONENT_INPUT_TEXT_EDITOR); // <-- Focus
self.mount_radio(
super::COMPONENT_RADIO_DEFAULT_PROTOCOL,
"Default protocol",
&["SFTP", "SCP", "FTP", "FTPS", "AWS S3"],
0,
Color::LightCyan,
);
self.mount_radio(
super::COMPONENT_RADIO_HIDDEN_FILES,
"Show hidden files (by default)?",
&["Yes", "No"],
0,
Color::LightRed,
);
self.mount_radio(
super::COMPONENT_RADIO_UPDATES,
"Check for updates?",
&["Yes", "No"],
0,
Color::LightYellow,
);
self.mount_radio(
super::COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE,
"Prompt when replacing existing files?",
&["Yes", "No"],
0,
Color::LightCyan,
);
self.mount_radio(
super::COMPONENT_RADIO_GROUP_DIRS,
"Group directories",
&["Display first", "Display last", "No"],
0,
Color::LightMagenta,
);
self.mount_input(
super::COMPONENT_INPUT_LOCAL_FILE_FMT,
"File formatter syntax (local)",
Color::LightGreen,
InputType::Text,
);
self.mount_input(
super::COMPONENT_INPUT_REMOTE_FILE_FMT,
"File formatter syntax (remote)",
Color::LightCyan,
InputType::Text,
);
self.mount_radio(
super::COMPONENT_RADIO_NOTIFICATIONS_ENABLED,
"Enable notifications?",
&["Yes", "No"],
0,
Color::LightRed,
);
self.view.mount(
super::COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD,
Box::new(Bytes::new(
BytesPropsBuilder::default()
.with_foreground(Color::LightYellow)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightYellow)
.with_label("Notifications: minimum transfer size", Alignment::Left)
.build(),
)),
);
// Init view (and mount commons)
self.new_app(ViewLayout::SetupForm);
// Load values
self.load_input_values();
// Active text editor
assert!(self.app.active(&Id::Config(IdConfig::TextEditor)).is_ok());
}
pub(super) fn view_setup(&mut self) {
let mut ctx: Context = self.context.take().unwrap();
let _ = ctx.terminal().draw(|f| {
let _ = ctx.terminal().raw_mut().draw(|f| {
// Prepare main chunks
let chunks = Layout::default()
.direction(Direction::Vertical)
@@ -152,8 +69,8 @@ impl SetupActivity {
)
.split(f.size());
// Render common widget
self.view.render(super::COMPONENT_RADIO_TAB, f, chunks[0]);
self.view.render(super::COMPONENT_TEXT_FOOTER, f, chunks[2]);
self.app.view(&Id::Common(IdCommon::Header), f, chunks[0]);
self.app.view(&Id::Common(IdCommon::Footer), f, chunks[2]);
// Make chunks (two columns)
let ui_cfg_chunks = Layout::default()
.direction(Direction::Horizontal)
@@ -174,27 +91,27 @@ impl SetupActivity {
.as_ref(),
)
.split(ui_cfg_chunks[0]);
self.view
.render(super::COMPONENT_INPUT_TEXT_EDITOR, f, ui_cfg_chunks_col1[0]);
self.view.render(
super::COMPONENT_RADIO_DEFAULT_PROTOCOL,
self.app
.view(&Id::Config(IdConfig::TextEditor), f, ui_cfg_chunks_col1[0]);
self.app.view(
&Id::Config(IdConfig::DefaultProtocol),
f,
ui_cfg_chunks_col1[1],
);
self.view.render(
super::COMPONENT_RADIO_HIDDEN_FILES,
self.app
.view(&Id::Config(IdConfig::HiddenFiles), f, ui_cfg_chunks_col1[2]);
self.app.view(
&Id::Config(IdConfig::CheckUpdates),
f,
ui_cfg_chunks_col1[2],
ui_cfg_chunks_col1[3],
);
self.view
.render(super::COMPONENT_RADIO_UPDATES, f, ui_cfg_chunks_col1[3]);
self.view.render(
super::COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE,
self.app.view(
&Id::Config(IdConfig::PromptOnFileReplace),
f,
ui_cfg_chunks_col1[4],
);
self.view
.render(super::COMPONENT_RADIO_GROUP_DIRS, f, ui_cfg_chunks_col1[5]);
self.app
.view(&Id::Config(IdConfig::GroupDirs), f, ui_cfg_chunks_col1[5]);
// Column 2
let ui_cfg_chunks_col2 = Layout::default()
.direction(Direction::Vertical)
@@ -209,59 +126,28 @@ impl SetupActivity {
.as_ref(),
)
.split(ui_cfg_chunks[1]);
self.view.render(
super::COMPONENT_INPUT_LOCAL_FILE_FMT,
self.app.view(
&Id::Config(IdConfig::LocalFileFmt),
f,
ui_cfg_chunks_col2[0],
);
self.view.render(
super::COMPONENT_INPUT_REMOTE_FILE_FMT,
self.app.view(
&Id::Config(IdConfig::RemoteFileFmt),
f,
ui_cfg_chunks_col2[1],
);
self.view.render(
super::COMPONENT_RADIO_NOTIFICATIONS_ENABLED,
self.app.view(
&Id::Config(IdConfig::NotificationsEnabled),
f,
ui_cfg_chunks_col2[2],
);
self.view.render(
super::COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD,
self.app.view(
&Id::Config(IdConfig::NotificationsThreshold),
f,
ui_cfg_chunks_col2[3],
);
// Popups
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(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(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(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);
}
}
self.view_popups(f);
});
// Put context back to context
self.context = Some(ctx);
@@ -272,125 +158,127 @@ impl SetupActivity {
/// Load values from configuration into input fields
pub(crate) fn load_input_values(&mut self) {
// Text editor
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_TEXT_EDITOR) {
let text_editor: String =
String::from(self.config().get_text_editor().as_path().to_string_lossy());
let props = InputPropsBuilder::from(props)
.with_value(text_editor)
.build();
let _ = self.view.update(super::COMPONENT_INPUT_TEXT_EDITOR, props);
}
let text_editor: String =
String::from(self.config().get_text_editor().as_path().to_string_lossy());
assert!(self
.app
.remount(
Id::Config(IdConfig::TextEditor),
Box::new(components::TextEditor::new(text_editor.as_str())),
vec![]
)
.is_ok());
// Protocol
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_DEFAULT_PROTOCOL) {
let protocol: usize = match self.config().get_default_protocol() {
FileTransferProtocol::Sftp => 0,
FileTransferProtocol::Scp => 1,
FileTransferProtocol::Ftp(false) => 2,
FileTransferProtocol::Ftp(true) => 3,
FileTransferProtocol::AwsS3 => 4,
};
let props = RadioPropsBuilder::from(props).with_value(protocol).build();
let _ = self
.view
.update(super::COMPONENT_RADIO_DEFAULT_PROTOCOL, props);
}
assert!(self
.app
.remount(
Id::Config(IdConfig::DefaultProtocol),
Box::new(components::DefaultProtocol::new(
self.config().get_default_protocol()
)),
vec![]
)
.is_ok());
// Hidden files
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_HIDDEN_FILES) {
let hidden: usize = match self.config().get_show_hidden_files() {
true => 0,
false => 1,
};
let props = RadioPropsBuilder::from(props).with_value(hidden).build();
let _ = self.view.update(super::COMPONENT_RADIO_HIDDEN_FILES, props);
}
assert!(self
.app
.remount(
Id::Config(IdConfig::HiddenFiles),
Box::new(components::HiddenFiles::new(
self.config().get_show_hidden_files()
)),
vec![]
)
.is_ok());
// Updates
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_UPDATES) {
let updates: usize = match self.config().get_check_for_updates() {
true => 0,
false => 1,
};
let props = RadioPropsBuilder::from(props).with_value(updates).build();
let _ = self.view.update(super::COMPONENT_RADIO_UPDATES, props);
}
assert!(self
.app
.remount(
Id::Config(IdConfig::CheckUpdates),
Box::new(components::CheckUpdates::new(
self.config().get_check_for_updates()
)),
vec![]
)
.is_ok());
// File replace
if let Some(props) = self
.view
.get_props(super::COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE)
{
let updates: usize = match self.config().get_prompt_on_file_replace() {
true => 0,
false => 1,
};
let props = RadioPropsBuilder::from(props).with_value(updates).build();
let _ = self
.view
.update(super::COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE, props);
}
assert!(self
.app
.remount(
Id::Config(IdConfig::PromptOnFileReplace),
Box::new(components::PromptOnFileReplace::new(
self.config().get_prompt_on_file_replace()
)),
vec![]
)
.is_ok());
// Group dirs
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_GROUP_DIRS) {
let dirs: usize = match self.config().get_group_dirs() {
Some(GroupDirs::First) => 0,
Some(GroupDirs::Last) => 1,
None => 2,
};
let props = RadioPropsBuilder::from(props).with_value(dirs).build();
let _ = self.view.update(super::COMPONENT_RADIO_GROUP_DIRS, props);
}
assert!(self
.app
.remount(
Id::Config(IdConfig::GroupDirs),
Box::new(components::GroupDirs::new(self.config().get_group_dirs())),
vec![]
)
.is_ok());
// Local File Fmt
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_LOCAL_FILE_FMT) {
let file_fmt: String = self.config().get_local_file_fmt().unwrap_or_default();
let props = InputPropsBuilder::from(props).with_value(file_fmt).build();
let _ = self
.view
.update(super::COMPONENT_INPUT_LOCAL_FILE_FMT, props);
}
assert!(self
.app
.remount(
Id::Config(IdConfig::LocalFileFmt),
Box::new(components::LocalFileFmt::new(
&self.config().get_local_file_fmt().unwrap_or_default()
)),
vec![]
)
.is_ok());
// Remote File Fmt
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_REMOTE_FILE_FMT) {
let file_fmt: String = self.config().get_remote_file_fmt().unwrap_or_default();
let props = InputPropsBuilder::from(props).with_value(file_fmt).build();
let _ = self
.view
.update(super::COMPONENT_INPUT_REMOTE_FILE_FMT, props);
}
assert!(self
.app
.remount(
Id::Config(IdConfig::RemoteFileFmt),
Box::new(components::RemoteFileFmt::new(
&self.config().get_remote_file_fmt().unwrap_or_default()
)),
vec![]
)
.is_ok());
// Notifications enabled
if let Some(props) = self
.view
.get_props(super::COMPONENT_RADIO_NOTIFICATIONS_ENABLED)
{
let enabled: usize = match self.config().get_notifications() {
true => 0,
false => 1,
};
let props = RadioPropsBuilder::from(props).with_value(enabled).build();
let _ = self
.view
.update(super::COMPONENT_RADIO_NOTIFICATIONS_ENABLED, props);
}
assert!(self
.app
.remount(
Id::Config(IdConfig::NotificationsEnabled),
Box::new(components::NotificationsEnabled::new(
self.config().get_notifications()
)),
vec![]
)
.is_ok());
// Notifications threshold
if let Some(props) = self
.view
.get_props(super::COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD)
{
let value: u64 = self.config().get_notification_threshold();
let props = BytesPropsBuilder::from(props).with_value(value).build();
let _ = self
.view
.update(super::COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD, props);
}
assert!(self
.app
.remount(
Id::Config(IdConfig::NotificationsThreshold),
Box::new(components::NotificationsThreshold::new(&fmt_bytes(
self.config().get_notification_threshold()
))),
vec![]
)
.is_ok());
}
/// ### collect_input_values
///
/// Collect values from input and put them into the configuration
pub(crate) fn collect_input_values(&mut self) {
if let Some(Payload::One(Value::Str(editor))) =
self.view.get_state(super::COMPONENT_INPUT_TEXT_EDITOR)
if let Ok(State::One(StateValue::String(editor))) =
self.app.state(&Id::Config(IdConfig::TextEditor))
{
self.config_mut()
.set_text_editor(PathBuf::from(editor.as_str()));
}
if let Some(Payload::One(Value::Usize(protocol))) =
self.view.get_state(super::COMPONENT_RADIO_DEFAULT_PROTOCOL)
if let Ok(State::One(StateValue::Usize(protocol))) =
self.app.state(&Id::Config(IdConfig::DefaultProtocol))
{
let protocol: FileTransferProtocol = match protocol {
1 => FileTransferProtocol::Scp,
@@ -401,37 +289,36 @@ impl SetupActivity {
};
self.config_mut().set_default_protocol(protocol);
}
if let Some(Payload::One(Value::Usize(opt))) =
self.view.get_state(super::COMPONENT_RADIO_HIDDEN_FILES)
if let Ok(State::One(StateValue::Usize(opt))) =
self.app.state(&Id::Config(IdConfig::HiddenFiles))
{
let show: bool = matches!(opt, 0);
self.config_mut().set_show_hidden_files(show);
}
if let Some(Payload::One(Value::Usize(opt))) =
self.view.get_state(super::COMPONENT_RADIO_UPDATES)
if let Ok(State::One(StateValue::Usize(opt))) =
self.app.state(&Id::Config(IdConfig::CheckUpdates))
{
let check: bool = matches!(opt, 0);
self.config_mut().set_check_for_updates(check);
}
if let Some(Payload::One(Value::Usize(opt))) = self
.view
.get_state(super::COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE)
if let Ok(State::One(StateValue::Usize(opt))) =
self.app.state(&Id::Config(IdConfig::PromptOnFileReplace))
{
let check: bool = matches!(opt, 0);
self.config_mut().set_prompt_on_file_replace(check);
}
if let Some(Payload::One(Value::Str(fmt))) =
self.view.get_state(super::COMPONENT_INPUT_LOCAL_FILE_FMT)
if let Ok(State::One(StateValue::String(fmt))) =
self.app.state(&Id::Config(IdConfig::LocalFileFmt))
{
self.config_mut().set_local_file_fmt(fmt);
}
if let Some(Payload::One(Value::Str(fmt))) =
self.view.get_state(super::COMPONENT_INPUT_REMOTE_FILE_FMT)
if let Ok(State::One(StateValue::String(fmt))) =
self.app.state(&Id::Config(IdConfig::RemoteFileFmt))
{
self.config_mut().set_remote_file_fmt(fmt);
}
if let Some(Payload::One(Value::Usize(opt))) =
self.view.get_state(super::COMPONENT_RADIO_GROUP_DIRS)
if let Ok(State::One(StateValue::Usize(opt))) =
self.app.state(&Id::Config(IdConfig::GroupDirs))
{
let dirs: Option<GroupDirs> = match opt {
0 => Some(GroupDirs::First),
@@ -440,15 +327,14 @@ impl SetupActivity {
};
self.config_mut().set_group_dirs(dirs);
}
if let Some(Payload::One(Value::Usize(opt))) = self
.view
.get_state(super::COMPONENT_RADIO_NOTIFICATIONS_ENABLED)
if let Ok(State::One(StateValue::Usize(opt))) =
self.app.state(&Id::Config(IdConfig::NotificationsEnabled))
{
self.config_mut().set_notifications(opt == 0);
}
if let Some(Payload::One(Value::U64(bytes))) = self
.view
.get_state(super::COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD)
if let Ok(State::One(StateValue::U64(bytes))) = self
.app
.state(&Id::Config(IdConfig::NotificationsThreshold))
{
self.config_mut().set_notification_threshold(bytes);
}

View File

@@ -27,20 +27,12 @@
* SOFTWARE.
*/
// Locals
use super::{Context, SetupActivity};
use crate::ui::components::bookmark_list::{BookmarkList, BookmarkListPropsBuilder};
use super::{components, Context, Id, IdCommon, IdSsh, SetupActivity, ViewLayout};
use crate::utils::ui::draw_area_in;
// Ext
use tui_realm_stdlib::{Input, InputPropsBuilder};
use tuirealm::tui::{
layout::{Constraint, Direction, Layout},
style::Color,
widgets::{BorderType, Borders, Clear},
};
use tuirealm::{
props::{Alignment, PropsBuilder},
View,
};
use tuirealm::tui::layout::{Constraint, Direction, Layout};
use tuirealm::tui::widgets::Clear;
impl SetupActivity {
// -- view
@@ -49,34 +41,17 @@ impl SetupActivity {
///
/// Initialize ssh keys view
pub(super) fn init_ssh_keys(&mut self) {
// Init view
self.view = View::init();
// Common stuff
// Radio tab
// Radio tab
self.mount_header_tab(1);
// Footer
self.mount_footer();
self.view.mount(
super::COMPONENT_LIST_SSH_KEYS,
Box::new(BookmarkList::new(
BookmarkListPropsBuilder::default()
.with_title("SSH keys", Alignment::Left)
.with_borders(Borders::ALL, BorderType::Plain, Color::LightGreen)
.with_background(Color::LightGreen)
.with_foreground(Color::Black)
.build(),
)),
);
// Give focus
self.view.active(super::COMPONENT_LIST_SSH_KEYS);
// Init view (and mount commons)
self.new_app(ViewLayout::SshKeys);
// Load keys
self.reload_ssh_keys();
// Give focus
assert!(self.app.active(&Id::Ssh(IdSsh::SshKeys)).is_ok());
}
pub(crate) fn view_ssh_keys(&mut self) {
let mut ctx: Context = self.context.take().unwrap();
let _ = ctx.terminal().draw(|f| {
let _ = ctx.terminal().raw_mut().draw(|f| {
// Prepare main chunks
let chunks = Layout::default()
.direction(Direction::Vertical)
@@ -91,72 +66,31 @@ impl SetupActivity {
)
.split(f.size());
// Render common widget
self.view.render(super::COMPONENT_RADIO_TAB, f, chunks[0]);
self.view.render(super::COMPONENT_TEXT_FOOTER, f, chunks[2]);
self.view
.render(super::COMPONENT_LIST_SSH_KEYS, f, chunks[1]);
self.app.view(&Id::Common(IdCommon::Header), f, chunks[0]);
self.app.view(&Id::Common(IdCommon::Footer), f, chunks[2]);
self.app.view(&Id::Ssh(IdSsh::SshKeys), f, chunks[1]);
// Popups
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(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(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(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(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);
self.view
.render(super::COMPONENT_RADIO_DEL_SSH_KEY, f, popup);
}
}
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);
let popup_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Length(3), // Host
Constraint::Length(3), // Username
]
.as_ref(),
)
.split(popup);
self.view
.render(super::COMPONENT_INPUT_SSH_HOST, f, popup_chunks[0]);
self.view
.render(super::COMPONENT_INPUT_SSH_USERNAME, f, popup_chunks[1]);
}
self.view_popups(f);
if self.app.mounted(&Id::Ssh(IdSsh::DelSshKeyPopup)) {
let popup = draw_area_in(f.size(), 30, 10);
f.render_widget(Clear, popup);
self.app.view(&Id::Ssh(IdSsh::DelSshKeyPopup), f, popup);
} else if self.app.mounted(&Id::Ssh(IdSsh::SshHost)) {
let popup = draw_area_in(f.size(), 50, 20);
f.render_widget(Clear, popup);
let popup_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Length(3), // Host
Constraint::Length(3), // Username
]
.as_ref(),
)
.split(popup);
self.app.view(&Id::Ssh(IdSsh::SshHost), f, popup_chunks[0]);
self.app
.view(&Id::Ssh(IdSsh::SshUsername), f, popup_chunks[1]);
}
});
// Put context back to context
@@ -169,82 +103,74 @@ impl SetupActivity {
///
/// Mount delete ssh key component
pub(crate) fn mount_del_ssh_key(&mut self) {
self.mount_radio_dialog(
super::COMPONENT_RADIO_DEL_SSH_KEY,
"Delete key?",
&["Yes", "No"],
1,
Color::LightRed,
);
assert!(self
.app
.remount(
Id::Ssh(IdSsh::DelSshKeyPopup),
Box::new(components::DelSshKeyPopup::default()),
vec![]
)
.is_ok());
assert!(self.app.active(&Id::Ssh(IdSsh::DelSshKeyPopup)).is_ok());
}
/// ### umount_del_ssh_key
///
/// Umount delete ssh key
pub(crate) fn umount_del_ssh_key(&mut self) {
self.view.umount(super::COMPONENT_RADIO_DEL_SSH_KEY);
let _ = self.app.umount(&Id::Ssh(IdSsh::DelSshKeyPopup));
}
/// ### mount_new_ssh_key
///
/// Mount new ssh key prompt
pub(crate) fn mount_new_ssh_key(&mut self) {
self.view.mount(
super::COMPONENT_INPUT_SSH_HOST,
Box::new(Input::new(
InputPropsBuilder::default()
.with_label("Hostname or address", Alignment::Center)
.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(
InputPropsBuilder::default()
.with_label("Username", Alignment::Center)
.with_borders(
Borders::BOTTOM | Borders::RIGHT | Borders::LEFT,
BorderType::Plain,
Color::Reset,
)
.build(),
)),
);
self.view.active(super::COMPONENT_INPUT_SSH_HOST);
assert!(self
.app
.remount(
Id::Ssh(IdSsh::SshHost),
Box::new(components::SshHost::default()),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Ssh(IdSsh::SshUsername),
Box::new(components::SshUsername::default()),
vec![]
)
.is_ok());
assert!(self.app.active(&Id::Ssh(IdSsh::SshHost)).is_ok());
}
/// ### umount_new_ssh_key
///
/// Umount new ssh key prompt
pub(crate) fn umount_new_ssh_key(&mut self) {
self.view.umount(super::COMPONENT_INPUT_SSH_HOST);
self.view.umount(super::COMPONENT_INPUT_SSH_USERNAME);
let _ = self.app.umount(&Id::Ssh(IdSsh::SshUsername));
let _ = self.app.umount(&Id::Ssh(IdSsh::SshHost));
}
/// ### reload_ssh_keys
///
/// Reload ssh keys
pub(crate) fn reload_ssh_keys(&mut self) {
// get props
if let Some(props) = self.view.get_props(super::COMPONENT_LIST_SSH_KEYS) {
// Create texts
let keys: Vec<String> = self
.config()
.iter_ssh_keys()
.map(|x| {
let (addr, username, _) = self.config().get_ssh_key(x).ok().unwrap().unwrap();
format!("{} at {}", addr, username)
})
.collect();
let props = BookmarkListPropsBuilder::from(props)
.with_bookmarks(keys)
.build();
self.view.update(super::COMPONENT_LIST_SSH_KEYS, props);
}
let keys: Vec<String> = self
.config()
.iter_ssh_keys()
.map(|x| {
let (addr, username, _) = self.config().get_ssh_key(x).ok().unwrap().unwrap();
format!("{} at {}", addr, username)
})
.collect();
assert!(self
.app
.remount(
Id::Ssh(IdSsh::SshKeys),
Box::new(components::SshKeys::new(&keys)),
vec![]
)
.is_ok());
}
}

View File

@@ -27,22 +27,10 @@
* SOFTWARE.
*/
// Locals
use super::{Context, SetupActivity};
use crate::config::themes::Theme;
use crate::ui::components::color_picker::{ColorPicker, ColorPickerPropsBuilder};
use crate::utils::parser::parse_color;
use crate::utils::ui::draw_area_in;
use super::{components, Context, Id, IdCommon, IdTheme, SetupActivity, Theme, ViewLayout};
// Ext
use tui_realm_stdlib::{Label, LabelPropsBuilder};
use tuirealm::tui::{
layout::{Constraint, Direction, Layout},
style::Color,
widgets::{BorderType, Borders, Clear},
};
use tuirealm::{
props::{Alignment, PropsBuilder},
Payload, Value, View,
};
use tuirealm::tui::layout::{Constraint, Direction, Layout};
impl SetupActivity {
// -- view
@@ -51,96 +39,19 @@ impl SetupActivity {
///
/// Initialize thene view
pub(super) fn init_theme(&mut self) {
// Init view
self.view = View::init();
// Common stuff
// Radio tab
self.mount_header_tab(2);
// Footer
self.mount_footer();
// auth colors
self.mount_title(super::COMPONENT_COLOR_AUTH_TITLE, "Authentication styles");
self.mount_color_picker(super::COMPONENT_COLOR_AUTH_PROTOCOL, "Protocol");
self.mount_color_picker(super::COMPONENT_COLOR_AUTH_ADDR, "Ip address");
self.mount_color_picker(super::COMPONENT_COLOR_AUTH_PORT, "Port");
self.mount_color_picker(super::COMPONENT_COLOR_AUTH_USERNAME, "Username");
self.mount_color_picker(super::COMPONENT_COLOR_AUTH_PASSWORD, "Password");
self.mount_color_picker(super::COMPONENT_COLOR_AUTH_BOOKMARKS, "Bookmarks");
self.mount_color_picker(super::COMPONENT_COLOR_AUTH_RECENTS, "Recent connections");
// Misc
self.mount_title(super::COMPONENT_COLOR_MISC_TITLE, "Misc styles");
self.mount_color_picker(super::COMPONENT_COLOR_MISC_ERROR, "Error");
self.mount_color_picker(super::COMPONENT_COLOR_MISC_INFO, "Info dialogs");
self.mount_color_picker(super::COMPONENT_COLOR_MISC_INPUT, "Input fields");
self.mount_color_picker(super::COMPONENT_COLOR_MISC_KEYS, "Key strokes");
self.mount_color_picker(super::COMPONENT_COLOR_MISC_QUIT, "Quit dialogs");
self.mount_color_picker(super::COMPONENT_COLOR_MISC_SAVE, "Save confirmations");
self.mount_color_picker(super::COMPONENT_COLOR_MISC_WARN, "Warnings");
// Transfer (1)
self.mount_title(super::COMPONENT_COLOR_TRANSFER_TITLE, "Transfer styles");
self.mount_color_picker(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG,
"Local explorer background",
);
self.mount_color_picker(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG,
"Local explorer foreground",
);
self.mount_color_picker(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG,
"Local explorer highlighted",
);
self.mount_color_picker(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG,
"Remote explorer background",
);
self.mount_color_picker(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG,
"Remote explorer foreground",
);
self.mount_color_picker(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG,
"Remote explorer highlighted",
);
self.mount_color_picker(
super::COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL,
"'Full transfer' Progress bar",
);
self.mount_color_picker(
super::COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL,
"'Partial transfer' Progress bar",
);
// Transfer (2)
self.mount_title(
super::COMPONENT_COLOR_TRANSFER_TITLE_2,
"Transfer styles (2)",
);
self.mount_color_picker(
super::COMPONENT_COLOR_TRANSFER_LOG_BG,
"Log window background",
);
self.mount_color_picker(super::COMPONENT_COLOR_TRANSFER_LOG_WIN, "Log window");
self.mount_color_picker(
super::COMPONENT_COLOR_TRANSFER_STATUS_SORTING,
"File sorting",
);
self.mount_color_picker(
super::COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN,
"Hidden files",
);
self.mount_color_picker(
super::COMPONENT_COLOR_TRANSFER_STATUS_SYNC,
"Synchronized browsing",
);
// Init view (and mount commons)
self.new_app(ViewLayout::Theme);
// Mount titles
self.load_titles();
// Load styles
self.load_styles();
// Active first field
self.view.active(super::COMPONENT_COLOR_AUTH_PROTOCOL);
assert!(self.app.active(&Id::Theme(IdTheme::AuthProtocol)).is_ok());
}
pub(super) fn view_theme(&mut self) {
let mut ctx: Context = self.context.take().unwrap();
let _ = ctx.terminal().draw(|f| {
let _ = ctx.terminal().raw_mut().draw(|f| {
// Prepare main chunks
let chunks = Layout::default()
.direction(Direction::Vertical)
@@ -155,8 +66,8 @@ impl SetupActivity {
)
.split(f.size());
// Render common widget
self.view.render(super::COMPONENT_RADIO_TAB, f, chunks[0]);
self.view.render(super::COMPONENT_TEXT_FOOTER, f, chunks[2]);
self.app.view(&Id::Common(IdCommon::Header), f, chunks[0]);
self.app.view(&Id::Common(IdCommon::Footer), f, chunks[2]);
// Make chunks
let colors_layout = Layout::default()
.direction(Direction::Horizontal)
@@ -186,34 +97,22 @@ impl SetupActivity {
.as_ref(),
)
.split(colors_layout[0]);
self.view
.render(super::COMPONENT_COLOR_AUTH_TITLE, f, auth_colors_layout[0]);
self.view.render(
super::COMPONENT_COLOR_AUTH_PROTOCOL,
f,
auth_colors_layout[1],
);
self.view
.render(super::COMPONENT_COLOR_AUTH_ADDR, f, auth_colors_layout[2]);
self.view
.render(super::COMPONENT_COLOR_AUTH_PORT, f, auth_colors_layout[3]);
self.view.render(
super::COMPONENT_COLOR_AUTH_USERNAME,
f,
auth_colors_layout[4],
);
self.view.render(
super::COMPONENT_COLOR_AUTH_PASSWORD,
f,
auth_colors_layout[5],
);
self.view.render(
super::COMPONENT_COLOR_AUTH_BOOKMARKS,
f,
auth_colors_layout[6],
);
self.view.render(
super::COMPONENT_COLOR_AUTH_RECENTS,
self.app
.view(&Id::Theme(IdTheme::AuthTitle), f, auth_colors_layout[0]);
self.app
.view(&Id::Theme(IdTheme::AuthProtocol), f, auth_colors_layout[1]);
self.app
.view(&Id::Theme(IdTheme::AuthAddress), f, auth_colors_layout[2]);
self.app
.view(&Id::Theme(IdTheme::AuthPort), f, auth_colors_layout[3]);
self.app
.view(&Id::Theme(IdTheme::AuthUsername), f, auth_colors_layout[4]);
self.app
.view(&Id::Theme(IdTheme::AuthPassword), f, auth_colors_layout[5]);
self.app
.view(&Id::Theme(IdTheme::AuthBookmarks), f, auth_colors_layout[6]);
self.app.view(
&Id::Theme(IdTheme::AuthRecentHosts),
f,
auth_colors_layout[7],
);
@@ -233,22 +132,22 @@ impl SetupActivity {
.as_ref(),
)
.split(colors_layout[1]);
self.view
.render(super::COMPONENT_COLOR_MISC_TITLE, f, misc_colors_layout[0]);
self.view
.render(super::COMPONENT_COLOR_MISC_ERROR, f, misc_colors_layout[1]);
self.view
.render(super::COMPONENT_COLOR_MISC_INFO, f, misc_colors_layout[2]);
self.view
.render(super::COMPONENT_COLOR_MISC_INPUT, f, misc_colors_layout[3]);
self.view
.render(super::COMPONENT_COLOR_MISC_KEYS, f, misc_colors_layout[4]);
self.view
.render(super::COMPONENT_COLOR_MISC_QUIT, f, misc_colors_layout[5]);
self.view
.render(super::COMPONENT_COLOR_MISC_SAVE, f, misc_colors_layout[6]);
self.view
.render(super::COMPONENT_COLOR_MISC_WARN, f, misc_colors_layout[7]);
self.app
.view(&Id::Theme(IdTheme::MiscTitle), f, misc_colors_layout[0]);
self.app
.view(&Id::Theme(IdTheme::MiscError), f, misc_colors_layout[1]);
self.app
.view(&Id::Theme(IdTheme::MiscInfo), f, misc_colors_layout[2]);
self.app
.view(&Id::Theme(IdTheme::MiscInput), f, misc_colors_layout[3]);
self.app
.view(&Id::Theme(IdTheme::MiscKeys), f, misc_colors_layout[4]);
self.app
.view(&Id::Theme(IdTheme::MiscQuit), f, misc_colors_layout[5]);
self.app
.view(&Id::Theme(IdTheme::MiscSave), f, misc_colors_layout[6]);
self.app
.view(&Id::Theme(IdTheme::MiscWarn), f, misc_colors_layout[7]);
let transfer_colors_layout_col1 = Layout::default()
.direction(Direction::Vertical)
@@ -266,38 +165,38 @@ impl SetupActivity {
.as_ref(),
)
.split(colors_layout[2]);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_TITLE,
self.app.view(
&Id::Theme(IdTheme::TransferTitle),
f,
transfer_colors_layout_col1[0],
);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG,
self.app.view(
&Id::Theme(IdTheme::ExplorerLocalBg),
f,
transfer_colors_layout_col1[1],
);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG,
self.app.view(
&Id::Theme(IdTheme::ExplorerLocalFg),
f,
transfer_colors_layout_col1[2],
);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG,
self.app.view(
&Id::Theme(IdTheme::ExplorerLocalHg),
f,
transfer_colors_layout_col1[3],
);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG,
self.app.view(
&Id::Theme(IdTheme::ExplorerRemoteBg),
f,
transfer_colors_layout_col1[4],
);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG,
self.app.view(
&Id::Theme(IdTheme::ExplorerRemoteFg),
f,
transfer_colors_layout_col1[5],
);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG,
self.app.view(
&Id::Theme(IdTheme::ExplorerRemoteHg),
f,
transfer_colors_layout_col1[6],
);
@@ -317,332 +216,328 @@ impl SetupActivity {
.as_ref(),
)
.split(colors_layout[3]);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_TITLE_2,
self.app.view(
&Id::Theme(IdTheme::TransferTitle2),
f,
transfer_colors_layout_col2[0],
);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL,
self.app.view(
&Id::Theme(IdTheme::ProgBarFull),
f,
transfer_colors_layout_col2[1],
);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL,
self.app.view(
&Id::Theme(IdTheme::ProgBarPartial),
f,
transfer_colors_layout_col2[2],
);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_LOG_BG,
self.app.view(
&Id::Theme(IdTheme::LogBg),
f,
transfer_colors_layout_col2[3],
);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_LOG_WIN,
self.app.view(
&Id::Theme(IdTheme::LogWindow),
f,
transfer_colors_layout_col2[4],
);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_STATUS_SORTING,
self.app.view(
&Id::Theme(IdTheme::StatusSorting),
f,
transfer_colors_layout_col2[5],
);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN,
self.app.view(
&Id::Theme(IdTheme::StatusHidden),
f,
transfer_colors_layout_col2[6],
);
self.view.render(
super::COMPONENT_COLOR_TRANSFER_STATUS_SYNC,
self.app.view(
&Id::Theme(IdTheme::StatusSync),
f,
transfer_colors_layout_col2[7],
);
// Popups
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(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(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(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);
}
}
self.view_popups(f);
});
// Put context back to context
self.context = Some(ctx);
}
fn load_titles(&mut self) {
assert!(self
.app
.remount(
Id::Theme(IdTheme::AuthTitle),
Box::new(components::AuthTitle::default()),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::MiscTitle),
Box::new(components::MiscTitle::default()),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::TransferTitle),
Box::new(components::TransferTitle::default()),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::TransferTitle2),
Box::new(components::TransferTitle2::default()),
vec![]
)
.is_ok());
}
/// ### load_styles
///
/// Load values from theme into input fields
pub(crate) fn load_styles(&mut self) {
let theme: Theme = self.theme().clone();
self.update_color(super::COMPONENT_COLOR_AUTH_ADDR, theme.auth_address);
self.update_color(super::COMPONENT_COLOR_AUTH_BOOKMARKS, theme.auth_bookmarks);
self.update_color(super::COMPONENT_COLOR_AUTH_PASSWORD, theme.auth_password);
self.update_color(super::COMPONENT_COLOR_AUTH_PORT, theme.auth_port);
self.update_color(super::COMPONENT_COLOR_AUTH_PROTOCOL, theme.auth_protocol);
self.update_color(super::COMPONENT_COLOR_AUTH_RECENTS, theme.auth_recents);
self.update_color(super::COMPONENT_COLOR_AUTH_USERNAME, theme.auth_username);
self.update_color(super::COMPONENT_COLOR_MISC_ERROR, theme.misc_error_dialog);
self.update_color(super::COMPONENT_COLOR_MISC_INFO, theme.misc_info_dialog);
self.update_color(super::COMPONENT_COLOR_MISC_INPUT, theme.misc_input_dialog);
self.update_color(super::COMPONENT_COLOR_MISC_KEYS, theme.misc_keys);
self.update_color(super::COMPONENT_COLOR_MISC_QUIT, theme.misc_quit_dialog);
self.update_color(super::COMPONENT_COLOR_MISC_SAVE, theme.misc_save_dialog);
self.update_color(super::COMPONENT_COLOR_MISC_WARN, theme.misc_warn_dialog);
self.update_color(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG,
theme.transfer_local_explorer_background,
);
self.update_color(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG,
theme.transfer_local_explorer_foreground,
);
self.update_color(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG,
theme.transfer_local_explorer_highlighted,
);
self.update_color(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG,
theme.transfer_remote_explorer_background,
);
self.update_color(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG,
theme.transfer_remote_explorer_foreground,
);
self.update_color(
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG,
theme.transfer_remote_explorer_highlighted,
);
self.update_color(
super::COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL,
theme.transfer_progress_bar_full,
);
self.update_color(
super::COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL,
theme.transfer_progress_bar_partial,
);
self.update_color(
super::COMPONENT_COLOR_TRANSFER_LOG_BG,
theme.transfer_log_background,
);
self.update_color(
super::COMPONENT_COLOR_TRANSFER_LOG_WIN,
theme.transfer_log_window,
);
self.update_color(
super::COMPONENT_COLOR_TRANSFER_STATUS_SORTING,
theme.transfer_status_sorting,
);
self.update_color(
super::COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN,
theme.transfer_status_hidden,
);
self.update_color(
super::COMPONENT_COLOR_TRANSFER_STATUS_SYNC,
theme.transfer_status_sync_browsing,
);
}
/// ### collect_styles
///
/// Collect values from input and put them into the theme.
/// If a component has an invalid color, returns Err(component_id)
pub(crate) fn collect_styles(&mut self) -> Result<(), &'static str> {
// auth
let auth_address: Color = self
.get_color(super::COMPONENT_COLOR_AUTH_ADDR)
.map_err(|_| super::COMPONENT_COLOR_AUTH_ADDR)?;
let auth_bookmarks: Color = self
.get_color(super::COMPONENT_COLOR_AUTH_BOOKMARKS)
.map_err(|_| super::COMPONENT_COLOR_AUTH_BOOKMARKS)?;
let auth_password: Color = self
.get_color(super::COMPONENT_COLOR_AUTH_PASSWORD)
.map_err(|_| super::COMPONENT_COLOR_AUTH_PASSWORD)?;
let auth_port: Color = self
.get_color(super::COMPONENT_COLOR_AUTH_PORT)
.map_err(|_| super::COMPONENT_COLOR_AUTH_PORT)?;
let auth_protocol: Color = self
.get_color(super::COMPONENT_COLOR_AUTH_PROTOCOL)
.map_err(|_| super::COMPONENT_COLOR_AUTH_PROTOCOL)?;
let auth_recents: Color = self
.get_color(super::COMPONENT_COLOR_AUTH_RECENTS)
.map_err(|_| super::COMPONENT_COLOR_AUTH_RECENTS)?;
let auth_username: Color = self
.get_color(super::COMPONENT_COLOR_AUTH_USERNAME)
.map_err(|_| super::COMPONENT_COLOR_AUTH_USERNAME)?;
// misc
let misc_error_dialog: Color = self
.get_color(super::COMPONENT_COLOR_MISC_ERROR)
.map_err(|_| super::COMPONENT_COLOR_MISC_ERROR)?;
let misc_info_dialog: Color = self
.get_color(super::COMPONENT_COLOR_MISC_INFO)
.map_err(|_| super::COMPONENT_COLOR_MISC_INFO)?;
let misc_input_dialog: Color = self
.get_color(super::COMPONENT_COLOR_MISC_INPUT)
.map_err(|_| super::COMPONENT_COLOR_MISC_INPUT)?;
let misc_keys: Color = self
.get_color(super::COMPONENT_COLOR_MISC_KEYS)
.map_err(|_| super::COMPONENT_COLOR_MISC_KEYS)?;
let misc_quit_dialog: Color = self
.get_color(super::COMPONENT_COLOR_MISC_QUIT)
.map_err(|_| super::COMPONENT_COLOR_MISC_QUIT)?;
let misc_save_dialog: Color = self
.get_color(super::COMPONENT_COLOR_MISC_SAVE)
.map_err(|_| super::COMPONENT_COLOR_MISC_SAVE)?;
let misc_warn_dialog: Color = self
.get_color(super::COMPONENT_COLOR_MISC_WARN)
.map_err(|_| super::COMPONENT_COLOR_MISC_WARN)?;
// transfer
let transfer_local_explorer_background: Color = self
.get_color(super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG)
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG)?;
let transfer_local_explorer_foreground: Color = self
.get_color(super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG)
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG)?;
let transfer_local_explorer_highlighted: Color = self
.get_color(super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG)
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG)?;
let transfer_remote_explorer_background: Color = self
.get_color(super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG)
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG)?;
let transfer_remote_explorer_foreground: Color = self
.get_color(super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG)
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG)?;
let transfer_remote_explorer_highlighted: Color = self
.get_color(super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG)
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG)?;
let transfer_log_background: Color = self
.get_color(super::COMPONENT_COLOR_TRANSFER_LOG_BG)
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_LOG_BG)?;
let transfer_log_window: Color = self
.get_color(super::COMPONENT_COLOR_TRANSFER_LOG_WIN)
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_LOG_WIN)?;
let transfer_progress_bar_full: Color = self
.get_color(super::COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL)
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL)?;
let transfer_progress_bar_partial: Color = self
.get_color(super::COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL)
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL)?;
let transfer_status_hidden: Color = self
.get_color(super::COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN)
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN)?;
let transfer_status_sorting: Color = self
.get_color(super::COMPONENT_COLOR_TRANSFER_STATUS_SORTING)
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_STATUS_SORTING)?;
let transfer_status_sync_browsing: Color = self
.get_color(super::COMPONENT_COLOR_TRANSFER_STATUS_SYNC)
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_STATUS_SYNC)?;
// Update theme
let mut theme: &mut Theme = self.theme_mut();
theme.auth_address = auth_address;
theme.auth_bookmarks = auth_bookmarks;
theme.auth_password = auth_password;
theme.auth_port = auth_port;
theme.auth_protocol = auth_protocol;
theme.auth_recents = auth_recents;
theme.auth_username = auth_username;
theme.misc_error_dialog = misc_error_dialog;
theme.misc_info_dialog = misc_info_dialog;
theme.misc_input_dialog = misc_input_dialog;
theme.misc_keys = misc_keys;
theme.misc_quit_dialog = misc_quit_dialog;
theme.misc_save_dialog = misc_save_dialog;
theme.misc_warn_dialog = misc_warn_dialog;
theme.transfer_local_explorer_background = transfer_local_explorer_background;
theme.transfer_local_explorer_foreground = transfer_local_explorer_foreground;
theme.transfer_local_explorer_highlighted = transfer_local_explorer_highlighted;
theme.transfer_remote_explorer_background = transfer_remote_explorer_background;
theme.transfer_remote_explorer_foreground = transfer_remote_explorer_foreground;
theme.transfer_remote_explorer_highlighted = transfer_remote_explorer_highlighted;
theme.transfer_log_background = transfer_log_background;
theme.transfer_log_window = transfer_log_window;
theme.transfer_progress_bar_full = transfer_progress_bar_full;
theme.transfer_progress_bar_partial = transfer_progress_bar_partial;
theme.transfer_status_hidden = transfer_status_hidden;
theme.transfer_status_sorting = transfer_status_sorting;
theme.transfer_status_sync_browsing = transfer_status_sync_browsing;
Ok(())
}
/// ### update_color
///
/// Update color for provided component
fn update_color(&mut self, component: &str, color: Color) {
if let Some(props) = self.view.get_props(component) {
self.view.update(
component,
ColorPickerPropsBuilder::from(props)
.with_color(&color)
.build(),
);
}
}
/// ### get_color
///
/// Get color from component
fn get_color(&self, component: &str) -> Result<Color, ()> {
match self.view.get_state(component) {
Some(Payload::One(Value::Str(color))) => match parse_color(color.as_str()) {
Some(c) => Ok(c),
None => Err(()),
},
_ => Err(()),
}
}
/// ### mount_color_picker
///
/// Mount color picker with provided data
fn mount_color_picker(&mut self, id: &str, label: &str) {
self.view.mount(
id,
Box::new(ColorPicker::new(
ColorPickerPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, Color::Reset)
.with_label(label.to_string(), Alignment::Left)
.build(),
)),
);
}
/// ### mount_title
///
/// Mount title
fn mount_title(&mut self, id: &str, text: &str) {
self.view.mount(
id,
Box::new(Label::new(
LabelPropsBuilder::default()
.bold()
.with_text(text.to_string())
.build(),
)),
);
assert!(self
.app
.remount(
Id::Theme(IdTheme::AuthAddress),
Box::new(components::AuthAddress::new(theme.auth_address)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::AuthBookmarks),
Box::new(components::AuthBookmarks::new(theme.auth_bookmarks)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::AuthPassword),
Box::new(components::AuthPassword::new(theme.auth_password)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::AuthPort),
Box::new(components::AuthPort::new(theme.auth_port)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::AuthProtocol),
Box::new(components::AuthProtocol::new(theme.auth_protocol)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::AuthRecentHosts),
Box::new(components::AuthRecentHosts::new(theme.auth_recents)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::AuthUsername),
Box::new(components::AuthUsername::new(theme.auth_username)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::MiscError),
Box::new(components::MiscError::new(theme.misc_error_dialog)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::MiscInfo),
Box::new(components::MiscInfo::new(theme.misc_info_dialog)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::MiscInput),
Box::new(components::MiscInput::new(theme.misc_input_dialog)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::MiscKeys),
Box::new(components::MiscKeys::new(theme.misc_keys)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::MiscQuit),
Box::new(components::MiscQuit::new(theme.misc_quit_dialog)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::MiscSave),
Box::new(components::MiscSave::new(theme.misc_save_dialog)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::MiscWarn),
Box::new(components::MiscWarn::new(theme.misc_warn_dialog)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::ExplorerLocalBg),
Box::new(components::ExplorerLocalBg::new(
theme.transfer_local_explorer_background
)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::ExplorerLocalFg),
Box::new(components::ExplorerLocalFg::new(
theme.transfer_local_explorer_foreground
)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::ExplorerLocalHg),
Box::new(components::ExplorerLocalHg::new(
theme.transfer_local_explorer_highlighted
)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::ExplorerRemoteBg),
Box::new(components::ExplorerRemoteBg::new(
theme.transfer_remote_explorer_background
)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::ExplorerRemoteFg),
Box::new(components::ExplorerRemoteFg::new(
theme.transfer_remote_explorer_foreground
)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::ExplorerRemoteHg),
Box::new(components::ExplorerRemoteHg::new(
theme.transfer_remote_explorer_highlighted
)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::ProgBarFull),
Box::new(components::ProgBarFull::new(
theme.transfer_progress_bar_full
)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::ProgBarPartial),
Box::new(components::ProgBarPartial::new(
theme.transfer_progress_bar_partial
)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::LogBg),
Box::new(components::LogBg::new(theme.transfer_log_background)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::LogWindow),
Box::new(components::LogWindow::new(theme.transfer_log_window)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::StatusSorting),
Box::new(components::StatusSorting::new(
theme.transfer_status_sorting
)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::StatusHidden),
Box::new(components::StatusHidden::new(theme.transfer_status_hidden)),
vec![]
)
.is_ok());
assert!(self
.app
.remount(
Id::Theme(IdTheme::StatusSync),
Box::new(components::StatusSync::new(
theme.transfer_status_sync_browsing
)),
vec![]
)
.is_ok());
}
}

View File

@@ -1,456 +0,0 @@
//! ## Bookmark list
//!
//! `BookmarkList` component renders a bookmark list tab
/**
* 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_realm_stdlib::utils::get_block;
use tuirealm::event::{Event, KeyCode};
use tuirealm::props::{Alignment, BlockTitle, BordersProps, Props, PropsBuilder};
use tuirealm::tui::{
layout::{Corner, Rect},
style::{Color, Style},
text::Span,
widgets::{BorderType, Borders, List, ListItem, ListState},
};
use tuirealm::{Component, Frame, Msg, Payload, PropPayload, PropValue, Value};
// -- props
const PROP_BOOKMARKS: &str = "bookmarks";
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_title<S: AsRef<str>>(&mut self, text: S, alignment: Alignment) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props.title = Some(BlockTitle::new(text, alignment));
}
self
}
pub fn with_bookmarks(&mut self, bookmarks: Vec<String>) -> &mut Self {
if let Some(props) = self.props.as_mut() {
let bookmarks: Vec<PropValue> = bookmarks.into_iter().map(PropValue::Str).collect();
props
.own
.insert(PROP_BOOKMARKS, PropPayload::Vec(bookmarks));
}
self
}
}
// -- states
/// ## OwnStates
///
/// OwnStates contains states for this component
#[derive(Clone)]
struct OwnStates {
list_index: usize, // Index of selected element in list
list_len: usize, // Length of file list
focus: bool, // Has focus?
}
impl Default for OwnStates {
fn default() -> Self {
OwnStates {
list_index: 0,
list_len: 0,
focus: false,
}
}
}
impl OwnStates {
/// ### set_list_len
///
/// Set list length
pub fn set_list_len(&mut self, len: usize) {
self.list_len = len;
}
/// ### get_list_index
///
/// Return current value for list index
pub fn get_list_index(&self) -> usize {
self.list_index
}
/// ### incr_list_index
///
/// Incremenet list index
pub fn incr_list_index(&mut self) {
// Check if index is at last element
if self.list_index + 1 < self.list_len {
self.list_index += 1;
}
}
/// ### decr_list_index
///
/// Decrement list index
pub fn decr_list_index(&mut self) {
// Check if index is bigger than 0
if self.list_index > 0 {
self.list_index -= 1;
}
}
/// ### reset_list_index
///
/// Reset list index to 0
pub fn reset_list_index(&mut self) {
self.list_index = 0;
}
}
// -- Component
/// ## BookmarkList
///
/// Bookmark list component
pub struct BookmarkList {
props: Props,
states: OwnStates,
}
impl BookmarkList {
/// ### new
///
/// Instantiates a new FileList 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 list length
states.set_list_len(Self::bookmarks_len(&props));
BookmarkList { props, states }
}
fn bookmarks_len(props: &Props) -> usize {
match props.own.get(PROP_BOOKMARKS) {
None => 0,
Some(bookmarks) => bookmarks.unwrap_vec().len(),
}
}
}
impl Component for BookmarkList {
#[cfg(not(tarpaulin_include))]
fn render(&self, render: &mut Frame, area: Rect) {
if self.props.visible {
// Make list
let list_item: Vec<ListItem> = match self.props.own.get(PROP_BOOKMARKS) {
Some(PropPayload::Vec(lines)) => lines
.iter()
.map(|x| x.unwrap_str())
.map(|x| ListItem::new(Span::from(x.to_string())))
.collect(),
_ => vec![],
};
let (fg, bg): (Color, Color) = match self.states.focus {
true => (self.props.foreground, self.props.background),
false => (Color::Reset, Color::Reset),
};
// Render
let mut state: ListState = ListState::default();
state.select(Some(self.states.list_index));
render.render_stateful_widget(
List::new(list_item)
.block(get_block(
&self.props.borders,
self.props.title.as_ref(),
self.states.focus,
))
.start_corner(Corner::TopLeft)
.highlight_style(
Style::default()
.bg(bg)
.fg(fg)
.add_modifier(self.props.modifiers),
),
area,
&mut state,
);
}
}
fn update(&mut self, props: Props) -> Msg {
self.props = props;
// re-Set list length
self.states.set_list_len(Self::bookmarks_len(&self.props));
// Reset list index
self.states.reset_list_index();
Msg::None
}
fn get_props(&self) -> Props {
self.props.clone()
}
fn on(&mut self, ev: Event) -> Msg {
// Match event
if let Event::Key(key) = ev {
match key.code {
KeyCode::Down => {
// Update states
self.states.incr_list_index();
Msg::None
}
KeyCode::Up => {
// Update states
self.states.decr_list_index();
Msg::None
}
KeyCode::PageDown => {
// Update states
for _ in 0..8 {
self.states.incr_list_index();
}
Msg::None
}
KeyCode::PageUp => {
// Update states
for _ in 0..8 {
self.states.decr_list_index();
}
Msg::None
}
KeyCode::Enter => {
// Report event
Msg::OnSubmit(self.get_state())
}
_ => {
// Return key event to activity
Msg::OnKey(key)
}
}
} else {
// Unhandled event
Msg::None
}
}
fn get_state(&self) -> Payload {
Payload::One(Value::Usize(self.states.get_list_index()))
}
fn blur(&mut self) {
self.states.focus = false;
}
fn active(&mut self) {
self.states.focus = true;
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use tuirealm::event::KeyEvent;
#[test]
fn test_ui_components_bookmarks_list() {
// Make component
let mut component: BookmarkList = BookmarkList::new(
BookmarkListPropsBuilder::default()
.hidden()
.visible()
.with_foreground(Color::Red)
.with_background(Color::Blue)
.with_borders(Borders::ALL, BorderType::Double, Color::Red)
.with_title("filelist", Alignment::Left)
.with_bookmarks(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.title.as_ref().unwrap().text(), "filelist");
assert_eq!(
component
.props
.own
.get(PROP_BOOKMARKS)
.unwrap()
.unwrap_vec()
.len(),
2
);
// Verify states
assert_eq!(component.states.list_index, 0);
assert_eq!(component.states.list_len, 2);
assert_eq!(component.states.focus, false);
// Focus
component.active();
assert_eq!(component.states.focus, true);
component.blur();
assert_eq!(component.states.focus, false);
// Update
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::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(
BookmarkListPropsBuilder::from(component.get_props())
.with_bookmarks(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_state(), Payload::One(Value::Usize(0)));
// Render
assert_eq!(component.states.list_index, 0);
// Handle inputs
assert_eq!(
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(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(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(Event::Key(KeyEvent::from(KeyCode::PageUp))),
Msg::None
);
// Index should be incremented
assert_eq!(component.states.list_index, 0);
// Enter
assert_eq!(
component.on(Event::Key(KeyEvent::from(KeyCode::Enter))),
Msg::OnSubmit(Payload::One(Value::Usize(0)))
);
// On key
assert_eq!(
component.on(Event::Key(KeyEvent::from(KeyCode::Backspace))),
Msg::OnKey(KeyEvent::from(KeyCode::Backspace))
);
}
}

View File

@@ -1,310 +0,0 @@
//! ## Bytes
//!
//! `Bytes` component extends an `Input` component in order to provide an input type for byte size.
/**
* 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 crate::utils::fmt::fmt_bytes;
use crate::utils::parser::parse_bytesize;
// ext
use tui_realm_stdlib::{Input, InputPropsBuilder};
use tuirealm::event::Event;
use tuirealm::props::{Alignment, Props, PropsBuilder};
use tuirealm::tui::{
layout::Rect,
style::Color,
widgets::{BorderType, Borders},
};
use tuirealm::{Component, Frame, Msg, Payload, Value};
// -- props
/// ## BytesPropsBuilder
///
/// A wrapper around an `InputPropsBuilder`
pub struct BytesPropsBuilder {
puppet: InputPropsBuilder,
}
impl Default for BytesPropsBuilder {
fn default() -> Self {
Self {
puppet: InputPropsBuilder::default(),
}
}
}
impl PropsBuilder for BytesPropsBuilder {
fn build(&mut self) -> Props {
self.puppet.build()
}
fn hidden(&mut self) -> &mut Self {
self.puppet.hidden();
self
}
fn visible(&mut self) -> &mut Self {
self.puppet.visible();
self
}
}
impl From<Props> for BytesPropsBuilder {
fn from(props: Props) -> Self {
BytesPropsBuilder {
puppet: InputPropsBuilder::from(props),
}
}
}
impl BytesPropsBuilder {
/// ### with_borders
///
/// Set component borders style
pub fn with_borders(
&mut self,
borders: Borders,
variant: BorderType,
color: Color,
) -> &mut Self {
self.puppet.with_borders(borders, variant, color);
self
}
/// ### with_label
///
/// Set input label
pub fn with_label<S: AsRef<str>>(&mut self, label: S, alignment: Alignment) -> &mut Self {
self.puppet.with_label(label, alignment);
self
}
/// ### with_color
///
/// Set initial value for component
pub fn with_foreground(&mut self, color: Color) -> &mut Self {
self.puppet.with_foreground(color);
self
}
/// ### with_color
///
/// Set initial value for component
pub fn with_value(&mut self, val: u64) -> &mut Self {
self.puppet.with_value(fmt_bytes(val));
self
}
}
// -- component
/// ## Bytes
///
/// a wrapper component of `Input` which adds a superset of rules to behave as a color picker
pub struct Bytes {
input: Input,
native_color: Color,
}
impl Bytes {
/// ### new
///
/// Instantiate a new `Bytes`
pub fn new(props: Props) -> Self {
// Instantiate a new color picker using input
Self {
native_color: props.foreground,
input: Input::new(props),
}
}
/// ### update_colors
///
/// Update colors to match selected color, with provided one
fn update_colors(&mut self, color: Color) {
let mut props = self.get_props();
props.foreground = color;
props.borders.color = color;
let _ = self.input.update(props);
}
}
impl Component for Bytes {
/// ### 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 Frame, area: Rect) {
self.input.render(render, 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 {
let msg: Msg = self.input.update(props);
match msg {
Msg::OnChange(Payload::One(Value::Str(input))) => {
match parse_bytesize(input.as_str()) {
Some(bytes) => {
// return OK
self.update_colors(self.native_color);
Msg::OnChange(Payload::One(Value::U64(bytes.as_u64())))
}
None => {
// Invalid color
self.update_colors(Color::Red);
Msg::None
}
}
}
msg => 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) -> Props {
self.input.get_props()
}
/// ### on
///
/// Handle input event and update internal states.
/// Returns a Msg to the view
fn on(&mut self, ev: Event) -> Msg {
// Capture message from input
match self.input.on(ev) {
Msg::OnChange(Payload::One(Value::Str(input))) => {
// Capture color and validate
match parse_bytesize(input.as_str()) {
Some(bytes) => {
// Update color and return OK
self.update_colors(self.native_color);
Msg::OnChange(Payload::One(Value::U64(bytes.as_u64())))
}
None => {
// Invalid color
self.update_colors(Color::Red);
Msg::None
}
}
}
Msg::OnSubmit(_) => Msg::None,
msg => msg,
}
}
/// ### get_state
///
/// Get current state from component
/// For this component returns Unsigned if the input type is a number, otherwise a text
/// The value is always the current input.
fn get_state(&self) -> Payload {
match self.input.get_state() {
Payload::One(Value::Str(bytes)) => match parse_bytesize(bytes.as_str()) {
None => Payload::None,
Some(bytes) => Payload::One(Value::U64(bytes.as_u64())),
},
_ => Payload::None,
}
}
// -- events
/// ### blur
///
/// Blur component; basically remove focus
fn blur(&mut self) {
self.input.blur();
}
/// ### active
///
/// Active component; basically give focus
fn active(&mut self) {
self.input.active();
}
}
#[cfg(test)]
mod test {
use super::*;
use crossterm::event::{KeyCode, KeyEvent};
use pretty_assertions::assert_eq;
#[test]
fn bytes_input() {
let mut component: Bytes = Bytes::new(
BytesPropsBuilder::default()
.visible()
.with_value(1024)
.with_borders(Borders::ALL, BorderType::Double, Color::Rgb(204, 170, 0))
.with_label("omar", Alignment::Left)
.with_foreground(Color::Red)
.build(),
);
// Focus
component.blur();
component.active();
// Get value
assert_eq!(component.get_state(), Payload::One(Value::U64(1024)));
// Set an invalid color
let props = InputPropsBuilder::from(component.get_props())
.with_value(String::from("#pippo1"))
.hidden()
.build();
assert_eq!(component.update(props), Msg::None);
assert_eq!(component.get_state(), Payload::None);
// Reset color
let props = BytesPropsBuilder::from(component.get_props())
.with_value(111)
.hidden()
.build();
assert_eq!(
component.update(props),
Msg::OnChange(Payload::One(Value::U64(111)))
);
// Backspace (invalid)
assert_eq!(
component.on(Event::Key(KeyEvent::from(KeyCode::Backspace))),
Msg::None
);
// Press '1'
assert_eq!(
component.on(Event::Key(KeyEvent::from(KeyCode::Char('B')))),
Msg::OnChange(Payload::One(Value::U64(111)))
);
}
}

View File

@@ -1,301 +0,0 @@
//! ## ColorPicker
//!
//! `ColorPicker` component extends an `Input` component in order to provide some extra features
//! for the color picker.
/**
* 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 crate::utils::fmt::fmt_color;
use crate::utils::parser::parse_color;
// ext
use tui_realm_stdlib::{Input, InputPropsBuilder};
use tuirealm::event::Event;
use tuirealm::props::{Alignment, Props, PropsBuilder};
use tuirealm::tui::{
layout::Rect,
style::Color,
widgets::{BorderType, Borders},
};
use tuirealm::{Component, Frame, Msg, Payload, Value};
// -- props
/// ## ColorPickerPropsBuilder
///
/// A wrapper around an `InputPropsBuilder`
pub struct ColorPickerPropsBuilder {
puppet: InputPropsBuilder,
}
impl Default for ColorPickerPropsBuilder {
fn default() -> Self {
Self {
puppet: InputPropsBuilder::default(),
}
}
}
impl PropsBuilder for ColorPickerPropsBuilder {
fn build(&mut self) -> Props {
self.puppet.build()
}
fn hidden(&mut self) -> &mut Self {
self.puppet.hidden();
self
}
fn visible(&mut self) -> &mut Self {
self.puppet.visible();
self
}
}
impl From<Props> for ColorPickerPropsBuilder {
fn from(props: Props) -> Self {
ColorPickerPropsBuilder {
puppet: InputPropsBuilder::from(props),
}
}
}
impl ColorPickerPropsBuilder {
/// ### with_borders
///
/// Set component borders style
pub fn with_borders(
&mut self,
borders: Borders,
variant: BorderType,
color: Color,
) -> &mut Self {
self.puppet.with_borders(borders, variant, color);
self
}
/// ### with_label
///
/// Set input label
pub fn with_label<S: AsRef<str>>(&mut self, label: S, alignment: Alignment) -> &mut Self {
self.puppet.with_label(label, alignment);
self
}
/// ### with_color
///
/// Set initial value for component
pub fn with_color(&mut self, color: &Color) -> &mut Self {
self.puppet.with_value(fmt_color(color));
self
}
}
// -- component
/// ## ColorPicker
///
/// a wrapper component of `Input` which adds a superset of rules to behave as a color picker
pub struct ColorPicker {
input: Input,
}
impl ColorPicker {
/// ### new
///
/// Instantiate a new `ColorPicker`
pub fn new(props: Props) -> Self {
// Instantiate a new color picker using input
Self {
input: Input::new(props),
}
}
/// ### update_colors
///
/// Update colors to match selected color, with provided one
fn update_colors(&mut self, color: Color) {
let mut props = self.get_props();
props.foreground = color;
props.borders.color = color;
let _ = self.input.update(props);
}
}
impl Component for ColorPicker {
/// ### 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 Frame, area: Rect) {
self.input.render(render, 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 {
let msg: Msg = self.input.update(props);
match msg {
Msg::OnChange(Payload::One(Value::Str(input))) => match parse_color(input.as_str()) {
Some(color) => {
// Update color and return OK
self.update_colors(color);
Msg::OnChange(Payload::One(Value::Str(input)))
}
None => {
// Invalid color
self.update_colors(Color::Red);
Msg::None
}
},
msg => 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) -> Props {
self.input.get_props()
}
/// ### on
///
/// Handle input event and update internal states.
/// Returns a Msg to the view
fn on(&mut self, ev: Event) -> Msg {
// Capture message from input
match self.input.on(ev) {
Msg::OnChange(Payload::One(Value::Str(input))) => {
// Capture color and validate
match parse_color(input.as_str()) {
Some(color) => {
// Update color and return OK
self.update_colors(color);
Msg::OnChange(Payload::One(Value::Str(input)))
}
None => {
// Invalid color
self.update_colors(Color::Red);
Msg::None
}
}
}
Msg::OnSubmit(_) => Msg::None,
msg => msg,
}
}
/// ### get_state
///
/// Get current state from component
/// For this component returns Unsigned if the input type is a number, otherwise a text
/// The value is always the current input.
fn get_state(&self) -> Payload {
match self.input.get_state() {
Payload::One(Value::Str(color)) => match parse_color(color.as_str()) {
None => Payload::None,
Some(_) => Payload::One(Value::Str(color)),
},
_ => Payload::None,
}
}
// -- events
/// ### blur
///
/// Blur component; basically remove focus
fn blur(&mut self) {
self.input.blur();
}
/// ### active
///
/// Active component; basically give focus
fn active(&mut self) {
self.input.active();
}
}
#[cfg(test)]
mod test {
use super::*;
use crossterm::event::{KeyCode, KeyEvent};
use pretty_assertions::assert_eq;
#[test]
fn test_ui_components_color_picker() {
let mut component: ColorPicker = ColorPicker::new(
ColorPickerPropsBuilder::default()
.visible()
.with_color(&Color::Rgb(204, 170, 0))
.with_borders(Borders::ALL, BorderType::Double, Color::Rgb(204, 170, 0))
.with_label("omar", Alignment::Left)
.build(),
);
// Focus
component.blur();
component.active();
// Get value
assert_eq!(
component.get_state(),
Payload::One(Value::Str(String::from("#ccaa00")))
);
// Set an invalid color
let props = InputPropsBuilder::from(component.get_props())
.with_value(String::from("#pippo1"))
.hidden()
.build();
assert_eq!(component.update(props), Msg::None);
assert_eq!(component.get_state(), Payload::None);
// Reset color
let props = ColorPickerPropsBuilder::from(component.get_props())
.with_color(&Color::Rgb(204, 170, 0))
.hidden()
.build();
assert_eq!(
component.update(props),
Msg::OnChange(Payload::One(Value::Str("#ccaa00".to_string())))
);
// Backspace (invalid)
assert_eq!(
component.on(Event::Key(KeyEvent::from(KeyCode::Backspace))),
Msg::None
);
// Press '1'
assert_eq!(
component.on(Event::Key(KeyEvent::from(KeyCode::Char('1')))),
Msg::OnChange(Payload::One(Value::Str(String::from("#ccaa01"))))
);
}
}

View File

@@ -1,765 +0,0 @@
//! ## FileList
//!
//! `FileList` component renders a file list tab
/**
* 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_realm_stdlib::utils::get_block;
use tuirealm::event::{Event, KeyCode, KeyModifiers};
use tuirealm::props::{
Alignment, BlockTitle, BordersProps, PropPayload, PropValue, Props, PropsBuilder,
};
use tuirealm::tui::{
layout::{Corner, Rect},
style::{Color, Style},
text::Span,
widgets::{BorderType, Borders, List, ListItem, ListState},
};
use tuirealm::{Component, Frame, Msg, Payload, Value};
// -- props
const PROP_FILES: &str = "files";
const PALETTE_HIGHLIGHT_COLOR: &str = "props-highlight-color";
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_highlight_color
///
/// Set highlighted color
pub fn with_highlight_color(&mut self, color: Color) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props.palette.insert(PALETTE_HIGHLIGHT_COLOR, 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_title<S: AsRef<str>>(&mut self, text: S, alignment: Alignment) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props.title = Some(BlockTitle::new(text, alignment));
}
self
}
pub fn with_files(&mut self, files: Vec<String>) -> &mut Self {
if let Some(props) = self.props.as_mut() {
let files: Vec<PropValue> = files.into_iter().map(PropValue::Str).collect();
props.own.insert(PROP_FILES, PropPayload::Vec(files));
}
self
}
}
// -- states
/// ## OwnStates
///
/// OwnStates contains states for this component
#[derive(Clone)]
struct OwnStates {
list_index: usize, // Index of selected element in list
selected: Vec<usize>, // Selected files
focus: bool, // Has focus?
}
impl Default for OwnStates {
fn default() -> Self {
OwnStates {
list_index: 0,
selected: Vec::new(),
focus: false,
}
}
}
impl OwnStates {
/// ### init_list_states
///
/// Initialize list states
pub fn init_list_states(&mut self, len: usize) {
self.selected = Vec::with_capacity(len);
self.fix_list_index();
}
/// ### list_index
///
/// Return current value for list index
pub fn list_index(&self) -> usize {
self.list_index
}
/// ### incr_list_index
///
/// Incremenet list index.
/// If `can_rewind` is `true` the index rewinds when boundary is reached
pub fn incr_list_index(&mut self, can_rewind: bool) {
// Check if index is at last element
if self.list_index + 1 < self.list_len() {
self.list_index += 1;
} else if can_rewind {
self.list_index = 0;
}
}
/// ### decr_list_index
///
/// Decrement list index
/// If `can_rewind` is `true` the index rewinds when boundary is reached
pub fn decr_list_index(&mut self, can_rewind: bool) {
// Check if index is bigger than 0
if self.list_index > 0 {
self.list_index -= 1;
} else if self.list_len() > 0 && can_rewind {
self.list_index = self.list_len() - 1;
}
}
/// ### list_len
///
/// Returns the length of the file list, which is actually the capacity of the selection vector
pub fn list_len(&self) -> usize {
self.selected.capacity()
}
/// ### is_selected
///
/// Returns whether the file with index `entry` is selected
pub fn is_selected(&self, entry: usize) -> bool {
self.selected.contains(&entry)
}
/// ### is_selection_empty
///
/// Returns whether the selection is currently empty
pub fn is_selection_empty(&self) -> bool {
self.selected.is_empty()
}
/// ### get_selection
///
/// Returns current file selection
pub fn get_selection(&self) -> Vec<usize> {
self.selected.clone()
}
/// ### fix_list_index
///
/// Keep index if possible, otherwise set to lenght - 1
fn fix_list_index(&mut self) {
if self.list_index >= self.list_len() && self.list_len() > 0 {
self.list_index = self.list_len() - 1;
} else if self.list_len() == 0 {
self.list_index = 0;
}
}
// -- select manipulation
/// ### toggle_file
///
/// Select or deselect file with provided entry index
pub fn toggle_file(&mut self, entry: usize) {
match self.is_selected(entry) {
true => self.deselect(entry),
false => self.select(entry),
}
}
/// ### select_all
///
/// Select all files
pub fn select_all(&mut self) {
for i in 0..self.list_len() {
self.select(i);
}
}
/// ### select
///
/// Select provided index if not selected yet
fn select(&mut self, entry: usize) {
if !self.is_selected(entry) {
self.selected.push(entry);
}
}
/// ### deselect
///
/// Remove element file with associated index
fn deselect(&mut self, entry: usize) {
if self.is_selected(entry) {
self.selected.retain(|&x| x != entry);
}
}
}
// -- Component
/// ## FileList
///
/// File list component
pub struct FileList {
props: Props,
states: OwnStates,
}
impl FileList {
/// ### new
///
/// Instantiates a new FileList starting from Props
/// The method also initializes the component states.
pub fn new(props: Props) -> Self {
// Initialize states
let mut states: OwnStates = OwnStates::default();
// Init list states
states.init_list_states(Self::files_len(&props));
FileList { props, states }
}
fn files_len(props: &Props) -> usize {
match props.own.get(PROP_FILES) {
None => 0,
Some(files) => files.unwrap_vec().len(),
}
}
}
impl Component for FileList {
#[cfg(not(tarpaulin_include))]
fn render(&self, render: &mut Frame, area: Rect) {
if self.props.visible {
// Make list
let list_item: Vec<ListItem> = match self.props.own.get(PROP_FILES) {
Some(PropPayload::Vec(lines)) => lines
.iter()
.enumerate()
.map(|(num, line)| {
let to_display: String = match self.states.is_selected(num) {
true => format!("*{}", line.unwrap_str()),
false => line.unwrap_str().to_string(),
};
ListItem::new(Span::from(to_display))
})
.collect(),
_ => vec![],
};
let highlighted_color: Color = match self.props.palette.get(PALETTE_HIGHLIGHT_COLOR) {
Some(c) => *c,
_ => Color::Reset,
};
let (h_fg, h_bg): (Color, Color) = match self.states.focus {
true => (Color::Black, highlighted_color),
false => (highlighted_color, self.props.background),
};
// Render
let mut state: ListState = ListState::default();
state.select(Some(self.states.list_index));
render.render_stateful_widget(
List::new(list_item)
.block(get_block(
&self.props.borders,
self.props.title.as_ref(),
self.states.focus,
))
.start_corner(Corner::TopLeft)
.style(
Style::default()
.fg(self.props.foreground)
.bg(self.props.background),
)
.highlight_style(
Style::default()
.bg(h_bg)
.fg(h_fg)
.add_modifier(self.props.modifiers),
),
area,
&mut state,
);
}
}
fn update(&mut self, props: Props) -> Msg {
self.props = props;
// re-Set list states
self.states.init_list_states(Self::files_len(&self.props));
Msg::None
}
fn get_props(&self) -> Props {
self.props.clone()
}
fn on(&mut self, ev: Event) -> Msg {
// Match event
if let Event::Key(key) = ev {
match key.code {
KeyCode::Down => {
// Update states
self.states.incr_list_index(true);
Msg::None
}
KeyCode::Up => {
// Update states
self.states.decr_list_index(true);
Msg::None
}
KeyCode::PageDown => {
// Update states
for _ in 0..8 {
self.states.incr_list_index(false);
}
Msg::None
}
KeyCode::PageUp => {
// Update states
for _ in 0..8 {
self.states.decr_list_index(false);
}
Msg::None
}
KeyCode::Char('a') => match key.modifiers.intersects(KeyModifiers::CONTROL) {
// CTRL+A
true => {
// Select all
self.states.select_all();
Msg::None
}
false => Msg::OnKey(key),
},
KeyCode::Char('m') => {
// Toggle current file in selection
self.states.toggle_file(self.states.list_index());
Msg::None
}
KeyCode::Enter => Msg::OnSubmit(self.get_state()),
_ => {
// Return key event to activity
Msg::OnKey(key)
}
}
} else {
// Unhandled event
Msg::None
}
}
/// ### get_state
///
/// Get state returns for this component two different payloads based on the states:
/// - if the file selection is empty, returns the highlighted item as `One` of `Usize`
/// - if at least one item is selected, return the selected as a `Vec` of `Usize`
fn get_state(&self) -> Payload {
match self.states.is_selection_empty() {
true => Payload::One(Value::Usize(self.states.list_index())),
false => Payload::Vec(
self.states
.get_selection()
.into_iter()
.map(Value::Usize)
.collect(),
),
}
}
// -- 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 pretty_assertions::assert_eq;
use tuirealm::event::{KeyEvent, KeyModifiers};
#[test]
fn test_ui_components_file_list_states() {
let mut states: OwnStates = OwnStates::default();
assert_eq!(states.list_len(), 0);
assert_eq!(states.selected.len(), 0);
assert_eq!(states.focus, false);
// Init states
states.init_list_states(4);
assert_eq!(states.list_len(), 4);
assert_eq!(states.selected.len(), 0);
assert!(states.is_selection_empty());
// Select all files
states.select_all();
assert_eq!(states.list_len(), 4);
assert_eq!(states.selected.len(), 4);
assert_eq!(states.is_selection_empty(), false);
assert_eq!(states.get_selection(), vec![0, 1, 2, 3]);
// Verify reset
states.init_list_states(5);
assert_eq!(states.list_len(), 5);
assert_eq!(states.selected.len(), 0);
// Toggle file
states.toggle_file(2);
assert_eq!(states.list_len(), 5);
assert_eq!(states.selected.len(), 1);
assert_eq!(states.selected[0], 2);
states.toggle_file(4);
assert_eq!(states.list_len(), 5);
assert_eq!(states.selected.len(), 2);
assert_eq!(states.selected[1], 4);
states.toggle_file(2);
assert_eq!(states.list_len(), 5);
assert_eq!(states.selected.len(), 1);
assert_eq!(states.selected[0], 4);
// Select twice (nothing should change)
states.select(4);
assert_eq!(states.list_len(), 5);
assert_eq!(states.selected.len(), 1);
assert_eq!(states.selected[0], 4);
// Deselect not-selectd item
states.deselect(2);
assert_eq!(states.list_len(), 5);
assert_eq!(states.selected.len(), 1);
assert_eq!(states.selected[0], 4);
// Index
states.init_list_states(2);
// Incr
states.incr_list_index(false);
assert_eq!(states.list_index(), 1);
states.incr_list_index(false);
assert_eq!(states.list_index(), 1);
states.incr_list_index(true);
assert_eq!(states.list_index(), 0);
// Decr
states.list_index = 1;
states.decr_list_index(false);
assert_eq!(states.list_index(), 0);
states.decr_list_index(false);
assert_eq!(states.list_index(), 0);
states.decr_list_index(true);
assert_eq!(states.list_index(), 1);
// Try fixing index
states.init_list_states(5);
states.list_index = 4;
states.init_list_states(3);
assert_eq!(states.list_index(), 2);
states.init_list_states(6);
assert_eq!(states.list_index(), 2);
// Focus
states.focus = true;
assert_eq!(states.focus, true);
}
#[test]
fn test_ui_components_file_list() {
// Make component
let mut component: FileList = FileList::new(
FileListPropsBuilder::default()
.hidden()
.visible()
.with_foreground(Color::Red)
.with_background(Color::Blue)
.with_highlight_color(Color::LightRed)
.with_borders(Borders::ALL, BorderType::Double, Color::Red)
.with_title("files", Alignment::Left)
.with_files(vec![String::from("file1"), String::from("file2")])
.build(),
);
assert_eq!(
*component
.props
.palette
.get(PALETTE_HIGHLIGHT_COLOR)
.unwrap(),
Color::LightRed
);
assert_eq!(component.props.foreground, Color::Red);
assert_eq!(component.props.background, Color::Blue);
assert_eq!(component.props.visible, true);
assert_eq!(component.props.title.as_ref().unwrap().text(), "files");
assert_eq!(
component
.props
.own
.get(PROP_FILES)
.as_ref()
.unwrap()
.unwrap_vec()
.len(),
2
);
// Verify states
assert_eq!(component.states.list_index, 0);
assert_eq!(component.states.selected.len(), 0);
assert_eq!(component.states.list_len(), 2);
assert_eq!(component.states.selected.capacity(), 2);
assert_eq!(component.states.focus, false);
// Focus
component.active();
assert_eq!(component.states.focus, true);
component.blur();
assert_eq!(component.states.focus, false);
// Update
let props = FileListPropsBuilder::from(component.get_props())
.with_foreground(Color::Yellow)
.hidden()
.build();
assert_eq!(component.update(props), Msg::None);
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(
FileListPropsBuilder::from(component.get_props())
.with_files(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_state(), Payload::One(Value::Usize(1)));
// Render
assert_eq!(component.states.list_index, 1);
// Handle inputs
assert_eq!(
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(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(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(Event::Key(KeyEvent::from(KeyCode::PageUp))),
Msg::None
);
// Index should be incremented
assert_eq!(component.states.list_index, 0);
// Enter
assert_eq!(
component.on(Event::Key(KeyEvent::from(KeyCode::Enter))),
Msg::OnSubmit(Payload::One(Value::Usize(0)))
);
// On key
assert_eq!(
component.on(Event::Key(KeyEvent::from(KeyCode::Backspace))),
Msg::OnKey(KeyEvent::from(KeyCode::Backspace))
);
// Verify 'A' still works
assert_eq!(
component.on(Event::Key(KeyEvent::from(KeyCode::Char('a')))),
Msg::OnKey(KeyEvent::from(KeyCode::Char('a')))
);
// Ctrl + a
assert_eq!(
component.on(Event::Key(KeyEvent::new(
KeyCode::Char('a'),
KeyModifiers::CONTROL
))),
Msg::None
);
assert_eq!(component.states.selected.len(), component.states.list_len());
}
#[test]
fn test_ui_components_file_list_selection() {
// Make component
let mut component: FileList = FileList::new(
FileListPropsBuilder::default()
.with_files(vec![
String::from("file1"),
String::from("file2"),
String::from("file3"),
])
.build(),
);
// Get state
assert_eq!(component.get_state(), Payload::One(Value::Usize(0)));
// Select one
assert_eq!(
component.on(Event::Key(KeyEvent::from(KeyCode::Char('m')))),
Msg::None
);
// Now should be a vec
assert_eq!(component.get_state(), Payload::Vec(vec![Value::Usize(0)]));
// De-select
assert_eq!(
component.on(Event::Key(KeyEvent::from(KeyCode::Char('m')))),
Msg::None
);
assert_eq!(component.get_state(), Payload::One(Value::Usize(0)));
// Go down
assert_eq!(
component.on(Event::Key(KeyEvent::from(KeyCode::Down))),
Msg::None
);
// Select
assert_eq!(
component.on(Event::Key(KeyEvent::from(KeyCode::Char('m')))),
Msg::None
);
assert_eq!(component.get_state(), Payload::Vec(vec![Value::Usize(1)]));
// Go down and select
assert_eq!(
component.on(Event::Key(KeyEvent::from(KeyCode::Down))),
Msg::None
);
assert_eq!(
component.on(Event::Key(KeyEvent::from(KeyCode::Char('m')))),
Msg::None
);
assert_eq!(
component.get_state(),
Payload::Vec(vec![Value::Usize(1), Value::Usize(2)])
);
// Select all
assert_eq!(
component.on(Event::Key(KeyEvent {
code: KeyCode::Char('a'),
modifiers: KeyModifiers::CONTROL,
})),
Msg::None
);
// All selected
assert_eq!(
component.get_state(),
Payload::Vec(vec![Value::Usize(1), Value::Usize(2), Value::Usize(0)])
);
// Update files
component.update(
FileListPropsBuilder::from(component.get_props())
.with_files(vec![String::from("file1"), String::from("file2")])
.build(),
);
// Selection should now be empty
assert_eq!(component.get_state(), Payload::One(Value::Usize(1)));
}
}

View File

@@ -1,433 +0,0 @@
//! ## LogBox
//!
//! `LogBox` component renders a log box view
/**
* 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_realm_stdlib::utils::{get_block, wrap_spans};
use tuirealm::event::{Event, KeyCode};
use tuirealm::props::{
Alignment, BlockTitle, BordersProps, Props, PropsBuilder, Table as TextTable,
};
use tuirealm::tui::{
layout::{Corner, Rect},
style::{Color, Style},
widgets::{BorderType, Borders, List, ListItem, ListState},
};
use tuirealm::{Component, Frame, Msg, Payload, PropPayload, PropValue, Value};
// -- props
const PROP_TABLE: &str = "table";
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
}
/// ### 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
}
pub fn with_title<S: AsRef<str>>(&mut self, text: S, alignment: Alignment) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props.title = Some(BlockTitle::new(text, alignment));
}
self
}
pub fn with_log(&mut self, table: TextTable) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props
.own
.insert(PROP_TABLE, PropPayload::One(PropValue::Table(table)));
}
self
}
}
// -- states
/// ## OwnStates
///
/// OwnStates contains states for this component
#[derive(Clone)]
struct OwnStates {
list_index: usize, // Index of selected element in list
list_len: usize, // Length of file list
focus: bool, // Has focus?
}
impl Default for OwnStates {
fn default() -> Self {
OwnStates {
list_index: 0,
list_len: 0,
focus: false,
}
}
}
impl OwnStates {
/// ### set_list_len
///
/// Set list length
pub fn set_list_len(&mut self, len: usize) {
self.list_len = len;
}
/// ### get_list_index
///
/// Return current value for list index
pub fn get_list_index(&self) -> usize {
self.list_index
}
/// ### incr_list_index
///
/// Incremenet list index
pub fn incr_list_index(&mut self) {
// Check if index is at last element
if self.list_index + 1 < self.list_len {
self.list_index += 1;
}
}
/// ### decr_list_index
///
/// Decrement list index
pub fn decr_list_index(&mut self) {
// Check if index is bigger than 0
if self.list_index > 0 {
self.list_index -= 1;
}
}
/// ### reset_list_index
///
/// Reset list index to last element
pub fn reset_list_index(&mut self) {
self.list_index = 0; // Last element is always 0
}
}
// -- Component
/// ## LogBox
///
/// LogBox list component
pub struct LogBox {
props: Props,
states: OwnStates,
}
impl LogBox {
/// ### new
///
/// Instantiates a new FileList 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 list length
states.set_list_len(Self::table_len(&props));
// Reset list index
states.reset_list_index();
LogBox { props, states }
}
fn table_len(props: &Props) -> usize {
match props.own.get(PROP_TABLE) {
Some(PropPayload::One(PropValue::Table(table))) => table.len(),
_ => 0,
}
}
}
impl Component for LogBox {
#[cfg(not(tarpaulin_include))]
fn render(&self, render: &mut Frame, area: Rect) {
if self.props.visible {
let width: usize = area.width as usize - 4;
// Make list
let list_items: Vec<ListItem> = match self.props.own.get(PROP_TABLE) {
Some(PropPayload::One(PropValue::Table(table))) => table
.iter()
.map(|row| ListItem::new(wrap_spans(row, width, &self.props)))
.collect(), // Make List item from TextSpan
_ => Vec::new(),
};
let w = List::new(list_items)
.block(get_block(
&self.props.borders,
self.props.title.as_ref(),
self.states.focus,
))
.start_corner(Corner::BottomLeft)
.highlight_symbol(">> ")
.style(Style::default().bg(self.props.background))
.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);
}
}
fn update(&mut self, props: Props) -> Msg {
self.props = props;
// re-Set list length
self.states.set_list_len(Self::table_len(&self.props));
// Reset list index
self.states.reset_list_index();
Msg::None
}
fn get_props(&self) -> Props {
self.props.clone()
}
fn on(&mut self, ev: Event) -> Msg {
// Match event
if let Event::Key(key) = ev {
match key.code {
KeyCode::Up => {
// Update states
self.states.incr_list_index();
Msg::None
}
KeyCode::Down => {
// Update states
self.states.decr_list_index();
Msg::None
}
KeyCode::PageUp => {
// Update states
for _ in 0..8 {
self.states.incr_list_index();
}
Msg::None
}
KeyCode::PageDown => {
// Update states
for _ in 0..8 {
self.states.decr_list_index();
}
Msg::None
}
_ => {
// Return key event to activity
Msg::OnKey(key)
}
}
} else {
// Unhandled event
Msg::None
}
}
fn get_state(&self) -> Payload {
Payload::One(Value::Usize(self.states.get_list_index()))
}
fn blur(&mut self) {
self.states.focus = false;
}
fn active(&mut self) {
self.states.focus = true;
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use tuirealm::event::{KeyCode, KeyEvent};
use tuirealm::props::{TableBuilder, TextSpan};
use tuirealm::tui::style::Color;
#[test]
fn test_ui_components_logbox() {
let mut component: LogBox = LogBox::new(
LogboxPropsBuilder::default()
.hidden()
.visible()
.with_borders(Borders::ALL, BorderType::Double, Color::Red)
.with_background(Color::Blue)
.with_title("Log", Alignment::Left)
.with_log(
TableBuilder::default()
.add_col(TextSpan::from("12:29"))
.add_col(TextSpan::from("system crashed"))
.add_row()
.add_col(TextSpan::from("12:38"))
.add_col(TextSpan::from("system alive"))
.build(),
)
.build(),
);
assert_eq!(component.props.visible, true);
assert_eq!(component.props.background, Color::Blue);
assert_eq!(component.props.title.as_ref().unwrap().text(), "Log");
// Verify states
assert_eq!(component.states.list_index, 0);
assert_eq!(component.states.list_len, 2);
assert_eq!(component.states.focus, false);
// Focus
component.active();
assert_eq!(component.states.focus, true);
component.blur();
assert_eq!(component.states.focus, false);
// Update
let props = LogboxPropsBuilder::from(component.get_props())
.hidden()
.build();
assert_eq!(component.update(props), Msg::None);
assert_eq!(component.props.visible, false);
// Increment list index
component.states.list_index += 1;
assert_eq!(component.states.list_index, 1);
// Update
component.update(
LogboxPropsBuilder::from(component.get_props())
.with_log(
TableBuilder::default()
.add_col(TextSpan::from("12:29"))
.add_col(TextSpan::from("system crashed"))
.add_row()
.add_col(TextSpan::from("12:38"))
.add_col(TextSpan::from("system alive"))
.add_row()
.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_state(), Payload::One(Value::Usize(0)));
// RenderData
assert_eq!(component.states.list_index, 0);
// Set cursor to 0
component.states.list_index = 0;
// Handle inputs
assert_eq!(
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(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(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(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(Event::Key(KeyEvent::from(KeyCode::Backspace))),
Msg::OnKey(KeyEvent::from(KeyCode::Backspace))
);
}
}

View File

@@ -1,33 +0,0 @@
//! ## Components
//!
//! `Components` is the module which contains the definitions for all the GUI components for termscp
/**
* 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.
*/
// exports
pub mod bookmark_list;
pub mod bytes;
pub mod color_picker;
pub mod file_list;
pub mod logbox;

View File

@@ -26,34 +26,21 @@
* SOFTWARE.
*/
// Locals
use super::input::InputHandler;
use super::store::Store;
use crate::filetransfer::FileTransferParams;
use crate::system::config_client::ConfigClient;
use crate::system::theme_provider::ThemeProvider;
// Includes
#[cfg(target_family = "unix")]
use crossterm::{
event::DisableMouseCapture,
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen},
};
use std::io::{stdout, Stdout};
use tuirealm::tui::backend::CrosstermBackend;
use tuirealm::tui::Terminal;
type TuiTerminal = Terminal<CrosstermBackend<Stdout>>;
use tuirealm::terminal::TerminalBridge;
/// ## Context
///
/// Context holds data structures used by the ui
/// Context holds data structures shared by the activities
pub struct Context {
ft_params: Option<FileTransferParams>,
config_client: ConfigClient,
pub(crate) store: Store,
input_hnd: InputHandler,
pub(crate) terminal: TuiTerminal,
pub(crate) terminal: TerminalBridge,
theme_provider: ThemeProvider,
error: Option<String>,
}
@@ -71,8 +58,7 @@ impl Context {
ft_params: None,
config_client,
store: Store::init(),
input_hnd: InputHandler::new(),
terminal: Terminal::new(CrosstermBackend::new(Self::stdout())).unwrap(),
terminal: TerminalBridge::new().expect("Could not initialize terminal"),
theme_provider,
error,
}
@@ -92,10 +78,6 @@ impl Context {
&mut self.config_client
}
pub(crate) fn input_hnd(&self) -> &InputHandler {
&self.input_hnd
}
pub(crate) fn store(&self) -> &Store {
&self.store
}
@@ -112,7 +94,7 @@ impl Context {
&mut self.theme_provider
}
pub fn terminal(&mut self) -> &mut TuiTerminal {
pub fn terminal(&mut self) -> &mut TerminalBridge {
&mut self.terminal
}
@@ -137,75 +119,13 @@ impl Context {
pub fn error(&mut self) -> Option<String> {
self.error.take()
}
/// ### enter_alternate_screen
///
/// Enter alternate screen (gui window)
#[cfg(target_family = "unix")]
pub fn enter_alternate_screen(&mut self) {
match execute!(
self.terminal.backend_mut(),
EnterAlternateScreen,
DisableMouseCapture
) {
Err(err) => error!("Failed to enter alternate screen: {}", err),
Ok(_) => info!("Entered alternate screen"),
}
}
/// ### enter_alternate_screen
///
/// Enter alternate screen (gui window)
#[cfg(target_family = "windows")]
pub fn enter_alternate_screen(&self) {}
/// ### leave_alternate_screen
///
/// Go back to normal screen (gui window)
#[cfg(target_family = "unix")]
pub fn leave_alternate_screen(&mut self) {
match execute!(
self.terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
) {
Err(err) => error!("Failed to leave alternate screen: {}", err),
Ok(_) => info!("Left alternate screen"),
}
}
/// ### leave_alternate_screen
///
/// Go back to normal screen (gui window)
#[cfg(target_family = "windows")]
pub fn leave_alternate_screen(&self) {}
/// ### clear_screen
///
/// Clear terminal screen
pub fn clear_screen(&mut self) {
match self.terminal.clear() {
Err(err) => error!("Failed to clear screen: {}", err),
Ok(_) => info!("Cleared screen"),
}
}
#[cfg(target_family = "unix")]
fn stdout() -> Stdout {
let mut stdout = stdout();
assert!(execute!(stdout, EnterAlternateScreen).is_ok());
stdout
}
#[cfg(target_family = "windows")]
fn stdout() -> Stdout {
stdout()
}
}
impl Drop for Context {
fn drop(&mut self) {
// Re-enable terminal stuff
self.leave_alternate_screen();
let _ = self.terminal.disable_raw_mode();
let _ = self.terminal.leave_alternate_screen();
let _ = self.terminal.clear_screen();
}
}

View File

@@ -1,102 +0,0 @@
//! ## Input
//!
//! `input` is the module which provides all the functionalities related to input events in the user interface
/**
* 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 crossterm::event::{poll, read, Event};
use std::time::Duration;
/// ## InputHandler
///
/// InputHandler is the struct which runs a thread which waits for
/// input events from the user and reports them through a receiver
pub(crate) struct InputHandler;
impl InputHandler {
/// ### InputHandler
///
///
pub(crate) fn new() -> InputHandler {
InputHandler {}
}
/// ### fetch_events
///
/// Check if new events have been received from handler
#[allow(dead_code)]
pub(crate) fn fetch_events(&self) -> Result<Vec<Event>, ()> {
let mut inbox: Vec<Event> = Vec::new();
loop {
match self.read_event() {
Ok(ev_opt) => match ev_opt {
Some(ev) => inbox.push(ev),
None => break,
},
Err(_) => return Err(()),
}
}
Ok(inbox)
}
/// ### read_event
///
/// Read event from input listener
pub(crate) fn read_event(&self) -> Result<Option<Event>, ()> {
if let Ok(available) = poll(Duration::from_millis(10)) {
match available {
true => {
// Read event
if let Ok(ev) = read() {
Ok(Some(ev))
} else {
Err(())
}
}
false => Ok(None),
}
} else {
Err(())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ui_input_new() {
let _: InputHandler = InputHandler::new();
}
/* ERRORS ON GITHUB ACTIONS
#[test]
fn test_ui_input_fetch() {
let input_hnd: InputHandler = InputHandler::new();
// Try recv
assert_eq!(input_hnd.fetch_messages().ok().unwrap().len(), 0);
}*/
}

View File

@@ -1,215 +0,0 @@
//! ## Keymap
//!
//! Keymap contains pub constants which can be used in the `update` function to match messages
/**
* 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 tuirealm::event::{KeyCode, KeyEvent, KeyModifiers};
use tuirealm::Msg;
// -- Special keys
pub const MSG_KEY_ENTER: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Enter,
modifiers: KeyModifiers::NONE,
});
pub const MSG_KEY_ESC: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Esc,
modifiers: KeyModifiers::NONE,
});
pub const MSG_KEY_TAB: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Tab,
modifiers: KeyModifiers::NONE,
});
pub const MSG_KEY_DEL: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Delete,
modifiers: KeyModifiers::NONE,
});
pub const MSG_KEY_BACKSPACE: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Backspace,
modifiers: KeyModifiers::NONE,
});
pub const MSG_KEY_DOWN: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Down,
modifiers: KeyModifiers::NONE,
});
pub const MSG_KEY_LEFT: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Left,
modifiers: KeyModifiers::NONE,
});
pub const MSG_KEY_RIGHT: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Right,
modifiers: KeyModifiers::NONE,
});
pub const MSG_KEY_UP: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Up,
modifiers: KeyModifiers::NONE,
});
pub const MSG_KEY_SPACE: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Char(' '),
modifiers: KeyModifiers::NONE,
});
// -- char keys
pub const MSG_KEY_CHAR_A: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Char('a'),
modifiers: KeyModifiers::NONE,
});
pub const MSG_KEY_CHAR_B: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Char('b'),
modifiers: KeyModifiers::NONE,
});
pub const MSG_KEY_CHAR_C: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::NONE,
});
pub const MSG_KEY_CHAR_D: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Char('d'),
modifiers: KeyModifiers::NONE,
});
pub const MSG_KEY_CHAR_E: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Char('e'),
modifiers: KeyModifiers::NONE,
});
pub const MSG_KEY_CHAR_F: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Char('f'),
modifiers: KeyModifiers::NONE,
});
pub const MSG_KEY_CHAR_G: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Char('g'),
modifiers: KeyModifiers::NONE,
});
pub const MSG_KEY_CHAR_H: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Char('h'),
modifiers: KeyModifiers::NONE,
});
pub const MSG_KEY_CHAR_I: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Char('i'),
modifiers: KeyModifiers::NONE,
});
/*
pub const MSG_KEY_CHAR_J: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Char('j'),
modifiers: KeyModifiers::NONE,
});
pub const MSG_KEY_CHAR_K: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Char('k'),
modifiers: KeyModifiers::NONE,
});
*/
pub const MSG_KEY_CHAR_L: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Char('l'),
modifiers: KeyModifiers::NONE,
});
/*
pub const MSG_KEY_CHAR_M: Msg = Msg::OnKey(KeyEvent { NOTE: used for mark
code: KeyCode::Char('m'),
modifiers: KeyModifiers::NONE,
});
*/
pub const MSG_KEY_CHAR_N: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Char('n'),
modifiers: KeyModifiers::NONE,
});
pub const MSG_KEY_CHAR_O: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Char('o'),
modifiers: KeyModifiers::NONE,
});
/*
pub const MSG_KEY_CHAR_P: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Char('p'),
modifiers: KeyModifiers::NONE,
});
*/
pub const MSG_KEY_CHAR_Q: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Char('q'),
modifiers: KeyModifiers::NONE,
});
pub const MSG_KEY_CHAR_R: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Char('r'),
modifiers: KeyModifiers::NONE,
});
pub const MSG_KEY_CHAR_S: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Char('s'),
modifiers: KeyModifiers::NONE,
});
/*
pub const MSG_KEY_CHAR_T: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Char('t'),
modifiers: KeyModifiers::NONE,
});
*/
pub const MSG_KEY_CHAR_U: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Char('u'),
modifiers: KeyModifiers::NONE,
});
pub const MSG_KEY_CHAR_V: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Char('v'),
modifiers: KeyModifiers::NONE,
});
pub const MSG_KEY_CHAR_W: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Char('w'),
modifiers: KeyModifiers::NONE,
});
pub const MSG_KEY_CHAR_X: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Char('x'),
modifiers: KeyModifiers::NONE,
});
pub const MSG_KEY_CHAR_Y: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Char('y'),
modifiers: KeyModifiers::NONE,
});
/*
pub const MSG_KEY_CHAR_Z: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Char('z'),
modifiers: KeyModifiers::NONE,
});
*/
// -- control
pub const MSG_KEY_CTRL_C: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
});
pub const MSG_KEY_CTRL_E: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Char('e'),
modifiers: KeyModifiers::CONTROL,
});
pub const MSG_KEY_CTRL_H: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Char('h'),
modifiers: KeyModifiers::CONTROL,
});
pub const MSG_KEY_CTRL_N: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Char('n'),
modifiers: KeyModifiers::CONTROL,
});
pub const MSG_KEY_CTRL_R: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Char('r'),
modifiers: KeyModifiers::CONTROL,
});
pub const MSG_KEY_CTRL_S: Msg = Msg::OnKey(KeyEvent {
code: KeyCode::Char('s'),
modifiers: KeyModifiers::CONTROL,
});

View File

@@ -27,8 +27,5 @@
*/
// Modules
pub mod activities;
pub(crate) mod components;
pub mod context;
pub(crate) mod input;
pub(crate) mod keymap;
pub(crate) mod store;

View File

@@ -101,7 +101,7 @@ lazy_static! {
* - group 1: amount (number)
* - group 4: unit (K, M, G, T, P)
*/
static ref BYTESIZE_REGEX: Regex = Regex::new(r"(:?([0-9])+)( )*(:?[KMGTP])?B").unwrap();
static ref BYTESIZE_REGEX: Regex = Regex::new(r"(:?([0-9])+)( )*(:?[KMGTP])?B$").unwrap();
}
// -- remote opts
@@ -1133,5 +1133,8 @@ mod tests {
assert_eq!(parse_bytesize("2 GB").unwrap().as_u64(), 2147483648);
assert_eq!(parse_bytesize("1 TB").unwrap().as_u64(), 1099511627776);
assert!(parse_bytesize("1 XB").is_none());
assert!(parse_bytesize("1 GB aaaaa").is_none());
assert!(parse_bytesize("1 GBaaaaa").is_none());
assert!(parse_bytesize("1MBaaaaa").is_none());
}
}