initialize new Rust project with Cargo configuration and dependencies
This commit is contained in:
349
src/main.rs
Normal file
349
src/main.rs
Normal 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!(),
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user