mirror of
https://github.com/veeso/termscp.git
synced 2025-12-07 09:36:00 -08:00
Migrated termscp to tui-realm 1.x
This commit is contained in:
committed by
Christian Visintin
parent
30851a78e8
commit
54b5583d1a
10
CHANGELOG.md
10
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
|
||||
|
||||
|
||||
27
Cargo.lock
generated
27
Cargo.lock
generated
@@ -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]]
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -39,7 +39,6 @@ use crate::ui::context::Context;
|
||||
|
||||
// Namespaces
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
/// ### NextActivity
|
||||
@@ -56,7 +55,7 @@ pub enum NextActivity {
|
||||
/// The activity manager takes care of running activities and handling them until the application has ended
|
||||
pub struct ActivityManager {
|
||||
context: Option<Context>,
|
||||
interval: Duration,
|
||||
ticks: Duration,
|
||||
local_dir: PathBuf,
|
||||
}
|
||||
|
||||
@@ -64,7 +63,7 @@ impl ActivityManager {
|
||||
/// ### new
|
||||
///
|
||||
/// Initializes a new Activity Manager
|
||||
pub fn new(local_dir: &Path, interval: Duration) -> Result<ActivityManager, HostError> {
|
||||
pub fn new(local_dir: &Path, ticks: Duration) -> Result<ActivityManager, HostError> {
|
||||
// Prepare Context
|
||||
// Initialize configuration client
|
||||
let (config_client, error): (ConfigClient, Option<String>) =
|
||||
@@ -80,7 +79,7 @@ impl ActivityManager {
|
||||
Ok(ActivityManager {
|
||||
context: Some(ctx),
|
||||
local_dir: local_dir.to_path_buf(),
|
||||
interval,
|
||||
ticks,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -123,7 +122,7 @@ impl ActivityManager {
|
||||
fn run_authentication(&mut self) -> Option<NextActivity> {
|
||||
info!("Starting AuthActivity...");
|
||||
// Prepare activity
|
||||
let mut activity: AuthActivity = AuthActivity::default();
|
||||
let mut activity: AuthActivity = AuthActivity::new(self.ticks);
|
||||
// Prepare result
|
||||
let result: Option<NextActivity>;
|
||||
// Get context
|
||||
@@ -162,8 +161,6 @@ impl ActivityManager {
|
||||
_ => { /* Nothing to do */ }
|
||||
}
|
||||
}
|
||||
// Sleep for ticks
|
||||
sleep(self.interval);
|
||||
}
|
||||
// Destroy activity
|
||||
self.context = activity.on_destroy();
|
||||
@@ -205,7 +202,8 @@ impl ActivityManager {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let mut activity: FileTransferActivity = FileTransferActivity::new(host, protocol);
|
||||
let mut activity: FileTransferActivity =
|
||||
FileTransferActivity::new(host, protocol, self.ticks);
|
||||
// Prepare result
|
||||
let result: Option<NextActivity>;
|
||||
// Create activity
|
||||
@@ -230,8 +228,6 @@ impl ActivityManager {
|
||||
_ => { /* Nothing to do */ }
|
||||
}
|
||||
}
|
||||
// Sleep for ticks
|
||||
sleep(self.interval);
|
||||
}
|
||||
// Destroy activity
|
||||
self.context = activity.on_destroy();
|
||||
@@ -245,7 +241,7 @@ impl ActivityManager {
|
||||
/// Returns the next activity to run
|
||||
fn run_setup(&mut self) -> Option<NextActivity> {
|
||||
// Prepare activity
|
||||
let mut activity: SetupActivity = SetupActivity::default();
|
||||
let mut activity: SetupActivity = SetupActivity::new(self.ticks);
|
||||
// Get context
|
||||
let ctx: Context = match self.context.take() {
|
||||
Some(ctx) => ctx,
|
||||
@@ -264,8 +260,6 @@ impl ActivityManager {
|
||||
info!("SetupActivity terminated due to 'Quit'");
|
||||
break;
|
||||
}
|
||||
// Sleep for ticks
|
||||
sleep(self.interval);
|
||||
}
|
||||
// Destroy activity
|
||||
self.context = activity.on_destroy();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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(""));
|
||||
}
|
||||
}
|
||||
|
||||
445
src/ui/activities/auth/components/bookmarks.rs
Normal file
445
src/ui/activities/auth/components/bookmarks.rs
Normal file
@@ -0,0 +1,445 @@
|
||||
//! ## Bookmarks
|
||||
//!
|
||||
//! auth activity bookmarks components
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
use super::Msg;
|
||||
|
||||
use tui_realm_stdlib::{Input, List, Radio};
|
||||
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
|
||||
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
|
||||
use tuirealm::props::{Alignment, BorderSides, BorderType, Borders, Color, InputType, TextSpan};
|
||||
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
|
||||
|
||||
// -- bookmark list
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct BookmarksList {
|
||||
component: List,
|
||||
}
|
||||
|
||||
impl BookmarksList {
|
||||
pub fn new(bookmarks: &[String], color: Color) -> Self {
|
||||
Self {
|
||||
component: List::default()
|
||||
.borders(Borders::default().color(color).modifiers(BorderType::Plain))
|
||||
.highlighted_color(color)
|
||||
.rewind(true)
|
||||
.scroll(true)
|
||||
.step(4)
|
||||
.title("Bookmarks", Alignment::Left)
|
||||
.rows(
|
||||
bookmarks
|
||||
.iter()
|
||||
.map(|x| vec![TextSpan::from(x.as_str())])
|
||||
.collect(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for BookmarksList {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Down, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Down));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
||||
self.perform(Cmd::Move(Direction::Up));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::PageDown,
|
||||
..
|
||||
}) => {
|
||||
self.perform(Cmd::Scroll(Direction::Down));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::PageUp, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Scroll(Direction::Up));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Home, ..
|
||||
}) => {
|
||||
self.perform(Cmd::GoTo(Position::Begin));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||
self.perform(Cmd::GoTo(Position::End));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => match self.state() {
|
||||
State::One(StateValue::Usize(choice)) => Some(Msg::LoadBookmark(choice)),
|
||||
_ => Some(Msg::None),
|
||||
},
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Right, ..
|
||||
}) => Some(Msg::BookmarksListBlur),
|
||||
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::BookmarksTabBlur),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Delete, ..
|
||||
}) => Some(Msg::ShowDeleteBookmarkPopup),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- recents list
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct RecentsList {
|
||||
component: List,
|
||||
}
|
||||
|
||||
impl RecentsList {
|
||||
pub fn new(bookmarks: &[String], color: Color) -> Self {
|
||||
Self {
|
||||
component: List::default()
|
||||
.borders(Borders::default().color(color).modifiers(BorderType::Plain))
|
||||
.highlighted_color(color)
|
||||
.rewind(true)
|
||||
.scroll(true)
|
||||
.step(4)
|
||||
.title("Recent connections", Alignment::Left)
|
||||
.rows(
|
||||
bookmarks
|
||||
.iter()
|
||||
.map(|x| vec![TextSpan::from(x.as_str())])
|
||||
.collect(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for RecentsList {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Down, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Down));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
||||
self.perform(Cmd::Move(Direction::Up));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::PageDown,
|
||||
..
|
||||
}) => {
|
||||
self.perform(Cmd::Scroll(Direction::Down));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::PageUp, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Scroll(Direction::Up));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Home, ..
|
||||
}) => {
|
||||
self.perform(Cmd::GoTo(Position::Begin));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||
self.perform(Cmd::GoTo(Position::End));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => match self.state() {
|
||||
State::One(StateValue::Usize(choice)) => Some(Msg::LoadRecent(choice)),
|
||||
_ => Some(Msg::None),
|
||||
},
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Left, ..
|
||||
}) => Some(Msg::RececentsListBlur),
|
||||
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::BookmarksTabBlur),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Delete, ..
|
||||
}) => Some(Msg::ShowDeleteRecentPopup),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- delete bookmark
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct DeleteBookmarkPopup {
|
||||
component: Radio,
|
||||
}
|
||||
|
||||
impl DeleteBookmarkPopup {
|
||||
pub fn new(color: Color) -> Self {
|
||||
Self {
|
||||
component: Radio::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.choices(&["Yes", "No"])
|
||||
.value(1)
|
||||
.rewind(true)
|
||||
.foreground(color)
|
||||
.title("Delete selected bookmark?", Alignment::Center),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for DeleteBookmarkPopup {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Msg::CloseDeleteBookmark),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Left, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Left));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Right, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Right));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => {
|
||||
if matches!(
|
||||
self.perform(Cmd::Submit),
|
||||
CmdResult::Submit(State::One(StateValue::Usize(0)))
|
||||
) {
|
||||
Some(Msg::DeleteBookmark)
|
||||
} else {
|
||||
Some(Msg::CloseDeleteBookmark)
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- delete recent
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct DeleteRecentPopup {
|
||||
component: Radio,
|
||||
}
|
||||
|
||||
impl DeleteRecentPopup {
|
||||
pub fn new(color: Color) -> Self {
|
||||
Self {
|
||||
component: Radio::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.choices(&["Yes", "No"])
|
||||
.value(1)
|
||||
.rewind(true)
|
||||
.foreground(color)
|
||||
.title("Delete selected recent host?", Alignment::Center),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for DeleteRecentPopup {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Msg::CloseDeleteRecent),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Left, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Left));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Right, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Right));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => {
|
||||
if matches!(
|
||||
self.perform(Cmd::Submit),
|
||||
CmdResult::Submit(State::One(StateValue::Usize(0)))
|
||||
) {
|
||||
Some(Msg::DeleteRecent)
|
||||
} else {
|
||||
Some(Msg::CloseDeleteRecent)
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- bookmark name
|
||||
|
||||
// -- save password
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct BookmarkSavePassword {
|
||||
component: Radio,
|
||||
}
|
||||
|
||||
impl BookmarkSavePassword {
|
||||
pub fn new(color: Color) -> Self {
|
||||
Self {
|
||||
component: Radio::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(Color::Reset)
|
||||
.sides(BorderSides::BOTTOM | BorderSides::LEFT | BorderSides::RIGHT)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.choices(&["Yes", "No"])
|
||||
.value(0)
|
||||
.rewind(true)
|
||||
.foreground(color)
|
||||
.title("Save password?", Alignment::Center),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for BookmarkSavePassword {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Msg::CloseSaveBookmark),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Left, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Left));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Right, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Right));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => Some(Msg::SaveBookmark),
|
||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::SaveBookmarkPasswordBlur),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- new bookmark name
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct BookmarkName {
|
||||
component: Input,
|
||||
}
|
||||
|
||||
impl BookmarkName {
|
||||
pub fn new(color: Color) -> Self {
|
||||
Self {
|
||||
component: Input::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(Color::Reset)
|
||||
.sides(BorderSides::TOP | BorderSides::LEFT | BorderSides::RIGHT)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.title("Bookmark name", Alignment::Left)
|
||||
.input_type(InputType::Text),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for BookmarkName {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Left, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Left));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Right, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Right));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Home, ..
|
||||
}) => {
|
||||
self.perform(Cmd::GoTo(Position::Begin));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||
self.perform(Cmd::GoTo(Position::End));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Delete, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Cancel);
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Backspace,
|
||||
..
|
||||
}) => {
|
||||
self.perform(Cmd::Delete);
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char(ch),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => {
|
||||
self.perform(Cmd::Type(ch));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => Some(Msg::SaveBookmark),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Down, ..
|
||||
}) => Some(Msg::BookmarkNameBlur),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
694
src/ui/activities/auth/components/form.rs
Normal file
694
src/ui/activities/auth/components/form.rs
Normal file
@@ -0,0 +1,694 @@
|
||||
//! ## Form
|
||||
//!
|
||||
//! auth activity components for file transfer params form
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
use super::{FileTransferProtocol, Msg};
|
||||
|
||||
use tui_realm_stdlib::{Input, Radio};
|
||||
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
|
||||
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
|
||||
use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style};
|
||||
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
|
||||
|
||||
// -- protocol
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct ProtocolRadio {
|
||||
component: Radio,
|
||||
}
|
||||
|
||||
impl ProtocolRadio {
|
||||
pub fn new(default_protocol: FileTransferProtocol, color: Color) -> Self {
|
||||
Self {
|
||||
component: Radio::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.choices(&["SFTP", "SCP", "FTP", "FTPS", "AWS S3"])
|
||||
.foreground(color)
|
||||
.rewind(true)
|
||||
.title("Protocol", Alignment::Left)
|
||||
.value(Self::protocol_enum_to_opt(default_protocol)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### protocol_opt_to_enum
|
||||
///
|
||||
/// Convert radio index for protocol into a `FileTransferProtocol`
|
||||
fn protocol_opt_to_enum(protocol: usize) -> FileTransferProtocol {
|
||||
match protocol {
|
||||
1 => FileTransferProtocol::Scp,
|
||||
2 => FileTransferProtocol::Ftp(false),
|
||||
3 => FileTransferProtocol::Ftp(true),
|
||||
4 => FileTransferProtocol::AwsS3,
|
||||
_ => FileTransferProtocol::Sftp,
|
||||
}
|
||||
}
|
||||
|
||||
/// ### protocol_enum_to_opt
|
||||
///
|
||||
/// Convert `FileTransferProtocol` enum into radio group index
|
||||
fn protocol_enum_to_opt(protocol: FileTransferProtocol) -> usize {
|
||||
match protocol {
|
||||
FileTransferProtocol::Sftp => 0,
|
||||
FileTransferProtocol::Scp => 1,
|
||||
FileTransferProtocol::Ftp(false) => 2,
|
||||
FileTransferProtocol::Ftp(true) => 3,
|
||||
FileTransferProtocol::AwsS3 => 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for ProtocolRadio {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
let result = match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Left, ..
|
||||
}) => self.perform(Cmd::Move(Direction::Left)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Right, ..
|
||||
}) => self.perform(Cmd::Move(Direction::Right)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => return Some(Msg::Connect),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Down, ..
|
||||
}) => return Some(Msg::ProtocolBlurDown),
|
||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => return Some(Msg::ProtocolBlurUp),
|
||||
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => return Some(Msg::ParamsFormBlur),
|
||||
_ => return None,
|
||||
};
|
||||
match result {
|
||||
CmdResult::Changed(State::One(StateValue::Usize(choice))) => {
|
||||
Some(Msg::ProtocolChanged(Self::protocol_opt_to_enum(choice)))
|
||||
}
|
||||
_ => Some(Msg::None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- address
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct InputAddress {
|
||||
component: Input,
|
||||
}
|
||||
|
||||
impl InputAddress {
|
||||
pub fn new(host: &str, color: Color) -> Self {
|
||||
Self {
|
||||
component: Input::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.placeholder("127.0.0.1", Style::default().fg(Color::Rgb(128, 128, 128)))
|
||||
.title("Remote host", Alignment::Left)
|
||||
.input_type(InputType::Text)
|
||||
.value(host),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for InputAddress {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Left, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Left));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Right, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Right));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Home, ..
|
||||
}) => {
|
||||
self.perform(Cmd::GoTo(Position::Begin));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||
self.perform(Cmd::GoTo(Position::End));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Delete, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Cancel);
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Backspace,
|
||||
..
|
||||
}) => {
|
||||
self.perform(Cmd::Delete);
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char(ch),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => {
|
||||
self.perform(Cmd::Type(ch));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => Some(Msg::Connect),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Down, ..
|
||||
}) => Some(Msg::AddressBlurDown),
|
||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::AddressBlurUp),
|
||||
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::ParamsFormBlur),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- port number
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct InputPort {
|
||||
component: Input,
|
||||
}
|
||||
|
||||
impl InputPort {
|
||||
pub fn new(port: u16, color: Color) -> Self {
|
||||
Self {
|
||||
component: Input::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.placeholder("22", Style::default().fg(Color::Rgb(128, 128, 128)))
|
||||
.input_type(InputType::UnsignedInteger)
|
||||
.input_len(5)
|
||||
.title("Port number", Alignment::Left)
|
||||
.value(port.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for InputPort {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Left, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Left));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Right, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Right));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Home, ..
|
||||
}) => {
|
||||
self.perform(Cmd::GoTo(Position::Begin));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||
self.perform(Cmd::GoTo(Position::End));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Delete, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Cancel);
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Backspace,
|
||||
..
|
||||
}) => {
|
||||
self.perform(Cmd::Delete);
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char(ch),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => {
|
||||
self.perform(Cmd::Type(ch));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => Some(Msg::Connect),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Down, ..
|
||||
}) => Some(Msg::PortBlurDown),
|
||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::PortBlurUp),
|
||||
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::ParamsFormBlur),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- username
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct InputUsername {
|
||||
component: Input,
|
||||
}
|
||||
|
||||
impl InputUsername {
|
||||
pub fn new(username: &str, color: Color) -> Self {
|
||||
Self {
|
||||
component: Input::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.placeholder("root", Style::default().fg(Color::Rgb(128, 128, 128)))
|
||||
.title("Username", Alignment::Left)
|
||||
.input_type(InputType::Text)
|
||||
.value(username),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for InputUsername {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Left, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Left));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Right, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Right));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Home, ..
|
||||
}) => {
|
||||
self.perform(Cmd::GoTo(Position::Begin));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||
self.perform(Cmd::GoTo(Position::End));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Delete, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Cancel);
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Backspace,
|
||||
..
|
||||
}) => {
|
||||
self.perform(Cmd::Delete);
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char(ch),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => {
|
||||
self.perform(Cmd::Type(ch));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => Some(Msg::Connect),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Down, ..
|
||||
}) => Some(Msg::UsernameBlurDown),
|
||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::UsernameBlurUp),
|
||||
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::ParamsFormBlur),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- password
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct InputPassword {
|
||||
component: Input,
|
||||
}
|
||||
|
||||
impl InputPassword {
|
||||
pub fn new(password: &str, color: Color) -> Self {
|
||||
Self {
|
||||
component: Input::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.title("Password", Alignment::Left)
|
||||
.input_type(InputType::Password('*'))
|
||||
.value(password),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for InputPassword {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Left, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Left));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Right, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Right));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Home, ..
|
||||
}) => {
|
||||
self.perform(Cmd::GoTo(Position::Begin));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||
self.perform(Cmd::GoTo(Position::End));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Delete, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Cancel);
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Backspace,
|
||||
..
|
||||
}) => {
|
||||
self.perform(Cmd::Delete);
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char(ch),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => {
|
||||
self.perform(Cmd::Type(ch));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => Some(Msg::Connect),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Down, ..
|
||||
}) => Some(Msg::PasswordBlurDown),
|
||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::PasswordBlurUp),
|
||||
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::ParamsFormBlur),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- s3 bucket
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct InputS3Bucket {
|
||||
component: Input,
|
||||
}
|
||||
|
||||
impl InputS3Bucket {
|
||||
pub fn new(bucket: &str, color: Color) -> Self {
|
||||
Self {
|
||||
component: Input::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.placeholder("my-bucket", Style::default().fg(Color::Rgb(128, 128, 128)))
|
||||
.title("Bucket name", Alignment::Left)
|
||||
.input_type(InputType::Text)
|
||||
.value(bucket),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for InputS3Bucket {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Left, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Left));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Right, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Right));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Home, ..
|
||||
}) => {
|
||||
self.perform(Cmd::GoTo(Position::Begin));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||
self.perform(Cmd::GoTo(Position::End));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Delete, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Cancel);
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Backspace,
|
||||
..
|
||||
}) => {
|
||||
self.perform(Cmd::Delete);
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char(ch),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => {
|
||||
self.perform(Cmd::Type(ch));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => Some(Msg::Connect),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Down, ..
|
||||
}) => Some(Msg::S3BucketBlurDown),
|
||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::S3BucketBlurUp),
|
||||
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::ParamsFormBlur),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- s3 bucket
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct InputS3Region {
|
||||
component: Input,
|
||||
}
|
||||
|
||||
impl InputS3Region {
|
||||
pub fn new(region: &str, color: Color) -> Self {
|
||||
Self {
|
||||
component: Input::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.placeholder("eu-west-1", Style::default().fg(Color::Rgb(128, 128, 128)))
|
||||
.title("Region", Alignment::Left)
|
||||
.input_type(InputType::Text)
|
||||
.value(region),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for InputS3Region {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Left, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Left));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Right, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Right));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Home, ..
|
||||
}) => {
|
||||
self.perform(Cmd::GoTo(Position::Begin));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||
self.perform(Cmd::GoTo(Position::End));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Delete, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Cancel);
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Backspace,
|
||||
..
|
||||
}) => {
|
||||
self.perform(Cmd::Delete);
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char(ch),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => {
|
||||
self.perform(Cmd::Type(ch));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => Some(Msg::Connect),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Down, ..
|
||||
}) => Some(Msg::S3RegionBlurDown),
|
||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::S3RegionBlurUp),
|
||||
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::ParamsFormBlur),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- s3 bucket
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct InputS3Profile {
|
||||
component: Input,
|
||||
}
|
||||
|
||||
impl InputS3Profile {
|
||||
pub fn new(profile: &str, color: Color) -> Self {
|
||||
Self {
|
||||
component: Input::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.placeholder("default", Style::default().fg(Color::Rgb(128, 128, 128)))
|
||||
.title("Profile", Alignment::Left)
|
||||
.input_type(InputType::Text)
|
||||
.value(profile),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for InputS3Profile {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Left, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Left));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Right, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Right));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Home, ..
|
||||
}) => {
|
||||
self.perform(Cmd::GoTo(Position::Begin));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||
self.perform(Cmd::GoTo(Position::End));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Delete, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Cancel);
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Backspace,
|
||||
..
|
||||
}) => {
|
||||
self.perform(Cmd::Delete);
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char(ch),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => {
|
||||
self.perform(Cmd::Type(ch));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => Some(Msg::Connect),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Down, ..
|
||||
}) => Some(Msg::S3ProfileBlurDown),
|
||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::S3ProfileBlurUp),
|
||||
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::ParamsFormBlur),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
91
src/ui/activities/auth/components/mod.rs
Normal file
91
src/ui/activities/auth/components/mod.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
//! ## Components
|
||||
//!
|
||||
//! auth activity components
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
use super::{FileTransferProtocol, Msg};
|
||||
|
||||
mod bookmarks;
|
||||
mod form;
|
||||
mod popup;
|
||||
mod text;
|
||||
|
||||
pub use bookmarks::{
|
||||
BookmarkName, BookmarkSavePassword, BookmarksList, DeleteBookmarkPopup, DeleteRecentPopup,
|
||||
RecentsList,
|
||||
};
|
||||
pub use form::{
|
||||
InputAddress, InputPassword, InputPort, InputS3Bucket, InputS3Profile, InputS3Region,
|
||||
InputUsername, ProtocolRadio,
|
||||
};
|
||||
pub use popup::{
|
||||
ErrorPopup, InfoPopup, InstallUpdatePopup, Keybindings, QuitPopup, ReleaseNotes, WaitPopup,
|
||||
WindowSizeError,
|
||||
};
|
||||
pub use text::{HelpText, NewVersionDisclaimer, Subtitle, Title};
|
||||
|
||||
use tui_realm_stdlib::Phantom;
|
||||
use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent};
|
||||
use tuirealm::{Component, MockComponent};
|
||||
|
||||
// -- global listener
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct GlobalListener {
|
||||
component: Phantom,
|
||||
}
|
||||
|
||||
impl Default for GlobalListener {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
component: Phantom::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for GlobalListener {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Msg::ShowQuitPopup),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('c'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
}) => Some(Msg::EnterSetup),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('h'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
}) => Some(Msg::ShowKeybindingsPopup),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('r'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
}) => Some(Msg::ShowReleaseNotes),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('s'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
}) => Some(Msg::ShowSaveBookmarkPopup),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
452
src/ui/activities/auth/components/popup.rs
Normal file
452
src/ui/activities/auth/components/popup.rs
Normal file
@@ -0,0 +1,452 @@
|
||||
//! ## Popup
|
||||
//!
|
||||
//! auth activity popups
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
use super::Msg;
|
||||
|
||||
use tui_realm_stdlib::{List, Paragraph, Radio, Textarea};
|
||||
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
|
||||
use tuirealm::event::{Key, KeyEvent};
|
||||
use tuirealm::props::{Alignment, BorderType, Borders, Color, TableBuilder, TextSpan};
|
||||
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
|
||||
|
||||
// -- error popup
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct ErrorPopup {
|
||||
component: Paragraph,
|
||||
}
|
||||
|
||||
impl ErrorPopup {
|
||||
pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self {
|
||||
Self {
|
||||
component: Paragraph::default()
|
||||
.alignment(Alignment::Center)
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.text(&[TextSpan::from(text.as_ref())])
|
||||
.wrap(true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for ErrorPopup {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Esc | Key::Enter,
|
||||
..
|
||||
}) => Some(Msg::CloseErrorPopup),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- info popup
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct InfoPopup {
|
||||
component: Paragraph,
|
||||
}
|
||||
|
||||
impl InfoPopup {
|
||||
pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self {
|
||||
Self {
|
||||
component: Paragraph::default()
|
||||
.alignment(Alignment::Center)
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.text(&[TextSpan::from(text.as_ref())])
|
||||
.wrap(true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for InfoPopup {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Esc | Key::Enter,
|
||||
..
|
||||
}) => Some(Msg::CloseInfoPopup),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- wait popup
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct WaitPopup {
|
||||
component: Paragraph,
|
||||
}
|
||||
|
||||
impl WaitPopup {
|
||||
pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self {
|
||||
Self {
|
||||
component: Paragraph::default()
|
||||
.alignment(Alignment::Center)
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.text(&[TextSpan::from(text.as_ref())])
|
||||
.wrap(true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for WaitPopup {
|
||||
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// -- window size error
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct WindowSizeError {
|
||||
component: Paragraph,
|
||||
}
|
||||
|
||||
impl WindowSizeError {
|
||||
pub fn new(color: Color) -> Self {
|
||||
Self {
|
||||
component: Paragraph::default()
|
||||
.alignment(Alignment::Center)
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.text(&[TextSpan::from(
|
||||
"termscp requires at least 24 lines of height to run",
|
||||
)])
|
||||
.wrap(true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for WindowSizeError {
|
||||
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// -- quit popup
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct QuitPopup {
|
||||
component: Radio,
|
||||
}
|
||||
|
||||
impl QuitPopup {
|
||||
pub fn new(color: Color) -> Self {
|
||||
Self {
|
||||
component: Radio::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.title("Quit termscp?", Alignment::Center)
|
||||
.rewind(true)
|
||||
.choices(&["Yes", "No"]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for QuitPopup {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Msg::CloseQuitPopup),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Left, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Left));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Right, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Right));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => {
|
||||
if matches!(
|
||||
self.perform(Cmd::Submit),
|
||||
CmdResult::Submit(State::One(StateValue::Usize(0)))
|
||||
) {
|
||||
Some(Msg::Quit)
|
||||
} else {
|
||||
Some(Msg::CloseQuitPopup)
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- install update popup
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct InstallUpdatePopup {
|
||||
component: Radio,
|
||||
}
|
||||
|
||||
impl InstallUpdatePopup {
|
||||
pub fn new(color: Color) -> Self {
|
||||
Self {
|
||||
component: Radio::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.title("Install update?", Alignment::Center)
|
||||
.rewind(true)
|
||||
.choices(&["Yes", "No"]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for InstallUpdatePopup {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Msg::CloseInstallUpdatePopup),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Left, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Left));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Right, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Right));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => {
|
||||
if matches!(
|
||||
self.perform(Cmd::Submit),
|
||||
CmdResult::Submit(State::One(StateValue::Usize(0)))
|
||||
) {
|
||||
Some(Msg::InstallUpdate)
|
||||
} else {
|
||||
Some(Msg::CloseInstallUpdatePopup)
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- release notes popup
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct ReleaseNotes {
|
||||
component: Textarea,
|
||||
}
|
||||
|
||||
impl ReleaseNotes {
|
||||
pub fn new(notes: &str, color: Color) -> Self {
|
||||
Self {
|
||||
component: Textarea::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.title("Release notes", Alignment::Center)
|
||||
.text_rows(
|
||||
notes
|
||||
.lines()
|
||||
.map(TextSpan::from)
|
||||
.collect::<Vec<TextSpan>>()
|
||||
.as_slice(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for ReleaseNotes {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Esc | Key::Enter,
|
||||
..
|
||||
}) => Some(Msg::CloseInstallUpdatePopup),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Down, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Down));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
||||
self.perform(Cmd::Move(Direction::Up));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::PageDown,
|
||||
..
|
||||
}) => {
|
||||
self.perform(Cmd::Scroll(Direction::Down));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::PageUp, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Scroll(Direction::Up));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Home, ..
|
||||
}) => {
|
||||
self.perform(Cmd::GoTo(Position::Begin));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||
self.perform(Cmd::GoTo(Position::End));
|
||||
Some(Msg::None)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- keybindings popup
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct Keybindings {
|
||||
component: List,
|
||||
}
|
||||
|
||||
impl Keybindings {
|
||||
pub fn new(color: Color) -> Self {
|
||||
Self {
|
||||
component: List::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.highlighted_str("? ")
|
||||
.title("Keybindings", Alignment::Center)
|
||||
.scroll(true)
|
||||
.step(4)
|
||||
.rows(
|
||||
TableBuilder::default()
|
||||
.add_col(TextSpan::new("<ESC>").bold().fg(color))
|
||||
.add_col(TextSpan::from(" Quit termscp"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::new("<TAB>").bold().fg(color))
|
||||
.add_col(TextSpan::from(" Switch from form and bookmarks"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::new("<RIGHT/LEFT>").bold().fg(color))
|
||||
.add_col(TextSpan::from(" Switch bookmark tab"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::new("<UP/DOWN>").bold().fg(color))
|
||||
.add_col(TextSpan::from(" Move up/down in current tab"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::new("<ENTER>").bold().fg(color))
|
||||
.add_col(TextSpan::from(" Connect/Load bookmark"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::new("<DEL|E>").bold().fg(color))
|
||||
.add_col(TextSpan::from(" Delete selected bookmark"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::new("<CTRL+C>").bold().fg(color))
|
||||
.add_col(TextSpan::from(" Enter setup"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::new("<CTRL+S>").bold().fg(color))
|
||||
.add_col(TextSpan::from(" Save bookmark"))
|
||||
.build(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for Keybindings {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Esc | Key::Enter,
|
||||
..
|
||||
}) => Some(Msg::CloseKeybindingsPopup),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Down, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Down));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
||||
self.perform(Cmd::Move(Direction::Up));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::PageDown,
|
||||
..
|
||||
}) => {
|
||||
self.perform(Cmd::Scroll(Direction::Down));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::PageUp, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Scroll(Direction::Up));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Home, ..
|
||||
}) => {
|
||||
self.perform(Cmd::GoTo(Position::Begin));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||
self.perform(Cmd::GoTo(Position::End));
|
||||
Some(Msg::None)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
132
src/ui/activities/auth/components/text.rs
Normal file
132
src/ui/activities/auth/components/text.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
//! ## Text
|
||||
//!
|
||||
//! auth activity texts
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
use super::Msg;
|
||||
|
||||
use tui_realm_stdlib::{Label, Span};
|
||||
use tuirealm::props::{Color, TextModifiers, TextSpan};
|
||||
use tuirealm::{Component, Event, MockComponent, NoUserEvent};
|
||||
|
||||
// -- Title
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct Title {
|
||||
component: Label,
|
||||
}
|
||||
|
||||
impl Default for Title {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
component: Label::default()
|
||||
.modifiers(TextModifiers::BOLD | TextModifiers::ITALIC)
|
||||
.text("$ termscp"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for Title {
|
||||
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// -- subtitle
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct Subtitle {
|
||||
component: Label,
|
||||
}
|
||||
|
||||
impl Default for Subtitle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
component: Label::default()
|
||||
.modifiers(TextModifiers::BOLD | TextModifiers::ITALIC)
|
||||
.text(format!("$ version {}", env!("CARGO_PKG_VERSION"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for Subtitle {
|
||||
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// -- new version disclaimer
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct NewVersionDisclaimer {
|
||||
component: Span,
|
||||
}
|
||||
|
||||
impl NewVersionDisclaimer {
|
||||
pub fn new(new_version: &str, color: Color) -> Self {
|
||||
Self {
|
||||
component: Span::default().foreground(color).spans(&[
|
||||
TextSpan::from("termscp "),
|
||||
TextSpan::new(new_version).underlined().bold(),
|
||||
TextSpan::from(
|
||||
" is NOW available! Install update and view release notes with <CTRL+R>",
|
||||
),
|
||||
]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for NewVersionDisclaimer {
|
||||
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// -- HelpText
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct HelpText {
|
||||
component: Span,
|
||||
}
|
||||
|
||||
impl HelpText {
|
||||
pub fn new(key_color: Color) -> Self {
|
||||
Self {
|
||||
component: Span::default().spans(&[
|
||||
TextSpan::new("Press ").bold(),
|
||||
TextSpan::new("<CTRL+H>").bold().fg(key_color),
|
||||
TextSpan::new(" to show keybindings; ").bold(),
|
||||
TextSpan::new("<CTRL+C>").bold().fg(key_color),
|
||||
TextSpan::new(" to enter setup").bold(),
|
||||
]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for HelpText {
|
||||
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -31,32 +31,6 @@ use crate::system::auto_update::{Release, Update, UpdateStatus};
|
||||
use crate::system::notifications::Notification;
|
||||
|
||||
impl AuthActivity {
|
||||
/// ### protocol_opt_to_enum
|
||||
///
|
||||
/// Convert radio index for protocol into a `FileTransferProtocol`
|
||||
pub(super) fn protocol_opt_to_enum(protocol: usize) -> FileTransferProtocol {
|
||||
match protocol {
|
||||
1 => FileTransferProtocol::Scp,
|
||||
2 => FileTransferProtocol::Ftp(false),
|
||||
3 => FileTransferProtocol::Ftp(true),
|
||||
4 => FileTransferProtocol::AwsS3,
|
||||
_ => FileTransferProtocol::Sftp,
|
||||
}
|
||||
}
|
||||
|
||||
/// ### protocol_enum_to_opt
|
||||
///
|
||||
/// Convert `FileTransferProtocol` enum into radio group index
|
||||
pub(super) fn protocol_enum_to_opt(protocol: FileTransferProtocol) -> usize {
|
||||
match protocol {
|
||||
FileTransferProtocol::Sftp => 0,
|
||||
FileTransferProtocol::Scp => 1,
|
||||
FileTransferProtocol::Ftp(false) => 2,
|
||||
FileTransferProtocol::Ftp(true) => 3,
|
||||
FileTransferProtocol::AwsS3 => 4,
|
||||
}
|
||||
}
|
||||
|
||||
/// ### get_default_port_for_protocol
|
||||
///
|
||||
/// Get the default port for protocol
|
||||
@@ -91,9 +65,8 @@ impl AuthActivity {
|
||||
///
|
||||
/// Collect host params as `FileTransferParams`
|
||||
pub(super) fn collect_host_params(&self) -> Result<FileTransferParams, &'static str> {
|
||||
let protocol: FileTransferProtocol = self.get_protocol();
|
||||
match protocol {
|
||||
FileTransferProtocol::AwsS3 => self.collect_s3_host_params(protocol),
|
||||
match self.protocol {
|
||||
FileTransferProtocol::AwsS3 => self.collect_s3_host_params(),
|
||||
protocol => self.collect_generic_host_params(protocol),
|
||||
}
|
||||
}
|
||||
@@ -135,10 +108,7 @@ impl AuthActivity {
|
||||
/// ### collect_s3_host_params
|
||||
///
|
||||
/// Get input values from fields or return an error if fields are invalid to work as aws s3
|
||||
pub(super) fn collect_s3_host_params(
|
||||
&self,
|
||||
protocol: FileTransferProtocol,
|
||||
) -> Result<FileTransferParams, &'static str> {
|
||||
pub(super) fn collect_s3_host_params(&self) -> Result<FileTransferParams, &'static str> {
|
||||
let (bucket, region, profile): (String, String, Option<String>) =
|
||||
self.get_s3_params_input();
|
||||
if bucket.is_empty() {
|
||||
@@ -148,7 +118,7 @@ impl AuthActivity {
|
||||
return Err("Invalid region");
|
||||
}
|
||||
Ok(FileTransferParams {
|
||||
protocol,
|
||||
protocol: FileTransferProtocol::AwsS3,
|
||||
params: ProtocolParams::AwsS3(AwsS3Params::new(bucket, region, profile)),
|
||||
entry_directory: None,
|
||||
})
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
*/
|
||||
// Sub modules
|
||||
mod bookmarks;
|
||||
mod components;
|
||||
mod misc;
|
||||
mod update;
|
||||
mod view;
|
||||
@@ -39,37 +40,101 @@ use crate::system::bookmarks_client::BookmarksClient;
|
||||
use crate::system::config_client::ConfigClient;
|
||||
|
||||
// Includes
|
||||
use crossterm::event::Event;
|
||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||
use tuirealm::{Update, View};
|
||||
use std::time::Duration;
|
||||
use tuirealm::listener::EventListenerCfg;
|
||||
use tuirealm::{application::PollStrategy, Application, NoUserEvent, Update};
|
||||
|
||||
// -- components
|
||||
const COMPONENT_TEXT_H1: &str = "TEXT_H1";
|
||||
const COMPONENT_TEXT_H2: &str = "TEXT_H2";
|
||||
const COMPONENT_TEXT_NEW_VERSION: &str = "TEXT_NEW_VERSION";
|
||||
const COMPONENT_TEXT_NEW_VERSION_NOTES: &str = "TEXTAREA_NEW_VERSION";
|
||||
const COMPONENT_TEXT_FOOTER: &str = "TEXT_FOOTER";
|
||||
const COMPONENT_TEXT_HELP: &str = "TEXT_HELP";
|
||||
const COMPONENT_TEXT_ERROR: &str = "TEXT_ERROR";
|
||||
const COMPONENT_TEXT_INFO: &str = "TEXT_INFO";
|
||||
const COMPONENT_TEXT_WAIT: &str = "TEXT_WAIT";
|
||||
const COMPONENT_TEXT_SIZE_ERR: &str = "TEXT_SIZE_ERR";
|
||||
const COMPONENT_INPUT_ADDR: &str = "INPUT_ADDRESS";
|
||||
const COMPONENT_INPUT_PORT: &str = "INPUT_PORT";
|
||||
const COMPONENT_INPUT_USERNAME: &str = "INPUT_USERNAME";
|
||||
const COMPONENT_INPUT_PASSWORD: &str = "INPUT_PASSWORD";
|
||||
const COMPONENT_INPUT_BOOKMARK_NAME: &str = "INPUT_BOOKMARK_NAME";
|
||||
const COMPONENT_INPUT_S3_BUCKET: &str = "INPUT_S3_BUCKET";
|
||||
const COMPONENT_INPUT_S3_REGION: &str = "INPUT_S3_REGION";
|
||||
const COMPONENT_INPUT_S3_PROFILE: &str = "INPUT_S3_PROFILE";
|
||||
const COMPONENT_RADIO_PROTOCOL: &str = "RADIO_PROTOCOL";
|
||||
const COMPONENT_RADIO_QUIT: &str = "RADIO_QUIT";
|
||||
const COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK: &str = "RADIO_DELETE_BOOKMARK";
|
||||
const COMPONENT_RADIO_BOOKMARK_DEL_RECENT: &str = "RADIO_DELETE_RECENT";
|
||||
const COMPONENT_RADIO_BOOKMARK_SAVE_PWD: &str = "RADIO_SAVE_PASSWORD";
|
||||
const COMPONENT_RADIO_INSTALL_UPDATE: &str = "RADIO_INSTALL_UPDATE";
|
||||
const COMPONENT_BOOKMARKS_LIST: &str = "BOOKMARKS_LIST";
|
||||
const COMPONENT_RECENTS_LIST: &str = "RECENTS_LIST";
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
||||
pub enum Id {
|
||||
Address,
|
||||
BookmarkName,
|
||||
BookmarkSavePassword,
|
||||
BookmarksList,
|
||||
DeleteBookmarkPopup,
|
||||
DeleteRecentPopup,
|
||||
ErrorPopup,
|
||||
GlobalListener,
|
||||
HelpText,
|
||||
InfoPopup,
|
||||
InstallUpdatePopup,
|
||||
Keybindings,
|
||||
NewVersionChangelog,
|
||||
NewVersionDisclaimer,
|
||||
Password,
|
||||
Port,
|
||||
Protocol,
|
||||
QuitPopup,
|
||||
RecentsList,
|
||||
S3Bucket,
|
||||
S3Profile,
|
||||
S3Region,
|
||||
Subtitle,
|
||||
Title,
|
||||
Username,
|
||||
WaitPopup,
|
||||
WindowSizeError,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Msg {
|
||||
AddressBlurDown,
|
||||
AddressBlurUp,
|
||||
BookmarksListBlur,
|
||||
BookmarksTabBlur,
|
||||
CloseDeleteBookmark,
|
||||
CloseDeleteRecent,
|
||||
CloseErrorPopup,
|
||||
CloseInfoPopup,
|
||||
CloseInstallUpdatePopup,
|
||||
CloseKeybindingsPopup,
|
||||
CloseQuitPopup,
|
||||
CloseSaveBookmark,
|
||||
Connect,
|
||||
DeleteBookmark,
|
||||
DeleteRecent,
|
||||
EnterSetup,
|
||||
InstallUpdate,
|
||||
LoadBookmark(usize),
|
||||
LoadRecent(usize),
|
||||
ParamsFormBlur,
|
||||
PasswordBlurDown,
|
||||
PasswordBlurUp,
|
||||
PortBlurDown,
|
||||
PortBlurUp,
|
||||
ProtocolBlurDown,
|
||||
ProtocolBlurUp,
|
||||
ProtocolChanged(FileTransferProtocol),
|
||||
Quit,
|
||||
RececentsListBlur,
|
||||
S3BucketBlurDown,
|
||||
S3BucketBlurUp,
|
||||
S3ProfileBlurDown,
|
||||
S3ProfileBlurUp,
|
||||
S3RegionBlurDown,
|
||||
S3RegionBlurUp,
|
||||
SaveBookmark,
|
||||
BookmarkNameBlur,
|
||||
SaveBookmarkPasswordBlur,
|
||||
ShowDeleteBookmarkPopup,
|
||||
ShowDeleteRecentPopup,
|
||||
ShowKeybindingsPopup,
|
||||
ShowQuitPopup,
|
||||
ShowReleaseNotes,
|
||||
ShowSaveBookmarkPopup,
|
||||
UsernameBlurDown,
|
||||
UsernameBlurUp,
|
||||
None,
|
||||
}
|
||||
|
||||
/// ## InputMask
|
||||
///
|
||||
/// Auth form input mask
|
||||
#[derive(Eq, PartialEq)]
|
||||
enum InputMask {
|
||||
Generic,
|
||||
AwsS3,
|
||||
}
|
||||
|
||||
// Store keys
|
||||
const STORE_KEY_LATEST_VERSION: &str = "AUTH_LATEST_VERSION";
|
||||
@@ -79,34 +144,39 @@ const STORE_KEY_RELEASE_NOTES: &str = "AUTH_RELEASE_NOTES";
|
||||
///
|
||||
/// AuthActivity is the data holder for the authentication activity
|
||||
pub struct AuthActivity {
|
||||
exit_reason: Option<ExitReason>,
|
||||
context: Option<Context>,
|
||||
view: View,
|
||||
app: Application<Id, Msg, NoUserEvent>,
|
||||
bookmarks_client: Option<BookmarksClient>,
|
||||
redraw: bool, // Should ui actually be redrawned?
|
||||
bookmarks_list: Vec<String>, // List of bookmarks
|
||||
recents_list: Vec<String>, // list of recents
|
||||
}
|
||||
|
||||
impl Default for AuthActivity {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
/// List of bookmarks
|
||||
bookmarks_list: Vec<String>,
|
||||
/// List of recent hosts
|
||||
recents_list: Vec<String>,
|
||||
/// Exit reason
|
||||
exit_reason: Option<ExitReason>,
|
||||
/// Should redraw ui
|
||||
redraw: bool,
|
||||
/// Protocol
|
||||
protocol: FileTransferProtocol,
|
||||
context: Option<Context>,
|
||||
}
|
||||
|
||||
impl AuthActivity {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiates a new AuthActivity
|
||||
pub fn new() -> AuthActivity {
|
||||
pub fn new(ticks: Duration) -> AuthActivity {
|
||||
AuthActivity {
|
||||
exit_reason: None,
|
||||
app: Application::init(
|
||||
EventListenerCfg::default()
|
||||
.default_input_listener(ticks)
|
||||
.poll_timeout(ticks),
|
||||
),
|
||||
context: None,
|
||||
view: View::init(),
|
||||
bookmarks_client: None,
|
||||
redraw: true, // True at startup
|
||||
bookmarks_list: Vec::new(),
|
||||
exit_reason: None,
|
||||
recents_list: Vec::new(),
|
||||
redraw: true,
|
||||
protocol: FileTransferProtocol::Sftp,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,9 +212,11 @@ impl AuthActivity {
|
||||
///
|
||||
/// Get current input mask to show
|
||||
fn input_mask(&self) -> InputMask {
|
||||
match self.get_protocol() {
|
||||
match self.protocol {
|
||||
FileTransferProtocol::AwsS3 => InputMask::AwsS3,
|
||||
_ => InputMask::Generic,
|
||||
FileTransferProtocol::Ftp(_)
|
||||
| FileTransferProtocol::Scp
|
||||
| FileTransferProtocol::Sftp => InputMask::Generic,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,9 +234,11 @@ impl Activity for AuthActivity {
|
||||
// Set context
|
||||
self.context = Some(context);
|
||||
// Clear terminal
|
||||
self.context_mut().clear_screen();
|
||||
if let Err(err) = self.context_mut().terminal().clear_screen() {
|
||||
error!("Failed to clear screen: {}", err);
|
||||
}
|
||||
// Put raw mode on enabled
|
||||
if let Err(err) = enable_raw_mode() {
|
||||
if let Err(err) = self.context_mut().terminal().enable_raw_mode() {
|
||||
error!("Failed to enter raw mode: {}", err);
|
||||
}
|
||||
// If check for updates is enabled, check for updates
|
||||
@@ -194,24 +268,23 @@ impl Activity for AuthActivity {
|
||||
if self.context.is_none() {
|
||||
return;
|
||||
}
|
||||
// Read one event
|
||||
if let Ok(Some(event)) = self.context().input_hnd().read_event() {
|
||||
// Set redraw to true
|
||||
self.redraw = true;
|
||||
// Handle on resize
|
||||
if let Event::Resize(_, h) = event {
|
||||
self.check_minimum_window_size(h);
|
||||
// Tick
|
||||
match self.app.tick(PollStrategy::UpTo(3)) {
|
||||
Ok(messages) => {
|
||||
for msg in messages.into_iter() {
|
||||
let mut msg = Some(msg);
|
||||
while msg.is_some() {
|
||||
msg = self.update(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
self.mount_error(format!("Application error: {}", err));
|
||||
}
|
||||
// Handle event on view and update
|
||||
let msg = self.view.on(event);
|
||||
self.update(msg);
|
||||
}
|
||||
// Redraw if necessary
|
||||
// View
|
||||
if self.redraw {
|
||||
// View
|
||||
self.view();
|
||||
// Set redraw to false
|
||||
self.redraw = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,26 +304,12 @@ impl Activity for AuthActivity {
|
||||
/// This function finally releases the context
|
||||
fn on_destroy(&mut self) -> Option<Context> {
|
||||
// Disable raw mode
|
||||
if let Err(err) = disable_raw_mode() {
|
||||
if let Err(err) = self.context_mut().terminal().disable_raw_mode() {
|
||||
error!("Failed to disable raw mode: {}", err);
|
||||
}
|
||||
self.context.as_ref()?;
|
||||
// Clear terminal and return
|
||||
match self.context.take() {
|
||||
Some(mut ctx) => {
|
||||
ctx.clear_screen();
|
||||
Some(ctx)
|
||||
}
|
||||
None => None,
|
||||
if let Err(err) = self.context_mut().terminal().clear_screen() {
|
||||
error!("Failed to clear screen: {}", err);
|
||||
}
|
||||
self.context.take()
|
||||
}
|
||||
}
|
||||
|
||||
/// ## InputMask
|
||||
///
|
||||
/// Auth form input mask
|
||||
#[derive(Eq, PartialEq)]
|
||||
enum InputMask {
|
||||
Generic,
|
||||
AwsS3,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! ## AuthActivity
|
||||
//! ## Update
|
||||
//!
|
||||
//! `auth_activity` is the module which implements the authentication activity
|
||||
//! Update impl
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
@@ -25,415 +25,222 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// locals
|
||||
use super::{
|
||||
AuthActivity, FileTransferProtocol, InputMask, COMPONENT_BOOKMARKS_LIST, COMPONENT_INPUT_ADDR,
|
||||
COMPONENT_INPUT_BOOKMARK_NAME, COMPONENT_INPUT_PASSWORD, COMPONENT_INPUT_PORT,
|
||||
COMPONENT_INPUT_S3_BUCKET, COMPONENT_INPUT_S3_PROFILE, COMPONENT_INPUT_S3_REGION,
|
||||
COMPONENT_INPUT_USERNAME, COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK,
|
||||
COMPONENT_RADIO_BOOKMARK_DEL_RECENT, COMPONENT_RADIO_BOOKMARK_SAVE_PWD,
|
||||
COMPONENT_RADIO_INSTALL_UPDATE, COMPONENT_RADIO_PROTOCOL, COMPONENT_RADIO_QUIT,
|
||||
COMPONENT_RECENTS_LIST, COMPONENT_TEXT_ERROR, COMPONENT_TEXT_HELP, COMPONENT_TEXT_INFO,
|
||||
COMPONENT_TEXT_NEW_VERSION_NOTES, COMPONENT_TEXT_SIZE_ERR, COMPONENT_TEXT_WAIT,
|
||||
};
|
||||
use crate::ui::keymap::*;
|
||||
use tui_realm_stdlib::InputPropsBuilder;
|
||||
use tuirealm::{Msg, Payload, PropsBuilder, Update, Value};
|
||||
use super::{AuthActivity, ExitReason, Id, InputMask, Msg, Update};
|
||||
|
||||
// -- update
|
||||
use tuirealm::{State, StateValue};
|
||||
|
||||
impl Update for AuthActivity {
|
||||
/// ### update
|
||||
///
|
||||
/// Update auth activity model based on msg
|
||||
/// The function exits when returns None
|
||||
fn update(&mut self, msg: Option<(String, Msg)>) -> Option<(String, Msg)> {
|
||||
let ref_msg: Option<(&str, &Msg)> = msg.as_ref().map(|(s, msg)| (s.as_str(), msg));
|
||||
// Match msg
|
||||
match ref_msg {
|
||||
None => None, // Exit after None
|
||||
Some(msg) => match msg {
|
||||
// Focus ( DOWN )
|
||||
(COMPONENT_RADIO_PROTOCOL, key) if key == &MSG_KEY_DOWN => {
|
||||
// Give focus based on current mask
|
||||
match self.input_mask() {
|
||||
InputMask::Generic => self.view.active(COMPONENT_INPUT_ADDR),
|
||||
InputMask::AwsS3 => self.view.active(COMPONENT_INPUT_S3_BUCKET),
|
||||
};
|
||||
None
|
||||
}
|
||||
// -- generic mask (DOWN)
|
||||
(COMPONENT_INPUT_ADDR, key) if key == &MSG_KEY_DOWN => {
|
||||
self.view.active(COMPONENT_INPUT_PORT);
|
||||
None
|
||||
}
|
||||
(COMPONENT_INPUT_PORT, key) if key == &MSG_KEY_DOWN => {
|
||||
self.view.active(COMPONENT_INPUT_USERNAME);
|
||||
None
|
||||
}
|
||||
(COMPONENT_INPUT_USERNAME, key) if key == &MSG_KEY_DOWN => {
|
||||
self.view.active(COMPONENT_INPUT_PASSWORD);
|
||||
None
|
||||
}
|
||||
(COMPONENT_INPUT_PASSWORD, key) if key == &MSG_KEY_DOWN => {
|
||||
self.view.active(COMPONENT_RADIO_PROTOCOL);
|
||||
None
|
||||
}
|
||||
// -- s3 mask (DOWN)
|
||||
(COMPONENT_INPUT_S3_BUCKET, key) if key == &MSG_KEY_DOWN => {
|
||||
self.view.active(COMPONENT_INPUT_S3_REGION);
|
||||
None
|
||||
}
|
||||
(COMPONENT_INPUT_S3_REGION, key) if key == &MSG_KEY_DOWN => {
|
||||
self.view.active(COMPONENT_INPUT_S3_PROFILE);
|
||||
None
|
||||
}
|
||||
(COMPONENT_INPUT_S3_PROFILE, key) if key == &MSG_KEY_DOWN => {
|
||||
self.view.active(COMPONENT_RADIO_PROTOCOL);
|
||||
None
|
||||
}
|
||||
// Focus ( UP )
|
||||
// -- generic (UP)
|
||||
(COMPONENT_INPUT_PASSWORD, key) if key == &MSG_KEY_UP => {
|
||||
self.view.active(COMPONENT_INPUT_USERNAME);
|
||||
None
|
||||
}
|
||||
(COMPONENT_INPUT_USERNAME, key) if key == &MSG_KEY_UP => {
|
||||
self.view.active(COMPONENT_INPUT_PORT);
|
||||
None
|
||||
}
|
||||
(COMPONENT_INPUT_PORT, key) if key == &MSG_KEY_UP => {
|
||||
self.view.active(COMPONENT_INPUT_ADDR);
|
||||
None
|
||||
}
|
||||
(COMPONENT_INPUT_ADDR, key) if key == &MSG_KEY_UP => {
|
||||
self.view.active(COMPONENT_RADIO_PROTOCOL);
|
||||
None
|
||||
}
|
||||
// -- s3 (UP)
|
||||
(COMPONENT_INPUT_S3_BUCKET, key) if key == &MSG_KEY_UP => {
|
||||
self.view.active(COMPONENT_RADIO_PROTOCOL);
|
||||
None
|
||||
}
|
||||
(COMPONENT_INPUT_S3_REGION, key) if key == &MSG_KEY_UP => {
|
||||
self.view.active(COMPONENT_INPUT_S3_BUCKET);
|
||||
None
|
||||
}
|
||||
(COMPONENT_INPUT_S3_PROFILE, key) if key == &MSG_KEY_UP => {
|
||||
self.view.active(COMPONENT_INPUT_S3_REGION);
|
||||
None
|
||||
}
|
||||
(COMPONENT_RADIO_PROTOCOL, key) if key == &MSG_KEY_UP => {
|
||||
// Give focus based on current mask
|
||||
match self.input_mask() {
|
||||
InputMask::Generic => self.view.active(COMPONENT_INPUT_PASSWORD),
|
||||
InputMask::AwsS3 => self.view.active(COMPONENT_INPUT_S3_PROFILE),
|
||||
};
|
||||
None
|
||||
}
|
||||
// Protocol - On Change
|
||||
(COMPONENT_RADIO_PROTOCOL, Msg::OnChange(Payload::One(Value::Usize(protocol)))) => {
|
||||
// If port is standard, update the current port with default for selected protocol
|
||||
let protocol: FileTransferProtocol = Self::protocol_opt_to_enum(*protocol);
|
||||
// Get port
|
||||
let port: u16 = self.get_input_port();
|
||||
match Self::is_port_standard(port) {
|
||||
false => None, // Return None
|
||||
true => {
|
||||
self.update_input_port(Self::get_default_port_for_protocol(protocol))
|
||||
}
|
||||
impl Update<Msg> for AuthActivity {
|
||||
fn update(&mut self, msg: Option<Msg>) -> Option<Msg> {
|
||||
self.redraw = true;
|
||||
match msg.unwrap_or(Msg::None) {
|
||||
Msg::AddressBlurDown => {
|
||||
assert!(self.app.active(&Id::Port).is_ok());
|
||||
}
|
||||
Msg::AddressBlurUp => {
|
||||
assert!(self.app.active(&Id::Protocol).is_ok());
|
||||
}
|
||||
Msg::BookmarksListBlur => {
|
||||
assert!(self.app.active(&Id::RecentsList).is_ok());
|
||||
}
|
||||
Msg::BookmarkNameBlur => {
|
||||
assert!(self.app.active(&Id::BookmarkSavePassword).is_ok());
|
||||
}
|
||||
Msg::BookmarksTabBlur => {
|
||||
assert!(self.app.active(&Id::Protocol).is_ok());
|
||||
}
|
||||
Msg::CloseDeleteBookmark => {
|
||||
assert!(self.app.umount(&Id::DeleteBookmarkPopup).is_ok());
|
||||
}
|
||||
Msg::CloseDeleteRecent => {
|
||||
assert!(self.app.umount(&Id::DeleteRecentPopup).is_ok());
|
||||
}
|
||||
Msg::CloseErrorPopup => {
|
||||
self.umount_error();
|
||||
}
|
||||
Msg::CloseInfoPopup => {
|
||||
self.umount_info();
|
||||
}
|
||||
Msg::CloseInstallUpdatePopup => {
|
||||
assert!(self.app.umount(&Id::NewVersionChangelog).is_ok());
|
||||
assert!(self.app.umount(&Id::InstallUpdatePopup).is_ok());
|
||||
}
|
||||
Msg::CloseKeybindingsPopup => {
|
||||
self.umount_help();
|
||||
}
|
||||
Msg::CloseQuitPopup => self.umount_quit(),
|
||||
Msg::CloseSaveBookmark => {
|
||||
assert!(self.app.umount(&Id::BookmarkName).is_ok());
|
||||
assert!(self.app.umount(&Id::BookmarkSavePassword).is_ok());
|
||||
}
|
||||
Msg::Connect => {
|
||||
match self.collect_host_params() {
|
||||
Err(err) => {
|
||||
// mount error
|
||||
self.mount_error(err);
|
||||
}
|
||||
Ok(params) => {
|
||||
self.save_recent();
|
||||
// Set file transfer params to context
|
||||
self.context_mut().set_ftparams(params);
|
||||
// Set exit reason
|
||||
self.exit_reason = Some(super::ExitReason::Connect);
|
||||
}
|
||||
}
|
||||
// Bookmarks commands
|
||||
// <RIGHT> / <LEFT>
|
||||
(COMPONENT_BOOKMARKS_LIST, key) if key == &MSG_KEY_RIGHT => {
|
||||
// Give focus to recents
|
||||
self.view.active(COMPONENT_RECENTS_LIST);
|
||||
None
|
||||
}
|
||||
(COMPONENT_RECENTS_LIST, key) if key == &MSG_KEY_LEFT => {
|
||||
// Give focus to bookmarks
|
||||
self.view.active(COMPONENT_BOOKMARKS_LIST);
|
||||
None
|
||||
}
|
||||
// <DEL | 'E'>
|
||||
(COMPONENT_BOOKMARKS_LIST, key)
|
||||
if key == &MSG_KEY_DEL || key == &MSG_KEY_CHAR_E =>
|
||||
{
|
||||
// Show delete popup
|
||||
self.mount_bookmark_del_dialog();
|
||||
None
|
||||
}
|
||||
(COMPONENT_RECENTS_LIST, key) if key == &MSG_KEY_DEL || key == &MSG_KEY_CHAR_E => {
|
||||
// Show delete popup
|
||||
self.mount_recent_del_dialog();
|
||||
None
|
||||
}
|
||||
// Enter
|
||||
(COMPONENT_BOOKMARKS_LIST, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => {
|
||||
self.load_bookmark(*idx);
|
||||
// Give focus to input password (or to protocol if not generic)
|
||||
self.view.active(match self.input_mask() {
|
||||
InputMask::Generic => COMPONENT_INPUT_PASSWORD,
|
||||
InputMask::AwsS3 => COMPONENT_INPUT_S3_BUCKET,
|
||||
});
|
||||
None
|
||||
}
|
||||
(COMPONENT_RECENTS_LIST, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => {
|
||||
self.load_recent(*idx);
|
||||
// Give focus to input password
|
||||
self.view.active(match self.input_mask() {
|
||||
InputMask::Generic => COMPONENT_INPUT_PASSWORD,
|
||||
InputMask::AwsS3 => COMPONENT_INPUT_S3_BUCKET,
|
||||
});
|
||||
None
|
||||
}
|
||||
// Bookmark radio
|
||||
// Del bookmarks
|
||||
(
|
||||
COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK,
|
||||
Msg::OnSubmit(Payload::One(Value::Usize(index))),
|
||||
) => {
|
||||
// hide bookmark delete
|
||||
}
|
||||
Msg::DeleteBookmark => {
|
||||
if let Ok(State::One(StateValue::Usize(idx))) = self.app.state(&Id::BookmarksList) {
|
||||
// Umount dialog
|
||||
self.umount_bookmark_del_dialog();
|
||||
// Index must be 0 => YES
|
||||
match *index {
|
||||
0 => {
|
||||
// Get selected bookmark
|
||||
match self.view.get_state(COMPONENT_BOOKMARKS_LIST) {
|
||||
Some(Payload::One(Value::Usize(index))) => {
|
||||
// Delete bookmark
|
||||
self.del_bookmark(index);
|
||||
// Update bookmarks
|
||||
self.view_bookmarks()
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
(
|
||||
COMPONENT_RADIO_BOOKMARK_DEL_RECENT,
|
||||
Msg::OnSubmit(Payload::One(Value::Usize(index))),
|
||||
) => {
|
||||
// hide bookmark delete
|
||||
self.umount_recent_del_dialog();
|
||||
// Index must be 0 => YES
|
||||
match *index {
|
||||
0 => {
|
||||
// Get selected bookmark
|
||||
match self.view.get_state(COMPONENT_RECENTS_LIST) {
|
||||
Some(Payload::One(Value::Usize(index))) => {
|
||||
// Delete recent
|
||||
self.del_recent(index);
|
||||
// Update bookmarks
|
||||
self.view_recent_connections()
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
// <ESC> hide tab
|
||||
(COMPONENT_RADIO_BOOKMARK_DEL_RECENT, key) if key == &MSG_KEY_ESC => {
|
||||
self.umount_recent_del_dialog();
|
||||
None
|
||||
}
|
||||
(COMPONENT_RADIO_BOOKMARK_DEL_RECENT, _) => None,
|
||||
(COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK, key) if key == &MSG_KEY_ESC => {
|
||||
self.umount_bookmark_del_dialog();
|
||||
None
|
||||
}
|
||||
(COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK, _) => None,
|
||||
// Error message
|
||||
(COMPONENT_TEXT_ERROR, key) if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => {
|
||||
// Umount text error
|
||||
self.umount_error();
|
||||
None
|
||||
}
|
||||
// -- Text info
|
||||
(COMPONENT_TEXT_INFO, key) if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => {
|
||||
// Umount text info
|
||||
self.umount_info();
|
||||
None
|
||||
}
|
||||
(COMPONENT_TEXT_ERROR, _) | (COMPONENT_TEXT_INFO, _) => None,
|
||||
// -- Text wait
|
||||
(COMPONENT_TEXT_WAIT, _) => None,
|
||||
// -- Release notes
|
||||
(COMPONENT_TEXT_NEW_VERSION_NOTES, key) if key == &MSG_KEY_ESC => {
|
||||
// Umount release notes
|
||||
self.umount_release_notes();
|
||||
None
|
||||
}
|
||||
(COMPONENT_TEXT_NEW_VERSION_NOTES, key) if key == &MSG_KEY_TAB => {
|
||||
// Focus to radio update
|
||||
self.view.active(COMPONENT_RADIO_INSTALL_UPDATE);
|
||||
None
|
||||
}
|
||||
(COMPONENT_TEXT_NEW_VERSION_NOTES, _) => None,
|
||||
// -- Install update radio
|
||||
(COMPONENT_RADIO_INSTALL_UPDATE, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => {
|
||||
// Install update
|
||||
self.install_update();
|
||||
None
|
||||
}
|
||||
(COMPONENT_RADIO_INSTALL_UPDATE, Msg::OnSubmit(Payload::One(Value::Usize(1)))) => {
|
||||
// Umount
|
||||
self.umount_release_notes();
|
||||
None
|
||||
}
|
||||
(COMPONENT_RADIO_INSTALL_UPDATE, key) if key == &MSG_KEY_TAB => {
|
||||
// Focus to changelog
|
||||
self.view.active(COMPONENT_TEXT_NEW_VERSION_NOTES);
|
||||
None
|
||||
}
|
||||
(COMPONENT_RADIO_INSTALL_UPDATE, _) => None,
|
||||
// Help
|
||||
(_, key) if key == &MSG_KEY_CTRL_H => {
|
||||
// Show help
|
||||
self.mount_help();
|
||||
None
|
||||
}
|
||||
// Release notes
|
||||
(_, key) if key == &MSG_KEY_CTRL_R => {
|
||||
// Show release notes
|
||||
self.mount_release_notes();
|
||||
None
|
||||
}
|
||||
(COMPONENT_TEXT_HELP, key) if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => {
|
||||
// Hide text help
|
||||
self.umount_help();
|
||||
None
|
||||
}
|
||||
(COMPONENT_TEXT_HELP, _) => None,
|
||||
// Enter setup
|
||||
(_, key) if key == &MSG_KEY_CTRL_C => {
|
||||
self.exit_reason = Some(super::ExitReason::EnterSetup);
|
||||
None
|
||||
}
|
||||
// Save bookmark; show popup
|
||||
(_, key) if key == &MSG_KEY_CTRL_S => {
|
||||
// Show popup
|
||||
self.mount_bookmark_save_dialog();
|
||||
// Give focus to bookmark name
|
||||
self.view.active(COMPONENT_INPUT_BOOKMARK_NAME);
|
||||
None
|
||||
}
|
||||
(COMPONENT_INPUT_BOOKMARK_NAME, key) if key == &MSG_KEY_DOWN => {
|
||||
// Give focus to pwd
|
||||
self.view.active(COMPONENT_RADIO_BOOKMARK_SAVE_PWD);
|
||||
None
|
||||
}
|
||||
(COMPONENT_RADIO_BOOKMARK_SAVE_PWD, key) if key == &MSG_KEY_UP => {
|
||||
// Give focus to pwd
|
||||
self.view.active(COMPONENT_INPUT_BOOKMARK_NAME);
|
||||
None
|
||||
}
|
||||
// Save bookmark
|
||||
(COMPONENT_INPUT_BOOKMARK_NAME, Msg::OnSubmit(_))
|
||||
| (COMPONENT_RADIO_BOOKMARK_SAVE_PWD, Msg::OnSubmit(_)) => {
|
||||
// Get values
|
||||
let bookmark_name: String =
|
||||
match self.view.get_state(COMPONENT_INPUT_BOOKMARK_NAME) {
|
||||
Some(Payload::One(Value::Str(s))) => s,
|
||||
_ => String::new(),
|
||||
};
|
||||
let save_pwd: bool = matches!(
|
||||
self.view.get_state(COMPONENT_RADIO_BOOKMARK_SAVE_PWD),
|
||||
Some(Payload::One(Value::Usize(0)))
|
||||
);
|
||||
// Save bookmark
|
||||
if !bookmark_name.is_empty() {
|
||||
self.save_bookmark(bookmark_name, save_pwd);
|
||||
}
|
||||
// Umount popup
|
||||
self.umount_bookmark_save_dialog();
|
||||
// Reload bookmarks
|
||||
// Delete bookmark
|
||||
self.del_bookmark(idx);
|
||||
// Update bookmarks
|
||||
self.view_bookmarks()
|
||||
}
|
||||
// Hide save bookmark
|
||||
(COMPONENT_INPUT_BOOKMARK_NAME, key) | (COMPONENT_RADIO_BOOKMARK_SAVE_PWD, key)
|
||||
if key == &MSG_KEY_ESC =>
|
||||
{
|
||||
// Umount popup
|
||||
self.umount_bookmark_save_dialog();
|
||||
None
|
||||
}
|
||||
(COMPONENT_INPUT_BOOKMARK_NAME, _) | (COMPONENT_RADIO_BOOKMARK_SAVE_PWD, _) => None,
|
||||
// Quit dialog
|
||||
(COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(choice)))) => {
|
||||
// If choice is 0, quit termscp
|
||||
if *choice == 0 {
|
||||
self.exit_reason = Some(super::ExitReason::Quit);
|
||||
}
|
||||
self.umount_quit();
|
||||
None
|
||||
}
|
||||
(COMPONENT_RADIO_QUIT, key) if key == &MSG_KEY_ESC => {
|
||||
self.umount_quit();
|
||||
None
|
||||
}
|
||||
// -- text size error; block everything
|
||||
(COMPONENT_TEXT_SIZE_ERR, _) => None,
|
||||
// <TAB> bookmarks
|
||||
(COMPONENT_BOOKMARKS_LIST, key) | (COMPONENT_RECENTS_LIST, key)
|
||||
if key == &MSG_KEY_TAB =>
|
||||
{
|
||||
// Give focus to address
|
||||
self.view.active(COMPONENT_RADIO_PROTOCOL);
|
||||
None
|
||||
}
|
||||
// Any <TAB>, go to bookmarks
|
||||
(_, key) if key == &MSG_KEY_TAB => {
|
||||
self.view.active(COMPONENT_BOOKMARKS_LIST);
|
||||
None
|
||||
}
|
||||
// On submit on any unhandled (connect)
|
||||
(_, Msg::OnSubmit(_)) => self.on_unhandled_submit(),
|
||||
(_, key) if key == &MSG_KEY_ENTER => self.on_unhandled_submit(),
|
||||
// <ESC> => Quit
|
||||
(_, key) if key == &MSG_KEY_ESC => {
|
||||
self.mount_quit();
|
||||
None
|
||||
}
|
||||
(_, _) => None, // Ignore other events
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthActivity {
|
||||
fn update_input_port(&mut self, port: u16) -> Option<(String, Msg)> {
|
||||
match self.view.get_props(COMPONENT_INPUT_PORT) {
|
||||
None => None,
|
||||
Some(props) => {
|
||||
let props = InputPropsBuilder::from(props)
|
||||
.with_value(port.to_string())
|
||||
.build();
|
||||
self.view.update(COMPONENT_INPUT_PORT, props)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_unhandled_submit(&mut self) -> Option<(String, Msg)> {
|
||||
// Validate fields
|
||||
match self.collect_host_params() {
|
||||
Err(err) => {
|
||||
// mount error
|
||||
self.mount_error(err);
|
||||
Msg::DeleteRecent => {
|
||||
if let Ok(State::One(StateValue::Usize(idx))) = self.app.state(&Id::RecentsList) {
|
||||
// Umount dialog
|
||||
self.umount_recent_del_dialog();
|
||||
// Delete recent
|
||||
self.del_recent(idx);
|
||||
// Update recents
|
||||
self.view_recent_connections();
|
||||
}
|
||||
}
|
||||
Ok(params) => {
|
||||
self.save_recent();
|
||||
// Set file transfer params to context
|
||||
self.context_mut().set_ftparams(params);
|
||||
// Set exit reason
|
||||
self.exit_reason = Some(super::ExitReason::Connect);
|
||||
Msg::EnterSetup => {
|
||||
self.exit_reason = Some(ExitReason::EnterSetup);
|
||||
}
|
||||
Msg::InstallUpdate => {
|
||||
self.install_update();
|
||||
}
|
||||
Msg::LoadBookmark(i) => {
|
||||
self.load_bookmark(i);
|
||||
// Give focus to input password (or to protocol if not generic)
|
||||
assert!(self
|
||||
.app
|
||||
.active(match self.input_mask() {
|
||||
InputMask::Generic => &Id::Password,
|
||||
InputMask::AwsS3 => &Id::S3Bucket,
|
||||
})
|
||||
.is_ok());
|
||||
}
|
||||
Msg::LoadRecent(i) => {
|
||||
self.load_recent(i);
|
||||
// Give focus to input password (or to protocol if not generic)
|
||||
assert!(self
|
||||
.app
|
||||
.active(match self.input_mask() {
|
||||
InputMask::Generic => &Id::Password,
|
||||
InputMask::AwsS3 => &Id::S3Bucket,
|
||||
})
|
||||
.is_ok());
|
||||
}
|
||||
Msg::ParamsFormBlur => {
|
||||
assert!(self.app.active(&Id::BookmarksList).is_ok());
|
||||
}
|
||||
Msg::PasswordBlurDown => {
|
||||
assert!(self.app.active(&Id::Protocol).is_ok());
|
||||
}
|
||||
Msg::PasswordBlurUp => {
|
||||
assert!(self.app.active(&Id::Username).is_ok());
|
||||
}
|
||||
Msg::PortBlurDown => {
|
||||
assert!(self.app.active(&Id::Username).is_ok());
|
||||
}
|
||||
Msg::PortBlurUp => {
|
||||
assert!(self.app.active(&Id::Address).is_ok());
|
||||
}
|
||||
Msg::ProtocolBlurDown => {
|
||||
assert!(self
|
||||
.app
|
||||
.active(match self.input_mask() {
|
||||
InputMask::Generic => &Id::Address,
|
||||
InputMask::AwsS3 => &Id::S3Bucket,
|
||||
})
|
||||
.is_ok());
|
||||
}
|
||||
Msg::ProtocolBlurUp => {
|
||||
assert!(self
|
||||
.app
|
||||
.active(match self.input_mask() {
|
||||
InputMask::Generic => &Id::Password,
|
||||
InputMask::AwsS3 => &Id::S3Profile,
|
||||
})
|
||||
.is_ok());
|
||||
}
|
||||
Msg::ProtocolChanged(protocol) => {
|
||||
self.protocol = protocol;
|
||||
// Update port
|
||||
let port: u16 = self.get_input_port();
|
||||
if Self::is_port_standard(port) {
|
||||
self.mount_port(Self::get_default_port_for_protocol(protocol));
|
||||
}
|
||||
}
|
||||
Msg::Quit => {
|
||||
self.exit_reason = Some(ExitReason::Quit);
|
||||
}
|
||||
Msg::RececentsListBlur => {
|
||||
assert!(self.app.active(&Id::BookmarksList).is_ok());
|
||||
}
|
||||
Msg::S3BucketBlurDown => {
|
||||
assert!(self.app.active(&Id::S3Region).is_ok());
|
||||
}
|
||||
Msg::S3BucketBlurUp => {
|
||||
assert!(self.app.active(&Id::Protocol).is_ok());
|
||||
}
|
||||
Msg::S3RegionBlurDown => {
|
||||
assert!(self.app.active(&Id::S3Profile).is_ok());
|
||||
}
|
||||
Msg::S3RegionBlurUp => {
|
||||
assert!(self.app.active(&Id::S3Bucket).is_ok());
|
||||
}
|
||||
Msg::S3ProfileBlurDown => {
|
||||
assert!(self.app.active(&Id::Protocol).is_ok());
|
||||
}
|
||||
Msg::S3ProfileBlurUp => {
|
||||
assert!(self.app.active(&Id::S3Region).is_ok());
|
||||
}
|
||||
Msg::SaveBookmark => {
|
||||
// get bookmark name
|
||||
let (name, save_password) = self.get_new_bookmark();
|
||||
// Save bookmark
|
||||
if !name.is_empty() {
|
||||
self.save_bookmark(name, save_password);
|
||||
}
|
||||
// Umount popup
|
||||
self.umount_bookmark_save_dialog();
|
||||
// Reload bookmarks
|
||||
self.view_bookmarks()
|
||||
}
|
||||
Msg::SaveBookmarkPasswordBlur => {
|
||||
assert!(self.app.active(&Id::BookmarkName).is_ok());
|
||||
}
|
||||
Msg::ShowDeleteBookmarkPopup => {
|
||||
self.mount_bookmark_del_dialog();
|
||||
}
|
||||
Msg::ShowDeleteRecentPopup => {
|
||||
self.mount_recent_del_dialog();
|
||||
}
|
||||
Msg::ShowKeybindingsPopup => {
|
||||
self.mount_keybindings();
|
||||
}
|
||||
Msg::ShowQuitPopup => {
|
||||
self.mount_quit();
|
||||
}
|
||||
Msg::ShowReleaseNotes => {
|
||||
self.mount_release_notes();
|
||||
}
|
||||
Msg::ShowSaveBookmarkPopup => {
|
||||
self.mount_bookmark_save_dialog();
|
||||
}
|
||||
Msg::UsernameBlurDown => {
|
||||
assert!(self.app.active(&Id::Password).is_ok());
|
||||
}
|
||||
Msg::UsernameBlurUp => {
|
||||
assert!(self.app.active(&Id::Port).is_ok());
|
||||
}
|
||||
Msg::None => {}
|
||||
}
|
||||
// Return None
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -26,10 +26,10 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
pub(self) use super::{
|
||||
browser::FileExplorerTab, FileTransferActivity, FsEntry, LogLevel, TransferOpts,
|
||||
browser::FileExplorerTab, FileTransferActivity, FsEntry, Id, LogLevel, TransferOpts,
|
||||
TransferPayload,
|
||||
};
|
||||
use tuirealm::{Payload, Value};
|
||||
use tuirealm::{State, StateValue};
|
||||
|
||||
// actions
|
||||
pub(crate) mod change_dir;
|
||||
@@ -79,7 +79,7 @@ impl FileTransferActivity {
|
||||
///
|
||||
/// Get local file entry
|
||||
pub(crate) fn get_local_selected_entries(&self) -> SelectedEntry {
|
||||
match self.get_selected_index(super::COMPONENT_EXPLORER_LOCAL) {
|
||||
match self.get_selected_index(&Id::ExplorerLocal) {
|
||||
SelectedEntryIndex::One(idx) => SelectedEntry::from(self.local().get(idx)),
|
||||
SelectedEntryIndex::Many(files) => {
|
||||
let files: Vec<&FsEntry> = files
|
||||
@@ -97,7 +97,7 @@ impl FileTransferActivity {
|
||||
///
|
||||
/// Get remote file entry
|
||||
pub(crate) fn get_remote_selected_entries(&self) -> SelectedEntry {
|
||||
match self.get_selected_index(super::COMPONENT_EXPLORER_REMOTE) {
|
||||
match self.get_selected_index(&Id::ExplorerRemote) {
|
||||
SelectedEntryIndex::One(idx) => SelectedEntry::from(self.remote().get(idx)),
|
||||
SelectedEntryIndex::Many(files) => {
|
||||
let files: Vec<&FsEntry> = files
|
||||
@@ -115,7 +115,7 @@ impl FileTransferActivity {
|
||||
///
|
||||
/// Get remote file entry
|
||||
pub(crate) fn get_found_selected_entries(&self) -> SelectedEntry {
|
||||
match self.get_selected_index(super::COMPONENT_EXPLORER_FIND) {
|
||||
match self.get_selected_index(&Id::ExplorerFind) {
|
||||
SelectedEntryIndex::One(idx) => {
|
||||
SelectedEntry::from(self.found().as_ref().unwrap().get(idx))
|
||||
}
|
||||
@@ -133,14 +133,14 @@ impl FileTransferActivity {
|
||||
|
||||
// -- private
|
||||
|
||||
fn get_selected_index(&self, component: &str) -> SelectedEntryIndex {
|
||||
match self.view.get_state(component) {
|
||||
Some(Payload::One(Value::Usize(idx))) => SelectedEntryIndex::One(idx),
|
||||
Some(Payload::Vec(files)) => {
|
||||
fn get_selected_index(&self, id: &Id) -> SelectedEntryIndex {
|
||||
match self.app.state(id) {
|
||||
Ok(State::One(StateValue::Usize(idx))) => SelectedEntryIndex::One(idx),
|
||||
Ok(State::Vec(files)) => {
|
||||
let list: Vec<usize> = files
|
||||
.iter()
|
||||
.map(|x| match x {
|
||||
Value::Usize(v) => *v,
|
||||
StateValue::Usize(v) => *v,
|
||||
_ => 0,
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
296
src/ui/activities/filetransfer/components/log.rs
Normal file
296
src/ui/activities/filetransfer/components/log.rs
Normal file
@@ -0,0 +1,296 @@
|
||||
//! ## Log
|
||||
//!
|
||||
//! log tab component
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
use super::{Msg, UiMsg};
|
||||
|
||||
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
|
||||
use tuirealm::event::{Key, KeyEvent};
|
||||
use tuirealm::props::{Alignment, AttrValue, Attribute, Borders, Color, Style, Table};
|
||||
use tuirealm::tui::layout::Corner;
|
||||
use tuirealm::tui::widgets::{List as TuiList, ListItem, ListState};
|
||||
use tuirealm::{Component, Event, MockComponent, NoUserEvent, Props, State, StateValue};
|
||||
|
||||
pub struct Log {
|
||||
props: Props,
|
||||
states: OwnStates,
|
||||
}
|
||||
|
||||
impl Log {
|
||||
pub fn new(lines: Table, fg: Color, bg: Color) -> Self {
|
||||
let mut props = Props::default();
|
||||
props.set(
|
||||
Attribute::Borders,
|
||||
AttrValue::Borders(Borders::default().color(fg)),
|
||||
);
|
||||
props.set(Attribute::Background, AttrValue::Color(bg));
|
||||
props.set(Attribute::Content, AttrValue::Table(lines));
|
||||
Self {
|
||||
props,
|
||||
states: OwnStates::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MockComponent for Log {
|
||||
fn view(&mut self, frame: &mut tuirealm::Frame, area: tuirealm::tui::layout::Rect) {
|
||||
let width: usize = area.width as usize - 4;
|
||||
let focus = self
|
||||
.props
|
||||
.get_or(Attribute::Focus, AttrValue::Flag(false))
|
||||
.unwrap_flag();
|
||||
let fg = self
|
||||
.props
|
||||
.get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
|
||||
.unwrap_color();
|
||||
let bg = self
|
||||
.props
|
||||
.get_or(Attribute::Background, AttrValue::Color(Color::Reset))
|
||||
.unwrap_color();
|
||||
// Make list
|
||||
let list_items: Vec<ListItem> = self
|
||||
.props
|
||||
.get(Attribute::Content)
|
||||
.unwrap()
|
||||
.unwrap_table()
|
||||
.iter()
|
||||
.map(|row| ListItem::new(tui_realm_stdlib::utils::wrap_spans(row, width, &self.props)))
|
||||
.collect();
|
||||
let w = TuiList::new(list_items)
|
||||
.block(tui_realm_stdlib::utils::get_block(
|
||||
Borders::default().color(fg),
|
||||
Some(("Log".to_string(), Alignment::Left)),
|
||||
focus,
|
||||
None,
|
||||
))
|
||||
.start_corner(Corner::BottomLeft)
|
||||
.highlight_symbol(">> ")
|
||||
.style(Style::default().bg(bg))
|
||||
.highlight_style(Style::default());
|
||||
let mut state: ListState = ListState::default();
|
||||
state.select(Some(self.states.get_list_index()));
|
||||
frame.render_stateful_widget(w, area, &mut state);
|
||||
}
|
||||
|
||||
fn query(&self, attr: Attribute) -> Option<AttrValue> {
|
||||
self.props.get(attr)
|
||||
}
|
||||
|
||||
fn attr(&mut self, attr: Attribute, value: AttrValue) {
|
||||
self.props.set(attr, value);
|
||||
if matches!(attr, Attribute::Content) {
|
||||
self.states.set_list_len(
|
||||
match self.props.get(Attribute::Content).map(|x| x.unwrap_table()) {
|
||||
Some(spans) => spans.len(),
|
||||
_ => 0,
|
||||
},
|
||||
);
|
||||
self.states.reset_list_index();
|
||||
}
|
||||
}
|
||||
|
||||
fn state(&self) -> State {
|
||||
State::One(StateValue::Usize(self.states.get_list_index()))
|
||||
}
|
||||
|
||||
fn perform(&mut self, cmd: Cmd) -> CmdResult {
|
||||
match cmd {
|
||||
Cmd::Move(Direction::Down) => {
|
||||
let prev = self.states.get_list_index();
|
||||
self.states.incr_list_index();
|
||||
if prev != self.states.get_list_index() {
|
||||
CmdResult::Changed(self.state())
|
||||
} else {
|
||||
CmdResult::None
|
||||
}
|
||||
}
|
||||
Cmd::Move(Direction::Up) => {
|
||||
let prev = self.states.get_list_index();
|
||||
self.states.decr_list_index();
|
||||
if prev != self.states.get_list_index() {
|
||||
CmdResult::Changed(self.state())
|
||||
} else {
|
||||
CmdResult::None
|
||||
}
|
||||
}
|
||||
Cmd::Scroll(Direction::Down) => {
|
||||
let prev = self.states.get_list_index();
|
||||
(0..8).for_each(|_| self.states.incr_list_index());
|
||||
if prev != self.states.get_list_index() {
|
||||
CmdResult::Changed(self.state())
|
||||
} else {
|
||||
CmdResult::None
|
||||
}
|
||||
}
|
||||
Cmd::Scroll(Direction::Up) => {
|
||||
let prev = self.states.get_list_index();
|
||||
(0..8).for_each(|_| self.states.decr_list_index());
|
||||
if prev != self.states.get_list_index() {
|
||||
CmdResult::Changed(self.state())
|
||||
} else {
|
||||
CmdResult::None
|
||||
}
|
||||
}
|
||||
Cmd::GoTo(Position::Begin) => {
|
||||
let prev = self.states.get_list_index();
|
||||
self.states.reset_list_index();
|
||||
if prev != self.states.get_list_index() {
|
||||
CmdResult::Changed(self.state())
|
||||
} else {
|
||||
CmdResult::None
|
||||
}
|
||||
}
|
||||
Cmd::GoTo(Position::End) => {
|
||||
let prev = self.states.get_list_index();
|
||||
self.states.list_index_at_last();
|
||||
if prev != self.states.get_list_index() {
|
||||
CmdResult::Changed(self.state())
|
||||
} else {
|
||||
CmdResult::None
|
||||
}
|
||||
}
|
||||
_ => CmdResult::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for Log {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
||||
self.perform(Cmd::Move(Direction::Down));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Down, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Up));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::PageUp, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Scroll(Direction::Down));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::PageDown,
|
||||
..
|
||||
}) => {
|
||||
self.perform(Cmd::Scroll(Direction::Up));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||
self.perform(Cmd::GoTo(Position::Begin));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Home, ..
|
||||
}) => {
|
||||
self.perform(Cmd::GoTo(Position::End));
|
||||
Some(Msg::None)
|
||||
}
|
||||
// -- comp msg
|
||||
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::Ui(UiMsg::LogTabbed)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- states
|
||||
|
||||
/// ## OwnStates
|
||||
///
|
||||
/// OwnStates contains states for this component
|
||||
#[derive(Clone)]
|
||||
struct OwnStates {
|
||||
list_index: usize, // Index of selected element in list
|
||||
list_len: usize, // Length of file list
|
||||
focus: bool, // Has focus?
|
||||
}
|
||||
|
||||
impl Default for OwnStates {
|
||||
fn default() -> Self {
|
||||
OwnStates {
|
||||
list_index: 0,
|
||||
list_len: 0,
|
||||
focus: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OwnStates {
|
||||
/// ### set_list_len
|
||||
///
|
||||
/// Set list length
|
||||
pub fn set_list_len(&mut self, len: usize) {
|
||||
self.list_len = len;
|
||||
}
|
||||
|
||||
/// ### get_list_index
|
||||
///
|
||||
/// Return current value for list index
|
||||
pub fn get_list_index(&self) -> usize {
|
||||
self.list_index
|
||||
}
|
||||
|
||||
/// ### incr_list_index
|
||||
///
|
||||
/// Incremenet list index
|
||||
pub fn incr_list_index(&mut self) {
|
||||
// Check if index is at last element
|
||||
if self.list_index + 1 < self.list_len {
|
||||
self.list_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// ### decr_list_index
|
||||
///
|
||||
/// Decrement list index
|
||||
pub fn decr_list_index(&mut self) {
|
||||
// Check if index is bigger than 0
|
||||
if self.list_index > 0 {
|
||||
self.list_index -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// ### list_index_at_last
|
||||
///
|
||||
/// Set list index at last item
|
||||
pub fn list_index_at_last(&mut self) {
|
||||
self.list_index = match self.list_len {
|
||||
0 => 0,
|
||||
len => len - 1,
|
||||
};
|
||||
}
|
||||
|
||||
/// ### reset_list_index
|
||||
///
|
||||
/// Reset list index to last element
|
||||
pub fn reset_list_index(&mut self) {
|
||||
self.list_index = 0; // Last element is always 0
|
||||
}
|
||||
}
|
||||
72
src/ui/activities/filetransfer/components/mod.rs
Normal file
72
src/ui/activities/filetransfer/components/mod.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
//! ## Components
|
||||
//!
|
||||
//! file transfer activity components
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
use super::{Msg, TransferMsg, UiMsg};
|
||||
|
||||
use tui_realm_stdlib::Phantom;
|
||||
use tuirealm::{
|
||||
event::{Event, Key, KeyEvent, KeyModifiers},
|
||||
Component, MockComponent, NoUserEvent,
|
||||
};
|
||||
|
||||
// -- export
|
||||
mod log;
|
||||
mod popups;
|
||||
mod transfer;
|
||||
|
||||
pub use self::log::Log;
|
||||
pub use popups::{
|
||||
CopyPopup, DeletePopup, DisconnectPopup, ErrorPopup, ExecPopup, FatalPopup, FileInfoPopup,
|
||||
FindPopup, GoToPopup, KeybindingsPopup, MkdirPopup, NewfilePopup, OpenWithPopup,
|
||||
ProgressBarFull, ProgressBarPartial, QuitPopup, RenamePopup, ReplacePopup,
|
||||
ReplacingFilesListPopup, SaveAsPopup, SortingPopup, StatusBarLocal, StatusBarRemote, WaitPopup,
|
||||
};
|
||||
pub use transfer::{ExplorerFind, ExplorerLocal, ExplorerRemote};
|
||||
|
||||
#[derive(Default, MockComponent)]
|
||||
pub struct GlobalListener {
|
||||
component: Phantom,
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for GlobalListener {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||
Some(Msg::Ui(UiMsg::ShowDisconnectPopup))
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('q'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowQuitPopup)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('h'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowKeybindingsPopup)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
1689
src/ui/activities/filetransfer/components/popups.rs
Normal file
1689
src/ui/activities/filetransfer/components/popups.rs
Normal file
File diff suppressed because it is too large
Load Diff
400
src/ui/activities/filetransfer/components/transfer/file_list.rs
Normal file
400
src/ui/activities/filetransfer/components/transfer/file_list.rs
Normal file
@@ -0,0 +1,400 @@
|
||||
//! ## FileList
|
||||
//!
|
||||
//! `FileList` component renders a file list tab
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
|
||||
use tuirealm::props::{
|
||||
Alignment, AttrValue, Attribute, Borders, Color, Style, Table, TextModifiers,
|
||||
};
|
||||
use tuirealm::tui::layout::Corner;
|
||||
use tuirealm::tui::text::{Span, Spans};
|
||||
use tuirealm::tui::widgets::{List as TuiList, ListItem, ListState};
|
||||
use tuirealm::{MockComponent, Props, State, StateValue};
|
||||
|
||||
pub const FILE_LIST_CMD_SELECT_ALL: &str = "A";
|
||||
|
||||
/// ## OwnStates
|
||||
///
|
||||
/// OwnStates contains states for this component
|
||||
#[derive(Clone)]
|
||||
struct OwnStates {
|
||||
list_index: usize, // Index of selected element in list
|
||||
selected: Vec<usize>, // Selected files
|
||||
}
|
||||
|
||||
impl Default for OwnStates {
|
||||
fn default() -> Self {
|
||||
OwnStates {
|
||||
list_index: 0,
|
||||
selected: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OwnStates {
|
||||
/// ### init_list_states
|
||||
///
|
||||
/// Initialize list states
|
||||
pub fn init_list_states(&mut self, len: usize) {
|
||||
self.selected = Vec::with_capacity(len);
|
||||
self.fix_list_index();
|
||||
}
|
||||
|
||||
/// ### list_index
|
||||
///
|
||||
/// Return current value for list index
|
||||
pub fn list_index(&self) -> usize {
|
||||
self.list_index
|
||||
}
|
||||
|
||||
/// ### incr_list_index
|
||||
///
|
||||
/// Incremenet list index.
|
||||
/// If `can_rewind` is `true` the index rewinds when boundary is reached
|
||||
pub fn incr_list_index(&mut self, can_rewind: bool) {
|
||||
// Check if index is at last element
|
||||
if self.list_index + 1 < self.list_len() {
|
||||
self.list_index += 1;
|
||||
} else if can_rewind {
|
||||
self.list_index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// ### decr_list_index
|
||||
///
|
||||
/// Decrement list index
|
||||
/// If `can_rewind` is `true` the index rewinds when boundary is reached
|
||||
pub fn decr_list_index(&mut self, can_rewind: bool) {
|
||||
// Check if index is bigger than 0
|
||||
if self.list_index > 0 {
|
||||
self.list_index -= 1;
|
||||
} else if self.list_len() > 0 && can_rewind {
|
||||
self.list_index = self.list_len() - 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn list_index_at_first(&mut self) {
|
||||
self.list_index = 0;
|
||||
}
|
||||
|
||||
pub fn list_index_at_last(&mut self) {
|
||||
self.list_index = match self.list_len() {
|
||||
0 => 0,
|
||||
len => len - 1,
|
||||
};
|
||||
}
|
||||
|
||||
/// ### list_len
|
||||
///
|
||||
/// Returns the length of the file list, which is actually the capacity of the selection vector
|
||||
pub fn list_len(&self) -> usize {
|
||||
self.selected.capacity()
|
||||
}
|
||||
|
||||
/// ### is_selected
|
||||
///
|
||||
/// Returns whether the file with index `entry` is selected
|
||||
pub fn is_selected(&self, entry: usize) -> bool {
|
||||
self.selected.contains(&entry)
|
||||
}
|
||||
|
||||
/// ### is_selection_empty
|
||||
///
|
||||
/// Returns whether the selection is currently empty
|
||||
pub fn is_selection_empty(&self) -> bool {
|
||||
self.selected.is_empty()
|
||||
}
|
||||
|
||||
/// ### get_selection
|
||||
///
|
||||
/// Returns current file selection
|
||||
pub fn get_selection(&self) -> Vec<usize> {
|
||||
self.selected.clone()
|
||||
}
|
||||
|
||||
/// ### fix_list_index
|
||||
///
|
||||
/// Keep index if possible, otherwise set to lenght - 1
|
||||
fn fix_list_index(&mut self) {
|
||||
if self.list_index >= self.list_len() && self.list_len() > 0 {
|
||||
self.list_index = self.list_len() - 1;
|
||||
} else if self.list_len() == 0 {
|
||||
self.list_index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// -- select manipulation
|
||||
|
||||
/// ### toggle_file
|
||||
///
|
||||
/// Select or deselect file with provided entry index
|
||||
pub fn toggle_file(&mut self, entry: usize) {
|
||||
match self.is_selected(entry) {
|
||||
true => self.deselect(entry),
|
||||
false => self.select(entry),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### select_all
|
||||
///
|
||||
/// Select all files
|
||||
pub fn select_all(&mut self) {
|
||||
for i in 0..self.list_len() {
|
||||
self.select(i);
|
||||
}
|
||||
}
|
||||
|
||||
/// ### select
|
||||
///
|
||||
/// Select provided index if not selected yet
|
||||
fn select(&mut self, entry: usize) {
|
||||
if !self.is_selected(entry) {
|
||||
self.selected.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
/// ### deselect
|
||||
///
|
||||
/// Remove element file with associated index
|
||||
fn deselect(&mut self, entry: usize) {
|
||||
if self.is_selected(entry) {
|
||||
self.selected.retain(|&x| x != entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FileList {
|
||||
props: Props,
|
||||
states: OwnStates,
|
||||
}
|
||||
|
||||
impl FileList {
|
||||
pub fn foreground(mut self, fg: Color) -> Self {
|
||||
self.attr(Attribute::Foreground, AttrValue::Color(fg));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn background(mut self, bg: Color) -> Self {
|
||||
self.attr(Attribute::Background, AttrValue::Color(bg));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn borders(mut self, b: Borders) -> Self {
|
||||
self.attr(Attribute::Borders, AttrValue::Borders(b));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn title<S: AsRef<str>>(mut self, t: S, a: Alignment) -> Self {
|
||||
self.attr(
|
||||
Attribute::Title,
|
||||
AttrValue::Title((t.as_ref().to_string(), a)),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn highlighted_color(mut self, c: Color) -> Self {
|
||||
self.attr(Attribute::HighlightedColor, AttrValue::Color(c));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn rows(mut self, rows: Table) -> Self {
|
||||
self.attr(Attribute::Content, AttrValue::Table(rows));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl MockComponent for FileList {
|
||||
fn view(&mut self, frame: &mut tuirealm::Frame, area: tuirealm::tui::layout::Rect) {
|
||||
let title = self
|
||||
.props
|
||||
.get_or(
|
||||
Attribute::Title,
|
||||
AttrValue::Title((String::default(), Alignment::Left)),
|
||||
)
|
||||
.unwrap_title();
|
||||
let borders = self
|
||||
.props
|
||||
.get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
|
||||
.unwrap_borders();
|
||||
let focus = self
|
||||
.props
|
||||
.get_or(Attribute::Focus, AttrValue::Flag(false))
|
||||
.unwrap_flag();
|
||||
let div = tui_realm_stdlib::utils::get_block(borders, Some(title), focus, None);
|
||||
// Make list entries
|
||||
let list_items: Vec<ListItem> = match self
|
||||
.props
|
||||
.get(Attribute::Content)
|
||||
.map(|x| x.unwrap_table())
|
||||
{
|
||||
Some(table) => table
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(num, row)| {
|
||||
let columns: Vec<Span> = row
|
||||
.iter()
|
||||
.map(|col| {
|
||||
let (fg, bg, mut modifiers) =
|
||||
tui_realm_stdlib::utils::use_or_default_styles(&self.props, col);
|
||||
if self.states.is_selected(num) {
|
||||
modifiers |= TextModifiers::REVERSED
|
||||
| TextModifiers::UNDERLINED
|
||||
| TextModifiers::ITALIC;
|
||||
}
|
||||
Span::styled(
|
||||
col.content.clone(),
|
||||
Style::default().add_modifier(modifiers).fg(fg).bg(bg),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
ListItem::new(Spans::from(columns))
|
||||
})
|
||||
.collect(), // Make List item from TextSpan
|
||||
_ => Vec::new(),
|
||||
};
|
||||
let highlighted_color = self
|
||||
.props
|
||||
.get(Attribute::HighlightedColor)
|
||||
.map(|x| x.unwrap_color());
|
||||
let modifiers = match focus {
|
||||
true => TextModifiers::REVERSED,
|
||||
false => TextModifiers::empty(),
|
||||
};
|
||||
// Make list
|
||||
let mut list = TuiList::new(list_items)
|
||||
.block(div)
|
||||
.start_corner(Corner::TopLeft);
|
||||
if let Some(highlighted_color) = highlighted_color {
|
||||
list = list.highlight_style(
|
||||
Style::default()
|
||||
.fg(highlighted_color)
|
||||
.add_modifier(modifiers),
|
||||
);
|
||||
}
|
||||
let mut state: ListState = ListState::default();
|
||||
state.select(Some(self.states.list_index));
|
||||
frame.render_stateful_widget(list, area, &mut state);
|
||||
}
|
||||
|
||||
fn attr(&mut self, attr: Attribute, value: AttrValue) {
|
||||
self.props.set(attr, value);
|
||||
if matches!(attr, Attribute::Content) {
|
||||
self.states.init_list_states(
|
||||
match self.props.get(Attribute::Content).map(|x| x.unwrap_table()) {
|
||||
Some(spans) => spans.len(),
|
||||
_ => 0,
|
||||
},
|
||||
);
|
||||
self.states.fix_list_index();
|
||||
}
|
||||
}
|
||||
|
||||
fn query(&self, attr: Attribute) -> Option<AttrValue> {
|
||||
self.props.get(attr)
|
||||
}
|
||||
|
||||
fn state(&self) -> State {
|
||||
match self.states.is_selection_empty() {
|
||||
true => State::One(StateValue::Usize(self.states.list_index())),
|
||||
false => State::Vec(
|
||||
self.states
|
||||
.get_selection()
|
||||
.into_iter()
|
||||
.map(StateValue::Usize)
|
||||
.collect(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn perform(&mut self, cmd: Cmd) -> CmdResult {
|
||||
match cmd {
|
||||
Cmd::Move(Direction::Down) => {
|
||||
let prev = self.states.list_index;
|
||||
self.states.incr_list_index(true);
|
||||
if prev != self.states.list_index {
|
||||
CmdResult::Changed(self.state())
|
||||
} else {
|
||||
CmdResult::None
|
||||
}
|
||||
}
|
||||
Cmd::Move(Direction::Up) => {
|
||||
let prev = self.states.list_index;
|
||||
self.states.decr_list_index(true);
|
||||
if prev != self.states.list_index {
|
||||
CmdResult::Changed(self.state())
|
||||
} else {
|
||||
CmdResult::None
|
||||
}
|
||||
}
|
||||
Cmd::Scroll(Direction::Down) => {
|
||||
let prev = self.states.list_index;
|
||||
(0..8).for_each(|_| self.states.incr_list_index(false));
|
||||
if prev != self.states.list_index {
|
||||
CmdResult::Changed(self.state())
|
||||
} else {
|
||||
CmdResult::None
|
||||
}
|
||||
}
|
||||
Cmd::Scroll(Direction::Up) => {
|
||||
let prev = self.states.list_index;
|
||||
(0..8).for_each(|_| self.states.decr_list_index(false));
|
||||
if prev != self.states.list_index {
|
||||
CmdResult::Changed(self.state())
|
||||
} else {
|
||||
CmdResult::None
|
||||
}
|
||||
}
|
||||
Cmd::GoTo(Position::Begin) => {
|
||||
let prev = self.states.list_index;
|
||||
self.states.list_index_at_first();
|
||||
if prev != self.states.list_index {
|
||||
CmdResult::Changed(self.state())
|
||||
} else {
|
||||
CmdResult::None
|
||||
}
|
||||
}
|
||||
Cmd::GoTo(Position::End) => {
|
||||
let prev = self.states.list_index;
|
||||
self.states.list_index_at_last();
|
||||
if prev != self.states.list_index {
|
||||
CmdResult::Changed(self.state())
|
||||
} else {
|
||||
CmdResult::None
|
||||
}
|
||||
}
|
||||
Cmd::Custom(FILE_LIST_CMD_SELECT_ALL) => {
|
||||
self.states.select_all();
|
||||
CmdResult::None
|
||||
}
|
||||
Cmd::Toggle => {
|
||||
self.states.toggle_file(self.states.list_index());
|
||||
CmdResult::None
|
||||
}
|
||||
_ => CmdResult::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
494
src/ui/activities/filetransfer/components/transfer/mod.rs
Normal file
494
src/ui/activities/filetransfer/components/transfer/mod.rs
Normal file
@@ -0,0 +1,494 @@
|
||||
//! ## Transfer
|
||||
//!
|
||||
//! file transfer components
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
use super::{Msg, TransferMsg, UiMsg};
|
||||
|
||||
mod file_list;
|
||||
use file_list::FileList;
|
||||
|
||||
use tuirealm::command::{Cmd, Direction, Position};
|
||||
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
|
||||
use tuirealm::props::{Alignment, Borders, Color, TextSpan};
|
||||
use tuirealm::{Component, Event, MockComponent, NoUserEvent};
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct ExplorerFind {
|
||||
component: FileList,
|
||||
}
|
||||
|
||||
impl ExplorerFind {
|
||||
pub fn new<S: AsRef<str>>(title: S, files: &[&str], bg: Color, fg: Color, hg: Color) -> Self {
|
||||
Self {
|
||||
component: FileList::default()
|
||||
.background(bg)
|
||||
.borders(Borders::default().color(hg))
|
||||
.foreground(fg)
|
||||
.highlighted_color(hg)
|
||||
.title(title, Alignment::Left)
|
||||
.rows(files.iter().map(|x| vec![TextSpan::from(x)]).collect()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for ExplorerFind {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Down, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Down));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
||||
self.perform(Cmd::Move(Direction::Up));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::PageDown,
|
||||
..
|
||||
}) => {
|
||||
self.perform(Cmd::Scroll(Direction::Down));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::PageUp, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Scroll(Direction::Up));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Home, ..
|
||||
}) => {
|
||||
self.perform(Cmd::GoTo(Position::Begin));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||
self.perform(Cmd::GoTo(Position::End));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('a'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
}) => {
|
||||
let _ = self.perform(Cmd::Custom(file_list::FILE_LIST_CMD_SELECT_ALL));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('m'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => {
|
||||
let _ = self.perform(Cmd::Toggle);
|
||||
Some(Msg::None)
|
||||
}
|
||||
// -- comp msg
|
||||
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
|
||||
Some(Msg::Ui(UiMsg::ExplorerTabbed))
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||
Some(Msg::Ui(UiMsg::CloseFindExplorer))
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Left | Key::Right,
|
||||
..
|
||||
}) => Some(Msg::Ui(UiMsg::ChangeTransferWindow)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => Some(Msg::Transfer(TransferMsg::EnterDirectory)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char(' '),
|
||||
..
|
||||
}) => Some(Msg::Transfer(TransferMsg::TransferFile)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Backspace,
|
||||
..
|
||||
}) => Some(Msg::Transfer(TransferMsg::GoToPreviousDirectory)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('a'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ToggleHiddenFiles)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('b'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowFileSortingPopup)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('e') | Key::Delete,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowDeletePopup)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('i'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowFileInfoPopup)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('s'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowSaveAsPopup)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('v'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Transfer(TransferMsg::OpenFile)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('w'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowOpenWithPopup)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct ExplorerLocal {
|
||||
component: FileList,
|
||||
}
|
||||
|
||||
impl ExplorerLocal {
|
||||
pub fn new<S: AsRef<str>>(title: S, files: &[&str], bg: Color, fg: Color, hg: Color) -> Self {
|
||||
Self {
|
||||
component: FileList::default()
|
||||
.background(bg)
|
||||
.borders(Borders::default().color(hg))
|
||||
.foreground(fg)
|
||||
.highlighted_color(hg)
|
||||
.title(title, Alignment::Left)
|
||||
.rows(files.iter().map(|x| vec![TextSpan::from(x)]).collect()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for ExplorerLocal {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Down, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Down));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
||||
self.perform(Cmd::Move(Direction::Up));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::PageDown,
|
||||
..
|
||||
}) => {
|
||||
self.perform(Cmd::Scroll(Direction::Down));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::PageUp, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Scroll(Direction::Up));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Home, ..
|
||||
}) => {
|
||||
self.perform(Cmd::GoTo(Position::Begin));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||
self.perform(Cmd::GoTo(Position::End));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('a'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
}) => {
|
||||
let _ = self.perform(Cmd::Custom(file_list::FILE_LIST_CMD_SELECT_ALL));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('m'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => {
|
||||
let _ = self.perform(Cmd::Toggle);
|
||||
Some(Msg::None)
|
||||
}
|
||||
// -- comp msg
|
||||
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
|
||||
Some(Msg::Ui(UiMsg::ExplorerTabbed))
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||
Some(Msg::Ui(UiMsg::ShowDisconnectPopup))
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Right, ..
|
||||
}) => Some(Msg::Ui(UiMsg::ChangeTransferWindow)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Backspace,
|
||||
..
|
||||
}) => Some(Msg::Transfer(TransferMsg::GoToPreviousDirectory)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => Some(Msg::Transfer(TransferMsg::EnterDirectory)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char(' '),
|
||||
..
|
||||
}) => Some(Msg::Transfer(TransferMsg::TransferFile)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('a'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ToggleHiddenFiles)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('b'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowFileSortingPopup)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('c'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowCopyPopup)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('d'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowMkdirPopup)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('e') | Key::Delete,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowDeletePopup)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('f'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowFindPopup)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('g'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowGotoPopup)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('i'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowFileInfoPopup)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('l'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Transfer(TransferMsg::ReloadDir)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('n'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowNewFilePopup)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('o'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Transfer(TransferMsg::OpenTextFile)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('r'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowRenamePopup)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('s'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowSaveAsPopup)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('u'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Transfer(TransferMsg::GoToParentDirectory)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('x'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowExecPopup)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('y'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ToggleSyncBrowsing)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('v'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Transfer(TransferMsg::OpenFile)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('w'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowOpenWithPopup)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct ExplorerRemote {
|
||||
component: FileList,
|
||||
}
|
||||
|
||||
impl ExplorerRemote {
|
||||
pub fn new<S: AsRef<str>>(title: S, files: &[&str], bg: Color, fg: Color, hg: Color) -> Self {
|
||||
Self {
|
||||
component: FileList::default()
|
||||
.background(bg)
|
||||
.borders(Borders::default().color(hg))
|
||||
.foreground(fg)
|
||||
.highlighted_color(hg)
|
||||
.title(title, Alignment::Left)
|
||||
.rows(files.iter().map(|x| vec![TextSpan::from(x)]).collect()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for ExplorerRemote {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Down, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Down));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
||||
self.perform(Cmd::Move(Direction::Up));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::PageDown,
|
||||
..
|
||||
}) => {
|
||||
self.perform(Cmd::Scroll(Direction::Down));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::PageUp, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Scroll(Direction::Up));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Home, ..
|
||||
}) => {
|
||||
self.perform(Cmd::GoTo(Position::Begin));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||
self.perform(Cmd::GoTo(Position::End));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('a'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
}) => {
|
||||
let _ = self.perform(Cmd::Custom(file_list::FILE_LIST_CMD_SELECT_ALL));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('m'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => {
|
||||
let _ = self.perform(Cmd::Toggle);
|
||||
Some(Msg::None)
|
||||
}
|
||||
// -- comp msg
|
||||
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
|
||||
Some(Msg::Ui(UiMsg::ExplorerTabbed))
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||
Some(Msg::Ui(UiMsg::ShowDisconnectPopup))
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Left, ..
|
||||
}) => Some(Msg::Ui(UiMsg::ChangeTransferWindow)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Backspace,
|
||||
..
|
||||
}) => Some(Msg::Transfer(TransferMsg::GoToPreviousDirectory)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => Some(Msg::Transfer(TransferMsg::EnterDirectory)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char(' '),
|
||||
..
|
||||
}) => Some(Msg::Transfer(TransferMsg::TransferFile)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('a'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ToggleHiddenFiles)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('b'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowFileSortingPopup)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('c'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowCopyPopup)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('d'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowMkdirPopup)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('e') | Key::Delete,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowDeletePopup)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('f'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowFindPopup)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('g'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowGotoPopup)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('i'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowFileInfoPopup)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('l'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Transfer(TransferMsg::ReloadDir)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('n'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowNewFilePopup)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('o'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Transfer(TransferMsg::OpenTextFile)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('r'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowRenamePopup)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('s'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowSaveAsPopup)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('u'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Transfer(TransferMsg::GoToParentDirectory)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('x'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowExecPopup)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('y'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ToggleSyncBrowsing)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('v'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Transfer(TransferMsg::OpenFile)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('w'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowOpenWithPopup)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -22,22 +22,50 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// Locals
|
||||
use super::{ConfigClient, FileTransferActivity, LogLevel, LogRecord, TransferPayload};
|
||||
use super::{
|
||||
browser::FileExplorerTab, ConfigClient, FileTransferActivity, Id, LogLevel, LogRecord,
|
||||
TransferPayload,
|
||||
};
|
||||
use crate::filetransfer::ProtocolParams;
|
||||
use crate::system::environment;
|
||||
use crate::system::notifications::Notification;
|
||||
use crate::system::sshkey_storage::SshKeyStorage;
|
||||
use crate::utils::fmt::fmt_millis;
|
||||
use crate::utils::fmt::{fmt_millis, fmt_path_elide_ex};
|
||||
use crate::utils::path;
|
||||
// Ext
|
||||
use bytesize::ByteSize;
|
||||
use std::env;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tuirealm::Update;
|
||||
use tuirealm::props::{
|
||||
Alignment, AttrValue, Attribute, Color, PropPayload, PropValue, TableBuilder, TextSpan,
|
||||
};
|
||||
use tuirealm::{PollStrategy, Update};
|
||||
|
||||
const LOG_CAPACITY: usize = 256;
|
||||
|
||||
impl FileTransferActivity {
|
||||
/// ### tick
|
||||
///
|
||||
/// Call `Application::tick()` and process messages in `Update`
|
||||
pub(super) fn tick(&mut self) {
|
||||
match self.app.tick(PollStrategy::UpTo(3)) {
|
||||
Ok(messages) => {
|
||||
if !messages.is_empty() {
|
||||
self.redraw = true;
|
||||
}
|
||||
for msg in messages.into_iter() {
|
||||
let mut msg = Some(msg);
|
||||
while msg.is_some() {
|
||||
msg = self.update(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
self.mount_error(format!("Application error: {}", err));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### log
|
||||
///
|
||||
/// Add message to log events
|
||||
@@ -57,8 +85,7 @@ impl FileTransferActivity {
|
||||
// Eventually push front the new record
|
||||
self.log_records.push_front(record);
|
||||
// Update log
|
||||
let msg = self.update_logbox();
|
||||
self.update(msg);
|
||||
self.update_logbox();
|
||||
}
|
||||
|
||||
/// ### log_and_alert
|
||||
@@ -68,8 +95,7 @@ impl FileTransferActivity {
|
||||
self.mount_error(msg.as_str());
|
||||
self.log(level, msg);
|
||||
// Update log
|
||||
let msg = self.update_logbox();
|
||||
self.update(msg);
|
||||
self.update_logbox();
|
||||
}
|
||||
|
||||
/// ### init_config_client
|
||||
@@ -108,23 +134,6 @@ impl FileTransferActivity {
|
||||
env::set_var("EDITOR", self.config().get_text_editor());
|
||||
}
|
||||
|
||||
/// ### read_input_event
|
||||
///
|
||||
/// Read one event.
|
||||
/// Returns whether at least one event has been handled
|
||||
pub(super) fn read_input_event(&mut self) -> bool {
|
||||
if let Ok(Some(event)) = self.context().input_hnd().read_event() {
|
||||
// Handle event
|
||||
let msg = self.view.on(event);
|
||||
self.update(msg);
|
||||
// Return true
|
||||
true
|
||||
} else {
|
||||
// Error
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// ### local_to_abs_path
|
||||
///
|
||||
/// Convert a path to absolute according to local explorer
|
||||
@@ -231,4 +240,245 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### update_local_filelist
|
||||
///
|
||||
/// Update local file list
|
||||
pub(super) fn update_local_filelist(&mut self) {
|
||||
// Get width
|
||||
let width: usize = self
|
||||
.context()
|
||||
.store()
|
||||
.get_unsigned(super::STORAGE_EXPLORER_WIDTH)
|
||||
.unwrap_or(256);
|
||||
let hostname: String = match hostname::get() {
|
||||
Ok(h) => {
|
||||
let hostname: String = h.as_os_str().to_string_lossy().to_string();
|
||||
let tokens: Vec<&str> = hostname.split('.').collect();
|
||||
String::from(*tokens.get(0).unwrap_or(&"localhost"))
|
||||
}
|
||||
Err(_) => String::from("localhost"),
|
||||
};
|
||||
let hostname: String = format!(
|
||||
"{}:{} ",
|
||||
hostname,
|
||||
fmt_path_elide_ex(self.local().wrkdir.as_path(), width, hostname.len() + 3) // 3 because of '/…/'
|
||||
);
|
||||
let files: Vec<Vec<TextSpan>> = self
|
||||
.local()
|
||||
.iter_files()
|
||||
.map(|x| vec![TextSpan::from(self.local().fmt_file(x))])
|
||||
.collect();
|
||||
// Update content and title
|
||||
assert!(self
|
||||
.app
|
||||
.attr(
|
||||
&Id::ExplorerLocal,
|
||||
Attribute::Content,
|
||||
AttrValue::Table(files)
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.attr(
|
||||
&Id::ExplorerLocal,
|
||||
Attribute::Title,
|
||||
AttrValue::Title((hostname, Alignment::Left))
|
||||
)
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
/// ### update_remote_filelist
|
||||
///
|
||||
/// Update remote file list
|
||||
pub(super) fn update_remote_filelist(&mut self) {
|
||||
let width: usize = self
|
||||
.context()
|
||||
.store()
|
||||
.get_unsigned(super::STORAGE_EXPLORER_WIDTH)
|
||||
.unwrap_or(256);
|
||||
let hostname = self.get_remote_hostname();
|
||||
let hostname: String = format!(
|
||||
"{}:{} ",
|
||||
hostname,
|
||||
fmt_path_elide_ex(
|
||||
self.remote().wrkdir.as_path(),
|
||||
width,
|
||||
hostname.len() + 3 // 3 because of '/…/'
|
||||
)
|
||||
);
|
||||
let files: Vec<Vec<TextSpan>> = self
|
||||
.remote()
|
||||
.iter_files()
|
||||
.map(|x| vec![TextSpan::from(self.remote().fmt_file(x))])
|
||||
.collect();
|
||||
// Update content and title
|
||||
assert!(self
|
||||
.app
|
||||
.attr(
|
||||
&Id::ExplorerRemote,
|
||||
Attribute::Content,
|
||||
AttrValue::Table(files)
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.attr(
|
||||
&Id::ExplorerRemote,
|
||||
Attribute::Title,
|
||||
AttrValue::Title((hostname, Alignment::Left))
|
||||
)
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
/// ### update_logbox
|
||||
///
|
||||
/// Update log box
|
||||
pub(super) fn update_logbox(&mut self) {
|
||||
let mut table: TableBuilder = TableBuilder::default();
|
||||
for (idx, record) in self.log_records.iter().enumerate() {
|
||||
// Add row if not first row
|
||||
if idx > 0 {
|
||||
table.add_row();
|
||||
}
|
||||
let fg = match record.level {
|
||||
LogLevel::Error => Color::Red,
|
||||
LogLevel::Warn => Color::Yellow,
|
||||
LogLevel::Info => Color::Green,
|
||||
};
|
||||
table
|
||||
.add_col(TextSpan::from(format!(
|
||||
"{}",
|
||||
record.time.format("%Y-%m-%dT%H:%M:%S%Z")
|
||||
)))
|
||||
.add_col(TextSpan::from(" ["))
|
||||
.add_col(
|
||||
TextSpan::new(
|
||||
format!(
|
||||
"{:5}",
|
||||
match record.level {
|
||||
LogLevel::Error => "ERROR",
|
||||
LogLevel::Warn => "WARN",
|
||||
LogLevel::Info => "INFO",
|
||||
}
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.fg(fg),
|
||||
)
|
||||
.add_col(TextSpan::from("]: "))
|
||||
.add_col(TextSpan::from(record.msg.as_str()));
|
||||
}
|
||||
assert!(self
|
||||
.app
|
||||
.attr(
|
||||
&Id::Log,
|
||||
Attribute::Content,
|
||||
AttrValue::Table(table.build())
|
||||
)
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
pub(super) fn update_progress_bar(&mut self, filename: String) {
|
||||
assert!(self
|
||||
.app
|
||||
.attr(
|
||||
&Id::ProgressBarFull,
|
||||
Attribute::Text,
|
||||
AttrValue::String(self.transfer.full.to_string())
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.attr(
|
||||
&Id::ProgressBarFull,
|
||||
Attribute::Value,
|
||||
AttrValue::Payload(PropPayload::One(PropValue::F64(
|
||||
self.transfer.full.calc_progress()
|
||||
)))
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.attr(
|
||||
&Id::ProgressBarPartial,
|
||||
Attribute::Text,
|
||||
AttrValue::String(self.transfer.partial.to_string())
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.attr(
|
||||
&Id::ProgressBarPartial,
|
||||
Attribute::Value,
|
||||
AttrValue::Payload(PropPayload::One(PropValue::F64(
|
||||
self.transfer.partial.calc_progress()
|
||||
)))
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.attr(
|
||||
&Id::ProgressBarPartial,
|
||||
Attribute::Title,
|
||||
AttrValue::Title((filename, Alignment::Left))
|
||||
)
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
/// ### finalize_find
|
||||
///
|
||||
/// Finalize find process
|
||||
pub(super) fn finalize_find(&mut self) {
|
||||
// Set found to none
|
||||
self.browser.del_found();
|
||||
// Restore tab
|
||||
let new_tab = match self.browser.tab() {
|
||||
FileExplorerTab::FindLocal => FileExplorerTab::Local,
|
||||
FileExplorerTab::FindRemote => FileExplorerTab::Remote,
|
||||
_ => FileExplorerTab::Local,
|
||||
};
|
||||
// Give focus to new tab
|
||||
match new_tab {
|
||||
FileExplorerTab::Local => assert!(self.app.active(&Id::ExplorerLocal).is_ok()),
|
||||
FileExplorerTab::Remote => {
|
||||
assert!(self.app.active(&Id::ExplorerRemote).is_ok())
|
||||
}
|
||||
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => {
|
||||
assert!(self.app.active(&Id::ExplorerFind).is_ok())
|
||||
}
|
||||
}
|
||||
self.browser.change_tab(new_tab);
|
||||
}
|
||||
|
||||
pub(super) fn update_find_list(&mut self) {
|
||||
let files: Vec<Vec<TextSpan>> = self
|
||||
.found()
|
||||
.unwrap()
|
||||
.iter_files()
|
||||
.map(|x| vec![TextSpan::from(self.found().unwrap().fmt_file(x))])
|
||||
.collect();
|
||||
assert!(self
|
||||
.app
|
||||
.attr(
|
||||
&Id::ExplorerFind,
|
||||
Attribute::Content,
|
||||
AttrValue::Table(files)
|
||||
)
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
pub(super) fn update_browser_file_list(&mut self) {
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::Local | FileExplorerTab::FindLocal => self.update_local_filelist(),
|
||||
FileExplorerTab::Remote | FileExplorerTab::FindRemote => self.update_remote_filelist(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn update_browser_file_list_swapped(&mut self) {
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::Local | FileExplorerTab::FindLocal => self.update_remote_filelist(),
|
||||
FileExplorerTab::Remote | FileExplorerTab::FindRemote => self.update_local_filelist(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,19 +26,20 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// This module is split into files, cause it's just too big
|
||||
pub(self) mod actions;
|
||||
pub(self) mod lib;
|
||||
pub(self) mod misc;
|
||||
pub(self) mod session;
|
||||
pub(self) mod update;
|
||||
pub(self) mod view;
|
||||
mod actions;
|
||||
mod components;
|
||||
mod lib;
|
||||
mod misc;
|
||||
mod session;
|
||||
mod update;
|
||||
mod view;
|
||||
|
||||
// locals
|
||||
use super::{Activity, Context, ExitReason};
|
||||
use crate::config::themes::Theme;
|
||||
use crate::filetransfer::{FileTransfer, FileTransferProtocol};
|
||||
use crate::filetransfer::{FtpFileTransfer, S3FileTransfer, ScpFileTransfer, SftpFileTransfer};
|
||||
use crate::fs::explorer::FileExplorer;
|
||||
use crate::fs::explorer::{FileExplorer, FileSorting};
|
||||
use crate::fs::FsEntry;
|
||||
use crate::host::Localhost;
|
||||
use crate::system::config_client::ConfigClient;
|
||||
@@ -49,10 +50,10 @@ pub(self) use session::TransferPayload;
|
||||
|
||||
// Includes
|
||||
use chrono::{DateTime, Local};
|
||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||
use std::collections::VecDeque;
|
||||
use std::time::Duration;
|
||||
use tempfile::TempDir;
|
||||
use tuirealm::View;
|
||||
use tuirealm::{Application, EventListenerCfg, NoUserEvent};
|
||||
|
||||
// -- Storage keys
|
||||
|
||||
@@ -61,34 +62,115 @@ const STORAGE_PENDING_TRANSFER: &str = "FILETRANSFER_PENDING_TRANSFER";
|
||||
|
||||
// -- components
|
||||
|
||||
const COMPONENT_EXPLORER_LOCAL: &str = "EXPLORER_LOCAL";
|
||||
const COMPONENT_EXPLORER_REMOTE: &str = "EXPLORER_REMOTE";
|
||||
const COMPONENT_EXPLORER_FIND: &str = "EXPLORER_FIND";
|
||||
const COMPONENT_LOG_BOX: &str = "LOG_BOX";
|
||||
const COMPONENT_PROGRESS_BAR_FULL: &str = "PROGRESS_BAR_FULL";
|
||||
const COMPONENT_PROGRESS_BAR_PARTIAL: &str = "PROGRESS_BAR_PARTIAL";
|
||||
const COMPONENT_TEXT_ERROR: &str = "TEXT_ERROR";
|
||||
const COMPONENT_TEXT_FATAL: &str = "TEXT_FATAL";
|
||||
const COMPONENT_TEXT_HELP: &str = "TEXT_HELP";
|
||||
const COMPONENT_TEXT_WAIT: &str = "TEXT_WAIT";
|
||||
const COMPONENT_INPUT_COPY: &str = "INPUT_COPY";
|
||||
const COMPONENT_INPUT_EXEC: &str = "INPUT_EXEC";
|
||||
const COMPONENT_INPUT_FIND: &str = "INPUT_FIND";
|
||||
const COMPONENT_INPUT_GOTO: &str = "INPUT_GOTO";
|
||||
const COMPONENT_INPUT_MKDIR: &str = "INPUT_MKDIR";
|
||||
const COMPONENT_INPUT_NEWFILE: &str = "INPUT_NEWFILE";
|
||||
const COMPONENT_INPUT_OPEN_WITH: &str = "INPUT_OPEN_WITH";
|
||||
const COMPONENT_INPUT_RENAME: &str = "INPUT_RENAME";
|
||||
const COMPONENT_INPUT_SAVEAS: &str = "INPUT_SAVEAS";
|
||||
const COMPONENT_RADIO_DELETE: &str = "RADIO_DELETE";
|
||||
const COMPONENT_RADIO_REPLACE: &str = "RADIO_REPLACE"; // NOTE: used for file transfers, to choose whether to replace files
|
||||
const COMPONENT_RADIO_DISCONNECT: &str = "RADIO_DISCONNECT";
|
||||
const COMPONENT_RADIO_QUIT: &str = "RADIO_QUIT";
|
||||
const COMPONENT_RADIO_SORTING: &str = "RADIO_SORTING";
|
||||
const COMPONENT_SPAN_STATUS_BAR_LOCAL: &str = "STATUS_BAR_LOCAL";
|
||||
const COMPONENT_SPAN_STATUS_BAR_REMOTE: &str = "STATUS_BAR_REMOTE";
|
||||
const COMPONENT_LIST_FILEINFO: &str = "LIST_FILEINFO";
|
||||
const COMPONENT_LIST_REPLACING_FILES: &str = "LIST_REPLACING_FILES"; // NOTE: used for file transfers, to list files which are going to be replaced
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
||||
enum Id {
|
||||
CopyPopup,
|
||||
DeletePopup,
|
||||
DisconnectPopup,
|
||||
ErrorPopup,
|
||||
ExecPopup,
|
||||
ExplorerFind,
|
||||
ExplorerLocal,
|
||||
ExplorerRemote,
|
||||
FatalPopup,
|
||||
FileInfoPopup,
|
||||
FindPopup,
|
||||
GlobalListener,
|
||||
GotoPopup,
|
||||
KeybindingsPopup,
|
||||
Log,
|
||||
MkdirPopup,
|
||||
NewfilePopup,
|
||||
OpenWithPopup,
|
||||
ProgressBarFull,
|
||||
ProgressBarPartial,
|
||||
QuitPopup,
|
||||
RenamePopup,
|
||||
ReplacePopup,
|
||||
ReplacingFilesListPopup,
|
||||
SaveAsPopup,
|
||||
SortingPopup,
|
||||
StatusBarLocal,
|
||||
StatusBarRemote,
|
||||
WaitPopup,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum Msg {
|
||||
Transfer(TransferMsg),
|
||||
Ui(UiMsg),
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum TransferMsg {
|
||||
AbortTransfer,
|
||||
CopyFileTo(String),
|
||||
DeleteFile,
|
||||
EnterDirectory,
|
||||
ExecuteCmd(String),
|
||||
GoTo(String),
|
||||
GoToParentDirectory,
|
||||
GoToPreviousDirectory,
|
||||
Mkdir(String),
|
||||
NewFile(String),
|
||||
OpenFile,
|
||||
OpenFileWith(String),
|
||||
OpenTextFile,
|
||||
ReloadDir,
|
||||
RenameFile(String),
|
||||
SaveFileAs(String),
|
||||
SearchFile(String),
|
||||
TransferFile,
|
||||
TransferPendingFile,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum UiMsg {
|
||||
ChangeFileSorting(FileSorting),
|
||||
ChangeTransferWindow,
|
||||
CloseCopyPopup,
|
||||
CloseDeletePopup,
|
||||
CloseDisconnectPopup,
|
||||
CloseErrorPopup,
|
||||
CloseExecPopup,
|
||||
CloseFatalPopup,
|
||||
CloseFileInfoPopup,
|
||||
CloseFileSortingPopup,
|
||||
CloseFindExplorer,
|
||||
CloseFindPopup,
|
||||
CloseGotoPopup,
|
||||
CloseKeybindingsPopup,
|
||||
CloseMkdirPopup,
|
||||
CloseNewFilePopup,
|
||||
CloseOpenWithPopup,
|
||||
CloseQuitPopup,
|
||||
CloseReplacePopups,
|
||||
CloseRenamePopup,
|
||||
CloseSaveAsPopup,
|
||||
Disconnect,
|
||||
ExplorerTabbed,
|
||||
LogTabbed,
|
||||
Quit,
|
||||
ReplacePopupTabbed,
|
||||
ShowCopyPopup,
|
||||
ShowDeletePopup,
|
||||
ShowDisconnectPopup,
|
||||
ShowExecPopup,
|
||||
ShowFileInfoPopup,
|
||||
ShowFileSortingPopup,
|
||||
ShowFindPopup,
|
||||
ShowGotoPopup,
|
||||
ShowKeybindingsPopup,
|
||||
ShowMkdirPopup,
|
||||
ShowNewFilePopup,
|
||||
ShowOpenWithPopup,
|
||||
ShowQuitPopup,
|
||||
ShowRenamePopup,
|
||||
ShowSaveAsPopup,
|
||||
ToggleHiddenFiles,
|
||||
ToggleSyncBrowsing,
|
||||
}
|
||||
|
||||
/// ## LogLevel
|
||||
///
|
||||
@@ -125,28 +207,43 @@ impl LogRecord {
|
||||
///
|
||||
/// FileTransferActivity is the data holder for the file transfer activity
|
||||
pub struct FileTransferActivity {
|
||||
exit_reason: Option<ExitReason>, // Exit reason
|
||||
context: Option<Context>, // Context holder
|
||||
view: View, // View
|
||||
host: Localhost, // Localhost
|
||||
client: Box<dyn FileTransfer>, // File transfer client
|
||||
browser: Browser, // Browser
|
||||
log_records: VecDeque<LogRecord>, // Log records
|
||||
transfer: TransferStates, // Transfer states
|
||||
cache: Option<TempDir>, // Temporary directory where to store stuff
|
||||
/// Exit reason
|
||||
exit_reason: Option<ExitReason>,
|
||||
/// Context holder
|
||||
context: Option<Context>,
|
||||
/// Tui-realm application
|
||||
app: Application<Id, Msg, NoUserEvent>,
|
||||
/// Whether should redraw UI
|
||||
redraw: bool,
|
||||
/// Localhost bridge
|
||||
host: Localhost,
|
||||
/// Remote host
|
||||
client: Box<dyn FileTransfer>,
|
||||
/// Browser
|
||||
browser: Browser,
|
||||
/// Current log lines
|
||||
log_records: VecDeque<LogRecord>,
|
||||
transfer: TransferStates,
|
||||
/// Temporary directory where to store temporary stuff
|
||||
cache: Option<TempDir>,
|
||||
}
|
||||
|
||||
impl FileTransferActivity {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiates a new FileTransferActivity
|
||||
pub fn new(host: Localhost, protocol: FileTransferProtocol) -> FileTransferActivity {
|
||||
pub fn new(host: Localhost, protocol: FileTransferProtocol, ticks: Duration) -> Self {
|
||||
// Get config client
|
||||
let config_client: ConfigClient = Self::init_config_client();
|
||||
FileTransferActivity {
|
||||
Self {
|
||||
exit_reason: None,
|
||||
context: None,
|
||||
view: View::init(),
|
||||
app: Application::init(
|
||||
EventListenerCfg::default()
|
||||
.poll_timeout(ticks)
|
||||
.default_input_listener(ticks),
|
||||
),
|
||||
redraw: true,
|
||||
host,
|
||||
client: match protocol {
|
||||
FileTransferProtocol::Sftp => Box::new(SftpFileTransfer::new(
|
||||
@@ -257,9 +354,11 @@ impl Activity for FileTransferActivity {
|
||||
// Set context
|
||||
self.context = Some(context);
|
||||
// Clear terminal
|
||||
self.context_mut().clear_screen();
|
||||
if let Err(err) = self.context.as_mut().unwrap().terminal().clear_screen() {
|
||||
error!("Failed to clear screen: {}", err);
|
||||
}
|
||||
// Put raw mode on enabled
|
||||
if let Err(err) = enable_raw_mode() {
|
||||
if let Err(err) = self.context_mut().terminal().enable_raw_mode() {
|
||||
error!("Failed to enter raw mode: {}", err);
|
||||
}
|
||||
// Get files at current pwd
|
||||
@@ -284,14 +383,12 @@ impl Activity for FileTransferActivity {
|
||||
/// `on_draw` is the function which draws the graphical interface.
|
||||
/// This function must be called at each tick to refresh the interface
|
||||
fn on_draw(&mut self) {
|
||||
// Should ui actually be redrawned?
|
||||
let mut redraw: bool = false;
|
||||
// Context must be something
|
||||
if self.context.is_none() {
|
||||
return;
|
||||
}
|
||||
// Check if connected (popup must be None, otherwise would try reconnecting in loop in case of error)
|
||||
if !self.client.is_connected() && self.view.get_props(COMPONENT_TEXT_FATAL).is_none() {
|
||||
if !self.client.is_connected() && !self.app.mounted(&Id::FatalPopup) {
|
||||
let ftparams = self.context().ft_params().unwrap();
|
||||
// print params
|
||||
let msg: String = Self::get_connection_msg(&ftparams.params);
|
||||
@@ -302,12 +399,11 @@ impl Activity for FileTransferActivity {
|
||||
// Connect to remote
|
||||
self.connect();
|
||||
// Redraw
|
||||
redraw = true;
|
||||
self.redraw = true;
|
||||
}
|
||||
// Handle input events (if false, becomes true; otherwise remains true)
|
||||
redraw |= self.read_input_event();
|
||||
// @! draw interface
|
||||
if redraw {
|
||||
self.tick();
|
||||
// View
|
||||
if self.redraw {
|
||||
self.view();
|
||||
}
|
||||
}
|
||||
@@ -333,20 +429,16 @@ impl Activity for FileTransferActivity {
|
||||
}
|
||||
}
|
||||
// Disable raw mode
|
||||
if let Err(err) = disable_raw_mode() {
|
||||
if let Err(err) = self.context_mut().terminal().disable_raw_mode() {
|
||||
error!("Failed to disable raw mode: {}", err);
|
||||
}
|
||||
if let Err(err) = self.context_mut().terminal().clear_screen() {
|
||||
error!("Failed to clear screen: {}", err);
|
||||
}
|
||||
// Disconnect client
|
||||
if self.client.is_connected() {
|
||||
let _ = self.client.disconnect();
|
||||
}
|
||||
// Clear terminal and return
|
||||
match self.context.take() {
|
||||
Some(mut ctx) => {
|
||||
ctx.clear_screen();
|
||||
Some(ctx)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
self.context.take()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -505,7 +505,7 @@ impl FileTransferActivity {
|
||||
>= 500
|
||||
{
|
||||
// Read events
|
||||
self.read_input_event();
|
||||
self.tick();
|
||||
// Reset instant
|
||||
last_input_event_fetch = Some(Instant::now());
|
||||
}
|
||||
@@ -937,7 +937,7 @@ impl FileTransferActivity {
|
||||
>= 500
|
||||
{
|
||||
// Read events
|
||||
self.read_input_event();
|
||||
self.tick();
|
||||
// Reset instant
|
||||
last_input_event_fetch = Some(Instant::now());
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -27,13 +27,12 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// Locals
|
||||
use super::{SetupActivity, ViewLayout};
|
||||
use super::{Id, IdSsh, IdTheme, SetupActivity, ViewLayout};
|
||||
// Ext
|
||||
use crate::config::themes::Theme;
|
||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||
use std::env;
|
||||
use tuirealm::tui::style::Color;
|
||||
use tuirealm::{Payload, Value};
|
||||
use tuirealm::{State, StateValue};
|
||||
|
||||
impl SetupActivity {
|
||||
/// ### action_on_esc
|
||||
@@ -78,7 +77,7 @@ impl SetupActivity {
|
||||
// Collect input values if in theme form
|
||||
if self.layout == ViewLayout::Theme {
|
||||
self.collect_styles()
|
||||
.map_err(|e| format!("'{}' has an invalid color", e))?;
|
||||
.map_err(|e| format!("'{:?}' has an invalid color", e))?;
|
||||
}
|
||||
// save theme
|
||||
self.save_theme()
|
||||
@@ -93,7 +92,7 @@ impl SetupActivity {
|
||||
ViewLayout::SetupForm => self.collect_input_values(),
|
||||
ViewLayout::Theme => self
|
||||
.collect_styles()
|
||||
.map_err(|e| format!("'{}' has an invalid color", e))?,
|
||||
.map_err(|e| format!("'{:?}' has an invalid color", e))?,
|
||||
_ => {}
|
||||
}
|
||||
// Update view
|
||||
@@ -133,8 +132,8 @@ impl SetupActivity {
|
||||
pub(super) fn action_delete_ssh_key(&mut self) {
|
||||
// Get key
|
||||
// get index
|
||||
let idx: Option<usize> = match self.view.get_state(super::COMPONENT_LIST_SSH_KEYS) {
|
||||
Some(Payload::One(Value::Usize(idx))) => Some(idx),
|
||||
let idx: Option<usize> = match self.app.state(&Id::Ssh(IdSsh::SshKeys)) {
|
||||
Ok(State::One(StateValue::Usize(idx))) => Some(idx),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(idx) = idx {
|
||||
@@ -166,29 +165,27 @@ impl SetupActivity {
|
||||
/// Create a new ssh key
|
||||
pub(super) fn action_new_ssh_key(&mut self) {
|
||||
// get parameters
|
||||
let host: String = match self.view.get_state(super::COMPONENT_INPUT_SSH_HOST) {
|
||||
Some(Payload::One(Value::Str(host))) => host,
|
||||
let host: String = match self.app.state(&Id::Ssh(IdSsh::SshHost)) {
|
||||
Ok(State::One(StateValue::String(host))) => host,
|
||||
_ => String::new(),
|
||||
};
|
||||
let username: String = match self.view.get_state(super::COMPONENT_INPUT_SSH_USERNAME) {
|
||||
Some(Payload::One(Value::Str(user))) => user,
|
||||
let username: String = match self.app.state(&Id::Ssh(IdSsh::SshUsername)) {
|
||||
Ok(State::One(StateValue::String(user))) => user,
|
||||
_ => String::new(),
|
||||
};
|
||||
// Prepare text editor
|
||||
env::set_var("EDITOR", self.config().get_text_editor());
|
||||
let placeholder: String = format!("# Type private SSH key for {}@{}\n", username, host);
|
||||
// Put input mode back to normal
|
||||
if let Err(err) = disable_raw_mode() {
|
||||
error!("Failed to disable raw mode: {}", err);
|
||||
if let Err(err) = self.context_mut().terminal().disable_raw_mode() {
|
||||
error!("Could not disable raw mode: {}", err);
|
||||
}
|
||||
// Leave alternate mode
|
||||
if let Some(ctx) = self.context.as_mut() {
|
||||
ctx.leave_alternate_screen();
|
||||
}
|
||||
// Re-enable raw mode
|
||||
if let Err(err) = enable_raw_mode() {
|
||||
error!("Failed to enter raw mode: {}", err);
|
||||
if let Err(err) = self.context_mut().terminal().leave_alternate_screen() {
|
||||
error!("Could not leave alternate screen: {}", err);
|
||||
}
|
||||
// Lock ports
|
||||
assert!(self.app.lock_ports().is_ok());
|
||||
// Write key to file
|
||||
match edit::edit(placeholder.as_bytes()) {
|
||||
Ok(rsa_key) => {
|
||||
@@ -215,101 +212,246 @@ impl SetupActivity {
|
||||
}
|
||||
// Restore terminal
|
||||
if let Some(ctx) = self.context.as_mut() {
|
||||
// Clear screen
|
||||
ctx.clear_screen();
|
||||
if let Err(err) = ctx.terminal().clear_screen() {
|
||||
error!("Could not clear screen screen: {}", err);
|
||||
}
|
||||
// Enter alternate mode
|
||||
ctx.enter_alternate_screen();
|
||||
if let Err(err) = ctx.terminal().enter_alternate_screen() {
|
||||
error!("Could not enter alternate screen: {}", err);
|
||||
}
|
||||
// Re-enable raw mode
|
||||
if let Err(err) = ctx.terminal().enable_raw_mode() {
|
||||
error!("Failed to enter raw mode: {}", err);
|
||||
}
|
||||
// Unlock ports
|
||||
assert!(self.app.unlock_ports().is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
/// ### set_color
|
||||
///
|
||||
/// Given a component and a color, save the color into the theme
|
||||
pub(super) fn action_save_color(&mut self, component: &str, color: Color) {
|
||||
pub(super) fn action_save_color(&mut self, component: IdTheme, color: Color) {
|
||||
let theme: &mut Theme = self.theme_mut();
|
||||
match component {
|
||||
super::COMPONENT_COLOR_AUTH_ADDR => {
|
||||
IdTheme::AuthAddress => {
|
||||
theme.auth_address = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_AUTH_BOOKMARKS => {
|
||||
IdTheme::AuthBookmarks => {
|
||||
theme.auth_bookmarks = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_AUTH_PASSWORD => {
|
||||
IdTheme::AuthPassword => {
|
||||
theme.auth_password = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_AUTH_PORT => {
|
||||
IdTheme::AuthPort => {
|
||||
theme.auth_port = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_AUTH_PROTOCOL => {
|
||||
IdTheme::AuthProtocol => {
|
||||
theme.auth_protocol = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_AUTH_RECENTS => {
|
||||
IdTheme::AuthRecentHosts => {
|
||||
theme.auth_recents = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_AUTH_USERNAME => {
|
||||
IdTheme::AuthUsername => {
|
||||
theme.auth_username = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_MISC_ERROR => {
|
||||
IdTheme::MiscError => {
|
||||
theme.misc_error_dialog = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_MISC_INFO => {
|
||||
IdTheme::MiscInfo => {
|
||||
theme.misc_info_dialog = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_MISC_INPUT => {
|
||||
IdTheme::MiscInput => {
|
||||
theme.misc_input_dialog = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_MISC_KEYS => {
|
||||
IdTheme::MiscKeys => {
|
||||
theme.misc_keys = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_MISC_QUIT => {
|
||||
IdTheme::MiscQuit => {
|
||||
theme.misc_quit_dialog = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_MISC_SAVE => {
|
||||
IdTheme::MiscSave => {
|
||||
theme.misc_save_dialog = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_MISC_WARN => {
|
||||
IdTheme::MiscWarn => {
|
||||
theme.misc_warn_dialog = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG => {
|
||||
IdTheme::ExplorerLocalBg => {
|
||||
theme.transfer_local_explorer_background = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG => {
|
||||
IdTheme::ExplorerLocalFg => {
|
||||
theme.transfer_local_explorer_foreground = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG => {
|
||||
IdTheme::ExplorerLocalHg => {
|
||||
theme.transfer_local_explorer_highlighted = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG => {
|
||||
IdTheme::ExplorerRemoteBg => {
|
||||
theme.transfer_remote_explorer_background = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG => {
|
||||
IdTheme::ExplorerRemoteFg => {
|
||||
theme.transfer_remote_explorer_foreground = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG => {
|
||||
IdTheme::ExplorerRemoteHg => {
|
||||
theme.transfer_remote_explorer_highlighted = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_TRANSFER_LOG_BG => {
|
||||
IdTheme::LogBg => {
|
||||
theme.transfer_log_background = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_TRANSFER_LOG_WIN => {
|
||||
IdTheme::LogWindow => {
|
||||
theme.transfer_log_window = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL => {
|
||||
IdTheme::ProgBarFull => {
|
||||
theme.transfer_progress_bar_full = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL => {
|
||||
IdTheme::ProgBarPartial => {
|
||||
theme.transfer_progress_bar_partial = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN => {
|
||||
IdTheme::StatusHidden => {
|
||||
theme.transfer_status_hidden = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_TRANSFER_STATUS_SORTING => {
|
||||
IdTheme::StatusSorting => {
|
||||
theme.transfer_status_sorting = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_TRANSFER_STATUS_SYNC => {
|
||||
IdTheme::StatusSync => {
|
||||
theme.transfer_status_sync_browsing = color;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### collect_styles
|
||||
///
|
||||
/// Collect values from input and put them into the theme.
|
||||
/// If a component has an invalid color, returns Err(component_id)
|
||||
fn collect_styles(&mut self) -> Result<(), Id> {
|
||||
// auth
|
||||
let auth_address = self
|
||||
.get_color(&Id::Theme(IdTheme::AuthAddress))
|
||||
.map_err(|_| Id::Theme(IdTheme::AuthAddress))?;
|
||||
let auth_bookmarks = self
|
||||
.get_color(&Id::Theme(IdTheme::AuthBookmarks))
|
||||
.map_err(|_| Id::Theme(IdTheme::AuthBookmarks))?;
|
||||
let auth_password = self
|
||||
.get_color(&Id::Theme(IdTheme::AuthPassword))
|
||||
.map_err(|_| Id::Theme(IdTheme::AuthPassword))?;
|
||||
let auth_port = self
|
||||
.get_color(&Id::Theme(IdTheme::AuthPort))
|
||||
.map_err(|_| Id::Theme(IdTheme::AuthPort))?;
|
||||
let auth_protocol = self
|
||||
.get_color(&Id::Theme(IdTheme::AuthProtocol))
|
||||
.map_err(|_| Id::Theme(IdTheme::AuthProtocol))?;
|
||||
let auth_recents = self
|
||||
.get_color(&Id::Theme(IdTheme::AuthRecentHosts))
|
||||
.map_err(|_| Id::Theme(IdTheme::AuthRecentHosts))?;
|
||||
let auth_username = self
|
||||
.get_color(&Id::Theme(IdTheme::AuthUsername))
|
||||
.map_err(|_| Id::Theme(IdTheme::AuthUsername))?;
|
||||
// misc
|
||||
let misc_error_dialog = self
|
||||
.get_color(&Id::Theme(IdTheme::MiscError))
|
||||
.map_err(|_| Id::Theme(IdTheme::MiscError))?;
|
||||
let misc_info_dialog = self
|
||||
.get_color(&Id::Theme(IdTheme::MiscInfo))
|
||||
.map_err(|_| Id::Theme(IdTheme::MiscInfo))?;
|
||||
let misc_input_dialog = self
|
||||
.get_color(&Id::Theme(IdTheme::MiscInput))
|
||||
.map_err(|_| Id::Theme(IdTheme::MiscInput))?;
|
||||
let misc_keys = self
|
||||
.get_color(&Id::Theme(IdTheme::MiscKeys))
|
||||
.map_err(|_| Id::Theme(IdTheme::MiscKeys))?;
|
||||
let misc_quit_dialog = self
|
||||
.get_color(&Id::Theme(IdTheme::MiscQuit))
|
||||
.map_err(|_| Id::Theme(IdTheme::MiscQuit))?;
|
||||
let misc_save_dialog = self
|
||||
.get_color(&Id::Theme(IdTheme::MiscSave))
|
||||
.map_err(|_| Id::Theme(IdTheme::MiscSave))?;
|
||||
let misc_warn_dialog = self
|
||||
.get_color(&Id::Theme(IdTheme::MiscWarn))
|
||||
.map_err(|_| Id::Theme(IdTheme::MiscWarn))?;
|
||||
// transfer
|
||||
let transfer_local_explorer_background = self
|
||||
.get_color(&Id::Theme(IdTheme::ExplorerLocalBg))
|
||||
.map_err(|_| Id::Theme(IdTheme::ExplorerLocalBg))?;
|
||||
let transfer_local_explorer_foreground = self
|
||||
.get_color(&Id::Theme(IdTheme::ExplorerLocalFg))
|
||||
.map_err(|_| Id::Theme(IdTheme::ExplorerLocalFg))?;
|
||||
let transfer_local_explorer_highlighted = self
|
||||
.get_color(&Id::Theme(IdTheme::ExplorerLocalHg))
|
||||
.map_err(|_| Id::Theme(IdTheme::ExplorerLocalHg))?;
|
||||
let transfer_remote_explorer_background = self
|
||||
.get_color(&Id::Theme(IdTheme::ExplorerRemoteBg))
|
||||
.map_err(|_| Id::Theme(IdTheme::ExplorerRemoteBg))?;
|
||||
let transfer_remote_explorer_foreground = self
|
||||
.get_color(&Id::Theme(IdTheme::ExplorerRemoteFg))
|
||||
.map_err(|_| Id::Theme(IdTheme::ExplorerRemoteFg))?;
|
||||
let transfer_remote_explorer_highlighted = self
|
||||
.get_color(&Id::Theme(IdTheme::ExplorerRemoteHg))
|
||||
.map_err(|_| Id::Theme(IdTheme::ExplorerRemoteHg))?;
|
||||
let transfer_log_background = self
|
||||
.get_color(&Id::Theme(IdTheme::LogBg))
|
||||
.map_err(|_| Id::Theme(IdTheme::LogBg))?;
|
||||
let transfer_log_window = self
|
||||
.get_color(&Id::Theme(IdTheme::LogWindow))
|
||||
.map_err(|_| Id::Theme(IdTheme::LogWindow))?;
|
||||
let transfer_progress_bar_full = self
|
||||
.get_color(&Id::Theme(IdTheme::ProgBarFull))
|
||||
.map_err(|_| Id::Theme(IdTheme::ProgBarFull))?;
|
||||
let transfer_progress_bar_partial = self
|
||||
.get_color(&Id::Theme(IdTheme::ProgBarPartial))
|
||||
.map_err(|_| Id::Theme(IdTheme::ProgBarPartial))?;
|
||||
let transfer_status_hidden = self
|
||||
.get_color(&Id::Theme(IdTheme::StatusHidden))
|
||||
.map_err(|_| Id::Theme(IdTheme::StatusHidden))?;
|
||||
let transfer_status_sorting = self
|
||||
.get_color(&Id::Theme(IdTheme::StatusSorting))
|
||||
.map_err(|_| Id::Theme(IdTheme::StatusSorting))?;
|
||||
let transfer_status_sync_browsing = self
|
||||
.get_color(&Id::Theme(IdTheme::StatusSync))
|
||||
.map_err(|_| Id::Theme(IdTheme::StatusSync))?;
|
||||
// Update theme
|
||||
let mut theme: &mut Theme = self.theme_mut();
|
||||
theme.auth_address = auth_address;
|
||||
theme.auth_bookmarks = auth_bookmarks;
|
||||
theme.auth_password = auth_password;
|
||||
theme.auth_port = auth_port;
|
||||
theme.auth_protocol = auth_protocol;
|
||||
theme.auth_recents = auth_recents;
|
||||
theme.auth_username = auth_username;
|
||||
theme.misc_error_dialog = misc_error_dialog;
|
||||
theme.misc_info_dialog = misc_info_dialog;
|
||||
theme.misc_input_dialog = misc_input_dialog;
|
||||
theme.misc_keys = misc_keys;
|
||||
theme.misc_quit_dialog = misc_quit_dialog;
|
||||
theme.misc_save_dialog = misc_save_dialog;
|
||||
theme.misc_warn_dialog = misc_warn_dialog;
|
||||
theme.transfer_local_explorer_background = transfer_local_explorer_background;
|
||||
theme.transfer_local_explorer_foreground = transfer_local_explorer_foreground;
|
||||
theme.transfer_local_explorer_highlighted = transfer_local_explorer_highlighted;
|
||||
theme.transfer_remote_explorer_background = transfer_remote_explorer_background;
|
||||
theme.transfer_remote_explorer_foreground = transfer_remote_explorer_foreground;
|
||||
theme.transfer_remote_explorer_highlighted = transfer_remote_explorer_highlighted;
|
||||
theme.transfer_log_background = transfer_log_background;
|
||||
theme.transfer_log_window = transfer_log_window;
|
||||
theme.transfer_progress_bar_full = transfer_progress_bar_full;
|
||||
theme.transfer_progress_bar_partial = transfer_progress_bar_partial;
|
||||
theme.transfer_status_hidden = transfer_status_hidden;
|
||||
theme.transfer_status_sorting = transfer_status_sorting;
|
||||
theme.transfer_status_sync_browsing = transfer_status_sync_browsing;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// ### get_color
|
||||
///
|
||||
/// Get color from component
|
||||
fn get_color(&self, component: &Id) -> Result<Color, ()> {
|
||||
match self.app.state(component) {
|
||||
Ok(State::One(StateValue::String(color))) => {
|
||||
match crate::utils::parser::parse_color(color.as_str()) {
|
||||
Some(c) => Ok(c),
|
||||
None => Err(()),
|
||||
}
|
||||
}
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
334
src/ui/activities/setup/components/commons.rs
Normal file
334
src/ui/activities/setup/components/commons.rs
Normal file
@@ -0,0 +1,334 @@
|
||||
//! ## Config
|
||||
//!
|
||||
//! config tab components
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
use super::{CommonMsg, Msg, ViewLayout};
|
||||
|
||||
use tui_realm_stdlib::{List, Paragraph, Radio, Span};
|
||||
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
|
||||
use tuirealm::event::{Key, KeyEvent};
|
||||
use tuirealm::props::{Alignment, BorderSides, BorderType, Borders, Color, TableBuilder, TextSpan};
|
||||
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct ErrorPopup {
|
||||
component: Paragraph,
|
||||
}
|
||||
|
||||
impl ErrorPopup {
|
||||
pub fn new<S: AsRef<str>>(text: S) -> Self {
|
||||
Self {
|
||||
component: Paragraph::default()
|
||||
.alignment(Alignment::Center)
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(Color::Red)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(Color::Red)
|
||||
.text(&[TextSpan::from(text.as_ref())])
|
||||
.wrap(true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for ErrorPopup {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Esc | Key::Enter,
|
||||
..
|
||||
}) => Some(Msg::Common(CommonMsg::CloseErrorPopup)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct Footer {
|
||||
component: Span,
|
||||
}
|
||||
|
||||
impl Default for Footer {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
component: Span::default().spans(&[
|
||||
TextSpan::new("Press ").bold(),
|
||||
TextSpan::new("<CTRL+H>").bold().fg(Color::Cyan),
|
||||
TextSpan::new(" to show keybindings; ").bold(),
|
||||
TextSpan::new("<CTRL+S>").bold().fg(Color::Cyan),
|
||||
TextSpan::new(" to save parameters; ").bold(),
|
||||
TextSpan::new("<TAB>").bold().fg(Color::Cyan),
|
||||
TextSpan::new(" to change panel").bold(),
|
||||
]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for Footer {
|
||||
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct Header {
|
||||
component: Radio,
|
||||
}
|
||||
|
||||
impl Header {
|
||||
pub fn new(layout: ViewLayout) -> Self {
|
||||
Self {
|
||||
component: Radio::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(Color::Yellow)
|
||||
.sides(BorderSides::BOTTOM),
|
||||
)
|
||||
.choices(&["User interface", "SSH Keys", "Theme"])
|
||||
.foreground(Color::Yellow)
|
||||
.value(match layout {
|
||||
ViewLayout::SetupForm => 0,
|
||||
ViewLayout::SshKeys => 1,
|
||||
ViewLayout::Theme => 2,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for Header {
|
||||
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct Keybindings {
|
||||
component: List,
|
||||
}
|
||||
|
||||
impl Default for Keybindings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
component: List::default()
|
||||
.borders(Borders::default().modifiers(BorderType::Rounded))
|
||||
.title("Keybindings", Alignment::Center)
|
||||
.scroll(true)
|
||||
.highlighted_str("? ")
|
||||
.rows(
|
||||
TableBuilder::default()
|
||||
.add_col(TextSpan::new("<ESC>").bold().fg(Color::Cyan))
|
||||
.add_col(TextSpan::from(" Exit setup"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::new("<TAB>").bold().fg(Color::Cyan))
|
||||
.add_col(TextSpan::from(" Change setup page"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::new("<RIGHT/LEFT>").bold().fg(Color::Cyan))
|
||||
.add_col(TextSpan::from(" Change cursor"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::new("<UP/DOWN>").bold().fg(Color::Cyan))
|
||||
.add_col(TextSpan::from(" Change input field"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::new("<ENTER>").bold().fg(Color::Cyan))
|
||||
.add_col(TextSpan::from(" Select / Dismiss popup"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::new("<DEL|E>").bold().fg(Color::Cyan))
|
||||
.add_col(TextSpan::from(" Delete SSH key"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::new("<CTRL+N>").bold().fg(Color::Cyan))
|
||||
.add_col(TextSpan::from(" New SSH key"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::new("<CTRL+R>").bold().fg(Color::Cyan))
|
||||
.add_col(TextSpan::from(" Revert changes"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::new("<CTRL+S>").bold().fg(Color::Cyan))
|
||||
.add_col(TextSpan::from(" Save configuration"))
|
||||
.build(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for Keybindings {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Esc | Key::Enter,
|
||||
..
|
||||
}) => Some(Msg::Common(CommonMsg::CloseKeybindingsPopup)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Down, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Down));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
||||
self.perform(Cmd::Move(Direction::Up));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::PageDown,
|
||||
..
|
||||
}) => {
|
||||
self.perform(Cmd::Scroll(Direction::Down));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::PageUp, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Scroll(Direction::Up));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Home, ..
|
||||
}) => {
|
||||
self.perform(Cmd::GoTo(Position::Begin));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||
self.perform(Cmd::GoTo(Position::End));
|
||||
Some(Msg::None)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct QuitPopup {
|
||||
component: Radio,
|
||||
}
|
||||
|
||||
impl Default for QuitPopup {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
component: Radio::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(Color::Red)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(Color::Red)
|
||||
.title(
|
||||
"There are unsaved changes! Save changes before leaving?",
|
||||
Alignment::Center,
|
||||
)
|
||||
.rewind(true)
|
||||
.choices(&["Save", "Don't save", "Cancel"]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for QuitPopup {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||
Some(Msg::Common(CommonMsg::CloseQuitPopup))
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Left, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Left));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Right, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Right));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => match self.perform(Cmd::Submit) {
|
||||
CmdResult::Submit(State::One(StateValue::Usize(0))) => {
|
||||
Some(Msg::Common(CommonMsg::SaveAndQuit))
|
||||
}
|
||||
CmdResult::Submit(State::One(StateValue::Usize(1))) => {
|
||||
Some(Msg::Common(CommonMsg::Quit))
|
||||
}
|
||||
_ => Some(Msg::Common(CommonMsg::CloseQuitPopup)),
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct SavePopup {
|
||||
component: Radio,
|
||||
}
|
||||
|
||||
impl Default for SavePopup {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
component: Radio::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(Color::Yellow)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(Color::Yellow)
|
||||
.title("Save changes?", Alignment::Center)
|
||||
.rewind(true)
|
||||
.choices(&["Yes", "No"]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for SavePopup {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||
Some(Msg::Common(CommonMsg::CloseSavePopup))
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Left, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Left));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Right, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Right));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => {
|
||||
if matches!(
|
||||
self.perform(Cmd::Submit),
|
||||
CmdResult::Submit(State::One(StateValue::Usize(0)))
|
||||
) {
|
||||
Some(Msg::Common(CommonMsg::SaveConfig))
|
||||
} else {
|
||||
Some(Msg::Common(CommonMsg::CloseSavePopup))
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
489
src/ui/activities/setup/components/config.rs
Normal file
489
src/ui/activities/setup/components/config.rs
Normal file
@@ -0,0 +1,489 @@
|
||||
//! ## Config
|
||||
//!
|
||||
//! config tab components
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
use super::{ConfigMsg, Msg};
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
use crate::fs::explorer::GroupDirs as GroupDirsEnum;
|
||||
use crate::utils::parser::parse_bytesize;
|
||||
|
||||
use tui_realm_stdlib::{Input, Radio};
|
||||
use tuirealm::command::{Cmd, Direction, Position};
|
||||
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
|
||||
use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style};
|
||||
use tuirealm::{Component, Event, MockComponent, NoUserEvent};
|
||||
|
||||
// -- components
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct CheckUpdates {
|
||||
component: Radio,
|
||||
}
|
||||
|
||||
impl CheckUpdates {
|
||||
pub fn new(enabled: bool) -> Self {
|
||||
Self {
|
||||
component: Radio::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(Color::LightYellow)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.choices(&["Yes", "No"])
|
||||
.foreground(Color::LightYellow)
|
||||
.rewind(true)
|
||||
.title("Check for updates?", Alignment::Left)
|
||||
.value(if enabled { 0 } else { 1 }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for CheckUpdates {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
handle_radio_ev(
|
||||
self,
|
||||
ev,
|
||||
Msg::Config(ConfigMsg::CheckUpdatesBlurDown),
|
||||
Msg::Config(ConfigMsg::CheckUpdatesBlurUp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct DefaultProtocol {
|
||||
component: Radio,
|
||||
}
|
||||
|
||||
impl DefaultProtocol {
|
||||
pub fn new(protocol: FileTransferProtocol) -> Self {
|
||||
Self {
|
||||
component: Radio::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(Color::Cyan)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.choices(&["SFTP", "SCP", "FTP", "FTPS", "AWS S3"])
|
||||
.foreground(Color::Cyan)
|
||||
.rewind(true)
|
||||
.title("Default protocol", Alignment::Left)
|
||||
.value(match protocol {
|
||||
FileTransferProtocol::AwsS3 => 4,
|
||||
FileTransferProtocol::Ftp(true) => 3,
|
||||
FileTransferProtocol::Ftp(false) => 2,
|
||||
FileTransferProtocol::Scp => 1,
|
||||
FileTransferProtocol::Sftp => 0,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for DefaultProtocol {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
handle_radio_ev(
|
||||
self,
|
||||
ev,
|
||||
Msg::Config(ConfigMsg::DefaultProtocolBlurDown),
|
||||
Msg::Config(ConfigMsg::DefaultProtocolBlurUp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct GroupDirs {
|
||||
component: Radio,
|
||||
}
|
||||
|
||||
impl GroupDirs {
|
||||
pub fn new(opt: Option<GroupDirsEnum>) -> Self {
|
||||
Self {
|
||||
component: Radio::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(Color::LightMagenta)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.choices(&["Display first", "Display last", "No"])
|
||||
.foreground(Color::LightMagenta)
|
||||
.rewind(true)
|
||||
.title("Group directories", Alignment::Left)
|
||||
.value(match opt {
|
||||
Some(GroupDirsEnum::First) => 0,
|
||||
Some(GroupDirsEnum::Last) => 1,
|
||||
None => 2,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for GroupDirs {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
handle_radio_ev(
|
||||
self,
|
||||
ev,
|
||||
Msg::Config(ConfigMsg::GroupDirsBlurDown),
|
||||
Msg::Config(ConfigMsg::GroupDirsBlurUp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct HiddenFiles {
|
||||
component: Radio,
|
||||
}
|
||||
|
||||
impl HiddenFiles {
|
||||
pub fn new(enabled: bool) -> Self {
|
||||
Self {
|
||||
component: Radio::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(Color::LightRed)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.choices(&["Yes", "No"])
|
||||
.foreground(Color::LightRed)
|
||||
.rewind(true)
|
||||
.title("Show hidden files? (by default)", Alignment::Left)
|
||||
.value(if enabled { 0 } else { 1 }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for HiddenFiles {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
handle_radio_ev(
|
||||
self,
|
||||
ev,
|
||||
Msg::Config(ConfigMsg::HiddenFilesBlurDown),
|
||||
Msg::Config(ConfigMsg::HiddenFilesBlurUp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct NotificationsEnabled {
|
||||
component: Radio,
|
||||
}
|
||||
|
||||
impl NotificationsEnabled {
|
||||
pub fn new(enabled: bool) -> Self {
|
||||
Self {
|
||||
component: Radio::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(Color::LightRed)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.choices(&["Yes", "No"])
|
||||
.foreground(Color::LightRed)
|
||||
.rewind(true)
|
||||
.title("Enable notifications?", Alignment::Left)
|
||||
.value(if enabled { 0 } else { 1 }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for NotificationsEnabled {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
handle_radio_ev(
|
||||
self,
|
||||
ev,
|
||||
Msg::Config(ConfigMsg::NotificationsEnabledBlurDown),
|
||||
Msg::Config(ConfigMsg::NotificationsEnabledBlurUp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct PromptOnFileReplace {
|
||||
component: Radio,
|
||||
}
|
||||
|
||||
impl PromptOnFileReplace {
|
||||
pub fn new(enabled: bool) -> Self {
|
||||
Self {
|
||||
component: Radio::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(Color::LightBlue)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.choices(&["Yes", "No"])
|
||||
.foreground(Color::LightBlue)
|
||||
.rewind(true)
|
||||
.title("Prompt when replacing existing files?", Alignment::Left)
|
||||
.value(if enabled { 0 } else { 1 }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for PromptOnFileReplace {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
handle_radio_ev(
|
||||
self,
|
||||
ev,
|
||||
Msg::Config(ConfigMsg::PromptOnFileReplaceBlurDown),
|
||||
Msg::Config(ConfigMsg::PromptOnFileReplaceBlurUp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct LocalFileFmt {
|
||||
component: Input,
|
||||
}
|
||||
|
||||
impl LocalFileFmt {
|
||||
pub fn new(value: &str) -> Self {
|
||||
Self {
|
||||
component: Input::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(Color::LightGreen)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(Color::LightGreen)
|
||||
.input_type(InputType::Text)
|
||||
.placeholder(
|
||||
"{NAME:36} {PEX} {SIZE} {MTIME:17:%b %d %Y %H:%M}",
|
||||
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||
)
|
||||
.title("File formatter syntax (local)", Alignment::Left)
|
||||
.value(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for LocalFileFmt {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
handle_input_ev(
|
||||
self,
|
||||
ev,
|
||||
Msg::Config(ConfigMsg::LocalFileFmtBlurDown),
|
||||
Msg::Config(ConfigMsg::LocalFileFmtBlurUp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct NotificationsThreshold {
|
||||
component: Input,
|
||||
}
|
||||
|
||||
impl NotificationsThreshold {
|
||||
pub fn new(value: &str) -> Self {
|
||||
// -- validators
|
||||
fn validate(bytes: &str) -> bool {
|
||||
parse_bytesize(bytes).is_some()
|
||||
}
|
||||
fn char_valid(_input: &str, incoming: char) -> bool {
|
||||
incoming.is_digit(10) || ['B', 'K', 'M', 'G', 'T', 'P'].contains(&incoming)
|
||||
}
|
||||
Self {
|
||||
component: Input::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(Color::LightYellow)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(Color::LightYellow)
|
||||
.invalid_style(Style::default().fg(Color::Red))
|
||||
.input_type(InputType::Custom(validate, char_valid))
|
||||
.placeholder("64 MB", Style::default().fg(Color::Rgb(128, 128, 128)))
|
||||
.title("Notifications: minimum transfer size", Alignment::Left)
|
||||
.value(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for NotificationsThreshold {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
handle_input_ev(
|
||||
self,
|
||||
ev,
|
||||
Msg::Config(ConfigMsg::NotificationsThresholdBlurDown),
|
||||
Msg::Config(ConfigMsg::NotificationsThresholdBlurUp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct RemoteFileFmt {
|
||||
component: Input,
|
||||
}
|
||||
|
||||
impl RemoteFileFmt {
|
||||
pub fn new(value: &str) -> Self {
|
||||
Self {
|
||||
component: Input::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(Color::Cyan)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(Color::Cyan)
|
||||
.input_type(InputType::Text)
|
||||
.placeholder(
|
||||
"{NAME:36} {PEX} {SIZE} {MTIME:17:%b %d %Y %H:%M}",
|
||||
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||
)
|
||||
.title("File formatter syntax (remote)", Alignment::Left)
|
||||
.value(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for RemoteFileFmt {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
handle_input_ev(
|
||||
self,
|
||||
ev,
|
||||
Msg::Config(ConfigMsg::RemoteFileFmtBlurDown),
|
||||
Msg::Config(ConfigMsg::RemoteFileFmtBlurUp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct TextEditor {
|
||||
component: Input,
|
||||
}
|
||||
|
||||
impl TextEditor {
|
||||
pub fn new(value: &str) -> Self {
|
||||
Self {
|
||||
component: Input::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(Color::LightGreen)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(Color::LightGreen)
|
||||
.input_type(InputType::Text)
|
||||
.placeholder("vim", Style::default().fg(Color::Rgb(128, 128, 128)))
|
||||
.title("Text editor", Alignment::Left)
|
||||
.value(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for TextEditor {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
handle_input_ev(
|
||||
self,
|
||||
ev,
|
||||
Msg::Config(ConfigMsg::TextEditorBlurDown),
|
||||
Msg::Config(ConfigMsg::TextEditorBlurUp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// -- event handler
|
||||
|
||||
fn handle_input_ev(
|
||||
component: &mut dyn Component<Msg, NoUserEvent>,
|
||||
ev: Event<NoUserEvent>,
|
||||
on_key_down: Msg,
|
||||
on_key_up: Msg,
|
||||
) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Left, ..
|
||||
}) => {
|
||||
component.perform(Cmd::Move(Direction::Left));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Right, ..
|
||||
}) => {
|
||||
component.perform(Cmd::Move(Direction::Right));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Home, ..
|
||||
}) => {
|
||||
component.perform(Cmd::GoTo(Position::Begin));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||
component.perform(Cmd::GoTo(Position::End));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Delete, ..
|
||||
}) => {
|
||||
component.perform(Cmd::Cancel);
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Backspace,
|
||||
..
|
||||
}) => {
|
||||
component.perform(Cmd::Delete);
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char(ch),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => {
|
||||
component.perform(Cmd::Type(ch));
|
||||
Some(Msg::Config(ConfigMsg::ConfigChanged))
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Down, ..
|
||||
}) => Some(on_key_down),
|
||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(on_key_up),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_radio_ev(
|
||||
component: &mut dyn Component<Msg, NoUserEvent>,
|
||||
ev: Event<NoUserEvent>,
|
||||
on_key_down: Msg,
|
||||
on_key_up: Msg,
|
||||
) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Left, ..
|
||||
}) => {
|
||||
component.perform(Cmd::Move(Direction::Left));
|
||||
Some(Msg::Config(ConfigMsg::ConfigChanged))
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Right, ..
|
||||
}) => {
|
||||
component.perform(Cmd::Move(Direction::Right));
|
||||
Some(Msg::Config(ConfigMsg::ConfigChanged))
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Down, ..
|
||||
}) => Some(on_key_down),
|
||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(on_key_up),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
86
src/ui/activities/setup/components/mod.rs
Normal file
86
src/ui/activities/setup/components/mod.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
//! ## Components
|
||||
//!
|
||||
//! setup activity components
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
use super::{CommonMsg, ConfigMsg, Msg, SshMsg, ThemeMsg, ViewLayout};
|
||||
|
||||
mod commons;
|
||||
mod config;
|
||||
mod ssh;
|
||||
mod theme;
|
||||
|
||||
pub(super) use commons::{ErrorPopup, Footer, Header, Keybindings, QuitPopup, SavePopup};
|
||||
pub(super) use config::{
|
||||
CheckUpdates, DefaultProtocol, GroupDirs, HiddenFiles, LocalFileFmt, NotificationsEnabled,
|
||||
NotificationsThreshold, PromptOnFileReplace, RemoteFileFmt, TextEditor,
|
||||
};
|
||||
pub(super) use ssh::{DelSshKeyPopup, SshHost, SshKeys, SshUsername};
|
||||
pub(super) use theme::*;
|
||||
|
||||
use tui_realm_stdlib::Phantom;
|
||||
use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent};
|
||||
use tuirealm::{Component, MockComponent};
|
||||
|
||||
// -- global listener
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct GlobalListener {
|
||||
component: Phantom,
|
||||
}
|
||||
|
||||
impl Default for GlobalListener {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
component: Phantom::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for GlobalListener {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||
Some(Msg::Common(CommonMsg::ShowQuitPopup))
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
|
||||
Some(Msg::Common(CommonMsg::ChangeLayout))
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('h'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
}) => Some(Msg::Common(CommonMsg::ShowKeybindings)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('r'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
}) => Some(Msg::Common(CommonMsg::RevertChanges)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('s'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
}) => Some(Msg::Common(CommonMsg::ShowSavePopup)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
339
src/ui/activities/setup/components/ssh.rs
Normal file
339
src/ui/activities/setup/components/ssh.rs
Normal file
@@ -0,0 +1,339 @@
|
||||
//! ## Ssh
|
||||
//!
|
||||
//! ssh components
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
use super::{Msg, SshMsg};
|
||||
|
||||
use tui_realm_stdlib::{Input, List, Radio};
|
||||
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
|
||||
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
|
||||
use tuirealm::props::{
|
||||
Alignment, BorderSides, BorderType, Borders, Color, InputType, Style, TextSpan,
|
||||
};
|
||||
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
|
||||
|
||||
/* DelSshKeyPopup,
|
||||
SshHost,
|
||||
SshKeys,
|
||||
SshUsername, */
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct DelSshKeyPopup {
|
||||
component: Radio,
|
||||
}
|
||||
|
||||
impl Default for DelSshKeyPopup {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
component: Radio::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(Color::Red)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.choices(&["Yes", "No"])
|
||||
.foreground(Color::Red)
|
||||
.rewind(true)
|
||||
.title("Delete key?", Alignment::Center)
|
||||
.value(1),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for DelSshKeyPopup {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||
Some(Msg::Ssh(SshMsg::CloseDelSshKeyPopup))
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Left, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Left));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Right, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Right));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => {
|
||||
if matches!(
|
||||
self.perform(Cmd::Submit),
|
||||
CmdResult::Submit(State::One(StateValue::Usize(0)))
|
||||
) {
|
||||
Some(Msg::Ssh(SshMsg::DeleteSshKey))
|
||||
} else {
|
||||
Some(Msg::Ssh(SshMsg::CloseDelSshKeyPopup))
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct SshKeys {
|
||||
component: List,
|
||||
}
|
||||
|
||||
impl SshKeys {
|
||||
pub fn new(keys: &[String]) -> Self {
|
||||
Self {
|
||||
component: List::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(Color::LightGreen)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(Color::LightGreen)
|
||||
.highlighted_color(Color::LightGreen)
|
||||
.rewind(true)
|
||||
.rows(keys.iter().map(|x| vec![TextSpan::from(x)]).collect())
|
||||
.step(4)
|
||||
.scroll(true)
|
||||
.title("SSH Keys", Alignment::Left),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for SshKeys {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Down, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Down));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
||||
self.perform(Cmd::Move(Direction::Up));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::PageDown,
|
||||
..
|
||||
}) => {
|
||||
self.perform(Cmd::Scroll(Direction::Down));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::PageUp, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Scroll(Direction::Up));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Home, ..
|
||||
}) => {
|
||||
self.perform(Cmd::GoTo(Position::Begin));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||
self.perform(Cmd::GoTo(Position::End));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => match self.state() {
|
||||
State::One(StateValue::Usize(choice)) => Some(Msg::Ssh(SshMsg::EditSshKey(choice))),
|
||||
_ => Some(Msg::None),
|
||||
},
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Delete, ..
|
||||
}) => Some(Msg::Ssh(SshMsg::ShowDelSshKeyPopup)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('n'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
}) => Some(Msg::Ssh(SshMsg::ShowNewSshKeyPopup)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct SshHost {
|
||||
component: Input,
|
||||
}
|
||||
|
||||
impl Default for SshHost {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
component: Input::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.sides(BorderSides::TOP | BorderSides::RIGHT | BorderSides::LEFT),
|
||||
)
|
||||
.input_type(InputType::Text)
|
||||
.placeholder(
|
||||
"192.168.1.2",
|
||||
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||
)
|
||||
.title("Hostname or address", Alignment::Center),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for SshHost {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Left, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Left));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Right, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Right));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Home, ..
|
||||
}) => {
|
||||
self.perform(Cmd::GoTo(Position::Begin));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||
self.perform(Cmd::GoTo(Position::End));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Delete, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Cancel);
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Backspace,
|
||||
..
|
||||
}) => {
|
||||
self.perform(Cmd::Delete);
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char(ch),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => {
|
||||
self.perform(Cmd::Type(ch));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => Some(Msg::Ssh(SshMsg::SaveSshKey)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Down, ..
|
||||
}) => Some(Msg::Ssh(SshMsg::SshHostBlur)),
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||
Some(Msg::Ssh(SshMsg::CloseNewSshKeyPopup))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct SshUsername {
|
||||
component: Input,
|
||||
}
|
||||
|
||||
impl Default for SshUsername {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
component: Input::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.sides(BorderSides::BOTTOM | BorderSides::RIGHT | BorderSides::LEFT),
|
||||
)
|
||||
.input_type(InputType::Text)
|
||||
.placeholder("root", Style::default().fg(Color::Rgb(128, 128, 128)))
|
||||
.title("Username", Alignment::Center),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for SshUsername {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Left, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Left));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Right, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Right));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Home, ..
|
||||
}) => {
|
||||
self.perform(Cmd::GoTo(Position::Begin));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||
self.perform(Cmd::GoTo(Position::End));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Delete, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Cancel);
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Backspace,
|
||||
..
|
||||
}) => {
|
||||
self.perform(Cmd::Delete);
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char(ch),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => {
|
||||
self.perform(Cmd::Type(ch));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => Some(Msg::Ssh(SshMsg::SaveSshKey)),
|
||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
||||
Some(Msg::Ssh(SshMsg::SshUsernameBlur))
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||
Some(Msg::Ssh(SshMsg::CloseNewSshKeyPopup))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
910
src/ui/activities/setup/components/theme.rs
Normal file
910
src/ui/activities/setup/components/theme.rs
Normal file
@@ -0,0 +1,910 @@
|
||||
//! ## Theme
|
||||
//!
|
||||
//! theme tab components
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
use super::{Msg, ThemeMsg};
|
||||
use crate::ui::activities::setup::IdTheme;
|
||||
|
||||
use tui_realm_stdlib::{Input, Label};
|
||||
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
|
||||
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
|
||||
use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style, TextModifiers};
|
||||
use tuirealm::{
|
||||
AttrValue, Attribute, Component, Event, MockComponent, NoUserEvent, State, StateValue,
|
||||
};
|
||||
|
||||
// -- components
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct AuthTitle {
|
||||
component: Label,
|
||||
}
|
||||
|
||||
impl Default for AuthTitle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
component: Label::default()
|
||||
.modifiers(TextModifiers::BOLD)
|
||||
.text("Authentication styles"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for AuthTitle {
|
||||
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct MiscTitle {
|
||||
component: Label,
|
||||
}
|
||||
|
||||
impl Default for MiscTitle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
component: Label::default()
|
||||
.modifiers(TextModifiers::BOLD)
|
||||
.text("Misc styles"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for MiscTitle {
|
||||
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct TransferTitle {
|
||||
component: Label,
|
||||
}
|
||||
|
||||
impl Default for TransferTitle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
component: Label::default()
|
||||
.modifiers(TextModifiers::BOLD)
|
||||
.text("Transfer styles"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for TransferTitle {
|
||||
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct TransferTitle2 {
|
||||
component: Label,
|
||||
}
|
||||
|
||||
impl Default for TransferTitle2 {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
component: Label::default()
|
||||
.modifiers(TextModifiers::BOLD)
|
||||
.text("Transfer styles (2)"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for TransferTitle2 {
|
||||
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct AuthAddress {
|
||||
component: InputColor,
|
||||
}
|
||||
|
||||
impl AuthAddress {
|
||||
pub fn new(value: Color) -> Self {
|
||||
Self {
|
||||
component: InputColor::new(
|
||||
"Ip Address",
|
||||
IdTheme::AuthAddress,
|
||||
value,
|
||||
Msg::Theme(ThemeMsg::AuthAddressBlurDown),
|
||||
Msg::Theme(ThemeMsg::AuthAddressBlurUp),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for AuthAddress {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
self.component.on(ev)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct AuthBookmarks {
|
||||
component: InputColor,
|
||||
}
|
||||
|
||||
impl AuthBookmarks {
|
||||
pub fn new(value: Color) -> Self {
|
||||
Self {
|
||||
component: InputColor::new(
|
||||
"Bookmarks",
|
||||
IdTheme::AuthBookmarks,
|
||||
value,
|
||||
Msg::Theme(ThemeMsg::AuthBookmarksBlurDown),
|
||||
Msg::Theme(ThemeMsg::AuthBookmarksBlurUp),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for AuthBookmarks {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
self.component.on(ev)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct AuthPassword {
|
||||
component: InputColor,
|
||||
}
|
||||
|
||||
impl AuthPassword {
|
||||
pub fn new(value: Color) -> Self {
|
||||
Self {
|
||||
component: InputColor::new(
|
||||
"Password",
|
||||
IdTheme::AuthPassword,
|
||||
value,
|
||||
Msg::Theme(ThemeMsg::AuthPasswordBlurDown),
|
||||
Msg::Theme(ThemeMsg::AuthPasswordBlurUp),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for AuthPassword {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
self.component.on(ev)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct AuthPort {
|
||||
component: InputColor,
|
||||
}
|
||||
|
||||
impl AuthPort {
|
||||
pub fn new(value: Color) -> Self {
|
||||
Self {
|
||||
component: InputColor::new(
|
||||
"Port",
|
||||
IdTheme::AuthPort,
|
||||
value,
|
||||
Msg::Theme(ThemeMsg::AuthPortBlurDown),
|
||||
Msg::Theme(ThemeMsg::AuthPortBlurUp),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for AuthPort {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
self.component.on(ev)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct AuthProtocol {
|
||||
component: InputColor,
|
||||
}
|
||||
|
||||
impl AuthProtocol {
|
||||
pub fn new(value: Color) -> Self {
|
||||
Self {
|
||||
component: InputColor::new(
|
||||
"Protocol",
|
||||
IdTheme::AuthProtocol,
|
||||
value,
|
||||
Msg::Theme(ThemeMsg::AuthProtocolBlurDown),
|
||||
Msg::Theme(ThemeMsg::AuthProtocolBlurUp),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for AuthProtocol {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
self.component.on(ev)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct AuthRecentHosts {
|
||||
component: InputColor,
|
||||
}
|
||||
|
||||
impl AuthRecentHosts {
|
||||
pub fn new(value: Color) -> Self {
|
||||
Self {
|
||||
component: InputColor::new(
|
||||
"Recent connections",
|
||||
IdTheme::AuthRecentHosts,
|
||||
value,
|
||||
Msg::Theme(ThemeMsg::AuthRecentHostsBlurDown),
|
||||
Msg::Theme(ThemeMsg::AuthRecentHostsBlurUp),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for AuthRecentHosts {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
self.component.on(ev)
|
||||
}
|
||||
}
|
||||
#[derive(MockComponent)]
|
||||
pub struct AuthUsername {
|
||||
component: InputColor,
|
||||
}
|
||||
|
||||
impl AuthUsername {
|
||||
pub fn new(value: Color) -> Self {
|
||||
Self {
|
||||
component: InputColor::new(
|
||||
"Username",
|
||||
IdTheme::AuthUsername,
|
||||
value,
|
||||
Msg::Theme(ThemeMsg::AuthUsernameBlurDown),
|
||||
Msg::Theme(ThemeMsg::AuthUsernameBlurUp),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for AuthUsername {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
self.component.on(ev)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct ExplorerLocalBg {
|
||||
component: InputColor,
|
||||
}
|
||||
|
||||
impl ExplorerLocalBg {
|
||||
pub fn new(value: Color) -> Self {
|
||||
Self {
|
||||
component: InputColor::new(
|
||||
"Local explorer background",
|
||||
IdTheme::ExplorerLocalBg,
|
||||
value,
|
||||
Msg::Theme(ThemeMsg::ExplorerLocalBgBlurDown),
|
||||
Msg::Theme(ThemeMsg::ExplorerLocalBgBlurUp),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for ExplorerLocalBg {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
self.component.on(ev)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct ExplorerLocalFg {
|
||||
component: InputColor,
|
||||
}
|
||||
|
||||
impl ExplorerLocalFg {
|
||||
pub fn new(value: Color) -> Self {
|
||||
Self {
|
||||
component: InputColor::new(
|
||||
"Local explorer foreground",
|
||||
IdTheme::ExplorerLocalFg,
|
||||
value,
|
||||
Msg::Theme(ThemeMsg::ExplorerLocalFgBlurDown),
|
||||
Msg::Theme(ThemeMsg::ExplorerLocalFgBlurUp),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for ExplorerLocalFg {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
self.component.on(ev)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct ExplorerLocalHg {
|
||||
component: InputColor,
|
||||
}
|
||||
|
||||
impl ExplorerLocalHg {
|
||||
pub fn new(value: Color) -> Self {
|
||||
Self {
|
||||
component: InputColor::new(
|
||||
"Local explorer highlighted",
|
||||
IdTheme::ExplorerLocalHg,
|
||||
value,
|
||||
Msg::Theme(ThemeMsg::ExplorerLocalHgBlurDown),
|
||||
Msg::Theme(ThemeMsg::ExplorerLocalHgBlurUp),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for ExplorerLocalHg {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
self.component.on(ev)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct ExplorerRemoteBg {
|
||||
component: InputColor,
|
||||
}
|
||||
|
||||
impl ExplorerRemoteBg {
|
||||
pub fn new(value: Color) -> Self {
|
||||
Self {
|
||||
component: InputColor::new(
|
||||
"Remote explorer background",
|
||||
IdTheme::ExplorerRemoteBg,
|
||||
value,
|
||||
Msg::Theme(ThemeMsg::ExplorerRemoteBgBlurDown),
|
||||
Msg::Theme(ThemeMsg::ExplorerRemoteBgBlurUp),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for ExplorerRemoteBg {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
self.component.on(ev)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct ExplorerRemoteFg {
|
||||
component: InputColor,
|
||||
}
|
||||
|
||||
impl ExplorerRemoteFg {
|
||||
pub fn new(value: Color) -> Self {
|
||||
Self {
|
||||
component: InputColor::new(
|
||||
"Remote explorer foreground",
|
||||
IdTheme::ExplorerRemoteFg,
|
||||
value,
|
||||
Msg::Theme(ThemeMsg::ExplorerRemoteFgBlurDown),
|
||||
Msg::Theme(ThemeMsg::ExplorerRemoteFgBlurUp),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for ExplorerRemoteFg {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
self.component.on(ev)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct ExplorerRemoteHg {
|
||||
component: InputColor,
|
||||
}
|
||||
|
||||
impl ExplorerRemoteHg {
|
||||
pub fn new(value: Color) -> Self {
|
||||
Self {
|
||||
component: InputColor::new(
|
||||
"Remote explorer highlighted",
|
||||
IdTheme::ExplorerRemoteHg,
|
||||
value,
|
||||
Msg::Theme(ThemeMsg::ExplorerRemoteHgBlurDown),
|
||||
Msg::Theme(ThemeMsg::ExplorerRemoteHgBlurUp),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for ExplorerRemoteHg {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
self.component.on(ev)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct LogBg {
|
||||
component: InputColor,
|
||||
}
|
||||
|
||||
impl LogBg {
|
||||
pub fn new(value: Color) -> Self {
|
||||
Self {
|
||||
component: InputColor::new(
|
||||
"Log window background",
|
||||
IdTheme::LogBg,
|
||||
value,
|
||||
Msg::Theme(ThemeMsg::LogBgBlurDown),
|
||||
Msg::Theme(ThemeMsg::LogBgBlurUp),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for LogBg {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
self.component.on(ev)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct LogWindow {
|
||||
component: InputColor,
|
||||
}
|
||||
|
||||
impl LogWindow {
|
||||
pub fn new(value: Color) -> Self {
|
||||
Self {
|
||||
component: InputColor::new(
|
||||
"Log window",
|
||||
IdTheme::LogWindow,
|
||||
value,
|
||||
Msg::Theme(ThemeMsg::LogWindowBlurDown),
|
||||
Msg::Theme(ThemeMsg::LogWindowBlurUp),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for LogWindow {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
self.component.on(ev)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct MiscError {
|
||||
component: InputColor,
|
||||
}
|
||||
|
||||
impl MiscError {
|
||||
pub fn new(value: Color) -> Self {
|
||||
Self {
|
||||
component: InputColor::new(
|
||||
"Error",
|
||||
IdTheme::MiscError,
|
||||
value,
|
||||
Msg::Theme(ThemeMsg::MiscErrorBlurDown),
|
||||
Msg::Theme(ThemeMsg::MiscErrorBlurUp),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for MiscError {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
self.component.on(ev)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct MiscInfo {
|
||||
component: InputColor,
|
||||
}
|
||||
|
||||
impl MiscInfo {
|
||||
pub fn new(value: Color) -> Self {
|
||||
Self {
|
||||
component: InputColor::new(
|
||||
"Info",
|
||||
IdTheme::MiscInfo,
|
||||
value,
|
||||
Msg::Theme(ThemeMsg::MiscInfoBlurDown),
|
||||
Msg::Theme(ThemeMsg::MiscInfoBlurUp),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for MiscInfo {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
self.component.on(ev)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct MiscInput {
|
||||
component: InputColor,
|
||||
}
|
||||
|
||||
impl MiscInput {
|
||||
pub fn new(value: Color) -> Self {
|
||||
Self {
|
||||
component: InputColor::new(
|
||||
"Input",
|
||||
IdTheme::MiscInput,
|
||||
value,
|
||||
Msg::Theme(ThemeMsg::MiscInputBlurDown),
|
||||
Msg::Theme(ThemeMsg::MiscInputBlurUp),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for MiscInput {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
self.component.on(ev)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct MiscKeys {
|
||||
component: InputColor,
|
||||
}
|
||||
|
||||
impl MiscKeys {
|
||||
pub fn new(value: Color) -> Self {
|
||||
Self {
|
||||
component: InputColor::new(
|
||||
"Key strokes",
|
||||
IdTheme::MiscKeys,
|
||||
value,
|
||||
Msg::Theme(ThemeMsg::MiscKeysBlurDown),
|
||||
Msg::Theme(ThemeMsg::MiscKeysBlurUp),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for MiscKeys {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
self.component.on(ev)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct MiscQuit {
|
||||
component: InputColor,
|
||||
}
|
||||
|
||||
impl MiscQuit {
|
||||
pub fn new(value: Color) -> Self {
|
||||
Self {
|
||||
component: InputColor::new(
|
||||
"Quit dialogs",
|
||||
IdTheme::MiscQuit,
|
||||
value,
|
||||
Msg::Theme(ThemeMsg::MiscQuitBlurDown),
|
||||
Msg::Theme(ThemeMsg::MiscQuitBlurUp),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for MiscQuit {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
self.component.on(ev)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct MiscSave {
|
||||
component: InputColor,
|
||||
}
|
||||
|
||||
impl MiscSave {
|
||||
pub fn new(value: Color) -> Self {
|
||||
Self {
|
||||
component: InputColor::new(
|
||||
"Save confirmations",
|
||||
IdTheme::MiscSave,
|
||||
value,
|
||||
Msg::Theme(ThemeMsg::MiscSaveBlurDown),
|
||||
Msg::Theme(ThemeMsg::MiscSaveBlurUp),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for MiscSave {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
self.component.on(ev)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct MiscWarn {
|
||||
component: InputColor,
|
||||
}
|
||||
|
||||
impl MiscWarn {
|
||||
pub fn new(value: Color) -> Self {
|
||||
Self {
|
||||
component: InputColor::new(
|
||||
"Warnings",
|
||||
IdTheme::MiscWarn,
|
||||
value,
|
||||
Msg::Theme(ThemeMsg::MiscWarnBlurDown),
|
||||
Msg::Theme(ThemeMsg::MiscWarnBlurUp),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for MiscWarn {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
self.component.on(ev)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct ProgBarFull {
|
||||
component: InputColor,
|
||||
}
|
||||
|
||||
impl ProgBarFull {
|
||||
pub fn new(value: Color) -> Self {
|
||||
Self {
|
||||
component: InputColor::new(
|
||||
"'Full transfer' Progress bar",
|
||||
IdTheme::ProgBarFull,
|
||||
value,
|
||||
Msg::Theme(ThemeMsg::ProgBarFullBlurDown),
|
||||
Msg::Theme(ThemeMsg::ProgBarFullBlurUp),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for ProgBarFull {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
self.component.on(ev)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct ProgBarPartial {
|
||||
component: InputColor,
|
||||
}
|
||||
|
||||
impl ProgBarPartial {
|
||||
pub fn new(value: Color) -> Self {
|
||||
Self {
|
||||
component: InputColor::new(
|
||||
"'Partial transfer' Progress bar",
|
||||
IdTheme::ProgBarPartial,
|
||||
value,
|
||||
Msg::Theme(ThemeMsg::ProgBarPartialBlurDown),
|
||||
Msg::Theme(ThemeMsg::ProgBarPartialBlurUp),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for ProgBarPartial {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
self.component.on(ev)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct StatusHidden {
|
||||
component: InputColor,
|
||||
}
|
||||
|
||||
impl StatusHidden {
|
||||
pub fn new(value: Color) -> Self {
|
||||
Self {
|
||||
component: InputColor::new(
|
||||
"Hidden files",
|
||||
IdTheme::StatusHidden,
|
||||
value,
|
||||
Msg::Theme(ThemeMsg::StatusHiddenBlurDown),
|
||||
Msg::Theme(ThemeMsg::StatusHiddenBlurUp),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for StatusHidden {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
self.component.on(ev)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct StatusSorting {
|
||||
component: InputColor,
|
||||
}
|
||||
|
||||
impl StatusSorting {
|
||||
pub fn new(value: Color) -> Self {
|
||||
Self {
|
||||
component: InputColor::new(
|
||||
"File sorting",
|
||||
IdTheme::StatusSorting,
|
||||
value,
|
||||
Msg::Theme(ThemeMsg::StatusSortingBlurDown),
|
||||
Msg::Theme(ThemeMsg::StatusSortingBlurUp),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for StatusSorting {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
self.component.on(ev)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct StatusSync {
|
||||
component: InputColor,
|
||||
}
|
||||
|
||||
impl StatusSync {
|
||||
pub fn new(value: Color) -> Self {
|
||||
Self {
|
||||
component: InputColor::new(
|
||||
"Synchronized browsing",
|
||||
IdTheme::StatusSync,
|
||||
value,
|
||||
Msg::Theme(ThemeMsg::StatusSyncBlurDown),
|
||||
Msg::Theme(ThemeMsg::StatusSyncBlurUp),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for StatusSync {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
self.component.on(ev)
|
||||
}
|
||||
}
|
||||
|
||||
// -- input color
|
||||
|
||||
#[derive(MockComponent)]
|
||||
struct InputColor {
|
||||
component: Input,
|
||||
id: IdTheme,
|
||||
on_key_down: Msg,
|
||||
on_key_up: Msg,
|
||||
}
|
||||
|
||||
impl InputColor {
|
||||
pub fn new(name: &str, id: IdTheme, color: Color, on_key_down: Msg, on_key_up: Msg) -> Self {
|
||||
let value = crate::utils::fmt::fmt_color(&color);
|
||||
Self {
|
||||
component: Input::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.input_type(InputType::Color)
|
||||
.placeholder("#aa33ee", Style::default().fg(Color::Rgb(128, 128, 128)))
|
||||
.title(name, Alignment::Left)
|
||||
.value(value),
|
||||
id,
|
||||
on_key_down,
|
||||
on_key_up,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_color(&mut self, result: CmdResult) -> Option<Msg> {
|
||||
if let CmdResult::Changed(State::One(StateValue::String(color))) = result {
|
||||
let color = tuirealm::utils::parser::parse_color(&color).unwrap();
|
||||
self.attr(Attribute::Foreground, AttrValue::Color(color));
|
||||
self.attr(
|
||||
Attribute::Borders,
|
||||
AttrValue::Borders(
|
||||
Borders::default()
|
||||
.modifiers(BorderType::Rounded)
|
||||
.color(color),
|
||||
),
|
||||
);
|
||||
Some(Msg::Theme(ThemeMsg::ColorChanged(self.id.clone(), color)))
|
||||
} else {
|
||||
self.attr(Attribute::Foreground, AttrValue::Color(Color::Red));
|
||||
self.attr(
|
||||
Attribute::Borders,
|
||||
AttrValue::Borders(
|
||||
Borders::default()
|
||||
.modifiers(BorderType::Rounded)
|
||||
.color(Color::Red),
|
||||
),
|
||||
);
|
||||
Some(Msg::None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for InputColor {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Left, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Left));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Right, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Right));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Home, ..
|
||||
}) => {
|
||||
self.perform(Cmd::GoTo(Position::Begin));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||
self.perform(Cmd::GoTo(Position::End));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Delete, ..
|
||||
}) => {
|
||||
let result = self.perform(Cmd::Cancel);
|
||||
self.update_color(result)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Backspace,
|
||||
..
|
||||
}) => {
|
||||
let result = self.perform(Cmd::Delete);
|
||||
self.update_color(result)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char(ch),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => {
|
||||
let result = self.perform(Cmd::Type(ch));
|
||||
self.update_color(result)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Down, ..
|
||||
}) => Some(self.on_key_down.clone()),
|
||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(self.on_key_up.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
*/
|
||||
// Submodules
|
||||
mod actions;
|
||||
mod components;
|
||||
mod config;
|
||||
mod update;
|
||||
mod view;
|
||||
@@ -38,71 +39,209 @@ use crate::config::themes::Theme;
|
||||
use crate::system::config_client::ConfigClient;
|
||||
use crate::system::theme_provider::ThemeProvider;
|
||||
// Ext
|
||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||
use tuirealm::{Update, View};
|
||||
use std::time::Duration;
|
||||
use tuirealm::listener::EventListenerCfg;
|
||||
use tuirealm::props::Color;
|
||||
use tuirealm::{application::PollStrategy, Application, NoUserEvent, Update};
|
||||
|
||||
// -- components
|
||||
// -- common
|
||||
const COMPONENT_TEXT_HELP: &str = "TEXT_HELP";
|
||||
const COMPONENT_TEXT_FOOTER: &str = "TEXT_FOOTER";
|
||||
const COMPONENT_TEXT_ERROR: &str = "TEXT_ERROR";
|
||||
const COMPONENT_RADIO_QUIT: &str = "RADIO_QUIT";
|
||||
const COMPONENT_RADIO_SAVE: &str = "RADIO_SAVE";
|
||||
const COMPONENT_RADIO_TAB: &str = "RADIO_TAB";
|
||||
// -- config
|
||||
const COMPONENT_INPUT_TEXT_EDITOR: &str = "INPUT_TEXT_EDITOR";
|
||||
const COMPONENT_RADIO_DEFAULT_PROTOCOL: &str = "RADIO_DEFAULT_PROTOCOL";
|
||||
const COMPONENT_RADIO_HIDDEN_FILES: &str = "RADIO_HIDDEN_FILES";
|
||||
const COMPONENT_RADIO_UPDATES: &str = "RADIO_CHECK_UPDATES";
|
||||
const COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE: &str = "RADIO_PROMPT_ON_FILE_REPLACE";
|
||||
const COMPONENT_RADIO_GROUP_DIRS: &str = "RADIO_GROUP_DIRS";
|
||||
const COMPONENT_INPUT_LOCAL_FILE_FMT: &str = "INPUT_LOCAL_FILE_FMT";
|
||||
const COMPONENT_INPUT_REMOTE_FILE_FMT: &str = "INPUT_REMOTE_FILE_FMT";
|
||||
const COMPONENT_RADIO_NOTIFICATIONS_ENABLED: &str = "RADIO_NOTIFICATIONS_ENABLED";
|
||||
const COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD: &str = "INPUT_NOTIFICATIONS_THRESHOLD";
|
||||
// -- ssh keys
|
||||
const COMPONENT_LIST_SSH_KEYS: &str = "LIST_SSH_KEYS";
|
||||
const COMPONENT_INPUT_SSH_HOST: &str = "INPUT_SSH_HOST";
|
||||
const COMPONENT_INPUT_SSH_USERNAME: &str = "INPUT_SSH_USERNAME";
|
||||
const COMPONENT_RADIO_DEL_SSH_KEY: &str = "RADIO_DEL_SSH_KEY";
|
||||
// -- theme
|
||||
const COMPONENT_COLOR_AUTH_TITLE: &str = "COMPONENT_COLOR_AUTH_TITLE";
|
||||
const COMPONENT_COLOR_MISC_TITLE: &str = "COMPONENT_COLOR_MISC_TITLE";
|
||||
const COMPONENT_COLOR_TRANSFER_TITLE: &str = "COMPONENT_COLOR_TRANSFER_TITLE";
|
||||
const COMPONENT_COLOR_TRANSFER_TITLE_2: &str = "COMPONENT_COLOR_TRANSFER_TITLE_2";
|
||||
const COMPONENT_COLOR_AUTH_ADDR: &str = "COMPONENT_COLOR_AUTH_ADDR";
|
||||
const COMPONENT_COLOR_AUTH_BOOKMARKS: &str = "COMPONENT_COLOR_AUTH_BOOKMARKS";
|
||||
const COMPONENT_COLOR_AUTH_PASSWORD: &str = "COMPONENT_COLOR_AUTH_PASSWORD";
|
||||
const COMPONENT_COLOR_AUTH_PORT: &str = "COMPONENT_COLOR_AUTH_PORT";
|
||||
const COMPONENT_COLOR_AUTH_PROTOCOL: &str = "COMPONENT_COLOR_AUTH_PROTOCOL";
|
||||
const COMPONENT_COLOR_AUTH_RECENTS: &str = "COMPONENT_COLOR_AUTH_RECENTS";
|
||||
const COMPONENT_COLOR_AUTH_USERNAME: &str = "COMPONENT_COLOR_AUTH_USERNAME";
|
||||
const COMPONENT_COLOR_MISC_ERROR: &str = "COMPONENT_COLOR_MISC_ERROR";
|
||||
const COMPONENT_COLOR_MISC_INFO: &str = "COMPONENT_COLOR_MISC_INFO";
|
||||
const COMPONENT_COLOR_MISC_INPUT: &str = "COMPONENT_COLOR_MISC_INPUT";
|
||||
const COMPONENT_COLOR_MISC_KEYS: &str = "COMPONENT_COLOR_MISC_KEYS";
|
||||
const COMPONENT_COLOR_MISC_QUIT: &str = "COMPONENT_COLOR_MISC_QUIT";
|
||||
const COMPONENT_COLOR_MISC_SAVE: &str = "COMPONENT_COLOR_MISC_SAVE";
|
||||
const COMPONENT_COLOR_MISC_WARN: &str = "COMPONENT_COLOR_MISC_WARN";
|
||||
const COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG: &str =
|
||||
"COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG";
|
||||
const COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG: &str =
|
||||
"COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG";
|
||||
const COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG: &str =
|
||||
"COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG";
|
||||
const COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG: &str =
|
||||
"COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG";
|
||||
const COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG: &str =
|
||||
"COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG";
|
||||
const COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG: &str =
|
||||
"COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG";
|
||||
const COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL: &str = "COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL";
|
||||
const COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL: &str = "COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL";
|
||||
const COMPONENT_COLOR_TRANSFER_LOG_BG: &str = "COMPONENT_COLOR_TRANSFER_LOG_BG";
|
||||
const COMPONENT_COLOR_TRANSFER_LOG_WIN: &str = "COMPONENT_COLOR_TRANSFER_LOG_WIN";
|
||||
const COMPONENT_COLOR_TRANSFER_STATUS_SORTING: &str = "COMPONENT_COLOR_TRANSFER_STATUS_SORTING";
|
||||
const COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN: &str = "COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN";
|
||||
const COMPONENT_COLOR_TRANSFER_STATUS_SYNC: &str = "COMPONENT_COLOR_TRANSFER_STATUS_SYNC";
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
||||
enum Id {
|
||||
Common(IdCommon),
|
||||
Config(IdConfig),
|
||||
Ssh(IdSsh),
|
||||
Theme(IdTheme),
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
||||
enum IdCommon {
|
||||
ErrorPopup,
|
||||
Footer,
|
||||
GlobalListener,
|
||||
Header,
|
||||
Keybindings,
|
||||
QuitPopup,
|
||||
SavePopup,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
||||
enum IdConfig {
|
||||
CheckUpdates,
|
||||
DefaultProtocol,
|
||||
GroupDirs,
|
||||
HiddenFiles,
|
||||
LocalFileFmt,
|
||||
NotificationsEnabled,
|
||||
NotificationsThreshold,
|
||||
PromptOnFileReplace,
|
||||
RemoteFileFmt,
|
||||
TextEditor,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
||||
enum IdSsh {
|
||||
DelSshKeyPopup,
|
||||
SshHost,
|
||||
SshKeys,
|
||||
SshUsername,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
||||
pub enum IdTheme {
|
||||
AuthAddress,
|
||||
AuthBookmarks,
|
||||
AuthPassword,
|
||||
AuthPort,
|
||||
AuthProtocol,
|
||||
AuthRecentHosts,
|
||||
AuthTitle,
|
||||
AuthUsername,
|
||||
ExplorerLocalBg,
|
||||
ExplorerLocalFg,
|
||||
ExplorerLocalHg,
|
||||
ExplorerRemoteBg,
|
||||
ExplorerRemoteFg,
|
||||
ExplorerRemoteHg,
|
||||
LogBg,
|
||||
LogWindow,
|
||||
MiscError,
|
||||
MiscInfo,
|
||||
MiscInput,
|
||||
MiscKeys,
|
||||
MiscQuit,
|
||||
MiscSave,
|
||||
MiscTitle,
|
||||
MiscWarn,
|
||||
ProgBarFull,
|
||||
ProgBarPartial,
|
||||
StatusHidden,
|
||||
StatusSorting,
|
||||
StatusSync,
|
||||
TransferTitle,
|
||||
TransferTitle2,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Msg {
|
||||
Common(CommonMsg),
|
||||
Config(ConfigMsg),
|
||||
Ssh(SshMsg),
|
||||
Theme(ThemeMsg),
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum CommonMsg {
|
||||
ChangeLayout,
|
||||
CloseErrorPopup,
|
||||
CloseKeybindingsPopup,
|
||||
CloseQuitPopup,
|
||||
CloseSavePopup,
|
||||
Quit,
|
||||
RevertChanges,
|
||||
SaveAndQuit,
|
||||
SaveConfig,
|
||||
ShowKeybindings,
|
||||
ShowQuitPopup,
|
||||
ShowSavePopup,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum ConfigMsg {
|
||||
CheckUpdatesBlurDown,
|
||||
CheckUpdatesBlurUp,
|
||||
ConfigChanged,
|
||||
DefaultProtocolBlurDown,
|
||||
DefaultProtocolBlurUp,
|
||||
GroupDirsBlurDown,
|
||||
GroupDirsBlurUp,
|
||||
HiddenFilesBlurDown,
|
||||
HiddenFilesBlurUp,
|
||||
LocalFileFmtBlurDown,
|
||||
LocalFileFmtBlurUp,
|
||||
NotificationsEnabledBlurDown,
|
||||
NotificationsEnabledBlurUp,
|
||||
NotificationsThresholdBlurDown,
|
||||
NotificationsThresholdBlurUp,
|
||||
PromptOnFileReplaceBlurDown,
|
||||
PromptOnFileReplaceBlurUp,
|
||||
RemoteFileFmtBlurDown,
|
||||
RemoteFileFmtBlurUp,
|
||||
TextEditorBlurDown,
|
||||
TextEditorBlurUp,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum SshMsg {
|
||||
CloseDelSshKeyPopup,
|
||||
CloseNewSshKeyPopup,
|
||||
DeleteSshKey,
|
||||
EditSshKey(usize),
|
||||
SaveSshKey,
|
||||
ShowDelSshKeyPopup,
|
||||
ShowNewSshKeyPopup,
|
||||
SshHostBlur,
|
||||
SshUsernameBlur,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum ThemeMsg {
|
||||
AuthAddressBlurDown,
|
||||
AuthAddressBlurUp,
|
||||
AuthBookmarksBlurDown,
|
||||
AuthBookmarksBlurUp,
|
||||
AuthPasswordBlurDown,
|
||||
AuthPasswordBlurUp,
|
||||
AuthPortBlurDown,
|
||||
AuthPortBlurUp,
|
||||
AuthProtocolBlurDown,
|
||||
AuthProtocolBlurUp,
|
||||
AuthRecentHostsBlurDown,
|
||||
AuthRecentHostsBlurUp,
|
||||
AuthUsernameBlurDown,
|
||||
AuthUsernameBlurUp,
|
||||
ColorChanged(IdTheme, Color),
|
||||
ExplorerLocalBgBlurDown,
|
||||
ExplorerLocalBgBlurUp,
|
||||
ExplorerLocalFgBlurDown,
|
||||
ExplorerLocalFgBlurUp,
|
||||
ExplorerLocalHgBlurDown,
|
||||
ExplorerLocalHgBlurUp,
|
||||
ExplorerRemoteBgBlurDown,
|
||||
ExplorerRemoteBgBlurUp,
|
||||
ExplorerRemoteFgBlurDown,
|
||||
ExplorerRemoteFgBlurUp,
|
||||
ExplorerRemoteHgBlurDown,
|
||||
ExplorerRemoteHgBlurUp,
|
||||
LogBgBlurDown,
|
||||
LogBgBlurUp,
|
||||
LogWindowBlurDown,
|
||||
LogWindowBlurUp,
|
||||
MiscErrorBlurDown,
|
||||
MiscErrorBlurUp,
|
||||
MiscInfoBlurDown,
|
||||
MiscInfoBlurUp,
|
||||
MiscInputBlurDown,
|
||||
MiscInputBlurUp,
|
||||
MiscKeysBlurDown,
|
||||
MiscKeysBlurUp,
|
||||
MiscQuitBlurDown,
|
||||
MiscQuitBlurUp,
|
||||
MiscSaveBlurDown,
|
||||
MiscSaveBlurUp,
|
||||
MiscWarnBlurDown,
|
||||
MiscWarnBlurUp,
|
||||
ProgBarFullBlurDown,
|
||||
ProgBarFullBlurUp,
|
||||
ProgBarPartialBlurDown,
|
||||
ProgBarPartialBlurUp,
|
||||
StatusHiddenBlurDown,
|
||||
StatusHiddenBlurUp,
|
||||
StatusSortingBlurDown,
|
||||
StatusSortingBlurUp,
|
||||
StatusSyncBlurDown,
|
||||
StatusSyncBlurUp,
|
||||
}
|
||||
|
||||
// -- store
|
||||
const STORE_CONFIG_CHANGED: &str = "SETUP_CONFIG_CHANGED";
|
||||
@@ -110,8 +249,8 @@ const STORE_CONFIG_CHANGED: &str = "SETUP_CONFIG_CHANGED";
|
||||
/// ### ViewLayout
|
||||
///
|
||||
/// Current view layout
|
||||
#[derive(std::cmp::PartialEq)]
|
||||
enum ViewLayout {
|
||||
#[derive(PartialEq)]
|
||||
pub enum ViewLayout {
|
||||
SetupForm,
|
||||
SshKeys,
|
||||
Theme,
|
||||
@@ -121,26 +260,28 @@ enum ViewLayout {
|
||||
///
|
||||
/// Setup activity states holder
|
||||
pub struct SetupActivity {
|
||||
app: Application<Id, Msg, NoUserEvent>,
|
||||
exit_reason: Option<ExitReason>,
|
||||
context: Option<Context>, // Context holder
|
||||
view: View, // View
|
||||
layout: ViewLayout, // View layout
|
||||
redraw: bool,
|
||||
}
|
||||
|
||||
impl Default for SetupActivity {
|
||||
fn default() -> Self {
|
||||
SetupActivity {
|
||||
impl SetupActivity {
|
||||
pub fn new(ticks: Duration) -> Self {
|
||||
Self {
|
||||
app: Application::init(
|
||||
EventListenerCfg::default()
|
||||
.default_input_listener(ticks)
|
||||
.poll_timeout(ticks),
|
||||
),
|
||||
exit_reason: None,
|
||||
context: None,
|
||||
view: View::init(),
|
||||
layout: ViewLayout::SetupForm,
|
||||
redraw: true, // Draw at first `on_draw`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SetupActivity {
|
||||
/// ### context
|
||||
///
|
||||
/// Returns a reference to context
|
||||
@@ -205,11 +346,13 @@ impl Activity for SetupActivity {
|
||||
// Set context
|
||||
self.context = Some(context);
|
||||
// Clear terminal
|
||||
self.context.as_mut().unwrap().clear_screen();
|
||||
if let Err(err) = self.context.as_mut().unwrap().terminal().clear_screen() {
|
||||
error!("Failed to clear screen: {}", err);
|
||||
}
|
||||
// Set config changed to false
|
||||
self.set_config_changed(false);
|
||||
// Put raw mode on enabled
|
||||
if let Err(err) = enable_raw_mode() {
|
||||
if let Err(err) = self.context_mut().terminal().enable_raw_mode() {
|
||||
error!("Failed to enter raw mode: {}", err);
|
||||
}
|
||||
// Init view
|
||||
@@ -229,20 +372,25 @@ impl Activity for SetupActivity {
|
||||
if self.context.is_none() {
|
||||
return;
|
||||
}
|
||||
// Read one event
|
||||
if let Ok(Some(event)) = self.context().input_hnd().read_event() {
|
||||
// Set redraw to true
|
||||
self.redraw = true;
|
||||
// Handle event
|
||||
let msg = self.view.on(event);
|
||||
self.update(msg);
|
||||
match self.app.tick(PollStrategy::UpTo(3)) {
|
||||
Ok(messages) => {
|
||||
if !messages.is_empty() {
|
||||
self.redraw = true;
|
||||
}
|
||||
for msg in messages.into_iter() {
|
||||
let mut msg = Some(msg);
|
||||
while msg.is_some() {
|
||||
msg = self.update(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
self.mount_error(format!("Application error: {}", err));
|
||||
}
|
||||
}
|
||||
// Redraw if necessary
|
||||
// View
|
||||
if self.redraw {
|
||||
// View
|
||||
self.view();
|
||||
// Redraw back to false
|
||||
self.redraw = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,17 +410,12 @@ impl Activity for SetupActivity {
|
||||
/// This function finally releases the context
|
||||
fn on_destroy(&mut self) -> Option<Context> {
|
||||
// Disable raw mode
|
||||
if let Err(err) = disable_raw_mode() {
|
||||
if let Err(err) = self.context_mut().terminal().disable_raw_mode() {
|
||||
error!("Failed to disable raw mode: {}", err);
|
||||
}
|
||||
self.context.as_ref()?;
|
||||
// Clear terminal and return
|
||||
match self.context.take() {
|
||||
Some(mut ctx) => {
|
||||
ctx.clear_screen();
|
||||
Some(ctx)
|
||||
}
|
||||
None => None,
|
||||
if let Err(err) = self.context_mut().terminal().clear_screen() {
|
||||
error!("Failed to clear screen: {}", err);
|
||||
}
|
||||
self.context.take()
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -31,18 +31,15 @@ pub mod ssh_keys;
|
||||
pub mod theme;
|
||||
|
||||
use super::*;
|
||||
use crate::utils::ui::draw_area_in;
|
||||
pub use setup::*;
|
||||
pub use ssh_keys::*;
|
||||
pub use theme::*;
|
||||
// Ext
|
||||
use tui_realm_stdlib::{
|
||||
Input, InputPropsBuilder, List, ListPropsBuilder, Paragraph, ParagraphPropsBuilder, Radio,
|
||||
RadioPropsBuilder, Span, SpanPropsBuilder,
|
||||
};
|
||||
use tuirealm::props::{Alignment, InputType, PropsBuilder, TableBuilder, TextSpan};
|
||||
use tuirealm::tui::{
|
||||
style::Color,
|
||||
widgets::{BorderType, Borders},
|
||||
|
||||
use tuirealm::tui::widgets::Clear;
|
||||
use tuirealm::{
|
||||
event::{Key, KeyEvent, KeyModifiers},
|
||||
Frame, Sub, SubClause, SubEventClause,
|
||||
};
|
||||
|
||||
impl SetupActivity {
|
||||
@@ -61,6 +58,7 @@ impl SetupActivity {
|
||||
///
|
||||
/// View gui
|
||||
pub(super) fn view(&mut self) {
|
||||
self.redraw = false;
|
||||
match self.layout {
|
||||
ViewLayout::SetupForm => self.view_setup(),
|
||||
ViewLayout::SshKeys => self.view_ssh_keys(),
|
||||
@@ -73,238 +71,229 @@ impl SetupActivity {
|
||||
/// ### mount_error
|
||||
///
|
||||
/// Mount error box
|
||||
pub(super) fn mount_error(&mut self, text: &str) {
|
||||
self.mount_text_dialog(super::COMPONENT_TEXT_ERROR, text, Color::Red);
|
||||
pub(super) fn mount_error<S: AsRef<str>>(&mut self, text: S) {
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Common(IdCommon::ErrorPopup),
|
||||
Box::new(components::ErrorPopup::new(text)),
|
||||
vec![],
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self.app.active(&Id::Common(IdCommon::ErrorPopup)).is_ok());
|
||||
}
|
||||
|
||||
/// ### umount_error
|
||||
///
|
||||
/// Umount error message
|
||||
pub(super) fn umount_error(&mut self) {
|
||||
self.view.umount(super::COMPONENT_TEXT_ERROR);
|
||||
let _ = self.app.umount(&Id::Common(IdCommon::ErrorPopup));
|
||||
}
|
||||
|
||||
/// ### mount_quit
|
||||
///
|
||||
/// Mount quit popup
|
||||
pub(super) fn mount_quit(&mut self) {
|
||||
self.mount_radio_dialog(
|
||||
super::COMPONENT_RADIO_QUIT,
|
||||
"There are unsaved changes! Save changes before leaving?",
|
||||
&["Save", "Don't save", "Cancel"],
|
||||
0,
|
||||
Color::LightRed,
|
||||
);
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Common(IdCommon::QuitPopup),
|
||||
Box::new(components::QuitPopup::default()),
|
||||
vec![],
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self.app.active(&Id::Common(IdCommon::QuitPopup)).is_ok());
|
||||
}
|
||||
|
||||
/// ### umount_quit
|
||||
///
|
||||
/// Umount quit
|
||||
pub(super) fn umount_quit(&mut self) {
|
||||
self.view.umount(super::COMPONENT_RADIO_QUIT);
|
||||
let _ = self.app.umount(&Id::Common(IdCommon::QuitPopup));
|
||||
}
|
||||
|
||||
/// ### mount_save_popup
|
||||
///
|
||||
/// Mount save popup
|
||||
pub(super) fn mount_save_popup(&mut self) {
|
||||
self.mount_radio_dialog(
|
||||
super::COMPONENT_RADIO_SAVE,
|
||||
"Save changes?",
|
||||
&["Yes", "No"],
|
||||
0,
|
||||
Color::LightYellow,
|
||||
);
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Common(IdCommon::SavePopup),
|
||||
Box::new(components::SavePopup::default()),
|
||||
vec![],
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self.app.active(&Id::Common(IdCommon::SavePopup)).is_ok());
|
||||
}
|
||||
|
||||
/// ### umount_quit
|
||||
///
|
||||
/// Umount quit
|
||||
pub(super) fn umount_save_popup(&mut self) {
|
||||
self.view.umount(super::COMPONENT_RADIO_SAVE);
|
||||
}
|
||||
|
||||
pub(self) fn mount_header_tab(&mut self, idx: usize) {
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_TAB,
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.with_color(Color::LightYellow)
|
||||
.with_inverted_color(Color::Black)
|
||||
.with_borders(Borders::BOTTOM, BorderType::Thick, Color::White)
|
||||
.with_options(&[
|
||||
String::from("User Interface"),
|
||||
String::from("SSH Keys"),
|
||||
String::from("Theme"),
|
||||
])
|
||||
.with_value(idx)
|
||||
.rewind(true)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
pub(self) fn mount_footer(&mut self) {
|
||||
self.view.mount(
|
||||
super::COMPONENT_TEXT_FOOTER,
|
||||
Box::new(Span::new(
|
||||
SpanPropsBuilder::default()
|
||||
.with_spans(vec![
|
||||
TextSpan::new("Press ").bold(),
|
||||
TextSpan::new("<CTRL+H>").bold().fg(Color::Cyan),
|
||||
TextSpan::new(" to show keybindings").bold(),
|
||||
])
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
let _ = self.app.umount(&Id::Common(IdCommon::SavePopup));
|
||||
}
|
||||
|
||||
/// ### mount_help
|
||||
///
|
||||
/// Mount help
|
||||
pub(super) fn mount_help(&mut self) {
|
||||
self.view.mount(
|
||||
super::COMPONENT_TEXT_HELP,
|
||||
Box::new(List::new(
|
||||
ListPropsBuilder::default()
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
|
||||
.with_highlighted_str(Some("?"))
|
||||
.with_max_scroll_step(8)
|
||||
.bold()
|
||||
.with_title("Help", Alignment::Center)
|
||||
.scrollable(true)
|
||||
.with_rows(
|
||||
TableBuilder::default()
|
||||
.add_col(TextSpan::new("<ESC>").bold().fg(Color::Cyan))
|
||||
.add_col(TextSpan::from(" Exit setup"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::new("<TAB>").bold().fg(Color::Cyan))
|
||||
.add_col(TextSpan::from(" Change setup page"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::new("<RIGHT/LEFT>").bold().fg(Color::Cyan))
|
||||
.add_col(TextSpan::from(" Change cursor"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::new("<UP/DOWN>").bold().fg(Color::Cyan))
|
||||
.add_col(TextSpan::from(" Change input field"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::new("<ENTER>").bold().fg(Color::Cyan))
|
||||
.add_col(TextSpan::from(" Select / Dismiss popup"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::new("<DEL|E>").bold().fg(Color::Cyan))
|
||||
.add_col(TextSpan::from(" Delete SSH key"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::new("<CTRL+N>").bold().fg(Color::Cyan))
|
||||
.add_col(TextSpan::from(" New SSH key"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::new("<CTRL+R>").bold().fg(Color::Cyan))
|
||||
.add_col(TextSpan::from(" Revert changes"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::new("<CTRL+S>").bold().fg(Color::Cyan))
|
||||
.add_col(TextSpan::from(" Save configuration"))
|
||||
.build(),
|
||||
)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
// Active help
|
||||
self.view.active(super::COMPONENT_TEXT_HELP);
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Common(IdCommon::Keybindings),
|
||||
Box::new(components::Keybindings::default()),
|
||||
vec![],
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self.app.active(&Id::Common(IdCommon::Keybindings)).is_ok());
|
||||
}
|
||||
|
||||
/// ### umount_help
|
||||
///
|
||||
/// Umount help
|
||||
pub(super) fn umount_help(&mut self) {
|
||||
self.view.umount(super::COMPONENT_TEXT_HELP);
|
||||
let _ = self.app.umount(&Id::Common(IdCommon::Keybindings));
|
||||
}
|
||||
|
||||
// -- mount helpers
|
||||
|
||||
fn mount_text_dialog(&mut self, id: &str, text: &str, color: Color) {
|
||||
// Mount
|
||||
self.view.mount(
|
||||
id,
|
||||
Box::new(Paragraph::new(
|
||||
ParagraphPropsBuilder::default()
|
||||
.with_borders(Borders::ALL, BorderType::Thick, color)
|
||||
.with_foreground(color)
|
||||
.bold()
|
||||
.with_text_alignment(Alignment::Center)
|
||||
.with_texts(vec![TextSpan::from(text)])
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
// Give focus to error
|
||||
self.view.active(id);
|
||||
}
|
||||
|
||||
fn mount_radio_dialog(
|
||||
&mut self,
|
||||
id: &str,
|
||||
text: &str,
|
||||
opts: &[&str],
|
||||
default: usize,
|
||||
color: Color,
|
||||
) {
|
||||
self.view.mount(
|
||||
id,
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.with_color(color)
|
||||
.with_inverted_color(Color::Black)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, color)
|
||||
.with_title(text, Alignment::Center)
|
||||
.with_options(opts)
|
||||
.with_value(default)
|
||||
.rewind(true)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
// Active
|
||||
self.view.active(id);
|
||||
}
|
||||
|
||||
fn mount_radio(&mut self, id: &str, text: &str, opts: &[&str], default: usize, color: Color) {
|
||||
self.view.mount(
|
||||
id,
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.with_color(color)
|
||||
.with_inverted_color(Color::Black)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, color)
|
||||
.with_title(text, Alignment::Left)
|
||||
.with_options(opts)
|
||||
.with_value(default)
|
||||
.rewind(true)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
fn mount_input(&mut self, id: &str, label: &str, fg: Color, typ: InputType) {
|
||||
self.mount_input_ex(id, label, fg, typ, None, None);
|
||||
}
|
||||
|
||||
fn mount_input_ex(
|
||||
&mut self,
|
||||
id: &str,
|
||||
label: &str,
|
||||
fg: Color,
|
||||
typ: InputType,
|
||||
len: Option<usize>,
|
||||
value: Option<String>,
|
||||
) {
|
||||
let mut props = InputPropsBuilder::default();
|
||||
props
|
||||
.with_foreground(fg)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, fg)
|
||||
.with_label(label, Alignment::Left)
|
||||
.with_input(typ);
|
||||
if let Some(len) = len {
|
||||
props.with_input_len(len);
|
||||
pub(super) fn view_popups(&mut self, f: &mut Frame) {
|
||||
if self.app.mounted(&Id::Common(IdCommon::ErrorPopup)) {
|
||||
let popup = draw_area_in(f.size(), 50, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
// make popup
|
||||
self.app.view(&Id::Common(IdCommon::ErrorPopup), f, popup);
|
||||
} else if self.app.mounted(&Id::Common(IdCommon::QuitPopup)) {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 40, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
self.app.view(&Id::Common(IdCommon::QuitPopup), f, popup);
|
||||
} else if self.app.mounted(&Id::Common(IdCommon::Keybindings)) {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 50, 70);
|
||||
f.render_widget(Clear, popup);
|
||||
self.app.view(&Id::Common(IdCommon::Keybindings), f, popup);
|
||||
} else if self.app.mounted(&Id::Common(IdCommon::SavePopup)) {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 30, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
self.app.view(&Id::Common(IdCommon::SavePopup), f, popup);
|
||||
}
|
||||
if let Some(value) = value {
|
||||
props.with_value(value);
|
||||
}
|
||||
self.view.mount(id, Box::new(Input::new(props.build())));
|
||||
}
|
||||
|
||||
/// ### new_app
|
||||
///
|
||||
/// Clean app up and remount common components and global listener
|
||||
fn new_app(&mut self, layout: ViewLayout) {
|
||||
self.app.umount_all();
|
||||
self.mount_global_listener();
|
||||
self.mount_commons(layout);
|
||||
}
|
||||
|
||||
/// ### mount_commons
|
||||
///
|
||||
/// Mount common components
|
||||
fn mount_commons(&mut self, layout: ViewLayout) {
|
||||
// Radio tab
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Common(IdCommon::Header),
|
||||
Box::new(components::Header::new(layout)),
|
||||
vec![],
|
||||
)
|
||||
.is_ok());
|
||||
// Footer
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Common(IdCommon::Footer),
|
||||
Box::new(components::Footer::default()),
|
||||
vec![],
|
||||
)
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
/// ### mount_global_listener
|
||||
///
|
||||
/// Mount global listener
|
||||
fn mount_global_listener(&mut self) {
|
||||
assert!(self
|
||||
.app
|
||||
.mount(
|
||||
Id::Common(IdCommon::GlobalListener),
|
||||
Box::new(components::GlobalListener::default()),
|
||||
vec![
|
||||
Sub::new(
|
||||
SubEventClause::Keyboard(KeyEvent {
|
||||
code: Key::Esc,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}),
|
||||
Self::no_popup_mounted_clause(),
|
||||
),
|
||||
Sub::new(
|
||||
SubEventClause::Keyboard(KeyEvent {
|
||||
code: Key::Tab,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}),
|
||||
Self::no_popup_mounted_clause(),
|
||||
),
|
||||
Sub::new(
|
||||
SubEventClause::Keyboard(KeyEvent {
|
||||
code: Key::Char('h'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
}),
|
||||
Self::no_popup_mounted_clause(),
|
||||
),
|
||||
Sub::new(
|
||||
SubEventClause::Keyboard(KeyEvent {
|
||||
code: Key::Char('r'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
}),
|
||||
Self::no_popup_mounted_clause(),
|
||||
),
|
||||
Sub::new(
|
||||
SubEventClause::Keyboard(KeyEvent {
|
||||
code: Key::Char('s'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
}),
|
||||
Self::no_popup_mounted_clause(),
|
||||
),
|
||||
]
|
||||
)
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
/// ### no_popup_mounted_clause
|
||||
///
|
||||
/// Returns a sub clause which requires that no popup is mounted in order to be satisfied
|
||||
fn no_popup_mounted_clause() -> SubClause<Id> {
|
||||
SubClause::And(
|
||||
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(Id::Common(
|
||||
IdCommon::ErrorPopup,
|
||||
))))),
|
||||
Box::new(SubClause::And(
|
||||
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(Id::Common(
|
||||
IdCommon::Keybindings,
|
||||
))))),
|
||||
Box::new(SubClause::And(
|
||||
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(Id::Common(
|
||||
IdCommon::QuitPopup,
|
||||
))))),
|
||||
Box::new(SubClause::And(
|
||||
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(Id::Common(
|
||||
IdCommon::SavePopup,
|
||||
))))),
|
||||
Box::new(SubClause::And(
|
||||
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(Id::Ssh(
|
||||
IdSsh::DelSshKeyPopup,
|
||||
))))),
|
||||
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(Id::Ssh(
|
||||
IdSsh::SshHost,
|
||||
))))),
|
||||
)),
|
||||
)),
|
||||
)),
|
||||
)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,23 +27,15 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// Locals
|
||||
use super::{Context, InputType, SetupActivity};
|
||||
use super::{components, Context, Id, IdCommon, IdConfig, SetupActivity, ViewLayout};
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
use crate::fs::explorer::GroupDirs;
|
||||
use crate::ui::components::bytes::{Bytes, BytesPropsBuilder};
|
||||
use crate::utils::ui::draw_area_in;
|
||||
use crate::utils::fmt::fmt_bytes;
|
||||
|
||||
// Ext
|
||||
use std::path::PathBuf;
|
||||
use tui_realm_stdlib::{InputPropsBuilder, RadioPropsBuilder};
|
||||
use tuirealm::tui::{
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::Color,
|
||||
widgets::{BorderType, Borders, Clear},
|
||||
};
|
||||
use tuirealm::{
|
||||
props::{Alignment, PropsBuilder},
|
||||
Payload, Value, View,
|
||||
};
|
||||
use tuirealm::tui::layout::{Constraint, Direction, Layout};
|
||||
use tuirealm::{State, StateValue};
|
||||
|
||||
impl SetupActivity {
|
||||
// -- view
|
||||
@@ -52,92 +44,17 @@ impl SetupActivity {
|
||||
///
|
||||
/// Initialize setup view
|
||||
pub(super) fn init_setup(&mut self) {
|
||||
// Init view
|
||||
self.view = View::init();
|
||||
// Common stuff
|
||||
// Radio tab
|
||||
self.mount_header_tab(0);
|
||||
// Footer
|
||||
self.mount_footer();
|
||||
// Input fields
|
||||
self.mount_input(
|
||||
super::COMPONENT_INPUT_TEXT_EDITOR,
|
||||
"Text editor",
|
||||
Color::LightGreen,
|
||||
InputType::Text,
|
||||
);
|
||||
self.view.active(super::COMPONENT_INPUT_TEXT_EDITOR); // <-- Focus
|
||||
self.mount_radio(
|
||||
super::COMPONENT_RADIO_DEFAULT_PROTOCOL,
|
||||
"Default protocol",
|
||||
&["SFTP", "SCP", "FTP", "FTPS", "AWS S3"],
|
||||
0,
|
||||
Color::LightCyan,
|
||||
);
|
||||
self.mount_radio(
|
||||
super::COMPONENT_RADIO_HIDDEN_FILES,
|
||||
"Show hidden files (by default)?",
|
||||
&["Yes", "No"],
|
||||
0,
|
||||
Color::LightRed,
|
||||
);
|
||||
self.mount_radio(
|
||||
super::COMPONENT_RADIO_UPDATES,
|
||||
"Check for updates?",
|
||||
&["Yes", "No"],
|
||||
0,
|
||||
Color::LightYellow,
|
||||
);
|
||||
self.mount_radio(
|
||||
super::COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE,
|
||||
"Prompt when replacing existing files?",
|
||||
&["Yes", "No"],
|
||||
0,
|
||||
Color::LightCyan,
|
||||
);
|
||||
self.mount_radio(
|
||||
super::COMPONENT_RADIO_GROUP_DIRS,
|
||||
"Group directories",
|
||||
&["Display first", "Display last", "No"],
|
||||
0,
|
||||
Color::LightMagenta,
|
||||
);
|
||||
self.mount_input(
|
||||
super::COMPONENT_INPUT_LOCAL_FILE_FMT,
|
||||
"File formatter syntax (local)",
|
||||
Color::LightGreen,
|
||||
InputType::Text,
|
||||
);
|
||||
self.mount_input(
|
||||
super::COMPONENT_INPUT_REMOTE_FILE_FMT,
|
||||
"File formatter syntax (remote)",
|
||||
Color::LightCyan,
|
||||
InputType::Text,
|
||||
);
|
||||
self.mount_radio(
|
||||
super::COMPONENT_RADIO_NOTIFICATIONS_ENABLED,
|
||||
"Enable notifications?",
|
||||
&["Yes", "No"],
|
||||
0,
|
||||
Color::LightRed,
|
||||
);
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD,
|
||||
Box::new(Bytes::new(
|
||||
BytesPropsBuilder::default()
|
||||
.with_foreground(Color::LightYellow)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightYellow)
|
||||
.with_label("Notifications: minimum transfer size", Alignment::Left)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
// Init view (and mount commons)
|
||||
self.new_app(ViewLayout::SetupForm);
|
||||
// Load values
|
||||
self.load_input_values();
|
||||
// Active text editor
|
||||
assert!(self.app.active(&Id::Config(IdConfig::TextEditor)).is_ok());
|
||||
}
|
||||
|
||||
pub(super) fn view_setup(&mut self) {
|
||||
let mut ctx: Context = self.context.take().unwrap();
|
||||
let _ = ctx.terminal().draw(|f| {
|
||||
let _ = ctx.terminal().raw_mut().draw(|f| {
|
||||
// Prepare main chunks
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
@@ -152,8 +69,8 @@ impl SetupActivity {
|
||||
)
|
||||
.split(f.size());
|
||||
// Render common widget
|
||||
self.view.render(super::COMPONENT_RADIO_TAB, f, chunks[0]);
|
||||
self.view.render(super::COMPONENT_TEXT_FOOTER, f, chunks[2]);
|
||||
self.app.view(&Id::Common(IdCommon::Header), f, chunks[0]);
|
||||
self.app.view(&Id::Common(IdCommon::Footer), f, chunks[2]);
|
||||
// Make chunks (two columns)
|
||||
let ui_cfg_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
@@ -174,27 +91,27 @@ impl SetupActivity {
|
||||
.as_ref(),
|
||||
)
|
||||
.split(ui_cfg_chunks[0]);
|
||||
self.view
|
||||
.render(super::COMPONENT_INPUT_TEXT_EDITOR, f, ui_cfg_chunks_col1[0]);
|
||||
self.view.render(
|
||||
super::COMPONENT_RADIO_DEFAULT_PROTOCOL,
|
||||
self.app
|
||||
.view(&Id::Config(IdConfig::TextEditor), f, ui_cfg_chunks_col1[0]);
|
||||
self.app.view(
|
||||
&Id::Config(IdConfig::DefaultProtocol),
|
||||
f,
|
||||
ui_cfg_chunks_col1[1],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_RADIO_HIDDEN_FILES,
|
||||
self.app
|
||||
.view(&Id::Config(IdConfig::HiddenFiles), f, ui_cfg_chunks_col1[2]);
|
||||
self.app.view(
|
||||
&Id::Config(IdConfig::CheckUpdates),
|
||||
f,
|
||||
ui_cfg_chunks_col1[2],
|
||||
ui_cfg_chunks_col1[3],
|
||||
);
|
||||
self.view
|
||||
.render(super::COMPONENT_RADIO_UPDATES, f, ui_cfg_chunks_col1[3]);
|
||||
self.view.render(
|
||||
super::COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE,
|
||||
self.app.view(
|
||||
&Id::Config(IdConfig::PromptOnFileReplace),
|
||||
f,
|
||||
ui_cfg_chunks_col1[4],
|
||||
);
|
||||
self.view
|
||||
.render(super::COMPONENT_RADIO_GROUP_DIRS, f, ui_cfg_chunks_col1[5]);
|
||||
self.app
|
||||
.view(&Id::Config(IdConfig::GroupDirs), f, ui_cfg_chunks_col1[5]);
|
||||
// Column 2
|
||||
let ui_cfg_chunks_col2 = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
@@ -209,59 +126,28 @@ impl SetupActivity {
|
||||
.as_ref(),
|
||||
)
|
||||
.split(ui_cfg_chunks[1]);
|
||||
self.view.render(
|
||||
super::COMPONENT_INPUT_LOCAL_FILE_FMT,
|
||||
self.app.view(
|
||||
&Id::Config(IdConfig::LocalFileFmt),
|
||||
f,
|
||||
ui_cfg_chunks_col2[0],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_INPUT_REMOTE_FILE_FMT,
|
||||
self.app.view(
|
||||
&Id::Config(IdConfig::RemoteFileFmt),
|
||||
f,
|
||||
ui_cfg_chunks_col2[1],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_RADIO_NOTIFICATIONS_ENABLED,
|
||||
self.app.view(
|
||||
&Id::Config(IdConfig::NotificationsEnabled),
|
||||
f,
|
||||
ui_cfg_chunks_col2[2],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD,
|
||||
self.app.view(
|
||||
&Id::Config(IdConfig::NotificationsThreshold),
|
||||
f,
|
||||
ui_cfg_chunks_col2[3],
|
||||
);
|
||||
// Popups
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) {
|
||||
if props.visible {
|
||||
let popup = draw_area_in(f.size(), 50, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
// make popup
|
||||
self.view.render(super::COMPONENT_TEXT_ERROR, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) {
|
||||
if props.visible {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 40, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
self.view.render(super::COMPONENT_RADIO_QUIT, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_HELP) {
|
||||
if props.visible {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 50, 70);
|
||||
f.render_widget(Clear, popup);
|
||||
self.view.render(super::COMPONENT_TEXT_HELP, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_SAVE) {
|
||||
if props.visible {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 30, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
self.view.render(super::COMPONENT_RADIO_SAVE, f, popup);
|
||||
}
|
||||
}
|
||||
self.view_popups(f);
|
||||
});
|
||||
// Put context back to context
|
||||
self.context = Some(ctx);
|
||||
@@ -272,125 +158,127 @@ impl SetupActivity {
|
||||
/// Load values from configuration into input fields
|
||||
pub(crate) fn load_input_values(&mut self) {
|
||||
// Text editor
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_TEXT_EDITOR) {
|
||||
let text_editor: String =
|
||||
String::from(self.config().get_text_editor().as_path().to_string_lossy());
|
||||
let props = InputPropsBuilder::from(props)
|
||||
.with_value(text_editor)
|
||||
.build();
|
||||
let _ = self.view.update(super::COMPONENT_INPUT_TEXT_EDITOR, props);
|
||||
}
|
||||
let text_editor: String =
|
||||
String::from(self.config().get_text_editor().as_path().to_string_lossy());
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Config(IdConfig::TextEditor),
|
||||
Box::new(components::TextEditor::new(text_editor.as_str())),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
// Protocol
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_DEFAULT_PROTOCOL) {
|
||||
let protocol: usize = match self.config().get_default_protocol() {
|
||||
FileTransferProtocol::Sftp => 0,
|
||||
FileTransferProtocol::Scp => 1,
|
||||
FileTransferProtocol::Ftp(false) => 2,
|
||||
FileTransferProtocol::Ftp(true) => 3,
|
||||
FileTransferProtocol::AwsS3 => 4,
|
||||
};
|
||||
let props = RadioPropsBuilder::from(props).with_value(protocol).build();
|
||||
let _ = self
|
||||
.view
|
||||
.update(super::COMPONENT_RADIO_DEFAULT_PROTOCOL, props);
|
||||
}
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Config(IdConfig::DefaultProtocol),
|
||||
Box::new(components::DefaultProtocol::new(
|
||||
self.config().get_default_protocol()
|
||||
)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
// Hidden files
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_HIDDEN_FILES) {
|
||||
let hidden: usize = match self.config().get_show_hidden_files() {
|
||||
true => 0,
|
||||
false => 1,
|
||||
};
|
||||
let props = RadioPropsBuilder::from(props).with_value(hidden).build();
|
||||
let _ = self.view.update(super::COMPONENT_RADIO_HIDDEN_FILES, props);
|
||||
}
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Config(IdConfig::HiddenFiles),
|
||||
Box::new(components::HiddenFiles::new(
|
||||
self.config().get_show_hidden_files()
|
||||
)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
// Updates
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_UPDATES) {
|
||||
let updates: usize = match self.config().get_check_for_updates() {
|
||||
true => 0,
|
||||
false => 1,
|
||||
};
|
||||
let props = RadioPropsBuilder::from(props).with_value(updates).build();
|
||||
let _ = self.view.update(super::COMPONENT_RADIO_UPDATES, props);
|
||||
}
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Config(IdConfig::CheckUpdates),
|
||||
Box::new(components::CheckUpdates::new(
|
||||
self.config().get_check_for_updates()
|
||||
)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
// File replace
|
||||
if let Some(props) = self
|
||||
.view
|
||||
.get_props(super::COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE)
|
||||
{
|
||||
let updates: usize = match self.config().get_prompt_on_file_replace() {
|
||||
true => 0,
|
||||
false => 1,
|
||||
};
|
||||
let props = RadioPropsBuilder::from(props).with_value(updates).build();
|
||||
let _ = self
|
||||
.view
|
||||
.update(super::COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE, props);
|
||||
}
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Config(IdConfig::PromptOnFileReplace),
|
||||
Box::new(components::PromptOnFileReplace::new(
|
||||
self.config().get_prompt_on_file_replace()
|
||||
)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
// Group dirs
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_GROUP_DIRS) {
|
||||
let dirs: usize = match self.config().get_group_dirs() {
|
||||
Some(GroupDirs::First) => 0,
|
||||
Some(GroupDirs::Last) => 1,
|
||||
None => 2,
|
||||
};
|
||||
let props = RadioPropsBuilder::from(props).with_value(dirs).build();
|
||||
let _ = self.view.update(super::COMPONENT_RADIO_GROUP_DIRS, props);
|
||||
}
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Config(IdConfig::GroupDirs),
|
||||
Box::new(components::GroupDirs::new(self.config().get_group_dirs())),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
// Local File Fmt
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_LOCAL_FILE_FMT) {
|
||||
let file_fmt: String = self.config().get_local_file_fmt().unwrap_or_default();
|
||||
let props = InputPropsBuilder::from(props).with_value(file_fmt).build();
|
||||
let _ = self
|
||||
.view
|
||||
.update(super::COMPONENT_INPUT_LOCAL_FILE_FMT, props);
|
||||
}
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Config(IdConfig::LocalFileFmt),
|
||||
Box::new(components::LocalFileFmt::new(
|
||||
&self.config().get_local_file_fmt().unwrap_or_default()
|
||||
)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
// Remote File Fmt
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_REMOTE_FILE_FMT) {
|
||||
let file_fmt: String = self.config().get_remote_file_fmt().unwrap_or_default();
|
||||
let props = InputPropsBuilder::from(props).with_value(file_fmt).build();
|
||||
let _ = self
|
||||
.view
|
||||
.update(super::COMPONENT_INPUT_REMOTE_FILE_FMT, props);
|
||||
}
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Config(IdConfig::RemoteFileFmt),
|
||||
Box::new(components::RemoteFileFmt::new(
|
||||
&self.config().get_remote_file_fmt().unwrap_or_default()
|
||||
)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
// Notifications enabled
|
||||
if let Some(props) = self
|
||||
.view
|
||||
.get_props(super::COMPONENT_RADIO_NOTIFICATIONS_ENABLED)
|
||||
{
|
||||
let enabled: usize = match self.config().get_notifications() {
|
||||
true => 0,
|
||||
false => 1,
|
||||
};
|
||||
let props = RadioPropsBuilder::from(props).with_value(enabled).build();
|
||||
let _ = self
|
||||
.view
|
||||
.update(super::COMPONENT_RADIO_NOTIFICATIONS_ENABLED, props);
|
||||
}
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Config(IdConfig::NotificationsEnabled),
|
||||
Box::new(components::NotificationsEnabled::new(
|
||||
self.config().get_notifications()
|
||||
)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
// Notifications threshold
|
||||
if let Some(props) = self
|
||||
.view
|
||||
.get_props(super::COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD)
|
||||
{
|
||||
let value: u64 = self.config().get_notification_threshold();
|
||||
let props = BytesPropsBuilder::from(props).with_value(value).build();
|
||||
let _ = self
|
||||
.view
|
||||
.update(super::COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD, props);
|
||||
}
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Config(IdConfig::NotificationsThreshold),
|
||||
Box::new(components::NotificationsThreshold::new(&fmt_bytes(
|
||||
self.config().get_notification_threshold()
|
||||
))),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
/// ### collect_input_values
|
||||
///
|
||||
/// Collect values from input and put them into the configuration
|
||||
pub(crate) fn collect_input_values(&mut self) {
|
||||
if let Some(Payload::One(Value::Str(editor))) =
|
||||
self.view.get_state(super::COMPONENT_INPUT_TEXT_EDITOR)
|
||||
if let Ok(State::One(StateValue::String(editor))) =
|
||||
self.app.state(&Id::Config(IdConfig::TextEditor))
|
||||
{
|
||||
self.config_mut()
|
||||
.set_text_editor(PathBuf::from(editor.as_str()));
|
||||
}
|
||||
if let Some(Payload::One(Value::Usize(protocol))) =
|
||||
self.view.get_state(super::COMPONENT_RADIO_DEFAULT_PROTOCOL)
|
||||
if let Ok(State::One(StateValue::Usize(protocol))) =
|
||||
self.app.state(&Id::Config(IdConfig::DefaultProtocol))
|
||||
{
|
||||
let protocol: FileTransferProtocol = match protocol {
|
||||
1 => FileTransferProtocol::Scp,
|
||||
@@ -401,37 +289,36 @@ impl SetupActivity {
|
||||
};
|
||||
self.config_mut().set_default_protocol(protocol);
|
||||
}
|
||||
if let Some(Payload::One(Value::Usize(opt))) =
|
||||
self.view.get_state(super::COMPONENT_RADIO_HIDDEN_FILES)
|
||||
if let Ok(State::One(StateValue::Usize(opt))) =
|
||||
self.app.state(&Id::Config(IdConfig::HiddenFiles))
|
||||
{
|
||||
let show: bool = matches!(opt, 0);
|
||||
self.config_mut().set_show_hidden_files(show);
|
||||
}
|
||||
if let Some(Payload::One(Value::Usize(opt))) =
|
||||
self.view.get_state(super::COMPONENT_RADIO_UPDATES)
|
||||
if let Ok(State::One(StateValue::Usize(opt))) =
|
||||
self.app.state(&Id::Config(IdConfig::CheckUpdates))
|
||||
{
|
||||
let check: bool = matches!(opt, 0);
|
||||
self.config_mut().set_check_for_updates(check);
|
||||
}
|
||||
if let Some(Payload::One(Value::Usize(opt))) = self
|
||||
.view
|
||||
.get_state(super::COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE)
|
||||
if let Ok(State::One(StateValue::Usize(opt))) =
|
||||
self.app.state(&Id::Config(IdConfig::PromptOnFileReplace))
|
||||
{
|
||||
let check: bool = matches!(opt, 0);
|
||||
self.config_mut().set_prompt_on_file_replace(check);
|
||||
}
|
||||
if let Some(Payload::One(Value::Str(fmt))) =
|
||||
self.view.get_state(super::COMPONENT_INPUT_LOCAL_FILE_FMT)
|
||||
if let Ok(State::One(StateValue::String(fmt))) =
|
||||
self.app.state(&Id::Config(IdConfig::LocalFileFmt))
|
||||
{
|
||||
self.config_mut().set_local_file_fmt(fmt);
|
||||
}
|
||||
if let Some(Payload::One(Value::Str(fmt))) =
|
||||
self.view.get_state(super::COMPONENT_INPUT_REMOTE_FILE_FMT)
|
||||
if let Ok(State::One(StateValue::String(fmt))) =
|
||||
self.app.state(&Id::Config(IdConfig::RemoteFileFmt))
|
||||
{
|
||||
self.config_mut().set_remote_file_fmt(fmt);
|
||||
}
|
||||
if let Some(Payload::One(Value::Usize(opt))) =
|
||||
self.view.get_state(super::COMPONENT_RADIO_GROUP_DIRS)
|
||||
if let Ok(State::One(StateValue::Usize(opt))) =
|
||||
self.app.state(&Id::Config(IdConfig::GroupDirs))
|
||||
{
|
||||
let dirs: Option<GroupDirs> = match opt {
|
||||
0 => Some(GroupDirs::First),
|
||||
@@ -440,15 +327,14 @@ impl SetupActivity {
|
||||
};
|
||||
self.config_mut().set_group_dirs(dirs);
|
||||
}
|
||||
if let Some(Payload::One(Value::Usize(opt))) = self
|
||||
.view
|
||||
.get_state(super::COMPONENT_RADIO_NOTIFICATIONS_ENABLED)
|
||||
if let Ok(State::One(StateValue::Usize(opt))) =
|
||||
self.app.state(&Id::Config(IdConfig::NotificationsEnabled))
|
||||
{
|
||||
self.config_mut().set_notifications(opt == 0);
|
||||
}
|
||||
if let Some(Payload::One(Value::U64(bytes))) = self
|
||||
.view
|
||||
.get_state(super::COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD)
|
||||
if let Ok(State::One(StateValue::U64(bytes))) = self
|
||||
.app
|
||||
.state(&Id::Config(IdConfig::NotificationsThreshold))
|
||||
{
|
||||
self.config_mut().set_notification_threshold(bytes);
|
||||
}
|
||||
|
||||
@@ -27,20 +27,12 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// Locals
|
||||
use super::{Context, SetupActivity};
|
||||
use crate::ui::components::bookmark_list::{BookmarkList, BookmarkListPropsBuilder};
|
||||
use super::{components, Context, Id, IdCommon, IdSsh, SetupActivity, ViewLayout};
|
||||
use crate::utils::ui::draw_area_in;
|
||||
|
||||
// Ext
|
||||
use tui_realm_stdlib::{Input, InputPropsBuilder};
|
||||
use tuirealm::tui::{
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::Color,
|
||||
widgets::{BorderType, Borders, Clear},
|
||||
};
|
||||
use tuirealm::{
|
||||
props::{Alignment, PropsBuilder},
|
||||
View,
|
||||
};
|
||||
use tuirealm::tui::layout::{Constraint, Direction, Layout};
|
||||
use tuirealm::tui::widgets::Clear;
|
||||
|
||||
impl SetupActivity {
|
||||
// -- view
|
||||
@@ -49,34 +41,17 @@ impl SetupActivity {
|
||||
///
|
||||
/// Initialize ssh keys view
|
||||
pub(super) fn init_ssh_keys(&mut self) {
|
||||
// Init view
|
||||
self.view = View::init();
|
||||
// Common stuff
|
||||
// Radio tab
|
||||
// Radio tab
|
||||
self.mount_header_tab(1);
|
||||
// Footer
|
||||
self.mount_footer();
|
||||
self.view.mount(
|
||||
super::COMPONENT_LIST_SSH_KEYS,
|
||||
Box::new(BookmarkList::new(
|
||||
BookmarkListPropsBuilder::default()
|
||||
.with_title("SSH keys", Alignment::Left)
|
||||
.with_borders(Borders::ALL, BorderType::Plain, Color::LightGreen)
|
||||
.with_background(Color::LightGreen)
|
||||
.with_foreground(Color::Black)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
// Give focus
|
||||
self.view.active(super::COMPONENT_LIST_SSH_KEYS);
|
||||
// Init view (and mount commons)
|
||||
self.new_app(ViewLayout::SshKeys);
|
||||
// Load keys
|
||||
self.reload_ssh_keys();
|
||||
// Give focus
|
||||
assert!(self.app.active(&Id::Ssh(IdSsh::SshKeys)).is_ok());
|
||||
}
|
||||
|
||||
pub(crate) fn view_ssh_keys(&mut self) {
|
||||
let mut ctx: Context = self.context.take().unwrap();
|
||||
let _ = ctx.terminal().draw(|f| {
|
||||
let _ = ctx.terminal().raw_mut().draw(|f| {
|
||||
// Prepare main chunks
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
@@ -91,72 +66,31 @@ impl SetupActivity {
|
||||
)
|
||||
.split(f.size());
|
||||
// Render common widget
|
||||
self.view.render(super::COMPONENT_RADIO_TAB, f, chunks[0]);
|
||||
self.view.render(super::COMPONENT_TEXT_FOOTER, f, chunks[2]);
|
||||
self.view
|
||||
.render(super::COMPONENT_LIST_SSH_KEYS, f, chunks[1]);
|
||||
self.app.view(&Id::Common(IdCommon::Header), f, chunks[0]);
|
||||
self.app.view(&Id::Common(IdCommon::Footer), f, chunks[2]);
|
||||
self.app.view(&Id::Ssh(IdSsh::SshKeys), f, chunks[1]);
|
||||
// Popups
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) {
|
||||
if props.visible {
|
||||
let popup = draw_area_in(f.size(), 50, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
// make popup
|
||||
self.view.render(super::COMPONENT_TEXT_ERROR, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) {
|
||||
if props.visible {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 40, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
self.view.render(super::COMPONENT_RADIO_QUIT, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_HELP) {
|
||||
if props.visible {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 50, 70);
|
||||
f.render_widget(Clear, popup);
|
||||
self.view.render(super::COMPONENT_TEXT_HELP, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_SAVE) {
|
||||
if props.visible {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 30, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
self.view.render(super::COMPONENT_RADIO_SAVE, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_DEL_SSH_KEY) {
|
||||
if props.visible {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 30, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
self.view
|
||||
.render(super::COMPONENT_RADIO_DEL_SSH_KEY, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_SSH_HOST) {
|
||||
if props.visible {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 50, 20);
|
||||
f.render_widget(Clear, popup);
|
||||
let popup_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3), // Host
|
||||
Constraint::Length(3), // Username
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(popup);
|
||||
self.view
|
||||
.render(super::COMPONENT_INPUT_SSH_HOST, f, popup_chunks[0]);
|
||||
self.view
|
||||
.render(super::COMPONENT_INPUT_SSH_USERNAME, f, popup_chunks[1]);
|
||||
}
|
||||
self.view_popups(f);
|
||||
if self.app.mounted(&Id::Ssh(IdSsh::DelSshKeyPopup)) {
|
||||
let popup = draw_area_in(f.size(), 30, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
self.app.view(&Id::Ssh(IdSsh::DelSshKeyPopup), f, popup);
|
||||
} else if self.app.mounted(&Id::Ssh(IdSsh::SshHost)) {
|
||||
let popup = draw_area_in(f.size(), 50, 20);
|
||||
f.render_widget(Clear, popup);
|
||||
let popup_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3), // Host
|
||||
Constraint::Length(3), // Username
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(popup);
|
||||
self.app.view(&Id::Ssh(IdSsh::SshHost), f, popup_chunks[0]);
|
||||
self.app
|
||||
.view(&Id::Ssh(IdSsh::SshUsername), f, popup_chunks[1]);
|
||||
}
|
||||
});
|
||||
// Put context back to context
|
||||
@@ -169,82 +103,74 @@ impl SetupActivity {
|
||||
///
|
||||
/// Mount delete ssh key component
|
||||
pub(crate) fn mount_del_ssh_key(&mut self) {
|
||||
self.mount_radio_dialog(
|
||||
super::COMPONENT_RADIO_DEL_SSH_KEY,
|
||||
"Delete key?",
|
||||
&["Yes", "No"],
|
||||
1,
|
||||
Color::LightRed,
|
||||
);
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Ssh(IdSsh::DelSshKeyPopup),
|
||||
Box::new(components::DelSshKeyPopup::default()),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self.app.active(&Id::Ssh(IdSsh::DelSshKeyPopup)).is_ok());
|
||||
}
|
||||
|
||||
/// ### umount_del_ssh_key
|
||||
///
|
||||
/// Umount delete ssh key
|
||||
pub(crate) fn umount_del_ssh_key(&mut self) {
|
||||
self.view.umount(super::COMPONENT_RADIO_DEL_SSH_KEY);
|
||||
let _ = self.app.umount(&Id::Ssh(IdSsh::DelSshKeyPopup));
|
||||
}
|
||||
|
||||
/// ### mount_new_ssh_key
|
||||
///
|
||||
/// Mount new ssh key prompt
|
||||
pub(crate) fn mount_new_ssh_key(&mut self) {
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_SSH_HOST,
|
||||
Box::new(Input::new(
|
||||
InputPropsBuilder::default()
|
||||
.with_label("Hostname or address", Alignment::Center)
|
||||
.with_borders(
|
||||
Borders::TOP | Borders::RIGHT | Borders::LEFT,
|
||||
BorderType::Plain,
|
||||
Color::Reset,
|
||||
)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_SSH_USERNAME,
|
||||
Box::new(Input::new(
|
||||
InputPropsBuilder::default()
|
||||
.with_label("Username", Alignment::Center)
|
||||
.with_borders(
|
||||
Borders::BOTTOM | Borders::RIGHT | Borders::LEFT,
|
||||
BorderType::Plain,
|
||||
Color::Reset,
|
||||
)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
self.view.active(super::COMPONENT_INPUT_SSH_HOST);
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Ssh(IdSsh::SshHost),
|
||||
Box::new(components::SshHost::default()),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Ssh(IdSsh::SshUsername),
|
||||
Box::new(components::SshUsername::default()),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self.app.active(&Id::Ssh(IdSsh::SshHost)).is_ok());
|
||||
}
|
||||
|
||||
/// ### umount_new_ssh_key
|
||||
///
|
||||
/// Umount new ssh key prompt
|
||||
pub(crate) fn umount_new_ssh_key(&mut self) {
|
||||
self.view.umount(super::COMPONENT_INPUT_SSH_HOST);
|
||||
self.view.umount(super::COMPONENT_INPUT_SSH_USERNAME);
|
||||
let _ = self.app.umount(&Id::Ssh(IdSsh::SshUsername));
|
||||
let _ = self.app.umount(&Id::Ssh(IdSsh::SshHost));
|
||||
}
|
||||
|
||||
/// ### reload_ssh_keys
|
||||
///
|
||||
/// Reload ssh keys
|
||||
pub(crate) fn reload_ssh_keys(&mut self) {
|
||||
// get props
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_LIST_SSH_KEYS) {
|
||||
// Create texts
|
||||
let keys: Vec<String> = self
|
||||
.config()
|
||||
.iter_ssh_keys()
|
||||
.map(|x| {
|
||||
let (addr, username, _) = self.config().get_ssh_key(x).ok().unwrap().unwrap();
|
||||
format!("{} at {}", addr, username)
|
||||
})
|
||||
.collect();
|
||||
let props = BookmarkListPropsBuilder::from(props)
|
||||
.with_bookmarks(keys)
|
||||
.build();
|
||||
self.view.update(super::COMPONENT_LIST_SSH_KEYS, props);
|
||||
}
|
||||
let keys: Vec<String> = self
|
||||
.config()
|
||||
.iter_ssh_keys()
|
||||
.map(|x| {
|
||||
let (addr, username, _) = self.config().get_ssh_key(x).ok().unwrap().unwrap();
|
||||
format!("{} at {}", addr, username)
|
||||
})
|
||||
.collect();
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Ssh(IdSsh::SshKeys),
|
||||
Box::new(components::SshKeys::new(&keys)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,22 +27,10 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// Locals
|
||||
use super::{Context, SetupActivity};
|
||||
use crate::config::themes::Theme;
|
||||
use crate::ui::components::color_picker::{ColorPicker, ColorPickerPropsBuilder};
|
||||
use crate::utils::parser::parse_color;
|
||||
use crate::utils::ui::draw_area_in;
|
||||
use super::{components, Context, Id, IdCommon, IdTheme, SetupActivity, Theme, ViewLayout};
|
||||
|
||||
// Ext
|
||||
use tui_realm_stdlib::{Label, LabelPropsBuilder};
|
||||
use tuirealm::tui::{
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::Color,
|
||||
widgets::{BorderType, Borders, Clear},
|
||||
};
|
||||
use tuirealm::{
|
||||
props::{Alignment, PropsBuilder},
|
||||
Payload, Value, View,
|
||||
};
|
||||
use tuirealm::tui::layout::{Constraint, Direction, Layout};
|
||||
|
||||
impl SetupActivity {
|
||||
// -- view
|
||||
@@ -51,96 +39,19 @@ impl SetupActivity {
|
||||
///
|
||||
/// Initialize thene view
|
||||
pub(super) fn init_theme(&mut self) {
|
||||
// Init view
|
||||
self.view = View::init();
|
||||
// Common stuff
|
||||
// Radio tab
|
||||
self.mount_header_tab(2);
|
||||
// Footer
|
||||
self.mount_footer();
|
||||
// auth colors
|
||||
self.mount_title(super::COMPONENT_COLOR_AUTH_TITLE, "Authentication styles");
|
||||
self.mount_color_picker(super::COMPONENT_COLOR_AUTH_PROTOCOL, "Protocol");
|
||||
self.mount_color_picker(super::COMPONENT_COLOR_AUTH_ADDR, "Ip address");
|
||||
self.mount_color_picker(super::COMPONENT_COLOR_AUTH_PORT, "Port");
|
||||
self.mount_color_picker(super::COMPONENT_COLOR_AUTH_USERNAME, "Username");
|
||||
self.mount_color_picker(super::COMPONENT_COLOR_AUTH_PASSWORD, "Password");
|
||||
self.mount_color_picker(super::COMPONENT_COLOR_AUTH_BOOKMARKS, "Bookmarks");
|
||||
self.mount_color_picker(super::COMPONENT_COLOR_AUTH_RECENTS, "Recent connections");
|
||||
// Misc
|
||||
self.mount_title(super::COMPONENT_COLOR_MISC_TITLE, "Misc styles");
|
||||
self.mount_color_picker(super::COMPONENT_COLOR_MISC_ERROR, "Error");
|
||||
self.mount_color_picker(super::COMPONENT_COLOR_MISC_INFO, "Info dialogs");
|
||||
self.mount_color_picker(super::COMPONENT_COLOR_MISC_INPUT, "Input fields");
|
||||
self.mount_color_picker(super::COMPONENT_COLOR_MISC_KEYS, "Key strokes");
|
||||
self.mount_color_picker(super::COMPONENT_COLOR_MISC_QUIT, "Quit dialogs");
|
||||
self.mount_color_picker(super::COMPONENT_COLOR_MISC_SAVE, "Save confirmations");
|
||||
self.mount_color_picker(super::COMPONENT_COLOR_MISC_WARN, "Warnings");
|
||||
// Transfer (1)
|
||||
self.mount_title(super::COMPONENT_COLOR_TRANSFER_TITLE, "Transfer styles");
|
||||
self.mount_color_picker(
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG,
|
||||
"Local explorer background",
|
||||
);
|
||||
self.mount_color_picker(
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG,
|
||||
"Local explorer foreground",
|
||||
);
|
||||
self.mount_color_picker(
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG,
|
||||
"Local explorer highlighted",
|
||||
);
|
||||
self.mount_color_picker(
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG,
|
||||
"Remote explorer background",
|
||||
);
|
||||
self.mount_color_picker(
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG,
|
||||
"Remote explorer foreground",
|
||||
);
|
||||
self.mount_color_picker(
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG,
|
||||
"Remote explorer highlighted",
|
||||
);
|
||||
self.mount_color_picker(
|
||||
super::COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL,
|
||||
"'Full transfer' Progress bar",
|
||||
);
|
||||
self.mount_color_picker(
|
||||
super::COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL,
|
||||
"'Partial transfer' Progress bar",
|
||||
);
|
||||
// Transfer (2)
|
||||
self.mount_title(
|
||||
super::COMPONENT_COLOR_TRANSFER_TITLE_2,
|
||||
"Transfer styles (2)",
|
||||
);
|
||||
self.mount_color_picker(
|
||||
super::COMPONENT_COLOR_TRANSFER_LOG_BG,
|
||||
"Log window background",
|
||||
);
|
||||
self.mount_color_picker(super::COMPONENT_COLOR_TRANSFER_LOG_WIN, "Log window");
|
||||
self.mount_color_picker(
|
||||
super::COMPONENT_COLOR_TRANSFER_STATUS_SORTING,
|
||||
"File sorting",
|
||||
);
|
||||
self.mount_color_picker(
|
||||
super::COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN,
|
||||
"Hidden files",
|
||||
);
|
||||
self.mount_color_picker(
|
||||
super::COMPONENT_COLOR_TRANSFER_STATUS_SYNC,
|
||||
"Synchronized browsing",
|
||||
);
|
||||
// Init view (and mount commons)
|
||||
self.new_app(ViewLayout::Theme);
|
||||
// Mount titles
|
||||
self.load_titles();
|
||||
// Load styles
|
||||
self.load_styles();
|
||||
// Active first field
|
||||
self.view.active(super::COMPONENT_COLOR_AUTH_PROTOCOL);
|
||||
assert!(self.app.active(&Id::Theme(IdTheme::AuthProtocol)).is_ok());
|
||||
}
|
||||
|
||||
pub(super) fn view_theme(&mut self) {
|
||||
let mut ctx: Context = self.context.take().unwrap();
|
||||
let _ = ctx.terminal().draw(|f| {
|
||||
let _ = ctx.terminal().raw_mut().draw(|f| {
|
||||
// Prepare main chunks
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
@@ -155,8 +66,8 @@ impl SetupActivity {
|
||||
)
|
||||
.split(f.size());
|
||||
// Render common widget
|
||||
self.view.render(super::COMPONENT_RADIO_TAB, f, chunks[0]);
|
||||
self.view.render(super::COMPONENT_TEXT_FOOTER, f, chunks[2]);
|
||||
self.app.view(&Id::Common(IdCommon::Header), f, chunks[0]);
|
||||
self.app.view(&Id::Common(IdCommon::Footer), f, chunks[2]);
|
||||
// Make chunks
|
||||
let colors_layout = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
@@ -186,34 +97,22 @@ impl SetupActivity {
|
||||
.as_ref(),
|
||||
)
|
||||
.split(colors_layout[0]);
|
||||
self.view
|
||||
.render(super::COMPONENT_COLOR_AUTH_TITLE, f, auth_colors_layout[0]);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_AUTH_PROTOCOL,
|
||||
f,
|
||||
auth_colors_layout[1],
|
||||
);
|
||||
self.view
|
||||
.render(super::COMPONENT_COLOR_AUTH_ADDR, f, auth_colors_layout[2]);
|
||||
self.view
|
||||
.render(super::COMPONENT_COLOR_AUTH_PORT, f, auth_colors_layout[3]);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_AUTH_USERNAME,
|
||||
f,
|
||||
auth_colors_layout[4],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_AUTH_PASSWORD,
|
||||
f,
|
||||
auth_colors_layout[5],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_AUTH_BOOKMARKS,
|
||||
f,
|
||||
auth_colors_layout[6],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_AUTH_RECENTS,
|
||||
self.app
|
||||
.view(&Id::Theme(IdTheme::AuthTitle), f, auth_colors_layout[0]);
|
||||
self.app
|
||||
.view(&Id::Theme(IdTheme::AuthProtocol), f, auth_colors_layout[1]);
|
||||
self.app
|
||||
.view(&Id::Theme(IdTheme::AuthAddress), f, auth_colors_layout[2]);
|
||||
self.app
|
||||
.view(&Id::Theme(IdTheme::AuthPort), f, auth_colors_layout[3]);
|
||||
self.app
|
||||
.view(&Id::Theme(IdTheme::AuthUsername), f, auth_colors_layout[4]);
|
||||
self.app
|
||||
.view(&Id::Theme(IdTheme::AuthPassword), f, auth_colors_layout[5]);
|
||||
self.app
|
||||
.view(&Id::Theme(IdTheme::AuthBookmarks), f, auth_colors_layout[6]);
|
||||
self.app.view(
|
||||
&Id::Theme(IdTheme::AuthRecentHosts),
|
||||
f,
|
||||
auth_colors_layout[7],
|
||||
);
|
||||
@@ -233,22 +132,22 @@ impl SetupActivity {
|
||||
.as_ref(),
|
||||
)
|
||||
.split(colors_layout[1]);
|
||||
self.view
|
||||
.render(super::COMPONENT_COLOR_MISC_TITLE, f, misc_colors_layout[0]);
|
||||
self.view
|
||||
.render(super::COMPONENT_COLOR_MISC_ERROR, f, misc_colors_layout[1]);
|
||||
self.view
|
||||
.render(super::COMPONENT_COLOR_MISC_INFO, f, misc_colors_layout[2]);
|
||||
self.view
|
||||
.render(super::COMPONENT_COLOR_MISC_INPUT, f, misc_colors_layout[3]);
|
||||
self.view
|
||||
.render(super::COMPONENT_COLOR_MISC_KEYS, f, misc_colors_layout[4]);
|
||||
self.view
|
||||
.render(super::COMPONENT_COLOR_MISC_QUIT, f, misc_colors_layout[5]);
|
||||
self.view
|
||||
.render(super::COMPONENT_COLOR_MISC_SAVE, f, misc_colors_layout[6]);
|
||||
self.view
|
||||
.render(super::COMPONENT_COLOR_MISC_WARN, f, misc_colors_layout[7]);
|
||||
self.app
|
||||
.view(&Id::Theme(IdTheme::MiscTitle), f, misc_colors_layout[0]);
|
||||
self.app
|
||||
.view(&Id::Theme(IdTheme::MiscError), f, misc_colors_layout[1]);
|
||||
self.app
|
||||
.view(&Id::Theme(IdTheme::MiscInfo), f, misc_colors_layout[2]);
|
||||
self.app
|
||||
.view(&Id::Theme(IdTheme::MiscInput), f, misc_colors_layout[3]);
|
||||
self.app
|
||||
.view(&Id::Theme(IdTheme::MiscKeys), f, misc_colors_layout[4]);
|
||||
self.app
|
||||
.view(&Id::Theme(IdTheme::MiscQuit), f, misc_colors_layout[5]);
|
||||
self.app
|
||||
.view(&Id::Theme(IdTheme::MiscSave), f, misc_colors_layout[6]);
|
||||
self.app
|
||||
.view(&Id::Theme(IdTheme::MiscWarn), f, misc_colors_layout[7]);
|
||||
|
||||
let transfer_colors_layout_col1 = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
@@ -266,38 +165,38 @@ impl SetupActivity {
|
||||
.as_ref(),
|
||||
)
|
||||
.split(colors_layout[2]);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_TRANSFER_TITLE,
|
||||
self.app.view(
|
||||
&Id::Theme(IdTheme::TransferTitle),
|
||||
f,
|
||||
transfer_colors_layout_col1[0],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG,
|
||||
self.app.view(
|
||||
&Id::Theme(IdTheme::ExplorerLocalBg),
|
||||
f,
|
||||
transfer_colors_layout_col1[1],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG,
|
||||
self.app.view(
|
||||
&Id::Theme(IdTheme::ExplorerLocalFg),
|
||||
f,
|
||||
transfer_colors_layout_col1[2],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG,
|
||||
self.app.view(
|
||||
&Id::Theme(IdTheme::ExplorerLocalHg),
|
||||
f,
|
||||
transfer_colors_layout_col1[3],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG,
|
||||
self.app.view(
|
||||
&Id::Theme(IdTheme::ExplorerRemoteBg),
|
||||
f,
|
||||
transfer_colors_layout_col1[4],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG,
|
||||
self.app.view(
|
||||
&Id::Theme(IdTheme::ExplorerRemoteFg),
|
||||
f,
|
||||
transfer_colors_layout_col1[5],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG,
|
||||
self.app.view(
|
||||
&Id::Theme(IdTheme::ExplorerRemoteHg),
|
||||
f,
|
||||
transfer_colors_layout_col1[6],
|
||||
);
|
||||
@@ -317,332 +216,328 @@ impl SetupActivity {
|
||||
.as_ref(),
|
||||
)
|
||||
.split(colors_layout[3]);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_TRANSFER_TITLE_2,
|
||||
self.app.view(
|
||||
&Id::Theme(IdTheme::TransferTitle2),
|
||||
f,
|
||||
transfer_colors_layout_col2[0],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL,
|
||||
self.app.view(
|
||||
&Id::Theme(IdTheme::ProgBarFull),
|
||||
f,
|
||||
transfer_colors_layout_col2[1],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL,
|
||||
self.app.view(
|
||||
&Id::Theme(IdTheme::ProgBarPartial),
|
||||
f,
|
||||
transfer_colors_layout_col2[2],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_TRANSFER_LOG_BG,
|
||||
self.app.view(
|
||||
&Id::Theme(IdTheme::LogBg),
|
||||
f,
|
||||
transfer_colors_layout_col2[3],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_TRANSFER_LOG_WIN,
|
||||
self.app.view(
|
||||
&Id::Theme(IdTheme::LogWindow),
|
||||
f,
|
||||
transfer_colors_layout_col2[4],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_TRANSFER_STATUS_SORTING,
|
||||
self.app.view(
|
||||
&Id::Theme(IdTheme::StatusSorting),
|
||||
f,
|
||||
transfer_colors_layout_col2[5],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN,
|
||||
self.app.view(
|
||||
&Id::Theme(IdTheme::StatusHidden),
|
||||
f,
|
||||
transfer_colors_layout_col2[6],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_TRANSFER_STATUS_SYNC,
|
||||
self.app.view(
|
||||
&Id::Theme(IdTheme::StatusSync),
|
||||
f,
|
||||
transfer_colors_layout_col2[7],
|
||||
);
|
||||
// Popups
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) {
|
||||
if props.visible {
|
||||
let popup = draw_area_in(f.size(), 50, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
// make popup
|
||||
self.view.render(super::COMPONENT_TEXT_ERROR, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) {
|
||||
if props.visible {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 40, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
self.view.render(super::COMPONENT_RADIO_QUIT, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_HELP) {
|
||||
if props.visible {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 50, 70);
|
||||
f.render_widget(Clear, popup);
|
||||
self.view.render(super::COMPONENT_TEXT_HELP, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_SAVE) {
|
||||
if props.visible {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 30, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
self.view.render(super::COMPONENT_RADIO_SAVE, f, popup);
|
||||
}
|
||||
}
|
||||
self.view_popups(f);
|
||||
});
|
||||
// Put context back to context
|
||||
self.context = Some(ctx);
|
||||
}
|
||||
|
||||
fn load_titles(&mut self) {
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Theme(IdTheme::AuthTitle),
|
||||
Box::new(components::AuthTitle::default()),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Theme(IdTheme::MiscTitle),
|
||||
Box::new(components::MiscTitle::default()),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Theme(IdTheme::TransferTitle),
|
||||
Box::new(components::TransferTitle::default()),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Theme(IdTheme::TransferTitle2),
|
||||
Box::new(components::TransferTitle2::default()),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
/// ### load_styles
|
||||
///
|
||||
/// Load values from theme into input fields
|
||||
pub(crate) fn load_styles(&mut self) {
|
||||
let theme: Theme = self.theme().clone();
|
||||
self.update_color(super::COMPONENT_COLOR_AUTH_ADDR, theme.auth_address);
|
||||
self.update_color(super::COMPONENT_COLOR_AUTH_BOOKMARKS, theme.auth_bookmarks);
|
||||
self.update_color(super::COMPONENT_COLOR_AUTH_PASSWORD, theme.auth_password);
|
||||
self.update_color(super::COMPONENT_COLOR_AUTH_PORT, theme.auth_port);
|
||||
self.update_color(super::COMPONENT_COLOR_AUTH_PROTOCOL, theme.auth_protocol);
|
||||
self.update_color(super::COMPONENT_COLOR_AUTH_RECENTS, theme.auth_recents);
|
||||
self.update_color(super::COMPONENT_COLOR_AUTH_USERNAME, theme.auth_username);
|
||||
self.update_color(super::COMPONENT_COLOR_MISC_ERROR, theme.misc_error_dialog);
|
||||
self.update_color(super::COMPONENT_COLOR_MISC_INFO, theme.misc_info_dialog);
|
||||
self.update_color(super::COMPONENT_COLOR_MISC_INPUT, theme.misc_input_dialog);
|
||||
self.update_color(super::COMPONENT_COLOR_MISC_KEYS, theme.misc_keys);
|
||||
self.update_color(super::COMPONENT_COLOR_MISC_QUIT, theme.misc_quit_dialog);
|
||||
self.update_color(super::COMPONENT_COLOR_MISC_SAVE, theme.misc_save_dialog);
|
||||
self.update_color(super::COMPONENT_COLOR_MISC_WARN, theme.misc_warn_dialog);
|
||||
self.update_color(
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG,
|
||||
theme.transfer_local_explorer_background,
|
||||
);
|
||||
self.update_color(
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG,
|
||||
theme.transfer_local_explorer_foreground,
|
||||
);
|
||||
self.update_color(
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG,
|
||||
theme.transfer_local_explorer_highlighted,
|
||||
);
|
||||
self.update_color(
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG,
|
||||
theme.transfer_remote_explorer_background,
|
||||
);
|
||||
self.update_color(
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG,
|
||||
theme.transfer_remote_explorer_foreground,
|
||||
);
|
||||
self.update_color(
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG,
|
||||
theme.transfer_remote_explorer_highlighted,
|
||||
);
|
||||
self.update_color(
|
||||
super::COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL,
|
||||
theme.transfer_progress_bar_full,
|
||||
);
|
||||
self.update_color(
|
||||
super::COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL,
|
||||
theme.transfer_progress_bar_partial,
|
||||
);
|
||||
self.update_color(
|
||||
super::COMPONENT_COLOR_TRANSFER_LOG_BG,
|
||||
theme.transfer_log_background,
|
||||
);
|
||||
self.update_color(
|
||||
super::COMPONENT_COLOR_TRANSFER_LOG_WIN,
|
||||
theme.transfer_log_window,
|
||||
);
|
||||
self.update_color(
|
||||
super::COMPONENT_COLOR_TRANSFER_STATUS_SORTING,
|
||||
theme.transfer_status_sorting,
|
||||
);
|
||||
self.update_color(
|
||||
super::COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN,
|
||||
theme.transfer_status_hidden,
|
||||
);
|
||||
self.update_color(
|
||||
super::COMPONENT_COLOR_TRANSFER_STATUS_SYNC,
|
||||
theme.transfer_status_sync_browsing,
|
||||
);
|
||||
}
|
||||
|
||||
/// ### collect_styles
|
||||
///
|
||||
/// Collect values from input and put them into the theme.
|
||||
/// If a component has an invalid color, returns Err(component_id)
|
||||
pub(crate) fn collect_styles(&mut self) -> Result<(), &'static str> {
|
||||
// auth
|
||||
let auth_address: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_AUTH_ADDR)
|
||||
.map_err(|_| super::COMPONENT_COLOR_AUTH_ADDR)?;
|
||||
let auth_bookmarks: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_AUTH_BOOKMARKS)
|
||||
.map_err(|_| super::COMPONENT_COLOR_AUTH_BOOKMARKS)?;
|
||||
let auth_password: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_AUTH_PASSWORD)
|
||||
.map_err(|_| super::COMPONENT_COLOR_AUTH_PASSWORD)?;
|
||||
let auth_port: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_AUTH_PORT)
|
||||
.map_err(|_| super::COMPONENT_COLOR_AUTH_PORT)?;
|
||||
let auth_protocol: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_AUTH_PROTOCOL)
|
||||
.map_err(|_| super::COMPONENT_COLOR_AUTH_PROTOCOL)?;
|
||||
let auth_recents: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_AUTH_RECENTS)
|
||||
.map_err(|_| super::COMPONENT_COLOR_AUTH_RECENTS)?;
|
||||
let auth_username: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_AUTH_USERNAME)
|
||||
.map_err(|_| super::COMPONENT_COLOR_AUTH_USERNAME)?;
|
||||
// misc
|
||||
let misc_error_dialog: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_MISC_ERROR)
|
||||
.map_err(|_| super::COMPONENT_COLOR_MISC_ERROR)?;
|
||||
let misc_info_dialog: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_MISC_INFO)
|
||||
.map_err(|_| super::COMPONENT_COLOR_MISC_INFO)?;
|
||||
let misc_input_dialog: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_MISC_INPUT)
|
||||
.map_err(|_| super::COMPONENT_COLOR_MISC_INPUT)?;
|
||||
let misc_keys: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_MISC_KEYS)
|
||||
.map_err(|_| super::COMPONENT_COLOR_MISC_KEYS)?;
|
||||
let misc_quit_dialog: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_MISC_QUIT)
|
||||
.map_err(|_| super::COMPONENT_COLOR_MISC_QUIT)?;
|
||||
let misc_save_dialog: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_MISC_SAVE)
|
||||
.map_err(|_| super::COMPONENT_COLOR_MISC_SAVE)?;
|
||||
let misc_warn_dialog: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_MISC_WARN)
|
||||
.map_err(|_| super::COMPONENT_COLOR_MISC_WARN)?;
|
||||
// transfer
|
||||
let transfer_local_explorer_background: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG)
|
||||
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG)?;
|
||||
let transfer_local_explorer_foreground: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG)
|
||||
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG)?;
|
||||
let transfer_local_explorer_highlighted: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG)
|
||||
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG)?;
|
||||
let transfer_remote_explorer_background: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG)
|
||||
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG)?;
|
||||
let transfer_remote_explorer_foreground: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG)
|
||||
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG)?;
|
||||
let transfer_remote_explorer_highlighted: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG)
|
||||
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG)?;
|
||||
let transfer_log_background: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_TRANSFER_LOG_BG)
|
||||
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_LOG_BG)?;
|
||||
let transfer_log_window: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_TRANSFER_LOG_WIN)
|
||||
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_LOG_WIN)?;
|
||||
let transfer_progress_bar_full: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL)
|
||||
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL)?;
|
||||
let transfer_progress_bar_partial: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL)
|
||||
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL)?;
|
||||
let transfer_status_hidden: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN)
|
||||
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN)?;
|
||||
let transfer_status_sorting: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_TRANSFER_STATUS_SORTING)
|
||||
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_STATUS_SORTING)?;
|
||||
let transfer_status_sync_browsing: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_TRANSFER_STATUS_SYNC)
|
||||
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_STATUS_SYNC)?;
|
||||
// Update theme
|
||||
let mut theme: &mut Theme = self.theme_mut();
|
||||
theme.auth_address = auth_address;
|
||||
theme.auth_bookmarks = auth_bookmarks;
|
||||
theme.auth_password = auth_password;
|
||||
theme.auth_port = auth_port;
|
||||
theme.auth_protocol = auth_protocol;
|
||||
theme.auth_recents = auth_recents;
|
||||
theme.auth_username = auth_username;
|
||||
theme.misc_error_dialog = misc_error_dialog;
|
||||
theme.misc_info_dialog = misc_info_dialog;
|
||||
theme.misc_input_dialog = misc_input_dialog;
|
||||
theme.misc_keys = misc_keys;
|
||||
theme.misc_quit_dialog = misc_quit_dialog;
|
||||
theme.misc_save_dialog = misc_save_dialog;
|
||||
theme.misc_warn_dialog = misc_warn_dialog;
|
||||
theme.transfer_local_explorer_background = transfer_local_explorer_background;
|
||||
theme.transfer_local_explorer_foreground = transfer_local_explorer_foreground;
|
||||
theme.transfer_local_explorer_highlighted = transfer_local_explorer_highlighted;
|
||||
theme.transfer_remote_explorer_background = transfer_remote_explorer_background;
|
||||
theme.transfer_remote_explorer_foreground = transfer_remote_explorer_foreground;
|
||||
theme.transfer_remote_explorer_highlighted = transfer_remote_explorer_highlighted;
|
||||
theme.transfer_log_background = transfer_log_background;
|
||||
theme.transfer_log_window = transfer_log_window;
|
||||
theme.transfer_progress_bar_full = transfer_progress_bar_full;
|
||||
theme.transfer_progress_bar_partial = transfer_progress_bar_partial;
|
||||
theme.transfer_status_hidden = transfer_status_hidden;
|
||||
theme.transfer_status_sorting = transfer_status_sorting;
|
||||
theme.transfer_status_sync_browsing = transfer_status_sync_browsing;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// ### update_color
|
||||
///
|
||||
/// Update color for provided component
|
||||
fn update_color(&mut self, component: &str, color: Color) {
|
||||
if let Some(props) = self.view.get_props(component) {
|
||||
self.view.update(
|
||||
component,
|
||||
ColorPickerPropsBuilder::from(props)
|
||||
.with_color(&color)
|
||||
.build(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// ### get_color
|
||||
///
|
||||
/// Get color from component
|
||||
fn get_color(&self, component: &str) -> Result<Color, ()> {
|
||||
match self.view.get_state(component) {
|
||||
Some(Payload::One(Value::Str(color))) => match parse_color(color.as_str()) {
|
||||
Some(c) => Ok(c),
|
||||
None => Err(()),
|
||||
},
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### mount_color_picker
|
||||
///
|
||||
/// Mount color picker with provided data
|
||||
fn mount_color_picker(&mut self, id: &str, label: &str) {
|
||||
self.view.mount(
|
||||
id,
|
||||
Box::new(ColorPicker::new(
|
||||
ColorPickerPropsBuilder::default()
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::Reset)
|
||||
.with_label(label.to_string(), Alignment::Left)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
/// ### mount_title
|
||||
///
|
||||
/// Mount title
|
||||
fn mount_title(&mut self, id: &str, text: &str) {
|
||||
self.view.mount(
|
||||
id,
|
||||
Box::new(Label::new(
|
||||
LabelPropsBuilder::default()
|
||||
.bold()
|
||||
.with_text(text.to_string())
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Theme(IdTheme::AuthAddress),
|
||||
Box::new(components::AuthAddress::new(theme.auth_address)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Theme(IdTheme::AuthBookmarks),
|
||||
Box::new(components::AuthBookmarks::new(theme.auth_bookmarks)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Theme(IdTheme::AuthPassword),
|
||||
Box::new(components::AuthPassword::new(theme.auth_password)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Theme(IdTheme::AuthPort),
|
||||
Box::new(components::AuthPort::new(theme.auth_port)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Theme(IdTheme::AuthProtocol),
|
||||
Box::new(components::AuthProtocol::new(theme.auth_protocol)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Theme(IdTheme::AuthRecentHosts),
|
||||
Box::new(components::AuthRecentHosts::new(theme.auth_recents)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Theme(IdTheme::AuthUsername),
|
||||
Box::new(components::AuthUsername::new(theme.auth_username)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Theme(IdTheme::MiscError),
|
||||
Box::new(components::MiscError::new(theme.misc_error_dialog)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Theme(IdTheme::MiscInfo),
|
||||
Box::new(components::MiscInfo::new(theme.misc_info_dialog)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Theme(IdTheme::MiscInput),
|
||||
Box::new(components::MiscInput::new(theme.misc_input_dialog)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Theme(IdTheme::MiscKeys),
|
||||
Box::new(components::MiscKeys::new(theme.misc_keys)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Theme(IdTheme::MiscQuit),
|
||||
Box::new(components::MiscQuit::new(theme.misc_quit_dialog)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Theme(IdTheme::MiscSave),
|
||||
Box::new(components::MiscSave::new(theme.misc_save_dialog)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Theme(IdTheme::MiscWarn),
|
||||
Box::new(components::MiscWarn::new(theme.misc_warn_dialog)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Theme(IdTheme::ExplorerLocalBg),
|
||||
Box::new(components::ExplorerLocalBg::new(
|
||||
theme.transfer_local_explorer_background
|
||||
)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Theme(IdTheme::ExplorerLocalFg),
|
||||
Box::new(components::ExplorerLocalFg::new(
|
||||
theme.transfer_local_explorer_foreground
|
||||
)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Theme(IdTheme::ExplorerLocalHg),
|
||||
Box::new(components::ExplorerLocalHg::new(
|
||||
theme.transfer_local_explorer_highlighted
|
||||
)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Theme(IdTheme::ExplorerRemoteBg),
|
||||
Box::new(components::ExplorerRemoteBg::new(
|
||||
theme.transfer_remote_explorer_background
|
||||
)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Theme(IdTheme::ExplorerRemoteFg),
|
||||
Box::new(components::ExplorerRemoteFg::new(
|
||||
theme.transfer_remote_explorer_foreground
|
||||
)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Theme(IdTheme::ExplorerRemoteHg),
|
||||
Box::new(components::ExplorerRemoteHg::new(
|
||||
theme.transfer_remote_explorer_highlighted
|
||||
)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Theme(IdTheme::ProgBarFull),
|
||||
Box::new(components::ProgBarFull::new(
|
||||
theme.transfer_progress_bar_full
|
||||
)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Theme(IdTheme::ProgBarPartial),
|
||||
Box::new(components::ProgBarPartial::new(
|
||||
theme.transfer_progress_bar_partial
|
||||
)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Theme(IdTheme::LogBg),
|
||||
Box::new(components::LogBg::new(theme.transfer_log_background)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Theme(IdTheme::LogWindow),
|
||||
Box::new(components::LogWindow::new(theme.transfer_log_window)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Theme(IdTheme::StatusSorting),
|
||||
Box::new(components::StatusSorting::new(
|
||||
theme.transfer_status_sorting
|
||||
)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Theme(IdTheme::StatusHidden),
|
||||
Box::new(components::StatusHidden::new(theme.transfer_status_hidden)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::Theme(IdTheme::StatusSync),
|
||||
Box::new(components::StatusSync::new(
|
||||
theme.transfer_status_sync_browsing
|
||||
)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,456 +0,0 @@
|
||||
//! ## Bookmark list
|
||||
//!
|
||||
//! `BookmarkList` component renders a bookmark list tab
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// ext
|
||||
use tui_realm_stdlib::utils::get_block;
|
||||
use tuirealm::event::{Event, KeyCode};
|
||||
use tuirealm::props::{Alignment, BlockTitle, BordersProps, Props, PropsBuilder};
|
||||
use tuirealm::tui::{
|
||||
layout::{Corner, Rect},
|
||||
style::{Color, Style},
|
||||
text::Span,
|
||||
widgets::{BorderType, Borders, List, ListItem, ListState},
|
||||
};
|
||||
use tuirealm::{Component, Frame, Msg, Payload, PropPayload, PropValue, Value};
|
||||
|
||||
// -- props
|
||||
const PROP_BOOKMARKS: &str = "bookmarks";
|
||||
|
||||
pub struct BookmarkListPropsBuilder {
|
||||
props: Option<Props>,
|
||||
}
|
||||
|
||||
impl Default for BookmarkListPropsBuilder {
|
||||
fn default() -> Self {
|
||||
BookmarkListPropsBuilder {
|
||||
props: Some(Props::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PropsBuilder for BookmarkListPropsBuilder {
|
||||
fn build(&mut self) -> Props {
|
||||
self.props.take().unwrap()
|
||||
}
|
||||
|
||||
fn hidden(&mut self) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.visible = false;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn visible(&mut self) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.visible = true;
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Props> for BookmarkListPropsBuilder {
|
||||
fn from(props: Props) -> Self {
|
||||
BookmarkListPropsBuilder { props: Some(props) }
|
||||
}
|
||||
}
|
||||
|
||||
impl BookmarkListPropsBuilder {
|
||||
/// ### with_foreground
|
||||
///
|
||||
/// Set foreground color for area
|
||||
pub fn with_foreground(&mut self, color: Color) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.foreground = color;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// ### with_background
|
||||
///
|
||||
/// Set background color for area
|
||||
pub fn with_background(&mut self, color: Color) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.background = color;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// ### with_borders
|
||||
///
|
||||
/// Set component borders style
|
||||
pub fn with_borders(
|
||||
&mut self,
|
||||
borders: Borders,
|
||||
variant: BorderType,
|
||||
color: Color,
|
||||
) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.borders = BordersProps {
|
||||
borders,
|
||||
variant,
|
||||
color,
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_title<S: AsRef<str>>(&mut self, text: S, alignment: Alignment) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.title = Some(BlockTitle::new(text, alignment));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_bookmarks(&mut self, bookmarks: Vec<String>) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
let bookmarks: Vec<PropValue> = bookmarks.into_iter().map(PropValue::Str).collect();
|
||||
props
|
||||
.own
|
||||
.insert(PROP_BOOKMARKS, PropPayload::Vec(bookmarks));
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// -- states
|
||||
|
||||
/// ## OwnStates
|
||||
///
|
||||
/// OwnStates contains states for this component
|
||||
#[derive(Clone)]
|
||||
struct OwnStates {
|
||||
list_index: usize, // Index of selected element in list
|
||||
list_len: usize, // Length of file list
|
||||
focus: bool, // Has focus?
|
||||
}
|
||||
|
||||
impl Default for OwnStates {
|
||||
fn default() -> Self {
|
||||
OwnStates {
|
||||
list_index: 0,
|
||||
list_len: 0,
|
||||
focus: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OwnStates {
|
||||
/// ### set_list_len
|
||||
///
|
||||
/// Set list length
|
||||
pub fn set_list_len(&mut self, len: usize) {
|
||||
self.list_len = len;
|
||||
}
|
||||
|
||||
/// ### get_list_index
|
||||
///
|
||||
/// Return current value for list index
|
||||
pub fn get_list_index(&self) -> usize {
|
||||
self.list_index
|
||||
}
|
||||
|
||||
/// ### incr_list_index
|
||||
///
|
||||
/// Incremenet list index
|
||||
pub fn incr_list_index(&mut self) {
|
||||
// Check if index is at last element
|
||||
if self.list_index + 1 < self.list_len {
|
||||
self.list_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// ### decr_list_index
|
||||
///
|
||||
/// Decrement list index
|
||||
pub fn decr_list_index(&mut self) {
|
||||
// Check if index is bigger than 0
|
||||
if self.list_index > 0 {
|
||||
self.list_index -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// ### reset_list_index
|
||||
///
|
||||
/// Reset list index to 0
|
||||
pub fn reset_list_index(&mut self) {
|
||||
self.list_index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// -- Component
|
||||
|
||||
/// ## BookmarkList
|
||||
///
|
||||
/// Bookmark list component
|
||||
pub struct BookmarkList {
|
||||
props: Props,
|
||||
states: OwnStates,
|
||||
}
|
||||
|
||||
impl BookmarkList {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiates a new FileList starting from Props
|
||||
/// The method also initializes the component states.
|
||||
pub fn new(props: Props) -> Self {
|
||||
// Initialize states
|
||||
let mut states: OwnStates = OwnStates::default();
|
||||
// Set list length
|
||||
states.set_list_len(Self::bookmarks_len(&props));
|
||||
BookmarkList { props, states }
|
||||
}
|
||||
|
||||
fn bookmarks_len(props: &Props) -> usize {
|
||||
match props.own.get(PROP_BOOKMARKS) {
|
||||
None => 0,
|
||||
Some(bookmarks) => bookmarks.unwrap_vec().len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for BookmarkList {
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
fn render(&self, render: &mut Frame, area: Rect) {
|
||||
if self.props.visible {
|
||||
// Make list
|
||||
let list_item: Vec<ListItem> = match self.props.own.get(PROP_BOOKMARKS) {
|
||||
Some(PropPayload::Vec(lines)) => lines
|
||||
.iter()
|
||||
.map(|x| x.unwrap_str())
|
||||
.map(|x| ListItem::new(Span::from(x.to_string())))
|
||||
.collect(),
|
||||
_ => vec![],
|
||||
};
|
||||
let (fg, bg): (Color, Color) = match self.states.focus {
|
||||
true => (self.props.foreground, self.props.background),
|
||||
false => (Color::Reset, Color::Reset),
|
||||
};
|
||||
// Render
|
||||
let mut state: ListState = ListState::default();
|
||||
state.select(Some(self.states.list_index));
|
||||
render.render_stateful_widget(
|
||||
List::new(list_item)
|
||||
.block(get_block(
|
||||
&self.props.borders,
|
||||
self.props.title.as_ref(),
|
||||
self.states.focus,
|
||||
))
|
||||
.start_corner(Corner::TopLeft)
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.bg(bg)
|
||||
.fg(fg)
|
||||
.add_modifier(self.props.modifiers),
|
||||
),
|
||||
area,
|
||||
&mut state,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, props: Props) -> Msg {
|
||||
self.props = props;
|
||||
// re-Set list length
|
||||
self.states.set_list_len(Self::bookmarks_len(&self.props));
|
||||
// Reset list index
|
||||
self.states.reset_list_index();
|
||||
Msg::None
|
||||
}
|
||||
|
||||
fn get_props(&self) -> Props {
|
||||
self.props.clone()
|
||||
}
|
||||
|
||||
fn on(&mut self, ev: Event) -> Msg {
|
||||
// Match event
|
||||
if let Event::Key(key) = ev {
|
||||
match key.code {
|
||||
KeyCode::Down => {
|
||||
// Update states
|
||||
self.states.incr_list_index();
|
||||
Msg::None
|
||||
}
|
||||
KeyCode::Up => {
|
||||
// Update states
|
||||
self.states.decr_list_index();
|
||||
Msg::None
|
||||
}
|
||||
KeyCode::PageDown => {
|
||||
// Update states
|
||||
for _ in 0..8 {
|
||||
self.states.incr_list_index();
|
||||
}
|
||||
Msg::None
|
||||
}
|
||||
KeyCode::PageUp => {
|
||||
// Update states
|
||||
for _ in 0..8 {
|
||||
self.states.decr_list_index();
|
||||
}
|
||||
Msg::None
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
// Report event
|
||||
Msg::OnSubmit(self.get_state())
|
||||
}
|
||||
_ => {
|
||||
// Return key event to activity
|
||||
Msg::OnKey(key)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Unhandled event
|
||||
Msg::None
|
||||
}
|
||||
}
|
||||
|
||||
fn get_state(&self) -> Payload {
|
||||
Payload::One(Value::Usize(self.states.get_list_index()))
|
||||
}
|
||||
|
||||
fn blur(&mut self) {
|
||||
self.states.focus = false;
|
||||
}
|
||||
|
||||
fn active(&mut self) {
|
||||
self.states.focus = true;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
use tuirealm::event::KeyEvent;
|
||||
|
||||
#[test]
|
||||
fn test_ui_components_bookmarks_list() {
|
||||
// Make component
|
||||
let mut component: BookmarkList = BookmarkList::new(
|
||||
BookmarkListPropsBuilder::default()
|
||||
.hidden()
|
||||
.visible()
|
||||
.with_foreground(Color::Red)
|
||||
.with_background(Color::Blue)
|
||||
.with_borders(Borders::ALL, BorderType::Double, Color::Red)
|
||||
.with_title("filelist", Alignment::Left)
|
||||
.with_bookmarks(vec![String::from("file1"), String::from("file2")])
|
||||
.build(),
|
||||
);
|
||||
assert_eq!(component.props.foreground, Color::Red);
|
||||
assert_eq!(component.props.background, Color::Blue);
|
||||
assert_eq!(component.props.visible, true);
|
||||
assert_eq!(component.props.title.as_ref().unwrap().text(), "filelist");
|
||||
assert_eq!(
|
||||
component
|
||||
.props
|
||||
.own
|
||||
.get(PROP_BOOKMARKS)
|
||||
.unwrap()
|
||||
.unwrap_vec()
|
||||
.len(),
|
||||
2
|
||||
);
|
||||
// Verify states
|
||||
assert_eq!(component.states.list_index, 0);
|
||||
assert_eq!(component.states.list_len, 2);
|
||||
assert_eq!(component.states.focus, false);
|
||||
// Focus
|
||||
component.active();
|
||||
assert_eq!(component.states.focus, true);
|
||||
component.blur();
|
||||
assert_eq!(component.states.focus, false);
|
||||
// Update
|
||||
let props = BookmarkListPropsBuilder::from(component.get_props())
|
||||
.with_foreground(Color::Yellow)
|
||||
.hidden()
|
||||
.build();
|
||||
assert_eq!(component.update(props), Msg::None);
|
||||
assert_eq!(component.props.foreground, Color::Yellow);
|
||||
assert_eq!(component.props.visible, false);
|
||||
// Increment list index
|
||||
component.states.list_index += 1;
|
||||
assert_eq!(component.states.list_index, 1);
|
||||
// Update
|
||||
component.update(
|
||||
BookmarkListPropsBuilder::from(component.get_props())
|
||||
.with_bookmarks(vec![
|
||||
String::from("file1"),
|
||||
String::from("file2"),
|
||||
String::from("file3"),
|
||||
])
|
||||
.build(),
|
||||
);
|
||||
// Verify states
|
||||
assert_eq!(component.states.list_index, 0);
|
||||
assert_eq!(component.states.list_len, 3);
|
||||
// get value
|
||||
assert_eq!(component.get_state(), Payload::One(Value::Usize(0)));
|
||||
// Render
|
||||
assert_eq!(component.states.list_index, 0);
|
||||
// Handle inputs
|
||||
assert_eq!(
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::Down))),
|
||||
Msg::None
|
||||
);
|
||||
// Index should be incremented
|
||||
assert_eq!(component.states.list_index, 1);
|
||||
// Index should be decremented
|
||||
assert_eq!(
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::Up))),
|
||||
Msg::None
|
||||
);
|
||||
// Index should be incremented
|
||||
assert_eq!(component.states.list_index, 0);
|
||||
// Index should be 2
|
||||
assert_eq!(
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::PageDown))),
|
||||
Msg::None
|
||||
);
|
||||
// Index should be incremented
|
||||
assert_eq!(component.states.list_index, 2);
|
||||
// Index should be 0
|
||||
assert_eq!(
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::PageUp))),
|
||||
Msg::None
|
||||
);
|
||||
// Index should be incremented
|
||||
assert_eq!(component.states.list_index, 0);
|
||||
// Enter
|
||||
assert_eq!(
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::Enter))),
|
||||
Msg::OnSubmit(Payload::One(Value::Usize(0)))
|
||||
);
|
||||
// On key
|
||||
assert_eq!(
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::Backspace))),
|
||||
Msg::OnKey(KeyEvent::from(KeyCode::Backspace))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,310 +0,0 @@
|
||||
//! ## Bytes
|
||||
//!
|
||||
//! `Bytes` component extends an `Input` component in order to provide an input type for byte size.
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// locals
|
||||
use crate::utils::fmt::fmt_bytes;
|
||||
use crate::utils::parser::parse_bytesize;
|
||||
// ext
|
||||
use tui_realm_stdlib::{Input, InputPropsBuilder};
|
||||
use tuirealm::event::Event;
|
||||
use tuirealm::props::{Alignment, Props, PropsBuilder};
|
||||
use tuirealm::tui::{
|
||||
layout::Rect,
|
||||
style::Color,
|
||||
widgets::{BorderType, Borders},
|
||||
};
|
||||
use tuirealm::{Component, Frame, Msg, Payload, Value};
|
||||
|
||||
// -- props
|
||||
|
||||
/// ## BytesPropsBuilder
|
||||
///
|
||||
/// A wrapper around an `InputPropsBuilder`
|
||||
pub struct BytesPropsBuilder {
|
||||
puppet: InputPropsBuilder,
|
||||
}
|
||||
|
||||
impl Default for BytesPropsBuilder {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
puppet: InputPropsBuilder::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PropsBuilder for BytesPropsBuilder {
|
||||
fn build(&mut self) -> Props {
|
||||
self.puppet.build()
|
||||
}
|
||||
|
||||
fn hidden(&mut self) -> &mut Self {
|
||||
self.puppet.hidden();
|
||||
self
|
||||
}
|
||||
|
||||
fn visible(&mut self) -> &mut Self {
|
||||
self.puppet.visible();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Props> for BytesPropsBuilder {
|
||||
fn from(props: Props) -> Self {
|
||||
BytesPropsBuilder {
|
||||
puppet: InputPropsBuilder::from(props),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BytesPropsBuilder {
|
||||
/// ### with_borders
|
||||
///
|
||||
/// Set component borders style
|
||||
pub fn with_borders(
|
||||
&mut self,
|
||||
borders: Borders,
|
||||
variant: BorderType,
|
||||
color: Color,
|
||||
) -> &mut Self {
|
||||
self.puppet.with_borders(borders, variant, color);
|
||||
self
|
||||
}
|
||||
|
||||
/// ### with_label
|
||||
///
|
||||
/// Set input label
|
||||
pub fn with_label<S: AsRef<str>>(&mut self, label: S, alignment: Alignment) -> &mut Self {
|
||||
self.puppet.with_label(label, alignment);
|
||||
self
|
||||
}
|
||||
|
||||
/// ### with_color
|
||||
///
|
||||
/// Set initial value for component
|
||||
pub fn with_foreground(&mut self, color: Color) -> &mut Self {
|
||||
self.puppet.with_foreground(color);
|
||||
self
|
||||
}
|
||||
|
||||
/// ### with_color
|
||||
///
|
||||
/// Set initial value for component
|
||||
pub fn with_value(&mut self, val: u64) -> &mut Self {
|
||||
self.puppet.with_value(fmt_bytes(val));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// -- component
|
||||
|
||||
/// ## Bytes
|
||||
///
|
||||
/// a wrapper component of `Input` which adds a superset of rules to behave as a color picker
|
||||
pub struct Bytes {
|
||||
input: Input,
|
||||
native_color: Color,
|
||||
}
|
||||
|
||||
impl Bytes {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiate a new `Bytes`
|
||||
pub fn new(props: Props) -> Self {
|
||||
// Instantiate a new color picker using input
|
||||
Self {
|
||||
native_color: props.foreground,
|
||||
input: Input::new(props),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### update_colors
|
||||
///
|
||||
/// Update colors to match selected color, with provided one
|
||||
fn update_colors(&mut self, color: Color) {
|
||||
let mut props = self.get_props();
|
||||
props.foreground = color;
|
||||
props.borders.color = color;
|
||||
let _ = self.input.update(props);
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Bytes {
|
||||
/// ### render
|
||||
///
|
||||
/// Based on the current properties and states, renders a widget using the provided render engine in the provided Area
|
||||
/// If focused, cursor is also set (if supported by widget)
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
fn render(&self, render: &mut Frame, area: Rect) {
|
||||
self.input.render(render, area);
|
||||
}
|
||||
|
||||
/// ### update
|
||||
///
|
||||
/// Update component properties
|
||||
/// Properties should first be retrieved through `get_props` which creates a builder from
|
||||
/// existing properties and then edited before calling update.
|
||||
/// Returns a Msg to the view
|
||||
fn update(&mut self, props: Props) -> Msg {
|
||||
let msg: Msg = self.input.update(props);
|
||||
match msg {
|
||||
Msg::OnChange(Payload::One(Value::Str(input))) => {
|
||||
match parse_bytesize(input.as_str()) {
|
||||
Some(bytes) => {
|
||||
// return OK
|
||||
self.update_colors(self.native_color);
|
||||
Msg::OnChange(Payload::One(Value::U64(bytes.as_u64())))
|
||||
}
|
||||
None => {
|
||||
// Invalid color
|
||||
self.update_colors(Color::Red);
|
||||
Msg::None
|
||||
}
|
||||
}
|
||||
}
|
||||
msg => msg,
|
||||
}
|
||||
}
|
||||
|
||||
/// ### get_props
|
||||
///
|
||||
/// Returns a props builder starting from component properties.
|
||||
/// This returns a prop builder in order to make easier to create
|
||||
/// new properties for the element.
|
||||
fn get_props(&self) -> Props {
|
||||
self.input.get_props()
|
||||
}
|
||||
|
||||
/// ### on
|
||||
///
|
||||
/// Handle input event and update internal states.
|
||||
/// Returns a Msg to the view
|
||||
fn on(&mut self, ev: Event) -> Msg {
|
||||
// Capture message from input
|
||||
match self.input.on(ev) {
|
||||
Msg::OnChange(Payload::One(Value::Str(input))) => {
|
||||
// Capture color and validate
|
||||
match parse_bytesize(input.as_str()) {
|
||||
Some(bytes) => {
|
||||
// Update color and return OK
|
||||
self.update_colors(self.native_color);
|
||||
Msg::OnChange(Payload::One(Value::U64(bytes.as_u64())))
|
||||
}
|
||||
None => {
|
||||
// Invalid color
|
||||
self.update_colors(Color::Red);
|
||||
Msg::None
|
||||
}
|
||||
}
|
||||
}
|
||||
Msg::OnSubmit(_) => Msg::None,
|
||||
msg => msg,
|
||||
}
|
||||
}
|
||||
|
||||
/// ### get_state
|
||||
///
|
||||
/// Get current state from component
|
||||
/// For this component returns Unsigned if the input type is a number, otherwise a text
|
||||
/// The value is always the current input.
|
||||
fn get_state(&self) -> Payload {
|
||||
match self.input.get_state() {
|
||||
Payload::One(Value::Str(bytes)) => match parse_bytesize(bytes.as_str()) {
|
||||
None => Payload::None,
|
||||
Some(bytes) => Payload::One(Value::U64(bytes.as_u64())),
|
||||
},
|
||||
_ => Payload::None,
|
||||
}
|
||||
}
|
||||
|
||||
// -- events
|
||||
|
||||
/// ### blur
|
||||
///
|
||||
/// Blur component; basically remove focus
|
||||
fn blur(&mut self) {
|
||||
self.input.blur();
|
||||
}
|
||||
|
||||
/// ### active
|
||||
///
|
||||
/// Active component; basically give focus
|
||||
fn active(&mut self) {
|
||||
self.input.active();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn bytes_input() {
|
||||
let mut component: Bytes = Bytes::new(
|
||||
BytesPropsBuilder::default()
|
||||
.visible()
|
||||
.with_value(1024)
|
||||
.with_borders(Borders::ALL, BorderType::Double, Color::Rgb(204, 170, 0))
|
||||
.with_label("omar", Alignment::Left)
|
||||
.with_foreground(Color::Red)
|
||||
.build(),
|
||||
);
|
||||
// Focus
|
||||
component.blur();
|
||||
component.active();
|
||||
// Get value
|
||||
assert_eq!(component.get_state(), Payload::One(Value::U64(1024)));
|
||||
// Set an invalid color
|
||||
let props = InputPropsBuilder::from(component.get_props())
|
||||
.with_value(String::from("#pippo1"))
|
||||
.hidden()
|
||||
.build();
|
||||
assert_eq!(component.update(props), Msg::None);
|
||||
assert_eq!(component.get_state(), Payload::None);
|
||||
// Reset color
|
||||
let props = BytesPropsBuilder::from(component.get_props())
|
||||
.with_value(111)
|
||||
.hidden()
|
||||
.build();
|
||||
assert_eq!(
|
||||
component.update(props),
|
||||
Msg::OnChange(Payload::One(Value::U64(111)))
|
||||
);
|
||||
// Backspace (invalid)
|
||||
assert_eq!(
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::Backspace))),
|
||||
Msg::None
|
||||
);
|
||||
// Press '1'
|
||||
assert_eq!(
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::Char('B')))),
|
||||
Msg::OnChange(Payload::One(Value::U64(111)))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,301 +0,0 @@
|
||||
//! ## ColorPicker
|
||||
//!
|
||||
//! `ColorPicker` component extends an `Input` component in order to provide some extra features
|
||||
//! for the color picker.
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// locals
|
||||
use crate::utils::fmt::fmt_color;
|
||||
use crate::utils::parser::parse_color;
|
||||
// ext
|
||||
use tui_realm_stdlib::{Input, InputPropsBuilder};
|
||||
use tuirealm::event::Event;
|
||||
use tuirealm::props::{Alignment, Props, PropsBuilder};
|
||||
use tuirealm::tui::{
|
||||
layout::Rect,
|
||||
style::Color,
|
||||
widgets::{BorderType, Borders},
|
||||
};
|
||||
use tuirealm::{Component, Frame, Msg, Payload, Value};
|
||||
|
||||
// -- props
|
||||
|
||||
/// ## ColorPickerPropsBuilder
|
||||
///
|
||||
/// A wrapper around an `InputPropsBuilder`
|
||||
pub struct ColorPickerPropsBuilder {
|
||||
puppet: InputPropsBuilder,
|
||||
}
|
||||
|
||||
impl Default for ColorPickerPropsBuilder {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
puppet: InputPropsBuilder::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PropsBuilder for ColorPickerPropsBuilder {
|
||||
fn build(&mut self) -> Props {
|
||||
self.puppet.build()
|
||||
}
|
||||
|
||||
fn hidden(&mut self) -> &mut Self {
|
||||
self.puppet.hidden();
|
||||
self
|
||||
}
|
||||
|
||||
fn visible(&mut self) -> &mut Self {
|
||||
self.puppet.visible();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Props> for ColorPickerPropsBuilder {
|
||||
fn from(props: Props) -> Self {
|
||||
ColorPickerPropsBuilder {
|
||||
puppet: InputPropsBuilder::from(props),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorPickerPropsBuilder {
|
||||
/// ### with_borders
|
||||
///
|
||||
/// Set component borders style
|
||||
pub fn with_borders(
|
||||
&mut self,
|
||||
borders: Borders,
|
||||
variant: BorderType,
|
||||
color: Color,
|
||||
) -> &mut Self {
|
||||
self.puppet.with_borders(borders, variant, color);
|
||||
self
|
||||
}
|
||||
|
||||
/// ### with_label
|
||||
///
|
||||
/// Set input label
|
||||
pub fn with_label<S: AsRef<str>>(&mut self, label: S, alignment: Alignment) -> &mut Self {
|
||||
self.puppet.with_label(label, alignment);
|
||||
self
|
||||
}
|
||||
|
||||
/// ### with_color
|
||||
///
|
||||
/// Set initial value for component
|
||||
pub fn with_color(&mut self, color: &Color) -> &mut Self {
|
||||
self.puppet.with_value(fmt_color(color));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// -- component
|
||||
|
||||
/// ## ColorPicker
|
||||
///
|
||||
/// a wrapper component of `Input` which adds a superset of rules to behave as a color picker
|
||||
pub struct ColorPicker {
|
||||
input: Input,
|
||||
}
|
||||
|
||||
impl ColorPicker {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiate a new `ColorPicker`
|
||||
pub fn new(props: Props) -> Self {
|
||||
// Instantiate a new color picker using input
|
||||
Self {
|
||||
input: Input::new(props),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### update_colors
|
||||
///
|
||||
/// Update colors to match selected color, with provided one
|
||||
fn update_colors(&mut self, color: Color) {
|
||||
let mut props = self.get_props();
|
||||
props.foreground = color;
|
||||
props.borders.color = color;
|
||||
let _ = self.input.update(props);
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for ColorPicker {
|
||||
/// ### render
|
||||
///
|
||||
/// Based on the current properties and states, renders a widget using the provided render engine in the provided Area
|
||||
/// If focused, cursor is also set (if supported by widget)
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
fn render(&self, render: &mut Frame, area: Rect) {
|
||||
self.input.render(render, area);
|
||||
}
|
||||
|
||||
/// ### update
|
||||
///
|
||||
/// Update component properties
|
||||
/// Properties should first be retrieved through `get_props` which creates a builder from
|
||||
/// existing properties and then edited before calling update.
|
||||
/// Returns a Msg to the view
|
||||
fn update(&mut self, props: Props) -> Msg {
|
||||
let msg: Msg = self.input.update(props);
|
||||
match msg {
|
||||
Msg::OnChange(Payload::One(Value::Str(input))) => match parse_color(input.as_str()) {
|
||||
Some(color) => {
|
||||
// Update color and return OK
|
||||
self.update_colors(color);
|
||||
Msg::OnChange(Payload::One(Value::Str(input)))
|
||||
}
|
||||
None => {
|
||||
// Invalid color
|
||||
self.update_colors(Color::Red);
|
||||
Msg::None
|
||||
}
|
||||
},
|
||||
msg => msg,
|
||||
}
|
||||
}
|
||||
|
||||
/// ### get_props
|
||||
///
|
||||
/// Returns a props builder starting from component properties.
|
||||
/// This returns a prop builder in order to make easier to create
|
||||
/// new properties for the element.
|
||||
fn get_props(&self) -> Props {
|
||||
self.input.get_props()
|
||||
}
|
||||
|
||||
/// ### on
|
||||
///
|
||||
/// Handle input event and update internal states.
|
||||
/// Returns a Msg to the view
|
||||
fn on(&mut self, ev: Event) -> Msg {
|
||||
// Capture message from input
|
||||
match self.input.on(ev) {
|
||||
Msg::OnChange(Payload::One(Value::Str(input))) => {
|
||||
// Capture color and validate
|
||||
match parse_color(input.as_str()) {
|
||||
Some(color) => {
|
||||
// Update color and return OK
|
||||
self.update_colors(color);
|
||||
Msg::OnChange(Payload::One(Value::Str(input)))
|
||||
}
|
||||
None => {
|
||||
// Invalid color
|
||||
self.update_colors(Color::Red);
|
||||
Msg::None
|
||||
}
|
||||
}
|
||||
}
|
||||
Msg::OnSubmit(_) => Msg::None,
|
||||
msg => msg,
|
||||
}
|
||||
}
|
||||
|
||||
/// ### get_state
|
||||
///
|
||||
/// Get current state from component
|
||||
/// For this component returns Unsigned if the input type is a number, otherwise a text
|
||||
/// The value is always the current input.
|
||||
fn get_state(&self) -> Payload {
|
||||
match self.input.get_state() {
|
||||
Payload::One(Value::Str(color)) => match parse_color(color.as_str()) {
|
||||
None => Payload::None,
|
||||
Some(_) => Payload::One(Value::Str(color)),
|
||||
},
|
||||
_ => Payload::None,
|
||||
}
|
||||
}
|
||||
|
||||
// -- events
|
||||
|
||||
/// ### blur
|
||||
///
|
||||
/// Blur component; basically remove focus
|
||||
fn blur(&mut self) {
|
||||
self.input.blur();
|
||||
}
|
||||
|
||||
/// ### active
|
||||
///
|
||||
/// Active component; basically give focus
|
||||
fn active(&mut self) {
|
||||
self.input.active();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_ui_components_color_picker() {
|
||||
let mut component: ColorPicker = ColorPicker::new(
|
||||
ColorPickerPropsBuilder::default()
|
||||
.visible()
|
||||
.with_color(&Color::Rgb(204, 170, 0))
|
||||
.with_borders(Borders::ALL, BorderType::Double, Color::Rgb(204, 170, 0))
|
||||
.with_label("omar", Alignment::Left)
|
||||
.build(),
|
||||
);
|
||||
// Focus
|
||||
component.blur();
|
||||
component.active();
|
||||
// Get value
|
||||
assert_eq!(
|
||||
component.get_state(),
|
||||
Payload::One(Value::Str(String::from("#ccaa00")))
|
||||
);
|
||||
// Set an invalid color
|
||||
let props = InputPropsBuilder::from(component.get_props())
|
||||
.with_value(String::from("#pippo1"))
|
||||
.hidden()
|
||||
.build();
|
||||
assert_eq!(component.update(props), Msg::None);
|
||||
assert_eq!(component.get_state(), Payload::None);
|
||||
// Reset color
|
||||
let props = ColorPickerPropsBuilder::from(component.get_props())
|
||||
.with_color(&Color::Rgb(204, 170, 0))
|
||||
.hidden()
|
||||
.build();
|
||||
assert_eq!(
|
||||
component.update(props),
|
||||
Msg::OnChange(Payload::One(Value::Str("#ccaa00".to_string())))
|
||||
);
|
||||
// Backspace (invalid)
|
||||
assert_eq!(
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::Backspace))),
|
||||
Msg::None
|
||||
);
|
||||
// Press '1'
|
||||
assert_eq!(
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::Char('1')))),
|
||||
Msg::OnChange(Payload::One(Value::Str(String::from("#ccaa01"))))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,765 +0,0 @@
|
||||
//! ## FileList
|
||||
//!
|
||||
//! `FileList` component renders a file list tab
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// ext
|
||||
use tui_realm_stdlib::utils::get_block;
|
||||
use tuirealm::event::{Event, KeyCode, KeyModifiers};
|
||||
use tuirealm::props::{
|
||||
Alignment, BlockTitle, BordersProps, PropPayload, PropValue, Props, PropsBuilder,
|
||||
};
|
||||
use tuirealm::tui::{
|
||||
layout::{Corner, Rect},
|
||||
style::{Color, Style},
|
||||
text::Span,
|
||||
widgets::{BorderType, Borders, List, ListItem, ListState},
|
||||
};
|
||||
use tuirealm::{Component, Frame, Msg, Payload, Value};
|
||||
|
||||
// -- props
|
||||
|
||||
const PROP_FILES: &str = "files";
|
||||
const PALETTE_HIGHLIGHT_COLOR: &str = "props-highlight-color";
|
||||
|
||||
pub struct FileListPropsBuilder {
|
||||
props: Option<Props>,
|
||||
}
|
||||
|
||||
impl Default for FileListPropsBuilder {
|
||||
fn default() -> Self {
|
||||
FileListPropsBuilder {
|
||||
props: Some(Props::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PropsBuilder for FileListPropsBuilder {
|
||||
fn build(&mut self) -> Props {
|
||||
self.props.take().unwrap()
|
||||
}
|
||||
|
||||
fn hidden(&mut self) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.visible = false;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn visible(&mut self) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.visible = true;
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Props> for FileListPropsBuilder {
|
||||
fn from(props: Props) -> Self {
|
||||
FileListPropsBuilder { props: Some(props) }
|
||||
}
|
||||
}
|
||||
|
||||
impl FileListPropsBuilder {
|
||||
/// ### with_foreground
|
||||
///
|
||||
/// Set foreground color for area
|
||||
pub fn with_foreground(&mut self, color: Color) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.foreground = color;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// ### with_background
|
||||
///
|
||||
/// Set background color for area
|
||||
pub fn with_background(&mut self, color: Color) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.background = color;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// ### with_highlight_color
|
||||
///
|
||||
/// Set highlighted color
|
||||
pub fn with_highlight_color(&mut self, color: Color) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.palette.insert(PALETTE_HIGHLIGHT_COLOR, color);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// ### with_borders
|
||||
///
|
||||
/// Set component borders style
|
||||
pub fn with_borders(
|
||||
&mut self,
|
||||
borders: Borders,
|
||||
variant: BorderType,
|
||||
color: Color,
|
||||
) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.borders = BordersProps {
|
||||
borders,
|
||||
variant,
|
||||
color,
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_title<S: AsRef<str>>(&mut self, text: S, alignment: Alignment) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.title = Some(BlockTitle::new(text, alignment));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_files(&mut self, files: Vec<String>) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
let files: Vec<PropValue> = files.into_iter().map(PropValue::Str).collect();
|
||||
props.own.insert(PROP_FILES, PropPayload::Vec(files));
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// -- states
|
||||
|
||||
/// ## OwnStates
|
||||
///
|
||||
/// OwnStates contains states for this component
|
||||
#[derive(Clone)]
|
||||
struct OwnStates {
|
||||
list_index: usize, // Index of selected element in list
|
||||
selected: Vec<usize>, // Selected files
|
||||
focus: bool, // Has focus?
|
||||
}
|
||||
|
||||
impl Default for OwnStates {
|
||||
fn default() -> Self {
|
||||
OwnStates {
|
||||
list_index: 0,
|
||||
selected: Vec::new(),
|
||||
focus: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OwnStates {
|
||||
/// ### init_list_states
|
||||
///
|
||||
/// Initialize list states
|
||||
pub fn init_list_states(&mut self, len: usize) {
|
||||
self.selected = Vec::with_capacity(len);
|
||||
self.fix_list_index();
|
||||
}
|
||||
|
||||
/// ### list_index
|
||||
///
|
||||
/// Return current value for list index
|
||||
pub fn list_index(&self) -> usize {
|
||||
self.list_index
|
||||
}
|
||||
|
||||
/// ### incr_list_index
|
||||
///
|
||||
/// Incremenet list index.
|
||||
/// If `can_rewind` is `true` the index rewinds when boundary is reached
|
||||
pub fn incr_list_index(&mut self, can_rewind: bool) {
|
||||
// Check if index is at last element
|
||||
if self.list_index + 1 < self.list_len() {
|
||||
self.list_index += 1;
|
||||
} else if can_rewind {
|
||||
self.list_index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// ### decr_list_index
|
||||
///
|
||||
/// Decrement list index
|
||||
/// If `can_rewind` is `true` the index rewinds when boundary is reached
|
||||
pub fn decr_list_index(&mut self, can_rewind: bool) {
|
||||
// Check if index is bigger than 0
|
||||
if self.list_index > 0 {
|
||||
self.list_index -= 1;
|
||||
} else if self.list_len() > 0 && can_rewind {
|
||||
self.list_index = self.list_len() - 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// ### list_len
|
||||
///
|
||||
/// Returns the length of the file list, which is actually the capacity of the selection vector
|
||||
pub fn list_len(&self) -> usize {
|
||||
self.selected.capacity()
|
||||
}
|
||||
|
||||
/// ### is_selected
|
||||
///
|
||||
/// Returns whether the file with index `entry` is selected
|
||||
pub fn is_selected(&self, entry: usize) -> bool {
|
||||
self.selected.contains(&entry)
|
||||
}
|
||||
|
||||
/// ### is_selection_empty
|
||||
///
|
||||
/// Returns whether the selection is currently empty
|
||||
pub fn is_selection_empty(&self) -> bool {
|
||||
self.selected.is_empty()
|
||||
}
|
||||
|
||||
/// ### get_selection
|
||||
///
|
||||
/// Returns current file selection
|
||||
pub fn get_selection(&self) -> Vec<usize> {
|
||||
self.selected.clone()
|
||||
}
|
||||
|
||||
/// ### fix_list_index
|
||||
///
|
||||
/// Keep index if possible, otherwise set to lenght - 1
|
||||
fn fix_list_index(&mut self) {
|
||||
if self.list_index >= self.list_len() && self.list_len() > 0 {
|
||||
self.list_index = self.list_len() - 1;
|
||||
} else if self.list_len() == 0 {
|
||||
self.list_index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// -- select manipulation
|
||||
|
||||
/// ### toggle_file
|
||||
///
|
||||
/// Select or deselect file with provided entry index
|
||||
pub fn toggle_file(&mut self, entry: usize) {
|
||||
match self.is_selected(entry) {
|
||||
true => self.deselect(entry),
|
||||
false => self.select(entry),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### select_all
|
||||
///
|
||||
/// Select all files
|
||||
pub fn select_all(&mut self) {
|
||||
for i in 0..self.list_len() {
|
||||
self.select(i);
|
||||
}
|
||||
}
|
||||
|
||||
/// ### select
|
||||
///
|
||||
/// Select provided index if not selected yet
|
||||
fn select(&mut self, entry: usize) {
|
||||
if !self.is_selected(entry) {
|
||||
self.selected.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
/// ### deselect
|
||||
///
|
||||
/// Remove element file with associated index
|
||||
fn deselect(&mut self, entry: usize) {
|
||||
if self.is_selected(entry) {
|
||||
self.selected.retain(|&x| x != entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- Component
|
||||
|
||||
/// ## FileList
|
||||
///
|
||||
/// File list component
|
||||
pub struct FileList {
|
||||
props: Props,
|
||||
states: OwnStates,
|
||||
}
|
||||
|
||||
impl FileList {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiates a new FileList starting from Props
|
||||
/// The method also initializes the component states.
|
||||
pub fn new(props: Props) -> Self {
|
||||
// Initialize states
|
||||
let mut states: OwnStates = OwnStates::default();
|
||||
// Init list states
|
||||
states.init_list_states(Self::files_len(&props));
|
||||
FileList { props, states }
|
||||
}
|
||||
|
||||
fn files_len(props: &Props) -> usize {
|
||||
match props.own.get(PROP_FILES) {
|
||||
None => 0,
|
||||
Some(files) => files.unwrap_vec().len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for FileList {
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
fn render(&self, render: &mut Frame, area: Rect) {
|
||||
if self.props.visible {
|
||||
// Make list
|
||||
let list_item: Vec<ListItem> = match self.props.own.get(PROP_FILES) {
|
||||
Some(PropPayload::Vec(lines)) => lines
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(num, line)| {
|
||||
let to_display: String = match self.states.is_selected(num) {
|
||||
true => format!("*{}", line.unwrap_str()),
|
||||
false => line.unwrap_str().to_string(),
|
||||
};
|
||||
ListItem::new(Span::from(to_display))
|
||||
})
|
||||
.collect(),
|
||||
_ => vec![],
|
||||
};
|
||||
let highlighted_color: Color = match self.props.palette.get(PALETTE_HIGHLIGHT_COLOR) {
|
||||
Some(c) => *c,
|
||||
_ => Color::Reset,
|
||||
};
|
||||
let (h_fg, h_bg): (Color, Color) = match self.states.focus {
|
||||
true => (Color::Black, highlighted_color),
|
||||
false => (highlighted_color, self.props.background),
|
||||
};
|
||||
// Render
|
||||
let mut state: ListState = ListState::default();
|
||||
state.select(Some(self.states.list_index));
|
||||
render.render_stateful_widget(
|
||||
List::new(list_item)
|
||||
.block(get_block(
|
||||
&self.props.borders,
|
||||
self.props.title.as_ref(),
|
||||
self.states.focus,
|
||||
))
|
||||
.start_corner(Corner::TopLeft)
|
||||
.style(
|
||||
Style::default()
|
||||
.fg(self.props.foreground)
|
||||
.bg(self.props.background),
|
||||
)
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.bg(h_bg)
|
||||
.fg(h_fg)
|
||||
.add_modifier(self.props.modifiers),
|
||||
),
|
||||
area,
|
||||
&mut state,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, props: Props) -> Msg {
|
||||
self.props = props;
|
||||
// re-Set list states
|
||||
self.states.init_list_states(Self::files_len(&self.props));
|
||||
Msg::None
|
||||
}
|
||||
|
||||
fn get_props(&self) -> Props {
|
||||
self.props.clone()
|
||||
}
|
||||
|
||||
fn on(&mut self, ev: Event) -> Msg {
|
||||
// Match event
|
||||
if let Event::Key(key) = ev {
|
||||
match key.code {
|
||||
KeyCode::Down => {
|
||||
// Update states
|
||||
self.states.incr_list_index(true);
|
||||
Msg::None
|
||||
}
|
||||
KeyCode::Up => {
|
||||
// Update states
|
||||
self.states.decr_list_index(true);
|
||||
Msg::None
|
||||
}
|
||||
KeyCode::PageDown => {
|
||||
// Update states
|
||||
for _ in 0..8 {
|
||||
self.states.incr_list_index(false);
|
||||
}
|
||||
Msg::None
|
||||
}
|
||||
KeyCode::PageUp => {
|
||||
// Update states
|
||||
for _ in 0..8 {
|
||||
self.states.decr_list_index(false);
|
||||
}
|
||||
Msg::None
|
||||
}
|
||||
KeyCode::Char('a') => match key.modifiers.intersects(KeyModifiers::CONTROL) {
|
||||
// CTRL+A
|
||||
true => {
|
||||
// Select all
|
||||
self.states.select_all();
|
||||
Msg::None
|
||||
}
|
||||
false => Msg::OnKey(key),
|
||||
},
|
||||
KeyCode::Char('m') => {
|
||||
// Toggle current file in selection
|
||||
self.states.toggle_file(self.states.list_index());
|
||||
Msg::None
|
||||
}
|
||||
KeyCode::Enter => Msg::OnSubmit(self.get_state()),
|
||||
_ => {
|
||||
// Return key event to activity
|
||||
Msg::OnKey(key)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Unhandled event
|
||||
Msg::None
|
||||
}
|
||||
}
|
||||
|
||||
/// ### get_state
|
||||
///
|
||||
/// Get state returns for this component two different payloads based on the states:
|
||||
/// - if the file selection is empty, returns the highlighted item as `One` of `Usize`
|
||||
/// - if at least one item is selected, return the selected as a `Vec` of `Usize`
|
||||
fn get_state(&self) -> Payload {
|
||||
match self.states.is_selection_empty() {
|
||||
true => Payload::One(Value::Usize(self.states.list_index())),
|
||||
false => Payload::Vec(
|
||||
self.states
|
||||
.get_selection()
|
||||
.into_iter()
|
||||
.map(Value::Usize)
|
||||
.collect(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// -- events
|
||||
|
||||
/// ### blur
|
||||
///
|
||||
/// Blur component; basically remove focus
|
||||
fn blur(&mut self) {
|
||||
self.states.focus = false;
|
||||
}
|
||||
|
||||
/// ### active
|
||||
///
|
||||
/// Active component; basically give focus
|
||||
fn active(&mut self) {
|
||||
self.states.focus = true;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
use tuirealm::event::{KeyEvent, KeyModifiers};
|
||||
|
||||
#[test]
|
||||
fn test_ui_components_file_list_states() {
|
||||
let mut states: OwnStates = OwnStates::default();
|
||||
assert_eq!(states.list_len(), 0);
|
||||
assert_eq!(states.selected.len(), 0);
|
||||
assert_eq!(states.focus, false);
|
||||
// Init states
|
||||
states.init_list_states(4);
|
||||
assert_eq!(states.list_len(), 4);
|
||||
assert_eq!(states.selected.len(), 0);
|
||||
assert!(states.is_selection_empty());
|
||||
// Select all files
|
||||
states.select_all();
|
||||
assert_eq!(states.list_len(), 4);
|
||||
assert_eq!(states.selected.len(), 4);
|
||||
assert_eq!(states.is_selection_empty(), false);
|
||||
assert_eq!(states.get_selection(), vec![0, 1, 2, 3]);
|
||||
// Verify reset
|
||||
states.init_list_states(5);
|
||||
assert_eq!(states.list_len(), 5);
|
||||
assert_eq!(states.selected.len(), 0);
|
||||
// Toggle file
|
||||
states.toggle_file(2);
|
||||
assert_eq!(states.list_len(), 5);
|
||||
assert_eq!(states.selected.len(), 1);
|
||||
assert_eq!(states.selected[0], 2);
|
||||
states.toggle_file(4);
|
||||
assert_eq!(states.list_len(), 5);
|
||||
assert_eq!(states.selected.len(), 2);
|
||||
assert_eq!(states.selected[1], 4);
|
||||
states.toggle_file(2);
|
||||
assert_eq!(states.list_len(), 5);
|
||||
assert_eq!(states.selected.len(), 1);
|
||||
assert_eq!(states.selected[0], 4);
|
||||
// Select twice (nothing should change)
|
||||
states.select(4);
|
||||
assert_eq!(states.list_len(), 5);
|
||||
assert_eq!(states.selected.len(), 1);
|
||||
assert_eq!(states.selected[0], 4);
|
||||
// Deselect not-selectd item
|
||||
states.deselect(2);
|
||||
assert_eq!(states.list_len(), 5);
|
||||
assert_eq!(states.selected.len(), 1);
|
||||
assert_eq!(states.selected[0], 4);
|
||||
// Index
|
||||
states.init_list_states(2);
|
||||
// Incr
|
||||
states.incr_list_index(false);
|
||||
assert_eq!(states.list_index(), 1);
|
||||
states.incr_list_index(false);
|
||||
assert_eq!(states.list_index(), 1);
|
||||
states.incr_list_index(true);
|
||||
assert_eq!(states.list_index(), 0);
|
||||
// Decr
|
||||
states.list_index = 1;
|
||||
states.decr_list_index(false);
|
||||
assert_eq!(states.list_index(), 0);
|
||||
states.decr_list_index(false);
|
||||
assert_eq!(states.list_index(), 0);
|
||||
states.decr_list_index(true);
|
||||
assert_eq!(states.list_index(), 1);
|
||||
// Try fixing index
|
||||
states.init_list_states(5);
|
||||
states.list_index = 4;
|
||||
states.init_list_states(3);
|
||||
assert_eq!(states.list_index(), 2);
|
||||
states.init_list_states(6);
|
||||
assert_eq!(states.list_index(), 2);
|
||||
// Focus
|
||||
states.focus = true;
|
||||
assert_eq!(states.focus, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ui_components_file_list() {
|
||||
// Make component
|
||||
let mut component: FileList = FileList::new(
|
||||
FileListPropsBuilder::default()
|
||||
.hidden()
|
||||
.visible()
|
||||
.with_foreground(Color::Red)
|
||||
.with_background(Color::Blue)
|
||||
.with_highlight_color(Color::LightRed)
|
||||
.with_borders(Borders::ALL, BorderType::Double, Color::Red)
|
||||
.with_title("files", Alignment::Left)
|
||||
.with_files(vec![String::from("file1"), String::from("file2")])
|
||||
.build(),
|
||||
);
|
||||
assert_eq!(
|
||||
*component
|
||||
.props
|
||||
.palette
|
||||
.get(PALETTE_HIGHLIGHT_COLOR)
|
||||
.unwrap(),
|
||||
Color::LightRed
|
||||
);
|
||||
assert_eq!(component.props.foreground, Color::Red);
|
||||
assert_eq!(component.props.background, Color::Blue);
|
||||
assert_eq!(component.props.visible, true);
|
||||
assert_eq!(component.props.title.as_ref().unwrap().text(), "files");
|
||||
assert_eq!(
|
||||
component
|
||||
.props
|
||||
.own
|
||||
.get(PROP_FILES)
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.unwrap_vec()
|
||||
.len(),
|
||||
2
|
||||
);
|
||||
// Verify states
|
||||
assert_eq!(component.states.list_index, 0);
|
||||
assert_eq!(component.states.selected.len(), 0);
|
||||
assert_eq!(component.states.list_len(), 2);
|
||||
assert_eq!(component.states.selected.capacity(), 2);
|
||||
assert_eq!(component.states.focus, false);
|
||||
// Focus
|
||||
component.active();
|
||||
assert_eq!(component.states.focus, true);
|
||||
component.blur();
|
||||
assert_eq!(component.states.focus, false);
|
||||
// Update
|
||||
let props = FileListPropsBuilder::from(component.get_props())
|
||||
.with_foreground(Color::Yellow)
|
||||
.hidden()
|
||||
.build();
|
||||
assert_eq!(component.update(props), Msg::None);
|
||||
assert_eq!(component.props.visible, false);
|
||||
assert_eq!(component.props.foreground, Color::Yellow);
|
||||
// Increment list index
|
||||
component.states.list_index += 1;
|
||||
assert_eq!(component.states.list_index, 1);
|
||||
// Update
|
||||
component.update(
|
||||
FileListPropsBuilder::from(component.get_props())
|
||||
.with_files(vec![
|
||||
String::from("file1"),
|
||||
String::from("file2"),
|
||||
String::from("file3"),
|
||||
])
|
||||
.build(),
|
||||
);
|
||||
// Verify states
|
||||
assert_eq!(component.states.list_index, 1); // Kept
|
||||
assert_eq!(component.states.list_len(), 3);
|
||||
// get value
|
||||
assert_eq!(component.get_state(), Payload::One(Value::Usize(1)));
|
||||
// Render
|
||||
assert_eq!(component.states.list_index, 1);
|
||||
// Handle inputs
|
||||
assert_eq!(
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::Down))),
|
||||
Msg::None
|
||||
);
|
||||
// Index should be incremented
|
||||
assert_eq!(component.states.list_index, 2);
|
||||
// Index should be decremented
|
||||
assert_eq!(
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::Up))),
|
||||
Msg::None
|
||||
);
|
||||
// Index should be incremented
|
||||
assert_eq!(component.states.list_index, 1);
|
||||
// Index should be 2
|
||||
assert_eq!(
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::PageDown))),
|
||||
Msg::None
|
||||
);
|
||||
// Index should be incremented
|
||||
assert_eq!(component.states.list_index, 2);
|
||||
// Index should be 0
|
||||
assert_eq!(
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::PageUp))),
|
||||
Msg::None
|
||||
);
|
||||
// Index should be incremented
|
||||
assert_eq!(component.states.list_index, 0);
|
||||
// Enter
|
||||
assert_eq!(
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::Enter))),
|
||||
Msg::OnSubmit(Payload::One(Value::Usize(0)))
|
||||
);
|
||||
// On key
|
||||
assert_eq!(
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::Backspace))),
|
||||
Msg::OnKey(KeyEvent::from(KeyCode::Backspace))
|
||||
);
|
||||
// Verify 'A' still works
|
||||
assert_eq!(
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::Char('a')))),
|
||||
Msg::OnKey(KeyEvent::from(KeyCode::Char('a')))
|
||||
);
|
||||
// Ctrl + a
|
||||
assert_eq!(
|
||||
component.on(Event::Key(KeyEvent::new(
|
||||
KeyCode::Char('a'),
|
||||
KeyModifiers::CONTROL
|
||||
))),
|
||||
Msg::None
|
||||
);
|
||||
assert_eq!(component.states.selected.len(), component.states.list_len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ui_components_file_list_selection() {
|
||||
// Make component
|
||||
let mut component: FileList = FileList::new(
|
||||
FileListPropsBuilder::default()
|
||||
.with_files(vec![
|
||||
String::from("file1"),
|
||||
String::from("file2"),
|
||||
String::from("file3"),
|
||||
])
|
||||
.build(),
|
||||
);
|
||||
// Get state
|
||||
assert_eq!(component.get_state(), Payload::One(Value::Usize(0)));
|
||||
// Select one
|
||||
assert_eq!(
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::Char('m')))),
|
||||
Msg::None
|
||||
);
|
||||
// Now should be a vec
|
||||
assert_eq!(component.get_state(), Payload::Vec(vec![Value::Usize(0)]));
|
||||
// De-select
|
||||
assert_eq!(
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::Char('m')))),
|
||||
Msg::None
|
||||
);
|
||||
assert_eq!(component.get_state(), Payload::One(Value::Usize(0)));
|
||||
// Go down
|
||||
assert_eq!(
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::Down))),
|
||||
Msg::None
|
||||
);
|
||||
// Select
|
||||
assert_eq!(
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::Char('m')))),
|
||||
Msg::None
|
||||
);
|
||||
assert_eq!(component.get_state(), Payload::Vec(vec![Value::Usize(1)]));
|
||||
// Go down and select
|
||||
assert_eq!(
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::Down))),
|
||||
Msg::None
|
||||
);
|
||||
assert_eq!(
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::Char('m')))),
|
||||
Msg::None
|
||||
);
|
||||
assert_eq!(
|
||||
component.get_state(),
|
||||
Payload::Vec(vec![Value::Usize(1), Value::Usize(2)])
|
||||
);
|
||||
// Select all
|
||||
assert_eq!(
|
||||
component.on(Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('a'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
})),
|
||||
Msg::None
|
||||
);
|
||||
// All selected
|
||||
assert_eq!(
|
||||
component.get_state(),
|
||||
Payload::Vec(vec![Value::Usize(1), Value::Usize(2), Value::Usize(0)])
|
||||
);
|
||||
// Update files
|
||||
component.update(
|
||||
FileListPropsBuilder::from(component.get_props())
|
||||
.with_files(vec![String::from("file1"), String::from("file2")])
|
||||
.build(),
|
||||
);
|
||||
// Selection should now be empty
|
||||
assert_eq!(component.get_state(), Payload::One(Value::Usize(1)));
|
||||
}
|
||||
}
|
||||
@@ -1,433 +0,0 @@
|
||||
//! ## LogBox
|
||||
//!
|
||||
//! `LogBox` component renders a log box view
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// ext
|
||||
use tui_realm_stdlib::utils::{get_block, wrap_spans};
|
||||
use tuirealm::event::{Event, KeyCode};
|
||||
use tuirealm::props::{
|
||||
Alignment, BlockTitle, BordersProps, Props, PropsBuilder, Table as TextTable,
|
||||
};
|
||||
use tuirealm::tui::{
|
||||
layout::{Corner, Rect},
|
||||
style::{Color, Style},
|
||||
widgets::{BorderType, Borders, List, ListItem, ListState},
|
||||
};
|
||||
use tuirealm::{Component, Frame, Msg, Payload, PropPayload, PropValue, Value};
|
||||
|
||||
// -- props
|
||||
|
||||
const PROP_TABLE: &str = "table";
|
||||
|
||||
pub struct LogboxPropsBuilder {
|
||||
props: Option<Props>,
|
||||
}
|
||||
|
||||
impl Default for LogboxPropsBuilder {
|
||||
fn default() -> Self {
|
||||
LogboxPropsBuilder {
|
||||
props: Some(Props::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PropsBuilder for LogboxPropsBuilder {
|
||||
fn build(&mut self) -> Props {
|
||||
self.props.take().unwrap()
|
||||
}
|
||||
|
||||
fn hidden(&mut self) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.visible = false;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn visible(&mut self) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.visible = true;
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Props> for LogboxPropsBuilder {
|
||||
fn from(props: Props) -> Self {
|
||||
LogboxPropsBuilder { props: Some(props) }
|
||||
}
|
||||
}
|
||||
|
||||
impl LogboxPropsBuilder {
|
||||
/// ### with_borders
|
||||
///
|
||||
/// Set component borders style
|
||||
pub fn with_borders(
|
||||
&mut self,
|
||||
borders: Borders,
|
||||
variant: BorderType,
|
||||
color: Color,
|
||||
) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.borders = BordersProps {
|
||||
borders,
|
||||
variant,
|
||||
color,
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// ### with_background
|
||||
///
|
||||
/// Set background color for area
|
||||
pub fn with_background(&mut self, color: Color) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.background = color;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_title<S: AsRef<str>>(&mut self, text: S, alignment: Alignment) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.title = Some(BlockTitle::new(text, alignment));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_log(&mut self, table: TextTable) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props
|
||||
.own
|
||||
.insert(PROP_TABLE, PropPayload::One(PropValue::Table(table)));
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// -- states
|
||||
|
||||
/// ## OwnStates
|
||||
///
|
||||
/// OwnStates contains states for this component
|
||||
#[derive(Clone)]
|
||||
struct OwnStates {
|
||||
list_index: usize, // Index of selected element in list
|
||||
list_len: usize, // Length of file list
|
||||
focus: bool, // Has focus?
|
||||
}
|
||||
|
||||
impl Default for OwnStates {
|
||||
fn default() -> Self {
|
||||
OwnStates {
|
||||
list_index: 0,
|
||||
list_len: 0,
|
||||
focus: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OwnStates {
|
||||
/// ### set_list_len
|
||||
///
|
||||
/// Set list length
|
||||
pub fn set_list_len(&mut self, len: usize) {
|
||||
self.list_len = len;
|
||||
}
|
||||
|
||||
/// ### get_list_index
|
||||
///
|
||||
/// Return current value for list index
|
||||
pub fn get_list_index(&self) -> usize {
|
||||
self.list_index
|
||||
}
|
||||
|
||||
/// ### incr_list_index
|
||||
///
|
||||
/// Incremenet list index
|
||||
pub fn incr_list_index(&mut self) {
|
||||
// Check if index is at last element
|
||||
if self.list_index + 1 < self.list_len {
|
||||
self.list_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// ### decr_list_index
|
||||
///
|
||||
/// Decrement list index
|
||||
pub fn decr_list_index(&mut self) {
|
||||
// Check if index is bigger than 0
|
||||
if self.list_index > 0 {
|
||||
self.list_index -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// ### reset_list_index
|
||||
///
|
||||
/// Reset list index to last element
|
||||
pub fn reset_list_index(&mut self) {
|
||||
self.list_index = 0; // Last element is always 0
|
||||
}
|
||||
}
|
||||
|
||||
// -- Component
|
||||
|
||||
/// ## LogBox
|
||||
///
|
||||
/// LogBox list component
|
||||
pub struct LogBox {
|
||||
props: Props,
|
||||
states: OwnStates,
|
||||
}
|
||||
|
||||
impl LogBox {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiates a new FileList starting from Props
|
||||
/// The method also initializes the component states.
|
||||
pub fn new(props: Props) -> Self {
|
||||
// Initialize states
|
||||
let mut states: OwnStates = OwnStates::default();
|
||||
// Set list length
|
||||
states.set_list_len(Self::table_len(&props));
|
||||
// Reset list index
|
||||
states.reset_list_index();
|
||||
LogBox { props, states }
|
||||
}
|
||||
|
||||
fn table_len(props: &Props) -> usize {
|
||||
match props.own.get(PROP_TABLE) {
|
||||
Some(PropPayload::One(PropValue::Table(table))) => table.len(),
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for LogBox {
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
fn render(&self, render: &mut Frame, area: Rect) {
|
||||
if self.props.visible {
|
||||
let width: usize = area.width as usize - 4;
|
||||
// Make list
|
||||
let list_items: Vec<ListItem> = match self.props.own.get(PROP_TABLE) {
|
||||
Some(PropPayload::One(PropValue::Table(table))) => table
|
||||
.iter()
|
||||
.map(|row| ListItem::new(wrap_spans(row, width, &self.props)))
|
||||
.collect(), // Make List item from TextSpan
|
||||
_ => Vec::new(),
|
||||
};
|
||||
let w = List::new(list_items)
|
||||
.block(get_block(
|
||||
&self.props.borders,
|
||||
self.props.title.as_ref(),
|
||||
self.states.focus,
|
||||
))
|
||||
.start_corner(Corner::BottomLeft)
|
||||
.highlight_symbol(">> ")
|
||||
.style(Style::default().bg(self.props.background))
|
||||
.highlight_style(Style::default().add_modifier(self.props.modifiers));
|
||||
let mut state: ListState = ListState::default();
|
||||
state.select(Some(self.states.list_index));
|
||||
render.render_stateful_widget(w, area, &mut state);
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, props: Props) -> Msg {
|
||||
self.props = props;
|
||||
// re-Set list length
|
||||
self.states.set_list_len(Self::table_len(&self.props));
|
||||
// Reset list index
|
||||
self.states.reset_list_index();
|
||||
Msg::None
|
||||
}
|
||||
|
||||
fn get_props(&self) -> Props {
|
||||
self.props.clone()
|
||||
}
|
||||
|
||||
fn on(&mut self, ev: Event) -> Msg {
|
||||
// Match event
|
||||
if let Event::Key(key) = ev {
|
||||
match key.code {
|
||||
KeyCode::Up => {
|
||||
// Update states
|
||||
self.states.incr_list_index();
|
||||
Msg::None
|
||||
}
|
||||
KeyCode::Down => {
|
||||
// Update states
|
||||
self.states.decr_list_index();
|
||||
Msg::None
|
||||
}
|
||||
KeyCode::PageUp => {
|
||||
// Update states
|
||||
for _ in 0..8 {
|
||||
self.states.incr_list_index();
|
||||
}
|
||||
Msg::None
|
||||
}
|
||||
KeyCode::PageDown => {
|
||||
// Update states
|
||||
for _ in 0..8 {
|
||||
self.states.decr_list_index();
|
||||
}
|
||||
Msg::None
|
||||
}
|
||||
_ => {
|
||||
// Return key event to activity
|
||||
Msg::OnKey(key)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Unhandled event
|
||||
Msg::None
|
||||
}
|
||||
}
|
||||
|
||||
fn get_state(&self) -> Payload {
|
||||
Payload::One(Value::Usize(self.states.get_list_index()))
|
||||
}
|
||||
|
||||
fn blur(&mut self) {
|
||||
self.states.focus = false;
|
||||
}
|
||||
|
||||
fn active(&mut self) {
|
||||
self.states.focus = true;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
use tuirealm::event::{KeyCode, KeyEvent};
|
||||
use tuirealm::props::{TableBuilder, TextSpan};
|
||||
use tuirealm::tui::style::Color;
|
||||
|
||||
#[test]
|
||||
fn test_ui_components_logbox() {
|
||||
let mut component: LogBox = LogBox::new(
|
||||
LogboxPropsBuilder::default()
|
||||
.hidden()
|
||||
.visible()
|
||||
.with_borders(Borders::ALL, BorderType::Double, Color::Red)
|
||||
.with_background(Color::Blue)
|
||||
.with_title("Log", Alignment::Left)
|
||||
.with_log(
|
||||
TableBuilder::default()
|
||||
.add_col(TextSpan::from("12:29"))
|
||||
.add_col(TextSpan::from("system crashed"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::from("12:38"))
|
||||
.add_col(TextSpan::from("system alive"))
|
||||
.build(),
|
||||
)
|
||||
.build(),
|
||||
);
|
||||
assert_eq!(component.props.visible, true);
|
||||
assert_eq!(component.props.background, Color::Blue);
|
||||
assert_eq!(component.props.title.as_ref().unwrap().text(), "Log");
|
||||
// Verify states
|
||||
assert_eq!(component.states.list_index, 0);
|
||||
assert_eq!(component.states.list_len, 2);
|
||||
assert_eq!(component.states.focus, false);
|
||||
// Focus
|
||||
component.active();
|
||||
assert_eq!(component.states.focus, true);
|
||||
component.blur();
|
||||
assert_eq!(component.states.focus, false);
|
||||
// Update
|
||||
let props = LogboxPropsBuilder::from(component.get_props())
|
||||
.hidden()
|
||||
.build();
|
||||
assert_eq!(component.update(props), Msg::None);
|
||||
assert_eq!(component.props.visible, false);
|
||||
// Increment list index
|
||||
component.states.list_index += 1;
|
||||
assert_eq!(component.states.list_index, 1);
|
||||
// Update
|
||||
component.update(
|
||||
LogboxPropsBuilder::from(component.get_props())
|
||||
.with_log(
|
||||
TableBuilder::default()
|
||||
.add_col(TextSpan::from("12:29"))
|
||||
.add_col(TextSpan::from("system crashed"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::from("12:38"))
|
||||
.add_col(TextSpan::from("system alive"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::from("12:41"))
|
||||
.add_col(TextSpan::from("system is going down for REBOOT"))
|
||||
.build(),
|
||||
)
|
||||
.build(),
|
||||
);
|
||||
// Verify states
|
||||
assert_eq!(component.states.list_index, 0); // Last item
|
||||
assert_eq!(component.states.list_len, 3);
|
||||
// get value
|
||||
assert_eq!(component.get_state(), Payload::One(Value::Usize(0)));
|
||||
// RenderData
|
||||
assert_eq!(component.states.list_index, 0);
|
||||
// Set cursor to 0
|
||||
component.states.list_index = 0;
|
||||
// Handle inputs
|
||||
assert_eq!(
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::Up))),
|
||||
Msg::None
|
||||
);
|
||||
// Index should be incremented
|
||||
assert_eq!(component.states.list_index, 1);
|
||||
// Index should be decremented
|
||||
assert_eq!(
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::Down))),
|
||||
Msg::None
|
||||
);
|
||||
// Index should be incremented
|
||||
assert_eq!(component.states.list_index, 0);
|
||||
// Index should be 2
|
||||
assert_eq!(
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::PageUp))),
|
||||
Msg::None
|
||||
);
|
||||
// Index should be incremented
|
||||
assert_eq!(component.states.list_index, 2);
|
||||
// Index should be 0
|
||||
assert_eq!(
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::PageDown))),
|
||||
Msg::None
|
||||
);
|
||||
// Index should be incremented
|
||||
assert_eq!(component.states.list_index, 0);
|
||||
// On key
|
||||
assert_eq!(
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::Backspace))),
|
||||
Msg::OnKey(KeyEvent::from(KeyCode::Backspace))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -26,34 +26,21 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// Locals
|
||||
use super::input::InputHandler;
|
||||
use super::store::Store;
|
||||
use crate::filetransfer::FileTransferParams;
|
||||
use crate::system::config_client::ConfigClient;
|
||||
use crate::system::theme_provider::ThemeProvider;
|
||||
|
||||
// Includes
|
||||
#[cfg(target_family = "unix")]
|
||||
use crossterm::{
|
||||
event::DisableMouseCapture,
|
||||
execute,
|
||||
terminal::{EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::io::{stdout, Stdout};
|
||||
use tuirealm::tui::backend::CrosstermBackend;
|
||||
use tuirealm::tui::Terminal;
|
||||
|
||||
type TuiTerminal = Terminal<CrosstermBackend<Stdout>>;
|
||||
use tuirealm::terminal::TerminalBridge;
|
||||
|
||||
/// ## Context
|
||||
///
|
||||
/// Context holds data structures used by the ui
|
||||
/// Context holds data structures shared by the activities
|
||||
pub struct Context {
|
||||
ft_params: Option<FileTransferParams>,
|
||||
config_client: ConfigClient,
|
||||
pub(crate) store: Store,
|
||||
input_hnd: InputHandler,
|
||||
pub(crate) terminal: TuiTerminal,
|
||||
pub(crate) terminal: TerminalBridge,
|
||||
theme_provider: ThemeProvider,
|
||||
error: Option<String>,
|
||||
}
|
||||
@@ -71,8 +58,7 @@ impl Context {
|
||||
ft_params: None,
|
||||
config_client,
|
||||
store: Store::init(),
|
||||
input_hnd: InputHandler::new(),
|
||||
terminal: Terminal::new(CrosstermBackend::new(Self::stdout())).unwrap(),
|
||||
terminal: TerminalBridge::new().expect("Could not initialize terminal"),
|
||||
theme_provider,
|
||||
error,
|
||||
}
|
||||
@@ -92,10 +78,6 @@ impl Context {
|
||||
&mut self.config_client
|
||||
}
|
||||
|
||||
pub(crate) fn input_hnd(&self) -> &InputHandler {
|
||||
&self.input_hnd
|
||||
}
|
||||
|
||||
pub(crate) fn store(&self) -> &Store {
|
||||
&self.store
|
||||
}
|
||||
@@ -112,7 +94,7 @@ impl Context {
|
||||
&mut self.theme_provider
|
||||
}
|
||||
|
||||
pub fn terminal(&mut self) -> &mut TuiTerminal {
|
||||
pub fn terminal(&mut self) -> &mut TerminalBridge {
|
||||
&mut self.terminal
|
||||
}
|
||||
|
||||
@@ -137,75 +119,13 @@ impl Context {
|
||||
pub fn error(&mut self) -> Option<String> {
|
||||
self.error.take()
|
||||
}
|
||||
|
||||
/// ### enter_alternate_screen
|
||||
///
|
||||
/// Enter alternate screen (gui window)
|
||||
#[cfg(target_family = "unix")]
|
||||
pub fn enter_alternate_screen(&mut self) {
|
||||
match execute!(
|
||||
self.terminal.backend_mut(),
|
||||
EnterAlternateScreen,
|
||||
DisableMouseCapture
|
||||
) {
|
||||
Err(err) => error!("Failed to enter alternate screen: {}", err),
|
||||
Ok(_) => info!("Entered alternate screen"),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### enter_alternate_screen
|
||||
///
|
||||
/// Enter alternate screen (gui window)
|
||||
#[cfg(target_family = "windows")]
|
||||
pub fn enter_alternate_screen(&self) {}
|
||||
|
||||
/// ### leave_alternate_screen
|
||||
///
|
||||
/// Go back to normal screen (gui window)
|
||||
#[cfg(target_family = "unix")]
|
||||
pub fn leave_alternate_screen(&mut self) {
|
||||
match execute!(
|
||||
self.terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
) {
|
||||
Err(err) => error!("Failed to leave alternate screen: {}", err),
|
||||
Ok(_) => info!("Left alternate screen"),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### leave_alternate_screen
|
||||
///
|
||||
/// Go back to normal screen (gui window)
|
||||
#[cfg(target_family = "windows")]
|
||||
pub fn leave_alternate_screen(&self) {}
|
||||
|
||||
/// ### clear_screen
|
||||
///
|
||||
/// Clear terminal screen
|
||||
pub fn clear_screen(&mut self) {
|
||||
match self.terminal.clear() {
|
||||
Err(err) => error!("Failed to clear screen: {}", err),
|
||||
Ok(_) => info!("Cleared screen"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
fn stdout() -> Stdout {
|
||||
let mut stdout = stdout();
|
||||
assert!(execute!(stdout, EnterAlternateScreen).is_ok());
|
||||
stdout
|
||||
}
|
||||
|
||||
#[cfg(target_family = "windows")]
|
||||
fn stdout() -> Stdout {
|
||||
stdout()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Context {
|
||||
fn drop(&mut self) {
|
||||
// Re-enable terminal stuff
|
||||
self.leave_alternate_screen();
|
||||
let _ = self.terminal.disable_raw_mode();
|
||||
let _ = self.terminal.leave_alternate_screen();
|
||||
let _ = self.terminal.clear_screen();
|
||||
}
|
||||
}
|
||||
|
||||
102
src/ui/input.rs
102
src/ui/input.rs
@@ -1,102 +0,0 @@
|
||||
//! ## Input
|
||||
//!
|
||||
//! `input` is the module which provides all the functionalities related to input events in the user interface
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
use crossterm::event::{poll, read, Event};
|
||||
use std::time::Duration;
|
||||
|
||||
/// ## InputHandler
|
||||
///
|
||||
/// InputHandler is the struct which runs a thread which waits for
|
||||
/// input events from the user and reports them through a receiver
|
||||
pub(crate) struct InputHandler;
|
||||
|
||||
impl InputHandler {
|
||||
/// ### InputHandler
|
||||
///
|
||||
///
|
||||
pub(crate) fn new() -> InputHandler {
|
||||
InputHandler {}
|
||||
}
|
||||
|
||||
/// ### fetch_events
|
||||
///
|
||||
/// Check if new events have been received from handler
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn fetch_events(&self) -> Result<Vec<Event>, ()> {
|
||||
let mut inbox: Vec<Event> = Vec::new();
|
||||
loop {
|
||||
match self.read_event() {
|
||||
Ok(ev_opt) => match ev_opt {
|
||||
Some(ev) => inbox.push(ev),
|
||||
None => break,
|
||||
},
|
||||
Err(_) => return Err(()),
|
||||
}
|
||||
}
|
||||
Ok(inbox)
|
||||
}
|
||||
|
||||
/// ### read_event
|
||||
///
|
||||
/// Read event from input listener
|
||||
pub(crate) fn read_event(&self) -> Result<Option<Event>, ()> {
|
||||
if let Ok(available) = poll(Duration::from_millis(10)) {
|
||||
match available {
|
||||
true => {
|
||||
// Read event
|
||||
if let Ok(ev) = read() {
|
||||
Ok(Some(ev))
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
false => Ok(None),
|
||||
}
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_ui_input_new() {
|
||||
let _: InputHandler = InputHandler::new();
|
||||
}
|
||||
|
||||
/* ERRORS ON GITHUB ACTIONS
|
||||
#[test]
|
||||
fn test_ui_input_fetch() {
|
||||
let input_hnd: InputHandler = InputHandler::new();
|
||||
// Try recv
|
||||
assert_eq!(input_hnd.fetch_messages().ok().unwrap().len(), 0);
|
||||
}*/
|
||||
}
|
||||
215
src/ui/keymap.rs
215
src/ui/keymap.rs
@@ -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,
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user