UI code reworked

This commit is contained in:
Ultradesu
2025-07-22 20:10:44 +03:00
parent c342134f03
commit 7cc446d227
4 changed files with 667 additions and 286 deletions

View File

@@ -3,6 +3,12 @@ name = "khm"
version = "0.6.3" version = "0.6.3"
edition = "2021" edition = "2021"
authors = ["AB <ab@hexor.cy>"] authors = ["AB <ab@hexor.cy>"]
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] [dependencies]
actix-web = "4" actix-web = "4"

View File

@@ -102,7 +102,7 @@ impl ConnectionTab {
} }
/// Check for test/sync results /// 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<String>) {
// Check for test connection result // Check for test connection result
if let Some(receiver) = &self.test_result_receiver { if let Some(receiver) = &self.test_result_receiver {
if let Ok(result) = receiver.try_recv() { if let Ok(result) = receiver.try_recv() {
@@ -121,10 +121,16 @@ impl ConnectionTab {
flow: settings.flow.clone() flow: settings.flow.clone()
}; };
info!("Connection test successful: {}", message); info!("Connection test successful: {}", message);
// Add to UI log
super::ui::add_log_entry(operation_log, format!("✅ Connection test successful: {}", message));
} }
Err(error) => { Err(error) => {
self.connection_status = ConnectionStatus::Error(error); self.connection_status = ConnectionStatus::Error(error.clone());
error!("Connection test failed"); 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; self.test_result_receiver = None;
@@ -142,10 +148,16 @@ impl ConnectionTab {
let keys_count = parse_keys_count(&message); let keys_count = parse_keys_count(&message);
self.sync_status = SyncStatus::Success { keys_count }; self.sync_status = SyncStatus::Success { keys_count };
info!("Sync successful: {}", message); info!("Sync successful: {}", message);
// Add to UI log
super::ui::add_log_entry(operation_log, format!("✅ Sync completed: {}", message));
} }
Err(error) => { Err(error) => {
self.sync_status = SyncStatus::Error(error); self.sync_status = SyncStatus::Error(error.clone());
error!("Sync failed"); error!("Sync failed");
// Add to UI log
super::ui::add_log_entry(operation_log, format!("❌ Sync failed: {}", error));
} }
} }
self.sync_result_receiver = None; self.sync_result_receiver = None;

View File

@@ -2,7 +2,7 @@ use eframe::egui;
use crate::gui::common::{KhmSettings, get_config_path}; use crate::gui::common::{KhmSettings, get_config_path};
use super::connection::{ConnectionTab, ConnectionStatus, SyncStatus, save_settings_validated}; 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( pub fn render_connection_tab(
ui: &mut egui::Ui, ui: &mut egui::Ui,
ctx: &egui::Context, ctx: &egui::Context,
@@ -12,259 +12,408 @@ pub fn render_connection_tab(
operation_log: &mut Vec<String> operation_log: &mut Vec<String>
) { ) {
// Check for connection test and sync results // 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(); // Use scrollable area for the entire content
let button_area_height = 120.0; // Reserve space for buttons and status egui::ScrollArea::vertical()
let content_height = available_height - button_area_height; .auto_shrink([false; 2])
.show(ui, |ui| {
// Main content area (scrollable) ui.spacing_mut().item_spacing = egui::vec2(6.0, 8.0);
ui.allocate_ui_with_layout( ui.spacing_mut().button_padding = egui::vec2(12.0, 6.0);
[ui.available_width(), content_height].into(), ui.spacing_mut().indent = 16.0;
egui::Layout::top_down(egui::Align::Min),
|ui| { // Connection Status Card at top (full width)
egui::ScrollArea::vertical() render_connection_status_card(ui, connection_tab);
.auto_shrink([false; 2])
.show(ui, |ui| { // Main configuration area - horizontal layout
// Connection section ui.horizontal_top(|ui| {
render_connection_section(ui, settings, connection_tab); let available_width = ui.available_width();
ui.add_space(10.0); let left_panel_width = available_width * 0.6;
let right_panel_width = available_width * 0.38;
// Local settings section
render_local_settings_section(ui, settings); // Left panel - Connection and Local config
ui.add_space(15.0); ui.allocate_ui_with_layout(
[left_panel_width, ui.available_height()].into(),
// Auto-sync section egui::Layout::top_down(egui::Align::Min),
render_auto_sync_section(ui, settings, auto_sync_interval_str); |ui| {
ui.add_space(10.0); // Connection Configuration Card
render_connection_config_card(ui, settings);
// Configuration file location
render_config_location_section(ui); // Local Configuration Card
}); render_local_config_card(ui, settings);
},
);
// 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"));
}
} }
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::<u32>() { ui.add_space(8.0);
if value > 0 {
settings.auto_sync_interval_minutes = value;
}
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { // Right panel - Auto-sync and System info
if is_auto_sync_enabled { ui.allocate_ui_with_layout(
ui.label(egui::RichText::new("🔄 Enabled").color(egui::Color32::GREEN)); [right_panel_width, ui.available_height()].into(),
} else { egui::Layout::top_down(egui::Align::Min),
ui.label(egui::RichText::new("❌ Disabled").color(egui::Color32::YELLOW)); |ui| {
ui.label("(Configure host, flow & enable in-place sync)"); // 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) { /// Auto-sync configuration card
ui.group(|ui| { fn render_auto_sync_card(ui: &mut egui::Ui, settings: &mut KhmSettings, auto_sync_interval_str: &mut String) {
ui.set_min_width(ui.available_width()); 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.horizontal(|ui| {
ui.label("🗁 Config file:"); ui.label("🔄");
let config_path = get_config_path(); 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.add_sized(
[ui.available_width(), 20.0], [80.0, 26.0], // Smaller height
egui::TextEdit::singleline(&mut config_path.display().to_string()) egui::TextEdit::singleline(auto_sync_interval_str)
.interactive(false) .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::<u32>() {
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, ui: &mut egui::Ui,
ctx: &egui::Context, ctx: &egui::Context,
settings: &KhmSettings, settings: &KhmSettings,
connection_tab: &mut ConnectionTab, connection_tab: &mut ConnectionTab,
operation_log: &mut Vec<String> operation_log: &mut Vec<String>
) { ) {
let button_area_height = 120.0; ui.add_space(8.0);
ui.allocate_ui_with_layout( // Validation message
[ui.available_width(), button_area_height].into(), let save_enabled = !settings.host.is_empty() && !settings.flow.is_empty();
egui::Layout::bottom_up(egui::Align::Min), if !save_enabled {
|ui| { ui.horizontal(|ui| {
// Operation log area ui.label("⚠️");
ui.group(|ui| { ui.label(egui::RichText::new("Complete server configuration to enable saving")
ui.set_min_width(ui.available_width()); .size(12.0)
ui.vertical(|ui| { .color(egui::Color32::LIGHT_YELLOW)
ui.label(egui::RichText::new("📄 Operation Log").size(14.0).strong()); .italics());
ui.add_space(5.0); });
ui.add_space(8.0);
let log_text = operation_log.join("\n"); }
ui.add_sized(
[ui.available_width(), 60.0], // Action buttons with modern styling
egui::TextEdit::multiline(&mut log_text.clone()) render_modern_action_buttons(ui, ctx, settings, connection_tab, save_enabled, operation_log);
.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);
},
);
} }
fn render_action_buttons( /// Modern action buttons with improved styling and layout
fn render_modern_action_buttons(
ui: &mut egui::Ui, ui: &mut egui::Ui,
ctx: &egui::Context, ctx: &egui::Context,
settings: &KhmSettings, settings: &KhmSettings,
@@ -273,10 +422,23 @@ fn render_action_buttons(
operation_log: &mut Vec<String> operation_log: &mut Vec<String>
) { ) {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 8.0;
// Primary actions (left side)
if ui.add_enabled( if ui.add_enabled(
save_enabled, save_enabled,
egui::Button::new("💾 Save Settings") egui::Button::new(
.min_size(egui::vec2(120.0, 32.0)) 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() { ).clicked() {
match save_settings_validated(settings) { match save_settings_validated(settings) {
Ok(()) => { Ok(()) => {
@@ -290,44 +452,75 @@ fn render_action_buttons(
} }
if ui.add( if ui.add(
egui::Button::new("✖ Cancel") egui::Button::new(
.min_size(egui::vec2(80.0, 32.0)) 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() { ).clicked() {
ctx.send_viewport_cmd(egui::ViewportCommand::Close); ctx.send_viewport_cmd(egui::ViewportCommand::Close);
} }
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { // Spacer
let can_test = !settings.host.is_empty() && !settings.flow.is_empty() && !connection_tab.is_testing_connection; ui.add_space(ui.available_width() - 220.0);
let can_sync = !settings.host.is_empty() && !settings.flow.is_empty() && !connection_tab.is_syncing;
// Secondary actions (right side)
if ui.add_enabled( let can_test = !settings.host.is_empty() && !settings.flow.is_empty() && !connection_tab.is_testing_connection;
can_test, let can_sync = !settings.host.is_empty() && !settings.flow.is_empty() && !connection_tab.is_syncing;
egui::Button::new(
if ui.add_enabled(
can_test,
egui::Button::new(
egui::RichText::new(
if connection_tab.is_testing_connection { if connection_tab.is_testing_connection {
" Testing..." "🔄 Testing..."
} else { } else {
"🔍 Test Connection" "🔍 Test"
} }
).min_size(egui::vec2(120.0, 32.0)) )
).clicked() { .size(13.0)
add_log_entry(operation_log, "🔍 Starting connection test...".to_string()); .color(egui::Color32::WHITE)
connection_tab.start_test(settings, ctx); )
} .fill(if can_test {
egui::Color32::from_rgb(16, 124, 16)
if ui.add_enabled( } else {
can_sync, ui.visuals().widgets.inactive.bg_fill
egui::Button::new( })
.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 { if connection_tab.is_syncing {
"🔄 Syncing..." "🔄 Syncing..."
} else { } else {
"🔄 Sync Now" "🔄 Sync"
} }
).min_size(egui::vec2(100.0, 32.0)) )
).clicked() { .size(13.0)
add_log_entry(operation_log, "🔄 Starting manual sync...".to_string()); .color(egui::Color32::WHITE)
connection_tab.start_sync(settings, ctx); )
} .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);
}
}); });
} }

View File

@@ -47,25 +47,123 @@ impl eframe::App for SettingsWindow {
// Apply enhanced modern dark theme // Apply enhanced modern dark theme
apply_modern_theme(ctx); apply_modern_theme(ctx);
egui::CentralPanel::default() // Bottom panel for Activity Log (fixed at bottom)
.frame(egui::Frame::none().inner_margin(egui::Margin::same(20.0))) 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| { .show(ctx, |ui| {
// Header with title render_bottom_activity_log(ui, &mut self.operation_log);
ui.horizontal(|ui| { });
ui.heading(egui::RichText::new("🔑 KHM Settings").size(24.0));
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.horizontal(|ui| {
ui.selectable_value(&mut self.current_tab, SettingsTab::Connection, "📃 Settings"); ui.spacing_mut().item_spacing.x = 6.0;
ui.selectable_value(&mut self.current_tab, SettingsTab::Admin, "🔧 Admin");
// 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(16.0);
ui.add_space(15.0);
// Content area with proper spacing
match self.current_tab { match self.current_tab {
SettingsTab::Connection => { SettingsTab::Connection => {
render_connection_tab( 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) { fn apply_modern_theme(ctx: &egui::Context) {
let mut visuals = egui::Visuals::dark(); let mut visuals = egui::Visuals::dark();
visuals.window_fill = egui::Color32::from_gray(25);
visuals.panel_fill = egui::Color32::from_gray(30); // Modern color palette
visuals.faint_bg_color = egui::Color32::from_gray(35); visuals.window_fill = egui::Color32::from_gray(18); // Darker background
visuals.extreme_bg_color = egui::Color32::from_gray(15); 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.button_frame = true;
visuals.collapsing_header_frame = true; visuals.collapsing_header_frame = true;
visuals.indent_has_left_vline = true; visuals.indent_has_left_vline = true;
visuals.menu_rounding = egui::Rounding::same(8.0); visuals.striped = true;
visuals.window_rounding = egui::Rounding::same(12.0);
visuals.widgets.noninteractive.rounding = egui::Rounding::same(6.0); // Modern rounded corners
visuals.widgets.inactive.rounding = egui::Rounding::same(6.0); let rounding = egui::Rounding::same(8.0);
visuals.widgets.hovered.rounding = egui::Rounding::same(6.0); visuals.menu_rounding = rounding;
visuals.widgets.active.rounding = egui::Rounding::same(6.0); 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); ctx.set_visuals(visuals);
} }
/// Render bottom activity log panel
fn render_bottom_activity_log(ui: &mut egui::Ui, operation_log: &mut Vec<String>) {
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 /// Create window icon for settings window
pub fn create_window_icon() -> egui::IconData { pub fn create_window_icon() -> egui::IconData {
// Create a simple programmatic icon (blue square with white border) // 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() { pub fn run_settings_window() {
let options = eframe::NativeOptions { let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default() viewport: egui::ViewportBuilder::default()
.with_title("KHM Settings") .with_title("KHM Settings")
.with_inner_size([600.0, 800.0]) .with_inner_size([900.0, 905.0]) // Decreased height by another 15px
.with_min_inner_size([500.0, 650.0]) .with_min_inner_size([900.0, 905.0]) // Fixed size
.with_resizable(true) .with_max_inner_size([900.0, 905.0]) // Same as min - fixed size
.with_icon(create_window_icon()), .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() ..Default::default()
}; };