use par_term_config::Config;
use super::SettingsUI;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BannerKind {
Error,
Info,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Banner {
pub kind: BannerKind,
pub text: String,
}
impl SettingsUI {
pub fn show_error_banner(&mut self, text: impl Into<String>) {
self.banner = Some(Banner {
kind: BannerKind::Error,
text: text.into(),
});
}
pub fn show_info_banner(&mut self, text: impl Into<String>) {
self.banner = Some(Banner {
kind: BannerKind::Info,
text: text.into(),
});
}
pub fn banner(&self) -> Option<&Banner> {
self.banner.as_ref()
}
pub fn report_config_save(&mut self, result: Result<(), String>) {
let previous = self.config_save_rollback.take();
match result {
Ok(()) => {
if let Some(previous) = previous {
self.announce_restart_needed(&previous);
}
}
Err(error) => {
if let Some(previous) = previous {
self.baseline_config = previous;
}
self.close_pending = false;
self.show_error_banner(format!(
"Could not save {}: {error}. Your changes are still unsaved.",
Config::config_path().display()
));
}
}
}
pub fn report_profile_save(&mut self, result: Result<(), String>) {
let previous = self.profile_save_rollback.take();
if let Err(error) = result {
if let Some(previous) = previous {
self.profile_modal_ui.restore_baseline(previous);
}
self.close_pending = false;
self.show_error_banner(format!(
"Could not save profiles: {error}. Your profile changes are still unsaved."
));
}
}
pub fn config_changed_on_disk(&mut self, on_disk: Config) {
if super::configs_equal(&on_disk, &self.baseline_config) {
return;
}
if self.has_unsaved_changes() {
self.disk_config_pending = Some(on_disk);
self.show_info_banner(
"config.yaml changed on disk. Load it to drop your unsaved edits, \
or Save to write over it.",
);
} else {
self.force_update_config(on_disk);
self.show_info_banner("Loaded config.yaml, which changed on disk.");
}
}
pub fn load_pending_disk_config(&mut self) {
if let Some(on_disk) = self.disk_config_pending.take() {
self.force_update_config(on_disk);
self.banner = None;
}
}
pub(super) fn render_banner(&mut self, ui: &mut egui::Ui) {
let Some(banner) = &self.banner else {
return;
};
let (fill, stroke) = match banner.kind {
BannerKind::Error => (
egui::Color32::from_rgb(70, 24, 24),
egui::Color32::from_rgb(244, 67, 54),
),
BannerKind::Info => (
egui::Color32::from_rgb(20, 44, 70),
egui::Color32::from_rgb(33, 150, 243),
),
};
let text = banner.text.clone();
let mut dismiss = false;
let mut load = false;
egui::Frame::NONE
.fill(fill)
.stroke(egui::Stroke::new(1.0, stroke))
.corner_radius(4.0)
.inner_margin(egui::Margin::symmetric(8, 6))
.show(ui, |ui| {
ui.horizontal(|ui| {
ui.add(
egui::Label::new(egui::RichText::new(text).color(egui::Color32::WHITE))
.wrap(),
);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.small_button("✕").on_hover_text("Dismiss").clicked() {
dismiss = true;
}
if self.disk_config_pending.is_some()
&& ui
.small_button("Load from disk")
.on_hover_text("Replace your unsaved edits with config.yaml")
.clicked()
{
load = true;
}
});
});
});
if load {
self.load_pending_disk_config();
} else if dismiss {
self.banner = None;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use par_term_config::Profile;
#[test]
fn failed_config_save_restores_the_baseline_and_shows_an_error() {
let mut settings = SettingsUI::new_for_tests(Config::default());
let original = settings.config.window.window_opacity;
settings.config.window.window_opacity = 0.3;
settings.request_save().expect("Save hands over the config");
assert!(!settings.has_unsaved_changes());
settings.report_config_save(Err("disk full".to_string()));
assert_eq!(settings.baseline_config().window.window_opacity, original);
assert!(settings.has_unsaved_changes(), "the edit is unsaved again");
let banner = settings.banner().expect("banner shown");
assert_eq!(banner.kind, BannerKind::Error);
assert!(banner.text.contains("disk full"));
}
#[test]
fn failed_save_during_close_keeps_the_window_open() {
let mut settings = SettingsUI::new_for_tests(Config::default());
settings.config.window.window_opacity = 0.3;
assert!(!settings.request_close());
settings.resolve_close_prompt(crate::settings_ui::ClosePromptChoice::Save);
settings.report_config_save(Err("read-only".to_string()));
assert!(!settings.take_close_ready());
}
#[test]
fn failed_profile_save_marks_profiles_unsaved_again() {
let mut settings = SettingsUI::new_for_tests(Config::default());
settings.sync_profiles(vec![Profile::new("a")]);
settings
.profile_modal_ui
.add_profile_for_test(Profile::new("b"));
settings.request_save();
settings.take_profile_save_request().expect("queued");
assert!(!settings.has_unsaved_changes());
settings.report_profile_save(Err("permission denied".to_string()));
assert!(settings.has_unsaved_changes());
assert_eq!(settings.banner().map(|b| b.kind), Some(BannerKind::Error));
}
#[test]
fn disk_change_without_edits_moves_the_baseline() {
let mut settings = SettingsUI::new_for_tests(Config::default());
let on_disk = Config {
font_size: 31.0,
..Config::default()
};
settings.config_changed_on_disk(on_disk);
assert_eq!(settings.config.font_size, 31.0);
assert_eq!(settings.baseline_config().font_size, 31.0);
assert!(!settings.has_unsaved_changes());
}
#[test]
fn disk_change_with_edits_is_held_until_loaded() {
let mut settings = SettingsUI::new_for_tests(Config::default());
settings.config.window.window_opacity = 0.2;
let on_disk = Config {
font_size: 31.0,
..Config::default()
};
settings.config_changed_on_disk(on_disk);
assert_eq!(settings.config.window.window_opacity, 0.2, "edits kept");
assert_ne!(settings.config.font_size, 31.0);
assert_eq!(settings.banner().map(|b| b.kind), Some(BannerKind::Info));
settings.load_pending_disk_config();
assert_eq!(settings.config.font_size, 31.0);
assert!(!settings.has_unsaved_changes());
}
#[test]
fn successful_save_shows_no_error() {
let mut settings = SettingsUI::new_for_tests(Config::default());
settings.config.window.window_opacity = 0.3;
settings.request_save();
settings.report_config_save(Ok(()));
assert!(
settings
.banner()
.is_none_or(|b| b.kind != BannerKind::Error)
);
}
}