use crate::SettingsUI;
use crate::section::{INPUT_WIDTH, collapsing_section};
use par_term_config::Config;
use std::collections::HashSet;
pub(super) fn show_import_export_section(
ui: &mut egui::Ui,
settings: &mut SettingsUI,
changes_this_frame: &mut bool,
collapsed: &mut HashSet<String>,
) {
collapsing_section(
ui,
"Import/Export Preferences",
"advanced_import_export",
true,
collapsed,
|ui| {
ui.label("Export your current configuration or import settings from a file or URL.");
ui.add_space(8.0);
ui.label(egui::RichText::new("Export").strong());
ui.add_space(4.0);
if ui
.button("Export Preferences to File")
.on_hover_text("Save the current configuration to a YAML file")
.clicked()
{
export_preferences(settings);
}
ui.add_space(12.0);
ui.label(egui::RichText::new("Import from File").strong());
ui.add_space(4.0);
ui.horizontal(|ui| {
if ui
.button("Import & Replace")
.on_hover_text("Replace the entire configuration with settings from a file")
.clicked()
{
import_preferences_from_file(settings, changes_this_frame, ImportMode::Replace);
}
if ui
.button("Import & Merge")
.on_hover_text(
"Merge settings from a file into the current configuration \
(only overrides non-default values)",
)
.clicked()
{
import_preferences_from_file(settings, changes_this_frame, ImportMode::Merge);
}
});
ui.add_space(12.0);
ui.label(egui::RichText::new("Import from URL").strong());
ui.add_space(4.0);
ui.horizontal(|ui| {
ui.label("URL:");
ui.add(
egui::TextEdit::singleline(&mut settings.temp_import_url)
.desired_width(INPUT_WIDTH)
.hint_text("https://example.com/config.yaml"),
);
});
ui.horizontal(|ui| {
let url_valid = !settings.temp_import_url.trim().is_empty()
&& (settings.temp_import_url.starts_with("http://")
|| settings.temp_import_url.starts_with("https://"));
if ui
.add_enabled(url_valid, egui::Button::new("Fetch & Replace"))
.on_hover_text("Download and replace the current configuration")
.clicked()
{
import_preferences_from_url(settings, changes_this_frame, ImportMode::Replace);
}
if ui
.add_enabled(url_valid, egui::Button::new("Fetch & Merge"))
.on_hover_text("Download and merge into the current configuration")
.clicked()
{
import_preferences_from_url(settings, changes_this_frame, ImportMode::Merge);
}
});
if let Some(ref msg) = settings.import_export_status {
ui.add_space(4.0);
let color = if settings.import_export_is_error {
egui::Color32::from_rgb(255, 100, 100)
} else {
egui::Color32::from_rgb(100, 200, 100)
};
ui.label(egui::RichText::new(msg.as_str()).color(color));
}
ui.add_space(4.0);
ui.label(
egui::RichText::new(
"Merge mode preserves your existing settings and only applies \
values that differ from defaults in the imported file.",
)
.small()
.color(egui::Color32::GRAY),
);
},
);
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum ImportMode {
Replace,
Merge,
}
fn export_preferences(settings: &mut SettingsUI) {
let path = rfd::FileDialog::new()
.set_title("Export Preferences")
.add_filter("YAML", &["yaml", "yml"])
.set_file_name("par-term-config.yaml")
.save_file();
if let Some(path) = path {
match serde_yaml_ng::to_string(&settings.config) {
Ok(yaml) => {
if let Err(e) = std::fs::write(&path, yaml) {
settings.import_export_status = Some(format!("Failed to write file: {}", e));
settings.import_export_is_error = true;
log::error!("Failed to export preferences: {}", e);
} else {
settings.import_export_status = Some(format!("Exported to {}", path.display()));
settings.import_export_is_error = false;
log::info!("Exported preferences to {}", path.display());
}
}
Err(e) => {
settings.import_export_status = Some(format!("Failed to serialize config: {}", e));
settings.import_export_is_error = true;
log::error!("Failed to serialize preferences: {}", e);
}
}
}
}
fn import_preferences_from_file(
settings: &mut SettingsUI,
changes_this_frame: &mut bool,
mode: ImportMode,
) {
let path = rfd::FileDialog::new()
.set_title("Import Preferences")
.add_filter("YAML", &["yaml", "yml"])
.pick_file();
if let Some(path) = path {
match std::fs::read_to_string(&path) {
Ok(content) => {
apply_imported_config(settings, changes_this_frame, &content, mode);
}
Err(e) => {
settings.import_export_status = Some(format!("Failed to read file: {}", e));
settings.import_export_is_error = true;
log::error!("Failed to read preferences file: {}", e);
}
}
}
}
fn import_preferences_from_url(
settings: &mut SettingsUI,
changes_this_frame: &mut bool,
mode: ImportMode,
) {
let url = settings.temp_import_url.trim().to_string();
if url.is_empty() {
return;
}
let agent = crate::http_agent();
match agent.get(&url).call() {
Ok(response) => match response.into_body().read_to_string() {
Ok(body) => {
apply_imported_config(settings, changes_this_frame, &body, mode);
}
Err(e) => {
settings.import_export_status = Some(format!("Failed to read response: {}", e));
settings.import_export_is_error = true;
log::error!("Failed to read URL response body: {}", e);
}
},
Err(e) => {
settings.import_export_status = Some(format!("Failed to fetch URL: {}", e));
settings.import_export_is_error = true;
log::error!("Failed to fetch preferences from URL: {}", e);
}
}
}
fn apply_imported_config(
settings: &mut SettingsUI,
changes_this_frame: &mut bool,
content: &str,
mode: ImportMode,
) {
match serde_yaml_ng::from_str::<Config>(content) {
Ok(imported) => {
match mode {
ImportMode::Replace => {
settings.config = imported;
}
ImportMode::Merge => {
merge_config(&mut settings.config, &imported);
}
}
settings.sync_all_temps_from_config();
settings.has_changes = true;
*changes_this_frame = true;
settings.import_export_status = Some(match mode {
ImportMode::Replace => "Configuration replaced successfully.".to_string(),
ImportMode::Merge => "Configuration merged successfully.".to_string(),
});
settings.import_export_is_error = false;
log::info!(
"Imported preferences (mode={:?})",
match mode {
ImportMode::Replace => "replace",
ImportMode::Merge => "merge",
}
);
}
Err(e) => {
settings.import_export_status = Some(format!("Invalid config file: {}", e));
settings.import_export_is_error = true;
log::error!("Failed to parse imported config: {}", e);
}
}
}
pub fn merge_config(current: &mut Config, imported: &Config) {
let defaults = Config::default();
let default_val: serde_yaml_ng::Value =
serde_yaml_ng::from_str(&serde_yaml_ng::to_string(&defaults).unwrap_or_default())
.unwrap_or(serde_yaml_ng::Value::Null);
let imported_val: serde_yaml_ng::Value =
serde_yaml_ng::from_str(&serde_yaml_ng::to_string(imported).unwrap_or_default())
.unwrap_or(serde_yaml_ng::Value::Null);
let mut current_val: serde_yaml_ng::Value =
serde_yaml_ng::from_str(&serde_yaml_ng::to_string(&*current).unwrap_or_default())
.unwrap_or(serde_yaml_ng::Value::Null);
if let (
serde_yaml_ng::Value::Mapping(ref default_map),
serde_yaml_ng::Value::Mapping(ref imported_map),
serde_yaml_ng::Value::Mapping(current_map),
) = (default_val, imported_val, &mut current_val)
{
for (key, imported_field) in imported_map {
let default_field = default_map.get(key);
if default_field != Some(imported_field) {
current_map.insert(key.clone(), imported_field.clone());
}
}
}
if let Ok(merged) = serde_yaml_ng::from_value::<Config>(current_val) {
*current = merged;
}
}