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

@@ -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

View File

@@ -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),

View File

@@ -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
}

View File

@@ -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(

View File

@@ -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(_) => {

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
// 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),
}
}

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.
/// 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());
}

View File

@@ -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);
}

View File

@@ -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());
}
}

View File

@@ -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) => {

View File

@@ -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());
}
}

View File

@@ -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;
}

View File

@@ -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() {

View File

@@ -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());
}
}

View File

@@ -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(()),

View File

@@ -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();