diff --git a/Cargo.toml b/Cargo.toml index 8130608..a83d38d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,12 @@ name = "khm" version = "0.6.3" edition = "2021" authors = ["AB "] +description = "KHM - Known Hosts Manager for SSH key management and synchronization" +homepage = "https://github.com/house-of-vanity/khm" +repository = "https://github.com/house-of-vanity/khm" +license = "WTFPL" +keywords = ["ssh", "known-hosts", "security", "system-admin", "automation"] +categories = ["command-line-utilities", "network-programming"] [dependencies] actix-web = "4" diff --git a/src/gui/settings/connection.rs b/src/gui/settings/connection.rs index 367e7f4..83fc6be 100644 --- a/src/gui/settings/connection.rs +++ b/src/gui/settings/connection.rs @@ -102,7 +102,7 @@ impl ConnectionTab { } /// Check for test/sync results - pub fn check_results(&mut self, ctx: &egui::Context, settings: &KhmSettings) { + pub fn check_results(&mut self, ctx: &egui::Context, settings: &KhmSettings, operation_log: &mut Vec) { // Check for test connection result if let Some(receiver) = &self.test_result_receiver { if let Ok(result) = receiver.try_recv() { @@ -121,10 +121,16 @@ impl ConnectionTab { flow: settings.flow.clone() }; info!("Connection test successful: {}", message); + + // Add to UI log + super::ui::add_log_entry(operation_log, format!("✅ Connection test successful: {}", message)); } Err(error) => { - self.connection_status = ConnectionStatus::Error(error); + self.connection_status = ConnectionStatus::Error(error.clone()); error!("Connection test failed"); + + // Add to UI log + super::ui::add_log_entry(operation_log, format!("❌ Connection test failed: {}", error)); } } self.test_result_receiver = None; @@ -142,10 +148,16 @@ impl ConnectionTab { let keys_count = parse_keys_count(&message); self.sync_status = SyncStatus::Success { keys_count }; info!("Sync successful: {}", message); + + // Add to UI log + super::ui::add_log_entry(operation_log, format!("✅ Sync completed: {}", message)); } Err(error) => { - self.sync_status = SyncStatus::Error(error); + self.sync_status = SyncStatus::Error(error.clone()); error!("Sync failed"); + + // Add to UI log + super::ui::add_log_entry(operation_log, format!("❌ Sync failed: {}", error)); } } self.sync_result_receiver = None; diff --git a/src/gui/settings/ui.rs b/src/gui/settings/ui.rs index 9b493dc..58c897d 100644 --- a/src/gui/settings/ui.rs +++ b/src/gui/settings/ui.rs @@ -2,7 +2,7 @@ use eframe::egui; use crate::gui::common::{KhmSettings, get_config_path}; use super::connection::{ConnectionTab, ConnectionStatus, SyncStatus, save_settings_validated}; -/// Render connection settings tab +/// Render connection settings tab with modern horizontal UI design pub fn render_connection_tab( ui: &mut egui::Ui, ctx: &egui::Context, @@ -12,259 +12,408 @@ pub fn render_connection_tab( operation_log: &mut Vec ) { // Check for connection test and sync results - connection_tab.check_results(ctx, settings); + connection_tab.check_results(ctx, settings, operation_log); - let available_height = ui.available_height(); - let button_area_height = 120.0; // Reserve space for buttons and status - let content_height = available_height - button_area_height; - - // Main content area (scrollable) - ui.allocate_ui_with_layout( - [ui.available_width(), content_height].into(), - egui::Layout::top_down(egui::Align::Min), - |ui| { - egui::ScrollArea::vertical() - .auto_shrink([false; 2]) - .show(ui, |ui| { - // Connection section - render_connection_section(ui, settings, connection_tab); - ui.add_space(10.0); - - // Local settings section - render_local_settings_section(ui, settings); - ui.add_space(15.0); - - // Auto-sync section - render_auto_sync_section(ui, settings, auto_sync_interval_str); - ui.add_space(10.0); - - // Configuration file location - render_config_location_section(ui); - }); - }, - ); - - // Bottom area with buttons and log - render_bottom_area(ui, ctx, settings, connection_tab, operation_log); -} - -fn render_connection_section(ui: &mut egui::Ui, settings: &mut KhmSettings, connection_tab: &ConnectionTab) { - ui.group(|ui| { - ui.set_min_width(ui.available_width()); - ui.vertical(|ui| { - ui.horizontal(|ui| { - ui.label(egui::RichText::new("🌐 Connection").size(16.0).strong()); - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - // Display connection status with details - match &connection_tab.connection_status { - ConnectionStatus::Connected { keys_count, flow } => { - let status_text = if flow.is_empty() { - format!("Connected ({} keys)", keys_count) - } else { - format!("Connected to '{}' ({} keys)", flow, keys_count) - }; - ui.add_enabled(false, egui::Checkbox::new(&mut true, status_text)); - } - ConnectionStatus::Error(error_msg) => { - ui.label(egui::RichText::new("❌ Error").color(egui::Color32::RED)) - .on_hover_text(error_msg); - } - ConnectionStatus::Unknown => { - ui.add_enabled(false, egui::Checkbox::new(&mut false, "Not connected")); - } + // Use scrollable area for the entire content + egui::ScrollArea::vertical() + .auto_shrink([false; 2]) + .show(ui, |ui| { + ui.spacing_mut().item_spacing = egui::vec2(6.0, 8.0); + ui.spacing_mut().button_padding = egui::vec2(12.0, 6.0); + ui.spacing_mut().indent = 16.0; + + // Connection Status Card at top (full width) + render_connection_status_card(ui, connection_tab); + + // Main configuration area - horizontal layout + ui.horizontal_top(|ui| { + let available_width = ui.available_width(); + let left_panel_width = available_width * 0.6; + let right_panel_width = available_width * 0.38; + + // Left panel - Connection and Local config + ui.allocate_ui_with_layout( + [left_panel_width, ui.available_height()].into(), + egui::Layout::top_down(egui::Align::Min), + |ui| { + // Connection Configuration Card + render_connection_config_card(ui, settings); + + // Local Configuration Card + render_local_config_card(ui, settings); } - - if connection_tab.is_testing_connection { - ui.spinner(); - ui.label(egui::RichText::new("Testing...").italics()); - } - }); - }); - - // Display sync status if available - match &connection_tab.sync_status { - SyncStatus::Success { keys_count } => { - ui.horizontal(|ui| { - ui.label("🔄 Last sync:"); - ui.label(egui::RichText::new(format!("{} keys synced", keys_count)) - .color(egui::Color32::GREEN)); - }); - } - SyncStatus::Error(error_msg) => { - ui.horizontal(|ui| { - ui.label("🔄 Last sync:"); - ui.label(egui::RichText::new("Failed") - .color(egui::Color32::RED)) - .on_hover_text(error_msg); - }); - } - SyncStatus::Unknown => {} - } - - ui.add_space(5.0); - - egui::Grid::new("connection_grid") - .num_columns(2) - .min_col_width(120.0) - .spacing([10.0, 8.0]) - .show(ui, |ui| { - ui.label("Host URL:"); - ui.add_sized( - [ui.available_width(), 20.0], - egui::TextEdit::singleline(&mut settings.host) - .hint_text("https://your-khm-server.com") - ); - ui.end_row(); - - ui.label("Flow Name:"); - ui.add_sized( - [ui.available_width(), 20.0], - egui::TextEdit::singleline(&mut settings.flow) - .hint_text("production, staging, etc.") - ); - ui.end_row(); - - ui.label("Basic Auth:"); - ui.add_sized( - [ui.available_width(), 20.0], - egui::TextEdit::singleline(&mut settings.basic_auth) - .hint_text("username:password (optional)") - .password(true) - ); - ui.end_row(); - }); - }); - }); -} - -fn render_local_settings_section(ui: &mut egui::Ui, settings: &mut KhmSettings) { - ui.group(|ui| { - ui.set_min_width(ui.available_width()); - ui.vertical(|ui| { - ui.label(egui::RichText::new("📁 Local Settings").size(16.0).strong()); - ui.add_space(8.0); - - egui::Grid::new("local_grid") - .num_columns(2) - .min_col_width(120.0) - .spacing([10.0, 8.0]) - .show(ui, |ui| { - ui.label("Known Hosts File:"); - ui.add_sized( - [ui.available_width(), 20.0], - egui::TextEdit::singleline(&mut settings.known_hosts) - .hint_text("~/.ssh/known_hosts") - ); - ui.end_row(); - }); - - ui.add_space(8.0); - ui.checkbox(&mut settings.in_place, "✏ Update known_hosts file in-place after sync"); - }); - }); -} - -fn render_auto_sync_section(ui: &mut egui::Ui, settings: &mut KhmSettings, auto_sync_interval_str: &mut String) { - ui.group(|ui| { - ui.set_min_width(ui.available_width()); - ui.vertical(|ui| { - ui.label(egui::RichText::new("🔄 Auto Sync").size(16.0).strong()); - ui.add_space(8.0); - - let is_auto_sync_enabled = !settings.host.is_empty() - && !settings.flow.is_empty() - && settings.in_place; - - ui.horizontal(|ui| { - ui.label("Interval (minutes):"); - ui.add_sized( - [80.0, 20.0], - egui::TextEdit::singleline(auto_sync_interval_str) ); - if let Ok(value) = auto_sync_interval_str.parse::() { - if value > 0 { - settings.auto_sync_interval_minutes = value; - } - } + ui.add_space(8.0); - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if is_auto_sync_enabled { - ui.label(egui::RichText::new("🔄 Enabled").color(egui::Color32::GREEN)); - } else { - ui.label(egui::RichText::new("❌ Disabled").color(egui::Color32::YELLOW)); - ui.label("(Configure host, flow & enable in-place sync)"); + // Right panel - Auto-sync and System info + ui.allocate_ui_with_layout( + [right_panel_width, ui.available_height()].into(), + egui::Layout::top_down(egui::Align::Min), + |ui| { + // Auto-sync Configuration Card + render_auto_sync_card(ui, settings, auto_sync_interval_str); + + // System Information Card + render_system_info_card(ui); } + ); + }); + + ui.add_space(12.0); + + // Action buttons at bottom + render_action_section(ui, ctx, settings, connection_tab, operation_log); + }); +} + +/// Connection status card with modern visual design +fn render_connection_status_card(ui: &mut egui::Ui, connection_tab: &ConnectionTab) { + let frame = egui::Frame::group(ui.style()) + .fill(ui.visuals().faint_bg_color) + .stroke(egui::Stroke::new(1.0, ui.visuals().widgets.noninteractive.bg_stroke.color)) + .rounding(6.0) + .inner_margin(egui::Margin::same(12.0)); + + frame.show(ui, |ui| { + // Header with status indicator + ui.horizontal(|ui| { + let (status_icon, status_text, status_color) = match &connection_tab.connection_status { + ConnectionStatus::Connected { keys_count, flow } => { + let text = if flow.is_empty() { + format!("Connected • {} keys", keys_count) + } else { + format!("Connected to '{}' • {} keys", flow, keys_count) + }; + ("🟢", text, egui::Color32::GREEN) + } + ConnectionStatus::Error(error_msg) => { + ("🔴", format!("Connection Error: {}", error_msg), egui::Color32::RED) + } + ConnectionStatus::Unknown => { + ("⚫", "Not Connected".to_string(), ui.visuals().text_color()) + } + }; + + ui.label(egui::RichText::new(status_icon).size(14.0)); + ui.label(egui::RichText::new("Connection Status").size(14.0).strong()); + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if connection_tab.is_testing_connection { + ui.spinner(); + ui.label(egui::RichText::new("Testing...").italics().color(ui.visuals().weak_text_color())); + } else { + ui.label(egui::RichText::new(&status_text).size(13.0).color(status_color)); + } + }); + }); + + // Sync status - always visible + ui.add_space(6.0); + ui.separator(); + ui.add_space(6.0); + + ui.horizontal(|ui| { + ui.label("🔄"); + ui.label("Last Sync:"); + + match &connection_tab.sync_status { + SyncStatus::Success { keys_count } => { + ui.label(egui::RichText::new(format!("✅ {} keys synced", keys_count)) + .size(13.0).color(egui::Color32::GREEN)); + } + SyncStatus::Error(error_msg) => { + ui.label(egui::RichText::new("❌ Failed") + .size(13.0).color(egui::Color32::RED)) + .on_hover_text(error_msg); + } + SyncStatus::Unknown => { + ui.label(egui::RichText::new("No sync performed yet") + .size(13.0).color(ui.visuals().weak_text_color())); + } + } + }); + }); + + ui.add_space(8.0); +} + +/// Connection configuration card with input fields +fn render_connection_config_card(ui: &mut egui::Ui, settings: &mut KhmSettings) { + let frame = egui::Frame::group(ui.style()) + .fill(ui.visuals().faint_bg_color) + .stroke(egui::Stroke::new(1.0, ui.visuals().widgets.noninteractive.bg_stroke.color)) + .rounding(6.0) + .inner_margin(egui::Margin::same(12.0)); + + frame.show(ui, |ui| { + // Header + ui.horizontal(|ui| { + ui.label("🌐"); + ui.label(egui::RichText::new("Server Configuration").size(14.0).strong()); + }); + + ui.add_space(8.0); + + // Input fields with better spacing + ui.vertical(|ui| { + ui.spacing_mut().item_spacing.y = 8.0; + + // Host URL + ui.vertical(|ui| { + ui.label(egui::RichText::new("Host URL").size(13.0).strong()); + ui.add_space(3.0); + ui.add_sized( + [ui.available_width(), 28.0], // Smaller height for better centering + egui::TextEdit::singleline(&mut settings.host) + .hint_text("https://your-khm-server.com") + .font(egui::FontId::new(14.0, egui::FontFamily::Monospace)) + .margin(egui::Margin::symmetric(8.0, 6.0)) // Better vertical centering + ); + }); + + // Flow Name + ui.vertical(|ui| { + ui.label(egui::RichText::new("Flow Name").size(13.0).strong()); + ui.add_space(3.0); + ui.add_sized( + [ui.available_width(), 28.0], + egui::TextEdit::singleline(&mut settings.flow) + .hint_text("production, staging, development") + .font(egui::FontId::new(14.0, egui::FontFamily::Proportional)) + .margin(egui::Margin::symmetric(8.0, 6.0)) + ); + }); + + // Basic Auth (optional) + ui.vertical(|ui| { + ui.horizontal(|ui| { + ui.label(egui::RichText::new("Basic Authentication").size(13.0).strong()); + ui.label(egui::RichText::new("(optional)").size(12.0).weak().italics()); + }); + ui.add_space(3.0); + ui.add_sized( + [ui.available_width(), 28.0], + egui::TextEdit::singleline(&mut settings.basic_auth) + .hint_text("username:password") + .password(true) + .font(egui::FontId::new(14.0, egui::FontFamily::Monospace)) + .margin(egui::Margin::symmetric(8.0, 6.0)) + ); + }); + }); + }); + + ui.add_space(8.0); +} + +/// Local configuration card +fn render_local_config_card(ui: &mut egui::Ui, settings: &mut KhmSettings) { + let frame = egui::Frame::group(ui.style()) + .fill(ui.visuals().faint_bg_color) + .stroke(egui::Stroke::new(1.0, ui.visuals().widgets.noninteractive.bg_stroke.color)) + .rounding(6.0) + .inner_margin(egui::Margin::same(12.0)); + + frame.show(ui, |ui| { + // Header + ui.horizontal(|ui| { + ui.label("📁"); + ui.label(egui::RichText::new("Local Configuration").size(14.0).strong()); + }); + + ui.add_space(8.0); + + // Known hosts file + ui.vertical(|ui| { + ui.label(egui::RichText::new("Known Hosts File Path").size(13.0).strong()); + ui.add_space(3.0); + ui.add_sized( + [ui.available_width(), 28.0], + egui::TextEdit::singleline(&mut settings.known_hosts) + .hint_text("~/.ssh/known_hosts") + .font(egui::FontId::new(14.0, egui::FontFamily::Monospace)) + .margin(egui::Margin::symmetric(8.0, 6.0)) + ); + + ui.add_space(8.0); + + // In-place update option with better styling + ui.horizontal(|ui| { + ui.checkbox(&mut settings.in_place, ""); + ui.vertical(|ui| { + ui.label(egui::RichText::new("Update file in-place after sync").size(13.0).strong()); + ui.label(egui::RichText::new("Automatically modify the known_hosts file when synchronizing").size(12.0).weak().italics()); }); }); }); }); + + ui.add_space(8.0); } -fn render_config_location_section(ui: &mut egui::Ui) { - ui.group(|ui| { - ui.set_min_width(ui.available_width()); +/// Auto-sync configuration card +fn render_auto_sync_card(ui: &mut egui::Ui, settings: &mut KhmSettings, auto_sync_interval_str: &mut String) { + let frame = egui::Frame::group(ui.style()) + .fill(ui.visuals().faint_bg_color) + .stroke(egui::Stroke::new(1.0, ui.visuals().widgets.noninteractive.bg_stroke.color)) + .rounding(6.0) + .inner_margin(egui::Margin::same(12.0)); + + frame.show(ui, |ui| { + let is_auto_sync_enabled = !settings.host.is_empty() + && !settings.flow.is_empty() + && settings.in_place; + + // Header with status ui.horizontal(|ui| { - ui.label("🗁 Config file:"); - let config_path = get_config_path(); + ui.label("🔄"); + ui.label(egui::RichText::new("Auto Sync").size(14.0).strong()); + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + let (status_text, status_color) = if is_auto_sync_enabled { + ("● Active", egui::Color32::GREEN) + } else { + ("○ Inactive", egui::Color32::from_gray(128)) + }; + + ui.label(egui::RichText::new(status_text).size(12.0).color(status_color)); + }); + }); + + ui.add_space(8.0); + + // Interval setting + ui.horizontal(|ui| { + ui.label(egui::RichText::new("Interval").size(13.0).strong()); + ui.add_space(6.0); ui.add_sized( - [ui.available_width(), 20.0], - egui::TextEdit::singleline(&mut config_path.display().to_string()) - .interactive(false) - ); + [80.0, 26.0], // Smaller height + egui::TextEdit::singleline(auto_sync_interval_str) + .font(egui::FontId::new(14.0, egui::FontFamily::Monospace)) + .margin(egui::Margin::symmetric(6.0, 5.0)) + ); + ui.label("min"); + + // Update the actual setting + if let Ok(value) = auto_sync_interval_str.parse::() { + if value > 0 { + settings.auto_sync_interval_minutes = value; + } + } + }); + + // Requirements - always visible + ui.add_space(8.0); + ui.separator(); + ui.add_space(8.0); + + ui.vertical(|ui| { + ui.label(egui::RichText::new("Requirements:").size(12.0).strong()); + ui.add_space(3.0); + + let host_ok = !settings.host.is_empty(); + let flow_ok = !settings.flow.is_empty(); + let in_place_ok = settings.in_place; + + ui.horizontal(|ui| { + let (icon, color) = if host_ok { ("✅", egui::Color32::GREEN) } else { ("❌", egui::Color32::RED) }; + ui.label(egui::RichText::new(icon).color(color)); + ui.label(egui::RichText::new("Host URL").size(11.0)); + }); + + ui.horizontal(|ui| { + let (icon, color) = if flow_ok { ("✅", egui::Color32::GREEN) } else { ("❌", egui::Color32::RED) }; + ui.label(egui::RichText::new(icon).color(color)); + ui.label(egui::RichText::new("Flow name").size(11.0)); + }); + + ui.horizontal(|ui| { + let (icon, color) = if in_place_ok { ("✅", egui::Color32::GREEN) } else { ("❌", egui::Color32::RED) }; + ui.label(egui::RichText::new(icon).color(color)); + ui.label(egui::RichText::new("In-place update").size(11.0)); + }); }); }); + + ui.add_space(8.0); } -fn render_bottom_area( +/// System information card +fn render_system_info_card(ui: &mut egui::Ui) { + let frame = egui::Frame::group(ui.style()) + .fill(ui.visuals().extreme_bg_color) + .stroke(egui::Stroke::new(1.0, ui.visuals().widgets.noninteractive.bg_stroke.color)) + .rounding(6.0) + .inner_margin(egui::Margin::same(12.0)); + + frame.show(ui, |ui| { + // Header + ui.horizontal(|ui| { + ui.label("⚙️"); + ui.label(egui::RichText::new("System Info").size(14.0).strong()); + }); + + ui.add_space(8.0); + + // Config file location + ui.vertical(|ui| { + ui.label(egui::RichText::new("Config File").size(13.0).strong()); + ui.add_space(3.0); + + let config_path = get_config_path(); + let path_str = config_path.display().to_string(); + + ui.vertical(|ui| { + ui.add_sized( + [ui.available_width(), 26.0], // Smaller height + egui::TextEdit::singleline(&mut path_str.clone()) + .interactive(false) + .font(egui::FontId::new(12.0, egui::FontFamily::Monospace)) + .margin(egui::Margin::symmetric(8.0, 5.0)) + ); + + ui.add_space(4.0); + + if ui.small_button("📋 Copy Path").clicked() { + ui.output_mut(|o| o.copied_text = path_str); + } + }); + }); + }); + + ui.add_space(8.0); +} + +/// Action section with buttons only (Activity Log moved to bottom panel) +fn render_action_section( ui: &mut egui::Ui, ctx: &egui::Context, settings: &KhmSettings, connection_tab: &mut ConnectionTab, operation_log: &mut Vec ) { - let button_area_height = 120.0; + ui.add_space(8.0); - ui.allocate_ui_with_layout( - [ui.available_width(), button_area_height].into(), - egui::Layout::bottom_up(egui::Align::Min), - |ui| { - // Operation log area - ui.group(|ui| { - ui.set_min_width(ui.available_width()); - ui.vertical(|ui| { - ui.label(egui::RichText::new("📄 Operation Log").size(14.0).strong()); - ui.add_space(5.0); - - let log_text = operation_log.join("\n"); - ui.add_sized( - [ui.available_width(), 60.0], - egui::TextEdit::multiline(&mut log_text.clone()) - .font(egui::FontId::monospace(10.0)) - .interactive(false) - ); - }); - }); - - ui.add_space(8.0); - - // Show validation hints - let save_enabled = !settings.host.is_empty() && !settings.flow.is_empty(); - if !save_enabled { - ui.label(egui::RichText::new("❗ Please fill in Host URL and Flow Name to save settings") - .color(egui::Color32::YELLOW) - .italics()); - } - - ui.add_space(5.0); - - // Action buttons - render_action_buttons(ui, ctx, settings, connection_tab, save_enabled, operation_log); - }, - ); + // Validation message + let save_enabled = !settings.host.is_empty() && !settings.flow.is_empty(); + if !save_enabled { + ui.horizontal(|ui| { + ui.label("⚠️"); + ui.label(egui::RichText::new("Complete server configuration to enable saving") + .size(12.0) + .color(egui::Color32::LIGHT_YELLOW) + .italics()); + }); + ui.add_space(8.0); + } + + // Action buttons with modern styling + render_modern_action_buttons(ui, ctx, settings, connection_tab, save_enabled, operation_log); } -fn render_action_buttons( +/// Modern action buttons with improved styling and layout +fn render_modern_action_buttons( ui: &mut egui::Ui, ctx: &egui::Context, settings: &KhmSettings, @@ -273,10 +422,23 @@ fn render_action_buttons( operation_log: &mut Vec ) { ui.horizontal(|ui| { + ui.spacing_mut().item_spacing.x = 8.0; + + // Primary actions (left side) if ui.add_enabled( save_enabled, - egui::Button::new("💾 Save Settings") - .min_size(egui::vec2(120.0, 32.0)) + egui::Button::new( + egui::RichText::new("💾 Save & Close") + .size(13.0) + .color(egui::Color32::WHITE) + ) + .fill(if save_enabled { + egui::Color32::from_rgb(0, 120, 212) + } else { + ui.visuals().widgets.inactive.bg_fill + }) + .min_size(egui::vec2(120.0, 32.0)) + .rounding(6.0) ).clicked() { match save_settings_validated(settings) { Ok(()) => { @@ -290,44 +452,75 @@ fn render_action_buttons( } if ui.add( - egui::Button::new("✖ Cancel") - .min_size(egui::vec2(80.0, 32.0)) + egui::Button::new( + egui::RichText::new("✖ Cancel") + .size(13.0) + .color(ui.visuals().text_color()) + ) + .stroke(egui::Stroke::new(1.0, ui.visuals().text_color())) + .fill(egui::Color32::TRANSPARENT) + .min_size(egui::vec2(80.0, 32.0)) + .rounding(6.0) ).clicked() { ctx.send_viewport_cmd(egui::ViewportCommand::Close); } - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - let can_test = !settings.host.is_empty() && !settings.flow.is_empty() && !connection_tab.is_testing_connection; - let can_sync = !settings.host.is_empty() && !settings.flow.is_empty() && !connection_tab.is_syncing; - - if ui.add_enabled( - can_test, - egui::Button::new( + // Spacer + ui.add_space(ui.available_width() - 220.0); + + // Secondary actions (right side) + let can_test = !settings.host.is_empty() && !settings.flow.is_empty() && !connection_tab.is_testing_connection; + let can_sync = !settings.host.is_empty() && !settings.flow.is_empty() && !connection_tab.is_syncing; + + if ui.add_enabled( + can_test, + egui::Button::new( + egui::RichText::new( if connection_tab.is_testing_connection { - "▶ Testing..." + "🔄 Testing..." } else { - "🔍 Test Connection" + "🔍 Test" } - ).min_size(egui::vec2(120.0, 32.0)) - ).clicked() { - add_log_entry(operation_log, "🔍 Starting connection test...".to_string()); - connection_tab.start_test(settings, ctx); - } - - if ui.add_enabled( - can_sync, - egui::Button::new( + ) + .size(13.0) + .color(egui::Color32::WHITE) + ) + .fill(if can_test { + egui::Color32::from_rgb(16, 124, 16) + } else { + ui.visuals().widgets.inactive.bg_fill + }) + .min_size(egui::vec2(80.0, 32.0)) + .rounding(6.0) + ).on_hover_text("Test server connection").clicked() { + add_log_entry(operation_log, "🔍 Testing connection...".to_string()); + connection_tab.start_test(settings, ctx); + } + + if ui.add_enabled( + can_sync, + egui::Button::new( + egui::RichText::new( if connection_tab.is_syncing { "🔄 Syncing..." } else { - "🔄 Sync Now" + "🔄 Sync" } - ).min_size(egui::vec2(100.0, 32.0)) - ).clicked() { - add_log_entry(operation_log, "🔄 Starting manual sync...".to_string()); - connection_tab.start_sync(settings, ctx); - } - }); + ) + .size(13.0) + .color(egui::Color32::WHITE) + ) + .fill(if can_sync { + egui::Color32::from_rgb(255, 140, 0) + } else { + ui.visuals().widgets.inactive.bg_fill + }) + .min_size(egui::vec2(80.0, 32.0)) + .rounding(6.0) + ).on_hover_text("Synchronize SSH keys now").clicked() { + add_log_entry(operation_log, "🔄 Starting sync...".to_string()); + connection_tab.start_sync(settings, ctx); + } }); } diff --git a/src/gui/settings/window.rs b/src/gui/settings/window.rs index 4dfba7a..c69eafc 100644 --- a/src/gui/settings/window.rs +++ b/src/gui/settings/window.rs @@ -47,25 +47,123 @@ impl eframe::App for SettingsWindow { // Apply enhanced modern dark theme apply_modern_theme(ctx); - egui::CentralPanel::default() - .frame(egui::Frame::none().inner_margin(egui::Margin::same(20.0))) + // Bottom panel for Activity Log (fixed at bottom) + egui::TopBottomPanel::bottom("activity_log_panel") + .resizable(false) + .min_height(140.0) + .max_height(140.0) + .frame(egui::Frame::none() + .fill(egui::Color32::from_gray(12)) + .stroke(egui::Stroke::new(1.0, egui::Color32::from_gray(60))) + ) .show(ctx, |ui| { - // Header with title - ui.horizontal(|ui| { - ui.heading(egui::RichText::new("🔑 KHM Settings").size(24.0)); + render_bottom_activity_log(ui, &mut self.operation_log); + }); + + egui::CentralPanel::default() + .frame(egui::Frame::none() + .fill(egui::Color32::from_gray(18)) + .inner_margin(egui::Margin::same(20.0)) + ) + .show(ctx, |ui| { + // Modern header with gradient-like styling + let header_frame = egui::Frame::none() + .fill(ui.visuals().panel_fill) + .rounding(egui::Rounding::same(8.0)) + .inner_margin(egui::Margin::same(12.0)) + .stroke(egui::Stroke::new(1.0, ui.visuals().widgets.noninteractive.bg_stroke.color)); + + header_frame.show(ui, |ui| { + ui.horizontal(|ui| { + ui.add_space(4.0); + ui.label("🔑"); + ui.heading(egui::RichText::new("KHM Settings").size(20.0).strong()); + ui.label(egui::RichText::new( + "(Known Hosts Manager for SSH key management and synchronization)" + ).size(11.0).weak().italics()); + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + // Version from Cargo.toml + let version = env!("CARGO_PKG_VERSION"); + if ui.small_button(format!("v{}", version)) + .on_hover_text(format!( + "{}\n{}\nRepository: {}\nLicense: {}", + env!("CARGO_PKG_DESCRIPTION"), + env!("CARGO_PKG_AUTHORS"), + env!("CARGO_PKG_REPOSITORY"), + "WTFPL" + )) + .clicked() + { + // Open repository URL + if let Err(_) = std::process::Command::new("open") + .arg(env!("CARGO_PKG_REPOSITORY")) + .spawn() + { + // Fallback for non-macOS systems + let _ = std::process::Command::new("xdg-open") + .arg(env!("CARGO_PKG_REPOSITORY")) + .spawn(); + } + } + }); + }); }); - ui.add_space(10.0); + ui.add_space(12.0); - // Tab selector + // Modern tab selector with card styling ui.horizontal(|ui| { - ui.selectable_value(&mut self.current_tab, SettingsTab::Connection, "📃 Settings"); - ui.selectable_value(&mut self.current_tab, SettingsTab::Admin, "🔧 Admin"); + ui.spacing_mut().item_spacing.x = 6.0; + + // Connection/Settings Tab + let connection_selected = matches!(self.current_tab, SettingsTab::Connection); + let connection_button = egui::Button::new( + egui::RichText::new("🌐 Connection").size(13.0) + ) + .fill(if connection_selected { + egui::Color32::from_rgb(0, 120, 212) + } else { + ui.visuals().widgets.inactive.bg_fill + }) + .stroke(if connection_selected { + egui::Stroke::new(1.0, egui::Color32::from_rgb(0, 120, 212)) + } else { + egui::Stroke::new(1.0, ui.visuals().widgets.noninteractive.bg_stroke.color) + }) + .rounding(6.0) + .min_size(egui::vec2(110.0, 32.0)); + + if ui.add(connection_button).clicked() { + self.current_tab = SettingsTab::Connection; + } + + // Admin Tab + let admin_selected = matches!(self.current_tab, SettingsTab::Admin); + let admin_button = egui::Button::new( + egui::RichText::new("🔧 Admin Panel").size(13.0) + ) + .fill(if admin_selected { + egui::Color32::from_rgb(120, 80, 0) + } else { + ui.visuals().widgets.inactive.bg_fill + }) + .stroke(if admin_selected { + egui::Stroke::new(1.0, egui::Color32::from_rgb(120, 80, 0)) + } else { + egui::Stroke::new(1.0, ui.visuals().widgets.noninteractive.bg_stroke.color) + }) + .rounding(6.0) + .min_size(egui::vec2(110.0, 32.0)); + + if ui.add(admin_button).clicked() { + self.current_tab = SettingsTab::Admin; + } }); - ui.separator(); - ui.add_space(15.0); + ui.add_space(16.0); + // Content area with proper spacing match self.current_tab { SettingsTab::Connection => { render_connection_tab( @@ -352,25 +450,93 @@ impl SettingsWindow { } } -/// Apply modern dark theme for the settings window +/// Apply modern dark theme for the settings window with enhanced styling fn apply_modern_theme(ctx: &egui::Context) { let mut visuals = egui::Visuals::dark(); - visuals.window_fill = egui::Color32::from_gray(25); - visuals.panel_fill = egui::Color32::from_gray(30); - visuals.faint_bg_color = egui::Color32::from_gray(35); - visuals.extreme_bg_color = egui::Color32::from_gray(15); + + // Modern color palette + visuals.window_fill = egui::Color32::from_gray(18); // Darker background + visuals.panel_fill = egui::Color32::from_gray(24); // Panel background + visuals.faint_bg_color = egui::Color32::from_gray(32); // Card background + visuals.extreme_bg_color = egui::Color32::from_gray(12); // Darkest areas + + // Enhanced widget styling visuals.button_frame = true; visuals.collapsing_header_frame = true; visuals.indent_has_left_vline = true; - visuals.menu_rounding = egui::Rounding::same(8.0); - visuals.window_rounding = egui::Rounding::same(12.0); - visuals.widgets.noninteractive.rounding = egui::Rounding::same(6.0); - visuals.widgets.inactive.rounding = egui::Rounding::same(6.0); - visuals.widgets.hovered.rounding = egui::Rounding::same(6.0); - visuals.widgets.active.rounding = egui::Rounding::same(6.0); + visuals.striped = true; + + // Modern rounded corners + let rounding = egui::Rounding::same(8.0); + visuals.menu_rounding = rounding; + visuals.window_rounding = egui::Rounding::same(16.0); + visuals.widgets.noninteractive.rounding = rounding; + visuals.widgets.inactive.rounding = rounding; + visuals.widgets.hovered.rounding = rounding; + visuals.widgets.active.rounding = rounding; + + // Better widget colors + visuals.widgets.noninteractive.bg_fill = egui::Color32::from_gray(40); + visuals.widgets.inactive.bg_fill = egui::Color32::from_gray(45); + visuals.widgets.hovered.bg_fill = egui::Color32::from_gray(55); + visuals.widgets.active.bg_fill = egui::Color32::from_gray(60); + + // Subtle borders + let border_color = egui::Color32::from_gray(60); + visuals.widgets.noninteractive.bg_stroke = egui::Stroke::new(1.0, border_color); + visuals.widgets.inactive.bg_stroke = egui::Stroke::new(1.0, border_color); + visuals.widgets.hovered.bg_stroke = egui::Stroke::new(1.5, egui::Color32::from_gray(80)); + visuals.widgets.active.bg_stroke = egui::Stroke::new(1.5, egui::Color32::from_gray(100)); + ctx.set_visuals(visuals); } +/// Render bottom activity log panel +fn render_bottom_activity_log(ui: &mut egui::Ui, operation_log: &mut Vec) { + ui.add_space(18.0); // Larger top padding + + ui.horizontal(|ui| { + ui.add_space(8.0); + ui.label("📋"); + ui.label(egui::RichText::new("Activity Log").size(13.0).strong()); + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| { + ui.add_space(8.0); + if ui.small_button("🗑 Clear").clicked() { + operation_log.clear(); + } + }); + }); + + ui.add_space(8.0); + + // Add horizontal margin for the text area + ui.horizontal(|ui| { + ui.add_space(8.0); // Left margin + + // Show last 5 log entries in multiline text + let log_text = if operation_log.is_empty() { + "No recent activity".to_string() + } else { + let start_idx = if operation_log.len() > 5 { + operation_log.len() - 5 + } else { + 0 + }; + operation_log[start_idx..].join("\n") + }; + + ui.add_sized( + [ui.available_width() - 8.0, 80.0], // Account for right margin + egui::TextEdit::multiline(&mut log_text.clone()) + .font(egui::FontId::new(11.0, egui::FontFamily::Monospace)) + .interactive(false) + ); + + ui.add_space(8.0); // Right margin + }); +} + /// Create window icon for settings window pub fn create_window_icon() -> egui::IconData { // Create a simple programmatic icon (blue square with white border) @@ -394,15 +560,19 @@ pub fn create_window_icon() -> egui::IconData { } } -/// Run the settings window application +/// Run the settings window application with modern horizontal styling pub fn run_settings_window() { let options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() .with_title("KHM Settings") - .with_inner_size([600.0, 800.0]) - .with_min_inner_size([500.0, 650.0]) - .with_resizable(true) - .with_icon(create_window_icon()), + .with_inner_size([900.0, 905.0]) // Decreased height by another 15px + .with_min_inner_size([900.0, 905.0]) // Fixed size + .with_max_inner_size([900.0, 905.0]) // Same as min - fixed size + .with_resizable(false) // Disable resizing since window is fixed size + .with_icon(create_window_icon()) + .with_decorations(true) + .with_transparent(false), + centered: true, ..Default::default() };