diff --git a/CHANGELOG.md b/CHANGELOG.md index a840fd5..aeb8521 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,12 @@ Released on FIXME: - **Tui-realm migration**: - migrated application to tui-realm 1.x - Improved application performance +- **SSH Config** + - Added `ssh config` parameter in configuration + - It is now possible to specify the ssh configuration file to use + - The supported parameters are described at . + - If the field is left empty, **no file will be loaded**. + - **By default, no file will be used**. - Dependencies: - Updated `tui-realm` to `1.3.0` - Updated `tui-realm-stdlib` to `1.1.4` diff --git a/docs/de/man.md b/docs/de/man.md index 0a1d753..1610e19 100644 --- a/docs/de/man.md +++ b/docs/de/man.md @@ -308,6 +308,7 @@ These parameters can be changed: - **Local File formatter syntax**: syntax to display file info for each file in the local explorer. See [File explorer format](#file-explorer-format) - **Enable notifications?**: If set to `Yes`, notifications will be displayed. - **Notifications: minimum transfer size**: if transfer size is greater or equal than the specified value, notifications for transfer will be displayed. The accepted values are in format `{UNSIGNED} B/KB/MB/GB/TB/PB` +- **SSH configuration path**: Set SSH configuration file to use when connecting to a SCP/SFTP server. If unset (empty) no file will be used. You can specify a path starting with `~` to indicate the home path (e.g. `~/.ssh/config`) ### SSH Key Storage 🔐 diff --git a/docs/es/man.md b/docs/es/man.md index cf627bf..0436208 100644 --- a/docs/es/man.md +++ b/docs/es/man.md @@ -307,6 +307,7 @@ Estos parámetros se pueden cambiar: - **Local File formatter syntax**: sintaxis para mostrar información de archivo para cada archivo en el explorador local. Consulte [Formato del explorador de archivos](#formato-del-explorador-de-archivos). - **Enable notifications?**: Si se establece en "Sí", se mostrarán las notificaciones. - **Notifications: minimum transfer size**: si el tamaño de la transferencia es mayor o igual que el valor especificado, se mostrarán notificaciones de transferencia. Los valores aceptados están en formato `{UNSIGNED} B/KB/MB/GB/TB/PB` +- **SSH configuration path**: Configure el archivo de configuración SSH para usar al conectarse a un servidor SCP / SFTP. Si no se configura (está vacío), no se utilizará ningún archivo. Puede especificar una ruta que comience con `~` para indicar la ruta de inicio (por ejemplo, `~/.ssh/config`) ### SSH Key Storage 🔐 diff --git a/docs/fr/man.md b/docs/fr/man.md index 6252d25..be855fc 100644 --- a/docs/fr/man.md +++ b/docs/fr/man.md @@ -305,6 +305,7 @@ Ces paramètres peuvent être modifiés : - **Local File formatter syntax**: syntaxe pour afficher les informations de fichier pour chaque fichier dans l'explorateur local. Voir [File explorer format](#format-de-lexplorateur-de-fichiers) - **Enable notifications?**: S'il est défini sur `Yes`, les notifications seront affichées. - **Notifications: minimum transfer size**: si la taille du transfert est supérieure ou égale à la valeur spécifiée, les notifications de transfert seront affichées. Les valeurs acceptées sont au format `{UNSIGNED} B/KB/MB/GB/TB/PB` +- **SSH configuration path** : définissez le fichier de configuration SSH à utiliser lors de la connexion à un serveur SCP/SFTP. S'il n'est pas défini (vide), aucun fichier ne sera utilisé. Vous pouvez spécifier un chemin commençant par `~` pour indiquer le chemin d'accueil (par exemple `~/.ssh/config`) ### SSH Key Storage 🔐 diff --git a/docs/it/man.md b/docs/it/man.md index 4789d7b..dcafab9 100644 --- a/docs/it/man.md +++ b/docs/it/man.md @@ -303,6 +303,7 @@ Questi parametri possono essere impostati: - **Local File formatter syntax**: La formattazione da usare per formattare i file sull'explorer locale. Vedi [File explorer format](#file-explorer-format) - **Enable notifications?**: Se impostato a `yes`, le notifiche desktop saranno abilitate. - **Notifications: minimum transfer size**: se la dimensione di un trasferimento supera o è uguale al valore impostato, al termine del trasferimento riceverai una notifica desktop (se queste sono abilitate). Il formato del valore dev'essere `{UNSIGNED} B/KB/MB/GB/TB/PB` +- **SSH configuration path**: Imposta il percorso del file di configurazione per SSH, per quando ci si connette ad un server SFTP/SCP. Se lasciato vuoto, nessun file verrà usato. Il percorso può anche iniziare con `~` per indicare il percorso della home dell'utente attuale (e.s. `~/.ssh/config`). ### SSH Key Storage 🔐 diff --git a/docs/man.md b/docs/man.md index c782937..05cf148 100644 --- a/docs/man.md +++ b/docs/man.md @@ -306,6 +306,7 @@ These parameters can be changed: - **Local File formatter syntax**: syntax to display file info for each file in the local explorer. See [File explorer format](#file-explorer-format) - **Enable notifications?**: If set to `Yes`, notifications will be displayed. - **Notifications: minimum transfer size**: if transfer size is greater or equal than the specified value, notifications for transfer will be displayed. The accepted values are in format `{UNSIGNED} B/KB/MB/GB/TB/PB` +- **SSH configuration path**: Set SSH configuration file to use when connecting to a SCP/SFTP server. If unset (empty) no file will be used. You can specify a path starting with `~` to indicate the home path (e.g. `~/.ssh/config`) ### SSH Key Storage 🔐 diff --git a/docs/zh-CN/man.md b/docs/zh-CN/man.md index d19e64e..86bb933 100644 --- a/docs/zh-CN/man.md +++ b/docs/zh-CN/man.md @@ -299,6 +299,7 @@ termscp和书签一样,只需要保证这些路径是可访问的: - **Local File formatter syntax**:在本地资源管理器中显示每个文件的文件信息的语法。参见[资源管理器格式](#资源管理器格式) - **Enable notifications?**: 如果设置为 `Yes`,则会显示通知。 - **Notifications: minimum transfer size**: 如果传输大小大于或等于指定值,将显示传输通知。 接受的值格式为 `{UNSIGNED} B/KB/MB/GB/TB/PB` +- **SSH Configuration path**:设置连接到 SCP/SFTP 服务器时使用的 SSH 配置文件。 如果未设置(空),则不会使用任何文件。 你可以指定一个以 `~` 开头的路径来表示主路径(例如 `~/.ssh/config`) ### SSH Key Storage diff --git a/src/config/params.rs b/src/config/params.rs index 3c8b5c1..8f575bb 100644 --- a/src/config/params.rs +++ b/src/config/params.rs @@ -52,16 +52,21 @@ pub struct UserInterfaceConfig { pub check_for_updates: Option, // @! Since 0.3.3 pub prompt_on_file_replace: Option, // @! Since 0.7.0; Default True pub group_dirs: Option, - pub file_fmt: Option, // Refers to local host (for backward compatibility) - pub remote_file_fmt: Option, // @! Since 0.5.0 - pub notifications: Option, // @! Since 0.7.0; Default true + /// file fmt. Refers to local host (for backward compatibility) + pub file_fmt: Option, + pub remote_file_fmt: Option, // @! Since 0.5.0 + pub notifications: Option, // @! Since 0.7.0; Default true pub notification_threshold: Option, // @! Since 0.7.0; Default 512MB } #[derive(Deserialize, Serialize, Debug, Default)] /// Contains configuratio related to remote hosts pub struct RemoteConfig { - pub ssh_keys: HashMap, // Association between host name and path to private key + /// Ssh configuration path. If NONE, won't be read + pub ssh_config: Option, + /// Association between host name and path to private key + /// NOTE: this parameter must stay as last: + pub ssh_keys: HashMap, } impl Default for UserInterfaceConfig { @@ -99,7 +104,10 @@ mod tests { String::from("192.168.1.31"), PathBuf::from("/tmp/private.key"), ); - let remote: RemoteConfig = RemoteConfig { ssh_keys: keys }; + let remote: RemoteConfig = RemoteConfig { + ssh_keys: keys, + ssh_config: Some(String::from("~/.ssh/config")), + }; let ui: UserInterfaceConfig = UserInterfaceConfig { default_protocol: String::from("SFTP"), text_editor: PathBuf::from("nano"), @@ -130,6 +138,10 @@ mod tests { .unwrap(), PathBuf::from("/tmp/private.key") ); + assert_eq!( + cfg.remote.ssh_config.as_deref().unwrap(), + String::from("~/.ssh/config") + ); assert_eq!(cfg.user_interface.default_protocol, String::from("SFTP")); assert_eq!(cfg.user_interface.text_editor, PathBuf::from("nano")); assert_eq!(cfg.user_interface.show_hidden_files, true); diff --git a/src/config/serialization.rs b/src/config/serialization.rs index 2ddb8ed..84198c9 100644 --- a/src/config/serialization.rs +++ b/src/config/serialization.rs @@ -197,6 +197,11 @@ mod tests { assert_eq!(cfg.user_interface.notifications.unwrap(), false); assert_eq!(cfg.user_interface.notification_threshold.unwrap(), 1024); assert_eq!(cfg.user_interface.group_dirs, Some(String::from("last"))); + // Remote + assert_eq!( + cfg.remote.ssh_config.as_deref(), + Some("/home/omar/.ssh/config") + ); assert_eq!( cfg.user_interface.file_fmt, Some(String::from("{NAME} {PEX}")) @@ -244,6 +249,7 @@ mod tests { assert!(cfg.user_interface.remote_file_fmt.is_none()); assert!(cfg.user_interface.notifications.is_none()); assert!(cfg.user_interface.notification_threshold.is_none()); + assert!(cfg.remote.ssh_config.is_none()); // Verify keys assert_eq!( *cfg.remote @@ -322,6 +328,9 @@ mod tests { notifications = false notification_threshold = 1024 + [remote] + ssh_config = "/home/omar/.ssh/config" + [remote.ssh_keys] "192.168.1.31" = "/home/omar/.ssh/raspberry.key" "192.168.1.32" = "/home/omar/.ssh/beaglebone.key" diff --git a/src/filetransfer/builder.rs b/src/filetransfer/builder.rs index 4942c24..d389bde 100644 --- a/src/filetransfer/builder.rs +++ b/src/filetransfer/builder.rs @@ -36,6 +36,7 @@ use remotefs::client::{ ssh::{ScpFs, SftpFs, SshOpts}, }; use remotefs::RemoteFs; +use std::path::PathBuf; /// Remotefs builder pub struct Builder; @@ -117,6 +118,9 @@ impl Builder { if let Some(password) = params.password { opts = opts.password(password); } + if let Some(config_path) = config_client.get_ssh_config() { + opts = opts.config_file(PathBuf::from(config_path)); + } opts } diff --git a/src/system/config_client.rs b/src/system/config_client.rs index e87a03e..7b850c0 100644 --- a/src/system/config_client.rs +++ b/src/system/config_client.rs @@ -239,6 +239,18 @@ impl ConfigClient { self.config.user_interface.notification_threshold = Some(value); } + // Remote params + + /// Get ssh config path + pub fn get_ssh_config(&self) -> Option<&str> { + self.config.remote.ssh_config.as_deref() + } + + /// Set ssh config path + pub fn set_ssh_config(&mut self, p: Option) { + self.config.remote.ssh_config = p; + } + // SSH Keys /// Save a SSH key into configuration. @@ -655,6 +667,20 @@ mod tests { assert_eq!(client.get_notification_threshold(), 64); } + #[test] + fn test_system_config_remote_ssh_config() { + let tmp_dir: TempDir = TempDir::new().ok().unwrap(); + let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path()); + let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path()) + .ok() + .unwrap(); + assert_eq!(client.get_ssh_config(), None); // Null ? + client.set_ssh_config(Some(String::from("/tmp/ssh_config"))); + assert_eq!(client.get_ssh_config(), Some("/tmp/ssh_config")); + client.set_ssh_config(None); + assert_eq!(client.get_ssh_config(), None); + } + #[test] fn test_system_config_ssh_keys() { let tmp_dir: TempDir = TempDir::new().ok().unwrap(); diff --git a/src/ui/activities/auth/components/bookmarks.rs b/src/ui/activities/auth/components/bookmarks.rs index 51acfa9..4745d22 100644 --- a/src/ui/activities/auth/components/bookmarks.rs +++ b/src/ui/activities/auth/components/bookmarks.rs @@ -391,6 +391,7 @@ impl BookmarkName { impl Component for BookmarkName { fn on(&mut self, ev: Event) -> Option { match ev { + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Msg::CloseSaveBookmark), Event::Keyboard(KeyEvent { code: Key::Left, .. }) => { diff --git a/src/ui/activities/setup/components/config.rs b/src/ui/activities/setup/components/config.rs index cc7ffa9..1c3bbe7 100644 --- a/src/ui/activities/setup/components/config.rs +++ b/src/ui/activities/setup/components/config.rs @@ -368,6 +368,43 @@ impl Component for RemoteFileFmt { } } +#[derive(MockComponent)] +pub struct SshConfig { + component: Input, +} + +impl SshConfig { + pub fn new(value: &str) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(Color::LightBlue) + .modifiers(BorderType::Rounded), + ) + .foreground(Color::LightBlue) + .input_type(InputType::Text) + .placeholder( + "~/.ssh/config", + Style::default().fg(Color::Rgb(128, 128, 128)), + ) + .title("SSH configuration path", Alignment::Left) + .value(value), + } + } +} + +impl Component for SshConfig { + fn on(&mut self, ev: Event) -> Option { + handle_input_ev( + self, + ev, + Msg::Config(ConfigMsg::SshConfigBlurDown), + Msg::Config(ConfigMsg::SshConfigBlurUp), + ) + } +} + #[derive(MockComponent)] pub struct TextEditor { component: Input, @@ -448,7 +485,7 @@ fn handle_input_ev( } Event::Keyboard(KeyEvent { code: Key::Char(ch), - modifiers: KeyModifiers::NONE, + modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, }) => { component.perform(Cmd::Type(ch)); Some(Msg::Config(ConfigMsg::ConfigChanged)) diff --git a/src/ui/activities/setup/components/mod.rs b/src/ui/activities/setup/components/mod.rs index e9b58a5..841dad0 100644 --- a/src/ui/activities/setup/components/mod.rs +++ b/src/ui/activities/setup/components/mod.rs @@ -35,7 +35,7 @@ mod theme; pub(super) use commons::{ErrorPopup, Footer, Header, Keybindings, QuitPopup, SavePopup}; pub(super) use config::{ CheckUpdates, DefaultProtocol, GroupDirs, HiddenFiles, LocalFileFmt, NotificationsEnabled, - NotificationsThreshold, PromptOnFileReplace, RemoteFileFmt, TextEditor, + NotificationsThreshold, PromptOnFileReplace, RemoteFileFmt, SshConfig, TextEditor, }; pub(super) use ssh::{DelSshKeyPopup, SshHost, SshKeys, SshUsername}; pub(super) use theme::*; diff --git a/src/ui/activities/setup/config.rs b/src/ui/activities/setup/config.rs index c4cbf92..9137d3d 100644 --- a/src/ui/activities/setup/config.rs +++ b/src/ui/activities/setup/config.rs @@ -36,7 +36,10 @@ impl SetupActivity { pub(super) fn save_config(&mut self) -> Result<(), String> { match self.config().write_config() { Ok(_) => Ok(()), - Err(err) => Err(format!("Could not save configuration: {}", err)), + Err(err) => { + error!("Could not save configuration: {}", err); + Err(format!("Could not save configuration: {}", err)) + } } } diff --git a/src/ui/activities/setup/mod.rs b/src/ui/activities/setup/mod.rs index aba8e8a..8a3029a 100644 --- a/src/ui/activities/setup/mod.rs +++ b/src/ui/activities/setup/mod.rs @@ -75,6 +75,7 @@ enum IdConfig { NotificationsThreshold, PromptOnFileReplace, RemoteFileFmt, + SshConfig, TextEditor, } @@ -167,6 +168,8 @@ pub enum ConfigMsg { PromptOnFileReplaceBlurUp, RemoteFileFmtBlurDown, RemoteFileFmtBlurUp, + SshConfigBlurDown, + SshConfigBlurUp, TextEditorBlurDown, TextEditorBlurUp, } diff --git a/src/ui/activities/setup/update.rs b/src/ui/activities/setup/update.rs index 8267f1f..7072759 100644 --- a/src/ui/activities/setup/update.rs +++ b/src/ui/activities/setup/update.rs @@ -80,11 +80,13 @@ impl SetupActivity { CommonMsg::RevertChanges => match self.layout { ViewLayout::Theme => { if let Err(err) = self.action_reset_theme() { + error!("Failed to reset theme: {}", err); self.mount_error(err); } } ViewLayout::SshKeys | ViewLayout::SetupForm => { if let Err(err) = self.action_reset_config() { + error!("Failed to reset config: {}", err); self.mount_error(err); } } @@ -92,6 +94,7 @@ impl SetupActivity { CommonMsg::SaveAndQuit => { // Save changes if let Err(err) = self.action_save_all() { + error!("Failed to save config: {}", err); self.mount_error(err.as_str()); } // Exit @@ -99,6 +102,7 @@ impl SetupActivity { } CommonMsg::SaveConfig => { if let Err(err) = self.action_save_all() { + error!("Failed to save config: {}", err); self.mount_error(err.as_str()); } self.umount_save_popup(); @@ -173,7 +177,7 @@ impl SetupActivity { .is_ok()); } ConfigMsg::NotificationsThresholdBlurDown => { - assert!(self.app.active(&Id::Config(IdConfig::TextEditor)).is_ok()); + assert!(self.app.active(&Id::Config(IdConfig::SshConfig)).is_ok()); } ConfigMsg::NotificationsThresholdBlurUp => { assert!(self @@ -203,6 +207,12 @@ impl SetupActivity { .is_ok()); } ConfigMsg::TextEditorBlurUp => { + assert!(self.app.active(&Id::Config(IdConfig::SshConfig)).is_ok()); + } + ConfigMsg::SshConfigBlurDown => { + assert!(self.app.active(&Id::Config(IdConfig::TextEditor)).is_ok()); + } + ConfigMsg::SshConfigBlurUp => { assert!(self .app .active(&Id::Config(IdConfig::NotificationsThreshold)) @@ -230,6 +240,7 @@ impl SetupActivity { } SshMsg::EditSshKey(i) => { if let Err(err) = self.edit_ssh_key(i) { + error!("Failed to edit ssh key: {}", err); self.mount_error(err.as_str()); } } diff --git a/src/ui/activities/setup/view/setup.rs b/src/ui/activities/setup/view/setup.rs index 1f6946a..14ede28 100644 --- a/src/ui/activities/setup/view/setup.rs +++ b/src/ui/activities/setup/view/setup.rs @@ -119,6 +119,7 @@ impl SetupActivity { Constraint::Length(3), // Remote Format input Constraint::Length(3), // Notifications enabled Constraint::Length(3), // Notifications threshold + Constraint::Length(3), // Ssh config Constraint::Length(1), // Filler ] .as_ref(), @@ -144,6 +145,8 @@ impl SetupActivity { f, ui_cfg_chunks_col2[3], ); + self.app + .view(&Id::Config(IdConfig::SshConfig), f, ui_cfg_chunks_col2[4]); // Popups self.view_popups(f); }); @@ -261,6 +264,17 @@ impl SetupActivity { vec![] ) .is_ok()); + // Ssh config + assert!(self + .app + .remount( + Id::Config(IdConfig::SshConfig), + Box::new(components::SshConfig::new( + self.config().get_ssh_config().unwrap_or("") + )), + vec![] + ) + .is_ok()); } /// Collect values from input and put them into the configuration @@ -332,5 +346,19 @@ impl SetupActivity { { self.config_mut().set_notification_threshold(bytes); } + if let Ok(State::One(StateValue::String(mut path))) = + self.app.state(&Id::Config(IdConfig::SshConfig)) + { + if path.is_empty() { + self.config_mut().set_ssh_config(None); + } else { + // Replace '~' with home path + if path.starts_with('~') { + let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("/root")); + path = path.replacen('~', &home_dir.to_string_lossy(), 1); + } + self.config_mut().set_ssh_config(Some(path)); + } + } } }