mirror of
https://github.com/veeso/termscp.git
synced 2026-09-27 06:21:22 -07:00
feat(smb): add SMB dialect selection and persistence (#445)
Deploy docs to GitHub Pages / deploy (push) Has been cancelled
Install.sh / build (ubuntu-latest) (push) Has been cancelled
CI / toolchain (push) Has been cancelled
CI / fmt (push) Has been cancelled
CI / install-scripts (push) Has been cancelled
Install.sh / build (macos-latest) (push) Has been cancelled
Site / build-site (push) Has been cancelled
CI / crates-macos-latest (push) Has been cancelled
CI / crates-ubuntu-latest (push) Has been cancelled
CI / crates-windows-latest (push) Has been cancelled
CI / doc (push) Has been cancelled
CI / deny (push) Has been cancelled
Deploy docs to GitHub Pages / deploy (push) Has been cancelled
Install.sh / build (ubuntu-latest) (push) Has been cancelled
CI / toolchain (push) Has been cancelled
CI / fmt (push) Has been cancelled
CI / install-scripts (push) Has been cancelled
Install.sh / build (macos-latest) (push) Has been cancelled
Site / build-site (push) Has been cancelled
CI / crates-macos-latest (push) Has been cancelled
CI / crates-ubuntu-latest (push) Has been cancelled
CI / crates-windows-latest (push) Has been cancelled
CI / doc (push) Has been cancelled
CI / deny (push) Has been cancelled
Support Auto, SMB1, SMB2, and SMB3 selection on Unix, bound negotiation to the selected dialect family, preserve legacy bookmarks as Auto, and document the new option. Windows keeps operating-system-managed negotiation. Closes #439
This commit is contained in:
@@ -101,6 +101,10 @@ pub enum AuthFormId {
|
||||
SmbShare,
|
||||
#[cfg(posix)]
|
||||
SmbWorkgroup,
|
||||
#[cfg(posix)]
|
||||
SmbDialect,
|
||||
#[cfg(posix)]
|
||||
SmbDialectWarning,
|
||||
Username,
|
||||
WebDAVUri,
|
||||
}
|
||||
@@ -209,6 +213,10 @@ pub enum UiAuthFormMsg {
|
||||
SmbWorkgroupDown,
|
||||
#[cfg(posix)]
|
||||
SmbWorkgroupUp,
|
||||
#[cfg(posix)]
|
||||
SmbDialectBlurDown,
|
||||
#[cfg(posix)]
|
||||
SmbDialectBlurUp,
|
||||
UsernameBlurDown,
|
||||
UsernameBlurUp,
|
||||
WebDAVUriBlurDown,
|
||||
|
||||
@@ -315,6 +315,8 @@ impl AuthActivity {
|
||||
self.mount_smb_share(form_tab, ¶ms.share);
|
||||
#[cfg(posix)]
|
||||
self.mount_smb_workgroup(form_tab, params.workgroup.as_deref().unwrap_or(""));
|
||||
#[cfg(posix)]
|
||||
self.mount_smb_dialect(form_tab, params.dialect);
|
||||
}
|
||||
|
||||
fn load_bookmark_webdav_into_gui(&mut self, form_tab: FormTab, params: WebDAVProtocolParams) {
|
||||
|
||||
@@ -13,8 +13,6 @@ pub use bookmarks::{
|
||||
BookmarkName, BookmarkSavePassword, BookmarksList, DeleteBookmarkPopup, DeleteRecentPopup,
|
||||
RecentsList,
|
||||
};
|
||||
#[cfg(posix)]
|
||||
pub use form::InputSmbWorkgroup;
|
||||
pub use form::{
|
||||
HostBridgeProtocolRadio, InputAddress, InputGcsBucket, InputGcsEndpoint,
|
||||
InputGcsServiceAccountKey, InputKubeClientCert, InputKubeClientKey, InputKubeClusterUrl,
|
||||
@@ -23,6 +21,8 @@ pub use form::{
|
||||
InputS3Region, InputS3SecretAccessKey, InputS3SecurityToken, InputS3SessionToken,
|
||||
InputSmbShare, InputUsername, InputWebDAVUri, RadioS3NewPathStyle, RemoteProtocolRadio,
|
||||
};
|
||||
#[cfg(posix)]
|
||||
pub use form::{InputSmbWorkgroup, RadioSmbDialect, SmbDialectWarning};
|
||||
pub use popup::{
|
||||
ErrorPopup, InfoPopup, InstallUpdatePopup, Keybindings, QuitPopup, ReleaseNotes, WaitPopup,
|
||||
WindowSizeError,
|
||||
|
||||
@@ -54,7 +54,7 @@ pub use s3::{
|
||||
};
|
||||
pub use smb::InputSmbShare;
|
||||
#[cfg(posix)]
|
||||
pub use smb::InputSmbWorkgroup;
|
||||
pub use smb::{InputSmbWorkgroup, RadioSmbDialect, SmbDialectWarning};
|
||||
pub use webdav::InputWebDAVUri;
|
||||
|
||||
fn handle_input_ev(
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
#[cfg(posix)]
|
||||
use tui_realm_stdlib::components::Span;
|
||||
use tuirealm::component::{AppComponent, Component};
|
||||
use tuirealm::event::NoUserEvent;
|
||||
#[cfg(posix)]
|
||||
use tuirealm::props::SpanStatic;
|
||||
|
||||
use super::*;
|
||||
#[cfg(posix)]
|
||||
use crate::filetransfer::params::SmbDialect;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct InputSmbShare {
|
||||
@@ -85,3 +91,163 @@ impl AppComponent<Msg, NoUserEvent> for InputSmbWorkgroup {
|
||||
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, posix))]
|
||||
mod test {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
use crate::filetransfer::params::SmbDialect;
|
||||
|
||||
#[test]
|
||||
fn should_map_radio_options_to_dialect() {
|
||||
assert_eq!(RadioSmbDialect::opt_to_dialect(0), SmbDialect::Auto);
|
||||
assert_eq!(RadioSmbDialect::opt_to_dialect(1), SmbDialect::Smb1);
|
||||
assert_eq!(RadioSmbDialect::opt_to_dialect(2), SmbDialect::Smb2);
|
||||
assert_eq!(RadioSmbDialect::opt_to_dialect(3), SmbDialect::Smb3);
|
||||
assert_eq!(RadioSmbDialect::opt_to_dialect(99), SmbDialect::Auto);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_map_dialect_to_radio_options() {
|
||||
for dialect in [
|
||||
SmbDialect::Auto,
|
||||
SmbDialect::Smb1,
|
||||
SmbDialect::Smb2,
|
||||
SmbDialect::Smb3,
|
||||
] {
|
||||
let opt = RadioSmbDialect::dialect_to_opt(dialect);
|
||||
assert_eq!(RadioSmbDialect::opt_to_dialect(opt), dialect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(posix)]
|
||||
const RADIO_SMB_DIALECT_AUTO: usize = 0;
|
||||
#[cfg(posix)]
|
||||
const RADIO_SMB_DIALECT_SMB1: usize = 1;
|
||||
#[cfg(posix)]
|
||||
const RADIO_SMB_DIALECT_SMB2: usize = 2;
|
||||
#[cfg(posix)]
|
||||
const RADIO_SMB_DIALECT_SMB3: usize = 3;
|
||||
|
||||
/// Radio to select the SMB protocol family.
|
||||
#[cfg(posix)]
|
||||
#[derive(Component)]
|
||||
pub struct RadioSmbDialect {
|
||||
component: Radio,
|
||||
form_tab: FormTab,
|
||||
}
|
||||
|
||||
#[cfg(posix)]
|
||||
impl RadioSmbDialect {
|
||||
pub fn new(dialect: SmbDialect, form_tab: FormTab, color: Color) -> Self {
|
||||
Self {
|
||||
component: Radio::default()
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.fg(color)
|
||||
.add_modifier(TextModifiers::REVERSED),
|
||||
)
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.choices(["Auto", "SMB1 (insecure)", "SMB2", "SMB3"])
|
||||
.rewind(true)
|
||||
.title(Title::from("SMB version").alignment(HorizontalAlignment::Left))
|
||||
.value(Self::dialect_to_opt(dialect)),
|
||||
form_tab,
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts the radio choice index to a dialect. Unknown indexes map to `Auto`.
|
||||
pub fn opt_to_dialect(opt: usize) -> SmbDialect {
|
||||
match opt {
|
||||
RADIO_SMB_DIALECT_SMB1 => SmbDialect::Smb1,
|
||||
RADIO_SMB_DIALECT_SMB2 => SmbDialect::Smb2,
|
||||
RADIO_SMB_DIALECT_SMB3 => SmbDialect::Smb3,
|
||||
_ => SmbDialect::Auto,
|
||||
}
|
||||
}
|
||||
|
||||
fn dialect_to_opt(dialect: SmbDialect) -> usize {
|
||||
match dialect {
|
||||
SmbDialect::Auto => RADIO_SMB_DIALECT_AUTO,
|
||||
SmbDialect::Smb1 => RADIO_SMB_DIALECT_SMB1,
|
||||
SmbDialect::Smb2 => RADIO_SMB_DIALECT_SMB2,
|
||||
SmbDialect::Smb3 => RADIO_SMB_DIALECT_SMB3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(posix)]
|
||||
impl AppComponent<Msg, NoUserEvent> for RadioSmbDialect {
|
||||
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::Enter, ..
|
||||
}) => Some(Msg::Form(FormMsg::Connect)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Down, ..
|
||||
}) => Some(if self.form_tab == FormTab::Remote {
|
||||
Msg::Ui(UiMsg::Remote(UiAuthFormMsg::SmbDialectBlurDown))
|
||||
} else {
|
||||
Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::SmbDialectBlurDown))
|
||||
}),
|
||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
||||
Some(if self.form_tab == FormTab::Remote {
|
||||
Msg::Ui(UiMsg::Remote(UiAuthFormMsg::SmbDialectBlurUp))
|
||||
} else {
|
||||
Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::SmbDialectBlurUp))
|
||||
})
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
|
||||
Some(if self.form_tab == FormTab::Remote {
|
||||
Msg::Ui(UiMsg::Remote(UiAuthFormMsg::ParamsFormBlur))
|
||||
} else {
|
||||
Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::ParamsFormBlur))
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One-line warning shown above the dialect radio while SMB1 is selected.
|
||||
#[cfg(posix)]
|
||||
#[derive(Component)]
|
||||
pub struct SmbDialectWarning {
|
||||
component: Span,
|
||||
}
|
||||
|
||||
#[cfg(posix)]
|
||||
impl SmbDialectWarning {
|
||||
pub fn new(color: Color) -> Self {
|
||||
Self {
|
||||
component: Span::default().foreground(color).spans([SpanStatic::from(
|
||||
"Warning: SMB1 is deprecated and insecure. Use it only for isolated legacy devices.",
|
||||
)]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(posix)]
|
||||
impl AppComponent<Msg, NoUserEvent> for SmbDialectWarning {
|
||||
fn on(&mut self, _ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,7 +358,7 @@ impl AuthActivity {
|
||||
InputMask::Localhost => unreachable!(),
|
||||
InputMask::Generic => Id::HostBridge(AuthFormId::Password),
|
||||
#[cfg(posix)]
|
||||
InputMask::Smb => Id::HostBridge(AuthFormId::SmbWorkgroup),
|
||||
InputMask::Smb => Id::HostBridge(AuthFormId::SmbDialect),
|
||||
#[cfg(win)]
|
||||
InputMask::Smb => Id::HostBridge(AuthFormId::Password),
|
||||
InputMask::Kube => Id::HostBridge(AuthFormId::KubeClientKey),
|
||||
@@ -483,12 +483,20 @@ impl AuthActivity {
|
||||
}
|
||||
#[cfg(posix)]
|
||||
UiAuthFormMsg::SmbWorkgroupDown => {
|
||||
self.activate_component(Id::HostBridge(AuthFormId::RemoteDirectory))
|
||||
self.activate_component(Id::HostBridge(AuthFormId::SmbDialect))
|
||||
}
|
||||
#[cfg(posix)]
|
||||
UiAuthFormMsg::SmbWorkgroupUp => {
|
||||
self.activate_component(Id::HostBridge(AuthFormId::Password))
|
||||
}
|
||||
#[cfg(posix)]
|
||||
UiAuthFormMsg::SmbDialectBlurDown => {
|
||||
self.activate_component(Id::HostBridge(AuthFormId::RemoteDirectory))
|
||||
}
|
||||
#[cfg(posix)]
|
||||
UiAuthFormMsg::SmbDialectBlurUp => {
|
||||
self.activate_component(Id::HostBridge(AuthFormId::SmbWorkgroup))
|
||||
}
|
||||
UiAuthFormMsg::UsernameBlurDown => {
|
||||
self.activate_component(Id::HostBridge(AuthFormId::Password))
|
||||
}
|
||||
@@ -601,7 +609,7 @@ impl AuthActivity {
|
||||
InputMask::Localhost => unreachable!(),
|
||||
InputMask::Generic => Id::Remote(AuthFormId::Password),
|
||||
#[cfg(posix)]
|
||||
InputMask::Smb => Id::Remote(AuthFormId::SmbWorkgroup),
|
||||
InputMask::Smb => Id::Remote(AuthFormId::SmbDialect),
|
||||
#[cfg(win)]
|
||||
InputMask::Smb => Id::Remote(AuthFormId::Password),
|
||||
InputMask::Kube => Id::Remote(AuthFormId::KubeClientKey),
|
||||
@@ -726,12 +734,20 @@ impl AuthActivity {
|
||||
}
|
||||
#[cfg(posix)]
|
||||
UiAuthFormMsg::SmbWorkgroupDown => {
|
||||
self.activate_component(Id::Remote(AuthFormId::RemoteDirectory))
|
||||
self.activate_component(Id::Remote(AuthFormId::SmbDialect))
|
||||
}
|
||||
#[cfg(posix)]
|
||||
UiAuthFormMsg::SmbWorkgroupUp => {
|
||||
self.activate_component(Id::Remote(AuthFormId::Password))
|
||||
}
|
||||
#[cfg(posix)]
|
||||
UiAuthFormMsg::SmbDialectBlurDown => {
|
||||
self.activate_component(Id::Remote(AuthFormId::RemoteDirectory))
|
||||
}
|
||||
#[cfg(posix)]
|
||||
UiAuthFormMsg::SmbDialectBlurUp => {
|
||||
self.activate_component(Id::Remote(AuthFormId::SmbWorkgroup))
|
||||
}
|
||||
UiAuthFormMsg::UsernameBlurDown => {
|
||||
self.activate_component(Id::Remote(AuthFormId::Password))
|
||||
}
|
||||
|
||||
+156
-58
@@ -3,7 +3,7 @@
|
||||
//! `auth_activity` is the module which implements the authentication activity
|
||||
|
||||
use tuirealm::props::Color;
|
||||
use tuirealm::ratatui::layout::{Constraint, Direction, Layout};
|
||||
use tuirealm::ratatui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use tuirealm::ratatui::widgets::Clear;
|
||||
use tuirealm::terminal::TerminalAdapter;
|
||||
|
||||
@@ -12,6 +12,8 @@ use super::{
|
||||
InputMask, components,
|
||||
};
|
||||
use crate::filetransfer::params::DEFAULT_GCS_ENDPOINT;
|
||||
#[cfg(posix)]
|
||||
use crate::filetransfer::params::SmbDialect;
|
||||
use crate::utils::ui::{Popup, Size};
|
||||
|
||||
#[path = "view/mounting.rs"]
|
||||
@@ -73,6 +75,10 @@ impl AuthActivity {
|
||||
self.mount_smb_share(FormTab::HostBridge, "");
|
||||
#[cfg(posix)]
|
||||
self.mount_smb_workgroup(FormTab::HostBridge, "");
|
||||
#[cfg(posix)]
|
||||
self.mount_smb_dialect(FormTab::HostBridge, SmbDialect::default());
|
||||
#[cfg(posix)]
|
||||
self.mount_smb_dialect_warning(FormTab::HostBridge);
|
||||
self.mount_webdav_uri(FormTab::HostBridge, "");
|
||||
|
||||
let remote_default_protocol = self.context().config().get_default_protocol();
|
||||
@@ -107,6 +113,10 @@ impl AuthActivity {
|
||||
self.mount_smb_share(FormTab::Remote, "");
|
||||
#[cfg(posix)]
|
||||
self.mount_smb_workgroup(FormTab::Remote, "");
|
||||
#[cfg(posix)]
|
||||
self.mount_smb_dialect(FormTab::Remote, SmbDialect::default());
|
||||
#[cfg(posix)]
|
||||
self.mount_smb_dialect_warning(FormTab::Remote);
|
||||
self.mount_webdav_uri(FormTab::Remote, "");
|
||||
|
||||
if let Some(version) = self
|
||||
@@ -247,8 +257,10 @@ impl AuthActivity {
|
||||
f: &mut tuirealm::ratatui::Frame<'_>,
|
||||
area: tuirealm::ratatui::layout::Rect,
|
||||
) {
|
||||
let input_mask_size = Self::input_mask_size(self.host_bridge_input_mask());
|
||||
let input_mask = self.host_bridge_input_mask();
|
||||
let protocol_and_mask_chunks = Layout::default()
|
||||
.constraints([Constraint::Length(3), Constraint::Length(12)].as_ref())
|
||||
.constraints([Constraint::Length(3), Constraint::Length(input_mask_size)].as_ref())
|
||||
.direction(Direction::Vertical)
|
||||
.split(area);
|
||||
|
||||
@@ -258,36 +270,25 @@ impl AuthActivity {
|
||||
protocol_and_mask_chunks[0],
|
||||
);
|
||||
|
||||
let input_mask = Layout::default()
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.direction(Direction::Vertical)
|
||||
.split(protocol_and_mask_chunks[1]);
|
||||
match self.host_bridge_input_mask() {
|
||||
InputMask::AwsS3 => self.render_view_ids(f, input_mask, self.get_host_bridge_s3_view()),
|
||||
InputMask::Gcs => self.render_view_ids(f, input_mask, self.get_host_bridge_gcs_view()),
|
||||
InputMask::Generic => {
|
||||
self.render_view_ids(f, input_mask, self.get_host_bridge_generic_params_view())
|
||||
}
|
||||
InputMask::Kube => {
|
||||
self.render_view_ids(f, input_mask, self.get_host_bridge_kube_view())
|
||||
}
|
||||
let view_ids = match input_mask {
|
||||
InputMask::AwsS3 => self.get_host_bridge_s3_view(),
|
||||
InputMask::Gcs => self.get_host_bridge_gcs_view(),
|
||||
InputMask::Generic => self.get_host_bridge_generic_params_view(),
|
||||
InputMask::Kube => self.get_host_bridge_kube_view(),
|
||||
InputMask::Localhost => {
|
||||
let view_ids = self.get_host_bridge_localhost_view();
|
||||
self.app.view(&view_ids[0], f, input_mask[0]);
|
||||
self.app.view(&view_ids[0], f, protocol_and_mask_chunks[1]);
|
||||
return;
|
||||
}
|
||||
InputMask::Smb => self.render_view_ids(f, input_mask, self.get_host_bridge_smb_view()),
|
||||
InputMask::WebDAV => {
|
||||
self.render_view_ids(f, input_mask, self.get_host_bridge_webdav_view())
|
||||
}
|
||||
}
|
||||
InputMask::Smb => self.get_host_bridge_smb_view(),
|
||||
InputMask::WebDAV => self.get_host_bridge_webdav_view(),
|
||||
};
|
||||
self.render_form_rows(
|
||||
f,
|
||||
protocol_and_mask_chunks[1],
|
||||
FormTab::HostBridge,
|
||||
view_ids,
|
||||
);
|
||||
}
|
||||
|
||||
fn render_remote_input_mask(
|
||||
@@ -295,8 +296,10 @@ impl AuthActivity {
|
||||
f: &mut tuirealm::ratatui::Frame<'_>,
|
||||
area: tuirealm::ratatui::layout::Rect,
|
||||
) {
|
||||
let input_mask_size = Self::input_mask_size(self.remote_input_mask());
|
||||
let input_mask = self.remote_input_mask();
|
||||
let protocol_and_mask_chunks = Layout::default()
|
||||
.constraints([Constraint::Length(3), Constraint::Length(12)].as_ref())
|
||||
.constraints([Constraint::Length(3), Constraint::Length(input_mask_size)].as_ref())
|
||||
.direction(Direction::Vertical)
|
||||
.split(area);
|
||||
|
||||
@@ -306,40 +309,135 @@ impl AuthActivity {
|
||||
protocol_and_mask_chunks[0],
|
||||
);
|
||||
|
||||
let input_mask = Layout::default()
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.direction(Direction::Vertical)
|
||||
.split(protocol_and_mask_chunks[1]);
|
||||
match self.remote_input_mask() {
|
||||
InputMask::AwsS3 => self.render_view_ids(f, input_mask, self.get_remote_s3_view()),
|
||||
InputMask::Gcs => self.render_view_ids(f, input_mask, self.get_remote_gcs_view()),
|
||||
InputMask::Generic => {
|
||||
self.render_view_ids(f, input_mask, self.get_remote_generic_params_view())
|
||||
}
|
||||
InputMask::Kube => self.render_view_ids(f, input_mask, self.get_remote_kube_view()),
|
||||
let view_ids = match input_mask {
|
||||
InputMask::AwsS3 => self.get_remote_s3_view(),
|
||||
InputMask::Gcs => self.get_remote_gcs_view(),
|
||||
InputMask::Generic => self.get_remote_generic_params_view(),
|
||||
InputMask::Kube => self.get_remote_kube_view(),
|
||||
InputMask::Localhost => unreachable!(),
|
||||
InputMask::Smb => self.render_view_ids(f, input_mask, self.get_remote_smb_view()),
|
||||
InputMask::WebDAV => self.render_view_ids(f, input_mask, self.get_remote_webdav_view()),
|
||||
}
|
||||
InputMask::Smb => self.get_remote_smb_view(),
|
||||
InputMask::WebDAV => self.get_remote_webdav_view(),
|
||||
};
|
||||
self.render_form_rows(f, protocol_and_mask_chunks[1], FormTab::Remote, view_ids);
|
||||
}
|
||||
|
||||
fn render_view_ids(
|
||||
/// Splits `area` into four 3-line form rows. When `warning_row` is
|
||||
/// `Some(index)`, a 1-line row is inserted directly above row `index` and
|
||||
/// returned as the second tuple element.
|
||||
fn split_input_mask(area: Rect, warning_row: Option<usize>) -> ([Rect; 4], Option<Rect>) {
|
||||
let mut constraints = Vec::with_capacity(6);
|
||||
for row in 0..4 {
|
||||
if warning_row == Some(row) {
|
||||
constraints.push(Constraint::Length(1));
|
||||
}
|
||||
constraints.push(Constraint::Length(3));
|
||||
}
|
||||
constraints.push(Constraint::Min(0));
|
||||
let chunks = Layout::default()
|
||||
.constraints(constraints)
|
||||
.direction(Direction::Vertical)
|
||||
.split(area);
|
||||
|
||||
let mut rows = [Rect::default(); 4];
|
||||
let mut warning = None;
|
||||
let mut chunk = 0;
|
||||
for (row, slot) in rows.iter_mut().enumerate() {
|
||||
if warning_row == Some(row) {
|
||||
warning = Some(chunks[chunk]);
|
||||
chunk += 1;
|
||||
}
|
||||
*slot = chunks[chunk];
|
||||
chunk += 1;
|
||||
}
|
||||
(rows, warning)
|
||||
}
|
||||
|
||||
/// Returns the visible row index of the SMB dialect radio when the form
|
||||
/// shows SMB and SMB1 is selected; `None` otherwise.
|
||||
#[cfg(posix)]
|
||||
fn smb_dialect_warning_row(&self, form_tab: FormTab, view_ids: &[Id; 4]) -> Option<usize> {
|
||||
let input_mask = match form_tab {
|
||||
FormTab::HostBridge => self.host_bridge_input_mask(),
|
||||
FormTab::Remote => self.remote_input_mask(),
|
||||
};
|
||||
if input_mask != InputMask::Smb || self.get_input_smb_dialect(form_tab) != SmbDialect::Smb1
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let dialect_id = Self::form_tab_id(form_tab, AuthFormId::SmbDialect);
|
||||
view_ids.iter().position(|id| *id == dialect_id)
|
||||
}
|
||||
|
||||
#[cfg(win)]
|
||||
fn smb_dialect_warning_row(&self, _form_tab: FormTab, _view_ids: &[Id; 4]) -> Option<usize> {
|
||||
None
|
||||
}
|
||||
|
||||
fn render_form_rows(
|
||||
&mut self,
|
||||
f: &mut tuirealm::ratatui::Frame<'_>,
|
||||
input_mask: std::rc::Rc<[tuirealm::ratatui::layout::Rect]>,
|
||||
area: Rect,
|
||||
form_tab: FormTab,
|
||||
view_ids: [Id; 4],
|
||||
) {
|
||||
self.app.view(&view_ids[0], f, input_mask[0]);
|
||||
self.app.view(&view_ids[1], f, input_mask[1]);
|
||||
self.app.view(&view_ids[2], f, input_mask[2]);
|
||||
self.app.view(&view_ids[3], f, input_mask[3]);
|
||||
let warning_row = self.smb_dialect_warning_row(form_tab, &view_ids);
|
||||
let (rows, warning) = Self::split_input_mask(area, warning_row);
|
||||
#[cfg(posix)]
|
||||
if let Some(rect) = warning {
|
||||
let id = Self::form_tab_id(form_tab, AuthFormId::SmbDialectWarning);
|
||||
self.app.view(&id, f, rect);
|
||||
}
|
||||
#[cfg(win)]
|
||||
let _ = warning;
|
||||
for (id, rect) in view_ids.iter().zip(rows) {
|
||||
self.app.view(id, f, rect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use pretty_assertions::assert_eq;
|
||||
use tuirealm::ratatui::layout::Rect;
|
||||
|
||||
use super::AuthActivity;
|
||||
|
||||
#[test]
|
||||
fn should_split_input_mask_without_warning() {
|
||||
let area = Rect::new(0, 0, 40, 13);
|
||||
|
||||
let (rows, warning) = AuthActivity::split_input_mask(area, None);
|
||||
|
||||
assert_eq!(warning, None);
|
||||
assert_eq!(rows[0], Rect::new(0, 0, 40, 3));
|
||||
assert_eq!(rows[1], Rect::new(0, 3, 40, 3));
|
||||
assert_eq!(rows[2], Rect::new(0, 6, 40, 3));
|
||||
assert_eq!(rows[3], Rect::new(0, 9, 40, 3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_split_input_mask_with_middle_warning() {
|
||||
let area = Rect::new(0, 0, 40, 13);
|
||||
|
||||
let (rows, warning) = AuthActivity::split_input_mask(area, Some(2));
|
||||
|
||||
assert_eq!(warning, Some(Rect::new(0, 6, 40, 1)));
|
||||
assert_eq!(rows[0], Rect::new(0, 0, 40, 3));
|
||||
assert_eq!(rows[1], Rect::new(0, 3, 40, 3));
|
||||
assert_eq!(rows[2], Rect::new(0, 7, 40, 3));
|
||||
assert_eq!(rows[3], Rect::new(0, 10, 40, 3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_split_input_mask_with_first_row_warning() {
|
||||
let area = Rect::new(0, 0, 40, 13);
|
||||
|
||||
let (rows, warning) = AuthActivity::split_input_mask(area, Some(0));
|
||||
|
||||
assert_eq!(warning, Some(Rect::new(0, 0, 40, 1)));
|
||||
assert_eq!(rows[0], Rect::new(0, 1, 40, 3));
|
||||
assert_eq!(rows[1], Rect::new(0, 4, 40, 3));
|
||||
assert_eq!(rows[2], Rect::new(0, 7, 40, 3));
|
||||
assert_eq!(rows[3], Rect::new(0, 10, 40, 3));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use super::*;
|
||||
#[cfg(posix)]
|
||||
use crate::filetransfer::params::SmbDialect;
|
||||
use crate::ui::activities::auth::STORE_KEY_RELEASE_NOTES;
|
||||
|
||||
impl AuthActivity {
|
||||
@@ -705,6 +707,36 @@ impl AuthActivity {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(posix)]
|
||||
pub(in crate::ui::activities::auth) fn mount_smb_dialect(
|
||||
&mut self,
|
||||
form_tab: FormTab,
|
||||
dialect: SmbDialect,
|
||||
) {
|
||||
let color = self.theme().auth_protocol;
|
||||
let id = Self::form_tab_id(form_tab, AuthFormId::SmbDialect);
|
||||
if let Err(err) = self.app.remount(
|
||||
id,
|
||||
Box::new(components::RadioSmbDialect::new(dialect, form_tab, color)),
|
||||
vec![],
|
||||
) {
|
||||
error!("Failed to remount component: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(posix)]
|
||||
pub(in crate::ui::activities::auth) fn mount_smb_dialect_warning(&mut self, form_tab: FormTab) {
|
||||
let color = self.theme().misc_warn_dialog;
|
||||
let id = Self::form_tab_id(form_tab, AuthFormId::SmbDialectWarning);
|
||||
if let Err(err) = self.app.remount(
|
||||
id,
|
||||
Box::new(components::SmbDialectWarning::new(color)),
|
||||
vec![],
|
||||
) {
|
||||
error!("Failed to remount component: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::ui::activities::auth) fn mount_webdav_uri(
|
||||
&mut self,
|
||||
form_tab: FormTab,
|
||||
|
||||
@@ -5,10 +5,14 @@ use tuirealm::state::{State, StateValue};
|
||||
|
||||
use super::*;
|
||||
use crate::filetransfer::FileTransferParams;
|
||||
#[cfg(posix)]
|
||||
use crate::filetransfer::params::SmbDialect;
|
||||
use crate::filetransfer::params::{
|
||||
AwsS3Params, GenericProtocolParams, GoogleCloudStorageParams, KubeProtocolParams,
|
||||
ProtocolParams, SmbParams, WebDAVProtocolParams,
|
||||
};
|
||||
#[cfg(posix)]
|
||||
use crate::ui::activities::auth::components::RadioSmbDialect;
|
||||
|
||||
impl AuthActivity {
|
||||
pub(in crate::ui::activities::auth) fn get_generic_params_input(
|
||||
@@ -82,6 +86,7 @@ impl AuthActivity {
|
||||
) -> SmbParams {
|
||||
let share = self.get_input_smb_share(form_tab);
|
||||
let workgroup = self.get_input_smb_workgroup(form_tab);
|
||||
let dialect = self.get_input_smb_dialect(form_tab);
|
||||
let address = self.get_input_addr(form_tab);
|
||||
let port = self.get_input_port(form_tab);
|
||||
let username = self.get_input_username(form_tab);
|
||||
@@ -92,6 +97,7 @@ impl AuthActivity {
|
||||
.username(username)
|
||||
.password(password)
|
||||
.workgroup(workgroup)
|
||||
.dialect(dialect)
|
||||
}
|
||||
|
||||
#[cfg(win)]
|
||||
@@ -452,6 +458,20 @@ impl AuthActivity {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(posix)]
|
||||
pub(in crate::ui::activities::auth) fn get_input_smb_dialect(
|
||||
&self,
|
||||
form_tab: FormTab,
|
||||
) -> SmbDialect {
|
||||
match self
|
||||
.app
|
||||
.state(&Self::form_tab_id(form_tab, AuthFormId::SmbDialect))
|
||||
{
|
||||
Ok(State::Single(StateValue::Usize(opt))) => RadioSmbDialect::opt_to_dialect(opt),
|
||||
_ => SmbDialect::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::ui::activities::auth) fn get_new_bookmark(&self) -> (String, bool) {
|
||||
let name = match self.app.state(&Id::BookmarkName) {
|
||||
Ok(State::Single(StateValue::String(name))) => name,
|
||||
@@ -473,8 +493,10 @@ impl AuthActivity {
|
||||
+ 3
|
||||
}
|
||||
|
||||
fn input_mask_size(input_mask: InputMask) -> u16 {
|
||||
pub(in crate::ui::activities::auth) fn input_mask_size(input_mask: InputMask) -> u16 {
|
||||
match input_mask {
|
||||
// One extra line for the SMB1 warning above the dialect radio.
|
||||
InputMask::Smb if cfg!(posix) => 13,
|
||||
InputMask::AwsS3
|
||||
| InputMask::Gcs
|
||||
| InputMask::Generic
|
||||
|
||||
@@ -289,15 +289,21 @@ impl AuthActivity {
|
||||
Id::HostBridge(AuthFormId::Password),
|
||||
Id::HostBridge(AuthFormId::SmbWorkgroup),
|
||||
],
|
||||
Some(&Id::HostBridge(AuthFormId::RemoteDirectory)) => [
|
||||
Some(&Id::HostBridge(AuthFormId::SmbDialect)) => [
|
||||
Id::HostBridge(AuthFormId::Username),
|
||||
Id::HostBridge(AuthFormId::Password),
|
||||
Id::HostBridge(AuthFormId::SmbWorkgroup),
|
||||
Id::HostBridge(AuthFormId::SmbDialect),
|
||||
],
|
||||
Some(&Id::HostBridge(AuthFormId::RemoteDirectory)) => [
|
||||
Id::HostBridge(AuthFormId::Password),
|
||||
Id::HostBridge(AuthFormId::SmbWorkgroup),
|
||||
Id::HostBridge(AuthFormId::SmbDialect),
|
||||
Id::HostBridge(AuthFormId::RemoteDirectory),
|
||||
],
|
||||
Some(&Id::HostBridge(AuthFormId::LocalDirectory)) => [
|
||||
Id::HostBridge(AuthFormId::Password),
|
||||
Id::HostBridge(AuthFormId::SmbWorkgroup),
|
||||
Id::HostBridge(AuthFormId::SmbDialect),
|
||||
Id::HostBridge(AuthFormId::RemoteDirectory),
|
||||
Id::HostBridge(AuthFormId::LocalDirectory),
|
||||
],
|
||||
@@ -336,15 +342,21 @@ impl AuthActivity {
|
||||
Id::Remote(AuthFormId::Password),
|
||||
Id::Remote(AuthFormId::SmbWorkgroup),
|
||||
],
|
||||
Some(&Id::Remote(AuthFormId::RemoteDirectory)) => [
|
||||
Some(&Id::Remote(AuthFormId::SmbDialect)) => [
|
||||
Id::Remote(AuthFormId::Username),
|
||||
Id::Remote(AuthFormId::Password),
|
||||
Id::Remote(AuthFormId::SmbWorkgroup),
|
||||
Id::Remote(AuthFormId::SmbDialect),
|
||||
],
|
||||
Some(&Id::Remote(AuthFormId::RemoteDirectory)) => [
|
||||
Id::Remote(AuthFormId::Password),
|
||||
Id::Remote(AuthFormId::SmbWorkgroup),
|
||||
Id::Remote(AuthFormId::SmbDialect),
|
||||
Id::Remote(AuthFormId::RemoteDirectory),
|
||||
],
|
||||
Some(&Id::Remote(AuthFormId::LocalDirectory)) => [
|
||||
Id::Remote(AuthFormId::Password),
|
||||
Id::Remote(AuthFormId::SmbWorkgroup),
|
||||
Id::Remote(AuthFormId::SmbDialect),
|
||||
Id::Remote(AuthFormId::RemoteDirectory),
|
||||
Id::Remote(AuthFormId::LocalDirectory),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user