Skip to main content

cpdb_sys/
frontend.rs

1//! Safe wrapper around `cpdb_frontend_obj_t`.
2//!
3//! A [`Frontend`] is the central object for printer discovery. It manages
4//! a D-Bus connection, holds the backend list, and owns the hash table
5//! of discovered printers.
6//!
7//! # Threading
8//!
9//! [`Frontend`] is [`Send`] but **not** [`Sync`]. Most methods take
10//! `&self` for ergonomics but mutate the underlying C state, and
11//! cpdb-libs does not lock internally. If you need concurrent access,
12//! wrap the frontend in a [`std::sync::Mutex`].
13
14use super::bindings as ffi;
15use super::callbacks::{self, PrinterUpdate};
16use super::printer::Printer;
17use crate::error::{CpdbError, Result};
18use std::ffi::{CStr, CString};
19use std::mem::MaybeUninit;
20use std::ptr::NonNull;
21
22/// Safe wrapper around `cpdb_frontend_obj_t`.
23pub struct Frontend {
24    raw: NonNull<ffi::cpdb_frontend_obj_t>,
25}
26
27// SAFETY: `Frontend` owns its `cpdb_frontend_obj_t *`. Moving it across
28// threads is fine; concurrent access from multiple threads is not, so we
29// deliberately omit `Sync`.
30unsafe impl Send for Frontend {}
31
32impl Frontend {
33    // ─── Construction ────────────────────────────────────────────────────────
34
35    /// Creates a new frontend with the default printer-update callback.
36    pub fn new() -> Result<Self> {
37        Self::new_internal(None)
38    }
39
40    /// Creates a new frontend with a raw printer-update callback.
41    ///
42    /// Prefer [`Frontend::new_with_observer`] for closure-based callbacks.
43    /// This entry point exists for callers that need to interoperate with a
44    /// hand-written `extern "C"` callback.
45    ///
46    /// The callback is invoked from the cpdb-libs internal D-Bus listener
47    /// thread when printers are added, removed, or change state. It must be
48    /// thread-safe and must not call back into this `Frontend`.
49    pub fn new_with_callback(cb: ffi::cpdb_printer_callback) -> Result<Self> {
50        Self::new_internal(cb)
51    }
52
53    /// Creates a new frontend with a closure-based printer observer.
54    ///
55    /// The observer is invoked from cpdb-libs' internal D-Bus listener
56    /// thread when printers are added, removed, or change state. Because
57    /// `cpdb_printer_callback` carries no `user_data`, the closure is
58    /// stored in a process-global registry keyed by the frontend pointer
59    /// and removed automatically when the [`Frontend`] is dropped.
60    ///
61    /// The closure must be `Send + 'static`. Panics inside the closure are
62    /// caught by `catch_unwind` and absorbed; do not rely on panic-based
63    /// flow control inside callbacks.
64    pub fn new_with_observer<F>(observer: F) -> Result<Self>
65    where
66        F: FnMut(&Printer<'_>, PrinterUpdate) + Send + 'static,
67    {
68        let cb: ffi::cpdb_printer_callback = Some(callbacks::printer_trampoline);
69        let frontend = Self::new_internal(cb)?;
70        callbacks::register_printer_observer(frontend.raw.as_ptr(), Box::new(observer));
71        Ok(frontend)
72    }
73
74    fn new_internal(cb: ffi::cpdb_printer_callback) -> Result<Self> {
75        // SAFETY: `cpdbGetNewFrontendObj` is a constructor; the callback may
76        // be null.
77        let raw = unsafe { ffi::cpdbGetNewFrontendObj(cb) };
78        NonNull::new(raw)
79            .map(|raw| Self { raw })
80            .ok_or_else(|| CpdbError::FrontendError("cpdbGetNewFrontendObj returned null".into()))
81    }
82
83    /// Wraps an already-allocated frontend object.
84    ///
85    /// # Safety
86    /// `raw` must be a valid pointer to a `cpdb_frontend_obj_t` obtained from
87    /// cpdb-libs, not aliased by any other Rust handle, and not yet freed.
88    /// Ownership transfers to the returned `Frontend`.
89    pub unsafe fn from_raw(raw: *mut ffi::cpdb_frontend_obj_t) -> Result<Self> {
90        NonNull::new(raw)
91            .map(|raw| Self { raw })
92            .ok_or(CpdbError::NullPointer)
93    }
94
95    /// Returns the raw pointer for use within this crate.
96    #[doc(hidden)]
97    pub fn as_raw(&self) -> *mut ffi::cpdb_frontend_obj_t {
98        self.raw.as_ptr()
99    }
100
101    // ─── Lifecycle ───────────────────────────────────────────────────────────
102
103    /// Tells the frontend to ignore the previously saved settings file.
104    ///
105    /// Must be called before [`Frontend::connect_to_dbus`] to take effect.
106    pub fn ignore_last_saved_settings(&self) {
107        // SAFETY: pointer is non-null.
108        unsafe { ffi::cpdbIgnoreLastSavedSettings(self.raw.as_ptr()) };
109    }
110
111    /// Connects to the session D-Bus and activates the print backends.
112    pub fn connect_to_dbus(&self) -> Result<()> {
113        // SAFETY: pointer is non-null.
114        unsafe { ffi::cpdbConnectToDBus(self.raw.as_ptr()) };
115        Ok(())
116    }
117
118    /// Returns `true` when cpdb-libs currently holds a live D-Bus connection.
119    ///
120    /// This consults `cpdbGetDbusConnection`, which is a process-global —
121    /// not per-frontend — query, so the result reflects the overall
122    /// cpdb-libs state rather than this specific [`Frontend`].
123    pub fn dbus_connected() -> bool {
124        // SAFETY: no arguments; returns either a live `GDBusConnection *`
125        // or null.
126        !unsafe { ffi::cpdbGetDbusConnection() }.is_null()
127    }
128
129    /// Disconnects from D-Bus.
130    pub fn disconnect_from_dbus(&self) -> Result<()> {
131        // SAFETY: pointer is non-null.
132        unsafe { ffi::cpdbDisconnectFromDBus(self.raw.as_ptr()) };
133        Ok(())
134    }
135
136    /// Re-activates the backends (rediscovers printers without disconnecting).
137    pub fn activate_backends(&self) {
138        // SAFETY: pointer is non-null.
139        unsafe { ffi::cpdbActivateBackends(self.raw.as_ptr()) };
140    }
141
142    /// Starts the background thread that periodically refreshes the backend list.
143    pub fn start_backend_list_refreshing(&self) {
144        // SAFETY: pointer is non-null.
145        unsafe { ffi::cpdbStartBackendListRefreshing(self.raw.as_ptr()) };
146    }
147
148    /// Stops the backend-list refreshing thread; blocks until it joins.
149    pub fn stop_backend_list_refreshing(&self) {
150        // SAFETY: pointer is non-null.
151        unsafe { ffi::cpdbStopBackendListRefreshing(self.raw.as_ptr()) };
152    }
153
154    /// Starts the printer-listing flow (`cpdbStartListingPrinters`), creating
155    /// a fresh frontend bound to the supplied callback.
156    pub fn start_listing(cb: ffi::cpdb_printer_callback) -> Result<Self> {
157        // SAFETY: callback may be null per upstream docs.
158        let raw = unsafe { ffi::cpdbStartListingPrinters(cb) };
159        NonNull::new(raw).map(|raw| Self { raw }).ok_or_else(|| {
160            CpdbError::FrontendError("cpdbStartListingPrinters returned null".into())
161        })
162    }
163
164    /// Stops the printer-listing flow.
165    pub fn stop_listing_printers(&self) {
166        // SAFETY: pointer is non-null.
167        unsafe { ffi::cpdbStopListingPrinters(self.raw.as_ptr()) };
168    }
169
170    // ─── Visibility toggles ──────────────────────────────────────────────────
171
172    /// Hides remote printers from subsequent listings.
173    pub fn hide_remote_printers(&self) {
174        // SAFETY: pointer is non-null.
175        unsafe { ffi::cpdbHideRemotePrinters(self.raw.as_ptr()) };
176    }
177
178    /// Unhides remote printers.
179    pub fn unhide_remote_printers(&self) {
180        // SAFETY: pointer is non-null.
181        unsafe { ffi::cpdbUnhideRemotePrinters(self.raw.as_ptr()) };
182    }
183
184    /// Hides temporary printers from subsequent listings.
185    pub fn hide_temporary_printers(&self) {
186        // SAFETY: pointer is non-null.
187        unsafe { ffi::cpdbHideTemporaryPrinters(self.raw.as_ptr()) };
188    }
189
190    /// Unhides temporary printers.
191    pub fn unhide_temporary_printers(&self) {
192        // SAFETY: pointer is non-null.
193        unsafe { ffi::cpdbUnhideTemporaryPrinters(self.raw.as_ptr()) };
194    }
195
196    // ─── Lookup ──────────────────────────────────────────────────────────────
197
198    /// Finds a printer by `(id, backend)`.
199    ///
200    /// The returned [`Printer`] borrows from `self`.
201    pub fn find_printer<'f>(&'f self, printer_id: &str, backend_name: &str) -> Result<Printer<'f>> {
202        let c_id = CString::new(printer_id)?;
203        let c_backend = CString::new(backend_name)?;
204        // SAFETY: pointers are non-null; the CStrings outlive the call.
205        let raw = unsafe {
206            ffi::cpdbFindPrinterObj(self.raw.as_ptr(), c_id.as_ptr(), c_backend.as_ptr())
207        };
208        if raw.is_null() {
209            Err(CpdbError::NotFound(format!(
210                "printer '{printer_id}' on backend '{backend_name}'"
211            )))
212        } else {
213            Printer::from_raw_borrowed(raw)
214        }
215    }
216
217    /// Returns the user-default printer, if one is set.
218    pub fn get_default_printer(&self) -> Result<Printer<'_>> {
219        // SAFETY: pointer is non-null.
220        let raw = unsafe { ffi::cpdbGetDefaultPrinter(self.raw.as_ptr()) };
221        if raw.is_null() {
222            Err(CpdbError::NotFound("default printer".into()))
223        } else {
224            Printer::from_raw_borrowed(raw)
225        }
226    }
227
228    /// Returns the default printer for a specific backend, if one is set.
229    pub fn get_default_printer_for_backend(&self, backend_name: &str) -> Result<Printer<'_>> {
230        let c_backend = CString::new(backend_name)?;
231        // SAFETY: pointer is non-null; the CString outlives the call.
232        let raw =
233            unsafe { ffi::cpdbGetDefaultPrinterForBackend(self.raw.as_ptr(), c_backend.as_ptr()) };
234        if raw.is_null() {
235            Err(CpdbError::NotFound(format!(
236                "default printer for backend '{backend_name}'"
237            )))
238        } else {
239            Printer::from_raw_borrowed(raw)
240        }
241    }
242
243    /// Asks every backend to refresh its printer list.
244    pub fn refresh_printers(&self) {
245        // SAFETY: pointer is non-null.
246        unsafe { ffi::cpdbGetAllPrinters(self.raw.as_ptr()) };
247    }
248
249    /// Adds an owned printer to the frontend's table.
250    ///
251    /// Takes ownership of the printer — cpdb-libs becomes responsible for
252    /// freeing it. The argument must be an owned [`Printer`] (e.g. from
253    /// [`Printer::load_from_file`]); attempting to insert a borrowed
254    /// printer is rejected to prevent the same pointer ending up in two
255    /// hash tables.
256    ///
257    /// Returns `Ok(true)` when the printer was inserted; `Ok(false)` when
258    /// it was rejected by the backend.
259    ///
260    /// [`Printer::load_from_file`]: crate::Printer::load_from_file
261    pub fn add_printer(&self, printer: Printer<'static>) -> Result<bool> {
262        if !printer.is_owned() {
263            return Err(CpdbError::PrinterError(
264                "add_printer requires an owned Printer".into(),
265            ));
266        }
267        // SAFETY: ownership of the raw pointer is transferred to cpdb-libs
268        // by forgetting the Rust `Printer` (so its Drop does not run).
269        let raw = printer.as_raw();
270        std::mem::forget(printer);
271        let added = unsafe { ffi::cpdbAddPrinter(self.raw.as_ptr(), raw) };
272        Ok(added != 0)
273    }
274
275    /// Removes a printer from the frontend's table by `(id, backend)`.
276    ///
277    /// Returns the *owned* removed printer when present, so the caller can
278    /// inspect it before dropping (cpdb-libs hands ownership back).
279    pub fn remove_printer(
280        &self,
281        printer_id: &str,
282        backend_name: &str,
283    ) -> Result<Option<Printer<'static>>> {
284        let c_id = CString::new(printer_id)?;
285        let c_backend = CString::new(backend_name)?;
286        // SAFETY: pointers are non-null; the CStrings outlive the call.
287        let raw =
288            unsafe { ffi::cpdbRemovePrinter(self.raw.as_ptr(), c_id.as_ptr(), c_backend.as_ptr()) };
289        if raw.is_null() {
290            Ok(None)
291        } else {
292            Printer::from_raw_owned(raw).map(Some)
293        }
294    }
295
296    /// Asks a specific backend to refresh its printer list. Returns `true` on success.
297    pub fn refresh_printer_list(&self, backend_name: &str) -> Result<bool> {
298        let c_backend = CString::new(backend_name)?;
299        // SAFETY: pointers are non-null; the CString outlives the call.
300        let ok = unsafe { ffi::cpdbRefreshPrinterList(self.raw.as_ptr(), c_backend.as_ptr()) };
301        Ok(ok)
302    }
303
304    /// Returns every printer currently known by walking the internal hash table.
305    ///
306    /// The returned printers borrow from `self`.
307    pub fn get_printers(&self) -> Result<Vec<Printer<'_>>> {
308        // SAFETY: dereferencing the printer table field is sound; we only
309        // read borrowed pointers and never write through them.
310        let table = unsafe { (*self.raw.as_ptr()).printer } as *mut glib_sys::GHashTable;
311        if table.is_null() {
312            return Ok(Vec::new());
313        }
314
315        let mut printers: Vec<Printer<'_>> = Vec::new();
316        // SAFETY: iterator is initialised on the stack and iterated
317        // synchronously; the table is not mutated during this loop.
318        unsafe {
319            let mut iter = MaybeUninit::<glib_sys::GHashTableIter>::uninit();
320            glib_sys::g_hash_table_iter_init(iter.as_mut_ptr(), table);
321            let mut iter = iter.assume_init();
322
323            let mut key: glib_sys::gpointer = std::ptr::null_mut();
324            let mut value: glib_sys::gpointer = std::ptr::null_mut();
325            while glib_sys::g_hash_table_iter_next(&mut iter, &mut key, &mut value) != 0 {
326                let raw = value as *mut ffi::cpdb_printer_obj_t;
327                if let Ok(p) = Printer::from_raw_borrowed(raw) {
328                    printers.push(p);
329                }
330            }
331        }
332        Ok(printers)
333    }
334
335    /// Looks up the first printer whose `name` field equals the argument.
336    ///
337    /// When multiple printers share a name across backends, the first one
338    /// encountered during hash-table iteration wins. Prefer
339    /// [`Frontend::find_printer`] when you can supply a backend name.
340    pub fn get_printer<'f>(&'f self, name: &str) -> Result<Printer<'f>> {
341        // SAFETY: dereferencing the printer table field is sound.
342        let table = unsafe { (*self.raw.as_ptr()).printer } as *mut glib_sys::GHashTable;
343        if table.is_null() {
344            return Err(CpdbError::NotFound(format!("printer '{name}'")));
345        }
346        let needle = name.as_bytes();
347
348        // SAFETY: see `get_printers`.
349        unsafe {
350            let mut iter = MaybeUninit::<glib_sys::GHashTableIter>::uninit();
351            glib_sys::g_hash_table_iter_init(iter.as_mut_ptr(), table);
352            let mut iter = iter.assume_init();
353
354            let mut key: glib_sys::gpointer = std::ptr::null_mut();
355            let mut value: glib_sys::gpointer = std::ptr::null_mut();
356            while glib_sys::g_hash_table_iter_next(&mut iter, &mut key, &mut value) != 0 {
357                let raw = value as *mut ffi::cpdb_printer_obj_t;
358                if raw.is_null() {
359                    continue;
360                }
361                let name_ptr = (*raw).name;
362                if name_ptr.is_null() {
363                    continue;
364                }
365                if CStr::from_ptr(name_ptr).to_bytes() == needle {
366                    return Printer::from_raw_borrowed(raw);
367                }
368            }
369        }
370        Err(CpdbError::NotFound(format!("printer '{name}'")))
371    }
372}
373
374impl Drop for Frontend {
375    fn drop(&mut self) {
376        // Unregister any observer FIRST so an in-flight callback from
377        // cpdb-libs' D-Bus thread finds an empty slot and bails out
378        // instead of touching a half-freed object.
379        callbacks::unregister_printer_observer(self.raw.as_ptr());
380        // SAFETY: we own the pointer.
381        unsafe { ffi::cpdbDeleteFrontendObj(self.raw.as_ptr()) };
382    }
383}