mirror of
https://github.com/veeso/termscp.git
synced 2025-12-06 09:05:36 -08:00
feat: Import bookmarks from ssh config with a CLI command (#364)
* 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:
committed by
GitHub
parent
4bebec369f
commit
f4156a5059
@@ -46,6 +46,7 @@
|
||||
|
||||
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 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.
|
||||
|
||||
591
Cargo.lock
generated
591
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@ name = "termscp"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/veeso/termscp"
|
||||
version = "0.19.0"
|
||||
rust-version = "1.87.0"
|
||||
rust-version = "1.88.0"
|
||||
|
||||
[package.metadata.rpm]
|
||||
package = "termscp"
|
||||
@@ -71,11 +71,12 @@ self_update = { version = "^0.42", default-features = false, features = [
|
||||
"compression-zip-deflate",
|
||||
] }
|
||||
serde = { version = "^1", features = ["derive"] }
|
||||
shellexpand = "3"
|
||||
simplelog = "^0.12"
|
||||
ssh2-config = "^0.6"
|
||||
tempfile = "3"
|
||||
thiserror = "2"
|
||||
tokio = { version = "1.44", features = ["rt"] }
|
||||
tokio = { version = "1", features = ["rt"] }
|
||||
toml = "^0.9"
|
||||
tui-realm-stdlib = "3"
|
||||
tuirealm = "3"
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
- [Unterbefehle](#unterbefehle)
|
||||
- [Ein Thema importieren](#ein-thema-importieren)
|
||||
- [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-Anmeldeinformationen 🦊](#s3-anmeldeinformationen-)
|
||||
- [Dateiexplorer 📂](#dateiexplorer-)
|
||||
@@ -29,9 +33,9 @@
|
||||
- [AWS S3 Adressargument](#aws-s3-adressargument-1)
|
||||
- [SMB Adressargument](#smb-adressargument-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)
|
||||
- [Neueste Version installieren](#neueste-version-installieren-1)
|
||||
- [Neueste Version installieren](#neueste-version-installieren-2)
|
||||
- [S3-Verbindungsparameter](#s3-verbindungsparameter-1)
|
||||
- [S3-Anmeldeinformationen 🦊](#s3-anmeldeinformationen--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
|
||||
|
||||
### 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
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
- [Argumento de dirección de WebDAV](#argumento-de-dirección-de-webdav)
|
||||
- [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-)
|
||||
- [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)
|
||||
- [Credenciales de S3 🦊](#credenciales-de-s3-)
|
||||
- [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`
|
||||
- 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
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
- [Argument d'adresse WebDAV](#argument-dadresse-webdav)
|
||||
- [Argument d'adresse SMB](#argument-dadresse-smb)
|
||||
- [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)
|
||||
- [Identifiants S3 🦊](#identifiants-s3-)
|
||||
- [Explorateur de fichiers 📂](#explorateur-de-fichiers-)
|
||||
@@ -142,7 +146,6 @@ syntaxe **Other systems**:
|
||||
smb://[username@]<server-name>[:port]/<share>[/path/.../]
|
||||
```
|
||||
|
||||
|
||||
#### 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.
|
||||
@@ -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`
|
||||
- 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
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
- [Argomento indirizzo per WebDAV](#argomento-indirizzo-per-webdav)
|
||||
- [Indirizzo SMB](#indirizzo-smb)
|
||||
- [Come fornire la password 🔐](#come-fornire-la-password-)
|
||||
- [Sottocomandi](#sottocomandi)
|
||||
- [Importare un tema](#importare-un-tema)
|
||||
- [Installare l’ultima versione](#installare-lultima-versione)
|
||||
- [Importare host SSH](#importare-host-ssh)
|
||||
- [Parametri di connessione S3](#parametri-di-connessione-s3)
|
||||
- [Credenziali S3 🦊](#credenziali-s3-)
|
||||
- [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/.../]
|
||||
```
|
||||
|
||||
|
||||
#### Come fornire la password 🔐
|
||||
|
||||
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`
|
||||
- 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 l’ultima 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
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
- [Subcommands](#subcommands)
|
||||
- [Import a theme](#import-a-theme)
|
||||
- [Install latest version](#install-latest-version)
|
||||
- [Import ssh hosts](#import-ssh-hosts)
|
||||
- [S3 connection parameters](#s3-connection-parameters)
|
||||
- [S3 credentials 🦊](#s3-credentials-)
|
||||
- [File explorer 📂](#file-explorer-)
|
||||
@@ -166,6 +167,12 @@ Run termscp as `termscp theme <theme-file>`
|
||||
|
||||
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
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
- [Subcomandos](#subcomandos)
|
||||
- [Importar um Tema](#importar-um-tema)
|
||||
- [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)
|
||||
- [Credenciais do S3 🦊](#credenciais-do-s3-)
|
||||
- [Explorador de Arquivos 📂](#explorador-de-arquivos-)
|
||||
@@ -164,6 +165,12 @@ Execute o termscp como `termscp theme <theme-file>`
|
||||
|
||||
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
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
- [WebDAV 地址参数](#webdav-地址参数)
|
||||
- [SMB 地址参数](#smb-地址参数)
|
||||
- [如何输入密码](#如何输入密码)
|
||||
- [子命令](#子命令)
|
||||
- [导入主题](#导入主题)
|
||||
- [安装最新版本](#安装最新版本)
|
||||
- [导入 SSH 主机](#导入-ssh-主机)
|
||||
- [S3 连接参数](#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`
|
||||
- 提示输入密码:如果你不使用前面的任何方法,你会被提示输入密码,就像 `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 连接参数
|
||||
|
||||
@@ -448,35 +448,7 @@ impl ActivityManager {
|
||||
// -- misc
|
||||
|
||||
fn init_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),
|
||||
}
|
||||
crate::support::bookmarks_client(keyring)
|
||||
}
|
||||
|
||||
/// Initialize configuration client
|
||||
|
||||
26
src/cli.rs
26
src/cli.rs
@@ -15,6 +15,9 @@ use crate::system::logging::LogLevel;
|
||||
|
||||
pub enum Task {
|
||||
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),
|
||||
InstallUpdate,
|
||||
Version,
|
||||
@@ -72,7 +75,8 @@ pub struct Args {
|
||||
#[argh(subcommand)]
|
||||
pub enum ArgsSubcommands {
|
||||
Config(ConfigArgs),
|
||||
LoadTheme(LoadThemeArgs),
|
||||
ImportSshHosts(ImportSshHostsArgs),
|
||||
ImportTheme(ImportThemeArgs),
|
||||
Update(UpdateArgs),
|
||||
}
|
||||
|
||||
@@ -86,10 +90,20 @@ pub struct ConfigArgs {}
|
||||
#[argh(subcommand, name = "update")]
|
||||
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)]
|
||||
/// import the specified theme
|
||||
#[argh(subcommand, name = "theme")]
|
||||
pub struct LoadThemeArgs {
|
||||
pub struct ImportThemeArgs {
|
||||
#[argh(positional)]
|
||||
/// theme file
|
||||
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 {
|
||||
Self {
|
||||
task: Task::ImportTheme(theme),
|
||||
|
||||
@@ -65,10 +65,10 @@ impl FileExplorerBuilder {
|
||||
|
||||
/// Set formatter for FileExplorer
|
||||
pub fn with_formatter(&mut self, fmt_str: Option<&str>) -> &mut FileExplorerBuilder {
|
||||
if let Some(e) = self.explorer.as_mut() {
|
||||
if let Some(fmt_str) = fmt_str {
|
||||
e.fmt = Formatter::new(fmt_str);
|
||||
}
|
||||
if let Some(e) = self.explorer.as_mut()
|
||||
&& let Some(fmt_str) = fmt_str
|
||||
{
|
||||
e.fmt = Formatter::new(fmt_str);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
@@ -245,10 +245,10 @@ impl RemoteFsBuilder {
|
||||
}
|
||||
// For SSH protocols, only set password if explicitly provided and non-empty.
|
||||
// This allows the SSH library to prioritize key-based and agent authentication.
|
||||
if let Some(password) = params.password {
|
||||
if !password.is_empty() {
|
||||
opts = opts.password(password);
|
||||
}
|
||||
if let Some(password) = params.password
|
||||
&& !password.is_empty()
|
||||
{
|
||||
opts = opts.password(password);
|
||||
}
|
||||
if let Some(config_path) = config_client.get_ssh_config() {
|
||||
opts = opts.config_file(
|
||||
|
||||
27
src/main.rs
27
src/main.rs
@@ -22,7 +22,7 @@ extern crate log;
|
||||
extern crate magic_crypt;
|
||||
|
||||
use std::env;
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
use self::activity_manager::{ActivityManager, NextActivity};
|
||||
@@ -72,7 +72,10 @@ fn main() -> MainResult<()> {
|
||||
fn parse_args(args: Args) -> Result<RunOpts, String> {
|
||||
let run_opts = match args.nested {
|
||||
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(),
|
||||
None => {
|
||||
let mut run_opts: RunOpts = RunOpts::default();
|
||||
@@ -111,10 +114,10 @@ fn parse_args(args: Args) -> Result<RunOpts, String> {
|
||||
};
|
||||
|
||||
// Local directory
|
||||
if let Some(localdir) = run_opts.remote.local_dir.as_deref() {
|
||||
if let Err(err) = env::set_current_dir(localdir) {
|
||||
return Err(format!("Bad working directory argument: {err}"));
|
||||
}
|
||||
if let Some(localdir) = run_opts.remote.local_dir.as_deref()
|
||||
&& let Err(err) = env::set_current_dir(localdir)
|
||||
{
|
||||
return Err(format!("Bad working directory argument: {err}"));
|
||||
}
|
||||
|
||||
run_opts
|
||||
@@ -127,6 +130,7 @@ fn parse_args(args: Args) -> Result<RunOpts, String> {
|
||||
/// Run task and return rc
|
||||
fn run(run_opts: RunOpts) -> MainResult<()> {
|
||||
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::InstallUpdate => run_install_update(),
|
||||
Task::Activity(activity) => {
|
||||
@@ -145,6 +149,17 @@ fn print_version() -> MainResult<()> {
|
||||
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<()> {
|
||||
match support::import_theme(theme) {
|
||||
Ok(_) => {
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
//!
|
||||
//! 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::path::{Path, PathBuf};
|
||||
|
||||
pub use self::import_ssh_hosts::import_ssh_hosts;
|
||||
use crate::system::auto_update::{Update, UpdateStatus};
|
||||
use crate::system::bookmarks_client::BookmarksClient;
|
||||
use crate::system::config_client::ConfigClient;
|
||||
use crate::system::environment;
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
326
src/support/import_ssh_hosts.rs
Normal file
326
src/support/import_ssh_hosts.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -300,19 +300,18 @@ impl ConfigClient {
|
||||
|
||||
/// Get ssh key from host.
|
||||
/// 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) -> std::io::Result<Option<SshHost>> {
|
||||
pub fn get_ssh_key(&self, mkey: &str) -> Option<SshHost> {
|
||||
if self.degraded {
|
||||
return Ok(None);
|
||||
return None;
|
||||
}
|
||||
// Check if Key exists
|
||||
match self.config.remote.ssh_keys.get(mkey) {
|
||||
None => Ok(None),
|
||||
None => None,
|
||||
Some(key_path) => {
|
||||
// Get host and username
|
||||
let (host, username): (String, String) = Self::get_ssh_tokens(mkey);
|
||||
// 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
|
||||
assert!(client.add_ssh_key("Omar", "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.read_config().is_err());
|
||||
}
|
||||
@@ -493,7 +492,7 @@ mod tests {
|
||||
let mut expected_key_path: PathBuf = key_path;
|
||||
expected_key_path.push("pi@192.168.1.31.key");
|
||||
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("pi"),
|
||||
@@ -684,7 +683,7 @@ mod tests {
|
||||
);
|
||||
// Iterate 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.1, String::from("pi"));
|
||||
let mut expected_key_path: PathBuf = key_path.clone();
|
||||
@@ -699,7 +698,7 @@ mod tests {
|
||||
assert_eq!(key, rsa_key);
|
||||
}
|
||||
// Unexisting key
|
||||
assert!(client.get_ssh_key("test").ok().unwrap().is_none());
|
||||
assert!(client.get_ssh_key("test").is_none());
|
||||
// Delete key
|
||||
assert!(client.del_ssh_key("192.168.1.31", "pi").is_ok());
|
||||
}
|
||||
|
||||
@@ -103,17 +103,11 @@ impl From<&ConfigClient> for SshKeyStorage {
|
||||
// Iterate over keys in storage
|
||||
for key in cfg_client.iter_ssh_keys() {
|
||||
match cfg_client.get_ssh_key(key) {
|
||||
Ok(host) => match host {
|
||||
Some((addr, username, rsa_key_path)) => {
|
||||
let key_name: String = Self::make_mapkey(&addr, &username);
|
||||
hosts.insert(key_name, rsa_key_path);
|
||||
}
|
||||
None => continue,
|
||||
},
|
||||
Err(err) => {
|
||||
error!("Failed to get SSH key for {}: {}", key, err);
|
||||
continue;
|
||||
Some((addr, username, rsa_key_path)) => {
|
||||
let key_name: String = Self::make_mapkey(&addr, &username);
|
||||
hosts.insert(key_name, rsa_key_path);
|
||||
}
|
||||
None => continue,
|
||||
}
|
||||
info!("Got SSH key for {}", key);
|
||||
}
|
||||
|
||||
@@ -30,13 +30,13 @@ impl AuthActivity {
|
||||
pub(super) fn load_bookmark(&mut self, form_tab: FormTab, idx: usize) {
|
||||
if let Some(bookmarks_cli) = self.bookmarks_client() {
|
||||
// Iterate over bookmarks
|
||||
if let Some(key) = self.bookmarks_list.get(idx) {
|
||||
if let Some(bookmark) = bookmarks_cli.get_bookmark(key) {
|
||||
// Load parameters into components
|
||||
match form_tab {
|
||||
FormTab::Remote => self.load_remote_bookmark_into_gui(bookmark),
|
||||
FormTab::HostBridge => self.load_host_bridge_bookmark_into_gui(bookmark),
|
||||
}
|
||||
if let Some(key) = self.bookmarks_list.get(idx)
|
||||
&& let Some(bookmark) = bookmarks_cli.get_bookmark(key)
|
||||
{
|
||||
// Load parameters into components
|
||||
match form_tab {
|
||||
FormTab::Remote => self.load_remote_bookmark_into_gui(bookmark),
|
||||
FormTab::HostBridge => self.load_host_bridge_bookmark_into_gui(bookmark),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,13 +99,13 @@ impl AuthActivity {
|
||||
pub(super) fn load_recent(&mut self, form_tab: FormTab, idx: usize) {
|
||||
if let Some(client) = self.bookmarks_client() {
|
||||
// Iterate over bookmarks
|
||||
if let Some(key) = self.recents_list.get(idx) {
|
||||
if let Some(bookmark) = client.get_recent(key) {
|
||||
// Load parameters
|
||||
match form_tab {
|
||||
FormTab::Remote => self.load_remote_bookmark_into_gui(bookmark),
|
||||
FormTab::HostBridge => self.load_host_bridge_bookmark_into_gui(bookmark),
|
||||
}
|
||||
if let Some(key) = self.recents_list.get(idx)
|
||||
&& let Some(bookmark) = client.get_recent(key)
|
||||
{
|
||||
// Load parameters
|
||||
match form_tab {
|
||||
FormTab::Remote => self.load_remote_bookmark_into_gui(bookmark),
|
||||
FormTab::HostBridge => self.load_host_bridge_bookmark_into_gui(bookmark),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,10 +129,10 @@ impl AuthActivity {
|
||||
|
||||
/// Write bookmarks to file
|
||||
fn write_bookmarks(&mut self) {
|
||||
if let Some(bookmarks_cli) = self.bookmarks_client() {
|
||||
if let Err(err) = bookmarks_cli.write_bookmarks() {
|
||||
self.mount_error(format!("Could not write bookmarks: {err}").as_str());
|
||||
}
|
||||
if let Some(bookmarks_cli) = self.bookmarks_client()
|
||||
&& let Err(err) = bookmarks_cli.write_bookmarks()
|
||||
{
|
||||
self.mount_error(format!("Could not write bookmarks: {err}").as_str());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -126,13 +126,13 @@ impl AuthActivity {
|
||||
self.host_bridge_protocol = protocol;
|
||||
// Update port
|
||||
let port: u16 = self.get_input_port(FormTab::HostBridge);
|
||||
if let HostBridgeProtocol::Remote(remote_protocol) = protocol {
|
||||
if Self::is_port_standard(port) {
|
||||
self.mount_port(
|
||||
FormTab::HostBridge,
|
||||
Self::get_default_port_for_protocol(remote_protocol),
|
||||
);
|
||||
}
|
||||
if let HostBridgeProtocol::Remote(remote_protocol) = protocol
|
||||
&& Self::is_port_standard(port)
|
||||
{
|
||||
self.mount_port(
|
||||
FormTab::HostBridge,
|
||||
Self::get_default_port_for_protocol(remote_protocol),
|
||||
);
|
||||
}
|
||||
}
|
||||
FormMsg::RemoteProtocolChanged(protocol) => {
|
||||
|
||||
@@ -687,30 +687,30 @@ impl AuthActivity {
|
||||
|
||||
/// mount release notes text area
|
||||
pub(super) fn mount_release_notes(&mut self) {
|
||||
if let Some(ctx) = self.context.as_ref() {
|
||||
if let Some(release_notes) = ctx.store().get_string(super::STORE_KEY_RELEASE_NOTES) {
|
||||
// make spans
|
||||
let info_color = self.theme().misc_info_dialog;
|
||||
assert!(
|
||||
self.app
|
||||
.remount(
|
||||
Id::NewVersionChangelog,
|
||||
Box::new(components::ReleaseNotes::new(release_notes, info_color)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
assert!(
|
||||
self.app
|
||||
.remount(
|
||||
Id::InstallUpdatePopup,
|
||||
Box::new(components::InstallUpdatePopup::new(info_color)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
assert!(self.app.active(&Id::InstallUpdatePopup).is_ok());
|
||||
}
|
||||
if let Some(ctx) = self.context.as_ref()
|
||||
&& let Some(release_notes) = ctx.store().get_string(super::STORE_KEY_RELEASE_NOTES)
|
||||
{
|
||||
// make spans
|
||||
let info_color = self.theme().misc_info_dialog;
|
||||
assert!(
|
||||
self.app
|
||||
.remount(
|
||||
Id::NewVersionChangelog,
|
||||
Box::new(components::ReleaseNotes::new(release_notes, info_color)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
assert!(
|
||||
self.app
|
||||
.remount(
|
||||
Id::InstallUpdatePopup,
|
||||
Box::new(components::InstallUpdatePopup::new(info_color)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
assert!(self.app.active(&Id::InstallUpdatePopup).is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use tui_realm_stdlib::Input;
|
||||
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
|
||||
@@ -158,7 +158,7 @@ impl OwnStates {
|
||||
.unwrap_or_else(|| PathBuf::from("/"));
|
||||
|
||||
// if path is `.`, then return None
|
||||
if parent == PathBuf::from(".") {
|
||||
if parent == Path::new(".") {
|
||||
return Suggestion::None;
|
||||
}
|
||||
|
||||
|
||||
@@ -506,10 +506,10 @@ impl Activity for FileTransferActivity {
|
||||
/// This function must be called once before terminating the activity.
|
||||
fn on_destroy(&mut self) -> Option<Context> {
|
||||
// Destroy cache
|
||||
if let Some(cache) = self.cache.take() {
|
||||
if let Err(err) = cache.close() {
|
||||
error!("Failed to delete cache: {}", err);
|
||||
}
|
||||
if let Some(cache) = self.cache.take()
|
||||
&& let Err(err) = cache.close()
|
||||
{
|
||||
error!("Failed to delete cache: {}", err);
|
||||
}
|
||||
// Disable raw mode
|
||||
if let Err(err) = self.context_mut().terminal().disable_raw_mode() {
|
||||
|
||||
@@ -99,27 +99,14 @@ impl SetupActivity {
|
||||
Ok(State::One(StateValue::Usize(idx))) => Some(idx),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(idx) = idx {
|
||||
let key: Option<String> = self.config().iter_ssh_keys().nth(idx).cloned();
|
||||
if let Some(key) = key {
|
||||
match self.config().get_ssh_key(&key) {
|
||||
Ok(opt) => {
|
||||
if let Some((host, username, _)) = opt {
|
||||
if let Err(err) = self.delete_ssh_key(host.as_str(), username.as_str())
|
||||
{
|
||||
// Report error
|
||||
self.mount_error(err.as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
// Report error
|
||||
self.mount_error(
|
||||
format!("Could not get ssh key \"{key}\": {err}").as_str(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// get ssh key and delete it
|
||||
if let Some(Err(err)) = idx
|
||||
.and_then(|i| self.config().iter_ssh_keys().nth(i).cloned())
|
||||
.and_then(|key| self.config().get_ssh_key(&key))
|
||||
.map(|(host, username, _)| self.delete_ssh_key(host.as_str(), username.as_str()))
|
||||
{
|
||||
// Report error
|
||||
self.mount_error(err.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -77,16 +77,11 @@ impl SetupActivity {
|
||||
Some(key) => {
|
||||
// Get key path
|
||||
match ctx.config().get_ssh_key(key) {
|
||||
Ok(ssh_key) => match ssh_key {
|
||||
None => Ok(()),
|
||||
Some((_, _, key_path)) => {
|
||||
match edit::edit_file(key_path.as_path()) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(format!("Could not edit ssh key: {err}")),
|
||||
}
|
||||
}
|
||||
None => Ok(()),
|
||||
Some((_, _, key_path)) => match edit::edit_file(key_path.as_path()) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(format!("Could not edit ssh key: {err}")),
|
||||
},
|
||||
Err(err) => Err(format!("Could not read ssh key: {err}")),
|
||||
}
|
||||
}
|
||||
None => Ok(()),
|
||||
|
||||
@@ -126,7 +126,7 @@ impl SetupActivity {
|
||||
.config()
|
||||
.iter_ssh_keys()
|
||||
.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}")
|
||||
})
|
||||
.collect();
|
||||
|
||||
Reference in New Issue
Block a user