From 54b5583d1af7a6eaa145bf3c496668ccc7d1fd39 Mon Sep 17 00:00:00 2001 From: veeso Date: Sun, 21 Nov 2021 10:02:03 +0100 Subject: [PATCH] Migrated termscp to tui-realm 1.x --- CHANGELOG.md | 10 + Cargo.lock | 27 +- Cargo.toml | 5 +- src/activity_manager.rs | 20 +- src/fs/explorer/formatter.rs | 1 + src/system/auto_update.rs | 10 +- src/ui/activities/auth/bookmarks.rs | 60 +- .../activities/auth/components/bookmarks.rs | 445 +++++ src/ui/activities/auth/components/form.rs | 694 +++++++ src/ui/activities/auth/components/mod.rs | 91 + src/ui/activities/auth/components/popup.rs | 452 +++++ src/ui/activities/auth/components/text.rs | 132 ++ src/ui/activities/auth/misc.rs | 38 +- src/ui/activities/auth/mod.rs | 221 ++- src/ui/activities/auth/update.rs | 607 ++---- src/ui/activities/auth/view.rs | 1085 +++++------ .../activities/filetransfer/actions/edit.rs | 26 +- src/ui/activities/filetransfer/actions/mod.rs | 20 +- .../activities/filetransfer/actions/open.rs | 4 +- .../activities/filetransfer/components/log.rs | 296 +++ .../activities/filetransfer/components/mod.rs | 72 + .../filetransfer/components/popups.rs | 1689 +++++++++++++++++ .../components/transfer/file_list.rs | 400 ++++ .../filetransfer/components/transfer/mod.rs | 494 +++++ src/ui/activities/filetransfer/lib/browser.rs | 2 +- src/ui/activities/filetransfer/misc.rs | 298 ++- src/ui/activities/filetransfer/mod.rs | 228 ++- src/ui/activities/filetransfer/session.rs | 4 +- src/ui/activities/filetransfer/update.rs | 1417 +++++--------- src/ui/activities/filetransfer/view.rs | 1403 ++++++-------- src/ui/activities/setup/actions.rs | 242 ++- src/ui/activities/setup/components/commons.rs | 334 ++++ src/ui/activities/setup/components/config.rs | 489 +++++ src/ui/activities/setup/components/mod.rs | 86 + src/ui/activities/setup/components/ssh.rs | 339 ++++ src/ui/activities/setup/components/theme.rs | 910 +++++++++ src/ui/activities/setup/config.rs | 21 +- src/ui/activities/setup/mod.rs | 331 +++- src/ui/activities/setup/update.rs | 1135 ++++------- src/ui/activities/setup/view/mod.rs | 383 ++-- src/ui/activities/setup/view/setup.rs | 420 ++-- src/ui/activities/setup/view/ssh_keys.rs | 230 +-- src/ui/activities/setup/view/theme.rs | 795 ++++---- src/ui/components/bookmark_list.rs | 456 ----- src/ui/components/bytes.rs | 310 --- src/ui/components/color_picker.rs | 301 --- src/ui/components/file_list.rs | 765 -------- src/ui/components/logbox.rs | 433 ----- src/ui/components/mod.rs | 33 - src/ui/context.rs | 96 +- src/ui/input.rs | 102 - src/ui/keymap.rs | 215 --- src/ui/mod.rs | 3 - src/utils/parser.rs | 5 +- 54 files changed, 10994 insertions(+), 7691 deletions(-) create mode 100644 src/ui/activities/auth/components/bookmarks.rs create mode 100644 src/ui/activities/auth/components/form.rs create mode 100644 src/ui/activities/auth/components/mod.rs create mode 100644 src/ui/activities/auth/components/popup.rs create mode 100644 src/ui/activities/auth/components/text.rs create mode 100644 src/ui/activities/filetransfer/components/log.rs create mode 100644 src/ui/activities/filetransfer/components/mod.rs create mode 100644 src/ui/activities/filetransfer/components/popups.rs create mode 100644 src/ui/activities/filetransfer/components/transfer/file_list.rs create mode 100644 src/ui/activities/filetransfer/components/transfer/mod.rs create mode 100644 src/ui/activities/setup/components/commons.rs create mode 100644 src/ui/activities/setup/components/config.rs create mode 100644 src/ui/activities/setup/components/mod.rs create mode 100644 src/ui/activities/setup/components/ssh.rs create mode 100644 src/ui/activities/setup/components/theme.rs delete mode 100644 src/ui/components/bookmark_list.rs delete mode 100644 src/ui/components/bytes.rs delete mode 100644 src/ui/components/color_picker.rs delete mode 100644 src/ui/components/file_list.rs delete mode 100644 src/ui/components/logbox.rs delete mode 100644 src/ui/components/mod.rs delete mode 100644 src/ui/input.rs delete mode 100644 src/ui/keymap.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c0c128..5c5db35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Cargo.lock b/Cargo.lock index 7dddc94..43a3e13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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]] diff --git a/Cargo.toml b/Cargo.toml index e1d214a..2c55609 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/activity_manager.rs b/src/activity_manager.rs index 4988524..1171b39 100644 --- a/src/activity_manager.rs +++ b/src/activity_manager.rs @@ -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, - 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 { + pub fn new(local_dir: &Path, ticks: Duration) -> Result { // Prepare Context // Initialize configuration client let (config_client, error): (ConfigClient, Option) = @@ -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 { info!("Starting AuthActivity..."); // Prepare activity - let mut activity: AuthActivity = AuthActivity::default(); + let mut activity: AuthActivity = AuthActivity::new(self.ticks); // Prepare result let result: Option; // 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; // 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 { // 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(); diff --git a/src/fs/explorer/formatter.rs b/src/fs/explorer/formatter.rs index 4acaf6c..440e89a 100644 --- a/src/fs/explorer/formatter.rs +++ b/src/fs/explorer/formatter.rs @@ -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 { diff --git a/src/system/auto_update.rs b/src/system/auto_update.rs index 0194d4d..6f55171 100644 --- a/src/system/auto_update.rs +++ b/src/system/auto_update.rs @@ -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()); diff --git a/src/ui/activities/auth/bookmarks.rs b/src/ui/activities/auth/bookmarks.rs index 699f04c..f1634b3 100644 --- a/src/ui/activities/auth/bookmarks.rs +++ b/src/ui/activities/auth/bookmarks.rs @@ -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("")); } } diff --git a/src/ui/activities/auth/components/bookmarks.rs b/src/ui/activities/auth/components/bookmarks.rs new file mode 100644 index 0000000..51acfa9 --- /dev/null +++ b/src/ui/activities/auth/components/bookmarks.rs @@ -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 for BookmarksList { + fn on(&mut self, ev: Event) -> Option { + 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 for RecentsList { + fn on(&mut self, ev: Event) -> Option { + 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 for DeleteBookmarkPopup { + fn on(&mut self, ev: Event) -> Option { + 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 for DeleteRecentPopup { + fn on(&mut self, ev: Event) -> Option { + 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 for BookmarkSavePassword { + fn on(&mut self, ev: Event) -> Option { + 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 for BookmarkName { + fn on(&mut self, ev: Event) -> Option { + 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, + } + } +} diff --git a/src/ui/activities/auth/components/form.rs b/src/ui/activities/auth/components/form.rs new file mode 100644 index 0000000..29a5c72 --- /dev/null +++ b/src/ui/activities/auth/components/form.rs @@ -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 for ProtocolRadio { + fn on(&mut self, ev: Event) -> Option { + 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 for InputAddress { + fn on(&mut self, ev: Event) -> Option { + 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 for InputPort { + fn on(&mut self, ev: Event) -> Option { + 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 for InputUsername { + fn on(&mut self, ev: Event) -> Option { + 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 for InputPassword { + fn on(&mut self, ev: Event) -> Option { + 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 for InputS3Bucket { + fn on(&mut self, ev: Event) -> Option { + 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 for InputS3Region { + fn on(&mut self, ev: Event) -> Option { + 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 for InputS3Profile { + fn on(&mut self, ev: Event) -> Option { + 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, + } + } +} diff --git a/src/ui/activities/auth/components/mod.rs b/src/ui/activities/auth/components/mod.rs new file mode 100644 index 0000000..908cd36 --- /dev/null +++ b/src/ui/activities/auth/components/mod.rs @@ -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 for GlobalListener { + fn on(&mut self, ev: Event) -> Option { + 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, + } + } +} diff --git a/src/ui/activities/auth/components/popup.rs b/src/ui/activities/auth/components/popup.rs new file mode 100644 index 0000000..3613ee8 --- /dev/null +++ b/src/ui/activities/auth/components/popup.rs @@ -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>(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 for ErrorPopup { + fn on(&mut self, ev: Event) -> Option { + 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>(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 for InfoPopup { + fn on(&mut self, ev: Event) -> Option { + 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>(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 for WaitPopup { + fn on(&mut self, _ev: Event) -> Option { + 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 for WindowSizeError { + fn on(&mut self, _ev: Event) -> Option { + 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 for QuitPopup { + fn on(&mut self, ev: Event) -> Option { + 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 for InstallUpdatePopup { + fn on(&mut self, ev: Event) -> Option { + 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::>() + .as_slice(), + ), + } + } +} + +impl Component for ReleaseNotes { + fn on(&mut self, ev: Event) -> Option { + 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("").bold().fg(color)) + .add_col(TextSpan::from(" Quit termscp")) + .add_row() + .add_col(TextSpan::new("").bold().fg(color)) + .add_col(TextSpan::from(" Switch from form and bookmarks")) + .add_row() + .add_col(TextSpan::new("").bold().fg(color)) + .add_col(TextSpan::from(" Switch bookmark tab")) + .add_row() + .add_col(TextSpan::new("").bold().fg(color)) + .add_col(TextSpan::from(" Move up/down in current tab")) + .add_row() + .add_col(TextSpan::new("").bold().fg(color)) + .add_col(TextSpan::from(" Connect/Load bookmark")) + .add_row() + .add_col(TextSpan::new("").bold().fg(color)) + .add_col(TextSpan::from(" Delete selected bookmark")) + .add_row() + .add_col(TextSpan::new("").bold().fg(color)) + .add_col(TextSpan::from(" Enter setup")) + .add_row() + .add_col(TextSpan::new("").bold().fg(color)) + .add_col(TextSpan::from(" Save bookmark")) + .build(), + ), + } + } +} + +impl Component for Keybindings { + fn on(&mut self, ev: Event) -> Option { + 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, + } + } +} diff --git a/src/ui/activities/auth/components/text.rs b/src/ui/activities/auth/components/text.rs new file mode 100644 index 0000000..c2f7fa7 --- /dev/null +++ b/src/ui/activities/auth/components/text.rs @@ -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 for Title { + fn on(&mut self, _ev: Event) -> Option { + 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 for Subtitle { + fn on(&mut self, _ev: Event) -> Option { + 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 ", + ), + ]), + } + } +} + +impl Component for NewVersionDisclaimer { + fn on(&mut self, _ev: Event) -> Option { + 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("").bold().fg(key_color), + TextSpan::new(" to show keybindings; ").bold(), + TextSpan::new("").bold().fg(key_color), + TextSpan::new(" to enter setup").bold(), + ]), + } + } +} + +impl Component for HelpText { + fn on(&mut self, _ev: Event) -> Option { + None + } +} diff --git a/src/ui/activities/auth/misc.rs b/src/ui/activities/auth/misc.rs index f85c09e..5e83118 100644 --- a/src/ui/activities/auth/misc.rs +++ b/src/ui/activities/auth/misc.rs @@ -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 { - 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 { + pub(super) fn collect_s3_host_params(&self) -> Result { let (bucket, region, profile): (String, String, Option) = 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, }) diff --git a/src/ui/activities/auth/mod.rs b/src/ui/activities/auth/mod.rs index a568235..2d9da50 100644 --- a/src/ui/activities/auth/mod.rs +++ b/src/ui/activities/auth/mod.rs @@ -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, - context: Option, - view: View, + app: Application, bookmarks_client: Option, - redraw: bool, // Should ui actually be redrawned? - bookmarks_list: Vec, // List of bookmarks - recents_list: Vec, // list of recents -} - -impl Default for AuthActivity { - fn default() -> Self { - Self::new() - } + /// List of bookmarks + bookmarks_list: Vec, + /// List of recent hosts + recents_list: Vec, + /// Exit reason + exit_reason: Option, + /// Should redraw ui + redraw: bool, + /// Protocol + protocol: FileTransferProtocol, + context: Option, } 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 { // 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, -} diff --git a/src/ui/activities/auth/update.rs b/src/ui/activities/auth/update.rs index de76419..2457bee 100644 --- a/src/ui/activities/auth/update.rs +++ b/src/ui/activities/auth/update.rs @@ -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 for AuthActivity { + fn update(&mut self, msg: Option) -> Option { + 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 - // / - (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 - } - // - (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, - } - } - // 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, - // 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 , 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(), - // => 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 } } diff --git a/src/ui/activities/auth/view.rs b/src/ui/activities/auth/view.rs index 21c22b4..ec81b0b 100644 --- a/src/ui/activities/auth/view.rs +++ b/src/ui/activities/auth/view.rs @@ -26,26 +26,15 @@ * SOFTWARE. */ // Locals -use super::{AuthActivity, Context, FileTransferProtocol, InputMask}; +use super::{components, AuthActivity, Context, FileTransferProtocol, Id, InputMask}; use crate::filetransfer::params::ProtocolParams; use crate::filetransfer::FileTransferParams; -use crate::ui::components::bookmark_list::{BookmarkList, BookmarkListPropsBuilder}; use crate::utils::ui::draw_area_in; -// Ext -use tui_realm_stdlib::{ - Input, InputPropsBuilder, Label, LabelPropsBuilder, List, ListPropsBuilder, Paragraph, - ParagraphPropsBuilder, Radio, RadioPropsBuilder, Span, SpanPropsBuilder, Textarea, - TextareaPropsBuilder, -}; -use tuirealm::tui::{ - layout::{Constraint, Direction, Layout}, - style::Color, - widgets::{BorderType, Borders, Clear}, -}; -use tuirealm::{ - props::{Alignment, InputType, PropsBuilder, TableBuilder, TextSpan}, - Msg, Payload, Value, -}; + +use std::str::FromStr; +use tuirealm::tui::layout::{Constraint, Direction, Layout}; +use tuirealm::tui::widgets::Clear; +use tuirealm::{State, StateValue, Sub, SubClause, SubEventClause}; impl AuthActivity { /// ### init @@ -53,110 +42,40 @@ impl AuthActivity { /// Initialize view, mounting all startup components inside the view pub(super) fn init(&mut self) { let key_color = self.theme().misc_keys; - let addr_color = self.theme().auth_address; - let protocol_color = self.theme().auth_protocol; - let port_color = self.theme().auth_port; - let username_color = self.theme().auth_username; - let password_color = self.theme().auth_password; - let bookmarks_color = self.theme().auth_bookmarks; - let recents_color = self.theme().auth_recents; + let info_color = self.theme().misc_info_dialog; // Headers - self.view.mount( - super::COMPONENT_TEXT_H1, - Box::new(Label::new( - LabelPropsBuilder::default() - .bold() - .italic() - .with_text(String::from("$ termscp")) - .build(), - )), - ); - self.view.mount( - super::COMPONENT_TEXT_H2, - Box::new(Label::new( - LabelPropsBuilder::default() - .bold() - .italic() - .with_text(format!("$ version {}", env!("CARGO_PKG_VERSION"))) - .build(), - )), - ); + assert!(self + .app + .mount(Id::Title, Box::new(components::Title::default()), vec![]) + .is_ok()); + assert!(self + .app + .mount( + Id::Subtitle, + Box::new(components::Subtitle::default()), + vec![] + ) + .is_ok()); // Footer - self.view.mount( - super::COMPONENT_TEXT_FOOTER, - Box::new(Span::new( - SpanPropsBuilder::default() - .with_spans(vec![ - TextSpan::new("Press ").bold(), - TextSpan::new("").bold().fg(key_color), - TextSpan::new(" to show keybindings; ").bold(), - TextSpan::new("").bold().fg(key_color), - TextSpan::new(" to enter setup").bold(), - ]) - .build(), - )), - ); + assert!(self + .app + .mount( + Id::HelpText, + Box::new(components::HelpText::new(key_color)), + vec![] + ) + .is_ok()); // Get default protocol let default_protocol: FileTransferProtocol = self.context().config().get_default_protocol(); - // Protocol - self.mount_radio( - super::COMPONENT_RADIO_PROTOCOL, - "Protocol", - &["SFTP", "SCP", "FTP", "FTPS", "AWS S3"], - Self::protocol_enum_to_opt(default_protocol), - protocol_color, - ); - // Address - self.mount_input( - super::COMPONENT_INPUT_ADDR, - "Remote host", - addr_color, - InputType::Text, - ); - // Port - self.mount_input_ex( - super::COMPONENT_INPUT_PORT, - "Port number", - port_color, - InputType::Number, - Some(5), - Some(Self::get_default_port_for_protocol(default_protocol).to_string()), - ); - // Username - self.mount_input( - super::COMPONENT_INPUT_USERNAME, - "Username", - username_color, - InputType::Text, - ); - // Password - self.mount_input( - super::COMPONENT_INPUT_PASSWORD, - "Password", - password_color, - InputType::Password, - ); - // Bucket - self.mount_input( - super::COMPONENT_INPUT_S3_BUCKET, - "Bucket name", - addr_color, - InputType::Text, - ); - // Region - self.mount_input( - super::COMPONENT_INPUT_S3_REGION, - "Region", - port_color, - InputType::Text, - ); - // Profile - self.mount_input( - super::COMPONENT_INPUT_S3_PROFILE, - "Profile", - username_color, - InputType::Text, - ); + // Auth form + self.mount_protocol(default_protocol); + self.mount_address(""); + self.mount_port(Self::get_default_port_for_protocol(default_protocol)); + self.mount_username(""); + self.mount_password(""); + self.mount_s3_bucket(""); + self.mount_s3_profile(""); + self.mount_s3_region(""); // Version notice if let Some(version) = self .context() @@ -164,57 +83,34 @@ impl AuthActivity { .get_string(super::STORE_KEY_LATEST_VERSION) { let version: String = version.to_string(); - self.view.mount( - super::COMPONENT_TEXT_NEW_VERSION, - Box::new(Span::new( - SpanPropsBuilder::default() - .with_foreground(Color::Yellow) - .with_spans(vec![ - TextSpan::from("termscp "), - TextSpan::new(version.as_str()).underlined().bold(), - TextSpan::from(" is NOW available! Install update and view release notes with "), - ]) - .build(), - )), - ); + assert!(self + .app + .mount( + Id::NewVersionDisclaimer, + Box::new(components::NewVersionDisclaimer::new( + version.as_str(), + info_color + )), + vec![] + ) + .is_ok()); } - // Bookmarks - self.view.mount( - super::COMPONENT_BOOKMARKS_LIST, - Box::new(BookmarkList::new( - BookmarkListPropsBuilder::default() - .with_background(bookmarks_color) - .with_foreground(Color::Black) - .with_borders(Borders::ALL, BorderType::Plain, bookmarks_color) - .with_title("Bookmarks", Alignment::Left) - .build(), - )), - ); - // Recents - self.view.mount( - super::COMPONENT_RECENTS_LIST, - Box::new(BookmarkList::new( - BookmarkListPropsBuilder::default() - .with_background(recents_color) - .with_foreground(Color::Black) - .with_borders(Borders::ALL, BorderType::Plain, recents_color) - .with_title("Recent connections", Alignment::Left) - .build(), - )), - ); // Load bookmarks - let _ = self.view_bookmarks(); - let _ = self.view_recent_connections(); + self.view_bookmarks(); + self.view_recent_connections(); + // Global listener + self.init_global_listener(); // Active protocol - self.view.active(super::COMPONENT_RADIO_PROTOCOL); + assert!(self.app.active(&Id::Protocol).is_ok()); } /// ### view /// /// Display view on canvas pub(super) fn view(&mut self) { + self.redraw = false; let mut ctx: Context = self.context.take().unwrap(); - let _ = ctx.terminal().draw(|f| { + let _ = ctx.terminal().raw_mut().draw(|f| { // Check window size let height: u16 = f.size().height; self.check_minimum_window_size(height); @@ -278,159 +174,101 @@ impl AuthActivity { .split(chunks[1]); // Render // Auth chunks - self.view - .render(super::COMPONENT_TEXT_H1, f, auth_chunks[0]); - self.view - .render(super::COMPONENT_TEXT_H2, f, auth_chunks[1]); - self.view - .render(super::COMPONENT_TEXT_NEW_VERSION, f, auth_chunks[2]); - self.view - .render(super::COMPONENT_RADIO_PROTOCOL, f, auth_chunks[3]); + self.app.view(&Id::Title, f, auth_chunks[0]); + self.app.view(&Id::Subtitle, f, auth_chunks[1]); + self.app.view(&Id::NewVersionDisclaimer, f, auth_chunks[2]); + self.app.view(&Id::Protocol, f, auth_chunks[3]); // Render input mask match self.input_mask() { InputMask::AwsS3 => { - self.view - .render(super::COMPONENT_INPUT_S3_BUCKET, f, input_mask[0]); - self.view - .render(super::COMPONENT_INPUT_S3_REGION, f, input_mask[1]); - self.view - .render(super::COMPONENT_INPUT_S3_PROFILE, f, input_mask[2]); + self.app.view(&Id::S3Bucket, f, input_mask[0]); + self.app.view(&Id::S3Region, f, input_mask[1]); + self.app.view(&Id::S3Profile, f, input_mask[2]); } InputMask::Generic => { - self.view - .render(super::COMPONENT_INPUT_ADDR, f, input_mask[0]); - self.view - .render(super::COMPONENT_INPUT_PORT, f, input_mask[1]); - self.view - .render(super::COMPONENT_INPUT_USERNAME, f, input_mask[2]); - self.view - .render(super::COMPONENT_INPUT_PASSWORD, f, input_mask[3]); + self.app.view(&Id::Address, f, input_mask[0]); + self.app.view(&Id::Port, f, input_mask[1]); + self.app.view(&Id::Username, f, input_mask[2]); + self.app.view(&Id::Password, f, input_mask[3]); } } - self.view - .render(super::COMPONENT_TEXT_FOOTER, f, auth_chunks[5]); + self.app.view(&Id::HelpText, f, auth_chunks[5]); // Bookmark chunks - self.view - .render(super::COMPONENT_BOOKMARKS_LIST, f, bookmark_chunks[0]); - self.view - .render(super::COMPONENT_RECENTS_LIST, f, bookmark_chunks[1]); + self.app.view(&Id::BookmarksList, f, bookmark_chunks[0]); + self.app.view(&Id::RecentsList, f, bookmark_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_TEXT_INFO) { - 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_INFO, f, popup); - } - } - if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_WAIT) { - if props.visible { - let popup = draw_area_in(f.size(), 50, 10); - f.render_widget(Clear, popup); - // make popup - self.view.render(super::COMPONENT_TEXT_WAIT, f, popup); - } - } - if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_SIZE_ERR) { - if props.visible { - let popup = draw_area_in(f.size(), 80, 20); - f.render_widget(Clear, popup); - // make popup - self.view.render(super::COMPONENT_TEXT_SIZE_ERR, 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(), 30, 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_RADIO_BOOKMARK_DEL_BOOKMARK) - { - 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_BOOKMARK_DEL_BOOKMARK, f, popup); - } - } - if let Some(props) = self - .view - .get_props(super::COMPONENT_RADIO_BOOKMARK_DEL_RECENT) - { - 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_BOOKMARK_DEL_RECENT, f, popup); - } - } - if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_NEW_VERSION_NOTES) { - if props.visible { - // make popup - let popup = draw_area_in(f.size(), 90, 85); - f.render_widget(Clear, popup); - let popup_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Percentage(90), // Notes - Constraint::Length(3), // Install radio - ] - .as_ref(), - ) - .split(popup); - self.view - .render(super::COMPONENT_TEXT_NEW_VERSION_NOTES, f, popup_chunks[0]); - self.view - .render(super::COMPONENT_RADIO_INSTALL_UPDATE, f, popup_chunks[1]); - } - } - 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_BOOKMARK_SAVE_PWD) - { - if props.visible { - // make popup - let popup = draw_area_in(f.size(), 20, 20); - f.render_widget(Clear, popup); - let popup_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Length(3), // Input form - Constraint::Length(2), // Yes/No - ] - .as_ref(), - ) - .split(popup); - self.view - .render(super::COMPONENT_INPUT_BOOKMARK_NAME, f, popup_chunks[0]); - self.view - .render(super::COMPONENT_RADIO_BOOKMARK_SAVE_PWD, f, popup_chunks[1]); - } + if self.app.mounted(&Id::ErrorPopup) { + let popup = draw_area_in(f.size(), 50, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::ErrorPopup, f, popup); + } else if self.app.mounted(&Id::InfoPopup) { + let popup = draw_area_in(f.size(), 50, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::InfoPopup, f, popup); + } else if self.app.mounted(&Id::WaitPopup) { + let popup = draw_area_in(f.size(), 50, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::WaitPopup, f, popup); + } else if self.app.mounted(&Id::WindowSizeError) { + let popup = draw_area_in(f.size(), 80, 20); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::WindowSizeError, f, popup); + } else if self.app.mounted(&Id::QuitPopup) { + // make popup + let popup = draw_area_in(f.size(), 30, 10); + f.render_widget(Clear, popup); + self.app.view(&Id::QuitPopup, f, popup); + } else if self.app.mounted(&Id::DeleteBookmarkPopup) { + // make popup + let popup = draw_area_in(f.size(), 30, 10); + f.render_widget(Clear, popup); + self.app.view(&Id::DeleteBookmarkPopup, f, popup); + } else if self.app.mounted(&Id::DeleteRecentPopup) { + // make popup + let popup = draw_area_in(f.size(), 30, 10); + f.render_widget(Clear, popup); + self.app.view(&Id::DeleteRecentPopup, f, popup); + } else if self.app.mounted(&Id::NewVersionChangelog) { + // make popup + let popup = draw_area_in(f.size(), 90, 85); + f.render_widget(Clear, popup); + let popup_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Percentage(90), // Notes + Constraint::Length(3), // Install radio + ] + .as_ref(), + ) + .split(popup); + self.app.view(&Id::NewVersionChangelog, f, popup_chunks[0]); + self.app.view(&Id::InstallUpdatePopup, f, popup_chunks[1]); + } else if self.app.mounted(&Id::Keybindings) { + // make popup + let popup = draw_area_in(f.size(), 50, 70); + f.render_widget(Clear, popup); + self.app.view(&Id::Keybindings, f, popup); + } else if self.app.mounted(&Id::BookmarkSavePassword) { + // make popup + let popup = draw_area_in(f.size(), 20, 20); + f.render_widget(Clear, popup); + let popup_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Length(3), // Input form + Constraint::Length(2), // Yes/No + ] + .as_ref(), + ) + .split(popup); + self.app.view(&Id::BookmarkName, f, popup_chunks[0]); + self.app.view(&Id::BookmarkSavePassword, f, popup_chunks[1]); } }); self.context = Some(ctx); @@ -441,7 +279,7 @@ impl AuthActivity { /// ### view_bookmarks /// /// Make text span from bookmarks - pub(super) fn view_bookmarks(&mut self) -> Option<(String, Msg)> { + pub(super) fn view_bookmarks(&mut self) { let bookmarks: Vec = self .bookmarks_list .iter() @@ -456,24 +294,21 @@ impl AuthActivity { ) }) .collect(); - match self.view.get_props(super::COMPONENT_BOOKMARKS_LIST) { - None => None, - Some(props) => { - let msg = self.view.update( - super::COMPONENT_BOOKMARKS_LIST, - BookmarkListPropsBuilder::from(props) - .with_bookmarks(bookmarks) - .build(), - ); - msg - } - } + let bookmarks_color = self.theme().auth_bookmarks; + assert!(self + .app + .remount( + Id::BookmarksList, + Box::new(components::BookmarksList::new(&bookmarks, bookmarks_color)), + vec![] + ) + .is_ok()); } /// ### view_recent_connections /// /// View recent connections - pub(super) fn view_recent_connections(&mut self) -> Option<(String, Msg)> { + pub(super) fn view_recent_connections(&mut self) { let bookmarks: Vec = self .recents_list .iter() @@ -487,18 +322,15 @@ impl AuthActivity { ) }) .collect(); - match self.view.get_props(super::COMPONENT_RECENTS_LIST) { - None => None, - Some(props) => { - let msg = self.view.update( - super::COMPONENT_RECENTS_LIST, - BookmarkListPropsBuilder::from(props) - .with_bookmarks(bookmarks) - .build(), - ); - msg - } - } + let recents_color = self.theme().auth_recents; + assert!(self + .app + .remount( + Id::RecentsList, + Box::new(components::RecentsList::new(&bookmarks, recents_color)), + vec![] + ) + .is_ok()); } // -- mount @@ -508,14 +340,22 @@ impl AuthActivity { /// Mount error box pub(super) fn mount_error>(&mut self, text: S) { let err_color = self.theme().misc_error_dialog; - self.mount_text_dialog(super::COMPONENT_TEXT_ERROR, text.as_ref(), err_color); + assert!(self + .app + .remount( + Id::ErrorPopup, + Box::new(components::ErrorPopup::new(text, err_color)), + vec![] + ) + .is_ok()); + assert!(self.app.active(&Id::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::ErrorPopup); } /// ### mount_info @@ -523,14 +363,22 @@ impl AuthActivity { /// Mount info box pub(super) fn mount_info>(&mut self, text: S) { let color = self.theme().misc_info_dialog; - self.mount_text_dialog(super::COMPONENT_TEXT_INFO, text.as_ref(), color); + assert!(self + .app + .remount( + Id::InfoPopup, + Box::new(components::InfoPopup::new(text, color)), + vec![] + ) + .is_ok()); + assert!(self.app.active(&Id::InfoPopup).is_ok()); } /// ### umount_info /// /// Umount info message pub(super) fn umount_info(&mut self) { - self.view.umount(super::COMPONENT_TEXT_INFO); + let _ = self.app.umount(&Id::InfoPopup); } /// ### mount_error @@ -538,14 +386,22 @@ impl AuthActivity { /// Mount wait box pub(super) fn mount_wait(&mut self, text: &str) { let wait_color = self.theme().misc_info_dialog; - self.mount_text_dialog(super::COMPONENT_TEXT_WAIT, text, wait_color); + assert!(self + .app + .remount( + Id::WaitPopup, + Box::new(components::WaitPopup::new(text, wait_color)), + vec![] + ) + .is_ok()); + assert!(self.app.active(&Id::WaitPopup).is_ok()); } /// ### umount_wait /// /// Umount wait message pub(super) fn umount_wait(&mut self) { - self.view.umount(super::COMPONENT_TEXT_WAIT); + let _ = self.app.umount(&Id::WaitPopup); } /// ### mount_size_err @@ -554,18 +410,22 @@ impl AuthActivity { pub(super) fn mount_size_err(&mut self) { // Mount let err_color = self.theme().misc_error_dialog; - self.mount_text_dialog( - super::COMPONENT_TEXT_SIZE_ERR, - "termscp requires at least 24 lines of height to run", - err_color, - ); + assert!(self + .app + .remount( + Id::WindowSizeError, + Box::new(components::WindowSizeError::new(err_color)), + vec![] + ) + .is_ok()); + assert!(self.app.active(&Id::WindowSizeError).is_ok()); } /// ### umount_size_err /// /// Umount error size error pub(super) fn umount_size_err(&mut self) { - self.view.umount(super::COMPONENT_TEXT_SIZE_ERR); + let _ = self.app.umount(&Id::WindowSizeError); } /// ### mount_quit @@ -574,20 +434,22 @@ impl AuthActivity { pub(super) fn mount_quit(&mut self) { // Protocol let quit_color = self.theme().misc_quit_dialog; - self.mount_radio_dialog( - super::COMPONENT_RADIO_QUIT, - "Quit termscp?", - &["Yes", "No"], - 0, - quit_color, - ); + assert!(self + .app + .remount( + Id::QuitPopup, + Box::new(components::QuitPopup::new(quit_color)), + vec![] + ) + .is_ok()); + assert!(self.app.active(&Id::QuitPopup).is_ok()); } /// ### umount_quit /// /// Umount quit popup pub(super) fn umount_quit(&mut self) { - self.view.umount(super::COMPONENT_RADIO_QUIT); + let _ = self.app.umount(&Id::QuitPopup); } /// ### mount_bookmark_del_dialog @@ -595,21 +457,22 @@ impl AuthActivity { /// Mount bookmark delete dialog pub(super) fn mount_bookmark_del_dialog(&mut self) { let warn_color = self.theme().misc_warn_dialog; - self.mount_radio_dialog( - super::COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK, - "Delete bookmark?", - &["Yes", "No"], - 1, - warn_color, - ); + assert!(self + .app + .remount( + Id::DeleteBookmarkPopup, + Box::new(components::DeleteBookmarkPopup::new(warn_color)), + vec![] + ) + .is_ok()); + assert!(self.app.active(&Id::DeleteBookmarkPopup).is_ok()); } /// ### umount_bookmark_del_dialog /// /// umount delete bookmark dialog pub(super) fn umount_bookmark_del_dialog(&mut self) { - self.view - .umount(super::COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK); + let _ = self.app.umount(&Id::DeleteBookmarkPopup); } /// ### mount_bookmark_del_dialog @@ -617,20 +480,22 @@ impl AuthActivity { /// Mount recent delete dialog pub(super) fn mount_recent_del_dialog(&mut self) { let warn_color = self.theme().misc_warn_dialog; - self.mount_radio_dialog( - super::COMPONENT_RADIO_BOOKMARK_DEL_RECENT, - "Delete bookmark?", - &["Yes", "No"], - 1, - warn_color, - ); + assert!(self + .app + .remount( + Id::DeleteRecentPopup, + Box::new(components::DeleteRecentPopup::new(warn_color)), + vec![] + ) + .is_ok()); + assert!(self.app.active(&Id::DeleteRecentPopup).is_ok()); } /// ### umount_recent_del_dialog /// /// umount delete recent dialog pub(super) fn umount_recent_del_dialog(&mut self) { - self.view.umount(super::COMPONENT_RADIO_BOOKMARK_DEL_RECENT); + let _ = self.app.umount(&Id::DeleteRecentPopup); } /// ### mount_bookmark_save_dialog @@ -639,102 +504,56 @@ impl AuthActivity { pub(super) fn mount_bookmark_save_dialog(&mut self) { let save_color = self.theme().misc_save_dialog; let warn_color = self.theme().misc_warn_dialog; - self.view.mount( - super::COMPONENT_INPUT_BOOKMARK_NAME, - Box::new(Input::new( - InputPropsBuilder::default() - .with_foreground(save_color) - .with_label("Save bookmark as…", Alignment::Center) - .with_borders( - Borders::TOP | Borders::RIGHT | Borders::LEFT, - BorderType::Rounded, - Color::Reset, - ) - .build(), - )), - ); - self.view.mount( - super::COMPONENT_RADIO_BOOKMARK_SAVE_PWD, - Box::new(Radio::new( - RadioPropsBuilder::default() - .with_color(warn_color) - .with_borders( - Borders::BOTTOM | Borders::RIGHT | Borders::LEFT, - BorderType::Rounded, - Color::Reset, - ) - .with_title("Save password?", Alignment::Center) - .with_options(&[String::from("Yes"), String::from("No")]) - .rewind(true) - .build(), - )), - ); + assert!(self + .app + .remount( + Id::BookmarkName, + Box::new(components::BookmarkName::new(save_color)), + vec![] + ) + .is_ok()); + assert!(self + .app + .remount( + Id::BookmarkSavePassword, + Box::new(components::BookmarkSavePassword::new(warn_color)), + vec![] + ) + .is_ok()); // Give focus to input bookmark name - self.view.active(super::COMPONENT_INPUT_BOOKMARK_NAME); + assert!(self.app.active(&Id::BookmarkName).is_ok()); } /// ### umount_bookmark_save_dialog /// /// Umount bookmark save dialog pub(super) fn umount_bookmark_save_dialog(&mut self) { - self.view.umount(super::COMPONENT_RADIO_BOOKMARK_SAVE_PWD); - self.view.umount(super::COMPONENT_INPUT_BOOKMARK_NAME); + let _ = self.app.umount(&Id::BookmarkName); + let _ = self.app.umount(&Id::BookmarkSavePassword); } - /// ### mount_help + /// ### mount_keybindings /// - /// Mount help - pub(super) fn mount_help(&mut self) { + /// Mount keybindings + pub(super) fn mount_keybindings(&mut self) { let key_color = self.theme().misc_keys; - 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) - .scrollable(true) - .bold() - .with_title("Help", Alignment::Center) - .with_rows( - TableBuilder::default() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Quit termscp")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Switch from form and bookmarks")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Switch bookmark tab")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Move up/down in current tab")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Connect/Load bookmark")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Delete selected bookmark")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Enter setup")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Save bookmark")) - .build(), - ) - .build(), - )), - ); + assert!(self + .app + .remount( + Id::Keybindings, + Box::new(components::Keybindings::new(key_color)), + vec![] + ) + .is_ok()); // Active help - self.view.active(super::COMPONENT_TEXT_HELP); + assert!(self.app.active(&Id::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::Keybindings); } /// ### mount_release_notes @@ -744,26 +563,24 @@ impl AuthActivity { if let Some(ctx) = self.context.as_ref() { if let Some(release_notes) = ctx.store().get_string(super::STORE_KEY_RELEASE_NOTES) { // make spans - let spans: Vec = release_notes.lines().map(TextSpan::from).collect(); let info_color = self.theme().misc_info_dialog; - self.view.mount( - super::COMPONENT_TEXT_NEW_VERSION_NOTES, - Box::new(Textarea::new( - TextareaPropsBuilder::default() - .with_borders(Borders::ALL, BorderType::Rounded, info_color) - .with_title("Release notes", Alignment::Center) - .with_texts(spans) - .build(), - )), - ); - // Mount install popup - self.mount_radio_dialog( - super::COMPONENT_RADIO_INSTALL_UPDATE, - "Install new version?", - &["Yes", "No"], - 0, - info_color, - ); + assert!(self + .app + .remount( + Id::NewVersionChangelog, + Box::new(components::ReleaseNotes::new(release_notes, info_color)), + vec![] + ) + .is_ok()); + assert!(self + .app + .remount( + Id::InstallUpdatePopup, + Box::new(components::InstallUpdatePopup::new(info_color)), + vec![] + ) + .is_ok()); + assert!(self.app.active(&Id::InstallUpdatePopup).is_ok()); } } } @@ -772,17 +589,108 @@ impl AuthActivity { /// /// Umount release notes text area pub(super) fn umount_release_notes(&mut self) { - self.view.umount(super::COMPONENT_TEXT_NEW_VERSION_NOTES); - self.view.umount(super::COMPONENT_RADIO_INSTALL_UPDATE); + let _ = self.app.umount(&Id::NewVersionChangelog); + let _ = self.app.umount(&Id::InstallUpdatePopup); } - /// ### get_protocol - /// - /// Get protocol from view - pub(super) fn get_protocol(&self) -> FileTransferProtocol { - self.get_input_protocol() + pub(super) fn mount_protocol(&mut self, protocol: FileTransferProtocol) { + let protocol_color = self.theme().auth_protocol; + assert!(self + .app + .remount( + Id::Protocol, + Box::new(components::ProtocolRadio::new(protocol, protocol_color)), + vec![] + ) + .is_ok()); } + pub(super) fn mount_address(&mut self, address: &str) { + let addr_color = self.theme().auth_address; + assert!(self + .app + .remount( + Id::Address, + Box::new(components::InputAddress::new(address, addr_color)), + vec![] + ) + .is_ok()); + } + + pub(super) fn mount_port(&mut self, port: u16) { + let port_color = self.theme().auth_port; + assert!(self + .app + .remount( + Id::Port, + Box::new(components::InputPort::new(port, port_color)), + vec![] + ) + .is_ok()); + } + + pub(crate) fn mount_username(&mut self, username: &str) { + let username_color = self.theme().auth_username; + assert!(self + .app + .remount( + Id::Username, + Box::new(components::InputUsername::new(username, username_color)), + vec![] + ) + .is_ok()); + } + + pub(crate) fn mount_password(&mut self, password: &str) { + let password_color = self.theme().auth_password; + assert!(self + .app + .remount( + Id::Password, + Box::new(components::InputPassword::new(password, password_color)), + vec![] + ) + .is_ok()); + } + + pub(super) fn mount_s3_bucket(&mut self, bucket: &str) { + let addr_color = self.theme().auth_address; + assert!(self + .app + .remount( + Id::S3Bucket, + Box::new(components::InputS3Bucket::new(bucket, addr_color)), + vec![] + ) + .is_ok()); + } + + pub(super) fn mount_s3_region(&mut self, region: &str) { + let port_color = self.theme().auth_port; + assert!(self + .app + .remount( + Id::S3Region, + Box::new(components::InputS3Region::new(region, port_color)), + vec![] + ) + .is_ok()); + } + + pub(crate) fn mount_s3_profile(&mut self, profile: &str) { + let username_color = self.theme().auth_username; + assert!(self + .app + .remount( + Id::S3Profile, + Box::new(components::InputS3Profile::new(profile, username_color)), + vec![] + ) + .is_ok()); + } + + // -- query + /// ### get_generic_params /// /// Collect input values from view @@ -805,67 +713,77 @@ impl AuthActivity { } pub(super) fn get_input_addr(&self) -> String { - match self.view.get_state(super::COMPONENT_INPUT_ADDR) { - Some(Payload::One(Value::Str(x))) => x, + match self.app.state(&Id::Address) { + Ok(State::One(StateValue::String(x))) => x, _ => String::new(), } } pub(super) fn get_input_port(&self) -> u16 { - match self.view.get_state(super::COMPONENT_INPUT_PORT) { - Some(Payload::One(Value::Usize(x))) => match x > 65535 { - true => 0, - false => x as u16, + match self.app.state(&Id::Port) { + Ok(State::One(StateValue::String(x))) => match u16::from_str(x.as_str()) { + Ok(v) => v, + _ => 0, }, _ => 0, } } - pub(super) fn get_input_protocol(&self) -> FileTransferProtocol { - match self.view.get_state(super::COMPONENT_RADIO_PROTOCOL) { - Some(Payload::One(Value::Usize(x))) => Self::protocol_opt_to_enum(x), - _ => FileTransferProtocol::Sftp, - } - } - pub(super) fn get_input_username(&self) -> String { - match self.view.get_state(super::COMPONENT_INPUT_USERNAME) { - Some(Payload::One(Value::Str(x))) => x, + match self.app.state(&Id::Username) { + Ok(State::One(StateValue::String(x))) => x, _ => String::new(), } } pub(super) fn get_input_password(&self) -> String { - match self.view.get_state(super::COMPONENT_INPUT_PASSWORD) { - Some(Payload::One(Value::Str(x))) => x, + match self.app.state(&Id::Password) { + Ok(State::One(StateValue::String(x))) => x, _ => String::new(), } } pub(super) fn get_input_s3_bucket(&self) -> String { - match self.view.get_state(super::COMPONENT_INPUT_S3_BUCKET) { - Some(Payload::One(Value::Str(x))) => x, + match self.app.state(&Id::S3Bucket) { + Ok(State::One(StateValue::String(x))) => x, _ => String::new(), } } pub(super) fn get_input_s3_region(&self) -> String { - match self.view.get_state(super::COMPONENT_INPUT_S3_REGION) { - Some(Payload::One(Value::Str(x))) => x, + match self.app.state(&Id::S3Region) { + Ok(State::One(StateValue::String(x))) => x, _ => String::new(), } } pub(super) fn get_input_s3_profile(&self) -> Option { - match self.view.get_state(super::COMPONENT_INPUT_S3_PROFILE) { - Some(Payload::One(Value::Str(x))) => match x.is_empty() { - true => None, - false => Some(x), - }, + match self.app.state(&Id::S3Profile) { + Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), _ => None, } } + /// ### get_new_bookmark + /// + /// Get new bookmark params + pub(super) fn get_new_bookmark(&self) -> (String, bool) { + let name = match self.app.state(&Id::BookmarkName) { + Ok(State::One(StateValue::String(name))) => name, + _ => String::default(), + }; + if matches!( + self.app.state(&Id::BookmarkSavePassword), + Ok(State::One(StateValue::Usize(0))) + ) { + (name, true) + } else { + (name, false) + } + } + + // -- len + /// ### input_mask_size /// /// Returns the input mask size based on current input mask @@ -876,6 +794,8 @@ impl AuthActivity { } } + // -- fmt + /// ### fmt_bookmark /// /// Format bookmark to display on ui @@ -913,94 +833,95 @@ impl AuthActivity { } } - // -- mount helpers + fn init_global_listener(&mut self) { + use tuirealm::event::{Key, KeyEvent, KeyModifiers}; + assert!(self + .app + .mount( + Id::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::Char('c'), + modifiers: KeyModifiers::CONTROL, + }), + 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()); + } - 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(), + /// ### 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 { + SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::ErrorPopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::InfoPopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::Keybindings, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::DeleteBookmarkPopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::DeleteRecentPopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::InstallUpdatePopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::BookmarkSavePassword, + )))), + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::WaitPopup, + )))), + )), + )), + )), + )), + )), )), - ); - // 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, - value: Option, - ) { - 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); - } - if let Some(value) = value { - props.with_value(value); - } - self.view.mount(id, Box::new(Input::new(props.build()))); + ) } } diff --git a/src/ui/activities/filetransfer/actions/edit.rs b/src/ui/activities/filetransfer/actions/edit.rs index eacb24b..c6545a5 100644 --- a/src/ui/activities/filetransfer/actions/edit.rs +++ b/src/ui/activities/filetransfer/actions/edit.rs @@ -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(()) } diff --git a/src/ui/activities/filetransfer/actions/mod.rs b/src/ui/activities/filetransfer/actions/mod.rs index e384951..2e4fad8 100644 --- a/src/ui/activities/filetransfer/actions/mod.rs +++ b/src/ui/activities/filetransfer/actions/mod.rs @@ -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 = files .iter() .map(|x| match x { - Value::Usize(v) => *v, + StateValue::Usize(v) => *v, _ => 0, }) .collect(); diff --git a/src/ui/activities/filetransfer/actions/open.rs b/src/ui/activities/filetransfer/actions/open.rs index c3de73a..86f056e 100644 --- a/src/ui/activities/filetransfer/actions/open.rs +++ b/src/ui/activities/filetransfer/actions/open.rs @@ -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); + } } } } diff --git a/src/ui/activities/filetransfer/components/log.rs b/src/ui/activities/filetransfer/components/log.rs new file mode 100644 index 0000000..2ddb935 --- /dev/null +++ b/src/ui/activities/filetransfer/components/log.rs @@ -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 = 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 { + 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 for Log { + fn on(&mut self, ev: Event) -> Option { + 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 + } +} diff --git a/src/ui/activities/filetransfer/components/mod.rs b/src/ui/activities/filetransfer/components/mod.rs new file mode 100644 index 0000000..cd79cd7 --- /dev/null +++ b/src/ui/activities/filetransfer/components/mod.rs @@ -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 for GlobalListener { + fn on(&mut self, ev: Event) -> Option { + 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, + } + } +} diff --git a/src/ui/activities/filetransfer/components/popups.rs b/src/ui/activities/filetransfer/components/popups.rs new file mode 100644 index 0000000..a124013 --- /dev/null +++ b/src/ui/activities/filetransfer/components/popups.rs @@ -0,0 +1,1689 @@ +//! ## Popups +//! +//! popups 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::super::Browser; +use super::{Msg, TransferMsg, UiMsg}; +use crate::fs::explorer::FileSorting; +use crate::fs::FsEntry; +use crate::utils::fmt::fmt_time; + +use bytesize::ByteSize; +use std::path::PathBuf; + +use tui_realm_stdlib::{Input, List, Paragraph, ProgressBar, Radio, Span}; +use tuirealm::command::{Cmd, CmdResult, Direction, Position}; +use tuirealm::event::{Key, KeyEvent, KeyModifiers}; +use tuirealm::props::{ + Alignment, BorderSides, BorderType, Borders, Color, InputType, Style, TableBuilder, TextSpan, +}; +use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; +#[cfg(target_family = "unix")] +use users::{get_group_by_gid, get_user_by_uid}; + +#[derive(MockComponent)] +pub struct CopyPopup { + component: Input, +} + +impl CopyPopup { + pub fn new(color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .input_type(InputType::Text) + .placeholder( + "destination", + Style::default().fg(Color::Rgb(128, 128, 128)), + ) + .title("Copy file(s) to…", Alignment::Center), + } + } +} + +impl Component for CopyPopup { + fn on(&mut self, ev: Event) -> Option { + 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, .. + }) => match self.state() { + State::One(StateValue::String(i)) => { + Some(Msg::Transfer(TransferMsg::CopyFileTo(i))) + } + _ => Some(Msg::None), + }, + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseCopyPopup)) + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct DeletePopup { + component: Radio, +} + +impl DeletePopup { + pub fn new(color: Color) -> Self { + Self { + component: Radio::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .choices(&["Yes", "No"]) + .value(1) + .title("Delete file(s)?", Alignment::Center), + } + } +} + +impl Component for DeletePopup { + fn on(&mut self, ev: Event) -> Option { + 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::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseDeletePopup)) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => { + if matches!( + self.perform(Cmd::Submit), + CmdResult::Submit(State::One(StateValue::Usize(0))) + ) { + Some(Msg::Transfer(TransferMsg::DeleteFile)) + } else { + Some(Msg::Ui(UiMsg::CloseDeletePopup)) + } + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct DisconnectPopup { + component: Radio, +} + +impl DisconnectPopup { + pub fn new(color: Color) -> Self { + Self { + component: Radio::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .choices(&["Yes", "No"]) + .title("Are you sure you want to disconnect?", Alignment::Center), + } + } +} + +impl Component for DisconnectPopup { + fn on(&mut self, ev: Event) -> Option { + 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::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseDisconnectPopup)) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => { + if matches!( + self.perform(Cmd::Submit), + CmdResult::Submit(State::One(StateValue::Usize(0))) + ) { + Some(Msg::Ui(UiMsg::Disconnect)) + } else { + Some(Msg::Ui(UiMsg::CloseDisconnectPopup)) + } + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct ErrorPopup { + component: Paragraph, +} + +impl ErrorPopup { + pub fn new>(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 for ErrorPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Esc | Key::Enter, + .. + }) => Some(Msg::Ui(UiMsg::CloseErrorPopup)), + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct ExecPopup { + component: Input, +} + +impl ExecPopup { + pub fn new(color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .input_type(InputType::Text) + .placeholder("ps a", Style::default().fg(Color::Rgb(128, 128, 128))) + .title("Execute command", Alignment::Center), + } + } +} + +impl Component for ExecPopup { + fn on(&mut self, ev: Event) -> Option { + 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, .. + }) => match self.state() { + State::One(StateValue::String(i)) => { + Some(Msg::Transfer(TransferMsg::ExecuteCmd(i))) + } + _ => Some(Msg::None), + }, + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseExecPopup)) + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct FatalPopup { + component: Paragraph, +} + +impl FatalPopup { + pub fn new>(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 for FatalPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Esc | Key::Enter, + .. + }) => Some(Msg::Ui(UiMsg::CloseFatalPopup)), + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct FileInfoPopup { + component: List, +} + +impl FileInfoPopup { + pub fn new(file: &FsEntry) -> Self { + let mut texts: TableBuilder = TableBuilder::default(); + // Abs path + let real_path: Option = { + let real_file: FsEntry = file.get_realfile(); + match real_file.get_abs_path() != file.get_abs_path() { + true => Some(real_file.get_abs_path()), + false => None, + } + }; + let path: String = match real_path { + Some(symlink) => format!("{} -> {}", file.get_abs_path().display(), symlink.display()), + None => format!("{}", file.get_abs_path().display()), + }; + // Make texts + texts + .add_col(TextSpan::from("Path: ")) + .add_col(TextSpan::new(path.as_str()).fg(Color::Yellow)); + if let Some(filetype) = file.get_ftype() { + texts + .add_row() + .add_col(TextSpan::from("File type: ")) + .add_col(TextSpan::new(filetype.as_str()).fg(Color::LightGreen)); + } + let (bsize, size): (ByteSize, usize) = (ByteSize(file.get_size() as u64), file.get_size()); + texts + .add_row() + .add_col(TextSpan::from("Size: ")) + .add_col(TextSpan::new(format!("{} ({})", bsize, size).as_str()).fg(Color::Cyan)); + let ctime: String = fmt_time(file.get_creation_time(), "%b %d %Y %H:%M:%S"); + let atime: String = fmt_time(file.get_last_access_time(), "%b %d %Y %H:%M:%S"); + let mtime: String = fmt_time(file.get_creation_time(), "%b %d %Y %H:%M:%S"); + texts + .add_row() + .add_col(TextSpan::from("Creation time: ")) + .add_col(TextSpan::new(ctime.as_str()).fg(Color::LightGreen)); + texts + .add_row() + .add_col(TextSpan::from("Last modified time: ")) + .add_col(TextSpan::new(mtime.as_str()).fg(Color::LightBlue)); + texts + .add_row() + .add_col(TextSpan::from("Last access time: ")) + .add_col(TextSpan::new(atime.as_str()).fg(Color::LightRed)); + // User + #[cfg(target_family = "unix")] + let username: String = match file.get_user() { + Some(uid) => match get_user_by_uid(uid) { + Some(user) => user.name().to_string_lossy().to_string(), + None => uid.to_string(), + }, + None => String::from("0"), + }; + #[cfg(target_os = "windows")] + let username: String = format!("{}", file.get_user().unwrap_or(0)); + // Group + #[cfg(target_family = "unix")] + let group: String = match file.get_group() { + Some(gid) => match get_group_by_gid(gid) { + Some(group) => group.name().to_string_lossy().to_string(), + None => gid.to_string(), + }, + None => String::from("0"), + }; + #[cfg(target_os = "windows")] + let group: String = format!("{}", file.get_group().unwrap_or(0)); + texts + .add_row() + .add_col(TextSpan::from("User: ")) + .add_col(TextSpan::new(username.as_str()).fg(Color::LightYellow)); + texts + .add_row() + .add_col(TextSpan::from("Group: ")) + .add_col(TextSpan::new(group.as_str()).fg(Color::Blue)); + Self { + component: List::default() + .borders(Borders::default().modifiers(BorderType::Rounded)) + .scroll(false) + .title(file.get_name(), Alignment::Left) + .rows(texts.build()), + } + } +} + +impl Component for FileInfoPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Esc | Key::Enter, + .. + }) => Some(Msg::Ui(UiMsg::CloseFileInfoPopup)), + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct FindPopup { + component: Input, +} + +impl FindPopup { + pub fn new(color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .input_type(InputType::Text) + .placeholder( + "Search files by name", + Style::default().fg(Color::Rgb(128, 128, 128)), + ) + .title("*.txt", Alignment::Center), + } + } +} + +impl Component for FindPopup { + fn on(&mut self, ev: Event) -> Option { + 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, .. + }) => match self.state() { + State::One(StateValue::String(i)) => { + Some(Msg::Transfer(TransferMsg::SearchFile(i))) + } + _ => Some(Msg::None), + }, + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseFindPopup)) + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct GoToPopup { + component: Input, +} + +impl GoToPopup { + pub fn new(color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .input_type(InputType::Text) + .placeholder( + "/foo/bar/buzz", + Style::default().fg(Color::Rgb(128, 128, 128)), + ) + .title("Go to…", Alignment::Center), + } + } +} + +impl Component for GoToPopup { + fn on(&mut self, ev: Event) -> Option { + 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, .. + }) => match self.state() { + State::One(StateValue::String(i)) => Some(Msg::Transfer(TransferMsg::GoTo(i))), + _ => Some(Msg::None), + }, + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseGotoPopup)) + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct KeybindingsPopup { + component: List, +} + +impl KeybindingsPopup { + pub fn new(key_color: Color) -> Self { + Self { + component: List::default() + .borders(Borders::default().modifiers(BorderType::Rounded)) + .scroll(true) + .step(8) + .highlighted_str("? ") + .title("Keybindings", Alignment::Center) + .rows( + TableBuilder::default() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Disconnect")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from( + " Switch between explorer and logs", + )) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Go to previous directory")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Change explorer tab")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Move up/down in list")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Enter directory")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Upload/Download file")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Toggle hidden files")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Change file sorting mode")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Copy")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Make directory")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Search files")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Go to path")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Show help")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Show info about selected file")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Reload directory content")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Select file")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Create new file")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from( + " Open text file with preferred editor", + )) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Quit termscp")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Rename file")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Save file as")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Go to parent directory")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from( + " Open file with default application for file type", + )) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from( + " Open file with specified application", + )) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Execute shell command")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Toggle synchronized browsing")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Delete selected file")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Select all files")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Interrupt file transfer")) + .build(), + ), + } + } +} + +impl Component for KeybindingsPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Esc | Key::Enter, + .. + }) => Some(Msg::Ui(UiMsg::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 MkdirPopup { + component: Input, +} + +impl MkdirPopup { + pub fn new(color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .input_type(InputType::Text) + .placeholder( + "New directory name", + Style::default().fg(Color::Rgb(128, 128, 128)), + ) + .title("directory-name", Alignment::Center), + } + } +} + +impl Component for MkdirPopup { + fn on(&mut self, ev: Event) -> Option { + 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, .. + }) => match self.state() { + State::One(StateValue::String(i)) => Some(Msg::Transfer(TransferMsg::Mkdir(i))), + _ => Some(Msg::None), + }, + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseMkdirPopup)) + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct NewfilePopup { + component: Input, +} + +impl NewfilePopup { + pub fn new(color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .input_type(InputType::Text) + .placeholder( + "New file name", + Style::default().fg(Color::Rgb(128, 128, 128)), + ) + .title("file.txt", Alignment::Center), + } + } +} + +impl Component for NewfilePopup { + fn on(&mut self, ev: Event) -> Option { + 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, .. + }) => match self.state() { + State::One(StateValue::String(i)) => Some(Msg::Transfer(TransferMsg::NewFile(i))), + _ => Some(Msg::None), + }, + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseNewFilePopup)) + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct OpenWithPopup { + component: Input, +} + +impl OpenWithPopup { + pub fn new(color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .input_type(InputType::Text) + .placeholder( + "Open file with…", + Style::default().fg(Color::Rgb(128, 128, 128)), + ) + .title("vscode", Alignment::Center), + } + } +} + +impl Component for OpenWithPopup { + fn on(&mut self, ev: Event) -> Option { + 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, .. + }) => match self.state() { + State::One(StateValue::String(i)) => { + Some(Msg::Transfer(TransferMsg::OpenFileWith(i))) + } + _ => Some(Msg::None), + }, + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseOpenWithPopup)) + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct ProgressBarFull { + component: ProgressBar, +} + +impl ProgressBarFull { + pub fn new>(prog: f64, label: S, title: S, color: Color) -> Self { + Self { + component: ProgressBar::default() + .borders( + Borders::default() + .modifiers(BorderType::Rounded) + .sides(BorderSides::TOP | BorderSides::LEFT | BorderSides::RIGHT), + ) + .foreground(color) + .label(label) + .progress(prog) + .title(title, Alignment::Center), + } + } +} + +impl Component for ProgressBarFull { + fn on(&mut self, ev: Event) -> Option { + if matches!( + ev, + Event::Keyboard(KeyEvent { + code: Key::Char('c'), + modifiers: KeyModifiers::CONTROL + }) + ) { + Some(Msg::Transfer(TransferMsg::AbortTransfer)) + } else { + None + } + } +} + +#[derive(MockComponent)] +pub struct ProgressBarPartial { + component: ProgressBar, +} + +impl ProgressBarPartial { + pub fn new>(prog: f64, label: S, title: S, color: Color) -> Self { + Self { + component: ProgressBar::default() + .borders( + Borders::default() + .modifiers(BorderType::Rounded) + .sides(BorderSides::BOTTOM | BorderSides::LEFT | BorderSides::RIGHT), + ) + .foreground(color) + .label(label) + .progress(prog) + .title(title, Alignment::Center), + } + } +} + +impl Component for ProgressBarPartial { + fn on(&mut self, ev: Event) -> Option { + if matches!( + ev, + Event::Keyboard(KeyEvent { + code: Key::Char('c'), + modifiers: KeyModifiers::CONTROL + }) + ) { + Some(Msg::Transfer(TransferMsg::AbortTransfer)) + } else { + None + } + } +} + +#[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) + .choices(&["Yes", "No"]) + .title("Are you sure you want to quit termscp?", Alignment::Center), + } + } +} + +impl Component for QuitPopup { + fn on(&mut self, ev: Event) -> Option { + 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::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseQuitPopup)) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => { + if matches!( + self.perform(Cmd::Submit), + CmdResult::Submit(State::One(StateValue::Usize(0))) + ) { + Some(Msg::Ui(UiMsg::Quit)) + } else { + Some(Msg::Ui(UiMsg::CloseQuitPopup)) + } + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct RenamePopup { + component: Input, +} + +impl RenamePopup { + pub fn new(color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .input_type(InputType::Text) + .placeholder( + "/foo/bar/buzz.txt", + Style::default().fg(Color::Rgb(128, 128, 128)), + ) + .title("Move file(s) to…", Alignment::Center), + } + } +} + +impl Component for RenamePopup { + fn on(&mut self, ev: Event) -> Option { + 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, .. + }) => match self.state() { + State::One(StateValue::String(i)) => { + Some(Msg::Transfer(TransferMsg::RenameFile(i))) + } + _ => Some(Msg::None), + }, + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseRenamePopup)) + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct ReplacePopup { + component: Radio, +} + +impl ReplacePopup { + pub fn new(filename: Option<&str>, color: Color) -> Self { + let text = match filename { + Some(f) => format!(r#"File "{}" already exists. Overwrite file?"#, f), + None => "Overwrite files?".to_string(), + }; + Self { + component: Radio::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .choices(&["Yes", "No"]) + .title(text, Alignment::Center), + } + } +} + +impl Component for ReplacePopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { + Some(Msg::Ui(UiMsg::ReplacePopupTabbed)) + } + 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::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseReplacePopups)) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => { + if matches!( + self.perform(Cmd::Submit), + CmdResult::Submit(State::One(StateValue::Usize(0))) + ) { + Some(Msg::Transfer(TransferMsg::TransferPendingFile)) + } else { + Some(Msg::Ui(UiMsg::CloseReplacePopups)) + } + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct ReplacingFilesListPopup { + component: List, +} + +impl ReplacingFilesListPopup { + pub fn new(files: &[&str], color: Color) -> Self { + Self { + component: List::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .scroll(true) + .step(4) + .highlighted_color(color) + .highlighted_str("➤ ") + .title( + "The following files are going to be replaced", + Alignment::Center, + ) + .rows(files.iter().map(|x| vec![TextSpan::from(x)]).collect()), + } + } +} + +impl Component for ReplacingFilesListPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseReplacePopups)) + } + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { + Some(Msg::Ui(UiMsg::ReplacePopupTabbed)) + } + 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 SaveAsPopup { + component: Input, +} + +impl SaveAsPopup { + pub fn new(color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .input_type(InputType::Text) + .placeholder( + "/foo/bar/buzz.txt", + Style::default().fg(Color::Rgb(128, 128, 128)), + ) + .title("Save as…", Alignment::Center), + } + } +} + +impl Component for SaveAsPopup { + fn on(&mut self, ev: Event) -> Option { + 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, .. + }) => match self.state() { + State::One(StateValue::String(i)) => { + Some(Msg::Transfer(TransferMsg::SaveFileAs(i))) + } + _ => Some(Msg::None), + }, + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseSaveAsPopup)) + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct SortingPopup { + component: Radio, +} + +impl SortingPopup { + pub fn new(value: FileSorting, color: Color) -> Self { + Self { + component: Radio::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .choices(&["Name", "Modify time", "Creation time", "Size"]) + .title("Sort files by…", Alignment::Center) + .value(match value { + FileSorting::CreationTime => 2, + FileSorting::ModifyTime => 1, + FileSorting::Name => 0, + FileSorting::Size => 3, + }), + } + } +} + +impl Component for SortingPopup { + fn on(&mut self, ev: Event) -> Option { + 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::Esc | Key::Enter, + .. + }) => return Some(Msg::Ui(UiMsg::CloseFileSortingPopup)), + _ => return None, + }; + if let CmdResult::Changed(State::One(StateValue::Usize(i))) = result { + Some(Msg::Ui(UiMsg::ChangeFileSorting(match i { + 0 => FileSorting::Name, + 1 => FileSorting::ModifyTime, + 2 => FileSorting::CreationTime, + 3 => FileSorting::Size, + _ => FileSorting::Name, + }))) + } else { + Some(Msg::None) + } + } +} + +#[derive(MockComponent)] +pub struct StatusBarLocal { + component: Span, +} + +impl StatusBarLocal { + pub fn new(browser: &Browser, sorting_color: Color, hidden_color: Color) -> Self { + let file_sorting = file_sorting_label(browser.local().file_sorting); + let hidden_files = hidden_files_label(browser.local().hidden_files_visible()); + Self { + component: Span::default().spans(&[ + TextSpan::new("File sorting: ").fg(sorting_color), + TextSpan::new(file_sorting).fg(sorting_color).reversed(), + TextSpan::new(" Hidden files: ").fg(hidden_color), + TextSpan::new(hidden_files).fg(hidden_color).reversed(), + ]), + } + } +} + +impl Component for StatusBarLocal { + fn on(&mut self, _ev: Event) -> Option { + None + } +} + +#[derive(MockComponent)] +pub struct StatusBarRemote { + component: Span, +} + +impl StatusBarRemote { + pub fn new( + browser: &Browser, + sorting_color: Color, + hidden_color: Color, + sync_color: Color, + ) -> Self { + let file_sorting = file_sorting_label(browser.remote().file_sorting); + let hidden_files = hidden_files_label(browser.remote().hidden_files_visible()); + let sync_browsing = match browser.sync_browsing { + true => "ON ", + false => "OFF", + }; + Self { + component: Span::default().spans(&[ + TextSpan::new("File sorting: ").fg(sorting_color), + TextSpan::new(file_sorting).fg(sorting_color).reversed(), + TextSpan::new(" Hidden files: ").fg(hidden_color), + TextSpan::new(hidden_files).fg(hidden_color).reversed(), + TextSpan::new(" Sync browsing: ").fg(sync_color), + TextSpan::new(sync_browsing).fg(sync_color).reversed(), + ]), + } + } +} + +impl Component for StatusBarRemote { + fn on(&mut self, _ev: Event) -> Option { + None + } +} + +fn file_sorting_label(sorting: FileSorting) -> &'static str { + match sorting { + FileSorting::CreationTime => "By creation time", + FileSorting::ModifyTime => "By modify time", + FileSorting::Name => "By name", + FileSorting::Size => "By size", + } +} + +fn hidden_files_label(visible: bool) -> &'static str { + match visible { + true => "Show", + false => "Hide", + } +} + +#[derive(MockComponent)] +pub struct WaitPopup { + component: Paragraph, +} + +impl WaitPopup { + pub fn new>(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 for WaitPopup { + fn on(&mut self, _ev: Event) -> Option { + None + } +} diff --git a/src/ui/activities/filetransfer/components/transfer/file_list.rs b/src/ui/activities/filetransfer/components/transfer/file_list.rs new file mode 100644 index 0000000..27ca9c0 --- /dev/null +++ b/src/ui/activities/filetransfer/components/transfer/file_list.rs @@ -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, // 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 { + 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>(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 = match self + .props + .get(Attribute::Content) + .map(|x| x.unwrap_table()) + { + Some(table) => table + .iter() + .enumerate() + .map(|(num, row)| { + let columns: Vec = 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 { + 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, + } + } +} diff --git a/src/ui/activities/filetransfer/components/transfer/mod.rs b/src/ui/activities/filetransfer/components/transfer/mod.rs new file mode 100644 index 0000000..26a6ff0 --- /dev/null +++ b/src/ui/activities/filetransfer/components/transfer/mod.rs @@ -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>(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 for ExplorerFind { + fn on(&mut self, ev: Event) -> Option { + 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>(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 for ExplorerLocal { + fn on(&mut self, ev: Event) -> Option { + 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>(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 for ExplorerRemote { + fn on(&mut self, ev: Event) -> Option { + 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, + } + } +} diff --git a/src/ui/activities/filetransfer/lib/browser.rs b/src/ui/activities/filetransfer/lib/browser.rs index 396f371..ecd948e 100644 --- a/src/ui/activities/filetransfer/lib/browser.rs +++ b/src/ui/activities/filetransfer/lib/browser.rs @@ -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, diff --git a/src/ui/activities/filetransfer/misc.rs b/src/ui/activities/filetransfer/misc.rs index 1ae4c67..808e069 100644 --- a/src/ui/activities/filetransfer/misc.rs +++ b/src/ui/activities/filetransfer/misc.rs @@ -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> = 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> = 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> = 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(), + } + } } diff --git a/src/ui/activities/filetransfer/mod.rs b/src/ui/activities/filetransfer/mod.rs index 3a9f6ab..905da93 100644 --- a/src/ui/activities/filetransfer/mod.rs +++ b/src/ui/activities/filetransfer/mod.rs @@ -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, // Exit reason - context: Option, // Context holder - view: View, // View - host: Localhost, // Localhost - client: Box, // File transfer client - browser: Browser, // Browser - log_records: VecDeque, // Log records - transfer: TransferStates, // Transfer states - cache: Option, // Temporary directory where to store stuff + /// Exit reason + exit_reason: Option, + /// Context holder + context: Option, + /// Tui-realm application + app: Application, + /// Whether should redraw UI + redraw: bool, + /// Localhost bridge + host: Localhost, + /// Remote host + client: Box, + /// Browser + browser: Browser, + /// Current log lines + log_records: VecDeque, + transfer: TransferStates, + /// Temporary directory where to store temporary stuff + cache: Option, } 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() } } diff --git a/src/ui/activities/filetransfer/session.rs b/src/ui/activities/filetransfer/session.rs index 09cdd9f..4d90943 100644 --- a/src/ui/activities/filetransfer/session.rs +++ b/src/ui/activities/filetransfer/session.rs @@ -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()); } diff --git a/src/ui/activities/filetransfer/update.rs b/src/ui/activities/filetransfer/update.rs index da003bf..af5e54b 100644 --- a/src/ui/activities/filetransfer/update.rs +++ b/src/ui/activities/filetransfer/update.rs @@ -29,1002 +29,459 @@ use super::{ actions::SelectedEntry, browser::{FileExplorerTab, FoundExplorerTab}, - FileTransferActivity, LogLevel, TransferOpts, COMPONENT_EXPLORER_FIND, - COMPONENT_EXPLORER_LOCAL, COMPONENT_EXPLORER_REMOTE, COMPONENT_INPUT_COPY, - COMPONENT_INPUT_EXEC, COMPONENT_INPUT_FIND, COMPONENT_INPUT_GOTO, COMPONENT_INPUT_MKDIR, - COMPONENT_INPUT_NEWFILE, COMPONENT_INPUT_OPEN_WITH, COMPONENT_INPUT_RENAME, - COMPONENT_INPUT_SAVEAS, COMPONENT_LIST_FILEINFO, COMPONENT_LIST_REPLACING_FILES, - COMPONENT_LOG_BOX, COMPONENT_PROGRESS_BAR_FULL, COMPONENT_PROGRESS_BAR_PARTIAL, - COMPONENT_RADIO_DELETE, COMPONENT_RADIO_DISCONNECT, COMPONENT_RADIO_QUIT, - COMPONENT_RADIO_REPLACE, COMPONENT_RADIO_SORTING, COMPONENT_TEXT_ERROR, COMPONENT_TEXT_FATAL, - COMPONENT_TEXT_HELP, + ExitReason, FileTransferActivity, Id, Msg, TransferMsg, TransferOpts, UiMsg, }; -use crate::fs::explorer::FileSorting; use crate::fs::FsEntry; -use crate::ui::components::{file_list::FileListPropsBuilder, logbox::LogboxPropsBuilder}; -use crate::ui::keymap::*; -use crate::utils::fmt::fmt_path_elide_ex; // externals -use tui_realm_stdlib::ProgressBarPropsBuilder; use tuirealm::{ - props::{Alignment, PropsBuilder, TableBuilder, TextSpan}, - tui::style::Color, - Msg, Payload, Update, Value, + props::{AttrValue, Attribute}, + State, StateValue, Update, }; -impl Update for FileTransferActivity { - // -- update - - /// ### 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 { - // -- local tab - (COMPONENT_EXPLORER_LOCAL, key) - if key == &MSG_KEY_RIGHT - && matches!(self.browser.found_tab(), Some(FoundExplorerTab::Remote)) => - { - // Go to find explorer - self.view.active(COMPONENT_EXPLORER_FIND); - self.browser.change_tab(FileExplorerTab::FindRemote); - None - } - (COMPONENT_EXPLORER_LOCAL, key) if key == &MSG_KEY_RIGHT => { - // Change tab - self.view.active(COMPONENT_EXPLORER_REMOTE); - self.browser.change_tab(FileExplorerTab::Remote); - None - } - (COMPONENT_EXPLORER_LOCAL, key) if key == &MSG_KEY_BACKSPACE => { - // Go to previous directory - self.action_go_to_previous_local_dir(false); - if self.browser.sync_browsing { - let _ = self.update_remote_filelist(); - } - // Reload file list component - self.update_local_filelist() - } - (COMPONENT_EXPLORER_LOCAL, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => { - // Match selected file - let mut entry: Option = None; - if let Some(e) = self.local().get(*idx) { - entry = Some(e.clone()); - } - if let Some(entry) = entry { - if self.action_submit_local(entry) { - // Update file list if sync - if self.browser.sync_browsing { - let _ = self.update_remote_filelist(); - } - self.update_local_filelist() - } else { - None - } - } else { - None - } - } - (COMPONENT_EXPLORER_LOCAL, key) if key == &MSG_KEY_SPACE => { - self.action_local_send(); - self.update_remote_filelist() - } - (COMPONENT_EXPLORER_LOCAL, key) if key == &MSG_KEY_CHAR_A => { - // Toggle hidden files - self.local_mut().toggle_hidden_files(); - // Update status bar - self.refresh_local_status_bar(); - // Reload file list component - self.update_local_filelist() - } - (COMPONENT_EXPLORER_LOCAL, key) if key == &MSG_KEY_CHAR_I => { - if let SelectedEntry::One(file) = self.get_local_selected_entries() { - self.mount_file_info(&file); - } - None - } - (COMPONENT_EXPLORER_LOCAL, key) if key == &MSG_KEY_CHAR_L => { - // Reload directory - self.reload_local_dir(); - // Reload file list component - self.update_local_filelist() - } - (COMPONENT_EXPLORER_LOCAL, key) if key == &MSG_KEY_CHAR_O => { - self.action_edit_local_file(); - // Reload file list component - self.update_local_filelist() - } - (COMPONENT_EXPLORER_LOCAL, key) if key == &MSG_KEY_CHAR_U => { - self.action_go_to_local_upper_dir(false); - if self.browser.sync_browsing { - let _ = self.update_remote_filelist(); - } - // Reload file list component - self.update_local_filelist() - } - // -- remote tab - (COMPONENT_EXPLORER_REMOTE, key) - if key == &MSG_KEY_LEFT - && matches!(self.browser.found_tab(), Some(FoundExplorerTab::Local)) => - { - // Go to find explorer - self.view.active(COMPONENT_EXPLORER_FIND); - self.browser.change_tab(FileExplorerTab::FindLocal); - None - } - (COMPONENT_EXPLORER_REMOTE, key) if key == &MSG_KEY_LEFT => { - // Change tab - self.view.active(COMPONENT_EXPLORER_LOCAL); - self.browser.change_tab(FileExplorerTab::Local); - None - } - (COMPONENT_EXPLORER_REMOTE, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => { - // Match selected file - let mut entry: Option = None; - if let Some(e) = self.remote().get(*idx) { - entry = Some(e.clone()); - } - if let Some(entry) = entry { - if self.action_submit_remote(entry) { - // Update file list if sync - if self.browser.sync_browsing { - let _ = self.update_local_filelist(); - } - self.update_remote_filelist() - } else { - None - } - } else { - None - } - } - (COMPONENT_EXPLORER_REMOTE, key) if key == &MSG_KEY_SPACE => { - self.action_remote_recv(); - self.update_local_filelist() - } - (COMPONENT_EXPLORER_REMOTE, key) if key == &MSG_KEY_BACKSPACE => { - // Go to previous directory - self.action_go_to_previous_remote_dir(false); - // If sync is enabled update local too - if self.browser.sync_browsing { - let _ = self.update_local_filelist(); - } - // Reload file list component - self.update_remote_filelist() - } - (COMPONENT_EXPLORER_REMOTE, key) if key == &MSG_KEY_CHAR_A => { - // Toggle hidden files - self.remote_mut().toggle_hidden_files(); - // Update status bar - self.refresh_remote_status_bar(); - // Reload file list component - self.update_remote_filelist() - } - (COMPONENT_EXPLORER_REMOTE, key) if key == &MSG_KEY_CHAR_I => { - if let SelectedEntry::One(file) = self.get_remote_selected_entries() { - self.mount_file_info(&file); - } - None - } - (COMPONENT_EXPLORER_REMOTE, key) if key == &MSG_KEY_CHAR_L => { - // Reload directory - self.reload_remote_dir(); - // Reload file list component - self.update_remote_filelist() - } - (COMPONENT_EXPLORER_REMOTE, key) if key == &MSG_KEY_CHAR_O => { - // Edit file - self.action_edit_remote_file(); - // Reload file list component - self.update_remote_filelist() - } - (COMPONENT_EXPLORER_REMOTE, key) if key == &MSG_KEY_CHAR_U => { - self.action_go_to_remote_upper_dir(false); - if self.browser.sync_browsing { - let _ = self.update_local_filelist(); - } - // Reload file list component - self.update_remote_filelist() - } - // -- common explorer keys - (COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key) - if key == &MSG_KEY_CHAR_B => - { - // Show sorting file - self.mount_file_sorting(); - None - } - (COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key) - if key == &MSG_KEY_CHAR_C => - { - self.mount_copy(); - None - } - (COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key) - if key == &MSG_KEY_CHAR_D => - { - self.mount_mkdir(); - None - } - (COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key) - if key == &MSG_KEY_CHAR_F => - { - self.mount_find_input(); - None - } - (COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key) - if key == &MSG_KEY_CHAR_G => - { - self.mount_goto(); - None - } - (COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key) - if key == &MSG_KEY_CHAR_H => - { - self.mount_help(); - None - } - (COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key) - if key == &MSG_KEY_CHAR_N => - { - self.mount_newfile(); - None - } - (COMPONENT_EXPLORER_LOCAL, key) - | (COMPONENT_EXPLORER_REMOTE, key) - | (COMPONENT_LOG_BOX, key) - if key == &MSG_KEY_CHAR_Q => - { - self.mount_quit(); - None - } - (COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key) - if key == &MSG_KEY_CHAR_R => - { - // Mount rename - self.mount_rename(); - None - } - (COMPONENT_EXPLORER_LOCAL, key) - | (COMPONENT_EXPLORER_REMOTE, key) - | (COMPONENT_EXPLORER_FIND, key) - if key == &MSG_KEY_CHAR_S => - { - // Mount save as - self.mount_saveas(); - None - } - (COMPONENT_EXPLORER_LOCAL, key) - | (COMPONENT_EXPLORER_REMOTE, key) - | (COMPONENT_EXPLORER_FIND, key) - if key == &MSG_KEY_CHAR_V => - { - // View - match self.browser.tab() { - FileExplorerTab::Local => self.action_open_local(), - FileExplorerTab::Remote => self.action_open_remote(), - FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => { - self.action_find_open() - } - } - None - } - (COMPONENT_EXPLORER_LOCAL, key) - | (COMPONENT_EXPLORER_REMOTE, key) - | (COMPONENT_EXPLORER_FIND, key) - if key == &MSG_KEY_CHAR_W => - { - // Open with - self.mount_openwith(); - None - } - (COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key) - if key == &MSG_KEY_CHAR_X => - { - // Mount exec - self.mount_exec(); - None - } - (COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key) - if key == &MSG_KEY_CHAR_Y => - { - // Toggle browser sync - self.browser.toggle_sync_browsing(); - // Update status bar - self.refresh_remote_status_bar(); - None - } - (COMPONENT_EXPLORER_LOCAL, key) - | (COMPONENT_EXPLORER_REMOTE, key) - | (COMPONENT_LOG_BOX, key) - if key == &MSG_KEY_ESC => - { - self.mount_disconnect(); - None - } - (COMPONENT_EXPLORER_LOCAL, key) - | (COMPONENT_EXPLORER_REMOTE, key) - | (COMPONENT_EXPLORER_FIND, key) - if key == &MSG_KEY_CHAR_E || key == &MSG_KEY_DEL => - { - self.mount_radio_delete(); - None - } - // -- find result explorer - (COMPONENT_EXPLORER_FIND, key) - if key == &MSG_KEY_RIGHT - && matches!(self.browser.tab(), FileExplorerTab::FindLocal) => - { - // Active remote explorer - self.view.active(COMPONENT_EXPLORER_REMOTE); - self.browser.change_tab(FileExplorerTab::Remote); - None - } - (COMPONENT_EXPLORER_FIND, key) - if key == &MSG_KEY_LEFT - && matches!(self.browser.tab(), FileExplorerTab::FindRemote) => - { - // Active local explorer - self.view.active(COMPONENT_EXPLORER_LOCAL); - self.browser.change_tab(FileExplorerTab::Local); - None - } - (COMPONENT_EXPLORER_FIND, key) if key == &MSG_KEY_ESC => { - // Umount find - self.umount_find(); - // Finalize find - self.finalize_find(); - None - } - (COMPONENT_EXPLORER_FIND, Msg::OnSubmit(_)) => { - // Find changedir - self.action_find_changedir(); - // Umount find - self.umount_find(); - // Finalize find - self.finalize_find(); - // Reload files - match self.browser.tab() { - FileExplorerTab::Local => self.update_local_filelist(), - FileExplorerTab::Remote => self.update_remote_filelist(), - _ => None, - } - } - (COMPONENT_EXPLORER_FIND, key) if key == &MSG_KEY_SPACE => { - // Get entry - self.action_find_transfer(TransferOpts::default()); - // Reload files - match self.browser.tab() { - // NOTE: swapped by purpose - FileExplorerTab::FindLocal => self.update_remote_filelist(), - FileExplorerTab::FindRemote => self.update_local_filelist(), - _ => None, - } - } - // -- switch to log - (COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key) - if key == &MSG_KEY_TAB => - { - self.view.active(COMPONENT_LOG_BOX); // Active log box - None - } - // -- Log box - (COMPONENT_LOG_BOX, key) if key == &MSG_KEY_TAB => { - self.view.blur(); // Blur log box - None - } - // -- copy popup - (COMPONENT_INPUT_COPY, key) if key == &MSG_KEY_ESC => { - self.umount_copy(); - None - } - (COMPONENT_INPUT_COPY, Msg::OnSubmit(Payload::One(Value::Str(input)))) => { - // Copy file - self.umount_copy(); - self.mount_blocking_wait("Copying file(s)…"); - match self.browser.tab() { - FileExplorerTab::Local => self.action_local_copy(input.to_string()), - FileExplorerTab::Remote => self.action_remote_copy(input.to_string()), - _ => panic!("Found tab doesn't support COPY"), - } - self.umount_wait(); - // Reload files - match self.browser.tab() { - FileExplorerTab::Local => self.update_local_filelist(), - FileExplorerTab::Remote => self.update_remote_filelist(), - _ => None, - } - } - (COMPONENT_INPUT_COPY, _) => None, - // -- exec popup - (COMPONENT_INPUT_EXEC, key) if key == &MSG_KEY_ESC => { - self.umount_exec(); - None - } - (COMPONENT_INPUT_EXEC, Msg::OnSubmit(Payload::One(Value::Str(input)))) => { - // Exex command - self.umount_exec(); - self.mount_blocking_wait(format!("Executing '{}'…", input).as_str()); - match self.browser.tab() { - FileExplorerTab::Local => self.action_local_exec(input.to_string()), - FileExplorerTab::Remote => self.action_remote_exec(input.to_string()), - _ => panic!("Found tab doesn't support EXEC"), - } - self.umount_wait(); - // Reload files - match self.browser.tab() { - FileExplorerTab::Local => self.update_local_filelist(), - FileExplorerTab::Remote => self.update_remote_filelist(), - _ => None, - } - } - (COMPONENT_INPUT_EXEC, _) => None, - // -- find popup - (COMPONENT_INPUT_FIND, key) if key == &MSG_KEY_ESC => { - self.umount_find_input(); - None - } - (COMPONENT_INPUT_FIND, Msg::OnSubmit(Payload::One(Value::Str(input)))) => { - self.umount_find_input(); - // Mount wait - self.mount_blocking_wait(format!(r#"Searching for "{}"…"#, input).as_str()); - // Find - let res: Result, String> = match self.browser.tab() { - FileExplorerTab::Local => self.action_local_find(input.to_string()), - FileExplorerTab::Remote => self.action_remote_find(input.to_string()), - _ => panic!("Trying to search for files, while already in a find result"), - }; - // Umount wait - self.umount_wait(); - // Match result - match res { - Err(err) => { - // Mount error - self.mount_error(err.as_str()); - } - Ok(files) if files.is_empty() => { - // If no file has been found notify user - self.mount_info( - format!(r#"Could not find any file matching "{}""#, input).as_str(), - ); - } - Ok(files) => { - // Get wrkdir - let wrkdir = match self.browser.tab() { - FileExplorerTab::Local => self.local().wrkdir.clone(), - _ => self.remote().wrkdir.clone(), - }; - // Create explorer and load files - self.browser.set_found( - match self.browser.tab() { - FileExplorerTab::Local => FoundExplorerTab::Local, - _ => FoundExplorerTab::Remote, - }, - files, - wrkdir.as_path(), - ); - // Mount result widget - self.mount_find(input); - self.update_find_list(); - // Initialize tab - self.browser.change_tab(match self.browser.tab() { - FileExplorerTab::Local => FileExplorerTab::FindLocal, - FileExplorerTab::Remote => FileExplorerTab::FindRemote, - _ => FileExplorerTab::FindLocal, - }); - } - } - None - } - // -- goto popup - (COMPONENT_INPUT_GOTO, key) if key == &MSG_KEY_ESC => { - self.umount_goto(); - None - } - (COMPONENT_INPUT_GOTO, Msg::OnSubmit(Payload::One(Value::Str(input)))) => { - match self.browser.tab() { - FileExplorerTab::Local => { - self.action_change_local_dir(input.to_string(), false) - } - FileExplorerTab::Remote => { - self.action_change_remote_dir(input.to_string(), false) - } - _ => panic!("Found tab doesn't support GOTO"), - } - // Umount - self.umount_goto(); - // Reload files if sync - if self.browser.sync_browsing { - match self.browser.tab() { - FileExplorerTab::Remote => self.update_local_filelist(), - FileExplorerTab::Local => self.update_remote_filelist(), - _ => None, - }; - } - // Reload files - match self.browser.tab() { - FileExplorerTab::Local => self.update_local_filelist(), - FileExplorerTab::Remote => self.update_remote_filelist(), - _ => None, - } - } - (COMPONENT_INPUT_GOTO, _) => None, - // -- make directory - (COMPONENT_INPUT_MKDIR, key) if key == &MSG_KEY_ESC => { - self.umount_mkdir(); - None - } - (COMPONENT_INPUT_MKDIR, Msg::OnSubmit(Payload::One(Value::Str(input)))) => { - match self.browser.tab() { - FileExplorerTab::Local => self.action_local_mkdir(input.to_string()), - FileExplorerTab::Remote => self.action_remote_mkdir(input.to_string()), - _ => panic!("Found tab doesn't support MKDIR"), - } - self.umount_mkdir(); - // Reload files - match self.browser.tab() { - FileExplorerTab::Local => self.update_local_filelist(), - FileExplorerTab::Remote => self.update_remote_filelist(), - _ => None, - } - } - (COMPONENT_INPUT_MKDIR, _) => None, - // -- new file - (COMPONENT_INPUT_NEWFILE, key) if key == &MSG_KEY_ESC => { - self.umount_newfile(); - None - } - (COMPONENT_INPUT_NEWFILE, Msg::OnSubmit(Payload::One(Value::Str(input)))) => { - match self.browser.tab() { - FileExplorerTab::Local => self.action_local_newfile(input.to_string()), - FileExplorerTab::Remote => self.action_remote_newfile(input.to_string()), - _ => panic!("Found tab doesn't support NEWFILE"), - } - self.umount_newfile(); - // Reload files - match self.browser.tab() { - FileExplorerTab::Local => self.update_local_filelist(), - FileExplorerTab::Remote => self.update_remote_filelist(), - _ => None, - } - } - (COMPONENT_INPUT_NEWFILE, _) => None, - // -- open with - (COMPONENT_INPUT_OPEN_WITH, key) if key == &MSG_KEY_ESC => { - self.umount_openwith(); - None - } - (COMPONENT_INPUT_OPEN_WITH, Msg::OnSubmit(Payload::One(Value::Str(input)))) => { - match self.browser.tab() { - FileExplorerTab::Local => self.action_local_open_with(input), - FileExplorerTab::Remote => self.action_remote_open_with(input), - FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => { - self.action_find_open_with(input) - } - } - self.umount_openwith(); - None - } - (COMPONENT_INPUT_OPEN_WITH, _) => None, - // -- rename - (COMPONENT_INPUT_RENAME, key) if key == &MSG_KEY_ESC => { - self.umount_rename(); - None - } - (COMPONENT_INPUT_RENAME, Msg::OnSubmit(Payload::One(Value::Str(input)))) => { - self.umount_rename(); - self.mount_blocking_wait("Moving file(s)…"); - match self.browser.tab() { - FileExplorerTab::Local => self.action_local_rename(input.to_string()), - FileExplorerTab::Remote => self.action_remote_rename(input.to_string()), - _ => panic!("Found tab doesn't support RENAME"), - } - self.umount_wait(); - // Reload files - match self.browser.tab() { - FileExplorerTab::Local => self.update_local_filelist(), - FileExplorerTab::Remote => self.update_remote_filelist(), - _ => None, - } - } - (COMPONENT_INPUT_RENAME, _) => None, - // -- save as - (COMPONENT_INPUT_SAVEAS, key) if key == &MSG_KEY_ESC => { - self.umount_saveas(); - None - } - (COMPONENT_INPUT_SAVEAS, Msg::OnSubmit(Payload::One(Value::Str(input)))) => { - match self.browser.tab() { - FileExplorerTab::Local => self.action_local_saveas(input.to_string()), - FileExplorerTab::Remote => self.action_remote_saveas(input.to_string()), - FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => { - // Get entry - self.action_find_transfer(TransferOpts::default().save_as(Some(input))); - } - } - self.umount_saveas(); - // Reload files - match self.browser.tab() { - // NOTE: Swapped is intentional - FileExplorerTab::Local => self.update_remote_filelist(), - FileExplorerTab::Remote => self.update_local_filelist(), - FileExplorerTab::FindLocal => self.update_remote_filelist(), - FileExplorerTab::FindRemote => self.update_local_filelist(), - } - } - (COMPONENT_INPUT_SAVEAS, _) => None, - // -- fileinfo - (COMPONENT_LIST_FILEINFO, key) | (COMPONENT_LIST_FILEINFO, key) - if key == &MSG_KEY_ENTER || key == &MSG_KEY_ESC => - { - self.umount_file_info(); - None - } - (COMPONENT_LIST_FILEINFO, _) => None, - // -- delete - (COMPONENT_RADIO_DELETE, key) - if key == &MSG_KEY_ESC - || key == &Msg::OnSubmit(Payload::One(Value::Usize(1))) => - { - self.umount_radio_delete(); - None - } - (COMPONENT_RADIO_DELETE, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => { - // Choice is 'YES' - self.umount_radio_delete(); - self.mount_blocking_wait("Removing file(s)…"); - match self.browser.tab() { - FileExplorerTab::Local => self.action_local_delete(), - FileExplorerTab::Remote => self.action_remote_delete(), - FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => { - // Get entry - self.action_find_delete(); - // Delete entries - match self.view.get_state(COMPONENT_EXPLORER_FIND) { - Some(Payload::One(Value::Usize(idx))) => { - // Reload entries - self.found_mut().unwrap().del_entry(idx); - } - Some(Payload::Vec(values)) => { - values - .iter() - .map(|x| match x { - Value::Usize(v) => *v, - _ => 0, - }) - .for_each(|x| self.found_mut().unwrap().del_entry(x)); - } - _ => {} - } - self.update_find_list(); - } - } - self.umount_wait(); - // Reload files - match self.browser.tab() { - FileExplorerTab::Local => self.update_local_filelist(), - FileExplorerTab::Remote => self.update_remote_filelist(), - FileExplorerTab::FindLocal => self.update_local_filelist(), - FileExplorerTab::FindRemote => self.update_remote_filelist(), - } - } - (COMPONENT_RADIO_DELETE, _) => None, - // -- replace - (COMPONENT_RADIO_REPLACE, key) - if key == &MSG_KEY_ESC - || key == &Msg::OnSubmit(Payload::One(Value::Usize(1))) => - { - self.umount_radio_replace(); - None - } - (COMPONENT_RADIO_REPLACE, key) if key == &MSG_KEY_TAB => { - if self.is_radio_replace_extended() { - self.view.active(COMPONENT_LIST_REPLACING_FILES); - } - None - } - (COMPONENT_RADIO_REPLACE, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => { - // Choice is 'YES' - self.umount_radio_replace(); - self.action_finalize_pending_transfer(); - None - } - (COMPONENT_RADIO_REPLACE, _) => None, - (COMPONENT_LIST_REPLACING_FILES, key) if key == &MSG_KEY_TAB => { - self.view.active(COMPONENT_RADIO_REPLACE); - None - } - (COMPONENT_LIST_REPLACING_FILES, key) if key == &MSG_KEY_ESC => { - self.umount_radio_replace(); - None - } - (COMPONENT_LIST_REPLACING_FILES, _) => None, - // -- disconnect - (COMPONENT_RADIO_DISCONNECT, key) - if key == &MSG_KEY_ESC - || key == &Msg::OnSubmit(Payload::One(Value::Usize(1))) => - { - self.umount_disconnect(); - None - } - (COMPONENT_RADIO_DISCONNECT, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => { - self.disconnect(); - self.umount_disconnect(); - None - } - (COMPONENT_RADIO_DISCONNECT, _) => None, - // -- quit - (COMPONENT_RADIO_QUIT, key) - if key == &MSG_KEY_ESC - || key == &Msg::OnSubmit(Payload::One(Value::Usize(1))) => - { - self.umount_quit(); - None - } - (COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => { - self.disconnect_and_quit(); - self.umount_quit(); - None - } - (COMPONENT_RADIO_QUIT, _) => None, - // -- sorting - (COMPONENT_RADIO_SORTING, key) if key == &MSG_KEY_ESC => { - self.umount_file_sorting(); - None - } - (COMPONENT_RADIO_SORTING, Msg::OnSubmit(_)) => { - self.umount_file_sorting(); - None - } - (COMPONENT_RADIO_SORTING, Msg::OnChange(Payload::One(Value::Usize(mode)))) => { - // Get sorting mode - let sorting: FileSorting = match mode { - 1 => FileSorting::ModifyTime, - 2 => FileSorting::CreationTime, - 3 => FileSorting::Size, - _ => FileSorting::Name, - }; - match self.browser.tab() { - FileExplorerTab::Local => self.local_mut().sort_by(sorting), - FileExplorerTab::Remote => self.remote_mut().sort_by(sorting), - _ => panic!("Found result doesn't support SORTING"), - } - // Update status bar - match self.browser.tab() { - FileExplorerTab::Local => self.refresh_local_status_bar(), - FileExplorerTab::Remote => self.refresh_remote_status_bar(), - _ => panic!("Found result doesn't support SORTING"), - }; - // Reload files - match self.browser.tab() { - FileExplorerTab::Local => self.update_local_filelist(), - FileExplorerTab::Remote => self.update_remote_filelist(), - _ => None, - } - } - (COMPONENT_RADIO_SORTING, _) => None, - // -- error - (COMPONENT_TEXT_ERROR, key) | (COMPONENT_TEXT_ERROR, key) - if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => - { - self.umount_error(); - None - } - (COMPONENT_TEXT_ERROR, _) => None, - // -- fatal - (COMPONENT_TEXT_FATAL, key) | (COMPONENT_TEXT_FATAL, key) - if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => - { - self.exit_reason = Some(super::ExitReason::Disconnect); - None - } - (COMPONENT_TEXT_FATAL, _) => None, - // -- help - (COMPONENT_TEXT_HELP, key) | (COMPONENT_TEXT_HELP, key) - if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => - { - self.umount_help(); - None - } - (COMPONENT_TEXT_HELP, _) => None, - // -- progress bar - (COMPONENT_PROGRESS_BAR_PARTIAL, key) if key == &MSG_KEY_CTRL_C => { - // Set transfer aborted to True - self.transfer.abort(); - None - } - (COMPONENT_PROGRESS_BAR_PARTIAL, _) => None, - // -- fallback - (_, _) => None, // Nothing to do - }, +impl Update for FileTransferActivity { + fn update(&mut self, msg: Option) -> Option { + match msg.unwrap_or(Msg::None) { + Msg::None => None, + Msg::Transfer(msg) => self.update_transfer(msg), + Msg::Ui(msg) => self.update_ui(msg), } } } impl FileTransferActivity { - /// ### update_local_filelist - /// - /// Update local file list - pub(super) fn update_local_filelist(&mut self) -> Option<(String, Msg)> { - match self.view.get_props(super::COMPONENT_EXPLORER_LOCAL) { - Some(props) => { - // 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 = self - .local() - .iter_files() - .map(|x: &FsEntry| self.local().fmt_file(x)) - .collect(); - // Update - let props = FileListPropsBuilder::from(props) - .with_files(files) - .with_title(hostname, Alignment::Left) - .build(); - // Update - self.view.update(super::COMPONENT_EXPLORER_LOCAL, props) + fn update_transfer(&mut self, msg: TransferMsg) -> Option { + match msg { + TransferMsg::AbortTransfer => { + self.transfer.abort(); } - None => None, - } - } - - /// ### update_remote_filelist - /// - /// Update remote file list - pub(super) fn update_remote_filelist(&mut self) -> Option<(String, Msg)> { - match self.view.get_props(super::COMPONENT_EXPLORER_REMOTE) { - Some(props) => { - // Get width - 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 = self - .remote() - .iter_files() - .map(|x: &FsEntry| self.remote().fmt_file(x)) - .collect(); - // Update - let props = FileListPropsBuilder::from(props) - .with_files(files) - .with_title(hostname, Alignment::Left) - .build(); - self.view.update(super::COMPONENT_EXPLORER_REMOTE, props) - } - None => None, - } - } - - /// ### update_logbox - /// - /// Update log box - pub(super) fn update_logbox(&mut self) -> Option<(String, Msg)> { - match self.view.get_props(super::COMPONENT_LOG_BOX) { - Some(props) => { - // Make log entries - 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_ref())); + TransferMsg::CopyFileTo(dest) => { + self.umount_copy(); + self.mount_blocking_wait("Copying file(s)…"); + match self.browser.tab() { + FileExplorerTab::Local => self.action_local_copy(dest), + FileExplorerTab::Remote => self.action_remote_copy(dest), + _ => panic!("Found tab doesn't support COPY"), } - let table = table.build(); - let props = LogboxPropsBuilder::from(props).with_log(table).build(); - self.view.update(super::COMPONENT_LOG_BOX, props) + self.umount_wait(); + // Reload files + self.update_browser_file_list() } - None => None, - } - } - - pub(super) fn update_progress_bar(&mut self, filename: String) -> Option<(String, Msg)> { - if let Some(props) = self.view.get_props(COMPONENT_PROGRESS_BAR_FULL) { - let props = ProgressBarPropsBuilder::from(props) - .with_label(self.transfer.full.to_string()) - .with_progress(self.transfer.full.calc_progress()) - .build(); - let _ = self.view.update(COMPONENT_PROGRESS_BAR_FULL, props); - } - match self.view.get_props(COMPONENT_PROGRESS_BAR_PARTIAL) { - Some(props) => { - let props = ProgressBarPropsBuilder::from(props) - .with_title(filename, Alignment::Center) - .with_label(self.transfer.partial.to_string()) - .with_progress(self.transfer.partial.calc_progress()) - .build(); - self.view.update(COMPONENT_PROGRESS_BAR_PARTIAL, props) + TransferMsg::DeleteFile => { + self.umount_radio_delete(); + self.mount_blocking_wait("Removing file(s)…"); + match self.browser.tab() { + FileExplorerTab::Local => self.action_local_delete(), + FileExplorerTab::Remote => self.action_remote_delete(), + FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => { + // Get entry + self.action_find_delete(); + // Delete entries + match self.app.state(&Id::ExplorerFind) { + Ok(State::One(StateValue::Usize(idx))) => { + // Reload entries + self.found_mut().unwrap().del_entry(idx); + } + Ok(State::Vec(values)) => { + values + .iter() + .map(|x| match x { + StateValue::Usize(v) => *v, + _ => 0, + }) + .for_each(|x| self.found_mut().unwrap().del_entry(x)); + } + _ => {} + } + self.update_find_list(); + } + } + self.umount_wait(); + // Reload files + match self.browser.tab() { + FileExplorerTab::Local => self.update_local_filelist(), + FileExplorerTab::Remote => self.update_remote_filelist(), + FileExplorerTab::FindLocal => self.update_local_filelist(), + FileExplorerTab::FindRemote => self.update_remote_filelist(), + } } - None => None, - } - } - - /// ### finalize_find - /// - /// Finalize find process - fn finalize_find(&mut self) { - // Set found to none - self.browser.del_found(); - // Restore tab - self.browser.change_tab(match self.browser.tab() { - FileExplorerTab::FindLocal => FileExplorerTab::Local, - FileExplorerTab::FindRemote => FileExplorerTab::Remote, - _ => FileExplorerTab::Local, - }); - } - - fn update_find_list(&mut self) -> Option<(String, Msg)> { - match self.view.get_props(COMPONENT_EXPLORER_FIND) { - None => None, - Some(props) => { - // Prepare files - let files: Vec = self - .found() - .unwrap() - .iter_files() - .map(|x: &FsEntry| self.found().unwrap().fmt_file(x)) - .collect(); - let props = FileListPropsBuilder::from(props).with_files(files).build(); - self.view.update(COMPONENT_EXPLORER_FIND, props) + TransferMsg::EnterDirectory if self.browser.tab() == FileExplorerTab::Local => { + if let SelectedEntry::One(entry) = self.get_local_selected_entries() { + if self.action_submit_local(entry) { + // Update file list if sync + if self.browser.sync_browsing { + let _ = self.update_remote_filelist(); + } + self.update_local_filelist(); + } + } + } + TransferMsg::EnterDirectory if self.browser.tab() == FileExplorerTab::Remote => { + if let SelectedEntry::One(entry) = self.get_remote_selected_entries() { + if self.action_submit_remote(entry) { + // Update file list if sync + if self.browser.sync_browsing { + let _ = self.update_local_filelist(); + } + self.update_remote_filelist(); + } + } + } + TransferMsg::EnterDirectory => { + // NOTE: is find explorer + // Find changedir + self.action_find_changedir(); + // Umount find + self.umount_find(); + // Finalize find + self.finalize_find(); + // Reload files + self.update_browser_file_list() + } + TransferMsg::ExecuteCmd(cmd) => { + // Exex command + self.umount_exec(); + self.mount_blocking_wait(format!("Executing '{}'…", cmd).as_str()); + match self.browser.tab() { + FileExplorerTab::Local => self.action_local_exec(cmd), + FileExplorerTab::Remote => self.action_remote_exec(cmd), + _ => panic!("Found tab doesn't support EXEC"), + } + self.umount_wait(); + // Reload files + self.update_browser_file_list() + } + TransferMsg::GoTo(dir) => { + match self.browser.tab() { + FileExplorerTab::Local => self.action_change_local_dir(dir, false), + FileExplorerTab::Remote => self.action_change_remote_dir(dir, false), + _ => panic!("Found tab doesn't support GOTO"), + } + // Umount + self.umount_goto(); + // Reload files if sync + if self.browser.sync_browsing { + self.update_browser_file_list_swapped(); + } + // Reload files + self.update_browser_file_list() + } + TransferMsg::GoToParentDirectory => { + match self.browser.tab() { + FileExplorerTab::Local => { + self.action_go_to_local_upper_dir(false); + if self.browser.sync_browsing { + let _ = self.update_remote_filelist(); + } + // Reload file list component + self.update_local_filelist() + } + FileExplorerTab::Remote => { + self.action_go_to_remote_upper_dir(false); + if self.browser.sync_browsing { + let _ = self.update_local_filelist(); + } + // Reload file list component + self.update_remote_filelist() + } + _ => {} + } + } + TransferMsg::GoToPreviousDirectory => { + match self.browser.tab() { + FileExplorerTab::Local => { + self.action_go_to_previous_local_dir(false); + if self.browser.sync_browsing { + let _ = self.update_remote_filelist(); + } + // Reload file list component + self.update_local_filelist() + } + FileExplorerTab::Remote => { + self.action_go_to_previous_remote_dir(false); + if self.browser.sync_browsing { + let _ = self.update_local_filelist(); + } + // Reload file list component + self.update_remote_filelist() + } + _ => {} + } + } + TransferMsg::Mkdir(dir) => { + match self.browser.tab() { + FileExplorerTab::Local => self.action_local_mkdir(dir), + FileExplorerTab::Remote => self.action_remote_mkdir(dir), + _ => {} + } + self.umount_mkdir(); + // Reload files + self.update_browser_file_list() + } + TransferMsg::NewFile(name) => { + match self.browser.tab() { + FileExplorerTab::Local => self.action_local_newfile(name), + FileExplorerTab::Remote => self.action_remote_newfile(name), + _ => {} + } + self.umount_newfile(); + // Reload files + self.update_browser_file_list() + } + TransferMsg::OpenFile => match self.browser.tab() { + FileExplorerTab::Local => self.action_open_local(), + FileExplorerTab::Remote => self.action_open_remote(), + FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => self.action_find_open(), + }, + TransferMsg::OpenFileWith(prog) => { + match self.browser.tab() { + FileExplorerTab::Local => self.action_local_open_with(&prog), + FileExplorerTab::Remote => self.action_remote_open_with(&prog), + FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => { + self.action_find_open_with(&prog) + } + } + self.umount_openwith(); + } + TransferMsg::OpenTextFile => { + match self.browser.tab() { + FileExplorerTab::Local => self.action_edit_local_file(), + FileExplorerTab::Remote => self.action_edit_remote_file(), + _ => {} + } + self.update_browser_file_list() + } + TransferMsg::ReloadDir => self.update_browser_file_list(), + TransferMsg::RenameFile(dest) => { + self.umount_rename(); + self.mount_blocking_wait("Moving file(s)…"); + match self.browser.tab() { + FileExplorerTab::Local => self.action_local_rename(dest), + FileExplorerTab::Remote => self.action_remote_rename(dest), + _ => {} + } + self.umount_wait(); + // Reload files + self.update_browser_file_list() + } + TransferMsg::SaveFileAs(dest) => { + self.umount_saveas(); + match self.browser.tab() { + FileExplorerTab::Local => self.action_local_saveas(dest), + FileExplorerTab::Remote => self.action_remote_saveas(dest), + FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => { + // Get entry + self.action_find_transfer(TransferOpts::default().save_as(Some(dest))); + } + } + self.umount_saveas(); + // Reload files + self.update_browser_file_list_swapped(); + } + TransferMsg::SearchFile(search) => { + self.umount_find_input(); + // Mount wait + self.mount_blocking_wait(format!(r#"Searching for "{}"…"#, search).as_str()); + // Find + let res: Result, String> = match self.browser.tab() { + FileExplorerTab::Local => self.action_local_find(search.clone()), + FileExplorerTab::Remote => self.action_remote_find(search.clone()), + _ => panic!("Trying to search for files, while already in a find result"), + }; + // Umount wait + self.umount_wait(); + // Match result + match res { + Err(err) => { + // Mount error + self.mount_error(err.as_str()); + } + Ok(files) if files.is_empty() => { + // If no file has been found notify user + self.mount_info( + format!(r#"Could not find any file matching "{}""#, search).as_str(), + ); + } + Ok(files) => { + // Get wrkdir + let wrkdir = match self.browser.tab() { + FileExplorerTab::Local => self.local().wrkdir.clone(), + _ => self.remote().wrkdir.clone(), + }; + // Create explorer and load files + self.browser.set_found( + match self.browser.tab() { + FileExplorerTab::Local => FoundExplorerTab::Local, + _ => FoundExplorerTab::Remote, + }, + files, + wrkdir.as_path(), + ); + // Mount result widget + self.mount_find(&search); + self.update_find_list(); + // Initialize tab + self.browser.change_tab(match self.browser.tab() { + FileExplorerTab::Local => FileExplorerTab::FindLocal, + FileExplorerTab::Remote => FileExplorerTab::FindRemote, + _ => FileExplorerTab::FindLocal, + }); + } + } + } + TransferMsg::TransferFile => { + match self.browser.tab() { + FileExplorerTab::Local => self.action_local_send(), + FileExplorerTab::Remote => self.action_remote_recv(), + FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => { + self.action_find_transfer(TransferOpts::default()) + } + } + self.update_browser_file_list_swapped(); + } + TransferMsg::TransferPendingFile => { + self.umount_radio_replace(); + self.action_finalize_pending_transfer(); } } + // Force redraw + self.redraw = true; + None + } + + fn update_ui(&mut self, msg: UiMsg) -> Option { + match msg { + UiMsg::ChangeFileSorting(sorting) => { + match self.browser.tab() { + FileExplorerTab::Local | FileExplorerTab::FindLocal => { + self.local_mut().sort_by(sorting); + self.refresh_local_status_bar(); + } + FileExplorerTab::Remote | FileExplorerTab::FindRemote => { + self.remote_mut().sort_by(sorting); + self.refresh_remote_status_bar() + } + } + self.update_browser_file_list(); + } + UiMsg::ChangeTransferWindow => { + let new_tab = match self.browser.tab() { + FileExplorerTab::Local if self.browser.found().is_some() => { + FileExplorerTab::FindRemote + } + FileExplorerTab::FindLocal | FileExplorerTab::Local => FileExplorerTab::Remote, + FileExplorerTab::Remote if self.browser.found().is_some() => { + FileExplorerTab::FindLocal + } + FileExplorerTab::FindRemote | FileExplorerTab::Remote => FileExplorerTab::Local, + }; + // Set focus + 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); + } + UiMsg::CloseCopyPopup => self.umount_copy(), + UiMsg::CloseDeletePopup => self.umount_radio_delete(), + UiMsg::CloseDisconnectPopup => self.umount_disconnect(), + UiMsg::CloseErrorPopup => self.umount_error(), + UiMsg::CloseExecPopup => self.umount_exec(), + UiMsg::CloseFatalPopup => { + self.umount_fatal(); + self.exit_reason = Some(ExitReason::Disconnect); + } + UiMsg::CloseFileInfoPopup => self.umount_file_info(), + UiMsg::CloseFileSortingPopup => self.umount_file_sorting(), + UiMsg::CloseFindExplorer => { + self.finalize_find(); + self.umount_find(); + } + UiMsg::CloseFindPopup => self.umount_find_input(), + UiMsg::CloseGotoPopup => self.umount_goto(), + UiMsg::CloseKeybindingsPopup => self.umount_help(), + UiMsg::CloseMkdirPopup => self.umount_mkdir(), + UiMsg::CloseNewFilePopup => self.umount_newfile(), + UiMsg::CloseOpenWithPopup => self.umount_openwith(), + UiMsg::CloseQuitPopup => self.umount_quit(), + UiMsg::CloseRenamePopup => self.umount_rename(), + UiMsg::CloseReplacePopups => { + self.umount_radio_replace(); + } + UiMsg::CloseSaveAsPopup => self.umount_saveas(), + UiMsg::Disconnect => { + self.disconnect(); + self.umount_disconnect(); + } + UiMsg::ExplorerTabbed => { + assert!(self.app.active(&Id::Log).is_ok()); + } + UiMsg::LogTabbed => { + assert!(self.app.active(&Id::ExplorerLocal).is_ok()); + } + UiMsg::Quit => { + self.disconnect_and_quit(); + self.umount_quit(); + } + UiMsg::ReplacePopupTabbed => { + if let Ok(Some(AttrValue::Flag(true))) = + self.app.query(&Id::ReplacePopup, Attribute::Focus) + { + assert!(self.app.active(&Id::ReplacingFilesListPopup).is_ok()); + } else { + assert!(self.app.active(&Id::ReplacePopup).is_ok()); + } + } + UiMsg::ShowCopyPopup => self.mount_copy(), + UiMsg::ShowDeletePopup => self.mount_radio_delete(), + UiMsg::ShowDisconnectPopup => self.mount_disconnect(), + UiMsg::ShowExecPopup => self.mount_exec(), + UiMsg::ShowFileInfoPopup if self.browser.tab() == FileExplorerTab::Local => { + if let SelectedEntry::One(file) = self.get_local_selected_entries() { + self.mount_file_info(&file); + } + } + UiMsg::ShowFileInfoPopup if self.browser.tab() == FileExplorerTab::Remote => { + if let SelectedEntry::One(file) = self.get_remote_selected_entries() { + self.mount_file_info(&file); + } + } + UiMsg::ShowFileInfoPopup => { + if let SelectedEntry::One(file) = self.get_found_selected_entries() { + self.mount_file_info(&file); + } + } + UiMsg::ShowFileSortingPopup => self.mount_file_sorting(), + UiMsg::ShowFindPopup => self.mount_find_input(), + UiMsg::ShowGotoPopup => self.mount_goto(), + UiMsg::ShowKeybindingsPopup => self.mount_help(), + UiMsg::ShowMkdirPopup => self.mount_mkdir(), + UiMsg::ShowNewFilePopup => self.mount_newfile(), + UiMsg::ShowOpenWithPopup => self.mount_openwith(), + UiMsg::ShowQuitPopup => self.mount_quit(), + UiMsg::ShowRenamePopup => self.mount_rename(), + UiMsg::ShowSaveAsPopup => self.mount_saveas(), + UiMsg::ToggleHiddenFiles => match self.browser.tab() { + FileExplorerTab::FindLocal | FileExplorerTab::Local => { + self.browser.local_mut().toggle_hidden_files(); + self.refresh_local_status_bar(); + self.update_browser_file_list(); + } + FileExplorerTab::FindRemote | FileExplorerTab::Remote => { + self.browser.remote_mut().toggle_hidden_files(); + self.refresh_remote_status_bar(); + self.update_browser_file_list(); + } + }, + UiMsg::ToggleSyncBrowsing => { + self.browser.toggle_sync_browsing(); + self.refresh_remote_status_bar(); + } + } + None } } diff --git a/src/ui/activities/filetransfer/view.rs b/src/ui/activities/filetransfer/view.rs index 5cf74ad..8a03f34 100644 --- a/src/ui/activities/filetransfer/view.rs +++ b/src/ui/activities/filetransfer/view.rs @@ -28,33 +28,17 @@ // locals use super::{ browser::{FileExplorerTab, FoundExplorerTab}, - Context, FileTransferActivity, + components, Context, FileTransferActivity, Id, }; use crate::fs::explorer::FileSorting; use crate::fs::FsEntry; -use crate::ui::components::{ - file_list::{FileList, FileListPropsBuilder}, - logbox::{LogBox, LogboxPropsBuilder}, -}; use crate::ui::store::Store; -use crate::utils::fmt::fmt_time; use crate::utils::ui::draw_area_in; // Ext -use bytesize::ByteSize; -use std::path::PathBuf; -use tui_realm_stdlib::{ - Input, InputPropsBuilder, List, ListPropsBuilder, Paragraph, ParagraphPropsBuilder, - ProgressBar, ProgressBarPropsBuilder, Radio, RadioPropsBuilder, Span, SpanPropsBuilder, Table, - TablePropsBuilder, -}; -use tuirealm::props::{Alignment, PropsBuilder, TableBuilder, TextSpan}; -use tuirealm::tui::{ - layout::{Constraint, Direction, Layout}, - style::Color, - widgets::{BorderType, Borders, Clear}, -}; -#[cfg(target_family = "unix")] -use users::{get_group_by_gid, get_user_by_uid}; +use tuirealm::event::{Key, KeyEvent, KeyModifiers}; +use tuirealm::tui::layout::{Constraint, Direction, Layout}; +use tuirealm::tui::widgets::Clear; +use tuirealm::{Sub, SubClause, SubEventClause}; impl FileTransferActivity { // -- init @@ -72,57 +56,52 @@ impl FileTransferActivity { let remote_explorer_highlighted = self.theme().transfer_remote_explorer_highlighted; let log_panel = self.theme().transfer_log_window; let log_background = self.theme().transfer_log_background; - self.view.mount( - super::COMPONENT_EXPLORER_LOCAL, - Box::new(FileList::new( - FileListPropsBuilder::default() - .with_highlight_color(local_explorer_highlighted) - .with_background(local_explorer_background) - .with_foreground(local_explorer_foreground) - .with_borders(Borders::ALL, BorderType::Plain, local_explorer_highlighted) - .build(), - )), - ); - // Mount remote file explorer - self.view.mount( - super::COMPONENT_EXPLORER_REMOTE, - Box::new(FileList::new( - FileListPropsBuilder::default() - .with_highlight_color(remote_explorer_highlighted) - .with_background(remote_explorer_background) - .with_foreground(remote_explorer_foreground) - .with_borders(Borders::ALL, BorderType::Plain, remote_explorer_highlighted) - .build(), - )), - ); - // Mount log box - self.view.mount( - super::COMPONENT_LOG_BOX, - Box::new(LogBox::new( - LogboxPropsBuilder::default() - .with_title("Log", Alignment::Left) - .with_background(log_background) - .with_borders(Borders::ALL, BorderType::Plain, log_panel) - .build(), - )), - ); - // Mount status bars - self.view.mount( - super::COMPONENT_SPAN_STATUS_BAR_LOCAL, - Box::new(Span::new(SpanPropsBuilder::default().build())), - ); - self.view.mount( - super::COMPONENT_SPAN_STATUS_BAR_REMOTE, - Box::new(Span::new(SpanPropsBuilder::default().build())), - ); - // Load process bar + assert!(self + .app + .mount( + Id::ExplorerLocal, + Box::new(components::ExplorerLocal::new( + "", + &[], + local_explorer_background, + local_explorer_foreground, + local_explorer_highlighted + )), + vec![] + ) + .is_ok()); + assert!(self + .app + .mount( + Id::ExplorerRemote, + Box::new(components::ExplorerRemote::new( + "", + &[], + remote_explorer_background, + remote_explorer_foreground, + remote_explorer_highlighted + )), + vec![] + ) + .is_ok()); + assert!(self + .app + .mount( + Id::Log, + Box::new(components::Log::new(vec![], log_panel, log_background)), + vec![] + ) + .is_ok()); + // Load status bar self.refresh_local_status_bar(); self.refresh_remote_status_bar(); // Update components let _ = self.update_local_filelist(); let _ = self.update_remote_filelist(); + // Global listener + self.mount_global_listener(); // Give focus to local explorer - self.view.active(super::COMPONENT_EXPLORER_LOCAL); + assert!(self.app.active(&Id::ExplorerLocal).is_ok()); } // -- view @@ -131,9 +110,10 @@ impl FileTransferActivity { /// /// View gui pub(super) fn view(&mut self) { + self.redraw = false; let mut context: Context = self.context.take().unwrap(); let store: &mut Store = &mut context.store; - let _ = context.terminal.draw(|f| { + let _ = context.terminal.raw_mut().draw(|f| { // Prepare chunks let chunks = Layout::default() .direction(Direction::Vertical) @@ -169,228 +149,152 @@ impl FileTransferActivity { // Draw explorers // @! Local explorer (Find or default) if matches!(self.browser.found_tab(), Some(FoundExplorerTab::Local)) { - self.view - .render(super::COMPONENT_EXPLORER_FIND, f, tabs_chunks[0]); + self.app.view(&Id::ExplorerFind, f, tabs_chunks[0]); } else { - self.view - .render(super::COMPONENT_EXPLORER_LOCAL, f, tabs_chunks[0]); + self.app.view(&Id::ExplorerLocal, f, tabs_chunks[0]); } // @! Remote explorer (Find or default) if matches!(self.browser.found_tab(), Some(FoundExplorerTab::Remote)) { - self.view - .render(super::COMPONENT_EXPLORER_FIND, f, tabs_chunks[1]); + self.app.view(&Id::ExplorerFind, f, tabs_chunks[1]); } else { - self.view - .render(super::COMPONENT_EXPLORER_REMOTE, f, tabs_chunks[1]); + self.app.view(&Id::ExplorerRemote, f, tabs_chunks[1]); } // Draw log box - self.view - .render(super::COMPONENT_LOG_BOX, f, bottom_chunks[1]); + self.app.view(&Id::Log, f, bottom_chunks[1]); // Draw status bar - self.view.render( - super::COMPONENT_SPAN_STATUS_BAR_LOCAL, - f, - status_bar_chunks[0], - ); - self.view.render( - super::COMPONENT_SPAN_STATUS_BAR_REMOTE, - f, - status_bar_chunks[1], - ); + self.app.view(&Id::StatusBarLocal, f, status_bar_chunks[0]); + self.app.view(&Id::StatusBarRemote, f, status_bar_chunks[1]); // @! Draw popups - if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_COPY) { - if props.visible { - let popup = draw_area_in(f.size(), 40, 10); - f.render_widget(Clear, popup); - // make popup - self.view.render(super::COMPONENT_INPUT_COPY, f, popup); - } - } - if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_FIND) { - if props.visible { - let popup = draw_area_in(f.size(), 40, 10); - f.render_widget(Clear, popup); - // make popup - self.view.render(super::COMPONENT_INPUT_FIND, f, popup); - } - } - if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_GOTO) { - if props.visible { - let popup = draw_area_in(f.size(), 40, 10); - f.render_widget(Clear, popup); - // make popup - self.view.render(super::COMPONENT_INPUT_GOTO, f, popup); - } - } - if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_MKDIR) { - if props.visible { - let popup = draw_area_in(f.size(), 40, 10); - f.render_widget(Clear, popup); - // make popup - self.view.render(super::COMPONENT_INPUT_MKDIR, f, popup); - } - } - if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_NEWFILE) { - if props.visible { - let popup = draw_area_in(f.size(), 40, 10); - f.render_widget(Clear, popup); - // make popup - self.view.render(super::COMPONENT_INPUT_NEWFILE, f, popup); - } - } - if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_OPEN_WITH) { - if props.visible { - let popup = draw_area_in(f.size(), 40, 10); - f.render_widget(Clear, popup); - // make popup - self.view.render(super::COMPONENT_INPUT_OPEN_WITH, f, popup); - } - } - if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_RENAME) { - if props.visible { - let popup = draw_area_in(f.size(), 40, 10); - f.render_widget(Clear, popup); - // make popup - self.view.render(super::COMPONENT_INPUT_RENAME, f, popup); - } - } - if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_SAVEAS) { - if props.visible { - let popup = draw_area_in(f.size(), 40, 10); - f.render_widget(Clear, popup); - // make popup - self.view.render(super::COMPONENT_INPUT_SAVEAS, f, popup); - } - } - if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_EXEC) { - if props.visible { - let popup = draw_area_in(f.size(), 40, 10); - f.render_widget(Clear, popup); - // make popup - self.view.render(super::COMPONENT_INPUT_EXEC, f, popup); - } - } - if let Some(props) = self.view.get_props(super::COMPONENT_LIST_FILEINFO) { - if props.visible { + if self.app.mounted(&Id::CopyPopup) { + let popup = draw_area_in(f.size(), 40, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::CopyPopup, f, popup); + } else if self.app.mounted(&Id::FindPopup) { + let popup = draw_area_in(f.size(), 40, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::FindPopup, f, popup); + } else if self.app.mounted(&Id::GotoPopup) { + let popup = draw_area_in(f.size(), 40, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::GotoPopup, f, popup); + } else if self.app.mounted(&Id::MkdirPopup) { + let popup = draw_area_in(f.size(), 40, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::MkdirPopup, f, popup); + } else if self.app.mounted(&Id::NewfilePopup) { + let popup = draw_area_in(f.size(), 40, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::NewfilePopup, f, popup); + } else if self.app.mounted(&Id::OpenWithPopup) { + let popup = draw_area_in(f.size(), 40, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::OpenWithPopup, f, popup); + } else if self.app.mounted(&Id::RenamePopup) { + let popup = draw_area_in(f.size(), 40, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::RenamePopup, f, popup); + } else if self.app.mounted(&Id::SaveAsPopup) { + let popup = draw_area_in(f.size(), 40, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::SaveAsPopup, f, popup); + } else if self.app.mounted(&Id::ExecPopup) { + let popup = draw_area_in(f.size(), 40, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::ExecPopup, f, popup); + } else if self.app.mounted(&Id::FileInfoPopup) { + let popup = draw_area_in(f.size(), 50, 50); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::FileInfoPopup, f, popup); + } else if self.app.mounted(&Id::ProgressBarPartial) { + let popup = draw_area_in(f.size(), 50, 20); + f.render_widget(Clear, popup); + // make popup + let popup_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Percentage(50), // Full + Constraint::Percentage(50), // Partial + ] + .as_ref(), + ) + .split(popup); + self.app.view(&Id::ProgressBarFull, f, popup_chunks[0]); + self.app.view(&Id::ProgressBarPartial, f, popup_chunks[1]); + } else if self.app.mounted(&Id::DeletePopup) { + let popup = draw_area_in(f.size(), 30, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::DeletePopup, f, popup); + } else if self.app.mounted(&Id::ReplacePopup) { + // NOTE: handle extended / normal modes + if self.is_radio_replace_extended() { let popup = draw_area_in(f.size(), 50, 50); f.render_widget(Clear, popup); - // make popup - self.view.render(super::COMPONENT_LIST_FILEINFO, f, popup); - } - } - if let Some(props) = self.view.get_props(super::COMPONENT_PROGRESS_BAR_PARTIAL) { - if props.visible { - let popup = draw_area_in(f.size(), 50, 20); - f.render_widget(Clear, popup); - // make popup let popup_chunks = Layout::default() .direction(Direction::Vertical) .constraints( [ - Constraint::Percentage(50), // Full - Constraint::Percentage(50), // Partial + Constraint::Percentage(85), // List + Constraint::Percentage(15), // Radio ] .as_ref(), ) .split(popup); - self.view - .render(super::COMPONENT_PROGRESS_BAR_FULL, f, popup_chunks[0]); - self.view - .render(super::COMPONENT_PROGRESS_BAR_PARTIAL, f, popup_chunks[1]); - } - } - if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_DELETE) { - if props.visible { - let popup = draw_area_in(f.size(), 30, 10); - f.render_widget(Clear, popup); - // make popup - self.view.render(super::COMPONENT_RADIO_DELETE, f, popup); - } - } - if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_REPLACE) { - if props.visible { - // NOTE: handle extended / normal modes - if self.is_radio_replace_extended() { - let popup = draw_area_in(f.size(), 50, 50); - f.render_widget(Clear, popup); - let popup_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Percentage(85), // List - Constraint::Percentage(15), // Radio - ] - .as_ref(), - ) - .split(popup); - self.view - .render(super::COMPONENT_LIST_REPLACING_FILES, f, popup_chunks[0]); - self.view - .render(super::COMPONENT_RADIO_REPLACE, f, popup_chunks[1]); - } else { - let popup = draw_area_in(f.size(), 50, 10); - f.render_widget(Clear, popup); - // make popup - self.view.render(super::COMPONENT_RADIO_REPLACE, f, popup); - } - } - } - if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_DISCONNECT) { - if props.visible { - let popup = draw_area_in(f.size(), 30, 10); - f.render_widget(Clear, popup); - // make popup - self.view - .render(super::COMPONENT_RADIO_DISCONNECT, f, popup); - } - } - if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) { - if props.visible { - let popup = draw_area_in(f.size(), 30, 10); - f.render_widget(Clear, popup); - // make popup - self.view.render(super::COMPONENT_RADIO_QUIT, f, popup); - } - } - if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_SORTING) { - if props.visible { + self.app + .view(&Id::ReplacingFilesListPopup, f, popup_chunks[0]); + self.app.view(&Id::ReplacePopup, f, popup_chunks[1]); + } else { let popup = draw_area_in(f.size(), 50, 10); f.render_widget(Clear, popup); // make popup - self.view.render(super::COMPONENT_RADIO_SORTING, f, popup); - } - } - if let Some(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_TEXT_FATAL) { - if props.visible { - let popup = draw_area_in(f.size(), 50, 10); - f.render_widget(Clear, popup); - // make popup - self.view.render(super::COMPONENT_TEXT_FATAL, f, popup); - } - } - if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_WAIT) { - if props.visible { - let popup = draw_area_in(f.size(), 50, 10); - f.render_widget(Clear, popup); - // make popup - self.view.render(super::COMPONENT_TEXT_WAIT, f, popup); - } - } - if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_HELP) { - if props.visible { - let popup = draw_area_in(f.size(), 50, 80); - f.render_widget(Clear, popup); - // make popup - self.view.render(super::COMPONENT_TEXT_HELP, f, popup); + self.app.view(&Id::ReplacePopup, f, popup); } + } else if self.app.mounted(&Id::DisconnectPopup) { + let popup = draw_area_in(f.size(), 30, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::DisconnectPopup, f, popup); + } else if self.app.mounted(&Id::QuitPopup) { + let popup = draw_area_in(f.size(), 30, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::QuitPopup, f, popup); + } else if self.app.mounted(&Id::SortingPopup) { + let popup = draw_area_in(f.size(), 50, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::SortingPopup, f, popup); + } else if self.app.mounted(&Id::ErrorPopup) { + let popup = draw_area_in(f.size(), 50, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::ErrorPopup, f, popup); + } else if self.app.mounted(&Id::FatalPopup) { + let popup = draw_area_in(f.size(), 50, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::FatalPopup, f, popup); + } else if self.app.mounted(&Id::WaitPopup) { + let popup = draw_area_in(f.size(), 50, 10); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::WaitPopup, f, popup); + } else if self.app.mounted(&Id::KeybindingsPopup) { + let popup = draw_area_in(f.size(), 50, 80); + f.render_widget(Clear, popup); + // make popup + self.app.view(&Id::KeybindingsPopup, f, popup); } }); // Re-give context @@ -402,50 +306,85 @@ impl FileTransferActivity { /// ### mount_info /// /// Mount info box - pub(super) fn mount_info(&mut self, text: &str) { + pub(super) fn mount_info>(&mut self, text: S) { // Mount let info_color = self.theme().misc_info_dialog; - self.mount_text_dialog(super::COMPONENT_TEXT_ERROR, text, info_color); + assert!(self + .app + .remount( + Id::ErrorPopup, + Box::new(components::ErrorPopup::new(text, info_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::ErrorPopup).is_ok()); } /// ### mount_error /// /// Mount error box - pub(super) fn mount_error(&mut self, text: &str) { + pub(super) fn mount_error>(&mut self, text: S) { // Mount let error_color = self.theme().misc_error_dialog; - self.mount_text_dialog(super::COMPONENT_TEXT_ERROR, text, error_color); + assert!(self + .app + .remount( + Id::ErrorPopup, + Box::new(components::ErrorPopup::new(text, error_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::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::ErrorPopup); } - pub(super) fn mount_fatal(&mut self, text: &str) { + pub(super) fn mount_fatal>(&mut self, text: S) { // Mount let error_color = self.theme().misc_error_dialog; - self.mount_text_dialog(super::COMPONENT_TEXT_FATAL, text, error_color); + assert!(self + .app + .remount( + Id::FatalPopup, + Box::new(components::FatalPopup::new(text, error_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::FatalPopup).is_ok()); } - pub(super) fn mount_wait(&mut self, text: &str) { - self.mount_wait_ex(text); + /// ### umount_fatal + /// + /// Umount fatal error message + pub(super) fn umount_fatal(&mut self) { + let _ = self.app.umount(&Id::FatalPopup); } - pub(super) fn mount_blocking_wait(&mut self, text: &str) { - self.mount_wait_ex(text); + pub(super) fn mount_wait>(&mut self, text: S) { + let color = self.theme().misc_info_dialog; + assert!(self + .app + .remount( + Id::WaitPopup, + Box::new(components::WaitPopup::new(text, color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::WaitPopup).is_ok()); + } + + pub(super) fn mount_blocking_wait>(&mut self, text: S) { + self.mount_wait(text); self.view(); } - fn mount_wait_ex(&mut self, text: &str) { - let color = self.theme().misc_info_dialog; - self.mount_text_dialog(super::COMPONENT_TEXT_WAIT, text, color); - } - pub(super) fn umount_wait(&mut self) { - self.view.umount(super::COMPONENT_TEXT_WAIT); + let _ = self.app.umount(&Id::WaitPopup); } /// ### mount_quit @@ -454,20 +393,22 @@ impl FileTransferActivity { pub(super) fn mount_quit(&mut self) { // Protocol let quit_color = self.theme().misc_quit_dialog; - self.mount_radio_dialog( - super::COMPONENT_RADIO_QUIT, - "Are you sure you want to quit?", - &["Yes", "No"], - 0, - quit_color, - ); + assert!(self + .app + .remount( + Id::QuitPopup, + Box::new(components::QuitPopup::new(quit_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::QuitPopup).is_ok()); } /// ### umount_quit /// /// Umount quit popup pub(super) fn umount_quit(&mut self) { - self.view.umount(super::COMPONENT_RADIO_QUIT); + let _ = self.app.umount(&Id::QuitPopup); } /// ### mount_disconnect @@ -476,53 +417,61 @@ impl FileTransferActivity { pub(super) fn mount_disconnect(&mut self) { // Protocol let quit_color = self.theme().misc_quit_dialog; - self.mount_radio_dialog( - super::COMPONENT_RADIO_DISCONNECT, - "Are you sure you want to disconnect?", - &["Yes", "No"], - 0, - quit_color, - ); + assert!(self + .app + .remount( + Id::DisconnectPopup, + Box::new(components::DisconnectPopup::new(quit_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::DisconnectPopup).is_ok()); } /// ### umount_disconnect /// /// Umount disconnect popup pub(super) fn umount_disconnect(&mut self) { - self.view.umount(super::COMPONENT_RADIO_DISCONNECT); + let _ = self.app.umount(&Id::DisconnectPopup); } pub(super) fn mount_copy(&mut self) { let input_color = self.theme().misc_input_dialog; - self.mount_input_dialog( - super::COMPONENT_INPUT_COPY, - "Copy file(s) to…", - "", - input_color, - ); + assert!(self + .app + .remount( + Id::CopyPopup, + Box::new(components::CopyPopup::new(input_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::CopyPopup).is_ok()); } pub(super) fn umount_copy(&mut self) { - self.view.umount(super::COMPONENT_INPUT_COPY); + let _ = self.app.umount(&Id::CopyPopup); } pub(super) fn mount_exec(&mut self) { let input_color = self.theme().misc_input_dialog; - self.mount_input_dialog( - super::COMPONENT_INPUT_EXEC, - "Execute command", - "", - input_color, - ); + assert!(self + .app + .remount( + Id::ExecPopup, + Box::new(components::ExecPopup::new(input_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::ExecPopup).is_ok()); } pub(super) fn umount_exec(&mut self) { - self.view.umount(super::COMPONENT_INPUT_EXEC); + let _ = self.app.umount(&Id::ExecPopup); } pub(super) fn mount_find(&mut self, search: &str) { // Get color - let (bg, fg, hg): (Color, Color, Color) = match self.browser.tab() { + let (bg, fg, hg) = match self.browser.tab() { FileExplorerTab::Local | FileExplorerTab::FindLocal => ( self.theme().transfer_local_explorer_background, self.theme().transfer_local_explorer_foreground, @@ -535,162 +484,182 @@ impl FileTransferActivity { ), }; // Mount component - self.view.mount( - super::COMPONENT_EXPLORER_FIND, - Box::new(FileList::new( - FileListPropsBuilder::default() - .with_title( - format!("Search results for \"{}\"", search), - Alignment::Left, - ) - .with_borders(Borders::ALL, BorderType::Plain, hg) - .with_highlight_color(hg) - .with_background(bg) - .with_foreground(fg) - .build(), - )), - ); - // Give focus to explorer findd - self.view.active(super::COMPONENT_EXPLORER_FIND); + assert!(self + .app + .remount( + Id::ExplorerFind, + Box::new(components::ExplorerFind::new( + format!(r#"Search results for "{}""#, search), + &[], + bg, + fg, + hg + )), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::ExplorerFind).is_ok()); } pub(super) fn umount_find(&mut self) { - self.view.umount(super::COMPONENT_EXPLORER_FIND); + let _ = self.app.umount(&Id::ExplorerFind); } pub(super) fn mount_find_input(&mut self) { let input_color = self.theme().misc_input_dialog; - self.mount_input_dialog( - super::COMPONENT_INPUT_FIND, - "Search files by name", - "", - input_color, - ); + assert!(self + .app + .remount( + Id::FindPopup, + Box::new(components::FindPopup::new(input_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::FindPopup).is_ok()); } pub(super) fn umount_find_input(&mut self) { // Umount input find - self.view.umount(super::COMPONENT_INPUT_FIND); + let _ = self.app.umount(&Id::FindPopup); } pub(super) fn mount_goto(&mut self) { let input_color = self.theme().misc_input_dialog; - self.mount_input_dialog( - super::COMPONENT_INPUT_GOTO, - "Change working directory", - "", - input_color, - ); + assert!(self + .app + .remount( + Id::GotoPopup, + Box::new(components::GoToPopup::new(input_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::GotoPopup).is_ok()); } pub(super) fn umount_goto(&mut self) { - self.view.umount(super::COMPONENT_INPUT_GOTO); + let _ = self.app.umount(&Id::GotoPopup); } pub(super) fn mount_mkdir(&mut self) { let input_color = self.theme().misc_input_dialog; - self.mount_input_dialog( - super::COMPONENT_INPUT_MKDIR, - "Insert directory name", - "", - input_color, - ); + assert!(self + .app + .remount( + Id::MkdirPopup, + Box::new(components::MkdirPopup::new(input_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::MkdirPopup).is_ok()); } pub(super) fn umount_mkdir(&mut self) { - self.view.umount(super::COMPONENT_INPUT_MKDIR); + let _ = self.app.umount(&Id::MkdirPopup); } pub(super) fn mount_newfile(&mut self) { let input_color = self.theme().misc_input_dialog; - self.mount_input_dialog( - super::COMPONENT_INPUT_NEWFILE, - "New file name", - "", - input_color, - ); + assert!(self + .app + .remount( + Id::NewfilePopup, + Box::new(components::NewfilePopup::new(input_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::NewfilePopup).is_ok()); } pub(super) fn umount_newfile(&mut self) { - self.view.umount(super::COMPONENT_INPUT_NEWFILE); + let _ = self.app.umount(&Id::NewfilePopup); } pub(super) fn mount_openwith(&mut self) { let input_color = self.theme().misc_input_dialog; - self.mount_input_dialog( - super::COMPONENT_INPUT_OPEN_WITH, - "Open file with…", - "", - input_color, - ); + assert!(self + .app + .remount( + Id::OpenWithPopup, + Box::new(components::OpenWithPopup::new(input_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::OpenWithPopup).is_ok()); } pub(super) fn umount_openwith(&mut self) { - self.view.umount(super::COMPONENT_INPUT_OPEN_WITH); + let _ = self.app.umount(&Id::OpenWithPopup); } pub(super) fn mount_rename(&mut self) { let input_color = self.theme().misc_input_dialog; - self.mount_input_dialog( - super::COMPONENT_INPUT_RENAME, - "Move file(s) to…", - "", - input_color, - ); + assert!(self + .app + .remount( + Id::RenamePopup, + Box::new(components::RenamePopup::new(input_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::RenamePopup).is_ok()); } pub(super) fn umount_rename(&mut self) { - self.view.umount(super::COMPONENT_INPUT_RENAME); + let _ = self.app.umount(&Id::RenamePopup); } pub(super) fn mount_saveas(&mut self) { let input_color = self.theme().misc_input_dialog; - self.mount_input_dialog(super::COMPONENT_INPUT_SAVEAS, "Save as…", "", input_color); + assert!(self + .app + .remount( + Id::SaveAsPopup, + Box::new(components::SaveAsPopup::new(input_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::SaveAsPopup).is_ok()); } pub(super) fn umount_saveas(&mut self) { - self.view.umount(super::COMPONENT_INPUT_SAVEAS); + let _ = self.app.umount(&Id::SaveAsPopup); } pub(super) fn mount_progress_bar(&mut self, root_name: String) { let prog_color_full = self.theme().transfer_progress_bar_full; let prog_color_partial = self.theme().transfer_progress_bar_partial; - self.view.mount( - super::COMPONENT_PROGRESS_BAR_FULL, - Box::new(ProgressBar::new( - ProgressBarPropsBuilder::default() - .with_progbar_color(prog_color_full) - .with_background(Color::Black) - .with_borders( - Borders::TOP | Borders::RIGHT | Borders::LEFT, - BorderType::Rounded, - Color::Reset, - ) - .with_title(root_name, Alignment::Center) - .build(), - )), - ); - self.view.mount( - super::COMPONENT_PROGRESS_BAR_PARTIAL, - Box::new(ProgressBar::new( - ProgressBarPropsBuilder::default() - .with_progbar_color(prog_color_partial) - .with_background(Color::Black) - .with_borders( - Borders::BOTTOM | Borders::RIGHT | Borders::LEFT, - BorderType::Rounded, - Color::Reset, - ) - .with_title("Please wait", Alignment::Center) - .build(), - )), - ); - self.view.active(super::COMPONENT_PROGRESS_BAR_PARTIAL); + assert!(self + .app + .remount( + Id::ProgressBarFull, + Box::new(components::ProgressBarFull::new( + 0.0, + "", + &root_name, + prog_color_full + )), + vec![], + ) + .is_ok()); + assert!(self + .app + .remount( + Id::ProgressBarPartial, + Box::new(components::ProgressBarPartial::new( + 0.0, + "", + "Please wait", + prog_color_partial + )), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::ProgressBarPartial).is_ok()); } pub(super) fn umount_progress_bar(&mut self) { - self.view.umount(super::COMPONENT_PROGRESS_BAR_PARTIAL); - self.view.umount(super::COMPONENT_PROGRESS_BAR_FULL); + let _ = self.app.umount(&Id::ProgressBarPartial); + let _ = self.app.umount(&Id::ProgressBarFull); } pub(super) fn mount_file_sorting(&mut self) { @@ -698,244 +667,136 @@ impl FileTransferActivity { let sorting: FileSorting = match self.browser.tab() { FileExplorerTab::Local => self.local().get_file_sorting(), FileExplorerTab::Remote => self.remote().get_file_sorting(), - _ => panic!("You can't mount file sorting when in found result"), + _ => return, }; - let index: usize = match sorting { - FileSorting::CreationTime => 2, - FileSorting::ModifyTime => 1, - FileSorting::Name => 0, - FileSorting::Size => 3, - }; - self.mount_radio_dialog( - super::COMPONENT_RADIO_SORTING, - "Sort files by", - &["Name", "Modify time", "Creation time", "Size"], - index, - sorting_color, - ); + assert!(self + .app + .remount( + Id::SortingPopup, + Box::new(components::SortingPopup::new(sorting, sorting_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::SortingPopup).is_ok()); } pub(super) fn umount_file_sorting(&mut self) { - self.view.umount(super::COMPONENT_RADIO_SORTING); + let _ = self.app.umount(&Id::SortingPopup); } pub(super) fn mount_radio_delete(&mut self) { let warn_color = self.theme().misc_warn_dialog; - self.mount_radio_dialog( - super::COMPONENT_RADIO_DELETE, - "Delete file", - &["Yes", "No"], - 1, - warn_color, - ); + assert!(self + .app + .remount( + Id::DeletePopup, + Box::new(components::DeletePopup::new(warn_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::DeletePopup).is_ok()); } pub(super) fn umount_radio_delete(&mut self) { - self.view.umount(super::COMPONENT_RADIO_DELETE); + let _ = self.app.umount(&Id::DeletePopup); } pub(super) fn mount_radio_replace(&mut self, file_name: &str) { let warn_color = self.theme().misc_warn_dialog; - self.mount_radio_dialog( - super::COMPONENT_RADIO_REPLACE, - format!("File '{}' already exists. Overwrite file?", file_name), - &["Yes", "No"], - 0, - warn_color, - ); + assert!(self + .app + .remount( + Id::ReplacePopup, + Box::new(components::ReplacePopup::new(Some(file_name), warn_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::ReplacePopup).is_ok()); } pub(super) fn mount_radio_replace_many(&mut self, files: &[&str]) { let warn_color = self.theme().misc_warn_dialog; - // Make rows - let rows = files.iter().map(|x| vec![TextSpan::new(x)]).collect(); - self.view.mount( - super::COMPONENT_LIST_REPLACING_FILES, - Box::new(List::new( - ListPropsBuilder::default() - .with_borders(Borders::ALL, BorderType::Rounded, warn_color) - .scrollable(true) - .with_highlighted_color(warn_color) - .with_highlighted_str(Some("> ")) - .with_title( - "The following files are going to be replaced", - Alignment::Center, - ) - .with_foreground(warn_color) - .with_rows(rows) - .build(), - )), - ); - self.mount_radio_dialog( - super::COMPONENT_RADIO_REPLACE, - "Overwrite files?", - &["Yes", "No"], - 0, - warn_color, - ); + assert!(self + .app + .remount( + Id::ReplacingFilesListPopup, + Box::new(components::ReplacingFilesListPopup::new(files, warn_color)), + vec![], + ) + .is_ok()); + assert!(self + .app + .remount( + Id::ReplacePopup, + Box::new(components::ReplacePopup::new(None, warn_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::ReplacePopup).is_ok()); } /// ### is_radio_replace_extended /// /// Returns whether radio replace is in "extended" mode (for many files) pub(super) fn is_radio_replace_extended(&self) -> bool { - self.view - .get_state(super::COMPONENT_LIST_REPLACING_FILES) - .is_some() + self.app.mounted(&Id::ReplacingFilesListPopup) } pub(super) fn umount_radio_replace(&mut self) { - self.view.umount(super::COMPONENT_RADIO_REPLACE); - self.view.umount(super::COMPONENT_LIST_REPLACING_FILES); // NOTE: replace anyway + let _ = self.app.umount(&Id::ReplacePopup); + let _ = self.app.umount(&Id::ReplacingFilesListPopup); // NOTE: replace anyway } pub(super) fn mount_file_info(&mut self, file: &FsEntry) { - let mut texts: TableBuilder = TableBuilder::default(); - // Abs path - let real_path: Option = { - let real_file: FsEntry = file.get_realfile(); - match real_file.get_abs_path() != file.get_abs_path() { - true => Some(real_file.get_abs_path()), - false => None, - } - }; - let path: String = match real_path { - Some(symlink) => format!("{} -> {}", file.get_abs_path().display(), symlink.display()), - None => format!("{}", file.get_abs_path().display()), - }; - // Make texts - texts - .add_col(TextSpan::from("Path: ")) - .add_col(TextSpan::new(path.as_str()).fg(Color::Yellow)); - if let Some(filetype) = file.get_ftype() { - texts - .add_row() - .add_col(TextSpan::from("File type: ")) - .add_col(TextSpan::new(filetype.as_str()).fg(Color::LightGreen)); - } - let (bsize, size): (ByteSize, usize) = (ByteSize(file.get_size() as u64), file.get_size()); - texts - .add_row() - .add_col(TextSpan::from("Size: ")) - .add_col(TextSpan::new(format!("{} ({})", bsize, size).as_str()).fg(Color::Cyan)); - let ctime: String = fmt_time(file.get_creation_time(), "%b %d %Y %H:%M:%S"); - let atime: String = fmt_time(file.get_last_access_time(), "%b %d %Y %H:%M:%S"); - let mtime: String = fmt_time(file.get_creation_time(), "%b %d %Y %H:%M:%S"); - texts - .add_row() - .add_col(TextSpan::from("Creation time: ")) - .add_col(TextSpan::new(ctime.as_str()).fg(Color::LightGreen)); - texts - .add_row() - .add_col(TextSpan::from("Last modified time: ")) - .add_col(TextSpan::new(mtime.as_str()).fg(Color::LightBlue)); - texts - .add_row() - .add_col(TextSpan::from("Last access time: ")) - .add_col(TextSpan::new(atime.as_str()).fg(Color::LightRed)); - // User - #[cfg(target_family = "unix")] - let username: String = match file.get_user() { - Some(uid) => match get_user_by_uid(uid) { - Some(user) => user.name().to_string_lossy().to_string(), - None => uid.to_string(), - }, - None => String::from("0"), - }; - #[cfg(target_os = "windows")] - let username: String = format!("{}", file.get_user().unwrap_or(0)); - // Group - #[cfg(target_family = "unix")] - let group: String = match file.get_group() { - Some(gid) => match get_group_by_gid(gid) { - Some(group) => group.name().to_string_lossy().to_string(), - None => gid.to_string(), - }, - None => String::from("0"), - }; - #[cfg(target_os = "windows")] - let group: String = format!("{}", file.get_group().unwrap_or(0)); - texts - .add_row() - .add_col(TextSpan::from("User: ")) - .add_col(TextSpan::new(username.as_str()).fg(Color::LightYellow)); - texts - .add_row() - .add_col(TextSpan::from("Group: ")) - .add_col(TextSpan::new(group.as_str()).fg(Color::Blue)); - self.view.mount( - super::COMPONENT_LIST_FILEINFO, - Box::new(Table::new( - TablePropsBuilder::default() - .with_borders(Borders::ALL, BorderType::Rounded, Color::White) - .with_title(file.get_name(), Alignment::Left) - .with_table(texts.build()) - .build(), - )), - ); - self.view.active(super::COMPONENT_LIST_FILEINFO); + assert!(self + .app + .remount( + Id::FileInfoPopup, + Box::new(components::FileInfoPopup::new(file)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::FileInfoPopup).is_ok()); } pub(super) fn umount_file_info(&mut self) { - self.view.umount(super::COMPONENT_LIST_FILEINFO); + let _ = self.app.umount(&Id::FileInfoPopup); } pub(super) fn refresh_local_status_bar(&mut self) { let sorting_color = self.theme().transfer_status_sorting; let hidden_color = self.theme().transfer_status_hidden; - let local_bar_spans: Vec = vec![ - TextSpan::new("File sorting: ").fg(sorting_color), - TextSpan::new(Self::get_file_sorting_str(self.local().get_file_sorting())) - .fg(sorting_color) - .reversed(), - TextSpan::new(" Hidden files: ").fg(hidden_color), - TextSpan::new(Self::get_hidden_files_str( - self.local().hidden_files_visible(), - )) - .fg(hidden_color) - .reversed(), - ]; - if let Some(props) = self.view.get_props(super::COMPONENT_SPAN_STATUS_BAR_LOCAL) { - self.view.update( - super::COMPONENT_SPAN_STATUS_BAR_LOCAL, - SpanPropsBuilder::from(props) - .with_spans(local_bar_spans) - .build(), - ); - } + assert!(self + .app + .remount( + Id::StatusBarLocal, + Box::new(components::StatusBarLocal::new( + &self.browser, + sorting_color, + hidden_color + )), + vec![], + ) + .is_ok()); } pub(super) fn refresh_remote_status_bar(&mut self) { let sorting_color = self.theme().transfer_status_sorting; let hidden_color = self.theme().transfer_status_hidden; let sync_color = self.theme().transfer_status_sync_browsing; - let remote_bar_spans: Vec = vec![ - TextSpan::new("File sorting: ").fg(sorting_color), - TextSpan::new(Self::get_file_sorting_str(self.remote().get_file_sorting())) - .fg(sorting_color) - .reversed(), - TextSpan::new(" Hidden files: ").fg(hidden_color), - TextSpan::new(Self::get_hidden_files_str( - self.remote().hidden_files_visible(), - )) - .fg(hidden_color) - .reversed(), - TextSpan::new(" Sync Browsing: ").fg(sync_color), - TextSpan::new(match self.browser.sync_browsing { - true => "ON ", - false => "OFF", - }) - .fg(sync_color) - .reversed(), - ]; - if let Some(props) = self.view.get_props(super::COMPONENT_SPAN_STATUS_BAR_REMOTE) { - self.view.update( - super::COMPONENT_SPAN_STATUS_BAR_REMOTE, - SpanPropsBuilder::from(props) - .with_spans(remote_bar_spans) - .build(), - ); - } + assert!(self + .app + .remount( + Id::StatusBarRemote, + Box::new(components::StatusBarRemote::new( + &self.browser, + sorting_color, + hidden_color, + sync_color + )), + vec![], + ) + .is_ok()); } /// ### mount_help @@ -943,199 +804,165 @@ impl FileTransferActivity { /// Mount help pub(super) fn mount_help(&mut self) { let key_color = self.theme().misc_keys; - 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() - .scrollable(true) - .with_title("Help", Alignment::Center) - .with_rows( - TableBuilder::default() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Disconnect")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from( - " Switch between explorer and logs", - )) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Go to previous directory")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Change explorer tab")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Move up/down in list")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Enter directory")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Upload/Download file")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Toggle hidden files")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Change file sorting mode")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Copy")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Make directory")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Go to path")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Show help")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Show info about selected file")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Reload directory content")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Select file")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Create new file")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from( - " Open text file with preferred editor", - )) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Quit termscp")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Rename file")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Save file as")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Go to parent directory")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from( - " Open file with default application for file type", - )) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from( - " Open file with specified application", - )) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Execute shell command")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Toggle synchronized browsing")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Delete selected file")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Select all files")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Interrupt file transfer")) - .build(), - ) - .build(), - )), - ); - // Active help - self.view.active(super::COMPONENT_TEXT_HELP); + assert!(self + .app + .remount( + Id::KeybindingsPopup, + Box::new(components::KeybindingsPopup::new(key_color)), + vec![], + ) + .is_ok()); + assert!(self.app.active(&Id::KeybindingsPopup).is_ok()); } pub(super) fn umount_help(&mut self) { - self.view.umount(super::COMPONENT_TEXT_HELP); + let _ = self.app.umount(&Id::KeybindingsPopup); } - fn get_file_sorting_str(mode: FileSorting) -> &'static str { - match mode { - FileSorting::Name => "By name", - FileSorting::CreationTime => "By creation time", - FileSorting::ModifyTime => "By modify time", - FileSorting::Size => "By size", - } + fn mount_global_listener(&mut self) { + assert!(self + .app + .mount( + Id::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::Char('h'), + modifiers: KeyModifiers::NONE, + }), + Self::no_popup_mounted_clause(), + ), + Sub::new( + SubEventClause::Keyboard(KeyEvent { + code: Key::Char('q'), + modifiers: KeyModifiers::NONE, + }), + Self::no_popup_mounted_clause(), + ), + ] + ) + .is_ok()); } - fn get_hidden_files_str(show: bool) -> &'static str { - match show { - true => "Show", - false => "Hide", - } - } - - // -- 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(), + /// ### 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 { + SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::CopyPopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::DeletePopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::DisconnectPopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::ErrorPopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::ExecPopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::FatalPopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::FileInfoPopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::GotoPopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::KeybindingsPopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::MkdirPopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::NewfilePopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::OpenWithPopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::ProgressBarFull, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::ProgressBarPartial, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::ExplorerFind, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::QuitPopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::RenamePopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::ReplacePopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::SaveAsPopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::SortingPopup, + )))), + Box::new(SubClause::And( + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::FindPopup, + )))), + Box::new(SubClause::Not(Box::new(SubClause::IsMounted( + Id::WaitPopup, + )))), + )), + )), + )), + )), + )), + )), + )), + )), + )), + )), + )), + )), + )), + )), + )), + )), + )), + )), + )), )), - ); - // Give focus to error - self.view.active(id); - } - - fn mount_input_dialog(&mut self, id: &str, text: &str, val: &str, color: Color) { - self.view.mount( - id, - Box::new(Input::new( - InputPropsBuilder::default() - .with_foreground(color) - .with_label(text, Alignment::Center) - .with_borders(Borders::ALL, BorderType::Rounded, color) - .with_value(val.to_string()) - .build(), - )), - ); - self.view.active(id); - } - - fn mount_radio_dialog>( - &mut self, - id: &str, - text: S, - 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.as_ref(), Alignment::Center) - .with_options(opts) - .with_value(default) - .rewind(true) - .build(), - )), - ); - // Active - self.view.active(id); + ) } } diff --git a/src/ui/activities/setup/actions.rs b/src/ui/activities/setup/actions.rs index 6be4711..89e3696 100644 --- a/src/ui/activities/setup/actions.rs +++ b/src/ui/activities/setup/actions.rs @@ -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 = match self.view.get_state(super::COMPONENT_LIST_SSH_KEYS) { - Some(Payload::One(Value::Usize(idx))) => Some(idx), + let idx: Option = 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 { + 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(()), + } + } } diff --git a/src/ui/activities/setup/components/commons.rs b/src/ui/activities/setup/components/commons.rs new file mode 100644 index 0000000..3344f92 --- /dev/null +++ b/src/ui/activities/setup/components/commons.rs @@ -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>(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 for ErrorPopup { + fn on(&mut self, ev: Event) -> Option { + 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("").bold().fg(Color::Cyan), + TextSpan::new(" to show keybindings; ").bold(), + TextSpan::new("").bold().fg(Color::Cyan), + TextSpan::new(" to save parameters; ").bold(), + TextSpan::new("").bold().fg(Color::Cyan), + TextSpan::new(" to change panel").bold(), + ]), + } + } +} + +impl Component for Footer { + fn on(&mut self, _ev: Event) -> Option { + 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 for Header { + fn on(&mut self, _ev: Event) -> Option { + 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("").bold().fg(Color::Cyan)) + .add_col(TextSpan::from(" Exit setup")) + .add_row() + .add_col(TextSpan::new("").bold().fg(Color::Cyan)) + .add_col(TextSpan::from(" Change setup page")) + .add_row() + .add_col(TextSpan::new("").bold().fg(Color::Cyan)) + .add_col(TextSpan::from(" Change cursor")) + .add_row() + .add_col(TextSpan::new("").bold().fg(Color::Cyan)) + .add_col(TextSpan::from(" Change input field")) + .add_row() + .add_col(TextSpan::new("").bold().fg(Color::Cyan)) + .add_col(TextSpan::from(" Select / Dismiss popup")) + .add_row() + .add_col(TextSpan::new("").bold().fg(Color::Cyan)) + .add_col(TextSpan::from(" Delete SSH key")) + .add_row() + .add_col(TextSpan::new("").bold().fg(Color::Cyan)) + .add_col(TextSpan::from(" New SSH key")) + .add_row() + .add_col(TextSpan::new("").bold().fg(Color::Cyan)) + .add_col(TextSpan::from(" Revert changes")) + .add_row() + .add_col(TextSpan::new("").bold().fg(Color::Cyan)) + .add_col(TextSpan::from(" Save configuration")) + .build(), + ), + } + } +} + +impl Component for Keybindings { + fn on(&mut self, ev: Event) -> Option { + 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 for QuitPopup { + fn on(&mut self, ev: Event) -> Option { + 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 for SavePopup { + fn on(&mut self, ev: Event) -> Option { + 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, + } + } +} diff --git a/src/ui/activities/setup/components/config.rs b/src/ui/activities/setup/components/config.rs new file mode 100644 index 0000000..09bb4b7 --- /dev/null +++ b/src/ui/activities/setup/components/config.rs @@ -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 for CheckUpdates { + fn on(&mut self, ev: Event) -> Option { + 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 for DefaultProtocol { + fn on(&mut self, ev: Event) -> Option { + 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) -> 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 for GroupDirs { + fn on(&mut self, ev: Event) -> Option { + 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 for HiddenFiles { + fn on(&mut self, ev: Event) -> Option { + 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 for NotificationsEnabled { + fn on(&mut self, ev: Event) -> Option { + 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 for PromptOnFileReplace { + fn on(&mut self, ev: Event) -> Option { + 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 for LocalFileFmt { + fn on(&mut self, ev: Event) -> Option { + 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 for NotificationsThreshold { + fn on(&mut self, ev: Event) -> Option { + 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 for RemoteFileFmt { + fn on(&mut self, ev: Event) -> Option { + 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 for TextEditor { + fn on(&mut self, ev: Event) -> Option { + handle_input_ev( + self, + ev, + Msg::Config(ConfigMsg::TextEditorBlurDown), + Msg::Config(ConfigMsg::TextEditorBlurUp), + ) + } +} + +// -- event handler + +fn handle_input_ev( + component: &mut dyn Component, + ev: Event, + on_key_down: Msg, + on_key_up: Msg, +) -> Option { + 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, + ev: Event, + on_key_down: Msg, + on_key_up: Msg, +) -> Option { + 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, + } +} diff --git a/src/ui/activities/setup/components/mod.rs b/src/ui/activities/setup/components/mod.rs new file mode 100644 index 0000000..b1e3794 --- /dev/null +++ b/src/ui/activities/setup/components/mod.rs @@ -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 for GlobalListener { + fn on(&mut self, ev: Event) -> Option { + 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, + } + } +} diff --git a/src/ui/activities/setup/components/ssh.rs b/src/ui/activities/setup/components/ssh.rs new file mode 100644 index 0000000..cbd43d5 --- /dev/null +++ b/src/ui/activities/setup/components/ssh.rs @@ -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 for DelSshKeyPopup { + fn on(&mut self, ev: Event) -> Option { + 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 for SshKeys { + fn on(&mut self, ev: Event) -> Option { + 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 for SshHost { + fn on(&mut self, ev: Event) -> Option { + 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 for SshUsername { + fn on(&mut self, ev: Event) -> Option { + 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, + } + } +} diff --git a/src/ui/activities/setup/components/theme.rs b/src/ui/activities/setup/components/theme.rs new file mode 100644 index 0000000..3b93724 --- /dev/null +++ b/src/ui/activities/setup/components/theme.rs @@ -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 for AuthTitle { + fn on(&mut self, _ev: Event) -> Option { + 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 for MiscTitle { + fn on(&mut self, _ev: Event) -> Option { + 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 for TransferTitle { + fn on(&mut self, _ev: Event) -> Option { + 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 for TransferTitle2 { + fn on(&mut self, _ev: Event) -> Option { + 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 for AuthAddress { + fn on(&mut self, ev: Event) -> Option { + 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 for AuthBookmarks { + fn on(&mut self, ev: Event) -> Option { + 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 for AuthPassword { + fn on(&mut self, ev: Event) -> Option { + 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 for AuthPort { + fn on(&mut self, ev: Event) -> Option { + 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 for AuthProtocol { + fn on(&mut self, ev: Event) -> Option { + 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 for AuthRecentHosts { + fn on(&mut self, ev: Event) -> Option { + 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 for AuthUsername { + fn on(&mut self, ev: Event) -> Option { + 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 for ExplorerLocalBg { + fn on(&mut self, ev: Event) -> Option { + 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 for ExplorerLocalFg { + fn on(&mut self, ev: Event) -> Option { + 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 for ExplorerLocalHg { + fn on(&mut self, ev: Event) -> Option { + 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 for ExplorerRemoteBg { + fn on(&mut self, ev: Event) -> Option { + 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 for ExplorerRemoteFg { + fn on(&mut self, ev: Event) -> Option { + 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 for ExplorerRemoteHg { + fn on(&mut self, ev: Event) -> Option { + 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 for LogBg { + fn on(&mut self, ev: Event) -> Option { + 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 for LogWindow { + fn on(&mut self, ev: Event) -> Option { + 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 for MiscError { + fn on(&mut self, ev: Event) -> Option { + 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 for MiscInfo { + fn on(&mut self, ev: Event) -> Option { + 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 for MiscInput { + fn on(&mut self, ev: Event) -> Option { + 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 for MiscKeys { + fn on(&mut self, ev: Event) -> Option { + 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 for MiscQuit { + fn on(&mut self, ev: Event) -> Option { + 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 for MiscSave { + fn on(&mut self, ev: Event) -> Option { + 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 for MiscWarn { + fn on(&mut self, ev: Event) -> Option { + 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 for ProgBarFull { + fn on(&mut self, ev: Event) -> Option { + 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 for ProgBarPartial { + fn on(&mut self, ev: Event) -> Option { + 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 for StatusHidden { + fn on(&mut self, ev: Event) -> Option { + 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 for StatusSorting { + fn on(&mut self, ev: Event) -> Option { + 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 for StatusSync { + fn on(&mut self, ev: Event) -> Option { + 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 { + 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 for InputColor { + fn on(&mut self, ev: Event) -> Option { + 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, + } + } +} diff --git a/src/ui/activities/setup/config.rs b/src/ui/activities/setup/config.rs index 8e811c5..5575679 100644 --- a/src/ui/activities/setup/config.rs +++ b/src/ui/activities/setup/config.rs @@ -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 } diff --git a/src/ui/activities/setup/mod.rs b/src/ui/activities/setup/mod.rs index fc0730d..ba75b96 100644 --- a/src/ui/activities/setup/mod.rs +++ b/src/ui/activities/setup/mod.rs @@ -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, exit_reason: Option, context: Option, // 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 { // 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() } } diff --git a/src/ui/activities/setup/update.rs b/src/ui/activities/setup/update.rs index cbeae39..5e91aff 100644 --- a/src/ui/activities/setup/update.rs +++ b/src/ui/activities/setup/update.rs @@ -28,737 +28,446 @@ */ // locals use super::{ - SetupActivity, ViewLayout, COMPONENT_COLOR_AUTH_ADDR, COMPONENT_COLOR_AUTH_BOOKMARKS, - COMPONENT_COLOR_AUTH_PASSWORD, COMPONENT_COLOR_AUTH_PORT, COMPONENT_COLOR_AUTH_PROTOCOL, - COMPONENT_COLOR_AUTH_RECENTS, COMPONENT_COLOR_AUTH_USERNAME, COMPONENT_COLOR_MISC_ERROR, - COMPONENT_COLOR_MISC_INFO, COMPONENT_COLOR_MISC_INPUT, COMPONENT_COLOR_MISC_KEYS, - COMPONENT_COLOR_MISC_QUIT, COMPONENT_COLOR_MISC_SAVE, COMPONENT_COLOR_MISC_WARN, - COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG, COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG, - COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG, COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG, - COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG, COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG, - COMPONENT_COLOR_TRANSFER_LOG_BG, COMPONENT_COLOR_TRANSFER_LOG_WIN, - COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL, COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL, - COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN, COMPONENT_COLOR_TRANSFER_STATUS_SORTING, - COMPONENT_COLOR_TRANSFER_STATUS_SYNC, COMPONENT_INPUT_LOCAL_FILE_FMT, - COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD, COMPONENT_INPUT_REMOTE_FILE_FMT, - COMPONENT_INPUT_SSH_HOST, COMPONENT_INPUT_SSH_USERNAME, COMPONENT_INPUT_TEXT_EDITOR, - COMPONENT_LIST_SSH_KEYS, COMPONENT_RADIO_DEFAULT_PROTOCOL, COMPONENT_RADIO_DEL_SSH_KEY, - COMPONENT_RADIO_GROUP_DIRS, COMPONENT_RADIO_HIDDEN_FILES, - COMPONENT_RADIO_NOTIFICATIONS_ENABLED, COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE, - COMPONENT_RADIO_QUIT, COMPONENT_RADIO_SAVE, COMPONENT_RADIO_UPDATES, COMPONENT_TEXT_ERROR, - COMPONENT_TEXT_HELP, + CommonMsg, ConfigMsg, Id, IdConfig, IdSsh, IdTheme, Msg, SetupActivity, SshMsg, ThemeMsg, + ViewLayout, }; -use crate::ui::keymap::*; -use crate::utils::parser::parse_color; // ext -use tuirealm::{Msg, Payload, Update, Value}; +use tuirealm::Update; -impl Update for SetupActivity { +impl Update for SetupActivity { /// ### 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)> { - match self.layout { - ViewLayout::SetupForm => self.update_setup(msg), - ViewLayout::SshKeys => self.update_ssh_keys(msg), - ViewLayout::Theme => self.update_theme(msg), + fn update(&mut self, msg: Option) -> Option { + match msg.unwrap_or(Msg::None) { + Msg::Common(msg) => self.common_update(msg), + Msg::Config(msg) => self.config_update(msg), + Msg::Ssh(msg) => self.ssh_update(msg), + Msg::Theme(msg) => self.theme_update(msg), + Msg::None => None, } } } impl SetupActivity { - fn update_setup(&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, - Some(msg) => match msg { - // Input field - (COMPONENT_INPUT_TEXT_EDITOR, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_RADIO_DEFAULT_PROTOCOL); - None - } - (COMPONENT_RADIO_DEFAULT_PROTOCOL, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_RADIO_HIDDEN_FILES); - None - } - (COMPONENT_RADIO_HIDDEN_FILES, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_RADIO_UPDATES); - None - } - (COMPONENT_RADIO_UPDATES, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE); - None - } - (COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_RADIO_GROUP_DIRS); - None - } - (COMPONENT_RADIO_GROUP_DIRS, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_INPUT_LOCAL_FILE_FMT); - None - } - (COMPONENT_INPUT_LOCAL_FILE_FMT, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_INPUT_REMOTE_FILE_FMT); - None - } - (COMPONENT_INPUT_REMOTE_FILE_FMT, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_RADIO_NOTIFICATIONS_ENABLED); - None - } - (COMPONENT_RADIO_NOTIFICATIONS_ENABLED, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD); - None - } - (COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_INPUT_TEXT_EDITOR); - None - } - // Input field - (COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_RADIO_NOTIFICATIONS_ENABLED); - None - } - (COMPONENT_RADIO_NOTIFICATIONS_ENABLED, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_INPUT_REMOTE_FILE_FMT); - None - } - (COMPONENT_INPUT_REMOTE_FILE_FMT, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_INPUT_LOCAL_FILE_FMT); - None - } - (COMPONENT_INPUT_LOCAL_FILE_FMT, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_RADIO_GROUP_DIRS); - None - } - (COMPONENT_RADIO_GROUP_DIRS, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE); - None - } - (COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_RADIO_UPDATES); - None - } - (COMPONENT_RADIO_UPDATES, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_RADIO_HIDDEN_FILES); - None - } - (COMPONENT_RADIO_HIDDEN_FILES, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_RADIO_DEFAULT_PROTOCOL); - None - } - (COMPONENT_RADIO_DEFAULT_PROTOCOL, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_INPUT_TEXT_EDITOR); - None - } - (COMPONENT_INPUT_TEXT_EDITOR, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD); - None - } - // Error or - (COMPONENT_TEXT_ERROR, key) | (COMPONENT_TEXT_ERROR, key) - if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => - { - // Umount text error - self.umount_error(); - None - } - (COMPONENT_TEXT_ERROR, _) => None, - // Exit - (COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => { - // Save changes - if let Err(err) = self.action_save_all() { - self.mount_error(err.as_str()); - } - // Exit - self.exit_reason = Some(super::ExitReason::Quit); - None - } - (COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(1)))) => { - // Quit - self.exit_reason = Some(super::ExitReason::Quit); - self.umount_quit(); - None - } - (COMPONENT_RADIO_QUIT, Msg::OnSubmit(_)) => { - // Umount popup - self.umount_quit(); - None - } - (COMPONENT_RADIO_QUIT, _) => None, - // Close help - (COMPONENT_TEXT_HELP, key) | (COMPONENT_TEXT_HELP, key) - if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => - { - // Umount help - self.umount_help(); - None - } - (COMPONENT_TEXT_HELP, _) => None, - // Save popup - (COMPONENT_RADIO_SAVE, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => { - // Save config - if let Err(err) = self.action_save_all() { - self.mount_error(err.as_str()); - } - self.umount_save_popup(); - None - } - (COMPONENT_RADIO_SAVE, Msg::OnSubmit(_)) => { - // Umount radio save - self.umount_save_popup(); - None - } - (COMPONENT_RADIO_SAVE, _) => None, - // Detect config changed - (_, Msg::OnChange(_)) => { - // An input field has changed value; report config changed - self.set_config_changed(true); - None - } - // Show help - (_, key) if key == &MSG_KEY_CTRL_H => { - // Show help - self.mount_help(); - None - } - (_, key) if key == &MSG_KEY_TAB => { - // Change view - if let Err(err) = self.action_change_tab(ViewLayout::SshKeys) { - self.mount_error(err.as_str()); - } - None - } - // Revert changes - (_, key) if key == &MSG_KEY_CTRL_R => { - // Revert changes - if let Err(err) = self.action_reset_config() { - self.mount_error(err.as_str()); - } - None - } - // Save - (_, key) if key == &MSG_KEY_CTRL_S => { - // Show save - self.mount_save_popup(); - None - } - // - (_, key) if key == &MSG_KEY_ESC => { - self.action_on_esc(); - None - } - (_, _) => None, // Nothing to do - }, - } - } - - fn update_ssh_keys(&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, - Some(msg) => match msg { - // Error or - (COMPONENT_TEXT_ERROR, key) | (COMPONENT_TEXT_ERROR, key) - if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => - { - // Umount text error - self.umount_error(); - None - } - (COMPONENT_TEXT_ERROR, _) => None, - // Exit - (COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => { - // Save changes - if let Err(err) = self.action_save_all() { - self.mount_error(err.as_str()); - } - // Exit - self.exit_reason = Some(super::ExitReason::Quit); - None - } - (COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(1)))) => { - // Quit - self.exit_reason = Some(super::ExitReason::Quit); - self.umount_quit(); - None - } - (COMPONENT_RADIO_QUIT, Msg::OnSubmit(_)) => { - // Umount popup - self.umount_quit(); - None - } - (COMPONENT_RADIO_QUIT, _) => None, - // Close help - (COMPONENT_TEXT_HELP, key) | (COMPONENT_TEXT_HELP, key) - if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => - { - // Umount help - self.umount_help(); - None - } - (COMPONENT_TEXT_HELP, _) => None, - // Delete key - (COMPONENT_RADIO_DEL_SSH_KEY, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => { - // Delete key - self.action_delete_ssh_key(); - // Reload ssh keys - self.reload_ssh_keys(); - // Delete popup - self.umount_del_ssh_key(); - None - } - (COMPONENT_RADIO_DEL_SSH_KEY, Msg::OnSubmit(_)) => { - // Umount - self.umount_del_ssh_key(); - None - } - (COMPONENT_RADIO_DEL_SSH_KEY, _) => None, - // Save popup - (COMPONENT_RADIO_SAVE, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => { - // Save config - if let Err(err) = self.action_save_all() { - self.mount_error(err.as_str()); - } - self.umount_save_popup(); - None - } - (COMPONENT_RADIO_SAVE, Msg::OnSubmit(_)) => { - // Umount radio save - self.umount_save_popup(); - None - } - (COMPONENT_RADIO_SAVE, _) => None, - // Edit SSH Key - // Show help - (_, key) if key == &MSG_KEY_CTRL_H => { - // Show help - self.mount_help(); - None - } - // New key - (COMPONENT_INPUT_SSH_HOST, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_INPUT_SSH_USERNAME); - None - } - (COMPONENT_INPUT_SSH_USERNAME, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_INPUT_SSH_HOST); - None - } - // New key - (COMPONENT_INPUT_SSH_USERNAME, key) | (COMPONENT_INPUT_SSH_USERNAME, key) - if key == &MSG_KEY_UP || key == &MSG_KEY_TAB => - { - self.view.active(COMPONENT_INPUT_SSH_HOST); - None - } - (COMPONENT_INPUT_SSH_HOST, key) | (COMPONENT_INPUT_SSH_HOST, key) - if key == &MSG_KEY_UP || key == &MSG_KEY_TAB => - { - self.view.active(COMPONENT_INPUT_SSH_USERNAME); - None - } - // New key - (COMPONENT_INPUT_SSH_HOST, Msg::OnSubmit(_)) - | (COMPONENT_INPUT_SSH_USERNAME, Msg::OnSubmit(_)) => { - // Save ssh key - self.action_new_ssh_key(); - self.umount_new_ssh_key(); - self.reload_ssh_keys(); - None - } - // New key - (COMPONENT_INPUT_SSH_HOST, key) | (COMPONENT_INPUT_SSH_USERNAME, key) - if key == &MSG_KEY_ESC => - { - // Umount new ssh key - self.umount_new_ssh_key(); - None - } - // New key - (COMPONENT_LIST_SSH_KEYS, key) if key == &MSG_KEY_CTRL_N => { - // Show new key popup - self.mount_new_ssh_key(); - None - } - // Edit key - (COMPONENT_LIST_SSH_KEYS, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => { - // Edit ssh key - if let Err(err) = self.edit_ssh_key(*idx) { - self.mount_error(err.as_str()); - } - None - } - // Show delete - (COMPONENT_LIST_SSH_KEYS, key) | (COMPONENT_LIST_SSH_KEYS, key) - if key == &MSG_KEY_CTRL_E || key == &MSG_KEY_DEL => - { - // Show delete key - self.mount_del_ssh_key(); - None - } - (_, key) if key == &MSG_KEY_TAB => { - // Change view - if let Err(err) = self.action_change_tab(ViewLayout::Theme) { - self.mount_error(err.as_str()); - } - None - } - // Revert changes - (_, key) if key == &MSG_KEY_CTRL_R => { - // Revert changes - if let Err(err) = self.action_reset_config() { - self.mount_error(err.as_str()); - } - None - } - // Save - (_, key) if key == &MSG_KEY_CTRL_S => { - // Show save - self.mount_save_popup(); - None - } - // - (_, key) if key == &MSG_KEY_ESC => { - self.action_on_esc(); - None - } - (_, _) => None, // Nothing to do - }, - } - } - - fn update_theme(&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, - Some(msg) => match msg { - // Input fields - (COMPONENT_COLOR_AUTH_PROTOCOL, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_AUTH_ADDR); - None - } - (COMPONENT_COLOR_AUTH_ADDR, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_AUTH_PORT); - None - } - (COMPONENT_COLOR_AUTH_PORT, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_AUTH_USERNAME); - None - } - (COMPONENT_COLOR_AUTH_USERNAME, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_AUTH_PASSWORD); - None - } - (COMPONENT_COLOR_AUTH_PASSWORD, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_AUTH_BOOKMARKS); - None - } - (COMPONENT_COLOR_AUTH_BOOKMARKS, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_AUTH_RECENTS); - None - } - (COMPONENT_COLOR_AUTH_RECENTS, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_MISC_ERROR); - None - } - (COMPONENT_COLOR_MISC_ERROR, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_MISC_INFO); - None - } - (COMPONENT_COLOR_MISC_INFO, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_MISC_INPUT); - None - } - (COMPONENT_COLOR_MISC_INPUT, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_MISC_KEYS); - None - } - (COMPONENT_COLOR_MISC_KEYS, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_MISC_QUIT); - None - } - (COMPONENT_COLOR_MISC_QUIT, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_MISC_SAVE); - None - } - (COMPONENT_COLOR_MISC_SAVE, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_MISC_WARN); - None - } - (COMPONENT_COLOR_MISC_WARN, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG); - None - } - (COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG); - None - } - (COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG); - None - } - (COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG, key) if key == &MSG_KEY_DOWN => { - self.view - .active(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG); - None - } - (COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG, key) if key == &MSG_KEY_DOWN => { - self.view - .active(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG); - None - } - (COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG, key) if key == &MSG_KEY_DOWN => { - self.view - .active(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG); - None - } - (COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL); - None - } - (COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL); - None - } - (COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_TRANSFER_LOG_BG); - None - } - (COMPONENT_COLOR_TRANSFER_LOG_BG, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_TRANSFER_LOG_WIN); - None - } - (COMPONENT_COLOR_TRANSFER_LOG_WIN, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_TRANSFER_STATUS_SORTING); - None - } - (COMPONENT_COLOR_TRANSFER_STATUS_SORTING, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN); - None - } - (COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_TRANSFER_STATUS_SYNC); - None - } - (COMPONENT_COLOR_TRANSFER_STATUS_SYNC, key) if key == &MSG_KEY_DOWN => { - self.view.active(COMPONENT_COLOR_AUTH_PROTOCOL); - None - } - (COMPONENT_COLOR_AUTH_PROTOCOL, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_TRANSFER_STATUS_SYNC); - None - } - (COMPONENT_COLOR_AUTH_ADDR, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_AUTH_PROTOCOL); - None - } - (COMPONENT_COLOR_AUTH_PORT, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_AUTH_ADDR); - None - } - (COMPONENT_COLOR_AUTH_USERNAME, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_AUTH_PORT); - None - } - (COMPONENT_COLOR_AUTH_PASSWORD, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_AUTH_USERNAME); - None - } - (COMPONENT_COLOR_AUTH_BOOKMARKS, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_AUTH_PASSWORD); - None - } - (COMPONENT_COLOR_AUTH_RECENTS, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_AUTH_BOOKMARKS); - None - } - (COMPONENT_COLOR_MISC_ERROR, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_AUTH_RECENTS); - None - } - (COMPONENT_COLOR_MISC_INFO, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_MISC_ERROR); - None - } - (COMPONENT_COLOR_MISC_INPUT, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_MISC_INFO); - None - } - (COMPONENT_COLOR_MISC_KEYS, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_MISC_INPUT); - None - } - (COMPONENT_COLOR_MISC_QUIT, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_MISC_KEYS); - None - } - (COMPONENT_COLOR_MISC_SAVE, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_MISC_QUIT); - None - } - (COMPONENT_COLOR_MISC_WARN, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_MISC_SAVE); - None - } - (COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_MISC_WARN); - None - } - (COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG); - None - } - (COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG); - None - } - (COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG); - None - } - (COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG, key) if key == &MSG_KEY_UP => { - self.view - .active(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG); - None - } - (COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG, key) if key == &MSG_KEY_UP => { - self.view - .active(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG); - None - } - (COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL, key) if key == &MSG_KEY_UP => { - self.view - .active(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG); - None - } - (COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL); - None - } - (COMPONENT_COLOR_TRANSFER_LOG_BG, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL); - None - } - (COMPONENT_COLOR_TRANSFER_LOG_WIN, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_TRANSFER_LOG_BG); - None - } - (COMPONENT_COLOR_TRANSFER_STATUS_SORTING, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_TRANSFER_LOG_WIN); - None - } - (COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_TRANSFER_STATUS_SORTING); - None - } - (COMPONENT_COLOR_TRANSFER_STATUS_SYNC, key) if key == &MSG_KEY_UP => { - self.view.active(COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN); - None - } - // On color change - (component, Msg::OnChange(Payload::One(Value::Str(color)))) => { - if let Some(color) = parse_color(color) { - self.action_save_color(component, color); - // Set unsaved changes to true - self.set_config_changed(true); - } - None - } - // Error or - (COMPONENT_TEXT_ERROR, key) | (COMPONENT_TEXT_ERROR, key) - if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => - { - // Umount text error - self.umount_error(); - None - } - (COMPONENT_TEXT_ERROR, _) => None, - // Exit - (COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => { - // Save changes - if let Err(err) = self.action_save_all() { - self.mount_error(err.as_str()); - } - // Exit - self.exit_reason = Some(super::ExitReason::Quit); - None - } - (COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(1)))) => { - // Quit - self.exit_reason = Some(super::ExitReason::Quit); - self.umount_quit(); - None - } - (COMPONENT_RADIO_QUIT, Msg::OnSubmit(_)) => { - // Umount popup - self.umount_quit(); - None - } - (COMPONENT_RADIO_QUIT, _) => None, - // Close help - (COMPONENT_TEXT_HELP, key) | (COMPONENT_TEXT_HELP, key) - if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => - { - // Umount help - self.umount_help(); - None - } - (COMPONENT_TEXT_HELP, _) => None, - // Save popup - (COMPONENT_RADIO_SAVE, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => { - // Save config - if let Err(err) = self.action_save_all() { - self.mount_error(err.as_str()); - } - self.umount_save_popup(); - None - } - (COMPONENT_RADIO_SAVE, Msg::OnSubmit(_)) => { - // Umount radio save - self.umount_save_popup(); - None - } - (COMPONENT_RADIO_SAVE, _) => None, - // Edit SSH Key - // Show help - (_, key) if key == &MSG_KEY_CTRL_H => { - // Show help - self.mount_help(); - None - } - (_, key) if key == &MSG_KEY_TAB => { - // Change view - if let Err(err) = self.action_change_tab(ViewLayout::SetupForm) { - self.mount_error(err.as_str()); - } - None - } - // Revert changes - (_, key) if key == &MSG_KEY_CTRL_R => { - // Revert changes + fn common_update(&mut self, msg: CommonMsg) -> Option { + match msg { + CommonMsg::ChangeLayout => { + let new_layout = match self.layout { + ViewLayout::SetupForm => ViewLayout::SshKeys, + ViewLayout::SshKeys => ViewLayout::Theme, + ViewLayout::Theme => ViewLayout::SetupForm, + }; + if let Err(err) = self.action_change_tab(new_layout) { + self.mount_error(err.as_str()); + } + } + CommonMsg::CloseErrorPopup => { + self.umount_error(); + } + CommonMsg::CloseKeybindingsPopup => { + self.umount_help(); + } + CommonMsg::CloseQuitPopup => { + self.umount_quit(); + } + CommonMsg::CloseSavePopup => { + self.umount_save_popup(); + } + CommonMsg::Quit => { + self.exit_reason = Some(super::ExitReason::Quit); + } + CommonMsg::RevertChanges => match self.layout { + ViewLayout::Theme => { if let Err(err) = self.action_reset_theme() { - self.mount_error(err.as_str()); + self.mount_error(err); } - None } - // Save - (_, key) if key == &MSG_KEY_CTRL_S => { - // Show save - self.mount_save_popup(); - None + ViewLayout::SshKeys | ViewLayout::SetupForm => { + if let Err(err) = self.action_reset_config() { + self.mount_error(err); + } } - // - (_, key) if key == &MSG_KEY_ESC => { - self.action_on_esc(); - None - } - (_, _) => None, // Nothing to do }, + CommonMsg::SaveAndQuit => { + // Save changes + if let Err(err) = self.action_save_all() { + self.mount_error(err.as_str()); + } + // Exit + self.exit_reason = Some(super::ExitReason::Quit); + } + CommonMsg::SaveConfig => { + if let Err(err) = self.action_save_all() { + self.mount_error(err.as_str()); + } + self.umount_save_popup(); + } + CommonMsg::ShowKeybindings => { + self.mount_help(); + } + CommonMsg::ShowQuitPopup => { + self.action_on_esc(); + } + CommonMsg::ShowSavePopup => { + self.mount_save_popup(); + } } + None + } + + fn config_update(&mut self, msg: ConfigMsg) -> Option { + match msg { + ConfigMsg::CheckUpdatesBlurDown => { + assert!(self + .app + .active(&Id::Config(IdConfig::PromptOnFileReplace)) + .is_ok()); + } + ConfigMsg::CheckUpdatesBlurUp => { + assert!(self.app.active(&Id::Config(IdConfig::HiddenFiles)).is_ok()); + } + ConfigMsg::DefaultProtocolBlurDown => { + assert!(self.app.active(&Id::Config(IdConfig::HiddenFiles)).is_ok()); + } + ConfigMsg::DefaultProtocolBlurUp => { + assert!(self.app.active(&Id::Config(IdConfig::TextEditor)).is_ok()); + } + ConfigMsg::GroupDirsBlurDown => { + assert!(self.app.active(&Id::Config(IdConfig::LocalFileFmt)).is_ok()); + } + ConfigMsg::GroupDirsBlurUp => { + assert!(self + .app + .active(&Id::Config(IdConfig::PromptOnFileReplace)) + .is_ok()); + } + ConfigMsg::HiddenFilesBlurDown => { + assert!(self.app.active(&Id::Config(IdConfig::CheckUpdates)).is_ok()); + } + ConfigMsg::HiddenFilesBlurUp => { + assert!(self + .app + .active(&Id::Config(IdConfig::DefaultProtocol)) + .is_ok()); + } + ConfigMsg::LocalFileFmtBlurDown => { + assert!(self + .app + .active(&Id::Config(IdConfig::RemoteFileFmt)) + .is_ok()); + } + ConfigMsg::LocalFileFmtBlurUp => { + assert!(self.app.active(&Id::Config(IdConfig::GroupDirs)).is_ok()); + } + ConfigMsg::NotificationsEnabledBlurDown => { + assert!(self + .app + .active(&Id::Config(IdConfig::NotificationsThreshold)) + .is_ok()); + } + ConfigMsg::NotificationsEnabledBlurUp => { + assert!(self + .app + .active(&Id::Config(IdConfig::RemoteFileFmt)) + .is_ok()); + } + ConfigMsg::NotificationsThresholdBlurDown => { + assert!(self.app.active(&Id::Config(IdConfig::TextEditor)).is_ok()); + } + ConfigMsg::NotificationsThresholdBlurUp => { + assert!(self + .app + .active(&Id::Config(IdConfig::NotificationsEnabled)) + .is_ok()); + } + ConfigMsg::PromptOnFileReplaceBlurDown => { + assert!(self.app.active(&Id::Config(IdConfig::GroupDirs)).is_ok()); + } + ConfigMsg::PromptOnFileReplaceBlurUp => { + assert!(self.app.active(&Id::Config(IdConfig::CheckUpdates)).is_ok()); + } + ConfigMsg::RemoteFileFmtBlurDown => { + assert!(self + .app + .active(&Id::Config(IdConfig::NotificationsEnabled)) + .is_ok()); + } + ConfigMsg::RemoteFileFmtBlurUp => { + assert!(self.app.active(&Id::Config(IdConfig::LocalFileFmt)).is_ok()); + } + ConfigMsg::TextEditorBlurDown => { + assert!(self + .app + .active(&Id::Config(IdConfig::DefaultProtocol)) + .is_ok()); + } + ConfigMsg::TextEditorBlurUp => { + assert!(self + .app + .active(&Id::Config(IdConfig::NotificationsThreshold)) + .is_ok()); + } + ConfigMsg::ConfigChanged => { + self.set_config_changed(true); + } + } + None + } + + fn ssh_update(&mut self, msg: SshMsg) -> Option { + match msg { + SshMsg::CloseDelSshKeyPopup => { + self.umount_del_ssh_key(); + } + SshMsg::CloseNewSshKeyPopup => { + self.umount_new_ssh_key(); + } + SshMsg::DeleteSshKey => { + self.action_delete_ssh_key(); + self.umount_del_ssh_key(); + self.reload_ssh_keys(); + } + SshMsg::EditSshKey(i) => { + if let Err(err) = self.edit_ssh_key(i) { + self.mount_error(err.as_str()); + } + } + SshMsg::SaveSshKey => { + self.action_new_ssh_key(); + self.umount_new_ssh_key(); + self.reload_ssh_keys(); + } + SshMsg::ShowDelSshKeyPopup => { + self.mount_del_ssh_key(); + } + SshMsg::ShowNewSshKeyPopup => { + self.mount_new_ssh_key(); + } + SshMsg::SshHostBlur => { + assert!(self.app.active(&Id::Ssh(IdSsh::SshUsername)).is_ok()); + } + SshMsg::SshUsernameBlur => { + assert!(self.app.active(&Id::Ssh(IdSsh::SshHost)).is_ok()); + } + } + None + } + + fn theme_update(&mut self, msg: ThemeMsg) -> Option { + match msg { + ThemeMsg::AuthAddressBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::AuthPort)).is_ok()); + } + ThemeMsg::AuthAddressBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::AuthProtocol)).is_ok()); + } + ThemeMsg::AuthBookmarksBlurDown => { + assert!(self + .app + .active(&Id::Theme(IdTheme::AuthRecentHosts)) + .is_ok()); + } + ThemeMsg::AuthBookmarksBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::AuthPassword)).is_ok()); + } + ThemeMsg::AuthPasswordBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::AuthBookmarks)).is_ok()); + } + ThemeMsg::AuthPasswordBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::AuthUsername)).is_ok()); + } + ThemeMsg::AuthPortBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::AuthUsername)).is_ok()); + } + ThemeMsg::AuthPortBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::AuthAddress)).is_ok()); + } + ThemeMsg::AuthProtocolBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::AuthAddress)).is_ok()); + } + ThemeMsg::AuthProtocolBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::StatusSync)).is_ok()); + } + ThemeMsg::AuthRecentHostsBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::MiscError)).is_ok()); + } + ThemeMsg::AuthRecentHostsBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::AuthBookmarks)).is_ok()); + } + ThemeMsg::AuthUsernameBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::AuthPassword)).is_ok()); + } + ThemeMsg::AuthUsernameBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::AuthPort)).is_ok()); + } + ThemeMsg::MiscErrorBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::MiscInfo)).is_ok()); + } + ThemeMsg::MiscErrorBlurUp => { + assert!(self + .app + .active(&Id::Theme(IdTheme::AuthRecentHosts)) + .is_ok()); + } + ThemeMsg::MiscInfoBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::MiscInput)).is_ok()); + } + ThemeMsg::MiscInfoBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::MiscError)).is_ok()); + } + ThemeMsg::MiscInputBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::MiscKeys)).is_ok()); + } + ThemeMsg::MiscInputBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::MiscInfo)).is_ok()); + } + ThemeMsg::MiscKeysBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::MiscQuit)).is_ok()); + } + ThemeMsg::MiscKeysBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::MiscInput)).is_ok()); + } + ThemeMsg::MiscQuitBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::MiscSave)).is_ok()); + } + ThemeMsg::MiscQuitBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::MiscKeys)).is_ok()); + } + ThemeMsg::MiscSaveBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::MiscWarn)).is_ok()); + } + ThemeMsg::MiscSaveBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::MiscQuit)).is_ok()); + } + ThemeMsg::MiscWarnBlurDown => { + assert!(self + .app + .active(&Id::Theme(IdTheme::ExplorerLocalBg)) + .is_ok()); + } + ThemeMsg::MiscWarnBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::MiscSave)).is_ok()); + } + ThemeMsg::ExplorerLocalBgBlurDown => { + assert!(self + .app + .active(&Id::Theme(IdTheme::ExplorerLocalFg)) + .is_ok()); + } + ThemeMsg::ExplorerLocalBgBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::MiscWarn)).is_ok()); + } + ThemeMsg::ExplorerLocalFgBlurDown => { + assert!(self + .app + .active(&Id::Theme(IdTheme::ExplorerLocalHg)) + .is_ok()); + } + ThemeMsg::ExplorerLocalFgBlurUp => { + assert!(self + .app + .active(&Id::Theme(IdTheme::ExplorerLocalBg)) + .is_ok()); + } + ThemeMsg::ExplorerLocalHgBlurDown => { + assert!(self + .app + .active(&Id::Theme(IdTheme::ExplorerRemoteBg)) + .is_ok()); + } + ThemeMsg::ExplorerLocalHgBlurUp => { + assert!(self + .app + .active(&Id::Theme(IdTheme::ExplorerLocalFg)) + .is_ok()); + } + ThemeMsg::ExplorerRemoteBgBlurDown => { + assert!(self + .app + .active(&Id::Theme(IdTheme::ExplorerRemoteFg)) + .is_ok()); + } + ThemeMsg::ExplorerRemoteBgBlurUp => { + assert!(self + .app + .active(&Id::Theme(IdTheme::ExplorerLocalHg)) + .is_ok()); + } + ThemeMsg::ExplorerRemoteFgBlurDown => { + assert!(self + .app + .active(&Id::Theme(IdTheme::ExplorerRemoteHg)) + .is_ok()); + } + ThemeMsg::ExplorerRemoteFgBlurUp => { + assert!(self + .app + .active(&Id::Theme(IdTheme::ExplorerRemoteBg)) + .is_ok()); + } + ThemeMsg::ExplorerRemoteHgBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::ProgBarFull)).is_ok()); + } + ThemeMsg::ExplorerRemoteHgBlurUp => { + assert!(self + .app + .active(&Id::Theme(IdTheme::ExplorerRemoteFg)) + .is_ok()); + } + ThemeMsg::ProgBarFullBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::ProgBarPartial)).is_ok()); + } + ThemeMsg::ProgBarFullBlurUp => { + assert!(self + .app + .active(&Id::Theme(IdTheme::ExplorerRemoteHg)) + .is_ok()); + } + ThemeMsg::ProgBarPartialBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::LogBg)).is_ok()); + } + ThemeMsg::ProgBarPartialBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::ProgBarFull)).is_ok()); + } + ThemeMsg::LogBgBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::LogWindow)).is_ok()); + } + ThemeMsg::LogBgBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::ProgBarPartial)).is_ok()); + } + ThemeMsg::LogWindowBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::StatusSorting)).is_ok()); + } + ThemeMsg::LogWindowBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::LogBg)).is_ok()); + } + ThemeMsg::StatusSortingBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::StatusHidden)).is_ok()); + } + ThemeMsg::StatusSortingBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::LogWindow)).is_ok()); + } + ThemeMsg::StatusHiddenBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::StatusSync)).is_ok()); + } + ThemeMsg::StatusHiddenBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::StatusSorting)).is_ok()); + } + ThemeMsg::StatusSyncBlurDown => { + assert!(self.app.active(&Id::Theme(IdTheme::AuthProtocol)).is_ok()); + } + ThemeMsg::StatusSyncBlurUp => { + assert!(self.app.active(&Id::Theme(IdTheme::StatusHidden)).is_ok()); + } + ThemeMsg::ColorChanged(id, color) => { + self.action_save_color(id, color); + // Set unsaved changes to true + self.set_config_changed(true); + } + } + None } } diff --git a/src/ui/activities/setup/view/mod.rs b/src/ui/activities/setup/view/mod.rs index f3452be..4da7eaf 100644 --- a/src/ui/activities/setup/view/mod.rs +++ b/src/ui/activities/setup/view/mod.rs @@ -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>(&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("").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("").bold().fg(Color::Cyan)) - .add_col(TextSpan::from(" Exit setup")) - .add_row() - .add_col(TextSpan::new("").bold().fg(Color::Cyan)) - .add_col(TextSpan::from(" Change setup page")) - .add_row() - .add_col(TextSpan::new("").bold().fg(Color::Cyan)) - .add_col(TextSpan::from(" Change cursor")) - .add_row() - .add_col(TextSpan::new("").bold().fg(Color::Cyan)) - .add_col(TextSpan::from(" Change input field")) - .add_row() - .add_col(TextSpan::new("").bold().fg(Color::Cyan)) - .add_col(TextSpan::from(" Select / Dismiss popup")) - .add_row() - .add_col(TextSpan::new("").bold().fg(Color::Cyan)) - .add_col(TextSpan::from(" Delete SSH key")) - .add_row() - .add_col(TextSpan::new("").bold().fg(Color::Cyan)) - .add_col(TextSpan::from(" New SSH key")) - .add_row() - .add_col(TextSpan::new("").bold().fg(Color::Cyan)) - .add_col(TextSpan::from(" Revert changes")) - .add_row() - .add_col(TextSpan::new("").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, - value: Option, - ) { - 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 { + 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, + ))))), + )), + )), + )), + )), + ) } } diff --git a/src/ui/activities/setup/view/setup.rs b/src/ui/activities/setup/view/setup.rs index 618aab8..b80ea25 100644 --- a/src/ui/activities/setup/view/setup.rs +++ b/src/ui/activities/setup/view/setup.rs @@ -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 = 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); } diff --git a/src/ui/activities/setup/view/ssh_keys.rs b/src/ui/activities/setup/view/ssh_keys.rs index b7ddca9..2a50193 100644 --- a/src/ui/activities/setup/view/ssh_keys.rs +++ b/src/ui/activities/setup/view/ssh_keys.rs @@ -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 = 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 = 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()); } } diff --git a/src/ui/activities/setup/view/theme.rs b/src/ui/activities/setup/view/theme.rs index 2c51034..4fd0361 100644 --- a/src/ui/activities/setup/view/theme.rs +++ b/src/ui/activities/setup/view/theme.rs @@ -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 { - 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()); } } diff --git a/src/ui/components/bookmark_list.rs b/src/ui/components/bookmark_list.rs deleted file mode 100644 index 01d81c0..0000000 --- a/src/ui/components/bookmark_list.rs +++ /dev/null @@ -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, -} - -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 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>(&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) -> &mut Self { - if let Some(props) = self.props.as_mut() { - let bookmarks: Vec = 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 = 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)) - ); - } -} diff --git a/src/ui/components/bytes.rs b/src/ui/components/bytes.rs deleted file mode 100644 index 92d2a38..0000000 --- a/src/ui/components/bytes.rs +++ /dev/null @@ -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 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>(&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))) - ); - } -} diff --git a/src/ui/components/color_picker.rs b/src/ui/components/color_picker.rs deleted file mode 100644 index 36ae99d..0000000 --- a/src/ui/components/color_picker.rs +++ /dev/null @@ -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 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>(&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")))) - ); - } -} diff --git a/src/ui/components/file_list.rs b/src/ui/components/file_list.rs deleted file mode 100644 index 5982973..0000000 --- a/src/ui/components/file_list.rs +++ /dev/null @@ -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, -} - -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 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>(&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) -> &mut Self { - if let Some(props) = self.props.as_mut() { - let files: Vec = 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, // 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 { - 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 = 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))); - } -} diff --git a/src/ui/components/logbox.rs b/src/ui/components/logbox.rs deleted file mode 100644 index 41ac7d1..0000000 --- a/src/ui/components/logbox.rs +++ /dev/null @@ -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, -} - -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 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>(&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 = 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)) - ); - } -} diff --git a/src/ui/components/mod.rs b/src/ui/components/mod.rs deleted file mode 100644 index 342483d..0000000 --- a/src/ui/components/mod.rs +++ /dev/null @@ -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; diff --git a/src/ui/context.rs b/src/ui/context.rs index 37ce545..776cb38 100644 --- a/src/ui/context.rs +++ b/src/ui/context.rs @@ -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>; +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, config_client: ConfigClient, pub(crate) store: Store, - input_hnd: InputHandler, - pub(crate) terminal: TuiTerminal, + pub(crate) terminal: TerminalBridge, theme_provider: ThemeProvider, error: Option, } @@ -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 { 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(); } } diff --git a/src/ui/input.rs b/src/ui/input.rs deleted file mode 100644 index 083d488..0000000 --- a/src/ui/input.rs +++ /dev/null @@ -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, ()> { - let mut inbox: Vec = 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, ()> { - 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); - }*/ -} diff --git a/src/ui/keymap.rs b/src/ui/keymap.rs deleted file mode 100644 index 99bc124..0000000 --- a/src/ui/keymap.rs +++ /dev/null @@ -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, -}); diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 268a5ab..4b7cc90 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -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; diff --git a/src/utils/parser.rs b/src/utils/parser.rs index b6d8841..d640a8e 100644 --- a/src/utils/parser.rs +++ b/src/utils/parser.rs @@ -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()); } }