Migrated termscp to tui-realm 1.x

This commit is contained in:
veeso
2022-01-06 10:44:34 +01:00
committed by Christian Visintin
parent 30851a78e8
commit 54b5583d1a
54 changed files with 10994 additions and 7691 deletions
+9 -51
View File
@@ -33,8 +33,6 @@ use crate::system::environment;
// Ext
use std::path::PathBuf;
use tui_realm_stdlib::{InputPropsBuilder, RadioPropsBuilder};
use tuirealm::PropsBuilder;
impl AuthActivity {
/// ### del_bookmark
@@ -234,12 +232,8 @@ impl AuthActivity {
/// Load bookmark data into the gui components
fn load_bookmark_into_gui(&mut self, bookmark: FileTransferParams) {
// Load parameters into components
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_PROTOCOL) {
let props = RadioPropsBuilder::from(props)
.with_value(Self::protocol_enum_to_opt(bookmark.protocol))
.build();
self.view.update(super::COMPONENT_RADIO_PROTOCOL, props);
}
self.protocol = bookmark.protocol;
self.mount_protocol(bookmark.protocol);
match bookmark.params {
ProtocolParams::AwsS3(params) => self.load_bookmark_s3_into_gui(params),
ProtocolParams::Generic(params) => self.load_bookmark_generic_into_gui(params),
@@ -247,51 +241,15 @@ impl AuthActivity {
}
fn load_bookmark_generic_into_gui(&mut self, params: GenericProtocolParams) {
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_ADDR) {
let props = InputPropsBuilder::from(props)
.with_value(params.address.clone())
.build();
self.view.update(super::COMPONENT_INPUT_ADDR, props);
}
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_PORT) {
let props = InputPropsBuilder::from(props)
.with_value(params.port.to_string())
.build();
self.view.update(super::COMPONENT_INPUT_PORT, props);
}
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_USERNAME) {
let props = InputPropsBuilder::from(props)
.with_value(params.username.as_deref().unwrap_or_default().to_string())
.build();
self.view.update(super::COMPONENT_INPUT_USERNAME, props);
}
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_PASSWORD) {
let props = InputPropsBuilder::from(props)
.with_value(params.password.as_deref().unwrap_or_default().to_string())
.build();
self.view.update(super::COMPONENT_INPUT_PASSWORD, props);
}
self.mount_address(params.address.as_str());
self.mount_port(params.port);
self.mount_username(params.username.as_deref().unwrap_or(""));
self.mount_password(params.password.as_deref().unwrap_or(""));
}
fn load_bookmark_s3_into_gui(&mut self, params: AwsS3Params) {
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_S3_BUCKET) {
let props = InputPropsBuilder::from(props)
.with_value(params.bucket_name.clone())
.build();
self.view.update(super::COMPONENT_INPUT_S3_BUCKET, props);
}
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_S3_REGION) {
let props = InputPropsBuilder::from(props)
.with_value(params.region.clone())
.build();
self.view.update(super::COMPONENT_INPUT_S3_REGION, props);
}
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_S3_PROFILE) {
let props = InputPropsBuilder::from(props)
.with_value(params.profile.as_deref().unwrap_or_default().to_string())
.build();
self.view.update(super::COMPONENT_INPUT_S3_PROFILE, props);
}
self.mount_s3_bucket(params.bucket_name.as_str());
self.mount_s3_region(params.region.as_str());
self.mount_s3_profile(params.profile.as_deref().unwrap_or(""));
}
}
@@ -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
View File
@@ -0,0 +1,694 @@
//! ## Form
//!
//! auth activity components for file transfer params form
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
use super::{FileTransferProtocol, Msg};
use tui_realm_stdlib::{Input, Radio};
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
// -- protocol
#[derive(MockComponent)]
pub struct ProtocolRadio {
component: Radio,
}
impl ProtocolRadio {
pub fn new(default_protocol: FileTransferProtocol, color: Color) -> Self {
Self {
component: Radio::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.choices(&["SFTP", "SCP", "FTP", "FTPS", "AWS S3"])
.foreground(color)
.rewind(true)
.title("Protocol", Alignment::Left)
.value(Self::protocol_enum_to_opt(default_protocol)),
}
}
/// ### protocol_opt_to_enum
///
/// Convert radio index for protocol into a `FileTransferProtocol`
fn protocol_opt_to_enum(protocol: usize) -> FileTransferProtocol {
match protocol {
1 => FileTransferProtocol::Scp,
2 => FileTransferProtocol::Ftp(false),
3 => FileTransferProtocol::Ftp(true),
4 => FileTransferProtocol::AwsS3,
_ => FileTransferProtocol::Sftp,
}
}
/// ### protocol_enum_to_opt
///
/// Convert `FileTransferProtocol` enum into radio group index
fn protocol_enum_to_opt(protocol: FileTransferProtocol) -> usize {
match protocol {
FileTransferProtocol::Sftp => 0,
FileTransferProtocol::Scp => 1,
FileTransferProtocol::Ftp(false) => 2,
FileTransferProtocol::Ftp(true) => 3,
FileTransferProtocol::AwsS3 => 4,
}
}
}
impl Component<Msg, NoUserEvent> for ProtocolRadio {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
let result = match ev {
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => self.perform(Cmd::Move(Direction::Left)),
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => self.perform(Cmd::Move(Direction::Right)),
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => return Some(Msg::Connect),
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => return Some(Msg::ProtocolBlurDown),
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => return Some(Msg::ProtocolBlurUp),
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => return Some(Msg::ParamsFormBlur),
_ => return None,
};
match result {
CmdResult::Changed(State::One(StateValue::Usize(choice))) => {
Some(Msg::ProtocolChanged(Self::protocol_opt_to_enum(choice)))
}
_ => Some(Msg::None),
}
}
}
// -- address
#[derive(MockComponent)]
pub struct InputAddress {
component: Input,
}
impl InputAddress {
pub fn new(host: &str, color: Color) -> Self {
Self {
component: Input::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.placeholder("127.0.0.1", Style::default().fg(Color::Rgb(128, 128, 128)))
.title("Remote host", Alignment::Left)
.input_type(InputType::Text)
.value(host),
}
}
}
impl Component<Msg, NoUserEvent> for InputAddress {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Delete, ..
}) => {
self.perform(Cmd::Cancel);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Backspace,
..
}) => {
self.perform(Cmd::Delete);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Char(ch),
modifiers: KeyModifiers::NONE,
}) => {
self.perform(Cmd::Type(ch));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => Some(Msg::Connect),
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => Some(Msg::AddressBlurDown),
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::AddressBlurUp),
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::ParamsFormBlur),
_ => None,
}
}
}
// -- port number
#[derive(MockComponent)]
pub struct InputPort {
component: Input,
}
impl InputPort {
pub fn new(port: u16, color: Color) -> Self {
Self {
component: Input::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.placeholder("22", Style::default().fg(Color::Rgb(128, 128, 128)))
.input_type(InputType::UnsignedInteger)
.input_len(5)
.title("Port number", Alignment::Left)
.value(port.to_string()),
}
}
}
impl Component<Msg, NoUserEvent> for InputPort {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Delete, ..
}) => {
self.perform(Cmd::Cancel);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Backspace,
..
}) => {
self.perform(Cmd::Delete);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Char(ch),
modifiers: KeyModifiers::NONE,
}) => {
self.perform(Cmd::Type(ch));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => Some(Msg::Connect),
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => Some(Msg::PortBlurDown),
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::PortBlurUp),
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::ParamsFormBlur),
_ => None,
}
}
}
// -- username
#[derive(MockComponent)]
pub struct InputUsername {
component: Input,
}
impl InputUsername {
pub fn new(username: &str, color: Color) -> Self {
Self {
component: Input::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.placeholder("root", Style::default().fg(Color::Rgb(128, 128, 128)))
.title("Username", Alignment::Left)
.input_type(InputType::Text)
.value(username),
}
}
}
impl Component<Msg, NoUserEvent> for InputUsername {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Delete, ..
}) => {
self.perform(Cmd::Cancel);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Backspace,
..
}) => {
self.perform(Cmd::Delete);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Char(ch),
modifiers: KeyModifiers::NONE,
}) => {
self.perform(Cmd::Type(ch));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => Some(Msg::Connect),
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => Some(Msg::UsernameBlurDown),
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::UsernameBlurUp),
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::ParamsFormBlur),
_ => None,
}
}
}
// -- password
#[derive(MockComponent)]
pub struct InputPassword {
component: Input,
}
impl InputPassword {
pub fn new(password: &str, color: Color) -> Self {
Self {
component: Input::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.title("Password", Alignment::Left)
.input_type(InputType::Password('*'))
.value(password),
}
}
}
impl Component<Msg, NoUserEvent> for InputPassword {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Delete, ..
}) => {
self.perform(Cmd::Cancel);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Backspace,
..
}) => {
self.perform(Cmd::Delete);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Char(ch),
modifiers: KeyModifiers::NONE,
}) => {
self.perform(Cmd::Type(ch));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => Some(Msg::Connect),
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => Some(Msg::PasswordBlurDown),
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::PasswordBlurUp),
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::ParamsFormBlur),
_ => None,
}
}
}
// -- s3 bucket
#[derive(MockComponent)]
pub struct InputS3Bucket {
component: Input,
}
impl InputS3Bucket {
pub fn new(bucket: &str, color: Color) -> Self {
Self {
component: Input::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.placeholder("my-bucket", Style::default().fg(Color::Rgb(128, 128, 128)))
.title("Bucket name", Alignment::Left)
.input_type(InputType::Text)
.value(bucket),
}
}
}
impl Component<Msg, NoUserEvent> for InputS3Bucket {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Delete, ..
}) => {
self.perform(Cmd::Cancel);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Backspace,
..
}) => {
self.perform(Cmd::Delete);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Char(ch),
modifiers: KeyModifiers::NONE,
}) => {
self.perform(Cmd::Type(ch));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => Some(Msg::Connect),
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => Some(Msg::S3BucketBlurDown),
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::S3BucketBlurUp),
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::ParamsFormBlur),
_ => None,
}
}
}
// -- s3 bucket
#[derive(MockComponent)]
pub struct InputS3Region {
component: Input,
}
impl InputS3Region {
pub fn new(region: &str, color: Color) -> Self {
Self {
component: Input::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.placeholder("eu-west-1", Style::default().fg(Color::Rgb(128, 128, 128)))
.title("Region", Alignment::Left)
.input_type(InputType::Text)
.value(region),
}
}
}
impl Component<Msg, NoUserEvent> for InputS3Region {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Delete, ..
}) => {
self.perform(Cmd::Cancel);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Backspace,
..
}) => {
self.perform(Cmd::Delete);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Char(ch),
modifiers: KeyModifiers::NONE,
}) => {
self.perform(Cmd::Type(ch));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => Some(Msg::Connect),
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => Some(Msg::S3RegionBlurDown),
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::S3RegionBlurUp),
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::ParamsFormBlur),
_ => None,
}
}
}
// -- s3 bucket
#[derive(MockComponent)]
pub struct InputS3Profile {
component: Input,
}
impl InputS3Profile {
pub fn new(profile: &str, color: Color) -> Self {
Self {
component: Input::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.placeholder("default", Style::default().fg(Color::Rgb(128, 128, 128)))
.title("Profile", Alignment::Left)
.input_type(InputType::Text)
.value(profile),
}
}
}
impl Component<Msg, NoUserEvent> for InputS3Profile {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Delete, ..
}) => {
self.perform(Cmd::Cancel);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Backspace,
..
}) => {
self.perform(Cmd::Delete);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Char(ch),
modifiers: KeyModifiers::NONE,
}) => {
self.perform(Cmd::Type(ch));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => Some(Msg::Connect),
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => Some(Msg::S3ProfileBlurDown),
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(Msg::S3ProfileBlurUp),
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::ParamsFormBlur),
_ => None,
}
}
}
+91
View File
@@ -0,0 +1,91 @@
//! ## Components
//!
//! auth activity components
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
use super::{FileTransferProtocol, Msg};
mod bookmarks;
mod form;
mod popup;
mod text;
pub use bookmarks::{
BookmarkName, BookmarkSavePassword, BookmarksList, DeleteBookmarkPopup, DeleteRecentPopup,
RecentsList,
};
pub use form::{
InputAddress, InputPassword, InputPort, InputS3Bucket, InputS3Profile, InputS3Region,
InputUsername, ProtocolRadio,
};
pub use popup::{
ErrorPopup, InfoPopup, InstallUpdatePopup, Keybindings, QuitPopup, ReleaseNotes, WaitPopup,
WindowSizeError,
};
pub use text::{HelpText, NewVersionDisclaimer, Subtitle, Title};
use tui_realm_stdlib::Phantom;
use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent};
use tuirealm::{Component, MockComponent};
// -- global listener
#[derive(MockComponent)]
pub struct GlobalListener {
component: Phantom,
}
impl Default for GlobalListener {
fn default() -> Self {
Self {
component: Phantom::default(),
}
}
}
impl Component<Msg, NoUserEvent> for GlobalListener {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Msg::ShowQuitPopup),
Event::Keyboard(KeyEvent {
code: Key::Char('c'),
modifiers: KeyModifiers::CONTROL,
}) => Some(Msg::EnterSetup),
Event::Keyboard(KeyEvent {
code: Key::Char('h'),
modifiers: KeyModifiers::CONTROL,
}) => Some(Msg::ShowKeybindingsPopup),
Event::Keyboard(KeyEvent {
code: Key::Char('r'),
modifiers: KeyModifiers::CONTROL,
}) => Some(Msg::ShowReleaseNotes),
Event::Keyboard(KeyEvent {
code: Key::Char('s'),
modifiers: KeyModifiers::CONTROL,
}) => Some(Msg::ShowSaveBookmarkPopup),
_ => None,
}
}
}
+452
View File
@@ -0,0 +1,452 @@
//! ## Popup
//!
//! auth activity popups
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
use super::Msg;
use tui_realm_stdlib::{List, Paragraph, Radio, Textarea};
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::event::{Key, KeyEvent};
use tuirealm::props::{Alignment, BorderType, Borders, Color, TableBuilder, TextSpan};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
// -- error popup
#[derive(MockComponent)]
pub struct ErrorPopup {
component: Paragraph,
}
impl ErrorPopup {
pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self {
Self {
component: Paragraph::default()
.alignment(Alignment::Center)
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.text(&[TextSpan::from(text.as_ref())])
.wrap(true),
}
}
}
impl Component<Msg, NoUserEvent> for ErrorPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Esc | Key::Enter,
..
}) => Some(Msg::CloseErrorPopup),
_ => None,
}
}
}
// -- info popup
#[derive(MockComponent)]
pub struct InfoPopup {
component: Paragraph,
}
impl InfoPopup {
pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self {
Self {
component: Paragraph::default()
.alignment(Alignment::Center)
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.text(&[TextSpan::from(text.as_ref())])
.wrap(true),
}
}
}
impl Component<Msg, NoUserEvent> for InfoPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Esc | Key::Enter,
..
}) => Some(Msg::CloseInfoPopup),
_ => None,
}
}
}
// -- wait popup
#[derive(MockComponent)]
pub struct WaitPopup {
component: Paragraph,
}
impl WaitPopup {
pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self {
Self {
component: Paragraph::default()
.alignment(Alignment::Center)
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.text(&[TextSpan::from(text.as_ref())])
.wrap(true),
}
}
}
impl Component<Msg, NoUserEvent> for WaitPopup {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
None
}
}
// -- window size error
#[derive(MockComponent)]
pub struct WindowSizeError {
component: Paragraph,
}
impl WindowSizeError {
pub fn new(color: Color) -> Self {
Self {
component: Paragraph::default()
.alignment(Alignment::Center)
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.text(&[TextSpan::from(
"termscp requires at least 24 lines of height to run",
)])
.wrap(true),
}
}
}
impl Component<Msg, NoUserEvent> for WindowSizeError {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
None
}
}
// -- quit popup
#[derive(MockComponent)]
pub struct QuitPopup {
component: Radio,
}
impl QuitPopup {
pub fn new(color: Color) -> Self {
Self {
component: Radio::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.title("Quit termscp?", Alignment::Center)
.rewind(true)
.choices(&["Yes", "No"]),
}
}
}
impl Component<Msg, NoUserEvent> for QuitPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Msg::CloseQuitPopup),
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => {
if matches!(
self.perform(Cmd::Submit),
CmdResult::Submit(State::One(StateValue::Usize(0)))
) {
Some(Msg::Quit)
} else {
Some(Msg::CloseQuitPopup)
}
}
_ => None,
}
}
}
// -- install update popup
#[derive(MockComponent)]
pub struct InstallUpdatePopup {
component: Radio,
}
impl InstallUpdatePopup {
pub fn new(color: Color) -> Self {
Self {
component: Radio::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.title("Install update?", Alignment::Center)
.rewind(true)
.choices(&["Yes", "No"]),
}
}
}
impl Component<Msg, NoUserEvent> for InstallUpdatePopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Msg::CloseInstallUpdatePopup),
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => {
if matches!(
self.perform(Cmd::Submit),
CmdResult::Submit(State::One(StateValue::Usize(0)))
) {
Some(Msg::InstallUpdate)
} else {
Some(Msg::CloseInstallUpdatePopup)
}
}
_ => None,
}
}
}
// -- release notes popup
#[derive(MockComponent)]
pub struct ReleaseNotes {
component: Textarea,
}
impl ReleaseNotes {
pub fn new(notes: &str, color: Color) -> Self {
Self {
component: Textarea::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.title("Release notes", Alignment::Center)
.text_rows(
notes
.lines()
.map(TextSpan::from)
.collect::<Vec<TextSpan>>()
.as_slice(),
),
}
}
}
impl Component<Msg, NoUserEvent> for ReleaseNotes {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Esc | Key::Enter,
..
}) => Some(Msg::CloseInstallUpdatePopup),
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => {
self.perform(Cmd::Move(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
self.perform(Cmd::Move(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::PageDown,
..
}) => {
self.perform(Cmd::Scroll(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::PageUp, ..
}) => {
self.perform(Cmd::Scroll(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
_ => None,
}
}
}
// -- keybindings popup
#[derive(MockComponent)]
pub struct Keybindings {
component: List,
}
impl Keybindings {
pub fn new(color: Color) -> Self {
Self {
component: List::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.highlighted_str("? ")
.title("Keybindings", Alignment::Center)
.scroll(true)
.step(4)
.rows(
TableBuilder::default()
.add_col(TextSpan::new("<ESC>").bold().fg(color))
.add_col(TextSpan::from(" Quit termscp"))
.add_row()
.add_col(TextSpan::new("<TAB>").bold().fg(color))
.add_col(TextSpan::from(" Switch from form and bookmarks"))
.add_row()
.add_col(TextSpan::new("<RIGHT/LEFT>").bold().fg(color))
.add_col(TextSpan::from(" Switch bookmark tab"))
.add_row()
.add_col(TextSpan::new("<UP/DOWN>").bold().fg(color))
.add_col(TextSpan::from(" Move up/down in current tab"))
.add_row()
.add_col(TextSpan::new("<ENTER>").bold().fg(color))
.add_col(TextSpan::from(" Connect/Load bookmark"))
.add_row()
.add_col(TextSpan::new("<DEL|E>").bold().fg(color))
.add_col(TextSpan::from(" Delete selected bookmark"))
.add_row()
.add_col(TextSpan::new("<CTRL+C>").bold().fg(color))
.add_col(TextSpan::from(" Enter setup"))
.add_row()
.add_col(TextSpan::new("<CTRL+S>").bold().fg(color))
.add_col(TextSpan::from(" Save bookmark"))
.build(),
),
}
}
}
impl Component<Msg, NoUserEvent> for Keybindings {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Esc | Key::Enter,
..
}) => Some(Msg::CloseKeybindingsPopup),
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => {
self.perform(Cmd::Move(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
self.perform(Cmd::Move(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::PageDown,
..
}) => {
self.perform(Cmd::Scroll(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::PageUp, ..
}) => {
self.perform(Cmd::Scroll(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
_ => None,
}
}
}
+132
View File
@@ -0,0 +1,132 @@
//! ## Text
//!
//! auth activity texts
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
use super::Msg;
use tui_realm_stdlib::{Label, Span};
use tuirealm::props::{Color, TextModifiers, TextSpan};
use tuirealm::{Component, Event, MockComponent, NoUserEvent};
// -- Title
#[derive(MockComponent)]
pub struct Title {
component: Label,
}
impl Default for Title {
fn default() -> Self {
Self {
component: Label::default()
.modifiers(TextModifiers::BOLD | TextModifiers::ITALIC)
.text("$ termscp"),
}
}
}
impl Component<Msg, NoUserEvent> for Title {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
None
}
}
// -- subtitle
#[derive(MockComponent)]
pub struct Subtitle {
component: Label,
}
impl Default for Subtitle {
fn default() -> Self {
Self {
component: Label::default()
.modifiers(TextModifiers::BOLD | TextModifiers::ITALIC)
.text(format!("$ version {}", env!("CARGO_PKG_VERSION"))),
}
}
}
impl Component<Msg, NoUserEvent> for Subtitle {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
None
}
}
// -- new version disclaimer
#[derive(MockComponent)]
pub struct NewVersionDisclaimer {
component: Span,
}
impl NewVersionDisclaimer {
pub fn new(new_version: &str, color: Color) -> Self {
Self {
component: Span::default().foreground(color).spans(&[
TextSpan::from("termscp "),
TextSpan::new(new_version).underlined().bold(),
TextSpan::from(
" is NOW available! Install update and view release notes with <CTRL+R>",
),
]),
}
}
}
impl Component<Msg, NoUserEvent> for NewVersionDisclaimer {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
None
}
}
// -- HelpText
#[derive(MockComponent)]
pub struct HelpText {
component: Span,
}
impl HelpText {
pub fn new(key_color: Color) -> Self {
Self {
component: Span::default().spans(&[
TextSpan::new("Press ").bold(),
TextSpan::new("<CTRL+H>").bold().fg(key_color),
TextSpan::new(" to show keybindings; ").bold(),
TextSpan::new("<CTRL+C>").bold().fg(key_color),
TextSpan::new(" to enter setup").bold(),
]),
}
}
}
impl Component<Msg, NoUserEvent> for HelpText {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
None
}
}
+4 -34
View File
@@ -31,32 +31,6 @@ use crate::system::auto_update::{Release, Update, UpdateStatus};
use crate::system::notifications::Notification;
impl AuthActivity {
/// ### protocol_opt_to_enum
///
/// Convert radio index for protocol into a `FileTransferProtocol`
pub(super) fn protocol_opt_to_enum(protocol: usize) -> FileTransferProtocol {
match protocol {
1 => FileTransferProtocol::Scp,
2 => FileTransferProtocol::Ftp(false),
3 => FileTransferProtocol::Ftp(true),
4 => FileTransferProtocol::AwsS3,
_ => FileTransferProtocol::Sftp,
}
}
/// ### protocol_enum_to_opt
///
/// Convert `FileTransferProtocol` enum into radio group index
pub(super) fn protocol_enum_to_opt(protocol: FileTransferProtocol) -> usize {
match protocol {
FileTransferProtocol::Sftp => 0,
FileTransferProtocol::Scp => 1,
FileTransferProtocol::Ftp(false) => 2,
FileTransferProtocol::Ftp(true) => 3,
FileTransferProtocol::AwsS3 => 4,
}
}
/// ### get_default_port_for_protocol
///
/// Get the default port for protocol
@@ -91,9 +65,8 @@ impl AuthActivity {
///
/// Collect host params as `FileTransferParams`
pub(super) fn collect_host_params(&self) -> Result<FileTransferParams, &'static str> {
let protocol: FileTransferProtocol = self.get_protocol();
match protocol {
FileTransferProtocol::AwsS3 => self.collect_s3_host_params(protocol),
match self.protocol {
FileTransferProtocol::AwsS3 => self.collect_s3_host_params(),
protocol => self.collect_generic_host_params(protocol),
}
}
@@ -135,10 +108,7 @@ impl AuthActivity {
/// ### collect_s3_host_params
///
/// Get input values from fields or return an error if fields are invalid to work as aws s3
pub(super) fn collect_s3_host_params(
&self,
protocol: FileTransferProtocol,
) -> Result<FileTransferParams, &'static str> {
pub(super) fn collect_s3_host_params(&self) -> Result<FileTransferParams, &'static str> {
let (bucket, region, profile): (String, String, Option<String>) =
self.get_s3_params_input();
if bucket.is_empty() {
@@ -148,7 +118,7 @@ impl AuthActivity {
return Err("Invalid region");
}
Ok(FileTransferParams {
protocol,
protocol: FileTransferProtocol::AwsS3,
params: ProtocolParams::AwsS3(AwsS3Params::new(bucket, region, profile)),
entry_directory: None,
})
+140 -81
View File
@@ -27,6 +27,7 @@
*/
// Sub modules
mod bookmarks;
mod components;
mod misc;
mod update;
mod view;
@@ -39,37 +40,101 @@ use crate::system::bookmarks_client::BookmarksClient;
use crate::system::config_client::ConfigClient;
// Includes
use crossterm::event::Event;
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use tuirealm::{Update, View};
use std::time::Duration;
use tuirealm::listener::EventListenerCfg;
use tuirealm::{application::PollStrategy, Application, NoUserEvent, Update};
// -- components
const COMPONENT_TEXT_H1: &str = "TEXT_H1";
const COMPONENT_TEXT_H2: &str = "TEXT_H2";
const COMPONENT_TEXT_NEW_VERSION: &str = "TEXT_NEW_VERSION";
const COMPONENT_TEXT_NEW_VERSION_NOTES: &str = "TEXTAREA_NEW_VERSION";
const COMPONENT_TEXT_FOOTER: &str = "TEXT_FOOTER";
const COMPONENT_TEXT_HELP: &str = "TEXT_HELP";
const COMPONENT_TEXT_ERROR: &str = "TEXT_ERROR";
const COMPONENT_TEXT_INFO: &str = "TEXT_INFO";
const COMPONENT_TEXT_WAIT: &str = "TEXT_WAIT";
const COMPONENT_TEXT_SIZE_ERR: &str = "TEXT_SIZE_ERR";
const COMPONENT_INPUT_ADDR: &str = "INPUT_ADDRESS";
const COMPONENT_INPUT_PORT: &str = "INPUT_PORT";
const COMPONENT_INPUT_USERNAME: &str = "INPUT_USERNAME";
const COMPONENT_INPUT_PASSWORD: &str = "INPUT_PASSWORD";
const COMPONENT_INPUT_BOOKMARK_NAME: &str = "INPUT_BOOKMARK_NAME";
const COMPONENT_INPUT_S3_BUCKET: &str = "INPUT_S3_BUCKET";
const COMPONENT_INPUT_S3_REGION: &str = "INPUT_S3_REGION";
const COMPONENT_INPUT_S3_PROFILE: &str = "INPUT_S3_PROFILE";
const COMPONENT_RADIO_PROTOCOL: &str = "RADIO_PROTOCOL";
const COMPONENT_RADIO_QUIT: &str = "RADIO_QUIT";
const COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK: &str = "RADIO_DELETE_BOOKMARK";
const COMPONENT_RADIO_BOOKMARK_DEL_RECENT: &str = "RADIO_DELETE_RECENT";
const COMPONENT_RADIO_BOOKMARK_SAVE_PWD: &str = "RADIO_SAVE_PASSWORD";
const COMPONENT_RADIO_INSTALL_UPDATE: &str = "RADIO_INSTALL_UPDATE";
const COMPONENT_BOOKMARKS_LIST: &str = "BOOKMARKS_LIST";
const COMPONENT_RECENTS_LIST: &str = "RECENTS_LIST";
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
pub enum Id {
Address,
BookmarkName,
BookmarkSavePassword,
BookmarksList,
DeleteBookmarkPopup,
DeleteRecentPopup,
ErrorPopup,
GlobalListener,
HelpText,
InfoPopup,
InstallUpdatePopup,
Keybindings,
NewVersionChangelog,
NewVersionDisclaimer,
Password,
Port,
Protocol,
QuitPopup,
RecentsList,
S3Bucket,
S3Profile,
S3Region,
Subtitle,
Title,
Username,
WaitPopup,
WindowSizeError,
}
#[derive(Debug, PartialEq)]
pub enum Msg {
AddressBlurDown,
AddressBlurUp,
BookmarksListBlur,
BookmarksTabBlur,
CloseDeleteBookmark,
CloseDeleteRecent,
CloseErrorPopup,
CloseInfoPopup,
CloseInstallUpdatePopup,
CloseKeybindingsPopup,
CloseQuitPopup,
CloseSaveBookmark,
Connect,
DeleteBookmark,
DeleteRecent,
EnterSetup,
InstallUpdate,
LoadBookmark(usize),
LoadRecent(usize),
ParamsFormBlur,
PasswordBlurDown,
PasswordBlurUp,
PortBlurDown,
PortBlurUp,
ProtocolBlurDown,
ProtocolBlurUp,
ProtocolChanged(FileTransferProtocol),
Quit,
RececentsListBlur,
S3BucketBlurDown,
S3BucketBlurUp,
S3ProfileBlurDown,
S3ProfileBlurUp,
S3RegionBlurDown,
S3RegionBlurUp,
SaveBookmark,
BookmarkNameBlur,
SaveBookmarkPasswordBlur,
ShowDeleteBookmarkPopup,
ShowDeleteRecentPopup,
ShowKeybindingsPopup,
ShowQuitPopup,
ShowReleaseNotes,
ShowSaveBookmarkPopup,
UsernameBlurDown,
UsernameBlurUp,
None,
}
/// ## InputMask
///
/// Auth form input mask
#[derive(Eq, PartialEq)]
enum InputMask {
Generic,
AwsS3,
}
// Store keys
const STORE_KEY_LATEST_VERSION: &str = "AUTH_LATEST_VERSION";
@@ -79,34 +144,39 @@ const STORE_KEY_RELEASE_NOTES: &str = "AUTH_RELEASE_NOTES";
///
/// AuthActivity is the data holder for the authentication activity
pub struct AuthActivity {
exit_reason: Option<ExitReason>,
context: Option<Context>,
view: View,
app: Application<Id, Msg, NoUserEvent>,
bookmarks_client: Option<BookmarksClient>,
redraw: bool, // Should ui actually be redrawned?
bookmarks_list: Vec<String>, // List of bookmarks
recents_list: Vec<String>, // list of recents
}
impl Default for AuthActivity {
fn default() -> Self {
Self::new()
}
/// List of bookmarks
bookmarks_list: Vec<String>,
/// List of recent hosts
recents_list: Vec<String>,
/// Exit reason
exit_reason: Option<ExitReason>,
/// Should redraw ui
redraw: bool,
/// Protocol
protocol: FileTransferProtocol,
context: Option<Context>,
}
impl AuthActivity {
/// ### new
///
/// Instantiates a new AuthActivity
pub fn new() -> AuthActivity {
pub fn new(ticks: Duration) -> AuthActivity {
AuthActivity {
exit_reason: None,
app: Application::init(
EventListenerCfg::default()
.default_input_listener(ticks)
.poll_timeout(ticks),
),
context: None,
view: View::init(),
bookmarks_client: None,
redraw: true, // True at startup
bookmarks_list: Vec::new(),
exit_reason: None,
recents_list: Vec::new(),
redraw: true,
protocol: FileTransferProtocol::Sftp,
}
}
@@ -142,9 +212,11 @@ impl AuthActivity {
///
/// Get current input mask to show
fn input_mask(&self) -> InputMask {
match self.get_protocol() {
match self.protocol {
FileTransferProtocol::AwsS3 => InputMask::AwsS3,
_ => InputMask::Generic,
FileTransferProtocol::Ftp(_)
| FileTransferProtocol::Scp
| FileTransferProtocol::Sftp => InputMask::Generic,
}
}
}
@@ -162,9 +234,11 @@ impl Activity for AuthActivity {
// Set context
self.context = Some(context);
// Clear terminal
self.context_mut().clear_screen();
if let Err(err) = self.context_mut().terminal().clear_screen() {
error!("Failed to clear screen: {}", err);
}
// Put raw mode on enabled
if let Err(err) = enable_raw_mode() {
if let Err(err) = self.context_mut().terminal().enable_raw_mode() {
error!("Failed to enter raw mode: {}", err);
}
// If check for updates is enabled, check for updates
@@ -194,24 +268,23 @@ impl Activity for AuthActivity {
if self.context.is_none() {
return;
}
// Read one event
if let Ok(Some(event)) = self.context().input_hnd().read_event() {
// Set redraw to true
self.redraw = true;
// Handle on resize
if let Event::Resize(_, h) = event {
self.check_minimum_window_size(h);
// Tick
match self.app.tick(PollStrategy::UpTo(3)) {
Ok(messages) => {
for msg in messages.into_iter() {
let mut msg = Some(msg);
while msg.is_some() {
msg = self.update(msg);
}
}
}
Err(err) => {
self.mount_error(format!("Application error: {}", err));
}
// Handle event on view and update
let msg = self.view.on(event);
self.update(msg);
}
// Redraw if necessary
// View
if self.redraw {
// View
self.view();
// Set redraw to false
self.redraw = false;
}
}
@@ -231,26 +304,12 @@ impl Activity for AuthActivity {
/// This function finally releases the context
fn on_destroy(&mut self) -> Option<Context> {
// Disable raw mode
if let Err(err) = disable_raw_mode() {
if let Err(err) = self.context_mut().terminal().disable_raw_mode() {
error!("Failed to disable raw mode: {}", err);
}
self.context.as_ref()?;
// Clear terminal and return
match self.context.take() {
Some(mut ctx) => {
ctx.clear_screen();
Some(ctx)
}
None => None,
if let Err(err) = self.context_mut().terminal().clear_screen() {
error!("Failed to clear screen: {}", err);
}
self.context.take()
}
}
/// ## InputMask
///
/// Auth form input mask
#[derive(Eq, PartialEq)]
enum InputMask {
Generic,
AwsS3,
}
+207 -400
View File
@@ -1,6 +1,6 @@
//! ## AuthActivity
//! ## Update
//!
//! `auth_activity` is the module which implements the authentication activity
//! Update impl
/**
* MIT License
@@ -25,415 +25,222 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// locals
use super::{
AuthActivity, FileTransferProtocol, InputMask, COMPONENT_BOOKMARKS_LIST, COMPONENT_INPUT_ADDR,
COMPONENT_INPUT_BOOKMARK_NAME, COMPONENT_INPUT_PASSWORD, COMPONENT_INPUT_PORT,
COMPONENT_INPUT_S3_BUCKET, COMPONENT_INPUT_S3_PROFILE, COMPONENT_INPUT_S3_REGION,
COMPONENT_INPUT_USERNAME, COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK,
COMPONENT_RADIO_BOOKMARK_DEL_RECENT, COMPONENT_RADIO_BOOKMARK_SAVE_PWD,
COMPONENT_RADIO_INSTALL_UPDATE, COMPONENT_RADIO_PROTOCOL, COMPONENT_RADIO_QUIT,
COMPONENT_RECENTS_LIST, COMPONENT_TEXT_ERROR, COMPONENT_TEXT_HELP, COMPONENT_TEXT_INFO,
COMPONENT_TEXT_NEW_VERSION_NOTES, COMPONENT_TEXT_SIZE_ERR, COMPONENT_TEXT_WAIT,
};
use crate::ui::keymap::*;
use tui_realm_stdlib::InputPropsBuilder;
use tuirealm::{Msg, Payload, PropsBuilder, Update, Value};
use super::{AuthActivity, ExitReason, Id, InputMask, Msg, Update};
// -- update
use tuirealm::{State, StateValue};
impl Update for AuthActivity {
/// ### update
///
/// Update auth activity model based on msg
/// The function exits when returns None
fn update(&mut self, msg: Option<(String, Msg)>) -> Option<(String, Msg)> {
let ref_msg: Option<(&str, &Msg)> = msg.as_ref().map(|(s, msg)| (s.as_str(), msg));
// Match msg
match ref_msg {
None => None, // Exit after None
Some(msg) => match msg {
// Focus ( DOWN )
(COMPONENT_RADIO_PROTOCOL, key) if key == &MSG_KEY_DOWN => {
// Give focus based on current mask
match self.input_mask() {
InputMask::Generic => self.view.active(COMPONENT_INPUT_ADDR),
InputMask::AwsS3 => self.view.active(COMPONENT_INPUT_S3_BUCKET),
};
None
}
// -- generic mask (DOWN)
(COMPONENT_INPUT_ADDR, key) if key == &MSG_KEY_DOWN => {
self.view.active(COMPONENT_INPUT_PORT);
None
}
(COMPONENT_INPUT_PORT, key) if key == &MSG_KEY_DOWN => {
self.view.active(COMPONENT_INPUT_USERNAME);
None
}
(COMPONENT_INPUT_USERNAME, key) if key == &MSG_KEY_DOWN => {
self.view.active(COMPONENT_INPUT_PASSWORD);
None
}
(COMPONENT_INPUT_PASSWORD, key) if key == &MSG_KEY_DOWN => {
self.view.active(COMPONENT_RADIO_PROTOCOL);
None
}
// -- s3 mask (DOWN)
(COMPONENT_INPUT_S3_BUCKET, key) if key == &MSG_KEY_DOWN => {
self.view.active(COMPONENT_INPUT_S3_REGION);
None
}
(COMPONENT_INPUT_S3_REGION, key) if key == &MSG_KEY_DOWN => {
self.view.active(COMPONENT_INPUT_S3_PROFILE);
None
}
(COMPONENT_INPUT_S3_PROFILE, key) if key == &MSG_KEY_DOWN => {
self.view.active(COMPONENT_RADIO_PROTOCOL);
None
}
// Focus ( UP )
// -- generic (UP)
(COMPONENT_INPUT_PASSWORD, key) if key == &MSG_KEY_UP => {
self.view.active(COMPONENT_INPUT_USERNAME);
None
}
(COMPONENT_INPUT_USERNAME, key) if key == &MSG_KEY_UP => {
self.view.active(COMPONENT_INPUT_PORT);
None
}
(COMPONENT_INPUT_PORT, key) if key == &MSG_KEY_UP => {
self.view.active(COMPONENT_INPUT_ADDR);
None
}
(COMPONENT_INPUT_ADDR, key) if key == &MSG_KEY_UP => {
self.view.active(COMPONENT_RADIO_PROTOCOL);
None
}
// -- s3 (UP)
(COMPONENT_INPUT_S3_BUCKET, key) if key == &MSG_KEY_UP => {
self.view.active(COMPONENT_RADIO_PROTOCOL);
None
}
(COMPONENT_INPUT_S3_REGION, key) if key == &MSG_KEY_UP => {
self.view.active(COMPONENT_INPUT_S3_BUCKET);
None
}
(COMPONENT_INPUT_S3_PROFILE, key) if key == &MSG_KEY_UP => {
self.view.active(COMPONENT_INPUT_S3_REGION);
None
}
(COMPONENT_RADIO_PROTOCOL, key) if key == &MSG_KEY_UP => {
// Give focus based on current mask
match self.input_mask() {
InputMask::Generic => self.view.active(COMPONENT_INPUT_PASSWORD),
InputMask::AwsS3 => self.view.active(COMPONENT_INPUT_S3_PROFILE),
};
None
}
// Protocol - On Change
(COMPONENT_RADIO_PROTOCOL, Msg::OnChange(Payload::One(Value::Usize(protocol)))) => {
// If port is standard, update the current port with default for selected protocol
let protocol: FileTransferProtocol = Self::protocol_opt_to_enum(*protocol);
// Get port
let port: u16 = self.get_input_port();
match Self::is_port_standard(port) {
false => None, // Return None
true => {
self.update_input_port(Self::get_default_port_for_protocol(protocol))
}
impl Update<Msg> for AuthActivity {
fn update(&mut self, msg: Option<Msg>) -> Option<Msg> {
self.redraw = true;
match msg.unwrap_or(Msg::None) {
Msg::AddressBlurDown => {
assert!(self.app.active(&Id::Port).is_ok());
}
Msg::AddressBlurUp => {
assert!(self.app.active(&Id::Protocol).is_ok());
}
Msg::BookmarksListBlur => {
assert!(self.app.active(&Id::RecentsList).is_ok());
}
Msg::BookmarkNameBlur => {
assert!(self.app.active(&Id::BookmarkSavePassword).is_ok());
}
Msg::BookmarksTabBlur => {
assert!(self.app.active(&Id::Protocol).is_ok());
}
Msg::CloseDeleteBookmark => {
assert!(self.app.umount(&Id::DeleteBookmarkPopup).is_ok());
}
Msg::CloseDeleteRecent => {
assert!(self.app.umount(&Id::DeleteRecentPopup).is_ok());
}
Msg::CloseErrorPopup => {
self.umount_error();
}
Msg::CloseInfoPopup => {
self.umount_info();
}
Msg::CloseInstallUpdatePopup => {
assert!(self.app.umount(&Id::NewVersionChangelog).is_ok());
assert!(self.app.umount(&Id::InstallUpdatePopup).is_ok());
}
Msg::CloseKeybindingsPopup => {
self.umount_help();
}
Msg::CloseQuitPopup => self.umount_quit(),
Msg::CloseSaveBookmark => {
assert!(self.app.umount(&Id::BookmarkName).is_ok());
assert!(self.app.umount(&Id::BookmarkSavePassword).is_ok());
}
Msg::Connect => {
match self.collect_host_params() {
Err(err) => {
// mount error
self.mount_error(err);
}
Ok(params) => {
self.save_recent();
// Set file transfer params to context
self.context_mut().set_ftparams(params);
// Set exit reason
self.exit_reason = Some(super::ExitReason::Connect);
}
}
// Bookmarks commands
// <RIGHT> / <LEFT>
(COMPONENT_BOOKMARKS_LIST, key) if key == &MSG_KEY_RIGHT => {
// Give focus to recents
self.view.active(COMPONENT_RECENTS_LIST);
None
}
(COMPONENT_RECENTS_LIST, key) if key == &MSG_KEY_LEFT => {
// Give focus to bookmarks
self.view.active(COMPONENT_BOOKMARKS_LIST);
None
}
// <DEL | 'E'>
(COMPONENT_BOOKMARKS_LIST, key)
if key == &MSG_KEY_DEL || key == &MSG_KEY_CHAR_E =>
{
// Show delete popup
self.mount_bookmark_del_dialog();
None
}
(COMPONENT_RECENTS_LIST, key) if key == &MSG_KEY_DEL || key == &MSG_KEY_CHAR_E => {
// Show delete popup
self.mount_recent_del_dialog();
None
}
// Enter
(COMPONENT_BOOKMARKS_LIST, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => {
self.load_bookmark(*idx);
// Give focus to input password (or to protocol if not generic)
self.view.active(match self.input_mask() {
InputMask::Generic => COMPONENT_INPUT_PASSWORD,
InputMask::AwsS3 => COMPONENT_INPUT_S3_BUCKET,
});
None
}
(COMPONENT_RECENTS_LIST, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => {
self.load_recent(*idx);
// Give focus to input password
self.view.active(match self.input_mask() {
InputMask::Generic => COMPONENT_INPUT_PASSWORD,
InputMask::AwsS3 => COMPONENT_INPUT_S3_BUCKET,
});
None
}
// Bookmark radio
// Del bookmarks
(
COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK,
Msg::OnSubmit(Payload::One(Value::Usize(index))),
) => {
// hide bookmark delete
}
Msg::DeleteBookmark => {
if let Ok(State::One(StateValue::Usize(idx))) = self.app.state(&Id::BookmarksList) {
// Umount dialog
self.umount_bookmark_del_dialog();
// Index must be 0 => YES
match *index {
0 => {
// Get selected bookmark
match self.view.get_state(COMPONENT_BOOKMARKS_LIST) {
Some(Payload::One(Value::Usize(index))) => {
// Delete bookmark
self.del_bookmark(index);
// Update bookmarks
self.view_bookmarks()
}
_ => None,
}
}
_ => None,
}
}
(
COMPONENT_RADIO_BOOKMARK_DEL_RECENT,
Msg::OnSubmit(Payload::One(Value::Usize(index))),
) => {
// hide bookmark delete
self.umount_recent_del_dialog();
// Index must be 0 => YES
match *index {
0 => {
// Get selected bookmark
match self.view.get_state(COMPONENT_RECENTS_LIST) {
Some(Payload::One(Value::Usize(index))) => {
// Delete recent
self.del_recent(index);
// Update bookmarks
self.view_recent_connections()
}
_ => None,
}
}
_ => None,
}
}
// <ESC> hide tab
(COMPONENT_RADIO_BOOKMARK_DEL_RECENT, key) if key == &MSG_KEY_ESC => {
self.umount_recent_del_dialog();
None
}
(COMPONENT_RADIO_BOOKMARK_DEL_RECENT, _) => None,
(COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK, key) if key == &MSG_KEY_ESC => {
self.umount_bookmark_del_dialog();
None
}
(COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK, _) => None,
// Error message
(COMPONENT_TEXT_ERROR, key) if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => {
// Umount text error
self.umount_error();
None
}
// -- Text info
(COMPONENT_TEXT_INFO, key) if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => {
// Umount text info
self.umount_info();
None
}
(COMPONENT_TEXT_ERROR, _) | (COMPONENT_TEXT_INFO, _) => None,
// -- Text wait
(COMPONENT_TEXT_WAIT, _) => None,
// -- Release notes
(COMPONENT_TEXT_NEW_VERSION_NOTES, key) if key == &MSG_KEY_ESC => {
// Umount release notes
self.umount_release_notes();
None
}
(COMPONENT_TEXT_NEW_VERSION_NOTES, key) if key == &MSG_KEY_TAB => {
// Focus to radio update
self.view.active(COMPONENT_RADIO_INSTALL_UPDATE);
None
}
(COMPONENT_TEXT_NEW_VERSION_NOTES, _) => None,
// -- Install update radio
(COMPONENT_RADIO_INSTALL_UPDATE, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => {
// Install update
self.install_update();
None
}
(COMPONENT_RADIO_INSTALL_UPDATE, Msg::OnSubmit(Payload::One(Value::Usize(1)))) => {
// Umount
self.umount_release_notes();
None
}
(COMPONENT_RADIO_INSTALL_UPDATE, key) if key == &MSG_KEY_TAB => {
// Focus to changelog
self.view.active(COMPONENT_TEXT_NEW_VERSION_NOTES);
None
}
(COMPONENT_RADIO_INSTALL_UPDATE, _) => None,
// Help
(_, key) if key == &MSG_KEY_CTRL_H => {
// Show help
self.mount_help();
None
}
// Release notes
(_, key) if key == &MSG_KEY_CTRL_R => {
// Show release notes
self.mount_release_notes();
None
}
(COMPONENT_TEXT_HELP, key) if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => {
// Hide text help
self.umount_help();
None
}
(COMPONENT_TEXT_HELP, _) => None,
// Enter setup
(_, key) if key == &MSG_KEY_CTRL_C => {
self.exit_reason = Some(super::ExitReason::EnterSetup);
None
}
// Save bookmark; show popup
(_, key) if key == &MSG_KEY_CTRL_S => {
// Show popup
self.mount_bookmark_save_dialog();
// Give focus to bookmark name
self.view.active(COMPONENT_INPUT_BOOKMARK_NAME);
None
}
(COMPONENT_INPUT_BOOKMARK_NAME, key) if key == &MSG_KEY_DOWN => {
// Give focus to pwd
self.view.active(COMPONENT_RADIO_BOOKMARK_SAVE_PWD);
None
}
(COMPONENT_RADIO_BOOKMARK_SAVE_PWD, key) if key == &MSG_KEY_UP => {
// Give focus to pwd
self.view.active(COMPONENT_INPUT_BOOKMARK_NAME);
None
}
// Save bookmark
(COMPONENT_INPUT_BOOKMARK_NAME, Msg::OnSubmit(_))
| (COMPONENT_RADIO_BOOKMARK_SAVE_PWD, Msg::OnSubmit(_)) => {
// Get values
let bookmark_name: String =
match self.view.get_state(COMPONENT_INPUT_BOOKMARK_NAME) {
Some(Payload::One(Value::Str(s))) => s,
_ => String::new(),
};
let save_pwd: bool = matches!(
self.view.get_state(COMPONENT_RADIO_BOOKMARK_SAVE_PWD),
Some(Payload::One(Value::Usize(0)))
);
// Save bookmark
if !bookmark_name.is_empty() {
self.save_bookmark(bookmark_name, save_pwd);
}
// Umount popup
self.umount_bookmark_save_dialog();
// Reload bookmarks
// Delete bookmark
self.del_bookmark(idx);
// Update bookmarks
self.view_bookmarks()
}
// Hide save bookmark
(COMPONENT_INPUT_BOOKMARK_NAME, key) | (COMPONENT_RADIO_BOOKMARK_SAVE_PWD, key)
if key == &MSG_KEY_ESC =>
{
// Umount popup
self.umount_bookmark_save_dialog();
None
}
(COMPONENT_INPUT_BOOKMARK_NAME, _) | (COMPONENT_RADIO_BOOKMARK_SAVE_PWD, _) => None,
// Quit dialog
(COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(choice)))) => {
// If choice is 0, quit termscp
if *choice == 0 {
self.exit_reason = Some(super::ExitReason::Quit);
}
self.umount_quit();
None
}
(COMPONENT_RADIO_QUIT, key) if key == &MSG_KEY_ESC => {
self.umount_quit();
None
}
// -- text size error; block everything
(COMPONENT_TEXT_SIZE_ERR, _) => None,
// <TAB> bookmarks
(COMPONENT_BOOKMARKS_LIST, key) | (COMPONENT_RECENTS_LIST, key)
if key == &MSG_KEY_TAB =>
{
// Give focus to address
self.view.active(COMPONENT_RADIO_PROTOCOL);
None
}
// Any <TAB>, go to bookmarks
(_, key) if key == &MSG_KEY_TAB => {
self.view.active(COMPONENT_BOOKMARKS_LIST);
None
}
// On submit on any unhandled (connect)
(_, Msg::OnSubmit(_)) => self.on_unhandled_submit(),
(_, key) if key == &MSG_KEY_ENTER => self.on_unhandled_submit(),
// <ESC> => Quit
(_, key) if key == &MSG_KEY_ESC => {
self.mount_quit();
None
}
(_, _) => None, // Ignore other events
},
}
}
}
impl AuthActivity {
fn update_input_port(&mut self, port: u16) -> Option<(String, Msg)> {
match self.view.get_props(COMPONENT_INPUT_PORT) {
None => None,
Some(props) => {
let props = InputPropsBuilder::from(props)
.with_value(port.to_string())
.build();
self.view.update(COMPONENT_INPUT_PORT, props)
}
}
}
fn on_unhandled_submit(&mut self) -> Option<(String, Msg)> {
// Validate fields
match self.collect_host_params() {
Err(err) => {
// mount error
self.mount_error(err);
Msg::DeleteRecent => {
if let Ok(State::One(StateValue::Usize(idx))) = self.app.state(&Id::RecentsList) {
// Umount dialog
self.umount_recent_del_dialog();
// Delete recent
self.del_recent(idx);
// Update recents
self.view_recent_connections();
}
}
Ok(params) => {
self.save_recent();
// Set file transfer params to context
self.context_mut().set_ftparams(params);
// Set exit reason
self.exit_reason = Some(super::ExitReason::Connect);
Msg::EnterSetup => {
self.exit_reason = Some(ExitReason::EnterSetup);
}
Msg::InstallUpdate => {
self.install_update();
}
Msg::LoadBookmark(i) => {
self.load_bookmark(i);
// Give focus to input password (or to protocol if not generic)
assert!(self
.app
.active(match self.input_mask() {
InputMask::Generic => &Id::Password,
InputMask::AwsS3 => &Id::S3Bucket,
})
.is_ok());
}
Msg::LoadRecent(i) => {
self.load_recent(i);
// Give focus to input password (or to protocol if not generic)
assert!(self
.app
.active(match self.input_mask() {
InputMask::Generic => &Id::Password,
InputMask::AwsS3 => &Id::S3Bucket,
})
.is_ok());
}
Msg::ParamsFormBlur => {
assert!(self.app.active(&Id::BookmarksList).is_ok());
}
Msg::PasswordBlurDown => {
assert!(self.app.active(&Id::Protocol).is_ok());
}
Msg::PasswordBlurUp => {
assert!(self.app.active(&Id::Username).is_ok());
}
Msg::PortBlurDown => {
assert!(self.app.active(&Id::Username).is_ok());
}
Msg::PortBlurUp => {
assert!(self.app.active(&Id::Address).is_ok());
}
Msg::ProtocolBlurDown => {
assert!(self
.app
.active(match self.input_mask() {
InputMask::Generic => &Id::Address,
InputMask::AwsS3 => &Id::S3Bucket,
})
.is_ok());
}
Msg::ProtocolBlurUp => {
assert!(self
.app
.active(match self.input_mask() {
InputMask::Generic => &Id::Password,
InputMask::AwsS3 => &Id::S3Profile,
})
.is_ok());
}
Msg::ProtocolChanged(protocol) => {
self.protocol = protocol;
// Update port
let port: u16 = self.get_input_port();
if Self::is_port_standard(port) {
self.mount_port(Self::get_default_port_for_protocol(protocol));
}
}
Msg::Quit => {
self.exit_reason = Some(ExitReason::Quit);
}
Msg::RececentsListBlur => {
assert!(self.app.active(&Id::BookmarksList).is_ok());
}
Msg::S3BucketBlurDown => {
assert!(self.app.active(&Id::S3Region).is_ok());
}
Msg::S3BucketBlurUp => {
assert!(self.app.active(&Id::Protocol).is_ok());
}
Msg::S3RegionBlurDown => {
assert!(self.app.active(&Id::S3Profile).is_ok());
}
Msg::S3RegionBlurUp => {
assert!(self.app.active(&Id::S3Bucket).is_ok());
}
Msg::S3ProfileBlurDown => {
assert!(self.app.active(&Id::Protocol).is_ok());
}
Msg::S3ProfileBlurUp => {
assert!(self.app.active(&Id::S3Region).is_ok());
}
Msg::SaveBookmark => {
// get bookmark name
let (name, save_password) = self.get_new_bookmark();
// Save bookmark
if !name.is_empty() {
self.save_bookmark(name, save_password);
}
// Umount popup
self.umount_bookmark_save_dialog();
// Reload bookmarks
self.view_bookmarks()
}
Msg::SaveBookmarkPasswordBlur => {
assert!(self.app.active(&Id::BookmarkName).is_ok());
}
Msg::ShowDeleteBookmarkPopup => {
self.mount_bookmark_del_dialog();
}
Msg::ShowDeleteRecentPopup => {
self.mount_recent_del_dialog();
}
Msg::ShowKeybindingsPopup => {
self.mount_keybindings();
}
Msg::ShowQuitPopup => {
self.mount_quit();
}
Msg::ShowReleaseNotes => {
self.mount_release_notes();
}
Msg::ShowSaveBookmarkPopup => {
self.mount_bookmark_save_dialog();
}
Msg::UsernameBlurDown => {
assert!(self.app.active(&Id::Password).is_ok());
}
Msg::UsernameBlurUp => {
assert!(self.app.active(&Id::Port).is_ok());
}
Msg::None => {}
}
// Return None
None
}
}
File diff suppressed because it is too large Load Diff