Skip to main content

ipp_printer_app/
printer.rs

1//! Per-printer configuration and runtime state.
2
3use std::sync::Arc;
4
5use parking_lot::RwLock;
6
7use crate::flags::PrinterReason;
8
9/// Static printer capabilities supplied by the consumer crate (typically
10/// loaded from a config file). Carries everything the framework needs to
11/// build the IPP `Get-Printer-Attributes` response.
12#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
13#[allow(missing_docs)]
14pub struct PrinterConfig {
15    pub name: String,
16    pub driver_name: String,
17    pub make_and_model: String,
18    pub device_id: String,
19    pub device_uri: String,
20    pub dpi: i32,
21    pub printhead_width_dots: u32,
22    pub media_names: Vec<String>,
23    pub media_sizes: Vec<[i32; 2]>,
24    /// Darkness 0–100 (maps to print density).
25    pub darkness: i32,
26    /// MIME types the consumer's print callback can decode, emitted as
27    /// `document-format-supported`. Empty falls back to the framework's raster
28    /// defaults (`image/pwg-raster`, `application/vnd.cups-raster`,
29    /// `application/octet-stream`). Add `image/jpeg` etc. when the backend can
30    /// handle them.
31    #[serde(default)]
32    pub document_formats: Vec<String>,
33}
34
35impl PrinterConfig {
36    /// Build the canonical `ipp://<host>:<port>/ipp/print/<name>` URI. If
37    /// `host` is unspecified (`0.0.0.0`, `::`, empty), advertises
38    /// `localhost` so CUPS and mDNS clients get a reachable address.
39    pub fn printer_uri(&self, host: &str, port: u16) -> String {
40        let h = if host == "0.0.0.0" || host == "::" || host.is_empty() {
41            "localhost"
42        } else {
43            host
44        };
45        format!("ipp://{h}:{port}/ipp/print/{}", self.name)
46    }
47}
48
49/// IPP `printer-state` enum (RFC 8011 §5.4.11).
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51#[repr(u32)]
52#[allow(missing_docs)]
53pub enum IppPrinterState {
54    Idle = 3,
55    Processing = 4,
56    Stopped = 5,
57}
58
59/// Runtime printer entry in the server registry.
60#[derive(Debug, Clone)]
61#[allow(missing_docs)]
62pub struct PrinterRecord {
63    pub config: PrinterConfig,
64    pub state: IppPrinterState,
65    pub reasons: PrinterReason,
66    pub uuid: String,
67    /// Live media loaded in the device, set by the status poller. `None` until
68    /// the first successful poll — the attribute builder then falls back to the
69    /// configured default for `media-ready` / `media-col-ready`.
70    pub ready_media: Option<crate::device::ReadyMedia>,
71    /// Live remaining-supply level 0–100 from the status poller. `None` falls
72    /// back to a full static `printer-supply`.
73    pub supply_percent: Option<u8>,
74}
75
76impl PrinterRecord {
77    /// Wrap a config in a fresh record (state = `Idle`, no reasons set, new UUID).
78    pub fn new(config: PrinterConfig) -> Self {
79        Self {
80            uuid: uuid::Uuid::new_v4().to_string(),
81            state: IppPrinterState::Idle,
82            reasons: PrinterReason::empty(),
83            ready_media: None,
84            supply_percent: None,
85            config,
86        }
87    }
88}
89
90/// Borrowed view of a printer passed into [`crate::RasterDriver`] callbacks.
91///
92/// `record` is exposed for direct access; the helpers below are the
93/// commonly-needed shortcuts.
94pub struct PrinterHandle<'a> {
95    /// The underlying registry entry.
96    pub record: &'a PrinterRecord,
97}
98
99impl<'a> PrinterHandle<'a> {
100    /// Driver name from the config (matches the value supplied by
101    /// [`crate::DeviceBackend::driver_for_device`]).
102    pub fn driver_name(&self) -> &str {
103        &self.record.config.driver_name
104    }
105
106    /// Configured darkness, 0–100.
107    pub fn darkness(&self) -> i32 {
108        self.record.config.darkness
109    }
110
111    /// Printhead width in dots.
112    pub fn printhead_width_dots(&self) -> u32 {
113        self.record.config.printhead_width_dots
114    }
115}
116
117/// Shared printer registry. Cheap to clone (it's an `Arc`).
118pub type PrinterRegistry = Arc<RwLock<Vec<PrinterRecord>>>;
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    /// A persisted config written before `document_formats` existed must still
125    /// deserialize (the field is `#[serde(default)]` → empty).
126    #[test]
127    fn config_without_document_formats_loads() {
128        let json = r#"{
129            "name": "p", "driver_name": "d", "make_and_model": "m",
130            "device_id": "", "device_uri": "mock://x", "dpi": 203,
131            "printhead_width_dots": 384, "media_names": [], "media_sizes": [],
132            "darkness": 50
133        }"#;
134        let cfg: PrinterConfig = serde_json::from_str(json).expect("back-compat load");
135        assert!(cfg.document_formats.is_empty());
136    }
137}