Skip to main content

cpdb_rs/
config.rs

1//! Printer configuration persistence types.
2//!
3//! Provides a helper structure to store global defaults and per-printer overrides.
4
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8/// Saved user preferences for the print dialog.
9///
10/// Keeps track of the last selected printer, global default settings,
11/// and per-printer customized settings (e.g. paper size, quality options).
12#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
13pub struct PrinterConfig {
14    /// Last-used printer identifier
15    pub last_printer_id: Option<String>,
16    /// Last-used backend name
17    pub last_backend: Option<String>,
18    /// Global settings that apply to all printers unless overridden (e.g. `"copies" => "1"`)
19    pub global_settings: HashMap<String, String>,
20    /// Per-printer settings keyed by `"printer_id@backend"`
21    pub printer_settings: HashMap<String, HashMap<String, String>>,
22}
23
24impl PrinterConfig {
25    /// Creates a new empty `PrinterConfig`.
26    pub fn new() -> Self {
27        Self::default()
28    }
29
30    /// Returns `true` if this config has a saved printer.
31    pub fn has_printer(&self) -> bool {
32        self.last_printer_id.is_some()
33    }
34
35    /// Sets the last-used printer and backend.
36    pub fn set_last_printer(&mut self, printer_id: &str, backend: &str) {
37        self.last_printer_id = Some(printer_id.to_string());
38        self.last_backend = Some(backend.to_string());
39    }
40
41    /// Gets a global setting.
42    pub fn get_global_setting(&self, key: &str) -> Option<&str> {
43        self.global_settings.get(key).map(|s| s.as_str())
44    }
45
46    /// Sets a global setting.
47    pub fn set_global_setting(&mut self, key: String, value: String) {
48        self.global_settings.insert(key, value);
49    }
50
51    /// Gets the merged settings for a specific printer.
52    ///
53    /// This combines the `global_settings` with the printer-specific settings,
54    /// where printer-specific values override global values.
55    pub fn get_settings_for(&self, printer_id: &str, backend: &str) -> HashMap<String, String> {
56        let mut merged = self.global_settings.clone();
57        let key = format!("{}@{}", printer_id, backend);
58        if let Some(specific) = self.printer_settings.get(&key) {
59            for (k, v) in specific {
60                merged.insert(k.clone(), v.clone());
61            }
62        }
63        merged
64    }
65
66    /// Updates or replaces the settings for a specific printer.
67    pub fn set_settings_for(
68        &mut self,
69        printer_id: &str,
70        backend: &str,
71        settings: HashMap<String, String>,
72    ) {
73        let key = format!("{}@{}", printer_id, backend);
74        self.printer_settings.insert(key, settings);
75    }
76
77    /// Sets or updates a single setting for a specific printer.
78    pub fn set_setting_for(&mut self, printer_id: &str, backend: &str, key: String, value: String) {
79        let p_key = format!("{}@{}", printer_id, backend);
80        self.printer_settings
81            .entry(p_key)
82            .or_default()
83            .insert(key, value);
84    }
85
86    /// Gets a specific setting for a printer (printer-specific or global fallback).
87    pub fn get_setting_for(&self, printer_id: &str, backend: &str, key: &str) -> Option<&str> {
88        let p_key = format!("{}@{}", printer_id, backend);
89        if let Some(val) = self.printer_settings.get(&p_key).and_then(|m| m.get(key)) {
90            Some(val.as_str())
91        } else {
92            self.get_global_setting(key)
93        }
94    }
95
96    /// Removes all printer-specific settings stored for a printer.
97    pub fn remove_settings_for(&mut self, printer_id: &str, backend: &str) {
98        let key = format!("{}@{}", printer_id, backend);
99        self.printer_settings.remove(&key);
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn default_config_is_empty() {
109        let config = PrinterConfig::default();
110        assert!(config.last_printer_id.is_none());
111        assert!(config.last_backend.is_none());
112        assert!(config.global_settings.is_empty());
113        assert!(config.printer_settings.is_empty());
114        assert!(!config.has_printer());
115    }
116
117    #[test]
118    fn set_last_printer_works() {
119        let mut config = PrinterConfig::new();
120        config.set_last_printer("HP-123", "CUPS");
121        assert_eq!(config.last_printer_id.as_deref(), Some("HP-123"));
122        assert_eq!(config.last_backend.as_deref(), Some("CUPS"));
123        assert!(config.has_printer());
124    }
125
126    #[test]
127    fn global_settings_store_and_fallback() {
128        let mut config = PrinterConfig::new();
129        config.set_global_setting("copies".to_string(), "2".to_string());
130        assert_eq!(config.get_global_setting("copies"), Some("2"));
131        assert_eq!(config.get_global_setting("nonexistent"), None);
132
133        // check fallback via get_setting_for
134        assert_eq!(
135            config.get_setting_for("HP-123", "CUPS", "copies"),
136            Some("2")
137        );
138    }
139
140    #[test]
141    fn printer_specific_overrides() {
142        let mut config = PrinterConfig::new();
143        config.set_global_setting("copies".to_string(), "1".to_string());
144        config.set_global_setting("media".to_string(), "Letter".to_string());
145
146        let mut hp_settings = HashMap::new();
147        hp_settings.insert("media".to_string(), "A4".to_string());
148        config.set_settings_for("HP-123", "CUPS", hp_settings);
149
150        // HP-123 should have media=A4 (overridden) and copies=1 (fallback)
151        assert_eq!(
152            config.get_setting_for("HP-123", "CUPS", "media"),
153            Some("A4")
154        );
155        assert_eq!(
156            config.get_setting_for("HP-123", "CUPS", "copies"),
157            Some("1")
158        );
159
160        // Update a single setting for HP-123 via set_setting_for
161        config.set_setting_for(
162            "HP-123",
163            "CUPS",
164            "sides".to_string(),
165            "two-sided-long-edge".to_string(),
166        );
167        assert_eq!(
168            config.get_setting_for("HP-123", "CUPS", "sides"),
169            Some("two-sided-long-edge")
170        );
171
172        // General settings for HP-123 should be merged
173        let merged = config.get_settings_for("HP-123", "CUPS");
174        assert_eq!(merged.get("media").map(|s| s.as_str()), Some("A4"));
175        assert_eq!(merged.get("copies").map(|s| s.as_str()), Some("1"));
176        assert_eq!(
177            merged.get("sides").map(|s| s.as_str()),
178            Some("two-sided-long-edge")
179        );
180
181        // Remove HP-123 settings and check it falls back to Letter
182        config.remove_settings_for("HP-123", "CUPS");
183        assert_eq!(
184            config.get_setting_for("HP-123", "CUPS", "media"),
185            Some("Letter")
186        );
187        assert_eq!(config.get_setting_for("HP-123", "CUPS", "sides"), None);
188    }
189
190    #[test]
191    fn serde_roundtrip_json() {
192        let mut config = PrinterConfig::new();
193        config.set_last_printer("Epson", "CUPS");
194        config.set_global_setting("copies".to_string(), "3".to_string());
195
196        let mut ep_settings = HashMap::new();
197        ep_settings.insert("media".to_string(), "A4".to_string());
198        config.set_settings_for("Epson", "CUPS", ep_settings);
199
200        let json = serde_json::to_string(&config).unwrap();
201        let loaded: PrinterConfig = serde_json::from_str(&json).unwrap();
202        assert_eq!(config, loaded);
203    }
204}