feat: Import bookmarks from ssh config with a CLI command (#364)
Some checks failed
Linux / build (push) Has been cancelled
MacOS / build (push) Has been cancelled
Windows / build (push) Has been cancelled

* feat: Import bookmarks from ssh config with a CLI command

Use import-ssh-hosts to import all the possible hosts by the configured ssh config or the default one on your machine

closes #331
This commit is contained in:
Christian Visintin
2025-11-08 15:32:52 +01:00
committed by GitHub
parent 4bebec369f
commit f4156a5059
27 changed files with 883 additions and 481 deletions

View File

@@ -46,6 +46,7 @@
Released on 20/09/2025 Released on 20/09/2025
- [Issue 331](https://github.com/veeso/termscp/issues/331): Added new `import-ssh-hosts` CLI subcommand to import all the hosts from the ssh config as bookmarks.
- [Issue 356](https://github.com/veeso/termscp/issues/356): Fixed SSH auth issue not trying with the password if any RSA key was found. - [Issue 356](https://github.com/veeso/termscp/issues/356): Fixed SSH auth issue not trying with the password if any RSA key was found.
- [Issue 334](https://github.com/veeso/termscp/issues/334): SMB support for MacOS with vendored build of libsmbclient. - [Issue 334](https://github.com/veeso/termscp/issues/334): SMB support for MacOS with vendored build of libsmbclient.
- [Issue 337](https://github.com/veeso/termscp/issues/337): Migrated to libssh.org on Linux and MacOS for better ssh agent support. - [Issue 337](https://github.com/veeso/termscp/issues/337): Migrated to libssh.org on Linux and MacOS for better ssh agent support.

591
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,7 @@ name = "termscp"
readme = "README.md" readme = "README.md"
repository = "https://github.com/veeso/termscp" repository = "https://github.com/veeso/termscp"
version = "0.19.0" version = "0.19.0"
rust-version = "1.87.0" rust-version = "1.88.0"
[package.metadata.rpm] [package.metadata.rpm]
package = "termscp" package = "termscp"
@@ -71,11 +71,12 @@ self_update = { version = "^0.42", default-features = false, features = [
"compression-zip-deflate", "compression-zip-deflate",
] } ] }
serde = { version = "^1", features = ["derive"] } serde = { version = "^1", features = ["derive"] }
shellexpand = "3"
simplelog = "^0.12" simplelog = "^0.12"
ssh2-config = "^0.6" ssh2-config = "^0.6"
tempfile = "3" tempfile = "3"
thiserror = "2" thiserror = "2"
tokio = { version = "1.44", features = ["rt"] } tokio = { version = "1", features = ["rt"] }
toml = "^0.9" toml = "^0.9"
tui-realm-stdlib = "3" tui-realm-stdlib = "3"
tuirealm = "3" tuirealm = "3"

View File

@@ -10,6 +10,10 @@
- [Unterbefehle](#unterbefehle) - [Unterbefehle](#unterbefehle)
- [Ein Thema importieren](#ein-thema-importieren) - [Ein Thema importieren](#ein-thema-importieren)
- [Neueste Version installieren](#neueste-version-installieren) - [Neueste Version installieren](#neueste-version-installieren)
- [Unterbefehle](#unterbefehle-1)
- [Ein Theme importieren](#ein-theme-importieren)
- [Neueste Version installieren](#neueste-version-installieren-1)
- [SSH-Hosts importieren](#ssh-hosts-importieren)
- [S3-Verbindungsparameter](#s3-verbindungsparameter) - [S3-Verbindungsparameter](#s3-verbindungsparameter)
- [S3-Anmeldeinformationen 🦊](#s3-anmeldeinformationen-) - [S3-Anmeldeinformationen 🦊](#s3-anmeldeinformationen-)
- [Dateiexplorer 📂](#dateiexplorer-) - [Dateiexplorer 📂](#dateiexplorer-)
@@ -29,9 +33,9 @@
- [AWS S3 Adressargument](#aws-s3-adressargument-1) - [AWS S3 Adressargument](#aws-s3-adressargument-1)
- [SMB Adressargument](#smb-adressargument-1) - [SMB Adressargument](#smb-adressargument-1)
- [Wie das Passwort bereitgestellt werden kann 🔐](#wie-das-passwort-bereitgestellt-werden-kann--1) - [Wie das Passwort bereitgestellt werden kann 🔐](#wie-das-passwort-bereitgestellt-werden-kann--1)
- [Unterbefehle](#unterbefehle-1) - [Unterbefehle](#unterbefehle-2)
- [Ein Thema importieren](#ein-thema-importieren-1) - [Ein Thema importieren](#ein-thema-importieren-1)
- [Neueste Version installieren](#neueste-version-installieren-1) - [Neueste Version installieren](#neueste-version-installieren-2)
- [S3-Verbindungsparameter](#s3-verbindungsparameter-1) - [S3-Verbindungsparameter](#s3-verbindungsparameter-1)
- [S3-Anmeldeinformationen 🦊](#s3-anmeldeinformationen--1) - [S3-Anmeldeinformationen 🦊](#s3-anmeldeinformationen--1)
- [Dateiexplorer 📂](#dateiexplorer--1) - [Dateiexplorer 📂](#dateiexplorer--1)
@@ -173,6 +177,22 @@ Führen Sie termscp als `termscp theme <thema-datei>` aus
Führen Sie termscp als `termscp update` aus Führen Sie termscp als `termscp update` aus
### Unterbefehle
#### Ein Theme importieren
Führen Sie termscp mit `termscp theme <theme-datei>` aus.
#### Neueste Version installieren
Führen Sie termscp mit `termscp update` aus.
#### SSH-Hosts importieren
Führen Sie termscp mit `termscp import-ssh-hosts [ssh-config-datei]` aus.
Importieren Sie alle Hosts aus der angegebenen SSH-Konfigurationsdatei (wenn keine angegeben ist, wird `~/.ssh/config` verwendet) als Lesezeichen in termscp. Identitätsdateien werden ebenfalls als SSH-Schlüssel in termscp importiert.
--- ---
## S3-Verbindungsparameter ## S3-Verbindungsparameter

View File

@@ -8,6 +8,10 @@
- [Argumento de dirección de WebDAV](#argumento-de-dirección-de-webdav) - [Argumento de dirección de WebDAV](#argumento-de-dirección-de-webdav)
- [Argumento dirección por SMB](#argumento-dirección-por-smb) - [Argumento dirección por SMB](#argumento-dirección-por-smb)
- [Cómo se puede proporcionar la contraseña 🔐](#cómo-se-puede-proporcionar-la-contraseña-) - [Cómo se puede proporcionar la contraseña 🔐](#cómo-se-puede-proporcionar-la-contraseña-)
- [Subcomandos](#subcomandos)
- [Importar un tema](#importar-un-tema)
- [Instalar la versión más reciente](#instalar-la-versión-más-reciente)
- [Importar hosts SSH](#importar-hosts-ssh)
- [S3 parámetros de conexión](#s3-parámetros-de-conexión) - [S3 parámetros de conexión](#s3-parámetros-de-conexión)
- [Credenciales de S3 🦊](#credenciales-de-s3-) - [Credenciales de S3 🦊](#credenciales-de-s3-)
- [Explorador de archivos 📂](#explorador-de-archivos-) - [Explorador de archivos 📂](#explorador-de-archivos-)
@@ -153,6 +157,22 @@ La contraseña se puede proporcionar básicamente a través de 3 formas cuando s
- Con `sshpass`: puede proporcionar la contraseña a través de `sshpass`, p. ej. `sshpass -f ~/.ssh/topsecret.key termscp cvisintin@192.168.1.31` - Con `sshpass`: puede proporcionar la contraseña a través de `sshpass`, p. ej. `sshpass -f ~/.ssh/topsecret.key termscp cvisintin@192.168.1.31`
- Se te pedirá que ingreses: si no utilizas ninguno de los métodos anteriores, se te pedirá la contraseña, como ocurre con las herramientas más clásicas como `scp`, `ssh`, etc. - Se te pedirá que ingreses: si no utilizas ninguno de los métodos anteriores, se te pedirá la contraseña, como ocurre con las herramientas más clásicas como `scp`, `ssh`, etc.
### Subcomandos
#### Importar un tema
Ejecute termscp como `termscp theme <archivo-tema>`
#### Instalar la versión más reciente
Ejecute termscp como `termscp update`
#### Importar hosts SSH
Ejecute termscp como `termscp import-ssh-hosts [archivo-config-ssh]`
Importa todos los hosts del archivo de configuración SSH especificado (si no se proporciona, se usará `~/.ssh/config`) como marcadores en termscp. Los archivos de identidad también se importarán como claves SSH en termscp.
--- ---
## S3 parámetros de conexión ## S3 parámetros de conexión

View File

@@ -8,6 +8,10 @@
- [Argument d'adresse WebDAV](#argument-dadresse-webdav) - [Argument d'adresse WebDAV](#argument-dadresse-webdav)
- [Argument d'adresse SMB](#argument-dadresse-smb) - [Argument d'adresse SMB](#argument-dadresse-smb)
- [Comment le mot de passe peut être fourni 🔐](#comment-le-mot-de-passe-peut-être-fourni-) - [Comment le mot de passe peut être fourni 🔐](#comment-le-mot-de-passe-peut-être-fourni-)
- [Sous-commandes](#sous-commandes)
- [Importer un thème](#importer-un-thème)
- [Installer la dernière version](#installer-la-dernière-version)
- [Importer des hôtes SSH](#importer-des-hôtes-ssh)
- [S3 paramètres de connexion](#s3-paramètres-de-connexion) - [S3 paramètres de connexion](#s3-paramètres-de-connexion)
- [Identifiants S3 🦊](#identifiants-s3-) - [Identifiants S3 🦊](#identifiants-s3-)
- [Explorateur de fichiers 📂](#explorateur-de-fichiers-) - [Explorateur de fichiers 📂](#explorateur-de-fichiers-)
@@ -142,7 +146,6 @@ syntaxe **Other systems**:
smb://[username@]<server-name>[:port]/<share>[/path/.../] smb://[username@]<server-name>[:port]/<share>[/path/.../]
``` ```
#### Comment le mot de passe peut être fourni 🔐 #### Comment le mot de passe peut être fourni 🔐
Vous avez probablement remarqué que, lorsque vous fournissez l'adresse comme argument, il n'y a aucun moyen de fournir le mot de passe. Vous avez probablement remarqué que, lorsque vous fournissez l'adresse comme argument, il n'y a aucun moyen de fournir le mot de passe.
@@ -152,6 +155,22 @@ Le mot de passe peut être fourni de 3 manières lorsque l'argument d'adresse es
- Avec `sshpass`: vous pouvez fournir un mot de passe via `sshpass`, par ex. `sshpass -f ~/.ssh/topsecret.key termscp cvisintin@192.168.1.31` - Avec `sshpass`: vous pouvez fournir un mot de passe via `sshpass`, par ex. `sshpass -f ~/.ssh/topsecret.key termscp cvisintin@192.168.1.31`
- Il vous sera demandé : si vous n'utilisez aucune des méthodes précédentes, le mot de passe vous sera demandé, comme c'est le cas avec les outils plus classiques tels que `scp`, `ssh`, etc. - Il vous sera demandé : si vous n'utilisez aucune des méthodes précédentes, le mot de passe vous sera demandé, comme c'est le cas avec les outils plus classiques tels que `scp`, `ssh`, etc.
### Sous-commandes
#### Importer un thème
Exécutez termscp avec `termscp theme <fichier-thème>`
#### Installer la dernière version
Exécutez termscp avec `termscp update`
#### Importer des hôtes SSH
Exécutez termscp avec `termscp import-ssh-hosts [fichier-config-ssh]`
Importez tous les hôtes du fichier de configuration SSH spécifié (si non fourni, `~/.ssh/config` sera utilisé) comme favoris dans termscp. Les fichiers d'identité seront également importés comme clés SSH dans termscp.
--- ---
## S3 paramètres de connexion ## S3 paramètres de connexion

View File

@@ -8,6 +8,10 @@
- [Argomento indirizzo per WebDAV](#argomento-indirizzo-per-webdav) - [Argomento indirizzo per WebDAV](#argomento-indirizzo-per-webdav)
- [Indirizzo SMB](#indirizzo-smb) - [Indirizzo SMB](#indirizzo-smb)
- [Come fornire la password 🔐](#come-fornire-la-password-) - [Come fornire la password 🔐](#come-fornire-la-password-)
- [Sottocomandi](#sottocomandi)
- [Importare un tema](#importare-un-tema)
- [Installare lultima versione](#installare-lultima-versione)
- [Importare host SSH](#importare-host-ssh)
- [Parametri di connessione S3](#parametri-di-connessione-s3) - [Parametri di connessione S3](#parametri-di-connessione-s3)
- [Credenziali S3 🦊](#credenziali-s3-) - [Credenziali S3 🦊](#credenziali-s3-)
- [File explorer 📂](#file-explorer-) - [File explorer 📂](#file-explorer-)
@@ -140,7 +144,6 @@ SMB ha una sintassi differente rispetto agli altri protocolli e cambia in base a
smb://[username@]<server-name>[:port]/<share>[/path/.../] smb://[username@]<server-name>[:port]/<share>[/path/.../]
``` ```
#### Come fornire la password 🔐 #### Come fornire la password 🔐
Quando si usa l'argomento indirizzo non è possibile fornire la password direttamente nell'argomento, esistono però altri metodi per farlo: Quando si usa l'argomento indirizzo non è possibile fornire la password direttamente nell'argomento, esistono però altri metodi per farlo:
@@ -149,6 +152,22 @@ Quando si usa l'argomento indirizzo non è possibile fornire la password diretta
- Tramite `sshpass`: puoi fornire la password tramite l'applicazione GNU/Linux sshpass `sshpass -f ~/.ssh/topsecret.key termscp cvisintin@192.168.1.31` - Tramite `sshpass`: puoi fornire la password tramite l'applicazione GNU/Linux sshpass `sshpass -f ~/.ssh/topsecret.key termscp cvisintin@192.168.1.31`
- Forniscila quando richiesta: se non la fornisci tramite nessun metodo precedente, alla connessione ti verrà richiesto di fornirla in un prompt che la oscurerà (come avviene con sudo tipo). - Forniscila quando richiesta: se non la fornisci tramite nessun metodo precedente, alla connessione ti verrà richiesto di fornirla in un prompt che la oscurerà (come avviene con sudo tipo).
### Sottocomandi
#### Importare un tema
Esegui termscp come `termscp theme <file-tema>`
#### Installare lultima versione
Esegui termscp come `termscp update`
#### Importare host SSH
Esegui termscp come `termscp import-ssh-hosts [file-config-ssh]`
Importa tutti gli host dal file di configurazione SSH specificato (se non fornito, verrà usato `~/.ssh/config`) come segnalibri in termscp. I file di identità verranno importati come chiavi SSH in termscp.
--- ---
## Parametri di connessione S3 ## Parametri di connessione S3

View File

@@ -11,6 +11,7 @@
- [Subcommands](#subcommands) - [Subcommands](#subcommands)
- [Import a theme](#import-a-theme) - [Import a theme](#import-a-theme)
- [Install latest version](#install-latest-version) - [Install latest version](#install-latest-version)
- [Import ssh hosts](#import-ssh-hosts)
- [S3 connection parameters](#s3-connection-parameters) - [S3 connection parameters](#s3-connection-parameters)
- [S3 credentials 🦊](#s3-credentials-) - [S3 credentials 🦊](#s3-credentials-)
- [File explorer 📂](#file-explorer-) - [File explorer 📂](#file-explorer-)
@@ -166,6 +167,12 @@ Run termscp as `termscp theme <theme-file>`
Run termscp as `termscp update` Run termscp as `termscp update`
#### Import ssh hosts
Run termscp as `termscp import-ssh-hosts [ssh-config-file]`
Import all the hosts from the specified ssh config file (if not provided, `~/.ssh/config` will be used) as bookmarks in termscp. Identity files will be imported as ssh keys in termscp too.
--- ---
## S3 connection parameters ## S3 connection parameters

View File

@@ -11,6 +11,7 @@
- [Subcomandos](#subcomandos) - [Subcomandos](#subcomandos)
- [Importar um Tema](#importar-um-tema) - [Importar um Tema](#importar-um-tema)
- [Instalar a Última Versão](#instalar-a-última-versão) - [Instalar a Última Versão](#instalar-a-última-versão)
- [Importar hosts SSH](#importar-hosts-ssh)
- [Parâmetros de Conexão do S3](#parâmetros-de-conexão-do-s3) - [Parâmetros de Conexão do S3](#parâmetros-de-conexão-do-s3)
- [Credenciais do S3 🦊](#credenciais-do-s3-) - [Credenciais do S3 🦊](#credenciais-do-s3-)
- [Explorador de Arquivos 📂](#explorador-de-arquivos-) - [Explorador de Arquivos 📂](#explorador-de-arquivos-)
@@ -164,6 +165,12 @@ Execute o termscp como `termscp theme <theme-file>`
Execute o termscp como `termscp update` Execute o termscp como `termscp update`
#### Importar hosts SSH
Execute o termscp como `termscp import-ssh-hosts [arquivo-config-ssh]`
Importe todos os hosts do arquivo de configuração SSH especificado (se não for fornecido, `~/.ssh/config` será usado) como favoritos no termscp. Os arquivos de identidade também serão importados como chaves SSH no termscp.
--- ---
## Parâmetros de Conexão do S3 ## Parâmetros de Conexão do S3

View File

@@ -8,6 +8,10 @@
- [WebDAV 地址参数](#webdav-地址参数) - [WebDAV 地址参数](#webdav-地址参数)
- [SMB 地址参数](#smb-地址参数) - [SMB 地址参数](#smb-地址参数)
- [如何输入密码](#如何输入密码) - [如何输入密码](#如何输入密码)
- [子命令](#子命令)
- [导入主题](#导入主题)
- [安装最新版本](#安装最新版本)
- [导入 SSH 主机](#导入-ssh-主机)
- [S3 连接参数](#s3-连接参数) - [S3 连接参数](#s3-连接参数)
- [Aws S3 凭证](#aws-s3-凭证) - [Aws S3 凭证](#aws-s3-凭证)
- [文件浏览](#文件浏览) - [文件浏览](#文件浏览)
@@ -149,6 +153,21 @@ smb://[username@]<server-name>[:port]/<share>[/path/.../]
- 通过 `sshpass`: 你可以通过 `sshpass` 传入密码, 例如: `sshpass -f ~/.ssh/topsecret.key termscp cvisintin@192.168.1.31` - 通过 `sshpass`: 你可以通过 `sshpass` 传入密码, 例如: `sshpass -f ~/.ssh/topsecret.key termscp cvisintin@192.168.1.31`
- 提示输入密码:如果你不使用前面的任何方法,你会被提示输入密码,就像 `scp`、`ssh` 等比较经典的工具上一样。 - 提示输入密码:如果你不使用前面的任何方法,你会被提示输入密码,就像 `scp`、`ssh` 等比较经典的工具上一样。
### 子命令
#### 导入主题
以 termscp theme <theme-file> 的方式运行 termscp。
#### 安装最新版本
以 termscp update 的方式运行 termscp。
#### 导入 SSH 主机
以 `termscp import-ssh-hosts [ssh-config-file]` 的方式运行 termscp。
从指定的 SSH 配置文件中导入所有主机(如果未提供,则使用 `~/.ssh/config`)作为 termscp 中的书签。身份文件也会作为 SSH 密钥导入到 termscp 中。
--- ---
## S3 连接参数 ## S3 连接参数

View File

@@ -448,35 +448,7 @@ impl ActivityManager {
// -- misc // -- misc
fn init_bookmarks_client(keyring: bool) -> Result<Option<BookmarksClient>, String> { fn init_bookmarks_client(keyring: bool) -> Result<Option<BookmarksClient>, String> {
// Get config dir crate::support::bookmarks_client(keyring)
match environment::init_config_dir() {
Ok(path) => {
// If some configure client, otherwise do nothing; don't bother users telling them that bookmarks are not supported on their system.
if let Some(config_dir_path) = path {
let bookmarks_file: PathBuf =
environment::get_bookmarks_paths(config_dir_path.as_path());
// Initialize client
BookmarksClient::new(
bookmarks_file.as_path(),
config_dir_path.as_path(),
16,
keyring,
)
.map(Option::Some)
.map_err(|e| {
format!(
"Could not initialize bookmarks (at \"{}\", \"{}\"): {}",
bookmarks_file.display(),
config_dir_path.display(),
e
)
})
} else {
Ok(None)
}
}
Err(err) => Err(err),
}
} }
/// Initialize configuration client /// Initialize configuration client

View File

@@ -15,6 +15,9 @@ use crate::system::logging::LogLevel;
pub enum Task { pub enum Task {
Activity(NextActivity), Activity(NextActivity),
/// Import ssh hosts from the specified ssh config file, or from the default location
/// and save them as bookmarks.
ImportSshHosts(Option<PathBuf>),
ImportTheme(PathBuf), ImportTheme(PathBuf),
InstallUpdate, InstallUpdate,
Version, Version,
@@ -72,7 +75,8 @@ pub struct Args {
#[argh(subcommand)] #[argh(subcommand)]
pub enum ArgsSubcommands { pub enum ArgsSubcommands {
Config(ConfigArgs), Config(ConfigArgs),
LoadTheme(LoadThemeArgs), ImportSshHosts(ImportSshHostsArgs),
ImportTheme(ImportThemeArgs),
Update(UpdateArgs), Update(UpdateArgs),
} }
@@ -86,10 +90,20 @@ pub struct ConfigArgs {}
#[argh(subcommand, name = "update")] #[argh(subcommand, name = "update")]
pub struct UpdateArgs {} pub struct UpdateArgs {}
#[derive(FromArgs)]
/// import ssh hosts from the specified ssh config file, or from the default location
/// and save them as bookmarks.
#[argh(subcommand, name = "import-ssh-hosts")]
pub struct ImportSshHostsArgs {
#[argh(positional)]
/// optional ssh config file; if not specified, the default location will be used
pub ssh_config: Option<PathBuf>,
}
#[derive(FromArgs)] #[derive(FromArgs)]
/// import the specified theme /// import the specified theme
#[argh(subcommand, name = "theme")] #[argh(subcommand, name = "theme")]
pub struct LoadThemeArgs { pub struct ImportThemeArgs {
#[argh(positional)] #[argh(positional)]
/// theme file /// theme file
pub theme: PathBuf, pub theme: PathBuf,
@@ -118,6 +132,14 @@ impl RunOpts {
} }
} }
pub fn import_ssh_hosts(ssh_config: Option<PathBuf>, keyring: bool) -> Self {
Self {
task: Task::ImportSshHosts(ssh_config),
keyring,
..Default::default()
}
}
pub fn import_theme(theme: PathBuf) -> Self { pub fn import_theme(theme: PathBuf) -> Self {
Self { Self {
task: Task::ImportTheme(theme), task: Task::ImportTheme(theme),

View File

@@ -65,11 +65,11 @@ impl FileExplorerBuilder {
/// Set formatter for FileExplorer /// Set formatter for FileExplorer
pub fn with_formatter(&mut self, fmt_str: Option<&str>) -> &mut FileExplorerBuilder { pub fn with_formatter(&mut self, fmt_str: Option<&str>) -> &mut FileExplorerBuilder {
if let Some(e) = self.explorer.as_mut() { if let Some(e) = self.explorer.as_mut()
if let Some(fmt_str) = fmt_str { && let Some(fmt_str) = fmt_str
{
e.fmt = Formatter::new(fmt_str); e.fmt = Formatter::new(fmt_str);
} }
}
self self
} }
} }

View File

@@ -245,11 +245,11 @@ impl RemoteFsBuilder {
} }
// For SSH protocols, only set password if explicitly provided and non-empty. // For SSH protocols, only set password if explicitly provided and non-empty.
// This allows the SSH library to prioritize key-based and agent authentication. // This allows the SSH library to prioritize key-based and agent authentication.
if let Some(password) = params.password { if let Some(password) = params.password
if !password.is_empty() { && !password.is_empty()
{
opts = opts.password(password); opts = opts.password(password);
} }
}
if let Some(config_path) = config_client.get_ssh_config() { if let Some(config_path) = config_client.get_ssh_config() {
opts = opts.config_file( opts = opts.config_file(
PathBuf::from(config_path), PathBuf::from(config_path),

View File

@@ -22,7 +22,7 @@ extern crate log;
extern crate magic_crypt; extern crate magic_crypt;
use std::env; use std::env;
use std::path::Path; use std::path::{Path, PathBuf};
use std::time::Duration; use std::time::Duration;
use self::activity_manager::{ActivityManager, NextActivity}; use self::activity_manager::{ActivityManager, NextActivity};
@@ -72,7 +72,10 @@ fn main() -> MainResult<()> {
fn parse_args(args: Args) -> Result<RunOpts, String> { fn parse_args(args: Args) -> Result<RunOpts, String> {
let run_opts = match args.nested { let run_opts = match args.nested {
Some(ArgsSubcommands::Update(_)) => RunOpts::update(), Some(ArgsSubcommands::Update(_)) => RunOpts::update(),
Some(ArgsSubcommands::LoadTheme(args)) => RunOpts::import_theme(args.theme), Some(ArgsSubcommands::ImportSshHosts(subargs)) => {
RunOpts::import_ssh_hosts(subargs.ssh_config, !args.wno_keyring)
}
Some(ArgsSubcommands::ImportTheme(args)) => RunOpts::import_theme(args.theme),
Some(ArgsSubcommands::Config(_)) => RunOpts::config(), Some(ArgsSubcommands::Config(_)) => RunOpts::config(),
None => { None => {
let mut run_opts: RunOpts = RunOpts::default(); let mut run_opts: RunOpts = RunOpts::default();
@@ -111,11 +114,11 @@ fn parse_args(args: Args) -> Result<RunOpts, String> {
}; };
// Local directory // Local directory
if let Some(localdir) = run_opts.remote.local_dir.as_deref() { if let Some(localdir) = run_opts.remote.local_dir.as_deref()
if let Err(err) = env::set_current_dir(localdir) { && let Err(err) = env::set_current_dir(localdir)
{
return Err(format!("Bad working directory argument: {err}")); return Err(format!("Bad working directory argument: {err}"));
} }
}
run_opts run_opts
} }
@@ -127,6 +130,7 @@ fn parse_args(args: Args) -> Result<RunOpts, String> {
/// Run task and return rc /// Run task and return rc
fn run(run_opts: RunOpts) -> MainResult<()> { fn run(run_opts: RunOpts) -> MainResult<()> {
match run_opts.task { match run_opts.task {
Task::ImportSshHosts(ssh_config) => run_import_ssh_hosts(ssh_config, run_opts.keyring),
Task::ImportTheme(theme) => run_import_theme(&theme), Task::ImportTheme(theme) => run_import_theme(&theme),
Task::InstallUpdate => run_install_update(), Task::InstallUpdate => run_install_update(),
Task::Activity(activity) => { Task::Activity(activity) => {
@@ -145,6 +149,17 @@ fn print_version() -> MainResult<()> {
Ok(()) Ok(())
} }
fn run_import_ssh_hosts(ssh_config_path: Option<PathBuf>, keyring: bool) -> MainResult<()> {
support::import_ssh_hosts(ssh_config_path, keyring)
.map(|_| {
println!("SSH hosts have been successfully imported!");
})
.map_err(|err| {
eprintln!("{err}");
err.into()
})
}
fn run_import_theme(theme: &Path) -> MainResult<()> { fn run_import_theme(theme: &Path) -> MainResult<()> {
match support::import_theme(theme) { match support::import_theme(theme) {
Ok(_) => { Ok(_) => {

View File

@@ -2,11 +2,14 @@
//! //!
//! this module exposes some extra run modes for termscp, meant to be used for "support", such as installing themes //! this module exposes some extra run modes for termscp, meant to be used for "support", such as installing themes
// mod mod import_ssh_hosts;
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
pub use self::import_ssh_hosts::import_ssh_hosts;
use crate::system::auto_update::{Update, UpdateStatus}; use crate::system::auto_update::{Update, UpdateStatus};
use crate::system::bookmarks_client::BookmarksClient;
use crate::system::config_client::ConfigClient; use crate::system::config_client::ConfigClient;
use crate::system::environment; use crate::system::environment;
use crate::system::notifications::Notification; use crate::system::notifications::Notification;
@@ -83,3 +86,36 @@ fn get_config_client() -> Option<ConfigClient> {
} }
} }
} }
/// Init [`BookmarksClient`].
pub fn bookmarks_client(keyring: bool) -> Result<Option<BookmarksClient>, String> {
// Get config dir
match environment::init_config_dir() {
Ok(path) => {
// If some configure client, otherwise do nothing; don't bother users telling them that bookmarks are not supported on their system.
if let Some(config_dir_path) = path {
let bookmarks_file: PathBuf =
environment::get_bookmarks_paths(config_dir_path.as_path());
// Initialize client
BookmarksClient::new(
bookmarks_file.as_path(),
config_dir_path.as_path(),
16,
keyring,
)
.map(Option::Some)
.map_err(|e| {
format!(
"Could not initialize bookmarks (at \"{}\", \"{}\"): {}",
bookmarks_file.display(),
config_dir_path.display(),
e
)
})
} else {
Ok(None)
}
}
Err(err) => Err(err),
}
}

View File

@@ -0,0 +1,326 @@
use std::fs::File;
use std::io::BufReader;
use std::path::PathBuf;
use ssh2_config::{Host, HostClause, ParseRule, SshConfig};
use crate::filetransfer::params::GenericProtocolParams;
use crate::filetransfer::{FileTransferParams, FileTransferProtocol, ProtocolParams};
/// Parameters required to add an ssh key for a host.
struct SshKeyParams {
host: String,
ssh_key: String,
username: String,
}
/// Import ssh hosts from the specified ssh config file, or from the default location
/// and save them as bookmarks.
pub fn import_ssh_hosts(ssh_config: Option<PathBuf>, keyring: bool) -> Result<(), String> {
// get config client
let mut cfg_client = super::get_config_client()
.ok_or_else(|| String::from("Could not import ssh hosts: could not load configuration"))?;
// resolve ssh_config
let ssh_config = ssh_config.or_else(|| cfg_client.get_ssh_config().map(PathBuf::from));
// load bookmarks client
let mut bookmarks_client = super::bookmarks_client(keyring)?
.ok_or_else(|| String::from("Could not import ssh hosts: could not load bookmarks"))?;
// load ssh config
let ssh_config = match ssh_config {
Some(p) => {
debug!("Importing ssh hosts from file: {}", p.display());
let mut reader = BufReader::new(
File::open(&p)
.map_err(|e| format!("Could not open ssh config file {}: {e}", p.display()))?,
);
SshConfig::default().parse(&mut reader, ParseRule::ALLOW_UNKNOWN_FIELDS)
}
None => {
debug!("Importing ssh hosts from default location");
SshConfig::parse_default_file(ParseRule::ALLOW_UNKNOWN_FIELDS)
}
}
.map_err(|e| format!("Could not parse ssh config file: {e}"))?;
// iter hosts and add bookmarks
ssh_config
.get_hosts()
.iter()
.flat_map(host_to_params)
.for_each(|(name, params, identity_file_params)| {
debug!("Adding bookmark for host: {name} with params: {params:?}");
bookmarks_client.add_bookmark(name, params, false);
// add ssh key if any
if let Some(identity_file_params) = identity_file_params {
debug!(
"Host {host} has identity file, will add ssh key for it",
host = identity_file_params.host
);
if let Err(err) = cfg_client.add_ssh_key(
&identity_file_params.host,
&identity_file_params.username,
&identity_file_params.ssh_key,
) {
error!(
"Could not add ssh key for host {host}: {err}",
host = identity_file_params.host
);
}
}
});
// save bookmarks
if let Err(err) = bookmarks_client.write_bookmarks() {
return Err(format!(
"Could not save imported ssh hosts as bookmarks: {err}"
));
}
println!("Imported ssh hosts");
Ok(())
}
/// Tries to derive [`FileTransferParams`] from the specified ssh host.
fn host_to_params(
host: &Host,
) -> impl Iterator<Item = (String, FileTransferParams, Option<SshKeyParams>)> {
host.pattern
.iter()
.filter_map(|pattern| host_pattern_to_params(host, pattern))
}
/// Tries to derive [`FileTransferParams`] from the specified ssh host and pattern.
///
/// If `IdentityFile` is specified in the host parameters, it will be included in the returned tuple.
fn host_pattern_to_params(
host: &Host,
pattern: &HostClause,
) -> Option<(String, FileTransferParams, Option<SshKeyParams>)> {
debug!("Processing host with pattern: {pattern:?}",);
if pattern.negated || pattern.pattern.contains('*') || pattern.pattern.contains('?') {
debug!("Skipping host with pattern: {pattern}",);
return None;
}
let address = host
.params
.host_name
.as_deref()
.unwrap_or(pattern.pattern.as_str())
.to_string();
debug!("Resolved address for pattern {pattern}: {address}");
let port = host.params.port.unwrap_or(22);
debug!("Resolved port for pattern {pattern}: {port}");
let username = host.params.user.clone();
debug!("Resolved username for pattern {pattern}: {username:?}");
let identity_file_params = resolve_identity_file_path(host, pattern, &address);
Some((
pattern.to_string(),
FileTransferParams::new(
FileTransferProtocol::Sftp,
ProtocolParams::Generic(
GenericProtocolParams::default()
.address(address)
.port(port)
.username(username),
),
),
identity_file_params,
))
}
fn resolve_identity_file_path(
host: &Host,
pattern: &HostClause,
resolved_address: &str,
) -> Option<SshKeyParams> {
let (Some(username), Some(identity_file)) = (
host.params.user.as_ref(),
host.params.identity_file.as_ref().and_then(|v| v.first()),
) else {
debug!(
"No identity file specified for host {host}, skipping ssh key import",
host = pattern.pattern
);
return None;
};
// expand tilde
let identity_filepath = shellexpand::tilde(&identity_file.display().to_string()).to_string();
debug!("Resolved identity file for pattern {pattern}: {identity_filepath}",);
let Ok(mut ssh_file) = File::open(identity_file) else {
error!(
"Could not open identity file {identity_filepath} for host {host}",
host = pattern.pattern
);
return None;
};
let mut ssh_key = String::new();
use std::io::Read as _;
if let Err(err) = ssh_file.read_to_string(&mut ssh_key) {
error!(
"Could not read identity file {identity_filepath} for host {host}: {err}",
host = pattern.pattern
);
return None;
}
Some(SshKeyParams {
host: resolved_address.to_string(),
username: username.clone(),
ssh_key,
})
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use tempfile::NamedTempFile;
use super::*;
use crate::system::bookmarks_client::BookmarksClient;
#[test]
fn test_should_import_ssh_hosts() {
let ssh_test_config = ssh_test_config();
// import ssh hosts
let result = import_ssh_hosts(Some(ssh_test_config.config.path().to_path_buf()), false);
assert!(result.is_ok());
// verify imported hosts
let config_client = super::super::get_config_client()
.ok_or_else(|| String::from("Could not import ssh hosts: could not load configuration"))
.expect("failed to load config client");
// load bookmarks client
let bookmarks_client = super::super::bookmarks_client(false)
.expect("failed to load bookmarks client")
.expect("bookmarks client is none");
// verify bookmarks
check_bookmark(&bookmarks_client, "test1", "test1.example.com", 2200, None);
check_bookmark(
&bookmarks_client,
"test2",
"test2.example.com",
22,
Some("test2user"),
);
check_bookmark(
&bookmarks_client,
"test3",
"test3.example.com",
2222,
Some("test3user"),
);
// verify ssh keys
let (host, username, _key) = config_client
.get_ssh_key("test3user@test3.example.com")
.expect("ssh key is missing for test3user@test3.example.com");
assert_eq!(host, "test3.example.com");
assert_eq!(username, "test3user");
}
fn check_bookmark(
bookmarks_client: &BookmarksClient,
name: &str,
expected_address: &str,
expected_port: u16,
expected_username: Option<&str>,
) {
// verify bookmarks
let bookmark = bookmarks_client
.get_bookmark(name)
.expect("failed to get bookmark");
let params1 = bookmark
.params
.generic_params()
.expect("should have generic params");
assert_eq!(params1.address, expected_address);
assert_eq!(params1.port, expected_port);
assert_eq!(params1.username.as_deref(), expected_username);
assert!(params1.password.is_none());
}
struct SshTestConfig {
config: NamedTempFile,
#[allow(dead_code)]
identity_file: NamedTempFile,
}
fn ssh_test_config() -> SshTestConfig {
use std::io::Write as _;
let mut identity_file = NamedTempFile::new().expect("failed to create tempfile");
writeln!(
identity_file,
r"-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
NhAAAAAwEAAQAAAQEAxKyYUMRCNPlb4ZV1VMofrzApu2l3wgP4Ot9wBvHsw/+RMpcHIbQK
9iQqAVp8Z+M1fJyPXTKjoJtIzuCLF6Sjo0KI7/tFTh+yPnA5QYNLZOIRZb8skumL4gwHww
5Z942FDPuUDQ30C2mZR9lr3Cd5pA8S1ZSPTAV9QQHkpgoS8cAL8QC6dp3CJjUC8wzvXh3I
oN3bTKxCpM10KMEVuWO3lM4Nvr71auB9gzo1sFJ3bwebCZIRH01FROyA/GXRiaOtJFG/9N
nWWI/iG5AJzArKpLZNHIP+FxV/NoRH0WBXm9Wq5MrBYrD1NQzm+kInpS/2sXk3m1aZWqLm
HF2NKRXSbQAAA8iI+KSniPikpwAAAAdzc2gtcnNhAAABAQDErJhQxEI0+VvhlXVUyh+vMC
m7aXfCA/g633AG8ezD/5EylwchtAr2JCoBWnxn4zV8nI9dMqOgm0jO4IsXpKOjQojv+0VO
H7I+cDlBg0tk4hFlvyyS6YviDAfDDln3jYUM+5QNDfQLaZlH2WvcJ3mkDxLVlI9MBX1BAe
SmChLxwAvxALp2ncImNQLzDO9eHcig3dtMrEKkzXQowRW5Y7eUzg2+vvVq4H2DOjWwUndv
B5sJkhEfTUVE7ID8ZdGJo60kUb/02dZYj+IbkAnMCsqktk0cg/4XFX82hEfRYFeb1arkys
FisPU1DOb6QielL/axeTebVplaouYcXY0pFdJtAAAAAwEAAQAAAP8u3PFuTVV5SfGazwIm
MgNaux82iOsAT/HWFWecQAkqqrruUw5f+YajH/riV61NE9aq2qNOkcJrgpTWtqpt980GGd
SHWlgpRWQzfIooEiDk6Pk8RVFZsEykkDlJQSIu2onZjhi5A5ojHgZoGGabDsztSqoyOjPq
6WPvGYRiDAR3leBMyp1WufBCJqAsC4L8CjPJSmnZhc5a0zXkC9Syz74Fa08tdM7bGhtvP1
GmzuYxkgxHH2IFeoumUSBHRiTZayGuRUDel6jgEiUMxenaDKXe7FpYzMm9tQZA10Mm4LhK
5rP9nd2/KRTFRnfZMnKvtIRC9vtlSLBe14qw+4ZCl60AAACAf1kghlO3+HIWplOmk/lCL0
w75Zz+RdvueL9UuoyNN1QrUEY420LsixgWSeRPby+Rb/hW+XSAZJQHowQ8acFJhU85So7f
4O4wcDuE4f6hpsW9tTfkCEUdLCQJ7EKLCrod6jIV7hvI6rvXiVucRpeAzdOaq4uzj2cwDd
tOdYVsnmQAAACBAOVxBsvO/Sr3rZUbNtA6KewZh/09HNGoKNaCeiD7vaSn2UJbbPRByF/o
Oo5zv8ee8r3882NnmG808XfSn7pPZAzbbTmOaJt0fmyZhivCghSNzV6njW3o0PdnC0fGZQ
ruVXgkd7RJFbsIiD4dDcF4VCjwWHfTK21EOgJUA5pN6TNvAAAAgQDbcJWRx8Uyhkj2+srb
3n2Rt6CR7kEl9cw17ItFjMn+pO81/5U2aGw0iLlX7E06TAMQC+dyW/WaxQRey8RRdtbJ1e
TNKCN34QCWkyuYRHGhcNc0quEDayPw5QWGXlP4BzjfRUcPxY9cCXLe5wDLYsX33HwOAc59
RorU9FCmS/654wAAABFyb290QDhjNTBmZDRjMzQ1YQECAw==
-----END OPENSSH PRIVATE KEY-----"
)
.expect("failed to write identity file");
let mut file = NamedTempFile::new().expect("failed to create tempfile");
// let's declare a couple of hosts
writeln!(
file,
r#"
Host test1
HostName test1.example.com
Port 2200
Host test2
HostName test2.example.com
User test2user
Host test3
HostName test3.example.com
User test3user
Port 2222
IdentityFile {identity_path}
"#,
identity_path = identity_file.path().display()
)
.expect("failed to write ssh config");
SshTestConfig {
config: file,
identity_file,
}
}
}

View File

@@ -300,19 +300,18 @@ impl ConfigClient {
/// Get ssh key from host. /// Get ssh key from host.
/// None is returned if key doesn't exist /// None is returned if key doesn't exist
/// `std::io::Error` is returned in case it was not possible to read the key file pub fn get_ssh_key(&self, mkey: &str) -> Option<SshHost> {
pub fn get_ssh_key(&self, mkey: &str) -> std::io::Result<Option<SshHost>> {
if self.degraded { if self.degraded {
return Ok(None); return None;
} }
// Check if Key exists // Check if Key exists
match self.config.remote.ssh_keys.get(mkey) { match self.config.remote.ssh_keys.get(mkey) {
None => Ok(None), None => None,
Some(key_path) => { Some(key_path) => {
// Get host and username // Get host and username
let (host, username): (String, String) = Self::get_ssh_tokens(mkey); let (host, username): (String, String) = Self::get_ssh_tokens(mkey);
// Return key // Return key
Ok(Some((host, username, PathBuf::from(key_path)))) Some((host, username, PathBuf::from(key_path)))
} }
} }
} }
@@ -451,7 +450,7 @@ mod tests {
// I/O // I/O
assert!(client.add_ssh_key("Omar", "omar", "omar").is_err()); assert!(client.add_ssh_key("Omar", "omar", "omar").is_err());
assert!(client.del_ssh_key("omar", "omar").is_err()); assert!(client.del_ssh_key("omar", "omar").is_err());
assert!(client.get_ssh_key("omar").ok().unwrap().is_none()); assert!(client.get_ssh_key("omar").is_none());
assert!(client.write_config().is_err()); assert!(client.write_config().is_err());
assert!(client.read_config().is_err()); assert!(client.read_config().is_err());
} }
@@ -493,7 +492,7 @@ mod tests {
let mut expected_key_path: PathBuf = key_path; let mut expected_key_path: PathBuf = key_path;
expected_key_path.push("pi@192.168.1.31.key"); expected_key_path.push("pi@192.168.1.31.key");
assert_eq!( assert_eq!(
client.get_ssh_key("pi@192.168.1.31").unwrap().unwrap(), client.get_ssh_key("pi@192.168.1.31").unwrap(),
( (
String::from("192.168.1.31"), String::from("192.168.1.31"),
String::from("pi"), String::from("pi"),
@@ -684,7 +683,7 @@ mod tests {
); );
// Iterate keys // Iterate keys
for key in client.iter_ssh_keys() { for key in client.iter_ssh_keys() {
let host: SshHost = client.get_ssh_key(key).ok().unwrap().unwrap(); let host: SshHost = client.get_ssh_key(key).unwrap();
assert_eq!(host.0, String::from("192.168.1.31")); assert_eq!(host.0, String::from("192.168.1.31"));
assert_eq!(host.1, String::from("pi")); assert_eq!(host.1, String::from("pi"));
let mut expected_key_path: PathBuf = key_path.clone(); let mut expected_key_path: PathBuf = key_path.clone();
@@ -699,7 +698,7 @@ mod tests {
assert_eq!(key, rsa_key); assert_eq!(key, rsa_key);
} }
// Unexisting key // Unexisting key
assert!(client.get_ssh_key("test").ok().unwrap().is_none()); assert!(client.get_ssh_key("test").is_none());
// Delete key // Delete key
assert!(client.del_ssh_key("192.168.1.31", "pi").is_ok()); assert!(client.del_ssh_key("192.168.1.31", "pi").is_ok());
} }

View File

@@ -103,17 +103,11 @@ impl From<&ConfigClient> for SshKeyStorage {
// Iterate over keys in storage // Iterate over keys in storage
for key in cfg_client.iter_ssh_keys() { for key in cfg_client.iter_ssh_keys() {
match cfg_client.get_ssh_key(key) { match cfg_client.get_ssh_key(key) {
Ok(host) => match host {
Some((addr, username, rsa_key_path)) => { Some((addr, username, rsa_key_path)) => {
let key_name: String = Self::make_mapkey(&addr, &username); let key_name: String = Self::make_mapkey(&addr, &username);
hosts.insert(key_name, rsa_key_path); hosts.insert(key_name, rsa_key_path);
} }
None => continue, None => continue,
},
Err(err) => {
error!("Failed to get SSH key for {}: {}", key, err);
continue;
}
} }
info!("Got SSH key for {}", key); info!("Got SSH key for {}", key);
} }

View File

@@ -30,8 +30,9 @@ impl AuthActivity {
pub(super) fn load_bookmark(&mut self, form_tab: FormTab, idx: usize) { pub(super) fn load_bookmark(&mut self, form_tab: FormTab, idx: usize) {
if let Some(bookmarks_cli) = self.bookmarks_client() { if let Some(bookmarks_cli) = self.bookmarks_client() {
// Iterate over bookmarks // Iterate over bookmarks
if let Some(key) = self.bookmarks_list.get(idx) { if let Some(key) = self.bookmarks_list.get(idx)
if let Some(bookmark) = bookmarks_cli.get_bookmark(key) { && let Some(bookmark) = bookmarks_cli.get_bookmark(key)
{
// Load parameters into components // Load parameters into components
match form_tab { match form_tab {
FormTab::Remote => self.load_remote_bookmark_into_gui(bookmark), FormTab::Remote => self.load_remote_bookmark_into_gui(bookmark),
@@ -40,7 +41,6 @@ impl AuthActivity {
} }
} }
} }
}
/// Save current input fields as a bookmark /// Save current input fields as a bookmark
pub(super) fn save_bookmark(&mut self, form_tab: FormTab, name: String, save_password: bool) { pub(super) fn save_bookmark(&mut self, form_tab: FormTab, name: String, save_password: bool) {
@@ -99,8 +99,9 @@ impl AuthActivity {
pub(super) fn load_recent(&mut self, form_tab: FormTab, idx: usize) { pub(super) fn load_recent(&mut self, form_tab: FormTab, idx: usize) {
if let Some(client) = self.bookmarks_client() { if let Some(client) = self.bookmarks_client() {
// Iterate over bookmarks // Iterate over bookmarks
if let Some(key) = self.recents_list.get(idx) { if let Some(key) = self.recents_list.get(idx)
if let Some(bookmark) = client.get_recent(key) { && let Some(bookmark) = client.get_recent(key)
{
// Load parameters // Load parameters
match form_tab { match form_tab {
FormTab::Remote => self.load_remote_bookmark_into_gui(bookmark), FormTab::Remote => self.load_remote_bookmark_into_gui(bookmark),
@@ -109,7 +110,6 @@ impl AuthActivity {
} }
} }
} }
}
/// Save current input fields as a "recent" /// Save current input fields as a "recent"
pub(super) fn save_recent(&mut self) { pub(super) fn save_recent(&mut self) {
@@ -129,12 +129,12 @@ impl AuthActivity {
/// Write bookmarks to file /// Write bookmarks to file
fn write_bookmarks(&mut self) { fn write_bookmarks(&mut self) {
if let Some(bookmarks_cli) = self.bookmarks_client() { if let Some(bookmarks_cli) = self.bookmarks_client()
if let Err(err) = bookmarks_cli.write_bookmarks() { && let Err(err) = bookmarks_cli.write_bookmarks()
{
self.mount_error(format!("Could not write bookmarks: {err}").as_str()); self.mount_error(format!("Could not write bookmarks: {err}").as_str());
} }
} }
}
/// Initialize bookmarks client /// Initialize bookmarks client
pub(super) fn init_bookmarks_client(&mut self) { pub(super) fn init_bookmarks_client(&mut self) {

View File

@@ -126,15 +126,15 @@ impl AuthActivity {
self.host_bridge_protocol = protocol; self.host_bridge_protocol = protocol;
// Update port // Update port
let port: u16 = self.get_input_port(FormTab::HostBridge); let port: u16 = self.get_input_port(FormTab::HostBridge);
if let HostBridgeProtocol::Remote(remote_protocol) = protocol { if let HostBridgeProtocol::Remote(remote_protocol) = protocol
if Self::is_port_standard(port) { && Self::is_port_standard(port)
{
self.mount_port( self.mount_port(
FormTab::HostBridge, FormTab::HostBridge,
Self::get_default_port_for_protocol(remote_protocol), Self::get_default_port_for_protocol(remote_protocol),
); );
} }
} }
}
FormMsg::RemoteProtocolChanged(protocol) => { FormMsg::RemoteProtocolChanged(protocol) => {
self.remote_protocol = protocol; self.remote_protocol = protocol;
// Update port // Update port

View File

@@ -687,8 +687,9 @@ impl AuthActivity {
/// mount release notes text area /// mount release notes text area
pub(super) fn mount_release_notes(&mut self) { pub(super) fn mount_release_notes(&mut self) {
if let Some(ctx) = self.context.as_ref() { if let Some(ctx) = self.context.as_ref()
if let Some(release_notes) = ctx.store().get_string(super::STORE_KEY_RELEASE_NOTES) { && let Some(release_notes) = ctx.store().get_string(super::STORE_KEY_RELEASE_NOTES)
{
// make spans // make spans
let info_color = self.theme().misc_info_dialog; let info_color = self.theme().misc_info_dialog;
assert!( assert!(
@@ -712,7 +713,6 @@ impl AuthActivity {
assert!(self.app.active(&Id::InstallUpdatePopup).is_ok()); assert!(self.app.active(&Id::InstallUpdatePopup).is_ok());
} }
} }
}
/// Umount release notes text area /// Umount release notes text area
pub(super) fn umount_release_notes(&mut self) { pub(super) fn umount_release_notes(&mut self) {

View File

@@ -1,4 +1,4 @@
use std::path::PathBuf; use std::path::{Path, PathBuf};
use tui_realm_stdlib::Input; use tui_realm_stdlib::Input;
use tuirealm::command::{Cmd, CmdResult, Direction, Position}; use tuirealm::command::{Cmd, CmdResult, Direction, Position};
@@ -158,7 +158,7 @@ impl OwnStates {
.unwrap_or_else(|| PathBuf::from("/")); .unwrap_or_else(|| PathBuf::from("/"));
// if path is `.`, then return None // if path is `.`, then return None
if parent == PathBuf::from(".") { if parent == Path::new(".") {
return Suggestion::None; return Suggestion::None;
} }

View File

@@ -506,11 +506,11 @@ impl Activity for FileTransferActivity {
/// This function must be called once before terminating the activity. /// This function must be called once before terminating the activity.
fn on_destroy(&mut self) -> Option<Context> { fn on_destroy(&mut self) -> Option<Context> {
// Destroy cache // Destroy cache
if let Some(cache) = self.cache.take() { if let Some(cache) = self.cache.take()
if let Err(err) = cache.close() { && let Err(err) = cache.close()
{
error!("Failed to delete cache: {}", err); error!("Failed to delete cache: {}", err);
} }
}
// Disable raw mode // Disable raw mode
if let Err(err) = self.context_mut().terminal().disable_raw_mode() { if let Err(err) = self.context_mut().terminal().disable_raw_mode() {
error!("Failed to disable raw mode: {}", err); error!("Failed to disable raw mode: {}", err);

View File

@@ -99,29 +99,16 @@ impl SetupActivity {
Ok(State::One(StateValue::Usize(idx))) => Some(idx), Ok(State::One(StateValue::Usize(idx))) => Some(idx),
_ => None, _ => None,
}; };
if let Some(idx) = idx { // get ssh key and delete it
let key: Option<String> = self.config().iter_ssh_keys().nth(idx).cloned(); if let Some(Err(err)) = idx
if let Some(key) = key { .and_then(|i| self.config().iter_ssh_keys().nth(i).cloned())
match self.config().get_ssh_key(&key) { .and_then(|key| self.config().get_ssh_key(&key))
Ok(opt) => { .map(|(host, username, _)| self.delete_ssh_key(host.as_str(), username.as_str()))
if let Some((host, username, _)) = opt {
if let Err(err) = self.delete_ssh_key(host.as_str(), username.as_str())
{ {
// Report error // Report error
self.mount_error(err.as_str()); self.mount_error(err.as_str());
} }
} }
}
Err(err) => {
// Report error
self.mount_error(
format!("Could not get ssh key \"{key}\": {err}").as_str(),
);
}
}
}
}
}
/// Create a new ssh key /// Create a new ssh key
pub(super) fn action_new_ssh_key(&mut self) -> Result<(), String> { pub(super) fn action_new_ssh_key(&mut self) -> Result<(), String> {

View File

@@ -77,16 +77,11 @@ impl SetupActivity {
Some(key) => { Some(key) => {
// Get key path // Get key path
match ctx.config().get_ssh_key(key) { match ctx.config().get_ssh_key(key) {
Ok(ssh_key) => match ssh_key {
None => Ok(()), None => Ok(()),
Some((_, _, key_path)) => { Some((_, _, key_path)) => match edit::edit_file(key_path.as_path()) {
match edit::edit_file(key_path.as_path()) {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(err) => Err(format!("Could not edit ssh key: {err}")), Err(err) => Err(format!("Could not edit ssh key: {err}")),
}
}
}, },
Err(err) => Err(format!("Could not read ssh key: {err}")),
} }
} }
None => Ok(()), None => Ok(()),

View File

@@ -126,7 +126,7 @@ impl SetupActivity {
.config() .config()
.iter_ssh_keys() .iter_ssh_keys()
.map(|x| { .map(|x| {
let (addr, username, _) = self.config().get_ssh_key(x).ok().unwrap().unwrap(); let (addr, username, _) = self.config().get_ssh_key(x).unwrap();
format!("{username} at {addr}") format!("{username} at {addr}")
}) })
.collect(); .collect();