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

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!(),
}
}
}