use super::SettingsUI;
use par_term_config::snippets::SnippetLibrary;
pub(super) fn export_snippets(settings: &mut SettingsUI) {
let path = rfd::FileDialog::new()
.set_title("Export Snippets")
.add_filter("YAML", &["yaml", "yml"])
.set_file_name("snippets.yaml")
.save_file();
if let Some(path) = path {
let library = SnippetLibrary {
snippets: settings.config.snippets.clone(),
};
let result = serde_yaml_ng::to_string(&library)
.map_err(|e| format!("could not serialize snippets: {e}"))
.and_then(|yaml| {
par_term_config::atomic_save::save_string_atomic_preserving_mode(&path, &yaml)
.map_err(|e| format!("{e:#}"))
});
match result {
Ok(()) => settings.show_info_banner(format!(
"Exported {} snippets to {}",
library.snippets.len(),
path.display()
)),
Err(e) => {
log::error!("Failed to export snippets: {e}");
settings.show_error_banner(format!(
"Could not export snippets to {}: {e}",
path.display()
));
}
}
}
}
#[derive(Debug, Default, PartialEq, Eq)]
pub(crate) struct SnippetImportSummary {
pub imported: usize,
pub duplicate_titles: Vec<String>,
pub cleared_keybinding_titles: Vec<String>,
}
impl SnippetImportSummary {
fn message(&self, source: &std::path::Path) -> String {
let mut text = format!(
"Imported {} snippets from {}.",
self.imported,
source.display()
);
if !self.duplicate_titles.is_empty() {
text.push_str(&format!(
" Skipped {} already present (same ID): {}.",
self.duplicate_titles.len(),
self.duplicate_titles.join(", ")
));
}
if !self.cleared_keybinding_titles.is_empty() {
text.push_str(&format!(
" Keybinding removed because it was already in use: {}.",
self.cleared_keybinding_titles.join(", ")
));
}
text
}
}
pub(crate) fn merge_snippet_library(
settings: &mut SettingsUI,
library: SnippetLibrary,
) -> SnippetImportSummary {
let existing_ids: std::collections::HashSet<String> = settings
.config
.snippets
.iter()
.map(|s| s.id.clone())
.collect();
let mut summary = SnippetImportSummary::default();
for mut snippet in library.snippets {
if existing_ids.contains(&snippet.id) {
summary.duplicate_titles.push(snippet.title);
continue;
}
if let Some(ref kb) = snippet.keybinding
&& settings.check_keybinding_conflict(kb, None).is_some()
{
snippet.keybinding = None;
summary
.cleared_keybinding_titles
.push(snippet.title.clone());
}
settings.config.snippets.push(snippet);
summary.imported += 1;
}
summary
}
pub(super) fn import_snippets(settings: &mut SettingsUI, changes_this_frame: &mut bool) {
let path = rfd::FileDialog::new()
.set_title("Import Snippets")
.add_filter("YAML", &["yaml", "yml"])
.pick_file();
let Some(path) = path else {
return;
};
let content =
std::fs::read_to_string(&path).map_err(|e| format!("could not read the file: {e}"));
import_snippet_text(settings, changes_this_frame, &path, content);
}
pub(crate) fn import_snippet_text(
settings: &mut SettingsUI,
changes_this_frame: &mut bool,
path: &std::path::Path,
content: Result<String, String>,
) {
let library = content.and_then(|content| {
serde_yaml_ng::from_str::<SnippetLibrary>(&content)
.map_err(|e| format!("not a snippet library: {e}"))
});
match library {
Ok(library) => {
let summary = merge_snippet_library(settings, library);
if summary.imported > 0 {
settings.has_changes = true;
*changes_this_frame = true;
}
log::info!("{}", summary.message(path));
settings.show_info_banner(summary.message(path));
}
Err(e) => {
log::error!("Failed to import snippets: {e}");
settings.show_error_banner(format!(
"Could not import snippets from {}: {e}",
path.display()
));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use par_term_config::Config;
use par_term_config::snippets::SnippetConfig;
fn snippet(id: &str, title: &str, key: Option<&str>) -> SnippetConfig {
let mut s = SnippetConfig::new(id.to_string(), title.to_string(), "echo".to_string());
s.keybinding = key.map(str::to_string);
s
}
#[test]
fn import_reports_duplicates_and_cleared_keybindings() {
let config = Config {
snippets: vec![snippet("one", "One", Some("Ctrl+Alt+J"))],
..Config::default()
};
let mut settings = SettingsUI::new_for_tests(config);
let summary = merge_snippet_library(
&mut settings,
SnippetLibrary {
snippets: vec![
snippet("one", "One again", None),
snippet("two", "Two", Some("Ctrl+Alt+J")),
snippet("three", "Three", None),
],
},
);
assert_eq!(summary.imported, 2);
assert_eq!(summary.duplicate_titles, ["One again"]);
assert_eq!(summary.cleared_keybinding_titles, ["Two"]);
assert_eq!(settings.config.snippets.len(), 3);
let message = summary.message(std::path::Path::new("lib.yaml"));
assert!(message.contains("One again") && message.contains("Two"));
}
#[test]
fn snippet_import_failures_show_an_error_banner() {
use crate::settings_ui::BannerKind;
let path = std::path::Path::new("lib.yaml");
for content in [
Err("could not read the file: denied".to_string()),
Ok("snippets: [not, a, snippet".to_string()),
] {
let mut settings = SettingsUI::new_for_tests(Config::default());
let mut changed = false;
import_snippet_text(&mut settings, &mut changed, path, content);
assert_eq!(settings.banner().map(|b| b.kind), Some(BannerKind::Error));
assert!(!changed);
}
}
}