initialize new Rust project with Cargo configuration and dependencies

This commit is contained in:
2025-03-13 02:42:31 +01:00
commit ec7d93eead
4 changed files with 741 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

377
Cargo.lock generated Normal file
View File

@@ -0,0 +1,377 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "bitflags"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "console"
version = "0.15.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
dependencies = [
"encode_unicode",
"libc",
"once_cell",
"unicode-width",
"windows-sys",
]
[[package]]
name = "dialoguer"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de"
dependencies = [
"console",
"shell-words",
"tempfile",
"thiserror 1.0.69",
"zeroize",
]
[[package]]
name = "dirs"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys",
]
[[package]]
name = "encode_unicode"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]]
name = "errno"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "fastrand"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "getrandom"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
]
[[package]]
name = "getrandom"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
dependencies = [
"cfg-if",
"libc",
"wasi 0.13.3+wasi-0.2.2",
"windows-targets",
]
[[package]]
name = "libc"
version = "0.2.171"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
[[package]]
name = "libredox"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags",
"libc",
]
[[package]]
name = "linux-raw-sys"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9"
[[package]]
name = "once_cell"
version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cde51589ab56b20a6f686b2c68f7a0bd6add753d697abf720d63f8db3ab7b1ad"
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "proc-macro2"
version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_users"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
dependencies = [
"getrandom 0.2.15",
"libredox",
"thiserror 2.0.12",
]
[[package]]
name = "rustix"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7178faa4b75a30e269c71e61c353ce2748cf3d76f0c44c393f4e60abf49b825"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "shell-words"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
[[package]]
name = "ssh-profiles"
version = "0.1.0"
dependencies = [
"dialoguer",
"dirs",
]
[[package]]
name = "syn"
version = "2.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tempfile"
version = "3.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c317e0a526ee6120d8dabad239c8dadca62b24b6f168914bbbc8e2fb1f0e567"
dependencies = [
"cfg-if",
"fastrand",
"getrandom 0.3.1",
"once_cell",
"rustix",
"windows-sys",
]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl 1.0.69",
]
[[package]]
name = "thiserror"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [
"thiserror-impl 2.0.12",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "thiserror-impl"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "unicode-width"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasi"
version = "0.13.3+wasi-0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2"
dependencies = [
"wit-bindgen-rt",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "wit-bindgen-rt"
version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
dependencies = [
"bitflags",
]
[[package]]
name = "zeroize"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"

14
Cargo.toml Normal file
View File

@@ -0,0 +1,14 @@
[package]
name = "ssh-profiles"
version = "0.1.0"
edition = "2024"
[dependencies]
dialoguer = "0.11.0"
dirs = "6.0.0"
[profile.release]
strip = true
opt-level = "z"
lto = true
codegen-units = 1

349
src/main.rs Normal file
View File

@@ -0,0 +1,349 @@
use dialoguer::{Input, Select};
use std::ops::Not;
use std::path::PathBuf;
use std::{fs, io};
#[derive(Clone, Debug)]
struct SshProfile {
host: String,
hostname: String,
port: u16,
user: String,
identity_file: Option<String>,
password_authentication: Option<bool>,
}
fn get_ssh_config_path() -> PathBuf {
let home_dir = dirs::home_dir().expect("Failed to get home directory");
home_dir.join(".ssh/config")
}
fn ensure_config_exists() -> io::Result<()> {
let path = get_ssh_config_path();
if !path.exists() {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&path, "")?;
}
Ok(())
}
fn read_ssh_config() -> io::Result<String> {
ensure_config_exists()?;
let path = get_ssh_config_path();
fs::read_to_string(path)
}
fn parse_ssh_profiles(config: &str) -> Vec<SshProfile> {
let mut profiles = Vec::new();
let mut lines = config.lines();
while let Some(line) = lines.next() {
if line.starts_with("Host ") {
let host = line[5..].trim().to_string();
let mut hostname = String::new();
let mut user = String::new();
let mut port = 22;
let mut identity_file = None;
let mut password_authentication = None;
while let Some(next_line) = lines.next() {
let trimmed = next_line.trim();
if trimmed.starts_with("HostName ") {
hostname = trimmed[9..].to_string();
} else if trimmed.starts_with("User ") {
user = trimmed[5..].to_string();
} else if trimmed.starts_with("Port ") {
port = trimmed[5..].parse::<u16>().unwrap();
} else if trimmed.starts_with("IdentityFile ") {
identity_file = Some(trimmed[13..].to_string());
} else if trimmed.starts_with("PasswordAuthentication ") {
let value = trimmed[23..].to_lowercase();
match value.as_str() {
"yes" => password_authentication = Some(true),
"no" => password_authentication = Some(false),
_ => {}
}
} else if trimmed.is_empty() {
break;
}
}
profiles.push(SshProfile {
host,
hostname,
port,
user,
identity_file,
password_authentication,
});
}
}
profiles
}
fn write_ssh_config(profiles: &[SshProfile]) -> io::Result<()> {
let path = get_ssh_config_path();
let mut content = String::new();
for profile in profiles {
match (&profile.identity_file, profile.password_authentication) {
(Some(_), Some(_)) => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Profile cannot have both identity file and password authentication",
));
}
(None, None) => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Profile must have either identity file or password authentication",
));
}
(Some(identity_file), None) => {
content.push_str(&format!(
"Host {}\n HostName {}\n User {}\n Port {}\n IdentityFile {}\n\n",
profile.host, profile.hostname, profile.user, profile.port, identity_file
));
}
(None, Some(password_authentication)) => match password_authentication {
true => {
content.push_str(&format!(
"Host {}\n HostName {}\n User {}\n Port {}\n PasswordAuthentication yes\n\n",
profile.host, profile.hostname, profile.user, profile.port
));
}
false => {
content.push_str(&format!(
"Host {}\n HostName {}\n User {}\n Port {}\n PasswordAuthentication no\n\n",
profile.host, profile.hostname, profile.user, profile.port
));
}
},
}
}
fs::write(path, content)
}
fn list_profiles(profiles: &[SshProfile]) {
println!("Available SSH profiles:");
if profiles.is_empty() {
println!("No profiles found");
return;
}
for profile in profiles {
println!(
"- {} ({}@{}:{})",
profile.host, profile.user, profile.hostname, profile.port
);
}
}
fn add_profile(profiles: &mut Vec<SshProfile>) {
let host: String = Input::new()
.with_prompt("Enter Host name")
.interact_text()
.unwrap();
let hostname: String = Input::new()
.with_prompt("Enter HostName")
.interact_text()
.unwrap();
let user: String = Input::new()
.with_prompt("Enter User")
.interact_text()
.unwrap();
let port: u16 = Input::new()
.with_prompt("Enter Port")
.default(22)
.interact()
.unwrap();
let identity_file: Option<String> = Input::new()
.with_prompt("Enter IdentityFile")
.allow_empty(true)
.default(String::new())
.interact_text()
.ok()
.map(|s| s.is_empty().not().then(|| s))
.flatten();
let password_authentication: Option<bool> = Input::new()
.with_prompt("Enter PasswordAuthentication")
.allow_empty(true)
.interact()
.ok();
match (&identity_file, password_authentication) {
(Some(_), Some(_)) => {
println!("Profile cannot have both identity file and password authentication");
return;
}
(None, None) => {
println!("Profile must have either identity file or password authentication");
return;
}
_ => {}
}
profiles.push(SshProfile {
host,
hostname,
user,
port,
identity_file,
password_authentication,
});
println!("Profile added successfully");
}
fn edit_profile(profiles: &mut Vec<SshProfile>) {
let options = profiles
.iter()
.map(|profile| profile.host.clone())
.collect::<Vec<String>>();
if let Ok(selection) = Select::new()
.with_prompt("Select a profile to edit")
.items(&options)
.interact()
{
let profile = &mut profiles[selection];
let options = [
"Host",
"HostName",
"User",
"Port",
"IdentityFile",
"PasswordAuthentication",
];
let choice = Select::new()
.with_prompt("Choose a field to edit")
.items(&options)
.interact()
.unwrap();
match choice {
0 => {
let host: String = Input::new()
.with_prompt("Enter Host name")
.default(profile.host.clone())
.interact_text()
.unwrap();
profile.host = host;
}
1 => {
let hostname: String = Input::new()
.with_prompt("Enter HostName")
.default(profile.hostname.clone())
.interact_text()
.unwrap();
profile.hostname = hostname;
}
2 => {
let user: String = Input::new()
.with_prompt("Enter User")
.default(profile.user.clone())
.interact_text()
.unwrap();
profile.user = user;
}
3 => {
let port: u16 = Input::new()
.with_prompt("Enter Port")
.default(profile.port)
.interact()
.unwrap();
profile.port = port;
}
4 => {
if profile.password_authentication.is_some() {
println!("Profile already has PasswordAuthentication set");
return;
}
let identity_file: Option<String> = Input::new()
.with_prompt("Enter IdentityFile")
.default(profile.identity_file.clone().unwrap_or_default())
.interact_text()
.ok()
.map(|s| s.is_empty().not().then(|| s))
.flatten();
profile.identity_file = identity_file;
}
5 => {
if profile.identity_file.is_some() {
println!("Profile already has IdentityFile set");
return;
}
let value = profile.password_authentication.unwrap_or(false);
let password_authentication: Option<bool> = Input::new()
.with_prompt("Enter PasswordAuthentication")
.default(value)
.interact()
.ok();
profile.password_authentication = password_authentication;
}
_ => unreachable!(),
}
println!("Profile edited successfully");
}
}
fn remove_profile(profiles: &mut Vec<SshProfile>) {
let options: Vec<String> = profiles
.iter()
.map(|profile| profile.host.clone())
.collect();
if let Ok(selection) = Select::new()
.with_prompt("Select a profile to remove")
.items(&options)
.interact()
{
profiles.remove(selection);
println!("Profile removed successfully");
}
}
fn main() {
let config = read_ssh_config().expect("Failed to read SSH config");
let mut profiles = parse_ssh_profiles(&config);
loop {
let options = [
"List Profiles",
"Add Profile",
"Edit Profile",
"Remove Profile",
"Exit",
];
let choice = Select::new()
.with_prompt("Choose an action")
.items(&options)
.interact()
.unwrap();
match choice {
0 => list_profiles(&profiles),
1 => {
add_profile(&mut profiles);
write_ssh_config(&profiles).unwrap();
}
2 => {
edit_profile(&mut profiles);
write_ssh_config(&profiles).unwrap();
}
3 => {
remove_profile(&mut profiles);
write_ssh_config(&profiles).unwrap();
}
4 => break,
_ => unreachable!(),
}
}
}