1mod preset;
2pub mod versioned;
3
4use crate::color::{Color, BLACK};
5use crate::device::CURRENT_DEVICE;
6use crate::fl;
7use crate::frontlight::LightLevels;
8use crate::i18n::I18nDisplay;
9use crate::metadata::{SortMethod, TextAlign};
10use crate::unit::mm_to_px;
11use fxhash::FxHashSet;
12use unic_langid::LanguageIdentifier;
13
14pub use self::preset::{guess_frontlight, LightPreset};
15use serde::{Deserialize, Serialize};
16use std::collections::{BTreeMap, HashMap};
17use std::env;
18use std::fmt::{self, Debug};
19use std::ops::{Index, IndexMut};
20use std::path::PathBuf;
21
22pub const SETTINGS_PATH: &str = "Settings.toml";
23pub const DEFAULT_FONT_PATH: &str = "/mnt/onboard/fonts";
24pub const INTERNAL_CARD_ROOT: &str = "/mnt/onboard";
25pub const EXTERNAL_CARD_ROOT: &str = "/mnt/sd";
26const LOGO_SPECIAL_PATH: &str = "logo:";
27const COVER_SPECIAL_PATH: &str = "cover:";
28
29#[derive(Debug, Clone, Eq, PartialEq)]
32pub enum IntermissionDisplay {
33 Logo,
35 Cover,
37 Image(PathBuf),
39}
40
41impl Serialize for IntermissionDisplay {
42 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
43 where
44 S: serde::Serializer,
45 {
46 match self {
47 IntermissionDisplay::Logo => serializer.serialize_str(LOGO_SPECIAL_PATH),
48 IntermissionDisplay::Cover => serializer.serialize_str(COVER_SPECIAL_PATH),
49 IntermissionDisplay::Image(path) => {
50 serializer.serialize_str(path.to_string_lossy().as_ref())
51 }
52 }
53 }
54}
55
56impl<'de> Deserialize<'de> for IntermissionDisplay {
57 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
58 where
59 D: serde::Deserializer<'de>,
60 {
61 let s = String::deserialize(deserializer)?;
62 Ok(match s.as_str() {
63 LOGO_SPECIAL_PATH => IntermissionDisplay::Logo,
64 COVER_SPECIAL_PATH => IntermissionDisplay::Cover,
65 _ => IntermissionDisplay::Image(PathBuf::from(s)),
66 })
67 }
68}
69
70impl fmt::Display for IntermissionDisplay {
71 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
72 match self {
73 IntermissionDisplay::Logo => write!(f, "Logo"),
74 IntermissionDisplay::Cover => write!(f, "Cover"),
75 IntermissionDisplay::Image(_) => write!(f, "Custom"),
76 }
77 }
78}
79
80pub const DEFAULT_FONT_SIZE: f32 = 11.0;
82pub const DEFAULT_MARGIN_WIDTH: i32 = 8;
84pub const DEFAULT_LINE_HEIGHT: f32 = 1.2;
86pub const DEFAULT_FONT_FAMILY: &str = "Libertinus Serif";
88pub const DEFAULT_TEXT_ALIGN: TextAlign = TextAlign::Left;
90pub const HYPHEN_PENALTY: i32 = 50;
91pub const STRETCH_TOLERANCE: f32 = 1.26;
92
93#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
94#[serde(rename_all = "kebab-case")]
95pub enum RotationLock {
96 Landscape,
97 Portrait,
98 Current,
99}
100
101#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
102#[serde(rename_all = "kebab-case")]
103pub enum ButtonScheme {
104 Natural,
105 Inverted,
106}
107
108impl fmt::Display for ButtonScheme {
109 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
110 Debug::fmt(self, f)
111 }
112}
113
114impl I18nDisplay for ButtonScheme {
115 fn to_i18n_string(&self) -> String {
116 match self {
117 ButtonScheme::Natural => fl!("settings-button-scheme-natural"),
118 ButtonScheme::Inverted => fl!("settings-button-scheme-inverted"),
119 }
120 }
121}
122
123#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
124#[serde(rename_all = "kebab-case")]
125pub enum IntermKind {
126 Suspend,
127 PowerOff,
128 Share,
129}
130
131impl IntermKind {
132 pub fn text(&self) -> &str {
133 match self {
134 IntermKind::Suspend => "Sleeping",
135 IntermKind::PowerOff => "Powered off",
136 IntermKind::Share => "Shared",
137 }
138 }
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
143#[serde(rename_all = "kebab-case")]
144pub struct Intermissions {
145 suspend: IntermissionDisplay,
146 power_off: IntermissionDisplay,
147 share: IntermissionDisplay,
148}
149
150impl Index<IntermKind> for Intermissions {
151 type Output = IntermissionDisplay;
152
153 fn index(&self, key: IntermKind) -> &Self::Output {
154 match key {
155 IntermKind::Suspend => &self.suspend,
156 IntermKind::PowerOff => &self.power_off,
157 IntermKind::Share => &self.share,
158 }
159 }
160}
161
162impl IndexMut<IntermKind> for Intermissions {
163 fn index_mut(&mut self, key: IntermKind) -> &mut Self::Output {
164 match key {
165 IntermKind::Suspend => &mut self.suspend,
166 IntermKind::PowerOff => &mut self.power_off,
167 IntermKind::Share => &mut self.share,
168 }
169 }
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
173#[serde(default, rename_all = "kebab-case")]
174pub struct Settings {
175 pub selected_library: usize,
176 pub keyboard_layout: String,
177 pub frontlight: bool,
178 pub wifi: bool,
179 pub inverted: bool,
180 pub sleep_cover: bool,
181 pub auto_share: bool,
182 #[serde(skip_serializing_if = "Option::is_none")]
183 pub rotation_lock: Option<RotationLock>,
184 pub button_scheme: ButtonScheme,
185 pub auto_suspend: f32,
186 pub auto_power_off: f32,
187 pub time_format: String,
188 pub date_format: String,
189 #[serde(skip_serializing_if = "Option::is_none")]
190 pub external_urls_queue: Option<PathBuf>,
191 #[serde(skip_serializing_if = "Vec::is_empty")]
192 pub libraries: Vec<LibrarySettings>,
193 pub intermissions: Intermissions,
194 #[serde(skip_serializing_if = "Vec::is_empty")]
195 pub frontlight_presets: Vec<LightPreset>,
196 pub home: HomeSettings,
197 pub reader: ReaderSettings,
198 pub import: ImportSettings,
199 pub dictionary: DictionarySettings,
200 pub sketch: SketchSettings,
201 pub calculator: CalculatorSettings,
202 pub battery: BatterySettings,
203 pub frontlight_levels: LightLevels,
204 pub ota: OtaSettings,
205 pub logging: LoggingSettings,
206 pub settings_retention: usize,
207 #[serde(skip_serializing_if = "Option::is_none")]
208 pub locale: Option<LanguageIdentifier>,
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize)]
212#[serde(default, rename_all = "kebab-case")]
213pub struct LibrarySettings {
214 pub name: String,
215 pub path: PathBuf,
216 pub sort_method: SortMethod,
217 pub first_column: FirstColumn,
218 pub second_column: SecondColumn,
219 pub thumbnail_previews: bool,
220 #[serde(skip_serializing_if = "Vec::is_empty")]
221 pub hooks: Vec<Hook>,
222 #[serde(skip_serializing_if = "Option::is_none")]
223 pub finished: Option<FinishedAction>,
224}
225
226impl Default for LibrarySettings {
227 fn default() -> Self {
228 LibrarySettings {
229 name: "Unnamed".to_string(),
230 path: env::current_dir()
231 .ok()
232 .unwrap_or_else(|| PathBuf::from("/")),
233 sort_method: SortMethod::Opened,
234 first_column: FirstColumn::TitleAndAuthor,
235 second_column: SecondColumn::Progress,
236 thumbnail_previews: true,
237 hooks: Vec::new(),
238 finished: None,
239 }
240 }
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize)]
244#[serde(default, rename_all = "kebab-case")]
245pub struct ImportSettings {
246 pub startup_trigger: bool,
247 pub sync_metadata: bool,
248 pub metadata_kinds: FxHashSet<String>,
249 pub allowed_kinds: FxHashSet<String>,
250}
251
252#[derive(Debug, Clone, Serialize, Deserialize)]
253#[serde(default, rename_all = "kebab-case")]
254pub struct DictionarySettings {
255 pub margin_width: i32,
256 pub font_size: f32,
257 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
258 pub languages: BTreeMap<String, Vec<String>>,
259}
260
261impl Default for DictionarySettings {
262 fn default() -> Self {
263 DictionarySettings {
264 font_size: 11.0,
265 margin_width: 4,
266 languages: BTreeMap::new(),
267 }
268 }
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize)]
272#[serde(default, rename_all = "kebab-case")]
273pub struct SketchSettings {
274 pub save_path: PathBuf,
275 pub notify_success: bool,
276 pub pen: Pen,
277}
278
279#[derive(Debug, Clone, Serialize, Deserialize)]
280#[serde(default, rename_all = "kebab-case")]
281pub struct CalculatorSettings {
282 pub font_size: f32,
283 pub margin_width: i32,
284 pub history_size: usize,
285}
286
287#[derive(Debug, Clone, Serialize, Deserialize)]
288#[serde(default, rename_all = "kebab-case")]
289pub struct Pen {
290 pub size: i32,
291 pub color: Color,
292 pub dynamic: bool,
293 pub amplitude: f32,
294 pub min_speed: f32,
295 pub max_speed: f32,
296}
297
298impl Default for Pen {
299 fn default() -> Self {
300 Pen {
301 size: 2,
302 color: BLACK,
303 dynamic: true,
304 amplitude: 4.0,
305 min_speed: 0.0,
306 max_speed: mm_to_px(254.0, CURRENT_DEVICE.dpi),
307 }
308 }
309}
310
311impl Default for SketchSettings {
312 fn default() -> Self {
313 SketchSettings {
314 save_path: PathBuf::from("Sketches"),
315 notify_success: true,
316 pen: Pen::default(),
317 }
318 }
319}
320
321impl Default for CalculatorSettings {
322 fn default() -> Self {
323 CalculatorSettings {
324 font_size: 8.0,
325 margin_width: 2,
326 history_size: 4096,
327 }
328 }
329}
330
331#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
332#[serde(rename_all = "kebab-case")]
333pub struct Columns {
334 first: FirstColumn,
335 second: SecondColumn,
336}
337
338#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
339#[serde(rename_all = "kebab-case")]
340pub enum FirstColumn {
341 TitleAndAuthor,
342 FileName,
343}
344
345#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
346#[serde(rename_all = "kebab-case")]
347pub enum SecondColumn {
348 Progress,
349 Year,
350}
351
352#[derive(Debug, Clone, Serialize, Deserialize)]
353#[serde(default, rename_all = "kebab-case")]
354pub struct Hook {
355 pub path: PathBuf,
356 pub program: PathBuf,
357 pub sort_method: Option<SortMethod>,
358 pub first_column: Option<FirstColumn>,
359 pub second_column: Option<SecondColumn>,
360}
361
362impl Default for Hook {
363 fn default() -> Self {
364 Hook {
365 path: PathBuf::default(),
366 program: PathBuf::default(),
367 sort_method: None,
368 first_column: None,
369 second_column: None,
370 }
371 }
372}
373
374#[derive(Debug, Clone, Serialize, Deserialize)]
375#[serde(default, rename_all = "kebab-case")]
376pub struct HomeSettings {
377 pub address_bar: bool,
378 pub navigation_bar: bool,
379 pub max_levels: usize,
380 pub max_trash_size: u64,
381}
382
383#[derive(Debug, Clone, Serialize, Deserialize)]
384#[serde(default, rename_all = "kebab-case")]
385pub struct RefreshRateSettings {
386 #[serde(flatten)]
387 pub global: RefreshRatePair,
388 #[serde(skip_serializing_if = "HashMap::is_empty")]
389 pub by_kind: HashMap<String, RefreshRatePair>,
390}
391
392#[derive(Debug, Clone, Serialize, Deserialize)]
393#[serde(rename_all = "kebab-case")]
394pub struct RefreshRatePair {
395 pub regular: u8,
396 pub inverted: u8,
397}
398
399#[derive(Debug, Clone, Serialize, Deserialize)]
400#[serde(default, rename_all = "kebab-case")]
401pub struct ReaderSettings {
402 pub finished: FinishedAction,
403 pub south_east_corner: SouthEastCornerAction,
404 pub bottom_right_gesture: BottomRightGestureAction,
405 pub south_strip: SouthStripAction,
406 pub west_strip: WestStripAction,
407 pub east_strip: EastStripAction,
408 pub strip_width: f32,
409 pub corner_width: f32,
410 pub font_path: String,
411 pub font_family: String,
412 pub font_size: f32,
413 pub min_font_size: f32,
414 pub max_font_size: f32,
415 pub text_align: TextAlign,
416 pub margin_width: i32,
417 pub min_margin_width: i32,
418 pub max_margin_width: i32,
419 pub line_height: f32,
420 pub continuous_fit_to_width: bool,
421 pub ignore_document_css: bool,
422 pub dithered_kinds: FxHashSet<String>,
423 pub paragraph_breaker: ParagraphBreakerSettings,
424 pub refresh_rate: RefreshRateSettings,
425}
426
427#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
428#[serde(default, rename_all = "kebab-case")]
429pub struct ParagraphBreakerSettings {
430 pub hyphen_penalty: i32,
431 pub stretch_tolerance: f32,
432}
433
434#[derive(Debug, Clone, Serialize, Deserialize)]
435#[serde(default, rename_all = "kebab-case")]
436pub struct BatterySettings {
437 pub warn: f32,
438 pub power_off: f32,
439}
440
441#[derive(Debug, Clone, Serialize, Deserialize)]
443#[serde(default, rename_all = "kebab-case")]
444pub struct LoggingSettings {
445 pub enabled: bool,
447 pub level: String,
449 pub max_files: usize,
451 pub directory: PathBuf,
453 #[serde(skip_serializing_if = "Option::is_none")]
455 pub otlp_endpoint: Option<String>,
456 pub enable_kern_log: bool,
458 pub enable_dbus_log: bool,
460}
461
462#[derive(Debug, Clone, Default, Serialize, Deserialize)]
468#[serde(default, rename_all = "kebab-case")]
469pub struct OtaSettings {}
470
471#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
472#[serde(rename_all = "kebab-case")]
473pub enum FinishedAction {
474 Notify,
475 Close,
476 GoToNext,
477}
478
479impl fmt::Display for FinishedAction {
480 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
481 match self {
482 FinishedAction::Notify => write!(f, "Notify"),
483 FinishedAction::Close => write!(f, "Close"),
484 FinishedAction::GoToNext => write!(f, "Go to Next"),
485 }
486 }
487}
488
489impl I18nDisplay for FinishedAction {
490 fn to_i18n_string(&self) -> String {
491 match self {
492 FinishedAction::Notify => fl!("settings-finished-action-notify"),
493 FinishedAction::Close => fl!("settings-finished-action-close"),
494 FinishedAction::GoToNext => fl!("settings-finished-action-goto-next"),
495 }
496 }
497}
498
499#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
500#[serde(rename_all = "kebab-case")]
501pub enum SouthEastCornerAction {
502 NextPage,
503 GoToPage,
504}
505
506#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
507#[serde(rename_all = "kebab-case")]
508pub enum BottomRightGestureAction {
509 ToggleDithered,
510 ToggleInverted,
511}
512
513#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
514#[serde(rename_all = "kebab-case")]
515pub enum SouthStripAction {
516 ToggleBars,
517 NextPage,
518}
519
520#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
521#[serde(rename_all = "kebab-case")]
522pub enum EastStripAction {
523 PreviousPage,
524 NextPage,
525 None,
526}
527
528#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
529#[serde(rename_all = "kebab-case")]
530pub enum WestStripAction {
531 PreviousPage,
532 NextPage,
533 None,
534}
535
536impl Default for RefreshRateSettings {
537 fn default() -> Self {
538 RefreshRateSettings {
539 global: RefreshRatePair {
540 regular: 8,
541 inverted: 2,
542 },
543 by_kind: HashMap::new(),
544 }
545 }
546}
547
548impl Default for HomeSettings {
549 fn default() -> Self {
550 HomeSettings {
551 address_bar: false,
552 navigation_bar: true,
553 max_levels: 3,
554 max_trash_size: 32 * (1 << 20),
555 }
556 }
557}
558
559impl Default for ParagraphBreakerSettings {
560 fn default() -> Self {
561 ParagraphBreakerSettings {
562 hyphen_penalty: HYPHEN_PENALTY,
563 stretch_tolerance: STRETCH_TOLERANCE,
564 }
565 }
566}
567
568impl Default for ReaderSettings {
569 fn default() -> Self {
570 ReaderSettings {
571 finished: FinishedAction::Close,
572 south_east_corner: SouthEastCornerAction::GoToPage,
573 bottom_right_gesture: BottomRightGestureAction::ToggleDithered,
574 south_strip: SouthStripAction::ToggleBars,
575 west_strip: WestStripAction::PreviousPage,
576 east_strip: EastStripAction::NextPage,
577 strip_width: 0.6,
578 corner_width: 0.4,
579 font_path: DEFAULT_FONT_PATH.to_string(),
580 font_family: DEFAULT_FONT_FAMILY.to_string(),
581 font_size: DEFAULT_FONT_SIZE,
582 min_font_size: DEFAULT_FONT_SIZE / 2.0,
583 max_font_size: 3.0 * DEFAULT_FONT_SIZE / 2.0,
584 text_align: DEFAULT_TEXT_ALIGN,
585 margin_width: DEFAULT_MARGIN_WIDTH,
586 min_margin_width: DEFAULT_MARGIN_WIDTH.saturating_sub(8),
587 max_margin_width: DEFAULT_MARGIN_WIDTH.saturating_add(2),
588 line_height: DEFAULT_LINE_HEIGHT,
589 continuous_fit_to_width: true,
590 ignore_document_css: false,
591 dithered_kinds: ["cbz", "png", "jpg", "jpeg"]
592 .iter()
593 .map(|k| k.to_string())
594 .collect(),
595 paragraph_breaker: ParagraphBreakerSettings::default(),
596 refresh_rate: RefreshRateSettings::default(),
597 }
598 }
599}
600
601impl Default for ImportSettings {
602 fn default() -> Self {
603 ImportSettings {
604 startup_trigger: true,
605 sync_metadata: true,
606 metadata_kinds: ["epub", "pdf", "djvu"]
607 .iter()
608 .map(|k| k.to_string())
609 .collect(),
610 allowed_kinds: [
611 "pdf", "djvu", "epub", "fb2", "txt", "xps", "oxps", "mobi", "cbz",
612 ]
613 .iter()
614 .map(|k| k.to_string())
615 .collect(),
616 }
617 }
618}
619
620impl Default for BatterySettings {
621 fn default() -> Self {
622 BatterySettings {
623 warn: 10.0,
624 power_off: 3.0,
625 }
626 }
627}
628
629impl Default for LoggingSettings {
630 fn default() -> Self {
631 LoggingSettings {
632 enabled: true,
633 level: "info".to_string(),
634 max_files: 3,
635 directory: PathBuf::from("logs"),
636 otlp_endpoint: None,
637 enable_kern_log: false,
638 enable_dbus_log: false,
639 }
640 }
641}
642
643impl Default for Settings {
644 fn default() -> Self {
645 Settings {
646 selected_library: 0,
647 #[cfg(feature = "emulator")]
648 libraries: vec![LibrarySettings {
649 name: "Cadmus Source".to_string(),
650 path: PathBuf::from("."),
651 ..Default::default()
652 }],
653 #[cfg(not(feature = "emulator"))]
654 libraries: vec![
655 LibrarySettings {
656 name: "On Board".to_string(),
657 path: PathBuf::from(INTERNAL_CARD_ROOT),
658 hooks: vec![Hook {
659 path: PathBuf::from("Articles"),
660 program: PathBuf::from("bin/article_fetcher/article_fetcher"),
661 sort_method: Some(SortMethod::Added),
662 first_column: Some(FirstColumn::TitleAndAuthor),
663 second_column: Some(SecondColumn::Progress),
664 }],
665 ..Default::default()
666 },
667 LibrarySettings {
668 name: "Removable".to_string(),
669 path: PathBuf::from(EXTERNAL_CARD_ROOT),
670 ..Default::default()
671 },
672 LibrarySettings {
673 name: "Dropbox".to_string(),
674 path: PathBuf::from("/mnt/onboard/.kobo/dropbox"),
675 ..Default::default()
676 },
677 LibrarySettings {
678 name: "KePub".to_string(),
679 path: PathBuf::from("/mnt/onboard/.kobo/kepub"),
680 ..Default::default()
681 },
682 ],
683 external_urls_queue: Some(PathBuf::from("bin/article_fetcher/urls.txt")),
684 keyboard_layout: "English".to_string(),
685 frontlight: true,
686 wifi: false,
687 inverted: false,
688 sleep_cover: true,
689 auto_share: false,
690 rotation_lock: None,
691 button_scheme: ButtonScheme::Natural,
692 auto_suspend: 30.0,
693 auto_power_off: 3.0,
694 time_format: "%H:%M".to_string(),
695 date_format: "%A, %B %-d, %Y".to_string(),
696 intermissions: Intermissions {
697 suspend: IntermissionDisplay::Logo,
698 power_off: IntermissionDisplay::Logo,
699 share: IntermissionDisplay::Logo,
700 },
701 home: HomeSettings::default(),
702 reader: ReaderSettings::default(),
703 import: ImportSettings::default(),
704 dictionary: DictionarySettings::default(),
705 sketch: SketchSettings::default(),
706 calculator: CalculatorSettings::default(),
707 battery: BatterySettings::default(),
708 frontlight_levels: LightLevels::default(),
709 frontlight_presets: Vec::new(),
710 ota: OtaSettings::default(),
711 logging: LoggingSettings::default(),
712 settings_retention: 3,
713 locale: None,
714 }
715 }
716}
717
718#[cfg(test)]
719mod tests {
720 use super::*;
721
722 #[test]
723 fn test_ota_settings_serializes_empty() {
724 let settings = OtaSettings::default();
725 let serialized = toml::to_string(&settings).expect("Failed to serialize");
726 assert!(
727 serialized.is_empty(),
728 "OtaSettings should serialize to an empty string"
729 );
730 }
731
732 #[test]
733 fn test_intermissions_struct_serialization() {
734 let intermissions = Intermissions {
735 suspend: IntermissionDisplay::Logo,
736 power_off: IntermissionDisplay::Cover,
737 share: IntermissionDisplay::Image(PathBuf::from("/custom/share.png")),
738 };
739
740 let serialized = toml::to_string(&intermissions).expect("Failed to serialize");
741
742 assert!(
743 serialized.contains("logo:"),
744 "Should contain logo: for suspend"
745 );
746 assert!(
747 serialized.contains("cover:"),
748 "Should contain cover: for power-off"
749 );
750 assert!(
751 serialized.contains("/custom/share.png"),
752 "Should contain custom path for share"
753 );
754 }
755
756 #[test]
757 fn test_intermissions_struct_deserialization() {
758 let toml_str = r#"
759suspend = "logo:"
760power-off = "cover:"
761share = "/path/to/custom.png"
762"#;
763
764 let intermissions: Intermissions = toml::from_str(toml_str).expect("Failed to deserialize");
765
766 assert!(
767 matches!(intermissions.suspend, IntermissionDisplay::Logo),
768 "suspend should deserialize to Logo"
769 );
770 assert!(
771 matches!(intermissions.power_off, IntermissionDisplay::Cover),
772 "power_off should deserialize to Cover"
773 );
774 assert!(
775 matches!(
776 intermissions.share,
777 IntermissionDisplay::Image(ref path) if path == &PathBuf::from("/path/to/custom.png")
778 ),
779 "share should deserialize to Image with correct path"
780 );
781 }
782
783 #[test]
784 fn test_intermissions_struct_round_trip() {
785 let original = Intermissions {
786 suspend: IntermissionDisplay::Logo,
787 power_off: IntermissionDisplay::Cover,
788 share: IntermissionDisplay::Image(PathBuf::from("/some/custom/image.jpg")),
789 };
790
791 let serialized = toml::to_string(&original).expect("Failed to serialize");
792 let deserialized: Intermissions =
793 toml::from_str(&serialized).expect("Failed to deserialize");
794
795 assert_eq!(
796 original.suspend, deserialized.suspend,
797 "suspend should survive round trip"
798 );
799 assert_eq!(
800 original.power_off, deserialized.power_off,
801 "power_off should survive round trip"
802 );
803 assert_eq!(
804 original.share, deserialized.share,
805 "share should survive round trip"
806 );
807 }
808}