mirror of
https://github.com/house-of-vanity/khm.git
synced 2025-08-21 14:27:14 +00:00
UI code reworked
This commit is contained in:
@@ -3,6 +3,12 @@ name = "khm"
|
||||
version = "0.6.3"
|
||||
edition = "2021"
|
||||
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]
|
||||
actix-web = "4"
|
||||
|
@@ -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<String>) {
|
||||
// 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;
|
||||
|
@@ -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<String>
|
||||
) {
|
||||
// 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::<u32>() {
|
||||
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::<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,
|
||||
ctx: &egui::Context,
|
||||
settings: &KhmSettings,
|
||||
connection_tab: &mut ConnectionTab,
|
||||
operation_log: &mut Vec<String>
|
||||
) {
|
||||
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<String>
|
||||
) {
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -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<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
|
||||
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()
|
||||
};
|
||||
|
||||
|
Reference in New Issue
Block a user