From 398e93428136faaa842310c2dbb79ef97affcf5f Mon Sep 17 00:00:00 2001 From: Cleboost Date: Sat, 13 Sep 2025 21:25:37 +0200 Subject: [PATCH 1/2] Add server deletion functionality to UI Introduces the ability to delete SSH servers from the UI. Adds `delete_ssh_server` service, a confirmation dialog for deletion, and integrates the delete button into the server card component. Special hosts are protected from deletion. --- src/service/mod.rs | 2 +- src/service/ssh_config.rs | 61 +++++++++++++++++++++++++++++++++ src/ui/component/server_card.rs | 49 +++++++++++++++++--------- src/ui/modal/delete_server.rs | 34 ++++++++++++++++++ src/ui/modal/mod.rs | 1 + 5 files changed, 129 insertions(+), 18 deletions(-) create mode 100644 src/ui/modal/delete_server.rs diff --git a/src/service/mod.rs b/src/service/mod.rs index 15401d1..31d3369 100644 --- a/src/service/mod.rs +++ b/src/service/mod.rs @@ -1,5 +1,5 @@ pub mod ssh_config; pub mod ssh_keys; -pub use ssh_config::{SshServer, load_ssh_servers}; +pub use ssh_config::{SshServer, load_ssh_servers, delete_ssh_server}; pub use ssh_keys::{delete_key_pair, load_ssh_keys, read_key_content, regenerate_public_key}; diff --git a/src/service/ssh_config.rs b/src/service/ssh_config.rs index e73c5ce..bf8b223 100644 --- a/src/service/ssh_config.rs +++ b/src/service/ssh_config.rs @@ -73,3 +73,64 @@ fn parse_ssh_config(content: &str) -> Result, Box Result<(), Box> { + let home_dir = std::env::var("HOME")?; + let config_path = Path::new(&home_dir).join(".ssh").join("config"); + + if !config_path.exists() { + return Err("SSH config file does not exist".into()); + } + + let content = fs::read_to_string(&config_path)?; + let new_content = remove_server_from_config(&content, server_name)?; + + fs::write(&config_path, new_content)?; + Ok(()) +} + +fn remove_server_from_config(content: &str, server_name: &str) -> Result> { + let lines: Vec<&str> = content.lines().collect(); + let mut new_lines = Vec::new(); + let mut skip_section = false; + let mut in_host_section = false; + + for line in lines { + let trimmed_line = line.trim(); + + // Check if this is a Host directive + if trimmed_line.to_lowercase().starts_with("host ") { + let host_name = trimmed_line[5..].trim(); + + if host_name == server_name { + // Found the server to delete, start skipping + skip_section = true; + in_host_section = true; + continue; + } else { + // Different host, stop skipping if we were + skip_section = false; + in_host_section = true; + } + } else if trimmed_line.is_empty() && in_host_section { + // Empty line after host section, stop skipping + if skip_section { + skip_section = false; + in_host_section = false; + continue; // Skip this empty line too + } + in_host_section = false; + } else if !trimmed_line.is_empty() && !trimmed_line.starts_with('#') && in_host_section { + // This is a configuration line within a host section + if skip_section { + continue; // Skip this line + } + } + + if !skip_section { + new_lines.push(line); + } + } + + Ok(new_lines.join("\n")) +} diff --git a/src/ui/component/server_card.rs b/src/ui/component/server_card.rs index 2e3a955..d4fcab0 100644 --- a/src/ui/component/server_card.rs +++ b/src/ui/component/server_card.rs @@ -1,7 +1,7 @@ use crate::service::SshServer; -use crate::ui::modal::edit_server::create_edit_server_dialog; +use crate::ui::modal::{edit_server::create_edit_server_dialog, delete_server::create_delete_server_dialog}; use gtk4::prelude::*; -use gtk4::{Box, Button, Frame, Image, Label, Orientation}; +use gtk4::{Box as GtkBox, Button, Frame, Image, Label, Orientation}; use libadwaita::prelude::AdwDialogExt; use std::process::Command; use std::rc::Rc; @@ -23,20 +23,20 @@ pub fn create_server_card( card.set_margin_bottom(8); card.set_width_request(300); - let main_container = Box::new(Orientation::Vertical, 12); + let main_container = GtkBox::new(Orientation::Vertical, 12); main_container.set_margin_top(16); main_container.set_margin_bottom(16); main_container.set_margin_start(16); main_container.set_margin_end(16); - let header_container = Box::new(Orientation::Horizontal, 12); + let header_container = GtkBox::new(Orientation::Horizontal, 12); header_container.set_halign(gtk4::Align::Start); let server_icon = Image::from_icon_name("network-server-symbolic"); server_icon.set_icon_size(gtk4::IconSize::Large); header_container.append(&server_icon); - let title_container = Box::new(Orientation::Vertical, 4); + let title_container = GtkBox::new(Orientation::Vertical, 4); title_container.set_halign(gtk4::Align::Start); title_container.set_hexpand(true); title_container.set_margin_end(16); @@ -56,7 +56,7 @@ pub fn create_server_card( let is_special_host = server.name == "aur.archlinux.org"; - let actions_container = Box::new(Orientation::Horizontal, 8); + let actions_container = GtkBox::new(Orientation::Horizontal, 8); actions_container.set_halign(gtk4::Align::End); actions_container.set_valign(gtk4::Align::Center); @@ -85,11 +85,24 @@ pub fn create_server_card( } actions_container.append(&edit_button); + let delete_button = Button::builder() + .icon_name("user-trash-symbolic") + .css_classes(vec!["circular", "flat", "destructive-action"]) + .build(); + + if is_special_host { + delete_button.set_sensitive(false); + delete_button.set_tooltip_text(Some("This host is special and can't be deleted")); + } else { + delete_button.set_tooltip_text(Some("Delete server")); + } + actions_container.append(&delete_button); + header_container.append(&actions_container); main_container.append(&header_container); if let Some(port) = server.port { - let details_container = Box::new(Orientation::Vertical, 6); + let details_container = GtkBox::new(Orientation::Vertical, 6); details_container.set_margin_top(8); let port_label = Label::new(Some(&format!("Port: {}", port))); @@ -121,10 +134,6 @@ pub fn create_server_card( match result { Ok(_) => { - println!( - "Opening SSH connection to {} in {}", - server_name_clone, terminal - ); success = true; break; } @@ -142,10 +151,6 @@ pub fn create_server_card( match fallback_result { Ok(_) => { - println!( - "Opening SSH connection to {} using fallback method", - server_name_clone - ); } Err(e) => { eprintln!("Failed to open any terminal for SSH connection: {}", e); @@ -158,8 +163,6 @@ pub fn create_server_card( let parent_window_clone = parent_window.cloned(); let on_save_clone = on_save.clone(); edit_button.connect_clicked(move |_| { - println!("Edit button clicked for server: {}", server_clone.name); - println!("on_save_clone is: {:?}", on_save_clone.is_some()); let edit_dialog = create_edit_server_dialog( &server_clone, on_save_clone.as_ref().map(|f| { @@ -174,6 +177,18 @@ pub fn create_server_card( } }); + let server_clone_for_delete = server.clone(); + let parent_window_clone_for_delete = parent_window.cloned(); + let on_save_clone_for_delete = on_save.clone(); + delete_button.connect_clicked(move |_| { + let delete_dialog = create_delete_server_dialog(&server_clone_for_delete.name, on_save_clone_for_delete.clone()); + if let Some(parent) = &parent_window_clone_for_delete { + delete_dialog.present(Some(parent)); + } else { + delete_dialog.show(); + } + }); + card.set_child(Some(&main_container)); if is_special_host { diff --git a/src/ui/modal/delete_server.rs b/src/ui/modal/delete_server.rs new file mode 100644 index 0000000..436a086 --- /dev/null +++ b/src/ui/modal/delete_server.rs @@ -0,0 +1,34 @@ +use crate::service::delete_ssh_server; +use std::rc::Rc; +use libadwaita::prelude::*; +use libadwaita::{AlertDialog, ResponseAppearance}; + +pub fn create_delete_server_dialog(server_name: &str, on_delete: Option>) -> AlertDialog { + let dialog = AlertDialog::builder() + .heading("Delete Server") + .body(&format!("Are you sure you want to delete the server \"{}\" ?\n\nThis action is irreversible.", server_name)) + .build(); + + dialog.add_response("cancel", "Cancel"); + dialog.add_response("delete", "Delete"); + + dialog.set_response_appearance("delete", ResponseAppearance::Destructive); + dialog.set_default_response(Some("cancel")); + dialog.set_close_response("cancel"); + + let server_name = server_name.to_string(); + dialog.connect_response(None, move |_, response| { + if response == "delete" { + match delete_ssh_server(&server_name) { + Ok(_) => { + if let Some(refresh_fn) = &on_delete { + refresh_fn(); + } + } + Err(e) => eprintln!("Error deleting server '{}': {}", server_name, e), + } + } + }); + + dialog +} diff --git a/src/ui/modal/mod.rs b/src/ui/modal/mod.rs index 233a52e..091601a 100644 --- a/src/ui/modal/mod.rs +++ b/src/ui/modal/mod.rs @@ -1,5 +1,6 @@ pub mod about; pub mod add_server; pub mod delete_key; +pub mod delete_server; pub mod edit_server; pub mod info_key; From b18ae40ca548280ded2f07fad25a8bac9e64bc46 Mon Sep 17 00:00:00 2001 From: Cleboost Date: Sat, 13 Sep 2025 21:30:25 +0200 Subject: [PATCH 2/2] Refactor server deletion logic and UI formatting Improves code formatting and readability in server deletion modal and server card components. Refactors the SSH config server removal function for better clarity and maintainability. No functional changes to logic; only code style and structure updates. --- src/service/mod.rs | 2 +- src/service/ssh_config.rs | 20 +++++++++----------- src/ui/component/server_card.rs | 12 ++++++++---- src/ui/modal/delete_server.rs | 12 +++++++++--- 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/src/service/mod.rs b/src/service/mod.rs index 31d3369..65d33d5 100644 --- a/src/service/mod.rs +++ b/src/service/mod.rs @@ -1,5 +1,5 @@ pub mod ssh_config; pub mod ssh_keys; -pub use ssh_config::{SshServer, load_ssh_servers, delete_ssh_server}; +pub use ssh_config::{SshServer, delete_ssh_server, load_ssh_servers}; pub use ssh_keys::{delete_key_pair, load_ssh_keys, read_key_content, regenerate_public_key}; diff --git a/src/service/ssh_config.rs b/src/service/ssh_config.rs index bf8b223..25dc738 100644 --- a/src/service/ssh_config.rs +++ b/src/service/ssh_config.rs @@ -84,12 +84,15 @@ pub fn delete_ssh_server(server_name: &str) -> Result<(), Box Result> { +fn remove_server_from_config( + content: &str, + server_name: &str, +) -> Result> { let lines: Vec<&str> = content.lines().collect(); let mut new_lines = Vec::new(); let mut skip_section = false; @@ -97,33 +100,28 @@ fn remove_server_from_config(content: &str, server_name: &str) -> Result { - } + Ok(_) => {} Err(e) => { eprintln!("Failed to open any terminal for SSH connection: {}", e); } @@ -181,7 +182,10 @@ pub fn create_server_card( let parent_window_clone_for_delete = parent_window.cloned(); let on_save_clone_for_delete = on_save.clone(); delete_button.connect_clicked(move |_| { - let delete_dialog = create_delete_server_dialog(&server_clone_for_delete.name, on_save_clone_for_delete.clone()); + let delete_dialog = create_delete_server_dialog( + &server_clone_for_delete.name, + on_save_clone_for_delete.clone(), + ); if let Some(parent) = &parent_window_clone_for_delete { delete_dialog.present(Some(parent)); } else { diff --git a/src/ui/modal/delete_server.rs b/src/ui/modal/delete_server.rs index 436a086..c5c359f 100644 --- a/src/ui/modal/delete_server.rs +++ b/src/ui/modal/delete_server.rs @@ -1,12 +1,18 @@ use crate::service::delete_ssh_server; -use std::rc::Rc; use libadwaita::prelude::*; use libadwaita::{AlertDialog, ResponseAppearance}; +use std::rc::Rc; -pub fn create_delete_server_dialog(server_name: &str, on_delete: Option>) -> AlertDialog { +pub fn create_delete_server_dialog( + server_name: &str, + on_delete: Option>, +) -> AlertDialog { let dialog = AlertDialog::builder() .heading("Delete Server") - .body(&format!("Are you sure you want to delete the server \"{}\" ?\n\nThis action is irreversible.", server_name)) + .body(&format!( + "Are you sure you want to delete the server \"{}\" ?\n\nThis action is irreversible.", + server_name + )) .build(); dialog.add_response("cancel", "Cancel");